using ccache in tests workflow (#40369)
* Add ccache support for improved build performance
- Introduced ccache setup in GitHub Actions workflows to cache compilation results, reducing build times.
- Updated `build_python.sh` to include an option for enabling ccache, modifying the compiler commands accordingly.
- Added environment variable configurations for ccache in multiple build jobs to ensure consistent usage across platforms.
- Enhanced the `BUILD.gn` file to declare a new argument for the C/C++ compiler wrapper.
- Included ccache statistics reporting in the CI workflow for performance monitoring.
* adjust step name
* Add setup-ccache action
* cleanup
* reformat
* shfmt
* rollback some script changes
* fix indentation
* export CACHE_KEY from action
* update cache strategy
* testing
* testing
* restore conditions after testing
* Add options to disable ccache and specify cache suffix in setup-ccache action
- Introduced 'disable' input to skip cache restore and set CCACHE_DISABLE=1.
- Added 'cache-suffix' input for optional suffix to cache key for rotation.
- Updated tests workflow to include inputs for disabling ccache and specifying cache suffix.
- Adjusted cache restore and save conditions based on new inputs.
* update docs
* Revert Darwin jobs to match master; keep ccache improvements only in Linux REPL job.
* lint markdown
* commit copilot suggestion
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
diff --git a/.github/actions/setup-ccache/action.yml b/.github/actions/setup-ccache/action.yml
new file mode 100644
index 0000000..df327e8
--- /dev/null
+++ b/.github/actions/setup-ccache/action.yml
@@ -0,0 +1,67 @@
+name: 'Setup ccache'
+description: 'Configures and restores the ccache cache'
+inputs:
+  build-variant:
+    required: true
+    description: 'The build variant for cache key uniqueness'
+  disable:
+    required: false
+    default: 'false'
+    description: 'If true, skip cache restore and set CCACHE_DISABLE=1 to bypass ccache.'
+  cache-suffix:
+    required: false
+    default: ''
+    description: 'Optional suffix appended to the cache key to force rotation (e.g., a short hash).'
+outputs:
+  cache-primary-key:
+    description: "The primary key used for the cache"
+    value: ${{ steps.cache-vars.outputs.cache_key }}
+  cache-hit:
+    description: "A boolean value to indicate an exact match was found for the primary key"
+    value: ${{ steps.ccache-restore.outputs.cache-hit }}
+runs:
+  using: "composite"
+  steps:
+    - name: Configure ccache
+      shell: bash
+      run: |
+        mkdir -p .ccache
+        echo "CCACHE_BASEDIR=${{ github.workspace }}" >> $GITHUB_ENV
+        echo "CCACHE_SLOPPINESS=time_macros" >> $GITHUB_ENV
+        echo "CCACHE_DIR=${{ github.workspace }}/.ccache" >> $GITHUB_ENV
+        echo "CCACHE_COMPILERCHECK=content" >> $GITHUB_ENV
+        echo "CCACHE_MAXSIZE=5G" >> $GITHUB_ENV
+        echo "CCACHE_COMPRESS=1" >> $GITHUB_ENV
+        echo "CCACHE_COMPRESSLEVEL=6" >> $GITHUB_ENV
+    - name: Optionally disable ccache
+      if: ${{ inputs.disable == 'true' }}
+      shell: bash
+      run: |
+        echo "CCACHE_DISABLE=1" >> $GITHUB_ENV
+    - name: Prepare cache variables
+      id: cache-vars
+      shell: bash
+      run: |
+        CACHE_WEEK=$(date +%G-%V)
+        echo "CACHE_WEEK=${CACHE_WEEK}" >> "$GITHUB_ENV"
+
+        CACHE_SUFFIX="${{ inputs.cache-suffix }}"
+        if [ -n "$CACHE_SUFFIX" ]; then
+          CCACHE_KEY="ccache-${{ runner.os }}-${{ inputs.build-variant }}-w${CACHE_WEEK}-${CACHE_SUFFIX}"
+        else
+          CCACHE_KEY="ccache-${{ runner.os }}-${{ inputs.build-variant }}-w${CACHE_WEEK}"
+        fi
+        echo "CCACHE_KEY=${CCACHE_KEY}" >> "$GITHUB_ENV"
+        echo "cache_key=${CCACHE_KEY}" >> "$GITHUB_OUTPUT"
+
+        CCACHE_RESTORE_KEY="ccache-${{ runner.os }}-${{ inputs.build-variant }}-"
+        echo "CCACHE_RESTORE_KEY=${CCACHE_RESTORE_KEY}" >> "$GITHUB_ENV"
+    - name: Restore ccache
+      if: ${{ inputs.disable != 'true' }}
+      id: ccache-restore
+      uses: actions/cache/restore@v4
+      with:
+        path: .ccache
+        key: ${{ env.CCACHE_KEY }}
+        restore-keys: |
+          ${{ env.CCACHE_RESTORE_KEY }}
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index f2ea1fa..ecb2a04 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -22,6 +22,18 @@
     pull_request:
     merge_group:
     workflow_dispatch:
+        inputs:
+            disable_ccache:
+                description: 'Disable ccache restore and builds (sets CCACHE_DISABLE=1)'
+                required: false
+                default: 'false'
+                type: choice
+                options: ['false', 'true']
+            cache_suffix:
+                description: 'Optional suffix appended to ccache key to rotate (e.g. short hash)'
+                required: false
+                default: ''
+                type: string
 
 concurrency:
     group: ${{ github.ref }}-${{ github.workflow }}-${{ (github.event_name == 'pull_request' && github.event.number) || (github.event_name == 'workflow_dispatch' && github.run_number) || github.sha }}
@@ -43,6 +55,8 @@
             CHIP_TOOL_VARIANT: ${{matrix.chip_tool}}
             TSAN_OPTIONS: "halt_on_error=1 suppressions=scripts/tests/chiptest/tsan-linux-suppressions.txt"
             LSAN_OPTIONS: detect_leaks=1
+            DISABLE_CCACHE: ${{ (github.event_name == 'workflow_dispatch' && inputs.disable_ccache == 'true') && 'true' || (contains(github.event.head_commit.message, '[no-ccache]') && 'true') || 'false' }}
+            CCACHE_KEY_SUFFIX: ${{ github.event_name == 'workflow_dispatch' && inputs.cache_suffix || '' }}
 
         if: github.actor != 'restyled-io[bot]'
         runs-on: ubuntu-latest
@@ -80,6 +94,14 @@
             - name: Set up a IPV6 known environment
               uses: ./.github/actions/add-ipv6
 
+            - name: Setup and Restore Cache
+              id: ccache
+              uses: ./.github/actions/setup-ccache
+              with:
+                build-variant: ${{ matrix.build_variant }}
+                disable: ${{ env.DISABLE_CCACHE }}
+                cache-suffix: ${{ env.CCACHE_KEY_SUFFIX }}
+
             - name: Validate that xml are parsable
               # The sub-items being run here are the same as the input XMLs listed
               # at src/app/zap-templates/zcl/zcl.json
@@ -233,11 +255,16 @@
                       src/app/zap-templates/zcl/data-model/chip/zone-management-cluster.xml \
                   "
             - name: Build Apps
+              id: build_with_cache
+              continue-on-error: true
+              env:
+                  CCACHE_DIR: "${{ github.workspace }}/.ccache"
               run: |
                   ./scripts/run_in_build_env.sh \
                      "./scripts/build_python.sh \
                         --install_virtual_env out/venv \
                         --include_pytest_deps yes \
+                        $([ "${DISABLE_CCACHE}" != "true" ] && echo --enable-ccache) \
                      "
                   ./scripts/run_in_build_env.sh \
                      "./scripts/build/build_examples.py \
@@ -254,9 +281,43 @@
                         --target linux-x64-network-manager-${BUILD_VARIANT} \
                         --target linux-x64-energy-gateway-${BUILD_VARIANT} \
                         --target linux-x64-energy-management-${BUILD_VARIANT} \
+                        --pw-command-launcher=ccache \
                         build \
                         --copy-artifacts-to objdir-clone \
                      "
+            - name: Retry build without cache on master
+              if: ${{ steps.build_with_cache.outcome == 'failure' && github.ref == 'refs/heads/master' }}
+              env:
+                  CCACHE_DIR: "${{ github.workspace }}/.ccache"
+              run: |
+                  echo "CCACHE_DISABLE=1" >> $GITHUB_ENV
+                  rm -rf "${{ github.workspace }}/.ccache" || true
+                  ./scripts/run_in_build_env.sh \
+                     "./scripts/build_python.sh \
+                        --install_virtual_env out/venv \
+                        --include_pytest_deps yes \
+                     "
+                  ./scripts/run_in_build_env.sh \
+                     "./scripts/build/build_examples.py \
+                        --target linux-x64-chip-tool${CHIP_TOOL_VARIANT}-${BUILD_VARIANT} \
+                        --target linux-x64-all-clusters-${BUILD_VARIANT} \
+                        --target linux-x64-lock-${BUILD_VARIANT} \
+                        --target linux-x64-ota-provider-${BUILD_VARIANT} \
+                        --target linux-x64-ota-requestor-${BUILD_VARIANT} \
+                        --target linux-x64-tv-app-${BUILD_VARIANT} \
+                        --target linux-x64-bridge-${BUILD_VARIANT} \
+                        --target linux-x64-lit-icd-${BUILD_VARIANT} \
+                        --target linux-x64-microwave-oven-${BUILD_VARIIANT} \
+                        --target linux-x64-rvc-${BUILD_VARIANT} \
+                        --target linux-x64-network-manager-${BUILD_VARIANT} \
+                        --target linux-x64-energy-gateway-${BUILD_VARIIANT} \
+                        --target linux-x64-energy-management-${BUILD_VARIANT} \
+                        build \
+                        --copy-artifacts-to objdir-clone \
+                     "
+
+            - name: ccache stats
+              run: ccache -s
 
             - name: Run Tests using the python parser sending commands to chip-tool
               run: |
@@ -362,6 +423,12 @@
                   path: objdir-clone/
                   # objdirs are big; don't hold on to them too long.
                   retention-days: 5
+            - name: Save ccache
+              if: github.ref == 'refs/heads/master' && steps.ccache.outputs.cache-hit != 'true' && env.DISABLE_CCACHE != 'true' && env.CCACHE_DISABLE != '1'
+              uses: actions/cache/save@v4
+              with:
+                  path: .ccache
+                  key: ${{ env.CCACHE_KEY }}
 
     test_suites_darwin:
         name: Test Suites - Darwin
@@ -521,161 +588,209 @@
             - name: Set up a IPV6 known envionment
               uses: ./.github/actions/add-ipv6
 
+            - name: Setup and Restore Cache
+              id: ccache
+              uses: ./.github/actions/setup-ccache
+              with:
+                build-variant: ${{ matrix.build_variant }}
+
             - name: Build python env
-              run: scripts/run_in_build_env.sh './scripts/build_python.sh --install_virtual_env out/venv'
+              run: scripts/run_in_build_env.sh './scripts/build_python.sh --install_virtual_env out/venv --enable-ccache'
 
             - name: Build linux-x64-all-clusters
+              env:
+                  CCACHE_DIR: "${{ github.workspace }}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-all-clusters-${BUILD_VARIANT}-tsan-clang-test
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-all-clusters-${BUILD_VARIANT}-tsan-clang-test
                   "
             # TODO: determine why some apps are tsan and some are not (and does that add runtime overhead? )
             - name: Build linux-x64-bridge
+              env:
+                  CCACHE_DIR: "${{ github.workspace }}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-bridge-${BUILD_VARIANT}-tsan-clang-test
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-bridge-${BUILD_VARIANT}-tsan-clang-test"
 
             - name: Build linux-x64-lock
+              env:
+                  CCACHE_DIR: "${{ github.workspace }}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-lock-${BUILD_VARIANT}-tsan-clang-test
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-lock-${BUILD_VARIANT}-tsan-clang-test"
 
             - name: Build linux-x64-lit-icd
+              env:
+                  CCACHE_DIR: "${{ github.workspace }}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-lit-icd-${BUILD_VARIANT}-tsan-clang-test
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-lit-icd-${BUILD_VARIANT}-tsan-clang-test"
 
             - name: Build linux-x64-air-purifier
+              env:
+                  CCACHE_DIR: "${{ github.workspace }}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-air-purifier-${BUILD_VARIANT}-tsan-clang-test
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-air-purifier-${BUILD_VARIANT}-tsan-clang-test"
 
             - name: Build linux-x64-energy-gateway
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-energy-gateway-${BUILD_VARIANT}-tsan-clang-test
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-energy-gateway-${BUILD_VARIANT}-tsan-clang-test"
 
             - name: Build linux-x64-energy-management
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-energy-management-${BUILD_VARIANT}-tsan-clang-test
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-energy-management-${BUILD_VARIANT}-tsan-clang-test"
 
             - name: Build linux-x64-microwave-oven
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-microwave-oven-${BUILD_VARIANT}-tsan-clang-test
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-microwave-oven-${BUILD_VARIANT}-tsan-clang-test"
 
             - name: Build linux-x64-rvc
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-rvc-${BUILD_VARIANT}-tsan-clang-test
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-rvc-${BUILD_VARIANT}-tsan-clang-test"
 
             - name: Build linux-x64-network-manager
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-network-manager-${BUILD_VARIANT}-tsan-clang-test
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-network-manager-${BUILD_VARIANT}-tsan-clang-test"
 
             - name: Build linux-x64-fabric-admin-rpc
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-fabric-admin-rpc-${BUILD_VARIANT}-clang
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-fabric-admin-rpc-${BUILD_VARIANT}-clang"
 
             - name: Build linux-x64-fabric-bridge-rpc
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-fabric-bridge-rpc-${BUILD_VARIANT}-clang
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-fabric-bridge-rpc-${BUILD_VARIANT}-clang"
 
             - name: Build linux-x64-fabric-sync
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-fabric-sync-${BUILD_VARIANT}-clang
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-fabric-sync-${BUILD_VARIANT}-clang"
 
             # TODO: camera app needs clang support
             - name: Build linux-x64-camera
+              env:
+                  CCACHE_DIR: "${{ github.workspace }}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-camera
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-camera"
 
             - name: Build linux-x64-camera-controller
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-camera-controller
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-camera-controller"
 
             - name: Build linux-x64-light-data-model-no-unique-id
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-light-data-model-no-unique-id-${BUILD_VARIANT}-clang
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-light-data-model-no-unique-id-${BUILD_VARIANT}-clang"
 
-            # TODO: T&C app needs variants (probably)
             - name: Build linux-x64-terms-and-conditions
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-terms-and-conditions
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-terms-and-conditions"
 
             - name: Build linux-x64-python-bindings
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-python-bindings-webrtc
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-python-bindings-webrtc"
 
-            # TODO: JS admin app app needs variants (probably)
             - name: Build linux-x64-jf-control-app
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-jf-control-app
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-jf-control-app"
 
-            # TODO: JF admin app app needs variants (probably)
             - name: Build linux-x64-jf-admin-app
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-jf-admin-app
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-jf-admin-app"
 
             - name: Build linux-x64-closure-app
+              env:
+                  CCACHE_DIR: "${GITHUB_WORKSPACE}/.ccache"
               run: >-
                   ./scripts/run_in_build_env.sh "./scripts/build/build_examples.py
                   --target linux-x64-closure-${BUILD_VARIANT}-tsan-clang-test
-                  build --copy-artifacts-to objdir-clone
+                  --pw-command-launcher=ccache build --copy-artifacts-to objdir-clone
                   && rm -rf out/linux-x64-closure-${BUILD_VARIANT}-tsan-clang-test"
 
+            - name: ccache stats
+              run: ccache -s
+
             - name: Install push_av_server dependencies
               run: >-
                   ./scripts/run_in_python_env.sh out/venv \
@@ -770,6 +885,12 @@
                   path: objdir-clone/
                   # objdirs are big; don't hold on to them too long.
                   retention-days: 5
+            - name: Save ccache
+              if: github.ref == 'refs/heads/master' && steps.ccache.outputs.cache-hit != 'true'
+              uses: actions/cache/save@v4
+              with:
+                  path: .ccache
+                  key: ${{ env.CCACHE_KEY }}
 
     repl_tests_darwin:
         name: REPL Tests - Darwin (Build Only)
diff --git a/BUILD.gn b/BUILD.gn
index 507d5af..c3583e4 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -24,6 +24,11 @@
 import("$dir_pw_build/python_dist.gni")
 import("$dir_pw_build/python_venv.gni")
 
+declare_args() {
+  # Value to passed to the c/c++ compiler wrapper.
+  cc_wrapper = ""
+}
+
 # This build file should not be used in superproject builds.
 assert(chip_root == "//")
 
diff --git a/docs/tips_and_troubleshooting/ci-ccache.md b/docs/tips_and_troubleshooting/ci-ccache.md
new file mode 100644
index 0000000..58be67f
--- /dev/null
+++ b/docs/tips_and_troubleshooting/ci-ccache.md
@@ -0,0 +1,36 @@
+### ccache in CI: troubleshooting and controls
+
+This document applies to GitHub Actions CI only.
+
+The CI uses ccache to accelerate C/C++ builds. The cache is stored via GitHub
+Actions Cache with weekly rotation, and ccache validates every object retrieval
+for correctness.
+
+Common tasks (CI)
+
+-   Disable ccache for a run:
+    -   Add `[no-ccache]` in the commit message, or
+    -   Manually dispatch the `Tests` workflow with `disable_ccache=true`.
+-   Force cache rotation for a run:
+    -   Manually dispatch `Tests` with `cache_suffix` set to any string (e.g., a
+        short hash). This appends to the cache key so a new cache archive is
+        created.
+
+When master builds fail
+
+-   The Linux job automatically retries without ccache if the initial build
+    fails on `master`.
+
+Debugging tips (CI)
+
+-   Print stats: use the existing `ccache -s` step to inspect hit/miss rates.
+-   Compiler/version changes: protected by `CCACHE_COMPILERCHECK=content`, so
+    caches are automatically missed on compiler changes.
+-   Time macros: CI uses `CCACHE_SLOPPINESS=time_macros`. If your build embeds
+    `__DATE__`/`__TIME__`, be aware objects may be reused across runs for these
+    macros.
+
+Notes
+
+-   External cache keys rotate weekly; ccache still validates per-object for
+    correctness.
diff --git a/scripts/build_python.sh b/scripts/build_python.sh
index 9fb5083..5cee556 100755
--- a/scripts/build_python.sh
+++ b/scripts/build_python.sh
@@ -51,6 +51,7 @@
 declare -a extra_gn_args
 declare chip_build_controller_dynamic_server=true
 declare enable_pw_rpc=false
+declare enable_ccache=no
 declare enable_webrtc=true
 
 help() {
@@ -86,6 +87,7 @@
   -ds, --chip_build_controller_dynamic_server <true/false>  Enable dynamic server in controller.
                                                             Defaults to $chip_build_controller_dynamic_server.
   -pw  --enable_pw_rpc <true/false>                         Build Pw Python wheels. Defaults to $enable_pw_rpc.
+  --enable-ccache                                           Use ccache for building python wheels. Defaults to $enable_ccache.
 "
 }
 
@@ -193,6 +195,9 @@
             fi
             shift
             ;;
+        --enable-ccache)
+            enable_ccache=yes
+            ;;
         -*)
             help
             echo "Unknown Option \"$1\""
@@ -216,6 +221,7 @@
 echo "  chip_build_controller_dynamic_server=\"$chip_build_controller_dynamic_server\""
 echo "  chip_support_webrtc_python_bindings=\"$enable_webrtc\""
 echo "  enable_pw_rpc=\"$enable_pw_rpc\""
+echo "  enable_ccache=\"$enable_ccache\""
 
 if [[ ${#extra_gn_args[@]} -gt 0 ]]; then
     echo "In addition, the following extra args will added to gn command line: ${extra_gn_args[*]}"
@@ -270,6 +276,10 @@
     "chip_support_webrtc_python_bindings=$enable_webrtc"
     "chip_device_config_enable_joint_fabric=true"
 )
+# Add ccache support through pw_command_launcher when enabled
+if [[ "$enable_ccache" == "yes" ]]; then
+    gn_args+=("pw_command_launcher=\"ccache\"")
+fi
 if [[ -n "$chip_mdns" ]]; then
     gn_args+=("chip_mdns=\"$chip_mdns\"")
 fi
@@ -280,13 +290,24 @@
     gn_args+=("chip_code_pre_generated_directory=\"$pregen_dir\"")
 fi
 if [[ -n $wifi_paf_config ]]; then
-    args+=("$wifi_paf_config")
+    gn_args+=("$wifi_paf_config")
 fi
 # Append extra arguments provided by the user.
 gn_args+=("${extra_gn_args[@]}")
 
 gn --root="$CHIP_ROOT" gen "$OUTPUT_ROOT" --args="${gn_args[*]}"
 
+# Set up ccache environment for compilation
+if [[ "$enable_ccache" == "yes" ]]; then
+    # Only wrap if not already wrapped with ccache
+    if [[ -n "$CC" ]] && [[ "$CC" != ccache* ]]; then
+        export CC="ccache $CC"
+    fi
+    if [[ -n "$CXX" ]] && [[ "$CXX" != ccache* ]]; then
+        export CXX="ccache $CXX"
+    fi
+fi
+
 # Compile Python wheels
 ninja -C "$OUTPUT_ROOT" python_wheels