#!/usr/bin/env bash # 由能管理 CI 镜像的维护者运行;不得在 PR job 内提供 Docker API/发布权限。 set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" base_ref="${1:?usage: build-gitea-rust-cache.sh }" candidate_tag="${2:?candidate image tag is required}" requested_source_commit="${3:-}" if [[ "$#" -gt 3 ]]; then echo 'usage: build-gitea-rust-cache.sh [master-commit-sha]' >&2 exit 2 fi if [[ -n "${requested_source_commit}" && ! "${requested_source_commit}" =~ ^[0-9a-fA-F]{40}$ ]]; then echo 'master-commit-sha must be a complete 40-character SHA.' >&2 exit 2 fi # 与实际 Gitea checkout 路径一致;Rust 对象 key 包含编译 cwd,不能随意换临时根。 workspace=/workspace/GenarrativeAI/Genarrative [[ "${CI:-}" != true ]] || { echo 'Run on the trusted image builder, outside CI jobs.' >&2; exit 1; } base_id="$(docker image inspect --format '{{.Id}}' "${base_ref}")" [[ "${base_id}" =~ ^sha256:[a-f0-9]{64}$ ]] # 删除容器内旧对象不能释放镜像底层;每次必须从不含对象快照的基础镜像重建。 docker run --rm --network none --read-only --cap-drop=ALL \ --security-opt=no-new-privileges --entrypoint /bin/bash "${base_id}" -c ' if [[ -e /opt/genarrative-ci/rust-cache ]]; then echo "基础镜像已包含 Rust 对象缓存;请使用不含对象快照的原始 CI 镜像,禁止叠层。" >&2 exit 1 fi ' bash "${repo_root}/scripts/gitea-ci-job-image.sh" verify "${base_id}" # 只归档远端 master 的确定提交;不复制当前工作区或本地凭据。 git -C "${repo_root}" fetch --no-tags origin refs/heads/master master_commit="$(git -C "${repo_root}" rev-parse FETCH_HEAD^{commit})" if [[ -n "${requested_source_commit}" ]]; then git -C "${repo_root}" cat-file -e "${requested_source_commit}^{commit}" if ! git -C "${repo_root}" merge-base --is-ancestor "${requested_source_commit}" "${master_commit}"; then echo "master-commit-sha is not contained in fetched master: ${requested_source_commit}" >&2 exit 1 fi source_commit="$(git -C "${repo_root}" rev-parse "${requested_source_commit}^{commit}")" else source_commit="${master_commit}" fi work_dir="$(mktemp -d "${TMPDIR:-/tmp}/gitea-rust-cache.XXXXXX")" container_id='' cleanup() { if [[ -n "${container_id}" ]]; then docker rm -f "${container_id}" >/dev/null; fi rm -rf -- "${work_dir}" } trap cleanup EXIT archive=sccache-v0.18.0-x86_64-unknown-linux-musl.tar.gz curl --fail --location --retry 3 --connect-timeout 15 --max-time 180 \ "https://github.com/mozilla/sccache/releases/download/v0.18.0/${archive}" \ --output "${work_dir}/${archive}" printf '45f1447fbe231e3037bde351ef70677dd212216c8d62ae7ca409fecc4d6acc89 %s\n' "${work_dir}/${archive}" | sha256sum --check tar -xzf "${work_dir}/${archive}" --directory "${work_dir}" mkdir "${work_dir}/snapshot" cp "${work_dir}/sccache-v0.18.0-x86_64-unknown-linux-musl/sccache" "${work_dir}/snapshot/sccache" printf '%s\n' "${source_commit}" > "${work_dir}/snapshot/source-commit.txt" printf '%s\n' "${base_id}" > "${work_dir}/snapshot/base-image.txt" printf '%s\n' "${workspace}" > "${work_dir}/snapshot/workspace.txt" # 临时容器不挂载宿主目录/socket,不携带 Git/OSS/Jenkins 凭据,限制资源占用。 container_id="$(docker run --detach --cpus=4 --memory=12g --pids-limit=1024 \ --cap-drop=ALL --security-opt=no-new-privileges \ --entrypoint /bin/bash "${base_id}" -c 'sleep infinity')" docker exec "${container_id}" mkdir -p "${workspace}" /opt/genarrative-ci/rust-cache/objects git -C "${repo_root}" archive "${source_commit}" | docker cp - "${container_id}:${workspace}" docker cp "${work_dir}/snapshot/." "${container_id}:/opt/genarrative-ci/rust-cache/" docker exec "${container_id}" cp "${workspace}/scripts/ci-rust-cache.sh" /tmp/ci-rust-cache.sh docker exec --interactive --workdir "${workspace}" "${container_id}" bash -s <<'WARM' set -euo pipefail rustc -vV > /opt/genarrative-ci/rust-cache/rustc.txt export GITHUB_ENV=/tmp/rust-cache.env CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=4 CI=true # sccache 对 CARGO_*(除 jobserver/jobs 等特例)参与 hash,必须与 workflow 对齐。 export CARGO_HTTP_MULTIPLEXING=false CARGO_NET_RETRY=10 CARGO_TERM_COLOR=always bash /tmp/ci-rust-cache.sh prepare set -a source "${GITHUB_ENV}" set +a test -n "${RUSTC_WRAPPER}" trap 'bash /tmp/ci-rust-cache.sh report' EXIT # 与 CI 的工作目录、features 和目标逐项对齐,只编译,不运行测试/应用。 echo '[rust-cache] warming backend tests and checks' cargo test --locked --workspace --exclude spacetime-module --no-fail-fast \ --manifest-path server-rs/Cargo.toml --no-run cargo test --locked -p spacetime-module --no-fail-fast \ --manifest-path server-rs/Cargo.toml --no-run cargo check --locked -p api-server --all-targets --manifest-path server-rs/Cargo.toml cargo check --locked -p spacetime-module --manifest-path server-rs/Cargo.toml echo '[rust-cache] warming AGC standalone, shared and plugin crates' for manifest in \ server-rs/crates/agent-runtime-core/Cargo.toml \ server-rs/crates/agent-runtime-orchestration/Cargo.toml \ plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml; do # 与 CI 一样,这些 crate 没有已提交的独立 Cargo.lock。 cargo test --manifest-path "${manifest}" --no-run done cargo test --locked -p platform-llm --manifest-path server-rs/Cargo.toml --no-run cargo test --locked -p shared-contracts --manifest-path server-rs/Cargo.toml --no-run for editor in unity godot; do cargo test --locked --manifest-path "plugins/agc-${editor}-editor/native/${editor}-editor-bridge/Cargo.toml" --no-run done echo '[rust-cache] warming desktop tests and AGC prompt contracts' cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml --no-run cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml \ --test runtime_prompt_bundle_build --test prompt_source_boundaries --no-run # Cargo 的 fresh 判断不保证不同 cwd 都经过 sccache;先清掉临时容器中的 target。 cargo clean --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml echo '[rust-cache] warming AGC Rust lanes' ( cd apps/ai-game-creator-shell/src-tauri cargo test --locked --manifest-path Cargo.toml \ --bin genarrative-ai-game-creator-shell --no-run ) cargo clean --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml echo '[rust-cache] warming AGC agent-run smoke executable' ( cd apps/ai-game-creator-shell cargo build --manifest-path src-tauri/Cargo.toml ) du -sh /opt/genarrative-ci/rust-cache/objects WARM docker cp "${container_id}:/opt/genarrative-ci/rust-cache/." "${work_dir}/snapshot/" docker rm -f "${container_id}" >/dev/null container_id='' # 从原基础镜像重新组装,只 COPY 对象快照;不 commit 含源码/target 的预热容器。 cat > "${work_dir}/Dockerfile" < "${work_dir}/.dockerignore" docker build --pull=false --tag "${candidate_tag}" "${work_dir}" bash "${repo_root}/scripts/gitea-ci-job-image.sh" verify "${candidate_tag}" printf 'snapshot_source=%s\ncandidate_image=%s\n' "${source_commit}" "$(docker image inspect --format '{{.Id}}' "${candidate_tag}")" echo 'Candidate only: the runner configuration and running jobs have not been changed.'