electrobun 1.6.0-beta.0 → 1.7.0-beta.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.
@@ -3,6 +3,16 @@
3
3
  * Used in electrobun.config.ts files
4
4
  */
5
5
 
6
+ /**
7
+ * Bun.build() options that can be passed through to the bundler.
8
+ * Excludes options that are controlled by Electrobun (entrypoints, outdir, target).
9
+ * See https://bun.sh/docs/bundler for full documentation.
10
+ */
11
+ type BunBuildOptions = Omit<
12
+ Parameters<typeof Bun.build>[0],
13
+ "entrypoints" | "outdir" | "target"
14
+ >;
15
+
6
16
  export interface ElectrobunConfig {
7
17
  /**
8
18
  * Application metadata configuration
@@ -48,7 +58,9 @@ export interface ElectrobunConfig {
48
58
  */
49
59
  build?: {
50
60
  /**
51
- * Bun process build configuration
61
+ * Bun process build configuration.
62
+ * Accepts all Bun.build() options (plugins, sourcemap, minify, define, etc.)
63
+ * in addition to the entrypoint. See https://bun.sh/docs/bundler
52
64
  */
53
65
  bun?: {
54
66
  /**
@@ -56,16 +68,12 @@ export interface ElectrobunConfig {
56
68
  * @default "src/bun/index.ts"
57
69
  */
58
70
  entrypoint?: string;
59
-
60
- /**
61
- * External modules to exclude from bundling
62
- * @default []
63
- */
64
- external?: string[];
65
- };
71
+ } & BunBuildOptions;
66
72
 
67
73
  /**
68
- * Browser view build configurations
74
+ * Browser view build configurations.
75
+ * Each view accepts all Bun.build() options (plugins, sourcemap, minify, define, etc.)
76
+ * in addition to the entrypoint. See https://bun.sh/docs/bundler
69
77
  */
70
78
  views?: {
71
79
  [viewName: string]: {
@@ -73,12 +81,7 @@ export interface ElectrobunConfig {
73
81
  * Entry point for this view's TypeScript code
74
82
  */
75
83
  entrypoint: string;
76
-
77
- /**
78
- * External modules to exclude from bundling for this view
79
- */
80
- external?: string[];
81
- };
84
+ } & BunBuildOptions;
82
85
  };
83
86
 
84
87
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrobun",
3
- "version": "1.6.0-beta.0",
3
+ "version": "1.7.0-beta.1",
4
4
  "description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
5
5
  "license": "MIT",
6
6
  "author": "Blackboard Technologies Inc.",
package/src/cli/index.ts CHANGED
@@ -704,10 +704,9 @@ const defaultConfig = {
704
704
  },
705
705
  bun: {
706
706
  entrypoint: "src/bun/index.ts",
707
- external: [] as string[],
708
707
  },
709
708
  views: undefined as
710
- | Record<string, { entrypoint: string; external?: string[] }>
709
+ | Record<string, { entrypoint: string; [key: string]: unknown }>
711
710
  | undefined,
712
711
  copy: undefined as Record<string, string> | undefined,
713
712
  },
@@ -1127,9 +1126,9 @@ async function createAppImage(
1127
1126
  },
1128
1127
  );
1129
1128
 
1130
- // Create AppRun script (the entry point)
1131
- const appBundleBasename = basename(resolvedAppBundlePath);
1132
- const appRunContent = `#!/bin/bash
1129
+ // Create AppRun script (the entry point)
1130
+ const appBundleBasename = basename(resolvedAppBundlePath);
1131
+ const appRunContent = `#!/bin/bash
1133
1132
  # AppRun script for ${appFileName}
1134
1133
  HERE="$(dirname "$(readlink -f "\${0}")")"
1135
1134
  EXEC="\${HERE}/usr/bin/${appBundleBasename}/bin/launcher"
@@ -1145,9 +1144,9 @@ exec "\${EXEC}" "\$@"
1145
1144
  writeFileSync(appRunPath, appRunContent);
1146
1145
  execSync(`chmod +x ${escapePathForTerminal(appRunPath)}`);
1147
1146
 
1148
- // Create .desktop file in AppDir root
1149
- // Always include Icon field since we always create an icon (either real or placeholder)
1150
- const desktopContent = `[Desktop Entry]
1147
+ // Create .desktop file in AppDir root
1148
+ // Always include Icon field since we always create an icon (either real or placeholder)
1149
+ const desktopContent = `[Desktop Entry]
1151
1150
  Version=1.0
1152
1151
  Type=Application
1153
1152
  Name=${config.app.name}
@@ -1162,7 +1161,7 @@ Categories=Utility;
1162
1161
  const desktopPath = join(appDirPath, `${appFileName}.desktop`);
1163
1162
  writeFileSync(desktopPath, desktopContent);
1164
1163
 
1165
- // Copy icon if available, or create a minimal placeholder
1164
+ // Copy icon if available, or create a minimal placeholder
1166
1165
  if (
1167
1166
  config.build.linux?.icon &&
1168
1167
  existsSync(join(projectRoot, config.build.linux.icon))
@@ -1182,72 +1181,72 @@ Categories=Utility;
1182
1181
  // Create a minimal 1x1 transparent PNG as placeholder to satisfy appimagetool
1183
1182
  // This prevents "Icon entry not found" errors
1184
1183
  const placeholderPNG = Buffer.from([
1185
- 0x89,
1186
- 0x50,
1187
- 0x4e,
1188
- 0x47,
1189
- 0x0d,
1190
- 0x0a,
1191
- 0x1a,
1192
- 0x0a, // PNG signature
1193
- 0x00,
1194
- 0x00,
1195
- 0x00,
1196
- 0x0d,
1197
- 0x49,
1198
- 0x48,
1199
- 0x44,
1200
- 0x52, // IHDR chunk
1201
- 0x00,
1202
- 0x00,
1203
- 0x00,
1204
- 0x01,
1205
- 0x00,
1206
- 0x00,
1207
- 0x00,
1208
- 0x01, // 1x1 dimensions
1209
- 0x08,
1210
- 0x06,
1211
- 0x00,
1212
- 0x00,
1213
- 0x00,
1214
- 0x1f,
1215
- 0x15,
1216
- 0xc4, // 8-bit RGBA
1217
- 0x89,
1218
- 0x00,
1219
- 0x00,
1220
- 0x00,
1221
- 0x0b,
1222
- 0x49,
1223
- 0x44,
1224
- 0x41, // IDAT chunk
1225
- 0x54,
1226
- 0x08,
1227
- 0x99,
1228
- 0x01,
1229
- 0x00,
1230
- 0x00,
1231
- 0x05,
1232
- 0x00,
1233
- 0x01,
1234
- 0x06,
1235
- 0x7a,
1236
- 0x81,
1237
- 0x7c,
1238
- 0x00,
1239
- 0x00,
1240
- 0x00, // IEND chunk
1241
- 0x00,
1242
- 0x49,
1243
- 0x45,
1244
- 0x4e,
1245
- 0x44,
1246
- 0xae,
1247
- 0x42,
1248
- 0x60,
1249
- 0x82,
1250
- ]);
1184
+ 0x89,
1185
+ 0x50,
1186
+ 0x4e,
1187
+ 0x47,
1188
+ 0x0d,
1189
+ 0x0a,
1190
+ 0x1a,
1191
+ 0x0a, // PNG signature
1192
+ 0x00,
1193
+ 0x00,
1194
+ 0x00,
1195
+ 0x0d,
1196
+ 0x49,
1197
+ 0x48,
1198
+ 0x44,
1199
+ 0x52, // IHDR chunk
1200
+ 0x00,
1201
+ 0x00,
1202
+ 0x00,
1203
+ 0x01,
1204
+ 0x00,
1205
+ 0x00,
1206
+ 0x00,
1207
+ 0x01, // 1x1 dimensions
1208
+ 0x08,
1209
+ 0x06,
1210
+ 0x00,
1211
+ 0x00,
1212
+ 0x00,
1213
+ 0x1f,
1214
+ 0x15,
1215
+ 0xc4, // 8-bit RGBA
1216
+ 0x89,
1217
+ 0x00,
1218
+ 0x00,
1219
+ 0x00,
1220
+ 0x0b,
1221
+ 0x49,
1222
+ 0x44,
1223
+ 0x41, // IDAT chunk
1224
+ 0x54,
1225
+ 0x08,
1226
+ 0x99,
1227
+ 0x01,
1228
+ 0x00,
1229
+ 0x00,
1230
+ 0x05,
1231
+ 0x00,
1232
+ 0x01,
1233
+ 0x06,
1234
+ 0x7a,
1235
+ 0x81,
1236
+ 0x7c,
1237
+ 0x00,
1238
+ 0x00,
1239
+ 0x00, // IEND chunk
1240
+ 0x00,
1241
+ 0x49,
1242
+ 0x45,
1243
+ 0x4e,
1244
+ 0x44,
1245
+ 0xae,
1246
+ 0x42,
1247
+ 0x60,
1248
+ 0x82,
1249
+ ]);
1251
1250
 
1252
1251
  const iconDestPath = join(appDirPath, `${appFileName}.png`);
1253
1252
  const dirIconPath = join(appDirPath, ".DirIcon");
@@ -1260,18 +1259,18 @@ Categories=Utility;
1260
1259
  );
1261
1260
  }
1262
1261
 
1263
- // Generate the AppImage using appimagetool
1262
+ // Generate the AppImage using appimagetool
1264
1263
  const appImagePath = join(buildFolder, `${appFileName}.AppImage`);
1265
1264
  if (existsSync(appImagePath)) {
1266
1265
  unlinkSync(appImagePath);
1267
1266
  }
1268
1267
 
1269
- // console.log(`DEBUG: AppDir path: ${appDirPath}`);
1270
- // console.log(`DEBUG: Does AppDir exist? ${existsSync(appDirPath)}`);
1268
+ // console.log(`DEBUG: AppDir path: ${appDirPath}`);
1269
+ // console.log(`DEBUG: Does AppDir exist? ${existsSync(appDirPath)}`);
1271
1270
  console.log(`Generating AppImage: ${appImagePath}`);
1272
1271
  const appImageArch = ARCH === "arm64" ? "aarch64" : "x86_64";
1273
1272
 
1274
- // Use full path to appimagetool if not in PATH
1273
+ // Use full path to appimagetool if not in PATH
1275
1274
  let appimagetoolBase = "appimagetool";
1276
1275
  try {
1277
1276
  execSync("which appimagetool", { stdio: "ignore" });
@@ -1288,7 +1287,7 @@ Categories=Utility;
1288
1287
  }
1289
1288
  }
1290
1289
 
1291
- // Get the command with proper environment for vendored libfuse2
1290
+ // Get the command with proper environment for vendored libfuse2
1292
1291
  const appimagetoolCmd = getAppImageToolCommand().replace(
1293
1292
  "appimagetool",
1294
1293
  appimagetoolBase,
@@ -1312,14 +1311,14 @@ Categories=Utility;
1312
1311
  throw error;
1313
1312
  }
1314
1313
 
1315
- // Verify the AppImage was created
1314
+ // Verify the AppImage was created
1316
1315
  if (!existsSync(appImagePath)) {
1317
1316
  throw new Error(
1318
1317
  `AppImage was not created at expected path: ${appImagePath}`,
1319
1318
  );
1320
1319
  }
1321
1320
 
1322
- // Extract and copy icon for desktop shortcut
1321
+ // Extract and copy icon for desktop shortcut
1323
1322
  const iconExtractPath = join(buildFolder, `${appFileName}.png`);
1324
1323
  if (
1325
1324
  config.build.linux?.icon &&
@@ -1331,23 +1330,23 @@ Categories=Utility;
1331
1330
  } else {
1332
1331
  // Create placeholder icon for desktop shortcut
1333
1332
  const placeholderPNG = Buffer.from([
1334
- 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
1335
- 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
1336
- 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00,
1337
- 0x0b, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x01, 0x00, 0x00, 0x05, 0x00,
1338
- 0x01, 0x06, 0x7a, 0x81, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
1339
- 0x44, 0xae, 0x42, 0x60, 0x82,
1340
- ]);
1333
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
1334
+ 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
1335
+ 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00,
1336
+ 0x0b, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x01, 0x00, 0x00, 0x05, 0x00,
1337
+ 0x01, 0x06, 0x7a, 0x81, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
1338
+ 0x44, 0xae, 0x42, 0x60, 0x82,
1339
+ ]);
1341
1340
  writeFileSync(iconExtractPath, new Uint8Array(placeholderPNG));
1342
1341
  console.log(
1343
1342
  `✓ Created placeholder icon for desktop shortcut: ${iconExtractPath}`,
1344
1343
  );
1345
1344
  }
1346
1345
 
1347
- // Create desktop shortcut alongside the AppImage
1346
+ // Create desktop shortcut alongside the AppImage
1348
1347
  const desktopShortcutPath = join(buildFolder, `${appFileName}.desktop`);
1349
1348
 
1350
- const desktopShortcutContent = `[Desktop Entry]
1349
+ const desktopShortcutContent = `[Desktop Entry]
1351
1350
  Version=1.0
1352
1351
  Type=Application
1353
1352
  Name=${config.app.name}
@@ -2420,10 +2419,11 @@ ${schemesXml}
2420
2419
  // transpile developer's bun code
2421
2420
  const bunDestFolder = join(appBundleAppCodePath, "bun");
2422
2421
  // Build bun-javascript ts files
2422
+ const { entrypoint: _bunEntrypoint, ...bunBuildOptions } = bunConfig;
2423
2423
  const buildResult = await Bun.build({
2424
+ ...bunBuildOptions,
2424
2425
  entrypoints: [bunSource],
2425
2426
  outdir: bunDestFolder,
2426
- external: bunConfig.external || [],
2427
2427
  // minify: true, // todo (yoav): add minify in canary and prod builds
2428
2428
  target: "bun",
2429
2429
  });
@@ -2462,10 +2462,11 @@ ${schemesXml}
2462
2462
 
2463
2463
  // console.info(`bundling ${viewSource} to ${viewDestFolder} with config: `, viewConfig);
2464
2464
 
2465
+ const { entrypoint: _viewEntrypoint, ...viewBuildOptions } = viewConfig;
2465
2466
  const buildResult = await Bun.build({
2467
+ ...viewBuildOptions,
2466
2468
  entrypoints: [viewSource],
2467
2469
  outdir: viewDestFolder,
2468
- external: viewConfig.external || [],
2469
2470
  target: "browser",
2470
2471
  });
2471
2472
 
@@ -2660,20 +2661,36 @@ ${schemesXml}
2660
2661
  }
2661
2662
  }
2662
2663
 
2663
- // All the unique files are in the bundle now. Create an initial temporary tar file
2664
- // for hashing the contents
2665
- // tar the signed and notarized app bundle
2666
- // Use sanitized appFileName for tarball paths (URL-safe), but tar content uses actual bundle folder
2667
- const tmpTarPath = join(
2668
- buildFolder,
2669
- `${appFileName}${targetOS === "macos" ? ".app" : ""}-temp.tar`,
2670
- );
2671
- createTar(tmpTarPath, buildFolder, [basename(appBundleFolderPath)]);
2672
- const tmpTarBuffer = await Bun.file(tmpTarPath).arrayBuffer();
2673
- // Note: wyhash is the default in Bun.hash but that may change in the future
2674
- // so we're being explicit here.
2675
- const hash = Bun.hash.wyhash(tmpTarBuffer, 43770n).toString(36);
2676
- unlinkSync(tmpTarPath);
2664
+ // Create a content hash for version.json. In non-dev builds this is used
2665
+ // by the updater to detect changes. For dev builds we skip it since
2666
+ // the updater isn't relevant.
2667
+ let hash: string;
2668
+ if (buildEnvironment === "dev") {
2669
+ hash = "dev";
2670
+ } else {
2671
+ // Walk the app bundle and create an in-memory tar for hashing
2672
+ // (no temp file on disk). This runs after ASAR packing so the
2673
+ // hash reflects the final shipped bundle contents.
2674
+ console.time("Generate Bundle hash");
2675
+ const bundleFiles: Record<string, Blob> = {};
2676
+ const bundleBase = basename(appBundleFolderPath);
2677
+ const entries = readdirSync(appBundleFolderPath, {
2678
+ recursive: true,
2679
+ });
2680
+ for (const entry of entries) {
2681
+ const entryPath = entry.toString();
2682
+ const fullPath = join(appBundleFolderPath, entryPath);
2683
+ if (statSync(fullPath).isFile()) {
2684
+ bundleFiles[join(bundleBase, entryPath)] = Bun.file(fullPath);
2685
+ }
2686
+ }
2687
+ const archiveBytes = await new Bun.Archive(bundleFiles).bytes();
2688
+ // Note: wyhash is the default in Bun.hash but that may change in the future
2689
+ // so we're being explicit here.
2690
+ hash = Bun.hash.wyhash(archiveBytes, 43770n).toString(36);
2691
+ console.timeEnd("Generate Bundle hash");
2692
+ }
2693
+
2677
2694
  // const bunVersion = execSync(`${bunBinarySourcePath} --version`).toString().trim();
2678
2695
 
2679
2696
  // version.json inside the app bundle
@@ -2819,9 +2836,7 @@ ${schemesXml}
2819
2836
  );
2820
2837
 
2821
2838
  const appImageTarPath = join(buildFolder, `${appFileName}.tar`);
2822
- console.log(
2823
- `Creating tar of installer contents: ${appImageTarPath}`,
2824
- );
2839
+ console.log(`Creating tar of installer contents: ${appImageTarPath}`);
2825
2840
 
2826
2841
  // Tar the inner directory
2827
2842
  createTar(appImageTarPath, tempDirPath, [appFileName]);
@@ -2964,15 +2979,15 @@ ${schemesXml}
2964
2979
  const decompressResult = Bun.spawnSync(
2965
2980
  [
2966
2981
  zstdPath,
2967
- "decompress",
2968
- "-i",
2969
- prevVersionCompressedTarballPath,
2970
- "-o",
2971
- prevTarballPath,
2972
- ],
2973
- {
2974
- cwd: buildFolder,
2975
- stdout: "inherit",
2982
+ "decompress",
2983
+ "-i",
2984
+ prevVersionCompressedTarballPath,
2985
+ "-o",
2986
+ prevTarballPath,
2987
+ ],
2988
+ {
2989
+ cwd: buildFolder,
2990
+ stdout: "inherit",
2976
2991
  stderr: "inherit",
2977
2992
  },
2978
2993
  );
@@ -3208,7 +3223,10 @@ ${schemesXml}
3208
3223
  )} -ov -format ULFO ${escapePathForTerminal(dmgCreationPath)}`,
3209
3224
  );
3210
3225
 
3211
- if (buildEnvironment === "stable" && dmgCreationPath !== finalDmgPath) {
3226
+ if (
3227
+ buildEnvironment === "stable" &&
3228
+ dmgCreationPath !== finalDmgPath
3229
+ ) {
3212
3230
  renameSync(dmgCreationPath, finalDmgPath);
3213
3231
  }
3214
3232
  artifactsToUpload.push(finalDmgPath);
@@ -3319,7 +3337,10 @@ ${schemesXml}
3319
3337
 
3320
3338
  artifactsToUpload.forEach((filePath) => {
3321
3339
  const filename = basename(filePath);
3322
- const destination = join(artifactFolder, `${platformPrefix}-${filename}`);
3340
+ const destination = join(
3341
+ artifactFolder,
3342
+ `${platformPrefix}-${filename}`,
3343
+ );
3323
3344
  try {
3324
3345
  renameSync(filePath, destination);
3325
3346
  } catch {
@@ -3861,57 +3882,57 @@ ${schemesXml}
3861
3882
  const wrapperAppDirPath = join(buildFolder, `${wrapperName}.AppDir`);
3862
3883
 
3863
3884
  // Clean up any existing AppDir
3864
- if (existsSync(wrapperAppDirPath)) {
3865
- rmSync(wrapperAppDirPath, { recursive: true, force: true });
3866
- }
3867
- mkdirSync(wrapperAppDirPath, { recursive: true });
3885
+ if (existsSync(wrapperAppDirPath)) {
3886
+ rmSync(wrapperAppDirPath, { recursive: true, force: true });
3887
+ }
3888
+ mkdirSync(wrapperAppDirPath, { recursive: true });
3868
3889
 
3869
- try {
3870
- // Create usr/bin directory structure
3871
- const usrBinPath = join(wrapperAppDirPath, "usr", "bin");
3872
- mkdirSync(usrBinPath, { recursive: true });
3890
+ try {
3891
+ // Create usr/bin directory structure
3892
+ const usrBinPath = join(wrapperAppDirPath, "usr", "bin");
3893
+ mkdirSync(usrBinPath, { recursive: true });
3873
3894
 
3874
- // Create self-extracting binary with embedded archive (following magic markers pattern)
3875
- const targetPaths = getPlatformPaths("linux", ARCH);
3895
+ // Create self-extracting binary with embedded archive (following magic markers pattern)
3896
+ const targetPaths = getPlatformPaths("linux", ARCH);
3876
3897
 
3877
- // Read the extractor binary
3878
- const extractorBinary = readFileSync(targetPaths.EXTRACTOR);
3898
+ // Read the extractor binary
3899
+ const extractorBinary = readFileSync(targetPaths.EXTRACTOR);
3879
3900
 
3880
- // Read the compressed archive
3881
- const compressedArchive = readFileSync(compressedTarPath);
3901
+ // Read the compressed archive
3902
+ const compressedArchive = readFileSync(compressedTarPath);
3882
3903
 
3883
- // Create metadata JSON
3884
- const metadata = {
3885
- identifier: config.app.identifier,
3886
- name: config.app.name,
3887
- channel: buildEnvironment,
3888
- hash: hash,
3889
- };
3890
- const metadataJson = JSON.stringify(metadata);
3891
- const metadataBuffer = Buffer.from(metadataJson, "utf8");
3892
-
3893
- // Create marker buffers
3894
- const metadataMarker = Buffer.from("ELECTROBUN_METADATA_V1", "utf8");
3895
- const archiveMarker = Buffer.from("ELECTROBUN_ARCHIVE_V1", "utf8");
3896
-
3897
- // Combine extractor + metadata marker + metadata + archive marker + archive
3898
- const combinedBuffer = Buffer.concat([
3899
- new Uint8Array(extractorBinary),
3900
- new Uint8Array(metadataMarker),
3901
- new Uint8Array(metadataBuffer),
3902
- new Uint8Array(archiveMarker),
3903
- new Uint8Array(compressedArchive),
3904
- ]);
3905
-
3906
- // Write the self-extracting binary to AppImage/usr/bin/
3907
- const wrapperExtractorPath = join(usrBinPath, wrapperName);
3908
- writeFileSync(wrapperExtractorPath, new Uint8Array(combinedBuffer), {
3909
- mode: 0o755,
3910
- });
3911
- execSync(`chmod +x ${escapePathForTerminal(wrapperExtractorPath)}`);
3904
+ // Create metadata JSON
3905
+ const metadata = {
3906
+ identifier: config.app.identifier,
3907
+ name: config.app.name,
3908
+ channel: buildEnvironment,
3909
+ hash: hash,
3910
+ };
3911
+ const metadataJson = JSON.stringify(metadata);
3912
+ const metadataBuffer = Buffer.from(metadataJson, "utf8");
3913
+
3914
+ // Create marker buffers
3915
+ const metadataMarker = Buffer.from("ELECTROBUN_METADATA_V1", "utf8");
3916
+ const archiveMarker = Buffer.from("ELECTROBUN_ARCHIVE_V1", "utf8");
3917
+
3918
+ // Combine extractor + metadata marker + metadata + archive marker + archive
3919
+ const combinedBuffer = Buffer.concat([
3920
+ new Uint8Array(extractorBinary),
3921
+ new Uint8Array(metadataMarker),
3922
+ new Uint8Array(metadataBuffer),
3923
+ new Uint8Array(archiveMarker),
3924
+ new Uint8Array(compressedArchive),
3925
+ ]);
3926
+
3927
+ // Write the self-extracting binary to AppImage/usr/bin/
3928
+ const wrapperExtractorPath = join(usrBinPath, wrapperName);
3929
+ writeFileSync(wrapperExtractorPath, new Uint8Array(combinedBuffer), {
3930
+ mode: 0o755,
3931
+ });
3932
+ execSync(`chmod +x ${escapePathForTerminal(wrapperExtractorPath)}`);
3912
3933
 
3913
- // Create AppRun script
3914
- const appRunContent = `#!/bin/bash
3934
+ // Create AppRun script
3935
+ const appRunContent = `#!/bin/bash
3915
3936
  # AppRun script for ${wrapperName}
3916
3937
  HERE="$(dirname "$(readlink -f "\${0}")")"
3917
3938
  EXEC="\${HERE}/usr/bin/${wrapperName}"
@@ -3920,17 +3941,17 @@ EXEC="\${HERE}/usr/bin/${wrapperName}"
3920
3941
  exec "\${EXEC}" "\$@"
3921
3942
  `;
3922
3943
 
3923
- const appRunPath = join(wrapperAppDirPath, "AppRun");
3924
- writeFileSync(appRunPath, appRunContent);
3925
- execSync(`chmod +x ${escapePathForTerminal(appRunPath)}`);
3944
+ const appRunPath = join(wrapperAppDirPath, "AppRun");
3945
+ writeFileSync(appRunPath, appRunContent);
3946
+ execSync(`chmod +x ${escapePathForTerminal(appRunPath)}`);
3926
3947
 
3927
- // Check if icon will be available
3928
- const hasWrapperIcon =
3929
- config.build.linux?.icon &&
3930
- existsSync(join(projectRoot, config.build.linux.icon));
3948
+ // Check if icon will be available
3949
+ const hasWrapperIcon =
3950
+ config.build.linux?.icon &&
3951
+ existsSync(join(projectRoot, config.build.linux.icon));
3931
3952
 
3932
- // Create desktop file
3933
- const desktopContent = `[Desktop Entry]
3953
+ // Create desktop file
3954
+ const desktopContent = `[Desktop Entry]
3934
3955
  Version=1.0
3935
3956
  Type=Application
3936
3957
  Name=${config.app.name} Installer
@@ -3940,88 +3961,88 @@ Terminal=false
3940
3961
  Categories=Utility;
3941
3962
  `;
3942
3963
 
3943
- const desktopPath = join(wrapperAppDirPath, `${wrapperName}.desktop`);
3944
- writeFileSync(desktopPath, desktopContent);
3964
+ const desktopPath = join(wrapperAppDirPath, `${wrapperName}.desktop`);
3965
+ writeFileSync(desktopPath, desktopContent);
3945
3966
 
3946
- // Copy icon if available
3947
- if (hasWrapperIcon) {
3948
- const iconSourcePath = join(projectRoot, config.build.linux.icon);
3949
- const iconDestPath = join(wrapperAppDirPath, `${wrapperName}.png`);
3950
- const dirIconPath = join(wrapperAppDirPath, ".DirIcon");
3967
+ // Copy icon if available
3968
+ if (hasWrapperIcon) {
3969
+ const iconSourcePath = join(projectRoot, config.build.linux.icon);
3970
+ const iconDestPath = join(wrapperAppDirPath, `${wrapperName}.png`);
3971
+ const dirIconPath = join(wrapperAppDirPath, ".DirIcon");
3951
3972
 
3952
- cpSync(iconSourcePath, iconDestPath, { dereference: true });
3953
- cpSync(iconSourcePath, dirIconPath, { dereference: true });
3973
+ cpSync(iconSourcePath, iconDestPath, { dereference: true });
3974
+ cpSync(iconSourcePath, dirIconPath, { dereference: true });
3954
3975
 
3955
- console.log(
3956
- `Copied icon for wrapper AppImage: ${iconSourcePath} -> ${iconDestPath}`,
3957
- );
3958
- }
3976
+ console.log(
3977
+ `Copied icon for wrapper AppImage: ${iconSourcePath} -> ${iconDestPath}`,
3978
+ );
3979
+ }
3959
3980
 
3960
- // Ensure appimagetool is available
3961
- await ensureAppImageTooling();
3981
+ // Ensure appimagetool is available
3982
+ await ensureAppImageTooling();
3962
3983
 
3963
- // Generate the wrapper AppImage
3964
- if (existsSync(wrapperAppImagePath)) {
3965
- unlinkSync(wrapperAppImagePath);
3966
- }
3984
+ // Generate the wrapper AppImage
3985
+ if (existsSync(wrapperAppImagePath)) {
3986
+ unlinkSync(wrapperAppImagePath);
3987
+ }
3967
3988
 
3968
- console.log(`Creating wrapper AppImage: ${wrapperAppImagePath}`);
3969
- const appImageArch = ARCH === "arm64" ? "aarch64" : "x86_64";
3989
+ console.log(`Creating wrapper AppImage: ${wrapperAppImagePath}`);
3990
+ const appImageArch = ARCH === "arm64" ? "aarch64" : "x86_64";
3970
3991
 
3971
- // Use appimagetool to create the wrapper AppImage
3972
- let appimagetoolBase = "appimagetool";
3973
- try {
3974
- execSync("which appimagetool", { stdio: "ignore" });
3975
- } catch {
3976
- const localBinPath = join(
3977
- process.env["HOME"] || "",
3978
- ".local",
3979
- "bin",
3992
+ // Use appimagetool to create the wrapper AppImage
3993
+ let appimagetoolBase = "appimagetool";
3994
+ try {
3995
+ execSync("which appimagetool", { stdio: "ignore" });
3996
+ } catch {
3997
+ const localBinPath = join(
3998
+ process.env["HOME"] || "",
3999
+ ".local",
4000
+ "bin",
4001
+ "appimagetool",
4002
+ );
4003
+ if (existsSync(localBinPath)) {
4004
+ appimagetoolBase = localBinPath;
4005
+ }
4006
+ }
4007
+
4008
+ // Get the command with proper environment for vendored libfuse2
4009
+ const appimagetoolCmd = getAppImageToolCommand().replace(
3980
4010
  "appimagetool",
4011
+ appimagetoolBase,
3981
4012
  );
3982
- if (existsSync(localBinPath)) {
3983
- appimagetoolBase = localBinPath;
3984
- }
3985
- }
3986
4013
 
3987
- // Get the command with proper environment for vendored libfuse2
3988
- const appimagetoolCmd = getAppImageToolCommand().replace(
3989
- "appimagetool",
3990
- appimagetoolBase,
3991
- );
4014
+ try {
4015
+ execSync(
4016
+ `ARCH=${appImageArch} ${appimagetoolCmd} --no-appstream ${escapePathForTerminal(wrapperAppDirPath)} ${escapePathForTerminal(wrapperAppImagePath)}`,
4017
+ {
4018
+ stdio: "inherit",
4019
+ env: { ...process.env, ARCH: appImageArch },
4020
+ },
4021
+ );
4022
+ } catch (error) {
4023
+ console.error("Failed to create wrapper AppImage:", error);
4024
+ throw error;
4025
+ }
3992
4026
 
3993
- try {
3994
- execSync(
3995
- `ARCH=${appImageArch} ${appimagetoolCmd} --no-appstream ${escapePathForTerminal(wrapperAppDirPath)} ${escapePathForTerminal(wrapperAppImagePath)}`,
3996
- {
3997
- stdio: "inherit",
3998
- env: { ...process.env, ARCH: appImageArch },
3999
- },
4000
- );
4001
- } catch (error) {
4002
- console.error("Failed to create wrapper AppImage:", error);
4003
- throw error;
4004
- }
4027
+ // Verify the wrapper AppImage was created
4028
+ if (!existsSync(wrapperAppImagePath)) {
4029
+ throw new Error(
4030
+ `Wrapper AppImage was not created at expected path: ${wrapperAppImagePath}`,
4031
+ );
4032
+ }
4005
4033
 
4006
- // Verify the wrapper AppImage was created
4007
- if (!existsSync(wrapperAppImagePath)) {
4008
- throw new Error(
4009
- `Wrapper AppImage was not created at expected path: ${wrapperAppImagePath}`,
4034
+ const stats = statSync(wrapperAppImagePath);
4035
+ console.log(
4036
+ `✓ Linux wrapper AppImage created: ${wrapperAppImagePath} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`,
4010
4037
  );
4011
- }
4012
-
4013
- const stats = statSync(wrapperAppImagePath);
4014
- console.log(
4015
- `✓ Linux wrapper AppImage created: ${wrapperAppImagePath} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`,
4016
- );
4017
4038
 
4018
- return wrapperAppImagePath;
4019
- } finally {
4020
- if (existsSync(wrapperAppDirPath)) {
4021
- rmSync(wrapperAppDirPath, { recursive: true, force: true });
4039
+ return wrapperAppImagePath;
4040
+ } finally {
4041
+ if (existsSync(wrapperAppDirPath)) {
4042
+ rmSync(wrapperAppDirPath, { recursive: true, force: true });
4043
+ }
4022
4044
  }
4023
4045
  }
4024
- }
4025
4046
 
4026
4047
  function codesignAppBundle(
4027
4048
  appBundleOrDmgPath: string,