UNPKG

skia-canvas

Version:

A multi-threaded, GPU-accelerated, Canvas API for Node

156 lines (132 loc) 5.87 kB
import zlib from 'zlib' import stream from 'stream' import crypto from 'crypto' import child_process from 'child_process' import {createReadStream, createWriteStream, existsSync} from 'fs' import {readFile, writeFile, rm} from 'fs/promises' import {resolve} from 'path' import {promisify} from 'util' import {family} from 'detect-libc' import https from 'follow-redirects/https.js' import {HttpsProxyAgent} from 'https-proxy-agent' const pipeline = promisify(stream.pipeline) const exec = promisify(child_process.exec); const ROOT = resolve(`${import.meta.dirname}/..`) const REPO_URL = "https://github.com/samizdatco/skia-canvas" const BINARY_HOST = `${REPO_URL}/releases/download` const BINARY_PATH = `${ROOT}/lib/skia.node` const PACKAGE_JSON = `${ROOT}/package.json` const PROXY_URL = process.env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY || process.env.npm_config_https_proxy || process.env.npm_config_proxy const CARGO_FEATURES = { darwin: "metal,window", linux: "vulkan,window,freetype", win32: "vulkan,window", }[process.platform] class Hasher extends stream.Transform { #digest constructor(options) { super(options) this.hash = crypto.createHash('sha256') } _transform(chunk, encoding, callback) { this.hash.update(chunk) this.push(chunk) callback() } get digest(){ this.#digest = this.#digest || `sha256:${this.hash.digest('hex')}` return this.#digest } } async function config(){ let package_json = JSON.parse(await readFile(PACKAGE_JSON)), {platform, arch} = process, libc = await family() let {version, prebuild} = package_json, triplet = [platform, arch, libc].filter(t=>t).join('-') return {version, triplet, prebuild} } async function snapshot(){ let {version} = await config(), json = (await exec(`gh release view v${version} --json assets`)).stdout, {assets} = JSON.parse(json), hashes = Object.fromEntries(assets.map(({name, digest}) => [name, digest])) exec(`npm pkg set prebuild='${JSON.stringify(hashes)}' --json`) } async function upload(){ let {version, triplet} = await config(), artifact = `${ROOT}/${triplet}.gz` try{ await pipeline( createReadStream(BINARY_PATH), zlib.createGzip(), createWriteStream(artifact) ) await exec(`gh release upload v${version} ${artifact}`) }catch(e){ console.error(e.message) process.exit(1) } } async function download(...args){ if (existsSync(BINARY_PATH)) return // nothing to be done if skia.node already exists let {version, triplet, prebuild} = await config(), url = `${BINARY_HOST}/v${version}/${triplet}.gz`, agent = PROXY_URL ? new HttpsProxyAgent(PROXY_URL) : undefined try{ let body = await new Promise((res, rej) => { https.get(url, {agent}, resp => { let {statusCode:status} = resp if (status == 404) rej(Error(`Prebuilt library not found at "${url}" (HTTP error ${status})`)) else if (status < 200 || status >= 300) rej(Error(`Failed to load prebuilt binary from "${url}" (HTTP error ${status})`)) else res(resp) }) }) console.log(`Fetched prebuilt libary from "${url}"`) // write to /lib/skia.node while also hashing the .gz file let sha = new Hasher() let gunzip = zlib.createGunzip() await pipeline(body, sha, gunzip, createWriteStream(BINARY_PATH)) // verify hash if `prebuild` obj exists in package.json (i.e., this is a published module, not a repo copy) let official = (prebuild || {})[`${triplet}.gz`], actual = sha.digest if (official && actual != official){ await rm(BINARY_PATH, {force:true}) throw Error(`Prebuilt library file '${triplet}.gz' failed integrity check\nDownloaded: ${url}\nExpected: ${official}\nReceived: ${actual}`) } }catch(e){ console.warn(e.message) // optionally fall back to compiling locally if (!args.includes('--or-compile') || !existsSync(`${ROOT}/Cargo.toml`)) process.exit(1) else compile('--fallback') } } function compile(...args){ let optimization = args.includes('custom') || args.includes('dev') ? '' : "--release", customFeatures = args.includes('custom') && (args[args.indexOf('custom')+1] || '').replace(/[^[a-z0-9\_\-\,]/g, ''), features = `--features "${args.includes('custom') ? customFeatures || '' : CARGO_FEATURES}"`, isFallback = args.includes('--fallback'), isSrcRepo = existsSync(`${ROOT}/Cargo.toml`) if (!isSrcRepo) throw Error(`Cannot compile from npm version of skia-canvas: clone source from ${REPO_URL}`) else if (isFallback) console.log("\nAttempting to rebuild locally...") else console.warn(`cargo build ${[optimization, features].filter(s=>s).join(' ')}`) let {status} = child_process.spawnSync( `cargo-cp-artifact -nc ${BINARY_PATH} -- cargo build --message-format=json-render-diagnostics ${optimization} ${features}`, {shell:true, stdio:'inherit'} ) process.exit(status) } async function usage(){ let {triplet} = await config() console.log("usage: prebuild.mjs <action>") console.log("\nactions:") console.log(` compile - build /lib/skia.node from source using locally installed rustc`) console.log(` download - fetch precompiled /lib/skia.node appropriate for this platform (${triplet})`) console.log(` upload - post this platform's skia.node to the ${version} release on GitHub`) console.log(` snapshot - add hashes of all uploaded assets to package.json (for publishing)`) } async function main(){ let cmd = process.argv[2], args = process.argv.slice(3) await ({upload, download, snapshot, compile}[cmd] || usage)(...args) } main()