"""Compile patch/src and refresh patch/prebuilt/. Only needed after changing patch/src. python tools/build_patch.py [facade-install-folder] Players never run this: the compiled classes are committed in patch/prebuilt/ and tools/install_patch.py uses those. It needs a JDK (9 to 21) on PATH and a Facade install to compile against. 1. Compiles patch/src with javac, targeting Java 8, the oldest it can emit. 2. Retargets the classes to Java 1.4 (class file version 48), after checking they reference nothing a 1.4 runtime lacks. That is sound because the sources stick to 1.4 language features and APIs; the StackMapTable attribute javac adds is unknown to old JVMs, which ignore unknown attributes. 3. Copies the classes that ship into patch/prebuilt/ and records a digest of the sources beside them, so a test can tell when they are out of date. Run tools/test_patch.py afterwards: it executes the result on the game's own JVM, which is the real proof. """ import hashlib import shutil import subprocess import sys from classfile import INVOKE_DYNAMIC_TAGS, JAVA_1_4_MAJOR, ClassFile from facade_install import BUILD_CLASSES, PREBUILT_CLASSES, REPO, from_args from original_parser import renamed_original_parser from patch_manifest import GENERATED, PREBUILT SOURCES = REPO / "patch" / "src" DIGEST_FILE = PREBUILT_CLASSES / "SOURCES.sha256" # Anything here in a constant pool means the class would die on Java 1.4. It # is a list of the usual suspects (string "+", autoboxing, for-each, enums, # try-with-resources, lambdas), not a proof: a 1.5+ method on an old class, # such as String.format, gets through. tools/test_patch.py is the real check. # Method names and descriptors are separate pool entries, so the autoboxing # markers are the valueOf descriptors on their own. TOO_NEW = ("java/lang/StringBuilder", "java/lang/invoke/", "java/util/Objects", "java/lang/AutoCloseable", "addSuppressed", "java/lang/Iterable", "java/lang/Enum", "(I)Ljava/lang/Integer;", "(J)Ljava/lang/Long;", "(Z)Ljava/lang/Boolean;", "(C)Ljava/lang/Character;", "(D)Ljava/lang/Double;") def sources_digest(): """One hash over every source file, independent of line endings.""" digest = hashlib.sha256() for path in sorted(SOURCES.rglob("*.java")): digest.update(path.relative_to(SOURCES).as_posix().encode()) digest.update(path.read_bytes().replace(b"\r\n", b"\n")) return digest.hexdigest() def compile_sources(classes): sources = sorted(SOURCES.rglob("*.java")) command = ["javac", "--release", "8", "-nowarn", "-Xlint:-options", "-cp", "%s;%s" % (BUILD_CLASSES, classes), "-d", str(BUILD_CLASSES)] try: result = subprocess.run(command + [str(s) for s in sources], capture_output=True, text=True) except FileNotFoundError: sys.exit("javac not found on PATH; install a JDK (9 to 21)") if result.returncode != 0: sys.exit("javac failed:\n%s%s" % (result.stdout, result.stderr)) return [BUILD_CLASSES / s.relative_to(SOURCES).with_suffix(".class") for s in sources] def retarget(path): parsed = ClassFile(path.read_bytes()) pool_text = "\n".join(parsed.utf8_strings()) problems = [marker for marker in TOO_NEW if marker in pool_text] if parsed.tags() & INVOKE_DYNAMIC_TAGS: problems.append("invokedynamic/method handles") if problems: sys.exit("%s uses things Java 1.4 lacks: %s" % (path.name, ", ".join(problems))) parsed.major, parsed.minor = JAVA_1_4_MAJOR, 0 path.write_bytes(parsed.to_bytes()) def main(): install = from_args(sys.argv) if BUILD_CLASSES.exists(): shutil.rmtree(BUILD_CLASSES) # The sources call the original parser by its new name, so it has to exist # to compile against (and test_patch.py runs from build/classes). original = BUILD_CLASSES / GENERATED[0] original.parent.mkdir(parents=True, exist_ok=True) original.write_bytes(renamed_original_parser(install.classes)) for path in compile_sources(install.classes): retarget(path) if PREBUILT_CLASSES.exists(): shutil.rmtree(PREBUILT_CLASSES) for name in PREBUILT: target = PREBUILT_CLASSES / name target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(BUILD_CLASSES / name, target) DIGEST_FILE.write_text(sources_digest() + "\n", encoding="ascii") print("built %d classes for Java 1.4 into %s; commit that folder" % (len(PREBUILT), PREBUILT_CLASSES.relative_to(REPO))) if __name__ == "__main__": main()