33036b65fc
Project CI / AI game creator shell Rust crates (push) Successful in 3m3s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m50s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 6m16s
Project CI / Backend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
tauri-bundler 打包时现场从 GitHub 下载 nsis-3.11.zip 与 nsis_tauri_utils.dll 且不重试, Checkout 的 git clean -fdx 又会删掉工作区内的 target/.tauri,于是每个构建都要重下, 响应一旦被截断只会抛 `io: unexpected end of file`,让发布在 Rust 编译数分钟后失败。 - Jenkins Checkout 的 git clean 增加 -e apps/ai-game-creator-shell/src-tauri/target/.tauri,只保留工具缓存 - 新增 nsis-toolset.mjs:按固定 SHA1 预置 target/.tauri/NSIS,带 4 次重试并把原始归档缓存到工作区外 - 新增 ensure-nsis-toolset.mjs 与 nsis:prepare 脚本,作为 Jenkins 预检入口 - buildRelease 在 Windows 目标打包前接入工具链预置,预置失败即失败关闭 - Jenkins 新增 Tauri NSIS toolchain 阶段,在编译前预置、跑回归测试并校验 makensis 可执行 - 新增 nsis-toolset.test.mjs 并补 3 项 build-release 预置接入测试 - 同步开发运维文档与 shared-memory 排障记录
332 lines
9.3 KiB
JavaScript
332 lines
9.3 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { test } from 'node:test';
|
|
|
|
import JSZip from 'jszip';
|
|
|
|
import {
|
|
ensureNsisToolset,
|
|
extractNsisArchive,
|
|
NSIS_ARCHIVE_ASSET_NAME,
|
|
NSIS_ARCHIVE_TOP_LEVEL_DIR,
|
|
NSIS_REQUIRED_FILES,
|
|
NSIS_TAURI_UTILS_ASSET_NAME,
|
|
NSIS_TOOLSET_DIR_NAME,
|
|
prepareNsisToolsetForRelease,
|
|
resolveArchiveEntryTarget,
|
|
resolveDownloadUrl,
|
|
resolveNsisCacheDir,
|
|
resolveTauriToolsDir,
|
|
sha1Of,
|
|
verifyNsisToolset,
|
|
} from './nsis-toolset.mjs';
|
|
|
|
const appRoot = path.resolve(
|
|
path.dirname(new URL(import.meta.url).pathname),
|
|
'..',
|
|
);
|
|
const silentLogger = { log() {}, warn() {} };
|
|
|
|
function createSandbox() {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'agc-nsis-toolset-'));
|
|
}
|
|
|
|
/** 与真实归档同构的最小 zip:只保留 bundler 必需文件。 */
|
|
async function createArchiveFixture(extraEntries = {}) {
|
|
const zip = new JSZip();
|
|
for (const relativePath of NSIS_REQUIRED_FILES) {
|
|
zip.file(
|
|
`${NSIS_ARCHIVE_TOP_LEVEL_DIR}/${relativePath}`,
|
|
`fixture:${relativePath}`,
|
|
);
|
|
}
|
|
for (const [name, contents] of Object.entries(extraEntries)) {
|
|
zip.file(name, contents);
|
|
}
|
|
return zip.generateAsync({ type: 'nodebuffer' });
|
|
}
|
|
|
|
function fixtureAssets({ archive, utils }) {
|
|
return [
|
|
{
|
|
assetName: NSIS_ARCHIVE_ASSET_NAME,
|
|
url: `https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/${NSIS_ARCHIVE_ASSET_NAME}`,
|
|
sha1: sha1Of(archive),
|
|
},
|
|
{
|
|
assetName: NSIS_TAURI_UTILS_ASSET_NAME,
|
|
url: `https://github.com/tauri-apps/nsis-tauri-utils/releases/download/nsis_tauri_utils-v0.5.3/${NSIS_TAURI_UTILS_ASSET_NAME}`,
|
|
sha1: sha1Of(utils),
|
|
},
|
|
];
|
|
}
|
|
|
|
function fixtureFetch({ archive, utils, failures = 0 }) {
|
|
let remainingFailures = failures;
|
|
const calls = [];
|
|
const fetchImpl = async (url) => {
|
|
calls.push(url);
|
|
if (remainingFailures > 0) {
|
|
remainingFailures -= 1;
|
|
throw new Error('network truncated');
|
|
}
|
|
const body = url.includes(NSIS_TAURI_UTILS_ASSET_NAME) ? utils : archive;
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
statusText: 'OK',
|
|
arrayBuffer: async () => body,
|
|
};
|
|
};
|
|
return { fetchImpl, calls };
|
|
}
|
|
|
|
test('NSIS 工具链目录与 Tauri useLocalToolsDir 配置保持一致', () => {
|
|
const config = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(appRoot, 'src-tauri', 'tauri.windows.conf.json'),
|
|
'utf8',
|
|
),
|
|
);
|
|
assert.equal(config.bundle.useLocalToolsDir, true);
|
|
assert.equal(
|
|
resolveTauriToolsDir(appRoot),
|
|
path.join(appRoot, 'src-tauri', 'target', '.tauri'),
|
|
);
|
|
});
|
|
|
|
test('缓存目录默认落在工作区之外并支持环境变量覆盖', () => {
|
|
assert.equal(
|
|
resolveNsisCacheDir(
|
|
{ AGC_TAURI_NSIS_CACHE_DIR: '/tmp/agc-cache' },
|
|
'linux',
|
|
),
|
|
'/tmp/agc-cache',
|
|
);
|
|
assert.equal(
|
|
resolveNsisCacheDir({ ProgramData: 'D:\\ProgramData' }, 'win32'),
|
|
path.join('D:\\ProgramData', 'genarrative', 'tauri-nsis-cache'),
|
|
);
|
|
assert.ok(
|
|
resolveNsisCacheDir({}, 'linux').endsWith(
|
|
path.join('.cache', 'genarrative', 'tauri-nsis-cache'),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('下载地址支持 tauri bundler 的两套 GitHub 镜像开关', () => {
|
|
const url =
|
|
'https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/nsis-3.11.zip';
|
|
assert.equal(resolveDownloadUrl(url, {}), url);
|
|
assert.equal(
|
|
resolveDownloadUrl(url, {
|
|
TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE:
|
|
'https://mirror.example.com/<owner>/<repo>/<version>/<asset>',
|
|
}),
|
|
'https://mirror.example.com/tauri-apps/binary-releases/nsis-3.11/nsis-3.11.zip',
|
|
);
|
|
assert.equal(
|
|
resolveDownloadUrl(url, {
|
|
TAURI_BUNDLER_TOOLS_GITHUB_MIRROR: 'https://mirror.example.com/',
|
|
}),
|
|
`https://mirror.example.com/${url}`,
|
|
);
|
|
});
|
|
|
|
test('工具链已就绪时零下载复用', async () => {
|
|
const sandbox = createSandbox();
|
|
const toolsDir = path.join(sandbox, '.tauri');
|
|
const cacheDir = path.join(sandbox, 'cache');
|
|
const archive = await createArchiveFixture();
|
|
const utils = Buffer.from('nsis-tauri-utils-dll');
|
|
const assets = fixtureAssets({ archive, utils });
|
|
|
|
await ensureNsisToolset({
|
|
toolsDir,
|
|
cacheDir,
|
|
assets,
|
|
fetchImpl: fixtureFetch({ archive, utils }).fetchImpl,
|
|
logger: silentLogger,
|
|
});
|
|
|
|
let fetchCalls = 0;
|
|
const reused = await ensureNsisToolset({
|
|
toolsDir,
|
|
cacheDir,
|
|
assets,
|
|
fetchImpl: async () => {
|
|
fetchCalls += 1;
|
|
throw new Error('工具链已就绪时不应访问网络');
|
|
},
|
|
logger: silentLogger,
|
|
});
|
|
assert.equal(reused.reused, true);
|
|
assert.deepEqual(reused.downloaded, []);
|
|
assert.equal(fetchCalls, 0);
|
|
assert.equal(reused.nsisDir, path.join(toolsDir, NSIS_TOOLSET_DIR_NAME));
|
|
});
|
|
|
|
test('冷启动时下载、校验、解压并落缓存,重跑走缓存', async () => {
|
|
const sandbox = createSandbox();
|
|
const toolsDir = path.join(sandbox, '.tauri');
|
|
const cacheDir = path.join(sandbox, 'cache');
|
|
const archive = await createArchiveFixture();
|
|
const utils = Buffer.from('nsis-tauri-utils-dll');
|
|
const assets = fixtureAssets({ archive, utils });
|
|
const { fetchImpl, calls } = fixtureFetch({ archive, utils });
|
|
|
|
const result = await ensureNsisToolset({
|
|
toolsDir,
|
|
cacheDir,
|
|
assets,
|
|
fetchImpl,
|
|
logger: silentLogger,
|
|
});
|
|
assert.deepEqual(calls.length, 2);
|
|
assert.deepEqual(result.downloaded, [
|
|
NSIS_ARCHIVE_ASSET_NAME,
|
|
NSIS_TAURI_UTILS_ASSET_NAME,
|
|
]);
|
|
assert.equal(
|
|
verifyNsisToolset(path.join(toolsDir, NSIS_TOOLSET_DIR_NAME), {
|
|
utilsSha1: sha1Of(utils),
|
|
}).ok,
|
|
true,
|
|
);
|
|
assert.equal(
|
|
fs.readFileSync(
|
|
path.join(
|
|
toolsDir,
|
|
NSIS_TOOLSET_DIR_NAME,
|
|
'Plugins/x86-unicode/additional/nsis_tauri_utils.dll',
|
|
),
|
|
'utf8',
|
|
),
|
|
'nsis-tauri-utils-dll',
|
|
);
|
|
|
|
// 第二次构建:清空工作区工具目录后仍应零下载恢复(模拟 Jenkins git clean -fdx)。
|
|
fs.rmSync(path.join(toolsDir, NSIS_TOOLSET_DIR_NAME), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
const offline = await ensureNsisToolset({
|
|
toolsDir,
|
|
cacheDir,
|
|
assets,
|
|
fetchImpl: async () => {
|
|
throw new Error('命中缓存时不应访问网络');
|
|
},
|
|
logger: silentLogger,
|
|
});
|
|
assert.equal(
|
|
verifyNsisToolset(offline.nsisDir, { utilsSha1: sha1Of(utils) }).ok,
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('下载失败按次数重试,最终成功', async () => {
|
|
const sandbox = createSandbox();
|
|
const archive = await createArchiveFixture();
|
|
const utils = Buffer.from('dll');
|
|
const { fetchImpl, calls } = fixtureFetch({ archive, utils, failures: 2 });
|
|
const result = await ensureNsisToolset({
|
|
toolsDir: path.join(sandbox, '.tauri'),
|
|
cacheDir: path.join(sandbox, 'cache'),
|
|
assets: fixtureAssets({ archive, utils }),
|
|
fetchImpl,
|
|
attempts: 3,
|
|
retryDelayMs: 1,
|
|
logger: silentLogger,
|
|
});
|
|
assert.equal(result.downloaded.length, 2);
|
|
assert.equal(calls.length, 4);
|
|
});
|
|
|
|
test('哈希不匹配时报错并给出可操作提示', async () => {
|
|
const sandbox = createSandbox();
|
|
const { fetchImpl } = fixtureFetch({
|
|
archive: Buffer.from('corrupted'),
|
|
utils: Buffer.from('corrupted'),
|
|
});
|
|
await assert.rejects(
|
|
ensureNsisToolset({
|
|
toolsDir: path.join(sandbox, '.tauri'),
|
|
cacheDir: path.join(sandbox, 'cache'),
|
|
assets: [
|
|
{
|
|
assetName: NSIS_ARCHIVE_ASSET_NAME,
|
|
url: 'https://github.com/a/b/releases/download/1/n.zip',
|
|
sha1: 'deadbeef',
|
|
},
|
|
{
|
|
assetName: NSIS_TAURI_UTILS_ASSET_NAME,
|
|
url: 'https://github.com/a/b/releases/download/1/n.dll',
|
|
sha1: 'deadbeef',
|
|
},
|
|
],
|
|
fetchImpl,
|
|
attempts: 2,
|
|
retryDelayMs: 1,
|
|
logger: silentLogger,
|
|
}),
|
|
/SHA1 不匹配/u,
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(path.join(sandbox, 'cache', NSIS_ARCHIVE_ASSET_NAME)),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('归档越界路径与缺失必需文件都会失败关闭', async () => {
|
|
const sandbox = createSandbox();
|
|
assert.throws(
|
|
() =>
|
|
resolveArchiveEntryTarget(path.join(sandbox, 'extract'), '../escape.txt'),
|
|
/越界路径/u,
|
|
);
|
|
assert.equal(
|
|
resolveArchiveEntryTarget(path.join(sandbox, 'extract'), 'nsis-3.11/a/b'),
|
|
path.resolve(sandbox, 'extract', 'nsis-3.11/a/b'),
|
|
);
|
|
|
|
const emptyArchive = new JSZip()
|
|
.file(`${NSIS_ARCHIVE_TOP_LEVEL_DIR}/makensis.exe`, 'only-one')
|
|
.generateAsync({ type: 'nodebuffer' });
|
|
const emptyPath = path.join(sandbox, 'incomplete.zip');
|
|
fs.writeFileSync(emptyPath, await emptyArchive);
|
|
const toolsDir = path.join(sandbox, 'incomplete-tools');
|
|
await extractNsisArchive(emptyPath, toolsDir);
|
|
const status = verifyNsisToolset(
|
|
path.join(toolsDir, NSIS_ARCHIVE_TOP_LEVEL_DIR),
|
|
);
|
|
assert.equal(status.ok, false);
|
|
assert.ok(status.missing.includes('Bin/makensis.exe'));
|
|
});
|
|
|
|
test('非 Windows 目标或 --no-bundle 不预置工具链', async () => {
|
|
let called = 0;
|
|
const deps = {
|
|
ensure: async () => {
|
|
called += 1;
|
|
},
|
|
};
|
|
assert.equal(
|
|
await prepareNsisToolsetForRelease(
|
|
{ target: 'x86_64-pc-windows-msvc' },
|
|
{ bundling: false, ...deps },
|
|
),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
await prepareNsisToolsetForRelease(
|
|
{ target: 'aarch64-apple-darwin' },
|
|
deps,
|
|
),
|
|
null,
|
|
);
|
|
assert.equal(called, 0);
|
|
});
|