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