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