diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e552d182..7e48d062 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -476,13 +476,37 @@ jobs: timeout-minutes: 10 run: yarn electron:prepare-package - - name: Package Electron app + - name: Make Electron installer shell: bash timeout-minutes: 30 run: | set -o pipefail - yarn electron-forge package 2>&1 | tee forge-output.log - ls -la out/ + yarn electron-forge make --platform=win32 --arch=x64 2>&1 | tee forge-output.log + ls -la out/make/ + + - name: Verify ASAR runtime files + shell: bash + run: | + APP_ASAR=$(find out -path '*/resources/app.asar' | head -n 1) + KUBO_EXE=$(find out -path '*/resources/app.asar.unpacked/bin/win/ipfs.exe' | head -n 1) + SQLITE_MODULE=$(find out -path '*/resources/app.asar.unpacked/node_modules/better-sqlite3/build/Release/better_sqlite3.node' | head -n 1) + DATACHANNEL_MODULE=$(find out -path '*/resources/app.asar.unpacked/node_modules/node-datachannel/build/Release/node_datachannel.node' | head -n 1) + echo "Found ASAR: $APP_ASAR" + echo "Found Kubo: $KUBO_EXE" + echo "Found SQLite module: $SQLITE_MODULE" + echo "Found data channel module: $DATACHANNEL_MODULE" + test -f "$APP_ASAR" + test -f "$KUBO_EXE" + test -f "$SQLITE_MODULE" + test -f "$DATACHANNEL_MODULE" + "$KUBO_EXE" version + + - name: Verify Squirrel installer + shell: bash + run: | + SETUP_EXE=$(find out/make -iname '*setup*.exe' | head -n 1) + echo "Found installer: $SETUP_EXE" + test -n "$SETUP_EXE" && test -f "$SETUP_EXE" && echo "Installer is valid" - name: Verify executable shell: bash diff --git a/electron/kubo-paths.js b/electron/kubo-paths.js new file mode 100644 index 00000000..e44d030c --- /dev/null +++ b/electron/kubo-paths.js @@ -0,0 +1,16 @@ +import path from 'node:path'; + +export const getIpfsBinaryName = (platform = process.platform) => (platform === 'win32' ? 'ipfs.exe' : 'ipfs'); + +const getPlatformDirectory = (platform) => { + if (platform === 'win32') return 'win'; + if (platform === 'darwin') return 'mac'; + return 'linux'; +}; + +export const getBundledKuboPath = (rootPath, platform = process.platform) => path.join(rootPath, 'bin', getPlatformDirectory(platform), getIpfsBinaryName(platform)); + +export const getPackagedKuboPaths = (resourcesPath, platform = process.platform) => [ + getBundledKuboPath(path.join(resourcesPath, 'app.asar.unpacked'), platform), + getBundledKuboPath(path.join(resourcesPath, 'app'), platform), +]; diff --git a/electron/kubo-paths.test.js b/electron/kubo-paths.test.js new file mode 100644 index 00000000..da989dee --- /dev/null +++ b/electron/kubo-paths.test.js @@ -0,0 +1,18 @@ +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { getPackagedKuboPaths } from './kubo-paths.js'; + +describe('getPackagedKuboPaths', () => { + it.each([ + ['win32', 'win', 'ipfs.exe'], + ['darwin', 'mac', 'ipfs'], + ['linux', 'linux', 'ipfs'], + ])('prefers the unpacked ASAR binary on %s and retains the loose-package fallback', (platform, platformDirectory, binaryName) => { + const resourcesPath = path.join('/tmp', '5chan', 'resources'); + + expect(getPackagedKuboPaths(resourcesPath, platform)).toEqual([ + path.join(resourcesPath, 'app.asar.unpacked', 'bin', platformDirectory, binaryName), + path.join(resourcesPath, 'app', 'bin', platformDirectory, binaryName), + ]); + }); +}); diff --git a/electron/start-ipfs.js b/electron/start-ipfs.js index 7acf1fb9..f2f5f7fd 100644 --- a/electron/start-ipfs.js +++ b/electron/start-ipfs.js @@ -6,22 +6,11 @@ import ps from 'node:process'; import proxyServer from './proxy-server.js'; import tcpPortUsed from 'tcp-port-used'; import { fileURLToPath, pathToFileURL } from 'url'; +import { getBundledKuboPath, getIpfsBinaryName, getPackagedKuboPaths } from './kubo-paths.js'; import { getPkcDataPath } from './pkc-paths.js'; const dirname = path.join(path.dirname(fileURLToPath(import.meta.url))); const projectRoot = path.join(dirname, '..'); -// Get platform-specific binary name -const getIpfsBinaryName = () => (process.platform === 'win32' ? 'ipfs.exe' : 'ipfs'); - -// Get platform subdirectory name for bin/ folder -const getPlatformDir = () => { - if (process.platform === 'win32') return 'win'; - if (process.platform === 'darwin') return 'mac'; - return 'linux'; -}; - -const getBundledKuboPath = (rootPath) => path.join(rootPath, 'bin', getPlatformDir(), getIpfsBinaryName()); - const downloadBundledIpfsClients = async () => { // before-pack.js is excluded from packaged builds, so only load it in dev. const { downloadIpfsClients } = await import('./before-pack.js'); @@ -50,17 +39,16 @@ const getKuboPath = async () => { throw new Error(`Kubo binary download completed but '${bundledKuboPath}' was not found`); } else { // In production, the binary is downloaded to bin//ipfs by generateAssets hook - // With asar: false, files are at resources/app/ instead of resources/app.asar.unpacked const appPath = process.resourcesPath; const binaryName = getIpfsBinaryName(); - // Try the bin/ directory first (where generateAssets downloads binaries) - const binDirPath = getBundledKuboPath(path.join(appPath, 'app')); - if (fs.existsSync(binDirPath)) { - return binDirPath; + // Prefer the unpacked ASAR path, then support older loose-file packages. + const packagedBinPaths = getPackagedKuboPaths(appPath); + const packagedBinPath = packagedBinPaths.find((candidatePath) => fs.existsSync(candidatePath)); + if (packagedBinPath) { + return packagedBinPath; } - // Fallback: try app.asar.unpacked for ASAR builds (if we ever re-enable ASAR) const unpackedPath = path.join(appPath, 'app.asar.unpacked'); const kuboModulePath = path.join(unpackedPath, 'node_modules', 'kubo'); @@ -84,7 +72,7 @@ const getKuboPath = async () => { return appKuboBinPath; } - throw new Error(`Could not find kubo binary. Checked: ${binDirPath}, ${kuboBinPath}, ${appKuboBinPath}`); + throw new Error(`Could not find kubo binary. Checked: ${[...packagedBinPaths, kuboBinPath, appKuboBinPath].join(', ')}`); } } }; diff --git a/forge.config.js b/forge.config.js index 341b51d3..21f5a3de 100644 --- a/forge.config.js +++ b/forge.config.js @@ -11,10 +11,12 @@ const config = { appBundleId: '5chan.desktop', icon: './public/icon', // electron-forge adds the correct extension per platform - // NOTE: asar is disabled because of a bug where electron-packager silently fails - // during asar creation with 5chan's large node_modules. The app works fine without it. - // TODO: investigate and fix the asar creation issue - asar: false, + // Keep Squirrel from processing the deeply nested dependency tree as loose files. + // Kubo and native modules still need real filesystem paths at runtime. + asar: { + // Use forward slashes so minimatch also recognizes the glob on Windows. + unpackDir: 'bin/**/*', + }, // Exclude unnecessary files from the package ignore: [ @@ -81,6 +83,13 @@ const config = { }, }, + plugins: [ + { + name: '@electron-forge/plugin-auto-unpack-natives', + config: {}, + }, + ], + makers: [ // macOS { diff --git a/package.json b/package.json index d4ce722b..c8730dd3 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "@electron-forge/maker-dmg": "7.8.0", "@electron-forge/maker-squirrel": "7.8.0", "@electron-forge/maker-zip": "7.8.0", + "@electron-forge/plugin-auto-unpack-natives": "7.8.0", "@electron/rebuild": "3.7.2", "@reforged/maker-appimage": "5.1.1", "@types/lodash": "4.17.24", diff --git a/yarn.lock b/yarn.lock index 3341345d..2cf5eff5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24,6 +24,7 @@ __metadata: "@electron-forge/maker-dmg": "npm:7.8.0" "@electron-forge/maker-squirrel": "npm:7.8.0" "@electron-forge/maker-zip": "npm:7.8.0" + "@electron-forge/plugin-auto-unpack-natives": "npm:7.8.0" "@electron/rebuild": "npm:3.7.2" "@floating-ui/react": "npm:0.26.1" "@pkcprotocol/pkc-js": "npm:0.0.73" @@ -2054,6 +2055,16 @@ __metadata: languageName: node linkType: hard +"@electron-forge/plugin-auto-unpack-natives@npm:7.8.0": + version: 7.8.0 + resolution: "@electron-forge/plugin-auto-unpack-natives@npm:7.8.0" + dependencies: + "@electron-forge/plugin-base": "npm:7.8.0" + "@electron-forge/shared-types": "npm:7.8.0" + checksum: 10c0/d684741844c8809e20130d737024d060be7e7f4395006065aa6c62461ce6a5c24d86c856370075c426f89304f4ef380dbe2aee51fffe10d3e734c24f28fda811 + languageName: node + linkType: hard + "@electron-forge/plugin-base@npm:7.8.0": version: 7.8.0 resolution: "@electron-forge/plugin-base@npm:7.8.0"