Skip to content

Commit 1a6e3f3

Browse files
create addLibraryFile function (#84)
Add support for adding precompiled libraries to the APK, for example the Vulkan Validation layers: https://developer.android.com/ndk/guides/graphics/validation-layer **Changelog** - add `addLibraryFile` function to `Apk` - add `test/build` setup that gets executed as part of CI to at least execute behaviour not covered in examples like `addLibraryFile` and `b.lazyImport` **Screenshot of APK contents** <img width="1284" height="212" alt="image" src="https://github.com/user-attachments/assets/3731ae17-7132-4217-aa54-e3308a98df01" /> Fixes #77
1 parent 72bfbf4 commit 1a6e3f3

10 files changed

Lines changed: 3149 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ jobs:
9393
with:
9494
version: ${{ needs.setup.outputs.zig-stable-version }}
9595

96+
- name: Build Test (Zig Stable)
97+
run: zig build -Dandroid=true --verbose
98+
working-directory: test/build
99+
96100
- name: Build Minimal Example (Zig Stable)
97101
run: zig build -Dandroid=true --verbose
98102
working-directory: examples/minimal
@@ -178,6 +182,10 @@ jobs:
178182
with:
179183
version: "master"
180184

185+
- name: Build Test (Zig Nightly)
186+
run: zig build -Dandroid=true --verbose
187+
working-directory: test/build
188+
181189
- name: Build Minimal Example (Zig Nightly)
182190
run: zig build -Dandroid=true --verbose
183191
working-directory: examples/minimal
@@ -224,6 +232,10 @@ jobs:
224232
with:
225233
version: ${{ needs.setup.outputs.zig-previous-stable-version }}
226234

235+
- name: Build Test (Zig Nightly)
236+
run: zig build -Dandroid=true --verbose
237+
working-directory: test/build
238+
227239
- name: Build Minimal Example
228240
run: zig build -Dandroid=true --verbose
229241
working-directory: examples/minimal

src/androidbuild/Apk.zig

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const Target = std.Target;
44
const Step = std.Build.Step;
55
const ResolvedTarget = std.Build.ResolvedTarget;
66
const LazyPath = std.Build.LazyPath;
7+
const ArrayList = std.ArrayListUnmanaged;
78
const builtin = @import("builtin");
89

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

5559
pub const Options = struct {
5660
/// APK file output name, ie. "{name}.apk"
@@ -96,6 +100,7 @@ pub fn create(sdk: *Sdk, options: Options) *Apk {
96100
.api_level = options.api_level,
97101
.key_store = null,
98102
.android_manifest = null,
103+
.precompiled_library_files = .empty,
99104
.artifacts = .empty,
100105
.java_files = .empty,
101106
.resources = .empty,
@@ -176,6 +181,18 @@ pub fn setKeyStore(apk: *Apk, key_store: KeyStore) void {
176181
apk.key_store = key_store;
177182
}
178183

184+
/// Add precompiled library files
185+
///
186+
/// This is useful for when you want to consume vendors compiled library files such as the Vulkan Validation layers
187+
/// ie. https://developer.android.com/ndk/guides/graphics/validation-layer
188+
pub fn addLibraryFile(apk: *Apk, android_target: androidbuild.AndroidTarget, path: LazyPath) void {
189+
const b = apk.b;
190+
apk.precompiled_library_files.append(b.allocator, .{
191+
.target = android_target.target(b),
192+
.path = path,
193+
}) catch @panic("OOM");
194+
}
195+
179196
fn addLibraryPaths(apk: *Apk, module: *std.Build.Module) void {
180197
const b = apk.b;
181198
const android_ndk_sysroot = apk.ndk.sysroot_path;
@@ -446,6 +463,21 @@ fn doInstallApk(apk: *Apk) Allocator.Error!*Step.InstallFile {
446463
// - classes.dex
447464
const apk_files = b.addWriteFiles();
448465

466+
// Add support for adding compiled library files (Vulkan Validation layers)
467+
// ie. https://developer.android.com/ndk/guides/graphics/validation-layer
468+
for (apk.precompiled_library_files.items) |precompiled_library| {
469+
const so_dir = androidbuild.getTargetLibDir(b, precompiled_library.target);
470+
// NOTE(jae): 2026-04-12
471+
// Can likely just change to "precompiled_library.path.basename()" in the future if this breaks
472+
const precompiled_lib_basename = std.fs.path.basename(switch (precompiled_library.path) {
473+
.src_path => |sp| sp.sub_path,
474+
.cwd_relative => |sub_path| sub_path,
475+
.generated => @panic("invalid precompiled library, cannot be generated"),
476+
.dependency => |dep| dep.sub_path,
477+
});
478+
_ = apk_files.addCopyFile(precompiled_library.path, b.fmt("lib/{s}/{s}", .{ so_dir, precompiled_lib_basename }));
479+
}
480+
449481
// These files belong in root and *must not* be compressed
450482
// - resources.arsc
451483
const apk_files_not_compressed = b.addWriteFiles();
@@ -968,4 +1000,11 @@ fn updatePathWithJdk(apk: *Apk, run: *std.Build.Step.Run) Allocator.Error!void {
9681000
}
9691001
}
9701002

1003+
const PrecompiledLibraryFile = struct {
1004+
/// The target it belongs to
1005+
target: ResolvedTarget,
1006+
/// A precompiled *.so file like "libVkLayer_khronos_validation.so"
1007+
path: LazyPath,
1008+
};
1009+
9711010
const Apk = @This();

src/androidbuild/androidbuild.zig

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,48 @@ pub fn getAndroidTriple(target: ResolvedTarget) error{InvalidAndroidTarget}![]co
5555
};
5656
}
5757

58+
/// List of supported Android targets, this is used as a shorthand for API functions like "apk.addLibraryFile".
59+
pub const AndroidTarget = enum {
60+
arm64_v8a,
61+
armeabi_v7a,
62+
x86_64,
63+
x86,
64+
65+
pub fn target(at: AndroidTarget, b: *std.Build) ResolvedTarget {
66+
const android_target_query: AndroidTargetQuery = switch (at) {
67+
.arm64_v8a => .{
68+
// aarch64-linux-android
69+
.cpu_arch = .aarch64,
70+
.cpu_features_add = Target.aarch64.featureSet(&.{.v8a}),
71+
},
72+
.armeabi_v7a => .{
73+
// arm-linux-androideabi
74+
.cpu_arch = .arm,
75+
.cpu_features_add = Target.arm.featureSet(&.{.v7a}),
76+
},
77+
.x86_64 => .{
78+
// x86_64-linux-android
79+
.cpu_arch = .x86_64,
80+
},
81+
.x86 => .{
82+
// i686-linux-android
83+
.cpu_arch = .x86,
84+
},
85+
};
86+
return b.resolveTargetQuery(android_target_query.queryTarget());
87+
}
88+
89+
/// The "lib/{AndroidTarget}" directory name as it appears in an APK
90+
pub fn lib(at: AndroidTarget) []const u8 {
91+
return switch (at) {
92+
.arm64_v8a => "arm64-v8a",
93+
.armeabi_v7a => "armeabi-v7a",
94+
.x86_64 => "x86_64",
95+
.x86 => "x86",
96+
};
97+
}
98+
};
99+
58100
/// Will return a slice of Android targets
59101
/// - If -Dandroid=true, return all Android targets (x86, x86_64, aarch64, etc)
60102
/// - If -Dtarget=aarch64-linux-android, return a slice with the one specified Android target
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools"
4+
package="com.zig.minimal">
5+
6+
<application
7+
android:icon="@mipmap/ic_launcher"
8+
android:label="@string/app_name"
9+
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
10+
android:hasCode="false"
11+
tools:targetApi="31">
12+
<activity
13+
android:name="android.app.NativeActivity"
14+
android:exported="true"
15+
android:label="@string/app_name">
16+
<intent-filter>
17+
<action android:name="android.intent.action.MAIN" />
18+
19+
<category android:name="android.intent.category.LAUNCHER" />
20+
</intent-filter>
21+
</activity>
22+
</application>
23+
24+
</manifest>
4.7 KB
Loading
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<!-- Pretty name of your app -->
4+
<string name="app_name">Zig Build Test</string>
5+
<!--
6+
This is required for the APK name. This identifies your app, Android will associate
7+
your signing key with this identifier and will prevent updates if the key changes.
8+
-->
9+
<string name="package_name">com.zig.build_test</string>
10+
</resources>

test/build/build.zig

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//! This module is for testing that we implemented certain build features and to at least make sure
2+
//! there is code coverage for new APIs added.
3+
//!
4+
//! TODO(Jae): 2026-04-12
5+
//! Ideally adding functions to also validate the output APK file would be nice too.
6+
7+
const std = @import("std");
8+
const builtin = @import("builtin");
9+
10+
const android = @import("android");
11+
12+
pub fn build(b: *std.Build) void {
13+
const exe_name: []const u8 = "build_test";
14+
const root_target = b.standardTargetOptions(.{});
15+
const optimize = b.standardOptimizeOption(.{});
16+
const android_targets = android.standardTargets(b, root_target);
17+
18+
// NOTE(jae): 2026-04-12
19+
// Run it *after* the "standardTargets" call
20+
testLazyImportAndResolveTargets(b, root_target);
21+
22+
var root_target_single = [_]std.Build.ResolvedTarget{root_target};
23+
const targets: []std.Build.ResolvedTarget = if (android_targets.len == 0)
24+
root_target_single[0..]
25+
else
26+
android_targets;
27+
28+
const android_apk: ?*android.Apk = blk: {
29+
if (android_targets.len == 0) break :blk null;
30+
31+
const android_sdk = android.Sdk.create(b, .{});
32+
const apk = android_sdk.createApk(.{
33+
.name = exe_name,
34+
.api_level = .android15,
35+
.build_tools_version = "35.0.1",
36+
.ndk_version = "29.0.13113456",
37+
});
38+
const key_store_file = android_sdk.createKeyStore(.example);
39+
apk.setKeyStore(key_store_file);
40+
apk.setAndroidManifest(b.path("android/AndroidManifest.xml"));
41+
apk.addResourceDirectory(b.path("android/res"));
42+
43+
testAddLibraryFile(b, apk);
44+
45+
break :blk apk;
46+
};
47+
48+
for (targets) |target| {
49+
const app_module = b.createModule(.{
50+
.target = target,
51+
.optimize = optimize,
52+
.root_source_file = b.path("src/build_test_main.zig"),
53+
});
54+
55+
var exe: *std.Build.Step.Compile = if (target.result.abi.isAndroid()) b.addLibrary(.{
56+
.name = "main",
57+
.root_module = app_module,
58+
.linkage = .dynamic,
59+
}) else b.addExecutable(.{
60+
.name = exe_name,
61+
.root_module = app_module,
62+
});
63+
64+
// if building as library for Android, add this target
65+
// NOTE: Android has different CPU targets so you need to build a version of your
66+
// code for x86, x86_64, arm, arm64 and more
67+
if (target.result.abi.isAndroid()) {
68+
const apk: *android.Apk = android_apk orelse @panic("Android APK should be initialized");
69+
const android_dep = b.dependency("android", .{
70+
.optimize = optimize,
71+
.target = target,
72+
});
73+
exe.root_module.addImport("android", android_dep.module("android"));
74+
75+
apk.addArtifact(exe);
76+
} else {
77+
b.installArtifact(exe);
78+
79+
// If only 1 target, add "run" step
80+
if (targets.len == 1) {
81+
const run_step = b.step("run", "Run the application");
82+
const run_cmd = b.addRunArtifact(exe);
83+
run_step.dependOn(&run_cmd.step);
84+
}
85+
}
86+
}
87+
if (android_apk) |apk| {
88+
testInstallAndAddRunStep(b, apk);
89+
}
90+
}
91+
92+
/// Test calling lazyImport and then calling "resolveTargets"
93+
///
94+
/// PR: https://github.com/silbinarywolf/zig-android-sdk/pull/83
95+
fn testLazyImportAndResolveTargets(b: *std.Build, root_target: std.Build.ResolvedTarget) void {
96+
const all_android_targets = true;
97+
const android_targets: []std.Build.ResolvedTarget = blk: {
98+
if (all_android_targets or root_target.result.abi.isAndroid()) {
99+
if (b.lazyImport(@This(), "lazy_android")) |lazy_android| {
100+
break :blk lazy_android.resolveTargets(b, .{
101+
.default_target = root_target,
102+
.all_targets = true,
103+
});
104+
}
105+
}
106+
break :blk &[0]std.Build.ResolvedTarget{};
107+
};
108+
if (android_targets.len != 4) @panic("expected 'resolveTargets' it to return 4 Android targets");
109+
}
110+
111+
/// Test the addLibraryFile functionality
112+
///
113+
/// Requested feature here: https://github.com/silbinarywolf/zig-android-sdk/issues/77
114+
fn testAddLibraryFile(b: *std.Build, apk: *android.Apk) void {
115+
const vulkan_validation_dep = b.lazyDependency("vulkan_validation", .{}) orelse return;
116+
apk.addLibraryFile(.arm64_v8a, vulkan_validation_dep.path("arm64-v8a/libVkLayer_khronos_validation.so"));
117+
apk.addLibraryFile(.armeabi_v7a, vulkan_validation_dep.path("armeabi-v7a/libVkLayer_khronos_validation.so"));
118+
apk.addLibraryFile(.x86, vulkan_validation_dep.path("x86/libVkLayer_khronos_validation.so"));
119+
apk.addLibraryFile(.x86_64, vulkan_validation_dep.path("x86_64/libVkLayer_khronos_validation.so"));
120+
}
121+
122+
fn testInstallAndAddRunStep(b: *std.Build, apk: *android.Apk) void {
123+
const installed_apk = apk.addInstallApk();
124+
b.getInstallStep().dependOn(&installed_apk.step);
125+
126+
const android_sdk = apk.sdk;
127+
const run_step = b.step("run", "Install and run the application on an Android device");
128+
const adb_install = android_sdk.addAdbInstall(installed_apk.source);
129+
const adb_start = android_sdk.addAdbStart("com.zig.build_test/android.app.NativeActivity");
130+
adb_start.step.dependOn(&adb_install.step);
131+
run_step.dependOn(&adb_start.step);
132+
}

test/build/build.zig.zon

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.{
2+
.name = .android_build_test,
3+
.version = "0.0.0",
4+
.dependencies = .{
5+
.android = .{
6+
.path = "../..",
7+
},
8+
.lazy_android = .{
9+
.path = "../..",
10+
.lazy = true,
11+
},
12+
.vulkan_validation = .{
13+
.url = "https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download/vulkan-sdk-1.4.341.0/android-binaries-1.4.341.0.zip",
14+
.hash = "N-V-__8AABTXlAV0z_BGl5-lZeOEm_d2gHEhExT2qjxMqQ72",
15+
.lazy = true,
16+
},
17+
},
18+
.paths = .{
19+
"build.zig",
20+
"build.zig.zon",
21+
},
22+
.fingerprint = 0xb15d5a3541f113ad,
23+
}

0 commit comments

Comments
 (0)