返回 DeepSeek-Reasonix
build-release-cli-candidate.mjs
根目录 / scripts / build-release-cli-candidate.mjs
1 import { createHash } from "node:crypto";
2 import { execFileSync } from "node:child_process";
3 import {
4 copyFileSync,
5 cpSync,
6 mkdirSync,
7 mkdtempSync,
8 readFileSync,
9 rmSync,
10 writeFileSync,
11 } from "node:fs";
12 import { tmpdir } from "node:os";
13 import path from "node:path";
14 import { fileURLToPath, pathToFileURL } from "node:url";
15
16 const HERE = path.dirname(fileURLToPath(import.meta.url));
17 const ROOT = path.join(HERE, "..");
18 const VERSION_RE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
19 const SHA_RE = /^[0-9a-f]{40}$/;
20 export const targets = [
21 { key: "darwin-arm64", npm: "darwin-arm64", goos: "darwin", goarch: "arm64" },
22 { key: "darwin-amd64", npm: "darwin-x64", goos: "darwin", goarch: "amd64" },
23 { key: "linux-arm64", npm: "linux-arm64", goos: "linux", goarch: "arm64" },
24 { key: "linux-amd64", npm: "linux-x64", goos: "linux", goarch: "amd64" },
25 { key: "windows-arm64", npm: "win32-arm64", goos: "windows", goarch: "arm64" },
26 { key: "windows-amd64", npm: "win32-x64", goos: "windows", goarch: "amd64" },
27 ];
28
29 function sha256(file) {
30 return createHash("sha256").update(readFileSync(file)).digest("hex");
31 }
32
33 export function platformPackage(target, version, candidateSha) {
34 return {
35 name: `@reasonix/cli-${target.npm}`,
36 version,
37 description: `reasonix prebuilt binary for ${target.npm}.`,
38 os: [target.goos === "windows" ? "win32" : target.goos],
39 cpu: [target.goarch === "amd64" ? "x64" : "arm64"],
40 files: ["bin/"],
41 license: "MIT",
42 repository: { type: "git", url: "git+https://github.com/esengine/DeepSeek-Reasonix.git" },
43 reasonixCandidateSha: candidateSha,
44 gitHead: candidateSha,
45 };
46 }
47
48 export function renderHomebrewCask(version, sums) {
49 const requireSum = name => {
50 if (!/^[0-9a-f]{64}$/.test(sums[name] ?? "")) throw new Error(`missing checksum for ${name}`);
51 return sums[name];
52 };
53 return `# This file was generated by Reasonix release candidate preparation. DO NOT EDIT.
54 cask "reasonix" do
55 postflight_steps do
56 if OS.mac?
57 system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/reasonix"]
58 end
59 end
60
61 version "${version}"
62
63 on_macos do
64 on_arm do
65 sha256 "${requireSum("reasonix-darwin-arm64.tar.gz")}"
66 url "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v#{version}/reasonix-darwin-arm64.tar.gz"
67 end
68 on_intel do
69 sha256 "${requireSum("reasonix-darwin-amd64.tar.gz")}"
70 url "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v#{version}/reasonix-darwin-amd64.tar.gz"
71 end
72 end
73 on_linux do
74 on_arm do
75 sha256 "${requireSum("reasonix-linux-arm64.tar.gz")}"
76 url "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v#{version}/reasonix-linux-arm64.tar.gz"
77 end
78 on_intel do
79 sha256 "${requireSum("reasonix-linux-amd64.tar.gz")}"
80 url "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v#{version}/reasonix-linux-amd64.tar.gz"
81 end
82 end
83
84 name "reasonix"
85 desc "Cache-first DeepSeek coding agent for the terminal."
86 homepage "https://github.com/esengine/DeepSeek-Reasonix"
87
88 livecheck do
89 skip "Auto-generated on release."
90 end
91
92 binary "reasonix"
93 end
94 `;
95 }
96
97 function run(command, args, options = {}) {
98 execFileSync(command, args, { stdio: "inherit", ...options });
99 }
100
101 function makeArchive(target, binary, cliDirectory, work) {
102 const stage = path.join(work, `cli-${target.key}`);
103 mkdirSync(stage);
104 for (const name of ["CHANGELOG.md", "LICENSE", "README.md", "README.zh-CN.md"]) copyFileSync(path.join(ROOT, name), path.join(stage, name));
105 copyFileSync(binary, path.join(stage, target.goos === "windows" ? "reasonix.exe" : "reasonix"));
106 const extension = target.goos === "windows" ? "zip" : "tar.gz";
107 const archive = path.join(cliDirectory, `reasonix-${target.key}.${extension}`);
108 if (target.goos === "windows") run("zip", ["-q", "-r", archive, "."], { cwd: stage });
109 else run("tar", ["-czf", archive, "-C", stage, "."]);
110 return archive;
111 }
112
113 function packNpm(target, binary, npmDirectory, work, version, candidateSha) {
114 const stage = path.join(work, `npm-${target.npm}`);
115 mkdirSync(path.join(stage, "bin"), { recursive: true });
116 copyFileSync(binary, path.join(stage, "bin", target.goos === "windows" ? "reasonix.exe" : "reasonix"));
117 writeFileSync(path.join(stage, "package.json"), `${JSON.stringify(platformPackage(target, version, candidateSha), null, 2)}\n`);
118 run("npm", ["pack", stage, "--pack-destination", npmDirectory, "--silent"]);
119 }
120
121 function packNpmRoot(npmDirectory, work, version, candidateSha) {
122 const stage = path.join(work, "npm-reasonix");
123 mkdirSync(stage);
124 cpSync(path.join(ROOT, "npm", "reasonix", "bin"), path.join(stage, "bin"), { recursive: true });
125 copyFileSync(path.join(ROOT, "README.md"), path.join(stage, "README.md"));
126 const pkg = JSON.parse(readFileSync(path.join(ROOT, "npm", "reasonix", "package.json"), "utf8"));
127 pkg.version = version;
128 pkg.reasonixCandidateSha = candidateSha;
129 pkg.gitHead = candidateSha;
130 for (const name of Object.keys(pkg.optionalDependencies)) pkg.optionalDependencies[name] = version;
131 writeFileSync(path.join(stage, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
132 run("npm", ["pack", stage, "--pack-destination", npmDirectory, "--silent"]);
133 }
134
135 export function buildCandidate(version, output, env = process.env) {
136 if (!VERSION_RE.test(version)) throw new Error("candidate version must be MAJOR.MINOR.PATCH");
137 const candidateSha = env.RELEASE_SOURCE_SHA || execFileSync("git", ["rev-parse", "HEAD"], { cwd: ROOT, encoding: "utf8" }).trim();
138 if (!SHA_RE.test(candidateSha)) throw new Error("candidate source SHA must be full length");
139 const buildTime = env.RELEASE_BUILD_TIME;
140 if (!buildTime || !Number.isFinite(new Date(buildTime).valueOf())) throw new Error("RELEASE_BUILD_TIME must be an ISO timestamp");
141 mkdirSync(output);
142 const cliDirectory = path.join(output, "cli");
143 const npmDirectory = path.join(output, "npm");
144 const binaries = path.join(output, "binaries");
145 mkdirSync(cliDirectory); mkdirSync(npmDirectory); mkdirSync(binaries);
146 const work = mkdtempSync(path.join(tmpdir(), "reasonix-cli-candidate-"));
147 try {
148 for (const target of targets) {
149 const filename = target.goos === "windows" ? "reasonix.exe" : "reasonix";
150 const directory = path.join(binaries, target.key);
151 mkdirSync(directory);
152 const binary = path.join(directory, filename);
153 const ldflags = `-s -w -X main.version=v${version} -X main.gitCommit=${candidateSha.slice(0, 12)} -X main.buildTimeUTC=${buildTime} -X reasonix/internal/productdocs.linkedVersion=v${version} -X reasonix/internal/productdocs.linkedRevision=${candidateSha}`;
154 run("go", ["build", "-trimpath", "-ldflags", ldflags, "-o", binary, "./cmd/reasonix"], {
155 cwd: ROOT, env: { ...env, CGO_ENABLED: "0", GOOS: target.goos, GOARCH: target.goarch },
156 });
157 makeArchive(target, binary, cliDirectory, work);
158 packNpm(target, binary, npmDirectory, work, version, candidateSha);
159 }
160 packNpmRoot(npmDirectory, work, version, candidateSha);
161 const assets = targets.map(target => `reasonix-${target.key}.${target.goos === "windows" ? "zip" : "tar.gz"}`).sort();
162 const sums = Object.fromEntries(assets.map(name => [name, sha256(path.join(cliDirectory, name))]));
163 writeFileSync(path.join(cliDirectory, "SHA256SUMS"), `${assets.map(name => `${sums[name]} ${name}`).join("\n")}\n`);
164 writeFileSync(path.join(cliDirectory, "reasonix.rb"), renderHomebrewCask(version, sums));
165 } finally {
166 rmSync(work, { recursive: true, force: true });
167 }
168 }
169
170 if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
171 const [version, output] = process.argv.slice(2);
172 if (!version || !output) throw new Error("usage: build-release-cli-candidate.mjs VERSION OUTPUT");
173 buildCandidate(version, path.resolve(output));
174 }
175
175 lines Plain Text