# Native Android (Bionic) Build on Termux with Zig 0.15.2

**URL:** <https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897>\
**Category:** Showcase\
**Tags:** zig-0-15-2\
**Created:** [April 12, 2026, 11:09am UTC](https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897 "2026-04-12T11:09:47Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![Liveiciee](https://ziggit.dev/user_avatar/ziggit.dev/liveiciee/32/8253_2.png) [@Liveiciee](https://ziggit.dev/u/Liveiciee)\
**Post date:** [April 12, 2026, 11:09am UTC](https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897/1 "2026-04-12T11:09:47Z")

</div>

After way too many failed builds, I finally got a native Android binary — dynamically linked against Bionic libc — compiled directly on Termux using Zig 0.15.2, without musl.

Posting this because I couldn’t find a working solution anywhere, and I wasted a lot of time on dead ends.

> **Disclaimer:** This is a workaround, not official support. It will break when Zig ships proper Bionic support ([#23906](https://github.com/ziglang/zig/issues/23906)). Use for on-device experiments only.

* * *

## The Problem

Zig 0.15.2 doesn’t ship prebuilt Bionic libc. Any attempt to build an executable gives:

```zig
error: unable to provide libc for target 'aarch64-linux.5.10...6.16-android.34'

```

The frustrating part: even `link_libc = false` doesn’t help. Zig still injects `-lc -lm -ldl` internally for Android targets, and there’s no official API to stop it. `zig cc -nostdlib` also fails the internal target check.

The only way out is to bypass Zig’s linker entirely.

* * *

## What Finally Worked

The trick is to split compilation and linking into two separate steps:

1. **Compile to object** with `b.addObject()` and `.pic = true` — this avoids Zig’s libc check entirely
2. **Link manually** with `ld.lld` against `/system/lib64`
3. Use **NDK Clang’s `compiler_rt`** (not Zig’s cache — the cache version is non-PIC and causes `R_AARCH64_ABS64` errors)
4. Run **`termux-elf-cleaner`** at the end — without this the binary crashes on Android 14 with a TLS alignment error

The `build.zig` scans `$PREFIX/lib/clang/` for the highest installed Clang version to find `compiler_rt` dynamically, so it doesn’t break when Termux updates packages.

* * *

## build.zig

```zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const prefix = std.posix.getenv("PREFIX") orelse "/data/data/com.termux/files/usr";
    const is_termux = std.posix.getenv("TERMUX_VERSION") != null;

    if (!is_termux) {
        const exe = b.addExecutable(.{
            .name = "server",
            .root_module = b.createModule(.{
                .root_source_file = b.path("src/main.zig"),
                .target = b.standardTargetOptions(.{}),
                .optimize = b.standardOptimizeOption(.{}),
            }),
        });
        b.installArtifact(exe);
        return;
    }

    const target = b.resolveTargetQuery(.{
        .cpu_arch = .aarch64,
        .os_tag = .linux,
        .abi = .android,
        .android_api_level = 34,
    });
    const optimize = b.standardOptimizeOption(.{});

    const obj = b.addObject(.{
        .name = "server",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
            .link_libc = false,
            .pic = true, // required for PIE on Android
        }),
    });
    obj.root_module.addImport("websocket", b.dependency("websocket", .{
        .target = target, .optimize = optimize,
    }).module("websocket"));
    obj.addIncludePath(.{ .cwd_relative = b.fmt("{s}/include", .{prefix}) });

    const out = "zig-out/bin/server";
    const mkdir = b.addSystemCommand(&.{ "mkdir", "-p", "zig-out/bin" });
    mkdir.step.dependOn(&obj.step);

    // Find compiler_rt from highest installed Clang version
    const rt_path = blk: {
        var best_path: ?[]const u8 = null;
        var best_ver: u32 = 0;
        const clang_base = b.fmt("{s}/lib/clang", .{prefix});
        if (std.fs.openDirAbsolute(clang_base, .{ .iterate = true })) |tmp| {
            var dir = tmp;
            defer dir.close();
            var it = dir.iterate();
            while (it.next() catch null) |entry| {
                if (entry.kind != .directory) continue;
                const ver = std.fmt.parseInt(u32, entry.name, 10) catch continue;
                const cand = b.fmt("{s}/{s}/lib/linux/libclang_rt.builtins-aarch64-android.a", .{ clang_base, entry.name });
                std.fs.accessAbsolute(cand, .{}) catch continue;
                if (ver > best_ver) {
                    best_ver = ver;
                    if (best_path) |old| b.allocator.free(old);
                    best_path = b.dupe(cand);
                }
            }
        } else |_| {}
        if (best_path) |p| break :blk p;
        @panic("compiler_rt not found - install ndk-sysroot");
    };
    defer b.allocator.free(rt_path);

    const link = b.addSystemCommand(&.{
        "ld.lld", "-EL", "-z", "now", "-z", "relro", "-z", "max-page-size=16384",
        "--pack-dyn-relocs=android+relr", "--hash-style=gnu", "--eh-frame-hdr",
        "-m", "aarch64linux", "-pie",
        "-dynamic-linker", "/system/bin/linker64",
        "-o", out,
        b.fmt("{s}/lib/crtbegin_dynamic.o", .{prefix}),
    });
    link.addFileArg(obj.getEmittedBin());
    link.addArgs(&.{ "-L/system/lib64", b.fmt("-L{s}/lib", .{prefix}) });
    link.addArgs(&.{ "-lc", "-lm", "-ldl" });
    link.addArg(rt_path);
    link.addArg(b.fmt("{s}/lib/crtend_android.o", .{prefix}));
    link.step.dependOn(&mkdir.step);

    const cleaner = b.addSystemCommand(&.{ "termux-elf-cleaner", out });
    cleaner.step.dependOn(&link.step);

    const install = b.step("install-server", "Build for Termux");
    install.dependOn(&cleaner.step);
    b.step("run", "").dependOn(install);
}

```

* * *

## Verification

```zig
$ zig build install-server
using compiler_rt: /data/data/com.termux/files/usr/lib/clang/21/lib/linux/libclang_rt.builtins-aarch64-android.a
termux-elf-cleaner: Changing TLS alignment for 'zig-out/bin/server' to 64, instead of 8
termux-elf-cleaner: Replacing unsupported DF_1_* flags 134217729 with 1

$ ./zig-out/bin/server
[INFO] WebSocket server on ws://0.0.0.0:8080/ws
[INFO] Game loop started (12.5 ticks/sec)

```

`readelf -d` confirms `NEEDED libc.so`, `NEEDED libdl.so` — proper dynamic linking against Bionic.

* * *

## A Few Things That Tripped Me Up (Zig 0.15.2)

- `std.process.getenv` is gone, use `std.posix.getenv`
- `b.dupe()` no longer returns an error union — remove the `catch`
- `openDirAbsolute` result must be captured as `|tmp|` then `var dir = tmp`
- `Dir` and iterators must be `var`, not `const`
- `.pic = true` is mandatory for Android PIE — without it you get `R_AARCH64_ABS64` relocation errors
- Use NDK Clang’s `libclang_rt.builtins-aarch64-android.a`, not `libcompiler_rt.a` from Zig’s cache (it’s non-PIC)

* * *

Hopefully this saves someone else a long night. Waiting for proper Bionic support in a future Zig release.

Happy hacking 👻

---

<div class="post-metadata">

**Author:** ![vulpesx](https://ziggit.dev/user_avatar/ziggit.dev/vulpesx/32/3989_2.png) [@vulpesx](https://ziggit.dev/u/vulpesx)\
**Post date:** [April 12, 2026, 11:19am UTC](https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897/2 "2026-04-12T11:19:31Z")

</div>

you didn’t mention it, so I am curious if you tried `artifact.setLibcFile(b.path("to/file/describing/libc")`

the file is in the same format you get from `zig libc`, it describes where to find all the things zig needs to link to a libc.

---

<div class="post-metadata">

**Author:** ![Liveiciee](https://ziggit.dev/user_avatar/ziggit.dev/liveiciee/32/8253_2.png) [@Liveiciee](https://ziggit.dev/u/Liveiciee)\
**Post date:** [April 12, 2026, 11:21am UTC](https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897/3 "2026-04-12T11:21:18Z")

</div>

Yes, I tried `exe.setLibCFile()` early on! Created a `libc.txt` pointing to Termux’s Bionic paths:

include\_dir=/data/data/com.termux/files/usr/include  
sys\_include\_dir=/data/data/com.termux/files/usr/include  
crt\_dir=/data/data/com.termux/files/usr/lib  
…

Unfortunately, Zig 0.15.2 still internally injects `-lc -lm -ldl` flags and fails with “unable to provide libc for target”. Even with `link_libc = false` in the module, the flags persist. That’s what ultimately forced the manual `ld.lld` approach.

If you know a way to _truly_ disable those flags, I’m all ears! 👻

---

<div class="post-metadata">

**Author:** ![Liveiciee](https://ziggit.dev/user_avatar/ziggit.dev/liveiciee/32/8253_2.png) [@Liveiciee](https://ziggit.dev/u/Liveiciee)\
**Post date:** [April 13, 2026, 1:43pm UTC](https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897/4 "2026-04-13T13:43:27Z")

</div>

**Update - Apr 13, 2026**

No one asked for this update, but I’ve been using it every day anyway. Here’s a nicer version. Still no official Bionic support from Zig — we wait.

Changes:

- auto-detects arch via uname -m and picks the right compiler\_rt, linker, and lib dir for aarch64, x86\_64, arm, and i686
- adds a tiny per-arch cache for the compiler\_rt path, so the first build scans `/lib/clang` once, later builds reuse it
- on my Termux device this dropped incremental builds from ~20s to ~0.7s

No behavior change otherwise, still needs `termux-elf-cleaner`.

Verification:

```plaintext
$ zig build
using compiler_rt: /data/data/com.termux/files/usr/lib/clang/21/lib/linux/libclang_rt.builtins-aarch64-android.a
termux-elf-cleaner: Changing TLS alignment for 'zig-out/bin/server' to 64, instead of 8
termux-elf-cleaner: Replacing unsupported DF_1_* flags 134217729 with 1 in 'zig-out/bin/server'

```

Full updated `build.zig`:

```zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const prefix = std.posix.getenv("PREFIX") orelse "/data/data/com.termux/files/usr";
    const is_termux = std.posix.getenv("TERMUX_VERSION")!= null;

    if (!is_termux) {
        const exe = b.addExecutable(.{
           .name = "server",
           .root_module = b.createModule(.{
               .root_source_file = b.path("src/main.zig"),
               .target = b.standardTargetOptions(.{}),
               .optimize = b.standardOptimizeOption(.{}),
            }),
        });
        b.installArtifact(exe);
        return;
    }

    const arch_info = detectArch(b) catch @panic("failed to detect arch");
    defer b.allocator.free(arch_info.triple);
    defer b.allocator.free(arch_info.rt_name);
    defer b.allocator.free(arch_info.linker);
    defer b.allocator.free(arch_info.lib_dir);
    defer b.allocator.free(arch_info.lld_emul);

    const target = b.resolveTargetQuery(.{
       .cpu_arch = arch_info.cpu_arch,
       .os_tag =.linux,
       .abi =.android,
       .android_api_level = 34,
    });
    const optimize = b.standardOptimizeOption(.{});

    const obj = b.addObject(.{
       .name = "server",
       .root_module = b.createModule(.{
           .root_source_file = b.path("src/main.zig"),
           .target = target,
           .optimize = optimize,
           .link_libc = false,
           .pic = true,
        }),
    });
    obj.root_module.addImport("websocket", b.dependency("websocket",.{
       .target = target,.optimize = optimize,
    }).module("websocket"));
    obj.addIncludePath(.{.cwd_relative = b.fmt("{s}/include",.{prefix}) });

    const out = "zig-out/bin/server";
    const mkdir = b.addSystemCommand(&.{ "mkdir", "-p", "zig-out/bin" });
    mkdir.step.dependOn(&obj.step);

    const rt_path = getCachedRtPath(b, prefix, arch_info.rt_name, arch_info.triple) catch @panic("compiler_rt not found - install ndk-sysroot");
    defer b.allocator.free(rt_path);

    const link = b.addSystemCommand(&.{
        "ld.lld", "-EL", "-z", "now", "-z", "relro", "-z", "max-page-size=16384",
        "--pack-dyn-relocs=android+relr", "--hash-style=gnu", "--eh-frame-hdr",
        "-m", arch_info.lld_emul, "-pie",
        "-dynamic-linker", arch_info.linker,
        "-o", out,
        b.fmt("{s}/lib/crtbegin_dynamic.o",.{prefix}),
    });
    link.addFileArg(obj.getEmittedBin());
    link.addArgs(&.{ b.fmt("-L{s}",.{arch_info.lib_dir}), b.fmt("-L{s}/lib",.{prefix}) });
    link.addArgs(&.{ "-lc", "-lm", "-ldl" });
    link.addArg(rt_path);
    link.addArg(b.fmt("{s}/lib/crtend_android.o",.{prefix}));
    link.step.dependOn(&mkdir.step);

    const cleaner = b.addSystemCommand(&.{ "termux-elf-cleaner", out });
    cleaner.step.dependOn(&link.step);

    const install = b.step("install-server", "Build for Termux");
    install.dependOn(&cleaner.step);
    b.default_step.dependOn(install);
}

const ArchInfo = struct {
    cpu_arch: std.Target.Cpu.Arch,
    triple: []const u8,
    rt_name: []const u8,
    linker: []const u8,
    lib_dir: []const u8,
    lld_emul: []const u8,
};

fn detectArch(b: *std.Build)!ArchInfo {
    const result = b.run(&.{ "uname", "-m" });
    const arch = std.mem.trim(u8, result, " \n\r\t");

    if (std.mem.eql(u8, arch, "aarch64")) {
        return.{
           .cpu_arch =.aarch64,
           .triple = try b.allocator.dupe(u8, "aarch64"),
           .rt_name = try b.allocator.dupe(u8, "libclang_rt.builtins-aarch64-android.a"),
           .linker = try b.allocator.dupe(u8, "/system/bin/linker64"),
           .lib_dir = try b.allocator.dupe(u8, "/system/lib64"),
           .lld_emul = try b.allocator.dupe(u8, "aarch64linux"),
        };
    } else if (std.mem.eql(u8, arch, "x86_64")) {
        return.{
           .cpu_arch =.x86_64,
           .triple = try b.allocator.dupe(u8, "x86_64"),
           .rt_name = try b.allocator.dupe(u8, "libclang_rt.builtins-x86_64-android.a"),
           .linker = try b.allocator.dupe(u8, "/system/bin/linker64"),
           .lib_dir = try b.allocator.dupe(u8, "/system/lib64"),
           .lld_emul = try b.allocator.dupe(u8, "elf_x86_64"),
        };
    } else if (std.mem.eql(u8, arch, "i686") or std.mem.eql(u8, arch, "i386")) {
        return.{
           .cpu_arch =.x86,
           .triple = try b.allocator.dupe(u8, "i686"),
           .rt_name = try b.allocator.dupe(u8, "libclang_rt.builtins-i686-android.a"),
           .linker = try b.allocator.dupe(u8, "/system/bin/linker"),
           .lib_dir = try b.allocator.dupe(u8, "/system/lib"),
           .lld_emul = try b.allocator.dupe(u8, "elf_i386"),
        };
    } else if (std.mem.startsWith(u8, arch, "arm")) {
        return.{
           .cpu_arch =.arm,
           .triple = try b.allocator.dupe(u8, "arm"),
           .rt_name = try b.allocator.dupe(u8, "libclang_rt.builtins-arm-android.a"),
           .linker = try b.allocator.dupe(u8, "/system/bin/linker"),
           .lib_dir = try b.allocator.dupe(u8, "/system/lib"),
           .lld_emul = try b.allocator.dupe(u8, "armelf_linux_eabi"),
        };
    } else {
        std.debug.print("Unknown arch '{s}', falling back to aarch64\n",.{arch});
        return.{
           .cpu_arch =.aarch64,
           .triple = try b.allocator.dupe(u8, "aarch64"),
           .rt_name = try b.allocator.dupe(u8, "libclang_rt.builtins-aarch64-android.a"),
           .linker = try b.allocator.dupe(u8, "/system/bin/linker64"),
           .lib_dir = try b.allocator.dupe(u8, "/system/lib64"),
           .lld_emul = try b.allocator.dupe(u8, "aarch64linux"),
        };
    }
}

fn getCachedRtPath(b: *std.Build, prefix: []const u8, rt_name: []const u8, arch: []const u8)![]u8 {
    const cache_dir = "zig-cache";
    const cache_file = b.fmt("{s}/rt_path_{s}.txt",.{ cache_dir, arch });
    std.fs.cwd().makePath(cache_dir) catch {};

    if (std.fs.cwd().readFileAlloc(b.allocator, cache_file, 1024)) |cached| {
        defer b.allocator.free(cached);
        const path = std.mem.trim(u8, cached, " \n\r\t");
        std.fs.accessAbsolute(path,.{}) catch {
            return scanRtPath(b, prefix, rt_name, cache_file);
        };
        std.debug.print("CACHE HIT [{s}]: {s}\n",.{ arch, path });
        return b.allocator.dupe(u8, path);
    } else |_| {
        return scanRtPath(b, prefix, rt_name, cache_file);
    }
}

fn scanRtPath(b: *std.Build, prefix: []const u8, rt_name: []const u8, cache_file: []const u8)![]u8 {
    var best_path:?[]u8 = null;
    var best_ver: u32 = 0;
    const clang_base = b.fmt("{s}/lib/clang",.{prefix});

    if (std.fs.openDirAbsolute(clang_base,.{.iterate = true })) |tmp| {
        var dir = tmp;
        defer dir.close();
        var it = dir.iterate();
        while (it.next() catch null) |entry| {
            if (entry.kind!=.directory) continue;
            const ver = std.fmt.parseInt(u32, entry.name, 10) catch continue;
            const cand = b.fmt("{s}/{s}/lib/linux/{s}",.{ clang_base, entry.name, rt_name });
            std.fs.accessAbsolute(cand,.{}) catch continue;
            if (ver > best_ver) {
                best_ver = ver;
                if (best_path) |old| b.allocator.free(old);
                best_path = try b.allocator.dupe(u8, cand);
            }
        }
    } else |_| {}

    const path = best_path orelse return error.NotFound;
    std.fs.cwd().writeFile(.{.sub_path = cache_file,.data = path }) catch {};
    std.debug.print("using compiler_rt: {s}\n",.{path});
    return path;
}

```

---

<div class="post-metadata">

**Author:** ![Liveiciee](https://ziggit.dev/user_avatar/ziggit.dev/liveiciee/32/8253_2.png) [@Liveiciee](https://ziggit.dev/u/Liveiciee)\
**Post date:** [April 13, 2026, 4:36pm UTC](https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897/5 "2026-04-13T16:36:30Z")

</div>

**UPDATE Apr 14 - Termux full LLVM build**

Fixed the panic from Apr 13. Changes:

- rename custom install step to install-bin
- use llvm-strip instead of GNU strip
- robust compiler\_rt caching
- full arch detection

tested on Zig 0.15.2, clang 21.1.8, Android API 34 (Termux)

Verification:

```plaintext
$ zig build run
→ Android API 34 | git:b4e6b98
using compiler_rt: /data/.../libclang_rt.builtins-aarch64-android.a
CACHE HIT :...
termux-elf-cleaner: Changing TLS alignment...
[16:27:00] INFO(default): Game loaded from save.bin
[16:27:00] INFO(default): WebSocket server on ws://0.0.0.0:8080/ws

```

Full `build.zig`:

```zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const prefix = std.posix.getenv("PREFIX") orelse "/data/data/com.termux/files/usr";
    const is_termux = std.posix.getenv("TERMUX_VERSION") != null;

    if (!is_termux) {
        const exe = b.addExecutable(.{
            .name = "server",
            .root_module = b.createModule(.{
                .root_source_file = b.path("src/main.zig"),
                .target = b.standardTargetOptions(.{}),
                .optimize = b.standardOptimizeOption(.{}),
            }),
        });
        b.installArtifact(exe);
        return;
    }

    const api_level = detectApiLevel(b);
    const optimize = b.standardOptimizeOption(.{});
    const build_all = b.option(bool, "all", "Build for all Android architectures") orelse false;
    const git_hash = getGitHash(b);

    std.debug.print("→ Android API {d} | git:{s}\n", .{ api_level, git_hash });

    const opts = b.addOptions();
    opts.addOption([]const u8, "git_hash", git_hash);
    opts.addOption(u32, "android_api", api_level);

    if (build_all) {
        const all_step = b.step("build-all", "Build for aarch64, x86_64, arm, i686");
        const arches = [_]std.Target.Cpu.Arch{ .aarch64, .x86_64, .arm, .x86 };
        for (arches) |cpu| {
            const info = archInfo(cpu);
            const step = buildOne(b, prefix, info, api_level, optimize, true, opts);
            all_step.dependOn(step);
        }
        b.default_step.dependOn(all_step);
        return;
    }

    const info = detectArch(b) catch @panic("arch detect failed");
    const final_step = buildOne(b, prefix, info, api_level, optimize, false, opts);
    b.default_step.dependOn(final_step);

    const run_cmd = b.addSystemCommand(&.{ "./zig-out/bin/server" });
    run_cmd.step.dependOn(final_step);
    const run_step = b.step("run", "Build and run");
    run_step.dependOn(&run_cmd.step);

    const watch_cmd = b.addSystemCommand(&.{ "sh", "-c", "while inotifywait -qq -r -e modify,close_write,create,delete src build.zig 2>/dev/null; do clear; echo '=== rebuild ==='; zig build run; done" });
    const watch_step = b.step("watch", "Watch src/ and auto-run");
    watch_step.dependOn(&watch_cmd.step);

    const rel_step = buildOne(b, prefix, info, api_level, .ReleaseSmall, false, opts);
    const strip_cmd = b.addSystemCommand(&.{ "llvm-strip", "zig-out/bin/server" });
    strip_cmd.step.dependOn(rel_step);
    const size_cmd = b.addSystemCommand(&.{ "sh", "-c", "du -h zig-out/bin/server" });
    size_cmd.step.dependOn(&strip_cmd.step);
    const release_step = b.step("release", "ReleaseSmall + strip");
    release_step.dependOn(&size_cmd.step);

    const install_cmd = b.addSystemCommand(&.{ "cp", "-f", "zig-out/bin/server", b.fmt("{s}/bin/server", .{prefix}) });
    install_cmd.step.dependOn(final_step);
    const install_step = b.step("install-bin", "Install to $PREFIX/bin");
    install_step.dependOn(&install_cmd.step);

    const deps_cmd = b.addSystemCommand(&.{ "sh", "-c", "pkg list-installed 2>/dev/null | grep -q '^clang/' || pkg install -y clang lld ndk-sysroot termux-elf-cleaner inotify-tools" });
    const deps_step = b.step("deps", "Install Termux deps");
    deps_step.dependOn(&deps_cmd.step);

    const clean_cache = b.addSystemCommand(&.{ "rm", "-rf", "zig-cache" });
    b.step("clean-cache", "Clear rt cache").dependOn(&clean_cache.step);
    const clean_all = b.addSystemCommand(&.{ "rm", "-rf", "zig-out", "zig-cache" });
    b.step("clean-all", "Nuke everything").dependOn(&clean_all.step);
}

const ArchInfo = struct {
    cpu_arch: std.Target.Cpu.Arch,
    triple: []const u8,
    rt_name: []const u8,
    linker: []const u8,
    lib_dir: []const u8,
    lld_emul: []const u8,
};

fn archInfo(cpu: std.Target.Cpu.Arch) ArchInfo {
    return switch (cpu) {
        .aarch64 => .{ .cpu_arch = .aarch64, .triple = "aarch64", .rt_name = "libclang_rt.builtins-aarch64-android.a", .linker = "/system/bin/linker64", .lib_dir = "/system/lib64", .lld_emul = "aarch64linux" },
        .x86_64 => .{ .cpu_arch = .x86_64, .triple = "x86_64", .rt_name = "libclang_rt.builtins-x86_64-android.a", .linker = "/system/bin/linker64", .lib_dir = "/system/lib64", .lld_emul = "elf_x86_64" },
        .x86 => .{ .cpu_arch = .x86, .triple = "i686", .rt_name = "libclang_rt.builtins-i686-android.a", .linker = "/system/bin/linker", .lib_dir = "/system/lib", .lld_emul = "elf_i386" },
        .arm => .{ .cpu_arch = .arm, .triple = "arm", .rt_name = "libclang_rt.builtins-arm-android.a", .linker = "/system/bin/linker", .lib_dir = "/system/lib", .lld_emul = "armelf_linux_eabi" },
        else => @panic("unsupported"),
    };
}

fn detectArch(b: *std.Build) !ArchInfo {
    const out = b.run(&.{ "uname", "-m" });
    const arch = std.mem.trim(u8, out, " \n\r\t");
    if (std.mem.eql(u8, arch, "aarch64")) return archInfo(.aarch64);
    if (std.mem.eql(u8, arch, "x86_64")) return archInfo(.x86_64);
    if (std.mem.eql(u8, arch, "x86") or std.mem.eql(u8, arch, "i386") or std.mem.eql(u8, arch, "i686")) return archInfo(.x86);
    if (std.mem.startsWith(u8, arch, "arm")) return archInfo(.arm);
    return archInfo(.aarch64);
}

fn detectApiLevel(b: *std.Build) u32 {
    const r = std.process.Child.run(.{
        .allocator = b.allocator,
        .argv = &.{ "getprop", "ro.build.version.sdk" },
    }) catch return 34;
    defer b.allocator.free(r.stdout);
    defer b.allocator.free(r.stderr);
    const s = std.mem.trim(u8, r.stdout, " \n\r\t");
    return std.fmt.parseInt(u32, s, 10) catch 34;
}

fn getGitHash(b: *std.Build) []const u8 {
    const r = std.process.Child.run(.{
        .allocator = b.allocator,
        .argv = &.{ "git", "rev-parse", "--short", "HEAD" },
    }) catch return "nogit";
    defer b.allocator.free(r.stdout);
    defer b.allocator.free(r.stderr);
    const h = std.mem.trim(u8, r.stdout, " \n\r\t");
    return b.allocator.dupe(u8, h) catch "nogit";
}

fn buildOne(b: *std.Build, prefix: []const u8, info: ArchInfo, api: u32, optimize: std.builtin.OptimizeMode, use_subdir: bool, opts: *std.Build.Step.Options) *std.Build.Step {
    const target = b.resolveTargetQuery(.{
        .cpu_arch = info.cpu_arch,
        .os_tag = .linux,
        .abi = .android,
        .android_api_level = api,
    });

    const mod = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = false,
        .pic = true,
    });
    mod.addOptions("build_info", opts);

    const obj = b.addObject(.{
        .name = b.fmt("server-{s}", .{info.triple}),
        .root_module = mod,
    });
    obj.root_module.addImport("websocket", b.dependency("websocket", .{ .target = target, .optimize = optimize }).module("websocket"));
    obj.addIncludePath(.{ .cwd_relative = b.fmt("{s}/include", .{prefix}) });

    const out_dir = if (use_subdir) b.fmt("zig-out/{s}/bin", .{info.triple}) else "zig-out/bin";
    const out = b.fmt("{s}/server", .{out_dir});

    const mkdir = b.addSystemCommand(&.{ "mkdir", "-p", out_dir });
    mkdir.step.dependOn(&obj.step);

    const rt_path = getCachedRtPath(b, prefix, info.rt_name, info.triple) catch {
        std.debug.print("\nERROR: compiler_rt missing for {s}\nRun: pkg install clang lld\n", .{info.triple});
        @panic("cannot continue");
    };
    defer b.allocator.free(rt_path);

    const link = b.addSystemCommand(&.{
        "ld.lld", "-EL", "-z", "now", "-z", "relro", "-z", "max-page-size=16384",
        "--pack-dyn-relocs=android+relr", "--hash-style=gnu", "--eh-frame-hdr",
        "-m", info.lld_emul, "-pie",
        "-dynamic-linker", info.linker,
        "-o", out,
        b.fmt("{s}/lib/crtbegin_dynamic.o", .{prefix}),
    });
    link.addFileArg(obj.getEmittedBin());
    link.addArgs(&.{ b.fmt("-L{s}", .{info.lib_dir}), b.fmt("-L{s}/lib", .{prefix}) });
    link.addArgs(&.{ "-lc", "-lm", "-ldl" });
    link.addArg(rt_path);
    link.addArg(b.fmt("{s}/lib/crtend_android.o", .{prefix}));
    link.step.dependOn(&mkdir.step);

    const cleaner = b.addSystemCommand(&.{ "termux-elf-cleaner", out });
    cleaner.step.dependOn(&link.step);

    return &cleaner.step;
}

fn getCachedRtPath(b: *std.Build, prefix: []const u8, rt_name: []const u8, arch: []const u8) ![]u8 {
    const cache_dir = "zig-cache";
    const cache_file = b.fmt("{s}/rt_{s}.txt", .{ cache_dir, arch });
    std.fs.cwd().makePath(cache_dir) catch {};

    const clang_base = b.fmt("{s}/lib/clang", .{prefix});
    var current_max: u32 = 0;
    if (std.fs.openDirAbsolute(clang_base, .{ .iterate = true })) |tmp| {
        var dir = tmp;
        defer dir.close();
        var it = dir.iterate();
        while (it.next() catch null) |e| {
            if (e.kind != .directory) continue;
            const v = std.fmt.parseInt(u32, e.name, 10) catch continue;
            if (v > current_max) current_max = v;
        }
    } else |_| {}

    if (std.fs.cwd().readFileAlloc(b.allocator, cache_file, 1024)) |cached| {
        defer b.allocator.free(cached);
        var it = std.mem.splitScalar(u8, cached, '\n');
        const path = std.mem.trim(u8, it.first(), " \n\r\t");
        const ver = std.fmt.parseInt(u32, std.mem.trim(u8, it.next() orelse "0", " \n\r\t"), 10) catch 0;
        if (ver == current_max and ver != 0) {
            std.fs.accessAbsolute(path, .{}) catch return scanRt(b, prefix, rt_name, cache_file, current_max);
            std.debug.print("CACHE HIT [{s}]: {s}\n", .{ arch, path });
            return b.allocator.dupe(u8, path);
        }
    } else |_| {}
    return scanRt(b, prefix, rt_name, cache_file, current_max);
}

fn scanRt(b: *std.Build, prefix: []const u8, rt_name: []const u8, cache_file: []const u8, max_ver: u32) ![]u8 {
    var best: ?[]u8 = null;
    var best_ver: u32 = 0;
    const base = b.fmt("{s}/lib/clang", .{prefix});
    if (std.fs.openDirAbsolute(base, .{ .iterate = true })) |tmp| {
        var dir = tmp;
        defer dir.close();
        var it = dir.iterate();
        while (it.next() catch null) |e| {
            if (e.kind != .directory) continue;
            const v = std.fmt.parseInt(u32, e.name, 10) catch continue;
            const cand = b.fmt("{s}/{s}/lib/linux/{s}", .{ base, e.name, rt_name });
            std.fs.accessAbsolute(cand, .{}) catch continue;
            if (v > best_ver) {
                best_ver = v;
                if (best) |o| b.allocator.free(o);
                best = try b.allocator.dupe(u8, cand);
            }
        }
    } else |_| {}
    const path = best orelse return error.NotFound;
    const data = b.fmt("{s}\n{d}", .{ path, if (best_ver == 0) max_ver else best_ver });
    std.fs.cwd().writeFile(.{ .sub_path = cache_file, .data = data }) catch {};
    std.debug.print("using compiler_rt: {s}\n", .{path});
    return path;
}

```

---

<div class="post-metadata">

**Author:** ![Liveiciee](https://ziggit.dev/user_avatar/ziggit.dev/liveiciee/32/8253_2.png) [@Liveiciee](https://ziggit.dev/u/Liveiciee)\
**Post date:** [April 13, 2026, 7:55pm UTC](https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897/6 "2026-04-13T19:55:50Z")

</div>

**Quick update to my April 14 post**

That version still worked, but it relied on `termux-elf-cleaner` and manual ld.lld flags. With Zig 0.15.2 I wanted something self-contained, so I rewrote the build to do the fix inside Zig.

The only real change is a tiny `FixElf` step that runs after linking. It just opens the binary and patches the `PT_GNU_RELRO` alignment to 64, which Android Bionic needs. No external tools anymore.

just works, and the output is way smaller too:

- Debug: ~3.6 MB
- ReleaseFast: ~221 KB
- ReleaseSmall: ~130 KB

Full `build.zig` I’m using now:

```zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.resolveTargetQuery(.{
    .cpu_arch =.aarch64,
    .os_tag =.linux,
    .abi =.android,
    });
    const optimize = b.standardOptimizeOption(.{});
    const embed_self = b.option(bool, "embed-self", "Embed source tar into binary") orelse false;

    const opts = b.addOptions();
    opts.addOption([]const u8, "git", getGit(b));
    opts.addOption([]const u8, "date", "2026-04-14");
    opts.addOption([]const u8, "zig_version", @import("builtin").zig_version_string);

    const wf = b.addWriteFiles();
    _ = wf.addCopyFile(b.path("client/index.html"), "index.html");
    _ = wf.addCopyFile(b.path("client/style.css"), "style.css");

    const js_gz_cmd = b.addSystemCommand(&.{ "sh", "-c", "gzip -c9 client/app.js 2>/dev/null || cat client/app.js" });
    const js_gz = js_gz_cmd.captureStdOut();
    _ = wf.addCopyFile(js_gz, "app.js.gz");

    const assets_zig = wf.add("assets.zig",
        \\pub const index = @embedFile("index.html");
        \\pub const style = @embedFile("style.css");
        \\pub const app_js_gz = @embedFile("app.js.gz");
    );
    const assets_mod = b.addModule("assets",.{.root_source_file = assets_zig });

    const proto_zig = wf.add("protocol.zig",
        \\pub const Packet = packed struct { x: f32, y: f32, hp: u8, id: u16 };
        \\pub const SIZE = @sizeOf(Packet);
    );
    const proto_mod = b.addModule("proto",.{.root_source_file = proto_zig });

    const ws_dummy_build = wf.add("ws_build.zig", "");
    const ws_build_mod = b.addModule("build",.{.root_source_file = ws_dummy_build });
    const ws_mod = b.addModule("websocket",.{
   .root_source_file = b.path("libs/websocket/src/websocket.zig"),
    });
    ws_mod.addImport("build", ws_build_mod);

    const modes = [_]std.builtin.OptimizeMode{.Debug,.ReleaseSmall,.ReleaseFast };
    var exes: [3]*std.Build.Step.Compile = undefined;
    var fixes: [3]*FixElf = undefined;

    for (modes, 0..) |m, i| {
        const exe = b.addExecutable(.{
       .name = b.fmt("server-{s}",.{@tagName(m)}),
       .root_module = b.createModule(.{
           .root_source_file = b.path("src/main.zig"),
           .target = target,
           .optimize = m,
           .strip = m!=.Debug,
            }),
        });
        exe.pie = true;
        exe.root_module.addOptions("build_info", opts);
        exe.root_module.addImport("assets", assets_mod);
        exe.root_module.addImport("proto", proto_mod);
        exe.root_module.addImport("websocket", ws_mod);
        exe.step.dependOn(&wf.step);

        if (embed_self) {
            const tar = b.addSystemCommand(&.{ "sh", "-c", "tar czf - src build.zig" });
            const self_wf = b.addWriteFiles();
            _ = self_wf.addCopyFile(tar.captureStdOut(), "self.tgz");
            const self_zig = self_wf.add("self.zig", "pub const bundle = @embedFile(\"self.tgz\");\n");
            const self_mod = b.addModule("self",.{.root_source_file = self_zig });
            exe.root_module.addImport("self", self_mod);
            exe.step.dependOn(&self_wf.step);
        }

        const fix = FixElf.create(b, exe);
        fix.step.dependOn(&exe.step);
        b.getInstallStep().dependOn(&fix.step);
        b.installArtifact(exe);
        exes[i] = exe;
        fixes[i] = fix;

        if (m == optimize) {
            const install = b.addInstallFile(exe.getEmittedBin(), "bin/server");
            b.getInstallStep().dependOn(&install.step);
        }
    }

    const bench_step = b.step("bench", "Show sizes");
    for (exes, fixes) |exe, fix| {
        const show = b.addSystemCommand(&.{"ls","-lh"});
        show.addArtifactArg(exe);
        show.step.dependOn(&fix.step);
        bench_step.dependOn(&show.step);
    }
}

fn getGit(b: *std.Build) []const u8 {
    const child = std.process.Child.run(.{
   .allocator = b.allocator,
   .argv = &.{ "git", "rev-parse", "--short", "HEAD" },
    }) catch return "dev";
    defer b.allocator.free(child.stdout);
    defer b.allocator.free(child.stderr);
    return b.dupe(std.mem.trim(u8, child.stdout, " \r\n"));
}

const FixElf = struct {
    step: std.Build.Step,
    b: *std.Build,
    exe: *std.Build.Step.Compile,
    pub fn create(b: *std.Build, e: *std.Build.Step.Compile) *FixElf {
        const s = b.allocator.create(FixElf) catch @panic("oom");
        s.* =.{.step = std.Build.Step.init(.{.id =.custom,.name = "fix-elf",.owner = b,.makeFn = make}),.b = b,.exe = e};
        return s;
    }
    fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions)!void {
        const self: *FixElf = @fieldParentPtr("step", step);
        const path = self.exe.getEmittedBin().getPath(self.b);
        var f = try std.fs.cwd().openFile(path,.{.mode =.read_write }); defer f.close();
        var hdr: [64]u8 = undefined; _ = try f.preadAll(&hdr, 0); if (hdr[4]!=2) return;
        const phoff = std.mem.readInt(u64, hdr[32..40],.little);
        const phentsize = std.mem.readInt(u16, hdr[54..56],.little);
        const phnum = std.mem.readInt(u16, hdr[56..58],.little);
        var i: usize = 0; while (i < phnum) : (i += 1) { var ph: [56]u8 = undefined; _ = try f.preadAll(&ph, phoff + i * phentsize); if (std.mem.readInt(u32, ph[0..4],.little) == 7) { std.mem.writeInt(u64, ph[48..56], 64,.little); try f.pwriteAll(&ph, phoff + i * phentsize); break; } }
    }
};

```

Much cleaner than before, and it’s 100% Zig now. No more hunting for compiler\_rt or running cleaner by hand. Thank God 😭

---

<div class="post-metadata">

**Author:** ![Liveiciee](https://ziggit.dev/user_avatar/ziggit.dev/liveiciee/32/8253_2.png) [@Liveiciee](https://ziggit.dev/u/Liveiciee)\
**Post date:** [April 14, 2026, 2:24am UTC](https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897/7 "2026-04-14T02:24:07Z")

</div>

### **Termux + Zig 0.15.2, simplified stable form**

It’s basically a minimal Android build.zig, and I deleted almost everything.

**The change**

- Removed multi-arch build system
- Removed runtime probing and git/env logic
- Removed asset pipeline and extra build orchestration
- Kept single deterministic Android target + ELF fix only

Build.zig

```zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const resolved_target = if (target.query.os_tag == null)
        b.resolveTargetQuery(.{
            .cpu_arch = .aarch64,
            .os_tag = .linux,
            .abi = .android,
            .android_api_level = b.option(u32, "android-api", "Android API level") orelse 29,
        })
    else target;

    const exe = b.addExecutable(.{
        .name = "server",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = resolved_target,
            .optimize = optimize,
        }),
    });

    exe.pie = true;
    exe.root_module.link_libc = false;
    exe.root_module.strip = optimize != .Debug;
    exe.link_z_max_page_size = 16384;

    const build_options = b.addOptions();
    build_options.addOption(bool, "is_windows", false);
    build_options.addOption(bool, "is_darwin", false);
    build_options.addOption(bool, "is_linux", true);
    const build_mod = build_options.createModule();

    const websocket_mod = b.createModule(.{
        .root_source_file = b.path("libs/websocket/src/websocket.zig"),
        .target = resolved_target,
        .optimize = optimize,
    });

    websocket_mod.addImport("build", build_mod);
    exe.root_module.addImport("websocket", websocket_mod);

    b.installArtifact(exe);

    const fix = AndroidElfFix.create(b, exe);
    fix.step.dependOn(&exe.step);
    b.getInstallStep().dependOn(&fix.step);

    const check = b.step("check", "type check");
    check.dependOn(&exe.step);
}

const AndroidElfFix = struct {
    step: std.Build.Step,
    b: *std.Build,
    exe: *std.Build.Step.Compile,

    pub fn create(b: *std.Build, exe: *std.Build.Step.Compile) *AndroidElfFix {
        const self = b.allocator.create(AndroidElfFix) catch @panic("oom");
        self.* = .{
            .step = std.Build.Step.init(.{
                .id = .custom,
                .name = "fix elf",
                .owner = b,
                .makeFn = make,
            }),
            .b = b,
            .exe = exe,
        };
        return self;
    }

    fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
        const self: *AndroidElfFix = @fieldParentPtr("step", step);
        const path = self.exe.getEmittedBin().getPath(self.b);

        var file = try std.fs.cwd().openFile(path, .{ .mode = .read_write });
        defer file.close();

        var hdr: [64]u8 = undefined;
        _ = try file.preadAll(&hdr, 0);
        if (hdr[4] != 2) return;

        const phoff = std.mem.readInt(u64, hdr[32..40], .little);
        const phentsize = std.mem.readInt(u16, hdr[54..56], .little);
        const phnum = std.mem.readInt(u16, hdr[56..58], .little);

        var i: usize = 0;
        while (i < phnum) : (i += 1) {
            const off = phoff + i * phentsize;
            var ph: [56]u8 = undefined;
            _ = try file.preadAll(&ph, off);

            if (std.mem.readInt(u32, ph[0..4], .little) == 0x6474e552) {
                const old = std.mem.readInt(u64, ph[48..56], .little);
                if (old >= 16384) return;

                std.mem.writeInt(u64, ph[48..56], 16384, .little);
                try file.pwriteAll(&ph, off);
                return;
            }
        }
    }
};

```

**Known issues:**

- [#23906](https://github.com/ziglang/zig/issues/23906): Bionic libc not officially supported
- [#23813](https://github.com/ziglang/zig/issues/23813): dynamic linker skipped when link\_libc = false
- [#20117](https://github.com/ziglang/zig/issues/20117): getcontext error when linking libc on Android
- [#31306](https://codeberg.org/ziglang/zig/issues/31306): 16K page size should be automatic

Apr 14, 2026

---

<div class="post-metadata">

**Author:** ![Liveiciee](https://ziggit.dev/user_avatar/ziggit.dev/liveiciee/32/8253_2.png) [@Liveiciee](https://ziggit.dev/u/Liveiciee)\
**Post date:** [April 16, 2026, 1:18pm UTC](https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897/8 "2026-04-16T13:18:59Z")

</div>

update: this is the end 😹 turns out my kernel is 4.19 and zig 0.16.0 dropped support for anything below 5.10. staying on 0.15.2 until i either get a new phone or touch grass.

[0.16.0 Release Notes](https://ziglang.org/download/0.16.0/release-notes.html#OS-Version-Requirements)

---

<div class="post-metadata">

**Author:** ![MeKaLu](https://ziggit.dev/user_avatar/ziggit.dev/mekalu/32/6360_2.png) [@MeKaLu](https://ziggit.dev/u/MeKaLu)\
**Post date:** [April 16, 2026, 4:38pm UTC](https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897/9 "2026-04-16T16:38:40Z")

</div>

Providing a libc file actually worked for me, even with `link_libc = true`

Although i am doing this from linux and macos maybe something to do with it? Anyways here is the code(my build script is by no means perfect but it works): [https://codeberg.org/MeKaLu/Kut/src/branch/master/build.zig](https://codeberg.org/MeKaLu/Kut/src/branch/master/build.zig)

Take a look at `Android.zig` as well.

---

<div class="post-metadata">

**Author:** ![Liveiciee](https://ziggit.dev/user_avatar/ziggit.dev/liveiciee/32/8253_2.png) [@Liveiciee](https://ziggit.dev/u/Liveiciee)\
**Post date:** [April 17, 2026, 11:41am UTC](https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897/10 "2026-04-17T11:41:24Z")

</div>

Thanks for sharing build.zig and Android.zig, that’s clean.

Now I get why link\_libc = true worked for you. Generating a libc file on the fly pointing to NDK paths makes total sense. You’re basically handing Zig a full map of the toolchain so the linker doesn’t have to guess. Smart.

Also makes it obvious why our setups are completely different. Yours is the right way, cross-compiling from a real machine with the full NDK, building proper APKs and all. Mine’s a hacked together mess because I’m compiling directly on a phone in Termux with no space, no NDK, and a kernel from the stone age.

Just wanted to drop by and say your code is solid reference material. Learned a lot from it. Appreciate you putting it out there 🙏😴

---

<div class="post-metadata">

**Author:** ![gyre](https://ziggit.dev/user_avatar/ziggit.dev/gyre/32/8002_2.png) [@gyre](https://ziggit.dev/u/gyre)\
**Post date:** [April 21, 2026, 3:19pm UTC](https://ziggit.dev/t/native-android-bionic-build-on-termux-with-zig-0-15-2/14897/11 "2026-04-21T15:19:30Z")

</div>

me too. which means ‘zig build –watch’ crashes with ENOSYS
