Skip to content

Commit 6181efb

Browse files
committed
feat(release): 编译生成二进制校验和并随 ZIP 分发
1 parent 9ea4a2f commit 6181efb

10 files changed

Lines changed: 286 additions & 38 deletions

File tree

‎.github/workflows/release.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ on:
1010
- '.github/workflows/release.yaml'
1111
- '.github/workflows/check.yaml'
1212
- 'scripts/compile.ts'
13+
- 'scripts/checksum.ts'
1314
- 'scripts/platforms.ts'
1415
- 'scripts/release.ts'
1516
- 'scripts/smoke.ts'

‎CONTRIBUTING.md‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ bun run smoke
2424
| `bun run typecheck` | TypeScript 类型检查 |
2525
| `bun test` | 参数、配置、适配器、主类、类路径、交互和进程行为回归 |
2626
| `bun run check` | 类型检查与回归测试 |
27-
| `bun run compile` | 构建压缩后的本机二进制到 `dist`,与发布产物使用同一入口 |
27+
| `bun run compile` | 构建压缩后的本机二进制及其 `.sha256` 校验文件到 `dist`,与发布产物使用同一入口 |
2828
| `bun run smoke` | 使用该二进制运行真实 Maven / Gradle 夹具 |
2929

30-
编译入口支持等号形式的 `--target=<目标>` 和 `--outfile=<路径>`,可用目标与发布平台表一致。默认产物为 `dist/java-run`,Windows 自动使用 `.exe` 扩展名。二进制不自动加载运行目录中的 `.env` 或 `bunfig.toml`,显式继承的环境变量仍然生效。
30+
编译入口支持等号形式的 `--target=<目标>` 和 `--outfile=<路径>`,可用目标与发布平台表一致。默认产物为 `dist/java-run`,Windows 自动使用 `.exe` 扩展名。编译成功后在实际二进制旁生成 `<二进制文件名>.sha256`,例如 `dist/java-run.sha256` 或 `dist/java-run.exe.sha256`;自定义输出路径也使用同样规则。校验文件使用标准 SHA-256 清单格式,文件名相对于其所在目录。二进制不自动加载运行目录中的 `.env` 或 `bunfig.toml`,显式继承的环境变量仍然生效。
3131

3232
回归测试中的真实 Gradle 用例使用 PATH 中的 Gradle;找不到时会跳过,不能据此判断 Gradle 适配已经通过验收。可通过 `JAVA_RUN_TEST_GRADLE` 指定测试命令,通过 `GRADLE_USER_HOME` 隔离测试用的 Gradle 缓存:
3333

@@ -112,11 +112,11 @@ docs: 说明参数传递规则
112112
| `java-run-darwin-arm64.zip` | `bun-darwin-arm64` | `macos-15` |
113113
| `java-run-darwin-x64.zip` | `bun-darwin-x64` | `macos-15-intel` |
114114

115-
每个任务使用 `bun run compile` 生成一次待分发的压缩二进制,在 JDK 21 下执行完整 smoke。随后将该文件打包,不重新构建:Linux / macOS 使用 `zip`,Windows 使用 PowerShell `Compress-Archive`。实际 ZIP 会在原生环境中解压,核对其中二进制与已验收文件的字节一致,再使用 JDK 17 对解压出的文件执行 quick 启动验收。
115+
每个任务使用 `bun run compile` 生成一次待分发的压缩二进制和对应校验文件,在 JDK 21 下执行完整 smoke。打包前按编译时的校验文件核对源二进制,再将这两个文件一起打包:Linux / macOS 使用 `zip`,Windows 使用 PowerShell `Compress-Archive`。实际 ZIP 会在原生环境中解压,核对包内二进制及校验文件与源文件一致,并按包内校验文件验证二进制,再使用 JDK 17 对解压出的文件执行 quick 启动验收。
116116

117-
每份 ZIP 包含一个 `java-run-<平台>/` 目录,目录内为固定名称的 `java-run` 或 `java-run.exe`、从 [安装指南](docs/installation.md) 复制的 `INSTALL.md`,以及存在的许可证文件。编译参数、发布矩阵、包内目录和公开资产名称共用 [平台清单](scripts/platforms.ts)。公开名称不带版本号,版本由 `/releases/download/v<版本>/` URL 表达;`/releases/latest/download/` 提供最新稳定版的固定下载入口。
117+
每份 ZIP 包含一个 `java-run-<平台>/` 目录,目录内为固定名称的 `java-run` 或 `java-run.exe`、对应的 `java-run.sha256` 或 `java-run.exe.sha256`、从 [安装指南](docs/installation.md) 复制的 `INSTALL.md`,以及存在的许可证文件。编译参数、发布矩阵、包内目录和公开资产名称共用 [平台清单](scripts/platforms.ts)。公开名称不带版本号,版本由 `/releases/download/v<版本>/` URL 表达;`/releases/latest/download/` 提供最新稳定版的固定下载入口。
118118

119-
汇总步骤要求恰好包含上述五种非空 ZIP,拒绝缺失、多余或无效产物,然后生成并核对覆盖五个 ZIP 的 `SHA256SUMS`。源码 Check 的双 JDK 完整回归与发布文件的 JDK 21 full / JDK 17 quick 范围分别声明,不能将 quick 扩大为完整验收。Linux 验收采用 Ubuntu 24.04 的 glibc 环境,不代表 musl 或其他系统版本已通过验收。
119+
汇总步骤要求恰好包含上述五种非空 ZIP,拒绝缺失、多余或无效产物,然后生成并核对覆盖五个 ZIP 的 `SHA256SUMS`。包内 `.sha256` 校验二进制内容,Release 单独提供的 `SHA256SUMS` 校验最终下载的 ZIP。源码 Check 的双 JDK 完整回归与发布文件的 JDK 21 full / JDK 17 quick 范围分别声明,不能将 quick 扩大为完整验收。Linux 验收采用 Ubuntu 24.04 的 glibc 环境,不代表 musl 或其他系统版本已通过验收。
120120

121121
### 发布流程演练
122122

@@ -130,6 +130,16 @@ docs: 说明参数传递规则
130130
bun scripts/release.ts metadata
131131
```
132132

133+
在原生机器上可构建并验收对应平台的 ZIP,例如 macOS arm64:
134+
135+
```sh
136+
bun run compile
137+
bun run smoke --suite=full
138+
bun scripts/release.ts package darwin-arm64
139+
```
140+
141+
`package` 使用已有的二进制和校验文件,输出 `dist/java-run-darwin-arm64.zip`。其他平台使用对应的平台名,并在该平台的原生机器上运行。
142+
133143
### 标签发布与重试
134144

135145
任何 `v*` 标签 push 都进入发布校验,不受分支演练的路径条件限制。包版本必须是合法 SemVer,标签必须严格等于 `v<package.json 版本>`,且仓库需包含非空许可证文件。已经公开的标签版本会被拒绝,不能通过重跑覆盖。

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ java-run 为 Maven 和 Gradle 源码工作区提供统一的开发启动入口
1616

1717
macOS / Linux 使用 `uname -m` 查看体系结构:`x86_64` 对应 x64,`arm64` / `aarch64` 对应 arm64。Windows 可在“设置 → 系统 → 关于”查看系统类型,当前提供 x64 版本。Linux 产物在 Ubuntu 24.04 的 glibc 环境验收;musl(如 Alpine)和其他系统版本未纳入发布验收。
1818

19-
按照 [安装指南](docs/installation.md) 核对 SHA-256、解压并将可执行文件加入 PATH;ZIP 内也附有独立的 `INSTALL.md`。随后在 Java 工作区根目录执行:
19+
按照 [安装指南](docs/installation.md) 核对 ZIP 和包内二进制的 SHA-256,再将可执行文件加入 PATH;ZIP 内附有二进制校验文件和独立的 `INSTALL.md`。随后在 Java 工作区根目录执行:
2020

2121
```sh
2222
java-run
@@ -34,7 +34,7 @@ bun install --frozen-lockfile
3434
bun run compile
3535
```
3636

37-
产物为 `dist/java-run`,Windows 使用 `dist/java-run.exe`。将产物放入 PATH 后即可使用上述命令。源码调试可以直接使用:
37+
产物为 `dist/java-run` 和 `dist/java-run.sha256`;Windows 使用 `dist/java-run.exe` 和 `dist/java-run.exe.sha256`。校验文件记录二进制的 SHA-256,可按[安装指南](docs/installation.md)中的二进制校验命令在 `dist` 目录核对。将可执行文件放入 PATH 后即可使用上述命令。源码调试可以直接使用:
3838

3939
```sh
4040
bun run src/cli.ts --cwd /path/to/java-project

‎docs/installation.md‎

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ https://github.com/Vanisper/java-run/releases/download/vX.Y.Z/java-run-<平台>.
2525
https://github.com/Vanisper/java-run/releases/download/vX.Y.Z/SHA256SUMS
2626
```
2727

28-
将 `vX.Y.Z` 替换为所选 Release 的完整标签;预发布版本也使用完整标签下载。`/releases/latest/download/` 用于最新稳定版。ZIP 解压后只有一个 `java-run-<平台>/` 目录,内含 `java-run` 或 `java-run.exe`、本安装指南 `INSTALL.md`,以及存在时按原文件名附带的许可证。
28+
将 `vX.Y.Z` 替换为所选 Release 的完整标签;预发布版本也使用完整标签下载。`/releases/latest/download/` 用于最新稳定版。ZIP 解压后只有一个 `java-run-<平台>/` 目录,内含 `java-run` 或 `java-run.exe`、对应的 `java-run.sha256` 或 `java-run.exe.sha256`、本安装指南 `INSTALL.md`,以及存在时按原文件名附带的许可证。Release 的 `SHA256SUMS` 校验下载的 ZIP,包内 `.sha256` 校验解压后的二进制。
2929

3030
## macOS / Linux
3131

@@ -55,10 +55,28 @@ sha256sum "$asset"
5555

5656
```sh
5757
unzip "$asset"
58-
chmod +x "java-run-${platform}/java-run"
59-
"./java-run-${platform}/java-run" version
58+
cd "java-run-${platform}"
59+
```
60+
61+
在解压目录中核对二进制。macOS 使用:
62+
63+
```sh
64+
shasum -a 256 -c java-run.sha256
65+
```
66+
67+
Linux 使用:
68+
69+
```sh
70+
sha256sum --check java-run.sha256
71+
```
72+
73+
确认输出 `java-run: OK` 后,再检查版本并安装:
74+
75+
```sh
76+
chmod +x java-run
77+
./java-run version
6078
mkdir -p "$HOME/.local/bin"
61-
cp "java-run-${platform}/java-run" "$HOME/.local/bin/java-run"
79+
cp java-run "$HOME/.local/bin/java-run"
6280
export PATH="$HOME/.local/bin:$PATH"
6381
java-run version
6482
```
@@ -73,14 +91,24 @@ java-run version
7391
Get-FileHash -Algorithm SHA256 .\java-run-windows-x64.zip
7492
```
7593

76-
将结果与 `SHA256SUMS` 中 `java-run-windows-x64.zip` 对应的值核对,比较时忽略字母大小写。一致后解压并检查版本:
94+
将结果与 `SHA256SUMS` 中 `java-run-windows-x64.zip` 对应的值核对,比较时忽略字母大小写。一致后解压并核对二进制:
7795

7896
```powershell
7997
Expand-Archive -Path .\java-run-windows-x64.zip -DestinationPath .\java-run-download
80-
& .\java-run-download\java-run-windows-x64\java-run.exe version
98+
Set-Location .\java-run-download\java-run-windows-x64
99+
$expectedHash = ((Get-Content -Raw .\java-run.exe.sha256) -split '\s+')[0]
100+
if ((Get-FileHash -Algorithm SHA256 .\java-run.exe).Hash -ne $expectedHash) {
101+
throw 'java-run.exe SHA-256 校验失败'
102+
}
103+
```
104+
105+
校验通过后,在同一目录检查版本并安装:
106+
107+
```powershell
108+
& .\java-run.exe version
81109
$installDir = Join-Path $env:LOCALAPPDATA 'Programs\java-run'
82110
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
83-
Copy-Item -Force .\java-run-download\java-run-windows-x64\java-run.exe $installDir
111+
Copy-Item -Force .\java-run.exe $installDir
84112
$env:Path = "$installDir;$env:Path"
85113
java-run version
86114
```

‎docs/roadmap.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ java-run 的产品目标是为 Maven 与 Gradle 的 Java 源码工作区提供
3131
- 静态计划、帮助和版本遵守各自的副作用契约
3232
- Windows 编码、Maven 配置根和批处理调用有对应的原生验证
3333

34-
[Release 工作流](../.github/workflows/release.yaml) 在固定原生 runner 上构建 Windows x64、Linux x64 / arm64、macOS x64 / arm64 五种压缩二进制。每份文件在 JDK 21 下执行完整 smoke,在 JDK 17 下执行 quick 启动验收,再与独立安装指南、许可证打包为 ZIP。实际 ZIP 经原生解压、二进制字节一致性检查与 quick 启动验收后上传。汇总要求五种非空 ZIP 齐全,并生成和核对 SHA-256。
34+
[Release 工作流](../.github/workflows/release.yaml) 在固定原生 runner 上构建 Windows x64、Linux x64 / arm64、macOS x64 / arm64 五种压缩二进制。编译时生成对应的 `.sha256` 校验文件,打包前核对二进制,再与校验文件、独立安装指南、许可证一起打包为 ZIP。每份二进制在 JDK 21 下执行完整 smoke;实际 ZIP 经原生解压、源文件一致性与包内 SHA-256 校验后,使用 JDK 17 对解压出的文件执行 quick 启动验收。汇总要求五种非空 ZIP 齐全,并生成和核对覆盖 ZIP 的 `SHA256SUMS`。
3535

3636
ZIP 名称固定为 `java-run-<平台>.zip`,版本由 GitHub Release 的下载路径表达;每份包内使用固定的可执行文件名。Release 正文自动生成版本下载表、安装说明与 GitHub 变更说明,安装指南同时随 ZIP 分发。[安装指南](installation.md) 说明最新稳定版和指定版本的下载、校验、PATH 配置与升级方式。
3737

‎scripts/checksum.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { createHash, randomUUID } from 'node:crypto';
2+
import { createReadStream } from 'node:fs';
3+
import { lstat, readFile, rename, rm, writeFile } from 'node:fs/promises';
4+
import { basename } from 'node:path';
5+
6+
async function assertFile(path: string): Promise<void> {
7+
const stat = await lstat(path);
8+
if (!stat.isFile() || stat.size === 0) throw new Error(`文件不是非空普通文件:${path}`);
9+
}
10+
11+
/** 流式计算非空普通文件的 SHA-256,拒绝符号链接 */
12+
export async function sha256(path: string): Promise<string> {
13+
await assertFile(path);
14+
const hash = createHash('sha256');
15+
for await (const chunk of createReadStream(path)) hash.update(chunk);
16+
return hash.digest('hex');
17+
}
18+
19+
function checksumLine(binary: string, digest: string): string {
20+
const name = basename(binary);
21+
if (/[\r\n]/.test(name)) throw new Error('二进制文件名不能包含换行');
22+
// GNU 校验和对含反斜杠的文件名使用转义行前缀
23+
return name.includes('\\') ? `\\${digest} ${name.replaceAll('\\', '\\\\')}\n` : `${digest} ${name}\n`;
24+
}
25+
26+
/** 原子写入二进制旁的 .sha256,清单只引用同目录文件名 */
27+
export async function writeBinaryChecksum(binary: string): Promise<string> {
28+
const sidecar = `${binary}.sha256`;
29+
const content = checksumLine(binary, await sha256(binary));
30+
const temporary = `${sidecar}.${randomUUID()}.tmp`;
31+
try {
32+
await writeFile(temporary, content, { flag: 'wx' });
33+
await rename(temporary, sidecar);
34+
return sidecar;
35+
} finally { await rm(temporary, { force: true }); }
36+
}
37+
38+
/** 核对二进制、文件名与旁边的 .sha256;缺失、过期或格式不符时失败 */
39+
export async function verifyBinaryChecksum(binary: string): Promise<string> {
40+
const sidecar = `${binary}.sha256`;
41+
await assertFile(sidecar);
42+
const content = await readFile(sidecar, 'utf8');
43+
if (content !== checksumLine(binary, await sha256(binary))) {
44+
throw new Error(`二进制校验和或文件名不一致:${basename(sidecar)}`);
45+
}
46+
return sidecar;
47+
}

‎scripts/compile.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { spawnSync } from 'node:child_process';
2+
import { rm } from 'node:fs/promises';
23
import { resolve } from 'node:path';
4+
import { writeBinaryChecksum } from './checksum';
35
import { platforms, type BuildTarget } from './platforms';
46

57
const projectRoot = resolve(import.meta.dir, '..');
@@ -37,9 +39,12 @@ export function parseCompileOptions(argv: readonly string[], cwd = projectRoot):
3739
return options;
3840
}
3941

40-
/** 编译独立二进制,构建失败保留 Bun 的退出码 */
41-
function main(argv: string[]): number {
42+
/** 编译独立二进制及校验和,构建失败保留 Bun 的退出码 */
43+
async function main(argv: string[]): Promise<number> {
4244
const options = parseCompileOptions(argv);
45+
const windows = options.target ? options.target.startsWith('bun-windows-') : process.platform === 'win32';
46+
const binary = windows && !options.outfile.endsWith('.exe') ? `${options.outfile}.exe` : options.outfile;
47+
await rm(`${binary}.sha256`, { force: true });
4348
const args = [
4449
'build', resolve(projectRoot, 'src/cli.ts'), '--compile', '--minify', '--sourcemap',
4550
'--no-compile-autoload-dotenv', '--no-compile-autoload-bunfig',
@@ -48,12 +53,14 @@ function main(argv: string[]): number {
4853
if (options.target) args.push(`--target=${options.target}`);
4954
const result = spawnSync(process.execPath, args, { cwd: projectRoot, stdio: 'inherit' });
5055
if (result.error) throw result.error;
51-
return result.status ?? 1;
56+
const status = result.status ?? 1;
57+
if (status === 0) await writeBinaryChecksum(binary);
58+
return status;
5259
}
5360

5461
if (import.meta.main) {
5562
try {
56-
process.exitCode = main(Bun.argv.slice(2));
63+
process.exitCode = await main(Bun.argv.slice(2));
5764
} catch (error) {
5865
console.error(`java-run:${error instanceof Error ? error.message : String(error)}`);
5966
process.exitCode = 1;

0 commit comments

Comments
 (0)