blob: 9a5a7eed3d1ee20843132c3cb3240439d730b4c6 [file]
#!/usr/bin/env python3
# ***************************************************************************************************************************
# * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file
# * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
# * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
# * with the License. You may obtain a copy of the License at
# *
# * http://www.apache.org/licenses/LICENSE-2.0
# *
# * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
# * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
# * specific language governing permissions and limitations under the License.
# ***************************************************************************************************************************
"""
Drift guard for the published juneau-bom.
Every consumer-facing reactor module must be enumerated in juneau-bom/pom.xml's
<dependencyManagement>. This catches the common failure mode where a new
publishable module is added to the reactor but the maintainer forgets to add it
to the BOM, so downstreams importing the BOM silently can't omit its version.
A module is "consumer-facing publishable" when it is a leaf artifact
(packaging jar/bundle, or a leaf packaging=pom dependency bundle with no
<modules>) AND it is not one of the documented non-published modules:
- reactor aggregators (packaging=pom WITH <modules>) - not artifacts
- juneau-bom itself
- juneau-test-utils (test-only support)
- juneau-test-marshall-utils (test-only support, marshall-level fixtures)
- juneau-shaded/* (uber-jars, not BOM-managed)
- juneau-examples/*, juneau-petstore/* (sample apps, not published)
- juneau-integration-tests (test-only)
- juneau-distrib (assembly only)
Exit 0 when the BOM is complete (and free of stale entries); exit 1 otherwise.
"""
from __future__ import annotations
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
NS = {"m": "http://maven.apache.org/POM/4.0.0"}
# Path-prefix-based exclusions (relative to repo root).
EXCLUDED_PATH_PREFIXES = (
"juneau-shaded/",
"juneau-examples/",
"juneau-petstore/",
"juneau-distrib/",
)
# Artifact-id-based exclusions (leaf artifacts that are intentionally not published via the BOM).
EXCLUDED_ARTIFACTS = {
"juneau-bom", # the BOM cannot manage itself
"juneau-test-utils", # test-only support module
"juneau-test-marshall-utils", # test-only support module (marshall-level fixtures)
"juneau-integration-tests", # cross-module test harness, not published
"juneau", # reactor root (defensive; also an aggregator)
}
def _text(el: ET.Element, tag: str) -> str | None:
child = el.find(f"m:{tag}", NS)
return child.text.strip() if child is not None and child.text else None
def repo_root() -> Path:
# scripts/ lives directly under the reactor root.
return Path(__file__).resolve().parent.parent
def own_artifact_id(root: ET.Element) -> str | None:
"""The module's own artifactId - the direct <project><artifactId>, not the <parent> one."""
return _text(root, "artifactId")
def is_aggregator(root: ET.Element) -> bool:
"""A reactor aggregator: packaging=pom AND declares <modules>."""
packaging = _text(root, "packaging") or "jar"
has_modules = root.find("m:modules", NS) is not None
return packaging == "pom" and has_modules
def collect_publishable(root_dir: Path) -> dict[str, Path]:
"""Map of consumer-facing publishable artifactId -> pom path."""
out: dict[str, Path] = {}
for pom in sorted(root_dir.rglob("pom.xml")):
rel = pom.relative_to(root_dir).as_posix()
if "/target/" in f"/{rel}":
continue
if any(rel.startswith(p) for p in EXCLUDED_PATH_PREFIXES):
continue
try:
root = ET.parse(pom).getroot()
except ET.ParseError as e:
print(f"WARNING: could not parse {rel}: {e}", file=sys.stderr)
continue
aid = own_artifact_id(root)
if not aid or aid in EXCLUDED_ARTIFACTS:
continue
if is_aggregator(root):
continue
# Skip the integration-tests reactor module (artifactId 'juneau', already excluded) and
# any build-overlay helper poms living under an excluded tree (handled by path prefixes).
out[aid] = pom
return out
def collect_bom_entries(bom_pom: Path) -> set[str]:
root = ET.parse(bom_pom).getroot()
dm = root.find("m:dependencyManagement", NS)
if dm is None:
return set()
out: set[str] = set()
for dep in dm.iterfind("m:dependencies/m:dependency", NS):
group = _text(dep, "groupId")
if group != "org.apache.juneau":
continue
aid = _text(dep, "artifactId")
if aid:
out.add(aid)
return out
def main() -> int:
root_dir = repo_root()
bom_pom = root_dir / "juneau-bom" / "pom.xml"
if not bom_pom.is_file():
print(f"ERROR: BOM not found at {bom_pom}", file=sys.stderr)
return 1
publishable = collect_publishable(root_dir)
bom = collect_bom_entries(bom_pom)
missing = sorted(set(publishable) - bom)
stale = sorted(bom - set(publishable))
if not missing and not stale:
print(f"OK: juneau-bom enumerates all {len(publishable)} consumer-facing modules.")
return 0
if missing:
print("ERROR: the following published modules are MISSING from juneau-bom:", file=sys.stderr)
for aid in missing:
print(f" - {aid} ({publishable[aid].relative_to(root_dir).as_posix()})", file=sys.stderr)
if stale:
print("ERROR: juneau-bom lists artifacts that are NOT consumer-facing reactor modules:", file=sys.stderr)
for aid in stale:
print(f" - {aid}", file=sys.stderr)
print(
"\nFix: add the missing <dependency> entries to juneau-bom/pom.xml (or remove stale ones), "
"or update the exclusion lists in scripts/check-bom-completeness.py if the module is "
"intentionally not published.",
file=sys.stderr,
)
return 1
if __name__ == "__main__":
sys.exit(main())