seabox 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,697 +1,701 @@
1
- /**
2
- * multi-target-builder.mjs
3
- * Orchestrate parallel builds for multiple target platforms.
4
- */
5
-
6
- import fs from 'fs';
7
- import path from 'path';
8
- import { execSync } from 'child_process';
9
- import crypto from 'crypto';
10
- import { fileURLToPath } from 'url';
11
- import Module from 'module';
12
-
13
- import { bundleWithRollup } from './rolldown-bundler.mjs';
14
- import { BuildCache } from './build-cache.mjs';
15
- import { parseTarget } from './config.mjs';
16
- import { generateManifest, serializeManifest } from './manifest.mjs';
17
- import { createSeaConfig, writeSeaConfigJson, generateBlob } from './blob.mjs';
18
- import { fetchNodeBinary } from './fetch-node.mjs';
19
- import { injectBlob } from './inject.mjs';
20
- import { generateEncryptionKey, encryptAssets, keyToObfuscatedCode } from './crypto-assets.mjs';
21
- import { obfuscateBootstrap } from './obfuscate.mjs';
22
- import { bundleEntry } from './entry-bundler.mjs';
23
- import * as diag from './diagnostics.mjs';
24
-
25
- const __filename = fileURLToPath(import.meta.url);
26
- const __dirname = path.dirname(__filename);
27
- const require = Module.createRequire(import.meta.url);
28
-
29
- /**
30
- * Multi-target build orchestrator
31
- */
32
- export class MultiTargetBuilder {
33
- /**
34
- * @param {import('./config.mjs').SeaboxConfigV2} config - Build configuration
35
- * @param {string} projectRoot - Project root directory
36
- */
37
- constructor(config, projectRoot = process.cwd()) {
38
- this.config = config;
39
- this.projectRoot = projectRoot;
40
- this.cache = new BuildCache(path.join(projectRoot, 'node_modules', '.cache', '.seabox-cache'));
41
- this.verbose = config.verbose || false;
42
-
43
- // Set verbose mode for diagnostics
44
- diag.setVerbose(this.verbose);
45
- }
46
-
47
- /**
48
- * Build all configured targets
49
- * @returns {Promise<Array<{target: string, path: string}>>}
50
- */
51
- async buildAll() {
52
- diag.header('Seabox Multi-Target Build');
53
-
54
- // Step 1: Bundle entry once (platform-agnostic JavaScript)
55
- const { bundledPath, nativeModules, detectedAssets } = await this.bundleEntry();
56
-
57
- if (this.verbose && detectedAssets.size > 0) {
58
- diag.separator();
59
- diag.info('Auto-detected assets:');
60
- for (const assetPath of detectedAssets) {
61
- diag.listItem(assetPath, 1);
62
- }
63
- }
64
-
65
- if (this.verbose && nativeModules.size > 0) {
66
- diag.separator();
67
- diag.info('Native modules detected:');
68
- for (const [name, info] of nativeModules) {
69
- diag.listItem(`${name}: ${info.packageRoot}`, 1);
70
- }
71
- }
72
-
73
- // Step 2: Build all targets (can be parallelized)
74
- diag.separator();
75
- diag.info(`Building ${this.config.outputs.length} target(s)...`);
76
- diag.separator();
77
-
78
- const buildPromises = this.config.outputs.map((output, index) =>
79
- this.buildTarget(output, bundledPath, nativeModules, detectedAssets, index + 1)
80
- );
81
-
82
- const results = await Promise.all(buildPromises);
83
-
84
- diag.summary('All builds completed successfully!');
85
-
86
- return results;
87
- }
88
-
89
- /**
90
- * Bundle the entry file with Rollup
91
- * @returns {Promise<{bundledPath: string, nativeModules: Map}>}
92
- */
93
- async bundleEntry() {
94
- diag.step(1, 6, 'Bundling application with Rollup...');
95
-
96
- const entryPath = path.resolve(this.projectRoot, this.config.entry);
97
-
98
- if (!fs.existsSync(entryPath)) {
99
- throw new Error(`Entry file not found: ${entryPath}`);
100
- }
101
-
102
- const bundledPath = path.join(this.projectRoot, 'out', '_sea-entry.js');
103
-
104
- // Create output directory
105
- fs.mkdirSync(path.dirname(bundledPath), { recursive: true });
106
-
107
- const result = await bundleWithRollup(
108
- entryPath,
109
- bundledPath,
110
- { ...this.config, _projectRoot: this.projectRoot },
111
- process.platform, // Use current platform for bundling (JavaScript is platform-agnostic)
112
- process.arch,
113
- this.verbose
114
- );
115
-
116
- diag.success(`Bundle created: ${bundledPath}`);
117
-
118
- return result;
119
- }
120
-
121
-
122
-
123
- /**
124
- * Build a single target
125
- * @param {import('./config.mjs').OutputTarget} outputConfig - Target configuration
126
- * @param {string} bundledEntryPath - Path to bundled entry
127
- * @param {Map} nativeModules - Detected native modules
128
- * @param {Set<string>} detectedAssets - Auto-detected assets from bundler
129
- * @param {number} buildNumber - Build number for display
130
- * @returns {Promise<{target: string, path: string}>}
131
- */
132
- async buildTarget(outputConfig, bundledEntryPath, nativeModules, detectedAssets, buildNumber) {
133
- const { target, path: outputPath, output: executableName } = outputConfig;
134
- const { nodeVersion, platform, arch } = parseTarget(target);
135
-
136
- diag.subheader(`[Build ${buildNumber}] Target: ${target}`);
137
-
138
- // Step 1: Rebuild native modules for this target
139
- const rebuiltModules = await this.rebuildNativeModulesForTarget(
140
- nativeModules,
141
- target,
142
- buildNumber
143
- );
144
-
145
- // Step 2: Collect config assets (manual globs)
146
- const configAssets = await this.collectConfigAssets(
147
- this.config.assets || [],
148
- buildNumber
149
- );
150
-
151
- // Step 3: Collect auto-detected assets (from path.join(__dirname, ...))
152
- const autoAssets = await this.collectDetectedAssets(
153
- detectedAssets,
154
- buildNumber
155
- );
156
-
157
- // Step 4: Collect platform-specific libraries (DLLs/SOs)
158
- const platformLibraries = await this.collectPlatformLibraries(
159
- outputConfig.libraries,
160
- platform,
161
- arch,
162
- buildNumber
163
- );
164
-
165
- // Step 5: Prepare bundled entry with bootstrap
166
- const finalEntryPath = await this.prepareFinalEntry(
167
- bundledEntryPath,
168
- buildNumber
169
- );
170
-
171
- // Step 6: Combine all assets (dedupe by assetKey)
172
- const assetMap = new Map();
173
-
174
- // Add in order of priority (later overwrites earlier)
175
- for (const asset of [...rebuiltModules, ...configAssets, ...autoAssets, ...platformLibraries]) {
176
- assetMap.set(asset.assetKey, asset);
177
- }
178
-
179
- const allAssets = Array.from(assetMap.values());
180
-
181
- // Verbose logging: List all embedded assets
182
- if (this.verbose) {
183
- diag.separator();
184
- diag.verbose(`Embedded Assets (${allAssets.length} total):`, 1);
185
-
186
- const nativeAssets = allAssets.filter(a => a.assetKey.includes('.node'));
187
- const libraryAssets = allAssets.filter(a => a.isBinary && !a.assetKey.includes('.node'));
188
- const regularAssets = allAssets.filter(a => !a.isBinary);
189
-
190
- if (nativeAssets.length > 0) {
191
- diag.verbose(`Native Modules (${nativeAssets.length}):`, 1);
192
- for (const asset of nativeAssets) {
193
- const size = diag.formatSize(fs.statSync(asset.sourcePath).size);
194
- const displayName = path.basename(asset.assetKey);
195
- diag.verbose(`- ${displayName} (${size})`, 2);
196
- }
197
- }
198
-
199
- if (libraryAssets.length > 0) {
200
- diag.verbose(`Platform Libraries (${libraryAssets.length}):`, 1);
201
- for (const asset of libraryAssets) {
202
- const size = diag.formatSize(fs.statSync(asset.sourcePath).size);
203
- diag.verbose(`- ${asset.assetKey} (${size})`, 2);
204
- }
205
- }
206
-
207
- if (regularAssets.length > 0) {
208
- diag.verbose(`Regular Assets (${regularAssets.length}):`, 1);
209
- for (const asset of regularAssets) {
210
- const size = diag.formatSize(fs.statSync(asset.sourcePath).size);
211
- diag.verbose(`- ${asset.assetKey} (${size})`, 2);
212
- }
213
- }
214
- }
215
-
216
- // Step 7: Generate SEA
217
- await this.generateSEAForTarget({
218
- assets: allAssets,
219
- entryPath: finalEntryPath,
220
- target,
221
- outputPath,
222
- executableName,
223
- platform,
224
- arch,
225
- nodeVersion,
226
- rcedit: outputConfig.rcedit,
227
- buildNumber
228
- });
229
-
230
- const finalPath = path.join(outputPath, executableName);
231
- diag.success(`Build complete: ${finalPath}`);
232
-
233
- // Apply custom signing if configured
234
- if (this.config.sign) {
235
- await this.applyCustomSigning(finalPath, target, platform, arch, nodeVersion, buildNumber);
236
- }
237
-
238
- return {
239
- target,
240
- path: finalPath
241
- };
242
- }
243
-
244
- /**
245
- * Rebuild native modules for specific target
246
- */
247
- async rebuildNativeModulesForTarget(nativeModules, target, buildNumber) {
248
- if (nativeModules.size === 0) {
249
- return [];
250
- }
251
-
252
- diag.buildStep(buildNumber, 1, `Rebuilding ${nativeModules.size} native module(s)...`);
253
-
254
- const rebuiltAssets = [];
255
- const { platform, arch } = parseTarget(target);
256
-
257
- for (const [moduleKey, moduleInfo] of nativeModules) {
258
- const moduleName = moduleInfo.moduleName || path.basename(moduleKey, '.node');
259
-
260
- try {
261
- // Skip rebuilding if this is already a prebuild for the target platform
262
- if (moduleInfo.isPrebuild) {
263
- diag.verbose(`Using prebuild: ${moduleName}`, 2);
264
-
265
- rebuiltAssets.push({
266
- sourcePath: moduleInfo.binaryPath,
267
- assetKey: moduleInfo.assetKey, // Use original assetKey from bundler
268
- isBinary: true,
269
- hash: await this.computeHash(moduleInfo.binaryPath)
270
- });
271
- continue;
272
- }
273
-
274
- // Check cache first
275
- const cachedBuild = this.cache.getCachedNativeBuild(moduleInfo.packageRoot, target);
276
-
277
- if (cachedBuild) {
278
- diag.verbose(`Using cached build: ${moduleName}`, 2);
279
-
280
- rebuiltAssets.push({
281
- sourcePath: cachedBuild,
282
- assetKey: moduleInfo.assetKey, // Use original assetKey from bundler
283
- isBinary: true,
284
- hash: await this.computeHash(cachedBuild)
285
- });
286
- continue;
287
- }
288
-
289
- // Rebuild the module
290
- diag.verbose(`Rebuilding: ${moduleName}`, 2);
291
-
292
- await this.rebuildNativeModule(moduleInfo.packageRoot, target);
293
-
294
- // Find the built binary
295
- const builtPath = await this.findBuiltBinary(moduleInfo, target);
296
-
297
- if (builtPath) {
298
- // Cache the build
299
- this.cache.cacheNativeBuild(moduleInfo.packageRoot, target, builtPath);
300
-
301
- rebuiltAssets.push({
302
- sourcePath: builtPath,
303
- assetKey: moduleInfo.assetKey, // Use original assetKey from bundler
304
- isBinary: true,
305
- hash: await this.computeHash(builtPath)
306
- });
307
-
308
- diag.verbose(`Built: ${moduleName} -> ${builtPath}`, 2);
309
- }
310
- } catch (err) {
311
- diag.warn(`Failed to rebuild ${moduleName}: ${err.message}`, 2);
312
- }
313
- }
314
-
315
- diag.success('Native modules processed');
316
- return rebuiltAssets;
317
- }
318
-
319
- /**
320
- * Rebuild a single native module
321
- */
322
- async rebuildNativeModule(packageRoot, target) {
323
- const rebuildScript = path.join(__dirname, '..', 'bin', 'seabox-rebuild.mjs');
324
-
325
- if (!fs.existsSync(rebuildScript)) {
326
- throw new Error('seabox-rebuild.mjs not found');
327
- }
328
-
329
- try {
330
- execSync(`node "${rebuildScript}" --target ${target} "${packageRoot}"`, {
331
- stdio: this.verbose ? 'inherit' : 'pipe',
332
- cwd: this.projectRoot
333
- });
334
- } catch (err) {
335
- throw new Error(`Rebuild failed: ${err.message}`);
336
- }
337
- }
338
-
339
- /**
340
- * Find the built .node binary
341
- */
342
- async findBuiltBinary(moduleInfo, target) {
343
- // Try common build locations
344
- const searchPaths = [
345
- path.join(moduleInfo.packageRoot, 'build/Release'),
346
- path.join(moduleInfo.packageRoot, 'build/Debug'),
347
- moduleInfo.buildPath
348
- ];
349
-
350
- for (const searchPath of searchPaths) {
351
- if (fs.existsSync(searchPath)) {
352
- const files = fs.readdirSync(searchPath);
353
- const nodeFile = files.find(f => f.endsWith('.node'));
354
-
355
- if (nodeFile) {
356
- return path.join(searchPath, nodeFile);
357
- }
358
- }
359
- }
360
-
361
- return null;
362
- }
363
-
364
- /**
365
- * Collect platform-specific libraries (DLLs, SOs, DYLIBs) that need filesystem extraction
366
- * Only includes libraries explicitly listed in config patterns - no automatic discovery.
367
- * Libraries referenced in code (e.g., path.join(__dirname, './lib/foo.dll')) are
368
- * already captured by the bundler's asset detection.
369
- *
370
- * @param {string[]} libraryPatterns - Explicit glob patterns for libraries from config
371
- * @param {string} platform - Target platform
372
- * @param {string} arch - Target architecture
373
- * @param {number} buildNumber - Build number for display
374
- */
375
- async collectPlatformLibraries(libraryPatterns, platform, arch, buildNumber) {
376
- // Only process explicitly configured library patterns
377
- // Do NOT use default patterns - this prevents automatic inclusion of unrelated DLLs
378
- if (!libraryPatterns || libraryPatterns.length === 0) {
379
- return [];
380
- }
381
-
382
- diag.buildStep(buildNumber, 4, `Collecting platform libraries (${platform})...`);
383
-
384
- const { glob } = await import('glob');
385
- const libraries = [];
386
-
387
- for (const pattern of libraryPatterns) {
388
- const matches = await glob(pattern, {
389
- cwd: this.projectRoot,
390
- nodir: true,
391
- absolute: false,
392
- ignore: [
393
- '**/node_modules/**',
394
- '**/.git/**',
395
- '**/dist/**',
396
- '**/build/**',
397
- '**/out/**',
398
- '**/bin/**',
399
- '**/obj/**',
400
- '**/tools/**',
401
- '**/.seabox-cache/**'
402
- ]
403
- });
404
-
405
- for (const match of matches) {
406
- const sourcePath = path.resolve(this.projectRoot, match);
407
-
408
- libraries.push({
409
- sourcePath,
410
- assetKey: match.replace(/\\/g, '/'),
411
- isBinary: true,
412
- hash: await this.computeHash(sourcePath)
413
- });
414
- }
415
- }
416
-
417
- if (libraries.length > 0) {
418
- diag.success(`Collected ${libraries.length} library file(s)`);
419
- }
420
-
421
- return libraries;
422
- }
423
-
424
- /**
425
- * Collect assets from config globs
426
- * @param {string[]} assetPatterns - Glob patterns for assets
427
- * @param {number} buildNumber - Build number for display
428
- */
429
- async collectConfigAssets(assetPatterns, buildNumber) {
430
- if (!assetPatterns || assetPatterns.length === 0) {
431
- return [];
432
- }
433
-
434
- diag.buildStep(buildNumber, 2, 'Collecting config assets...');
435
-
436
- const { glob } = await import('glob');
437
- const assets = [];
438
-
439
- for (const pattern of assetPatterns) {
440
- const matches = await glob(pattern, {
441
- cwd: this.projectRoot,
442
- nodir: true,
443
- absolute: false,
444
- ignore: [
445
- '**/node_modules/**',
446
- '**/.git/**',
447
- '**/dist/**',
448
- '**/build/**',
449
- '**/out/**',
450
- '**/bin/**',
451
- '**/obj/**',
452
- '**/tools/**',
453
- '**/.seabox-cache/**'
454
- ]
455
- });
456
-
457
- for (const match of matches) {
458
- const sourcePath = path.resolve(this.projectRoot, match);
459
-
460
- assets.push({
461
- sourcePath,
462
- assetKey: match.replace(/\\/g, '/'),
463
- isBinary: this.isBinaryFile(sourcePath),
464
- hash: await this.computeHash(sourcePath)
465
- });
466
- }
467
- }
468
-
469
- if (assets.length > 0) {
470
- diag.success(`Collected ${assets.length} config asset(s)`);
471
- }
472
-
473
- return assets;
474
- }
475
-
476
- /**
477
- * Collect auto-detected assets from bundler (path.join(__dirname, ...))
478
- * @param {Set<string>} detectedAssets - Asset paths detected during bundling
479
- * @param {number} buildNumber - Build number for display
480
- */
481
- async collectDetectedAssets(detectedAssets, buildNumber) {
482
- if (!detectedAssets || detectedAssets.size === 0) {
483
- return [];
484
- }
485
-
486
- diag.buildStep(buildNumber, 3, 'Processing auto-detected assets...');
487
-
488
- const assets = [];
489
-
490
- for (const assetKey of detectedAssets) {
491
- const sourcePath = path.resolve(this.projectRoot, assetKey);
492
-
493
- // Verify file still exists
494
- if (fs.existsSync(sourcePath)) {
495
- assets.push({
496
- sourcePath,
497
- assetKey: assetKey,
498
- isBinary: this.isBinaryFile(sourcePath),
499
- hash: await this.computeHash(sourcePath)
500
- });
501
- }
502
- }
503
-
504
- if (assets.length > 0) {
505
- diag.success(`Collected ${assets.length} auto-detected asset(s)`);
506
- }
507
-
508
- return assets;
509
- }
510
-
511
- /**
512
- * Check if a file is binary based on extension
513
- */
514
- isBinaryFile(filePath) {
515
- const binaryExtensions = ['.node', '.dll', '.so', '.dylib', '.exe', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot'];
516
- const ext = path.extname(filePath).toLowerCase();
517
- return binaryExtensions.includes(ext);
518
- }
519
-
520
- /**
521
- * Prepare final entry with bootstrap
522
- */
523
- async prepareFinalEntry(bundledEntryPath, buildNumber) {
524
- diag.buildStep(buildNumber, 3, 'Preparing bootstrap...');
525
-
526
- const bootstrapPath = path.join(__dirname, 'bootstrap.cjs');
527
- const bootstrapContent = fs.readFileSync(bootstrapPath, 'utf8');
528
-
529
- let augmentedBootstrap = bootstrapContent;
530
-
531
- // Handle encryption if enabled
532
- if (this.config.encryptAssets) {
533
- const encryptionKey = generateEncryptionKey();
534
- const keyCode = keyToObfuscatedCode(encryptionKey);
535
-
536
- const encryptionSetup = `
537
- const SEA_ENCRYPTION_KEY = ${keyCode};
538
- const SEA_ENCRYPTED_ASSETS = new Set([]);
539
- `;
540
-
541
- augmentedBootstrap = bootstrapContent.replace(
542
- " 'use strict';",
543
- " 'use strict';\n" + encryptionSetup
544
- );
545
-
546
- diag.verbose('Encryption enabled and bootstrap obfuscated', 2);
547
-
548
- augmentedBootstrap = obfuscateBootstrap(augmentedBootstrap);
549
- }
550
-
551
- // Read bundled entry
552
- const entryContent = fs.readFileSync(bundledEntryPath, 'utf8');
553
-
554
- // Bundle with bootstrap
555
- const finalEntry = bundleEntry(entryContent, augmentedBootstrap, this.config.useSnapshot, this.verbose);
556
-
557
- // Write final entry
558
- const finalPath = bundledEntryPath.replace('.js', '-final.js');
559
- fs.writeFileSync(finalPath, finalEntry, 'utf8');
560
-
561
- diag.success('Bootstrap prepared');
562
- return finalPath;
563
- }
564
-
565
- /**
566
- * Generate SEA for a specific target
567
- */
568
- async generateSEAForTarget(options) {
569
- const {
570
- assets,
571
- entryPath,
572
- target,
573
- outputPath: outputDir,
574
- executableName,
575
- platform,
576
- arch,
577
- nodeVersion,
578
- rcedit,
579
- buildNumber
580
- } = options;
581
-
582
- diag.buildStep(buildNumber, 4, 'Generating SEA blob...');
583
-
584
- // Generate manifest
585
- const manifest = generateManifest(
586
- assets,
587
- {
588
- _packageName: this.config._packageName || 'app',
589
- _packageVersion: this.config._packageVersion || '1.0.0',
590
- cacheLocation: this.config.cacheLocation
591
- },
592
- platform,
593
- arch
594
- );
595
-
596
- const manifestJson = serializeManifest(manifest);
597
- const manifestAsset = {
598
- sourcePath: null,
599
- assetKey: 'sea-manifest.json',
600
- isBinary: false,
601
- content: Buffer.from(manifestJson, 'utf8')
602
- };
603
-
604
- const allAssets = [...assets, manifestAsset];
605
-
606
- // Create SEA config
607
- const tempDir = path.join(this.projectRoot, 'out', '.sea-temp', target);
608
- fs.mkdirSync(tempDir, { recursive: true });
609
-
610
- const blobOutputPath = path.join(tempDir, 'sea-blob.blob');
611
- const seaConfig = createSeaConfig(entryPath, blobOutputPath, allAssets, this.config);
612
-
613
- const seaConfigPath = path.join(tempDir, 'sea-config.json');
614
- writeSeaConfigJson(seaConfig, seaConfigPath, allAssets, tempDir);
615
-
616
- // Generate blob
617
- await generateBlob(seaConfigPath, process.execPath);
618
- diag.success('SEA blob generated');
619
-
620
- // Fetch Node binary
621
- diag.buildStep(buildNumber, 5, 'Fetching Node.js binary...');
622
- const cacheDir = path.join(this.projectRoot, 'node_modules', '.cache', 'sea-node-binaries');
623
- const nodeBinary = await fetchNodeBinary(nodeVersion, platform, arch, cacheDir);
624
- diag.success('Node binary ready');
625
-
626
- // Inject blob
627
- diag.buildStep(buildNumber, 6, 'Injecting blob into executable...');
628
- const outputExe = path.join(outputDir, executableName);
629
- await injectBlob(nodeBinary, blobOutputPath, outputExe, platform, this.verbose, rcedit);
630
-
631
- // Cleanup
632
- if (!this.verbose) {
633
- fs.rmSync(tempDir, { recursive: true, force: true });
634
- }
635
-
636
- const size = diag.formatSize(fs.statSync(outputExe).size);
637
- diag.success(`Injected (${size})`);
638
- }
639
-
640
- /**
641
- * Compute SHA-256 hash of a file
642
- */
643
- async computeHash(filePath) {
644
- return new Promise((resolve, reject) => {
645
- const hash = crypto.createHash('sha256');
646
- const stream = fs.createReadStream(filePath);
647
- stream.on('data', chunk => hash.update(chunk));
648
- stream.on('end', () => resolve(hash.digest('hex')));
649
- stream.on('error', reject);
650
- });
651
- }
652
-
653
- /**
654
- * Apply custom signing script
655
- * @param {string} exePath - Path to executable
656
- * @param {string} target - Build target
657
- * @param {string} platform - Platform (win32, linux, darwin)
658
- * @param {string} arch - Architecture (x64, arm64)
659
- * @param {string} nodeVersion - Node version
660
- * @param {number} buildNumber - Build number
661
- */
662
- async applyCustomSigning(exePath, target, platform, arch, nodeVersion, buildNumber) {
663
- diag.buildStep(buildNumber, 7, 'Applying custom signing...');
664
-
665
- try {
666
- const signScriptPath = path.resolve(this.projectRoot, this.config.sign);
667
-
668
- if (!fs.existsSync(signScriptPath)) {
669
- throw new Error(`Signing script not found: ${signScriptPath}`);
670
- }
671
-
672
- // Dynamic import of the signing script - convert to file:// URL for Windows
673
- const { pathToFileURL } = await import('url');
674
- const signModuleURL = pathToFileURL(signScriptPath).href;
675
- const signModule = await import(signModuleURL);
676
- const signFunction = signModule.default;
677
-
678
- if (typeof signFunction !== 'function') {
679
- throw new Error('Signing script must export a default function');
680
- }
681
-
682
- // Call the signing function with config
683
- await signFunction({
684
- exePath: path.resolve(exePath),
685
- target,
686
- platform,
687
- arch,
688
- nodeVersion,
689
- projectRoot: this.projectRoot
690
- });
691
-
692
- diag.success('Custom signing applied');
693
- } catch (error) {
694
- throw new Error(`Signing failed: ${error.message}`);
695
- }
696
- }
697
- }
1
+ /**
2
+ * multi-target-builder.mjs
3
+ * Orchestrate parallel builds for multiple target platforms.
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { execSync } from 'child_process';
9
+ import crypto from 'crypto';
10
+ import { fileURLToPath } from 'url';
11
+ import Module from 'module';
12
+
13
+ import { bundleWithRollup } from './rolldown-bundler.mjs';
14
+ import { BuildCache } from './build-cache.mjs';
15
+ import { parseTarget } from './config.mjs';
16
+ import { generateManifest, serializeManifest } from './manifest.mjs';
17
+ import { createSeaConfig, writeSeaConfigJson, generateBlob } from './blob.mjs';
18
+ import { fetchNodeBinary } from './fetch-node.mjs';
19
+ import { injectBlob } from './inject.mjs';
20
+ import { generateEncryptionKey, encryptAssets, keyToObfuscatedCode } from './crypto-assets.mjs';
21
+ import { obfuscateBootstrap } from './obfuscate.mjs';
22
+ import { bundleEntry } from './entry-bundler.mjs';
23
+ import * as diag from './diagnostics.mjs';
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = path.dirname(__filename);
27
+ const require = Module.createRequire(import.meta.url);
28
+
29
+ /**
30
+ * Multi-target build orchestrator
31
+ */
32
+ export class MultiTargetBuilder {
33
+ /**
34
+ * @param {import('./config.mjs').SeaboxConfigV2} config - Build configuration
35
+ * @param {string} projectRoot - Project root directory
36
+ */
37
+ constructor(config, projectRoot = process.cwd()) {
38
+ this.config = config;
39
+ this.projectRoot = projectRoot;
40
+ this.cache = new BuildCache(path.join(projectRoot, 'node_modules', '.cache', '.seabox-cache'));
41
+ this.verbose = config.verbose || false;
42
+
43
+ // Set verbose mode for diagnostics
44
+ diag.setVerbose(this.verbose);
45
+ }
46
+
47
+ /**
48
+ * Build all configured targets
49
+ * @returns {Promise<Array<{target: string, path: string}>>}
50
+ */
51
+ async buildAll() {
52
+ diag.header('Seabox Multi-Target Build');
53
+
54
+ // Step 1: Bundle entry once (platform-agnostic JavaScript)
55
+ const { bundledPath, nativeModules, detectedAssets } = await this.bundleEntry();
56
+
57
+ if (this.verbose && detectedAssets.size > 0) {
58
+ diag.separator();
59
+ diag.info('Auto-detected assets:');
60
+ for (const assetPath of detectedAssets) {
61
+ diag.listItem(assetPath, 1);
62
+ }
63
+ }
64
+
65
+ if (this.verbose && nativeModules.size > 0) {
66
+ diag.separator();
67
+ diag.info('Native modules detected:');
68
+ for (const [name, info] of nativeModules) {
69
+ diag.listItem(`${name}: ${info.packageRoot}`, 1);
70
+ }
71
+ }
72
+
73
+ // Step 2: Build all targets (can be parallelized)
74
+ diag.separator();
75
+ diag.info(`Building ${this.config.outputs.length} target(s)...`);
76
+ diag.separator();
77
+
78
+ const buildPromises = this.config.outputs.map((output, index) =>
79
+ this.buildTarget(output, bundledPath, nativeModules, detectedAssets, index + 1)
80
+ );
81
+
82
+ const results = await Promise.all(buildPromises);
83
+
84
+ diag.summary('All builds completed successfully!');
85
+
86
+ return results;
87
+ }
88
+
89
+ /**
90
+ * Bundle the entry file with Rollup
91
+ * @returns {Promise<{bundledPath: string, nativeModules: Map}>}
92
+ */
93
+ async bundleEntry() {
94
+ diag.step(1, 6, 'Bundling application with Rollup...');
95
+
96
+ const entryPath = path.resolve(this.projectRoot, this.config.entry);
97
+
98
+ if (!fs.existsSync(entryPath)) {
99
+ throw new Error(`Entry file not found: ${entryPath}`);
100
+ }
101
+
102
+ const bundledPath = path.join(this.projectRoot, 'out', '_sea-entry.js');
103
+
104
+ // Create output directory
105
+ fs.mkdirSync(path.dirname(bundledPath), { recursive: true });
106
+
107
+ const result = await bundleWithRollup(
108
+ entryPath,
109
+ bundledPath,
110
+ { ...this.config, _projectRoot: this.projectRoot },
111
+ process.platform, // Use current platform for bundling (JavaScript is platform-agnostic)
112
+ process.arch,
113
+ this.verbose
114
+ );
115
+
116
+ diag.success(`Bundle created: ${bundledPath}`);
117
+
118
+ return result;
119
+ }
120
+
121
+
122
+
123
+ /**
124
+ * Build a single target
125
+ * @param {import('./config.mjs').OutputTarget} outputConfig - Target configuration
126
+ * @param {string} bundledEntryPath - Path to bundled entry
127
+ * @param {Map} nativeModules - Detected native modules
128
+ * @param {Set<string>} detectedAssets - Auto-detected assets from bundler
129
+ * @param {number} buildNumber - Build number for display
130
+ * @returns {Promise<{target: string, path: string}>}
131
+ */
132
+ async buildTarget(outputConfig, bundledEntryPath, nativeModules, detectedAssets, buildNumber) {
133
+ const { target, path: outputPath, output: executableName } = outputConfig;
134
+ const { nodeVersion, platform, arch } = parseTarget(target);
135
+
136
+ diag.subheader(`[Build ${buildNumber}] Target: ${target}`);
137
+
138
+ // Step 1: Rebuild native modules for this target
139
+ const rebuiltModules = await this.rebuildNativeModulesForTarget(
140
+ nativeModules,
141
+ target,
142
+ buildNumber
143
+ );
144
+
145
+ // Step 2: Collect config assets (manual globs)
146
+ const configAssets = await this.collectConfigAssets(
147
+ this.config.assets || [],
148
+ buildNumber
149
+ );
150
+
151
+ // Step 3: Collect auto-detected assets (from path.join(__dirname, ...))
152
+ const autoAssets = await this.collectDetectedAssets(
153
+ detectedAssets,
154
+ buildNumber
155
+ );
156
+
157
+ // Step 4: Collect platform-specific libraries (DLLs/SOs)
158
+ const platformLibraries = await this.collectPlatformLibraries(
159
+ outputConfig.libraries,
160
+ platform,
161
+ arch,
162
+ buildNumber
163
+ );
164
+
165
+ // Step 5: Prepare bundled entry with bootstrap
166
+ const finalEntryPath = await this.prepareFinalEntry(
167
+ bundledEntryPath,
168
+ buildNumber
169
+ );
170
+
171
+ // Step 6: Combine all assets (dedupe by assetKey)
172
+ const assetMap = new Map();
173
+
174
+ // Add in order of priority (later overwrites earlier)
175
+ for (const asset of [...rebuiltModules, ...configAssets, ...autoAssets, ...platformLibraries]) {
176
+ assetMap.set(asset.assetKey, asset);
177
+ }
178
+
179
+ const allAssets = Array.from(assetMap.values());
180
+
181
+ // Verbose logging: List all embedded assets
182
+ if (this.verbose) {
183
+ diag.separator();
184
+ diag.verbose(`Embedded Assets (${allAssets.length} total):`, 1);
185
+
186
+ const nativeAssets = allAssets.filter(a => a.assetKey.includes('.node'));
187
+ const libraryAssets = allAssets.filter(a => a.isBinary && !a.assetKey.includes('.node'));
188
+ const regularAssets = allAssets.filter(a => !a.isBinary);
189
+
190
+ if (nativeAssets.length > 0) {
191
+ diag.verbose(`Native Modules (${nativeAssets.length}):`, 1);
192
+ for (const asset of nativeAssets) {
193
+ const size = diag.formatSize(fs.statSync(asset.sourcePath).size);
194
+ const displayName = path.basename(asset.assetKey);
195
+ diag.verbose(`- ${displayName} (${size})`, 2);
196
+ }
197
+ }
198
+
199
+ if (libraryAssets.length > 0) {
200
+ diag.verbose(`Platform Libraries (${libraryAssets.length}):`, 1);
201
+ for (const asset of libraryAssets) {
202
+ const size = diag.formatSize(fs.statSync(asset.sourcePath).size);
203
+ diag.verbose(`- ${asset.assetKey} (${size})`, 2);
204
+ }
205
+ }
206
+
207
+ if (regularAssets.length > 0) {
208
+ diag.verbose(`Regular Assets (${regularAssets.length}):`, 1);
209
+ for (const asset of regularAssets) {
210
+ const size = diag.formatSize(fs.statSync(asset.sourcePath).size);
211
+ diag.verbose(`- ${asset.assetKey} (${size})`, 2);
212
+ }
213
+ }
214
+ }
215
+
216
+ // Step 7: Generate SEA
217
+ await this.generateSEAForTarget({
218
+ assets: allAssets,
219
+ entryPath: finalEntryPath,
220
+ target,
221
+ outputPath,
222
+ executableName,
223
+ platform,
224
+ arch,
225
+ nodeVersion,
226
+ rcedit: outputConfig.rcedit,
227
+ buildNumber
228
+ });
229
+
230
+ const finalPath = path.join(outputPath, executableName);
231
+ diag.success(`Build complete: ${finalPath}`);
232
+
233
+ // Apply custom signing if configured
234
+ if (this.config.sign) {
235
+ await this.applyCustomSigning(finalPath, target, platform, arch, nodeVersion, buildNumber);
236
+ }
237
+
238
+ return {
239
+ target,
240
+ path: finalPath
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Rebuild native modules for specific target
246
+ */
247
+ async rebuildNativeModulesForTarget(nativeModules, target, buildNumber) {
248
+ if (nativeModules.size === 0) {
249
+ return [];
250
+ }
251
+
252
+ diag.buildStep(buildNumber, 1, `Rebuilding ${nativeModules.size} native module(s)...`);
253
+
254
+ const rebuiltAssets = [];
255
+ const { platform, arch } = parseTarget(target);
256
+
257
+ for (const [moduleKey, moduleInfo] of nativeModules) {
258
+ const moduleName = moduleInfo.moduleName || path.basename(moduleKey, '.node');
259
+
260
+ try {
261
+ // Skip rebuilding if this is already a prebuild for the target platform
262
+ if (moduleInfo.isPrebuild) {
263
+ diag.verbose(`Using prebuild: ${moduleName}`, 2);
264
+
265
+ rebuiltAssets.push({
266
+ sourcePath: moduleInfo.binaryPath,
267
+ assetKey: moduleInfo.assetKey, // Use original assetKey from bundler
268
+ isBinary: true,
269
+ hash: await this.computeHash(moduleInfo.binaryPath)
270
+ });
271
+ continue;
272
+ }
273
+
274
+ // Check cache first
275
+ const cachedBuild = this.cache.getCachedNativeBuild(moduleInfo.packageRoot, target);
276
+
277
+ if (cachedBuild) {
278
+ diag.verbose(`Using cached build: ${moduleName}`, 2);
279
+
280
+ rebuiltAssets.push({
281
+ sourcePath: cachedBuild,
282
+ assetKey: moduleInfo.assetKey, // Use original assetKey from bundler
283
+ isBinary: true,
284
+ hash: await this.computeHash(cachedBuild)
285
+ });
286
+ continue;
287
+ }
288
+
289
+ // Rebuild the module
290
+ diag.verbose(`Rebuilding: ${moduleName} for Node.js ${parseTarget(target).nodeVersion}`, 2);
291
+
292
+ await this.rebuildNativeModule(moduleInfo.packageRoot, target);
293
+
294
+ // Find the built binary
295
+ const builtPath = await this.findBuiltBinary(moduleInfo, target);
296
+
297
+ if (builtPath) {
298
+ // Cache the build
299
+ this.cache.cacheNativeBuild(moduleInfo.packageRoot, target, builtPath);
300
+
301
+ rebuiltAssets.push({
302
+ sourcePath: builtPath,
303
+ assetKey: moduleInfo.assetKey, // Use original assetKey from bundler
304
+ isBinary: true,
305
+ hash: await this.computeHash(builtPath)
306
+ });
307
+
308
+ diag.verbose(`Built: ${moduleName} -> ${builtPath}`, 2);
309
+ }
310
+ } catch (err) {
311
+ diag.warn(`Failed to rebuild ${moduleName}: ${err.message}`, 2);
312
+ }
313
+ }
314
+
315
+ diag.success('Native modules processed');
316
+ return rebuiltAssets;
317
+ }
318
+
319
+ /**
320
+ * Rebuild a single native module
321
+ */
322
+ async rebuildNativeModule(packageRoot, target) {
323
+ const rebuildScript = path.join(__dirname, '..', 'bin', 'seabox-rebuild.mjs');
324
+
325
+ if (!fs.existsSync(rebuildScript)) {
326
+ throw new Error('seabox-rebuild.mjs not found');
327
+ }
328
+
329
+ const { nodeVersion, platform, arch } = parseTarget(target);
330
+
331
+ try {
332
+ // Always inherit stdio to show node-gyp progress (header downloads, etc.)
333
+ execSync(`node "${rebuildScript}" "${packageRoot}" ${nodeVersion} ${platform} ${arch}${this.verbose ? ' --verbose' : ''}`, {
334
+ stdio: 'inherit', // Show node-gyp output including header downloads
335
+ cwd: this.projectRoot
336
+ });
337
+ } catch (err) {
338
+ throw new Error(`Rebuild failed: ${err.message}`);
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Find the built .node binary
344
+ */
345
+ async findBuiltBinary(moduleInfo, target) {
346
+ // Try common build locations
347
+ const searchPaths = [
348
+ path.join(moduleInfo.packageRoot, 'build/Release'),
349
+ path.join(moduleInfo.packageRoot, 'build/Debug'),
350
+ moduleInfo.buildPath
351
+ ];
352
+
353
+ for (const searchPath of searchPaths) {
354
+ if (fs.existsSync(searchPath)) {
355
+ const files = fs.readdirSync(searchPath);
356
+ const nodeFile = files.find(f => f.endsWith('.node'));
357
+
358
+ if (nodeFile) {
359
+ return path.join(searchPath, nodeFile);
360
+ }
361
+ }
362
+ }
363
+
364
+ return null;
365
+ }
366
+
367
+ /**
368
+ * Collect platform-specific libraries (DLLs, SOs, DYLIBs) that need filesystem extraction
369
+ * Only includes libraries explicitly listed in config patterns - no automatic discovery.
370
+ * Libraries referenced in code (e.g., path.join(__dirname, './lib/foo.dll')) are
371
+ * already captured by the bundler's asset detection.
372
+ *
373
+ * @param {string[]} libraryPatterns - Explicit glob patterns for libraries from config
374
+ * @param {string} platform - Target platform
375
+ * @param {string} arch - Target architecture
376
+ * @param {number} buildNumber - Build number for display
377
+ */
378
+ async collectPlatformLibraries(libraryPatterns, platform, arch, buildNumber) {
379
+ // Only process explicitly configured library patterns
380
+ // Do NOT use default patterns - this prevents automatic inclusion of unrelated DLLs
381
+ if (!libraryPatterns || libraryPatterns.length === 0) {
382
+ return [];
383
+ }
384
+
385
+ diag.buildStep(buildNumber, 4, `Collecting platform libraries (${platform})...`);
386
+
387
+ const { glob } = await import('glob');
388
+ const libraries = [];
389
+
390
+ for (const pattern of libraryPatterns) {
391
+ const matches = await glob(pattern, {
392
+ cwd: this.projectRoot,
393
+ nodir: true,
394
+ absolute: false,
395
+ ignore: [
396
+ '**/node_modules/**',
397
+ '**/.git/**',
398
+ '**/dist/**',
399
+ '**/build/**',
400
+ '**/out/**',
401
+ '**/bin/**',
402
+ '**/obj/**',
403
+ '**/tools/**',
404
+ '**/.seabox-cache/**'
405
+ ]
406
+ });
407
+
408
+ for (const match of matches) {
409
+ const sourcePath = path.resolve(this.projectRoot, match);
410
+
411
+ libraries.push({
412
+ sourcePath,
413
+ assetKey: match.replace(/\\/g, '/'),
414
+ isBinary: true,
415
+ hash: await this.computeHash(sourcePath)
416
+ });
417
+ }
418
+ }
419
+
420
+ if (libraries.length > 0) {
421
+ diag.success(`Collected ${libraries.length} library file(s)`);
422
+ }
423
+
424
+ return libraries;
425
+ }
426
+
427
+ /**
428
+ * Collect assets from config globs
429
+ * @param {string[]} assetPatterns - Glob patterns for assets
430
+ * @param {number} buildNumber - Build number for display
431
+ */
432
+ async collectConfigAssets(assetPatterns, buildNumber) {
433
+ if (!assetPatterns || assetPatterns.length === 0) {
434
+ return [];
435
+ }
436
+
437
+ diag.buildStep(buildNumber, 2, 'Collecting config assets...');
438
+
439
+ const { glob } = await import('glob');
440
+ const assets = [];
441
+
442
+ for (const pattern of assetPatterns) {
443
+ const matches = await glob(pattern, {
444
+ cwd: this.projectRoot,
445
+ nodir: true,
446
+ absolute: false,
447
+ ignore: [
448
+ '**/node_modules/**',
449
+ '**/.git/**',
450
+ '**/dist/**',
451
+ '**/build/**',
452
+ '**/out/**',
453
+ '**/bin/**',
454
+ '**/obj/**',
455
+ '**/tools/**',
456
+ '**/.seabox-cache/**'
457
+ ]
458
+ });
459
+
460
+ for (const match of matches) {
461
+ const sourcePath = path.resolve(this.projectRoot, match);
462
+
463
+ assets.push({
464
+ sourcePath,
465
+ assetKey: match.replace(/\\/g, '/'),
466
+ isBinary: this.isBinaryFile(sourcePath),
467
+ hash: await this.computeHash(sourcePath)
468
+ });
469
+ }
470
+ }
471
+
472
+ if (assets.length > 0) {
473
+ diag.success(`Collected ${assets.length} config asset(s)`);
474
+ }
475
+
476
+ return assets;
477
+ }
478
+
479
+ /**
480
+ * Collect auto-detected assets from bundler (path.join(__dirname, ...))
481
+ * @param {Set<string>} detectedAssets - Asset paths detected during bundling
482
+ * @param {number} buildNumber - Build number for display
483
+ */
484
+ async collectDetectedAssets(detectedAssets, buildNumber) {
485
+ if (!detectedAssets || detectedAssets.size === 0) {
486
+ return [];
487
+ }
488
+
489
+ diag.buildStep(buildNumber, 3, 'Processing auto-detected assets...');
490
+
491
+ const assets = [];
492
+
493
+ for (const assetKey of detectedAssets) {
494
+ const sourcePath = path.resolve(this.projectRoot, assetKey);
495
+
496
+ // Verify file still exists
497
+ if (fs.existsSync(sourcePath)) {
498
+ assets.push({
499
+ sourcePath,
500
+ assetKey: assetKey,
501
+ isBinary: this.isBinaryFile(sourcePath),
502
+ hash: await this.computeHash(sourcePath)
503
+ });
504
+ }
505
+ }
506
+
507
+ if (assets.length > 0) {
508
+ diag.success(`Collected ${assets.length} auto-detected asset(s)`);
509
+ }
510
+
511
+ return assets;
512
+ }
513
+
514
+ /**
515
+ * Check if a file is binary based on extension
516
+ */
517
+ isBinaryFile(filePath) {
518
+ const binaryExtensions = ['.node', '.dll', '.so', '.dylib', '.exe', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot'];
519
+ const ext = path.extname(filePath).toLowerCase();
520
+ return binaryExtensions.includes(ext);
521
+ }
522
+
523
+ /**
524
+ * Prepare final entry with bootstrap
525
+ */
526
+ async prepareFinalEntry(bundledEntryPath, buildNumber) {
527
+ diag.buildStep(buildNumber, 3, 'Preparing bootstrap...');
528
+
529
+ const bootstrapPath = path.join(__dirname, 'bootstrap.cjs');
530
+ const bootstrapContent = fs.readFileSync(bootstrapPath, 'utf8');
531
+
532
+ let augmentedBootstrap = bootstrapContent;
533
+
534
+ // Handle encryption if enabled
535
+ if (this.config.encryptAssets) {
536
+ const encryptionKey = generateEncryptionKey();
537
+ const keyCode = keyToObfuscatedCode(encryptionKey);
538
+
539
+ const encryptionSetup = `
540
+ const SEA_ENCRYPTION_KEY = ${keyCode};
541
+ const SEA_ENCRYPTED_ASSETS = new Set([]);
542
+ `;
543
+
544
+ augmentedBootstrap = bootstrapContent.replace(
545
+ " 'use strict';",
546
+ " 'use strict';\n" + encryptionSetup
547
+ );
548
+
549
+ diag.verbose('Encryption enabled and bootstrap obfuscated', 2);
550
+
551
+ augmentedBootstrap = obfuscateBootstrap(augmentedBootstrap);
552
+ }
553
+
554
+ // Read bundled entry
555
+ const entryContent = fs.readFileSync(bundledEntryPath, 'utf8');
556
+
557
+ // Bundle with bootstrap
558
+ const finalEntry = bundleEntry(entryContent, augmentedBootstrap, this.config.useSnapshot, this.verbose);
559
+
560
+ // Write final entry
561
+ const finalPath = bundledEntryPath.replace('.js', '-final.js');
562
+ fs.writeFileSync(finalPath, finalEntry, 'utf8');
563
+
564
+ diag.success('Bootstrap prepared');
565
+ return finalPath;
566
+ }
567
+
568
+ /**
569
+ * Generate SEA for a specific target
570
+ */
571
+ async generateSEAForTarget(options) {
572
+ const {
573
+ assets,
574
+ entryPath,
575
+ target,
576
+ outputPath: outputDir,
577
+ executableName,
578
+ platform,
579
+ arch,
580
+ nodeVersion,
581
+ rcedit,
582
+ buildNumber
583
+ } = options;
584
+
585
+ diag.buildStep(buildNumber, 4, 'Generating SEA blob...');
586
+
587
+ // Generate manifest
588
+ const manifest = generateManifest(
589
+ assets,
590
+ {
591
+ _packageName: this.config._packageName || 'app',
592
+ _packageVersion: this.config._packageVersion || '1.0.0',
593
+ cacheLocation: this.config.cacheLocation
594
+ },
595
+ platform,
596
+ arch
597
+ );
598
+
599
+ const manifestJson = serializeManifest(manifest);
600
+ const manifestAsset = {
601
+ sourcePath: null,
602
+ assetKey: 'sea-manifest.json',
603
+ isBinary: false,
604
+ content: Buffer.from(manifestJson, 'utf8')
605
+ };
606
+
607
+ const allAssets = [...assets, manifestAsset];
608
+
609
+ // Create SEA config
610
+ const tempDir = path.join(this.projectRoot, 'out', '.sea-temp', target);
611
+ fs.mkdirSync(tempDir, { recursive: true });
612
+
613
+ const blobOutputPath = path.join(tempDir, 'sea-blob.blob');
614
+ const seaConfig = createSeaConfig(entryPath, blobOutputPath, allAssets, this.config);
615
+
616
+ const seaConfigPath = path.join(tempDir, 'sea-config.json');
617
+ writeSeaConfigJson(seaConfig, seaConfigPath, allAssets, tempDir);
618
+
619
+ // Fetch Node binary FIRST - needed for blob generation
620
+ diag.buildStep(buildNumber, 5, 'Fetching Node.js binary...');
621
+ const cacheDir = path.join(this.projectRoot, 'node_modules', '.cache', 'sea-node-binaries');
622
+ const nodeBinary = await fetchNodeBinary(nodeVersion, platform, arch, cacheDir);
623
+ diag.success('Node binary ready');
624
+
625
+ // Generate blob using the TARGET Node.js binary (not current process.execPath)
626
+ // This is critical for snapshots - they must be built with the target Node version
627
+ await generateBlob(seaConfigPath, nodeBinary);
628
+ diag.success('SEA blob generated');
629
+
630
+ // Inject blob
631
+ diag.buildStep(buildNumber, 6, 'Injecting blob into executable...');
632
+ const outputExe = path.join(outputDir, executableName);
633
+ await injectBlob(nodeBinary, blobOutputPath, outputExe, platform, this.verbose, rcedit);
634
+
635
+ // Cleanup
636
+ if (!this.verbose) {
637
+ fs.rmSync(tempDir, { recursive: true, force: true });
638
+ }
639
+
640
+ const size = diag.formatSize(fs.statSync(outputExe).size);
641
+ diag.success(`Injected (${size})`);
642
+ }
643
+
644
+ /**
645
+ * Compute SHA-256 hash of a file
646
+ */
647
+ async computeHash(filePath) {
648
+ return new Promise((resolve, reject) => {
649
+ const hash = crypto.createHash('sha256');
650
+ const stream = fs.createReadStream(filePath);
651
+ stream.on('data', chunk => hash.update(chunk));
652
+ stream.on('end', () => resolve(hash.digest('hex')));
653
+ stream.on('error', reject);
654
+ });
655
+ }
656
+
657
+ /**
658
+ * Apply custom signing script
659
+ * @param {string} exePath - Path to executable
660
+ * @param {string} target - Build target
661
+ * @param {string} platform - Platform (win32, linux, darwin)
662
+ * @param {string} arch - Architecture (x64, arm64)
663
+ * @param {string} nodeVersion - Node version
664
+ * @param {number} buildNumber - Build number
665
+ */
666
+ async applyCustomSigning(exePath, target, platform, arch, nodeVersion, buildNumber) {
667
+ diag.buildStep(buildNumber, 7, 'Applying custom signing...');
668
+
669
+ try {
670
+ const signScriptPath = path.resolve(this.projectRoot, this.config.sign);
671
+
672
+ if (!fs.existsSync(signScriptPath)) {
673
+ throw new Error(`Signing script not found: ${signScriptPath}`);
674
+ }
675
+
676
+ // Dynamic import of the signing script - convert to file:// URL for Windows
677
+ const { pathToFileURL } = await import('url');
678
+ const signModuleURL = pathToFileURL(signScriptPath).href;
679
+ const signModule = await import(signModuleURL);
680
+ const signFunction = signModule.default;
681
+
682
+ if (typeof signFunction !== 'function') {
683
+ throw new Error('Signing script must export a default function');
684
+ }
685
+
686
+ // Call the signing function with config
687
+ await signFunction({
688
+ exePath: path.resolve(exePath),
689
+ target,
690
+ platform,
691
+ arch,
692
+ nodeVersion,
693
+ projectRoot: this.projectRoot
694
+ });
695
+
696
+ diag.success('Custom signing applied');
697
+ } catch (error) {
698
+ throw new Error(`Signing failed: ${error.message}`);
699
+ }
700
+ }
701
+ }