138 lines
4.9 KiB
JavaScript
138 lines
4.9 KiB
JavaScript
#!/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;
|
|
});
|
|
}
|