Files
cline/apps/cli/script/compile-binary.ts
T
Dominic Cooney 82486434b3 fix(cli): make build:platforms:single work on Windows (#15206)
* fix(cli): make build:platforms:single work on Windows

Replace shell rm -rf with rmSync in apps/cli/script/build.ts, retry
temporary smoke-test directory cleanup while the detached hub process
exits on Windows, and use mkdtempSync with non-Windows chmodSync in
apps/cli/script/compile-binary.ts.

* fix(cli): warn if smoke-test directory cleanup fails after retries

Use rmSync maxRetries/retryDelay for post-smoke-test cleanup and log a
warning with the directory path and error if removal still fails.
2026-10-20 08:47:31 +09:00

172 lines
5.6 KiB
TypeScript

import {
chmodSync,
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, relative, resolve } from "node:path";
import { $ } from "bun";
import { buildDashboardRuntimeDirectory } from "./dashboard-runtime";
const cliDir = resolve(import.meta.dir, "..");
const rootDir = resolve(cliDir, "../..");
// Telemetry / OTEL environment variables that should be baked into the
// compiled binary at build time. Mirrors the list of secrets injected by the
// `cli-publish` GitHub Actions workflow. These are inlined via Bun's `define`
// so the CLI ships with the production telemetry configuration without
// requiring the end user to set any env vars.
const BUILD_TIME_INLINED_ENV_VARS = [
"TELEMETRY_SERVICE_API_KEY",
"ERROR_SERVICE_API_KEY",
"OTEL_TELEMETRY_ENABLED",
"OTEL_LOGS_EXPORTER",
"OTEL_METRICS_EXPORTER",
"OTEL_TRACES_EXPORTER",
"CLINE_TRACE_SAMPLE_PERCENT",
"CLINE_TRACE_RECORD_CONTENT",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_HEADERS",
] as const;
function buildInlinedEnvDefines(): Record<string, string> {
const defines: Record<string, string> = {};
for (const name of BUILD_TIME_INLINED_ENV_VARS) {
defines[`process.env.${name}`] = JSON.stringify(process.env[name] ?? "");
}
return defines;
}
function findOpenTuiParserWorker(): string {
const localPath = resolve(
cliDir,
"node_modules/@opentui/core/parser.worker.js",
);
const rootPath = resolve(
rootDir,
"node_modules/@opentui/core/parser.worker.js",
);
const parserWorkerPath = existsSync(localPath) ? localPath : rootPath;
return realpathSync(parserWorkerPath);
}
export function readOpenTuiVersion(): string | undefined {
const pkg = JSON.parse(readFileSync(join(cliDir, "package.json"), "utf-8"));
return pkg.dependencies?.["@opentui/core"];
}
/**
* Bun only has the host platform's OpenTUI native package by default, and a
* cross-compiled build cannot resolve the FFI layer without the target's.
* Installs every platform variant of the pinned version.
*/
export async function installOpenTuiNativeVariants(): Promise<void> {
const opentuiVersion = readOpenTuiVersion();
if (!opentuiVersion) {
return;
}
console.log(
`Installing all platform variants of @opentui/core@${opentuiVersion}...`,
);
await $`bun install --no-save --os="*" --cpu="*" @opentui/core@${opentuiVersion}`.cwd(
cliDir,
);
}
export interface CompileCliBinaryOptions {
bunTarget: Bun.Build.CompileTarget;
outfile: string;
/** Runtime flags baked into the executable, e.g. `--use-system-ca`. */
execArgv?: string[];
/**
* Read `.env` / `bunfig.toml` from the launch directory before the CLI
* starts. Bun's default; binaries launched in directories the user did not
* pick (an SSH home, a GUI app's cwd) should turn this off.
*/
autoloadLaunchDirectoryConfig?: boolean;
}
/** Compiles the CLI entrypoint into one self-contained executable. */
export async function compileCliBinary(
input: CompileCliBinaryOptions,
): Promise<void> {
const pluginRuntime = join(rootDir, "sdk/packages/core/dist/plugin-runtime");
const dashboardRuntime = await buildDashboardRuntimeDirectory();
const parserWorker = findOpenTuiParserWorker();
const targetOs = input.bunTarget.includes("windows") ? "windows" : "posix";
const bunfsRoot = targetOs === "windows" ? "B:/~BUN/root/" : "/$bunfs/root/";
const parserWorkerPath = relative(rootDir, parserWorker).replaceAll(
"\\",
"/",
);
// Build to /tmp first so Bun's temp-file rename stays on one filesystem
// layer in containerized environments (virtiofs, overlayfs).
const entrypoint = join(cliDir, "src/index.ts");
const scratchRoot = process.platform === "win32" ? tmpdir() : "/tmp";
const tmpDir = mkdtempSync(
join(scratchRoot, `cline-build-${input.bunTarget}-`),
);
const tmpOutfile = join(
tmpDir,
input.outfile.endsWith(".exe") ? "cline.exe" : "cline",
);
const previousCwd = process.cwd();
process.chdir(scratchRoot);
let result: Awaited<ReturnType<typeof Bun.build>>;
try {
const autoload = input.autoloadLaunchDirectoryConfig !== false;
result = await Bun.build({
entrypoints: [entrypoint, parserWorker],
root: rootDir,
splitting: true,
compile: {
target: input.bunTarget,
outfile: tmpOutfile,
assets: [pluginRuntime, dashboardRuntime],
// Plugins import SDK packages from the runtime tree; let Bun resolve
// their package exports without falling back to transpiling the SDK.
autoloadPackageJson: true,
...(input.execArgv?.length ? { execArgv: input.execArgv } : {}),
...(autoload ? {} : { autoloadDotenv: false, autoloadBunfig: false }),
},
minify: true,
external: ["@anthropic-ai/vertex-sdk"],
define: {
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + parserWorkerPath,
// compile.assets keeps each directory tree under its basename.
__CLINE_PLUGIN_RUNTIME_DIR__: JSON.stringify(
`${bunfsRoot}plugin-runtime`,
),
__CLINE_DASHBOARD_RUNTIME_DIR__: JSON.stringify(`${bunfsRoot}webview`),
// Inline telemetry/OTEL env vars at build time so the compiled
// binary ships with production telemetry configuration baked in.
...buildInlinedEnvDefines(),
},
throw: false,
});
if (!result.success) {
for (const log of result.logs) {
console.error(log);
}
throw new Error(`CLI build failed for ${input.bunTarget}`);
}
mkdirSync(dirname(input.outfile), { recursive: true });
copyFileSync(tmpOutfile, input.outfile);
if (process.platform !== "win32") {
chmodSync(input.outfile, 0o755);
}
} finally {
process.chdir(previousCwd);
rmSync(tmpDir, { recursive: true, force: true });
}
}