/* eslint-disable import/no-relative-packages, no-console, no-restricted-globals */ import { createHash } from 'node:crypto' import path from 'node:path' import * as fs from 'node:fs' import { spawn } from 'node:child_process' import { Readable } from 'node:stream' import { fileURLToPath } from 'node:url' import * as glob from 'glob' import { getCurrentBranch, getCurrentCommit } from '../../scripts/git-utils.js' const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url))) const GITHUB_TOKEN = process.env.GITHUB_TOKEN let SKIP_PREBUILT = process.env.BUILD_FOR_DOCS === '1' if (!GITHUB_TOKEN && !SKIP_PREBUILT) { console.warn('GITHUB_TOKEN is required to publish crypto-node, skipping prebuilt artifacts') SKIP_PREBUILT = true } const GITHUB_HEADERS = { 'Authorization': `Bearer ${GITHUB_TOKEN}`, 'Content-Type': 'application/json', 'X-GitHub-Api-Version': '2022-11-28', } const API_PREFIX = 'https://api.github.com/repos/mtcute/mtcute/actions/workflows/node-prebuilt.yaml' const PLATFORMS = ['ubuntu', 'macos', 'windows'] async function findArtifactsByHash(hash) { const runs = await fetch(`${API_PREFIX}/runs?per_page=100`, { headers: GITHUB_HEADERS }).then(r => r.json()) for (const run of runs.workflow_runs) { if (run.conclusion !== 'success' || run.status !== 'completed') continue const artifacts = await fetch(`${run.url}/artifacts`, { headers: GITHUB_HEADERS }) .then(r => r.json()) .then(r => r.artifacts) for (const it of artifacts) { if (it.expired) continue const parts = it.name.split('-') if (parts[0] === 'prebuilt' && PLATFORMS.includes(parts[1]) && parts[3] === hash) { return artifacts } } } return null } async function runWorkflow(commit, hash) { const createRes = await fetch(`${API_PREFIX}/dispatches`, { method: 'POST', headers: GITHUB_HEADERS, body: JSON.stringify({ ref: getCurrentBranch(), inputs: { commit, hash }, }), }) if (createRes.status !== 204) { const text = await createRes.text() throw new Error(`Failed to run workflow: ${createRes.status} ${text}`) } // wait for the workflow to finish // github api is awesome and doesn't return the run id, so let's just assume it's the last one await new Promise(resolve => setTimeout(resolve, 5000)) const runsRes = await fetch(`${API_PREFIX}/runs`, { headers: GITHUB_HEADERS, }).then(r => r.json()) let run = runsRes.workflow_runs[0] while (run.status === 'queued' || run.status === 'in_progress') { await new Promise(resolve => setTimeout(resolve, 5000)) run = await fetch(run.url, { headers: GITHUB_HEADERS }).then(r => r.json()) } if (run.status !== 'completed') { throw new Error(`Workflow ${run.id} failed: ${run.status}`) } if (run.conclusion !== 'success') { throw new Error(`Workflow ${run.id} failed: ${run.conclusion}`) } // fetch artifacts const artifacts = await fetch(`${run.url}/artifacts`, { headers: GITHUB_HEADERS }) .then(r => r.json()) .then(r => r.artifacts) // validate their names for (const it of artifacts) { const parts = it.name.split('-') if (parts[0] !== 'prebuilt' || !PLATFORMS.includes(parts[1]) || parts[3] !== hash) { throw new Error(`Invalid artifact name: ${it.name}`) } } return artifacts } async function extractArtifacts(artifacts) { fs.mkdirSync(path.join(__dirname, 'dist/prebuilds'), { recursive: true }) await Promise.all( artifacts.map(async (it) => { const platform = it.name.split('-').slice(1, 3).join('-') const res = await fetch(it.archive_download_url, { headers: GITHUB_HEADERS, redirect: 'manual', }) if (res.status !== 302) { const text = await res.text() throw new Error(`Failed to download artifact ${it.name}: ${res.status} ${text}`) } const zip = await fetch(res.headers.get('location')) const outFile = path.join(__dirname, 'dist/prebuilds', `${platform}.zip`) const stream = fs.createWriteStream(outFile) await new Promise((resolve, reject) => { stream.on('finish', resolve) stream.on('error', reject) Readable.fromWeb(zip.body).pipe(stream) }) // extract the zip await new Promise((resolve, reject) => { const child = spawn('unzip', [outFile, '-d', path.join(__dirname, 'dist/prebuilds')], { stdio: 'inherit', }) child.on('exit', (code) => { if (code !== 0) { reject(new Error(`Failed to extract ${outFile}: ${code}`)) } else { resolve() } }) }) fs.unlinkSync(outFile) }), ) } export default () => ({ async final({ packageDir, outDir }) { const libDir = path.resolve(packageDir, 'lib') if (!SKIP_PREBUILT) { // generate sources hash const hashes = [] for (const file of glob.sync(path.join(libDir, '**/*'))) { const hash = createHash('sha256') hash.update(fs.readFileSync(file)) hashes.push(hash.digest('hex')) } const hash = createHash('sha256') .update(hashes.join('\n')) .digest('hex') console.log(hash) console.log('[i] Checking for prebuilt artifacts for %s', hash) let artifacts = await findArtifactsByHash(hash) if (!artifacts) { console.log('[i] No artifacts found, running workflow') artifacts = await runWorkflow(getCurrentCommit(), hash) } console.log('[i] Extracting artifacts') await extractArtifacts(artifacts) } // copy native sources and binding.gyp file fs.cpSync(libDir, path.join(outDir, 'lib'), { recursive: true }) const bindingGyp = fs.readFileSync(path.join(packageDir, 'binding.gyp'), 'utf8') fs.writeFileSync( path.join(outDir, 'binding.gyp'), bindingGyp // replace paths to crypto .replace(/"\.\.\/crypto/g, '"crypto'), ) // for some unknown fucking reason ts doesn't do this fs.copyFileSync(path.join(packageDir, 'src/native.cjs'), path.join(outDir, 'native.cjs')) }, })