Node Js Raspberry Pi Installer Code

This is one of many of my earlier GoLang Projects. I didn’t write any tests, or code coverage. I just wanted to replace a shell script that I had written for new programmers that want to experience their Raspberry Pi computer with NodeJs. When I initially written the installer as a shell script, NodeJs wasn’t included in the Raspian Operating system when I had initially started the project. If you are a developer, you may or may not know that not only is the chipset a different architecture for the Raspberry Pi computers, but that different Raspberry Pi computers have different CPU architectures. Installations can become more difficult if you don’t know how to compile project source code from scratch, and this helped a lot of people save time and jump into the NodeJs ecosystem. I suppose that I could have created deb installers on behalf of the official arm releases, but I wanted to take a slightly different approach. I’m a bit interested in the multiple different package managers that different flavors of linux offer, and I believe that developers might need to find different ways in which we approach our software stack.


package main

import (
	"archive/tar"
	"bufio"
	"compress/gzip"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/exec"
	"os/user"
	"path"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"
	"syscall"

	"github.com/audstanley/NodeJs-Raspberry-Pi/rpistringsarray"
)

// The Color object is to pretty print in the linux terminal.
type Color string

// Declaring the color codes for linux terminal
const (
	ColorBlack  Color = "\u001b[30m"
	ColorRed    Color = "\u001b[31m"
	ColorGreen  Color = "\u001b[32m"
	ColorCyan   Color = "\u001b[36m"
	ColorYellow Color = "\u001b[33m"
	ColorBlue   Color = "\u001b[34m"
	ColorReset  Color = "\u001b[0m"
)

// NodeElement stores the full version path as well as the direct link to the download
type NodeElement struct {
	BigVersion   int
	MedVersion   int
	SmallVersion int
	A7Link       string
	A6Link       string
	idx          int
}

// used to print colorized test in terminal
func colorize(color Color, message string) {
	fmt.Println(string(color), message, string(ColorReset))
}

func colorizeWithoutNewLine(color Color, message string) {
	fmt.Print(string(color), message, string(ColorReset))
}

// PrintLogo prints the application logo as a concurrent process
func PrintLogo(wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Print("\n\n")
	colorize(ColorCyan, "                                   ;                ")
	colorize(ColorCyan, "                                   +++              ")
	colorize(ColorCyan, "                                   +++              ")
	colorize(ColorCyan, "                                   +++              ")
	colorize(ColorCyan, "    ''++''       :;;',        ,+++;+++    ''++''    ")
	colorize(ColorCyan, " :++++++++++: ;;;;;;;''''  '++++'+++++ :++++++++++: ")
	colorize(ColorCyan, " +++.    .++' ';;;;;;;;''  +++     +++ '++. '' .+++ ")
	colorize(ColorCyan, " +++.    .++' '';;;;;;;;'  +++     +++ '++. '' ''   ")
	colorize(ColorCyan, " +++.    .++' ''';;;;;;;;  '++++'+++++ :+++++,      ")
	colorize(ColorCyan, " :          ,   '';;;,        ,+++;       ''++'.    ")
	colorize(ColorGreen, "                    #+''#';+''+'                   ")
	colorize(ColorGreen, "                    ';;;+#'+;;'                    ")
	colorize(ColorRed, "                     .###''@#@                       ")
	colorize(ColorRed, "                     '@++@@'+#@                      ")
	colorize(ColorRed, "                   :'@'''@'''''+                     ")
	colorize(ColorRed, "                    ##@@'''#@+#@                     ")
	colorize(ColorRed, "                    .'''#''@''#                      ")
	colorize(ColorRed, "                      '#'''#'                        ")
	fmt.Print("\n\n")
	colorizeWithoutNewLine(ColorYellow, "  Developed By:")
	colorize(ColorCyan, "Richard Stanley")
	colorize(ColorCyan, " https://www.audstanley.com")
	colorizeWithoutNewLine(ColorCyan, "  This installer also works on ")
	colorizeWithoutNewLine(ColorYellow, "x86_64")
	colorizeWithoutNewLine(ColorCyan, ", and ")
	colorizeWithoutNewLine(ColorYellow, "arm64")
	colorize(ColorCyan, "processors.")
	colorize(ColorCyan, " πŸ‘ The only nodeJs installer that you really need for Linux")
	fmt.Print("\n\n")

}

// RequestForArchitectureOfficial makes the GET request (as a concurrent waitgroup) for the architecture passed and returns []NodeBigVersion struct
func RequestForArchitectureOfficial(n *map[string]NodeElement, a7BigV *map[int][]int, latestVersionArm7 *int, w *sync.WaitGroup, arch *string) {
	// defer the waitgroup, which will end at the end of the function.  This is for concurrency, and saves
	// time to not make the https requests in series.
	defer w.Done()

	resp, err := http.Get("https://nodejs.org/dist/")
	if err != nil {
		log.Fatalln(err)
	}

	// Parse the Body of the get request
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatalln(err)
	}

	// Match all the versions that are avaible for downloading
	re := regexp.MustCompile(`<a href="v((\d{1,})\.(\d{1,})\.(\d{1,}))`)
	info := re.FindAllStringSubmatch(string(body), -1)

	// Populate the map with all the Node Elements for quick searching.
	for _, v := range info {
		link := v[1]
		bigV, _ := strconv.Atoi(v[2])
		medV, _ := strconv.Atoi(v[3])
		smallV, _ := strconv.Atoi(v[4])

		if bigV != 0 {
			(*n)[link] = NodeElement{bigV, medV, smallV,
				`https://nodejs.org/dist/v` + link + `/node-v` +
					link + `-linux-` + *arch + `.tar.gz`,
				`https://nodejs.org/dist/v` + link +
					`/node-v` + link + `-linux-armv6l.tar.gz`, -1}
			(*a7BigV)[bigV] = append((*a7BigV)[bigV], medV)
			if *latestVersionArm7 < bigV {
				*latestVersionArm7 = bigV
			}
		}
	}
}

// RequestForArm6Unofficial makes the GET request (as a concurrent waitgroup) for ARM6 processors and returns []NodeBigVersion struct
func RequestForArm6Unofficial(n *map[string]NodeElement,
	a6BigV *map[int][]int, latestVersionArm6 *int, w *sync.WaitGroup) {
	// defer the waitgroup, which will end at the end of the function.  This is for concurrency, and saves
	// time to not make the https requests in series.
	defer w.Done()

	resp, err := http.Get("https://unofficial-builds.nodejs.org/download/release/")
	if err != nil {
		log.Fatalln(err)
	}

	// Parse the Body of the get request
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatalln(err)
	}

	// Match all the versions that are avaible for downloading
	re := regexp.MustCompile(`<a href="v((\d{1,})\.(\d{1,})\.(\d{1,}))`)
	info := re.FindAllStringSubmatch(string(body), -1)

	// Populate the map with all the Node Elements for quick searching.
	for _, v := range info {
		link := v[1]
		bigV, _ := strconv.Atoi(v[2])
		medV, _ := strconv.Atoi(v[3])
		smallV, _ := strconv.Atoi(v[4])

		// For ARM6, we only need versions 12 and above for the unofficial releases
		if bigV >= 12 {
			(*n)[link] = NodeElement{bigV, medV, smallV, ``,
				`https://unofficial-builds.nodejs.org/download/release/v` +
					link + `/node-v` + link + `-linux-armv6l.tar.gz`, -1}
			(*a6BigV)[bigV] = append((*a6BigV)[bigV], medV)
			if *latestVersionArm6 < bigV {
				*latestVersionArm6 = bigV
			}
		}
	}

}

func deleteFolder(folder *string, w *sync.WaitGroup) {
	defer w.Done()
	os.RemoveAll(*folder)
}

func deleteFile(file *string, w *sync.WaitGroup) {
	defer w.Done()
	os.Remove(*file)

}

// DownloadFile will download a url to a local file. It's efficient because it will
// write as it downloads and not load the whole file into memory.
func DownloadFile(filepath string, url string) error {

	// Get the data
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Create the file
	out, err := os.Create(filepath)
	if err != nil {
		return err
	}
	defer out.Close()

	// Write the body to file
	_, err = io.Copy(out, resp.Body)
	return err
}

// Untar takes a destination path and a reader; a tar reader loops over the tarfile
// creating the file structure at 'dst' along the way, and writing any files
func Untar(dst string, r io.Reader) error {

	gzr, err := gzip.NewReader(r)
	if err != nil {
		return err
	}
	defer gzr.Close()

	tr := tar.NewReader(gzr)

	for {
		header, err := tr.Next()

		switch {

		// if no more files are found return
		case err == io.EOF:
			return nil

		// return any other error
		case err != nil:
			return err

		// if the header is nil, just skip it (not sure how this happens)
		case header == nil:
			continue
		}

		// the target location where the dir/file should be created
		target := filepath.Join(dst, header.Name)

		// the following switch could also be done using fi.Mode(), not sure if there
		// a benefit of using one vs. the other.
		// fi := header.FileInfo()

		// check the file type
		switch header.Typeflag {

		// if its a dir and it doesn't exist create it
		case tar.TypeDir:
			if _, err := os.Stat(target); err != nil {
				if err := os.MkdirAll(target, 0755); err != nil {
					return err
				}
			}

		// if it's a file create it
		case tar.TypeReg:
			f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
			if err != nil {
				return err
			}

			// copy over contents
			if _, err := io.Copy(f, tr); err != nil {
				return err
			}

			// manually close here after each file operation; defering would cause each file close
			// to wait until all operations have completed.
			f.Close()
		}
	}
}

// CopyFile copies a single file from src to dst
func CopyFile(src, dst string) error {
	var err error
	var srcfd *os.File
	var dstfd *os.File
	var srcinfo os.FileInfo

	if srcfd, err = os.Open(src); err != nil {
		return err
	}
	defer srcfd.Close()

	if dstfd, err = os.Create(dst); err != nil {
		return err
	}
	defer dstfd.Close()

	if _, err = io.Copy(dstfd, srcfd); err != nil {
		return err
	}
	if srcinfo, err = os.Stat(src); err != nil {
		return err
	}
	return os.Chmod(dst, srcinfo.Mode())
}

// CopyDirectory copies a whole directory recursively
func CopyDirectory(src string, dst string) error {
	var err error
	var fds []os.FileInfo
	var srcinfo os.FileInfo

	if srcinfo, err = os.Stat(src); err != nil {
		return err
	}

	if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
		return err
	}

	if fds, err = ioutil.ReadDir(src); err != nil {
		return err
	}
	for _, fd := range fds {
		srcfp := path.Join(src, fd.Name())
		dstfp := path.Join(dst, fd.Name())

		if fd.IsDir() {
			if err = CopyDirectory(srcfp, dstfp); err != nil {
				fmt.Println(err)
			}
		} else {
			if err = CopyFile(srcfp, dstfp); err != nil {
				fmt.Println(err)
			} else {
				colorizeWithoutNewLine(ColorGreen, "Copying_File: "+srcfp+"\n    To: "+dstfp+"\n")
			}
		}
	}
	return nil
}

// SelectionDataFromUser prompts the user for which specific version of NodeJs they wish to install
func SelectionDataFromUser(a7 *map[string]NodeElement, version *string) string {
	versionMatched, _ := strconv.Atoi(regexp.MustCompile(`^([0-9]+)$`).FindStringSubmatch(*version)[1])
	var populatedVersions []NodeElement

	for _, element := range *a7 {
		if versionMatched == element.BigVersion {
			populatedVersions = append(populatedVersions, element)
		}
	}
	sort.SliceStable(populatedVersions, func(i, j int) bool {
		return populatedVersions[i].MedVersion < populatedVersions[j].MedVersion
	})

	for i, v := range populatedVersions {
		colorize(ColorYellow, "  "+strconv.Itoa(i)+": NodeJs version "+strconv.Itoa(v.BigVersion)+"."+strconv.Itoa(v.MedVersion)+"."+strconv.Itoa(v.SmallVersion)+" : "+v.A7Link)
	}

	reader := bufio.NewReader(os.Stdin)
	colorize(ColorCyan, "Please select a subversion from the list of integers")
	colorize(ColorCyan, "---------------------")

	selection := ""
	for {
		colorizeWithoutNewLine(ColorCyan, "-> ")
		text, _ := reader.ReadString('\n')
		text = strings.Replace(text, "\n", "", -1)

		if regexp.MustCompile(`^[0-9]+$`).MatchString(text) {
			userInput, _ := strconv.Atoi(regexp.MustCompile(`^([0-9]+)$`).FindStringSubmatch(text)[1])

			if userInput >= 0 && userInput < len(populatedVersions) {
				colorize(ColorGreen, "Selection is Valid")
				selection = strconv.Itoa(populatedVersions[userInput].BigVersion) + "." +
					strconv.Itoa(populatedVersions[userInput].MedVersion) + "." +
					strconv.Itoa(populatedVersions[userInput].SmallVersion)
				break

			} else {
				colorize(ColorRed, "Selection is invalid, Please input and integer from 0 to "+strconv.Itoa(len(populatedVersions)-1))
			}

		} else {
			colorize(ColorRed, "Invalid Selection")
		}
	}
	return selection
}

// GetTheMostLatestVersionOfNodeJs will return the most up to date version of NodeJs
func GetTheMostLatestVersionOfNodeJs(a7 *map[string]NodeElement, version *string, architecture *string) string {
	versionMatched, _ := strconv.Atoi(regexp.MustCompile(`^([0-9]+)$`).FindStringSubmatch(*version)[1])
	var populatedVersions []NodeElement

	for _, element := range *a7 {
		if versionMatched == element.BigVersion {
			populatedVersions = append(populatedVersions, element)
		}
	}

	sort.SliceStable(populatedVersions, func(i, j int) bool {
		return populatedVersions[i].MedVersion < populatedVersions[j].MedVersion
	})

	idx := len(populatedVersions) - 1
	return strconv.Itoa(populatedVersions[idx].BigVersion) + "." + strconv.Itoa(populatedVersions[idx].MedVersion) + "." + strconv.Itoa(populatedVersions[idx].SmallVersion)

}

// RunInstallation will take the specific version number of NodeJs, and install for the appropriate architecture
func RunInstallation(n *map[string]NodeElement, version *string, architecture *string) string {
	if val, ok := (*n)[*version]; ok {
		link := val.A7Link
		if *architecture == "armv6l" {
			link = val.A6Link
		}
		colorize(ColorGreen, "Downloading: "+link)
		err := DownloadFile("/tmp/node-v"+*version+"-linux-"+*architecture+".tar.gz", link)

		if err != nil {
			log.Println(err)
			os.Exit(1)
		}

		f, _ := os.Open("/tmp/node-v" + *version + "-linux-" + *architecture + ".tar.gz")
		defer f.Close()
		colorize(ColorGreen, "Untaring: "+"/tmp/node-v"+*version+"-linux-"+*architecture+".tar.gz")
		Untar("/tmp/node-v"+*version+"-linux-"+*architecture, f)
		colorize(ColorGreen, "Copying files over to the appropriate location, and creating symlinks")
		err = CopyDirectory("/tmp/node-v"+*version+"-linux-"+*architecture+"/node-v"+*version+"-linux-"+*architecture, "/opt/nodejs")

		if err != nil {
			colorize(ColorRed, "There was a problem with making a copy of the nodeJs directory: node-v"+*version)
			fmt.Println(err)
			os.Exit(1)
		}

		err = CopyDirectory("/opt/nodejs/lib/node_modules", "/usr/lib/node_modules")
		if err != nil {
			fmt.Println(err)
		}
		err = CopyFile("/opt/nodejs/bin/node", "/usr/bin/node")
		if err != nil {
			fmt.Println(err)
		}
		currentDir, _ := os.Getwd()
		os.Chdir("/usr/bin")
		exec.Command("ln", "-sf", "../lib/node_modules/npm/bin/npm", "/usr/bin/npm").Run()
		exec.Command("ln", "-sf", "../lib/node_modules/npm/bin/npm-cli.js", "/usr/bin/npm-cli.js").Run()
		exec.Command("ln", "-sf", "../lib/node_modules/npm/bin/npx", "/usr/bin/npx").Run()
		exec.Command("ln", "-sf", "../lib/node_modules/", "/usr/bin/node_modules").Run()
		os.Chdir(currentDir)

	} else {
		colorize(ColorRed, *version+" is not an available version for NodeJs on "+*architecture)
	}
	return "/tmp/node" + *version + "-linux-" + *architecture

}

func runCommandConcurrent(command *string, args *[]string, w *sync.WaitGroup) {
	defer w.Done()
	cmd := exec.Command(*command, *args...)
	cmd.Run()
}

func main() {
	var wg sync.WaitGroup

	// We should varify that the user is root.
	user, _ := user.Current()

	if user.Uid != "0" {
		wg.Add(1)
		go PrintLogo(&wg)
		wg.Wait()
		colorize(ColorRed, "\n\n    You need to run node-install as root")
		os.Exit(1)
	}

	var latestVersionArm7 int
	var latestVersionArm6 int
	a6 := make(map[string]NodeElement)
	a7 := make(map[string]NodeElement)
	a6BigV := make(map[int][]int)
	a7BigV := make(map[int][]int)

	version := flag.String("v", "", "to install a specific version of NodeJs")
	latest := flag.Bool("a", false, "to install the latest version of NodeJs")

	var uname syscall.Utsname
	syscall.Uname(&uname)

	// When cross compiling for arm, we need to use a slightly different rpistringsarray.ArrayToString function.
	// The compiler will work with the "-tags arm" argument that we assign to go build.
	// so "go build -tags arm", will use the arm.go file's function for the rpistringsarray.ArrayToString function,
	// and go build (with no tag argument) will use the x64.go version of the rpistringsarray.ArrayToString function.
	// There is a difference in the way the CPU architecture deals with ascii integer values (as a [65]uint8 - unsigned, and not a [65]int8).
	architecture := rpistringsarray.ArrayToString(uname.Machine)

	if architecture == "x86_64" {
		architecture = "x64"
	} else if architecture == "aarch64" {
		architecture = "arm64"
	}

	nodeJsSymlinks := []string{"/usr/bin/node", "/usr/bin/nodejs", "/usr/lib/nodejs", "/usr/sbin/node", "/sbin/node", "/sbin/node", "/usr/local/bin/node", "/usr/bin/npm", "/usr/sbin/npm", "/sbin/npm", "/usr/local/bin/npm", "/usr/bin/node_modules"}
	updateAlternatives := "/usr/bin/update-alternatives"
	nodeAndNpmSymlinks := [][]string{
		{"--install", "/usr/bin/node", "node", "/opt/nodejs/bin/node", "1"},
	}
	nodeJsDirectory := "/opt/nodejs/"

	wgCount := 4 + len(nodeJsSymlinks)
	wg.Add(wgCount)
	go PrintLogo(&wg)
	for i := range nodeJsSymlinks {
		go deleteFile(&nodeJsSymlinks[i], &wg)
	}
	go deleteFolder(&nodeJsDirectory, &wg)
	go RequestForArchitectureOfficial(&a7, &a7BigV, &latestVersionArm7, &wg, &architecture) // Making the body requests as a concurrent task
	go RequestForArm6Unofficial(&a6, &a6BigV, &latestVersionArm6, &wg)                      // Making the body requests as a concurrent task
	wg.Wait()

	colorize(ColorGreen, "Obtaining NodeJs for architecture: "+architecture)
	flag.Parse()

	// If the architecture is arm6, we are going to use the a7 map of elements, but overwrite the links in the NodeElement.A6Link
	// This way, official builds up to version 12 will be installed for arm6, and unofficial builds will be installed for versions 12+
	// Ultimately, this will add arm6 support for the pi zero for as long as the unofficial builds are released.
	if architecture == "armv6l" {

		for key := range a6 {
			a7[key] = a6[key]
		}

	}

	if *version != "" && regexp.MustCompile(`^[0-9]+$`).MatchString(*version) && !(*latest) {
		selection := SelectionDataFromUser(&a7, version)
		tmpFile := RunInstallation(&a7, &selection, &architecture)
		tarFile := tmpFile + ".tar.gz"
		nodeDownloadedFolder := tmpFile + "/"
		wgCount = 2 + len(nodeAndNpmSymlinks)
		wg.Add(wgCount)
		for i := range nodeAndNpmSymlinks {
			go runCommandConcurrent(&updateAlternatives, &nodeAndNpmSymlinks[i], &wg)
		}
		go deleteFile(&tarFile, &wg)
		go deleteFolder(&nodeDownloadedFolder, &wg)
		wg.Wait()
		colorize(ColorGreen, "πŸ‘ good to go πŸ‘")

	} else if *version != "" && regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+$`).MatchString(*version) && !(*latest) {
		tmpFile := RunInstallation(&a7, version, &architecture)
		tarFile := tmpFile + ".tar.gz"
		nodeDownloadedFolder := tmpFile + "/"
		wgCount = 2 + len(nodeAndNpmSymlinks)
		wg.Add(wgCount)
		for i := range nodeAndNpmSymlinks {
			go runCommandConcurrent(&updateAlternatives, &nodeAndNpmSymlinks[i], &wg)
		}
		go deleteFile(&tarFile, &wg)
		go deleteFolder(&nodeDownloadedFolder, &wg)
		wg.Wait()
		colorize(ColorGreen, "πŸ‘ good to go πŸ‘")
	} else if *latest && *version == "" {
		// install the latest version of NodeJs
		colorize(ColorGreen, "Installing latest version of NodeJs")
		latestVersion := strconv.Itoa(latestVersionArm7)
		latestVersionAsString := GetTheMostLatestVersionOfNodeJs(&a7, &latestVersion, &architecture)
		colorize(ColorGreen, "  version: "+latestVersionAsString)
		tmpFile := RunInstallation(&a7, &latestVersionAsString, &architecture)
		tarFile := tmpFile + ".tar.gz"
		nodeDownloadedFolder := tmpFile + "/"
		wgCount = 2 + len(nodeAndNpmSymlinks)
		wg.Add(wgCount)
		for i := range nodeAndNpmSymlinks {
			go runCommandConcurrent(&updateAlternatives, &nodeAndNpmSymlinks[i], &wg)
		}
		go deleteFile(&tarFile, &wg)
		go deleteFolder(&nodeDownloadedFolder, &wg)
		wg.Wait()
		colorize(ColorGreen, "πŸ‘ good to go πŸ‘")

	} else if !*latest && *version == "" {
		colorize(ColorRed, "You need to at least specify one option")
		os.Exit(1)
	} else if *latest && *version != "" {
		colorize(ColorRed, "You cannot run the latest install flag : -a, and a version selection: -v at the same time. Use one or the other")
		os.Exit(1)
	}

}


Link to the Project