electrobun 0.0.18 → 0.0.19-beta.6
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/BETA_RELEASE.md +67 -0
- package/README.md +48 -21
- package/{dist → bin}/electrobun +0 -0
- package/package.json +23 -29
- package/src/cli/bun.lockb +0 -0
- package/src/cli/index.ts +1527 -0
- package/src/cli/package-lock.json +93 -0
- package/src/cli/package.json +14 -0
- package/test-npm-install.sh +34 -0
- package/dist/api/browser/builtinrpcSchema.ts +0 -10
- package/dist/api/browser/index.ts +0 -474
- package/dist/api/browser/stylesAndElements.ts +0 -3
- package/dist/api/browser/webviewtag.ts +0 -650
- package/dist/api/bun/core/ApplicationMenu.ts +0 -66
- package/dist/api/bun/core/BrowserView.ts +0 -417
- package/dist/api/bun/core/BrowserWindow.ts +0 -178
- package/dist/api/bun/core/ContextMenu.ts +0 -67
- package/dist/api/bun/core/Paths.ts +0 -5
- package/dist/api/bun/core/Socket.ts +0 -181
- package/dist/api/bun/core/Tray.ts +0 -105
- package/dist/api/bun/core/Updater.ts +0 -388
- package/dist/api/bun/core/Utils.ts +0 -48
- package/dist/api/bun/events/applicationEvents.ts +0 -14
- package/dist/api/bun/events/event.ts +0 -29
- package/dist/api/bun/events/eventEmitter.ts +0 -45
- package/dist/api/bun/events/trayEvents.ts +0 -9
- package/dist/api/bun/events/webviewEvents.ts +0 -19
- package/dist/api/bun/events/windowEvents.ts +0 -12
- package/dist/api/bun/index.ts +0 -45
- package/dist/api/bun/proc/zig.ts +0 -622
- package/dist/bsdiff +0 -0
- package/dist/bspatch +0 -0
- package/dist/bun +0 -0
- package/dist/extractor +0 -0
- package/dist/launcher +0 -0
- package/dist/webview +0 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,1527 @@
|
|
|
1
|
+
import { join, dirname, basename } from "path";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
cpSync,
|
|
6
|
+
rmdirSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
createWriteStream,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
} from "fs";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
import tar from "tar";
|
|
13
|
+
import { ZstdInit } from "@oneidentity/zstd-js/wasm";
|
|
14
|
+
import {platform, arch} from 'os';
|
|
15
|
+
// import { loadBsdiff, loadBspatch } from 'bsdiff-wasm';
|
|
16
|
+
// MacOS named pipes hang at around 4KB
|
|
17
|
+
const MAX_CHUNK_SIZE = 1024 * 2;
|
|
18
|
+
|
|
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
|
+
|
|
47
|
+
|
|
48
|
+
const binExt = OS === 'win' ? '.exe' : '';
|
|
49
|
+
|
|
50
|
+
// this when run as an npm script this will be where the folder where package.json is.
|
|
51
|
+
const projectRoot = process.cwd();
|
|
52
|
+
const configName = "electrobun.config";
|
|
53
|
+
const configPath = join(projectRoot, configName);
|
|
54
|
+
|
|
55
|
+
// Note: cli args can be called via npm bun /path/to/electorbun/binary arg1 arg2
|
|
56
|
+
const indexOfElectrobun = process.argv.findIndex((arg) =>
|
|
57
|
+
arg.includes("electrobun")
|
|
58
|
+
);
|
|
59
|
+
const commandArg = process.argv[indexOfElectrobun + 1] || "launcher";
|
|
60
|
+
|
|
61
|
+
const ELECTROBUN_DEP_PATH = join(projectRoot, "node_modules", "electrobun");
|
|
62
|
+
|
|
63
|
+
// When debugging electrobun with the example app use the builds (dev or release) right from the source folder
|
|
64
|
+
// For developers using electrobun cli via npm use the release versions in /dist
|
|
65
|
+
// 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
|
+
|
|
94
|
+
async function ensureCoreDependencies() {
|
|
95
|
+
// Check if core dependencies already exist
|
|
96
|
+
if (existsSync(PATHS.BUN_BINARY) && existsSync(PATHS.LAUNCHER_RELEASE)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log('Core dependencies not found, downloading...');
|
|
101
|
+
|
|
102
|
+
// Get the current Electrobun version from package.json
|
|
103
|
+
const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
|
|
104
|
+
let version = 'latest';
|
|
105
|
+
|
|
106
|
+
if (existsSync(packageJsonPath)) {
|
|
107
|
+
try {
|
|
108
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
109
|
+
version = `v${packageJson.version}`;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn('Could not read package version, using latest');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const platformName = OS === 'macos' ? 'darwin' : OS === 'win' ? 'win32' : 'linux';
|
|
116
|
+
const archName = ARCH;
|
|
117
|
+
const mainTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-${platformName}-${archName}.tar.gz`;
|
|
118
|
+
|
|
119
|
+
console.log(`Downloading core binaries from: ${mainTarballUrl}`);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Download main tarball
|
|
123
|
+
const response = await fetch(mainTarballUrl);
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new Error(`Failed to download binaries: ${response.status} ${response.statusText}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create temp file
|
|
129
|
+
const tempFile = join(ELECTROBUN_DEP_PATH, 'main-temp.tar.gz');
|
|
130
|
+
const fileStream = createWriteStream(tempFile);
|
|
131
|
+
|
|
132
|
+
// Write response to file
|
|
133
|
+
if (response.body) {
|
|
134
|
+
const reader = response.body.getReader();
|
|
135
|
+
while (true) {
|
|
136
|
+
const { done, value } = await reader.read();
|
|
137
|
+
if (done) break;
|
|
138
|
+
fileStream.write(Buffer.from(value));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
fileStream.end();
|
|
142
|
+
|
|
143
|
+
// Extract to dist directory
|
|
144
|
+
console.log('Extracting core dependencies...');
|
|
145
|
+
await tar.x({
|
|
146
|
+
file: tempFile,
|
|
147
|
+
cwd: join(ELECTROBUN_DEP_PATH, 'dist'),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Clean up temp file
|
|
151
|
+
unlinkSync(tempFile);
|
|
152
|
+
|
|
153
|
+
console.log('Core dependencies downloaded and cached successfully');
|
|
154
|
+
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error('Failed to download core dependencies:', error.message);
|
|
157
|
+
console.error('Please ensure you have an internet connection and the release exists.');
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function ensureCEFDependencies() {
|
|
163
|
+
// Check if CEF dependencies already exist
|
|
164
|
+
if (existsSync(PATHS.CEF_DIR)) {
|
|
165
|
+
console.log('CEF dependencies found, using cached version');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log('CEF dependencies not found, downloading...');
|
|
170
|
+
|
|
171
|
+
// Get the current Electrobun version from package.json
|
|
172
|
+
const packageJsonPath = join(ELECTROBUN_DEP_PATH, 'package.json');
|
|
173
|
+
let version = 'latest';
|
|
174
|
+
|
|
175
|
+
if (existsSync(packageJsonPath)) {
|
|
176
|
+
try {
|
|
177
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
178
|
+
version = `v${packageJson.version}`;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.warn('Could not read package version, using latest');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const platformName = OS === 'macos' ? 'darwin' : OS === 'win' ? 'win32' : 'linux';
|
|
185
|
+
const archName = ARCH;
|
|
186
|
+
const cefTarballUrl = `https://github.com/blackboardsh/electrobun/releases/download/${version}/electrobun-cef-${platformName}-${archName}.tar.gz`;
|
|
187
|
+
|
|
188
|
+
console.log(`Downloading CEF from: ${cefTarballUrl}`);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
// Download CEF tarball
|
|
192
|
+
const response = await fetch(cefTarballUrl);
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
throw new Error(`Failed to download CEF: ${response.status} ${response.statusText}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Create temp file
|
|
198
|
+
const tempFile = join(ELECTROBUN_DEP_PATH, 'cef-temp.tar.gz');
|
|
199
|
+
const fileStream = createWriteStream(tempFile);
|
|
200
|
+
|
|
201
|
+
// Write response to file
|
|
202
|
+
if (response.body) {
|
|
203
|
+
const reader = response.body.getReader();
|
|
204
|
+
while (true) {
|
|
205
|
+
const { done, value } = await reader.read();
|
|
206
|
+
if (done) break;
|
|
207
|
+
fileStream.write(Buffer.from(value));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
fileStream.end();
|
|
211
|
+
|
|
212
|
+
// Extract to dist directory
|
|
213
|
+
console.log('Extracting CEF dependencies...');
|
|
214
|
+
await tar.x({
|
|
215
|
+
file: tempFile,
|
|
216
|
+
cwd: join(ELECTROBUN_DEP_PATH, 'dist'),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Clean up temp file
|
|
220
|
+
unlinkSync(tempFile);
|
|
221
|
+
|
|
222
|
+
console.log('CEF dependencies downloaded and cached successfully');
|
|
223
|
+
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error('Failed to download CEF dependencies:', error.message);
|
|
226
|
+
console.error('Please ensure you have an internet connection and the release exists.');
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const commandDefaults = {
|
|
232
|
+
init: {
|
|
233
|
+
projectRoot,
|
|
234
|
+
config: "electrobun.config",
|
|
235
|
+
},
|
|
236
|
+
build: {
|
|
237
|
+
projectRoot,
|
|
238
|
+
config: "electrobun.config",
|
|
239
|
+
},
|
|
240
|
+
dev: {
|
|
241
|
+
projectRoot,
|
|
242
|
+
config: "electrobun.config",
|
|
243
|
+
},
|
|
244
|
+
launcher: {
|
|
245
|
+
projectRoot,
|
|
246
|
+
config: "electrobun.config",
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// todo (yoav): add types for config
|
|
251
|
+
const defaultConfig = {
|
|
252
|
+
app: {
|
|
253
|
+
name: "MyApp",
|
|
254
|
+
identifier: "com.example.myapp",
|
|
255
|
+
version: "0.1.0",
|
|
256
|
+
},
|
|
257
|
+
build: {
|
|
258
|
+
buildFolder: "build",
|
|
259
|
+
artifactFolder: "artifacts",
|
|
260
|
+
mac: {
|
|
261
|
+
codesign: false,
|
|
262
|
+
notarize: false,
|
|
263
|
+
bundleCEF: false,
|
|
264
|
+
entitlements: {
|
|
265
|
+
// This entitlement is required for Electrobun apps with a hardened runtime (required for notarization) to run on macos
|
|
266
|
+
"com.apple.security.cs.allow-jit": true,
|
|
267
|
+
},
|
|
268
|
+
icons: "icon.iconset",
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
scripts: {
|
|
272
|
+
postBuild: "",
|
|
273
|
+
},
|
|
274
|
+
release: {
|
|
275
|
+
bucketUrl: "",
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const command = commandDefaults[commandArg];
|
|
280
|
+
|
|
281
|
+
if (!command) {
|
|
282
|
+
console.error("Invalid command: ", commandArg);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const config = getConfig();
|
|
287
|
+
|
|
288
|
+
const envArg =
|
|
289
|
+
process.argv.find((arg) => arg.startsWith("env="))?.split("=")[1] || "";
|
|
290
|
+
|
|
291
|
+
const validEnvironments = ["dev", "canary", "stable"];
|
|
292
|
+
|
|
293
|
+
// todo (yoav): dev, canary, and stable;
|
|
294
|
+
const buildEnvironment: "dev" | "canary" | "stable" =
|
|
295
|
+
validEnvironments.includes(envArg) ? envArg : "dev";
|
|
296
|
+
|
|
297
|
+
// todo (yoav): dev builds should include the branch name, and/or allow configuration via external config
|
|
298
|
+
const buildSubFolder = `${buildEnvironment}`;
|
|
299
|
+
|
|
300
|
+
const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
|
|
301
|
+
|
|
302
|
+
const artifactFolder = join(
|
|
303
|
+
projectRoot,
|
|
304
|
+
config.build.artifactFolder,
|
|
305
|
+
buildSubFolder
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const buildIcons = (appBundleFolderResourcesPath: string) => {
|
|
309
|
+
if (OS === 'macos' && config.build.mac.icons) {
|
|
310
|
+
const iconSourceFolder = join(projectRoot, config.build.mac.icons);
|
|
311
|
+
const iconDestPath = join(appBundleFolderResourcesPath, "AppIcon.icns");
|
|
312
|
+
if (existsSync(iconSourceFolder)) {
|
|
313
|
+
Bun.spawnSync(
|
|
314
|
+
["iconutil", "-c", "icns", "-o", iconDestPath, iconSourceFolder],
|
|
315
|
+
{
|
|
316
|
+
cwd: appBundleFolderResourcesPath,
|
|
317
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
318
|
+
env: {
|
|
319
|
+
...process.env,
|
|
320
|
+
ELECTROBUN_BUILD_ENV: buildEnvironment,
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
function escapePathForTerminal(filePath: string) {
|
|
329
|
+
// List of special characters to escape
|
|
330
|
+
const specialChars = [
|
|
331
|
+
" ",
|
|
332
|
+
"(",
|
|
333
|
+
")",
|
|
334
|
+
"&",
|
|
335
|
+
"|",
|
|
336
|
+
";",
|
|
337
|
+
"<",
|
|
338
|
+
">",
|
|
339
|
+
"`",
|
|
340
|
+
"\\",
|
|
341
|
+
'"',
|
|
342
|
+
"'",
|
|
343
|
+
"$",
|
|
344
|
+
"*",
|
|
345
|
+
"?",
|
|
346
|
+
"[",
|
|
347
|
+
"]",
|
|
348
|
+
"#",
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
let escapedPath = "";
|
|
352
|
+
for (const char of filePath) {
|
|
353
|
+
if (specialChars.includes(char)) {
|
|
354
|
+
escapedPath += `\\${char}`;
|
|
355
|
+
} else {
|
|
356
|
+
escapedPath += char;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return escapedPath;
|
|
361
|
+
}
|
|
362
|
+
// MyApp
|
|
363
|
+
|
|
364
|
+
// const appName = config.app.name.replace(/\s/g, '-').toLowerCase();
|
|
365
|
+
|
|
366
|
+
const appFileName = (
|
|
367
|
+
buildEnvironment === "stable"
|
|
368
|
+
? config.app.name
|
|
369
|
+
: `${config.app.name}-${buildEnvironment}`
|
|
370
|
+
)
|
|
371
|
+
.replace(/\s/g, "")
|
|
372
|
+
.replace(/\./g, "-");
|
|
373
|
+
const bundleFileName = OS === 'macos' ? `${appFileName}.app` : appFileName;
|
|
374
|
+
|
|
375
|
+
// const logPath = `/Library/Logs/Electrobun/ExampleApp/dev/out.log`;
|
|
376
|
+
|
|
377
|
+
let proc = null;
|
|
378
|
+
|
|
379
|
+
if (commandArg === "init") {
|
|
380
|
+
// todo (yoav): init a repo folder structure
|
|
381
|
+
console.log("initializing electrobun project");
|
|
382
|
+
} else if (commandArg === "build") {
|
|
383
|
+
// refresh build folder
|
|
384
|
+
if (existsSync(buildFolder)) {
|
|
385
|
+
rmdirSync(buildFolder, { recursive: true });
|
|
386
|
+
}
|
|
387
|
+
mkdirSync(buildFolder, { recursive: true });
|
|
388
|
+
// bundle bun to build/bun
|
|
389
|
+
const bunConfig = config.build.bun;
|
|
390
|
+
const bunSource = join(projectRoot, bunConfig.entrypoint);
|
|
391
|
+
|
|
392
|
+
if (!existsSync(bunSource)) {
|
|
393
|
+
console.error(
|
|
394
|
+
`failed to bundle ${bunSource} because it doesn't exist.\n You need a config.build.bun.entrypoint source file to build.`
|
|
395
|
+
);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// build macos bundle
|
|
400
|
+
const {
|
|
401
|
+
appBundleFolderPath,
|
|
402
|
+
appBundleFolderContentsPath,
|
|
403
|
+
appBundleMacOSPath,
|
|
404
|
+
appBundleFolderResourcesPath,
|
|
405
|
+
appBundleFolderFrameworksPath,
|
|
406
|
+
} = createAppBundle(appFileName, buildFolder);
|
|
407
|
+
|
|
408
|
+
const appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
|
|
409
|
+
|
|
410
|
+
mkdirSync(appBundleAppCodePath, { recursive: true });
|
|
411
|
+
|
|
412
|
+
// const bundledBunPath = join(appBundleMacOSPath, 'bun');
|
|
413
|
+
// cpSync(bunPath, bundledBunPath);
|
|
414
|
+
|
|
415
|
+
// Note: for sandboxed apps, MacOS will use the CFBundleIdentifier to create a unique container for the app,
|
|
416
|
+
// mirroring folders like Application Support, Caches, etc. in the user's Library folder that the sandboxed app
|
|
417
|
+
// gets access to.
|
|
418
|
+
|
|
419
|
+
// We likely want to let users configure this for different environments (eg: dev, canary, stable) and/or
|
|
420
|
+
// provide methods to help segment data in those folders based on channel/environment
|
|
421
|
+
const InfoPlistContents = `<?xml version="1.0" encoding="UTF-8"?>
|
|
422
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
423
|
+
<plist version="1.0">
|
|
424
|
+
<dict>
|
|
425
|
+
<key>CFBundleExecutable</key>
|
|
426
|
+
<string>launcher</string>
|
|
427
|
+
<key>CFBundleIdentifier</key>
|
|
428
|
+
<string>${config.app.identifier}</string>
|
|
429
|
+
<key>CFBundleName</key>
|
|
430
|
+
<string>${appFileName}</string>
|
|
431
|
+
<key>CFBundleVersion</key>
|
|
432
|
+
<string>${config.app.version}</string>
|
|
433
|
+
<key>CFBundlePackageType</key>
|
|
434
|
+
<string>APPL</string>
|
|
435
|
+
<key>CFBundleIconFile</key>
|
|
436
|
+
<string>AppIcon</string>
|
|
437
|
+
</dict>
|
|
438
|
+
</plist>`;
|
|
439
|
+
|
|
440
|
+
await Bun.write(
|
|
441
|
+
join(appBundleFolderContentsPath, "Info.plist"),
|
|
442
|
+
InfoPlistContents
|
|
443
|
+
);
|
|
444
|
+
// in dev builds the log file is a named pipe so we can stream it back to the terminal
|
|
445
|
+
// in canary/stable builds it'll be a regular log file
|
|
446
|
+
// const LauncherContents = `#!/bin/bash
|
|
447
|
+
// # change directory from whatever open was or double clicking on the app to the dir of the bin in the app bundle
|
|
448
|
+
// cd "$(dirname "$0")"/
|
|
449
|
+
|
|
450
|
+
// # Define the log file path
|
|
451
|
+
// LOG_FILE="$HOME/${logPath}"
|
|
452
|
+
|
|
453
|
+
// # Ensure the directory exists
|
|
454
|
+
// mkdir -p "$(dirname "$LOG_FILE")"
|
|
455
|
+
|
|
456
|
+
// if [[ ! -p $LOG_FILE ]]; then
|
|
457
|
+
// mkfifo $LOG_FILE
|
|
458
|
+
// fi
|
|
459
|
+
|
|
460
|
+
// # Execute bun and redirect stdout and stderr to the log file
|
|
461
|
+
// ./bun ../Resources/app/bun/index.js >"$LOG_FILE" 2>&1
|
|
462
|
+
// `;
|
|
463
|
+
|
|
464
|
+
// // Launcher binary
|
|
465
|
+
// // todo (yoav): This will likely be a zig compiled binary in the future
|
|
466
|
+
// Bun.write(join(appBundleMacOSPath, 'MyApp'), LauncherContents);
|
|
467
|
+
// chmodSync(join(appBundleMacOSPath, 'MyApp'), '755');
|
|
468
|
+
// const zigLauncherBinarySource = join(projectRoot, 'node_modules', 'electrobun', 'src', 'launcher', 'zig-out', 'bin', 'launcher');
|
|
469
|
+
// const zigLauncherDestination = join(appBundleMacOSPath, 'MyApp');
|
|
470
|
+
// const destLauncherFolder = dirname(zigLauncherDestination);
|
|
471
|
+
// if (!existsSync(destLauncherFolder)) {
|
|
472
|
+
// // console.info('creating folder: ', destFolder);
|
|
473
|
+
// mkdirSync(destLauncherFolder, {recursive: true});
|
|
474
|
+
// }
|
|
475
|
+
// cpSync(zigLauncherBinarySource, zigLauncherDestination, {recursive: true, dereference: true});
|
|
476
|
+
const bunCliLauncherBinarySource =
|
|
477
|
+
buildEnvironment === "dev"
|
|
478
|
+
? // Note: in dev use the cli as the launcher
|
|
479
|
+
PATHS.LAUNCHER_DEV
|
|
480
|
+
: // Note: for release use the zig launcher optimized for smol size
|
|
481
|
+
PATHS.LAUNCHER_RELEASE;
|
|
482
|
+
const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + binExt;
|
|
483
|
+
const destLauncherFolder = dirname(bunCliLauncherDestination);
|
|
484
|
+
if (!existsSync(destLauncherFolder)) {
|
|
485
|
+
// console.info('creating folder: ', destFolder);
|
|
486
|
+
mkdirSync(destLauncherFolder, { recursive: true });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
cpSync(bunCliLauncherBinarySource, bunCliLauncherDestination, {
|
|
490
|
+
recursive: true,
|
|
491
|
+
dereference: true,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
cpSync(PATHS.MAIN_JS, join(appBundleMacOSPath, 'main.js'));
|
|
495
|
+
|
|
496
|
+
// Bun runtime binary
|
|
497
|
+
// todo (yoav): this only works for the current architecture
|
|
498
|
+
const bunBinarySourcePath = PATHS.BUN_BINARY;
|
|
499
|
+
// Note: .bin/bun binary in node_modules is a symlink to the versioned one in another place
|
|
500
|
+
// in node_modules, so we have to dereference here to get the actual binary in the bundle.
|
|
501
|
+
const bunBinaryDestInBundlePath = join(appBundleMacOSPath, "bun") + binExt;
|
|
502
|
+
const destFolder2 = dirname(bunBinaryDestInBundlePath);
|
|
503
|
+
if (!existsSync(destFolder2)) {
|
|
504
|
+
// console.info('creating folder: ', destFolder);
|
|
505
|
+
mkdirSync(destFolder2, { recursive: true });
|
|
506
|
+
}
|
|
507
|
+
cpSync(bunBinarySourcePath, bunBinaryDestInBundlePath, { dereference: true });
|
|
508
|
+
|
|
509
|
+
// copy native wrapper dynamic library
|
|
510
|
+
if (OS === 'macos') {
|
|
511
|
+
const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_MACOS;
|
|
512
|
+
const nativeWrapperMacosDestination = join(
|
|
513
|
+
appBundleMacOSPath,
|
|
514
|
+
"libNativeWrapper.dylib"
|
|
515
|
+
);
|
|
516
|
+
cpSync(nativeWrapperMacosSource, nativeWrapperMacosDestination, {
|
|
517
|
+
dereference: true,
|
|
518
|
+
});
|
|
519
|
+
} else if (OS === 'win') {
|
|
520
|
+
const nativeWrapperMacosSource = PATHS.NATIVE_WRAPPER_WIN;
|
|
521
|
+
const nativeWrapperMacosDestination = join(
|
|
522
|
+
appBundleMacOSPath,
|
|
523
|
+
"libNativeWrapper.dll"
|
|
524
|
+
);
|
|
525
|
+
cpSync(nativeWrapperMacosSource, nativeWrapperMacosDestination, {
|
|
526
|
+
dereference: true,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const webview2LibSource = PATHS.WEBVIEW2LOADER_WIN;
|
|
530
|
+
const webview2LibDestination = join(
|
|
531
|
+
appBundleMacOSPath,
|
|
532
|
+
"WebView2Loader.dll"
|
|
533
|
+
); ;
|
|
534
|
+
// copy webview2 system webview library
|
|
535
|
+
cpSync(webview2LibSource, webview2LibDestination);
|
|
536
|
+
|
|
537
|
+
} else if (OS === 'linux') {
|
|
538
|
+
const nativeWrapperLinuxSource = PATHS.NATIVE_WRAPPER_LINUX;
|
|
539
|
+
const nativeWrapperLinuxDestination = join(
|
|
540
|
+
appBundleMacOSPath,
|
|
541
|
+
"libNativeWrapper.so"
|
|
542
|
+
);
|
|
543
|
+
if (existsSync(nativeWrapperLinuxSource)) {
|
|
544
|
+
cpSync(nativeWrapperLinuxSource, nativeWrapperLinuxDestination, {
|
|
545
|
+
dereference: true,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Ensure core binaries are available
|
|
551
|
+
await ensureCoreDependencies();
|
|
552
|
+
|
|
553
|
+
// Download CEF binaries if needed when bundleCEF is enabled
|
|
554
|
+
if ((OS === 'macos' && config.build.mac?.bundleCEF) ||
|
|
555
|
+
(OS === 'win' && config.build.win?.bundleCEF) ||
|
|
556
|
+
(OS === 'linux' && config.build.linux?.bundleCEF)) {
|
|
557
|
+
|
|
558
|
+
await ensureCEFDependencies();
|
|
559
|
+
if (OS === 'macos') {
|
|
560
|
+
const cefFrameworkSource = PATHS.CEF_FRAMEWORK_MACOS;
|
|
561
|
+
const cefFrameworkDestination = join(
|
|
562
|
+
appBundleFolderFrameworksPath,
|
|
563
|
+
"Chromium Embedded Framework.framework"
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
cpSync(cefFrameworkSource, cefFrameworkDestination, {
|
|
567
|
+
recursive: true,
|
|
568
|
+
dereference: true,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
// cef helpers
|
|
573
|
+
const cefHelperNames = [
|
|
574
|
+
"bun Helper",
|
|
575
|
+
"bun Helper (Alerts)",
|
|
576
|
+
"bun Helper (GPU)",
|
|
577
|
+
"bun Helper (Plugin)",
|
|
578
|
+
"bun Helper (Renderer)",
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
const helperSourcePath = PATHS.CEF_HELPER_MACOS;
|
|
582
|
+
cefHelperNames.forEach((helperName) => {
|
|
583
|
+
const destinationPath = join(
|
|
584
|
+
appBundleFolderFrameworksPath,
|
|
585
|
+
`${helperName}.app`,
|
|
586
|
+
`Contents`,
|
|
587
|
+
`MacOS`,
|
|
588
|
+
`${helperName}`
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
const destFolder4 = dirname(destinationPath);
|
|
592
|
+
if (!existsSync(destFolder4)) {
|
|
593
|
+
// console.info('creating folder: ', destFolder4);
|
|
594
|
+
mkdirSync(destFolder4, { recursive: true });
|
|
595
|
+
}
|
|
596
|
+
cpSync(helperSourcePath, destinationPath, {
|
|
597
|
+
recursive: true,
|
|
598
|
+
dereference: true,
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
} else if (OS === 'win') {
|
|
602
|
+
// Copy CEF DLLs from dist/cef/ to the main executable directory
|
|
603
|
+
const electrobunDistPath = join(ELECTROBUN_DEP_PATH, "dist");
|
|
604
|
+
const cefSourcePath = join(electrobunDistPath, "cef");
|
|
605
|
+
const cefDllFiles = [
|
|
606
|
+
'libcef.dll',
|
|
607
|
+
'chrome_elf.dll',
|
|
608
|
+
'd3dcompiler_47.dll',
|
|
609
|
+
'libEGL.dll',
|
|
610
|
+
'libGLESv2.dll',
|
|
611
|
+
'vk_swiftshader.dll',
|
|
612
|
+
'vulkan-1.dll'
|
|
613
|
+
];
|
|
614
|
+
|
|
615
|
+
cefDllFiles.forEach(dllFile => {
|
|
616
|
+
const sourcePath = join(cefSourcePath, dllFile);
|
|
617
|
+
const destPath = join(appBundleMacOSPath, dllFile);
|
|
618
|
+
if (existsSync(sourcePath)) {
|
|
619
|
+
cpSync(sourcePath, destPath);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Copy icudtl.dat to MacOS root (same folder as libcef.dll) - required for CEF initialization
|
|
624
|
+
const icuDataSource = join(cefSourcePath, 'icudtl.dat');
|
|
625
|
+
const icuDataDest = join(appBundleMacOSPath, 'icudtl.dat');
|
|
626
|
+
if (existsSync(icuDataSource)) {
|
|
627
|
+
cpSync(icuDataSource, icuDataDest);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Copy essential CEF pak files to MacOS root (same folder as libcef.dll) - required for CEF resources
|
|
631
|
+
const essentialPakFiles = ['chrome_100_percent.pak', 'resources.pak', 'v8_context_snapshot.bin'];
|
|
632
|
+
essentialPakFiles.forEach(pakFile => {
|
|
633
|
+
const sourcePath = join(cefSourcePath, pakFile);
|
|
634
|
+
const destPath = join(appBundleMacOSPath, pakFile);
|
|
635
|
+
|
|
636
|
+
if (existsSync(sourcePath)) {
|
|
637
|
+
cpSync(sourcePath, destPath);
|
|
638
|
+
} else {
|
|
639
|
+
console.log(`WARNING: Missing CEF file: ${sourcePath}`);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Copy CEF resources to MacOS/cef/ subdirectory for other resources like locales
|
|
644
|
+
const cefResourcesSource = join(electrobunDistPath, 'cef');
|
|
645
|
+
const cefResourcesDestination = join(appBundleMacOSPath, 'cef');
|
|
646
|
+
|
|
647
|
+
if (existsSync(cefResourcesSource)) {
|
|
648
|
+
cpSync(cefResourcesSource, cefResourcesDestination, {
|
|
649
|
+
recursive: true,
|
|
650
|
+
dereference: true,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Copy CEF helper processes with different names
|
|
655
|
+
const cefHelperNames = [
|
|
656
|
+
"bun Helper",
|
|
657
|
+
"bun Helper (Alerts)",
|
|
658
|
+
"bun Helper (GPU)",
|
|
659
|
+
"bun Helper (Plugin)",
|
|
660
|
+
"bun Helper (Renderer)",
|
|
661
|
+
];
|
|
662
|
+
|
|
663
|
+
const helperSourcePath = PATHS.CEF_HELPER_WIN;
|
|
664
|
+
if (existsSync(helperSourcePath)) {
|
|
665
|
+
cefHelperNames.forEach((helperName) => {
|
|
666
|
+
const destinationPath = join(appBundleMacOSPath, `${helperName}.exe`);
|
|
667
|
+
cpSync(helperSourcePath, destinationPath);
|
|
668
|
+
|
|
669
|
+
});
|
|
670
|
+
} else {
|
|
671
|
+
console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
|
|
672
|
+
}
|
|
673
|
+
} else if (OS === 'linux') {
|
|
674
|
+
// Copy CEF shared libraries from dist/cef/ to the main executable directory
|
|
675
|
+
const electrobunDistPath = join(ELECTROBUN_DEP_PATH, "dist");
|
|
676
|
+
const cefSourcePath = join(electrobunDistPath, "cef");
|
|
677
|
+
|
|
678
|
+
if (existsSync(cefSourcePath)) {
|
|
679
|
+
const cefSoFiles = [
|
|
680
|
+
'libcef.so',
|
|
681
|
+
'libEGL.so',
|
|
682
|
+
'libGLESv2.so',
|
|
683
|
+
'libvk_swiftshader.so',
|
|
684
|
+
'libvulkan.so.1'
|
|
685
|
+
];
|
|
686
|
+
|
|
687
|
+
cefSoFiles.forEach(soFile => {
|
|
688
|
+
const sourcePath = join(cefSourcePath, soFile);
|
|
689
|
+
const destPath = join(appBundleMacOSPath, soFile);
|
|
690
|
+
if (existsSync(sourcePath)) {
|
|
691
|
+
cpSync(sourcePath, destPath);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// Copy icudtl.dat to MacOS root (same folder as libcef.so) - required for CEF initialization
|
|
696
|
+
const icuDataSource = join(cefSourcePath, 'icudtl.dat');
|
|
697
|
+
const icuDataDest = join(appBundleMacOSPath, 'icudtl.dat');
|
|
698
|
+
if (existsSync(icuDataSource)) {
|
|
699
|
+
cpSync(icuDataSource, icuDataDest);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Copy .pak files and other CEF resources to the main executable directory
|
|
703
|
+
const pakFiles = [
|
|
704
|
+
'icudtl.dat',
|
|
705
|
+
'v8_context_snapshot.bin',
|
|
706
|
+
'snapshot_blob.bin',
|
|
707
|
+
'resources.pak',
|
|
708
|
+
'chrome_100_percent.pak',
|
|
709
|
+
'chrome_200_percent.pak',
|
|
710
|
+
'locales',
|
|
711
|
+
'chrome-sandbox',
|
|
712
|
+
'vk_swiftshader_icd.json'
|
|
713
|
+
];
|
|
714
|
+
pakFiles.forEach(pakFile => {
|
|
715
|
+
const sourcePath = join(cefSourcePath, pakFile);
|
|
716
|
+
const destPath = join(appBundleMacOSPath, pakFile);
|
|
717
|
+
if (existsSync(sourcePath)) {
|
|
718
|
+
cpSync(sourcePath, destPath, { recursive: true });
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// Copy locales to cef subdirectory
|
|
723
|
+
const cefResourcesDestination = join(appBundleMacOSPath, 'cef');
|
|
724
|
+
if (!existsSync(cefResourcesDestination)) {
|
|
725
|
+
mkdirSync(cefResourcesDestination, { recursive: true });
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Copy all CEF shared libraries to cef subdirectory as well (for RPATH $ORIGIN/cef)
|
|
729
|
+
cefSoFiles.forEach(soFile => {
|
|
730
|
+
const sourcePath = join(cefSourcePath, soFile);
|
|
731
|
+
const destPath = join(cefResourcesDestination, soFile);
|
|
732
|
+
if (existsSync(sourcePath)) {
|
|
733
|
+
cpSync(sourcePath, destPath);
|
|
734
|
+
console.log(`Copied CEF library to cef subdirectory: ${soFile}`);
|
|
735
|
+
} else {
|
|
736
|
+
console.log(`WARNING: Missing CEF library: ${sourcePath}`);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// Copy essential CEF files to cef subdirectory as well (for RPATH $ORIGIN/cef)
|
|
741
|
+
const cefEssentialFiles = ['vk_swiftshader_icd.json'];
|
|
742
|
+
cefEssentialFiles.forEach(cefFile => {
|
|
743
|
+
const sourcePath = join(cefSourcePath, cefFile);
|
|
744
|
+
const destPath = join(cefResourcesDestination, cefFile);
|
|
745
|
+
if (existsSync(sourcePath)) {
|
|
746
|
+
cpSync(sourcePath, destPath);
|
|
747
|
+
console.log(`Copied CEF essential file to cef subdirectory: ${cefFile}`);
|
|
748
|
+
} else {
|
|
749
|
+
console.log(`WARNING: Missing CEF essential file: ${sourcePath}`);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Copy CEF helper processes with different names
|
|
754
|
+
const cefHelperNames = [
|
|
755
|
+
"bun Helper",
|
|
756
|
+
"bun Helper (Alerts)",
|
|
757
|
+
"bun Helper (GPU)",
|
|
758
|
+
"bun Helper (Plugin)",
|
|
759
|
+
"bun Helper (Renderer)",
|
|
760
|
+
];
|
|
761
|
+
|
|
762
|
+
const helperSourcePath = PATHS.CEF_HELPER_LINUX;
|
|
763
|
+
if (existsSync(helperSourcePath)) {
|
|
764
|
+
cefHelperNames.forEach((helperName) => {
|
|
765
|
+
const destinationPath = join(appBundleMacOSPath, helperName);
|
|
766
|
+
cpSync(helperSourcePath, destinationPath);
|
|
767
|
+
console.log(`Copied CEF helper: ${helperName}`);
|
|
768
|
+
});
|
|
769
|
+
} else {
|
|
770
|
+
console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
// copy native bindings
|
|
778
|
+
const bsPatchSource = PATHS.BSPATCH;
|
|
779
|
+
const bsPatchDestination = join(appBundleMacOSPath, "bspatch") + binExt;
|
|
780
|
+
const bsPatchDestFolder = dirname(bsPatchDestination);
|
|
781
|
+
if (!existsSync(bsPatchDestFolder)) {
|
|
782
|
+
mkdirSync(bsPatchDestFolder, { recursive: true });
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
cpSync(bsPatchSource, bsPatchDestination, {
|
|
786
|
+
recursive: true,
|
|
787
|
+
dereference: true,
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// transpile developer's bun code
|
|
791
|
+
const bunDestFolder = join(appBundleAppCodePath, "bun");
|
|
792
|
+
// Build bun-javascript ts files
|
|
793
|
+
const buildResult = await Bun.build({
|
|
794
|
+
entrypoints: [bunSource],
|
|
795
|
+
outdir: bunDestFolder,
|
|
796
|
+
external: bunConfig.external || [],
|
|
797
|
+
// minify: true, // todo (yoav): add minify in canary and prod builds
|
|
798
|
+
target: "bun",
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
if (!buildResult.success) {
|
|
802
|
+
console.error("failed to build", bunSource, buildResult.logs);
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// transpile developer's view code
|
|
807
|
+
// Build webview-javascript ts files
|
|
808
|
+
// bundle all the bundles
|
|
809
|
+
for (const viewName in config.build.views) {
|
|
810
|
+
const viewConfig = config.build.views[viewName];
|
|
811
|
+
|
|
812
|
+
const viewSource = join(projectRoot, viewConfig.entrypoint);
|
|
813
|
+
if (!existsSync(viewSource)) {
|
|
814
|
+
console.error(`failed to bundle ${viewSource} because it doesn't exist.`);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const viewDestFolder = join(appBundleAppCodePath, "views", viewName);
|
|
819
|
+
|
|
820
|
+
if (!existsSync(viewDestFolder)) {
|
|
821
|
+
// console.info('creating folder: ', viewDestFolder);
|
|
822
|
+
mkdirSync(viewDestFolder, { recursive: true });
|
|
823
|
+
} else {
|
|
824
|
+
console.error(
|
|
825
|
+
"continuing, but ",
|
|
826
|
+
viewDestFolder,
|
|
827
|
+
"unexpectedly already exists in the build folder"
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// console.info(`bundling ${viewSource} to ${viewDestFolder} with config: `, viewConfig);
|
|
832
|
+
|
|
833
|
+
const buildResult = await Bun.build({
|
|
834
|
+
entrypoints: [viewSource],
|
|
835
|
+
outdir: viewDestFolder,
|
|
836
|
+
external: viewConfig.external || [],
|
|
837
|
+
target: "browser",
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
if (!buildResult.success) {
|
|
841
|
+
console.error("failed to build", viewSource, buildResult.logs);
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// Copy assets like html, css, images, and other files
|
|
846
|
+
for (const relSource in config.build.copy) {
|
|
847
|
+
const source = join(projectRoot, relSource);
|
|
848
|
+
if (!existsSync(source)) {
|
|
849
|
+
console.error(`failed to copy ${source} because it doesn't exist.`);
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const destination = join(
|
|
854
|
+
appBundleAppCodePath,
|
|
855
|
+
config.build.copy[relSource]
|
|
856
|
+
);
|
|
857
|
+
const destFolder = dirname(destination);
|
|
858
|
+
|
|
859
|
+
if (!existsSync(destFolder)) {
|
|
860
|
+
// console.info('creating folder: ', destFolder);
|
|
861
|
+
mkdirSync(destFolder, { recursive: true });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// todo (yoav): add ability to swap out BUILD VARS
|
|
865
|
+
cpSync(source, destination, { recursive: true, dereference: true });
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
buildIcons(appBundleFolderResourcesPath);
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
// Run postBuild script
|
|
873
|
+
if (config.scripts.postBuild) {
|
|
874
|
+
|
|
875
|
+
Bun.spawnSync([bunBinarySourcePath, config.scripts.postBuild], {
|
|
876
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
877
|
+
env: {
|
|
878
|
+
...process.env,
|
|
879
|
+
ELECTROBUN_BUILD_ENV: buildEnvironment,
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
// All the unique files are in the bundle now. Create an initial temporary tar file
|
|
884
|
+
// for hashing the contents
|
|
885
|
+
// tar the signed and notarized app bundle
|
|
886
|
+
const tmpTarPath = `${appBundleFolderPath}-temp.tar`;
|
|
887
|
+
await tar.c(
|
|
888
|
+
{
|
|
889
|
+
gzip: false,
|
|
890
|
+
file: tmpTarPath,
|
|
891
|
+
cwd: buildFolder,
|
|
892
|
+
},
|
|
893
|
+
[basename(appBundleFolderPath)]
|
|
894
|
+
);
|
|
895
|
+
const tmpTarball = Bun.file(tmpTarPath);
|
|
896
|
+
const tmpTarBuffer = await tmpTarball.arrayBuffer();
|
|
897
|
+
// Note: wyhash is the default in Bun.hash but that may change in the future
|
|
898
|
+
// so we're being explicit here.
|
|
899
|
+
const hash = Bun.hash.wyhash(tmpTarBuffer, 43770n).toString(36);
|
|
900
|
+
|
|
901
|
+
unlinkSync(tmpTarPath);
|
|
902
|
+
// const bunVersion = execSync(`${bunBinarySourcePath} --version`).toString().trim();
|
|
903
|
+
|
|
904
|
+
// version.json inside the app bundle
|
|
905
|
+
const versionJsonContent = JSON.stringify({
|
|
906
|
+
version: config.app.version,
|
|
907
|
+
// The first tar file does not include this, it gets hashed,
|
|
908
|
+
// then the hash is included in another tar file. That later one
|
|
909
|
+
// then gets used for patching and updating.
|
|
910
|
+
hash: hash,
|
|
911
|
+
channel: buildEnvironment,
|
|
912
|
+
bucketUrl: config.release.bucketUrl,
|
|
913
|
+
name: appFileName,
|
|
914
|
+
identifier: config.app.identifier,
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
await Bun.write(
|
|
918
|
+
join(appBundleFolderResourcesPath, "version.json"),
|
|
919
|
+
versionJsonContent
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
// todo (yoav): add these to config
|
|
923
|
+
const shouldCodesign =
|
|
924
|
+
buildEnvironment !== "dev" && config.build.mac.codesign;
|
|
925
|
+
const shouldNotarize = shouldCodesign && config.build.mac.notarize;
|
|
926
|
+
|
|
927
|
+
if (shouldCodesign) {
|
|
928
|
+
codesignAppBundle(
|
|
929
|
+
appBundleFolderPath,
|
|
930
|
+
join(buildFolder, "entitlements.plist")
|
|
931
|
+
);
|
|
932
|
+
} else {
|
|
933
|
+
console.log("skipping codesign");
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// codesign
|
|
937
|
+
// NOTE: Codesigning fails in dev mode (when using a single-file-executable bun cli as the launcher)
|
|
938
|
+
// see https://github.com/oven-sh/bun/issues/7208
|
|
939
|
+
if (shouldNotarize) {
|
|
940
|
+
notarizeAndStaple(appBundleFolderPath);
|
|
941
|
+
} else {
|
|
942
|
+
console.log("skipping notarization");
|
|
943
|
+
}
|
|
944
|
+
if (buildEnvironment !== "dev") {
|
|
945
|
+
const artifactsToUpload = [];
|
|
946
|
+
// zstd wasm https://github.com/OneIdentity/zstd-js
|
|
947
|
+
// tar https://github.com/isaacs/node-tar
|
|
948
|
+
|
|
949
|
+
// steps:
|
|
950
|
+
// 1. [done] build the app bundle, code sign, notarize, staple.
|
|
951
|
+
// 2. tar and zstd the app bundle (two separate files)
|
|
952
|
+
// 3. build another app bundle for the self-extracting app bundle with the zstd in Resources
|
|
953
|
+
// 4. code sign and notarize the self-extracting app bundle
|
|
954
|
+
// 5. while waiting for that notarization, download the prev app bundle, extract the tar, and generate a bsdiff patch
|
|
955
|
+
// 6. when notarization is complete, generate a dmg of the self-extracting app bundle
|
|
956
|
+
// 6.5. code sign and notarize the dmg
|
|
957
|
+
// 7. copy artifacts to directory [self-extractor dmg, zstd app bundle, bsdiff patch, update.json]
|
|
958
|
+
|
|
959
|
+
const tarPath = `${appBundleFolderPath}.tar`;
|
|
960
|
+
|
|
961
|
+
// tar the signed and notarized app bundle
|
|
962
|
+
await tar.c(
|
|
963
|
+
{
|
|
964
|
+
gzip: false,
|
|
965
|
+
file: tarPath,
|
|
966
|
+
cwd: buildFolder,
|
|
967
|
+
},
|
|
968
|
+
[basename(appBundleFolderPath)]
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
const tarball = Bun.file(tarPath);
|
|
972
|
+
const tarBuffer = await tarball.arrayBuffer();
|
|
973
|
+
|
|
974
|
+
// Note: The playground app bundle is around 48MB.
|
|
975
|
+
// compression on m1 max with 64GB ram:
|
|
976
|
+
// brotli: 1min 38s, 48MB -> 11.1MB
|
|
977
|
+
// zstd: 15s, 48MB -> 12.1MB
|
|
978
|
+
// zstd is the clear winner here. dev iteration speed gain of 1min 15s per build is much more valubale
|
|
979
|
+
// than saving 1 more MB of space/bandwidth.
|
|
980
|
+
|
|
981
|
+
const compressedTarPath = `${tarPath}.zst`;
|
|
982
|
+
artifactsToUpload.push(compressedTarPath);
|
|
983
|
+
|
|
984
|
+
// zstd compress tarball
|
|
985
|
+
// todo (yoav): consider using c bindings for zstd for speed instead of wasm
|
|
986
|
+
// we already have it in the bsdiff binary
|
|
987
|
+
console.log("compressing tarball...");
|
|
988
|
+
await ZstdInit().then(async ({ ZstdSimple, ZstdStream }) => {
|
|
989
|
+
// Note: Simple is much faster than stream, but stream is better for large files
|
|
990
|
+
// todo (yoav): consider a file size cutoff to switch to stream instead of simple.
|
|
991
|
+
if (tarball.size > 0) {
|
|
992
|
+
// Uint8 array filestream of the tar file
|
|
993
|
+
|
|
994
|
+
const data = new Uint8Array(tarBuffer);
|
|
995
|
+
const compressionLevel = 22;
|
|
996
|
+
const compressedData = ZstdSimple.compress(data, compressionLevel);
|
|
997
|
+
|
|
998
|
+
console.log(
|
|
999
|
+
"compressed",
|
|
1000
|
+
compressedData.length,
|
|
1001
|
+
"bytes",
|
|
1002
|
+
"from",
|
|
1003
|
+
data.length,
|
|
1004
|
+
"bytes"
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
await Bun.write(compressedTarPath, compressedData);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// we can delete the original app bundle since we've tarred and zstd it. We need to create the self-extracting app bundle
|
|
1012
|
+
// now and it needs the same name as the original app bundle.
|
|
1013
|
+
rmdirSync(appBundleFolderPath, { recursive: true });
|
|
1014
|
+
|
|
1015
|
+
const selfExtractingBundle = createAppBundle(appFileName, buildFolder);
|
|
1016
|
+
const compressedTarballInExtractingBundlePath = join(
|
|
1017
|
+
selfExtractingBundle.appBundleFolderResourcesPath,
|
|
1018
|
+
`${hash}.tar.zst`
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
// copy the zstd tarball to the self-extracting app bundle
|
|
1022
|
+
cpSync(compressedTarPath, compressedTarballInExtractingBundlePath);
|
|
1023
|
+
|
|
1024
|
+
const selfExtractorBinSourcePath = PATHS.EXTRACTOR;
|
|
1025
|
+
const selfExtractorBinDestinationPath = join(
|
|
1026
|
+
selfExtractingBundle.appBundleMacOSPath,
|
|
1027
|
+
"launcher"
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
cpSync(selfExtractorBinSourcePath, selfExtractorBinDestinationPath, {
|
|
1031
|
+
dereference: true,
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
buildIcons(appBundleFolderResourcesPath);
|
|
1035
|
+
await Bun.write(
|
|
1036
|
+
join(selfExtractingBundle.appBundleFolderContentsPath, "Info.plist"),
|
|
1037
|
+
InfoPlistContents
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
if (shouldCodesign) {
|
|
1041
|
+
codesignAppBundle(
|
|
1042
|
+
selfExtractingBundle.appBundleFolderPath,
|
|
1043
|
+
join(buildFolder, "entitlements.plist")
|
|
1044
|
+
);
|
|
1045
|
+
} else {
|
|
1046
|
+
console.log("skipping codesign");
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Note: we need to notarize the original app bundle, the self-extracting app bundle, and the dmg
|
|
1050
|
+
if (shouldNotarize) {
|
|
1051
|
+
notarizeAndStaple(selfExtractingBundle.appBundleFolderPath);
|
|
1052
|
+
} else {
|
|
1053
|
+
console.log("skipping notarization");
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
console.log("creating dmg...");
|
|
1057
|
+
// make a dmg
|
|
1058
|
+
const dmgPath = join(buildFolder, `${appFileName}.dmg`);
|
|
1059
|
+
artifactsToUpload.push(dmgPath);
|
|
1060
|
+
// hdiutil create -volname "YourAppName" -srcfolder /path/to/YourApp.app -ov -format UDZO YourAppName.dmg
|
|
1061
|
+
// Note: use UDBZ for better compression vs. UDZO
|
|
1062
|
+
execSync(
|
|
1063
|
+
`hdiutil create -volname "${appFileName}" -srcfolder ${escapePathForTerminal(
|
|
1064
|
+
appBundleFolderPath
|
|
1065
|
+
)} -ov -format UDBZ ${escapePathForTerminal(dmgPath)}`
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1068
|
+
if (shouldCodesign) {
|
|
1069
|
+
codesignAppBundle(dmgPath);
|
|
1070
|
+
} else {
|
|
1071
|
+
console.log("skipping codesign");
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (shouldNotarize) {
|
|
1075
|
+
notarizeAndStaple(dmgPath);
|
|
1076
|
+
} else {
|
|
1077
|
+
console.log("skipping notarization");
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// refresh artifacts folder
|
|
1081
|
+
console.log("creating artifacts folder...");
|
|
1082
|
+
if (existsSync(artifactFolder)) {
|
|
1083
|
+
console.info("deleting artifact folder: ", artifactFolder);
|
|
1084
|
+
rmdirSync(artifactFolder, { recursive: true });
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
mkdirSync(artifactFolder, { recursive: true });
|
|
1088
|
+
|
|
1089
|
+
console.log("creating update.json...");
|
|
1090
|
+
// update.json for the channel in that channel's build folder
|
|
1091
|
+
const updateJsonContent = JSON.stringify({
|
|
1092
|
+
// The version isn't really used for updating, but it's nice to have for
|
|
1093
|
+
// the download button or display on your marketing site or in the app.
|
|
1094
|
+
version: config.app.version,
|
|
1095
|
+
hash: hash.toString(),
|
|
1096
|
+
// channel: buildEnvironment,
|
|
1097
|
+
// bucketUrl: config.release.bucketUrl
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
await Bun.write(join(artifactFolder, "update.json"), updateJsonContent);
|
|
1101
|
+
|
|
1102
|
+
// generate bsdiff
|
|
1103
|
+
// https://storage.googleapis.com/eggbun-static/electrobun-playground/canary/ElectrobunPlayground-canary.app.tar.zst
|
|
1104
|
+
console.log("bucketUrl: ", config.release.bucketUrl);
|
|
1105
|
+
|
|
1106
|
+
console.log("generating a patch from the previous version...");
|
|
1107
|
+
const urlToPrevUpdateJson = join(
|
|
1108
|
+
config.release.bucketUrl,
|
|
1109
|
+
buildEnvironment,
|
|
1110
|
+
`update.json`
|
|
1111
|
+
);
|
|
1112
|
+
const cacheBuster = Math.random().toString(36).substring(7);
|
|
1113
|
+
const updateJsonResponse = await fetch(
|
|
1114
|
+
urlToPrevUpdateJson + `?${cacheBuster}`
|
|
1115
|
+
).catch((err) => {
|
|
1116
|
+
console.log("bucketURL not found: ", err);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
const urlToLatestTarball = join(
|
|
1120
|
+
config.release.bucketUrl,
|
|
1121
|
+
buildEnvironment,
|
|
1122
|
+
`${appFileName}.app.tar.zst`
|
|
1123
|
+
);
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
// attempt to get the previous version to create a patch file
|
|
1127
|
+
if (updateJsonResponse.ok) {
|
|
1128
|
+
const prevUpdateJson = await updateJsonResponse.json();
|
|
1129
|
+
|
|
1130
|
+
const prevHash = prevUpdateJson.hash;
|
|
1131
|
+
console.log("PREVIOUS HASH", prevHash);
|
|
1132
|
+
|
|
1133
|
+
// todo (yoav): should be able to stream and decompress in the same step
|
|
1134
|
+
|
|
1135
|
+
const response = await fetch(urlToLatestTarball + `?${cacheBuster}`);
|
|
1136
|
+
const prevVersionCompressedTarballPath = join(
|
|
1137
|
+
buildFolder,
|
|
1138
|
+
"prev.tar.zst"
|
|
1139
|
+
);
|
|
1140
|
+
|
|
1141
|
+
if (response.ok && response.body) {
|
|
1142
|
+
const reader = response.body.getReader();
|
|
1143
|
+
|
|
1144
|
+
const writer = Bun.file(prevVersionCompressedTarballPath).writer();
|
|
1145
|
+
|
|
1146
|
+
while (true) {
|
|
1147
|
+
const { done, value } = await reader.read();
|
|
1148
|
+
if (done) break;
|
|
1149
|
+
await writer.write(value);
|
|
1150
|
+
}
|
|
1151
|
+
await writer.flush();
|
|
1152
|
+
writer.end();
|
|
1153
|
+
|
|
1154
|
+
console.log("decompress prev funn bundle...");
|
|
1155
|
+
const prevTarballPath = join(buildFolder, "prev.tar");
|
|
1156
|
+
await ZstdInit().then(async ({ ZstdSimple }) => {
|
|
1157
|
+
const data = new Uint8Array(
|
|
1158
|
+
await Bun.file(prevVersionCompressedTarballPath).arrayBuffer()
|
|
1159
|
+
);
|
|
1160
|
+
const uncompressedData = ZstdSimple.decompress(data);
|
|
1161
|
+
await Bun.write(prevTarballPath, uncompressedData);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
console.log("diff previous and new tarballs...");
|
|
1165
|
+
// Run it as a separate process to leverage multi-threadedness
|
|
1166
|
+
// especially for creating multiple diffs in parallel
|
|
1167
|
+
const bsdiffpath = PATHS.BSDIFF;
|
|
1168
|
+
const patchFilePath = join(buildFolder, `${prevHash}.patch`);
|
|
1169
|
+
artifactsToUpload.push(patchFilePath);
|
|
1170
|
+
const result = Bun.spawnSync(
|
|
1171
|
+
[bsdiffpath, prevTarballPath, tarPath, patchFilePath, "--use-zstd"],
|
|
1172
|
+
{ cwd: buildFolder }
|
|
1173
|
+
);
|
|
1174
|
+
console.log(
|
|
1175
|
+
"bsdiff result: ",
|
|
1176
|
+
result.stdout.toString(),
|
|
1177
|
+
result.stderr.toString()
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
} else {
|
|
1181
|
+
console.log("prevoius version not found at: ", urlToLatestTarball);
|
|
1182
|
+
console.log("skipping diff generation");
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// compress all the upload files
|
|
1186
|
+
console.log("copying artifacts...");
|
|
1187
|
+
|
|
1188
|
+
artifactsToUpload.forEach((filePath) => {
|
|
1189
|
+
const filename = basename(filePath);
|
|
1190
|
+
cpSync(filePath, join(artifactFolder, filename));
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
// todo: now just upload the artifacts to your bucket replacing the ones that exist
|
|
1194
|
+
// you'll end up with a sequence of patch files that will
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// NOTE: verify codesign
|
|
1198
|
+
// codesign --verify --deep --strict --verbose=2 <app path>
|
|
1199
|
+
|
|
1200
|
+
// Note: verify notarization
|
|
1201
|
+
// spctl --assess --type execute --verbose <app path>
|
|
1202
|
+
|
|
1203
|
+
// Note: for .dmg spctl --assess will respond with "rejected (*the code is valid* but does not seem to be an app)" which is valid
|
|
1204
|
+
// an actual failed response for a dmg is "source=no usable signature"
|
|
1205
|
+
// for a dmg.
|
|
1206
|
+
// can also use stapler validate -v to validate the dmg and look for teamId, signingId, and the response signedTicket
|
|
1207
|
+
// stapler validate -v <app path>
|
|
1208
|
+
} else if (commandArg === "dev") {
|
|
1209
|
+
// todo (yoav): rename to start
|
|
1210
|
+
|
|
1211
|
+
// run the project in dev mode
|
|
1212
|
+
// this runs the cli in debug mode, on macos executes the app bundle,
|
|
1213
|
+
// there is another copy of the cli in the app bundle that will execute the app
|
|
1214
|
+
// the two cli processes communicate via named pipes and together manage the dev
|
|
1215
|
+
// lifecycle and debug functionality
|
|
1216
|
+
|
|
1217
|
+
// Note: this cli will be a bun single-file-executable
|
|
1218
|
+
// Note: we want to use the version of bun that's packaged with electrobun
|
|
1219
|
+
// const bunPath = join(projectRoot, 'node_modules', '.bin', 'bun');
|
|
1220
|
+
// const mainPath = join(buildFolder, 'bun', 'index.js');
|
|
1221
|
+
// const mainPath = join(buildFolder, bundleFileName);
|
|
1222
|
+
// console.log('running ', bunPath, mainPath);
|
|
1223
|
+
|
|
1224
|
+
// Note: open will open the app bundle as a completely different process
|
|
1225
|
+
// This is critical to fully test the app (including plist configuration, etc.)
|
|
1226
|
+
// but also to get proper cmd+tab and dock behaviour and not run the windowed app
|
|
1227
|
+
// as a child of the terminal process which steels keyboard focus from any descendant nswindows.
|
|
1228
|
+
// Bun.spawn(["open", mainPath], {
|
|
1229
|
+
// env: {},
|
|
1230
|
+
// });
|
|
1231
|
+
|
|
1232
|
+
let mainProc;
|
|
1233
|
+
let bundleExecPath: string;
|
|
1234
|
+
|
|
1235
|
+
if (OS === 'macos') {
|
|
1236
|
+
bundleExecPath = join(buildFolder, bundleFileName, "Contents", 'MacOS');
|
|
1237
|
+
} else if (OS === 'linux' || OS === 'win') {
|
|
1238
|
+
bundleExecPath = join(buildFolder, bundleFileName, "bin");
|
|
1239
|
+
} else {
|
|
1240
|
+
throw new Error(`Unsupported OS: ${OS}`);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (OS === 'macos') {
|
|
1244
|
+
|
|
1245
|
+
mainProc = Bun.spawn([join(bundleExecPath,'bun'), join(bundleExecPath, 'main.js')], {
|
|
1246
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
1247
|
+
cwd: bundleExecPath
|
|
1248
|
+
})
|
|
1249
|
+
} else if (OS === 'win') {
|
|
1250
|
+
// Try the main process
|
|
1251
|
+
mainProc = Bun.spawn(['./bun.exe', './main.js'], {
|
|
1252
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
1253
|
+
cwd: bundleExecPath,
|
|
1254
|
+
onExit: (proc, exitCode, signalCode, error) => {
|
|
1255
|
+
console.log('Bun process exited:', { exitCode, signalCode, error });
|
|
1256
|
+
}
|
|
1257
|
+
})
|
|
1258
|
+
} else if (OS === 'linux') {
|
|
1259
|
+
let env = { ...process.env };
|
|
1260
|
+
|
|
1261
|
+
// Add LD_PRELOAD for CEF libraries to fix static TLS allocation issues
|
|
1262
|
+
if (config.build.linux?.bundleCEF) {
|
|
1263
|
+
const cefLibs = ['./libcef.so', './libvk_swiftshader.so'];
|
|
1264
|
+
const existingCefLibs = cefLibs.filter(lib => existsSync(join(bundleExecPath, lib)));
|
|
1265
|
+
|
|
1266
|
+
if (existingCefLibs.length > 0) {
|
|
1267
|
+
env['LD_PRELOAD'] = existingCefLibs.join(':');
|
|
1268
|
+
console.log(`Using LD_PRELOAD for CEF: ${env['LD_PRELOAD']}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
mainProc = Bun.spawn([join(bundleExecPath, 'bun'), join(bundleExecPath, 'main.js')], {
|
|
1273
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
1274
|
+
cwd: bundleExecPath,
|
|
1275
|
+
env
|
|
1276
|
+
})
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
process.on("SIGINT", () => {
|
|
1280
|
+
console.log('exit command')
|
|
1281
|
+
// toLauncherPipe.write("exit command\n");
|
|
1282
|
+
mainProc.kill();
|
|
1283
|
+
process.exit();
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function getConfig() {
|
|
1289
|
+
let loadedConfig = {};
|
|
1290
|
+
if (existsSync(configPath)) {
|
|
1291
|
+
const configFileContents = readFileSync(configPath, "utf8");
|
|
1292
|
+
// Note: we want this to hard fail if there's a syntax error
|
|
1293
|
+
loadedConfig = JSON.parse(configFileContents);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// todo (yoav): write a deep clone fn
|
|
1297
|
+
return {
|
|
1298
|
+
...defaultConfig,
|
|
1299
|
+
...loadedConfig,
|
|
1300
|
+
app: {
|
|
1301
|
+
...defaultConfig.app,
|
|
1302
|
+
...(loadedConfig?.app || {}),
|
|
1303
|
+
},
|
|
1304
|
+
build: {
|
|
1305
|
+
...defaultConfig.build,
|
|
1306
|
+
...(loadedConfig?.build || {}),
|
|
1307
|
+
mac: {
|
|
1308
|
+
...defaultConfig.build.mac,
|
|
1309
|
+
...(loadedConfig?.build?.mac || {}),
|
|
1310
|
+
entitlements: {
|
|
1311
|
+
...defaultConfig.build.mac.entitlements,
|
|
1312
|
+
...(loadedConfig?.build?.mac?.entitlements || {}),
|
|
1313
|
+
},
|
|
1314
|
+
},
|
|
1315
|
+
},
|
|
1316
|
+
scripts: {
|
|
1317
|
+
...defaultConfig.scripts,
|
|
1318
|
+
...(loadedConfig?.scripts || {}),
|
|
1319
|
+
},
|
|
1320
|
+
release: {
|
|
1321
|
+
...defaultConfig.release,
|
|
1322
|
+
...(loadedConfig?.release || {}),
|
|
1323
|
+
},
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function buildEntitlementsFile(entitlements) {
|
|
1328
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1329
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1330
|
+
<plist version="1.0">
|
|
1331
|
+
<dict>
|
|
1332
|
+
${Object.keys(entitlements)
|
|
1333
|
+
.map((key) => {
|
|
1334
|
+
return `<key>${key}</key>\n${getEntitlementValue(entitlements[key])}`;
|
|
1335
|
+
})
|
|
1336
|
+
.join("\n")}
|
|
1337
|
+
</dict>
|
|
1338
|
+
</plist>
|
|
1339
|
+
`;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function getEntitlementValue(value: boolean | string) {
|
|
1343
|
+
if (typeof value === "boolean") {
|
|
1344
|
+
return `<${value.toString()}/>`;
|
|
1345
|
+
} else {
|
|
1346
|
+
return value;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function codesignAppBundle(
|
|
1351
|
+
appBundleOrDmgPath: string,
|
|
1352
|
+
entitlementsFilePath?: string
|
|
1353
|
+
) {
|
|
1354
|
+
console.log("code signing...");
|
|
1355
|
+
if (OS !== 'macos' || !config.build.mac.codesign) {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const ELECTROBUN_DEVELOPER_ID = process.env["ELECTROBUN_DEVELOPER_ID"];
|
|
1360
|
+
|
|
1361
|
+
if (!ELECTROBUN_DEVELOPER_ID) {
|
|
1362
|
+
console.error("Env var ELECTROBUN_DEVELOPER_ID is required to codesign");
|
|
1363
|
+
process.exit(1);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// list of entitlements https://developer.apple.com/documentation/security/hardened_runtime?language=objc
|
|
1367
|
+
// todo (yoav): consider allowing separate entitlements config for each binary
|
|
1368
|
+
// const entitlementsFilePath = join(buildFolder, 'entitlements.plist');
|
|
1369
|
+
|
|
1370
|
+
// codesign --deep --force --verbose --timestamp --sign "ELECTROBUN_DEVELOPER_ID" --options runtime --entitlements entitlementsFilePath appBundleOrDmgPath`
|
|
1371
|
+
|
|
1372
|
+
if (entitlementsFilePath) {
|
|
1373
|
+
const entitlementsFileContents = buildEntitlementsFile(
|
|
1374
|
+
config.build.mac.entitlements
|
|
1375
|
+
);
|
|
1376
|
+
Bun.write(entitlementsFilePath, entitlementsFileContents);
|
|
1377
|
+
|
|
1378
|
+
execSync(
|
|
1379
|
+
`codesign --deep --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" --options runtime --entitlements ${entitlementsFilePath} ${escapePathForTerminal(
|
|
1380
|
+
appBundleOrDmgPath
|
|
1381
|
+
)}`
|
|
1382
|
+
);
|
|
1383
|
+
} else {
|
|
1384
|
+
execSync(
|
|
1385
|
+
`codesign --deep --force --verbose --timestamp --sign "${ELECTROBUN_DEVELOPER_ID}" ${escapePathForTerminal(
|
|
1386
|
+
appBundleOrDmgPath
|
|
1387
|
+
)}`
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function notarizeAndStaple(appOrDmgPath: string) {
|
|
1393
|
+
if (OS !== 'macos' || !config.build.mac.notarize) {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
let fileToNotarize = appOrDmgPath;
|
|
1398
|
+
// codesign
|
|
1399
|
+
// NOTE: Codesigning fails in dev mode (when using a single-file-executable bun cli as the launcher)
|
|
1400
|
+
// see https://github.com/oven-sh/bun/issues/7208
|
|
1401
|
+
// if (shouldNotarize) {
|
|
1402
|
+
console.log("notarizing...");
|
|
1403
|
+
const zipPath = appOrDmgPath + ".zip";
|
|
1404
|
+
// if (appOrDmgPath.endsWith('.app')) {
|
|
1405
|
+
const appBundleFileName = basename(appOrDmgPath);
|
|
1406
|
+
// if we're codesigning the .app we have to zip it first
|
|
1407
|
+
execSync(
|
|
1408
|
+
`zip -y -r -9 ${escapePathForTerminal(zipPath)} ${escapePathForTerminal(
|
|
1409
|
+
appBundleFileName
|
|
1410
|
+
)}`,
|
|
1411
|
+
{
|
|
1412
|
+
cwd: dirname(appOrDmgPath),
|
|
1413
|
+
}
|
|
1414
|
+
);
|
|
1415
|
+
fileToNotarize = zipPath;
|
|
1416
|
+
// }
|
|
1417
|
+
|
|
1418
|
+
const ELECTROBUN_APPLEID = process.env["ELECTROBUN_APPLEID"];
|
|
1419
|
+
|
|
1420
|
+
if (!ELECTROBUN_APPLEID) {
|
|
1421
|
+
console.error("Env var ELECTROBUN_APPLEID is required to notarize");
|
|
1422
|
+
process.exit(1);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const ELECTROBUN_APPLEIDPASS = process.env["ELECTROBUN_APPLEIDPASS"];
|
|
1426
|
+
|
|
1427
|
+
if (!ELECTROBUN_APPLEIDPASS) {
|
|
1428
|
+
console.error("Env var ELECTROBUN_APPLEIDPASS is required to notarize");
|
|
1429
|
+
process.exit(1);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const ELECTROBUN_TEAMID = process.env["ELECTROBUN_TEAMID"];
|
|
1433
|
+
|
|
1434
|
+
if (!ELECTROBUN_TEAMID) {
|
|
1435
|
+
console.error("Env var ELECTROBUN_TEAMID is required to notarize");
|
|
1436
|
+
process.exit(1);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// notarize
|
|
1440
|
+
// todo (yoav): follow up on options here like --s3-acceleration and --webhook
|
|
1441
|
+
// todo (yoav): don't use execSync since it's blocking and we'll only see the output at the end
|
|
1442
|
+
const statusInfo = execSync(
|
|
1443
|
+
`xcrun notarytool submit --apple-id "${ELECTROBUN_APPLEID}" --password "${ELECTROBUN_APPLEIDPASS}" --team-id "${ELECTROBUN_TEAMID}" --wait ${escapePathForTerminal(
|
|
1444
|
+
fileToNotarize
|
|
1445
|
+
)}`
|
|
1446
|
+
).toString();
|
|
1447
|
+
const uuid = statusInfo.match(/id: ([^\n]+)/)?.[1];
|
|
1448
|
+
console.log("statusInfo", statusInfo);
|
|
1449
|
+
console.log("uuid", uuid);
|
|
1450
|
+
|
|
1451
|
+
if (statusInfo.match("Current status: Invalid")) {
|
|
1452
|
+
console.error("notarization failed", statusInfo);
|
|
1453
|
+
const log = execSync(
|
|
1454
|
+
`xcrun notarytool log --apple-id "${ELECTROBUN_APPLEID}" --password "${ELECTROBUN_APPLEIDPASS}" --team-id "${ELECTROBUN_TEAMID}" ${uuid}`
|
|
1455
|
+
).toString();
|
|
1456
|
+
console.log("log", log);
|
|
1457
|
+
process.exit(1);
|
|
1458
|
+
}
|
|
1459
|
+
// check notarization
|
|
1460
|
+
// todo (yoav): actually check result
|
|
1461
|
+
// use `notarytool info` or some other request thing to check separately from the wait above
|
|
1462
|
+
|
|
1463
|
+
// stable notarization
|
|
1464
|
+
console.log("stapling...");
|
|
1465
|
+
execSync(`xcrun stapler staple ${escapePathForTerminal(appOrDmgPath)}`);
|
|
1466
|
+
|
|
1467
|
+
if (existsSync(zipPath)) {
|
|
1468
|
+
unlinkSync(zipPath);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// Note: supposedly the app bundle name is relevant to code sign/notarization so we need to make the app bundle and the self-extracting wrapper app bundle
|
|
1473
|
+
// 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
|
+
// either way you can pass in the parent folder here for that flexibility.
|
|
1475
|
+
// for intel/arm builds on mac we'll probably have separate subfolders as well and build them in parallel.
|
|
1476
|
+
function createAppBundle(bundleName: string, parentFolder: string) {
|
|
1477
|
+
if (OS === 'macos') {
|
|
1478
|
+
// macOS bundle structure
|
|
1479
|
+
const bundleFileName = `${bundleName}.app`;
|
|
1480
|
+
const appBundleFolderPath = join(parentFolder, bundleFileName);
|
|
1481
|
+
const appBundleFolderContentsPath = join(appBundleFolderPath, "Contents");
|
|
1482
|
+
const appBundleMacOSPath = join(appBundleFolderContentsPath, "MacOS");
|
|
1483
|
+
const appBundleFolderResourcesPath = join(
|
|
1484
|
+
appBundleFolderContentsPath,
|
|
1485
|
+
"Resources"
|
|
1486
|
+
);
|
|
1487
|
+
const appBundleFolderFrameworksPath = join(
|
|
1488
|
+
appBundleFolderContentsPath,
|
|
1489
|
+
"Frameworks"
|
|
1490
|
+
);
|
|
1491
|
+
|
|
1492
|
+
// we don't have to make all the folders, just the deepest ones
|
|
1493
|
+
mkdirSync(appBundleMacOSPath, { recursive: true });
|
|
1494
|
+
mkdirSync(appBundleFolderResourcesPath, { recursive: true });
|
|
1495
|
+
mkdirSync(appBundleFolderFrameworksPath, { recursive: true });
|
|
1496
|
+
|
|
1497
|
+
return {
|
|
1498
|
+
appBundleFolderPath,
|
|
1499
|
+
appBundleFolderContentsPath,
|
|
1500
|
+
appBundleMacOSPath,
|
|
1501
|
+
appBundleFolderResourcesPath,
|
|
1502
|
+
appBundleFolderFrameworksPath,
|
|
1503
|
+
};
|
|
1504
|
+
} else if (OS === 'linux' || OS === 'win') {
|
|
1505
|
+
// Linux/Windows simpler structure
|
|
1506
|
+
const appBundleFolderPath = join(parentFolder, bundleName);
|
|
1507
|
+
const appBundleFolderContentsPath = appBundleFolderPath; // No Contents folder needed
|
|
1508
|
+
const appBundleMacOSPath = join(appBundleFolderPath, "bin"); // Use bin instead of MacOS
|
|
1509
|
+
const appBundleFolderResourcesPath = join(appBundleFolderPath, "Resources");
|
|
1510
|
+
const appBundleFolderFrameworksPath = join(appBundleFolderPath, "lib"); // Use lib instead of Frameworks
|
|
1511
|
+
|
|
1512
|
+
// Create directories
|
|
1513
|
+
mkdirSync(appBundleMacOSPath, { recursive: true });
|
|
1514
|
+
mkdirSync(appBundleFolderResourcesPath, { recursive: true });
|
|
1515
|
+
mkdirSync(appBundleFolderFrameworksPath, { recursive: true });
|
|
1516
|
+
|
|
1517
|
+
return {
|
|
1518
|
+
appBundleFolderPath,
|
|
1519
|
+
appBundleFolderContentsPath,
|
|
1520
|
+
appBundleMacOSPath,
|
|
1521
|
+
appBundleFolderResourcesPath,
|
|
1522
|
+
appBundleFolderFrameworksPath,
|
|
1523
|
+
};
|
|
1524
|
+
} else {
|
|
1525
|
+
throw new Error(`Unsupported OS: ${OS}`);
|
|
1526
|
+
}
|
|
1527
|
+
}
|