Skip to content

Commit

Permalink
Added Windows/zip support, caching
Browse files Browse the repository at this point in the history
  • Loading branch information
james-pre committed Nov 29, 2024
1 parent 10765f3 commit 8a5b78a
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 35 deletions.
3 changes: 2 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ export default tseslint.config({
'@typescript-eslint/require-await': 'warn',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'warn',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/only-throw-error': 'off',
},
});
27 changes: 26 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,22 @@
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "tsc -p tsconfig.json --noEmit && eslint src",
"build": "tsconfig -p tsconfig.json",
"build": "tsc -p tsconfig.json",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"@types/adm-zip": "^0.5.7",
"@types/node": "^22.10.1",
"@types/tar": "^6.1.13",
"eslint": "^9.15.0",
"globals": "^15.12.0",
"prettier": "^3.4.1",
"typescript": "^5.7.2",
"typescript-eslint": "^8.16.0"
},
"dependencies": {
"@types/tar": "^6.1.13",
"adm-zip": "^0.5.16",
"postject": "^1.0.0-alpha.6",
"tar": "^7.4.3"
}
Expand Down
106 changes: 76 additions & 30 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#!/bin/env node
import { execSync } from 'node:child_process';
import * as fs from 'node:fs';
import { dirname, join, parse } from 'node:path';
import { parseArgs } from 'node:util';
import { inject } from 'postject';
import { extract } from 'tar';
import AdmZip from 'adm-zip';

const { values: options, positionals } = parseArgs({
options: {
Expand All @@ -13,6 +15,7 @@ const { values: options, positionals } = parseArgs({
output: { short: 'o', type: 'string' },
clean: { type: 'boolean', default: false },
node: { short: 'N', type: 'string', default: 'v' + process.versions.node },
platform: { short: 'P', type: 'string', multiple: true, default: [process.platform + '-' + process.arch] },
},
allowPositionals: true,
});
Expand All @@ -29,7 +32,9 @@ Options:
--quiet,-q Hide non-error output
--verbose,-w Show all output
--output, -o <prefix> The output prefix
--tempd Temporary files directory,
--clean Remove temporary files
--node,-N <version> Specify the Node version
--platform, -P <plat> Specify which platform(s) to build for
`);
process.exit(0);
}
Expand All @@ -39,21 +44,31 @@ if (options.verbose && options.quiet) {
process.exit(1);
}

if (options.clean) {
_log('Removing temporary files...');
fs.rmSync(tempDir, { recursive: true, force: true });
}

if (positionals.length != 1) {
if (options.clean) process.exit(0);
console.error('Incorrect number of positional arguments, expected 1');
process.exit(1);
}

const prefix = options.output ?? parse(positionals[0]).name;
const entryName = parse(positionals[0]).name;

if (options.clean) {
_log('Removing temporary files...');
fs.rmSync(tempDir, { recursive: true });
let prefix = options.output ?? entryName;

if (/\w$/.test(prefix)) {
prefix += '-';
}
fs.mkdirSync(tempDir, { recursive: true });

const configPath = join(tempDir, 'sea.json'),
blobPath = join(tempDir, 'server.blob');
_log('Prefix:', prefix);

fs.mkdirSync(join(tempDir, 'node'), { recursive: true });

const configPath = join(tempDir, entryName + '.json'),
blobPath = join(tempDir, entryName + '.blob');

fs.writeFileSync(
configPath,
Expand All @@ -69,45 +84,76 @@ const blob = fs.readFileSync(blobPath);

fs.mkdirSync(prefix.endsWith('/') ? prefix : dirname(prefix), { recursive: true });

/**
* Builds a SEA for a target (e.g. win-x64, linux-arm64)
*/
async function buildSEA(target: string) {
!options.quiet && console.log('Creating SEA for:', target);
const isWindows = target.startsWith('win');

const seaPath = join(prefix, isWindows ? target + '.exe' : target);
async function getNode(archiveBase: string) {
const isWindows = archiveBase.startsWith('node-win');

const archiveFile = `node-${options.node}-${target}.${isWindows ? 'zip' : 'tar.gz'}`;
const archiveFile = archiveBase + '.' + (isWindows ? 'zip' : 'tar.gz');
const archivePath = join(tempDir, archiveFile);
try {
const url = `https://nodejs.org/dist/${options.node}/${archiveFile}`;
_log('Fetching:', url);
const response = await fetch(url);
fs.writeFileSync(archivePath, new Uint8Array(await response.arrayBuffer()));
} catch {
console.error(`Failed to download Node v${options.node} for ${target}`);
const execName = join(tempDir, archiveBase);

if (fs.existsSync(execName)) {
_log('Found existing:', archiveBase);
return;
}

const extractedDir = join(tempDir, target);
fs.mkdirSync(extractedDir, { recursive: true });
if (fs.existsSync(archivePath)) {
_log('Found existing archive:', archiveFile);
} else {
try {
const url = `https://nodejs.org/dist/${options.node}/${archiveFile}`;
_log('Fetching:', url);
const response = await fetch(url);
fs.writeFileSync(archivePath, new Uint8Array(await response.arrayBuffer()));
} catch {
throw ['Failed to download:', archiveBase];
}
}

_log('Extracting:', archivePath);
_log('Extracting:', archiveFile);
if (isWindows) {
const zip = new AdmZip(archivePath);
const data = zip.readFile(isWindows ? 'node.exe' : 'bin/node');
if (!data) {
throw ['Missing node executable:', archiveBase];
}
fs.writeFileSync(execName, data);
} else {
await extract({
file: archivePath,
gzip: true,
cwd: extractedDir,
cwd: join(tempDir, 'node'),
});
fs.copyFileSync(join(tempDir, 'node', archiveBase, isWindows ? 'node.exe' : 'bin/node'), execName);
}
}

/**
* Builds a SEA for a target (e.g. win-x64, linux-arm64)
*/
async function buildSEA(target: string) {
!options.quiet && console.log('Creating SEA for:', target);
const isWindows = target.startsWith('win');

const seaPath = prefix + (isWindows ? target + '.exe' : target);

const archiveBase = `node-${options.node}-${target}`;

try {
await getNode(archiveBase);
} catch (e: any) {
console.error(...(Array.isArray(e) ? e : [e]));
return;
}

fs.mkdirSync(dirname(seaPath), { recursive: true });
fs.copyFileSync(join(extractedDir, isWindows ? 'node.exe' : 'node'), seaPath);
fs.copyFileSync(join(tempDir, archiveBase), seaPath);
_log('Injecting:', seaPath);
inject(seaPath, 'NODE_SEA_BLOB', blob, {
await inject(seaPath, 'NODE_SEA_BLOB', blob, {
machoSegmentName: 'NODE_SEA',
sentinelFuse: 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
});
}

for (const target of options.platform) {
await buildSEA(target);
}
2 changes: 1 addition & 1 deletion src/postject.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ declare module 'postject' {
sentinelFuse?: string;
}

function inject(filename: string, resourceName: string, resourceData: Buffer, options: InjectOptions): void;
function inject(filename: string, resourceName: string, resourceData: Buffer, options: InjectOptions): Promise<void>;
}

0 comments on commit 8a5b78a

Please sign in to comment.