[llvm] wip: id checks (PR #173284)
Fabrice de Gans via llvm-commits
llvm-commits at lists.llvm.org
Mon Dec 22 09:22:01 PST 2025
https://github.com/Steelskin updated https://github.com/llvm/llvm-project/pull/173284
>From de6379ddf5ee154cb7ff604e9d0b25aacb38c53e 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 | 107 +++++++
llvm/include/llvm/ADT/APSInt.h | 2 +-
llvm/utils/git/ids-check-helper.py | 441 +++++++++++++++++++++++++++++
3 files changed, 549 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..e375543eba31a
--- /dev/null
+++ b/.github/workflows/ids-check.yml
@@ -0,0 +1,107 @@
+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: 10
+
+ 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 github
+
+ - 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:
+ GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
+ CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
+ # Create an empty comments file so the upload step doesn't fail.
+ run: |
+ cd ${{ github.workspace }}/llvm-project
+ echo "[]" > comments &&
+ python ./llvm/utils/git/ids-check-helper.py \
+ --write-comment-to-file \
+ --token ${{ secrets.GITHUB_TOKEN }} \
+ --issue-number $GITHUB_PR_NUMBER \
+ --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"
+
+ - uses: actions/upload-artifact at 330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
+ if: always()
+ with:
+ name: workflow-args
+ path: |
+ comments
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..f285654d1a569
--- /dev/null
+++ b/llvm/utils/git/ids-check-helper.py
@@ -0,0 +1,441 @@
+#!/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 = ""
+ repo: str = ""
+ token: str = ""
+ issue_number: int = 0
+ write_comment_to_file: bool = False
+ 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.repo = getattr(args, "repo", "")
+ self.token = getattr(args, "token", "")
+ self.issue_number = getattr(args, "issue_number", 0)
+ self.write_comment_to_file = getattr(args, "write_comment_to_file", False)
+ self.verbose = getattr(args, "verbose", True)
+
+
+class IdsChecker:
+ """
+ Checker for LLVM ABI annotations using the idt tool.
+ """
+
+ COMMENT_TAG = "<!--LLVM IDS CHECK COMMENT-->"
+ name = "ids-check"
+ friendly_name = "LLVM ABI annotation checker"
+ comment: dict = {}
+
+ # Macro definition used for all export macros
+ MACRO_DEFINITION = '__attribute__((visibility("default")))'
+
+ @property
+ def comment_tag(self) -> str:
+ return self.COMMENT_TAG
+
+ @property
+ def instructions(self) -> str:
+ # Provide basic usage instructions
+ return f"""git diff origin/main HEAD -- 'llvm/include/llvm/**/*.h' 'llvm/include/llvm-c/**/*.h' 'llvm/include/llvm/Demangle/**/*.h'
+Then run idt on the changed files with appropriate --export-macro and --include-header flags."""
+
+ def pr_comment_text_for_diff(self, diff: str) -> str:
+ return f"""
+:warning: {self.friendly_name}, {self.name} found issues in your code. :warning:
+
+<details>
+<summary>
+You can test this locally with the following command:
+</summary>
+
+``````````bash
+{self.instructions}
+``````````
+
+:warning:
+The reproduction instructions above might return results for more than one PR
+in a stack if you are using a stacked PR workflow. You can limit the results by
+changing `origin/main` to the base branch/commit you want to compare against.
+:warning:
+
+</details>
+
+<details>
+<summary>
+View the diff from {self.name} here.
+</summary>
+
+``````````diff
+{diff}
+``````````
+
+</details>
+"""
+
+ # TODO: any type should be replaced with the correct github type, but it requires refactoring to
+ # not require the github module to be installed everywhere.
+ def find_comment(self, pr: any) -> any:
+ for comment in pr.as_issue().get_comments():
+ if self.comment_tag in comment.body:
+ return comment
+ return None
+
+ def update_pr(
+ self, comment_text: str, args: IdsCheckArgs, create_new: bool
+ ) -> None:
+ import github
+ from github import IssueComment, PullRequest
+
+ repo = github.Github(args.token).get_repo(args.repo)
+ pr = repo.get_issue(args.issue_number).as_pull_request()
+
+ comment_text = self.comment_tag + "\n\n" + comment_text
+
+ existing_comment = self.find_comment(pr)
+
+ if args.write_comment_to_file:
+ if create_new or existing_comment:
+ self.comment = {"body": comment_text}
+ if existing_comment:
+ self.comment["id"] = existing_comment.id
+ return
+
+ if existing_comment:
+ existing_comment.edit(comment_text)
+ elif create_new:
+ pr.as_issue().create_comment(comment_text)
+
+ # 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()
+ should_update_gh = args.token and args.repo
+
+ if diff:
+ if should_update_gh:
+ comment_text = self.pr_comment_text_for_diff(diff)
+ self.update_pr(comment_text, args, create_new=True)
+ else:
+ 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
+ else:
+ if should_update_gh:
+ comment_text = (
+ ":white_check_mark: With the latest revision "
+ f"this PR passed the {self.friendly_name}."
+ )
+ self.update_pr(comment_text, args, create_new=False)
+ 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("--token", type=str, help="GitHub authentication token")
+ parser.add_argument(
+ "--repo",
+ type=str,
+ default=os.getenv("GITHUB_REPOSITORY", "llvm/llvm-project"),
+ help="The GitHub repository that we are working with in the form of <owner>/<repo> (e.g. llvm/llvm-project)",
+ )
+ parser.add_argument("--issue-number", type=int, help="GitHub issue/PR number")
+ 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(
+ "--write-comment-to-file",
+ action="store_true",
+ help="Don't post comments on the PR, instead write the comments and metadata a file called 'comment'",
+ )
+ 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()
+ exit_code = checker.run(args)
+
+ if checker.comment:
+ with open("comment", "w") as f:
+ import json
+
+ json.dump([checker.comment], f)
+
+ sys.exit(exit_code)
More information about the llvm-commits
mailing list