From 1e59a97333b4637a1e05e2a20b49fb9530d5eead Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 25 Dec 2025 11:01:11 +0200 Subject: [PATCH 1/6] Add CLDC11 API compatibility workflow --- .../workflows/cldc11-api-compatibility.yml | 59 ++++ scripts/cldc11_api_compat_check.py | 279 ++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 .github/workflows/cldc11-api-compatibility.yml create mode 100644 scripts/cldc11_api_compat_check.py diff --git a/.github/workflows/cldc11-api-compatibility.yml b/.github/workflows/cldc11-api-compatibility.yml new file mode 100644 index 0000000000..0da726252a --- /dev/null +++ b/.github/workflows/cldc11-api-compatibility.yml @@ -0,0 +1,59 @@ +name: CLDC11 API Compatibility + +on: + push: + paths: + - 'Ports/CLDC11/**' + - 'vm/JavaAPI/**' + - '.github/workflows/cldc11-api-compatibility.yml' + - 'scripts/cldc11_api_compat_check.py' + pull_request: + paths: + - 'Ports/CLDC11/**' + - 'vm/JavaAPI/**' + - '.github/workflows/cldc11-api-compatibility.yml' + - 'scripts/cldc11_api_compat_check.py' + +jobs: + api-compatibility: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 8 for builds + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '8' + + - name: Compile CLDC11 classes + run: | + mkdir -p Ports/CLDC11/build/classes + find Ports/CLDC11/src/java -name '*.java' > cldc11-sources.txt + javac -encoding windows-1252 -source 1.6 -target 1.6 -XDignore.symbol.file \ + -d Ports/CLDC11/build/classes @cldc11-sources.txt + + - name: Build vm/JavaAPI classes + run: mvn -f vm/pom.xml -pl JavaAPI -am package -DskipTests + + - name: Set up JDK 11 for compatibility checks + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '11' + + - name: Validate CLDC11 API subset + run: | + python scripts/cldc11_api_compat_check.py \ + --cldc-classes Ports/CLDC11/build/classes \ + --javaapi-classes vm/JavaAPI/target/classes \ + --extra-report cldc11-extra-apis.txt + + - name: Upload extra API report + if: always() + uses: actions/upload-artifact@v4 + with: + name: cldc11-extra-apis + path: cldc11-extra-apis.txt diff --git a/scripts/cldc11_api_compat_check.py b/scripts/cldc11_api_compat_check.py new file mode 100644 index 0000000000..815d24e11d --- /dev/null +++ b/scripts/cldc11_api_compat_check.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +""" +Validate that the CLDC11 API surface is a binary compatible subset of Java SE 11 +and the vm/JavaAPI project. + +The script compares public and protected methods and fields using `javap -s -public`. +It expects pre-built class directories for the CLDC11 and JavaAPI projects. +""" +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from dataclasses import dataclass, field +from typing import Dict, Iterable, List, Optional, Set, Tuple + + +Member = Tuple[str, str, bool, str] +""" +A member descriptor in the form `(name, descriptor, is_static, kind)`. +`kind` is either `method` or `field`. +""" + + +@dataclass +class ApiSurface: + """Collection of public/protected API members for a class.""" + + methods: Set[Member] = field(default_factory=set) + fields: Set[Member] = field(default_factory=set) + + def add(self, member: Member) -> None: + if member[3] == "method": + self.methods.add(member) + else: + self.fields.add(member) + + def missing_from(self, other: "ApiSurface") -> Tuple[Set[Member], Set[Member]]: + return self.methods - other.methods, self.fields - other.fields + + def extras_over(self, other: "ApiSurface") -> Tuple[Set[Member], Set[Member]]: + return self.methods - other.methods, self.fields - other.fields + + +class JavapError(RuntimeError): + pass + + +def discover_classes(root: str) -> List[str]: + classes: List[str] = [] + for base, _, files in os.walk(root): + for filename in files: + if not filename.endswith(".class"): + continue + if filename in {"module-info.class", "package-info.class"}: + continue + full_path = os.path.join(base, filename) + rel_path = os.path.relpath(full_path, root) + binary_name = rel_path[:-6].replace(os.sep, ".") + classes.append(binary_name) + return classes + + +def run_javap(target: str, javap_cmd: str) -> str: + proc = subprocess.run( + [javap_cmd, "-public", "-s", target], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if proc.returncode != 0: + raise JavapError(proc.stderr.strip() or proc.stdout.strip()) + return proc.stdout + + +def parse_members(javap_output: str) -> ApiSurface: + api = ApiSurface() + pending: Optional[Tuple[str, bool, str]] = None # name, is_static, kind + + for raw_line in javap_output.splitlines(): + line = raw_line.strip() + if not line or line.startswith("Compiled from"): + continue + if line.endswith("{"): + continue + if line.startswith("descriptor:"): + if pending is None: + continue + descriptor = line.split(":", 1)[1].strip() + name, is_static, kind = pending + api.add((name, descriptor, is_static, kind)) + pending = None + continue + + if line.startswith("Runtime") or line.startswith("Signature:") or line.startswith("Exceptions:"): + pending = None + continue + + if "(" in line or line.endswith(";"): + if line.startswith("//"): + continue + if line.endswith(" class"): + continue + if line.endswith("interface"): + continue + + is_static = " static " in f" {line} " + if "(" in line: + name_section = line.split("(")[0].strip() + name = name_section.split()[-1] + kind = "method" + else: + name = line.rstrip(";").split()[-1] + kind = "field" + pending = (name, is_static, kind) + + return api + + +def collect_class_api_from_file(class_name: str, classes_root: str, javap_cmd: str) -> ApiSurface: + class_path = os.path.join(classes_root, *class_name.split(".")) + ".class" + output = run_javap(class_path, javap_cmd) + return parse_members(output) + + +def collect_class_api_from_jdk(class_name: str, javap_cmd: str) -> ApiSurface: + output = run_javap(class_name, javap_cmd) + return parse_members(output) + + +def format_member(member: Member) -> str: + name, descriptor, is_static, kind = member + static_prefix = "static " if is_static else "" + return f"{kind}: {static_prefix}{name} {descriptor}" + + +def ensure_subset( + source_classes: List[str], + source_root: str, + target_lookup, + target_label: str, + javap_cmd: str, +) -> Tuple[bool, List[str]]: + ok = True + messages: List[str] = [] + + for class_name in sorted(source_classes): + try: + source_api = collect_class_api_from_file(class_name, source_root, javap_cmd) + except JavapError as exc: + ok = False + messages.append(f"Failed to read {class_name} from {source_root}: {exc}") + continue + + target_api = target_lookup(class_name) + if target_api is None: + ok = False + messages.append(f"Missing class in {target_label}: {class_name}") + continue + + missing_methods, missing_fields = source_api.missing_from(target_api) + if missing_methods or missing_fields: + ok = False + messages.append(f"Incompatibilities for {class_name} against {target_label}:") + for member in sorted(missing_methods | missing_fields): + messages.append(f" - {format_member(member)}") + + return ok, messages + + +def collect_javaapi_map(javaapi_root: str, javap_cmd: str) -> Dict[str, ApiSurface]: + classes = discover_classes(javaapi_root) + api_map: Dict[str, ApiSurface] = {} + for class_name in classes: + api_map[class_name] = collect_class_api_from_file(class_name, javaapi_root, javap_cmd) + return api_map + + +def write_extra_report( + cldc_classes: Dict[str, ApiSurface], + javaapi_classes: Dict[str, ApiSurface], + report_path: str, +) -> None: + lines: List[str] = [ + "Extra APIs present in vm/JavaAPI but not in CLDC11", + "", + ] + + extra_classes = sorted(set(javaapi_classes) - set(cldc_classes)) + if extra_classes: + lines.append("Classes only in vm/JavaAPI:") + lines.extend([f" - {name}" for name in extra_classes]) + lines.append("") + + shared_classes = set(javaapi_classes) & set(cldc_classes) + extra_members: List[str] = [] + for class_name in sorted(shared_classes): + javaapi_api = javaapi_classes[class_name] + cldc_api = cldc_classes[class_name] + extra_methods, extra_fields = javaapi_api.extras_over(cldc_api) + if not extra_methods and not extra_fields: + continue + extra_members.append(class_name) + extra_members.append("") + for member in sorted(extra_methods | extra_fields): + extra_members.append(f" + {format_member(member)}") + extra_members.append("") + + if extra_members: + lines.append("Additional members on classes shared between vm/JavaAPI and CLDC11:") + lines.append("") + lines.extend(extra_members) + + if len(lines) == 2: + lines.append("No extra APIs detected.") + + os.makedirs(os.path.dirname(os.path.abspath(report_path)) or ".", exist_ok=True) + with open(report_path, "w", encoding="utf-8") as handle: + handle.write("\n".join(lines).rstrip() + "\n") + + +def main(argv: Optional[Iterable[str]] = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cldc-classes", required=True, help="Path to compiled CLDC11 classes directory") + parser.add_argument("--javaapi-classes", required=True, help="Path to compiled vm/JavaAPI classes directory") + parser.add_argument("--extra-report", required=True, help="File path to write the extra API report") + parser.add_argument( + "--javap", + default=os.path.join(os.environ.get("JAVA_HOME", ""), "bin", "javap"), + help="Path to the javap executable from Java SE 11", + ) + args = parser.parse_args(argv) + + javap_cmd = args.javap or "javap" + + cldc_classes = discover_classes(args.cldc_classes) + if not cldc_classes: + print(f"No class files found under {args.cldc_classes}", file=sys.stderr) + return 1 + + javaapi_map = collect_javaapi_map(args.javaapi_classes, javap_cmd) + + def jdk_lookup(name: str) -> Optional[ApiSurface]: + try: + return collect_class_api_from_jdk(name, javap_cmd) + except JavapError: + return None + + def javaapi_lookup(name: str) -> Optional[ApiSurface]: + return javaapi_map.get(name) + + java_ok, java_messages = ensure_subset( + cldc_classes, + args.cldc_classes, + jdk_lookup, + "Java SE 11", + javap_cmd, + ) + + api_ok, api_messages = ensure_subset( + cldc_classes, + args.cldc_classes, + javaapi_lookup, + "vm/JavaAPI", + javap_cmd, + ) + + cldc_map = {name: collect_class_api_from_file(name, args.cldc_classes, javap_cmd) for name in cldc_classes} + write_extra_report(cldc_map, javaapi_map, args.extra_report) + + messages = java_messages + api_messages + if messages: + print("\n".join(messages), file=sys.stderr) + return 0 if java_ok and api_ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From c61e7bac969c90720923d289f3208c46e95e2b98 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 25 Dec 2025 13:41:00 +0200 Subject: [PATCH 2/6] Improve CLDC11 API compatibility workflow --- .../scripts}/cldc11_api_compat_check.py | 27 ++++++++++++++++--- .../workflows/cldc11-api-compatibility.yml | 9 ++++--- 2 files changed, 30 insertions(+), 6 deletions(-) rename {scripts => .github/scripts}/cldc11_api_compat_check.py (90%) diff --git a/scripts/cldc11_api_compat_check.py b/.github/scripts/cldc11_api_compat_check.py similarity index 90% rename from scripts/cldc11_api_compat_check.py rename to .github/scripts/cldc11_api_compat_check.py index 815d24e11d..7dbf144136 100644 --- a/scripts/cldc11_api_compat_check.py +++ b/.github/scripts/cldc11_api_compat_check.py @@ -16,6 +16,12 @@ from typing import Dict, Iterable, List, Optional, Set, Tuple +def log(message: str) -> None: + """Emit a progress message immediately.""" + + print(message, flush=True) + + Member = Tuple[str, str, bool, str] """ A member descriptor in the form `(name, descriptor, is_static, kind)`. @@ -145,7 +151,7 @@ def ensure_subset( ok = True messages: List[str] = [] - for class_name in sorted(source_classes): + for index, class_name in enumerate(sorted(source_classes), start=1): try: source_api = collect_class_api_from_file(class_name, source_root, javap_cmd) except JavapError as exc: @@ -153,6 +159,9 @@ def ensure_subset( messages.append(f"Failed to read {class_name} from {source_root}: {exc}") continue + if index % 25 == 0: + log(f" Processed {index}/{len(source_classes)} classes for {target_label} subset check...") + target_api = target_lookup(class_name) if target_api is None: ok = False @@ -172,8 +181,10 @@ def ensure_subset( def collect_javaapi_map(javaapi_root: str, javap_cmd: str) -> Dict[str, ApiSurface]: classes = discover_classes(javaapi_root) api_map: Dict[str, ApiSurface] = {} - for class_name in classes: + for index, class_name in enumerate(classes, start=1): api_map[class_name] = collect_class_api_from_file(class_name, javaapi_root, javap_cmd) + if index % 25 == 0: + log(f" Indexed {index}/{len(classes)} vm/JavaAPI classes...") return api_map @@ -227,7 +238,11 @@ def main(argv: Optional[Iterable[str]] = None) -> int: parser.add_argument("--extra-report", required=True, help="File path to write the extra API report") parser.add_argument( "--javap", - default=os.path.join(os.environ.get("JAVA_HOME", ""), "bin", "javap"), + default=( + os.path.join(os.environ.get("JAVA_HOME", ""), "bin", "javap") + if os.environ.get("JAVA_HOME") + else "javap" + ), help="Path to the javap executable from Java SE 11", ) args = parser.parse_args(argv) @@ -239,7 +254,10 @@ def main(argv: Optional[Iterable[str]] = None) -> int: print(f"No class files found under {args.cldc_classes}", file=sys.stderr) return 1 + log(f"Discovered {len(cldc_classes)} CLDC11 classes; building API maps...") + javaapi_map = collect_javaapi_map(args.javaapi_classes, javap_cmd) + log(f"Collected API surface for {len(javaapi_map)} vm/JavaAPI classes") def jdk_lookup(name: str) -> Optional[ApiSurface]: try: @@ -250,6 +268,7 @@ def jdk_lookup(name: str) -> Optional[ApiSurface]: def javaapi_lookup(name: str) -> Optional[ApiSurface]: return javaapi_map.get(name) + log("Validating CLDC11 API against Java SE 11...") java_ok, java_messages = ensure_subset( cldc_classes, args.cldc_classes, @@ -258,6 +277,7 @@ def javaapi_lookup(name: str) -> Optional[ApiSurface]: javap_cmd, ) + log("Validating CLDC11 API against vm/JavaAPI...") api_ok, api_messages = ensure_subset( cldc_classes, args.cldc_classes, @@ -268,6 +288,7 @@ def javaapi_lookup(name: str) -> Optional[ApiSurface]: cldc_map = {name: collect_class_api_from_file(name, args.cldc_classes, javap_cmd) for name in cldc_classes} write_extra_report(cldc_map, javaapi_map, args.extra_report) + log(f"Wrote extra API report to {args.extra_report}") messages = java_messages + api_messages if messages: diff --git a/.github/workflows/cldc11-api-compatibility.yml b/.github/workflows/cldc11-api-compatibility.yml index 0da726252a..5b263a0f9e 100644 --- a/.github/workflows/cldc11-api-compatibility.yml +++ b/.github/workflows/cldc11-api-compatibility.yml @@ -2,21 +2,24 @@ name: CLDC11 API Compatibility on: push: + branches: [master] paths: - 'Ports/CLDC11/**' - 'vm/JavaAPI/**' - '.github/workflows/cldc11-api-compatibility.yml' - - 'scripts/cldc11_api_compat_check.py' + - '.github/scripts/cldc11_api_compat_check.py' pull_request: paths: - 'Ports/CLDC11/**' - 'vm/JavaAPI/**' - '.github/workflows/cldc11-api-compatibility.yml' - - 'scripts/cldc11_api_compat_check.py' + - '.github/scripts/cldc11_api_compat_check.py' jobs: api-compatibility: runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout repository @@ -46,7 +49,7 @@ jobs: - name: Validate CLDC11 API subset run: | - python scripts/cldc11_api_compat_check.py \ + python .github/scripts/cldc11_api_compat_check.py \ --cldc-classes Ports/CLDC11/build/classes \ --javaapi-classes vm/JavaAPI/target/classes \ --extra-report cldc11-extra-apis.txt From 29bb62ea032e666db658a304d19cb3eb615e1cd1 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 25 Dec 2025 13:41:04 +0200 Subject: [PATCH 3/6] Handle inherited public APIs in CLDC11 checker --- .github/scripts/cldc11_api_compat_check.py | 215 +++++++++++++++++---- 1 file changed, 176 insertions(+), 39 deletions(-) diff --git a/.github/scripts/cldc11_api_compat_check.py b/.github/scripts/cldc11_api_compat_check.py index 7dbf144136..e3aa4a4800 100644 --- a/.github/scripts/cldc11_api_compat_check.py +++ b/.github/scripts/cldc11_api_compat_check.py @@ -10,6 +10,7 @@ import argparse import os +import re import subprocess import sys from dataclasses import dataclass, field @@ -53,6 +54,17 @@ class JavapError(RuntimeError): pass +@dataclass +class ClassInfo: + """Metadata about a compiled class.""" + + name: str + api: ApiSurface + supers: List[str] + is_public: bool + kind: str + + def discover_classes(root: str) -> List[str]: classes: List[str] = [] for base, _, files in os.walk(root): @@ -124,15 +136,95 @@ def parse_members(javap_output: str) -> ApiSurface: return api -def collect_class_api_from_file(class_name: str, classes_root: str, javap_cmd: str) -> ApiSurface: +def parse_class_info(javap_output: str) -> ClassInfo: + api = ApiSurface() + supers: List[str] = [] + pending: Optional[Tuple[str, bool, str]] = None + class_name: Optional[str] = None + is_public = False + kind = "class" + + header_pattern = re.compile( + r"(?Ppublic|protected)?\s*(?Pclass|interface|enum)\s+", + r"(?P[\w.$]+)", + r"(?:\s+extends\s+(?P[^\{]+?))?", + r"(?:\s+implements\s+(?P[^\{]+))?", + ) + + for raw_line in javap_output.splitlines(): + line = raw_line.strip() + if not line or line.startswith("Compiled from"): + continue + + if class_name is None: + match = header_pattern.search(line.rstrip("{")) + if match: + class_name = match.group("name") + kind = match.group("kind") + is_public = match.group("visibility") == "public" + extends_clause = match.group("extends") + implements_clause = match.group("implements") + for clause in (extends_clause, implements_clause): + if not clause: + continue + supers.extend([part.strip() for part in clause.split(',') if part.strip()]) + continue + + if line.endswith("{"): + continue + if line.startswith("descriptor:"): + if pending is None: + continue + descriptor = line.split(":", 1)[1].strip() + name, is_static, kind = pending + api.add((name, descriptor, is_static, kind)) + pending = None + continue + + if line.startswith("Runtime") or line.startswith("Signature:") or line.startswith("Exceptions:"): + pending = None + continue + + if "(" in line or line.endswith(";"): + if line.startswith("//"): + continue + if line.endswith(" class"): + continue + if line.endswith("interface"): + continue + + is_static_member = " static " in f" {line} " + if "(" in line: + name_section = line.split("(")[0].strip() + name = name_section.split()[-1] + kind = "method" + else: + name = line.rstrip(";").split()[-1] + kind = "field" + pending = (name, is_static_member, kind) + + if class_name is None: + raise ValueError("Unable to determine class name from javap output") + + if not supers and kind == "class" and class_name != "java.lang.Object": + supers.append("java.lang.Object") + + return ClassInfo(name=class_name, api=api, supers=supers, is_public=is_public, kind=kind) + +def collect_class_info_from_file(class_name: str, classes_root: str, javap_cmd: str) -> Optional[ClassInfo]: class_path = os.path.join(classes_root, *class_name.split(".")) + ".class" + if not os.path.exists(class_path): + return None output = run_javap(class_path, javap_cmd) - return parse_members(output) + return parse_class_info(output) -def collect_class_api_from_jdk(class_name: str, javap_cmd: str) -> ApiSurface: - output = run_javap(class_name, javap_cmd) - return parse_members(output) +def collect_class_info_from_jdk(class_name: str, javap_cmd: str) -> Optional[ClassInfo]: + try: + output = run_javap(class_name, javap_cmd) + except JavapError: + return None + return parse_class_info(output) def format_member(member: Member) -> str: @@ -141,28 +233,52 @@ def format_member(member: Member) -> str: return f"{kind}: {static_prefix}{name} {descriptor}" +def build_full_api( + class_name: str, + lookup, + cache: Dict[str, Optional[ApiSurface]], +) -> Optional[ApiSurface]: + if class_name in cache: + return cache[class_name] + + info = lookup(class_name) + if info is None: + cache[class_name] = None + return None + + merged = ApiSurface(set(info.api.methods), set(info.api.fields)) + for parent in info.supers: + parent_api = build_full_api(parent, lookup, cache) + if parent_api: + merged.methods |= parent_api.methods + merged.fields |= parent_api.fields + + cache[class_name] = merged + return merged + + def ensure_subset( source_classes: List[str], - source_root: str, + source_lookup, target_lookup, target_label: str, - javap_cmd: str, ) -> Tuple[bool, List[str]]: ok = True messages: List[str] = [] + source_cache: Dict[str, Optional[ApiSurface]] = {} + target_cache: Dict[str, Optional[ApiSurface]] = {} for index, class_name in enumerate(sorted(source_classes), start=1): - try: - source_api = collect_class_api_from_file(class_name, source_root, javap_cmd) - except JavapError as exc: + source_api = build_full_api(class_name, source_lookup, source_cache) + if source_api is None: ok = False - messages.append(f"Failed to read {class_name} from {source_root}: {exc}") + messages.append(f"Failed to read {class_name} from source classes") continue if index % 25 == 0: log(f" Processed {index}/{len(source_classes)} classes for {target_label} subset check...") - target_api = target_lookup(class_name) + target_api = build_full_api(class_name, target_lookup, target_cache) if target_api is None: ok = False messages.append(f"Missing class in {target_label}: {class_name}") @@ -178,19 +294,22 @@ def ensure_subset( return ok, messages -def collect_javaapi_map(javaapi_root: str, javap_cmd: str) -> Dict[str, ApiSurface]: +def collect_javaapi_map(javaapi_root: str, javap_cmd: str) -> Dict[str, ClassInfo]: classes = discover_classes(javaapi_root) - api_map: Dict[str, ApiSurface] = {} + api_map: Dict[str, ClassInfo] = {} for index, class_name in enumerate(classes, start=1): - api_map[class_name] = collect_class_api_from_file(class_name, javaapi_root, javap_cmd) + info = collect_class_info_from_file(class_name, javaapi_root, javap_cmd) + if info: + api_map[class_name] = info if index % 25 == 0: log(f" Indexed {index}/{len(classes)} vm/JavaAPI classes...") return api_map def write_extra_report( - cldc_classes: Dict[str, ApiSurface], - javaapi_classes: Dict[str, ApiSurface], + cldc_classes: Dict[str, ClassInfo], + javaapi_classes: Dict[str, ClassInfo], + public_cldc: Set[str], report_path: str, ) -> None: lines: List[str] = [ @@ -198,17 +317,21 @@ def write_extra_report( "", ] - extra_classes = sorted(set(javaapi_classes) - set(cldc_classes)) + extra_classes = sorted( + name + for name, info in javaapi_classes.items() + if info.is_public and name not in public_cldc + ) if extra_classes: lines.append("Classes only in vm/JavaAPI:") lines.extend([f" - {name}" for name in extra_classes]) lines.append("") - shared_classes = set(javaapi_classes) & set(cldc_classes) + shared_classes = {name for name in javaapi_classes if name in public_cldc and javaapi_classes[name].is_public} extra_members: List[str] = [] for class_name in sorted(shared_classes): - javaapi_api = javaapi_classes[class_name] - cldc_api = cldc_classes[class_name] + javaapi_api = javaapi_classes[class_name].api + cldc_api = cldc_classes[class_name].api extra_methods, extra_fields = javaapi_api.extras_over(cldc_api) if not extra_methods and not extra_fields: continue @@ -249,45 +372,59 @@ def main(argv: Optional[Iterable[str]] = None) -> int: javap_cmd = args.javap or "javap" - cldc_classes = discover_classes(args.cldc_classes) - if not cldc_classes: + cldc_class_names = discover_classes(args.cldc_classes) + if not cldc_class_names: print(f"No class files found under {args.cldc_classes}", file=sys.stderr) return 1 - log(f"Discovered {len(cldc_classes)} CLDC11 classes; building API maps...") + log(f"Discovered {len(cldc_class_names)} CLDC11 classes; building API maps...") javaapi_map = collect_javaapi_map(args.javaapi_classes, javap_cmd) log(f"Collected API surface for {len(javaapi_map)} vm/JavaAPI classes") - def jdk_lookup(name: str) -> Optional[ApiSurface]: - try: - return collect_class_api_from_jdk(name, javap_cmd) - except JavapError: - return None + cldc_lookup_cache: Dict[str, Optional[ClassInfo]] = {} + java_lookup_cache: Dict[str, Optional[ClassInfo]] = {name: info for name, info in javaapi_map.items()} + jdk_lookup_cache: Dict[str, Optional[ClassInfo]] = {} + + def cldc_lookup(name: str) -> Optional[ClassInfo]: + if name not in cldc_lookup_cache: + cldc_lookup_cache[name] = collect_class_info_from_file(name, args.cldc_classes, javap_cmd) + return cldc_lookup_cache[name] + + def jdk_lookup(name: str) -> Optional[ClassInfo]: + if name not in jdk_lookup_cache: + jdk_lookup_cache[name] = collect_class_info_from_jdk(name, javap_cmd) + return jdk_lookup_cache[name] + + def javaapi_lookup(name: str) -> Optional[ClassInfo]: + return java_lookup_cache.get(name) - def javaapi_lookup(name: str) -> Optional[ApiSurface]: - return javaapi_map.get(name) + public_cldc_classes = [ + name for name in cldc_class_names if (cldc_lookup(name) and cldc_lookup_cache[name].is_public) + ] + + if not public_cldc_classes: + print("No public classes discovered in CLDC11 output", file=sys.stderr) + return 1 log("Validating CLDC11 API against Java SE 11...") java_ok, java_messages = ensure_subset( - cldc_classes, - args.cldc_classes, + public_cldc_classes, + cldc_lookup, jdk_lookup, "Java SE 11", - javap_cmd, ) log("Validating CLDC11 API against vm/JavaAPI...") api_ok, api_messages = ensure_subset( - cldc_classes, - args.cldc_classes, + public_cldc_classes, + cldc_lookup, javaapi_lookup, "vm/JavaAPI", - javap_cmd, ) - cldc_map = {name: collect_class_api_from_file(name, args.cldc_classes, javap_cmd) for name in cldc_classes} - write_extra_report(cldc_map, javaapi_map, args.extra_report) + cldc_map = {name: info for name, info in cldc_lookup_cache.items() if info is not None} + write_extra_report(cldc_map, javaapi_map, set(public_cldc_classes), args.extra_report) log(f"Wrote extra API report to {args.extra_report}") messages = java_messages + api_messages From 6e685c429a19f009a44c3e550e2e9492bd06cf49 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 25 Dec 2025 14:04:12 +0200 Subject: [PATCH 4/6] Fix regex construction in CLDC11 checker --- .github/scripts/cldc11_api_compat_check.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/cldc11_api_compat_check.py b/.github/scripts/cldc11_api_compat_check.py index e3aa4a4800..cf49427ef4 100644 --- a/.github/scripts/cldc11_api_compat_check.py +++ b/.github/scripts/cldc11_api_compat_check.py @@ -145,9 +145,9 @@ def parse_class_info(javap_output: str) -> ClassInfo: kind = "class" header_pattern = re.compile( - r"(?Ppublic|protected)?\s*(?Pclass|interface|enum)\s+", - r"(?P[\w.$]+)", - r"(?:\s+extends\s+(?P[^\{]+?))?", + r"(?Ppublic|protected)?\s*(?Pclass|interface|enum)\s+" + r"(?P[\w.$]+)" + r"(?:\s+extends\s+(?P[^\{]+?))?" r"(?:\s+implements\s+(?P[^\{]+))?", ) From 1a2edbd5761433e9b8e20f68906af3c00761fa97 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 25 Dec 2025 18:51:32 +0200 Subject: [PATCH 5/6] Fix extends parsing in CLDC11 compatibility checker --- .github/scripts/cldc11_api_compat_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/cldc11_api_compat_check.py b/.github/scripts/cldc11_api_compat_check.py index cf49427ef4..6dd313ca44 100644 --- a/.github/scripts/cldc11_api_compat_check.py +++ b/.github/scripts/cldc11_api_compat_check.py @@ -147,7 +147,7 @@ def parse_class_info(javap_output: str) -> ClassInfo: header_pattern = re.compile( r"(?Ppublic|protected)?\s*(?Pclass|interface|enum)\s+" r"(?P[\w.$]+)" - r"(?:\s+extends\s+(?P[^\{]+?))?" + r"(?:\s+extends\s+(?P[^\{]+?)(?=\s+implements|$))?" r"(?:\s+implements\s+(?P[^\{]+))?", ) From 066f8019b32986c22a30daacc35e0ccf7b69aca0 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:10:19 +0200 Subject: [PATCH 6/6] Detect unexpected supertypes in CLDC11 compatibility check --- .github/scripts/cldc11_api_compat_check.py | 23 +++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/scripts/cldc11_api_compat_check.py b/.github/scripts/cldc11_api_compat_check.py index 6dd313ca44..02e17d5f88 100644 --- a/.github/scripts/cldc11_api_compat_check.py +++ b/.github/scripts/cldc11_api_compat_check.py @@ -269,19 +269,36 @@ def ensure_subset( target_cache: Dict[str, Optional[ApiSurface]] = {} for index, class_name in enumerate(sorted(source_classes), start=1): - source_api = build_full_api(class_name, source_lookup, source_cache) - if source_api is None: + source_info = source_lookup(class_name) + if source_info is None: ok = False messages.append(f"Failed to read {class_name} from source classes") continue + source_api = build_full_api(class_name, source_lookup, source_cache) + assert source_api is not None + if index % 25 == 0: log(f" Processed {index}/{len(source_classes)} classes for {target_label} subset check...") + target_info = target_lookup(class_name) + if target_info is None: + ok = False + messages.append(f"Missing class in {target_label}: {class_name}") + continue + target_api = build_full_api(class_name, target_lookup, target_cache) if target_api is None: ok = False - messages.append(f"Missing class in {target_label}: {class_name}") + messages.append(f"Failed to read {class_name} from {target_label}") + continue + + extra_supers = [stype for stype in source_info.supers if stype not in target_info.supers] + if extra_supers: + ok = False + messages.append(f"Incompatibilities for {class_name} against {target_label}:") + for stype in extra_supers: + messages.append(f" - supertype: {stype} not present in {target_label} declaration") continue missing_methods, missing_fields = source_api.missing_from(target_api)