sparkbun 0.1.9 → 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 +120 -127
- package/src/installer/installer-wrapper-template.ts +114 -25
|
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;
|
|
@@ -3282,76 +3282,74 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3282
3282
|
}
|
|
3283
3283
|
mkdirSync(buildFolder, { recursive: true });
|
|
3284
3284
|
|
|
3285
|
-
|
|
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";
|
|
3286
3290
|
|
|
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 });
|
|
3291
|
+
// --- Create archives first (they go into the compiled binary, not the runtime bundle) ---
|
|
3294
3292
|
|
|
3295
|
-
|
|
3293
|
+
const stagingDir = join(buildFolder, ".installer-staging");
|
|
3294
|
+
if (existsSync(stagingDir)) rmSync(stagingDir, { recursive: true, force: true });
|
|
3295
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
3296
3296
|
|
|
3297
|
-
const
|
|
3298
|
-
const launcherDestination = join(appBundleMacOSPath, launcherBinaryName) + targetBinExt;
|
|
3299
|
-
mkdirSync(dirname(launcherDestination), { recursive: true });
|
|
3297
|
+
const archiveManifest: Record<string, string> = {};
|
|
3300
3298
|
|
|
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;
|
|
3299
|
+
if (archives.length > 0) {
|
|
3300
|
+
console.log("Creating archives (LZMA solid via NSIS)...");
|
|
3305
3301
|
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
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
|
+
}
|
|
3310
3307
|
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
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}`);
|
|
3326
3330
|
}
|
|
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
3331
|
|
|
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
|
-
}
|
|
3332
|
+
unlinkSync(nsiPath);
|
|
3348
3333
|
|
|
3349
|
-
|
|
3350
|
-
|
|
3334
|
+
const archiveSize = statSync(archiveDestPath).size;
|
|
3335
|
+
console.log(` ${name}: ${(archiveSize / 1024 / 1024).toFixed(2)} MB`);
|
|
3336
|
+
archiveManifest[name] = archiveFileName;
|
|
3337
|
+
}
|
|
3351
3338
|
}
|
|
3352
3339
|
|
|
3353
|
-
// ---
|
|
3340
|
+
// --- Build the runtime bundle (DLLs, views, app code — NO launcher binary, NO archives) ---
|
|
3354
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
|
|
3355
3353
|
if (targetOS === "win") {
|
|
3356
3354
|
cpSync(targetPaths.CORE_WIN, join(appBundleMacOSPath, "SparkBunCore.dll"), { dereference: true });
|
|
3357
3355
|
cpSync(targetPaths.NATIVE_WRAPPER_WIN, join(appBundleMacOSPath, "libNativeWrapper.dll"), { dereference: true });
|
|
@@ -3372,8 +3370,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3372
3370
|
cpSync(asarLibSource, join(appBundleMacOSPath, "libasar" + libExt), { dereference: true });
|
|
3373
3371
|
}
|
|
3374
3372
|
|
|
3375
|
-
//
|
|
3376
|
-
|
|
3373
|
+
// Bundle bun code
|
|
3377
3374
|
const bunConfig = config.build.bun;
|
|
3378
3375
|
const bunSource = join(projectRoot, bunConfig.entrypoint);
|
|
3379
3376
|
if (!existsSync(bunSource)) {
|
|
@@ -3394,8 +3391,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3394
3391
|
throw new Error("Build failed: bun build failed");
|
|
3395
3392
|
}
|
|
3396
3393
|
|
|
3397
|
-
//
|
|
3398
|
-
|
|
3394
|
+
// Bundle views
|
|
3399
3395
|
for (const viewName in config.build.views) {
|
|
3400
3396
|
const viewConfig = config.build.views[viewName]!;
|
|
3401
3397
|
const viewSource = join(projectRoot, viewConfig.entrypoint);
|
|
@@ -3420,8 +3416,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3420
3416
|
}
|
|
3421
3417
|
}
|
|
3422
3418
|
|
|
3423
|
-
//
|
|
3424
|
-
|
|
3419
|
+
// Copy assets
|
|
3425
3420
|
for (const relSource in config.build.copy) {
|
|
3426
3421
|
const source = join(projectRoot, relSource);
|
|
3427
3422
|
if (!existsSync(source)) {
|
|
@@ -3438,8 +3433,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3438
3433
|
cpSync(source, destination, { recursive: true, dereference: true });
|
|
3439
3434
|
}
|
|
3440
3435
|
|
|
3441
|
-
//
|
|
3442
|
-
|
|
3436
|
+
// Write build.json
|
|
3443
3437
|
const buildJson = {
|
|
3444
3438
|
name: config.app.name,
|
|
3445
3439
|
identifier: config.app.identifier,
|
|
@@ -3449,69 +3443,42 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3449
3443
|
};
|
|
3450
3444
|
await Bun.write(join(appBundleAppCodePath, "build.json"), JSON.stringify(buildJson, null, 2));
|
|
3451
3445
|
|
|
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);
|
|
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
|
+
);
|
|
3472
3451
|
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
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
|
+
}
|
|
3476
3466
|
}
|
|
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
3467
|
}
|
|
3468
|
+
await walkBundle(appBundleFolderPath, bundleBase);
|
|
3485
3469
|
|
|
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
|
|
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());
|
|
3499
3473
|
rmSync(appBundleFolderPath, { recursive: true });
|
|
3500
3474
|
|
|
3501
|
-
|
|
3475
|
+
const runtimeSize = statSync(runtimeCompressedPath).size;
|
|
3476
|
+
console.log(` Runtime bundle: ${(runtimeSize / 1024 / 1024).toFixed(2)} MB`);
|
|
3502
3477
|
|
|
3503
|
-
|
|
3504
|
-
const outputPath = outPath
|
|
3505
|
-
? path.resolve(projectRoot, outPath)
|
|
3506
|
-
: join(buildFolder, defaultOutName);
|
|
3478
|
+
// --- Generate entrypoint and compile single installer exe ---
|
|
3507
3479
|
|
|
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"));
|
|
3480
|
+
// Copy runtime bundle into staging
|
|
3481
|
+
copyFileSync(runtimeCompressedPath, join(stagingDir, "runtime-bundle.tar.gz"));
|
|
3515
3482
|
|
|
3516
3483
|
// Write metadata
|
|
3517
3484
|
const metadata: Record<string, unknown> = {
|
|
@@ -3519,6 +3486,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3519
3486
|
version: installerVersion,
|
|
3520
3487
|
identifier: config.app.identifier,
|
|
3521
3488
|
channel: buildEnvironment,
|
|
3489
|
+
requireAdmin: (config.build?.win as any)?.requireAdmin || (config.build?.linux as any)?.requireAdmin || false,
|
|
3522
3490
|
};
|
|
3523
3491
|
if (metadataPath) {
|
|
3524
3492
|
const userMetadata = JSON.parse(readFileSync(path.resolve(projectRoot, metadataPath), "utf-8"));
|
|
@@ -3526,12 +3494,37 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3526
3494
|
}
|
|
3527
3495
|
writeFileSync(join(stagingDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
3528
3496
|
|
|
3529
|
-
//
|
|
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.
|
|
3530
3500
|
const wrapperTemplatePath = join(cliDir, "..", "installer", "installer-wrapper-template.ts");
|
|
3531
3501
|
copyFileSync(wrapperTemplatePath, join(stagingDir, "installer.ts"));
|
|
3532
3502
|
|
|
3533
|
-
|
|
3534
|
-
const
|
|
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...");
|
|
3535
3528
|
|
|
3536
3529
|
const installerCompileOptions: any = {
|
|
3537
3530
|
target: `bun-${targetOSName}-${targetARCH}`,
|
|
@@ -4226,7 +4219,7 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
4226
4219
|
writeFileSync(join(stagingDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
4227
4220
|
|
|
4228
4221
|
// Copy installer template
|
|
4229
|
-
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"));
|
|
4230
4223
|
const templatePath = join(cliDir, "..", "installer", "installer-template.ts");
|
|
4231
4224
|
copyFileSync(templatePath, join(stagingDir, "installer.ts"));
|
|
4232
4225
|
|
|
@@ -1,63 +1,152 @@
|
|
|
1
|
-
import
|
|
1
|
+
import runtimeArchive from "./runtime-bundle.tar.gz" with { type: "file" };
|
|
2
2
|
import metadataJson from "./metadata.json" with { type: "file" };
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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";
|
|
5
7
|
|
|
6
8
|
interface Metadata {
|
|
7
9
|
name: string;
|
|
8
10
|
version?: string;
|
|
11
|
+
requireAdmin?: boolean;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
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.
|
|
12
18
|
const tempDir = join(
|
|
13
19
|
process.env.TEMP || process.env.TMP || process.env.LOCALAPPDATA || ".",
|
|
14
|
-
`.sparkbun-installer-${
|
|
20
|
+
`.sparkbun-installer-${metadata.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
15
21
|
);
|
|
16
22
|
|
|
17
23
|
try {
|
|
18
|
-
|
|
24
|
+
if (existsSync(tempDir)) {
|
|
25
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
19
27
|
|
|
20
|
-
const archiveBytes = await Bun.file(
|
|
28
|
+
const archiveBytes = await Bun.file(runtimeArchive).bytes();
|
|
21
29
|
const archive = new Bun.Archive(archiveBytes);
|
|
22
30
|
mkdirSync(tempDir, { recursive: true });
|
|
23
31
|
await archive.extract(tempDir);
|
|
24
32
|
|
|
25
|
-
// Find the
|
|
26
|
-
|
|
27
|
-
let
|
|
28
|
-
const
|
|
29
|
-
for (const entry of entries) {
|
|
33
|
+
// Find the bin directory inside the extracted bundle
|
|
34
|
+
let binDir = tempDir;
|
|
35
|
+
let bundleRoot = tempDir;
|
|
36
|
+
for (const entry of readdirSync(tempDir)) {
|
|
30
37
|
const entryPath = join(tempDir, entry);
|
|
31
38
|
if (existsSync(join(entryPath, "bin"))) {
|
|
32
|
-
|
|
39
|
+
binDir = join(entryPath, "bin");
|
|
40
|
+
bundleRoot = entryPath;
|
|
33
41
|
break;
|
|
34
42
|
}
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
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
|
+
};
|
|
40
76
|
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
}
|
|
43
104
|
}
|
|
44
105
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
},
|
|
48
122
|
});
|
|
49
123
|
|
|
50
|
-
|
|
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
|
+
);
|
|
51
143
|
|
|
52
|
-
//
|
|
144
|
+
// Cleanup temp on exit
|
|
53
145
|
try {
|
|
54
146
|
rmSync(tempDir, { recursive: true, force: true });
|
|
55
147
|
} catch {}
|
|
56
|
-
|
|
57
|
-
process.exit(exitCode);
|
|
58
148
|
} catch (e: any) {
|
|
59
149
|
console.error(`Installer failed: ${e.message}`);
|
|
60
|
-
// Clean up on failure
|
|
61
150
|
try {
|
|
62
151
|
rmSync(tempDir, { recursive: true, force: true });
|
|
63
152
|
} catch {}
|