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.
183 lines
8.5 KiB
Python
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()
|