Skip to content

Commit 7477bc1

Browse files
committed
Implement standalone executables
1 parent def09c5 commit 7477bc1

20 files changed

+1479
-115
lines changed

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ Guide for future agents working on this codebase. Focus on traps, cross-cutting
6767
- `prepare` runs `npm run clean && npm run build`; npm package bins point to `dist/**`. Always rebuild before publishing so dist matches src.
6868
- `helpers/prepublishOnly.js` enforces branch `trunk` for `npm publish --tag latest` and optionally reruns `npm test`. Release flows expect a clean node version that satisfies `engines.node`.
6969

70+
## Standalone SEA Packaging
71+
- Canonical runbook for standalone executable build/signing is in `docs/SEA-BUILD-SIGNING.md`. Use it for macOS, Linux, Windows native, and WSL-mediated Windows builds.
72+
- SEA builds are Node 22 only (enforced in `helpers/build-sea.js`); always verify `node -v` before `npm run build:sea`.
73+
- The executable is self-contained for Node runtime + JS deps, but `dev-env` commands still require host Docker/Compose availability.
74+
7075
## Common Pitfalls Checklist
7176
- Running CLI without a token opens a browser (`open`) and waits for interactive input—pass `--help` or set `WPVIP_DEPLOY_TOKEN` in automation.
7277
- Forgetting `--app/--env` or alias when a command expects them triggers extra GraphQL lookups and prompts; in headless contexts set `_opts.appContext=false` or supply explicit flags.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# CLAUDE
22

33
For guidance on working in this repo, traps, and migration notes, see `AGENTS.md`.
4+
For standalone SEA build/signing by platform, see `docs/SEA-BUILD-SIGNING.md`.

docs/SEA-BUILD-SIGNING.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# SEA Build and Signing Runbook
2+
3+
Purpose: build and sign the standalone VIP CLI executable (`dist/sea/vip` or `dist/sea/vip.exe`) for each supported platform.
4+
5+
This repo uses `helpers/build-sea.js` and the `npm run build:sea` script. SEA build is pinned to Node 22.
6+
7+
## Shared Prerequisites
8+
- Use Node 22.x exactly for SEA builds.
9+
- Install dependencies before building: `npm ci`.
10+
- Build from repo root.
11+
- The build script embeds:
12+
- Node runtime
13+
- bundled CLI code (`src/bin/vip-sea.js`)
14+
- SEA assets and runtime dependency archive (`sea.node_modules.tgz`)
15+
- Output paths:
16+
- Unix: `dist/sea/vip`
17+
- Windows: `dist/sea/vip.exe`
18+
19+
## Shared Build Steps
20+
```bash
21+
npm ci
22+
npm run build
23+
npm run build:sea
24+
```
25+
26+
Quick smoke checks after every build:
27+
```bash
28+
dist/sea/vip --version
29+
dist/sea/vip whoami --help
30+
dist/sea/vip dev-env info --help
31+
```
32+
33+
## macOS (native)
34+
Node/tool setup:
35+
```bash
36+
export NVM_DIR="$HOME/.nvm"
37+
. "$NVM_DIR/nvm.sh"
38+
nvm use 22
39+
node -v
40+
```
41+
42+
Build:
43+
```bash
44+
npm ci
45+
npm run build
46+
npm run build:sea
47+
```
48+
49+
Notes:
50+
- `helpers/build-sea.js` already does ad-hoc signing (`codesign --sign -`) after blob injection so local execution works.
51+
- For distribution, replace ad-hoc signature with a real Developer ID certificate.
52+
53+
Distribution signing:
54+
```bash
55+
codesign --remove-signature dist/sea/vip
56+
codesign --sign "Developer ID Application: <TEAM/ORG>" --force --options runtime dist/sea/vip
57+
codesign --verify --strict --verbose=2 dist/sea/vip
58+
spctl -a -t exec -vv dist/sea/vip
59+
```
60+
61+
## Linux (native)
62+
Node/tool setup:
63+
```bash
64+
node -v
65+
```
66+
(Use Node 22 before build.)
67+
68+
Build:
69+
```bash
70+
npm ci
71+
npm run build
72+
npm run build:sea
73+
chmod +x dist/sea/vip
74+
```
75+
76+
Signing guidance:
77+
- Linux does not have a universal OS-enforced Authenticode-style executable signature.
78+
- Recommended: publish checksums and detached signatures.
79+
80+
Checksum + GPG example:
81+
```bash
82+
sha256sum dist/sea/vip > dist/sea/vip.sha256
83+
gpg --armor --detach-sign dist/sea/vip
84+
```
85+
86+
Cosign blob example:
87+
```bash
88+
cosign sign-blob --yes --output-signature dist/sea/vip.sig dist/sea/vip
89+
cosign verify-blob --signature dist/sea/vip.sig dist/sea/vip
90+
```
91+
92+
## Windows (native)
93+
Use PowerShell or `cmd.exe` on Windows (not WSL) when producing Windows artifacts.
94+
95+
Build:
96+
```powershell
97+
npm ci
98+
npm run build
99+
npm run build:sea
100+
.\dist\sea\vip.exe --version
101+
```
102+
103+
Authenticode signing (SignTool):
104+
```powershell
105+
signtool sign /fd SHA256 /td SHA256 /tr http://timestamp.digicert.com /a .\dist\sea\vip.exe
106+
signtool verify /pa /v .\dist\sea\vip.exe
107+
```
108+
109+
If your cert is in a PFX file:
110+
```powershell
111+
signtool sign /f C:\path\cert.pfx /p <PFX_PASSWORD> /fd SHA256 /td SHA256 /tr http://timestamp.digicert.com .\dist\sea\vip.exe
112+
```
113+
114+
## Windows from WSL
115+
Important: WSL builds Linux binaries by default.
116+
117+
- If target is Linux binary: build/sign inside WSL using the Linux flow.
118+
- If target is Windows `.exe`: run the build and signing commands in Windows context.
119+
120+
From WSL, invoke Windows PowerShell for a Windows-target build:
121+
```bash
122+
WIN_REPO_PATH="$(wslpath -w "$PWD")"
123+
powershell.exe -NoProfile -Command "Set-Location '$WIN_REPO_PATH'; npm ci; npm run build; npm run build:sea"
124+
```
125+
126+
Then sign in Windows context:
127+
```bash
128+
powershell.exe -NoProfile -Command "Set-Location '$WIN_REPO_PATH'; signtool sign /fd SHA256 /td SHA256 /tr http://timestamp.digicert.com /a .\\dist\\sea\\vip.exe; signtool verify /pa /v .\\dist\\sea\\vip.exe"
129+
```
130+
131+
## Release Checklist for Agents
132+
- Confirm Node 22 before SEA build.
133+
- Confirm artifact type matches target OS (`vip` vs `vip.exe`).
134+
- Run smoke checks on the produced executable.
135+
- Apply platform-appropriate signature method.
136+
- Verify signature/checksum before publishing.
137+
- Record signing method and timestamp authority in release notes.

helpers/build-sea.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env node
2+
3+
const { buildSync } = require( 'esbuild' );
4+
const { spawnSync } = require( 'node:child_process' );
5+
const { chmodSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } = require( 'node:fs' );
6+
const path = require( 'node:path' );
7+
const tar = require( 'tar' );
8+
9+
const SEA_FUSE = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2';
10+
11+
const projectRoot = path.resolve( __dirname, '..' );
12+
const seaDir = path.join( projectRoot, 'dist', 'sea' );
13+
const bundlePath = path.join( seaDir, 'vip.bundle.cjs' );
14+
const blobPath = path.join( seaDir, 'vip.blob' );
15+
const seaConfigPath = path.join( seaDir, 'sea-config.json' );
16+
const executablePath = path.join( seaDir, process.platform === 'win32' ? 'vip.exe' : 'vip' );
17+
const nodeModulesArchivePath = path.join( seaDir, 'node_modules.tgz' );
18+
19+
function run( command, args, options = {} ) {
20+
const result = spawnSync( command, args, {
21+
cwd: projectRoot,
22+
stdio: 'inherit',
23+
...options,
24+
} );
25+
26+
if ( result.status !== 0 ) {
27+
process.exit( result.status || 1 );
28+
}
29+
}
30+
31+
function ensureNode22() {
32+
const major = Number( process.versions.node.split( '.' )[ 0 ] );
33+
if ( major !== 22 ) {
34+
console.error(
35+
`Error: SEA build requires Node 22.x. Current version is ${ process.versions.node }.`
36+
);
37+
process.exit( 1 );
38+
}
39+
}
40+
41+
async function createRuntimeArchive() {
42+
await tar.c(
43+
{
44+
cwd: projectRoot,
45+
file: nodeModulesArchivePath,
46+
gzip: true,
47+
portable: true,
48+
noMtime: true,
49+
},
50+
[ 'node_modules' ]
51+
);
52+
}
53+
54+
function writeSeaConfig() {
55+
const config = {
56+
main: bundlePath,
57+
output: blobPath,
58+
disableExperimentalSEAWarning: true,
59+
assets: {
60+
'dev-env.lando.template.yml.ejs': path.join(
61+
projectRoot,
62+
'assets',
63+
'dev-env.lando.template.yml.ejs'
64+
),
65+
'dev-env.nginx.template.conf.ejs': path.join(
66+
projectRoot,
67+
'assets',
68+
'dev-env.nginx.template.conf.ejs'
69+
),
70+
'sea.node_modules.tgz': nodeModulesArchivePath,
71+
},
72+
};
73+
74+
writeFileSync( seaConfigPath, `${ JSON.stringify( config, null, 2 ) }\n`, 'utf8' );
75+
}
76+
77+
function buildBundle() {
78+
buildSync( {
79+
entryPoints: [ path.join( projectRoot, 'src', 'bin', 'vip-sea.js' ) ],
80+
bundle: true,
81+
platform: 'node',
82+
target: 'node22',
83+
format: 'cjs',
84+
outfile: bundlePath,
85+
external: [
86+
'@postman/node-keytar',
87+
'@postman/node-keytar/*',
88+
'cpu-features',
89+
'cpu-features/*',
90+
'lando',
91+
'lando/*',
92+
'ssh2',
93+
'ssh2/*',
94+
'*.node',
95+
],
96+
} );
97+
}
98+
99+
function stripBundleShebang() {
100+
const bundleContent = readFileSync( bundlePath, 'utf8' );
101+
if ( ! bundleContent.startsWith( '#!' ) ) {
102+
return;
103+
}
104+
105+
writeFileSync( bundlePath, bundleContent.replace( /^#![^\n]*\n/, '' ), 'utf8' );
106+
}
107+
108+
function buildBlob() {
109+
run( process.execPath, [ '--experimental-sea-config', seaConfigPath ] );
110+
}
111+
112+
function prepareExecutable() {
113+
copyFileSync( process.execPath, executablePath );
114+
115+
if ( process.platform === 'darwin' ) {
116+
run( 'codesign', [ '--remove-signature', executablePath ] );
117+
}
118+
}
119+
120+
function injectBlob() {
121+
const postjectCli = require.resolve( 'postject/dist/cli.js' );
122+
const args = [
123+
postjectCli,
124+
executablePath,
125+
'NODE_SEA_BLOB',
126+
blobPath,
127+
'--sentinel-fuse',
128+
SEA_FUSE,
129+
];
130+
131+
if ( process.platform === 'darwin' ) {
132+
args.push( '--macho-segment-name', 'NODE_SEA' );
133+
}
134+
135+
if ( process.platform === 'windows' ) {
136+
args.push( '--overwrite' );
137+
}
138+
139+
run( process.execPath, args );
140+
}
141+
142+
function finalizeExecutable() {
143+
if ( process.platform === 'darwin' ) {
144+
run( 'codesign', [ '--sign', '-', '--force', executablePath ] );
145+
}
146+
147+
if ( process.platform !== 'win32' ) {
148+
chmodSync( executablePath, 0o755 );
149+
}
150+
}
151+
152+
async function main() {
153+
ensureNode22();
154+
mkdirSync( seaDir, { recursive: true } );
155+
await createRuntimeArchive();
156+
writeSeaConfig();
157+
buildBundle();
158+
stripBundleShebang();
159+
buildBlob();
160+
prepareExecutable();
161+
injectBlob();
162+
finalizeExecutable();
163+
164+
console.log( `SEA executable written to ${ executablePath }` );
165+
}
166+
167+
void main();

0 commit comments

Comments
 (0)