|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Validate that the CLDC11 API surface is a binary compatible subset of Java SE 11 |
| 4 | +and the vm/JavaAPI project. |
| 5 | +
|
| 6 | +The script compares public and protected methods and fields using `javap -s -public`. |
| 7 | +It expects pre-built class directories for the CLDC11 and JavaAPI projects. |
| 8 | +""" |
| 9 | +from __future__ import annotations |
| 10 | + |
| 11 | +import argparse |
| 12 | +import os |
| 13 | +import subprocess |
| 14 | +import sys |
| 15 | +from dataclasses import dataclass, field |
| 16 | +from typing import Dict, Iterable, List, Optional, Set, Tuple |
| 17 | + |
| 18 | + |
| 19 | +Member = Tuple[str, str, bool, str] |
| 20 | +""" |
| 21 | +A member descriptor in the form `(name, descriptor, is_static, kind)`. |
| 22 | +`kind` is either `method` or `field`. |
| 23 | +""" |
| 24 | + |
| 25 | + |
| 26 | +@dataclass |
| 27 | +class ApiSurface: |
| 28 | + """Collection of public/protected API members for a class.""" |
| 29 | + |
| 30 | + methods: Set[Member] = field(default_factory=set) |
| 31 | + fields: Set[Member] = field(default_factory=set) |
| 32 | + |
| 33 | + def add(self, member: Member) -> None: |
| 34 | + if member[3] == "method": |
| 35 | + self.methods.add(member) |
| 36 | + else: |
| 37 | + self.fields.add(member) |
| 38 | + |
| 39 | + def missing_from(self, other: "ApiSurface") -> Tuple[Set[Member], Set[Member]]: |
| 40 | + return self.methods - other.methods, self.fields - other.fields |
| 41 | + |
| 42 | + def extras_over(self, other: "ApiSurface") -> Tuple[Set[Member], Set[Member]]: |
| 43 | + return self.methods - other.methods, self.fields - other.fields |
| 44 | + |
| 45 | + |
| 46 | +class JavapError(RuntimeError): |
| 47 | + pass |
| 48 | + |
| 49 | + |
| 50 | +def discover_classes(root: str) -> List[str]: |
| 51 | + classes: List[str] = [] |
| 52 | + for base, _, files in os.walk(root): |
| 53 | + for filename in files: |
| 54 | + if not filename.endswith(".class"): |
| 55 | + continue |
| 56 | + if filename in {"module-info.class", "package-info.class"}: |
| 57 | + continue |
| 58 | + full_path = os.path.join(base, filename) |
| 59 | + rel_path = os.path.relpath(full_path, root) |
| 60 | + binary_name = rel_path[:-6].replace(os.sep, ".") |
| 61 | + classes.append(binary_name) |
| 62 | + return classes |
| 63 | + |
| 64 | + |
| 65 | +def run_javap(target: str, javap_cmd: str) -> str: |
| 66 | + proc = subprocess.run( |
| 67 | + [javap_cmd, "-public", "-s", target], |
| 68 | + stdout=subprocess.PIPE, |
| 69 | + stderr=subprocess.PIPE, |
| 70 | + text=True, |
| 71 | + ) |
| 72 | + if proc.returncode != 0: |
| 73 | + raise JavapError(proc.stderr.strip() or proc.stdout.strip()) |
| 74 | + return proc.stdout |
| 75 | + |
| 76 | + |
| 77 | +def parse_members(javap_output: str) -> ApiSurface: |
| 78 | + api = ApiSurface() |
| 79 | + pending: Optional[Tuple[str, bool, str]] = None # name, is_static, kind |
| 80 | + |
| 81 | + for raw_line in javap_output.splitlines(): |
| 82 | + line = raw_line.strip() |
| 83 | + if not line or line.startswith("Compiled from"): |
| 84 | + continue |
| 85 | + if line.endswith("{"): |
| 86 | + continue |
| 87 | + if line.startswith("descriptor:"): |
| 88 | + if pending is None: |
| 89 | + continue |
| 90 | + descriptor = line.split(":", 1)[1].strip() |
| 91 | + name, is_static, kind = pending |
| 92 | + api.add((name, descriptor, is_static, kind)) |
| 93 | + pending = None |
| 94 | + continue |
| 95 | + |
| 96 | + if line.startswith("Runtime") or line.startswith("Signature:") or line.startswith("Exceptions:"): |
| 97 | + pending = None |
| 98 | + continue |
| 99 | + |
| 100 | + if "(" in line or line.endswith(";"): |
| 101 | + if line.startswith("//"): |
| 102 | + continue |
| 103 | + if line.endswith(" class"): |
| 104 | + continue |
| 105 | + if line.endswith("interface"): |
| 106 | + continue |
| 107 | + |
| 108 | + is_static = " static " in f" {line} " |
| 109 | + if "(" in line: |
| 110 | + name_section = line.split("(")[0].strip() |
| 111 | + name = name_section.split()[-1] |
| 112 | + kind = "method" |
| 113 | + else: |
| 114 | + name = line.rstrip(";").split()[-1] |
| 115 | + kind = "field" |
| 116 | + pending = (name, is_static, kind) |
| 117 | + |
| 118 | + return api |
| 119 | + |
| 120 | + |
| 121 | +def collect_class_api_from_file(class_name: str, classes_root: str, javap_cmd: str) -> ApiSurface: |
| 122 | + class_path = os.path.join(classes_root, *class_name.split(".")) + ".class" |
| 123 | + output = run_javap(class_path, javap_cmd) |
| 124 | + return parse_members(output) |
| 125 | + |
| 126 | + |
| 127 | +def collect_class_api_from_jdk(class_name: str, javap_cmd: str) -> ApiSurface: |
| 128 | + output = run_javap(class_name, javap_cmd) |
| 129 | + return parse_members(output) |
| 130 | + |
| 131 | + |
| 132 | +def format_member(member: Member) -> str: |
| 133 | + name, descriptor, is_static, kind = member |
| 134 | + static_prefix = "static " if is_static else "" |
| 135 | + return f"{kind}: {static_prefix}{name} {descriptor}" |
| 136 | + |
| 137 | + |
| 138 | +def ensure_subset( |
| 139 | + source_classes: List[str], |
| 140 | + source_root: str, |
| 141 | + target_lookup, |
| 142 | + target_label: str, |
| 143 | + javap_cmd: str, |
| 144 | +) -> Tuple[bool, List[str]]: |
| 145 | + ok = True |
| 146 | + messages: List[str] = [] |
| 147 | + |
| 148 | + for class_name in sorted(source_classes): |
| 149 | + try: |
| 150 | + source_api = collect_class_api_from_file(class_name, source_root, javap_cmd) |
| 151 | + except JavapError as exc: |
| 152 | + ok = False |
| 153 | + messages.append(f"Failed to read {class_name} from {source_root}: {exc}") |
| 154 | + continue |
| 155 | + |
| 156 | + target_api = target_lookup(class_name) |
| 157 | + if target_api is None: |
| 158 | + ok = False |
| 159 | + messages.append(f"Missing class in {target_label}: {class_name}") |
| 160 | + continue |
| 161 | + |
| 162 | + missing_methods, missing_fields = source_api.missing_from(target_api) |
| 163 | + if missing_methods or missing_fields: |
| 164 | + ok = False |
| 165 | + messages.append(f"Incompatibilities for {class_name} against {target_label}:") |
| 166 | + for member in sorted(missing_methods | missing_fields): |
| 167 | + messages.append(f" - {format_member(member)}") |
| 168 | + |
| 169 | + return ok, messages |
| 170 | + |
| 171 | + |
| 172 | +def collect_javaapi_map(javaapi_root: str, javap_cmd: str) -> Dict[str, ApiSurface]: |
| 173 | + classes = discover_classes(javaapi_root) |
| 174 | + api_map: Dict[str, ApiSurface] = {} |
| 175 | + for class_name in classes: |
| 176 | + api_map[class_name] = collect_class_api_from_file(class_name, javaapi_root, javap_cmd) |
| 177 | + return api_map |
| 178 | + |
| 179 | + |
| 180 | +def write_extra_report( |
| 181 | + cldc_classes: Dict[str, ApiSurface], |
| 182 | + javaapi_classes: Dict[str, ApiSurface], |
| 183 | + report_path: str, |
| 184 | +) -> None: |
| 185 | + lines: List[str] = [ |
| 186 | + "Extra APIs present in vm/JavaAPI but not in CLDC11", |
| 187 | + "", |
| 188 | + ] |
| 189 | + |
| 190 | + extra_classes = sorted(set(javaapi_classes) - set(cldc_classes)) |
| 191 | + if extra_classes: |
| 192 | + lines.append("Classes only in vm/JavaAPI:") |
| 193 | + lines.extend([f" - {name}" for name in extra_classes]) |
| 194 | + lines.append("") |
| 195 | + |
| 196 | + shared_classes = set(javaapi_classes) & set(cldc_classes) |
| 197 | + extra_members: List[str] = [] |
| 198 | + for class_name in sorted(shared_classes): |
| 199 | + javaapi_api = javaapi_classes[class_name] |
| 200 | + cldc_api = cldc_classes[class_name] |
| 201 | + extra_methods, extra_fields = javaapi_api.extras_over(cldc_api) |
| 202 | + if not extra_methods and not extra_fields: |
| 203 | + continue |
| 204 | + extra_members.append(class_name) |
| 205 | + extra_members.append("") |
| 206 | + for member in sorted(extra_methods | extra_fields): |
| 207 | + extra_members.append(f" + {format_member(member)}") |
| 208 | + extra_members.append("") |
| 209 | + |
| 210 | + if extra_members: |
| 211 | + lines.append("Additional members on classes shared between vm/JavaAPI and CLDC11:") |
| 212 | + lines.append("") |
| 213 | + lines.extend(extra_members) |
| 214 | + |
| 215 | + if len(lines) == 2: |
| 216 | + lines.append("No extra APIs detected.") |
| 217 | + |
| 218 | + os.makedirs(os.path.dirname(os.path.abspath(report_path)) or ".", exist_ok=True) |
| 219 | + with open(report_path, "w", encoding="utf-8") as handle: |
| 220 | + handle.write("\n".join(lines).rstrip() + "\n") |
| 221 | + |
| 222 | + |
| 223 | +def main(argv: Optional[Iterable[str]] = None) -> int: |
| 224 | + parser = argparse.ArgumentParser(description=__doc__) |
| 225 | + parser.add_argument("--cldc-classes", required=True, help="Path to compiled CLDC11 classes directory") |
| 226 | + parser.add_argument("--javaapi-classes", required=True, help="Path to compiled vm/JavaAPI classes directory") |
| 227 | + parser.add_argument("--extra-report", required=True, help="File path to write the extra API report") |
| 228 | + parser.add_argument( |
| 229 | + "--javap", |
| 230 | + default=os.path.join(os.environ.get("JAVA_HOME", ""), "bin", "javap"), |
| 231 | + help="Path to the javap executable from Java SE 11", |
| 232 | + ) |
| 233 | + args = parser.parse_args(argv) |
| 234 | + |
| 235 | + javap_cmd = args.javap or "javap" |
| 236 | + |
| 237 | + cldc_classes = discover_classes(args.cldc_classes) |
| 238 | + if not cldc_classes: |
| 239 | + print(f"No class files found under {args.cldc_classes}", file=sys.stderr) |
| 240 | + return 1 |
| 241 | + |
| 242 | + javaapi_map = collect_javaapi_map(args.javaapi_classes, javap_cmd) |
| 243 | + |
| 244 | + def jdk_lookup(name: str) -> Optional[ApiSurface]: |
| 245 | + try: |
| 246 | + return collect_class_api_from_jdk(name, javap_cmd) |
| 247 | + except JavapError: |
| 248 | + return None |
| 249 | + |
| 250 | + def javaapi_lookup(name: str) -> Optional[ApiSurface]: |
| 251 | + return javaapi_map.get(name) |
| 252 | + |
| 253 | + java_ok, java_messages = ensure_subset( |
| 254 | + cldc_classes, |
| 255 | + args.cldc_classes, |
| 256 | + jdk_lookup, |
| 257 | + "Java SE 11", |
| 258 | + javap_cmd, |
| 259 | + ) |
| 260 | + |
| 261 | + api_ok, api_messages = ensure_subset( |
| 262 | + cldc_classes, |
| 263 | + args.cldc_classes, |
| 264 | + javaapi_lookup, |
| 265 | + "vm/JavaAPI", |
| 266 | + javap_cmd, |
| 267 | + ) |
| 268 | + |
| 269 | + cldc_map = {name: collect_class_api_from_file(name, args.cldc_classes, javap_cmd) for name in cldc_classes} |
| 270 | + write_extra_report(cldc_map, javaapi_map, args.extra_report) |
| 271 | + |
| 272 | + messages = java_messages + api_messages |
| 273 | + if messages: |
| 274 | + print("\n".join(messages), file=sys.stderr) |
| 275 | + return 0 if java_ok and api_ok else 1 |
| 276 | + |
| 277 | + |
| 278 | +if __name__ == "__main__": |
| 279 | + raise SystemExit(main()) |
0 commit comments