Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

210 lines
6.3 KiB
JavaScript

import { execFile } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import {
chmod,
copyFile,
mkdir,
mkdtemp,
readdir,
readFile,
rename,
rm,
stat,
writeFile
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
const execFileAsync = promisify(execFile);
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, "../../..");
const lock = JSON.parse(
await readFile(join(repoRoot, "config/tutti.app-runtime.lock.json"), "utf8")
);
const rtk = lock.rtk;
if (!rtk?.version || !rtk?.license || !rtk?.artifacts) {
throw new Error(
"config/tutti.app-runtime.lock.json has no RTK artifact catalog"
);
}
const platforms = parsePlatformArguments(process.argv.slice(2));
const outputRoot = resolve(
process.env.TUTTI_RTK_STAGING_DIR?.trim() ||
join(repoRoot, "apps/desktop/build/rtk")
);
await rm(outputRoot, { recursive: true, force: true });
await mkdir(outputRoot, { recursive: true });
const extracted = new Map();
const extractionRoots = [];
try {
for (const platform of platforms) {
const artifact = rtk.artifacts[platform];
if (!artifact)
throw new Error(`RTK artifact catalog does not contain ${platform}`);
const archive = await resolveVerifiedDownload(artifact, "archive");
const extractionRoot = await mkdtemp(join(tmpdir(), "tutti-rtk-extract-"));
extractionRoots.push(extractionRoot);
await extractArchive(archive, extractionRoot);
const executable = await findFile(extractionRoot, artifact.executable);
if (!executable) {
throw new Error(
`${basename(archive)} does not contain ${artifact.executable}`
);
}
extracted.set(platform, executable);
}
const outputName = platforms[0].startsWith("windows-") ? "rtk.exe" : "rtk";
const outputExecutable = join(outputRoot, outputName);
if (platforms.length === 1) {
await copyFile(extracted.get(platforms[0]), outputExecutable);
} else {
const expected = new Set(["darwin-arm64", "darwin-amd64"]);
if (
platforms.length !== expected.size ||
platforms.some((value) => !expected.has(value))
) {
throw new Error(
"multiple RTK artifacts are supported only for macOS universal packaging"
);
}
await execFileAsync("lipo", [
"-create",
extracted.get("darwin-arm64"),
extracted.get("darwin-amd64"),
"-output",
outputExecutable
]);
await execFileAsync("lipo", [
outputExecutable,
"-verify_arch",
"arm64",
"x86_64"
]);
}
await chmod(outputExecutable, 0o755);
const license = await resolveVerifiedDownload(rtk.license, "license");
await copyFile(license, join(outputRoot, "LICENSE"));
await writeFile(
join(outputRoot, "THIRD_PARTY_NOTICES.md"),
`# RTK third-party notice
Tutti packages an unmodified RTK ${rtk.version} executable for Session-scoped
Agent tool-output compression and the Tutti integrated terminal.
- Project: https://github.com/rtk-ai/rtk
- Release: https://github.com/rtk-ai/rtk/releases/tag/v${rtk.version}
- License: Apache-2.0; see LICENSE in this directory
`,
"utf8"
);
await writeFile(
join(outputRoot, "runtime.json"),
`${JSON.stringify(
{
schemaVersion: "tutti.rtk.v1",
version: rtk.version,
executable: outputName,
platforms
},
null,
2
)}\n`,
"utf8"
);
const { stdout } = await execFileAsync(outputExecutable, ["--version"]);
if (stdout.trim() !== `rtk ${rtk.version}`) {
throw new Error(`bundled RTK version mismatch: ${stdout.trim()}`);
}
} finally {
await Promise.all(
extractionRoots.map((path) => rm(path, { recursive: true, force: true }))
);
}
async function extractArchive(archive, destination) {
if (process.platform === "win32") {
await execFileAsync("powershell.exe", [
"-NoProfile",
"-NonInteractive",
"-Command",
"& { param($archive, $destination) Expand-Archive -LiteralPath $archive -DestinationPath $destination -Force }",
archive,
destination
]);
return;
}
await execFileAsync("tar", ["-xf", archive, "-C", destination]);
}
function parsePlatformArguments(args) {
const values = args
.filter((argument) => argument.startsWith("--platform="))
.flatMap((argument) => argument.slice("--platform=".length).split(","))
.map((value) => value.trim())
.filter(Boolean);
if (values.length === 0 || values.length !== args.length) {
throw new Error(
"provide one or more --platform=<runtime-platform> arguments"
);
}
return [...new Set(values)];
}
async function resolveVerifiedDownload(artifact, kind) {
const cacheRoot = join(tmpdir(), "tutti-build-cache", "rtk", rtk.version);
const path = join(cacheRoot, basename(new URL(artifact.url).pathname));
await mkdir(cacheRoot, { recursive: true });
try {
await verifyFile(path, artifact, kind);
return path;
} catch {
await rm(path, { force: true });
}
const response = await fetch(artifact.url, { redirect: "follow" });
if (!response.ok)
throw new Error(`download ${artifact.url}: HTTP ${response.status}`);
const temporary = `${path}.${randomUUID()}.tmp`;
await writeFile(temporary, Buffer.from(await response.arrayBuffer()), {
flag: "wx"
});
try {
await verifyFile(temporary, artifact, kind);
await rename(temporary, path);
} finally {
await rm(temporary, { force: true });
}
return path;
}
async function verifyFile(path, artifact, kind) {
const info = await stat(path);
if (!info.isFile() || info.size !== artifact.sizeBytes) {
throw new Error(`RTK ${kind} size mismatch for ${path}`);
}
const actual = createHash("sha256")
.update(await readFile(path))
.digest("hex");
if (actual !== artifact.sha256) {
throw new Error(`RTK ${kind} checksum mismatch for ${path}`);
}
}
async function findFile(root, name) {
for (const entry of await readdir(root, { withFileTypes: true })) {
const path = join(root, entry.name);
if (entry.isFile() && entry.name === name) return path;
if (entry.isDirectory()) {
const nested = await findFile(path, name);
if (nested) return nested;
}
}
return null;
}