sparkbun 0.1.9 → 0.2.1
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-arm64/libNativeWrapper.dll +0 -0
- package/dist-win-x64/libNativeWrapper.dll +0 -0
- 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 +141 -128
- package/src/installer/installer-wrapper-template.ts +128 -52
|
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
|
|
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;
|
|
@@ -2078,7 +2078,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
2078
2078
|
}
|
|
2079
2079
|
|
|
2080
2080
|
// Compile launcher from source using Bun.build() API
|
|
2081
|
-
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"));
|
|
2082
2082
|
const sparkbunRoot = join(cliDir, "..", "..");
|
|
2083
2083
|
const launcherSourcePath = join(sparkbunRoot, "src", "launcher", "main.ts");
|
|
2084
2084
|
const bunTarget = `bun-${targetOS === "macos" ? "darwin" : targetOS === "win" ? "windows" : "linux"}-${currentTarget.arch}` as const;
|
|
@@ -3198,6 +3198,23 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3198
3198
|
// stapler validate -v <app path>
|
|
3199
3199
|
}
|
|
3200
3200
|
|
|
3201
|
+
/**
|
|
3202
|
+
* Build a graphical installer as a single-file executable.
|
|
3203
|
+
*
|
|
3204
|
+
* The output is a SparkBun app compiled into one binary. It uses a two-phase
|
|
3205
|
+
* execution model: on first launch it extracts a small runtime bundle (~1MB
|
|
3206
|
+
* of DLLs, views, app code) to temp, copies itself there, and re-launches
|
|
3207
|
+
* with admin elevation. On second launch (from temp, with DLLs present) it
|
|
3208
|
+
* loads SparkBunCore and runs the webview-based installer UI.
|
|
3209
|
+
*
|
|
3210
|
+
* Payload archives are compressed with LZMA via NSIS and embedded directly
|
|
3211
|
+
* in the binary. They stay embedded until the user triggers installation,
|
|
3212
|
+
* at which point the app code writes them to disk and runs them as silent
|
|
3213
|
+
* self-extractors.
|
|
3214
|
+
*
|
|
3215
|
+
* This is a separate pipeline from `runBuild` — it does not produce update
|
|
3216
|
+
* artifacts, delta patches, DMGs, or .deb packages.
|
|
3217
|
+
*/
|
|
3201
3218
|
async function runInstallerBuild(
|
|
3202
3219
|
config: Awaited<ReturnType<typeof getConfig>>,
|
|
3203
3220
|
buildEnvironment: "dev" | "stable",
|
|
@@ -3282,76 +3299,74 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3282
3299
|
}
|
|
3283
3300
|
mkdirSync(buildFolder, { recursive: true });
|
|
3284
3301
|
|
|
3285
|
-
|
|
3302
|
+
const cliDir = dirname(decodeURIComponent(new URL(import.meta.url).pathname).replace(/^\/([A-Za-z]:)/, "$1"));
|
|
3303
|
+
const sparkbunRoot = join(cliDir, "..", "..");
|
|
3304
|
+
const bunTarget = `bun-${targetOS === "macos" ? "darwin" : targetOS === "win" ? "windows" : "linux"}-${targetARCH}` as const;
|
|
3305
|
+
const targetOSName = targetOS === "macos" ? "darwin" : targetOS === "win" ? "windows" : "linux";
|
|
3306
|
+
const isWindows = targetOS === "win";
|
|
3286
3307
|
|
|
3287
|
-
|
|
3288
|
-
const bundle = createAppBundle(appFileName, buildFolder, targetOS);
|
|
3289
|
-
const appBundleFolderPath = bundle.appBundleFolderPath;
|
|
3290
|
-
const appBundleMacOSPath = bundle.appBundleMacOSPath;
|
|
3291
|
-
const appBundleFolderResourcesPath = bundle.appBundleFolderResourcesPath;
|
|
3292
|
-
const appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
|
|
3293
|
-
mkdirSync(appBundleAppCodePath, { recursive: true });
|
|
3308
|
+
// --- Create archives first (they go into the compiled binary, not the runtime bundle) ---
|
|
3294
3309
|
|
|
3295
|
-
|
|
3310
|
+
const stagingDir = join(buildFolder, ".installer-staging");
|
|
3311
|
+
if (existsSync(stagingDir)) rmSync(stagingDir, { recursive: true, force: true });
|
|
3312
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
3296
3313
|
|
|
3297
|
-
const
|
|
3298
|
-
const launcherDestination = join(appBundleMacOSPath, launcherBinaryName) + targetBinExt;
|
|
3299
|
-
mkdirSync(dirname(launcherDestination), { recursive: true });
|
|
3314
|
+
const archiveManifest: Record<string, string> = {};
|
|
3300
3315
|
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
const launcherSourcePath = join(sparkbunRoot, "src", "launcher", "main.ts");
|
|
3304
|
-
const bunTarget = `bun-${targetOS === "macos" ? "darwin" : targetOS === "win" ? "windows" : "linux"}-${targetARCH}` as const;
|
|
3316
|
+
if (archives.length > 0) {
|
|
3317
|
+
console.log("Creating archives (LZMA solid via NSIS)...");
|
|
3305
3318
|
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3319
|
+
const nsisDir = join(SPARKBUN_DEP_PATH, `dist-${targetOS}-${targetARCH}`, "nsis");
|
|
3320
|
+
const makensisPath = join(nsisDir, "Bin", "makensis.exe");
|
|
3321
|
+
if (!existsSync(makensisPath)) {
|
|
3322
|
+
throw new Error(`makensis not found at ${makensisPath}. LZMA compression requires NSIS binaries in dist-${targetOS}-${targetARCH}/nsis/`);
|
|
3323
|
+
}
|
|
3310
3324
|
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3325
|
+
for (const { name, path: archiveSrcPath } of archives) {
|
|
3326
|
+
const archiveFileName = `archive-${name}.exe`;
|
|
3327
|
+
const archiveDestPath = join(stagingDir, archiveFileName);
|
|
3328
|
+
const nsiPath = join(stagingDir, `archive-${name}.nsi`);
|
|
3329
|
+
|
|
3330
|
+
console.log(` Archiving ${name}: ${archiveSrcPath}`);
|
|
3331
|
+
|
|
3332
|
+
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`;
|
|
3333
|
+
writeFileSync(nsiPath, nsiContent);
|
|
3334
|
+
|
|
3335
|
+
const result = Bun.spawnSync([makensisPath, nsiPath], {
|
|
3336
|
+
cwd: nsisDir,
|
|
3337
|
+
env: { ...process.env, NSISDIR: nsisDir },
|
|
3338
|
+
stdout: "pipe",
|
|
3339
|
+
stderr: "pipe",
|
|
3340
|
+
});
|
|
3341
|
+
|
|
3342
|
+
if (result.exitCode !== 0) {
|
|
3343
|
+
const stderr = result.stderr.toString();
|
|
3344
|
+
const stdout = result.stdout.toString();
|
|
3345
|
+
console.error(`makensis failed for ${name}:`, stderr || stdout);
|
|
3346
|
+
throw new Error(`Failed to compress archive: ${name}`);
|
|
3326
3347
|
}
|
|
3327
|
-
}
|
|
3328
|
-
compileOptions.windows = {
|
|
3329
|
-
hideConsole: true,
|
|
3330
|
-
...(icoPath && { icon: icoPath }),
|
|
3331
|
-
title: config.app.name,
|
|
3332
|
-
version: installerVersion,
|
|
3333
|
-
description: config.app.name,
|
|
3334
|
-
publisher: installerPublisher || " ",
|
|
3335
|
-
copyright: (config.app as any).copyright || " ",
|
|
3336
|
-
};
|
|
3337
|
-
}
|
|
3338
3348
|
|
|
3339
|
-
|
|
3340
|
-
const launcherBuild = await Bun.build({
|
|
3341
|
-
entrypoints: [launcherSourcePath],
|
|
3342
|
-
compile: compileOptions,
|
|
3343
|
-
});
|
|
3344
|
-
if (!launcherBuild.success) {
|
|
3345
|
-
console.error("Launcher compilation failed:", launcherBuild.logs);
|
|
3346
|
-
throw new Error("Launcher compilation failed");
|
|
3347
|
-
}
|
|
3349
|
+
unlinkSync(nsiPath);
|
|
3348
3350
|
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
+
const archiveSize = statSync(archiveDestPath).size;
|
|
3352
|
+
console.log(` ${name}: ${(archiveSize / 1024 / 1024).toFixed(2)} MB`);
|
|
3353
|
+
archiveManifest[name] = archiveFileName;
|
|
3354
|
+
}
|
|
3351
3355
|
}
|
|
3352
3356
|
|
|
3353
|
-
// ---
|
|
3357
|
+
// --- Build the runtime bundle (DLLs, views, app code — NO launcher binary, NO archives) ---
|
|
3354
3358
|
|
|
3359
|
+
console.log("Building runtime bundle...");
|
|
3360
|
+
const appFileName = getAppFileName(config.app.name, buildEnvironment);
|
|
3361
|
+
const bundle = createAppBundle(appFileName, buildFolder, targetOS);
|
|
3362
|
+
const appBundleFolderPath = bundle.appBundleFolderPath;
|
|
3363
|
+
const appBundleMacOSPath = bundle.appBundleMacOSPath;
|
|
3364
|
+
const appBundleFolderResourcesPath = bundle.appBundleFolderResourcesPath;
|
|
3365
|
+
const appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
|
|
3366
|
+
mkdirSync(appBundleAppCodePath, { recursive: true });
|
|
3367
|
+
mkdirSync(appBundleMacOSPath, { recursive: true });
|
|
3368
|
+
|
|
3369
|
+
// Copy native wrappers
|
|
3355
3370
|
if (targetOS === "win") {
|
|
3356
3371
|
cpSync(targetPaths.CORE_WIN, join(appBundleMacOSPath, "SparkBunCore.dll"), { dereference: true });
|
|
3357
3372
|
cpSync(targetPaths.NATIVE_WRAPPER_WIN, join(appBundleMacOSPath, "libNativeWrapper.dll"), { dereference: true });
|
|
@@ -3372,8 +3387,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3372
3387
|
cpSync(asarLibSource, join(appBundleMacOSPath, "libasar" + libExt), { dereference: true });
|
|
3373
3388
|
}
|
|
3374
3389
|
|
|
3375
|
-
//
|
|
3376
|
-
|
|
3390
|
+
// Bundle bun code
|
|
3377
3391
|
const bunConfig = config.build.bun;
|
|
3378
3392
|
const bunSource = join(projectRoot, bunConfig.entrypoint);
|
|
3379
3393
|
if (!existsSync(bunSource)) {
|
|
@@ -3394,8 +3408,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3394
3408
|
throw new Error("Build failed: bun build failed");
|
|
3395
3409
|
}
|
|
3396
3410
|
|
|
3397
|
-
//
|
|
3398
|
-
|
|
3411
|
+
// Bundle views
|
|
3399
3412
|
for (const viewName in config.build.views) {
|
|
3400
3413
|
const viewConfig = config.build.views[viewName]!;
|
|
3401
3414
|
const viewSource = join(projectRoot, viewConfig.entrypoint);
|
|
@@ -3420,8 +3433,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3420
3433
|
}
|
|
3421
3434
|
}
|
|
3422
3435
|
|
|
3423
|
-
//
|
|
3424
|
-
|
|
3436
|
+
// Copy assets
|
|
3425
3437
|
for (const relSource in config.build.copy) {
|
|
3426
3438
|
const source = join(projectRoot, relSource);
|
|
3427
3439
|
if (!existsSync(source)) {
|
|
@@ -3438,8 +3450,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3438
3450
|
cpSync(source, destination, { recursive: true, dereference: true });
|
|
3439
3451
|
}
|
|
3440
3452
|
|
|
3441
|
-
//
|
|
3442
|
-
|
|
3453
|
+
// Write build.json
|
|
3443
3454
|
const buildJson = {
|
|
3444
3455
|
name: config.app.name,
|
|
3445
3456
|
identifier: config.app.identifier,
|
|
@@ -3449,69 +3460,42 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3449
3460
|
};
|
|
3450
3461
|
await Bun.write(join(appBundleAppCodePath, "build.json"), JSON.stringify(buildJson, null, 2));
|
|
3451
3462
|
|
|
3452
|
-
//
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
for (const { name, path: archiveSrcPath } of archives) {
|
|
3459
|
-
const archiveFileName = `archive-${name}.tar.gz`;
|
|
3460
|
-
const archiveDestPath = join(appBundleFolderResourcesPath, archiveFileName);
|
|
3461
|
-
|
|
3462
|
-
console.log(` Archiving ${name}: ${archiveSrcPath}`);
|
|
3463
|
-
const tarPath = join(buildFolder, `archive-${name}.tar`);
|
|
3464
|
-
createTar(tarPath, dirname(archiveSrcPath), [basename(archiveSrcPath)]);
|
|
3465
|
-
|
|
3466
|
-
const tarBytes = readFileSync(tarPath);
|
|
3467
|
-
const gzipped = Bun.gzipSync(tarBytes);
|
|
3468
|
-
writeFileSync(archiveDestPath, gzipped);
|
|
3469
|
-
|
|
3470
|
-
// Clean up intermediate tar
|
|
3471
|
-
unlinkSync(tarPath);
|
|
3463
|
+
// Write archives.json manifest into runtime bundle so app code knows archive names
|
|
3464
|
+
await Bun.write(
|
|
3465
|
+
join(appBundleFolderResourcesPath, "archives.json"),
|
|
3466
|
+
JSON.stringify(archiveManifest, null, 2),
|
|
3467
|
+
);
|
|
3472
3468
|
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3469
|
+
// --- Create runtime bundle tar.gz (small — no launcher binary, no archives) ---
|
|
3470
|
+
|
|
3471
|
+
console.log("Packaging runtime bundle (gzip level 12)...");
|
|
3472
|
+
const runtimeFiles: Record<string, ArrayBuffer> = {};
|
|
3473
|
+
const bundleBase = basename(appBundleFolderPath);
|
|
3474
|
+
async function walkBundle(dir: string, prefix: string) {
|
|
3475
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
3476
|
+
const fullPath = join(dir, entry.name);
|
|
3477
|
+
const archivePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
3478
|
+
if (entry.isDirectory()) {
|
|
3479
|
+
await walkBundle(fullPath, archivePath);
|
|
3480
|
+
} else {
|
|
3481
|
+
runtimeFiles[archivePath] = await Bun.file(fullPath).arrayBuffer();
|
|
3482
|
+
}
|
|
3476
3483
|
}
|
|
3477
|
-
|
|
3478
|
-
// Write archives.json manifest
|
|
3479
|
-
await Bun.write(
|
|
3480
|
-
join(appBundleFolderResourcesPath, "archives.json"),
|
|
3481
|
-
JSON.stringify(archiveManifest, null, 2),
|
|
3482
|
-
);
|
|
3483
|
-
console.log(`Archives manifest written (${archives.length} archives)`);
|
|
3484
3484
|
}
|
|
3485
|
+
await walkBundle(appBundleFolderPath, bundleBase);
|
|
3485
3486
|
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
const tarPath = join(buildFolder, `${appFileName}.tar`);
|
|
3490
|
-
createTar(tarPath, buildFolder, [basename(appBundleFolderPath)]);
|
|
3491
|
-
|
|
3492
|
-
const compressedTarPath = `${tarPath}.gz`;
|
|
3493
|
-
const tarBytes = readFileSync(tarPath);
|
|
3494
|
-
const gzipped = Bun.gzipSync(tarBytes);
|
|
3495
|
-
writeFileSync(compressedTarPath, gzipped);
|
|
3496
|
-
unlinkSync(tarPath);
|
|
3497
|
-
|
|
3498
|
-
// Clean up the uncompressed app bundle
|
|
3487
|
+
const runtimeArchive = new Bun.Archive(runtimeFiles, { compress: "gzip", level: 12 });
|
|
3488
|
+
const runtimeCompressedPath = join(buildFolder, `${appFileName}-runtime.tar.gz`);
|
|
3489
|
+
await Bun.write(runtimeCompressedPath, await runtimeArchive.bytes());
|
|
3499
3490
|
rmSync(appBundleFolderPath, { recursive: true });
|
|
3500
3491
|
|
|
3501
|
-
|
|
3492
|
+
const runtimeSize = statSync(runtimeCompressedPath).size;
|
|
3493
|
+
console.log(` Runtime bundle: ${(runtimeSize / 1024 / 1024).toFixed(2)} MB`);
|
|
3502
3494
|
|
|
3503
|
-
|
|
3504
|
-
const outputPath = outPath
|
|
3505
|
-
? path.resolve(projectRoot, outPath)
|
|
3506
|
-
: join(buildFolder, defaultOutName);
|
|
3495
|
+
// --- Generate entrypoint and compile single installer exe ---
|
|
3507
3496
|
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
if (existsSync(stagingDir)) rmSync(stagingDir, { recursive: true, force: true });
|
|
3511
|
-
mkdirSync(stagingDir, { recursive: true });
|
|
3512
|
-
|
|
3513
|
-
// Copy app bundle archive
|
|
3514
|
-
copyFileSync(compressedTarPath, join(stagingDir, "app-archive.tar.gz"));
|
|
3497
|
+
// Copy runtime bundle into staging
|
|
3498
|
+
copyFileSync(runtimeCompressedPath, join(stagingDir, "runtime-bundle.tar.gz"));
|
|
3515
3499
|
|
|
3516
3500
|
// Write metadata
|
|
3517
3501
|
const metadata: Record<string, unknown> = {
|
|
@@ -3519,6 +3503,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3519
3503
|
version: installerVersion,
|
|
3520
3504
|
identifier: config.app.identifier,
|
|
3521
3505
|
channel: buildEnvironment,
|
|
3506
|
+
requireAdmin: (config.build?.win as any)?.requireAdmin || (config.build?.linux as any)?.requireAdmin || false,
|
|
3522
3507
|
};
|
|
3523
3508
|
if (metadataPath) {
|
|
3524
3509
|
const userMetadata = JSON.parse(readFileSync(path.resolve(projectRoot, metadataPath), "utf-8"));
|
|
@@ -3526,16 +3511,42 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3526
3511
|
}
|
|
3527
3512
|
writeFileSync(join(stagingDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
3528
3513
|
|
|
3529
|
-
//
|
|
3514
|
+
// Generate the entrypoint that imports the wrapper template + all archive files
|
|
3515
|
+
// The wrapper template imports runtime-bundle.tar.gz and metadata.json.
|
|
3516
|
+
// We generate an archives module that the app code can import at runtime.
|
|
3530
3517
|
const wrapperTemplatePath = join(cliDir, "..", "installer", "installer-wrapper-template.ts");
|
|
3531
3518
|
copyFileSync(wrapperTemplatePath, join(stagingDir, "installer.ts"));
|
|
3532
3519
|
|
|
3533
|
-
|
|
3534
|
-
const
|
|
3520
|
+
// Generate archives.ts that embeds each archive via static imports
|
|
3521
|
+
const archiveImports: string[] = [];
|
|
3522
|
+
const archiveExports: string[] = [];
|
|
3523
|
+
for (const { name } of archives) {
|
|
3524
|
+
const fileName = archiveManifest[name];
|
|
3525
|
+
archiveImports.push(`import archive_${name} from "./${fileName}" with { type: "file" };`);
|
|
3526
|
+
archiveExports.push(` "${name}": archive_${name},`);
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
const archivesModuleContent = `${archiveImports.join("\n")}
|
|
3530
|
+
|
|
3531
|
+
export const embeddedArchives: Record<string, string> = {
|
|
3532
|
+
${archiveExports.join("\n")}
|
|
3533
|
+
};
|
|
3534
|
+
`;
|
|
3535
|
+
writeFileSync(join(stagingDir, "embedded-archives.ts"), archivesModuleContent);
|
|
3536
|
+
|
|
3537
|
+
// --- Compile single installer exe ---
|
|
3538
|
+
|
|
3539
|
+
const defaultOutName = `${installerName.replace(/ /g, "-")}-Setup${targetBinExt}`;
|
|
3540
|
+
const outputPath = outPath
|
|
3541
|
+
? path.resolve(projectRoot, outPath)
|
|
3542
|
+
: join(buildFolder, defaultOutName);
|
|
3543
|
+
|
|
3544
|
+
console.log("Compiling installer executable...");
|
|
3535
3545
|
|
|
3536
3546
|
const installerCompileOptions: any = {
|
|
3537
3547
|
target: `bun-${targetOSName}-${targetARCH}`,
|
|
3538
3548
|
outfile: outputPath,
|
|
3549
|
+
minify: true,
|
|
3539
3550
|
};
|
|
3540
3551
|
|
|
3541
3552
|
if (isWindows && OS === "win") {
|
|
@@ -3554,7 +3565,9 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3554
3565
|
}
|
|
3555
3566
|
}
|
|
3556
3567
|
installerCompileOptions.windows = {
|
|
3557
|
-
hideConsole
|
|
3568
|
+
// Don't set hideConsole — it conflicts with the ShellExecuteW("runas")
|
|
3569
|
+
// elevation flow in the wrapper template. The PE subsystem patch
|
|
3570
|
+
// (CONSOLE -> WINDOWS) applied after compilation hides the console.
|
|
3558
3571
|
...(icoPath && { icon: icoPath }),
|
|
3559
3572
|
title: `${installerName} Setup`,
|
|
3560
3573
|
version: installerVersion,
|
|
@@ -4226,7 +4239,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
4226
4239
|
writeFileSync(join(stagingDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
4227
4240
|
|
|
4228
4241
|
// Copy installer template
|
|
4229
|
-
const cliDir = dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"));
|
|
4242
|
+
const cliDir = dirname(decodeURIComponent(new URL(import.meta.url).pathname).replace(/^\/([A-Za-z]:)/, "$1"));
|
|
4230
4243
|
const templatePath = join(cliDir, "..", "installer", "installer-template.ts");
|
|
4231
4244
|
copyFileSync(templatePath, join(stagingDir, "installer.ts"));
|
|
4232
4245
|
|
|
@@ -1,69 +1,145 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SparkBun Installer Wrapper Template
|
|
3
|
+
*
|
|
4
|
+
* This file is the entrypoint for installers built with `bun sparkbun installer`.
|
|
5
|
+
* It gets compiled into a single executable that contains:
|
|
6
|
+
* - A small runtime bundle (DLLs, views, app code — ~1MB compressed)
|
|
7
|
+
* - The installer's payload archives (LZMA compressed via NSIS)
|
|
8
|
+
*
|
|
9
|
+
* The binary uses a two-phase execution model with a single Bun runtime:
|
|
10
|
+
*
|
|
11
|
+
* Phase 1 (first launch — user double-clicks the exe):
|
|
12
|
+
* No native DLLs next to the exe, so we can't load SparkBunCore yet.
|
|
13
|
+
* Extract the runtime bundle to a temp directory, copy ourselves there
|
|
14
|
+
* (so the same binary now has DLLs next to it), and re-launch the copy
|
|
15
|
+
* with admin elevation via ShellExecuteW("runas"). The UAC prompt shows
|
|
16
|
+
* the exe's PE metadata (app name, publisher) since Windows reads it
|
|
17
|
+
* from the binary being elevated.
|
|
18
|
+
*
|
|
19
|
+
* Phase 2 (second launch — from temp, elevated):
|
|
20
|
+
* DLLs are next to us. Load SparkBunCore via FFI, start the app code
|
|
21
|
+
* as a Worker thread, and run the native event loop. The app code opens
|
|
22
|
+
* a webview window (the installer wizard UI) and can access the embedded
|
|
23
|
+
* payload archives via globalThis.__installerArchives when the user
|
|
24
|
+
* triggers installation.
|
|
25
|
+
*
|
|
26
|
+
* The payload archives stay embedded in the binary at all times — they are
|
|
27
|
+
* never extracted to temp. Only when the user clicks "Install" does the app
|
|
28
|
+
* code write them to disk and run them (NSIS silent self-extractors).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import runtimeArchive from "./runtime-bundle.tar.gz" with { type: "file" };
|
|
2
32
|
import metadataJson from "./metadata.json" with { type: "file" };
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
33
|
+
import { embeddedArchives } from "./embedded-archives";
|
|
34
|
+
import { join, dirname, basename } from "path";
|
|
35
|
+
import { existsSync, mkdirSync, readdirSync, copyFileSync } from "fs";
|
|
36
|
+
import { dlopen, suffix, ptr } from "bun:ffi";
|
|
5
37
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
38
|
+
const metadata = await Bun.file(metadataJson).json();
|
|
39
|
+
const binDir = dirname(process.execPath);
|
|
40
|
+
const coreLibName = process.platform === "win32"
|
|
41
|
+
? "SparkBunCore.dll"
|
|
42
|
+
: `libSparkBunCore.${suffix}`;
|
|
43
|
+
const hasRuntime = existsSync(join(binDir, coreLibName));
|
|
10
44
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
process.env.TEMP || process.env.TMP || process.env.LOCALAPPDATA || ".",
|
|
14
|
-
`.sparkbun-installer-${Date.now()}`,
|
|
15
|
-
);
|
|
45
|
+
if (hasRuntime) {
|
|
46
|
+
// ── Phase 2: DLLs are next to us — run the SparkBun app ──
|
|
16
47
|
|
|
17
|
-
|
|
18
|
-
|
|
48
|
+
process.chdir(binDir);
|
|
49
|
+
|
|
50
|
+
// Expose embedded archives to the app code via globalThis.
|
|
51
|
+
// The app's install logic reads these paths to extract payloads.
|
|
52
|
+
(globalThis as any).__installerArchives = embeddedArchives;
|
|
53
|
+
|
|
54
|
+
const coreLibPath = join(binDir, coreLibName);
|
|
55
|
+
const lib = dlopen(coreLibPath, {
|
|
56
|
+
sparkbun_core_run_main_thread: {
|
|
57
|
+
args: ["cstring", "cstring", "cstring", "i32"],
|
|
58
|
+
returns: "i32",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const resourcesDir = join(binDir, "..", "Resources");
|
|
63
|
+
const appEntrypointPath = join(resourcesDir, "app", "bun", "index.js");
|
|
64
|
+
|
|
65
|
+
// Read app identity from build.json for SparkBunCore initialization
|
|
66
|
+
let identifier = metadata.name || "";
|
|
67
|
+
let name = metadata.name || "";
|
|
68
|
+
let channel = "";
|
|
69
|
+
try {
|
|
70
|
+
const buildJsonPath = join(resourcesDir, "app", "build.json");
|
|
71
|
+
if (existsSync(buildJsonPath)) {
|
|
72
|
+
const buildConfig = JSON.parse(require("fs").readFileSync(buildJsonPath, "utf-8"));
|
|
73
|
+
identifier = buildConfig.identifier || identifier;
|
|
74
|
+
name = buildConfig.name || name;
|
|
75
|
+
channel = buildConfig.channel || "";
|
|
76
|
+
}
|
|
77
|
+
} catch {}
|
|
78
|
+
|
|
79
|
+
// Suppress default signal handling so the native event loop controls shutdown
|
|
80
|
+
process.on("SIGINT", () => {});
|
|
81
|
+
process.on("SIGTERM", () => {});
|
|
82
|
+
|
|
83
|
+
// Start the app code in a Worker thread (same pattern as SparkBun's launcher).
|
|
84
|
+
// The Worker runs the developer's Bun entrypoint which creates BrowserWindows,
|
|
85
|
+
// sets up RPC handlers, etc.
|
|
86
|
+
new Worker(appEntrypointPath);
|
|
87
|
+
|
|
88
|
+
// Run the native event loop on the main thread (blocks until app exits)
|
|
89
|
+
lib.symbols.sparkbun_core_run_main_thread(
|
|
90
|
+
ptr(new Uint8Array(Buffer.from(identifier + "\0", "utf8"))),
|
|
91
|
+
ptr(new Uint8Array(Buffer.from(name + "\0", "utf8"))),
|
|
92
|
+
ptr(new Uint8Array(Buffer.from(channel + "\0", "utf8"))),
|
|
93
|
+
0,
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
// ── Phase 1: No DLLs — extract runtime, copy self, launch elevated ──
|
|
97
|
+
|
|
98
|
+
const tempDir = join(
|
|
99
|
+
process.env.TEMP || process.env.LOCALAPPDATA || ".",
|
|
100
|
+
`.sparkbun-installer-${metadata.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
101
|
+
);
|
|
19
102
|
|
|
20
|
-
const archiveBytes = await Bun.file(archiveData).bytes();
|
|
21
|
-
const archive = new Bun.Archive(archiveBytes);
|
|
22
103
|
mkdirSync(tempDir, { recursive: true });
|
|
104
|
+
const archiveBytes = await Bun.file(runtimeArchive).bytes();
|
|
105
|
+
const archive = new Bun.Archive(archiveBytes);
|
|
23
106
|
await archive.extract(tempDir);
|
|
24
107
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const entries = readdirSync(tempDir);
|
|
29
|
-
for (const entry of entries) {
|
|
108
|
+
// The runtime bundle extracts as AppName/bin/ with DLLs inside
|
|
109
|
+
let extractedBinDir = tempDir;
|
|
110
|
+
for (const entry of readdirSync(tempDir)) {
|
|
30
111
|
const entryPath = join(tempDir, entry);
|
|
31
112
|
if (existsSync(join(entryPath, "bin"))) {
|
|
32
|
-
|
|
113
|
+
extractedBinDir = join(entryPath, "bin");
|
|
33
114
|
break;
|
|
34
115
|
}
|
|
35
116
|
}
|
|
36
117
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!existsSync(launcherPath)) {
|
|
42
|
-
throw new Error(`Launcher not found: ${launcherPath}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const proc = Bun.spawn([launcherPath], {
|
|
46
|
-
cwd: join(appDir, "bin"),
|
|
47
|
-
stdio: ["inherit", "inherit", "inherit"],
|
|
48
|
-
});
|
|
118
|
+
// Copy ourselves next to the DLLs so the second launch detects hasRuntime
|
|
119
|
+
const destExe = join(extractedBinDir, basename(process.execPath));
|
|
120
|
+
copyFileSync(process.execPath, destExe);
|
|
49
121
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
122
|
+
// Launch the copy with elevation using the same pattern as installer-template.ts.
|
|
123
|
+
// ShellExecuteW("runas") triggers the UAC prompt showing the exe's PE metadata.
|
|
124
|
+
if (process.platform === "win32" && metadata.requireAdmin) {
|
|
125
|
+
const shell32 = dlopen("shell32.dll", {
|
|
126
|
+
ShellExecuteW: {
|
|
127
|
+
args: ["ptr", "ptr", "ptr", "ptr", "ptr", "i32"],
|
|
128
|
+
returns: "ptr",
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const encode = (s: string) => ptr(new Uint8Array(Buffer.from(s + "\0", "utf-16le")));
|
|
132
|
+
shell32.symbols.ShellExecuteW(
|
|
133
|
+
null,
|
|
134
|
+
encode("runas"),
|
|
135
|
+
encode(destExe),
|
|
136
|
+
null,
|
|
137
|
+
encode(extractedBinDir),
|
|
138
|
+
1,
|
|
139
|
+
);
|
|
140
|
+
shell32.close();
|
|
141
|
+
} else {
|
|
142
|
+
Bun.spawn([destExe], { cwd: extractedBinDir });
|
|
67
143
|
}
|
|
68
|
-
|
|
144
|
+
await Bun.sleep(1000);
|
|
69
145
|
}
|