Files
nvm-windows/src/nvm.go

763 lines
24 KiB
Go

package main
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"./nvm/web"
"./nvm/arch"
"./nvm/file"
"./nvm/node"
"github.com/olekukonko/tablewriter"
)
const (
NvmVersion = "1.1.8"
)
type Environment struct {
settings string
root string
symlink string
arch string
node_mirror string
npm_mirror string
proxy string
originalpath string
originalversion string
verifyssl bool
}
var home = filepath.Clean(os.Getenv("NVM_HOME")+"\\settings.txt")
var symlink = filepath.Clean(os.Getenv("NVM_SYMLINK"))
var env = &Environment{
settings: home,
root: "",
symlink: symlink,
arch: os.Getenv("PROCESSOR_ARCHITECTURE"),
node_mirror: "",
npm_mirror: "",
proxy: "none",
originalpath: "",
originalversion: "",
verifyssl: true,
}
func main() {
args := os.Args
detail := ""
procarch := arch.Validate(env.arch)
setup()
// Capture any additional arguments
if len(args) > 2 {
detail = args[2]
}
if len(args) > 3 {
if (args[3] == "32" || args[3] == "64") {
procarch = args[3]
}
}
if len(args) < 2 {
help()
return
}
// Run the appropriate method
switch args[1] {
case "install": install(detail,procarch)
case "uninstall": uninstall(detail)
case "use": use(detail,procarch)
case "list": list(detail)
case "ls": list(detail)
case "on": enable()
case "off": disable()
case "root":
if len(args) == 3 {
updateRootDir(args[2])
} else {
fmt.Println("\nCurrent Root: "+env.root)
}
case "version":
fmt.Println(NvmVersion)
case "v":
fmt.Println(NvmVersion)
case "arch":
if strings.Trim(detail," \r\n") != "" {
detail = strings.Trim(detail," \r\n")
if detail != "32" && detail != "64" {
fmt.Println("\""+detail+"\" is an invalid architecture. Use 32 or 64.")
return
}
env.arch = detail
saveSettings()
fmt.Println("Default architecture set to "+detail+"-bit.")
return
}
_, a := node.GetCurrentVersion()
fmt.Println("System Default: "+env.arch+"-bit.")
fmt.Println("Currently Configured: "+a+"-bit.")
case "proxy":
if detail == "" {
fmt.Println("Current proxy: "+env.proxy)
} else {
env.proxy = detail
saveSettings()
}
//case "update": update()
case "node_mirror": setNodeMirror(detail)
case "npm_mirror": setNpmMirror(detail)
default: help()
}
}
// ===============================================================
// BEGIN | CLI functions
// ===============================================================
func setNodeMirror(uri string) {
env.node_mirror = uri
saveSettings()
}
func setNpmMirror(uri string) {
env.npm_mirror = uri
saveSettings()
}
/*
func update() {
cmd := exec.Command("cmd", "/d", "echo", "testing")
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
}
}
*/
func install(version string, cpuarch string) {
args := os.Args
lastarg := args[len(args) - 1]
if lastarg == "--insecure" {
env.verifyssl = false
}
if version == "" {
fmt.Println("\nInvalid version.")
fmt.Println(" ")
help()
return
}
cpuarch = strings.ToLower(cpuarch)
if cpuarch != "" {
if cpuarch != "32" && cpuarch != "64" && cpuarch != "all" {
fmt.Println("\""+cpuarch+"\" is not a valid CPU architecture. Must be 32 or 64.")
return
}
} else {
cpuarch = env.arch
}
if cpuarch != "all" {
cpuarch = arch.Validate(cpuarch)
}
// If user specifies "latest" version, find out what version is
if version == "latest" {
url := web.GetFullNodeUrl("latest/SHASUMS256.txt");
content := web.GetRemoteTextFile(url)
re := regexp.MustCompile("node-v(.+)+msi")
reg := regexp.MustCompile("node-v|-x.+")
version = reg.ReplaceAllString(re.FindString(content),"")
}
// if the user specifies only the major version number then install the latest
// version of the major version number
if len(version) == 1 {
version = findLatestSubVersion(version)
} else {
version = cleanVersion(version)
}
if checkVersionExceedsLatest(version) {
fmt.Println("Node.js v"+version+" is not yet released or available.")
return
}
if cpuarch == "64" && !web.IsNode64bitAvailable(version) {
fmt.Println("Node.js v"+version+" is only available in 32-bit.")
return
}
// Check to see if the version is already installed
if !node.IsVersionInstalled(env.root,version,cpuarch) {
if !node.IsVersionAvailable(version){
url := web.GetFullNodeUrl("index.json")
fmt.Println("\nVersion "+version+" is not available.\n\nThe complete list of available versions can be found at " + url)
return
}
// Make the output directories
os.Mkdir(filepath.Join(env.root, "v"+version), os.ModeDir)
os.Mkdir(filepath.Join(env.root, "v"+version, "node_modules"), os.ModeDir)
// Warn the user if they're attempting to install without verifying the remote SSL cert
if !env.verifyssl {
fmt.Println("\nWARNING: The remote SSL certificate will not be validated during the download process.\n")
}
// Download node
if (cpuarch == "32" || cpuarch == "all") && !node.IsVersionInstalled(env.root,version,"32") {
success := web.GetNodeJS(env.root,version,"32");
if !success {
os.RemoveAll(filepath.Join(env.root, "v"+version, "node_modules"))
fmt.Println("Could not download node.js v"+version+" 32-bit executable.")
return
}
}
if (cpuarch == "64" || cpuarch == "all") && !node.IsVersionInstalled(env.root,version,"64") {
success := web.GetNodeJS(env.root,version,"64");
if !success {
os.RemoveAll(filepath.Join(env.root, "v"+version, "node_modules"))
fmt.Println("Could not download node.js v"+version+" 64-bit executable.")
return
}
}
if file.Exists(filepath.Join(env.root, "v"+version, "node_modules", "npm")) {
return
}
// If successful, add npm
npmv := getNpmVersion(version)
success := web.GetNpm(env.root, getNpmVersion(version))
if success {
fmt.Printf("Installing npm v"+npmv+"...")
// new temp directory under the nvm root
tempDir := filepath.Join(env.root, "temp")
// Extract npm to the temp directory
err := file.Unzip(filepath.Join(tempDir, "npm-v"+npmv+".zip"), filepath.Join(tempDir, "nvm-npm"))
// Copy the npm and npm.cmd files to the installation directory
tempNpmBin := filepath.Join(tempDir, "nvm-npm", "cli-"+npmv, "bin")
// Support npm < 6.2.0
if file.Exists(tempNpmBin) == false {
tempNpmBin = filepath.Join(tempDir, "nvm-npm", "npm-"+npmv, "bin")
}
if file.Exists(tempNpmBin) == false {
log.Fatal("Failed to extract npm. Could not find " + tempNpmBin)
}
// Standard npm support
os.Rename(filepath.Join(tempNpmBin, "npm"), filepath.Join(env.root, "v"+version, "npm"))
os.Rename(filepath.Join(tempNpmBin, "npm.cmd"),filepath.Join(env.root, "v"+version, "npm.cmd"))
// npx support
if _, err := os.Stat(filepath.Join(tempNpmBin, "npx")); err == nil {
os.Rename(filepath.Join(tempNpmBin, "npx"), filepath.Join(env.root, "v"+version, "npx"))
os.Rename(filepath.Join(tempNpmBin, "npx.cmd"), filepath.Join(env.root, "v"+version, "npx.cmd"))
}
npmSourcePath := filepath.Join(tempDir, "nvm-npm", "npm-"+npmv)
if file.Exists(npmSourcePath) == false {
npmSourcePath = filepath.Join(tempDir, "nvm-npm", "cli-"+npmv)
}
moveNpmErr := os.Rename(npmSourcePath, filepath.Join(env.root, "v"+version, "node_modules", "npm"))
if moveNpmErr != nil {
// sometimes Windows can take some time to enable access to large amounts of files after unzip, use exponential backoff to wait until it is ready
for _, i := range [5]int{1, 2, 4, 8, 16} {
time.Sleep(time.Duration(i)*time.Second)
moveNpmErr = os.Rename(npmSourcePath, filepath.Join(env.root, "v"+version, "node_modules", "npm"))
if moveNpmErr == nil { break }
}
}
if err == nil && moveNpmErr == nil {
// Remove the temp directory
// may consider keep the temp files here
os.RemoveAll(tempDir)
fmt.Println("\n\nInstallation complete. If you want to use this version, type\n\nnvm use "+version)
} else if moveNpmErr != nil {
fmt.Println("Error: Unable to move directory "+npmSourcePath+" to node_modules: "+moveNpmErr.Error())
} else {
fmt.Println("Error: Unable to install NPM: "+err.Error());
}
} else {
fmt.Println("Could not download npm for node v"+version+".")
fmt.Println("Please visit https://github.com/npm/cli/releases/tag/v"+npmv+" to download npm.")
fmt.Println("It should be extracted to "+env.root+"\\v"+version)
}
// Reset the SSL verification
env.verifyssl = true
// 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 {
fmt.Println("Version "+version+" is already installed.")
return
}
}
func uninstall(version string) {
// Make sure a version is specified
if len(version) == 0 {
fmt.Println("Provide the version you want to uninstall.")
help()
return
}
version = cleanVersion(version)
// Determine if the version exists and skip if it doesn't
if node.IsVersionInstalled(env.root,version,"32") || node.IsVersionInstalled(env.root,version,"64") {
fmt.Printf("Uninstalling node v"+version+"...")
v, _ := node.GetCurrentVersion()
if v == version {
runElevated(fmt.Sprintf(`"%s" cmd /C rmdir "%s"`,
filepath.Join(env.root, "elevate.cmd"),
filepath.Clean(env.symlink)))
}
e := os.RemoveAll(filepath.Join(env.root, "v"+version))
if e != nil {
fmt.Println("Error removing node v"+version)
fmt.Println("Manually remove " + filepath.Join(env.root, "v"+version) + ".")
} else {
fmt.Printf(" done")
}
} else {
fmt.Println("node v"+version+" is not installed. Type \"nvm list\" to see what is installed.")
}
return
}
func findLatestSubVersion(version string) string {
url := web.GetFullNodeUrl("latest-v" + version + ".x" + "/SHASUMS256.txt")
content := web.GetRemoteTextFile(url)
re := regexp.MustCompile("node-v(.+)+msi")
reg := regexp.MustCompile("node-v|-x.+")
latest := reg.ReplaceAllString(re.FindString(content), "")
return latest
}
func use(version string, cpuarch string) {
if version == "32" || version == "64" {
cpuarch = version
v, _ := node.GetCurrentVersion()
version = v
}
cpuarch = arch.Validate(cpuarch)
version = cleanVersion(version)
// Make sure the version is installed. If not, warn.
if !node.IsVersionInstalled(env.root,version,cpuarch) {
fmt.Println("node v"+version+" ("+cpuarch+"-bit) is not installed.")
if cpuarch == "32" {
if node.IsVersionInstalled(env.root,version,"64") {
fmt.Println("\nDid you mean node v"+version+" (64-bit)?\nIf so, type \"nvm use "+version+" 64\" to use it.")
}
}
if cpuarch == "64" {
if node.IsVersionInstalled(env.root,version,"32") {
fmt.Println("\nDid you mean node v"+version+" (32-bit)?\nIf so, type \"nvm use "+version+" 32\" to use it.")
}
}
return
}
// Remove symlink if it already exists
sym, _ := os.Stat(env.symlink)
if sym != nil {
if !runElevated(fmt.Sprintf(`"%s" cmd /C rmdir "%s"`,
filepath.Join(env.root, "elevate.cmd"),
filepath.Clean(env.symlink))) {
return
}
}
// Create new symlink
if !runElevated(fmt.Sprintf(`"%s" cmd /C mklink /D "%s" "%s"`,
filepath.Join(env.root, "elevate.cmd"),
filepath.Clean(env.symlink),
filepath.Join(env.root, "v"+version))) {
return
}
// Use the assigned CPU architecture
cpuarch = arch.Validate(cpuarch)
nodepath := filepath.Join(env.root, "v"+version, "node.exe")
node32path := filepath.Join(env.root, "v"+version, "node32.exe")
node64path := filepath.Join(env.root, "v"+version, "node64.exe")
node32exists := file.Exists(node32path)
node64exists := file.Exists(node64path)
nodeexists := file.Exists(nodepath)
if node32exists && cpuarch == "32" { // user wants 32, but node.exe is 64
if nodeexists {
os.Rename(nodepath, node64path) // node.exe -> node64.exe
}
os.Rename(node32path, nodepath) // node32.exe -> node.exe
}
if node64exists && cpuarch == "64" { // user wants 64, but node.exe is 32
if nodeexists {
os.Rename(nodepath, node32path) // node.exe -> node32.exe
}
os.Rename(node64path, nodepath) // node64.exe -> node.exe
}
fmt.Println("Now using node v"+version+" ("+cpuarch+"-bit)")
}
func useArchitecture(a string) {
if strings.ContainsAny("32",os.Getenv("PROCESSOR_ARCHITECTURE")) {
fmt.Println("This computer only supports 32-bit processing.")
return
}
if a == "32" || a == "64" {
env.arch = a
saveSettings()
fmt.Println("Set to "+a+"-bit mode")
} else {
fmt.Println("Cannot set architecture to "+a+". Must be 32 or 64 are acceptable values.")
}
}
func list(listtype string) {
if listtype == "" {
listtype = "installed"
}
if listtype != "installed" && listtype != "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
}
if listtype == "installed" {
fmt.Println("")
inuse, a := node.GetCurrentVersion()
v := node.GetInstalled(env.root)
for i := 0; i < len(v); i++ {
version := v[i]
isnode, _ := regexp.MatchString("v",version)
str := ""
if isnode {
if "v"+inuse == version {
str = str+" * "
} else {
str = str+" "
}
str = str+regexp.MustCompile("v").ReplaceAllString(version,"")
if "v"+inuse == version {
str = str+" (Currently using "+a+"-bit executable)"
// str = ansi.Color(str,"green:black")
}
fmt.Printf(str+"\n")
}
}
if len(v) == 0 {
fmt.Println("No installations recognized.")
}
} else {
_, lts, current, stable, unstable, _ := node.GetAvailable()
releases := 20
data := make([][]string, releases, releases + 5)
for i := 0; i < releases; i++ {
release := make([]string, 4, 6)
release[0] = ""
release[1] = ""
release[2] = ""
release[3] = ""
if len(current) > i {
if len(current[i]) > 0 {
release[0] = current[i]
}
}
if len(lts) > i {
if len(lts[i]) > 0 {
release[1] = lts[i]
}
}
if len(stable) > i {
if len(stable[i]) > 0 {
release[2] = stable[i]
}
}
if len(unstable) > i {
if len(unstable[i]) > 0 {
release[3] = unstable[i]
}
}
data[i] = release
}
fmt.Println("")
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{" Current ", " LTS ", " Old Stable ", "Old Unstable"})
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
table.SetAlignment(tablewriter.ALIGN_CENTER)
table.SetCenterSeparator("|")
table.AppendBulk(data) // Add Bulk Data
table.Render()
fmt.Println("\nThis is a partial list. For a complete list, visit https://nodejs.org/download/release")
}
}
func enable() {
dir := ""
files, _ := ioutil.ReadDir(env.root)
for _, f := range files {
if f.IsDir() {
isnode, _ := regexp.MatchString("v",f.Name())
if isnode {
dir = f.Name()
}
}
}
fmt.Println("nvm enabled")
if dir != "" {
use(strings.Trim(regexp.MustCompile("v").ReplaceAllString(dir,"")," \n\r"),env.arch)
} else {
fmt.Println("No versions of node.js found. Try installing the latest by typing nvm install latest")
}
}
func disable() {
if !runElevated(fmt.Sprintf(`"%s" cmd /C rmdir "%s"`,
filepath.Join(env.root, "elevate.cmd"),
filepath.Clean(env.symlink))) {
return
}
fmt.Println("nvm disabled")
}
func help() {
fmt.Println("\nRunning version "+NvmVersion+".")
fmt.Println("\nUsage:")
fmt.Println(" ")
fmt.Println(" nvm arch : Show if node is running in 32 or 64 bit mode.")
fmt.Println(" nvm install <version> [arch] : The version can be a node.js version or \"latest\" for the latest stable version.")
fmt.Println(" Optionally specify whether to install the 32 or 64 bit version (defaults to system arch).")
fmt.Println(" Set [arch] to \"all\" to install 32 AND 64 bit versions.")
fmt.Println(" Add --insecure to the end of this command to bypass SSL validation of the remote download server.")
fmt.Println(" nvm list [available] : List the node.js installations. Type \"available\" at the end to see what can be installed. Aliased as ls.")
fmt.Println(" nvm on : Enable node.js version management.")
fmt.Println(" nvm off : Disable node.js version management.")
fmt.Println(" nvm proxy [url] : Set a proxy to use for downloads. Leave [url] blank to see the current proxy.")
fmt.Println(" Set [url] to \"none\" to remove the proxy.")
fmt.Println(" nvm node_mirror [url] : Set the node mirror. Defaults to https://nodejs.org/dist/. Leave [url] blank to use default url.")
fmt.Println(" nvm npm_mirror [url] : Set the npm mirror. Defaults to https://github.com/npm/cli/archive/. Leave [url] blank to default url.")
fmt.Println(" nvm uninstall <version> : The version must be a specific version.")
// fmt.Println(" nvm update : Automatically update nvm to the latest version.")
fmt.Println(" nvm use [version] [arch] : Switch to use the specified version. Optionally specify 32/64bit architecture.")
fmt.Println(" nvm use <arch> will continue using the selected version, but switch to 32/64 bit mode.")
fmt.Println(" nvm root [path] : Set the directory where nvm should store different versions of node.js.")
fmt.Println(" If <path> is not set, the current root will be displayed.")
fmt.Println(" nvm version : Displays the current running version of nvm for Windows. Aliased as v.")
fmt.Println(" ")
}
// ===============================================================
// END | CLI functions
// ===============================================================
// ===============================================================
// BEGIN | Utility functions
// ===============================================================
func checkVersionExceedsLatest(version string) bool{
//content := web.GetRemoteTextFile("http://nodejs.org/dist/latest/SHASUMS256.txt")
url := web.GetFullNodeUrl("latest/SHASUMS256.txt");
content := web.GetRemoteTextFile(url)
re := regexp.MustCompile("node-v(.+)+msi")
reg := regexp.MustCompile("node-v|-x.+")
latest := reg.ReplaceAllString(re.FindString(content),"")
var vArr = strings.Split(version,".")
var lArr = strings.Split(latest, ".")
for index := range lArr {
lat,_ := strconv.Atoi(lArr[index])
ver,_ := strconv.Atoi(vArr[index])
//Should check for valid input (checking for conversion errors) but this tool is made to trust the user
if ver < lat {
return false
} else if ver > lat {
return true
}
}
return false
}
func cleanVersion(version string) string {
re := regexp.MustCompile("\\d+.\\d+.\\d+")
matched := re.FindString(version)
if len(matched) == 0 {
re = regexp.MustCompile("\\d+.\\d+")
matched = re.FindString(version)
if len(matched) == 0 {
matched = version + ".0.0"
} else {
matched = matched + ".0"
}
fmt.Println(matched)
}
return matched
}
// Given a node.js version, returns the associated npm version
func getNpmVersion(nodeversion string) string {
_, _, _, _, _, npm := node.GetAvailable()
return npm[nodeversion]
}
func updateRootDir(path string) {
_, err := os.Stat(path)
if err != nil {
fmt.Println(path+" does not exist or could not be found.")
return
}
currentRoot := env.root
env.root = filepath.Clean(path)
// Copy command files
os.Link(filepath.Clean(currentRoot + "/elevate.cmd"), filepath.Clean(env.root + "/elevate.cmd"))
os.Link(filepath.Clean(currentRoot + "/elevate.cmd"), filepath.Clean(env.root + "/elevate.vbs"))
saveSettings()
if currentRoot != env.root {
fmt.Println("\nRoot has been changed from " + currentRoot + " to " + path)
}
}
func runElevated(command string) bool {
c := exec.Command("cmd") // dummy executable that actually needs to exist but we'll overwrite using .SysProcAttr
// Based on the official docs, syscall.SysProcAttr.CmdLine doesn't exist.
// But it does and is vital:
// https://github.com/golang/go/issues/15566#issuecomment-333274825
// https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773
c.SysProcAttr = &syscall.SysProcAttr{CmdLine: command}
var stderr bytes.Buffer
c.Stderr = &stderr
err := c.Run()
if err != nil {
fmt.Println(fmt.Sprint(err) + ": " + stderr.String())
return false
}
return true
}
func saveSettings() {
content := "root: " + strings.Trim(env.root, " \n\r") + "\r\narch: " + strings.Trim(env.arch, " \n\r") + "\r\nproxy: " + strings.Trim(env.proxy, " \n\r") + "\r\noriginalpath: " + strings.Trim(env.originalpath, " \n\r") + "\r\noriginalversion: " + strings.Trim(env.originalversion, " \n\r")
content = content + "\r\nnode_mirror: " + strings.Trim(env.node_mirror, " \n\r") + "\r\nnpm_mirror: " + strings.Trim(env.npm_mirror, " \n\r")
ioutil.WriteFile(env.settings, []byte(content), 0644)
}
// NOT USED?
/*
func useArchitecture(a string) {
if strings.ContainsAny("32",os.Getenv("PROCESSOR_ARCHITECTURE")) {
fmt.Println("This computer only supports 32-bit processing.")
return
}
if a == "32" || a == "64" {
env.arch = a
saveSettings()
fmt.Println("Set to "+a+"-bit mode")
} else {
fmt.Println("Cannot set architecture to "+a+". Must be 32 or 64 are acceptable values.")
}
}
*/
// ===============================================================
// END | Utility functions
// ===============================================================
func setup() {
lines, err := file.ReadLines(env.settings)
if err != nil {
fmt.Println("\nERROR",err)
os.Exit(1)
}
// Process each line and extract the value
for _, line := range lines {
line = strings.Trim(line, " \r\n")
if strings.HasPrefix(line, "root:") {
env.root = filepath.Clean(strings.TrimSpace(regexp.MustCompile("^root:").ReplaceAllString(line, "")))
} else if strings.HasPrefix(line, "originalpath:") {
env.originalpath = filepath.Clean(strings.TrimSpace(regexp.MustCompile("^originalpath:").ReplaceAllString(line, "")))
} else if strings.HasPrefix(line, "originalversion:") {
env.originalversion = strings.TrimSpace(regexp.MustCompile("^originalversion:").ReplaceAllString(line, ""))
} else if strings.HasPrefix(line, "arch:") {
env.arch = strings.TrimSpace(regexp.MustCompile("^arch:").ReplaceAllString(line, ""))
} else if strings.HasPrefix(line, "node_mirror:") {
env.node_mirror = strings.TrimSpace(regexp.MustCompile("^node_mirror:").ReplaceAllString(line, ""))
} else if strings.HasPrefix(line, "npm_mirror:") {
env.npm_mirror = strings.TrimSpace(regexp.MustCompile("^npm_mirror:").ReplaceAllString(line, ""))
} else if strings.HasPrefix(line, "proxy:") {
env.proxy = strings.TrimSpace(regexp.MustCompile("^proxy:").ReplaceAllString(line, ""))
if env.proxy != "none" && env.proxy != "" {
if strings.ToLower(env.proxy[0:4]) != "http" {
env.proxy = "http://"+env.proxy
}
web.SetProxy(env.proxy, env.verifyssl)
}
}
}
web.SetMirrors(env.node_mirror, env.npm_mirror)
env.arch = arch.Validate(env.arch)
// Make sure the directories exist
_, e := os.Stat(env.root)
if e != nil {
fmt.Println(env.root+" could not be found or does not exist. Exiting.")
return
}
}