Files
OpenPrestige/scripts/build_imgui_core.py
T
bbldrizzy 2d6b2a3cd1 Build offline OpenPrestige release bundles
Rename the injector and package it with an embedded payload for Windows, Linux, and macOS x64/ARM64. Add a core-only ImGui native build, multi-platform CI and release packaging, and shared per-user storage with legacy config migration.
2026-11-03 18:45:13 +00:00

183 lines
8.5 KiB
Python

"""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 <vector>\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()