#!/usr/bin/env bash # 消费镜像内可信快照;写入留在任务容器,master push 可在任务结束后导出。 set -euo pipefail cache_root="${GENARRATIVE_CI_RUST_CACHE_ROOT:-/opt/genarrative-ci/rust-cache}" cache_binary="${cache_root}/sccache" state="${GENARRATIVE_CI_RUST_CACHE_STATE:-}" configure_local_cache() { # 不继承开发机/其它流水线的 OSS、S3、GHA 或 daemon 配置。 local variable for variable in ${!SCCACHE_@}; do unset "${variable}"; done export SCCACHE_CONF="${state}/config" export SCCACHE_DIR="${cache_root}/objects" export SCCACHE_CACHE_SIZE=4G export SCCACHE_SERVER_UDS="${state}/server.sock" # 单个不可缓存的链接/测试阶段可能超过一分钟;保持统计直到 report 显式停止。 export SCCACHE_IDLE_TIMEOUT=0 export SCCACHE_IGNORE_SERVER_IO_ERROR=1 } case "${1:-}" in prepare) : "${GITHUB_ENV:?GITHUB_ENV is required}" printf 'RUSTC_WRAPPER=\nCARGO_BUILD_RUSTC_WRAPPER=\nGENARRATIVE_CI_RUST_CACHE_STATE=\nGENARRATIVE_CI_RUST_CACHE_EXPORT_READY=\n' >> "${GITHUB_ENV}" fallback() { printf '[rust-cache] mode=direct reason=%s\n' "$1"; exit 0; } [[ -x "${cache_binary}" && -d "${cache_root}/objects" && -f "${cache_root}/rustc.txt" && -f "${cache_root}/source-commit.txt" && -f "${cache_root}/workspace.txt" ]] \ || fallback snapshot-unavailable if ! rustc -vV | cmp -s - "${cache_root}/rustc.txt"; then fallback toolchain-mismatch fi if [[ "$(pwd -P)" != "$(cat "${cache_root}/workspace.txt")" ]]; then fallback workspace-mismatch fi # 使用短 Unix socket 路径,避免共享固定端口或超出 sockaddr_un 长度。 state="$(mktemp -d /tmp/ci-rust-cache.XXXXXX)" printf 'server_startup_timeout_ms = 5000\n' > "${state}/config" configure_local_cache # 探测必须暴露 daemon 故障;只有正式编译允许 sccache 的 IO 回退。 unset SCCACHE_IGNORE_SERVER_IO_ERROR if ! timeout --kill-after=2 15 "${cache_binary}" "$(command -v rustc)" -vV > "${state}/probe.log" 2>&1; then timeout --kill-after=2 5 "${cache_binary}" --stop-server >/dev/null 2>&1 || true rm -rf -- "${state}" fallback wrapper-probe-failed fi script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/$(basename "${BASH_SOURCE[0]}")" if [[ "${GITHUB_EVENT_NAME:-}" == push && "${GITHUB_REF:-}" == refs/heads/master ]]; then # 只记 key/大小/时间,不读取对象正文;PR 没有扫描与打包开销。 python3 "$(dirname "${script_path}")/export-gitea-rust-cache.py" baseline "${state}/baseline.json" fi # wrapper 路径也参与 Rust cache key;固定容器内路径,隔离由 job 容器保证。 wrapper_path="${cache_root}/rustc-wrapper" printf '#!/usr/bin/env bash\nexec bash %q "$@"\n' "${script_path}" > "${wrapper_path}" chmod 700 "${wrapper_path}" { printf 'GENARRATIVE_CI_RUST_CACHE_ROOT=%s\n' "${cache_root}" printf 'GENARRATIVE_CI_RUST_CACHE_STATE=%s\n' "${state}" printf 'RUSTC_WRAPPER=%s\nCARGO_BUILD_RUSTC_WRAPPER=%s\n' "${wrapper_path}" "${wrapper_path}" } >> "${GITHUB_ENV}" printf '[rust-cache] mode=sccache snapshot=%s\n' "$(cat "${cache_root}/source-commit.txt")" ;; report) if [[ -n "${state}" && -d "${state}" ]]; then configure_local_cache timeout --kill-after=2 5 "${cache_binary}" --show-stats || true # 只有成功停服后才允许读取对象;失败不能把仍在写入的目录发布成完整快照。 if timeout --kill-after=2 15 "${cache_binary}" --stop-server >/dev/null 2>&1; then if [[ "${GITHUB_EVENT_NAME:-}" == push && "${GITHUB_REF:-}" == refs/heads/master && ! -f "${state}/disabled" && -f "${state}/baseline.json" ]]; then mv -- "${state}/baseline.json" "${cache_root}/export-baseline.json" printf 'GENARRATIVE_CI_RUST_CACHE_EXPORT_READY=1\n' >> "${GITHUB_ENV}" fi fi rm -rf -- "${state}" else printf '[rust-cache] mode=direct\n' fi ;; *) # Cargo wrapper 协议:第一个参数是真实 rustc。失去缓存状态时仍执行原编译。 if [[ -z "${state}" || ! -f "${state}/config" || -f "${state}/disabled" || ! -x "${cache_binary}" ]]; then exec "$@" fi configure_local_cache status=0 "${cache_binary}" "$@" || status=$? if [[ "${status}" == 2 ]]; then # 固定 sccache 版本的自身错误码为 2;实际 rustc 失败通常为 1/101。 # 只重试这次编译,不重跑 Cargo 或测试,真实编译错误仍按 rustc 状态返回。 echo '[rust-cache] cache infrastructure failed; compiling directly' >&2 # 后续 crate 直接编译,避免坏 daemon 让每个 crate 都支付一次启动超时。 : > "${state}/disabled" exec "$@" fi exit "${status}" ;; esac