From 9cc24de0895129084b4ff8cff444b48d8b9d896a Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Sat, 25 Jul 2026 08:32:47 -0400 Subject: [PATCH 1/3] build: -Dandroid-ndk option to cross-compile libtile57.a for Android MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zig ships no bionic headers, so the C deps (Lua, libtess2, nanosvg, stb) fail to find string.h/math.h/asm/* when targeting *-linux-android. Add a -Dandroid-ndk option that generates the Zig libc config from the NDK sysroot (include dir, the arch asm/ include in the triple subdir, and the per-API crt dir) and pins it to the lib artifact via setLibCFile — scoped to the lib so it doesn't disturb the host tools pinned to linux-musl (a global --libc does). zig build lib -Dtarget=aarch64-linux-android -Dandroid-ndk=$ANDROID_NDK Produces an ELF64 AArch64 libtile57.a. Native builds are unaffected (the libc file is null off-android). --- build.zig | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/build.zig b/build.zig index 80d4d281..15a70d01 100644 --- a/build.zig +++ b/build.zig @@ -201,6 +201,42 @@ fn addPkgTest( return tm; } +// The NDK triple for an *-linux-android target, or null when the target isn't +// android. Drives the sysroot's arch-specific include + crt subdirs. +fn androidTriple(target: std.Build.ResolvedTarget) ?[]const u8 { + const t = target.result; + if (t.abi != .android and t.abi != .androideabi) return null; + return switch (t.cpu.arch) { + .aarch64 => "aarch64-linux-android", + .x86_64 => "x86_64-linux-android", + .x86 => "i686-linux-android", + .arm, .thumb => "arm-linux-androideabi", + else => null, + }; +} + +// Generate a Zig `--libc` config for the NDK sysroot and return it as a file the +// caller pins to the android artifact via `setLibCFile`. The `asm/` arch headers +// live in the triple subdir (sys_include_dir), which a plain `--sysroot` misses. +fn androidLibcFile(b: *std.Build, ndk: []const u8, triple: []const u8, api: u32) std.Build.LazyPath { + const host = switch (@import("builtin").os.tag) { + .macos => "darwin-x86_64", // the NDK ships x86_64 host binaries even on arm64 macs + .windows => "windows-x86_64", + else => "linux-x86_64", + }; + const sysroot = b.fmt("{s}/toolchains/llvm/prebuilt/{s}/sysroot", .{ ndk, host }); + const content = b.fmt( + \\include_dir={s}/usr/include + \\sys_include_dir={s}/usr/include/{s} + \\crt_dir={s}/usr/lib/{s}/{d} + \\msvc_lib_dir= + \\kernel32_lib_dir= + \\gcc_dir= + \\ + , .{ sysroot, sysroot, triple, sysroot, triple, api }); + return b.addWriteFiles().add(b.fmt("android-libc-{s}.txt", .{triple}), content); +} + pub fn build(b: *std.Build) void { // The S-101 PortrayalCatalog source (submodule, or the lazy dependency for // a fetched package). On the first pass of a fetch this is null — return so @@ -218,6 +254,19 @@ pub fn build(b: *std.Build) void { // the -Doptimize option entirely. const optimize = b.option(std.builtin.OptimizeMode, "optimize", "Prioritize performance, safety, or binary size") orelse .ReleaseFast; + // Cross-compiling to *-linux-android: Zig ships no bionic headers, so the C + // deps (Lua, libtess2, nanosvg, stb) need the NDK sysroot. Pass the NDK root + // with `-Dandroid-ndk=` and this generates the Zig libc config (include + // + arch `asm/` include + per-API crt dir) and pins it to the lib artifact + // ONLY — a global `--libc` would break the host tools pinned to linux-musl. + // `zig build lib -Dtarget=aarch64-linux-android -Dandroid-ndk=$ANDROID_NDK`. + const android_ndk = b.option([]const u8, "android-ndk", "Android NDK root (enables the *-linux-android libc for the lib)"); + const android_api = b.option(u32, "android-api", "Android API level for the NDK sysroot (default 24)") orelse 24; + const android_libc: ?std.Build.LazyPath = if (androidTriple(target)) |triple| + (if (android_ndk) |ndk| androidLibcFile(b, ndk, triple, android_api) else null) + else + null; + // Lua: POSIX feature flags on Unix; Windows lets luaconf.h auto-pick // LUA_USE_WINDOWS. The portray module is target-agnostic (it inherits each // consumer's target), but on a given host the lib + baker share this OS, so @@ -496,6 +545,8 @@ pub fn build(b: *std.Build) void { lib_mod.addImport("buildinfo", buildinfo.createModule()); } const lib = b.addLibrary(.{ .name = "tile57", .linkage = .static, .root_module = lib_mod }); + // Android cross-compile: point the C deps at the NDK sysroot (see -Dandroid-ndk). + if (android_libc) |libc| lib.setLibCFile(libc); // The archive for zig-package consumers (lookout-core links it into its own // build): a named lazy path, NOT dep.artifact() — the default install step // installs the `tile57` CLI under the same name, and on macOS the lib From af2f1a0f5aca6ede4363e1cd4f3da9e1439a8c23 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Sat, 25 Jul 2026 12:32:26 -0400 Subject: [PATCH 2/3] build: auto-detect the NDK host toolchain dir for android androidLibcFile hard-coded darwin-x86_64; on other hosts (darwin-arm64 NDKs, linux) the sysroot path was wrong -> "stdio.h/assert.h file not found" when compiling the C deps. Probe the prebuilt/ dirs and pick the one that exists (via std.Io.Dir.accessAbsolute), falling back to the host-OS default. --- build.zig | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/build.zig b/build.zig index 15a70d01..ab9ed81c 100644 --- a/build.zig +++ b/build.zig @@ -218,13 +218,26 @@ fn androidTriple(target: std.Build.ResolvedTarget) ?[]const u8 { // Generate a Zig `--libc` config for the NDK sysroot and return it as a file the // caller pins to the android artifact via `setLibCFile`. The `asm/` arch headers // live in the triple subdir (sys_include_dir), which a plain `--sysroot` misses. -fn androidLibcFile(b: *std.Build, ndk: []const u8, triple: []const u8, api: u32) std.Build.LazyPath { - const host = switch (@import("builtin").os.tag) { - .macos => "darwin-x86_64", // the NDK ships x86_64 host binaries even on arm64 macs +fn ndkSysroot(b: *std.Build, ndk: []const u8) []const u8 { + const base = b.fmt("{s}/toolchains/llvm/prebuilt", .{ndk}); + // The NDK ships one host toolchain dir; pick the one that exists rather than + // assume (darwin-arm64 vs darwin-x86_64 vary by NDK/host). + const candidates = [_][]const u8{ "darwin-arm64", "darwin-x86_64", "linux-x86_64", "windows-x86_64" }; + for (candidates) |host| { + const sysroot = b.fmt("{s}/{s}/sysroot", .{ base, host }); + std.Io.Dir.accessAbsolute(b.graph.io, b.fmt("{s}/usr/include", .{sysroot}), .{}) catch continue; + return sysroot; + } + const def = switch (@import("builtin").os.tag) { + .macos => "darwin-x86_64", .windows => "windows-x86_64", else => "linux-x86_64", }; - const sysroot = b.fmt("{s}/toolchains/llvm/prebuilt/{s}/sysroot", .{ ndk, host }); + return b.fmt("{s}/{s}/sysroot", .{ base, def }); +} + +fn androidLibcFile(b: *std.Build, ndk: []const u8, triple: []const u8, api: u32) std.Build.LazyPath { + const sysroot = ndkSysroot(b, ndk); const content = b.fmt( \\include_dir={s}/usr/include \\sys_include_dir={s}/usr/include/{s} From daf13061910a44b392a5ba724c94c0015ba181bc Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Sat, 25 Jul 2026 12:44:59 -0400 Subject: [PATCH 3/3] build: probe the NDK host-toolchain dir OS-default-first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The darwin-arm64-first probe could pick a nonexistent toolchain dir on some setups (accessAbsolute behaviour varies), yielding a bad sysroot -> "stdlib.h not found". Order candidates host-OS-default first (darwin-x86_64 — what the NDK ships, even on Apple silicon) and fall back to it, so the sysroot is correct regardless. --- build.zig | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/build.zig b/build.zig index ab9ed81c..032cd378 100644 --- a/build.zig +++ b/build.zig @@ -220,20 +220,21 @@ fn androidTriple(target: std.Build.ResolvedTarget) ?[]const u8 { // live in the triple subdir (sys_include_dir), which a plain `--sysroot` misses. fn ndkSysroot(b: *std.Build, ndk: []const u8) []const u8 { const base = b.fmt("{s}/toolchains/llvm/prebuilt", .{ndk}); - // The NDK ships one host toolchain dir; pick the one that exists rather than - // assume (darwin-arm64 vs darwin-x86_64 vary by NDK/host). - const candidates = [_][]const u8{ "darwin-arm64", "darwin-x86_64", "linux-x86_64", "windows-x86_64" }; + // The NDK ships one host toolchain dir. Probe the host-OS default FIRST (what + // the NDK actually ships — e.g. darwin-x86_64 even on Apple silicon) so the + // result is correct regardless of how accessAbsolute behaves; only fall + // through to alternates (a future darwin-arm64 toolchain) if it's absent. + const candidates: []const []const u8 = switch (@import("builtin").os.tag) { + .macos => &.{ "darwin-x86_64", "darwin-arm64" }, + .windows => &.{"windows-x86_64"}, + else => &.{ "linux-x86_64", "linux-aarch64" }, + }; for (candidates) |host| { const sysroot = b.fmt("{s}/{s}/sysroot", .{ base, host }); std.Io.Dir.accessAbsolute(b.graph.io, b.fmt("{s}/usr/include", .{sysroot}), .{}) catch continue; return sysroot; } - const def = switch (@import("builtin").os.tag) { - .macos => "darwin-x86_64", - .windows => "windows-x86_64", - else => "linux-x86_64", - }; - return b.fmt("{s}/{s}/sysroot", .{ base, def }); + return b.fmt("{s}/{s}/sysroot", .{ base, candidates[0] }); // default; clear path in errors } fn androidLibcFile(b: *std.Build, ndk: []const u8, triple: []const u8, api: u32) std.Build.LazyPath {