feat(code-runner): add Go and Rust language support

为 Code Runner 新增 Go 和 Rust 两种编译型语言,沿用现有 base 镜像分层结构。

Go:
- run_cmd 直接用 go run(单条命令,内部编译+运行)
- 镜像把 GOCACHE/GOTMPDIR/GOPATH 重定向到 /tmp tmpfs
  (只读根文件系统下 /Users/issuser/.cache 不可写)
- default_limits: 1 核 / 256MB / 10s(编译冷启动比解释型慢)

Rust:
- rustc 编译 + 运行是两步,但 docker.rs 注入脚本用 exec 执行 run_cmd,
  exec 替换 shell 进程后 'A && B' 后半段不执行,
  因此镜像内置 /usr/local/bin/run-rust.sh wrapper 封装两步
- default_limits: 1 核 / 512MB / 15s(rustc 内存大、编译慢)

新增镜像与白名单后,默认 CODE_RUNNER_LANGUAGES=python,node,go,rust。
This commit is contained in:
xfy 2026-07-09 09:40:32 +08:00
parent d690910efa
commit c624997c04
9 changed files with 138 additions and 4 deletions

View File

@ -140,7 +140,7 @@ CODE_RUNNER_MAX_CPU_CORES=2.0
CODE_RUNNER_MAX_MEMORY_MB=1024
CODE_RUNNER_MAX_TIMEOUT_SECS=30
CODE_RUNNER_MAX_OUTPUT_BYTES=1048576
CODE_RUNNER_LANGUAGES=python,node
CODE_RUNNER_LANGUAGES=python,node,go,rust
DOCKER_SOCKET_PATH=/var/run/docker.sock
# Rate limits (governor keyed limiters)

View File

@ -185,7 +185,7 @@ Readers can execute fenced code blocks in isolated Docker containers; authors ge
**Governor 0.8 caveat**: `Quota::per_day` does not exist; `CODE_EXEC_DAILY_LIMITER` uses `Quota::with_period(24h).allow_burst(daily)`. `RATE_LIMIT_CODE_EXEC_PER_SEC` must be an integer (governor's `per_second` takes `NonZeroU32`; decimals in `.env` fall back to the default 1).
**Runner images** (`docker/`): `build-runners.sh` builds `yggdrasil-runner-base``yggdrasil-runner-python``yggdrasil-runner-node`; tags must match `LANGUAGES` image fields. Python image symlinks `python``python3` to match `run_cmd`. `runner.toml` files are image self-descriptions only — runtime config is the Rust `LANGUAGES` registry + `CODE_RUNNER_*` env, not parsed from toml.
**Runner images** (`docker/`): `build-runners.sh` builds `yggdrasil-runner-base``yggdrasil-runner-python``yggdrasil-runner-node``yggdrasil-runner-go``yggdrasil-runner-rust`; tags must match `LANGUAGES` image fields. Python image symlinks `python``python3` to match `run_cmd`. Go image redirects `GOCACHE`/`GOTMPDIR`/`GOPATH` to `/tmp` (read-only rootfs makes `$HOME/.cache` unwritable). Rust image ships a `/usr/local/bin/run-rust.sh` wrapper because `rustc` compile + run is two steps and `docker.rs` injects `run_cmd` via `exec``exec A && B` would replace the shell and never reach `B`, so the two steps are encapsulated in the wrapper. `runner.toml` files are image self-descriptions only — runtime config is the Rust `LANGUAGES` registry + `CODE_RUNNER_*` env, not parsed from toml.
## Frontend Lib Subprojects

View File

@ -1,11 +1,13 @@
#!/bin/sh
# 构建代码运行器所需的沙箱镜像base → python / node
# 构建代码运行器所需的沙箱镜像base → python / node / go / rust
#
# 依赖:本机已安装 docker。构建顺序固定python/node FROM base
# 依赖:本机已安装 docker。构建顺序固定python/node/go/rust 均为 FROM base
# 镜像 tag 与 src/api/code_runner/languages.rs 的注册项严格对应:
# yggdrasil-runner-base:latest
# yggdrasil-runner-python:latest
# yggdrasil-runner-node:latest
# yggdrasil-runner-go:latest
# yggdrasil-runner-rust:latest
set -e
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
@ -19,6 +21,12 @@ docker build -t yggdrasil-runner-python:latest "$SCRIPT_DIR/runner-python"
echo "==> Building yggdrasil-runner-node:latest"
docker build -t yggdrasil-runner-node:latest "$SCRIPT_DIR/runner-node"
echo "==> Building yggdrasil-runner-go:latest"
docker build -t yggdrasil-runner-go:latest "$SCRIPT_DIR/runner-go"
echo "==> Building yggdrasil-runner-rust:latest"
docker build -t yggdrasil-runner-rust:latest "$SCRIPT_DIR/runner-rust"
echo "==> Done. Images:"
docker images --filter "reference=yggdrasil-runner-*" \
--format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"

View File

@ -0,0 +1,21 @@
# Go 运行沙箱镜像。
# 与 src/api/code_runner/languages.rs 的 go 注册项对齐:
# image: yggdrasil-runner-go:latest
# run_cmd: go run /code/main.go
#
# go run 是单条命令(内部编译 + 运行),可直接作为 run_cmd无需 wrapper
# 注意:运行时只读根文件系统 + tmpfs /code /tmp$HOME/.cache 不可写,
# 必须把 GOCACHE / GOTMPDIR / GOPATH 重定向到可写的 /tmp tmpfs。
FROM yggdrasil-runner-base:latest
RUN apk add --no-cache go \
&& mkdir -p /tmp/go-cache /tmp/gopath \
&& chown -R runner:runner /tmp/go-cache /tmp/gopath
ENV GOCACHE=/tmp/go-cache \
GOTMPDIR=/tmp \
GOPATH=/tmp/gopath
COPY runner.toml /runner.toml
USER runner

View File

@ -0,0 +1,15 @@
# Go runner 镜像的运行时默认值(参考用)。
# 实际生效配置由 Rust 侧 LANGUAGES 注册表 + CODE_RUNNER_* 环境变量决定(见
# src/api/code_runner/languages.rs、src/infra/runner_config.rs本文件仅作为
# 镜像构建产物的自描述,便于运维核对镜像与代码的契约一致。
language = "go"
extension = "go"
run_command = "go run /code/main.go"
allow_network = false
[default_limits]
cpu_cores = 1.0
memory_mb = 256
timeout_secs = 10
output_bytes = 1048576
allow_network = false

View File

@ -0,0 +1,18 @@
# Rust 运行沙箱镜像。
# 与 src/api/code_runner/languages.rs 的 rust 注册项对齐:
# image: yggdrasil-runner-rust:latest
# run_cmd: /usr/local/bin/run-rust.sh
#
# rustc 编译 + 运行是两步,但 docker.rs 的注入脚本用 exec 执行 run_cmd
# exec 会替换 shell 进程,导致 "A && B" 的后半段不执行。
# 因此镜像内置 run-rust.sh wrapper 封装「编译并运行」逻辑。
FROM yggdrasil-runner-base:latest
RUN apk add --no-cache rust
COPY run-rust.sh /usr/local/bin/run-rust.sh
RUN chmod +x /usr/local/bin/run-rust.sh
COPY runner.toml /runner.toml
USER runner

View File

@ -0,0 +1,13 @@
#!/bin/sh
# Rust 源码编译并运行的 wrapper。
#
# 背景src/infra/docker.rs 的源码注入脚本为
# sh -c "cat > /code/main.rs && exec {run_cmd}"
# 其中 exec 会用 run_cmd 进程替换 sh 进程。若直接写
# "rustc -o /tmp/main /code/main.rs && /tmp/main"
# exec 替换后 && 后半段永远不会执行exec 成功即不再返回 shell
# 因此必须把「编译 + 运行」封装进本脚本,由 sh 解释执行:
# rustc 编译成功后,再用 exec 替换为编译产物,避免多一层常驻进程。
#
# 编译产物写到 /tmp/maintmpfs 64m 可写;只读根文件系统下 / 不可写)。
rustc -o /tmp/main /code/main.rs && exec /tmp/main

View File

@ -0,0 +1,19 @@
# Rust runner 镜像的运行时默认值(参考用)。
# 实际生效配置由 Rust 侧 LANGUAGES 注册表 + CODE_RUNNER_* 环境变量决定(见
# src/api/code_runner/languages.rs、src/infra/runner_config.rs本文件仅作为
# 镜像构建产物的自描述,便于运维核对镜像与代码的契约一致。
#
# run_command 指向镜像内置的 wrapper 脚本run-rust.sh而非裸 rustc 命令:
# rustc 编译 + 运行是两步docker.rs 注入脚本用 exec 执行 run_cmd
# exec 替换 shell 进程后 "A && B" 后半段不执行,故封装进脚本。
language = "rust"
extension = "rs"
run_command = "/usr/local/bin/run-rust.sh"
allow_network = false
[default_limits]
cpu_cores = 1.0
memory_mb = 512
timeout_secs = 15
output_bytes = 1048576
allow_network = false

View File

@ -63,6 +63,46 @@ pub static LANGUAGES: LazyLock<HashMap<String, LanguageDef>> = LazyLock::new(||
},
);
// 编译型语言go run 是单条命令(内部编译 + 运行),可直接作为 run_cmd。
// 只读根文件系统下 $HOME/.cache 不可写,镜像已把 GOCACHE/GOTMPDIR/GOPATH
// 重定向到可写的 /tmp tmpfs。编译冷启动比解释型慢timeout 提到 10s。
m.insert(
"go".to_string(),
LanguageDef {
image: "yggdrasil-runner-go:latest".to_string(),
run_cmd: "go run /code/main.go".to_string(),
extension: "go".to_string(),
default_limits: ResourceLimits {
cpu_cores: 1.0,
memory_mb: 256,
timeout_secs: 10,
output_bytes: 1_048_576,
allow_network: false,
},
allow_network: false,
},
);
// rustc 编译 + 运行是两步,但 docker.rs 注入脚本用 exec 执行 run_cmd
// exec 替换 shell 进程后 "A && B" 后半段不执行,故镜像内置 run-rust.sh wrapper。
// rustc 内存开销大、编译慢memory 提到 512MB、timeout 提到 15s。
m.insert(
"rust".to_string(),
LanguageDef {
image: "yggdrasil-runner-rust:latest".to_string(),
run_cmd: "/usr/local/bin/run-rust.sh".to_string(),
extension: "rs".to_string(),
default_limits: ResourceLimits {
cpu_cores: 1.0,
memory_mb: 512,
timeout_secs: 15,
output_bytes: 1_048_576,
allow_network: false,
},
allow_network: false,
},
);
m
});