From bd9611650fd1c8e72367cf781270ddf3494ff451 Mon Sep 17 00:00:00 2001 From: Elias Andualem Date: Sat, 21 Feb 2026 18:07:03 +0800 Subject: [PATCH] build: add support for Android NDK path configuration --- build.zig.zon | 1 + pkg/android-ndk/build.zig | 189 ++++++++++++++++++++++++++++++++++ pkg/android-ndk/build.zig.zon | 7 ++ pkg/highway/build.zig | 5 + pkg/highway/build.zig.zon | 1 + pkg/simdutf/build.zig | 5 + pkg/simdutf/build.zig.zon | 1 + pkg/utfcpp/build.zig | 5 + pkg/utfcpp/build.zig.zon | 1 + src/build/GhosttyLibVt.zig | 4 + 10 files changed, 219 insertions(+) create mode 100644 pkg/android-ndk/build.zig create mode 100644 pkg/android-ndk/build.zig.zon diff --git a/build.zig.zon b/build.zig.zon index fad3500d5..d2247bcc3 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -115,6 +115,7 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, + .android_ndk = .{ .path = "./pkg/android-ndk" }, .iterm2_themes = .{ .url = "https://deps.files.ghostty.org/ghostty-themes-release-20260202-151632-49169e9.tgz", .hash = "N-V-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z", diff --git a/pkg/android-ndk/build.zig b/pkg/android-ndk/build.zig new file mode 100644 index 000000000..8008c5aed --- /dev/null +++ b/pkg/android-ndk/build.zig @@ -0,0 +1,189 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +pub fn build(_: *std.Build) !void {} + +// Configure the step to point to the Android NDK for libc and include +// paths. This requires the Android NDK installed in the system and +// setting the appropriate environment variables or installing the NDK +// in the default location. +// +// The environment variables can be set as follows: +// - `ANDROID_NDK_HOME`: Directly points to the NDK path. +// - `ANDROID_HOME` or `ANDROID_SDK_ROOT`: Points to the Android SDK path; +// latest NDK will be automatically selected. +// +// NB: This is a workaround until zig natively supports bionic +// cross-compilation (ziglang/zig#23906). +pub fn addPaths(b: *std.Build, step: *std.Build.Step.Compile) !void { + const Cache = struct { + const Key = struct { + arch: std.Target.Cpu.Arch, + abi: std.Target.Abi, + api_level: u32, + }; + + var map: std.AutoHashMapUnmanaged(Key, ?struct { + libc: std.Build.LazyPath, + cpp_include: []const u8, + lib: []const u8, + }) = .{}; + }; + + const target = step.rootModuleTarget(); + const gop = try Cache.map.getOrPut(b.allocator, .{ + .arch = target.cpu.arch, + .abi = target.abi, + .api_level = target.os.version_range.linux.android, + }); + + if (!gop.found_existing) { + const ndk_path = findNDKPath(b.allocator) orelse { + gop.value_ptr.* = null; + return error.AndroidNDKNotFound; + }; + + var ndk_dir = std.fs.openDirAbsolute(ndk_path, .{}) catch { + gop.value_ptr.* = null; + return error.AndroidNDKNotFound; + }; + defer ndk_dir.close(); + + const ndk_triple = ndkTriple(target) orelse { + gop.value_ptr.* = null; + return error.AndroidNDKUnsupportedTarget; + }; + + const host = hostTag() orelse { + gop.value_ptr.* = null; + return error.AndroidNDKUnsupportedHost; + }; + + const sysroot = try std.fs.path.join(b.allocator, &.{ + ndk_path, "toolchains", "llvm", "prebuilt", host, "sysroot", + }); + const include_dir = try std.fs.path.join( + b.allocator, + &.{ sysroot, "usr", "include" }, + ); + const sys_include_dir = try std.fs.path.join( + b.allocator, + &.{ sysroot, "usr", "include", ndk_triple }, + ); + + var api_buf: [10]u8 = undefined; + const api_level = target.os.version_range.linux.android; + const api_level_str = std.fmt.bufPrint(&api_buf, "{d}", .{api_level}) catch unreachable; + const c_runtime_dir = try std.fs.path.join( + b.allocator, + &.{ sysroot, "usr", "lib", ndk_triple, api_level_str }, + ); + + const libc_txt = try std.fmt.allocPrint(b.allocator, + \\include_dir={s} + \\sys_include_dir={s} + \\crt_dir={s} + \\msvc_lib_dir= + \\kernel32_lib_dir= + \\gcc_dir= + , .{ include_dir, sys_include_dir, c_runtime_dir }); + + const wf = b.addWriteFiles(); + const libc_path = wf.add("libc.txt", libc_txt); + const lib = try std.fs.path.join(b.allocator, &.{ sysroot, "usr", "lib", ndk_triple }); + const cpp_include = try std.fs.path.join(b.allocator, &.{ sysroot, "usr", "include", "c++", "v1" }); + + gop.value_ptr.* = .{ + .lib = lib, + .libc = libc_path, + .cpp_include = cpp_include, + }; + } + + const value = gop.value_ptr.* orelse return error.AndroidNDKNotFound; + + step.setLibCFile(value.libc); + step.root_module.addSystemIncludePath(.{ .cwd_relative = value.cpp_include }); + step.root_module.addLibraryPath(.{ .cwd_relative = value.lib }); +} + +fn findNDKPath(allocator: std.mem.Allocator) ?[]const u8 { + // Check if user has set the environment variable for the NDK path. + if (std.process.getEnvVarOwned(allocator, "ANDROID_NDK_HOME") catch null) |value| { + if (value.len > 0) return value; + } + + // Check the common environment variables for the Android SDK path and look for the NDK inside it. + inline for (.{ "ANDROID_HOME", "ANDROID_SDK_ROOT" }) |env| { + if (std.process.getEnvVarOwned(allocator, env) catch null) |sdk| { + if (sdk.len > 0) { + if (findLatestNDK(allocator, sdk)) |ndk| return ndk; + } + } + } + + // As a fallback, we assume the most common/default SDK path based on the OS. + const home = std.process.getEnvVarOwned( + allocator, + if (builtin.os.tag == .windows) "LOCALAPPDATA" else "HOME", + ) catch return null; + + const default_sdk_path = std.fs.path.join(allocator, &.{ + home, switch (builtin.os.tag) { + .linux => "Android/sdk", + .macos => "Library/Android/Sdk", + .windows => "Android/Sdk", + else => return null, + }, + }) catch return null; + return findLatestNDK(allocator, default_sdk_path); +} + +fn findLatestNDK(allocator: std.mem.Allocator, sdk_path: []const u8) ?[]const u8 { + const ndk_dir = std.fs.path.join(allocator, &.{ sdk_path, "ndk" }) catch return null; + var dir = std.fs.openDirAbsolute(ndk_dir, .{ .iterate = true }) catch return null; + defer dir.close(); + + var latest_version: ?[]const u8 = null; + var latest_parsed: ?std.SemanticVersion = null; + var iterator = dir.iterate(); + + while (iterator.next() catch null) |file| { + if (file.kind != .directory) continue; + const parsed = std.SemanticVersion.parse(file.name) catch continue; + if (latest_version == null or parsed.order(latest_parsed.?) == .gt) { + if (latest_version) |old| allocator.free(old); + latest_version = allocator.dupe(u8, file.name) catch return null; + latest_parsed = parsed; + } + } + + if (latest_version) |version| { + return std.fs.path.join(allocator, &.{ sdk_path, "ndk", version }) catch return null; + } + + return null; +} + +fn hostTag() ?[]const u8 { + return switch (builtin.os.tag) { + .linux => "linux-x86_64", + // All darwin hosts use the same prebuilt binaries + // (https://developer.android.com/ndk/guides/other_build_systems). + .macos => "darwin-x86_64", + .windows => "windows-x86_64", + else => null, + }; +} + +// We must map the target architecture to the corresponding NDK triple following the NDK +// documentation: https://android.googlesource.com/platform/ndk/+/master/docs/BuildSystemMaintainers.md#architectures +fn ndkTriple(target: std.Target) ?[]const u8 { + return switch (target.cpu.arch) { + .arm => "arm-linux-androideabi", + .aarch64 => "aarch64-linux-android", + .x86 => "i686-linux-android", + .x86_64 => "x86_64-linux-android", + else => null, + }; +} diff --git a/pkg/android-ndk/build.zig.zon b/pkg/android-ndk/build.zig.zon new file mode 100644 index 000000000..97febcefb --- /dev/null +++ b/pkg/android-ndk/build.zig.zon @@ -0,0 +1,7 @@ +.{ + .name = .android_ndk, + .version = "0.0.1", + .dependencies = .{}, + .fingerprint = 0xee68d62c5a97b68b, + .paths = .{""}, +} diff --git a/pkg/highway/build.zig b/pkg/highway/build.zig index 3715baf4a..b6e188b13 100644 --- a/pkg/highway/build.zig +++ b/pkg/highway/build.zig @@ -31,6 +31,11 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } + if (target.result.abi.isAndroid()) { + const android_ndk = @import("android_ndk"); + try android_ndk.addPaths(b, lib); + } + var flags: std.ArrayList([]const u8) = .empty; defer flags.deinit(b.allocator); try flags.appendSlice(b.allocator, &.{ diff --git a/pkg/highway/build.zig.zon b/pkg/highway/build.zig.zon index 0777fcb7a..4870d1db5 100644 --- a/pkg/highway/build.zig.zon +++ b/pkg/highway/build.zig.zon @@ -12,5 +12,6 @@ }, .apple_sdk = .{ .path = "../apple-sdk" }, + .android_ndk = .{ .path = "../android-ndk" }, }, } diff --git a/pkg/simdutf/build.zig b/pkg/simdutf/build.zig index 3123cab21..8dcd141c1 100644 --- a/pkg/simdutf/build.zig +++ b/pkg/simdutf/build.zig @@ -20,6 +20,11 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } + if (target.result.abi.isAndroid()) { + const android_ndk = @import("android_ndk"); + try android_ndk.addPaths(b, lib); + } + var flags: std.ArrayList([]const u8) = .empty; defer flags.deinit(b.allocator); // Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414 diff --git a/pkg/simdutf/build.zig.zon b/pkg/simdutf/build.zig.zon index cd81c841e..afbef5418 100644 --- a/pkg/simdutf/build.zig.zon +++ b/pkg/simdutf/build.zig.zon @@ -5,5 +5,6 @@ .paths = .{""}, .dependencies = .{ .apple_sdk = .{ .path = "../apple-sdk" }, + .android_ndk = .{ .path = "../android-ndk" }, }, } diff --git a/pkg/utfcpp/build.zig b/pkg/utfcpp/build.zig index e06813b83..08efb4ac8 100644 --- a/pkg/utfcpp/build.zig +++ b/pkg/utfcpp/build.zig @@ -19,6 +19,11 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } + if (target.result.abi.isAndroid()) { + const android_ndk = @import("android_ndk"); + try android_ndk.addPaths(b, lib); + } + var flags: std.ArrayList([]const u8) = .empty; defer flags.deinit(b.allocator); diff --git a/pkg/utfcpp/build.zig.zon b/pkg/utfcpp/build.zig.zon index eff395a60..1077e9655 100644 --- a/pkg/utfcpp/build.zig.zon +++ b/pkg/utfcpp/build.zig.zon @@ -12,5 +12,6 @@ }, .apple_sdk = .{ .path = "../apple-sdk" }, + .android_ndk = .{ .path = "../android-ndk" }, }, } diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 2f3d4a124..99c603275 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -62,6 +62,10 @@ pub fn initShared( .{ .include_extensions = &.{".h"} }, ); + if (lib.rootModuleTarget().abi.isAndroid()) { + try @import("android_ndk").addPaths(b, lib); + } + if (lib.rootModuleTarget().os.tag.isDarwin()) { // Self-hosted x86_64 doesn't work for darwin. It may not work // for other platforms too but definitely darwin.