diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index a799635..bf18544 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -16,5 +16,8 @@ jobs: - name: Checkout repository uses: https://gitea.com/actions/checkout@v4 + - name: Install dependencies + run: npm ci + - name: Run template tests run: bash tests/run_test.sh diff --git a/scripts/electron-launcher.js b/scripts/electron-launcher.js index e2bfe1c..b1f3384 100644 --- a/scripts/electron-launcher.js +++ b/scripts/electron-launcher.js @@ -1,5 +1,6 @@ 'use strict'; +const { spawnSync } = require('node:child_process'); const fs = require('node:fs'); const path = require('node:path'); @@ -34,6 +35,41 @@ function platformBinaryCandidates(platform) { } } +function repairElectronInstall({ + packageRoot, + platform = process.platform, + arch = process.arch, +}) { + const installScript = path.join(packageRoot, 'install.js'); + if (!fs.existsSync(installScript)) { + return false; + } + + const result = spawnSync(process.execPath, [installScript], { + cwd: packageRoot, + env: { + ...process.env, + npm_config_platform: platform, + npm_config_arch: arch, + }, + stdio: 'inherit', + }); + + if (result.error) { + throw result.error; + } + + if (result.signal) { + throw new Error(`Electron repair was interrupted by ${result.signal}`); + } + + if (result.status !== 0) { + throw new Error(`Electron repair failed with exit code ${result.status ?? 1}`); + } + + return true; +} + function buildMissingElectronError({ packageRoot, distDir, candidatePaths }) { const tried = candidatePaths.map((candidate) => ` - ${candidate}`).join('\n'); return [ @@ -57,6 +93,7 @@ function resolveElectronBinary({ packageRoot = resolveElectronPackageRoot(), platform = process.platform, overrideDistPath = process.env.ELECTRON_OVERRIDE_DIST_PATH || null, + arch = process.arch, } = {}) { if (!packageRoot && !overrideDistPath) { throw new Error( @@ -79,6 +116,22 @@ function resolveElectronBinary({ const resolved = candidatePaths.find((candidate) => fs.existsSync(candidate)); if (!resolved) { + if (packageRoot && repairElectronInstall({ packageRoot, platform, arch })) { + const repairedHint = readElectronPathHint(packageRoot); + const repairedCandidates = []; + if (repairedHint) { + repairedCandidates.push(path.join(distDir, repairedHint)); + } + for (const relativePath of platformBinaryCandidates(platform)) { + repairedCandidates.push(path.join(distDir, relativePath)); + } + + const repaired = repairedCandidates.find((candidate) => fs.existsSync(candidate)); + if (repaired) { + return repaired; + } + } + throw new Error(buildMissingElectronError({ packageRoot, distDir, candidatePaths })); } @@ -88,6 +141,7 @@ function resolveElectronBinary({ module.exports = { buildMissingElectronError, readElectronPathHint, + repairElectronInstall, resolveElectronBinary, resolveElectronPackageRoot, platformBinaryCandidates, diff --git a/scripts/start-electron.js b/scripts/start-electron.js index 81a767c..a773ab7 100644 --- a/scripts/start-electron.js +++ b/scripts/start-electron.js @@ -5,7 +5,13 @@ const { spawn } = require('node:child_process'); const { resolveElectronBinary } = require('./electron-launcher'); -const electronPath = resolveElectronBinary(); +let electronPath; +try { + electronPath = resolveElectronBinary(); +} catch (error) { + console.error(error && error.message ? error.message : error); + process.exit(1); +} const env = { ...process.env }; delete env.ELECTRON_RUN_AS_NODE; diff --git a/tests/unit/electron-launcher.test.js b/tests/unit/electron-launcher.test.js index 2510556..6cd0d5a 100644 --- a/tests/unit/electron-launcher.test.js +++ b/tests/unit/electron-launcher.test.js @@ -7,6 +7,7 @@ const path = require('node:path'); const { buildMissingElectronError, + repairElectronInstall, resolveElectronBinary, } = require('../../scripts/electron-launcher'); const { makeTmpDir, rmrf } = require('./helpers'); @@ -38,6 +39,32 @@ test('falls back to the platform binary when path.txt is absent', (t) => { ); }); +test('repairs a broken Electron install before resolving the binary', (t) => { + const root = makeTmpDir('electron-repair'); + t.after(() => rmrf(root)); + + fs.mkdirSync(path.join(root, 'dist'), { recursive: true }); + fs.writeFileSync( + path.join(root, 'install.js'), + [ + "const fs = require('node:fs');", + "const path = require('node:path');", + "fs.mkdirSync(path.join(__dirname, 'dist'), { recursive: true });", + "fs.writeFileSync(path.join(__dirname, 'dist', 'electron.exe'), 'binary');", + "fs.writeFileSync(path.join(__dirname, 'path.txt'), 'electron.exe');", + ].join('\n') + ); + + assert.equal( + repairElectronInstall({ packageRoot: root, platform: 'win32' }), + true + ); + assert.equal( + resolveElectronBinary({ packageRoot: root, platform: 'win32' }), + path.join(root, 'dist', 'electron.exe') + ); +}); + test('reports a helpful error when the runtime is missing', (t) => { const root = makeTmpDir('electron-missing'); t.after(() => rmrf(root));