"""Build the core-only JNI library matching imgui-java 1.92.7.1. Requires Python 3, a JDK, CMake, and a C++17 compiler. Select a cross compiler with --toolchain or a Visual Studio target with --architecture ARM64. """ import argparse import os import re from pathlib import Path import shutil import subprocess import tempfile import urllib.request import zipfile ROOT = Path(__file__).resolve().parents[1] BINDING_COMMIT = "b164a88602af3e28483efbb4d85aea372010c7c5" IMGUI_COMMIT = "b1bcb12a624af7509894c8e77dd47416997777fa" NATIVE_FILES = ( "_common.h", "_internal.h", "imconfig.h", "jni_jvm", "jni_common", "jni_binding_struct", "jni_callbacks", "jni_assertion", "jni_internal", ) def write_if_changed(path, data): if not path.is_file() or path.read_bytes() != data: path.write_bytes(data) def download(url, destination): if not destination.is_file(): destination.parent.mkdir(parents=True, exist_ok=True) with tempfile.NamedTemporaryFile(dir=destination.parent, suffix=".download", delete=False) as output: temporary = Path(output.name) try: with urllib.request.urlopen(url, timeout=60) as response: shutil.copyfileobj(response, output) except BaseException: output.close() temporary.unlink(missing_ok=True) raise try: if not destination.is_file(): try: temporary.replace(destination) except PermissionError: if not destination.is_file(): raise finally: temporary.unlink(missing_ok=True) return destination def archive(repository, commit, cache): path = download(f"https://codeload.github.com/{repository}/zip/{commit}", cache / "downloads" / f"{repository.split('/')[-1]}-{commit}.zip") return zipfile.ZipFile(path) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--build-dir", type=Path, default=ROOT / "build/imgui-core") parser.add_argument("--output", type=Path, required=True) parser.add_argument("--toolchain", type=Path) parser.add_argument("--architecture", choices=("x64", "ARM64")) parser.add_argument("--cache-dir", type=Path) parser.add_argument("--zig-target", choices=( "x86_64-windows-gnu", "aarch64-windows-gnu", "x86_64-linux-gnu.2.17", "aarch64-linux-gnu.2.17", "x86_64-macos.11.0", "aarch64-macos.11.0", )) args = parser.parse_args() # cmd.exe cannot use extended-length (\\?\) working directory paths. build = Path(os.path.abspath(args.build_dir)) cache = Path(os.path.abspath(args.cache_dir)) if args.cache_dir else build output = Path(os.path.abspath(args.output)) generated = build / "jni" sources = build / "sources" for directory in (build, output, generated, sources): directory.mkdir(parents=True, exist_ok=True) dependencies = [] for group, artifact, version in ( ("com.badlogicgames.gdx", "gdx-jnigen", "2.5.2"), ("com.badlogicgames.gdx", "gdx-jnigen-loader", "2.5.2"), ("com.github.javaparser", "javaparser-core", "3.14.14"), ("io.github.spair", "imgui-java-binding", "1.92.7.1"), ): name = f"{artifact}-{version}.jar" dependencies.append(download( f"https://repo.maven.apache.org/maven2/{group.replace('.', '/')}/{artifact}/{version}/{name}", cache / "deps" / name, )) binding = dependencies[-1] with zipfile.ZipFile(dependencies[0]) as generator: header_prefix = "com/badlogic/gdx/jnigen/resources/headers/" for entry in generator.infolist(): if entry.filename.startswith(header_prefix) and not entry.is_dir(): destination = generated / "jni-headers" / entry.filename[len(header_prefix):] destination.parent.mkdir(parents=True, exist_ok=True) write_if_changed(destination, generator.read(entry)) with archive("SpaiR/imgui-java", BINDING_COMMIT, cache) as upstream: prefix = upstream.namelist()[0] java_prefix = prefix + "imgui-binding/src/generated/java/" native_prefix = prefix + "imgui-binding/src/main/native/" for entry in upstream.infolist(): if entry.filename.startswith(java_prefix) and entry.filename.endswith(".java"): relative = entry.filename[len(java_prefix):] if relative.startswith(("imgui/extension/", "imgui/lwjgl3/")): continue target = sources / relative target.parent.mkdir(parents=True, exist_ok=True) write_if_changed(target, upstream.read(entry)) if entry.filename.startswith(native_prefix): name = entry.filename[len(native_prefix):] if name in NATIVE_FILES or name.rsplit(".", 1)[0] in NATIVE_FILES: write_if_changed(generated / name, upstream.read(entry)) write_if_changed(output / "LICENSE-imgui-java.txt", upstream.read(prefix + "LICENSE")) with archive("ocornut/imgui", IMGUI_COMMIT, cache) as upstream: prefix = upstream.namelist()[0] for entry in upstream.infolist(): name = entry.filename[len(prefix):] if "/" not in name and name.endswith((".h", ".cpp")) and name != "imconfig.h": write_if_changed(generated / name, upstream.read(entry)) write_if_changed(output / "LICENSE-imgui.txt", upstream.read(prefix + "LICENSE.txt")) java_home = os.environ.get("JAVA_HOME") java = str(Path(java_home) / "bin/java") if java_home else shutil.which("java") if not java: raise RuntimeError("Set JAVA_HOME to a JDK.") env = os.environ.copy() if java_home: env["PATH"] = str(Path(java_home) / "bin") + os.pathsep + env["PATH"] # jnigen constructs its javac command as a string. Relative paths avoid its # whitespace bug without depending on Windows short filename support. write_if_changed(build / "binding.jar", binding.read_bytes()) subprocess.run([ java, "-cp", os.pathsep.join(map(str, dependencies)), str(ROOT / "scripts/imgui-core/GenerateCore.java"), "sources", "binding.jar", "jni", ], check=True, env=env, cwd=build) # Upstream's GNU builds accept variable-length arrays. Use standard C++ # storage in generated JNI code so the identical methods also build in MSVC. declaration = re.compile(r"\b(ImVec2|const char\*|jlong)\s+(\w+)\[(\w+)\];") for source in generated.glob("imgui_*.cpp"): text = source.read_text(encoding="utf-8") text, count = declaration.subn( lambda match: f"std::vector<{match[1]}> {match[2]}Storage({match[3]});\n" f" auto* {match[2]} = {match[2]}Storage.data();", text, ) if count: source.write_text("#include \n" + text, encoding="utf-8") configuration = [ "cmake", "-S", str(ROOT / "scripts/imgui-core"), "-B", str(build / "cmake"), f"-DCORE_JNI_DIR={generated.as_posix()}", f"-DCORE_OUTPUT_DIR={output.as_posix()}", "-DCMAKE_BUILD_TYPE=Release", ] if args.zig_target: if args.toolchain or args.architecture: parser.error("--zig-target cannot be combined with --toolchain or --architecture") import ziglang executable = Path(ziglang.__file__).parent / ("zig.exe" if os.name == "nt" else "zig") family = "Windows" if "windows" in args.zig_target else "Darwin" if "macos" in args.zig_target else "Linux" toolchain = build / "zig-toolchain.cmake" toolchain.write_text( f'set(CMAKE_SYSTEM_NAME {family})\n' f'set(CMAKE_SYSTEM_PROCESSOR {args.zig_target.split("-")[0]})\n' f'set(CMAKE_CXX_COMPILER "{executable.as_posix()}")\n' f'set(CMAKE_CXX_COMPILER_ARG1 "c++ -target {args.zig_target} -Wno-nullability-completeness")\n', encoding="utf-8", ) configuration.extend(("-G", "Ninja", f"-DCMAKE_TOOLCHAIN_FILE={toolchain.as_posix()}")) if args.toolchain: configuration.append(f"-DCMAKE_TOOLCHAIN_FILE={args.toolchain.resolve().as_posix()}") if args.architecture: configuration.extend(("-A", args.architecture)) subprocess.run(configuration, check=True, env=env) subprocess.run(["cmake", "--build", str(build / "cmake"), "--config", "Release", "--parallel", "4"], check=True, env=env) if __name__ == "__main__": main()