CI: Improve ccache handling in build and test jobs (#7874)

Cache key isolation: Include the build flavour (ccwarn/asan/gcov) in the
build ccache key, so the coverage (gcov), asan, and RTLMeter
(ccwarn-off) configurations no longer share and thrash the single
'<runs-on>-<cc>' bucket, nor warm-start from each other's (near-useless)
caches.

Prune stale entries instead of a fixed size cap: After building/testing,
evict ccache entries this run did not touch (--evict-older-than, based
on the run's own duration).  This bounds the saved cache to the current
working set, so CCACHE_MAXSIZE and CCACHE_LIMIT_MULTIPLE are no longer
needed and are removed (ccache falls back to its 5G default ceiling, but
is realistically much smaller).

ccache configuration:
- Drop CCACHE_COMPRESS (already the default in ccache 4.x).
- Add CCACHE_COMPILERCHECK=content, so a changed compiler mtime on a rebuilt
  ephemeral runner image does not invalidate the whole restored cache.
- Hoist all ccache-related variables (CACHE_BASE_KEY, CCACHE_*, CXX) to the
  workflow-level env.

Remove dead ccache scaffolding:
- reusable-lint-py: the job never compiles or caches; drop its ccache env.
- reusable-rtlmeter-run: remove the disabled persistent-cache step and its
  storage variables; keep CCACHE_DISABLE=1, since caching must stay off to
  keep RTLMeter timings representative.

Store test job ccache as artifacts for reuse
This commit is contained in:
Geza Lore 2026-07-06 01:44:32 +01:00 committed by GitHub
parent 8b564f509b
commit 5750c888a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 106 additions and 37 deletions

View File

@ -16,6 +16,7 @@ on:
permissions:
contents: read
actions: read
defaults:
run:

View File

@ -13,6 +13,7 @@ on:
permissions:
contents: read
actions: read
defaults:
run:

View File

@ -45,9 +45,9 @@ on:
value: ${{ jobs.build.outputs.archive }}
env:
CCACHE_COMPRESS: 1
CACHE_BASE_KEY: build-${{ inputs.runs-on }}-${{ inputs.cc }}${{ inputs.ccwarn && '-ccwarn' || '' }}${{ inputs.dev-asan && '-asan' || '' }}${{ inputs.dev-gcov && '-gcov' || '' }}
CCACHE_COMPILERCHECK: content
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_LIMIT_MULTIPLE: 0.95
defaults:
run:
@ -61,9 +61,6 @@ jobs:
runs-on: ${{ inputs.runs-on }}
outputs:
archive: ${{ steps.create-archive.outputs.archive }}
env:
CACHE_BASE_KEY: build-${{ inputs.runs-on }}-${{ inputs.cc }}
CCACHE_MAXSIZE: 1000M
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
@ -74,7 +71,13 @@ jobs:
# behind 'verilator --version'
fetch-depth: ${{ (inputs.install || inputs.dev-gcov) && '0' || '1' }}
# ccache is unreliable on the macOS runners; disable it there
- name: Disable ccache on macOS
if: ${{ startsWith(inputs.runs-on, 'macos') }}
run: echo "CCACHE_DISABLE=1" >> "$GITHUB_ENV"
- name: Cache $CCACHE_DIR
if: ${{ env.CCACHE_DISABLE != '1' }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
env:
CACHE_KEY: ${{ env.CACHE_BASE_KEY }}-ccache
@ -82,7 +85,7 @@ jobs:
path: ${{ env.CCACHE_DIR }}
key: ${{ env.CACHE_KEY }}-${{ inputs.sha }}
restore-keys: |
${{ env.CACHE_KEY }}-
${{ env.CACHE_KEY }}
- name: Install packages for build
run: ./ci/ci-install.bash build

View File

@ -7,11 +7,6 @@ name: reusable-lint-py
on:
workflow_call:
env:
CCACHE_COMPRESS: 1
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_LIMIT_MULTIPLE: 0.95
defaults:
run:
shell: bash

View File

@ -53,8 +53,8 @@ defaults:
shell: bash
env:
CCACHE_DIR: ${{ github.workspace }}/ccache
CCACHE_MAXSIZE: 512M
# RTLMeter measures compile/execute times, so caching must stay off to keep
# timings representative; ccache is still on PATH but acts as a pass-through
CCACHE_DISABLE: 1
jobs:
@ -82,14 +82,6 @@ jobs:
working-directory: rtlmeter
run: make venv
- name: Use saved ccache
if: ${{ env.CCACHE_DISABLE == 0 }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
with:
path: ${{ env.CCACHE_DIR }}
key: rtlmeter-run-ccache-${{ inputs.runs-on }}-${{ inputs.cc }}-${{ inputs.cases }}-${{ inputs.compileArgs }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: rtlmeter-run-ccache-${{ inputs.runs-on }}-${{ inputs.cc }}-${{ inputs.cases }}-${{ inputs.compileArgs }}
########################################################################
# Run with new Verilator
########################################################################

View File

@ -34,10 +34,15 @@ on:
required: true
type: string
permissions:
contents: read
actions: read
env:
CCACHE_COMPRESS: 1
CCACHE_COMPILERCHECK: content
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_LIMIT_MULTIPLE: 0.95
CXX: ${{ inputs.cc == 'clang' && 'clang++' || 'g++' }}
GH_TOKEN: ${{ github.token }}
defaults:
run:
@ -49,10 +54,6 @@ jobs:
test:
runs-on: ${{ inputs.runs-on }}
name: Test
env:
CXX: ${{ inputs.cc == 'clang' && 'clang++' || 'g++' }}
CACHE_BASE_KEY: test-${{ inputs.runs-on }}-${{ inputs.cc }}-${{ inputs.reloc }}-${{ inputs.suite }}
CCACHE_MAXSIZE: 100M # Per build per suite (* 5 * 5 = 2500M in total)
steps:
- name: Download repository archive
@ -67,15 +68,73 @@ jobs:
tar --zstd -x -f ${{ inputs.archive }}
ls -lsha
- name: Cache $CCACHE_DIR
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
# Test-job ccache is stored as an artifact, not actions/cache
# - pull_request: restore this PR's own previous run; if none, fall back to the PR's target branch.
# - branch push: restore that branch's own cache
# Artifact names are a global, unauthenticated namespace, so a PR could
# upload a "-branch-<x>" artifact impersonating a branch. Branch-scoped
# restores are therefore provenance-checked below (must come from a same-repo
# push to that branch); the pr-<N> scope is not (a PR can only affect its own
# unmerged cache).
- name: Compute ccache artifact name
# Pass github refs via env, not ${{ }} interpolation, so a branch name
# containing shell metacharacters can't break or inject into the script.
env:
CACHE_KEY: ${{ env.CACHE_BASE_KEY }}-ccache2
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ env.CACHE_KEY }}-${{ github.sha }}
restore-keys: |
${{ env.CACHE_KEY }}-
BASE_REF: ${{ github.event.pull_request.base.ref }}
REF_NAME: ${{ github.ref_name }}
run: |
base="ccache-${{ inputs.runs-on }}-${{ inputs.cc }}-${{ inputs.suite }}"
sanitize() { printf '%s' "$1" | tr '/' '_'; } # artifact names cannot contain '/'
if [ "${{ github.event_name }}" = pull_request ]; then
echo "CCACHE_ARTIFACT=${base}-pr-${{ github.event.pull_request.number }}" >> "$GITHUB_ENV"
echo "CCACHE_ARTIFACT_BRANCH=" >> "$GITHUB_ENV"
echo "CCACHE_FALLBACK=${base}-branch-$(sanitize "$BASE_REF")" >> "$GITHUB_ENV"
echo "CCACHE_FALLBACK_BRANCH=${BASE_REF}" >> "$GITHUB_ENV"
else
echo "CCACHE_ARTIFACT=${base}-branch-$(sanitize "$REF_NAME")" >> "$GITHUB_ENV"
echo "CCACHE_ARTIFACT_BRANCH=${REF_NAME}" >> "$GITHUB_ENV"
fi
- name: Restore ccache from artifact
continue-on-error: true # a cold start is fine; never fail the job here
run: |
set -euo pipefail
mkdir -p "$CCACHE_DIR"
# Return newest non-expired artifact named $1 (empty if none). When
# $2 (branch) is non-empty, require provenance: the artifact must come from
# a same-repo run (head_repository_id == repository_id) whose head branch
# is $2, so a fork PR cannot forge a branch-scoped artifact.
newest_run_id() {
want="$1" br="$2" gh api \
"/repos/$GITHUB_REPOSITORY/actions/artifacts?name=$1&per_page=100" \
--jq '.artifacts[]
| select(.expired == false and .name == env.want)
| select(env.br == ""
or (.workflow_run.head_branch == env.br
and .workflow_run.head_repository_id == .workflow_run.repository_id))
| [.created_at, (.workflow_run.id | tostring)] | @tsv' \
| sort | tail -n1 | cut -f2
}
restore() { # $1=name $2=branch(empty ok); returns 0 on a successful restore
local name="$1" br="$2" rid tarball
[ -n "$name" ] || return 1
rid="$(newest_run_id "$name" "$br")" || return 1
[ -n "$rid" ] || return 1
echo "Restoring ccache from '$name' (run $rid)"
gh run download "$rid" --name "$name" --dir "$RUNNER_TEMP/ccache-dl" || return 1
tarball="$(find "$RUNNER_TEMP/ccache-dl" -name ccache.tar.zst -print -quit)"
[ -n "$tarball" ] || return 1
tar -I zstd -x -f "$tarball" -C "$CCACHE_DIR"
}
if restore "${CCACHE_ARTIFACT:-}" "${CCACHE_ARTIFACT_BRANCH:-}"; then
echo "Restored primary ccache"
exit 0
fi
if restore "${CCACHE_FALLBACK:-}" "${CCACHE_FALLBACK_BRANCH:-}"; then
echo "Restored fallback ccache"
exit 0
fi
echo "No ccache artifact found; starting cold"
- name: Install test dependencies
run: |
@ -105,6 +164,20 @@ jobs:
path: ${{ github.workspace }}/repo/obj_coverage/verilator-${{ inputs.suite }}.info
name: code-coverage-${{ inputs.suite }}
- name: Pack ccache
if: ${{ !cancelled() && env.CCACHE_ARTIFACT != '' }}
run: tar -I 'zstd -T0' -cf "$GITHUB_WORKSPACE/ccache.tar.zst" -C "$CCACHE_DIR" .
- name: Save ccache artifact
if: ${{ !cancelled() && env.CCACHE_ARTIFACT != '' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ env.CCACHE_ARTIFACT }}
path: ${{ github.workspace }}/ccache.tar.zst
retention-days: 3
overwrite: true
compression-level: 0
- name: Fail job if a test failed
if: ${{ steps.run-test.outcome == 'failure' && !cancelled() }}
run: |-

View File

@ -78,5 +78,8 @@ autoconf
# Build
ccache -z
BUILD_START=$SECONDS
"$MAKE" -j "$NPROC" -k
ccache -svv
ccache --evict-older-than "$((SECONDS - BUILD_START + 60))s"
ccache -svv

View File

@ -26,8 +26,6 @@ case "$(uname -s)" in
HOST_OS=macOS
MAKE=make
NPROC=$(sysctl -n hw.logicalcpu)
# Disable ccache, doesn't always work in GitHub Actions
export OBJCACHE=
;;
*)
fatal "Unknown host OS: '$(uname -s)'"

View File

@ -73,6 +73,7 @@ fi
# Run the specified suite
ccache -z
TEST_START=$SECONDS
case $OPT_SUITE in
dist-vlt-0)
"$MAKE" -C "$TEST_REGRESS" SCENARIOS="--dist --vlt --driver-clean" DRIVER_HASHSET=--hashset=0/4
@ -163,4 +164,6 @@ case $OPT_SUITE in
;;
esac
ccache -svv
ccache --evict-older-than "$((SECONDS - TEST_START + 60))s"
ccache -svv
uptime # To see load average