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/dist/api/bun/core/ApplicationMenu.ts +1 -26
- package/dist/api/bun/core/ContextMenu.ts +1 -27
- package/dist/api/bun/core/Updater.ts +35 -70
- package/dist/api/bun/core/menuRoles.ts +181 -0
- package/package.json +1 -1
- package/src/cli/index.ts +175 -1001
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
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
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
|
|
3205
|
-
|
|
3206
|
-
|
|
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
|
-
//
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
3181
|
+
linuxCompressedTarPath,
|
|
3182
|
+
appFileName,
|
|
3183
|
+
config,
|
|
3184
|
+
buildEnvironment,
|
|
3185
|
+
hash,
|
|
3186
|
+
targetPaths,
|
|
3742
3187
|
);
|
|
3743
|
-
artifactsToUpload.push(
|
|
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
|
-
//
|
|
3872
|
-
|
|
3873
|
-
|
|
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"
|
|
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
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
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,
|