electrobun 0.0.19-beta.8 → 0.0.19-beta.81

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 (39) hide show
  1. package/BUILD.md +90 -0
  2. package/bin/electrobun.cjs +165 -0
  3. package/debug.js +5 -0
  4. package/dist/api/browser/builtinrpcSchema.ts +19 -0
  5. package/dist/api/browser/index.ts +409 -0
  6. package/dist/api/browser/rpc/webview.ts +79 -0
  7. package/dist/api/browser/stylesAndElements.ts +3 -0
  8. package/dist/api/browser/webviewtag.ts +534 -0
  9. package/dist/api/bun/core/ApplicationMenu.ts +66 -0
  10. package/dist/api/bun/core/BrowserView.ts +349 -0
  11. package/dist/api/bun/core/BrowserWindow.ts +191 -0
  12. package/dist/api/bun/core/ContextMenu.ts +67 -0
  13. package/dist/api/bun/core/Paths.ts +5 -0
  14. package/dist/api/bun/core/Socket.ts +181 -0
  15. package/dist/api/bun/core/Tray.ts +107 -0
  16. package/dist/api/bun/core/Updater.ts +547 -0
  17. package/dist/api/bun/core/Utils.ts +48 -0
  18. package/dist/api/bun/events/ApplicationEvents.ts +14 -0
  19. package/dist/api/bun/events/event.ts +29 -0
  20. package/dist/api/bun/events/eventEmitter.ts +45 -0
  21. package/dist/api/bun/events/trayEvents.ts +9 -0
  22. package/dist/api/bun/events/webviewEvents.ts +16 -0
  23. package/dist/api/bun/events/windowEvents.ts +12 -0
  24. package/dist/api/bun/index.ts +45 -0
  25. package/dist/api/bun/proc/linux.md +43 -0
  26. package/dist/api/bun/proc/native.ts +1220 -0
  27. package/dist/api/shared/platform.ts +48 -0
  28. package/dist/main.js +53 -0
  29. package/package.json +15 -7
  30. package/src/cli/index.ts +1034 -210
  31. package/templates/hello-world/README.md +57 -0
  32. package/templates/hello-world/bun.lock +63 -0
  33. package/templates/hello-world/electrobun.config +18 -0
  34. package/templates/hello-world/package.json +16 -0
  35. package/templates/hello-world/src/bun/index.ts +15 -0
  36. package/templates/hello-world/src/mainview/index.css +124 -0
  37. package/templates/hello-world/src/mainview/index.html +47 -0
  38. package/templates/hello-world/src/mainview/index.ts +5 -0
  39. package/bin/electrobun +0 -0
package/src/cli/index.ts CHANGED
@@ -2,50 +2,30 @@ import { join, dirname, basename } from "path";
2
2
  import {
3
3
  existsSync,
4
4
  readFileSync,
5
+ writeFileSync,
5
6
  cpSync,
6
7
  rmdirSync,
7
8
  mkdirSync,
8
9
  createWriteStream,
9
10
  unlinkSync,
11
+ readdirSync,
12
+ rmSync,
13
+ symlinkSync,
14
+ statSync,
15
+ copyFileSync,
10
16
  } from "fs";
11
17
  import { execSync } from "child_process";
12
18
  import tar from "tar";
19
+ import archiver from "archiver";
13
20
  import { ZstdInit } from "@oneidentity/zstd-js/wasm";
14
- import {platform, arch} from 'os';
21
+ import { OS, ARCH } from '../shared/platform';
22
+ import { getTemplate, getTemplateNames } from './templates/embedded';
15
23
  // import { loadBsdiff, loadBspatch } from 'bsdiff-wasm';
16
24
  // MacOS named pipes hang at around 4KB
17
25
  const MAX_CHUNK_SIZE = 1024 * 2;
18
26
 
19
- // TODO: dedup with built.ts
20
- const OS: 'win' | 'linux' | 'macos' = getPlatform();
21
- const ARCH: 'arm64' | 'x64' = getArch();
22
-
23
- function getPlatform() {
24
- switch (platform()) {
25
- case "win32":
26
- return 'win';
27
- case "darwin":
28
- return 'macos';
29
- case 'linux':
30
- return 'linux';
31
- default:
32
- throw 'unsupported platform';
33
- }
34
- }
35
-
36
- function getArch() {
37
- switch (arch()) {
38
- case "arm64":
39
- return 'arm64';
40
- case "x64":
41
- return 'x64';
42
- default:
43
- throw 'unsupported arch'
44
- }
45
- }
46
27
 
47
-
48
- const binExt = OS === 'win' ? '.exe' : '';
28
+ // const binExt = OS === 'win' ? '.exe' : '';
49
29
 
50
30
  // this when run as an npm script this will be where the folder where package.json is.
51
31
  const projectRoot = process.cwd();
@@ -56,48 +36,88 @@ const configPath = join(projectRoot, configName);
56
36
  const indexOfElectrobun = process.argv.findIndex((arg) =>
57
37
  arg.includes("electrobun")
58
38
  );
59
- const commandArg = process.argv[indexOfElectrobun + 1] || "launcher";
39
+ const commandArg = process.argv[indexOfElectrobun + 1] || "build";
60
40
 
61
41
  const ELECTROBUN_DEP_PATH = join(projectRoot, "node_modules", "electrobun");
62
42
 
63
43
  // When debugging electrobun with the example app use the builds (dev or release) right from the source folder
64
44
  // For developers using electrobun cli via npm use the release versions in /dist
65
45
  // 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
46
 
94
- async function ensureCoreDependencies() {
95
- // Check if core dependencies already exist
96
- if (existsSync(PATHS.BUN_BINARY) && existsSync(PATHS.LAUNCHER_RELEASE)) {
47
+ // Function to get platform-specific paths
48
+ function getPlatformPaths(targetOS: 'macos' | 'win' | 'linux', targetArch: 'arm64' | 'x64') {
49
+ const binExt = targetOS === 'win' ? '.exe' : '';
50
+ const platformDistDir = join(ELECTROBUN_DEP_PATH, `dist-${targetOS}-${targetArch}`);
51
+ const sharedDistDir = join(ELECTROBUN_DEP_PATH, "dist");
52
+
53
+ return {
54
+ // Platform-specific binaries (from dist-OS-ARCH/)
55
+ BUN_BINARY: join(platformDistDir, "bun") + binExt,
56
+ LAUNCHER_DEV: join(platformDistDir, "electrobun") + binExt,
57
+ LAUNCHER_RELEASE: join(platformDistDir, "launcher") + binExt,
58
+ NATIVE_WRAPPER_MACOS: join(platformDistDir, "libNativeWrapper.dylib"),
59
+ NATIVE_WRAPPER_WIN: join(platformDistDir, "libNativeWrapper.dll"),
60
+ NATIVE_WRAPPER_LINUX: join(platformDistDir, "libNativeWrapper.so"),
61
+ NATIVE_WRAPPER_LINUX_CEF: join(platformDistDir, "libNativeWrapper_cef.so"),
62
+ WEBVIEW2LOADER_WIN: join(platformDistDir, "WebView2Loader.dll"),
63
+ BSPATCH: join(platformDistDir, "bspatch") + binExt,
64
+ EXTRACTOR: join(platformDistDir, "extractor") + binExt,
65
+ BSDIFF: join(platformDistDir, "bsdiff") + binExt,
66
+ CEF_FRAMEWORK_MACOS: join(platformDistDir, "cef", "Chromium Embedded Framework.framework"),
67
+ CEF_HELPER_MACOS: join(platformDistDir, "cef", "process_helper"),
68
+ CEF_HELPER_WIN: join(platformDistDir, "cef", "process_helper.exe"),
69
+ CEF_HELPER_LINUX: join(platformDistDir, "cef", "process_helper"),
70
+ CEF_DIR: join(platformDistDir, "cef"),
71
+
72
+ // Shared platform-independent files (from dist/)
73
+ // These work with existing package.json and development workflow
74
+ MAIN_JS: join(sharedDistDir, "main.js"),
75
+ API_DIR: join(sharedDistDir, "api"),
76
+ };
77
+ }
78
+
79
+ // Default PATHS for host platform (backward compatibility)
80
+ const PATHS = getPlatformPaths(OS, ARCH);
81
+
82
+ async function ensureCoreDependencies(targetOS?: 'macos' | 'win' | 'linux', targetArch?: 'arm64' | 'x64') {
83
+ // Use provided target platform or default to host platform
84
+ const platformOS = targetOS || OS;
85
+ const platformArch = targetArch || ARCH;
86
+
87
+ // Get platform-specific paths
88
+ const platformPaths = getPlatformPaths(platformOS, platformArch);
89
+
90
+ // Check platform-specific binaries
91
+ const requiredBinaries = [
92
+ platformPaths.BUN_BINARY,
93
+ platformPaths.LAUNCHER_RELEASE,
94
+ // Platform-specific native wrapper
95
+ platformOS === 'macos' ? platformPaths.NATIVE_WRAPPER_MACOS :
96
+ platformOS === 'win' ? platformPaths.NATIVE_WRAPPER_WIN :
97
+ platformPaths.NATIVE_WRAPPER_LINUX
98
+ ];
99
+
100
+ // Check shared files (main.js should be in shared dist/)
101
+ const requiredSharedFiles = [
102
+ platformPaths.MAIN_JS
103
+ ];
104
+
105
+ const missingBinaries = requiredBinaries.filter(file => !existsSync(file));
106
+ const missingSharedFiles = requiredSharedFiles.filter(file => !existsSync(file));
107
+
108
+ // If only shared files are missing, that's expected in production (they come via npm)
109
+ if (missingBinaries.length === 0 && missingSharedFiles.length > 0) {
110
+ console.log(`Shared files missing (expected in production): ${missingSharedFiles.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', ')}`);
111
+ }
112
+
113
+ // Only download if platform-specific binaries are missing
114
+ if (missingBinaries.length === 0) {
97
115
  return;
98
116
  }
99
117
 
100
- console.log('Core dependencies not found, downloading...');
118
+ // Show which binaries are missing
119
+ console.log(`Core dependencies not found for ${platformOS}-${platformArch}. Missing files:`, missingBinaries.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', '));
120
+ console.log(`Downloading core binaries for ${platformOS}-${platformArch}...`);
101
121
 
102
122
  // Get the current Electrobun version from package.json
103
123
  const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
@@ -112,61 +132,153 @@ async function ensureCoreDependencies() {
112
132
  }
113
133
  }
114
134
 
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`;
135
+ const platformName = platformOS === 'macos' ? 'darwin' : platformOS === 'win' ? 'win' : 'linux';
136
+ const archName = platformArch;
137
+ const coreTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-core-${platformName}-${archName}.tar.gz`;
118
138
 
119
- console.log(`Downloading core binaries from: ${mainTarballUrl}`);
139
+ console.log(`Downloading core binaries from: ${coreTarballUrl}`);
120
140
 
121
141
  try {
122
- // Download main tarball
123
- const response = await fetch(mainTarballUrl);
142
+ // Download core binaries tarball
143
+ const response = await fetch(coreTarballUrl);
124
144
  if (!response.ok) {
125
145
  throw new Error(`Failed to download binaries: ${response.status} ${response.statusText}`);
126
146
  }
127
147
 
128
148
  // Create temp file
129
- const tempFile = join(ELECTROBUN_DEP_PATH, 'main-temp.tar.gz');
149
+ const tempFile = join(ELECTROBUN_DEP_PATH, `core-${platformOS}-${platformArch}-temp.tar.gz`);
130
150
  const fileStream = createWriteStream(tempFile);
131
151
 
132
152
  // Write response to file
133
153
  if (response.body) {
134
154
  const reader = response.body.getReader();
155
+ let totalBytes = 0;
135
156
  while (true) {
136
157
  const { done, value } = await reader.read();
137
158
  if (done) break;
138
- fileStream.write(Buffer.from(value));
159
+ const buffer = Buffer.from(value);
160
+ fileStream.write(buffer);
161
+ totalBytes += buffer.length;
139
162
  }
163
+ console.log(`Downloaded ${totalBytes} bytes for ${platformOS}-${platformArch}`);
140
164
  }
141
- fileStream.end();
142
165
 
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'),
166
+ // Ensure file is properly closed before proceeding
167
+ await new Promise((resolve, reject) => {
168
+ fileStream.end((err) => {
169
+ if (err) reject(err);
170
+ else resolve(null);
171
+ });
148
172
  });
149
173
 
174
+ // Verify the downloaded file exists and has content
175
+ if (!existsSync(tempFile)) {
176
+ throw new Error(`Downloaded file not found: ${tempFile}`);
177
+ }
178
+
179
+ const fileSize = require('fs').statSync(tempFile).size;
180
+ if (fileSize === 0) {
181
+ throw new Error(`Downloaded file is empty: ${tempFile}`);
182
+ }
183
+
184
+ console.log(`Verified download: ${tempFile} (${fileSize} bytes)`);
185
+
186
+ // Extract to platform-specific dist directory
187
+ console.log(`Extracting core dependencies for ${platformOS}-${platformArch}...`);
188
+ const platformDistPath = join(ELECTROBUN_DEP_PATH, `dist-${platformOS}-${platformArch}`);
189
+ mkdirSync(platformDistPath, { recursive: true });
190
+
191
+ // Use Windows native tar.exe on Windows due to npm tar library issues
192
+ if (OS === 'win') {
193
+ console.log('Using Windows native tar.exe for reliable extraction...');
194
+ execSync(`tar -xf "${tempFile}" -C "${platformDistPath}"`, {
195
+ stdio: 'inherit',
196
+ cwd: platformDistPath
197
+ });
198
+ } else {
199
+ await tar.x({
200
+ file: tempFile,
201
+ cwd: platformDistPath,
202
+ preservePaths: false,
203
+ strip: 0,
204
+ });
205
+ }
206
+
207
+ // NOTE: We no longer copy main.js from platform-specific downloads
208
+ // Platform-specific downloads should only contain native binaries
209
+ // main.js and api/ should be shipped via npm in the shared dist/ folder
210
+
150
211
  // Clean up temp file
151
212
  unlinkSync(tempFile);
152
213
 
153
- console.log('Core dependencies downloaded and cached successfully');
214
+ // Debug: List what was actually extracted
215
+ try {
216
+ const extractedFiles = readdirSync(platformDistPath);
217
+ console.log(`Extracted files to ${platformDistPath}:`, extractedFiles);
218
+
219
+ // Check if files are in subdirectories
220
+ for (const file of extractedFiles) {
221
+ const filePath = join(platformDistPath, file);
222
+ const stat = require('fs').statSync(filePath);
223
+ if (stat.isDirectory()) {
224
+ const subFiles = readdirSync(filePath);
225
+ console.log(` ${file}/: ${subFiles.join(', ')}`);
226
+ }
227
+ }
228
+ } catch (e) {
229
+ console.error('Could not list extracted files:', e);
230
+ }
154
231
 
155
- } catch (error) {
156
- console.error('Failed to download core dependencies:', error.message);
232
+ // Verify extraction completed successfully - check platform-specific binaries only
233
+ const requiredBinaries = [
234
+ platformPaths.BUN_BINARY,
235
+ platformPaths.LAUNCHER_RELEASE,
236
+ platformOS === 'macos' ? platformPaths.NATIVE_WRAPPER_MACOS :
237
+ platformOS === 'win' ? platformPaths.NATIVE_WRAPPER_WIN :
238
+ platformPaths.NATIVE_WRAPPER_LINUX
239
+ ];
240
+
241
+ const missingBinaries = requiredBinaries.filter(file => !existsSync(file));
242
+ if (missingBinaries.length > 0) {
243
+ console.error(`Missing binaries after extraction: ${missingBinaries.map(f => f.replace(ELECTROBUN_DEP_PATH, '.')).join(', ')}`);
244
+ console.error('This suggests the tarball structure is different than expected');
245
+ }
246
+
247
+ // For development: if main.js doesn't exist in shared dist/, copy from platform-specific download as fallback
248
+ const sharedDistPath = join(ELECTROBUN_DEP_PATH, 'dist');
249
+ const extractedMainJs = join(platformDistPath, 'main.js');
250
+ const sharedMainJs = join(sharedDistPath, 'main.js');
251
+
252
+ if (existsSync(extractedMainJs) && !existsSync(sharedMainJs)) {
253
+ console.log('Development fallback: copying main.js from platform-specific download to shared dist/');
254
+ mkdirSync(sharedDistPath, { recursive: true });
255
+ cpSync(extractedMainJs, sharedMainJs);
256
+ }
257
+
258
+ console.log(`Core dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
259
+
260
+ } catch (error: any) {
261
+ console.error(`Failed to download core dependencies for ${platformOS}-${platformArch}:`, error.message);
157
262
  console.error('Please ensure you have an internet connection and the release exists.');
158
263
  process.exit(1);
159
264
  }
160
265
  }
161
266
 
162
- async function ensureCEFDependencies() {
267
+ async function ensureCEFDependencies(targetOS?: 'macos' | 'win' | 'linux', targetArch?: 'arm64' | 'x64') {
268
+ // Use provided target platform or default to host platform
269
+ const platformOS = targetOS || OS;
270
+ const platformArch = targetArch || ARCH;
271
+
272
+ // Get platform-specific paths
273
+ const platformPaths = getPlatformPaths(platformOS, platformArch);
274
+
163
275
  // Check if CEF dependencies already exist
164
- if (existsSync(PATHS.CEF_DIR)) {
165
- console.log('CEF dependencies found, using cached version');
276
+ if (existsSync(platformPaths.CEF_DIR)) {
277
+ console.log(`CEF dependencies found for ${platformOS}-${platformArch}, using cached version`);
166
278
  return;
167
279
  }
168
280
 
169
- console.log('CEF dependencies not found, downloading...');
281
+ console.log(`CEF dependencies not found for ${platformOS}-${platformArch}, downloading...`);
170
282
 
171
283
  // Get the current Electrobun version from package.json
172
284
  const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
@@ -181,8 +293,8 @@ async function ensureCEFDependencies() {
181
293
  }
182
294
  }
183
295
 
184
- const platformName = OS === 'macos' ? 'darwin' : OS === 'win' ? 'win32' : 'linux';
185
- const archName = ARCH;
296
+ const platformName = platformOS === 'macos' ? 'darwin' : platformOS === 'win' ? 'win' : 'linux';
297
+ const archName = platformArch;
186
298
  const cefTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-cef-${platformName}-${archName}.tar.gz`;
187
299
 
188
300
  console.log(`Downloading CEF from: ${cefTarballUrl}`);
@@ -195,7 +307,7 @@ async function ensureCEFDependencies() {
195
307
  }
196
308
 
197
309
  // Create temp file
198
- const tempFile = join(ELECTROBUN_DEP_PATH, 'cef-temp.tar.gz');
310
+ const tempFile = join(ELECTROBUN_DEP_PATH, `cef-${platformOS}-${platformArch}-temp.tar.gz`);
199
311
  const fileStream = createWriteStream(tempFile);
200
312
 
201
313
  // Write response to file
@@ -209,20 +321,49 @@ async function ensureCEFDependencies() {
209
321
  }
210
322
  fileStream.end();
211
323
 
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
- });
324
+ // Extract to platform-specific dist directory
325
+ console.log(`Extracting CEF dependencies for ${platformOS}-${platformArch}...`);
326
+ const platformDistPath = join(ELECTROBUN_DEP_PATH, `dist-${platformOS}-${platformArch}`);
327
+ mkdirSync(platformDistPath, { recursive: true });
328
+
329
+ // Use Windows native tar.exe on Windows due to npm tar library issues
330
+ if (OS === 'win') {
331
+ console.log('Using Windows native tar.exe for reliable extraction...');
332
+ execSync(`tar -xf "${tempFile}" -C "${platformDistPath}"`, {
333
+ stdio: 'inherit',
334
+ cwd: platformDistPath
335
+ });
336
+ } else {
337
+ await tar.x({
338
+ file: tempFile,
339
+ cwd: platformDistPath,
340
+ preservePaths: false,
341
+ strip: 0,
342
+ });
343
+ }
218
344
 
219
345
  // Clean up temp file
220
346
  unlinkSync(tempFile);
221
347
 
222
- console.log('CEF dependencies downloaded and cached successfully');
348
+ // Debug: List what was actually extracted for CEF
349
+ try {
350
+ const extractedFiles = readdirSync(platformDistPath);
351
+ console.log(`CEF extracted files to ${platformDistPath}:`, extractedFiles);
352
+
353
+ // Check if CEF directory was created
354
+ const cefDir = join(platformDistPath, 'cef');
355
+ if (existsSync(cefDir)) {
356
+ const cefFiles = readdirSync(cefDir);
357
+ console.log(`CEF directory contents: ${cefFiles.slice(0, 10).join(', ')}${cefFiles.length > 10 ? '...' : ''}`);
358
+ }
359
+ } catch (e) {
360
+ console.error('Could not list CEF extracted files:', e);
361
+ }
223
362
 
224
- } catch (error) {
225
- console.error('Failed to download CEF dependencies:', error.message);
363
+ console.log(`CEF dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`);
364
+
365
+ } catch (error: any) {
366
+ console.error(`Failed to download CEF dependencies for ${platformOS}-${platformArch}:`, error.message);
226
367
  console.error('Please ensure you have an internet connection and the release exists.');
227
368
  process.exit(1);
228
369
  }
@@ -241,10 +382,6 @@ const commandDefaults = {
241
382
  projectRoot,
242
383
  config: "electrobun.config",
243
384
  },
244
- launcher: {
245
- projectRoot,
246
- config: "electrobun.config",
247
- },
248
385
  };
249
386
 
250
387
  // todo (yoav): add types for config
@@ -257,6 +394,7 @@ const defaultConfig = {
257
394
  build: {
258
395
  buildFolder: "build",
259
396
  artifactFolder: "artifacts",
397
+ targets: undefined, // Will default to current platform if not specified
260
398
  mac: {
261
399
  codesign: false,
262
400
  notarize: false,
@@ -267,6 +405,16 @@ const defaultConfig = {
267
405
  },
268
406
  icons: "icon.iconset",
269
407
  },
408
+ win: {
409
+ bundleCEF: false,
410
+ },
411
+ linux: {
412
+ bundleCEF: false,
413
+ },
414
+ bun: {
415
+ entrypoint: "src/bun/index.ts",
416
+ external: [],
417
+ },
270
418
  },
271
419
  scripts: {
272
420
  postBuild: "",
@@ -286,16 +434,187 @@ if (!command) {
286
434
  const config = getConfig();
287
435
 
288
436
  const envArg =
289
- process.argv.find((arg) => arg.startsWith("env="))?.split("=")[1] || "";
437
+ process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
438
+
439
+ const targetsArg =
440
+ process.argv.find((arg) => arg.startsWith("--targets="))?.split("=")[1] || "";
290
441
 
291
442
  const validEnvironments = ["dev", "canary", "stable"];
292
443
 
293
444
  // todo (yoav): dev, canary, and stable;
294
445
  const buildEnvironment: "dev" | "canary" | "stable" =
295
- validEnvironments.includes(envArg) ? envArg : "dev";
446
+ validEnvironments.includes(envArg || "dev") ? (envArg || "dev") : "dev";
447
+
448
+ // Determine build targets
449
+ type BuildTarget = { os: 'macos' | 'win' | 'linux', arch: 'arm64' | 'x64' };
450
+
451
+ function parseBuildTargets(): BuildTarget[] {
452
+ // If explicit targets provided via CLI
453
+ if (targetsArg) {
454
+ if (targetsArg === 'current') {
455
+ return [{ os: OS, arch: ARCH }];
456
+ } else if (targetsArg === 'all') {
457
+ return parseConfigTargets();
458
+ } else {
459
+ // Parse comma-separated targets like "macos-arm64,win-x64"
460
+ return targetsArg.split(',').map(target => {
461
+ const [os, arch] = target.trim().split('-') as [string, string];
462
+ if (!['macos', 'win', 'linux'].includes(os) || !['arm64', 'x64'].includes(arch)) {
463
+ console.error(`Invalid target: ${target}. Format should be: os-arch (e.g., macos-arm64)`);
464
+ process.exit(1);
465
+ }
466
+ return { os, arch } as BuildTarget;
467
+ });
468
+ }
469
+ }
470
+
471
+ // Default behavior: always build for current platform only
472
+ // This ensures predictable, fast builds unless explicitly requesting multi-platform
473
+ return [{ os: OS, arch: ARCH }];
474
+ }
475
+
476
+ function parseConfigTargets(): BuildTarget[] {
477
+ // If config has targets, use them
478
+ if (config.build.targets && config.build.targets.length > 0) {
479
+ return config.build.targets.map(target => {
480
+ if (target === 'current') {
481
+ return { os: OS, arch: ARCH };
482
+ }
483
+ const [os, arch] = target.split('-') as [string, string];
484
+ if (!['macos', 'win', 'linux'].includes(os) || !['arm64', 'x64'].includes(arch)) {
485
+ console.error(`Invalid target in config: ${target}. Format should be: os-arch (e.g., macos-arm64)`);
486
+ process.exit(1);
487
+ }
488
+ return { os, arch } as BuildTarget;
489
+ });
490
+ }
491
+
492
+ // If no config targets and --targets=all, use all available platforms
493
+ if (targetsArg === 'all') {
494
+ console.log('No targets specified in config, using all available platforms');
495
+ return [
496
+ { os: 'macos', arch: 'arm64' },
497
+ { os: 'macos', arch: 'x64' },
498
+ { os: 'win', arch: 'x64' },
499
+ { os: 'linux', arch: 'x64' },
500
+ { os: 'linux', arch: 'arm64' }
501
+ ];
502
+ }
503
+
504
+ // Default to current platform
505
+ return [{ os: OS, arch: ARCH }];
506
+ }
507
+
508
+ const buildTargets = parseBuildTargets();
509
+
510
+ // Show build targets to user
511
+ if (buildTargets.length === 1) {
512
+ console.log(`Building for ${buildTargets[0].os}-${buildTargets[0].arch} (${buildEnvironment})`);
513
+ } else {
514
+ const targetList = buildTargets.map(t => `${t.os}-${t.arch}`).join(', ');
515
+ console.log(`Building for multiple targets: ${targetList} (${buildEnvironment})`);
516
+ console.log(`Running ${buildTargets.length} parallel builds...`);
517
+
518
+ // Spawn parallel build processes
519
+ const buildPromises = buildTargets.map(async (target) => {
520
+ const targetString = `${target.os}-${target.arch}`;
521
+ const prefix = `[${targetString}]`;
522
+
523
+ try {
524
+ // Try to find the electrobun binary in node_modules/.bin or use bunx
525
+ const electrobunBin = join(projectRoot, 'node_modules', '.bin', 'electrobun');
526
+ let command: string[];
527
+
528
+ if (existsSync(electrobunBin)) {
529
+ command = [electrobunBin, 'build', `--env=${buildEnvironment}`, `--targets=${targetString}`];
530
+ } else {
531
+ // Fallback to bunx which should resolve node_modules binaries
532
+ command = ['bunx', 'electrobun', 'build', `--env=${buildEnvironment}`, `--targets=${targetString}`];
533
+ }
534
+
535
+ console.log(`${prefix} Running:`, command.join(' '));
536
+
537
+ const result = await Bun.spawn(command, {
538
+ stdio: ['inherit', 'pipe', 'pipe'],
539
+ env: process.env,
540
+ cwd: projectRoot // Ensure we're in the right directory
541
+ });
542
+
543
+ // Pipe output with prefix
544
+ if (result.stdout) {
545
+ const reader = result.stdout.getReader();
546
+ while (true) {
547
+ const { done, value } = await reader.read();
548
+ if (done) break;
549
+ const text = new TextDecoder().decode(value);
550
+ // Add prefix to each line
551
+ const prefixedText = text.split('\n').map(line =>
552
+ line ? `${prefix} ${line}` : line
553
+ ).join('\n');
554
+ process.stdout.write(prefixedText);
555
+ }
556
+ }
557
+
558
+ if (result.stderr) {
559
+ const reader = result.stderr.getReader();
560
+ while (true) {
561
+ const { done, value } = await reader.read();
562
+ if (done) break;
563
+ const text = new TextDecoder().decode(value);
564
+ const prefixedText = text.split('\n').map(line =>
565
+ line ? `${prefix} ${line}` : line
566
+ ).join('\n');
567
+ process.stderr.write(prefixedText);
568
+ }
569
+ }
570
+
571
+ const exitCode = await result.exited;
572
+ return { target, exitCode, success: exitCode === 0 };
573
+
574
+ } catch (error) {
575
+ console.error(`${prefix} Failed to start build:`, error);
576
+ return { target, exitCode: 1, success: false, error };
577
+ }
578
+ });
579
+
580
+ // Wait for all builds to complete
581
+ const results = await Promise.allSettled(buildPromises);
582
+
583
+ // Report final results
584
+ console.log('\n=== Build Results ===');
585
+ let allSucceeded = true;
586
+
587
+ for (const result of results) {
588
+ if (result.status === 'fulfilled') {
589
+ const { target, success, exitCode } = result.value;
590
+ const status = success ? '✅ SUCCESS' : '❌ FAILED';
591
+ console.log(`${target.os}-${target.arch}: ${status} (exit code: ${exitCode})`);
592
+ if (!success) allSucceeded = false;
593
+ } else {
594
+ console.log(`Build rejected: ${result.reason}`);
595
+ allSucceeded = false;
596
+ }
597
+ }
598
+
599
+ if (!allSucceeded) {
600
+ console.log('\nSome builds failed. Check the output above for details.');
601
+ process.exit(1);
602
+ } else {
603
+ console.log('\nAll builds completed successfully! 🎉');
604
+ }
605
+
606
+ process.exit(0);
607
+ }
296
608
 
297
609
  // todo (yoav): dev builds should include the branch name, and/or allow configuration via external config
298
- const buildSubFolder = `${buildEnvironment}`;
610
+ // For now, assume single target build (we'll refactor for multi-target later)
611
+ const currentTarget = buildTargets[0];
612
+ const buildSubFolder = `${buildEnvironment}-${currentTarget.os}-${currentTarget.arch}`;
613
+
614
+ // Use target OS/ARCH for build logic (instead of current machine's OS/ARCH)
615
+ const targetOS = currentTarget.os;
616
+ const targetARCH = currentTarget.arch;
617
+ const targetBinExt = targetOS === 'win' ? '.exe' : '';
299
618
 
300
619
  const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
301
620
 
@@ -370,16 +689,71 @@ const appFileName = (
370
689
  )
371
690
  .replace(/\s/g, "")
372
691
  .replace(/\./g, "-");
373
- const bundleFileName = OS === 'macos' ? `${appFileName}.app` : appFileName;
692
+ const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName;
374
693
 
375
694
  // const logPath = `/Library/Logs/Electrobun/ExampleApp/dev/out.log`;
376
695
 
377
696
  let proc = null;
378
697
 
379
698
  if (commandArg === "init") {
380
- // todo (yoav): init a repo folder structure
381
- console.log("initializing electrobun project");
699
+ const projectName = process.argv[indexOfElectrobun + 2] || "my-electrobun-app";
700
+ const templateName = process.argv.find(arg => arg.startsWith("--template="))?.split("=")[1] || "hello-world";
701
+
702
+ console.log(`🚀 Initializing Electrobun project: ${projectName}`);
703
+
704
+ // Validate template name
705
+ const availableTemplates = getTemplateNames();
706
+ if (!availableTemplates.includes(templateName)) {
707
+ console.error(`❌ Template "${templateName}" not found.`);
708
+ console.log(`Available templates: ${availableTemplates.join(", ")}`);
709
+ process.exit(1);
710
+ }
711
+
712
+ const template = getTemplate(templateName);
713
+ if (!template) {
714
+ console.error(`❌ Could not load template "${templateName}"`);
715
+ process.exit(1);
716
+ }
717
+
718
+ // Create project directory
719
+ const projectPath = join(process.cwd(), projectName);
720
+ if (existsSync(projectPath)) {
721
+ console.error(`❌ Directory "${projectName}" already exists.`);
722
+ process.exit(1);
723
+ }
724
+
725
+ mkdirSync(projectPath, { recursive: true });
726
+
727
+ // Extract template files
728
+ let fileCount = 0;
729
+ for (const [relativePath, content] of Object.entries(template.files)) {
730
+ const fullPath = join(projectPath, relativePath);
731
+ const dir = dirname(fullPath);
732
+
733
+ // Create directory if it doesn't exist
734
+ mkdirSync(dir, { recursive: true });
735
+
736
+ // Write file
737
+ writeFileSync(fullPath, content, 'utf-8');
738
+ fileCount++;
739
+ }
740
+
741
+ console.log(`✅ Created ${fileCount} files from "${templateName}" template`);
742
+ console.log(`📁 Project created at: ${projectPath}`);
743
+ console.log("");
744
+ console.log("📦 Next steps:");
745
+ console.log(` cd ${projectName}`);
746
+ console.log(" bun install");
747
+ console.log(" bunx electrobun dev");
748
+ console.log("");
749
+ console.log("🎉 Happy building with Electrobun!");
382
750
  } else if (commandArg === "build") {
751
+ // Ensure core binaries are available for the target platform before starting build
752
+ await ensureCoreDependencies(currentTarget.os, currentTarget.arch);
753
+
754
+ // Get platform-specific paths for the current target
755
+ const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);
756
+
383
757
  // refresh build folder
384
758
  if (existsSync(buildFolder)) {
385
759
  rmdirSync(buildFolder, { recursive: true });
@@ -403,7 +777,7 @@ if (commandArg === "init") {
403
777
  appBundleMacOSPath,
404
778
  appBundleFolderResourcesPath,
405
779
  appBundleFolderFrameworksPath,
406
- } = createAppBundle(appFileName, buildFolder);
780
+ } = createAppBundle(appFileName, buildFolder, targetOS);
407
781
 
408
782
  const appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
409
783
 
@@ -473,32 +847,30 @@ if (commandArg === "init") {
473
847
  // mkdirSync(destLauncherFolder, {recursive: true});
474
848
  // }
475
849
  // cpSync(zigLauncherBinarySource, zigLauncherDestination, {recursive: true, dereference: true});
476
- const bunCliLauncherBinarySource =
477
- buildEnvironment === "dev"
478
- ? // Note: in dev use the cli as the launcher
479
- PATHS.LAUNCHER_DEV
480
- : // Note: for release use the zig launcher optimized for smol size
481
- PATHS.LAUNCHER_RELEASE;
482
- const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + binExt;
483
- const destLauncherFolder = dirname(bunCliLauncherDestination);
484
- if (!existsSync(destLauncherFolder)) {
485
- // console.info('creating folder: ', destFolder);
486
- mkdirSync(destLauncherFolder, { recursive: true });
487
- }
850
+ // Only copy launcher for non-dev builds
851
+ if (buildEnvironment !== "dev") {
852
+ const bunCliLauncherBinarySource = targetPaths.LAUNCHER_RELEASE;
853
+ const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + targetBinExt;
854
+ const destLauncherFolder = dirname(bunCliLauncherDestination);
855
+ if (!existsSync(destLauncherFolder)) {
856
+ // console.info('creating folder: ', destFolder);
857
+ mkdirSync(destLauncherFolder, { recursive: true });
858
+ }
488
859
 
489
- cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
490
- recursive: true,
491
- dereference: true,
492
- });
860
+ cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
861
+ recursive: true,
862
+ dereference: true,
863
+ });
864
+ }
493
865
 
494
- cpSync(PATHS.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
866
+ cpSync(targetPaths.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
495
867
 
496
868
  // Bun runtime binary
497
869
  // todo (yoav): this only works for the current architecture
498
- const bunBinarySourcePath = PATHS.BUN_BINARY;
870
+ const bunBinarySourcePath = targetPaths.BUN_BINARY;
499
871
  // Note: .bin/bun binary in node_modules is a symlink to the versioned one in another place
500
872
  // in node_modules, so we have to dereference here to get the actual binary in the bundle.
501
- const bunBinaryDestInBundlePath = join(appBundleMacOSPath, "bun") + binExt;
873
+ const bunBinaryDestInBundlePath = join(appBundleMacOSPath, "bun") + targetBinExt;
502
874
  const destFolder2 = dirname(bunBinaryDestInBundlePath);
503
875
  if (!existsSync(destFolder2)) {
504
876
  // console.info('creating folder: ', destFolder);
@@ -507,8 +879,8 @@ if (commandArg === "init") {
507
879
  cpSync(bunBinarySourcePath, bunBinaryDestInBundlePath, { dereference: true });
508
880
 
509
881
  // copy native wrapper dynamic library
510
- if (OS === 'macos') {
511
- const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_MACOS;
882
+ if (targetOS === 'macos') {
883
+ const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_MACOS;
512
884
  const nativeWrapperMacosDestination = join(
513
885
  appBundleMacOSPath,
514
886
  "libNativeWrapper.dylib"
@@ -516,8 +888,8 @@ if (commandArg === "init") {
516
888
  cpSync(nativeWrapperMacosSource, nativeWrapperMacosDestination, {
517
889
  dereference: true,
518
890
  });
519
- } else if (OS === 'win') {
520
- const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_WIN;
891
+ } else if (targetOS === 'win') {
892
+ const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_WIN;
521
893
  const nativeWrapperMacosDestination = join(
522
894
  appBundleMacOSPath,
523
895
  "libNativeWrapper.dll"
@@ -526,7 +898,7 @@ if (commandArg === "init") {
526
898
  dereference: true,
527
899
  });
528
900
 
529
- const webview2LibSource = PATHS.WEBVIEW2LOADER_WIN;
901
+ const webview2LibSource = targetPaths.WEBVIEW2LOADER_WIN;
530
902
  const webview2LibDestination = join(
531
903
  appBundleMacOSPath,
532
904
  "WebView2Loader.dll"
@@ -534,30 +906,34 @@ if (commandArg === "init") {
534
906
  // copy webview2 system webview library
535
907
  cpSync(webview2LibSource, webview2LibDestination);
536
908
 
537
- } else if (OS === 'linux') {
538
- const nativeWrapperLinuxSource = PATHS.NATIVE_WRAPPER_LINUX;
909
+ } else if (targetOS === 'linux') {
910
+ // Choose the appropriate native wrapper based on bundleCEF setting
911
+ const useCEF = config.build.linux?.bundleCEF;
912
+ const nativeWrapperLinuxSource = useCEF ? targetPaths.NATIVE_WRAPPER_LINUX_CEF : targetPaths.NATIVE_WRAPPER_LINUX;
539
913
  const nativeWrapperLinuxDestination = join(
540
914
  appBundleMacOSPath,
541
915
  "libNativeWrapper.so"
542
916
  );
917
+
543
918
  if (existsSync(nativeWrapperLinuxSource)) {
544
919
  cpSync(nativeWrapperLinuxSource, nativeWrapperLinuxDestination, {
545
920
  dereference: true,
546
921
  });
922
+ console.log(`Using ${useCEF ? 'CEF' : 'GTK'} native wrapper for Linux`);
923
+ } else {
924
+ throw new Error(`Native wrapper not found: ${nativeWrapperLinuxSource}`);
547
925
  }
548
926
  }
549
927
 
550
- // Ensure core binaries are available
551
- await ensureCoreDependencies();
552
928
 
553
929
  // 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)) {
930
+ if ((targetOS === 'macos' && config.build.mac?.bundleCEF) ||
931
+ (targetOS === 'win' && config.build.win?.bundleCEF) ||
932
+ (targetOS === 'linux' && config.build.linux?.bundleCEF)) {
557
933
 
558
- await ensureCEFDependencies();
559
- if (OS === 'macos') {
560
- const cefFrameworkSource = PATHS.CEF_FRAMEWORK_MACOS;
934
+ await ensureCEFDependencies(currentTarget.os, currentTarget.arch);
935
+ if (targetOS === 'macos') {
936
+ const cefFrameworkSource = targetPaths.CEF_FRAMEWORK_MACOS;
561
937
  const cefFrameworkDestination = join(
562
938
  appBundleFolderFrameworksPath,
563
939
  "Chromium Embedded Framework.framework"
@@ -578,7 +954,7 @@ if (commandArg === "init") {
578
954
  "bun Helper (Renderer)",
579
955
  ];
580
956
 
581
- const helperSourcePath = PATHS.CEF_HELPER_MACOS;
957
+ const helperSourcePath = targetPaths.CEF_HELPER_MACOS;
582
958
  cefHelperNames.forEach((helperName) => {
583
959
  const destinationPath = join(
584
960
  appBundleFolderFrameworksPath,
@@ -598,10 +974,9 @@ if (commandArg === "init") {
598
974
  dereference: true,
599
975
  });
600
976
  });
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");
977
+ } else if (targetOS === 'win') {
978
+ // Copy CEF DLLs from platform-specific dist/cef/ to the main executable directory
979
+ const cefSourcePath = targetPaths.CEF_DIR;
605
980
  const cefDllFiles = [
606
981
  'libcef.dll',
607
982
  'chrome_elf.dll',
@@ -641,7 +1016,7 @@ if (commandArg === "init") {
641
1016
  });
642
1017
 
643
1018
  // Copy CEF resources to MacOS/cef/ subdirectory for other resources like locales
644
- const cefResourcesSource = join(electrobunDistPath, 'cef');
1019
+ const cefResourcesSource = targetPaths.CEF_DIR;
645
1020
  const cefResourcesDestination = join(appBundleMacOSPath, 'cef');
646
1021
 
647
1022
  if (existsSync(cefResourcesSource)) {
@@ -660,7 +1035,7 @@ if (commandArg === "init") {
660
1035
  "bun Helper (Renderer)",
661
1036
  ];
662
1037
 
663
- const helperSourcePath = PATHS.CEF_HELPER_WIN;
1038
+ const helperSourcePath = targetPaths.CEF_HELPER_WIN;
664
1039
  if (existsSync(helperSourcePath)) {
665
1040
  cefHelperNames.forEach((helperName) => {
666
1041
  const destinationPath = join(appBundleMacOSPath, `${helperName}.exe`);
@@ -670,10 +1045,9 @@ if (commandArg === "init") {
670
1045
  } else {
671
1046
  console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
672
1047
  }
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");
1048
+ } else if (targetOS === 'linux') {
1049
+ // Copy CEF shared libraries from platform-specific dist/cef/ to the main executable directory
1050
+ const cefSourcePath = targetPaths.CEF_DIR;
677
1051
 
678
1052
  if (existsSync(cefSourcePath)) {
679
1053
  const cefSoFiles = [
@@ -684,11 +1058,13 @@ if (commandArg === "init") {
684
1058
  'libvulkan.so.1'
685
1059
  ];
686
1060
 
1061
+ // Copy CEF .so files to main directory as symlinks to cef/ subdirectory
687
1062
  cefSoFiles.forEach(soFile => {
688
1063
  const sourcePath = join(cefSourcePath, soFile);
689
1064
  const destPath = join(appBundleMacOSPath, soFile);
690
1065
  if (existsSync(sourcePath)) {
691
- cpSync(sourcePath, destPath);
1066
+ // We'll create the actual file in cef/ and symlink from main directory
1067
+ // This will be done after the cef/ directory is populated
692
1068
  }
693
1069
  });
694
1070
 
@@ -750,6 +1126,30 @@ if (commandArg === "init") {
750
1126
  }
751
1127
  });
752
1128
 
1129
+ // Create symlinks from main directory to cef/ subdirectory for .so files
1130
+ console.log('Creating symlinks for CEF libraries...');
1131
+ cefSoFiles.forEach(soFile => {
1132
+ const cefFilePath = join(cefResourcesDestination, soFile);
1133
+ const mainDirPath = join(appBundleMacOSPath, soFile);
1134
+
1135
+ if (existsSync(cefFilePath)) {
1136
+ try {
1137
+ // Remove any existing file/symlink in main directory
1138
+ if (existsSync(mainDirPath)) {
1139
+ rmSync(mainDirPath);
1140
+ }
1141
+ // Create symlink from main directory to cef/ subdirectory
1142
+ symlinkSync(join('cef', soFile), mainDirPath);
1143
+ console.log(`Created symlink for CEF library: ${soFile} -> cef/${soFile}`);
1144
+ } catch (error) {
1145
+ console.log(`WARNING: Failed to create symlink for ${soFile}: ${error}`);
1146
+ // Fallback to copying the file
1147
+ cpSync(cefFilePath, mainDirPath);
1148
+ console.log(`Fallback: Copied CEF library to main directory: ${soFile}`);
1149
+ }
1150
+ }
1151
+ });
1152
+
753
1153
  // Copy CEF helper processes with different names
754
1154
  const cefHelperNames = [
755
1155
  "bun Helper",
@@ -759,12 +1159,12 @@ if (commandArg === "init") {
759
1159
  "bun Helper (Renderer)",
760
1160
  ];
761
1161
 
762
- const helperSourcePath = PATHS.CEF_HELPER_LINUX;
1162
+ const helperSourcePath = targetPaths.CEF_HELPER_LINUX;
763
1163
  if (existsSync(helperSourcePath)) {
764
1164
  cefHelperNames.forEach((helperName) => {
765
1165
  const destinationPath = join(appBundleMacOSPath, helperName);
766
1166
  cpSync(helperSourcePath, destinationPath);
767
- console.log(`Copied CEF helper: ${helperName}`);
1167
+ // console.log(`Copied CEF helper: ${helperName}`);
768
1168
  });
769
1169
  } else {
770
1170
  console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
@@ -775,8 +1175,8 @@ if (commandArg === "init") {
775
1175
 
776
1176
 
777
1177
  // copy native bindings
778
- const bsPatchSource = PATHS.BSPATCH;
779
- const bsPatchDestination = join(appBundleMacOSPath, "bspatch") + binExt;
1178
+ const bsPatchSource = targetPaths.BSPATCH;
1179
+ const bsPatchDestination = join(appBundleMacOSPath, "bspatch") + targetBinExt;
780
1180
  const bsPatchDestFolder = dirname(bsPatchDestination);
781
1181
  if (!existsSync(bsPatchDestFolder)) {
782
1182
  mkdirSync(bsPatchDestFolder, { recursive: true });
@@ -871,12 +1271,21 @@ if (commandArg === "init") {
871
1271
 
872
1272
  // Run postBuild script
873
1273
  if (config.scripts.postBuild) {
874
-
875
- Bun.spawnSync([bunBinarySourcePath, config.scripts.postBuild], {
1274
+ // Use host platform's bun binary for running scripts, not target platform's
1275
+ const hostPaths = getPlatformPaths(OS, ARCH);
1276
+
1277
+ Bun.spawnSync([hostPaths.BUN_BINARY, config.scripts.postBuild], {
876
1278
  stdio: ["ignore", "inherit", "inherit"],
877
1279
  env: {
878
1280
  ...process.env,
879
1281
  ELECTROBUN_BUILD_ENV: buildEnvironment,
1282
+ ELECTROBUN_OS: targetOS, // Use target OS for environment variables
1283
+ ELECTROBUN_ARCH: targetARCH, // Use target ARCH for environment variables
1284
+ ELECTROBUN_BUILD_DIR: buildFolder,
1285
+ ELECTROBUN_APP_NAME: appFileName,
1286
+ ELECTROBUN_APP_VERSION: config.app.version,
1287
+ ELECTROBUN_APP_IDENTIFIER: config.app.identifier,
1288
+ ELECTROBUN_ARTIFACT_DIR: artifactFolder,
880
1289
  },
881
1290
  });
882
1291
  }
@@ -920,8 +1329,9 @@ if (commandArg === "init") {
920
1329
  );
921
1330
 
922
1331
  // todo (yoav): add these to config
1332
+ // Only codesign/notarize when building macOS targets on macOS host
923
1333
  const shouldCodesign =
924
- buildEnvironment !== "dev" && config.build.mac.codesign;
1334
+ buildEnvironment !== "dev" && targetOS === 'macos' && OS === 'macos' && config.build.mac.codesign;
925
1335
  const shouldNotarize = shouldCodesign && config.build.mac.notarize;
926
1336
 
927
1337
  if (shouldCodesign) {
@@ -956,6 +1366,8 @@ if (commandArg === "init") {
956
1366
  // 6.5. code sign and notarize the dmg
957
1367
  // 7. copy artifacts to directory [self-extractor dmg, zstd app bundle, bsdiff patch, update.json]
958
1368
 
1369
+ // Platform suffix is only used for folder names, not file names
1370
+ const platformSuffix = `-${targetOS}-${targetARCH}`;
959
1371
  const tarPath = `${appBundleFolderPath}.tar`;
960
1372
 
961
1373
  // tar the signed and notarized app bundle
@@ -978,7 +1390,7 @@ if (commandArg === "init") {
978
1390
  // zstd is the clear winner here. dev iteration speed gain of 1min 15s per build is much more valubale
979
1391
  // than saving 1 more MB of space/bandwidth.
980
1392
 
981
- const compressedTarPath = `${tarPath}.zst`;
1393
+ let compressedTarPath = `${tarPath}.zst`;
982
1394
  artifactsToUpload.push(compressedTarPath);
983
1395
 
984
1396
  // zstd compress tarball
@@ -988,19 +1400,21 @@ if (commandArg === "init") {
988
1400
  await ZstdInit().then(async ({ ZstdSimple, ZstdStream }) => {
989
1401
  // Note: Simple is much faster than stream, but stream is better for large files
990
1402
  // todo (yoav): consider a file size cutoff to switch to stream instead of simple.
1403
+ const useStream = tarball.size > 100 * 1024 * 1024;
1404
+
991
1405
  if (tarball.size > 0) {
992
1406
  // Uint8 array filestream of the tar file
993
-
994
1407
  const data = new Uint8Array(tarBuffer);
995
- const compressionLevel = 22;
1408
+
1409
+ const compressionLevel = 22; // Maximum compression - now safe with stripped CEF libraries
996
1410
  const compressedData = ZstdSimple.compress(data, compressionLevel);
997
1411
 
998
1412
  console.log(
999
1413
  "compressed",
1000
- compressedData.length,
1414
+ data.length,
1001
1415
  "bytes",
1002
1416
  "from",
1003
- data.length,
1417
+ tarBuffer.byteLength,
1004
1418
  "bytes"
1005
1419
  );
1006
1420
 
@@ -1012,7 +1426,7 @@ if (commandArg === "init") {
1012
1426
  // now and it needs the same name as the original app bundle.
1013
1427
  rmdirSync(appBundleFolderPath, { recursive: true });
1014
1428
 
1015
- const selfExtractingBundle = createAppBundle(appFileName, buildFolder);
1429
+ const selfExtractingBundle = createAppBundle(appFileName, buildFolder, targetOS);
1016
1430
  const compressedTarballInExtractingBundlePath = join(
1017
1431
  selfExtractingBundle.appBundleFolderResourcesPath,
1018
1432
  `${hash}.tar.zst`
@@ -1021,7 +1435,7 @@ if (commandArg === "init") {
1021
1435
  // copy the zstd tarball to the self-extracting app bundle
1022
1436
  cpSync(compressedTarPath, compressedTarballInExtractingBundlePath);
1023
1437
 
1024
- const selfExtractorBinSourcePath = PATHS.EXTRACTOR;
1438
+ const selfExtractorBinSourcePath = targetPaths.EXTRACTOR;
1025
1439
  const selfExtractorBinDestinationPath = join(
1026
1440
  selfExtractingBundle.appBundleMacOSPath,
1027
1441
  "launcher"
@@ -1053,28 +1467,117 @@ if (commandArg === "init") {
1053
1467
  console.log("skipping notarization");
1054
1468
  }
1055
1469
 
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
- );
1470
+ // DMG creation for macOS only
1471
+ if (targetOS === 'macos') {
1472
+ console.log("creating dmg...");
1473
+ // make a dmg
1474
+ const dmgPath = join(buildFolder, `${appFileName}.dmg`);
1475
+ artifactsToUpload.push(dmgPath);
1476
+ // hdiutil create -volname "YourAppName" -srcfolder /path/to/YourApp.app -ov -format UDZO YourAppName.dmg
1477
+ // Note: use ULFO (lzfse) for better compatibility with large CEF frameworks and modern macOS
1478
+ execSync(
1479
+ `hdiutil create -volname "${appFileName}" -srcfolder ${escapePathForTerminal(
1480
+ appBundleFolderPath
1481
+ )} -ov -format ULFO ${escapePathForTerminal(dmgPath)}`
1482
+ );
1067
1483
 
1068
- if (shouldCodesign) {
1069
- codesignAppBundle(dmgPath);
1070
- } else {
1071
- console.log("skipping codesign");
1072
- }
1484
+ if (shouldCodesign) {
1485
+ codesignAppBundle(dmgPath);
1486
+ } else {
1487
+ console.log("skipping codesign");
1488
+ }
1073
1489
 
1074
- if (shouldNotarize) {
1075
- notarizeAndStaple(dmgPath);
1490
+ if (shouldNotarize) {
1491
+ notarizeAndStaple(dmgPath);
1492
+ } else {
1493
+ console.log("skipping notarization");
1494
+ }
1076
1495
  } else {
1077
- console.log("skipping notarization");
1496
+ // For Windows and Linux, add the self-extracting bundle directly
1497
+ const platformBundlePath = join(buildFolder, `${appFileName}${platformSuffix}${targetOS === 'win' ? '.exe' : ''}`);
1498
+ // Copy the self-extracting bundle to platform-specific filename
1499
+ if (targetOS === 'win') {
1500
+ // On Windows, create a self-extracting exe
1501
+ const selfExtractingExePath = await createWindowsSelfExtractingExe(
1502
+ buildFolder,
1503
+ compressedTarPath,
1504
+ appFileName,
1505
+ targetPaths,
1506
+ buildEnvironment,
1507
+ hash
1508
+ );
1509
+
1510
+ // Wrap Windows installer files in zip for distribution
1511
+ const wrappedExePath = await wrapWindowsInstallerInZip(selfExtractingExePath, buildFolder);
1512
+ artifactsToUpload.push(wrappedExePath);
1513
+
1514
+ // Also keep the raw exe for backwards compatibility (optional)
1515
+ // artifactsToUpload.push(selfExtractingExePath);
1516
+ } else if (targetOS === 'linux') {
1517
+ // Create desktop file for Linux
1518
+ const desktopFileContent = `[Desktop Entry]
1519
+ Version=1.0
1520
+ Type=Application
1521
+ Name=${config.package?.name || config.app.name}
1522
+ Comment=${config.package?.description || config.app.description || ''}
1523
+ Exec=${appFileName}
1524
+ Icon=${appFileName}
1525
+ Terminal=false
1526
+ StartupWMClass=${appFileName}
1527
+ Categories=Application;
1528
+ `;
1529
+
1530
+ const desktopFilePath = join(appBundleFolderPath, `${appFileName}.desktop`);
1531
+ writeFileSync(desktopFilePath, desktopFileContent);
1532
+
1533
+ // Make desktop file executable
1534
+ execSync(`chmod +x ${escapePathForTerminal(desktopFilePath)}`);
1535
+
1536
+ // Create user-friendly launcher script
1537
+ const launcherScriptContent = `#!/bin/bash
1538
+ # ${config.package?.name || config.app.name} Launcher
1539
+ # This script launches the application from any location
1540
+
1541
+ # Get the directory where this script is located
1542
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
1543
+
1544
+ # Find the launcher binary relative to this script
1545
+ LAUNCHER_BINARY="\$SCRIPT_DIR/bin/launcher"
1546
+
1547
+ if [ ! -x "\$LAUNCHER_BINARY" ]; then
1548
+ echo "Error: Could not find launcher binary at \$LAUNCHER_BINARY"
1549
+ exit 1
1550
+ fi
1551
+
1552
+ # Launch the application
1553
+ exec "\$LAUNCHER_BINARY" "\$@"
1554
+ `;
1555
+
1556
+ const launcherScriptPath = join(appBundleFolderPath, `${appFileName}.sh`);
1557
+ writeFileSync(launcherScriptPath, launcherScriptContent);
1558
+ execSync(`chmod +x ${escapePathForTerminal(launcherScriptPath)}`);
1559
+
1560
+ // Create self-extracting Linux binary (similar to Windows approach)
1561
+ const selfExtractingLinuxPath = await createLinuxSelfExtractingBinary(
1562
+ buildFolder,
1563
+ compressedTarPath,
1564
+ appFileName,
1565
+ targetPaths,
1566
+ buildEnvironment
1567
+ );
1568
+
1569
+ // Wrap Linux .run file in tar.gz to preserve permissions
1570
+ const wrappedRunPath = await wrapInArchive(selfExtractingLinuxPath, buildFolder, 'tar.gz');
1571
+ artifactsToUpload.push(wrappedRunPath);
1572
+
1573
+ // Also keep the raw .run for backwards compatibility (optional)
1574
+ // artifactsToUpload.push(selfExtractingLinuxPath);
1575
+
1576
+ // On Linux, create a tar.gz of the bundle
1577
+ const linuxTarPath = join(buildFolder, `${appFileName}.tar.gz`);
1578
+ execSync(`tar -czf ${escapePathForTerminal(linuxTarPath)} -C ${escapePathForTerminal(buildFolder)} ${escapePathForTerminal(basename(appBundleFolderPath))}`);
1579
+ artifactsToUpload.push(linuxTarPath);
1580
+ }
1078
1581
  }
1079
1582
 
1080
1583
  // refresh artifacts folder
@@ -1093,39 +1596,48 @@ if (commandArg === "init") {
1093
1596
  // the download button or display on your marketing site or in the app.
1094
1597
  version: config.app.version,
1095
1598
  hash: hash.toString(),
1599
+ platform: OS,
1600
+ arch: ARCH,
1096
1601
  // channel: buildEnvironment,
1097
1602
  // bucketUrl: config.release.bucketUrl
1098
1603
  });
1099
1604
 
1100
- await Bun.write(join(artifactFolder, "update.json"), updateJsonContent);
1605
+ // update.json (no platform suffix in filename, platform is in folder name)
1606
+ await Bun.write(join(artifactFolder, 'update.json'), updateJsonContent);
1101
1607
 
1102
1608
  // generate bsdiff
1103
1609
  // https://storage.googleapis.com/eggbun-static/electrobun-playground/canary/ElectrobunPlayground-canary.app.tar.zst
1104
1610
  console.log("bucketUrl: ", config.release.bucketUrl);
1105
1611
 
1106
1612
  console.log("generating a patch from the previous version...");
1107
- const urlToPrevUpdateJson = join(
1108
- config.release.bucketUrl,
1109
- buildEnvironment,
1110
- `update.json`
1111
- );
1112
- const cacheBuster = Math.random().toString(36).substring(7);
1113
- const updateJsonResponse = await fetch(
1114
- urlToPrevUpdateJson + `?${cacheBuster}`
1115
- ).catch((err) => {
1116
- console.log("bucketURL not found: ", err);
1117
- });
1613
+
1614
+ // Skip patch generation if bucketUrl is not configured
1615
+ if (!config.release.bucketUrl || config.release.bucketUrl.trim() === '') {
1616
+ console.log("No bucketUrl configured, skipping patch generation");
1617
+ console.log("To enable patch generation, configure bucketUrl in your electrobun.config");
1618
+ } else {
1619
+ const urlToPrevUpdateJson = join(
1620
+ config.release.bucketUrl,
1621
+ buildSubFolder,
1622
+ 'update.json'
1623
+ );
1624
+ const cacheBuster = Math.random().toString(36).substring(7);
1625
+ const updateJsonResponse = await fetch(
1626
+ urlToPrevUpdateJson + `?${cacheBuster}`
1627
+ ).catch((err) => {
1628
+ console.log("bucketURL not found: ", err);
1629
+ });
1118
1630
 
1119
1631
  const urlToLatestTarball = join(
1120
1632
  config.release.bucketUrl,
1121
- buildEnvironment,
1633
+ buildSubFolder,
1122
1634
  `${appFileName}.app.tar.zst`
1123
1635
  );
1124
1636
 
1125
1637
 
1126
1638
  // attempt to get the previous version to create a patch file
1127
- if (updateJsonResponse.ok) {
1128
- const prevUpdateJson = await updateJsonResponse.json();
1639
+ if (updateJsonResponse && updateJsonResponse.ok) {
1640
+ const prevUpdateJson = await updateJsonResponse!.json();
1129
1641
 
1130
1642
  const prevHash = prevUpdateJson.hash;
1131
1643
  console.log("PREVIOUS HASH", prevHash);
@@ -1164,7 +1676,7 @@ if (commandArg === "init") {
1164
1676
  console.log("diff previous and new tarballs...");
1165
1677
  // Run it as a separate process to leverage multi-threadedness
1166
1678
  // especially for creating multiple diffs in parallel
1167
- const bsdiffpath = PATHS.BSDIFF;
1679
+ const bsdiffpath = targetPaths.BSDIFF;
1168
1680
  const patchFilePath = join(buildFolder, `${prevHash}.patch`);
1169
1681
  artifactsToUpload.push(patchFilePath);
1170
1682
  const result = Bun.spawnSync(
@@ -1181,6 +1693,7 @@ if (commandArg === "init") {
1181
1693
  console.log("prevoius version not found at: ", urlToLatestTarball);
1182
1694
  console.log("skipping diff generation");
1183
1695
  }
1696
+ } // End of bucketUrl validation block
1184
1697
 
1185
1698
  // compress all the upload files
1186
1699
  console.log("copying artifacts...");
@@ -1209,10 +1722,7 @@ if (commandArg === "init") {
1209
1722
  // todo (yoav): rename to start
1210
1723
 
1211
1724
  // run the project in dev mode
1212
- // this runs the cli in debug mode, on macos executes the app bundle,
1213
- // there is another copy of the cli in the app bundle that will execute the app
1214
- // the two cli processes communicate via named pipes and together manage the dev
1215
- // lifecycle and debug functionality
1725
+ // this runs the bundled bun binary with main.js directly
1216
1726
 
1217
1727
  // Note: this cli will be a bun single-file-executable
1218
1728
  // Note: we want to use the version of bun that's packaged with electrobun
@@ -1290,7 +1800,12 @@ function getConfig() {
1290
1800
  if (existsSync(configPath)) {
1291
1801
  const configFileContents = readFileSync(configPath, "utf8");
1292
1802
  // Note: we want this to hard fail if there's a syntax error
1293
- loadedConfig = JSON.parse(configFileContents);
1803
+ try {
1804
+ loadedConfig = JSON.parse(configFileContents);
1805
+ } catch (error) {
1806
+ console.error("Failed to parse config file:", error);
1807
+ console.error("using default config instead");
1808
+ }
1294
1809
  }
1295
1810
 
1296
1811
  // todo (yoav): write a deep clone fn
@@ -1312,6 +1827,18 @@ function getConfig() {
1312
1827
  ...(loadedConfig?.build?.mac?.entitlements || {}),
1313
1828
  },
1314
1829
  },
1830
+ win: {
1831
+ ...defaultConfig.build.win,
1832
+ ...(loadedConfig?.build?.win || {}),
1833
+ },
1834
+ linux: {
1835
+ ...defaultConfig.build.linux,
1836
+ ...(loadedConfig?.build?.linux || {}),
1837
+ },
1838
+ bun: {
1839
+ ...defaultConfig.build.bun,
1840
+ ...(loadedConfig?.build?.bun || {}),
1841
+ }
1315
1842
  },
1316
1843
  scripts: {
1317
1844
  ...defaultConfig.scripts,
@@ -1347,6 +1874,303 @@ function getEntitlementValue(value: boolean | string) {
1347
1874
  }
1348
1875
  }
1349
1876
 
1877
+ async function createWindowsSelfExtractingExe(
1878
+ buildFolder: string,
1879
+ compressedTarPath: string,
1880
+ appFileName: string,
1881
+ targetPaths: any,
1882
+ buildEnvironment: string,
1883
+ hash: string
1884
+ ): Promise<string> {
1885
+ console.log("Creating Windows installer with separate archive...");
1886
+
1887
+ // Format: MyApp-Setup.exe (stable) or MyApp-Setup-canary.exe (non-stable)
1888
+ const setupFileName = buildEnvironment === "stable"
1889
+ ? `${config.app.name}-Setup.exe`
1890
+ : `${config.app.name}-Setup-${buildEnvironment}.exe`;
1891
+
1892
+ const outputExePath = join(buildFolder, setupFileName);
1893
+
1894
+ // Copy the extractor exe
1895
+ const extractorExe = readFileSync(targetPaths.EXTRACTOR);
1896
+ writeFileSync(outputExePath, extractorExe);
1897
+
1898
+ // Create metadata JSON file
1899
+ const metadata = {
1900
+ identifier: config.app.identifier,
1901
+ name: config.app.name,
1902
+ channel: buildEnvironment,
1903
+ hash: hash
1904
+ };
1905
+ const metadataJson = JSON.stringify(metadata, null, 2);
1906
+ const metadataFileName = setupFileName.replace('.exe', '.metadata.json');
1907
+ const metadataPath = join(buildFolder, metadataFileName);
1908
+ writeFileSync(metadataPath, metadataJson);
1909
+
1910
+ // Copy the compressed archive with matching name
1911
+ const archiveFileName = setupFileName.replace('.exe', '.tar.zst');
1912
+ const archivePath = join(buildFolder, archiveFileName);
1913
+ copyFileSync(compressedTarPath, archivePath);
1914
+
1915
+ // Make the exe executable (though Windows doesn't need chmod)
1916
+ if (OS !== 'win') {
1917
+ execSync(`chmod +x ${escapePathForTerminal(outputExePath)}`);
1918
+ }
1919
+
1920
+ const exeSize = statSync(outputExePath).size;
1921
+ const archiveSize = statSync(archivePath).size;
1922
+ const totalSize = exeSize + archiveSize;
1923
+
1924
+ console.log(`Created Windows installer:`);
1925
+ console.log(` - Extractor: ${outputExePath} (${(exeSize / 1024 / 1024).toFixed(2)} MB)`);
1926
+ console.log(` - Archive: ${archivePath} (${(archiveSize / 1024 / 1024).toFixed(2)} MB)`);
1927
+ console.log(` - Metadata: ${metadataPath}`);
1928
+ console.log(` - Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
1929
+
1930
+ return outputExePath;
1931
+ }
1932
+
1933
+ async function createLinuxSelfExtractingBinary(
1934
+ buildFolder: string,
1935
+ compressedTarPath: string,
1936
+ appFileName: string,
1937
+ targetPaths: any,
1938
+ buildEnvironment: string
1939
+ ): Promise<string> {
1940
+ console.log("Creating self-extracting Linux binary...");
1941
+
1942
+ // Format: MyApp-Setup.run (stable) or MyApp-Setup-canary.run (non-stable)
1943
+ const setupFileName = buildEnvironment === "stable"
1944
+ ? `${config.app.name}-Setup.run`
1945
+ : `${config.app.name}-Setup-${buildEnvironment}.run`;
1946
+
1947
+ const outputPath = join(buildFolder, setupFileName);
1948
+
1949
+ // Read the extractor binary
1950
+ const extractorBinary = readFileSync(targetPaths.EXTRACTOR);
1951
+
1952
+ // Read the compressed archive
1953
+ const compressedArchive = readFileSync(compressedTarPath);
1954
+
1955
+ // Create metadata JSON
1956
+ const metadata = {
1957
+ identifier: config.app.identifier,
1958
+ name: config.app.name,
1959
+ channel: buildEnvironment
1960
+ };
1961
+ const metadataJson = JSON.stringify(metadata);
1962
+ const metadataBuffer = Buffer.from(metadataJson, 'utf8');
1963
+
1964
+ // Create marker buffers
1965
+ const metadataMarker = Buffer.from('ELECTROBUN_METADATA_V1', 'utf8');
1966
+ const archiveMarker = Buffer.from('ELECTROBUN_ARCHIVE_V1', 'utf8');
1967
+
1968
+ // Combine extractor + metadata marker + metadata + archive marker + archive
1969
+ const combinedBuffer = Buffer.concat([
1970
+ extractorBinary,
1971
+ metadataMarker,
1972
+ metadataBuffer,
1973
+ archiveMarker,
1974
+ compressedArchive
1975
+ ]);
1976
+
1977
+ // Write the self-extracting binary
1978
+ writeFileSync(outputPath, combinedBuffer, { mode: 0o755 });
1979
+
1980
+ // Ensure it's executable (redundant but explicit)
1981
+ execSync(`chmod +x ${escapePathForTerminal(outputPath)}`);
1982
+
1983
+ console.log(`Created self-extracting Linux binary: ${outputPath} (${(combinedBuffer.length / 1024 / 1024).toFixed(2)} MB)`);
1984
+
1985
+ return outputPath;
1986
+ }
1987
+
1988
+ async function wrapWindowsInstallerInZip(exePath: string, buildFolder: string): Promise<string> {
1989
+ const exeName = basename(exePath);
1990
+ const exeStem = exeName.replace('.exe', '');
1991
+
1992
+ // Derive the paths for metadata and archive files
1993
+ const metadataPath = join(buildFolder, `${exeStem}.metadata.json`);
1994
+ const archivePath = join(buildFolder, `${exeStem}.tar.zst`);
1995
+ const zipPath = join(buildFolder, `${exeStem}.zip`);
1996
+
1997
+ // Verify all files exist
1998
+ if (!existsSync(exePath)) {
1999
+ throw new Error(`Installer exe not found: ${exePath}`);
2000
+ }
2001
+ if (!existsSync(metadataPath)) {
2002
+ throw new Error(`Metadata file not found: ${metadataPath}`);
2003
+ }
2004
+ if (!existsSync(archivePath)) {
2005
+ throw new Error(`Archive file not found: ${archivePath}`);
2006
+ }
2007
+
2008
+ // Create zip archive
2009
+ const output = createWriteStream(zipPath);
2010
+ const archive = archiver('zip', {
2011
+ zlib: { level: 9 } // Maximum compression
2012
+ });
2013
+
2014
+ return new Promise((resolve, reject) => {
2015
+ output.on('close', () => {
2016
+ console.log(`Created Windows installer package: ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(2)} MB)`);
2017
+ resolve(zipPath);
2018
+ });
2019
+
2020
+ archive.on('error', (err) => {
2021
+ reject(err);
2022
+ });
2023
+
2024
+ archive.pipe(output);
2025
+
2026
+ // Add all three files to the archive
2027
+ archive.file(exePath, { name: basename(exePath) });
2028
+ archive.file(metadataPath, { name: basename(metadataPath) });
2029
+ archive.file(archivePath, { name: basename(archivePath) });
2030
+
2031
+ archive.finalize();
2032
+ });
2033
+ }
2034
+
2035
+ async function wrapInArchive(filePath: string, buildFolder: string, archiveType: 'tar.gz' | 'zip'): Promise<string> {
2036
+ const fileName = basename(filePath);
2037
+ const fileDir = dirname(filePath);
2038
+
2039
+ if (archiveType === 'tar.gz') {
2040
+ // Output filename: Setup.exe -> Setup.exe.tar.gz or Setup.run -> Setup.run.tar.gz
2041
+ const archivePath = filePath + '.tar.gz';
2042
+
2043
+ // For Linux files, ensure they have executable permissions before archiving
2044
+ if (fileName.endsWith('.run')) {
2045
+ try {
2046
+ // Try to set executable permissions (will only work on Unix-like systems)
2047
+ execSync(`chmod +x ${escapePathForTerminal(filePath)}`, { stdio: 'ignore' });
2048
+ } catch {
2049
+ // Ignore errors on Windows hosts
2050
+ }
2051
+ }
2052
+
2053
+ // Create tar.gz archive preserving permissions
2054
+ // Using the tar package for cross-platform compatibility
2055
+ await tar.c(
2056
+ {
2057
+ gzip: true,
2058
+ file: archivePath,
2059
+ cwd: fileDir,
2060
+ portable: true, // Ensures consistent behavior across platforms
2061
+ preservePaths: false,
2062
+ // The tar package should preserve file modes when creating archives
2063
+ },
2064
+ [fileName]
2065
+ );
2066
+
2067
+ console.log(`Created archive: ${archivePath} (preserving executable permissions)`);
2068
+ return archivePath;
2069
+ } else if (archiveType === 'zip') {
2070
+ // Output filename: Setup.exe -> Setup.zip
2071
+ const archivePath = filePath.replace(/\.[^.]+$/, '.zip');
2072
+
2073
+ // Create zip archive
2074
+ const output = createWriteStream(archivePath);
2075
+ const archive = archiver('zip', {
2076
+ zlib: { level: 9 } // Maximum compression
2077
+ });
2078
+
2079
+ return new Promise((resolve, reject) => {
2080
+ output.on('close', () => {
2081
+ console.log(`Created archive: ${archivePath} (${(archive.pointer() / 1024 / 1024).toFixed(2)} MB)`);
2082
+ resolve(archivePath);
2083
+ });
2084
+
2085
+ archive.on('error', (err) => {
2086
+ reject(err);
2087
+ });
2088
+
2089
+ archive.pipe(output);
2090
+
2091
+ // Add the file to the archive
2092
+ archive.file(filePath, { name: fileName });
2093
+
2094
+ archive.finalize();
2095
+ });
2096
+ }
2097
+ }
2098
+
2099
+ async function createAppImage(buildFolder: string, appBundlePath: string, appFileName: string, config: any): Promise<string | null> {
2100
+ try {
2101
+ console.log("Creating AppImage...");
2102
+
2103
+ // Create AppDir structure
2104
+ const appDirPath = join(buildFolder, `${appFileName}.AppDir`);
2105
+ mkdirSync(appDirPath, { recursive: true });
2106
+
2107
+ // Copy app bundle contents to AppDir
2108
+ const appDirAppPath = join(appDirPath, "app");
2109
+ cpSync(appBundlePath, appDirAppPath, { recursive: true });
2110
+
2111
+ // Create AppRun script (main executable for AppImage)
2112
+ const appRunContent = `#!/bin/bash
2113
+ HERE="$(dirname "$(readlink -f "\${0}")")"
2114
+ export APPDIR="\$HERE"
2115
+ cd "\$HERE"
2116
+ exec "\$HERE/app/bin/launcher" "\$@"
2117
+ `;
2118
+
2119
+ const appRunPath = join(appDirPath, "AppRun");
2120
+ writeFileSync(appRunPath, appRunContent);
2121
+ execSync(`chmod +x ${escapePathForTerminal(appRunPath)}`);
2122
+
2123
+ // Create desktop file in AppDir root
2124
+ const desktopContent = `[Desktop Entry]
2125
+ Version=1.0
2126
+ Type=Application
2127
+ Name=${config.package?.name || config.app.name}
2128
+ Comment=${config.package?.description || config.app.description || ''}
2129
+ Exec=AppRun
2130
+ Icon=${appFileName}
2131
+ Terminal=false
2132
+ StartupWMClass=${appFileName}
2133
+ Categories=Application;
2134
+ `;
2135
+
2136
+ const appDirDesktopPath = join(appDirPath, `${appFileName}.desktop`);
2137
+ writeFileSync(appDirDesktopPath, desktopContent);
2138
+
2139
+ // Copy icon if it exists
2140
+ const iconPath = config.build.linux?.appImageIcon;
2141
+ if (iconPath && existsSync(iconPath)) {
2142
+ const iconDestPath = join(appDirPath, `${appFileName}.png`);
2143
+ cpSync(iconPath, iconDestPath);
2144
+ }
2145
+
2146
+ // Try to create AppImage using available tools
2147
+ const appImagePath = join(buildFolder, `${appFileName}.AppImage`);
2148
+
2149
+ // Check for appimagetool
2150
+ try {
2151
+ execSync('which appimagetool', { stdio: 'pipe' });
2152
+ console.log("Using appimagetool to create AppImage...");
2153
+ execSync(`appimagetool ${escapePathForTerminal(appDirPath)} ${escapePathForTerminal(appImagePath)}`, { stdio: 'inherit' });
2154
+ return appImagePath;
2155
+ } catch {
2156
+ // Check for Docker
2157
+ try {
2158
+ execSync('which docker', { stdio: 'pipe' });
2159
+ console.log("Using Docker to create AppImage...");
2160
+ execSync(`docker run --rm -v "${buildFolder}:/workspace" linuxserver/appimagetool "/workspace/${basename(appDirPath)}" "/workspace/${basename(appImagePath)}"`, { stdio: 'inherit' });
2161
+ return appImagePath;
2162
+ } catch {
2163
+ console.warn("Neither appimagetool nor Docker found. AppImage creation skipped.");
2164
+ console.warn("To create AppImages, install appimagetool or Docker.");
2165
+ return null;
2166
+ }
2167
+ }
2168
+ } catch (error) {
2169
+ console.error("Failed to create AppImage:", error);
2170
+ return null;
2171
+ }
2172
+ }
2173
+
1350
2174
  function codesignAppBundle(
1351
2175
  appBundleOrDmgPath: string,
1352
2176
  entitlementsFilePath?: string
@@ -1473,8 +2297,8 @@ function notarizeAndStaple(appOrDmgPath: string) {
1473
2297
  // 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
2298
  // either way you can pass in the parent folder here for that flexibility.
1475
2299
  // 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') {
2300
+ function createAppBundle(bundleName: string, parentFolder: string, targetOS: 'macos' | 'win' | 'linux') {
2301
+ if (targetOS === 'macos') {
1478
2302
  // macOS bundle structure
1479
2303
  const bundleFileName = `${bundleName}.app`;
1480
2304
  const appBundleFolderPath = join(parentFolder, bundleFileName);
@@ -1501,7 +2325,7 @@ function createAppBundle(bundleName: string, parentFolder: string) {
1501
2325
  appBundleFolderResourcesPath,
1502
2326
  appBundleFolderFrameworksPath,
1503
2327
  };
1504
- } else if (OS === 'linux' || OS === 'win') {
2328
+ } else if (targetOS === 'linux' || targetOS === 'win') {
1505
2329
  // Linux/Windows simpler structure
1506
2330
  const appBundleFolderPath = join(parentFolder, bundleName);
1507
2331
  const appBundleFolderContentsPath = appBundleFolderPath; // No Contents folder needed
@@ -1522,6 +2346,6 @@ function createAppBundle(bundleName: string, parentFolder: string) {
1522
2346
  appBundleFolderFrameworksPath,
1523
2347
  };
1524
2348
  } else {
1525
- throw new Error(`Unsupported OS: ${OS}`);
2349
+ throw new Error(`Unsupported OS: ${targetOS}`);
1526
2350
  }
1527
2351
  }