#!/usr/bin/env node import { createHash } from 'node:crypto'; import { chmod, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import * as tar from 'tar'; import { getPythonTarget, PYTHON_RELEASE, PYTHON_TARGETS, PYTHON_VERSION, } from './bundled-python-manifest.mjs'; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const DEFAULT_RESOURCES_ROOT = path.resolve(SCRIPT_DIR, '..', 'resources'); const PLATFORM_TARGETS = Object.freeze({ win: ['win32-x64'], mac: ['darwin-x64', 'darwin-arm64'], linux: ['linux-x64', 'linux-arm64'], }); async function defaultExtractArchive(archivePath, destination) { await tar.x({ file: archivePath, cwd: destination, gzip: true }); } function markerFor(targetId, target) { return { release: PYTHON_RELEASE, sha256: target.sha256, target: targetId, version: `${PYTHON_VERSION}+${PYTHON_RELEASE}`, }; } async function hasValidStaging(destination, targetId, target) { try { const marker = JSON.parse(await readFile(path.join(destination, '.niancode-python.json'), 'utf8')); await readFile(path.join(destination, target.executable)); return JSON.stringify(marker) === JSON.stringify(markerFor(targetId, target)); } catch { return false; } } async function downloadArchive(targetId, target, fetchImpl) { const urls = [target.url, target.mirrorUrl].filter(Boolean); const failures = []; for (const url of urls) { try { const response = await fetchImpl(url); if (!response?.ok) { failures.push(`${url}: HTTP ${response?.status ?? 'unknown'}`); continue; } return Buffer.from(await response.arrayBuffer()); } catch (error) { const cause = error?.cause?.code ?? error?.code ?? error?.cause?.message ?? error?.message ?? String(error); failures.push(`${url}: ${cause}`); } } throw new Error(`Failed to download bundled Python for ${targetId}: ${failures.join('; ')}`); } export async function stagePythonTarget(targetId, options = {}) { const pinnedTarget = getPythonTarget(targetId); const target = options.targetOverride ?? pinnedTarget; const resourcesRoot = path.resolve(options.resourcesRoot ?? DEFAULT_RESOURCES_ROOT); const pythonRoot = path.join(resourcesRoot, 'python'); const destination = path.join(pythonRoot, targetId); const fetchImpl = options.fetchImpl ?? globalThis.fetch; const extractArchive = options.extractArchive ?? defaultExtractArchive; if (!options.targetOverride && await hasValidStaging(destination, targetId, target)) { return destination; } await mkdir(pythonRoot, { recursive: true }); const temporary = await mkdtemp(path.join(pythonRoot, `.${targetId}-`)); const archivePath = path.join(temporary, target.archiveName); const extracted = path.join(temporary, 'extracted'); try { const archive = await downloadArchive(targetId, target, fetchImpl); const actualSha256 = createHash('sha256').update(archive).digest('hex'); if (actualSha256 !== target.sha256) { throw new Error(`SHA-256 mismatch for ${targetId}: expected ${target.sha256}, received ${actualSha256}`); } await writeFile(archivePath, archive); await mkdir(extracted, { recursive: true }); await extractArchive(archivePath, extracted); const executable = path.join(extracted, target.executable); await readFile(executable); if (targetId !== 'win32-x64') await chmod(executable, 0o755); await writeFile( path.join(extracted, '.niancode-python.json'), `${JSON.stringify(markerFor(targetId, target), null, 2)}\n`, 'utf8', ); await rm(destination, { recursive: true, force: true }); await rename(extracted, destination); return destination; } finally { await rm(temporary, { recursive: true, force: true }); } } function selectedTargets(argv) { if (argv.includes('--all')) return Object.keys(PYTHON_TARGETS); const platformArg = argv.find((value) => value.startsWith('--platform=')); if (platformArg) { const platform = platformArg.slice('--platform='.length); const targets = PLATFORM_TARGETS[platform]; if (!targets) throw new Error(`Unsupported bundled Python platform: ${platform}`); return targets; } const targetArg = argv.find((value) => value.startsWith('--target=')); if (targetArg) return [targetArg.slice('--target='.length)]; return [`${os.platform()}-${os.arch()}`]; } async function main() { for (const targetId of selectedTargets(process.argv.slice(2))) { const destination = await stagePythonTarget(targetId); process.stdout.write(`[python] staged ${targetId} at ${destination}\n`); } } if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { main().catch((error) => { process.stderr.write(`[python] ${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 1; }); }