diff --git a/src/nvm.go b/src/nvm.go index 9466d4ac675a493270249573f41f60edc7d9ae77..c1775accc83e8b5e352362f8c56efad74356b81c 100644 --- a/src/nvm.go +++ b/src/nvm.go @@ -11,21 +11,31 @@ import ( "io/ioutil" "regexp" "bytes" + "encoding/json" + "archive/zip" + "log" + "bufio" ) var root = "" +var symlink = "" func main() { args := os.Args detail := "" - setRootDir(filepath.Dir(args[0])) + setRootDir() // Capture any additional arguments if (len(args) > 2) { detail = strings.ToLower(args[2]) } + if (len(args) < 2){ + help() + return + } + // Run the appropriate method switch args[1] { case "install": install(detail) @@ -58,27 +68,47 @@ func install(version string) { // Check to see if the version is already installed if !isVersionInstalled(version) { - // If the version does not exist, download it to the temp directory. - success := download(version); + // Make the output directories + os.Mkdir(root+"\\v"+version,os.ModeDir) + os.Mkdir(root+"\\v"+version+"\\node_modules",os.ModeDir) + + // Download node + success := downloadNodeJs(version); - // Run the installer + // If successful, add npm if success { - fmt.Printf("Installing v"+version+"... ") - os.Mkdir(root+"\\v"+version,os.ModeDir) - cmd := exec.Command("msiexec.exe","/i",os.TempDir()+"\\"+"node-v"+version+".msi","INSTALLDIR="+root+"\\v"+version,"/qb") - err := cmd.Run() - if err != nil { - fmt.Println("ERROR") - fmt.Print(err) - os.Exit(1) + npmv := getNpmVersion(version) + success = downloadNpm(getNpmVersion(version)) + if success { + fmt.Printf("Installing npm v"+npmv+"...") + + // Extract npm to the temp directory + unzip(os.TempDir()+"\\npm-v"+npmv+".zip",os.TempDir()+"\\nvm-npm") + + // Copy the npm and npm.cmd files to the installation directory + os.Rename(os.TempDir()+"\\nvm-npm\\npm-"+npmv+"\\bin\\npm",root+"\\v"+version+"\\npm") + os.Rename(os.TempDir()+"\\nvm-npm\\npm-"+npmv+"\\bin\\npm.cmd",root+"\\v"+version+"\\npm.cmd") + os.Rename(os.TempDir()+"\\nvm-npm\\npm-"+npmv,root+"\\v"+version+"\\node_modules\\npm") + + // Remove the source file + os.RemoveAll(os.TempDir()+"\\nvm-npm") + + fmt.Printf(" done.") + fmt.Println("\n\nInstallation complete. If you want to use this version, type\n\nnvm use "+version) + } else { + fmt.Println("Could not download npm for node v"+version+".") + fmt.Println("Please visit https://github.com/npm/npm/releases/tag/v"+npmv+" to download npm.") + fmt.Println("It should be extracted to "+root+"\\v"+version) } - fmt.Printf("done.") + } else { + fmt.Println("Could not download node.js executable for version "+version+".") } + return - // Clean up - fmt.Printf("\nCleaning up... ") - os.Remove(os.TempDir()+"\\"+"node-v"+version+".msi") - fmt.Printf("done.\n") + // Move node and npm to their directory, then update the symlink + // Remember to set the symlink path in the PATH during the installation + // If this is ever shipped for Mac, it should use homebrew. + // If this ever ships on Linux, it should be on bintray so it can use yum, apt-get, etc. return } else { @@ -98,7 +128,7 @@ func uninstall(version string) { // Determine if the version exists and skip if it doesn't if isVersionInstalled(version) { - fmt.Printf("\nUninstalling node v"+version+"...") + fmt.Printf("Uninstalling node v"+version+"...") e := os.RemoveAll(root+"\\v"+version) if e != nil { fmt.Println("Error removing node v"+version) @@ -118,8 +148,23 @@ func use(version string) { return } - // Create the symlink - c := exec.Command(root+"\\elevate.cmd", "cmd", "/C", "mklink", "/D", root+"\\action", root+"\\v"+version) + // Create or update the symlink + sym, serr := os.Stat(symlink) + sym = sym + if serr == nil { + cmd := exec.Command(root+"\\elevate.cmd", "cmd", "/C", "rmdir", symlink) + var output bytes.Buffer + var _stderr bytes.Buffer + cmd.Stdout = &output + cmd.Stderr = &_stderr + perr := cmd.Run() + if perr != nil { + fmt.Println(fmt.Sprint(perr) + ": " + _stderr.String()) + return + } + } + + c := exec.Command(root+"\\elevate.cmd", "cmd", "/C", "mklink", "/D", symlink, root+"\\v"+version) var out bytes.Buffer var stderr bytes.Buffer c.Stdout = &out @@ -137,7 +182,7 @@ func list(listtype string) { listtype = "installed" } if listtype != "installed" && listtype != "available" { - fmt.Println("\nInvalid list option.\n\nPlease use on of the following\n - wnvm list\n - wnvm list installed\n - wnvm list available") + fmt.Println("\nInvalid list option.\n\nPlease use on of the following\n - nvm list\n - nvm list installed\n - nvm list available") help() return } @@ -154,28 +199,18 @@ func disable() { fmt.Printf("Disable by removing the symlink in PATH var") } -func setRootDir(path string) { - // Prompt user, warning them what they're going to do - rootdir, err := filepath.Abs(filepath.Dir(path+"\\")) - if err != nil { - fmt.Println("Error setting root directory") - os.Exit(1) - } - root = rootdir - fmt.Println("\nSet the root path to "+root) -} - func help() { fmt.Println("\nUsage:\n") fmt.Println(" nvm install : The version can be a node.js version or \"latest\" for the latest stable version.") fmt.Println(" nvm uninstall : The version must be a specific version.") fmt.Println(" nvm use : Switch to use the specified version.") fmt.Println(" nvm list [type] : type can be \"available\" (from nodejs.org),") - fmt.Println(" \"installed\" (what is currently on the computer),") - fmt.Println(" or left blank (same as \"installed\").") - fmt.Println(" nvm enable : Enable node.js version management.") - fmt.Println(" nvm disable : Disable node.js version management.") - fmt.Println(" nvm root : Set the directory where wnvm should install different node.js versions.\n") + fmt.Println(" \"installed\" (what is currently on the computer),") + fmt.Println(" or left blank (same as \"installed\").") + fmt.Println(" nvm on : Enable node.js version management.") + fmt.Println(" nvm off : Disable node.js version management.") + fmt.Println(" nvm root : Set the directory where wnvm should install different node.js versions.") + fmt.Println(" If is not set, the current root will be displayed.\n") } func getRemoteTextFile(url string) string { @@ -197,13 +232,61 @@ func getRemoteTextFile(url string) string { return "" } -// Download an MSI to the temp directory -func download(v string) bool { +// Given a node.js version, returns the associated npm version +func getNpmVersion(nodeversion string) string { + + // Get raw text + text := getRemoteTextFile("https://raw.githubusercontent.com/coreybutler/nodedistro/master/nodeversions.json") + + // Parse + var data interface{} + json.Unmarshal([]byte(text), &data); + body := data.(map[string]interface{}) + all := body["all"] + npm := all.(map[string]interface{}) + + return npm[nodeversion].(string) +} + +func downloadNodeJs(v string) bool { - url := "http://nodejs.org/dist/v"+v+"/node-v"+v+"-x86.msi" - fileName := os.TempDir()+"\\"+"node-v"+v+".msi" + url := "http://nodejs.org/dist/v"+v+"/node.exe" + fileName := root+"\\v"+v+"\\node.exe" - fmt.Printf("\nDownloading node.js version "+v+"... ") + fmt.Printf("Downloading node.js version "+v+"... ") + + output, err := os.Create(fileName) + if err != nil { + fmt.Println("Error while creating", fileName, "-", err) + } + defer output.Close() + + response, err := http.Get(url) + if err != nil { + fmt.Println("Error while downloading", url, "-", err) + } + defer response.Body.Close() + + n, err := io.Copy(output, response.Body) + if err != nil { + fmt.Println("Error while downloading", url, "-", err) + } + + if response.Status[0:3] == "200" { + fmt.Println(n, "bytes downloaded.") + } else { + fmt.Println("ERROR") + } + + return response.Status[0:3] == "200" +} + +func downloadNpm(v string) bool { + + url := "https://github.com/npm/npm/archive/v"+v+".zip" + fileName := os.TempDir()+"\\"+"npm-v"+v+".zip" + + fmt.Printf("Downloading npm version "+v+"... ") output, err := os.Create(fileName) if err != nil { @@ -236,3 +319,79 @@ func isVersionInstalled(version string) bool { src = src return err == nil } + +// Function courtesy http://stackoverflow.com/users/1129149/swtdrgn +func unzip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + rc, err := f.Open() + if err != nil { + return err + } + defer rc.Close() + + fpath := filepath.Join(dest, f.Name) + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, f.Mode()) + } else { + var fdir string + if lastIndex := strings.LastIndex(fpath,string(os.PathSeparator)); lastIndex > -1 { + fdir = fpath[:lastIndex] + } + + err = os.MkdirAll(fdir, f.Mode()) + if err != nil { + log.Fatal(err) + return err + } + f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(f, rc) + if err != nil { + return err + } + } + } + return nil +} + +func readLines(path string) ([]string, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + return lines, scanner.Err() +} + +func setRootDir() { + lines, err := readLines(os.Getenv("APPDATA")+"\\nvm\\settings.txt") + if err != nil { + fmt.Println("\nERROR",err) + os.Exit(1) + } + + // Process each line and extract the value + for _, line := range lines { + if strings.Contains(line,"root:") { + root = strings.Trim(regexp.MustCompile("root:").ReplaceAllString(line,"")," ") + } else if strings.Contains(line,"path:") { + symlink = strings.Trim(regexp.MustCompile("path:").ReplaceAllString(line,"")," ") + } + } +}