electrobun 0.0.19-beta.58 → 0.0.19-beta.60

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli/index.ts +456 -168
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrobun",
3
- "version": "0.0.19-beta.58",
3
+ "version": "0.0.19-beta.60",
4
4
  "description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
5
5
  "license": "MIT",
6
6
  "author": "Blackboard Technologies Inc.",
package/src/cli/index.ts CHANGED
@@ -65,56 +65,81 @@ const ELECTROBUN_DEP_PATH = join(projectRoot, "node_modules", "electrobun");
65
65
  // When debugging electrobun with the example app use the builds (dev or release) right from the source folder
66
66
  // For developers using electrobun cli via npm use the release versions in /dist
67
67
  // This lets us not have to commit src build folders to git and provide pre-built binaries
68
- const PATHS = {
69
- BUN_BINARY: join(ELECTROBUN_DEP_PATH, "dist", "bun") + binExt,
70
- LAUNCHER_DEV: join(ELECTROBUN_DEP_PATH, "dist", "electrobun") + binExt,
71
- LAUNCHER_RELEASE: join(ELECTROBUN_DEP_PATH, "dist", "launcher") + binExt,
72
- MAIN_JS: join(ELECTROBUN_DEP_PATH, "dist", "main.js"),
73
- NATIVE_WRAPPER_MACOS: join(
74
- ELECTROBUN_DEP_PATH,
75
- "dist",
76
- "libNativeWrapper.dylib"
77
- ),
78
- NATIVE_WRAPPER_WIN: join(ELECTROBUN_DEP_PATH, "dist", "libNativeWrapper.dll"),
79
- NATIVE_WRAPPER_LINUX: join(ELECTROBUN_DEP_PATH, "dist", "libNativeWrapper.so"),
80
- NATIVE_WRAPPER_LINUX_CEF: join(ELECTROBUN_DEP_PATH, "dist", "libNativeWrapper_cef.so"),
81
- WEBVIEW2LOADER_WIN: join(ELECTROBUN_DEP_PATH, "dist", "WebView2Loader.dll"),
82
- BSPATCH: join(ELECTROBUN_DEP_PATH, "dist", "bspatch") + binExt,
83
- EXTRACTOR: join(ELECTROBUN_DEP_PATH, "dist", "extractor") + binExt,
84
- BSDIFF: join(ELECTROBUN_DEP_PATH, "dist", "bsdiff") + binExt,
85
- CEF_FRAMEWORK_MACOS: join(
86
- ELECTROBUN_DEP_PATH,
87
- "dist",
88
- "cef",
89
- "Chromium Embedded Framework.framework"
90
- ),
91
- CEF_HELPER_MACOS: join(ELECTROBUN_DEP_PATH, "dist", "cef", "process_helper"),
92
- CEF_HELPER_WIN: join(ELECTROBUN_DEP_PATH, "dist", "cef", "process_helper.exe"),
93
- CEF_HELPER_LINUX: join(ELECTROBUN_DEP_PATH, "dist", "cef", "process_helper"),
94
- CEF_DIR: join(ELECTROBUN_DEP_PATH, "dist", "cef"),
95
- };
96
68
 
97
- async function ensureCoreDependencies() {
98
- // Check if all core dependencies exist
99
- const requiredFiles = [
100
- PATHS.BUN_BINARY,
101
- PATHS.LAUNCHER_RELEASE,
102
- PATHS.MAIN_JS,
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,
103
116
  // Platform-specific native wrapper
104
- OS === 'macos' ? PATHS.NATIVE_WRAPPER_MACOS :
105
- OS === 'win' ? PATHS.NATIVE_WRAPPER_WIN :
106
- PATHS.NATIVE_WRAPPER_LINUX
117
+ platformOS === 'macos' ? platformPaths.NATIVE_WRAPPER_MACOS :
118
+ platformOS === 'win' ? platformPaths.NATIVE_WRAPPER_WIN :
119
+ platformPaths.NATIVE_WRAPPER_LINUX
107
120
  ];
108
121
 
109
- const allFilesExist = requiredFiles.every(file => existsSync(file));
110
- if (allFilesExist) {
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) {
111
137
  return;
112
138
  }
113
139
 
114
- // Show which files are missing
115
- const missingFiles = requiredFiles.filter(file => !existsSync(file));
116
- console.log('Core dependencies not found. Missing files:', missingFiles.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', '));
117
- console.log('Downloading core binaries...');
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}...`);
118
143
 
119
144
  // Get the current Electrobun version from package.json
120
145
  const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
@@ -129,8 +154,8 @@ async function ensureCoreDependencies() {
129
154
  }
130
155
  }
131
156
 
132
- const platformName = OS === 'macos' ? 'darwin' : OS === 'win' ? 'win' : 'linux';
133
- const archName = ARCH;
157
+ const platformName = platformOS === 'macos' ? 'darwin' : platformOS === 'win' ? 'win' : 'linux';
158
+ const archName = platformArch;
134
159
  const coreTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-core-${platformName}-${archName}.tar.gz`;
135
160
 
136
161
  console.log(`Downloading core binaries from: ${coreTarballUrl}`);
@@ -143,7 +168,7 @@ async function ensureCoreDependencies() {
143
168
  }
144
169
 
145
170
  // Create temp file
146
- const tempFile = join(ELECTROBUN_DEP_PATH, 'main-temp.tar.gz');
171
+ const tempFile = join(ELECTROBUN_DEP_PATH, `core-${platformOS}-${platformArch}-temp.tar.gz`);
147
172
  const fileStream = createWriteStream(tempFile);
148
173
 
149
174
  // Write response to file
@@ -157,73 +182,102 @@ async function ensureCoreDependencies() {
157
182
  }
158
183
  fileStream.end();
159
184
 
160
- // Extract to dist directory
161
- console.log('Extracting core dependencies...');
162
- const distPath = join(ELECTROBUN_DEP_PATH, 'dist');
163
- mkdirSync(distPath, { recursive: true });
185
+ // Extract to platform-specific dist directory
186
+ console.log(`Extracting core dependencies for ${platformOS}-${platformArch}...`);
187
+ const platformDistPath = join(ELECTROBUN_DEP_PATH, `dist-${platformOS}-${platformArch}`);
188
+ mkdirSync(platformDistPath, { recursive: true });
164
189
 
165
190
  // Use Windows native tar.exe on Windows due to npm tar library issues
166
191
  if (OS === 'win') {
167
192
  console.log('Using Windows native tar.exe for reliable extraction...');
168
- execSync(`tar -xf "${tempFile}" -C "${distPath}"`, {
193
+ execSync(`tar -xf "${tempFile}" -C "${platformDistPath}"`, {
169
194
  stdio: 'inherit',
170
- cwd: distPath
195
+ cwd: platformDistPath
171
196
  });
172
197
  } else {
173
198
  await tar.x({
174
199
  file: tempFile,
175
- cwd: distPath,
200
+ cwd: platformDistPath,
176
201
  preservePaths: false,
177
202
  strip: 0,
178
203
  });
179
204
  }
180
205
 
206
+ // NOTE: We no longer copy main.js from platform-specific downloads
207
+ // Platform-specific downloads should only contain native binaries
208
+ // main.js and api/ should be shipped via npm in the shared dist/ folder
209
+
181
210
  // Clean up temp file
182
211
  unlinkSync(tempFile);
183
212
 
184
- // Verify extraction completed successfully
185
- const mainJsPath = join(ELECTROBUN_DEP_PATH, 'dist', 'main.js');
186
- if (!existsSync(mainJsPath)) {
187
- console.error('Warning: main.js was not extracted properly');
188
- console.error('This may be caused by Windows Defender or antivirus software quarantining .js files');
189
- console.error('Try temporarily disabling real-time protection or adding an exclusion for the electrobun directory');
213
+ // Debug: List what was actually extracted
214
+ try {
215
+ const extractedFiles = readdirSync(platformDistPath);
216
+ console.log(`Extracted files to ${platformDistPath}:`, extractedFiles);
190
217
 
191
- // List what was actually extracted for debugging
192
- try {
193
- const extractedFiles = readdirSync(join(ELECTROBUN_DEP_PATH, 'dist'));
194
- console.log('Extracted files:', extractedFiles);
195
-
196
- // Check if API directory exists but is empty
197
- const apiPath = join(ELECTROBUN_DEP_PATH, 'dist', 'api');
198
- if (existsSync(apiPath)) {
199
- const apiFiles = readdirSync(apiPath);
200
- console.log('API directory contents:', apiFiles);
201
- if (apiFiles.length === 0) {
202
- console.error('API directory is empty - this confirms antivirus is likely quarantining script files');
203
- }
218
+ // Check if files are in subdirectories
219
+ for (const file of extractedFiles) {
220
+ const filePath = join(platformDistPath, file);
221
+ const stat = require('fs').statSync(filePath);
222
+ if (stat.isDirectory()) {
223
+ const subFiles = readdirSync(filePath);
224
+ console.log(` ${file}/: ${subFiles.join(', ')}`);
204
225
  }
205
- } catch (e) {
206
- console.error('Could not list extracted files');
207
226
  }
227
+ } catch (e) {
228
+ console.error('Could not list extracted files:', e);
229
+ }
230
+
231
+ // Verify extraction completed successfully - check platform-specific binaries only
232
+ const requiredBinaries = [
233
+ platformPaths.BUN_BINARY,
234
+ platformPaths.LAUNCHER_RELEASE,
235
+ platformOS === 'macos' ? platformPaths.NATIVE_WRAPPER_MACOS :
236
+ platformOS === 'win' ? platformPaths.NATIVE_WRAPPER_WIN :
237
+ platformPaths.NATIVE_WRAPPER_LINUX
238
+ ];
239
+
240
+ const missingBinaries = requiredBinaries.filter(file => !existsSync(file));
241
+ if (missingBinaries.length > 0) {
242
+ console.error(`Missing binaries after extraction: ${missingBinaries.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', ')}`);
243
+ console.error('This suggests the tarball structure is different than expected');
208
244
  }
209
245
 
210
- console.log('Core dependencies downloaded and cached successfully');
246
+ // For development: if main.js doesn't exist in shared dist/, copy from platform-specific download as fallback
247
+ const sharedDistPath = join(ELECTROBUN_DEP_PATH, 'dist');
248
+ const extractedMainJs = join(platformDistPath, 'main.js');
249
+ const sharedMainJs = join(sharedDistPath, 'main.js');
211
250
 
212
- } catch (error) {
213
- console.error('Failed to download core dependencies:', error.message);
251
+ if (existsSync(extractedMainJs) && !existsSync(sharedMainJs)) {
252
+ console.log('Development fallback: copying main.js from platform-specific download to shared dist/');
253
+ mkdirSync(sharedDistPath, { recursive: true });
254
+ cpSync(extractedMainJs, sharedMainJs);
255
+ }
256
+
257
+ console.log(`Core dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
258
+
259
+ } catch (error: any) {
260
+ console.error(`Failed to download core dependencies for ${platformOS}-${platformArch}:`, error.message);
214
261
  console.error('Please ensure you have an internet connection and the release exists.');
215
262
  process.exit(1);
216
263
  }
217
264
  }
218
265
 
219
- async function ensureCEFDependencies() {
266
+ async function ensureCEFDependencies(targetOS?: 'macos' | 'win' | 'linux', targetArch?: 'arm64' | 'x64') {
267
+ // Use provided target platform or default to host platform
268
+ const platformOS = targetOS || OS;
269
+ const platformArch = targetArch || ARCH;
270
+
271
+ // Get platform-specific paths
272
+ const platformPaths = getPlatformPaths(platformOS, platformArch);
273
+
220
274
  // Check if CEF dependencies already exist
221
- if (existsSync(PATHS.CEF_DIR)) {
222
- console.log('CEF dependencies found, using cached version');
275
+ if (existsSync(platformPaths.CEF_DIR)) {
276
+ console.log(`CEF dependencies found for ${platformOS}-${platformArch}, using cached version`);
223
277
  return;
224
278
  }
225
279
 
226
- console.log('CEF dependencies not found, downloading...');
280
+ console.log(`CEF dependencies not found for ${platformOS}-${platformArch}, downloading...`);
227
281
 
228
282
  // Get the current Electrobun version from package.json
229
283
  const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
@@ -238,8 +292,8 @@ async function ensureCEFDependencies() {
238
292
  }
239
293
  }
240
294
 
241
- const platformName = OS === 'macos' ? 'darwin' : OS === 'win' ? 'win' : 'linux';
242
- const archName = ARCH;
295
+ const platformName = platformOS === 'macos' ? 'darwin' : platformOS === 'win' ? 'win' : 'linux';
296
+ const archName = platformArch;
243
297
  const cefTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-cef-${platformName}-${archName}.tar.gz`;
244
298
 
245
299
  console.log(`Downloading CEF from: ${cefTarballUrl}`);
@@ -252,7 +306,7 @@ async function ensureCEFDependencies() {
252
306
  }
253
307
 
254
308
  // Create temp file
255
- const tempFile = join(ELECTROBUN_DEP_PATH, 'cef-temp.tar.gz');
309
+ const tempFile = join(ELECTROBUN_DEP_PATH, `cef-${platformOS}-${platformArch}-temp.tar.gz`);
256
310
  const fileStream = createWriteStream(tempFile);
257
311
 
258
312
  // Write response to file
@@ -266,21 +320,22 @@ async function ensureCEFDependencies() {
266
320
  }
267
321
  fileStream.end();
268
322
 
269
- // Extract to dist directory
270
- console.log('Extracting CEF dependencies...');
271
- const cefDistPath = join(ELECTROBUN_DEP_PATH, 'dist');
323
+ // Extract to platform-specific dist directory
324
+ console.log(`Extracting CEF dependencies for ${platformOS}-${platformArch}...`);
325
+ const platformDistPath = join(ELECTROBUN_DEP_PATH, `dist-${platformOS}-${platformArch}`);
326
+ mkdirSync(platformDistPath, { recursive: true });
272
327
 
273
328
  // Use Windows native tar.exe on Windows due to npm tar library issues
274
329
  if (OS === 'win') {
275
330
  console.log('Using Windows native tar.exe for reliable extraction...');
276
- execSync(`tar -xf "${tempFile}" -C "${cefDistPath}"`, {
331
+ execSync(`tar -xf "${tempFile}" -C "${platformDistPath}"`, {
277
332
  stdio: 'inherit',
278
- cwd: cefDistPath
333
+ cwd: platformDistPath
279
334
  });
280
335
  } else {
281
336
  await tar.x({
282
337
  file: tempFile,
283
- cwd: cefDistPath,
338
+ cwd: platformDistPath,
284
339
  preservePaths: false,
285
340
  strip: 0,
286
341
  });
@@ -289,10 +344,25 @@ async function ensureCEFDependencies() {
289
344
  // Clean up temp file
290
345
  unlinkSync(tempFile);
291
346
 
292
- console.log('CEF dependencies downloaded and cached successfully');
347
+ // Debug: List what was actually extracted for CEF
348
+ try {
349
+ const extractedFiles = readdirSync(platformDistPath);
350
+ console.log(`CEF extracted files to ${platformDistPath}:`, extractedFiles);
351
+
352
+ // Check if CEF directory was created
353
+ const cefDir = join(platformDistPath, 'cef');
354
+ if (existsSync(cefDir)) {
355
+ const cefFiles = readdirSync(cefDir);
356
+ console.log(`CEF directory contents: ${cefFiles.slice(0, 10).join(', ')}${cefFiles.length > 10 ? '...' : ''}`);
357
+ }
358
+ } catch (e) {
359
+ console.error('Could not list CEF extracted files:', e);
360
+ }
361
+
362
+ console.log(`CEF dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
293
363
 
294
- } catch (error) {
295
- console.error('Failed to download CEF dependencies:', error.message);
364
+ } catch (error: any) {
365
+ console.error(`Failed to download CEF dependencies for ${platformOS}-${platformArch}:`, error.message);
296
366
  console.error('Please ensure you have an internet connection and the release exists.');
297
367
  process.exit(1);
298
368
  }
@@ -327,6 +397,7 @@ const defaultConfig = {
327
397
  build: {
328
398
  buildFolder: "build",
329
399
  artifactFolder: "artifacts",
400
+ targets: undefined, // Will default to current platform if not specified
330
401
  mac: {
331
402
  codesign: false,
332
403
  notarize: false,
@@ -360,7 +431,10 @@ if (!command) {
360
431
  const config = getConfig();
361
432
 
362
433
  const envArg =
363
- process.argv.find((arg) => arg.startsWith("env="))?.split("=")[1] || "";
434
+ process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
435
+
436
+ const targetsArg =
437
+ process.argv.find((arg) => arg.startsWith("--targets="))?.split("=")[1] || "";
364
438
 
365
439
  const validEnvironments = ["dev", "canary", "stable"];
366
440
 
@@ -368,8 +442,175 @@ const validEnvironments = ["dev", "canary", "stable"];
368
442
  const buildEnvironment: "dev" | "canary" | "stable" =
369
443
  validEnvironments.includes(envArg) ? envArg : "dev";
370
444
 
445
+ // Determine build targets
446
+ type BuildTarget = { os: 'macos' | 'win' | 'linux', arch: 'arm64' | 'x64' };
447
+
448
+ function parseBuildTargets(): BuildTarget[] {
449
+ // If explicit targets provided via CLI
450
+ if (targetsArg) {
451
+ if (targetsArg === 'current') {
452
+ return [{ os: OS, arch: ARCH }];
453
+ } else if (targetsArg === 'all') {
454
+ return parseConfigTargets();
455
+ } else {
456
+ // Parse comma-separated targets like "macos-arm64,win-x64"
457
+ return targetsArg.split(',').map(target => {
458
+ const [os, arch] = target.trim().split('-') as [string, string];
459
+ if (!['macos', 'win', 'linux'].includes(os) || !['arm64', 'x64'].includes(arch)) {
460
+ console.error(`Invalid target: ${target}. Format should be: os-arch (e.g., macos-arm64)`);
461
+ process.exit(1);
462
+ }
463
+ return { os, arch } as BuildTarget;
464
+ });
465
+ }
466
+ }
467
+
468
+ // Default behavior: always build for current platform only
469
+ // This ensures predictable, fast builds unless explicitly requesting multi-platform
470
+ return [{ os: OS, arch: ARCH }];
471
+ }
472
+
473
+ function parseConfigTargets(): BuildTarget[] {
474
+ // If config has targets, use them
475
+ if (config.build.targets && config.build.targets.length > 0) {
476
+ return config.build.targets.map(target => {
477
+ if (target === 'current') {
478
+ return { os: OS, arch: ARCH };
479
+ }
480
+ const [os, arch] = target.split('-') as [string, string];
481
+ if (!['macos', 'win', 'linux'].includes(os) || !['arm64', 'x64'].includes(arch)) {
482
+ console.error(`Invalid target in config: ${target}. Format should be: os-arch (e.g., macos-arm64)`);
483
+ process.exit(1);
484
+ }
485
+ return { os, arch } as BuildTarget;
486
+ });
487
+ }
488
+
489
+ // If no config targets and --targets=all, use all available platforms
490
+ if (targetsArg === 'all') {
491
+ console.log('No targets specified in config, using all available platforms');
492
+ return [
493
+ { os: 'macos', arch: 'arm64' },
494
+ { os: 'macos', arch: 'x64' },
495
+ { os: 'win', arch: 'x64' },
496
+ { os: 'linux', arch: 'x64' },
497
+ { os: 'linux', arch: 'arm64' }
498
+ ];
499
+ }
500
+
501
+ // Default to current platform
502
+ return [{ os: OS, arch: ARCH }];
503
+ }
504
+
505
+ const buildTargets = parseBuildTargets();
506
+
507
+ // Show build targets to user
508
+ if (buildTargets.length === 1) {
509
+ console.log(`Building for ${buildTargets[0].os}-${buildTargets[0].arch} (${buildEnvironment})`);
510
+ } else {
511
+ const targetList = buildTargets.map(t => `${t.os}-${t.arch}`).join(', ');
512
+ console.log(`Building for multiple targets: ${targetList} (${buildEnvironment})`);
513
+ console.log(`Running ${buildTargets.length} parallel builds...`);
514
+
515
+ // Spawn parallel build processes
516
+ const buildPromises = buildTargets.map(async (target) => {
517
+ const targetString = `${target.os}-${target.arch}`;
518
+ const prefix = `[${targetString}]`;
519
+
520
+ try {
521
+ // Try to find the electrobun binary in node_modules/.bin or use bunx
522
+ const electrobunBin = join(projectRoot, 'node_modules', '.bin', 'electrobun');
523
+ let command: string[];
524
+
525
+ if (existsSync(electrobunBin)) {
526
+ command = [electrobunBin, 'build', `--env=${buildEnvironment}`, `--targets=${targetString}`];
527
+ } else {
528
+ // Fallback to bunx which should resolve node_modules binaries
529
+ command = ['bunx', 'electrobun', 'build', `--env=${buildEnvironment}`, `--targets=${targetString}`];
530
+ }
531
+
532
+ console.log(`${prefix} Running:`, command.join(' '));
533
+
534
+ const result = await Bun.spawn(command, {
535
+ stdio: ['inherit', 'pipe', 'pipe'],
536
+ env: process.env,
537
+ cwd: projectRoot // Ensure we're in the right directory
538
+ });
539
+
540
+ // Pipe output with prefix
541
+ if (result.stdout) {
542
+ const reader = result.stdout.getReader();
543
+ while (true) {
544
+ const { done, value } = await reader.read();
545
+ if (done) break;
546
+ const text = new TextDecoder().decode(value);
547
+ // Add prefix to each line
548
+ const prefixedText = text.split('\n').map(line =>
549
+ line ? `${prefix} ${line}` : line
550
+ ).join('\n');
551
+ process.stdout.write(prefixedText);
552
+ }
553
+ }
554
+
555
+ if (result.stderr) {
556
+ const reader = result.stderr.getReader();
557
+ while (true) {
558
+ const { done, value } = await reader.read();
559
+ if (done) break;
560
+ const text = new TextDecoder().decode(value);
561
+ const prefixedText = text.split('\n').map(line =>
562
+ line ? `${prefix} ${line}` : line
563
+ ).join('\n');
564
+ process.stderr.write(prefixedText);
565
+ }
566
+ }
567
+
568
+ const exitCode = await result.exited;
569
+ return { target, exitCode, success: exitCode === 0 };
570
+
571
+ } catch (error) {
572
+ console.error(`${prefix} Failed to start build:`, error);
573
+ return { target, exitCode: 1, success: false, error };
574
+ }
575
+ });
576
+
577
+ // Wait for all builds to complete
578
+ const results = await Promise.allSettled(buildPromises);
579
+
580
+ // Report final results
581
+ console.log('\n=== Build Results ===');
582
+ let allSucceeded = true;
583
+
584
+ for (const result of results) {
585
+ if (result.status === 'fulfilled') {
586
+ const { target, success, exitCode } = result.value;
587
+ const status = success ? '✅ SUCCESS' : '❌ FAILED';
588
+ console.log(`${target.os}-${target.arch}: ${status} (exit code: ${exitCode})`);
589
+ if (!success) allSucceeded = false;
590
+ } else {
591
+ console.log(`Build rejected: ${result.reason}`);
592
+ allSucceeded = false;
593
+ }
594
+ }
595
+
596
+ if (!allSucceeded) {
597
+ console.log('\nSome builds failed. Check the output above for details.');
598
+ process.exit(1);
599
+ } else {
600
+ console.log('\nAll builds completed successfully! 🎉');
601
+ }
602
+
603
+ process.exit(0);
604
+ }
605
+
371
606
  // todo (yoav): dev builds should include the branch name, and/or allow configuration via external config
372
- const buildSubFolder = `${buildEnvironment}`;
607
+ // For now, assume single target build (we'll refactor for multi-target later)
608
+ const currentTarget = buildTargets[0];
609
+ const buildSubFolder = `${buildEnvironment}-${currentTarget.os}-${currentTarget.arch}`;
610
+
611
+ // Use target OS/ARCH for build logic (instead of current machine's OS/ARCH)
612
+ const targetOS = currentTarget.os;
613
+ const targetARCH = currentTarget.arch;
373
614
 
374
615
  const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
375
616
 
@@ -444,7 +685,7 @@ const appFileName = (
444
685
  )
445
686
  .replace(/\s/g, "")
446
687
  .replace(/\./g, "-");
447
- const bundleFileName = OS === 'macos' ? `${appFileName}.app` : appFileName;
688
+ const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName;
448
689
 
449
690
  // const logPath = `/Library/Logs/Electrobun/ExampleApp/dev/out.log`;
450
691
 
@@ -454,8 +695,11 @@ if (commandArg === "init") {
454
695
  // todo (yoav): init a repo folder structure
455
696
  console.log("initializing electrobun project");
456
697
  } else if (commandArg === "build") {
457
- // Ensure core binaries are available before starting build
458
- await ensureCoreDependencies();
698
+ // Ensure core binaries are available for the target platform before starting build
699
+ await ensureCoreDependencies(currentTarget.os, currentTarget.arch);
700
+
701
+ // Get platform-specific paths for the current target
702
+ const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);
459
703
 
460
704
  // refresh build folder
461
705
  if (existsSync(buildFolder)) {
@@ -480,7 +724,7 @@ if (commandArg === "init") {
480
724
  appBundleMacOSPath,
481
725
  appBundleFolderResourcesPath,
482
726
  appBundleFolderFrameworksPath,
483
- } = createAppBundle(appFileName, buildFolder);
727
+ } = createAppBundle(appFileName, buildFolder, targetOS);
484
728
 
485
729
  const appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
486
730
 
@@ -552,7 +796,7 @@ if (commandArg === "init") {
552
796
  // cpSync(zigLauncherBinarySource, zigLauncherDestination, {recursive: true, dereference: true});
553
797
  // For dev builds, use the actual CLI binary that's currently running
554
798
  // It could be in .cache (npm install) or bin (local dev)
555
- let devLauncherPath = PATHS.LAUNCHER_DEV;
799
+ let devLauncherPath = targetPaths.LAUNCHER_DEV;
556
800
  if (buildEnvironment === "dev" && !existsSync(devLauncherPath)) {
557
801
  // Check .cache location (npm installed)
558
802
  const cachePath = join(ELECTROBUN_DEP_PATH, ".cache", "electrobun") + binExt;
@@ -571,7 +815,7 @@ if (commandArg === "init") {
571
815
  buildEnvironment === "dev"
572
816
  ? devLauncherPath
573
817
  : // Note: for release use the zig launcher optimized for smol size
574
- PATHS.LAUNCHER_RELEASE;
818
+ targetPaths.LAUNCHER_RELEASE;
575
819
  const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + binExt;
576
820
  const destLauncherFolder = dirname(bunCliLauncherDestination);
577
821
  if (!existsSync(destLauncherFolder)) {
@@ -584,11 +828,11 @@ if (commandArg === "init") {
584
828
  dereference: true,
585
829
  });
586
830
 
587
- cpSync(PATHS.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
831
+ cpSync(targetPaths.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
588
832
 
589
833
  // Bun runtime binary
590
834
  // todo (yoav): this only works for the current architecture
591
- const bunBinarySourcePath = PATHS.BUN_BINARY;
835
+ const bunBinarySourcePath = targetPaths.BUN_BINARY;
592
836
  // Note: .bin/bun binary in node_modules is a symlink to the versioned one in another place
593
837
  // in node_modules, so we have to dereference here to get the actual binary in the bundle.
594
838
  const bunBinaryDestInBundlePath = join(appBundleMacOSPath, "bun") + binExt;
@@ -600,8 +844,8 @@ if (commandArg === "init") {
600
844
  cpSync(bunBinarySourcePath, bunBinaryDestInBundlePath, { dereference: true });
601
845
 
602
846
  // copy native wrapper dynamic library
603
- if (OS === 'macos') {
604
- const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_MACOS;
847
+ if (targetOS === 'macos') {
848
+ const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_MACOS;
605
849
  const nativeWrapperMacosDestination = join(
606
850
  appBundleMacOSPath,
607
851
  "libNativeWrapper.dylib"
@@ -609,8 +853,8 @@ if (commandArg === "init") {
609
853
  cpSync(nativeWrapperMacosSource, nativeWrapperMacosDestination, {
610
854
  dereference: true,
611
855
  });
612
- } else if (OS === 'win') {
613
- const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_WIN;
856
+ } else if (targetOS === 'win') {
857
+ const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_WIN;
614
858
  const nativeWrapperMacosDestination = join(
615
859
  appBundleMacOSPath,
616
860
  "libNativeWrapper.dll"
@@ -619,7 +863,7 @@ if (commandArg === "init") {
619
863
  dereference: true,
620
864
  });
621
865
 
622
- const webview2LibSource = PATHS.WEBVIEW2LOADER_WIN;
866
+ const webview2LibSource = targetPaths.WEBVIEW2LOADER_WIN;
623
867
  const webview2LibDestination = join(
624
868
  appBundleMacOSPath,
625
869
  "WebView2Loader.dll"
@@ -627,10 +871,10 @@ if (commandArg === "init") {
627
871
  // copy webview2 system webview library
628
872
  cpSync(webview2LibSource, webview2LibDestination);
629
873
 
630
- } else if (OS === 'linux') {
874
+ } else if (targetOS === 'linux') {
631
875
  // Choose the appropriate native wrapper based on bundleCEF setting
632
876
  const useCEF = config.build.linux?.bundleCEF;
633
- const nativeWrapperLinuxSource = useCEF ? PATHS.NATIVE_WRAPPER_LINUX_CEF : PATHS.NATIVE_WRAPPER_LINUX;
877
+ const nativeWrapperLinuxSource = useCEF ? targetPaths.NATIVE_WRAPPER_LINUX_CEF : targetPaths.NATIVE_WRAPPER_LINUX;
634
878
  const nativeWrapperLinuxDestination = join(
635
879
  appBundleMacOSPath,
636
880
  "libNativeWrapper.so"
@@ -648,13 +892,13 @@ if (commandArg === "init") {
648
892
 
649
893
 
650
894
  // Download CEF binaries if needed when bundleCEF is enabled
651
- if ((OS === 'macos' && config.build.mac?.bundleCEF) ||
652
- (OS === 'win' && config.build.win?.bundleCEF) ||
653
- (OS === 'linux' && config.build.linux?.bundleCEF)) {
895
+ if ((targetOS === 'macos' && config.build.mac?.bundleCEF) ||
896
+ (targetOS === 'win' && config.build.win?.bundleCEF) ||
897
+ (targetOS === 'linux' && config.build.linux?.bundleCEF)) {
654
898
 
655
- await ensureCEFDependencies();
656
- if (OS === 'macos') {
657
- const cefFrameworkSource = PATHS.CEF_FRAMEWORK_MACOS;
899
+ await ensureCEFDependencies(currentTarget.os, currentTarget.arch);
900
+ if (targetOS === 'macos') {
901
+ const cefFrameworkSource = targetPaths.CEF_FRAMEWORK_MACOS;
658
902
  const cefFrameworkDestination = join(
659
903
  appBundleFolderFrameworksPath,
660
904
  "Chromium Embedded Framework.framework"
@@ -675,7 +919,7 @@ if (commandArg === "init") {
675
919
  "bun Helper (Renderer)",
676
920
  ];
677
921
 
678
- const helperSourcePath = PATHS.CEF_HELPER_MACOS;
922
+ const helperSourcePath = targetPaths.CEF_HELPER_MACOS;
679
923
  cefHelperNames.forEach((helperName) => {
680
924
  const destinationPath = join(
681
925
  appBundleFolderFrameworksPath,
@@ -695,10 +939,9 @@ if (commandArg === "init") {
695
939
  dereference: true,
696
940
  });
697
941
  });
698
- } else if (OS === 'win') {
699
- // Copy CEF DLLs from dist/cef/ to the main executable directory
700
- const electrobunDistPath = join(ELECTROBUN_DEP_PATH, "dist");
701
- const cefSourcePath = join(electrobunDistPath, "cef");
942
+ } else if (targetOS === 'win') {
943
+ // Copy CEF DLLs from platform-specific dist/cef/ to the main executable directory
944
+ const cefSourcePath = targetPaths.CEF_DIR;
702
945
  const cefDllFiles = [
703
946
  'libcef.dll',
704
947
  'chrome_elf.dll',
@@ -738,7 +981,7 @@ if (commandArg === "init") {
738
981
  });
739
982
 
740
983
  // Copy CEF resources to MacOS/cef/ subdirectory for other resources like locales
741
- const cefResourcesSource = join(electrobunDistPath, 'cef');
984
+ const cefResourcesSource = targetPaths.CEF_DIR;
742
985
  const cefResourcesDestination = join(appBundleMacOSPath, 'cef');
743
986
 
744
987
  if (existsSync(cefResourcesSource)) {
@@ -757,7 +1000,7 @@ if (commandArg === "init") {
757
1000
  "bun Helper (Renderer)",
758
1001
  ];
759
1002
 
760
- const helperSourcePath = PATHS.CEF_HELPER_WIN;
1003
+ const helperSourcePath = targetPaths.CEF_HELPER_WIN;
761
1004
  if (existsSync(helperSourcePath)) {
762
1005
  cefHelperNames.forEach((helperName) => {
763
1006
  const destinationPath = join(appBundleMacOSPath, `${helperName}.exe`);
@@ -767,10 +1010,9 @@ if (commandArg === "init") {
767
1010
  } else {
768
1011
  console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
769
1012
  }
770
- } else if (OS === 'linux') {
771
- // Copy CEF shared libraries from dist/cef/ to the main executable directory
772
- const electrobunDistPath = join(ELECTROBUN_DEP_PATH, "dist");
773
- const cefSourcePath = join(electrobunDistPath, "cef");
1013
+ } else if (targetOS === 'linux') {
1014
+ // Copy CEF shared libraries from platform-specific dist/cef/ to the main executable directory
1015
+ const cefSourcePath = targetPaths.CEF_DIR;
774
1016
 
775
1017
  if (existsSync(cefSourcePath)) {
776
1018
  const cefSoFiles = [
@@ -856,7 +1098,7 @@ if (commandArg === "init") {
856
1098
  "bun Helper (Renderer)",
857
1099
  ];
858
1100
 
859
- const helperSourcePath = PATHS.CEF_HELPER_LINUX;
1101
+ const helperSourcePath = targetPaths.CEF_HELPER_LINUX;
860
1102
  if (existsSync(helperSourcePath)) {
861
1103
  cefHelperNames.forEach((helperName) => {
862
1104
  const destinationPath = join(appBundleMacOSPath, helperName);
@@ -872,7 +1114,7 @@ if (commandArg === "init") {
872
1114
 
873
1115
 
874
1116
  // copy native bindings
875
- const bsPatchSource = PATHS.BSPATCH;
1117
+ const bsPatchSource = targetPaths.BSPATCH;
876
1118
  const bsPatchDestination = join(appBundleMacOSPath, "bspatch") + binExt;
877
1119
  const bsPatchDestFolder = dirname(bsPatchDestination);
878
1120
  if (!existsSync(bsPatchDestFolder)) {
@@ -968,12 +1210,21 @@ if (commandArg === "init") {
968
1210
 
969
1211
  // Run postBuild script
970
1212
  if (config.scripts.postBuild) {
971
-
972
- Bun.spawnSync([bunBinarySourcePath, config.scripts.postBuild], {
1213
+ // Use host platform's bun binary for running scripts, not target platform's
1214
+ const hostPaths = getPlatformPaths(OS, ARCH);
1215
+
1216
+ Bun.spawnSync([hostPaths.BUN_BINARY, config.scripts.postBuild], {
973
1217
  stdio: ["ignore", "inherit", "inherit"],
974
1218
  env: {
975
1219
  ...process.env,
976
1220
  ELECTROBUN_BUILD_ENV: buildEnvironment,
1221
+ ELECTROBUN_OS: targetOS, // Use target OS for environment variables
1222
+ ELECTROBUN_ARCH: targetARCH, // Use target ARCH for environment variables
1223
+ ELECTROBUN_BUILD_DIR: buildFolder,
1224
+ ELECTROBUN_APP_NAME: appFileName,
1225
+ ELECTROBUN_APP_VERSION: config.app.version,
1226
+ ELECTROBUN_APP_IDENTIFIER: config.app.identifier,
1227
+ ELECTROBUN_ARTIFACT_DIR: artifactFolder,
977
1228
  },
978
1229
  });
979
1230
  }
@@ -1017,8 +1268,9 @@ if (commandArg === "init") {
1017
1268
  );
1018
1269
 
1019
1270
  // todo (yoav): add these to config
1271
+ // Only codesign/notarize when building macOS targets on macOS host
1020
1272
  const shouldCodesign =
1021
- buildEnvironment !== "dev" && config.build.mac.codesign;
1273
+ buildEnvironment !== "dev" && targetOS === 'macos' && OS === 'macos' && config.build.mac.codesign;
1022
1274
  const shouldNotarize = shouldCodesign && config.build.mac.notarize;
1023
1275
 
1024
1276
  if (shouldCodesign) {
@@ -1053,6 +1305,8 @@ if (commandArg === "init") {
1053
1305
  // 6.5. code sign and notarize the dmg
1054
1306
  // 7. copy artifacts to directory [self-extractor dmg, zstd app bundle, bsdiff patch, update.json]
1055
1307
 
1308
+ // Add platform suffix for all artifacts
1309
+ const platformSuffix = `-${targetOS}-${targetARCH}`;
1056
1310
  const tarPath = `${appBundleFolderPath}.tar`;
1057
1311
 
1058
1312
  // tar the signed and notarized app bundle
@@ -1076,7 +1330,8 @@ if (commandArg === "init") {
1076
1330
  // than saving 1 more MB of space/bandwidth.
1077
1331
 
1078
1332
  const compressedTarPath = `${tarPath}.zst`;
1079
- artifactsToUpload.push(compressedTarPath);
1333
+ const platformCompressedTarPath = compressedTarPath.replace('.tar.zst', `${platformSuffix}.tar.zst`);
1334
+ artifactsToUpload.push(platformCompressedTarPath);
1080
1335
 
1081
1336
  // zstd compress tarball
1082
1337
  // todo (yoav): consider using c bindings for zstd for speed instead of wasm
@@ -1085,23 +1340,28 @@ if (commandArg === "init") {
1085
1340
  await ZstdInit().then(async ({ ZstdSimple, ZstdStream }) => {
1086
1341
  // Note: Simple is much faster than stream, but stream is better for large files
1087
1342
  // todo (yoav): consider a file size cutoff to switch to stream instead of simple.
1343
+ const useStream = tarball.size > 100 * 1024 * 1024;
1344
+
1088
1345
  if (tarball.size > 0) {
1089
- // Uint8 array filestream of the tar file
1346
+ // Uint8 array filestream of the tar file
1090
1347
 
1091
1348
  const data = new Uint8Array(tarBuffer);
1349
+
1092
1350
  const compressionLevel = 22;
1093
1351
  const compressedData = ZstdSimple.compress(data, compressionLevel);
1094
1352
 
1095
1353
  console.log(
1096
1354
  "compressed",
1097
- compressedData.length,
1355
+ data.length,
1098
1356
  "bytes",
1099
1357
  "from",
1100
- data.length,
1358
+ tarBuffer.byteLength,
1101
1359
  "bytes"
1102
1360
  );
1103
1361
 
1104
1362
  await Bun.write(compressedTarPath, compressedData);
1363
+ // Copy to platform-specific filename for upload
1364
+ cpSync(compressedTarPath, platformCompressedTarPath);
1105
1365
  }
1106
1366
  });
1107
1367
 
@@ -1109,7 +1369,7 @@ if (commandArg === "init") {
1109
1369
  // now and it needs the same name as the original app bundle.
1110
1370
  rmdirSync(appBundleFolderPath, { recursive: true });
1111
1371
 
1112
- const selfExtractingBundle = createAppBundle(appFileName, buildFolder);
1372
+ const selfExtractingBundle = createAppBundle(appFileName, buildFolder, targetOS);
1113
1373
  const compressedTarballInExtractingBundlePath = join(
1114
1374
  selfExtractingBundle.appBundleFolderResourcesPath,
1115
1375
  `${hash}.tar.zst`
@@ -1118,7 +1378,7 @@ if (commandArg === "init") {
1118
1378
  // copy the zstd tarball to the self-extracting app bundle
1119
1379
  cpSync(compressedTarPath, compressedTarballInExtractingBundlePath);
1120
1380
 
1121
- const selfExtractorBinSourcePath = PATHS.EXTRACTOR;
1381
+ const selfExtractorBinSourcePath = targetPaths.EXTRACTOR;
1122
1382
  const selfExtractorBinDestinationPath = join(
1123
1383
  selfExtractingBundle.appBundleMacOSPath,
1124
1384
  "launcher"
@@ -1150,28 +1410,49 @@ if (commandArg === "init") {
1150
1410
  console.log("skipping notarization");
1151
1411
  }
1152
1412
 
1153
- console.log("creating dmg...");
1154
- // make a dmg
1155
- const dmgPath = join(buildFolder, `${appFileName}.dmg`);
1156
- artifactsToUpload.push(dmgPath);
1157
- // hdiutil create -volname "YourAppName" -srcfolder /path/to/YourApp.app -ov -format UDZO YourAppName.dmg
1158
- // Note: use UDBZ for better compression vs. UDZO
1159
- execSync(
1160
- `hdiutil create -volname "${appFileName}" -srcfolder ${escapePathForTerminal(
1161
- appBundleFolderPath
1162
- )} -ov -format UDBZ ${escapePathForTerminal(dmgPath)}`
1163
- );
1413
+ // DMG creation for macOS only
1414
+ if (targetOS === 'macos') {
1415
+ console.log("creating dmg...");
1416
+ // make a dmg
1417
+ const dmgPath = join(buildFolder, `${appFileName}.dmg`);
1418
+ const platformDmgPath = join(buildFolder, `${appFileName}${platformSuffix}.dmg`);
1419
+ artifactsToUpload.push(platformDmgPath);
1420
+ // hdiutil create -volname "YourAppName" -srcfolder /path/to/YourApp.app -ov -format UDZO YourAppName.dmg
1421
+ // Note: use UDBZ for better compression vs. UDZO
1422
+ execSync(
1423
+ `hdiutil create -volname "${appFileName}" -srcfolder ${escapePathForTerminal(
1424
+ appBundleFolderPath
1425
+ )} -ov -format UDBZ ${escapePathForTerminal(dmgPath)}`
1426
+ );
1164
1427
 
1165
- if (shouldCodesign) {
1166
- codesignAppBundle(dmgPath);
1167
- } else {
1168
- console.log("skipping codesign");
1169
- }
1428
+ if (shouldCodesign) {
1429
+ codesignAppBundle(dmgPath);
1430
+ } else {
1431
+ console.log("skipping codesign");
1432
+ }
1170
1433
 
1171
- if (shouldNotarize) {
1172
- notarizeAndStaple(dmgPath);
1434
+ if (shouldNotarize) {
1435
+ notarizeAndStaple(dmgPath);
1436
+ } else {
1437
+ console.log("skipping notarization");
1438
+ }
1439
+
1440
+ // Copy to platform-specific filename
1441
+ cpSync(dmgPath, platformDmgPath);
1173
1442
  } else {
1174
- console.log("skipping notarization");
1443
+ // For Windows and Linux, add the self-extracting bundle directly
1444
+ const platformBundlePath = join(buildFolder, `${appFileName}${platformSuffix}${targetOS === 'win' ? '.exe' : ''}`);
1445
+ // Copy the self-extracting bundle to platform-specific filename
1446
+ if (targetOS === 'win') {
1447
+ // On Windows, create a self-extracting exe
1448
+ // For now, just copy the bundle folder
1449
+ artifactsToUpload.push(compressedTarPath.replace('.tar.zst', `${platformSuffix}.tar.zst`));
1450
+ } else if (targetOS === 'linux') {
1451
+ // On Linux, create a tar.gz of the bundle
1452
+ const linuxTarPath = join(buildFolder, `${appFileName}${platformSuffix}.tar.gz`);
1453
+ execSync(`tar -czf ${escapePathForTerminal(linuxTarPath)} -C ${escapePathForTerminal(buildFolder)} ${escapePathForTerminal(basename(appBundleFolderPath))}`);
1454
+ artifactsToUpload.push(linuxTarPath);
1455
+ }
1175
1456
  }
1176
1457
 
1177
1458
  // refresh artifacts folder
@@ -1190,11 +1471,15 @@ if (commandArg === "init") {
1190
1471
  // the download button or display on your marketing site or in the app.
1191
1472
  version: config.app.version,
1192
1473
  hash: hash.toString(),
1474
+ platform: OS,
1475
+ arch: ARCH,
1193
1476
  // channel: buildEnvironment,
1194
1477
  // bucketUrl: config.release.bucketUrl
1195
1478
  });
1196
1479
 
1197
- await Bun.write(join(artifactFolder, "update.json"), updateJsonContent);
1480
+ // Platform-specific update.json
1481
+ const platformUpdateJsonName = `update${platformSuffix}.json`;
1482
+ await Bun.write(join(artifactFolder, platformUpdateJsonName), updateJsonContent);
1198
1483
 
1199
1484
  // generate bsdiff
1200
1485
  // https://storage.googleapis.com/eggbun-static/electrobun-playground/canary/ElectrobunPlayground-canary.app.tar.zst
@@ -1204,7 +1489,7 @@ if (commandArg === "init") {
1204
1489
  const urlToPrevUpdateJson = join(
1205
1490
  config.release.bucketUrl,
1206
1491
  buildEnvironment,
1207
- `update.json`
1492
+ `update${platformSuffix}.json`
1208
1493
  );
1209
1494
  const cacheBuster = Math.random().toString(36).substring(7);
1210
1495
  const updateJsonResponse = await fetch(
@@ -1216,13 +1501,13 @@ if (commandArg === "init") {
1216
1501
  const urlToLatestTarball = join(
1217
1502
  config.release.bucketUrl,
1218
1503
  buildEnvironment,
1219
- `${appFileName}.app.tar.zst`
1504
+ `${appFileName}.app${platformSuffix}.tar.zst`
1220
1505
  );
1221
1506
 
1222
1507
 
1223
1508
  // attempt to get the previous version to create a patch file
1224
- if (updateJsonResponse.ok) {
1225
- const prevUpdateJson = await updateJsonResponse.json();
1509
+ if (updateJsonResponse && updateJsonResponse.ok) {
1510
+ const prevUpdateJson = await updateJsonResponse!.json();
1226
1511
 
1227
1512
  const prevHash = prevUpdateJson.hash;
1228
1513
  console.log("PREVIOUS HASH", prevHash);
@@ -1261,9 +1546,10 @@ if (commandArg === "init") {
1261
1546
  console.log("diff previous and new tarballs...");
1262
1547
  // Run it as a separate process to leverage multi-threadedness
1263
1548
  // especially for creating multiple diffs in parallel
1264
- const bsdiffpath = PATHS.BSDIFF;
1549
+ const bsdiffpath = targetPaths.BSDIFF;
1265
1550
  const patchFilePath = join(buildFolder, `${prevHash}.patch`);
1266
- artifactsToUpload.push(patchFilePath);
1551
+ const platformPatchFilePath = join(buildFolder, `${prevHash}${platformSuffix}.patch`);
1552
+ artifactsToUpload.push(platformPatchFilePath);
1267
1553
  const result = Bun.spawnSync(
1268
1554
  [bsdiffpath, prevTarballPath, tarPath, patchFilePath, "--use-zstd"],
1269
1555
  { cwd: buildFolder }
@@ -1273,6 +1559,8 @@ if (commandArg === "init") {
1273
1559
  result.stdout.toString(),
1274
1560
  result.stderr.toString()
1275
1561
  );
1562
+ // Copy to platform-specific filename
1563
+ cpSync(patchFilePath, platformPatchFilePath);
1276
1564
  }
1277
1565
  } else {
1278
1566
  console.log("prevoius version not found at: ", urlToLatestTarball);
@@ -1587,8 +1875,8 @@ function notarizeAndStaple(appOrDmgPath: string) {
1587
1875
  // 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.
1588
1876
  // either way you can pass in the parent folder here for that flexibility.
1589
1877
  // for intel/arm builds on mac we'll probably have separate subfolders as well and build them in parallel.
1590
- function createAppBundle(bundleName: string, parentFolder: string) {
1591
- if (OS === 'macos') {
1878
+ function createAppBundle(bundleName: string, parentFolder: string, targetOS: 'macos' | 'win' | 'linux') {
1879
+ if (targetOS === 'macos') {
1592
1880
  // macOS bundle structure
1593
1881
  const bundleFileName = `${bundleName}.app`;
1594
1882
  const appBundleFolderPath = join(parentFolder, bundleFileName);
@@ -1615,7 +1903,7 @@ function createAppBundle(bundleName: string, parentFolder: string) {
1615
1903
  appBundleFolderResourcesPath,
1616
1904
  appBundleFolderFrameworksPath,
1617
1905
  };
1618
- } else if (OS === 'linux' || OS === 'win') {
1906
+ } else if (targetOS === 'linux' || targetOS === 'win') {
1619
1907
  // Linux/Windows simpler structure
1620
1908
  const appBundleFolderPath = join(parentFolder, bundleName);
1621
1909
  const appBundleFolderContentsPath = appBundleFolderPath; // No Contents folder needed
@@ -1636,6 +1924,6 @@ function createAppBundle(bundleName: string, parentFolder: string) {
1636
1924
  appBundleFolderFrameworksPath,
1637
1925
  };
1638
1926
  } else {
1639
- throw new Error(`Unsupported OS: ${OS}`);
1927
+ throw new Error(`Unsupported OS: ${targetOS}`);
1640
1928
  }
1641
1929
  }