Files
Cesarus85--mjolnir-vr/tools/android_build.py
T
Stefan MaierandClaude Opus 5.5 ce59138a2b Play in VR on OpenXR headsets (Quest 3/3S, Steam Frame)
The Android port gets an OpenXR session in the host (Khronos loader, core
OpenXR plus XR_KHR_opengl_es_enable): on a headset the game keeps its
640x480 layout drawn at the eye images' resolution, renders the player's
view once per eye from the tracked eye poses through asymmetric frusta,
and shows the HUD and menus as a head-locked or room-fixed quad layer.
The head aims the player; the right stick turns; the Touch controllers
map onto the Xbox pad; rumble goes to the controllers. Frames without a
player view (menus, loading, cinematics) go onto a screen in the room.

A marker file, xr_test, runs a self-test of the session, layers, colour
path and controllers without game data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 09:17:19 +02:00

571 lines
25 KiB
Python

"""Ninja rules for the Android build (``ninja android``).
The Android port (port/android/README.md) runs the game as ILP32 AArch64
code - 32-bit pointers, as the game's data formats require - inside an
ordinary 64-bit Android app. This graph builds
- the guest image, build/android/halo_guest.elf: the game sources, the
platform layer shared with the Linux port (port/linux/src) and the guest
runtime (port/android/guest) with a subset of musl as its C library, all
compiled by clang for arm64_32-apple-watchos, converted to ELF assembly
(tools/android_asm_convert.py), assembled for AArch64 and linked at a fixed
address below 4 GB;
- the host library, build/android/jniLibs/arm64-v8a/libmain.so, with the
Android NDK: the loader and the services the guest calls, over SDL3;
- SDL3 itself, with the NDK's CMake toolchain file;
and stages both, with the SDL3 Java sources, for the Gradle project in
port/android/app, which ``ninja android_apk`` then assembles.
Like the Linux build, this is independent of the byte-matching graph.
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
from .linux_build import LINUX_PROFILE, XDK_INCLUDE, compile_launcher, pgo_mode, pgo_profile, profile_use_flags, xdk_headers
from .ninja_syntax import Writer
PORT_DIR = Path("port/android")
LINUX_DIR = Path("port/linux")
BUILD = Path("build/android")
THIRD_PARTY = BUILD / "third_party"
# the TOML parser config.toml is read with (port/linux/src/port_config.c)
TOML_DIR = Path("port/third_party/tomlc17")
KCP_DIR = Path("port/third_party/kcp")
MUSL_VERSION = "1.2.5"
MUSL_DIR = THIRD_PARTY / f"musl-{MUSL_VERSION}"
MUSL_URL = f"https://musl.libc.org/releases/musl-{MUSL_VERSION}.tar.gz"
SDL_TAG = "release-3.4.16"
SDL_DIR = THIRD_PARTY / "SDL3"
SDL_URL = "https://github.com/libsdl-org/SDL.git"
# the Khronos OpenXR loader for Android (the VR build, host_xr.c): its
# headers and library are linked from here; the app packages the same AAR
# through Gradle (port/android/app/build.gradle), which also merges the
# manifest entries the loader needs to find the runtime
OPENXR_VERSION = "1.1.63"
OPENXR_DIR = THIRD_PARTY / f"openxr-{OPENXR_VERSION}"
OPENXR_URL = ("https://repo1.maven.org/maven2/org/khronos/openxr/openxr_loader_for_android/"
f"{OPENXR_VERSION}/openxr_loader_for_android-{OPENXR_VERSION}.aar")
ANDROID_API = 28
# The guest ABI: AArch64 code with 32-bit pointers (clang's only such target
# is Apple's arm64_32, whose Mach-O output is converted afterwards). The
# Darwin environment is hidden from the sources; the C library is musl.
GUEST_ABI_FLAGS = [
"--target=arm64_32-apple-watchos",
"-U__APPLE__",
"-U__MACH__",
"-fno-define-target-os-macros",
"-D__linux__=1",
"-D__unix__=1",
"-DHALO_ANDROID=1",
# ARMv8.0: nothing the emulator's binary translation or an older
# device could lack (Darwin targets otherwise assume pointer
# authentication and FP16)
"-mcpu=cortex-a53",
"-nostdinc",
"-fshort-wchar",
"-fno-stack-protector",
"-fno-unwind-tables",
"-fno-asynchronous-unwind-tables",
"-femulated-tls",
"-mllvm",
"-aarch64-neon-syntax=generic",
# no fused multiply-add: the game was written for x87/SSE arithmetic,
# and its debug assertions (colours within 0..1, unit vectors) trip on
# the different rounding of fused operations
"-ffp-contract=off",
"-O2",
]
# as the Linux build (tools/linux_build.py), minus what only x86 needs
GUEST_CODE_FLAGS = [
"-fms-extensions",
"-fcommon",
"-fno-strict-aliasing",
"-fwrapv",
"-fno-delete-null-pointer-checks",
"-fno-omit-frame-pointer",
*(f"-fno-builtin-{name}" for name in (
"wcslen", "wcsnlen", "wcschr", "wcsrchr", "wcscmp", "wcsncmp", "wcscpy",
"wcsncpy", "wcscat", "wcsncat", "wmemchr", "wmemcmp", "wmemcpy",
"wmemmove", "wmemset",
)),
]
MUSL_DIRECTORIES = [
"conf", "ctype", "dirent", "env", "errno", "exit", "fcntl", "internal",
"locale", "malloc", "malloc/mallocng", "math", "mman", "multibyte",
"prng", "sched", "select", "signal", "stat", "stdio", "stdlib", "string",
"time", "unistd",
]
MUSL_FILES = [
"thread/__lock.c", "thread/__wait.c", "thread/__timedwait.c",
"thread/__syscall_cp.c", "thread/vmlock.c", "thread/pthread_self.c",
"thread/pthread_equal.c", "thread/pthread_once.c",
"thread/pthread_setcancelstate.c", "thread/pthread_testcancel.c",
"thread/default_attr.c", "thread/lock_ptc.c", "misc/getauxval.c",
"linux/sysinfo.c", "misc/basename.c", "misc/dirname.c",
"misc/realpath.c", "misc/uname.c", "misc/ioctl.c", "misc/getrlimit.c",
"misc/syscall.c", "network/htonl.c", "network/htons.c", "network/ntohl.c",
"network/ntohs.c", "network/inet_addr.c", "network/inet_aton.c",
"network/inet_ntoa.c", "network/inet_pton.c", "network/inet_ntop.c",
]
MUSL_THREAD_PREFIXES = (
"pthread_attr_", "pthread_cond", "pthread_mutex", "pthread_rwlock",
"pthread_spin", "sem_",
)
# replaced by the guest runtime (port/android/guest/runtime)
MUSL_EXCLUDE = {
"env/__stack_chk.c", "env/__init_tls.c", "env/__libc_start_main.c",
"env/__reset_tls.c", "malloc/oldmalloc", "thread/pthread_create.c",
# unused, and its compiler barrier is an inline assembly statement
"string/explicit_bzero.c",
}
# game files that call variadic functions without a prototype in scope, which
# only works under x86's calling convention (tools/android_abi_check.py)
VARIADIC_PROTOTYPE_FILES = {
"source/ai/action_uncover.c", "source/ai/ai.c", "source/ai/ai_debug.c",
"source/bungie_net/common/public_key_crypt.c", "source/camera/editor_flying_camera.c",
"source/game/cheats.c", "source/game/game_engine.c", "source/game/players.c",
"source/hs/hs.c", "source/interface/hud_nav_points.c",
"source/networking/telnet_console.c", "source/rasterizer/xbox/rasterizer_xbox_errors.c",
"source/render/render.c",
}
HOST_LIBRARIES = ["SDL3", "GLESv3", "EGL", "log", "android", "m", "dl"]
def _quote(path: Any) -> str:
text = str(path).replace(os.sep, "/")
return f'"{text}"' if " " in text else text
def _find_ndk() -> Optional[Path]:
for variable in ("ANDROID_NDK_HOME", "ANDROID_NDK_ROOT", "ANDROID_NDK"):
if os.environ.get(variable) and Path(os.environ[variable]).is_dir():
return Path(os.environ[variable])
for variable in ("ANDROID_HOME", "ANDROID_SDK_ROOT"):
sdk = os.environ.get(variable)
if sdk and (Path(sdk) / "ndk").is_dir():
versions = sorted((Path(sdk) / "ndk").iterdir())
if versions:
return versions[-1]
for sdk in (Path.home() / "Android/Sdk", Path("/opt/android-sdk")):
if (sdk / "ndk").is_dir():
versions = sorted((sdk / "ndk").iterdir())
if versions:
return versions[-1]
return None
def fetch_third_party() -> None:
"""Download musl and SDL3 (configure time, once)."""
THIRD_PARTY.mkdir(parents=True, exist_ok=True)
if not MUSL_DIR.is_dir():
print(f"Downloading {MUSL_URL}")
archive = THIRD_PARTY / f"musl-{MUSL_VERSION}.tar.gz"
subprocess.run(["curl", "-sSfL", "-o", str(archive), MUSL_URL], check=True)
subprocess.run(["tar", "xzf", archive.name], cwd=THIRD_PARTY, check=True)
archive.unlink()
if not SDL_DIR.is_dir():
print(f"Cloning SDL3 {SDL_TAG}")
subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", SDL_TAG, SDL_URL, str(SDL_DIR)],
check=True)
if not OPENXR_DIR.is_dir():
print(f"Downloading {OPENXR_URL}")
import zipfile
archive = THIRD_PARTY / f"openxr-{OPENXR_VERSION}.aar"
subprocess.run(["curl", "-sSfL", "-o", str(archive), OPENXR_URL], check=True)
staging = THIRD_PARTY / f"openxr-{OPENXR_VERSION}.partial"
shutil.rmtree(staging, ignore_errors=True)
with zipfile.ZipFile(archive) as aar:
for member in aar.namelist():
if member.startswith("prefab/modules/headers/include/openxr/") and member.endswith(".h"):
target = staging / "include" / "openxr" / Path(member).name
elif member == "jni/arm64-v8a/libopenxr_loader.so":
target = staging / "lib" / "libopenxr_loader.so"
else:
continue
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(aar.read(member))
staging.rename(OPENXR_DIR)
archive.unlink()
def _musl_sources() -> List[Path]:
src = MUSL_DIR / "src"
result = set()
for directory in MUSL_DIRECTORIES:
for path in (src / directory).glob("*.c"):
result.add(path)
for name in MUSL_FILES:
result.add(src / name)
for path in (src / "thread").glob("*.c"):
if path.name.startswith(MUSL_THREAD_PREFIXES):
result.add(path)
sources = []
for path in sorted(result):
relative = path.relative_to(src).as_posix()
if relative in MUSL_EXCLUDE or any(relative.startswith(e + "/") for e in MUSL_EXCLUDE):
continue
sources.append(path)
return sources
def android_configure_inputs() -> List[Path]:
return [Path(__file__), PORT_DIR / "guest" / "runtime", PORT_DIR / "host", LINUX_DIR / "src"]
def generate_android_build(n: Writer, sln: Any) -> None:
config_path = LINUX_DIR / "port.json"
if not config_path.is_file() or not (PORT_DIR / "host").is_dir():
return
ndk = Path(sln.android_ndk) if getattr(sln, "android_ndk", None) else _find_ndk()
if not ndk or not ndk.is_dir():
n.comment("Android build: no NDK found (set ANDROID_NDK_HOME or pass --android-ndk)")
return
try:
fetch_third_party()
except (subprocess.CalledProcessError, OSError) as error:
print(f"Android build disabled: cannot fetch musl/SDL3 ({error})", file=sys.stderr)
return
import json
config: Dict[str, Any] = json.loads(config_path.read_text(encoding="utf-8"))
# the NDK ships one prebuilt toolchain per build host (linux-x86_64,
# darwin-x86_64, windows-x86_64); use whichever this NDK has
prebuilt = ndk / "toolchains" / "llvm" / "prebuilt"
host_tag = {"linux": "linux-x86_64", "darwin": "darwin-x86_64"}.get(sys.platform, "linux-x86_64")
toolchain = prebuilt / host_tag
if not toolchain.is_dir():
toolchain = next((p for p in sorted(prebuilt.iterdir()) if p.is_dir()), toolchain)
sysroot_include = toolchain / "sysroot" / "usr" / "include"
host_cc = toolchain / "bin" / f"aarch64-linux-android{ANDROID_API}-clang"
ndk_bin = toolchain / "bin"
guest_cc = getattr(sln, "android_guest_cc", None) or "clang"
guest_dir = BUILD / "guest"
obj_dir = guest_dir / "obj"
gen_dir = guest_dir / "gen"
libc_include = guest_dir / "libc_include"
libc_internal = guest_dir / "libc_internal"
gl_include = guest_dir / "gl_include"
arch = PORT_DIR / "guest" / "libc" / "arch" / "arm64_32"
semantics_header = Path("build/linux/halo_msvc_semantics.h")
platform_semantics_header = Path("build/linux/platform_msvc_semantics.h")
prefix_header = LINUX_DIR / "include" / "halo_linux_prefix.h"
image = BUILD / "halo_guest.elf"
sdl_build = BUILD / "sdl3-build"
libsdl = sdl_build / "libSDL3.so"
jni_dir = BUILD / "jniLibs" / "arm64-v8a"
libmain = jni_dir / "libmain.so"
assets_dir = BUILD / "assets"
python = "$python"
n.comment("Android build (ninja android); see port/android/README.md")
n.variable("android_guest_cc", guest_cc)
n.variable("android_host_cc", str(host_cc))
n.variable("android_ndk_bin", str(ndk_bin))
# ---------- generated headers and sources
alltypes = libc_include / "bits" / "alltypes.h"
syscall_h = libc_include / "bits" / "syscall.h"
version_h = libc_internal / "version.h"
n.rule(
name="android_alltypes",
command=f"sed -f {MUSL_DIR}/tools/mkalltypes.sed $in > $out",
description="ANDROID MUSL $out",
)
n.build(outputs=alltypes, rule="android_alltypes",
inputs=[arch / "bits" / "alltypes.h.in", MUSL_DIR / "include" / "alltypes.h.in"])
n.rule(
name="android_syscall_h",
command="cp $in $out && sed -n -e s/__NR_/SYS_/p < $in >> $out",
description="ANDROID MUSL $out",
)
n.build(outputs=syscall_h, rule="android_syscall_h", inputs=arch / "bits" / "syscall.h.in")
n.rule(
name="android_version_h",
command=f"echo '#define VERSION \"{MUSL_VERSION}\"' > $out",
description="ANDROID MUSL $out",
)
n.build(outputs=version_h, rule="android_version_h")
# the NDK's OpenGL ES headers (C declarations only) for the guest
gl_stamp = gl_include / "stamp"
n.rule(
name="android_gl_include",
command=(f"mkdir -p {gl_include} && ln -sfn {sysroot_include}/GLES2 {gl_include}/GLES2 && "
f"ln -sfn {sysroot_include}/GLES3 {gl_include}/GLES3 && "
f"ln -sfn {sysroot_include}/KHR {gl_include}/KHR && touch $out"),
description="ANDROID GL HEADERS",
)
n.build(outputs=gl_stamp, rule="android_gl_include")
guest_gl_c = gen_dir / "guest_gl.c"
gl_imports = gen_dir / "gl_imports.list"
n.rule(
name="android_gl_stubs",
command=(f"{python} tools/android_gl_stubs.py {LINUX_DIR}/src/gl.h {sysroot_include}/GLES3/gl32.h "
f"{sysroot_include}/GLES2/gl2ext.h {guest_gl_c} {gl_imports}"),
description="ANDROID GL STUBS",
)
n.build(outputs=[guest_gl_c, gl_imports], rule="android_gl_stubs",
implicit=[Path("tools/android_gl_stubs.py"), LINUX_DIR / "src" / "gl.h"])
guest_posix_c = gen_dir / "guest_posix.c"
posix_imports = gen_dir / "posix_imports.list"
n.rule(
name="android_posix_stubs",
command=f"{python} tools/android_posix_stubs.py {LINUX_DIR}/src/posix.h {guest_posix_c} {posix_imports}",
description="ANDROID POSIX STUBS",
)
n.build(outputs=[guest_posix_c, posix_imports], rule="android_posix_stubs",
implicit=[Path("tools/android_posix_stubs.py"), LINUX_DIR / "src" / "posix.h"])
imports_s = gen_dir / "imports.s"
host_table_c = BUILD / "host" / "host_import_table.c"
host_imports_list = PORT_DIR / "host_imports.list"
n.rule(
name="android_imports",
command=f"{python} tools/android_imports.py --host-table {host_table_c} {imports_s} $in",
description="ANDROID IMPORTS",
)
n.build(outputs=[imports_s, host_table_c], rule="android_imports",
inputs=[host_imports_list, posix_imports, gl_imports],
implicit=[Path("tools/android_imports.py")])
generated_headers = [*xdk_headers(), alltypes, syscall_h, version_h, gl_stamp,
semantics_header, platform_semantics_header]
# ---------- guest compilation: C -> Darwin assembly -> ELF assembly -> object
n.rule(
name="android_guest_cc",
command=(f"{compile_launcher(sln)}$android_guest_cc -MMD -MF $out.d $cflags -S $in -o $out.darwin.s && "
f"{python} tools/android_asm_convert.py $out.darwin.s $out.s && "
f"$android_guest_cc --target=aarch64-linux-android -c $out.s -o $out"),
description="ANDROID CC $out",
depfile="$out.d",
deps="gcc",
)
n.rule(
name="android_guest_as",
command="$android_guest_cc --target=aarch64-linux-android -c $in -o $out",
description="ANDROID AS $out",
)
libc_includes = [
f"-isystem {libc_include}", f"-isystem {arch}", f"-isystem {MUSL_DIR}/arch/generic",
f"-isystem {MUSL_DIR}/include",
]
guest_abi = " ".join(GUEST_ABI_FLAGS + (["-DHALO_RELEASE"] if getattr(sln, "port_release", False) else []))
guest_code = " ".join(GUEST_CODE_FLAGS)
tool_implicit = [Path("tools/android_asm_convert.py"), *generated_headers]
# profile-guided optimisation with the Linux build's profile (committed,
# or trained by the Linux build with --pgo=train): the game and platform
# code are the same, and functions that differ simply go without
profile = pgo_profile(sln, LINUX_PROFILE if pgo_mode(sln) == "train" else None, [LINUX_PROFILE], guest_cc)
profile_flags = " ".join(profile_use_flags(profile))
if profile:
tool_implicit.append(profile)
def guest_object(source: Path, cflags: str, prefix: str = "") -> Path:
obj = obj_dir / prefix / Path(str(source).lstrip("/")).with_suffix(".o")
if str(source).startswith(str(BUILD)):
obj = obj_dir / prefix / source.relative_to(BUILD).with_suffix(".o")
n.build(outputs=obj, rule="android_guest_cc", inputs=source, implicit=tool_implicit,
variables={"cflags": cflags})
return obj
# musl
musl_cflags = " ".join([
guest_abi, "-std=c99", "-ffreestanding", "-fno-common", "-D_XOPEN_SOURCE=700", "-w",
f"-I{arch}", f"-I{MUSL_DIR}/arch/generic", f"-I{libc_internal}",
f"-I{PORT_DIR}/guest/libc/src_include", f"-I{MUSL_DIR}/src/include",
f"-I{MUSL_DIR}/src/internal", f"-I{libc_include}", f"-I{MUSL_DIR}/include",
])
musl_objects = [guest_object(source, musl_cflags, "musl") for source in _musl_sources()]
libguestc = guest_dir / "libguestc.a"
n.rule(
name="android_ar",
command="rm -f $out && $android_ndk_bin/llvm-ar rcs $out @$out.rsp",
description="ANDROID AR $out",
rspfile="$out.rsp",
rspfile_content="$in_newline",
)
n.build(outputs=libguestc, rule="android_ar", inputs=musl_objects)
# the game
objects: List[Path] = []
excluded = set(config.get("exclude_sources", []))
game_flags = [
"-std=gnu89", "-D__STRICT_ANSI__", "-w",
"-Wno-error=incompatible-pointer-types",
"-Wno-error=incompatible-function-pointer-types",
"-Wno-error=int-conversion",
"-Wno-error=implicit-function-declaration",
"-Wno-error=implicit-int",
"-Wno-error=return-type",
]
for proj in sln.projects:
if proj.name not in config["projects"]:
continue
options = proj.options
defines = " ".join(f"-D{d}" for d in options.get("defines") or [])
includes = " ".join(
f"-I{_quote(d)}" for d in options.get("include_dirs") or [] if Path(d) != Path("xbox/include")
)
game_cflags = " ".join([
guest_abi, guest_code, " ".join(game_flags), profile_flags,
f"-include {prefix_header}", f"-include {semantics_header}", defines,
f"-I{LINUX_DIR}/include", includes, *libc_includes, f"-idirafter {XDK_INCLUDE}",
])
for obj in proj.objects:
name = str(obj.file_path).replace(os.sep, "/")
if obj.status.name == "Missing" or name in excluded or obj.file_path.suffix.lower() != ".c":
continue
cflags = game_cflags
if name in VARIADIC_PROTOTYPE_FILES:
cflags += f" -include {PORT_DIR}/include/halo_android_variadic_prototypes.h"
objects.append(guest_object(obj.file_path, cflags))
for source in sorted(Path(config["game_sources"]).glob("*.c")):
objects.append(guest_object(source, game_cflags))
# the platform layer shared with Linux, and the guest runtime
platform_cflags = " ".join([
guest_abi, guest_code, "-std=gnu11", "-D_GNU_SOURCE", "-DHALO_LINUX_PLATFORM_LAYER", "-w", profile_flags,
f"-include {prefix_header}", f"-include {platform_semantics_header}",
f"-I{LINUX_DIR}/src", f"-I{LINUX_DIR}/include", f"-I{PORT_DIR}/guest/runtime",
f"-I{PORT_DIR}/include", f"-I{TOML_DIR}", f"-I{KCP_DIR}", "-Isource -Isource/cseries",
f"-I{SDL_DIR}/include", f"-I{gl_include}", *libc_includes, f"-idirafter {XDK_INCLUDE}",
])
guest_host_only = {"memory_watch.c"} # replaced by guest_memory_watch.c
for source in sorted((LINUX_DIR / "src").glob("*.c")):
if source.name.startswith("posix_") or source.name in guest_host_only:
continue
objects.append(guest_object(source, platform_cflags))
# the settings file's parser (port/third_party/tomlc17)
objects.append(guest_object(TOML_DIR / "tomlc17.c", platform_cflags))
# internet play's reliable streams (port/third_party/kcp; p2p.c)
objects.append(guest_object(KCP_DIR / "ikcp.c", platform_cflags))
runtime_internal_cflags = " ".join([
guest_abi, "-std=c99", "-ffreestanding", "-fno-common", "-D_XOPEN_SOURCE=700", "-D_GNU_SOURCE",
f"-I{PORT_DIR}/guest/runtime", f"-I{PORT_DIR}/include",
f"-I{arch}", f"-I{MUSL_DIR}/arch/generic", f"-I{libc_internal}",
f"-I{PORT_DIR}/guest/libc/src_include", f"-I{MUSL_DIR}/src/include",
f"-I{MUSL_DIR}/src/internal", f"-I{libc_include}", f"-I{MUSL_DIR}/include",
])
runtime_cflags = " ".join([
guest_abi, guest_code, "-std=gnu11", "-D_GNU_SOURCE",
f"-I{PORT_DIR}/guest/runtime", f"-I{PORT_DIR}/include", f"-I{LINUX_DIR}/src",
f"-I{SDL_DIR}/include", f"-I{gl_include}", *libc_includes,
])
runtime_dir = PORT_DIR / "guest" / "runtime"
for source in sorted(runtime_dir.glob("*.c")):
if source.name in ("guest_thread.c", "guest_start.c"):
objects.append(guest_object(source, runtime_internal_cflags))
elif source.name == "guest_memory_watch.c":
objects.append(guest_object(source, platform_cflags))
else:
objects.append(guest_object(source, runtime_cflags))
objects.append(guest_object(guest_gl_c, runtime_cflags))
objects.append(guest_object(guest_posix_c, runtime_cflags))
imports_o = obj_dir / "gen" / "imports.o"
n.build(outputs=imports_o, rule="android_guest_as", inputs=imports_s)
objects.append(imports_o)
# ---------- the guest image
linker_script = PORT_DIR / "guest" / "guest.ld"
n.rule(
name="android_guest_link",
command=(f"$android_ndk_bin/ld.lld -m aarch64linux -static -nostdlib -T {linker_script} "
f"-Map $out.map -o $out @$out.rsp {libguestc} "
"$$($android_host_cc -print-libgcc-file-name)"),
description="ANDROID LINK $out",
rspfile="$out.rsp",
rspfile_content="$in_newline",
)
n.build(outputs=image, rule="android_guest_link", inputs=objects, implicit=[libguestc, linker_script])
# ---------- SDL3
n.rule(
name="android_sdl3",
command=(f"cmake -S {SDL_DIR} -B {sdl_build} -G Ninja "
f"-DCMAKE_TOOLCHAIN_FILE={ndk}/build/cmake/android.toolchain.cmake "
f"-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-{ANDROID_API} -DCMAKE_BUILD_TYPE=Release "
f"-DSDL_SHARED=ON -DSDL_STATIC=OFF -DSDL_TEST_LIBRARY=OFF -DSDL_TESTS=OFF -DSDL_EXAMPLES=OFF "
f"> {BUILD}/sdl3-configure.log && ninja -C {sdl_build} > {BUILD}/sdl3-build.log"),
description="ANDROID SDL3",
pool="console",
)
n.build(outputs=libsdl, rule="android_sdl3", implicit=[SDL_DIR / "CMakeLists.txt"])
# ---------- the host library
host_objects: List[Path] = []
host_obj_dir = BUILD / "host" / "obj"
n.rule(
name="android_host_cc",
command="$android_host_cc -MMD -MF $out.d $cflags -c $in -o $out",
description="ANDROID HOST CC $out",
depfile="$out.d",
deps="gcc",
)
host_cflags = " ".join([
"-O2", "-g", "-fPIC", "-Wall", "-Wno-unused-function", "-D_GNU_SOURCE",
f"-I{PORT_DIR}/include", f"-I{PORT_DIR}/host", f"-I{SDL_DIR}/include", f"-I{LINUX_DIR}/src",
f"-I{TOML_DIR}", f"-I{OPENXR_DIR}/include",
])
host_sources = sorted((PORT_DIR / "host").glob("*.c")) + [
LINUX_DIR / "src" / "posix_files.c", LINUX_DIR / "src" / "posix_net.c",
# the app reads debug.sample_seconds from config.toml (host_main.c)
TOML_DIR / "tomlc17.c",
]
for source in host_sources:
obj = host_obj_dir / (source.name + ".o")
n.build(outputs=obj, rule="android_host_cc", inputs=source, variables={"cflags": host_cflags})
host_objects.append(obj)
table_obj = host_obj_dir / "host_import_table.c.o"
n.build(outputs=table_obj, rule="android_host_cc", inputs=host_table_c, variables={"cflags": host_cflags})
host_objects.append(table_obj)
n.rule(
name="android_host_link",
command=(f"$android_host_cc -shared -o $out $in -L{sdl_build} -L{OPENXR_DIR}/lib -lopenxr_loader "
+ " ".join(f"-l{lib}" for lib in HOST_LIBRARIES)
+ " -Wl,-z,max-page-size=16384 -Wl,--no-undefined"),
description="ANDROID HOST LINK $out",
)
n.build(outputs=libmain, rule="android_host_link", inputs=host_objects, implicit=[libsdl])
# ---------- staging for Gradle
staged_sdl = jni_dir / "libSDL3.so"
staged_image = assets_dir / "halo_guest.elf"
n.rule(name="android_copy", command="cp $in $out", description="ANDROID STAGE $out")
n.build(outputs=staged_sdl, rule="android_copy", inputs=libsdl)
n.build(outputs=staged_image, rule="android_copy", inputs=image)
n.build(outputs="android", rule="phony", inputs=[libmain, staged_sdl, staged_image])
apk = PORT_DIR / "app" / "build" / "outputs" / "apk" / "debug" / "app-debug.apk"
n.rule(
name="android_gradle",
# Gradle leaves the APK alone when its contents would not change
command=(f"cd {PORT_DIR} && ./gradlew --console=plain -q assembleDebug && "
"touch app/build/outputs/apk/debug/app-debug.apk"),
description="ANDROID GRADLE $out",
pool="console",
)
n.build(outputs=apk, rule="android_gradle", inputs=[libmain, staged_sdl, staged_image])
n.build(outputs="android_apk", rule="phony", inputs=apk)
n.newline()