electrobun 0.0.19-beta.74 → 0.0.19-beta.76

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.
Files changed (2) hide show
  1. package/package.json +3 -1
  2. package/src/cli/index.ts +155 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrobun",
3
- "version": "0.0.19-beta.74",
3
+ "version": "0.0.19-beta.76",
4
4
  "description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
5
5
  "license": "MIT",
6
6
  "author": "Blackboard Technologies Inc.",
@@ -48,10 +48,12 @@
48
48
  "build:push:artifacts": "bun scripts/build-and-upload-artifacts.js"
49
49
  },
50
50
  "devDependencies": {
51
+ "@types/archiver": "^6.0.3",
51
52
  "@types/bun": "1.1.9"
52
53
  },
53
54
  "dependencies": {
54
55
  "@oneidentity/zstd-js": "^1.0.3",
56
+ "archiver": "^7.0.1",
55
57
  "rpc-anywhere": "1.5.0",
56
58
  "tar": "^6.2.1"
57
59
  }
package/src/cli/index.ts CHANGED
@@ -11,9 +11,12 @@ import {
11
11
  readdirSync,
12
12
  rmSync,
13
13
  symlinkSync,
14
+ statSync,
15
+ copyFileSync,
14
16
  } from "fs";
15
17
  import { execSync } from "child_process";
16
18
  import tar from "tar";
19
+ import archiver from "archiver";
17
20
  import { ZstdInit } from "@oneidentity/zstd-js/wasm";
18
21
  import { OS, ARCH } from '../shared/platform';
19
22
  import { getTemplate, getTemplateNames } from './templates/embedded';
@@ -22,7 +25,7 @@ import { getTemplate, getTemplateNames } from './templates/embedded';
22
25
  const MAX_CHUNK_SIZE = 1024 * 2;
23
26
 
24
27
 
25
- const binExt = OS === 'win' ? '.exe' : '';
28
+ // const binExt = OS === 'win' ? '.exe' : '';
26
29
 
27
30
  // this when run as an npm script this will be where the folder where package.json is.
28
31
  const projectRoot = process.cwd();
@@ -611,6 +614,7 @@ const buildSubFolder = `${buildEnvironment}-${currentTarget.os}-${currentTarget.
611
614
  // Use target OS/ARCH for build logic (instead of current machine's OS/ARCH)
612
615
  const targetOS = currentTarget.os;
613
616
  const targetARCH = currentTarget.arch;
617
+ const targetBinExt = targetOS === 'win' ? '.exe' : '';
614
618
 
615
619
  const buildFolder = join(projectRoot, config.build.buildFolder, buildSubFolder);
616
620
 
@@ -846,7 +850,7 @@ if (commandArg === "init") {
846
850
  // Only copy launcher for non-dev builds
847
851
  if (buildEnvironment !== "dev") {
848
852
  const bunCliLauncherBinarySource = targetPaths.LAUNCHER_RELEASE;
849
- const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + binExt;
853
+ const bunCliLauncherDestination = join(appBundleMacOSPath, "launcher") + targetBinExt;
850
854
  const destLauncherFolder = dirname(bunCliLauncherDestination);
851
855
  if (!existsSync(destLauncherFolder)) {
852
856
  // console.info('creating folder: ', destFolder);
@@ -866,7 +870,7 @@ if (commandArg === "init") {
866
870
  const bunBinarySourcePath = targetPaths.BUN_BINARY;
867
871
  // Note: .bin/bun binary in node_modules is a symlink to the versioned one in another place
868
872
  // in node_modules, so we have to dereference here to get the actual binary in the bundle.
869
- const bunBinaryDestInBundlePath = join(appBundleMacOSPath, "bun") + binExt;
873
+ const bunBinaryDestInBundlePath = join(appBundleMacOSPath, "bun") + targetBinExt;
870
874
  const destFolder2 = dirname(bunBinaryDestInBundlePath);
871
875
  if (!existsSync(destFolder2)) {
872
876
  // console.info('creating folder: ', destFolder);
@@ -1172,7 +1176,7 @@ if (commandArg === "init") {
1172
1176
 
1173
1177
  // copy native bindings
1174
1178
  const bsPatchSource = targetPaths.BSPATCH;
1175
- const bsPatchDestination = join(appBundleMacOSPath, "bspatch") + binExt;
1179
+ const bsPatchDestination = join(appBundleMacOSPath, "bspatch") + targetBinExt;
1176
1180
  const bsPatchDestFolder = dirname(bsPatchDestination);
1177
1181
  if (!existsSync(bsPatchDestFolder)) {
1178
1182
  mkdirSync(bsPatchDestFolder, { recursive: true });
@@ -1501,7 +1505,13 @@ if (commandArg === "init") {
1501
1505
  targetPaths,
1502
1506
  buildEnvironment
1503
1507
  );
1504
- artifactsToUpload.push(selfExtractingExePath);
1508
+
1509
+ // Wrap Windows installer files in zip for distribution
1510
+ const wrappedExePath = await wrapWindowsInstallerInZip(selfExtractingExePath, buildFolder);
1511
+ artifactsToUpload.push(wrappedExePath);
1512
+
1513
+ // Also keep the raw exe for backwards compatibility (optional)
1514
+ // artifactsToUpload.push(selfExtractingExePath);
1505
1515
  } else if (targetOS === 'linux') {
1506
1516
  // Create desktop file for Linux
1507
1517
  const desktopFileContent = `[Desktop Entry]
@@ -1554,7 +1564,13 @@ exec "\$LAUNCHER_BINARY" "\$@"
1554
1564
  targetPaths,
1555
1565
  buildEnvironment
1556
1566
  );
1557
- artifactsToUpload.push(selfExtractingLinuxPath);
1567
+
1568
+ // Wrap Linux .run file in tar.gz to preserve permissions
1569
+ const wrappedRunPath = await wrapInArchive(selfExtractingLinuxPath, buildFolder, 'tar.gz');
1570
+ artifactsToUpload.push(wrappedRunPath);
1571
+
1572
+ // Also keep the raw .run for backwards compatibility (optional)
1573
+ // artifactsToUpload.push(selfExtractingLinuxPath);
1558
1574
 
1559
1575
  // On Linux, create a tar.gz of the bundle
1560
1576
  const linuxTarPath = join(buildFolder, `${appFileName}.tar.gz`);
@@ -1864,7 +1880,7 @@ async function createWindowsSelfExtractingExe(
1864
1880
  targetPaths: any,
1865
1881
  buildEnvironment: string
1866
1882
  ): Promise<string> {
1867
- console.log("Creating self-extracting Windows exe...");
1883
+ console.log("Creating Windows installer with separate archive...");
1868
1884
 
1869
1885
  // Format: MyApp-Setup.exe (stable) or MyApp-Setup-canary.exe (non-stable)
1870
1886
  const setupFileName = buildEnvironment === "stable"
@@ -1873,43 +1889,40 @@ async function createWindowsSelfExtractingExe(
1873
1889
 
1874
1890
  const outputExePath = join(buildFolder, setupFileName);
1875
1891
 
1876
- // Read the extractor exe
1892
+ // Copy the extractor exe
1877
1893
  const extractorExe = readFileSync(targetPaths.EXTRACTOR);
1894
+ writeFileSync(outputExePath, extractorExe);
1878
1895
 
1879
- // Read the compressed archive
1880
- const compressedArchive = readFileSync(compressedTarPath);
1881
-
1882
- // Create metadata JSON
1896
+ // Create metadata JSON file
1883
1897
  const metadata = {
1884
1898
  identifier: config.app.identifier,
1885
1899
  name: config.app.name,
1886
1900
  channel: buildEnvironment
1887
1901
  };
1888
- const metadataJson = JSON.stringify(metadata);
1889
- const metadataBuffer = Buffer.from(metadataJson, 'utf8');
1902
+ const metadataJson = JSON.stringify(metadata, null, 2);
1903
+ const metadataFileName = setupFileName.replace('.exe', '.metadata.json');
1904
+ const metadataPath = join(buildFolder, metadataFileName);
1905
+ writeFileSync(metadataPath, metadataJson);
1890
1906
 
1891
- // Create marker buffers
1892
- const metadataMarker = Buffer.from('ELECTROBUN_METADATA_V1', 'utf8');
1893
- const archiveMarker = Buffer.from('ELECTROBUN_ARCHIVE_V1', 'utf8');
1907
+ // Copy the compressed archive with matching name
1908
+ const archiveFileName = setupFileName.replace('.exe', '.tar.zst');
1909
+ const archivePath = join(buildFolder, archiveFileName);
1910
+ copyFileSync(compressedTarPath, archivePath);
1894
1911
 
1895
- // Combine extractor + metadata marker + metadata + archive marker + archive
1896
- const combinedBuffer = Buffer.concat([
1897
- extractorExe,
1898
- metadataMarker,
1899
- metadataBuffer,
1900
- archiveMarker,
1901
- compressedArchive
1902
- ]);
1903
-
1904
- // Write the self-extracting exe
1905
- writeFileSync(outputExePath, combinedBuffer);
1906
-
1907
- // Make it executable (though Windows doesn't need chmod)
1912
+ // Make the exe executable (though Windows doesn't need chmod)
1908
1913
  if (OS !== 'win') {
1909
1914
  execSync(`chmod +x ${escapePathForTerminal(outputExePath)}`);
1910
1915
  }
1911
1916
 
1912
- console.log(`Created self-extracting exe: ${outputExePath} (${(combinedBuffer.length / 1024 / 1024).toFixed(2)} MB)`);
1917
+ const exeSize = statSync(outputExePath).size;
1918
+ const archiveSize = statSync(archivePath).size;
1919
+ const totalSize = exeSize + archiveSize;
1920
+
1921
+ console.log(`Created Windows installer:`);
1922
+ console.log(` - Extractor: ${outputExePath} (${(exeSize / 1024 / 1024).toFixed(2)} MB)`);
1923
+ console.log(` - Archive: ${archivePath} (${(archiveSize / 1024 / 1024).toFixed(2)} MB)`);
1924
+ console.log(` - Metadata: ${metadataPath}`);
1925
+ console.log(` - Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
1913
1926
 
1914
1927
  return outputExePath;
1915
1928
  }
@@ -1969,6 +1982,117 @@ async function createLinuxSelfExtractingBinary(
1969
1982
  return outputPath;
1970
1983
  }
1971
1984
 
1985
+ async function wrapWindowsInstallerInZip(exePath: string, buildFolder: string): Promise<string> {
1986
+ const exeName = basename(exePath);
1987
+ const exeStem = exeName.replace('.exe', '');
1988
+
1989
+ // Derive the paths for metadata and archive files
1990
+ const metadataPath = join(buildFolder, `${exeStem}.metadata.json`);
1991
+ const archivePath = join(buildFolder, `${exeStem}.tar.zst`);
1992
+ const zipPath = join(buildFolder, `${exeStem}.zip`);
1993
+
1994
+ // Verify all files exist
1995
+ if (!existsSync(exePath)) {
1996
+ throw new Error(`Installer exe not found: ${exePath}`);
1997
+ }
1998
+ if (!existsSync(metadataPath)) {
1999
+ throw new Error(`Metadata file not found: ${metadataPath}`);
2000
+ }
2001
+ if (!existsSync(archivePath)) {
2002
+ throw new Error(`Archive file not found: ${archivePath}`);
2003
+ }
2004
+
2005
+ // Create zip archive
2006
+ const output = createWriteStream(zipPath);
2007
+ const archive = archiver('zip', {
2008
+ zlib: { level: 9 } // Maximum compression
2009
+ });
2010
+
2011
+ return new Promise((resolve, reject) => {
2012
+ output.on('close', () => {
2013
+ console.log(`Created Windows installer package: ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(2)} MB)`);
2014
+ resolve(zipPath);
2015
+ });
2016
+
2017
+ archive.on('error', (err) => {
2018
+ reject(err);
2019
+ });
2020
+
2021
+ archive.pipe(output);
2022
+
2023
+ // Add all three files to the archive
2024
+ archive.file(exePath, { name: basename(exePath) });
2025
+ archive.file(metadataPath, { name: basename(metadataPath) });
2026
+ archive.file(archivePath, { name: basename(archivePath) });
2027
+
2028
+ archive.finalize();
2029
+ });
2030
+ }
2031
+
2032
+ async function wrapInArchive(filePath: string, buildFolder: string, archiveType: 'tar.gz' | 'zip'): Promise<string> {
2033
+ const fileName = basename(filePath);
2034
+ const fileDir = dirname(filePath);
2035
+
2036
+ if (archiveType === 'tar.gz') {
2037
+ // Output filename: Setup.exe -> Setup.exe.tar.gz or Setup.run -> Setup.run.tar.gz
2038
+ const archivePath = filePath + '.tar.gz';
2039
+
2040
+ // For Linux files, ensure they have executable permissions before archiving
2041
+ if (fileName.endsWith('.run')) {
2042
+ try {
2043
+ // Try to set executable permissions (will only work on Unix-like systems)
2044
+ execSync(`chmod +x ${escapePathForTerminal(filePath)}`, { stdio: 'ignore' });
2045
+ } catch {
2046
+ // Ignore errors on Windows hosts
2047
+ }
2048
+ }
2049
+
2050
+ // Create tar.gz archive preserving permissions
2051
+ // Using the tar package for cross-platform compatibility
2052
+ await tar.c(
2053
+ {
2054
+ gzip: true,
2055
+ file: archivePath,
2056
+ cwd: fileDir,
2057
+ portable: true, // Ensures consistent behavior across platforms
2058
+ preservePaths: false,
2059
+ // The tar package should preserve file modes when creating archives
2060
+ },
2061
+ [fileName]
2062
+ );
2063
+
2064
+ console.log(`Created archive: ${archivePath} (preserving executable permissions)`);
2065
+ return archivePath;
2066
+ } else if (archiveType === 'zip') {
2067
+ // Output filename: Setup.exe -> Setup.zip
2068
+ const archivePath = filePath.replace(/\.[^.]+$/, '.zip');
2069
+
2070
+ // Create zip archive
2071
+ const output = createWriteStream(archivePath);
2072
+ const archive = archiver('zip', {
2073
+ zlib: { level: 9 } // Maximum compression
2074
+ });
2075
+
2076
+ return new Promise((resolve, reject) => {
2077
+ output.on('close', () => {
2078
+ console.log(`Created archive: ${archivePath} (${(archive.pointer() / 1024 / 1024).toFixed(2)} MB)`);
2079
+ resolve(archivePath);
2080
+ });
2081
+
2082
+ archive.on('error', (err) => {
2083
+ reject(err);
2084
+ });
2085
+
2086
+ archive.pipe(output);
2087
+
2088
+ // Add the file to the archive
2089
+ archive.file(filePath, { name: fileName });
2090
+
2091
+ archive.finalize();
2092
+ });
2093
+ }
2094
+ }
2095
+
1972
2096
  async function createAppImage(buildFolder: string, appBundlePath: string, appFileName: string, config: any): Promise<string | null> {
1973
2097
  try {
1974
2098
  console.log("Creating AppImage...");