Add persistent Rust and Go tooling to container

This commit is contained in:
Chris Wanstrath 2026-03-14 21:18:02 -07:00
parent c4c0703bee
commit 3c6cd05ef7

View File

@ -9,7 +9,13 @@ const DEBUG = !!process.env.DEBUG
const CONTAINER_NAME = "sandlot"
const USER = "ubuntu"
const CLAUDE_BIN = `/home/${USER}/.local/bin/claude`
const CONTAINER_PATH = `/home/${USER}/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`
const CONTAINER_PATH = `/sandlot/.cargo/bin:/sandlot/.go/bin:/sandlot/.gopath/bin:/home/${USER}/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`
const CONTAINER_ENV = {
RUSTUP_HOME: "/sandlot/.rustup",
CARGO_HOME: "/sandlot/.cargo",
GOROOT: "/sandlot/.go",
GOPATH: "/sandlot/.gopath",
}
/** Translate a host path to its corresponding container path. */
export function containerPath(hostPath: string): string {
@ -142,6 +148,30 @@ async function installTooling(cached: boolean, log?: (msg: string) => void): Pro
// Cache binaries for next time
await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"cp ~/.local/bin/bun ~/.local/bin/claude ~/.local/bin/neofetch /sandlot/.cache/ && cp /tmp/nvim.tar.gz /sandlot/.cache/nvim.tar.gz"}`.nothrow().quiet()
await installPersistentTooling(log)
}
/** Install Rust and Go to /sandlot/ so they persist across container recreates. */
async function installPersistentTooling(log?: (msg: string) => void): Promise<void> {
// Rust — skip if already installed on the persistent mount
const hasRust = await $`container exec --user ${USER} ${CONTAINER_NAME} test -x /sandlot/.cargo/bin/rustc`.nothrow().quiet()
if (hasRust.exitCode !== 0) {
log?.("Installing Rust")
await run(
$`container exec --user ${USER} ${CONTAINER_NAME} env ${`RUSTUP_HOME=${CONTAINER_ENV.RUSTUP_HOME}`} ${`CARGO_HOME=${CONTAINER_ENV.CARGO_HOME}`} bash -c ${"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"}`,
"Rust installation")
}
// Go — skip if already installed on the persistent mount
const hasGo = await $`container exec --user ${USER} ${CONTAINER_NAME} test -x /sandlot/.go/bin/go`.nothrow().quiet()
if (hasGo.exitCode !== 0) {
log?.("Installing Go")
await run(
$`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"mkdir -p /sandlot/.go && curl -fsSL https://go.dev/dl/go1.24.1.linux-arm64.tar.gz | tar xz -C /sandlot/.go --strip-components=1"}`,
"Go installation")
await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"mkdir -p /sandlot/.gopath"}`.nothrow().quiet()
}
}
/** Write a script to a temp file, copy it into ~/.local/bin/ in the container, then clean up. */
@ -273,6 +303,7 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?:
systemPromptLines.push(
"The host's ~/.sandlot is mounted at /sandlot.",
"Bun is installed at ~/.local/bin/bun. Use bun instead of node/npm.",
"Rust (cargo/rustc) is installed at /sandlot/.cargo/. Go is installed at /sandlot/.go/.",
)
if (opts?.print) {
systemPromptLines.push("IMPORTANT: Do not use plan mode. Do not call the EnterPlanMode tool. Proceed directly with the task.")
@ -280,7 +311,8 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?:
const systemPrompt = systemPromptLines.join("\n")
const term = process.env.TERM || "xterm-256color"
const args = ["container", "exec", "-it", "--user", USER, "--workdir", cwd, CONTAINER_NAME, "env", `TERM=${term}`, `PATH=${CONTAINER_PATH}`, CLAUDE_BIN, "--dangerously-skip-permissions", "--model", "claude-opus-4-6", "--append-system-prompt", systemPrompt]
const envArgs = [`TERM=${term}`, `PATH=${CONTAINER_PATH}`, ...Object.entries(CONTAINER_ENV).map(([k, v]) => `${k}=${v}`)]
const args = ["container", "exec", "-it", "--user", USER, "--workdir", cwd, CONTAINER_NAME, "env", ...envArgs, CLAUDE_BIN, "--dangerously-skip-permissions", "--model", "claude-opus-4-6", "--append-system-prompt", systemPrompt]
if (opts?.continue) args.push("--continue")
if (opts?.print) args.push("-p", opts.print)
else if (opts?.prompt) args.push(opts.prompt)
@ -309,7 +341,8 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?:
export async function shell(workdir?: string): Promise<void> {
const args = ["container", "exec", "-it", "--user", USER]
if (workdir) args.push("--workdir", containerPath(workdir))
args.push(CONTAINER_NAME, "env", "TERM=xterm-256color", `PATH=${CONTAINER_PATH}`, "fish", "--login")
const envArgs = ["TERM=xterm-256color", `PATH=${CONTAINER_PATH}`, ...Object.entries(CONTAINER_ENV).map(([k, v]) => `${k}=${v}`)]
args.push(CONTAINER_NAME, "env", ...envArgs, "fish", "--login")
const proc = Bun.spawn(args, { stdin: "inherit", stdout: "inherit", stderr: "inherit" })
await proc.exited
}
@ -317,7 +350,7 @@ export async function shell(workdir?: string): Promise<void> {
/** Run neofetch in the container. */
export async function neofetch(): Promise<void> {
const proc = Bun.spawn(
["container", "exec", "--user", USER, CONTAINER_NAME, "env", `PATH=${CONTAINER_PATH}`, "neofetch"],
["container", "exec", "--user", USER, CONTAINER_NAME, "env", `PATH=${CONTAINER_PATH}`, ...Object.entries(CONTAINER_ENV).map(([k, v]) => `${k}=${v}`), "neofetch"],
{ stdin: "inherit", stdout: "inherit", stderr: "inherit" },
)
await proc.exited
@ -325,7 +358,8 @@ export async function neofetch(): Promise<void> {
/** Run a bash command in the container at the given workdir, capturing output. */
export async function exec(workdir: string, command: string): Promise<{ exitCode: number; stdout: string; stderr: string }> {
const result = await $`container exec --user ${USER} --workdir ${containerPath(workdir)} ${CONTAINER_NAME} bash -c ${"export PATH=$HOME/.local/bin:$PATH; " + command}`.nothrow().quiet()
const envExports = Object.entries(CONTAINER_ENV).map(([k, v]) => `export ${k}=${v}`).join("; ")
const result = await $`container exec --user ${USER} --workdir ${containerPath(workdir)} ${CONTAINER_NAME} bash -c ${`export PATH=$HOME/.local/bin:$PATH; ${envExports}; ` + command}`.nothrow().quiet()
return {
exitCode: result.exitCode,
stdout: result.stdout.toString().trim(),