Node.js Raspberry Pi Installer - Technical Implementation

Language Architecture License

Executive Summary

This Go application provides automated Node.js installation across all Raspberry Pi models and major Linux architectures. Originally developed to address the lack of Node.js availability in the Raspberry Pi OS repository, this utility simplifies complex cross-compilation requirements and enables developers to quickly establish Node.js development environments on embedded systems.

Problem Statement

When this project was initiated, Node.js was not included in the Raspberry Pi OS repository, requiring users to compile from source code manually. This created significant barriers for developers working with different Raspberry Pi models due to:

Architecture Overview

Design Philosophy

The application implements a cross-platform solution with the following architectural principles:

Technical Implementation

Core Components

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

Data Structures

NodeElement Structure:

type NodeElement struct {
    BigVersion   int    // Major version number
    MedVersion   int    // Minor version number  
    SmallVersion int    // Patch version number
    A7Link       string // ARMv7/ARM64 download URL
    A6Link       string // ARMv6 download URL
    idx          int    // Sort index for user selection
}

User Interface Components

Terminal Color System:

type Color string

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

Key Features

Architecture Detection

The application automatically detects system architecture using platform-specific string handling: - ARMv6 for Pi Zero, Zero W - ARMv7 for Pi 1, 2, 3
- ARMv8/ARM64 for Pi 4 - x86_64 for standard Linux systems

Version Management

Installation Process

  1. Architecture Detection: Platform-specific CPU identification
  2. Version Retrieval: HTTP requests to official Node.js distribution
  3. Binary Download: Parallel download with progress indication
  4. Extraction: Decompression and binary placement
  5. System Integration: Update-alternatives configuration and PATH setup
  6. Cleanup: Automatic removal of temporary files

Performance Optimizations

Concurrent Operations

func RequestForArchitectureOfficial(n *map[string]NodeElement, a7BigV *map[int][]int, 
    latestVersionArm7 *int, w *sync.WaitGroup, arch *string) {
    defer w.Done()
    
    resp, err := http.Get("https://nodejs.org/dist/")
    // Concurrent HTTP request processing
    // Parse response and populate version map
}

Error Handling

Package Manager Integration

The application integrates with system package managers using:

update-alternatives --install /usr/bin/node /usr/local/bin/node

This approach ensures: - System-level binary registration - Proper PATH configuration - Compatibility with existing Node.js installations - Clean uninstallation capabilities

Technical Benefits

Development Efficiency

System Compatibility

Community Impact

Code Quality Considerations

Current Implementation

For production deployment, consider implementing:

  1. Comprehensive Testing Suite

    func TestArchitectureDetection(t *testing.T) {
       // Mock different system architectures
       // Verify detection logic
    }
    
  2. Configuration File Support

    node-install:
     default_version: "lts"
     install_path: "/usr/local"
     mirror: "https://nodejs.org/dist"
    
  3. Logging and Monitoring

    log.Printf("Architecture detected: %s", architecture)
    log.Printf("Downloaded version: %s", version)
    
  4. Security Enhancements

    • Binary signature verification
    • HTTPS download enforcement
    • checksum validation for downloaded files

Project Repository

Source: github.com/audstanley/NodeJs-Raspberry-Pi

This implementation demonstrates practical solutions to complex cross-platform development challenges while maintaining code simplicity and user accessibility.

// 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