sparkbun 0.1.7 → 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 +1 -1
- package/src/cli/index.ts +443 -72
- package/src/installer/installer-wrapper-template.ts +69 -0
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(
|
|
@@ -2548,7 +2559,8 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
2548
2559
|
// libasar is still loaded by SparkBunCore at runtime — needed until
|
|
2549
2560
|
// native wrappers are recompiled without ASAR support
|
|
2550
2561
|
const libExt = targetOS === "win" ? ".dll" : targetOS === "macos" ? ".dylib" : ".so";
|
|
2551
|
-
const
|
|
2562
|
+
const platformDistDir = join(SPARKBUN_DEP_PATH, `dist-${targetOS}-${targetARCH}`);
|
|
2563
|
+
const asarLibSource = join(platformDistDir, "libasar" + libExt);
|
|
2552
2564
|
if (existsSync(asarLibSource)) {
|
|
2553
2565
|
cpSync(asarLibSource, join(appBundleMacOSPath, "libasar" + libExt), {
|
|
2554
2566
|
dereference: true,
|
|
@@ -2813,9 +2825,12 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
2813
2825
|
artifactsToUpload.push(debPath);
|
|
2814
2826
|
}
|
|
2815
2827
|
|
|
2816
|
-
//
|
|
2817
|
-
//
|
|
2818
|
-
|
|
2828
|
+
// For Windows/Linux, the app bundle is deleted after tarring since
|
|
2829
|
+
// createCompiledInstaller builds a standalone installer binary.
|
|
2830
|
+
// For macOS, keep the app bundle — it goes directly into the DMG.
|
|
2831
|
+
if (targetOS !== "macos") {
|
|
2832
|
+
rmSync(appBundleFolderPath, { recursive: true });
|
|
2833
|
+
}
|
|
2819
2834
|
|
|
2820
2835
|
// generate bsdiff
|
|
2821
2836
|
// https://storage.googleapis.com/eggbun-static/sparkbun-playground/canary/SparkBunPlayground-canary.app.tar.gz
|
|
@@ -2973,84 +2988,52 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
2973
2988
|
unlinkSync(tarPath);
|
|
2974
2989
|
}
|
|
2975
2990
|
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
// copy the gzip tarball to the self-extracting app bundle
|
|
2987
|
-
cpSync(compressedTarPath, compressedTarballInExtractingBundlePath, {
|
|
2988
|
-
dereference: true,
|
|
2989
|
-
});
|
|
2990
|
-
|
|
2991
|
-
// macOS uses the Zig extractor in the self-extracting .app bundle.
|
|
2992
|
-
// Windows/Linux use createCompiledInstaller instead.
|
|
2993
|
-
if (targetOS === "macos") {
|
|
2994
|
-
const selfExtractorBinSourcePath = targetPaths.EXTRACTOR;
|
|
2995
|
-
const selfExtractorBinDestinationPath = join(
|
|
2996
|
-
selfExtractingBundle.appBundleMacOSPath,
|
|
2997
|
-
config.app.name.replace(/ /g, ""),
|
|
2991
|
+
// For Windows/Linux, create a self-extracting installer bundle
|
|
2992
|
+
if (targetOS !== "macos") {
|
|
2993
|
+
const selfExtractingBundle = createAppBundle(
|
|
2994
|
+
bundleName,
|
|
2995
|
+
buildFolder,
|
|
2996
|
+
targetOS,
|
|
2997
|
+
);
|
|
2998
|
+
const compressedTarballInExtractingBundlePath = join(
|
|
2999
|
+
selfExtractingBundle.appBundleFolderResourcesPath,
|
|
3000
|
+
`${hash}.tar.gz`,
|
|
2998
3001
|
);
|
|
2999
3002
|
|
|
3000
|
-
cpSync(
|
|
3003
|
+
cpSync(compressedTarPath, compressedTarballInExtractingBundlePath, {
|
|
3001
3004
|
dereference: true,
|
|
3002
3005
|
});
|
|
3003
|
-
}
|
|
3004
|
-
|
|
3005
|
-
buildIcons(
|
|
3006
|
-
selfExtractingBundle.appBundleFolderResourcesPath,
|
|
3007
|
-
selfExtractingBundle.appBundleFolderPath,
|
|
3008
|
-
);
|
|
3009
|
-
await Bun.write(
|
|
3010
|
-
join(selfExtractingBundle.appBundleFolderContentsPath, "Info.plist"),
|
|
3011
|
-
InfoPlistContents,
|
|
3012
|
-
);
|
|
3013
3006
|
|
|
3014
|
-
|
|
3015
|
-
const extractorMetadata = {
|
|
3016
|
-
identifier: config.app.identifier,
|
|
3017
|
-
name: config.app.name,
|
|
3018
|
-
channel: buildEnvironment,
|
|
3019
|
-
hash: hash,
|
|
3020
|
-
};
|
|
3021
|
-
await Bun.write(
|
|
3022
|
-
join(
|
|
3007
|
+
buildIcons(
|
|
3023
3008
|
selfExtractingBundle.appBundleFolderResourcesPath,
|
|
3024
|
-
"metadata.json",
|
|
3025
|
-
),
|
|
3026
|
-
JSON.stringify(extractorMetadata, null, 2),
|
|
3027
|
-
);
|
|
3028
|
-
|
|
3029
|
-
// Run postWrap hook after self-extracting bundle is created, before code signing
|
|
3030
|
-
// This is where you can add files to the wrapper (e.g., for liquid glass support)
|
|
3031
|
-
runHook("postWrap", {
|
|
3032
|
-
SPARKBUN_WRAPPER_BUNDLE_PATH:
|
|
3033
3009
|
selfExtractingBundle.appBundleFolderPath,
|
|
3034
|
-
|
|
3010
|
+
);
|
|
3011
|
+
await Bun.write(
|
|
3012
|
+
join(selfExtractingBundle.appBundleFolderContentsPath, "Info.plist"),
|
|
3013
|
+
InfoPlistContents,
|
|
3014
|
+
);
|
|
3035
3015
|
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3016
|
+
const extractorMetadata = {
|
|
3017
|
+
identifier: config.app.identifier,
|
|
3018
|
+
name: config.app.name,
|
|
3019
|
+
channel: buildEnvironment,
|
|
3020
|
+
hash: hash,
|
|
3021
|
+
};
|
|
3022
|
+
await Bun.write(
|
|
3023
|
+
join(
|
|
3024
|
+
selfExtractingBundle.appBundleFolderResourcesPath,
|
|
3025
|
+
"metadata.json",
|
|
3026
|
+
),
|
|
3027
|
+
JSON.stringify(extractorMetadata, null, 2),
|
|
3041
3028
|
);
|
|
3042
|
-
} else {
|
|
3043
|
-
console.log("skipping codesign");
|
|
3044
|
-
}
|
|
3045
3029
|
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
console.log("skipping notarization");
|
|
3030
|
+
runHook("postWrap", {
|
|
3031
|
+
SPARKBUN_WRAPPER_BUNDLE_PATH:
|
|
3032
|
+
selfExtractingBundle.appBundleFolderPath,
|
|
3033
|
+
});
|
|
3051
3034
|
}
|
|
3052
3035
|
|
|
3053
|
-
// DMG creation for macOS
|
|
3036
|
+
// DMG creation for macOS — uses the real app bundle directly
|
|
3054
3037
|
if (targetOS === "macos" && config.build.mac?.createDmg !== false) {
|
|
3055
3038
|
console.log("creating dmg...");
|
|
3056
3039
|
const finalDmgPath = join(buildFolder, `${appFileName}.dmg`);
|
|
@@ -3078,10 +3061,10 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3078
3061
|
// Copy the app bundle to the staging directory
|
|
3079
3062
|
const stagedAppPath = join(
|
|
3080
3063
|
dmgStagingDir,
|
|
3081
|
-
basename(
|
|
3064
|
+
basename(appBundleFolderPath),
|
|
3082
3065
|
);
|
|
3083
3066
|
execSync(
|
|
3084
|
-
`cp -R ${escapePathForTerminal(
|
|
3067
|
+
`cp -R ${escapePathForTerminal(appBundleFolderPath)} ${escapePathForTerminal(stagedAppPath)}`,
|
|
3085
3068
|
);
|
|
3086
3069
|
|
|
3087
3070
|
// Create a symlink to /Applications for easy drag-and-drop installation
|
|
@@ -3215,6 +3198,394 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3215
3198
|
// stapler validate -v <app path>
|
|
3216
3199
|
}
|
|
3217
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
|
+
|
|
3218
3589
|
// Take over as the terminal's foreground process group (macOS/Linux).
|
|
3219
3590
|
// This prevents the parent bun script runner from receiving SIGINT
|
|
3220
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
|
+
}
|