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)
}
}