electrobun 0.0.19-beta.6 → 0.0.19-beta.61

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()) {
@@ -63,41 +65,81 @@ const ELECTROBUN_DEP_PATH = join(projectRoot, "node_modules", "electrobun");
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,21 +154,21 @@ 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
@@ -140,33 +182,102 @@ async function ensureCoreDependencies() {
140
182
  }
141
183
  fileStream.end();
142
184
 
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'),
148
- });
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 });
189
+
190
+ // Use Windows native tar.exe on Windows due to npm tar library issues
191
+ if (OS === 'win') {
192
+ console.log('Using Windows native tar.exe for reliable extraction...');
193
+ execSync(`tar -xf "${tempFile}" -C "${platformDistPath}"`, {
194
+ stdio: 'inherit',
195
+ cwd: platformDistPath
196
+ });
197
+ } else {
198
+ await tar.x({
199
+ file: tempFile,
200
+ cwd: platformDistPath,
201
+ preservePaths: false,
202
+ strip: 0,
203
+ });
204
+ }
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
149
209
 
150
210
  // Clean up temp file
151
211
  unlinkSync(tempFile);
152
212
 
153
- console.log('Core dependencies downloaded and cached successfully');
213
+ // Debug: List what was actually extracted
214
+ try {
215
+ const extractedFiles = readdirSync(platformDistPath);
216
+ console.log(`Extracted files to ${platformDistPath}:`, extractedFiles);
217
+
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(', ')}`);
225
+ }
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');
244
+ }
245
+
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');
154
250
 
155
- } catch (error) {
156
- 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);
157
261
  console.error('Please ensure you have an internet connection and the release exists.');
158
262
  process.exit(1);
159
263
  }
160
264
  }
161
265
 
162
- 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
+
163
274
  // Check if CEF dependencies already exist
164
- if (existsSync(PATHS.CEF_DIR)) {
165
- 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`);
166
277
  return;
167
278
  }
168
279
 
169
- console.log('CEF dependencies not found, downloading...');
280
+ console.log(`CEF dependencies not found for ${platformOS}-${platformArch}, downloading...`);
170
281
 
171
282
  // Get the current Electrobun version from package.json
172
283
  const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
@@ -181,8 +292,8 @@ async function ensureCEFDependencies() {
181
292
  }
182
293
  }
183
294
 
184
- const platformName = OS === 'macos' ? 'darwin' : OS === 'win' ? 'win32' : 'linux';
185
- const archName = ARCH;
295
+ const platformName = platformOS === 'macos' ? 'darwin' : platformOS === 'win' ? 'win' : 'linux';
296
+ const archName = platformArch;
186
297
  const cefTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-cef-${platformName}-${archName}.tar.gz`;
187
298
 
188
299
  console.log(`Downloading CEF from: ${cefTarballUrl}`);
@@ -195,7 +306,7 @@ async function ensureCEFDependencies() {
195
306
  }
196
307
 
197
308
  // Create temp file
198
- const tempFile = join(ELECTROBUN_DEP_PATH, 'cef-temp.tar.gz');
309
+ const tempFile = join(ELECTROBUN_DEP_PATH, `cef-${platformOS}-${platformArch}-temp.tar.gz`);
199
310
  const fileStream = createWriteStream(tempFile);
200
311
 
201
312
  // Write response to file
@@ -209,20 +320,49 @@ async function ensureCEFDependencies() {
209
320
  }
210
321
  fileStream.end();
211
322
 
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
- });
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 });
327
+
328
+ // Use Windows native tar.exe on Windows due to npm tar library issues
329
+ if (OS === 'win') {
330
+ console.log('Using Windows native tar.exe for reliable extraction...');
331
+ execSync(`tar -xf "${tempFile}" -C "${platformDistPath}"`, {
332
+ stdio: 'inherit',
333
+ cwd: platformDistPath
334
+ });
335
+ } else {
336
+ await tar.x({
337
+ file: tempFile,
338
+ cwd: platformDistPath,
339
+ preservePaths: false,
340
+ strip: 0,
341
+ });
342
+ }
218
343
 
219
344
  // Clean up temp file
220
345
  unlinkSync(tempFile);
221
346
 
222
- 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`);
223
363
 
224
- } catch (error) {
225
- 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);
226
366
  console.error('Please ensure you have an internet connection and the release exists.');
227
367
  process.exit(1);
228
368
  }
@@ -257,6 +397,7 @@ const defaultConfig = {
257
397
  build: {
258
398
  buildFolder: "build",
259
399
  artifactFolder: "artifacts",
400
+ targets: undefined, // Will default to current platform if not specified
260
401
  mac: {
261
402
  codesign: false,
262
403
  notarize: false,
@@ -267,6 +408,10 @@ const defaultConfig = {
267
408
  },
268
409
  icons: "icon.iconset",
269
410
  },
411
+ bun: {
412
+ entrypoint: "src/bun/index.ts",
413
+ external: [],
414
+ },
270
415
  },
271
416
  scripts: {
272
417
  postBuild: "",
@@ -286,7 +431,10 @@ if (!command) {
286
431
  const config = getConfig();
287
432
 
288
433
  const envArg =
289
- 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] || "";
290
438
 
291
439
  const validEnvironments = ["dev", "canary", "stable"];
292
440
 
@@ -294,8 +442,175 @@ const validEnvironments = ["dev", "canary", "stable"];
294
442
  const buildEnvironment: "dev" | "canary" | "stable" =
295
443
  validEnvironments.includes(envArg) ? envArg : "dev";
296
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
+
297
606
  // todo (yoav): dev builds should include the branch name, and/or allow configuration via external config
298
- 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;
299
614
 
300
615
  const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
301
616
 
@@ -370,7 +685,7 @@ const appFileName = (
370
685
  )
371
686
  .replace(/\s/g, "")
372
687
  .replace(/\./g, "-");
373
- const bundleFileName = OS === 'macos' ? `${appFileName}.app` : appFileName;
688
+ const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName;
374
689
 
375
690
  // const logPath = `/Library/Logs/Electrobun/ExampleApp/dev/out.log`;
376
691
 
@@ -380,6 +695,12 @@ if (commandArg === "init") {
380
695
  // todo (yoav): init a repo folder structure
381
696
  console.log("initializing electrobun project");
382
697
  } else if (commandArg === "build") {
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);
703
+
383
704
  // refresh build folder
384
705
  if (existsSync(buildFolder)) {
385
706
  rmdirSync(buildFolder, { recursive: true });
@@ -403,7 +724,7 @@ if (commandArg === "init") {
403
724
  appBundleMacOSPath,
404
725
  appBundleFolderResourcesPath,
405
726
  appBundleFolderFrameworksPath,
406
- } = createAppBundle(appFileName, buildFolder);
727
+ } = createAppBundle(appFileName, buildFolder, targetOS);
407
728
 
408
729
  const appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
409
730
 
@@ -473,12 +794,28 @@ if (commandArg === "init") {
473
794
  // mkdirSync(destLauncherFolder, {recursive: true});
474
795
  // }
475
796
  // cpSync(zigLauncherBinarySource, zigLauncherDestination, {recursive: true, dereference: true});
797
+ // For dev builds, use the actual CLI binary that's currently running
798
+ // It could be in .cache (npm install) or bin (local dev)
799
+ let devLauncherPath = targetPaths.LAUNCHER_DEV;
800
+ if (buildEnvironment === "dev" && !existsSync(devLauncherPath)) {
801
+ // Check .cache location (npm installed)
802
+ const cachePath = join(ELECTROBUN_DEP_PATH, ".cache", "electrobun") + binExt;
803
+ if (existsSync(cachePath)) {
804
+ devLauncherPath = cachePath;
805
+ } else {
806
+ // Check bin location (local dev)
807
+ const binPath = join(ELECTROBUN_DEP_PATH, "bin", "electrobun") + binExt;
808
+ if (existsSync(binPath)) {
809
+ devLauncherPath = binPath;
810
+ }
811
+ }
812
+ }
813
+
476
814
  const bunCliLauncherBinarySource =
477
815
  buildEnvironment === "dev"
478
- ? // Note: in dev use the cli as the launcher
479
- PATHS.LAUNCHER_DEV
816
+ ? devLauncherPath
480
817
  : // Note: for release use the zig launcher optimized for smol size
481
- PATHS.LAUNCHER_RELEASE;
818
+ targetPaths.LAUNCHER_RELEASE;
482
819
  const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + binExt;
483
820
  const destLauncherFolder = dirname(bunCliLauncherDestination);
484
821
  if (!existsSync(destLauncherFolder)) {
@@ -491,11 +828,11 @@ if (commandArg === "init") {
491
828
  dereference: true,
492
829
  });
493
830
 
494
- cpSync(PATHS.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
831
+ cpSync(targetPaths.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
495
832
 
496
833
  // Bun runtime binary
497
834
  // todo (yoav): this only works for the current architecture
498
- const bunBinarySourcePath = PATHS.BUN_BINARY;
835
+ const bunBinarySourcePath = targetPaths.BUN_BINARY;
499
836
  // Note: .bin/bun binary in node_modules is a symlink to the versioned one in another place
500
837
  // in node_modules, so we have to dereference here to get the actual binary in the bundle.
501
838
  const bunBinaryDestInBundlePath = join(appBundleMacOSPath, "bun") + binExt;
@@ -507,8 +844,8 @@ if (commandArg === "init") {
507
844
  cpSync(bunBinarySourcePath, bunBinaryDestInBundlePath, { dereference: true });
508
845
 
509
846
  // copy native wrapper dynamic library
510
- if (OS === 'macos') {
511
- const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_MACOS;
847
+ if (targetOS === 'macos') {
848
+ const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_MACOS;
512
849
  const nativeWrapperMacosDestination = join(
513
850
  appBundleMacOSPath,
514
851
  "libNativeWrapper.dylib"
@@ -516,8 +853,8 @@ if (commandArg === "init") {
516
853
  cpSync(nativeWrapperMacosSource, nativeWrapperMacosDestination, {
517
854
  dereference: true,
518
855
  });
519
- } else if (OS === 'win') {
520
- const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_WIN;
856
+ } else if (targetOS === 'win') {
857
+ const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_WIN;
521
858
  const nativeWrapperMacosDestination = join(
522
859
  appBundleMacOSPath,
523
860
  "libNativeWrapper.dll"
@@ -526,7 +863,7 @@ if (commandArg === "init") {
526
863
  dereference: true,
527
864
  });
528
865
 
529
- const webview2LibSource = PATHS.WEBVIEW2LOADER_WIN;
866
+ const webview2LibSource = targetPaths.WEBVIEW2LOADER_WIN;
530
867
  const webview2LibDestination = join(
531
868
  appBundleMacOSPath,
532
869
  "WebView2Loader.dll"
@@ -534,30 +871,34 @@ if (commandArg === "init") {
534
871
  // copy webview2 system webview library
535
872
  cpSync(webview2LibSource, webview2LibDestination);
536
873
 
537
- } else if (OS === 'linux') {
538
- const nativeWrapperLinuxSource = PATHS.NATIVE_WRAPPER_LINUX;
874
+ } else if (targetOS === 'linux') {
875
+ // Choose the appropriate native wrapper based on bundleCEF setting
876
+ const useCEF = config.build.linux?.bundleCEF;
877
+ const nativeWrapperLinuxSource = useCEF ? targetPaths.NATIVE_WRAPPER_LINUX_CEF : targetPaths.NATIVE_WRAPPER_LINUX;
539
878
  const nativeWrapperLinuxDestination = join(
540
879
  appBundleMacOSPath,
541
880
  "libNativeWrapper.so"
542
881
  );
882
+
543
883
  if (existsSync(nativeWrapperLinuxSource)) {
544
884
  cpSync(nativeWrapperLinuxSource, nativeWrapperLinuxDestination, {
545
885
  dereference: true,
546
886
  });
887
+ console.log(`Using ${useCEF ? 'CEF' : 'GTK'} native wrapper for Linux`);
888
+ } else {
889
+ throw new Error(`Native wrapper not found: ${nativeWrapperLinuxSource}`);
547
890
  }
548
891
  }
549
892
 
550
- // Ensure core binaries are available
551
- await ensureCoreDependencies();
552
893
 
553
894
  // 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)) {
895
+ if ((targetOS === 'macos' && config.build.mac?.bundleCEF) ||
896
+ (targetOS === 'win' && config.build.win?.bundleCEF) ||
897
+ (targetOS === 'linux' && config.build.linux?.bundleCEF)) {
557
898
 
558
- await ensureCEFDependencies();
559
- if (OS === 'macos') {
560
- const cefFrameworkSource = PATHS.CEF_FRAMEWORK_MACOS;
899
+ await ensureCEFDependencies(currentTarget.os, currentTarget.arch);
900
+ if (targetOS === 'macos') {
901
+ const cefFrameworkSource = targetPaths.CEF_FRAMEWORK_MACOS;
561
902
  const cefFrameworkDestination = join(
562
903
  appBundleFolderFrameworksPath,
563
904
  "Chromium Embedded Framework.framework"
@@ -578,7 +919,7 @@ if (commandArg === "init") {
578
919
  "bun Helper (Renderer)",
579
920
  ];
580
921
 
581
- const helperSourcePath = PATHS.CEF_HELPER_MACOS;
922
+ const helperSourcePath = targetPaths.CEF_HELPER_MACOS;
582
923
  cefHelperNames.forEach((helperName) => {
583
924
  const destinationPath = join(
584
925
  appBundleFolderFrameworksPath,
@@ -598,10 +939,9 @@ if (commandArg === "init") {
598
939
  dereference: true,
599
940
  });
600
941
  });
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");
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;
605
945
  const cefDllFiles = [
606
946
  'libcef.dll',
607
947
  'chrome_elf.dll',
@@ -641,7 +981,7 @@ if (commandArg === "init") {
641
981
  });
642
982
 
643
983
  // Copy CEF resources to MacOS/cef/ subdirectory for other resources like locales
644
- const cefResourcesSource = join(electrobunDistPath, 'cef');
984
+ const cefResourcesSource = targetPaths.CEF_DIR;
645
985
  const cefResourcesDestination = join(appBundleMacOSPath, 'cef');
646
986
 
647
987
  if (existsSync(cefResourcesSource)) {
@@ -660,7 +1000,7 @@ if (commandArg === "init") {
660
1000
  "bun Helper (Renderer)",
661
1001
  ];
662
1002
 
663
- const helperSourcePath = PATHS.CEF_HELPER_WIN;
1003
+ const helperSourcePath = targetPaths.CEF_HELPER_WIN;
664
1004
  if (existsSync(helperSourcePath)) {
665
1005
  cefHelperNames.forEach((helperName) => {
666
1006
  const destinationPath = join(appBundleMacOSPath, `${helperName}.exe`);
@@ -670,10 +1010,9 @@ if (commandArg === "init") {
670
1010
  } else {
671
1011
  console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
672
1012
  }
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");
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;
677
1016
 
678
1017
  if (existsSync(cefSourcePath)) {
679
1018
  const cefSoFiles = [
@@ -759,12 +1098,12 @@ if (commandArg === "init") {
759
1098
  "bun Helper (Renderer)",
760
1099
  ];
761
1100
 
762
- const helperSourcePath = PATHS.CEF_HELPER_LINUX;
1101
+ const helperSourcePath = targetPaths.CEF_HELPER_LINUX;
763
1102
  if (existsSync(helperSourcePath)) {
764
1103
  cefHelperNames.forEach((helperName) => {
765
1104
  const destinationPath = join(appBundleMacOSPath, helperName);
766
1105
  cpSync(helperSourcePath, destinationPath);
767
- console.log(`Copied CEF helper: ${helperName}`);
1106
+ // console.log(`Copied CEF helper: ${helperName}`);
768
1107
  });
769
1108
  } else {
770
1109
  console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
@@ -775,7 +1114,7 @@ if (commandArg === "init") {
775
1114
 
776
1115
 
777
1116
  // copy native bindings
778
- const bsPatchSource = PATHS.BSPATCH;
1117
+ const bsPatchSource = targetPaths.BSPATCH;
779
1118
  const bsPatchDestination = join(appBundleMacOSPath, "bspatch") + binExt;
780
1119
  const bsPatchDestFolder = dirname(bsPatchDestination);
781
1120
  if (!existsSync(bsPatchDestFolder)) {
@@ -871,12 +1210,21 @@ if (commandArg === "init") {
871
1210
 
872
1211
  // Run postBuild script
873
1212
  if (config.scripts.postBuild) {
874
-
875
- 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], {
876
1217
  stdio: ["ignore", "inherit", "inherit"],
877
1218
  env: {
878
1219
  ...process.env,
879
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,
880
1228
  },
881
1229
  });
882
1230
  }
@@ -920,8 +1268,9 @@ if (commandArg === "init") {
920
1268
  );
921
1269
 
922
1270
  // todo (yoav): add these to config
1271
+ // Only codesign/notarize when building macOS targets on macOS host
923
1272
  const shouldCodesign =
924
- buildEnvironment !== "dev" && config.build.mac.codesign;
1273
+ buildEnvironment !== "dev" && targetOS === 'macos' && OS === 'macos' && config.build.mac.codesign;
925
1274
  const shouldNotarize = shouldCodesign && config.build.mac.notarize;
926
1275
 
927
1276
  if (shouldCodesign) {
@@ -956,6 +1305,8 @@ if (commandArg === "init") {
956
1305
  // 6.5. code sign and notarize the dmg
957
1306
  // 7. copy artifacts to directory [self-extractor dmg, zstd app bundle, bsdiff patch, update.json]
958
1307
 
1308
+ // Add platform suffix for all artifacts
1309
+ const platformSuffix = `-${targetOS}-${targetARCH}`;
959
1310
  const tarPath = `${appBundleFolderPath}.tar`;
960
1311
 
961
1312
  // tar the signed and notarized app bundle
@@ -979,7 +1330,8 @@ if (commandArg === "init") {
979
1330
  // than saving 1 more MB of space/bandwidth.
980
1331
 
981
1332
  const compressedTarPath = `${tarPath}.zst`;
982
- artifactsToUpload.push(compressedTarPath);
1333
+ const platformCompressedTarPath = compressedTarPath.replace('.tar.zst', `${platformSuffix}.tar.zst`);
1334
+ artifactsToUpload.push(platformCompressedTarPath);
983
1335
 
984
1336
  // zstd compress tarball
985
1337
  // todo (yoav): consider using c bindings for zstd for speed instead of wasm
@@ -988,23 +1340,28 @@ if (commandArg === "init") {
988
1340
  await ZstdInit().then(async ({ ZstdSimple, ZstdStream }) => {
989
1341
  // Note: Simple is much faster than stream, but stream is better for large files
990
1342
  // todo (yoav): consider a file size cutoff to switch to stream instead of simple.
1343
+ const useStream = tarball.size > 100 * 1024 * 1024;
1344
+
991
1345
  if (tarball.size > 0) {
992
- // Uint8 array filestream of the tar file
1346
+ // Uint8 array filestream of the tar file
993
1347
 
994
1348
  const data = new Uint8Array(tarBuffer);
1349
+
995
1350
  const compressionLevel = 22;
996
1351
  const compressedData = ZstdSimple.compress(data, compressionLevel);
997
1352
 
998
1353
  console.log(
999
1354
  "compressed",
1000
- compressedData.length,
1355
+ data.length,
1001
1356
  "bytes",
1002
1357
  "from",
1003
- data.length,
1358
+ tarBuffer.byteLength,
1004
1359
  "bytes"
1005
1360
  );
1006
1361
 
1007
1362
  await Bun.write(compressedTarPath, compressedData);
1363
+ // Copy to platform-specific filename for upload
1364
+ cpSync(compressedTarPath, platformCompressedTarPath);
1008
1365
  }
1009
1366
  });
1010
1367
 
@@ -1012,7 +1369,7 @@ if (commandArg === "init") {
1012
1369
  // now and it needs the same name as the original app bundle.
1013
1370
  rmdirSync(appBundleFolderPath, { recursive: true });
1014
1371
 
1015
- const selfExtractingBundle = createAppBundle(appFileName, buildFolder);
1372
+ const selfExtractingBundle = createAppBundle(appFileName, buildFolder, targetOS);
1016
1373
  const compressedTarballInExtractingBundlePath = join(
1017
1374
  selfExtractingBundle.appBundleFolderResourcesPath,
1018
1375
  `${hash}.tar.zst`
@@ -1021,7 +1378,7 @@ if (commandArg === "init") {
1021
1378
  // copy the zstd tarball to the self-extracting app bundle
1022
1379
  cpSync(compressedTarPath, compressedTarballInExtractingBundlePath);
1023
1380
 
1024
- const selfExtractorBinSourcePath = PATHS.EXTRACTOR;
1381
+ const selfExtractorBinSourcePath = targetPaths.EXTRACTOR;
1025
1382
  const selfExtractorBinDestinationPath = join(
1026
1383
  selfExtractingBundle.appBundleMacOSPath,
1027
1384
  "launcher"
@@ -1053,28 +1410,49 @@ if (commandArg === "init") {
1053
1410
  console.log("skipping notarization");
1054
1411
  }
1055
1412
 
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
- );
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
+ );
1067
1427
 
1068
- if (shouldCodesign) {
1069
- codesignAppBundle(dmgPath);
1070
- } else {
1071
- console.log("skipping codesign");
1072
- }
1428
+ if (shouldCodesign) {
1429
+ codesignAppBundle(dmgPath);
1430
+ } else {
1431
+ console.log("skipping codesign");
1432
+ }
1073
1433
 
1074
- if (shouldNotarize) {
1075
- 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);
1076
1442
  } else {
1077
- 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
+ }
1078
1456
  }
1079
1457
 
1080
1458
  // refresh artifacts folder
@@ -1093,11 +1471,15 @@ if (commandArg === "init") {
1093
1471
  // the download button or display on your marketing site or in the app.
1094
1472
  version: config.app.version,
1095
1473
  hash: hash.toString(),
1474
+ platform: OS,
1475
+ arch: ARCH,
1096
1476
  // channel: buildEnvironment,
1097
1477
  // bucketUrl: config.release.bucketUrl
1098
1478
  });
1099
1479
 
1100
- 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);
1101
1483
 
1102
1484
  // generate bsdiff
1103
1485
  // https://storage.googleapis.com/eggbun-static/electrobun-playground/canary/ElectrobunPlayground-canary.app.tar.zst
@@ -1107,7 +1489,7 @@ if (commandArg === "init") {
1107
1489
  const urlToPrevUpdateJson = join(
1108
1490
  config.release.bucketUrl,
1109
1491
  buildEnvironment,
1110
- `update.json`
1492
+ `update${platformSuffix}.json`
1111
1493
  );
1112
1494
  const cacheBuster = Math.random().toString(36).substring(7);
1113
1495
  const updateJsonResponse = await fetch(
@@ -1119,13 +1501,13 @@ if (commandArg === "init") {
1119
1501
  const urlToLatestTarball = join(
1120
1502
  config.release.bucketUrl,
1121
1503
  buildEnvironment,
1122
- `${appFileName}.app.tar.zst`
1504
+ `${appFileName}.app${platformSuffix}.tar.zst`
1123
1505
  );
1124
1506
 
1125
1507
 
1126
1508
  // attempt to get the previous version to create a patch file
1127
- if (updateJsonResponse.ok) {
1128
- const prevUpdateJson = await updateJsonResponse.json();
1509
+ if (updateJsonResponse && updateJsonResponse.ok) {
1510
+ const prevUpdateJson = await updateJsonResponse!.json();
1129
1511
 
1130
1512
  const prevHash = prevUpdateJson.hash;
1131
1513
  console.log("PREVIOUS HASH", prevHash);
@@ -1164,9 +1546,10 @@ if (commandArg === "init") {
1164
1546
  console.log("diff previous and new tarballs...");
1165
1547
  // Run it as a separate process to leverage multi-threadedness
1166
1548
  // especially for creating multiple diffs in parallel
1167
- const bsdiffpath = PATHS.BSDIFF;
1549
+ const bsdiffpath = targetPaths.BSDIFF;
1168
1550
  const patchFilePath = join(buildFolder, `${prevHash}.patch`);
1169
- artifactsToUpload.push(patchFilePath);
1551
+ const platformPatchFilePath = join(buildFolder, `${prevHash}${platformSuffix}.patch`);
1552
+ artifactsToUpload.push(platformPatchFilePath);
1170
1553
  const result = Bun.spawnSync(
1171
1554
  [bsdiffpath, prevTarballPath, tarPath, patchFilePath, "--use-zstd"],
1172
1555
  { cwd: buildFolder }
@@ -1176,6 +1559,8 @@ if (commandArg === "init") {
1176
1559
  result.stdout.toString(),
1177
1560
  result.stderr.toString()
1178
1561
  );
1562
+ // Copy to platform-specific filename
1563
+ cpSync(patchFilePath, platformPatchFilePath);
1179
1564
  }
1180
1565
  } else {
1181
1566
  console.log("prevoius version not found at: ", urlToLatestTarball);
@@ -1290,7 +1675,12 @@ function getConfig() {
1290
1675
  if (existsSync(configPath)) {
1291
1676
  const configFileContents = readFileSync(configPath, "utf8");
1292
1677
  // Note: we want this to hard fail if there's a syntax error
1293
- loadedConfig = JSON.parse(configFileContents);
1678
+ try {
1679
+ loadedConfig = JSON.parse(configFileContents);
1680
+ } catch (error) {
1681
+ console.error("Failed to parse config file:", error);
1682
+ console.error("using default config instead");
1683
+ }
1294
1684
  }
1295
1685
 
1296
1686
  // todo (yoav): write a deep clone fn
@@ -1312,6 +1702,18 @@ function getConfig() {
1312
1702
  ...(loadedConfig?.build?.mac?.entitlements || {}),
1313
1703
  },
1314
1704
  },
1705
+ win: {
1706
+ ...defaultConfig.build.win,
1707
+ ...(loadedConfig?.build?.win || {}),
1708
+ },
1709
+ linux: {
1710
+ ...defaultConfig.build.linux,
1711
+ ...(loadedConfig?.build?.linux || {}),
1712
+ },
1713
+ bun: {
1714
+ ...defaultConfig.build.bun,
1715
+ ...(loadedConfig?.build?.bun || {}),
1716
+ }
1315
1717
  },
1316
1718
  scripts: {
1317
1719
  ...defaultConfig.scripts,
@@ -1473,8 +1875,8 @@ function notarizeAndStaple(appOrDmgPath: string) {
1473
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.
1474
1876
  // either way you can pass in the parent folder here for that flexibility.
1475
1877
  // 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') {
1878
+ function createAppBundle(bundleName: string, parentFolder: string, targetOS: 'macos' | 'win' | 'linux') {
1879
+ if (targetOS === 'macos') {
1478
1880
  // macOS bundle structure
1479
1881
  const bundleFileName = `${bundleName}.app`;
1480
1882
  const appBundleFolderPath = join(parentFolder, bundleFileName);
@@ -1501,7 +1903,7 @@ function createAppBundle(bundleName: string, parentFolder: string) {
1501
1903
  appBundleFolderResourcesPath,
1502
1904
  appBundleFolderFrameworksPath,
1503
1905
  };
1504
- } else if (OS === 'linux' || OS === 'win') {
1906
+ } else if (targetOS === 'linux' || targetOS === 'win') {
1505
1907
  // Linux/Windows simpler structure
1506
1908
  const appBundleFolderPath = join(parentFolder, bundleName);
1507
1909
  const appBundleFolderContentsPath = appBundleFolderPath; // No Contents folder needed
@@ -1522,6 +1924,6 @@ function createAppBundle(bundleName: string, parentFolder: string) {
1522
1924
  appBundleFolderFrameworksPath,
1523
1925
  };
1524
1926
  } else {
1525
- throw new Error(`Unsupported OS: ${OS}`);
1927
+ throw new Error(`Unsupported OS: ${targetOS}`);
1526
1928
  }
1527
1929
  }