Skip to content

Commit 97b8d8c

Browse files
author
Sebastian Benjamin
committed
Build from any host to any target and fix JBrowseManager test
1 parent 6ba7c3d commit 97b8d8c

2 files changed

Lines changed: 158 additions & 93 deletions

File tree

Lines changed: 157 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,185 @@
1+
// Build SEA for all platforms (win/linux/macos) from any host.
2+
// - Creates sea-prep.blob once from resources/external/jbrowse.js
3+
// - Downloads official Node runtimes for the same Node version as process.execPath
4+
// - Injects the blob into each runtime with postject
5+
// - Writes outputs to resources/external/jb-cli:
6+
// cli-win.exe, cli-linux, cli-macos
7+
// - Cleans up temp files and resources/external/jbrowse.js at the end
8+
19
import { execFileSync, execSync, spawnSync } from 'node:child_process';
2-
import { copyFileSync, chmodSync, existsSync, mkdirSync, rmSync, renameSync, writeFileSync } from 'node:fs';
10+
import {
11+
copyFileSync, chmodSync, existsSync, mkdirSync, rmSync, renameSync, writeFileSync
12+
} from 'node:fs';
313
import { basename, join, resolve } from 'node:path';
4-
5-
const input = process.argv[2];
6-
if (!input) {
7-
console.error('Usage: node build-sea.mjs <input.js> [outputBaseName]');
8-
process.exit(1);
9-
}
10-
const inputAbs = resolve(input);
11-
const baseName = process.argv[3] || basename(input, '.js');
14+
import https from 'node:https';
15+
import { createWriteStream } from 'node:fs';
1216

1317
const ROOT = resolve('.');
1418
const OUTDIR = join(ROOT, 'resources', 'external', 'jb-cli');
15-
mkdirSync(OUTDIR, { recursive: true });
16-
17-
const platform = process.platform; // 'win32' | 'darwin' | 'linux'
18-
const outName =
19-
platform === 'win32' ? 'cli-win.exe' :
20-
platform === 'darwin' ? 'cli-macos' : 'cli-linux';
21-
22-
const tmpCfg = 'sea-config.json';
23-
const tmpBlob = 'sea-prep.blob';
24-
const tmpOut = `${baseName}${platform === 'win32' ? '.exe' : ''}`;
19+
const TMPDIR = join(ROOT, '.sea-tmp');
20+
const INPUT_JS = join(ROOT, 'resources', 'external', 'jbrowse.js');
21+
22+
const NODE_VERSION = process.versions.node;
23+
const DIST_BASE = process.env.NODE_DIST_URL || 'https://nodejs.org/dist';
24+
const TARGETS = [
25+
['win-x64', 'cli-win.exe', 'zip'],
26+
['linux-x64', 'cli-linux', 'tar.xz'],
27+
['darwin-x64','cli-macos', 'tar.xz'],
28+
// ['darwin-arm64','cli-macos-arm64','tar.xz'],
29+
// ['linux-arm64','cli-linux-arm64','tar.xz'],
30+
];
2531

26-
console.log(`SEA build: ${inputAbs} -> ${join(OUTDIR, outName)} [${platform}]`);
32+
const SENTINEL = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2';
33+
const POSTJECT_PKG = 'postject@1.0.0-alpha.6';
2734

28-
// 1) Create SEA config
29-
const cfg = {
30-
main: inputAbs,
31-
output: tmpBlob,
32-
disableExperimentalSEAWarning: true,
33-
};
34-
writeFileSync(tmpCfg, JSON.stringify(cfg, null, 2));
35+
function log(s){ console.log(s); }
36+
function warn(s){ console.warn(s); }
37+
function fail(e){ console.error(e); process.exit(1); }
3538

36-
// 2) Produce blob
37-
execFileSync(process.execPath, ['--experimental-sea-config', tmpCfg], { stdio: 'inherit' });
39+
function ensureDirs(){
40+
mkdirSync(OUTDIR, { recursive: true });
41+
mkdirSync(TMPDIR, { recursive: true });
42+
}
3843

39-
// 3) Copy current Node runtime as the base executable
40-
copyFileSync(process.execPath, tmpOut);
44+
function httpDownload(url, dest){
45+
return new Promise((resolveP, rejectP)=>{
46+
const file = createWriteStream(dest);
47+
https.get(url, res=>{
48+
if(res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location){
49+
httpDownload(res.headers.location, dest).then(resolveP, rejectP);
50+
return;
51+
}
52+
if(res.statusCode !== 200){
53+
rejectP(new Error(`Download failed ${res.statusCode}: ${url}`));
54+
return;
55+
}
56+
res.pipe(file);
57+
file.on('finish', ()=>file.close(()=>resolveP(void 0)));
58+
}).on('error', err=>{ rejectP(err); });
59+
});
60+
}
4161

42-
// 4) Remove signature (it'll be invalid once we postject the Jbrowse CLI into the node runtime executable)
43-
if (platform === 'darwin') {
44-
try { spawnSync('codesign', ['--remove-signature', tmpOut], { stdio: 'inherit' }); } catch {}
62+
function runOrThrow(cmd, args, opts={}){
63+
execFileSync(cmd, args, { stdio: 'inherit', ...opts });
4564
}
46-
if (platform === 'win32') {
47-
try { spawnSync('signtool', ['remove', '/s', tmpOut], { stdio: 'inherit' }); } catch {}
65+
66+
function shellOrThrow(command){
67+
execSync(command, { stdio: 'inherit', shell: true });
4868
}
4969

50-
// Helper: run postject with multiple fallbacks
51-
function runPostject(argsList) {
52-
const isWin = platform === 'win32';
70+
// postject runner with fallbacks
71+
function runPostject(args){
72+
const isWin = process.platform === 'win32';
5373
const npxCmd = isWin ? 'npx.cmd' : 'npx';
5474
const npmCmd = isWin ? 'npm.cmd' : 'npm';
55-
const postjectPkg = 'postject@1.0.0-alpha.6';
56-
57-
// Attempt 1: npx (execFileSync)
58-
try {
59-
execFileSync(npxCmd, ['--yes', postjectPkg, ...argsList], { stdio: 'inherit' });
60-
return;
61-
} catch (e1) {
62-
console.warn('[postject] npx (execFileSync) failed, trying npm exec…');
63-
// Attempt 2: npm exec (execFileSync)
64-
try {
65-
execFileSync(npmCmd, ['exec', '-y', postjectPkg, '--', ...argsList], { stdio: 'inherit' });
66-
return;
67-
} catch (e2) {
68-
console.warn('[postject] npm exec (execFileSync) failed, trying shell npx…');
69-
// Attempt 3: npx via shell (execSync)
70-
try {
71-
const cmd = `npx --yes ${postjectPkg} ${argsList.map(a => `"${a}"`).join(' ')}`;
72-
execSync(cmd, { stdio: 'inherit', shell: true });
73-
return;
74-
} catch (e3) {
75-
console.warn('[postject] shell npx failed, trying shell npm exec…');
76-
// Attempt 4: npm exec via shell (execSync)
77-
const cmd2 = `npm exec -y ${postjectPkg} -- ${argsList.map(a => `"${a}"`).join(' ')}`;
78-
execSync(cmd2, { stdio: 'inherit', shell: true });
79-
}
80-
}
81-
}
75+
try { runOrThrow(npxCmd, ['--yes', POSTJECT_PKG, ...args]); return; }
76+
catch{ warn('[postject] npx failed, trying npm exec…'); }
77+
try { runOrThrow(npmCmd, ['exec', '-y', POSTJECT_PKG, '--', ...args]); return; }
78+
catch{ warn('[postject] npm exec failed, trying shell npx…'); }
79+
try { shellOrThrow(`npx --yes ${POSTJECT_PKG} ${args.map(a=>`"${a}"`).join(' ')}`); return; }
80+
catch{ warn('[postject] shell npx failed, trying shell npm exec…'); }
81+
shellOrThrow(`npm exec -y ${POSTJECT_PKG} -- ${args.map(a=>`"${a}"`).join(' ')}`);
8282
}
8383

84-
// Postject magic from the docs to do the jbrowse->node appending
85-
const SENTINEL = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2';
86-
const postjectArgs = platform === 'darwin'
87-
? [tmpOut, 'NODE_SEA_BLOB', tmpBlob, '--sentinel-fuse', SENTINEL, '--macho-segment-name', 'NODE_SEA']
88-
: [tmpOut, 'NODE_SEA_BLOB', tmpBlob, '--sentinel-fuse', SENTINEL];
84+
function extractZip(zipPath, outDir){
85+
if (process.platform === 'win32') {
86+
shellOrThrow(`powershell -NoProfile -Command "Expand-Archive -Force '${zipPath.replace(/'/g, "''")}' '${outDir.replace(/'/g, "''")}'"`);
87+
} else {
88+
shellOrThrow(`unzip -o "${zipPath}" -d "${outDir}"`);
89+
}
90+
}
8991

90-
// 5) Inject the SEA blob
91-
runPostject(postjectArgs);
92+
// extract only bin/node from tar.xz to avoid symlink issues on Windows
93+
function extractNodeFromTarXz(tarxzPath, outDir, target) {
94+
const innerPath = `node-v${NODE_VERSION}-${target}/bin/node`;
95+
try { shellOrThrow(`tar -xJf "${tarxzPath}" -C "${outDir}" "${innerPath}"`); }
96+
catch { shellOrThrow(`bsdtar -xf "${tarxzPath}" -C "${outDir}" "${innerPath}"`); }
97+
return join(outDir, innerPath);
98+
}
9299

93-
// 6) Re-sign for the new combined executable
94-
if (platform === 'darwin') {
95-
try { spawnSync('codesign', ['--sign', '-', tmpOut], { stdio: 'inherit' }); } catch {}
100+
function nodeBinaryPathFromExtract(dir, target){
101+
const base = `node-v${NODE_VERSION}-${target}`;
102+
if (target.startsWith('win')) return join(dir, base, 'node.exe');
103+
return join(dir, base, 'bin', 'node');
96104
}
97-
if (platform === 'win32') {
98-
try { spawnSync('signtool', ['sign', '/fd', 'SHA256', tmpOut], { stdio: 'inherit' }); } catch {}
105+
106+
function injectForTarget(target, outName, archiveExt, blobPath){
107+
const distUrl = `${DIST_BASE}/v${NODE_VERSION}/node-v${NODE_VERSION}-${target}.${archiveExt}`;
108+
const dlPath = join(TMPDIR, `node-${target}.${archiveExt}`);
109+
110+
log(`\n=== Target ${target} ===`);
111+
log(`Downloading: ${distUrl}`);
112+
httpDownload(distUrl, dlPath).then(()=>{
113+
log('Extracting…');
114+
let nodePath;
115+
if (archiveExt === 'zip') {
116+
extractZip(dlPath, TMPDIR);
117+
nodePath = nodeBinaryPathFromExtract(TMPDIR, target);
118+
} else {
119+
nodePath = extractNodeFromTarXz(dlPath, TMPDIR, target);
120+
}
121+
if (!existsSync(nodePath)) fail(`node binary not found in ${nodePath}`);
122+
123+
const workExe = join(TMPDIR, `work-${outName}`);
124+
copyFileSync(nodePath, workExe);
125+
126+
const postjectArgs = target.startsWith('darwin')
127+
? [workExe, 'NODE_SEA_BLOB', blobPath, '--sentinel-fuse', SENTINEL, '--macho-segment-name', 'NODE_SEA']
128+
: [workExe, 'NODE_SEA_BLOB', blobPath, '--sentinel-fuse', SENTINEL];
129+
130+
log('Injecting SEA blob…');
131+
runPostject(postjectArgs);
132+
133+
if (!target.startsWith('win')) chmodSync(workExe, 0o755);
134+
135+
const finalPath = join(OUTDIR, outName);
136+
if (existsSync(finalPath)) rmSync(finalPath, { force: true });
137+
renameSync(workExe, finalPath);
138+
139+
rmSync(dlPath, { force: true });
140+
try { rmSync(join(TMPDIR, `node-v${NODE_VERSION}-${target}`), { recursive: true, force: true }); } catch {}
141+
log(`Wrote ${finalPath}`);
142+
}).catch(fail);
99143
}
100144

101-
// 7) POSIX chmod
102-
if (platform !== 'win32') {
103-
chmodSync(tmpOut, 0o755);
145+
function buildBlobOnce(){
146+
const cfgPath = join(TMPDIR, 'sea-config.json');
147+
const blobPath = join(TMPDIR, 'sea-prep.blob');
148+
log('Creating SEA blob…');
149+
const cfg = { main: INPUT_JS, output: blobPath, disableExperimentalSEAWarning: true };
150+
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
151+
runOrThrow(process.execPath, ['--experimental-sea-config', cfgPath]);
152+
return blobPath;
104153
}
105154

106-
// 8) Move to final destination
107-
const finalPath = join(OUTDIR, outName);
108-
if (existsSync(finalPath)) rmSync(finalPath, { force: true });
109-
renameSync(tmpOut, finalPath);
155+
async function main(){
156+
ensureDirs();
157+
if (!existsSync(INPUT_JS)) fail(`Missing ${INPUT_JS}. Run your fetch step first.`);
158+
rmSync(TMPDIR, { recursive: true, force: true });
159+
mkdirSync(TMPDIR, { recursive: true });
160+
161+
const blob = buildBlobOnce();
162+
163+
for (const [target, outName, ext] of TARGETS) {
164+
await new Promise((resolveP, rejectP)=>{
165+
try {
166+
injectForTarget(target, outName, ext, blob);
167+
const interval = setInterval(()=>{
168+
if (existsSync(join(OUTDIR, outName))) {
169+
clearInterval(interval);
170+
resolveP();
171+
}
172+
}, 500);
173+
} catch (e) {
174+
rejectP(e);
175+
}
176+
});
177+
}
110178

111-
// 9) Cleanup
112-
rmSync(tmpCfg, { force: true });
113-
rmSync(tmpBlob, { force: true });
179+
try { rmSync(TMPDIR, { recursive: true, force: true }); } catch {}
180+
try { if (existsSync(INPUT_JS)) rmSync(INPUT_JS, { force: true }); } catch {}
114181

115-
const jbrowseJsPath = join(ROOT, 'resources', 'external', 'jbrowse.js');
116-
if (existsSync(jbrowseJsPath)) {
117-
rmSync(jbrowseJsPath, { force: true });
182+
log('\nAll targets built.');
118183
}
119184

120-
console.log(`Done: ${finalPath}`);
185+
main().catch(fail);

jbrowse/src/org/labkey/jbrowse/JBrowseManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ public void testJBrowseCli() throws Exception
241241
SimpleScriptWrapper wrapper = new SimpleScriptWrapper(_log);
242242
wrapper.setThrowNonZeroExits(false);
243243

244-
String output = wrapper.executeWithOutput(Arrays.asList(exe.getPath(), "help"));
244+
String output = wrapper.executeWithOutput(Arrays.asList(exe.getPath(), "--help"));
245245
if (wrapper.getLastReturnCode() != 0)
246246
{
247247
_log.error("Non-zero exit from testJBrowseCli: " + wrapper.getLastReturnCode());

0 commit comments

Comments
 (0)