diff --git a/.github/workflows/check-abi-comment.yml b/.github/workflows/check-abi-comment.yml
new file mode 100644
index 00000000..0faa6a76
--- /dev/null
+++ b/.github/workflows/check-abi-comment.yml
@@ -0,0 +1,91 @@
+name: Check ABI Comment
+
+# Posts the result of the "Check ABI" workflow back onto the pull request.
+# Because it is triggered by workflow_run it runs in the base repository
+# context with a writable token, so it can comment on fork PRs without ever
+# checking out or executing untrusted PR code.
+#
+# One comment per PR is created on the first run and updated in place after.
+#
+# NOTE: workflow_run only triggers from workflow files on the default branch, so
+# this begins working once merged to master.
+
+on:
+ workflow_run:
+ workflows: [Check ABI]
+ types: [completed]
+
+permissions:
+ actions: read # download the summary artifacts from the triggering run
+ contents: read
+ pull-requests: write # comment on the pull request (issues API, PR target)
+
+jobs:
+ comment:
+ runs-on: ubuntu-latest
+ if: >-
+ github.event.workflow_run.event == 'pull_request' ||
+ github.event.workflow_run.event == 'pull_request_review'
+ steps:
+ - name: Download summaries
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
+ continue-on-error: true
+ with:
+ pattern: abi-summary-*
+ merge-multiple: true
+ run-id: ${{ github.event.workflow_run.id }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ - name: Post or update PR comment
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ RUN_URL: ${{ github.event.workflow_run.html_url }}
+ HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
+ with:
+ script: |
+ const fs = require('fs');
+ const marker = '';
+ const { owner, repo } = context.repo;
+
+ if (!fs.existsSync('pr_number.txt')) {
+ core.info('No summary artifact found; nothing to comment on.');
+ return;
+ }
+ const issue_number = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim(), 10);
+ if (!Number.isInteger(issue_number)) {
+ core.info('Invalid PR number; skipping.');
+ return;
+ }
+ // The artifact comes from PR-controlled code; only trust the number if
+ // that PR's head is the commit the run checked.
+ const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: issue_number });
+ if (pr.head.sha !== process.env.HEAD_SHA) {
+ core.info(`PR #${issue_number} head is ${pr.head.sha}, run checked ${process.env.HEAD_SHA}; skipping.`);
+ return;
+ }
+
+ const sections = ['last-release', 'master']
+ .filter(base => fs.existsSync(`${base}.md`))
+ .map(base => fs.readFileSync(`${base}.md`, 'utf8').trim());
+
+ const body = [
+ marker,
+ '### Level Zero loader ABI check',
+ '',
+ ...sections.flatMap(s => [s, '']),
+ `[Workflow run](${process.env.RUN_URL})`,
+ '',
+ 'If an ABI change is intended, a reviewer with write access can accept it: '
+ + 'leave an **approval review** on the PR head commit with the keyword '
+ + '`abi-failure-approved` in the review body. That re-runs the check.',
+ ].join('\n');
+
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ owner, repo, issue_number, per_page: 100,
+ });
+ const existing = comments.find(c => c.body && c.body.includes(marker));
+
+ if (existing) {
+ await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
+ } else {
+ await github.rest.issues.createComment({ owner, repo, issue_number, body });
+ }
diff --git a/.github/workflows/check-abi.yml b/.github/workflows/check-abi.yml
index a82892e9..49577b4f 100644
--- a/.github/workflows/check-abi.yml
+++ b/.github/workflows/check-abi.yml
@@ -1,154 +1,235 @@
+name: Check ABI
+
on:
pull_request:
branches: [master]
+ # An approval review with the keyword re-runs the check, so the override
+ # below takes effect without a manual re-run.
+ pull_request_review:
+ types: [submitted]
+ # Seeds the base ABI dump cache from master. Caches saved by pull_request
+ # runs are visible only to that PR, so without this every new PR rebuilds
+ # the base.
+ push:
+ branches: [master]
workflow_dispatch:
permissions: read-all
jobs:
- # Use https://lvc.github.io/abi-compliance-checker/ to compare builds
- # of pull requests against master. Compares checkouts of github.base_ref and
- # github.head_ref.
- check-abi-last-release:
+ # Use https://lvc.github.io/abi-compliance-checker/ to compare builds of
+ # pull requests against the latest release and against master.
+ #
+ # A failure can be accepted by a reviewer: leave an approval review on the
+ # PR head commit with the keyword abi-failure-approved in the review body.
+ check-abi:
+ name: check-abi-${{ matrix.base }}
runs-on: ubuntu-latest
- if: github.repository_owner == 'oneapi-src'
+ if: >-
+ github.repository_owner == 'oneapi-src' &&
+ (github.event_name != 'pull_request_review' ||
+ (github.event.review.state == 'approved' &&
+ contains(github.event.review.body, 'abi-failure-approved')))
+ strategy:
+ fail-fast: false
+ matrix:
+ base: [last-release, master]
+ env:
+ # Pinned so a new upstream commit cannot change the result between runs.
+ # The base dump cache key includes the abi-dumper version.
+ ABI_DUMPER_REF: 9ed641529d411ca0f824175489c1f67ec924edae # 1.4
+ ABI_CHECKER_VERSION: '2.3'
+ # A push to master only seeds the base dump cache; there is no head to compare.
+ COMPARE: ${{ github.event_name != 'push' }}
steps:
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y elfutils universal-ctags vtable-dumper
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ if: matrix.base == 'last-release'
with:
fetch-depth: 0 # Required due to the way Git works, without it this action won't be able to find any or the correct tags
- uses: actions-ecosystem/action-get-latest-tag@b7c32daec3395a9616f88548363a42652b22d435 # v1.6.0
+ if: matrix.base == 'last-release'
id: get-latest-tag
with:
semver_only: true
- name: Checkout base
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
- ref: ${{ steps.get-latest-tag.outputs.tag }}
+ ref: ${{ matrix.base == 'last-release' && steps.get-latest-tag.outputs.tag || github.event.pull_request.base.ref || 'master' }}
clean: true
path: base
+ - name: Compute base ABI dump cache key
+ # The dump depends on the base commit, the compiler (runner image) and
+ # the abi-dumper version.
+ id: base-key
+ run: |
+ echo "key=abi-dump-$(git -C base rev-parse HEAD)-$ImageOS-$ImageVersion-$ABI_DUMPER_REF" >> "$GITHUB_OUTPUT"
+ - name: Restore base ABI dump cache
+ # On a hit the base build and dump below are skipped.
+ id: cache-base-dump
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: base.dump
+ key: ${{ steps.base-key.outputs.key }}
- name: Checkout head
+ if: env.COMPARE == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
clean: true
path: head
ref: ${{ github.event.pull_request.head.sha }}
- name: Build debug base
+ if: steps.cache-base-dump.outputs.cache-hit != 'true'
run: |
mkdir base/build
cd base/build
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-Og -g -fno-lto" -DCMAKE_EXE_LINKER_FLAGS="-fno-lto" -DCMAKE_SHARED_LINKER_FLAGS="-fno-lto" ..
make -j$(nproc)
- name: Build debug head
+ if: env.COMPARE == 'true'
run: |
mkdir head/build
cd head/build
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-Og -g -fno-lto" -DCMAKE_EXE_LINKER_FLAGS="-fno-lto" -DCMAKE_SHARED_LINKER_FLAGS="-fno-lto" ..
make -j$(nproc)
- name: Download and setup abi-dumper
+ if: steps.cache-base-dump.outputs.cache-hit != 'true' || env.COMPARE == 'true'
run: |
git clone https://github.com/lvc/abi-dumper.git abi-dumper
+ git -C abi-dumper checkout --detach "$ABI_DUMPER_REF"
cp abi-dumper/abi-dumper.pl .
chmod +x abi-dumper.pl
- name: Generate dump for base
+ if: steps.cache-base-dump.outputs.cache-hit != 'true'
run: |
./abi-dumper.pl \
./base/build/lib/libze_loader.so \
-vnum $(cat ./base/build/VERSION) -lambda \
-public-headers ./base/include \
-o ./base.dump
+ - name: Save base ABI dump cache
+ if: steps.cache-base-dump.outputs.cache-hit != 'true'
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: base.dump
+ key: ${{ steps.base-key.outputs.key }}
- name: Generate dump for head
+ if: env.COMPARE == 'true'
run: |
./abi-dumper.pl \
./head/build/lib/libze_loader.so \
-vnum $(cat ./head/build/VERSION)-1 -lambda \
-public-headers ./head/include \
-o ./head.dump
+ - name: Ignore ZE_API_VERSION_CURRENT bump
+ # ZE_API_VERSION_CURRENT is expected to move with every minor spec
+ # version. Copy the base value into head.dump so the bump is not
+ # reported as an ABI problem.
+ if: env.COMPARE == 'true'
+ run: python3 head/scripts/update_ze_api_version.py base.dump head.dump
- name: Download and setup abi-compliance-checker
- run: |
- wget https://github.com/lvc/abi-compliance-checker/tarball/master -O abi-compliance-checker.tar.gz
- tar -xzf abi-compliance-checker.tar.gz
- chmod +x lvc-abi-compliance-checker-*/abi-compliance-checker.pl
+ if: env.COMPARE == 'true'
+ run: |
+ wget https://github.com/lvc/abi-compliance-checker/tarball/$ABI_CHECKER_VERSION -O abi-compliance-checker.tar.gz
+ mkdir abi-compliance-checker
+ tar -xzf abi-compliance-checker.tar.gz --strip-components=1 -C abi-compliance-checker
+ chmod +x abi-compliance-checker/abi-compliance-checker.pl
+ - name: Downgrade DWARF storage-location rules to Low
+ # abi-dumper keeps the LAST entry of a DWARF location list, not the first,
+ # so these two rules report where -Og parked a value late in the function
+ # body, not how it was passed. On x86-64 the real argument registers and
+ # stack slots follow from parameter types and order alone, so any genuine
+ # break is already reported by Parameter_Type_*/Added_*/Removed_* rules.
+ # Low keeps both findings in the report without failing the job.
+ if: env.COMPARE == 'true'
+ shell: python3 {0}
+ run: |
+ import re
+ path = "abi-compliance-checker/modules/RulesBin.xml"
+ text = open(path).read()
+ # Fail only when the rule id is gone (the checker version changed).
+ for rule in ("Parameter_Changed_Register", "Parameter_Changed_Offset"):
+ text, count = re.subn(
+ r"(\s*%s\s*\s*\s*)(?:High|Low)(\s*)" % rule,
+ r"\1Low\2", text)
+ print("%s: %d rule(s) downgraded" % (rule, count))
+ assert count == 1, "rule %s not found -- checker version changed?" % rule
+ open(path, "w").write(text)
- name: Compare dumps
+ if: env.COMPARE == 'true'
+ id: abi-check
+ continue-on-error: true
run: |
- lvc-abi-compliance-checker-*/abi-compliance-checker.pl \
+ set -o pipefail
+ abi-compliance-checker/abi-compliance-checker.pl \
-l libze_loader \
-old base.dump \
-new head.dump \
- -report-path release_report.html
- - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
- if: always()
- with:
- name: abi-report-last-release
- path: release_report.html
- check-abi-master:
- runs-on: ubuntu-latest
- if: github.repository_owner == 'oneapi-src'
- steps:
- - name: Install dependencies
- run: |
- sudo apt update
- sudo apt install -y elfutils universal-ctags vtable-dumper
- - name: Checkout base
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ -report-path ${{ matrix.base }}_report.html 2>&1 | tee compare.log
+ - name: Check for reviewer approval of ABI failure
+ # Only an approval from someone with write access, given on the commit
+ # that was checked, counts. A later push needs a new approval.
+ id: check-approval
+ if: steps.abi-check.outcome == 'failure' && github.event.pull_request
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
- ref: ${{ github.base_ref || 'master' }}
- clean: true
- path: base
- - name: Checkout head
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ script: |
+ const reviews = await github.paginate(github.rest.pulls.listReviews, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: context.payload.pull_request.number,
+ });
+ const approved = reviews.some(review =>
+ review.state === 'APPROVED' &&
+ review.commit_id === context.payload.pull_request.head.sha &&
+ ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(review.author_association) &&
+ (review.body || '').toLowerCase().includes('abi-failure-approved'));
+ core.info(approved ? 'ABI failure has been approved by a reviewer'
+ : 'No reviewer approval found for ABI failure');
+ core.setOutput('approved', approved ? 'true' : 'false');
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+ if: always() && env.COMPARE == 'true'
+ id: report
with:
- clean: true
- path: head
- ref: ${{ github.event.pull_request.head.sha }}
- - name: Build debug base
- run: |
- mkdir base/build
- cd base/build
- cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-Og -g -fno-lto" -DCMAKE_EXE_LINKER_FLAGS="-fno-lto" -DCMAKE_SHARED_LINKER_FLAGS="-fno-lto" ..
- make -j$(nproc)
- - name: Build debug head
- run: |
- mkdir head/build
- cd head/build
- cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-Og -g -fno-lto" -DCMAKE_EXE_LINKER_FLAGS="-fno-lto" -DCMAKE_SHARED_LINKER_FLAGS="-fno-lto" ..
- make -j$(nproc)
- - name: Download and setup abi-dumper
- run: |
- git clone https://github.com/lvc/abi-dumper.git abi-dumper
- cp abi-dumper/abi-dumper.pl .
- chmod +x abi-dumper.pl
- - name: Generate dump for base
- run: |
- ./abi-dumper.pl \
- ./base/build/lib/libze_loader.so \
- -vnum $(cat ./base/build/VERSION) -lambda \
- -public-headers ./base/include \
- -o ./base.dump
- - name: Generate dump for head
- run: |
- ./abi-dumper.pl \
- ./head/build/lib/libze_loader.so \
- -vnum $(cat ./head/build/VERSION)-1 -lambda \
- -public-headers ./head/include \
- -o ./head.dump
- - name: Download and setup abi-compliance-checker
- run: |
- wget https://github.com/lvc/abi-compliance-checker/tarball/master -O abi-compliance-checker.tar.gz
- tar -xzf abi-compliance-checker.tar.gz
- chmod +x lvc-abi-compliance-checker-*/abi-compliance-checker.pl
- - name: Compare dumps
- run: |
- lvc-abi-compliance-checker-*/abi-compliance-checker.pl \
- -l libze_loader \
- -old base.dump \
- -new head.dump \
- -report-path master_report.html
+ name: abi-report-${{ matrix.base }}
+ path: ${{ matrix.base }}_report.html
+ - name: Write summary
+ # Read by the "Check ABI Comment" workflow, which posts it on the PR.
+ if: always() && env.COMPARE == 'true' && github.event.pull_request
+ env:
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ OUTCOME: ${{ steps.abi-check.outcome }}
+ APPROVED: ${{ steps.check-approval.outputs.approved }}
+ REPORT_URL: ${{ steps.report.outputs.artifact-url }}
+ BASE_REF: ${{ matrix.base == 'last-release' && steps.get-latest-tag.outputs.tag || github.event.pull_request.base.ref }}
+ run: |
+ mkdir -p abi-summary
+ echo "$PR_NUMBER" > abi-summary/pr_number.txt
+ case "$OUTCOME/$APPROVED" in
+ success/*) status=":white_check_mark: no ABI problems" ;;
+ failure/true) status=":warning: ABI problems, approved by a reviewer" ;;
+ failure/*) status=":x: ABI problems" ;;
+ *) status=":grey_question: the check did not run to the end" ;;
+ esac
+ {
+ echo "**${{ matrix.base }}** (\`$BASE_REF\`): $status. [Report]($REPORT_URL)"
+ echo
+ echo '```'
+ grep -E 'compatibility' compare.log 2>/dev/null || echo "No result from abi-compliance-checker; see the job log."
+ echo '```'
+ } > abi-summary/${{ matrix.base }}.md
+ cat abi-summary/${{ matrix.base }}.md >> "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
- if: always()
+ if: always() && env.COMPARE == 'true' && github.event.pull_request
with:
- name: abi-report-master
- path: master_report.html
\ No newline at end of file
+ name: abi-summary-${{ matrix.base }}
+ path: abi-summary
+ - name: Fail if ABI check failed and not approved
+ if: steps.abi-check.outcome == 'failure' && steps.check-approval.outputs.approved != 'true'
+ run: |
+ echo "::error::ABI compliance check failed and no reviewer approval found."
+ echo "If the change is intended, a reviewer with write access can approve the PR head commit with 'abi-failure-approved' in the review body."
+ exit 1
diff --git a/scripts/update_ze_api_version.py b/scripts/update_ze_api_version.py
new file mode 100755
index 00000000..bc4fa01b
--- /dev/null
+++ b/scripts/update_ze_api_version.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+"""
+ Copyright (C) 2026 Intel Corporation
+
+ SPDX-License-Identifier: MIT
+
+Script to update ZE_API_VERSION_CURRENT value in a new dump file to match the old dump file.
+This ensures ABI compatibility by reverting the version to the previous value.
+"""
+
+import re
+import sys
+import argparse
+
+
+def get_ze_api_version_current_value(dump_file_path):
+ """Extract the ZE_API_VERSION_CURRENT value from a dump file."""
+ try:
+ with open(dump_file_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Pattern to match ZE_API_VERSION_CURRENT and its value
+ pattern = r"'name'\s*=>\s*'ZE_API_VERSION_CURRENT',\s*\n\s*'value'\s*=>\s*'(\d+)'"
+ match = re.search(pattern, content)
+
+ if match:
+ return match.group(1)
+ else:
+ print(f"Error: Could not find ZE_API_VERSION_CURRENT in {dump_file_path}")
+ return None
+ except FileNotFoundError:
+ print(f"Error: File {dump_file_path} not found")
+ return None
+ except Exception as e:
+ print(f"Error reading {dump_file_path}: {e}")
+ return None
+
+
+def update_ze_api_version_current(dump_file_path, old_value, new_value):
+ """Update ZE_API_VERSION_CURRENT value in the dump file."""
+ try:
+ with open(dump_file_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Pattern to match ZE_API_VERSION_CURRENT and replace its value
+ pattern = r"('name'\s*=>\s*'ZE_API_VERSION_CURRENT',\s*\n\s*'value'\s*=>\s*')(\d+)(')"
+
+ def replace_value(match):
+ if match.group(2) == new_value:
+ return match.group(1) + old_value + match.group(3)
+ return match.group(0) # No change if value doesn't match expected
+
+ updated_content = re.sub(pattern, replace_value, content)
+
+ # Check if any replacement was made
+ if updated_content == content:
+ print(f"Warning: No replacement made. Expected to find value '{new_value}' but it was not found.")
+ return False
+
+ # Write the updated content back to the file
+ with open(dump_file_path, 'w', encoding='utf-8') as f:
+ f.write(updated_content)
+
+ print(f"Successfully updated ZE_API_VERSION_CURRENT from '{new_value}' to '{old_value}' in {dump_file_path}")
+ return True
+
+ except Exception as e:
+ print(f"Error updating {dump_file_path}: {e}")
+ return False
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Update ZE_API_VERSION_CURRENT in new dump file to match old dump file"
+ )
+ parser.add_argument(
+ "old_dump",
+ help="Path to the old dump file (reference for the target value)"
+ )
+ parser.add_argument(
+ "new_dump",
+ help="Path to the new dump file (to be modified)"
+ )
+ parser.add_argument(
+ "--dry-run",
+ action="store_true",
+ help="Show what would be changed without actually modifying the file"
+ )
+
+ args = parser.parse_args()
+
+ # Get the ZE_API_VERSION_CURRENT value from the old dump
+ old_value = get_ze_api_version_current_value(args.old_dump)
+ if old_value is None:
+ sys.exit(1)
+
+ # Get the ZE_API_VERSION_CURRENT value from the new dump
+ new_value = get_ze_api_version_current_value(args.new_dump)
+ if new_value is None:
+ sys.exit(1)
+
+ print(f"Old dump ({args.old_dump}) ZE_API_VERSION_CURRENT value: {old_value}")
+ print(f"New dump ({args.new_dump}) ZE_API_VERSION_CURRENT value: {new_value}")
+
+ if old_value == new_value:
+ print("Values are already the same. No update needed.")
+ sys.exit(0)
+
+ if args.dry_run:
+ print(f"DRY RUN: Would update ZE_API_VERSION_CURRENT from '{new_value}' to '{old_value}' in {args.new_dump}")
+ sys.exit(0)
+
+ # Perform the update
+ success = update_ze_api_version_current(args.new_dump, old_value, new_value)
+
+ if success:
+ # Verify the change
+ updated_value = get_ze_api_version_current_value(args.new_dump)
+ if updated_value == old_value:
+ print("Verification successful: Value has been updated correctly.")
+ else:
+ print(f"Verification failed: Expected '{old_value}', but found '{updated_value}'")
+ sys.exit(1)
+ else:
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()