#!/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}" # 与实际 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 source_commit="$(git -C "${repo_root}" rev-parse FETCH_HEAD^{commit})" 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 cp "${repo_root}/scripts/ci-rust-cache.sh" "${container_id}:/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 cd apps/ai-game-creator-shell/src-tauri cargo test --locked --manifest-path Cargo.toml \ --bin genarrative-ai-game-creator-shell --no-run 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.'