From 8966c15eba17b882909318ff7dec7b446933f586 Mon Sep 17 00:00:00 2001 From: Jae B Date: Thu, 13 Mar 2025 19:54:51 +1100 Subject: [PATCH 1/2] tidy up source so that structs are using top-level namespace --- build.zig | 20 +- src/androidbuild/WindowsSdk.zig | 5 +- src/androidbuild/androidbuild.zig | 6 +- src/androidbuild/apk.zig | 1105 +++++++++---------- src/androidbuild/builtin_options_update.zig | 101 +- src/androidbuild/d8glob.zig | 164 +-- src/androidbuild/tools.zig | 727 ++++++------ 7 files changed, 1061 insertions(+), 1067 deletions(-) diff --git a/build.zig b/build.zig index 5327af9..e40d380 100644 --- a/build.zig +++ b/build.zig @@ -1,17 +1,21 @@ const std = @import("std"); const androidbuild = @import("src/androidbuild/androidbuild.zig"); -const apk = @import("src/androidbuild/apk.zig"); -const tools = @import("src/androidbuild/tools.zig"); +const Apk = @import("src/androidbuild/apk.zig"); // Expose Android build functionality for use in your build.zig -pub const ToolsOptions = tools.ToolsOptions; -pub const Tools = tools.Tools; -pub const APK = apk.APK; -pub const APILevel = androidbuild.APILevel; -pub const CreateKey = tools.CreateKey; +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 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; + /// 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 { const target = b.standardTargetOptions(.{}); @@ -24,7 +28,7 @@ pub fn build(b: *std.Build) void { }); // Create stub of builtin options. - // This is discovered and then replace in src/androidbuild/apk.zig + // This is discovered and then replaced in src/androidbuild/Apk.zig const android_builtin_options = std.Build.addOptions(b); android_builtin_options.addOption([:0]const u8, "package_name", ""); module.addImport("android_builtin", android_builtin_options.createModule()); diff --git a/src/androidbuild/WindowsSdk.zig b/src/androidbuild/WindowsSdk.zig index 70fdba3..253ca67 100644 --- a/src/androidbuild/WindowsSdk.zig +++ b/src/androidbuild/WindowsSdk.zig @@ -1,6 +1,5 @@ -// NOTE(jae): 2024-09-15 -// Copy paste of lib/std/zig/WindowsSdk.zig but cutdown to only use Registry functions - +//! NOTE(jae): 2024-09-15 +//! Copy paste of lib/std/zig/WindowsSdk.zig but cutdown to only use Registry functions const WindowsSdk = @This(); const std = @import("std"); const builtin = @import("builtin"); diff --git a/src/androidbuild/androidbuild.zig b/src/androidbuild/androidbuild.zig index e422bbf..859b7d0 100644 --- a/src/androidbuild/androidbuild.zig +++ b/src/androidbuild/androidbuild.zig @@ -5,6 +5,8 @@ const Target = std.Target; const ResolvedTarget = std.Build.ResolvedTarget; const LazyPath = std.Build.LazyPath; +const log = std.log.scoped(.@"zig-android-sdk"); + /// API Level is an enum the maps the Android OS version to the API level /// /// https://en.wikipedia.org/wiki/Android_version_history @@ -35,6 +37,8 @@ pub const APILevel = enum(u32) { android14 = 34, /// Vanilla Ice Cream android15 = 35, + /// Baklava + android16 = 36, // allow custom overrides (incase this library is not up to date with the latest android version) _, }; @@ -97,8 +101,6 @@ pub fn runNameContext(comptime name: []const u8) []const u8 { return "zig-android-sdk " ++ name; } -const log = std.log.scoped(.@"zig-android-sdk"); - pub fn printErrorsAndExit(message: []const u8, errors: []const []const u8) noreturn { nosuspend { log.err("{s}", .{message}); diff --git a/src/androidbuild/apk.zig b/src/androidbuild/apk.zig index 5692118..6460e6d 100644 --- a/src/androidbuild/apk.zig +++ b/src/androidbuild/apk.zig @@ -1,10 +1,10 @@ const std = @import("std"); const androidbuild = @import("androidbuild.zig"); -const D8Glob = @import("d8glob.zig").D8Glob; -const Tools = @import("tools.zig").Tools; -const BuiltinOptionsUpdate = @import("builtin_options_update.zig").BuiltinOptionsUpdate; +const Tools = @import("tools.zig"); +const BuiltinOptionsUpdate = @import("builtin_options_update.zig"); const KeyStore = androidbuild.KeyStore; +const D8Glob = @import("d8glob.zig"); const getAndroidTriple = androidbuild.getAndroidTriple; const runNameContext = androidbuild.runNameContext; const printErrorsAndExit = androidbuild.printErrorsAndExit; @@ -14,634 +14,616 @@ const Step = std.Build.Step; const ResolvedTarget = std.Build.ResolvedTarget; const LazyPath = std.Build.LazyPath; -pub const APK = struct { - pub const AddJavaSourceFileOption = struct { - file: LazyPath, - // NOTE(jae): 2024-09-17 - // Consider adding flags to define/declare the target Java version for this file. - // Not sure what we'll need in the future. - // flags: []const []const u8 = &.{}, - }; - pub const AddJavaSourceFilesOptions = struct { - root: LazyPath, - files: []const []const u8, - }; - pub const Resource = union(enum) { - // file: File, - directory: Directory, +pub const Resource = union(enum) { + // file: File, + directory: Directory, - // pub const File = struct { - // source: LazyPath, - // }; + // pub const File = struct { + // source: LazyPath, + // }; - pub const Directory = struct { - source: LazyPath, - }; + pub const Directory = struct { + source: LazyPath, }; +}; - b: *std.Build, - tools: *const Tools, - - 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) *@This() { - const apk: *@This() = b.allocator.create(@This()) catch @panic("OOM"); - apk.* = .{ - .b = b, - .tools = tools, - .key_store = null, - .android_manifest = null, - .artifacts = .{}, - .java_files = .{}, - .resources = .{}, - }; - return apk; - } +b: *std.Build, +tools: *const Tools, + +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 { + const apk: *Apk = b.allocator.create(Apk) catch @panic("OOM"); + apk.* = .{ + .b = b, + .tools = tools, + .key_store = null, + .android_manifest = null, + .artifacts = .{}, + .java_files = .{}, + .resources = .{}, + }; + return apk; +} - /// Set the AndroidManifest.xml file to use - pub fn setAndroidManifest(apk: *@This(), path: LazyPath) void { - apk.android_manifest = path; - } +/// Set the AndroidManifest.xml file to use +pub fn setAndroidManifest(apk: *Apk, path: LazyPath) void { + apk.android_manifest = path; +} - /// Set the directory of your Android /res/ folder. - /// ie. - /// - values/strings.xml - /// - mipmap-hdpi/ic_launcher.png - /// - mipmap-mdpi/ic_launcher.png - /// - etc - pub fn addResourceDirectory(apk: *@This(), dir: LazyPath) void { - const b = apk.b; - apk.resources.append(b.allocator, Resource{ - .directory = .{ - .source = dir, - }, - }) catch @panic("OOM"); - } +/// Set the directory of your Android /res/ folder. +/// ie. +/// - values/strings.xml +/// - mipmap-hdpi/ic_launcher.png +/// - mipmap-mdpi/ic_launcher.png +/// - etc +pub fn addResourceDirectory(apk: *Apk, dir: LazyPath) void { + const b = apk.b; + apk.resources.append(b.allocator, Resource{ + .directory = .{ + .source = dir, + }, + }) catch @panic("OOM"); +} - /// Add artifact to the Android build, this should be a shared library (*.so) - /// that targets x86, x86_64, aarch64, etc - pub fn addArtifact(apk: *@This(), compile: *std.Build.Step.Compile) void { - const b = apk.b; - apk.artifacts.append(b.allocator, compile) catch @panic("OOM"); - } +/// Add artifact to the Android build, this should be a shared library (*.so) +/// that targets x86, x86_64, aarch64, etc +pub fn addArtifact(apk: *Apk, compile: *std.Build.Step.Compile) void { + const b = apk.b; + apk.artifacts.append(b.allocator, compile) catch @panic("OOM"); +} - /// Add Java file to be transformed into DEX bytecode and packaged into a classes.dex file in the root - /// of your APK. - pub fn addJavaSourceFile(apk: *@This(), options: AddJavaSourceFileOption) void { - const b = apk.b; - apk.java_files.append(b.allocator, options.file.dupe(b)) catch @panic("OOM"); - } +pub const AddJavaSourceFileOption = struct { + file: LazyPath, + // NOTE(jae): 2024-09-17 + // Consider adding flags to define/declare the target Java version for this file. + // Not sure what we'll need in the future. + // flags: []const []const u8 = &.{}, +}; - pub fn addJavaSourceFiles(apk: *@This(), options: AddJavaSourceFilesOptions) void { - const b = apk.b; - for (options.files) |path| { - apk.addJavaSourceFile(.{ .file = options.root.path(b, path) }); - } - } +/// Add Java file to be transformed into DEX bytecode and packaged into a classes.dex file in the root +/// of your APK. +pub fn addJavaSourceFile(apk: *Apk, options: AddJavaSourceFileOption) void { + const b = apk.b; + apk.java_files.append(b.allocator, options.file.dupe(b)) catch @panic("OOM"); +} + +pub const AddJavaSourceFilesOptions = struct { + root: LazyPath, + files: []const []const u8, +}; - /// Set the keystore file used to sign the APK file - /// 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())); - pub fn setKeyStore(apk: *@This(), key_store: KeyStore) void { - apk.key_store = key_store; +pub fn addJavaSourceFiles(apk: *Apk, options: AddJavaSourceFilesOptions) void { + const b = apk.b; + for (options.files) |path| { + apk.addJavaSourceFile(.{ .file = options.root.path(b, path) }); } +} - fn addLibraryPaths(apk: *@This(), module: *std.Build.Module) void { - const b = apk.b; - const android_ndk_sysroot = apk.tools.ndk_sysroot_path; +/// Set the keystore file used to sign the APK file +/// 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())); +pub fn setKeyStore(apk: *Apk, key_store: KeyStore) void { + apk.key_store = key_store; +} - // get target - const target: ResolvedTarget = module.resolved_target orelse { - @panic(b.fmt("no 'target' set on Android module", .{})); - }; - const system_target = getAndroidTriple(target) catch |err| @panic(@errorName(err)); +fn addLibraryPaths(apk: *Apk, module: *std.Build.Module) void { + const b = apk.b; + const android_ndk_sysroot = apk.tools.ndk_sysroot_path; - // NOTE(jae): 2024-09-11 - // 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); + // get target + const target: ResolvedTarget = module.resolved_target orelse { + @panic(b.fmt("no 'target' set on Android module", .{})); + }; + const system_target = getAndroidTriple(target) catch |err| @panic(@errorName(err)); - // NOTE(jae): 2025-03-09 - // Resolve issue where building SDL2 gets the following error for 'arm-linux-androideabi' - // SDL2-2.32.2/src/cpuinfo/SDL_cpuinfo.c:93:10: error: 'cpu-features.h' file not found - // - // This include is specifically needed for: #if defined(__ANDROID__) && defined(__arm__) && !defined(HAVE_GETAUXVAL) - // - // ie. $ANDROID_HOME/ndk/{ndk_version}/sources/android/cpufeatures - if (module.resolved_target) |resolved_target| { - if (resolved_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 }) }); - } - } + // NOTE(jae): 2024-09-11 + // 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); - // ie. $ANDROID_HOME/ndk/{ndk_version}/toolchains/llvm/prebuilt/{host_os_and_arch}/sysroot ++ usr/lib/aarch64-linux-android/35 - module.addLibraryPath(.{ .cwd_relative = b.fmt("{s}/usr/lib/{s}/{d}", .{ android_ndk_sysroot, system_target, android_api_version }) }); - // ie. $ANDROID_HOME/ndk/{ndk_version}/toolchains/llvm/prebuilt/{host_os_and_arch}/sysroot ++ /usr/lib/aarch64-linux-android - module.addLibraryPath(.{ .cwd_relative = b.fmt("{s}/usr/lib/{s}", .{ android_ndk_sysroot, system_target }) }); + // NOTE(jae): 2025-03-09 + // Resolve issue where building SDL2 gets the following error for 'arm-linux-androideabi' + // SDL2-2.32.2/src/cpuinfo/SDL_cpuinfo.c:93:10: error: 'cpu-features.h' file not found + // + // This include is specifically needed for: #if defined(__ANDROID__) && defined(__arm__) && !defined(HAVE_GETAUXVAL) + // + // ie. $ANDROID_HOME/ndk/{ndk_version}/sources/android/cpufeatures + if (module.resolved_target) |resolved_target| { + if (resolved_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 }) }); + } } - pub fn installApk(apk: *@This()) void { - const b = apk.b; - const install_apk = apk.addInstallApk(); - b.getInstallStep().dependOn(&install_apk.step); - } + // ie. $ANDROID_HOME/ndk/{ndk_version}/toolchains/llvm/prebuilt/{host_os_and_arch}/sysroot ++ usr/lib/aarch64-linux-android/35 + module.addLibraryPath(.{ .cwd_relative = b.fmt("{s}/usr/lib/{s}/{d}", .{ android_ndk_sysroot, system_target, android_api_version }) }); + // ie. $ANDROID_HOME/ndk/{ndk_version}/toolchains/llvm/prebuilt/{host_os_and_arch}/sysroot ++ /usr/lib/aarch64-linux-android + module.addLibraryPath(.{ .cwd_relative = b.fmt("{s}/usr/lib/{s}", .{ android_ndk_sysroot, system_target }) }); +} - pub fn addInstallApk(apk: *@This()) *Step.InstallFile { - return apk.doInstallApk() catch |err| switch (err) { - error.OutOfMemory => @panic("OOM"), - }; - } +pub fn installApk(apk: *Apk) void { + const b = apk.b; + const install_apk = apk.addInstallApk(); + b.getInstallStep().dependOn(&install_apk.step); +} - fn doInstallApk(apk: *@This()) std.mem.Allocator.Error!*Step.InstallFile { - const b = apk.b; +pub fn addInstallApk(apk: *Apk) *Step.InstallFile { + return apk.doInstallApk() catch |err| switch (err) { + error.OutOfMemory => @panic("OOM"), + }; +} - const key_store: KeyStore = apk.key_store orelse .{ - .file = .{ .cwd_relative = "" }, - .password = "", - }; +fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { + const b = apk.b; - // validate - { - var errors = std.ArrayList([]const u8).init(b.allocator); - if (key_store.password.len == 0) { - try errors.append("Keystore not configured with password, must be setup with setKeyStore"); - } - if (apk.android_manifest == null) { - try errors.append("AndroidManifest.xml not configured, must be set with setAndroidManifest"); - } - if (apk.artifacts.items.len == 0) { - try errors.append("Must add at least one artifact targeting a valid Android CPU architecture: aarch64, x86_64, x86, etc"); - } else { - for (apk.artifacts.items, 0..) |artifact, i| { - if (artifact.kind == .exe) { - try errors.append(b.fmt("artifact[{}]: must make Android artifacts be created with addSharedLibrary, not addExecutable", .{i})); - } else { - if (artifact.linkage) |linkage| { - if (linkage != .dynamic) { - try errors.append(b.fmt("artifact[{}]: invalid linkage, expected it to be created via addSharedLibrary", .{i})); - } - } else { - try errors.append(b.fmt("artifact[{}]: unable to get linkage from artifact, expected it to be created via addSharedLibrary", .{i})); - } - } - if (artifact.root_module.resolved_target) |target| { - if (!target.result.abi.isAndroid()) { - try errors.append(b.fmt("artifact[{}]: must be targetting Android abi", .{i})); - continue; + const key_store: KeyStore = apk.key_store orelse .{ + .file = .{ .cwd_relative = "" }, + .password = "", + }; + + // validate + { + var errors = std.ArrayList([]const u8).init(b.allocator); + if (key_store.password.len == 0) { + try errors.append("Keystore not configured with password, must be setup with setKeyStore"); + } + if (apk.android_manifest == null) { + try errors.append("AndroidManifest.xml not configured, must be set with setAndroidManifest"); + } + if (apk.artifacts.items.len == 0) { + try errors.append("Must add at least one artifact targeting a valid Android CPU architecture: aarch64, x86_64, x86, etc"); + } else { + for (apk.artifacts.items, 0..) |artifact, i| { + if (artifact.kind == .exe) { + try errors.append(b.fmt("artifact[{}]: must make Android artifacts be created with addSharedLibrary, not addExecutable", .{i})); + } else { + if (artifact.linkage) |linkage| { + if (linkage != .dynamic) { + try errors.append(b.fmt("artifact[{}]: invalid linkage, expected it to be created via addSharedLibrary", .{i})); } } else { - try errors.append(b.fmt("artifact[{}]: unable to get resolved target from artifact", .{i})); + try errors.append(b.fmt("artifact[{}]: unable to get linkage from artifact, expected it to be created via addSharedLibrary", .{i})); } } + if (artifact.root_module.resolved_target) |target| { + if (!target.result.abi.isAndroid()) { + try errors.append(b.fmt("artifact[{}]: must be targetting Android abi", .{i})); + continue; + } + } else { + try errors.append(b.fmt("artifact[{}]: unable to get resolved target from artifact", .{i})); + } } - if (apk.java_files.items.len == 0) { - // NOTE(jae): 2024-09-19 - // We can probably avoid this with a stub or something but for now error so that an "adb install" - // doesn't give users: - // - Scanning Failed.: Package /data/app/base.apk code is missing] - try errors.append(b.fmt("must add at least one Java file to build", .{})); - } - if (errors.items.len > 0) { - printErrorsAndExit("misconfigured Android APK", errors.items); - } } + if (apk.java_files.items.len == 0) { + // NOTE(jae): 2024-09-19 + // We can probably avoid this with a stub or something but for now error so that an "adb install" + // doesn't give users: + // - Scanning Failed.: Package /data/app/base.apk code is missing] + try errors.append(b.fmt("must add at least one Java file to build", .{})); + } + if (errors.items.len > 0) { + printErrorsAndExit("misconfigured Android APK", errors.items); + } + } - // Setup AndroidManifest.xml - const android_manifest_file: LazyPath = apk.android_manifest orelse { - @panic("call setAndroidManifestFile and point to your AndroidManifest.xml file"); - }; + // Setup AndroidManifest.xml + const android_manifest_file: LazyPath = apk.android_manifest orelse { + @panic("call setAndroidManifestFile and point to your AndroidManifest.xml file"); + }; - // TODO(jae): 2024-10-01 - // Add option where you can explicitly set an optional release mode with like: - // - setMode(.debug) - // - // If that value ISN'T set then we can just infer based on optimization level. - const debug_apk: bool = blk: { - for (apk.artifacts.items) |root_artifact| { - if (root_artifact.root_module.optimize) |optimize| { - if (optimize == .Debug) { - break :blk true; - } + // TODO(jae): 2024-10-01 + // Add option where you can explicitly set an optional release mode with like: + // - setMode(.debug) + // + // If that value ISN'T set then we can just infer based on optimization level. + const debug_apk: bool = blk: { + for (apk.artifacts.items) |root_artifact| { + if (root_artifact.root_module.optimize) |optimize| { + if (optimize == .Debug) { + break :blk true; } } - break :blk false; - }; + } + break :blk false; + }; + + // Make resources.apk from: + // - resources.flat.zip (created from "aapt2 compile") + // - res/values/strings.xml -> values_strings.arsc.flat + // - AndroidManifest.xml + // + // This also validates your AndroidManifest.xml and can catch configuration errors + // which "aapt" was not capable of. + // See: https://developer.android.com/tools/aapt2#aapt2_element_hierarchy + // 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, + "link", + "-I", // add an existing package to base include set + apk.tools.root_jar, + }); + aapt2link.setName(runNameContext("aapt2 link")); + + if (b.verbose) { + aapt2link.addArg("-v"); + } + + // Inserts android:debuggable="true" in to the application node of the manifest, + // making the application debuggable even on production devices. + if (debug_apk) { + aapt2link.addArg("--debug-mode"); + } + + // full path to AndroidManifest.xml to include in APK + // ie. --manifest AndroidManifest.xml + aapt2link.addArg("--manifest"); + aapt2link.addFileArg(android_manifest_file); + + aapt2link.addArgs(&[_][]const u8{ + "--target-sdk-version", + b.fmt("{d}", .{@intFromEnum(apk.tools.api_level)}), + }); - // Make resources.apk from: - // - resources.flat.zip (created from "aapt2 compile") - // - res/values/strings.xml -> values_strings.arsc.flat - // - AndroidManifest.xml + // NOTE(jae): 2024-10-02 + // Explored just outputting to dir but it gets errors like: + // - error: failed to write res/mipmap-mdpi-v4/ic_launcher.png to archive: + // The system cannot find the file specified. (2). // - // This also validates your AndroidManifest.xml and can catch configuration errors - // which "aapt" was not capable of. - // See: https://developer.android.com/tools/aapt2#aapt2_element_hierarchy - // 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, - "link", - "-I", // add an existing package to base include set - apk.tools.root_jar, - }); - aapt2link.setName(runNameContext("aapt2 link")); - - if (b.verbose) { - aapt2link.addArg("-v"); - } + // So... I'll stick with the creating an APK and extracting it approach. + // aapt2link.addArg("--output-to-dir"); // Requires: Android SDK Build Tools 28.0.0 or higher + // aapt2link.addArg("-o"); + // const resources_apk_dir = aapt2link.addOutputDirectoryArg("resources"); + + aapt2link.addArg("-o"); + const resources_apk_file = aapt2link.addOutputFileArg("resources.apk"); + + // TODO(jae): 2024-09-17 + // Add support for asset directories + // Additional directory + // aapt.step.dependOn(&resource_write_files.step); + // for (app_config.asset_directories) |dir| { + // make_unsigned_apk.addArg("-A"); // additional directory in which to find raw asset files + // make_unsigned_apk.addArg(sdk.b.pathFromRoot(dir)); + // } + + // Add resource files + for (apk.resources.items) |resource| { + const resources_flat_zip = resblk: { + // Make zip of compiled resource files, ie. + // - res/values/strings.xml -> values_strings.arsc.flat + // - mipmap/ic_launcher.png -> mipmap-ic_launcher.png.flat + switch (resource) { + .directory => |resource_directory| { + const aapt2compile = b.addSystemCommand(&[_][]const u8{ + apk.tools.build_tools.aapt2, + "compile", + }); + aapt2compile.setName(runNameContext("aapt2 compile [dir]")); + + // add directory + aapt2compile.addArg("--dir"); + aapt2compile.addDirectoryArg(resource_directory.source); + + aapt2compile.addArg("-o"); + const resources_flat_zip_file = aapt2compile.addOutputFileArg("resource_dir.flat.zip"); + + break :resblk resources_flat_zip_file; + }, + } + }; - // Inserts android:debuggable="true" in to the application node of the manifest, - // making the application debuggable even on production devices. - if (debug_apk) { - aapt2link.addArg("--debug-mode"); - } + // Add resources.flat.zip + aapt2link.addFileArg(resources_flat_zip); + } - // full path to AndroidManifest.xml to include in APK - // ie. --manifest AndroidManifest.xml - aapt2link.addArg("--manifest"); - aapt2link.addFileArg(android_manifest_file); - - aapt2link.addArgs(&[_][]const u8{ - "--target-sdk-version", - b.fmt("{d}", .{@intFromEnum(apk.tools.api_level)}), - }); - - // NOTE(jae): 2024-10-02 - // Explored just outputting to dir but it gets errors like: - // - error: failed to write res/mipmap-mdpi-v4/ic_launcher.png to archive: - // The system cannot find the file specified. (2). - // - // So... I'll stick with the creating an APK and extracting it approach. - // aapt2link.addArg("--output-to-dir"); // Requires: Android SDK Build Tools 28.0.0 or higher - // aapt2link.addArg("-o"); - // const resources_apk_dir = aapt2link.addOutputDirectoryArg("resources"); - - aapt2link.addArg("-o"); - const resources_apk_file = aapt2link.addOutputFileArg("resources.apk"); - - // TODO(jae): 2024-09-17 - // Add support for asset directories - // Additional directory - // aapt.step.dependOn(&resource_write_files.step); - // for (app_config.asset_directories) |dir| { - // make_unsigned_apk.addArg("-A"); // additional directory in which to find raw asset files - // make_unsigned_apk.addArg(sdk.b.pathFromRoot(dir)); - // } - - // Add resource files - for (apk.resources.items) |resource| { - const resources_flat_zip = resblk: { - // Make zip of compiled resource files, ie. - // - res/values/strings.xml -> values_strings.arsc.flat - // - mipmap/ic_launcher.png -> mipmap-ic_launcher.png.flat - switch (resource) { - .directory => |resource_directory| { - const aapt2compile = b.addSystemCommand(&[_][]const u8{ - apk.tools.build_tools.aapt2, - "compile", - }); - aapt2compile.setName(runNameContext("aapt2 compile [dir]")); - - // add directory - aapt2compile.addArg("--dir"); - aapt2compile.addDirectoryArg(resource_directory.source); - - aapt2compile.addArg("-o"); - const resources_flat_zip_file = aapt2compile.addOutputFileArg("resource_dir.flat.zip"); - - break :resblk resources_flat_zip_file; - }, - } - }; + break :blk resources_apk_file; + }; - // Add resources.flat.zip - aapt2link.addFileArg(resources_flat_zip); - } + const package_name_file = blk: { + const aapt2packagename = b.addSystemCommand(&[_][]const u8{ + apk.tools.build_tools.aapt2, + "dump", + "packagename", + }); + aapt2packagename.setName(runNameContext("aapt2 dump packagename")); + aapt2packagename.addFileArg(resources_apk); + break :blk aapt2packagename.captureStdOut(); + }; - break :blk resources_apk_file; - }; + const android_builtin = blk: { + const android_builtin_options = std.Build.addOptions(b); + BuiltinOptionsUpdate.create(b, android_builtin_options, package_name_file); + break :blk android_builtin_options.createModule(); + }; - const package_name_file = blk: { - const aapt2packagename = b.addSystemCommand(&[_][]const u8{ - apk.tools.build_tools.aapt2, - "dump", - "packagename", - }); - aapt2packagename.setName(runNameContext("aapt2 dump packagename")); - aapt2packagename.addFileArg(resources_apk); - break :blk aapt2packagename.captureStdOut(); + // We could also use that information to create easy to use Zig step like + // - zig build adb-uninstall (adb uninstall "com.zig.sdl2") + // - zig build adb-logcat + // - Works if process isn't running anymore/crashed: Powershell: adb logcat | Select-String com.zig.sdl2: + // - Only works if process is running: adb logcat --pid=`adb shell pidof -s com.zig.sdl2` + // + // ADB install doesn't require the package name however. + // - zig build adb-install (adb install ./zig-out/bin/minimal.apk) + + // These are files that belong in root like: + // - lib/x86_64/libmain.so + // - lib/x86_64/libSDL2.so + // - lib/x86/libmain.so + // - classes.dex + const apk_files = b.addWriteFiles(); + + // Add build artifacts, usually a shared library targetting: + // - aarch64-linux-android + // - arm-linux-androideabi + // - i686-linux-android + // - x86_64-linux-android + for (apk.artifacts.items, 0..) |artifact, artifact_index| { + const target: ResolvedTarget = artifact.root_module.resolved_target orelse { + @panic(b.fmt("artifact[{d}] has no 'target' set", .{artifact_index})); }; - const android_builtin = blk: { - const android_builtin_options = std.Build.addOptions(b); - BuiltinOptionsUpdate.create(b, android_builtin_options, package_name_file); - break :blk android_builtin_options.createModule(); + // 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}/libmain.so", .{so_dir})); - // We could also use that information to create easy to use Zig step like - // - zig build adb-uninstall (adb uninstall "com.zig.sdl2") - // - zig build adb-logcat - // - Works if process isn't running anymore/crashed: Powershell: adb logcat | Select-String com.zig.sdl2: - // - Only works if process is running: adb logcat --pid=`adb shell pidof -s com.zig.sdl2` - // - // ADB install doesn't require the package name however. - // - zig build adb-install (adb install ./zig-out/bin/minimal.apk) - - // These are files that belong in root like: - // - lib/x86_64/libmain.so - // - lib/x86_64/libSDL2.so - // - lib/x86/libmain.so - // - classes.dex - const apk_files = b.addWriteFiles(); - - // Add build artifacts, usually a shared library targetting: - // - aarch64-linux-android - // - arm-linux-androideabi - // - i686-linux-android - // - x86_64-linux-android - for (apk.artifacts.items, 0..) |artifact, artifact_index| { - const target: ResolvedTarget = artifact.root_module.resolved_target orelse { - @panic(b.fmt("artifact[{d}] has no 'target' set", .{artifact_index})); - }; - - // 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}/libmain.so", .{so_dir})); - - // 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); - } + // 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); } - apk.tools.setLibCFile(artifact); - apk.addLibraryPaths(artifact.root_module); - artifact.linkLibC(); } + apk.tools.setLibCFile(artifact); + apk.addLibraryPaths(artifact.root_module); + artifact.linkLibC(); + } - // Add module - artifact.root_module.addImport("android_builtin", android_builtin); + // Add module + artifact.root_module.addImport("android_builtin", android_builtin); - var modules_it = artifact.root_module.import_table.iterator(); - while (modules_it.next()) |entry| { - const module = entry.value_ptr.*; - if (module.import_table.get("android_builtin")) |_| { - module.addImport("android_builtin", android_builtin); - } + var modules_it = artifact.root_module.import_table.iterator(); + while (modules_it.next()) |entry| { + const module = entry.value_ptr.*; + if (module.import_table.get("android_builtin")) |_| { + module.addImport("android_builtin", android_builtin); } - - // NOTE(jae): 2024-08-09 - // Try to fix compilation issues for ARM 32-bit (ie. arm-linux-androideabi) - // if (target.result.cpu.arch == .arm) { - // // artifact.root_module.addCMacro("__ARM_ARCH_7A__", ""); - // // artifact.root_module.addCMacro("_ARM_ARCH_7", ""); - // // artifact.root_module.addCMacro("__ARM_ARCH", "7"); // '__ARM_ARCH' macro redefined - // // artifact.root_module.addCMacro("_M_ARM", ""); // Fix "openxr/src/common/platform_utils.hpp" No architecture string known! - // } - - // 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); } - // Add *.jar files - // - Even if java_files.items.len == 0, we still always add the root_jar - 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, - // 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 - // Source: https://github.com/libsdl-org/SDL/blob/release-2.30.7/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java#L2045 - "-encoding", - "utf8", - "-cp", - apk.tools.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"); - const java_classes_output_dir = javac_cmd.addOutputDirectoryArg("android_classes"); - - // Add Java files - for (apk.java_files.items) |java_file| { - javac_cmd.addFileArg(java_file); - } + // 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); + } - // 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, - }); - d8.setName(runNameContext("d8")); + // Add *.jar files + // - Even if java_files.items.len == 0, we still always add the root_jar + 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, + // 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 + // Source: https://github.com/libsdl-org/SDL/blob/release-2.30.7/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java#L2045 + "-encoding", + "utf8", + "-cp", + apk.tools.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"); + const java_classes_output_dir = javac_cmd.addOutputDirectoryArg("android_classes"); + + // Add Java files + for (apk.java_files.items) |java_file| { + javac_cmd.addFileArg(java_file); + } - // ie. android_sdk/platforms/android-{api-level}/android.jar - d8.addArg("--lib"); - d8.addArg(apk.tools.root_jar); + // 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, + }); + d8.setName(runNameContext("d8")); - d8.addArg("--output"); - const dex_output_dir = d8.addOutputDirectoryArg("android_dex"); + // ie. android_sdk/platforms/android-{api-level}/android.jar + d8.addArg("--lib"); + d8.addArg(apk.tools.root_jar); - // NOTE(jae): 2024-09-22 - // As per documentation for d8, we may want to specific the minimum API level we want - // to support. Not sure how to test or expose this yet. See: https://developer.android.com/tools/d8 - // d8.addArg("--min-api"); - // d8.addArg(number_as_string); + d8.addArg("--output"); + const dex_output_dir = d8.addOutputDirectoryArg("android_dex"); - // add each output *.class file - D8Glob.addClassFilesRecursively(b, d8, java_classes_output_dir); - const dex_file = dex_output_dir.path(b, "classes.dex"); + // NOTE(jae): 2024-09-22 + // As per documentation for d8, we may want to specific the minimum API level we want + // to support. Not sure how to test or expose this yet. See: https://developer.android.com/tools/d8 + // d8.addArg("--min-api"); + // d8.addArg(number_as_string); - // Append classes.dex to apk - _ = apk_files.addCopyFile(dex_file, "classes.dex"); - } + // add each output *.class file + D8Glob.create(b, d8, java_classes_output_dir); + const dex_file = dex_output_dir.path(b, "classes.dex"); - // Extract compiled resources.apk and add contents to the folder we'll zip with "jar" below - // 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, - }); - jar.setName(runNameContext("jar (unzip resources.apk)")); - if (b.verbose) { - jar.addArg("--verbose"); - } + // Append classes.dex to apk + _ = apk_files.addCopyFile(dex_file, "classes.dex"); + } - // Extract *.apk file created with "aapt2 link" - jar.addArg("--extract"); - jar.addPrefixedFileArg("--file=", resources_apk); - - // NOTE(jae): 2024-09-30 - // Extract to directory of resources_apk and force add that to the overall apk files. - // This currently has an issue where because we can't use "addOutputDirectoryArg" this - // step will always be executed. - const extracted_apk_dir = resources_apk.dirname(); - jar.setCwd(extracted_apk_dir); - _ = apk_files.addCopyDirectory(extracted_apk_dir, "", .{ - // Ignore the *.apk that exists in this directory - .exclude_extensions = &.{".apk"}, - }); - apk_files.step.dependOn(&jar.step); + // Extract compiled resources.apk and add contents to the folder we'll zip with "jar" below + // 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, + }); + jar.setName(runNameContext("jar (unzip resources.apk)")); + if (b.verbose) { + jar.addArg("--verbose"); } - // Create zip via "jar" as it's cross-platform and aapt2 can't zip *.so or *.dex files. - // - lib/**/*.so - // - classes.dex - // - {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, - }); - jar.setName(runNameContext("jar (zip compress apk)")); - - const directory_to_zip = apk_files.getDirectory(); - jar.setCwd(directory_to_zip); - // NOTE(jae): 2024-09-30 - // Hack to ensure this side-effect re-triggers zipping this up - jar.addFileInput(directory_to_zip.path(b, "AndroidManifest.xml")); - - // -c = compress - // -f specify filename - // -M do not include a MANIFEST file - const compress_zip_arg = "-cfM"; - if (b.verbose) jar.addArg(compress_zip_arg ++ "v") else jar.addArg(compress_zip_arg); - const output_zip_file = jar.addOutputFileArg("compiled_code.zip"); - jar.addArg("."); - - break :blk output_zip_file; - }; + // Extract *.apk file created with "aapt2 link" + jar.addArg("--extract"); + jar.addPrefixedFileArg("--file=", resources_apk); + + // NOTE(jae): 2024-09-30 + // Extract to directory of resources_apk and force add that to the overall apk files. + // This currently has an issue where because we can't use "addOutputDirectoryArg" this + // step will always be executed. + const extracted_apk_dir = resources_apk.dirname(); + jar.setCwd(extracted_apk_dir); + _ = apk_files.addCopyDirectory(extracted_apk_dir, "", .{ + // Ignore the *.apk that exists in this directory + .exclude_extensions = &.{".apk"}, + }); + apk_files.step.dependOn(&jar.step); + } + + // Create zip via "jar" as it's cross-platform and aapt2 can't zip *.so or *.dex files. + // - lib/**/*.so + // - classes.dex + // - {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, + }); + jar.setName(runNameContext("jar (zip compress apk)")); + + const directory_to_zip = apk_files.getDirectory(); + jar.setCwd(directory_to_zip); + // NOTE(jae): 2024-09-30 + // Hack to ensure this side-effect re-triggers zipping this up + jar.addFileInput(directory_to_zip.path(b, "AndroidManifest.xml")); + + // -c = compress + // -f specify filename + // -M do not include a MANIFEST file + const compress_zip_arg = "-cfM"; + if (b.verbose) jar.addArg(compress_zip_arg ++ "v") else jar.addArg(compress_zip_arg); + const output_zip_file = jar.addOutputFileArg("compiled_code.zip"); + jar.addArg("."); + + break :blk output_zip_file; + }; - // NOTE(jae): 2024-09-28 - https://github.com/silbinarywolf/zig-android-sdk/issues/8 - // Experimented with using "lint" but it didn't actually catch the issue described - // in the above Github, ie. having "" - // outside of an + // NOTE(jae): 2024-09-28 - https://github.com/silbinarywolf/zig-android-sdk/issues/8 + // Experimented with using "lint" but it didn't actually catch the issue described + // in the above Github, ie. having "" + // outside of an + // + // const lint = b.addSystemCommand(&[_][]const u8{ + // apk.tools.commandline_tools.lint, + // }); + // lint.setEnvironmentVariable("PATH", b.pathJoin(&.{ apk.tools.jdk_path, "bin" })); + // lint.setEnvironmentVariable("JAVA_HOME", apk.tools.jdk_path); + // lint.addFileArg(android_manifest_file); + + const apk_name = apk.artifacts.items[0].name; + + // Align contents of .apk (zip) + const aligned_apk_file: LazyPath = blk: { + var zipalign = b.addSystemCommand(&[_][]const u8{ + apk.tools.build_tools.zipalign, + }); + zipalign.setName(runNameContext("zipalign")); + + // If you use apksigner, zipalign must be used before the APK file has been signed. + // If you sign your APK using apksigner and make further changes to the APK, its signature is invalidated. + // Source: https://developer.android.com/tools/zipalign (10th Sept, 2024) // - // const lint = b.addSystemCommand(&[_][]const u8{ - // apk.tools.commandline_tools.lint, - // }); - // lint.setEnvironmentVariable("PATH", b.pathJoin(&.{ apk.tools.jdk_path, "bin" })); - // lint.setEnvironmentVariable("JAVA_HOME", apk.tools.jdk_path); - // lint.addFileArg(android_manifest_file); - - const apk_name = apk.artifacts.items[0].name; - - // Align contents of .apk (zip) - const aligned_apk_file: LazyPath = blk: { - var zipalign = b.addSystemCommand(&[_][]const u8{ - apk.tools.build_tools.zipalign, - }); - zipalign.setName(runNameContext("zipalign")); - - // If you use apksigner, zipalign must be used before the APK file has been signed. - // If you sign your APK using apksigner and make further changes to the APK, its signature is invalidated. - // Source: https://developer.android.com/tools/zipalign (10th Sept, 2024) - // - // Example: "zipalign -P 16 -f -v 4 infile.apk outfile.apk" - if (b.verbose) { - zipalign.addArg("-v"); - } - zipalign.addArgs(&.{ - "-P", // aligns uncompressed .so files to the specified page size in KiB... - "16", // ... align to 16kb - "-f", // overwrite existing files - // "-z", // recompresses using Zopfli. (very very slow) - "4", - }); - - zipalign.addFileArg(zip_file); - const apk_file = zipalign.addOutputFileArg(b.fmt("aligned-{s}.apk", .{apk_name})); - break :blk apk_file; - }; + // Example: "zipalign -P 16 -f -v 4 infile.apk outfile.apk" + if (b.verbose) { + zipalign.addArg("-v"); + } + zipalign.addArgs(&.{ + "-P", // aligns uncompressed .so files to the specified page size in KiB... + "16", // ... align to 16kb + "-f", // overwrite existing files + // "-z", // recompresses using Zopfli. (very very slow) + "4", + }); + + zipalign.addFileArg(zip_file); + const apk_file = zipalign.addOutputFileArg(b.fmt("aligned-{s}.apk", .{apk_name})); + break :blk apk_file; + }; - // Sign apk - const signed_apk_file: LazyPath = blk: { - const apksigner = b.addSystemCommand(&[_][]const u8{ - apk.tools.build_tools.apksigner, - "sign", - }); - apksigner.setName(runNameContext("apksigner")); - apksigner.addArg("--ks"); // ks = keystore - apksigner.addFileArg(key_store.file); - apksigner.addArgs(&.{ "--ks-pass", b.fmt("pass:{s}", .{key_store.password}) }); - apksigner.addArg("--out"); - const signed_output_apk_file = apksigner.addOutputFileArg("signed-and-aligned-apk.apk"); - apksigner.addFileArg(aligned_apk_file); - break :blk signed_output_apk_file; - }; + // Sign apk + const signed_apk_file: LazyPath = blk: { + const apksigner = b.addSystemCommand(&[_][]const u8{ + apk.tools.build_tools.apksigner, + "sign", + }); + apksigner.setName(runNameContext("apksigner")); + apksigner.addArg("--ks"); // ks = keystore + apksigner.addFileArg(key_store.file); + apksigner.addArgs(&.{ "--ks-pass", b.fmt("pass:{s}", .{key_store.password}) }); + apksigner.addArg("--out"); + const signed_output_apk_file = apksigner.addOutputFileArg("signed-and-aligned-apk.apk"); + apksigner.addFileArg(aligned_apk_file); + break :blk signed_output_apk_file; + }; - const install_apk = b.addInstallBinFile(signed_apk_file, b.fmt("{s}.apk", .{apk_name})); - return install_apk; - } + const install_apk = b.addInstallBinFile(signed_apk_file, b.fmt("{s}.apk", .{apk_name})); + return install_apk; +} - fn updateLinkObjects(apk: *@This(), 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| { - 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 })); - } +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| { + 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 - const link_libc = artifact.root_module.link_libc orelse false; - const link_libcpp = artifact.root_module.link_libcpp orelse false; - if (link_libc or link_libcpp) { - // NOTE(jae): 2024-08-09 - // Try to fix compilation issues for arm-linux-androideabi - // if (target.result.cpu.arch == .arm) { - // other_step.root_module.addCMacro("_ARM_ARCH_7", ""); - // other_step.root_module.addCMacro("__ARM_ARCH_7A__", ""); // Fixes nothing - // other_step.root_module.addCMacro("__ARM_ARCH", "7"); // '__ARM_ARCH' macro redefined - // other_step.root_module.addCMacro("_M_ARM", ""); - // } - // artifact.root_module.addCMacro("__ANDROID__", "1"); - apk.tools.setLibCFile(artifact); - } + // If library is built using C or C++ then setLibCFile + const link_libc = artifact.root_module.link_libc orelse false; + const link_libcpp = artifact.root_module.link_libcpp orelse false; + if (link_libc or link_libcpp) { + apk.tools.setLibCFile(artifact); + } - // Add library paths to find "android", "log", etc - apk.addLibraryPaths(artifact.root_module); + // Add library paths to find "android", "log", etc + apk.addLibraryPaths(artifact.root_module); - // Update libraries linked to this library - apk.updateLinkObjects(artifact, so_dir, raw_top_level_apk_files); - }, - else => continue, - } - }, - else => {}, - } + // Update libraries linked to this library + apk.updateLinkObjects(artifact, so_dir, raw_top_level_apk_files); + }, + else => continue, + } + }, + else => {}, } } -}; +} fn updateSharedLibraryOptions(artifact: *std.Build.Step.Compile) void { if (artifact.linkage) |linkage| { @@ -697,5 +679,6 @@ fn updateSharedLibraryOptions(artifact: *std.Build.Step.Compile) void { // artifact.link_function_sections = true; // Seemingly not "needed" anymore, at least for x86_64 Android builds // artifact.export_table = true; - } + +const Apk = @This(); diff --git a/src/androidbuild/builtin_options_update.zig b/src/androidbuild/builtin_options_update.zig index 87de8e4..6b01636 100644 --- a/src/androidbuild/builtin_options_update.zig +++ b/src/androidbuild/builtin_options_update.zig @@ -1,3 +1,5 @@ +//! BuiltinOptionsUpdate will update the *Options + const std = @import("std"); const androidbuild = @import("androidbuild.zig"); const builtin = @import("builtin"); @@ -9,63 +11,62 @@ const fs = std.fs; const mem = std.mem; const assert = std.debug.assert; -/// BuiltinOptionsUpdate will update the *Options -pub const BuiltinOptionsUpdate = struct { - pub const base_id: Step.Id = .custom; +pub const base_id: Step.Id = .custom; + +step: Step, - step: Step, +options: *Options, +package_name_stdout: LazyPath, - 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"); + 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, + }), + .options = options, + .package_name_stdout = package_name_stdout, + }; + // Run step relies on this finishing + options.step.dependOn(&builtin_options_update.step); + // Depend on package name stdout before running this step + package_name_stdout.addStepDependencies(&builtin_options_update.step); +} - pub fn create(owner: *std.Build, options: *Options, package_name_stdout: LazyPath) void { - const builtin_options_update = owner.allocator.create(@This()) 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, - }), - .options = options, - .package_name_stdout = package_name_stdout, - }; - // Run step relies on this finishing - options.step.dependOn(&builtin_options_update.step); - // Depend on package name stdout before running this step - 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.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); +} - /// 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 { + const b = step.owner; + const builtin_options_update: *@This() = @fieldParentPtr("step", step); + const options = builtin_options_update.options; - fn make(step: *Step) !void { - const b = step.owner; - const builtin_options_update: *@This() = @fieldParentPtr("step", step); - const options = builtin_options_update.options; + const package_name_path = builtin_options_update.package_name_stdout.getPath2(b, step); - const package_name_path = builtin_options_update.package_name_stdout.getPath2(b, step); + const file = try fs.openFileAbsolute(package_name_path, .{}); - const file = try fs.openFileAbsolute(package_name_path, .{}); + // Read package name from stdout and strip line feed / carriage return + // ie. "com.zig.sdl2\n\r" + const package_name_filedata = try file.readToEndAlloc(b.allocator, 8192); + const package_name_stripped = std.mem.trimRight(u8, package_name_filedata, " \r\n"); + const package_name: [:0]const u8 = try b.allocator.dupeZ(u8, package_name_stripped); - // Read package name from stdout and strip line feed / carriage return - // ie. "com.zig.sdl2\n\r" - const package_name_filedata = try file.readToEndAlloc(b.allocator, 8192); - const package_name_stripped = std.mem.trimRight(u8, package_name_filedata, " \r\n"); - const package_name: [:0]const u8 = try b.allocator.dupeZ(u8, package_name_stripped); + options.addOption([:0]const u8, "package_name", package_name); +} - options.addOption([:0]const u8, "package_name", package_name); - } -}; +const BuiltinOptionsUpdate = @This(); diff --git a/src/androidbuild/d8glob.zig b/src/androidbuild/d8glob.zig index c52fd7e..635d8f4 100644 --- a/src/androidbuild/d8glob.zig +++ b/src/androidbuild/d8glob.zig @@ -1,3 +1,5 @@ +//! D8Glob is specific for D8 and is used to collect all *.class output files after a javac process generates them + const std = @import("std"); const androidbuild = @import("androidbuild.zig"); const builtin = @import("builtin"); @@ -9,97 +11,97 @@ const fs = std.fs; const mem = std.mem; const assert = std.debug.assert; -/// D8Glob is specific for D8 and is used to collect all *.class output files after a javac process generates them -pub const D8Glob = struct { - pub const base_id: Step.Id = .custom; +pub const base_id: Step.Id = .custom; - step: Step, +step: Step, - /// Runner to update - run: *Build.Step.Run, +/// Runner to update +run: *Build.Step.Run, - /// The directory that will contain the files to glob - dir: LazyPath, +/// The directory that will contain the files to glob +dir: LazyPath, - const file_ext = ".class"; +const file_ext = ".class"; - pub fn addClassFilesRecursively(owner: *std.Build, run: *Run, dir: LazyPath) void { - const glob = owner.allocator.create(@This()) catch @panic("OOM"); - glob.* = .{ - .step = Step.init(.{ - .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, - }), - .run = run, - .dir = dir, - }; - // Run step relies on this finishing - run.step.dependOn(&glob.step); - // If dir is generated then this will wait for that dir to generate - dir.addStepDependencies(&glob.step); - } +/// Creates a D8Glob step which is used to collect all *.class output files after a javac process generates them +pub fn create(owner: *std.Build, run: *Run, dir: LazyPath) void { + const glob = owner.allocator.create(@This()) catch @panic("OOM"); + glob.* = .{ + .step = Step.init(.{ + .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, + }), + .run = run, + .dir = dir, + }; + // Run step relies on this finishing + run.step.dependOn(&glob.step); + // If dir is generated then this will wait for that dir to generate + 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.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); - } +/// 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 { - const b = step.owner; - const arena = b.allocator; - const glob: *@This() = @fieldParentPtr("step", step); - const d8 = glob.run; +fn make(step: *Step) !void { + const b = step.owner; + const arena = b.allocator; + const glob: *@This() = @fieldParentPtr("step", step); + const d8 = glob.run; - const search_dir = glob.dir.getPath2(b, step); + const search_dir = glob.dir.getPath2(b, step); - // NOTE(jae): 2024-09-22 - // Change current working directory to where the Java classes are - // This is to avoid the Java error "command line too long" that can occur with d8 - // - // I was hitting this due to a path this long on Windows - // J:\ZigProjects\openxr-game\third-party\zig-android-sdk\examples\sdl2\.zig-cache\o\9012552ac182acf9dfb49627cf81376e\android_dex - // - // A deeper fix to this problem could be: - // - Zip up all the *.class files and just provide that as ONE argument or alternatively - // - If "d8" has the ability to pass a file of command line parameters, that would work too but I haven't seen any in the docs - d8.setCwd(glob.dir); + // NOTE(jae): 2024-09-22 + // Change current working directory to where the Java classes are + // This is to avoid the Java error "command line too long" that can occur with d8 + // + // I was hitting this due to a path this long on Windows + // J:\ZigProjects\openxr-game\third-party\zig-android-sdk\examples\sdl2\.zig-cache\o\9012552ac182acf9dfb49627cf81376e\android_dex + // + // A deeper fix to this problem could be: + // - Zip up all the *.class files and just provide that as ONE argument or alternatively + // - If "d8" has the ability to pass a file of command line parameters, that would work too but I haven't seen any in the docs + d8.setCwd(glob.dir); - var dir = try fs.openDirAbsolute(search_dir, .{ .iterate = true }); - defer dir.close(); - var walker = try dir.walk(arena); - defer walker.deinit(); - while (try walker.next()) |entry| { - if (entry.kind != .file) { - continue; - } - // NOTE(jae): 2024-10-01 - // Initially ignored classes with alternate API postfixes / etc but - // that did not work with SDL2 so no longer do that. - // - !std.mem.containsAtLeast(u8, entry.basename, 1, "$") and - // - !std.mem.containsAtLeast(u8, entry.basename, 1, "_API") - if (std.mem.endsWith(u8, entry.path, file_ext)) { - // NOTE(jae): 2024-09-22 - // We set the current working directory to "glob.Dir" and then make arguments be - // relative to that directory. - // - // This is to avoid the Java error "command line too long" that can occur with d8 - d8.addArg(entry.path); - d8.addFileInput(LazyPath{ - .cwd_relative = try fs.path.resolve(arena, &.{ search_dir, entry.path }), - }); - } + var dir = try fs.openDirAbsolute(search_dir, .{ .iterate = true }); + defer dir.close(); + var walker = try dir.walk(arena); + defer walker.deinit(); + while (try walker.next()) |entry| { + if (entry.kind != .file) { + continue; + } + // NOTE(jae): 2024-10-01 + // Initially ignored classes with alternate API postfixes / etc but + // that did not work with SDL2 so no longer do that. + // - !std.mem.containsAtLeast(u8, entry.basename, 1, "$") and + // - !std.mem.containsAtLeast(u8, entry.basename, 1, "_API") + if (std.mem.endsWith(u8, entry.path, file_ext)) { + // NOTE(jae): 2024-09-22 + // We set the current working directory to "glob.Dir" and then make arguments be + // relative to that directory. + // + // This is to avoid the Java error "command line too long" that can occur with d8 + d8.addArg(entry.path); + d8.addFileInput(LazyPath{ + .cwd_relative = try fs.path.resolve(arena, &.{ search_dir, entry.path }), + }); } } -}; +} + +const D8Glob = @This(); diff --git a/src/androidbuild/tools.zig b/src/androidbuild/tools.zig index 4c3f565..e42bf9f 100644 --- a/src/androidbuild/tools.zig +++ b/src/androidbuild/tools.zig @@ -18,41 +18,56 @@ const Step = Build.Step; const ResolvedTarget = Build.ResolvedTarget; const LazyPath = std.Build.LazyPath; -pub const CreateKey = struct { - pub const Algorithm = enum { - rsa, - - /// arg returns the keytool argument - fn arg(self: Algorithm) []const u8 { - return switch (self) { - .rsa => "RSA", - }; - } - }; - - alias: []const u8, - password: []const u8, - algorithm: Algorithm, - /// in bits, the maximum size of an RSA key supported by the Android keystore is 4096 bits (as of 2024) - key_size_in_bits: u32, - validity_in_days: u32, - /// https://stackoverflow.com/questions/3284055/what-should-i-use-for-distinguished-name-in-our-keystore-for-the-android-marke/3284135#3284135 - distinguished_name: []const u8, +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/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_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/cmdline_tools/bin or $ANDROID_HOME/tools/bin +/// +/// Available to download at: https://developer.android.com/studio#command-line-tools-only +/// The commandline tools ZIP expected looks like: commandlinetools-{OS}-11076708_latest.zip +cmdline_tools: struct { + /// lint [flags] + /// See documentation: https://developer.android.com/studio/write/lint#commandline + lint: []const u8, +}, +/// Binaries provided by the JDK that usually exist in: +/// - Non-Windows: $JAVA_HOME/bin +/// +/// Windows (either of these): +/// - C:\Program Files\Eclipse Adoptium\jdk-11.0.17.8-hotspot\ +/// - C:\Program Files\Java\jdk-17.0.4.1\ +java_tools: struct { + /// jar is used to zip up files in a cross-platform way that does not rely on + /// having "zip" in your command-line (Windows does not have this) + /// + /// ie. https://stackoverflow.com/a/18180154/5013410 + jar: []const u8, + javac: []const u8, + keytool: []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", - }; - } -}; +/// Deprecated: Use Options instead. +pub const ToolsOptions = Options; -pub const ToolsOptions = struct { +pub const Options = struct { /// ie. "35.0.0" build_tools_version: []const u8, /// ie. "27.0.12077973" @@ -61,254 +76,172 @@ pub const ToolsOptions = struct { api_level: APILevel, }; -pub const Tools = struct { - b: *Build, +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})), + }; - /// 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/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_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/cmdline_tools/bin or $ANDROID_HOME/tools/bin - /// - /// Available to download at: https://developer.android.com/studio#command-line-tools-only - /// The commandline tools ZIP expected looks like: commandlinetools-{OS}-11076708_latest.zip - cmdline_tools: struct { - /// lint [flags] - /// See documentation: https://developer.android.com/studio/write/lint#commandline - lint: []const u8, - }, - /// Binaries provided by the JDK that usually exist in: - /// - Non-Windows: $JAVA_HOME/bin - /// - /// Windows (either of these): - /// - C:\Program Files\Eclipse Adoptium\jdk-11.0.17.8-hotspot\ - /// - C:\Program Files\Java\jdk-17.0.4.1\ - java_tools: struct { - /// jar is used to zip up files in a cross-platform way that does not rely on - /// having "zip" in your command-line (Windows does not have this) - /// - /// ie. https://stackoverflow.com/a/18180154/5013410 - jar: []const u8, - javac: []const u8, - keytool: []const u8, - }, - - pub fn createKeyStore(tools: *const Tools, options: CreateKey) KeyStore { - const b = tools.b; - const keytool = b.addSystemCommand(&.{ - // https://docs.oracle.com/en/java/javase/17/docs/specs/man/keytool.html - tools.java_tools.keytool, - "-genkey", - "-v", - }); - keytool.setName(runNameContext("keytool")); - keytool.addArg("-keystore"); - const keystore_file = keytool.addOutputFileArg("zig-generated.keystore"); - keytool.addArgs(&.{ - // -alias "ca" - "-alias", - options.alias, - // -keyalg "rsa" - "-keyalg", - options.algorithm.arg(), - "-keysize", - b.fmt("{d}", .{options.key_size_in_bits}), - "-validity", - b.fmt("{d}", .{options.validity_in_days}), - "-storepass", - options.password, - "-keypass", - options.password, - // -dname "CN=example.com, OU=ID, O=Example, L=Doe, S=Jane, C=GB" - "-dname", - options.distinguished_name, - }); - // ignore stderr, it just gives you an output like: - // "Generating 4,096 bit RSA key pair and self-signed certificate (SHA384withRSA) with a validity of 10,000 days - // for: CN=example.com, OU=ID, O=Example, L=Doe, ST=Jane, C=GB" - _ = keytool.captureStdErr(); - return .{ - .file = keystore_file, - .password = options.password, - }; + // Discover tool paths + var path_search = PathSearch.init(b.allocator, host_os_tag) catch |err| switch (err) { + error.OutOfMemory => @panic("OOM"), + error.EnvironmentVariableNotFound => @panic("unable to find PATH as an environment variable"), + }; + const configured_jdk_path = getJDKPath(b.allocator) catch @panic("OOM"); + if (configured_jdk_path.len > 0) { + // Set JDK path here so it will not try searching for jarsigner.exe if searching for Android SDK + path_search.jdk_path = configured_jdk_path; } - - // 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; - - const target: ResolvedTarget = compile.root_module.resolved_target orelse { - @panic(b.fmt("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); + const configured_android_sdk_path = getAndroidSDKPath(b.allocator) catch @panic("OOM"); + if (configured_android_sdk_path.len > 0) { + // Set android SDK path here so it will not try searching for adb.exe if searching for JDK + path_search.android_sdk_path = configured_android_sdk_path; } + 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, + }); - pub fn create(b: *std.Build, options: ToolsOptions) *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})), - }; + // 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", + }); - // Discover tool paths - var path_search = PathSearch.init(b.allocator, host_os_tag) catch |err| switch (err) { - error.OutOfMemory => @panic("OOM"), - error.EnvironmentVariableNotFound => @panic("unable to find PATH as an environment variable"), + // Validate + var errors = std.ArrayList([]const u8).init(b.allocator); + defer errors.deinit(); + + // Get commandline tools path + // - 1st: $ANDROID_HOME/cmdline-tools/bin + // - 2nd: $ANDROID_HOME/tools/bin + const cmdline_tools_path = cmdlineblk: { + const cmdline_tools = b.pathResolve(&[_][]const u8{ android_sdk_path, "cmdline-tools", "latest", "bin" }); + std.fs.accessAbsolute(cmdline_tools, .{}) catch |cmderr| switch (cmderr) { + error.FileNotFound => { + const tools = b.pathResolve(&[_][]const u8{ android_sdk_path, "tools", "bin" }); + // Check if Commandline tools path is accessible + std.fs.accessAbsolute(tools, .{}) catch |toolerr| switch (toolerr) { + error.FileNotFound => { + const message = b.fmt("Android Command Line Tools not found. Expected at: {s} or {s}", .{ + cmdline_tools, + tools, + }); + errors.append(message) catch @panic("OOM"); + }, + else => { + const message = b.fmt("Android Command Line Tools path had unexpected error: {s} ({s})", .{ + @errorName(toolerr), + tools, + }); + errors.append(message) catch @panic("OOM"); + }, + }; + }, + else => { + const message = b.fmt("Android Command Line Tools path had unexpected error: {s} ({s})", .{ + @errorName(cmderr), + cmdline_tools, + }); + errors.append(message) catch @panic("OOM"); + }, }; - const configured_jdk_path = getJDKPath(b.allocator) catch @panic("OOM"); - if (configured_jdk_path.len > 0) { - // Set JDK path here so it will not try searching for jarsigner.exe if searching for Android SDK - path_search.jdk_path = configured_jdk_path; - } - const configured_android_sdk_path = getAndroidSDKPath(b.allocator) catch @panic("OOM"); - if (configured_android_sdk_path.len > 0) { - // Set android SDK path here so it will not try searching for adb.exe if searching for JDK - path_search.android_sdk_path = configured_android_sdk_path; - } - const android_sdk_path = path_search.findAndroidSDK(b.allocator) catch @panic("OOM"); - const jdk_path = path_search.findJDK(b.allocator) catch @panic("OOM"); + break :cmdlineblk cmdline_tools; + }; - // Get build tools path + if (jdk_path.len == 0) { + errors.append( + \\JDK not found. + \\- Download it from https://www.oracle.com/th/java/technologies/downloads/ + \\- Then configure your JDK_HOME environment variable to where you've installed it. + ) catch @panic("OOM"); + } + if (android_sdk_path.len == 0) { + errors.append( + \\Android SDK not found. + \\- Download it from https://developer.android.com/studio + \\- Then configure your ANDROID_HOME environment variable to where you've installed it." + ) catch @panic("OOM"); + } else { + // Check if build tools path is accessible // 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(); - - // Get commandline tools path - // - 1st: $ANDROID_HOME/cmdline-tools/bin - // - 2nd: $ANDROID_HOME/tools/bin - const cmdline_tools_path = cmdlineblk: { - const cmdline_tools = b.pathResolve(&[_][]const u8{ android_sdk_path, "cmdline-tools", "latest", "bin" }); - std.fs.accessAbsolute(cmdline_tools, .{}) catch |cmderr| switch (cmderr) { - error.FileNotFound => { - const tools = b.pathResolve(&[_][]const u8{ android_sdk_path, "tools", "bin" }); - // Check if Commandline tools path is accessible - std.fs.accessAbsolute(tools, .{}) catch |toolerr| switch (toolerr) { - error.FileNotFound => { - const message = b.fmt("Android Command Line Tools not found. Expected at: {s} or {s}", .{ - cmdline_tools, - tools, - }); - errors.append(message) catch @panic("OOM"); - }, - else => { - const message = b.fmt("Android Command Line Tools path had unexpected error: {s} ({s})", .{ - @errorName(toolerr), - tools, - }); - errors.append(message) catch @panic("OOM"); - }, - }; - }, - else => { - const message = b.fmt("Android Command Line Tools path had unexpected error: {s} ({s})", .{ - @errorName(cmderr), - cmdline_tools, - }); - errors.append(message) catch @panic("OOM"); - }, - }; - break :cmdlineblk cmdline_tools; + 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"); + }, }; - if (jdk_path.len == 0) { - errors.append( - \\JDK not found. - \\- Download it from https://www.oracle.com/th/java/technologies/downloads/ - \\- Then configure your JDK_HOME environment variable to where you've installed it. - ) catch @panic("OOM"); - } - if (android_sdk_path.len == 0) { - errors.append( - \\Android SDK not found. - \\- Download it from https://developer.android.com/studio - \\- Then configure your ANDROID_HOME environment variable to where you've installed it." - ) catch @panic("OOM"); - } else { - // 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) { + // 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 Build Tool version '{s}' not found. Install it via 'sdkmanager' or Android Studio.", .{ - options.build_tools_version, + 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 Build Tool version '{s}' had unexpected error: {s}", .{ - options.build_tools_version, + 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 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) { + // 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 version '{s}' not found. Install it via 'sdkmanager' or Android Studio.", .{ + 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 version '{s}' had unexpected error: {s} ({s})", .{ + const message = b.fmt("Android NDK sysroot '{s}' had unexpected error: {s}, at: '{s}'", .{ options.ndk_version, @errorName(err), - android_ndk_path, + android_ndk_sysroot, }); errors.append(message) catch @panic("OOM"); break :blk false; @@ -317,79 +250,28 @@ pub const Tools = struct { 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: { + // 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 sysroot '{s}' had unexpected error. Missing at '{s}'", .{ + const message = b.fmt("Android NDK version '{s}' does not support API Level {d}. No folder 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, + ndk_sysroot_target_api_version, }); errors.append(message) catch @panic("OOM"); break :blk false; }, else => { - const message = b.fmt("Android API level {d} had unexpected error: {s}, at: '{s}'", .{ + 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), - root_jar, + ndk_sysroot_target_api_version, }); errors.append(message) catch @panic("OOM"); break :blk false; @@ -398,62 +280,181 @@ pub const Tools = struct { 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); + } + if (errors.items.len > 0) { + printErrorsAndExit("unable to find required Android installation", errors.items); + } + + 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.* = .{ + .b = b, + .android_sdk_path = android_sdk_path, + .api_level = options.api_level, + .ndk_version = options.ndk_version, + .ndk_sysroot_path = android_ndk_sysroot, + .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}), + }), + }, + .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}" + }, + .java_tools = .{ + .jar = b.pathResolve(&[_][]const u8{ + jdk_path, "bin", b.fmt("jar{s}", .{exe_suffix}), + }), + .javac = b.pathResolve(&[_][]const u8{ + jdk_path, "bin", b.fmt("javac{s}", .{exe_suffix}), + }), + .keytool = b.pathResolve(&[_][]const u8{ + jdk_path, "bin", b.fmt("keytool{s}", .{exe_suffix}), + }), + }, + }; + return tools; +} + +pub const CreateKey = struct { + pub const Algorithm = enum { + rsa, + + /// arg returns the keytool argument + fn arg(self: Algorithm) []const u8 { + return switch (self) { + .rsa => "RSA", + }; } + }; - 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.* = .{ - .b = b, - .android_sdk_path = android_sdk_path, - .api_level = options.api_level, - .ndk_version = options.ndk_version, - .ndk_sysroot_path = android_ndk_sysroot, - .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}), - }), - }, - .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}" - }, - .java_tools = .{ - .jar = b.pathResolve(&[_][]const u8{ - jdk_path, "bin", b.fmt("jar{s}", .{exe_suffix}), - }), - .javac = b.pathResolve(&[_][]const u8{ - jdk_path, "bin", b.fmt("javac{s}", .{exe_suffix}), - }), - .keytool = b.pathResolve(&[_][]const u8{ - jdk_path, "bin", b.fmt("keytool{s}", .{exe_suffix}), - }), - }, + alias: []const u8, + password: []const u8, + algorithm: Algorithm, + /// in bits, the maximum size of an RSA key supported by the Android keystore is 4096 bits (as of 2024) + key_size_in_bits: u32, + validity_in_days: u32, + /// https://stackoverflow.com/questions/3284055/what-should-i-use-for-distinguished-name-in-our-keystore-for-the-android-marke/3284135#3284135 + 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", }; - return tools; } }; +pub fn createKeyStore(tools: *const Tools, options: CreateKey) KeyStore { + const b = tools.b; + const keytool = b.addSystemCommand(&.{ + // https://docs.oracle.com/en/java/javase/17/docs/specs/man/keytool.html + tools.java_tools.keytool, + "-genkey", + "-v", + }); + keytool.setName(runNameContext("keytool")); + keytool.addArg("-keystore"); + const keystore_file = keytool.addOutputFileArg("zig-generated.keystore"); + keytool.addArgs(&.{ + // -alias "ca" + "-alias", + options.alias, + // -keyalg "rsa" + "-keyalg", + options.algorithm.arg(), + "-keysize", + b.fmt("{d}", .{options.key_size_in_bits}), + "-validity", + b.fmt("{d}", .{options.validity_in_days}), + "-storepass", + options.password, + "-keypass", + options.password, + // -dname "CN=example.com, OU=ID, O=Example, L=Doe, S=Jane, C=GB" + "-dname", + options.distinguished_name, + }); + // ignore stderr, it just gives you an output like: + // "Generating 4,096 bit RSA key pair and self-signed certificate (SHA384withRSA) with a validity of 10,000 days + // for: CN=example.com, OU=ID, O=Example, L=Doe, ST=Jane, C=GB" + _ = keytool.captureStdErr(); + return .{ + .file = keystore_file, + .password = options.password, + }; +} + +// 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; + + const target: ResolvedTarget = compile.root_module.resolved_target orelse { + @panic(b.fmt("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); +} + 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. @@ -715,3 +716,5 @@ const PathSearch = struct { } } }; + +const Tools = @This(); From 0c9a65a6fb610c80a292057f1fc450c8240bb999 Mon Sep 17 00:00:00 2001 From: Jae B Date: Thu, 13 Mar 2025 19:56:13 +1100 Subject: [PATCH 2/2] more --- build.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig b/build.zig index e40d380..e91d9ba 100644 --- a/build.zig +++ b/build.zig @@ -28,7 +28,7 @@ pub fn build(b: *std.Build) void { }); // Create stub of builtin options. - // This is discovered and then replaced in src/androidbuild/Apk.zig + // This is discovered and then replaced by "Apk" in the build process const android_builtin_options = std.Build.addOptions(b); android_builtin_options.addOption([:0]const u8, "package_name", ""); module.addImport("android_builtin", android_builtin_options.createModule());