blob: 380cac3e39f8a21680a4677da7bfbbf774e0b50a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createHash } from 'node:crypto';
import { mkdir, open, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { execTar } from '../asf-source-release.mjs';
import { digest, readV8Entry, v8Files } from './v8-cache.mjs';
/** Pins are read from the verified Maka source, never from the download server. */
export async function downloadV8({ pin, directory, key, target }) {
if (!/^[0-9a-f]{64}$/.test(pin?.sha256) || new URL(pin.url).protocol !== 'https:')
throw new Error('Invalid V8 prebuilt pin');
await mkdir(directory, { recursive: true });
const archive = join(directory, 'bundle.tar.gz');
const response = await fetch(pin.url, { signal: AbortSignal.timeout(300_000) });
if (!response.ok || !response.body) {
await response.body?.cancel();
throw new Error('V8 prebuilt download failed: HTTP ' + response.status + ' ' + pin.url);
}
const file = await open(archive, 'wx');
const hash = createHash('sha256');
let bytes = 0;
try {
for await (const chunk of response.body) {
bytes += chunk.byteLength;
if (bytes > 128 * 1024 * 1024) throw new Error('V8 prebuilt download exceeds 128 MiB');
hash.update(chunk);
await file.writeFile(chunk);
}
} finally {
await file.close();
}
if (hash.digest('hex') !== pin.sha256) throw new Error('V8 prebuilt SHA-256 mismatch');
const files = v8Files(target);
const list = execTar(archive, ['-tzf'], {
encoding: 'utf8',
maxBuffer: 64 * 1024,
timeout: 60_000,
});
if (
JSON.stringify(list.trim().split(/\r?\n/).sort()) !== JSON.stringify(Object.keys(files).sort())
)
throw new Error('V8 prebuilt contains unexpected archive entries');
const kinds = execTar(archive, ['-tvzf'], {
encoding: 'utf8',
maxBuffer: 64 * 1024,
timeout: 60_000,
});
if (
kinds
.trim()
.split(/\r?\n/)
.some((line) => !line.startsWith('-'))
)
throw new Error('V8 prebuilt entries must be regular files');
const extracted = join(directory, 'files');
await mkdir(extracted);
// Extract to stdout individually: archive paths/links never control filesystem writes.
for (const [name, limit] of Object.entries(files)) {
const contents = execTar(archive, ['-xOzf', name], { maxBuffer: limit, timeout: 60_000 });
if (!contents.length) throw new Error('Empty V8 prebuilt file: ' + name);
await writeFile(join(extracted, name), contents, { flag: 'wx' });
}
const result = await readV8Entry(extracted, key, target);
console.error('Using pinned V8 prebuilt: ' + key);
return result;
}
/** Export only the validated bundle; promotion must use these exact archive bytes. */
export async function packV8({ directory, key, target, output, verifiedBy }) {
if (!/^[0-9a-f]{64}$/.test(key) || !/^[a-z0-9_-]+$/.test(target))
throw new Error('Invalid V8 prebuilt identity');
const entry = join(resolve(directory), key);
await readV8Entry(entry, key, target);
await mkdir(output, { recursive: true });
const name = 'v8-' + target + '-' + key + '.tar.gz';
const archive = resolve(output, name);
execTar(archive, ['-czf', '--format=ustar', '-C', entry, ...Object.keys(v8Files(target))], {
timeout: 180_000,
});
const result = {
key,
target,
archive: name,
sha256: await digest(archive),
...(verifiedBy ? { verifiedBy } : {}),
};
await writeFile(join(output, key + '.json'), JSON.stringify(result, null, 2) + '\n');
return result;
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
if (process.argv.length !== 6)
throw new Error('Usage: v8-prebuilt.mjs <cache> <key> <target> <output>');
const [, , directory, key, target, output] = process.argv;
console.log(JSON.stringify(await packV8({ directory, key, target, output })));
}