fix(build): Docker 构建透传 git 信息,修复 x-yggdrasil-git 恒为 unknown

.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,附说明
This commit is contained in:
xfy 2026-07-22 16:05:49 +08:00
parent eaabb87108
commit 856eeb758c
4 changed files with 66 additions and 22 deletions

View File

@ -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 <host>:/root/docker/yggdrasil/

View File

@ -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 . .

View File

@ -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

View File

@ -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_<KEY>` 环境变量 → 本地 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(降级路径)。