feat: complete WorkBuddy parity and Windows release workflow
This commit is contained in:
1 parent
143516b3a8
commit
d5512df549
434 files changed
+54977
-1854
No files matched your search
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Vendors the pinned OMP kernel binary and its license files into resources/omp.
|
||||
*
|
||||
* The binary is not committed to Git. `npm run omp:vendor` restores it from the
|
||||
* pinned upstream release (or from a local copy via `--from <file|directory>`)
|
||||
* and verifies every asset against resources/omp/runtime.lock.json.
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createReadStream, createWriteStream, promises as fs } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { Readable } from 'node:stream'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const project = fileURLToPath(new URL('../', import.meta.url))
|
||||
const directory = path.join(project, 'resources', 'omp')
|
||||
const lockPath = path.join(directory, 'runtime.lock.json')
|
||||
const hashPattern = /^[a-f0-9]{64}$/
|
||||
const namePattern = /^[A-Za-z0-9._-]+$/
|
||||
const releasePrefix = 'https://github.com/can1357/oh-my-pi/releases/download/'
|
||||
const rawPrefix = 'https://raw.githubusercontent.com/can1357/oh-my-pi/'
|
||||
const maxAssetBytes = 512 * 1024 * 1024
|
||||
|
||||
function asset(value, label) {
|
||||
if (!value || typeof value !== 'object' || typeof value.name !== 'string' || !namePattern.test(value.name)
|
||||
|| !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || value.bytes > maxAssetBytes
|
||||
|| typeof value.sha256 !== 'string' || !hashPattern.test(value.sha256)) {
|
||||
throw new Error(`Invalid ${label} entry in runtime.lock.json`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
async function readLock() {
|
||||
const lock = JSON.parse((await fs.readFile(lockPath, 'utf8')).replace(/^\uFEFF/, ''))
|
||||
if (lock.format !== 1 || lock.kernel !== 'omp' || lock.upstream !== 'oh-my-pi' || lock.platform !== 'win32-x64'
|
||||
|| typeof lock.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(lock.version)
|
||||
|| lock.releaseUrl !== `https://github.com/can1357/oh-my-pi/releases/tag/v${lock.version}`
|
||||
|| !Number.isFinite(Date.parse(lock.publishedAt))) {
|
||||
throw new Error('Invalid OMP runtime lock')
|
||||
}
|
||||
const binary = asset(lock.binary, 'binary')
|
||||
const license = asset(lock.license, 'license')
|
||||
const thirdPartyNotices = asset(lock.thirdPartyNotices, 'thirdPartyNotices')
|
||||
if (binary.name !== 'omp.exe' || license.name !== 'LICENSE' || thirdPartyNotices.name !== 'THIRD-PARTY-NOTICES.txt') {
|
||||
throw new Error('OMP runtime lock uses unexpected asset names')
|
||||
}
|
||||
return { lock, binary, license, thirdPartyNotices }
|
||||
}
|
||||
|
||||
async function sha256File(file) {
|
||||
const hash = createHash('sha256')
|
||||
for await (const chunk of createReadStream(file)) hash.update(chunk)
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
async function verifyFile(file, spec) {
|
||||
const stat = await fs.stat(file).catch(() => null)
|
||||
if (!stat?.isFile()) throw new Error(`Missing OMP asset: ${path.relative(project, file)}`)
|
||||
if (stat.size !== spec.bytes) throw new Error(`OMP asset size mismatch: ${spec.name} (${stat.size} != ${spec.bytes})`)
|
||||
const digest = await sha256File(file)
|
||||
if (digest !== spec.sha256) throw new Error(`OMP asset digest mismatch: ${spec.name}`)
|
||||
return digest
|
||||
}
|
||||
|
||||
async function copyVerified(source, target, spec) {
|
||||
if (path.resolve(source) === path.resolve(target)) return verifyFile(target, spec)
|
||||
const stat = await fs.stat(source)
|
||||
const file = stat.isDirectory() ? path.join(source, spec.name) : source
|
||||
const sourceStat = await fs.stat(file)
|
||||
if (!sourceStat.isFile() || sourceStat.size !== spec.bytes) throw new Error(`Local OMP asset has an unexpected size: ${file}`)
|
||||
const digest = await sha256File(file)
|
||||
if (digest !== spec.sha256) throw new Error(`Local OMP asset does not match the pinned release: ${file}`)
|
||||
const temporary = `${target}.part`
|
||||
await fs.copyFile(file, temporary)
|
||||
try {
|
||||
await verifyFile(temporary, spec)
|
||||
await fs.rename(temporary, target)
|
||||
} catch (error) {
|
||||
await fs.rm(temporary, { force: true })
|
||||
throw error
|
||||
}
|
||||
return digest
|
||||
}
|
||||
|
||||
async function download(url, target, spec) {
|
||||
const response = await fetch(url, {
|
||||
redirect: 'follow',
|
||||
headers: { 'user-agent': 'YCode OMP vendor' },
|
||||
signal: AbortSignal.timeout(10 * 60 * 1000)
|
||||
})
|
||||
if (!response.ok || !response.body) throw new Error(`OMP download failed (${response.status}): ${url}`)
|
||||
const declared = response.headers.get('content-length')
|
||||
if (declared && Number(declared) !== spec.bytes) throw new Error(`OMP download size header mismatch: ${spec.name}`)
|
||||
const temporary = `${target}.part`
|
||||
try {
|
||||
await pipeline(Readable.fromWeb(response.body), createWriteStream(temporary))
|
||||
const digest = await verifyFile(temporary, spec)
|
||||
await fs.rename(temporary, target)
|
||||
return digest
|
||||
} catch (error) {
|
||||
await fs.rm(temporary, { force: true })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function releaseUrl(version, name) {
|
||||
const url = `${releasePrefix}v${version}/${name}`
|
||||
if (!url.startsWith(releasePrefix) || !namePattern.test(name)) throw new Error(`Unsafe OMP release URL: ${url}`)
|
||||
return url
|
||||
}
|
||||
|
||||
function rawUrl(version, name) {
|
||||
const url = `${rawPrefix}v${version}/${name}`
|
||||
if (!url.startsWith(rawPrefix) || !namePattern.test(name)) throw new Error(`Unsafe OMP raw URL: ${url}`)
|
||||
return url
|
||||
}
|
||||
|
||||
async function ensureAsset({ lock, spec, source, target, verifyOnly, offline }) {
|
||||
if (await fs.stat(target).catch(() => null)) {
|
||||
const digest = await verifyFile(target, spec)
|
||||
console.log(`[omp] ${spec.name} verified (${spec.bytes} bytes, ${digest.slice(0, 12)}…)`)
|
||||
return
|
||||
}
|
||||
if (verifyOnly) throw new Error(`Missing OMP asset: ${path.relative(project, target)}`)
|
||||
if (source) {
|
||||
if (spec.name !== 'omp.exe') throw new Error(`--from only applies to omp.exe, not ${spec.name}`)
|
||||
const digest = await copyVerified(source, target, spec)
|
||||
console.log(`[omp] ${spec.name} imported from ${source} (${spec.bytes} bytes, ${digest.slice(0, 12)}…)`)
|
||||
return
|
||||
}
|
||||
if (offline) throw new Error(`OMP asset is missing in offline mode: ${path.relative(project, target)}`)
|
||||
try {
|
||||
const digest = await download(releaseUrl(lock.version, spec.name), target, spec)
|
||||
console.log(`[omp] ${spec.name} downloaded (${spec.bytes} bytes, ${digest.slice(0, 12)}…)`)
|
||||
} catch (releaseError) {
|
||||
if (spec.name === 'omp.exe') throw releaseError
|
||||
const digest = await download(rawUrl(lock.version, spec.name), target, spec)
|
||||
console.log(`[omp] ${spec.name} downloaded from raw source (${spec.bytes} bytes, ${digest.slice(0, 12)}…)`)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2)
|
||||
const verifyOnly = args.includes('--verify')
|
||||
const offline = args.includes('--offline')
|
||||
const fromIndex = args.indexOf('--from')
|
||||
const source = fromIndex >= 0 ? args[fromIndex + 1] : null
|
||||
if (fromIndex >= 0 && (!source || source.startsWith('--'))) throw new Error('--from requires a file or directory')
|
||||
|
||||
const { lock, binary, license, thirdPartyNotices } = await readLock()
|
||||
await fs.mkdir(directory, { recursive: true })
|
||||
console.log(`[omp] pinned ${lock.upstream} v${lock.version} (${lock.platform})`)
|
||||
|
||||
await ensureAsset({ lock, spec: binary, source, target: path.join(directory, binary.name), verifyOnly, offline })
|
||||
await ensureAsset({ lock, spec: license, target: path.join(directory, license.name), verifyOnly, offline })
|
||||
await ensureAsset({ lock, spec: thirdPartyNotices, target: path.join(directory, thirdPartyNotices.name), verifyOnly, offline })
|
||||
|
||||
console.log(`[omp] ready: ${path.relative(project, directory)}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[omp] ${error.message}`)
|
||||
process.exitCode = 1
|
||||
})
|
||||
Reference in new issue
Block a user