mirror of
https://github.com/cline/cline.git
synced 2026-10-24 12:03:46 +08:00
198 lines
8.2 KiB
TypeScript
198 lines
8.2 KiB
TypeScript
import { cpSync, existsSync, rmSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { $ } from "bun";
|
|
import {
|
|
compileCliBinary,
|
|
installOpenTuiNativeVariants,
|
|
} from "../../../cli/script/compile-binary";
|
|
import { telemetryDefineArgs } from "./telemetry-define-args";
|
|
|
|
// The desktop app ships two runtime artifacts and no app-specific executable:
|
|
//
|
|
// - The Cline CLI, compiled self-contained for this target (Tauri externalBin
|
|
// `bin/cline-cli`). `cline-cli hub ensure` starts or reuses the CLI-managed
|
|
// Hub, and the Hub daemon is that same binary.
|
|
// - The desktop backend (sidecar/ sources: the UI command router plus its
|
|
// HTTP/WebSocket server) as one JS bundle, executed on the CLI's embedded
|
|
// runtime. It attaches to the Hub as an ordinary client.
|
|
//
|
|
// Linux CLI copies are bundled as resources so SSH remotes run a CLI-managed
|
|
// Hub too; macOS remotes reuse the signed universal CLI itself.
|
|
|
|
const APP_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
const REPO_ROOT = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
const BIN_DIR = "src-tauri/bin";
|
|
export const DESKTOP_CLI_NAME = "cline-cli";
|
|
export const DESKTOP_BACKEND_BUNDLE = `${BIN_DIR}/desktop-backend/index.js`;
|
|
export const REMOTE_CLI_DIR = `${BIN_DIR}/remote-helpers`;
|
|
|
|
const resolveTargetTriple = async (): Promise<string> => {
|
|
const fromEnv = process.env.TAURI_ENV_TARGET_TRIPLE ?? process.env.TARGET;
|
|
if (fromEnv?.trim()) {
|
|
return fromEnv.trim();
|
|
}
|
|
|
|
const rustcVersion = await $`rustc -vV`.text();
|
|
const hostLine = rustcVersion
|
|
.split("\n")
|
|
.find((line) => line.startsWith("host: "));
|
|
const host = hostLine?.slice("host: ".length).trim();
|
|
if (!host) {
|
|
throw new Error("failed to resolve Rust host target triple");
|
|
}
|
|
return host;
|
|
};
|
|
|
|
// Bun cross-compiles --compile binaries, so a CI runner can produce the CLI
|
|
// for a different architecture than its own (e.g. the x86_64 macOS bundle
|
|
// from an arm64 runner).
|
|
const resolveBunCompileTarget = (
|
|
targetTriple: string,
|
|
): Bun.Build.CompileTarget => {
|
|
if (targetTriple.startsWith("aarch64-apple-darwin"))
|
|
return "bun-darwin-arm64";
|
|
if (targetTriple.startsWith("x86_64-apple-darwin")) return "bun-darwin-x64";
|
|
if (targetTriple.startsWith("x86_64-pc-windows")) return "bun-windows-x64";
|
|
if (targetTriple.startsWith("aarch64-pc-windows")) return "bun-windows-arm64";
|
|
// SSH hosts may predate AVX2 (e.g. Ivy Bridge Xeons). The default Bun
|
|
// x64 runtime can SIGILL before our entrypoint runs on those CPUs.
|
|
if (targetTriple.startsWith("x86_64-unknown-linux"))
|
|
return "bun-linux-x64-baseline";
|
|
if (targetTriple.startsWith("aarch64-unknown-linux"))
|
|
return "bun-linux-arm64";
|
|
throw new Error(`unsupported desktop target triple: ${targetTriple}`);
|
|
};
|
|
|
|
const desktopCliOutfile = (targetTriple: string): string => {
|
|
const extension = targetTriple.includes("windows") ? ".exe" : "";
|
|
return `${BIN_DIR}/${DESKTOP_CLI_NAME}-${targetTriple}${extension}`;
|
|
};
|
|
|
|
const compileDesktopCli = async (
|
|
targetTriple: string,
|
|
outfile = desktopCliOutfile(targetTriple),
|
|
): Promise<string> => {
|
|
await compileCliBinary({
|
|
bunTarget: resolveBunCompileTarget(targetTriple),
|
|
outfile: fileURLToPath(new URL(`../${outfile}`, import.meta.url)),
|
|
// A packaged app and an SSH session launch the CLI from directories the
|
|
// user did not choose (/, an SSH home). Reading .env or bunfig.toml
|
|
// from there could fail startup or leak credentials into the Hub.
|
|
autoloadLaunchDirectoryConfig: false,
|
|
// Bun only trusts its bundled Mozilla roots on macOS/Windows, so TLS to
|
|
// intranet endpoints signed by a corporate CA (LiteLLM proxies, MITM
|
|
// firewalls) fails with "unable to get local issuer certificate". Bake
|
|
// --use-system-ca in so the Hub also trusts the OS Keychain/cert store,
|
|
// as the npm wrapper does. `BUN_BE_BUN=1 cline-cli run` ignores baked-in
|
|
// flags, so main.rs passes it again when launching the backend.
|
|
execArgv: ["--use-system-ca"],
|
|
});
|
|
return outfile;
|
|
};
|
|
|
|
const bundleDesktopBackend = async (): Promise<void> => {
|
|
// Telemetry config must be inlined into the bundle: a packaged app launched
|
|
// from Finder/the Dock has no OTEL_* env at runtime, so without this the
|
|
// backend silently ships with telemetry disabled. Verify with
|
|
// `BUN_BE_BUN=1 cline-cli run <bundle> --telemetry-selfcheck`.
|
|
// The script runner does not mount the CLI's bunfs. Ship the same runtime
|
|
// artifact beside the backend, just as npm and VS Code hosts do.
|
|
const runtime = `${APP_ROOT}/${BIN_DIR}/desktop-backend/plugin-runtime`;
|
|
rmSync(runtime, { recursive: true, force: true });
|
|
cpSync(`${REPO_ROOT}/sdk/packages/core/dist/plugin-runtime`, runtime, {
|
|
recursive: true,
|
|
});
|
|
const defines = telemetryDefineArgs();
|
|
await $`bun build ./sidecar/index.ts --target bun --minify ${defines} --outfile ${DESKTOP_BACKEND_BUNDLE}`;
|
|
};
|
|
|
|
// Each compiled CLI embeds a full Bun runtime (~100 MB), and both Linux copies
|
|
// ship inside every desktop bundle. They must not be UPX-packed: the Bun 1.4
|
|
// runtime reads its embedded module payload back from its own executable at
|
|
// runtime, and on a packed binary those reads return NUL padding, so every
|
|
// `hub ensure` fails with "SyntaxError: Invalid character: '\0'" (#14815).
|
|
// `strip` is not an option either; it discards Bun's appended module payload.
|
|
|
|
// Linux x64 and arm64 cover common SSH hosts. macOS copies are deliberately
|
|
// not bundled: they would be Mach-O files under Contents/Resources, which
|
|
// Tauri does not codesign, and any unsigned Mach-O in the bundle fails
|
|
// notarization. Mac remotes are served by the signed desktop CLI instead
|
|
// (sidecar/remote-helper.ts).
|
|
//
|
|
// On a Windows host, Bun fails to extract the downloaded Linux runtime these
|
|
// cross-compiles need ("Failed to extract executable for 'bun-linux-x64-…'").
|
|
// Bun skips the download when `$BUN_INSTALL_CACHE_DIR/bun-<target>-v<version>`
|
|
// already exists, so seed those two files from the @oven/bun-<target> npm
|
|
// packages first; desktop-publish.yml does exactly that in its Windows job.
|
|
const buildRemoteClis = async (): Promise<void> => {
|
|
for (const targetTriple of [
|
|
"x86_64-unknown-linux-gnu",
|
|
"aarch64-unknown-linux-gnu",
|
|
]) {
|
|
await compileDesktopCli(
|
|
targetTriple,
|
|
`${REMOTE_CLI_DIR}/cline-${targetTriple}`,
|
|
);
|
|
}
|
|
};
|
|
|
|
// Tauri's universal-apple-darwin pseudo-target lipos the Rust binary itself
|
|
// but expects externalBin binaries to already be fat binaries named
|
|
// `<name>-universal-apple-darwin`, so build both slices and merge them here.
|
|
const buildUniversalMacCli = async (): Promise<void> => {
|
|
const arm64 = await compileDesktopCli("aarch64-apple-darwin");
|
|
const x64 = await compileDesktopCli("x86_64-apple-darwin");
|
|
const outfile = desktopCliOutfile("universal-apple-darwin");
|
|
await $`lipo -create -output ${outfile} ${arm64} ${x64}`;
|
|
await $`chmod +x ${outfile}`;
|
|
await $`lipo -info ${outfile}`;
|
|
};
|
|
|
|
const hasOpenTuiNativeVariant = (os: string, arch: string): boolean =>
|
|
existsSync(`${REPO_ROOT}/node_modules/.bun`) &&
|
|
[
|
|
...new Bun.Glob(`@opentui+core-${os}-${arch}@*`).scanSync({
|
|
cwd: `${REPO_ROOT}/node_modules/.bun`,
|
|
onlyFiles: false,
|
|
}),
|
|
].length > 0;
|
|
|
|
const main = async () => {
|
|
// The CLI, its Hub daemon, and the backend bundle must all carry the same
|
|
// SDK build identity, so build the SDK packages they bundle once, first.
|
|
await $`bun run build:sdk`.cwd(REPO_ROOT);
|
|
process.chdir(APP_ROOT);
|
|
const targetTriple = await resolveTargetTriple();
|
|
await $`mkdir -p ${BIN_DIR}/desktop-backend ${REMOTE_CLI_DIR}`;
|
|
// Every build cross-compiles the Linux SSH copies (and macOS builds both
|
|
// architectures), which needs the target platforms' OpenTUI native
|
|
// packages in addition to the host's.
|
|
const needed: Array<[string, string]> = [
|
|
["linux", "x64"],
|
|
["linux", "arm64"],
|
|
...(targetTriple === "universal-apple-darwin"
|
|
? ([
|
|
["darwin", "x64"],
|
|
["darwin", "arm64"],
|
|
] as Array<[string, string]>)
|
|
: []),
|
|
];
|
|
if (needed.some(([os, arch]) => !hasOpenTuiNativeVariant(os, arch))) {
|
|
await installOpenTuiNativeVariants();
|
|
}
|
|
await bundleDesktopBackend();
|
|
if (targetTriple === "universal-apple-darwin") {
|
|
await buildUniversalMacCli();
|
|
} else {
|
|
await compileDesktopCli(targetTriple);
|
|
}
|
|
await buildRemoteClis();
|
|
};
|
|
|
|
if (import.meta.main) {
|
|
main().catch((error: unknown) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|