174 lines
7.5 KiB
JavaScript
174 lines
7.5 KiB
JavaScript
/**
|
||||
|
|
* ClassIsland 联动插件构建脚本:`pnpm build:classisland-plugin` / `pnpm dist:classisland-plugin`
|
|||
|
|
*
|
|||
|
|
* 用法:
|
|||
|
|
* node scripts/build.mjs # Release 构建,产物在 packages/classisland-plugin/bin/Release
|
|||
|
|
* node scripts/build.mjs --cipx # 追加打包 .cipx(ClassIsland 插件包)并归集到 releases/classisland-plugin/
|
|||
|
|
* 额外参数会原样透传给 `dotnet build`(例如 `-p:ClassIslandPluginSdkVersion=2.1.1.1`)
|
|||
|
|
*
|
|||
|
|
* 为什么需要这层包装(本机踩过的坑,写在这里免得换台机器又踩一遍):
|
|||
|
|
* 1. `dotnet` 默认把 NuGet 缓存与临时文件写在 C 盘(`%USERPROFILE%\.nuget`、`%TEMP%`),
|
|||
|
|
* 本机 C 盘紧张时会直接构建失败。这里统一改到仓库内 `.cache/`(已 gitignore)。
|
|||
|
|
* 2. 插件包(.cipx)的最后一步要调 PowerShell 生成哈希清单,SDK 默认用 `pwsh`;
|
|||
|
|
* 没有 pwsh 的机器会失败,因此这里探测一次并显式传给 MSBuild。
|
|||
|
|
* 3. 构建完顺手校验"清单 / 程序集 / 版本号"三者一致,避免打出装不上的包。
|
|||
|
|
*/
|
|||
|
|
import { spawnSync } from 'node:child_process';
|
|||
|
|
import fs from 'node:fs';
|
|||
|
|
import path from 'node:path';
|
|||
|
|
import { fileURLToPath } from 'node:url';
|
|||
|
|
|
|||
|
|
const pluginDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|||
|
|
const repoRoot = path.resolve(pluginDir, '..', '..');
|
|||
|
|
const cacheDir = path.join(repoRoot, '.cache');
|
|||
|
|
const releasesDir = path.join(repoRoot, 'releases', 'classisland-plugin');
|
|||
|
|
|
|||
|
|
const argv = process.argv.slice(2);
|
|||
|
|
const withCipx = argv.includes('--cipx');
|
|||
|
|
/** `--` 之后的参数原样透传给 dotnet build;其余以 - 开头的参数同样透传 */
|
|||
|
|
const passthrough = argv.filter((item) => item !== '--cipx' && item !== '--');
|
|||
|
|
|
|||
|
|
const log = (message) => console.log(`[classisland-plugin] ${message}`);
|
|||
|
|
|
|||
|
|
/* ------------------------------------------------------------ 1. 环境准备 */
|
|||
|
|
|
|||
|
|
const env = { ...process.env };
|
|||
|
|
for (const sub of ['nuget', 'tmp', 'dotnet-home']) {
|
|||
|
|
fs.mkdirSync(path.join(cacheDir, sub), { recursive: true });
|
|||
|
|
}
|
|||
|
|
const setDefault = (key, value) => {
|
|||
|
|
if (!env[key]) env[key] = value;
|
|||
|
|
};
|
|||
|
|
setDefault('NUGET_PACKAGES', path.join(cacheDir, 'nuget'));
|
|||
|
|
setDefault('TEMP', path.join(cacheDir, 'tmp'));
|
|||
|
|
setDefault('TMP', env.TEMP);
|
|||
|
|
setDefault('DOTNET_CLI_HOME', path.join(cacheDir, 'dotnet-home'));
|
|||
|
|
env.DOTNET_NOLOGO = '1';
|
|||
|
|
env.DOTNET_CLI_TELEMETRY_OPTOUT = '1';
|
|||
|
|
|
|||
|
|
/** 可执行文件探测:先看显式配置,再问 PATH,最后试常见安装路径 */
|
|||
|
|
function resolveExecutable(envKey, names, fallbacks) {
|
|||
|
|
const candidates = [];
|
|||
|
|
if (env[envKey]) candidates.push(env[envKey]);
|
|||
|
|
const finder = process.platform === 'win32' ? 'where' : 'which';
|
|||
|
|
for (const name of names) {
|
|||
|
|
const found = spawnSync(finder, [name], { encoding: 'utf8', env });
|
|||
|
|
if (found.status === 0) {
|
|||
|
|
for (const line of String(found.stdout ?? '').split(/\r?\n/)) candidates.push(line.trim());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
candidates.push(...fallbacks);
|
|||
|
|
for (const candidate of candidates) {
|
|||
|
|
if (!candidate) continue;
|
|||
|
|
try {
|
|||
|
|
if (fs.statSync(candidate).isFile()) return candidate;
|
|||
|
|
} catch {
|
|||
|
|
/* 继续试下一个 */
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const dotnet = resolveExecutable(
|
|||
|
|
'DOTNET_EXE',
|
|||
|
|
['dotnet'],
|
|||
|
|
process.platform === 'win32'
|
|||
|
|
? [path.join(env.ProgramFiles ?? 'C:\\Program Files', 'dotnet', 'dotnet.exe')]
|
|||
|
|
: ['/usr/local/bin/dotnet', '/usr/bin/dotnet'],
|
|||
|
|
);
|
|||
|
|
if (!dotnet) {
|
|||
|
|
throw new Error('未找到 dotnet SDK(需要 .NET 8 SDK)。安装后用 DOTNET_EXE 指定完整路径。');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const powerShell = resolveExecutable(
|
|||
|
|
'POWERSHELL_EXE',
|
|||
|
|
['pwsh', 'powershell'],
|
|||
|
|
process.platform === 'win32'
|
|||
|
|
? [path.join(env.SystemRoot ?? 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')]
|
|||
|
|
: [],
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
/* ------------------------------------------------------------ 2. 构建 */
|
|||
|
|
|
|||
|
|
const buildArgs = ['build', '-c', 'Release'];
|
|||
|
|
if (withCipx) {
|
|||
|
|
buildArgs.push('-p:CreateCipx=true');
|
|||
|
|
// SDK 的 cipx 目标默认调 pwsh;这里按探测结果显式指定,避免"没有 pwsh 就打包失败"
|
|||
|
|
if (powerShell) buildArgs.push(`-p:PowershellBinaryName=${powerShell}`);
|
|||
|
|
}
|
|||
|
|
buildArgs.push(...passthrough);
|
|||
|
|
|
|||
|
|
log(`dotnet build(缓存目录 ${path.relative(repoRoot, cacheDir)})`);
|
|||
|
|
const build = spawnSync(dotnet, buildArgs, { cwd: pluginDir, stdio: 'inherit', env });
|
|||
|
|
if (build.error) throw new Error(`无法启动 dotnet:${build.error.message}`);
|
|||
|
|
if (build.status !== 0) throw new Error(`dotnet build 失败(退出码 ${build.status})`);
|
|||
|
|
|
|||
|
|
/* ------------------------------------------------------------ 3. 产物自检 */
|
|||
|
|
|
|||
|
|
const outputDir = path.join(pluginDir, 'bin', 'Release');
|
|||
|
|
const manifestPath = path.join(outputDir, 'manifest.yml');
|
|||
|
|
const assemblyPath = path.join(outputDir, 'ClassHelper.ClassIslandPlugin.dll');
|
|||
|
|
|
|||
|
|
for (const required of [manifestPath, assemblyPath, path.join(outputDir, 'icon.png')]) {
|
|||
|
|
if (!fs.existsSync(required)) {
|
|||
|
|
throw new Error(`构建产物缺少 ${path.relative(pluginDir, required)},ClassIsland 会加载失败`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const manifest = fs.readFileSync(manifestPath, 'utf8');
|
|||
|
|
// prettier 会把 YAML 字符串统一成单引号,因此两种引号都要能剥掉
|
|||
|
|
const entrance = /^entranceAssembly:\s*(.+?)\s*$/m.exec(manifest)?.[1]?.replace(/^['"]|['"]$/g, '');
|
|||
|
|
if (entrance !== path.basename(assemblyPath)) {
|
|||
|
|
throw new Error(
|
|||
|
|
`manifest.yml 的 entranceAssembly(${entrance})与程序集(${path.basename(assemblyPath)})不一致`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
const manifestVersion = /^version:\s*([\d.]+)/m.exec(manifest)?.[1];
|
|||
|
|
const bridgeSource = fs.readFileSync(path.join(pluginDir, 'src', 'Services', 'BridgeService.cs'), 'utf8');
|
|||
|
|
const codeVersion = /PluginVersion = "([\d.]+)"/.exec(bridgeSource)?.[1];
|
|||
|
|
if (!manifestVersion || manifestVersion !== codeVersion) {
|
|||
|
|
throw new Error(
|
|||
|
|
`版本号不一致:manifest.yml=${manifestVersion ?? '缺失'},BridgeService.cs=${codeVersion ?? '缺失'}`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
log(`产物自检通过(版本 ${manifestVersion},入口程序集 ${entrance})`);
|
|||
|
|
|
|||
|
|
/* ------------------------------------------------------------ 4. 打包归集 */
|
|||
|
|
|
|||
|
|
if (!withCipx) {
|
|||
|
|
log('完成。加 --cipx 可同时打包成 ClassIsland 插件包(.cipx)');
|
|||
|
|
process.exit(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const cipxDir = path.join(pluginDir, 'cipx');
|
|||
|
|
const cipxFile = path.join(cipxDir, 'ClassHelper.ClassIslandPlugin.cipx');
|
|||
|
|
if (!fs.existsSync(cipxFile)) {
|
|||
|
|
throw new Error(`未生成插件包:${path.relative(pluginDir, cipxFile)}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 归集到 releases/classisland-plugin/(与客户端、服务端交付产物同级;releases/ 不入库)
|
|||
|
|
fs.rmSync(releasesDir, { recursive: true, force: true });
|
|||
|
|
fs.mkdirSync(releasesDir, { recursive: true });
|
|||
|
|
fs.copyFileSync(cipxFile, path.join(releasesDir, path.basename(cipxFile)));
|
|||
|
|
|
|||
|
|
// 免安装目录:解压即用,直接放进 ClassIsland 的 Plugins 目录也能加载
|
|||
|
|
const portableDir = path.join(releasesDir, 'ClassHelper.ClassIslandPlugin');
|
|||
|
|
fs.mkdirSync(portableDir, { recursive: true });
|
|||
|
|
for (const name of fs.readdirSync(outputDir)) {
|
|||
|
|
const from = path.join(outputDir, name);
|
|||
|
|
if (!fs.statSync(from).isFile()) continue;
|
|||
|
|
// 依赖清单与调试符号对加载没影响,但会让老师误以为"要装一堆东西",因此只带最小集合
|
|||
|
|
if (!['ClassHelper.ClassIslandPlugin.dll', 'manifest.yml', 'icon.png', 'README.md'].includes(name)) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
fs.copyFileSync(from, path.join(portableDir, name));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (const file of fs.readdirSync(cipxDir)) {
|
|||
|
|
fs.copyFileSync(path.join(cipxDir, file), path.join(releasesDir, file));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log(`插件包已归集到 ${path.relative(repoRoot, releasesDir)}`);
|
|||
|
|
for (const file of fs.readdirSync(releasesDir)) {
|
|||
|
|
log(` ${file}`);
|
|||
|
|
}
|