NodeJS nexe resources - ScienceChronicle
ScienceChronicle
May 27, 2024

NodeJS nexe resources

Posted on May 27, 2024  •  4 minutes  • 765 words
Table of contents

For some reason, there is a scare description of how to add and access resources for executables created by nexe-cli. Here we make some clarifications.

What’s nexe-cli?

nexe-cli is an utility which creates a standalone executable from nodejs code. It compiles nodejs runtime for the target platform and creates a stanalone executable which contains the nodejs runtime and program js source code. Note, however, that nexe does not include node_modules folder in the executable so if the source code uses some of npm modules the distributed executable should be distributed along with the modules:

ls ./myexeproject
node_modules
myprogram.exe

Adding resources

nexe allows adding any kind of file as a resource to the executable file. Let’s see a concrete examlple:

tree ./myproject

.
├── build
│   ├── main.js
│   └── myprogram.exe
├── package.json
├── package-lock.json
├── resources
│   ├── rar
│   └── unrar.sh
├── src
│   └── main.ts
└── tsconfig.json

This is a typescript project which is compiled with tsc outputting the result into build/main.js. Then we create an executable with

nexe --build -i build/main.js -o build/myprogram.exe

We want to add two files to the executable, unrar.sh which is textual bash script and rar which is executable linux binary. We do this with:

nexe --build -r resources/**/* -i build/main.js -o build/myprogram.exe

Accessing the resources

The resources can be accessed by fs.readFile() or fs.readFileAsync() functions. Let’s read unrar.sh:

import fs from "fs"
import {exec} from "node:child_process"
import util from "node:util"
import path from "node:path"
async function main() {
	const scriptPath = path.join('resources', 'unrar.sh')
	console.log(`scriptPath: ${scriptPath}`)
	const scriptContent = fs.readFileSync(scriptPath, {encoding: "utf8"})
	console.log(scriptContent)
}

main()

To make sure our executable does not refer to original resources folder, we move it to some unrelated folder, ~/Downloads in this test. We run the program:

~/Downloads/myprogram.exe
scriptPath: resources/unrar.sh
#!/bin/bash
echo "Hello"

This is the text of the bash script.

Can we run executable directly from the our executable file? Let’s try:

import fs from "fs"
import {exec} from "node:child_process"
import util from "node:util"
import path from "node:path"
const execPromisified = util.promisify(exec);

async function main() {
	console.log(process.argv[2])
	const fileName = process.argv[2]
	const rarPath = path.join('resources', 'rar')
	const withArgs = `${rarPath} a x.rar ${fileName}`
	console.log(`rarPath: ${rarPath}`)
	const result = await execPromisified(withArgs)
	console.log(result.stdout)
}

main()

The code above tries to execute rar located at rosources folder inside executable. rar is run with command a x.rar which creates an archive x.rar from a file supplied as a cmd line argument.

Let’s try to compress some file in ~/Download folder, for example myprogram.exe itself:

cd ~/Downloads
./myprogram myprogram.exe

and we get the following error:

./myprogram.exe myprogram.exe 
myprogram.exe
rarPath: resources/rar
node:internal/errors:932
  const err = new Error(message);
              ^

Error: Command failed: resources/rar a x.rar myprogram.exe
/bin/sh: 1: resources/rar: not found

    at ChildProcess.exithandler (node:child_process:422:12)
    at ChildProcess.emit (node:events:518:28)
    at maybeClose (node:internal/child_process:1105:16)
    at Socket.<anonymous> (node:internal/child_process:457:11)
    at Socket.emit (node:events:518:28)
    at Pipe.<anonymous> (node:net:337:12) {
  code: 127,
  killed: false,
  signal: null,
  cmd: 'resources/rar a x.rar myprogram.exe',
  stdout: '',
  stderr: '/bin/sh: 1: resources/rar: not found\n'
}

Node.js v20.11.0

What happens here is that exec looks for rar in ~/Downloads/resources. Try to put resources/rar into ~/Downloads and all works without errors.

So, our conclusion is that exec() function does not know about the internal resources of our executable.

What we can do is to access the resources folder with fs.readFile() or fs.readFileSync() as mentioned by nexe documentation but not in very detailed way. That’s why we wrote that post.

import fs from "fs"
import {exec} from "node:child_process"
import util from "node:util"
import path from "node:path"

const execPromisified = util.promisify(exec);
async function main() {
	const rarPath = path.join('resources', 'rar')
	console.log(`rarPath: ${rarPath}`)
	const rarFile = fs.readFileSync(rarPath)
	fs.writeFileSync("myrar", rarFile)
	console.log(`__dirname value: ${__dirname}`)
	console.log(`process cwd: ${process.cwd()}`)
	const fileName = process.argv[2]
	const result = await execPromisified(`echo $(pwd); chmod +x myrar && ./myrar a x.rar ${fileName}`)
	console.log(result.stdout)
}

main()

In the code above, which is run once again in ~/Downloads directory, we read our rar file from the resource directory and write it as myrar file in ~/Downloads. We run the program:

cd ~/Downloads
./myprogram.exe myprogram.exe

which outputs:

rarPath: resources/rar
__dirname value: /home/user/Downloads/build
process cwd: /home/user/Downloads
/home/user/Downloads

RAR 7.01   Copyright (c) 1993-2024 Alexander Roshal   12 May 2024
Trial version             Type 'rar -?' for help

Evaluation copy. Please register.

Creating archive x.rar

Adding    myprpgram.exe                                                       OK 
Done

We succeeded. And we have some interesing observations.

process.cwd() reported executable current working directory is ~/Downloads. The same was reported by echo $(pwd) run by exec(). However, __dirname remembers the part of the path executable was created in, so it displays ${process.cwd()}/build.

Conclusion

To access resources in executable, use fs.readFile() or fs.readFileAsync() and write them to the disk if you want other functions like exec() to use them.


Share


Tags


Counters

Support us

Science Chronicle