sparkbun 0.1.8 → 0.2.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/dist-win-x64/nsis/Bin/makensis.exe +0 -0
- package/dist-win-x64/nsis/Stubs/bzip2-amd64-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/bzip2-x86-ansi +0 -0
- package/dist-win-x64/nsis/Stubs/bzip2-x86-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/bzip2_solid-amd64-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/bzip2_solid-x86-ansi +0 -0
- package/dist-win-x64/nsis/Stubs/bzip2_solid-x86-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/lz4-amd64-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/lz4-x86-ansi +0 -0
- package/dist-win-x64/nsis/Stubs/lz4-x86-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/lz4_solid-amd64-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/lz4_solid-x86-ansi +0 -0
- package/dist-win-x64/nsis/Stubs/lz4_solid-x86-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/lzma-amd64-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/lzma-x86-ansi +0 -0
- package/dist-win-x64/nsis/Stubs/lzma-x86-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/lzma_solid-amd64-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/lzma_solid-x86-ansi +0 -0
- package/dist-win-x64/nsis/Stubs/lzma_solid-x86-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/uninst +0 -0
- package/dist-win-x64/nsis/Stubs/zlib-amd64-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/zlib-x86-ansi +0 -0
- package/dist-win-x64/nsis/Stubs/zlib-x86-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/zlib_solid-amd64-unicode +0 -0
- package/dist-win-x64/nsis/Stubs/zlib_solid-x86-ansi +0 -0
- package/dist-win-x64/nsis/Stubs/zlib_solid-x86-unicode +0 -0
- package/package.json +1 -1
- package/src/cli/index.ts +395 -3
- package/src/installer/installer-wrapper-template.ts +158 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -76,7 +76,7 @@ const commandArg = process.argv[indexOfCli + 1] || "build";
|
|
|
76
76
|
// Walk up from projectRoot to find electrobun in node_modules (supports hoisted monorepo layouts)
|
|
77
77
|
function resolveSparkBunDir(): string {
|
|
78
78
|
// When running from SparkBun source (src/cli/index.ts), the package root is two levels up
|
|
79
|
-
const cliDir = dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"));
|
|
79
|
+
const cliDir = dirname(decodeURIComponent(new URL(import.meta.url).pathname).replace(/^\/([A-Za-z]:)/, "$1"));
|
|
80
80
|
const sourcePackageDir = join(cliDir, "..", "..");
|
|
81
81
|
if (existsSync(join(sourcePackageDir, "package.json")) && existsSync(join(sourcePackageDir, "src", "cli"))) {
|
|
82
82
|
return sourcePackageDir;
|
|
@@ -1663,6 +1663,17 @@ ${utiDecls}
|
|
|
1663
1663
|
}
|
|
1664
1664
|
await runAppWithSignalHandling(config);
|
|
1665
1665
|
}
|
|
1666
|
+
} else if (commandArg === "installer") {
|
|
1667
|
+
const config = await getConfig();
|
|
1668
|
+
const envArg =
|
|
1669
|
+
process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
|
|
1670
|
+
const buildEnvironment: "dev" | "stable" = envArg === "dev" ? "dev" : "stable";
|
|
1671
|
+
try {
|
|
1672
|
+
await runInstallerBuild(config, buildEnvironment);
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
console.error("Installer build failed:", error);
|
|
1675
|
+
process.exit(1);
|
|
1676
|
+
}
|
|
1666
1677
|
}
|
|
1667
1678
|
|
|
1668
1679
|
async function runBuild(
|
|
@@ -2067,7 +2078,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
2067
2078
|
}
|
|
2068
2079
|
|
|
2069
2080
|
// Compile launcher from source using Bun.build() API
|
|
2070
|
-
const cliDir = dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"));
|
|
2081
|
+
const cliDir = dirname(decodeURIComponent(new URL(import.meta.url).pathname).replace(/^\/([A-Za-z]:)/, "$1"));
|
|
2071
2082
|
const sparkbunRoot = join(cliDir, "..", "..");
|
|
2072
2083
|
const launcherSourcePath = join(sparkbunRoot, "src", "launcher", "main.ts");
|
|
2073
2084
|
const bunTarget = `bun-${targetOS === "macos" ? "darwin" : targetOS === "win" ? "windows" : "linux"}-${currentTarget.arch}` as const;
|
|
@@ -3187,6 +3198,387 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3187
3198
|
// stapler validate -v <app path>
|
|
3188
3199
|
}
|
|
3189
3200
|
|
|
3201
|
+
async function runInstallerBuild(
|
|
3202
|
+
config: Awaited<ReturnType<typeof getConfig>>,
|
|
3203
|
+
buildEnvironment: "dev" | "stable",
|
|
3204
|
+
) {
|
|
3205
|
+
// --- Parse installer-specific CLI flags ---
|
|
3206
|
+
|
|
3207
|
+
const archives: { name: string; path: string }[] = [];
|
|
3208
|
+
for (let i = 0; i < process.argv.length; i++) {
|
|
3209
|
+
if (process.argv[i] === "--archive" && process.argv[i + 1]) {
|
|
3210
|
+
const arg = process.argv[i + 1]!;
|
|
3211
|
+
const eqIdx = arg.indexOf("=");
|
|
3212
|
+
if (eqIdx === -1) {
|
|
3213
|
+
console.error(`Invalid --archive format: "${arg}". Expected: name=path`);
|
|
3214
|
+
process.exit(1);
|
|
3215
|
+
}
|
|
3216
|
+
const name = arg.substring(0, eqIdx);
|
|
3217
|
+
const archivePath = arg.substring(eqIdx + 1);
|
|
3218
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
3219
|
+
console.error(`Invalid archive name: "${name}". Must be a valid identifier ([a-zA-Z_][a-zA-Z0-9_]*).`);
|
|
3220
|
+
process.exit(1);
|
|
3221
|
+
}
|
|
3222
|
+
if (archives.some((a) => a.name === name)) {
|
|
3223
|
+
console.error(`Duplicate archive name: "${name}".`);
|
|
3224
|
+
process.exit(1);
|
|
3225
|
+
}
|
|
3226
|
+
const resolvedPath = path.resolve(projectRoot, archivePath);
|
|
3227
|
+
if (!existsSync(resolvedPath)) {
|
|
3228
|
+
console.error(`Archive path does not exist: ${resolvedPath}`);
|
|
3229
|
+
process.exit(1);
|
|
3230
|
+
}
|
|
3231
|
+
archives.push({ name, path: resolvedPath });
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
const getFlag = (prefix: string): string | undefined =>
|
|
3236
|
+
process.argv.find((arg) => arg.startsWith(prefix))?.split("=").slice(1).join("=");
|
|
3237
|
+
|
|
3238
|
+
const outPath = getFlag("--out=");
|
|
3239
|
+
const iconPath = getFlag("--icon=");
|
|
3240
|
+
const installerName = getFlag("--name=") || config.app.name;
|
|
3241
|
+
const installerVersion = getFlag("--version=") || config.app.version;
|
|
3242
|
+
const installerPublisher = getFlag("--publisher=") || (config.app as any).publisher || "";
|
|
3243
|
+
const metadataPath = getFlag("--metadata=");
|
|
3244
|
+
|
|
3245
|
+
// Determine target platform
|
|
3246
|
+
const targetArg = getFlag("--target=");
|
|
3247
|
+
let targetOS: "macos" | "win" | "linux" = OS;
|
|
3248
|
+
let targetARCH: "arm64" | "x64" = ARCH;
|
|
3249
|
+
if (targetArg) {
|
|
3250
|
+
const [tOS, tArch] = targetArg.split("-") as [string, string];
|
|
3251
|
+
const osMap: Record<string, "macos" | "win" | "linux"> = { macos: "macos", darwin: "macos", win: "win", windows: "win", linux: "linux" };
|
|
3252
|
+
const archMap: Record<string, "arm64" | "x64"> = { arm64: "arm64", aarch64: "arm64", x64: "x64", x86_64: "x64" };
|
|
3253
|
+
if (osMap[tOS] && archMap[tArch]) {
|
|
3254
|
+
targetOS = osMap[tOS];
|
|
3255
|
+
targetARCH = archMap[tArch];
|
|
3256
|
+
console.log(`Cross-compiling installer for ${targetOS}-${targetARCH}`);
|
|
3257
|
+
} else {
|
|
3258
|
+
console.error(`Invalid target: ${targetArg}. Use format: win-x64, linux-arm64, macos-arm64`);
|
|
3259
|
+
process.exit(1);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
const targetBinExt = targetOS === "win" ? ".exe" : "";
|
|
3264
|
+
|
|
3265
|
+
console.log(`Building installer: ${installerName} v${installerVersion}`);
|
|
3266
|
+
if (archives.length > 0) {
|
|
3267
|
+
console.log(`Archives: ${archives.map((a) => `${a.name}=${a.path}`).join(", ")}`);
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
// --- Ensure native dependencies ---
|
|
3271
|
+
|
|
3272
|
+
await ensureCoreDependencies(targetOS, targetARCH);
|
|
3273
|
+
const targetPaths = getPlatformPaths(targetOS, targetARCH);
|
|
3274
|
+
|
|
3275
|
+
// --- Set up build folder ---
|
|
3276
|
+
|
|
3277
|
+
const platformPrefix = getPlatformPrefix(buildEnvironment, targetOS, targetARCH);
|
|
3278
|
+
const buildFolder = join(projectRoot, config.build.buildFolder, `installer-${platformPrefix}`);
|
|
3279
|
+
|
|
3280
|
+
if (existsSync(buildFolder)) {
|
|
3281
|
+
rmSync(buildFolder, { recursive: true, force: true });
|
|
3282
|
+
}
|
|
3283
|
+
mkdirSync(buildFolder, { recursive: true });
|
|
3284
|
+
|
|
3285
|
+
const cliDir = dirname(decodeURIComponent(new URL(import.meta.url).pathname).replace(/^\/([A-Za-z]:)/, "$1"));
|
|
3286
|
+
const sparkbunRoot = join(cliDir, "..", "..");
|
|
3287
|
+
const bunTarget = `bun-${targetOS === "macos" ? "darwin" : targetOS === "win" ? "windows" : "linux"}-${targetARCH}` as const;
|
|
3288
|
+
const targetOSName = targetOS === "macos" ? "darwin" : targetOS === "win" ? "windows" : "linux";
|
|
3289
|
+
const isWindows = targetOS === "win";
|
|
3290
|
+
|
|
3291
|
+
// --- Create archives first (they go into the compiled binary, not the runtime bundle) ---
|
|
3292
|
+
|
|
3293
|
+
const stagingDir = join(buildFolder, ".installer-staging");
|
|
3294
|
+
if (existsSync(stagingDir)) rmSync(stagingDir, { recursive: true, force: true });
|
|
3295
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
3296
|
+
|
|
3297
|
+
const archiveManifest: Record<string, string> = {};
|
|
3298
|
+
|
|
3299
|
+
if (archives.length > 0) {
|
|
3300
|
+
console.log("Creating archives (LZMA solid via NSIS)...");
|
|
3301
|
+
|
|
3302
|
+
const nsisDir = join(SPARKBUN_DEP_PATH, `dist-${targetOS}-${targetARCH}`, "nsis");
|
|
3303
|
+
const makensisPath = join(nsisDir, "Bin", "makensis.exe");
|
|
3304
|
+
if (!existsSync(makensisPath)) {
|
|
3305
|
+
throw new Error(`makensis not found at ${makensisPath}. LZMA compression requires NSIS binaries in dist-${targetOS}-${targetARCH}/nsis/`);
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
for (const { name, path: archiveSrcPath } of archives) {
|
|
3309
|
+
const archiveFileName = `archive-${name}.exe`;
|
|
3310
|
+
const archiveDestPath = join(stagingDir, archiveFileName);
|
|
3311
|
+
const nsiPath = join(stagingDir, `archive-${name}.nsi`);
|
|
3312
|
+
|
|
3313
|
+
console.log(` Archiving ${name}: ${archiveSrcPath}`);
|
|
3314
|
+
|
|
3315
|
+
const nsiContent = `SetCompressor /SOLID lzma\nSilentInstall silent\nName "${name}"\nOutFile "${archiveDestPath.replace(/\\/g, "\\\\")}"\nInstallDir "$TEMP\\sparkbun-extract-${name}"\nSection\n SetOutPath "$INSTDIR"\n File /r "${archiveSrcPath.replace(/\\/g, "\\\\")}\\*.*"\nSectionEnd\n`;
|
|
3316
|
+
writeFileSync(nsiPath, nsiContent);
|
|
3317
|
+
|
|
3318
|
+
const result = Bun.spawnSync([makensisPath, nsiPath], {
|
|
3319
|
+
cwd: nsisDir,
|
|
3320
|
+
env: { ...process.env, NSISDIR: nsisDir },
|
|
3321
|
+
stdout: "pipe",
|
|
3322
|
+
stderr: "pipe",
|
|
3323
|
+
});
|
|
3324
|
+
|
|
3325
|
+
if (result.exitCode !== 0) {
|
|
3326
|
+
const stderr = result.stderr.toString();
|
|
3327
|
+
const stdout = result.stdout.toString();
|
|
3328
|
+
console.error(`makensis failed for ${name}:`, stderr || stdout);
|
|
3329
|
+
throw new Error(`Failed to compress archive: ${name}`);
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
unlinkSync(nsiPath);
|
|
3333
|
+
|
|
3334
|
+
const archiveSize = statSync(archiveDestPath).size;
|
|
3335
|
+
console.log(` ${name}: ${(archiveSize / 1024 / 1024).toFixed(2)} MB`);
|
|
3336
|
+
archiveManifest[name] = archiveFileName;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
// --- Build the runtime bundle (DLLs, views, app code — NO launcher binary, NO archives) ---
|
|
3341
|
+
|
|
3342
|
+
console.log("Building runtime bundle...");
|
|
3343
|
+
const appFileName = getAppFileName(config.app.name, buildEnvironment);
|
|
3344
|
+
const bundle = createAppBundle(appFileName, buildFolder, targetOS);
|
|
3345
|
+
const appBundleFolderPath = bundle.appBundleFolderPath;
|
|
3346
|
+
const appBundleMacOSPath = bundle.appBundleMacOSPath;
|
|
3347
|
+
const appBundleFolderResourcesPath = bundle.appBundleFolderResourcesPath;
|
|
3348
|
+
const appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
|
|
3349
|
+
mkdirSync(appBundleAppCodePath, { recursive: true });
|
|
3350
|
+
mkdirSync(appBundleMacOSPath, { recursive: true });
|
|
3351
|
+
|
|
3352
|
+
// Copy native wrappers
|
|
3353
|
+
if (targetOS === "win") {
|
|
3354
|
+
cpSync(targetPaths.CORE_WIN, join(appBundleMacOSPath, "SparkBunCore.dll"), { dereference: true });
|
|
3355
|
+
cpSync(targetPaths.NATIVE_WRAPPER_WIN, join(appBundleMacOSPath, "libNativeWrapper.dll"), { dereference: true });
|
|
3356
|
+
cpSync(targetPaths.WEBVIEW2LOADER_WIN, join(appBundleMacOSPath, "WebView2Loader.dll"), { dereference: true });
|
|
3357
|
+
} else if (targetOS === "macos") {
|
|
3358
|
+
cpSync(targetPaths.CORE_MACOS, join(appBundleMacOSPath, "libSparkBunCore.dylib"), { dereference: true });
|
|
3359
|
+
cpSync(targetPaths.NATIVE_WRAPPER_MACOS, join(appBundleMacOSPath, "libNativeWrapper.dylib"), { dereference: true });
|
|
3360
|
+
} else if (targetOS === "linux") {
|
|
3361
|
+
cpSync(targetPaths.CORE_LINUX, join(appBundleMacOSPath, "libSparkBunCore.so"), { dereference: true });
|
|
3362
|
+
cpSync(targetPaths.NATIVE_WRAPPER_LINUX, join(appBundleMacOSPath, "libNativeWrapper.so"), { dereference: true });
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
// libasar stub
|
|
3366
|
+
const libExt = targetOS === "win" ? ".dll" : targetOS === "macos" ? ".dylib" : ".so";
|
|
3367
|
+
const platformDistDir = join(SPARKBUN_DEP_PATH, `dist-${targetOS}-${targetARCH}`);
|
|
3368
|
+
const asarLibSource = join(platformDistDir, "libasar" + libExt);
|
|
3369
|
+
if (existsSync(asarLibSource)) {
|
|
3370
|
+
cpSync(asarLibSource, join(appBundleMacOSPath, "libasar" + libExt), { dereference: true });
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
// Bundle bun code
|
|
3374
|
+
const bunConfig = config.build.bun;
|
|
3375
|
+
const bunSource = join(projectRoot, bunConfig.entrypoint);
|
|
3376
|
+
if (!existsSync(bunSource)) {
|
|
3377
|
+
throw new Error(`Bun entrypoint not found: ${bunSource}`);
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
const bunDestFolder = join(appBundleAppCodePath, "bun");
|
|
3381
|
+
const { entrypoint: _bunEntrypoint, ...bunBuildOptions } = bunConfig;
|
|
3382
|
+
const bunBuildResult = await Bun.build({
|
|
3383
|
+
...bunBuildOptions,
|
|
3384
|
+
entrypoints: [bunSource],
|
|
3385
|
+
outdir: bunDestFolder,
|
|
3386
|
+
target: "bun",
|
|
3387
|
+
});
|
|
3388
|
+
if (!bunBuildResult.success) {
|
|
3389
|
+
console.error("Failed to build bun entrypoint:", bunSource);
|
|
3390
|
+
printBuildLogs(bunBuildResult.logs);
|
|
3391
|
+
throw new Error("Build failed: bun build failed");
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
// Bundle views
|
|
3395
|
+
for (const viewName in config.build.views) {
|
|
3396
|
+
const viewConfig = config.build.views[viewName]!;
|
|
3397
|
+
const viewSource = join(projectRoot, viewConfig.entrypoint);
|
|
3398
|
+
if (!existsSync(viewSource)) {
|
|
3399
|
+
console.error(`View entrypoint not found: ${viewSource}`);
|
|
3400
|
+
continue;
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
const viewDestFolder = join(appBundleAppCodePath, "views", viewName);
|
|
3404
|
+
mkdirSync(viewDestFolder, { recursive: true });
|
|
3405
|
+
|
|
3406
|
+
const { entrypoint: _viewEntrypoint, ...viewBuildOptions } = viewConfig;
|
|
3407
|
+
const viewBuildResult = await Bun.build({
|
|
3408
|
+
...viewBuildOptions,
|
|
3409
|
+
entrypoints: [viewSource],
|
|
3410
|
+
outdir: viewDestFolder,
|
|
3411
|
+
target: "browser",
|
|
3412
|
+
});
|
|
3413
|
+
if (!viewBuildResult.success) {
|
|
3414
|
+
console.error("Failed to build view:", viewSource);
|
|
3415
|
+
printBuildLogs(viewBuildResult.logs);
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
// Copy assets
|
|
3420
|
+
for (const relSource in config.build.copy) {
|
|
3421
|
+
const source = join(projectRoot, relSource);
|
|
3422
|
+
if (!existsSync(source)) {
|
|
3423
|
+
console.error(`Asset not found: ${source}`);
|
|
3424
|
+
continue;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
const destination = join(appBundleAppCodePath, config.build.copy[relSource]!);
|
|
3428
|
+
const destFolder = dirname(destination);
|
|
3429
|
+
if (!existsSync(destFolder)) {
|
|
3430
|
+
mkdirSync(destFolder, { recursive: true });
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
cpSync(source, destination, { recursive: true, dereference: true });
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
// Write build.json
|
|
3437
|
+
const buildJson = {
|
|
3438
|
+
name: config.app.name,
|
|
3439
|
+
identifier: config.app.identifier,
|
|
3440
|
+
version: installerVersion,
|
|
3441
|
+
channel: buildEnvironment,
|
|
3442
|
+
...config.runtime,
|
|
3443
|
+
};
|
|
3444
|
+
await Bun.write(join(appBundleAppCodePath, "build.json"), JSON.stringify(buildJson, null, 2));
|
|
3445
|
+
|
|
3446
|
+
// Write archives.json manifest into runtime bundle so app code knows archive names
|
|
3447
|
+
await Bun.write(
|
|
3448
|
+
join(appBundleFolderResourcesPath, "archives.json"),
|
|
3449
|
+
JSON.stringify(archiveManifest, null, 2),
|
|
3450
|
+
);
|
|
3451
|
+
|
|
3452
|
+
// --- Create runtime bundle tar.gz (small — no launcher binary, no archives) ---
|
|
3453
|
+
|
|
3454
|
+
console.log("Packaging runtime bundle (gzip level 12)...");
|
|
3455
|
+
const runtimeFiles: Record<string, ArrayBuffer> = {};
|
|
3456
|
+
const bundleBase = basename(appBundleFolderPath);
|
|
3457
|
+
async function walkBundle(dir: string, prefix: string) {
|
|
3458
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
3459
|
+
const fullPath = join(dir, entry.name);
|
|
3460
|
+
const archivePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
3461
|
+
if (entry.isDirectory()) {
|
|
3462
|
+
await walkBundle(fullPath, archivePath);
|
|
3463
|
+
} else {
|
|
3464
|
+
runtimeFiles[archivePath] = await Bun.file(fullPath).arrayBuffer();
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
await walkBundle(appBundleFolderPath, bundleBase);
|
|
3469
|
+
|
|
3470
|
+
const runtimeArchive = new Bun.Archive(runtimeFiles, { compress: "gzip", level: 12 });
|
|
3471
|
+
const runtimeCompressedPath = join(buildFolder, `${appFileName}-runtime.tar.gz`);
|
|
3472
|
+
await Bun.write(runtimeCompressedPath, await runtimeArchive.bytes());
|
|
3473
|
+
rmSync(appBundleFolderPath, { recursive: true });
|
|
3474
|
+
|
|
3475
|
+
const runtimeSize = statSync(runtimeCompressedPath).size;
|
|
3476
|
+
console.log(` Runtime bundle: ${(runtimeSize / 1024 / 1024).toFixed(2)} MB`);
|
|
3477
|
+
|
|
3478
|
+
// --- Generate entrypoint and compile single installer exe ---
|
|
3479
|
+
|
|
3480
|
+
// Copy runtime bundle into staging
|
|
3481
|
+
copyFileSync(runtimeCompressedPath, join(stagingDir, "runtime-bundle.tar.gz"));
|
|
3482
|
+
|
|
3483
|
+
// Write metadata
|
|
3484
|
+
const metadata: Record<string, unknown> = {
|
|
3485
|
+
name: installerName,
|
|
3486
|
+
version: installerVersion,
|
|
3487
|
+
identifier: config.app.identifier,
|
|
3488
|
+
channel: buildEnvironment,
|
|
3489
|
+
requireAdmin: (config.build?.win as any)?.requireAdmin || (config.build?.linux as any)?.requireAdmin || false,
|
|
3490
|
+
};
|
|
3491
|
+
if (metadataPath) {
|
|
3492
|
+
const userMetadata = JSON.parse(readFileSync(path.resolve(projectRoot, metadataPath), "utf-8"));
|
|
3493
|
+
Object.assign(metadata, userMetadata);
|
|
3494
|
+
}
|
|
3495
|
+
writeFileSync(join(stagingDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
3496
|
+
|
|
3497
|
+
// Generate the entrypoint that imports the wrapper template + all archive files
|
|
3498
|
+
// The wrapper template imports runtime-bundle.tar.gz and metadata.json.
|
|
3499
|
+
// We generate an archives module that the app code can import at runtime.
|
|
3500
|
+
const wrapperTemplatePath = join(cliDir, "..", "installer", "installer-wrapper-template.ts");
|
|
3501
|
+
copyFileSync(wrapperTemplatePath, join(stagingDir, "installer.ts"));
|
|
3502
|
+
|
|
3503
|
+
// Generate archives.ts that embeds each archive via static imports
|
|
3504
|
+
const archiveImports: string[] = [];
|
|
3505
|
+
const archiveExports: string[] = [];
|
|
3506
|
+
for (const { name } of archives) {
|
|
3507
|
+
const fileName = archiveManifest[name];
|
|
3508
|
+
archiveImports.push(`import archive_${name} from "./${fileName}" with { type: "file" };`);
|
|
3509
|
+
archiveExports.push(` "${name}": archive_${name},`);
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
const archivesModuleContent = `${archiveImports.join("\n")}
|
|
3513
|
+
|
|
3514
|
+
export const embeddedArchives: Record<string, string> = {
|
|
3515
|
+
${archiveExports.join("\n")}
|
|
3516
|
+
};
|
|
3517
|
+
`;
|
|
3518
|
+
writeFileSync(join(stagingDir, "embedded-archives.ts"), archivesModuleContent);
|
|
3519
|
+
|
|
3520
|
+
// --- Compile single installer exe ---
|
|
3521
|
+
|
|
3522
|
+
const defaultOutName = `${installerName.replace(/ /g, "-")}-Setup${targetBinExt}`;
|
|
3523
|
+
const outputPath = outPath
|
|
3524
|
+
? path.resolve(projectRoot, outPath)
|
|
3525
|
+
: join(buildFolder, defaultOutName);
|
|
3526
|
+
|
|
3527
|
+
console.log("Compiling installer executable...");
|
|
3528
|
+
|
|
3529
|
+
const installerCompileOptions: any = {
|
|
3530
|
+
target: `bun-${targetOSName}-${targetARCH}`,
|
|
3531
|
+
outfile: outputPath,
|
|
3532
|
+
};
|
|
3533
|
+
|
|
3534
|
+
if (isWindows && OS === "win") {
|
|
3535
|
+
let icoPath: string | undefined;
|
|
3536
|
+
if (iconPath) {
|
|
3537
|
+
const resolvedIcon = path.resolve(projectRoot, iconPath);
|
|
3538
|
+
if (existsSync(resolvedIcon)) {
|
|
3539
|
+
icoPath = resolvedIcon;
|
|
3540
|
+
if (resolvedIcon.toLowerCase().endsWith(".png")) {
|
|
3541
|
+
const pngToIco = (await import("png-to-ico")).default;
|
|
3542
|
+
const tempIcoPath = join(buildFolder, "temp-installer-icon.ico");
|
|
3543
|
+
const icoBuffer = await pngToIco(resolvedIcon);
|
|
3544
|
+
writeFileSync(tempIcoPath, new Uint8Array(icoBuffer));
|
|
3545
|
+
icoPath = tempIcoPath;
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
installerCompileOptions.windows = {
|
|
3550
|
+
hideConsole: true,
|
|
3551
|
+
...(icoPath && { icon: icoPath }),
|
|
3552
|
+
title: `${installerName} Setup`,
|
|
3553
|
+
version: installerVersion,
|
|
3554
|
+
description: `Installs ${installerName}`,
|
|
3555
|
+
publisher: installerPublisher || " ",
|
|
3556
|
+
copyright: (config.app as any).copyright || " ",
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
const installerBuild = await Bun.build({
|
|
3561
|
+
entrypoints: [join(stagingDir, "installer.ts")],
|
|
3562
|
+
compile: installerCompileOptions,
|
|
3563
|
+
});
|
|
3564
|
+
if (!installerBuild.success) {
|
|
3565
|
+
console.error("Installer compilation failed:", installerBuild.logs);
|
|
3566
|
+
throw new Error("Installer compilation failed");
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
if (isWindows) {
|
|
3570
|
+
patchPeSubsystem(outputPath);
|
|
3571
|
+
} else {
|
|
3572
|
+
execSync(`chmod +x ${escapePathForTerminal(outputPath)}`);
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
// Clean up staging
|
|
3576
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
3577
|
+
|
|
3578
|
+
const exeSize = statSync(outputPath).size;
|
|
3579
|
+
console.log(`\nInstaller built: ${outputPath} (${(exeSize / 1024 / 1024).toFixed(2)} MB)`);
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3190
3582
|
// Take over as the terminal's foreground process group (macOS/Linux).
|
|
3191
3583
|
// This prevents the parent bun script runner from receiving SIGINT
|
|
3192
3584
|
// when Ctrl+C is pressed, keeping the terminal busy until the app
|
|
@@ -3827,7 +4219,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3827
4219
|
writeFileSync(join(stagingDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
3828
4220
|
|
|
3829
4221
|
// Copy installer template
|
|
3830
|
-
const cliDir = dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"));
|
|
4222
|
+
const cliDir = dirname(decodeURIComponent(new URL(import.meta.url).pathname).replace(/^\/([A-Za-z]:)/, "$1"));
|
|
3831
4223
|
const templatePath = join(cliDir, "..", "installer", "installer-template.ts");
|
|
3832
4224
|
copyFileSync(templatePath, join(stagingDir, "installer.ts"));
|
|
3833
4225
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import runtimeArchive from "./runtime-bundle.tar.gz" with { type: "file" };
|
|
2
|
+
import metadataJson from "./metadata.json" with { type: "file" };
|
|
3
|
+
import { embeddedArchives } from "./embedded-archives";
|
|
4
|
+
import { join, dirname, resolve } from "path";
|
|
5
|
+
import { existsSync, mkdirSync, rmSync, readdirSync, writeFileSync } from "fs";
|
|
6
|
+
import { dlopen, suffix, ptr } from "bun:ffi";
|
|
7
|
+
|
|
8
|
+
interface Metadata {
|
|
9
|
+
name: string;
|
|
10
|
+
version?: string;
|
|
11
|
+
requireAdmin?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const metadata: Metadata = await Bun.file(metadataJson).json();
|
|
15
|
+
|
|
16
|
+
// Extract runtime files (DLLs, views, app code) to temp.
|
|
17
|
+
// Archives stay embedded in this binary — extracted during install by the app code.
|
|
18
|
+
const tempDir = join(
|
|
19
|
+
process.env.TEMP || process.env.TMP || process.env.LOCALAPPDATA || ".",
|
|
20
|
+
`.sparkbun-installer-${metadata.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(tempDir)) {
|
|
25
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const archiveBytes = await Bun.file(runtimeArchive).bytes();
|
|
29
|
+
const archive = new Bun.Archive(archiveBytes);
|
|
30
|
+
mkdirSync(tempDir, { recursive: true });
|
|
31
|
+
await archive.extract(tempDir);
|
|
32
|
+
|
|
33
|
+
// Find the bin directory inside the extracted bundle
|
|
34
|
+
let binDir = tempDir;
|
|
35
|
+
let bundleRoot = tempDir;
|
|
36
|
+
for (const entry of readdirSync(tempDir)) {
|
|
37
|
+
const entryPath = join(tempDir, entry);
|
|
38
|
+
if (existsSync(join(entryPath, "bin"))) {
|
|
39
|
+
binDir = join(entryPath, "bin");
|
|
40
|
+
bundleRoot = entryPath;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Write the embedded archive paths into the Resources directory so the app code can find them.
|
|
46
|
+
// Bun compile embeds files at virtual paths — we resolve them here and pass to the app.
|
|
47
|
+
const resourcesDir = join(bundleRoot, "Resources");
|
|
48
|
+
const archivePaths: Record<string, string> = {};
|
|
49
|
+
for (const [name, filePath] of Object.entries(embeddedArchives)) {
|
|
50
|
+
archivePaths[name] = filePath;
|
|
51
|
+
}
|
|
52
|
+
writeFileSync(
|
|
53
|
+
join(resourcesDir, "archive-paths.json"),
|
|
54
|
+
JSON.stringify(archivePaths, null, 2),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Set CWD to bin dir so the launcher can find DLLs and ../Resources
|
|
58
|
+
process.chdir(binDir);
|
|
59
|
+
|
|
60
|
+
// --- Admin elevation check ---
|
|
61
|
+
if (metadata.requireAdmin && process.platform !== "darwin") {
|
|
62
|
+
const isAdmin = (): boolean => {
|
|
63
|
+
if (process.platform === "win32") {
|
|
64
|
+
const shell32 = dlopen("shell32.dll", {
|
|
65
|
+
IsUserAnAdmin: { args: [], returns: "bool" },
|
|
66
|
+
});
|
|
67
|
+
const admin = shell32.symbols.IsUserAnAdmin();
|
|
68
|
+
shell32.close();
|
|
69
|
+
return admin;
|
|
70
|
+
}
|
|
71
|
+
if (process.platform === "linux") {
|
|
72
|
+
return process.getuid?.() === 0;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (!isAdmin()) {
|
|
78
|
+
if (process.platform === "win32") {
|
|
79
|
+
const shell32 = dlopen("shell32.dll", {
|
|
80
|
+
ShellExecuteW: {
|
|
81
|
+
args: ["ptr", "ptr", "ptr", "ptr", "ptr", "i32"],
|
|
82
|
+
returns: "ptr",
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const encode = (s: string) => ptr(new Uint8Array(Buffer.from(s + "\0", "utf-16le")));
|
|
86
|
+
shell32.symbols.ShellExecuteW(
|
|
87
|
+
null,
|
|
88
|
+
encode("runas"),
|
|
89
|
+
encode(process.argv[0]),
|
|
90
|
+
null,
|
|
91
|
+
null,
|
|
92
|
+
1,
|
|
93
|
+
);
|
|
94
|
+
shell32.close();
|
|
95
|
+
} else if (process.platform === "linux") {
|
|
96
|
+
Bun.spawnSync(["pkexec", process.argv[0], ...process.argv.slice(1)], {
|
|
97
|
+
stdout: "inherit",
|
|
98
|
+
stderr: "inherit",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Load SparkBunCore and run ---
|
|
107
|
+
const coreLibFileName =
|
|
108
|
+
process.platform === "win32"
|
|
109
|
+
? "SparkBunCore.dll"
|
|
110
|
+
: `libSparkBunCore.${suffix}`;
|
|
111
|
+
const coreLibPath = join(binDir, coreLibFileName);
|
|
112
|
+
|
|
113
|
+
if (!existsSync(coreLibPath)) {
|
|
114
|
+
throw new Error(`SparkBunCore not found at: ${coreLibPath}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const lib = dlopen(coreLibPath, {
|
|
118
|
+
sparkbun_core_run_main_thread: {
|
|
119
|
+
args: ["cstring", "cstring", "cstring", "i32"],
|
|
120
|
+
returns: "i32",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let channel = "";
|
|
125
|
+
let identifier = "";
|
|
126
|
+
let name = "";
|
|
127
|
+
try {
|
|
128
|
+
const versionJsonPath = join(bundleRoot, "Resources", "version.json");
|
|
129
|
+
if (existsSync(versionJsonPath)) {
|
|
130
|
+
const versionInfo = require(versionJsonPath);
|
|
131
|
+
identifier = versionInfo.identifier || "";
|
|
132
|
+
name = versionInfo.name || "";
|
|
133
|
+
channel = versionInfo.channel || "";
|
|
134
|
+
}
|
|
135
|
+
} catch {}
|
|
136
|
+
|
|
137
|
+
lib.symbols.sparkbun_core_run_main_thread(
|
|
138
|
+
Buffer.from((identifier || metadata.name) + "\0", "utf-8"),
|
|
139
|
+
Buffer.from((name || metadata.name) + "\0", "utf-8"),
|
|
140
|
+
Buffer.from(channel + "\0", "utf-8"),
|
|
141
|
+
0,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Cleanup temp on exit
|
|
145
|
+
try {
|
|
146
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
147
|
+
} catch {}
|
|
148
|
+
} catch (e: any) {
|
|
149
|
+
console.error(`Installer failed: ${e.message}`);
|
|
150
|
+
try {
|
|
151
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
152
|
+
} catch {}
|
|
153
|
+
if (process.platform === "win32") {
|
|
154
|
+
console.log("Press Enter to exit...");
|
|
155
|
+
for await (const _ of console) { break; }
|
|
156
|
+
}
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|