[llvm] wip: id checks (PR #173284)
Fabrice de Gans via llvm-commits
llvm-commits at lists.llvm.org
Mon Dec 22 09:02:32 PST 2025
https://github.com/Steelskin updated https://github.com/llvm/llvm-project/pull/173284
>From 058d61bd4af94661a8a6dfea28ffa0dc3ab11b87 Mon Sep 17 00:00:00 2001
From: Fabrice de Gans <fabrice at thebrowser.company>
Date: Wed, 17 Dec 2025 15:46:52 +0100
Subject: [PATCH] wip: id checks
---
.github/workflows/ids-check.yml | 94 +++++++++
llvm/include/llvm/ADT/APSInt.h | 2 +-
llvm/utils/git/ids-check-helper.py | 318 +++++++++++++++++++++++++++++
3 files changed, 413 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/ids-check.yml
create mode 100755 llvm/utils/git/ids-check-helper.py
diff --git a/.github/workflows/ids-check.yml b/.github/workflows/ids-check.yml
new file mode 100644
index 0000000000000..5413099c502d2
--- /dev/null
+++ b/.github/workflows/ids-check.yml
@@ -0,0 +1,94 @@
+name: ids-check
+on:
+ pull_request:
+ branches:
+ - main
+ - 'users/**'
+ push:
+ branches:
+ - 'main'
+ - 'release/**'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ if: github.repository_owner == 'llvm'
+ name: Check LLVM_ABI annotations with ids
+ runs-on: ubuntu-24.04
+ timeout-minutes: 5
+
+ steps:
+ - uses: actions/checkout at 8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.2.2
+ with:
+ repository: compnerd/ids
+ path: ${{ github.workspace }}/ids
+ ref: b3bf35dd13d7ff244a6a6d106fe58d0eedb5743e # main
+
+ - uses: actions/checkout at 8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.2.2
+ with:
+ path: ${{ github.workspace }}/llvm-project
+ fetch-depth: 2
+
+ - name: Get changed files
+ id: changed-files
+ uses: tj-actions/changed-files at 24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
+ with:
+ separator: ","
+ skip_initial_fetch: true
+ base_sha: 'HEAD~1'
+ sha: 'HEAD'
+
+ - name: Install dependencies
+ run: |
+ sudo apt install -y clang-19 ninja-build libclang-19-dev
+ pip install lit
+
+ - name: Configure and build minimal LLVM for use by ids
+ run: |
+ cmake -B ${{ github.workspace }}/llvm-project/build/ \
+ -S ${{ github.workspace }}/llvm-project/llvm/ \
+ -D CMAKE_BUILD_TYPE=Release \
+ -D CMAKE_C_COMPILER=clang \
+ -D CMAKE_CXX_COMPILER=clang++ \
+ -D LLVM_ENABLE_PROJECTS=clang \
+ -D LLVM_TARGETS_TO_BUILD="host" \
+ -D CMAKE_EXPORT_COMPILE_COMMANDS=ON \
+ -G Ninja
+ cd ${{ github.workspace }}/llvm-project/build/
+ ninja -t targets all | grep "CommonTableGen: phony$" | grep -v "/" | sed 's/:.*//'
+
+ - name: Configure ids
+ run: |
+ cmake -B ${{ github.workspace }}/ids/build/ \
+ -S ${{ github.workspace }}/ids/ \
+ -D CMAKE_BUILD_TYPE=Release \
+ -D CMAKE_C_COMPILER=clang \
+ -D CMAKE_CXX_COMPILER=clang++ \
+ -D CMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld \
+ -D LLVM_DIR=/usr/lib/llvm-19/lib/cmake/llvm/ \
+ -D Clang_DIR=/usr/lib/llvm-19/lib/cmake/clang/ \
+ -D FILECHECK_EXECUTABLE=$(which FileCheck-19) \
+ -D LIT_EXECUTABLE=$(which lit) \
+ -G Ninja
+
+ - name: Build ids
+ run: |
+ ninja -C ${{ github.workspace }}/ids/build/ all
+
+ - name: Run ids check
+ env:
+ CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
+ run: |
+ cd ${{ github.workspace }}/llvm-project
+ python ./llvm/utils/git/ids-check-helper.py \
+ --idt-path ${{ github.workspace }}/ids/build/bin/idt \
+ --compile-commands ${{ github.workspace }}/llvm-project/build/compile_commands.json \
+ --start-rev HEAD~1 \
+ --end-rev HEAD \
+ --changed-files "$CHANGED_FILES"
diff --git a/llvm/include/llvm/ADT/APSInt.h b/llvm/include/llvm/ADT/APSInt.h
index 88a7a6e71c817..65a3d0aa6f0b7 100644
--- a/llvm/include/llvm/ADT/APSInt.h
+++ b/llvm/include/llvm/ADT/APSInt.h
@@ -42,7 +42,7 @@ class [[nodiscard]] APSInt : public APInt {
/// constructed APSInt is determined automatically.
///
/// \param Str the string to be interpreted.
- LLVM_ABI explicit APSInt(StringRef Str);
+ explicit APSInt(StringRef Str);
/// Determine sign of this APSInt.
///
diff --git a/llvm/utils/git/ids-check-helper.py b/llvm/utils/git/ids-check-helper.py
new file mode 100755
index 0000000000000..d41c7ba268ab3
--- /dev/null
+++ b/llvm/utils/git/ids-check-helper.py
@@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+#
+# ====- ids-check-helper, runs ids-check from the ci or in a hook --*- python -*--==#
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+#
+# ==--------------------------------------------------------------------------------------==#
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import List, Optional
+
+"""
+This script is run by GitHub actions to ensure that the code in PRs properly
+labels LLVM APIs with `LLVM_ABI` so as not to break the LLVM DLL build. It can
+also be installed as a pre-commit git hook to check ABI annotations before
+submitting. The canonical source of this script is in the LLVM source tree
+under llvm/utils/git.
+
+This script uses the idt (Interface Diff Tool) to check for missing LLVM_ABI,
+LLVM_C_ABI, and DEMANGLE_ABI annotations in header files.
+
+You can install this script as a git hook by symlinking it to the .git/hooks
+directory:
+
+ln -s $(pwd)/llvm/utils/git/ids-check-helper.py .git/hooks/pre-commit
+
+You can control the exact path to idt and compile_commands.json with the
+following environment variables: $IDT_PATH and $COMPILE_COMMANDS_PATH.
+"""
+
+
+class IdsCheckArgs:
+ start_rev: str = ""
+ end_rev: str = ""
+ changed_files: List[str] = []
+ idt_path: str = ""
+ compile_commands: str = ""
+ verbose: bool = True
+
+ def __init__(self, args: argparse.Namespace) -> None:
+ self.start_rev = args.start_rev
+ self.end_rev = args.end_rev
+ self.changed_files = args.changed_files
+ self.idt_path = args.idt_path
+ self.compile_commands = args.compile_commands
+ self.verbose = getattr(args, "verbose", True)
+
+
+class IdsChecker:
+ """
+ Checker for LLVM ABI annotations using the idt tool.
+ """
+
+ # Macro definition used for all export macros
+ MACRO_DEFINITION = '__attribute__((visibility("default")))'
+
+ # Define the file categories and their corresponding configurations
+ FILE_CATEGORIES = [
+ {
+ "name": "LLVM headers",
+ "patterns": ["llvm/include/llvm/**/*.h"],
+ "excludes": [
+ "llvm/include/llvm/Debuginfod/",
+ "llvm/include/llvm/Demangle/",
+ ],
+ "export_macro": "LLVM_ABI",
+ "include_header": "llvm/include/llvm/Support/Compiler.h",
+ },
+ {
+ "name": "LLVM-C headers",
+ "patterns": ["llvm/include/llvm-c/**/*.h"],
+ "excludes": [],
+ "export_macro": "LLVM_C_ABI",
+ "include_header": "llvm/include/llvm-c/Visibility.h",
+ },
+ {
+ "name": "LLVM Demangle headers",
+ "patterns": ["llvm/include/llvm/Demangle/**/*.h"],
+ "excludes": [],
+ "export_macro": "DEMANGLE_ABI",
+ "include_header": "llvm/Demangle/Visibility.h",
+ },
+ ]
+
+ def filter_files_for_category(
+ self, changed_files: List[str], category: dict
+ ) -> List[str]:
+ """Filter changed files based on category patterns and excludes."""
+ filtered = []
+ for path in changed_files:
+ # Check if file matches any pattern
+ matches_pattern = False
+ for pattern in category["patterns"]:
+ # Simple pattern matching for /**/*.h style patterns
+ pattern_prefix = pattern.replace("/**/*.h", "")
+ if path.startswith(pattern_prefix) and path.endswith(".h"):
+ matches_pattern = True
+ break
+
+ if not matches_pattern:
+ continue
+
+ # Check if file should be excluded
+ excluded = False
+ for exclude in category["excludes"]:
+ if path.startswith(exclude):
+ excluded = True
+ break
+
+ if not excluded:
+ filtered.append(path)
+
+ return filtered
+
+ def run_idt_on_files(
+ self,
+ files: List[str],
+ category: dict,
+ args: IdsCheckArgs,
+ idt_path: str,
+ compile_commands: str,
+ ) -> bool:
+ """Run idt tool on the given files with category-specific configuration."""
+ if not files:
+ return True
+
+ if args.verbose:
+ print(
+ f"Running idt on {len(files)} {category['name']} file(s)...",
+ file=sys.stderr,
+ )
+
+ for file in files:
+ cmd = [
+ idt_path,
+ "-p",
+ compile_commands,
+ "--apply-fixits",
+ "--inplace",
+ f"--export-macro={category['export_macro']}",
+ f"--include-header={category['include_header']}",
+ f"--extra-arg=-D{category['export_macro']}={self.MACRO_DEFINITION}",
+ "--extra-arg=-Wno-macro-redefined",
+ file,
+ ]
+
+ if args.verbose:
+ print(f"Running: {' '.join(cmd)}", file=sys.stderr)
+
+ proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ if proc.returncode != 0:
+ sys.stdout.write(proc.stdout.decode("utf-8"))
+ sys.stderr.write(proc.stderr.decode("utf-8"))
+ print(
+ f"Error: idt failed on {file} with return code {proc.returncode}",
+ file=sys.stderr,
+ )
+ return False
+
+ return True
+
+ def get_changed_files(self, args: IdsCheckArgs) -> List[str]:
+ """Get list of changed files between revisions."""
+ if args.changed_files:
+ return args.changed_files
+
+ cmd = ["git", "diff", "--name-only", args.start_rev, args.end_rev]
+ if args.verbose:
+ print(f"Running: {' '.join(cmd)}", file=sys.stderr)
+
+ proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ if proc.returncode != 0:
+ print("Error: Failed to get changed files", file=sys.stderr)
+ sys.stderr.write(proc.stderr.decode("utf-8"))
+ return []
+
+ files = proc.stdout.decode("utf-8").strip().split("\n")
+ return [f for f in files if f]
+
+ def check_for_diff(self) -> Optional[str]:
+ """Check if there are any uncommitted changes after running idt."""
+ cmd = ["git", "diff"]
+ proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ diff = proc.stdout.decode("utf-8")
+ if diff:
+ return diff
+ return None
+
+ def run(self, args: IdsCheckArgs) -> int:
+ """Main entry point for running ids check."""
+ # Resolve idt path: prefer command line arg, then env var
+ idt_path = args.idt_path or os.environ.get("IDT_PATH")
+ if not idt_path:
+ print(
+ "Error: idt path not specified. Use --idt-path argument or set IDT_PATH environment variable",
+ file=sys.stderr,
+ )
+ return 1
+
+ if not os.path.exists(idt_path):
+ print(f"Error: idt tool not found at {idt_path}", file=sys.stderr)
+ return 1
+
+ # Resolve compile_commands path: prefer command line arg, then env var
+ compile_commands = args.compile_commands or os.environ.get(
+ "COMPILE_COMMANDS_PATH"
+ )
+ if not compile_commands:
+ print(
+ "Error: compile_commands.json path not specified. Use --compile-commands argument or set COMPILE_COMMANDS_PATH environment variable",
+ file=sys.stderr,
+ )
+ return 1
+
+ if not os.path.exists(compile_commands):
+ print(
+ f"Error: compile_commands.json not found at {compile_commands}",
+ file=sys.stderr,
+ )
+ return 1
+
+ # Get changed files
+ changed_files = self.get_changed_files(args)
+ if not changed_files:
+ if args.verbose:
+ print("No files changed, skipping ids check", file=sys.stderr)
+ return 0
+
+ # Process each category
+ any_processed = False
+ for category in self.FILE_CATEGORIES:
+ filtered_files = self.filter_files_for_category(changed_files, category)
+ if filtered_files:
+ any_processed = True
+ if not self.run_idt_on_files(
+ filtered_files, category, args, idt_path, compile_commands
+ ):
+ return 1
+
+ if not any_processed:
+ if args.verbose:
+ print(
+ "No relevant header files changed, skipping ids check",
+ file=sys.stderr,
+ )
+ return 0
+
+ # Check for differences
+ diff = self.check_for_diff()
+ if diff:
+ print("\nError: idt found missing LLVM_ABI annotations", file=sys.stderr)
+ print(
+ "Apply the following diff to fix the LLVM_ABI annotations:\n",
+ file=sys.stderr,
+ )
+ print(diff)
+ return 1
+
+ if args.verbose:
+ print("All files pass ids check", file=sys.stderr)
+ return 0
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="Check LLVM ABI annotations in header files"
+ )
+ parser.add_argument(
+ "--start-rev",
+ type=str,
+ required=True,
+ help="Compute changes from this revision",
+ )
+ parser.add_argument(
+ "--end-rev",
+ type=str,
+ required=True,
+ help="Compute changes to this revision",
+ )
+ parser.add_argument(
+ "--changed-files",
+ type=str,
+ help="Comma separated list of files that have been changed",
+ )
+ parser.add_argument(
+ "--idt-path",
+ type=str,
+ help="Path to the idt executable (can also be set via IDT_PATH environment variable)",
+ )
+ parser.add_argument(
+ "--compile-commands",
+ type=str,
+ help="Path to compile_commands.json (can also be set via COMPILE_COMMANDS_PATH environment variable)",
+ )
+ parser.add_argument(
+ "--verbose", action="store_true", default=True, help="Enable verbose output"
+ )
+
+ parsed_args = parser.parse_args()
+
+ # Parse changed files if provided
+ if parsed_args.changed_files:
+ parsed_args.changed_files = [
+ f.strip() for f in parsed_args.changed_files.split(",") if f.strip()
+ ]
+ else:
+ parsed_args.changed_files = []
+
+ args = IdsCheckArgs(parsed_args)
+ checker = IdsChecker()
+ sys.exit(checker.run(args))
+
More information about the llvm-commits
mailing list