mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 14:00:25 +02:00
Upstream's online co-op (network_actors.c, network_coop.c) ticks beside the VR port's VR relay and bots; its message types end at 77, below the VR port's from 240. The look and move flags the action test reads moved into player_control_action_test_note: the head's turning goes there too. The vertex shader's screen-space undo is upstream's on every platform now, with the multiview eye offset after it on Android. The APK carries upstream's brokers.txt beside the FDM layer stub; the updaters keep the VR port's own repository (upstream's now points at OpenCommunityEdition/OpenCE). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
664 lines
30 KiB
Python
664 lines
30 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.
|
|
"""
|
|
|
|
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, MINIUPNPC_DEFINES, MINIUPNPC_DIR, MUSL_MATH_DIR, XDK_INCLUDE,
|
|
compile_launcher, game_defines_and_includes, game_sources, miniupnpc_sources,
|
|
musl_math_sources, pgo_mode, pgo_profile,
|
|
profile_use_flags, xdk_headers)
|
|
from .embed_assets import hud_assets_build, hud_configure_inputs
|
|
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")
|
|
EXPAT_DIR = Path("port/third_party/expat")
|
|
EXPAT_SOURCES = ("xmlparse.c", "xmlrole.c", "xmltok.c")
|
|
KCP_DIR = Path("port/third_party/kcp")
|
|
MONOCYPHER_DIR = Path("port/third_party/monocypher")
|
|
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
|
|
|
|
|
|
# SDL3's Java takes the clipboard service for granted: Lepton, the Steam
|
|
# Frame's Android, has none, and the activity died starting. These edits let
|
|
# it do without one (the clipboard is then empty and keeps nothing).
|
|
SDL_JAVA_PATCHES = [
|
|
(" mClipMgr = (ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE);\n"
|
|
" mClipMgr.addPrimaryClipChangedListener(this);\n",
|
|
" mClipMgr = (ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE);\n"
|
|
" if (mClipMgr != null) /* halo-ce-vr: none in Lepton */\n"
|
|
" mClipMgr.addPrimaryClipChangedListener(this);\n"),
|
|
(" public boolean clipboardHasText() {\n",
|
|
" public boolean clipboardHasText() {\n"
|
|
" if (mClipMgr == null) /* halo-ce-vr */\n"
|
|
" return false;\n"),
|
|
(" public String clipboardGetText() {\n",
|
|
" public String clipboardGetText() {\n"
|
|
" if (mClipMgr == null) /* halo-ce-vr */\n"
|
|
" return null;\n"),
|
|
(" public void clipboardSetText(String string) {\n",
|
|
" public void clipboardSetText(String string) {\n"
|
|
" if (mClipMgr == null) /* halo-ce-vr */\n"
|
|
" return;\n"),
|
|
]
|
|
|
|
|
|
def patch_sdl_java() -> None:
|
|
"""SDL_JAVA_PATCHES, once, into the cloned SDL3's Java."""
|
|
path = SDL_DIR / "android-project" / "app" / "src" / "main" / "java" / "org" / "libsdl" / "app" / "SDLActivity.java"
|
|
if not path.is_file():
|
|
return
|
|
text = path.read_text()
|
|
if "halo-ce-vr: none in Lepton" in text:
|
|
return
|
|
for old, new in SDL_JAVA_PATCHES:
|
|
if text.count(old) != 1:
|
|
raise SystemExit(f"cannot patch {path}: SDL3's clipboard handler has changed")
|
|
text = text.replace(old, new)
|
|
path.write_text(text)
|
|
|
|
|
|
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)
|
|
patch_sdl_java()
|
|
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", *hud_configure_inputs()]
|
|
|
|
|
|
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"))
|
|
|
|
prebuilt = ndk / "toolchains" / "llvm" / "prebuilt"
|
|
_host_tag = os.environ.get("ANDROID_NDK_HOST_TAG", "")
|
|
if not _host_tag:
|
|
if sys.platform == "darwin":
|
|
_host_tag = "darwin-x86_64"
|
|
elif os.name == "nt":
|
|
_host_tag = "windows-x86_64"
|
|
else:
|
|
_host_tag = "linux-x86_64"
|
|
if not (prebuilt / _host_tag).is_dir():
|
|
# an NDK that names its host folder differently: take the one there is
|
|
_tags = sorted(entry.name for entry in prebuilt.iterdir() if entry.is_dir()) if prebuilt.is_dir() else []
|
|
if not _tags:
|
|
n.comment("Android build: the NDK has no LLVM toolchain")
|
|
return
|
|
_host_tag = _tags[0]
|
|
toolchain = prebuilt / _host_tag
|
|
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] = []
|
|
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",
|
|
]
|
|
game_cflags = " ".join([
|
|
guest_abi, guest_code, " ".join(game_flags), profile_flags,
|
|
f"-include {prefix_header}", f"-include {semantics_header}",
|
|
f"-I{LINUX_DIR}/include",
|
|
# the headers of the port's own game units (port/linux/game), for the
|
|
# game sources that call them
|
|
f"-iquote {Path(config['game_sources'])}",
|
|
game_defines_and_includes(config), *libc_includes, f"-idirafter {XDK_INCLUDE}",
|
|
])
|
|
for source in game_sources(config):
|
|
cflags = game_cflags
|
|
if source.as_posix() in VARIADIC_PROTOTYPE_FILES:
|
|
cflags += f" -include {PORT_DIR}/include/halo_android_variadic_prototypes.h"
|
|
objects.append(guest_object(source, 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{EXPAT_DIR}", f"-I{KCP_DIR}", f"-I{MONOCYPHER_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 high-res HUD's textures (port/assets/hud; port/linux/src/hud_hires.c)
|
|
for source in hud_assets_build(n, "android", gen_dir / "hud_hires_assets.c"):
|
|
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))
|
|
# the menus' XML parser (port/third_party/expat; menu_files.c)
|
|
for name in EXPAT_SOURCES:
|
|
objects.append(guest_object(EXPAT_DIR / name, platform_cflags))
|
|
# internet play's reliable streams (port/third_party/kcp; p2p.c)
|
|
objects.append(guest_object(KCP_DIR / "ikcp.c", platform_cflags))
|
|
# internet play's signatures, for public games' listings
|
|
# (port/third_party/monocypher; p2p_crypto.c)
|
|
for name in ("monocypher.c", "monocypher-ed25519.c"):
|
|
objects.append(guest_object(MONOCYPHER_DIR / name, platform_cflags))
|
|
# the game's sin, pow and the rest, the same on every port
|
|
# (port/include/halo_math.h)
|
|
musl_math_cflags = " ".join([
|
|
guest_abi, "-std=gnu11", "-w", profile_flags, *libc_includes, f"-I{MUSL_MATH_DIR}/include",
|
|
f"-include {MUSL_MATH_DIR}/include/libm.h",
|
|
])
|
|
for source in musl_math_sources():
|
|
objects.append(guest_object(source, musl_math_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 "
|
|
# 16 KB pages (Android 15 and later), as the host library
|
|
f"-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384 "
|
|
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)
|
|
# internet play's UPnP (posix_upnp.c, with port/third_party/miniupnpc),
|
|
# as the other posix_*.c in the host
|
|
miniupnpc_cflags = " ".join([host_cflags, f"-I{MINIUPNPC_DIR / 'include'}", f"-I{MINIUPNPC_DIR / 'src'}",
|
|
*MINIUPNPC_DEFINES])
|
|
for source in [LINUX_DIR / "src" / "posix_upnp.c", *miniupnpc_sources()]:
|
|
obj = host_obj_dir / ("miniupnpc_" + source.name + ".o" if source.parent.parent == MINIUPNPC_DIR
|
|
else source.name + ".o")
|
|
n.build(outputs=obj, rule="android_host_cc", inputs=source,
|
|
variables={"cflags": miniupnpc_cflags + (" -w" if source.name != "posix_upnp.c" else "")})
|
|
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])
|
|
|
|
# a do-nothing OpenXR layer under the Steam Frame's implicit layer's
|
|
# library name (port/android/xr_layer/fdm_injection_stub.c)
|
|
fdm_stub = jni_dir / "libVkLayer_VALVE_fdm_injection.so"
|
|
n.rule(
|
|
name="android_xr_layer",
|
|
command=(f"$android_host_cc -O2 -fPIC -shared -fvisibility=hidden -I{OPENXR_DIR}/include -o $out $in "
|
|
"-Wl,-z,max-page-size=16384"),
|
|
description="ANDROID XR LAYER $out",
|
|
)
|
|
n.build(outputs=fdm_stub, rule="android_xr_layer", inputs=PORT_DIR / "xr_layer" / "fdm_injection_stub.c")
|
|
|
|
# ---------- 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)
|
|
# internet play's MQTT brokers, in the APK: the app writes them beside
|
|
# config.toml (port/android/host/host_main.c)
|
|
staged_brokers = assets_dir / "brokers.txt"
|
|
n.build(outputs=staged_brokers, rule="android_copy", inputs=Path("port/assets/network/brokers.txt"))
|
|
n.build(outputs="android", rule="phony", inputs=[libmain, staged_sdl, staged_image, fdm_stub, staged_brokers])
|
|
|
|
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, fdm_stub, staged_brokers])
|
|
n.build(outputs="android_apk", rule="phony", inputs=apk)
|
|
n.newline()
|