diff --git a/.github/actions/build-gazebo-sitl/action.yml b/.github/actions/build-gazebo-sitl/action.yml new file mode 100644 index 0000000000..aae5a9565a --- /dev/null +++ b/.github/actions/build-gazebo-sitl/action.yml @@ -0,0 +1,21 @@ +name: Build Gazebo Classic SITL +description: Build PX4 firmware and Gazebo Classic plugins with ccache stats + +runs: + using: composite + steps: + - name: Build - PX4 Firmware (SITL) + shell: bash + run: make px4_sitl_default + + - name: Cache - Stats after PX4 Firmware + shell: bash + run: ccache -s + + - name: Build - Gazebo Classic Plugins + shell: bash + run: make px4_sitl_default sitl_gazebo-classic + + - name: Cache - Stats after Gazebo Plugins + shell: bash + run: ccache -s diff --git a/.github/actions/save-ccache/action.yml b/.github/actions/save-ccache/action.yml new file mode 100644 index 0000000000..c4db6b8b18 --- /dev/null +++ b/.github/actions/save-ccache/action.yml @@ -0,0 +1,22 @@ +name: Save ccache +description: Print ccache stats and save to cache + +inputs: + cache-primary-key: + description: Primary cache key from setup-ccache output + required: true + +runs: + using: composite + steps: + - name: Cache - Stats + if: always() + shell: bash + run: ccache -s + + - name: Cache - Save ccache + if: always() + uses: actions/cache/save@v4 + with: + path: ~/.ccache + key: ${{ inputs.cache-primary-key }} diff --git a/.github/actions/setup-ccache/action.yml b/.github/actions/setup-ccache/action.yml new file mode 100644 index 0000000000..f542c50d97 --- /dev/null +++ b/.github/actions/setup-ccache/action.yml @@ -0,0 +1,56 @@ +name: Setup ccache +description: Restore ccache from cache and configure ccache.conf + +inputs: + cache-key-prefix: + description: Cache key prefix (e.g. ccache-sitl) + required: true + max-size: + description: Max ccache size (e.g. 300M) + required: false + default: '300M' + base-dir: + description: ccache base_dir value + required: false + default: '${GITHUB_WORKSPACE}' + install-ccache: + description: Install ccache via apt before configuring + required: false + default: 'false' + +outputs: + cache-primary-key: + description: Primary cache key (pass to save-ccache) + value: ${{ steps.restore.outputs.cache-primary-key }} + +runs: + using: composite + steps: + - name: Cache - Install ccache + if: inputs.install-ccache == 'true' + shell: bash + run: apt-get update && apt-get install -y ccache + + - name: Cache - Restore ccache + id: restore + uses: actions/cache/restore@v4 + with: + path: ~/.ccache + key: ${{ inputs.cache-key-prefix }}-${{ github.ref_name }}-${{ github.sha }} + restore-keys: | + ${{ inputs.cache-key-prefix }}-${{ github.ref_name }}- + ${{ inputs.cache-key-prefix }}-${{ github.base_ref || 'main' }}- + ${{ inputs.cache-key-prefix }}- + + - name: Cache - Configure ccache + shell: bash + run: | + mkdir -p ~/.ccache + echo "base_dir = ${{ inputs.base-dir }}" > ~/.ccache/ccache.conf + echo "compression = true" >> ~/.ccache/ccache.conf + echo "compression_level = 6" >> ~/.ccache/ccache.conf + echo "max_size = ${{ inputs.max-size }}" >> ~/.ccache/ccache.conf + echo "hash_dir = false" >> ~/.ccache/ccache.conf + echo "compiler_check = content" >> ~/.ccache/ccache.conf + ccache -s + ccache -z diff --git a/Tools/ci/run-clang-tidy-pr.py b/Tools/ci/run-clang-tidy-pr.py new file mode 100755 index 0000000000..226bdae2e3 --- /dev/null +++ b/Tools/ci/run-clang-tidy-pr.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +Run clang-tidy incrementally on files changed in a PR. + +Usage: run-clang-tidy-pr.py + base-ref: e.g. origin/main + +Computes the set of translation units (TUs) affected by the PR diff, +then invokes Tools/run-clang-tidy.py on that subset only. +Exits 0 silently when no C++ files were changed. +""" + +import argparse +import json +import os +import subprocess +import sys + +EXTENSIONS_CPP = {'.cpp', '.c'} +EXTENSIONS_HDR = {'.hpp', '.h'} +# Manual exclusions from Makefile:508 +EXCLUDE_EXTRA = '|'.join([ + 'src/systemcmds/tests', + 'src/examples', + 'src/modules/gyro_fft/CMSIS_5', + 'src/lib/drivers/smbus', + 'src/drivers/gpio', + r'src/modules/commander/failsafe/emscripten', + r'failsafe_test\.dir', +]) + + +def repo_root(): + try: + return subprocess.check_output( + ['git', 'rev-parse', '--show-toplevel'], text=True).strip() + except subprocess.CalledProcessError: + print('error: not inside a git repository', file=sys.stderr) + sys.exit(1) + + +def changed_files(base_ref, root): + try: + out = subprocess.check_output( + ['git', 'diff', '--name-only', f'{base_ref}...HEAD', + '--', '*.cpp', '*.hpp', '*.h', '*.c'], + text=True, cwd=root).strip() + return out.splitlines() if out else [] + except subprocess.CalledProcessError: + print(f'error: could not diff against "{base_ref}" — ' + 'is the ref valid and fetched?', file=sys.stderr) + sys.exit(1) + + +def submodule_paths(root): + # Returns [] if .gitmodules is absent or has no paths — both valid + try: + out = subprocess.check_output( + ['git', 'config', '--file', '.gitmodules', + '--get-regexp', 'path'], + text=True, cwd=root).strip() + return [line.split()[1] for line in out.splitlines()] + except subprocess.CalledProcessError: + return [] + + +def build_exclude(root): + submodules = '|'.join(submodule_paths(root)) + return f'{submodules}|{EXCLUDE_EXTRA}' if submodules else EXCLUDE_EXTRA + + +def load_db(build_dir): + db_path = os.path.join(build_dir, 'compile_commands.json') + if not os.path.isfile(db_path): + print(f'error: {db_path} not found', file=sys.stderr) + print('Run "make px4_sitl_default-clang" first to generate ' + 'the compilation database', file=sys.stderr) + sys.exit(1) + try: + with open(db_path) as f: + return json.load(f) + except json.JSONDecodeError as e: + print(f'error: compile_commands.json is malformed: {e}', file=sys.stderr) + sys.exit(1) + + +def find_tus(changed, db, root): + db_files = {e['file'] for e in db} + result = set() + for f in changed: + abs_path = os.path.join(root, f) + ext = os.path.splitext(f)[1] + if ext in EXTENSIONS_CPP: + if abs_path in db_files: + result.add(abs_path) + elif ext in EXTENSIONS_HDR: + hdr = os.path.basename(f) + for e in db: + try: + if hdr in open(e['file']).read(): + result.add(e['file']) + except OSError: + pass # file deleted in PR — skip + return sorted(result) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('base_ref', + help='Git ref to diff against, e.g. origin/main') + args = parser.parse_args() + + root = repo_root() + build_dir = os.path.join(root, 'build', 'px4_sitl_default-clang') + + run_tidy = os.path.join(root, 'Tools', 'run-clang-tidy.py') + if not os.path.isfile(run_tidy): + print(f'error: {run_tidy} not found', file=sys.stderr) + sys.exit(1) + + changed = changed_files(args.base_ref, root) + if not changed: + print('No C++ files changed — skipping clang-tidy') + sys.exit(0) + + db = load_db(build_dir) + tus = find_tus(changed, db, root) + + if not tus: + print('No matching TUs in compile_commands.json — skipping clang-tidy') + sys.exit(0) + + print(f'Running clang-tidy on {len(tus)} translation unit(s)') + + result = subprocess.run( + [sys.executable, run_tidy, + '-header-filter=.*\\.hpp', + '-j0', + f'-exclude={build_exclude(root)}', + '-p', build_dir] + tus + ) + sys.exit(result.returncode) + + +if __name__ == '__main__': + main()