Compare commits

..

2 Commits

Author SHA1 Message Date
oxmc c48b40b14e Update src/steps/download.ts
Co-authored-by: Jared Allard <jaredallard@users.noreply.github.com>
2023-11-16 22:52:15 -08:00
oxmc 1267f5d41d add spacer 2023-11-14 16:03:27 -08:00
44 changed files with 7705 additions and 5666 deletions
-33
View File
@@ -1,33 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
"prettier/prettier": "warn",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-empty-function": "warn",
"no-useless-escape": "warn",
"no-empty": "warn",
"no-constant-condition": "warn",
"prefer-const": "warn"
},
"env": {
"node": true,
"es6": true
},
"ignorePatterns": ["lib/", "node_modules/", "*.js", "webpack.config.ts"]
}
-160
View File
@@ -1,160 +0,0 @@
name: Build and test asset
on:
schedule:
- cron: '0 10 * * 1' # every Monday at 10:00 UTC
workflow_dispatch:
inputs:
versions:
description: 'Node.js version(s) as JSON array'
required: true
default: '[ "lts/*", "22", "24" ]'
pull_request:
branches: [ master ]
push:
tags: [ '*' ]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ windows-latest, macos-latest, ubuntu-latest ]
# Use the input versions if provided, otherwise fall back to default
version: ${{ fromJSON(inputs.versions || '[ "lts/*", "22", "24" ]') }}
env:
NEXE_ASSET: ${{ github.workspace }}/nexe-asset
NEXE_TMP_CACHE_PATH: ${{ github.workspace }}/nexe-tmp
NEXE_TMP: ${{ github.workspace }}/nexe-tmp/standard
CCACHE_COMPRESS: '1'
ErrorView: NormalView
steps:
# Checkout repository
- uses: actions/checkout@v4
# Set up Node.js (using matrix version)
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.version }}
# Record the exact Node.js version for later use (crossplatform)
- name: Record exact Node.js version
shell: bash
run: echo "NODEJS_VERSION=$(node -p process.versions.node)" >> $GITHUB_ENV
# Install Node dependencies
- run: npm ci
# Python may be needed for native addons
- uses: actions/setup-python@v5
with:
python-version: '3.9'
# Windows: install NASM (required for some native modules)
- if: matrix.os == 'windows-latest'
run: choco install nasm
# Ubuntu: update package lists (kept for safety, can be removed if not needed)
- if: matrix.os == 'ubuntu-latest'
run: sudo apt-get update
# Linux/macOS: enable compiler caching with ccache
- if: matrix.os != 'windows-latest'
uses: hendrikmuhs/ccache-action@v1.2
with:
create-symlink: true
key: ${{ github.job }}-${{ matrix.os }}-${{ matrix.version }}
# Windows: adjust asset name and suffix
- if: matrix.os == 'windows-latest'
shell: bash
run: |
echo "NEXE_ASSET=${NEXE_ASSET}.exe" >> $GITHUB_ENV
echo "EXECUTABLE_SUFFIX=.exe" >> $GITHUB_ENV
# Main build & test (asset + integration)
- name: Build and test asset
run: node tasks/asset-build && node tasks/asset-test-build && npm run test:integration:run
# Rename the built asset to include target platform info
- name: Update release artifact name
shell: bash
run: |
nexe_target="$(node -p 'require("./lib/target").getTarget().toString()')"
echo "NEXE_TARGET=${nexe_target}" >> $GITHUB_ENV
artefact_name="nexe-asset-${nexe_target}"
mv "${NEXE_ASSET}" "${artefact_name}"
echo "NEXE_ASSET=${artefact_name}" >> $GITHUB_ENV
# Upload main asset
- name: Upload asset artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.NEXE_ASSET }}
path: ${{ env.NEXE_ASSET }}
if-no-files-found: error
# Upload integration test binary
- name: Upload integration test artifact
uses: actions/upload-artifact@v4
with:
name: integration-tests-${{ env.NEXE_TARGET }}${{ env.EXECUTABLE_SUFFIX }}
path: integration-tests${{ env.EXECUTABLE_SUFFIX }}
if-no-files-found: error
# If this is a tag push, attach the asset to the release
- if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
fail_on_unmatched_files: true
prerelease: true
files: ${{ env.NEXE_ASSET }}
# Extra step for Ubuntu: build static (musl) version
- if: matrix.os == 'ubuntu-latest'
name: musl static build
shell: bash
run: |
# Determine musl target and set up environment
nexe_target="$(node -p 'require("./lib/target").getTarget({ platform: "static" }).toString()')"
echo "NEXE_TARGET=${nexe_target}" >> $GITHUB_ENV
artefact_name="nexe-asset-${nexe_target}"
export NEXE_ASSET="${artefact_name}"
echo "NEXE_ASSET=${NEXE_ASSET}" >> $GITHUB_ENV
export MUSL_BUILD=yes NEXE_TMP="${NEXE_TMP_CACHE_PATH}/musl"
# Set up crosscompiler with ccache
eval $(npx -p node-musl musl-exports)
export CC="ccache ${CC}" CXX="ccache ${CXX}" LD="ccache ${LD}"
# Build and test musl version
node tasks/asset-build
node tasks/asset-test-build
npm run test:integration:run
# Upload musl asset
- if: matrix.os == 'ubuntu-latest'
name: Upload musl asset
uses: actions/upload-artifact@v4
with:
name: ${{ env.NEXE_ASSET }}
path: ${{ env.NEXE_ASSET }}
if-no-files-found: error
# Upload musl integration test binary
- if: matrix.os == 'ubuntu-latest'
name: Upload musl integration test
uses: actions/upload-artifact@v4
with:
name: integration-tests-${{ env.NEXE_TARGET }}${{ env.EXECUTABLE_SUFFIX }}
path: integration-tests
if-no-files-found: error
# If this is a tag push, also attach the musl asset to the release
- if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'ubuntu-latest'
uses: softprops/action-gh-release@v2
with:
fail_on_unmatched_files: true
prerelease: true
files: ${{ env.NEXE_ASSET }}
-14
View File
@@ -136,20 +136,6 @@ compile({
export function createBundle (options: NexeOptions): Promise<string> export function createBundle (options: NexeOptions): Promise<string>
``` ```
- default: true - default: true
- #### `bundleRules: object`
- Configuration for the runtime module resolution inside the snapshot.
This controls how bare module specifiers (e.g., `require('lodash')`) are resolved from the embedded `node_modules`.
- Properties:
- `conditions?: string[]` The set of [conditions](https://nodejs.org/api/packages.html#conditional-exports) to use when evaluating the exports field of a package.
- Example:
```
javascript
{
conditions: ['require', 'node', 'production'],
basePath: '/snapshot/packages'
}
```
- default: `{ conditions: ['require', 'node', 'default'], basePath: '/snapshot/node_modules' }`
- #### `name: string` - #### `name: string`
- Module friendly name of the application - Module friendly name of the application
- default: basename of the input file, or `nexe_${Date.now()}` - default: basename of the input file, or `nexe_${Date.now()}`
+6369 -3063
View File
File diff suppressed because it is too large Load Diff
+9 -12
View File
@@ -2,12 +2,11 @@
"name": "nexe", "name": "nexe",
"description": "Create a single executable out of your Node.js application", "description": "Create a single executable out of your Node.js application",
"license": "MIT", "license": "MIT",
"version": "5.0.0-beta", "version": "5.0.0-beta.1",
"contributors": [ "contributors": [
"Craig Condon <craig.j.condon@gmail.com> (http://crcn.io)", "Craig Condon <craig.j.condon@gmail.com> (http://crcn.io)",
"Jared Allard <jaredallard@outlook.com>", "Jared Allard <jaredallard@outlook.com>",
"Caleb Boyd <caleb.boyd@hotmail.com>", "Caleb Boyd <caleb.boyd@hotmail.com>"
"Seth Olivarez <me@oxmc.me> (http://oxmc.me)"
], ],
"scripts": { "scripts": {
"ci:build": "ts-node tasks/build", "ci:build": "ts-node tasks/build",
@@ -16,7 +15,7 @@
"test:integration:run": "run-script-os", "test:integration:run": "run-script-os",
"test:integration:run:win32": "integration-tests.exe", "test:integration:run:win32": "integration-tests.exe",
"test:integration:run:default": "./integration-tests", "test:integration:run:default": "./integration-tests",
"lint": "eslint \"{src,plugins,tasks}/**/*.ts\" --fix", "lint": "tslint \"{src,plugins,tasks}/**/*.ts\" --fix",
"prepare": "npm run lint && npm run build && npm test", "prepare": "npm run lint && npm run build && npm test",
"prebuild": "rimraf lib", "prebuild": "rimraf lib",
"build": "tsc --declaration && tsc -p tasks && webpack", "build": "tsc --declaration && tsc -p tasks && webpack",
@@ -50,12 +49,12 @@
"@yarnpkg/libzip": "^3.0.0-rc.43", "@yarnpkg/libzip": "^3.0.0-rc.43",
"app-builder": "^7.0.4", "app-builder": "^7.0.4",
"archiver": "^5.3.1", "archiver": "^5.3.1",
"axios": "^1.13.2",
"caw": "^2.0.1", "caw": "^2.0.1",
"chalk": "^2.4.2", "chalk": "^2.4.2",
"download": "^8.0.0",
"globby": "^11.0.2", "globby": "^11.0.2",
"got": "^12.6.0",
"meriyah": "^4.3.5", "meriyah": "^4.3.5",
"minimatch": "^10.1.2",
"minimist": "^1.2.8", "minimist": "^1.2.8",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"multistream": "^4.1.0", "multistream": "^4.1.0",
@@ -63,12 +62,12 @@
"resolve-dependencies": "^6.0.9", "resolve-dependencies": "^6.0.9",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"run-script-os": "^1.1.6", "run-script-os": "^1.1.6",
"tar": "^7.5.2",
"webpack-config-prefabs": "0.0.5" "webpack-config-prefabs": "0.0.5"
}, },
"devDependencies": { "devDependencies": {
"@types/archiver": "^5.3.2", "@types/archiver": "^5.3.2",
"@types/chai": "^4", "@types/chai": "^4",
"@types/download": "^8",
"@types/globby": "^9", "@types/globby": "^9",
"@types/lodash": "^4.14.192", "@types/lodash": "^4.14.192",
"@types/minimist": "^1.2.2", "@types/minimist": "^1.2.2",
@@ -78,17 +77,15 @@
"@types/ora": "^3.2.0", "@types/ora": "^3.2.0",
"@types/rimraf": "3.0.2", "@types/rimraf": "3.0.2",
"@types/semver": "^7.3.13", "@types/semver": "^7.3.13",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"chai": "^4.3.7", "chai": "^4.3.7",
"eslint": "^8.40.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"execa": "^5.1.1", "execa": "^5.1.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tslint": "^6.1.1",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.3.0",
"typescript": "^5.0.3", "typescript": "^5.0.3",
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1"
} }
+145 -165
View File
@@ -1,8 +1,8 @@
import { delimiter, resolve, normalize, join } from "path"; import { delimiter, resolve, normalize, join } from 'path'
import { Buffer } from "buffer"; import { Buffer } from 'buffer'
import { createReadStream, ReadStream } from "fs"; import { createReadStream, ReadStream } from 'fs'
import { spawn } from "child_process"; import { spawn } from 'child_process'
import { Logger, LogStep } from "./logger"; import { Logger, LogStep } from './logger'
import { import {
readFileAsync, readFileAsync,
writeFileAsync, writeFileAsync,
@@ -11,33 +11,35 @@ import {
dequote, dequote,
isWindows, isWindows,
bound, bound,
semverGt semverGt,
} from "./util"; wrap,
import { NexeOptions, version } from "./options"; } from './util'
import { NexeTarget } from "./target"; import { NexeOptions, version } from './options'
import { Readable, Transform } from "stream"; import { NexeTarget } from './target'
import MultiStream = require("multistream"); import { PassThrough, Readable, Stream, Transform } from 'stream'
import { Bundle, toStream } from "./fs/bundle"; import MultiStream = require('multistream')
import { File } from "resolve-dependencies"; import { Bundle, toStream } from './fs/bundle'
import { File } from 'resolve-dependencies'
import { FactoryStream, LazyStream } from 'multistream'
const isBsd = Boolean(~process.platform.indexOf("bsd")); const isBsd = Boolean(~process.platform.indexOf('bsd'))
const make = isWindows ? "vcbuild.bat" : isBsd ? "gmake" : "make"; const make = isWindows ? 'vcbuild.bat' : isBsd ? 'gmake' : 'make'
const configure = isWindows ? "configure" : "./configure"; const configure = isWindows ? 'configure' : './configure'
type StringReplacer = (match: string) => string; type StringReplacer = (match: string) => string
export interface NexeFile { export interface NexeFile {
filename: string; filename: string
absPath: string; absPath: string
contents: string | Buffer; contents: string | Buffer
} }
export { NexeOptions }; export { NexeOptions }
export class NexeError extends Error { export class NexeError extends Error {
constructor(m: string) { constructor(m: string) {
super(m); super(m)
Object.setPrototypeOf(this, NexeError.prototype); Object.setPrototypeOf(this, NexeError.prototype)
} }
} }
@@ -45,304 +47,282 @@ export class NexeCompiler {
/** /**
* Epoch of when compilation started * Epoch of when compilation started
*/ */
private start = Date.now(); private start = Date.now()
private compileStep: LogStep | undefined; private compileStep: LogStep | undefined
public log = new Logger(this.options.loglevel); public log = new Logger(this.options.loglevel)
/** /**
* Copy of process.env * Copy of process.env
*/ */
public env = { ...process.env }; public env = { ...process.env }
/** /**
* Virtual FileSystem * Virtual FileSystem
*/ */
public bundle: Bundle; public bundle: Bundle
/** /**
* Root directory for the source of the current build * Root directory for the source of the current build
*/ */
public src: string; public src: string
/** /**
* In memory files that are being manipulated by the compiler * In memory files that are being manipulated by the compiler
*/ */
public files: NexeFile[] = []; public files: NexeFile[] = []
/** /**
* Standalone pieces of code run before the application entrypoint * Standalone pieces of code run before the application entrypoint
*/ */
public shims: string[] = []; public shims: string[] = []
/** /**
* The last shim (defaults to "require('module').runMain()") * The last shim (defaults to "require('module').runMain()")
*/ */
public startup = ""; public startup: string = ''
/** /**
* The main entrypoint filename for your application - eg. node mainFile.js * The main entrypoint filename for your application - eg. node mainFile.js
*/ */
public entrypoint: string | undefined; public entrypoint: string | undefined
/** /**
* Not used * Not used
*/ */
public targets: NexeTarget[]; public targets: NexeTarget[]
/** /**
* Current target of the compiler * Current target of the compiler
*/ */
public target: NexeTarget; public target: NexeTarget
/** /**
* Output filename (-o myapp.exe) * Output filename (-o myapp.exe)
*/ */
public output = this.options.output; public output = this.options.output
/** /**
* Flag to indicate whether or notstdin was used for input * Flag to indicate whether or notstdin was used for input
*/ */
public stdinUsed = false; public stdinUsed = false
/** /**
* Path to the configure script * Path to the configure script
*/ */
public configureScript: string; public configureScript: string
/** /**
* The file path of node binary * The file path of node binary
*/ */
public nodeSrcBinPath: string; public nodeSrcBinPath: string
/** /**
* Remote asset path if available * Remote asset path if available
*/ */
public remoteAsset: string; public remoteAsset: string
constructor(public options: NexeOptions) { constructor(public options: NexeOptions) {
const { python } = this.options; const { python } = (this.options = options)
//SOMEDAY iterate over multiple targets with `--outDir` //SOMEDAY iterate over multiple targets with `--outDir`
this.targets = options.targets as NexeTarget[]; this.targets = options.targets as NexeTarget[]
this.target = this.targets[0]; this.target = this.targets[0]
if (!/https?:\/\//.test(options.remote)) { if (!/https?\:\/\//.test(options.remote)) {
throw new NexeError( throw new NexeError(`Invalid remote URI scheme (must be http or https): ${options.remote}`)
`Invalid remote URI scheme (must be http or https): ${options.remote}`
);
} }
this.remoteAsset = options.remote + "nexe-asset-" + this.target.toString(); this.remoteAsset = options.remote + this.target.toString()
this.src = join(this.options.temp, this.target.version); this.src = join(this.options.temp, this.target.version)
this.configureScript = this.configureScript = configure + (semverGt(this.target.version, '10.10.0') ? '.py' : '')
configure + (semverGt(this.target.version, "10.10.0") ? ".py" : "");
this.nodeSrcBinPath = isWindows this.nodeSrcBinPath = isWindows
? join(this.src, "Release", "node.exe") ? join(this.src, 'Release', 'node.exe')
: join(this.src, "out", "Release", "node"); : join(this.src, 'out', 'Release', 'node')
this.log.step("nexe " + version, "info"); this.log.step('nexe ' + version, 'info')
this.bundle = new Bundle(options); this.bundle = new Bundle(options)
if (isWindows) { if (isWindows) {
const originalPath = process.env.PATH; const originalPath = process.env.PATH
delete process.env.PATH; delete process.env.PATH
this.env = { ...process.env }; this.env = { ...process.env }
this.env.PATH = python this.env.PATH = python
? (this.env.PATH = ? (this.env.PATH = dequote(normalize(python)) + delimiter + originalPath)
dequote(normalize(python)) + delimiter + originalPath) : originalPath
: originalPath; process.env.PATH = originalPath
process.env.PATH = originalPath;
} else { } else {
this.env = { ...process.env }; this.env = { ...process.env }
if (python) this.env.PYTHON = python; python && (this.env.PYTHON = python)
} }
} }
@bound @bound
addResource(absoluteFileName: string, content?: Buffer | string | File) { addResource(absoluteFileName: string, content?: Buffer | string | File) {
return this.bundle.addResource(absoluteFileName, content); return this.bundle.addResource(absoluteFileName, content)
} }
@bound @bound
async readFileAsync(file: string) { async readFileAsync(file: string) {
this.assertBuild(); this.assertBuild()
let cachedFile = this.files.find( let cachedFile = this.files.find((x) => normalize(x.filename) === normalize(file))
(x) => normalize(x.filename) === normalize(file)
);
if (!cachedFile) { if (!cachedFile) {
const absPath = join(this.src, file); const absPath = join(this.src, file)
cachedFile = { cachedFile = {
absPath, absPath,
filename: file, filename: file,
contents: await readFileAsync(absPath, "utf-8").catch((x) => { contents: await readFileAsync(absPath, 'utf-8').catch((x) => {
if (x.code === "ENOENT") return ""; if (x.code === 'ENOENT') return ''
throw x; throw x
}), }),
}; }
this.files.push(cachedFile); this.files.push(cachedFile)
} }
return cachedFile; return cachedFile
} }
@bound @bound
writeFileAsync(file: string, contents: string | Buffer) { writeFileAsync(file: string, contents: string | Buffer) {
this.assertBuild(); this.assertBuild()
return writeFileAsync(join(this.src, file), contents); return writeFileAsync(join(this.src, file), contents)
} }
@bound @bound
async replaceInFileAsync( async replaceInFileAsync(file: string, replace: string | RegExp, value: string | StringReplacer) {
file: string, const entry = await this.readFileAsync(file)
replace: string | RegExp, entry.contents = entry.contents.toString().replace(replace, value as any)
value: string | StringReplacer
) {
const entry = await this.readFileAsync(file);
entry.contents = entry.contents.toString().replace(replace, value as any);
} }
@bound @bound
async setFileContentsAsync(file: string, contents: string | Buffer) { async setFileContentsAsync(file: string, contents: string | Buffer) {
const entry = await this.readFileAsync(file); const entry = await this.readFileAsync(file)
entry.contents = contents; entry.contents = contents
} }
quit(error?: any) { quit(error?: any) {
const time = Date.now() - this.start; const time = Date.now() - this.start
this.log.write(`Finished in ${time / 1000}s`, error ? "red" : "green"); this.log.write(`Finished in ${time / 1000}s`, error ? 'red' : 'green')
return this.log.flush(); return this.log.flush()
} }
assertBuild() { assertBuild() {
if (!this.options.build) { if (!this.options.build) {
throw new NexeError("This feature is only available with `--build`"); throw new NexeError('This feature is only available with `--build`')
} }
} }
public getNodeExecutableLocation(target?: NexeTarget) { public getNodeExecutableLocation(target?: NexeTarget) {
if (this.options.asset) { if (this.options.asset) {
return resolve(this.options.cwd, this.options.asset); return resolve(this.options.cwd, this.options.asset)
} }
if (target) { if (target) {
return join(this.options.temp, target.toString()); return join(this.options.temp, target.toString())
} }
return this.nodeSrcBinPath; return this.nodeSrcBinPath
} }
private _runBuildCommandAsync(command: string, args: string[]) { private _runBuildCommandAsync(command: string, args: string[]) {
if (this.log.verbose) { if (this.log.verbose) {
this.compileStep!.pause(); this.compileStep!.pause()
} }
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
spawn(command, args, { spawn(command, args, {
cwd: this.src, cwd: this.src,
env: this.env, env: this.env,
shell: true, stdio: this.log.verbose ? 'inherit' : 'ignore',
stdio: this.log.verbose ? "inherit" : "ignore",
}) })
.once("error", (e: Error) => { .once('error', (e: Error) => {
if (this.log.verbose) { if (this.log.verbose) {
this.compileStep!.resume(); this.compileStep!.resume()
} }
reject(e); reject(e)
}) })
.once("close", (code: number) => { .once('close', (code: number) => {
if (this.log.verbose) { if (this.log.verbose) {
this.compileStep!.resume(); this.compileStep!.resume()
} }
if (code != 0) { if (code != 0) {
const error = `${command} ${args.join( const error = `${command} ${args.join(' ')} exited with code: ${code}`
" " reject(new NexeError(error))
)} exited with code: ${code}`;
reject(new NexeError(error));
} }
resolve(); resolve()
}); })
}); })
} }
private _configureAsync() { private _configureAsync() {
if (isWindows && semverGt(this.target.version, "10.15.99")) { if (isWindows && semverGt(this.target.version, '10.15.99')) {
return Promise.resolve(); return Promise.resolve()
} }
return this._runBuildCommandAsync(this.env.PYTHON || "python", [ return this._runBuildCommandAsync(this.env.PYTHON || 'python', [
this.configureScript, this.configureScript,
...this.options.configure, ...this.options.configure,
]); ])
} }
public async build(): Promise<ReadStream> { public async build(): Promise<ReadStream> {
this.compileStep!.log( this.compileStep!.log(
`Configuring node build${this.options.configure.length ? ": " + this.options.configure : "..." `Configuring node build${
this.options.configure.length ? ': ' + this.options.configure : '...'
}` }`
); )
await this._configureAsync(); await this._configureAsync()
const buildOptions = this.options.make; const buildOptions = this.options.make
this.compileStep!.log( this.compileStep!.log(
`Compiling Node${buildOptions.length ? " with arguments: " + buildOptions : "..." `Compiling Node${buildOptions.length ? ' with arguments: ' + buildOptions : '...'}`
}` )
); await this._runBuildCommandAsync(make, buildOptions)
await this._runBuildCommandAsync(make, buildOptions); return createReadStream(this.getNodeExecutableLocation())
return createReadStream(this.getNodeExecutableLocation());
} }
private async _shouldCompileBinaryAsync( private async _shouldCompileBinaryAsync(binary: NodeJS.ReadableStream | null, location: string) {
binary: NodeJS.ReadableStream | null,
location: string
) {
//SOMEDAY combine make/configure/vcBuild/and modified times of included files //SOMEDAY combine make/configure/vcBuild/and modified times of included files
const { snapshot, build } = this.options; const { snapshot, build } = this.options
if (!binary) { if (!binary) {
return true; return true
} }
if (build && snapshot != null && (await pathExistsAsync(snapshot))) { if (build && snapshot != null && (await pathExistsAsync(snapshot))) {
const snapshotLastModified = (await statAsync(snapshot)).mtimeMs; const snapshotLastModified = (await statAsync(snapshot)).mtimeMs
const binaryLastModified = (await statAsync(location)).mtimeMs; const binaryLastModified = (await statAsync(location)).mtimeMs
return snapshotLastModified > binaryLastModified; return snapshotLastModified > binaryLastModified
} }
return false; return false
} }
async compileAsync(target: NexeTarget) { async compileAsync(target: NexeTarget) {
const step = (this.compileStep = this.log.step("Compiling result")); const step = (this.compileStep = this.log.step('Compiling result'))
const build = this.options.build; const build = this.options.build
const location = this.getNodeExecutableLocation(build ? undefined : target); const location = this.getNodeExecutableLocation(build ? undefined : target)
let binary = (await pathExistsAsync(location)) let binary = (await pathExistsAsync(location)) ? createReadStream(location) : null
? createReadStream(location)
: null;
if (await this._shouldCompileBinaryAsync(binary, location)) { if (await this._shouldCompileBinaryAsync(binary, location)) {
binary = await this.build(); binary = await this.build()
step.log("Node binary compiled"); step.log('Node binary compiled')
} }
return this._assembleDeliverable(binary!); return this._assembleDeliverable(binary!)
} }
code() { code() {
return [this.shims.join(""), this.startup].join(";"); return [this.shims.join(''), this.startup].join(';')
} }
private async _assembleDeliverable(binary: Readable) { private async _assembleDeliverable(binary: Readable) {
if (!this.options.mangle) { if (!this.options.mangle) {
return binary; return binary
} }
const launchCode = this.code(); const launchCode = this.code()
const codeSize = Buffer.byteLength(launchCode); const codeSize = Buffer.byteLength(launchCode)
const sentinel = Buffer.from("<nexe~~sentinel>"); const sentinel = Buffer.from('<nexe~~sentinel>')
let vfsSize = 0; let vfsSize = 0
// finalize() must be called before any reads — archiver won't emit chunks
// until finalized, so calling it inside the Transform's first-chunk callback
// creates a deadlock (Transform waits for chunks, archiver waits for finalize).
const bundleStream = this.bundle.toStream();
this.bundle.finalize();
const streams = [ const streams = [
binary, binary,
toStream(launchCode), toStream(launchCode),
bundleStream.pipe( this.bundle.toStream().pipe(
new Transform({ new Transform({
transform: (chunk, _, cb) => { transform: (chunk, _, cb) => {
if (chunk) vfsSize += chunk.length; vfsSize || this.bundle.finalize()
cb(null, chunk); chunk && (vfsSize += chunk.length)
cb(null, chunk)
}, },
}) })
), ),
]; ]
let done = false; let done = false
return new MultiStream((cb) => { return new MultiStream((cb) => {
if (done) cb(null, null); if (done) cb(null, null)
else if (streams.length) cb(null, streams.shift() as Readable); else if (streams.length) cb(null, streams.shift() as Readable)
else { else {
done = true; done = true
const trailers = Buffer.alloc(16); const trailers = Buffer.alloc(16)
trailers.writeDoubleLE(codeSize, 0); trailers.writeDoubleLE(codeSize, 0)
trailers.writeDoubleLE(vfsSize, 8); trailers.writeDoubleLE(vfsSize, 8)
cb(null, toStream(Buffer.concat([sentinel, trailers]))); cb(null, toStream(Buffer.concat([sentinel, trailers])))
} }
}); })
} }
} }
+186 -273
View File
@@ -1,4 +1,4 @@
import { Libzip } from "@yarnpkg/libzip"; import { Libzip } from '@yarnpkg/libzip'
import { import {
FakeFS, FakeFS,
PortablePath, PortablePath,
@@ -13,50 +13,45 @@ import {
npath, npath,
Filename, Filename,
CustomDir, CustomDir,
} from "@yarnpkg/fslib"; } from '@yarnpkg/fslib'
import { ZipFS, ZipOpenFS } from "@yarnpkg/libzip"; import { ZipFS, ZipOpenFS } from '@yarnpkg/libzip'
import { resolve, toNamespacedPath } from "path"; import { resolve, toNamespacedPath } from 'path'
import { constants } from "fs"; import { constants } from 'fs'
export type SnapshotZipFSOptions = { export type SnapshotZipFSOptions = {
baseFs: FakeFS<PortablePath>; baseFs: FakeFS<PortablePath>
libzip: Libzip | (() => Libzip); libzip: Libzip | (() => Libzip)
zipFs: ZipFS; zipFs: ZipFS
root: string; root: string
}; }
const uniqBy = ( const uniqBy = (arr: Array<string | Dirent>, pick: (...arg: any[]) => any) => {
arr: Array<string | Dirent<PortablePath>>, const seen = new Set()
pick: (...arg: any[]) => any
) => {
const seen = new Set();
return arr.filter((x) => { return arr.filter((x) => {
const key = pick(x); const key = pick(x)
if (seen.has(key)) return false; if (seen.has(key)) return false
seen.add(key); seen.add(key)
return true; return true
}); })
}; }
function uniqReaddir(arr: Array<string | Dirent<PortablePath>>) { function uniqReaddir(arr: Array<string | Dirent>) {
return uniqBy(arr, (s: string | Dirent<PortablePath>) => return uniqBy(arr, (s: string | Dirent) => (typeof s === 'string' ? s : s.name))
typeof s === "string" ? s : s.name
);
} }
export class SnapshotZipFS extends BasePortableFakeFS { export class SnapshotZipFS extends BasePortableFakeFS {
zipFs: ZipFS; zipFs: ZipFS
baseFs: FakeFS<PortablePath>; baseFs: FakeFS<PortablePath>
root: string; root: string
magic: number; magic: number
constructor(opts: SnapshotZipFSOptions) { constructor(opts: SnapshotZipFSOptions) {
super(); super()
this.zipFs = opts.zipFs; this.zipFs = opts.zipFs
this.baseFs = opts.baseFs; this.baseFs = opts.baseFs
this.root = opts.root; this.root = opts.root
this.magic = 0x2a << 24; this.magic = 0x2a << 24
} }
private readonly fdMap: Map<number, [ZipFS, number]> = new Map(); private readonly fdMap: Map<number, [ZipFS, number]> = new Map()
private nextFd = 3; private nextFd = 3
async makeCallPromise<T>( async makeCallPromise<T>(
p: FSPath<PortablePath>, p: FSPath<PortablePath>,
@@ -64,41 +59,38 @@ export class SnapshotZipFS extends BasePortableFakeFS {
accept: (zipFS: ZipFS, zipInfo: { subPath: PortablePath }) => Promise<T>, accept: (zipFS: ZipFS, zipInfo: { subPath: PortablePath }) => Promise<T>,
{ requireSubpath = true }: { requireSubpath?: boolean } = {} { requireSubpath = true }: { requireSubpath?: boolean } = {}
): Promise<T> { ): Promise<T> {
if (typeof p !== "string") return await discard(); if (typeof p !== 'string') return await discard()
const normalizedP = this.resolve(p); const normalizedP = this.resolve(p)
const zipInfo = this.findZip(normalizedP); const zipInfo = this.findZip(normalizedP)
if (!zipInfo) return await discard(); if (!zipInfo) return await discard()
if (requireSubpath && zipInfo.subPath === "/") return await discard(); if (requireSubpath && zipInfo.subPath === '/') return await discard()
return await accept(this.zipFs, zipInfo); return await accept(this.zipFs, zipInfo)
} }
makeCallSync<T>( makeCallSync<T>(
p: FSPath<PortablePath>, p: FSPath<PortablePath>,
discard: () => T, discard: () => T,
accept: ( accept: (zipFS: ZipFS, zipInfo: { subPath: PortablePath; archivePath: string }) => T,
zipFS: ZipFS,
zipInfo: { subPath: PortablePath; archivePath: string }
) => T,
{ requireSubpath = true }: { requireSubpath?: boolean } = {} { requireSubpath = true }: { requireSubpath?: boolean } = {}
): T { ): T {
if (typeof p !== "string") return discard(); if (typeof p !== 'string') return discard()
const normalizedP = this.resolve(p); const normalizedP = this.resolve(p)
const zipInfo = this.findZip(normalizedP); const zipInfo = this.findZip(normalizedP)
if (!zipInfo) return discard(); if (!zipInfo) return discard()
if (requireSubpath && zipInfo.subPath === "/") return discard(); if (requireSubpath && zipInfo.subPath === '/') return discard()
return accept(this.zipFs, { archivePath: "", ...zipInfo }); return accept(this.zipFs, { archivePath: '', ...zipInfo })
} }
async realpathPromise(p: PortablePath) { async realpathPromise(p: PortablePath) {
return await this.realpathSync(p); return await this.realpathSync(p)
} }
realpathSync(p: PortablePath) { realpathSync(p: PortablePath) {
@@ -107,29 +99,26 @@ export class SnapshotZipFS extends BasePortableFakeFS {
() => this.baseFs.realpathSync(p), () => this.baseFs.realpathSync(p),
(zipFs, { subPath }) => { (zipFs, { subPath }) => {
if (zipFs.lstatSync(subPath).isSymbolicLink()) { if (zipFs.lstatSync(subPath).isSymbolicLink()) {
return zipFs.realpathSync(subPath); return zipFs.realpathSync(subPath)
} else { } else {
// return the original path in case it wasn't under /snapshot, e.g. if it was for a node module - otherwise the node module parent path is the wrong one (and other things resolve relative to that) // return the original path in case it wasn't under /snapshot, e.g. if it was for a node module - otherwise the node module parent path is the wrong one (and other things resolve relative to that)
return p; return p
} }
} }
); )
} }
findZip(p: PortablePath) { findZip(p: PortablePath) {
p = this.resolve(p); p = this.resolve(p)
const snapshotPP = npath.toPortablePath("/snapshot"); const snapshotPP = npath.toPortablePath('/snapshot')
const pathsToTry = Array.from( const pathsToTry = Array.from(
new Set([ new Set([
p, p,
resolve("/snapshot", p), resolve('/snapshot', p),
ppath.resolve( ppath.resolve(
snapshotPP, snapshotPP,
npath.toPortablePath( npath.toPortablePath(
npath.relative( npath.relative(npath.fromPortablePath(this.root), npath.fromPortablePath(p))
npath.fromPortablePath(this.root),
npath.fromPortablePath(p)
)
) )
), ),
ppath.resolve( ppath.resolve(
@@ -141,50 +130,19 @@ export class SnapshotZipFS extends BasePortableFakeFS {
) )
) )
), ),
ppath.resolve(
snapshotPP,
npath.toPortablePath(
npath.relative(
npath.fromPortablePath(process.cwd()),
npath.fromPortablePath(p)
)
)
),
ppath.resolve(
snapshotPP,
npath.toPortablePath(
npath.relative(
toNamespacedPath(npath.fromPortablePath(process.cwd())),
toNamespacedPath(npath.fromPortablePath(p))
)
)
),
]) ])
); )
for (const path of pathsToTry) { for (const path of pathsToTry) {
const portablePath = npath.toPortablePath(path); const portablePath = npath.toPortablePath(path)
if (this.zipFs.existsSync(portablePath)) { if (this.zipFs.existsSync(portablePath)) {
return { return {
subPath: portablePath, subPath: portablePath,
};
}
}
// ESM imports often omit extensions (e.g. `import './lib/array'`).
// If the path has no extension, try .js / .mjs / .cjs fallbacks.
const basename = ppath.basename(p);
if (!ppath.extname(basename)) {
for (const path of pathsToTry) {
for (const ext of [".js", ".mjs", ".cjs"] as const) {
const candidate = npath.toPortablePath(path + ext);
if (this.zipFs.existsSync(candidate)) {
return { subPath: candidate };
}
} }
} }
} }
} }
async copyFilePromise(sourceP: PortablePath, destP: PortablePath, flags = 0) { async copyFilePromise(sourceP: PortablePath, destP: PortablePath, flags: number = 0) {
const fallback = async ( const fallback = async (
sourceFs: FakeFS<PortablePath>, sourceFs: FakeFS<PortablePath>,
sourceP: PortablePath, sourceP: PortablePath,
@@ -193,49 +151,40 @@ export class SnapshotZipFS extends BasePortableFakeFS {
) => { ) => {
if ((flags & constants.COPYFILE_FICLONE_FORCE) !== 0) if ((flags & constants.COPYFILE_FICLONE_FORCE) !== 0)
throw Object.assign( throw Object.assign(
new Error( new Error(`EXDEV: cross-device clone not permitted, copyfile '${sourceP}' -> ${destP}'`),
`EXDEV: cross-device clone not permitted, copyfile '${sourceP}' -> ${destP}'`
),
{ code: `EXDEV` } { code: `EXDEV` }
); )
if ( if (flags & constants.COPYFILE_EXCL && (await this.existsPromise(sourceP)))
flags & constants.COPYFILE_EXCL &&
(await this.existsPromise(sourceP))
)
throw Object.assign( throw Object.assign(
new Error( new Error(`EEXIST: file already exists, copyfile '${sourceP}' -> '${destP}'`),
`EEXIST: file already exists, copyfile '${sourceP}' -> '${destP}'`
),
{ code: `EEXIST` } { code: `EEXIST` }
); )
let content; let content
try { try {
content = await sourceFs.readFilePromise(sourceP); content = await sourceFs.readFilePromise(sourceP)
} catch (error) { } catch (error) {
throw Object.assign( throw Object.assign(
new Error( new Error(`EINVAL: invalid argument, copyfile '${sourceP}' -> '${destP}'`),
`EINVAL: invalid argument, copyfile '${sourceP}' -> '${destP}'`
),
{ code: `EINVAL` } { code: `EINVAL` }
); )
} }
await destFs.writeFilePromise(destP, content); await destFs.writeFilePromise(destP, content)
}; }
return await this.makeCallPromise( return await this.makeCallPromise(
sourceP, sourceP,
async () => { async () => {
return await this.baseFs.copyFilePromise(sourceP, destP, flags); return await this.baseFs.copyFilePromise(sourceP, destP, flags)
}, },
async (zipFsS, { subPath: subPathS }) => { async (zipFsS, { subPath: subPathS }) => {
return await fallback(zipFsS, subPathS, this.baseFs, destP); return await fallback(zipFsS, subPathS, this.baseFs, destP)
} }
); )
} }
copyFileSync(sourceP: PortablePath, destP: PortablePath, flags = 0) { copyFileSync(sourceP: PortablePath, destP: PortablePath, flags: number = 0) {
const fallback = ( const fallback = (
sourceFs: FakeFS<PortablePath>, sourceFs: FakeFS<PortablePath>,
sourceP: PortablePath, sourceP: PortablePath,
@@ -244,239 +193,203 @@ export class SnapshotZipFS extends BasePortableFakeFS {
) => { ) => {
if ((flags & constants.COPYFILE_FICLONE_FORCE) !== 0) if ((flags & constants.COPYFILE_FICLONE_FORCE) !== 0)
throw Object.assign( throw Object.assign(
new Error( new Error(`EXDEV: cross-device clone not permitted, copyfile '${sourceP}' -> ${destP}'`),
`EXDEV: cross-device clone not permitted, copyfile '${sourceP}' -> ${destP}'`
),
{ code: `EXDEV` } { code: `EXDEV` }
); )
if (flags & constants.COPYFILE_EXCL && this.existsSync(sourceP)) if (flags & constants.COPYFILE_EXCL && this.existsSync(sourceP))
throw Object.assign( throw Object.assign(
new Error( new Error(`EEXIST: file already exists, copyfile '${sourceP}' -> '${destP}'`),
`EEXIST: file already exists, copyfile '${sourceP}' -> '${destP}'`
),
{ code: `EEXIST` } { code: `EEXIST` }
); )
let content; let content
try { try {
content = sourceFs.readFileSync(sourceP); content = sourceFs.readFileSync(sourceP)
} catch (error) { } catch (error) {
throw Object.assign( throw Object.assign(
new Error( new Error(`EINVAL: invalid argument, copyfile '${sourceP}' -> '${destP}'`),
`EINVAL: invalid argument, copyfile '${sourceP}' -> '${destP}'`
),
{ code: `EINVAL` } { code: `EINVAL` }
); )
} }
destFs.writeFileSync(destP, content); destFs.writeFileSync(destP, content)
}; }
return this.makeCallSync( return this.makeCallSync(
sourceP, sourceP,
() => { () => {
return this.baseFs.copyFileSync(sourceP, destP, flags); return this.baseFs.copyFileSync(sourceP, destP, flags)
}, },
(zipFsS, { subPath: subPathS }) => { (zipFsS, { subPath: subPathS }) => {
return fallback(zipFsS, subPathS, this.baseFs, destP); return fallback(zipFsS, subPathS, this.baseFs, destP)
} }
); )
} }
async readdirPromise(p: PortablePath): Promise<Array<Filename>>; async readdirPromise(p: PortablePath): Promise<Array<Filename>>
async readdirPromise( async readdirPromise(
p: PortablePath, p: PortablePath,
opts: { withFileTypes: false } | null opts: { withFileTypes: false } | null
): Promise<Array<Filename>>; ): Promise<Array<Filename>>
async readdirPromise( async readdirPromise(p: PortablePath, opts: { withFileTypes: true }): Promise<Array<Dirent>>
p: PortablePath,
opts: { withFileTypes: true }
): Promise<Array<Dirent<PortablePath>>>;
async readdirPromise( async readdirPromise(
p: PortablePath, p: PortablePath,
opts: { withFileTypes: boolean } opts: { withFileTypes: boolean }
): Promise<Array<Filename> | Array<Dirent<PortablePath>>>; ): Promise<Array<Filename> | Array<Dirent>>
async readdirPromise( async readdirPromise(
p: PortablePath, p: PortablePath,
opts?: { withFileTypes?: boolean } | null opts?: { withFileTypes?: boolean } | null
): Promise<Array<string | Dirent<PortablePath>>> { ): Promise<Array<string | Dirent>> {
const fallback = async () => { const fallback = async () => {
return await this.baseFs.readdirPromise(p, opts as any); return await this.baseFs.readdirPromise(p, opts as any)
}; }
return await this.makeCallPromise( return await this.makeCallPromise(
p, p,
fallback, fallback,
async (zipFs, { subPath }) => { async (zipFs, { subPath }) => {
const fallbackPaths: Array<string | Dirent<PortablePath>> = const fallbackPaths: Array<string | Dirent> = await fallback().catch(() => [])
await fallback().catch(() => []);
return Promise.resolve( return Promise.resolve(
uniqReaddir( uniqReaddir(fallbackPaths.concat(await zipFs.readdirPromise(subPath, opts as any)))
fallbackPaths.concat( )
await zipFs.readdirPromise(subPath, opts as any)
)
)
);
}, },
{ {
requireSubpath: false, requireSubpath: false,
} }
); )
} }
readdirSync(p: PortablePath): Array<Filename>; readdirSync(p: PortablePath): Array<Filename>
readdirSync( readdirSync(p: PortablePath, opts: { withFileTypes: false } | null): Array<Filename>
p: PortablePath, readdirSync(p: PortablePath, opts: { withFileTypes: true }): Array<Dirent>
opts: { withFileTypes: false } | null readdirSync(p: PortablePath, opts: { withFileTypes: boolean }): Array<Filename> | Array<Dirent>
): Array<Filename>; readdirSync(p: PortablePath, opts?: { withFileTypes?: boolean } | null): Array<string | Dirent> {
readdirSync(
p: PortablePath,
opts: { withFileTypes: true }
): Array<Dirent<PortablePath>>;
readdirSync(
p: PortablePath,
opts: { withFileTypes: boolean }
): Array<Filename> | Array<Dirent<PortablePath>>;
readdirSync(
p: PortablePath,
opts?: { withFileTypes?: boolean } | null
): Array<string | Dirent<PortablePath>> {
const fallback = () => { const fallback = () => {
return this.baseFs.readdirSync(p, opts as any); return this.baseFs.readdirSync(p, opts as any)
}; }
return this.makeCallSync( return this.makeCallSync(
p, p,
fallback, fallback,
(zipFs, { subPath }) => { (zipFs, { subPath }) => {
let fallbackPaths: Array<string | Dirent<PortablePath>> = []; let fallbackPaths: Array<string | Dirent> = []
try { try {
fallbackPaths = fallback(); fallbackPaths = fallback()
} catch (e) { } } catch (e) {}
return fallbackPaths.concat( return fallbackPaths.concat(uniqReaddir(zipFs.readdirSync(subPath, opts as any)))
uniqReaddir(zipFs.readdirSync(subPath, opts as any))
);
}, },
{ {
requireSubpath: false, requireSubpath: false,
} }
); )
} }
async mkdirPromise(p: PortablePath, opts?: MkdirOptions) { async mkdirPromise(p: PortablePath, opts?: MkdirOptions) {
return await this.baseFs.mkdirPromise(p, opts); return await this.baseFs.mkdirPromise(p, opts)
} }
mkdirSync(p: PortablePath, opts?: MkdirOptions) { mkdirSync(p: PortablePath, opts?: MkdirOptions) {
return this.baseFs.mkdirSync(p, opts); return this.baseFs.mkdirSync(p, opts)
} }
async rmdirPromise(p: PortablePath, opts?: RmdirOptions) { async rmdirPromise(p: PortablePath, opts?: RmdirOptions) {
return await this.baseFs.rmdirPromise(p, opts); return await this.baseFs.rmdirPromise(p, opts)
} }
rmdirSync(p: PortablePath, opts?: RmdirOptions) { rmdirSync(p: PortablePath, opts?: RmdirOptions) {
return this.baseFs.rmdirSync(p, opts); return this.baseFs.rmdirSync(p, opts)
}
async rmPromise(p: PortablePath, opts?: any) {
return await this.baseFs.rmPromise(p, opts);
}
rmSync(p: PortablePath, opts?: any) {
return this.baseFs.rmSync(p, opts);
} }
async opendirPromise(p: PortablePath, opts?: OpendirOptions) { async opendirPromise(p: PortablePath, opts?: OpendirOptions) {
return this.opendirSync(p, opts); return this.opendirSync(p, opts)
} }
opendirSync(p: PortablePath, opts?: OpendirOptions) { opendirSync(p: PortablePath, opts?: OpendirOptions) {
const zipInfo = this.findZip(p); const zipInfo = this.findZip(p)
let zipFsDir: Dir<PortablePath> | null = null; let zipFsDir: Dir<PortablePath> | null = null
if (zipInfo) { if (zipInfo) {
zipFsDir = this.zipFs.opendirSync(zipInfo.subPath); zipFsDir = this.zipFs.opendirSync(zipInfo.subPath)
} }
let realFsDir: Dir<PortablePath> | null = null; let realFsDir: Dir<PortablePath> | null = null
try { try {
realFsDir = this.baseFs.opendirSync(p); realFsDir = this.baseFs.opendirSync(p)
} catch (e) { } catch (e) {
if (!zipFsDir) throw e; if (!zipFsDir) throw e
} }
const seen = new Set(); const seen = new Set()
const nextDirent = () => { const nextDirent = () => {
const entry = realFsDir?.readSync() || zipFsDir?.readSync(); const entry = realFsDir?.readSync() || zipFsDir?.readSync()
if (entry && !seen.has(entry.name)) { if (entry && !seen.has(entry.name)) {
seen.add(entry.name); seen.add(entry.name)
return entry; return entry
} }
return null; return null
}; }
const onClose = () => { const onClose = () => {
zipFsDir?.closeSync(); zipFsDir?.closeSync()
realFsDir?.closeSync(); realFsDir?.closeSync()
}; }
return new CustomDir(p, nextDirent, { onClose }); return new CustomDir(p, nextDirent, { onClose })
} }
accessPromise = ZipOpenFS.prototype.accessPromise; accessPromise = ZipOpenFS.prototype.accessPromise
accessSync = ZipOpenFS.prototype.accessSync; accessSync = ZipOpenFS.prototype.accessSync
appendFilePromise = ZipOpenFS.prototype.appendFilePromise; appendFilePromise = ZipOpenFS.prototype.appendFilePromise
appendFileSync = ZipOpenFS.prototype.appendFileSync; appendFileSync = ZipOpenFS.prototype.appendFileSync
chmodPromise = ZipOpenFS.prototype.chmodPromise; chmodPromise = ZipOpenFS.prototype.chmodPromise
chmodSync = ZipOpenFS.prototype.chmodSync; chmodSync = ZipOpenFS.prototype.chmodSync
fchmodPromise = ZipOpenFS.prototype.fchmodPromise; fchmodPromise = ZipOpenFS.prototype.fchmodPromise
fchmodSync = ZipOpenFS.prototype.fchmodSync; fchmodSync = ZipOpenFS.prototype.fchmodSync
chownPromise = ZipOpenFS.prototype.chownPromise; chownPromise = ZipOpenFS.prototype.chownPromise
chownSync = ZipOpenFS.prototype.chownSync; chownSync = ZipOpenFS.prototype.chownSync
fchownPromise = ZipOpenFS.prototype.fchownPromise; fchownPromise = ZipOpenFS.prototype.fchownPromise
fchownSync = ZipOpenFS.prototype.fchownSync; fchownSync = ZipOpenFS.prototype.fchownSync
closePromise = ZipOpenFS.prototype.closePromise; closePromise = ZipOpenFS.prototype.closePromise
closeSync = ZipOpenFS.prototype.closeSync; closeSync = ZipOpenFS.prototype.closeSync
createReadStream = ZipOpenFS.prototype.createReadStream; createReadStream = ZipOpenFS.prototype.createReadStream
createWriteStream = ZipOpenFS.prototype.createWriteStream; createWriteStream = ZipOpenFS.prototype.createWriteStream
existsPromise = ZipOpenFS.prototype.existsPromise; existsPromise = ZipOpenFS.prototype.existsPromise
existsSync = ZipOpenFS.prototype.existsSync; existsSync = ZipOpenFS.prototype.existsSync
fstatPromise = ZipOpenFS.prototype.fstatPromise; fstatPromise = ZipOpenFS.prototype.fstatPromise
fstatSync = ZipOpenFS.prototype.fstatSync; fstatSync = ZipOpenFS.prototype.fstatSync
getExtractHint = ZipOpenFS.prototype.getExtractHint; getExtractHint = ZipOpenFS.prototype.getExtractHint
getRealPath = ZipOpenFS.prototype.getRealPath; getRealPath = ZipOpenFS.prototype.getRealPath
linkPromise = ZipOpenFS.prototype.linkPromise; linkPromise = ZipOpenFS.prototype.linkPromise
linkSync = ZipOpenFS.prototype.linkSync; linkSync = ZipOpenFS.prototype.linkSync
lstatPromise = ZipOpenFS.prototype.lstatPromise; lstatPromise = ZipOpenFS.prototype.lstatPromise
lstatSync = ZipOpenFS.prototype.lstatSync; lstatSync = ZipOpenFS.prototype.lstatSync
openPromise = ZipOpenFS.prototype.openPromise; openPromise = ZipOpenFS.prototype.openPromise
openSync = ZipOpenFS.prototype.openSync; openSync = ZipOpenFS.prototype.openSync
readFilePromise = ZipOpenFS.prototype.readFilePromise; readFilePromise = ZipOpenFS.prototype.readFilePromise
readFileSync = ZipOpenFS.prototype.readFileSync; readFileSync = ZipOpenFS.prototype.readFileSync
readlinkPromise = ZipOpenFS.prototype.readlinkPromise; readlinkPromise = ZipOpenFS.prototype.readlinkPromise
readlinkSync = ZipOpenFS.prototype.readlinkSync; readlinkSync = ZipOpenFS.prototype.readlinkSync
readPromise = ZipOpenFS.prototype.readPromise; readPromise = ZipOpenFS.prototype.readPromise
readSync = ZipOpenFS.prototype.readSync; readSync = ZipOpenFS.prototype.readSync
renamePromise = ZipOpenFS.prototype.renamePromise; renamePromise = ZipOpenFS.prototype.renamePromise
renameSync = ZipOpenFS.prototype.renameSync; renameSync = ZipOpenFS.prototype.renameSync
resolve = ZipOpenFS.prototype.resolve; resolve = ZipOpenFS.prototype.resolve
statPromise = ZipOpenFS.prototype.statPromise; statPromise = ZipOpenFS.prototype.statPromise
statSync = ZipOpenFS.prototype.statSync; statSync = ZipOpenFS.prototype.statSync
symlinkPromise = ZipOpenFS.prototype.symlinkPromise; symlinkPromise = ZipOpenFS.prototype.symlinkPromise
symlinkSync = ZipOpenFS.prototype.symlinkSync; symlinkSync = ZipOpenFS.prototype.symlinkSync
truncatePromise = ZipOpenFS.prototype.truncatePromise; truncatePromise = ZipOpenFS.prototype.truncatePromise
truncateSync = ZipOpenFS.prototype.truncateSync; truncateSync = ZipOpenFS.prototype.truncateSync
ftruncatePromise = ZipOpenFS.prototype.ftruncatePromise; ftruncatePromise = ZipOpenFS.prototype.ftruncatePromise
ftruncateSync = ZipOpenFS.prototype.ftruncateSync; ftruncateSync = ZipOpenFS.prototype.ftruncateSync
unlinkPromise = ZipOpenFS.prototype.unlinkPromise; unlinkPromise = ZipOpenFS.prototype.unlinkPromise
unlinkSync = ZipOpenFS.prototype.unlinkSync; unlinkSync = ZipOpenFS.prototype.unlinkSync
unwatchFile = ZipOpenFS.prototype.unwatchFile; unwatchFile = ZipOpenFS.prototype.unwatchFile
utimesPromise = ZipOpenFS.prototype.utimesPromise; utimesPromise = ZipOpenFS.prototype.utimesPromise
utimesSync = ZipOpenFS.prototype.utimesSync; utimesSync = ZipOpenFS.prototype.utimesSync
lutimesPromise = ZipOpenFS.prototype.utimesPromise; lutimesPromise = ZipOpenFS.prototype.utimesPromise
lutimesSync = ZipOpenFS.prototype.utimesSync; lutimesSync = ZipOpenFS.prototype.utimesSync
watch = ZipOpenFS.prototype.watch; watch = ZipOpenFS.prototype.watch
watchFile = ZipOpenFS.prototype.watchFile; watchFile = ZipOpenFS.prototype.watchFile
writeFilePromise = ZipOpenFS.prototype.writeFilePromise; writeFilePromise = ZipOpenFS.prototype.writeFilePromise
writeFileSync = ZipOpenFS.prototype.writeFileSync; writeFileSync = ZipOpenFS.prototype.writeFileSync
writePromise = ZipOpenFS.prototype.writePromise; writePromise = ZipOpenFS.prototype.writePromise
writeSync = ZipOpenFS.prototype.writeSync; writeSync = ZipOpenFS.prototype.writeSync
// @ts-ignore // @ts-ignore
remapFd = ZipOpenFS.prototype.remapFd; remapFd = ZipOpenFS.prototype.remapFd
} }
+17 -25
View File
@@ -1,28 +1,20 @@
{ if (true) {
const __nexe_patches = (process.nexe = { patches: {} }).patches; const __nexe_patches = (process.nexe = { patches: {} }).patches
const slice = [].slice; const slice = [].slice
const __nexe_noop_patch = function (original) { const __nexe_noop_patch = function (original) {
const args = slice.call(arguments, 1); const args = slice.call(arguments, 1)
return original.apply(this, args); return original.apply(this, args)
}; }
const __nexe_patch = function (obj, method, patch) { const __nexe_patch = function (obj, method, patch) {
const original = obj[method]; const original = obj[method]
if (!original) return; if (!original) return
__nexe_patches[method] = patch; __nexe_patches[method] = patch
obj[method] = function () { obj[method] = function() {
const args = [original].concat(slice.call(arguments)); const args = [original].concat(slice.call(arguments))
return __nexe_patches[method].apply(this, args); return __nexe_patches[method].apply(this, args)
}; }
}; }
__nexe_patch( __nexe_patch((process).binding('fs'), 'internalModuleReadFile', __nexe_noop_patch)
process.binding("fs"), __nexe_patch((process).binding('fs'), 'internalModuleReadJSON', __nexe_noop_patch)
"internalModuleReadFile", __nexe_patch((process).binding('fs'), 'internalModuleStat', __nexe_noop_patch)
__nexe_noop_patch,
);
__nexe_patch(
process.binding("fs"),
"internalModuleReadJSON",
__nexe_noop_patch,
);
__nexe_patch(process.binding("fs"), "internalModuleStat", __nexe_noop_patch);
} }
+22 -25
View File
@@ -1,54 +1,51 @@
import { relative } from "path"; import { relative } from 'path'
import { Readable } from "stream"; import { Readable } from 'stream'
import { File } from "resolve-dependencies"; import { File } from 'resolve-dependencies'
import archiver from "archiver"; import archiver from 'archiver'
function makeRelativeToZip(cwd: string, path: string) { function makeRelativeToZip(cwd: string, path: string) {
return "/snapshot/" + relative(cwd, path); return '/snapshot/' + relative(cwd, path)
} }
export function toStream(content: Buffer | string) { export function toStream(content: Buffer | string) {
const readable = new Readable({ const readable = new Readable({
read() { read() {
this.push(content); this.push(content)
this.push(null); this.push(null)
}, },
}); })
return readable; return readable
} }
export class Bundle { export class Bundle {
cwd: string; cwd: string
files = new Set<string>(); files = new Set<string>()
zip: any; zip: any
constructor({ cwd }: { cwd: string } = { cwd: process.cwd() }) { constructor({ cwd }: { cwd: string } = { cwd: process.cwd() }) {
this.cwd = cwd; this.cwd = cwd
this.zip = archiver("zip"); this.zip = archiver('zip')
} }
get list() { get list() {
return Array.from(this.files); return Array.from(this.files)
} }
public addResource( public addResource(absoluteFileName: string, content?: File | Buffer | string) {
absoluteFileName: string, const destPath = makeRelativeToZip(this.cwd, absoluteFileName)
content?: File | Buffer | string
) {
const destPath = makeRelativeToZip(this.cwd, absoluteFileName);
if (!this.files.has(destPath)) { if (!this.files.has(destPath)) {
if (content == null) { if (content == null) {
this.zip.file(absoluteFileName, { name: destPath }); this.zip.file(absoluteFileName, { name: destPath })
} else { } else {
this.zip.append(content, { name: destPath }); this.zip.append(content, { name: destPath })
} }
this.files.add(destPath); this.files.add(destPath)
} }
} }
public finalize() { public finalize() {
return this.zip.finalize(); return this.zip.finalize()
} }
public toStream(): Readable { public toStream(): Readable {
return this.zip; return this.zip
} }
} }
+69 -344
View File
@@ -1,387 +1,112 @@
import { ZipFS, getLibzipSync } from "@yarnpkg/libzip"; import { ZipFS, getLibzipSync } from '@yarnpkg/libzip'
import { patchFs, PosixFS, NodeFS } from "@yarnpkg/fslib"; import { patchFs, npath, PosixFS, NodeFS } from '@yarnpkg/fslib'
import { SnapshotZipFS } from "./SnapshotZipFS"; import { SnapshotZipFS } from './SnapshotZipFS'
import * as assert from "assert"; import * as assert from 'assert'
import * as constants from "constants"; import * as constants from 'constants'
import { dirname, relative, sep } from "path"; import { dirname } from 'path'
export interface NexeHeader { export interface NexeHeader {
blobPath: string; blobPath: string
layout: { layout: {
resourceStart: number; resourceStart: number
resourceSize: number; resourceSize: number
contentSize: number; contentSize: number
contentStart: number; contentStart: number
}; }
} }
let originalFsMethods: any = null; let originalFsMethods: any = null
let lazyRestoreFs = () => {}; let lazyRestoreFs = () => {}
const patches = (process as any).nexe.patches || {}; const patches = (process as any).nexe.patches || {}
const originalPatches = { ...patches }; const originalPatches = { ...patches }
delete (process as any).nexe; delete (process as any).nexe
function shimFs(binary: NexeHeader, fs: typeof import("fs") = require("fs")) { function shimFs(binary: NexeHeader, fs: typeof import('fs') = require('fs')) {
if (originalFsMethods !== null) { if (originalFsMethods !== null) {
return; return
} }
originalFsMethods = Object.assign({}, fs); originalFsMethods = Object.assign({}, fs)
const realFs: typeof fs = { ...fs }; const realFs: typeof fs = { ...fs }
const nodeFs = new NodeFS(realFs); const nodeFs = new NodeFS(realFs)
const blob = Buffer.allocUnsafe(binary.layout.resourceSize); const blob = Buffer.allocUnsafe(binary.layout.resourceSize)
const blobFd = realFs.openSync(binary.blobPath, "r"); const blobFd = realFs.openSync(binary.blobPath, 'r')
const bytesRead = realFs.readSync( const bytesRead = realFs.readSync(
blobFd, blobFd,
blob, blob,
0, 0,
binary.layout.resourceSize, binary.layout.resourceSize,
binary.layout.resourceStart binary.layout.resourceStart
); )
assert.equal(bytesRead, binary.layout.resourceSize); assert.equal(bytesRead, binary.layout.resourceSize)
const zipFs = new ZipFS(blob, { readOnly: true }); const zipFs = new ZipFS(blob, { readOnly: true })
const snapshotZipFS = new SnapshotZipFS({ const snapshotZipFS = new SnapshotZipFS({
libzip: getLibzipSync(), libzip: getLibzipSync(),
zipFs, zipFs,
baseFs: nodeFs, baseFs: nodeFs,
root: dirname(process.argv[0]), // executable directory = original project root root: dirname(process.argv[0]),
}); })
const posixSnapshotZipFs = new PosixFS(snapshotZipFS); const posixSnapshotZipFs = new PosixFS(snapshotZipFS)
patchFs(fs, posixSnapshotZipFs); // patches global fs patchFs(fs, posixSnapshotZipFs)
// Capture the project root for path mapping let log = (_: string) => true
const projectRoot = dirname(process.argv[0]); if ((process.env.DEBUG || '').toLowerCase().includes('nexe:require')) {
const drive = projectRoot.slice(0, 2); // e.g., "C:"
// Enable logging with DEBUG=nexe:require
let log = (_: string) => true;
if ((process.env.DEBUG || "").toLowerCase().includes("nexe:require")) {
process.stderr.write( process.stderr.write(
`[nexe] - FILES ${JSON.stringify( // @ts-ignore
// @ts-ignore - accessing private property for debugging `[nexe] - FILES ${JSON.stringify(Array.from(zipFs.entries.keys()), null, 4)}\n`
Array.from(zipFs.entries.keys()), )
null,
4
)}\n`
);
process.stderr.write( process.stderr.write(
`[nexe] - DIRECTORIES ${JSON.stringify( // @ts-ignore
// @ts-ignore - accessing private property for debugging `[nexe] - DIRECTORIES ${JSON.stringify(Array.from(zipFs.listings.keys()), null, 4)}\n`
Array.from(zipFs.listings.keys()), )
null, log = (text: string) => {
4 return process.stderr.write(`[nexe] - ${text}\n`)
)}\n` }
);
log = (text: string) => process.stderr.write(`[nexe] - ${text}\n`);
} }
/**
* Convert a Windows path to the snapshot POSIX path.
* Handles:
* - Paths under the original project root (e.g., C:\Users\oxmc\Desktop\src\entry.js → /snapshot/src/entry.js)
* - Paths that have been converted to C:\snapshot\... (from _findPath returning /snapshot/...)
* - Already POSIX /snapshot/... paths (return as-is)
*/
function toSnapshotPath(filePath: string): string {
// Already a POSIX snapshot path
if (filePath.startsWith("/snapshot/")) {
return filePath;
}
// Handle Windows-style snapshot path: e.g., C:\snapshot\src\entry.js
if (filePath.startsWith(drive + "\\snapshot\\")) {
return "/snapshot/" + filePath.slice(drive.length + 9).replace(/\\/g, "/");
}
// Path under original project root
if (filePath.startsWith(projectRoot)) {
const rel = relative(projectRoot, filePath);
return "/snapshot/" + rel.replace(/\\/g, "/");
}
// Not mappable return unchanged (will likely fail)
return filePath;
}
// --- Internal Module Patches (unconditionally assigned) ---
function internalModuleReadFile(this: any, original: any, ...args: any[]) { function internalModuleReadFile(this: any, original: any, ...args: any[]) {
// Strip Windows extended-length path prefix if present log(`internalModuleReadFile ${args[0]}`)
let filePath = typeof args[0] === "string" ? args[0] : "";
if (filePath.startsWith("\\\\?\\")) {
filePath = filePath.slice(4);
}
const mappedPath = toSnapshotPath(filePath);
log(`internalModuleReadFile ${filePath} -> ${mappedPath}`);
try { try {
// Use the patched fs (which already sees the zip) return posixSnapshotZipFs.readFileSync(args[0], 'utf-8')
return fs.readFileSync(mappedPath, "utf-8");
} catch (e) { } catch (e) {
return ""; return ''
} }
} }
if (patches.internalModuleReadFile) {
patches.internalModuleReadFile = internalModuleReadFile; patches.internalModuleReadFile = internalModuleReadFile
}
// internalModuleReadJSON should return a string (or undefined) in Node 22 let returningArray: boolean
patches.internalModuleReadJSON = function (this: any, original: any, ...args: any[]) { patches.internalModuleReadJSON = function (this: any, original: any, ...args: any[]) {
const content = internalModuleReadFile.call(this, original, ...args); if (returningArray == null) returningArray = Array.isArray(original.call(this, ''))
return content === "" ? undefined : content; const res = internalModuleReadFile.call(this, original, ...args)
}; return returningArray && !Array.isArray(res)
? res === ''
// Handle Node 22+ signature: first arg is context (non-string), second is path ? []
function internalModuleStat(this: any, original: any, ...args: any[]) { : [res, /"(main|name|type|exports|imports)"/.test(res)]
const target = args[0]; : res
let statPath: string; }
patches.internalModuleStat = function (this: any, original: any, ...args: any[]) {
// Node 22+: args[0] is context (often a buffer), args[1] is the path string log(`internalModuleStat ${args[0]}`)
if (typeof target === "number") {
// file descriptor case unchanged
try {
originalFsMethods.fstatSync(target);
return 0;
} catch (e) {
return -constants.ENOENT;
}
}
if (typeof target === "string") {
// Node <22 or when a string is passed directly
statPath = target;
} else {
// Node 22+: path is the second argument
statPath = args[1];
}
if (typeof statPath !== "string") {
// Unexpected: fallback to original
return original.call(this, ...args);
}
// Strip Windows extended-length prefix
if (statPath.startsWith("\\\\?\\")) {
statPath = statPath.slice(4);
}
const mappedPath = toSnapshotPath(statPath);
log(`internalModuleStat ${statPath} -> ${mappedPath}`);
try { try {
const stat = fs.statSync(mappedPath); const stat = posixSnapshotZipFs.statSync(args[0])
return stat.isDirectory() ? 1 : 0; if (stat.isDirectory()) return 1
return 0
} catch (e) { } catch (e) {
// VFS has no entry — check the real filesystem at the original path. return -constants.ENOENT
// This handles dynamically created files (e.g. written via the patched fs)
// that don't exist in the zip.
try {
const realStat = originalFsMethods.statSync(statPath);
return realStat.isDirectory() ? 1 : 0;
} catch (e2) {
return -constants.ENOENT;
}
} }
} }
patches.internalModuleStat = internalModuleStat;
// --- Patch Module._findPath with a correct exports resolver ---
// Helper to check if a path exists and is a file
function isFile(p: string): boolean {
try {
const stat = fs.statSync(p);
return stat.isFile();
} catch {
return false;
}
}
// Recursive exports resolver following Node's conditional exports
function resolvePackageExports(exports: any, conditions: string[]): string | null {
if (typeof exports === "string") {
return exports;
}
if (!exports || typeof exports !== "object") {
return null;
}
// Handle "." subpath first (for bare module, we always start with ".")
const subExports = exports["."] ?? exports;
return resolveExportsObject(subExports, conditions);
}
function resolveExportsObject(obj: any, conditions: string[]): string | null {
if (typeof obj === "string") {
return obj;
}
if (!obj || typeof obj !== "object") {
return null;
}
// Iterate over keys in order; if a key matches a condition, take that branch
for (const key of Object.keys(obj)) {
if (conditions.includes(key)) {
const value = obj[key];
if (typeof value === "string") {
return value;
}
if (value && typeof value === "object") {
const nested = resolveExportsObject(value, conditions);
if (nested) return nested;
}
}
}
// If no matching condition, try "default" if not already tried
if (!conditions.includes("default") && obj.default) {
const def = obj.default;
if (typeof def === "string") return def;
if (def && typeof def === "object") {
return resolveExportsObject(def, conditions);
}
}
return null;
}
function resolveFromPkg(
pkg: any,
basePath: string,
request: string,
nodePath: any,
logFn: (s: string) => any,
isFileFn: (p: string) => boolean,
fsMod: typeof import("fs")
): string | null {
// 1. exports field
if (pkg.exports) {
const conditions = ["require", "node", "default"];
const exportTarget = resolvePackageExports(pkg.exports, conditions);
if (exportTarget) {
const candidate = nodePath.posix.join(basePath, request, exportTarget);
logFn(`_findPath exports target: ${candidate}`);
if (isFileFn(candidate)) return candidate;
for (const ext of [".js", ".json", ".node"]) {
const withExt = candidate + ext;
if (isFileFn(withExt)) return withExt;
}
}
}
// 2. main field
let main = "index.js";
if (typeof pkg.main === "string") main = pkg.main;
if (main.startsWith("./")) main = main.slice(2);
if (!main || main === ".") main = "index.js";
if (main.endsWith("/")) main += "index.js";
const mainPath = nodePath.posix.join(basePath, request, main);
logFn(`_findPath main: ${mainPath}`);
try {
const st = fsMod.statSync(mainPath);
if (st.isFile()) return mainPath;
if (st.isDirectory()) {
const idxPath = nodePath.posix.join(mainPath, "index.js");
if (isFileFn(idxPath)) return idxPath;
}
} catch (_) {}
for (const ext of [".js", ".json", ".node"]) {
const withExt = mainPath + ext;
if (isFileFn(withExt)) return withExt;
}
// 3. index.js fallback only when main not explicitly set
if (typeof pkg.main !== "string") {
const fallbackIndex = nodePath.posix.join(basePath, request, "index.js");
if (isFileFn(fallbackIndex)) return fallbackIndex;
}
// 4. dist subdirectory (axios-style)
const distIndex = nodePath.posix.join(basePath, request, "dist", "index.js");
if (isFileFn(distIndex)) return distIndex;
const distMain = nodePath.posix.join(basePath, request, "dist", request + ".js");
if (isFileFn(distMain)) return distMain;
return null;
}
let _origFindPath: ((...args: any[]) => any) | null = null;
try {
const Module = require("module");
const nodePath = require("path");
const capturedOrigFindPath = (Module as any)._findPath;
if (typeof capturedOrigFindPath === "function") {
_origFindPath = capturedOrigFindPath;
(Module as any)._findPath = function nexeFindPath(this: any, ...args: any[]) {
const request: string = args[0];
// For bare module specifiers, try VFS first. Node 22 removed
// internalModuleReadJSON from process.binding('fs'), so _findPath can
// no longer read package.json from the VFS and falls back to index.js.
// Our code reads package.json via the patched fs.readFileSync which works.
if (request && request[0] !== "." && !nodePath.isAbsolute(request)) {
const basePath = "/snapshot/node_modules";
const pkgJsonPath = nodePath.posix.join(basePath, request, "package.json");
let pkg: any;
try {
pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8") as string);
} catch (_) {
pkg = null;
}
if (pkg) {
const vfsResult = resolveFromPkg(pkg, basePath, request, nodePath, log, isFile, fs);
if (vfsResult) {
log(`_findPath VFS resolved ${request} -> ${vfsResult}`);
return vfsResult;
}
}
}
// Fall back to original for everything else (real fs, relative, absolute)
let result: any;
try {
result = capturedOrigFindPath.apply(this, args);
} catch (_) {
result = false;
}
if (result) return result;
// Secondary VFS fallback for paths that couldn't be resolved above
// (e.g. bare specifier where package.json wasn't found in /snapshot)
if (!request || request[0] === "." || nodePath.isAbsolute(request)) {
return result;
}
// Use snapshot node_modules as the base for all bare modules
const basePath2 = "/snapshot/node_modules";
const pkgJsonPath2 = nodePath.posix.join(basePath2, request, "package.json");
log(`_findPath secondary snapshot check: ${pkgJsonPath2}`);
let pkg2: any;
try {
pkg2 = JSON.parse(fs.readFileSync(pkgJsonPath2, "utf8") as string);
} catch (_) {
return result;
}
return resolveFromPkg(pkg2, basePath2, request, nodePath, log, isFile, fs) ?? result;
};
}
} catch (_) {
// Patching _findPath failed, ignore
}
lazyRestoreFs = () => { lazyRestoreFs = () => {
Object.assign(fs, originalFsMethods); Object.assign(fs, originalFsMethods)
Object.assign(patches, originalPatches); Object.assign(patches, originalPatches)
if (_origFindPath !== null) { lazyRestoreFs = () => {}
try { }
const Module = require("module");
(Module as any)._findPath = _origFindPath;
} catch (_) {}
_origFindPath = null;
}
lazyRestoreFs = () => {};
};
} }
function restoreFs() { function restoreFs() {
lazyRestoreFs(); lazyRestoreFs()
} }
export { shimFs, restoreFs }; export { shimFs, restoreFs }
+38 -38
View File
@@ -1,65 +1,65 @@
import colors from "chalk"; import colors from 'chalk'
import ora from "ora"; import ora from 'ora'
const frameLength = 120; const frameLength = 120
export interface LogStep { export interface LogStep {
modify(text: string, color?: string): void; modify(text: string, color?: string): void
log(text: string, color?: string): void; log(text: string, color?: string): void
pause(): void; pause(): void
resume(): void; resume(): void
} }
export class Logger { export class Logger {
public verbose: boolean; public verbose: boolean
private silent: boolean; private silent: boolean
private ora: any; private ora: any
private modify: Function; private modify: Function
public write: (text: string, color?: string) => void; public write: (text: string, color?: string) => void
constructor(level: "verbose" | "silent" | "info") { constructor(level: 'verbose' | 'silent' | 'info') {
this.verbose = level === "verbose"; this.verbose = level === 'verbose'
this.silent = level === "silent"; this.silent = level === 'silent'
if (!this.silent) { if (!this.silent) {
this.ora = ora({ this.ora = ora({
text: "Starting...", text: 'Starting...',
color: "blue", color: 'blue',
spinner: "dots", spinner: 'dots',
}); })
this.ora.stop(); this.ora.stop()
} }
const noop = () => { }; const noop = () => {}
this.modify = this.silent ? noop : this._modify.bind(this); this.modify = this.silent ? noop : this._modify.bind(this)
this.write = this.silent ? noop : this._write.bind(this); this.write = this.silent ? noop : this._write.bind(this)
} }
flush() { flush() {
!this.silent && this.ora.succeed(); !this.silent && this.ora.succeed()
return new Promise((resolve) => setTimeout(resolve, frameLength)); return new Promise((resolve) => setTimeout(resolve, frameLength))
} }
_write(update: string, color = "green") { _write(update: string, color = 'green') {
this.ora.succeed().text = (colors as any)[color](update); this.ora.succeed().text = (colors as any)[color](update)
this.ora.start(); this.ora.start()
} }
_modify(update: string, color = this.ora.color) { _modify(update: string, color = this.ora.color) {
this.ora.text = update; this.ora.text = update
this.ora.color = color; this.ora.color = color
} }
step(text: string, method = "succeed"): LogStep { step(text: string, method: string = 'succeed'): LogStep {
if (this.silent) { if (this.silent) {
return { modify() { }, log() { }, pause() { }, resume() { } }; return { modify() {}, log() {}, pause() {}, resume() {} }
} }
if (!this.ora.id) { if (!this.ora.id) {
this.ora.start().text = text; this.ora.start().text = text
if (method !== "succeed") { if (method !== 'succeed') {
this.ora[method](); this.ora[method]()
} }
} else { } else {
this.ora[method]().text = text; this.ora[method]().text = text
this.ora.start(); this.ora.start()
} }
return { return {
@@ -67,6 +67,6 @@ export class Logger {
log: this.verbose ? this.write : this.modify, log: this.verbose ? this.write : this.modify,
pause: () => this.ora.stopAndPersist(), pause: () => this.ora.stopAndPersist(),
resume: () => this.ora.start(), resume: () => this.ora.start(),
} as LogStep; } as LogStep
} }
} }
+24 -25
View File
@@ -1,14 +1,15 @@
import { compose } from "app-builder"; import { EOL } from 'os'
import { NexeCompiler } from "./compiler"; import { compose } from 'app-builder'
import { normalizeOptions, NexeOptions, NexePatch } from "./options"; import { NexeCompiler, NexeError } from './compiler'
import resource from "./steps/resource"; import { normalizeOptions, NexeOptions, NexePatch } from './options'
import clean from "./steps/clean"; import resource from './steps/resource'
import cli from "./steps/cli"; import clean from './steps/clean'
import bundle from "./steps/bundle"; import cli from './steps/cli'
import download from "./steps/download"; import bundle from './steps/bundle'
import shim from "./steps/shim"; import download from './steps/download'
import artifacts from "./steps/artifacts"; import shim from './steps/shim'
import patches from "./patches"; import artifacts from './steps/artifacts'
import patches from './patches'
async function compile( async function compile(
compilerOptions?: Partial<NexeOptions>, compilerOptions?: Partial<NexeOptions>,
@@ -16,11 +17,11 @@ async function compile(
) { ) {
let error: Error | null = null, let error: Error | null = null,
options: NexeOptions | null = null, options: NexeOptions | null = null,
compiler: NexeCompiler | null = null; compiler: NexeCompiler | null = null
try { try {
options = normalizeOptions(compilerOptions); options = normalizeOptions(compilerOptions)
compiler = new NexeCompiler(options); compiler = new NexeCompiler(options)
await compose( await compose(
clean, clean,
resource, resource,
@@ -28,23 +29,21 @@ async function compile(
bundle, bundle,
shim, shim,
download, download,
options.build options.build ? [artifacts, ...patches, ...(options.patches as NexePatch[])] : [],
? [artifacts, ...patches, ...(options.patches as NexePatch[])]
: [],
options.plugins as NexePatch[] options.plugins as NexePatch[]
)(compiler); )(compiler)
} catch (e: any) { } catch (e: any) {
error = e; error = e
} }
if (error) { if (error) {
compiler && compiler.quit(error); compiler && compiler.quit(error)
if (callback) return callback(error); if (callback) return callback(error)
return Promise.reject(error); return Promise.reject(error)
} }
if (callback) callback(null); if (callback) callback(null)
} }
export { compile, NexeCompiler }; export { compile, NexeCompiler }
export { argv, version, NexeOptions, help } from "./options"; export { argv, version, NexeOptions, help } from './options'
+156 -175
View File
@@ -1,72 +1,61 @@
import parseArgv from "minimist"; import parseArgv from 'minimist'
import { NexeCompiler, NexeError } from "./compiler"; import { NexeCompiler, NexeError } from './compiler'
import { isWindows, STDIN_FLAG } from "./util"; import { isWindows, STDIN_FLAG } from './util'
import { basename, extname, join, isAbsolute, resolve } from "path"; import { basename, extname, join, isAbsolute, resolve } from 'path'
import { getTarget, NexeTarget } from "./target"; import { getTarget, NexeTarget } from './target'
import { EOL, homedir } from "os"; import { EOL, homedir } from 'os'
import chalk from "chalk"; import chalk from 'chalk'
import { resolveSync } from "resolve-dependencies"; import { resolveSync } from 'resolve-dependencies'
const caw = require("caw"); const caw = require('caw')
const c = const c = process.platform === 'win32' ? chalk.constructor({ enabled: false }) : chalk
process.platform === "win32" ? chalk.constructor({ enabled: false }) : chalk;
export const version = "{{ version }}"; export const version = '{{ version }}'
export interface NexePatch { export interface NexePatch {
(compiler: NexeCompiler, next: () => Promise<void>): Promise<void>; (compiler: NexeCompiler, next: () => Promise<void>): Promise<void>
}
export interface ModuleBundleRule {
include?: string[];
exclude?: string[];
}
export interface BundleRules {
[moduleName: string]: ModuleBundleRule;
} }
export interface NexeOptions { export interface NexeOptions {
build: boolean; build: boolean
input: string; input: string
output: string; output: string
targets: (string | NexeTarget)[]; targets: (string | NexeTarget)[]
name: string; name: string
remote: string; remote: string
asset: string; asset: string
cwd: string; cwd: string
fs: boolean | string[]; fs: boolean | string[]
flags: string[]; flags: string[]
configure: string[]; configure: string[]
vcBuild: string[]; vcBuild: string[]
make: string[]; make: string[]
snapshot?: string; snapshot?: string
resources: string[]; resources: string[]
temp: string; temp: string
rc: { [key: string]: string }; rc: { [key: string]: string }
enableNodeCli: boolean; enableNodeCli: boolean
bundle: boolean | string; bundle: boolean | string
bundleRules?: BundleRules; patches: (string | NexePatch)[]
patches: (string | NexePatch)[]; plugins: (string | NexePatch)[]
plugins: (string | NexePatch)[]; native: any
native: any; mangle: boolean
mangle: boolean; ghToken: string
ghToken: string; sourceUrl?: string
sourceUrl?: string; enableStdIn?: boolean
enableStdIn?: boolean; python?: string
python?: string; loglevel: 'info' | 'silent' | 'verbose'
loglevel: "info" | "silent" | "verbose"; silent?: boolean
silent?: boolean; fakeArgv?: boolean
fakeArgv?: boolean; verbose?: boolean
verbose?: boolean; info?: boolean
info?: boolean; ico?: string
ico?: string; debugBundle?: boolean
debugBundle?: boolean; warmup?: string
warmup?: string; clean?: boolean
clean?: boolean;
/** /**
* Api Only * Api Only
*/ */
downloadOptions: any; downloadOptions: any
} }
const defaults = { const defaults = {
@@ -77,37 +66,37 @@ const defaults = {
mangle: true, mangle: true,
make: [], make: [],
targets: [], targets: [],
vcBuild: isWindows ? ["nosign", "release"] : [], vcBuild: isWindows ? ['nosign', 'release'] : [],
enableNodeCli: false, enableNodeCli: false,
build: false, build: false,
bundle: true, bundle: true,
patches: [], patches: [],
plugins: [], plugins: [],
remote: `https://github.com/oxmc/nexe/releases/download/v${require('../package.json').version}/`, remote: 'https://github.com/nexe/nexe/releases/download/v3.3.3/',
}; }
const alias = { const alias = {
i: "input", i: 'input',
o: "output", o: 'output',
v: "version", v: 'version',
a: "asset", a: 'asset',
t: "target", t: 'target',
b: "build", b: 'build',
n: "name", n: 'name',
r: "resource", r: 'resource',
p: "python", p: 'python',
f: "flag", f: 'flag',
c: "configure", c: 'configure',
m: "make", m: 'make',
h: "help", h: 'help',
l: "loglevel", l: 'loglevel',
"fake-argv": "fakeArgv", 'fake-argv': 'fakeArgv',
"gh-token": "ghToken", 'gh-token': 'ghToken',
}; }
const argv = parseArgv(process.argv, { alias, default: { ...defaults } }); const argv = parseArgv(process.argv, { alias, default: { ...defaults } })
let help = ` let help = `
${c.bold("nexe <entry-file> [options]")} ${c.bold('nexe <entry-file> [options]')}
${c.underline.bold("Options:")} ${c.underline.bold('Options:')}
-i --input -- application entry point -i --input -- application entry point
-o --output -- path to output file -o --output -- path to output file
@@ -117,7 +106,7 @@ ${c.bold("nexe <entry-file> [options]")}
--remote -- alternate location (URL) to download pre-built base (nexe) binaries from --remote -- alternate location (URL) to download pre-built base (nexe) binaries from
--plugin -- extend nexe runtime behavior --plugin -- extend nexe runtime behavior
${c.underline.bold("Building from source:")} ${c.underline.bold('Building from source:')}
-b --build -- build from source -b --build -- build from source
-p --python -- python3 (as python) executable path -p --python -- python3 (as python) executable path
@@ -132,7 +121,7 @@ ${c.bold("nexe <entry-file> [options]")}
--sourceUrl -- pass an alternate source (node.tar.gz) url --sourceUrl -- pass an alternate source (node.tar.gz) url
--enableNodeCli -- enable node cli enforcement (blocks app cli) --enableNodeCli -- enable node cli enforcement (blocks app cli)
${c.underline.bold("Other options:")} ${c.underline.bold('Other options:')}
--bundle -- custom bundling module with 'createBundle' export --bundle -- custom bundling module with 'createBundle' export
--temp -- temp file storage default '~/.nexe' --temp -- temp file storage default '~/.nexe'
@@ -142,11 +131,11 @@ ${c.bold("nexe <entry-file> [options]")}
--silent -- disable logging --silent -- disable logging
--verbose -- set logging to verbose --verbose -- set logging to verbose
-* variable key name * option can be used more than once`.trim(); -* variable key name * option can be used more than once`.trim()
help = EOL + help + EOL; help = EOL + help + EOL
function flatten(...args: any[]): string[] { function flatten(...args: any[]): string[] {
return ([] as string[]).concat(...args).filter((x) => x); return ([] as string[]).concat(...args).filter((x) => x)
} }
/** /**
@@ -159,48 +148,48 @@ function extractCliMap(match: RegExp, options: any) {
return Object.keys(options) return Object.keys(options)
.filter((x) => match.test(x)) .filter((x) => match.test(x))
.reduce((map: any, option: any) => { .reduce((map: any, option: any) => {
const key = option.split("-")[1]; const key = option.split('-')[1]
map[key] = options[option]; map[key] = options[option]
delete options[option]; delete options[option]
return map; return map
}, {} as any); }, {} as any)
} }
function extractLogLevel(options: NexeOptions) { function extractLogLevel(options: NexeOptions) {
if (options.loglevel) return options.loglevel; if (options.loglevel) return options.loglevel
if (options.silent) return "silent"; if (options.silent) return 'silent'
if (options.verbose) return "verbose"; if (options.verbose) return 'verbose'
return "info"; return 'info'
} }
function isName(name: string) { function isName(name: string) {
return name && name !== "index" && name !== STDIN_FLAG; return name && name !== 'index' && name !== STDIN_FLAG
} }
function extractName(options: NexeOptions) { function extractName(options: NexeOptions) {
let name = options.name; let name = options.name
//try and use the input filename as the output filename if its not index //try and use the input filename as the output filename if its not index
if (!isName(name) && typeof options.input === "string") { if (!isName(name) && typeof options.input === 'string') {
name = basename(options.input).replace(extname(options.input), ""); name = basename(options.input).replace(extname(options.input), '')
} }
//try and use the directory as the filename //try and use the directory as the filename
if (!isName(name) && basename(options.cwd)) { if (!isName(name) && basename(options.cwd)) {
name = basename(options.cwd); name = basename(options.cwd)
} }
return name.replace(/\.exe$/, ""); return name.replace(/\.exe$/, '')
} }
function padRelative(input: string) { function padRelative(input: string) {
let prefix = ""; let prefix = ''
if (!input.startsWith(".")) { if (!input.startsWith('.')) {
prefix = "./"; prefix = './'
} }
return prefix + input; return prefix + input
} }
function isEntryFile(filename?: string): filename is string { function isEntryFile(filename?: string): filename is string {
return Boolean(filename && !isAbsolute(filename)); return Boolean(filename && !isAbsolute(filename))
} }
export function resolveEntry( export function resolveEntry(
@@ -209,117 +198,109 @@ export function resolveEntry(
maybeEntry: string | undefined, maybeEntry: string | undefined,
bundle: boolean | string bundle: boolean | string
) { ) {
let result = null; let result = null
if (input === "-" || maybeEntry === "-") { if (input === '-' || maybeEntry === '-') {
return STDIN_FLAG; return STDIN_FLAG
} }
if (input && isAbsolute(input)) { if (input && isAbsolute(input)) {
return input; return input
} }
if (input) { if (input) {
const inputPath = padRelative(input); const inputPath = padRelative(input)
result = resolveSync(cwd, inputPath); result = resolveSync(cwd, inputPath)
} }
if (isEntryFile(maybeEntry) && (!result || !result.absPath)) { if (isEntryFile(maybeEntry) && (!result || !result.absPath)) {
const inputPath = padRelative(maybeEntry); const inputPath = padRelative(maybeEntry)
result = resolveSync(cwd, inputPath); result = resolveSync(cwd, inputPath)
} }
if ( if (!process.stdin.isTTY && (!result || !result.absPath) && bundle === defaults.bundle) {
!process.stdin.isTTY && return STDIN_FLAG
(!result || !result.absPath) &&
bundle === defaults.bundle
) {
return STDIN_FLAG;
} }
if (!result || !result.absPath) { if (!result || !result.absPath) {
result = resolveSync(cwd, "."); result = resolveSync(cwd, '.')
} }
if (!result.absPath) { if (!result.absPath) {
throw new NexeError(`Entry file "${input || ""}" not found!`); throw new NexeError(`Entry file "${input || ''}" not found!`)
} }
return result.absPath; return result.absPath
} }
function isCli(options?: Partial<NexeOptions>) { function isCli(options?: Partial<NexeOptions>) {
return argv === options; return argv === options
} }
function normalizeOptions(input?: Partial<NexeOptions>): NexeOptions { function normalizeOptions(input?: Partial<NexeOptions>): NexeOptions {
const options = Object.assign({}, defaults, input) as NexeOptions; const options = Object.assign({}, defaults, input) as NexeOptions
const opts = options as any; const opts = options as any
const cwd = (options.cwd = resolve(options.cwd)); const cwd = (options.cwd = resolve(options.cwd))
options.temp = options.temp options.temp = options.temp
? resolve(cwd, options.temp) ? resolve(cwd, options.temp)
: process.env.NEXE_TEMP || join(homedir(), ".nexe"); : process.env.NEXE_TEMP || join(homedir(), '.nexe')
const maybeEntry = isCli(input) ? argv._[argv._.length - 1] : undefined; const maybeEntry = isCli(input) ? argv._[argv._.length - 1] : undefined
options.input = resolveEntry(options.input, cwd, maybeEntry, options.bundle); options.input = resolveEntry(options.input, cwd, maybeEntry, options.bundle)
options.enableStdIn = isCli(input) && options.input === STDIN_FLAG; options.enableStdIn = isCli(input) && options.input === STDIN_FLAG
options.name = extractName(options); options.name = extractName(options)
options.loglevel = extractLogLevel(options); options.loglevel = extractLogLevel(options)
options.flags = flatten(opts.flag, options.flags); options.flags = flatten(opts.flag, options.flags)
options.targets = flatten(opts.target, options.targets).map(getTarget); options.targets = flatten(opts.target, options.targets).map(getTarget)
if (!options.targets.length) { if (!options.targets.length) {
options.targets.push(getTarget()); options.targets.push(getTarget())
} }
options.ghToken = options.ghToken || process.env.GITHUB_TOKEN || ""; options.ghToken = options.ghToken || process.env.GITHUB_TOKEN || ''
options.make = flatten(isWindows ? options.vcBuild : options.make); options.make = flatten(isWindows ? options.vcBuild : options.make)
options.configure = flatten(options.configure); options.configure = flatten(options.configure)
options.resources = flatten(opts.resource, options.resources); options.resources = flatten(opts.resource, options.resources)
if (!options.remote.endsWith("/")) { if (!options.remote.endsWith('/')) {
options.remote += "/"; options.remote += '/'
} }
options.downloadOptions = options.downloadOptions || {}; options.downloadOptions = options.downloadOptions || {}
options.downloadOptions.headers = options.downloadOptions.headers || {}; options.downloadOptions.headers = options.downloadOptions.headers || {}
options.downloadOptions.headers["User-Agent"] = options.downloadOptions.headers['User-Agent'] = 'nexe (https://www.npmjs.com/package/nexe)'
"nexe (https://www.npmjs.com/package/nexe)";
options.downloadOptions.agent = process.env.HTTPS_PROXY options.downloadOptions.agent = process.env.HTTPS_PROXY
? caw(process.env.HTTPS_PROXY, { protocol: "https" }) ? caw(process.env.HTTPS_PROXY, { protocol: 'https' })
: options.downloadOptions.agent || require("https").globalAgent; : options.downloadOptions.agent || require('https').globalAgent
options.downloadOptions.rejectUnauthorized = process.env options.downloadOptions.rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED
.NODE_TLS_REJECT_UNAUTHORIZED
? false ? false
: true; : true
options.rc = options.rc || extractCliMap(/^rc-.*/, options); options.rc = options.rc || extractCliMap(/^rc-.*/, options)
options.output = options.output =
(options.targets[0] as NexeTarget).platform === "windows" (options.targets[0] as NexeTarget).platform === 'windows'
? `${(options.output || options.name).replace(/\.exe$/, "")}.exe` ? `${(options.output || options.name).replace(/\.exe$/, '')}.exe`
: `${options.output || options.name}`; : `${options.output || options.name}`
options.output = resolve(cwd, options.output); options.output = resolve(cwd, options.output)
const requireDefault = (x: string) => { const requireDefault = (x: string) => {
if (typeof x === "string") { if (typeof x === 'string') {
return require(x).default; return require(x).default
} }
return x; return x
}; }
options.mangle = "mangle" in opts ? opts.mangle : true; options.mangle = 'mangle' in opts ? opts.mangle : true
options.plugins = flatten(opts.plugin, options.plugins).map(requireDefault); options.plugins = flatten(opts.plugin, options.plugins).map(requireDefault)
options.patches = flatten(opts.patch, options.patches).map(requireDefault); options.patches = flatten(opts.patch, options.patches).map(requireDefault)
if ((!options.mangle && !options.bundle) || options.patches.length) { if ((!options.mangle && !options.bundle) || options.patches.length) {
options.build = true; options.build = true
} }
if (options.build) { if (options.build) {
const { arch } = options.targets[0] as NexeTarget; const { arch } = options.targets[0] as NexeTarget
if (isWindows) { if (isWindows) {
options.make = Array.from(new Set(options.make.concat(arch))); options.make = Array.from(new Set(options.make.concat(arch)))
} else { } else {
options.configure = Array.from( options.configure = Array.from(new Set(options.configure.concat([`--dest-cpu=${arch}`])))
new Set(options.configure.concat([`--dest-cpu=${arch}`]))
);
} }
} }
Object.keys(alias) Object.keys(alias)
.filter((k) => k !== "rc") .filter((k) => k !== 'rc')
.forEach((x) => delete opts[x]); .forEach((x) => delete opts[x])
return options; return options
} }
export { argv, normalizeOptions, help }; export { argv, normalizeOptions, help }
+25 -67
View File
@@ -1,57 +1,34 @@
const fs = require("fs"), const fs = require('fs'),
fd = fs.openSync(process.execPath, "r"), fd = fs.openSync(process.execPath, 'r'),
stat = fs.fstatSync(fd), stat = fs.statSync(process.execPath),
tailSize = Math.min(stat.size, 16000), tailSize = Math.min(stat.size, 16000),
tailWindow = Buffer.alloc(tailSize), tailWindow = Buffer.from(Array(tailSize))
match = "<nexe" + "~~sentinel>",
matchLength = match.length,
lastBuffer = Buffer.alloc(matchLength + 32);
let offset = stat.size, fs.readSync(fd, tailWindow, 0, tailSize, stat.size - tailSize)
footerPosition = -1,
footerPositionOffset = 0,
footer: Buffer;
while (true) {
const bytesRead = fs.readSync(fd, tailWindow, 0, tailSize, offset - tailSize);
if (bytesRead === 0) break;
const combinedBuffers = Buffer.concat([tailWindow, lastBuffer]);
footerPosition = combinedBuffers.indexOf(match);
if (footerPosition > -1) {
footer = combinedBuffers.slice(footerPosition, footerPosition + 32);
break;
}
if (offset < 0) break;
tailWindow.copy(lastBuffer);
offset = offset - bytesRead;
}
const footerPosition = tailWindow.indexOf('<nexe~~sentinel>')
if (footerPosition == -1) { if (footerPosition == -1) {
throw "Invalid Nexe binary"; throw 'Invalid Nexe binary'
} }
const contentSize = footer!.readDoubleLE(16), const footer = tailWindow.slice(footerPosition, footerPosition + 32),
resourceSize = footer!.readDoubleLE(24), contentSize = footer.readDoubleLE(16),
contentStart = resourceSize = footer.readDoubleLE(24),
offset - tailSize + footerPosition - resourceSize - contentSize, contentStart = stat.size - tailSize + footerPosition - resourceSize - contentSize,
resourceStart = contentStart + contentSize; resourceStart = contentStart + contentSize
Object.defineProperty( Object.defineProperty(
process, process,
"__nexe", '__nexe',
(function () { (function () {
let nexeHeader: any = null; let nexeHeader: any = null
return { return {
get: function () { get: function () {
return nexeHeader; return nexeHeader
}, },
set: function (value: any) { set: function (value: any) {
if (nexeHeader) { if (nexeHeader) {
throw new Error("This property is readonly"); throw new Error('This property is readonly')
} }
nexeHeader = Object.assign({}, value, { nexeHeader = Object.assign({}, value, {
blobPath: process.execPath, blobPath: process.execPath,
@@ -62,38 +39,19 @@ Object.defineProperty(
resourceSize, resourceSize,
resourceStart, resourceStart,
}, },
}); })
Object.freeze(nexeHeader); Object.freeze(nexeHeader)
}, },
enumerable: false, enumerable: false,
configurable: false, configurable: false,
}; }
})() })()
); )
const contentBuffer = Buffer.alloc(contentSize), const contentBuffer = Buffer.from(Array(contentSize)),
Module = require("module"); Module = require('module')
fs.readSync(fd, contentBuffer, 0, contentSize, contentStart); fs.readSync(fd, contentBuffer, 0, contentSize, contentStart)
fs.closeSync(fd); fs.closeSync(fd)
// Node 24 changed finalizeResolution in the ESM loader to no longer catch ENOENT new Module(process.execPath, null)._compile(contentBuffer.toString(), process.execPath)
// from realpathSync. VFS paths don't exist on real FS, so binding.lstat (called
// internally by realpathSync) throws ENOENT and crashes ESM imports.
// The ESM resolver captures realpathSync via destructuring at load time, so
// patchFs (which runs later) doesn't reach it. We install an ENOENT-tolerant
// wrapper here, before any ESM code can load, so the resolver captures our version.
{
const _origRpSync: any = fs.realpathSync;
const nexeRpSync = function(p: string, opts?: any): string {
try { return _origRpSync(p, opts); }
catch (e: any) { if (e?.code === 'ENOENT') return p; throw e; }
};
(nexeRpSync as any).native = _origRpSync.native;
(fs as any).realpathSync = nexeRpSync;
}
new Module(process.execPath, null)._compile(
contentBuffer.slice(1).toString(),
process.execPath
);
+12 -13
View File
@@ -1,18 +1,17 @@
import { NexeCompiler } from "../compiler"; import { NexeCompiler } from '../compiler'
export default async function buildFixes( export default async function buildFixes(compiler: NexeCompiler, next: () => Promise<void>) {
compiler: NexeCompiler, if (!compiler.target.version.startsWith('8.2')) {
next: () => Promise<void> return next()
) {
if (!compiler.target.version.startsWith("8.2")) {
return next();
} }
await compiler.replaceInFileAsync( const file = await compiler.readFileAsync('./tools/msvs/find_python.cmd')
"./tools/msvs/find_python.cmd",
"%p%python.exe -V 2>&1",
'"%p%python.exe" -V 2>&1'
);
return next(); await compiler.replaceInFileAsync(
'./tools/msvs/find_python.cmd',
'%p%python.exe -V 2>&1',
'"%p%python.exe" -V 2>&1'
)
return next()
} }
+26 -43
View File
@@ -1,61 +1,44 @@
import { NexeCompiler } from "../compiler"; import { NexeCompiler } from '../compiler'
import { semverGt } from "../util"; import { semverGt } from '../util'
export default async function disableNodeCli( export default async function disableNodeCli(compiler: NexeCompiler, next: () => Promise<void>) {
compiler: NexeCompiler,
next: () => Promise<void>
) {
if (compiler.options.enableNodeCli) { if (compiler.options.enableNodeCli) {
return next(); return next()
} }
if (semverGt(compiler.target.version, "18.99")) { if (semverGt(compiler.target.version, '11.6.0')) {
await compiler.replaceInFileAsync( await compiler.replaceInFileAsync(
"src/node.cc", 'src/node.cc',
/(?<!static ExitCode )ProcessGlobalArgsInternal\(argv[^;]*;/gm,
"ExitCode::kNoFailure;/*$&*/"
);
} else if (semverGt(compiler.target.version, "11.6.0")) {
await compiler.replaceInFileAsync(
"src/node.cc",
/(?<!int )ProcessGlobalArgs\(argv[^;]*;/gm, /(?<!int )ProcessGlobalArgs\(argv[^;]*;/gm,
"0;/*$&*/" '0;/*$&*/'
); )
} else if (semverGt(compiler.target.version, "10.9")) { } else if (semverGt(compiler.target.version, '10.9')) {
await compiler.replaceInFileAsync('src/node.cc', /(?<!void )ProcessArgv\(argv/g, '//$&')
} else if (semverGt(compiler.target.version, '9.999')) {
await compiler.replaceInFileAsync( await compiler.replaceInFileAsync(
"src/node.cc", 'src/node.cc',
/(?<!void )ProcessArgv\(argv/g, 'int i = 1; i < v8_argc; i++',
"//$&" 'int i = v8_argc; i < v8_argc; i++'
); )
} else if (semverGt(compiler.target.version, "9.999")) { let matches = 0
await compiler.replaceInFileAsync( await compiler.replaceInFileAsync('src/node.cc', /v8_argc > 1/g, (match) => {
"src/node.cc", if (matches++) {
"int i = 1; i < v8_argc; i++", return 'false'
"int i = v8_argc; i < v8_argc; i++"
);
let matches = 0;
await compiler.replaceInFileAsync(
"src/node.cc",
/v8_argc > 1/g,
(match) => {
if (matches++) {
return "false";
}
return match;
} }
); return match
})
} else { } else {
const nodeccMarker = "argv[index][0] =="; const nodeccMarker = 'argv[index][0] =='
await compiler.replaceInFileAsync( await compiler.replaceInFileAsync(
"src/node.cc", 'src/node.cc',
`${nodeccMarker} '-'`, `${nodeccMarker} '-'`,
// allow NODE_OPTIONS, introduced in 8.0 // allow NODE_OPTIONS, introduced in 8.0
semverGt(compiler.target.version, "7.99") semverGt(compiler.target.version, '7.99')
? `(${nodeccMarker} (is_env ? '-' : ']'))` ? `(${nodeccMarker} (is_env ? '-' : ']'))`
: `(${nodeccMarker} ']')` : `(${nodeccMarker} ']')`
); )
} }
return next(); return next()
} }
+8 -11
View File
@@ -1,19 +1,16 @@
import { NexeCompiler } from "../compiler"; import { NexeCompiler } from '../compiler'
export default async function flags( export default async function flags(compiler: NexeCompiler, next: () => Promise<void>) {
compiler: NexeCompiler, const nodeflags = compiler.options.flags
next: () => Promise<void>
) {
const nodeflags = compiler.options.flags;
if (!nodeflags.length) { if (!nodeflags.length) {
return next(); return next()
} }
await compiler.replaceInFileAsync( await compiler.replaceInFileAsync(
"node.gyp", 'node.gyp',
"'node_v8_options%': ''", "'node_v8_options%': ''",
`'node_v8_options%': '${nodeflags.join(" ")}'` `'node_v8_options%': '${nodeflags.join(' ')}'`
); )
return next(); return next()
} }
-23
View File
@@ -1,23 +0,0 @@
import { NexeCompiler } from '../compiler'
export default async function fsStat(compiler: NexeCompiler, next: () => Promise<void>) {
await compiler.replaceInFileAsync(
'lib/fs.js',
'function statSync(path, options = { bigint: false, throwIfNoEntry: true }) {',
`function statSync(path, options = { bigint: false, throwIfNoEntry: true }) {
// If Node passes a file descriptor, we must fstat the REAL fs
if (typeof path === 'number') {
try {
return fstatSync(path, options);
} catch (e) {
if (options.throwIfNoEntry) {
throw e;
}
return options.bigint ? BigInt(-1) : -1;
}
}
`
)
return next()
}
+8 -8
View File
@@ -1,21 +1,21 @@
import { NexeCompiler } from "../compiler"; import { NexeCompiler } from '../compiler'
export default async function nodeGyp( export default async function nodeGyp(
{ files, replaceInFileAsync }: NexeCompiler, { files, replaceInFileAsync }: NexeCompiler,
next: () => Promise<void> next: () => Promise<void>
) { ) {
await next(); await next()
const nodeGypMarker = "'lib/fs.js',"; const nodeGypMarker = "'lib/fs.js',"
await replaceInFileAsync( await replaceInFileAsync(
"node.gyp", 'node.gyp',
nodeGypMarker, nodeGypMarker,
` `
${nodeGypMarker} ${nodeGypMarker}
${files ${files
.filter((x) => x.filename.startsWith("lib")) .filter((x) => x.filename.startsWith('lib'))
.map((x) => `'${x.filename}'`) .map((x) => `'${x.filename}'`)
.toString()}, .toString()},
`.trim() `.trim()
); )
} }
+8 -14
View File
@@ -1,18 +1,12 @@
import { normalize } from "path"; import { normalize } from 'path'
import { readFileAsync } from "../util"; import { readFileAsync } from '../util'
import { NexeCompiler } from "../compiler"; import { NexeCompiler } from '../compiler'
export default async function ico( export default async function ico(compiler: NexeCompiler, next: () => Promise<void>) {
compiler: NexeCompiler, const iconFile = compiler.options.ico
next: () => Promise<void>
) {
const iconFile = compiler.options.ico;
if (!iconFile) { if (!iconFile) {
return next(); return next()
} }
await compiler.setFileContentsAsync( await compiler.setFileContentsAsync('src/res/node.ico', await readFileAsync(normalize(iconFile)))
"src/res/node.ico", return next()
await readFileAsync(normalize(iconFile))
);
return next();
} }
+10 -13
View File
@@ -1,15 +1,12 @@
import gyp from "./gyp"; import gyp from './gyp'
import bootNexe from "./third-party-main"; import bootNexe from './third-party-main'
import buildFixes from "./build-fixes"; import buildFixes from './build-fixes'
import cli from "./disable-node-cli"; import cli from './disable-node-cli'
import flags from "./flags"; import flags from './flags'
import ico from "./ico"; import ico from './ico'
import rc from "./node-rc"; import rc from './node-rc'
import snapshot from "./snapshot"; import snapshot from './snapshot'
import fsStat from "./fs-stat-debug";
// Patches are applied in order, so if a patch depends on another patch, it should be listed after the patch it depends on. const patches = [gyp, bootNexe, buildFixes, cli, flags, ico, rc, snapshot]
// For example, if a patch modifies the output of gyp, it should be listed after gyp in this array.
const patches = [gyp, bootNexe, buildFixes, cli, flags, ico, rc, snapshot];
export default patches; export default patches
+14 -17
View File
@@ -1,31 +1,28 @@
import { NexeCompiler } from "../compiler"; import { NexeCompiler } from '../compiler'
export default async function nodeRc( export default async function nodeRc(compiler: NexeCompiler, next: () => Promise<void>) {
compiler: NexeCompiler, const options = compiler.options.rc
next: () => Promise<void>
) {
const options = compiler.options.rc;
if (!options) { if (!options) {
return next(); return next()
} }
const file = await compiler.readFileAsync("src/res/node.rc"); const file = await compiler.readFileAsync('src/res/node.rc')
Object.keys(options).forEach((key) => { Object.keys(options).forEach((key) => {
let value = options[key]; let value = options[key]
const isVar = /^[A-Z_]+$/.test(value); const isVar = /^[A-Z_]+$/.test(value)
value = isVar ? value : `"${value}"`; value = isVar ? value : `"${value}"`
file.contents = file.contents file.contents = file.contents
.toString() .toString()
.replace(new RegExp(`VALUE "${key}",.*`), `VALUE "${key}", ${value}`); .replace(new RegExp(`VALUE "${key}",.*`), `VALUE "${key}", ${value}`)
}); })
["PRODUCTVERSION", "FILEVERSION"].forEach((x) => { ;['PRODUCTVERSION', 'FILEVERSION'].forEach((x) => {
if (options[x]) { if (options[x]) {
file.contents = file.contents file.contents = file.contents
.toString() .toString()
.replace(new RegExp(x + " .*$", "m"), `${x} ${options[x]}`); .replace(new RegExp(x + ' .*$', 'm'), `${x} ${options[x]}`)
} }
}); })
return next(); return next()
} }
+10 -15
View File
@@ -1,24 +1,19 @@
import { resolve } from "path"; import { resolve } from 'path'
import { NexeCompiler } from "../compiler"; import { NexeCompiler } from '../compiler'
import { semverGt } from "../util"; import { semverGt } from '../util'
export default async function ( export default async function (compiler: NexeCompiler, next: () => Promise<void>) {
compiler: NexeCompiler, const { snapshot, warmup, cwd } = compiler.options
next: () => Promise<void>
) {
const { snapshot, warmup, cwd } = compiler.options;
if (!snapshot) { if (!snapshot) {
return next(); return next()
} }
const variablePrefix = semverGt(compiler.target.version, "11.0.0") const variablePrefix = semverGt(compiler.target.version, '11.0.0') ? 'v8_' : ''
? "v8_"
: "";
await compiler.replaceInFileAsync( await compiler.replaceInFileAsync(
compiler.configureScript, compiler.configureScript,
"def configure_v8(o):", 'def configure_v8(o):',
`def configure_v8(o):\n o['variables']['${variablePrefix}embed_script'] = r'${resolve( `def configure_v8(o):\n o['variables']['${variablePrefix}embed_script'] = r'${resolve(
cwd, cwd,
snapshot snapshot
@@ -26,7 +21,7 @@ export default async function (
cwd, cwd,
warmup || snapshot warmup || snapshot
)}'` )}'`
); )
return next(); return next()
} }
+65 -78
View File
@@ -1,43 +1,40 @@
import { NexeCompiler } from "../compiler"; import { NexeCompiler } from '../compiler'
import { parse } from "meriyah"; import { parse } from 'meriyah'
import { wrap, semverGt } from "../util"; import { wrap, semverGt } from '../util'
function walkSome(node: any, visit: Function) { function walkSome(node: any, visit: Function) {
if (!node || typeof node.type !== "string" || node._visited) { if (!node || typeof node.type !== 'string' || node._visited) {
return false; return false
} }
visit(node); visit(node)
node._visited = true; node._visited = true
for (const childNode in node) { for (let childNode in node) {
const child = node[childNode]; const child = node[childNode]
if (Array.isArray(child)) { if (Array.isArray(child)) {
for (let i = 0; i < child.length; i++) { for (let i = 0; i < child.length; i++) {
if (walkSome(child[i], visit)) { if (walkSome(child[i], visit)) {
return true; return true
} }
} }
} else if (walkSome(child, visit)) { } else if (walkSome(child, visit)) {
return true; return true
} }
} }
return false; return false
} }
export default async function main( export default async function main(compiler: NexeCompiler, next: () => Promise<void>) {
compiler: NexeCompiler, let bootFile = 'lib/internal/bootstrap_node.js'
next: () => Promise<void> const { version } = compiler.target
) {
let bootFile = "lib/internal/bootstrap_node.js";
const { version } = compiler.target;
if (version.startsWith("4.")) { if (version.startsWith('4.')) {
bootFile = "src/node.js"; bootFile = 'src/node.js'
} else if (semverGt(version, "18.11.99")) { } else if (semverGt(version, '18.11.99')) {
bootFile = "lib/internal/process/pre_execution.js"; bootFile = 'lib/internal/process/pre_execution.js'
} else if (semverGt(version, "11.99")) { } else if (semverGt(version, '11.99')) {
bootFile = "lib/internal/bootstrap/pre_execution.js"; bootFile = 'lib/internal/bootstrap/pre_execution.js'
} else if (semverGt(version, "9.10.1")) { } else if (semverGt(version, '9.10.1')) {
bootFile = "lib/internal/bootstrap/node.js"; bootFile = 'lib/internal/bootstrap/node.js'
} }
const file = await compiler.readFileAsync(bootFile), const file = await compiler.readFileAsync(bootFile),
@@ -45,87 +42,77 @@ export default async function main(
next: true, next: true,
globalReturn: true, globalReturn: true,
loc: true, loc: true,
specDeviation: true,
}), }),
location = { start: { line: 0 } }; location = { start: { line: 0 } }
walkSome(ast, (node: any) => { walkSome(ast, (node: any) => {
if (!location.start.line && node.type === "BlockStatement") { if (!location.start.line && node.type === 'BlockStatement') {
//Find the first block statement and mark the location //Find the first block statement and mark the location
Object.assign(location, node.loc); Object.assign(location, node.loc)
return true; return true
} }
}); })
const fileLines = file.contents.toString().split("\n"); const fileLines = file.contents.toString().split('\n')
if (semverGt(version, "18.16.99")) { if (semverGt(version, '19.99')) {
await compiler.replaceInFileAsync( await compiler.replaceInFileAsync(
"lib/internal/modules/cjs/loader.js", 'lib/internal/modules/cjs/loader.js',
"'use strict';", "'use strict';",
"'use strict';\n" + '{{ file("lib/fs/bootstrap.js") }}' + "\n" "'use strict';\n" + '{{ file("lib/fs/bootstrap.js") }}' + '\n'
); )
fileLines.splice(location.start.line, 0, "expandArgv1 = false;"); fileLines.splice(location.start.line, 0, 'expandArgv1 = false;')
} else { } else {
fileLines.splice( fileLines.splice(
location.start.line, location.start.line,
0, 0,
'{{ file("lib/fs/bootstrap.js") }}' + '{{ file("lib/fs/bootstrap.js") }}' +
"\n" + '\n' +
(semverGt(version, "11.99") ? "expandArgv1 = false;\n" : "") (semverGt(version, '11.99') ? 'expandArgv1 = false;\n' : '')
); )
} }
file.contents = fileLines.join("\n"); file.contents = fileLines.join('\n')
if (semverGt(version, "11.99")) { if (semverGt(version, '11.99')) {
if (semverGt(version, "12.17.99")) { if (semverGt(version, '12.17.99')) {
await compiler.replaceInFileAsync( await compiler.replaceInFileAsync(
bootFile, bootFile,
"initializeFrozenIntrinsics();", 'initializeFrozenIntrinsics();',
"initializeFrozenIntrinsics();\n" + 'initializeFrozenIntrinsics();\n' + wrap('{{ file("lib/patches/boot-nexe.js") }}')
wrap('{{ file("lib/patches/boot-nexe.js") }}') )
);
} else { } else {
await compiler.replaceInFileAsync( await compiler.replaceInFileAsync(
bootFile, bootFile,
"initializePolicy();", 'initializePolicy();',
"initializePolicy();\n" + wrap('{{ file("lib/patches/boot-nexe.js") }}') 'initializePolicy();\n' + wrap('{{ file("lib/patches/boot-nexe.js") }}')
); )
} }
await compiler.replaceInFileAsync( await compiler.replaceInFileAsync(
bootFile, bootFile,
"assert(!CJSLoader.hasLoadedAnyUserCJSModule)", 'assert(!CJSLoader.hasLoadedAnyUserCJSModule)',
"/*assert(!CJSLoader.hasLoadedAnyUserCJSModule)*/" '/*assert(!CJSLoader.hasLoadedAnyUserCJSModule)*/'
); )
// Node 22.22+ / 24 removed the ENOENT catch from finalizeResolution in the const { contents: nodeccContents } = await compiler.readFileAsync('src/node.cc')
// ESM resolver. realpathSync calls binding.lstat which throws ENOENT for VFS if (nodeccContents.includes('if (env->worker_context() != nullptr) {')) {
// paths (they don't exist on real FS). Restore the catch so VFS imports work.
await compiler.replaceInFileAsync(
"lib/internal/modules/esm/resolve.js",
/const real = realpathSync\(path, \{ encoding: ['"]utf8['"] \}\);/,
"let real; try { real = realpathSync(path, { encoding: 'utf8' }); } catch (e) { if (e.code !== 'ENOENT') throw e; real = path; }"
);
const { contents: nodeccContents } = await compiler.readFileAsync(
"src/node.cc"
);
if (nodeccContents.includes("if (env->worker_context() != nullptr) {")) {
await compiler.replaceInFileAsync( await compiler.replaceInFileAsync(
"src/node.cc", 'src/node.cc',
"if (env->worker_context() != nullptr) {", 'if (env->worker_context() != nullptr) {',
"if (env->worker_context() == nullptr) {\n" + 'if (env->worker_context() == nullptr) {\n' +
' return StartExecution(env, "internal/main/run_main_module"); } else {\n' ' return StartExecution(env, "internal/main/run_main_module"); } else {\n'
); )
} else { } else {
await compiler.replaceInFileAsync( await compiler.replaceInFileAsync(
"src/node.cc", 'src/node.cc',
"MaybeLocal<Value> StartMainThreadExecution(Environment* env) {", 'MaybeLocal<Value> StartMainThreadExecution(Environment* env) {',
"MaybeLocal<Value> StartMainThreadExecution(Environment* env) {\n" + 'MaybeLocal<Value> StartMainThreadExecution(Environment* env) {\n' +
' return StartExecution(env, "internal/main/run_main_module");\n' ' return StartExecution(env, "internal/main/run_main_module");\n'
); )
} }
} else { } else {
await compiler.setFileContentsAsync( await compiler.setFileContentsAsync(
"lib/_third_party_main.js", 'lib/_third_party_main.js',
'{{ file("lib/patches/boot-nexe.js") }}' '{{ file("lib/patches/boot-nexe.js") }}'
); )
} }
return next(); return next()
} }
+33 -47
View File
@@ -1,76 +1,62 @@
import axios from "axios"; import got = require('got')
import { import { platforms, architectures, NexeTarget, getTarget, targetsEqual } from './target'
platforms, export { NexeTarget }
architectures,
NexeTarget,
getTarget,
targetsEqual,
} from "./target";
export { NexeTarget };
const versionsToSkip = [ const versionsToSkip = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13, 15, 17, 19, 21, 22]
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13, 15, 17, 19, 21, 22,
];
export interface GitAsset { export interface GitAsset {
name: string; name: string
url: string; url: string
browser_download_url: string; browser_download_url: string
} }
export interface GitRelease { export interface GitRelease {
tag_name: string; tag_name: string
assets_url: string; assets_url: string
upload_url: string; upload_url: string
assets: GitAsset[]; assets: GitAsset[]
} }
interface NodeRelease { interface NodeRelease {
version: string; version: string
} }
async function getJson<T>(url: string, options?: any): Promise<T> { async function getJson<T>(url: string, options?: any) {
const response = await axios.get(url, options); return JSON.parse((await got(url, options)).body) as T
return response.data as T;
} }
function isBuildableVersion(version: string) { function isBuildableVersion(version: string) {
if (version === "12.11.0") { if (version === '12.11.0') {
return false; return false
} }
return !versionsToSkip.includes(Number(version.split(".")[0])); return !versionsToSkip.includes(Number(version.split('.')[0]))
} }
export function getLatestGitRelease(options?: any): Promise<GitRelease> { export function getLatestGitRelease(options?: any) {
return getJson<GitRelease>( return getJson<GitRelease>('https://api.github.com/repos/nexe/nexe/releases/latest', options)
"https://api.github.com/repos/nexe/nexe/releases/latest",
options
);
} }
export async function getUnBuiltReleases(options?: any) { export async function getUnBuiltReleases(options?: any) {
const nodeReleases = await getJson<NodeRelease[]>( const nodeReleases = await getJson<NodeRelease[]>(
"https://nodejs.org/download/release/index.json" 'https://nodejs.org/download/release/index.json'
); )
const existingVersions = (await getLatestGitRelease(options)).assets.map( const existingVersions = (await getLatestGitRelease(options)).assets.map((x) => getTarget(x.name))
(x) => getTarget(x.name)
);
const versionMap: { [key: string]: true } = {}; const versionMap: { [key: string]: true } = {}
return nodeReleases return nodeReleases
.reduce((versions: NexeTarget[], { version }) => { .reduce((versions: NexeTarget[], { version }) => {
version = version.replace("v", "").trim(); version = version.replace('v', '').trim()
if (!isBuildableVersion(version) || versionMap[version]) { if (!isBuildableVersion(version) || versionMap[version]) {
return versions; return versions
} }
versionMap[version] = true; versionMap[version] = true
platforms.forEach((platform) => { platforms.forEach((platform) => {
architectures.forEach((arch) => { architectures.forEach((arch) => {
if (arch === "x86" && platform === "mac") return; if (arch === 'x86' && platform === 'mac') return
if (arch.includes("arm")) return; if (arch.includes('arm')) return
versions.push(getTarget({ platform, arch, version })); versions.push(getTarget({ platform, arch, version }))
}); })
}); })
return versions; return versions
}, []) }, [])
.filter((x) => !existingVersions.some((t) => targetsEqual(t, x))); .filter((x) => !existingVersions.some((t) => targetsEqual(t, x)))
} }
+33 -41
View File
@@ -1,35 +1,33 @@
import { join, dirname } from "path"; import { join, dirname } from 'path'
import * as fs from "fs"; import * as fs from 'fs'
import { promisify } from "util"; import { promisify } from 'util'
import { readFileAsync, writeFileAsync, isDirectoryAsync } from "../util"; import { readFileAsync, writeFileAsync, isDirectoryAsync } from '../util'
import mkdirpAsync = require("mkdirp"); import mkdirpAsync = require('mkdirp')
import { NexeCompiler } from "../compiler"; import { NexeCompiler } from '../compiler'
const unlinkAsync = promisify(fs.unlink), const unlinkAsync = promisify(fs.unlink),
readdirAsync = promisify(fs.readdir); readdirAsync = promisify(fs.readdir)
function readDirAsync(dir: string): Promise<string[]> { function readDirAsync(dir: string): Promise<string[]> {
return readdirAsync(dir).then((paths) => { return readdirAsync(dir).then((paths) => {
return Promise.all( return Promise.all(
paths.map((file: string) => { paths.map((file: string) => {
const path = join(dir, file); const path = join(dir, file)
return isDirectoryAsync(path).then((x) => return isDirectoryAsync(path).then((x) => (x ? readDirAsync(path) : (path as any)))
x ? readDirAsync(path) : (path as any)
);
}) })
).then((result) => { ).then((result) => {
return [].concat(...(result as any)); return [].concat(...(result as any))
}); })
}); })
} }
function maybeReadFileContentsAsync(file: string) { function maybeReadFileContentsAsync(file: string) {
return readFileAsync(file, "utf-8").catch((e) => { return readFileAsync(file, 'utf-8').catch((e) => {
if (e.code === "ENOENT") { if (e.code === 'ENOENT') {
return ""; return ''
} }
throw e; throw e
}); })
} }
/** /**
@@ -41,36 +39,30 @@ function maybeReadFileContentsAsync(file: string) {
* - Original versions of sources to be patched are written to the temporary directory * - Original versions of sources to be patched are written to the temporary directory
* - Finally, The patched files are written into source. * - Finally, The patched files are written into source.
*/ */
export default async function artifacts( export default async function artifacts(compiler: NexeCompiler, next: () => Promise<void>) {
compiler: NexeCompiler, const { src } = compiler
next: () => Promise<void> const temp = join(src, 'nexe')
) { await mkdirpAsync(temp)
const { src } = compiler; const tmpFiles = await readDirAsync(temp)
const temp = join(src, "nexe");
await mkdirpAsync(temp);
const tmpFiles = await readDirAsync(temp);
await Promise.all( await Promise.all(
tmpFiles.map(async (path) => { tmpFiles.map(async (path) => {
return compiler.writeFileAsync( return compiler.writeFileAsync(path.replace(temp, ''), await readFileAsync(path, 'utf-8'))
path.replace(temp, ""),
await readFileAsync(path, "utf-8")
);
}) })
); )
await next(); await next()
await Promise.all(tmpFiles.map((x) => unlinkAsync(x))); await Promise.all(tmpFiles.map((x) => unlinkAsync(x)))
return Promise.all( return Promise.all(
compiler.files.map(async (file) => { compiler.files.map(async (file) => {
const sourceFile = join(src, file.filename); const sourceFile = join(src, file.filename)
const tempFile = join(temp, file.filename); const tempFile = join(temp, file.filename)
const fileContents = await maybeReadFileContentsAsync(sourceFile); const fileContents = await maybeReadFileContentsAsync(sourceFile)
await mkdirpAsync(dirname(tempFile)); await mkdirpAsync(dirname(tempFile))
await writeFileAsync(tempFile, fileContents); await writeFileAsync(tempFile, fileContents)
await compiler.writeFileAsync(file.filename, file.contents); await compiler.writeFileAsync(file.filename, file.contents)
}) })
); )
} }
+49 -255
View File
@@ -1,294 +1,88 @@
import { NexeCompiler, NexeError } from "../compiler"; import { NexeCompiler, NexeError } from '../compiler'
import { resolve, relative, sep, join } from "path"; import { resolve, relative } from 'path'
import resolveFiles, { resolveSync } from "resolve-dependencies"; import resolveFiles, { resolveSync } from 'resolve-dependencies'
import { dequote, STDIN_FLAG, semverGt } from "../util"; import { dequote, STDIN_FLAG, semverGt, each } from '../util'
import { Readable } from "stream"; import { Readable } from 'stream'
import { minimatch } from "minimatch";
import { readdirSync, statSync } from "fs";
type ModuleRule = {
include?: string[];
exclude?: string[];
};
function getModuleName(file: string): string | null {
const parts = file.split(/[\\/]/);
const idx = parts.lastIndexOf("node_modules");
if (idx === -1) return null;
const name = parts[idx + 1];
if (!name) return null;
return name.startsWith("@")
? parts.slice(idx + 1, idx + 3).join("/")
: name;
}
function getModuleBasePath(file: string, moduleName: string): string | null {
const parts = file.split(/[\\/]/);
const idx = parts.lastIndexOf("node_modules");
if (idx === -1) return null;
const baseParts = parts.slice(0, idx + 1);
if (moduleName.startsWith("@")) {
baseParts.push(...moduleName.split("/"));
} else {
baseParts.push(moduleName);
}
return baseParts.join(sep);
}
function getModuleRelativePath(file: string, moduleName: string): string {
const normalizedFile = file.replace(/\\/g, '/');
const normalizedModuleName = moduleName.replace(/\\/g, '/');
const marker = `node_modules/${normalizedModuleName}/`;
const idx = normalizedFile.indexOf(marker);
if (idx === -1) {
return "";
}
return normalizedFile.slice(idx + marker.length);
}
function getAllFilesInModule(basePath: string): string[] {
const files: string[] = [];
function walk(dir: string) {
try {
const entries = readdirSync(dir);
for (const entry of entries) {
const fullPath = join(dir, entry);
try {
const stat = statSync(fullPath);
if (stat.isDirectory()) {
walk(fullPath);
} else if (stat.isFile()) {
files.push(fullPath);
}
} catch (err) {
// Skip files we can't read
}
}
} catch (err) {
// Skip directories we can't read
}
}
walk(basePath);
return files;
}
function shouldIncludeFile(
file: string,
moduleName: string,
rule: ModuleRule
): boolean {
const rel = getModuleRelativePath(file, moduleName);
if (!rel) {
return false;
}
// Always include package.json
if (rel === "package.json") {
return true;
}
// If there's an include list, file MUST match it
if (rule.include && rule.include.length > 0) {
const matchesInclude = rule.include.some((g) => minimatch(rel, g));
if (!matchesInclude) {
return false;
}
}
// Check exclude patterns
if (rule.exclude && rule.exclude.length > 0) {
const matchesExclude = rule.exclude.some((g) => minimatch(rel, g));
if (matchesExclude) {
return false;
}
}
return true;
}
function getStdIn(stdin: Readable): Promise<string> { function getStdIn(stdin: Readable): Promise<string> {
let out = ""; let out = ''
return new Promise((resolve) => { return new Promise((resolve) => {
stdin stdin
.setEncoding("utf8") .setEncoding('utf8')
.on("readable", () => { .on('readable', () => {
let current; let current
while ((current = stdin.read())) { while ((current = stdin.read())) {
out += current; out += current
} }
}) })
.on("end", () => resolve(out.trim())); .on('end', () => resolve(out.trim()))
setTimeout(() => { setTimeout(() => {
if (!out.trim()) { if (!out.trim()) {
resolve(out.trim()); resolve(out.trim())
} }
}, 1000); }, 1000)
}); })
} }
export default async function bundle(compiler: NexeCompiler, next: any) { export default async function bundle(compiler: NexeCompiler, next: any) {
const { bundle: doBundle, cwd, input: inputPath, bundleRules } = compiler.options; const { bundle: doBundle, cwd, input: inputPath } = compiler.options
let input = inputPath
const rules: Record<string, ModuleRule> = compiler.entrypoint = './' + relative(cwd, input)
bundleRules && Object.keys(bundleRules).length ? bundleRules : {};
let input = inputPath; if (semverGt(compiler.target.version, '11.99')) {
compiler.entrypoint = "./" + relative(cwd, input); compiler.startup = ''
if (semverGt(compiler.target.version, "11.99")) {
compiler.startup = "";
} else { } else {
compiler.startup = ';require("module").runMain();'; compiler.startup = ';require("module").runMain();'
} }
if (!doBundle) { if (!doBundle) {
await compiler.addResource(resolve(cwd, input)); await compiler.addResource(resolve(cwd, input))
return next(); return next()
} }
let code = ""; let code = ''
if (typeof doBundle === "string") { if (typeof doBundle === 'string') {
code = await require(doBundle).createBundle(compiler.options); code = await require(doBundle).createBundle(compiler.options)
} }
if ( if (input === STDIN_FLAG && (code = code || dequote(await getStdIn(process.stdin)))) {
input === STDIN_FLAG && compiler.stdinUsed = true
(code = code || dequote(await getStdIn(process.stdin))) compiler.entrypoint = './__nexe_stdin.js'
) { await compiler.addResource(resolve(cwd, compiler.entrypoint), code)
compiler.stdinUsed = true; return next()
compiler.entrypoint = "./__nexe_stdin.js";
await compiler.addResource(resolve(cwd, compiler.entrypoint), code);
return next();
} }
if (input === STDIN_FLAG) { if (input === STDIN_FLAG) {
const maybeInput = resolveSync(cwd, "."); const maybeInput = resolveSync(cwd, '.')
if (!maybeInput || !maybeInput.absPath) { if (!maybeInput || !maybeInput.absPath) {
throw new NexeError("No valid input detected"); throw new NexeError('No valid input detected')
} }
input = maybeInput.absPath; input = maybeInput.absPath
compiler.entrypoint = "./" + relative(cwd, input); compiler.entrypoint = './' + relative(cwd, input)
} }
const step = compiler.log.step("Resolving dependencies..."); const step = compiler.log.step('Resolving dependencies...')
const { files, warnings } = await resolveFiles(input, { const { files, warnings } = await resolveFiles(
cwd, input,
expand: "variable", ...Object.keys(compiler.bundle.list).filter((x) => x.endsWith('.js') || x.endsWith('.mjs')),
loadContent: false, { cwd, expand: 'variable', loadContent: false }
}); )
if ( if (
warnings.filter( warnings.filter((x) => x.startsWith('Error parsing file') && !x.includes('node_modules')).length
(x) => x.startsWith("Error parsing file") && !x.includes("node_modules")
).length
) { ) {
throw new NexeError("Parsing Error:\n" + warnings.join("\n")); throw new NexeError('Parsing Error:\n' + warnings.join('\n'))
} }
// If no bundle rules, use original behavior //TODO: warnings.forEach((x) => console.log(x))
if (Object.keys(rules).length === 0) {
const pkgJsonsToAdd = new Set<string>();
await Promise.all(
Object.entries(files).map(([key]) => {
step.log(`Including dependency: ${key}`);
// Collect the package.json for each bundled module so Node can
// resolve the correct entry point (main/exports) at runtime.
const moduleName = getModuleName(key);
if (moduleName) {
const basePath = getModuleBasePath(key, moduleName);
if (basePath) {
pkgJsonsToAdd.add(join(basePath, "package.json"));
}
}
return compiler.addResource(key);
})
);
await Promise.all(
Array.from(pkgJsonsToAdd).map((pkgJson) => {
try {
statSync(pkgJson);
step.log(`Including package.json: ${pkgJson}`);
return compiler.addResource(pkgJson);
} catch (e) {
// package.json doesn't exist on disk, skip
}
})
);
return next();
}
// Track modules with explicit bundle rules
const modulesWithRules = new Map<string, string>();
for (const file of Object.keys(files)) {
const moduleName = getModuleName(file);
if (moduleName && rules[moduleName] && !modulesWithRules.has(moduleName)) {
const basePath = getModuleBasePath(file, moduleName);
if (basePath) {
modulesWithRules.set(moduleName, basePath);
}
}
}
// Collect files to include
const filesToInclude = new Set<string>();
// Add all files that AREN'T from modules with rules (preserve original behavior)
for (const file of Object.keys(files)) {
const moduleName = getModuleName(file);
// If this file is from a module with rules, skip it here
if (moduleName && modulesWithRules.has(moduleName)) {
continue;
}
// All other files get included as normal
filesToInclude.add(file);
}
// For modules WITH rules: scan and filter
for (const [moduleName, basePath] of modulesWithRules) {
const rule = rules[moduleName];
step.log(`Scanning ${moduleName} with bundle rules...`);
const moduleFiles = getAllFilesInModule(basePath);
for (const file of moduleFiles) {
const rel = getModuleRelativePath(file, moduleName);
if (shouldIncludeFile(file, moduleName, rule)) {
filesToInclude.add(file);
step.log(`Including ${rel} from ${moduleName}`);
} else {
step.log(`Excluding ${rel} from ${moduleName}`);
}
}
}
// Add all files
await Promise.all( await Promise.all(
Array.from(filesToInclude).map((file) => { Object.entries(files).map(([key]) => {
step.log(`Including dependency: ${file}`); step.log(`Including dependency: ${key}`)
return compiler.addResource(file); return compiler.addResource(key)
}) })
); )
return next(); return next()
} }
+13 -18
View File
@@ -1,24 +1,19 @@
import { NexeCompiler } from "../compiler"; import { NexeCompiler } from '../compiler'
import { rimrafAsync } from "../util"; import { rimrafAsync } from '../util'
import { NexeTarget } from "../target"; import { NexeTarget } from '../target'
export default async function clean( export default async function clean(compiler: NexeCompiler, next: () => Promise<any>) {
compiler: NexeCompiler, const { options } = compiler
next: () => Promise<any>
) {
const { options } = compiler;
if (options.clean) { if (options.clean) {
let path = compiler.src; let path = compiler.src
if (!options.build) { if (!options.build) {
path = compiler.getNodeExecutableLocation( path = compiler.getNodeExecutableLocation(compiler.options.targets[0] as NexeTarget)
compiler.options.targets[0] as NexeTarget
);
} }
const step = compiler.log.step("Cleaning up nexe build artifacts..."); const step = compiler.log.step('Cleaning up nexe build artifacts...')
step.log(`Deleting contents at: ${path}`); step.log(`Deleting contents at: ${path}`)
await rimrafAsync(path); await rimrafAsync(path)
step.log(`Deleted contents at: ${path}`); step.log(`Deleted contents at: ${path}`)
return compiler.quit(); return compiler.quit()
} }
return next(); return next()
} }
+30 -46
View File
@@ -1,17 +1,9 @@
import { platform } from "os"; import { dirname, normalize, relative, resolve } from 'path'
import { dirname, normalize, relative, resolve } from "path"; import { createWriteStream, chmodSync, statSync } from 'fs'
import { import { NexeCompiler } from '../compiler'
createWriteStream, import { NexeTarget } from '../target'
chmodSync, import { STDIN_FLAG } from '../util'
statSync, import mkdirp = require('mkdirp')
readFileSync,
writeFileSync,
} from "fs";
import { NexeCompiler } from "../compiler";
import { NexeTarget } from "../target";
import { STDIN_FLAG } from "../util";
import { patchMachOExecutable } from "./mach-o";
import mkdirp = require("mkdirp");
/** /**
* The "cli" step detects the appropriate input. If no input options are passed, * The "cli" step detects the appropriate input. If no input options are passed,
@@ -23,53 +15,45 @@ import mkdirp = require("mkdirp");
* @param {*} compiler * @param {*} compiler
* @param {*} next * @param {*} next
*/ */
export default async function cli( export default async function cli(compiler: NexeCompiler, next: () => Promise<void>) {
compiler: NexeCompiler, await next()
next: () => Promise<void>
) {
await next();
const { log } = compiler, const { log } = compiler,
target = compiler.options.targets.shift() as NexeTarget, target = compiler.options.targets.shift() as NexeTarget,
deliverable = await compiler.compileAsync(target), deliverable = await compiler.compileAsync(target),
output = normalize(compiler.output!); output = normalize(compiler.output!)
mkdirp.sync(dirname(output)); mkdirp.sync(dirname(output))
return new Promise((res, rej) => { return new Promise((res, rej) => {
const step = log.step("Writing result to file"); const step = log.step('Writing result to file')
deliverable deliverable
.pipe(createWriteStream(output)) .pipe(createWriteStream(output))
.on("error", rej) .on('error', rej)
.once("close", () => { .once('close', (e: Error) => {
if (compiler.output) { if (e) {
rej(e)
} else if (compiler.output) {
const output = compiler.output, const output = compiler.output,
mode = statSync(output).mode | 0o111, mode = statSync(output).mode | 0o111,
inputFileLogOutput = relative( inputFileLogOutput = relative(
process.cwd(), process.cwd(),
resolve( resolve(compiler.options.cwd, compiler.entrypoint || compiler.options.input)
compiler.options.cwd,
compiler.entrypoint || compiler.options.input
)
), ),
outputFileLogOutput = relative(process.cwd(), output); outputFileLogOutput = relative(process.cwd(), output)
if (platform() === "darwin") { chmodSync(output, mode.toString(8).slice(-3))
step.log("Preparing binary for macOS signing.");
writeFileSync(output, patchMachOExecutable(readFileSync(output)));
}
chmodSync(output, mode.toString(8).slice(-3));
step.log( step.log(
`Entry: '${compiler.stdinUsed `Entry: '${
? compiler.options.mangle compiler.stdinUsed
? STDIN_FLAG ? compiler.options.mangle
: "[none]" ? STDIN_FLAG
: inputFileLogOutput : '[none]'
: inputFileLogOutput
}' written to: ${outputFileLogOutput}` }' written to: ${outputFileLogOutput}`
); )
compiler.quit(); compiler.quit()
res(output); res(output)
} }
}); })
}); })
} }
+45 -136
View File
@@ -1,126 +1,48 @@
import axios from "axios"; import download = require('download')
import { pathExistsAsync } from "../util"; import { pathExistsAsync } from '../util'
import { LogStep } from "../logger"; import { LogStep } from '../logger'
import { NexeCompiler, NexeError } from "../compiler"; import { IncomingMessage } from 'http'
import { dirname } from "path"; import { NexeCompiler, NexeError } from '../compiler'
import { createWriteStream } from "fs"; import { dirname } from 'path'
import { pipeline } from "stream/promises";
import { createGunzip } from "zlib";
import * as tar from 'tar';
import fs from "fs/promises";
async function downloadWithProgress( function fetchNodeSourceAsync(dest: string, url: string, step: LogStep, options = {}) {
url: string, const setText = (p: number) => step.modify(`Downloading Node: ${p.toFixed()}%...`)
dest: string, return download(url, dest, Object.assign(options, { extract: true, strip: 1 }))
options: any = {}, .on('response', (res: IncomingMessage) => {
step?: LogStep const total = +res.headers['content-length']!
): Promise<void> { let current = 0
// Ensure destination directory exists res.on('data', (data) => {
await fs.mkdir(dirname(dest), { recursive: true }); current += data.length
setText((current / total) * 100)
const response = await axios({ if (current === total) {
url, step.log('Extracting Node...')
method: "GET", }
responseType: "stream", })
...options,
});
const total = parseInt(String(response.headers["content-length"] || "0"), 10);
let current = 0;
// Create write stream
const writer = createWriteStream(dest);
// Handle progress
response.data.on("data", (chunk: Buffer) => {
current += chunk.length;
if (step && total > 0) {
step.modify(`Downloading...${((current / total) * 100).toFixed()}%`);
}
});
// Pipe the response to file
await pipeline(response.data, writer);
if (step) {
step.log(`Download completed: ${dest}`);
}
}
async function fetchNodeSourceAsync(
dest: string,
url: string,
step: LogStep,
options = {}
) {
// Ensure destination directory exists
await fs.mkdir(dest, { recursive: true });
const setText = (p: number) =>
step.modify(`Downloading Node: ${p.toFixed()}%...`);
// Download the file first
const tempFile = dest + ".tar.gz";
const response = await axios({
url,
method: "GET",
responseType: "stream",
...options,
});
const total = parseInt(String(response.headers["content-length"] || "0"), 10);
let current = 0;
// Create write stream for the temp file
const writer = createWriteStream(tempFile);
// Track progress
response.data.on("data", (chunk: Buffer) => {
current += chunk.length;
if (total > 0) {
setText((current / total) * 100);
}
});
// Wait for download to complete
await pipeline(response.data, writer);
step.log("Extracting Node...");
// Extract the tar.gz file
await pipeline(
createReadStream(tempFile),
createGunzip(),
tar.x({
cwd: dest,
strip: 1,
}) })
); .then(() => step.log(`Node source extracted to: ${dest}`))
// Clean up temp file
await fs.unlink(tempFile);
step.log(`Node source extracted to: ${dest}`);
} }
async function fetchPrebuiltBinary(compiler: NexeCompiler, step: any) { async function fetchPrebuiltBinary(compiler: NexeCompiler, step: any) {
const { target, remoteAsset } = compiler, const { target, remoteAsset } = compiler,
filename = compiler.getNodeExecutableLocation(target); filename = compiler.getNodeExecutableLocation(target)
try { try {
await downloadWithProgress( await download(remoteAsset, dirname(filename), compiler.options.downloadOptions).on(
remoteAsset, 'response',
filename, (res: IncomingMessage) => {
compiler.options.downloadOptions, const total = +res.headers['content-length']!
step let current = 0
); res.on('data', (data) => {
current += data.length
step!.modify(`Downloading...${((current / total) * 100).toFixed()}%`)
})
}
)
} catch (e: any) { } catch (e: any) {
if (e.response?.status === 404) { if (e.statusCode === 404) {
throw new NexeError( throw new NexeError(`${remoteAsset} is not available, create it using the --build flag`)
`${remoteAsset} is not available, create it using the --build flag`
);
} else { } else {
throw new NexeError("Error downloading prebuilt binary: " + e.message); throw new NexeError('Error downloading prebuilt binary: ' + e)
} }
} }
} }
@@ -130,40 +52,27 @@ async function fetchPrebuiltBinary(compiler: NexeCompiler, step: any) {
* @param {*} compiler * @param {*} compiler
* @param {*} next * @param {*} next
*/ */
export default async function downloadNode( export default async function downloadNode(compiler: NexeCompiler, next: () => Promise<void>) {
compiler: NexeCompiler,
next: () => Promise<void>
) {
const { src, log, target } = compiler, const { src, log, target } = compiler,
{ version } = target, { version } = target,
{ sourceUrl, downloadOptions, build } = compiler.options, { sourceUrl, downloadOptions, build } = compiler.options,
url = url = sourceUrl || `https://nodejs.org/dist/v${version}/node-v${version}.tar.gz`,
sourceUrl ||
`https://nodejs.org/dist/v${version}/node-v${version}.tar.gz`,
step = log.step( step = log.step(
`Downloading ${build ? "" : "pre-built"} Node.js ${build ? `source from: ${url}` : "" `Downloading ${build ? '' : 'pre-built '}Node.js${build ? ` source from: ${url}` : ''}`
}`
), ),
exeLocation = compiler.getNodeExecutableLocation( exeLocation = compiler.getNodeExecutableLocation(build ? undefined : target),
build ? undefined : target downloadExists = await pathExistsAsync(build ? src : exeLocation)
),
downloadExists = await pathExistsAsync(build ? src : exeLocation);
if (downloadExists) { if (downloadExists) {
step.log("Already downloaded..."); step.log('Already downloaded...')
return next(); return next()
} }
if (build) { if (build) {
await fetchNodeSourceAsync(src, url, step, downloadOptions); await fetchNodeSourceAsync(src, url, step, downloadOptions)
} else { } else {
await fetchPrebuiltBinary(compiler, step); await fetchPrebuiltBinary(compiler, step)
} }
return next(); return next()
}
// Helper function to create a readable stream
function createReadStream(path: string) {
return require("fs").createReadStream(path);
} }
-84
View File
@@ -1,84 +0,0 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2021 Vercel, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* This code is originally from the [pkg](https://github.com/vercel/pkg).
*/
function parseCStr(buf: Buffer) {
for (let i = 0; i < buf.length; i += 1) {
if (buf[i] === 0) {
return buf.slice(0, i).toString();
}
}
}
function patchCommand(type: number, buf: Buffer, file: Buffer) {
// segment_64
if (type === 0x19) {
const name = parseCStr(buf.slice(0, 16));
if (name === "__LINKEDIT") {
const fileoff = buf.readBigUInt64LE(32);
const vmsizePatched = BigInt(file.length) - fileoff;
const filesizePatched = vmsizePatched;
buf.writeBigUInt64LE(vmsizePatched, 24);
buf.writeBigUInt64LE(filesizePatched, 40);
}
}
// symtab
if (type === 0x2) {
const stroff = buf.readUInt32LE(8);
const strsizePatched = file.length - stroff;
buf.writeUInt32LE(strsizePatched, 12);
}
}
function patchMachOExecutable(file: Buffer) {
const align = 8;
const hsize = 32;
const ncmds = file.readUInt32LE(16);
const buf = file.slice(hsize);
for (let offset = 0, i = 0; i < ncmds; i += 1) {
const type = buf.readUInt32LE(offset);
offset += 4;
const size = buf.readUInt32LE(offset) - 8;
offset += 4;
patchCommand(type, buf.slice(offset, offset + size), file);
offset += size;
if (offset & align) {
offset += align - (offset & align);
}
}
return file;
}
export { patchMachOExecutable };
+17 -25
View File
@@ -1,33 +1,25 @@
import { each } from "../util"; import { each } from '../util'
import globs from "globby"; import globs from 'globby'
import { resolve } from "path"; import { resolve } from 'path'
import { NexeCompiler } from "../compiler"; import { NexeCompiler } from '../compiler'
export default async function resource( export default async function resource(compiler: NexeCompiler, next: () => Promise<any>) {
compiler: NexeCompiler, const { cwd, resources } = compiler.options
next: () => Promise<any>
) {
const { cwd, resources } = compiler.options;
if (!resources.length) { if (!resources.length) {
return next(); return next()
} }
const step = compiler.log.step("Bundling Resources..."); const step = compiler.log.step('Bundling Resources...')
let count = 0; let count = 0
// workaround for https://github.com/sindresorhus/globby/issues/127 // workaround for https://github.com/sindresorhus/globby/issues/127
// and https://github.com/mrmlnc/fast-glob#pattern-syntax // and https://github.com/mrmlnc/fast-glob#pattern-syntax
const resourcesWithForwardSlashes = resources.map((r) => const resourcesWithForwardSlashes = resources.map((r) => r.replace(/\\/g, '/'))
r.replace(/\\/g, "/")
);
await each( await each(globs(resourcesWithForwardSlashes, { cwd, onlyFiles: true }), async (file) => {
globs(resourcesWithForwardSlashes, { cwd, onlyFiles: true }), count++
async (file) => { step.log(`Including file: ${file}`)
count++; await compiler.addResource(resolve(cwd, file))
step.log(`Including file: ${file}`); })
await compiler.addResource(resolve(cwd, file)); step.log(`Included ${count} file(s)`)
} return next()
);
step.log(`Included ${count} file(s)`);
return next();
} }
+16 -24
View File
@@ -1,32 +1,24 @@
import { NexeCompiler } from "../compiler"; import { NexeCompiler } from '../compiler'
import { wrap } from "../util"; import { wrap } from '../util'
export default async function ( export default async function (compiler: NexeCompiler, next: () => Promise<void>) {
compiler: NexeCompiler, await next()
next: () => Promise<void>
) {
await next();
compiler.shims.push( compiler.shims.push(
wrap( wrap(
[ [
"process.__nexe = {};", 'process.__nexe = {};',
"const fsPatcher = (function() {", 'const fsPatcher = (function() {',
"const module = {exports: {}};", 'const module = {exports: {}};',
"const exports = module.exports;", 'const exports = module.exports;',
'{{file("lib/fs/patch.bundle.js")}}', '{{file("lib/fs/patch.bundle.js")}}',
"return module.exports;", 'return module.exports;',
"})()", '})()',
"fsPatcher.shimFs(process.__nexe);", 'fsPatcher.shimFs(process.__nexe);',
// Re-apply ENOENT-tolerant realpathSync after shimFs overwrites boot-nexe's patch. compiler.options.fs ? '' : 'restoreFs();',
// Node 22.22+ / 24 no longer catches ENOENT in ESM finalizeResolution. ].join('\n')
// ESM resolver captures fs.realpathSync by destructuring at first ESM load,
// which happens after the shim runs — so this wrapper is what it captures.
"(function(){var _fs=require('fs'),_o=_fs.realpathSync;_fs.realpathSync=function(p,o){try{return _o.call(this,p,o);}catch(e){if(e&&e.code==='ENOENT')return p;throw e;}};_fs.realpathSync.native=_o.native||_o;})()",
compiler.options.fs ? "" : "restoreFs();",
].join("\n")
//TODO support only restoring specific methods //TODO support only restoring specific methods
) )
); )
compiler.shims.push( compiler.shims.push(
wrap(` wrap(`
if (process.argv[1] && process.env.NODE_UNIQUE_ID) { if (process.argv[1] && process.env.NODE_UNIQUE_ID) {
@@ -35,7 +27,7 @@ export default async function (
delete process.env.NODE_UNIQUE_ID delete process.env.NODE_UNIQUE_ID
} }
`) `)
); )
compiler.shims.push( compiler.shims.push(
wrap(` wrap(`
@@ -47,5 +39,5 @@ export default async function (
process.argv.splice(1,0, entry) process.argv.splice(1,0, entry)
} }
`) `)
); )
} }
+50 -61
View File
@@ -1,107 +1,96 @@
export type NodePlatform = "windows" | "mac" | "alpine" | "linux"; export type NodePlatform = 'windows' | 'mac' | 'alpine' | 'linux'
export type NodeArch = "x86" | "x64" | "arm" | "arm64"; export type NodeArch = 'x86' | 'x64' | 'arm' | 'arm64'
const platforms: NodePlatform[] = ["windows", "mac", "alpine", "linux"], const platforms: NodePlatform[] = ['windows', 'mac', 'alpine', 'linux'],
architectures: NodeArch[] = ["x86", "x64", "arm", "arm64"]; architectures: NodeArch[] = ['x86', 'x64', 'arm', 'arm64']
export { platforms, architectures }; export { platforms, architectures }
export interface NexeTarget { export interface NexeTarget {
version: string; version: string
platform: NodePlatform | string; platform: NodePlatform | string
arch: NodeArch | string; arch: NodeArch | string
} }
const prettyPlatform: { [key: string]: NodePlatform } = { const prettyPlatform: { [key: string]: NodePlatform } = {
win32: "windows", win32: 'windows',
windows: "windows", windows: 'windows',
win: "windows", win: 'windows',
darwin: "mac", darwin: 'mac',
macos: "mac", macos: 'mac',
mac: "mac", mac: 'mac',
linux: "linux", linux: 'linux',
static: "alpine", static: 'alpine',
alpine: "alpine", alpine: 'alpine',
}; }
const prettyArch: { [key: string]: NodeArch } = { const prettyArch: { [key: string]: NodeArch } = {
x86: "x86", x86: 'x86',
arm6: "arm", arm6: 'arm',
arm64: "arm64", arm64: 'arm64',
arm6l: "arm", arm6l: 'arm',
arm: "arm", arm: 'arm',
arm7: "arm", arm7: 'arm',
arm7l: "arm", arm7l: 'arm',
amd64: "x64", amd64: 'x64',
ia32: "x86", ia32: 'x86',
x32: "x86", x32: 'x86',
x64: "x64", x64: 'x64',
}; }
function isVersion(x: string) { function isVersion(x: string) {
if (!x) { if (!x) {
return false; return false
} }
return /^[\d]+$/.test(x.replace(/v|\.|\s+/g, "")); return /^[\d]+$/.test(x.replace(/v|\.|\s+/g, ''))
} }
function isPlatform(x: string): x is NodePlatform { function isPlatform(x: string): x is NodePlatform {
return x in prettyPlatform; return x in prettyPlatform
} }
function isArch(x: string): x is NodeArch { function isArch(x: string): x is NodeArch {
return x in prettyArch; return x in prettyArch
} }
class Target implements NexeTarget { class Target implements NexeTarget {
constructor( constructor(public arch: NodeArch, public platform: NodePlatform, public version: string) {}
public arch: NodeArch,
public platform: NodePlatform,
public version: string
) { }
toJSON() { toJSON() {
return this.toString(); return this.toString()
} }
toString() { toString() {
return `${this.platform}-${this.arch}-${this.version}`; return `${this.platform}-${this.arch}-${this.version}`
} }
} }
export function targetsEqual(a: NexeTarget, b: NexeTarget) { export function targetsEqual(a: NexeTarget, b: NexeTarget) {
return ( return a.arch === b.arch && a.platform === b.platform && a.version === b.version
a.arch === b.arch && a.platform === b.platform && a.version === b.version
);
} }
export function getTarget( export function getTarget(target: string | Partial<NexeTarget> = ''): NexeTarget {
target: string | Partial<NexeTarget> = "" const currentArch = process.arch
): NexeTarget { let arch = currentArch in prettyArch ? prettyArch[process.arch] : (process.arch as NodeArch),
const currentArch = process.arch;
let arch =
currentArch in prettyArch
? prettyArch[process.arch]
: (process.arch as NodeArch),
platform = prettyPlatform[process.platform], platform = prettyPlatform[process.platform],
version = process.version.slice(1); version = process.version.slice(1)
if (typeof target !== "string") { if (typeof target !== 'string') {
target = `${target.platform}-${target.arch}-${target.version}`; target = `${target.platform}-${target.arch}-${target.version}`
} }
target target
.toLowerCase() .toLowerCase()
.split("-") .split('-')
.forEach((x) => { .forEach((x) => {
if (isVersion(x)) { if (isVersion(x)) {
version = x.replace(/v/g, ""); version = x.replace(/v/g, '')
} }
if (isPlatform(x)) { if (isPlatform(x)) {
platform = prettyPlatform[x]; platform = prettyPlatform[x]
} }
if (isArch(x)) { if (isArch(x)) {
arch = prettyArch[x]; arch = prettyArch[x]
} }
}); })
return new Target(arch, platform, version); return new Target(arch, platform, version)
} }
+23
View File
@@ -0,0 +1,23 @@
declare module 'got' {
interface GotFn {
(url: string, options?: any): Promise<{ body: string }>
stream(url: string, optoins?: any): any
}
const got: GotFn
export = got
}
declare module 'download' {
import { Duplex } from 'stream'
interface DownloadOptions {
extract?: boolean
strip?: number
filename?: string
proxy?: string
}
function download(
url: string,
destination?: string | DownloadOptions,
options?: DownloadOptions
): PromiseLike<Buffer> & Duplex
export = download
}
+40 -40
View File
@@ -1,100 +1,100 @@
import { readFile, writeFile, stat } from "fs"; import { readFile, writeFile, stat } from 'fs'
import { execFile } from "child_process"; import { execFile } from 'child_process'
import { promisify } from "util"; import { promisify } from 'util'
import rimraf = require("rimraf"); import rimraf = require('rimraf')
const rimrafAsync = promisify(rimraf); const rimrafAsync = promisify(rimraf)
export const STDIN_FLAG = "[stdin]"; export const STDIN_FLAG = '[stdin]'
export async function each<T>( export async function each<T>(
list: T[] | Promise<T[]>, list: T[] | Promise<T[]>,
action: (item: T, index: number, list: T[]) => Promise<any> action: (item: T, index: number, list: T[]) => Promise<any>
) { ) {
const l = await list; const l = await list
return Promise.all(l.map(action)); return Promise.all(l.map(action))
} }
export function wrap(code: string) { export function wrap(code: string) {
return "!(function () {" + code + "})();"; return '!(function () {' + code + '})();'
} }
function falseOnEnoent(e: any) { function falseOnEnoent(e: any) {
if (e.code === "ENOENT") { if (e.code === 'ENOENT') {
return false; return false
} }
throw e; throw e
} }
function padRight(str: string, l: number) { function padRight(str: string, l: number) {
return (str + " ".repeat(l)).slice(0, l); return (str + ' '.repeat(l)).slice(0, l)
} }
const bound: MethodDecorator = function bound<T>( const bound: MethodDecorator = function bound<T>(
target: Object, target: Object,
propertyKey: string | symbol, propertyKey: string | Symbol,
descriptor: TypedPropertyDescriptor<T> descriptor: TypedPropertyDescriptor<T>
) { ) {
const configurable = true; const configurable = true
return { return {
configurable, configurable,
get(this: T) { get(this: T) {
const value = (descriptor.value as any).bind(this); const value = (descriptor.value as any).bind(this)
Object.defineProperty(this, propertyKey as string, { Object.defineProperty(this, propertyKey as string, {
configurable, configurable,
value, value,
writable: true, writable: true,
}); })
return value; return value
}, },
}; }
}; }
function dequote(input: string) { function dequote(input: string) {
input = input.trim(); input = input.trim()
const singleQuote = input.startsWith("'") && input.endsWith("'"); const singleQuote = input.startsWith("'") && input.endsWith("'")
const doubleQuote = input.startsWith('"') && input.endsWith('"'); const doubleQuote = input.startsWith('"') && input.endsWith('"')
if (singleQuote || doubleQuote) { if (singleQuote || doubleQuote) {
return input.slice(1).slice(0, -1); return input.slice(1).slice(0, -1)
} }
return input; return input
} }
export interface ReadFileAsync { export interface ReadFileAsync {
(path: string): Promise<Buffer>; (path: string): Promise<Buffer>
(path: string, encoding: string): Promise<string>; (path: string, encoding: string): Promise<string>
} }
const readFileAsync = promisify(readFile); const readFileAsync = promisify(readFile)
const writeFileAsync = promisify(writeFile); const writeFileAsync = promisify(writeFile)
const statAsync = promisify(stat); const statAsync = promisify(stat)
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile)
const isWindows = process.platform === "win32"; const isWindows = process.platform === 'win32'
function pathExistsAsync(path: string) { function pathExistsAsync(path: string) {
return statAsync(path) return statAsync(path)
.then((x) => true) .then((x) => true)
.catch(falseOnEnoent); .catch(falseOnEnoent)
} }
function isDirectoryAsync(path: string) { function isDirectoryAsync(path: string) {
return statAsync(path) return statAsync(path)
.then((x) => x.isDirectory()) .then((x) => x.isDirectory())
.catch(falseOnEnoent); .catch(falseOnEnoent)
} }
/** /**
* @param version See if this version is greather than the second one * @param version See if this version is greather than the second one
* @param operand Version to compare against * @param operand Version to compare against
*/ */
function semverGt(version: string, operand: string) { function semverGt(version: string, operand: string) {
const [cMajor, cMinor, cPatch] = version.split(".").map(Number); const [cMajor, cMinor, cPatch] = version.split('.').map(Number)
let [major, minor, patch] = operand.split(".").map(Number); let [major, minor, patch] = operand.split('.').map(Number)
if (!minor) minor = 0; if (!minor) minor = 0
if (!patch) patch = 0; if (!patch) patch = 0
return ( return (
cMajor > major || cMajor > major ||
(cMajor === major && cMinor > minor) || (cMajor === major && cMinor > minor) ||
(cMajor === major && cMinor === minor && cPatch > patch) (cMajor === major && cMinor === minor && cPatch > patch)
); )
} }
export { export {
@@ -110,4 +110,4 @@ export {
pathExistsAsync, pathExistsAsync,
isDirectoryAsync, isDirectoryAsync,
writeFileAsync, writeFileAsync,
}; }
-30
View File
@@ -1,30 +0,0 @@
'use strict'
const { resolve } = require('path')
const resolvePath = (path) => resolve(__dirname, '..', path)
async function compile() {
const executableSuffix = require('os').platform().startsWith('win') ? '.exe' : ''
const builtExecutablePath = require('path').join(process.env.NEXE_TMP, process.versions.node, `out/Release/node${executableSuffix}`)
try {
require('fs').unlinkSync(builtExecutablePath)
} catch(e) {}
const nexe = require('..')
console.error('Building asset')
return nexe.compile({
loglevel: 'verbose',
python: process.env.PYTHON || 'python',
mangle: false,
build: true,
output: process.env.NEXE_ASSET || `nexe-asset${executableSuffix}`,
input: resolvePath('test/asset-build-input.js'),
configure: process.env.MUSL_BUILD ? ['--fully-static'] : [],
temp: process.env.NEXE_TMP,
})
}
if (!('MAKEFLAGS' in process.env)) {
process.env.MAKEFLAGS = `-j${require('os').cpus().length + 1}`
}
compile()
-25
View File
@@ -1,25 +0,0 @@
'use strict'
const { resolve } = require('path')
const resolvePath = (path) => resolve(__dirname, '..', path)
async function build() {
const nexe = require('..')
console.error('Doing test build using asset')
if (!require('fs').existsSync(process.env.NEXE_ASSET)) {
console.error(`Pre-built asset not found at ${process.env.NEXE_ASSET}`)
process.exitCode = 1
return
}
return nexe.compile({
loglevel: 'verbose',
python: process.env.PYTHON || 'python',
build: true,
output: resolvePath('integration-tests'),
input: resolvePath('test/integration/index.js'),
resources: [resolvePath('test/integration'), resolvePath('node_modules')],
asset: process.env.NEXE_ASSET,
})
}
build()
+69 -93
View File
@@ -1,140 +1,116 @@
import * as nexe from "../lib/nexe"; import * as nexe from '../lib/nexe'
import { getUnBuiltReleases, getLatestGitRelease } from "../lib/releases"; import { getUnBuiltReleases, getLatestGitRelease } from '../lib/releases'
import { runDockerBuild } from "./docker"; import { runDockerBuild } from './docker'
import { getTarget, targetsEqual, NexeTarget } from "../lib/target"; import { getTarget, targetsEqual, NexeTarget } from '../lib/target'
import { import { pathExistsAsync, readFileAsync, execFileAsync, semverGt } from '../lib/util'
pathExistsAsync, import got from 'got'
readFileAsync, import { cpus } from 'os'
execFileAsync,
semverGt,
} from "../lib/util";
import axios from "axios";
import FormData from "form-data";
import { cpus } from "os";
const env = process.env, const env = process.env,
isPullRequest = env.BUILD_REASON === "PullRequest", isPullRequest = env.BUILD_REASON === 'PullRequest',
isWindows = process.platform === "win32", isWindows = process.platform === 'win32',
isLinux = process.platform === "linux", isLinux = process.platform === 'linux',
buildHost = env.AGENT_JOBNAME || (isWindows && "windows_2017_2015") || "", buildHost = env.AGENT_JOBNAME || (isWindows && 'windows_2017_2015') || '',
isMac = process.platform === "darwin", isMac = process.platform === 'darwin',
headers = { headers = {
Authorization: "token " + env.GITHUB_TOKEN, Authorization: 'token ' + env.GITHUB_TOKEN,
"User-Agent": "nexe (https://www.npmjs.com/package/nexe)", 'User-Agent': 'nexe (https://www.npmjs.com/package/nexe)',
}; }
if (require.main === module) { if (require.main === module) {
if (!isPullRequest) { if (!isPullRequest) {
build().catch((x) => { build().catch((x) => {
console.error(x); console.error(x)
process.exit(1); process.exit(1)
}); })
} }
} }
async function build() { async function build() {
const releases = await getUnBuiltReleases({ headers }); const releases = await getUnBuiltReleases({ headers })
if (!releases.length) { if (!releases.length) {
return; return
} }
const windowsBuild = releases.find((x) => x.platform === "windows"), const windowsBuild = releases.find((x) => x.platform === 'windows'),
macBuild = releases.find((x) => x.platform === "mac"), macBuild = releases.find((x) => x.platform === 'mac'),
linux = releases.find((x) => x.platform === "linux"), linux = releases.find((x) => x.platform === 'linux'),
alpine = releases.find((x) => x.platform === "alpine"); alpine = releases.find((x) => x.platform === 'alpine')
let target: NexeTarget | undefined; let target: NexeTarget | undefined
if (env.NEXE_VERSION) target = getTarget(env.NEXE_VERSION); if (env.NEXE_VERSION) target = getTarget(env.NEXE_VERSION)
else if (isWindows) target = windowsBuild; else if (isWindows) target = windowsBuild
else if (isMac) target = macBuild; else if (isMac) target = macBuild
else if (isLinux) target = linux; else if (isLinux) target = linux
if (buildHost.includes("alpine")) target = alpine; if (buildHost.includes('alpine')) target = alpine
if (!target) { if (!target) {
return console.log("Nothing to build..."); return console.log('Nothing to build...')
} }
if ( if (isWindows && buildHost.includes('2017') && !semverGt(target.version, '9.99.99')) {
isWindows && return console.log(`Not building ${target} on this host...`)
buildHost.includes("2017") &&
!semverGt(target.version, "9.99.99")
) {
return console.log(`Not building ${target} on this host...`);
} }
if ( if (isWindows && buildHost.includes('2015') && semverGt(target.version, '9.99.99')) {
isWindows && return console.log(`Not building ${target} on this host...`)
buildHost.includes("2015") &&
semverGt(target.version, "9.99.99")
) {
return console.log(`Not building ${target} on this host...`);
} }
const output = isWindows ? "./out.exe" : "./out", const output = isWindows ? './out.exe' : './out',
options = { options = {
mangle: false, mangle: false,
build: true, build: true,
verbose: Boolean(env.NEXE_VERBOSE!), verbose: Boolean(env.NEXE_VERBOSE!),
target, target,
make: ["-j" + cpus().length], make: ['-j' + cpus().length],
output, output,
}; }
console.log("Building: " + target + " on " + buildHost); console.log('Building: ' + target + ' on ' + buildHost)
const stop = keepalive(); const stop = keepalive()
if ( if (
[/*'arm7l', 'arm6l', 'arm64', */ "alpine"].includes(target.platform) && [/*'arm7l', 'arm6l', 'arm64', */ 'alpine'].includes(target.platform) &&
buildHost.includes("alpine") buildHost.includes('alpine')
) { ) {
await runDockerBuild(target); await runDockerBuild(target)
} else { } else {
await nexe.compile(options); await nexe.compile(options)
} }
stop(); stop()
if (await pathExistsAsync(output)) { if (await pathExistsAsync(output)) {
await assertNexeBinary(output); await assertNexeBinary(output)
const gitRelease = await getLatestGitRelease({ headers }), const gitRelease = await getLatestGitRelease({ headers }),
unbuiltReleases = await getUnBuiltReleases({ headers }); unbuiltReleases = await getUnBuiltReleases({ headers })
if (!unbuiltReleases.some((x) => targetsEqual(x, target!))) { if (!unbuiltReleases.some((x) => targetsEqual(x, target!))) {
console.log(`${target} already uploaded.`); console.log(`${target} already uploaded.`)
process.exit(0); process.exit(0)
return; return
} }
await got(gitRelease.upload_url.split('{')[0], {
// Create form data for file upload searchParams: { name: target.toString() },
const formData = new FormData(); body: await readFileAsync(output),
formData.append("file", await readFileAsync(output), { headers: {
filename: target.toString(), ...headers,
contentType: "application/octet-stream", 'Content-Type': 'application/octet-stream',
}); },
}).catch((reason) => {
const uploadUrl = gitRelease.upload_url.split("{")[0]; console.log(reason && reason.response && reason.response.body)
await axios throw reason
.post(uploadUrl, formData, { })
params: { name: target.toString() }, console.log(target + ' uploaded.')
headers: { process.exit(0)
...headers,
...formData.getHeaders(),
},
})
.catch((reason) => {
console.log(reason && reason.response && reason.response.data);
throw reason;
});
console.log(target + " uploaded.");
process.exit(0);
} }
} }
function keepalive() { function keepalive() {
const keepalive = setInterval(() => console.log("Building..."), 300 * 1000); const keepalive = setInterval(() => console.log('Building...'), 300 * 1000)
return () => clearInterval(keepalive); return () => clearInterval(keepalive)
} }
function assertNexeBinary(file: string) { function assertNexeBinary(file: string) {
return execFileAsync(file).catch((e) => { return execFileAsync(file).catch((e) => {
if (e && e.stack && e.stack.includes("Invalid Nexe binary")) { if (e && e.stack && e.stack.includes('Invalid Nexe binary')) {
return; return
} }
throw e; throw e
}); })
} }
+28 -39
View File
@@ -1,12 +1,12 @@
import { NexeTarget, architectures } from "../lib/target"; import { NexeTarget, architectures } from '../lib/target'
import { writeFileAsync, readFileAsync } from "../lib/util"; import { writeFileAsync, readFileAsync } from '../lib/util'
import axios from "axios"; import got from 'got'
import execa = require("execa"); import execa = require('execa')
import { appendFileSync } from "fs"; import { appendFileSync } from 'fs'
function alpine(target: NexeTarget) { function alpine(target: NexeTarget) {
return ` return `
FROM ${target.arch === "x64" ? "" : "i386/"}alpine:3.12 FROM ${target.arch === 'x64' ? '' : 'i386/'}alpine:3.12
RUN apk add --no-cache curl make gcc g++ binutils-gold python linux-headers paxctl libgcc libstdc++ git vim tar gzip wget RUN apk add --no-cache curl make gcc g++ binutils-gold python linux-headers paxctl libgcc libstdc++ git vim tar gzip wget
ENV NODE_VERSION=${target.version} ENV NODE_VERSION=${target.version}
ENV NEXE_VERSION=latest ENV NEXE_VERSION=latest
@@ -23,7 +23,7 @@ RUN rm /nexe_temp/\${NODE_VERSION}/out/Release/node && \
npm install -g nexe@\${NEXE_VERSION} npm install -g nexe@\${NEXE_VERSION}
RUN echo "console.log('hello world')" >> index.js && \ RUN echo "console.log('hello world')" >> index.js && \
nexe --build --no-mangle --temp /nexe_temp -c="--fully-static" -o out nexe --build --no-mangle --temp /nexe_temp -c="--fully-static" -o out
`.trim(); `.trim()
} }
function arm(target: NexeTarget) { function arm(target: NexeTarget) {
@@ -34,46 +34,35 @@ WORKDIR /
RUN yarn global add nexe@\${NEXE_VERSION} && \ RUN yarn global add nexe@\${NEXE_VERSION} && \
nexe --build --no-mangle -o out -t ${target.version} nexe --build --no-mangle -o out -t ${target.version}
`.trim(); `.trim()
} }
export async function runDockerBuild(target: NexeTarget) { export async function runDockerBuild(target: NexeTarget) {
//todo switch on alpine and arm //todo switch on alpine and arm
const dockerfile = alpine(target); const dockerfile = alpine(target)
await writeFileAsync("Dockerfile", dockerfile); await writeFileAsync('Dockerfile', dockerfile)
const outFilename = "nexe-docker-build-log.txt"; const outFilename = 'nexe-docker-build-log.txt'
await writeFileAsync(outFilename, ""); await writeFileAsync(outFilename, '')
const output: any = []; let output: any = []
try { try {
output.push(await execa(`docker build -t nexe-docker .`, { shell: true })); output.push(await execa(`docker build -t nexe-docker .`, { shell: true }))
output.push( output.push(await execa(`docker run -d --name nexe nexe-docker sh`, { shell: true }))
await execa(`docker run -d --name nexe nexe-docker sh`, { shell: true }) output.push(await execa(`docker cp nexe:/out out`, { shell: true }))
); output.push(await execa(`docker rm nexe`, { shell: true }))
output.push(await execa(`docker cp nexe:/out out`, { shell: true }));
output.push(await execa(`docker rm nexe`, { shell: true }));
} catch (e: any) { } catch (e: any) {
console.log("Error running docker"); console.log('Error running docker')
appendFileSync(outFilename, e.message); appendFileSync(outFilename, e.message)
} finally { } finally {
output.forEach((x: any) => { output.forEach((x: any) => {
appendFileSync(outFilename, x.stderr); appendFileSync(outFilename, x.stderr)
appendFileSync(outFilename, x.stdout); appendFileSync(outFilename, x.stdout)
}); })
await got(`https://transfer.sh/${Math.random().toString(36).substring(2)}.txt`, {
try { body: await readFileAsync(outFilename),
const response = await axios.put( method: 'PUT',
`https://transfer.sh/${Math.random().toString(36).substring(2)}.txt`, })
await readFileAsync(outFilename), .then((x) => console.log('Posted docker log: ', x.body))
{ .catch((e) => console.log('Error posting log', e))
headers: {
"Content-Type": "application/octet-stream",
},
}
);
console.log("Posted docker log: ", response.data);
} catch (e: any) {
console.log("Error posting log", e);
}
} }
} }
+17 -17
View File
@@ -1,31 +1,31 @@
import { writeFileSync, readFileSync } from "fs"; import { writeFileSync, readFileSync } from 'fs'
import { template } from "lodash"; import { template } from 'lodash'
/** /**
* post build step to insert code files into code files. * post build step to insert code files into code files.
* And the package.json version. * And the package.json version.
*/ */
cp("src/fs/package.json", "lib/fs/package.json"); cp('src/fs/package.json', 'lib/fs/package.json')
cp("src/fs/bootstrap.js", "lib/fs/bootstrap.js"); cp('src/fs/bootstrap.js', 'lib/fs/bootstrap.js')
cp("src/fs/README.md", "lib/fs/README.md"); cp('src/fs/README.md', 'lib/fs/README.md')
inject("lib/patches/third-party-main.js"); inject('lib/patches/third-party-main.js')
inject("lib/steps/shim.js"); inject('lib/steps/shim.js')
inject("lib/options.js", JSON.stringify(require("../package.json").version)); inject('lib/options.js', JSON.stringify(require('../package.json').version))
function inject(filename: string, version?: string) { function inject(filename: string, version?: string) {
const contents = template(readFileSync(filename, "utf8"), { const contents = template(readFileSync(filename, 'utf8'), {
interpolate: /['"]\{\{([\s\S]*?)\}\}['"]/, interpolate: /'\{\{([\s\S]*?)\}\}'/,
})({ })({
file: (path: string) => JSON.stringify(readFileSync(path, "utf8")), file: (path: string) => JSON.stringify(readFileSync(path, 'utf8')),
version, version,
}); })
writeFileSync(filename, contents); writeFileSync(filename, contents)
console.log(`Wrote: ${filename}`); console.log(`Wrote: ${filename}`)
} }
function cp(from: string, to: string) { function cp(from: string, to: string) {
const file = readFileSync(from); const file = readFileSync(from)
writeFileSync(to, file); writeFileSync(to, file)
console.log("Copied: ", from, "To: ", to); console.log('Copied: ', from, 'To: ', to)
} }
+11 -30
View File
@@ -1,42 +1,23 @@
const { mkdtemp, copyFile, realpath } = require('fs/promises') const { mkdtemp, copyFile } = require('fs/promises')
const os = require('os') const os = require('os')
const path = require('path') const path = require('path')
const rimraf = require('rimraf') const rimraf = require('rimraf')
const cp = require('child_process') const cp = require('child_process')
async function runTests() { async function runTests() {
const tempdir = await realpath(await mkdtemp(path.join(os.tmpdir(), 'nexe-integration-tests-'))) const tempdir = await mkdtemp(path.join(os.tmpdir(), 'nexe-integration-tests-'))
const secondTempdir = await mkdtemp(path.join(os.tmpdir(), 'nexe-integration-tests-without-executable-'))
const executable = path.join(tempdir, path.basename(process.argv[0])) const executable = path.join(tempdir, path.basename(process.argv[0]))
await copyFile(process.argv[0], executable) await copyFile(process.argv[0], executable)
process.on('beforeExit', () => { process.on('beforeExit', () => rimraf.sync(tempdir))
try { cp.spawn(executable,
rimraf.sync(tempdir) [
rimraf.sync(secondTempdir) path.join(tempdir, 'node_modules/mocha/bin/mocha.js'),
} catch(e) { path.join(tempdir, 'test/integration/tests.integration-spec.js')
console.error(`Ignoring error during integration test cleanup of "${tempdir}".`) ],
console.error(`Error: ${e}`) { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], cwd: tempdir }
} ).on('exit', (code) => {
process.exitCode = code
}) })
console.error('Running integration tests with the binary in the current working directory.')
spawnExecutable(tempdir, () => {
console.error('Running integration tests with the binary in another directory.')
spawnExecutable(secondTempdir)
})
function spawnExecutable(cwd, cb) {
cp.spawn(executable,
[
path.join(tempdir, 'node_modules/mocha/bin/mocha.js'),
path.join(tempdir, 'test/integration/tests.integration-spec.js')
],
{ stdio: ['inherit', 'inherit', 'inherit', 'ipc'], cwd }
).on('exit', (code) => {
process.exitCode = process.exitCode || code
if(cb) cb()
})
}
} }
runTests() runTests()
+1 -1
View File
@@ -43,7 +43,7 @@ describe('options', () => {
describe('remote', () => { describe('remote', () => {
it('should use default remote', () => { it('should use default remote', () => {
const options = normalizeOptions({}) const options = normalizeOptions({})
expect(options.remote).to.equal(`https://github.com/oxmc/nexe/releases/download/v${require('../package.json').version}/`) expect(options.remote).to.equal('https://github.com/nexe/nexe/releases/download/v3.3.3/')
}) })
it('should append trailing slash to third-party remote if necessary', () => { it('should append trailing slash to third-party remote if necessary', () => {
const options = normalizeOptions({ const options = normalizeOptions({
+9
View File
@@ -0,0 +1,9 @@
{
"rulesDirectory": ["tslint-plugin-prettier", "tslint-config-prettier"],
"linterOptions": {
"exclude": ["**/node_modules/**/*"]
},
"rules": {
"prettier": [true, { "semi": false, "printWidth": 100, "singleQuote": true }]
}
}