Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,12 @@ jobs:
zig build -Dandroid=true -Dcrash-on-exception --verbose
working-directory: examples/sdl2

- name: Build Raylib Example (Zig Stable)
run: zig build -Dandroid=true --verbose
working-directory: examples/raylib
# NOTE(jae): 2026-04-09
# Raylib example only runs on 0.16.X due to downstream dependencies
#
# - name: Build Raylib Example (Zig Stable)
# run: zig build -Dandroid=true --verbose
# working-directory: examples/raylib

# TODO(jae): 2025-03-30
# Need to figure out how to get 'adb shell monkey' to return an error code or be able to return an error code
Expand Down Expand Up @@ -183,12 +186,11 @@ jobs:
run: zig build -Dandroid=true --verbose
working-directory: examples/sdl2

# note(jae): 2026-01-10
# Downstream packages for Raylib won't work with Zig 0.15.2 *and* Zig 0.16.X
#
# - name: Build Raylib Example (Zig Nightly)
# run: zig build -Dandroid=true --verbose
# working-directory: examples/raylib
# note(jae): 2026-04-09
# Downstream packages for Raylib only support 0.16.X-dev right now.
- name: Build Raylib Example (Zig Nightly)
run: zig build -Dandroid=true --verbose
working-directory: examples/raylib
build-previous-stable:
name: Build Zig Previous
needs: [setup]
Expand Down
26 changes: 21 additions & 5 deletions examples/raylib/build.zig
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const std = @import("std");
const android = @import("android");
const LinkMode = std.builtin.LinkMode;

const android = @import("android");

const exe_name = "raylib";

pub fn build(b: *std.Build) void {
const exe_name = "raylib";
const root_target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const android_targets = android.standardTargets(b, root_target);
Expand Down Expand Up @@ -47,6 +49,10 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
.android_api_version = @as([]const u8, b.fmt("{}", .{@intFromEnum(apk.api_level)})),
.android_ndk = @as([]const u8, apk.ndk.path),
// NOTE(jae): 2026-04-09
// Must be statically built for Android, otherwise "android_main" is not handled here:
// https://github.com/raysan5/raylib/blob/f89d38b086c1d0a0c7e38c9c648aa91c05646300/src/platforms/rcore_android.c#L322
.linkage = LinkMode.static,
})
else
b.dependency("raylib_zig", .{
Expand All @@ -55,7 +61,14 @@ pub fn build(b: *std.Build) void {
.linkage = LinkMode.dynamic,
});

app.linkLibrary(raylib_dep.artifact("raylib"));
const raylib_lib = raylib_dep.artifact("raylib");
// NOTE(jae): 2026-04-09
// Enable PIC=true for Raylib, otherwise it will not compile.
//
// ld.lld: relocation R_AARCH64_ADD_ABS_LO12_NC cannot be used against symbol 'CORE'; recompile with -fPIC
// relocation R_X86_64_64 cannot be used against local symbol; recompile with -fPIC
raylib_lib.root_module.pic = true;
app.linkLibrary(raylib_lib);
app.addImport("raylib", raylib_dep.module("raylib"));

if (android_apk) |apk| {
Expand All @@ -71,12 +84,15 @@ pub fn build(b: *std.Build) void {
app.addIncludePath(native_app_glue_dir);

apk.addArtifact(b.addLibrary(.{
.linkage = .dynamic,
.name = "main",
.root_module = app,
.linkage = .dynamic,
}));
} else {
const exe = b.addExecutable(.{ .name = exe_name, .root_module = app });
const exe = b.addExecutable(.{
.name = exe_name,
.root_module = app,
});
b.installArtifact(exe);

const run_exe = b.addRunArtifact(exe);
Expand Down
6 changes: 3 additions & 3 deletions examples/raylib/build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
.{
.name = .raylib,
.version = "0.0.0",
.minimum_zig_version = "0.14.0",
.minimum_zig_version = "0.16.0",
.fingerprint = 0x13035e5cf42e313f,
.dependencies = .{
.android = .{ .path = "../.." },
.raylib_zig = .{
.url = "git+https://github.com/raylib-zig/raylib-zig#cd71c85d571027ac8033357f83b124ee051825b3",
.hash = "raylib_zig-5.6.0-dev-KE8REENOBQC-m5nK7M2b5aKSIubJPbPLUYcRhT7aT3RN",
.url = "git+https://github.com/raylib-zig/raylib-zig#a93d7f04aeeb138efc0a08b406aad74cdea17c1c",
.hash = "raylib_zig-5.6.0-dev-KE8REJ9mBQC9yBwvG5aBLUgMaGVGyzbrpyGZNUyzwvUI",
},
},
.paths = .{
Expand Down
30 changes: 27 additions & 3 deletions examples/raylib/src/main.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");

const android = @import("android");
const rl = @import("raylib");

Expand Down Expand Up @@ -35,12 +36,35 @@ else

comptime {
if (builtin.abi.isAndroid()) {
// Setup exported C-function as defined in AndroidManifest.xml
// ie. <meta-data android:name="android.app.lib_name" android:value="main"/>
// Setup exported C-function as defined in AndroidManifest.xml and Raylib.
//
// Android knows to natively call this library
// - <meta-data android:name="android.app.lib_name" android:value="main"/>
//
// Then Raylib makes the "android_main" entrypoint call the exported "main" C-function
// https://github.com/raysan5/raylib/blob/f89d38b086c1d0a0c7e38c9c648aa91c05646300/src/platforms/rcore_android.c#L322
@export(&androidMain, .{ .name = "main" });

// NOTE(jae): 2026-04-12
// As of March 2026, Raylib requires a linker flag to make __real_fopen exist
// https://github.com/raysan5/raylib/pull/5624
//
// Because Zig doesn't give access to linker flags to add (-Wl,--wrap=fopen), we just export __real_fopen ourselves and call it here:
// -Wl,--wrap=fopen
// Related comment: https://github.com/raysan5/raylib/blob/f89d38b086c1d0a0c7e38c9c648aa91c05646300/src/platforms/rcore_android.c#L299-L300
@export(&raylibFileOpen, .{ .name = "__real_fopen" });
}
}

fn raylibFileOpen(filename: [*c]const u8, modes: [*c]const u8) callconv(.c) ?*anyopaque {
return @import("std").c.fopen(filename, modes);
}

fn androidMain() callconv(.c) c_int {
return std.start.callMain();
main() catch |err| {
std.log.err("{t}", .{err});
if (@errorReturnTrace()) |trace| std.debug.dumpStackTrace(trace);
return 1;
};
return 0;
}
104 changes: 50 additions & 54 deletions src/androidbuild/Apk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -456,31 +456,9 @@ fn doInstallApk(apk: *Apk) Allocator.Error!*Step.InstallFile {
// - i686-linux-android
// - x86_64-linux-android
for (apk.artifacts.items, 0..) |artifact, artifact_index| {
const target: ResolvedTarget = artifact.root_module.resolved_target orelse {
if (artifact.root_module.resolved_target == null) {
@panic(b.fmt("artifact[{d}] has no 'target' set", .{artifact_index}));
};

// NOTE(jae): 2026-02-01
// If not explicitly set in users build, default to using LLVM and LLD for Android builds
// as that's the same toolchain that the Android SDK uses.
//
// This also can resolve issues with Zigs linker not yet supporting certain compression schemes/etc
if (artifact.use_llvm == null) {
artifact.use_llvm = true;
}
if (artifact.use_lld == null) {
artifact.use_lld = true;
}

// https://developer.android.com/ndk/guides/abis#native-code-in-app-packages
const so_dir: []const u8 = switch (target.result.cpu.arch) {
.aarch64 => "arm64-v8a",
.arm => "armeabi-v7a",
.x86_64 => "x86_64",
.x86 => "x86",
else => @panic(b.fmt("unsupported or unhandled arch: {s}", .{@tagName(target.result.cpu.arch)})),
};
_ = apk_files.addCopyFile(artifact.getEmittedBin(), b.fmt("lib/{s}/lib{s}.so", .{ so_dir, artifact.name }));

// Add module
// - If a module has no `root_source_file` (e.g you're only compiling C files using `addCSourceFiles`)
Expand Down Expand Up @@ -523,25 +501,21 @@ fn doInstallApk(apk: *Apk) Allocator.Error!*Step.InstallFile {
// update linked libraries that use C or C++ to:
// - use Android LibC file
// - add Android NDK library paths. (libandroid, liblog, etc)
apk.updateLinkObjects(artifact, so_dir, apk_files);
apk.updateLinkObjects(artifact, apk_files);

// update artifact to:
// - Be configured to work correctly on Android
// - To know where C header /lib files are via setLibCFile and linkLibC
// - Provide path to additional libraries to link to
{
if (artifact.linkage) |linkage| {
if (linkage == .dynamic) {
updateSharedLibraryOptions(artifact);
}
if (artifact.root_module.link_libc == null) {
artifact.root_module.link_libc = true;
}
apk.setLibCFile(artifact);
apk.addLibraryPaths(artifact.root_module);
artifact.root_module.link_libc = true;
apk.updateArtifact(artifact, apk_files);

// Apply workaround for Zig 0.14.0 stable
//
// This *must* occur after "apk.updateLinkObjects" for the root package otherwise
// This *must* occur after apk.updateArtifact (apk.updateLinkObjects) for the root package otherwise
// you may get an error like: "unable to find dynamic system library 'c++abi_zig_workaround'"
apk.applyLibLinkCppWorkaroundIssue19(artifact);
}
Expand Down Expand Up @@ -787,36 +761,58 @@ fn setLibCFile(apk: *Apk, compile: *Step.Compile) void {
compile.setLibCFile(android_libc_path);
}

fn updateLinkObjects(apk: *Apk, root_artifact: *Step.Compile, so_dir: []const u8, raw_top_level_apk_files: *Step.WriteFile) void {
fn updateArtifact(apk: *Apk, artifact: *Step.Compile, raw_top_level_apk_files: *Step.WriteFile) void {
const b = apk.b;

// If you have a library that is being built as an *.so then install it
// alongside your library.
//
// This was initially added to support building SDL2 with Zig.
if (artifact.linkage) |linkage| {
if (linkage == .dynamic) {
updateSharedLibraryOptions(artifact);

// https://developer.android.com/ndk/guides/abis#native-code-in-app-packages
const target = artifact.root_module.resolved_target orelse unreachable;
const so_dir = androidbuild.getTargetLibDir(b, target);
_ = raw_top_level_apk_files.addCopyFile(artifact.getEmittedBin(), b.fmt("lib/{s}/lib{s}.so", .{ so_dir, artifact.name }));
}
}

// NOTE(jae): 2026-02-01
// If not explicitly set in users build, default to using LLVM and LLD for Android builds
// as that's the same toolchain that the Android SDK uses.
//
// This also can resolve issues with Zigs linker not yet supporting certain compression schemes/etc
if (artifact.use_lld == null) {
artifact.use_lld = true;
}
if (artifact.use_llvm == null) {
artifact.use_llvm = true;
}

// If library is built using C or C++ then setLibCFile
if (artifact.root_module.link_libc == true or
artifact.root_module.link_libcpp == true)
{
apk.setLibCFile(artifact);
}

// Add library paths to find "android", "log", etc
apk.addLibraryPaths(artifact.root_module);
}

fn updateLinkObjects(apk: *Apk, root_artifact: *Step.Compile, raw_top_level_apk_files: *Step.WriteFile) void {
for (root_artifact.root_module.link_objects.items) |link_object| {
switch (link_object) {
.other_step => |artifact| {
switch (artifact.kind) {
.lib => {
// If you have a library that is being built as an *.so then install it
// alongside your library.
//
// This was initially added to support building SDL2 with Zig.
if (artifact.linkage) |linkage| {
if (linkage == .dynamic) {
updateSharedLibraryOptions(artifact);
_ = raw_top_level_apk_files.addCopyFile(artifact.getEmittedBin(), b.fmt("lib/{s}/lib{s}.so", .{ so_dir, artifact.name }));
}
}

// If library is built using C or C++ then setLibCFile
if (artifact.root_module.link_libc == true or
artifact.root_module.link_libcpp == true)
{
apk.setLibCFile(artifact);
}

// Add library paths to find "android", "log", etc
apk.addLibraryPaths(artifact.root_module);
// Update updateSharedLibraryOptions, libCFile, addLibraryPaths
apk.updateArtifact(artifact, raw_top_level_apk_files);

// Update libraries linked to this library
apk.updateLinkObjects(artifact, so_dir, raw_top_level_apk_files);
apk.updateLinkObjects(artifact, raw_top_level_apk_files);

// Apply workaround for Zig 0.14.0 and Zig 0.15.X
apk.applyLibLinkCppWorkaroundIssue19(artifact);
Expand Down
23 changes: 23 additions & 0 deletions src/androidbuild/androidbuild.zig
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,29 @@ pub fn resolveTargets(b: *std.Build, options: ResolveTargetOptions) []ResolvedTa
@panic(b.fmt("unsupported Android target given: {s}", .{linuxTriple}));
}

/// Get the library directory name of a given build target within an APK.
/// - armeabi-v7a
/// - arm64-v8a
/// - x86_64
/// - x86
///
/// Example of structure within an APK
/// - lib/armeabi-v7a/libfoo.so
/// - lib/arm64-v8a/libfoo.so
/// - lib/x86/libfoo.so
/// - lib/x86_64/libfoo.so
///
/// See documentation here: https://developer.android.com/ndk/guides/abis#native-code-in-app-packages
pub fn getTargetLibDir(b: *std.Build, target: ResolvedTarget) []const u8 {
return switch (target.result.cpu.arch) {
.aarch64 => "arm64-v8a",
.arm => "armeabi-v7a",
.x86_64 => "x86_64",
.x86 => "x86",
else => @panic(b.fmt("cannot determine APK library directory, unsupported or unhandled arch: {s}", .{@tagName(target.result.cpu.arch)})),
};
}

fn getAllAndroidTargets(b: *std.Build) []ResolvedTarget {
const resolved_targets = b.allocator.alloc(ResolvedTarget, supported_android_targets.len) catch @panic("OOM");
for (supported_android_targets, 0..) |android_target, i| {
Expand Down
Loading