import { spawn } from 'node:child_process'; import { mkdtemp, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'; import { arch, platform, tmpdir } from 'node:os'; import { dirname, join, relative, resolve, sep } from 'node:path'; import { pathToFileURL } from 'node:url'; import { Arch, Platform, build } from 'electron-builder'; import { parseProbeArgs, resolvePiRuntime, runProbe, validatePiIdentity, } from './probe-pi-runtime.mjs'; import { runLocalProviderContracts } from './probe-pi-provider-contracts.mjs'; import { PI_RUNTIME_MANIFEST, defaultPiBundleTarget, stagePiRuntimeBundle, } from './lib/pi-runtime-bundle.mjs'; const PRODUCT_NAME = 'MakelorePiProbe'; const ARTIFACT_LABEL = 'controlled-electron-builder-dir-extra-resources'; async function pathExists(path) { try { await stat(path); return true; } catch { return false; } } function currentTarget() { let platformTarget; if (platform() === 'win32') platformTarget = Platform.WINDOWS; else if (platform() === 'darwin') platformTarget = Platform.MAC; else if (platform() === 'linux') platformTarget = Platform.LINUX; else throw new Error(`Unsupported packaging platform: ${platform()}`); const architecture = Arch[arch()]; if (architecture == null) throw new Error(`Unsupported packaging architecture: ${arch()}`); return { platformTarget, architecture }; } async function findPackagedExecutable(root) { const matches = []; const visit = async (directory) => { for (const entry of await readdir(directory, { withFileTypes: true })) { const path = join(directory, entry.name); if (entry.isDirectory()) await visit(path); else if ( (platform() === 'win32' && entry.name === `${PRODUCT_NAME}.exe`) || (platform() === 'darwin' && entry.name === PRODUCT_NAME && directory.endsWith(`${sep}Contents${sep}MacOS`)) || (platform() === 'linux' && entry.name === PRODUCT_NAME) ) matches.push(path); } }; await visit(root); if (matches.length !== 1) { throw new Error(`Expected one packaged ${PRODUCT_NAME} executable, found ${matches.length}`); } return matches[0]; } export function packagedResourcesDirectory(executable, currentPlatform = platform()) { return currentPlatform === 'darwin' ? resolve(executable, '..', '..', 'Resources') : join(dirname(executable), 'resources'); } function runCommand(executable, args, options = {}) { return new Promise((resolvePromise, reject) => { const child = spawn(executable, args, { cwd: options.cwd, env: options.env ?? process.env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, }); let stdout = ''; let stderr = ''; child.stdout.on('data', (chunk) => { stdout += chunk.toString(); }); child.stderr.on('data', (chunk) => { stderr += chunk.toString(); }); child.once('error', reject); child.once('exit', (code, signal) => { if (code === 0) resolvePromise({ stdout, stderr }); else reject(new Error( `${executable} exited with code ${code ?? 'null'} signal ${signal ?? 'none'}: ${stderr || stdout}`, )); }); }); } export async function inspectPackagedClosure(executable, resourcesDirectory, assets) { const script = String.raw` const fs = require('node:fs'); const path = require('node:path'); const resources = process.env.PI_PROBE_RESOURCES_DIRECTORY; if (!resources) throw new Error('PI_PROBE_RESOURCES_DIRECTORY is required'); const root = path.join(resources, 'pi-runtime'); const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); const runtimeManifestPath = path.join(root, '${PI_RUNTIME_MANIFEST}'); const runtimeManifest = fs.existsSync(runtimeManifestPath) ? JSON.parse(fs.readFileSync(runtimeManifestPath, 'utf8')) : null; const shrinkwrap = JSON.parse(fs.readFileSync(path.join(root, 'npm-shrinkwrap.json'), 'utf8')); const assets = JSON.parse(process.env.PI_PROBE_ASSETS_JSON); const libc = process.platform === 'linux' ? (process.report?.getReport?.().header?.glibcVersionRuntime ? 'glibc' : 'musl') : undefined; const constraintMatches = (constraints, current) => { if (!constraints || constraints.length === 0 || !current) return true; const positive = constraints.filter((value) => !value.startsWith('!')); const negative = constraints.filter((value) => value.startsWith('!')).map((value) => value.slice(1)); return !negative.includes(current) && (positive.length === 0 || positive.includes(current)); }; const supportsCurrentPlatform = (entry) => constraintMatches(entry.os, process.platform) && constraintMatches(entry.cpu, process.arch) && constraintMatches(entry.libc, libc); const missingPackages = []; const relocatedPackages = []; let expectedPackages = 0; let platformSkippedPackages = 0; for (const [packagePath, entry] of Object.entries(shrinkwrap.packages || {})) { if (!packagePath || entry.dev) continue; if (!supportsCurrentPlatform(entry)) { platformSkippedPackages += 1; continue; } expectedPackages += 1; const exactPath = path.join(root, ...packagePath.split('/')); if (fs.existsSync(exactPath)) continue; const nestedMarker = '/node_modules/'; const packageTail = packagePath.includes(nestedMarker) ? packagePath.slice(packagePath.lastIndexOf(nestedMarker) + nestedMarker.length) : packagePath.slice('node_modules/'.length); const nameParts = packageTail.split('/'); const packageName = nameParts[0].startsWith('@') ? nameParts.slice(0, 2).join('/') : nameParts[0]; const flattenedPath = path.join(root, 'node_modules', ...packageName.split('/')); const flattenedPackageJson = path.join(flattenedPath, 'package.json'); if (fs.existsSync(flattenedPackageJson)) { const flattenedVersion = JSON.parse(fs.readFileSync(flattenedPackageJson, 'utf8')).version; if (flattenedVersion === entry.version) { relocatedPackages.push({ from: packagePath, to: 'node_modules/' + packageName, version: entry.version }); continue; } } missingPackages.push(packagePath); } const missingAssets = assets.filter((asset) => !fs.existsSync(path.join(root, ...asset.split('/')))); process.stdout.write(JSON.stringify({ packageName: packageJson.name, packageVersion: packageJson.version, runtimeManifestExists: Boolean(runtimeManifest), runtimeManifestTarget: runtimeManifest?.target ?? null, cliExists: fs.existsSync(path.join(root, 'dist', 'cli.js')), lockedPackages: Math.max(0, Object.keys(shrinkwrap.packages || {}).length - 1), expectedPackages, platformSkippedPackages, missingPackages, relocatedPackages, assetCount: assets.length, missingAssets, nativeAssetsOutsideAsar: assets.filter((asset) => asset.endsWith('.node')).length, })); `; const { stdout } = await runCommand(executable, ['-e', script], { env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', PI_PROBE_RESOURCES_DIRECTORY: resourcesDirectory, PI_PROBE_ASSETS_JSON: JSON.stringify(assets), }, }); const result = JSON.parse(stdout.trim()); if ( !result.cliExists || !result.runtimeManifestExists || result.missingPackages.length > 0 || result.missingAssets.length > 0 ) { throw new Error(`Packaged Pi closure is incomplete: ${JSON.stringify(result)}`); } return result; } function assertControlledOutput(projectRoot, outputDirectory) { const relativePath = relative(projectRoot, outputDirectory); if (!relativePath || relativePath.startsWith('..') || resolve(projectRoot, relativePath) !== outputDirectory) { throw new Error(`Refusing uncontrolled packaged-probe output: ${outputDirectory}`); } if (relativePath.split(sep).join('/') !== 'release/pi-runtime-probe') { throw new Error(`Unexpected packaged-probe output directory: ${outputDirectory}`); } } async function buildControlledArtifact(projectRoot, appDirectory, runtimeDirectory, outputDirectory) { const electronPackage = JSON.parse(await readFile(join(projectRoot, 'node_modules', 'electron', 'package.json'), 'utf8')); const { platformTarget, architecture } = currentTarget(); const buildResources = join(appDirectory, 'build-resources'); await mkdir(buildResources, { recursive: true }); await writeFile(join(appDirectory, 'package.json'), `${JSON.stringify({ name: 'makelore-pi-production-shape-probe', version: '1.0.0', main: 'main.js', }, null, 2)}\n`); await writeFile(join(appDirectory, 'main.js'), 'process.exit(0);\n'); const builderArtifacts = await build({ // Keep the probe application minimal while copying the permanent stage to // the same resources/pi-runtime location used by the product configuration. projectDir: appDirectory, targets: platformTarget.createTarget('dir', architecture), publish: 'never', config: { appId: 'app.niancode.desktop.pi-probe', productName: PRODUCT_NAME, electronVersion: electronPackage.version, electronDist: join(projectRoot, 'node_modules', 'electron', 'dist'), directories: { output: outputDirectory, buildResources, }, files: [ 'main.js', 'package.json', ], extraResources: [ { from: runtimeDirectory, to: 'pi-runtime', filter: ['**/*', '!node_modules{,/**/*}'], }, { from: join(runtimeDirectory, 'node_modules'), to: 'pi-runtime/node_modules', filter: ['**/*'], }, ], asar: true, npmRebuild: false, win: { executableName: PRODUCT_NAME, signAndEditExecutable: false, verifyUpdateCodeSignature: false, }, mac: { identity: null, hardenedRuntime: false, }, linux: { executableName: PRODUCT_NAME, }, }, }); return builderArtifacts; } export async function runPackagedProbe(options, projectRoot = process.cwd()) { const resolvedProjectRoot = resolve(projectRoot); const outputDirectory = resolve(resolvedProjectRoot, 'release', 'pi-runtime-probe'); assertControlledOutput(resolvedProjectRoot, outputDirectory); const scratchRoot = await mkdtemp(join(tmpdir(), 'makelore-pi-packaged-probe-')); const appDirectory = join(scratchRoot, 'app'); const runtimeDirectory = join(scratchRoot, 'runtime'); try { const sourceRuntime = await resolvePiRuntime(resolvedProjectRoot); const identity = await validatePiIdentity(sourceRuntime); const runtimeManifest = await stagePiRuntimeBundle({ projectRoot: resolvedProjectRoot, sourcePackageRoot: sourceRuntime.packageRoot, destination: runtimeDirectory, target: defaultPiBundleTarget(), }); const stagedClosure = { lockfileVersion: runtimeManifest.lockfileVersion, expectedPackages: runtimeManifest.productionPackages.length, platformSkippedPackages: runtimeManifest.platformSkippedPackages, assets: runtimeManifest.runtimeAssets, omittedPublishedDevDependencies: runtimeManifest.staging.omittedPublishedDevDependencies, manifest: runtimeManifest, }; await rm(outputDirectory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); await mkdir(dirname(outputDirectory), { recursive: true }); const builderArtifacts = await buildControlledArtifact( resolvedProjectRoot, appDirectory, runtimeDirectory, outputDirectory, ); const executable = await findPackagedExecutable(outputDirectory); const resourcesDirectory = packagedResourcesDirectory(executable); const appAsar = join(resourcesDirectory, 'app.asar'); const packagedRuntime = join(resourcesDirectory, 'pi-runtime'); const cliPath = join(packagedRuntime, 'dist', 'cli.js'); if (!await pathExists(appAsar)) throw new Error(`Packaged app.asar is missing: ${appAsar}`); const packagedClosure = await inspectPackagedClosure( executable, resourcesDirectory, stagedClosure.assets, ); const runtime = await runProbe({ ...options, reportPath: undefined, stage: false, keepStage: false, electronExecutablePath: executable, cliPath, artifactLabel: ARTIFACT_LABEL, }, resolvedProjectRoot); const providerContracts = await runLocalProviderContracts({ ...options, reportPath: undefined, stage: false, keepStage: false, electronExecutablePath: executable, cliPath, artifactLabel: ARTIFACT_LABEL, }, resolvedProjectRoot); const report = { schemaVersion: 1, generatedAt: new Date().toISOString(), identity, artifact: { label: ARTIFACT_LABEL, outputDirectory, executable, appAsar, runtimeRoot: packagedRuntime, builderArtifacts, }, stagedClosure, packagedClosure, runtime, providerContracts, result: runtime.result, decision: 'incomplete', }; if (options.reportPath) { await mkdir(dirname(options.reportPath), { recursive: true }); await writeFile(options.reportPath, `${JSON.stringify(report, null, 2)}\n`); } return report; } finally { await rm(scratchRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); } } async function main() { const options = parseProbeArgs(process.argv.slice(2)); if (options.help) { process.stdout.write('Usage: node scripts/probe-pi-packaged-runtime.mjs [probe options]\n'); return; } if (options.stage || options.keepStage || options.electronExecutablePath) { throw new Error('Packaged probe owns staging and runtime paths; do not pass staging or runtime override flags'); } const report = await runPackagedProbe(options); process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); if (report.result === 'fail') process.exitCode = 1; } const isMain = process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url; if (isMain) { main().catch((error) => { process.stderr.write(`${error.stack ?? error.message}\n`); process.exit(1); }); }