electrobun 0.0.19-beta.99 → 0.1.1
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.
- package/README.md +1 -1
- package/dist/api/browser/webviewtag.ts +54 -2
- package/dist/api/bun/ElectrobunConfig.ts +171 -0
- package/dist/api/bun/core/BrowserWindow.ts +4 -0
- package/dist/api/bun/core/Tray.ts +14 -0
- package/dist/api/bun/core/Updater.ts +4 -3
- package/dist/api/bun/index.ts +2 -0
- package/dist/api/bun/proc/native.ts +107 -5
- package/dist/main.js +5 -4
- package/package.json +4 -2
- package/src/cli/index.ts +621 -151
- package/templates/hello-world/bun.lock +164 -2
- package/templates/hello-world/electrobun.config.ts +28 -0
- package/templates/hello-world/src/bun/index.ts +2 -2
- package/templates/hello-world/src/mainview/index.html +5 -6
- package/templates/hello-world/src/mainview/index.ts +1 -5
- package/templates/interactive-playground/README.md +26 -0
- package/templates/interactive-playground/assets/tray-icon.png +0 -0
- package/templates/interactive-playground/electrobun.config.ts +36 -0
- package/templates/interactive-playground/package-lock.json +36 -0
- package/templates/interactive-playground/package.json +15 -0
- package/templates/interactive-playground/src/bun/demos/files.ts +70 -0
- package/templates/interactive-playground/src/bun/demos/menus.ts +139 -0
- package/templates/interactive-playground/src/bun/demos/rpc.ts +83 -0
- package/templates/interactive-playground/src/bun/demos/system.ts +72 -0
- package/templates/interactive-playground/src/bun/demos/updates.ts +105 -0
- package/templates/interactive-playground/src/bun/demos/windows.ts +90 -0
- package/templates/interactive-playground/src/bun/index.ts +124 -0
- package/templates/interactive-playground/src/bun/types/rpc.ts +109 -0
- package/templates/interactive-playground/src/mainview/components/EventLog.ts +107 -0
- package/templates/interactive-playground/src/mainview/components/Sidebar.ts +65 -0
- package/templates/interactive-playground/src/mainview/components/Toast.ts +57 -0
- package/templates/interactive-playground/src/mainview/demos/FileDemo.ts +211 -0
- package/templates/interactive-playground/src/mainview/demos/MenuDemo.ts +102 -0
- package/templates/interactive-playground/src/mainview/demos/RPCDemo.ts +229 -0
- package/templates/interactive-playground/src/mainview/demos/TrayDemo.ts +132 -0
- package/templates/interactive-playground/src/mainview/demos/WebViewDemo.ts +411 -0
- package/templates/interactive-playground/src/mainview/demos/WindowDemo.ts +207 -0
- package/templates/interactive-playground/src/mainview/index.css +538 -0
- package/templates/interactive-playground/src/mainview/index.html +103 -0
- package/templates/interactive-playground/src/mainview/index.ts +238 -0
- package/templates/multitab-browser/README.md +34 -0
- package/templates/multitab-browser/bun.lock +224 -0
- package/templates/multitab-browser/electrobun.config.ts +32 -0
- package/templates/multitab-browser/package-lock.json +20 -0
- package/templates/multitab-browser/package.json +12 -0
- package/templates/multitab-browser/src/bun/index.ts +144 -0
- package/templates/multitab-browser/src/bun/tabManager.ts +200 -0
- package/templates/multitab-browser/src/bun/types/rpc.ts +78 -0
- package/templates/multitab-browser/src/mainview/index.css +487 -0
- package/templates/multitab-browser/src/mainview/index.html +94 -0
- package/templates/multitab-browser/src/mainview/index.ts +634 -0
- package/templates/photo-booth/README.md +108 -0
- package/templates/photo-booth/bun.lock +239 -0
- package/templates/photo-booth/electrobun.config.ts +32 -0
- package/templates/photo-booth/package.json +17 -0
- package/templates/photo-booth/src/bun/index.ts +92 -0
- package/templates/photo-booth/src/mainview/index.css +465 -0
- package/templates/photo-booth/src/mainview/index.html +124 -0
- package/templates/photo-booth/src/mainview/index.ts +499 -0
- package/tests/bun.lock +14 -0
- package/tests/electrobun.config.ts +45 -0
- package/tests/package-lock.json +36 -0
- package/tests/package.json +13 -0
- package/tests/src/bun/index.ts +100 -0
- package/tests/src/bun/test-runner.ts +508 -0
- package/tests/src/mainview/index.html +110 -0
- package/tests/src/mainview/index.ts +458 -0
- package/tests/src/mainview/styles/main.css +451 -0
- package/tests/src/testviews/tray-test.html +57 -0
- package/tests/src/testviews/webview-mask.html +114 -0
- package/tests/src/testviews/webview-navigation.html +36 -0
- package/tests/src/testviews/window-create.html +17 -0
- package/tests/src/testviews/window-events.html +29 -0
- package/tests/src/testviews/window-focus.html +37 -0
- package/tests/src/webviewtag/index.ts +11 -0
- package/templates/hello-world/electrobun.config +0 -18
package/src/cli/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, dirname, basename } from "path";
|
|
1
|
+
import { join, dirname, basename, relative } from "path";
|
|
2
2
|
import {
|
|
3
3
|
existsSync,
|
|
4
4
|
readFileSync,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
copyFileSync,
|
|
16
16
|
} from "fs";
|
|
17
17
|
import { execSync } from "child_process";
|
|
18
|
+
import * as readline from "readline";
|
|
18
19
|
import tar from "tar";
|
|
19
20
|
import archiver from "archiver";
|
|
20
21
|
import { ZstdInit } from "@oneidentity/zstd-js/wasm";
|
|
@@ -29,8 +30,12 @@ const MAX_CHUNK_SIZE = 1024 * 2;
|
|
|
29
30
|
|
|
30
31
|
// this when run as an npm script this will be where the folder where package.json is.
|
|
31
32
|
const projectRoot = process.cwd();
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
|
|
34
|
+
// Find TypeScript ESM config file
|
|
35
|
+
function findConfigFile(): string | null {
|
|
36
|
+
const configFile = join(projectRoot, 'electrobun.config.ts');
|
|
37
|
+
return existsSync(configFile) ? configFile : null;
|
|
38
|
+
}
|
|
34
39
|
|
|
35
40
|
// Note: cli args can be called via npm bun /path/to/electorbun/binary arg1 arg2
|
|
36
41
|
const indexOfElectrobun = process.argv.findIndex((arg) =>
|
|
@@ -191,7 +196,8 @@ async function ensureCoreDependencies(targetOS?: 'macos' | 'win' | 'linux', targ
|
|
|
191
196
|
// Use Windows native tar.exe on Windows due to npm tar library issues
|
|
192
197
|
if (OS === 'win') {
|
|
193
198
|
console.log('Using Windows native tar.exe for reliable extraction...');
|
|
194
|
-
|
|
199
|
+
const relativeTempFile = relative(platformDistPath, tempFile);
|
|
200
|
+
execSync(`tar -xf "${relativeTempFile}"`, {
|
|
195
201
|
stdio: 'inherit',
|
|
196
202
|
cwd: platformDistPath
|
|
197
203
|
});
|
|
@@ -244,6 +250,9 @@ async function ensureCoreDependencies(targetOS?: 'macos' | 'win' | 'linux', targ
|
|
|
244
250
|
console.error('This suggests the tarball structure is different than expected');
|
|
245
251
|
}
|
|
246
252
|
|
|
253
|
+
// Note: We no longer need to remove or re-add signatures from downloaded binaries
|
|
254
|
+
// The CI-added adhoc signatures are actually required for macOS to run the binaries
|
|
255
|
+
|
|
247
256
|
// For development: if main.js doesn't exist in shared dist/, copy from platform-specific download as fallback
|
|
248
257
|
const sharedDistPath = join(ELECTROBUN_DEP_PATH, 'dist');
|
|
249
258
|
const extractedMainJs = join(platformDistPath, 'main.js');
|
|
@@ -297,53 +306,164 @@ async function ensureCEFDependencies(targetOS?: 'macos' | 'win' | 'linux', targe
|
|
|
297
306
|
const archName = platformArch;
|
|
298
307
|
const cefTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-cef-${platformName}-${archName}.tar.gz`;
|
|
299
308
|
|
|
300
|
-
|
|
309
|
+
// Helper function to download with retry logic
|
|
310
|
+
async function downloadWithRetry(url: string, filePath: string, maxRetries = 3): Promise<void> {
|
|
311
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
312
|
+
try {
|
|
313
|
+
console.log(`Downloading CEF (attempt ${attempt}/${maxRetries}) from: ${url}`);
|
|
314
|
+
|
|
315
|
+
const response = await fetch(url);
|
|
316
|
+
if (!response.ok) {
|
|
317
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Get content length for progress tracking
|
|
321
|
+
const contentLength = response.headers.get('content-length');
|
|
322
|
+
const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
|
|
323
|
+
|
|
324
|
+
// Create temp file with unique name to avoid conflicts
|
|
325
|
+
const fileStream = createWriteStream(filePath);
|
|
326
|
+
let downloadedSize = 0;
|
|
327
|
+
let lastReportedPercent = -1;
|
|
328
|
+
|
|
329
|
+
// Stream download with progress
|
|
330
|
+
if (response.body) {
|
|
331
|
+
const reader = response.body.getReader();
|
|
332
|
+
while (true) {
|
|
333
|
+
const { done, value } = await reader.read();
|
|
334
|
+
if (done) break;
|
|
335
|
+
|
|
336
|
+
const chunk = Buffer.from(value);
|
|
337
|
+
fileStream.write(chunk);
|
|
338
|
+
downloadedSize += chunk.length;
|
|
339
|
+
|
|
340
|
+
if (totalSize > 0) {
|
|
341
|
+
const percent = Math.round((downloadedSize / totalSize) * 100);
|
|
342
|
+
const percentTier = Math.floor(percent / 10) * 10;
|
|
343
|
+
if (percentTier > lastReportedPercent && percentTier <= 100) {
|
|
344
|
+
console.log(` Progress: ${percentTier}% (${Math.round(downloadedSize / 1024 / 1024)}MB/${Math.round(totalSize / 1024 / 1024)}MB)`);
|
|
345
|
+
lastReportedPercent = percentTier;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await new Promise((resolve, reject) => {
|
|
352
|
+
fileStream.end((error: any) => {
|
|
353
|
+
if (error) reject(error);
|
|
354
|
+
else resolve(void 0);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Verify file size if content-length was provided
|
|
359
|
+
if (totalSize > 0) {
|
|
360
|
+
const actualSize = (await import('fs')).statSync(filePath).size;
|
|
361
|
+
if (actualSize !== totalSize) {
|
|
362
|
+
throw new Error(`Downloaded file size mismatch: expected ${totalSize}, got ${actualSize}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.log(`✓ Download completed successfully (${Math.round(downloadedSize / 1024 / 1024)}MB)`);
|
|
367
|
+
return; // Success, exit retry loop
|
|
368
|
+
|
|
369
|
+
} catch (error: any) {
|
|
370
|
+
console.error(`Download attempt ${attempt} failed:`, error.message);
|
|
371
|
+
|
|
372
|
+
// Clean up partial download
|
|
373
|
+
if (existsSync(filePath)) {
|
|
374
|
+
unlinkSync(filePath);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (attempt === maxRetries) {
|
|
378
|
+
throw new Error(`Failed to download after ${maxRetries} attempts: ${error.message}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Wait before retrying (exponential backoff)
|
|
382
|
+
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s...
|
|
383
|
+
console.log(`Retrying in ${delay / 1000} seconds...`);
|
|
384
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
301
388
|
|
|
302
389
|
try {
|
|
303
|
-
//
|
|
304
|
-
const
|
|
305
|
-
if (!response.ok) {
|
|
306
|
-
throw new Error(`Failed to download CEF: ${response.status} ${response.statusText}`);
|
|
307
|
-
}
|
|
390
|
+
// Create temp file with unique name
|
|
391
|
+
const tempFile = join(ELECTROBUN_DEP_PATH, `cef-${platformOS}-${platformArch}-${Date.now()}.tar.gz`);
|
|
308
392
|
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
const fileStream = createWriteStream(tempFile);
|
|
312
|
-
|
|
313
|
-
// Write response to file
|
|
314
|
-
if (response.body) {
|
|
315
|
-
const reader = response.body.getReader();
|
|
316
|
-
while (true) {
|
|
317
|
-
const { done, value } = await reader.read();
|
|
318
|
-
if (done) break;
|
|
319
|
-
fileStream.write(Buffer.from(value));
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
fileStream.end();
|
|
393
|
+
// Download with retry logic
|
|
394
|
+
await downloadWithRetry(cefTarballUrl, tempFile);
|
|
323
395
|
|
|
324
396
|
// Extract to platform-specific dist directory
|
|
325
397
|
console.log(`Extracting CEF dependencies for ${platformOS}-${platformArch}...`);
|
|
326
398
|
const platformDistPath = join(ELECTROBUN_DEP_PATH, `dist-${platformOS}-${platformArch}`);
|
|
327
399
|
mkdirSync(platformDistPath, { recursive: true });
|
|
328
400
|
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
401
|
+
// Helper function to validate tar file before extraction
|
|
402
|
+
async function validateTarFile(filePath: string): Promise<void> {
|
|
403
|
+
try {
|
|
404
|
+
// Quick validation - try to read the tar file header
|
|
405
|
+
const fd = await import('fs').then(fs => fs.promises.readFile(filePath));
|
|
406
|
+
|
|
407
|
+
// Check if it's a gzip file (magic bytes: 1f 8b)
|
|
408
|
+
if (fd.length < 2 || fd[0] !== 0x1f || fd[1] !== 0x8b) {
|
|
409
|
+
throw new Error('Invalid gzip header - file may be corrupted');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
console.log(`✓ Tar file validation passed (${Math.round(fd.length / 1024 / 1024)}MB)`);
|
|
413
|
+
} catch (error: any) {
|
|
414
|
+
throw new Error(`Tar file validation failed: ${error.message}`);
|
|
415
|
+
}
|
|
343
416
|
}
|
|
344
417
|
|
|
345
|
-
//
|
|
346
|
-
|
|
418
|
+
// Validate downloaded file before extraction
|
|
419
|
+
await validateTarFile(tempFile);
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
// Use Windows native tar.exe on Windows due to npm tar library issues
|
|
423
|
+
if (OS === 'win') {
|
|
424
|
+
console.log('Using Windows native tar.exe for reliable extraction...');
|
|
425
|
+
const relativeTempFile = relative(platformDistPath, tempFile);
|
|
426
|
+
execSync(`tar -xf "${relativeTempFile}"`, {
|
|
427
|
+
stdio: 'inherit',
|
|
428
|
+
cwd: platformDistPath
|
|
429
|
+
});
|
|
430
|
+
} else {
|
|
431
|
+
await tar.x({
|
|
432
|
+
file: tempFile,
|
|
433
|
+
cwd: platformDistPath,
|
|
434
|
+
preservePaths: false,
|
|
435
|
+
strip: 0,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
console.log(`✓ Extraction completed successfully`);
|
|
440
|
+
|
|
441
|
+
} catch (error: any) {
|
|
442
|
+
// Check if CEF directory was created despite the error (partial extraction)
|
|
443
|
+
const cefDir = join(platformDistPath, 'cef');
|
|
444
|
+
if (existsSync(cefDir)) {
|
|
445
|
+
const cefFiles = readdirSync(cefDir);
|
|
446
|
+
if (cefFiles.length > 0) {
|
|
447
|
+
console.warn(`⚠️ Extraction warning: ${error.message}`);
|
|
448
|
+
console.warn(` However, CEF files were extracted (${cefFiles.length} files found).`);
|
|
449
|
+
console.warn(` Proceeding with partial extraction - this usually works fine.`);
|
|
450
|
+
// Don't throw - continue with what we have
|
|
451
|
+
} else {
|
|
452
|
+
// No files extracted, this is a real failure
|
|
453
|
+
throw new Error(`Extraction failed (no files extracted): ${error.message}`);
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
// No CEF directory created, this is a real failure
|
|
457
|
+
throw new Error(`Extraction failed (no CEF directory created): ${error.message}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Clean up temp file only after successful extraction
|
|
462
|
+
try {
|
|
463
|
+
unlinkSync(tempFile);
|
|
464
|
+
} catch (cleanupError) {
|
|
465
|
+
console.warn('Could not clean up temp file:', cleanupError);
|
|
466
|
+
}
|
|
347
467
|
|
|
348
468
|
// Debug: List what was actually extracted for CEF
|
|
349
469
|
try {
|
|
@@ -360,11 +480,28 @@ async function ensureCEFDependencies(targetOS?: 'macos' | 'win' | 'linux', targe
|
|
|
360
480
|
console.error('Could not list CEF extracted files:', e);
|
|
361
481
|
}
|
|
362
482
|
|
|
363
|
-
console.log(
|
|
483
|
+
console.log(`✓ CEF dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
|
|
364
484
|
|
|
365
485
|
} catch (error: any) {
|
|
366
486
|
console.error(`Failed to download CEF dependencies for ${platformOS}-${platformArch}:`, error.message);
|
|
367
|
-
|
|
487
|
+
|
|
488
|
+
// Provide helpful guidance based on the error
|
|
489
|
+
if (error.message.includes('corrupted download') || error.message.includes('zlib') || error.message.includes('unexpected end')) {
|
|
490
|
+
console.error('\n💡 This appears to be a download corruption issue. Suggestions:');
|
|
491
|
+
console.error(' • Check your internet connection stability');
|
|
492
|
+
console.error(' • Try running the command again (it will retry automatically)');
|
|
493
|
+
console.error(' • Clear the cache if the issue persists:');
|
|
494
|
+
console.error(` rm -rf "${ELECTROBUN_DEP_PATH}"`);
|
|
495
|
+
} else if (error.message.includes('HTTP 404') || error.message.includes('Not Found')) {
|
|
496
|
+
console.error('\n💡 The CEF release was not found. This could mean:');
|
|
497
|
+
console.error(' • The version specified doesn\'t have CEF binaries available');
|
|
498
|
+
console.error(' • You\'re using a development/unreleased version');
|
|
499
|
+
console.error(' • Try using a stable version instead');
|
|
500
|
+
} else {
|
|
501
|
+
console.error('\nPlease ensure you have an internet connection and the release exists.');
|
|
502
|
+
console.error(`If the problem persists, try clearing the cache: rm -rf "${ELECTROBUN_DEP_PATH}"`);
|
|
503
|
+
}
|
|
504
|
+
|
|
368
505
|
process.exit(1);
|
|
369
506
|
}
|
|
370
507
|
}
|
|
@@ -402,6 +539,9 @@ const defaultConfig = {
|
|
|
402
539
|
entitlements: {
|
|
403
540
|
// This entitlement is required for Electrobun apps with a hardened runtime (required for notarization) to run on macos
|
|
404
541
|
"com.apple.security.cs.allow-jit": true,
|
|
542
|
+
// Required for bun runtime to work with dynamic code execution and JIT compilation when signed
|
|
543
|
+
"com.apple.security.cs.allow-unsigned-executable-memory": true,
|
|
544
|
+
"com.apple.security.cs.disable-library-validation": true,
|
|
405
545
|
},
|
|
406
546
|
icons: "icon.iconset",
|
|
407
547
|
},
|
|
@@ -424,6 +564,55 @@ const defaultConfig = {
|
|
|
424
564
|
},
|
|
425
565
|
};
|
|
426
566
|
|
|
567
|
+
// Mapping of entitlements to their corresponding Info.plist usage description keys
|
|
568
|
+
const ENTITLEMENT_TO_PLIST_KEY: Record<string, string> = {
|
|
569
|
+
"com.apple.security.device.camera": "NSCameraUsageDescription",
|
|
570
|
+
"com.apple.security.device.microphone": "NSMicrophoneUsageDescription",
|
|
571
|
+
"com.apple.security.device.audio-input": "NSMicrophoneUsageDescription",
|
|
572
|
+
"com.apple.security.personal-information.location": "NSLocationUsageDescription",
|
|
573
|
+
"com.apple.security.personal-information.location-when-in-use": "NSLocationWhenInUseUsageDescription",
|
|
574
|
+
"com.apple.security.personal-information.contacts": "NSContactsUsageDescription",
|
|
575
|
+
"com.apple.security.personal-information.calendars": "NSCalendarsUsageDescription",
|
|
576
|
+
"com.apple.security.personal-information.reminders": "NSRemindersUsageDescription",
|
|
577
|
+
"com.apple.security.personal-information.photos-library": "NSPhotoLibraryUsageDescription",
|
|
578
|
+
"com.apple.security.personal-information.apple-music-library": "NSAppleMusicUsageDescription",
|
|
579
|
+
"com.apple.security.personal-information.motion": "NSMotionUsageDescription",
|
|
580
|
+
"com.apple.security.personal-information.speech-recognition": "NSSpeechRecognitionUsageDescription",
|
|
581
|
+
"com.apple.security.device.bluetooth": "NSBluetoothAlwaysUsageDescription",
|
|
582
|
+
"com.apple.security.files.user-selected.read-write": "NSDocumentsFolderUsageDescription",
|
|
583
|
+
"com.apple.security.files.downloads.read-write": "NSDownloadsFolderUsageDescription",
|
|
584
|
+
"com.apple.security.files.desktop.read-write": "NSDesktopFolderUsageDescription",
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// Helper function to escape XML special characters
|
|
588
|
+
function escapeXml(str: string): string {
|
|
589
|
+
return str
|
|
590
|
+
.replace(/&/g, '&')
|
|
591
|
+
.replace(/</g, '<')
|
|
592
|
+
.replace(/>/g, '>')
|
|
593
|
+
.replace(/"/g, '"')
|
|
594
|
+
.replace(/'/g, ''');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Helper function to generate usage description entries for Info.plist
|
|
598
|
+
function generateUsageDescriptions(entitlements: Record<string, boolean | string>): string {
|
|
599
|
+
const usageEntries: string[] = [];
|
|
600
|
+
|
|
601
|
+
for (const [entitlement, value] of Object.entries(entitlements)) {
|
|
602
|
+
const plistKey = ENTITLEMENT_TO_PLIST_KEY[entitlement];
|
|
603
|
+
if (plistKey && value) {
|
|
604
|
+
// Use the string value as description, or a default if it's just true
|
|
605
|
+
const description = typeof value === "string"
|
|
606
|
+
? escapeXml(value)
|
|
607
|
+
: `This app requires access for ${entitlement.split('.').pop()?.replace('-', ' ')}`;
|
|
608
|
+
|
|
609
|
+
usageEntries.push(` <key>${plistKey}</key>\n <string>${description}</string>`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return usageEntries.join('\n');
|
|
614
|
+
}
|
|
615
|
+
|
|
427
616
|
const command = commandDefaults[commandArg];
|
|
428
617
|
|
|
429
618
|
if (!command) {
|
|
@@ -431,19 +620,21 @@ if (!command) {
|
|
|
431
620
|
process.exit(1);
|
|
432
621
|
}
|
|
433
622
|
|
|
434
|
-
|
|
623
|
+
// Main execution function
|
|
624
|
+
async function main() {
|
|
625
|
+
const config = await getConfig();
|
|
435
626
|
|
|
436
|
-
const envArg =
|
|
437
|
-
|
|
627
|
+
const envArg =
|
|
628
|
+
process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
|
|
438
629
|
|
|
439
|
-
const targetsArg =
|
|
440
|
-
|
|
630
|
+
const targetsArg =
|
|
631
|
+
process.argv.find((arg) => arg.startsWith("--targets="))?.split("=")[1] || "";
|
|
441
632
|
|
|
442
|
-
const validEnvironments = ["dev", "canary", "stable"];
|
|
633
|
+
const validEnvironments = ["dev", "canary", "stable"];
|
|
443
634
|
|
|
444
|
-
// todo (yoav): dev, canary, and stable;
|
|
445
|
-
const buildEnvironment: "dev" | "canary" | "stable" =
|
|
446
|
-
|
|
635
|
+
// todo (yoav): dev, canary, and stable;
|
|
636
|
+
const buildEnvironment: "dev" | "canary" | "stable" =
|
|
637
|
+
validEnvironments.includes(envArg || "dev") ? (envArg || "dev") : "dev";
|
|
447
638
|
|
|
448
639
|
// Determine build targets
|
|
449
640
|
type BuildTarget = { os: 'macos' | 'win' | 'linux', arch: 'arm64' | 'x64' };
|
|
@@ -678,6 +869,13 @@ function escapePathForTerminal(filePath: string) {
|
|
|
678
869
|
|
|
679
870
|
return escapedPath;
|
|
680
871
|
}
|
|
872
|
+
|
|
873
|
+
function sanitizeVolumeNameForHdiutil(volumeName: string) {
|
|
874
|
+
// Remove or replace characters that cause issues with hdiutil volume mounting
|
|
875
|
+
// Parentheses and other special characters can cause "Operation not permitted" errors
|
|
876
|
+
return volumeName.replace(/[()]/g, '');
|
|
877
|
+
}
|
|
878
|
+
|
|
681
879
|
// MyApp
|
|
682
880
|
|
|
683
881
|
// const appName = config.app.name.replace(/\s/g, '-').toLowerCase();
|
|
@@ -696,57 +894,117 @@ const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName
|
|
|
696
894
|
let proc = null;
|
|
697
895
|
|
|
698
896
|
if (commandArg === "init") {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
console.log(`🚀 Initializing Electrobun project: ${projectName}`);
|
|
703
|
-
|
|
704
|
-
// Validate template name
|
|
705
|
-
const availableTemplates = getTemplateNames();
|
|
706
|
-
if (!availableTemplates.includes(templateName)) {
|
|
707
|
-
console.error(`❌ Template "${templateName}" not found.`);
|
|
708
|
-
console.log(`Available templates: ${availableTemplates.join(", ")}`);
|
|
709
|
-
process.exit(1);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const template = getTemplate(templateName);
|
|
713
|
-
if (!template) {
|
|
714
|
-
console.error(`❌ Could not load template "${templateName}"`);
|
|
715
|
-
process.exit(1);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// Create project directory
|
|
719
|
-
const projectPath = join(process.cwd(), projectName);
|
|
720
|
-
if (existsSync(projectPath)) {
|
|
721
|
-
console.error(`❌ Directory "${projectName}" already exists.`);
|
|
722
|
-
process.exit(1);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
mkdirSync(projectPath, { recursive: true });
|
|
726
|
-
|
|
727
|
-
// Extract template files
|
|
728
|
-
let fileCount = 0;
|
|
729
|
-
for (const [relativePath, content] of Object.entries(template.files)) {
|
|
730
|
-
const fullPath = join(projectPath, relativePath);
|
|
731
|
-
const dir = dirname(fullPath);
|
|
897
|
+
await (async () => {
|
|
898
|
+
const secondArg = process.argv[indexOfElectrobun + 2];
|
|
899
|
+
const availableTemplates = getTemplateNames();
|
|
732
900
|
|
|
733
|
-
|
|
734
|
-
|
|
901
|
+
let projectName: string;
|
|
902
|
+
let templateName: string;
|
|
735
903
|
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
904
|
+
// Check if --template= flag is used
|
|
905
|
+
const templateFlag = process.argv.find(arg => arg.startsWith("--template="));
|
|
906
|
+
if (templateFlag) {
|
|
907
|
+
// Traditional usage: electrobun init my-project --template=photo-booth
|
|
908
|
+
projectName = secondArg || "my-electrobun-app";
|
|
909
|
+
templateName = templateFlag.split("=")[1];
|
|
910
|
+
} else if (secondArg && availableTemplates.includes(secondArg)) {
|
|
911
|
+
// New intuitive usage: electrobun init photo-booth
|
|
912
|
+
projectName = secondArg; // Use template name as project name
|
|
913
|
+
templateName = secondArg;
|
|
914
|
+
} else {
|
|
915
|
+
// Interactive menu when no template specified
|
|
916
|
+
console.log("🚀 Welcome to Electrobun!");
|
|
917
|
+
console.log("");
|
|
918
|
+
console.log("Available templates:");
|
|
919
|
+
availableTemplates.forEach((template, index) => {
|
|
920
|
+
console.log(` ${index + 1}. ${template}`);
|
|
921
|
+
});
|
|
922
|
+
console.log("");
|
|
923
|
+
|
|
924
|
+
// Simple CLI selection using readline
|
|
925
|
+
const rl = readline.createInterface({
|
|
926
|
+
input: process.stdin,
|
|
927
|
+
output: process.stdout
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
const choice = await new Promise<string>((resolve) => {
|
|
931
|
+
rl.question('Select a template (enter number): ', (answer) => {
|
|
932
|
+
rl.close();
|
|
933
|
+
resolve(answer.trim());
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
const templateIndex = parseInt(choice) - 1;
|
|
938
|
+
if (templateIndex < 0 || templateIndex >= availableTemplates.length) {
|
|
939
|
+
console.error(`❌ Invalid selection. Please enter a number between 1 and ${availableTemplates.length}.`);
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
templateName = availableTemplates[templateIndex];
|
|
944
|
+
|
|
945
|
+
// Ask for project name
|
|
946
|
+
const rl2 = readline.createInterface({
|
|
947
|
+
input: process.stdin,
|
|
948
|
+
output: process.stdout
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
projectName = await new Promise<string>((resolve) => {
|
|
952
|
+
rl2.question(`Enter project name (default: my-${templateName}-app): `, (answer) => {
|
|
953
|
+
rl2.close();
|
|
954
|
+
resolve(answer.trim() || `my-${templateName}-app`);
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
console.log(`🚀 Initializing Electrobun project: ${projectName}`);
|
|
960
|
+
console.log(`📋 Using template: ${templateName}`);
|
|
961
|
+
|
|
962
|
+
// Validate template name
|
|
963
|
+
if (!availableTemplates.includes(templateName)) {
|
|
964
|
+
console.error(`❌ Template "${templateName}" not found.`);
|
|
965
|
+
console.log(`Available templates: ${availableTemplates.join(", ")}`);
|
|
966
|
+
process.exit(1);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const template = getTemplate(templateName);
|
|
970
|
+
if (!template) {
|
|
971
|
+
console.error(`❌ Could not load template "${templateName}"`);
|
|
972
|
+
process.exit(1);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Create project directory
|
|
976
|
+
const projectPath = join(process.cwd(), projectName);
|
|
977
|
+
if (existsSync(projectPath)) {
|
|
978
|
+
console.error(`❌ Directory "${projectName}" already exists.`);
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
mkdirSync(projectPath, { recursive: true });
|
|
983
|
+
|
|
984
|
+
// Extract template files
|
|
985
|
+
let fileCount = 0;
|
|
986
|
+
for (const [relativePath, content] of Object.entries(template.files)) {
|
|
987
|
+
const fullPath = join(projectPath, relativePath);
|
|
988
|
+
const dir = dirname(fullPath);
|
|
989
|
+
|
|
990
|
+
// Create directory if it doesn't exist
|
|
991
|
+
mkdirSync(dir, { recursive: true });
|
|
992
|
+
|
|
993
|
+
// Write file
|
|
994
|
+
writeFileSync(fullPath, content, 'utf-8');
|
|
995
|
+
fileCount++;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
console.log(`✅ Created ${fileCount} files from "${templateName}" template`);
|
|
999
|
+
console.log(`📁 Project created at: ${projectPath}`);
|
|
1000
|
+
console.log("");
|
|
1001
|
+
console.log("📦 Next steps:");
|
|
1002
|
+
console.log(` cd ${projectName}`);
|
|
1003
|
+
console.log(" bun install");
|
|
1004
|
+
console.log(" bun start");
|
|
1005
|
+
console.log("");
|
|
1006
|
+
console.log("🎉 Happy building with Electrobun!");
|
|
1007
|
+
})();
|
|
750
1008
|
} else if (commandArg === "build") {
|
|
751
1009
|
// Ensure core binaries are available for the target platform before starting build
|
|
752
1010
|
await ensureCoreDependencies(currentTarget.os, currentTarget.arch);
|
|
@@ -792,6 +1050,9 @@ if (commandArg === "init") {
|
|
|
792
1050
|
|
|
793
1051
|
// We likely want to let users configure this for different environments (eg: dev, canary, stable) and/or
|
|
794
1052
|
// provide methods to help segment data in those folders based on channel/environment
|
|
1053
|
+
// Generate usage descriptions from entitlements
|
|
1054
|
+
const usageDescriptions = generateUsageDescriptions(config.build.mac.entitlements || {});
|
|
1055
|
+
|
|
795
1056
|
const InfoPlistContents = `<?xml version="1.0" encoding="UTF-8"?>
|
|
796
1057
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
797
1058
|
<plist version="1.0">
|
|
@@ -807,7 +1068,7 @@ if (commandArg === "init") {
|
|
|
807
1068
|
<key>CFBundlePackageType</key>
|
|
808
1069
|
<string>APPL</string>
|
|
809
1070
|
<key>CFBundleIconFile</key>
|
|
810
|
-
<string>AppIcon</string
|
|
1071
|
+
<string>AppIcon</string>${usageDescriptions ? '\n' + usageDescriptions : ''}
|
|
811
1072
|
</dict>
|
|
812
1073
|
</plist>`;
|
|
813
1074
|
|
|
@@ -847,23 +1108,21 @@ if (commandArg === "init") {
|
|
|
847
1108
|
// mkdirSync(destLauncherFolder, {recursive: true});
|
|
848
1109
|
// }
|
|
849
1110
|
// cpSync(zigLauncherBinarySource, zigLauncherDestination, {recursive: true, dereference: true});
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
mkdirSync(destLauncherFolder, { recursive: true });
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
|
|
861
|
-
recursive: true,
|
|
862
|
-
dereference: true,
|
|
863
|
-
});
|
|
1111
|
+
// Copy zig launcher for all builds (dev, canary, stable)
|
|
1112
|
+
const bunCliLauncherBinarySource = targetPaths.LAUNCHER_RELEASE;
|
|
1113
|
+
const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + targetBinExt;
|
|
1114
|
+
const destLauncherFolder = dirname(bunCliLauncherDestination);
|
|
1115
|
+
if (!existsSync(destLauncherFolder)) {
|
|
1116
|
+
// console.info('creating folder: ', destFolder);
|
|
1117
|
+
mkdirSync(destLauncherFolder, { recursive: true });
|
|
864
1118
|
}
|
|
865
1119
|
|
|
866
|
-
cpSync(
|
|
1120
|
+
cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
|
|
1121
|
+
recursive: true,
|
|
1122
|
+
dereference: true,
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
cpSync(targetPaths.MAIN_JS, join(appBundleFolderResourcesPath, 'main.js'));
|
|
867
1126
|
|
|
868
1127
|
// Bun runtime binary
|
|
869
1128
|
// todo (yoav): this only works for the current architecture
|
|
@@ -1271,11 +1530,13 @@ if (commandArg === "init") {
|
|
|
1271
1530
|
|
|
1272
1531
|
// Run postBuild script
|
|
1273
1532
|
if (config.scripts.postBuild) {
|
|
1533
|
+
console.log("Running postBuild script:", config.scripts.postBuild);
|
|
1274
1534
|
// Use host platform's bun binary for running scripts, not target platform's
|
|
1275
1535
|
const hostPaths = getPlatformPaths(OS, ARCH);
|
|
1276
1536
|
|
|
1277
|
-
Bun.spawnSync([hostPaths.BUN_BINARY, config.scripts.postBuild], {
|
|
1537
|
+
const result = Bun.spawnSync([hostPaths.BUN_BINARY, config.scripts.postBuild], {
|
|
1278
1538
|
stdio: ["ignore", "inherit", "inherit"],
|
|
1539
|
+
cwd: projectRoot, // Add cwd to ensure script runs from project root
|
|
1279
1540
|
env: {
|
|
1280
1541
|
...process.env,
|
|
1281
1542
|
ELECTROBUN_BUILD_ENV: buildEnvironment,
|
|
@@ -1288,6 +1549,18 @@ if (commandArg === "init") {
|
|
|
1288
1549
|
ELECTROBUN_ARTIFACT_DIR: artifactFolder,
|
|
1289
1550
|
},
|
|
1290
1551
|
});
|
|
1552
|
+
|
|
1553
|
+
if (result.exitCode !== 0) {
|
|
1554
|
+
console.error("postBuild script failed with exit code:", result.exitCode);
|
|
1555
|
+
if (result.stderr) {
|
|
1556
|
+
console.error("stderr:", result.stderr.toString());
|
|
1557
|
+
}
|
|
1558
|
+
// Also log which bun binary we're trying to use
|
|
1559
|
+
console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);
|
|
1560
|
+
console.error("Script path:", config.scripts.postBuild);
|
|
1561
|
+
console.error("Working directory:", projectRoot);
|
|
1562
|
+
process.exit(1);
|
|
1563
|
+
}
|
|
1291
1564
|
}
|
|
1292
1565
|
// All the unique files are in the bundle now. Create an initial temporary tar file
|
|
1293
1566
|
// for hashing the contents
|
|
@@ -1476,7 +1749,7 @@ if (commandArg === "init") {
|
|
|
1476
1749
|
// hdiutil create -volname "YourAppName" -srcfolder /path/to/YourApp.app -ov -format UDZO YourAppName.dmg
|
|
1477
1750
|
// Note: use ULFO (lzfse) for better compatibility with large CEF frameworks and modern macOS
|
|
1478
1751
|
execSync(
|
|
1479
|
-
`hdiutil create -volname "${appFileName}" -srcfolder ${escapePathForTerminal(
|
|
1752
|
+
`hdiutil create -volname "${sanitizeVolumeNameForHdiutil(appFileName)}" -srcfolder ${escapePathForTerminal(
|
|
1480
1753
|
selfExtractingBundle.appBundleFolderPath
|
|
1481
1754
|
)} -ov -format ULFO ${escapePathForTerminal(dmgPath)}`
|
|
1482
1755
|
);
|
|
@@ -1741,24 +2014,27 @@ exec "\$LAUNCHER_BINARY" "\$@"
|
|
|
1741
2014
|
|
|
1742
2015
|
let mainProc;
|
|
1743
2016
|
let bundleExecPath: string;
|
|
2017
|
+
let bundleResourcesPath: string;
|
|
1744
2018
|
|
|
1745
2019
|
if (OS === 'macos') {
|
|
1746
2020
|
bundleExecPath = join(buildFolder, bundleFileName, "Contents", 'MacOS');
|
|
2021
|
+
bundleResourcesPath = join(buildFolder, bundleFileName, "Contents", 'Resources');
|
|
1747
2022
|
} else if (OS === 'linux' || OS === 'win') {
|
|
1748
2023
|
bundleExecPath = join(buildFolder, bundleFileName, "bin");
|
|
2024
|
+
bundleResourcesPath = join(buildFolder, bundleFileName, "Resources");
|
|
1749
2025
|
} else {
|
|
1750
2026
|
throw new Error(`Unsupported OS: ${OS}`);
|
|
1751
2027
|
}
|
|
1752
2028
|
|
|
1753
2029
|
if (OS === 'macos') {
|
|
1754
|
-
|
|
1755
|
-
mainProc = Bun.spawn([join(bundleExecPath,
|
|
2030
|
+
// Use the zig launcher for all builds (dev, canary, stable)
|
|
2031
|
+
mainProc = Bun.spawn([join(bundleExecPath, 'launcher')], {
|
|
1756
2032
|
stdio: ['inherit', 'inherit', 'inherit'],
|
|
1757
2033
|
cwd: bundleExecPath
|
|
1758
2034
|
})
|
|
1759
2035
|
} else if (OS === 'win') {
|
|
1760
|
-
// Try the main process
|
|
1761
|
-
mainProc = Bun.spawn(['./bun.exe', '
|
|
2036
|
+
// Try the main process - use relative path to Resources folder
|
|
2037
|
+
mainProc = Bun.spawn(['./bun.exe', '../Resources/main.js'], {
|
|
1762
2038
|
stdio: ['inherit', 'inherit', 'inherit'],
|
|
1763
2039
|
cwd: bundleExecPath,
|
|
1764
2040
|
onExit: (proc, exitCode, signalCode, error) => {
|
|
@@ -1779,7 +2055,7 @@ exec "\$LAUNCHER_BINARY" "\$@"
|
|
|
1779
2055
|
}
|
|
1780
2056
|
}
|
|
1781
2057
|
|
|
1782
|
-
mainProc = Bun.spawn([join(bundleExecPath, 'bun'), join(
|
|
2058
|
+
mainProc = Bun.spawn([join(bundleExecPath, 'bun'), join(bundleResourcesPath, 'main.js')], {
|
|
1783
2059
|
stdio: ['inherit', 'inherit', 'inherit'],
|
|
1784
2060
|
cwd: bundleExecPath,
|
|
1785
2061
|
env
|
|
@@ -1787,23 +2063,49 @@ exec "\$LAUNCHER_BINARY" "\$@"
|
|
|
1787
2063
|
}
|
|
1788
2064
|
|
|
1789
2065
|
process.on("SIGINT", () => {
|
|
1790
|
-
console.log('
|
|
1791
|
-
|
|
1792
|
-
mainProc
|
|
1793
|
-
|
|
2066
|
+
console.log('[electrobun dev] Received SIGINT, initiating graceful shutdown...')
|
|
2067
|
+
|
|
2068
|
+
if (mainProc) {
|
|
2069
|
+
// First attempt graceful shutdown by sending SIGINT to child
|
|
2070
|
+
console.log('[electrobun dev] Requesting graceful shutdown from app...')
|
|
2071
|
+
mainProc.kill("SIGINT");
|
|
2072
|
+
|
|
2073
|
+
// Give the app time to clean up (e.g., call killApp())
|
|
2074
|
+
setTimeout(() => {
|
|
2075
|
+
if (mainProc && !mainProc.killed) {
|
|
2076
|
+
console.log('[electrobun dev] App did not exit gracefully, forcing termination...')
|
|
2077
|
+
mainProc.kill("SIGKILL");
|
|
2078
|
+
}
|
|
2079
|
+
process.exit(0);
|
|
2080
|
+
}, 2000); // 2 second timeout for graceful shutdown
|
|
2081
|
+
} else {
|
|
2082
|
+
process.exit(0);
|
|
2083
|
+
}
|
|
1794
2084
|
});
|
|
1795
2085
|
|
|
1796
2086
|
}
|
|
1797
2087
|
|
|
1798
|
-
function getConfig() {
|
|
2088
|
+
async function getConfig() {
|
|
1799
2089
|
let loadedConfig = {};
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
2090
|
+
const foundConfigPath = findConfigFile();
|
|
2091
|
+
|
|
2092
|
+
if (foundConfigPath) {
|
|
2093
|
+
console.log(`Using config file: ${basename(foundConfigPath)}`);
|
|
2094
|
+
|
|
1803
2095
|
try {
|
|
1804
|
-
|
|
2096
|
+
// Use dynamic import for TypeScript ESM files
|
|
2097
|
+
// Bun handles TypeScript natively, no transpilation needed
|
|
2098
|
+
const configModule = await import(foundConfigPath);
|
|
2099
|
+
loadedConfig = configModule.default || configModule;
|
|
2100
|
+
|
|
2101
|
+
// Validate that we got a valid config object
|
|
2102
|
+
if (!loadedConfig || typeof loadedConfig !== 'object') {
|
|
2103
|
+
console.error("Config file must export a default object");
|
|
2104
|
+
console.error("using default config instead");
|
|
2105
|
+
loadedConfig = {};
|
|
2106
|
+
}
|
|
1805
2107
|
} catch (error) {
|
|
1806
|
-
console.error("Failed to
|
|
2108
|
+
console.error("Failed to load config file:", error);
|
|
1807
2109
|
console.error("using default config instead");
|
|
1808
2110
|
}
|
|
1809
2111
|
}
|
|
@@ -1851,7 +2153,7 @@ function getConfig() {
|
|
|
1851
2153
|
};
|
|
1852
2154
|
}
|
|
1853
2155
|
|
|
1854
|
-
function buildEntitlementsFile(entitlements) {
|
|
2156
|
+
function buildEntitlementsFile(entitlements: Record<string, boolean | string>) {
|
|
1855
2157
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1856
2158
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1857
2159
|
<plist version="1.0">
|
|
@@ -1870,7 +2172,8 @@ function getEntitlementValue(value: boolean | string) {
|
|
|
1870
2172
|
if (typeof value === "boolean") {
|
|
1871
2173
|
return `<${value.toString()}/>`;
|
|
1872
2174
|
} else {
|
|
1873
|
-
return
|
|
2175
|
+
// For string values (usage descriptions), still return boolean true for the entitlement
|
|
2176
|
+
return `<true/>`;
|
|
1874
2177
|
}
|
|
1875
2178
|
}
|
|
1876
2179
|
|
|
@@ -2187,30 +2490,189 @@ function codesignAppBundle(
|
|
|
2187
2490
|
process.exit(1);
|
|
2188
2491
|
}
|
|
2189
2492
|
|
|
2190
|
-
//
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2493
|
+
// If this is a DMG file, sign it directly
|
|
2494
|
+
if (appBundleOrDmgPath.endsWith('.dmg')) {
|
|
2495
|
+
execSync(
|
|
2496
|
+
`codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" ${escapePathForTerminal(
|
|
2497
|
+
appBundleOrDmgPath
|
|
2498
|
+
)}`
|
|
2499
|
+
);
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2195
2502
|
|
|
2503
|
+
// For app bundles, sign binaries individually to avoid --deep issues with notarization
|
|
2504
|
+
const contentsPath = join(appBundleOrDmgPath, 'Contents');
|
|
2505
|
+
const macosPath = join(contentsPath, 'MacOS');
|
|
2506
|
+
|
|
2507
|
+
// Prepare entitlements if provided
|
|
2196
2508
|
if (entitlementsFilePath) {
|
|
2197
2509
|
const entitlementsFileContents = buildEntitlementsFile(
|
|
2198
2510
|
config.build.mac.entitlements
|
|
2199
2511
|
);
|
|
2200
2512
|
Bun.write(entitlementsFilePath, entitlementsFileContents);
|
|
2513
|
+
}
|
|
2201
2514
|
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2515
|
+
// Sign frameworks first (CEF framework requires special handling)
|
|
2516
|
+
const frameworksPath = join(contentsPath, 'Frameworks');
|
|
2517
|
+
if (existsSync(frameworksPath)) {
|
|
2518
|
+
try {
|
|
2519
|
+
const frameworks = readdirSync(frameworksPath);
|
|
2520
|
+
for (const framework of frameworks) {
|
|
2521
|
+
if (framework.endsWith('.framework')) {
|
|
2522
|
+
const frameworkPath = join(frameworksPath, framework);
|
|
2523
|
+
|
|
2524
|
+
if (framework === 'Chromium Embedded Framework.framework') {
|
|
2525
|
+
console.log(`Signing CEF framework components: ${framework}`);
|
|
2526
|
+
|
|
2527
|
+
// Sign CEF libraries first
|
|
2528
|
+
const librariesPath = join(frameworkPath, 'Libraries');
|
|
2529
|
+
if (existsSync(librariesPath)) {
|
|
2530
|
+
const libraries = readdirSync(librariesPath);
|
|
2531
|
+
for (const library of libraries) {
|
|
2532
|
+
if (library.endsWith('.dylib')) {
|
|
2533
|
+
const libraryPath = join(librariesPath, library);
|
|
2534
|
+
console.log(`Signing CEF library: ${library}`);
|
|
2535
|
+
execSync(
|
|
2536
|
+
`codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${escapePathForTerminal(libraryPath)}`
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
// CEF helper apps are in the main Frameworks directory, not inside the CEF framework
|
|
2543
|
+
// We'll sign them after signing all frameworks
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// Sign the framework bundle itself (for CEF and any other frameworks)
|
|
2547
|
+
console.log(`Signing framework bundle: ${framework}`);
|
|
2548
|
+
execSync(
|
|
2549
|
+
`codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${escapePathForTerminal(frameworkPath)}`
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
} catch (err) {
|
|
2554
|
+
console.log("Error signing frameworks:", err);
|
|
2555
|
+
throw err; // Re-throw to fail the build since framework signing is critical
|
|
2556
|
+
}
|
|
2213
2557
|
}
|
|
2558
|
+
|
|
2559
|
+
// Sign CEF helper apps (they're in the main Frameworks directory, not inside CEF framework)
|
|
2560
|
+
const cefHelperApps = [
|
|
2561
|
+
'bun Helper.app',
|
|
2562
|
+
'bun Helper (GPU).app',
|
|
2563
|
+
'bun Helper (Plugin).app',
|
|
2564
|
+
'bun Helper (Alerts).app',
|
|
2565
|
+
'bun Helper (Renderer).app'
|
|
2566
|
+
];
|
|
2567
|
+
|
|
2568
|
+
for (const helperApp of cefHelperApps) {
|
|
2569
|
+
const helperPath = join(frameworksPath, helperApp);
|
|
2570
|
+
if (existsSync(helperPath)) {
|
|
2571
|
+
const helperExecutablePath = join(helperPath, 'Contents', 'MacOS', helperApp.replace('.app', ''));
|
|
2572
|
+
if (existsSync(helperExecutablePath)) {
|
|
2573
|
+
console.log(`Signing CEF helper executable: ${helperApp}`);
|
|
2574
|
+
const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
|
|
2575
|
+
execSync(
|
|
2576
|
+
`codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(helperExecutablePath)}`
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
console.log(`Signing CEF helper bundle: ${helperApp}`);
|
|
2581
|
+
const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
|
|
2582
|
+
execSync(
|
|
2583
|
+
`codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(helperPath)}`
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// Sign all binaries and libraries in MacOS folder and subdirectories
|
|
2589
|
+
console.log("Signing all binaries in MacOS folder...");
|
|
2590
|
+
|
|
2591
|
+
// Recursively find all executables and libraries in MacOS folder
|
|
2592
|
+
function findExecutables(dir: string): string[] {
|
|
2593
|
+
let executables: string[] = [];
|
|
2594
|
+
|
|
2595
|
+
try {
|
|
2596
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
2597
|
+
|
|
2598
|
+
for (const entry of entries) {
|
|
2599
|
+
const fullPath = join(dir, entry.name);
|
|
2600
|
+
|
|
2601
|
+
if (entry.isDirectory()) {
|
|
2602
|
+
// Recursively search subdirectories
|
|
2603
|
+
executables = executables.concat(findExecutables(fullPath));
|
|
2604
|
+
} else if (entry.isFile()) {
|
|
2605
|
+
// Check if it's an executable or library
|
|
2606
|
+
try {
|
|
2607
|
+
const fileInfo = execSync(`file -b "${fullPath}"`, { encoding: 'utf8' }).trim();
|
|
2608
|
+
if (fileInfo.includes('Mach-O') || entry.name.endsWith('.dylib')) {
|
|
2609
|
+
executables.push(fullPath);
|
|
2610
|
+
}
|
|
2611
|
+
} catch {
|
|
2612
|
+
// If file command fails, check by extension
|
|
2613
|
+
if (entry.name.endsWith('.dylib') || !entry.name.includes('.')) {
|
|
2614
|
+
// No extension often means executable
|
|
2615
|
+
executables.push(fullPath);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
} catch (err) {
|
|
2621
|
+
console.error(`Error scanning directory ${dir}:`, err);
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
return executables;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
const executablesInMacOS = findExecutables(macosPath);
|
|
2628
|
+
|
|
2629
|
+
// Sign each found executable
|
|
2630
|
+
for (const execPath of executablesInMacOS) {
|
|
2631
|
+
const fileName = basename(execPath);
|
|
2632
|
+
const relativePath = execPath.replace(macosPath + '/', '');
|
|
2633
|
+
|
|
2634
|
+
// Use filename as identifier (without extension)
|
|
2635
|
+
const identifier = fileName.replace(/\.[^.]+$/, '');
|
|
2636
|
+
|
|
2637
|
+
console.log(`Signing ${relativePath} with identifier ${identifier}`);
|
|
2638
|
+
const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
|
|
2639
|
+
|
|
2640
|
+
try {
|
|
2641
|
+
execSync(
|
|
2642
|
+
`codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime --identifier ${identifier} ${entitlementFlag} ${escapePathForTerminal(execPath)}`
|
|
2643
|
+
);
|
|
2644
|
+
} catch (err) {
|
|
2645
|
+
console.error(`Failed to sign ${relativePath}:`, err.message);
|
|
2646
|
+
// Continue signing other files even if one fails
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// Note: main.js is now in Resources and will be automatically sealed when signing the app bundle
|
|
2651
|
+
|
|
2652
|
+
// Sign the main executable (launcher) - this should use the app's bundle identifier, not "launcher"
|
|
2653
|
+
const launcherPath = join(macosPath, 'launcher');
|
|
2654
|
+
if (existsSync(launcherPath)) {
|
|
2655
|
+
console.log("Signing main executable (launcher)");
|
|
2656
|
+
const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
|
|
2657
|
+
try {
|
|
2658
|
+
execSync(
|
|
2659
|
+
`codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(launcherPath)}`
|
|
2660
|
+
);
|
|
2661
|
+
} catch (error) {
|
|
2662
|
+
console.error("Failed to sign launcher:", error.message);
|
|
2663
|
+
console.log("Attempting to sign launcher without runtime hardening...");
|
|
2664
|
+
execSync(
|
|
2665
|
+
`codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" ${entitlementFlag} ${escapePathForTerminal(launcherPath)}`
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// Finally, sign the app bundle itself (without --deep)
|
|
2671
|
+
console.log("Signing app bundle");
|
|
2672
|
+
const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
|
|
2673
|
+
execSync(
|
|
2674
|
+
`codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(appBundleOrDmgPath)}`
|
|
2675
|
+
);
|
|
2214
2676
|
}
|
|
2215
2677
|
|
|
2216
2678
|
function notarizeAndStaple(appOrDmgPath: string) {
|
|
@@ -2349,3 +2811,11 @@ function createAppBundle(bundleName: string, parentFolder: string, targetOS: 'ma
|
|
|
2349
2811
|
throw new Error(`Unsupported OS: ${targetOS}`);
|
|
2350
2812
|
}
|
|
2351
2813
|
}
|
|
2814
|
+
|
|
2815
|
+
} // End of main() function
|
|
2816
|
+
|
|
2817
|
+
// Run the main function
|
|
2818
|
+
main().catch((error) => {
|
|
2819
|
+
console.error('Fatal error:', error);
|
|
2820
|
+
process.exit(1);
|
|
2821
|
+
});
|