electrobun 0.13.0-beta.21 → 0.13.0-beta.22
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/Updater.ts +119 -97
- package/package.json +1 -1
- package/src/cli/index.ts +19 -13
|
@@ -8,6 +8,28 @@ import { OS as currentOS, ARCH as currentArch } from '../../shared/platform';
|
|
|
8
8
|
import { getPlatformFolder, getTarballFileName } from '../../shared/naming';
|
|
9
9
|
import { native } from '../proc/native';
|
|
10
10
|
|
|
11
|
+
// Helper to join URL paths without breaking the protocol (path.join mangles https:// to https:/)
|
|
12
|
+
function urlJoin(...parts: string[]): string {
|
|
13
|
+
if (parts.length === 0) return '';
|
|
14
|
+
|
|
15
|
+
// Start with the first part (base URL)
|
|
16
|
+
let result = parts[0];
|
|
17
|
+
|
|
18
|
+
// Join remaining parts
|
|
19
|
+
for (let i = 1; i < parts.length; i++) {
|
|
20
|
+
const part = parts[i];
|
|
21
|
+
if (!part) continue;
|
|
22
|
+
|
|
23
|
+
// Remove trailing slash from result and leading slash from part
|
|
24
|
+
const cleanResult = result.endsWith('/') ? result.slice(0, -1) : result;
|
|
25
|
+
const cleanPart = part.startsWith('/') ? part.slice(1) : part;
|
|
26
|
+
|
|
27
|
+
result = `${cleanResult}/${cleanPart}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
11
33
|
// setTimeout(async () => {
|
|
12
34
|
// console.log('killing')
|
|
13
35
|
// const { native } = await import('../proc/native');
|
|
@@ -144,7 +166,7 @@ const Updater = {
|
|
|
144
166
|
const channelBucketUrl = await Updater.channelBucketUrl();
|
|
145
167
|
const cacheBuster = Math.random().toString(36).substring(7);
|
|
146
168
|
const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
|
|
147
|
-
const updateInfoUrl =
|
|
169
|
+
const updateInfoUrl = urlJoin(localInfo.bucketUrl, platformFolder, `update.json?${cacheBuster}`);
|
|
148
170
|
|
|
149
171
|
try {
|
|
150
172
|
const updateInfoResponse = await fetch(updateInfoUrl);
|
|
@@ -213,7 +235,7 @@ const Updater = {
|
|
|
213
235
|
// check if there's a patch file for it
|
|
214
236
|
const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
|
|
215
237
|
const patchResponse = await fetch(
|
|
216
|
-
|
|
238
|
+
urlJoin(localInfo.bucketUrl, platformFolder, `${currentHash}.patch`)
|
|
217
239
|
);
|
|
218
240
|
|
|
219
241
|
if (!patchResponse.ok) {
|
|
@@ -332,7 +354,7 @@ const Updater = {
|
|
|
332
354
|
const cacheBuster = Math.random().toString(36).substring(7);
|
|
333
355
|
const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
|
|
334
356
|
const tarballName = getTarballFileName(appFileName, currentOS);
|
|
335
|
-
const urlToLatestTarball =
|
|
357
|
+
const urlToLatestTarball = urlJoin(
|
|
336
358
|
localInfo.bucketUrl,
|
|
337
359
|
platformFolder,
|
|
338
360
|
tarballName
|
|
@@ -471,10 +493,12 @@ const Updater = {
|
|
|
471
493
|
// Platform-specific path handling
|
|
472
494
|
let newAppBundlePath: string;
|
|
473
495
|
if (currentOS === 'linux') {
|
|
474
|
-
// On Linux,
|
|
496
|
+
// On Linux, the tarball contains a directory named {appFileName}, and inside it is the AppImage
|
|
497
|
+
// Structure: extractionDir/{appFileName}/{appFileName}.AppImage
|
|
498
|
+
const innerDirName = localInfo.name.replace(/ /g, "");
|
|
475
499
|
const appImageName = `${localInfo.name.replace(/ /g, "").replace(/\./g, "-")}.AppImage`;
|
|
476
|
-
newAppBundlePath = join(extractionDir, appImageName);
|
|
477
|
-
|
|
500
|
+
newAppBundlePath = join(extractionDir, innerDirName, appImageName);
|
|
501
|
+
|
|
478
502
|
// Verify the AppImage exists
|
|
479
503
|
if (!statSync(newAppBundlePath, { throwIfNoEntry: false })) {
|
|
480
504
|
console.error(`AppImage not found at: ${newAppBundlePath}`);
|
|
@@ -483,6 +507,14 @@ const Updater = {
|
|
|
483
507
|
const files = readdirSync(extractionDir);
|
|
484
508
|
for (const file of files) {
|
|
485
509
|
console.log(` - ${file}`);
|
|
510
|
+
// Also list contents of subdirectories
|
|
511
|
+
const subPath = join(extractionDir, file);
|
|
512
|
+
if (statSync(subPath).isDirectory()) {
|
|
513
|
+
const subFiles = readdirSync(subPath);
|
|
514
|
+
for (const subFile of subFiles) {
|
|
515
|
+
console.log(` - ${subFile}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
486
518
|
}
|
|
487
519
|
} catch (e) {
|
|
488
520
|
console.log("Could not list directory contents:", e);
|
|
@@ -530,9 +562,8 @@ const Updater = {
|
|
|
530
562
|
const appImageName = `${localInfo.name.replace(/ /g, "").replace(/\./g, "-")}.AppImage`;
|
|
531
563
|
runningAppBundlePath = join(appDataFolder, appImageName);
|
|
532
564
|
} else {
|
|
533
|
-
// On Windows, use
|
|
534
|
-
|
|
535
|
-
runningAppBundlePath = join(appDataFolder, `app-${currentHash}`);
|
|
565
|
+
// On Windows, use fixed 'app' folder to match extractor
|
|
566
|
+
runningAppBundlePath = join(appDataFolder, 'app');
|
|
536
567
|
}
|
|
537
568
|
}
|
|
538
569
|
// Platform-specific backup handling
|
|
@@ -588,101 +619,92 @@ const Updater = {
|
|
|
588
619
|
// Create/update launcher script that points to the AppImage
|
|
589
620
|
await createLinuxAppImageLauncherScript(runningAppBundlePath);
|
|
590
621
|
} else {
|
|
591
|
-
// On Windows, use
|
|
622
|
+
// On Windows, files are locked while in use, so we need a helper script
|
|
623
|
+
// that runs after the app exits to do the replacement
|
|
592
624
|
const parentDir = dirname(runningAppBundlePath);
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
//
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
::
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
::
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
::
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
::
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
:: Launch the app
|
|
640
|
-
cd /d "%APP_DIR%\\bin"
|
|
641
|
-
start "" launcher.exe
|
|
625
|
+
const updateScriptPath = join(parentDir, 'update.bat');
|
|
626
|
+
const backupDir = join(parentDir, 'app-backup');
|
|
627
|
+
|
|
628
|
+
// Create a batch script that will:
|
|
629
|
+
// 1. Wait for the current app to exit
|
|
630
|
+
// 2. Remove old backup
|
|
631
|
+
// 3. Move current app to backup
|
|
632
|
+
// 4. Move new app to current location
|
|
633
|
+
// 5. Launch the new app
|
|
634
|
+
// 6. Clean up
|
|
635
|
+
const updateScript = `@echo off
|
|
636
|
+
setlocal
|
|
637
|
+
|
|
638
|
+
:: Wait for the app to fully exit (check if launcher.exe is still running)
|
|
639
|
+
:waitloop
|
|
640
|
+
tasklist /FI "IMAGENAME eq launcher.exe" 2>NUL | find /I /N "launcher.exe">NUL
|
|
641
|
+
if "%ERRORLEVEL%"=="0" (
|
|
642
|
+
timeout /t 1 /nobreak >nul
|
|
643
|
+
goto waitloop
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
:: Small extra delay to ensure all file handles are released
|
|
647
|
+
timeout /t 1 /nobreak >nul
|
|
648
|
+
|
|
649
|
+
:: Remove old backup if exists
|
|
650
|
+
if exist "${backupDir.replace(/\//g, '\\')}" (
|
|
651
|
+
rmdir /s /q "${backupDir.replace(/\//g, '\\')}"
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
:: Backup current app folder
|
|
655
|
+
if exist "${runningAppBundlePath.replace(/\//g, '\\')}" (
|
|
656
|
+
move "${runningAppBundlePath.replace(/\//g, '\\')}" "${backupDir.replace(/\//g, '\\')}"
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
:: Move new app to current location
|
|
660
|
+
move "${newAppBundlePath.replace(/\//g, '\\')}" "${runningAppBundlePath.replace(/\//g, '\\')}"
|
|
661
|
+
|
|
662
|
+
:: Clean up extraction directory
|
|
663
|
+
rmdir /s /q "${extractionDir.replace(/\//g, '\\')}" 2>nul
|
|
664
|
+
|
|
665
|
+
:: Launch the new app
|
|
666
|
+
start "" /d "${join(runningAppBundlePath, 'bin').replace(/\//g, '\\')}" launcher.exe
|
|
667
|
+
|
|
668
|
+
:: Delete this update script
|
|
669
|
+
del "%~f0"
|
|
642
670
|
`;
|
|
643
|
-
|
|
644
|
-
await Bun.write(
|
|
645
|
-
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
671
|
+
|
|
672
|
+
await Bun.write(updateScriptPath, updateScript);
|
|
673
|
+
|
|
674
|
+
// Launch the update script detached - it will wait for us to exit
|
|
675
|
+
await Bun.spawn(["cmd", "/c", "start", "", "/min", updateScriptPath], { detached: true });
|
|
676
|
+
|
|
677
|
+
// Small delay to ensure the script starts
|
|
678
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
679
|
+
|
|
680
|
+
// Now exit - the script will take over
|
|
681
|
+
try {
|
|
682
|
+
native.symbols.killApp();
|
|
683
|
+
process.exit(0);
|
|
684
|
+
} catch (e) {
|
|
685
|
+
process.exit(0);
|
|
686
|
+
}
|
|
687
|
+
return; // Won't reach here, but for clarity
|
|
650
688
|
}
|
|
651
689
|
} catch (error) {
|
|
652
690
|
console.error("Failed to replace app with new version", error);
|
|
653
691
|
return;
|
|
654
692
|
}
|
|
655
693
|
|
|
656
|
-
// Cross-platform app launch
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
// On Windows, launch the run.bat file which handles versioning
|
|
671
|
-
const parentDir = dirname(runningAppBundlePath);
|
|
672
|
-
const runBatPath = join(parentDir, "run.bat");
|
|
673
|
-
|
|
674
|
-
// Use start command to launch detached process that survives parent termination
|
|
675
|
-
await Bun.spawn(["cmd", "/c", "start", "", "/d", parentDir, "run.bat"], { detached: true });
|
|
676
|
-
break;
|
|
677
|
-
case 'linux':
|
|
678
|
-
// On Linux, launch the AppImage directly
|
|
679
|
-
Bun.spawn(["sh", "-c", `"${runningAppBundlePath}" &`], { detached: true});
|
|
680
|
-
break;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Small delay on Windows to ensure new process starts before we terminate
|
|
684
|
-
if (currentOS === 'win') {
|
|
685
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
694
|
+
// Cross-platform app launch (Windows is handled above with its own update script)
|
|
695
|
+
if (currentOS === 'macos') {
|
|
696
|
+
// Use a detached shell so relaunch survives after killApp terminates the current process
|
|
697
|
+
await Bun.spawn(
|
|
698
|
+
[
|
|
699
|
+
"sh",
|
|
700
|
+
"-c",
|
|
701
|
+
`open "${runningAppBundlePath}" &`,
|
|
702
|
+
],
|
|
703
|
+
{ detached: true }
|
|
704
|
+
);
|
|
705
|
+
} else if (currentOS === 'linux') {
|
|
706
|
+
// On Linux, launch the AppImage directly
|
|
707
|
+
Bun.spawn(["sh", "-c", `"${runningAppBundlePath}" &`], { detached: true});
|
|
686
708
|
}
|
|
687
709
|
|
|
688
710
|
// Use native killApp to properly clean up all resources
|
|
@@ -702,7 +724,7 @@ start "" launcher.exe
|
|
|
702
724
|
channelBucketUrl: async () => {
|
|
703
725
|
await Updater.getLocallocalInfo();
|
|
704
726
|
const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
|
|
705
|
-
return
|
|
727
|
+
return urlJoin(localInfo.bucketUrl, platformFolder);
|
|
706
728
|
},
|
|
707
729
|
|
|
708
730
|
appDataFolder: async () => {
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -2098,20 +2098,21 @@ if (commandArg === "init") {
|
|
|
2098
2098
|
// Compress with Zstandard
|
|
2099
2099
|
console.log(`Compressing tar with zstd...`);
|
|
2100
2100
|
const uncompressedTarData = readFileSync(appImageTarPath);
|
|
2101
|
+
const compressedTarPath = `${appImageTarPath}.zst`;
|
|
2101
2102
|
await ZstdInit().then(async ({ ZstdSimple }) => {
|
|
2102
2103
|
const data = new Uint8Array(uncompressedTarData);
|
|
2103
2104
|
const compressionLevel = 22;
|
|
2104
2105
|
const compressedData = ZstdSimple.compress(data, compressionLevel);
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
console.log(`✓ Created compressed tar: ${compressedPath} (${(compressedData.length / 1024 / 1024).toFixed(2)} MB)`);
|
|
2106
|
+
writeFileSync(compressedTarPath, compressedData);
|
|
2107
|
+
console.log(`✓ Created compressed tar: ${compressedTarPath} (${(compressedData.length / 1024 / 1024).toFixed(2)} MB)`);
|
|
2108
2108
|
});
|
|
2109
|
-
|
|
2109
|
+
|
|
2110
2110
|
// Remove uncompressed tar
|
|
2111
2111
|
unlinkSync(appImageTarPath);
|
|
2112
|
-
|
|
2113
|
-
// Add
|
|
2114
|
-
|
|
2112
|
+
|
|
2113
|
+
// Add tar.zst for Updater API (delta updates)
|
|
2114
|
+
// Note: raw AppImage is NOT added - only the Setup.AppImage (zipped) is distributed
|
|
2115
|
+
artifactsToUpload.push(compressedTarPath);
|
|
2115
2116
|
}
|
|
2116
2117
|
}
|
|
2117
2118
|
|
|
@@ -2338,8 +2339,10 @@ if (commandArg === "init") {
|
|
|
2338
2339
|
buildEnvironment,
|
|
2339
2340
|
hash
|
|
2340
2341
|
);
|
|
2341
|
-
|
|
2342
|
-
|
|
2342
|
+
|
|
2343
|
+
// Wrap the Setup.AppImage in a tar.gz to preserve executable permissions
|
|
2344
|
+
const archivedAppImagePath = await wrapInArchive(selfExtractingAppImagePath, buildFolder, 'tar.gz');
|
|
2345
|
+
artifactsToUpload.push(archivedAppImagePath);
|
|
2343
2346
|
}
|
|
2344
2347
|
}
|
|
2345
2348
|
|
|
@@ -2918,11 +2921,14 @@ async function wrapInArchive(filePath: string, buildFolder: string, archiveType:
|
|
|
2918
2921
|
const fileDir = dirname(filePath);
|
|
2919
2922
|
|
|
2920
2923
|
if (archiveType === 'tar.gz') {
|
|
2921
|
-
// Output filename: Setup.exe -> Setup.exe.tar.gz
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
+
// Output filename: Setup.exe -> Setup.exe.tar.gz, Setup.AppImage -> Setup.tar.gz
|
|
2925
|
+
// For AppImage files, strip the .AppImage extension so archive extracts to .AppImage
|
|
2926
|
+
const archivePath = fileName.endsWith('.AppImage')
|
|
2927
|
+
? filePath.replace(/\.AppImage$/, '.tar.gz')
|
|
2928
|
+
: filePath + '.tar.gz';
|
|
2929
|
+
|
|
2924
2930
|
// For Linux files, ensure they have executable permissions before archiving
|
|
2925
|
-
if (fileName.endsWith('.run')) {
|
|
2931
|
+
if (fileName.endsWith('.run') || fileName.endsWith('.AppImage')) {
|
|
2926
2932
|
try {
|
|
2927
2933
|
// Try to set executable permissions (will only work on Unix-like systems)
|
|
2928
2934
|
execSync(`chmod +x ${escapePathForTerminal(filePath)}`, { stdio: 'ignore' });
|