feat: 增加 Linux 打包与 GitHub 构建发布链
构建与发布 / 单测与集成测试 (push) Waiting to run
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
构建与发布 / 发布 GitHub Release (push) Blocked by required conditions
构建与发布 / 单测与集成测试 (push) Waiting to run
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
构建与发布 / 发布 GitHub Release (push) Blocked by required conditions
新增 build-linux.js,直接从官方 zip 转写 tar 保住可执行位, Windows 上也能构建 Linux 包。新增 build-release.js 作为发布件 唯一出口,排除便携版 data/、回读产物校验内容、生成校验和。 GitHub Actions 分测试、四目标打包、标签发布三段。
This commit is contained in:
@@ -470,6 +470,112 @@ test('macOS 打包脚本保留签名前提并产出 arm64 DMG', () => {
|
||||
assert.match(build, /'--verify', '--deep', '--strict'/);
|
||||
});
|
||||
|
||||
test('Linux 打包直接从官方 zip 转写 tar 并保住可执行位', () => {
|
||||
const root = path.join(__dirname, '..', '..');
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
||||
const linux = require(path.join(root, 'build-linux.js'));
|
||||
|
||||
assert.strictEqual(pkg.scripts['build:linux'], 'node build-linux.js');
|
||||
|
||||
// 主程序、沙箱与共享库丢了执行位就起不来;普通资源不该被误判成可执行
|
||||
for (const name of ['PeopleLib', 'chrome-sandbox', 'chrome_crashpad_handler', 'libffmpeg.so']) {
|
||||
assert.ok(linux.isExecutableName(name), `${name} 应带可执行位`);
|
||||
}
|
||||
for (const name of ['resources/app/main.js', 'locales/zh-CN.pak', 'icudtl.dat']) {
|
||||
assert.ok(!linux.isExecutableName(name), `${name} 不该带可执行位`);
|
||||
}
|
||||
|
||||
// jszip 给出的权限位优先,缺失时才按文件名兜底
|
||||
assert.strictEqual(linux.modeOf({ unixPermissions: 0o644 }, 'chrome-sandbox'), 0o644);
|
||||
assert.strictEqual(linux.modeOf({}, 'chrome-sandbox'), 0o755);
|
||||
assert.strictEqual(linux.modeOf({ unixPermissions: null }, 'resources/app/main.js'), 0o644);
|
||||
|
||||
assert.strictEqual(linux.parseArch(['--arch', 'arm64']), 'arm64');
|
||||
assert.throws(() => linux.parseArch(['--arch', 'mips']), /不支持的架构/);
|
||||
});
|
||||
|
||||
test('Linux tar 条目对超长路径用 PAX 头,权限位可被标准解析读回', () => {
|
||||
const root = path.join(__dirname, '..', '..');
|
||||
const linux = require(path.join(root, 'build-linux.js'));
|
||||
const { readTarEntries } = require(path.join(root, 'build-release.js'));
|
||||
|
||||
// ustar 的 prefix 只能在斜杠处切分,undici 深层路径切不出合法组合
|
||||
const long = `PeopleLib-linux-x64/resources/app/node_modules/undici/lib/web/${'d'.repeat(60)}/x.js`;
|
||||
assert.ok(Buffer.byteLength(long) > 100);
|
||||
|
||||
const buffer = Buffer.concat([
|
||||
linux.tarEntry('PeopleLib-linux-x64/PeopleLib', 0o755, Buffer.from('bin')),
|
||||
linux.tarEntry(long, 0o644, Buffer.from('src')),
|
||||
Buffer.alloc(1024)
|
||||
]);
|
||||
const entries = readTarEntries(buffer);
|
||||
|
||||
assert.deepStrictEqual(entries.map((e) => e.name), ['PeopleLib-linux-x64/PeopleLib', long]);
|
||||
assert.strictEqual(entries[0].mode & 0o111, 0o111);
|
||||
assert.strictEqual(entries[1].mode & 0o111, 0);
|
||||
});
|
||||
|
||||
test('发布打包排除便携版 data 目录并按校验和回验', () => {
|
||||
const root = path.join(__dirname, '..', '..');
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
||||
const release = fs.readFileSync(path.join(root, 'build-release.js'), 'utf8');
|
||||
const { TARGETS } = require(path.join(root, 'build-release.js'));
|
||||
|
||||
assert.strictEqual(pkg.scripts.release, 'node build-release.js');
|
||||
assert.deepStrictEqual(
|
||||
Object.keys(TARGETS).sort(),
|
||||
['linux-arm64', 'linux-x64', 'macos-arm64', 'windows-x64']
|
||||
);
|
||||
// 发布件名字带版本号,Release 页面上不同版本的资产才不会互相覆盖
|
||||
for (const [key, target] of Object.entries(TARGETS)) {
|
||||
assert.ok(target.asset.includes(pkg.version), `${key} 发布件名缺少版本号`);
|
||||
}
|
||||
|
||||
// 便携版书库就在构建目录同级 data/,漏掉这条会把用户数据打进公开发布件
|
||||
assert.match(release, /!f\.rel\.startsWith\('data\/'\)/);
|
||||
assert.match(release, /forbidEntries\(names, \[\/\^data\\\/\/, \/\(\^\|\\\/\)_test\\\/\/\]\)/);
|
||||
// 跨平台产物汇总时逐个比对哈希与版本,防止挂上损坏或版本错配的资产
|
||||
assert.match(release, /if \(digest !== file\.sha256\) throw new Error/);
|
||||
assert.match(release, /manifest\.version !== VERSION/);
|
||||
});
|
||||
|
||||
test('CI 工作流覆盖三平台并在标签上核对版本后发布', () => {
|
||||
const root = path.join(__dirname, '..', '..');
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
||||
const workflow = fs.readFileSync(path.join(root, '.github', 'workflows', 'build.yml'), 'utf8');
|
||||
|
||||
for (const target of ['windows-2025', 'macos-15', 'ubuntu-24.04', 'ubuntu-24.04-arm']) {
|
||||
assert.ok(workflow.includes(target), `构建矩阵缺少 ${target}`);
|
||||
}
|
||||
// 集成测试要开真实窗口,无头 runner 上没有 xvfb 会直接崩
|
||||
assert.match(workflow, /xvfb-run -a npx electron/);
|
||||
for (const suite of ['startup', 'download', 'cover', 'annotation', 'reader-features', 'library-notes', 'ai-scope']) {
|
||||
assert.ok(workflow.includes(suite), `CI 缺少集成套件 ${suite}`);
|
||||
}
|
||||
for (const suite of fs.readdirSync(path.join(root, 'src', '_test', 'electron'))) {
|
||||
if (!suite.endsWith('.integration.js')) continue;
|
||||
assert.ok(
|
||||
workflow.includes(suite.replace('.integration.js', '')),
|
||||
`CI 漏跑集成套件 ${suite}`
|
||||
);
|
||||
}
|
||||
|
||||
// 仓库 .npmrc 指向 npmmirror,GitHub runner 在境外必须切回官方源
|
||||
assert.match(fs.readFileSync(path.join(root, '.npmrc'), 'utf8'), /registry\.npmmirror\.com/);
|
||||
assert.match(workflow, /npm_config_registry: https:\/\/registry\.npmjs\.org\//);
|
||||
assert.match(workflow, /ELECTRON_MIRROR: https:\/\/github\.com\/electron\/electron\/releases\/download\//);
|
||||
|
||||
// 打标签才发布,且发布前要求标签名与 package.json 版本一致
|
||||
assert.match(workflow, /if: github\.event_name == 'push' && github\.ref_type == 'tag'/);
|
||||
assert.match(workflow, /标签应为/);
|
||||
assert.ok(workflow.includes(`v'+p.version`));
|
||||
assert.match(workflow, /--verify dist\/release-downloads/);
|
||||
// 先建草稿再转正式,上传中途失败不会留下资产不全的 Release
|
||||
assert.match(workflow, /gh release edit "\$tag" --draft --verify-tag/);
|
||||
assert.match(workflow, /gh release edit "\$tag" --draft=false/);
|
||||
assert.ok(pkg.version && /^\d+\.\d+\.\d+$/.test(pkg.version));
|
||||
});
|
||||
|
||||
test('删除阅读资料先等待阅读器排空,后续迟到写入会被拒绝', () => {
|
||||
assert.match(mainSrc, /await requestReaderPurge\(id\)/);
|
||||
assert.match(mainSrc, /purgedReaderEntries\.add\(key\)/);
|
||||
|
||||
Reference in New Issue
Block a user