68 lines
3.6 KiB
JavaScript
68 lines
3.6 KiB
JavaScript
// Downloads official pinned archives only. Never executes downloaded Windows files.
|
|
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { spawnSync } from 'node:child_process'
|
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
import sevenZip from '7zip-bin'
|
|
import { transformRuntime } from './runtime-transform.mjs'
|
|
import { projectRoot, runtimeRoot, manifest, digest, inventory, verifyRuntimeBundle } from './runtime-bundle.mjs'
|
|
const cache = path.join(projectRoot, '.tmp/runtime-downloads')
|
|
async function renameExtractedDirectory(source, destination) {
|
|
for (let attempt = 0; ; attempt += 1) {
|
|
try {
|
|
await fs.rename(source, destination)
|
|
return
|
|
} catch (error) {
|
|
// Windows scanners can briefly hold freshly extracted executables open.
|
|
if (process.platform !== 'win32' || !['EPERM', 'EACCES', 'EBUSY'].includes(error.code) || attempt >= 9) throw error
|
|
await delay(100 * (attempt + 1))
|
|
}
|
|
}
|
|
}
|
|
await fs.mkdir(cache, { recursive: true })
|
|
const stage = await fs.mkdtemp(path.join(runtimeRoot, '.prepare-'))
|
|
try {
|
|
if (process.platform !== 'win32') await fs.chmod(sevenZip.path7za, 0o755)
|
|
for (const runtime of manifest.runtimes) {
|
|
const archive = path.join(cache, runtime.archive)
|
|
let bytes = await fs.readFile(archive).catch(() => null)
|
|
if (!bytes) {
|
|
console.log(`Downloading ${runtime.id} ${runtime.version} from ${runtime.url}`)
|
|
const response = await fetch(runtime.url, { signal: AbortSignal.timeout(300_000) })
|
|
if (!response.ok) throw new Error(`Download failed: ${response.status} ${runtime.url}`)
|
|
bytes = Buffer.from(await response.arrayBuffer())
|
|
}
|
|
if (digest(bytes, runtime.algorithm, runtime.digestEncoding) !== runtime.digest) throw new Error(`Archive integrity failure: ${runtime.id}; remove ${archive} to redownload`)
|
|
await fs.writeFile(archive, bytes)
|
|
const unpack = path.join(stage, `unpack-${runtime.id}`)
|
|
await fs.mkdir(unpack)
|
|
const result = spawnSync(sevenZip.path7za, ['x', archive, `-o${unpack}`, '-y'], { encoding: 'utf8', windowsHide: true })
|
|
if (result.error || result.status !== 0) throw new Error(`Archive extraction failed: ${runtime.id}: ${result.error || result.stderr || result.stdout}`)
|
|
const target = path.join(stage, runtime.id)
|
|
await renameExtractedDirectory(path.join(unpack, runtime.prefix), target)
|
|
await transformRuntime(runtime.id, target)
|
|
// No loose mutation of the extraction lock. A source update requires reviewed tree digest changes.
|
|
const tree = digest(JSON.stringify(await inventory(target)))
|
|
if (runtime.treeSha256 && tree !== runtime.treeSha256) throw new Error(`Extracted ${runtime.id} tree mismatch: ${tree}`)
|
|
if (!runtime.treeSha256) throw new Error(`Unpinned ${runtime.id} extracted tree: ${tree}`)
|
|
}
|
|
await verifyRuntimeBundle(stage)
|
|
const destination = path.join(runtimeRoot, 'win32-x64')
|
|
const previous = `${destination}.previous`
|
|
await fs.rm(previous, { recursive: true, force: true })
|
|
const exists = await fs.stat(destination).then(() => true, () => false)
|
|
if (exists) await fs.rename(destination, previous)
|
|
try {
|
|
await fs.mkdir(destination)
|
|
for (const runtime of manifest.runtimes) await fs.rename(path.join(stage, runtime.id), path.join(destination, runtime.id))
|
|
} catch (error) {
|
|
await fs.rm(destination, { recursive: true, force: true })
|
|
if (exists) await fs.rename(previous, destination)
|
|
throw error
|
|
}
|
|
await fs.rm(previous, { recursive: true, force: true })
|
|
console.log('Windows x64 runtimes prepared; native Windows smoke tests still required.')
|
|
} finally {
|
|
await fs.rm(stage, { recursive: true, force: true })
|
|
}
|