Makelore 2.0 initial clean snapshot
This commit is contained in:
137
scripts/download-bundled-python.mjs
Normal file
137
scripts/download-bundled-python.mjs
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user