electrobun 0.11.0-beta.3 → 0.13.0-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/bun.lock +25 -1
- package/dist/api/browser/rpc/webview.ts +9 -0
- package/dist/api/browser/webviewtag.ts +28 -0
- package/dist/api/bun/ElectrobunConfig.ts +38 -4
- package/dist/api/bun/core/BrowserView.ts +11 -10
- package/dist/api/bun/core/BrowserWindow.ts +71 -13
- package/dist/api/bun/core/BuildConfig.ts +38 -0
- package/dist/api/bun/core/Socket.ts +1 -1
- package/dist/api/bun/core/Updater.ts +67 -24
- package/dist/api/bun/index.ts +4 -0
- package/dist/api/bun/proc/native.ts +155 -57
- package/dist/main.js +8 -4
- package/package.json +3 -1
- package/src/cli/index.ts +930 -495
package/src/cli/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join, dirname, basename, relative } from "path";
|
|
2
|
+
import * as path from "path";
|
|
2
3
|
import {
|
|
3
4
|
existsSync,
|
|
4
5
|
readFileSync,
|
|
@@ -531,7 +532,7 @@ const commandDefaults = {
|
|
|
531
532
|
},
|
|
532
533
|
};
|
|
533
534
|
|
|
534
|
-
//
|
|
535
|
+
// TODO (yoav): add types for config
|
|
535
536
|
const defaultConfig = {
|
|
536
537
|
app: {
|
|
537
538
|
name: "MyApp",
|
|
@@ -569,7 +570,10 @@ const defaultConfig = {
|
|
|
569
570
|
},
|
|
570
571
|
},
|
|
571
572
|
scripts: {
|
|
573
|
+
preBuild: "",
|
|
572
574
|
postBuild: "",
|
|
575
|
+
postWrap: "",
|
|
576
|
+
postPackage: "",
|
|
573
577
|
},
|
|
574
578
|
release: {
|
|
575
579
|
bucketUrl: "",
|
|
@@ -606,6 +610,255 @@ function escapeXml(str: string): string {
|
|
|
606
610
|
.replace(/'/g, ''');
|
|
607
611
|
}
|
|
608
612
|
|
|
613
|
+
// Helper functions
|
|
614
|
+
function escapePathForTerminal(path: string): string {
|
|
615
|
+
return `"${path.replace(/"/g, '\\"')}"`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// AppImage tooling functions
|
|
619
|
+
async function ensureAppImageTooling(): Promise<void> {
|
|
620
|
+
// First check if FUSE2 is available
|
|
621
|
+
try {
|
|
622
|
+
execSync('ls /usr/lib/*/libfuse.so.2 || ls /lib/*/libfuse.so.2', { stdio: 'ignore' });
|
|
623
|
+
} catch (error) {
|
|
624
|
+
console.log('');
|
|
625
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
626
|
+
console.log('🚨 FUSE2 DEPENDENCY MISSING');
|
|
627
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
628
|
+
console.log('AppImage creation requires libfuse2, but it was not found on your system.');
|
|
629
|
+
console.log('');
|
|
630
|
+
console.log('Please install it using:');
|
|
631
|
+
console.log(' sudo apt update && sudo apt install -y libfuse2');
|
|
632
|
+
console.log('');
|
|
633
|
+
console.log('Without libfuse2, AppImage creation will fail with FUSE errors.');
|
|
634
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
635
|
+
console.log('');
|
|
636
|
+
throw new Error('libfuse2 is required for AppImage creation but not found. Please install it first.');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
// Check if appimagetool is available
|
|
641
|
+
execSync('which appimagetool', { stdio: 'ignore' });
|
|
642
|
+
console.log('✓ appimagetool found');
|
|
643
|
+
return;
|
|
644
|
+
} catch (error) {
|
|
645
|
+
// appimagetool not found, download it automatically
|
|
646
|
+
console.log('📥 appimagetool not found, downloading...');
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
// Determine architecture-specific download URL
|
|
650
|
+
const downloadUrl = ARCH === 'arm64'
|
|
651
|
+
? 'https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage'
|
|
652
|
+
: 'https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage';
|
|
653
|
+
|
|
654
|
+
// Download appimagetool
|
|
655
|
+
console.log(`Downloading appimagetool from ${downloadUrl}...`);
|
|
656
|
+
execSync(`wget -q "${downloadUrl}" -O /tmp/appimagetool.AppImage`, { stdio: 'inherit' });
|
|
657
|
+
|
|
658
|
+
// Make it executable
|
|
659
|
+
execSync('chmod +x /tmp/appimagetool.AppImage', { stdio: 'inherit' });
|
|
660
|
+
|
|
661
|
+
// Try to move to /usr/local/bin (with sudo)
|
|
662
|
+
try {
|
|
663
|
+
execSync('sudo mv /tmp/appimagetool.AppImage /usr/local/bin/appimagetool', { stdio: 'inherit' });
|
|
664
|
+
console.log('✓ appimagetool installed to /usr/local/bin/appimagetool');
|
|
665
|
+
} catch (sudoError) {
|
|
666
|
+
// Fallback: extract and place in user's local bin
|
|
667
|
+
console.log('sudo not available, installing to ~/.local/bin/...');
|
|
668
|
+
execSync('mkdir -p ~/.local/bin', { stdio: 'inherit' });
|
|
669
|
+
|
|
670
|
+
// Extract the AppImage to get the binary
|
|
671
|
+
execSync('cd /tmp && ./appimagetool.AppImage --appimage-extract >/dev/null 2>&1', { stdio: 'inherit' });
|
|
672
|
+
execSync('cp /tmp/squashfs-root/usr/bin/appimagetool ~/.local/bin/appimagetool', { stdio: 'inherit' });
|
|
673
|
+
execSync('chmod +x ~/.local/bin/appimagetool', { stdio: 'inherit' });
|
|
674
|
+
|
|
675
|
+
// Set up symlink for mksquashfs dependency
|
|
676
|
+
execSync('mkdir -p ~/.local/lib/appimagekit', { stdio: 'inherit' });
|
|
677
|
+
execSync('ln -sf /usr/bin/mksquashfs ~/.local/lib/appimagekit/mksquashfs', { stdio: 'inherit' });
|
|
678
|
+
|
|
679
|
+
// Clean up
|
|
680
|
+
execSync('rm -rf /tmp/appimagetool.AppImage /tmp/squashfs-root', { stdio: 'inherit' });
|
|
681
|
+
|
|
682
|
+
console.log('✓ appimagetool installed to ~/.local/bin/appimagetool');
|
|
683
|
+
console.log('Note: Make sure ~/.local/bin is in your PATH for future use');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
} catch (downloadError) {
|
|
687
|
+
console.error('Failed to download appimagetool:', downloadError);
|
|
688
|
+
throw new Error('Failed to install appimagetool automatically. Please install it manually.');
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function createAppImage(
|
|
694
|
+
appBundlePath: string,
|
|
695
|
+
appFileName: string,
|
|
696
|
+
config: any,
|
|
697
|
+
buildFolder: string
|
|
698
|
+
): Promise<string> {
|
|
699
|
+
console.log(`🚀 CREATING APPIMAGE WITH PATH: ${appBundlePath}`);
|
|
700
|
+
console.log(`DEBUG: createAppImage called with:`);
|
|
701
|
+
console.log(` appBundlePath: ${appBundlePath}`);
|
|
702
|
+
console.log(` appFileName: ${appFileName}`);
|
|
703
|
+
console.log(` buildFolder: ${buildFolder}`);
|
|
704
|
+
console.log(` current working directory: ${process.cwd()}`);
|
|
705
|
+
|
|
706
|
+
// Ensure appBundlePath is absolute - fix for when it's passed as basename only
|
|
707
|
+
let resolvedAppBundlePath = appBundlePath;
|
|
708
|
+
if (!path.isAbsolute(appBundlePath)) {
|
|
709
|
+
resolvedAppBundlePath = join(buildFolder, appBundlePath);
|
|
710
|
+
console.log(`DEBUG: Converted relative path to absolute: ${resolvedAppBundlePath}`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Create AppDir structure
|
|
714
|
+
const appDirPath = join(buildFolder, `${appFileName}.AppDir`);
|
|
715
|
+
if (existsSync(appDirPath)) {
|
|
716
|
+
rmSync(appDirPath, { recursive: true, force: true });
|
|
717
|
+
}
|
|
718
|
+
mkdirSync(appDirPath, { recursive: true });
|
|
719
|
+
|
|
720
|
+
// Copy the entire app bundle to AppDir/usr/bin/
|
|
721
|
+
const usrBinPath = join(appDirPath, 'usr', 'bin');
|
|
722
|
+
mkdirSync(usrBinPath, { recursive: true });
|
|
723
|
+
|
|
724
|
+
console.log(`DEBUG: Attempting to copy from: ${resolvedAppBundlePath}`);
|
|
725
|
+
console.log(`DEBUG: Does source exist? ${existsSync(resolvedAppBundlePath)}`);
|
|
726
|
+
console.log(`DEBUG: To destination: ${join(usrBinPath, basename(resolvedAppBundlePath))}`);
|
|
727
|
+
|
|
728
|
+
if (!existsSync(resolvedAppBundlePath)) {
|
|
729
|
+
throw new Error(`Source bundle does not exist: ${resolvedAppBundlePath}`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
console.log(`DEBUG: About to copy with cpSync:`);
|
|
733
|
+
console.log(` from: ${resolvedAppBundlePath} (exists: ${existsSync(resolvedAppBundlePath)})`);
|
|
734
|
+
console.log(` to: ${join(usrBinPath, basename(resolvedAppBundlePath))}`);
|
|
735
|
+
|
|
736
|
+
cpSync(resolvedAppBundlePath, join(usrBinPath, basename(resolvedAppBundlePath)), {
|
|
737
|
+
recursive: true,
|
|
738
|
+
dereference: true
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// Create AppRun script (the entry point)
|
|
742
|
+
const appBundleBasename = basename(resolvedAppBundlePath);
|
|
743
|
+
const appRunContent = `#!/bin/bash
|
|
744
|
+
# AppRun script for ${appFileName}
|
|
745
|
+
HERE="$(dirname "$(readlink -f "\${0}")")"
|
|
746
|
+
EXEC="\${HERE}/usr/bin/${appBundleBasename}/bin/launcher"
|
|
747
|
+
|
|
748
|
+
# Set up library path for CEF
|
|
749
|
+
export LD_LIBRARY_PATH="\${HERE}/usr/bin/${appBundleBasename}/bin:\${HERE}/usr/bin/${appBundleBasename}/lib:\${LD_LIBRARY_PATH}"
|
|
750
|
+
|
|
751
|
+
# Execute the application
|
|
752
|
+
exec "\${EXEC}" "\$@"
|
|
753
|
+
`;
|
|
754
|
+
|
|
755
|
+
const appRunPath = join(appDirPath, 'AppRun');
|
|
756
|
+
writeFileSync(appRunPath, appRunContent);
|
|
757
|
+
execSync(`chmod +x ${escapePathForTerminal(appRunPath)}`);
|
|
758
|
+
|
|
759
|
+
// Create .desktop file in AppDir root
|
|
760
|
+
const desktopContent = `[Desktop Entry]
|
|
761
|
+
Version=1.0
|
|
762
|
+
Type=Application
|
|
763
|
+
Name=${config.app.name}
|
|
764
|
+
Comment=${config.app.description || ''}
|
|
765
|
+
Exec=${appFileName}
|
|
766
|
+
Icon=${appFileName}
|
|
767
|
+
Terminal=false
|
|
768
|
+
StartupWMClass=${appFileName}
|
|
769
|
+
Categories=Utility;
|
|
770
|
+
`;
|
|
771
|
+
|
|
772
|
+
const desktopPath = join(appDirPath, `${appFileName}.desktop`);
|
|
773
|
+
writeFileSync(desktopPath, desktopContent);
|
|
774
|
+
|
|
775
|
+
// Copy icon if available
|
|
776
|
+
if (config.build.linux?.icon && existsSync(join(projectRoot, config.build.linux.icon))) {
|
|
777
|
+
const iconSourcePath = join(projectRoot, config.build.linux.icon);
|
|
778
|
+
const iconDestPath = join(appDirPath, `${appFileName}.png`);
|
|
779
|
+
const dirIconPath = join(appDirPath, '.DirIcon');
|
|
780
|
+
|
|
781
|
+
cpSync(iconSourcePath, iconDestPath, { dereference: true });
|
|
782
|
+
cpSync(iconSourcePath, dirIconPath, { dereference: true });
|
|
783
|
+
|
|
784
|
+
console.log(`Copied icon for AppImage: ${iconSourcePath} -> ${iconDestPath}`);
|
|
785
|
+
console.log(`Created .DirIcon: ${iconSourcePath} -> ${dirIconPath}`);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Generate the AppImage using appimagetool
|
|
789
|
+
const appImagePath = join(buildFolder, `${appFileName}.AppImage`);
|
|
790
|
+
if (existsSync(appImagePath)) {
|
|
791
|
+
unlinkSync(appImagePath);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
console.log(`DEBUG: AppDir path: ${appDirPath}`);
|
|
795
|
+
console.log(`DEBUG: Does AppDir exist? ${existsSync(appDirPath)}`);
|
|
796
|
+
console.log(`Generating AppImage: ${appImagePath}`);
|
|
797
|
+
const appImageArch = ARCH === 'arm64' ? 'aarch64' : 'x86_64';
|
|
798
|
+
|
|
799
|
+
// Use full path to appimagetool if not in PATH
|
|
800
|
+
let appimagetoolCmd = 'appimagetool';
|
|
801
|
+
try {
|
|
802
|
+
execSync('which appimagetool', { stdio: 'ignore' });
|
|
803
|
+
} catch {
|
|
804
|
+
// Try ~/.local/bin/appimagetool
|
|
805
|
+
const localBinPath = join(process.env['HOME'] || '', '.local', 'bin', 'appimagetool');
|
|
806
|
+
if (existsSync(localBinPath)) {
|
|
807
|
+
appimagetoolCmd = localBinPath;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
try {
|
|
812
|
+
// First try with --no-appstream flag to avoid some FUSE-related issues
|
|
813
|
+
execSync(`ARCH=${appImageArch} ${appimagetoolCmd} --no-appstream ${escapePathForTerminal(appDirPath)} ${escapePathForTerminal(appImagePath)}`, {
|
|
814
|
+
stdio: 'inherit',
|
|
815
|
+
env: { ...process.env, ARCH: appImageArch }
|
|
816
|
+
});
|
|
817
|
+
} catch (error) {
|
|
818
|
+
console.error('Failed to create AppImage:', error);
|
|
819
|
+
console.log('Note: If you see FUSE errors, you may need to install libfuse2:');
|
|
820
|
+
console.log(' sudo apt update && sudo apt install -y libfuse2');
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Verify the AppImage was created
|
|
825
|
+
if (!existsSync(appImagePath)) {
|
|
826
|
+
throw new Error(`AppImage was not created at expected path: ${appImagePath}`);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Extract and copy icon for desktop shortcut
|
|
830
|
+
const iconExtractPath = join(buildFolder, `${appFileName}.png`);
|
|
831
|
+
if (config.build.linux?.icon && existsSync(join(projectRoot, config.build.linux.icon))) {
|
|
832
|
+
const iconSourcePath = join(projectRoot, config.build.linux.icon);
|
|
833
|
+
cpSync(iconSourcePath, iconExtractPath, { dereference: true });
|
|
834
|
+
console.log(`✓ Icon extracted for desktop shortcut: ${iconExtractPath}`);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Create desktop shortcut alongside the AppImage
|
|
838
|
+
const desktopShortcutPath = join(buildFolder, `${appFileName}.desktop`);
|
|
839
|
+
const desktopShortcutContent = `[Desktop Entry]
|
|
840
|
+
Version=1.0
|
|
841
|
+
Type=Application
|
|
842
|
+
Name=${config.app.name}
|
|
843
|
+
Comment=${config.app.description || ''}
|
|
844
|
+
Exec=${appImagePath}
|
|
845
|
+
Icon=${iconExtractPath}
|
|
846
|
+
Terminal=false
|
|
847
|
+
StartupWMClass=${appFileName}
|
|
848
|
+
Categories=Utility;
|
|
849
|
+
`;
|
|
850
|
+
|
|
851
|
+
writeFileSync(desktopShortcutPath, desktopShortcutContent);
|
|
852
|
+
execSync(`chmod +x ${escapePathForTerminal(desktopShortcutPath)}`);
|
|
853
|
+
console.log(`✓ Desktop shortcut created: ${desktopShortcutPath}`);
|
|
854
|
+
|
|
855
|
+
// Clean up AppDir
|
|
856
|
+
rmSync(appDirPath, { recursive: true, force: true });
|
|
857
|
+
|
|
858
|
+
console.log(`✓ AppImage created: ${appImagePath}`);
|
|
859
|
+
return appImagePath;
|
|
860
|
+
}
|
|
861
|
+
|
|
609
862
|
// Helper function to generate usage description entries for Info.plist
|
|
610
863
|
function generateUsageDescriptions(entitlements: Record<string, boolean | string>): string {
|
|
611
864
|
const usageEntries: string[] = [];
|
|
@@ -648,286 +901,8 @@ ${schemesXml}
|
|
|
648
901
|
</array>`;
|
|
649
902
|
}
|
|
650
903
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (!command) {
|
|
654
|
-
console.error("Invalid command: ", commandArg);
|
|
655
|
-
process.exit(1);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Main execution function
|
|
659
|
-
async function main() {
|
|
660
|
-
const config = await getConfig();
|
|
661
|
-
|
|
662
|
-
const envArg =
|
|
663
|
-
process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
|
|
664
|
-
|
|
665
|
-
const targetsArg =
|
|
666
|
-
process.argv.find((arg) => arg.startsWith("--targets="))?.split("=")[1] || "";
|
|
667
|
-
|
|
668
|
-
const validEnvironments = ["dev", "canary", "stable"];
|
|
669
|
-
|
|
670
|
-
// todo (yoav): dev, canary, and stable;
|
|
671
|
-
const buildEnvironment: "dev" | "canary" | "stable" =
|
|
672
|
-
validEnvironments.includes(envArg || "dev") ? (envArg || "dev") : "dev";
|
|
673
|
-
|
|
674
|
-
// Determine build targets
|
|
675
|
-
type BuildTarget = { os: 'macos' | 'win' | 'linux', arch: 'arm64' | 'x64' };
|
|
676
|
-
|
|
677
|
-
function parseBuildTargets(): BuildTarget[] {
|
|
678
|
-
// If explicit targets provided via CLI
|
|
679
|
-
if (targetsArg) {
|
|
680
|
-
if (targetsArg === 'current') {
|
|
681
|
-
return [{ os: OS, arch: ARCH }];
|
|
682
|
-
} else if (targetsArg === 'all') {
|
|
683
|
-
return parseConfigTargets();
|
|
684
|
-
} else {
|
|
685
|
-
// Parse comma-separated targets like "macos-arm64,win-x64"
|
|
686
|
-
return targetsArg.split(',').map(target => {
|
|
687
|
-
const [os, arch] = target.trim().split('-') as [string, string];
|
|
688
|
-
if (!['macos', 'win', 'linux'].includes(os) || !['arm64', 'x64'].includes(arch)) {
|
|
689
|
-
console.error(`Invalid target: ${target}. Format should be: os-arch (e.g., macos-arm64)`);
|
|
690
|
-
process.exit(1);
|
|
691
|
-
}
|
|
692
|
-
return { os, arch } as BuildTarget;
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Default behavior: always build for current platform only
|
|
698
|
-
// This ensures predictable, fast builds unless explicitly requesting multi-platform
|
|
699
|
-
return [{ os: OS, arch: ARCH }];
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function parseConfigTargets(): BuildTarget[] {
|
|
703
|
-
// If config has targets, use them
|
|
704
|
-
if (config.build.targets && config.build.targets.length > 0) {
|
|
705
|
-
return config.build.targets.map(target => {
|
|
706
|
-
if (target === 'current') {
|
|
707
|
-
return { os: OS, arch: ARCH };
|
|
708
|
-
}
|
|
709
|
-
const [os, arch] = target.split('-') as [string, string];
|
|
710
|
-
if (!['macos', 'win', 'linux'].includes(os) || !['arm64', 'x64'].includes(arch)) {
|
|
711
|
-
console.error(`Invalid target in config: ${target}. Format should be: os-arch (e.g., macos-arm64)`);
|
|
712
|
-
process.exit(1);
|
|
713
|
-
}
|
|
714
|
-
return { os, arch } as BuildTarget;
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// If no config targets and --targets=all, use all available platforms
|
|
719
|
-
if (targetsArg === 'all') {
|
|
720
|
-
console.log('No targets specified in config, using all available platforms');
|
|
721
|
-
return [
|
|
722
|
-
{ os: 'macos', arch: 'arm64' },
|
|
723
|
-
{ os: 'macos', arch: 'x64' },
|
|
724
|
-
{ os: 'win', arch: 'x64' },
|
|
725
|
-
{ os: 'linux', arch: 'x64' },
|
|
726
|
-
{ os: 'linux', arch: 'arm64' }
|
|
727
|
-
];
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Default to current platform
|
|
731
|
-
return [{ os: OS, arch: ARCH }];
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const buildTargets = parseBuildTargets();
|
|
735
|
-
|
|
736
|
-
// Show build targets to user
|
|
737
|
-
if (buildTargets.length === 1) {
|
|
738
|
-
console.log(`Building for ${buildTargets[0].os}-${buildTargets[0].arch} (${buildEnvironment})`);
|
|
739
|
-
} else {
|
|
740
|
-
const targetList = buildTargets.map(t => `${t.os}-${t.arch}`).join(', ');
|
|
741
|
-
console.log(`Building for multiple targets: ${targetList} (${buildEnvironment})`);
|
|
742
|
-
console.log(`Running ${buildTargets.length} parallel builds...`);
|
|
743
|
-
|
|
744
|
-
// Spawn parallel build processes
|
|
745
|
-
const buildPromises = buildTargets.map(async (target) => {
|
|
746
|
-
const targetString = `${target.os}-${target.arch}`;
|
|
747
|
-
const prefix = `[${targetString}]`;
|
|
748
|
-
|
|
749
|
-
try {
|
|
750
|
-
// Try to find the electrobun binary in node_modules/.bin or use bunx
|
|
751
|
-
const electrobunBin = join(projectRoot, 'node_modules', '.bin', 'electrobun');
|
|
752
|
-
let command: string[];
|
|
753
|
-
|
|
754
|
-
if (existsSync(electrobunBin)) {
|
|
755
|
-
command = [electrobunBin, 'build', `--env=${buildEnvironment}`, `--targets=${targetString}`];
|
|
756
|
-
} else {
|
|
757
|
-
// Fallback to bunx which should resolve node_modules binaries
|
|
758
|
-
command = ['bunx', 'electrobun', 'build', `--env=${buildEnvironment}`, `--targets=${targetString}`];
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
console.log(`${prefix} Running:`, command.join(' '));
|
|
762
|
-
|
|
763
|
-
const result = await Bun.spawn(command, {
|
|
764
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
765
|
-
env: process.env,
|
|
766
|
-
cwd: projectRoot // Ensure we're in the right directory
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
// Pipe output with prefix
|
|
770
|
-
if (result.stdout) {
|
|
771
|
-
const reader = result.stdout.getReader();
|
|
772
|
-
while (true) {
|
|
773
|
-
const { done, value } = await reader.read();
|
|
774
|
-
if (done) break;
|
|
775
|
-
const text = new TextDecoder().decode(value);
|
|
776
|
-
// Add prefix to each line
|
|
777
|
-
const prefixedText = text.split('\n').map(line =>
|
|
778
|
-
line ? `${prefix} ${line}` : line
|
|
779
|
-
).join('\n');
|
|
780
|
-
process.stdout.write(prefixedText);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
if (result.stderr) {
|
|
785
|
-
const reader = result.stderr.getReader();
|
|
786
|
-
while (true) {
|
|
787
|
-
const { done, value } = await reader.read();
|
|
788
|
-
if (done) break;
|
|
789
|
-
const text = new TextDecoder().decode(value);
|
|
790
|
-
const prefixedText = text.split('\n').map(line =>
|
|
791
|
-
line ? `${prefix} ${line}` : line
|
|
792
|
-
).join('\n');
|
|
793
|
-
process.stderr.write(prefixedText);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
const exitCode = await result.exited;
|
|
798
|
-
return { target, exitCode, success: exitCode === 0 };
|
|
799
|
-
|
|
800
|
-
} catch (error) {
|
|
801
|
-
console.error(`${prefix} Failed to start build:`, error);
|
|
802
|
-
return { target, exitCode: 1, success: false, error };
|
|
803
|
-
}
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
// Wait for all builds to complete
|
|
807
|
-
const results = await Promise.allSettled(buildPromises);
|
|
808
|
-
|
|
809
|
-
// Report final results
|
|
810
|
-
console.log('\n=== Build Results ===');
|
|
811
|
-
let allSucceeded = true;
|
|
812
|
-
|
|
813
|
-
for (const result of results) {
|
|
814
|
-
if (result.status === 'fulfilled') {
|
|
815
|
-
const { target, success, exitCode } = result.value;
|
|
816
|
-
const status = success ? '✅ SUCCESS' : '❌ FAILED';
|
|
817
|
-
console.log(`${target.os}-${target.arch}: ${status} (exit code: ${exitCode})`);
|
|
818
|
-
if (!success) allSucceeded = false;
|
|
819
|
-
} else {
|
|
820
|
-
console.log(`Build rejected: ${result.reason}`);
|
|
821
|
-
allSucceeded = false;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if (!allSucceeded) {
|
|
826
|
-
console.log('\nSome builds failed. Check the output above for details.');
|
|
827
|
-
process.exit(1);
|
|
828
|
-
} else {
|
|
829
|
-
console.log('\nAll builds completed successfully! 🎉');
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
process.exit(0);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// todo (yoav): dev builds should include the branch name, and/or allow configuration via external config
|
|
836
|
-
// For now, assume single target build (we'll refactor for multi-target later)
|
|
837
|
-
const currentTarget = buildTargets[0];
|
|
838
|
-
const buildSubFolder = `${buildEnvironment}-${currentTarget.os}-${currentTarget.arch}`;
|
|
839
|
-
|
|
840
|
-
// Use target OS/ARCH for build logic (instead of current machine's OS/ARCH)
|
|
841
|
-
const targetOS = currentTarget.os;
|
|
842
|
-
const targetARCH = currentTarget.arch;
|
|
843
|
-
const targetBinExt = targetOS === 'win' ? '.exe' : '';
|
|
844
|
-
|
|
845
|
-
const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
|
|
846
|
-
|
|
847
|
-
const artifactFolder = join(
|
|
848
|
-
projectRoot,
|
|
849
|
-
config.build.artifactFolder,
|
|
850
|
-
buildSubFolder
|
|
851
|
-
);
|
|
852
|
-
|
|
853
|
-
const buildIcons = (appBundleFolderResourcesPath: string) => {
|
|
854
|
-
if (OS === 'macos' && config.build.mac.icons) {
|
|
855
|
-
const iconSourceFolder = join(projectRoot, config.build.mac.icons);
|
|
856
|
-
const iconDestPath = join(appBundleFolderResourcesPath, "AppIcon.icns");
|
|
857
|
-
if (existsSync(iconSourceFolder)) {
|
|
858
|
-
Bun.spawnSync(
|
|
859
|
-
["iconutil", "-c", "icns", "-o", iconDestPath, iconSourceFolder],
|
|
860
|
-
{
|
|
861
|
-
cwd: appBundleFolderResourcesPath,
|
|
862
|
-
stdio: ["ignore", "inherit", "inherit"],
|
|
863
|
-
env: {
|
|
864
|
-
...process.env,
|
|
865
|
-
ELECTROBUN_BUILD_ENV: buildEnvironment,
|
|
866
|
-
},
|
|
867
|
-
}
|
|
868
|
-
);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
};
|
|
872
|
-
|
|
873
|
-
function escapePathForTerminal(filePath: string) {
|
|
874
|
-
// List of special characters to escape
|
|
875
|
-
const specialChars = [
|
|
876
|
-
" ",
|
|
877
|
-
"(",
|
|
878
|
-
")",
|
|
879
|
-
"&",
|
|
880
|
-
"|",
|
|
881
|
-
";",
|
|
882
|
-
"<",
|
|
883
|
-
">",
|
|
884
|
-
"`",
|
|
885
|
-
"\\",
|
|
886
|
-
'"',
|
|
887
|
-
"'",
|
|
888
|
-
"$",
|
|
889
|
-
"*",
|
|
890
|
-
"?",
|
|
891
|
-
"[",
|
|
892
|
-
"]",
|
|
893
|
-
"#",
|
|
894
|
-
];
|
|
895
|
-
|
|
896
|
-
let escapedPath = "";
|
|
897
|
-
for (const char of filePath) {
|
|
898
|
-
if (specialChars.includes(char)) {
|
|
899
|
-
escapedPath += `\\${char}`;
|
|
900
|
-
} else {
|
|
901
|
-
escapedPath += char;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
return escapedPath;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
function sanitizeVolumeNameForHdiutil(volumeName: string) {
|
|
909
|
-
// Remove or replace characters that cause issues with hdiutil volume mounting
|
|
910
|
-
// Parentheses and other special characters can cause "Operation not permitted" errors
|
|
911
|
-
return volumeName.replace(/[()]/g, '');
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// MyApp
|
|
915
|
-
|
|
916
|
-
// const appName = config.app.name.replace(/\s/g, '-').toLowerCase();
|
|
917
|
-
|
|
918
|
-
const appFileName = (
|
|
919
|
-
buildEnvironment === "stable"
|
|
920
|
-
? config.app.name
|
|
921
|
-
: `${config.app.name}-${buildEnvironment}`
|
|
922
|
-
)
|
|
923
|
-
.replace(/\s/g, "")
|
|
924
|
-
.replace(/\./g, "-");
|
|
925
|
-
const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName;
|
|
926
|
-
|
|
927
|
-
// const logPath = `/Library/Logs/Electrobun/ExampleApp/dev/out.log`;
|
|
928
|
-
|
|
929
|
-
let proc = null;
|
|
930
|
-
|
|
904
|
+
// Execute command handling
|
|
905
|
+
(async () => {
|
|
931
906
|
if (commandArg === "init") {
|
|
932
907
|
await (async () => {
|
|
933
908
|
const secondArg = process.argv[indexOfElectrobun + 2];
|
|
@@ -1041,13 +1016,116 @@ if (commandArg === "init") {
|
|
|
1041
1016
|
console.log("🎉 Happy building with Electrobun!");
|
|
1042
1017
|
})();
|
|
1043
1018
|
} else if (commandArg === "build") {
|
|
1019
|
+
// Get config
|
|
1020
|
+
const config = await getConfig();
|
|
1021
|
+
|
|
1022
|
+
// Get environment
|
|
1023
|
+
const envArg = process.argv.find((arg) => arg.startsWith("--env="))?.split("=")[1] || "";
|
|
1024
|
+
const buildEnvironment = ["dev", "canary", "stable"].includes(envArg) ? envArg : "dev";
|
|
1025
|
+
|
|
1026
|
+
// Determine current platform as default target
|
|
1027
|
+
const currentTarget = { os: OS, arch: ARCH };
|
|
1028
|
+
|
|
1029
|
+
// Set up build variables
|
|
1030
|
+
const targetOS = currentTarget.os;
|
|
1031
|
+
const targetARCH = currentTarget.arch;
|
|
1032
|
+
const targetBinExt = targetOS === 'win' ? '.exe' : '';
|
|
1033
|
+
const appFileName = `${config.app.name.replace(/ /g, "")}-${buildEnvironment}`;
|
|
1034
|
+
const buildSubFolder = `${buildEnvironment}-${currentTarget.os}-${currentTarget.arch}`;
|
|
1035
|
+
const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
|
|
1036
|
+
const bundleFileName = targetOS === 'macos' ? `${appFileName}.app` : appFileName;
|
|
1037
|
+
const artifactFolder = join(projectRoot, config.build.artifactFolder, buildSubFolder);
|
|
1038
|
+
|
|
1044
1039
|
// Ensure core binaries are available for the target platform before starting build
|
|
1045
1040
|
await ensureCoreDependencies(currentTarget.os, currentTarget.arch);
|
|
1046
1041
|
|
|
1047
1042
|
// Get platform-specific paths for the current target
|
|
1048
1043
|
const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);
|
|
1049
1044
|
|
|
1050
|
-
//
|
|
1045
|
+
// Helper functions
|
|
1046
|
+
const sanitizeVolumeNameForHdiutil = (name: string) => {
|
|
1047
|
+
return name.replace(/[^a-zA-Z0-9 ]/g, '').trim();
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
// Helper to run lifecycle hook scripts
|
|
1051
|
+
const runHook = (hookName: keyof typeof config.scripts, extraEnv: Record<string, string> = {}) => {
|
|
1052
|
+
const hookScript = config.scripts[hookName];
|
|
1053
|
+
if (!hookScript) return;
|
|
1054
|
+
|
|
1055
|
+
console.log(`Running ${hookName} script:`, hookScript);
|
|
1056
|
+
// Use host platform's bun binary for running scripts, not target platform's
|
|
1057
|
+
const hostPaths = getPlatformPaths(OS, ARCH);
|
|
1058
|
+
|
|
1059
|
+
const result = Bun.spawnSync([hostPaths.BUN_BINARY, hookScript], {
|
|
1060
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
1061
|
+
cwd: projectRoot,
|
|
1062
|
+
env: {
|
|
1063
|
+
...process.env,
|
|
1064
|
+
ELECTROBUN_BUILD_ENV: buildEnvironment,
|
|
1065
|
+
ELECTROBUN_OS: targetOS,
|
|
1066
|
+
ELECTROBUN_ARCH: targetARCH,
|
|
1067
|
+
ELECTROBUN_BUILD_DIR: buildFolder,
|
|
1068
|
+
ELECTROBUN_APP_NAME: appFileName,
|
|
1069
|
+
ELECTROBUN_APP_VERSION: config.app.version,
|
|
1070
|
+
ELECTROBUN_APP_IDENTIFIER: config.app.identifier,
|
|
1071
|
+
ELECTROBUN_ARTIFACT_DIR: artifactFolder,
|
|
1072
|
+
...extraEnv,
|
|
1073
|
+
},
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
if (result.exitCode !== 0) {
|
|
1077
|
+
console.error(`${hookName} script failed with exit code:`, result.exitCode);
|
|
1078
|
+
if (result.stderr) {
|
|
1079
|
+
console.error("stderr:", result.stderr.toString());
|
|
1080
|
+
}
|
|
1081
|
+
console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);
|
|
1082
|
+
console.error("Script path:", hookScript);
|
|
1083
|
+
console.error("Working directory:", projectRoot);
|
|
1084
|
+
process.exit(1);
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
const buildIcons = (appBundleFolderResourcesPath: string) => {
|
|
1089
|
+
// Platform-specific icon handling
|
|
1090
|
+
if (targetOS === 'macos' && config.build.mac?.icon) {
|
|
1091
|
+
const iconPath = join(projectRoot, config.build.mac.icon);
|
|
1092
|
+
if (existsSync(iconPath)) {
|
|
1093
|
+
const targetIconPath = join(appBundleFolderResourcesPath, "AppIcon.icns");
|
|
1094
|
+
cpSync(iconPath, targetIconPath, { dereference: true });
|
|
1095
|
+
}
|
|
1096
|
+
} else if (targetOS === 'linux' && config.build.linux?.icon) {
|
|
1097
|
+
const iconSourcePath = join(projectRoot, config.build.linux.icon);
|
|
1098
|
+
if (existsSync(iconSourcePath)) {
|
|
1099
|
+
const standardIconPath = join(appBundleFolderResourcesPath, 'appIcon.png');
|
|
1100
|
+
|
|
1101
|
+
// Ensure Resources directory exists
|
|
1102
|
+
mkdirSync(appBundleFolderResourcesPath, { recursive: true });
|
|
1103
|
+
|
|
1104
|
+
// Copy the icon to standard location
|
|
1105
|
+
cpSync(iconSourcePath, standardIconPath, { dereference: true });
|
|
1106
|
+
console.log(`Copied Linux icon from ${iconSourcePath} to ${standardIconPath}`);
|
|
1107
|
+
|
|
1108
|
+
// Also copy icon for the extractor (expects it in Resources/app/icon.png before ASAR packaging)
|
|
1109
|
+
const extractorIconPath = join(appBundleFolderResourcesPath, 'app', 'icon.png');
|
|
1110
|
+
mkdirSync(join(appBundleFolderResourcesPath, 'app'), { recursive: true });
|
|
1111
|
+
cpSync(iconSourcePath, extractorIconPath, { dereference: true });
|
|
1112
|
+
console.log(`Copied Linux icon for extractor from ${iconSourcePath} to ${extractorIconPath}`);
|
|
1113
|
+
} else {
|
|
1114
|
+
console.log(`WARNING: Linux icon not found: ${iconSourcePath}`);
|
|
1115
|
+
}
|
|
1116
|
+
} else if (targetOS === 'win' && config.build.win?.icon) {
|
|
1117
|
+
const iconPath = join(projectRoot, config.build.win.icon);
|
|
1118
|
+
if (existsSync(iconPath)) {
|
|
1119
|
+
const targetIconPath = join(appBundleFolderResourcesPath, "app.ico");
|
|
1120
|
+
cpSync(iconPath, targetIconPath, { dereference: true });
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
// Run preBuild hook before anything starts
|
|
1126
|
+
runHook('preBuild');
|
|
1127
|
+
|
|
1128
|
+
// refresh build folder
|
|
1051
1129
|
if (existsSync(buildFolder)) {
|
|
1052
1130
|
rmdirSync(buildFolder, { recursive: true });
|
|
1053
1131
|
}
|
|
@@ -1158,6 +1236,59 @@ if (commandArg === "init") {
|
|
|
1158
1236
|
dereference: true,
|
|
1159
1237
|
});
|
|
1160
1238
|
|
|
1239
|
+
// On Windows, ensure launcher has .exe extension
|
|
1240
|
+
// Bun's cpSync on Windows may create files without .exe despite the destination path having it
|
|
1241
|
+
if (targetOS === 'win') {
|
|
1242
|
+
const launcherWithoutExt = join(appBundleMacOSPath, "launcher");
|
|
1243
|
+
|
|
1244
|
+
// Use PowerShell to force rename and add .exe extension
|
|
1245
|
+
// This bypasses Bun's PATHEXT behavior that treats launcher and launcher.exe as the same
|
|
1246
|
+
try {
|
|
1247
|
+
execSync(`powershell -Command "if (Test-Path '${launcherWithoutExt}') { Rename-Item -Path '${launcherWithoutExt}' -NewName 'launcher.exe' -Force }"`, { stdio: 'pipe' });
|
|
1248
|
+
console.log(`Ensured launcher has .exe extension on Windows`);
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
console.warn(`Warning: Could not rename launcher to launcher.exe: ${error}`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Embed icon into launcher.exe on Windows
|
|
1255
|
+
if (targetOS === 'win' && config.build.win?.icon) {
|
|
1256
|
+
const iconSourcePath = config.build.win.icon.startsWith('/') || config.build.win.icon.match(/^[a-zA-Z]:/)
|
|
1257
|
+
? config.build.win.icon
|
|
1258
|
+
: join(projectRoot, config.build.win.icon);
|
|
1259
|
+
|
|
1260
|
+
if (existsSync(iconSourcePath)) {
|
|
1261
|
+
console.log(`Embedding icon into launcher.exe: ${iconSourcePath}`);
|
|
1262
|
+
try {
|
|
1263
|
+
let iconPath = iconSourcePath;
|
|
1264
|
+
|
|
1265
|
+
// Convert PNG to ICO if needed
|
|
1266
|
+
if (iconSourcePath.toLowerCase().endsWith('.png')) {
|
|
1267
|
+
const pngToIco = (await import('png-to-ico')).default;
|
|
1268
|
+
const tempIcoPath = join(buildFolder, 'temp-launcher-icon.ico');
|
|
1269
|
+
const icoBuffer = await pngToIco(iconSourcePath);
|
|
1270
|
+
writeFileSync(tempIcoPath, icoBuffer);
|
|
1271
|
+
iconPath = tempIcoPath;
|
|
1272
|
+
console.log(`Converted PNG to ICO format for launcher: ${tempIcoPath}`);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// Use rcedit to embed the icon into launcher.exe
|
|
1276
|
+
const rcedit = (await import('rcedit')).default;
|
|
1277
|
+
await rcedit(bunCliLauncherDestination, {
|
|
1278
|
+
icon: iconPath
|
|
1279
|
+
});
|
|
1280
|
+
console.log(`Successfully embedded icon into launcher.exe`);
|
|
1281
|
+
|
|
1282
|
+
// Clean up temp ICO file
|
|
1283
|
+
if (iconPath !== iconSourcePath && existsSync(iconPath)) {
|
|
1284
|
+
unlinkSync(iconPath);
|
|
1285
|
+
}
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
console.warn(`Warning: Failed to embed icon into launcher.exe: ${error}`);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1161
1292
|
cpSync(targetPaths.MAIN_JS, join(appBundleFolderResourcesPath, 'main.js'), { dereference: true });
|
|
1162
1293
|
|
|
1163
1294
|
// Bun runtime binary
|
|
@@ -1173,6 +1304,44 @@ if (commandArg === "init") {
|
|
|
1173
1304
|
}
|
|
1174
1305
|
cpSync(bunBinarySourcePath, bunBinaryDestInBundlePath, { dereference: true });
|
|
1175
1306
|
|
|
1307
|
+
// Embed icon into bun.exe on Windows
|
|
1308
|
+
if (targetOS === 'win' && config.build.win?.icon) {
|
|
1309
|
+
const iconSourcePath = config.build.win.icon.startsWith('/') || config.build.win.icon.match(/^[a-zA-Z]:/)
|
|
1310
|
+
? config.build.win.icon
|
|
1311
|
+
: join(projectRoot, config.build.win.icon);
|
|
1312
|
+
|
|
1313
|
+
if (existsSync(iconSourcePath)) {
|
|
1314
|
+
console.log(`Embedding icon into bun.exe: ${iconSourcePath}`);
|
|
1315
|
+
try {
|
|
1316
|
+
let iconPath = iconSourcePath;
|
|
1317
|
+
|
|
1318
|
+
// Convert PNG to ICO if needed
|
|
1319
|
+
if (iconSourcePath.toLowerCase().endsWith('.png')) {
|
|
1320
|
+
const pngToIco = (await import('png-to-ico')).default;
|
|
1321
|
+
const tempIcoPath = join(buildFolder, 'temp-bun-icon.ico');
|
|
1322
|
+
const icoBuffer = await pngToIco(iconSourcePath);
|
|
1323
|
+
writeFileSync(tempIcoPath, icoBuffer);
|
|
1324
|
+
iconPath = tempIcoPath;
|
|
1325
|
+
console.log(`Converted PNG to ICO format for bun.exe: ${tempIcoPath}`);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// Use rcedit to embed the icon into bun.exe
|
|
1329
|
+
const rcedit = (await import('rcedit')).default;
|
|
1330
|
+
await rcedit(bunBinaryDestInBundlePath, {
|
|
1331
|
+
icon: iconPath
|
|
1332
|
+
});
|
|
1333
|
+
console.log(`Successfully embedded icon into bun.exe`);
|
|
1334
|
+
|
|
1335
|
+
// Clean up temp ICO file
|
|
1336
|
+
if (iconPath !== iconSourcePath && existsSync(iconPath)) {
|
|
1337
|
+
unlinkSync(iconPath);
|
|
1338
|
+
}
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
console.warn(`Warning: Failed to embed icon into bun.exe: ${error}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1176
1345
|
// copy native wrapper dynamic library
|
|
1177
1346
|
if (targetOS === 'macos') {
|
|
1178
1347
|
const nativeWrapperMacosSource = targetPaths.NATIVE_WRAPPER_MACOS;
|
|
@@ -1218,6 +1387,29 @@ if (commandArg === "init") {
|
|
|
1218
1387
|
} else {
|
|
1219
1388
|
throw new Error(`Native wrapper not found: ${nativeWrapperLinuxSource}`);
|
|
1220
1389
|
}
|
|
1390
|
+
|
|
1391
|
+
// Copy icon if specified for Linux to a standard location
|
|
1392
|
+
if (config.build.linux?.icon) {
|
|
1393
|
+
const iconSourcePath = join(projectRoot, config.build.linux.icon);
|
|
1394
|
+
if (existsSync(iconSourcePath)) {
|
|
1395
|
+
const standardIconPath = join(appBundleFolderResourcesPath, 'appIcon.png');
|
|
1396
|
+
|
|
1397
|
+
// Ensure Resources directory exists
|
|
1398
|
+
mkdirSync(appBundleFolderResourcesPath, { recursive: true });
|
|
1399
|
+
|
|
1400
|
+
// Copy the icon to standard location
|
|
1401
|
+
cpSync(iconSourcePath, standardIconPath, { dereference: true });
|
|
1402
|
+
console.log(`Copied Linux icon from ${iconSourcePath} to ${standardIconPath}`);
|
|
1403
|
+
|
|
1404
|
+
// Also copy icon for the extractor (expects it in Resources/app/icon.png before ASAR packaging)
|
|
1405
|
+
const extractorIconPath = join(appBundleFolderResourcesPath, 'app', 'icon.png');
|
|
1406
|
+
mkdirSync(join(appBundleFolderResourcesPath, 'app'), { recursive: true });
|
|
1407
|
+
cpSync(iconSourcePath, extractorIconPath, { dereference: true });
|
|
1408
|
+
console.log(`Copied Linux icon for extractor from ${iconSourcePath} to ${extractorIconPath}`);
|
|
1409
|
+
} else {
|
|
1410
|
+
console.log(`WARNING: Linux icon not found: ${iconSourcePath}`);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1221
1413
|
}
|
|
1222
1414
|
|
|
1223
1415
|
|
|
@@ -1577,6 +1769,7 @@ if (commandArg === "init") {
|
|
|
1577
1769
|
continue;
|
|
1578
1770
|
}
|
|
1579
1771
|
}
|
|
1772
|
+
|
|
1580
1773
|
// Copy assets like html, css, images, and other files
|
|
1581
1774
|
for (const relSource in config.build.copy) {
|
|
1582
1775
|
const source = join(projectRoot, relSource);
|
|
@@ -1605,39 +1798,7 @@ if (commandArg === "init") {
|
|
|
1605
1798
|
|
|
1606
1799
|
|
|
1607
1800
|
// Run postBuild script
|
|
1608
|
-
|
|
1609
|
-
console.log("Running postBuild script:", config.scripts.postBuild);
|
|
1610
|
-
// Use host platform's bun binary for running scripts, not target platform's
|
|
1611
|
-
const hostPaths = getPlatformPaths(OS, ARCH);
|
|
1612
|
-
|
|
1613
|
-
const result = Bun.spawnSync([hostPaths.BUN_BINARY, config.scripts.postBuild], {
|
|
1614
|
-
stdio: ["ignore", "inherit", "inherit"],
|
|
1615
|
-
cwd: projectRoot, // Add cwd to ensure script runs from project root
|
|
1616
|
-
env: {
|
|
1617
|
-
...process.env,
|
|
1618
|
-
ELECTROBUN_BUILD_ENV: buildEnvironment,
|
|
1619
|
-
ELECTROBUN_OS: targetOS, // Use target OS for environment variables
|
|
1620
|
-
ELECTROBUN_ARCH: targetARCH, // Use target ARCH for environment variables
|
|
1621
|
-
ELECTROBUN_BUILD_DIR: buildFolder,
|
|
1622
|
-
ELECTROBUN_APP_NAME: appFileName,
|
|
1623
|
-
ELECTROBUN_APP_VERSION: config.app.version,
|
|
1624
|
-
ELECTROBUN_APP_IDENTIFIER: config.app.identifier,
|
|
1625
|
-
ELECTROBUN_ARTIFACT_DIR: artifactFolder,
|
|
1626
|
-
},
|
|
1627
|
-
});
|
|
1628
|
-
|
|
1629
|
-
if (result.exitCode !== 0) {
|
|
1630
|
-
console.error("postBuild script failed with exit code:", result.exitCode);
|
|
1631
|
-
if (result.stderr) {
|
|
1632
|
-
console.error("stderr:", result.stderr.toString());
|
|
1633
|
-
}
|
|
1634
|
-
// Also log which bun binary we're trying to use
|
|
1635
|
-
console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);
|
|
1636
|
-
console.error("Script path:", config.scripts.postBuild);
|
|
1637
|
-
console.error("Working directory:", projectRoot);
|
|
1638
|
-
process.exit(1);
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1801
|
+
runHook('postBuild');
|
|
1641
1802
|
|
|
1642
1803
|
// Pack app resources into ASAR archive if enabled
|
|
1643
1804
|
if (config.build.useAsar) {
|
|
@@ -1778,6 +1939,23 @@ if (commandArg === "init") {
|
|
|
1778
1939
|
versionJsonContent
|
|
1779
1940
|
);
|
|
1780
1941
|
|
|
1942
|
+
// build.json inside the app bundle - runtime build configuration
|
|
1943
|
+
const platformConfig = targetOS === 'macos' ? config.build?.mac :
|
|
1944
|
+
targetOS === 'win' ? config.build?.win :
|
|
1945
|
+
config.build?.linux;
|
|
1946
|
+
|
|
1947
|
+
const bundlesCEF = platformConfig?.bundleCEF ?? false;
|
|
1948
|
+
|
|
1949
|
+
const buildJsonContent = JSON.stringify({
|
|
1950
|
+
defaultRenderer: platformConfig?.defaultRenderer ?? 'native',
|
|
1951
|
+
availableRenderers: bundlesCEF ? ['native', 'cef'] : ['native'],
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
await Bun.write(
|
|
1955
|
+
join(appBundleFolderResourcesPath, "build.json"),
|
|
1956
|
+
buildJsonContent
|
|
1957
|
+
);
|
|
1958
|
+
|
|
1781
1959
|
// todo (yoav): add these to config
|
|
1782
1960
|
// Only codesign/notarize when building macOS targets on macOS host
|
|
1783
1961
|
const shouldCodesign =
|
|
@@ -1801,8 +1979,113 @@ if (commandArg === "init") {
|
|
|
1801
1979
|
} else {
|
|
1802
1980
|
console.log("skipping notarization");
|
|
1803
1981
|
}
|
|
1982
|
+
|
|
1983
|
+
const artifactsToUpload = [];
|
|
1984
|
+
|
|
1985
|
+
console.log(`DEBUG: Checking for Linux AppImage creation - targetOS: ${targetOS}, buildEnvironment: ${buildEnvironment}`);
|
|
1986
|
+
|
|
1987
|
+
// Linux AppImage creation (for all build environments including dev)
|
|
1988
|
+
if (targetOS === 'linux') {
|
|
1989
|
+
console.log("DEBUG: Creating Linux AppImage...");
|
|
1990
|
+
// Ensure AppImage tooling is available
|
|
1991
|
+
await ensureAppImageTooling();
|
|
1992
|
+
|
|
1993
|
+
// Create AppImage from the app bundle (for both dev and production builds)
|
|
1994
|
+
console.log(`🔍 CALLING createAppImage with appBundleFolderPath: ${appBundleFolderPath}`);
|
|
1995
|
+
console.log(`🔍 buildFolder: ${buildFolder}`);
|
|
1996
|
+
console.log(`🔍 appFileName: ${appFileName}`);
|
|
1997
|
+
const appImagePath = await createAppImage(
|
|
1998
|
+
appBundleFolderPath,
|
|
1999
|
+
appFileName,
|
|
2000
|
+
config,
|
|
2001
|
+
buildFolder
|
|
2002
|
+
);
|
|
2003
|
+
|
|
2004
|
+
console.log(`✓ Linux AppImage created at: ${appImagePath}`);
|
|
2005
|
+
|
|
2006
|
+
// Only create compressed tar for non-dev builds
|
|
2007
|
+
if (buildEnvironment !== "dev") {
|
|
2008
|
+
// For Linux, create a compressed tar containing:
|
|
2009
|
+
// 1. The AppImage
|
|
2010
|
+
// 2. Desktop shortcut file
|
|
2011
|
+
// 3. Icon file
|
|
2012
|
+
// 4. Metadata
|
|
2013
|
+
|
|
2014
|
+
const tempDirName = `${appFileName}-installer-contents`;
|
|
2015
|
+
const tempDirPath = join(buildFolder, tempDirName);
|
|
2016
|
+
|
|
2017
|
+
// Clean up any existing temp directory
|
|
2018
|
+
if (existsSync(tempDirPath)) {
|
|
2019
|
+
rmSync(tempDirPath, { recursive: true });
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// Create temp directory structure
|
|
2023
|
+
mkdirSync(tempDirPath, { recursive: true });
|
|
2024
|
+
const innerDirPath = join(tempDirPath, appFileName);
|
|
2025
|
+
mkdirSync(innerDirPath, { recursive: true });
|
|
2026
|
+
|
|
2027
|
+
// Copy AppImage
|
|
2028
|
+
const appImageDestPath = join(innerDirPath, `${appFileName}.AppImage`);
|
|
2029
|
+
cpSync(appImagePath, appImageDestPath, { dereference: true });
|
|
2030
|
+
|
|
2031
|
+
// Copy desktop shortcut and icon (they were created alongside the AppImage)
|
|
2032
|
+
const desktopPath = join(buildFolder, `${appFileName}.desktop`);
|
|
2033
|
+
const iconPath = join(buildFolder, `${appFileName}.png`);
|
|
2034
|
+
|
|
2035
|
+
if (existsSync(desktopPath)) {
|
|
2036
|
+
cpSync(desktopPath, join(innerDirPath, `${appFileName}.desktop`));
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
if (existsSync(iconPath)) {
|
|
2040
|
+
cpSync(iconPath, join(innerDirPath, `${appFileName}.png`));
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// Create metadata file
|
|
2044
|
+
const metadata = {
|
|
2045
|
+
identifier: config.app.identifier,
|
|
2046
|
+
name: config.app.name,
|
|
2047
|
+
version: config.app.version,
|
|
2048
|
+
channel: buildEnvironment
|
|
2049
|
+
};
|
|
2050
|
+
writeFileSync(join(innerDirPath, 'metadata.json'), JSON.stringify(metadata, null, 2));
|
|
2051
|
+
|
|
2052
|
+
const appImageTarPath = join(buildFolder, `${appFileName}.tar`);
|
|
2053
|
+
console.log(`Creating tar of installer contents: ${appImageTarPath}`);
|
|
2054
|
+
|
|
2055
|
+
// Tar the inner directory
|
|
2056
|
+
await tar.create(
|
|
2057
|
+
{
|
|
2058
|
+
file: appImageTarPath,
|
|
2059
|
+
cwd: tempDirPath,
|
|
2060
|
+
gzip: false, // We'll compress with zstd after
|
|
2061
|
+
},
|
|
2062
|
+
[appFileName]
|
|
2063
|
+
);
|
|
2064
|
+
|
|
2065
|
+
// Clean up temp directory
|
|
2066
|
+
rmSync(tempDirPath, { recursive: true });
|
|
2067
|
+
|
|
2068
|
+
// Compress with Zstandard
|
|
2069
|
+
console.log(`Compressing tar with zstd...`);
|
|
2070
|
+
const uncompressedTarData = readFileSync(appImageTarPath);
|
|
2071
|
+
await ZstdInit().then(async ({ ZstdSimple }) => {
|
|
2072
|
+
const data = new Uint8Array(uncompressedTarData);
|
|
2073
|
+
const compressionLevel = 22;
|
|
2074
|
+
const compressedData = ZstdSimple.compress(data, compressionLevel);
|
|
2075
|
+
const compressedPath = `${appImageTarPath}.zst`;
|
|
2076
|
+
writeFileSync(compressedPath, compressedData);
|
|
2077
|
+
console.log(`✓ Created compressed tar: ${compressedPath} (${(compressedData.length / 1024 / 1024).toFixed(2)} MB)`);
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
// Remove uncompressed tar
|
|
2081
|
+
unlinkSync(appImageTarPath);
|
|
2082
|
+
|
|
2083
|
+
// Add AppImage to artifacts for distribution (for direct download)
|
|
2084
|
+
artifactsToUpload.push(appImagePath);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
1804
2088
|
if (buildEnvironment !== "dev") {
|
|
1805
|
-
const artifactsToUpload = [];
|
|
1806
2089
|
// zstd wasm https://github.com/OneIdentity/zstd-js
|
|
1807
2090
|
// tar https://github.com/isaacs/node-tar
|
|
1808
2091
|
|
|
@@ -1820,61 +2103,73 @@ if (commandArg === "init") {
|
|
|
1820
2103
|
const platformSuffix = `-${targetOS}-${targetARCH}`;
|
|
1821
2104
|
const tarPath = `${appBundleFolderPath}.tar`;
|
|
1822
2105
|
|
|
1823
|
-
//
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
// Note: The playground app bundle is around 48MB.
|
|
1837
|
-
// compression on m1 max with 64GB ram:
|
|
1838
|
-
// brotli: 1min 38s, 48MB -> 11.1MB
|
|
1839
|
-
// zstd: 15s, 48MB -> 12.1MB
|
|
1840
|
-
// zstd is the clear winner here. dev iteration speed gain of 1min 15s per build is much more valubale
|
|
1841
|
-
// than saving 1 more MB of space/bandwidth.
|
|
2106
|
+
// For Linux, we've already created the tar in the AppImage section above
|
|
2107
|
+
// For macOS/Windows, tar the signed and notarized app bundle
|
|
2108
|
+
if (targetOS !== 'linux') {
|
|
2109
|
+
await tar.c(
|
|
2110
|
+
{
|
|
2111
|
+
gzip: false,
|
|
2112
|
+
file: tarPath,
|
|
2113
|
+
cwd: buildFolder,
|
|
2114
|
+
},
|
|
2115
|
+
[basename(appBundleFolderPath)]
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
1842
2118
|
|
|
1843
2119
|
let compressedTarPath = `${tarPath}.zst`;
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2120
|
+
|
|
2121
|
+
// For Linux, skip compression as we already have the compressed tar
|
|
2122
|
+
if (targetOS === 'linux') {
|
|
2123
|
+
console.log("Linux tar.zst already created, skipping general compression step");
|
|
2124
|
+
// compressedTarPath already points to the right file
|
|
2125
|
+
} else {
|
|
2126
|
+
const tarball = Bun.file(tarPath);
|
|
2127
|
+
const tarBuffer = await tarball.arrayBuffer();
|
|
2128
|
+
|
|
2129
|
+
// Note: The playground app bundle is around 48MB.
|
|
2130
|
+
// compression on m1 max with 64GB ram:
|
|
2131
|
+
// brotli: 1min 38s, 48MB -> 11.1MB
|
|
2132
|
+
// zstd: 15s, 48MB -> 12.1MB
|
|
2133
|
+
// zstd is the clear winner here. dev iteration speed gain of 1min 15s per build is much more valubale
|
|
2134
|
+
// than saving 1 more MB of space/bandwidth.
|
|
2135
|
+
|
|
2136
|
+
artifactsToUpload.push(compressedTarPath);
|
|
2137
|
+
|
|
2138
|
+
// zstd compress tarball
|
|
2139
|
+
// todo (yoav): consider using c bindings for zstd for speed instead of wasm
|
|
2140
|
+
// we already have it in the bsdiff binary
|
|
2141
|
+
console.log("compressing tarball...");
|
|
2142
|
+
await ZstdInit().then(async ({ ZstdSimple, ZstdStream }) => {
|
|
2143
|
+
// Note: Simple is much faster than stream, but stream is better for large files
|
|
2144
|
+
// todo (yoav): consider a file size cutoff to switch to stream instead of simple.
|
|
2145
|
+
const useStream = tarball.size > 100 * 1024 * 1024;
|
|
1858
2146
|
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
data
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2147
|
+
if (tarball.size > 0) {
|
|
2148
|
+
// Uint8 array filestream of the tar file
|
|
2149
|
+
const data = new Uint8Array(tarBuffer);
|
|
2150
|
+
|
|
2151
|
+
const compressionLevel = 22; // Maximum compression - now safe with stripped CEF libraries
|
|
2152
|
+
const compressedData = ZstdSimple.compress(data, compressionLevel);
|
|
2153
|
+
|
|
2154
|
+
console.log(
|
|
2155
|
+
"compressed",
|
|
2156
|
+
data.length,
|
|
2157
|
+
"bytes",
|
|
2158
|
+
"from",
|
|
2159
|
+
tarBuffer.byteLength,
|
|
2160
|
+
"bytes"
|
|
2161
|
+
);
|
|
1870
2162
|
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2163
|
+
await Bun.write(compressedTarPath, compressedData);
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
1874
2167
|
|
|
1875
|
-
//
|
|
1876
|
-
//
|
|
1877
|
-
|
|
2168
|
+
// For macOS/Windows, delete the original app bundle since we've tarred it
|
|
2169
|
+
// For Linux, the app bundle was already converted to AppImage, so the directory might not exist
|
|
2170
|
+
if (targetOS !== 'linux') {
|
|
2171
|
+
rmdirSync(appBundleFolderPath, { recursive: true });
|
|
2172
|
+
}
|
|
1878
2173
|
|
|
1879
2174
|
const selfExtractingBundle = createAppBundle(appFileName, buildFolder, targetOS);
|
|
1880
2175
|
const compressedTarballInExtractingBundlePath = join(
|
|
@@ -1901,6 +2196,10 @@ if (commandArg === "init") {
|
|
|
1901
2196
|
InfoPlistContents
|
|
1902
2197
|
);
|
|
1903
2198
|
|
|
2199
|
+
// Run postWrap hook after self-extracting bundle is created, before code signing
|
|
2200
|
+
// This is where you can add files to the wrapper (e.g., for liquid glass support)
|
|
2201
|
+
runHook('postWrap', { ELECTROBUN_WRAPPER_BUNDLE_PATH: selfExtractingBundle.appBundleFolderPath });
|
|
2202
|
+
|
|
1904
2203
|
if (shouldCodesign) {
|
|
1905
2204
|
codesignAppBundle(
|
|
1906
2205
|
selfExtractingBundle.appBundleFolderPath,
|
|
@@ -1989,7 +2288,9 @@ if (commandArg === "init") {
|
|
|
1989
2288
|
appFileName,
|
|
1990
2289
|
targetPaths,
|
|
1991
2290
|
buildEnvironment,
|
|
1992
|
-
hash
|
|
2291
|
+
hash,
|
|
2292
|
+
config,
|
|
2293
|
+
projectRoot
|
|
1993
2294
|
);
|
|
1994
2295
|
|
|
1995
2296
|
// Wrap Windows installer files in zip for distribution
|
|
@@ -1999,48 +2300,19 @@ if (commandArg === "init") {
|
|
|
1999
2300
|
// Also keep the raw exe for backwards compatibility (optional)
|
|
2000
2301
|
// artifactsToUpload.push(selfExtractingExePath);
|
|
2001
2302
|
} else if (targetOS === 'linux') {
|
|
2002
|
-
//
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
Name=${config.package?.name || config.app.name}
|
|
2007
|
-
Comment=${config.package?.description || config.app.description || ''}
|
|
2008
|
-
Exec=${appFileName}
|
|
2009
|
-
Icon=${appFileName}
|
|
2010
|
-
Terminal=false
|
|
2011
|
-
StartupWMClass=${appFileName}
|
|
2012
|
-
Categories=Application;
|
|
2013
|
-
`;
|
|
2014
|
-
|
|
2015
|
-
const desktopFilePath = join(appBundleFolderPath, `${appFileName}.desktop`);
|
|
2016
|
-
writeFileSync(desktopFilePath, desktopFileContent);
|
|
2017
|
-
|
|
2018
|
-
// Make desktop file executable
|
|
2019
|
-
execSync(`chmod +x ${escapePathForTerminal(desktopFilePath)}`);
|
|
2020
|
-
|
|
2021
|
-
// Note: No longer creating shell script wrapper - users can run bin/launcher directly
|
|
2022
|
-
// or use the symlink created by the self-extractor
|
|
2023
|
-
|
|
2024
|
-
// Create self-extracting Linux binary (similar to Windows approach)
|
|
2025
|
-
const selfExtractingLinuxPath = await createLinuxSelfExtractingBinary(
|
|
2303
|
+
// On Linux, create a self-extracting AppImage with embedded archive
|
|
2304
|
+
// Use the Linux-specific compressed tar path
|
|
2305
|
+
const linuxCompressedTarPath = join(buildFolder, `${appFileName}.tar.zst`);
|
|
2306
|
+
const selfExtractingAppImagePath = await createLinuxSelfExtractingAppImage(
|
|
2026
2307
|
buildFolder,
|
|
2027
|
-
|
|
2308
|
+
linuxCompressedTarPath,
|
|
2028
2309
|
appFileName,
|
|
2029
|
-
|
|
2030
|
-
buildEnvironment
|
|
2310
|
+
config,
|
|
2311
|
+
buildEnvironment,
|
|
2312
|
+
hash
|
|
2031
2313
|
);
|
|
2032
2314
|
|
|
2033
|
-
|
|
2034
|
-
const wrappedRunPath = await wrapInArchive(selfExtractingLinuxPath, buildFolder, 'tar.gz');
|
|
2035
|
-
artifactsToUpload.push(wrappedRunPath);
|
|
2036
|
-
|
|
2037
|
-
// Also keep the raw .run for backwards compatibility (optional)
|
|
2038
|
-
// artifactsToUpload.push(selfExtractingLinuxPath);
|
|
2039
|
-
|
|
2040
|
-
// On Linux, create a tar.gz of the bundle (for build process, not uploaded to artifacts)
|
|
2041
|
-
const linuxTarPath = join(buildFolder, `${appFileName}.tar.gz`);
|
|
2042
|
-
execSync(`tar -czf ${escapePathForTerminal(linuxTarPath)} -C ${escapePathForTerminal(buildFolder)} ${escapePathForTerminal(basename(appBundleFolderPath))}`);
|
|
2043
|
-
// Note: Not adding to artifactsToUpload - we use .tar.zst for updates and .run.tar.gz for distribution
|
|
2315
|
+
artifactsToUpload.push(selfExtractingAppImagePath);
|
|
2044
2316
|
}
|
|
2045
2317
|
}
|
|
2046
2318
|
|
|
@@ -2173,6 +2445,9 @@ Categories=Application;
|
|
|
2173
2445
|
// you'll end up with a sequence of patch files that will
|
|
2174
2446
|
}
|
|
2175
2447
|
|
|
2448
|
+
// Run postPackage hook at the very end of the build process
|
|
2449
|
+
runHook('postPackage');
|
|
2450
|
+
|
|
2176
2451
|
// NOTE: verify codesign
|
|
2177
2452
|
// codesign --verify --deep --strict --verbose=2 <app path>
|
|
2178
2453
|
|
|
@@ -2190,6 +2465,17 @@ Categories=Application;
|
|
|
2190
2465
|
// run the project in dev mode
|
|
2191
2466
|
// this runs the bundled bun binary with main.js directly
|
|
2192
2467
|
|
|
2468
|
+
// Get config for dev mode
|
|
2469
|
+
const config = await getConfig();
|
|
2470
|
+
|
|
2471
|
+
// Set up dev build variables (similar to build mode)
|
|
2472
|
+
const buildEnvironment = "dev";
|
|
2473
|
+
const currentTarget = { os: OS, arch: ARCH };
|
|
2474
|
+
const appFileName = `${config.app.name.replace(/ /g, "")}-${buildEnvironment}`;
|
|
2475
|
+
const buildSubFolder = `${buildEnvironment}-${currentTarget.os}-${currentTarget.arch}`;
|
|
2476
|
+
const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
|
|
2477
|
+
const bundleFileName = OS === 'macos' ? `${appFileName}.app` : appFileName;
|
|
2478
|
+
|
|
2193
2479
|
// Note: this cli will be a bun single-file-executable
|
|
2194
2480
|
// Note: we want to use the version of bun that's packaged with electrobun
|
|
2195
2481
|
// const bunPath = join(projectRoot, 'node_modules', '.bin', 'bun');
|
|
@@ -2208,11 +2494,25 @@ Categories=Application;
|
|
|
2208
2494
|
let mainProc;
|
|
2209
2495
|
let bundleExecPath: string;
|
|
2210
2496
|
let bundleResourcesPath: string;
|
|
2497
|
+
let isAppImage = false;
|
|
2211
2498
|
|
|
2212
2499
|
if (OS === 'macos') {
|
|
2213
2500
|
bundleExecPath = join(buildFolder, bundleFileName, "Contents", 'MacOS');
|
|
2214
2501
|
bundleResourcesPath = join(buildFolder, bundleFileName, "Contents", 'Resources');
|
|
2215
|
-
} else if (OS === 'linux'
|
|
2502
|
+
} else if (OS === 'linux') {
|
|
2503
|
+
// Check if we have an AppImage or directory bundle
|
|
2504
|
+
const appImagePath = join(buildFolder, `${bundleFileName}.AppImage`);
|
|
2505
|
+
if (existsSync(appImagePath)) {
|
|
2506
|
+
// AppImage mode
|
|
2507
|
+
bundleExecPath = appImagePath;
|
|
2508
|
+
bundleResourcesPath = join(buildFolder, bundleFileName, "Resources"); // For compatibility
|
|
2509
|
+
isAppImage = true;
|
|
2510
|
+
} else {
|
|
2511
|
+
// Directory bundle mode (fallback)
|
|
2512
|
+
bundleExecPath = join(buildFolder, bundleFileName, "bin");
|
|
2513
|
+
bundleResourcesPath = join(buildFolder, bundleFileName, "Resources");
|
|
2514
|
+
}
|
|
2515
|
+
} else if (OS === 'win') {
|
|
2216
2516
|
bundleExecPath = join(buildFolder, bundleFileName, "bin");
|
|
2217
2517
|
bundleResourcesPath = join(buildFolder, bundleFileName, "Resources");
|
|
2218
2518
|
} else {
|
|
@@ -2221,7 +2521,8 @@ Categories=Application;
|
|
|
2221
2521
|
|
|
2222
2522
|
if (OS === 'macos' || OS === 'linux') {
|
|
2223
2523
|
// For Linux dev mode, update libNativeWrapper.so based on bundleCEF setting
|
|
2224
|
-
if (OS === 'linux') {
|
|
2524
|
+
if (OS === 'linux' && !isAppImage) {
|
|
2525
|
+
// Only update libNativeWrapper for directory bundle mode
|
|
2225
2526
|
const currentLibPath = join(bundleExecPath, 'libNativeWrapper.so');
|
|
2226
2527
|
const targetPaths = getPlatformPaths('linux', ARCH);
|
|
2227
2528
|
const correctLibSource = config.build.linux?.bundleCEF
|
|
@@ -2238,12 +2539,21 @@ Categories=Application;
|
|
|
2238
2539
|
}
|
|
2239
2540
|
}
|
|
2240
2541
|
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2542
|
+
if (OS === 'linux' && isAppImage) {
|
|
2543
|
+
// For Linux AppImage mode, execute the AppImage directly
|
|
2544
|
+
console.log(`Running AppImage: ${bundleExecPath}`);
|
|
2545
|
+
mainProc = Bun.spawn([bundleExecPath], {
|
|
2546
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
2547
|
+
cwd: dirname(bundleExecPath)
|
|
2548
|
+
});
|
|
2549
|
+
} else {
|
|
2550
|
+
// Use the zig launcher for macOS and directory bundle Linux
|
|
2551
|
+
mainProc = Bun.spawn([join(bundleExecPath, 'launcher')], {
|
|
2552
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
2553
|
+
cwd: bundleExecPath
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
} else if (OS === 'win') {
|
|
2247
2557
|
// Windows: Use launcher if available, otherwise fallback to direct execution
|
|
2248
2558
|
const launcherPath = join(bundleExecPath, 'launcher.exe');
|
|
2249
2559
|
if (existsSync(launcherPath)) {
|
|
@@ -2284,7 +2594,9 @@ Categories=Application;
|
|
|
2284
2594
|
}
|
|
2285
2595
|
});
|
|
2286
2596
|
|
|
2287
|
-
}
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// Helper functions
|
|
2288
2600
|
|
|
2289
2601
|
async function getConfig() {
|
|
2290
2602
|
let loadedConfig = {};
|
|
@@ -2384,21 +2696,63 @@ async function createWindowsSelfExtractingExe(
|
|
|
2384
2696
|
appFileName: string,
|
|
2385
2697
|
targetPaths: any,
|
|
2386
2698
|
buildEnvironment: string,
|
|
2387
|
-
hash: string
|
|
2699
|
+
hash: string,
|
|
2700
|
+
config: any,
|
|
2701
|
+
projectRoot: string
|
|
2388
2702
|
): Promise<string> {
|
|
2389
2703
|
console.log("Creating Windows installer with separate archive...");
|
|
2390
|
-
|
|
2704
|
+
|
|
2391
2705
|
// Format: MyApp-Setup.exe (stable) or MyApp-Setup-canary.exe (non-stable)
|
|
2392
|
-
const setupFileName = buildEnvironment === "stable"
|
|
2706
|
+
const setupFileName = buildEnvironment === "stable"
|
|
2393
2707
|
? `${config.app.name}-Setup.exe`
|
|
2394
2708
|
: `${config.app.name}-Setup-${buildEnvironment}.exe`;
|
|
2395
|
-
|
|
2709
|
+
|
|
2396
2710
|
const outputExePath = join(buildFolder, setupFileName);
|
|
2397
|
-
|
|
2711
|
+
|
|
2398
2712
|
// Copy the extractor exe
|
|
2399
2713
|
const extractorExe = readFileSync(targetPaths.EXTRACTOR);
|
|
2400
2714
|
writeFileSync(outputExePath, extractorExe);
|
|
2401
|
-
|
|
2715
|
+
|
|
2716
|
+
// Embed icon into the wrapper EXE if provided
|
|
2717
|
+
if (config.build.win?.icon) {
|
|
2718
|
+
const iconSourcePath = config.build.win.icon.startsWith('/') || config.build.win.icon.match(/^[a-zA-Z]:/)
|
|
2719
|
+
? config.build.win.icon
|
|
2720
|
+
: join(projectRoot, config.build.win.icon);
|
|
2721
|
+
|
|
2722
|
+
if (existsSync(iconSourcePath)) {
|
|
2723
|
+
console.log(`Embedding icon into Windows installer: ${iconSourcePath}`);
|
|
2724
|
+
try {
|
|
2725
|
+
let iconPath = iconSourcePath;
|
|
2726
|
+
|
|
2727
|
+
// Convert PNG to ICO if needed
|
|
2728
|
+
if (iconSourcePath.toLowerCase().endsWith('.png')) {
|
|
2729
|
+
const pngToIco = (await import('png-to-ico')).default;
|
|
2730
|
+
const tempIcoPath = join(buildFolder, 'temp-icon.ico');
|
|
2731
|
+
const icoBuffer = await pngToIco(iconSourcePath);
|
|
2732
|
+
writeFileSync(tempIcoPath, icoBuffer);
|
|
2733
|
+
iconPath = tempIcoPath;
|
|
2734
|
+
console.log(`Converted PNG to ICO format: ${tempIcoPath}`);
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
// Use rcedit to embed the icon
|
|
2738
|
+
const rcedit = (await import('rcedit')).default;
|
|
2739
|
+
await rcedit(outputExePath, {
|
|
2740
|
+
icon: iconPath
|
|
2741
|
+
});
|
|
2742
|
+
console.log(`Successfully embedded icon into ${setupFileName}`);
|
|
2743
|
+
|
|
2744
|
+
// Clean up temp ICO file
|
|
2745
|
+
if (iconPath !== iconSourcePath && existsSync(iconPath)) {
|
|
2746
|
+
unlinkSync(iconPath);
|
|
2747
|
+
}
|
|
2748
|
+
} catch (error) {
|
|
2749
|
+
console.warn(`Warning: Failed to embed icon into Windows installer: ${error}`);
|
|
2750
|
+
}
|
|
2751
|
+
} else {
|
|
2752
|
+
console.warn(`Warning: Windows icon not found at ${iconSourcePath}`);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2402
2756
|
// Create metadata JSON file
|
|
2403
2757
|
const metadata = {
|
|
2404
2758
|
identifier: config.app.identifier,
|
|
@@ -2410,27 +2764,27 @@ async function createWindowsSelfExtractingExe(
|
|
|
2410
2764
|
const metadataFileName = setupFileName.replace('.exe', '.metadata.json');
|
|
2411
2765
|
const metadataPath = join(buildFolder, metadataFileName);
|
|
2412
2766
|
writeFileSync(metadataPath, metadataJson);
|
|
2413
|
-
|
|
2767
|
+
|
|
2414
2768
|
// Copy the compressed archive with matching name
|
|
2415
2769
|
const archiveFileName = setupFileName.replace('.exe', '.tar.zst');
|
|
2416
2770
|
const archivePath = join(buildFolder, archiveFileName);
|
|
2417
2771
|
copyFileSync(compressedTarPath, archivePath);
|
|
2418
|
-
|
|
2772
|
+
|
|
2419
2773
|
// Make the exe executable (though Windows doesn't need chmod)
|
|
2420
2774
|
if (OS !== 'win') {
|
|
2421
2775
|
execSync(`chmod +x ${escapePathForTerminal(outputExePath)}`);
|
|
2422
2776
|
}
|
|
2423
|
-
|
|
2777
|
+
|
|
2424
2778
|
const exeSize = statSync(outputExePath).size;
|
|
2425
2779
|
const archiveSize = statSync(archivePath).size;
|
|
2426
2780
|
const totalSize = exeSize + archiveSize;
|
|
2427
|
-
|
|
2781
|
+
|
|
2428
2782
|
console.log(`Created Windows installer:`);
|
|
2429
2783
|
console.log(` - Extractor: ${outputExePath} (${(exeSize / 1024 / 1024).toFixed(2)} MB)`);
|
|
2430
2784
|
console.log(` - Archive: ${archivePath} (${(archiveSize / 1024 / 1024).toFixed(2)} MB)`);
|
|
2431
2785
|
console.log(` - Metadata: ${metadataPath}`);
|
|
2432
2786
|
console.log(` - Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
|
|
2433
|
-
|
|
2787
|
+
|
|
2434
2788
|
return outputExePath;
|
|
2435
2789
|
}
|
|
2436
2790
|
|
|
@@ -2526,12 +2880,14 @@ async function wrapWindowsInstallerInZip(exePath: string, buildFolder: string):
|
|
|
2526
2880
|
});
|
|
2527
2881
|
|
|
2528
2882
|
archive.pipe(output);
|
|
2529
|
-
|
|
2530
|
-
// Add
|
|
2883
|
+
|
|
2884
|
+
// Add Setup.exe at the root level for easy access
|
|
2531
2885
|
archive.file(exePath, { name: basename(exePath) });
|
|
2532
|
-
|
|
2533
|
-
archive
|
|
2534
|
-
|
|
2886
|
+
|
|
2887
|
+
// Put metadata and archive in a subdirectory to discourage manual extraction
|
|
2888
|
+
archive.file(metadataPath, { name: `.installer/${basename(metadataPath)}` });
|
|
2889
|
+
archive.file(archivePath, { name: `.installer/${basename(archivePath)}` });
|
|
2890
|
+
|
|
2535
2891
|
archive.finalize();
|
|
2536
2892
|
});
|
|
2537
2893
|
}
|
|
@@ -2600,81 +2956,159 @@ async function wrapInArchive(filePath: string, buildFolder: string, archiveType:
|
|
|
2600
2956
|
}
|
|
2601
2957
|
}
|
|
2602
2958
|
|
|
2603
|
-
async function
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2959
|
+
async function createLinuxSelfExtractingAppImage(
|
|
2960
|
+
buildFolder: string,
|
|
2961
|
+
compressedTarPath: string,
|
|
2962
|
+
appFileName: string,
|
|
2963
|
+
config: any,
|
|
2964
|
+
buildEnvironment: string,
|
|
2965
|
+
hash: string
|
|
2966
|
+
): Promise<string> {
|
|
2967
|
+
console.log('Creating Linux AppImage wrapper...');
|
|
2968
|
+
|
|
2969
|
+
// Create wrapper AppImage filename
|
|
2970
|
+
const wrapperName = buildEnvironment === 'stable'
|
|
2971
|
+
? `${config.app.name}-Setup`
|
|
2972
|
+
: `${config.app.name}-Setup-${buildEnvironment}`;
|
|
2973
|
+
|
|
2974
|
+
const wrapperAppImagePath = join(buildFolder, `${wrapperName}.AppImage`);
|
|
2975
|
+
const wrapperAppDirPath = join(buildFolder, `${wrapperName}.AppDir`);
|
|
2976
|
+
|
|
2977
|
+
// Clean up any existing AppDir
|
|
2978
|
+
if (existsSync(wrapperAppDirPath)) {
|
|
2979
|
+
rmSync(wrapperAppDirPath, { recursive: true, force: true });
|
|
2980
|
+
}
|
|
2981
|
+
mkdirSync(wrapperAppDirPath, { recursive: true });
|
|
2982
|
+
|
|
2983
|
+
// Create usr/bin directory structure
|
|
2984
|
+
const usrBinPath = join(wrapperAppDirPath, 'usr', 'bin');
|
|
2985
|
+
mkdirSync(usrBinPath, { recursive: true });
|
|
2986
|
+
|
|
2987
|
+
// Create self-extracting binary with embedded archive (following magic markers pattern)
|
|
2988
|
+
const targetPaths = getPlatformPaths('linux', ARCH);
|
|
2989
|
+
|
|
2990
|
+
// Read the extractor binary
|
|
2991
|
+
const extractorBinary = readFileSync(targetPaths.EXTRACTOR);
|
|
2992
|
+
|
|
2993
|
+
// Read the compressed archive
|
|
2994
|
+
const compressedArchive = readFileSync(compressedTarPath);
|
|
2995
|
+
|
|
2996
|
+
// Create metadata JSON
|
|
2997
|
+
const metadata = {
|
|
2998
|
+
identifier: config.app.identifier,
|
|
2999
|
+
name: config.app.name,
|
|
3000
|
+
channel: buildEnvironment,
|
|
3001
|
+
hash: hash
|
|
3002
|
+
};
|
|
3003
|
+
const metadataJson = JSON.stringify(metadata);
|
|
3004
|
+
const metadataBuffer = Buffer.from(metadataJson, 'utf8');
|
|
3005
|
+
|
|
3006
|
+
// Create marker buffers
|
|
3007
|
+
const metadataMarker = Buffer.from('ELECTROBUN_METADATA_V1', 'utf8');
|
|
3008
|
+
const archiveMarker = Buffer.from('ELECTROBUN_ARCHIVE_V1', 'utf8');
|
|
3009
|
+
|
|
3010
|
+
// Combine extractor + metadata marker + metadata + archive marker + archive
|
|
3011
|
+
const combinedBuffer = Buffer.concat([
|
|
3012
|
+
extractorBinary,
|
|
3013
|
+
metadataMarker,
|
|
3014
|
+
metadataBuffer,
|
|
3015
|
+
archiveMarker,
|
|
3016
|
+
compressedArchive
|
|
3017
|
+
]);
|
|
3018
|
+
|
|
3019
|
+
// Write the self-extracting binary to AppImage/usr/bin/
|
|
3020
|
+
const wrapperExtractorPath = join(usrBinPath, wrapperName);
|
|
3021
|
+
writeFileSync(wrapperExtractorPath, combinedBuffer, { mode: 0o755 });
|
|
3022
|
+
execSync(`chmod +x ${escapePathForTerminal(wrapperExtractorPath)}`);
|
|
3023
|
+
|
|
3024
|
+
// Create AppRun script
|
|
3025
|
+
const appRunContent = `#!/bin/bash
|
|
3026
|
+
# AppRun script for ${wrapperName}
|
|
2617
3027
|
HERE="$(dirname "$(readlink -f "\${0}")")"
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
3028
|
+
EXEC="\${HERE}/usr/bin/${wrapperName}"
|
|
3029
|
+
|
|
3030
|
+
# Execute the wrapper extractor
|
|
3031
|
+
exec "\${EXEC}" "\$@"
|
|
2621
3032
|
`;
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
3033
|
+
|
|
3034
|
+
const appRunPath = join(wrapperAppDirPath, 'AppRun');
|
|
3035
|
+
writeFileSync(appRunPath, appRunContent);
|
|
3036
|
+
execSync(`chmod +x ${escapePathForTerminal(appRunPath)}`);
|
|
3037
|
+
|
|
3038
|
+
// Create desktop file
|
|
3039
|
+
const desktopContent = `[Desktop Entry]
|
|
2629
3040
|
Version=1.0
|
|
2630
3041
|
Type=Application
|
|
2631
|
-
Name=${config.
|
|
2632
|
-
Comment
|
|
2633
|
-
Exec
|
|
2634
|
-
Icon=${
|
|
3042
|
+
Name=${config.app.name} Installer
|
|
3043
|
+
Comment=Install ${config.app.name}
|
|
3044
|
+
Exec=${wrapperName}
|
|
3045
|
+
Icon=${wrapperName}
|
|
2635
3046
|
Terminal=false
|
|
2636
|
-
|
|
2637
|
-
Categories=Application;
|
|
3047
|
+
Categories=Utility;
|
|
2638
3048
|
`;
|
|
3049
|
+
|
|
3050
|
+
const desktopPath = join(wrapperAppDirPath, `${wrapperName}.desktop`);
|
|
3051
|
+
writeFileSync(desktopPath, desktopContent);
|
|
3052
|
+
|
|
3053
|
+
// Copy icon if available
|
|
3054
|
+
if (config.build.linux?.icon && existsSync(join(projectRoot, config.build.linux.icon))) {
|
|
3055
|
+
const iconSourcePath = join(projectRoot, config.build.linux.icon);
|
|
3056
|
+
const iconDestPath = join(wrapperAppDirPath, `${wrapperName}.png`);
|
|
3057
|
+
const dirIconPath = join(wrapperAppDirPath, '.DirIcon');
|
|
2639
3058
|
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
// Copy icon if it exists
|
|
2644
|
-
const iconPath = config.build.linux?.appImageIcon;
|
|
2645
|
-
if (iconPath && existsSync(iconPath)) {
|
|
2646
|
-
const iconDestPath = join(appDirPath, `${appFileName}.png`);
|
|
2647
|
-
cpSync(iconPath, iconDestPath, { dereference: true });
|
|
2648
|
-
}
|
|
2649
|
-
|
|
2650
|
-
// Try to create AppImage using available tools
|
|
2651
|
-
const appImagePath = join(buildFolder, `${appFileName}.AppImage`);
|
|
3059
|
+
cpSync(iconSourcePath, iconDestPath, { dereference: true });
|
|
3060
|
+
cpSync(iconSourcePath, dirIconPath, { dereference: true });
|
|
2652
3061
|
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
3062
|
+
console.log(`Copied icon for wrapper AppImage: ${iconSourcePath} -> ${iconDestPath}`);
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
// Ensure appimagetool is available
|
|
3066
|
+
await ensureAppImageTooling();
|
|
3067
|
+
|
|
3068
|
+
// Generate the wrapper AppImage
|
|
3069
|
+
if (existsSync(wrapperAppImagePath)) {
|
|
3070
|
+
unlinkSync(wrapperAppImagePath);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
console.log(`Creating wrapper AppImage: ${wrapperAppImagePath}`);
|
|
3074
|
+
const appImageArch = ARCH === 'arm64' ? 'aarch64' : 'x86_64';
|
|
3075
|
+
|
|
3076
|
+
// Use appimagetool to create the wrapper AppImage
|
|
3077
|
+
let appimagetoolCmd = 'appimagetool';
|
|
3078
|
+
try {
|
|
3079
|
+
execSync('which appimagetool', { stdio: 'ignore' });
|
|
3080
|
+
} catch {
|
|
3081
|
+
const localBinPath = join(process.env['HOME'] || '', '.local', 'bin', 'appimagetool');
|
|
3082
|
+
if (existsSync(localBinPath)) {
|
|
3083
|
+
appimagetoolCmd = localBinPath;
|
|
2671
3084
|
}
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
try {
|
|
3088
|
+
execSync(`ARCH=${appImageArch} ${appimagetoolCmd} --no-appstream ${escapePathForTerminal(wrapperAppDirPath)} ${escapePathForTerminal(wrapperAppImagePath)}`, {
|
|
3089
|
+
stdio: 'inherit',
|
|
3090
|
+
env: { ...process.env, ARCH: appImageArch }
|
|
3091
|
+
});
|
|
2672
3092
|
} catch (error) {
|
|
2673
|
-
console.error(
|
|
2674
|
-
|
|
3093
|
+
console.error('Failed to create wrapper AppImage:', error);
|
|
3094
|
+
throw error;
|
|
2675
3095
|
}
|
|
3096
|
+
|
|
3097
|
+
// Clean up AppDir
|
|
3098
|
+
rmSync(wrapperAppDirPath, { recursive: true, force: true });
|
|
3099
|
+
|
|
3100
|
+
// Verify the wrapper AppImage was created
|
|
3101
|
+
if (!existsSync(wrapperAppImagePath)) {
|
|
3102
|
+
throw new Error(`Wrapper AppImage was not created at expected path: ${wrapperAppImagePath}`);
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
const stats = statSync(wrapperAppImagePath);
|
|
3106
|
+
console.log(`✓ Linux wrapper AppImage created: ${wrapperAppImagePath} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
|
3107
|
+
|
|
3108
|
+
return wrapperAppImagePath;
|
|
2676
3109
|
}
|
|
2677
3110
|
|
|
3111
|
+
|
|
2678
3112
|
function codesignAppBundle(
|
|
2679
3113
|
appBundleOrDmgPath: string,
|
|
2680
3114
|
entitlementsFilePath?: string
|
|
@@ -3013,10 +3447,11 @@ function createAppBundle(bundleName: string, parentFolder: string, targetOS: 'ma
|
|
|
3013
3447
|
}
|
|
3014
3448
|
}
|
|
3015
3449
|
|
|
3016
|
-
|
|
3450
|
+
// Close the command handling if/else chain
|
|
3017
3451
|
|
|
3018
|
-
//
|
|
3019
|
-
|
|
3452
|
+
// Close and execute the async IIFE
|
|
3453
|
+
})().catch((error) => {
|
|
3020
3454
|
console.error('Fatal error:', error);
|
|
3021
3455
|
process.exit(1);
|
|
3022
3456
|
});
|
|
3457
|
+
|