electrobun 0.13.0-beta.8 → 1.0.2-beta.0
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/ElectrobunConfig.ts +2 -2
- package/dist/api/bun/core/Updater.ts +176 -118
- package/dist/api/shared/naming.test.ts +47 -46
- package/dist/api/shared/naming.ts +35 -26
- package/package.json +2 -1
- package/src/cli/index.ts +96 -126
|
@@ -228,10 +228,10 @@ export interface ElectrobunConfig {
|
|
|
228
228
|
*/
|
|
229
229
|
release?: {
|
|
230
230
|
/**
|
|
231
|
-
* Base URL for artifact distribution (e.g., S3 bucket
|
|
231
|
+
* Base URL for artifact distribution (e.g., S3 bucket, GitHub Releases)
|
|
232
232
|
* Used for auto-updates and patch generation
|
|
233
233
|
*/
|
|
234
|
-
|
|
234
|
+
baseUrl?: string;
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -5,8 +5,31 @@ import { execSync } from "child_process";
|
|
|
5
5
|
import tar from "tar";
|
|
6
6
|
import { ZstdInit } from "@oneidentity/zstd-js/wasm";
|
|
7
7
|
import { OS as currentOS, ARCH as currentArch } from '../../shared/platform';
|
|
8
|
-
import {
|
|
8
|
+
import { getPlatformPrefix, 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')
|
|
@@ -107,7 +130,7 @@ function getAppDataDir(): string {
|
|
|
107
130
|
let localInfo: {
|
|
108
131
|
version: string;
|
|
109
132
|
hash: string;
|
|
110
|
-
|
|
133
|
+
baseUrl: string;
|
|
111
134
|
channel: string;
|
|
112
135
|
name: string;
|
|
113
136
|
identifier: string;
|
|
@@ -141,17 +164,36 @@ const Updater = {
|
|
|
141
164
|
};
|
|
142
165
|
}
|
|
143
166
|
|
|
144
|
-
const channelBucketUrl = await Updater.channelBucketUrl();
|
|
145
167
|
const cacheBuster = Math.random().toString(36).substring(7);
|
|
146
|
-
const
|
|
147
|
-
const updateInfoUrl =
|
|
168
|
+
const platformPrefix = getPlatformPrefix(localInfo.channel, currentOS, currentArch);
|
|
169
|
+
const updateInfoUrl = `${localInfo.baseUrl}/${platformPrefix}-update.json?${cacheBuster}`;
|
|
148
170
|
|
|
149
171
|
try {
|
|
150
172
|
const updateInfoResponse = await fetch(updateInfoUrl);
|
|
151
173
|
|
|
152
174
|
if (updateInfoResponse.ok) {
|
|
153
|
-
|
|
154
|
-
|
|
175
|
+
const responseText = await updateInfoResponse.text();
|
|
176
|
+
try {
|
|
177
|
+
updateInfo = JSON.parse(responseText);
|
|
178
|
+
} catch {
|
|
179
|
+
return {
|
|
180
|
+
version: "",
|
|
181
|
+
hash: "",
|
|
182
|
+
updateAvailable: false,
|
|
183
|
+
updateReady: false,
|
|
184
|
+
error: `Invalid update.json: failed to parse JSON`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!updateInfo.hash) {
|
|
189
|
+
return {
|
|
190
|
+
version: "",
|
|
191
|
+
hash: "",
|
|
192
|
+
updateAvailable: false,
|
|
193
|
+
updateReady: false,
|
|
194
|
+
error: `Invalid update.json: missing hash`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
155
197
|
|
|
156
198
|
if (updateInfo.hash !== localInfo.hash) {
|
|
157
199
|
updateInfo.updateAvailable = true;
|
|
@@ -211,9 +253,9 @@ const Updater = {
|
|
|
211
253
|
}
|
|
212
254
|
|
|
213
255
|
// check if there's a patch file for it
|
|
214
|
-
const
|
|
256
|
+
const platformPrefix = getPlatformPrefix(localInfo.channel, currentOS, currentArch);
|
|
215
257
|
const patchResponse = await fetch(
|
|
216
|
-
|
|
258
|
+
`${localInfo.baseUrl}/${platformPrefix}-${currentHash}.patch`
|
|
217
259
|
);
|
|
218
260
|
|
|
219
261
|
if (!patchResponse.ok) {
|
|
@@ -330,13 +372,9 @@ const Updater = {
|
|
|
330
372
|
// then just download it and unpack it
|
|
331
373
|
if (currentHash !== latestHash) {
|
|
332
374
|
const cacheBuster = Math.random().toString(36).substring(7);
|
|
333
|
-
const
|
|
375
|
+
const platformPrefix = getPlatformPrefix(localInfo.channel, currentOS, currentArch);
|
|
334
376
|
const tarballName = getTarballFileName(appFileName, currentOS);
|
|
335
|
-
const urlToLatestTarball =
|
|
336
|
-
localInfo.bucketUrl,
|
|
337
|
-
platformFolder,
|
|
338
|
-
tarballName
|
|
339
|
-
);
|
|
377
|
+
const urlToLatestTarball = `${localInfo.baseUrl}/${platformPrefix}-${tarballName}`;
|
|
340
378
|
const prevVersionCompressedTarballPath = join(
|
|
341
379
|
appDataFolder,
|
|
342
380
|
"self-extraction",
|
|
@@ -471,10 +509,12 @@ const Updater = {
|
|
|
471
509
|
// Platform-specific path handling
|
|
472
510
|
let newAppBundlePath: string;
|
|
473
511
|
if (currentOS === 'linux') {
|
|
474
|
-
// On Linux,
|
|
512
|
+
// On Linux, the tarball contains a directory named {appFileName}, and inside it is the AppImage
|
|
513
|
+
// Structure: extractionDir/{appFileName}/{appFileName}.AppImage
|
|
514
|
+
const innerDirName = localInfo.name.replace(/ /g, "");
|
|
475
515
|
const appImageName = `${localInfo.name.replace(/ /g, "").replace(/\./g, "-")}.AppImage`;
|
|
476
|
-
newAppBundlePath = join(extractionDir, appImageName);
|
|
477
|
-
|
|
516
|
+
newAppBundlePath = join(extractionDir, innerDirName, appImageName);
|
|
517
|
+
|
|
478
518
|
// Verify the AppImage exists
|
|
479
519
|
if (!statSync(newAppBundlePath, { throwIfNoEntry: false })) {
|
|
480
520
|
console.error(`AppImage not found at: ${newAppBundlePath}`);
|
|
@@ -483,6 +523,14 @@ const Updater = {
|
|
|
483
523
|
const files = readdirSync(extractionDir);
|
|
484
524
|
for (const file of files) {
|
|
485
525
|
console.log(` - ${file}`);
|
|
526
|
+
// Also list contents of subdirectories
|
|
527
|
+
const subPath = join(extractionDir, file);
|
|
528
|
+
if (statSync(subPath).isDirectory()) {
|
|
529
|
+
const subFiles = readdirSync(subPath);
|
|
530
|
+
for (const subFile of subFiles) {
|
|
531
|
+
console.log(` - ${subFile}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
486
534
|
}
|
|
487
535
|
} catch (e) {
|
|
488
536
|
console.log("Could not list directory contents:", e);
|
|
@@ -530,9 +578,8 @@ const Updater = {
|
|
|
530
578
|
const appImageName = `${localInfo.name.replace(/ /g, "").replace(/\./g, "-")}.AppImage`;
|
|
531
579
|
runningAppBundlePath = join(appDataFolder, appImageName);
|
|
532
580
|
} else {
|
|
533
|
-
// On Windows, use
|
|
534
|
-
|
|
535
|
-
runningAppBundlePath = join(appDataFolder, `app-${currentHash}`);
|
|
581
|
+
// On Windows, use fixed 'app' folder to match extractor
|
|
582
|
+
runningAppBundlePath = join(appDataFolder, 'app');
|
|
536
583
|
}
|
|
537
584
|
}
|
|
538
585
|
// Platform-specific backup handling
|
|
@@ -555,9 +602,18 @@ const Updater = {
|
|
|
555
602
|
|
|
556
603
|
// Move current running app to backup
|
|
557
604
|
renameSync(runningAppBundlePath, backupPath);
|
|
558
|
-
|
|
605
|
+
|
|
559
606
|
// Move new app to running location
|
|
560
607
|
renameSync(newAppBundlePath, runningAppBundlePath);
|
|
608
|
+
|
|
609
|
+
// Remove quarantine extended attributes to prevent "damaged" error
|
|
610
|
+
// The inner bundle is already signed/notarized, but macOS applies
|
|
611
|
+
// quarantine attributes when extracting from a downloaded archive
|
|
612
|
+
try {
|
|
613
|
+
execSync(`xattr -r -d com.apple.quarantine "${runningAppBundlePath}"`, { stdio: 'ignore' });
|
|
614
|
+
} catch (e) {
|
|
615
|
+
// Ignore errors - attribute may not exist
|
|
616
|
+
}
|
|
561
617
|
} else if (currentOS === 'linux') {
|
|
562
618
|
// On Linux, backup and replace AppImage file
|
|
563
619
|
// Remove existing backup if it exists
|
|
@@ -579,115 +635,117 @@ const Updater = {
|
|
|
579
635
|
// Create/update launcher script that points to the AppImage
|
|
580
636
|
await createLinuxAppImageLauncherScript(runningAppBundlePath);
|
|
581
637
|
} else {
|
|
582
|
-
// On Windows, use
|
|
638
|
+
// On Windows, files are locked while in use, so we need a helper script
|
|
639
|
+
// that runs after the app exits to do the replacement
|
|
583
640
|
const parentDir = dirname(runningAppBundlePath);
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
641
|
+
const updateScriptPath = join(parentDir, 'update.bat');
|
|
642
|
+
const backupDir = join(parentDir, 'app-backup');
|
|
643
|
+
const launcherPath = join(runningAppBundlePath, 'bin', 'launcher.exe');
|
|
644
|
+
|
|
645
|
+
// Convert paths to Windows format
|
|
646
|
+
const backupDirWin = backupDir.replace(/\//g, '\\');
|
|
647
|
+
const runningAppWin = runningAppBundlePath.replace(/\//g, '\\');
|
|
648
|
+
const newAppWin = newAppBundlePath.replace(/\//g, '\\');
|
|
649
|
+
const extractionDirWin = extractionDir.replace(/\//g, '\\');
|
|
650
|
+
const launcherPathWin = launcherPath.replace(/\//g, '\\');
|
|
651
|
+
|
|
652
|
+
// Create a batch script that will:
|
|
653
|
+
// 1. Wait for the current app to exit
|
|
654
|
+
// 2. Remove old backup
|
|
655
|
+
// 3. Move current app to backup
|
|
656
|
+
// 4. Move new app to current location
|
|
657
|
+
// 5. Launch the new app
|
|
658
|
+
// 6. Clean up
|
|
659
|
+
const updateScript = `@echo off
|
|
660
|
+
setlocal
|
|
661
|
+
|
|
662
|
+
:: Wait for the app to fully exit (check if launcher.exe is still running)
|
|
663
|
+
:waitloop
|
|
664
|
+
tasklist /FI "IMAGENAME eq launcher.exe" 2>NUL | find /I /N "launcher.exe">NUL
|
|
665
|
+
if "%ERRORLEVEL%"=="0" (
|
|
666
|
+
timeout /t 1 /nobreak >nul
|
|
667
|
+
goto waitloop
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
:: Small extra delay to ensure all file handles are released
|
|
671
|
+
timeout /t 2 /nobreak >nul
|
|
672
|
+
|
|
673
|
+
:: Remove old backup if exists
|
|
674
|
+
if exist "${backupDirWin}" (
|
|
675
|
+
rmdir /s /q "${backupDirWin}"
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
:: Backup current app folder
|
|
679
|
+
if exist "${runningAppWin}" (
|
|
680
|
+
move "${runningAppWin}" "${backupDirWin}"
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
:: Move new app to current location
|
|
684
|
+
move "${newAppWin}" "${runningAppWin}"
|
|
685
|
+
|
|
686
|
+
:: Clean up extraction directory
|
|
687
|
+
rmdir /s /q "${extractionDirWin}" 2>nul
|
|
688
|
+
|
|
689
|
+
:: Launch the new app
|
|
690
|
+
start "" "${launcherPathWin}"
|
|
691
|
+
|
|
692
|
+
:: Clean up scheduled tasks starting with ElectrobunUpdate_
|
|
693
|
+
for /f "tokens=1" %%t in ('schtasks /query /fo list ^| findstr /i "ElectrobunUpdate_"') do (
|
|
694
|
+
schtasks /delete /tn "%%t" /f >nul 2>&1
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
:: Delete this update script after a short delay
|
|
698
|
+
ping -n 2 127.0.0.1 >nul
|
|
699
|
+
del "%~f0"
|
|
633
700
|
`;
|
|
634
|
-
|
|
635
|
-
await Bun.write(
|
|
636
|
-
|
|
637
|
-
//
|
|
638
|
-
// This
|
|
639
|
-
|
|
640
|
-
|
|
701
|
+
|
|
702
|
+
await Bun.write(updateScriptPath, updateScript);
|
|
703
|
+
|
|
704
|
+
// Use Windows Task Scheduler to run the update script independently
|
|
705
|
+
// This ensures the script runs even after the app exits
|
|
706
|
+
const scriptPathWin = updateScriptPath.replace(/\//g, '\\');
|
|
707
|
+
const taskName = `ElectrobunUpdate_${Date.now()}`;
|
|
708
|
+
|
|
709
|
+
// Create a scheduled task that runs immediately and deletes itself
|
|
710
|
+
execSync(`schtasks /create /tn "${taskName}" /tr "cmd /c \\"${scriptPathWin}\\"" /sc once /st 00:00 /f`, { stdio: 'ignore' });
|
|
711
|
+
execSync(`schtasks /run /tn "${taskName}"`, { stdio: 'ignore' });
|
|
712
|
+
// The task will be cleaned up by Windows after it runs, or we delete it in the batch script
|
|
713
|
+
|
|
714
|
+
// Use quit() for graceful shutdown - this closes all windows and processes
|
|
715
|
+
quit();
|
|
641
716
|
}
|
|
642
717
|
} catch (error) {
|
|
643
718
|
console.error("Failed to replace app with new version", error);
|
|
644
719
|
return;
|
|
645
720
|
}
|
|
646
721
|
|
|
647
|
-
// Cross-platform app launch
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
// On Windows, launch the run.bat file which handles versioning
|
|
662
|
-
const parentDir = dirname(runningAppBundlePath);
|
|
663
|
-
const runBatPath = join(parentDir, "run.bat");
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
await Bun.spawn(["cmd", "/c", runBatPath], { detached: true });
|
|
667
|
-
break;
|
|
668
|
-
case 'linux':
|
|
669
|
-
// On Linux, launch the AppImage directly
|
|
670
|
-
Bun.spawn(["sh", "-c", `"${runningAppBundlePath}" &`], { detached: true});
|
|
671
|
-
break;
|
|
722
|
+
// Cross-platform app launch (Windows is handled above with its own update script)
|
|
723
|
+
if (currentOS === 'macos') {
|
|
724
|
+
// Use a detached shell so relaunch survives after killApp terminates the current process
|
|
725
|
+
await Bun.spawn(
|
|
726
|
+
[
|
|
727
|
+
"sh",
|
|
728
|
+
"-c",
|
|
729
|
+
`open "${runningAppBundlePath}" &`,
|
|
730
|
+
],
|
|
731
|
+
{ detached: true }
|
|
732
|
+
);
|
|
733
|
+
} else if (currentOS === 'linux') {
|
|
734
|
+
// On Linux, launch the AppImage directly
|
|
735
|
+
Bun.spawn(["sh", "-c", `"${runningAppBundlePath}" &`], { detached: true});
|
|
672
736
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
// Still call process.exit as a fallback
|
|
677
|
-
process.exit(0);
|
|
678
|
-
} catch (e) {
|
|
679
|
-
// Fallback if native binding fails
|
|
680
|
-
console.error('Failed to call native killApp:', e);
|
|
681
|
-
process.exit(0);
|
|
682
|
-
}
|
|
737
|
+
|
|
738
|
+
// Use quit() for graceful shutdown
|
|
739
|
+
quit();
|
|
683
740
|
}
|
|
684
741
|
}
|
|
685
742
|
},
|
|
686
743
|
|
|
687
744
|
channelBucketUrl: async () => {
|
|
688
745
|
await Updater.getLocallocalInfo();
|
|
689
|
-
|
|
690
|
-
|
|
746
|
+
// With flat prefix-based naming, channelBucketUrl is just the baseUrl
|
|
747
|
+
// Users can also use Updater.localInfo.baseUrl() directly
|
|
748
|
+
return localInfo.baseUrl;
|
|
691
749
|
},
|
|
692
750
|
|
|
693
751
|
appDataFolder: async () => {
|
|
@@ -712,8 +770,8 @@ start "" launcher.exe
|
|
|
712
770
|
channel: async () => {
|
|
713
771
|
return (await Updater.getLocallocalInfo()).channel;
|
|
714
772
|
},
|
|
715
|
-
|
|
716
|
-
return (await Updater.getLocallocalInfo()).
|
|
773
|
+
baseUrl: async () => {
|
|
774
|
+
return (await Updater.getLocallocalInfo()).baseUrl;
|
|
717
775
|
},
|
|
718
776
|
},
|
|
719
777
|
|
|
@@ -3,10 +3,9 @@ import {
|
|
|
3
3
|
sanitizeAppName,
|
|
4
4
|
getAppFileName,
|
|
5
5
|
getBundleFileName,
|
|
6
|
-
|
|
6
|
+
getPlatformPrefix,
|
|
7
7
|
getTarballFileName,
|
|
8
8
|
getWindowsSetupFileName,
|
|
9
|
-
getLinuxSetupFileName,
|
|
10
9
|
getLinuxAppImageBaseName,
|
|
11
10
|
getLinuxAppImageFileName,
|
|
12
11
|
sanitizeVolumeNameForHdiutil,
|
|
@@ -82,13 +81,13 @@ describe('getBundleFileName', () => {
|
|
|
82
81
|
});
|
|
83
82
|
});
|
|
84
83
|
|
|
85
|
-
describe('
|
|
86
|
-
it('constructs correct
|
|
87
|
-
expect(
|
|
88
|
-
expect(
|
|
89
|
-
expect(
|
|
90
|
-
expect(
|
|
91
|
-
expect(
|
|
84
|
+
describe('getPlatformPrefix', () => {
|
|
85
|
+
it('constructs correct prefix format for all platform combinations', () => {
|
|
86
|
+
expect(getPlatformPrefix('stable', 'macos', 'arm64')).toBe('stable-macos-arm64');
|
|
87
|
+
expect(getPlatformPrefix('stable', 'macos', 'x64')).toBe('stable-macos-x64');
|
|
88
|
+
expect(getPlatformPrefix('canary', 'win', 'x64')).toBe('canary-win-x64');
|
|
89
|
+
expect(getPlatformPrefix('dev', 'linux', 'arm64')).toBe('dev-linux-arm64');
|
|
90
|
+
expect(getPlatformPrefix('dev', 'linux', 'x64')).toBe('dev-linux-x64');
|
|
92
91
|
});
|
|
93
92
|
});
|
|
94
93
|
|
|
@@ -136,19 +135,10 @@ describe('getWindowsSetupFileName', () => {
|
|
|
136
135
|
it('includes channel suffix for dev builds', () => {
|
|
137
136
|
expect(getWindowsSetupFileName('MyApp', 'dev')).toBe('MyApp-Setup-dev.exe');
|
|
138
137
|
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe('getLinuxSetupFileName', () => {
|
|
142
|
-
it('returns AppName-Setup.run for stable builds', () => {
|
|
143
|
-
expect(getLinuxSetupFileName('MyApp', 'stable')).toBe('MyApp-Setup.run');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('includes channel suffix for canary builds', () => {
|
|
147
|
-
expect(getLinuxSetupFileName('MyApp', 'canary')).toBe('MyApp-Setup-canary.run');
|
|
148
|
-
});
|
|
149
138
|
|
|
150
|
-
it('
|
|
151
|
-
expect(
|
|
139
|
+
it('preserves spaces in app name', () => {
|
|
140
|
+
expect(getWindowsSetupFileName('My App', 'stable')).toBe('My App-Setup.exe');
|
|
141
|
+
expect(getWindowsSetupFileName('My App', 'canary')).toBe('My App-Setup-canary.exe');
|
|
152
142
|
});
|
|
153
143
|
});
|
|
154
144
|
|
|
@@ -160,6 +150,11 @@ describe('getLinuxAppImageBaseName', () => {
|
|
|
160
150
|
it('includes channel suffix for canary builds', () => {
|
|
161
151
|
expect(getLinuxAppImageBaseName('MyApp', 'canary')).toBe('MyApp-Setup-canary');
|
|
162
152
|
});
|
|
153
|
+
|
|
154
|
+
it('preserves spaces in app name', () => {
|
|
155
|
+
expect(getLinuxAppImageBaseName('My App', 'stable')).toBe('My App-Setup');
|
|
156
|
+
expect(getLinuxAppImageBaseName('My App', 'canary')).toBe('My App-Setup-canary');
|
|
157
|
+
});
|
|
163
158
|
});
|
|
164
159
|
|
|
165
160
|
describe('getLinuxAppImageFileName', () => {
|
|
@@ -170,6 +165,11 @@ describe('getLinuxAppImageFileName', () => {
|
|
|
170
165
|
it('returns full filename with .AppImage extension for canary', () => {
|
|
171
166
|
expect(getLinuxAppImageFileName('MyApp', 'canary')).toBe('MyApp-Setup-canary.AppImage');
|
|
172
167
|
});
|
|
168
|
+
|
|
169
|
+
it('preserves spaces in app name', () => {
|
|
170
|
+
expect(getLinuxAppImageFileName('My App', 'stable')).toBe('My App-Setup.AppImage');
|
|
171
|
+
expect(getLinuxAppImageFileName('My App', 'canary')).toBe('My App-Setup-canary.AppImage');
|
|
172
|
+
});
|
|
173
173
|
});
|
|
174
174
|
|
|
175
175
|
describe('sanitizeVolumeNameForHdiutil', () => {
|
|
@@ -195,63 +195,64 @@ describe('sanitizeVolumeNameForHdiutil', () => {
|
|
|
195
195
|
});
|
|
196
196
|
|
|
197
197
|
describe('getDmgVolumeName', () => {
|
|
198
|
-
it('
|
|
199
|
-
expect(getDmgVolumeName('MyApp', 'stable')).toBe('MyApp
|
|
198
|
+
it('returns plain name for stable builds', () => {
|
|
199
|
+
expect(getDmgVolumeName('MyApp', 'stable')).toBe('MyApp');
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
it('
|
|
203
|
-
expect(getDmgVolumeName('MyApp
|
|
202
|
+
it('adds channel suffix for non-stable builds', () => {
|
|
203
|
+
expect(getDmgVolumeName('MyApp', 'canary')).toBe('MyApp-canary');
|
|
204
204
|
});
|
|
205
205
|
|
|
206
206
|
it('sanitizes special characters', () => {
|
|
207
|
-
expect(getDmgVolumeName('My-App', 'stable')).toBe('MyApp
|
|
207
|
+
expect(getDmgVolumeName('My-App', 'stable')).toBe('MyApp');
|
|
208
|
+
expect(getDmgVolumeName('My-App', 'canary')).toBe('MyApp-canary');
|
|
208
209
|
});
|
|
209
210
|
});
|
|
210
211
|
|
|
211
212
|
describe('URL construction functions', () => {
|
|
212
|
-
const
|
|
213
|
-
const
|
|
213
|
+
const baseUrl = 'https://storage.example.com/releases';
|
|
214
|
+
const platformPrefix = 'canary-macos-arm64';
|
|
214
215
|
|
|
215
216
|
describe('getUpdateInfoUrl', () => {
|
|
216
|
-
it('constructs correct URL', () => {
|
|
217
|
-
expect(getUpdateInfoUrl(
|
|
218
|
-
.toBe('https://storage.example.com/releases/canary-macos-arm64
|
|
217
|
+
it('constructs correct flat URL with prefix', () => {
|
|
218
|
+
expect(getUpdateInfoUrl(baseUrl, platformPrefix))
|
|
219
|
+
.toBe('https://storage.example.com/releases/canary-macos-arm64-update.json');
|
|
219
220
|
});
|
|
220
221
|
|
|
221
222
|
it('handles bucket URLs with trailing content', () => {
|
|
222
223
|
expect(getUpdateInfoUrl('https://example.com/bucket', 'stable-win-x64'))
|
|
223
|
-
.toBe('https://example.com/bucket/stable-win-x64
|
|
224
|
+
.toBe('https://example.com/bucket/stable-win-x64-update.json');
|
|
224
225
|
});
|
|
225
226
|
});
|
|
226
227
|
|
|
227
228
|
describe('getPatchFileUrl', () => {
|
|
228
|
-
it('constructs correct URL with hash', () => {
|
|
229
|
-
expect(getPatchFileUrl(
|
|
230
|
-
.toBe('https://storage.example.com/releases/canary-macos-arm64
|
|
229
|
+
it('constructs correct flat URL with prefix and hash', () => {
|
|
230
|
+
expect(getPatchFileUrl(baseUrl, platformPrefix, 'abc123def456'))
|
|
231
|
+
.toBe('https://storage.example.com/releases/canary-macos-arm64-abc123def456.patch');
|
|
231
232
|
});
|
|
232
233
|
});
|
|
233
234
|
|
|
234
235
|
describe('getTarballUrl', () => {
|
|
235
|
-
it('constructs correct URL for macOS tarball', () => {
|
|
236
|
-
expect(getTarballUrl(
|
|
237
|
-
.toBe('https://storage.example.com/releases/canary-macos-arm64
|
|
236
|
+
it('constructs correct flat URL for macOS tarball', () => {
|
|
237
|
+
expect(getTarballUrl(baseUrl, platformPrefix, 'MyApp.app.tar.zst'))
|
|
238
|
+
.toBe('https://storage.example.com/releases/canary-macos-arm64-MyApp.app.tar.zst');
|
|
238
239
|
});
|
|
239
240
|
|
|
240
|
-
it('constructs correct URL for Windows tarball', () => {
|
|
241
|
-
expect(getTarballUrl(
|
|
242
|
-
.toBe('https://storage.example.com/releases/stable-win-x64
|
|
241
|
+
it('constructs correct flat URL for Windows tarball', () => {
|
|
242
|
+
expect(getTarballUrl(baseUrl, 'stable-win-x64', 'MyApp.tar.zst'))
|
|
243
|
+
.toBe('https://storage.example.com/releases/stable-win-x64-MyApp.tar.zst');
|
|
243
244
|
});
|
|
244
245
|
});
|
|
245
246
|
});
|
|
246
247
|
|
|
247
248
|
// Integration tests that verify CLI and Updater would produce matching values
|
|
248
249
|
describe('CLI and Updater consistency', () => {
|
|
249
|
-
it('produces matching platform
|
|
250
|
+
it('produces matching platform prefixes', () => {
|
|
250
251
|
// CLI uses: `${buildEnvironment}-${currentTarget.os}-${currentTarget.arch}`
|
|
251
252
|
// Updater uses: `${localInfo.channel}-${currentOS}-${currentArch}`
|
|
252
|
-
// Both should use
|
|
253
|
-
const cliResult =
|
|
254
|
-
const updaterResult =
|
|
253
|
+
// Both should use getPlatformPrefix()
|
|
254
|
+
const cliResult = getPlatformPrefix('canary', 'macos', 'arm64');
|
|
255
|
+
const updaterResult = getPlatformPrefix('canary', 'macos', 'arm64');
|
|
255
256
|
expect(cliResult).toBe(updaterResult);
|
|
256
257
|
expect(cliResult).toBe('canary-macos-arm64');
|
|
257
258
|
});
|
|
@@ -280,6 +281,6 @@ describe('CLI and Updater consistency', () => {
|
|
|
280
281
|
expect(getAppFileName('MyApp', 'stable')).not.toContain('-stable');
|
|
281
282
|
expect(getTarballFileName(getAppFileName('MyApp', 'stable'), 'macos')).toBe('MyApp.app.tar.zst');
|
|
282
283
|
expect(getWindowsSetupFileName('MyApp', 'stable')).toBe('MyApp-Setup.exe');
|
|
283
|
-
expect(
|
|
284
|
+
expect(getLinuxAppImageFileName('MyApp', 'stable')).toBe('MyApp-Setup.AppImage');
|
|
284
285
|
});
|
|
285
286
|
});
|
|
@@ -27,6 +27,19 @@ export function getAppFileName(appName: string, buildEnvironment: BuildEnvironme
|
|
|
27
27
|
return buildEnvironment === 'stable' ? sanitized : `${sanitized}-${buildEnvironment}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Generates the macOS bundle display name (with spaces preserved).
|
|
32
|
+
* Used for the actual .app folder name on macOS.
|
|
33
|
+
* Format: "App Name" (stable) or "App Name-channel" (non-stable)
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* getMacOSBundleDisplayName("My App", "stable") // "My App"
|
|
37
|
+
* getMacOSBundleDisplayName("My App", "canary") // "My App-canary"
|
|
38
|
+
*/
|
|
39
|
+
export function getMacOSBundleDisplayName(appName: string, buildEnvironment: BuildEnvironment): string {
|
|
40
|
+
return buildEnvironment === 'stable' ? appName : `${appName}-${buildEnvironment}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
/**
|
|
31
44
|
* Generates the bundle file name (with platform-specific extension).
|
|
32
45
|
* macOS: "AppName.app" or "AppName-channel.app"
|
|
@@ -38,10 +51,11 @@ export function getBundleFileName(appName: string, buildEnvironment: BuildEnviro
|
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
/**
|
|
41
|
-
* Generates the platform
|
|
54
|
+
* Generates the platform prefix for artifacts.
|
|
42
55
|
* Format: "channel-os-arch" (e.g., "stable-macos-arm64", "canary-win-x64")
|
|
56
|
+
* Used for flat file naming in artifact folders and bucket URLs.
|
|
43
57
|
*/
|
|
44
|
-
export function
|
|
58
|
+
export function getPlatformPrefix(buildEnvironment: BuildEnvironment, os: SupportedOS, arch: SupportedArch): string {
|
|
45
59
|
return `${buildEnvironment}-${os}-${arch}`;
|
|
46
60
|
}
|
|
47
61
|
|
|
@@ -56,7 +70,8 @@ export function getTarballFileName(appFileName: string, os: SupportedOS): string
|
|
|
56
70
|
|
|
57
71
|
/**
|
|
58
72
|
* Generates the Windows installer setup file name.
|
|
59
|
-
*
|
|
73
|
+
* Preserves spaces in app name for user-friendly display.
|
|
74
|
+
* Format: "App Name-Setup.exe" (stable) or "App Name-Setup-channel.exe" (non-stable)
|
|
60
75
|
*/
|
|
61
76
|
export function getWindowsSetupFileName(appName: string, buildEnvironment: BuildEnvironment): string {
|
|
62
77
|
return buildEnvironment === 'stable'
|
|
@@ -64,19 +79,10 @@ export function getWindowsSetupFileName(appName: string, buildEnvironment: Build
|
|
|
64
79
|
: `${appName}-Setup-${buildEnvironment}.exe`;
|
|
65
80
|
}
|
|
66
81
|
|
|
67
|
-
/**
|
|
68
|
-
* Generates the Linux self-extracting binary file name.
|
|
69
|
-
* Format: "AppName-Setup.run" (stable) or "AppName-Setup-channel.run" (non-stable)
|
|
70
|
-
*/
|
|
71
|
-
export function getLinuxSetupFileName(appName: string, buildEnvironment: BuildEnvironment): string {
|
|
72
|
-
return buildEnvironment === 'stable'
|
|
73
|
-
? `${appName}-Setup.run`
|
|
74
|
-
: `${appName}-Setup-${buildEnvironment}.run`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
82
|
/**
|
|
78
83
|
* Generates the Linux AppImage wrapper name (without extension).
|
|
79
|
-
*
|
|
84
|
+
* Preserves spaces in app name for user-friendly display.
|
|
85
|
+
* Format: "App Name-Setup" (stable) or "App Name-Setup-channel" (non-stable)
|
|
80
86
|
*/
|
|
81
87
|
export function getLinuxAppImageBaseName(appName: string, buildEnvironment: BuildEnvironment): string {
|
|
82
88
|
return buildEnvironment === 'stable'
|
|
@@ -100,32 +106,35 @@ export function sanitizeVolumeNameForHdiutil(name: string): string {
|
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
/**
|
|
103
|
-
* Generates the DMG volume name for macOS
|
|
104
|
-
*
|
|
105
|
-
*
|
|
109
|
+
* Generates the DMG volume name for macOS.
|
|
110
|
+
* Takes the original app name (with spaces) and preserves them for display.
|
|
111
|
+
* Format: "App Name" (stable) or "App Name-channel" (non-stable)
|
|
106
112
|
*/
|
|
107
|
-
export function getDmgVolumeName(
|
|
108
|
-
const baseName = sanitizeVolumeNameForHdiutil(
|
|
109
|
-
return buildEnvironment === 'stable' ?
|
|
113
|
+
export function getDmgVolumeName(appName: string, buildEnvironment: BuildEnvironment): string {
|
|
114
|
+
const baseName = sanitizeVolumeNameForHdiutil(appName);
|
|
115
|
+
return buildEnvironment === 'stable' ? baseName : `${baseName}-${buildEnvironment}`;
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
/**
|
|
113
119
|
* Constructs the full URL for the update.json file.
|
|
120
|
+
* Uses flat prefix-based naming for compatibility with GitHub Releases and other hosts.
|
|
114
121
|
*/
|
|
115
|
-
export function getUpdateInfoUrl(
|
|
116
|
-
return `${
|
|
122
|
+
export function getUpdateInfoUrl(baseUrl: string, platformPrefix: string): string {
|
|
123
|
+
return `${baseUrl}/${platformPrefix}-update.json`;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
/**
|
|
120
127
|
* Constructs the full URL for a patch file.
|
|
128
|
+
* Uses flat prefix-based naming for compatibility with GitHub Releases and other hosts.
|
|
121
129
|
*/
|
|
122
|
-
export function getPatchFileUrl(
|
|
123
|
-
return `${
|
|
130
|
+
export function getPatchFileUrl(baseUrl: string, platformPrefix: string, hash: string): string {
|
|
131
|
+
return `${baseUrl}/${platformPrefix}-${hash}.patch`;
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
/**
|
|
127
135
|
* Constructs the full URL for a tarball.
|
|
136
|
+
* Uses flat prefix-based naming for compatibility with GitHub Releases and other hosts.
|
|
128
137
|
*/
|
|
129
|
-
export function getTarballUrl(
|
|
130
|
-
return `${
|
|
138
|
+
export function getTarballUrl(baseUrl: string, platformPrefix: string, tarballFileName: string): string {
|
|
139
|
+
return `${baseUrl}/${platformPrefix}-${tarballFileName}`;
|
|
131
140
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "electrobun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.2-beta.0",
|
|
4
4
|
"description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Blackboard Technologies Inc.",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"dev:clean": "cd ../kitchen && rm -rf node_modules && rm -rf vendors/cef && cd ../package && bun dev",
|
|
36
36
|
"dev:rerun": "cd ../kitchen && bun start",
|
|
37
37
|
"dev:canary": "bun install && bun build:release && bun build:cli && cd ../kitchen && bun install && bun build:canary",
|
|
38
|
+
"dev:stable": "bun install && bun build:release && bun build:cli && cd ../kitchen && bun install && bun build:stable",
|
|
38
39
|
"run:template": "bun install && bun build:dev && bun build:cli && cd ../templates/interactive-playground && bun install && bun build:dev && bun start",
|
|
39
40
|
"dev:docs": "cd ../documentation && bun start",
|
|
40
41
|
"build:docs:release": "cd ../documentation && bun run build",
|
package/src/cli/index.ts
CHANGED
|
@@ -25,13 +25,13 @@ import { OS, ARCH } from '../shared/platform';
|
|
|
25
25
|
import {
|
|
26
26
|
getAppFileName,
|
|
27
27
|
getBundleFileName,
|
|
28
|
-
|
|
28
|
+
getPlatformPrefix,
|
|
29
29
|
getTarballFileName,
|
|
30
30
|
getWindowsSetupFileName,
|
|
31
|
-
getLinuxSetupFileName,
|
|
32
31
|
getLinuxAppImageBaseName,
|
|
33
32
|
sanitizeVolumeNameForHdiutil,
|
|
34
33
|
getDmgVolumeName,
|
|
34
|
+
getMacOSBundleDisplayName,
|
|
35
35
|
} from '../shared/naming';
|
|
36
36
|
import { getTemplate, getTemplateNames } from './templates/embedded';
|
|
37
37
|
// import { loadBsdiff, loadBspatch } from 'bsdiff-wasm';
|
|
@@ -107,7 +107,9 @@ async function ensureCoreDependencies(targetOS?: 'macos' | 'win' | 'linux', targ
|
|
|
107
107
|
|
|
108
108
|
// Check platform-specific binaries
|
|
109
109
|
const requiredBinaries = [
|
|
110
|
-
platformPaths.BUN_BINARY
|
|
110
|
+
platformPaths.BUN_BINARY,
|
|
111
|
+
platformPaths.BSDIFF,
|
|
112
|
+
platformPaths.BSPATCH
|
|
111
113
|
];
|
|
112
114
|
if (platformOS === 'macos') {
|
|
113
115
|
requiredBinaries.push(
|
|
@@ -254,7 +256,11 @@ async function ensureCoreDependencies(targetOS?: 'macos' | 'win' | 'linux', targ
|
|
|
254
256
|
}
|
|
255
257
|
|
|
256
258
|
// Verify extraction completed successfully - check platform-specific binaries only
|
|
257
|
-
const requiredBinaries = [
|
|
259
|
+
const requiredBinaries = [
|
|
260
|
+
platformPaths.BUN_BINARY,
|
|
261
|
+
platformPaths.BSDIFF,
|
|
262
|
+
platformPaths.BSPATCH
|
|
263
|
+
];
|
|
258
264
|
if (platformOS === 'macos') {
|
|
259
265
|
requiredBinaries.push(
|
|
260
266
|
platformPaths.LAUNCHER_RELEASE,
|
|
@@ -587,7 +593,7 @@ const defaultConfig = {
|
|
|
587
593
|
postPackage: "",
|
|
588
594
|
},
|
|
589
595
|
release: {
|
|
590
|
-
|
|
596
|
+
baseUrl: "",
|
|
591
597
|
},
|
|
592
598
|
};
|
|
593
599
|
|
|
@@ -708,17 +714,17 @@ async function createAppImage(
|
|
|
708
714
|
buildFolder: string
|
|
709
715
|
): Promise<string> {
|
|
710
716
|
console.log(`🚀 CREATING APPIMAGE WITH PATH: ${appBundlePath}`);
|
|
711
|
-
console.log(`DEBUG: createAppImage called with:`);
|
|
712
|
-
console.log(` appBundlePath: ${appBundlePath}`);
|
|
713
|
-
console.log(` appFileName: ${appFileName}`);
|
|
714
|
-
console.log(` buildFolder: ${buildFolder}`);
|
|
715
|
-
console.log(` current working directory: ${process.cwd()}`);
|
|
717
|
+
// console.log(`DEBUG: createAppImage called with:`);
|
|
718
|
+
// console.log(` appBundlePath: ${appBundlePath}`);
|
|
719
|
+
// console.log(` appFileName: ${appFileName}`);
|
|
720
|
+
// console.log(` buildFolder: ${buildFolder}`);
|
|
721
|
+
// console.log(` current working directory: ${process.cwd()}`);
|
|
716
722
|
|
|
717
723
|
// Ensure appBundlePath is absolute - fix for when it's passed as basename only
|
|
718
724
|
let resolvedAppBundlePath = appBundlePath;
|
|
719
725
|
if (!path.isAbsolute(appBundlePath)) {
|
|
720
726
|
resolvedAppBundlePath = join(buildFolder, appBundlePath);
|
|
721
|
-
console.log(`DEBUG: Converted relative path to absolute: ${resolvedAppBundlePath}`);
|
|
727
|
+
// console.log(`DEBUG: Converted relative path to absolute: ${resolvedAppBundlePath}`);
|
|
722
728
|
}
|
|
723
729
|
|
|
724
730
|
// Create AppDir structure
|
|
@@ -732,17 +738,17 @@ async function createAppImage(
|
|
|
732
738
|
const usrBinPath = join(appDirPath, 'usr', 'bin');
|
|
733
739
|
mkdirSync(usrBinPath, { recursive: true });
|
|
734
740
|
|
|
735
|
-
console.log(`DEBUG: Attempting to copy from: ${resolvedAppBundlePath}`);
|
|
736
|
-
console.log(`DEBUG: Does source exist? ${existsSync(resolvedAppBundlePath)}`);
|
|
737
|
-
console.log(`DEBUG: To destination: ${join(usrBinPath, basename(resolvedAppBundlePath))}`);
|
|
741
|
+
// console.log(`DEBUG: Attempting to copy from: ${resolvedAppBundlePath}`);
|
|
742
|
+
// console.log(`DEBUG: Does source exist? ${existsSync(resolvedAppBundlePath)}`);
|
|
743
|
+
// console.log(`DEBUG: To destination: ${join(usrBinPath, basename(resolvedAppBundlePath))}`);
|
|
738
744
|
|
|
739
745
|
if (!existsSync(resolvedAppBundlePath)) {
|
|
740
746
|
throw new Error(`Source bundle does not exist: ${resolvedAppBundlePath}`);
|
|
741
747
|
}
|
|
742
748
|
|
|
743
|
-
console.log(`DEBUG: About to copy with cpSync:`);
|
|
744
|
-
console.log(` from: ${resolvedAppBundlePath} (exists: ${existsSync(resolvedAppBundlePath)})`);
|
|
745
|
-
console.log(` to: ${join(usrBinPath, basename(resolvedAppBundlePath))}`);
|
|
749
|
+
// console.log(`DEBUG: About to copy with cpSync:`);
|
|
750
|
+
// console.log(` from: ${resolvedAppBundlePath} (exists: ${existsSync(resolvedAppBundlePath)})`);
|
|
751
|
+
// console.log(` to: ${join(usrBinPath, basename(resolvedAppBundlePath))}`);
|
|
746
752
|
|
|
747
753
|
cpSync(resolvedAppBundlePath, join(usrBinPath, basename(resolvedAppBundlePath)), {
|
|
748
754
|
recursive: true,
|
|
@@ -802,8 +808,8 @@ Categories=Utility;
|
|
|
802
808
|
unlinkSync(appImagePath);
|
|
803
809
|
}
|
|
804
810
|
|
|
805
|
-
console.log(`DEBUG: AppDir path: ${appDirPath}`);
|
|
806
|
-
console.log(`DEBUG: Does AppDir exist? ${existsSync(appDirPath)}`);
|
|
811
|
+
// console.log(`DEBUG: AppDir path: ${appDirPath}`);
|
|
812
|
+
// console.log(`DEBUG: Does AppDir exist? ${existsSync(appDirPath)}`);
|
|
807
813
|
console.log(`Generating AppImage: ${appImagePath}`);
|
|
808
814
|
const appImageArch = ARCH === 'arm64' ? 'aarch64' : 'x86_64';
|
|
809
815
|
|
|
@@ -1048,10 +1054,12 @@ if (commandArg === "init") {
|
|
|
1048
1054
|
const targetARCH = currentTarget.arch;
|
|
1049
1055
|
const targetBinExt = targetOS === 'win' ? '.exe' : '';
|
|
1050
1056
|
const appFileName = getAppFileName(config.app.name, buildEnvironment);
|
|
1051
|
-
|
|
1052
|
-
const
|
|
1057
|
+
// macOS bundle display name preserves spaces for the actual .app folder
|
|
1058
|
+
const macOSBundleDisplayName = getMacOSBundleDisplayName(config.app.name, buildEnvironment);
|
|
1059
|
+
const platformPrefix = getPlatformPrefix(buildEnvironment, currentTarget.os, currentTarget.arch);
|
|
1060
|
+
const buildFolder = join(projectRoot, config.build.buildFolder, platformPrefix);
|
|
1053
1061
|
const bundleFileName = getBundleFileName(config.app.name, buildEnvironment, targetOS);
|
|
1054
|
-
const artifactFolder = join(projectRoot, config.build.artifactFolder
|
|
1062
|
+
const artifactFolder = join(projectRoot, config.build.artifactFolder);
|
|
1055
1063
|
|
|
1056
1064
|
// Ensure core binaries are available for the target platform before starting build
|
|
1057
1065
|
await ensureCoreDependencies(currentTarget.os, currentTarget.arch);
|
|
@@ -1171,13 +1179,15 @@ if (commandArg === "init") {
|
|
|
1171
1179
|
}
|
|
1172
1180
|
|
|
1173
1181
|
// build macos bundle
|
|
1182
|
+
// Use display name (with spaces) for macOS bundle folders, sanitized name for other platforms
|
|
1183
|
+
const bundleName = targetOS === 'macos' ? macOSBundleDisplayName : appFileName;
|
|
1174
1184
|
const {
|
|
1175
1185
|
appBundleFolderPath,
|
|
1176
1186
|
appBundleFolderContentsPath,
|
|
1177
1187
|
appBundleMacOSPath,
|
|
1178
1188
|
appBundleFolderResourcesPath,
|
|
1179
1189
|
appBundleFolderFrameworksPath,
|
|
1180
|
-
} = createAppBundle(
|
|
1190
|
+
} = createAppBundle(bundleName, buildFolder, targetOS);
|
|
1181
1191
|
|
|
1182
1192
|
const appBundleAppCodePath = join(appBundleFolderResourcesPath, "app");
|
|
1183
1193
|
|
|
@@ -1206,7 +1216,7 @@ if (commandArg === "init") {
|
|
|
1206
1216
|
<key>CFBundleIdentifier</key>
|
|
1207
1217
|
<string>${config.app.identifier}</string>
|
|
1208
1218
|
<key>CFBundleName</key>
|
|
1209
|
-
<string>${
|
|
1219
|
+
<string>${bundleName}</string>
|
|
1210
1220
|
<key>CFBundleVersion</key>
|
|
1211
1221
|
<string>${config.app.version}</string>
|
|
1212
1222
|
<key>CFBundlePackageType</key>
|
|
@@ -1932,7 +1942,8 @@ if (commandArg === "init") {
|
|
|
1932
1942
|
// All the unique files are in the bundle now. Create an initial temporary tar file
|
|
1933
1943
|
// for hashing the contents
|
|
1934
1944
|
// tar the signed and notarized app bundle
|
|
1935
|
-
|
|
1945
|
+
// Use sanitized appFileName for tarball paths (URL-safe), but tar content uses actual bundle folder
|
|
1946
|
+
const tmpTarPath = join(buildFolder, `${appFileName}${targetOS === 'macos' ? '.app' : ''}-temp.tar`);
|
|
1936
1947
|
await tar.c(
|
|
1937
1948
|
{
|
|
1938
1949
|
gzip: false,
|
|
@@ -1958,7 +1969,7 @@ if (commandArg === "init") {
|
|
|
1958
1969
|
// then gets used for patching and updating.
|
|
1959
1970
|
hash: hash,
|
|
1960
1971
|
channel: buildEnvironment,
|
|
1961
|
-
|
|
1972
|
+
baseUrl: config.release.baseUrl,
|
|
1962
1973
|
name: appFileName,
|
|
1963
1974
|
identifier: config.app.identifier,
|
|
1964
1975
|
});
|
|
@@ -2011,12 +2022,9 @@ if (commandArg === "init") {
|
|
|
2011
2022
|
}
|
|
2012
2023
|
|
|
2013
2024
|
const artifactsToUpload = [];
|
|
2014
|
-
|
|
2015
|
-
console.log(`DEBUG: Checking for Linux AppImage creation - targetOS: ${targetOS}, buildEnvironment: ${buildEnvironment}`);
|
|
2016
2025
|
|
|
2017
2026
|
// Linux AppImage creation (for all build environments including dev)
|
|
2018
|
-
if (targetOS === 'linux') {
|
|
2019
|
-
console.log("DEBUG: Creating Linux AppImage...");
|
|
2027
|
+
if (targetOS === 'linux') {
|
|
2020
2028
|
// Ensure AppImage tooling is available
|
|
2021
2029
|
await ensureAppImageTooling();
|
|
2022
2030
|
|
|
@@ -2098,20 +2106,21 @@ if (commandArg === "init") {
|
|
|
2098
2106
|
// Compress with Zstandard
|
|
2099
2107
|
console.log(`Compressing tar with zstd...`);
|
|
2100
2108
|
const uncompressedTarData = readFileSync(appImageTarPath);
|
|
2109
|
+
const compressedTarPath = `${appImageTarPath}.zst`;
|
|
2101
2110
|
await ZstdInit().then(async ({ ZstdSimple }) => {
|
|
2102
2111
|
const data = new Uint8Array(uncompressedTarData);
|
|
2103
2112
|
const compressionLevel = 22;
|
|
2104
2113
|
const compressedData = ZstdSimple.compress(data, compressionLevel);
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
console.log(`✓ Created compressed tar: ${compressedPath} (${(compressedData.length / 1024 / 1024).toFixed(2)} MB)`);
|
|
2114
|
+
writeFileSync(compressedTarPath, compressedData);
|
|
2115
|
+
console.log(`✓ Created compressed tar: ${compressedTarPath} (${(compressedData.length / 1024 / 1024).toFixed(2)} MB)`);
|
|
2108
2116
|
});
|
|
2109
|
-
|
|
2110
|
-
//
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
// Add
|
|
2114
|
-
|
|
2117
|
+
|
|
2118
|
+
// Note: Don't delete uncompressed tar here - bsdiff needs it later for patch generation
|
|
2119
|
+
// It will be cleaned up after bsdiff runs
|
|
2120
|
+
|
|
2121
|
+
// Add tar.zst for Updater API (delta updates)
|
|
2122
|
+
// Note: raw AppImage is NOT added - only the Setup.AppImage (zipped) is distributed
|
|
2123
|
+
artifactsToUpload.push(compressedTarPath);
|
|
2115
2124
|
}
|
|
2116
2125
|
}
|
|
2117
2126
|
|
|
@@ -2131,7 +2140,8 @@ if (commandArg === "init") {
|
|
|
2131
2140
|
|
|
2132
2141
|
// Platform suffix is only used for folder names, not file names
|
|
2133
2142
|
const platformSuffix = `-${targetOS}-${targetARCH}`;
|
|
2134
|
-
|
|
2143
|
+
// Use sanitized appFileName for tarball path (URL-safe), but tar content uses actual bundle folder name
|
|
2144
|
+
const tarPath = join(buildFolder, `${appFileName}${targetOS === 'macos' ? '.app' : ''}.tar`);
|
|
2135
2145
|
|
|
2136
2146
|
// For Linux, we've already created the tar in the AppImage section above
|
|
2137
2147
|
// For macOS/Windows, tar the signed and notarized app bundle
|
|
@@ -2201,7 +2211,7 @@ if (commandArg === "init") {
|
|
|
2201
2211
|
rmdirSync(appBundleFolderPath, { recursive: true });
|
|
2202
2212
|
}
|
|
2203
2213
|
|
|
2204
|
-
const selfExtractingBundle = createAppBundle(
|
|
2214
|
+
const selfExtractingBundle = createAppBundle(bundleName, buildFolder, targetOS);
|
|
2205
2215
|
const compressedTarballInExtractingBundlePath = join(
|
|
2206
2216
|
selfExtractingBundle.appBundleFolderResourcesPath,
|
|
2207
2217
|
`${hash}.tar.zst`
|
|
@@ -2260,7 +2270,7 @@ if (commandArg === "init") {
|
|
|
2260
2270
|
buildEnvironment === "stable"
|
|
2261
2271
|
? join(buildFolder, `${appFileName}-stable.dmg`)
|
|
2262
2272
|
: finalDmgPath;
|
|
2263
|
-
const dmgVolumeName = getDmgVolumeName(
|
|
2273
|
+
const dmgVolumeName = getDmgVolumeName(config.app.name, buildEnvironment);
|
|
2264
2274
|
|
|
2265
2275
|
// Create a staging directory for DMG contents (app + Applications shortcut)
|
|
2266
2276
|
const dmgStagingDir = join(buildFolder, '.dmg-staging');
|
|
@@ -2338,8 +2348,10 @@ if (commandArg === "init") {
|
|
|
2338
2348
|
buildEnvironment,
|
|
2339
2349
|
hash
|
|
2340
2350
|
);
|
|
2341
|
-
|
|
2342
|
-
|
|
2351
|
+
|
|
2352
|
+
// Wrap the Setup.AppImage in a tar.gz to preserve executable permissions
|
|
2353
|
+
const archivedAppImagePath = await wrapInArchive(selfExtractingAppImagePath, buildFolder, 'tar.gz');
|
|
2354
|
+
artifactsToUpload.push(archivedAppImagePath);
|
|
2343
2355
|
}
|
|
2344
2356
|
}
|
|
2345
2357
|
|
|
@@ -2362,40 +2374,33 @@ if (commandArg === "init") {
|
|
|
2362
2374
|
platform: OS,
|
|
2363
2375
|
arch: ARCH,
|
|
2364
2376
|
// channel: buildEnvironment,
|
|
2365
|
-
//
|
|
2377
|
+
// baseUrl: config.release.baseUrl
|
|
2366
2378
|
});
|
|
2367
2379
|
|
|
2368
|
-
// update.json
|
|
2369
|
-
await Bun.write(join(artifactFolder,
|
|
2380
|
+
// update.json with platform prefix for flat naming structure
|
|
2381
|
+
await Bun.write(join(artifactFolder, `${platformPrefix}-update.json`), updateJsonContent);
|
|
2370
2382
|
|
|
2371
2383
|
// generate bsdiff
|
|
2372
2384
|
// https://storage.googleapis.com/eggbun-static/electrobun-playground/canary/ElectrobunPlayground-canary.app.tar.zst
|
|
2373
|
-
console.log("
|
|
2385
|
+
console.log("baseUrl: ", config.release.baseUrl);
|
|
2374
2386
|
|
|
2375
2387
|
console.log("generating a patch from the previous version...");
|
|
2376
|
-
|
|
2377
|
-
// Skip patch generation if
|
|
2378
|
-
if (!config.release.
|
|
2379
|
-
console.log("No
|
|
2380
|
-
console.log("To enable patch generation, configure
|
|
2388
|
+
|
|
2389
|
+
// Skip patch generation if baseUrl is not configured
|
|
2390
|
+
if (!config.release.baseUrl || config.release.baseUrl.trim() === '') {
|
|
2391
|
+
console.log("No baseUrl configured, skipping patch generation");
|
|
2392
|
+
console.log("To enable patch generation, configure baseUrl in your electrobun.config");
|
|
2381
2393
|
} else {
|
|
2382
|
-
const urlToPrevUpdateJson =
|
|
2383
|
-
config.release.bucketUrl,
|
|
2384
|
-
buildSubFolder,
|
|
2385
|
-
'update.json'
|
|
2386
|
-
);
|
|
2394
|
+
const urlToPrevUpdateJson = `${config.release.baseUrl}/${platformPrefix}-update.json`;
|
|
2387
2395
|
const cacheBuster = Math.random().toString(36).substring(7);
|
|
2388
2396
|
const updateJsonResponse = await fetch(
|
|
2389
2397
|
urlToPrevUpdateJson + `?${cacheBuster}`
|
|
2390
2398
|
).catch((err) => {
|
|
2391
|
-
console.log("
|
|
2399
|
+
console.log("baseUrl not found: ", err);
|
|
2392
2400
|
});
|
|
2393
2401
|
|
|
2394
|
-
const
|
|
2395
|
-
|
|
2396
|
-
buildSubFolder,
|
|
2397
|
-
`${appFileName}.app.tar.zst`
|
|
2398
|
-
);
|
|
2402
|
+
const tarballFileName = getTarballFileName(appFileName, OS);
|
|
2403
|
+
const urlToLatestTarball = `${config.release.baseUrl}/${platformPrefix}-${tarballFileName}`;
|
|
2399
2404
|
|
|
2400
2405
|
|
|
2401
2406
|
// attempt to get the previous version to create a patch file
|
|
@@ -2441,7 +2446,6 @@ if (commandArg === "init") {
|
|
|
2441
2446
|
// especially for creating multiple diffs in parallel
|
|
2442
2447
|
const bsdiffpath = targetPaths.BSDIFF;
|
|
2443
2448
|
const patchFilePath = join(buildFolder, `${prevHash}.patch`);
|
|
2444
|
-
artifactsToUpload.push(patchFilePath);
|
|
2445
2449
|
const result = Bun.spawnSync(
|
|
2446
2450
|
[bsdiffpath, prevTarballPath, tarPath, patchFilePath, "--use-zstd"],
|
|
2447
2451
|
{
|
|
@@ -2451,21 +2455,37 @@ if (commandArg === "init") {
|
|
|
2451
2455
|
}
|
|
2452
2456
|
);
|
|
2453
2457
|
if (!result.success) {
|
|
2454
|
-
|
|
2458
|
+
// Patch generation is non-critical - users will just download full updates instead of delta patches
|
|
2459
|
+
console.error("\n" + "=".repeat(80));
|
|
2460
|
+
console.error("WARNING: Patch generation failed (exit code " + result.exitCode + ")");
|
|
2461
|
+
console.error("Delta updates will not be available for this release.");
|
|
2462
|
+
console.error("Users will download the full update instead.");
|
|
2463
|
+
console.error("=".repeat(80) + "\n");
|
|
2464
|
+
} else {
|
|
2465
|
+
// Only add patch to artifacts if it was successfully created
|
|
2466
|
+
artifactsToUpload.push(patchFilePath);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// Clean up uncompressed tars now that bsdiff is done
|
|
2470
|
+
if (existsSync(tarPath)) {
|
|
2471
|
+
unlinkSync(tarPath);
|
|
2472
|
+
}
|
|
2473
|
+
if (existsSync(prevTarballPath)) {
|
|
2474
|
+
unlinkSync(prevTarballPath);
|
|
2455
2475
|
}
|
|
2456
2476
|
}
|
|
2457
2477
|
} else {
|
|
2458
2478
|
console.log("prevoius version not found at: ", urlToLatestTarball);
|
|
2459
2479
|
console.log("skipping diff generation");
|
|
2460
2480
|
}
|
|
2461
|
-
} // End of
|
|
2481
|
+
} // End of baseUrl validation block
|
|
2462
2482
|
|
|
2463
2483
|
// compress all the upload files
|
|
2464
2484
|
console.log("copying artifacts...");
|
|
2465
2485
|
|
|
2466
2486
|
artifactsToUpload.forEach((filePath) => {
|
|
2467
2487
|
const filename = basename(filePath);
|
|
2468
|
-
cpSync(filePath, join(artifactFolder, filename), { dereference: true });
|
|
2488
|
+
cpSync(filePath, join(artifactFolder, `${platformPrefix}-${filename}`), { dereference: true });
|
|
2469
2489
|
});
|
|
2470
2490
|
|
|
2471
2491
|
// todo: now just upload the artifacts to your bucket replacing the ones that exist
|
|
@@ -2811,59 +2831,6 @@ async function createWindowsSelfExtractingExe(
|
|
|
2811
2831
|
return outputExePath;
|
|
2812
2832
|
}
|
|
2813
2833
|
|
|
2814
|
-
async function createLinuxSelfExtractingBinary(
|
|
2815
|
-
buildFolder: string,
|
|
2816
|
-
compressedTarPath: string,
|
|
2817
|
-
appFileName: string,
|
|
2818
|
-
targetPaths: any,
|
|
2819
|
-
buildEnvironment: string,
|
|
2820
|
-
config: Awaited<ReturnType<typeof getConfig>>
|
|
2821
|
-
): Promise<string> {
|
|
2822
|
-
console.log("Creating self-extracting Linux binary...");
|
|
2823
|
-
|
|
2824
|
-
const setupFileName = getLinuxSetupFileName(config.app.name, buildEnvironment);
|
|
2825
|
-
|
|
2826
|
-
const outputPath = join(buildFolder, setupFileName);
|
|
2827
|
-
|
|
2828
|
-
// Read the extractor binary
|
|
2829
|
-
const extractorBinary = readFileSync(targetPaths.EXTRACTOR);
|
|
2830
|
-
|
|
2831
|
-
// Read the compressed archive
|
|
2832
|
-
const compressedArchive = readFileSync(compressedTarPath);
|
|
2833
|
-
|
|
2834
|
-
// Create metadata JSON
|
|
2835
|
-
const metadata = {
|
|
2836
|
-
identifier: config.app.identifier,
|
|
2837
|
-
name: config.app.name,
|
|
2838
|
-
channel: buildEnvironment
|
|
2839
|
-
};
|
|
2840
|
-
const metadataJson = JSON.stringify(metadata);
|
|
2841
|
-
const metadataBuffer = Buffer.from(metadataJson, 'utf8');
|
|
2842
|
-
|
|
2843
|
-
// Create marker buffers
|
|
2844
|
-
const metadataMarker = Buffer.from('ELECTROBUN_METADATA_V1', 'utf8');
|
|
2845
|
-
const archiveMarker = Buffer.from('ELECTROBUN_ARCHIVE_V1', 'utf8');
|
|
2846
|
-
|
|
2847
|
-
// Combine extractor + metadata marker + metadata + archive marker + archive
|
|
2848
|
-
const combinedBuffer = Buffer.concat([
|
|
2849
|
-
extractorBinary,
|
|
2850
|
-
metadataMarker,
|
|
2851
|
-
metadataBuffer,
|
|
2852
|
-
archiveMarker,
|
|
2853
|
-
compressedArchive
|
|
2854
|
-
]);
|
|
2855
|
-
|
|
2856
|
-
// Write the self-extracting binary
|
|
2857
|
-
writeFileSync(outputPath, combinedBuffer, { mode: 0o755 });
|
|
2858
|
-
|
|
2859
|
-
// Ensure it's executable (redundant but explicit)
|
|
2860
|
-
execSync(`chmod +x ${escapePathForTerminal(outputPath)}`);
|
|
2861
|
-
|
|
2862
|
-
console.log(`Created self-extracting Linux binary: ${outputPath} (${(combinedBuffer.length / 1024 / 1024).toFixed(2)} MB)`);
|
|
2863
|
-
|
|
2864
|
-
return outputPath;
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
2834
|
async function wrapWindowsInstallerInZip(exePath: string, buildFolder: string): Promise<string> {
|
|
2868
2835
|
const exeName = basename(exePath);
|
|
2869
2836
|
const exeStem = exeName.replace('.exe', '');
|
|
@@ -2918,11 +2885,14 @@ async function wrapInArchive(filePath: string, buildFolder: string, archiveType:
|
|
|
2918
2885
|
const fileDir = dirname(filePath);
|
|
2919
2886
|
|
|
2920
2887
|
if (archiveType === 'tar.gz') {
|
|
2921
|
-
// Output filename: Setup.exe -> Setup.exe.tar.gz
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2888
|
+
// Output filename: Setup.exe -> Setup.exe.tar.gz, Setup.AppImage -> Setup.tar.gz
|
|
2889
|
+
// For AppImage files, strip the .AppImage extension so archive extracts to .AppImage
|
|
2890
|
+
const archivePath = fileName.endsWith('.AppImage')
|
|
2891
|
+
? filePath.replace(/\.AppImage$/, '.tar.gz')
|
|
2892
|
+
: filePath + '.tar.gz';
|
|
2893
|
+
|
|
2894
|
+
// For Linux AppImage files, ensure they have executable permissions before archiving
|
|
2895
|
+
if (fileName.endsWith('.AppImage')) {
|
|
2926
2896
|
try {
|
|
2927
2897
|
// Try to set executable permissions (will only work on Unix-like systems)
|
|
2928
2898
|
execSync(`chmod +x ${escapePathForTerminal(filePath)}`, { stdio: 'ignore' });
|