electrobun 1.11.4-beta.0 → 1.11.5-beta.2

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/src/cli/index.ts CHANGED
@@ -28,7 +28,6 @@ import {
28
28
  getPlatformPrefix,
29
29
  getTarballFileName,
30
30
  getWindowsSetupFileName,
31
- getLinuxAppImageBaseName,
32
31
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
33
32
  sanitizeVolumeNameForHdiutil as _sanitizeVolumeNameForHdiutil,
34
33
  getDmgVolumeName,
@@ -58,15 +57,6 @@ function createTar(tarPath: string, cwd: string, entries: string[]) {
58
57
  }
59
58
 
60
59
  // Create a tar.gz file using system tar command
61
- function createTarGz(tarGzPath: string, cwd: string, entries: string[]) {
62
- execSync(
63
- `tar -czf "${tarGzPath}" ${entries.map((e) => `"${e}"`).join(" ")}`,
64
- {
65
- cwd,
66
- stdio: "pipe",
67
- },
68
- );
69
- }
70
60
 
71
61
  // this when run as an npm script this will be where the folder where package.json is.
72
62
  const projectRoot = process.cwd();
@@ -1195,608 +1185,129 @@ function escapePathForTerminal(path: string): string {
1195
1185
  return `"${path.replace(/"/g, '\\"')}"`;
1196
1186
  }
1197
1187
 
1198
- // Download libfuse2 for cross-distribution compatibility
1199
- async function downloadLibfuse2(vendorDir: string): Promise<void> {
1200
- const arch = ARCH === "arm64" ? "arm64" : "amd64";
1201
-
1202
- const tempDir = join(vendorDir, "temp");
1203
- mkdirSync(tempDir, { recursive: true });
1204
-
1205
- try {
1206
- // Strategy 1: Try to extract from the system if libfuse2 is installed but not in standard paths
1207
- try {
1208
- console.log("Checking for system libfuse2 in non-standard locations...");
1209
- const findResult = execSync(
1210
- `find /usr -name "libfuse.so.2*" -type f 2>/dev/null | head -1`,
1211
- { encoding: "utf8" },
1212
- ).trim();
1213
-
1214
- if (findResult) {
1215
- console.log(`Found system libfuse2 at: ${findResult}`);
1216
- execSync(`cp "${findResult}" "${join(vendorDir, "libfuse.so.2")}"`, {
1217
- stdio: "inherit",
1218
- });
1219
- console.log("✓ Copied system libfuse2 to vendor directory");
1220
- return;
1221
- }
1222
- } catch (e) {
1223
- // System search failed, continue with download
1224
- }
1225
-
1226
- // Strategy 2: Download pre-compiled binary from a reliable source
1227
- console.log(`Downloading pre-compiled libfuse2 for ${arch}...`);
1228
-
1229
- // We'll download from Ubuntu as it's a reliable source with good compatibility
1230
- const packageUrls: Record<string, string> = {
1231
- amd64:
1232
- "http://archive.ubuntu.com/ubuntu/pool/universe/f/fuse/libfuse2_2.9.9-3_amd64.deb",
1233
- arm64:
1234
- "http://ports.ubuntu.com/ubuntu-ports/pool/universe/f/fuse/libfuse2_2.9.9-3_arm64.deb",
1235
- };
1236
-
1237
- const packageUrl = packageUrls[arch];
1238
- if (!packageUrl) {
1239
- throw new Error(
1240
- `Unsupported architecture for libfuse2 download: ${arch}`,
1241
- );
1242
- }
1243
-
1244
- const packageFile = join(tempDir, "libfuse2.deb");
1245
-
1246
- // Download the package
1247
- execSync(`wget -q "${packageUrl}" -O "${packageFile}"`, {
1248
- stdio: "inherit",
1249
- });
1250
-
1251
- // Extract based on available tools
1252
- console.log("Extracting libfuse2...");
1253
-
1254
- // Check if we have 'ar' command (Debian-based)
1255
- let extractionMethod = "ar";
1256
- try {
1257
- execSync("which ar", { stdio: "ignore" });
1258
- } catch {
1259
- // Check if we have 'bsdtar' (available on many distros)
1260
- try {
1261
- execSync("which bsdtar", { stdio: "ignore" });
1262
- extractionMethod = "bsdtar";
1263
- } catch {
1264
- // Check if we have '7z' (available on many distros)
1265
- try {
1266
- execSync("which 7z", { stdio: "ignore" });
1267
- extractionMethod = "7z";
1268
- } catch {
1269
- // Fallback: try to install ar via busybox if available
1270
- try {
1271
- execSync("which busybox", { stdio: "ignore" });
1272
- extractionMethod = "busybox";
1273
- } catch {
1274
- throw new Error(
1275
- "No suitable extraction tool found (ar, bsdtar, 7z, or busybox)",
1276
- );
1277
- }
1278
- }
1279
- }
1280
- }
1281
-
1282
- // Extract the .deb package
1283
- switch (extractionMethod) {
1284
- case "ar":
1285
- execSync(`cd "${tempDir}" && ar x libfuse2.deb`, { stdio: "inherit" });
1286
- break;
1287
- case "bsdtar":
1288
- execSync(`cd "${tempDir}" && bsdtar -xf libfuse2.deb`, {
1289
- stdio: "inherit",
1290
- });
1291
- break;
1292
- case "7z":
1293
- execSync(`cd "${tempDir}" && 7z x libfuse2.deb`, { stdio: "inherit" });
1294
- break;
1295
- case "busybox":
1296
- execSync(`cd "${tempDir}" && busybox ar x libfuse2.deb`, {
1297
- stdio: "inherit",
1298
- });
1299
- break;
1300
- }
1301
-
1302
- // Extract the data archive (could be .tar.xz, .tar.gz, or .tar.zst)
1303
- const dataFiles = execSync(
1304
- `cd "${tempDir}" && ls data.tar.* 2>/dev/null || true`,
1305
- {
1306
- encoding: "utf8",
1307
- },
1308
- ).trim();
1309
-
1310
- if (dataFiles) {
1311
- execSync(`cd "${tempDir}" && tar -xf ${dataFiles}`, { stdio: "inherit" });
1312
- } else {
1313
- throw new Error("Could not find data archive in .deb package");
1314
- }
1315
-
1316
- // Find and copy the library
1317
- const libFiles = execSync(
1318
- `cd "${tempDir}" && find . -name "libfuse.so.2*" -type f | head -1`,
1319
- { encoding: "utf8" },
1320
- ).trim();
1321
-
1322
- if (!libFiles) {
1323
- throw new Error("Could not find libfuse.so.2 in extracted package");
1324
- }
1325
-
1326
- const sourcePath = join(tempDir, libFiles);
1327
- const destPath = join(vendorDir, "libfuse.so.2");
1328
-
1329
- execSync(`cp "${sourcePath}" "${destPath}"`, { stdio: "inherit" });
1330
- console.log("✓ libfuse2 vendored successfully");
1331
-
1332
- // Also copy libfuse.so.2.9.9 as some systems might need the versioned file
1333
- const versionedDestPath = join(vendorDir, "libfuse.so.2.9.9");
1334
- execSync(`cp "${sourcePath}" "${versionedDestPath}"`, { stdio: "inherit" });
1335
- } catch (error) {
1336
- // If download fails, provide helpful instructions for manual installation
1337
- console.error("Failed to download libfuse2 automatically");
1338
- console.log("\nManual installation instructions for common distributions:");
1339
- console.log(" Ubuntu/Debian: sudo apt install libfuse2");
1340
- console.log(" Fedora/RHEL: sudo dnf install fuse-libs");
1341
- console.log(" openSUSE: sudo zypper install libfuse2");
1342
- console.log(" Arch Linux: sudo pacman -S fuse2");
1343
- console.log(" Alpine: sudo apk add fuse");
1344
- throw error;
1345
- } finally {
1346
- // Clean up temp directory
1347
- rmSync(tempDir, { recursive: true, force: true });
1348
- }
1349
- }
1350
-
1351
- // Get the vendor directory path
1352
- function getVendorDir(): string {
1353
- return join(ELECTROBUN_DEP_PATH, "vendor");
1354
- }
1355
-
1356
- // Get appimagetool command with proper environment if using vendored libfuse2
1357
- function getAppImageToolCommand(): string {
1358
- const vendorDir = getVendorDir();
1359
- const vendoredLibfusePath = join(vendorDir, "libfuse.so.2");
1360
-
1361
- if (existsSync(vendoredLibfusePath)) {
1362
- // Use vendored libfuse2 by setting LD_LIBRARY_PATH
1363
- // Also set LD_PRELOAD to ensure our libfuse2 is used over system libs
1364
- return `LD_LIBRARY_PATH="${vendorDir}:$LD_LIBRARY_PATH" LD_PRELOAD="${vendoredLibfusePath}" appimagetool`;
1365
- }
1366
-
1367
- // Use system appimagetool without modifications
1368
- return "appimagetool";
1369
- }
1370
-
1371
- // AppImage tooling functions
1372
- async function ensureAppImageTooling(): Promise<void> {
1373
- // Create vendor directory for dependencies
1374
- const vendorDir = getVendorDir();
1375
- mkdirSync(vendorDir, { recursive: true });
1376
-
1377
- // First check if we have vendored libfuse2
1378
- const vendoredLibfusePath = join(vendorDir, "libfuse.so.2");
1379
-
1380
- if (existsSync(vendoredLibfusePath)) {
1381
- console.log("✓ Using vendored libfuse2");
1382
- } else {
1383
- // Check if FUSE2 is available system-wide
1384
- try {
1385
- execSync("ls /usr/lib/*/libfuse.so.2 || ls /lib/*/libfuse.so.2", {
1386
- stdio: "ignore",
1387
- });
1388
- console.log("✓ System libfuse2 found");
1389
- } catch (error) {
1390
- // libfuse2 not found, attempt to download it
1391
- console.log("📥 libfuse2 not found, downloading vendored copy...");
1392
-
1393
- try {
1394
- await downloadLibfuse2(vendorDir);
1395
- } catch (downloadError) {
1396
- console.log("");
1397
- console.log(
1398
- "═══════════════════════════════════════════════════════════════",
1399
- );
1400
- console.log("🚨 FUSE2 DEPENDENCY MISSING");
1401
- console.log(
1402
- "═══════════════════════════════════════════════════════════════",
1403
- );
1404
- console.log("Failed to download libfuse2 automatically.");
1405
- console.log("");
1406
- console.log("You can manually install it using:");
1407
- console.log(" sudo apt update && sudo apt install -y libfuse2");
1408
- console.log("");
1409
- console.log(
1410
- "Without libfuse2, AppImage creation will fail with FUSE errors.",
1411
- );
1412
- console.log(
1413
- "═══════════════════════════════════════════════════════════════",
1414
- );
1415
- console.log("");
1416
- throw new Error(
1417
- "libfuse2 is required for AppImage creation but not found. Please install it manually.",
1418
- );
1419
- }
1420
- }
1421
- }
1422
-
1423
- try {
1424
- // Check if appimagetool is available
1425
- execSync("which appimagetool", { stdio: "ignore" });
1426
- console.log("✓ appimagetool found");
1427
- return;
1428
- } catch (error) {
1429
- // appimagetool not found, download it automatically
1430
- console.log("📥 appimagetool not found, downloading...");
1431
-
1432
- try {
1433
- // Determine architecture-specific download URL
1434
- const downloadUrl =
1435
- ARCH === "arm64"
1436
- ? "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage"
1437
- : "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage";
1438
-
1439
- // Download appimagetool
1440
- console.log(`Downloading appimagetool from ${downloadUrl}...`);
1441
- execSync(`wget -q "${downloadUrl}" -O /tmp/appimagetool.AppImage`, {
1442
- stdio: "inherit",
1443
- });
1444
-
1445
- // Make it executable
1446
- execSync("chmod +x /tmp/appimagetool.AppImage", { stdio: "inherit" });
1447
-
1448
- // Try to move to /usr/local/bin (with sudo)
1449
- try {
1450
- execSync(
1451
- "sudo mv /tmp/appimagetool.AppImage /usr/local/bin/appimagetool",
1452
- { stdio: "inherit" },
1453
- );
1454
- console.log("✓ appimagetool installed to /usr/local/bin/appimagetool");
1455
- } catch (sudoError) {
1456
- // Fallback: extract and place in user's local bin
1457
- console.log("sudo not available, installing to ~/.local/bin/...");
1458
- execSync("mkdir -p ~/.local/bin", { stdio: "inherit" });
1459
-
1460
- // Extract the AppImage to get the binary
1461
- // Use vendored libfuse2 if available for extraction
1462
- const vendorDir = getVendorDir();
1463
- const vendoredLibfusePath = join(vendorDir, "libfuse.so.2");
1464
- const extractCmd = existsSync(vendoredLibfusePath)
1465
- ? `LD_LIBRARY_PATH="${vendorDir}:$LD_LIBRARY_PATH" ./appimagetool.AppImage --appimage-extract`
1466
- : "./appimagetool.AppImage --appimage-extract";
1467
-
1468
- execSync(`cd /tmp && ${extractCmd} >/dev/null 2>&1`, {
1469
- stdio: "inherit",
1470
- });
1471
- execSync(
1472
- "cp /tmp/squashfs-root/usr/bin/appimagetool ~/.local/bin/appimagetool",
1473
- { stdio: "inherit" },
1474
- );
1475
- execSync("chmod +x ~/.local/bin/appimagetool", { stdio: "inherit" });
1476
-
1477
- // Set up symlink for mksquashfs dependency
1478
- execSync("mkdir -p ~/.local/lib/appimagekit", { stdio: "inherit" });
1479
- execSync(
1480
- "ln -sf /usr/bin/mksquashfs ~/.local/lib/appimagekit/mksquashfs",
1481
- { stdio: "inherit" },
1482
- );
1483
-
1484
- // Clean up
1485
- execSync("rm -rf /tmp/appimagetool.AppImage /tmp/squashfs-root", {
1486
- stdio: "inherit",
1487
- });
1488
-
1489
- console.log("✓ appimagetool installed to ~/.local/bin/appimagetool");
1490
- console.log(
1491
- "Note: Make sure ~/.local/bin is in your PATH for future use",
1492
- );
1493
- }
1494
- } catch (downloadError) {
1495
- console.error("Failed to download appimagetool:", downloadError);
1496
- throw new Error(
1497
- "Failed to install appimagetool automatically. Please install it manually.",
1498
- );
1499
- }
1500
- }
1501
- }
1502
-
1503
- async function createAppImage(
1504
- appBundlePath: string,
1505
- appFileName: string,
1506
- config: any,
1507
- buildFolder: string,
1188
+ /**
1189
+ * Creates a Linux installer tar.gz containing:
1190
+ * - Self-extracting installer executable (with embedded app archive)
1191
+ * - README.txt with instructions
1192
+ *
1193
+ * This replaces the AppImage-based installer to avoid libfuse2 dependency.
1194
+ * The installer executable has the compressed app archive embedded within it
1195
+ * using magic markers, similar to how Windows installers work.
1196
+ */
1197
+ async function createLinuxInstallerArchive(
1198
+ buildFolder: string,
1199
+ compressedTarPath: string,
1200
+ appFileName: string,
1201
+ config: any,
1202
+ buildEnvironment: string,
1203
+ hash: string,
1204
+ targetPaths: ReturnType<typeof getPlatformPaths>,
1508
1205
  ): Promise<string> {
1509
- console.log(`🚀 CREATING APPIMAGE WITH PATH: ${appBundlePath}`);
1510
- // console.log(`DEBUG: createAppImage called with:`);
1511
- // console.log(` appBundlePath: ${appBundlePath}`);
1512
- // console.log(` appFileName: ${appFileName}`);
1513
- // console.log(` buildFolder: ${buildFolder}`);
1514
- // console.log(` current working directory: ${process.cwd()}`);
1515
-
1516
- // Ensure appBundlePath is absolute - fix for when it's passed as basename only
1517
- let resolvedAppBundlePath = appBundlePath;
1518
- if (!path.isAbsolute(appBundlePath)) {
1519
- resolvedAppBundlePath = join(buildFolder, appBundlePath);
1520
- // console.log(`DEBUG: Converted relative path to absolute: ${resolvedAppBundlePath}`);
1521
- }
1522
-
1523
- // Create AppDir structure
1524
- const appDirPath = join(buildFolder, `${appFileName}.AppDir`);
1525
- if (existsSync(appDirPath)) {
1526
- rmSync(appDirPath, { recursive: true, force: true });
1527
- }
1528
- mkdirSync(appDirPath, { recursive: true });
1529
-
1530
- try {
1531
- // Copy the entire app bundle to AppDir/usr/bin/
1532
- const usrBinPath = join(appDirPath, "usr", "bin");
1533
- mkdirSync(usrBinPath, { recursive: true });
1534
-
1535
- // console.log(`DEBUG: Attempting to copy from: ${resolvedAppBundlePath}`);
1536
- // console.log(`DEBUG: Does source exist? ${existsSync(resolvedAppBundlePath)}`);
1537
- // console.log(`DEBUG: To destination: ${join(usrBinPath, basename(resolvedAppBundlePath))}`);
1538
-
1539
- if (!existsSync(resolvedAppBundlePath)) {
1540
- throw new Error(`Source bundle does not exist: ${resolvedAppBundlePath}`);
1541
- }
1542
-
1543
- // console.log(`DEBUG: About to copy with cpSync:`);
1544
- // console.log(` from: ${resolvedAppBundlePath} (exists: ${existsSync(resolvedAppBundlePath)})`);
1545
- // console.log(` to: ${join(usrBinPath, basename(resolvedAppBundlePath))}`);
1546
-
1547
- cpSync(
1548
- resolvedAppBundlePath,
1549
- join(usrBinPath, basename(resolvedAppBundlePath)),
1550
- {
1551
- recursive: true,
1552
- dereference: true,
1553
- },
1554
- );
1555
-
1556
- // Create AppRun script (the entry point)
1557
- const appBundleBasename = basename(resolvedAppBundlePath);
1558
- const appRunContent = `#!/bin/bash
1559
- # AppRun script for ${appFileName}
1560
- HERE="$(dirname "$(readlink -f "\${0}")")"
1561
- EXEC="\${HERE}/usr/bin/${appBundleBasename}/bin/launcher"
1562
-
1563
- # Set up library path for CEF
1564
- export LD_LIBRARY_PATH="\${HERE}/usr/bin/${appBundleBasename}/bin:\${HERE}/usr/bin/${appBundleBasename}/lib:\${LD_LIBRARY_PATH}"
1565
-
1566
- # Execute the application
1567
- exec "\${EXEC}" "\$@"
1568
- `;
1569
-
1570
- const appRunPath = join(appDirPath, "AppRun");
1571
- writeFileSync(appRunPath, appRunContent);
1572
- execSync(`chmod +x ${escapePathForTerminal(appRunPath)}`);
1573
-
1574
- // Create .desktop file in AppDir root
1575
- // Always include Icon field since we always create an icon (either real or placeholder)
1576
- const desktopContent = `[Desktop Entry]
1577
- Version=1.0
1578
- Type=Application
1579
- Name=${config.app.name}
1580
- Comment=${config.app.description || ""}
1581
- Exec=${appFileName}
1582
- Icon=${appFileName}
1583
- Terminal=false
1584
- StartupWMClass=${appFileName}
1585
- Categories=Utility;
1206
+ console.log("Creating Linux installer archive...");
1207
+
1208
+ // Create installer name using sanitized app file name (no spaces, URL-safe)
1209
+ const installerName = buildEnvironment === "stable"
1210
+ ? `${appFileName}-Setup`
1211
+ : `${appFileName}-Setup-${buildEnvironment}`;
1212
+
1213
+ // Create temp directory for staging
1214
+ const stagingDir = join(buildFolder, `${installerName}-staging`);
1215
+ if (existsSync(stagingDir)) {
1216
+ rmSync(stagingDir, { recursive: true, force: true });
1217
+ }
1218
+ mkdirSync(stagingDir, { recursive: true });
1219
+
1220
+ try {
1221
+ // 1. Create self-extracting installer binary
1222
+ // Read the extractor binary
1223
+ const extractorBinary = readFileSync(targetPaths.EXTRACTOR);
1224
+
1225
+ // Read the compressed archive
1226
+ const compressedArchive = readFileSync(compressedTarPath);
1227
+
1228
+ // Create metadata JSON
1229
+ const metadata = {
1230
+ identifier: config.app.identifier,
1231
+ name: config.app.name,
1232
+ channel: buildEnvironment,
1233
+ hash: hash,
1234
+ };
1235
+ const metadataJson = JSON.stringify(metadata);
1236
+ const metadataBuffer = Buffer.from(metadataJson, "utf8");
1237
+
1238
+ // Create marker buffers
1239
+ const metadataMarker = Buffer.from("ELECTROBUN_METADATA_V1", "utf8");
1240
+ const archiveMarker = Buffer.from("ELECTROBUN_ARCHIVE_V1", "utf8");
1241
+
1242
+ // Combine extractor + metadata marker + metadata + archive marker + archive
1243
+ const combinedBuffer = Buffer.concat([
1244
+ new Uint8Array(extractorBinary),
1245
+ new Uint8Array(metadataMarker),
1246
+ new Uint8Array(metadataBuffer),
1247
+ new Uint8Array(archiveMarker),
1248
+ new Uint8Array(compressedArchive),
1249
+ ]);
1250
+
1251
+ // Write the self-extracting installer
1252
+ const installerPath = join(stagingDir, "installer");
1253
+ writeFileSync(installerPath, new Uint8Array(combinedBuffer), {
1254
+ mode: 0o755,
1255
+ });
1256
+ execSync(`chmod +x ${escapePathForTerminal(installerPath)}`);
1257
+
1258
+ // 2. Create README for clarity
1259
+ const readmeContent = `${config.app.name} Installer
1260
+ ========================
1261
+
1262
+ To install ${config.app.name}:
1263
+
1264
+ 1. Double-click the 'installer' file
1265
+ 2. Or run from terminal: ./installer
1266
+
1267
+ The installer will:
1268
+ - Extract the application to ~/.local/share/
1269
+ - Create a desktop shortcut with the app's icon
1270
+
1271
+ For more information, visit: ${config.app.homepage || 'https://electrobun.dev'}
1586
1272
  `;
1587
1273
 
1588
- const desktopPath = join(appDirPath, `${appFileName}.desktop`);
1589
- writeFileSync(desktopPath, desktopContent);
1590
-
1591
- // Copy icon if available, or create a minimal placeholder
1592
- if (
1593
- config.build.linux?.icon &&
1594
- existsSync(join(projectRoot, config.build.linux.icon))
1595
- ) {
1596
- const iconSourcePath = join(projectRoot, config.build.linux.icon);
1597
- const iconDestPath = join(appDirPath, `${appFileName}.png`);
1598
- const dirIconPath = join(appDirPath, ".DirIcon");
1599
-
1600
- cpSync(iconSourcePath, iconDestPath, { dereference: true });
1601
- cpSync(iconSourcePath, dirIconPath, { dereference: true });
1602
-
1603
- console.log(
1604
- `Copied icon for AppImage: ${iconSourcePath} -> ${iconDestPath}`,
1605
- );
1606
- console.log(`Created .DirIcon: ${iconSourcePath} -> ${dirIconPath}`);
1607
- } else {
1608
- // Create a minimal 1x1 transparent PNG as placeholder to satisfy appimagetool
1609
- // This prevents "Icon entry not found" errors
1610
- const placeholderPNG = Buffer.from([
1611
- 0x89,
1612
- 0x50,
1613
- 0x4e,
1614
- 0x47,
1615
- 0x0d,
1616
- 0x0a,
1617
- 0x1a,
1618
- 0x0a, // PNG signature
1619
- 0x00,
1620
- 0x00,
1621
- 0x00,
1622
- 0x0d,
1623
- 0x49,
1624
- 0x48,
1625
- 0x44,
1626
- 0x52, // IHDR chunk
1627
- 0x00,
1628
- 0x00,
1629
- 0x00,
1630
- 0x01,
1631
- 0x00,
1632
- 0x00,
1633
- 0x00,
1634
- 0x01, // 1x1 dimensions
1635
- 0x08,
1636
- 0x06,
1637
- 0x00,
1638
- 0x00,
1639
- 0x00,
1640
- 0x1f,
1641
- 0x15,
1642
- 0xc4, // 8-bit RGBA
1643
- 0x89,
1644
- 0x00,
1645
- 0x00,
1646
- 0x00,
1647
- 0x0b,
1648
- 0x49,
1649
- 0x44,
1650
- 0x41, // IDAT chunk
1651
- 0x54,
1652
- 0x08,
1653
- 0x99,
1654
- 0x01,
1655
- 0x00,
1656
- 0x00,
1657
- 0x05,
1658
- 0x00,
1659
- 0x01,
1660
- 0x06,
1661
- 0x7a,
1662
- 0x81,
1663
- 0x7c,
1664
- 0x00,
1665
- 0x00,
1666
- 0x00, // IEND chunk
1667
- 0x00,
1668
- 0x49,
1669
- 0x45,
1670
- 0x4e,
1671
- 0x44,
1672
- 0xae,
1673
- 0x42,
1674
- 0x60,
1675
- 0x82,
1676
- ]);
1677
-
1678
- const iconDestPath = join(appDirPath, `${appFileName}.png`);
1679
- const dirIconPath = join(appDirPath, ".DirIcon");
1680
-
1681
- writeFileSync(iconDestPath, new Uint8Array(placeholderPNG));
1682
- writeFileSync(dirIconPath, new Uint8Array(placeholderPNG));
1683
-
1684
- console.log(
1685
- `Created placeholder icon for AppImage (no icon specified in config)`,
1686
- );
1687
- }
1688
-
1689
- // Generate the AppImage using appimagetool
1690
- const appImagePath = join(buildFolder, `${appFileName}.AppImage`);
1691
- if (existsSync(appImagePath)) {
1692
- unlinkSync(appImagePath);
1693
- }
1694
-
1695
- // console.log(`DEBUG: AppDir path: ${appDirPath}`);
1696
- // console.log(`DEBUG: Does AppDir exist? ${existsSync(appDirPath)}`);
1697
- console.log(`Generating AppImage: ${appImagePath}`);
1698
- const appImageArch = ARCH === "arm64" ? "aarch64" : "x86_64";
1699
-
1700
- // Use full path to appimagetool if not in PATH
1701
- let appimagetoolBase = "appimagetool";
1702
- try {
1703
- execSync("which appimagetool", { stdio: "ignore" });
1704
- } catch {
1705
- // Try ~/.local/bin/appimagetool
1706
- const localBinPath = join(
1707
- process.env["HOME"] || "",
1708
- ".local",
1709
- "bin",
1710
- "appimagetool",
1711
- );
1712
- if (existsSync(localBinPath)) {
1713
- appimagetoolBase = localBinPath;
1714
- }
1715
- }
1716
-
1717
- // Get the command with proper environment for vendored libfuse2
1718
- const appimagetoolCmd = getAppImageToolCommand().replace(
1719
- "appimagetool",
1720
- appimagetoolBase,
1721
- );
1722
-
1723
- try {
1724
- // First try with --no-appstream flag to avoid some FUSE-related issues
1725
- execSync(
1726
- `ARCH=${appImageArch} ${appimagetoolCmd} --no-appstream ${escapePathForTerminal(appDirPath)} ${escapePathForTerminal(appImagePath)}`,
1727
- {
1728
- stdio: "inherit",
1729
- env: { ...process.env, ARCH: appImageArch },
1730
- },
1731
- );
1732
- } catch (error) {
1733
- console.error("Failed to create AppImage:", error);
1734
- console.log(
1735
- "Note: If you see FUSE errors, you may need to install libfuse2:",
1736
- );
1737
- console.log(" sudo apt update && sudo apt install -y libfuse2");
1738
- throw error;
1739
- }
1740
-
1741
- // Verify the AppImage was created
1742
- if (!existsSync(appImagePath)) {
1743
- throw new Error(
1744
- `AppImage was not created at expected path: ${appImagePath}`,
1745
- );
1746
- }
1747
-
1748
- // Extract and copy icon for desktop shortcut
1749
- const iconExtractPath = join(buildFolder, `${appFileName}.png`);
1750
- if (
1751
- config.build.linux?.icon &&
1752
- existsSync(join(projectRoot, config.build.linux.icon))
1753
- ) {
1754
- const iconSourcePath = join(projectRoot, config.build.linux.icon);
1755
- cpSync(iconSourcePath, iconExtractPath, { dereference: true });
1756
- console.log(`✓ Icon extracted for desktop shortcut: ${iconExtractPath}`);
1757
- } else {
1758
- // Create placeholder icon for desktop shortcut
1759
- const placeholderPNG = Buffer.from([
1760
- 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
1761
- 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
1762
- 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00,
1763
- 0x0b, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x01, 0x00, 0x00, 0x05, 0x00,
1764
- 0x01, 0x06, 0x7a, 0x81, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
1765
- 0x44, 0xae, 0x42, 0x60, 0x82,
1766
- ]);
1767
- writeFileSync(iconExtractPath, new Uint8Array(placeholderPNG));
1768
- console.log(
1769
- `✓ Created placeholder icon for desktop shortcut: ${iconExtractPath}`,
1770
- );
1771
- }
1274
+ writeFileSync(join(stagingDir, "README.txt"), readmeContent);
1275
+
1276
+ // 3. Create the tar.gz archive (extract contents directly, no nested folder)
1277
+ const archiveName = `${installerName}.tar.gz`;
1278
+ const archivePath = join(buildFolder, archiveName);
1279
+
1280
+ console.log(`Creating installer archive: ${archivePath}`);
1281
+
1282
+ // Use tar to create the archive, preserving executable permissions
1283
+ // The -C changes to the staging dir, then . archives its contents directly
1284
+ execSync(
1285
+ `tar -czf ${escapePathForTerminal(archivePath)} -C ${escapePathForTerminal(stagingDir)} .`,
1286
+ { stdio: 'inherit' }
1287
+ );
1288
+
1289
+ // Verify the archive was created
1290
+ if (!existsSync(archivePath)) {
1291
+ throw new Error(`Installer archive was not created at expected path: ${archivePath}`);
1292
+ }
1293
+
1294
+ const stats = statSync(archivePath);
1295
+ console.log(
1296
+ `✓ Linux installer archive created: ${archivePath} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`
1297
+ );
1298
+
1299
+ return archivePath;
1300
+ } finally {
1301
+ // Clean up staging directory
1302
+ if (existsSync(stagingDir)) {
1303
+ rmSync(stagingDir, { recursive: true, force: true });
1304
+ }
1305
+ }
1306
+ }
1772
1307
 
1773
- // Create desktop shortcut alongside the AppImage
1774
- const desktopShortcutPath = join(buildFolder, `${appFileName}.desktop`);
1775
1308
 
1776
- const desktopShortcutContent = `[Desktop Entry]
1777
- Version=1.0
1778
- Type=Application
1779
- Name=${config.app.name}
1780
- Comment=${config.app.description || ""}
1781
- Exec=${appImagePath}
1782
- Icon=${iconExtractPath}
1783
- Terminal=false
1784
- StartupWMClass=${appFileName}
1785
- Categories=Utility;
1786
- `;
1787
1309
 
1788
- writeFileSync(desktopShortcutPath, desktopShortcutContent);
1789
- execSync(`chmod +x ${escapePathForTerminal(desktopShortcutPath)}`);
1790
- console.log(`✓ Desktop shortcut created: ${desktopShortcutPath}`);
1791
1310
 
1792
- console.log(`✓ AppImage created: ${appImagePath}`);
1793
- return appImagePath;
1794
- } finally {
1795
- if (existsSync(appDirPath)) {
1796
- rmSync(appDirPath, { recursive: true, force: true });
1797
- }
1798
- }
1799
- }
1800
1311
 
1801
1312
  // Helper function to generate usage description entries for Info.plist
1802
1313
  function generateUsageDescriptions(
@@ -2080,7 +1591,7 @@ ${schemesXml}
2080
1591
  }
2081
1592
  };
2082
1593
 
2083
- const buildIcons = (appBundleFolderResourcesPath: string) => {
1594
+ const buildIcons = (appBundleFolderResourcesPath: string, appBundleFolderPath: string) => {
2084
1595
  // Platform-specific icon handling
2085
1596
  if (targetOS === "macos" && config.build.mac?.icons) {
2086
1597
  // macOS uses .iconset folders that get converted to .icns using iconutil
@@ -2140,6 +1651,24 @@ ${schemesXml}
2140
1651
  } else {
2141
1652
  console.log(`WARNING: Linux icon not found: ${iconSourcePath}`);
2142
1653
  }
1654
+
1655
+ // Create desktop file template for Linux
1656
+ const desktopContent = `[Desktop Entry]
1657
+ Version=1.0
1658
+ Type=Application
1659
+ Name=${config.app.name}
1660
+ Comment=${config.app.description || `${config.app.name} application`}
1661
+ Exec=launcher
1662
+ Icon=appIcon.png
1663
+ Terminal=false
1664
+ StartupWMClass=${config.app.name}
1665
+ Categories=Utility;Application;
1666
+ `;
1667
+
1668
+ const desktopFilePath = join(appBundleFolderPath, `${config.app.name}.desktop`);
1669
+ writeFileSync(desktopFilePath, desktopContent);
1670
+ console.log(`Created Linux desktop file: ${desktopFilePath}`);
1671
+
2143
1672
  } else if (targetOS === "win" && config.build.win?.icon) {
2144
1673
  const iconPath = join(projectRoot, config.build.win.icon);
2145
1674
  if (existsSync(iconPath)) {
@@ -2926,7 +2455,7 @@ ${schemesXml}
2926
2455
  cpSync(source, destination, { recursive: true, dereference: true });
2927
2456
  }
2928
2457
 
2929
- buildIcons(appBundleFolderResourcesPath);
2458
+ buildIcons(appBundleFolderResourcesPath, appBundleFolderPath);
2930
2459
 
2931
2460
  // Run postBuild script
2932
2461
  runHook("postBuild");
@@ -3111,10 +2640,18 @@ ${schemesXml}
3111
2640
  bundleFiles[join(bundleBase, entryPath)] = Bun.file(fullPath);
3112
2641
  }
3113
2642
  }
3114
- const archiveBytes = await new Bun.Archive(bundleFiles).bytes();
3115
- // Note: wyhash is the default in Bun.hash but that may change in the future
3116
- // so we're being explicit here.
3117
- hash = Bun.hash.wyhash(archiveBytes, 43770n).toString(36);
2643
+ // Check if Bun.Archive is available (Bun 1.3.0+)
2644
+ if (typeof Bun.Archive !== 'undefined') {
2645
+ const archiveBytes = await new Bun.Archive(bundleFiles).bytes();
2646
+ // Note: wyhash is the default in Bun.hash but that may change in the future
2647
+ // so we're being explicit here.
2648
+ hash = Bun.hash.wyhash(archiveBytes, 43770n).toString(36);
2649
+ } else {
2650
+ // Fallback for older Bun versions - use a simple hash of file paths
2651
+ console.warn("Bun.Archive not available, using fallback hash method");
2652
+ const fileList = Object.keys(bundleFiles).sort().join('\n');
2653
+ hash = Bun.hash.wyhash(fileList).toString(36);
2654
+ }
3118
2655
  console.timeEnd("Generate Bundle hash");
3119
2656
  }
3120
2657
 
@@ -3201,95 +2738,9 @@ ${schemesXml}
3201
2738
 
3202
2739
  const artifactsToUpload = [];
3203
2740
 
3204
- // Linux AppImage creation (skip for dev environment)
3205
- if (targetOS === "linux" && buildEnvironment !== "dev") {
3206
- // Ensure AppImage tooling is available
3207
- await ensureAppImageTooling();
3208
-
3209
- // Create AppImage from the app bundle (for canary and stable builds)
3210
- console.log(
3211
- `🔍 CALLING createAppImage with appBundleFolderPath: ${appBundleFolderPath}`,
3212
- );
3213
- console.log(`🔍 buildFolder: ${buildFolder}`);
3214
- console.log(`🔍 appFileName: ${appFileName}`);
3215
- const appImagePath = await createAppImage(
3216
- appBundleFolderPath,
3217
- appFileName,
3218
- config,
3219
- buildFolder,
3220
- );
3221
-
3222
- console.log(`✓ Linux AppImage created at: ${appImagePath}`);
3223
-
3224
- // Only create compressed tar for non-dev builds
3225
- if (buildEnvironment !== "dev") {
3226
- // For Linux, create a compressed tar containing:
3227
- // 1. The AppImage
3228
- // 2. Desktop shortcut file
3229
- // 3. Icon file
3230
- // 4. Metadata
3231
-
3232
- const tempDirName = `${appFileName}-tar-staging`;
3233
- const tempDirPath = join(buildFolder, tempDirName);
3234
-
3235
- // Clean up any existing temp directory
3236
- if (existsSync(tempDirPath)) {
3237
- rmSync(tempDirPath, { recursive: true });
3238
- }
3239
-
3240
- try {
3241
- // Create temp directory structure
3242
- mkdirSync(tempDirPath, { recursive: true });
3243
- const innerDirPath = join(tempDirPath, appFileName);
3244
- mkdirSync(innerDirPath, { recursive: true });
3245
-
3246
- // Copy AppImage (the inner app bundle on Linux)
3247
- const appImageDestPath = join(
3248
- innerDirPath,
3249
- `${appFileName}.AppImage`,
3250
- );
3251
- cpSync(appImagePath, appImageDestPath, { dereference: true });
3252
-
3253
- // Copy desktop shortcut and icon (they were created alongside the AppImage)
3254
- const desktopPath = join(buildFolder, `${appFileName}.desktop`);
3255
- const iconPath = join(buildFolder, `${appFileName}.png`);
3256
-
3257
- if (existsSync(desktopPath)) {
3258
- cpSync(desktopPath, join(innerDirPath, `${appFileName}.desktop`));
3259
- }
3260
-
3261
- if (existsSync(iconPath)) {
3262
- cpSync(iconPath, join(innerDirPath, `${appFileName}.png`));
3263
- }
3264
-
3265
- // Create metadata file
3266
- const metadata = {
3267
- identifier: config.app.identifier,
3268
- name: config.app.name,
3269
- version: config.app.version,
3270
- channel: buildEnvironment,
3271
- hash: hash,
3272
- };
3273
- writeFileSync(
3274
- join(innerDirPath, "metadata.json"),
3275
- JSON.stringify(metadata, null, 2),
3276
- );
3277
-
3278
- const appImageTarPath = join(buildFolder, `${appFileName}.tar`);
3279
- console.log(`Creating tar of installer contents: ${appImageTarPath}`);
3280
-
3281
- // Tar the inner directory
3282
- createTar(appImageTarPath, tempDirPath, [appFileName]);
3283
- } finally {
3284
- if (existsSync(tempDirPath)) {
3285
- rmSync(tempDirPath, { recursive: true });
3286
- }
3287
- }
3288
-
3289
- // Leave the tar uncompressed for diff generation; compression happens later.
3290
- // Note: Don't delete uncompressed tar here - bsdiff needs it later for patch generation.
3291
- }
3292
- }
2741
+ // Linux bundle preparation (skip tar creation for dev environment)
2742
+ // For Linux, the app bundle is already in the correct directory structure
2743
+ // The tar will be created in the common code path below
3293
2744
 
3294
2745
  if (buildEnvironment !== "dev") {
3295
2746
  // zig-zstd CLI (native zstd)
@@ -3313,10 +2764,11 @@ ${schemesXml}
3313
2764
  `${appFileName}${targetOS === "macos" ? ".app" : ""}.tar`,
3314
2765
  );
3315
2766
 
3316
- // For Linux, we've already created the tar in the AppImage section above
3317
- // For macOS/Windows, tar the signed and notarized app bundle
3318
- if (targetOS !== "linux") {
3319
- createTar(tarPath, buildFolder, [basename(appBundleFolderPath)]);
2767
+ // Tar the app bundle for all platforms
2768
+ createTar(tarPath, buildFolder, [basename(appBundleFolderPath)]);
2769
+
2770
+ // Remove the app bundle folder after tarring (except on Linux where it might be needed for dev)
2771
+ if (targetOS !== "linux" || buildEnvironment !== "dev") {
3320
2772
  rmdirSync(appBundleFolderPath, { recursive: true });
3321
2773
  }
3322
2774
 
@@ -3572,7 +3024,7 @@ ${schemesXml}
3572
3024
  dereference: true,
3573
3025
  });
3574
3026
 
3575
- buildIcons(selfExtractingBundle.appBundleFolderResourcesPath);
3027
+ buildIcons(selfExtractingBundle.appBundleFolderResourcesPath, selfExtractingBundle.appBundleFolderPath);
3576
3028
  await Bun.write(
3577
3029
  join(selfExtractingBundle.appBundleFolderContentsPath, "Info.plist"),
3578
3030
  InfoPlistContents,
@@ -3718,29 +3170,22 @@ ${schemesXml}
3718
3170
  // Also keep the raw exe for backwards compatibility (optional)
3719
3171
  // artifactsToUpload.push(selfExtractingExePath);
3720
3172
  } else if (targetOS === "linux") {
3721
- // On Linux, create a self-extracting AppImage with embedded archive
3173
+ // On Linux, create a self-extracting installer archive
3722
3174
  // Use the Linux-specific compressed tar path
3723
3175
  const linuxCompressedTarPath = join(
3724
3176
  buildFolder,
3725
3177
  `${appFileName}.tar.zst`,
3726
3178
  );
3727
- const selfExtractingAppImagePath =
3728
- await createLinuxSelfExtractingAppImage(
3729
- buildFolder,
3730
- linuxCompressedTarPath,
3731
- appFileName,
3732
- config,
3733
- buildEnvironment,
3734
- hash,
3735
- );
3736
-
3737
- // Wrap the Setup.AppImage in a tar.gz to preserve executable permissions
3738
- const archivedAppImagePath = await wrapInArchive(
3739
- selfExtractingAppImagePath,
3179
+ const installerArchivePath = await createLinuxInstallerArchive(
3740
3180
  buildFolder,
3741
- "tar.gz",
3181
+ linuxCompressedTarPath,
3182
+ appFileName,
3183
+ config,
3184
+ buildEnvironment,
3185
+ hash,
3186
+ targetPaths,
3742
3187
  );
3743
- artifactsToUpload.push(archivedAppImagePath);
3188
+ artifactsToUpload.push(installerArchivePath);
3744
3189
  }
3745
3190
  }
3746
3191
 
@@ -3857,8 +3302,6 @@ ${schemesXml}
3857
3302
  let bundleExecPath: string;
3858
3303
  // @ts-expect-error - reserved for future use
3859
3304
  let _bundleResourcesPath: string;
3860
- let isAppImage = false;
3861
-
3862
3305
  if (OS === "macos") {
3863
3306
  bundleExecPath = join(buildFolder, bundleFileName, "Contents", "MacOS");
3864
3307
  _bundleResourcesPath = join(
@@ -3868,18 +3311,9 @@ ${schemesXml}
3868
3311
  "Resources",
3869
3312
  );
3870
3313
  } else if (OS === "linux") {
3871
- // Check if we have an AppImage or directory bundle
3872
- const appImagePath = join(buildFolder, `${bundleFileName}.AppImage`);
3873
- if (existsSync(appImagePath)) {
3874
- // AppImage mode
3875
- bundleExecPath = appImagePath;
3876
- _bundleResourcesPath = join(buildFolder, bundleFileName, "Resources"); // For compatibility
3877
- isAppImage = true;
3878
- } else {
3879
- // Directory bundle mode (fallback)
3880
- bundleExecPath = join(buildFolder, bundleFileName, "bin");
3881
- _bundleResourcesPath = join(buildFolder, bundleFileName, "Resources");
3882
- }
3314
+ // Directory bundle mode
3315
+ bundleExecPath = join(buildFolder, bundleFileName, "bin");
3316
+ _bundleResourcesPath = join(buildFolder, bundleFileName, "Resources");
3883
3317
  } else if (OS === "win") {
3884
3318
  bundleExecPath = join(buildFolder, bundleFileName, "bin");
3885
3319
  _bundleResourcesPath = join(buildFolder, bundleFileName, "Resources");
@@ -3889,8 +3323,7 @@ ${schemesXml}
3889
3323
 
3890
3324
  if (OS === "macos" || OS === "linux") {
3891
3325
  // For Linux dev mode, update libNativeWrapper.so based on bundleCEF setting
3892
- if (OS === "linux" && !isAppImage) {
3893
- // Only update libNativeWrapper for directory bundle mode
3326
+ if (OS === "linux") {
3894
3327
  const currentLibPath = join(bundleExecPath, "libNativeWrapper.so");
3895
3328
  const targetPaths = getPlatformPaths("linux", ARCH);
3896
3329
  const correctLibSource = config.build.linux?.bundleCEF
@@ -3909,20 +3342,11 @@ ${schemesXml}
3909
3342
  }
3910
3343
  }
3911
3344
 
3912
- if (OS === "linux" && isAppImage) {
3913
- // For Linux AppImage mode, execute the AppImage directly
3914
- console.log(`Running AppImage: ${bundleExecPath}`);
3915
- mainProc = Bun.spawn([bundleExecPath], {
3916
- stdio: ["inherit", "inherit", "inherit"],
3917
- cwd: dirname(bundleExecPath),
3918
- });
3919
- } else {
3920
- // Use the zig launcher for macOS and directory bundle Linux
3921
- mainProc = Bun.spawn([join(bundleExecPath, "launcher")], {
3922
- stdio: ["inherit", "inherit", "inherit"],
3923
- cwd: bundleExecPath,
3924
- });
3925
- }
3345
+ // Use the zig launcher for macOS and Linux
3346
+ mainProc = Bun.spawn([join(bundleExecPath, "launcher")], {
3347
+ stdio: ["inherit", "inherit", "inherit"],
3348
+ cwd: bundleExecPath,
3349
+ });
3926
3350
  } else if (OS === "win") {
3927
3351
  mainProc = Bun.spawn([join(bundleExecPath, "launcher.exe")], {
3928
3352
  stdio: ["inherit", "inherit", "inherit"],
@@ -4224,257 +3648,7 @@ ${schemesXml}
4224
3648
  });
4225
3649
  }
4226
3650
 
4227
- async function wrapInArchive(
4228
- filePath: string,
4229
- _buildFolder: string,
4230
- archiveType: "tar.gz" | "zip",
4231
- ): Promise<string> {
4232
- const fileName = basename(filePath);
4233
- const fileDir = dirname(filePath);
4234
-
4235
- if (archiveType === "tar.gz") {
4236
- // Output filename: Setup.exe -> Setup.exe.tar.gz, Setup.AppImage -> Setup.tar.gz
4237
- // For AppImage files, strip the .AppImage extension so archive extracts to .AppImage
4238
- // Sanitize the archive filename (no spaces in artifact URLs) while inner files keep their original names
4239
- const sanitizedBase = (
4240
- fileName.endsWith(".AppImage")
4241
- ? fileName.replace(/\.AppImage$/, "")
4242
- : fileName
4243
- ).replace(/ /g, "");
4244
- const archivePath = join(fileDir, `${sanitizedBase}.tar.gz`);
4245
-
4246
- // For Linux AppImage files, ensure they have executable permissions before archiving
4247
- if (fileName.endsWith(".AppImage")) {
4248
- try {
4249
- // Try to set executable permissions (will only work on Unix-like systems)
4250
- execSync(`chmod +x ${escapePathForTerminal(filePath)}`, {
4251
- stdio: "ignore",
4252
- });
4253
- } catch {
4254
- // Ignore errors on Windows hosts
4255
- }
4256
- }
4257
-
4258
- // Create tar.gz archive using system tar (preserves file permissions)
4259
- createTarGz(archivePath, fileDir, [fileName]);
4260
-
4261
- console.log(
4262
- `Created archive: ${archivePath} (preserving executable permissions)`,
4263
- );
4264
- return archivePath;
4265
- } else if (archiveType === "zip") {
4266
- // Output filename: Setup.exe -> Setup.zip
4267
- const archivePath = filePath.replace(/\.[^.]+$/, ".zip");
4268
-
4269
- // Create zip archive
4270
- const output = createWriteStream(archivePath);
4271
- const archive = archiver("zip", {
4272
- zlib: { level: 9 }, // Maximum compression
4273
- });
4274
-
4275
- return new Promise((resolve, reject) => {
4276
- output.on("close", () => {
4277
- console.log(
4278
- `Created archive: ${archivePath} (${(archive.pointer() / 1024 / 1024).toFixed(2)} MB)`,
4279
- );
4280
- resolve(archivePath);
4281
- });
4282
-
4283
- archive.on("error", (err) => {
4284
- reject(err);
4285
- });
4286
-
4287
- archive.pipe(output);
4288
-
4289
- // Add the file to the archive
4290
- archive.file(filePath, { name: fileName });
4291
-
4292
- archive.finalize();
4293
- });
4294
- }
4295
- throw new Error(`Unsupported archive type: ${archiveType}`);
4296
- }
4297
-
4298
- async function createLinuxSelfExtractingAppImage(
4299
- buildFolder: string,
4300
- compressedTarPath: string,
4301
- _appFileName: string,
4302
- config: any,
4303
- buildEnvironment: string,
4304
- hash: string,
4305
- ): Promise<string> {
4306
- console.log("Creating Linux AppImage wrapper...");
4307
-
4308
- // Create wrapper AppImage filename
4309
- const wrapperName = getLinuxAppImageBaseName(
4310
- config.app.name,
4311
- buildEnvironment,
4312
- );
4313
- const wrapperAppImagePath = join(buildFolder, `${wrapperName}.AppImage`);
4314
- const wrapperAppDirPath = join(buildFolder, `${wrapperName}.AppDir`);
4315
-
4316
- // Clean up any existing AppDir
4317
- if (existsSync(wrapperAppDirPath)) {
4318
- rmSync(wrapperAppDirPath, { recursive: true, force: true });
4319
- }
4320
- mkdirSync(wrapperAppDirPath, { recursive: true });
4321
-
4322
- try {
4323
- // Create usr/bin directory structure
4324
- const usrBinPath = join(wrapperAppDirPath, "usr", "bin");
4325
- mkdirSync(usrBinPath, { recursive: true });
4326
-
4327
- // Create self-extracting binary with embedded archive (following magic markers pattern)
4328
- const targetPaths = getPlatformPaths("linux", ARCH);
4329
-
4330
- // Read the extractor binary
4331
- const extractorBinary = readFileSync(targetPaths.EXTRACTOR);
4332
-
4333
- // Read the compressed archive
4334
- const compressedArchive = readFileSync(compressedTarPath);
4335
-
4336
- // Create metadata JSON
4337
- const metadata = {
4338
- identifier: config.app.identifier,
4339
- name: config.app.name,
4340
- channel: buildEnvironment,
4341
- hash: hash,
4342
- };
4343
- const metadataJson = JSON.stringify(metadata);
4344
- const metadataBuffer = Buffer.from(metadataJson, "utf8");
4345
-
4346
- // Create marker buffers
4347
- const metadataMarker = Buffer.from("ELECTROBUN_METADATA_V1", "utf8");
4348
- const archiveMarker = Buffer.from("ELECTROBUN_ARCHIVE_V1", "utf8");
4349
-
4350
- // Combine extractor + metadata marker + metadata + archive marker + archive
4351
- const combinedBuffer = Buffer.concat([
4352
- new Uint8Array(extractorBinary),
4353
- new Uint8Array(metadataMarker),
4354
- new Uint8Array(metadataBuffer),
4355
- new Uint8Array(archiveMarker),
4356
- new Uint8Array(compressedArchive),
4357
- ]);
4358
-
4359
- // Write the self-extracting binary to AppImage/usr/bin/
4360
- const wrapperExtractorPath = join(usrBinPath, wrapperName);
4361
- writeFileSync(wrapperExtractorPath, new Uint8Array(combinedBuffer), {
4362
- mode: 0o755,
4363
- });
4364
- execSync(`chmod +x ${escapePathForTerminal(wrapperExtractorPath)}`);
4365
-
4366
- // Create AppRun script
4367
- const appRunContent = `#!/bin/bash
4368
- # AppRun script for ${wrapperName}
4369
- HERE="$(dirname "$(readlink -f "\${0}")")"
4370
- EXEC="\${HERE}/usr/bin/${wrapperName}"
4371
-
4372
- # Execute the wrapper extractor
4373
- exec "\${EXEC}" "\$@"
4374
- `;
4375
-
4376
- const appRunPath = join(wrapperAppDirPath, "AppRun");
4377
- writeFileSync(appRunPath, appRunContent);
4378
- execSync(`chmod +x ${escapePathForTerminal(appRunPath)}`);
4379
-
4380
- // Check if icon will be available
4381
- const hasWrapperIcon =
4382
- config.build.linux?.icon &&
4383
- existsSync(join(projectRoot, config.build.linux.icon));
4384
-
4385
- // Create desktop file
4386
- const desktopContent = `[Desktop Entry]
4387
- Version=1.0
4388
- Type=Application
4389
- Name=${config.app.name} Installer
4390
- Comment=Install ${config.app.name}
4391
- Exec=${wrapperName}${hasWrapperIcon ? `\nIcon=${wrapperName}` : ""}
4392
- Terminal=false
4393
- Categories=Utility;
4394
- `;
4395
-
4396
- const desktopPath = join(wrapperAppDirPath, `${wrapperName}.desktop`);
4397
- writeFileSync(desktopPath, desktopContent);
4398
-
4399
- // Copy icon if available
4400
- if (hasWrapperIcon) {
4401
- const iconSourcePath = join(projectRoot, config.build.linux.icon);
4402
- const iconDestPath = join(wrapperAppDirPath, `${wrapperName}.png`);
4403
- const dirIconPath = join(wrapperAppDirPath, ".DirIcon");
4404
-
4405
- cpSync(iconSourcePath, iconDestPath, { dereference: true });
4406
- cpSync(iconSourcePath, dirIconPath, { dereference: true });
4407
-
4408
- console.log(
4409
- `Copied icon for wrapper AppImage: ${iconSourcePath} -> ${iconDestPath}`,
4410
- );
4411
- }
4412
-
4413
- // Ensure appimagetool is available
4414
- await ensureAppImageTooling();
4415
-
4416
- // Generate the wrapper AppImage
4417
- if (existsSync(wrapperAppImagePath)) {
4418
- unlinkSync(wrapperAppImagePath);
4419
- }
4420
-
4421
- console.log(`Creating wrapper AppImage: ${wrapperAppImagePath}`);
4422
- const appImageArch = ARCH === "arm64" ? "aarch64" : "x86_64";
4423
-
4424
- // Use appimagetool to create the wrapper AppImage
4425
- let appimagetoolBase = "appimagetool";
4426
- try {
4427
- execSync("which appimagetool", { stdio: "ignore" });
4428
- } catch {
4429
- const localBinPath = join(
4430
- process.env["HOME"] || "",
4431
- ".local",
4432
- "bin",
4433
- "appimagetool",
4434
- );
4435
- if (existsSync(localBinPath)) {
4436
- appimagetoolBase = localBinPath;
4437
- }
4438
- }
4439
-
4440
- // Get the command with proper environment for vendored libfuse2
4441
- const appimagetoolCmd = getAppImageToolCommand().replace(
4442
- "appimagetool",
4443
- appimagetoolBase,
4444
- );
4445
-
4446
- try {
4447
- execSync(
4448
- `ARCH=${appImageArch} ${appimagetoolCmd} --no-appstream ${escapePathForTerminal(wrapperAppDirPath)} ${escapePathForTerminal(wrapperAppImagePath)}`,
4449
- {
4450
- stdio: "inherit",
4451
- env: { ...process.env, ARCH: appImageArch },
4452
- },
4453
- );
4454
- } catch (error) {
4455
- console.error("Failed to create wrapper AppImage:", error);
4456
- throw error;
4457
- }
4458
-
4459
- // Verify the wrapper AppImage was created
4460
- if (!existsSync(wrapperAppImagePath)) {
4461
- throw new Error(
4462
- `Wrapper AppImage was not created at expected path: ${wrapperAppImagePath}`,
4463
- );
4464
- }
4465
3651
 
4466
- const stats = statSync(wrapperAppImagePath);
4467
- console.log(
4468
- `✓ Linux wrapper AppImage created: ${wrapperAppImagePath} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`,
4469
- );
4470
-
4471
- return wrapperAppImagePath;
4472
- } finally {
4473
- if (existsSync(wrapperAppDirPath)) {
4474
- rmSync(wrapperAppDirPath, { recursive: true, force: true });
4475
- }
4476
- }
4477
- }
4478
3652
 
4479
3653
  function codesignAppBundle(
4480
3654
  appBundleOrDmgPath: string,