electrobun 0.13.0-beta.21 → 0.13.0-beta.23
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 +134 -107
- package/package.json +1 -1
- package/src/cli/index.ts +19 -13
|
@@ -7,6 +7,29 @@ import { ZstdInit } from "@oneidentity/zstd-js/wasm";
|
|
|
7
7
|
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
|
+
import { quit } from './Utils';
|
|
11
|
+
|
|
12
|
+
// Helper to join URL paths without breaking the protocol (path.join mangles https:// to https:/)
|
|
13
|
+
function urlJoin(...parts: string[]): string {
|
|
14
|
+
if (parts.length === 0) return '';
|
|
15
|
+
|
|
16
|
+
// Start with the first part (base URL)
|
|
17
|
+
let result = parts[0];
|
|
18
|
+
|
|
19
|
+
// Join remaining parts
|
|
20
|
+
for (let i = 1; i < parts.length; i++) {
|
|
21
|
+
const part = parts[i];
|
|
22
|
+
if (!part) continue;
|
|
23
|
+
|
|
24
|
+
// Remove trailing slash from result and leading slash from part
|
|
25
|
+
const cleanResult = result.endsWith('/') ? result.slice(0, -1) : result;
|
|
26
|
+
const cleanPart = part.startsWith('/') ? part.slice(1) : part;
|
|
27
|
+
|
|
28
|
+
result = `${cleanResult}/${cleanPart}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
10
33
|
|
|
11
34
|
// setTimeout(async () => {
|
|
12
35
|
// console.log('killing')
|
|
@@ -144,7 +167,7 @@ const Updater = {
|
|
|
144
167
|
const channelBucketUrl = await Updater.channelBucketUrl();
|
|
145
168
|
const cacheBuster = Math.random().toString(36).substring(7);
|
|
146
169
|
const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
|
|
147
|
-
const updateInfoUrl =
|
|
170
|
+
const updateInfoUrl = urlJoin(localInfo.bucketUrl, platformFolder, `update.json?${cacheBuster}`);
|
|
148
171
|
|
|
149
172
|
try {
|
|
150
173
|
const updateInfoResponse = await fetch(updateInfoUrl);
|
|
@@ -213,7 +236,7 @@ const Updater = {
|
|
|
213
236
|
// check if there's a patch file for it
|
|
214
237
|
const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
|
|
215
238
|
const patchResponse = await fetch(
|
|
216
|
-
|
|
239
|
+
urlJoin(localInfo.bucketUrl, platformFolder, `${currentHash}.patch`)
|
|
217
240
|
);
|
|
218
241
|
|
|
219
242
|
if (!patchResponse.ok) {
|
|
@@ -332,7 +355,7 @@ const Updater = {
|
|
|
332
355
|
const cacheBuster = Math.random().toString(36).substring(7);
|
|
333
356
|
const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
|
|
334
357
|
const tarballName = getTarballFileName(appFileName, currentOS);
|
|
335
|
-
const urlToLatestTarball =
|
|
358
|
+
const urlToLatestTarball = urlJoin(
|
|
336
359
|
localInfo.bucketUrl,
|
|
337
360
|
platformFolder,
|
|
338
361
|
tarballName
|
|
@@ -471,10 +494,12 @@ const Updater = {
|
|
|
471
494
|
// Platform-specific path handling
|
|
472
495
|
let newAppBundlePath: string;
|
|
473
496
|
if (currentOS === 'linux') {
|
|
474
|
-
// On Linux,
|
|
497
|
+
// On Linux, the tarball contains a directory named {appFileName}, and inside it is the AppImage
|
|
498
|
+
// Structure: extractionDir/{appFileName}/{appFileName}.AppImage
|
|
499
|
+
const innerDirName = localInfo.name.replace(/ /g, "");
|
|
475
500
|
const appImageName = `${localInfo.name.replace(/ /g, "").replace(/\./g, "-")}.AppImage`;
|
|
476
|
-
newAppBundlePath = join(extractionDir, appImageName);
|
|
477
|
-
|
|
501
|
+
newAppBundlePath = join(extractionDir, innerDirName, appImageName);
|
|
502
|
+
|
|
478
503
|
// Verify the AppImage exists
|
|
479
504
|
if (!statSync(newAppBundlePath, { throwIfNoEntry: false })) {
|
|
480
505
|
console.error(`AppImage not found at: ${newAppBundlePath}`);
|
|
@@ -483,6 +508,14 @@ const Updater = {
|
|
|
483
508
|
const files = readdirSync(extractionDir);
|
|
484
509
|
for (const file of files) {
|
|
485
510
|
console.log(` - ${file}`);
|
|
511
|
+
// Also list contents of subdirectories
|
|
512
|
+
const subPath = join(extractionDir, file);
|
|
513
|
+
if (statSync(subPath).isDirectory()) {
|
|
514
|
+
const subFiles = readdirSync(subPath);
|
|
515
|
+
for (const subFile of subFiles) {
|
|
516
|
+
console.log(` - ${subFile}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
486
519
|
}
|
|
487
520
|
} catch (e) {
|
|
488
521
|
console.log("Could not list directory contents:", e);
|
|
@@ -530,9 +563,8 @@ const Updater = {
|
|
|
530
563
|
const appImageName = `${localInfo.name.replace(/ /g, "").replace(/\./g, "-")}.AppImage`;
|
|
531
564
|
runningAppBundlePath = join(appDataFolder, appImageName);
|
|
532
565
|
} else {
|
|
533
|
-
// On Windows, use
|
|
534
|
-
|
|
535
|
-
runningAppBundlePath = join(appDataFolder, `app-${currentHash}`);
|
|
566
|
+
// On Windows, use fixed 'app' folder to match extractor
|
|
567
|
+
runningAppBundlePath = join(appDataFolder, 'app');
|
|
536
568
|
}
|
|
537
569
|
}
|
|
538
570
|
// Platform-specific backup handling
|
|
@@ -588,113 +620,108 @@ const Updater = {
|
|
|
588
620
|
// Create/update launcher script that points to the AppImage
|
|
589
621
|
await createLinuxAppImageLauncherScript(runningAppBundlePath);
|
|
590
622
|
} else {
|
|
591
|
-
// On Windows, use
|
|
623
|
+
// On Windows, files are locked while in use, so we need a helper script
|
|
624
|
+
// that runs after the app exits to do the replacement
|
|
592
625
|
const parentDir = dirname(runningAppBundlePath);
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
626
|
+
const updateScriptPath = join(parentDir, 'update.bat');
|
|
627
|
+
const backupDir = join(parentDir, 'app-backup');
|
|
628
|
+
const launcherPath = join(runningAppBundlePath, 'bin', 'launcher.exe');
|
|
629
|
+
|
|
630
|
+
// Convert paths to Windows format
|
|
631
|
+
const backupDirWin = backupDir.replace(/\//g, '\\');
|
|
632
|
+
const runningAppWin = runningAppBundlePath.replace(/\//g, '\\');
|
|
633
|
+
const newAppWin = newAppBundlePath.replace(/\//g, '\\');
|
|
634
|
+
const extractionDirWin = extractionDir.replace(/\//g, '\\');
|
|
635
|
+
const launcherPathWin = launcherPath.replace(/\//g, '\\');
|
|
636
|
+
|
|
637
|
+
// Create a batch script that will:
|
|
638
|
+
// 1. Wait for the current app to exit
|
|
639
|
+
// 2. Remove old backup
|
|
640
|
+
// 3. Move current app to backup
|
|
641
|
+
// 4. Move new app to current location
|
|
642
|
+
// 5. Launch the new app
|
|
643
|
+
// 6. Clean up
|
|
644
|
+
const updateScript = `@echo off
|
|
645
|
+
setlocal
|
|
646
|
+
|
|
647
|
+
:: Wait for the app to fully exit (check if launcher.exe is still running)
|
|
648
|
+
:waitloop
|
|
649
|
+
tasklist /FI "IMAGENAME eq launcher.exe" 2>NUL | find /I /N "launcher.exe">NUL
|
|
650
|
+
if "%ERRORLEVEL%"=="0" (
|
|
651
|
+
timeout /t 1 /nobreak >nul
|
|
652
|
+
goto waitloop
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
:: Small extra delay to ensure all file handles are released
|
|
656
|
+
timeout /t 2 /nobreak >nul
|
|
657
|
+
|
|
658
|
+
:: Remove old backup if exists
|
|
659
|
+
if exist "${backupDirWin}" (
|
|
660
|
+
rmdir /s /q "${backupDirWin}"
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
:: Backup current app folder
|
|
664
|
+
if exist "${runningAppWin}" (
|
|
665
|
+
move "${runningAppWin}" "${backupDirWin}"
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
:: Move new app to current location
|
|
669
|
+
move "${newAppWin}" "${runningAppWin}"
|
|
670
|
+
|
|
671
|
+
:: Clean up extraction directory
|
|
672
|
+
rmdir /s /q "${extractionDirWin}" 2>nul
|
|
673
|
+
|
|
674
|
+
:: Launch the new app
|
|
675
|
+
start "" "${launcherPathWin}"
|
|
676
|
+
|
|
677
|
+
:: Clean up scheduled tasks starting with ElectrobunUpdate_
|
|
678
|
+
for /f "tokens=1" %%t in ('schtasks /query /fo list ^| findstr /i "ElectrobunUpdate_"') do (
|
|
679
|
+
schtasks /delete /tn "%%t" /f >nul 2>&1
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
:: Delete this update script after a short delay
|
|
683
|
+
ping -n 2 127.0.0.1 >nul
|
|
684
|
+
del "%~f0"
|
|
642
685
|
`;
|
|
643
|
-
|
|
644
|
-
await Bun.write(
|
|
645
|
-
|
|
646
|
-
//
|
|
647
|
-
// This
|
|
648
|
-
|
|
649
|
-
|
|
686
|
+
|
|
687
|
+
await Bun.write(updateScriptPath, updateScript);
|
|
688
|
+
|
|
689
|
+
// Use Windows Task Scheduler to run the update script independently
|
|
690
|
+
// This ensures the script runs even after the app exits
|
|
691
|
+
const scriptPathWin = updateScriptPath.replace(/\//g, '\\');
|
|
692
|
+
const taskName = `ElectrobunUpdate_${Date.now()}`;
|
|
693
|
+
|
|
694
|
+
// Create a scheduled task that runs immediately and deletes itself
|
|
695
|
+
execSync(`schtasks /create /tn "${taskName}" /tr "cmd /c \\"${scriptPathWin}\\"" /sc once /st 00:00 /f`, { stdio: 'ignore' });
|
|
696
|
+
execSync(`schtasks /run /tn "${taskName}"`, { stdio: 'ignore' });
|
|
697
|
+
// The task will be cleaned up by Windows after it runs, or we delete it in the batch script
|
|
698
|
+
|
|
699
|
+
// Use quit() for graceful shutdown - this closes all windows and processes
|
|
700
|
+
quit();
|
|
650
701
|
}
|
|
651
702
|
} catch (error) {
|
|
652
703
|
console.error("Failed to replace app with new version", error);
|
|
653
704
|
return;
|
|
654
705
|
}
|
|
655
706
|
|
|
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));
|
|
707
|
+
// Cross-platform app launch (Windows is handled above with its own update script)
|
|
708
|
+
if (currentOS === 'macos') {
|
|
709
|
+
// Use a detached shell so relaunch survives after killApp terminates the current process
|
|
710
|
+
await Bun.spawn(
|
|
711
|
+
[
|
|
712
|
+
"sh",
|
|
713
|
+
"-c",
|
|
714
|
+
`open "${runningAppBundlePath}" &`,
|
|
715
|
+
],
|
|
716
|
+
{ detached: true }
|
|
717
|
+
);
|
|
718
|
+
} else if (currentOS === 'linux') {
|
|
719
|
+
// On Linux, launch the AppImage directly
|
|
720
|
+
Bun.spawn(["sh", "-c", `"${runningAppBundlePath}" &`], { detached: true});
|
|
686
721
|
}
|
|
687
722
|
|
|
688
|
-
// Use
|
|
689
|
-
|
|
690
|
-
native.symbols.killApp();
|
|
691
|
-
// Still call process.exit as a fallback
|
|
692
|
-
process.exit(0);
|
|
693
|
-
} catch (e) {
|
|
694
|
-
// Fallback if native binding fails
|
|
695
|
-
console.error('Failed to call native killApp:', e);
|
|
696
|
-
process.exit(0);
|
|
697
|
-
}
|
|
723
|
+
// Use quit() for graceful shutdown
|
|
724
|
+
quit();
|
|
698
725
|
}
|
|
699
726
|
}
|
|
700
727
|
},
|
|
@@ -702,7 +729,7 @@ start "" launcher.exe
|
|
|
702
729
|
channelBucketUrl: async () => {
|
|
703
730
|
await Updater.getLocallocalInfo();
|
|
704
731
|
const platformFolder = getPlatformFolder(localInfo.channel, currentOS, currentArch);
|
|
705
|
-
return
|
|
732
|
+
return urlJoin(localInfo.bucketUrl, platformFolder);
|
|
706
733
|
},
|
|
707
734
|
|
|
708
735
|
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' });
|