import { execFileSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs'; import { dirname, isAbsolute, join, parse, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; const DEFAULT_OUTPUT = '.kt-workspace/napcat-desktop-cn-build'; const DEFAULT_UPSTREAM_BASE = '5c18a62530d87dbadf53d267002894faa6ca7e90'; function readArg(name, fallback = '') { const index = process.argv.indexOf(`--${name}`); return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback; } function sha256File(filePath) { return createHash('sha256').update(readFileSync(filePath)).digest('hex'); } function listDirectory(directory) { return readdirSync(directory).sort(); } function sha256Directory(directory) { const hash = createHash('sha256'); const stack = [directory]; const files = []; while (stack.length > 0) { const current = stack.pop(); for (const entry of listDirectory(current)) { const absolute = join(current, entry); if (statSync(absolute).isDirectory()) { stack.push(absolute); } else { files.push(absolute); } } } files.sort(); for (const file of files) { hash.update(relative(directory, file).split(sep).join('/')); hash.update('\0'); hash.update(readFileSync(file)); hash.update('\0'); } return hash.digest('hex'); } function gitCommit(repoRoot) { return execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8', }).trim(); } function copyIntoContext(source, target) { cpSync(source, target, { recursive: true }); } function isInsideDirectory(parent, candidate) { const relativePath = relative(parent, candidate); return Boolean(relativePath) && !relativePath.startsWith('..') && !isAbsolute(relativePath); } function assertSafeOutputRoot(outputRootToCheck, apiRoot, napcatRootToCheck) { const workspaceRoot = resolve(apiRoot, '.kt-workspace'); const forbiddenRoots = new Set([ parse(outputRootToCheck).root, resolve(apiRoot), resolve(apiRoot, '..', '..'), workspaceRoot, resolve(napcatRootToCheck), dirname(resolve(napcatRootToCheck)), ]); if (forbiddenRoots.has(resolve(outputRootToCheck))) { throw new Error(`Refusing to delete unsafe output root: ${outputRootToCheck}`); } if (!isInsideDirectory(workspaceRoot, outputRootToCheck)) { throw new Error(`Output root must stay inside ${workspaceRoot}`); } } const apiRoot = resolve(fileURLToPath(new URL('..', import.meta.url))); const napcatRootArg = readArg('napcat-root'); const napcatRoot = resolve(napcatRootArg); const outputRoot = resolve(apiRoot, readArg('out', DEFAULT_OUTPUT)); const upstreamBaseCommit = readArg('upstream-base-commit', DEFAULT_UPSTREAM_BASE); const upstreamReleaseTag = readArg('upstream-release-tag', 'unknown'); const upstreamReleaseCommit = readArg('upstream-release-commit', upstreamBaseCommit); const napcatBaseImageDigest = readArg('napcat-base-image-digest', ''); const jenkinsBuildUrl = readArg('jenkins-build-url', ''); const shellDist = resolve(napcatRoot, 'packages/napcat-shell/dist'); const napcatMjs = resolve(shellDist, 'napcat.mjs'); if (!napcatRootArg || !existsSync(napcatRoot)) { throw new Error('--napcat-root must point to the NapCatQQ fork repository'); } if (!existsSync(napcatMjs)) { throw new Error(`NapCat shell build output is missing: ${napcatMjs}`); } assertSafeOutputRoot(outputRoot, apiRoot, napcatRoot); rmSync(outputRoot, { force: true, recursive: true }); mkdirSync(resolve(outputRoot, 'ci/napcat-desktop-cn'), { recursive: true }); copyIntoContext( resolve(apiRoot, 'ci/napcat-desktop-cn/Dockerfile'), resolve(outputRoot, 'ci/napcat-desktop-cn/Dockerfile'), ); copyIntoContext( resolve(apiRoot, 'ci/napcat-desktop-cn/verify.sh'), resolve(outputRoot, 'ci/napcat-desktop-cn/verify.sh'), ); copyIntoContext( resolve(apiRoot, 'ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh'), resolve(outputRoot, 'ci/napcat-desktop-cn/entrypoint-device-profile.patch.sh'), ); copyIntoContext(shellDist, resolve(outputRoot, 'NapCat.Shell')); const marker = { builtAt: new Date().toISOString(), distSha256: sha256Directory(shellDist), forkCommit: gitCommit(napcatRoot), jenkinsBuildUrl, napcatBaseImageDigest, napcatMjsSha256: sha256File(napcatMjs), upstreamBaseCommit, upstreamReleaseCommit, upstreamReleaseTag, }; writeFileSync( resolve(outputRoot, 'ci/napcat-desktop-cn/fork-artifact.json'), `${JSON.stringify(marker, null, 2)}\n`, 'utf8', ); process.stdout.write(`${JSON.stringify({ marker, outputRoot, }, null, 2)}\n`);