From cb17fcce145dbb91b57d415dcee7d6b5de5d9758 Mon Sep 17 00:00:00 2001 From: madomado Date: Sat, 29 Nov 2025 22:06:32 +0800 Subject: [PATCH] ci: add sccache [part 1] (#7704) (#7706) --- .github/scripts/configure-sccache.js | 158 +++++++++++++++++++++++++++ .github/scripts/sccache-stats.js | 109 ++++++++++++++++++ .github/workflows/json-build.yml | 16 +++ 3 files changed, 283 insertions(+) create mode 100644 .github/scripts/configure-sccache.js create mode 100644 .github/scripts/sccache-stats.js diff --git a/.github/scripts/configure-sccache.js b/.github/scripts/configure-sccache.js new file mode 100644 index 0000000000..e5c21ccd30 --- /dev/null +++ b/.github/scripts/configure-sccache.js @@ -0,0 +1,158 @@ +// Configure sccache environment variables for GitHub Actions cache integration +// +// This script is still unused until we build terra-sccache with this supported, +// Turns out that Fedora's sccache build has the GHA feature support disabled. +// +// Note: ACTIONS_CACHE_SERVICE_V2 and SCCACHE_GHA_ENABLED are set at workflow level +module.exports = async ({ github, context, core, exec }) => { + // Find sccache path (try which command) + let sccachePath = "sccache"; + try { + const result = await exec.getExecOutput("which", ["sccache"], { + ignoreReturnCode: true, + silent: true, + }); + if (result.exitCode === 0 && result.stdout.trim()) { + sccachePath = result.stdout.trim(); + core.info(`Found sccache at: ${sccachePath}`); + } + } catch (e) { + core.debug(`Could not find sccache path: ${e.message}`); + } + // Check sccache version + try { + const versionResult = await exec.getExecOutput(sccachePath, ["--version"], { + ignoreReturnCode: true, + silent: true, + }); + core.info(`sccache version: ${versionResult.stdout.trim()}`); + } catch (e) { + core.warning(`Could not get sccache version: ${e.message}`); + } + // Debug: Show what environment variables are available + core.info("=== Environment Variables Diagnostic ==="); + core.info(`SCCACHE_GHA_ENABLED: ${process.env.SCCACHE_GHA_ENABLED}`); + core.info( + `ACTIONS_CACHE_SERVICE_V2: ${process.env.ACTIONS_CACHE_SERVICE_V2}`, + ); + core.info( + `ACTIONS_RESULTS_URL: ${process.env.ACTIONS_RESULTS_URL ? "SET (length: " + process.env.ACTIONS_RESULTS_URL.length + ")" : "NOT SET"}`, + ); + core.info( + `ACTIONS_RUNTIME_TOKEN: ${process.env.ACTIONS_RUNTIME_TOKEN ? "SET (length: " + process.env.ACTIONS_RUNTIME_TOKEN.length + ")" : "NOT SET"}`, + ); + core.info(`RUSTC_WRAPPER: ${process.env.RUSTC_WRAPPER}`); + core.info(`SCCACHE_LOG: ${process.env.SCCACHE_LOG}`); + core.info("========================================"); + // Export SCCACHE_PATH so it's available to subsequent steps + core.exportVariable("SCCACHE_PATH", sccachePath); + // Expose the GHA cache related variables to make it easier for users to + // integrate with GHA support (from upstream mozilla/sccache-action) + if (process.env.ACTIONS_RESULTS_URL) { + core.exportVariable("ACTIONS_RESULTS_URL", process.env.ACTIONS_RESULTS_URL); + core.info("✓ Exported ACTIONS_RESULTS_URL"); + } else { + core.error( + "ACTIONS_RESULTS_URL is not set - GitHub Actions cache WILL NOT work", + ); + } + if (process.env.ACTIONS_RUNTIME_TOKEN) { + core.exportVariable( + "ACTIONS_RUNTIME_TOKEN", + process.env.ACTIONS_RUNTIME_TOKEN, + ); + core.info("✓ Exported ACTIONS_RUNTIME_TOKEN"); + } else { + core.error( + "ACTIONS_RUNTIME_TOKEN is not set - GitHub Actions cache WILL NOT work", + ); + } + // Set cache version and restore keys for this specific build matrix + if (process.env.SCCACHE_GHA_VERSION) { + core.exportVariable("SCCACHE_GHA_VERSION", process.env.SCCACHE_GHA_VERSION); + } + if (process.env.SCCACHE_GHA_CACHE_FROM) { + core.exportVariable( + "SCCACHE_GHA_CACHE_FROM", + process.env.SCCACHE_GHA_CACHE_FROM, + ); + } + // Check if cache busting is enabled + const inputs = + (github && + github.context && + github.context.payload && + github.context.payload.inputs) || + {}; + const rawBustCache = + inputs.bust_cache ?? + inputs.bustCache ?? + process.env.INPUT_BUST_CACHE ?? + process.env.BUST_CACHE; + let bustCache = false; + if (typeof rawBustCache === "string") { + const v = rawBustCache.toLowerCase().trim(); + bustCache = v === "true" || v === "1" || v === "yes"; + } else { + bustCache = !!rawBustCache; + } + if (bustCache) { + core.exportVariable("SCCACHE_RECACHE", "1"); + core.info("SCCACHE_RECACHE enabled because bust_cache is true"); + } + // Stop any running sccache daemon so it picks up the new environment variables + core.info("Stopping any running sccache daemon to pick up configuration..."); + try { + await exec.exec(sccachePath, ["--stop-server"], { + ignoreReturnCode: true, + }); + core.info("✓ sccache daemon stopped successfully"); + } catch (e) { + core.debug( + `Could not stop sccache daemon (it may not be running): ${e.message}`, + ); + } + // Verify sccache can see the GHA environment variables by starting server with explicit env + core.info("Starting sccache server with GHA environment variables..."); + const sccacheEnv = { + ...process.env, + SCCACHE_GHA_ENABLED: process.env.SCCACHE_GHA_ENABLED || "on", + ACTIONS_CACHE_SERVICE_V2: process.env.ACTIONS_CACHE_SERVICE_V2 || "on", + }; + try { + await exec.exec(sccachePath, ["--start-server"], { + ignoreReturnCode: true, + env: sccacheEnv, + }); + core.info("✓ sccache server started"); + } catch (e) { + core.warning(`Could not start sccache server: ${e.message}`); + } + // Show the current sccache configuration + core.info("Verifying sccache configuration:"); + try { + const statsResult = await exec.getExecOutput( + sccachePath, + ["--show-stats"], + { + ignoreReturnCode: true, + env: sccacheEnv, + }, + ); + // Check if it's using GitHub Actions cache + if (statsResult.stdout.includes("GitHub Actions")) { + core.info("✓ sccache is configured to use GitHub Actions cache"); + } else if (statsResult.stdout.includes("Local disk")) { + core.error( + "✗ sccache is using Local disk cache instead of GitHub Actions cache!", + ); + core.error( + "This means SCCACHE_GHA_ENABLED or required env vars are not being recognized.", + ); + core.info("Stats output:"); + core.info(statsResult.stdout); + } + } catch (e) { + core.debug(`Could not show sccache stats: ${e.message}`); + } +}; diff --git a/.github/scripts/sccache-stats.js b/.github/scripts/sccache-stats.js new file mode 100644 index 0000000000..f06621e423 --- /dev/null +++ b/.github/scripts/sccache-stats.js @@ -0,0 +1,109 @@ +module.exports = async ({ github, context, core, exec }) => { + if (!exec) { + throw new Error("exec parameter is required but was not provided"); + } + // Use SCCACHE_PATH if set, otherwise default to 'sccache' (will use PATH) + const sccachePath = process.env.SCCACHE_PATH || "sccache"; + core.debug(`Using sccache path: ${sccachePath}`); + const percentage = (x, y) => Math.round((x / y) * 100 || 0); + const plural = (count, base, pluralForm = base + "s") => + `${count} ${count === 1 ? base : pluralForm}`; + const sumStats = (stats) => + Object.values(stats.counts).reduce((acc, val) => acc + val, 0); + const formatDuration = (duration) => { + const ms = duration.nanos / 1e6; + return `${duration.secs}s ${ms}ms`; + }; + const formatJsonStats = (stats) => { + const cacheErrorCount = sumStats(stats.stats.cache_errors); + const cacheHitCount = sumStats(stats.stats.cache_hits); + const cacheMissCount = sumStats(stats.stats.cache_misses); + const totalHits = cacheHitCount + cacheMissCount + cacheErrorCount; + const ratio = percentage(cacheHitCount, totalHits); + const writeDuration = formatDuration(stats.stats.cache_write_duration); + const readDuration = formatDuration(stats.stats.cache_read_hit_duration); + const compilerDuration = formatDuration( + stats.stats.compiler_write_duration, + ); + const noticeHit = plural(cacheHitCount, "hit"); + const noticeMiss = plural(cacheMissCount, "miss", "misses"); + const noticeError = plural(cacheErrorCount, "error"); + const notice = `${ratio}% - ${noticeHit}, ${noticeMiss}, ${noticeError}`; + const table = [ + [{ data: "Cache hit %", header: true }, { data: `${ratio}%` }], + [ + { data: "Cache hits", header: true }, + { data: cacheHitCount.toString() }, + ], + [ + { data: "Cache misses", header: true }, + { data: cacheMissCount.toString() }, + ], + [ + { data: "Cache errors", header: true }, + { data: cacheErrorCount.toString() }, + ], + [ + { data: "Compile requests", header: true }, + { data: stats.stats.compile_requests.toString() }, + ], + [ + { data: "Requests executed", header: true }, + { data: stats.stats.requests_executed.toString() }, + ], + [ + { data: "Cache writes", header: true }, + { data: stats.stats.cache_writes.toString() }, + ], + [ + { data: "Cache write errors", header: true }, + { data: stats.stats.cache_write_errors.toString() }, + ], + [{ data: "Cache write duration", header: true }, { data: writeDuration }], + [ + { data: "Cache read hit duration", header: true }, + { data: readDuration }, + ], + [ + { data: "Compiler write duration", header: true }, + { data: compilerDuration }, + ], + ]; + return { table, notice }; + }; + const getOutput = async (command, args = []) => { + core.debug(`get_output: ${command} ${args.join(" ")}`); + const output = await exec.getExecOutput(command, args, { + ignoreReturnCode: false, + silent: false, + }); + if (!output.stdout.endsWith("\n")) { + process.stdout.write("\n"); + } + return output.stdout.toString(); + }; + const humanStats = await core.group("Get human-readable stats", async () => { + return getOutput(sccachePath, ["--show-stats"]); + }); + const jsonStats = await core.group("Get JSON stats", async () => { + return getOutput(sccachePath, ["--show-stats", "--stats-format=json"]); + }); + const stats = JSON.parse(jsonStats); + const formattedStats = formatJsonStats(stats); + core.notice(formattedStats.notice, { + title: `sccache stats - ${context.job}`, + }); + core.info("\nFull human-readable stats:"); + core.info(humanStats); + core.summary.addHeading("sccache stats", 2); + core.summary.addTable(formattedStats.table); + core.summary.addDetails( + "Full human-readable stats", + "\n\n```\n" + humanStats + "\n```\n\n", + ); + core.summary.addDetails( + "Full JSON Stats", + "\n\n```json\n" + JSON.stringify(stats, null, 2) + "\n```\n\n", + ); + await core.summary.write(); +}; diff --git a/.github/workflows/json-build.yml b/.github/workflows/json-build.yml index 02543c990c..af951f7b84 100644 --- a/.github/workflows/json-build.yml +++ b/.github/workflows/json-build.yml @@ -18,6 +18,11 @@ on: required: false type: string default: "" + bust_cache: + description: "Whether to bust the cache" + required: false + type: boolean + default: false workflow_dispatch: inputs: packages: @@ -30,6 +35,14 @@ on: type: boolean default: true +env: + RUSTC_WRAPPER: sccache + # SCCACHE_NO_DAEMON: "1" + # Disable incremental compilation so sccache works better + CARGO_INCREMENTAL: "false" + SCCACHE_GHA_ENABLED: "true" + SCCACHE_RECACHE: "${{ inputs.bust_cache == 'true' && 'true' || '' }}" + jobs: build: strategy: @@ -64,6 +77,9 @@ jobs: dir=$(dirname ${{ matrix.pkg.pkg }}) dnf5 builddep -y ${dir}/*.spec + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.9 + - name: Build with Andaman run: anda build -D "vendor Terra" ${{ matrix.pkg.pkg }} -c terra-${{ matrix.version }}-${{ matrix.pkg.arch }} ${{ !matrix.pkg.labels.mock == '1' && '-rrpmbuild' || '' }}