sparkbun 0.1.0
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/bin/sparkbun.cjs +18 -0
- package/dist-linux-arm64/bsdiff +0 -0
- package/dist-linux-arm64/bspatch +0 -0
- package/dist-linux-arm64/libElectrobunCore.so +0 -0
- package/dist-linux-arm64/libNativeWrapper.so +0 -0
- package/dist-linux-arm64/libasar.so +0 -0
- package/dist-linux-x64/bsdiff +0 -0
- package/dist-linux-x64/bspatch +0 -0
- package/dist-linux-x64/libElectrobunCore.so +0 -0
- package/dist-linux-x64/libNativeWrapper.so +0 -0
- package/dist-linux-x64/libasar.so +0 -0
- package/dist-macos-arm64/bsdiff +0 -0
- package/dist-macos-arm64/bspatch +0 -0
- package/dist-macos-arm64/libElectrobunCore.dylib +0 -0
- package/dist-macos-arm64/libNativeWrapper.dylib +0 -0
- package/dist-macos-arm64/libasar.dylib +0 -0
- package/dist-macos-arm64/libwebgpu_dawn.dylib +0 -0
- package/dist-macos-arm64/preload-full.js +885 -0
- package/dist-macos-arm64/preload-sandboxed.js +111 -0
- package/dist-macos-arm64/process_helper +0 -0
- package/dist-win-x64/ElectrobunCore.dll +0 -0
- package/dist-win-x64/WebView2Loader.dll +0 -0
- package/dist-win-x64/bsdiff.exe +0 -0
- package/dist-win-x64/bspatch.exe +0 -0
- package/dist-win-x64/libNativeWrapper.dll +0 -0
- package/dist-win-x64/zig-asar/arm64/libasar.dll +0 -0
- package/dist-win-x64/zig-asar/x64/libasar.dll +0 -0
- package/package.json +47 -0
- package/scripts/build-and-upload-artifacts.js +207 -0
- package/scripts/gen-webgpu-ffi.mjs +162 -0
- package/scripts/install-windows-deps.ps1 +80 -0
- package/scripts/package-release.js +237 -0
- package/scripts/push-version.js +84 -0
- package/scripts/update-bun-version.ts +122 -0
- package/scripts/update-cef-version.ts +145 -0
- package/src/browser/builtinrpcSchema.ts +19 -0
- package/src/browser/global.d.ts +36 -0
- package/src/browser/index.ts +234 -0
- package/src/browser/webviewtag.ts +88 -0
- package/src/browser/wgputag.ts +48 -0
- package/src/bun/SparkBunConfig.ts +497 -0
- package/src/bun/__tests__/ffi-contract.test.ts +105 -0
- package/src/bun/core/ApplicationMenu.ts +70 -0
- package/src/bun/core/BrowserView.ts +416 -0
- package/src/bun/core/BrowserWindow.ts +396 -0
- package/src/bun/core/BuildConfig.ts +71 -0
- package/src/bun/core/ContextMenu.ts +75 -0
- package/src/bun/core/GpuWindow.ts +289 -0
- package/src/bun/core/Paths.ts +5 -0
- package/src/bun/core/Socket.ts +22 -0
- package/src/bun/core/Tray.ts +197 -0
- package/src/bun/core/Updater.ts +1131 -0
- package/src/bun/core/Utils.ts +487 -0
- package/src/bun/core/WGPUView.ts +167 -0
- package/src/bun/core/menuRoles.ts +181 -0
- package/src/bun/events/ApplicationEvents.ts +22 -0
- package/src/bun/events/event.ts +27 -0
- package/src/bun/events/eventEmitter.ts +45 -0
- package/src/bun/events/trayEvents.ts +11 -0
- package/src/bun/events/webviewEvents.ts +39 -0
- package/src/bun/events/windowEvents.ts +23 -0
- package/src/bun/index.ts +120 -0
- package/src/bun/preload/.generated/compiled.ts +2 -0
- package/src/bun/preload/build.ts +65 -0
- package/src/bun/preload/dragRegions.ts +41 -0
- package/src/bun/preload/encryption.ts +86 -0
- package/src/bun/preload/events.ts +171 -0
- package/src/bun/preload/globals.d.ts +45 -0
- package/src/bun/preload/index-sandboxed.ts +28 -0
- package/src/bun/preload/index.ts +77 -0
- package/src/bun/preload/internalRpc.ts +80 -0
- package/src/bun/preload/overlaySync.ts +107 -0
- package/src/bun/preload/webviewTag.ts +451 -0
- package/src/bun/preload/wgpuTag.ts +246 -0
- package/src/bun/proc/linux.md +43 -0
- package/src/bun/proc/native.ts +3253 -0
- package/src/bun/webGPU.ts +346 -0
- package/src/bun/webgpuAdapter.ts +3011 -0
- package/src/cli/bun.lockb +0 -0
- package/src/cli/index.ts +4653 -0
- package/src/cli/package-lock.json +81 -0
- package/src/cli/package.json +11 -0
- package/src/cli/templates/embedded.ts +2 -0
- package/src/core/build.zig +16 -0
- package/src/core/main.zig +3378 -0
- package/src/extractor/build.zig +22 -0
- package/src/installer/installer-template.ts +216 -0
- package/src/launcher/main.ts +221 -0
- package/src/native/build/libNativeWrapper.so +0 -0
- package/src/native/linux/build/nativeWrapper.o +0 -0
- package/src/native/linux/cef_loader.cpp +110 -0
- package/src/native/linux/cef_loader.h +28 -0
- package/src/native/linux/cef_process_helper_linux.cpp +160 -0
- package/src/native/linux/nativeWrapper.cpp +11768 -0
- package/src/native/macos/cef_process_helper_mac.cc +160 -0
- package/src/native/macos/nativeWrapper.mm +9172 -0
- package/src/native/shared/accelerator_parser.h +72 -0
- package/src/native/shared/app_paths.h +110 -0
- package/src/native/shared/asar.h +35 -0
- package/src/native/shared/cache_migration.h +244 -0
- package/src/native/shared/callbacks.h +57 -0
- package/src/native/shared/cef_response_filter.h +189 -0
- package/src/native/shared/chromium_flags.h +181 -0
- package/src/native/shared/config.h +66 -0
- package/src/native/shared/download_event.h +197 -0
- package/src/native/shared/ffi_helpers.h +139 -0
- package/src/native/shared/glob_match.h +59 -0
- package/src/native/shared/json_menu_parser.h +223 -0
- package/src/native/shared/mime_types.h +101 -0
- package/src/native/shared/navigation_rules.h +98 -0
- package/src/native/shared/partition_context.h +137 -0
- package/src/native/shared/pending_resize_queue.h +45 -0
- package/src/native/shared/permissions.h +118 -0
- package/src/native/shared/permissions_cef.h +74 -0
- package/src/native/shared/preload_script.h +71 -0
- package/src/native/shared/shutdown_guard.h +134 -0
- package/src/native/shared/thread_safe_map.h +138 -0
- package/src/native/shared/webview_storage.h +91 -0
- package/src/native/win/cef_process_helper_win.cpp +143 -0
- package/src/native/win/dcomp_compositor.h +352 -0
- package/src/native/win/nativeWrapper.cpp +12434 -0
- package/src/npmbin/index.js +34 -0
- package/src/shared/bun-version.ts +3 -0
- package/src/shared/cef-version.ts +5 -0
- package/src/shared/naming.test.ts +327 -0
- package/src/shared/naming.ts +188 -0
- package/src/shared/platform.ts +48 -0
- package/src/shared/rpc.ts +541 -0
- package/src/shared/sparkbun-version.ts +2 -0
- package/src/types/three.d.ts +1 -0
- package/tsconfig.json +31 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,4653 @@
|
|
|
1
|
+
import { join, dirname, basename } from "path";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
cpSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
createWriteStream,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
symlinkSync,
|
|
14
|
+
statSync,
|
|
15
|
+
copyFileSync,
|
|
16
|
+
renameSync,
|
|
17
|
+
} from "fs";
|
|
18
|
+
import { execSync } from "child_process";
|
|
19
|
+
import * as readline from "readline";
|
|
20
|
+
import { OS, ARCH } from "../shared/platform";
|
|
21
|
+
import { DEFAULT_CEF_VERSION_STRING } from "../shared/cef-version";
|
|
22
|
+
import { BUN_VERSION } from "../shared/bun-version";
|
|
23
|
+
import { SPARKBUN_VERSION } from "../shared/sparkbun-version";
|
|
24
|
+
import {
|
|
25
|
+
getAppFileName,
|
|
26
|
+
getBundleFileName,
|
|
27
|
+
getPlatformPrefix,
|
|
28
|
+
getTarballFileName,
|
|
29
|
+
getWindowsSetupFileName,
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
31
|
+
sanitizeVolumeNameForHdiutil as _sanitizeVolumeNameForHdiutil,
|
|
32
|
+
getDmgVolumeName,
|
|
33
|
+
getMacOSBundleDisplayName,
|
|
34
|
+
} from "../shared/naming";
|
|
35
|
+
import { getTemplate, getTemplateNames } from "./templates/embedded";
|
|
36
|
+
// import { loadBsdiff, loadBspatch } from 'bsdiff-wasm';
|
|
37
|
+
// MacOS named pipes hang at around 4KB
|
|
38
|
+
// @ts-expect-error - reserved for future use
|
|
39
|
+
const _MAX_CHUNK_SIZE = 1024 * 2;
|
|
40
|
+
|
|
41
|
+
// const binExt = OS === 'win' ? '.exe' : '';
|
|
42
|
+
|
|
43
|
+
// Create a tar file using system tar command (preserves file permissions unlike Bun.Archive)
|
|
44
|
+
function createTar(tarPath: string, cwd: string, entries: string[]) {
|
|
45
|
+
// Use a relative path for the tar output on Windows to avoid bsdtar
|
|
46
|
+
// interpreting the "C:" drive letter as a remote host specifier.
|
|
47
|
+
const resolvedTarPath =
|
|
48
|
+
process.platform === "win32" ? path.relative(cwd, tarPath) : tarPath;
|
|
49
|
+
execSync(
|
|
50
|
+
`tar -cf "${resolvedTarPath}" ${entries.map((e) => `"${e}"`).join(" ")}`,
|
|
51
|
+
{
|
|
52
|
+
cwd,
|
|
53
|
+
stdio: "pipe",
|
|
54
|
+
// Prevent macOS tar from including Apple Double (._*) files. No-op on other platforms.
|
|
55
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create a tar.gz file using system tar command
|
|
61
|
+
|
|
62
|
+
// this when run as an npm script this will be where the folder where package.json is.
|
|
63
|
+
const projectRoot = process.cwd();
|
|
64
|
+
|
|
65
|
+
// Find TypeScript ESM config file
|
|
66
|
+
function findConfigFile(): string | null {
|
|
67
|
+
const configFile = join(projectRoot, "sparkbun.config.ts");
|
|
68
|
+
return existsSync(configFile) ? configFile : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Note: cli args can be called via npm bun /path/to/electorbun/binary arg1 arg2
|
|
72
|
+
const indexOfCli = process.argv.findIndex((arg) =>
|
|
73
|
+
arg.toLowerCase().includes("electrobun") || arg.toLowerCase().includes("sparkbun"),
|
|
74
|
+
);
|
|
75
|
+
const commandArg = process.argv[indexOfCli + 1] || "build";
|
|
76
|
+
|
|
77
|
+
// Walk up from projectRoot to find electrobun in node_modules (supports hoisted monorepo layouts)
|
|
78
|
+
function resolveSparkBunDir(): string {
|
|
79
|
+
// When running from SparkBun source (src/cli/index.ts), the package root is two levels up
|
|
80
|
+
const cliDir = dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"));
|
|
81
|
+
const sourcePackageDir = join(cliDir, "..", "..");
|
|
82
|
+
if (existsSync(join(sourcePackageDir, "package.json")) && existsSync(join(sourcePackageDir, "src", "cli"))) {
|
|
83
|
+
return sourcePackageDir;
|
|
84
|
+
}
|
|
85
|
+
let dir = projectRoot;
|
|
86
|
+
while (dir !== dirname(dir)) {
|
|
87
|
+
const candidate = join(dir, "node_modules", "sparkbun");
|
|
88
|
+
if (existsSync(join(candidate, "package.json"))) {
|
|
89
|
+
return candidate;
|
|
90
|
+
}
|
|
91
|
+
dir = dirname(dir);
|
|
92
|
+
}
|
|
93
|
+
return join(projectRoot, "node_modules", "sparkbun");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const SPARKBUN_DEP_PATH = resolveSparkBunDir();
|
|
97
|
+
const SPARKBUN_CACHE_PATH = join(dirname(SPARKBUN_DEP_PATH), ".sparkbun-cache");
|
|
98
|
+
|
|
99
|
+
// When debugging sparkbun with the example app use the builds (dev or release) right from the source folder
|
|
100
|
+
// For developers using sparkbun cli via npm use the release versions in /dist
|
|
101
|
+
// This lets us not have to commit src build folders to git and provide pre-built binaries
|
|
102
|
+
|
|
103
|
+
// Function to get platform-specific paths
|
|
104
|
+
function getPlatformPaths(
|
|
105
|
+
targetOS: "macos" | "win" | "linux",
|
|
106
|
+
targetArch: "arm64" | "x64",
|
|
107
|
+
) {
|
|
108
|
+
const binExt = targetOS === "win" ? ".exe" : "";
|
|
109
|
+
const platformDistDir = join(
|
|
110
|
+
SPARKBUN_DEP_PATH,
|
|
111
|
+
`dist-${targetOS}-${targetArch}`,
|
|
112
|
+
);
|
|
113
|
+
return {
|
|
114
|
+
// Platform-specific binaries (from dist-OS-ARCH/)
|
|
115
|
+
BUN_BINARY: join(platformDistDir, "bun") + binExt,
|
|
116
|
+
LAUNCHER_DEV: join(platformDistDir, "sparkbun") + binExt,
|
|
117
|
+
LAUNCHER_RELEASE: join(platformDistDir, "launcher") + binExt,
|
|
118
|
+
CORE_MACOS: join(platformDistDir, "libElectrobunCore.dylib"),
|
|
119
|
+
CORE_WIN: join(platformDistDir, "ElectrobunCore.dll"),
|
|
120
|
+
CORE_LINUX: join(platformDistDir, "libElectrobunCore.so"),
|
|
121
|
+
NATIVE_WRAPPER_MACOS: join(platformDistDir, "libNativeWrapper.dylib"),
|
|
122
|
+
NATIVE_WRAPPER_WIN: join(platformDistDir, "libNativeWrapper.dll"),
|
|
123
|
+
NATIVE_WRAPPER_LINUX: join(platformDistDir, "libNativeWrapper.so"),
|
|
124
|
+
NATIVE_WRAPPER_LINUX_CEF: join(platformDistDir, "libNativeWrapper_cef.so"),
|
|
125
|
+
WEBVIEW2LOADER_WIN: join(platformDistDir, "WebView2Loader.dll"),
|
|
126
|
+
BSPATCH: join(platformDistDir, "bspatch") + binExt,
|
|
127
|
+
EXTRACTOR: join(platformDistDir, "extractor") + binExt,
|
|
128
|
+
BSDIFF: join(platformDistDir, "bsdiff") + binExt,
|
|
129
|
+
CEF_FRAMEWORK_MACOS: join(
|
|
130
|
+
platformDistDir,
|
|
131
|
+
"cef",
|
|
132
|
+
"Chromium Embedded Framework.framework",
|
|
133
|
+
),
|
|
134
|
+
CEF_HELPER_MACOS: join(platformDistDir, "process_helper"),
|
|
135
|
+
CEF_HELPER_WIN: join(platformDistDir, "process_helper.exe"),
|
|
136
|
+
CEF_HELPER_LINUX: join(platformDistDir, "process_helper"),
|
|
137
|
+
CEF_DIR: join(platformDistDir, "cef"),
|
|
138
|
+
|
|
139
|
+
PRELOAD_FULL_JS: join(platformDistDir, "preload-full.js"),
|
|
140
|
+
PRELOAD_SANDBOXED_JS: join(platformDistDir, "preload-sandboxed.js"),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Default PATHS for host platform (backward compatibility)
|
|
145
|
+
// @ts-expect-error - reserved for future use
|
|
146
|
+
const _PATHS = getPlatformPaths(OS, ARCH);
|
|
147
|
+
|
|
148
|
+
function getCEFHelperNames(): string[] {
|
|
149
|
+
return [
|
|
150
|
+
"bun Helper",
|
|
151
|
+
"bun Helper (Alerts)",
|
|
152
|
+
"bun Helper (GPU)",
|
|
153
|
+
"bun Helper (Plugin)",
|
|
154
|
+
"bun Helper (Renderer)",
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function ensureCoreDependencies(
|
|
159
|
+
targetOS?: "macos" | "win" | "linux",
|
|
160
|
+
targetArch?: "arm64" | "x64",
|
|
161
|
+
) {
|
|
162
|
+
// Use provided target platform or default to host platform
|
|
163
|
+
const platformOS = targetOS || OS;
|
|
164
|
+
const platformArch = targetArch || ARCH;
|
|
165
|
+
|
|
166
|
+
// Get platform-specific paths
|
|
167
|
+
const platformPaths = getPlatformPaths(platformOS, platformArch);
|
|
168
|
+
|
|
169
|
+
// Check platform-specific binaries
|
|
170
|
+
// BUN_BINARY not required — SparkBun compiles the launcher via bun build --compile
|
|
171
|
+
const requiredBinaries = [
|
|
172
|
+
platformPaths.BSDIFF,
|
|
173
|
+
platformPaths.BSPATCH,
|
|
174
|
+
];
|
|
175
|
+
if (platformOS === "macos") {
|
|
176
|
+
requiredBinaries.push(
|
|
177
|
+
platformPaths.NATIVE_WRAPPER_MACOS,
|
|
178
|
+
);
|
|
179
|
+
} else if (platformOS === "win") {
|
|
180
|
+
requiredBinaries.push(platformPaths.NATIVE_WRAPPER_WIN);
|
|
181
|
+
} else {
|
|
182
|
+
requiredBinaries.push(platformPaths.NATIVE_WRAPPER_LINUX);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const missingBinaries = requiredBinaries.filter((file) => !existsSync(file));
|
|
186
|
+
|
|
187
|
+
// Only download if platform-specific binaries are missing
|
|
188
|
+
if (missingBinaries.length === 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Show which binaries are missing
|
|
193
|
+
console.log(
|
|
194
|
+
`Core dependencies not found for ${platformOS}-${platformArch}. Missing files:`,
|
|
195
|
+
missingBinaries.map((f) => f.replace(SPARKBUN_DEP_PATH, ".")).join(", "),
|
|
196
|
+
);
|
|
197
|
+
console.log(`Downloading core binaries for ${platformOS}-${platformArch}...`);
|
|
198
|
+
|
|
199
|
+
const version = `v${SPARKBUN_VERSION}`;
|
|
200
|
+
|
|
201
|
+
const platformName =
|
|
202
|
+
platformOS === "macos" ? "darwin" : platformOS === "win" ? "win" : "linux";
|
|
203
|
+
const archName = platformArch;
|
|
204
|
+
const coreTarballUrl = `https://github.com/gruntlord5/SparkBun/releases/download/${version}/sparkbun-core-${platformName}-${archName}.tar.gz`;
|
|
205
|
+
|
|
206
|
+
console.log(`Downloading core binaries from: ${coreTarballUrl}`);
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Download core binaries tarball
|
|
210
|
+
const response = await fetch(coreTarballUrl);
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Failed to download binaries: ${response.status} ${response.statusText}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Create temp file
|
|
218
|
+
const tempFile = join(
|
|
219
|
+
SPARKBUN_DEP_PATH,
|
|
220
|
+
`core-${platformOS}-${platformArch}-temp.tar.gz`,
|
|
221
|
+
);
|
|
222
|
+
const fileStream = createWriteStream(tempFile);
|
|
223
|
+
|
|
224
|
+
// Write response to file
|
|
225
|
+
if (response.body) {
|
|
226
|
+
const reader = response.body.getReader();
|
|
227
|
+
let totalBytes = 0;
|
|
228
|
+
while (true) {
|
|
229
|
+
const { done, value } = await reader.read();
|
|
230
|
+
if (done) break;
|
|
231
|
+
const buffer = Buffer.from(value);
|
|
232
|
+
fileStream.write(buffer);
|
|
233
|
+
totalBytes += buffer.length;
|
|
234
|
+
}
|
|
235
|
+
console.log(
|
|
236
|
+
`Downloaded ${totalBytes} bytes for ${platformOS}-${platformArch}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Ensure file is properly closed before proceeding
|
|
241
|
+
await new Promise((resolve, reject) => {
|
|
242
|
+
fileStream.end((err: Error | null | undefined) => {
|
|
243
|
+
if (err) reject(err);
|
|
244
|
+
else resolve(null);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Verify the downloaded file exists and has content
|
|
249
|
+
if (!existsSync(tempFile)) {
|
|
250
|
+
throw new Error(`Downloaded file not found: ${tempFile}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const fileSize = require("fs").statSync(tempFile).size;
|
|
254
|
+
if (fileSize === 0) {
|
|
255
|
+
throw new Error(`Downloaded file is empty: ${tempFile}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
console.log(`Verified download: ${tempFile} (${fileSize} bytes)`);
|
|
259
|
+
|
|
260
|
+
// Extract to platform-specific dist directory
|
|
261
|
+
console.log(
|
|
262
|
+
`Extracting core dependencies for ${platformOS}-${platformArch}...`,
|
|
263
|
+
);
|
|
264
|
+
const platformDistPath = join(
|
|
265
|
+
SPARKBUN_DEP_PATH,
|
|
266
|
+
`dist-${platformOS}-${platformArch}`,
|
|
267
|
+
);
|
|
268
|
+
mkdirSync(platformDistPath, { recursive: true });
|
|
269
|
+
|
|
270
|
+
const tarBytes = await Bun.file(tempFile).arrayBuffer();
|
|
271
|
+
const archive = new Bun.Archive(tarBytes);
|
|
272
|
+
await archive.extract(platformDistPath);
|
|
273
|
+
|
|
274
|
+
// NOTE: We no longer copy main.js from platform-specific downloads
|
|
275
|
+
// Platform-specific downloads should only contain native binaries
|
|
276
|
+
// main.js and api/ should be shipped via npm in the shared dist/ folder
|
|
277
|
+
|
|
278
|
+
// Clean up temp file
|
|
279
|
+
unlinkSync(tempFile);
|
|
280
|
+
|
|
281
|
+
// Debug: List what was actually extracted
|
|
282
|
+
try {
|
|
283
|
+
const extractedFiles = readdirSync(platformDistPath);
|
|
284
|
+
console.log(`Extracted files to ${platformDistPath}:`, extractedFiles);
|
|
285
|
+
|
|
286
|
+
// Check if files are in subdirectories
|
|
287
|
+
for (const file of extractedFiles) {
|
|
288
|
+
const filePath = join(platformDistPath, file);
|
|
289
|
+
const stat = require("fs").statSync(filePath);
|
|
290
|
+
if (stat.isDirectory()) {
|
|
291
|
+
const subFiles = readdirSync(filePath);
|
|
292
|
+
console.log(` ${file}/: ${subFiles.join(", ")}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch (e) {
|
|
296
|
+
console.error("Could not list extracted files:", e);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Verify extraction completed successfully - check platform-specific binaries only
|
|
300
|
+
const requiredBinaries = [
|
|
301
|
+
platformPaths.BUN_BINARY,
|
|
302
|
+
platformPaths.BSDIFF,
|
|
303
|
+
platformPaths.BSPATCH,
|
|
304
|
+
];
|
|
305
|
+
if (platformOS === "macos") {
|
|
306
|
+
requiredBinaries.push(
|
|
307
|
+
platformPaths.LAUNCHER_RELEASE,
|
|
308
|
+
platformPaths.NATIVE_WRAPPER_MACOS,
|
|
309
|
+
);
|
|
310
|
+
} else if (platformOS === "win") {
|
|
311
|
+
requiredBinaries.push(platformPaths.NATIVE_WRAPPER_WIN);
|
|
312
|
+
} else {
|
|
313
|
+
requiredBinaries.push(platformPaths.NATIVE_WRAPPER_LINUX);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const missingBinaries = requiredBinaries.filter(
|
|
317
|
+
(file) => !existsSync(file),
|
|
318
|
+
);
|
|
319
|
+
if (missingBinaries.length > 0) {
|
|
320
|
+
console.error(
|
|
321
|
+
`Missing binaries after extraction: ${missingBinaries.map((f) => f.replace(SPARKBUN_DEP_PATH, ".")).join(", ")}`,
|
|
322
|
+
);
|
|
323
|
+
console.error(
|
|
324
|
+
"This suggests the tarball structure is different than expected",
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Note: We no longer need to remove or re-add signatures from downloaded binaries
|
|
329
|
+
// The CI-added adhoc signatures are actually required for macOS to run the binaries
|
|
330
|
+
|
|
331
|
+
// For development: if main.js doesn't exist in shared dist/, copy from platform-specific download as fallback
|
|
332
|
+
const sharedDistPath = join(SPARKBUN_DEP_PATH, "dist");
|
|
333
|
+
const extractedMainJs = join(platformDistPath, "main.js");
|
|
334
|
+
const sharedMainJs = join(sharedDistPath, "main.js");
|
|
335
|
+
|
|
336
|
+
if (existsSync(extractedMainJs) && !existsSync(sharedMainJs)) {
|
|
337
|
+
console.log(
|
|
338
|
+
"Development fallback: copying main.js from platform-specific download to shared dist/",
|
|
339
|
+
);
|
|
340
|
+
mkdirSync(sharedDistPath, { recursive: true });
|
|
341
|
+
cpSync(extractedMainJs, sharedMainJs, { dereference: true });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.log(
|
|
345
|
+
`Core dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`,
|
|
346
|
+
);
|
|
347
|
+
} catch (error: any) {
|
|
348
|
+
console.error(
|
|
349
|
+
`Failed to download core dependencies for ${platformOS}-${platformArch}:`,
|
|
350
|
+
error.message,
|
|
351
|
+
);
|
|
352
|
+
console.error(
|
|
353
|
+
"Please ensure you have an internet connection and the release exists.",
|
|
354
|
+
);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Returns the effective CEF directory path. When a custom cefVersion is set,
|
|
361
|
+
* CEF files are stored in node_modules/.sparkbun-cache/ which survives
|
|
362
|
+
* both dist rebuilds and bun install (which replaces node_modules/electrobun).
|
|
363
|
+
* When using the default version, returns the standard dist-{platform}/cef/ path.
|
|
364
|
+
*/
|
|
365
|
+
function getEffectiveCEFDir(
|
|
366
|
+
platformOS: "macos" | "win" | "linux",
|
|
367
|
+
platformArch: "arm64" | "x64",
|
|
368
|
+
cefVersion?: string,
|
|
369
|
+
): string {
|
|
370
|
+
if (cefVersion) {
|
|
371
|
+
return join(SPARKBUN_CACHE_PATH, "cef-override", `${platformOS}-${platformArch}`);
|
|
372
|
+
}
|
|
373
|
+
return getPlatformPaths(platformOS, platformArch).CEF_DIR;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Returns the effective WGPU directory path. WGPU files are stored in
|
|
378
|
+
* node_modules/.sparkbun-cache/ to survive dist rebuilds and bun install.
|
|
379
|
+
*/
|
|
380
|
+
function getEffectiveWGPUDir(
|
|
381
|
+
platformOS: "macos" | "win" | "linux",
|
|
382
|
+
platformArch: "arm64" | "x64",
|
|
383
|
+
): string {
|
|
384
|
+
return join(
|
|
385
|
+
projectRoot,
|
|
386
|
+
"node_modules",
|
|
387
|
+
".sparkbun-cache",
|
|
388
|
+
"wgpu",
|
|
389
|
+
`${platformOS}-${platformArch}`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Trims an ICU .dat file to only include the specified locales.
|
|
395
|
+
* Uses icupkg (from ICU tools) to list and remove unwanted locale data.
|
|
396
|
+
*/
|
|
397
|
+
async function trimICUData(
|
|
398
|
+
source: string,
|
|
399
|
+
dest: string,
|
|
400
|
+
locales: string[],
|
|
401
|
+
): Promise<void> {
|
|
402
|
+
// Copy the full .dat file first
|
|
403
|
+
cpSync(source, dest);
|
|
404
|
+
|
|
405
|
+
// Try to find icupkg in PATH or common locations
|
|
406
|
+
let icupkgPath = "icupkg";
|
|
407
|
+
try {
|
|
408
|
+
execSync(`${icupkgPath} --help`, { stdio: "ignore" });
|
|
409
|
+
} catch {
|
|
410
|
+
// icupkg not available, skip trimming
|
|
411
|
+
throw new Error(
|
|
412
|
+
"icupkg not found in PATH. Install ICU tools to enable locale trimming.",
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// List all items in the .dat file
|
|
417
|
+
const listOutput = execSync(`${icupkgPath} -l "${dest}"`, {
|
|
418
|
+
encoding: "utf-8",
|
|
419
|
+
});
|
|
420
|
+
const allItems = listOutput.split("\n").filter((line) => line.trim());
|
|
421
|
+
|
|
422
|
+
// Locale-specific directories in ICU data
|
|
423
|
+
const localeDirs = [
|
|
424
|
+
"brkitr/",
|
|
425
|
+
"coll/",
|
|
426
|
+
"curr/",
|
|
427
|
+
"lang/",
|
|
428
|
+
"locales/",
|
|
429
|
+
"rbnf/",
|
|
430
|
+
"region/",
|
|
431
|
+
"unit/",
|
|
432
|
+
"zone/",
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
const toRemove = allItems.filter((item) => {
|
|
436
|
+
// Only consider items in locale-specific directories
|
|
437
|
+
const isLocaleItem = localeDirs.some((dir) => item.startsWith(dir));
|
|
438
|
+
if (!isLocaleItem) return false;
|
|
439
|
+
|
|
440
|
+
// Extract the basename (after the last /)
|
|
441
|
+
const basename = item.split("/").pop() || "";
|
|
442
|
+
// Remove file extension for matching
|
|
443
|
+
const name = basename.replace(/\.res$/, "");
|
|
444
|
+
|
|
445
|
+
// Keep items matching requested locales (exact match or with region suffix)
|
|
446
|
+
return !locales.some(
|
|
447
|
+
(l) =>
|
|
448
|
+
name === l ||
|
|
449
|
+
name === "root" ||
|
|
450
|
+
name.startsWith(`${l}_`) ||
|
|
451
|
+
name.startsWith(`${l}-`),
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
if (toRemove.length > 0) {
|
|
456
|
+
// Write removal list to temp file
|
|
457
|
+
const { tmpdir } = await import("os");
|
|
458
|
+
const removeListPath = join(tmpdir(), "icu-remove.txt");
|
|
459
|
+
writeFileSync(removeListPath, toRemove.join("\n"));
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
execSync(`${icupkgPath} -r "@${removeListPath}" "${dest}"`, {
|
|
463
|
+
stdio: "inherit",
|
|
464
|
+
});
|
|
465
|
+
} finally {
|
|
466
|
+
try {
|
|
467
|
+
unlinkSync(removeListPath);
|
|
468
|
+
} catch {
|
|
469
|
+
// ignore cleanup errors
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function ensureCEFDependencies(
|
|
476
|
+
targetOS?: "macos" | "win" | "linux",
|
|
477
|
+
targetArch?: "arm64" | "x64",
|
|
478
|
+
cefVersion?: string,
|
|
479
|
+
): Promise<string> {
|
|
480
|
+
// Use provided target platform or default to host platform
|
|
481
|
+
const platformOS = targetOS || OS;
|
|
482
|
+
const platformArch = targetArch || ARCH;
|
|
483
|
+
|
|
484
|
+
// Get platform-specific paths
|
|
485
|
+
const platformPaths = getPlatformPaths(platformOS, platformArch);
|
|
486
|
+
|
|
487
|
+
// If custom CEF version specified, download from Spotify CDN
|
|
488
|
+
// Custom CEF is stored in vendors/cef-override/ to survive dist rebuilds
|
|
489
|
+
if (cefVersion) {
|
|
490
|
+
const overrideDir = getEffectiveCEFDir(
|
|
491
|
+
platformOS,
|
|
492
|
+
platformArch,
|
|
493
|
+
cefVersion,
|
|
494
|
+
);
|
|
495
|
+
// Check if already downloaded with matching version
|
|
496
|
+
const cefVersionFile = join(overrideDir, ".cef-version");
|
|
497
|
+
if (existsSync(overrideDir) && existsSync(cefVersionFile)) {
|
|
498
|
+
const cachedVersion = readFileSync(cefVersionFile, "utf8").trim();
|
|
499
|
+
if (cachedVersion === cefVersion) {
|
|
500
|
+
console.log(
|
|
501
|
+
`Custom CEF ${cefVersion} already cached for ${platformOS}-${platformArch} at ${overrideDir}`,
|
|
502
|
+
);
|
|
503
|
+
return overrideDir;
|
|
504
|
+
}
|
|
505
|
+
// Version mismatch - remove stale cache
|
|
506
|
+
console.log(
|
|
507
|
+
`Cached CEF version "${cachedVersion}" does not match requested "${cefVersion}", re-downloading...`,
|
|
508
|
+
);
|
|
509
|
+
rmSync(overrideDir, { recursive: true, force: true });
|
|
510
|
+
} else if (existsSync(overrideDir)) {
|
|
511
|
+
// Override dir exists but no version stamp - remove it
|
|
512
|
+
rmSync(overrideDir, { recursive: true, force: true });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
await downloadAndExtractCustomCEF(cefVersion, platformOS, platformArch);
|
|
516
|
+
return overrideDir;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Check if CEF dependencies already exist
|
|
520
|
+
if (existsSync(platformPaths.CEF_DIR)) {
|
|
521
|
+
console.log(
|
|
522
|
+
`CEF dependencies found for ${platformOS}-${platformArch}, using cached version`,
|
|
523
|
+
);
|
|
524
|
+
return platformPaths.CEF_DIR;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
console.log(
|
|
528
|
+
`CEF dependencies not found for ${platformOS}-${platformArch}, downloading...`,
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
const version = `v${SPARKBUN_VERSION}`;
|
|
532
|
+
|
|
533
|
+
const platformName =
|
|
534
|
+
platformOS === "macos" ? "darwin" : platformOS === "win" ? "win" : "linux";
|
|
535
|
+
const archName = platformArch;
|
|
536
|
+
const cefTarballUrl = `https://github.com/gruntlord5/SparkBun/releases/download/${version}/sparkbun-cef-${platformName}-${archName}.tar.gz`;
|
|
537
|
+
|
|
538
|
+
// Helper function to download with retry logic
|
|
539
|
+
async function downloadWithRetry(
|
|
540
|
+
url: string,
|
|
541
|
+
filePath: string,
|
|
542
|
+
maxRetries = 3,
|
|
543
|
+
): Promise<void> {
|
|
544
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
545
|
+
try {
|
|
546
|
+
console.log(
|
|
547
|
+
`Downloading CEF (attempt ${attempt}/${maxRetries}) from: ${url}`,
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
const response = await fetch(url);
|
|
551
|
+
if (!response.ok) {
|
|
552
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Get content length for progress tracking
|
|
556
|
+
const contentLength = response.headers.get("content-length");
|
|
557
|
+
const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
|
|
558
|
+
|
|
559
|
+
// Create temp file with unique name to avoid conflicts
|
|
560
|
+
const fileStream = createWriteStream(filePath);
|
|
561
|
+
let downloadedSize = 0;
|
|
562
|
+
let lastReportedPercent = -1;
|
|
563
|
+
|
|
564
|
+
// Stream download with progress
|
|
565
|
+
if (response.body) {
|
|
566
|
+
const reader = response.body.getReader();
|
|
567
|
+
while (true) {
|
|
568
|
+
const { done, value } = await reader.read();
|
|
569
|
+
if (done) break;
|
|
570
|
+
|
|
571
|
+
const chunk = Buffer.from(value);
|
|
572
|
+
fileStream.write(chunk);
|
|
573
|
+
downloadedSize += chunk.length;
|
|
574
|
+
|
|
575
|
+
if (totalSize > 0) {
|
|
576
|
+
const percent = Math.round((downloadedSize / totalSize) * 100);
|
|
577
|
+
const percentTier = Math.floor(percent / 10) * 10;
|
|
578
|
+
if (percentTier > lastReportedPercent && percentTier <= 100) {
|
|
579
|
+
console.log(
|
|
580
|
+
` Progress: ${percentTier}% (${Math.round(downloadedSize / 1024 / 1024)}MB/${Math.round(totalSize / 1024 / 1024)}MB)`,
|
|
581
|
+
);
|
|
582
|
+
lastReportedPercent = percentTier;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
await new Promise((resolve, reject) => {
|
|
589
|
+
fileStream.end((error: any) => {
|
|
590
|
+
if (error) reject(error);
|
|
591
|
+
else resolve(void 0);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Verify file size if content-length was provided
|
|
596
|
+
if (totalSize > 0) {
|
|
597
|
+
const actualSize = (await import("fs")).statSync(filePath).size;
|
|
598
|
+
if (actualSize !== totalSize) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
`Downloaded file size mismatch: expected ${totalSize}, got ${actualSize}`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
console.log(
|
|
606
|
+
`✓ Download completed successfully (${Math.round(downloadedSize / 1024 / 1024)}MB)`,
|
|
607
|
+
);
|
|
608
|
+
return; // Success, exit retry loop
|
|
609
|
+
} catch (error: any) {
|
|
610
|
+
console.error(`Download attempt ${attempt} failed:`, error.message);
|
|
611
|
+
|
|
612
|
+
// Clean up partial download
|
|
613
|
+
if (existsSync(filePath)) {
|
|
614
|
+
unlinkSync(filePath);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (attempt === maxRetries) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
`Failed to download after ${maxRetries} attempts: ${error.message}`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Wait before retrying (exponential backoff)
|
|
624
|
+
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s...
|
|
625
|
+
console.log(`Retrying in ${delay / 1000} seconds...`);
|
|
626
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
// Create temp file with unique name
|
|
633
|
+
const tempFile = join(
|
|
634
|
+
SPARKBUN_DEP_PATH,
|
|
635
|
+
`cef-${platformOS}-${platformArch}-${Date.now()}.tar.gz`,
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
// Download with retry logic
|
|
639
|
+
await downloadWithRetry(cefTarballUrl, tempFile);
|
|
640
|
+
|
|
641
|
+
// Extract to platform-specific dist directory
|
|
642
|
+
console.log(
|
|
643
|
+
`Extracting CEF dependencies for ${platformOS}-${platformArch}...`,
|
|
644
|
+
);
|
|
645
|
+
const platformDistPath = join(
|
|
646
|
+
SPARKBUN_DEP_PATH,
|
|
647
|
+
`dist-${platformOS}-${platformArch}`,
|
|
648
|
+
);
|
|
649
|
+
mkdirSync(platformDistPath, { recursive: true });
|
|
650
|
+
|
|
651
|
+
// Helper function to validate tar file before extraction
|
|
652
|
+
async function validateTarFile(filePath: string): Promise<void> {
|
|
653
|
+
try {
|
|
654
|
+
// Quick validation - try to read the tar file header
|
|
655
|
+
const fd = await import("fs").then((fs) =>
|
|
656
|
+
fs.promises.readFile(filePath),
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
// Check if it's a gzip file (magic bytes: 1f 8b)
|
|
660
|
+
if (fd.length < 2 || fd[0] !== 0x1f || fd[1] !== 0x8b) {
|
|
661
|
+
throw new Error("Invalid gzip header - file may be corrupted");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
console.log(
|
|
665
|
+
`✓ Tar file validation passed (${Math.round(fd.length / 1024 / 1024)}MB)`,
|
|
666
|
+
);
|
|
667
|
+
} catch (error: any) {
|
|
668
|
+
throw new Error(`Tar file validation failed: ${error.message}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Validate downloaded file before extraction
|
|
673
|
+
await validateTarFile(tempFile);
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
const cefTarBytes = await Bun.file(tempFile).arrayBuffer();
|
|
677
|
+
const cefArchive = new Bun.Archive(cefTarBytes);
|
|
678
|
+
await cefArchive.extract(platformDistPath);
|
|
679
|
+
|
|
680
|
+
console.log(`✓ Extraction completed successfully`);
|
|
681
|
+
} catch (error: any) {
|
|
682
|
+
// Check if CEF directory was created despite the error (partial extraction)
|
|
683
|
+
const cefDir = join(platformDistPath, "cef");
|
|
684
|
+
if (existsSync(cefDir)) {
|
|
685
|
+
const cefFiles = readdirSync(cefDir);
|
|
686
|
+
if (cefFiles.length > 0) {
|
|
687
|
+
console.warn(`⚠️ Extraction warning: ${error.message}`);
|
|
688
|
+
console.warn(
|
|
689
|
+
` However, CEF files were extracted (${cefFiles.length} files found).`,
|
|
690
|
+
);
|
|
691
|
+
console.warn(
|
|
692
|
+
` Proceeding with partial extraction - this usually works fine.`,
|
|
693
|
+
);
|
|
694
|
+
// Don't throw - continue with what we have
|
|
695
|
+
} else {
|
|
696
|
+
// No files extracted, this is a real failure
|
|
697
|
+
throw new Error(
|
|
698
|
+
`Extraction failed (no files extracted): ${error.message}`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
} else {
|
|
702
|
+
// No CEF directory created, this is a real failure
|
|
703
|
+
throw new Error(
|
|
704
|
+
`Extraction failed (no CEF directory created): ${error.message}`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Clean up temp file only after successful extraction
|
|
710
|
+
try {
|
|
711
|
+
unlinkSync(tempFile);
|
|
712
|
+
} catch (cleanupError) {
|
|
713
|
+
console.warn("Could not clean up temp file:", cleanupError);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Debug: List what was actually extracted for CEF
|
|
717
|
+
try {
|
|
718
|
+
const extractedFiles = readdirSync(platformDistPath);
|
|
719
|
+
console.log(
|
|
720
|
+
`CEF extracted files to ${platformDistPath}:`,
|
|
721
|
+
extractedFiles,
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
// Check if CEF directory was created
|
|
725
|
+
const cefDir = join(platformDistPath, "cef");
|
|
726
|
+
if (existsSync(cefDir)) {
|
|
727
|
+
const cefFiles = readdirSync(cefDir);
|
|
728
|
+
console.log(
|
|
729
|
+
`CEF directory contents: ${cefFiles.slice(0, 10).join(", ")}${cefFiles.length > 10 ? "..." : ""}`,
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
} catch (e) {
|
|
733
|
+
console.error("Could not list CEF extracted files:", e);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
console.log(
|
|
737
|
+
`✓ CEF dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`,
|
|
738
|
+
);
|
|
739
|
+
return platformPaths.CEF_DIR;
|
|
740
|
+
} catch (error: any) {
|
|
741
|
+
console.error(
|
|
742
|
+
`Failed to download CEF dependencies for ${platformOS}-${platformArch}:`,
|
|
743
|
+
error.message,
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
// Provide helpful guidance based on the error
|
|
747
|
+
if (
|
|
748
|
+
error.message.includes("corrupted download") ||
|
|
749
|
+
error.message.includes("zlib") ||
|
|
750
|
+
error.message.includes("unexpected end")
|
|
751
|
+
) {
|
|
752
|
+
console.error(
|
|
753
|
+
"\n💡 This appears to be a download corruption issue. Suggestions:",
|
|
754
|
+
);
|
|
755
|
+
console.error(" • Check your internet connection stability");
|
|
756
|
+
console.error(
|
|
757
|
+
" • Try running the command again (it will retry automatically)",
|
|
758
|
+
);
|
|
759
|
+
console.error(" • Clear the cache if the issue persists:");
|
|
760
|
+
console.error(` rm -rf "${SPARKBUN_DEP_PATH}"`);
|
|
761
|
+
} else if (
|
|
762
|
+
error.message.includes("HTTP 404") ||
|
|
763
|
+
error.message.includes("Not Found")
|
|
764
|
+
) {
|
|
765
|
+
console.error("\n💡 The CEF release was not found. This could mean:");
|
|
766
|
+
console.error(
|
|
767
|
+
" • The version specified doesn't have CEF binaries available",
|
|
768
|
+
);
|
|
769
|
+
console.error(" • You're using a development/unreleased version");
|
|
770
|
+
console.error(" • Try using a stable version instead");
|
|
771
|
+
} else {
|
|
772
|
+
console.error(
|
|
773
|
+
"\nPlease ensure you have an internet connection and the release exists.",
|
|
774
|
+
);
|
|
775
|
+
console.error(
|
|
776
|
+
`If the problem persists, try clearing the cache: rm -rf "${SPARKBUN_DEP_PATH}"`,
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
async function ensureWGPUDependencies(
|
|
785
|
+
targetOS?: "macos" | "win" | "linux",
|
|
786
|
+
targetArch?: "arm64" | "x64",
|
|
787
|
+
wgpuVersion?: string,
|
|
788
|
+
): Promise<string> {
|
|
789
|
+
const platformOS = targetOS || OS;
|
|
790
|
+
const platformArch = targetArch || ARCH;
|
|
791
|
+
const wgpuDir = getEffectiveWGPUDir(platformOS, platformArch);
|
|
792
|
+
const versionFile = join(wgpuDir, ".wgpu-version");
|
|
793
|
+
|
|
794
|
+
const normalizedVersion =
|
|
795
|
+
wgpuVersion && wgpuVersion.length > 0
|
|
796
|
+
? wgpuVersion.startsWith("v")
|
|
797
|
+
? wgpuVersion
|
|
798
|
+
: `v${wgpuVersion}`
|
|
799
|
+
: "latest";
|
|
800
|
+
|
|
801
|
+
if (existsSync(wgpuDir) && existsSync(versionFile)) {
|
|
802
|
+
const cachedVersion = readFileSync(versionFile, "utf8").trim();
|
|
803
|
+
if (cachedVersion === normalizedVersion) {
|
|
804
|
+
console.log(
|
|
805
|
+
`WGPU ${normalizedVersion} already cached for ${platformOS}-${platformArch} at ${wgpuDir}`,
|
|
806
|
+
);
|
|
807
|
+
return wgpuDir;
|
|
808
|
+
}
|
|
809
|
+
console.log(
|
|
810
|
+
`Cached WGPU version "${cachedVersion}" does not match requested "${normalizedVersion}", re-downloading...`,
|
|
811
|
+
);
|
|
812
|
+
rmSync(wgpuDir, { recursive: true, force: true });
|
|
813
|
+
} else if (existsSync(wgpuDir)) {
|
|
814
|
+
rmSync(wgpuDir, { recursive: true, force: true });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const platformName =
|
|
818
|
+
platformOS === "macos" ? "darwin" : platformOS === "win" ? "win32" : "linux";
|
|
819
|
+
const archName = platformArch;
|
|
820
|
+
const baseUrl =
|
|
821
|
+
normalizedVersion === "latest"
|
|
822
|
+
? "https://github.com/blackboardsh/sparkbun-dawn/releases/latest/download"
|
|
823
|
+
: `https://github.com/blackboardsh/sparkbun-dawn/releases/download/${normalizedVersion}`;
|
|
824
|
+
const tarballUrl = `${baseUrl}/sparkbun-dawn-${platformName}-${archName}.tar.gz`;
|
|
825
|
+
|
|
826
|
+
async function downloadWithRetry(
|
|
827
|
+
url: string,
|
|
828
|
+
filePath: string,
|
|
829
|
+
maxRetries = 3,
|
|
830
|
+
): Promise<void> {
|
|
831
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
832
|
+
try {
|
|
833
|
+
console.log(
|
|
834
|
+
`Downloading WGPU (attempt ${attempt}/${maxRetries}) from: ${url}`,
|
|
835
|
+
);
|
|
836
|
+
const response = await fetch(url);
|
|
837
|
+
if (!response.ok) {
|
|
838
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const fileStream = createWriteStream(filePath);
|
|
842
|
+
if (response.body) {
|
|
843
|
+
const reader = response.body.getReader();
|
|
844
|
+
while (true) {
|
|
845
|
+
const { done, value } = await reader.read();
|
|
846
|
+
if (done) break;
|
|
847
|
+
fileStream.write(Buffer.from(value));
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
await new Promise((resolve, reject) => {
|
|
852
|
+
fileStream.end((err: Error | null | undefined) => {
|
|
853
|
+
if (err) reject(err);
|
|
854
|
+
else resolve(null);
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
return;
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (attempt === maxRetries) throw error;
|
|
860
|
+
console.log(`Download failed, retrying... (${attempt}/${maxRetries})`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
console.log(
|
|
867
|
+
`WGPU dependencies not found for ${platformOS}-${platformArch}, downloading...`,
|
|
868
|
+
);
|
|
869
|
+
const tempFile = join(
|
|
870
|
+
SPARKBUN_DEP_PATH,
|
|
871
|
+
`wgpu-${platformOS}-${platformArch}-${Date.now()}.tar.gz`,
|
|
872
|
+
);
|
|
873
|
+
await downloadWithRetry(tarballUrl, tempFile);
|
|
874
|
+
|
|
875
|
+
if (!existsSync(tempFile)) {
|
|
876
|
+
throw new Error(`Downloaded file not found: ${tempFile}`);
|
|
877
|
+
}
|
|
878
|
+
const fileSize = require("fs").statSync(tempFile).size;
|
|
879
|
+
if (fileSize === 0) {
|
|
880
|
+
throw new Error(`Downloaded file is empty: ${tempFile}`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const tempExtractDir = join(
|
|
884
|
+
SPARKBUN_DEP_PATH,
|
|
885
|
+
`wgpu-extract-${platformOS}-${platformArch}-${Date.now()}`,
|
|
886
|
+
);
|
|
887
|
+
mkdirSync(tempExtractDir, { recursive: true });
|
|
888
|
+
|
|
889
|
+
const tarBytes = await Bun.file(tempFile).arrayBuffer();
|
|
890
|
+
const archive = new Bun.Archive(tarBytes);
|
|
891
|
+
await archive.extract(tempExtractDir);
|
|
892
|
+
|
|
893
|
+
mkdirSync(wgpuDir, { recursive: true });
|
|
894
|
+
const extractedItems = readdirSync(tempExtractDir);
|
|
895
|
+
|
|
896
|
+
const moveAll = (fromDir: string) => {
|
|
897
|
+
for (const item of readdirSync(fromDir)) {
|
|
898
|
+
const src = join(fromDir, item);
|
|
899
|
+
const dest = join(wgpuDir, item);
|
|
900
|
+
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
|
|
901
|
+
renameSync(src, dest);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
if (extractedItems.length === 1) {
|
|
906
|
+
const firstItem = extractedItems[0];
|
|
907
|
+
if (!firstItem) {
|
|
908
|
+
throw new Error(`No extracted items found in ${tempExtractDir}`);
|
|
909
|
+
}
|
|
910
|
+
const single = join(tempExtractDir, firstItem);
|
|
911
|
+
const stat = require("fs").statSync(single);
|
|
912
|
+
if (stat.isDirectory()) {
|
|
913
|
+
moveAll(single);
|
|
914
|
+
} else {
|
|
915
|
+
moveAll(tempExtractDir);
|
|
916
|
+
}
|
|
917
|
+
} else {
|
|
918
|
+
moveAll(tempExtractDir);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
writeFileSync(versionFile, normalizedVersion);
|
|
922
|
+
|
|
923
|
+
rmSync(tempExtractDir, { recursive: true, force: true });
|
|
924
|
+
unlinkSync(tempFile);
|
|
925
|
+
|
|
926
|
+
console.log(
|
|
927
|
+
`✓ WGPU dependencies for ${platformOS}-${platformArch} downloaded and cached successfully`,
|
|
928
|
+
);
|
|
929
|
+
return wgpuDir;
|
|
930
|
+
} catch (error: any) {
|
|
931
|
+
if (existsSync(wgpuDir)) {
|
|
932
|
+
try {
|
|
933
|
+
rmSync(wgpuDir, { recursive: true, force: true });
|
|
934
|
+
} catch {}
|
|
935
|
+
}
|
|
936
|
+
console.error(
|
|
937
|
+
`Failed to download WGPU dependencies for ${platformOS}-${platformArch}:`,
|
|
938
|
+
error.message,
|
|
939
|
+
);
|
|
940
|
+
console.error("Please ensure you have an internet connection and the release exists.");
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Downloads CEF runtime files from Spotify CDN for a custom version override.
|
|
947
|
+
* Extracts the minimal distribution and restructures runtime files to the
|
|
948
|
+
* layout the CLI expects. No compilation is needed — process_helper ships in
|
|
949
|
+
* the core tarball and uses CEF's stable C API at runtime.
|
|
950
|
+
*
|
|
951
|
+
* The C API is designed for ABI stability within the same major version line.
|
|
952
|
+
* Across major versions, breaking changes are possible.
|
|
953
|
+
*/
|
|
954
|
+
async function downloadAndExtractCustomCEF(
|
|
955
|
+
cefVersion: string,
|
|
956
|
+
platformOS: "macos" | "win" | "linux",
|
|
957
|
+
platformArch: "arm64" | "x64",
|
|
958
|
+
) {
|
|
959
|
+
// Parse "CEF_VERSION+chromium-CHROMIUM_VERSION"
|
|
960
|
+
const match = cefVersion.match(/^(.+)\+chromium-(.+)$/);
|
|
961
|
+
if (!match) {
|
|
962
|
+
throw new Error(
|
|
963
|
+
`Invalid cefVersion format: "${cefVersion}". ` +
|
|
964
|
+
`Expected: "CEF_VERSION+chromium-CHROMIUM_VERSION" ` +
|
|
965
|
+
`(e.g. "144.0.11+ge135be2+chromium-144.0.7559.97")`,
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
const cefVer = match[1]!;
|
|
969
|
+
const chromiumVer = match[2]!;
|
|
970
|
+
|
|
971
|
+
// Map platform names to Spotify CDN naming
|
|
972
|
+
const cefPlatformMap: Record<string, string> = {
|
|
973
|
+
"macos-arm64": "macosarm64",
|
|
974
|
+
"macos-x64": "macosx64",
|
|
975
|
+
"win-x64": "windows64",
|
|
976
|
+
"win-arm64": "windowsarm64",
|
|
977
|
+
"linux-x64": "linux64",
|
|
978
|
+
"linux-arm64": "linuxarm64",
|
|
979
|
+
};
|
|
980
|
+
const cefPlatform = cefPlatformMap[`${platformOS}-${platformArch}`];
|
|
981
|
+
if (!cefPlatform) {
|
|
982
|
+
throw new Error(
|
|
983
|
+
`Unsupported platform/arch for custom CEF: ${platformOS}-${platformArch}`,
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// URL-encode the + as %2B
|
|
988
|
+
const encodedCefVer = cefVer.replace(/\+/g, "%2B");
|
|
989
|
+
const cefUrl = `https://cef-builds.spotifycdn.com/cef_binary_${encodedCefVer}%2Bchromium-${chromiumVer}_${cefPlatform}_minimal.tar.bz2`;
|
|
990
|
+
|
|
991
|
+
console.log(`Using custom CEF version: ${cefVersion}`);
|
|
992
|
+
console.log(`Downloading from: ${cefUrl}`);
|
|
993
|
+
|
|
994
|
+
// Store custom CEF in .sparkbun-cache so it survives dist rebuilds and bun install
|
|
995
|
+
const cefDir = getEffectiveCEFDir(platformOS, platformArch, cefVersion);
|
|
996
|
+
console.log(`Caching custom CEF to ${cefDir}`);
|
|
997
|
+
mkdirSync(cefDir, { recursive: true });
|
|
998
|
+
|
|
999
|
+
// Download to temp file
|
|
1000
|
+
const tempFile = join(
|
|
1001
|
+
SPARKBUN_DEP_PATH,
|
|
1002
|
+
`cef-custom-${platformOS}-${platformArch}-${Date.now()}.tar.bz2`,
|
|
1003
|
+
);
|
|
1004
|
+
|
|
1005
|
+
try {
|
|
1006
|
+
console.log(`Downloading custom CEF...`);
|
|
1007
|
+
const response = await fetch(cefUrl);
|
|
1008
|
+
if (!response.ok) {
|
|
1009
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const contentLength = response.headers.get("content-length");
|
|
1013
|
+
const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
|
|
1014
|
+
const fileStream = createWriteStream(tempFile);
|
|
1015
|
+
let downloadedSize = 0;
|
|
1016
|
+
let lastReportedPercent = -1;
|
|
1017
|
+
|
|
1018
|
+
if (response.body) {
|
|
1019
|
+
const reader = response.body.getReader();
|
|
1020
|
+
while (true) {
|
|
1021
|
+
const { done, value } = await reader.read();
|
|
1022
|
+
if (done) break;
|
|
1023
|
+
|
|
1024
|
+
const chunk = Buffer.from(value);
|
|
1025
|
+
fileStream.write(chunk);
|
|
1026
|
+
downloadedSize += chunk.length;
|
|
1027
|
+
|
|
1028
|
+
if (totalSize > 0) {
|
|
1029
|
+
const percent = Math.round((downloadedSize / totalSize) * 100);
|
|
1030
|
+
const percentTier = Math.floor(percent / 10) * 10;
|
|
1031
|
+
if (percentTier > lastReportedPercent && percentTier <= 100) {
|
|
1032
|
+
console.log(
|
|
1033
|
+
` Progress: ${percentTier}% (${Math.round(downloadedSize / 1024 / 1024)}MB/${Math.round(totalSize / 1024 / 1024)}MB)`,
|
|
1034
|
+
);
|
|
1035
|
+
lastReportedPercent = percentTier;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
await new Promise((resolve, reject) => {
|
|
1042
|
+
fileStream.end((error: any) => {
|
|
1043
|
+
if (error) reject(error);
|
|
1044
|
+
else resolve(void 0);
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
console.log(
|
|
1049
|
+
`Download completed (${Math.round(downloadedSize / 1024 / 1024)}MB), extracting...`,
|
|
1050
|
+
);
|
|
1051
|
+
|
|
1052
|
+
// Extract tar.bz2 using system tar (bz2 requires it)
|
|
1053
|
+
execSync(`tar -xjf "${tempFile}" --strip-components=1 -C "${cefDir}"`, {
|
|
1054
|
+
stdio: "inherit",
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// The Spotify distribution layout has runtime files in Release/ and Resources/
|
|
1058
|
+
// subdirectories, but the CLI expects them at the cef/ root. Copy them up.
|
|
1059
|
+
console.log("Copying CEF runtime files to expected locations...");
|
|
1060
|
+
const releaseDir = join(cefDir, "Release");
|
|
1061
|
+
const resourcesDir = join(cefDir, "Resources");
|
|
1062
|
+
|
|
1063
|
+
if (platformOS === "macos") {
|
|
1064
|
+
// macOS: copy the framework from Release/ to cef/ root
|
|
1065
|
+
const fwSrc = join(releaseDir, "Chromium Embedded Framework.framework");
|
|
1066
|
+
const fwDst = join(cefDir, "Chromium Embedded Framework.framework");
|
|
1067
|
+
if (existsSync(fwSrc) && !existsSync(fwDst)) {
|
|
1068
|
+
cpSync(fwSrc, fwDst, { recursive: true, dereference: true });
|
|
1069
|
+
}
|
|
1070
|
+
} else {
|
|
1071
|
+
// Windows and Linux: copy all files from Release/ and Resources/ to cef/ root
|
|
1072
|
+
if (existsSync(releaseDir)) {
|
|
1073
|
+
for (const entry of readdirSync(releaseDir)) {
|
|
1074
|
+
const src = join(releaseDir, entry);
|
|
1075
|
+
const dst = join(cefDir, entry);
|
|
1076
|
+
if (!existsSync(dst)) {
|
|
1077
|
+
cpSync(src, dst, { recursive: true, dereference: true });
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (existsSync(resourcesDir)) {
|
|
1082
|
+
for (const entry of readdirSync(resourcesDir)) {
|
|
1083
|
+
const src = join(resourcesDir, entry);
|
|
1084
|
+
const dst = join(cefDir, entry);
|
|
1085
|
+
if (!existsSync(dst)) {
|
|
1086
|
+
cpSync(src, dst, { recursive: true, dereference: true });
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Write version stamp
|
|
1093
|
+
writeFileSync(join(cefDir, ".cef-version"), cefVersion);
|
|
1094
|
+
|
|
1095
|
+
console.log(
|
|
1096
|
+
`Custom CEF ${cefVersion} for ${platformOS}-${platformArch} set up successfully`,
|
|
1097
|
+
);
|
|
1098
|
+
console.log(
|
|
1099
|
+
`Note: process_helper ships in the core tarball and uses CEF's stable C API.`,
|
|
1100
|
+
);
|
|
1101
|
+
console.log(
|
|
1102
|
+
`C API compatibility is expected within the same major version line.`,
|
|
1103
|
+
);
|
|
1104
|
+
} catch (error: any) {
|
|
1105
|
+
// Clean up on failure
|
|
1106
|
+
if (existsSync(cefDir)) {
|
|
1107
|
+
try {
|
|
1108
|
+
rmSync(cefDir, { recursive: true, force: true });
|
|
1109
|
+
} catch {}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
console.error(
|
|
1113
|
+
`Failed to set up custom CEF ${cefVersion} for ${platformOS}-${platformArch}:`,
|
|
1114
|
+
error.message,
|
|
1115
|
+
);
|
|
1116
|
+
console.error(
|
|
1117
|
+
`\nVerify the CEF version string and that it exists at: https://cef-builds.spotifycdn.com/`,
|
|
1118
|
+
);
|
|
1119
|
+
console.error(
|
|
1120
|
+
`Note: CEF's C API is ABI-stable within the same major version. ` +
|
|
1121
|
+
`Across major versions, breaking changes are possible.`,
|
|
1122
|
+
);
|
|
1123
|
+
process.exit(1);
|
|
1124
|
+
} finally {
|
|
1125
|
+
// Clean up temp file
|
|
1126
|
+
if (existsSync(tempFile)) {
|
|
1127
|
+
try {
|
|
1128
|
+
unlinkSync(tempFile);
|
|
1129
|
+
} catch {}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// @ts-expect-error - reserved for future use
|
|
1135
|
+
const _commandDefaults = {
|
|
1136
|
+
init: {
|
|
1137
|
+
projectRoot,
|
|
1138
|
+
config: "sparkbun.config",
|
|
1139
|
+
},
|
|
1140
|
+
build: {
|
|
1141
|
+
projectRoot,
|
|
1142
|
+
config: "sparkbun.config",
|
|
1143
|
+
},
|
|
1144
|
+
dev: {
|
|
1145
|
+
projectRoot,
|
|
1146
|
+
config: "sparkbun.config",
|
|
1147
|
+
},
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
type FileAssociation = {
|
|
1151
|
+
ext: string[];
|
|
1152
|
+
name: string;
|
|
1153
|
+
role?: "Editor" | "Viewer" | "Shell" | "None";
|
|
1154
|
+
icon?: string;
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
// Default values merged with user's sparkbun.config.ts
|
|
1158
|
+
// For the user-facing type, see SparkBunConfig in src/bun/SparkBunConfig.ts
|
|
1159
|
+
const defaultConfig = {
|
|
1160
|
+
app: {
|
|
1161
|
+
name: "MyApp",
|
|
1162
|
+
identifier: "com.example.myapp",
|
|
1163
|
+
version: "0.1.0",
|
|
1164
|
+
description: "" as string | undefined,
|
|
1165
|
+
urlSchemes: undefined as string[] | undefined,
|
|
1166
|
+
fileAssociations: undefined as FileAssociation[] | undefined,
|
|
1167
|
+
},
|
|
1168
|
+
build: {
|
|
1169
|
+
buildFolder: "build",
|
|
1170
|
+
artifactFolder: "artifacts",
|
|
1171
|
+
mainProcess: "bun" as const,
|
|
1172
|
+
cefVersion: undefined as string | undefined, // Override CEF version: "CEF_VERSION+chromium-CHROMIUM_VERSION"
|
|
1173
|
+
wgpuVersion: undefined as string | undefined, // Override Dawn (WebGPU) version: "0.2.3" or "v0.2.3-beta.0"
|
|
1174
|
+
bunVersion: undefined as string | undefined, // Override Bun runtime version: "1.4.2"
|
|
1175
|
+
locales: undefined as string[] | "*" | undefined, // ICU locales subset (Linux/Windows)
|
|
1176
|
+
mac: {
|
|
1177
|
+
codesign: false,
|
|
1178
|
+
createDmg: true,
|
|
1179
|
+
notarize: false,
|
|
1180
|
+
bundleCEF: false,
|
|
1181
|
+
bundleWGPU: false,
|
|
1182
|
+
entitlements: {
|
|
1183
|
+
// This entitlement is required for SparkBun apps with a hardened runtime (required for notarization) to run on macos
|
|
1184
|
+
"com.apple.security.cs.allow-jit": true,
|
|
1185
|
+
// Required for bun runtime to work with dynamic code execution and JIT compilation when signed
|
|
1186
|
+
"com.apple.security.cs.allow-unsigned-executable-memory": true,
|
|
1187
|
+
"com.apple.security.cs.disable-library-validation": true,
|
|
1188
|
+
} as Record<string, boolean | string>,
|
|
1189
|
+
icons: "icon.iconset",
|
|
1190
|
+
defaultRenderer: undefined as "native" | "cef" | undefined,
|
|
1191
|
+
chromiumFlags: undefined as Record<string, string | boolean> | undefined,
|
|
1192
|
+
},
|
|
1193
|
+
win: {
|
|
1194
|
+
bundleCEF: false,
|
|
1195
|
+
bundleWGPU: false,
|
|
1196
|
+
icon: undefined as string | undefined,
|
|
1197
|
+
defaultRenderer: undefined as "native" | "cef" | undefined,
|
|
1198
|
+
chromiumFlags: undefined as Record<string, string | boolean> | undefined,
|
|
1199
|
+
},
|
|
1200
|
+
linux: {
|
|
1201
|
+
bundleCEF: false,
|
|
1202
|
+
bundleWGPU: false,
|
|
1203
|
+
icon: undefined as string | undefined,
|
|
1204
|
+
defaultRenderer: undefined as "native" | "cef" | undefined,
|
|
1205
|
+
chromiumFlags: undefined as Record<string, string | boolean> | undefined,
|
|
1206
|
+
},
|
|
1207
|
+
bun: {
|
|
1208
|
+
entrypoint: "src/bun/index.ts",
|
|
1209
|
+
},
|
|
1210
|
+
views: undefined as
|
|
1211
|
+
| Record<string, { entrypoint: string; [key: string]: unknown }>
|
|
1212
|
+
| undefined,
|
|
1213
|
+
copy: undefined as Record<string, string> | undefined,
|
|
1214
|
+
watch: undefined as string[] | undefined,
|
|
1215
|
+
watchIgnore: undefined as string[] | undefined,
|
|
1216
|
+
},
|
|
1217
|
+
runtime: {} as Record<string, unknown>,
|
|
1218
|
+
scripts: {
|
|
1219
|
+
preBuild: "",
|
|
1220
|
+
postBuild: "",
|
|
1221
|
+
postWrap: "",
|
|
1222
|
+
postPackage: "",
|
|
1223
|
+
},
|
|
1224
|
+
release: {
|
|
1225
|
+
baseUrl: "",
|
|
1226
|
+
generatePatch: true,
|
|
1227
|
+
},
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
// Mapping of entitlements to their corresponding Info.plist usage description keys
|
|
1231
|
+
const ENTITLEMENT_TO_PLIST_KEY: Record<string, string> = {
|
|
1232
|
+
"com.apple.security.device.camera": "NSCameraUsageDescription",
|
|
1233
|
+
"com.apple.security.device.microphone": "NSMicrophoneUsageDescription",
|
|
1234
|
+
"com.apple.security.device.audio-input": "NSMicrophoneUsageDescription",
|
|
1235
|
+
"com.apple.security.personal-information.location":
|
|
1236
|
+
"NSLocationUsageDescription",
|
|
1237
|
+
"com.apple.security.personal-information.location-when-in-use":
|
|
1238
|
+
"NSLocationWhenInUseUsageDescription",
|
|
1239
|
+
"com.apple.security.personal-information.contacts":
|
|
1240
|
+
"NSContactsUsageDescription",
|
|
1241
|
+
"com.apple.security.personal-information.calendars":
|
|
1242
|
+
"NSCalendarsUsageDescription",
|
|
1243
|
+
"com.apple.security.personal-information.reminders":
|
|
1244
|
+
"NSRemindersUsageDescription",
|
|
1245
|
+
"com.apple.security.personal-information.photos-library":
|
|
1246
|
+
"NSPhotoLibraryUsageDescription",
|
|
1247
|
+
"com.apple.security.personal-information.apple-music-library":
|
|
1248
|
+
"NSAppleMusicUsageDescription",
|
|
1249
|
+
"com.apple.security.personal-information.motion": "NSMotionUsageDescription",
|
|
1250
|
+
"com.apple.security.personal-information.speech-recognition":
|
|
1251
|
+
"NSSpeechRecognitionUsageDescription",
|
|
1252
|
+
"com.apple.security.device.bluetooth": "NSBluetoothAlwaysUsageDescription",
|
|
1253
|
+
"com.apple.security.files.user-selected.read-write":
|
|
1254
|
+
"NSDocumentsFolderUsageDescription",
|
|
1255
|
+
"com.apple.security.files.downloads.read-write":
|
|
1256
|
+
"NSDownloadsFolderUsageDescription",
|
|
1257
|
+
"com.apple.security.files.desktop.read-write":
|
|
1258
|
+
"NSDesktopFolderUsageDescription",
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
// Helper function to escape XML special characters
|
|
1262
|
+
function escapeXml(str: string): string {
|
|
1263
|
+
return str
|
|
1264
|
+
.replace(/&/g, "&")
|
|
1265
|
+
.replace(/</g, "<")
|
|
1266
|
+
.replace(/>/g, ">")
|
|
1267
|
+
.replace(/"/g, """)
|
|
1268
|
+
.replace(/'/g, "'");
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function patchPeSubsystem(exePath: string): void {
|
|
1272
|
+
const buf = Buffer.from(readFileSync(exePath));
|
|
1273
|
+
const peOffset = buf.readUInt32LE(0x3c);
|
|
1274
|
+
const subsystemOffset = peOffset + 0x5c;
|
|
1275
|
+
const current = buf.readUInt16LE(subsystemOffset);
|
|
1276
|
+
if (current !== 2) {
|
|
1277
|
+
buf.writeUInt16LE(2, subsystemOffset);
|
|
1278
|
+
writeFileSync(exePath, buf);
|
|
1279
|
+
console.log(`Patched PE subsystem: ${current} -> 2 (WINDOWS)`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Helper functions
|
|
1284
|
+
function escapePathForTerminal(path: string): string {
|
|
1285
|
+
if (OS === "win") {
|
|
1286
|
+
return `"${path.replace(/"/g, '""')}"`;
|
|
1287
|
+
} else {
|
|
1288
|
+
return `'${path.replace(/'/g, "'\\''")}'`;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Creates a Linux installer tar.gz containing:
|
|
1294
|
+
* - Self-extracting installer executable (with embedded app archive)
|
|
1295
|
+
* - README.txt with instructions
|
|
1296
|
+
*
|
|
1297
|
+
* This replaces the AppImage-based installer to avoid libfuse2 dependency.
|
|
1298
|
+
* The installer executable has the compressed app archive embedded within it
|
|
1299
|
+
* using magic markers, similar to how Windows installers work.
|
|
1300
|
+
*/
|
|
1301
|
+
// Helper function to generate usage description entries for Info.plist
|
|
1302
|
+
function generateUsageDescriptions(
|
|
1303
|
+
entitlements: Record<string, boolean | string | string[]>,
|
|
1304
|
+
): string {
|
|
1305
|
+
const usageEntries: string[] = [];
|
|
1306
|
+
|
|
1307
|
+
for (const [entitlement, value] of Object.entries(entitlements)) {
|
|
1308
|
+
const plistKey = ENTITLEMENT_TO_PLIST_KEY[entitlement];
|
|
1309
|
+
if (plistKey && value) {
|
|
1310
|
+
// Use the string value as description, or a default if it's just true
|
|
1311
|
+
const description =
|
|
1312
|
+
typeof value === "string"
|
|
1313
|
+
? escapeXml(value)
|
|
1314
|
+
: `This app requires access for ${entitlement.split(".").pop()?.replace("-", " ")}`;
|
|
1315
|
+
|
|
1316
|
+
usageEntries.push(
|
|
1317
|
+
` <key>${plistKey}</key>\n <string>${description}</string>`,
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
return usageEntries.join("\n");
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// Helper function to generate CFBundleURLTypes for custom URL schemes
|
|
1326
|
+
function generateURLTypes(
|
|
1327
|
+
urlSchemes: string[] | undefined,
|
|
1328
|
+
identifier: string,
|
|
1329
|
+
): string {
|
|
1330
|
+
if (!urlSchemes || urlSchemes.length === 0) {
|
|
1331
|
+
return "";
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const schemesXml = urlSchemes
|
|
1335
|
+
.map((scheme) => ` <string>${escapeXml(scheme)}</string>`)
|
|
1336
|
+
.join("\n");
|
|
1337
|
+
|
|
1338
|
+
return ` <key>CFBundleURLTypes</key>
|
|
1339
|
+
<array>
|
|
1340
|
+
<dict>
|
|
1341
|
+
<key>CFBundleURLName</key>
|
|
1342
|
+
<string>${escapeXml(identifier)}</string>
|
|
1343
|
+
<key>CFBundleTypeRole</key>
|
|
1344
|
+
<string>Viewer</string>
|
|
1345
|
+
<key>CFBundleURLSchemes</key>
|
|
1346
|
+
<array>
|
|
1347
|
+
${schemesXml}
|
|
1348
|
+
</array>
|
|
1349
|
+
</dict>
|
|
1350
|
+
</array>`;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Generates CFBundleDocumentTypes and UTExportedTypeDeclarations for file associations.
|
|
1354
|
+
// Each association gets a UTI derived from the app identifier (e.g., com.example.app.myext).
|
|
1355
|
+
// LSItemContentTypes in CFBundleDocumentTypes references these UTIs so Launch Services
|
|
1356
|
+
// properly associates files with the app on modern macOS.
|
|
1357
|
+
function generateDocumentTypes(
|
|
1358
|
+
fileAssociations: FileAssociation[] | undefined,
|
|
1359
|
+
projectRoot: string,
|
|
1360
|
+
appIdentifier: string,
|
|
1361
|
+
): string {
|
|
1362
|
+
if (!fileAssociations || fileAssociations.length === 0) {
|
|
1363
|
+
return "";
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const validAssociations = fileAssociations.filter((assoc) => {
|
|
1367
|
+
if (!assoc.ext || assoc.ext.length === 0) {
|
|
1368
|
+
console.log(
|
|
1369
|
+
`WARNING: fileAssociations entry "${assoc.name || "(unnamed)"}" has no extensions — skipping`,
|
|
1370
|
+
);
|
|
1371
|
+
return false;
|
|
1372
|
+
}
|
|
1373
|
+
if (!assoc.name) {
|
|
1374
|
+
console.log(
|
|
1375
|
+
`WARNING: fileAssociations entry with extensions [${assoc.ext.join(", ")}] has no name — skipping`,
|
|
1376
|
+
);
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
return true;
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
if (validAssociations.length === 0) {
|
|
1383
|
+
return "";
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Clean extensions and warn about leading dots
|
|
1387
|
+
const cleaned = validAssociations.map((assoc) => ({
|
|
1388
|
+
...assoc,
|
|
1389
|
+
ext: assoc.ext.map((ext) => {
|
|
1390
|
+
const clean = ext.replace(/^\./, "");
|
|
1391
|
+
if (clean !== ext) {
|
|
1392
|
+
console.log(
|
|
1393
|
+
`WARNING: fileAssociations ext "${ext}" has a leading dot — stripping to "${clean}"`,
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
return clean;
|
|
1397
|
+
}),
|
|
1398
|
+
}));
|
|
1399
|
+
|
|
1400
|
+
// Generate CFBundleDocumentTypes with LSItemContentTypes
|
|
1401
|
+
const docTypes = cleaned
|
|
1402
|
+
.map((assoc) => {
|
|
1403
|
+
const role = assoc.role || "Viewer";
|
|
1404
|
+
// Resolve icon: only reference if file exists to avoid dangling plist entries
|
|
1405
|
+
let iconName = "";
|
|
1406
|
+
if (assoc.icon) {
|
|
1407
|
+
const iconSourcePath = join(projectRoot, assoc.icon);
|
|
1408
|
+
if (existsSync(iconSourcePath)) {
|
|
1409
|
+
iconName = basename(assoc.icon).replace(/\.icns$/i, "");
|
|
1410
|
+
} else {
|
|
1411
|
+
console.log(
|
|
1412
|
+
`WARNING: Document type icon not found: ${iconSourcePath} — skipping icon reference`,
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
const iconLine = iconName
|
|
1417
|
+
? ` <key>CFBundleTypeIconFile</key>\n <string>${escapeXml(iconName)}</string>\n`
|
|
1418
|
+
: "";
|
|
1419
|
+
// One UTI per extension, all listed under LSItemContentTypes
|
|
1420
|
+
const utiXml = assoc.ext
|
|
1421
|
+
.map(
|
|
1422
|
+
(ext) =>
|
|
1423
|
+
` <string>${escapeXml(appIdentifier)}.${escapeXml(ext)}</string>`,
|
|
1424
|
+
)
|
|
1425
|
+
.join("\n");
|
|
1426
|
+
const extsXml = assoc.ext
|
|
1427
|
+
.map(
|
|
1428
|
+
(ext) =>
|
|
1429
|
+
` <string>${escapeXml(ext)}</string>`,
|
|
1430
|
+
)
|
|
1431
|
+
.join("\n");
|
|
1432
|
+
|
|
1433
|
+
return ` <dict>
|
|
1434
|
+
<key>CFBundleTypeName</key>
|
|
1435
|
+
<string>${escapeXml(assoc.name)}</string>
|
|
1436
|
+
<key>CFBundleTypeRole</key>
|
|
1437
|
+
<string>${escapeXml(role)}</string>
|
|
1438
|
+
${iconLine} <key>LSItemContentTypes</key>
|
|
1439
|
+
<array>
|
|
1440
|
+
${utiXml}
|
|
1441
|
+
</array>
|
|
1442
|
+
<key>CFBundleTypeExtensions</key>
|
|
1443
|
+
<array>
|
|
1444
|
+
${extsXml}
|
|
1445
|
+
</array>
|
|
1446
|
+
</dict>`;
|
|
1447
|
+
})
|
|
1448
|
+
.join("\n");
|
|
1449
|
+
|
|
1450
|
+
// Generate UTExportedTypeDeclarations — one per extension
|
|
1451
|
+
const utiDecls = cleaned
|
|
1452
|
+
.flatMap((assoc) => {
|
|
1453
|
+
let iconName = "";
|
|
1454
|
+
if (assoc.icon) {
|
|
1455
|
+
const iconSourcePath = join(projectRoot, assoc.icon);
|
|
1456
|
+
if (existsSync(iconSourcePath)) {
|
|
1457
|
+
iconName = basename(assoc.icon).replace(/\.icns$/i, "");
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
const iconLine = iconName
|
|
1461
|
+
? ` <key>UTTypeIconFiles</key>
|
|
1462
|
+
<array>
|
|
1463
|
+
<string>${escapeXml(iconName)}</string>
|
|
1464
|
+
</array>\n`
|
|
1465
|
+
: "";
|
|
1466
|
+
return assoc.ext.map(
|
|
1467
|
+
(ext) => ` <dict>
|
|
1468
|
+
<key>UTTypeIdentifier</key>
|
|
1469
|
+
<string>${escapeXml(appIdentifier)}.${escapeXml(ext)}</string>
|
|
1470
|
+
<key>UTTypeDescription</key>
|
|
1471
|
+
<string>${escapeXml(assoc.name)}</string>
|
|
1472
|
+
<key>UTTypeConformsTo</key>
|
|
1473
|
+
<array>
|
|
1474
|
+
<string>public.data</string>
|
|
1475
|
+
</array>
|
|
1476
|
+
${iconLine} <key>UTTypeTagSpecification</key>
|
|
1477
|
+
<dict>
|
|
1478
|
+
<key>public.filename-extension</key>
|
|
1479
|
+
<array>
|
|
1480
|
+
<string>${escapeXml(ext)}</string>
|
|
1481
|
+
</array>
|
|
1482
|
+
</dict>
|
|
1483
|
+
</dict>`,
|
|
1484
|
+
);
|
|
1485
|
+
})
|
|
1486
|
+
.join("\n");
|
|
1487
|
+
|
|
1488
|
+
return ` <key>CFBundleDocumentTypes</key>
|
|
1489
|
+
<array>
|
|
1490
|
+
${docTypes}
|
|
1491
|
+
</array>
|
|
1492
|
+
<key>UTExportedTypeDeclarations</key>
|
|
1493
|
+
<array>
|
|
1494
|
+
${utiDecls}
|
|
1495
|
+
</array>`;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Execute command handling
|
|
1499
|
+
(async () => {
|
|
1500
|
+
if (commandArg === "init") {
|
|
1501
|
+
await (async () => {
|
|
1502
|
+
const secondArg = process.argv[indexOfCli + 2];
|
|
1503
|
+
const availableTemplates = getTemplateNames();
|
|
1504
|
+
|
|
1505
|
+
let projectName: string;
|
|
1506
|
+
let templateName: string;
|
|
1507
|
+
|
|
1508
|
+
// Check if --template= flag is used
|
|
1509
|
+
const templateFlag = process.argv.find((arg) =>
|
|
1510
|
+
arg.startsWith("--template="),
|
|
1511
|
+
);
|
|
1512
|
+
if (templateFlag) {
|
|
1513
|
+
// Traditional usage: sparkbun init my-project --template=photo-booth
|
|
1514
|
+
projectName = secondArg || "my-sparkbun-app";
|
|
1515
|
+
templateName = templateFlag.split("=")[1]!;
|
|
1516
|
+
} else if (secondArg && availableTemplates.includes(secondArg)) {
|
|
1517
|
+
// New intuitive usage: sparkbun init photo-booth
|
|
1518
|
+
projectName = secondArg; // Use template name as project name
|
|
1519
|
+
templateName = secondArg;
|
|
1520
|
+
} else {
|
|
1521
|
+
// Interactive menu when no template specified
|
|
1522
|
+
console.log("🚀 Welcome to SparkBun!");
|
|
1523
|
+
console.log("");
|
|
1524
|
+
console.log("Available templates:");
|
|
1525
|
+
availableTemplates.forEach((template, index) => {
|
|
1526
|
+
console.log(` ${index + 1}. ${template}`);
|
|
1527
|
+
});
|
|
1528
|
+
console.log("");
|
|
1529
|
+
|
|
1530
|
+
// Simple CLI selection using readline
|
|
1531
|
+
const rl = readline.createInterface({
|
|
1532
|
+
input: process.stdin,
|
|
1533
|
+
output: process.stdout,
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
const choice = await new Promise<string>((resolve) => {
|
|
1537
|
+
rl.question("Select a template (enter number): ", (answer) => {
|
|
1538
|
+
rl.close();
|
|
1539
|
+
resolve(answer.trim());
|
|
1540
|
+
});
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
const templateIndex = parseInt(choice) - 1;
|
|
1544
|
+
if (templateIndex < 0 || templateIndex >= availableTemplates.length) {
|
|
1545
|
+
console.error(
|
|
1546
|
+
`❌ Invalid selection. Please enter a number between 1 and ${availableTemplates.length}.`,
|
|
1547
|
+
);
|
|
1548
|
+
process.exit(1);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
templateName = availableTemplates[templateIndex]!;
|
|
1552
|
+
|
|
1553
|
+
// Ask for project name
|
|
1554
|
+
const rl2 = readline.createInterface({
|
|
1555
|
+
input: process.stdin,
|
|
1556
|
+
output: process.stdout,
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
projectName = await new Promise<string>((resolve) => {
|
|
1560
|
+
rl2.question(
|
|
1561
|
+
`Enter project name (default: my-${templateName}-app): `,
|
|
1562
|
+
(answer) => {
|
|
1563
|
+
rl2.close();
|
|
1564
|
+
resolve(answer.trim() || `my-${templateName}-app`);
|
|
1565
|
+
},
|
|
1566
|
+
);
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
console.log(`🚀 Initializing SparkBun project: ${projectName}`);
|
|
1571
|
+
console.log(`📋 Using template: ${templateName}`);
|
|
1572
|
+
|
|
1573
|
+
// Validate template name
|
|
1574
|
+
if (!availableTemplates.includes(templateName)) {
|
|
1575
|
+
console.error(`❌ Template "${templateName}" not found.`);
|
|
1576
|
+
console.log(`Available templates: ${availableTemplates.join(", ")}`);
|
|
1577
|
+
process.exit(1);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
const template = getTemplate(templateName);
|
|
1581
|
+
if (!template) {
|
|
1582
|
+
console.error(`❌ Could not load template "${templateName}"`);
|
|
1583
|
+
process.exit(1);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Create project directory
|
|
1587
|
+
const projectPath = join(process.cwd(), projectName);
|
|
1588
|
+
if (existsSync(projectPath)) {
|
|
1589
|
+
console.error(`❌ Directory "${projectName}" already exists.`);
|
|
1590
|
+
process.exit(1);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
mkdirSync(projectPath, { recursive: true });
|
|
1594
|
+
|
|
1595
|
+
// Extract template files
|
|
1596
|
+
let fileCount = 0;
|
|
1597
|
+
for (const [relativePath, content] of Object.entries(template.files)) {
|
|
1598
|
+
const fullPath = join(projectPath, relativePath);
|
|
1599
|
+
const dir = dirname(fullPath);
|
|
1600
|
+
|
|
1601
|
+
// Create directory if it doesn't exist
|
|
1602
|
+
mkdirSync(dir, { recursive: true });
|
|
1603
|
+
|
|
1604
|
+
// Write file (binary files are base64-encoded with a "base64:" prefix)
|
|
1605
|
+
if (content.startsWith("base64:")) {
|
|
1606
|
+
writeFileSync(fullPath, new Uint8Array(Buffer.from(content.slice(7), "base64")));
|
|
1607
|
+
} else {
|
|
1608
|
+
writeFileSync(fullPath, content, "utf-8");
|
|
1609
|
+
}
|
|
1610
|
+
fileCount++;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
console.log(
|
|
1614
|
+
`✅ Created ${fileCount} files from "${templateName}" template`,
|
|
1615
|
+
);
|
|
1616
|
+
console.log(`📁 Project created at: ${projectPath}`);
|
|
1617
|
+
console.log("");
|
|
1618
|
+
console.log("📦 Next steps:");
|
|
1619
|
+
console.log(` cd ${projectName}`);
|
|
1620
|
+
console.log(" bun install");
|
|
1621
|
+
console.log(" bun start");
|
|
1622
|
+
console.log("");
|
|
1623
|
+
console.log("🎉 Happy building with SparkBun!");
|
|
1624
|
+
console.log("");
|
|
1625
|
+
console.log(
|
|
1626
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
1627
|
+
);
|
|
1628
|
+
console.log("NOTE FOR AI ASSISTANTS: SparkBun is NOT Electron.");
|
|
1629
|
+
console.log(
|
|
1630
|
+
"Different architecture, different APIs. Do not use Electron patterns.",
|
|
1631
|
+
);
|
|
1632
|
+
console.log("SparkBun — Bun-native desktop app framework");
|
|
1633
|
+
console.log(
|
|
1634
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
1635
|
+
);
|
|
1636
|
+
})();
|
|
1637
|
+
} else if (commandArg === "build") {
|
|
1638
|
+
// Get config
|
|
1639
|
+
const config = await getConfig();
|
|
1640
|
+
|
|
1641
|
+
// Get environment
|
|
1642
|
+
const envArg =
|
|
1643
|
+
process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
|
|
1644
|
+
const buildEnvironment: "dev" | "canary" | "stable" = ["dev", "canary", "stable"].includes(
|
|
1645
|
+
envArg,
|
|
1646
|
+
)
|
|
1647
|
+
? (envArg as "dev" | "canary" | "stable")
|
|
1648
|
+
: "dev";
|
|
1649
|
+
|
|
1650
|
+
try {
|
|
1651
|
+
await runBuild(config, buildEnvironment);
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
console.error("Build failed:", error);
|
|
1654
|
+
process.exit(1);
|
|
1655
|
+
}
|
|
1656
|
+
} else if (commandArg === "run") {
|
|
1657
|
+
const config = await getConfig();
|
|
1658
|
+
await runAppWithSignalHandling(config);
|
|
1659
|
+
} else if (commandArg === "dev") {
|
|
1660
|
+
const config = await getConfig();
|
|
1661
|
+
const watchMode = process.argv.includes("--watch");
|
|
1662
|
+
|
|
1663
|
+
if (watchMode) {
|
|
1664
|
+
await runDevWatch(config);
|
|
1665
|
+
} else {
|
|
1666
|
+
try {
|
|
1667
|
+
await runBuild(config, "dev");
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
console.error("Build failed:", error);
|
|
1670
|
+
process.exit(1);
|
|
1671
|
+
}
|
|
1672
|
+
await runAppWithSignalHandling(config);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
async function runBuild(
|
|
1677
|
+
config: Awaited<ReturnType<typeof getConfig>>,
|
|
1678
|
+
buildEnvironment: "dev" | "canary" | "stable",
|
|
1679
|
+
) {
|
|
1680
|
+
// Determine target platform — allow override via --target=linux-arm64 etc.
|
|
1681
|
+
const targetArg = process.argv.find((arg) => arg.startsWith("--target="))?.split("=")[1];
|
|
1682
|
+
let currentTarget: { os: "macos" | "win" | "linux"; arch: "arm64" | "x64" } = { os: OS, arch: ARCH };
|
|
1683
|
+
if (targetArg) {
|
|
1684
|
+
const [tOS, tArch] = targetArg.split("-") as [string, string];
|
|
1685
|
+
const osMap: Record<string, "macos" | "win" | "linux"> = { macos: "macos", darwin: "macos", win: "win", windows: "win", linux: "linux" };
|
|
1686
|
+
const archMap: Record<string, "arm64" | "x64"> = { arm64: "arm64", aarch64: "arm64", x64: "x64", x86_64: "x64" };
|
|
1687
|
+
if (osMap[tOS] && archMap[tArch]) {
|
|
1688
|
+
currentTarget = { os: osMap[tOS], arch: archMap[tArch] };
|
|
1689
|
+
console.log(`Cross-compiling for ${currentTarget.os}-${currentTarget.arch}`);
|
|
1690
|
+
} else {
|
|
1691
|
+
console.error(`Invalid target: ${targetArg}. Use format: linux-arm64, win-x64, macos-arm64`);
|
|
1692
|
+
process.exit(1);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// Set up build variables
|
|
1697
|
+
const targetOS = currentTarget.os;
|
|
1698
|
+
const targetARCH = currentTarget.arch;
|
|
1699
|
+
const targetBinExt = targetOS === "win" ? ".exe" : "";
|
|
1700
|
+
const appFileName = getAppFileName(config.app.name, buildEnvironment);
|
|
1701
|
+
// macOS bundle display name preserves spaces for the actual .app folder
|
|
1702
|
+
const macOSBundleDisplayName = getMacOSBundleDisplayName(
|
|
1703
|
+
config.app.name,
|
|
1704
|
+
buildEnvironment,
|
|
1705
|
+
);
|
|
1706
|
+
const platformPrefix = getPlatformPrefix(
|
|
1707
|
+
buildEnvironment,
|
|
1708
|
+
currentTarget.os,
|
|
1709
|
+
currentTarget.arch,
|
|
1710
|
+
);
|
|
1711
|
+
const buildFolder = join(
|
|
1712
|
+
projectRoot,
|
|
1713
|
+
config.build.buildFolder,
|
|
1714
|
+
platformPrefix,
|
|
1715
|
+
);
|
|
1716
|
+
// @ts-expect-error - reserved for future use
|
|
1717
|
+
const _bundleFileName = getBundleFileName(
|
|
1718
|
+
config.app.name,
|
|
1719
|
+
buildEnvironment,
|
|
1720
|
+
targetOS,
|
|
1721
|
+
);
|
|
1722
|
+
const artifactFolder = join(projectRoot, config.build.artifactFolder);
|
|
1723
|
+
|
|
1724
|
+
// Ensure core binaries are available for the target platform before starting build
|
|
1725
|
+
await ensureCoreDependencies(currentTarget.os, currentTarget.arch);
|
|
1726
|
+
|
|
1727
|
+
// Get platform-specific paths for the current target
|
|
1728
|
+
const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);
|
|
1729
|
+
|
|
1730
|
+
// Helper to run lifecycle hook scripts
|
|
1731
|
+
const runHook = (
|
|
1732
|
+
hookName: keyof typeof config.scripts,
|
|
1733
|
+
extraEnv: Record<string, string> = {},
|
|
1734
|
+
) => {
|
|
1735
|
+
const hookScript = config.scripts[hookName];
|
|
1736
|
+
if (!hookScript) return;
|
|
1737
|
+
|
|
1738
|
+
console.log(`Running ${String(hookName)} script:`, hookScript);
|
|
1739
|
+
// Use host platform's bun binary for running scripts, not target platform's
|
|
1740
|
+
const hostPaths = getPlatformPaths(OS, ARCH);
|
|
1741
|
+
|
|
1742
|
+
const result = Bun.spawnSync([hostPaths.BUN_BINARY, hookScript], {
|
|
1743
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
1744
|
+
cwd: projectRoot,
|
|
1745
|
+
env: {
|
|
1746
|
+
...process.env,
|
|
1747
|
+
SPARKBUN_BUILD_ENV: buildEnvironment,
|
|
1748
|
+
SPARKBUN_OS: targetOS,
|
|
1749
|
+
SPARKBUN_ARCH: targetARCH,
|
|
1750
|
+
SPARKBUN_BUILD_DIR: buildFolder,
|
|
1751
|
+
SPARKBUN_APP_NAME: appFileName,
|
|
1752
|
+
SPARKBUN_APP_VERSION: config.app.version,
|
|
1753
|
+
SPARKBUN_APP_IDENTIFIER: config.app.identifier,
|
|
1754
|
+
SPARKBUN_ARTIFACT_DIR: artifactFolder,
|
|
1755
|
+
...extraEnv,
|
|
1756
|
+
},
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
if (result.exitCode !== 0) {
|
|
1760
|
+
console.error(
|
|
1761
|
+
`${String(hookName)} script failed with exit code:`,
|
|
1762
|
+
result.exitCode,
|
|
1763
|
+
);
|
|
1764
|
+
if (result.stderr) {
|
|
1765
|
+
console.error(
|
|
1766
|
+
"stderr:",
|
|
1767
|
+
new TextDecoder().decode(result.stderr as Uint8Array),
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);
|
|
1771
|
+
console.error("Script path:", hookScript);
|
|
1772
|
+
console.error("Working directory:", projectRoot);
|
|
1773
|
+
throw new Error("Build failed: hook script failed");
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
const buildIcons = (
|
|
1778
|
+
appBundleFolderResourcesPath: string,
|
|
1779
|
+
appBundleFolderPath: string,
|
|
1780
|
+
) => {
|
|
1781
|
+
// Platform-specific icon handling
|
|
1782
|
+
if (targetOS === "macos" && config.build.mac?.icons) {
|
|
1783
|
+
// macOS uses .iconset folders that get converted to .icns using iconutil
|
|
1784
|
+
// This only works when building on macOS since iconutil is a macOS-only tool
|
|
1785
|
+
const iconSourceFolder = join(projectRoot, config.build.mac.icons);
|
|
1786
|
+
const iconDestPath = join(appBundleFolderResourcesPath, "AppIcon.icns");
|
|
1787
|
+
if (existsSync(iconSourceFolder)) {
|
|
1788
|
+
if (OS === "macos") {
|
|
1789
|
+
if (config.build.mac.icons.endsWith(".icon")) {
|
|
1790
|
+
// .icon format (Icon Composer) — compile with actool
|
|
1791
|
+
// Produces Assets.car (Liquid Glass on macOS 26+) and .icns fallback
|
|
1792
|
+
const actoolCheck = Bun.spawnSync(
|
|
1793
|
+
["xcrun", "--find", "actool"],
|
|
1794
|
+
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
1795
|
+
);
|
|
1796
|
+
if (actoolCheck.exitCode !== 0) {
|
|
1797
|
+
throw new Error(
|
|
1798
|
+
"Building .icon files requires Xcode (actool is not available from Command Line Tools alone). " +
|
|
1799
|
+
"Install Xcode from the App Store, or set mac.icons to an .iconset folder instead.",
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
const iconStem = basename(config.build.mac.icons, ".icon");
|
|
1804
|
+
const partialPlistPath = join(
|
|
1805
|
+
buildFolder,
|
|
1806
|
+
".actool-partial-info.plist",
|
|
1807
|
+
);
|
|
1808
|
+
|
|
1809
|
+
console.log(
|
|
1810
|
+
"Compiling .icon file with actool (requires Xcode)...",
|
|
1811
|
+
);
|
|
1812
|
+
const result = Bun.spawnSync(
|
|
1813
|
+
[
|
|
1814
|
+
"xcrun",
|
|
1815
|
+
"actool",
|
|
1816
|
+
"--compile",
|
|
1817
|
+
appBundleFolderResourcesPath,
|
|
1818
|
+
"--app-icon",
|
|
1819
|
+
iconStem,
|
|
1820
|
+
"--platform",
|
|
1821
|
+
"macosx",
|
|
1822
|
+
"--minimum-deployment-target",
|
|
1823
|
+
"11.0",
|
|
1824
|
+
"--output-partial-info-plist",
|
|
1825
|
+
partialPlistPath,
|
|
1826
|
+
iconSourceFolder,
|
|
1827
|
+
],
|
|
1828
|
+
{
|
|
1829
|
+
cwd: projectRoot,
|
|
1830
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
1831
|
+
env: {
|
|
1832
|
+
...process.env,
|
|
1833
|
+
SPARKBUN_BUILD_ENV: buildEnvironment,
|
|
1834
|
+
},
|
|
1835
|
+
},
|
|
1836
|
+
);
|
|
1837
|
+
|
|
1838
|
+
if (result.exitCode !== 0) {
|
|
1839
|
+
throw new Error(
|
|
1840
|
+
`actool failed to compile ${config.build.mac.icons} (exit code ${result.exitCode})`,
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// actool produces <stem>.icns — rename to AppIcon.icns so
|
|
1845
|
+
// CFBundleIconFile ("AppIcon") resolves correctly
|
|
1846
|
+
const actoolIcns = join(
|
|
1847
|
+
appBundleFolderResourcesPath,
|
|
1848
|
+
`${iconStem}.icns`,
|
|
1849
|
+
);
|
|
1850
|
+
if (existsSync(actoolIcns) && actoolIcns !== iconDestPath) {
|
|
1851
|
+
renameSync(actoolIcns, iconDestPath);
|
|
1852
|
+
}
|
|
1853
|
+
} else {
|
|
1854
|
+
// Use iconutil to convert .iconset folder to .icns
|
|
1855
|
+
const result = Bun.spawnSync(
|
|
1856
|
+
[
|
|
1857
|
+
"iconutil",
|
|
1858
|
+
"-c",
|
|
1859
|
+
"icns",
|
|
1860
|
+
"-o",
|
|
1861
|
+
iconDestPath,
|
|
1862
|
+
iconSourceFolder,
|
|
1863
|
+
],
|
|
1864
|
+
{
|
|
1865
|
+
cwd: appBundleFolderResourcesPath,
|
|
1866
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
1867
|
+
env: {
|
|
1868
|
+
...process.env,
|
|
1869
|
+
SPARKBUN_BUILD_ENV: buildEnvironment,
|
|
1870
|
+
},
|
|
1871
|
+
},
|
|
1872
|
+
);
|
|
1873
|
+
|
|
1874
|
+
if (result.exitCode !== 0) {
|
|
1875
|
+
throw new Error(
|
|
1876
|
+
`iconutil failed to convert ${config.build.mac.icons} (exit code ${result.exitCode})`,
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
} else {
|
|
1881
|
+
console.log(
|
|
1882
|
+
`WARNING: Cannot build macOS icons on ${OS} - iconutil is only available on macOS`,
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
} else if (targetOS === "linux" && config.build.linux?.icon) {
|
|
1887
|
+
const iconSourcePath = join(projectRoot, config.build.linux.icon);
|
|
1888
|
+
if (existsSync(iconSourcePath)) {
|
|
1889
|
+
const standardIconPath = join(
|
|
1890
|
+
appBundleFolderResourcesPath,
|
|
1891
|
+
"appIcon.png",
|
|
1892
|
+
);
|
|
1893
|
+
|
|
1894
|
+
// Ensure Resources directory exists
|
|
1895
|
+
mkdirSync(appBundleFolderResourcesPath, { recursive: true });
|
|
1896
|
+
|
|
1897
|
+
// Copy the icon to standard location
|
|
1898
|
+
cpSync(iconSourcePath, standardIconPath, { dereference: true });
|
|
1899
|
+
console.log(
|
|
1900
|
+
`Copied Linux icon from ${iconSourcePath} to ${standardIconPath}`,
|
|
1901
|
+
);
|
|
1902
|
+
|
|
1903
|
+
// Also copy icon for the extractor
|
|
1904
|
+
const extractorIconPath = join(
|
|
1905
|
+
appBundleFolderResourcesPath,
|
|
1906
|
+
"app",
|
|
1907
|
+
"icon.png",
|
|
1908
|
+
);
|
|
1909
|
+
mkdirSync(join(appBundleFolderResourcesPath, "app"), {
|
|
1910
|
+
recursive: true,
|
|
1911
|
+
});
|
|
1912
|
+
cpSync(iconSourcePath, extractorIconPath, { dereference: true });
|
|
1913
|
+
console.log(
|
|
1914
|
+
`Copied Linux icon for extractor from ${iconSourcePath} to ${extractorIconPath}`,
|
|
1915
|
+
);
|
|
1916
|
+
} else {
|
|
1917
|
+
console.log(`WARNING: Linux icon not found: ${iconSourcePath}`);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// Create desktop file template for Linux
|
|
1921
|
+
const linuxRequireAdmin = config.build?.linux?.requireAdmin;
|
|
1922
|
+
const desktopContent = `[Desktop Entry]
|
|
1923
|
+
Version=1.0
|
|
1924
|
+
Type=Application
|
|
1925
|
+
Name=${config.app.name}
|
|
1926
|
+
Comment=${config.app.description || `${config.app.name} application`}
|
|
1927
|
+
Exec=${linuxRequireAdmin ? "pkexec launcher" : "launcher"}
|
|
1928
|
+
Icon=appIcon.png
|
|
1929
|
+
Terminal=false
|
|
1930
|
+
StartupWMClass=${config.app.name}
|
|
1931
|
+
Categories=Utility;
|
|
1932
|
+
`;
|
|
1933
|
+
|
|
1934
|
+
const desktopFilePath = join(
|
|
1935
|
+
appBundleFolderPath,
|
|
1936
|
+
`${config.app.name}.desktop`,
|
|
1937
|
+
);
|
|
1938
|
+
writeFileSync(desktopFilePath, desktopContent);
|
|
1939
|
+
console.log(`Created Linux desktop file: ${desktopFilePath}`);
|
|
1940
|
+
} else if (targetOS === "win" && config.build.win?.icon) {
|
|
1941
|
+
const iconPath = join(projectRoot, config.build.win.icon);
|
|
1942
|
+
if (existsSync(iconPath)) {
|
|
1943
|
+
const targetIconPath = join(appBundleFolderResourcesPath, "app.ico");
|
|
1944
|
+
cpSync(iconPath, targetIconPath, { dereference: true });
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// Copy document type icon files to the app bundle Resources folder
|
|
1949
|
+
if (targetOS === "macos" && config.app.fileAssociations) {
|
|
1950
|
+
for (const assoc of config.app.fileAssociations) {
|
|
1951
|
+
if (assoc.icon) {
|
|
1952
|
+
const iconSourcePath = join(projectRoot, assoc.icon);
|
|
1953
|
+
if (existsSync(iconSourcePath)) {
|
|
1954
|
+
const iconFileName = basename(iconSourcePath);
|
|
1955
|
+
const iconDestPath = join(
|
|
1956
|
+
appBundleFolderResourcesPath,
|
|
1957
|
+
iconFileName,
|
|
1958
|
+
);
|
|
1959
|
+
cpSync(iconSourcePath, iconDestPath, {
|
|
1960
|
+
dereference: true,
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
// Missing icon warning is handled by generateDocumentTypes
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
|
|
1969
|
+
// Run preBuild hook before anything starts
|
|
1970
|
+
runHook("preBuild");
|
|
1971
|
+
|
|
1972
|
+
// refresh build folder
|
|
1973
|
+
if (existsSync(buildFolder)) {
|
|
1974
|
+
rmSync(buildFolder, { recursive: true, force: true });
|
|
1975
|
+
}
|
|
1976
|
+
mkdirSync(buildFolder, { recursive: true });
|
|
1977
|
+
|
|
1978
|
+
const mainProcess = "bun";
|
|
1979
|
+
const bunConfig = config.build.bun;
|
|
1980
|
+
const bunSource = join(projectRoot, bunConfig.entrypoint);
|
|
1981
|
+
|
|
1982
|
+
if (!existsSync(bunSource)) {
|
|
1983
|
+
throw new Error(
|
|
1984
|
+
`failed to bundle ${bunSource} because it doesn't exist.\n You need a config.build.bun.entrypoint source file to build.`,
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// build macos bundle
|
|
1989
|
+
// Use display name (with spaces) for macOS bundle folders, sanitized name for other platforms
|
|
1990
|
+
let appBundleFolderPath: string;
|
|
1991
|
+
let appBundleFolderContentsPath: string;
|
|
1992
|
+
let appBundleMacOSPath: string;
|
|
1993
|
+
let appBundleFolderResourcesPath: string;
|
|
1994
|
+
let appBundleFolderFrameworksPath: string;
|
|
1995
|
+
let appBundleAppCodePath: string;
|
|
1996
|
+
const bundleName =
|
|
1997
|
+
targetOS === "macos" ? macOSBundleDisplayName : appFileName;
|
|
1998
|
+
|
|
1999
|
+
const bundle = createAppBundle(bundleName, buildFolder, targetOS);
|
|
2000
|
+
appBundleFolderPath = bundle.appBundleFolderPath;
|
|
2001
|
+
appBundleFolderContentsPath = bundle.appBundleFolderContentsPath;
|
|
2002
|
+
appBundleMacOSPath = bundle.appBundleMacOSPath;
|
|
2003
|
+
appBundleFolderResourcesPath = bundle.appBundleFolderResourcesPath;
|
|
2004
|
+
appBundleFolderFrameworksPath = bundle.appBundleFolderFrameworksPath;
|
|
2005
|
+
appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
|
|
2006
|
+
mkdirSync(appBundleAppCodePath, { recursive: true });
|
|
2007
|
+
|
|
2008
|
+
|
|
2009
|
+
// const bundledBunPath = join(appBundleMacOSPath, 'bun');
|
|
2010
|
+
// cpSync(bunPath, bundledBunPath);
|
|
2011
|
+
|
|
2012
|
+
// Note: for sandboxed apps, MacOS will use the CFBundleIdentifier to create a unique container for the app,
|
|
2013
|
+
// mirroring folders like Application Support, Caches, etc. in the user's Library folder that the sandboxed app
|
|
2014
|
+
// gets access to.
|
|
2015
|
+
|
|
2016
|
+
// We likely want to let users configure this for different environments (eg: dev, canary, stable) and/or
|
|
2017
|
+
// provide methods to help segment data in those folders based on channel/environment
|
|
2018
|
+
|
|
2019
|
+
let InfoPlistContents = "";
|
|
2020
|
+
|
|
2021
|
+
// Generate usage descriptions from entitlements
|
|
2022
|
+
const usageDescriptions = generateUsageDescriptions(
|
|
2023
|
+
config.build.mac.entitlements || {},
|
|
2024
|
+
);
|
|
2025
|
+
// Generate URL scheme handlers
|
|
2026
|
+
const urlTypes = generateURLTypes(
|
|
2027
|
+
config.app.urlSchemes,
|
|
2028
|
+
config.app.identifier,
|
|
2029
|
+
);
|
|
2030
|
+
// Generate document type associations
|
|
2031
|
+
const documentTypes = generateDocumentTypes(
|
|
2032
|
+
config.app.fileAssociations,
|
|
2033
|
+
projectRoot,
|
|
2034
|
+
config.app.identifier,
|
|
2035
|
+
);
|
|
2036
|
+
|
|
2037
|
+
// When using .icon format, CFBundleIconName is needed for Assets.car lookup
|
|
2038
|
+
const iconName = config.build.mac?.icons?.endsWith(".icon")
|
|
2039
|
+
? basename(config.build.mac.icons, ".icon")
|
|
2040
|
+
: null;
|
|
2041
|
+
|
|
2042
|
+
InfoPlistContents = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2043
|
+
|
|
2044
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2045
|
+
<plist version="1.0">
|
|
2046
|
+
<dict>
|
|
2047
|
+
<key>CFBundleExecutable</key>
|
|
2048
|
+
<string>${config.app.name.replace(/ /g, "")}</string>
|
|
2049
|
+
<key>CFBundleIdentifier</key>
|
|
2050
|
+
<string>${config.app.identifier}</string>
|
|
2051
|
+
<key>CFBundleName</key>
|
|
2052
|
+
<string>${bundleName}</string>
|
|
2053
|
+
<key>CFBundleVersion</key>
|
|
2054
|
+
<string>${config.app.version}</string>
|
|
2055
|
+
<key>CFBundlePackageType</key>
|
|
2056
|
+
<string>APPL</string>
|
|
2057
|
+
<key>CFBundleIconFile</key>
|
|
2058
|
+
<string>AppIcon</string>${iconName ? `\n <key>CFBundleIconName</key>\n <string>${iconName}</string>` : ""}${usageDescriptions ? "\n" +
|
|
2059
|
+
usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
2060
|
+
"\n" + documentTypes : ""}
|
|
2061
|
+
</dict>
|
|
2062
|
+
</plist>`;
|
|
2063
|
+
|
|
2064
|
+
await Bun.write(
|
|
2065
|
+
join(appBundleFolderContentsPath, "Info.plist"),
|
|
2066
|
+
InfoPlistContents,
|
|
2067
|
+
);
|
|
2068
|
+
// Compile launcher using bun build --compile
|
|
2069
|
+
const launcherBinaryName = config.app.name.replace(/ /g, "");
|
|
2070
|
+
const bunCliLauncherDestination =
|
|
2071
|
+
join(appBundleMacOSPath, launcherBinaryName) + targetBinExt;
|
|
2072
|
+
const destLauncherFolder = dirname(bunCliLauncherDestination);
|
|
2073
|
+
if (!existsSync(destLauncherFolder)) {
|
|
2074
|
+
mkdirSync(destLauncherFolder, { recursive: true });
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// Compile launcher from source using Bun.build() API
|
|
2078
|
+
const cliDir = dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"));
|
|
2079
|
+
const sparkbunRoot = join(cliDir, "..", "..");
|
|
2080
|
+
const launcherSourcePath = join(sparkbunRoot, "src", "launcher", "main.ts");
|
|
2081
|
+
const bunTarget = `bun-${targetOS === "macos" ? "darwin" : targetOS === "win" ? "windows" : "linux"}-${currentTarget.arch}` as const;
|
|
2082
|
+
|
|
2083
|
+
const compileOptions: any = {
|
|
2084
|
+
target: bunTarget,
|
|
2085
|
+
outfile: bunCliLauncherDestination,
|
|
2086
|
+
};
|
|
2087
|
+
|
|
2088
|
+
if (targetOS === "win") {
|
|
2089
|
+
let icoPath: string | undefined;
|
|
2090
|
+
if (config.build.win?.icon) {
|
|
2091
|
+
const iconSrc = config.build.win.icon.startsWith("/") || config.build.win.icon.match(/^[a-zA-Z]:/)
|
|
2092
|
+
? config.build.win.icon
|
|
2093
|
+
: join(projectRoot, config.build.win.icon);
|
|
2094
|
+
if (existsSync(iconSrc)) {
|
|
2095
|
+
icoPath = iconSrc;
|
|
2096
|
+
if (iconSrc.toLowerCase().endsWith(".png")) {
|
|
2097
|
+
const pngToIco = (await import("png-to-ico")).default;
|
|
2098
|
+
const tempIcoPath = join(buildFolder, "temp-launcher-icon.ico");
|
|
2099
|
+
const icoBuffer = await pngToIco(iconSrc);
|
|
2100
|
+
writeFileSync(tempIcoPath, new Uint8Array(icoBuffer));
|
|
2101
|
+
icoPath = tempIcoPath;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
compileOptions.windows = {
|
|
2106
|
+
hideConsole: true,
|
|
2107
|
+
...(icoPath && { icon: icoPath }),
|
|
2108
|
+
title: config.app.name,
|
|
2109
|
+
version: config.app.version,
|
|
2110
|
+
description: config.app.name,
|
|
2111
|
+
publisher: config.app.publisher || " ",
|
|
2112
|
+
copyright: config.app.copyright || " ",
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
console.log(`Compiling launcher with Bun.build()...`);
|
|
2117
|
+
const launcherBuild = await Bun.build({
|
|
2118
|
+
entrypoints: [launcherSourcePath],
|
|
2119
|
+
compile: compileOptions,
|
|
2120
|
+
});
|
|
2121
|
+
if (!launcherBuild.success) {
|
|
2122
|
+
console.error("Launcher compilation failed:", launcherBuild.logs);
|
|
2123
|
+
throw new Error("Launcher compilation failed");
|
|
2124
|
+
}
|
|
2125
|
+
console.log(`Compiled launcher: ${bunCliLauncherDestination}`);
|
|
2126
|
+
|
|
2127
|
+
if (targetOS === "win") {
|
|
2128
|
+
patchPeSubsystem(bunCliLauncherDestination);
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
cpSync(targetPaths.PRELOAD_FULL_JS, join(appBundleFolderResourcesPath, "preload-full.js"), {
|
|
2132
|
+
dereference: true,
|
|
2133
|
+
});
|
|
2134
|
+
cpSync(
|
|
2135
|
+
targetPaths.PRELOAD_SANDBOXED_JS,
|
|
2136
|
+
join(appBundleFolderResourcesPath, "preload-sandboxed.js"),
|
|
2137
|
+
{
|
|
2138
|
+
dereference: true,
|
|
2139
|
+
},
|
|
2140
|
+
);
|
|
2141
|
+
|
|
2142
|
+
|
|
2143
|
+
// copy native wrapper dynamic library
|
|
2144
|
+
if (targetOS === "macos") {
|
|
2145
|
+
cpSync(targetPaths.CORE_MACOS, join(appBundleMacOSPath, "libElectrobunCore.dylib"), {
|
|
2146
|
+
dereference: true,
|
|
2147
|
+
});
|
|
2148
|
+
|
|
2149
|
+
const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_MACOS;
|
|
2150
|
+
const nativeWrapperMacosDestination = join(
|
|
2151
|
+
appBundleMacOSPath,
|
|
2152
|
+
"libNativeWrapper.dylib",
|
|
2153
|
+
);
|
|
2154
|
+
cpSync(nativeWrapperMacosSource, nativeWrapperMacosDestination, {
|
|
2155
|
+
dereference: true,
|
|
2156
|
+
});
|
|
2157
|
+
} else if (targetOS === "win") {
|
|
2158
|
+
cpSync(targetPaths.CORE_WIN, join(appBundleMacOSPath, "ElectrobunCore.dll"), {
|
|
2159
|
+
dereference: true,
|
|
2160
|
+
});
|
|
2161
|
+
|
|
2162
|
+
const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_WIN;
|
|
2163
|
+
const nativeWrapperMacosDestination = join(
|
|
2164
|
+
appBundleMacOSPath,
|
|
2165
|
+
"libNativeWrapper.dll",
|
|
2166
|
+
);
|
|
2167
|
+
cpSync(nativeWrapperMacosSource, nativeWrapperMacosDestination, {
|
|
2168
|
+
dereference: true,
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
const webview2LibSource = targetPaths.WEBVIEW2LOADER_WIN;
|
|
2172
|
+
const webview2LibDestination = join(
|
|
2173
|
+
appBundleMacOSPath,
|
|
2174
|
+
"WebView2Loader.dll",
|
|
2175
|
+
);
|
|
2176
|
+
// copy webview2 system webview library
|
|
2177
|
+
cpSync(webview2LibSource, webview2LibDestination, { dereference: true });
|
|
2178
|
+
} else if (targetOS === "linux") {
|
|
2179
|
+
// Choose the appropriate native wrapper based on bundleCEF setting
|
|
2180
|
+
const useCEF = config.build.linux?.bundleCEF;
|
|
2181
|
+
cpSync(targetPaths.CORE_LINUX, join(appBundleMacOSPath, "libElectrobunCore.so"), {
|
|
2182
|
+
dereference: true,
|
|
2183
|
+
});
|
|
2184
|
+
const nativeWrapperLinuxSource = useCEF
|
|
2185
|
+
? targetPaths.NATIVE_WRAPPER_LINUX_CEF
|
|
2186
|
+
: targetPaths.NATIVE_WRAPPER_LINUX;
|
|
2187
|
+
const nativeWrapperLinuxDestination = join(
|
|
2188
|
+
appBundleMacOSPath,
|
|
2189
|
+
"libNativeWrapper.so",
|
|
2190
|
+
);
|
|
2191
|
+
|
|
2192
|
+
if (existsSync(nativeWrapperLinuxSource)) {
|
|
2193
|
+
cpSync(nativeWrapperLinuxSource, nativeWrapperLinuxDestination, {
|
|
2194
|
+
dereference: true,
|
|
2195
|
+
});
|
|
2196
|
+
console.log(
|
|
2197
|
+
`Using ${useCEF ? "CEF (with weak linking)" : "GTK-only"} native wrapper for Linux`,
|
|
2198
|
+
);
|
|
2199
|
+
} else {
|
|
2200
|
+
throw new Error(
|
|
2201
|
+
`Native wrapper not found: ${nativeWrapperLinuxSource}`,
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// Copy icon if specified for Linux to a standard location
|
|
2206
|
+
if (config.build.linux?.icon) {
|
|
2207
|
+
const iconSourcePath = join(projectRoot, config.build.linux.icon);
|
|
2208
|
+
if (existsSync(iconSourcePath)) {
|
|
2209
|
+
const standardIconPath = join(
|
|
2210
|
+
appBundleFolderResourcesPath,
|
|
2211
|
+
"appIcon.png",
|
|
2212
|
+
);
|
|
2213
|
+
|
|
2214
|
+
// Ensure Resources directory exists
|
|
2215
|
+
mkdirSync(appBundleFolderResourcesPath, { recursive: true });
|
|
2216
|
+
|
|
2217
|
+
// Copy the icon to standard location
|
|
2218
|
+
cpSync(iconSourcePath, standardIconPath, { dereference: true });
|
|
2219
|
+
console.log(
|
|
2220
|
+
`Copied Linux icon from ${iconSourcePath} to ${standardIconPath}`,
|
|
2221
|
+
);
|
|
2222
|
+
|
|
2223
|
+
// Also copy icon for the extractor
|
|
2224
|
+
const extractorIconPath = join(
|
|
2225
|
+
appBundleFolderResourcesPath,
|
|
2226
|
+
"app",
|
|
2227
|
+
"icon.png",
|
|
2228
|
+
);
|
|
2229
|
+
mkdirSync(join(appBundleFolderResourcesPath, "app"), {
|
|
2230
|
+
recursive: true,
|
|
2231
|
+
});
|
|
2232
|
+
cpSync(iconSourcePath, extractorIconPath, { dereference: true });
|
|
2233
|
+
console.log(
|
|
2234
|
+
`Copied Linux icon for extractor from ${iconSourcePath} to ${extractorIconPath}`,
|
|
2235
|
+
);
|
|
2236
|
+
} else {
|
|
2237
|
+
console.log(`WARNING: Linux icon not found: ${iconSourcePath}`);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// Download CEF binaries if needed when bundleCEF is enabled
|
|
2243
|
+
if (
|
|
2244
|
+
(targetOS === "macos" && config.build.mac?.bundleCEF) ||
|
|
2245
|
+
(targetOS === "win" && config.build.win?.bundleCEF) ||
|
|
2246
|
+
(targetOS === "linux" && config.build.linux?.bundleCEF)
|
|
2247
|
+
) {
|
|
2248
|
+
const effectiveCEFDir = await ensureCEFDependencies(
|
|
2249
|
+
currentTarget.os,
|
|
2250
|
+
currentTarget.arch,
|
|
2251
|
+
config.build.cefVersion,
|
|
2252
|
+
);
|
|
2253
|
+
if (targetOS === "macos") {
|
|
2254
|
+
const cefFrameworkSource = join(
|
|
2255
|
+
effectiveCEFDir,
|
|
2256
|
+
"Chromium Embedded Framework.framework",
|
|
2257
|
+
);
|
|
2258
|
+
const cefFrameworkDestination = join(
|
|
2259
|
+
appBundleFolderFrameworksPath,
|
|
2260
|
+
"Chromium Embedded Framework.framework",
|
|
2261
|
+
);
|
|
2262
|
+
|
|
2263
|
+
cpSync(cefFrameworkSource, cefFrameworkDestination, {
|
|
2264
|
+
recursive: true,
|
|
2265
|
+
dereference: true,
|
|
2266
|
+
});
|
|
2267
|
+
|
|
2268
|
+
// cef helpers
|
|
2269
|
+
const cefHelperNames = getCEFHelperNames();
|
|
2270
|
+
|
|
2271
|
+
const helperSourcePath = targetPaths.CEF_HELPER_MACOS;
|
|
2272
|
+
cefHelperNames.forEach((helperName) => {
|
|
2273
|
+
const destinationPath = join(
|
|
2274
|
+
appBundleFolderFrameworksPath,
|
|
2275
|
+
`${helperName}.app`,
|
|
2276
|
+
`Contents`,
|
|
2277
|
+
`MacOS`,
|
|
2278
|
+
`${helperName}`,
|
|
2279
|
+
);
|
|
2280
|
+
|
|
2281
|
+
const destFolder4 = dirname(destinationPath);
|
|
2282
|
+
if (!existsSync(destFolder4)) {
|
|
2283
|
+
// console.info('creating folder: ', destFolder4);
|
|
2284
|
+
mkdirSync(destFolder4, { recursive: true });
|
|
2285
|
+
}
|
|
2286
|
+
cpSync(helperSourcePath, destinationPath, {
|
|
2287
|
+
recursive: true,
|
|
2288
|
+
dereference: true,
|
|
2289
|
+
});
|
|
2290
|
+
});
|
|
2291
|
+
} else if (targetOS === "win") {
|
|
2292
|
+
// Copy CEF DLLs from CEF directory to the main executable directory
|
|
2293
|
+
const cefSourcePath = effectiveCEFDir;
|
|
2294
|
+
const cefDllFiles = [
|
|
2295
|
+
"libcef.dll",
|
|
2296
|
+
"chrome_elf.dll",
|
|
2297
|
+
"d3dcompiler_47.dll",
|
|
2298
|
+
"dxcompiler.dll",
|
|
2299
|
+
"dxil.dll",
|
|
2300
|
+
"libEGL.dll",
|
|
2301
|
+
"libGLESv2.dll",
|
|
2302
|
+
"vk_swiftshader.dll",
|
|
2303
|
+
"vulkan-1.dll",
|
|
2304
|
+
];
|
|
2305
|
+
|
|
2306
|
+
cefDllFiles.forEach((dllFile) => {
|
|
2307
|
+
const sourcePath = join(cefSourcePath, dllFile);
|
|
2308
|
+
const destPath = join(appBundleMacOSPath, dllFile);
|
|
2309
|
+
if (existsSync(sourcePath)) {
|
|
2310
|
+
cpSync(sourcePath, destPath, { dereference: true });
|
|
2311
|
+
}
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
// Copy icudtl.dat to MacOS root (same folder as libcef.dll) - required for CEF initialization
|
|
2315
|
+
const icuDataSource = join(cefSourcePath, "icudtl.dat");
|
|
2316
|
+
const icuDataDest = join(appBundleMacOSPath, "icudtl.dat");
|
|
2317
|
+
if (existsSync(icuDataSource)) {
|
|
2318
|
+
cpSync(icuDataSource, icuDataDest, { dereference: true });
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// Copy essential CEF pak files to MacOS root (same folder as libcef.dll) - required for CEF resources
|
|
2322
|
+
const essentialPakFiles = [
|
|
2323
|
+
"chrome_100_percent.pak",
|
|
2324
|
+
"resources.pak",
|
|
2325
|
+
"v8_context_snapshot.bin",
|
|
2326
|
+
];
|
|
2327
|
+
essentialPakFiles.forEach((pakFile) => {
|
|
2328
|
+
const sourcePath = join(cefSourcePath, pakFile);
|
|
2329
|
+
const destPath = join(appBundleMacOSPath, pakFile);
|
|
2330
|
+
|
|
2331
|
+
if (existsSync(sourcePath)) {
|
|
2332
|
+
cpSync(sourcePath, destPath, { dereference: true });
|
|
2333
|
+
} else {
|
|
2334
|
+
console.log(`WARNING: Missing CEF file: ${sourcePath}`);
|
|
2335
|
+
}
|
|
2336
|
+
});
|
|
2337
|
+
|
|
2338
|
+
// Copy CEF resources to MacOS/cef/ subdirectory for other resources like locales
|
|
2339
|
+
const cefResourcesSource = effectiveCEFDir;
|
|
2340
|
+
const cefResourcesDestination = join(appBundleMacOSPath, "cef");
|
|
2341
|
+
|
|
2342
|
+
if (existsSync(cefResourcesSource)) {
|
|
2343
|
+
cpSync(cefResourcesSource, cefResourcesDestination, {
|
|
2344
|
+
recursive: true,
|
|
2345
|
+
dereference: true,
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
// Copy CEF helper processes with different names
|
|
2350
|
+
const cefHelperNames = getCEFHelperNames();
|
|
2351
|
+
|
|
2352
|
+
const helperSourcePath = targetPaths.CEF_HELPER_WIN;
|
|
2353
|
+
if (existsSync(helperSourcePath)) {
|
|
2354
|
+
cefHelperNames.forEach((helperName) => {
|
|
2355
|
+
const destinationPath = join(
|
|
2356
|
+
appBundleMacOSPath,
|
|
2357
|
+
`${helperName}.exe`,
|
|
2358
|
+
);
|
|
2359
|
+
cpSync(helperSourcePath, destinationPath, { dereference: true });
|
|
2360
|
+
});
|
|
2361
|
+
} else {
|
|
2362
|
+
console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
|
|
2363
|
+
}
|
|
2364
|
+
} else if (targetOS === "linux") {
|
|
2365
|
+
// Copy CEF shared libraries from platform-specific dist/cef/ to the main executable directory
|
|
2366
|
+
const cefSourcePath = effectiveCEFDir;
|
|
2367
|
+
|
|
2368
|
+
if (existsSync(cefSourcePath)) {
|
|
2369
|
+
const cefSoFiles = [
|
|
2370
|
+
"libcef.so",
|
|
2371
|
+
"libEGL.so",
|
|
2372
|
+
"libGLESv2.so",
|
|
2373
|
+
"libvk_swiftshader.so",
|
|
2374
|
+
"libvulkan.so.1",
|
|
2375
|
+
];
|
|
2376
|
+
|
|
2377
|
+
// Copy CEF .so files to main directory as symlinks to cef/ subdirectory
|
|
2378
|
+
cefSoFiles.forEach((soFile) => {
|
|
2379
|
+
const sourcePath = join(cefSourcePath, soFile);
|
|
2380
|
+
// @ts-expect-error - reserved for future use
|
|
2381
|
+
const _destPath = join(appBundleMacOSPath, soFile);
|
|
2382
|
+
if (existsSync(sourcePath)) {
|
|
2383
|
+
// We'll create the actual file in cef/ and symlink from main directory
|
|
2384
|
+
// This will be done after the cef/ directory is populated
|
|
2385
|
+
}
|
|
2386
|
+
});
|
|
2387
|
+
|
|
2388
|
+
// Copy icudtl.dat to MacOS root (same folder as libcef.so) - required for CEF initialization
|
|
2389
|
+
const icuDataSource = join(cefSourcePath, "icudtl.dat");
|
|
2390
|
+
const icuDataDest = join(appBundleMacOSPath, "icudtl.dat");
|
|
2391
|
+
if (existsSync(icuDataSource)) {
|
|
2392
|
+
cpSync(icuDataSource, icuDataDest, { dereference: true });
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
// Copy .pak files and other CEF resources to the main executable directory
|
|
2396
|
+
const pakFiles = [
|
|
2397
|
+
"icudtl.dat",
|
|
2398
|
+
"v8_context_snapshot.bin",
|
|
2399
|
+
"snapshot_blob.bin",
|
|
2400
|
+
"resources.pak",
|
|
2401
|
+
"chrome_100_percent.pak",
|
|
2402
|
+
"chrome_200_percent.pak",
|
|
2403
|
+
"locales",
|
|
2404
|
+
"chrome-sandbox",
|
|
2405
|
+
"vk_swiftshader_icd.json",
|
|
2406
|
+
];
|
|
2407
|
+
pakFiles.forEach((pakFile) => {
|
|
2408
|
+
const sourcePath = join(cefSourcePath, pakFile);
|
|
2409
|
+
const destPath = join(appBundleMacOSPath, pakFile);
|
|
2410
|
+
if (existsSync(sourcePath)) {
|
|
2411
|
+
cpSync(sourcePath, destPath, {
|
|
2412
|
+
recursive: true,
|
|
2413
|
+
dereference: true,
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
});
|
|
2417
|
+
|
|
2418
|
+
// Copy locales to cef subdirectory
|
|
2419
|
+
const cefResourcesDestination = join(appBundleMacOSPath, "cef");
|
|
2420
|
+
if (!existsSync(cefResourcesDestination)) {
|
|
2421
|
+
mkdirSync(cefResourcesDestination, { recursive: true });
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
// Copy all CEF shared libraries to cef subdirectory as well (for RPATH $ORIGIN/cef)
|
|
2425
|
+
cefSoFiles.forEach((soFile) => {
|
|
2426
|
+
const sourcePath = join(cefSourcePath, soFile);
|
|
2427
|
+
const destPath = join(cefResourcesDestination, soFile);
|
|
2428
|
+
if (existsSync(sourcePath)) {
|
|
2429
|
+
cpSync(sourcePath, destPath, { dereference: true });
|
|
2430
|
+
console.log(`Copied CEF library to cef subdirectory: ${soFile}`);
|
|
2431
|
+
} else {
|
|
2432
|
+
console.log(`WARNING: Missing CEF library: ${sourcePath}`);
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
|
|
2436
|
+
// Copy essential CEF files to cef subdirectory as well (for RPATH $ORIGIN/cef)
|
|
2437
|
+
const cefEssentialFiles = ["vk_swiftshader_icd.json"];
|
|
2438
|
+
cefEssentialFiles.forEach((cefFile) => {
|
|
2439
|
+
const sourcePath = join(cefSourcePath, cefFile);
|
|
2440
|
+
const destPath = join(cefResourcesDestination, cefFile);
|
|
2441
|
+
if (existsSync(sourcePath)) {
|
|
2442
|
+
cpSync(sourcePath, destPath, { dereference: true });
|
|
2443
|
+
console.log(
|
|
2444
|
+
`Copied CEF essential file to cef subdirectory: ${cefFile}`,
|
|
2445
|
+
);
|
|
2446
|
+
} else {
|
|
2447
|
+
console.log(`WARNING: Missing CEF essential file: ${sourcePath}`);
|
|
2448
|
+
}
|
|
2449
|
+
});
|
|
2450
|
+
|
|
2451
|
+
// Create symlinks from main directory to cef/ subdirectory for .so files
|
|
2452
|
+
console.log("Creating symlinks for CEF libraries...");
|
|
2453
|
+
cefSoFiles.forEach((soFile) => {
|
|
2454
|
+
const cefFilePath = join(cefResourcesDestination, soFile);
|
|
2455
|
+
const mainDirPath = join(appBundleMacOSPath, soFile);
|
|
2456
|
+
|
|
2457
|
+
if (existsSync(cefFilePath)) {
|
|
2458
|
+
try {
|
|
2459
|
+
// Remove any existing file/symlink in main directory
|
|
2460
|
+
if (existsSync(mainDirPath)) {
|
|
2461
|
+
rmSync(mainDirPath);
|
|
2462
|
+
}
|
|
2463
|
+
// Create symlink from main directory to cef/ subdirectory
|
|
2464
|
+
symlinkSync(join("cef", soFile), mainDirPath);
|
|
2465
|
+
console.log(
|
|
2466
|
+
`Created symlink for CEF library: ${soFile} -> cef/${soFile}`,
|
|
2467
|
+
);
|
|
2468
|
+
} catch (error) {
|
|
2469
|
+
console.log(
|
|
2470
|
+
`WARNING: Failed to create symlink for ${soFile}: ${error}`,
|
|
2471
|
+
);
|
|
2472
|
+
// Fallback to copying the file
|
|
2473
|
+
cpSync(cefFilePath, mainDirPath, { dereference: true });
|
|
2474
|
+
console.log(
|
|
2475
|
+
`Fallback: Copying CEF library to main directory: ${soFile}`,
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2480
|
+
|
|
2481
|
+
// Copy CEF helper processes with different names
|
|
2482
|
+
const cefHelperNames = getCEFHelperNames();
|
|
2483
|
+
|
|
2484
|
+
const helperSourcePath = targetPaths.CEF_HELPER_LINUX;
|
|
2485
|
+
if (existsSync(helperSourcePath)) {
|
|
2486
|
+
cefHelperNames.forEach((helperName) => {
|
|
2487
|
+
const destinationPath = join(appBundleMacOSPath, helperName);
|
|
2488
|
+
cpSync(helperSourcePath, destinationPath, { dereference: true });
|
|
2489
|
+
// console.log(`Copied CEF helper: ${helperName}`);
|
|
2490
|
+
});
|
|
2491
|
+
} else {
|
|
2492
|
+
console.log(`WARNING: Missing CEF helper: ${helperSourcePath}`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// Download WGPU (Dawn) binaries if needed when bundleWGPU is enabled
|
|
2499
|
+
if (
|
|
2500
|
+
(targetOS === "macos" && config.build.mac?.bundleWGPU) ||
|
|
2501
|
+
(targetOS === "win" && config.build.win?.bundleWGPU) ||
|
|
2502
|
+
(targetOS === "linux" && config.build.linux?.bundleWGPU)
|
|
2503
|
+
) {
|
|
2504
|
+
const effectiveWGPUDir = await ensureWGPUDependencies(
|
|
2505
|
+
currentTarget.os,
|
|
2506
|
+
currentTarget.arch,
|
|
2507
|
+
config.build.wgpuVersion,
|
|
2508
|
+
);
|
|
2509
|
+
|
|
2510
|
+
const libCandidates =
|
|
2511
|
+
targetOS === "macos"
|
|
2512
|
+
? [
|
|
2513
|
+
join(effectiveWGPUDir, "lib", "libwebgpu_dawn.dylib"),
|
|
2514
|
+
join(effectiveWGPUDir, "lib", "libwebgpu_dawn_shared.dylib"),
|
|
2515
|
+
]
|
|
2516
|
+
: targetOS === "win"
|
|
2517
|
+
? [
|
|
2518
|
+
join(effectiveWGPUDir, "bin", "webgpu_dawn.dll"),
|
|
2519
|
+
join(effectiveWGPUDir, "bin", "libwebgpu_dawn.dll"),
|
|
2520
|
+
join(effectiveWGPUDir, "lib", "webgpu_dawn.dll"),
|
|
2521
|
+
join(effectiveWGPUDir, "lib", "libwebgpu_dawn.dll"),
|
|
2522
|
+
]
|
|
2523
|
+
: [
|
|
2524
|
+
join(effectiveWGPUDir, "lib", "libwebgpu_dawn.so"),
|
|
2525
|
+
join(effectiveWGPUDir, "lib", "libwebgpu_dawn_shared.so"),
|
|
2526
|
+
];
|
|
2527
|
+
|
|
2528
|
+
const libSource = libCandidates.find((p) => existsSync(p));
|
|
2529
|
+
if (!libSource) {
|
|
2530
|
+
throw new Error(
|
|
2531
|
+
`WGPU shared library not found in ${effectiveWGPUDir}. Checked: ${libCandidates.join(", ")}`,
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
const libDest = join(appBundleMacOSPath, basename(libSource));
|
|
2536
|
+
cpSync(libSource, libDest, { dereference: true });
|
|
2537
|
+
console.log(`Copied WGPU library to bundle: ${libDest}`);
|
|
2538
|
+
|
|
2539
|
+
// On Windows, Dawn needs d3dcompiler_47.dll for D3D shader compilation.
|
|
2540
|
+
// ARM64 Windows doesn't have an x64 version in system directories,
|
|
2541
|
+
// so bundle it alongside the WGPU library.
|
|
2542
|
+
if (targetOS === "win") {
|
|
2543
|
+
const d3dCandidates = [
|
|
2544
|
+
join(effectiveWGPUDir, "bin", "d3dcompiler_47.dll"),
|
|
2545
|
+
join(SPARKBUN_DEP_PATH, `dist-win-${currentTarget.arch}`, "d3dcompiler_47.dll"),
|
|
2546
|
+
join(SPARKBUN_DEP_PATH, "dist", "d3dcompiler_47.dll"),
|
|
2547
|
+
join(targetPaths.CEF_DIR, "d3dcompiler_47.dll"),
|
|
2548
|
+
];
|
|
2549
|
+
const d3dSource = d3dCandidates.find((p) => existsSync(p));
|
|
2550
|
+
if (d3dSource) {
|
|
2551
|
+
const d3dDest = join(appBundleMacOSPath, "d3dcompiler_47.dll");
|
|
2552
|
+
cpSync(d3dSource, d3dDest, { dereference: true });
|
|
2553
|
+
console.log(`Copied d3dcompiler_47.dll to bundle: ${d3dDest}`);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
// copy native bindings
|
|
2559
|
+
const bsPatchSource = targetPaths.BSPATCH;
|
|
2560
|
+
const bsPatchDestination =
|
|
2561
|
+
join(appBundleMacOSPath, "bspatch") + targetBinExt;
|
|
2562
|
+
const bsPatchDestFolder = dirname(bsPatchDestination);
|
|
2563
|
+
if (!existsSync(bsPatchDestFolder)) {
|
|
2564
|
+
mkdirSync(bsPatchDestFolder, { recursive: true });
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
cpSync(bsPatchSource, bsPatchDestination, {
|
|
2568
|
+
recursive: true,
|
|
2569
|
+
dereference: true,
|
|
2570
|
+
});
|
|
2571
|
+
|
|
2572
|
+
// libasar is still loaded by ElectrobunCore at runtime — needed until
|
|
2573
|
+
// native wrappers are recompiled without ASAR support
|
|
2574
|
+
const libExt = targetOS === "win" ? ".dll" : targetOS === "macos" ? ".dylib" : ".so";
|
|
2575
|
+
const asarLibSource = join(dirname(targetPaths.BSPATCH), "libasar" + libExt);
|
|
2576
|
+
if (existsSync(asarLibSource)) {
|
|
2577
|
+
cpSync(asarLibSource, join(appBundleMacOSPath, "libasar" + libExt), {
|
|
2578
|
+
dereference: true,
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
if (mainProcess === "bun") {
|
|
2583
|
+
// transpile developer's bun code
|
|
2584
|
+
const bunDestFolder = join(appBundleAppCodePath, "bun");
|
|
2585
|
+
const { entrypoint: _bunEntrypoint, ...bunBuildOptions } = bunConfig;
|
|
2586
|
+
const buildResult = await Bun.build({
|
|
2587
|
+
...bunBuildOptions,
|
|
2588
|
+
entrypoints: [bunSource],
|
|
2589
|
+
outdir: bunDestFolder,
|
|
2590
|
+
target: "bun",
|
|
2591
|
+
});
|
|
2592
|
+
|
|
2593
|
+
if (!buildResult.success) {
|
|
2594
|
+
console.error("failed to build", bunSource);
|
|
2595
|
+
printBuildLogs(buildResult.logs);
|
|
2596
|
+
throw new Error("Build failed: bun build failed");
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// transpile developer's view code
|
|
2601
|
+
// Build webview-javascript ts files
|
|
2602
|
+
// bundle all the bundles
|
|
2603
|
+
for (const viewName in config.build.views) {
|
|
2604
|
+
const viewConfig = config.build.views[viewName]!;
|
|
2605
|
+
|
|
2606
|
+
const viewSource = join(projectRoot, viewConfig.entrypoint);
|
|
2607
|
+
if (!existsSync(viewSource)) {
|
|
2608
|
+
console.error(
|
|
2609
|
+
`failed to bundle ${viewSource} because it doesn't exist.`,
|
|
2610
|
+
);
|
|
2611
|
+
continue;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
const viewDestFolder = join(appBundleAppCodePath, "views", viewName);
|
|
2615
|
+
|
|
2616
|
+
if (!existsSync(viewDestFolder)) {
|
|
2617
|
+
// console.info('creating folder: ', viewDestFolder);
|
|
2618
|
+
mkdirSync(viewDestFolder, { recursive: true });
|
|
2619
|
+
} else {
|
|
2620
|
+
console.error(
|
|
2621
|
+
"continuing, but ",
|
|
2622
|
+
viewDestFolder,
|
|
2623
|
+
"unexpectedly already exists in the build folder",
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
// console.info(`bundling ${viewSource} to ${viewDestFolder} with config: `, viewConfig);
|
|
2628
|
+
|
|
2629
|
+
const { entrypoint: _viewEntrypoint, ...viewBuildOptions } = viewConfig;
|
|
2630
|
+
const buildResult = await Bun.build({
|
|
2631
|
+
...viewBuildOptions,
|
|
2632
|
+
entrypoints: [viewSource],
|
|
2633
|
+
outdir: viewDestFolder,
|
|
2634
|
+
target: "browser",
|
|
2635
|
+
});
|
|
2636
|
+
|
|
2637
|
+
if (!buildResult.success) {
|
|
2638
|
+
console.error("failed to build", viewSource);
|
|
2639
|
+
printBuildLogs(buildResult.logs);
|
|
2640
|
+
continue;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
// Copy assets like html, css, images, and other files
|
|
2645
|
+
for (const relSource in config.build.copy) {
|
|
2646
|
+
const source = join(projectRoot, relSource);
|
|
2647
|
+
if (!existsSync(source)) {
|
|
2648
|
+
console.error(`failed to copy ${source} because it doesn't exist.`);
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
const destination = join(
|
|
2653
|
+
appBundleAppCodePath,
|
|
2654
|
+
config.build.copy[relSource]!,
|
|
2655
|
+
);
|
|
2656
|
+
const destFolder = dirname(destination);
|
|
2657
|
+
|
|
2658
|
+
if (!existsSync(destFolder)) {
|
|
2659
|
+
// console.info('creating folder: ', destFolder);
|
|
2660
|
+
mkdirSync(destFolder, { recursive: true });
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
cpSync(source, destination, { recursive: true, dereference: true });
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
buildIcons(appBundleFolderResourcesPath, appBundleFolderPath);
|
|
2667
|
+
|
|
2668
|
+
runHook("postBuild", {});
|
|
2669
|
+
|
|
2670
|
+
// Create a content hash for version.json. In non-dev builds this is used
|
|
2671
|
+
// by the updater to detect changes. For dev builds we skip it since
|
|
2672
|
+
// the updater isn't relevant.
|
|
2673
|
+
let hash: string;
|
|
2674
|
+
if (buildEnvironment === "dev") {
|
|
2675
|
+
hash = "dev";
|
|
2676
|
+
} else {
|
|
2677
|
+
// Walk the app bundle and create an in-memory tar for hashing
|
|
2678
|
+
// (no temp file on disk). This hashes the
|
|
2679
|
+
// hash reflects the final shipped bundle contents.
|
|
2680
|
+
console.time("Generate Bundle hash");
|
|
2681
|
+
const bundleFiles: Record<string, Blob> = {};
|
|
2682
|
+
const bundleBase = basename(appBundleFolderPath);
|
|
2683
|
+
const entries = readdirSync(appBundleFolderPath, {
|
|
2684
|
+
recursive: true,
|
|
2685
|
+
} as any) as string[];
|
|
2686
|
+
for (const entry of entries) {
|
|
2687
|
+
const entryPath = entry.toString();
|
|
2688
|
+
const fullPath = join(appBundleFolderPath, entryPath);
|
|
2689
|
+
if (statSync(fullPath).isFile()) {
|
|
2690
|
+
bundleFiles[join(bundleBase, entryPath)] = Bun.file(fullPath);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
// Check if Bun.Archive is available (Bun 1.3.0+)
|
|
2694
|
+
if (typeof Bun.Archive !== "undefined") {
|
|
2695
|
+
const archiveBytes = await new Bun.Archive(bundleFiles).bytes();
|
|
2696
|
+
// Note: wyhash is the default in Bun.hash but that may change in the future
|
|
2697
|
+
// so we're being explicit here.
|
|
2698
|
+
hash = Bun.hash.wyhash(archiveBytes, 43770n).toString(36);
|
|
2699
|
+
} else {
|
|
2700
|
+
// Fallback for older Bun versions - use a simple hash of file paths
|
|
2701
|
+
console.warn("Bun.Archive not available, using fallback hash method");
|
|
2702
|
+
const fileList = Object.keys(bundleFiles).sort().join("\n");
|
|
2703
|
+
hash = Bun.hash.wyhash(fileList).toString(36);
|
|
2704
|
+
}
|
|
2705
|
+
console.timeEnd("Generate Bundle hash");
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// const bunVersion = execSync(`${bunBinarySourcePath} --version`).toString().trim();
|
|
2709
|
+
|
|
2710
|
+
// version.json inside the app bundle
|
|
2711
|
+
const versionJsonContent = JSON.stringify({
|
|
2712
|
+
version: config.app.version,
|
|
2713
|
+
// The first tar file does not include this, it gets hashed,
|
|
2714
|
+
// then the hash is included in another tar file. That later one
|
|
2715
|
+
// then gets used for patching and updating.
|
|
2716
|
+
hash: hash,
|
|
2717
|
+
channel: buildEnvironment,
|
|
2718
|
+
baseUrl: config.release.baseUrl,
|
|
2719
|
+
name: appFileName,
|
|
2720
|
+
identifier: config.app.identifier,
|
|
2721
|
+
});
|
|
2722
|
+
|
|
2723
|
+
await Bun.write(
|
|
2724
|
+
join(appBundleFolderResourcesPath, "version.json"),
|
|
2725
|
+
versionJsonContent,
|
|
2726
|
+
);
|
|
2727
|
+
|
|
2728
|
+
// build.json inside the app bundle - runtime build configuration
|
|
2729
|
+
const platformConfig =
|
|
2730
|
+
targetOS === "macos"
|
|
2731
|
+
? config.build?.mac
|
|
2732
|
+
: targetOS === "win"
|
|
2733
|
+
? config.build?.win
|
|
2734
|
+
: config.build?.linux;
|
|
2735
|
+
|
|
2736
|
+
const bundlesCEF = platformConfig?.bundleCEF ?? false;
|
|
2737
|
+
|
|
2738
|
+
const buildJsonObj: Record<string, unknown> = {
|
|
2739
|
+
mainProcess,
|
|
2740
|
+
defaultRenderer: platformConfig?.defaultRenderer ?? "native",
|
|
2741
|
+
availableRenderers: bundlesCEF ? ["native", "cef"] : ["native"],
|
|
2742
|
+
runtime: config.runtime ?? {},
|
|
2743
|
+
...(bundlesCEF
|
|
2744
|
+
? { cefVersion: config.build?.cefVersion ?? DEFAULT_CEF_VERSION_STRING }
|
|
2745
|
+
: {}),
|
|
2746
|
+
...(mainProcess === "bun"
|
|
2747
|
+
? { bunVersion: config.build?.bunVersion ?? BUN_VERSION }
|
|
2748
|
+
: {}),
|
|
2749
|
+
};
|
|
2750
|
+
|
|
2751
|
+
// Include chromiumFlags only if the developer defined them
|
|
2752
|
+
if (
|
|
2753
|
+
platformConfig?.chromiumFlags &&
|
|
2754
|
+
Object.keys(platformConfig.chromiumFlags).length > 0
|
|
2755
|
+
) {
|
|
2756
|
+
buildJsonObj["chromiumFlags"] = platformConfig.chromiumFlags;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
if (platformConfig?.requireAdmin) {
|
|
2760
|
+
buildJsonObj["requireAdmin"] = true;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
const buildJsonContent = JSON.stringify(buildJsonObj);
|
|
2764
|
+
|
|
2765
|
+
await Bun.write(
|
|
2766
|
+
join(appBundleFolderResourcesPath, "build.json"),
|
|
2767
|
+
buildJsonContent,
|
|
2768
|
+
);
|
|
2769
|
+
|
|
2770
|
+
// Only codesign/notarize when building macOS targets on macOS host
|
|
2771
|
+
const shouldCodesign =
|
|
2772
|
+
buildEnvironment !== "dev" &&
|
|
2773
|
+
targetOS === "macos" &&
|
|
2774
|
+
OS === "macos" &&
|
|
2775
|
+
config.build.mac.codesign;
|
|
2776
|
+
const shouldNotarize = shouldCodesign && config.build.mac.notarize;
|
|
2777
|
+
|
|
2778
|
+
if (shouldCodesign) {
|
|
2779
|
+
codesignAppBundle(
|
|
2780
|
+
appBundleFolderPath,
|
|
2781
|
+
join(buildFolder, "entitlements.plist"),
|
|
2782
|
+
config,
|
|
2783
|
+
);
|
|
2784
|
+
} else {
|
|
2785
|
+
console.log("skipping codesign");
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
// codesign
|
|
2789
|
+
// NOTE: Codesigning fails in dev mode (when using a single-file-executable bun cli as the launcher)
|
|
2790
|
+
// see https://github.com/oven-sh/bun/issues/7208
|
|
2791
|
+
if (shouldNotarize) {
|
|
2792
|
+
notarizeAndStaple(appBundleFolderPath, config);
|
|
2793
|
+
} else {
|
|
2794
|
+
console.log("skipping notarization");
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
const artifactsToUpload = [];
|
|
2798
|
+
|
|
2799
|
+
// Linux bundle preparation (skip tar creation for dev environment)
|
|
2800
|
+
// For Linux, the app bundle is already in the correct directory structure
|
|
2801
|
+
// The tar will be created in the common code path below
|
|
2802
|
+
|
|
2803
|
+
if (buildEnvironment !== "dev") {
|
|
2804
|
+
// Archive compression (gzip via Bun)
|
|
2805
|
+
// tar https://github.com/isaacs/node-tar
|
|
2806
|
+
|
|
2807
|
+
// steps:
|
|
2808
|
+
// 1. [done] build the app bundle, code sign, notarize, staple.
|
|
2809
|
+
// 2. tar and gzip the app bundle
|
|
2810
|
+
// 3. build another app bundle for the self-extracting app bundle with the archive in Resources
|
|
2811
|
+
// 4. code sign and notarize the self-extracting app bundle
|
|
2812
|
+
// 5. while waiting for that notarization, download the prev app bundle, extract the tar, and generate a bsdiff patch
|
|
2813
|
+
// 6. when notarization is complete, generate a dmg of the self-extracting app bundle
|
|
2814
|
+
// 6.5. code sign and notarize the dmg
|
|
2815
|
+
// 7. copy artifacts to directory [self-extractor dmg, gzip app bundle, bsdiff patch, update.json]
|
|
2816
|
+
|
|
2817
|
+
// Platform suffix is only used for folder names, not file names
|
|
2818
|
+
const platformSuffix = `-${targetOS}-${targetARCH}`;
|
|
2819
|
+
// Use sanitized appFileName for tarball path (URL-safe), but tar content uses actual bundle folder name
|
|
2820
|
+
const tarPath = join(
|
|
2821
|
+
buildFolder,
|
|
2822
|
+
`${appFileName}${targetOS === "macos" ? ".app" : ""}.tar`,
|
|
2823
|
+
);
|
|
2824
|
+
|
|
2825
|
+
// Tar the app bundle for all platforms
|
|
2826
|
+
createTar(tarPath, buildFolder, [basename(appBundleFolderPath)]);
|
|
2827
|
+
|
|
2828
|
+
// Build .deb before deleting the app bundle (needs the full directory tree)
|
|
2829
|
+
if (targetOS === "linux" && config.build?.linux?.createDeb) {
|
|
2830
|
+
const debPath = await createDebPackage(
|
|
2831
|
+
buildFolder,
|
|
2832
|
+
appBundleFolderPath,
|
|
2833
|
+
config,
|
|
2834
|
+
targetARCH,
|
|
2835
|
+
projectRoot,
|
|
2836
|
+
);
|
|
2837
|
+
artifactsToUpload.push(debPath);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// This branch only runs for non-dev release packaging, so the temp app bundle
|
|
2841
|
+
// can always be removed after the tarball is produced.
|
|
2842
|
+
rmSync(appBundleFolderPath, { recursive: true });
|
|
2843
|
+
|
|
2844
|
+
// generate bsdiff
|
|
2845
|
+
// https://storage.googleapis.com/eggbun-static/sparkbun-playground/canary/SparkBunPlayground-canary.app.tar.gz
|
|
2846
|
+
console.log("baseUrl: ", config.release.baseUrl);
|
|
2847
|
+
|
|
2848
|
+
console.log("generating a patch from the previous version...");
|
|
2849
|
+
|
|
2850
|
+
// Skip patch generation if disabled
|
|
2851
|
+
if (config.release.generatePatch === false) {
|
|
2852
|
+
console.log(
|
|
2853
|
+
"Patch generation disabled (release.generatePatch = false)",
|
|
2854
|
+
);
|
|
2855
|
+
} else if (
|
|
2856
|
+
!config.release.baseUrl ||
|
|
2857
|
+
config.release.baseUrl.trim() === ""
|
|
2858
|
+
) {
|
|
2859
|
+
console.log("No baseUrl configured, skipping patch generation");
|
|
2860
|
+
console.log(
|
|
2861
|
+
"To enable patch generation, configure baseUrl in your sparkbun.config",
|
|
2862
|
+
);
|
|
2863
|
+
} else {
|
|
2864
|
+
const urlToPrevUpdateJson = `${config.release.baseUrl.replace(/\/+$/, "")}/${platformPrefix}-update.json`;
|
|
2865
|
+
const cacheBuster = Math.random().toString(36).substring(7);
|
|
2866
|
+
const updateJsonResponse = await fetch(
|
|
2867
|
+
urlToPrevUpdateJson + `?${cacheBuster}`,
|
|
2868
|
+
).catch((err) => {
|
|
2869
|
+
console.log("baseUrl not found: ", err);
|
|
2870
|
+
});
|
|
2871
|
+
|
|
2872
|
+
const tarballFileName = getTarballFileName(appFileName, OS);
|
|
2873
|
+
const urlToLatestTarball = `${config.release.baseUrl.replace(/\/+$/, "")}/${platformPrefix}-${tarballFileName}`;
|
|
2874
|
+
|
|
2875
|
+
// attempt to get the previous version to create a patch file
|
|
2876
|
+
if (updateJsonResponse && updateJsonResponse.ok) {
|
|
2877
|
+
const prevUpdateJson = await updateJsonResponse!.json();
|
|
2878
|
+
|
|
2879
|
+
const prevHash = prevUpdateJson.hash;
|
|
2880
|
+
console.log("PREVIOUS HASH", prevHash);
|
|
2881
|
+
|
|
2882
|
+
|
|
2883
|
+
const response = await fetch(urlToLatestTarball + `?${cacheBuster}`);
|
|
2884
|
+
const prevVersionCompressedTarballPath = join(
|
|
2885
|
+
buildFolder,
|
|
2886
|
+
"prev.tar.gz",
|
|
2887
|
+
);
|
|
2888
|
+
|
|
2889
|
+
if (response && response.ok && response.body) {
|
|
2890
|
+
const reader = response.body.getReader();
|
|
2891
|
+
const totalBytesHeader = response.headers.get("content-length");
|
|
2892
|
+
const totalBytes = totalBytesHeader
|
|
2893
|
+
? Number(totalBytesHeader)
|
|
2894
|
+
: undefined;
|
|
2895
|
+
let downloadedBytes = 0;
|
|
2896
|
+
let lastLogTime = Date.now();
|
|
2897
|
+
const logIntervalMs = 5_000;
|
|
2898
|
+
|
|
2899
|
+
const writer = Bun.file(prevVersionCompressedTarballPath).writer();
|
|
2900
|
+
|
|
2901
|
+
while (true) {
|
|
2902
|
+
const { done, value } = await reader.read();
|
|
2903
|
+
if (done) break;
|
|
2904
|
+
downloadedBytes += value.length;
|
|
2905
|
+
const now = Date.now();
|
|
2906
|
+
if (now - lastLogTime >= logIntervalMs) {
|
|
2907
|
+
if (totalBytes && Number.isFinite(totalBytes)) {
|
|
2908
|
+
const percent = (
|
|
2909
|
+
(downloadedBytes / totalBytes) *
|
|
2910
|
+
100
|
|
2911
|
+
).toFixed(1);
|
|
2912
|
+
console.log(
|
|
2913
|
+
`Downloading previous version... ${percent}% (${downloadedBytes}/${totalBytes} bytes)`,
|
|
2914
|
+
);
|
|
2915
|
+
} else {
|
|
2916
|
+
console.log(
|
|
2917
|
+
`Downloading previous version... ${downloadedBytes} bytes`,
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
lastLogTime = now;
|
|
2921
|
+
}
|
|
2922
|
+
await writer.write(value);
|
|
2923
|
+
}
|
|
2924
|
+
await writer.flush();
|
|
2925
|
+
writer.end();
|
|
2926
|
+
|
|
2927
|
+
console.log("decompress prev bundle...");
|
|
2928
|
+
const prevTarballPath = join(buildFolder, "prev.tar");
|
|
2929
|
+
let canGeneratePatch = true;
|
|
2930
|
+
|
|
2931
|
+
try {
|
|
2932
|
+
const compressed = readFileSync(prevVersionCompressedTarballPath);
|
|
2933
|
+
const decompressed = Bun.gunzipSync(compressed);
|
|
2934
|
+
writeFileSync(prevTarballPath, decompressed);
|
|
2935
|
+
} catch (err) {
|
|
2936
|
+
console.log(
|
|
2937
|
+
`Failed to decompress previous tarball: ${err}, skipping patch generation`,
|
|
2938
|
+
);
|
|
2939
|
+
canGeneratePatch = false;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
if (existsSync(prevVersionCompressedTarballPath)) {
|
|
2943
|
+
unlinkSync(prevVersionCompressedTarballPath);
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
if (canGeneratePatch) {
|
|
2947
|
+
console.log("diff previous and new tarballs...");
|
|
2948
|
+
// Run it as a separate process to leverage multi-threadedness
|
|
2949
|
+
// especially for creating multiple diffs in parallel
|
|
2950
|
+
const bsdiffpath = targetPaths.BSDIFF;
|
|
2951
|
+
const patchFilePath = join(buildFolder, `${prevHash}.patch`);
|
|
2952
|
+
const result = Bun.spawnSync(
|
|
2953
|
+
[
|
|
2954
|
+
bsdiffpath,
|
|
2955
|
+
prevTarballPath,
|
|
2956
|
+
tarPath,
|
|
2957
|
+
patchFilePath,
|
|
2958
|
+
],
|
|
2959
|
+
{
|
|
2960
|
+
cwd: buildFolder,
|
|
2961
|
+
stdout: "inherit",
|
|
2962
|
+
stderr: "inherit",
|
|
2963
|
+
},
|
|
2964
|
+
);
|
|
2965
|
+
if (!result.success) {
|
|
2966
|
+
// Patch generation is non-critical - users will just download full updates instead of delta patches
|
|
2967
|
+
console.error("\n" + "=".repeat(80));
|
|
2968
|
+
console.error(
|
|
2969
|
+
"WARNING: Patch generation failed (exit code " +
|
|
2970
|
+
result.exitCode +
|
|
2971
|
+
")",
|
|
2972
|
+
);
|
|
2973
|
+
console.error(
|
|
2974
|
+
"Delta updates will not be available for this release.",
|
|
2975
|
+
);
|
|
2976
|
+
console.error("Users will download the full update instead.");
|
|
2977
|
+
console.error("=".repeat(80) + "\n");
|
|
2978
|
+
} else {
|
|
2979
|
+
// Only add patch to artifacts if it was successfully created
|
|
2980
|
+
artifactsToUpload.push(patchFilePath);
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
// Clean up previous tarball now that bsdiff is done
|
|
2984
|
+
if (existsSync(prevTarballPath)) {
|
|
2985
|
+
unlinkSync(prevTarballPath);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
} else {
|
|
2989
|
+
console.log(
|
|
2990
|
+
"Failed to fetch previous tarball, skipping patch generation",
|
|
2991
|
+
);
|
|
2992
|
+
}
|
|
2993
|
+
} else {
|
|
2994
|
+
console.log("prevoius version not found at: ", urlToLatestTarball);
|
|
2995
|
+
console.log("skipping diff generation");
|
|
2996
|
+
}
|
|
2997
|
+
} // End of baseUrl validation block
|
|
2998
|
+
|
|
2999
|
+
let compressedTarPath = `${tarPath}.gz`;
|
|
3000
|
+
|
|
3001
|
+
{
|
|
3002
|
+
console.log("compressing tarball with gzip...");
|
|
3003
|
+
const tarBytes = readFileSync(tarPath);
|
|
3004
|
+
const gzipped = Bun.gzipSync(tarBytes);
|
|
3005
|
+
writeFileSync(compressedTarPath, gzipped);
|
|
3006
|
+
artifactsToUpload.push(compressedTarPath);
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
// Remove the uncompressed tar now that compression and diffing are done.
|
|
3010
|
+
if (existsSync(tarPath)) {
|
|
3011
|
+
unlinkSync(tarPath);
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
const selfExtractingBundle = createAppBundle(
|
|
3015
|
+
bundleName,
|
|
3016
|
+
buildFolder,
|
|
3017
|
+
targetOS,
|
|
3018
|
+
);
|
|
3019
|
+
const compressedTarballInExtractingBundlePath = join(
|
|
3020
|
+
selfExtractingBundle.appBundleFolderResourcesPath,
|
|
3021
|
+
`${hash}.tar.gz`,
|
|
3022
|
+
);
|
|
3023
|
+
|
|
3024
|
+
// copy the gzip tarball to the self-extracting app bundle
|
|
3025
|
+
cpSync(compressedTarPath, compressedTarballInExtractingBundlePath, {
|
|
3026
|
+
dereference: true,
|
|
3027
|
+
});
|
|
3028
|
+
|
|
3029
|
+
// macOS uses the Zig extractor in the self-extracting .app bundle.
|
|
3030
|
+
// Windows/Linux use createCompiledInstaller instead.
|
|
3031
|
+
if (targetOS === "macos") {
|
|
3032
|
+
const selfExtractorBinSourcePath = targetPaths.EXTRACTOR;
|
|
3033
|
+
const selfExtractorBinDestinationPath = join(
|
|
3034
|
+
selfExtractingBundle.appBundleMacOSPath,
|
|
3035
|
+
config.app.name.replace(/ /g, ""),
|
|
3036
|
+
);
|
|
3037
|
+
|
|
3038
|
+
cpSync(selfExtractorBinSourcePath, selfExtractorBinDestinationPath, {
|
|
3039
|
+
dereference: true,
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
buildIcons(
|
|
3044
|
+
selfExtractingBundle.appBundleFolderResourcesPath,
|
|
3045
|
+
selfExtractingBundle.appBundleFolderPath,
|
|
3046
|
+
);
|
|
3047
|
+
await Bun.write(
|
|
3048
|
+
join(selfExtractingBundle.appBundleFolderContentsPath, "Info.plist"),
|
|
3049
|
+
InfoPlistContents,
|
|
3050
|
+
);
|
|
3051
|
+
|
|
3052
|
+
// Write metadata.json to outer bundle (consistent with Windows/Linux)
|
|
3053
|
+
const extractorMetadata = {
|
|
3054
|
+
identifier: config.app.identifier,
|
|
3055
|
+
name: config.app.name,
|
|
3056
|
+
channel: buildEnvironment,
|
|
3057
|
+
hash: hash,
|
|
3058
|
+
};
|
|
3059
|
+
await Bun.write(
|
|
3060
|
+
join(
|
|
3061
|
+
selfExtractingBundle.appBundleFolderResourcesPath,
|
|
3062
|
+
"metadata.json",
|
|
3063
|
+
),
|
|
3064
|
+
JSON.stringify(extractorMetadata, null, 2),
|
|
3065
|
+
);
|
|
3066
|
+
|
|
3067
|
+
// Run postWrap hook after self-extracting bundle is created, before code signing
|
|
3068
|
+
// This is where you can add files to the wrapper (e.g., for liquid glass support)
|
|
3069
|
+
runHook("postWrap", {
|
|
3070
|
+
SPARKBUN_WRAPPER_BUNDLE_PATH:
|
|
3071
|
+
selfExtractingBundle.appBundleFolderPath,
|
|
3072
|
+
});
|
|
3073
|
+
|
|
3074
|
+
if (shouldCodesign) {
|
|
3075
|
+
codesignAppBundle(
|
|
3076
|
+
selfExtractingBundle.appBundleFolderPath,
|
|
3077
|
+
join(buildFolder, "entitlements.plist"),
|
|
3078
|
+
config,
|
|
3079
|
+
);
|
|
3080
|
+
} else {
|
|
3081
|
+
console.log("skipping codesign");
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
// Note: we need to notarize the original app bundle, the self-extracting app bundle, and the dmg
|
|
3085
|
+
if (shouldNotarize) {
|
|
3086
|
+
notarizeAndStaple(selfExtractingBundle.appBundleFolderPath, config);
|
|
3087
|
+
} else {
|
|
3088
|
+
console.log("skipping notarization");
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
// DMG creation for macOS only
|
|
3092
|
+
if (targetOS === "macos" && config.build.mac?.createDmg !== false) {
|
|
3093
|
+
console.log("creating dmg...");
|
|
3094
|
+
const finalDmgPath = join(buildFolder, `${appFileName}.dmg`);
|
|
3095
|
+
// NOTE: For some ungodly reason using the bare name in CI can conflict with some mysterious
|
|
3096
|
+
// already mounted volume. I suspect the sanitized appFileName can match your github repo
|
|
3097
|
+
// or some other tool is mounting something somewhere. Either way, as a workaround
|
|
3098
|
+
// while creating the dmg for a stable build we temporarily give it a -stable suffix
|
|
3099
|
+
// to match the behaviour of -canary builds.
|
|
3100
|
+
const dmgCreationPath =
|
|
3101
|
+
buildEnvironment === "stable"
|
|
3102
|
+
? join(buildFolder, `${appFileName}-stable.dmg`)
|
|
3103
|
+
: finalDmgPath;
|
|
3104
|
+
const dmgVolumeName = getDmgVolumeName(
|
|
3105
|
+
config.app.name,
|
|
3106
|
+
buildEnvironment,
|
|
3107
|
+
);
|
|
3108
|
+
|
|
3109
|
+
// Create a staging directory for DMG contents (app + Applications shortcut)
|
|
3110
|
+
const dmgStagingDir = join(buildFolder, ".dmg-staging");
|
|
3111
|
+
if (existsSync(dmgStagingDir)) {
|
|
3112
|
+
rmSync(dmgStagingDir, { recursive: true });
|
|
3113
|
+
}
|
|
3114
|
+
mkdirSync(dmgStagingDir, { recursive: true });
|
|
3115
|
+
try {
|
|
3116
|
+
// Copy the app bundle to the staging directory
|
|
3117
|
+
const stagedAppPath = join(
|
|
3118
|
+
dmgStagingDir,
|
|
3119
|
+
basename(selfExtractingBundle.appBundleFolderPath),
|
|
3120
|
+
);
|
|
3121
|
+
execSync(
|
|
3122
|
+
`cp -R ${escapePathForTerminal(selfExtractingBundle.appBundleFolderPath)} ${escapePathForTerminal(stagedAppPath)}`,
|
|
3123
|
+
);
|
|
3124
|
+
|
|
3125
|
+
// Create a symlink to /Applications for easy drag-and-drop installation
|
|
3126
|
+
const applicationsLink = join(dmgStagingDir, "Applications");
|
|
3127
|
+
symlinkSync("/Applications", applicationsLink);
|
|
3128
|
+
|
|
3129
|
+
// hdiutil create -volname "YourAppName" -srcfolder /path/to/staging -ov -format UDZO YourAppName.dmg
|
|
3130
|
+
// Note: use ULFO (lzfse) for better compatibility with large CEF frameworks and modern macOS
|
|
3131
|
+
execSync(
|
|
3132
|
+
`hdiutil create -volname "${dmgVolumeName}" -srcfolder ${escapePathForTerminal(
|
|
3133
|
+
dmgStagingDir,
|
|
3134
|
+
)} -ov -format ULFO ${escapePathForTerminal(dmgCreationPath)}`,
|
|
3135
|
+
);
|
|
3136
|
+
|
|
3137
|
+
if (
|
|
3138
|
+
buildEnvironment === "stable" &&
|
|
3139
|
+
dmgCreationPath !== finalDmgPath
|
|
3140
|
+
) {
|
|
3141
|
+
renameSync(dmgCreationPath, finalDmgPath);
|
|
3142
|
+
}
|
|
3143
|
+
artifactsToUpload.push(finalDmgPath);
|
|
3144
|
+
|
|
3145
|
+
if (shouldCodesign) {
|
|
3146
|
+
codesignAppBundle(finalDmgPath, undefined, config);
|
|
3147
|
+
} else {
|
|
3148
|
+
console.log("skipping codesign");
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
if (shouldNotarize) {
|
|
3152
|
+
notarizeAndStaple(finalDmgPath, config);
|
|
3153
|
+
} else {
|
|
3154
|
+
console.log("skipping notarization");
|
|
3155
|
+
}
|
|
3156
|
+
} finally {
|
|
3157
|
+
if (existsSync(dmgStagingDir)) {
|
|
3158
|
+
rmSync(dmgStagingDir, { recursive: true });
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
} else {
|
|
3162
|
+
if (targetOS === "macos") {
|
|
3163
|
+
console.log("skipping dmg");
|
|
3164
|
+
}
|
|
3165
|
+
// For Windows and Linux, add the self-extracting bundle directly
|
|
3166
|
+
// @ts-expect-error - reserved for future use
|
|
3167
|
+
const _platformBundlePath = join(
|
|
3168
|
+
buildFolder,
|
|
3169
|
+
`${appFileName}${platformSuffix}${targetOS === "win" ? ".exe" : ""}`,
|
|
3170
|
+
);
|
|
3171
|
+
// Copy the self-extracting bundle to platform-specific filename
|
|
3172
|
+
if (targetOS === "win" || targetOS === "linux") {
|
|
3173
|
+
const installerPath = await createCompiledInstaller(
|
|
3174
|
+
buildFolder,
|
|
3175
|
+
compressedTarPath,
|
|
3176
|
+
appFileName,
|
|
3177
|
+
targetPaths,
|
|
3178
|
+
buildEnvironment,
|
|
3179
|
+
hash,
|
|
3180
|
+
config,
|
|
3181
|
+
projectRoot,
|
|
3182
|
+
);
|
|
3183
|
+
artifactsToUpload.push(installerPath);
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// refresh artifacts folder
|
|
3189
|
+
console.log("creating artifacts folder...");
|
|
3190
|
+
if (existsSync(artifactFolder)) {
|
|
3191
|
+
console.info("deleting artifact folder: ", artifactFolder);
|
|
3192
|
+
rmSync(artifactFolder, { recursive: true });
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
mkdirSync(artifactFolder, { recursive: true });
|
|
3196
|
+
|
|
3197
|
+
console.log("creating update.json...");
|
|
3198
|
+
// update.json for the channel in that channel's build folder
|
|
3199
|
+
const updateJsonContent = JSON.stringify({
|
|
3200
|
+
// The version isn't really used for updating, but it's nice to have for
|
|
3201
|
+
// the download button or display on your marketing site or in the app.
|
|
3202
|
+
version: config.app.version,
|
|
3203
|
+
hash: hash.toString(),
|
|
3204
|
+
platform: OS,
|
|
3205
|
+
arch: ARCH,
|
|
3206
|
+
// channel: buildEnvironment,
|
|
3207
|
+
// baseUrl: config.release.baseUrl
|
|
3208
|
+
});
|
|
3209
|
+
|
|
3210
|
+
// update.json with platform prefix for flat naming structure
|
|
3211
|
+
await Bun.write(
|
|
3212
|
+
join(artifactFolder, `${platformPrefix}-update.json`),
|
|
3213
|
+
updateJsonContent,
|
|
3214
|
+
);
|
|
3215
|
+
|
|
3216
|
+
// compress all the upload files
|
|
3217
|
+
console.log("moving artifacts...");
|
|
3218
|
+
|
|
3219
|
+
artifactsToUpload.forEach((filePath) => {
|
|
3220
|
+
const filename = basename(filePath);
|
|
3221
|
+
const usePrefix = !filename.endsWith(".deb");
|
|
3222
|
+
const destination = join(
|
|
3223
|
+
artifactFolder,
|
|
3224
|
+
usePrefix ? `${platformPrefix}-${filename}` : filename,
|
|
3225
|
+
);
|
|
3226
|
+
try {
|
|
3227
|
+
renameSync(filePath, destination);
|
|
3228
|
+
} catch {
|
|
3229
|
+
cpSync(filePath, destination, { dereference: true });
|
|
3230
|
+
if (existsSync(filePath)) {
|
|
3231
|
+
unlinkSync(filePath);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
});
|
|
3235
|
+
|
|
3236
|
+
// todo: now just upload the artifacts to your bucket replacing the ones that exist
|
|
3237
|
+
// you'll end up with a sequence of patch files that will
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
// Run postPackage hook at the very end of the build process
|
|
3241
|
+
runHook("postPackage");
|
|
3242
|
+
|
|
3243
|
+
// NOTE: verify codesign
|
|
3244
|
+
// codesign --verify --deep --strict --verbose=2 <app path>
|
|
3245
|
+
|
|
3246
|
+
// Note: verify notarization
|
|
3247
|
+
// spctl --assess --type execute --verbose <app path>
|
|
3248
|
+
|
|
3249
|
+
// Note: for .dmg spctl --assess will respond with "rejected (*the code is valid* but does not seem to be an app)" which is valid
|
|
3250
|
+
// an actual failed response for a dmg is "source=no usable signature"
|
|
3251
|
+
// for a dmg.
|
|
3252
|
+
// can also use stapler validate -v to validate the dmg and look for teamId, signingId, and the response signedTicket
|
|
3253
|
+
// stapler validate -v <app path>
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
// Take over as the terminal's foreground process group (macOS/Linux).
|
|
3257
|
+
// This prevents the parent bun script runner from receiving SIGINT
|
|
3258
|
+
// when Ctrl+C is pressed, keeping the terminal busy until the app
|
|
3259
|
+
// finishes shutting down gracefully.
|
|
3260
|
+
// Call once per CLI session — returns a restore function.
|
|
3261
|
+
async function takeoverForeground(): Promise<() => void> {
|
|
3262
|
+
let restoreFn = () => {};
|
|
3263
|
+
if (OS === "win") return restoreFn;
|
|
3264
|
+
try {
|
|
3265
|
+
const { dlopen, ptr } = await import("bun:ffi");
|
|
3266
|
+
const libName = OS === "macos" ? "libSystem.B.dylib" : "libc.so.6";
|
|
3267
|
+
const libc = dlopen(libName, {
|
|
3268
|
+
open: { args: ["ptr", "i32"], returns: "i32" },
|
|
3269
|
+
close: { args: ["i32"], returns: "i32" },
|
|
3270
|
+
getpid: { args: [], returns: "i32" },
|
|
3271
|
+
setpgid: { args: ["i32", "i32"], returns: "i32" },
|
|
3272
|
+
tcgetpgrp: { args: ["i32"], returns: "i32" },
|
|
3273
|
+
tcsetpgrp: { args: ["i32", "i32"], returns: "i32" },
|
|
3274
|
+
signal: { args: ["i32", "ptr"], returns: "ptr" },
|
|
3275
|
+
});
|
|
3276
|
+
|
|
3277
|
+
const ttyPathBuf = new Uint8Array(Buffer.from("/dev/tty\0"));
|
|
3278
|
+
const ttyFd = libc.symbols.open(ptr(ttyPathBuf), 2); // O_RDWR
|
|
3279
|
+
|
|
3280
|
+
if (ttyFd >= 0) {
|
|
3281
|
+
const originalPgid = libc.symbols.tcgetpgrp(ttyFd);
|
|
3282
|
+
if (originalPgid >= 0) {
|
|
3283
|
+
// Ignore SIGTTOU at C level so tcsetpgrp works from background group.
|
|
3284
|
+
// bun's process.on("SIGTTOU") doesn't set the C-level disposition.
|
|
3285
|
+
// SIG_IGN = (void(*)(int))1, SIGTTOU = 22 on macOS/Linux
|
|
3286
|
+
libc.symbols.signal(22, 1);
|
|
3287
|
+
|
|
3288
|
+
if (libc.symbols.setpgid(0, 0) === 0) {
|
|
3289
|
+
const myPid = libc.symbols.getpid();
|
|
3290
|
+
if (libc.symbols.tcsetpgrp(ttyFd, myPid) === 0) {
|
|
3291
|
+
restoreFn = () => {
|
|
3292
|
+
try {
|
|
3293
|
+
libc.symbols.signal(22, 1);
|
|
3294
|
+
libc.symbols.tcsetpgrp(ttyFd, originalPgid);
|
|
3295
|
+
libc.symbols.close(ttyFd);
|
|
3296
|
+
} catch {}
|
|
3297
|
+
};
|
|
3298
|
+
} else {
|
|
3299
|
+
libc.symbols.setpgid(0, originalPgid);
|
|
3300
|
+
libc.symbols.close(ttyFd);
|
|
3301
|
+
}
|
|
3302
|
+
} else {
|
|
3303
|
+
libc.symbols.close(ttyFd);
|
|
3304
|
+
}
|
|
3305
|
+
} else {
|
|
3306
|
+
libc.symbols.close(ttyFd);
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
} catch {
|
|
3310
|
+
// Fall back to default behavior (prompt may return early on Ctrl+C)
|
|
3311
|
+
}
|
|
3312
|
+
return restoreFn;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
async function runApp(
|
|
3316
|
+
config: Awaited<ReturnType<typeof getConfig>>,
|
|
3317
|
+
options?: { onExit?: () => void },
|
|
3318
|
+
): Promise<{ kill: () => void; exited: Promise<number> }> {
|
|
3319
|
+
// Launch the already-built dev bundle
|
|
3320
|
+
|
|
3321
|
+
const buildEnvironment = "dev";
|
|
3322
|
+
const appFileName = getAppFileName(config.app.name, buildEnvironment);
|
|
3323
|
+
const macOSBundleDisplayName = getMacOSBundleDisplayName(
|
|
3324
|
+
config.app.name,
|
|
3325
|
+
buildEnvironment,
|
|
3326
|
+
);
|
|
3327
|
+
const buildSubFolder = `${buildEnvironment}-${OS}-${ARCH}`;
|
|
3328
|
+
const buildFolder = join(
|
|
3329
|
+
projectRoot,
|
|
3330
|
+
config.build.buildFolder,
|
|
3331
|
+
buildSubFolder,
|
|
3332
|
+
);
|
|
3333
|
+
const bundleFileName =
|
|
3334
|
+
OS === "macos" ? `${macOSBundleDisplayName}.app` : appFileName;
|
|
3335
|
+
|
|
3336
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3337
|
+
let mainProc: any;
|
|
3338
|
+
let bundleExecPath: string;
|
|
3339
|
+
// @ts-expect-error - reserved for future use
|
|
3340
|
+
let _bundleResourcesPath: string;
|
|
3341
|
+
if (OS === "macos") {
|
|
3342
|
+
bundleExecPath = join(buildFolder, bundleFileName, "Contents", "MacOS");
|
|
3343
|
+
_bundleResourcesPath = join(
|
|
3344
|
+
buildFolder,
|
|
3345
|
+
bundleFileName,
|
|
3346
|
+
"Contents",
|
|
3347
|
+
"Resources",
|
|
3348
|
+
);
|
|
3349
|
+
} else if (OS === "linux") {
|
|
3350
|
+
bundleExecPath = join(buildFolder, bundleFileName, "bin");
|
|
3351
|
+
_bundleResourcesPath = join(buildFolder, bundleFileName, "Resources");
|
|
3352
|
+
} else if (OS === "win") {
|
|
3353
|
+
bundleExecPath = join(buildFolder, bundleFileName, "bin");
|
|
3354
|
+
_bundleResourcesPath = join(buildFolder, bundleFileName, "Resources");
|
|
3355
|
+
} else {
|
|
3356
|
+
throw new Error(`Unsupported OS: ${OS}`);
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
if (OS === "macos" || OS === "linux") {
|
|
3360
|
+
// For Linux dev mode, update libNativeWrapper.so based on bundleCEF setting
|
|
3361
|
+
if (OS === "linux") {
|
|
3362
|
+
const currentLibPath = join(bundleExecPath, "libNativeWrapper.so");
|
|
3363
|
+
const targetPaths = getPlatformPaths("linux", ARCH);
|
|
3364
|
+
const correctLibSource = config.build.linux?.bundleCEF
|
|
3365
|
+
? targetPaths.NATIVE_WRAPPER_LINUX_CEF
|
|
3366
|
+
: targetPaths.NATIVE_WRAPPER_LINUX;
|
|
3367
|
+
|
|
3368
|
+
if (existsSync(correctLibSource)) {
|
|
3369
|
+
try {
|
|
3370
|
+
cpSync(correctLibSource, currentLibPath, { dereference: true });
|
|
3371
|
+
console.log(
|
|
3372
|
+
`Updated libNativeWrapper.so for ${config.build.linux?.bundleCEF ? "CEF (with weak linking)" : "GTK-only"} mode`,
|
|
3373
|
+
);
|
|
3374
|
+
} catch (error) {
|
|
3375
|
+
console.warn("Failed to update libNativeWrapper.so:", error);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
mainProc = Bun.spawn([join(bundleExecPath, config.app.name.replace(/ /g, ""))], {
|
|
3381
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
3382
|
+
cwd: bundleExecPath,
|
|
3383
|
+
});
|
|
3384
|
+
} else if (OS === "win") {
|
|
3385
|
+
mainProc = Bun.spawn([join(bundleExecPath, "launcher.exe")], {
|
|
3386
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
3387
|
+
cwd: bundleExecPath,
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
if (!mainProc) {
|
|
3392
|
+
throw new Error("Failed to spawn app process");
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
const exitedPromise = mainProc.exited.then((code: number) => {
|
|
3396
|
+
options?.onExit?.();
|
|
3397
|
+
return code ?? 0;
|
|
3398
|
+
});
|
|
3399
|
+
|
|
3400
|
+
return {
|
|
3401
|
+
kill: () => {
|
|
3402
|
+
try {
|
|
3403
|
+
mainProc.kill();
|
|
3404
|
+
} catch {}
|
|
3405
|
+
},
|
|
3406
|
+
exited: exitedPromise,
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
async function runAppWithSignalHandling(
|
|
3411
|
+
config: Awaited<ReturnType<typeof getConfig>>,
|
|
3412
|
+
) {
|
|
3413
|
+
const restoreForeground = await takeoverForeground();
|
|
3414
|
+
const handle = await runApp(config);
|
|
3415
|
+
|
|
3416
|
+
let sigintCount = 0;
|
|
3417
|
+
process.on("SIGINT", () => {
|
|
3418
|
+
sigintCount++;
|
|
3419
|
+
if (sigintCount === 1) {
|
|
3420
|
+
console.log(
|
|
3421
|
+
"\n[sparkbun dev] Shutting down gracefully... (press Ctrl+C again to force quit)",
|
|
3422
|
+
);
|
|
3423
|
+
} else {
|
|
3424
|
+
console.log("\n[sparkbun dev] Force quitting...");
|
|
3425
|
+
try {
|
|
3426
|
+
process.kill(0, "SIGKILL");
|
|
3427
|
+
} catch {}
|
|
3428
|
+
process.exit(0);
|
|
3429
|
+
}
|
|
3430
|
+
});
|
|
3431
|
+
|
|
3432
|
+
const code = await handle.exited;
|
|
3433
|
+
restoreForeground();
|
|
3434
|
+
process.exit(code);
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
async function runDevWatch(config: Awaited<ReturnType<typeof getConfig>>) {
|
|
3438
|
+
const { watch } = await import("fs");
|
|
3439
|
+
|
|
3440
|
+
// Collect watch directories from config entrypoints
|
|
3441
|
+
const watchDirs = new Set<string>();
|
|
3442
|
+
|
|
3443
|
+
// Bun entrypoint directory
|
|
3444
|
+
if (config.build.bun?.entrypoint) {
|
|
3445
|
+
watchDirs.add(join(projectRoot, dirname(config.build.bun.entrypoint)));
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
// View entrypoint directories
|
|
3449
|
+
if (config.build.views) {
|
|
3450
|
+
for (const viewConfig of Object.values(config.build.views)) {
|
|
3451
|
+
if (viewConfig.entrypoint) {
|
|
3452
|
+
watchDirs.add(join(projectRoot, dirname(viewConfig.entrypoint)));
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
// Copy source directories
|
|
3458
|
+
if (config.build.copy) {
|
|
3459
|
+
for (const src of Object.keys(config.build.copy)) {
|
|
3460
|
+
const srcPath = join(projectRoot, src);
|
|
3461
|
+
try {
|
|
3462
|
+
const stat = statSync(srcPath);
|
|
3463
|
+
watchDirs.add(stat.isDirectory() ? srcPath : dirname(srcPath));
|
|
3464
|
+
} catch {
|
|
3465
|
+
watchDirs.add(dirname(srcPath));
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// User-specified additional watch paths
|
|
3471
|
+
if (config.build.watch) {
|
|
3472
|
+
for (const entry of config.build.watch) {
|
|
3473
|
+
const entryPath = join(projectRoot, entry);
|
|
3474
|
+
try {
|
|
3475
|
+
const stat = statSync(entryPath);
|
|
3476
|
+
watchDirs.add(stat.isDirectory() ? entryPath : dirname(entryPath));
|
|
3477
|
+
} catch {
|
|
3478
|
+
// Path doesn't exist yet — watch its parent directory
|
|
3479
|
+
watchDirs.add(dirname(entryPath));
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
// Deduplicate overlapping directories (remove children if parent is watched)
|
|
3485
|
+
const sortedDirs = [...watchDirs].sort();
|
|
3486
|
+
const dedupedDirs = sortedDirs.filter((dir, i) => {
|
|
3487
|
+
return !sortedDirs.some(
|
|
3488
|
+
(other, j) => j < i && dir.startsWith(other + "/"),
|
|
3489
|
+
);
|
|
3490
|
+
});
|
|
3491
|
+
|
|
3492
|
+
if (dedupedDirs.length === 0) {
|
|
3493
|
+
console.error(
|
|
3494
|
+
"[sparkbun dev --watch] No directories to watch. Check your config entrypoints.",
|
|
3495
|
+
);
|
|
3496
|
+
process.exit(1);
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
console.log(`
|
|
3500
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
3501
|
+
║ SPARKBUN DEV --watch ║
|
|
3502
|
+
║ Watching ${String(dedupedDirs.length).padEnd(2)} director${dedupedDirs.length === 1 ? "y " : "ies"} ║
|
|
3503
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
3504
|
+
`);
|
|
3505
|
+
for (const dir of dedupedDirs) {
|
|
3506
|
+
console.log(` ${dir}`);
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
// Set up terminal foreground takeover once for the whole session
|
|
3510
|
+
const restoreForeground = await takeoverForeground();
|
|
3511
|
+
|
|
3512
|
+
// Paths to ignore in file watcher (build output, node_modules, artifacts)
|
|
3513
|
+
const buildDir = join(projectRoot, config.build.buildFolder);
|
|
3514
|
+
const artifactDir = join(projectRoot, config.build.artifactFolder);
|
|
3515
|
+
const ignoreDirs = [
|
|
3516
|
+
buildDir,
|
|
3517
|
+
artifactDir,
|
|
3518
|
+
join(projectRoot, "node_modules"),
|
|
3519
|
+
];
|
|
3520
|
+
|
|
3521
|
+
// Compile watchIgnore glob patterns
|
|
3522
|
+
const ignoreGlobs = (config.build.watchIgnore || []).map(
|
|
3523
|
+
(pattern) => new Bun.Glob(pattern),
|
|
3524
|
+
);
|
|
3525
|
+
|
|
3526
|
+
function shouldIgnore(fullPath: string): boolean {
|
|
3527
|
+
const resolvedFullPath = path.resolve(fullPath);
|
|
3528
|
+
const pathSegments = resolvedFullPath.split(path.sep).filter(Boolean);
|
|
3529
|
+
const genericIgnoredSegments = new Set([
|
|
3530
|
+
"node_modules",
|
|
3531
|
+
path.basename(buildDir),
|
|
3532
|
+
path.basename(artifactDir),
|
|
3533
|
+
".sparkbun-cache",
|
|
3534
|
+
]);
|
|
3535
|
+
if (pathSegments.some((segment) => genericIgnoredSegments.has(segment))) {
|
|
3536
|
+
return true;
|
|
3537
|
+
}
|
|
3538
|
+
// Check built-in ignore dirs
|
|
3539
|
+
if (
|
|
3540
|
+
ignoreDirs.some(
|
|
3541
|
+
(ignored) => {
|
|
3542
|
+
const relativeToIgnored = path.relative(ignored, resolvedFullPath);
|
|
3543
|
+
return (
|
|
3544
|
+
relativeToIgnored === "" ||
|
|
3545
|
+
(!relativeToIgnored.startsWith("..") &&
|
|
3546
|
+
!path.isAbsolute(relativeToIgnored))
|
|
3547
|
+
);
|
|
3548
|
+
},
|
|
3549
|
+
)
|
|
3550
|
+
) {
|
|
3551
|
+
return true;
|
|
3552
|
+
}
|
|
3553
|
+
// Check user-configured watchIgnore globs (match against project-relative path)
|
|
3554
|
+
const relativePath = path
|
|
3555
|
+
.relative(projectRoot, resolvedFullPath)
|
|
3556
|
+
.split(path.sep)
|
|
3557
|
+
.join("/");
|
|
3558
|
+
if (ignoreGlobs.some((glob) => glob.match(relativePath))) {
|
|
3559
|
+
return true;
|
|
3560
|
+
}
|
|
3561
|
+
return false;
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
let appHandle: { kill: () => void; exited: Promise<number> } | null = null;
|
|
3565
|
+
let lastChangedFile = "";
|
|
3566
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
3567
|
+
let shuttingDown = false;
|
|
3568
|
+
let isBuilding = false;
|
|
3569
|
+
let rebuildPending = false;
|
|
3570
|
+
let watchers: ReturnType<typeof watch>[] = [];
|
|
3571
|
+
|
|
3572
|
+
function startWatchers() {
|
|
3573
|
+
for (const dir of dedupedDirs) {
|
|
3574
|
+
const watcher = watch(dir, { recursive: true }, (_event, filename) => {
|
|
3575
|
+
if (shuttingDown) return;
|
|
3576
|
+
|
|
3577
|
+
if (filename) {
|
|
3578
|
+
const fullPath = join(dir, filename);
|
|
3579
|
+
if (shouldIgnore(fullPath)) {
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
lastChangedFile = fullPath;
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3586
|
+
debounceTimer = setTimeout(() => {
|
|
3587
|
+
triggerRebuild();
|
|
3588
|
+
}, 300);
|
|
3589
|
+
});
|
|
3590
|
+
watchers.push(watcher);
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
function stopWatchers() {
|
|
3595
|
+
for (const watcher of watchers) {
|
|
3596
|
+
try { watcher.close(); } catch {}
|
|
3597
|
+
}
|
|
3598
|
+
watchers = [];
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
async function triggerRebuild() {
|
|
3602
|
+
if (shuttingDown) return;
|
|
3603
|
+
|
|
3604
|
+
// Guard against concurrent builds — if already building, mark
|
|
3605
|
+
// that another rebuild is needed and let the current one finish.
|
|
3606
|
+
if (isBuilding) {
|
|
3607
|
+
rebuildPending = true;
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
isBuilding = true;
|
|
3611
|
+
rebuildPending = false;
|
|
3612
|
+
|
|
3613
|
+
// Stop watching during build so build output doesn't trigger more events
|
|
3614
|
+
stopWatchers();
|
|
3615
|
+
|
|
3616
|
+
// Cancel any lingering debounce timer that may have been queued
|
|
3617
|
+
// before stopWatchers took effect.
|
|
3618
|
+
if (debounceTimer) {
|
|
3619
|
+
clearTimeout(debounceTimer);
|
|
3620
|
+
debounceTimer = null;
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
const changedDisplay = lastChangedFile
|
|
3624
|
+
? lastChangedFile.replace(projectRoot + "/", "")
|
|
3625
|
+
: "unknown";
|
|
3626
|
+
console.log(`
|
|
3627
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
3628
|
+
║ FILE CHANGED: ${changedDisplay.padEnd(44)}║
|
|
3629
|
+
║ Rebuilding... ║
|
|
3630
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
3631
|
+
`);
|
|
3632
|
+
|
|
3633
|
+
// Kill running app if any
|
|
3634
|
+
if (appHandle) {
|
|
3635
|
+
appHandle.kill();
|
|
3636
|
+
try {
|
|
3637
|
+
await appHandle.exited;
|
|
3638
|
+
} catch {}
|
|
3639
|
+
appHandle = null;
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
try {
|
|
3643
|
+
await runBuild(config, "dev");
|
|
3644
|
+
console.log(
|
|
3645
|
+
"[sparkbun dev --watch] Build succeeded, launching app...",
|
|
3646
|
+
);
|
|
3647
|
+
|
|
3648
|
+
appHandle = await runApp(config, {
|
|
3649
|
+
onExit: () => {
|
|
3650
|
+
appHandle = null;
|
|
3651
|
+
},
|
|
3652
|
+
});
|
|
3653
|
+
} catch (error) {
|
|
3654
|
+
console.error("[sparkbun dev --watch] Build failed:", error);
|
|
3655
|
+
console.log("[sparkbun dev --watch] Waiting for file changes...");
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
isBuilding = false;
|
|
3659
|
+
|
|
3660
|
+
// Resume watching after build + hooks are done
|
|
3661
|
+
if (!shuttingDown) {
|
|
3662
|
+
startWatchers();
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
// If a file change came in while we were building, rebuild again.
|
|
3666
|
+
if (rebuildPending && !shuttingDown) {
|
|
3667
|
+
triggerRebuild();
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
function cleanup() {
|
|
3672
|
+
shuttingDown = true;
|
|
3673
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3674
|
+
stopWatchers();
|
|
3675
|
+
if (appHandle) {
|
|
3676
|
+
appHandle.kill();
|
|
3677
|
+
}
|
|
3678
|
+
restoreForeground();
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
// Ctrl+C handling for watch mode
|
|
3682
|
+
let sigintCount = 0;
|
|
3683
|
+
process.on("SIGINT", () => {
|
|
3684
|
+
sigintCount++;
|
|
3685
|
+
if (sigintCount === 1) {
|
|
3686
|
+
console.log(
|
|
3687
|
+
"\n[sparkbun dev --watch] Shutting down... (press Ctrl+C again to force quit)",
|
|
3688
|
+
);
|
|
3689
|
+
cleanup();
|
|
3690
|
+
// Wait briefly for app to exit, then exit
|
|
3691
|
+
setTimeout(() => process.exit(0), 2000);
|
|
3692
|
+
} else {
|
|
3693
|
+
try {
|
|
3694
|
+
process.kill(0, "SIGKILL");
|
|
3695
|
+
} catch {}
|
|
3696
|
+
process.exit(0);
|
|
3697
|
+
}
|
|
3698
|
+
});
|
|
3699
|
+
|
|
3700
|
+
// Initial build + launch (watchers start after build completes)
|
|
3701
|
+
try {
|
|
3702
|
+
await runBuild(config, "dev");
|
|
3703
|
+
appHandle = await runApp(config, {
|
|
3704
|
+
onExit: () => {
|
|
3705
|
+
appHandle = null;
|
|
3706
|
+
},
|
|
3707
|
+
});
|
|
3708
|
+
} catch (error) {
|
|
3709
|
+
console.error("[sparkbun dev --watch] Initial build failed:", error);
|
|
3710
|
+
console.log("[sparkbun dev --watch] Waiting for file changes...");
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
// Start watching only after initial build + all hooks are done
|
|
3714
|
+
startWatchers();
|
|
3715
|
+
|
|
3716
|
+
// Keep the process alive
|
|
3717
|
+
await new Promise(() => {});
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
// Helper functions
|
|
3721
|
+
|
|
3722
|
+
function formatBuildLogEntry(entry: any): string {
|
|
3723
|
+
if (!entry || typeof entry !== "object") return String(entry);
|
|
3724
|
+
const level = entry.level || "error";
|
|
3725
|
+
let message = entry.message || entry.text || String(entry);
|
|
3726
|
+
if (entry.location) {
|
|
3727
|
+
const loc = entry.location;
|
|
3728
|
+
const file = loc.file || loc.path || "unknown";
|
|
3729
|
+
const line = loc.line ?? loc.lineText ?? loc.lineNumber ?? "?";
|
|
3730
|
+
const col = loc.column ?? loc.col ?? loc.columnNumber ?? "?";
|
|
3731
|
+
message += ` (${file}:${line}:${col})`;
|
|
3732
|
+
}
|
|
3733
|
+
return `[bun.build:${level}] ${message}`;
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
function printBuildLogs(logs: any[] | undefined | null) {
|
|
3737
|
+
if (!logs || logs.length === 0) {
|
|
3738
|
+
console.error("[bun.build] No logs returned from Bun.build");
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
for (const entry of logs) {
|
|
3742
|
+
console.error(formatBuildLogEntry(entry));
|
|
3743
|
+
if (entry?.notes?.length) {
|
|
3744
|
+
for (const note of entry.notes) {
|
|
3745
|
+
console.error(` note: ${note.text ?? String(note)}`);
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
async function getConfig() {
|
|
3752
|
+
let loadedConfig: Partial<typeof defaultConfig> & Record<string, unknown> =
|
|
3753
|
+
{};
|
|
3754
|
+
const foundConfigPath = findConfigFile();
|
|
3755
|
+
|
|
3756
|
+
if (foundConfigPath) {
|
|
3757
|
+
console.log(`Using config file: ${basename(foundConfigPath)}`);
|
|
3758
|
+
|
|
3759
|
+
try {
|
|
3760
|
+
// Use dynamic import for TypeScript ESM files
|
|
3761
|
+
// Bun handles TypeScript natively, no transpilation needed
|
|
3762
|
+
const configModule = await import(foundConfigPath);
|
|
3763
|
+
loadedConfig = configModule.default || configModule;
|
|
3764
|
+
|
|
3765
|
+
// Validate that we got a valid config object
|
|
3766
|
+
if (!loadedConfig || typeof loadedConfig !== "object") {
|
|
3767
|
+
console.error("Config file must export a default object");
|
|
3768
|
+
console.error("using default config instead");
|
|
3769
|
+
loadedConfig = {};
|
|
3770
|
+
}
|
|
3771
|
+
} catch (error) {
|
|
3772
|
+
console.error("Failed to load config file:", error);
|
|
3773
|
+
console.error("using default config instead");
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
return {
|
|
3778
|
+
...defaultConfig,
|
|
3779
|
+
...loadedConfig,
|
|
3780
|
+
app: {
|
|
3781
|
+
...defaultConfig.app,
|
|
3782
|
+
...(loadedConfig?.app || {}),
|
|
3783
|
+
},
|
|
3784
|
+
build: {
|
|
3785
|
+
...defaultConfig.build,
|
|
3786
|
+
...(loadedConfig?.build || {}),
|
|
3787
|
+
mac: {
|
|
3788
|
+
...defaultConfig.build.mac,
|
|
3789
|
+
...(loadedConfig?.build?.mac || {}),
|
|
3790
|
+
entitlements: {
|
|
3791
|
+
...defaultConfig.build.mac.entitlements,
|
|
3792
|
+
...(loadedConfig?.build?.mac?.entitlements || {}),
|
|
3793
|
+
},
|
|
3794
|
+
},
|
|
3795
|
+
win: {
|
|
3796
|
+
...defaultConfig.build.win,
|
|
3797
|
+
...(loadedConfig?.build?.win || {}),
|
|
3798
|
+
},
|
|
3799
|
+
linux: {
|
|
3800
|
+
...defaultConfig.build.linux,
|
|
3801
|
+
...(loadedConfig?.build?.linux || {}),
|
|
3802
|
+
},
|
|
3803
|
+
bun: {
|
|
3804
|
+
...defaultConfig.build.bun,
|
|
3805
|
+
...(loadedConfig?.build?.bun || {}),
|
|
3806
|
+
},
|
|
3807
|
+
zig: {
|
|
3808
|
+
...defaultConfig.build.zig,
|
|
3809
|
+
...(loadedConfig?.build?.zig || {}),
|
|
3810
|
+
},
|
|
3811
|
+
},
|
|
3812
|
+
runtime: {
|
|
3813
|
+
...defaultConfig.runtime,
|
|
3814
|
+
...((loadedConfig as Record<string, any>)?.["runtime"] || {}),
|
|
3815
|
+
},
|
|
3816
|
+
scripts: {
|
|
3817
|
+
...defaultConfig.scripts,
|
|
3818
|
+
...(loadedConfig?.scripts || {}),
|
|
3819
|
+
},
|
|
3820
|
+
release: {
|
|
3821
|
+
...defaultConfig.release,
|
|
3822
|
+
...(loadedConfig?.release || {}),
|
|
3823
|
+
},
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
function buildEntitlementsFile(
|
|
3828
|
+
entitlements: Record<string, boolean | string | string[]>,
|
|
3829
|
+
) {
|
|
3830
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3831
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3832
|
+
<plist version="1.0">
|
|
3833
|
+
<dict>
|
|
3834
|
+
${Object.keys(entitlements)
|
|
3835
|
+
.map((key) => {
|
|
3836
|
+
return `<key>${key}</key>\n${getEntitlementValue(entitlements[key]!)}`;
|
|
3837
|
+
})
|
|
3838
|
+
.join("\n")}
|
|
3839
|
+
</dict>
|
|
3840
|
+
</plist>
|
|
3841
|
+
`;
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
function getEntitlementValue(value: boolean | string | string[]) {
|
|
3845
|
+
if (typeof value === "boolean") {
|
|
3846
|
+
return `<${value.toString()}/>`;
|
|
3847
|
+
} else if (Array.isArray(value)) {
|
|
3848
|
+
return `<array>\n${value.map((v) => ` <string>${v}</string>`).join("\n")}\n </array>`;
|
|
3849
|
+
} else {
|
|
3850
|
+
return `<string>${value}</string>`;
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
async function createCompiledInstaller(
|
|
3855
|
+
buildFolder: string,
|
|
3856
|
+
compressedTarPath: string,
|
|
3857
|
+
_appFileName: string,
|
|
3858
|
+
_targetPaths: any,
|
|
3859
|
+
buildEnvironment: string,
|
|
3860
|
+
hash: string,
|
|
3861
|
+
config: any,
|
|
3862
|
+
projectRoot: string,
|
|
3863
|
+
): Promise<string> {
|
|
3864
|
+
const targetOSName = OS === "macos" ? "darwin" : OS === "win" ? "windows" : "linux";
|
|
3865
|
+
const isWindows = OS === "win";
|
|
3866
|
+
|
|
3867
|
+
const setupFileName = isWindows
|
|
3868
|
+
? getWindowsSetupFileName(config.app.name, buildEnvironment)
|
|
3869
|
+
: _appFileName;
|
|
3870
|
+
const outputPath = join(buildFolder, setupFileName + (isWindows ? "" : ""));
|
|
3871
|
+
|
|
3872
|
+
console.log(`Creating ${isWindows ? "Windows" : "Linux"} installer via Bun.build()...`);
|
|
3873
|
+
|
|
3874
|
+
// Stage installer files
|
|
3875
|
+
const stagingDir = join(buildFolder, ".installer-staging");
|
|
3876
|
+
if (existsSync(stagingDir)) rmSync(stagingDir, { recursive: true, force: true });
|
|
3877
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
3878
|
+
|
|
3879
|
+
// Copy archive
|
|
3880
|
+
copyFileSync(compressedTarPath, join(stagingDir, "app-archive.tar.gz"));
|
|
3881
|
+
|
|
3882
|
+
// Write metadata
|
|
3883
|
+
const platformConfig = OS === "macos" ? config.build?.mac
|
|
3884
|
+
: OS === "win" ? config.build?.win
|
|
3885
|
+
: config.build?.linux;
|
|
3886
|
+
const metadata = {
|
|
3887
|
+
identifier: config.app.identifier,
|
|
3888
|
+
name: config.app.name,
|
|
3889
|
+
channel: buildEnvironment,
|
|
3890
|
+
hash: hash,
|
|
3891
|
+
...(platformConfig?.requireAdmin ? { requireAdmin: true } : {}),
|
|
3892
|
+
};
|
|
3893
|
+
writeFileSync(join(stagingDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
3894
|
+
|
|
3895
|
+
// Copy installer template
|
|
3896
|
+
const cliDir = dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"));
|
|
3897
|
+
const templatePath = join(cliDir, "..", "installer", "installer-template.ts");
|
|
3898
|
+
copyFileSync(templatePath, join(stagingDir, "installer.ts"));
|
|
3899
|
+
|
|
3900
|
+
const installerCompileOptions: any = {
|
|
3901
|
+
target: `bun-${targetOSName}-${ARCH}`,
|
|
3902
|
+
outfile: outputPath,
|
|
3903
|
+
};
|
|
3904
|
+
|
|
3905
|
+
if (isWindows) {
|
|
3906
|
+
let icoPath: string | undefined;
|
|
3907
|
+
if (config.build.win?.icon) {
|
|
3908
|
+
const iconSrc = config.build.win.icon.startsWith("/") || config.build.win.icon.match(/^[a-zA-Z]:/)
|
|
3909
|
+
? config.build.win.icon
|
|
3910
|
+
: join(projectRoot, config.build.win.icon);
|
|
3911
|
+
if (existsSync(iconSrc)) {
|
|
3912
|
+
icoPath = iconSrc;
|
|
3913
|
+
if (iconSrc.toLowerCase().endsWith(".png")) {
|
|
3914
|
+
const pngToIco = (await import("png-to-ico")).default;
|
|
3915
|
+
const tempIcoPath = join(buildFolder, "temp-installer-icon.ico");
|
|
3916
|
+
const icoBuffer = await pngToIco(iconSrc);
|
|
3917
|
+
writeFileSync(tempIcoPath, new Uint8Array(icoBuffer));
|
|
3918
|
+
icoPath = tempIcoPath;
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
installerCompileOptions.windows = {
|
|
3923
|
+
hideConsole: true,
|
|
3924
|
+
...(icoPath && { icon: icoPath }),
|
|
3925
|
+
title: `${config.app.name} Installer`,
|
|
3926
|
+
version: config.app.version,
|
|
3927
|
+
description: `Installs ${config.app.name}`,
|
|
3928
|
+
publisher: config.app.publisher || " ",
|
|
3929
|
+
copyright: config.app.copyright || " ",
|
|
3930
|
+
};
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
console.log("Compiling installer...");
|
|
3934
|
+
const installerBuild = await Bun.build({
|
|
3935
|
+
entrypoints: [join(stagingDir, "installer.ts")],
|
|
3936
|
+
compile: installerCompileOptions,
|
|
3937
|
+
});
|
|
3938
|
+
if (!installerBuild.success) {
|
|
3939
|
+
console.error("Installer compilation failed:", installerBuild.logs);
|
|
3940
|
+
throw new Error("Installer compilation failed");
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
if (isWindows) {
|
|
3944
|
+
patchPeSubsystem(outputPath);
|
|
3945
|
+
} else {
|
|
3946
|
+
execSync(`chmod +x ${escapePathForTerminal(outputPath)}`);
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
// Clean up staging
|
|
3950
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
3951
|
+
|
|
3952
|
+
const exeSize = statSync(outputPath).size;
|
|
3953
|
+
console.log(`Created installer: ${outputPath} (${(exeSize / 1024 / 1024).toFixed(2)} MB)`);
|
|
3954
|
+
|
|
3955
|
+
return outputPath;
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
function createArHeader(name: string, size: number, mode: number = 0o100644): Buffer {
|
|
3959
|
+
const header = Buffer.alloc(60, 0x20); // fill with spaces
|
|
3960
|
+
const now = Math.floor(Date.now() / 1000).toString();
|
|
3961
|
+
Buffer.from(name.padEnd(16, " ")).copy(header, 0); // filename
|
|
3962
|
+
Buffer.from(now.padEnd(12, " ")).copy(header, 16); // timestamp
|
|
3963
|
+
Buffer.from("0".padEnd(6, " ")).copy(header, 28); // owner
|
|
3964
|
+
Buffer.from("0".padEnd(6, " ")).copy(header, 34); // group
|
|
3965
|
+
Buffer.from(mode.toString(8).padEnd(8, " ")).copy(header, 40); // mode
|
|
3966
|
+
Buffer.from(size.toString().padEnd(10, " ")).copy(header, 48); // size
|
|
3967
|
+
header[58] = 0x60; // magic
|
|
3968
|
+
header[59] = 0x0A;
|
|
3969
|
+
return header;
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
function buildArArchive(members: { name: string; data: Buffer; mode?: number }[]): Buffer {
|
|
3973
|
+
const magic = Buffer.from("!<arch>\n");
|
|
3974
|
+
const parts: Buffer[] = [magic];
|
|
3975
|
+
for (const member of members) {
|
|
3976
|
+
parts.push(createArHeader(member.name, member.data.length, member.mode));
|
|
3977
|
+
parts.push(member.data);
|
|
3978
|
+
if (member.data.length % 2 !== 0) {
|
|
3979
|
+
parts.push(Buffer.from("\n"));
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
return Buffer.concat(parts);
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
function collectFiles(dir: string, prefix: string = ""): { path: string; fullPath: string; isDir: boolean }[] {
|
|
3986
|
+
const results: { path: string; fullPath: string; isDir: boolean }[] = [];
|
|
3987
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
3988
|
+
for (const entry of entries) {
|
|
3989
|
+
const fullPath = join(dir, entry.name);
|
|
3990
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
3991
|
+
if (entry.isDirectory()) {
|
|
3992
|
+
results.push({ path: relPath + "/", fullPath, isDir: true });
|
|
3993
|
+
results.push(...collectFiles(fullPath, relPath));
|
|
3994
|
+
} else {
|
|
3995
|
+
results.push({ path: relPath, fullPath, isDir: false });
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
return results;
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
function writeTarHeader(header: Buffer, file: { path: string; fullPath: string; isDir: boolean }, content?: Buffer): void {
|
|
4002
|
+
const tarPath = file.path;
|
|
4003
|
+
Buffer.from(tarPath).copy(header, 0, 0, Math.min(tarPath.length, 100));
|
|
4004
|
+
|
|
4005
|
+
// mtime from actual file
|
|
4006
|
+
const mtime = Math.floor(statSync(file.fullPath).mtimeMs / 1000);
|
|
4007
|
+
Buffer.from(mtime.toString(8).padStart(11, "0") + "\0").copy(header, 136);
|
|
4008
|
+
|
|
4009
|
+
// uid/gid = 0 (root)
|
|
4010
|
+
Buffer.from("0000000\0").copy(header, 108);
|
|
4011
|
+
Buffer.from("0000000\0").copy(header, 116);
|
|
4012
|
+
|
|
4013
|
+
// ustar magic
|
|
4014
|
+
Buffer.from("ustar\0").copy(header, 257);
|
|
4015
|
+
Buffer.from("00").copy(header, 263);
|
|
4016
|
+
Buffer.from("root\0").copy(header, 265);
|
|
4017
|
+
Buffer.from("root\0").copy(header, 297);
|
|
4018
|
+
|
|
4019
|
+
if (file.isDir) {
|
|
4020
|
+
Buffer.from("0040755\0").copy(header, 100);
|
|
4021
|
+
Buffer.from("00000000000\0").copy(header, 124);
|
|
4022
|
+
header[156] = 0x35; // '5'
|
|
4023
|
+
} else {
|
|
4024
|
+
const isExecutable = file.path.match(/\/bin\/|\/postinst$|\/preinst$|\/postrm$|\/prerm$/);
|
|
4025
|
+
Buffer.from(isExecutable ? "0100755\0" : "0100644\0").copy(header, 100);
|
|
4026
|
+
Buffer.from((content!.length).toString(8).padStart(11, "0") + "\0").copy(header, 124);
|
|
4027
|
+
header[156] = 0x30; // '0'
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
// checksum (must be computed last)
|
|
4031
|
+
Buffer.from(" ").copy(header, 148);
|
|
4032
|
+
let checksum = 0;
|
|
4033
|
+
for (let i = 0; i < 512; i++) checksum += header[i];
|
|
4034
|
+
Buffer.from(checksum.toString(8).padStart(6, "0") + "\0 ").copy(header, 148);
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
function buildTarGz(baseDir: string, prefix: string = "./"): Buffer {
|
|
4038
|
+
const files = collectFiles(baseDir);
|
|
4039
|
+
const blocks: Buffer[] = [];
|
|
4040
|
+
|
|
4041
|
+
for (const file of files) {
|
|
4042
|
+
const prefixedFile = { ...file, path: prefix + file.path };
|
|
4043
|
+
const header = Buffer.alloc(512, 0);
|
|
4044
|
+
|
|
4045
|
+
if (file.isDir) {
|
|
4046
|
+
writeTarHeader(header, prefixedFile);
|
|
4047
|
+
blocks.push(header);
|
|
4048
|
+
} else {
|
|
4049
|
+
const content = readFileSync(file.fullPath);
|
|
4050
|
+
writeTarHeader(header, prefixedFile, content);
|
|
4051
|
+
blocks.push(header);
|
|
4052
|
+
blocks.push(content);
|
|
4053
|
+
const padding = 512 - (content.length % 512);
|
|
4054
|
+
if (padding < 512) blocks.push(Buffer.alloc(padding, 0));
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
blocks.push(Buffer.alloc(1024, 0));
|
|
4059
|
+
const tarData = Buffer.concat(blocks);
|
|
4060
|
+
return Buffer.from(Bun.gzipSync(tarData));
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
async function createDebPackage(
|
|
4064
|
+
buildFolder: string,
|
|
4065
|
+
appBundleFolderPath: string,
|
|
4066
|
+
config: any,
|
|
4067
|
+
targetArch: string,
|
|
4068
|
+
projectRoot: string,
|
|
4069
|
+
): Promise<string> {
|
|
4070
|
+
const debArch = targetArch === "arm64" ? "arm64" : "amd64";
|
|
4071
|
+
const appNameNoSpaces = config.app.name.replace(/ /g, "");
|
|
4072
|
+
const pkgName = config.app.name.toLowerCase().replace(/ /g, "-");
|
|
4073
|
+
const installDir = config.build?.linux?.installDir || `/opt/${appNameNoSpaces}`;
|
|
4074
|
+
const version = config.app.version || "1.0.0";
|
|
4075
|
+
const exeName = appNameNoSpaces;
|
|
4076
|
+
|
|
4077
|
+
const defaultDeps = [
|
|
4078
|
+
"libwebkit2gtk-4.1-0",
|
|
4079
|
+
"libgtk-3-0t64 | libgtk-3-0",
|
|
4080
|
+
"libsoup-3.0-0",
|
|
4081
|
+
"libjavascriptcoregtk-4.1-0",
|
|
4082
|
+
"libayatana-appindicator3-1",
|
|
4083
|
+
"libgdk-pixbuf-2.0-0",
|
|
4084
|
+
];
|
|
4085
|
+
const deps = config.build?.linux?.debDependencies || defaultDeps;
|
|
4086
|
+
const requireAdmin = config.build?.linux?.requireAdmin;
|
|
4087
|
+
if (requireAdmin && !deps.some((d: string) => d.includes("pkexec"))) {
|
|
4088
|
+
deps.push("pkexec | policykit-1");
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
const maintainer = config.build?.linux?.debMaintainer
|
|
4092
|
+
|| config.app.publisher
|
|
4093
|
+
|| "Unknown";
|
|
4094
|
+
const category = config.build?.linux?.category || "Utility;Application;";
|
|
4095
|
+
|
|
4096
|
+
console.log(`Creating .deb package (${debArch})...`);
|
|
4097
|
+
|
|
4098
|
+
const stagingDir = join(buildFolder, ".deb-staging");
|
|
4099
|
+
if (existsSync(stagingDir)) rmSync(stagingDir, { recursive: true, force: true });
|
|
4100
|
+
|
|
4101
|
+
// Staging directories: control files and data files separately
|
|
4102
|
+
const controlDir = join(stagingDir, "control");
|
|
4103
|
+
const dataDir = join(stagingDir, "data");
|
|
4104
|
+
const appDestDir = join(dataDir, installDir.slice(1));
|
|
4105
|
+
const applicationsDir = join(dataDir, "usr", "share", "applications");
|
|
4106
|
+
mkdirSync(controlDir, { recursive: true });
|
|
4107
|
+
mkdirSync(appDestDir, { recursive: true });
|
|
4108
|
+
mkdirSync(applicationsDir, { recursive: true });
|
|
4109
|
+
|
|
4110
|
+
// Copy app files
|
|
4111
|
+
cpSync(appBundleFolderPath, appDestDir, { recursive: true, dereference: true });
|
|
4112
|
+
|
|
4113
|
+
// Patch all WM class occurrences in libNativeWrapper.so
|
|
4114
|
+
const nativeLib = join(appDestDir, "bin", "libNativeWrapper.so");
|
|
4115
|
+
const oldWmName = Buffer.from("SparkBunKitchenSink-dev");
|
|
4116
|
+
const wmClass = config.app.name.slice(0, oldWmName.length);
|
|
4117
|
+
if (existsSync(nativeLib)) {
|
|
4118
|
+
const newName = Buffer.alloc(oldWmName.length, 0);
|
|
4119
|
+
Buffer.from(wmClass).copy(newName);
|
|
4120
|
+
const data = readFileSync(nativeLib);
|
|
4121
|
+
let patched = 0;
|
|
4122
|
+
let offset = 0;
|
|
4123
|
+
while (true) {
|
|
4124
|
+
const idx = data.indexOf(oldWmName, offset);
|
|
4125
|
+
if (idx === -1) break;
|
|
4126
|
+
newName.copy(data, idx);
|
|
4127
|
+
offset = idx + oldWmName.length;
|
|
4128
|
+
patched++;
|
|
4129
|
+
}
|
|
4130
|
+
if (patched > 0) {
|
|
4131
|
+
writeFileSync(nativeLib, data);
|
|
4132
|
+
console.log(` Patched WM class in libNativeWrapper.so (${patched} occurrences)`);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
// Install app icon to system icon theme
|
|
4137
|
+
let iconName = pkgName;
|
|
4138
|
+
const resourcesDir = join(appDestDir, "Resources");
|
|
4139
|
+
if (existsSync(resourcesDir)) {
|
|
4140
|
+
const pngs = readdirSync(resourcesDir).filter((f: string) => f.endsWith(".png"));
|
|
4141
|
+
if (pngs.length > 0) {
|
|
4142
|
+
const iconSrc = join(resourcesDir, pngs[0]);
|
|
4143
|
+
for (const size of ["256x256", "128x128"]) {
|
|
4144
|
+
const iconDir = join(dataDir, "usr", "share", "icons", "hicolor", size, "apps");
|
|
4145
|
+
mkdirSync(iconDir, { recursive: true });
|
|
4146
|
+
cpSync(iconSrc, join(iconDir, `${pkgName}.png`));
|
|
4147
|
+
}
|
|
4148
|
+
console.log(" Installed app icon to system icon theme");
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
// .desktop file with absolute paths
|
|
4153
|
+
const execLine = requireAdmin
|
|
4154
|
+
? `pkexec ${installDir}/bin/${exeName}`
|
|
4155
|
+
: `${installDir}/bin/${exeName}`;
|
|
4156
|
+
|
|
4157
|
+
const desktopContent = `[Desktop Entry]
|
|
4158
|
+
Version=1.0
|
|
4159
|
+
Type=Application
|
|
4160
|
+
Name=${config.app.name}
|
|
4161
|
+
Comment=${config.app.description || `${config.app.name} application`}
|
|
4162
|
+
Exec=${execLine}
|
|
4163
|
+
Icon=${iconName}
|
|
4164
|
+
Terminal=false
|
|
4165
|
+
StartupWMClass=${wmClass}
|
|
4166
|
+
Categories=${category}
|
|
4167
|
+
`;
|
|
4168
|
+
writeFileSync(join(applicationsDir, `${pkgName}.desktop`), desktopContent);
|
|
4169
|
+
|
|
4170
|
+
// Polkit policy for admin elevation
|
|
4171
|
+
if (requireAdmin) {
|
|
4172
|
+
const policyId = config.app.identifier.replace(/-/g, ".");
|
|
4173
|
+
const policyDir = join(dataDir, "usr", "share", "polkit-1", "actions");
|
|
4174
|
+
mkdirSync(policyDir, { recursive: true });
|
|
4175
|
+
const policyContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
4176
|
+
<!DOCTYPE policyconfig PUBLIC
|
|
4177
|
+
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
|
4178
|
+
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
|
4179
|
+
<policyconfig>
|
|
4180
|
+
<action id="${policyId}.run">
|
|
4181
|
+
<description>Run ${config.app.name}</description>
|
|
4182
|
+
<message>Authentication is required to run ${config.app.name}</message>
|
|
4183
|
+
<defaults>
|
|
4184
|
+
<allow_any>auth_admin</allow_any>
|
|
4185
|
+
<allow_inactive>auth_admin</allow_inactive>
|
|
4186
|
+
<allow_active>auth_admin_keep</allow_active>
|
|
4187
|
+
</defaults>
|
|
4188
|
+
<annotate key="org.freedesktop.policykit.exec.path">${installDir}/bin/${exeName}</annotate>
|
|
4189
|
+
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
|
|
4190
|
+
</action>
|
|
4191
|
+
</policyconfig>
|
|
4192
|
+
`;
|
|
4193
|
+
writeFileSync(join(policyDir, `${policyId}.policy`), policyContent);
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
// control file — Description format: short synopsis on first line,
|
|
4197
|
+
// long description on subsequent lines indented with a single space
|
|
4198
|
+
const shortDesc = config.app.name;
|
|
4199
|
+
const longDesc = config.app.description
|
|
4200
|
+
? `\n ${config.app.description.replace(/\n/g, "\n ")}`
|
|
4201
|
+
: "";
|
|
4202
|
+
writeFileSync(join(controlDir, "control"), `Package: ${pkgName}
|
|
4203
|
+
Version: ${version}
|
|
4204
|
+
Section: utils
|
|
4205
|
+
Priority: optional
|
|
4206
|
+
Architecture: ${debArch}
|
|
4207
|
+
Depends: ${deps.join(", ")}
|
|
4208
|
+
Maintainer: ${maintainer}
|
|
4209
|
+
Description: ${shortDesc}${longDesc}
|
|
4210
|
+
`);
|
|
4211
|
+
|
|
4212
|
+
// postinst
|
|
4213
|
+
writeFileSync(join(controlDir, "postinst"), `#!/bin/sh
|
|
4214
|
+
chmod +x ${installDir}/bin/${exeName}
|
|
4215
|
+
chmod +x ${installDir}/bin/bspatch 2>/dev/null || true
|
|
4216
|
+
gtk-update-icon-cache -f -t /usr/share/icons/hicolor 2>/dev/null || true
|
|
4217
|
+
update-desktop-database /usr/share/applications 2>/dev/null || true
|
|
4218
|
+
`);
|
|
4219
|
+
|
|
4220
|
+
// Build .deb as ar archive: debian-binary + control.tar.gz + data.tar.gz
|
|
4221
|
+
const debianBinary = Buffer.from("2.0\n");
|
|
4222
|
+
const controlTarGz = buildTarGz(controlDir, "./");
|
|
4223
|
+
const dataTarGz = buildTarGz(dataDir, "./");
|
|
4224
|
+
|
|
4225
|
+
const debData = buildArArchive([
|
|
4226
|
+
{ name: "debian-binary", data: debianBinary },
|
|
4227
|
+
{ name: "control.tar.gz", data: controlTarGz },
|
|
4228
|
+
{ name: "data.tar.gz", data: dataTarGz },
|
|
4229
|
+
]);
|
|
4230
|
+
|
|
4231
|
+
const debFileName = `${pkgName}_${version}_${debArch}.deb`;
|
|
4232
|
+
const debOutputPath = join(buildFolder, debFileName);
|
|
4233
|
+
writeFileSync(debOutputPath, debData);
|
|
4234
|
+
|
|
4235
|
+
// Clean up
|
|
4236
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
4237
|
+
|
|
4238
|
+
const debSize = statSync(debOutputPath).size;
|
|
4239
|
+
console.log(`Created .deb: ${debOutputPath} (${(debSize / 1024 / 1024).toFixed(2)} MB)`);
|
|
4240
|
+
|
|
4241
|
+
return debOutputPath;
|
|
4242
|
+
}
|
|
4243
|
+
|
|
4244
|
+
function codesignAppBundle(
|
|
4245
|
+
appBundleOrDmgPath: string,
|
|
4246
|
+
entitlementsFilePath: string | undefined,
|
|
4247
|
+
config: Awaited<ReturnType<typeof getConfig>>,
|
|
4248
|
+
) {
|
|
4249
|
+
console.log("code signing...");
|
|
4250
|
+
if (OS !== "macos" || !config.build.mac.codesign) {
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
const SPARKBUN_DEVELOPER_ID = process.env["SPARKBUN_DEVELOPER_ID"];
|
|
4255
|
+
|
|
4256
|
+
if (!SPARKBUN_DEVELOPER_ID) {
|
|
4257
|
+
console.error("Env var SPARKBUN_DEVELOPER_ID is required to codesign");
|
|
4258
|
+
process.exit(1);
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
// If this is a DMG file, sign it directly
|
|
4262
|
+
if (appBundleOrDmgPath.endsWith(".dmg")) {
|
|
4263
|
+
execSync(
|
|
4264
|
+
`codesign --force --verbose --timestamp --sign "${SPARKBUN_DEVELOPER_ID}" ${escapePathForTerminal(
|
|
4265
|
+
appBundleOrDmgPath,
|
|
4266
|
+
)}`,
|
|
4267
|
+
);
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
// For app bundles, sign binaries individually to avoid --deep issues with notarization
|
|
4272
|
+
const contentsPath = join(appBundleOrDmgPath, "Contents");
|
|
4273
|
+
const macosPath = join(contentsPath, "MacOS");
|
|
4274
|
+
|
|
4275
|
+
// Prepare entitlements if provided
|
|
4276
|
+
if (entitlementsFilePath) {
|
|
4277
|
+
const entitlementsFileContents = buildEntitlementsFile(
|
|
4278
|
+
config.build.mac.entitlements,
|
|
4279
|
+
);
|
|
4280
|
+
Bun.write(entitlementsFilePath, entitlementsFileContents);
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
// Sign frameworks first (CEF framework requires special handling)
|
|
4284
|
+
const frameworksPath = join(contentsPath, "Frameworks");
|
|
4285
|
+
if (existsSync(frameworksPath)) {
|
|
4286
|
+
try {
|
|
4287
|
+
const frameworks = readdirSync(frameworksPath);
|
|
4288
|
+
for (const framework of frameworks) {
|
|
4289
|
+
if (framework.endsWith(".framework")) {
|
|
4290
|
+
const frameworkPath = join(frameworksPath, framework);
|
|
4291
|
+
|
|
4292
|
+
if (framework === "Chromium Embedded Framework.framework") {
|
|
4293
|
+
console.log(`Signing CEF framework components: ${framework}`);
|
|
4294
|
+
|
|
4295
|
+
// Sign CEF libraries first
|
|
4296
|
+
const librariesPath = join(frameworkPath, "Libraries");
|
|
4297
|
+
if (existsSync(librariesPath)) {
|
|
4298
|
+
const libraries = readdirSync(librariesPath);
|
|
4299
|
+
for (const library of libraries) {
|
|
4300
|
+
if (library.endsWith(".dylib")) {
|
|
4301
|
+
const libraryPath = join(librariesPath, library);
|
|
4302
|
+
console.log(`Signing CEF library: ${library}`);
|
|
4303
|
+
execSync(
|
|
4304
|
+
`codesign --force --verbose --timestamp --sign "${SPARKBUN_DEVELOPER_ID}" --options runtime ${escapePathForTerminal(libraryPath)}`,
|
|
4305
|
+
);
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
// CEF helper apps are in the main Frameworks directory, not inside the CEF framework
|
|
4311
|
+
// We'll sign them after signing all frameworks
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
// Sign the framework bundle itself (for CEF and any other frameworks)
|
|
4315
|
+
console.log(`Signing framework bundle: ${framework}`);
|
|
4316
|
+
execSync(
|
|
4317
|
+
`codesign --force --verbose --timestamp --sign "${SPARKBUN_DEVELOPER_ID}" --options runtime ${escapePathForTerminal(frameworkPath)}`,
|
|
4318
|
+
);
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
} catch (err) {
|
|
4322
|
+
console.log("Error signing frameworks:", err);
|
|
4323
|
+
throw err; // Re-throw to fail the build since framework signing is critical
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
// Sign CEF helper apps (they're in the main Frameworks directory, not inside CEF framework)
|
|
4328
|
+
const mainProcess = config.build.mainProcess ?? "bun";
|
|
4329
|
+
const cefHelperApps = getCEFHelperNames().map(
|
|
4330
|
+
(helperName) => `${helperName}.app`,
|
|
4331
|
+
);
|
|
4332
|
+
|
|
4333
|
+
for (const helperApp of cefHelperApps) {
|
|
4334
|
+
const helperPath = join(frameworksPath, helperApp);
|
|
4335
|
+
if (existsSync(helperPath)) {
|
|
4336
|
+
const helperExecutablePath = join(
|
|
4337
|
+
helperPath,
|
|
4338
|
+
"Contents",
|
|
4339
|
+
"MacOS",
|
|
4340
|
+
helperApp.replace(".app", ""),
|
|
4341
|
+
);
|
|
4342
|
+
if (existsSync(helperExecutablePath)) {
|
|
4343
|
+
console.log(`Signing CEF helper executable: ${helperApp}`);
|
|
4344
|
+
const entitlementFlag = entitlementsFilePath
|
|
4345
|
+
? `--entitlements ${entitlementsFilePath}`
|
|
4346
|
+
: "";
|
|
4347
|
+
execSync(
|
|
4348
|
+
`codesign --force --verbose --timestamp --sign "${SPARKBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(helperExecutablePath)}`,
|
|
4349
|
+
);
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
console.log(`Signing CEF helper bundle: ${helperApp}`);
|
|
4353
|
+
const entitlementFlag = entitlementsFilePath
|
|
4354
|
+
? `--entitlements ${entitlementsFilePath}`
|
|
4355
|
+
: "";
|
|
4356
|
+
execSync(
|
|
4357
|
+
`codesign --force --verbose --timestamp --sign "${SPARKBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(helperPath)}`,
|
|
4358
|
+
);
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
// Sign all binaries and libraries in MacOS folder and subdirectories
|
|
4363
|
+
console.log("Signing all binaries in MacOS folder...");
|
|
4364
|
+
|
|
4365
|
+
// Recursively find all executables and libraries in MacOS folder
|
|
4366
|
+
function findExecutables(dir: string): string[] {
|
|
4367
|
+
let executables: string[] = [];
|
|
4368
|
+
|
|
4369
|
+
try {
|
|
4370
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
4371
|
+
|
|
4372
|
+
for (const entry of entries) {
|
|
4373
|
+
const fullPath = join(dir, entry.name);
|
|
4374
|
+
|
|
4375
|
+
if (entry.isDirectory()) {
|
|
4376
|
+
// Recursively search subdirectories
|
|
4377
|
+
executables = executables.concat(findExecutables(fullPath));
|
|
4378
|
+
} else if (entry.isFile()) {
|
|
4379
|
+
// Check if it's an executable or library
|
|
4380
|
+
try {
|
|
4381
|
+
const fileInfo = execSync(`file -b ${escapePathForTerminal(fullPath)}`, {
|
|
4382
|
+
encoding: "utf8",
|
|
4383
|
+
}).trim();
|
|
4384
|
+
if (
|
|
4385
|
+
fileInfo.includes("Mach-O") ||
|
|
4386
|
+
entry.name.endsWith(".dylib")
|
|
4387
|
+
) {
|
|
4388
|
+
executables.push(fullPath);
|
|
4389
|
+
}
|
|
4390
|
+
} catch {
|
|
4391
|
+
// If file command fails, check by extension
|
|
4392
|
+
if (entry.name.endsWith(".dylib") || !entry.name.includes(".")) {
|
|
4393
|
+
// No extension often means executable
|
|
4394
|
+
executables.push(fullPath);
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
} catch (err) {
|
|
4400
|
+
console.error(`Error scanning directory ${dir}:`, err);
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
return executables;
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
const executablesInMacOS = findExecutables(macosPath);
|
|
4407
|
+
|
|
4408
|
+
// Sign each found executable
|
|
4409
|
+
for (const execPath of executablesInMacOS) {
|
|
4410
|
+
const fileName = basename(execPath);
|
|
4411
|
+
const relativePath = execPath.replace(macosPath + "/", "");
|
|
4412
|
+
|
|
4413
|
+
// Use filename as identifier (without extension)
|
|
4414
|
+
const identifier = fileName.replace(/\.[^.]+$/, "");
|
|
4415
|
+
|
|
4416
|
+
console.log(`Signing ${relativePath} with identifier ${identifier}`);
|
|
4417
|
+
const entitlementFlag = entitlementsFilePath
|
|
4418
|
+
? `--entitlements ${entitlementsFilePath}`
|
|
4419
|
+
: "";
|
|
4420
|
+
|
|
4421
|
+
try {
|
|
4422
|
+
execSync(
|
|
4423
|
+
`codesign --force --verbose --timestamp --sign "${SPARKBUN_DEVELOPER_ID}" --options runtime --identifier ${identifier} ${entitlementFlag} ${escapePathForTerminal(execPath)}`,
|
|
4424
|
+
);
|
|
4425
|
+
} catch (err) {
|
|
4426
|
+
console.error(
|
|
4427
|
+
`Failed to sign ${relativePath}:`,
|
|
4428
|
+
(err as Error).message,
|
|
4429
|
+
);
|
|
4430
|
+
// Continue signing other files even if one fails
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
// Sign .node native modules in Resources/app/bun
|
|
4435
|
+
const resourcesPath = join(contentsPath, "Resources", "app", "bun");
|
|
4436
|
+
if (existsSync(resourcesPath)) {
|
|
4437
|
+
console.log("Signing native modules in Resources/app/bun...");
|
|
4438
|
+
try {
|
|
4439
|
+
const nodeFiles = execSync(`find ${escapePathForTerminal(resourcesPath)} -name "*.node" -type f`, {
|
|
4440
|
+
encoding: "utf8",
|
|
4441
|
+
}).trim().split("\n").filter(Boolean);
|
|
4442
|
+
|
|
4443
|
+
for (const nodeFile of nodeFiles) {
|
|
4444
|
+
if (nodeFile) {
|
|
4445
|
+
console.log(`Signing native module: ${nodeFile.replace(resourcesPath + "/", "")}`);
|
|
4446
|
+
execSync(
|
|
4447
|
+
`codesign --force --verbose --timestamp --sign "${SPARKBUN_DEVELOPER_ID}" --options runtime ${escapePathForTerminal(nodeFile)}`,
|
|
4448
|
+
);
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
} catch (err) {
|
|
4452
|
+
console.error("Error signing native modules:", err);
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
// Note: main.js is now in Resources and will be automatically sealed when signing the app bundle
|
|
4457
|
+
|
|
4458
|
+
// Sign the main executable
|
|
4459
|
+
const launcherPath = join(macosPath, config.app.name.replace(/ /g, ""));
|
|
4460
|
+
if (existsSync(launcherPath)) {
|
|
4461
|
+
console.log("Signing main executable (launcher)");
|
|
4462
|
+
const entitlementFlag = entitlementsFilePath
|
|
4463
|
+
? `--entitlements ${entitlementsFilePath}`
|
|
4464
|
+
: "";
|
|
4465
|
+
try {
|
|
4466
|
+
execSync(
|
|
4467
|
+
`codesign --force --verbose --timestamp --sign "${SPARKBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(launcherPath)}`,
|
|
4468
|
+
);
|
|
4469
|
+
} catch (error) {
|
|
4470
|
+
console.error("Failed to sign launcher:", (error as Error).message);
|
|
4471
|
+
console.log("Attempting to sign launcher without runtime hardening...");
|
|
4472
|
+
execSync(
|
|
4473
|
+
`codesign --force --verbose --timestamp --sign "${SPARKBUN_DEVELOPER_ID}" ${entitlementFlag} ${escapePathForTerminal(launcherPath)}`,
|
|
4474
|
+
);
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
// Finally, sign the app bundle itself (without --deep)
|
|
4479
|
+
console.log("Signing app bundle");
|
|
4480
|
+
const entitlementFlag = entitlementsFilePath
|
|
4481
|
+
? `--entitlements ${entitlementsFilePath}`
|
|
4482
|
+
: "";
|
|
4483
|
+
execSync(
|
|
4484
|
+
`codesign --force --verbose --timestamp --sign "${SPARKBUN_DEVELOPER_ID}" --options runtime ${entitlementFlag} ${escapePathForTerminal(appBundleOrDmgPath)}`,
|
|
4485
|
+
);
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
function notarizeAndStaple(
|
|
4489
|
+
appOrDmgPath: string,
|
|
4490
|
+
config: Awaited<ReturnType<typeof getConfig>>,
|
|
4491
|
+
) {
|
|
4492
|
+
if (OS !== "macos" || !config.build.mac.notarize) {
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
4495
|
+
|
|
4496
|
+
let fileToNotarize = appOrDmgPath;
|
|
4497
|
+
// codesign
|
|
4498
|
+
// NOTE: Codesigning fails in dev mode (when using a single-file-executable bun cli as the launcher)
|
|
4499
|
+
// see https://github.com/oven-sh/bun/issues/7208
|
|
4500
|
+
// if (shouldNotarize) {
|
|
4501
|
+
console.log("notarizing...");
|
|
4502
|
+
const zipPath = appOrDmgPath + ".zip";
|
|
4503
|
+
// if (appOrDmgPath.endsWith('.app')) {
|
|
4504
|
+
const appBundleFileName = basename(appOrDmgPath);
|
|
4505
|
+
// if we're codesigning the .app we have to zip it first
|
|
4506
|
+
execSync(
|
|
4507
|
+
`zip -y -r -9 ${escapePathForTerminal(zipPath)} ${escapePathForTerminal(
|
|
4508
|
+
appBundleFileName,
|
|
4509
|
+
)}`,
|
|
4510
|
+
{
|
|
4511
|
+
cwd: dirname(appOrDmgPath),
|
|
4512
|
+
},
|
|
4513
|
+
);
|
|
4514
|
+
fileToNotarize = zipPath;
|
|
4515
|
+
// }
|
|
4516
|
+
|
|
4517
|
+
const SPARKBUN_APPLEID = process.env["SPARKBUN_APPLEID"];
|
|
4518
|
+
const SPARKBUN_APPLEIDPASS = process.env["SPARKBUN_APPLEIDPASS"];
|
|
4519
|
+
const SPARKBUN_TEAMID = process.env["SPARKBUN_TEAMID"];
|
|
4520
|
+
|
|
4521
|
+
const SPARKBUN_APPLEAPIISSUER = process.env["SPARKBUN_APPLEAPIISSUER"];
|
|
4522
|
+
const SPARKBUN_APPLEAPIKEY = process.env["SPARKBUN_APPLEAPIKEY"];
|
|
4523
|
+
const SPARKBUN_APPLEAPIKEYPATH = process.env["SPARKBUN_APPLEAPIKEYPATH"];
|
|
4524
|
+
|
|
4525
|
+
const useApiKey = SPARKBUN_APPLEAPIISSUER && SPARKBUN_APPLEAPIKEY && SPARKBUN_APPLEAPIKEYPATH;
|
|
4526
|
+
const useAppleId = SPARKBUN_APPLEID && SPARKBUN_APPLEIDPASS && SPARKBUN_TEAMID;
|
|
4527
|
+
|
|
4528
|
+
if (!useApiKey && !useAppleId) {
|
|
4529
|
+
console.error("Provide either App Store Connect API key credentials (SPARKBUN_APPLEAPIISSUER, SPARKBUN_APPLEAPIKEY, SPARKBUN_APPLEAPIKEYPATH) or Apple ID credentials (SPARKBUN_APPLEID, SPARKBUN_APPLEIDPASS, SPARKBUN_TEAMID) to notarize");
|
|
4530
|
+
process.exit(1);
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
let statusInfo: string;
|
|
4534
|
+
if (useApiKey) {
|
|
4535
|
+
if (!existsSync(SPARKBUN_APPLEAPIKEYPATH)) {
|
|
4536
|
+
console.error(`SPARKBUN_APPLEAPIKEYPATH does not exist: ${SPARKBUN_APPLEAPIKEYPATH}`);
|
|
4537
|
+
process.exit(1);
|
|
4538
|
+
}
|
|
4539
|
+
statusInfo = execSync(
|
|
4540
|
+
`xcrun notarytool submit --key "${SPARKBUN_APPLEAPIKEYPATH}" --key-id "${SPARKBUN_APPLEAPIKEY}" --issuer "${SPARKBUN_APPLEAPIISSUER}" --wait ${escapePathForTerminal(
|
|
4541
|
+
fileToNotarize,
|
|
4542
|
+
)}`,
|
|
4543
|
+
).toString();
|
|
4544
|
+
} else {
|
|
4545
|
+
// notarize
|
|
4546
|
+
statusInfo = execSync(
|
|
4547
|
+
`xcrun notarytool submit --apple-id "${SPARKBUN_APPLEID}" --password "${SPARKBUN_APPLEIDPASS}" --team-id "${SPARKBUN_TEAMID}" --wait ${escapePathForTerminal(
|
|
4548
|
+
fileToNotarize,
|
|
4549
|
+
)}`,
|
|
4550
|
+
).toString();
|
|
4551
|
+
}
|
|
4552
|
+
const uuid = statusInfo.match(/id: ([^\n]+)/)?.[1];
|
|
4553
|
+
console.log("statusInfo", statusInfo);
|
|
4554
|
+
console.log("uuid", uuid);
|
|
4555
|
+
|
|
4556
|
+
if (statusInfo.match("Current status: Invalid")) {
|
|
4557
|
+
console.error("notarization failed", statusInfo);
|
|
4558
|
+
let log: string;
|
|
4559
|
+
if (useApiKey) {
|
|
4560
|
+
log = execSync(
|
|
4561
|
+
`xcrun notarytool log --key "${SPARKBUN_APPLEAPIKEYPATH}" --key-id "${SPARKBUN_APPLEAPIKEY}" --issuer "${SPARKBUN_APPLEAPIISSUER}" ${uuid}`,
|
|
4562
|
+
).toString();
|
|
4563
|
+
} else {
|
|
4564
|
+
log = execSync(
|
|
4565
|
+
`xcrun notarytool log --apple-id "${SPARKBUN_APPLEID}" --password "${SPARKBUN_APPLEIDPASS}" --team-id "${SPARKBUN_TEAMID}" ${uuid}`,
|
|
4566
|
+
).toString();
|
|
4567
|
+
}
|
|
4568
|
+
console.log("log", log);
|
|
4569
|
+
process.exit(1);
|
|
4570
|
+
}
|
|
4571
|
+
// check notarization
|
|
4572
|
+
// use `notarytool info` or some other request thing to check separately from the wait above
|
|
4573
|
+
|
|
4574
|
+
// stable notarization
|
|
4575
|
+
console.log("stapling...");
|
|
4576
|
+
execSync(`xcrun stapler staple ${escapePathForTerminal(appOrDmgPath)}`);
|
|
4577
|
+
|
|
4578
|
+
if (existsSync(zipPath)) {
|
|
4579
|
+
unlinkSync(zipPath);
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
|
|
4583
|
+
// 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
|
|
4584
|
+
// 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.
|
|
4585
|
+
// either way you can pass in the parent folder here for that flexibility.
|
|
4586
|
+
// for intel/arm builds on mac we'll probably have separate subfolders as well and build them in parallel.
|
|
4587
|
+
function createAppBundle(
|
|
4588
|
+
bundleName: string,
|
|
4589
|
+
parentFolder: string,
|
|
4590
|
+
targetOS: "macos" | "win" | "linux",
|
|
4591
|
+
) {
|
|
4592
|
+
if (targetOS === "macos") {
|
|
4593
|
+
// macOS bundle structure
|
|
4594
|
+
const bundleFileName = `${bundleName}.app`;
|
|
4595
|
+
const appBundleFolderPath = join(parentFolder, bundleFileName);
|
|
4596
|
+
const appBundleFolderContentsPath = join(appBundleFolderPath, "Contents");
|
|
4597
|
+
const appBundleMacOSPath = join(appBundleFolderContentsPath, "MacOS");
|
|
4598
|
+
const appBundleFolderResourcesPath = join(
|
|
4599
|
+
appBundleFolderContentsPath,
|
|
4600
|
+
"Resources",
|
|
4601
|
+
);
|
|
4602
|
+
const appBundleFolderFrameworksPath = join(
|
|
4603
|
+
appBundleFolderContentsPath,
|
|
4604
|
+
"Frameworks",
|
|
4605
|
+
);
|
|
4606
|
+
|
|
4607
|
+
// we don't have to make all the folders, just the deepest ones
|
|
4608
|
+
mkdirSync(appBundleMacOSPath, { recursive: true });
|
|
4609
|
+
mkdirSync(appBundleFolderResourcesPath, { recursive: true });
|
|
4610
|
+
mkdirSync(appBundleFolderFrameworksPath, { recursive: true });
|
|
4611
|
+
|
|
4612
|
+
return {
|
|
4613
|
+
appBundleFolderPath,
|
|
4614
|
+
appBundleFolderContentsPath,
|
|
4615
|
+
appBundleMacOSPath,
|
|
4616
|
+
appBundleFolderResourcesPath,
|
|
4617
|
+
appBundleFolderFrameworksPath,
|
|
4618
|
+
};
|
|
4619
|
+
} else if (targetOS === "linux" || targetOS === "win") {
|
|
4620
|
+
// Linux/Windows simpler structure
|
|
4621
|
+
const appBundleFolderPath = join(parentFolder, bundleName);
|
|
4622
|
+
const appBundleFolderContentsPath = appBundleFolderPath; // No Contents folder needed
|
|
4623
|
+
const appBundleMacOSPath = join(appBundleFolderPath, "bin"); // Use bin instead of MacOS
|
|
4624
|
+
const appBundleFolderResourcesPath = join(
|
|
4625
|
+
appBundleFolderPath,
|
|
4626
|
+
"Resources",
|
|
4627
|
+
);
|
|
4628
|
+
const appBundleFolderFrameworksPath = join(appBundleFolderPath, "lib"); // Use lib instead of Frameworks
|
|
4629
|
+
|
|
4630
|
+
// Create directories
|
|
4631
|
+
mkdirSync(appBundleMacOSPath, { recursive: true });
|
|
4632
|
+
mkdirSync(appBundleFolderResourcesPath, { recursive: true });
|
|
4633
|
+
mkdirSync(appBundleFolderFrameworksPath, { recursive: true });
|
|
4634
|
+
|
|
4635
|
+
return {
|
|
4636
|
+
appBundleFolderPath,
|
|
4637
|
+
appBundleFolderContentsPath,
|
|
4638
|
+
appBundleMacOSPath,
|
|
4639
|
+
appBundleFolderResourcesPath,
|
|
4640
|
+
appBundleFolderFrameworksPath,
|
|
4641
|
+
};
|
|
4642
|
+
} else {
|
|
4643
|
+
throw new Error(`Unsupported OS: ${targetOS}`);
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
// Close the command handling if/else chain
|
|
4648
|
+
|
|
4649
|
+
// Close and execute the async IIFE
|
|
4650
|
+
})().catch((error) => {
|
|
4651
|
+
console.error("Fatal error:", error);
|
|
4652
|
+
process.exit(1);
|
|
4653
|
+
});
|