From 856eeb758c1549e005b6cead3c0b5d0b5e4cba5b Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 22 Jul 2026 16:05:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(build):=20Docker=20=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E9=80=8F=E4=BC=A0=20git=20=E4=BF=A1=E6=81=AF,=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20x-yggdrasil-git=20=E6=81=92=E4=B8=BA=20unknown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .dockerignore 排除 .git/,容器内 build.rs 跑不了 git 命令,导致 x-yggdrasil-git / git_hash / commit_date 响应头和启动日志全是 "unknown" (version 来自 CARGO_PKG_VERSION 不受影响)。 三级取值链(build.rs env_or_git): YGG_BUILD_* 环境变量 → 本地 git 命令 → "unknown" 第一级 std::env::var 优先读 docker --build-arg 注入的值,第二级保留 本地 cargo build 的既有行为,兜底不变。 - Dockerfile: 主应用 server build 前加 ARG+ENV 块(ARG 仅 RUN 可见, build.rs 由 cargo 调用非 RUN,故需 export 成 ENV 让 cargo build 继承) - Makefile: GIT_DESCRIBE/HASH/DATE 在宿主采集,GIT_BUILD_ARGS 复用块 注入 docker/docker-amd64/docker-multiarch 三个 target - deploy-to-linux skill: 两处 docker buildx build 命令(首装 + 更新) 加同名 --build-arg,附说明 --- .agents/skills/deploy-to-linux/SKILL.md | 13 +++++++-- Dockerfile | 18 ++++++++++++ Makefile | 19 +++++++++++-- build.rs | 38 ++++++++++++++----------- 4 files changed, 66 insertions(+), 22 deletions(-) diff --git a/.agents/skills/deploy-to-linux/SKILL.md b/.agents/skills/deploy-to-linux/SKILL.md index 29b561c..d9c25cc 100644 --- a/.agents/skills/deploy-to-linux/SKILL.md +++ b/.agents/skills/deploy-to-linux/SKILL.md @@ -110,12 +110,17 @@ grep UseVirtualizationFrameworkRosetta ~/Library/Group\ Containers/group.com.doc ### 主应用镜像 ```bash -docker buildx build --platform linux/amd64 --load -t localhost/yggdrasil:latest . +docker buildx build --platform linux/amd64 --load \ + --build-arg YGG_BUILD_GIT_DESCRIBE="$(git describe --tags --always --dirty)" \ + --build-arg YGG_BUILD_GIT_HASH="$(git rev-parse HEAD)" \ + --build-arg YGG_BUILD_GIT_COMMIT_DATE="$(git log -1 --format=%cd --date=iso-strict)" \ + -t localhost/yggdrasil:latest . ``` - Dockerfile 用 `dpkg --print-architecture` 检测架构,amd64 腿原生构建 `x86_64-unknown-linux-musl` 静态二进制 - 首次约 15-30 分钟(Rosetta 下 cargo 全量编译),有 buildkit 缓存后分钟级 - 产物 `localhost/yggdrasil:latest`,scratch 运行时层约 16MB +- **`--build-arg` 透传 git 信息**:`.dockerignore` 排除 `.git/`,容器内 `build.rs` 跑不了 git,必须在宿主采集后注入,否则镜像的 `x-yggdrasil-git` 响应头恒为 `unknown`。三个 `$(...)` 命令替换在本机 zsh/bash 下执行(非服务器)。`make docker-amd64` 等价(Makefile 已内置同样的采集+透传) ### 6 个 Code Runner 沙箱镜像 @@ -418,7 +423,11 @@ rm -f /tmp/yggdrasil-*.tar* /tmp/.env ```bash # 1. 本地重新构建主应用(有缓存,快) -docker buildx build --platform linux/amd64 --load -t localhost/yggdrasil:latest . +docker buildx build --platform linux/amd64 --load \ + --build-arg YGG_BUILD_GIT_DESCRIBE="$(git describe --tags --always --dirty)" \ + --build-arg YGG_BUILD_GIT_HASH="$(git rev-parse HEAD)" \ + --build-arg YGG_BUILD_GIT_COMMIT_DATE="$(git log -1 --format=%cd --date=iso-strict)" \ + -t localhost/yggdrasil:latest . # 2. 导出传输 docker save localhost/yggdrasil:latest | gzip > /tmp/yggdrasil-app.tar.gz scp /tmp/yggdrasil-app.tar.gz :/root/docker/yggdrasil/ diff --git a/Dockerfile b/Dockerfile index 04fccb8..3d13fb1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -148,6 +148,24 @@ COPY libs/lightbox/package.json libs/lightbox/ COPY libs/yggdrasil-core/package.json libs/yggdrasil-core/ RUN cd libs && pnpm install --frozen-lockfile +# Build-time git info, injected by the caller via --build-arg. `.dockerignore` +# excludes `.git/`, so build.rs can't run `git` inside the container — these +# ARGs are the only channel for git metadata to reach build.rs (it reads them +# via std::env::var, its first-precedence source). Defaults are empty so a +# bare `docker build` without args degrades to "unknown" gracefully (build.rs +# falls back to the git command, then "unknown"). +# +# Each ARG → ENV pair is needed: ARG is only visible in Dockerfile RUN +# commands (and build.rs is invoked by cargo, not a RUN), so we export it as +# ENV for the cargo build step to inherit. Both default to empty; Makefile's +# docker/docker-amd64/docker-multiarch targets override them on the host. +ARG YGG_BUILD_GIT_DESCRIBE="" +ARG YGG_BUILD_GIT_HASH="" +ARG YGG_BUILD_GIT_COMMIT_DATE="" +ENV YGG_BUILD_GIT_DESCRIBE=${YGG_BUILD_GIT_DESCRIBE} +ENV YGG_BUILD_GIT_HASH=${YGG_BUILD_GIT_HASH} +ENV YGG_BUILD_GIT_COMMIT_DATE=${YGG_BUILD_GIT_COMMIT_DATE} + # Copy the rest of the source tree and build everything. COPY . . diff --git a/Makefile b/Makefile index a968395..f3b9a3e 100644 --- a/Makefile +++ b/Makefile @@ -186,19 +186,32 @@ doc-open: # Push examples: # make docker-multiarch IMAGE=ghcr.io/owner/yggdrasil:latest # make docker-multiarch IMAGE=user/yggdrasil:v1 PLATFORMS=linux/amd64 +# +# git 信息透传:.dockerignore 排除 .git/,容器内 build.rs 跑不了 git 命令, +# 所以在宿主(本 Makefile 里)采集后用 --build-arg 注入。Dockerfile 把 ARG +# 再 export 成 ENV,build.rs 的 std::env::var 优先读它。三个值在 Make 变量 +# 里采集一次,所有 docker target 复用;git 不可用时退化为空串,Dockerfile +# 默认值也是空,build.rs 最终降级为 "unknown",不阻断构建。 IMAGE ?= yggdrasil PLATFORMS ?= linux/amd64,linux/arm64 +GIT_DESCRIBE := $(shell git describe --tags --always --dirty 2>/dev/null) +GIT_HASH := $(shell git rev-parse HEAD 2>/dev/null) +GIT_DATE := $(shell git log -1 --format=%cd --date=iso-strict 2>/dev/null) +# build-arg 复用块:每个 docker target 展开一次。空值也传(让 Dockerfile 默认接管)。 +GIT_BUILD_ARGS = --build-arg YGG_BUILD_GIT_DESCRIBE="$(GIT_DESCRIBE)" \ + --build-arg YGG_BUILD_GIT_HASH="$(GIT_HASH)" \ + --build-arg YGG_BUILD_GIT_COMMIT_DATE="$(GIT_DATE)" docker: - @docker buildx build --load -t yggdrasil . + @docker buildx build --load $(GIT_BUILD_ARGS) -t yggdrasil . # Cross-build x86_64 into the local daemon. buildx 仿真非原生架构,无需修改 Dockerfile # (它本就按 dpkg --print-architecture 自适应选 musl target)。Apple Silicon 上走 QEMU, # 比 native 慢;产物可直接 docker run / docker save 导出。 docker-amd64: - @docker buildx build --platform linux/amd64 --load -t yggdrasil:amd64 . + @docker buildx build --platform linux/amd64 --load $(GIT_BUILD_ARGS) -t yggdrasil:amd64 . docker-multiarch: - @docker buildx build --platform $(PLATFORMS) -t $(IMAGE) --push . + @docker buildx build --platform $(PLATFORMS) $(GIT_BUILD_ARGS) -t $(IMAGE) --push . clean: @cargo clean diff --git a/build.rs b/build.rs index cfbdcda..e630076 100644 --- a/build.rs +++ b/build.rs @@ -7,6 +7,9 @@ //! - git 不可用(非仓库 / tarball 构建)时降级为 "unknown",不 fail the build。 //! - 声明 rerun-if-changed=.git/HEAD,否则 cargo 默认仅在 build.rs 自身变化时 //! 重跑,会导致打印的还是旧 hash。 +//! - 三级取值:编译期环境变量(`YGG_BUILD_*`,CI / docker --build-arg 注入) +//! → 本地 `git` 命令 → `"unknown"`。Docker 构建时 `.git/` 不进构建上下文 +//! (`.dockerignore` 排除),靠 Makefile 在宿主采集后用 `--build-arg` 透传。 use std::process::Command; @@ -16,9 +19,9 @@ fn main() { // 工作区脏状态变化时 describe 的 --dirty 后缀也会变,重跑一次。 println!("cargo:rerun-if-changed=.git/index"); - set_env("YGG_BUILD_GIT_DESCRIBE", git_describe()); - set_env("YGG_BUILD_GIT_HASH", git_hash()); - set_env("YGG_BUILD_GIT_COMMIT_DATE", git_commit_date()); + set_env("YGG_BUILD_GIT_DESCRIBE", env_or_git("YGG_BUILD_GIT_DESCRIBE", &["describe", "--tags", "--always", "--dirty"])); + set_env("YGG_BUILD_GIT_HASH", env_or_git("YGG_BUILD_GIT_HASH", &["rev-parse", "HEAD"])); + set_env("YGG_BUILD_GIT_COMMIT_DATE", env_or_git("YGG_BUILD_GIT_COMMIT_DATE", &["log", "-1", "--format=%cd", "--date=iso-strict"])); set_env("YGG_BUILD_RUSTC_VERSION", rustc_version()); // 编译时刻(Unix 秒)。std 无 ISO 8601 格式化,存秒数由运行时 chrono 解析。 set_env("YGG_BUILD_TIME", build_time_unix()); @@ -29,20 +32,21 @@ fn set_env(key: &str, value: String) { println!("cargo:rustc-env={key}={value}"); } -/// `git describe --tags --always --dirty`:最紧凑的"版本 + 提交数 + hash + 脏标记"。 -fn git_describe() -> String { - git_output(&["describe", "--tags", "--always", "--dirty"]).unwrap_or_else(|| "unknown".into()) -} - -/// 完整 40 位 commit hash。 -fn git_hash() -> String { - git_output(&["rev-parse", "HEAD"]).unwrap_or_else(|| "unknown".into()) -} - -/// 提交时间(ISO 8601 strict,带时区偏移,可直接被 chrono 解析)。 -fn git_commit_date() -> String { - git_output(&["log", "-1", "--format=%cd", "--date=iso-strict"]) - .unwrap_or_else(|| "unknown".into()) +/// 三级取值:`YGG_BUILD_` 环境变量 → 本地 git 命令 → `"unknown"`。 +/// +/// - 第一级优先 `std::env::var`(Docker 构建时由 `--build-arg` → `ARG` → `ENV` +/// 注入到 build.rs 进程环境),非空即采用——这是 Docker 构建唯一能拿到 git +/// 信息的通道(`.git/` 被 `.dockerignore` 排除,容器内没有仓库可查)。 +/// - 第二级落回本地 `git` 命令(本地 `cargo build` / 非 Docker CI 走这条)。 +/// - 兜底 `"unknown"`(tarball 构建、git 未安装等)。 +fn env_or_git(env_key: &str, git_args: &[&str]) -> String { + if let Ok(v) = std::env::var(env_key) { + let v = v.trim(); + if !v.is_empty() { + return v.to_string(); + } + } + git_output(git_args).unwrap_or_else(|| "unknown".into()) } /// 执行一条 git 命令,返回 trim 后的 stdout。失败返回 None(降级路径)。