diff --git a/.github/workflows/ci-check.yml b/.github/workflows/ci-check.yml
index 574388f..293a54e 100644
--- a/.github/workflows/ci-check.yml
+++ b/.github/workflows/ci-check.yml
@@ -42,7 +42,6 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
- version: 11.7.0
run_install: false
- name: Locate pnpm store
@@ -54,9 +53,9 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
- key: ${{ runner.os }}-node-24-pnpm-11.7.0-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }}
+ key: ${{ runner.os }}-node-24-pnpm-10.29.2-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }}
restore-keys: |
- ${{ runner.os }}-node-24-pnpm-11.7.0-
+ ${{ runner.os }}-node-24-pnpm-10.29.2-
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -88,3 +87,63 @@ jobs:
- name: Run tests
if: matrix.check == 'Tests'
run: pnpm test
+
+ packaged-deps:
+ name: Packaged Deps
+ if: github.event.deleted != true
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ run_install: false
+
+ - name: Locate pnpm store
+ id: pnpm-store
+ shell: bash
+ run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
+
+ - name: Cache pnpm store
+ uses: actions/cache@v4
+ with:
+ path: ${{ steps.pnpm-store.outputs.path }}
+ key: ${{ runner.os }}-node-24-pnpm-10.29.2-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-node-24-pnpm-10.29.2-
+
+ - name: Cache Electron downloads
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cache/electron
+ ~/.cache/electron-builder
+ key: ${{ runner.os }}-electron-${{ hashFiles('pnpm-lock.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-electron-
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Build
+ run: pnpm run build
+
+ # Unpacked Linux app only: no installer, no signing, no publishing. It is enough to catch pnpm/electron-builder combinations that silently drop runtime deps from app.asar (#1400, electron-builder 26.13.1 ~ 26.17.0).
+ - name: Package Linux app
+ run: pnpm exec electron-builder --config electron-builder.config.ts --linux dir --publish never
+ env:
+ # scripts/notarize.js already returns early on non-darwin builds; skip registering afterSign at all to be explicit.
+ SKIP_NOTARIZE: 'true'
+
+ - name: Verify packaged runtime deps
+ run: pnpm run verify:packaged-deps
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 15a84ad..60a65bc 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -70,8 +70,6 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
- with:
- version: 10.29.2
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -126,8 +124,6 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
- with:
- version: 10.29.2
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -260,8 +256,6 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
- with:
- version: 10.29.2
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -305,8 +299,6 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
- with:
- version: 10.29.2
- name: Setup Node.js
uses: actions/setup-node@v6
diff --git a/package.json b/package.json
index 3580a72..b86fe9a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
{
"name": "picgo",
"version": "3.0.2",
+ "packageManager": "pnpm@10.29.2",
"private": true,
"main": "dist_electron/main/index.js",
"description": "A powerful & simple image uploader for creators.",
@@ -25,6 +26,7 @@
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps",
"upload-dist": "node ./scripts/upload-dist.js",
+ "verify:packaged-deps": "node ./scripts/verify-packaged-deps.js",
"lint:dpdm": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/background.ts",
"lint:dpdm-fe": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/renderer/main.tsx",
"check": "pnpm run tsc && pnpm run lint --fix",
diff --git a/patches/app-builder-lib@26.1.0.patch b/patches/app-builder-lib@26.1.0.patch
new file mode 100644
index 0000000..87b5e92
--- /dev/null
+++ b/patches/app-builder-lib@26.1.0.patch
@@ -0,0 +1,23 @@
+diff --git a/out/codeSign/macCodeSign.js b/out/codeSign/macCodeSign.js
+index 843a507ed3814f06c6ca99494ef1cf7bfb22067e..b2b27b474478d125d816b3f8e19498050ad602e6 100644
+--- a/out/codeSign/macCodeSign.js
++++ b/out/codeSign/macCodeSign.js
+@@ -157,15 +157,15 @@ async function createKeychain({ tmpDir, cscLink, cscKeyPassword, cscILink, cscIK
+ if (cscIKeyPassword != null) {
+ cscPasswords.push(cscIKeyPassword);
+ }
+- return await importCerts(keychainFile, certPaths, cscPasswords);
++ return await importCerts(keychainFile, certPaths, cscPasswords, keychainPassword);
+ }
+-async function importCerts(keychainFile, paths, keyPasswords) {
++async function importCerts(keychainFile, paths, keyPasswords, keychainPassword) {
+ for (let i = 0; i < paths.length; i++) {
+ const password = keyPasswords[i];
+ await (0, builder_util_1.exec)("/usr/bin/security", ["import", paths[i], "-k", keychainFile, "-T", "/usr/bin/codesign", "-T", "/usr/bin/productbuild", "-P", password]);
+ // https://stackoverflow.com/questions/39868578/security-codesign-in-sierra-keychain-ignores-access-control-settings-and-ui-p
+ // https://github.com/electron-userland/electron-packager/issues/701#issuecomment-322315996
+- await (0, builder_util_1.exec)("/usr/bin/security", ["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychainFile]);
++ await (0, builder_util_1.exec)("/usr/bin/security", ["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", keychainPassword, keychainFile]);
+ }
+ return {
+ keychainFile,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6c25272..0e5de47 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,6 +4,11 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+patchedDependencies:
+ app-builder-lib@26.1.0:
+ hash: 45818f0065bd4e018c505c5cf6c7feec2a8e8c095e28622ea88d15494c19fa30
+ path: patches/app-builder-lib@26.1.0.patch
+
importers:
.:
@@ -10119,7 +10124,7 @@ snapshots:
app-builder-bin@5.0.0-alpha.12: {}
- app-builder-lib@26.1.0(dmg-builder@26.1.0)(electron-builder-squirrel-windows@26.1.0):
+ app-builder-lib@26.1.0(patch_hash=45818f0065bd4e018c505c5cf6c7feec2a8e8c095e28622ea88d15494c19fa30)(dmg-builder@26.1.0)(electron-builder-squirrel-windows@26.1.0):
dependencies:
'@develar/schema-utils': 2.6.5
'@electron/asar': 3.4.1
@@ -11073,7 +11078,7 @@ snapshots:
dmg-builder@26.1.0(electron-builder-squirrel-windows@26.1.0):
dependencies:
- app-builder-lib: 26.1.0(dmg-builder@26.1.0)(electron-builder-squirrel-windows@26.1.0)
+ app-builder-lib: 26.1.0(patch_hash=45818f0065bd4e018c505c5cf6c7feec2a8e8c095e28622ea88d15494c19fa30)(dmg-builder@26.1.0)(electron-builder-squirrel-windows@26.1.0)
builder-util: 26.1.0
fs-extra: 10.1.0
iconv-lite: 0.6.3
@@ -11168,7 +11173,7 @@ snapshots:
electron-builder-squirrel-windows@26.1.0(dmg-builder@26.1.0):
dependencies:
- app-builder-lib: 26.1.0(dmg-builder@26.1.0)(electron-builder-squirrel-windows@26.1.0)
+ app-builder-lib: 26.1.0(patch_hash=45818f0065bd4e018c505c5cf6c7feec2a8e8c095e28622ea88d15494c19fa30)(dmg-builder@26.1.0)(electron-builder-squirrel-windows@26.1.0)
builder-util: 26.1.0
electron-winstaller: 5.4.0
transitivePeerDependencies:
@@ -11177,7 +11182,7 @@ snapshots:
electron-builder@26.1.0(electron-builder-squirrel-windows@26.1.0):
dependencies:
- app-builder-lib: 26.1.0(dmg-builder@26.1.0)(electron-builder-squirrel-windows@26.1.0)
+ app-builder-lib: 26.1.0(patch_hash=45818f0065bd4e018c505c5cf6c7feec2a8e8c095e28622ea88d15494c19fa30)(dmg-builder@26.1.0)(electron-builder-squirrel-windows@26.1.0)
builder-util: 26.1.0
builder-util-runtime: 9.5.0
chalk: 4.1.2
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index c824c54..b8e3a64 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -15,6 +15,9 @@ onlyBuiltDependencies:
- husky
- vue-demi
+patchedDependencies:
+ app-builder-lib@26.1.0: patches/app-builder-lib@26.1.0.patch
+
supportedArchitectures:
cpu: [ x64, arm64 ]
os: [ current ]
diff --git a/scripts/verify-packaged-deps.js b/scripts/verify-packaged-deps.js
new file mode 100644
index 0000000..3075d4a
--- /dev/null
+++ b/scripts/verify-packaged-deps.js
@@ -0,0 +1,381 @@
+/**
+ * Verify that the app.asar produced by electron-builder contains every runtime dependency of the main/preload bundles.
+ *
+ * Background: several releases built and signed fine but crashed on launch because electron-builder silently dropped
+ * runtime deps from app.asar (pnpm >= 10.29.3 + electron-builder 26.1.0 dropped `array-timsort`; electron-builder
+ * 26.13.1 ~ 26.17.0 dropped `picgo` itself because the root package shares its name). This script makes such builds
+ * fail in CI instead of after release.
+ *
+ * Two layers:
+ * 1. Static closure check: collect every bare module specifier that the packaged main/preload bundles still
+ * `require`/`import` at runtime (renderer code is bundled by vite and is ignored), then walk each package's
+ * `dependencies` inside the asar using Node's node_modules lookup algorithm. Missing optional dependencies are
+ * ignored (they are usually platform specific), everything else is reported with its full chain.
+ * 2. Runtime smoke test: run the packaged Electron binary with ELECTRON_RUN_AS_NODE=1 and `require` every external
+ * from inside app.asar. Only MODULE_NOT_FOUND errors fail the check; other errors are reported as warnings since
+ * some modules may legitimately expect a full Electron environment.
+ *
+ * Usage:
+ * node scripts/verify-packaged-deps.js [--dist
] [--asar ]... [--skip-runtime]
+ *
+ * Without `--asar`, every app.asar found under `--dist` (default `dist`) is checked.
+ */
+
+const fs = require('fs')
+const path = require('path')
+const { builtinModules } = require('module')
+const { spawnSync } = require('child_process')
+const ts = require('typescript')
+
+const ROOT_DIR = path.join(__dirname, '..')
+
+// Modules provided by the runtime itself rather than by node_modules.
+const RUNTIME_PROVIDED_MODULES = new Set(['electron', ...builtinModules])
+
+// Marker used to find the JSON result in the smoke test output, since required modules may print to stdout.
+const RESULT_MARKER = '__PICGO_VERIFY_PACKAGED_DEPS_RESULT__'
+
+const RUNTIME_TIMEOUT_MS = 60 * 1000
+
+function loadAsar () {
+ try {
+ return require('@electron/asar')
+ } catch {
+ // @electron/asar is a dependency of electron-builder (app-builder-lib); resolve it from there if it is not hoisted.
+ const appBuilderLibDir = path.dirname(require.resolve('app-builder-lib/package.json', {
+ paths: [path.dirname(require.resolve('electron-builder/package.json'))]
+ }))
+ return require(require.resolve('@electron/asar', { paths: [appBuilderLibDir] }))
+ }
+}
+
+const asar = loadAsar()
+
+function parseArgs (argv) {
+ const options = { dist: path.join(ROOT_DIR, 'dist'), asarPaths: [], skipRuntime: false }
+ for (let i = 0; i < argv.length; i++) {
+ const arg = argv[i]
+ if (arg === '--dist') {
+ options.dist = path.resolve(argv[++i])
+ } else if (arg === '--asar') {
+ options.asarPaths.push(path.resolve(argv[++i]))
+ } else if (arg === '--skip-runtime') {
+ options.skipRuntime = true
+ } else if (arg === '-h' || arg === '--help') {
+ console.log('Usage: node scripts/verify-packaged-deps.js [--dist ] [--asar ]... [--skip-runtime]')
+ process.exit(0)
+ } else {
+ throw new Error(`Unknown argument: ${arg}`)
+ }
+ }
+ return options
+}
+
+/**
+ * Find packaged apps under the electron-builder output dir, e.g. `dist/mac-arm64/PicGo.app/Contents/Resources/app.asar`,
+ * `dist/linux-unpacked/resources/app.asar` or `dist/win-unpacked/resources/app.asar`.
+ */
+function findAsarFiles (distDir) {
+ if (!fs.existsSync(distDir)) return []
+ const results = []
+ for (const entry of fs.readdirSync(distDir, { withFileTypes: true })) {
+ if (!entry.isDirectory()) continue
+ const outDir = path.join(distDir, entry.name)
+ const candidates = [path.join(outDir, 'resources', 'app.asar')]
+ for (const child of fs.readdirSync(outDir)) {
+ if (child.endsWith('.app')) candidates.push(path.join(outDir, child, 'Contents', 'Resources', 'app.asar'))
+ }
+ results.push(...candidates.filter(candidate => fs.existsSync(candidate)))
+ }
+ return results
+}
+
+/**
+ * Locate the Electron executable that ships next to the asar, so the smoke test runs with the exact runtime users get.
+ */
+function findAppExecutable (asarPath) {
+ const resourcesDir = path.dirname(asarPath)
+ if (path.basename(path.dirname(resourcesDir)) === 'Contents') {
+ const macOSDir = path.join(path.dirname(resourcesDir), 'MacOS')
+ const binaries = fs.existsSync(macOSDir) ? fs.readdirSync(macOSDir) : []
+ return binaries.length > 0 ? { platform: 'darwin', file: path.join(macOSDir, binaries[0]) } : null
+ }
+ const appDir = path.dirname(resourcesDir)
+ for (const file of fs.readdirSync(appDir)) {
+ const fullPath = path.join(appDir, file)
+ if (!fs.statSync(fullPath).isFile()) continue
+ if (file.toLowerCase() === 'picgo.exe') return { platform: 'win32', file: fullPath }
+ if (file === 'PicGo' || file === 'picgo') return { platform: 'linux', file: fullPath }
+ }
+ return null
+}
+
+class AsarArchive {
+ constructor (asarPath) {
+ this.asarPath = asarPath
+ this.entries = asar.listPackage(asarPath, { isPack: false }).map(entry => entry.split(path.sep).join('/'))
+ }
+
+ exists (filename) {
+ try {
+ asar.statFile(this.asarPath, filename.replace(/^\//, ''), true)
+ return true
+ } catch {
+ return false
+ }
+ }
+
+ readText (filename) {
+ return asar.extractFile(this.asarPath, filename.replace(/^\//, ''), true).toString('utf8')
+ }
+
+ readJson (filename) {
+ return JSON.parse(this.readText(filename))
+ }
+}
+
+function getPackageName (specifier) {
+ const parts = specifier.split('/')
+ return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
+}
+
+function isExternalPackageSpecifier (specifier) {
+ if (specifier.startsWith('.') || specifier.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(specifier)) return false
+ if (specifier.startsWith('node:')) return false
+ return !RUNTIME_PROVIDED_MODULES.has(getPackageName(specifier))
+}
+
+/**
+ * Collect module specifiers from require()/import()/import/export-from with string literal arguments. Parsing the AST
+ * (instead of grepping) avoids false positives from examples inside comments such as `require('yaku')`.
+ */
+function collectModuleSpecifiers (code, fileName) {
+ const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, false, ts.ScriptKind.JS)
+ const specifiers = new Set()
+ const visit = node => {
+ if (ts.isCallExpression(node) && node.arguments.length > 0 && ts.isStringLiteralLike(node.arguments[0])) {
+ const isRequire = ts.isIdentifier(node.expression) && node.expression.text === 'require'
+ const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword
+ if (isRequire || isDynamicImport) specifiers.add(node.arguments[0].text)
+ } else if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
+ specifiers.add(node.moduleSpecifier.text)
+ }
+ ts.forEachChild(node, visit)
+ }
+ visit(sourceFile)
+ return specifiers
+}
+
+/**
+ * Collect external packages required by the packaged main/preload code. Returns Map>.
+ */
+function collectExternalModules (archive) {
+ const externals = new Map()
+ const entryFiles = archive.entries.filter(entry => (
+ /\.(c|m)?js$/.test(entry) &&
+ !entry.split('/').includes('node_modules') &&
+ !entry.startsWith('/dist_electron/renderer/')
+ ))
+ for (const file of entryFiles) {
+ for (const specifier of collectModuleSpecifiers(archive.readText(file), file)) {
+ if (!isExternalPackageSpecifier(specifier)) continue
+ if (!externals.has(specifier)) externals.set(specifier, new Set())
+ externals.get(specifier).add(file.replace(/^\//, ''))
+ }
+ }
+ return { externals, entryFiles }
+}
+
+/**
+ * Node's node_modules lookup: from `fromDir` walk up to the archive root, trying `/node_modules/`.
+ */
+function resolvePackageDir (archive, fromDir, packageName) {
+ let dir = fromDir
+ while (true) {
+ if (path.posix.basename(dir) !== 'node_modules') {
+ const candidate = path.posix.join(dir, 'node_modules', packageName)
+ if (archive.exists(`${candidate}/package.json`)) return candidate
+ }
+ if (dir === '/') return null
+ dir = path.posix.dirname(dir)
+ }
+}
+
+function checkStaticClosure (archive, externals) {
+ const missing = []
+ const visited = new Set()
+ // Each queue item carries the dependency chain that led to it, so a failure can be reported as `a -> b -> c`.
+ const queue = []
+
+ for (const [specifier, files] of externals) {
+ const packageName = getPackageName(specifier)
+ const fromFile = [...files][0]
+ const packageDir = resolvePackageDir(archive, path.posix.dirname(`/${fromFile}`), packageName)
+ if (!packageDir) {
+ missing.push({ chain: [`${fromFile} (require)`, packageName] })
+ continue
+ }
+ queue.push({ packageDir, chain: [packageName] })
+ }
+
+ while (queue.length > 0) {
+ const { packageDir, chain } = queue.shift()
+ if (visited.has(packageDir)) continue
+ visited.add(packageDir)
+
+ const pkg = archive.readJson(`${packageDir}/package.json`)
+ const optionalDependencies = pkg.optionalDependencies || {}
+ const dependencyNames = new Set([...Object.keys(pkg.dependencies || {}), ...Object.keys(optionalDependencies)])
+
+ for (const dependencyName of dependencyNames) {
+ const dependencyDir = resolvePackageDir(archive, packageDir, dependencyName)
+ if (dependencyDir) {
+ queue.push({ packageDir: dependencyDir, chain: [...chain, dependencyName] })
+ } else if (!(dependencyName in optionalDependencies)) {
+ // Missing optional dependencies are expected: they are usually native binaries for other platforms/arches.
+ missing.push({ chain: [...chain, dependencyName] })
+ }
+ }
+ }
+
+ return { missing, checkedPackages: visited.size }
+}
+
+/**
+ * Root `dependencies` that are not in the asar at all. Most of them are renderer deps bundled by vite, so this is only
+ * a diagnostic hint (it makes the "root package name collision" failure mode obvious) and never fails the check.
+ */
+function findMissingRootDependencies (archive) {
+ const pkg = archive.readJson('/package.json')
+ return Object.keys(pkg.dependencies || {}).filter(name => !resolvePackageDir(archive, '/', name))
+}
+
+// Executed inside the packaged Electron binary with ELECTRON_RUN_AS_NODE=1; argv: [asarPath, marker, ...specifiers].
+const RUNTIME_CHECK_SOURCE = `
+const path = require('path')
+const { createRequire } = require('module')
+const [asarPath, marker, ...specifiers] = process.argv.slice(1)
+const requireFromApp = createRequire(path.join(asarPath, 'dist_electron', 'main', 'index.js'))
+const results = []
+for (const specifier of specifiers) {
+ try {
+ requireFromApp(specifier)
+ results.push({ specifier, ok: true })
+ } catch (error) {
+ results.push({ specifier, ok: false, code: error && error.code, message: String(error && error.message || error).split('\\n')[0] })
+ }
+}
+process.stdout.write('\\n' + marker + JSON.stringify(results) + '\\n', () => process.exit(0))
+`
+
+function runRuntimeCheck (asarPath, specifiers) {
+ const executable = findAppExecutable(asarPath)
+ if (!executable) {
+ return { skipped: 'packaged Electron executable not found' }
+ }
+ if (executable.platform !== process.platform) {
+ return { skipped: `executable is for ${executable.platform}, current platform is ${process.platform}` }
+ }
+
+ const result = spawnSync(executable.file, ['-e', RUNTIME_CHECK_SOURCE, asarPath, RESULT_MARKER, ...specifiers], {
+ cwd: path.dirname(asarPath),
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
+ encoding: 'utf8',
+ timeout: RUNTIME_TIMEOUT_MS,
+ maxBuffer: 64 * 1024 * 1024
+ })
+
+ const markerLine = (result.stdout || '').split('\n').find(line => line.startsWith(RESULT_MARKER))
+ if (!markerLine) {
+ const details = [
+ result.error ? `spawn error: ${result.error.message}` : `exit code: ${result.status}, signal: ${result.signal}`,
+ (result.stderr || '').trim().split('\n').slice(-20).join('\n')
+ ].filter(Boolean).join('\n')
+ return { fatal: `failed to run ${executable.file} with ELECTRON_RUN_AS_NODE=1\n${details}` }
+ }
+
+ return { results: JSON.parse(markerLine.slice(RESULT_MARKER.length)), executable: executable.file }
+}
+
+function verifyAsar (asarPath, options) {
+ const relativeAsarPath = path.relative(ROOT_DIR, asarPath) || asarPath
+ console.log(`\n[verify-packaged-deps] Checking ${relativeAsarPath}`)
+
+ const archive = new AsarArchive(asarPath)
+ const { externals, entryFiles } = collectExternalModules(archive)
+ if (entryFiles.length === 0) {
+ console.error(' [x] No main/preload JS files found in the asar')
+ return false
+ }
+ if (externals.size === 0) {
+ console.error(` [x] No external modules found in ${entryFiles.join(', ')}; refusing to report success on an empty check`)
+ return false
+ }
+
+ const specifiers = [...externals.keys()].sort()
+ console.log(` Entry files: ${entryFiles.map(file => file.replace(/^\//, '')).join(', ')}`)
+ console.log(` External modules (${specifiers.length}): ${specifiers.join(', ')}`)
+
+ let ok = true
+
+ const { missing, checkedPackages } = checkStaticClosure(archive, externals)
+ if (missing.length > 0) {
+ ok = false
+ console.error(` [x] Static check: ${missing.length} missing dependenc${missing.length === 1 ? 'y' : 'ies'} in app.asar:`)
+ for (const { chain } of missing) console.error(` ${chain.join(' -> ')}`)
+ } else {
+ console.log(` [ok] Static check: ${checkedPackages} packages in the runtime dependency closure all resolve`)
+ }
+
+ const missingRootDependencies = findMissingRootDependencies(archive)
+ if (missingRootDependencies.length > 0) {
+ console.warn(` [warn] Root package.json dependencies not present in app.asar (${missingRootDependencies.length}): ${missingRootDependencies.join(', ')}`)
+ }
+
+ if (options.skipRuntime) {
+ console.log(' [skip] Runtime check: --skip-runtime')
+ return ok
+ }
+
+ const runtime = runRuntimeCheck(asarPath, specifiers)
+ if (runtime.skipped) {
+ console.warn(` [skip] Runtime check: ${runtime.skipped}`)
+ } else if (runtime.fatal) {
+ ok = false
+ console.error(` [x] Runtime check: ${runtime.fatal}`)
+ } else {
+ const notFound = runtime.results.filter(result => !result.ok && result.code === 'MODULE_NOT_FOUND')
+ const otherErrors = runtime.results.filter(result => !result.ok && result.code !== 'MODULE_NOT_FOUND')
+ for (const result of otherErrors) {
+ // Not a packaging problem by itself: the module may depend on a full Electron environment when loaded.
+ console.warn(` [warn] Runtime check: require('${result.specifier}') threw ${result.code || 'an error'}: ${result.message}`)
+ }
+ if (notFound.length > 0) {
+ ok = false
+ console.error(` [x] Runtime check: ${notFound.length} module(s) failed with MODULE_NOT_FOUND:`)
+ for (const result of notFound) console.error(` require('${result.specifier}'): ${result.message}`)
+ } else {
+ console.log(` [ok] Runtime check: required ${runtime.results.length - otherErrors.length}/${runtime.results.length} externals with ${path.relative(ROOT_DIR, runtime.executable)} (ELECTRON_RUN_AS_NODE=1)`)
+ }
+ }
+
+ return ok
+}
+
+function main () {
+ const options = parseArgs(process.argv.slice(2))
+ const asarPaths = options.asarPaths.length > 0 ? options.asarPaths : findAsarFiles(options.dist)
+ if (asarPaths.length === 0) {
+ console.error(`[verify-packaged-deps] No app.asar found under ${options.dist}. Run electron-builder with a \`dir\` target first.`)
+ process.exit(1)
+ }
+
+ const failed = asarPaths.filter(asarPath => !verifyAsar(asarPath, options))
+ if (failed.length > 0) {
+ console.error(`\n[verify-packaged-deps] FAILED: ${failed.length}/${asarPaths.length} packaged app(s) are missing runtime dependencies.`)
+ process.exit(1)
+ }
+ console.log(`\n[verify-packaged-deps] OK: ${asarPaths.length} packaged app(s) verified.`)
+}
+
+main()