oimp
Version:
A CLI tool for generating OI problem and packages
237 lines (223 loc) • 9.35 kB
JavaScript
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,
};