electrobun 0.0.19-beta.7 → 0.0.19-beta.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli/index.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  mkdirSync,
8
8
  createWriteStream,
9
9
  unlinkSync,
10
+ readdirSync,
10
11
  } from "fs";
11
12
  import { execSync } from "child_process";
12
13
  import tar from "tar";
@@ -18,7 +19,8 @@ const MAX_CHUNK_SIZE = 1024 * 2;
18
19
 
19
20
  // TODO: dedup with built.ts
20
21
  const OS: 'win' | 'linux' | 'macos' = getPlatform();
21
- const ARCH: 'arm64' | 'x64' = getArch();
22
+ // Always use x64 for Windows since we only build x64 Windows binaries
23
+ const ARCH: 'arm64' | 'x64' = OS === 'win' ? 'x64' : getArch();
22
24
 
23
25
  function getPlatform() {
24
26
  switch (platform()) {
@@ -56,48 +58,88 @@ const configPath = join(projectRoot, configName);
56
58
  const indexOfElectrobun = process.argv.findIndex((arg) =>
57
59
  arg.includes("electrobun")
58
60
  );
59
- const commandArg = process.argv[indexOfElectrobun + 1] || "launcher";
61
+ const commandArg = process.argv[indexOfElectrobun + 1] || "build";
60
62
 
61
63
  const ELECTROBUN_DEP_PATH = join(projectRoot, "node_modules", "electrobun");
62
64
 
63
65
  // When debugging electrobun with the example app use the builds (dev or release) right from the source folder
64
66
  // For developers using electrobun cli via npm use the release versions in /dist
65
67
  // 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
68
 
94
- async function ensureCoreDependencies() {
95
- // Check if core dependencies already exist
96
- if (existsSync(PATHS.BUN_BINARY) && existsSync(PATHS.LAUNCHER_RELEASE)) {
69
+ // Function to get platform-specific paths
70
+ function getPlatformPaths(targetOS: 'macos' | 'win' | 'linux', targetArch: 'arm64' | 'x64') {
71
+ const binExt = targetOS === 'win' ? '.exe' : '';
72
+ const platformDistDir = join(ELECTROBUN_DEP_PATH, `dist-${targetOS}-${targetArch}`);
73
+ const sharedDistDir = join(ELECTROBUN_DEP_PATH, "dist");
74
+
75
+ return {
76
+ // Platform-specific binaries (from dist-OS-ARCH/)
77
+ BUN_BINARY: join(platformDistDir, "bun") + binExt,
78
+ LAUNCHER_DEV: join(platformDistDir, "electrobun") + binExt,
79
+ LAUNCHER_RELEASE: join(platformDistDir, "launcher") + binExt,
80
+ NATIVE_WRAPPER_MACOS: join(platformDistDir, "libNativeWrapper.dylib"),
81
+ NATIVE_WRAPPER_WIN: join(platformDistDir, "libNativeWrapper.dll"),
82
+ NATIVE_WRAPPER_LINUX: join(platformDistDir, "libNativeWrapper.so"),
83
+ NATIVE_WRAPPER_LINUX_CEF: join(platformDistDir, "libNativeWrapper_cef.so"),
84
+ WEBVIEW2LOADER_WIN: join(platformDistDir, "WebView2Loader.dll"),
85
+ BSPATCH: join(platformDistDir, "bspatch") + binExt,
86
+ EXTRACTOR: join(platformDistDir, "extractor") + binExt,
87
+ BSDIFF: join(platformDistDir, "bsdiff") + binExt,
88
+ CEF_FRAMEWORK_MACOS: join(platformDistDir, "cef", "Chromium Embedded Framework.framework"),
89
+ CEF_HELPER_MACOS: join(platformDistDir, "cef", "process_helper"),
90
+ CEF_HELPER_WIN: join(platformDistDir, "cef", "process_helper.exe"),
91
+ CEF_HELPER_LINUX: join(platformDistDir, "cef", "process_helper"),
92
+ CEF_DIR: join(platformDistDir, "cef"),
93
+
94
+ // Shared platform-independent files (from dist/)
95
+ // These work with existing package.json and development workflow
96
+ MAIN_JS: join(sharedDistDir, "main.js"),
97
+ API_DIR: join(sharedDistDir, "api"),
98
+ };
99
+ }
100
+
101
+ // Default PATHS for host platform (backward compatibility)
102
+ const PATHS = getPlatformPaths(OS, ARCH);
103
+
104
+ async function ensureCoreDependencies(targetOS?: 'macos' | 'win' | 'linux', targetArch?: 'arm64' | 'x64') {
105
+ // Use provided target platform or default to host platform
106
+ const platformOS = targetOS || OS;
107
+ const platformArch = targetArch || ARCH;
108
+
109
+ // Get platform-specific paths
110
+ const platformPaths = getPlatformPaths(platformOS, platformArch);
111
+
112
+ // Check platform-specific binaries
113
+ const requiredBinaries = [
114
+ platformPaths.BUN_BINARY,
115
+ platformPaths.LAUNCHER_RELEASE,
116
+ // Platform-specific native wrapper
117
+ platformOS === 'macos' ? platformPaths.NATIVE_WRAPPER_MACOS :
118
+ platformOS === 'win' ? platformPaths.NATIVE_WRAPPER_WIN :
119
+ platformPaths.NATIVE_WRAPPER_LINUX
120
+ ];
121
+
122
+ // Check shared files (main.js should be in shared dist/)
123
+ const requiredSharedFiles = [
124
+ platformPaths.MAIN_JS
125
+ ];
126
+
127
+ const missingBinaries = requiredBinaries.filter(file => !existsSync(file));
128
+ const missingSharedFiles = requiredSharedFiles.filter(file => !existsSync(file));
129
+
130
+ // If only shared files are missing, that's expected in production (they come via npm)
131
+ if (missingBinaries.length === 0 && missingSharedFiles.length > 0) {
132
+ console.log(`Shared files missing (expected in production): ${missingSharedFiles.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', ')}`);
133
+ }
134
+
135
+ // Only download if platform-specific binaries are missing
136
+ if (missingBinaries.length === 0) {
97
137
  return;
98
138
  }
99
139
 
100
- console.log('Core dependencies not found, downloading...');
140
+ // Show which binaries are missing
141
+ console.log(`Core dependencies not found for ${platformOS}-${platformArch}. Missing files:`, missingBinaries.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', '));
142
+ console.log(`Downloading core binaries for ${platformOS}-${platformArch}...`);
101
143
 
102
144
  // Get the current Electrobun version from package.json
103
145
  const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
@@ -112,61 +154,153 @@ async function ensureCoreDependencies() {
112
154
  }
113
155
  }
114
156
 
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`;
157
+ const platformName = platformOS === 'macos' ? 'darwin' : platformOS === 'win' ? 'win' : 'linux';
158
+ const archName = platformArch;
159
+ const coreTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-core-${platformName}-${archName}.tar.gz`;
118
160
 
119
- console.log(`Downloading core binaries from: ${mainTarballUrl}`);
161
+ console.log(`Downloading core binaries from: ${coreTarballUrl}`);
120
162
 
121
163
  try {
122
- // Download main tarball
123
- const response = await fetch(mainTarballUrl);
164
+ // Download core binaries tarball
165
+ const response = await fetch(coreTarballUrl);
124
166
  if (!response.ok) {
125
167
  throw new Error(`Failed to download binaries: ${response.status} ${response.statusText}`);
126
168
  }
127
169
 
128
170
  // Create temp file
129
- const tempFile = join(ELECTROBUN_DEP_PATH, 'main-temp.tar.gz');
171
+ const tempFile = join(ELECTROBUN_DEP_PATH, `core-${platformOS}-${platformArch}-temp.tar.gz`);
130
172
  const fileStream = createWriteStream(tempFile);
131
173
 
132
174
  // Write response to file
133
175
  if (response.body) {
134
176
  const reader = response.body.getReader();
177
+ let totalBytes = 0;
135
178
  while (true) {
136
179
  const { done, value } = await reader.read();
137
180
  if (done) break;
138
- fileStream.write(Buffer.from(value));
181
+ const buffer = Buffer.from(value);
182
+ fileStream.write(buffer);
183
+ totalBytes += buffer.length;
139
184
  }
185
+ console.log(`Downloaded ${totalBytes} bytes for ${platformOS}-${platformArch}`);
140
186
  }
141
- fileStream.end();
142
187
 
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'),
188
+ // Ensure file is properly closed before proceeding
189
+ await new Promise((resolve, reject) => {
190
+ fileStream.end((err) => {
191
+ if (err) reject(err);
192
+ else resolve(null);
193
+ });
148
194
  });
149
195
 
196
+ // Verify the downloaded file exists and has content
197
+ if (!existsSync(tempFile)) {
198
+ throw new Error(`Downloaded file not found: ${tempFile}`);
199
+ }
200
+
201
+ const fileSize = require('fs').statSync(tempFile).size;
202
+ if (fileSize === 0) {
203
+ throw new Error(`Downloaded file is empty: ${tempFile}`);
204
+ }
205
+
206
+ console.log(`Verified download: ${tempFile} (${fileSize} bytes)`);
207
+
208
+ // Extract to platform-specific dist directory
209
+ console.log(`Extracting core dependencies for ${platformOS}-${platformArch}...`);
210
+ const platformDistPath = join(ELECTROBUN_DEP_PATH, `dist-${platformOS}-${platformArch}`);
211
+ mkdirSync(platformDistPath, { recursive: true });
212
+
213
+ // Use Windows native tar.exe on Windows due to npm tar library issues
214
+ if (OS === 'win') {
215
+ console.log('Using Windows native tar.exe for reliable extraction...');
216
+ execSync(`tar -xf "${tempFile}" -C "${platformDistPath}"`, {
217
+ stdio: 'inherit',
218
+ cwd: platformDistPath
219
+ });
220
+ } else {
221
+ await tar.x({
222
+ file: tempFile,
223
+ cwd: platformDistPath,
224
+ preservePaths: false,
225
+ strip: 0,
226
+ });
227
+ }
228
+
229
+ // NOTE: We no longer copy main.js from platform-specific downloads
230
+ // Platform-specific downloads should only contain native binaries
231
+ // main.js and api/ should be shipped via npm in the shared dist/ folder
232
+
150
233
  // Clean up temp file
151
234
  unlinkSync(tempFile);
152
235
 
153
- console.log('Core dependencies downloaded and cached successfully');
236
+ // Debug: List what was actually extracted
237
+ try {
238
+ const extractedFiles = readdirSync(platformDistPath);
239
+ console.log(`Extracted files to ${platformDistPath}:`, extractedFiles);
240
+
241
+ // Check if files are in subdirectories
242
+ for (const file of extractedFiles) {
243
+ const filePath = join(platformDistPath, file);
244
+ const stat = require('fs').statSync(filePath);
245
+ if (stat.isDirectory()) {
246
+ const subFiles = readdirSync(filePath);
247
+ console.log(` ${file}/: ${subFiles.join(', ')}`);
248
+ }
249
+ }
250
+ } catch (e) {
251
+ console.error('Could not list extracted files:', e);
252
+ }
253
+
254
+ // Verify extraction completed successfully - check platform-specific binaries only
255
+ const requiredBinaries = [
256
+ platformPaths.BUN_BINARY,
257
+ platformPaths.LAUNCHER_RELEASE,
258
+ platformOS === 'macos' ? platformPaths.NATIVE_WRAPPER_MACOS :
259
+ platformOS === 'win' ? platformPaths.NATIVE_WRAPPER_WIN :
260
+ platformPaths.NATIVE_WRAPPER_LINUX
261
+ ];
262
+
263
+ const missingBinaries = requiredBinaries.filter(file => !existsSync(file));
264
+ if (missingBinaries.length > 0) {
265
+ console.error(`Missing binaries after extraction: ${missingBinaries.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', ')}`);
266
+ console.error('This suggests the tarball structure is different than expected');
267
+ }
268
+
269
+ // For development: if main.js doesn't exist in shared dist/, copy from platform-specific download as fallback
270
+ const sharedDistPath = join(ELECTROBUN_DEP_PATH, 'dist');
271
+ const extractedMainJs = join(platformDistPath, 'main.js');
272
+ const sharedMainJs = join(sharedDistPath, 'main.js');
273
+
274
+ if (existsSync(extractedMainJs) && !existsSync(sharedMainJs)) {
275
+ console.log('Development fallback: copying main.js from platform-specific download to shared dist/');
276
+ mkdirSync(sharedDistPath, { recursive: true });
277
+ cpSync(extractedMainJs, sharedMainJs);
278
+ }
279
+
280
+ console.log(`Core dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
154
281
 
155
- } catch (error) {
156
- console.error('Failed to download core dependencies:', error.message);
282
+ } catch (error: any) {
283
+ console.error(`Failed to download core dependencies for ${platformOS}-${platformArch}:`, error.message);
157
284
  console.error('Please ensure you have an internet connection and the release exists.');
158
285
  process.exit(1);
159
286
  }
160
287
  }
161
288
 
162
- async function ensureCEFDependencies() {
289
+ async function ensureCEFDependencies(targetOS?: 'macos' | 'win' | 'linux', targetArch?: 'arm64' | 'x64') {
290
+ // Use provided target platform or default to host platform
291
+ const platformOS = targetOS || OS;
292
+ const platformArch = targetArch || ARCH;
293
+
294
+ // Get platform-specific paths
295
+ const platformPaths = getPlatformPaths(platformOS, platformArch);
296
+
163
297
  // Check if CEF dependencies already exist
164
- if (existsSync(PATHS.CEF_DIR)) {
165
- console.log('CEF dependencies found, using cached version');
298
+ if (existsSync(platformPaths.CEF_DIR)) {
299
+ console.log(`CEF dependencies found for ${platformOS}-${platformArch}, using cached version`);
166
300
  return;
167
301
  }
168
302
 
169
- console.log('CEF dependencies not found, downloading...');
303
+ console.log(`CEF dependencies not found for ${platformOS}-${platformArch}, downloading...`);
170
304
 
171
305
  // Get the current Electrobun version from package.json
172
306
  const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
@@ -181,8 +315,8 @@ async function ensureCEFDependencies() {
181
315
  }
182
316
  }
183
317
 
184
- const platformName = OS === 'macos' ? 'darwin' : OS === 'win' ? 'win32' : 'linux';
185
- const archName = ARCH;
318
+ const platformName = platformOS === 'macos' ? 'darwin' : platformOS === 'win' ? 'win' : 'linux';
319
+ const archName = platformArch;
186
320
  const cefTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-cef-${platformName}-${archName}.tar.gz`;
187
321
 
188
322
  console.log(`Downloading CEF from: ${cefTarballUrl}`);
@@ -195,7 +329,7 @@ async function ensureCEFDependencies() {
195
329
  }
196
330
 
197
331
  // Create temp file
198
- const tempFile = join(ELECTROBUN_DEP_PATH, 'cef-temp.tar.gz');
332
+ const tempFile = join(ELECTROBUN_DEP_PATH, `cef-${platformOS}-${platformArch}-temp.tar.gz`);
199
333
  const fileStream = createWriteStream(tempFile);
200
334
 
201
335
  // Write response to file
@@ -209,20 +343,49 @@ async function ensureCEFDependencies() {
209
343
  }
210
344
  fileStream.end();
211
345
 
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
- });
346
+ // Extract to platform-specific dist directory
347
+ console.log(`Extracting CEF dependencies for ${platformOS}-${platformArch}...`);
348
+ const platformDistPath = join(ELECTROBUN_DEP_PATH, `dist-${platformOS}-${platformArch}`);
349
+ mkdirSync(platformDistPath, { recursive: true });
350
+
351
+ // Use Windows native tar.exe on Windows due to npm tar library issues
352
+ if (OS === 'win') {
353
+ console.log('Using Windows native tar.exe for reliable extraction...');
354
+ execSync(`tar -xf "${tempFile}" -C "${platformDistPath}"`, {
355
+ stdio: 'inherit',
356
+ cwd: platformDistPath
357
+ });
358
+ } else {
359
+ await tar.x({
360
+ file: tempFile,
361
+ cwd: platformDistPath,
362
+ preservePaths: false,
363
+ strip: 0,
364
+ });
365
+ }
218
366
 
219
367
  // Clean up temp file
220
368
  unlinkSync(tempFile);
221
369
 
222
- console.log('CEF dependencies downloaded and cached successfully');
370
+ // Debug: List what was actually extracted for CEF
371
+ try {
372
+ const extractedFiles = readdirSync(platformDistPath);
373
+ console.log(`CEF extracted files to ${platformDistPath}:`, extractedFiles);
374
+
375
+ // Check if CEF directory was created
376
+ const cefDir = join(platformDistPath, 'cef');
377
+ if (existsSync(cefDir)) {
378
+ const cefFiles = readdirSync(cefDir);
379
+ console.log(`CEF directory contents: ${cefFiles.slice(0, 10).join(', ')}${cefFiles.length > 10 ? '...' : ''}`);
380
+ }
381
+ } catch (e) {
382
+ console.error('Could not list CEF extracted files:', e);
383
+ }
384
+
385
+ console.log(`CEF dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
223
386
 
224
- } catch (error) {
225
- console.error('Failed to download CEF dependencies:', error.message);
387
+ } catch (error: any) {
388
+ console.error(`Failed to download CEF dependencies for ${platformOS}-${platformArch}:`, error.message);
226
389
  console.error('Please ensure you have an internet connection and the release exists.');
227
390
  process.exit(1);
228
391
  }
@@ -241,10 +404,6 @@ const commandDefaults = {
241
404
  projectRoot,
242
405
  config: "electrobun.config",
243
406
  },
244
- launcher: {
245
- projectRoot,
246
- config: "electrobun.config",
247
- },
248
407
  };
249
408
 
250
409
  // todo (yoav): add types for config
@@ -257,6 +416,7 @@ const defaultConfig = {
257
416
  build: {
258
417
  buildFolder: "build",
259
418
  artifactFolder: "artifacts",
419
+ targets: undefined, // Will default to current platform if not specified
260
420
  mac: {
261
421
  codesign: false,
262
422
  notarize: false,
@@ -267,6 +427,10 @@ const defaultConfig = {
267
427
  },
268
428
  icons: "icon.iconset",
269
429
  },
430
+ bun: {
431
+ entrypoint: "src/bun/index.ts",
432
+ external: [],
433
+ },
270
434
  },
271
435
  scripts: {
272
436
  postBuild: "",
@@ -286,7 +450,10 @@ if (!command) {
286
450
  const config = getConfig();
287
451
 
288
452
  const envArg =
289
- process.argv.find((arg) => arg.startsWith("env="))?.split("=")[1] || "";
453
+ process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
454
+
455
+ const targetsArg =
456
+ process.argv.find((arg) => arg.startsWith("--targets="))?.split("=")[1] || "";
290
457
 
291
458
  const validEnvironments = ["dev", "canary", "stable"];
292
459
 
@@ -294,8 +461,175 @@ const validEnvironments = ["dev", "canary", "stable"];
294
461
  const buildEnvironment: "dev" | "canary" | "stable" =
295
462
  validEnvironments.includes(envArg) ? envArg : "dev";
296
463
 
464
+ // Determine build targets
465
+ type BuildTarget = { os: 'macos' | 'win' | 'linux', arch: 'arm64' | 'x64' };
466
+
467
+ function parseBuildTargets(): BuildTarget[] {
468
+ // If explicit targets provided via CLI
469
+ if (targetsArg) {
470
+ if (targetsArg === 'current') {
471
+ return [{ os: OS, arch: ARCH }];
472
+ } else if (targetsArg === 'all') {
473
+ return parseConfigTargets();
474
+ } else {
475
+ // Parse comma-separated targets like "macos-arm64,win-x64"
476
+ return targetsArg.split(',').map(target => {
477
+ const [os, arch] = target.trim().split('-') as [string, string];
478
+ if (!['macos', 'win', 'linux'].includes(os) || !['arm64', 'x64'].includes(arch)) {
479
+ console.error(`Invalid target: ${target}. Format should be: os-arch (e.g., macos-arm64)`);
480
+ process.exit(1);
481
+ }
482
+ return { os, arch } as BuildTarget;
483
+ });
484
+ }
485
+ }
486
+
487
+ // Default behavior: always build for current platform only
488
+ // This ensures predictable, fast builds unless explicitly requesting multi-platform
489
+ return [{ os: OS, arch: ARCH }];
490
+ }
491
+
492
+ function parseConfigTargets(): BuildTarget[] {
493
+ // If config has targets, use them
494
+ if (config.build.targets && config.build.targets.length > 0) {
495
+ return config.build.targets.map(target => {
496
+ if (target === 'current') {
497
+ return { os: OS, arch: ARCH };
498
+ }
499
+ const [os, arch] = target.split('-') as [string, string];
500
+ if (!['macos', 'win', 'linux'].includes(os) || !['arm64', 'x64'].includes(arch)) {
501
+ console.error(`Invalid target in config: ${target}. Format should be: os-arch (e.g., macos-arm64)`);
502
+ process.exit(1);
503
+ }
504
+ return { os, arch } as BuildTarget;
505
+ });
506
+ }
507
+
508
+ // If no config targets and --targets=all, use all available platforms
509
+ if (targetsArg === 'all') {
510
+ console.log('No targets specified in config, using all available platforms');
511
+ return [
512
+ { os: 'macos', arch: 'arm64' },
513
+ { os: 'macos', arch: 'x64' },
514
+ { os: 'win', arch: 'x64' },
515
+ { os: 'linux', arch: 'x64' },
516
+ { os: 'linux', arch: 'arm64' }
517
+ ];
518
+ }
519
+
520
+ // Default to current platform
521
+ return [{ os: OS, arch: ARCH }];
522
+ }
523
+
524
+ const buildTargets = parseBuildTargets();
525
+
526
+ // Show build targets to user
527
+ if (buildTargets.length === 1) {
528
+ console.log(`Building for ${buildTargets[0].os}-${buildTargets[0].arch} (${buildEnvironment})`);
529
+ } else {
530
+ const targetList = buildTargets.map(t => `${t.os}-${t.arch}`).join(', ');
531
+ console.log(`Building for multiple targets: ${targetList} (${buildEnvironment})`);
532
+ console.log(`Running ${buildTargets.length} parallel builds...`);
533
+
534
+ // Spawn parallel build processes
535
+ const buildPromises = buildTargets.map(async (target) => {
536
+ const targetString = `${target.os}-${target.arch}`;
537
+ const prefix = `[${targetString}]`;
538
+
539
+ try {
540
+ // Try to find the electrobun binary in node_modules/.bin or use bunx
541
+ const electrobunBin = join(projectRoot, 'node_modules', '.bin', 'electrobun');
542
+ let command: string[];
543
+
544
+ if (existsSync(electrobunBin)) {
545
+ command = [electrobunBin, 'build', `--env=${buildEnvironment}`, `--targets=${targetString}`];
546
+ } else {
547
+ // Fallback to bunx which should resolve node_modules binaries
548
+ command = ['bunx', 'electrobun', 'build', `--env=${buildEnvironment}`, `--targets=${targetString}`];
549
+ }
550
+
551
+ console.log(`${prefix} Running:`, command.join(' '));
552
+
553
+ const result = await Bun.spawn(command, {
554
+ stdio: ['inherit', 'pipe', 'pipe'],
555
+ env: process.env,
556
+ cwd: projectRoot // Ensure we're in the right directory
557
+ });
558
+
559
+ // Pipe output with prefix
560
+ if (result.stdout) {
561
+ const reader = result.stdout.getReader();
562
+ while (true) {
563
+ const { done, value } = await reader.read();
564
+ if (done) break;
565
+ const text = new TextDecoder().decode(value);
566
+ // Add prefix to each line
567
+ const prefixedText = text.split('\n').map(line =>
568
+ line ? `${prefix} ${line}` : line
569
+ ).join('\n');
570
+ process.stdout.write(prefixedText);
571
+ }
572
+ }
573
+
574
+ if (result.stderr) {
575
+ const reader = result.stderr.getReader();
576
+ while (true) {
577
+ const { done, value } = await reader.read();
578
+ if (done) break;
579
+ const text = new TextDecoder().decode(value);
580
+ const prefixedText = text.split('\n').map(line =>
581
+ line ? `${prefix} ${line}` : line
582
+ ).join('\n');
583
+ process.stderr.write(prefixedText);
584
+ }
585
+ }
586
+
587
+ const exitCode = await result.exited;
588
+ return { target, exitCode, success: exitCode === 0 };
589
+
590
+ } catch (error) {
591
+ console.error(`${prefix} Failed to start build:`, error);
592
+ return { target, exitCode: 1, success: false, error };
593
+ }
594
+ });
595
+
596
+ // Wait for all builds to complete
597
+ const results = await Promise.allSettled(buildPromises);
598
+
599
+ // Report final results
600
+ console.log('\n=== Build Results ===');
601
+ let allSucceeded = true;
602
+
603
+ for (const result of results) {
604
+ if (result.status === 'fulfilled') {
605
+ const { target, success, exitCode } = result.value;
606
+ const status = success ? '✅ SUCCESS' : '❌ FAILED';
607
+ console.log(`${target.os}-${target.arch}: ${status} (exit code: ${exitCode})`);
608
+ if (!success) allSucceeded = false;
609
+ } else {
610
+ console.log(`Build rejected: ${result.reason}`);
611
+ allSucceeded = false;
612
+ }
613
+ }
614
+
615
+ if (!allSucceeded) {
616
+ console.log('\nSome builds failed. Check the output above for details.');
617
+ process.exit(1);
618
+ } else {
619
+ console.log('\nAll builds completed successfully! 🎉');
620
+ }
621
+
622
+ process.exit(0);
623
+ }
624
+
297
625
  // todo (yoav): dev builds should include the branch name, and/or allow configuration via external config
298
- const buildSubFolder = `${buildEnvironment}`;
626
+ // For now, assume single target build (we'll refactor for multi-target later)
627
+ const currentTarget = buildTargets[0];
628
+ const buildSubFolder = `${buildEnvironment}-${currentTarget.os}-${currentTarget.arch}`;
629
+
630
+ // Use target OS/ARCH for build logic (instead of current machine's OS/ARCH)
631
+ const targetOS = currentTarget.os;
632
+ const targetARCH = currentTarget.arch;
299
633
 
300
634
  const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
301
635
 
@@ -370,7 +704,7 @@ const appFileName = (
370
704
  )
371
705
  .replace(/\s/g, "")
372
706
  .replace(/\./g, "-");
373
- const bundleFileName = OS === 'macos' ? `${appFileName}.app` : appFileName;
707
+ const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName;
374
708
 
375
709
  // const logPath = `/Library/Logs/Electrobun/ExampleApp/dev/out.log`;
376
710
 
@@ -380,6 +714,12 @@ if (commandArg === "init") {
380
714
  // todo (yoav): init a repo folder structure
381
715
  console.log("initializing electrobun project");
382
716
  } else if (commandArg === "build") {
717
+ // Ensure core binaries are available for the target platform before starting build
718
+ await ensureCoreDependencies(currentTarget.os, currentTarget.arch);
719
+
720
+ // Get platform-specific paths for the current target
721
+ const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);
722
+
383
723
  // refresh build folder
384
724
  if (existsSync(buildFolder)) {
385
725
  rmdirSync(buildFolder, { recursive: true });
@@ -403,7 +743,7 @@ if (commandArg === "init") {
403
743
  appBundleMacOSPath,
404
744
  appBundleFolderResourcesPath,
405
745
  appBundleFolderFrameworksPath,
406
- } = createAppBundle(appFileName, buildFolder);
746
+ } = createAppBundle(appFileName, buildFolder, targetOS);
407
747
 
408
748
  const appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
409
749
 
@@ -473,29 +813,27 @@ if (commandArg === "init") {
473
813
  // mkdirSync(destLauncherFolder, {recursive: true});
474
814
  // }
475
815
  // 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;
483
- const destLauncherFolder = dirname(bunCliLauncherDestination);
484
- if (!existsSync(destLauncherFolder)) {
485
- // console.info('creating folder: ', destFolder);
486
- mkdirSync(destLauncherFolder, { recursive: true });
487
- }
816
+ // Only copy launcher for non-dev builds
817
+ if (buildEnvironment !== "dev") {
818
+ const bunCliLauncherBinarySource = targetPaths.LAUNCHER_RELEASE;
819
+ const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + binExt;
820
+ const destLauncherFolder = dirname(bunCliLauncherDestination);
821
+ if (!existsSync(destLauncherFolder)) {
822
+ // console.info('creating folder: ', destFolder);
823
+ mkdirSync(destLauncherFolder, { recursive: true });
824
+ }
488
825
 
489
- cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
490
- recursive: true,
491
- dereference: true,
492
- });
826
+ cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
827
+ recursive: true,
828
+ dereference: true,
829
+ });
830
+ }
493
831
 
494
- cpSync(PATHS.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
832
+ cpSync(targetPaths.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
495
833
 
496
834
  // Bun runtime binary
497
835
  // todo (yoav): this only works for the current architecture
498
- const bunBinarySourcePath = PATHS.BUN_BINARY;
836
+ const bunBinarySourcePath = targetPaths.BUN_BINARY;
499
837
  // Note: .bin/bun binary in node_modules is a symlink to the versioned one in another place
500
838
  // in node_modules, so we have to dereference here to get the actual binary in the bundle.
501
839
  const bunBinaryDestInBundlePath = join(appBundleMacOSPath, "bun") + binExt;
@@ -507,8 +845,8 @@ if (commandArg === "init") {
507
845
  cpSync(bunBinarySourcePath, bunBinaryDestInBundlePath, { dereference: true });
508
846
 
509
847
  // copy native wrapper dynamic library
510
- if (OS === 'macos') {
511
- const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_MACOS;
848
+ if (targetOS === 'macos') {
849
+ const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_MACOS;
512
850
  const nativeWrapperMacosDestination = join(
513
851
  appBundleMacOSPath,
514
852
  "libNativeWrapper.dylib"
@@ -516,8 +854,8 @@ if (commandArg === "init") {
516
854
  cpSync(nativeWrapperMacosSource, nativeWrapperMacosDestination, {
517
855
  dereference: true,
518
856
  });
519
- } else if (OS === 'win') {
520
- const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_WIN;
857
+ } else if (targetOS === 'win') {
858
+ const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_WIN;
521
859
  const nativeWrapperMacosDestination = join(
522
860
  appBundleMacOSPath,
523
861
  "libNativeWrapper.dll"
@@ -526,7 +864,7 @@ if (commandArg === "init") {
526
864
  dereference: true,
527
865
  });
528
866
 
529
- const webview2LibSource = PATHS.WEBVIEW2LOADER_WIN;
867
+ const webview2LibSource = targetPaths.WEBVIEW2LOADER_WIN;
530
868
  const webview2LibDestination = join(
531
869
  appBundleMacOSPath,
532
870
  "WebView2Loader.dll"
@@ -534,30 +872,34 @@ if (commandArg === "init") {
534
872
  // copy webview2 system webview library
535
873
  cpSync(webview2LibSource, webview2LibDestination);
536
874
 
537
- } else if (OS === 'linux') {
538
- const nativeWrapperLinuxSource = PATHS.NATIVE_WRAPPER_LINUX;
875
+ } else if (targetOS === 'linux') {
876
+ // Choose the appropriate native wrapper based on bundleCEF setting
877
+ const useCEF = config.build.linux?.bundleCEF;
878
+ const nativeWrapperLinuxSource = useCEF ? targetPaths.NATIVE_WRAPPER_LINUX_CEF : targetPaths.NATIVE_WRAPPER_LINUX;
539
879
  const nativeWrapperLinuxDestination = join(
540
880
  appBundleMacOSPath,
541
881
  "libNativeWrapper.so"
542
882
  );
883
+
543
884
  if (existsSync(nativeWrapperLinuxSource)) {
544
885
  cpSync(nativeWrapperLinuxSource, nativeWrapperLinuxDestination, {
545
886
  dereference: true,
546
887
  });
888
+ console.log(`Using ${useCEF ? 'CEF' : 'GTK'} native wrapper for Linux`);
889
+ } else {
890
+ throw new Error(`Native wrapper not found: ${nativeWrapperLinuxSource}`);
547
891
  }
548
892
  }
549
893
 
550
- // Ensure core binaries are available
551
- await ensureCoreDependencies();
552
894
 
553
895
  // 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)) {
896
+ if ((targetOS === 'macos' && config.build.mac?.bundleCEF) ||
897
+ (targetOS === 'win' && config.build.win?.bundleCEF) ||
898
+ (targetOS === 'linux' && config.build.linux?.bundleCEF)) {
557
899
 
558
- await ensureCEFDependencies();
559
- if (OS === 'macos') {
560
- const cefFrameworkSource = PATHS.CEF_FRAMEWORK_MACOS;
900
+ await ensureCEFDependencies(currentTarget.os, currentTarget.arch);
901
+ if (targetOS === 'macos') {
902
+ const cefFrameworkSource = targetPaths.CEF_FRAMEWORK_MACOS;
561
903
  const cefFrameworkDestination = join(
562
904
  appBundleFolderFrameworksPath,
563
905
  "Chromium Embedded Framework.framework"
@@ -578,7 +920,7 @@ if (commandArg === "init") {
578
920
  "bun Helper (Renderer)",
579
921
  ];
580
922
 
581
- const helperSourcePath = PATHS.CEF_HELPER_MACOS;
923
+ const helperSourcePath = targetPaths.CEF_HELPER_MACOS;
582
924
  cefHelperNames.forEach((helperName) => {
583
925
  const destinationPath = join(
584
926
  appBundleFolderFrameworksPath,
@@ -598,10 +940,9 @@ if (commandArg === "init") {
598
940
  dereference: true,
599
941
  });
600
942
  });
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");
943
+ } else if (targetOS === 'win') {
944
+ // Copy CEF DLLs from platform-specific dist/cef/ to the main executable directory
945
+ const cefSourcePath = targetPaths.CEF_DIR;
605
946
  const cefDllFiles = [
606
947
  'libcef.dll',
607
948
  'chrome_elf.dll',
@@ -641,7 +982,7 @@ if (commandArg === "init") {
641
982
  });
642
983
 
643
984
  // Copy CEF resources to MacOS/cef/ subdirectory for other resources like locales
644
- const cefResourcesSource = join(electrobunDistPath, 'cef');
985
+ const cefResourcesSource = targetPaths.CEF_DIR;
645
986
  const cefResourcesDestination = join(appBundleMacOSPath, 'cef');
646
987
 
647
988
  if (existsSync(cefResourcesSource)) {
@@ -660,7 +1001,7 @@ if (commandArg === "init") {
660
1001
  "bun Helper (Renderer)",
661
1002
  ];
662
1003
 
663
- const helperSourcePath = PATHS.CEF_HELPER_WIN;
1004
+ const helperSourcePath = targetPaths.CEF_HELPER_WIN;
664
1005
  if (existsSync(helperSourcePath)) {
665
1006
  cefHelperNames.forEach((helperName) => {
666
1007
  const destinationPath = join(appBundleMacOSPath, `${helperName}.exe`);
@@ -670,10 +1011,9 @@ if (commandArg === "init") {
670
1011
  } else {
671
1012
  console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
672
1013
  }
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");
1014
+ } else if (targetOS === 'linux') {
1015
+ // Copy CEF shared libraries from platform-specific dist/cef/ to the main executable directory
1016
+ const cefSourcePath = targetPaths.CEF_DIR;
677
1017
 
678
1018
  if (existsSync(cefSourcePath)) {
679
1019
  const cefSoFiles = [
@@ -759,12 +1099,12 @@ if (commandArg === "init") {
759
1099
  "bun Helper (Renderer)",
760
1100
  ];
761
1101
 
762
- const helperSourcePath = PATHS.CEF_HELPER_LINUX;
1102
+ const helperSourcePath = targetPaths.CEF_HELPER_LINUX;
763
1103
  if (existsSync(helperSourcePath)) {
764
1104
  cefHelperNames.forEach((helperName) => {
765
1105
  const destinationPath = join(appBundleMacOSPath, helperName);
766
1106
  cpSync(helperSourcePath, destinationPath);
767
- console.log(`Copied CEF helper: ${helperName}`);
1107
+ // console.log(`Copied CEF helper: ${helperName}`);
768
1108
  });
769
1109
  } else {
770
1110
  console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
@@ -775,7 +1115,7 @@ if (commandArg === "init") {
775
1115
 
776
1116
 
777
1117
  // copy native bindings
778
- const bsPatchSource = PATHS.BSPATCH;
1118
+ const bsPatchSource = targetPaths.BSPATCH;
779
1119
  const bsPatchDestination = join(appBundleMacOSPath, "bspatch") + binExt;
780
1120
  const bsPatchDestFolder = dirname(bsPatchDestination);
781
1121
  if (!existsSync(bsPatchDestFolder)) {
@@ -871,12 +1211,21 @@ if (commandArg === "init") {
871
1211
 
872
1212
  // Run postBuild script
873
1213
  if (config.scripts.postBuild) {
874
-
875
- Bun.spawnSync([bunBinarySourcePath, config.scripts.postBuild], {
1214
+ // Use host platform's bun binary for running scripts, not target platform's
1215
+ const hostPaths = getPlatformPaths(OS, ARCH);
1216
+
1217
+ Bun.spawnSync([hostPaths.BUN_BINARY, config.scripts.postBuild], {
876
1218
  stdio: ["ignore", "inherit", "inherit"],
877
1219
  env: {
878
1220
  ...process.env,
879
1221
  ELECTROBUN_BUILD_ENV: buildEnvironment,
1222
+ ELECTROBUN_OS: targetOS, // Use target OS for environment variables
1223
+ ELECTROBUN_ARCH: targetARCH, // Use target ARCH for environment variables
1224
+ ELECTROBUN_BUILD_DIR: buildFolder,
1225
+ ELECTROBUN_APP_NAME: appFileName,
1226
+ ELECTROBUN_APP_VERSION: config.app.version,
1227
+ ELECTROBUN_APP_IDENTIFIER: config.app.identifier,
1228
+ ELECTROBUN_ARTIFACT_DIR: artifactFolder,
880
1229
  },
881
1230
  });
882
1231
  }
@@ -920,8 +1269,9 @@ if (commandArg === "init") {
920
1269
  );
921
1270
 
922
1271
  // todo (yoav): add these to config
1272
+ // Only codesign/notarize when building macOS targets on macOS host
923
1273
  const shouldCodesign =
924
- buildEnvironment !== "dev" && config.build.mac.codesign;
1274
+ buildEnvironment !== "dev" && targetOS === 'macos' && OS === 'macos' && config.build.mac.codesign;
925
1275
  const shouldNotarize = shouldCodesign && config.build.mac.notarize;
926
1276
 
927
1277
  if (shouldCodesign) {
@@ -956,6 +1306,8 @@ if (commandArg === "init") {
956
1306
  // 6.5. code sign and notarize the dmg
957
1307
  // 7. copy artifacts to directory [self-extractor dmg, zstd app bundle, bsdiff patch, update.json]
958
1308
 
1309
+ // Add platform suffix for all artifacts
1310
+ const platformSuffix = `-${targetOS}-${targetARCH}`;
959
1311
  const tarPath = `${appBundleFolderPath}.tar`;
960
1312
 
961
1313
  // tar the signed and notarized app bundle
@@ -979,7 +1331,8 @@ if (commandArg === "init") {
979
1331
  // than saving 1 more MB of space/bandwidth.
980
1332
 
981
1333
  const compressedTarPath = `${tarPath}.zst`;
982
- artifactsToUpload.push(compressedTarPath);
1334
+ const platformCompressedTarPath = compressedTarPath.replace('.tar.zst', `${platformSuffix}.tar.zst`);
1335
+ artifactsToUpload.push(platformCompressedTarPath);
983
1336
 
984
1337
  // zstd compress tarball
985
1338
  // todo (yoav): consider using c bindings for zstd for speed instead of wasm
@@ -988,23 +1341,28 @@ if (commandArg === "init") {
988
1341
  await ZstdInit().then(async ({ ZstdSimple, ZstdStream }) => {
989
1342
  // Note: Simple is much faster than stream, but stream is better for large files
990
1343
  // todo (yoav): consider a file size cutoff to switch to stream instead of simple.
1344
+ const useStream = tarball.size > 100 * 1024 * 1024;
1345
+
991
1346
  if (tarball.size > 0) {
992
- // Uint8 array filestream of the tar file
1347
+ // Uint8 array filestream of the tar file
993
1348
 
994
1349
  const data = new Uint8Array(tarBuffer);
1350
+
995
1351
  const compressionLevel = 22;
996
1352
  const compressedData = ZstdSimple.compress(data, compressionLevel);
997
1353
 
998
1354
  console.log(
999
1355
  "compressed",
1000
- compressedData.length,
1356
+ data.length,
1001
1357
  "bytes",
1002
1358
  "from",
1003
- data.length,
1359
+ tarBuffer.byteLength,
1004
1360
  "bytes"
1005
1361
  );
1006
1362
 
1007
1363
  await Bun.write(compressedTarPath, compressedData);
1364
+ // Copy to platform-specific filename for upload
1365
+ cpSync(compressedTarPath, platformCompressedTarPath);
1008
1366
  }
1009
1367
  });
1010
1368
 
@@ -1012,7 +1370,7 @@ if (commandArg === "init") {
1012
1370
  // now and it needs the same name as the original app bundle.
1013
1371
  rmdirSync(appBundleFolderPath, { recursive: true });
1014
1372
 
1015
- const selfExtractingBundle = createAppBundle(appFileName, buildFolder);
1373
+ const selfExtractingBundle = createAppBundle(appFileName, buildFolder, targetOS);
1016
1374
  const compressedTarballInExtractingBundlePath = join(
1017
1375
  selfExtractingBundle.appBundleFolderResourcesPath,
1018
1376
  `${hash}.tar.zst`
@@ -1021,7 +1379,7 @@ if (commandArg === "init") {
1021
1379
  // copy the zstd tarball to the self-extracting app bundle
1022
1380
  cpSync(compressedTarPath, compressedTarballInExtractingBundlePath);
1023
1381
 
1024
- const selfExtractorBinSourcePath = PATHS.EXTRACTOR;
1382
+ const selfExtractorBinSourcePath = targetPaths.EXTRACTOR;
1025
1383
  const selfExtractorBinDestinationPath = join(
1026
1384
  selfExtractingBundle.appBundleMacOSPath,
1027
1385
  "launcher"
@@ -1053,28 +1411,49 @@ if (commandArg === "init") {
1053
1411
  console.log("skipping notarization");
1054
1412
  }
1055
1413
 
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
- );
1414
+ // DMG creation for macOS only
1415
+ if (targetOS === 'macos') {
1416
+ console.log("creating dmg...");
1417
+ // make a dmg
1418
+ const dmgPath = join(buildFolder, `${appFileName}.dmg`);
1419
+ const platformDmgPath = join(buildFolder, `${appFileName}${platformSuffix}.dmg`);
1420
+ artifactsToUpload.push(platformDmgPath);
1421
+ // hdiutil create -volname "YourAppName" -srcfolder /path/to/YourApp.app -ov -format UDZO YourAppName.dmg
1422
+ // Note: use ULFO (lzfse) for better compatibility with large CEF frameworks and modern macOS
1423
+ execSync(
1424
+ `hdiutil create -volname "${appFileName}" -srcfolder ${escapePathForTerminal(
1425
+ appBundleFolderPath
1426
+ )} -ov -format ULFO ${escapePathForTerminal(dmgPath)}`
1427
+ );
1067
1428
 
1068
- if (shouldCodesign) {
1069
- codesignAppBundle(dmgPath);
1070
- } else {
1071
- console.log("skipping codesign");
1072
- }
1429
+ if (shouldCodesign) {
1430
+ codesignAppBundle(dmgPath);
1431
+ } else {
1432
+ console.log("skipping codesign");
1433
+ }
1073
1434
 
1074
- if (shouldNotarize) {
1075
- notarizeAndStaple(dmgPath);
1435
+ if (shouldNotarize) {
1436
+ notarizeAndStaple(dmgPath);
1437
+ } else {
1438
+ console.log("skipping notarization");
1439
+ }
1440
+
1441
+ // Copy to platform-specific filename
1442
+ cpSync(dmgPath, platformDmgPath);
1076
1443
  } else {
1077
- console.log("skipping notarization");
1444
+ // For Windows and Linux, add the self-extracting bundle directly
1445
+ const platformBundlePath = join(buildFolder, `${appFileName}${platformSuffix}${targetOS === 'win' ? '.exe' : ''}`);
1446
+ // Copy the self-extracting bundle to platform-specific filename
1447
+ if (targetOS === 'win') {
1448
+ // On Windows, create a self-extracting exe
1449
+ // For now, just copy the bundle folder
1450
+ artifactsToUpload.push(compressedTarPath.replace('.tar.zst', `${platformSuffix}.tar.zst`));
1451
+ } else if (targetOS === 'linux') {
1452
+ // On Linux, create a tar.gz of the bundle
1453
+ const linuxTarPath = join(buildFolder, `${appFileName}${platformSuffix}.tar.gz`);
1454
+ execSync(`tar -czf ${escapePathForTerminal(linuxTarPath)} -C ${escapePathForTerminal(buildFolder)} ${escapePathForTerminal(basename(appBundleFolderPath))}`);
1455
+ artifactsToUpload.push(linuxTarPath);
1456
+ }
1078
1457
  }
1079
1458
 
1080
1459
  // refresh artifacts folder
@@ -1093,39 +1472,49 @@ if (commandArg === "init") {
1093
1472
  // the download button or display on your marketing site or in the app.
1094
1473
  version: config.app.version,
1095
1474
  hash: hash.toString(),
1475
+ platform: OS,
1476
+ arch: ARCH,
1096
1477
  // channel: buildEnvironment,
1097
1478
  // bucketUrl: config.release.bucketUrl
1098
1479
  });
1099
1480
 
1100
- await Bun.write(join(artifactFolder, "update.json"), updateJsonContent);
1481
+ // Platform-specific update.json
1482
+ const platformUpdateJsonName = `update${platformSuffix}.json`;
1483
+ await Bun.write(join(artifactFolder, platformUpdateJsonName), updateJsonContent);
1101
1484
 
1102
1485
  // generate bsdiff
1103
1486
  // https://storage.googleapis.com/eggbun-static/electrobun-playground/canary/ElectrobunPlayground-canary.app.tar.zst
1104
1487
  console.log("bucketUrl: ", config.release.bucketUrl);
1105
1488
 
1106
1489
  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
- });
1490
+
1491
+ // Skip patch generation if bucketUrl is not configured
1492
+ if (!config.release.bucketUrl || config.release.bucketUrl.trim() === '') {
1493
+ console.log("No bucketUrl configured, skipping patch generation");
1494
+ console.log("To enable patch generation, configure bucketUrl in your electrobun.config");
1495
+ } else {
1496
+ const urlToPrevUpdateJson = join(
1497
+ config.release.bucketUrl,
1498
+ buildEnvironment,
1499
+ `update${platformSuffix}.json`
1500
+ );
1501
+ const cacheBuster = Math.random().toString(36).substring(7);
1502
+ const updateJsonResponse = await fetch(
1503
+ urlToPrevUpdateJson + `?${cacheBuster}`
1504
+ ).catch((err) => {
1505
+ console.log("bucketURL not found: ", err);
1506
+ });
1118
1507
 
1119
1508
  const urlToLatestTarball = join(
1120
1509
  config.release.bucketUrl,
1121
1510
  buildEnvironment,
1122
- `${appFileName}.app.tar.zst`
1511
+ `${appFileName}.app${platformSuffix}.tar.zst`
1123
1512
  );
1124
1513
 
1125
1514
 
1126
1515
  // attempt to get the previous version to create a patch file
1127
- if (updateJsonResponse.ok) {
1128
- const prevUpdateJson = await updateJsonResponse.json();
1516
+ if (updateJsonResponse && updateJsonResponse.ok) {
1517
+ const prevUpdateJson = await updateJsonResponse!.json();
1129
1518
 
1130
1519
  const prevHash = prevUpdateJson.hash;
1131
1520
  console.log("PREVIOUS HASH", prevHash);
@@ -1164,9 +1553,10 @@ if (commandArg === "init") {
1164
1553
  console.log("diff previous and new tarballs...");
1165
1554
  // Run it as a separate process to leverage multi-threadedness
1166
1555
  // especially for creating multiple diffs in parallel
1167
- const bsdiffpath = PATHS.BSDIFF;
1556
+ const bsdiffpath = targetPaths.BSDIFF;
1168
1557
  const patchFilePath = join(buildFolder, `${prevHash}.patch`);
1169
- artifactsToUpload.push(patchFilePath);
1558
+ const platformPatchFilePath = join(buildFolder, `${prevHash}${platformSuffix}.patch`);
1559
+ artifactsToUpload.push(platformPatchFilePath);
1170
1560
  const result = Bun.spawnSync(
1171
1561
  [bsdiffpath, prevTarballPath, tarPath, patchFilePath, "--use-zstd"],
1172
1562
  { cwd: buildFolder }
@@ -1176,11 +1566,14 @@ if (commandArg === "init") {
1176
1566
  result.stdout.toString(),
1177
1567
  result.stderr.toString()
1178
1568
  );
1569
+ // Copy to platform-specific filename
1570
+ cpSync(patchFilePath, platformPatchFilePath);
1179
1571
  }
1180
1572
  } else {
1181
1573
  console.log("prevoius version not found at: ", urlToLatestTarball);
1182
1574
  console.log("skipping diff generation");
1183
1575
  }
1576
+ } // End of bucketUrl validation block
1184
1577
 
1185
1578
  // compress all the upload files
1186
1579
  console.log("copying artifacts...");
@@ -1209,10 +1602,7 @@ if (commandArg === "init") {
1209
1602
  // todo (yoav): rename to start
1210
1603
 
1211
1604
  // 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
1605
+ // this runs the bundled bun binary with main.js directly
1216
1606
 
1217
1607
  // Note: this cli will be a bun single-file-executable
1218
1608
  // Note: we want to use the version of bun that's packaged with electrobun
@@ -1290,7 +1680,12 @@ function getConfig() {
1290
1680
  if (existsSync(configPath)) {
1291
1681
  const configFileContents = readFileSync(configPath, "utf8");
1292
1682
  // Note: we want this to hard fail if there's a syntax error
1293
- loadedConfig = JSON.parse(configFileContents);
1683
+ try {
1684
+ loadedConfig = JSON.parse(configFileContents);
1685
+ } catch (error) {
1686
+ console.error("Failed to parse config file:", error);
1687
+ console.error("using default config instead");
1688
+ }
1294
1689
  }
1295
1690
 
1296
1691
  // todo (yoav): write a deep clone fn
@@ -1312,6 +1707,18 @@ function getConfig() {
1312
1707
  ...(loadedConfig?.build?.mac?.entitlements || {}),
1313
1708
  },
1314
1709
  },
1710
+ win: {
1711
+ ...defaultConfig.build.win,
1712
+ ...(loadedConfig?.build?.win || {}),
1713
+ },
1714
+ linux: {
1715
+ ...defaultConfig.build.linux,
1716
+ ...(loadedConfig?.build?.linux || {}),
1717
+ },
1718
+ bun: {
1719
+ ...defaultConfig.build.bun,
1720
+ ...(loadedConfig?.build?.bun || {}),
1721
+ }
1315
1722
  },
1316
1723
  scripts: {
1317
1724
  ...defaultConfig.scripts,
@@ -1473,8 +1880,8 @@ function notarizeAndStaple(appOrDmgPath: string) {
1473
1880
  // 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
1881
  // either way you can pass in the parent folder here for that flexibility.
1475
1882
  // 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') {
1883
+ function createAppBundle(bundleName: string, parentFolder: string, targetOS: 'macos' | 'win' | 'linux') {
1884
+ if (targetOS === 'macos') {
1478
1885
  // macOS bundle structure
1479
1886
  const bundleFileName = `${bundleName}.app`;
1480
1887
  const appBundleFolderPath = join(parentFolder, bundleFileName);
@@ -1501,7 +1908,7 @@ function createAppBundle(bundleName: string, parentFolder: string) {
1501
1908
  appBundleFolderResourcesPath,
1502
1909
  appBundleFolderFrameworksPath,
1503
1910
  };
1504
- } else if (OS === 'linux' || OS === 'win') {
1911
+ } else if (targetOS === 'linux' || targetOS === 'win') {
1505
1912
  // Linux/Windows simpler structure
1506
1913
  const appBundleFolderPath = join(parentFolder, bundleName);
1507
1914
  const appBundleFolderContentsPath = appBundleFolderPath; // No Contents folder needed
@@ -1522,6 +1929,6 @@ function createAppBundle(bundleName: string, parentFolder: string) {
1522
1929
  appBundleFolderFrameworksPath,
1523
1930
  };
1524
1931
  } else {
1525
- throw new Error(`Unsupported OS: ${OS}`);
1932
+ throw new Error(`Unsupported OS: ${targetOS}`);
1526
1933
  }
1527
1934
  }