diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69d11ed..9b39eb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: include: - - os: "ubuntu-22.04" + - os: "ubuntu-latest" - os: "windows-latest" - os: "macos-14" # arm64 as per table: https://github.com/actions/runner-images/blob/8a1eeaf6ac70c66f675a04078d1a7222edd42008/README.md#available-images @@ -45,7 +45,7 @@ jobs: # note(jae): 2024-09-15 # Uses download mirror first as preferred by Zig Foundation # see: https://ziglang.org/news/migrate-to-self-hosting/ - uses: mlugg/setup-zig@v1 + uses: mlugg/setup-zig@v2 with: version: "0.14.0" @@ -99,7 +99,7 @@ jobs: # - name: Setup Zig Nightly - uses: mlugg/setup-zig@v1 + uses: mlugg/setup-zig@v2 with: version: "master" diff --git a/README.md b/README.md index c1ec23a..0c89aa1 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,12 @@ zig build -Dandroid=true const android = @import("android"); pub fn build(b: *std.Build) !void { - const android_tools = android.Tools.create(b, ...); - const apk = android.APK.create(b, android_tools); + const android_sdk = android.Sdk.create(b, .{}); + const apk = android_sdk.createApk(.{ + .api_level = .android15, + .build_tools_version = "35.0.1", + .ndk_version = "29.0.13113456", + }); apk.setAndroidManifest(b.path("android/AndroidManifest.xml")); apk.addResourceDirectory(b.path("android/res")); apk.addJavaSourceFile(.{ .file = b.path("android/src/NativeInvocationHandler.java") }); diff --git a/build.zig b/build.zig index e91d9ba..4b092b3 100644 --- a/build.zig +++ b/build.zig @@ -1,20 +1,26 @@ const std = @import("std"); const androidbuild = @import("src/androidbuild/androidbuild.zig"); -const Apk = @import("src/androidbuild/apk.zig"); // Expose Android build functionality for use in your build.zig -pub const Tools = @import("src/androidbuild/tools.zig"); -pub const APK = Apk; // TODO(jae): 2025-03-13: Consider deprecating and using 'Apk' to be conventional to Zig -pub const APILevel = androidbuild.APILevel; // TODO(jae): 2025-03-13: Consider deprecating and using 'ApiLevel' to be conventional to Zig +// TODO: rename tools.zig to Sdk.zig +pub const Sdk = @import("src/androidbuild/tools.zig"); +pub const Apk = @import("src/androidbuild/apk.zig"); +pub const ApiLevel = androidbuild.ApiLevel; pub const standardTargets = androidbuild.standardTargets; -// Deprecated exposes fields - -/// Deprecated: Use Tools.Options instead. -pub const ToolsOptions = Tools.Options; -/// Deprecated: Use Tools.CreateKey instead. -pub const CreateKey = Tools.CreateKey; +// Deprecated exposed fields + +/// Deprecated: Use ApiLevel +pub const APILevel = @compileError("use android.ApiLevel instead of android.APILevel"); +/// Deprecated: Use Sdk instead +pub const Tools = @compileError("Use android.Sdk instead of android.Tools"); +/// Deprecated: Use Apk.Options instead. +pub const ToolsOptions = @compileError("Use android.Sdk.Options instead of android.Apk.Options with the Sdk.createApk method"); +/// Deprecated: Use Sdk.CreateKey instead. +pub const CreateKey = @compileError("Use android.Sdk.CreateKey instead of android.CreateKey. Change 'android_tools.createKeyStore(android.CreateKey.example())' to 'android_sdk.createKeyStore(.example)'"); +/// Deprecated: Use Apk not APK +pub const APK = @compileError("Use android.Apk instead of android.APK"); /// NOTE: As well as providing the "android" module this declaration is required so this can be imported by other build.zig files pub fn build(b: *std.Build) void { diff --git a/examples/minimal/README.md b/examples/minimal/README.md index 3af710b..db9ae39 100644 --- a/examples/minimal/README.md +++ b/examples/minimal/README.md @@ -2,6 +2,13 @@ As of 2024-09-19, this is a thrown together, very quick copy-paste of the minimal example from the original [ZigAndroidTemplate](https://github.com/ikskuh/ZigAndroidTemplate/blob/master/examples/minimal/main.zig) repository. +### Build and run natively on your operating system or install/run on Android device + +```sh +zig build run # Native +zig build run -Dandroid # Android +``` + ### Build, install to test one target against a local emulator and run ```sh @@ -13,7 +20,7 @@ adb shell am start -S -W -n com.zig.minimal/android.app.NativeActivity ### Build and install for all supported Android targets ```sh -zig build -Dandroid=true +zig build -Dandroid adb install ./zig-out/bin/minimal.apk ``` diff --git a/examples/minimal/build.zig b/examples/minimal/build.zig index 6df20bd..3653b34 100644 --- a/examples/minimal/build.zig +++ b/examples/minimal/build.zig @@ -14,19 +14,16 @@ pub fn build(b: *std.Build) void { else android_targets; - // If building with Android, initialize the tools / build - const android_apk: ?*android.APK = blk: { - if (android_targets.len == 0) { - break :blk null; - } - const android_tools = android.Tools.create(b, .{ + 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(.{ .api_level = .android15, .build_tools_version = "35.0.1", .ndk_version = "29.0.13113456", }); - const apk = android.APK.create(b, android_tools); - - const key_store_file = android_tools.createKeyStore(android.CreateKey.example()); + 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")); @@ -62,7 +59,7 @@ pub fn build(b: *std.Build) void { // 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 apk: *android.Apk = android_apk orelse @panic("Android APK should be initialized"); const android_dep = b.dependency("android", .{ .optimize = optimize, .target = target, @@ -82,6 +79,14 @@ pub fn build(b: *std.Build) void { } } if (android_apk) |apk| { - apk.installApk(); + 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.minimal/android.app.NativeActivity"); + adb_start.step.dependOn(&adb_install.step); + run_step.dependOn(&adb_start.step); } } diff --git a/examples/raylib/README.md b/examples/raylib/README.md index f61d924..fc95210 100644 --- a/examples/raylib/README.md +++ b/examples/raylib/README.md @@ -7,6 +7,13 @@ ld.lld: warning: /.zig-cache/o/4227869d730f094811a7cdaaab535797 ``` You can ignore this error for now. +### Build and run natively on your operating system or install/run on Android device + +```sh +zig build run # Native +zig build run -Dandroid # Android +``` + ### Build, install to test one target against a local emulator and run ```sh @@ -18,16 +25,10 @@ adb shell am start -S -W -n com.zig.raylib/android.app.NativeActivity ### Build and install for all supported Android targets ```sh -zig build -Dandroid=true +zig build -Dandroid adb install ./zig-out/bin/raylib.apk ``` -### Build and run natively on your operating system - -```sh -zig build run -``` - ### Uninstall your application If installing your application fails with something like: diff --git a/examples/raylib/build.zig b/examples/raylib/build.zig index 4b054b8..9f0e81c 100644 --- a/examples/raylib/build.zig +++ b/examples/raylib/build.zig @@ -1,10 +1,6 @@ const android = @import("android"); const std = @import("std"); -//This is targeting android version 10 / API level 29. -//Change the value here and in android/AndroidManifest.xml to target your desired API level -const android_version: android.APILevel = .android10; -const android_api = std.fmt.comptimePrint("{}", .{@intFromEnum(android_version)}); const exe_name = "raylib"; pub fn build(b: *std.Build) void { @@ -18,18 +14,17 @@ pub fn build(b: *std.Build) void { else android_targets; - const android_apk: ?*android.APK = blk: { - if (android_targets.len == 0) { - break :blk null; - } - const android_tools = android.Tools.create(b, .{ - .api_level = android_version, + 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(.{ + .api_level = .android10, .build_tools_version = "35.0.1", .ndk_version = "29.0.13113456", }); - const apk = android.APK.create(b, android_tools); - const key_store_file = android_tools.createKeyStore(android.CreateKey.example()); + 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")); @@ -51,38 +46,36 @@ pub fn build(b: *std.Build) void { lib.linkLibC(); b.installArtifact(lib); - const android_ndk_path = if(android_apk) |apk| (b.fmt("{s}/ndk/{s}", .{ apk.tools.android_sdk_path, apk.tools.ndk_version })) else ""; - const raylib_dep = if (target.result.abi.isAndroid()) ( - b.dependency("raylib_zig", .{ - .target = target, - .optimize = optimize, - .android_api_version = @as([]const u8, android_api), - .android_ndk = @as([]const u8, android_ndk_path), - })) else ( + const raylib_dep = if (android_apk) |apk| + b.dependency("raylib_zig", .{ + .target = target, + .optimize = optimize, + .android_api_version = @as([]const u8, b.fmt("{}", .{@intFromEnum(apk.api_level)})), + .android_ndk = @as([]const u8, apk.ndk.path), + }) + else b.dependency("raylib_zig", .{ .target = target, .optimize = optimize, - .shared = true - })); + .shared = true, + }); + const raylib_artifact = raylib_dep.artifact("raylib"); lib.linkLibrary(raylib_artifact); const raylib_mod = raylib_dep.module("raylib"); lib.root_module.addImport("raylib", raylib_mod); - if (target.result.abi.isAndroid()) { - const apk: *android.APK = android_apk orelse @panic("Android APK should be initialized"); + if (android_apk) |apk| { const android_dep = b.dependency("android", .{ .optimize = optimize, .target = target, }); - lib.root_module.linkSystemLibrary("android", .{ .preferred_link_mode = .dynamic }); lib.root_module.addImport("android", android_dep.module("android")); + lib.root_module.linkSystemLibrary("android", .{}); - const native_app_glue_dir: std.Build.LazyPath = .{ .cwd_relative = b.fmt("{s}/sources/android/native_app_glue", .{android_ndk_path}) }; + const native_app_glue_dir: std.Build.LazyPath = .{ .cwd_relative = b.fmt("{s}/sources/android/native_app_glue", .{apk.ndk.path}) }; lib.root_module.addCSourceFile(.{ .file = native_app_glue_dir.path(b, "android_native_app_glue.c") }); lib.root_module.addIncludePath(native_app_glue_dir); - - lib.root_module.linkSystemLibrary("log", .{ .preferred_link_mode = .dynamic }); apk.addArtifact(lib); } else { const exe = b.addExecutable(.{ .name = exe_name, .optimize = optimize, .root_module = lib_mod }); @@ -94,6 +87,14 @@ pub fn build(b: *std.Build) void { } } if (android_apk) |apk| { - apk.installApk(); + 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.raylib/android.app.NativeActivity"); + adb_start.step.dependOn(&adb_install.step); + run_step.dependOn(&adb_start.step); } } diff --git a/examples/sdl2/README.md b/examples/sdl2/README.md index 1f66fbf..aadc921 100644 --- a/examples/sdl2/README.md +++ b/examples/sdl2/README.md @@ -2,6 +2,13 @@ This is a copy-paste of [Andrew Kelly's SDL Zig Demo](https://github.com/andrewrk/sdl-zig-demo) but running on Android. The build is setup so you can also target your native operating system as well. +### Build and run natively on your operating system or install/run on Android device + +```sh +zig build run # Native +zig build run -Dandroid # Android +``` + ### Build, install to test one target against a local emulator and run ```sh @@ -17,12 +24,6 @@ zig build -Dandroid=true adb install ./zig-out/bin/sdl-zig-demo.apk ``` -### Build and run natively on your operating system - -```sh -zig build run -``` - ### Uninstall your application If installing your application fails with something like: diff --git a/examples/sdl2/build.zig b/examples/sdl2/build.zig index 75342b6..06bd422 100644 --- a/examples/sdl2/build.zig +++ b/examples/sdl2/build.zig @@ -15,12 +15,11 @@ pub fn build(b: *std.Build) void { else android_targets; - // If building with Android, initialize the tools / build - const android_apk: ?*android.APK = blk: { - if (android_targets.len == 0) { - break :blk null; - } - const android_tools = android.Tools.create(b, .{ + 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(.{ .api_level = .android15, .build_tools_version = "35.0.1", .ndk_version = "29.0.13113456", @@ -34,9 +33,8 @@ pub fn build(b: *std.Build) void { // - ndk/27.0.12077973/toolchains/llvm/prebuilt/{OS}-x86_64/sysroot/usr/include/android/hardware_buffer.h:322:42: // - error: expression is not an integral constant expression }); - const apk = android.APK.create(b, android_tools); - const key_store_file = android_tools.createKeyStore(android.CreateKey.example()); + 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")); @@ -113,7 +111,7 @@ pub fn build(b: *std.Build) void { // 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 apk: *android.Apk = android_apk orelse @panic("Android APK should be initialized"); const android_dep = b.dependency("android", .{ .optimize = optimize, .target = target, @@ -133,6 +131,14 @@ pub fn build(b: *std.Build) void { } } if (android_apk) |apk| { - apk.installApk(); + 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.sdl2/com.zig.sdl2.ZigSDLActivity"); + adb_start.step.dependOn(&adb_install.step); + run_step.dependOn(&adb_start.step); } } diff --git a/src/androidbuild/BuildTools.zig b/src/androidbuild/BuildTools.zig new file mode 100644 index 0000000..736fd70 --- /dev/null +++ b/src/androidbuild/BuildTools.zig @@ -0,0 +1,76 @@ +//! Setup the path to various command-line tools available in: +//! - $ANDROID_HOME/build-tools/35.0.0 + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +aapt2: []const u8, +zipalign: []const u8, +/// d8 is *.bat or shell script that requires "java"/"java.exe" to exist in your PATH +d8: []const u8, +/// apksigner is *.bat or shell script that requires "java"/"java.exe" to exist in your PATH +apksigner: []const u8, + +pub const empty: BuildTools = .{ + .aapt2 = &[0]u8{}, + .zipalign = &[0]u8{}, + .d8 = &[0]u8{}, + .apksigner = &[0]u8{}, +}; + +const BuildToolError = Allocator.Error || error{BuildToolFailed}; + +pub fn init(b: *std.Build, android_sdk_path: []const u8, build_tools_version: []const u8, errors: *std.ArrayList([]const u8)) BuildToolError!BuildTools { + const prev_errors_len = errors.items.len; + + // Get build tools path + // ie. $ANDROID_HOME/build-tools/35.0.0 + const build_tools_path = b.pathResolve(&[_][]const u8{ android_sdk_path, "build-tools", build_tools_version }); + + // TODO(jae): 2025-05-24 + // We could validate build_tool_version to ensure its 3 numbers with dots seperating + // ie. "35.0.0" + + // Check if build tools path is accessible + // ie. $ANDROID_HOME/build-tools/35.0.0 + std.fs.accessAbsolute(build_tools_path, .{}) catch |err| switch (err) { + error.FileNotFound => { + const message = b.fmt("Android Build Tool version '{s}' not found. Install it via 'sdkmanager' or Android Studio.", .{ + build_tools_version, + }); + errors.append(message) catch @panic("OOM"); + }, + else => { + const message = b.fmt("Android Build Tool version '{s}' had unexpected error: {s}", .{ + build_tools_version, + @errorName(err), + }); + errors.append(message) catch @panic("OOM"); + }, + }; + if (errors.items.len != prev_errors_len) { + return error.BuildToolFailed; + } + + const host_os_tag = b.graph.host.result.os.tag; + const exe_suffix = if (host_os_tag == .windows) ".exe" else ""; + const bat_suffix = if (host_os_tag == .windows) ".bat" else ""; + return .{ + .aapt2 = b.pathResolve(&[_][]const u8{ + build_tools_path, b.fmt("aapt2{s}", .{exe_suffix}), + }), + .zipalign = b.pathResolve(&[_][]const u8{ + build_tools_path, b.fmt("zipalign{s}", .{exe_suffix}), + }), + // d8/apksigner are *.bat or shell scripts that require "java"/"java.exe" to exist in + // your PATH + .d8 = b.pathResolve(&[_][]const u8{ + build_tools_path, b.fmt("d8{s}", .{bat_suffix}), + }), + .apksigner = b.pathResolve(&[_][]const u8{ + build_tools_path, b.fmt("apksigner{s}", .{bat_suffix}), + }), + }; +} + +pub const BuildTools = @This(); diff --git a/src/androidbuild/Ndk.zig b/src/androidbuild/Ndk.zig new file mode 100644 index 0000000..9609f45 --- /dev/null +++ b/src/androidbuild/Ndk.zig @@ -0,0 +1,177 @@ +//! Setup the path to various command-line tools available in: +//! - $ANDROID_HOME/ndk/29.0.13113456 + +const std = @import("std"); +const androidbuild = @import("androidbuild.zig"); + +const Allocator = std.mem.Allocator; +const ApiLevel = androidbuild.ApiLevel; + +/// ie. $ANDROID_HOME +android_sdk_path: []const u8, +/// ie. "27.0.12077973" +version: []const u8, +/// ie. "$ANDROID_HOME/ndk/{ndk_version}" +path: []const u8, +/// ie. "$ANDROID_HOME/ndk/{ndk_version}/toolchains/llvm/prebuilt/{host_os_and_arch}/sysroot" +sysroot_path: []const u8, +/// ie. "$ANDROID_HOME/ndk/{ndk_version}/toolchains/llvm/prebuilt/{host_os_and_arch}/sysroot/usr/include" +include_path: []const u8, + +pub const empty: Ndk = .{ + .android_sdk_path = &[0]u8{}, + .version = &[0]u8{}, + .path = &[0]u8{}, + .sysroot_path = &[0]u8{}, + .include_path = &[0]u8{}, +}; + +const NdkError = Allocator.Error || error{NdkFailed}; + +pub fn init(b: *std.Build, android_sdk_path: []const u8, ndk_version: []const u8, errors: *std.ArrayList([]const u8)) NdkError!Ndk { + // Get NDK path + // ie. $ANDROID_HOME/ndk/27.0.12077973 + const android_ndk_path = b.fmt("{s}/ndk/{s}", .{ android_sdk_path, ndk_version }); + + const has_ndk: bool = blk: { + std.fs.accessAbsolute(android_ndk_path, .{}) catch |err| switch (err) { + error.FileNotFound => { + const message = b.fmt("Android NDK version '{s}' not found. Install it via 'sdkmanager' or Android Studio.", .{ + ndk_version, + }); + try errors.append(message); + break :blk false; + }, + else => { + const message = b.fmt("Android NDK version '{s}' had unexpected error: {s} ({s})", .{ + ndk_version, + @errorName(err), + android_ndk_path, + }); + try errors.append(message); + break :blk false; + }, + }; + break :blk true; + }; + if (!has_ndk) { + return error.NdkFailed; + } + + const host_os_tag = b.graph.host.result.os.tag; + const host_os_and_arch: [:0]const u8 = switch (host_os_tag) { + .windows => "windows-x86_64", + .linux => "linux-x86_64", + .macos => "darwin-x86_64", + else => @panic(b.fmt("unhandled operating system: {}", .{host_os_tag})), + }; + + // Get NDK sysroot path + // ie. $ANDROID_HOME/ndk/{ndk_version}/toolchains/llvm/prebuilt/{host_os_and_arch}/sysroot + const ndk_sysroot = b.fmt("{s}/ndk/{s}/toolchains/llvm/prebuilt/{s}/sysroot", .{ + android_sdk_path, + ndk_version, + host_os_and_arch, + }); + + // Check if NDK sysroot path is accessible + const has_ndk_sysroot = blk: { + std.fs.accessAbsolute(ndk_sysroot, .{}) catch |err| switch (err) { + error.FileNotFound => { + const message = b.fmt("Android NDK sysroot '{s}' had unexpected error. Missing at '{s}'", .{ + ndk_version, + ndk_sysroot, + }); + try errors.append(message); + break :blk false; + }, + else => { + const message = b.fmt("Android NDK sysroot '{s}' had unexpected error: {s}, at: '{s}'", .{ + ndk_version, + @errorName(err), + ndk_sysroot, + }); + try errors.append(message); + break :blk false; + }, + }; + break :blk true; + }; + if (!has_ndk_sysroot) { + return error.NdkFailed; + } + + const ndk: Ndk = .{ + .android_sdk_path = android_sdk_path, + .path = android_ndk_path, + .version = ndk_version, + .sysroot_path = ndk_sysroot, + .include_path = b.fmt("{s}/usr/include", .{ndk_sysroot}), + }; + return ndk; +} + +pub fn validateApiLevel(ndk: *const Ndk, b: *std.Build, api_level: ApiLevel, errors: *std.ArrayList([]const u8)) void { + // Get root jar path + const root_jar = b.pathResolve(&[_][]const u8{ + ndk.android_sdk_path, + "platforms", + b.fmt("android-{d}", .{@intFromEnum(api_level)}), + "android.jar", + }); + + // Check if NDK sysroot/usr/lib/{target}/{api_level} path is accessible + _ = blk: { + // "x86" has existed since Android 4.1 (API version 16) + const x86_system_target = "i686-linux-android"; + const ndk_sysroot_target_api_version = b.fmt("{s}/usr/lib/{s}/{d}", .{ ndk.sysroot_path, x86_system_target, @intFromEnum(api_level) }); + std.fs.accessAbsolute(ndk_sysroot_target_api_version, .{}) catch |err| switch (err) { + error.FileNotFound => { + const message = b.fmt("Android NDK version '{s}' does not support API Level {d}. No folder at '{s}'", .{ + ndk.version, + @intFromEnum(api_level), + ndk_sysroot_target_api_version, + }); + errors.append(message) catch @panic("OOM"); + break :blk false; + }, + else => { + const message = b.fmt("Android NDK version '{s}' API Level {d} had unexpected error: {s}, at: '{s}'", .{ + ndk.version, + @intFromEnum(api_level), + @errorName(err), + ndk_sysroot_target_api_version, + }); + errors.append(message) catch @panic("OOM"); + break :blk false; + }, + }; + break :blk true; + }; + + // Check if platforms/android-{api-level}/android.jar exists + _ = blk: { + std.fs.accessAbsolute(root_jar, .{}) catch |err| switch (err) { + error.FileNotFound => { + const message = b.fmt("Android API level {d} not installed. Unable to find '{s}'", .{ + @intFromEnum(api_level), + root_jar, + }); + errors.append(message) catch @panic("OOM"); + break :blk false; + }, + else => { + const message = b.fmt("Android API level {d} had unexpected error: {s}, at: '{s}'", .{ + @intFromEnum(api_level), + @errorName(err), + root_jar, + }); + errors.append(message) catch @panic("OOM"); + break :blk false; + }, + }; + break :blk true; + }; +} + +pub const Ndk = @This(); diff --git a/src/androidbuild/androidbuild.zig b/src/androidbuild/androidbuild.zig index fbd9def..0f7901b 100644 --- a/src/androidbuild/androidbuild.zig +++ b/src/androidbuild/androidbuild.zig @@ -11,7 +11,8 @@ const log = std.log.scoped(.@"zig-android-sdk"); /// /// https://en.wikipedia.org/wiki/Android_version_history /// https://apilevels.com/ -pub const APILevel = enum(u32) { +pub const ApiLevel = enum(u32) { + none = 0, /// KitKat (2013) /// Android 4.4 = 19 android4_4 = 19, @@ -43,11 +44,6 @@ pub const APILevel = enum(u32) { _, }; -pub const KeyStore = struct { - file: LazyPath, - password: []const u8, -}; - pub fn getAndroidTriple(target: ResolvedTarget) error{InvalidAndroidTarget}![]const u8 { if (!target.result.abi.isAndroid()) return error.InvalidAndroidTarget; return switch (target.result.cpu.arch) { diff --git a/src/androidbuild/apk.zig b/src/androidbuild/apk.zig index 7a55350..ef99d7f 100644 --- a/src/androidbuild/apk.zig +++ b/src/androidbuild/apk.zig @@ -1,10 +1,14 @@ const std = @import("std"); const androidbuild = @import("androidbuild.zig"); -const Tools = @import("tools.zig"); +const Sdk = @import("tools.zig"); const BuiltinOptionsUpdate = @import("builtin_options_update.zig"); -const KeyStore = androidbuild.KeyStore; +const Ndk = @import("Ndk.zig"); +const BuildTools = @import("BuildTools.zig"); const D8Glob = @import("d8glob.zig"); + +const KeyStore = Sdk.KeyStore; +const ApiLevel = androidbuild.ApiLevel; const getAndroidTriple = androidbuild.getAndroidTriple; const runNameContext = androidbuild.runNameContext; const printErrorsAndExit = androidbuild.printErrorsAndExit; @@ -28,20 +32,57 @@ pub const Resource = union(enum) { }; b: *std.Build, -tools: *const Tools, - +sdk: *Sdk, +/// Path to Native Development Kit, this includes various C-code headers, libraries, and more. +/// ie. $ANDROID_HOME/ndk/29.0.13113456 +ndk: Ndk, +/// Paths to Build Tools such as aapt2, zipalign +/// ie. $ANDROID_HOME/build-tools/35.0.0 +build_tools: BuildTools, +/// API Level is the target Android API Level +/// ie. .android15 = 35 (android 15 uses API version 35) +api_level: ApiLevel, key_store: ?KeyStore, - android_manifest: ?LazyPath, artifacts: std.ArrayListUnmanaged(*Step.Compile), java_files: std.ArrayListUnmanaged(LazyPath), resources: std.ArrayListUnmanaged(Resource), -pub fn create(b: *std.Build, tools: *const Tools) *Apk { +pub const Options = struct { + /// ie. "35.0.0" + build_tools_version: []const u8, + /// ie. "27.0.12077973" + ndk_version: []const u8, + /// ie. .android15 = 35 (android 15 uses API version 35) + api_level: ApiLevel, +}; + +pub fn create(sdk: *Sdk, options: Options) *Apk { + const b = sdk.b; + + var errors = std.ArrayList([]const u8).init(b.allocator); + defer errors.deinit(); + + const build_tools = BuildTools.init(b, sdk.android_sdk_path, options.build_tools_version, &errors) catch |err| switch (err) { + error.BuildToolFailed => BuildTools.empty, // fallthruogh and print all errors below + error.OutOfMemory => @panic("OOM"), + }; + const ndk = Ndk.init(b, sdk.android_sdk_path, options.ndk_version, &errors) catch |err| switch (err) { + error.NdkFailed => Ndk.empty, // fallthrough and print all errors below + error.OutOfMemory => @panic("OOM"), + }; + ndk.validateApiLevel(b, options.api_level, &errors); + if (errors.items.len > 0) { + printErrorsAndExit("unable to find required Android installation", errors.items); + } + const apk: *Apk = b.allocator.create(Apk) catch @panic("OOM"); apk.* = .{ .b = b, - .tools = tools, + .sdk = sdk, + .ndk = ndk, + .build_tools = build_tools, + .api_level = options.api_level, .key_store = null, .android_manifest = null, .artifacts = .empty, @@ -109,14 +150,14 @@ pub fn addJavaSourceFiles(apk: *Apk, options: AddJavaSourceFilesOptions) void { /// This is required run on an Android device. /// /// If you want to just use a temporary key for local development, do something like this: -/// - apk.setKeyStore(android_tools.createKeyStore(android.CreateKey.example())); +/// - apk.setKeyStore(android_sdk.createKeyStore(.example); pub fn setKeyStore(apk: *Apk, key_store: KeyStore) void { apk.key_store = key_store; } fn addLibraryPaths(apk: *Apk, module: *std.Build.Module) void { const b = apk.b; - const android_ndk_sysroot = apk.tools.ndk_sysroot_path; + const android_ndk_sysroot = apk.ndk.sysroot_path; // get target const target: ResolvedTarget = module.resolved_target orelse { @@ -128,7 +169,7 @@ fn addLibraryPaths(apk: *Apk, module: *std.Build.Module) void { // These *must* be in order of API version, then architecture, then non-arch specific otherwise // when starting an *.so from Android or an emulator you can get an error message like this: // - "java.lang.UnsatisfiedLinkError: dlopen failed: TLS symbol "_ZZN8gwp_asan15getThreadLocalsEvE6Locals" in dlopened" - const android_api_version: u32 = @intFromEnum(apk.tools.api_level); + const android_api_version: u32 = @intFromEnum(apk.api_level); // NOTE(jae): 2025-03-09 // Resolve issue where building SDL2 gets the following error for 'arm-linux-androideabi' @@ -139,7 +180,7 @@ fn addLibraryPaths(apk: *Apk, module: *std.Build.Module) void { // ie. $ANDROID_HOME/ndk/{ndk_version}/sources/android/cpufeatures if (target.result.cpu.arch == .arm) { module.addIncludePath(.{ - .cwd_relative = b.fmt("{s}/ndk/{s}/sources/android/cpufeatures", .{ apk.tools.android_sdk_path, apk.tools.ndk_version }), + .cwd_relative = b.fmt("{s}/ndk/{s}/sources/android/cpufeatures", .{ apk.sdk.android_sdk_path, apk.ndk.version }), }); } @@ -164,10 +205,7 @@ pub fn addInstallApk(apk: *Apk) *Step.InstallFile { fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { const b = apk.b; - const key_store: KeyStore = apk.key_store orelse .{ - .file = .{ .cwd_relative = "" }, - .password = "", - }; + const key_store: KeyStore = apk.key_store orelse .empty; // validate { @@ -241,6 +279,14 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { break :blk false; }; + // ie. "$ANDROID_HOME/Sdk/platforms/android-{api_level}/android.jar" + const root_jar = b.pathResolve(&[_][]const u8{ + apk.sdk.android_sdk_path, + "platforms", + b.fmt("android-{d}", .{@intFromEnum(apk.api_level)}), + "android.jar", + }); + // Make resources.apk from: // - resources.flat.zip (created from "aapt2 compile") // - res/values/strings.xml -> values_strings.arsc.flat @@ -252,10 +298,10 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { // Snapshot: http://web.archive.org/web/20241001070128/https://developer.android.com/tools/aapt2#aapt2_element_hierarchy const resources_apk: LazyPath = blk: { const aapt2link = b.addSystemCommand(&[_][]const u8{ - apk.tools.build_tools.aapt2, + apk.build_tools.aapt2, "link", "-I", // add an existing package to base include set - apk.tools.root_jar, + root_jar, }); aapt2link.setName(runNameContext("aapt2 link")); @@ -276,7 +322,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { aapt2link.addArgs(&[_][]const u8{ "--target-sdk-version", - b.fmt("{d}", .{@intFromEnum(apk.tools.api_level)}), + b.fmt("{d}", .{@intFromEnum(apk.api_level)}), }); // NOTE(jae): 2024-10-02 @@ -310,7 +356,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { switch (resource) { .directory => |resource_directory| { const aapt2compile = b.addSystemCommand(&[_][]const u8{ - apk.tools.build_tools.aapt2, + apk.build_tools.aapt2, "compile", }); aapt2compile.setName(runNameContext("aapt2 compile [dir]")); @@ -336,7 +382,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { const package_name_file = blk: { const aapt2packagename = b.addSystemCommand(&[_][]const u8{ - apk.tools.build_tools.aapt2, + apk.build_tools.aapt2, "dump", "packagename", }); @@ -418,8 +464,8 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { continue; } const translate_c: *std.Build.Step.TranslateC = @fieldParentPtr("step", step); - translate_c.addIncludePath(.{ .cwd_relative = apk.tools.include_path }); - translate_c.addSystemIncludePath(.{ .cwd_relative = apk.tools.getSystemIncludePath(c_translate_target) }); + translate_c.addIncludePath(.{ .cwd_relative = apk.ndk.include_path }); + translate_c.addSystemIncludePath(.{ .cwd_relative = apk.getSystemIncludePath(c_translate_target) }); }, else => continue, } @@ -440,7 +486,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { updateSharedLibraryOptions(artifact); } } - apk.tools.setLibCFile(artifact); + apk.setLibCFile(artifact); apk.addLibraryPaths(artifact.root_module); artifact.linkLibC(); @@ -457,7 +503,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { if (apk.java_files.items.len > 0) { // https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html const javac_cmd = b.addSystemCommand(&[_][]const u8{ - apk.tools.java_tools.javac, + apk.sdk.java_tools.javac, // NOTE(jae): 2024-09-22 // Force encoding to be "utf8", this fixes the following error occuring in Windows: // error: unmappable character (0x8F) for encoding windows-1252 @@ -465,10 +511,10 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { "-encoding", "utf8", "-cp", - apk.tools.root_jar, - // NOTE(jae): 2024-09-19 - // Debug issues with the SDL.java classes - // "-Xlint:deprecation", + root_jar, + // NOTE(jae): 2024-09-19 + // Debug issues with the SDL.java classes + // "-Xlint:deprecation", }); javac_cmd.setName(runNameContext("javac")); javac_cmd.addArg("-d"); @@ -482,13 +528,13 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { // From d8.bat // call "%java_exe%" %javaOpts% -cp "%jarpath%" com.android.tools.r8.D8 %params% const d8 = b.addSystemCommand(&[_][]const u8{ - apk.tools.build_tools.d8, + apk.build_tools.d8, }); d8.setName(runNameContext("d8")); // ie. android_sdk/platforms/android-{api-level}/android.jar d8.addArg("--lib"); - d8.addArg(apk.tools.root_jar); + d8.addArg(root_jar); d8.addArg("--output"); const dex_output_dir = d8.addOutputDirectoryArg("android_dex"); @@ -511,7 +557,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { // See: https://musteresel.github.io/posts/2019/07/build-android-app-bundle-on-command-line.html { const jar = b.addSystemCommand(&[_][]const u8{ - apk.tools.java_tools.jar, + apk.sdk.java_tools.jar, }); jar.setName(runNameContext("jar (unzip resources.apk)")); if (b.verbose) { @@ -555,7 +601,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { // - {directory with all resource files like: AndroidManifest.xml, res/values/strings.xml} const zip_file: LazyPath = blk: { const jar = b.addSystemCommand(&[_][]const u8{ - apk.tools.java_tools.jar, + apk.sdk.java_tools.jar, }); jar.setName(runNameContext("jar (zip compress apk)")); @@ -582,7 +628,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { // Update zip with files that are not compressed (ie. resources.arsc) const update_zip: *Step = blk: { const jar = b.addSystemCommand(&[_][]const u8{ - apk.tools.java_tools.jar, + apk.sdk.java_tools.jar, }); jar.setName(runNameContext("jar (update zip with uncompressed files)")); @@ -622,7 +668,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { // Align contents of .apk (zip) const aligned_apk_file: LazyPath = blk: { var zipalign = b.addSystemCommand(&[_][]const u8{ - apk.tools.build_tools.zipalign, + apk.build_tools.zipalign, }); zipalign.setName(runNameContext("zipalign")); @@ -653,7 +699,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { // Sign apk const signed_apk_file: LazyPath = blk: { const apksigner = b.addSystemCommand(&[_][]const u8{ - apk.tools.build_tools.apksigner, + apk.build_tools.apksigner, "sign", }); apksigner.setName(runNameContext("apksigner")); @@ -670,6 +716,19 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { return install_apk; } +fn getSystemIncludePath(apk: *Apk, target: ResolvedTarget) []const u8 { + const b = apk.b; + const system_target = getAndroidTriple(target) catch |err| @panic(@errorName(err)); + return b.fmt("{s}/{s}", .{ apk.ndk.include_path, system_target }); +} + +fn setLibCFile(apk: *Apk, compile: *Step.Compile) void { + const tools = apk.sdk; + const android_libc_path = tools.createOrGetLibCFile(compile, apk.api_level, apk.ndk.sysroot_path, apk.ndk.version); + android_libc_path.addStepDependencies(&compile.step); + 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 { const b = apk.b; for (root_artifact.root_module.link_objects.items) |link_object| { @@ -692,7 +751,7 @@ fn updateLinkObjects(apk: *Apk, root_artifact: *Step.Compile, so_dir: []const u8 if (artifact.root_module.link_libc == true or artifact.root_module.link_libcpp == true) { - apk.tools.setLibCFile(artifact); + apk.setLibCFile(artifact); } // Add library paths to find "android", "log", etc @@ -730,7 +789,7 @@ fn applyLibLinkCppWorkaroundIssue19(apk: *Apk, artifact: *Step.Compile) void { const system_target = getAndroidTriple(artifact.root_module.resolved_target.?) catch |err| @panic(@errorName(err)); const lib_path: LazyPath = .{ - .cwd_relative = b.pathJoin(&.{ apk.tools.ndk_sysroot_path, "usr", "lib", system_target, "libc++abi.a" }), + .cwd_relative = b.pathJoin(&.{ apk.ndk.sysroot_path, "usr", "lib", system_target, "libc++abi.a" }), }; const libcpp_workaround = b.addWriteFiles(); const libcppabi_dir = libcpp_workaround.addCopyFile(lib_path, "libc++abi_zig_workaround.a").dirname(); diff --git a/src/androidbuild/builtin_options_update.zig b/src/androidbuild/builtin_options_update.zig index 6b01636..b85f574 100644 --- a/src/androidbuild/builtin_options_update.zig +++ b/src/androidbuild/builtin_options_update.zig @@ -19,16 +19,13 @@ options: *Options, package_name_stdout: LazyPath, pub fn create(owner: *std.Build, options: *Options, package_name_stdout: LazyPath) void { - const builtin_options_update = owner.allocator.create(@This()) catch @panic("OOM"); + const builtin_options_update = owner.allocator.create(BuiltinOptionsUpdate) catch @panic("OOM"); builtin_options_update.* = .{ .step = Step.init(.{ .id = base_id, .name = androidbuild.runNameContext("builtin_options_update"), .owner = owner, - .makeFn = comptime if (std.mem.eql(u8, builtin.zig_version_string, "0.13.0")) - make013 - else - makeLatest, + .makeFn = make, }), .options = options, .package_name_stdout = package_name_stdout, @@ -39,21 +36,9 @@ pub fn create(owner: *std.Build, options: *Options, package_name_stdout: LazyPat package_name_stdout.addStepDependencies(&builtin_options_update.step); } -/// make for zig 0.13.0 -fn make013(step: *Step, prog_node: std.Progress.Node) !void { - _ = prog_node; // autofix - try make(step); -} - -/// make for zig 0.14.0+ -fn makeLatest(step: *Step, options: Build.Step.MakeOptions) !void { - _ = options; // autofix - try make(step); -} - -fn make(step: *Step) !void { +fn make(step: *Step, _: Build.Step.MakeOptions) !void { const b = step.owner; - const builtin_options_update: *@This() = @fieldParentPtr("step", step); + const builtin_options_update: *BuiltinOptionsUpdate = @fieldParentPtr("step", step); const options = builtin_options_update.options; const package_name_path = builtin_options_update.package_name_stdout.getPath2(b, step); diff --git a/src/androidbuild/d8glob.zig b/src/androidbuild/d8glob.zig index 635d8f4..63b898e 100644 --- a/src/androidbuild/d8glob.zig +++ b/src/androidbuild/d8glob.zig @@ -31,10 +31,7 @@ pub fn create(owner: *std.Build, run: *Run, dir: LazyPath) void { .id = base_id, .name = androidbuild.runNameContext("d8glob"), .owner = owner, - .makeFn = comptime if (std.mem.eql(u8, builtin.zig_version_string, "0.13.0")) - make013 - else - makeLatest, + .makeFn = make, }), .run = run, .dir = dir, @@ -45,19 +42,7 @@ pub fn create(owner: *std.Build, run: *Run, dir: LazyPath) void { dir.addStepDependencies(&glob.step); } -/// make for zig 0.13.0 -fn make013(step: *Step, prog_node: std.Progress.Node) !void { - _ = prog_node; // autofix - try make(step); -} - -/// make for zig 0.14.0+ -fn makeLatest(step: *Step, options: Build.Step.MakeOptions) !void { - _ = options; // autofix - try make(step); -} - -fn make(step: *Step) !void { +fn make(step: *Step, _: Build.Step.MakeOptions) !void { const b = step.owner; const arena = b.allocator; const glob: *@This() = @fieldParentPtr("step", step); diff --git a/src/androidbuild/tools.zig b/src/androidbuild/tools.zig index 712606d..f269ff0 100644 --- a/src/androidbuild/tools.zig +++ b/src/androidbuild/tools.zig @@ -1,13 +1,13 @@ const std = @import("std"); const builtin = @import("builtin"); const androidbuild = @import("androidbuild.zig"); +const Allocator = std.mem.Allocator; /// Used for reading install locations from the registry const RegistryWtf8 = @import("WindowsSdk.zig").RegistryWtf8; const windows = std.os.windows; -const APILevel = androidbuild.APILevel; -const KeyStore = androidbuild.KeyStore; +const ApiLevel = androidbuild.ApiLevel; const getAndroidTriple = androidbuild.getAndroidTriple; const runNameContext = androidbuild.runNameContext; const printErrorsAndExit = androidbuild.printErrorsAndExit; @@ -17,29 +17,19 @@ const AccessError = std.fs.Dir.AccessError; const Step = Build.Step; const ResolvedTarget = Build.ResolvedTarget; const LazyPath = std.Build.LazyPath; +const Apk = @import("apk.zig"); +const Ndk = @import("Ndk.zig"); +const BuildTools = @import("BuildTools.zig"); b: *Build, /// On most platforms this will map to the $ANDROID_HOME environment variable android_sdk_path: []const u8, -/// ie. .android15 = 35 (android 15 uses API version 35) -api_level: APILevel, -/// ie. "27.0.12077973" -ndk_version: []const u8, -/// ie. "$ANDROID_HOME/ndk/{ndk_version}/toolchains/llvm/prebuilt/{host_os_and_arch}/sysroot" -ndk_sysroot_path: []const u8, -/// ie. "$ANDROID_HOME/ndk/{ndk_version}/toolchains/llvm/prebuilt/{host_os_and_arch}/sysroot/usr/include" -include_path: []const u8, -/// ie. "$ANDROID_HOME/Sdk/platforms/android-{api_level}/android.jar" -root_jar: []const u8, -// $JDK_HOME, $JAVA_HOME or auto-discovered from java binaries found in $PATH +/// $JDK_HOME, $JAVA_HOME or auto-discovered from java binaries found in $PATH jdk_path: []const u8, -/// ie. $ANDROID_HOME/build-tools/35.0.0 -build_tools: struct { - aapt2: []const u8, - zipalign: []const u8, - d8: []const u8, - apksigner: []const u8, +/// ie. $ANDROID_HOME/platform-tools +platform_tools: struct { + adb: []const u8, }, /// ie. $ANDROID_HOME/cmdline_tools/bin or $ANDROID_HOME/tools/bin /// @@ -49,6 +39,7 @@ cmdline_tools: struct { /// lint [flags] /// See documentation: https://developer.android.com/studio/write/lint#commandline lint: []const u8, + sdkmanager: []const u8, }, /// Binaries provided by the JDK that usually exist in: /// - Non-Windows: $JAVA_HOME/bin @@ -66,26 +57,13 @@ java_tools: struct { keytool: []const u8, }, -/// Deprecated: Use Options instead. -pub const ToolsOptions = Options; +/// Reserved for future use +const Options = struct {}; -pub const Options = struct { - /// ie. "35.0.0" - build_tools_version: []const u8, - /// ie. "27.0.12077973" - ndk_version: []const u8, - /// ie. .android15 = 35 (android 15 uses API version 35) - api_level: APILevel, -}; +pub fn create(b: *std.Build, options: Options) *Sdk { + _ = options; -pub fn create(b: *std.Build, options: Options) *Tools { const host_os_tag = b.graph.host.result.os.tag; - const host_os_and_arch: [:0]const u8 = switch (host_os_tag) { - .windows => "windows-x86_64", - .linux => "linux-x86_64", - .macos => "darwin-x86_64", - else => @panic(b.fmt("unhandled operating system: {}", .{host_os_tag})), - }; // Discover tool paths var path_search = PathSearch.init(b.allocator, host_os_tag) catch |err| switch (err) { @@ -105,30 +83,6 @@ pub fn create(b: *std.Build, options: Options) *Tools { const android_sdk_path = path_search.findAndroidSDK(b.allocator) catch @panic("OOM"); const jdk_path = path_search.findJDK(b.allocator) catch @panic("OOM"); - // Get build tools path - // ie. $ANDROID_HOME/build-tools/35.0.0 - const build_tools_path = b.pathResolve(&[_][]const u8{ android_sdk_path, "build-tools", options.build_tools_version }); - - // Get NDK path - // ie. $ANDROID_HOME/ndk/27.0.12077973 - const android_ndk_path = b.fmt("{s}/ndk/{s}", .{ android_sdk_path, options.ndk_version }); - - // Get NDK sysroot path - // ie. $ANDROID_HOME/ndk/{ndk_version}/toolchains/llvm/prebuilt/{host_os_and_arch}/sysroot - const android_ndk_sysroot = b.fmt("{s}/ndk/{s}/toolchains/llvm/prebuilt/{s}/sysroot", .{ - android_sdk_path, - options.ndk_version, - host_os_and_arch, - }); - - // Get root jar path - const root_jar = b.pathResolve(&[_][]const u8{ - android_sdk_path, - "platforms", - b.fmt("android-{d}", .{@intFromEnum(options.api_level)}), - "android.jar", - }); - // Validate var errors = std.ArrayList([]const u8).init(b.allocator); defer errors.deinit(); @@ -188,170 +142,32 @@ pub fn create(b: *std.Build, options: Options) *Tools { break :cmdlineblk cmdline_tools; }; - { - // Check if build tools path is accessible - // ie. $ANDROID_HOME/build-tools/35.0.0 - std.fs.accessAbsolute(build_tools_path, .{}) catch |err| switch (err) { - error.FileNotFound => { - const message = b.fmt("Android Build Tool version '{s}' not found. Install it via 'sdkmanager' or Android Studio.", .{ - options.build_tools_version, - }); - errors.append(message) catch @panic("OOM"); - }, - else => { - const message = b.fmt("Android Build Tool version '{s}' had unexpected error: {s}", .{ - options.build_tools_version, - @errorName(err), - }); - errors.append(message) catch @panic("OOM"); - }, - }; - - // Check if NDK path is accessible - // ie. $ANDROID_HOME/ndk/27.0.12077973 - const has_ndk: bool = blk: { - std.fs.accessAbsolute(android_ndk_path, .{}) catch |err| switch (err) { - error.FileNotFound => { - const message = b.fmt("Android NDK version '{s}' not found. Install it via 'sdkmanager' or Android Studio.", .{ - options.ndk_version, - }); - errors.append(message) catch @panic("OOM"); - break :blk false; - }, - else => { - const message = b.fmt("Android NDK version '{s}' had unexpected error: {s} ({s})", .{ - options.ndk_version, - @errorName(err), - android_ndk_path, - }); - errors.append(message) catch @panic("OOM"); - break :blk false; - }, - }; - break :blk true; - }; - - // Check if NDK API level is accessible - if (has_ndk) { - // Check if NDK sysroot path is accessible - const has_ndk_sysroot = blk: { - std.fs.accessAbsolute(android_ndk_sysroot, .{}) catch |err| switch (err) { - error.FileNotFound => { - const message = b.fmt("Android NDK sysroot '{s}' had unexpected error. Missing at '{s}'", .{ - options.ndk_version, - android_ndk_sysroot, - }); - errors.append(message) catch @panic("OOM"); - break :blk false; - }, - else => { - const message = b.fmt("Android NDK sysroot '{s}' had unexpected error: {s}, at: '{s}'", .{ - options.ndk_version, - @errorName(err), - android_ndk_sysroot, - }); - errors.append(message) catch @panic("OOM"); - break :blk false; - }, - }; - break :blk true; - }; - - // Check if NDK sysroot/usr/lib/{target}/{api_level} path is accessible - if (has_ndk_sysroot) { - _ = blk: { - // "x86" has existed since Android 4.1 (API version 16) - const x86_system_target = "i686-linux-android"; - const ndk_sysroot_target_api_version = b.fmt("{s}/usr/lib/{s}/{d}", .{ android_ndk_sysroot, x86_system_target, options.api_level }); - std.fs.accessAbsolute(android_ndk_sysroot, .{}) catch |err| switch (err) { - error.FileNotFound => { - const message = b.fmt("Android NDK version '{s}' does not support API Level {d}. No folder at '{s}'", .{ - options.ndk_version, - @intFromEnum(options.api_level), - ndk_sysroot_target_api_version, - }); - errors.append(message) catch @panic("OOM"); - break :blk false; - }, - else => { - const message = b.fmt("Android NDK version '{s}' API Level {d} had unexpected error: {s}, at: '{s}'", .{ - options.ndk_version, - @intFromEnum(options.api_level), - @errorName(err), - ndk_sysroot_target_api_version, - }); - errors.append(message) catch @panic("OOM"); - break :blk false; - }, - }; - break :blk true; - }; - } - - // Check if platforms/android-{api-level}/android.jar exists - _ = blk: { - std.fs.accessAbsolute(root_jar, .{}) catch |err| switch (err) { - error.FileNotFound => { - const message = b.fmt("Android API level {d} not installed. Unable to find '{s}'", .{ - @intFromEnum(options.api_level), - root_jar, - }); - errors.append(message) catch @panic("OOM"); - break :blk false; - }, - else => { - const message = b.fmt("Android API level {d} had unexpected error: {s}, at: '{s}'", .{ - @intFromEnum(options.api_level), - @errorName(err), - root_jar, - }); - errors.append(message) catch @panic("OOM"); - break :blk false; - }, - }; - break :blk true; - }; - } - } if (errors.items.len > 0) { printErrorsAndExit("unable to find required Android installation", errors.items); } + const platform_tools_path = b.pathResolve(&[_][]const u8{ android_sdk_path, "platform-tools" }); + const exe_suffix = if (host_os_tag == .windows) ".exe" else ""; const bat_suffix = if (host_os_tag == .windows) ".bat" else ""; - const tools: *Tools = b.allocator.create(Tools) catch @panic("OOM"); - tools.* = .{ + const sdk: *Sdk = b.allocator.create(Sdk) catch @panic("OOM"); + sdk.* = .{ .b = b, .android_sdk_path = android_sdk_path, - .api_level = options.api_level, - .ndk_version = options.ndk_version, - .ndk_sysroot_path = android_ndk_sysroot, - .include_path = b.fmt("{s}/usr/include", .{tools.ndk_sysroot_path}), - .root_jar = root_jar, .jdk_path = jdk_path, - .build_tools = .{ - .aapt2 = b.pathResolve(&[_][]const u8{ - build_tools_path, b.fmt("aapt2{s}", .{exe_suffix}), - }), - .zipalign = b.pathResolve(&[_][]const u8{ - build_tools_path, b.fmt("zipalign{s}", .{exe_suffix}), - }), - // d8/apksigner are *.bat or shell scripts that require "java"/"java.exe" to exist in - // your PATH - .d8 = b.pathResolve(&[_][]const u8{ - build_tools_path, b.fmt("d8{s}", .{bat_suffix}), - }), - .apksigner = b.pathResolve(&[_][]const u8{ - build_tools_path, b.fmt("apksigner{s}", .{bat_suffix}), + .platform_tools = .{ + .adb = b.pathResolve(&[_][]const u8{ + platform_tools_path, b.fmt("adb{s}", .{exe_suffix}), }), }, .cmdline_tools = .{ .lint = b.pathResolve(&[_][]const u8{ cmdline_tools_path, b.fmt("lint{s}", .{bat_suffix}), }), - // NOTE(jae): 2024-09-28 - // Consider adding sdkmanager.bat so you can do something like "zig build sdkmanager -- {args}" + .sdkmanager = b.pathResolve(&[_][]const u8{ + cmdline_tools_path, b.fmt("sdkmanager{s}", .{bat_suffix}), + }), }, .java_tools = .{ .jar = b.pathResolve(&[_][]const u8{ @@ -365,7 +181,71 @@ pub fn create(b: *std.Build, options: Options) *Tools { }), }, }; - return tools; + return sdk; +} + +pub fn createApk(sdk: *Sdk, options: Apk.Options) *Apk { + return Apk.create(sdk, options); +} + +// TODO: Consider adding step to run: sdkmanager --install "ndk;21.3.6528147" +// pub fn installNdkVersion(ndk_version: []const u8) *Step { +// } + +/// Start an installed application on your Android device or emulator. +/// To install an APK first see "addAdbInstall" +/// +/// ie. +/// - "adb shell am start -S -W -n com.zig.minimal/android.app.NativeActivity" +/// - "adb shell am start -S -W -n com.zig.sdl2/com.zig.sdl2.ZigSDLActivity" +pub fn addAdbStart(sdk: *Sdk, package_name_and_java_entry: []const u8) *Step.Run { + const b = sdk.b; + if (sdk.platform_tools.adb.len == 0) { + @panic("Cannot call addAdbStart as 'adb' is not installed"); + } + // TODO: Improve this to be its own special Step that can auto-detect the "com.zig.sdl2/com.zig.sdl2.ZigSDLActivity" data + const adb_shell_start = b.addSystemCommand(&.{ sdk.platform_tools.adb, "shell", "am", "start", "-S", "-W", "-n", package_name_and_java_entry }); + return adb_shell_start; +} + +/// Install an APK onto your Android device or emulator +/// ie. "adb install ./zig-out/bin/minimal.apk" +pub fn adbInstall(sdk: *Sdk, apk: LazyPath) void { + const b = sdk.b; + const adb_install = sdk.addAdbInstall(apk); + b.getInstallStep().dependOn(&adb_install.step); +} + +/// Install an APK onto your Android device or emulator +/// ie. "adb install ./zig-out/bin/minimal.apk" +pub fn addAdbInstall(sdk: *Sdk, apk: LazyPath) *Step.Run { + const b = sdk.b; + if (sdk.platform_tools.adb.len == 0) { + @panic("Cannot call addInstallApk as 'adb' is not installed"); + } + const adb_install = b.addSystemCommand(&.{ + sdk.platform_tools.adb, + "install", + }); + adb_install.addFileArg(apk); + return adb_install; +} + +/// EXPERIMENTAL: Allows invoking the Android SDK manager +/// ie. zig build -Dandroid sdkmanager -- --help +pub fn addSdkManagerStep(sdk: *Sdk) void { + const b = sdk.b; + const sdkmanager_step = b.step("sdkmanager", "Run the Android SDK Manager"); + const args = b.args orelse &.{}; + const sdkmanager = b.addSystemCommand(&.{sdk.cmdline_tools.sdkmanager}); + sdkmanager.setEnvironmentVariable("SKIP_JDK_VERSION_CHECK", "1"); + if (b.verbose) { + sdkmanager.addArg("--verbose"); + } + sdkmanager_step.dependOn(&sdkmanager.step); + for (args) |arg| { + sdkmanager.addArg(arg); + } } pub const CreateKey = struct { @@ -390,23 +270,21 @@ pub const CreateKey = struct { distinguished_name: []const u8, /// Generates an example key that you can use for debugging your application locally - pub fn example() @This() { - return .{ - .alias = "default", - .password = "example_password", - .algorithm = .rsa, - .key_size_in_bits = 4096, - .validity_in_days = 10_000, - .distinguished_name = "CN=example.com, OU=ID, O=Example, L=Doe, S=Jane, C=GB", - }; - } + pub const example: CreateKey = .{ + .alias = "default", + .password = "example_password", + .algorithm = .rsa, + .key_size_in_bits = 4096, + .validity_in_days = 10_000, + .distinguished_name = "CN=example.com, OU=ID, O=Example, L=Doe, S=Jane, C=GB", + }; }; -pub fn createKeyStore(tools: *const Tools, options: CreateKey) KeyStore { - const b = tools.b; +pub fn createKeyStore(sdk: *const Sdk, options: CreateKey) KeyStore { + const b = sdk.b; const keytool = b.addSystemCommand(&.{ // https://docs.oracle.com/en/java/javase/17/docs/specs/man/keytool.html - tools.java_tools.keytool, + sdk.java_tools.keytool, "-genkey", "-v", }); @@ -442,32 +320,18 @@ pub fn createKeyStore(tools: *const Tools, options: CreateKey) KeyStore { }; } -// TODO: Consider making this be setup on "create" and then we just pass in the "android_libc_writefile" -// anytime setLibCFile is called -pub fn setLibCFile(tools: *const Tools, compile: *Step.Compile) void { - const b = tools.b; +pub fn createOrGetLibCFile(sdk: *Sdk, compile: *Step.Compile, android_api_level: ApiLevel, ndk_sysroot_path: []const u8, ndk_version: []const u8) LazyPath { + const b = sdk.b; const target: ResolvedTarget = compile.root_module.resolved_target orelse @panic("no 'target' set on Android module"); const system_target = getAndroidTriple(target) catch |err| @panic(@errorName(err)); - const android_libc_path = createLibC( - b, - system_target, - tools.api_level, - tools.ndk_sysroot_path, - tools.ndk_version, - ); - android_libc_path.addStepDependencies(&compile.step); - compile.setLibCFile(android_libc_path); -} - -pub fn getSystemIncludePath(tools: *const Tools, target: ResolvedTarget) []const u8 { - const b = tools.b; - const system_target = getAndroidTriple(target) catch |err| @panic(@errorName(err)); - return b.fmt("{s}/{s}", .{ tools.include_path, system_target }); -} + // NOTE(jae): 2025-05-25 + // Tried just utilizing the target version here but it was very low (14) and there was no NDK libraries that went + // back that far for NDK version "29.0.13113456" + // const android_api_level: ApiLevel = @enumFromInt(target.result.os.version_range.linux.android); + // if (android_api_level == .none) @panic("no 'android' api level set on target"); -fn createLibC(b: *std.Build, system_target: []const u8, android_version: APILevel, ndk_sysroot_path: []const u8, ndk_version: []const u8) LazyPath { const libc_file_format = \\# Generated by zig-android-sdk. DO NOT EDIT. \\ @@ -498,7 +362,7 @@ fn createLibC(b: *std.Build, system_target: []const u8, android_version: APILeve const include_dir = b.fmt("{s}/usr/include", .{ndk_sysroot_path}); const sys_include_dir = b.fmt("{s}/usr/include/{s}", .{ ndk_sysroot_path, system_target }); - const crt_dir = b.fmt("{s}/usr/lib/{s}/{d}", .{ ndk_sysroot_path, system_target, @intFromEnum(android_version) }); + const crt_dir = b.fmt("{s}/usr/lib/{s}/{d}", .{ ndk_sysroot_path, system_target, @intFromEnum(android_api_level) }); const libc_file_contents = b.fmt(libc_file_format, .{ .include_dir = include_dir, @@ -506,7 +370,7 @@ fn createLibC(b: *std.Build, system_target: []const u8, android_version: APILeve .crt_dir = crt_dir, }); - const filename = b.fmt("android-libc_target-{s}_version-{}_ndk-{s}.conf", .{ system_target, @intFromEnum(android_version), ndk_version }); + const filename = b.fmt("android-libc_target-{s}_version-{}_ndk-{s}.conf", .{ system_target, @intFromEnum(android_api_level), ndk_version }); const write_file = b.addWriteFiles(); const android_libc_path = write_file.add(filename, libc_file_contents); @@ -515,24 +379,24 @@ fn createLibC(b: *std.Build, system_target: []const u8, android_version: APILeve /// Search JDK_HOME, and then JAVA_HOME fn getJDKPath(allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 { - const jdkHome = std.process.getEnvVarOwned(allocator, "JDK_HOME") catch |err| switch (err) { + const jdk_home = std.process.getEnvVarOwned(allocator, "JDK_HOME") catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.EnvironmentVariableNotFound => &[0]u8{}, // Windows-only error.InvalidWtf8 => @panic("JDK_HOME environment variable is invalid UTF-8"), }; - if (jdkHome.len > 0) { - return jdkHome; + if (jdk_home.len > 0) { + return jdk_home; } - const javaHome = std.process.getEnvVarOwned(allocator, "JAVA_HOME") catch |err| switch (err) { + const java_home = std.process.getEnvVarOwned(allocator, "JAVA_HOME") catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.EnvironmentVariableNotFound => &[0]u8{}, // Windows-only error.InvalidWtf8 => @panic("JAVA_HOME environment variable is invalid UTF-8"), }; - if (javaHome.len > 0) { - return javaHome; + if (java_home.len > 0) { + return java_home; } return &[0]u8{}; @@ -540,14 +404,14 @@ fn getJDKPath(allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 { /// Caller must free returned memory fn getAndroidSDKPath(allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 { - const androidHome = std.process.getEnvVarOwned(allocator, "ANDROID_HOME") catch |err| switch (err) { + const android_home = std.process.getEnvVarOwned(allocator, "ANDROID_HOME") catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.EnvironmentVariableNotFound => &[0]u8{}, // Windows-only error.InvalidWtf8 => @panic("ANDROID_HOME environment variable is invalid UTF-8"), }; - if (androidHome.len > 0) { - return androidHome; + if (android_home.len > 0) { + return android_home; } // Check for Android Studio @@ -555,12 +419,14 @@ fn getAndroidSDKPath(allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 .windows => { // First, see if SdkPath in the registry is set // - Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Android Studio - "SdkPath" - // - Computer\KHEY_CURRENT_USER\SOFTWARE\Android Studio - "SdkPath" + // - Computer\KHEY_CURRENT_USER\SOFTWARE\Android Studio - "SdkPath" const android_studio_sdk_path: []const u8 = blk: { for ([_]windows.HKEY{ windows.HKEY_CURRENT_USER, windows.HKEY_LOCAL_MACHINE }) |hkey| { const key = RegistryWtf8.openKey(hkey, "SOFTWARE", .{}) catch |err| switch (err) { error.KeyNotFound => continue, }; + // NOTE(jae): 2025-05-25 - build.txt file says "AI-243.24978.46.2431.13208083" + // For my install, "SdkPath" is an empty string, so this may not be used anymore. const sdk_path = key.getString(allocator, "Android Studio", "SdkPath") catch |err| switch (err) { error.StringNotFound, error.ValueNameNotFound, error.NotAString => continue, error.OutOfMemory => return error.OutOfMemory, @@ -665,6 +531,16 @@ fn getAndroidSDKPath(allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 return &[0]u8{}; } +pub const KeyStore = struct { + file: LazyPath, + password: []const u8, + + pub const empty: KeyStore = .{ + .file = .{ .cwd_relative = "" }, + .password = "", + }; +}; + /// Searches your PATH environment variable directories for adb, jarsigner, etc const PathSearch = struct { allocator: std.mem.Allocator, @@ -692,8 +568,8 @@ const PathSearch = struct { // setup binaries to search for const exe_suffix = if (host_os_tag == .windows) ".exe" else ""; - const adb = std.mem.concat(allocator, u8, &.{ "adb", exe_suffix }) catch |err| return err; - const jarsigner = std.mem.concat(allocator, u8, &.{ "jarsigner", exe_suffix }) catch |err| return err; + const adb = try std.mem.concat(allocator, u8, &.{ "adb", exe_suffix }); + const jarsigner = try std.mem.concat(allocator, u8, &.{ "jarsigner", exe_suffix }); const path_it = std.mem.splitScalar(u8, path_env, ';'); return .{ @@ -711,7 +587,7 @@ const PathSearch = struct { } /// Get the Android SDK Path, the caller owns the memory - pub fn findAndroidSDK(self: *PathSearch, allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 { + pub fn findAndroidSDK(self: *PathSearch, allocator: std.mem.Allocator) Allocator.Error![]const u8 { if (self.android_sdk_path == null) { // Iterate over PATH environment folders until we either hit the end or the Android SDK folder try self.getNext(.androidsdk); @@ -723,7 +599,7 @@ const PathSearch = struct { } /// Get the JDK Path, the caller owns the memory - pub fn findJDK(self: *PathSearch, allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 { + pub fn findJDK(self: *PathSearch, allocator: std.mem.Allocator) Allocator.Error![]const u8 { if (self.jdk_path == null) { // Iterate over PATH environment folders until we either hit the end or the Android SDK folder try self.getNext(.jdk); @@ -739,7 +615,7 @@ const PathSearch = struct { jdk, }; - fn getNext(self: *PathSearch, path: PathType) error{OutOfMemory}!void { + fn getNext(self: *PathSearch, path: PathType) Allocator.Error!void { const allocator = self.allocator; while (self.path_it.next()) |path_item| { if (path_item.len == 0) continue; @@ -804,4 +680,4 @@ const PathSearch = struct { } }; -const Tools = @This(); +const Sdk = @This();