Skip to content

Commit f715053

Browse files
Arm backend: Add public API manifest validator
Add a validator script that compares one public API manifest against the current backends/arm API using exact symbol, kind, and signature matching. Add regression tests for manifest drift, and run the validator from the Arm pre-push hook on every push. Change-Id: I4765510b8a2f8c5aa09717fc8287d976c770897d Signed-off-by: Sebastian Larsson <sebastian.larsson@arm.com>
1 parent 0a113f8 commit f715053

9 files changed

Lines changed: 704 additions & 12 deletions

File tree

backends/arm/public_api_manifests/api_manifest_running.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# LICENSE file in the root directory of this source tree.
55
#
66
# This file is generated by
7-
# backends/arm/scripts/generate_public_api_manifest.py
7+
# backends/arm/scripts/public_api_manifest/generate_public_api_manifest.py
88

99
[python]
1010

backends/arm/scripts/pre-push

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,37 @@ run_docgen_check() {
7070
fi
7171
}
7272

73+
run_public_api_validator() {
74+
echo -e "${INFO} Validating Arm public API manifests"
75+
76+
manifest_failures=0
77+
for manifest_path in backends/arm/public_api_manifests/api_manifest_*.toml; do
78+
if [[ ! -f "${manifest_path}" ]]; then
79+
continue
80+
fi
81+
manifest_name="${manifest_path##*/}"
82+
echo
83+
echo "=== ${manifest_name} ==="
84+
validator_output=$(
85+
python backends/arm/scripts/public_api_manifest/validate_public_api_manifest.py \
86+
--manifest "${manifest_path}" 2>&1
87+
)
88+
validator_status=$?
89+
printf '%s\n' "${validator_output}"
90+
if [[ ${validator_status} -ne 0 ]]; then
91+
manifest_failures=$((manifest_failures + 1))
92+
FAILED=1
93+
fi
94+
done
95+
96+
echo
97+
if [[ ${manifest_failures} -eq 0 ]]; then
98+
echo -e "${SUCCESS} Arm public API manifests OK"
99+
else
100+
echo -e "${ERROR} ${manifest_failures} manifest(s) failed validation"
101+
fi
102+
}
103+
73104
# This list of imperative verbs was compiled from the entire list of Executorch
74105
# commits. It should be fairly exhaustive, but add more verbs if you find one
75106
# that's missing.
@@ -149,7 +180,6 @@ for COMMIT in ${COMMITS}; do
149180
fi
150181
done
151182
fi
152-
153183
# Check license headers
154184
# We do a simple check of if all committed headers contain
155185
# "$current_year Arm". This does not guarantee OK in ci but should be ok
@@ -177,7 +207,7 @@ for COMMIT in ${COMMITS}; do
177207
for committed_file in "${license_files[@]}"; do
178208
# Skip files with certain extensions
179209
case "$committed_file" in
180-
*.md|*.md.in|*.json|*.yml|*.yaml|*.cmake|*.patch|.gitignore|*.bzl)
210+
*.md|*.md.in|*.json|*.yml|*.yaml|*.cmake|*.patch|*.bzl|.gitignore)
181211
echo -e "${INFO} Skipping license check for ${committed_file} (excluded extension)"
182212
continue
183213
;;
@@ -311,6 +341,8 @@ else
311341
echo -e "${INFO} Skipping Arm docgen (no public API inputs changed)"
312342
fi
313343

344+
run_public_api_validator
345+
314346
if [[ $FAILED ]]; then
315347
echo -e "${INFO} Fix your commit message errors with"\
316348
"'git commit --amend' or 'git commit --fixup=<SHA>'"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Manifests
2+
3+
Manifests are used to track the current public API of the Arm backend. They are
4+
generated with
5+
`python backends/arm/scripts/public_api_manifest/generate_public_api_manifest.py`.
6+
7+
## Running manifest
8+
9+
There is always one running manifest which has the main purpose of tracking the
10+
API surface inbetween releases.
11+
12+
## Static manifests
13+
14+
At any given time there may be up to two static manifests. These are generated
15+
in conjunction with a release and are used to track the API surface of that
16+
release. The main purpose of these is to make sure backwards compatibility.
17+
18+
A static manifest may never be changed. It belongs to a release and must be kept
19+
as is.
20+
21+
A static manifest should not live longer than 2 releases. It may then be
22+
removed.
23+
24+
# On release
25+
26+
With each release, check that the running manifest is up to date and reflects
27+
the API surface of the release. Then, copy the running manifest to a new static
28+
manifest for the release. This can be done by running
29+
`cp <running_manifest> <static_manifest>`. The new static manifest should be
30+
named according to the release, e.g. `api_manifest_1_3.toml` for release 1.3 and
31+
so on. If there are now more than two static manifests, remove the oldest one in
32+
the same commit.
33+
34+
# API changes
35+
36+
When introducing an API change, the running manifest must be updated to reflect
37+
the change. This is done by running the manifest generation script,
38+
`python backends/arm/scripts/public_api_manifest/generate_public_api_manifest.py`.
39+
This updates the running manifest.
40+
41+
To validate the running manifest directly, run
42+
`python backends/arm/scripts/public_api_manifest/validate_public_api_manifest.py`.
43+
44+
To validate all manifests, use `backends/arm/scripts/pre-push`. This is the
45+
check that must pass before the change is ready to merge.
46+
47+
Manifest validation only checks the API surface and signatures.
48+
49+
Running-manifest validation uses exact signature matching. Any intentional API
50+
change must update `api_manifest_running.toml`.
51+
52+
Static-manifest validation uses backward-compatibility matching. The old
53+
release signature must still be callable against the current API. For example,
54+
adding a trailing optional parameter is accepted for static manifests, while
55+
removing a parameter, reordering parameters, or adding a new required
56+
parameter still fails validation.
57+
58+
Reasons for passing validation may include:
59+
- Adding a new API symbol and adding it to the running manifest.
60+
- Removing an API that was marked as deprecated and no longer exists in any
61+
manifest.
62+
- Deprecated symbols do not break backward compatibility with static
63+
manifests.
64+
- Deprecating a symbol removes it from the running manifest, but it can only be
65+
removed fully once it no longer appears in any static manifest.
66+
- Extending a static-manifest signature in a backward-compatible way, such as
67+
adding a trailing optional parameter.
68+
69+
Reasons for failing validation may include:
70+
- Removing an API symbol without deprecation.
71+
- Changing a running-manifest signature without regenerating the running
72+
manifest.
73+
- Changing a static-manifest signature in a non-backward-compatible way.
74+
- New API symbol added but not added to the running manifest.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.

backends/arm/scripts/generate_public_api_manifest.py renamed to backends/arm/scripts/public_api_manifest/generate_public_api_manifest.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
2323
# LICENSE file in the root directory of this source tree.
2424
#
2525
# This file is generated by
26-
# backends/arm/scripts/generate_public_api_manifest.py
26+
# backends/arm/scripts/public_api_manifest/generate_public_api_manifest.py
2727
"""
2828
MANIFEST_PATH = (
29-
Path(__file__).resolve().parents[1]
29+
Path(__file__).resolve().parents[2]
3030
/ "public_api_manifests"
3131
/ "api_manifest_running.toml"
3232
)
@@ -81,8 +81,10 @@ def _collect_entry(
8181
path: str,
8282
obj: object,
8383
entries: dict[str, dict[str, str]],
84+
*,
85+
include_deprecated: bool = False,
8486
) -> None:
85-
if _is_unstable_api(obj):
87+
if _is_unstable_api(obj) and not include_deprecated:
8688
return
8789
entries[path] = {"kind": _api_kind(obj), "signature": _api_signature(path, obj)}
8890
if not inspect.isclass(obj):
@@ -96,13 +98,25 @@ def _collect_entry(
9698
continue
9799
member = getattr(obj, name)
98100
if inspect.isclass(member) or callable(member):
99-
_collect_entry(f"{path}.{name}", member, entries)
101+
_collect_entry(
102+
f"{path}.{name}",
103+
member,
104+
entries,
105+
include_deprecated=include_deprecated,
106+
)
100107

101108

102-
def _collect_public_api() -> dict[str, dict[str, str]]:
109+
def _collect_public_api(
110+
*, include_deprecated: bool = False
111+
) -> dict[str, dict[str, str]]:
103112
entries: dict[str, dict[str, str]] = {}
104113
for name in sorted(LAZY_IMPORTS):
105-
_collect_entry(name, getattr(arm, name), entries)
114+
_collect_entry(
115+
name,
116+
getattr(arm, name),
117+
entries,
118+
include_deprecated=include_deprecated,
119+
)
106120
return entries
107121

108122

@@ -124,9 +138,10 @@ def _render_manifest(entries: dict[str, dict[str, str]]) -> str:
124138
def generate_manifest_from_init(
125139
*,
126140
repo_path: Path | None = None,
141+
include_deprecated: bool = False,
127142
) -> str:
128143
del repo_path
129-
return _render_manifest(_collect_public_api())
144+
return _render_manifest(_collect_public_api(include_deprecated=include_deprecated))
130145

131146

132147
def main() -> None:

0 commit comments

Comments
 (0)