1+ import { execFileSync , execSync , spawnSync } from 'node:child_process' ;
2+ import { copyFileSync , chmodSync , existsSync , mkdirSync , rmSync , renameSync , writeFileSync } from 'node:fs' ;
3+ 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' ) ;
12+
13+ const ROOT = resolve ( '.' ) ;
14+ 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' : '' } ` ;
25+
26+ console . log ( `SEA build: ${ inputAbs } -> ${ join ( OUTDIR , outName ) } [${ platform } ]` ) ;
27+
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+
36+ // 2) Produce blob
37+ execFileSync ( process . execPath , [ '--experimental-sea-config' , tmpCfg ] , { stdio : 'inherit' } ) ;
38+
39+ // 3) Copy current Node runtime as the base executable
40+ copyFileSync ( process . execPath , tmpOut ) ;
41+
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 { }
45+ }
46+ if ( platform === 'win32' ) {
47+ try { spawnSync ( 'signtool' , [ 'remove' , '/s' , tmpOut ] , { stdio : 'inherit' } ) ; } catch { }
48+ }
49+
50+ // Helper: run postject with multiple fallbacks
51+ function runPostject ( argsList ) {
52+ const isWin = platform === 'win32' ;
53+ const npxCmd = isWin ? 'npx.cmd' : 'npx' ;
54+ 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+ }
82+ }
83+
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 ] ;
89+
90+ // 5) Inject the SEA blob
91+ runPostject ( postjectArgs ) ;
92+
93+ // 6) Re-sign for the new combined executable
94+ if ( platform === 'darwin' ) {
95+ try { spawnSync ( 'codesign' , [ '--sign' , '-' , tmpOut ] , { stdio : 'inherit' } ) ; } catch { }
96+ }
97+ if ( platform === 'win32' ) {
98+ try { spawnSync ( 'signtool' , [ 'sign' , '/fd' , 'SHA256' , tmpOut ] , { stdio : 'inherit' } ) ; } catch { }
99+ }
100+
101+ // 7) POSIX chmod
102+ if ( platform !== 'win32' ) {
103+ chmodSync ( tmpOut , 0o755 ) ;
104+ }
105+
106+ // 8) Move to final destination
107+ const finalPath = join ( OUTDIR , outName ) ;
108+ if ( existsSync ( finalPath ) ) rmSync ( finalPath , { force : true } ) ;
109+ renameSync ( tmpOut , finalPath ) ;
110+
111+ // 9) Cleanup
112+ rmSync ( tmpCfg , { force : true } ) ;
113+ rmSync ( tmpBlob , { force : true } ) ;
114+
115+ const jbrowseJsPath = join ( ROOT , 'resources' , 'external' , 'jbrowse.js' ) ;
116+ if ( existsSync ( jbrowseJsPath ) ) {
117+ rmSync ( jbrowseJsPath , { force : true } ) ;
118+ }
119+
120+ console . log ( `Done: ${ finalPath } ` ) ;
0 commit comments