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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ jobs:
with:
version: ${{ needs.setup.outputs.zig-stable-version }}

- name: Build Test (Zig Stable)
run: zig build -Dandroid=true --verbose
working-directory: test/build

- name: Build Minimal Example (Zig Stable)
run: zig build -Dandroid=true --verbose
working-directory: examples/minimal
Expand Down Expand Up @@ -178,6 +182,10 @@ jobs:
with:
version: "master"

- name: Build Test (Zig Nightly)
run: zig build -Dandroid=true --verbose
working-directory: test/build

- name: Build Minimal Example (Zig Nightly)
run: zig build -Dandroid=true --verbose
working-directory: examples/minimal
Expand Down Expand Up @@ -224,6 +232,10 @@ jobs:
with:
version: ${{ needs.setup.outputs.zig-previous-stable-version }}

- name: Build Test (Zig Nightly)
run: zig build -Dandroid=true --verbose
working-directory: test/build

- name: Build Minimal Example
run: zig build -Dandroid=true --verbose
working-directory: examples/minimal
Expand Down
47 changes: 43 additions & 4 deletions src/androidbuild/Apk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const Target = std.Target;
const Step = std.Build.Step;
const ResolvedTarget = std.Build.ResolvedTarget;
const LazyPath = std.Build.LazyPath;
const ArrayList = std.ArrayListUnmanaged;
const builtin = @import("builtin");

const androidbuild = @import("androidbuild.zig");
Expand Down Expand Up @@ -47,10 +48,13 @@ build_tools: BuildTools,
api_level: ApiLevel,
key_store: ?KeyStore,
android_manifest: ?LazyPath,
artifacts: std.ArrayListUnmanaged(*Step.Compile),
java_files: std.ArrayListUnmanaged(LazyPath),
resources: std.ArrayListUnmanaged(Resource),
assets: std.ArrayListUnmanaged(Resource),
artifacts: ArrayList(*Step.Compile),
/// Precompiled library files can be added to the APK to support features like Vulkan validation layers
/// ie. https://developer.android.com/ndk/guides/graphics/validation-layer
precompiled_library_files: ArrayList(PrecompiledLibraryFile),
java_files: ArrayList(LazyPath),
resources: ArrayList(Resource),
assets: ArrayList(Resource),

pub const Options = struct {
/// APK file output name, ie. "{name}.apk"
Expand Down Expand Up @@ -96,6 +100,7 @@ pub fn create(sdk: *Sdk, options: Options) *Apk {
.api_level = options.api_level,
.key_store = null,
.android_manifest = null,
.precompiled_library_files = .empty,
.artifacts = .empty,
.java_files = .empty,
.resources = .empty,
Expand Down Expand Up @@ -176,6 +181,18 @@ pub fn setKeyStore(apk: *Apk, key_store: KeyStore) void {
apk.key_store = key_store;
}

/// Add precompiled library files
///
/// This is useful for when you want to consume vendors compiled library files such as the Vulkan Validation layers
/// ie. https://developer.android.com/ndk/guides/graphics/validation-layer
pub fn addLibraryFile(apk: *Apk, android_target: androidbuild.AndroidTarget, path: LazyPath) void {
const b = apk.b;
apk.precompiled_library_files.append(b.allocator, .{
.target = android_target.target(b),
.path = path,
}) catch @panic("OOM");
}

fn addLibraryPaths(apk: *Apk, module: *std.Build.Module) void {
const b = apk.b;
const android_ndk_sysroot = apk.ndk.sysroot_path;
Expand Down Expand Up @@ -446,6 +463,21 @@ fn doInstallApk(apk: *Apk) Allocator.Error!*Step.InstallFile {
// - classes.dex
const apk_files = b.addWriteFiles();

// Add support for adding compiled library files (Vulkan Validation layers)
// ie. https://developer.android.com/ndk/guides/graphics/validation-layer
for (apk.precompiled_library_files.items) |precompiled_library| {
const so_dir = androidbuild.getTargetLibDir(b, precompiled_library.target);
// NOTE(jae): 2026-04-12
// Can likely just change to "precompiled_library.path.basename()" in the future if this breaks
const precompiled_lib_basename = std.fs.path.basename(switch (precompiled_library.path) {
.src_path => |sp| sp.sub_path,
.cwd_relative => |sub_path| sub_path,
.generated => @panic("invalid precompiled library, cannot be generated"),
.dependency => |dep| dep.sub_path,
});
_ = apk_files.addCopyFile(precompiled_library.path, b.fmt("lib/{s}/{s}", .{ so_dir, precompiled_lib_basename }));
}

// These files belong in root and *must not* be compressed
// - resources.arsc
const apk_files_not_compressed = b.addWriteFiles();
Expand Down Expand Up @@ -968,4 +1000,11 @@ fn updatePathWithJdk(apk: *Apk, run: *std.Build.Step.Run) Allocator.Error!void {
}
}

const PrecompiledLibraryFile = struct {
/// The target it belongs to
target: ResolvedTarget,
/// A precompiled *.so file like "libVkLayer_khronos_validation.so"
path: LazyPath,
};

const Apk = @This();
42 changes: 42 additions & 0 deletions src/androidbuild/androidbuild.zig
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,48 @@ pub fn getAndroidTriple(target: ResolvedTarget) error{InvalidAndroidTarget}![]co
};
}

/// List of supported Android targets, this is used as a shorthand for API functions like "apk.addLibraryFile".
pub const AndroidTarget = enum {
arm64_v8a,
armeabi_v7a,
x86_64,
x86,

pub fn target(at: AndroidTarget, b: *std.Build) ResolvedTarget {
const android_target_query: AndroidTargetQuery = switch (at) {
.arm64_v8a => .{
// aarch64-linux-android
.cpu_arch = .aarch64,
.cpu_features_add = Target.aarch64.featureSet(&.{.v8a}),
},
.armeabi_v7a => .{
// arm-linux-androideabi
.cpu_arch = .arm,
.cpu_features_add = Target.arm.featureSet(&.{.v7a}),
},
.x86_64 => .{
// x86_64-linux-android
.cpu_arch = .x86_64,
},
.x86 => .{
// i686-linux-android
.cpu_arch = .x86,
},
};
return b.resolveTargetQuery(android_target_query.queryTarget());
}

/// The "lib/{AndroidTarget}" directory name as it appears in an APK
pub fn lib(at: AndroidTarget) []const u8 {
return switch (at) {
.arm64_v8a => "arm64-v8a",
.armeabi_v7a => "armeabi-v7a",
.x86_64 => "x86_64",
.x86 => "x86",
};
}
};

/// Will return a slice of Android targets
/// - If -Dandroid=true, return all Android targets (x86, x86_64, aarch64, etc)
/// - If -Dtarget=aarch64-linux-android, return a slice with the one specified Android target
Expand Down
24 changes: 24 additions & 0 deletions test/build/android/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.zig.minimal">

<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:hasCode="false"
tools:targetApi="31">
<activity
android:name="android.app.NativeActivity"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Binary file added test/build/android/res/mipmap/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions test/build/android/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Pretty name of your app -->
<string name="app_name">Zig Build Test</string>
<!--
This is required for the APK name. This identifies your app, Android will associate
your signing key with this identifier and will prevent updates if the key changes.
-->
<string name="package_name">com.zig.build_test</string>
</resources>
132 changes: 132 additions & 0 deletions test/build/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//! This module is for testing that we implemented certain build features and to at least make sure
//! there is code coverage for new APIs added.
//!
//! TODO(Jae): 2026-04-12
//! Ideally adding functions to also validate the output APK file would be nice too.

const std = @import("std");
const builtin = @import("builtin");

const android = @import("android");

pub fn build(b: *std.Build) void {
const exe_name: []const u8 = "build_test";
const root_target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const android_targets = android.standardTargets(b, root_target);

// NOTE(jae): 2026-04-12
// Run it *after* the "standardTargets" call
testLazyImportAndResolveTargets(b, root_target);

var root_target_single = [_]std.Build.ResolvedTarget{root_target};
const targets: []std.Build.ResolvedTarget = if (android_targets.len == 0)
root_target_single[0..]
else
android_targets;

const android_apk: ?*android.Apk = blk: {
if (android_targets.len == 0) break :blk null;

const android_sdk = android.Sdk.create(b, .{});
const apk = android_sdk.createApk(.{
.name = exe_name,
.api_level = .android15,
.build_tools_version = "35.0.1",
.ndk_version = "29.0.13113456",
});
const key_store_file = android_sdk.createKeyStore(.example);
apk.setKeyStore(key_store_file);
apk.setAndroidManifest(b.path("android/AndroidManifest.xml"));
apk.addResourceDirectory(b.path("android/res"));

testAddLibraryFile(b, apk);

break :blk apk;
};

for (targets) |target| {
const app_module = b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/build_test_main.zig"),
});

var exe: *std.Build.Step.Compile = if (target.result.abi.isAndroid()) b.addLibrary(.{
.name = "main",
.root_module = app_module,
.linkage = .dynamic,
}) else b.addExecutable(.{
.name = exe_name,
.root_module = app_module,
});

// if building as library for Android, add this target
// NOTE: Android has different CPU targets so you need to build a version of your
// code for x86, x86_64, arm, arm64 and more
if (target.result.abi.isAndroid()) {
const apk: *android.Apk = android_apk orelse @panic("Android APK should be initialized");
const android_dep = b.dependency("android", .{
.optimize = optimize,
.target = target,
});
exe.root_module.addImport("android", android_dep.module("android"));

apk.addArtifact(exe);
} else {
b.installArtifact(exe);

// If only 1 target, add "run" step
if (targets.len == 1) {
const run_step = b.step("run", "Run the application");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
}
}
}
if (android_apk) |apk| {
testInstallAndAddRunStep(b, apk);
}
}

/// Test calling lazyImport and then calling "resolveTargets"
///
/// PR: https://github.com/silbinarywolf/zig-android-sdk/pull/83
fn testLazyImportAndResolveTargets(b: *std.Build, root_target: std.Build.ResolvedTarget) void {
const all_android_targets = true;
const android_targets: []std.Build.ResolvedTarget = blk: {
if (all_android_targets or root_target.result.abi.isAndroid()) {
if (b.lazyImport(@This(), "lazy_android")) |lazy_android| {
break :blk lazy_android.resolveTargets(b, .{
.default_target = root_target,
.all_targets = true,
});
}
}
break :blk &[0]std.Build.ResolvedTarget{};
};
if (android_targets.len != 4) @panic("expected 'resolveTargets' it to return 4 Android targets");
}

/// Test the addLibraryFile functionality
///
/// Requested feature here: https://github.com/silbinarywolf/zig-android-sdk/issues/77
fn testAddLibraryFile(b: *std.Build, apk: *android.Apk) void {
const vulkan_validation_dep = b.lazyDependency("vulkan_validation", .{}) orelse return;
apk.addLibraryFile(.arm64_v8a, vulkan_validation_dep.path("arm64-v8a/libVkLayer_khronos_validation.so"));
apk.addLibraryFile(.armeabi_v7a, vulkan_validation_dep.path("armeabi-v7a/libVkLayer_khronos_validation.so"));
apk.addLibraryFile(.x86, vulkan_validation_dep.path("x86/libVkLayer_khronos_validation.so"));
apk.addLibraryFile(.x86_64, vulkan_validation_dep.path("x86_64/libVkLayer_khronos_validation.so"));
}

fn testInstallAndAddRunStep(b: *std.Build, apk: *android.Apk) void {
const installed_apk = apk.addInstallApk();
b.getInstallStep().dependOn(&installed_apk.step);

const android_sdk = apk.sdk;
const run_step = b.step("run", "Install and run the application on an Android device");
const adb_install = android_sdk.addAdbInstall(installed_apk.source);
const adb_start = android_sdk.addAdbStart("com.zig.build_test/android.app.NativeActivity");
adb_start.step.dependOn(&adb_install.step);
run_step.dependOn(&adb_start.step);
}
23 changes: 23 additions & 0 deletions test/build/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.{
.name = .android_build_test,
.version = "0.0.0",
.dependencies = .{
.android = .{
.path = "../..",
},
.lazy_android = .{
.path = "../..",
.lazy = true,
},
.vulkan_validation = .{
.url = "https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download/vulkan-sdk-1.4.341.0/android-binaries-1.4.341.0.zip",
.hash = "N-V-__8AABTXlAV0z_BGl5-lZeOEm_d2gHEhExT2qjxMqQ72",
.lazy = true,
},
},
.paths = .{
"build.zig",
"build.zig.zon",
},
.fingerprint = 0xb15d5a3541f113ad,
}
Loading
Loading