Skip to content

Commit bc8d90b

Browse files
committed
ENH: Add SPDX version consistency test against UpdateFromUpstream.sh
Add VerifySPDXVersions.py that cross-checks SPDX_VERSION declared in each itk-module.cmake against the tag in UpdateFromUpstream.sh. For modules with parseable version tags (v1.2.3, R_2_7_4, hdf5_1.14.5, bare semver), the test verifies consistency. Modules tracking master, commit SHAs, or custom ITK tags are skipped. This catches the case where a vendored dependency is updated via UpdateFromUpstream.sh but SPDX_VERSION is not bumped. Currently validates 8 of 16 modules with UpdateFromUpstream.sh scripts.
1 parent 2fb9d9b commit bc8d90b

2 files changed

Lines changed: 158 additions & 0 deletions

File tree

CMake/ITKSBOMValidation.cmake

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,20 @@ set_tests_properties(
8888
LABELS
8989
"SBOM"
9090
)
91+
92+
# Verify SPDX_VERSION entries match UpdateFromUpstream.sh tags.
93+
# This catches the case where a vendored dependency is updated but
94+
# the SPDX_VERSION in itk-module.cmake is not bumped.
95+
add_test(
96+
NAME ITKSBOMVersionConsistency
97+
COMMAND
98+
${Python3_EXECUTABLE}
99+
"${ITK_SOURCE_DIR}/Utilities/Maintenance/VerifySPDXVersions.py"
100+
"${ITK_SOURCE_DIR}"
101+
)
102+
set_tests_properties(
103+
ITKSBOMVersionConsistency
104+
PROPERTIES
105+
LABELS
106+
"SBOM"
107+
)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/usr/bin/env python3
2+
"""Verify SPDX_VERSION in itk-module.cmake matches UpdateFromUpstream.sh tags.
3+
4+
For each ThirdParty module that has both an UpdateFromUpstream.sh with a
5+
parseable version tag and an SPDX_VERSION in itk-module.cmake, verify
6+
they are consistent.
7+
8+
Modules tracking 'master', commit SHAs, or custom ITK tags are skipped
9+
since their version cannot be derived from the tag alone.
10+
11+
Exit code 0 if all checked modules match, 1 if any mismatch is found.
12+
"""
13+
14+
import re
15+
import sys
16+
from pathlib import Path
17+
18+
19+
def extract_tag_from_upstream_script(script_path: Path) -> str | None:
20+
"""Extract the 'tag' value from UpdateFromUpstream.sh."""
21+
text = script_path.read_text()
22+
# Match: readonly tag="..." or tag="..." or readonly tag='...'
23+
m = re.search(r"""(?:readonly\s+)?tag\s*=\s*['"]([^'"]+)['"]""", text)
24+
return m.group(1) if m else None
25+
26+
27+
def normalize_version_from_tag(tag: str) -> str | None:
28+
"""Attempt to extract a semver-like version from a git tag.
29+
30+
Returns None if the tag is a SHA, 'master', or an unrecognizable format.
31+
"""
32+
# Skip SHAs (40-hex or short)
33+
if re.fullmatch(r"[0-9a-f]{7,40}", tag):
34+
return None
35+
# Skip 'master' or 'main'
36+
if tag in ("master", "main"):
37+
return None
38+
# Skip custom ITK tags like 'for/itk-20260305-4c99fca'
39+
if tag.startswith("for/"):
40+
return None
41+
42+
# Try common patterns:
43+
# v1.2.3 or V1.2.3
44+
m = re.fullmatch(r"[vV]?(\d+\.\d+(?:\.\d+)?)", tag)
45+
if m:
46+
return m.group(1)
47+
48+
# hdf5_1.14.5
49+
m = re.fullmatch(r"[a-zA-Z0-9]+[_-](\d+\.\d+(?:\.\d+)?)", tag)
50+
if m:
51+
return m.group(1)
52+
53+
# R_2_7_4 (Expat style)
54+
m = re.fullmatch(r"R_(\d+)_(\d+)_(\d+)", tag)
55+
if m:
56+
return f"{m.group(1)}.{m.group(2)}.{m.group(3)}"
57+
58+
# Bare version like 2.2.5 or 3.0.4
59+
m = re.fullmatch(r"(\d+\.\d+(?:\.\d+)?)", tag)
60+
if m:
61+
return m.group(1)
62+
63+
return None
64+
65+
66+
def extract_spdx_version(module_cmake: Path) -> str | None:
67+
"""Extract SPDX_VERSION value from itk-module.cmake."""
68+
text = module_cmake.read_text()
69+
m = re.search(r'SPDX_VERSION\s+"([^"]+)"', text)
70+
return m.group(1) if m else None
71+
72+
73+
def main() -> int:
74+
if len(sys.argv) > 1:
75+
itk_source = Path(sys.argv[1])
76+
else:
77+
# Default: assume running from ITK source root
78+
itk_source = Path(__file__).resolve().parents[2]
79+
80+
thirdparty_dir = itk_source / "Modules" / "ThirdParty"
81+
if not thirdparty_dir.is_dir():
82+
print(f"ERROR: {thirdparty_dir} not found", file=sys.stderr)
83+
return 1
84+
85+
errors = []
86+
checked = 0
87+
skipped = 0
88+
89+
for module_dir in sorted(thirdparty_dir.iterdir()):
90+
if not module_dir.is_dir():
91+
continue
92+
93+
upstream_script = module_dir / "UpdateFromUpstream.sh"
94+
module_cmake = module_dir / "itk-module.cmake"
95+
96+
if not upstream_script.exists() or not module_cmake.exists():
97+
continue
98+
99+
tag = extract_tag_from_upstream_script(upstream_script)
100+
if tag is None:
101+
skipped += 1
102+
continue
103+
104+
expected_version = normalize_version_from_tag(tag)
105+
if expected_version is None:
106+
skipped += 1
107+
continue
108+
109+
declared_version = extract_spdx_version(module_cmake)
110+
if declared_version is None:
111+
errors.append(
112+
f"{module_dir.name}: UpdateFromUpstream.sh tag='{tag}' "
113+
f"implies version {expected_version}, but no SPDX_VERSION "
114+
f"declared in itk-module.cmake"
115+
)
116+
checked += 1
117+
continue
118+
119+
if declared_version != expected_version:
120+
errors.append(
121+
f"{module_dir.name}: SPDX_VERSION='{declared_version}' "
122+
f"does not match UpdateFromUpstream.sh tag='{tag}' "
123+
f"(expected '{expected_version}')"
124+
)
125+
126+
checked += 1
127+
128+
print(f"Checked {checked} modules, skipped {skipped} " f"(master/SHA/custom tags)")
129+
130+
if errors:
131+
print(f"\n{len(errors)} version mismatch(es):", file=sys.stderr)
132+
for e in errors:
133+
print(f" FAIL: {e}", file=sys.stderr)
134+
return 1
135+
136+
print("All SPDX_VERSION entries match UpdateFromUpstream.sh tags.")
137+
return 0
138+
139+
140+
if __name__ == "__main__":
141+
sys.exit(main())

0 commit comments

Comments
 (0)