sparkbun 0.1.8 → 0.1.9
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/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -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(
|
|
@@ -3187,6 +3198,394 @@ 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
|
+
// --- Create app bundle ---
|
|
3286
|
+
|
|
3287
|
+
const appFileName = getAppFileName(config.app.name, buildEnvironment);
|
|
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 });
|
|
3294
|
+
|
|
3295
|
+
// --- Compile launcher ---
|
|
3296
|
+
|
|
3297
|
+
const launcherBinaryName = config.app.name.replace(/ /g, "");
|
|
3298
|
+
const launcherDestination = join(appBundleMacOSPath, launcherBinaryName) + targetBinExt;
|
|
3299
|
+
mkdirSync(dirname(launcherDestination), { recursive: true });
|
|
3300
|
+
|
|
3301
|
+
const cliDir = dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"));
|
|
3302
|
+
const sparkbunRoot = join(cliDir, "..", "..");
|
|
3303
|
+
const launcherSourcePath = join(sparkbunRoot, "src", "launcher", "main.ts");
|
|
3304
|
+
const bunTarget = `bun-${targetOS === "macos" ? "darwin" : targetOS === "win" ? "windows" : "linux"}-${targetARCH}` as const;
|
|
3305
|
+
|
|
3306
|
+
const compileOptions: any = {
|
|
3307
|
+
target: bunTarget,
|
|
3308
|
+
outfile: launcherDestination,
|
|
3309
|
+
};
|
|
3310
|
+
|
|
3311
|
+
if (targetOS === "win" && OS === "win") {
|
|
3312
|
+
let icoPath: string | undefined;
|
|
3313
|
+
if (config.build.win?.icon) {
|
|
3314
|
+
const iconSrc = config.build.win.icon.startsWith("/") || config.build.win.icon.match(/^[a-zA-Z]:/)
|
|
3315
|
+
? config.build.win.icon
|
|
3316
|
+
: join(projectRoot, config.build.win.icon);
|
|
3317
|
+
if (existsSync(iconSrc)) {
|
|
3318
|
+
icoPath = iconSrc;
|
|
3319
|
+
if (iconSrc.toLowerCase().endsWith(".png")) {
|
|
3320
|
+
const pngToIco = (await import("png-to-ico")).default;
|
|
3321
|
+
const tempIcoPath = join(buildFolder, "temp-launcher-icon.ico");
|
|
3322
|
+
const icoBuffer = await pngToIco(iconSrc);
|
|
3323
|
+
writeFileSync(tempIcoPath, new Uint8Array(icoBuffer));
|
|
3324
|
+
icoPath = tempIcoPath;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
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
|
+
|
|
3339
|
+
console.log("Compiling installer launcher...");
|
|
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
|
+
}
|
|
3348
|
+
|
|
3349
|
+
if (targetOS === "win") {
|
|
3350
|
+
patchPeSubsystem(launcherDestination);
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
// --- Copy native wrappers ---
|
|
3354
|
+
|
|
3355
|
+
if (targetOS === "win") {
|
|
3356
|
+
cpSync(targetPaths.CORE_WIN, join(appBundleMacOSPath, "SparkBunCore.dll"), { dereference: true });
|
|
3357
|
+
cpSync(targetPaths.NATIVE_WRAPPER_WIN, join(appBundleMacOSPath, "libNativeWrapper.dll"), { dereference: true });
|
|
3358
|
+
cpSync(targetPaths.WEBVIEW2LOADER_WIN, join(appBundleMacOSPath, "WebView2Loader.dll"), { dereference: true });
|
|
3359
|
+
} else if (targetOS === "macos") {
|
|
3360
|
+
cpSync(targetPaths.CORE_MACOS, join(appBundleMacOSPath, "libSparkBunCore.dylib"), { dereference: true });
|
|
3361
|
+
cpSync(targetPaths.NATIVE_WRAPPER_MACOS, join(appBundleMacOSPath, "libNativeWrapper.dylib"), { dereference: true });
|
|
3362
|
+
} else if (targetOS === "linux") {
|
|
3363
|
+
cpSync(targetPaths.CORE_LINUX, join(appBundleMacOSPath, "libSparkBunCore.so"), { dereference: true });
|
|
3364
|
+
cpSync(targetPaths.NATIVE_WRAPPER_LINUX, join(appBundleMacOSPath, "libNativeWrapper.so"), { dereference: true });
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
// libasar stub
|
|
3368
|
+
const libExt = targetOS === "win" ? ".dll" : targetOS === "macos" ? ".dylib" : ".so";
|
|
3369
|
+
const platformDistDir = join(SPARKBUN_DEP_PATH, `dist-${targetOS}-${targetARCH}`);
|
|
3370
|
+
const asarLibSource = join(platformDistDir, "libasar" + libExt);
|
|
3371
|
+
if (existsSync(asarLibSource)) {
|
|
3372
|
+
cpSync(asarLibSource, join(appBundleMacOSPath, "libasar" + libExt), { dereference: true });
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
// --- Bundle bun code ---
|
|
3376
|
+
|
|
3377
|
+
const bunConfig = config.build.bun;
|
|
3378
|
+
const bunSource = join(projectRoot, bunConfig.entrypoint);
|
|
3379
|
+
if (!existsSync(bunSource)) {
|
|
3380
|
+
throw new Error(`Bun entrypoint not found: ${bunSource}`);
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
const bunDestFolder = join(appBundleAppCodePath, "bun");
|
|
3384
|
+
const { entrypoint: _bunEntrypoint, ...bunBuildOptions } = bunConfig;
|
|
3385
|
+
const bunBuildResult = await Bun.build({
|
|
3386
|
+
...bunBuildOptions,
|
|
3387
|
+
entrypoints: [bunSource],
|
|
3388
|
+
outdir: bunDestFolder,
|
|
3389
|
+
target: "bun",
|
|
3390
|
+
});
|
|
3391
|
+
if (!bunBuildResult.success) {
|
|
3392
|
+
console.error("Failed to build bun entrypoint:", bunSource);
|
|
3393
|
+
printBuildLogs(bunBuildResult.logs);
|
|
3394
|
+
throw new Error("Build failed: bun build failed");
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
// --- Bundle views ---
|
|
3398
|
+
|
|
3399
|
+
for (const viewName in config.build.views) {
|
|
3400
|
+
const viewConfig = config.build.views[viewName]!;
|
|
3401
|
+
const viewSource = join(projectRoot, viewConfig.entrypoint);
|
|
3402
|
+
if (!existsSync(viewSource)) {
|
|
3403
|
+
console.error(`View entrypoint not found: ${viewSource}`);
|
|
3404
|
+
continue;
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
const viewDestFolder = join(appBundleAppCodePath, "views", viewName);
|
|
3408
|
+
mkdirSync(viewDestFolder, { recursive: true });
|
|
3409
|
+
|
|
3410
|
+
const { entrypoint: _viewEntrypoint, ...viewBuildOptions } = viewConfig;
|
|
3411
|
+
const viewBuildResult = await Bun.build({
|
|
3412
|
+
...viewBuildOptions,
|
|
3413
|
+
entrypoints: [viewSource],
|
|
3414
|
+
outdir: viewDestFolder,
|
|
3415
|
+
target: "browser",
|
|
3416
|
+
});
|
|
3417
|
+
if (!viewBuildResult.success) {
|
|
3418
|
+
console.error("Failed to build view:", viewSource);
|
|
3419
|
+
printBuildLogs(viewBuildResult.logs);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
// --- Copy assets ---
|
|
3424
|
+
|
|
3425
|
+
for (const relSource in config.build.copy) {
|
|
3426
|
+
const source = join(projectRoot, relSource);
|
|
3427
|
+
if (!existsSync(source)) {
|
|
3428
|
+
console.error(`Asset not found: ${source}`);
|
|
3429
|
+
continue;
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
const destination = join(appBundleAppCodePath, config.build.copy[relSource]!);
|
|
3433
|
+
const destFolder = dirname(destination);
|
|
3434
|
+
if (!existsSync(destFolder)) {
|
|
3435
|
+
mkdirSync(destFolder, { recursive: true });
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
cpSync(source, destination, { recursive: true, dereference: true });
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
// --- Write build.json ---
|
|
3442
|
+
|
|
3443
|
+
const buildJson = {
|
|
3444
|
+
name: config.app.name,
|
|
3445
|
+
identifier: config.app.identifier,
|
|
3446
|
+
version: installerVersion,
|
|
3447
|
+
channel: buildEnvironment,
|
|
3448
|
+
...config.runtime,
|
|
3449
|
+
};
|
|
3450
|
+
await Bun.write(join(appBundleAppCodePath, "build.json"), JSON.stringify(buildJson, null, 2));
|
|
3451
|
+
|
|
3452
|
+
// --- Create and embed archives ---
|
|
3453
|
+
|
|
3454
|
+
if (archives.length > 0) {
|
|
3455
|
+
console.log("Creating archives...");
|
|
3456
|
+
const archiveManifest: Record<string, string> = {};
|
|
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);
|
|
3472
|
+
|
|
3473
|
+
const gzSize = statSync(archiveDestPath).size;
|
|
3474
|
+
console.log(` ${name}: ${(gzSize / 1024 / 1024).toFixed(2)} MB`);
|
|
3475
|
+
archiveManifest[name] = archiveFileName;
|
|
3476
|
+
}
|
|
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
|
+
}
|
|
3485
|
+
|
|
3486
|
+
// --- Tar + gzip the app bundle ---
|
|
3487
|
+
|
|
3488
|
+
console.log("Packaging app bundle...");
|
|
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
|
|
3499
|
+
rmSync(appBundleFolderPath, { recursive: true });
|
|
3500
|
+
|
|
3501
|
+
// --- Compile outer installer exe ---
|
|
3502
|
+
|
|
3503
|
+
const defaultOutName = `${installerName.replace(/ /g, "-")}-Setup${targetBinExt}`;
|
|
3504
|
+
const outputPath = outPath
|
|
3505
|
+
? path.resolve(projectRoot, outPath)
|
|
3506
|
+
: join(buildFolder, defaultOutName);
|
|
3507
|
+
|
|
3508
|
+
console.log("Compiling installer executable...");
|
|
3509
|
+
const stagingDir = join(buildFolder, ".installer-wrapper-staging");
|
|
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"));
|
|
3515
|
+
|
|
3516
|
+
// Write metadata
|
|
3517
|
+
const metadata: Record<string, unknown> = {
|
|
3518
|
+
name: installerName,
|
|
3519
|
+
version: installerVersion,
|
|
3520
|
+
identifier: config.app.identifier,
|
|
3521
|
+
channel: buildEnvironment,
|
|
3522
|
+
};
|
|
3523
|
+
if (metadataPath) {
|
|
3524
|
+
const userMetadata = JSON.parse(readFileSync(path.resolve(projectRoot, metadataPath), "utf-8"));
|
|
3525
|
+
Object.assign(metadata, userMetadata);
|
|
3526
|
+
}
|
|
3527
|
+
writeFileSync(join(stagingDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
3528
|
+
|
|
3529
|
+
// Copy wrapper template
|
|
3530
|
+
const wrapperTemplatePath = join(cliDir, "..", "installer", "installer-wrapper-template.ts");
|
|
3531
|
+
copyFileSync(wrapperTemplatePath, join(stagingDir, "installer.ts"));
|
|
3532
|
+
|
|
3533
|
+
const targetOSName = targetOS === "macos" ? "darwin" : targetOS === "win" ? "windows" : "linux";
|
|
3534
|
+
const isWindows = targetOS === "win";
|
|
3535
|
+
|
|
3536
|
+
const installerCompileOptions: any = {
|
|
3537
|
+
target: `bun-${targetOSName}-${targetARCH}`,
|
|
3538
|
+
outfile: outputPath,
|
|
3539
|
+
};
|
|
3540
|
+
|
|
3541
|
+
if (isWindows && OS === "win") {
|
|
3542
|
+
let icoPath: string | undefined;
|
|
3543
|
+
if (iconPath) {
|
|
3544
|
+
const resolvedIcon = path.resolve(projectRoot, iconPath);
|
|
3545
|
+
if (existsSync(resolvedIcon)) {
|
|
3546
|
+
icoPath = resolvedIcon;
|
|
3547
|
+
if (resolvedIcon.toLowerCase().endsWith(".png")) {
|
|
3548
|
+
const pngToIco = (await import("png-to-ico")).default;
|
|
3549
|
+
const tempIcoPath = join(buildFolder, "temp-installer-icon.ico");
|
|
3550
|
+
const icoBuffer = await pngToIco(resolvedIcon);
|
|
3551
|
+
writeFileSync(tempIcoPath, new Uint8Array(icoBuffer));
|
|
3552
|
+
icoPath = tempIcoPath;
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
installerCompileOptions.windows = {
|
|
3557
|
+
hideConsole: true,
|
|
3558
|
+
...(icoPath && { icon: icoPath }),
|
|
3559
|
+
title: `${installerName} Setup`,
|
|
3560
|
+
version: installerVersion,
|
|
3561
|
+
description: `Installs ${installerName}`,
|
|
3562
|
+
publisher: installerPublisher || " ",
|
|
3563
|
+
copyright: (config.app as any).copyright || " ",
|
|
3564
|
+
};
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
const installerBuild = await Bun.build({
|
|
3568
|
+
entrypoints: [join(stagingDir, "installer.ts")],
|
|
3569
|
+
compile: installerCompileOptions,
|
|
3570
|
+
});
|
|
3571
|
+
if (!installerBuild.success) {
|
|
3572
|
+
console.error("Installer compilation failed:", installerBuild.logs);
|
|
3573
|
+
throw new Error("Installer compilation failed");
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
if (isWindows) {
|
|
3577
|
+
patchPeSubsystem(outputPath);
|
|
3578
|
+
} else {
|
|
3579
|
+
execSync(`chmod +x ${escapePathForTerminal(outputPath)}`);
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
// Clean up staging
|
|
3583
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
3584
|
+
|
|
3585
|
+
const exeSize = statSync(outputPath).size;
|
|
3586
|
+
console.log(`\nInstaller built: ${outputPath} (${(exeSize / 1024 / 1024).toFixed(2)} MB)`);
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3190
3589
|
// Take over as the terminal's foreground process group (macOS/Linux).
|
|
3191
3590
|
// This prevents the parent bun script runner from receiving SIGINT
|
|
3192
3591
|
// when Ctrl+C is pressed, keeping the terminal busy until the app
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import archiveData from "./app-archive.tar.gz" with { type: "file" };
|
|
2
|
+
import metadataJson from "./metadata.json" with { type: "file" };
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { existsSync, mkdirSync, rmSync } from "fs";
|
|
5
|
+
|
|
6
|
+
interface Metadata {
|
|
7
|
+
name: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const metadata: Metadata = await Bun.file(metadataJson).json();
|
|
12
|
+
const tempDir = join(
|
|
13
|
+
process.env.TEMP || process.env.TMP || process.env.LOCALAPPDATA || ".",
|
|
14
|
+
`.sparkbun-installer-${Date.now()}`,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
console.log(`Starting ${metadata.name} installer...`);
|
|
19
|
+
|
|
20
|
+
const archiveBytes = await Bun.file(archiveData).bytes();
|
|
21
|
+
const archive = new Bun.Archive(archiveBytes);
|
|
22
|
+
mkdirSync(tempDir, { recursive: true });
|
|
23
|
+
await archive.extract(tempDir);
|
|
24
|
+
|
|
25
|
+
// Find the launcher exe inside the extracted bundle
|
|
26
|
+
const { readdirSync } = await import("fs");
|
|
27
|
+
let appDir = tempDir;
|
|
28
|
+
const entries = readdirSync(tempDir);
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const entryPath = join(tempDir, entry);
|
|
31
|
+
if (existsSync(join(entryPath, "bin"))) {
|
|
32
|
+
appDir = entryPath;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const exeName = metadata.name.replace(/ /g, "");
|
|
38
|
+
const binExt = process.platform === "win32" ? ".exe" : "";
|
|
39
|
+
const launcherPath = join(appDir, "bin", `${exeName}${binExt}`);
|
|
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
|
+
});
|
|
49
|
+
|
|
50
|
+
const exitCode = await proc.exited;
|
|
51
|
+
|
|
52
|
+
// Clean up temp directory
|
|
53
|
+
try {
|
|
54
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
55
|
+
} catch {}
|
|
56
|
+
|
|
57
|
+
process.exit(exitCode);
|
|
58
|
+
} catch (e: any) {
|
|
59
|
+
console.error(`Installer failed: ${e.message}`);
|
|
60
|
+
// Clean up on failure
|
|
61
|
+
try {
|
|
62
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
63
|
+
} catch {}
|
|
64
|
+
if (process.platform === "win32") {
|
|
65
|
+
console.log("Press Enter to exit...");
|
|
66
|
+
for await (const _ of console) { break; }
|
|
67
|
+
}
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|