mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-23 06:00:25 +02:00
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>
571 lines
25 KiB
Python
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()
|