UNPKG

oimp

Version:

A CLI tool for generating OI problem and packages

237 lines (223 loc) 9.35 kB
const fs = require("fs"); const path = require("path"); const yaml = require("js-yaml"); const { mkdirp } = require("mkdirp"); const chalk = require("chalk"); const { execSync } = require('child_process'); const ora = require('ora').default; const util = require('util'); const exec = util.promisify(require('child_process').exec); const fsp = fs.promises; // 获取模板内容 function getTemplate(templateName) { return fs.readFileSync( path.join(__dirname, "templates", templateName), "utf-8" ); } // 写入文件,如果目录不存在则创建 function writeFileWithDir(filePath, content) { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { mkdirp.sync(dir); } fs.writeFileSync(filePath, content); } // 读取 YAML 文件 function readYaml(filePath) { try { return yaml.load(fs.readFileSync(filePath, "utf-8")); } catch (e) { console.error( chalk.red(`Error reading YAML file ${filePath}: ${e.message}`) ); return null; } } // 写入 YAML 文件 function writeYaml(filePath, data) { writeFileWithDir(filePath, yaml.dump(data)); } // 检查文件是否存在 function checkFileExists(filePath) { return fs.existsSync(filePath); } // 编译辅助函数(异步+动画) async function canCompileAsync(file, output, label) { try { await fsp.access(file); } catch { return false; } const spinner = ora(`正在编译 ${label} ...`).start(); try { await exec(`g++ -O2 -std=c++14 -o "${output}" "${file}"`); spinner.succeed(`${label} 编译成功`); return true; } catch (e) { spinner.fail(`${label} 编译失败`); if (e.stderr) { console.error(`编译错误信息:\n${e.stderr}`); } else if (e.stdout) { console.error(`编译输出:\n${e.stdout}`); } else { console.error(e); } return false; } } // 辅助函数:判断文件内容是否与模板一致 async function isFileSameAsTemplate(filePath, templateName) { try { await fsp.access(filePath); const userContent = (await fsp.readFile(filePath, 'utf-8')).replace(/\r\n/g, '\n'); const templateContent = (await fsp.readFile(path.join(__dirname, 'templates', templateName), 'utf-8')).replace(/\r\n/g, '\n'); return userContent === templateContent; } catch { return false; } } // 检查并推进 checklist 状态(全异步) async function updateChecklistStatus(problemDir) { const statusPath = path.join(problemDir, 'status.json'); try { await fsp.access(statusPath); } catch { return; } const status = JSON.parse(await fsp.readFile(statusPath, 'utf-8')); const srcDir = path.join(problemDir, 'src'); const checkBinDir = path.join(srcDir, '.check_bin'); try { await fsp.access(checkBinDir); } catch { await fsp.mkdir(checkBinDir, { recursive: true }); } // 1. 检查 mtime 变更 const files = ['generator.cpp', 'validator.cpp', 'std.cpp'].map(f => path.join(srcDir, f)); const mtimePath = path.join(problemDir, '.snapshots', 'file-mtime.json'); let lastMtime = {}; try { lastMtime = JSON.parse(await fsp.readFile(mtimePath, 'utf-8')); } catch {} const oldMtime = { ...lastMtime }; let changed = false; let currentMtime = {}; let changedMap = { 'std.cpp': false, 'generator.cpp': false, 'validator.cpp': false }; for (const file of files) { const key = path.basename(file); let mtime = 0; try { mtime = (await fsp.stat(file)).mtimeMs; } catch {} currentMtime[key] = mtime; if (lastMtime[key] && lastMtime[key] !== mtime) { changed = true; changedMap[key] = true; } lastMtime[key] = mtime; } // 优化:若 mtime 没变且状态为 done,直接返回 const allDone = status.std && status.std.status === 'done' && status.generator && status.generator.status === 'done' && status.validator && status.validator.status === 'done'; let mtimeUnchanged = true; for (const file of files) { const key = path.basename(file); if ((oldMtime[key] || 0) !== currentMtime[key]) mtimeUnchanged = false; } if (mtimeUnchanged && allDone) { return status; } if (changed) { status.testsample = { desc: '样例检测', status: 'need-redo' }; status.data = { desc: '测评数据', status: 'need-redo' }; status.check = { desc: '完整性检查', status: 'need-redo' }; status.package = { desc: '打包', status: 'need-redo' }; } // 写入 mtime 前确保目录存在 await fsp.mkdir(path.dirname(mtimePath), { recursive: true }); await fsp.writeFile(mtimePath, JSON.stringify(lastMtime, null, 2)); // 2. 题面检查 const problemMd = path.join(problemDir, 'problem_zh.md'); try { const content = (await fsp.readFile(problemMd, 'utf-8')).trim(); if (content && !/在这里写/.test(content)) { status.problem = { desc: '题面', status: 'done' }; } } catch {} // 3. 检查 std/generator/validator/checker 是否与模板一致 const stdFile = path.join(srcDir, 'std.cpp'); const generatorFile = path.join(srcDir, 'generator.cpp'); const validatorFile = path.join(srcDir, 'validator.cpp'); const checkerFile = path.join(srcDir, 'checker.cpp'); const stdOut = path.join(checkBinDir, 'std'); const generatorOut = path.join(checkBinDir, 'generator'); const validatorOut = path.join(checkBinDir, 'validator'); const checkerOut = path.join(srcDir, 'checker'); let stdIsTemplate = await isFileSameAsTemplate(stdFile, 'std.cpp'); let generatorIsTemplate = await isFileSameAsTemplate(generatorFile, 'generator.cpp'); let validatorIsTemplate = await isFileSameAsTemplate(validatorFile, 'validator.cpp'); let checkerIsTemplate = await isFileSameAsTemplate(checkerFile, 'checker.cpp'); // 4. 只对变更的文件编译检查,未变更的直接复用 checklist 状态 let compileResults = [true, true, true]; if (!stdIsTemplate) { if (changedMap['std.cpp']) { compileResults[0] = await canCompileAsync(stdFile, stdOut, '标准程序 std.cpp'); } else { compileResults[0] = status.std && status.std.status === 'done'; } } if (!generatorIsTemplate) { if (changedMap['generator.cpp']) { compileResults[1] = await canCompileAsync(generatorFile, generatorOut, '数据生成器 generator.cpp'); } else { compileResults[1] = status.generator && status.generator.status === 'done'; } } if (!validatorIsTemplate) { if (changedMap['validator.cpp']) { compileResults[2] = await canCompileAsync(validatorFile, validatorOut, '数据验证器 validator.cpp'); } else { compileResults[2] = status.validator && status.validator.status === 'done'; } } // checker.cpp 自动编译(仅在变更时编译) let checkerCompileResult = true; let checkerChanged = false; let checkerMtime = 0; try { checkerMtime = (await fsp.stat(checkerFile)).mtimeMs; } catch {} if (!lastMtime) lastMtime = {}; if (lastMtime['checker.cpp'] !== checkerMtime) checkerChanged = true; lastMtime['checker.cpp'] = checkerMtime; if (checkerChanged) { try { await fsp.access(checkerFile); await exec(`g++ -O2 -std=c++14 -o "${checkerOut}" "${checkerFile}"`); checkerCompileResult = true; } catch (e) { checkerCompileResult = false; console.error('checker.cpp 编译失败:'); if (e.stderr) { console.error(`编译错误信息:\n${e.stderr}`); } else if (e.stdout) { console.error(`编译输出:\n${e.stdout}`); } else { console.error(e); } } } else { // 未变更时复用上次状态 checkerCompileResult = status.checker && status.checker.status === 'done'; } status.std = { desc: '标准程序', status: compileResults[0] ? 'done' : 'pending' }; status.generator = { desc: '数据生成器', status: compileResults[1] ? 'done' : 'pending' }; status.validator = { desc: '数据验证器', status: compileResults[2] ? 'done' : 'pending' }; status.checker = { desc: '数据检查器', status: checkerCompileResult ? 'done' : 'pending' }; // 5. 只要有一项不是 done,回退 testsample/data/check/package const allReady2 = status.std && status.std.status === 'done' && status.generator && status.generator.status === 'done' && status.validator && status.validator.status === 'done'; if (!allReady2) { if (status.testsample && (status.testsample.status === 'done' || status.testsample.status === 'need-redo')) status.testsample.status = 'need-redo'; if (status.data && (status.data.status === 'done' || status.data.status === 'need-redo')) status.data.status = 'need-redo'; if (status.check && (status.check.status === 'done' || status.check.status === 'need-redo')) status.check.status = 'need-redo'; if (status.package && (status.package.status === 'done' || status.package.status === 'need-redo')) status.package.status = 'need-redo'; } // 6. 只有 testsample 为 done,data/check/package 才能 done if (status.testsample && status.testsample.status !== 'done') { if (status.data) status.data.status = 'need-redo'; if (status.check) status.check.status = 'need-redo'; if (status.package) status.package.status = 'need-redo'; } await fsp.writeFile(statusPath, JSON.stringify(status, null, 2)); return status; } module.exports = { getTemplate, writeFileWithDir, readYaml, writeYaml, checkFileExists, updateChecklistStatus, };