electrobun 0.0.19-beta.12 → 0.0.19-beta.128

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.
Files changed (100) hide show
  1. package/BUILD.md +90 -0
  2. package/README.md +1 -1
  3. package/bin/electrobun.cjs +31 -11
  4. package/debug.js +5 -0
  5. package/dist/api/browser/builtinrpcSchema.ts +19 -0
  6. package/dist/api/browser/index.ts +409 -0
  7. package/dist/api/browser/rpc/webview.ts +79 -0
  8. package/dist/api/browser/stylesAndElements.ts +3 -0
  9. package/dist/api/browser/webviewtag.ts +586 -0
  10. package/dist/api/bun/ElectrobunConfig.ts +171 -0
  11. package/dist/api/bun/core/ApplicationMenu.ts +66 -0
  12. package/dist/api/bun/core/BrowserView.ts +349 -0
  13. package/dist/api/bun/core/BrowserWindow.ts +195 -0
  14. package/dist/api/bun/core/ContextMenu.ts +67 -0
  15. package/dist/api/bun/core/Paths.ts +5 -0
  16. package/dist/api/bun/core/Socket.ts +181 -0
  17. package/dist/api/bun/core/Tray.ts +121 -0
  18. package/dist/api/bun/core/Updater.ts +681 -0
  19. package/dist/api/bun/core/Utils.ts +48 -0
  20. package/dist/api/bun/events/ApplicationEvents.ts +14 -0
  21. package/dist/api/bun/events/event.ts +29 -0
  22. package/dist/api/bun/events/eventEmitter.ts +45 -0
  23. package/dist/api/bun/events/trayEvents.ts +9 -0
  24. package/dist/api/bun/events/webviewEvents.ts +16 -0
  25. package/dist/api/bun/events/windowEvents.ts +12 -0
  26. package/dist/api/bun/index.ts +47 -0
  27. package/dist/api/bun/proc/linux.md +43 -0
  28. package/dist/api/bun/proc/native.ts +1322 -0
  29. package/dist/api/shared/platform.ts +48 -0
  30. package/dist/main.js +54 -0
  31. package/package.json +11 -6
  32. package/src/cli/index.ts +1353 -239
  33. package/templates/hello-world/README.md +57 -0
  34. package/templates/hello-world/bun.lock +225 -0
  35. package/templates/hello-world/electrobun.config.ts +28 -0
  36. package/templates/hello-world/package.json +16 -0
  37. package/templates/hello-world/src/bun/index.ts +15 -0
  38. package/templates/hello-world/src/mainview/index.css +124 -0
  39. package/templates/hello-world/src/mainview/index.html +46 -0
  40. package/templates/hello-world/src/mainview/index.ts +1 -0
  41. package/templates/interactive-playground/README.md +26 -0
  42. package/templates/interactive-playground/assets/tray-icon.png +0 -0
  43. package/templates/interactive-playground/electrobun.config.ts +36 -0
  44. package/templates/interactive-playground/package-lock.json +36 -0
  45. package/templates/interactive-playground/package.json +15 -0
  46. package/templates/interactive-playground/src/bun/demos/files.ts +70 -0
  47. package/templates/interactive-playground/src/bun/demos/menus.ts +139 -0
  48. package/templates/interactive-playground/src/bun/demos/rpc.ts +83 -0
  49. package/templates/interactive-playground/src/bun/demos/system.ts +72 -0
  50. package/templates/interactive-playground/src/bun/demos/updates.ts +105 -0
  51. package/templates/interactive-playground/src/bun/demos/windows.ts +90 -0
  52. package/templates/interactive-playground/src/bun/index.ts +124 -0
  53. package/templates/interactive-playground/src/bun/types/rpc.ts +109 -0
  54. package/templates/interactive-playground/src/mainview/components/EventLog.ts +107 -0
  55. package/templates/interactive-playground/src/mainview/components/Sidebar.ts +65 -0
  56. package/templates/interactive-playground/src/mainview/components/Toast.ts +57 -0
  57. package/templates/interactive-playground/src/mainview/demos/FileDemo.ts +211 -0
  58. package/templates/interactive-playground/src/mainview/demos/MenuDemo.ts +102 -0
  59. package/templates/interactive-playground/src/mainview/demos/RPCDemo.ts +229 -0
  60. package/templates/interactive-playground/src/mainview/demos/TrayDemo.ts +132 -0
  61. package/templates/interactive-playground/src/mainview/demos/WebViewDemo.ts +411 -0
  62. package/templates/interactive-playground/src/mainview/demos/WindowDemo.ts +207 -0
  63. package/templates/interactive-playground/src/mainview/index.css +538 -0
  64. package/templates/interactive-playground/src/mainview/index.html +103 -0
  65. package/templates/interactive-playground/src/mainview/index.ts +238 -0
  66. package/templates/multitab-browser/README.md +34 -0
  67. package/templates/multitab-browser/bun.lock +224 -0
  68. package/templates/multitab-browser/electrobun.config.ts +32 -0
  69. package/templates/multitab-browser/package-lock.json +20 -0
  70. package/templates/multitab-browser/package.json +12 -0
  71. package/templates/multitab-browser/src/bun/index.ts +144 -0
  72. package/templates/multitab-browser/src/bun/tabManager.ts +200 -0
  73. package/templates/multitab-browser/src/bun/types/rpc.ts +78 -0
  74. package/templates/multitab-browser/src/mainview/index.css +487 -0
  75. package/templates/multitab-browser/src/mainview/index.html +94 -0
  76. package/templates/multitab-browser/src/mainview/index.ts +629 -0
  77. package/templates/photo-booth/README.md +108 -0
  78. package/templates/photo-booth/bun.lock +239 -0
  79. package/templates/photo-booth/electrobun.config.ts +28 -0
  80. package/templates/photo-booth/package.json +16 -0
  81. package/templates/photo-booth/src/bun/index.ts +92 -0
  82. package/templates/photo-booth/src/mainview/index.css +465 -0
  83. package/templates/photo-booth/src/mainview/index.html +124 -0
  84. package/templates/photo-booth/src/mainview/index.ts +499 -0
  85. package/tests/bun.lock +14 -0
  86. package/tests/electrobun.config.ts +45 -0
  87. package/tests/package-lock.json +36 -0
  88. package/tests/package.json +13 -0
  89. package/tests/src/bun/index.ts +100 -0
  90. package/tests/src/bun/test-runner.ts +508 -0
  91. package/tests/src/mainview/index.html +110 -0
  92. package/tests/src/mainview/index.ts +458 -0
  93. package/tests/src/mainview/styles/main.css +451 -0
  94. package/tests/src/testviews/tray-test.html +57 -0
  95. package/tests/src/testviews/webview-mask.html +114 -0
  96. package/tests/src/testviews/webview-navigation.html +36 -0
  97. package/tests/src/testviews/window-create.html +17 -0
  98. package/tests/src/testviews/window-events.html +29 -0
  99. package/tests/src/testviews/window-focus.html +37 -0
  100. package/tests/src/webviewtag/index.ts +11 -0
package/src/cli/index.ts CHANGED
@@ -1,103 +1,128 @@
1
- import { join, dirname, basename } from "path";
1
+ import { join, dirname, basename, relative } from "path";
2
2
  import {
3
3
  existsSync,
4
4
  readFileSync,
5
+ writeFileSync,
5
6
  cpSync,
6
7
  rmdirSync,
7
8
  mkdirSync,
8
9
  createWriteStream,
9
10
  unlinkSync,
11
+ readdirSync,
12
+ rmSync,
13
+ symlinkSync,
14
+ statSync,
15
+ copyFileSync,
10
16
  } from "fs";
11
17
  import { execSync } from "child_process";
18
+ import * as readline from "readline";
12
19
  import tar from "tar";
20
+ import archiver from "archiver";
13
21
  import { ZstdInit } from "@oneidentity/zstd-js/wasm";
14
- import {platform, arch} from 'os';
22
+ import { OS, ARCH } from '../shared/platform';
23
+ import { getTemplate, getTemplateNames } from './templates/embedded';
15
24
  // import { loadBsdiff, loadBspatch } from 'bsdiff-wasm';
16
25
  // MacOS named pipes hang at around 4KB
17
26
  const MAX_CHUNK_SIZE = 1024 * 2;
18
27
 
19
- // TODO: dedup with built.ts
20
- const OS: 'win' | 'linux' | 'macos' = getPlatform();
21
- const ARCH: 'arm64' | 'x64' = getArch();
22
-
23
- function getPlatform() {
24
- switch (platform()) {
25
- case "win32":
26
- return 'win';
27
- case "darwin":
28
- return 'macos';
29
- case 'linux':
30
- return 'linux';
31
- default:
32
- throw 'unsupported platform';
33
- }
34
- }
35
28
 
36
- function getArch() {
37
- switch (arch()) {
38
- case "arm64":
39
- return 'arm64';
40
- case "x64":
41
- return 'x64';
42
- default:
43
- throw 'unsupported arch'
44
- }
45
- }
46
-
47
-
48
- const binExt = OS === 'win' ? '.exe' : '';
29
+ // const binExt = OS === 'win' ? '.exe' : '';
49
30
 
50
31
  // this when run as an npm script this will be where the folder where package.json is.
51
32
  const projectRoot = process.cwd();
52
- const configName = "electrobun.config";
53
- const configPath = join(projectRoot, configName);
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
+ }
54
39
 
55
40
  // Note: cli args can be called via npm bun /path/to/electorbun/binary arg1 arg2
56
41
  const indexOfElectrobun = process.argv.findIndex((arg) =>
57
42
  arg.includes("electrobun")
58
43
  );
59
- const commandArg = process.argv[indexOfElectrobun + 1] || "launcher";
44
+ const commandArg = process.argv[indexOfElectrobun + 1] || "build";
60
45
 
61
46
  const ELECTROBUN_DEP_PATH = join(projectRoot, "node_modules", "electrobun");
62
47
 
63
48
  // When debugging electrobun with the example app use the builds (dev or release) right from the source folder
64
49
  // For developers using electrobun cli via npm use the release versions in /dist
65
50
  // This lets us not have to commit src build folders to git and provide pre-built binaries
66
- const PATHS = {
67
- BUN_BINARY: join(ELECTROBUN_DEP_PATH, "dist", "bun") + binExt,
68
- LAUNCHER_DEV: join(ELECTROBUN_DEP_PATH, "dist", "electrobun") + binExt,
69
- LAUNCHER_RELEASE: join(ELECTROBUN_DEP_PATH, "dist", "launcher") + binExt,
70
- MAIN_JS: join(ELECTROBUN_DEP_PATH, "dist", "main.js"),
71
- NATIVE_WRAPPER_MACOS: join(
72
- ELECTROBUN_DEP_PATH,
73
- "dist",
74
- "libNativeWrapper.dylib"
75
- ),
76
- NATIVE_WRAPPER_WIN: join(ELECTROBUN_DEP_PATH, "dist", "libNativeWrapper.dll"),
77
- NATIVE_WRAPPER_LINUX: join(ELECTROBUN_DEP_PATH, "dist", "libNativeWrapper.so"),
78
- WEBVIEW2LOADER_WIN: join(ELECTROBUN_DEP_PATH, "dist", "WebView2Loader.dll"),
79
- BSPATCH: join(ELECTROBUN_DEP_PATH, "dist", "bspatch") + binExt,
80
- EXTRACTOR: join(ELECTROBUN_DEP_PATH, "dist", "extractor") + binExt,
81
- BSDIFF: join(ELECTROBUN_DEP_PATH, "dist", "bsdiff") + binExt,
82
- CEF_FRAMEWORK_MACOS: join(
83
- ELECTROBUN_DEP_PATH,
84
- "dist",
85
- "cef",
86
- "Chromium Embedded Framework.framework"
87
- ),
88
- CEF_HELPER_MACOS: join(ELECTROBUN_DEP_PATH, "dist", "cef", "process_helper"),
89
- CEF_HELPER_WIN: join(ELECTROBUN_DEP_PATH, "dist", "cef", "process_helper.exe"),
90
- CEF_HELPER_LINUX: join(ELECTROBUN_DEP_PATH, "dist", "cef", "process_helper"),
91
- CEF_DIR: join(ELECTROBUN_DEP_PATH, "dist", "cef"),
92
- };
93
51
 
94
- async function ensureCoreDependencies() {
95
- // Check if core dependencies already exist
96
- if (existsSync(PATHS.BUN_BINARY) && existsSync(PATHS.LAUNCHER_RELEASE)) {
52
+ // Function to get platform-specific paths
53
+ function getPlatformPaths(targetOS: 'macos' | 'win' | 'linux', targetArch: 'arm64' | 'x64') {
54
+ const binExt = targetOS === 'win' ? '.exe' : '';
55
+ const platformDistDir = join(ELECTROBUN_DEP_PATH, `dist-${targetOS}-${targetArch}`);
56
+ const sharedDistDir = join(ELECTROBUN_DEP_PATH, "dist");
57
+
58
+ return {
59
+ // Platform-specific binaries (from dist-OS-ARCH/)
60
+ BUN_BINARY: join(platformDistDir, "bun") + binExt,
61
+ LAUNCHER_DEV: join(platformDistDir, "electrobun") + binExt,
62
+ LAUNCHER_RELEASE: join(platformDistDir, "launcher") + binExt,
63
+ NATIVE_WRAPPER_MACOS: join(platformDistDir, "libNativeWrapper.dylib"),
64
+ NATIVE_WRAPPER_WIN: join(platformDistDir, "libNativeWrapper.dll"),
65
+ NATIVE_WRAPPER_LINUX: join(platformDistDir, "libNativeWrapper.so"),
66
+ NATIVE_WRAPPER_LINUX_CEF: join(platformDistDir, "libNativeWrapper_cef.so"),
67
+ WEBVIEW2LOADER_WIN: join(platformDistDir, "WebView2Loader.dll"),
68
+ BSPATCH: join(platformDistDir, "bspatch") + binExt,
69
+ EXTRACTOR: join(platformDistDir, "extractor") + binExt,
70
+ BSDIFF: join(platformDistDir, "bsdiff") + binExt,
71
+ CEF_FRAMEWORK_MACOS: join(platformDistDir, "cef", "Chromium Embedded Framework.framework"),
72
+ CEF_HELPER_MACOS: join(platformDistDir, "cef", "process_helper"),
73
+ CEF_HELPER_WIN: join(platformDistDir, "cef", "process_helper.exe"),
74
+ CEF_HELPER_LINUX: join(platformDistDir, "cef", "process_helper"),
75
+ CEF_DIR: join(platformDistDir, "cef"),
76
+
77
+ // Shared platform-independent files (from dist/)
78
+ // These work with existing package.json and development workflow
79
+ MAIN_JS: join(sharedDistDir, "main.js"),
80
+ API_DIR: join(sharedDistDir, "api"),
81
+ };
82
+ }
83
+
84
+ // Default PATHS for host platform (backward compatibility)
85
+ const PATHS = getPlatformPaths(OS, ARCH);
86
+
87
+ async function ensureCoreDependencies(targetOS?: 'macos' | 'win' | 'linux', targetArch?: 'arm64' | 'x64') {
88
+ // Use provided target platform or default to host platform
89
+ const platformOS = targetOS || OS;
90
+ const platformArch = targetArch || ARCH;
91
+
92
+ // Get platform-specific paths
93
+ const platformPaths = getPlatformPaths(platformOS, platformArch);
94
+
95
+ // Check platform-specific binaries
96
+ const requiredBinaries = [
97
+ platformPaths.BUN_BINARY,
98
+ platformPaths.LAUNCHER_RELEASE,
99
+ // Platform-specific native wrapper
100
+ platformOS === 'macos' ? platformPaths.NATIVE_WRAPPER_MACOS :
101
+ platformOS === 'win' ? platformPaths.NATIVE_WRAPPER_WIN :
102
+ platformPaths.NATIVE_WRAPPER_LINUX
103
+ ];
104
+
105
+ // Check shared files (main.js should be in shared dist/)
106
+ const requiredSharedFiles = [
107
+ platformPaths.MAIN_JS
108
+ ];
109
+
110
+ const missingBinaries = requiredBinaries.filter(file => !existsSync(file));
111
+ const missingSharedFiles = requiredSharedFiles.filter(file => !existsSync(file));
112
+
113
+ // If only shared files are missing, that's expected in production (they come via npm)
114
+ if (missingBinaries.length === 0 && missingSharedFiles.length > 0) {
115
+ console.log(`Shared files missing (expected in production): ${missingSharedFiles.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', ')}`);
116
+ }
117
+
118
+ // Only download if platform-specific binaries are missing
119
+ if (missingBinaries.length === 0) {
97
120
  return;
98
121
  }
99
122
 
100
- console.log('Core dependencies not found, downloading...');
123
+ // Show which binaries are missing
124
+ console.log(`Core dependencies not found for ${platformOS}-${platformArch}. Missing files:`, missingBinaries.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', '));
125
+ console.log(`Downloading core binaries for ${platformOS}-${platformArch}...`);
101
126
 
102
127
  // Get the current Electrobun version from package.json
103
128
  const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
@@ -112,61 +137,157 @@ async function ensureCoreDependencies() {
112
137
  }
113
138
  }
114
139
 
115
- const platformName = OS === 'macos' ? 'darwin' : OS === 'win' ? 'win32' : 'linux';
116
- const archName = ARCH;
117
- const mainTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-${platformName}-${archName}.tar.gz`;
140
+ const platformName = platformOS === 'macos' ? 'darwin' : platformOS === 'win' ? 'win' : 'linux';
141
+ const archName = platformArch;
142
+ const coreTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-core-${platformName}-${archName}.tar.gz`;
118
143
 
119
- console.log(`Downloading core binaries from: ${mainTarballUrl}`);
144
+ console.log(`Downloading core binaries from: ${coreTarballUrl}`);
120
145
 
121
146
  try {
122
- // Download main tarball
123
- const response = await fetch(mainTarballUrl);
147
+ // Download core binaries tarball
148
+ const response = await fetch(coreTarballUrl);
124
149
  if (!response.ok) {
125
150
  throw new Error(`Failed to download binaries: ${response.status} ${response.statusText}`);
126
151
  }
127
152
 
128
153
  // Create temp file
129
- const tempFile = join(ELECTROBUN_DEP_PATH, 'main-temp.tar.gz');
154
+ const tempFile = join(ELECTROBUN_DEP_PATH, `core-${platformOS}-${platformArch}-temp.tar.gz`);
130
155
  const fileStream = createWriteStream(tempFile);
131
156
 
132
157
  // Write response to file
133
158
  if (response.body) {
134
159
  const reader = response.body.getReader();
160
+ let totalBytes = 0;
135
161
  while (true) {
136
162
  const { done, value } = await reader.read();
137
163
  if (done) break;
138
- fileStream.write(Buffer.from(value));
164
+ const buffer = Buffer.from(value);
165
+ fileStream.write(buffer);
166
+ totalBytes += buffer.length;
139
167
  }
168
+ console.log(`Downloaded ${totalBytes} bytes for ${platformOS}-${platformArch}`);
140
169
  }
141
- fileStream.end();
142
170
 
143
- // Extract to dist directory
144
- console.log('Extracting core dependencies...');
145
- await tar.x({
146
- file: tempFile,
147
- cwd: join(ELECTROBUN_DEP_PATH, 'dist'),
171
+ // Ensure file is properly closed before proceeding
172
+ await new Promise((resolve, reject) => {
173
+ fileStream.end((err) => {
174
+ if (err) reject(err);
175
+ else resolve(null);
176
+ });
148
177
  });
149
178
 
179
+ // Verify the downloaded file exists and has content
180
+ if (!existsSync(tempFile)) {
181
+ throw new Error(`Downloaded file not found: ${tempFile}`);
182
+ }
183
+
184
+ const fileSize = require('fs').statSync(tempFile).size;
185
+ if (fileSize === 0) {
186
+ throw new Error(`Downloaded file is empty: ${tempFile}`);
187
+ }
188
+
189
+ console.log(`Verified download: ${tempFile} (${fileSize} bytes)`);
190
+
191
+ // Extract to platform-specific dist directory
192
+ console.log(`Extracting core dependencies for ${platformOS}-${platformArch}...`);
193
+ const platformDistPath = join(ELECTROBUN_DEP_PATH, `dist-${platformOS}-${platformArch}`);
194
+ mkdirSync(platformDistPath, { recursive: true });
195
+
196
+ // Use Windows native tar.exe on Windows due to npm tar library issues
197
+ if (OS === 'win') {
198
+ console.log('Using Windows native tar.exe for reliable extraction...');
199
+ const relativeTempFile = relative(platformDistPath, tempFile);
200
+ execSync(`tar -xf "${relativeTempFile}"`, {
201
+ stdio: 'inherit',
202
+ cwd: platformDistPath
203
+ });
204
+ } else {
205
+ await tar.x({
206
+ file: tempFile,
207
+ cwd: platformDistPath,
208
+ preservePaths: false,
209
+ strip: 0,
210
+ });
211
+ }
212
+
213
+ // NOTE: We no longer copy main.js from platform-specific downloads
214
+ // Platform-specific downloads should only contain native binaries
215
+ // main.js and api/ should be shipped via npm in the shared dist/ folder
216
+
150
217
  // Clean up temp file
151
218
  unlinkSync(tempFile);
152
219
 
153
- console.log('Core dependencies downloaded and cached successfully');
220
+ // Debug: List what was actually extracted
221
+ try {
222
+ const extractedFiles = readdirSync(platformDistPath);
223
+ console.log(`Extracted files to ${platformDistPath}:`, extractedFiles);
224
+
225
+ // Check if files are in subdirectories
226
+ for (const file of extractedFiles) {
227
+ const filePath = join(platformDistPath, file);
228
+ const stat = require('fs').statSync(filePath);
229
+ if (stat.isDirectory()) {
230
+ const subFiles = readdirSync(filePath);
231
+ console.log(` ${file}/: ${subFiles.join(', ')}`);
232
+ }
233
+ }
234
+ } catch (e) {
235
+ console.error('Could not list extracted files:', e);
236
+ }
154
237
 
155
- } catch (error) {
156
- console.error('Failed to download core dependencies:', error.message);
238
+ // Verify extraction completed successfully - check platform-specific binaries only
239
+ const requiredBinaries = [
240
+ platformPaths.BUN_BINARY,
241
+ platformPaths.LAUNCHER_RELEASE,
242
+ platformOS === 'macos' ? platformPaths.NATIVE_WRAPPER_MACOS :
243
+ platformOS === 'win' ? platformPaths.NATIVE_WRAPPER_WIN :
244
+ platformPaths.NATIVE_WRAPPER_LINUX
245
+ ];
246
+
247
+ const missingBinaries = requiredBinaries.filter(file => !existsSync(file));
248
+ if (missingBinaries.length > 0) {
249
+ console.error(`Missing binaries after extraction: ${missingBinaries.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', ')}`);
250
+ console.error('This suggests the tarball structure is different than expected');
251
+ }
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
+
256
+ // For development: if main.js doesn't exist in shared dist/, copy from platform-specific download as fallback
257
+ const sharedDistPath = join(ELECTROBUN_DEP_PATH, 'dist');
258
+ const extractedMainJs = join(platformDistPath, 'main.js');
259
+ const sharedMainJs = join(sharedDistPath, 'main.js');
260
+
261
+ if (existsSync(extractedMainJs) && !existsSync(sharedMainJs)) {
262
+ console.log('Development fallback: copying main.js from platform-specific download to shared dist/');
263
+ mkdirSync(sharedDistPath, { recursive: true });
264
+ cpSync(extractedMainJs, sharedMainJs);
265
+ }
266
+
267
+ console.log(`Core dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
268
+
269
+ } catch (error: any) {
270
+ console.error(`Failed to download core dependencies for ${platformOS}-${platformArch}:`, error.message);
157
271
  console.error('Please ensure you have an internet connection and the release exists.');
158
272
  process.exit(1);
159
273
  }
160
274
  }
161
275
 
162
- async function ensureCEFDependencies() {
276
+ async function ensureCEFDependencies(targetOS?: 'macos' | 'win' | 'linux', targetArch?: 'arm64' | 'x64') {
277
+ // Use provided target platform or default to host platform
278
+ const platformOS = targetOS || OS;
279
+ const platformArch = targetArch || ARCH;
280
+
281
+ // Get platform-specific paths
282
+ const platformPaths = getPlatformPaths(platformOS, platformArch);
283
+
163
284
  // Check if CEF dependencies already exist
164
- if (existsSync(PATHS.CEF_DIR)) {
165
- console.log('CEF dependencies found, using cached version');
285
+ if (existsSync(platformPaths.CEF_DIR)) {
286
+ console.log(`CEF dependencies found for ${platformOS}-${platformArch}, using cached version`);
166
287
  return;
167
288
  }
168
289
 
169
- console.log('CEF dependencies not found, downloading...');
290
+ console.log(`CEF dependencies not found for ${platformOS}-${platformArch}, downloading...`);
170
291
 
171
292
  // Get the current Electrobun version from package.json
172
293
  const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
@@ -181,8 +302,8 @@ async function ensureCEFDependencies() {
181
302
  }
182
303
  }
183
304
 
184
- const platformName = OS === 'macos' ? 'darwin' : OS === 'win' ? 'win32' : 'linux';
185
- const archName = ARCH;
305
+ const platformName = platformOS === 'macos' ? 'darwin' : platformOS === 'win' ? 'win' : 'linux';
306
+ const archName = platformArch;
186
307
  const cefTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-cef-${platformName}-${archName}.tar.gz`;
187
308
 
188
309
  console.log(`Downloading CEF from: ${cefTarballUrl}`);
@@ -195,7 +316,7 @@ async function ensureCEFDependencies() {
195
316
  }
196
317
 
197
318
  // Create temp file
198
- const tempFile = join(ELECTROBUN_DEP_PATH, 'cef-temp.tar.gz');
319
+ const tempFile = join(ELECTROBUN_DEP_PATH, `cef-${platformOS}-${platformArch}-temp.tar.gz`);
199
320
  const fileStream = createWriteStream(tempFile);
200
321
 
201
322
  // Write response to file
@@ -209,20 +330,50 @@ async function ensureCEFDependencies() {
209
330
  }
210
331
  fileStream.end();
211
332
 
212
- // Extract to dist directory
213
- console.log('Extracting CEF dependencies...');
214
- await tar.x({
215
- file: tempFile,
216
- cwd: join(ELECTROBUN_DEP_PATH, 'dist'),
217
- });
333
+ // Extract to platform-specific dist directory
334
+ console.log(`Extracting CEF dependencies for ${platformOS}-${platformArch}...`);
335
+ const platformDistPath = join(ELECTROBUN_DEP_PATH, `dist-${platformOS}-${platformArch}`);
336
+ mkdirSync(platformDistPath, { recursive: true });
337
+
338
+ // Use Windows native tar.exe on Windows due to npm tar library issues
339
+ if (OS === 'win') {
340
+ console.log('Using Windows native tar.exe for reliable extraction...');
341
+ const relativeTempFile = relative(platformDistPath, tempFile);
342
+ execSync(`tar -xf "${relativeTempFile}"`, {
343
+ stdio: 'inherit',
344
+ cwd: platformDistPath
345
+ });
346
+ } else {
347
+ await tar.x({
348
+ file: tempFile,
349
+ cwd: platformDistPath,
350
+ preservePaths: false,
351
+ strip: 0,
352
+ });
353
+ }
218
354
 
219
355
  // Clean up temp file
220
356
  unlinkSync(tempFile);
221
357
 
222
- console.log('CEF dependencies downloaded and cached successfully');
358
+ // Debug: List what was actually extracted for CEF
359
+ try {
360
+ const extractedFiles = readdirSync(platformDistPath);
361
+ console.log(`CEF extracted files to ${platformDistPath}:`, extractedFiles);
362
+
363
+ // Check if CEF directory was created
364
+ const cefDir = join(platformDistPath, 'cef');
365
+ if (existsSync(cefDir)) {
366
+ const cefFiles = readdirSync(cefDir);
367
+ console.log(`CEF directory contents: ${cefFiles.slice(0, 10).join(', ')}${cefFiles.length > 10 ? '...' : ''}`);
368
+ }
369
+ } catch (e) {
370
+ console.error('Could not list CEF extracted files:', e);
371
+ }
372
+
373
+ console.log(`CEF dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
223
374
 
224
- } catch (error) {
225
- console.error('Failed to download CEF dependencies:', error.message);
375
+ } catch (error: any) {
376
+ console.error(`Failed to download CEF dependencies for ${platformOS}-${platformArch}:`, error.message);
226
377
  console.error('Please ensure you have an internet connection and the release exists.');
227
378
  process.exit(1);
228
379
  }
@@ -241,10 +392,6 @@ const commandDefaults = {
241
392
  projectRoot,
242
393
  config: "electrobun.config",
243
394
  },
244
- launcher: {
245
- projectRoot,
246
- config: "electrobun.config",
247
- },
248
395
  };
249
396
 
250
397
  // todo (yoav): add types for config
@@ -257,6 +404,7 @@ const defaultConfig = {
257
404
  build: {
258
405
  buildFolder: "build",
259
406
  artifactFolder: "artifacts",
407
+ targets: undefined, // Will default to current platform if not specified
260
408
  mac: {
261
409
  codesign: false,
262
410
  notarize: false,
@@ -264,9 +412,22 @@ const defaultConfig = {
264
412
  entitlements: {
265
413
  // This entitlement is required for Electrobun apps with a hardened runtime (required for notarization) to run on macos
266
414
  "com.apple.security.cs.allow-jit": true,
415
+ // Required for bun runtime to work with dynamic code execution and JIT compilation when signed
416
+ "com.apple.security.cs.allow-unsigned-executable-memory": true,
417
+ "com.apple.security.cs.disable-library-validation": true,
267
418
  },
268
419
  icons: "icon.iconset",
269
420
  },
421
+ win: {
422
+ bundleCEF: false,
423
+ },
424
+ linux: {
425
+ bundleCEF: false,
426
+ },
427
+ bun: {
428
+ entrypoint: "src/bun/index.ts",
429
+ external: [],
430
+ },
270
431
  },
271
432
  scripts: {
272
433
  postBuild: "",
@@ -283,19 +444,192 @@ if (!command) {
283
444
  process.exit(1);
284
445
  }
285
446
 
286
- const config = getConfig();
447
+ // Main execution function
448
+ async function main() {
449
+ const config = await getConfig();
450
+
451
+ const envArg =
452
+ process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
287
453
 
288
- const envArg =
289
- process.argv.find((arg) => arg.startsWith("env="))?.split("=")[1] || "";
454
+ const targetsArg =
455
+ process.argv.find((arg) => arg.startsWith("--targets="))?.split("=")[1] || "";
290
456
 
291
- const validEnvironments = ["dev", "canary", "stable"];
457
+ const validEnvironments = ["dev", "canary", "stable"];
292
458
 
293
- // todo (yoav): dev, canary, and stable;
294
- const buildEnvironment: "dev" | "canary" | "stable" =
295
- validEnvironments.includes(envArg) ? envArg : "dev";
459
+ // todo (yoav): dev, canary, and stable;
460
+ const buildEnvironment: "dev" | "canary" | "stable" =
461
+ validEnvironments.includes(envArg || "dev") ? (envArg || "dev") : "dev";
462
+
463
+ // Determine build targets
464
+ type BuildTarget = { os: 'macos' | 'win' | 'linux', arch: 'arm64' | 'x64' };
465
+
466
+ function parseBuildTargets(): BuildTarget[] {
467
+ // If explicit targets provided via CLI
468
+ if (targetsArg) {
469
+ if (targetsArg === 'current') {
470
+ return [{ os: OS, arch: ARCH }];
471
+ } else if (targetsArg === 'all') {
472
+ return parseConfigTargets();
473
+ } else {
474
+ // Parse comma-separated targets like "macos-arm64,win-x64"
475
+ return targetsArg.split(',').map(target => {
476
+ const [os, arch] = target.trim().split('-') as [string, string];
477
+ if (!['macos', 'win', 'linux'].includes(os) || !['arm64', 'x64'].includes(arch)) {
478
+ console.error(`Invalid target: ${target}. Format should be: os-arch (e.g., macos-arm64)`);
479
+ process.exit(1);
480
+ }
481
+ return { os, arch } as BuildTarget;
482
+ });
483
+ }
484
+ }
485
+
486
+ // Default behavior: always build for current platform only
487
+ // This ensures predictable, fast builds unless explicitly requesting multi-platform
488
+ return [{ os: OS, arch: ARCH }];
489
+ }
490
+
491
+ function parseConfigTargets(): BuildTarget[] {
492
+ // If config has targets, use them
493
+ if (config.build.targets && config.build.targets.length > 0) {
494
+ return config.build.targets.map(target => {
495
+ if (target === 'current') {
496
+ return { os: OS, arch: ARCH };
497
+ }
498
+ const [os, arch] = target.split('-') as [string, string];
499
+ if (!['macos', 'win', 'linux'].includes(os) || !['arm64', 'x64'].includes(arch)) {
500
+ console.error(`Invalid target in config: ${target}. Format should be: os-arch (e.g., macos-arm64)`);
501
+ process.exit(1);
502
+ }
503
+ return { os, arch } as BuildTarget;
504
+ });
505
+ }
506
+
507
+ // If no config targets and --targets=all, use all available platforms
508
+ if (targetsArg === 'all') {
509
+ console.log('No targets specified in config, using all available platforms');
510
+ return [
511
+ { os: 'macos', arch: 'arm64' },
512
+ { os: 'macos', arch: 'x64' },
513
+ { os: 'win', arch: 'x64' },
514
+ { os: 'linux', arch: 'x64' },
515
+ { os: 'linux', arch: 'arm64' }
516
+ ];
517
+ }
518
+
519
+ // Default to current platform
520
+ return [{ os: OS, arch: ARCH }];
521
+ }
522
+
523
+ const buildTargets = parseBuildTargets();
524
+
525
+ // Show build targets to user
526
+ if (buildTargets.length === 1) {
527
+ console.log(`Building for ${buildTargets[0].os}-${buildTargets[0].arch} (${buildEnvironment})`);
528
+ } else {
529
+ const targetList = buildTargets.map(t => `${t.os}-${t.arch}`).join(', ');
530
+ console.log(`Building for multiple targets: ${targetList} (${buildEnvironment})`);
531
+ console.log(`Running ${buildTargets.length} parallel builds...`);
532
+
533
+ // Spawn parallel build processes
534
+ const buildPromises = buildTargets.map(async (target) => {
535
+ const targetString = `${target.os}-${target.arch}`;
536
+ const prefix = `[${targetString}]`;
537
+
538
+ try {
539
+ // Try to find the electrobun binary in node_modules/.bin or use bunx
540
+ const electrobunBin = join(projectRoot, 'node_modules', '.bin', 'electrobun');
541
+ let command: string[];
542
+
543
+ if (existsSync(electrobunBin)) {
544
+ command = [electrobunBin, 'build', `--env=${buildEnvironment}`, `--targets=${targetString}`];
545
+ } else {
546
+ // Fallback to bunx which should resolve node_modules binaries
547
+ command = ['bunx', 'electrobun', 'build', `--env=${buildEnvironment}`, `--targets=${targetString}`];
548
+ }
549
+
550
+ console.log(`${prefix} Running:`, command.join(' '));
551
+
552
+ const result = await Bun.spawn(command, {
553
+ stdio: ['inherit', 'pipe', 'pipe'],
554
+ env: process.env,
555
+ cwd: projectRoot // Ensure we're in the right directory
556
+ });
557
+
558
+ // Pipe output with prefix
559
+ if (result.stdout) {
560
+ const reader = result.stdout.getReader();
561
+ while (true) {
562
+ const { done, value } = await reader.read();
563
+ if (done) break;
564
+ const text = new TextDecoder().decode(value);
565
+ // Add prefix to each line
566
+ const prefixedText = text.split('\n').map(line =>
567
+ line ? `${prefix} ${line}` : line
568
+ ).join('\n');
569
+ process.stdout.write(prefixedText);
570
+ }
571
+ }
572
+
573
+ if (result.stderr) {
574
+ const reader = result.stderr.getReader();
575
+ while (true) {
576
+ const { done, value } = await reader.read();
577
+ if (done) break;
578
+ const text = new TextDecoder().decode(value);
579
+ const prefixedText = text.split('\n').map(line =>
580
+ line ? `${prefix} ${line}` : line
581
+ ).join('\n');
582
+ process.stderr.write(prefixedText);
583
+ }
584
+ }
585
+
586
+ const exitCode = await result.exited;
587
+ return { target, exitCode, success: exitCode === 0 };
588
+
589
+ } catch (error) {
590
+ console.error(`${prefix} Failed to start build:`, error);
591
+ return { target, exitCode: 1, success: false, error };
592
+ }
593
+ });
594
+
595
+ // Wait for all builds to complete
596
+ const results = await Promise.allSettled(buildPromises);
597
+
598
+ // Report final results
599
+ console.log('\n=== Build Results ===');
600
+ let allSucceeded = true;
601
+
602
+ for (const result of results) {
603
+ if (result.status === 'fulfilled') {
604
+ const { target, success, exitCode } = result.value;
605
+ const status = success ? '✅ SUCCESS' : '❌ FAILED';
606
+ console.log(`${target.os}-${target.arch}: ${status} (exit code: ${exitCode})`);
607
+ if (!success) allSucceeded = false;
608
+ } else {
609
+ console.log(`Build rejected: ${result.reason}`);
610
+ allSucceeded = false;
611
+ }
612
+ }
613
+
614
+ if (!allSucceeded) {
615
+ console.log('\nSome builds failed. Check the output above for details.');
616
+ process.exit(1);
617
+ } else {
618
+ console.log('\nAll builds completed successfully! 🎉');
619
+ }
620
+
621
+ process.exit(0);
622
+ }
296
623
 
297
624
  // todo (yoav): dev builds should include the branch name, and/or allow configuration via external config
298
- const buildSubFolder = `${buildEnvironment}`;
625
+ // For now, assume single target build (we'll refactor for multi-target later)
626
+ const currentTarget = buildTargets[0];
627
+ const buildSubFolder = `${buildEnvironment}-${currentTarget.os}-${currentTarget.arch}`;
628
+
629
+ // Use target OS/ARCH for build logic (instead of current machine's OS/ARCH)
630
+ const targetOS = currentTarget.os;
631
+ const targetARCH = currentTarget.arch;
632
+ const targetBinExt = targetOS === 'win' ? '.exe' : '';
299
633
 
300
634
  const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
301
635
 
@@ -359,6 +693,13 @@ function escapePathForTerminal(filePath: string) {
359
693
 
360
694
  return escapedPath;
361
695
  }
696
+
697
+ function sanitizeVolumeNameForHdiutil(volumeName: string) {
698
+ // Remove or replace characters that cause issues with hdiutil volume mounting
699
+ // Parentheses and other special characters can cause "Operation not permitted" errors
700
+ return volumeName.replace(/[()]/g, '');
701
+ }
702
+
362
703
  // MyApp
363
704
 
364
705
  // const appName = config.app.name.replace(/\s/g, '-').toLowerCase();
@@ -370,16 +711,131 @@ const appFileName = (
370
711
  )
371
712
  .replace(/\s/g, "")
372
713
  .replace(/\./g, "-");
373
- const bundleFileName = OS === 'macos' ? `${appFileName}.app` : appFileName;
714
+ const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName;
374
715
 
375
716
  // const logPath = `/Library/Logs/Electrobun/ExampleApp/dev/out.log`;
376
717
 
377
718
  let proc = null;
378
719
 
379
720
  if (commandArg === "init") {
380
- // todo (yoav): init a repo folder structure
381
- console.log("initializing electrobun project");
721
+ await (async () => {
722
+ const secondArg = process.argv[indexOfElectrobun + 2];
723
+ const availableTemplates = getTemplateNames();
724
+
725
+ let projectName: string;
726
+ let templateName: string;
727
+
728
+ // Check if --template= flag is used
729
+ const templateFlag = process.argv.find(arg => arg.startsWith("--template="));
730
+ if (templateFlag) {
731
+ // Traditional usage: electrobun init my-project --template=photo-booth
732
+ projectName = secondArg || "my-electrobun-app";
733
+ templateName = templateFlag.split("=")[1];
734
+ } else if (secondArg && availableTemplates.includes(secondArg)) {
735
+ // New intuitive usage: electrobun init photo-booth
736
+ projectName = secondArg; // Use template name as project name
737
+ templateName = secondArg;
738
+ } else {
739
+ // Interactive menu when no template specified
740
+ console.log("🚀 Welcome to Electrobun!");
741
+ console.log("");
742
+ console.log("Available templates:");
743
+ availableTemplates.forEach((template, index) => {
744
+ console.log(` ${index + 1}. ${template}`);
745
+ });
746
+ console.log("");
747
+
748
+ // Simple CLI selection using readline
749
+ const rl = readline.createInterface({
750
+ input: process.stdin,
751
+ output: process.stdout
752
+ });
753
+
754
+ const choice = await new Promise<string>((resolve) => {
755
+ rl.question('Select a template (enter number): ', (answer) => {
756
+ rl.close();
757
+ resolve(answer.trim());
758
+ });
759
+ });
760
+
761
+ const templateIndex = parseInt(choice) - 1;
762
+ if (templateIndex < 0 || templateIndex >= availableTemplates.length) {
763
+ console.error(`❌ Invalid selection. Please enter a number between 1 and ${availableTemplates.length}.`);
764
+ process.exit(1);
765
+ }
766
+
767
+ templateName = availableTemplates[templateIndex];
768
+
769
+ // Ask for project name
770
+ const rl2 = readline.createInterface({
771
+ input: process.stdin,
772
+ output: process.stdout
773
+ });
774
+
775
+ projectName = await new Promise<string>((resolve) => {
776
+ rl2.question(`Enter project name (default: my-${templateName}-app): `, (answer) => {
777
+ rl2.close();
778
+ resolve(answer.trim() || `my-${templateName}-app`);
779
+ });
780
+ });
781
+ }
782
+
783
+ console.log(`🚀 Initializing Electrobun project: ${projectName}`);
784
+ console.log(`📋 Using template: ${templateName}`);
785
+
786
+ // Validate template name
787
+ if (!availableTemplates.includes(templateName)) {
788
+ console.error(`❌ Template "${templateName}" not found.`);
789
+ console.log(`Available templates: ${availableTemplates.join(", ")}`);
790
+ process.exit(1);
791
+ }
792
+
793
+ const template = getTemplate(templateName);
794
+ if (!template) {
795
+ console.error(`❌ Could not load template "${templateName}"`);
796
+ process.exit(1);
797
+ }
798
+
799
+ // Create project directory
800
+ const projectPath = join(process.cwd(), projectName);
801
+ if (existsSync(projectPath)) {
802
+ console.error(`❌ Directory "${projectName}" already exists.`);
803
+ process.exit(1);
804
+ }
805
+
806
+ mkdirSync(projectPath, { recursive: true });
807
+
808
+ // Extract template files
809
+ let fileCount = 0;
810
+ for (const [relativePath, content] of Object.entries(template.files)) {
811
+ const fullPath = join(projectPath, relativePath);
812
+ const dir = dirname(fullPath);
813
+
814
+ // Create directory if it doesn't exist
815
+ mkdirSync(dir, { recursive: true });
816
+
817
+ // Write file
818
+ writeFileSync(fullPath, content, 'utf-8');
819
+ fileCount++;
820
+ }
821
+
822
+ console.log(`✅ Created ${fileCount} files from "${templateName}" template`);
823
+ console.log(`📁 Project created at: ${projectPath}`);
824
+ console.log("");
825
+ console.log("📦 Next steps:");
826
+ console.log(` cd ${projectName}`);
827
+ console.log(" bun install");
828
+ console.log(" bun start");
829
+ console.log("");
830
+ console.log("🎉 Happy building with Electrobun!");
831
+ })();
382
832
  } else if (commandArg === "build") {
833
+ // Ensure core binaries are available for the target platform before starting build
834
+ await ensureCoreDependencies(currentTarget.os, currentTarget.arch);
835
+
836
+ // Get platform-specific paths for the current target
837
+ const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);
838
+
383
839
  // refresh build folder
384
840
  if (existsSync(buildFolder)) {
385
841
  rmdirSync(buildFolder, { recursive: true });
@@ -403,7 +859,7 @@ if (commandArg === "init") {
403
859
  appBundleMacOSPath,
404
860
  appBundleFolderResourcesPath,
405
861
  appBundleFolderFrameworksPath,
406
- } = createAppBundle(appFileName, buildFolder);
862
+ } = createAppBundle(appFileName, buildFolder, targetOS);
407
863
 
408
864
  const appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
409
865
 
@@ -473,13 +929,9 @@ if (commandArg === "init") {
473
929
  // mkdirSync(destLauncherFolder, {recursive: true});
474
930
  // }
475
931
  // cpSync(zigLauncherBinarySource, zigLauncherDestination, {recursive: true, dereference: true});
476
- const bunCliLauncherBinarySource =
477
- buildEnvironment === "dev"
478
- ? // Note: in dev use the cli as the launcher
479
- PATHS.LAUNCHER_DEV
480
- : // Note: for release use the zig launcher optimized for smol size
481
- PATHS.LAUNCHER_RELEASE;
482
- const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + binExt;
932
+ // Copy zig launcher for all builds (dev, canary, stable)
933
+ const bunCliLauncherBinarySource = targetPaths.LAUNCHER_RELEASE;
934
+ const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + targetBinExt;
483
935
  const destLauncherFolder = dirname(bunCliLauncherDestination);
484
936
  if (!existsSync(destLauncherFolder)) {
485
937
  // console.info('creating folder: ', destFolder);
@@ -491,14 +943,14 @@ if (commandArg === "init") {
491
943
  dereference: true,
492
944
  });
493
945
 
494
- cpSync(PATHS.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
946
+ cpSync(targetPaths.MAIN_JS, join(appBundleFolderResourcesPath, 'main.js'));
495
947
 
496
948
  // Bun runtime binary
497
949
  // todo (yoav): this only works for the current architecture
498
- const bunBinarySourcePath = PATHS.BUN_BINARY;
950
+ const bunBinarySourcePath = targetPaths.BUN_BINARY;
499
951
  // Note: .bin/bun binary in node_modules is a symlink to the versioned one in another place
500
952
  // in node_modules, so we have to dereference here to get the actual binary in the bundle.
501
- const bunBinaryDestInBundlePath = join(appBundleMacOSPath, "bun") + binExt;
953
+ const bunBinaryDestInBundlePath = join(appBundleMacOSPath, "bun") + targetBinExt;
502
954
  const destFolder2 = dirname(bunBinaryDestInBundlePath);
503
955
  if (!existsSync(destFolder2)) {
504
956
  // console.info('creating folder: ', destFolder);
@@ -507,8 +959,8 @@ if (commandArg === "init") {
507
959
  cpSync(bunBinarySourcePath, bunBinaryDestInBundlePath, { dereference: true });
508
960
 
509
961
  // copy native wrapper dynamic library
510
- if (OS === 'macos') {
511
- const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_MACOS;
962
+ if (targetOS === 'macos') {
963
+ const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_MACOS;
512
964
  const nativeWrapperMacosDestination = join(
513
965
  appBundleMacOSPath,
514
966
  "libNativeWrapper.dylib"
@@ -516,8 +968,8 @@ if (commandArg === "init") {
516
968
  cpSync(nativeWrapperMacosSource, nativeWrapperMacosDestination, {
517
969
  dereference: true,
518
970
  });
519
- } else if (OS === 'win') {
520
- const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_WIN;
971
+ } else if (targetOS === 'win') {
972
+ const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_WIN;
521
973
  const nativeWrapperMacosDestination = join(
522
974
  appBundleMacOSPath,
523
975
  "libNativeWrapper.dll"
@@ -526,7 +978,7 @@ if (commandArg === "init") {
526
978
  dereference: true,
527
979
  });
528
980
 
529
- const webview2LibSource = PATHS.WEBVIEW2LOADER_WIN;
981
+ const webview2LibSource = targetPaths.WEBVIEW2LOADER_WIN;
530
982
  const webview2LibDestination = join(
531
983
  appBundleMacOSPath,
532
984
  "WebView2Loader.dll"
@@ -534,30 +986,34 @@ if (commandArg === "init") {
534
986
  // copy webview2 system webview library
535
987
  cpSync(webview2LibSource, webview2LibDestination);
536
988
 
537
- } else if (OS === 'linux') {
538
- const nativeWrapperLinuxSource = PATHS.NATIVE_WRAPPER_LINUX;
989
+ } else if (targetOS === 'linux') {
990
+ // Choose the appropriate native wrapper based on bundleCEF setting
991
+ const useCEF = config.build.linux?.bundleCEF;
992
+ const nativeWrapperLinuxSource = useCEF ? targetPaths.NATIVE_WRAPPER_LINUX_CEF : targetPaths.NATIVE_WRAPPER_LINUX;
539
993
  const nativeWrapperLinuxDestination = join(
540
994
  appBundleMacOSPath,
541
995
  "libNativeWrapper.so"
542
996
  );
997
+
543
998
  if (existsSync(nativeWrapperLinuxSource)) {
544
999
  cpSync(nativeWrapperLinuxSource, nativeWrapperLinuxDestination, {
545
1000
  dereference: true,
546
1001
  });
1002
+ console.log(`Using ${useCEF ? 'CEF' : 'GTK'} native wrapper for Linux`);
1003
+ } else {
1004
+ throw new Error(`Native wrapper not found: ${nativeWrapperLinuxSource}`);
547
1005
  }
548
1006
  }
549
1007
 
550
- // Ensure core binaries are available
551
- await ensureCoreDependencies();
552
1008
 
553
1009
  // Download CEF binaries if needed when bundleCEF is enabled
554
- if ((OS === 'macos' && config.build.mac?.bundleCEF) ||
555
- (OS === 'win' && config.build.win?.bundleCEF) ||
556
- (OS === 'linux' && config.build.linux?.bundleCEF)) {
1010
+ if ((targetOS === 'macos' && config.build.mac?.bundleCEF) ||
1011
+ (targetOS === 'win' && config.build.win?.bundleCEF) ||
1012
+ (targetOS === 'linux' && config.build.linux?.bundleCEF)) {
557
1013
 
558
- await ensureCEFDependencies();
559
- if (OS === 'macos') {
560
- const cefFrameworkSource = PATHS.CEF_FRAMEWORK_MACOS;
1014
+ await ensureCEFDependencies(currentTarget.os, currentTarget.arch);
1015
+ if (targetOS === 'macos') {
1016
+ const cefFrameworkSource = targetPaths.CEF_FRAMEWORK_MACOS;
561
1017
  const cefFrameworkDestination = join(
562
1018
  appBundleFolderFrameworksPath,
563
1019
  "Chromium Embedded Framework.framework"
@@ -578,7 +1034,7 @@ if (commandArg === "init") {
578
1034
  "bun Helper (Renderer)",
579
1035
  ];
580
1036
 
581
- const helperSourcePath = PATHS.CEF_HELPER_MACOS;
1037
+ const helperSourcePath = targetPaths.CEF_HELPER_MACOS;
582
1038
  cefHelperNames.forEach((helperName) => {
583
1039
  const destinationPath = join(
584
1040
  appBundleFolderFrameworksPath,
@@ -598,10 +1054,9 @@ if (commandArg === "init") {
598
1054
  dereference: true,
599
1055
  });
600
1056
  });
601
- } else if (OS === 'win') {
602
- // Copy CEF DLLs from dist/cef/ to the main executable directory
603
- const electrobunDistPath = join(ELECTROBUN_DEP_PATH, "dist");
604
- const cefSourcePath = join(electrobunDistPath, "cef");
1057
+ } else if (targetOS === 'win') {
1058
+ // Copy CEF DLLs from platform-specific dist/cef/ to the main executable directory
1059
+ const cefSourcePath = targetPaths.CEF_DIR;
605
1060
  const cefDllFiles = [
606
1061
  'libcef.dll',
607
1062
  'chrome_elf.dll',
@@ -641,7 +1096,7 @@ if (commandArg === "init") {
641
1096
  });
642
1097
 
643
1098
  // Copy CEF resources to MacOS/cef/ subdirectory for other resources like locales
644
- const cefResourcesSource = join(electrobunDistPath, 'cef');
1099
+ const cefResourcesSource = targetPaths.CEF_DIR;
645
1100
  const cefResourcesDestination = join(appBundleMacOSPath, 'cef');
646
1101
 
647
1102
  if (existsSync(cefResourcesSource)) {
@@ -660,7 +1115,7 @@ if (commandArg === "init") {
660
1115
  "bun Helper (Renderer)",
661
1116
  ];
662
1117
 
663
- const helperSourcePath = PATHS.CEF_HELPER_WIN;
1118
+ const helperSourcePath = targetPaths.CEF_HELPER_WIN;
664
1119
  if (existsSync(helperSourcePath)) {
665
1120
  cefHelperNames.forEach((helperName) => {
666
1121
  const destinationPath = join(appBundleMacOSPath, `${helperName}.exe`);
@@ -670,10 +1125,9 @@ if (commandArg === "init") {
670
1125
  } else {
671
1126
  console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
672
1127
  }
673
- } else if (OS === 'linux') {
674
- // Copy CEF shared libraries from dist/cef/ to the main executable directory
675
- const electrobunDistPath = join(ELECTROBUN_DEP_PATH, "dist");
676
- const cefSourcePath = join(electrobunDistPath, "cef");
1128
+ } else if (targetOS === 'linux') {
1129
+ // Copy CEF shared libraries from platform-specific dist/cef/ to the main executable directory
1130
+ const cefSourcePath = targetPaths.CEF_DIR;
677
1131
 
678
1132
  if (existsSync(cefSourcePath)) {
679
1133
  const cefSoFiles = [
@@ -684,11 +1138,13 @@ if (commandArg === "init") {
684
1138
  'libvulkan.so.1'
685
1139
  ];
686
1140
 
1141
+ // Copy CEF .so files to main directory as symlinks to cef/ subdirectory
687
1142
  cefSoFiles.forEach(soFile => {
688
1143
  const sourcePath = join(cefSourcePath, soFile);
689
1144
  const destPath = join(appBundleMacOSPath, soFile);
690
1145
  if (existsSync(sourcePath)) {
691
- cpSync(sourcePath, destPath);
1146
+ // We'll create the actual file in cef/ and symlink from main directory
1147
+ // This will be done after the cef/ directory is populated
692
1148
  }
693
1149
  });
694
1150
 
@@ -750,6 +1206,30 @@ if (commandArg === "init") {
750
1206
  }
751
1207
  });
752
1208
 
1209
+ // Create symlinks from main directory to cef/ subdirectory for .so files
1210
+ console.log('Creating symlinks for CEF libraries...');
1211
+ cefSoFiles.forEach(soFile => {
1212
+ const cefFilePath = join(cefResourcesDestination, soFile);
1213
+ const mainDirPath = join(appBundleMacOSPath, soFile);
1214
+
1215
+ if (existsSync(cefFilePath)) {
1216
+ try {
1217
+ // Remove any existing file/symlink in main directory
1218
+ if (existsSync(mainDirPath)) {
1219
+ rmSync(mainDirPath);
1220
+ }
1221
+ // Create symlink from main directory to cef/ subdirectory
1222
+ symlinkSync(join('cef', soFile), mainDirPath);
1223
+ console.log(`Created symlink for CEF library: ${soFile} -> cef/${soFile}`);
1224
+ } catch (error) {
1225
+ console.log(`WARNING: Failed to create symlink for ${soFile}: ${error}`);
1226
+ // Fallback to copying the file
1227
+ cpSync(cefFilePath, mainDirPath);
1228
+ console.log(`Fallback: Copied CEF library to main directory: ${soFile}`);
1229
+ }
1230
+ }
1231
+ });
1232
+
753
1233
  // Copy CEF helper processes with different names
754
1234
  const cefHelperNames = [
755
1235
  "bun Helper",
@@ -759,12 +1239,12 @@ if (commandArg === "init") {
759
1239
  "bun Helper (Renderer)",
760
1240
  ];
761
1241
 
762
- const helperSourcePath = PATHS.CEF_HELPER_LINUX;
1242
+ const helperSourcePath = targetPaths.CEF_HELPER_LINUX;
763
1243
  if (existsSync(helperSourcePath)) {
764
1244
  cefHelperNames.forEach((helperName) => {
765
1245
  const destinationPath = join(appBundleMacOSPath, helperName);
766
1246
  cpSync(helperSourcePath, destinationPath);
767
- console.log(`Copied CEF helper: ${helperName}`);
1247
+ // console.log(`Copied CEF helper: ${helperName}`);
768
1248
  });
769
1249
  } else {
770
1250
  console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
@@ -775,8 +1255,8 @@ if (commandArg === "init") {
775
1255
 
776
1256
 
777
1257
  // copy native bindings
778
- const bsPatchSource = PATHS.BSPATCH;
779
- const bsPatchDestination = join(appBundleMacOSPath, "bspatch") + binExt;
1258
+ const bsPatchSource = targetPaths.BSPATCH;
1259
+ const bsPatchDestination = join(appBundleMacOSPath, "bspatch") + targetBinExt;
780
1260
  const bsPatchDestFolder = dirname(bsPatchDestination);
781
1261
  if (!existsSync(bsPatchDestFolder)) {
782
1262
  mkdirSync(bsPatchDestFolder, { recursive: true });
@@ -871,14 +1351,37 @@ if (commandArg === "init") {
871
1351
 
872
1352
  // Run postBuild script
873
1353
  if (config.scripts.postBuild) {
874
-
875
- Bun.spawnSync([bunBinarySourcePath, config.scripts.postBuild], {
1354
+ console.log("Running postBuild script:", config.scripts.postBuild);
1355
+ // Use host platform's bun binary for running scripts, not target platform's
1356
+ const hostPaths = getPlatformPaths(OS, ARCH);
1357
+
1358
+ const result = Bun.spawnSync([hostPaths.BUN_BINARY, config.scripts.postBuild], {
876
1359
  stdio: ["ignore", "inherit", "inherit"],
1360
+ cwd: projectRoot, // Add cwd to ensure script runs from project root
877
1361
  env: {
878
1362
  ...process.env,
879
1363
  ELECTROBUN_BUILD_ENV: buildEnvironment,
1364
+ ELECTROBUN_OS: targetOS, // Use target OS for environment variables
1365
+ ELECTROBUN_ARCH: targetARCH, // Use target ARCH for environment variables
1366
+ ELECTROBUN_BUILD_DIR: buildFolder,
1367
+ ELECTROBUN_APP_NAME: appFileName,
1368
+ ELECTROBUN_APP_VERSION: config.app.version,
1369
+ ELECTROBUN_APP_IDENTIFIER: config.app.identifier,
1370
+ ELECTROBUN_ARTIFACT_DIR: artifactFolder,
880
1371
  },
881
1372
  });
1373
+
1374
+ if (result.exitCode !== 0) {
1375
+ console.error("postBuild script failed with exit code:", result.exitCode);
1376
+ if (result.stderr) {
1377
+ console.error("stderr:", result.stderr.toString());
1378
+ }
1379
+ // Also log which bun binary we're trying to use
1380
+ console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);
1381
+ console.error("Script path:", config.scripts.postBuild);
1382
+ console.error("Working directory:", projectRoot);
1383
+ process.exit(1);
1384
+ }
882
1385
  }
883
1386
  // All the unique files are in the bundle now. Create an initial temporary tar file
884
1387
  // for hashing the contents
@@ -920,8 +1423,9 @@ if (commandArg === "init") {
920
1423
  );
921
1424
 
922
1425
  // todo (yoav): add these to config
1426
+ // Only codesign/notarize when building macOS targets on macOS host
923
1427
  const shouldCodesign =
924
- buildEnvironment !== "dev" && config.build.mac.codesign;
1428
+ buildEnvironment !== "dev" && targetOS === 'macos' && OS === 'macos' && config.build.mac.codesign;
925
1429
  const shouldNotarize = shouldCodesign && config.build.mac.notarize;
926
1430
 
927
1431
  if (shouldCodesign) {
@@ -956,6 +1460,8 @@ if (commandArg === "init") {
956
1460
  // 6.5. code sign and notarize the dmg
957
1461
  // 7. copy artifacts to directory [self-extractor dmg, zstd app bundle, bsdiff patch, update.json]
958
1462
 
1463
+ // Platform suffix is only used for folder names, not file names
1464
+ const platformSuffix = `-${targetOS}-${targetARCH}`;
959
1465
  const tarPath = `${appBundleFolderPath}.tar`;
960
1466
 
961
1467
  // tar the signed and notarized app bundle
@@ -978,7 +1484,7 @@ if (commandArg === "init") {
978
1484
  // zstd is the clear winner here. dev iteration speed gain of 1min 15s per build is much more valubale
979
1485
  // than saving 1 more MB of space/bandwidth.
980
1486
 
981
- const compressedTarPath = `${tarPath}.zst`;
1487
+ let compressedTarPath = `${tarPath}.zst`;
982
1488
  artifactsToUpload.push(compressedTarPath);
983
1489
 
984
1490
  // zstd compress tarball
@@ -988,19 +1494,21 @@ if (commandArg === "init") {
988
1494
  await ZstdInit().then(async ({ ZstdSimple, ZstdStream }) => {
989
1495
  // Note: Simple is much faster than stream, but stream is better for large files
990
1496
  // todo (yoav): consider a file size cutoff to switch to stream instead of simple.
1497
+ const useStream = tarball.size > 100 * 1024 * 1024;
1498
+
991
1499
  if (tarball.size > 0) {
992
1500
  // Uint8 array filestream of the tar file
993
-
994
1501
  const data = new Uint8Array(tarBuffer);
995
- const compressionLevel = 22;
1502
+
1503
+ const compressionLevel = 22; // Maximum compression - now safe with stripped CEF libraries
996
1504
  const compressedData = ZstdSimple.compress(data, compressionLevel);
997
1505
 
998
1506
  console.log(
999
1507
  "compressed",
1000
- compressedData.length,
1508
+ data.length,
1001
1509
  "bytes",
1002
1510
  "from",
1003
- data.length,
1511
+ tarBuffer.byteLength,
1004
1512
  "bytes"
1005
1513
  );
1006
1514
 
@@ -1012,7 +1520,7 @@ if (commandArg === "init") {
1012
1520
  // now and it needs the same name as the original app bundle.
1013
1521
  rmdirSync(appBundleFolderPath, { recursive: true });
1014
1522
 
1015
- const selfExtractingBundle = createAppBundle(appFileName, buildFolder);
1523
+ const selfExtractingBundle = createAppBundle(appFileName, buildFolder, targetOS);
1016
1524
  const compressedTarballInExtractingBundlePath = join(
1017
1525
  selfExtractingBundle.appBundleFolderResourcesPath,
1018
1526
  `${hash}.tar.zst`
@@ -1021,7 +1529,7 @@ if (commandArg === "init") {
1021
1529
  // copy the zstd tarball to the self-extracting app bundle
1022
1530
  cpSync(compressedTarPath, compressedTarballInExtractingBundlePath);
1023
1531
 
1024
- const selfExtractorBinSourcePath = PATHS.EXTRACTOR;
1532
+ const selfExtractorBinSourcePath = targetPaths.EXTRACTOR;
1025
1533
  const selfExtractorBinDestinationPath = join(
1026
1534
  selfExtractingBundle.appBundleMacOSPath,
1027
1535
  "launcher"
@@ -1031,7 +1539,7 @@ if (commandArg === "init") {
1031
1539
  dereference: true,
1032
1540
  });
1033
1541
 
1034
- buildIcons(appBundleFolderResourcesPath);
1542
+ buildIcons(selfExtractingBundle.appBundleFolderResourcesPath);
1035
1543
  await Bun.write(
1036
1544
  join(selfExtractingBundle.appBundleFolderContentsPath, "Info.plist"),
1037
1545
  InfoPlistContents
@@ -1053,28 +1561,117 @@ if (commandArg === "init") {
1053
1561
  console.log("skipping notarization");
1054
1562
  }
1055
1563
 
1056
- console.log("creating dmg...");
1057
- // make a dmg
1058
- const dmgPath = join(buildFolder, `${appFileName}.dmg`);
1059
- artifactsToUpload.push(dmgPath);
1060
- // hdiutil create -volname "YourAppName" -srcfolder /path/to/YourApp.app -ov -format UDZO YourAppName.dmg
1061
- // Note: use UDBZ for better compression vs. UDZO
1062
- execSync(
1063
- `hdiutil create -volname "${appFileName}" -srcfolder ${escapePathForTerminal(
1064
- appBundleFolderPath
1065
- )} -ov -format UDBZ ${escapePathForTerminal(dmgPath)}`
1066
- );
1564
+ // DMG creation for macOS only
1565
+ if (targetOS === 'macos') {
1566
+ console.log("creating dmg...");
1567
+ // make a dmg
1568
+ const dmgPath = join(buildFolder, `${appFileName}.dmg`);
1569
+ artifactsToUpload.push(dmgPath);
1570
+ // hdiutil create -volname "YourAppName" -srcfolder /path/to/YourApp.app -ov -format UDZO YourAppName.dmg
1571
+ // Note: use ULFO (lzfse) for better compatibility with large CEF frameworks and modern macOS
1572
+ execSync(
1573
+ `hdiutil create -volname "${sanitizeVolumeNameForHdiutil(appFileName)}" -srcfolder ${escapePathForTerminal(
1574
+ selfExtractingBundle.appBundleFolderPath
1575
+ )} -ov -format ULFO ${escapePathForTerminal(dmgPath)}`
1576
+ );
1067
1577
 
1068
- if (shouldCodesign) {
1069
- codesignAppBundle(dmgPath);
1070
- } else {
1071
- console.log("skipping codesign");
1072
- }
1578
+ if (shouldCodesign) {
1579
+ codesignAppBundle(dmgPath);
1580
+ } else {
1581
+ console.log("skipping codesign");
1582
+ }
1073
1583
 
1074
- if (shouldNotarize) {
1075
- notarizeAndStaple(dmgPath);
1584
+ if (shouldNotarize) {
1585
+ notarizeAndStaple(dmgPath);
1586
+ } else {
1587
+ console.log("skipping notarization");
1588
+ }
1076
1589
  } else {
1077
- console.log("skipping notarization");
1590
+ // For Windows and Linux, add the self-extracting bundle directly
1591
+ const platformBundlePath = join(buildFolder, `${appFileName}${platformSuffix}${targetOS === 'win' ? '.exe' : ''}`);
1592
+ // Copy the self-extracting bundle to platform-specific filename
1593
+ if (targetOS === 'win') {
1594
+ // On Windows, create a self-extracting exe
1595
+ const selfExtractingExePath = await createWindowsSelfExtractingExe(
1596
+ buildFolder,
1597
+ compressedTarPath,
1598
+ appFileName,
1599
+ targetPaths,
1600
+ buildEnvironment,
1601
+ hash
1602
+ );
1603
+
1604
+ // Wrap Windows installer files in zip for distribution
1605
+ const wrappedExePath = await wrapWindowsInstallerInZip(selfExtractingExePath, buildFolder);
1606
+ artifactsToUpload.push(wrappedExePath);
1607
+
1608
+ // Also keep the raw exe for backwards compatibility (optional)
1609
+ // artifactsToUpload.push(selfExtractingExePath);
1610
+ } else if (targetOS === 'linux') {
1611
+ // Create desktop file for Linux
1612
+ const desktopFileContent = `[Desktop Entry]
1613
+ Version=1.0
1614
+ Type=Application
1615
+ Name=${config.package?.name || config.app.name}
1616
+ Comment=${config.package?.description || config.app.description || ''}
1617
+ Exec=${appFileName}
1618
+ Icon=${appFileName}
1619
+ Terminal=false
1620
+ StartupWMClass=${appFileName}
1621
+ Categories=Application;
1622
+ `;
1623
+
1624
+ const desktopFilePath = join(appBundleFolderPath, `${appFileName}.desktop`);
1625
+ writeFileSync(desktopFilePath, desktopFileContent);
1626
+
1627
+ // Make desktop file executable
1628
+ execSync(`chmod +x ${escapePathForTerminal(desktopFilePath)}`);
1629
+
1630
+ // Create user-friendly launcher script
1631
+ const launcherScriptContent = `#!/bin/bash
1632
+ # ${config.package?.name || config.app.name} Launcher
1633
+ # This script launches the application from any location
1634
+
1635
+ # Get the directory where this script is located
1636
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
1637
+
1638
+ # Find the launcher binary relative to this script
1639
+ LAUNCHER_BINARY="\$SCRIPT_DIR/bin/launcher"
1640
+
1641
+ if [ ! -x "\$LAUNCHER_BINARY" ]; then
1642
+ echo "Error: Could not find launcher binary at \$LAUNCHER_BINARY"
1643
+ exit 1
1644
+ fi
1645
+
1646
+ # Launch the application
1647
+ exec "\$LAUNCHER_BINARY" "\$@"
1648
+ `;
1649
+
1650
+ const launcherScriptPath = join(appBundleFolderPath, `${appFileName}.sh`);
1651
+ writeFileSync(launcherScriptPath, launcherScriptContent);
1652
+ execSync(`chmod +x ${escapePathForTerminal(launcherScriptPath)}`);
1653
+
1654
+ // Create self-extracting Linux binary (similar to Windows approach)
1655
+ const selfExtractingLinuxPath = await createLinuxSelfExtractingBinary(
1656
+ buildFolder,
1657
+ compressedTarPath,
1658
+ appFileName,
1659
+ targetPaths,
1660
+ buildEnvironment
1661
+ );
1662
+
1663
+ // Wrap Linux .run file in tar.gz to preserve permissions
1664
+ const wrappedRunPath = await wrapInArchive(selfExtractingLinuxPath, buildFolder, 'tar.gz');
1665
+ artifactsToUpload.push(wrappedRunPath);
1666
+
1667
+ // Also keep the raw .run for backwards compatibility (optional)
1668
+ // artifactsToUpload.push(selfExtractingLinuxPath);
1669
+
1670
+ // On Linux, create a tar.gz of the bundle
1671
+ const linuxTarPath = join(buildFolder, `${appFileName}.tar.gz`);
1672
+ execSync(`tar -czf ${escapePathForTerminal(linuxTarPath)} -C ${escapePathForTerminal(buildFolder)} ${escapePathForTerminal(basename(appBundleFolderPath))}`);
1673
+ artifactsToUpload.push(linuxTarPath);
1674
+ }
1078
1675
  }
1079
1676
 
1080
1677
  // refresh artifacts folder
@@ -1093,39 +1690,48 @@ if (commandArg === "init") {
1093
1690
  // the download button or display on your marketing site or in the app.
1094
1691
  version: config.app.version,
1095
1692
  hash: hash.toString(),
1693
+ platform: OS,
1694
+ arch: ARCH,
1096
1695
  // channel: buildEnvironment,
1097
1696
  // bucketUrl: config.release.bucketUrl
1098
1697
  });
1099
1698
 
1100
- await Bun.write(join(artifactFolder, "update.json"), updateJsonContent);
1699
+ // update.json (no platform suffix in filename, platform is in folder name)
1700
+ await Bun.write(join(artifactFolder, 'update.json'), updateJsonContent);
1101
1701
 
1102
1702
  // generate bsdiff
1103
1703
  // https://storage.googleapis.com/eggbun-static/electrobun-playground/canary/ElectrobunPlayground-canary.app.tar.zst
1104
1704
  console.log("bucketUrl: ", config.release.bucketUrl);
1105
1705
 
1106
1706
  console.log("generating a patch from the previous version...");
1107
- const urlToPrevUpdateJson = join(
1108
- config.release.bucketUrl,
1109
- buildEnvironment,
1110
- `update.json`
1111
- );
1112
- const cacheBuster = Math.random().toString(36).substring(7);
1113
- const updateJsonResponse = await fetch(
1114
- urlToPrevUpdateJson + `?${cacheBuster}`
1115
- ).catch((err) => {
1116
- console.log("bucketURL not found: ", err);
1117
- });
1707
+
1708
+ // Skip patch generation if bucketUrl is not configured
1709
+ if (!config.release.bucketUrl || config.release.bucketUrl.trim() === '') {
1710
+ console.log("No bucketUrl configured, skipping patch generation");
1711
+ console.log("To enable patch generation, configure bucketUrl in your electrobun.config");
1712
+ } else {
1713
+ const urlToPrevUpdateJson = join(
1714
+ config.release.bucketUrl,
1715
+ buildSubFolder,
1716
+ 'update.json'
1717
+ );
1718
+ const cacheBuster = Math.random().toString(36).substring(7);
1719
+ const updateJsonResponse = await fetch(
1720
+ urlToPrevUpdateJson + `?${cacheBuster}`
1721
+ ).catch((err) => {
1722
+ console.log("bucketURL not found: ", err);
1723
+ });
1118
1724
 
1119
1725
  const urlToLatestTarball = join(
1120
1726
  config.release.bucketUrl,
1121
- buildEnvironment,
1727
+ buildSubFolder,
1122
1728
  `${appFileName}.app.tar.zst`
1123
1729
  );
1124
1730
 
1125
1731
 
1126
1732
  // attempt to get the previous version to create a patch file
1127
- if (updateJsonResponse.ok) {
1128
- const prevUpdateJson = await updateJsonResponse.json();
1733
+ if (updateJsonResponse && updateJsonResponse.ok) {
1734
+ const prevUpdateJson = await updateJsonResponse!.json();
1129
1735
 
1130
1736
  const prevHash = prevUpdateJson.hash;
1131
1737
  console.log("PREVIOUS HASH", prevHash);
@@ -1164,7 +1770,7 @@ if (commandArg === "init") {
1164
1770
  console.log("diff previous and new tarballs...");
1165
1771
  // Run it as a separate process to leverage multi-threadedness
1166
1772
  // especially for creating multiple diffs in parallel
1167
- const bsdiffpath = PATHS.BSDIFF;
1773
+ const bsdiffpath = targetPaths.BSDIFF;
1168
1774
  const patchFilePath = join(buildFolder, `${prevHash}.patch`);
1169
1775
  artifactsToUpload.push(patchFilePath);
1170
1776
  const result = Bun.spawnSync(
@@ -1181,6 +1787,7 @@ if (commandArg === "init") {
1181
1787
  console.log("prevoius version not found at: ", urlToLatestTarball);
1182
1788
  console.log("skipping diff generation");
1183
1789
  }
1790
+ } // End of bucketUrl validation block
1184
1791
 
1185
1792
  // compress all the upload files
1186
1793
  console.log("copying artifacts...");
@@ -1209,10 +1816,7 @@ if (commandArg === "init") {
1209
1816
  // todo (yoav): rename to start
1210
1817
 
1211
1818
  // run the project in dev mode
1212
- // this runs the cli in debug mode, on macos executes the app bundle,
1213
- // there is another copy of the cli in the app bundle that will execute the app
1214
- // the two cli processes communicate via named pipes and together manage the dev
1215
- // lifecycle and debug functionality
1819
+ // this runs the bundled bun binary with main.js directly
1216
1820
 
1217
1821
  // Note: this cli will be a bun single-file-executable
1218
1822
  // Note: we want to use the version of bun that's packaged with electrobun
@@ -1231,24 +1835,27 @@ if (commandArg === "init") {
1231
1835
 
1232
1836
  let mainProc;
1233
1837
  let bundleExecPath: string;
1838
+ let bundleResourcesPath: string;
1234
1839
 
1235
1840
  if (OS === 'macos') {
1236
1841
  bundleExecPath = join(buildFolder, bundleFileName, "Contents", 'MacOS');
1842
+ bundleResourcesPath = join(buildFolder, bundleFileName, "Contents", 'Resources');
1237
1843
  } else if (OS === 'linux' || OS === 'win') {
1238
1844
  bundleExecPath = join(buildFolder, bundleFileName, "bin");
1845
+ bundleResourcesPath = join(buildFolder, bundleFileName, "Resources");
1239
1846
  } else {
1240
1847
  throw new Error(`Unsupported OS: ${OS}`);
1241
1848
  }
1242
1849
 
1243
1850
  if (OS === 'macos') {
1244
-
1245
- mainProc = Bun.spawn([join(bundleExecPath,'bun'), join(bundleExecPath, 'main.js')], {
1851
+ // Use the zig launcher for all builds (dev, canary, stable)
1852
+ mainProc = Bun.spawn([join(bundleExecPath, 'launcher')], {
1246
1853
  stdio: ['inherit', 'inherit', 'inherit'],
1247
1854
  cwd: bundleExecPath
1248
1855
  })
1249
1856
  } else if (OS === 'win') {
1250
- // Try the main process
1251
- mainProc = Bun.spawn(['./bun.exe', './main.js'], {
1857
+ // Try the main process - use relative path to Resources folder
1858
+ mainProc = Bun.spawn(['./bun.exe', '../Resources/main.js'], {
1252
1859
  stdio: ['inherit', 'inherit', 'inherit'],
1253
1860
  cwd: bundleExecPath,
1254
1861
  onExit: (proc, exitCode, signalCode, error) => {
@@ -1269,7 +1876,7 @@ if (commandArg === "init") {
1269
1876
  }
1270
1877
  }
1271
1878
 
1272
- mainProc = Bun.spawn([join(bundleExecPath, 'bun'), join(bundleExecPath, 'main.js')], {
1879
+ mainProc = Bun.spawn([join(bundleExecPath, 'bun'), join(bundleResourcesPath, 'main.js')], {
1273
1880
  stdio: ['inherit', 'inherit', 'inherit'],
1274
1881
  cwd: bundleExecPath,
1275
1882
  env
@@ -1277,20 +1884,51 @@ if (commandArg === "init") {
1277
1884
  }
1278
1885
 
1279
1886
  process.on("SIGINT", () => {
1280
- console.log('exit command')
1281
- // toLauncherPipe.write("exit command\n");
1282
- mainProc.kill();
1283
- process.exit();
1887
+ console.log('[electrobun dev] Received SIGINT, initiating graceful shutdown...')
1888
+
1889
+ if (mainProc) {
1890
+ // First attempt graceful shutdown by sending SIGINT to child
1891
+ console.log('[electrobun dev] Requesting graceful shutdown from app...')
1892
+ mainProc.kill("SIGINT");
1893
+
1894
+ // Give the app time to clean up (e.g., call killApp())
1895
+ setTimeout(() => {
1896
+ if (mainProc && !mainProc.killed) {
1897
+ console.log('[electrobun dev] App did not exit gracefully, forcing termination...')
1898
+ mainProc.kill("SIGKILL");
1899
+ }
1900
+ process.exit(0);
1901
+ }, 2000); // 2 second timeout for graceful shutdown
1902
+ } else {
1903
+ process.exit(0);
1904
+ }
1284
1905
  });
1285
1906
 
1286
1907
  }
1287
1908
 
1288
- function getConfig() {
1909
+ async function getConfig() {
1289
1910
  let loadedConfig = {};
1290
- if (existsSync(configPath)) {
1291
- const configFileContents = readFileSync(configPath, "utf8");
1292
- // Note: we want this to hard fail if there's a syntax error
1293
- loadedConfig = JSON.parse(configFileContents);
1911
+ const foundConfigPath = findConfigFile();
1912
+
1913
+ if (foundConfigPath) {
1914
+ console.log(`Using config file: ${basename(foundConfigPath)}`);
1915
+
1916
+ try {
1917
+ // Use dynamic import for TypeScript ESM files
1918
+ // Bun handles TypeScript natively, no transpilation needed
1919
+ const configModule = await import(foundConfigPath);
1920
+ loadedConfig = configModule.default || configModule;
1921
+
1922
+ // Validate that we got a valid config object
1923
+ if (!loadedConfig || typeof loadedConfig !== 'object') {
1924
+ console.error("Config file must export a default object");
1925
+ console.error("using default config instead");
1926
+ loadedConfig = {};
1927
+ }
1928
+ } catch (error) {
1929
+ console.error("Failed to load config file:", error);
1930
+ console.error("using default config instead");
1931
+ }
1294
1932
  }
1295
1933
 
1296
1934
  // todo (yoav): write a deep clone fn
@@ -1312,6 +1950,18 @@ function getConfig() {
1312
1950
  ...(loadedConfig?.build?.mac?.entitlements || {}),
1313
1951
  },
1314
1952
  },
1953
+ win: {
1954
+ ...defaultConfig.build.win,
1955
+ ...(loadedConfig?.build?.win || {}),
1956
+ },
1957
+ linux: {
1958
+ ...defaultConfig.build.linux,
1959
+ ...(loadedConfig?.build?.linux || {}),
1960
+ },
1961
+ bun: {
1962
+ ...defaultConfig.build.bun,
1963
+ ...(loadedConfig?.build?.bun || {}),
1964
+ }
1315
1965
  },
1316
1966
  scripts: {
1317
1967
  ...defaultConfig.scripts,
@@ -1347,6 +1997,303 @@ function getEntitlementValue(value: boolean | string) {
1347
1997
  }
1348
1998
  }
1349
1999
 
2000
+ async function createWindowsSelfExtractingExe(
2001
+ buildFolder: string,
2002
+ compressedTarPath: string,
2003
+ appFileName: string,
2004
+ targetPaths: any,
2005
+ buildEnvironment: string,
2006
+ hash: string
2007
+ ): Promise<string> {
2008
+ console.log("Creating Windows installer with separate archive...");
2009
+
2010
+ // Format: MyApp-Setup.exe (stable) or MyApp-Setup-canary.exe (non-stable)
2011
+ const setupFileName = buildEnvironment === "stable"
2012
+ ? `${config.app.name}-Setup.exe`
2013
+ : `${config.app.name}-Setup-${buildEnvironment}.exe`;
2014
+
2015
+ const outputExePath = join(buildFolder, setupFileName);
2016
+
2017
+ // Copy the extractor exe
2018
+ const extractorExe = readFileSync(targetPaths.EXTRACTOR);
2019
+ writeFileSync(outputExePath, extractorExe);
2020
+
2021
+ // Create metadata JSON file
2022
+ const metadata = {
2023
+ identifier: config.app.identifier,
2024
+ name: config.app.name,
2025
+ channel: buildEnvironment,
2026
+ hash: hash
2027
+ };
2028
+ const metadataJson = JSON.stringify(metadata, null, 2);
2029
+ const metadataFileName = setupFileName.replace('.exe', '.metadata.json');
2030
+ const metadataPath = join(buildFolder, metadataFileName);
2031
+ writeFileSync(metadataPath, metadataJson);
2032
+
2033
+ // Copy the compressed archive with matching name
2034
+ const archiveFileName = setupFileName.replace('.exe', '.tar.zst');
2035
+ const archivePath = join(buildFolder, archiveFileName);
2036
+ copyFileSync(compressedTarPath, archivePath);
2037
+
2038
+ // Make the exe executable (though Windows doesn't need chmod)
2039
+ if (OS !== 'win') {
2040
+ execSync(`chmod +x ${escapePathForTerminal(outputExePath)}`);
2041
+ }
2042
+
2043
+ const exeSize = statSync(outputExePath).size;
2044
+ const archiveSize = statSync(archivePath).size;
2045
+ const totalSize = exeSize + archiveSize;
2046
+
2047
+ console.log(`Created Windows installer:`);
2048
+ console.log(` - Extractor: ${outputExePath} (${(exeSize / 1024 / 1024).toFixed(2)} MB)`);
2049
+ console.log(` - Archive: ${archivePath} (${(archiveSize / 1024 / 1024).toFixed(2)} MB)`);
2050
+ console.log(` - Metadata: ${metadataPath}`);
2051
+ console.log(` - Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
2052
+
2053
+ return outputExePath;
2054
+ }
2055
+
2056
+ async function createLinuxSelfExtractingBinary(
2057
+ buildFolder: string,
2058
+ compressedTarPath: string,
2059
+ appFileName: string,
2060
+ targetPaths: any,
2061
+ buildEnvironment: string
2062
+ ): Promise<string> {
2063
+ console.log("Creating self-extracting Linux binary...");
2064
+
2065
+ // Format: MyApp-Setup.run (stable) or MyApp-Setup-canary.run (non-stable)
2066
+ const setupFileName = buildEnvironment === "stable"
2067
+ ? `${config.app.name}-Setup.run`
2068
+ : `${config.app.name}-Setup-${buildEnvironment}.run`;
2069
+
2070
+ const outputPath = join(buildFolder, setupFileName);
2071
+
2072
+ // Read the extractor binary
2073
+ const extractorBinary = readFileSync(targetPaths.EXTRACTOR);
2074
+
2075
+ // Read the compressed archive
2076
+ const compressedArchive = readFileSync(compressedTarPath);
2077
+
2078
+ // Create metadata JSON
2079
+ const metadata = {
2080
+ identifier: config.app.identifier,
2081
+ name: config.app.name,
2082
+ channel: buildEnvironment
2083
+ };
2084
+ const metadataJson = JSON.stringify(metadata);
2085
+ const metadataBuffer = Buffer.from(metadataJson, 'utf8');
2086
+
2087
+ // Create marker buffers
2088
+ const metadataMarker = Buffer.from('ELECTROBUN_METADATA_V1', 'utf8');
2089
+ const archiveMarker = Buffer.from('ELECTROBUN_ARCHIVE_V1', 'utf8');
2090
+
2091
+ // Combine extractor + metadata marker + metadata + archive marker + archive
2092
+ const combinedBuffer = Buffer.concat([
2093
+ extractorBinary,
2094
+ metadataMarker,
2095
+ metadataBuffer,
2096
+ archiveMarker,
2097
+ compressedArchive
2098
+ ]);
2099
+
2100
+ // Write the self-extracting binary
2101
+ writeFileSync(outputPath, combinedBuffer, { mode: 0o755 });
2102
+
2103
+ // Ensure it's executable (redundant but explicit)
2104
+ execSync(`chmod +x ${escapePathForTerminal(outputPath)}`);
2105
+
2106
+ console.log(`Created self-extracting Linux binary: ${outputPath} (${(combinedBuffer.length / 1024 / 1024).toFixed(2)} MB)`);
2107
+
2108
+ return outputPath;
2109
+ }
2110
+
2111
+ async function wrapWindowsInstallerInZip(exePath: string, buildFolder: string): Promise<string> {
2112
+ const exeName = basename(exePath);
2113
+ const exeStem = exeName.replace('.exe', '');
2114
+
2115
+ // Derive the paths for metadata and archive files
2116
+ const metadataPath = join(buildFolder, `${exeStem}.metadata.json`);
2117
+ const archivePath = join(buildFolder, `${exeStem}.tar.zst`);
2118
+ const zipPath = join(buildFolder, `${exeStem}.zip`);
2119
+
2120
+ // Verify all files exist
2121
+ if (!existsSync(exePath)) {
2122
+ throw new Error(`Installer exe not found: ${exePath}`);
2123
+ }
2124
+ if (!existsSync(metadataPath)) {
2125
+ throw new Error(`Metadata file not found: ${metadataPath}`);
2126
+ }
2127
+ if (!existsSync(archivePath)) {
2128
+ throw new Error(`Archive file not found: ${archivePath}`);
2129
+ }
2130
+
2131
+ // Create zip archive
2132
+ const output = createWriteStream(zipPath);
2133
+ const archive = archiver('zip', {
2134
+ zlib: { level: 9 } // Maximum compression
2135
+ });
2136
+
2137
+ return new Promise((resolve, reject) => {
2138
+ output.on('close', () => {
2139
+ console.log(`Created Windows installer package: ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(2)} MB)`);
2140
+ resolve(zipPath);
2141
+ });
2142
+
2143
+ archive.on('error', (err) => {
2144
+ reject(err);
2145
+ });
2146
+
2147
+ archive.pipe(output);
2148
+
2149
+ // Add all three files to the archive
2150
+ archive.file(exePath, { name: basename(exePath) });
2151
+ archive.file(metadataPath, { name: basename(metadataPath) });
2152
+ archive.file(archivePath, { name: basename(archivePath) });
2153
+
2154
+ archive.finalize();
2155
+ });
2156
+ }
2157
+
2158
+ async function wrapInArchive(filePath: string, buildFolder: string, archiveType: 'tar.gz' | 'zip'): Promise<string> {
2159
+ const fileName = basename(filePath);
2160
+ const fileDir = dirname(filePath);
2161
+
2162
+ if (archiveType === 'tar.gz') {
2163
+ // Output filename: Setup.exe -> Setup.exe.tar.gz or Setup.run -> Setup.run.tar.gz
2164
+ const archivePath = filePath + '.tar.gz';
2165
+
2166
+ // For Linux files, ensure they have executable permissions before archiving
2167
+ if (fileName.endsWith('.run')) {
2168
+ try {
2169
+ // Try to set executable permissions (will only work on Unix-like systems)
2170
+ execSync(`chmod +x ${escapePathForTerminal(filePath)}`, { stdio: 'ignore' });
2171
+ } catch {
2172
+ // Ignore errors on Windows hosts
2173
+ }
2174
+ }
2175
+
2176
+ // Create tar.gz archive preserving permissions
2177
+ // Using the tar package for cross-platform compatibility
2178
+ await tar.c(
2179
+ {
2180
+ gzip: true,
2181
+ file: archivePath,
2182
+ cwd: fileDir,
2183
+ portable: true, // Ensures consistent behavior across platforms
2184
+ preservePaths: false,
2185
+ // The tar package should preserve file modes when creating archives
2186
+ },
2187
+ [fileName]
2188
+ );
2189
+
2190
+ console.log(`Created archive: ${archivePath} (preserving executable permissions)`);
2191
+ return archivePath;
2192
+ } else if (archiveType === 'zip') {
2193
+ // Output filename: Setup.exe -> Setup.zip
2194
+ const archivePath = filePath.replace(/\.[^.]+$/, '.zip');
2195
+
2196
+ // Create zip archive
2197
+ const output = createWriteStream(archivePath);
2198
+ const archive = archiver('zip', {
2199
+ zlib: { level: 9 } // Maximum compression
2200
+ });
2201
+
2202
+ return new Promise((resolve, reject) => {
2203
+ output.on('close', () => {
2204
+ console.log(`Created archive: ${archivePath} (${(archive.pointer() / 1024 / 1024).toFixed(2)} MB)`);
2205
+ resolve(archivePath);
2206
+ });
2207
+
2208
+ archive.on('error', (err) => {
2209
+ reject(err);
2210
+ });
2211
+
2212
+ archive.pipe(output);
2213
+
2214
+ // Add the file to the archive
2215
+ archive.file(filePath, { name: fileName });
2216
+
2217
+ archive.finalize();
2218
+ });
2219
+ }
2220
+ }
2221
+
2222
+ async function createAppImage(buildFolder: string, appBundlePath: string, appFileName: string, config: any): Promise<string | null> {
2223
+ try {
2224
+ console.log("Creating AppImage...");
2225
+
2226
+ // Create AppDir structure
2227
+ const appDirPath = join(buildFolder, `${appFileName}.AppDir`);
2228
+ mkdirSync(appDirPath, { recursive: true });
2229
+
2230
+ // Copy app bundle contents to AppDir
2231
+ const appDirAppPath = join(appDirPath, "app");
2232
+ cpSync(appBundlePath, appDirAppPath, { recursive: true });
2233
+
2234
+ // Create AppRun script (main executable for AppImage)
2235
+ const appRunContent = `#!/bin/bash
2236
+ HERE="$(dirname "$(readlink -f "\${0}")")"
2237
+ export APPDIR="\$HERE"
2238
+ cd "\$HERE"
2239
+ exec "\$HERE/app/bin/launcher" "\$@"
2240
+ `;
2241
+
2242
+ const appRunPath = join(appDirPath, "AppRun");
2243
+ writeFileSync(appRunPath, appRunContent);
2244
+ execSync(`chmod +x ${escapePathForTerminal(appRunPath)}`);
2245
+
2246
+ // Create desktop file in AppDir root
2247
+ const desktopContent = `[Desktop Entry]
2248
+ Version=1.0
2249
+ Type=Application
2250
+ Name=${config.package?.name || config.app.name}
2251
+ Comment=${config.package?.description || config.app.description || ''}
2252
+ Exec=AppRun
2253
+ Icon=${appFileName}
2254
+ Terminal=false
2255
+ StartupWMClass=${appFileName}
2256
+ Categories=Application;
2257
+ `;
2258
+
2259
+ const appDirDesktopPath = join(appDirPath, `${appFileName}.desktop`);
2260
+ writeFileSync(appDirDesktopPath, desktopContent);
2261
+
2262
+ // Copy icon if it exists
2263
+ const iconPath = config.build.linux?.appImageIcon;
2264
+ if (iconPath && existsSync(iconPath)) {
2265
+ const iconDestPath = join(appDirPath, `${appFileName}.png`);
2266
+ cpSync(iconPath, iconDestPath);
2267
+ }
2268
+
2269
+ // Try to create AppImage using available tools
2270
+ const appImagePath = join(buildFolder, `${appFileName}.AppImage`);
2271
+
2272
+ // Check for appimagetool
2273
+ try {
2274
+ execSync('which appimagetool', { stdio: 'pipe' });
2275
+ console.log("Using appimagetool to create AppImage...");
2276
+ execSync(`appimagetool ${escapePathForTerminal(appDirPath)} ${escapePathForTerminal(appImagePath)}`, { stdio: 'inherit' });
2277
+ return appImagePath;
2278
+ } catch {
2279
+ // Check for Docker
2280
+ try {
2281
+ execSync('which docker', { stdio: 'pipe' });
2282
+ console.log("Using Docker to create AppImage...");
2283
+ execSync(`docker run --rm -v "${buildFolder}:/workspace" linuxserver/appimagetool "/workspace/${basename(appDirPath)}" "/workspace/${basename(appImagePath)}"`, { stdio: 'inherit' });
2284
+ return appImagePath;
2285
+ } catch {
2286
+ console.warn("Neither appimagetool nor Docker found. AppImage creation skipped.");
2287
+ console.warn("To create AppImages, install appimagetool or Docker.");
2288
+ return null;
2289
+ }
2290
+ }
2291
+ } catch (error) {
2292
+ console.error("Failed to create AppImage:", error);
2293
+ return null;
2294
+ }
2295
+ }
2296
+
1350
2297
  function codesignAppBundle(
1351
2298
  appBundleOrDmgPath: string,
1352
2299
  entitlementsFilePath?: string
@@ -1363,30 +2310,189 @@ function codesignAppBundle(
1363
2310
  process.exit(1);
1364
2311
  }
1365
2312
 
1366
- // list of entitlements https://developer.apple.com/documentation/security/hardened_runtime?language=objc
1367
- // todo (yoav): consider allowing separate entitlements config for each binary
1368
- // const entitlementsFilePath = join(buildFolder, 'entitlements.plist');
1369
-
1370
- // codesign --deep --force --verbose --timestamp --sign "ELECTROBUN_DEVELOPER_ID" --options runtime --entitlements entitlementsFilePath appBundleOrDmgPath`
2313
+ // If this is a DMG file, sign it directly
2314
+ if (appBundleOrDmgPath.endsWith('.dmg')) {
2315
+ execSync(
2316
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" ${escapePathForTerminal(
2317
+ appBundleOrDmgPath
2318
+ )}`
2319
+ );
2320
+ return;
2321
+ }
1371
2322
 
2323
+ // For app bundles, sign binaries individually to avoid --deep issues with notarization
2324
+ const contentsPath = join(appBundleOrDmgPath, 'Contents');
2325
+ const macosPath = join(contentsPath, 'MacOS');
2326
+
2327
+ // Prepare entitlements if provided
1372
2328
  if (entitlementsFilePath) {
1373
2329
  const entitlementsFileContents = buildEntitlementsFile(
1374
2330
  config.build.mac.entitlements
1375
2331
  );
1376
2332
  Bun.write(entitlementsFilePath, entitlementsFileContents);
2333
+ }
1377
2334
 
1378
- execSync(
1379
- `codesign --deep --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime --entitlements ${entitlementsFilePath} ${escapePathForTerminal(
1380
- appBundleOrDmgPath
1381
- )}`
1382
- );
1383
- } else {
1384
- execSync(
1385
- `codesign --deep --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" ${escapePathForTerminal(
1386
- appBundleOrDmgPath
1387
- )}`
1388
- );
2335
+ // Sign frameworks first (CEF framework requires special handling)
2336
+ const frameworksPath = join(contentsPath, 'Frameworks');
2337
+ if (existsSync(frameworksPath)) {
2338
+ try {
2339
+ const frameworks = readdirSync(frameworksPath);
2340
+ for (const framework of frameworks) {
2341
+ if (framework.endsWith('.framework')) {
2342
+ const frameworkPath = join(frameworksPath, framework);
2343
+
2344
+ if (framework === 'Chromium Embedded Framework.framework') {
2345
+ console.log(`Signing CEF framework components: ${framework}`);
2346
+
2347
+ // Sign CEF libraries first
2348
+ const librariesPath = join(frameworkPath, 'Libraries');
2349
+ if (existsSync(librariesPath)) {
2350
+ const libraries = readdirSync(librariesPath);
2351
+ for (const library of libraries) {
2352
+ if (library.endsWith('.dylib')) {
2353
+ const libraryPath = join(librariesPath, library);
2354
+ console.log(`Signing CEF library: ${library}`);
2355
+ execSync(
2356
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${escapePathForTerminal(libraryPath)}`
2357
+ );
2358
+ }
2359
+ }
2360
+ }
2361
+
2362
+ // CEF helper apps are in the main Frameworks directory, not inside the CEF framework
2363
+ // We'll sign them after signing all frameworks
2364
+ }
2365
+
2366
+ // Sign the framework bundle itself (for CEF and any other frameworks)
2367
+ console.log(`Signing framework bundle: ${framework}`);
2368
+ execSync(
2369
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${escapePathForTerminal(frameworkPath)}`
2370
+ );
2371
+ }
2372
+ }
2373
+ } catch (err) {
2374
+ console.log("Error signing frameworks:", err);
2375
+ throw err; // Re-throw to fail the build since framework signing is critical
2376
+ }
2377
+ }
2378
+
2379
+ // Sign CEF helper apps (they're in the main Frameworks directory, not inside CEF framework)
2380
+ const cefHelperApps = [
2381
+ 'bun Helper.app',
2382
+ 'bun Helper (GPU).app',
2383
+ 'bun Helper (Plugin).app',
2384
+ 'bun Helper (Alerts).app',
2385
+ 'bun Helper (Renderer).app'
2386
+ ];
2387
+
2388
+ for (const helperApp of cefHelperApps) {
2389
+ const helperPath = join(frameworksPath, helperApp);
2390
+ if (existsSync(helperPath)) {
2391
+ const helperExecutablePath = join(helperPath, 'Contents', 'MacOS', helperApp.replace('.app', ''));
2392
+ if (existsSync(helperExecutablePath)) {
2393
+ console.log(`Signing CEF helper executable: ${helperApp}`);
2394
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2395
+ execSync(
2396
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(helperExecutablePath)}`
2397
+ );
2398
+ }
2399
+
2400
+ console.log(`Signing CEF helper bundle: ${helperApp}`);
2401
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2402
+ execSync(
2403
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(helperPath)}`
2404
+ );
2405
+ }
1389
2406
  }
2407
+
2408
+ // Sign all binaries and libraries in MacOS folder and subdirectories
2409
+ console.log("Signing all binaries in MacOS folder...");
2410
+
2411
+ // Recursively find all executables and libraries in MacOS folder
2412
+ function findExecutables(dir: string): string[] {
2413
+ let executables: string[] = [];
2414
+
2415
+ try {
2416
+ const entries = readdirSync(dir, { withFileTypes: true });
2417
+
2418
+ for (const entry of entries) {
2419
+ const fullPath = join(dir, entry.name);
2420
+
2421
+ if (entry.isDirectory()) {
2422
+ // Recursively search subdirectories
2423
+ executables = executables.concat(findExecutables(fullPath));
2424
+ } else if (entry.isFile()) {
2425
+ // Check if it's an executable or library
2426
+ try {
2427
+ const fileInfo = execSync(`file -b "${fullPath}"`, { encoding: 'utf8' }).trim();
2428
+ if (fileInfo.includes('Mach-O') || entry.name.endsWith('.dylib')) {
2429
+ executables.push(fullPath);
2430
+ }
2431
+ } catch {
2432
+ // If file command fails, check by extension
2433
+ if (entry.name.endsWith('.dylib') || !entry.name.includes('.')) {
2434
+ // No extension often means executable
2435
+ executables.push(fullPath);
2436
+ }
2437
+ }
2438
+ }
2439
+ }
2440
+ } catch (err) {
2441
+ console.error(`Error scanning directory ${dir}:`, err);
2442
+ }
2443
+
2444
+ return executables;
2445
+ }
2446
+
2447
+ const executablesInMacOS = findExecutables(macosPath);
2448
+
2449
+ // Sign each found executable
2450
+ for (const execPath of executablesInMacOS) {
2451
+ const fileName = basename(execPath);
2452
+ const relativePath = execPath.replace(macosPath + '/', '');
2453
+
2454
+ // Use filename as identifier (without extension)
2455
+ const identifier = fileName.replace(/\.[^.]+$/, '');
2456
+
2457
+ console.log(`Signing ${relativePath} with identifier ${identifier}`);
2458
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2459
+
2460
+ try {
2461
+ execSync(
2462
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime --identifier ${identifier} ${entitlementFlag} ${escapePathForTerminal(execPath)}`
2463
+ );
2464
+ } catch (err) {
2465
+ console.error(`Failed to sign ${relativePath}:`, err.message);
2466
+ // Continue signing other files even if one fails
2467
+ }
2468
+ }
2469
+
2470
+ // Note: main.js is now in Resources and will be automatically sealed when signing the app bundle
2471
+
2472
+ // Sign the main executable (launcher) - this should use the app's bundle identifier, not "launcher"
2473
+ const launcherPath = join(macosPath, 'launcher');
2474
+ if (existsSync(launcherPath)) {
2475
+ console.log("Signing main executable (launcher)");
2476
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2477
+ try {
2478
+ execSync(
2479
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(launcherPath)}`
2480
+ );
2481
+ } catch (error) {
2482
+ console.error("Failed to sign launcher:", error.message);
2483
+ console.log("Attempting to sign launcher without runtime hardening...");
2484
+ execSync(
2485
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" ${entitlementFlag} ${escapePathForTerminal(launcherPath)}`
2486
+ );
2487
+ }
2488
+ }
2489
+
2490
+ // Finally, sign the app bundle itself (without --deep)
2491
+ console.log("Signing app bundle");
2492
+ const entitlementFlag = entitlementsFilePath ? `--entitlements ${entitlementsFilePath}` : '';
2493
+ execSync(
2494
+ `codesign --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(appBundleOrDmgPath)}`
2495
+ );
1390
2496
  }
1391
2497
 
1392
2498
  function notarizeAndStaple(appOrDmgPath: string) {
@@ -1473,8 +2579,8 @@ function notarizeAndStaple(appOrDmgPath: string) {
1473
2579
  // have the same name but different subfolders in our build directory. or I guess delete the first one after tar/compression and then create the other one.
1474
2580
  // either way you can pass in the parent folder here for that flexibility.
1475
2581
  // for intel/arm builds on mac we'll probably have separate subfolders as well and build them in parallel.
1476
- function createAppBundle(bundleName: string, parentFolder: string) {
1477
- if (OS === 'macos') {
2582
+ function createAppBundle(bundleName: string, parentFolder: string, targetOS: 'macos' | 'win' | 'linux') {
2583
+ if (targetOS === 'macos') {
1478
2584
  // macOS bundle structure
1479
2585
  const bundleFileName = `${bundleName}.app`;
1480
2586
  const appBundleFolderPath = join(parentFolder, bundleFileName);
@@ -1501,7 +2607,7 @@ function createAppBundle(bundleName: string, parentFolder: string) {
1501
2607
  appBundleFolderResourcesPath,
1502
2608
  appBundleFolderFrameworksPath,
1503
2609
  };
1504
- } else if (OS === 'linux' || OS === 'win') {
2610
+ } else if (targetOS === 'linux' || targetOS === 'win') {
1505
2611
  // Linux/Windows simpler structure
1506
2612
  const appBundleFolderPath = join(parentFolder, bundleName);
1507
2613
  const appBundleFolderContentsPath = appBundleFolderPath; // No Contents folder needed
@@ -1522,6 +2628,14 @@ function createAppBundle(bundleName: string, parentFolder: string) {
1522
2628
  appBundleFolderFrameworksPath,
1523
2629
  };
1524
2630
  } else {
1525
- throw new Error(`Unsupported OS: ${OS}`);
2631
+ throw new Error(`Unsupported OS: ${targetOS}`);
1526
2632
  }
1527
2633
  }
2634
+
2635
+ } // End of main() function
2636
+
2637
+ // Run the main function
2638
+ main().catch((error) => {
2639
+ console.error('Fatal error:', error);
2640
+ process.exit(1);
2641
+ });