diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5606fb..c7fa18d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,6 +138,9 @@ jobs: run: zig build -Dandroid=true --verbose working-directory: examples/sdl2 - - name: Build Raylib Example (Zig Stable) - run: zig build -Dandroid=true --verbose - working-directory: examples/raylib + # note(jae): 2026-01-10 + # Downstream packages for Raylib won't work with Zig 0.15.2 *and* Zig 0.16.X + # + # - name: Build Raylib Example (Zig Nightly) + # run: zig build -Dandroid=true --verbose + # working-directory: examples/raylib diff --git a/build.zig b/build.zig index 4b092b3..6da6662 100644 --- a/build.zig +++ b/build.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const androidbuild = @import("src/androidbuild/androidbuild.zig"); // Expose Android build functionality for use in your build.zig @@ -27,17 +28,56 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const module = b.addModule("android", .{ + // Create stub of builtin options. + // 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", ""); + const android_builtin_module = android_builtin_options.createModule(); + + // Create android module + const android_module = b.addModule("android", .{ .root_source_file = b.path("src/android/android.zig"), .target = target, .optimize = optimize, }); + const ndk_module = b.createModule(.{ + .root_source_file = b.path("src/android/ndk/ndk.zig"), + .target = target, + .optimize = optimize, + }); + android_module.addImport("ndk", ndk_module); + android_module.addImport("android_builtin", android_builtin_module); - // Create stub of builtin options. - // 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()); + // Add backwards compatibility modules + if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) { + // Deprecated: Allow older Zig builds to work + var zig014 = b.createModule(.{ + .root_source_file = b.path("src/android/zig014/zig014.zig"), + .target = target, + .optimize = optimize, + }); + zig014.addImport("ndk", ndk_module); + zig014.addImport("android_builtin", android_builtin_module); + android_module.addImport("zig014", zig014); + } + if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) { + // Add as a module to deal with @Type(.enum_literal) being deprecated + const zig015 = b.createModule(.{ + .root_source_file = b.path("src/android/zig015/zig015.zig"), + .target = target, + .optimize = optimize, + }); + android_module.addImport("zig015", zig015); + } + if (builtin.zig_version.major == 0 and builtin.zig_version.minor >= 16) { + // Add as a module to deal with @Type(.enum_literal) being deprecated + const zig016 = b.createModule(.{ + .root_source_file = b.path("src/android/zig016/zig016.zig"), + .target = target, + .optimize = optimize, + }); + android_module.addImport("zig016", zig016); + } - module.linkSystemLibrary("log", .{}); + android_module.linkSystemLibrary("log", .{}); } diff --git a/build.zig.zon b/build.zig.zon index ccd6e4a..484222c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .android, - .version = "0.2.0", + .version = "0.3.0", .dependencies = .{}, .paths = .{ "build.zig", diff --git a/examples/minimal/src/minimal.zig b/examples/minimal/src/minimal.zig index 06ea918..96548ac 100644 --- a/examples/minimal/src/minimal.zig +++ b/examples/minimal/src/minimal.zig @@ -12,8 +12,8 @@ pub const std_options: std.Options = if (builtin.abi.isAndroid()) else .{}; -/// custom panic handler for Android -pub const panic = if (builtin.abi.isAndroid()) +/// Deprecated: Zig 0.15.2 and lower only, Custom panic handler for Android +pub const panic = if (builtin.abi.isAndroid() and builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) android.panic else std.debug.FullPanic(std.debug.defaultPanic); diff --git a/examples/raylib/src/main.zig b/examples/raylib/src/main.zig index 98c0dc8..3d57302 100644 --- a/examples/raylib/src/main.zig +++ b/examples/raylib/src/main.zig @@ -3,7 +3,7 @@ const builtin = @import("builtin"); const android = @import("android"); const rl = @import("raylib"); -pub fn main() void { +pub fn main() !void { const screenWidth = 800; const screenHeight = 450; rl.initWindow(screenWidth, screenHeight, "raylib-zig [core] example - basic window"); @@ -19,8 +19,8 @@ pub fn main() void { } } -/// custom panic handler for Android -pub const panic = if (builtin.abi.isAndroid()) +/// Deprecated: Zig 0.15.2 and lower only, Custom panic handler for Android +pub const panic = if (builtin.abi.isAndroid() and builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) android.panic else std.debug.FullPanic(std.debug.defaultPanic); diff --git a/examples/sdl2/build.zig b/examples/sdl2/build.zig index 06bd422..ba3ce55 100644 --- a/examples/sdl2/build.zig +++ b/examples/sdl2/build.zig @@ -21,8 +21,8 @@ pub fn build(b: *std.Build) void { 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", + .build_tools_version = "36.1.0", + .ndk_version = "29.0.14206865", // NOTE(jae): 2025-03-09 // Previously this example used 'ndk' "27.0.12077973". // @@ -70,14 +70,6 @@ pub fn build(b: *std.Build) void { .optimize = optimize, .root_source_file = b.path("src/sdl-zig-demo.zig"), }); - var exe: *std.Build.Step.Compile = if (target.result.abi.isAndroid()) b.addLibrary(.{ - .name = exe_name, - .root_module = app_module, - .linkage = .dynamic, - }) else b.addExecutable(.{ - .name = exe_name, - .root_module = app_module, - }); const library_optimize = if (!target.result.abi.isAndroid()) optimize @@ -96,15 +88,15 @@ pub fn build(b: *std.Build) void { if (target.result.os.tag == .linux and !target.result.abi.isAndroid()) { // The SDL package doesn't work for Linux yet, so we rely on system // packages for now. - exe.linkSystemLibrary("SDL2"); - exe.linkLibC(); + app_module.linkSystemLibrary("SDL2", .{}); + app_module.link_libc = true; } else { const sdl_lib = sdl_dep.artifact("SDL2"); - exe.linkLibrary(sdl_lib); + app_module.linkLibrary(sdl_lib); } const sdl_module = sdl_dep.module("sdl"); - exe.root_module.addImport("sdl", sdl_module); + app_module.addImport("sdl", sdl_module); } // if building as library for Android, add this target @@ -116,10 +108,19 @@ pub fn build(b: *std.Build) void { .optimize = optimize, .target = target, }); - exe.root_module.addImport("android", android_dep.module("android")); + app_module.addImport("android", android_dep.module("android")); - apk.addArtifact(exe); + const exe_lib = b.addLibrary(.{ + .name = exe_name, + .root_module = app_module, + .linkage = .dynamic, + }); + apk.addArtifact(exe_lib); } else { + const exe = b.addExecutable(.{ + .name = exe_name, + .root_module = app_module, + }); b.installArtifact(exe); // If only 1 target, add "run" step diff --git a/examples/sdl2/src/sdl-zig-demo.zig b/examples/sdl2/src/sdl-zig-demo.zig index 5123269..819c358 100644 --- a/examples/sdl2/src/sdl-zig-demo.zig +++ b/examples/sdl2/src/sdl-zig-demo.zig @@ -6,7 +6,7 @@ const sdl = @import("sdl"); const log = std.log; const assert = std.debug.assert; -/// custom standard options for Android +// custom standard options for Android pub const std_options: std.Options = if (builtin.abi.isAndroid()) .{ .logFn = android.logFn, @@ -14,8 +14,8 @@ pub const std_options: std.Options = if (builtin.abi.isAndroid()) else .{}; -/// custom panic handler for Android -pub const panic = if (builtin.abi.isAndroid()) +/// Deprecated: Zig 0.15.2 and lower only, Custom panic handler for Android +pub const panic = if (builtin.abi.isAndroid() and builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) android.panic else std.debug.FullPanic(std.debug.defaultPanic); @@ -29,7 +29,20 @@ comptime { /// This needs to be exported for Android builds fn SDL_main() callconv(.c) void { if (comptime builtin.abi.isAndroid()) { - _ = std.start.callMain(); + if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) { + _ = std.start.callMain(); + } else if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) { + main() catch |err| { + log.err("{s}", .{@errorName(err)}); + if (@errorReturnTrace()) |trace| { + if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) { + std.debug.dumpStackTrace(trace.*); + } else { + std.debug.dumpStackTrace(trace); + } + } + }; + } } else { @compileError("SDL_main should not be called outside of Android builds"); } diff --git a/examples/sdl2/third-party/sdl2/build.zig b/examples/sdl2/third-party/sdl2/build.zig index 359ca59..7641cca 100644 --- a/examples/sdl2/third-party/sdl2/build.zig +++ b/examples/sdl2/third-party/sdl2/build.zig @@ -10,89 +10,90 @@ pub fn build(b: *std.Build) !void { const sdl_include_path = sdl_path.path(b, "include"); const is_shared_library = target.result.abi.isAndroid(); // NOTE(jae): 2024-09-22: Android uses shared library as SDL2 loads it as part of SDLActivity.java + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }); const lib = b.addLibrary(.{ .name = "SDL2", - .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - .link_libc = true, - }), + .root_module = mod, .linkage = if (is_shared_library) .dynamic else .static, }); - lib.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = sdl_path, .files = &generic_src_files, }); - lib.root_module.addCMacro("SDL_USE_BUILTIN_OPENGL_DEFINITIONS", "1"); + mod.addCMacro("SDL_USE_BUILTIN_OPENGL_DEFINITIONS", "1"); - var sdl_config_header: ?*std.Build.Step.ConfigHeader = null; + // var sdl_config_header: ?*std.Build.Step.ConfigHeader = null; switch (target.result.os.tag) { .windows => { // Between Zig 0.13.0 and Zig 0.14.0, "windows.gaming.input.h" was removed from "lib/libc/include/any-windows-any" // This folder brings all headers needed by that one file so that SDL can be compiled for Windows. - lib.addIncludePath(b.path("upstream/any-windows-any")); + mod.addIncludePath(b.path("upstream/any-windows-any")); - lib.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = sdl_path, .files = &windows_src_files, }); - lib.linkSystemLibrary("setupapi"); - lib.linkSystemLibrary("winmm"); - lib.linkSystemLibrary("gdi32"); - lib.linkSystemLibrary("imm32"); - lib.linkSystemLibrary("version"); - lib.linkSystemLibrary("oleaut32"); - lib.linkSystemLibrary("ole32"); + mod.linkSystemLibrary("setupapi", .{}); + mod.linkSystemLibrary("winmm", .{}); + mod.linkSystemLibrary("gdi32", .{}); + mod.linkSystemLibrary("imm32", .{}); + mod.linkSystemLibrary("version", .{}); + mod.linkSystemLibrary("oleaut32", .{}); + mod.linkSystemLibrary("ole32", .{}); }, .macos => { // NOTE(jae): 2024-07-07 // Cross-compilation from Linux to Mac requires more effort currently (Zig 0.13.0) // See: https://github.com/ziglang/zig/issues/1349 - lib.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = sdl_path, .files = &darwin_src_files, }); - lib.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = sdl_path, .files = &objective_c_src_files, .flags = &.{"-fobjc-arc"}, }); - lib.linkFramework("OpenGL"); - lib.linkFramework("Metal"); - lib.linkFramework("CoreVideo"); - lib.linkFramework("Cocoa"); - lib.linkFramework("IOKit"); - lib.linkFramework("ForceFeedback"); - lib.linkFramework("Carbon"); - lib.linkFramework("CoreAudio"); - lib.linkFramework("AudioToolbox"); - lib.linkFramework("AVFoundation"); - lib.linkFramework("Foundation"); - lib.linkFramework("GameController"); - lib.linkFramework("CoreHaptics"); + mod.linkFramework("OpenGL", .{}); + mod.linkFramework("Metal", .{}); + mod.linkFramework("CoreVideo", .{}); + mod.linkFramework("Cocoa", .{}); + mod.linkFramework("IOKit", .{}); + mod.linkFramework("ForceFeedback", .{}); + mod.linkFramework("Carbon", .{}); + mod.linkFramework("CoreAudio", .{}); + mod.linkFramework("AudioToolbox", .{}); + mod.linkFramework("AVFoundation", .{}); + mod.linkFramework("Foundation", .{}); + mod.linkFramework("GameController", .{}); + mod.linkFramework("CoreHaptics", .{}); }, else => { if (target.result.abi.isAndroid()) { - lib.root_module.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = sdl_path, .files = &android_src_files, }); // NOTE(jae): 2024-09-22 // Build settings taken from: SDL2-2.32.2/src/hidapi/android/jni/Android.mk // SDLActivity.java by default expects to be able to load this library - lib.root_module.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = sdl_path, .files = &[_][]const u8{ "src/hidapi/android/hid.cpp", }, .flags = &.{"-std=c++11"}, }); - lib.linkLibCpp(); + mod.link_libcpp = true; // This is needed for "src/render/opengles/SDL_render_gles.c" to compile - lib.root_module.addCMacro("GL_GLEXT_PROTOTYPES", "1"); + mod.addCMacro("GL_GLEXT_PROTOTYPES", "1"); // Add Java files to dependency const java_dir = sdl_dep.path("android-project/app/src/main/java/org/libsdl/app"); @@ -113,12 +114,12 @@ pub fn build(b: *std.Build) !void { } // https://github.com/libsdl-org/SDL/blob/release-2.30.6/Android.mk#L82C62-L82C69 - lib.linkSystemLibrary("dl"); - lib.linkSystemLibrary("GLESv1_CM"); - lib.linkSystemLibrary("GLESv2"); - lib.linkSystemLibrary("OpenSLES"); - lib.linkSystemLibrary("log"); - lib.linkSystemLibrary("android"); + mod.linkSystemLibrary("dl", .{}); + mod.linkSystemLibrary("GLESv1_CM", .{}); + mod.linkSystemLibrary("GLESv2", .{}); + mod.linkSystemLibrary("OpenSLES", .{}); + mod.linkSystemLibrary("log", .{}); + mod.linkSystemLibrary("android", .{}); // SDLActivity.java's getMainFunction defines the entrypoint as "SDL_main" // So your main / root file will need something like this for Android @@ -130,23 +131,26 @@ pub fn build(b: *std.Build) !void { // if (builtin.abi.isAndroid()) @export(&android_sdl_main, .{ .name = "SDL_main", .linkage = .strong }); // } } else { - const config_header = b.addConfigHeader(.{ - .style = .{ .cmake = sdl_include_path.path(b, "SDL_config.h.cmake") }, - .include_path = "SDL/SDL_config.h", - }, .{}); - sdl_config_header = config_header; - - lib.addConfigHeader(config_header); - lib.installConfigHeader(config_header); + // NOTE(jae): 2026-01-10 + // Not maintained for example anymore. + // + // const config_header = b.addConfigHeader(.{ + // .style = .{ .cmake = sdl_include_path.path(b, "SDL_config.h.cmake") }, + // .include_path = "SDL/SDL_config.h", + // }, .{}); + // sdl_config_header = config_header; + // + // mod.addConfigHeader(config_header); + // lib.installConfigHeader(config_header); } }, } // NOTE(jae): 2024-07-07 // This must come *after* addConfigHeader logic above for per-OS so that the include for SDL_config.h takes precedence - lib.addIncludePath(sdl_include_path); + mod.addIncludePath(sdl_include_path); // NOTE(jae): 2024-04-07 // Not installing header as we include/export it from the module - // lib.installHeadersDirectory("include", "SDL"); + // mod.installHeadersDirectory("include", "SDL"); b.installArtifact(lib); var sdl_c_module = b.addTranslateC(.{ @@ -154,10 +158,29 @@ pub fn build(b: *std.Build) !void { .optimize = .ReleaseFast, .root_source_file = b.path("src/sdl.h"), }); - if (sdl_config_header) |config_header| { - sdl_c_module.addConfigHeader(config_header); - } + // if (sdl_config_header) |config_header| { + // sdl_c_module.addConfigHeader(config_header); + // } sdl_c_module.addIncludePath(sdl_include_path); + if (target.result.abi.isAndroid()) { + // NOTE(jae): 2026-01-10 + // Resolve issue in Zig 0.16.X at least, where _Nonnull causes issues when building SDL2 + // for Android. + // + // error: nullability specifier cannot be applied to non-pointer type 'unsigned short [3]' + // in: ndk/29.0.14206865/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/stdlib.h:163 + sdl_c_module.defineCMacro("_Nonnull", ""); + + // NOTE(jae): 2026-01-10 + // Resolve issue in Zig 0.16.X at least, where ARM_NEON_H causes issues when building SDL2 + // for Android. + // + // error: expected ')', found 'an identifier' + // in: lib/include/arm_neon.h:74025:73: error: expected ')', found 'an identifier' + if (target.result.cpu.arch == .aarch64) { + sdl_c_module.defineCMacro("__ARM_NEON_H", "1"); + } + } _ = b.addModule("sdl", .{ .target = target, @@ -243,7 +266,7 @@ const generic_src_files = [_][]const u8{ "src/stdlib/SDL_malloc.c", "src/stdlib/SDL_mslibc.c", "src/stdlib/SDL_qsort.c", - "src/stdlib/SDL_stdlib.c", + //"src/stdlib/SDL_stdmod.c", // Removed between 2.32.2 and 2.32.10 "src/stdlib/SDL_string.c", "src/stdlib/SDL_strtokr.c", "src/thread/SDL_thread.c", diff --git a/examples/sdl2/third-party/sdl2/build.zig.zon b/examples/sdl2/third-party/sdl2/build.zig.zon index cfe2e9f..7300517 100644 --- a/examples/sdl2/third-party/sdl2/build.zig.zon +++ b/examples/sdl2/third-party/sdl2/build.zig.zon @@ -1,12 +1,12 @@ .{ .name = "sdl", - .version = "0.0.0", + .version = "2.32.10", .dependencies = .{ .sdl2 = .{ // NOTE(jae): 2024-06-30 // Using ".zip" as "tar.gz" fails on Windows for Zig 0.13.0 due to symlink issue with something in the android folders - .url = "https://github.com/libsdl-org/SDL/archive/refs/tags/release-2.32.2.zip", - .hash = "12204a4a9e9f41fc906decd762be78b9e80de65a7bdec428aa0dfdf03f46e7614d9e", + .url = "https://github.com/libsdl-org/SDL/archive/refs/tags/release-2.32.10.zip", + .hash = "N-V-__8AAFcztQQdVKXrgoq8Tf7zye7uFtMaKf1YBm1QYPN3", }, }, .paths = .{ diff --git a/src/android/Logger.zig b/src/android/Logger.zig new file mode 100644 index 0000000..bfcd214 --- /dev/null +++ b/src/android/Logger.zig @@ -0,0 +1,97 @@ +//! Logger is a Writer interface that logs out to Android via "__android_log_write" calls + +const std = @import("std"); +const ndk = @import("ndk"); +const Level = ndk.Level; +const Writer = std.Io.Writer; + +/// Default to the "package" attribute defined in AndroidManifest.xml +/// +/// If tag isn't set when calling "__android_log_write" then it *usually* defaults to the current +/// package name, ie. "com.zig.minimal" +/// +/// However if running via a seperate thread, then it seems to use that threads +/// tag, which means if you log after running code through sdl_main, it won't print +/// logs with the package name. +/// +/// To workaround this, we bake the package name into the Zig binaries. +const package_name: [:0]const u8 = @import("android_builtin").package_name; + +level: Level, +writer: Writer, + +const vtable: Writer.VTable = .{ + .drain = Logger.drain, +}; + +pub fn init(level: Level, buffer: []u8) Logger { + return .{ + .level = level, + .writer = .{ + .buffer = buffer, + .vtable = &vtable, + }, + }; +} + +fn log_each_newline(logger: *Logger, buffer: []const u8) Writer.Error!usize { + var written: usize = 0; + var bytes_to_log = buffer; + while (std.mem.indexOfScalar(u8, bytes_to_log, '\n')) |newline_pos| { + const line = bytes_to_log[0..newline_pos]; + bytes_to_log = bytes_to_log[newline_pos + 1 ..]; + logString(logger.level, line); + written += line.len; + } + if (bytes_to_log.len == 0) return written; + logString(logger.level, bytes_to_log); + written += bytes_to_log.len; + return written; +} + +fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { + const logger: *Logger = @alignCast(@fieldParentPtr("writer", w)); + var written: usize = 0; + + // Consume 'buffer[0..end]' first + written += try logger.log_each_newline(w.buffer[0..w.end]); + w.end = 0; + + // NOTE(jae): 2025-07-27 + // The logic below should probably try to collect the buffers / pattern + // below into one buffer first so that newlines are handled as expected but I'm not willing + // to put the effort in. + + // Write additional overflow data + const slice = data[0 .. data.len - 1]; + for (slice) |bytes| { + written += try logger.log_each_newline(bytes); + } + + // The last element of data is repeated as necessary + const pattern = data[data.len - 1]; + switch (pattern.len) { + 0 => {}, + 1 => { + written += try logger.log_each_newline(pattern); + }, + else => { + for (0..splat) |_| { + written += try logger.log_each_newline(pattern); + } + }, + } + return written; +} + +pub fn logString(android_log_level: Level, text: []const u8) void { + _ = ndk.__android_log_print( + @intFromEnum(android_log_level), + comptime if (package_name.len == 0) null else package_name.ptr, + "%.*s", + text.len, + text.ptr, + ); +} + +const Logger = @This(); diff --git a/src/android/Zig015_Panic.zig b/src/android/Zig015_Panic.zig new file mode 100644 index 0000000..d082a13 --- /dev/null +++ b/src/android/Zig015_Panic.zig @@ -0,0 +1,250 @@ +//! Panic is a copy-paste of the panic logic from Zig but replaces usages of getStdErr with our own writer +//! This is deprecated from Zig 0.16.x-dev onwards due to being buggy and hard to maintain. +//! +//! Example output (Zig 0.13.0): +//! 09-22 13:08:49.578 3390 3390 F com.zig.minimal: thread 3390 panic: your panic message here +//! 09-22 13:08:49.637 3390 3390 F com.zig.minimal: zig-android-sdk/examples\minimal/src/minimal.zig:33:15: 0x7ccb77b282dc in nativeActivityOnCreate (minimal) +//! 09-22 13:08:49.637 3390 3390 F com.zig.minimal: zig-android-sdk/examples/minimal/src/minimal.zig:84:27: 0x7ccb77b28650 in ANativeActivity_onCreate (minimal) +//! 09-22 13:08:49.637 3390 3390 F com.zig.minimal: ???:?:?: 0x7ccea4021d9c in ??? (libandroid_runtime.so) + +const std = @import("std"); +const ndk = @import("ndk"); +const builtin = @import("builtin"); +const Logger = @import("Logger.zig"); + +const Level = ndk.Level; + +const package_name = @import("android_builtin").package_name; + +const LogWriter_Zig014 = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) + @import("zig014").LogWriter +else + void; + +/// Non-zero whenever the program triggered a panic. +/// The counter is incremented/decremented atomically. +var panicking = std.atomic.Value(u8).init(0); + +/// Counts how many times the panic handler is invoked by this thread. +/// This is used to catch and handle panics triggered by the panic handler. +threadlocal var panic_stage: usize = 0; + +pub fn panic(message: []const u8, ret_addr: ?usize) noreturn { + @branchHint(.cold); + if (comptime !builtin.abi.isAndroid()) @compileError("do not use Android panic for non-Android builds"); + + const first_trace_addr = ret_addr orelse @returnAddress(); + panicImpl(first_trace_addr, message); +} + +/// Must be called only after adding 1 to `panicking`. There are three callsites. +fn waitForOtherThreadToFinishPanicking() void { + if (panicking.fetchSub(1, .seq_cst) != 1) { + // Another thread is panicking, wait for the last one to finish + // and call abort() + if (builtin.single_threaded) unreachable; + + // Sleep forever without hammering the CPU + var futex = std.atomic.Value(u32).init(0); + while (true) std.Thread.Futex.wait(&futex, 0); + unreachable; + } +} + +fn resetSegfaultHandler() void { + // NOTE(jae): 2024-09-22 + // Not applicable for Android as it runs on the OS tag Linux + // if (builtin.os.tag == .windows) { + // if (windows_segfault_handle) |handle| { + // assert(windows.kernel32.RemoveVectoredExceptionHandler(handle) != 0); + // windows_segfault_handle = null; + // } + // return; + // } + var act = posix.Sigaction{ + .handler = .{ .handler = posix.SIG.DFL }, + .mask = if (builtin.zig_version.major == 0 and builtin.zig_version.minor == 14) + // Legacy 0.14.0 + posix.empty_sigset + else + // 0.15.0-dev+ + posix.sigemptyset(), + .flags = 0, + }; + std.debug.updateSegfaultHandler(&act); +} + +const io = struct { + /// Collect data in writer buffer and flush to Android logs per newline + var android_log_writer_buffer: [8192]u8 = undefined; + + /// The primary motivation for recursive mutex here is so that a panic while + /// android log writer mutex is held still dumps the stack trace and other debug + /// information. + var android_log_writer_mutex = std.Thread.Mutex.Recursive.init; + + var android_panic_log_writer = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) + LogWriter_Zig014{ .level = .fatal } + else + Logger.init(.fatal, &android_log_writer_buffer); + + fn lockAndroidLogWriter() if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) + LogWriter_Zig014.GenericWriter + else + *std.Io.Writer { + android_log_writer_mutex.lock(); + if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) { + android_panic_log_writer.flush(); + return android_panic_log_writer.writer(); + } else { + android_panic_log_writer.writer.flush() catch {}; + return &android_panic_log_writer.writer; + } + } + + fn unlockAndroidLogWriter() void { + if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) { + android_panic_log_writer.flush(); + } else { + android_panic_log_writer.writer.flush() catch {}; + } + android_log_writer_mutex.unlock(); + } +}; + +const posix = std.posix; + +/// Panic is a copy-paste of the panic logic from Zig but replaces usages of getStdErr with our own writer +/// +/// - Provide custom "io" namespace so we can easily customize getStdErr() to be our own writer +/// - Provide other functions from std.debug.* +fn panicImpl(first_trace_addr: ?usize, msg: []const u8) noreturn { + @branchHint(.cold); + + if (std.options.enable_segfault_handler) { + // If a segfault happens while panicking, we want it to actually segfault, not trigger + // the handler. + resetSegfaultHandler(); + } + + // Note there is similar logic in handleSegfaultPosix and handleSegfaultWindowsExtra. + nosuspend switch (panic_stage) { + 0 => { + panic_stage = 1; + + _ = panicking.fetchAdd(1, .seq_cst); + + // Make sure to release the mutex when done + { + if (builtin.single_threaded) { + _ = ndk.__android_log_print( + @intFromEnum(Level.fatal), + comptime if (package_name.len == 0) null else package_name.ptr, + "panic: %.*s", + msg.len, + msg.ptr, + ); + } else { + const current_thread_id: u32 = std.Thread.getCurrentId(); + _ = ndk.__android_log_print( + @intFromEnum(Level.fatal), + comptime if (package_name.len == 0) null else package_name.ptr, + "thread %d panic: %.*s", + current_thread_id, + msg.len, + msg.ptr, + ); + } + if (@errorReturnTrace()) |t| dumpStackTrace(t.*); + if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) { + dumpCurrentStackTrace_014(first_trace_addr); + } else { + const stderr = io.lockAndroidLogWriter(); + defer io.unlockAndroidLogWriter(); + std.debug.dumpCurrentStackTraceToWriter(first_trace_addr orelse @returnAddress(), stderr) catch {}; + } + } + + waitForOtherThreadToFinishPanicking(); + }, + 1 => { + panic_stage = 2; + + // A panic happened while trying to print a previous panic message, + // we're still holding the mutex but that's fine as we're going to + // call abort() + android_fatal_log("Panicked during a panic. Aborting."); + }, + else => { + // Panicked while printing "Panicked during a panic." + }, + }; + + @trap(); +} + +fn dumpStackTrace(stack_trace: std.builtin.StackTrace) void { + nosuspend { + if (comptime builtin.target.cpu.arch.isWasm()) { + @compileError("cannot use Android logger with Wasm"); + } + if (builtin.strip_debug_info) { + android_fatal_log("Unable to dump stack trace: debug info stripped"); + } + const debug_info = std.debug.getSelfDebugInfo() catch |err| { + android_fatal_print_c_string("Unable to dump stack trace: Unable to open debug info: %s", @errorName(err)); + return; + }; + const stderr = io.lockAndroidLogWriter(); + defer io.unlockAndroidLogWriter(); + std.debug.writeStackTrace(stack_trace, stderr, debug_info, .no_color) catch |err| { + android_fatal_print_c_string("Unable to dump stack trace: %s", @errorName(err)); + return; + }; + } +} + +/// Deprecated: Only used for current Zig 0.14.1 stable builds, +fn dumpCurrentStackTrace_014(start_addr: ?usize) void { + nosuspend { + if (comptime builtin.target.cpu.arch.isWasm()) { + @compileError("cannot use Android logger with Wasm"); + } + if (builtin.strip_debug_info) { + android_fatal_log("Unable to dump stack trace: debug info stripped"); + return; + } + const debug_info = std.debug.getSelfDebugInfo() catch |err| { + android_fatal_print_c_string("Unable to dump stack trace: Unable to open debug info: %s", @errorName(err)); + return; + }; + const stderr = io.lockAndroidLogWriter(); + defer io.unlockAndroidLogWriter(); + std.debug.writeCurrentStackTrace(stderr, debug_info, .no_color, start_addr) catch |err| { + android_fatal_print_c_string("Unable to dump stack trace: %s", @errorName(err)); + return; + }; + } +} + +fn android_fatal_log(message: [:0]const u8) void { + _ = ndk.__android_log_write( + @intFromEnum(Level.fatal), + comptime if (package_name.len == 0) null else package_name.ptr, + message, + ); +} + +fn android_fatal_print_c_string( + comptime fmt: [:0]const u8, + c_str: [:0]const u8, +) void { + _ = ndk.__android_log_print( + @intFromEnum(Level.fatal), + comptime if (package_name.len == 0) null else package_name.ptr, + fmt, + c_str.ptr, + ); +} + +const Panic = @This(); diff --git a/src/android/android.zig b/src/android/android.zig index 2f19ed2..89445f8 100644 --- a/src/android/android.zig +++ b/src/android/android.zig @@ -1,524 +1,65 @@ const std = @import("std"); const builtin = @import("builtin"); -// TODO(jae): 2024-10-03 -// Consider exposing this in the future -// pub const builtin = android_builtin; +const ndk = @import("ndk"); +const zig014 = @import("zig014"); +const zig015 = @import("zig015"); +const zig016 = @import("zig016"); -const android_builtin = struct { - const ab = @import("android_builtin"); - - /// package name extracted from your AndroidManifest.xml file - /// ie. "com.zig.sdl2" - pub const package_name: [:0]const u8 = ab.package_name; -}; - -/// Default to the "package" attribute defined in AndroidManifest.xml -/// -/// If tag isn't set when calling "__android_log_write" then it *usually* defaults to the current -/// package name, ie. "com.zig.minimal" -/// -/// However if running via a seperate thread, then it seems to use that threads -/// tag, which means if you log after running code through sdl_main, it won't print -/// logs with the package name. -/// -/// To workaround this, we bake the package name into the Zig binaries. -const log_tag: [:0]const u8 = android_builtin.package_name; - -/// Writes the constant string text to the log, with priority prio and tag tag. -/// Returns: 1 if the message was written to the log, or -EPERM if it was not; see __android_log_is_loggable(). -/// Source: https://developer.android.com/ndk/reference/group/logging -extern "log" fn __android_log_write(prio: c_int, tag: [*c]const u8, text: [*c]const u8) c_int; - -/// Writes a formatted string to the log, with priority prio and tag tag. -/// The details of formatting are the same as for printf(3) -/// Returns: 1 if the message was written to the log, or -EPERM if it was not; see __android_log_is_loggable(). -/// Source: https://man7.org/linux/man-pages/man3/printf.3.html -extern "log" fn __android_log_print(prio: c_int, tag: [*c]const u8, text: [*c]const u8, ...) c_int; +const Logger = @import("Logger.zig"); +const Level = ndk.Level; /// Alternate panic implementation that calls __android_log_write so that you can see the logging via "adb logcat" -pub const panic = std.debug.FullPanic(Panic.panic); - -/// Levels for Android -pub const Level = enum(u8) { - // silent = 8, // Android docs: For internal use only. - // Fatal: Android only, for use when aborting - fatal = 7, // ANDROID_LOG_FATAL - /// Error: something has gone wrong. This might be recoverable or might - /// be followed by the program exiting. - err = 6, // ANDROID_LOG_ERROR - /// Warning: it is uncertain if something has gone wrong or not, but the - /// circumstances would be worth investigating. - warn = 5, // ANDROID_LOG_WARN - /// Info: general messages about the state of the program. - info = 4, // ANDROID_LOG_INFO - /// Debug: messages only useful for debugging. - debug = 3, // ANDROID_LOG_DEBUG - // verbose = 2, // ANDROID_LOG_VERBOSE - // default = 1, // ANDROID_LOG_DEFAULT - - // Returns a string literal of the given level in full text form. - // pub fn asText(comptime self: Level) []const u8 { - // return switch (self) { - // .err => "error", - // .warn => "warning", - // .info => "info", - // .debug => "debug", - // }; - // } -}; +pub const panic = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + std.debug.FullPanic(@import("Zig015_Panic.zig").panic) +else + @compileError("Android panic handler is no longer maintained as of Zig 0.16.x-dev"); /// Alternate log function implementation that calls __android_log_write so that you can see the logging via "adb logcat" pub const logFn = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) - LogWriter_Zig014.logFn + zig014.LogWriter.logFn +else if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + zig015.wrapLogFn(androidLogFn) else - AndroidLog.logFn; - -/// LogWriter_Zig014 was was taken basically as is from: https://github.com/ikskuh/ZigAndroidTemplate -/// -/// Deprecated: To be removed when Zig 0.15.x is stable -const LogWriter_Zig014 = struct { - level: Level, - - line_buffer: [8192]u8 = undefined, - line_len: usize = 0, - - const Error = error{}; - const Writer = std.io.Writer(*@This(), Error, write); - - fn logFn( - comptime message_level: std.log.Level, - comptime scope: @Type(.enum_literal), - comptime format: []const u8, - args: anytype, - ) void { - // NOTE(jae): 2024-09-11 - // Zig has a colon ": " or "): " for scoped but Android logs just do that after being flushed - // So we don't do that here. - const prefix2 = if (scope == .default) "" else "(" ++ @tagName(scope) ++ ")"; // "): "; - var androidLogWriter = comptime @This(){ - .level = switch (message_level) { - // => .ANDROID_LOG_VERBOSE, // No mapping - .debug => .debug, // android.ANDROID_LOG_DEBUG = 3, - .info => .info, // android.ANDROID_LOG_INFO = 4, - .warn => .warn, // android.ANDROID_LOG_WARN = 5, - .err => .err, // android.ANDROID_LOG_WARN = 6, - }, - }; - const logger = androidLogWriter.writer(); - - nosuspend { - logger.print(prefix2 ++ format ++ "\n", args) catch return; - androidLogWriter.flush(); - } - } - - fn write(self: *@This(), buffer: []const u8) Error!usize { - for (buffer) |char| { - switch (char) { - '\n' => { - self.flush(); - }, - else => { - if (self.line_len >= self.line_buffer.len - 1) { - self.flush(); - } - self.line_buffer[self.line_len] = char; - self.line_len += 1; - }, - } - } - return buffer.len; - } - - fn flush(self: *@This()) void { - if (self.line_len > 0) { - std.debug.assert(self.line_len < self.line_buffer.len - 1); - self.line_buffer[self.line_len] = 0; - if (log_tag.len == 0) { - _ = __android_log_write( - @intFromEnum(self.level), - null, - &self.line_buffer, - ); - } else { - _ = __android_log_write( - @intFromEnum(self.level), - log_tag.ptr, - &self.line_buffer, - ); - } - } - self.line_len = 0; - } - - fn writer(self: *@This()) Writer { - return Writer{ .context = self }; - } -}; - -/// AndroidLog is a Writer interface that logs out to Android via "__android_log_write" calls -const AndroidLog = struct { - level: Level, - writer: std.Io.Writer, - - const vtable: std.Io.Writer.VTable = .{ - .drain = @This().drain, - }; - - fn init(level: Level, buffer: []u8) AndroidLog { - return .{ - .level = level, - .writer = .{ - .buffer = buffer, - .vtable = &vtable, - }, - }; - } - - fn log_each_newline(logger: *AndroidLog, buffer: []const u8) std.Io.Writer.Error!usize { - var written: usize = 0; - var bytes_to_log = buffer; - while (std.mem.indexOfScalar(u8, bytes_to_log, '\n')) |newline_pos| { - const line = bytes_to_log[0..newline_pos]; - bytes_to_log = bytes_to_log[newline_pos + 1 ..]; - android_log_string(logger.level, line); - written += line.len; - } - if (bytes_to_log.len == 0) return written; - android_log_string(logger.level, bytes_to_log); - written += bytes_to_log.len; - return written; - } - - fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { - const logger: *AndroidLog = @alignCast(@fieldParentPtr("writer", w)); - var written: usize = 0; - - // Consume 'buffer[0..end]' first - written += try logger.log_each_newline(w.buffer[0..w.end]); - w.end = 0; - - // NOTE(jae): 2025-07-27 - // The logic below should probably try to collect the buffers / pattern - // below into one buffer first so that newlines are handled as expected but I'm not willing - // to put the effort in. - - // Write additional overflow data - const slice = data[0 .. data.len - 1]; - for (slice) |bytes| { - written += try logger.log_each_newline(bytes); - } - - // The last element of data is repeated as necessary - const pattern = data[data.len - 1]; - switch (pattern.len) { - 0 => {}, - 1 => { - written += try logger.log_each_newline(pattern); - }, - else => { - for (0..splat) |_| { - written += try logger.log_each_newline(pattern); - } - }, - } - return written; - } - - fn logFn( - comptime message_level: std.log.Level, - comptime scope: @Type(.enum_literal), - comptime format: []const u8, - args: anytype, - ) void { - // If there are no arguments or '{}' patterns in the logging, just call Android log directly - const ArgsType = @TypeOf(args); - const args_type_info = @typeInfo(ArgsType); - if (args_type_info != .@"struct") { - @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType)); - } - const fields_info = args_type_info.@"struct".fields; - if (fields_info.len == 0 and - comptime std.mem.indexOfScalar(u8, format, '{') == null) - { - _ = __android_log_print( - @intFromEnum(Level.fatal), - comptime if (log_tag.len == 0) null else log_tag.ptr, - "%.*s", - format.len, - format.ptr, - ); - return; - } - - // NOTE(jae): 2024-09-11 - // Zig has a colon ": " or "): " for scoped but Android logs just do that after being flushed - // So we don't do that here. - const prefix2 = if (scope == .default) "" else "(" ++ @tagName(scope) ++ ")"; // "): "; - - const android_log_level: Level = switch (message_level) { - // => .ANDROID_LOG_VERBOSE, // No mapping - .debug => .debug, // android.ANDROID_LOG_DEBUG = 3, - .info => .info, // android.ANDROID_LOG_INFO = 4, - .warn => .warn, // android.ANDROID_LOG_WARN = 5, - .err => .err, // android.ANDROID_LOG_WARN = 6, - }; - var buffer: [8192]u8 = undefined; - var logger = AndroidLog.init(android_log_level, &buffer); - - nosuspend { - logger.writer.print(prefix2 ++ format ++ "\n", args) catch return; - logger.writer.flush() catch return; - } - } -}; - -/// Panic is a copy-paste of the panic logic from Zig but replaces usages of getStdErr with our own writer -/// -/// Example output (Zig 0.13.0): -/// 09-22 13:08:49.578 3390 3390 F com.zig.minimal: thread 3390 panic: your panic message here -/// 09-22 13:08:49.637 3390 3390 F com.zig.minimal: zig-android-sdk/examples\minimal/src/minimal.zig:33:15: 0x7ccb77b282dc in nativeActivityOnCreate (minimal) -/// 09-22 13:08:49.637 3390 3390 F com.zig.minimal: zig-android-sdk/examples/minimal/src/minimal.zig:84:27: 0x7ccb77b28650 in ANativeActivity_onCreate (minimal) -/// 09-22 13:08:49.637 3390 3390 F com.zig.minimal: ???:?:?: 0x7ccea4021d9c in ??? (libandroid_runtime.so) -const Panic = struct { - /// Non-zero whenever the program triggered a panic. - /// The counter is incremented/decremented atomically. - var panicking = std.atomic.Value(u8).init(0); - - /// Counts how many times the panic handler is invoked by this thread. - /// This is used to catch and handle panics triggered by the panic handler. - threadlocal var panic_stage: usize = 0; - - const is_zig_014_or_less = (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14); - - fn panic(message: []const u8, ret_addr: ?usize) noreturn { - @branchHint(.cold); - if (comptime !builtin.abi.isAndroid()) @compileError("do not use Android panic for non-Android builds"); - // if (!is_zig_014_or_less) @compileError("Android Panic needs to be updated to the newer io.Writer vtable implementation to work in Zig 0.15.0+"); - const first_trace_addr = ret_addr orelse @returnAddress(); - panicImpl(first_trace_addr, message); - } - - /// Must be called only after adding 1 to `panicking`. There are three callsites. - fn waitForOtherThreadToFinishPanicking() void { - if (panicking.fetchSub(1, .seq_cst) != 1) { - // Another thread is panicking, wait for the last one to finish - // and call abort() - if (builtin.single_threaded) unreachable; - - // Sleep forever without hammering the CPU - var futex = std.atomic.Value(u32).init(0); - while (true) std.Thread.Futex.wait(&futex, 0); - unreachable; - } - } - - fn resetSegfaultHandler() void { - // NOTE(jae): 2024-09-22 - // Not applicable for Android as it runs on the OS tag Linux - // if (builtin.os.tag == .windows) { - // if (windows_segfault_handle) |handle| { - // assert(windows.kernel32.RemoveVectoredExceptionHandler(handle) != 0); - // windows_segfault_handle = null; - // } - // return; - // } - var act = posix.Sigaction{ - .handler = .{ .handler = posix.SIG.DFL }, - .mask = if (builtin.zig_version.major == 0 and builtin.zig_version.minor == 14) - // Legacy 0.14.0 - posix.empty_sigset - else - // 0.15.0-dev+ - posix.sigemptyset(), - .flags = 0, - }; - std.debug.updateSegfaultHandler(&act); + zig016.wrapLogFn(androidLogFn); + +fn androidLogFn( + comptime message_level: std.log.Level, + // NOTE(jae): 2026-01-10 + // Just make our log function here use the precomputed text and get the Zig 0.15.2 and Zig 0.16.x-dev+ + // implementation to pass in the following: + // - const scope_prefix_text = if (scope == .default) "" else "(" ++ @tagName(scope) ++ ")"; // "): "; + comptime scope_prefix_text: [:0]const u8, + comptime format: []const u8, + args: anytype, +) void { + // If there are no arguments or '{}' patterns in the logging, just call Android log directly + const ArgsType = @TypeOf(args); + const args_type_info = @typeInfo(ArgsType); + if (args_type_info != .@"struct") { + @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType)); } - const io = struct { - /// Collect data in writer buffer and flush to Android logs per newline - var android_log_writer_buffer: [8192]u8 = undefined; - - /// The primary motivation for recursive mutex here is so that a panic while - /// android log writer mutex is held still dumps the stack trace and other debug - /// information. - var android_log_writer_mutex = std.Thread.Mutex.Recursive.init; - - var android_panic_log_writer = if (is_zig_014_or_less) - LogWriter_Zig014{ - .level = .fatal, - } - else - AndroidLog.init(.fatal, &android_log_writer_buffer); - - fn lockAndroidLogWriter() if (is_zig_014_or_less) - std.io.GenericWriter(*LogWriter_Zig014, LogWriter_Zig014.Error, LogWriter_Zig014.write) - else - *std.Io.Writer { - android_log_writer_mutex.lock(); - if (is_zig_014_or_less) { - android_panic_log_writer.flush(); - return android_panic_log_writer.writer(); - } else { - android_panic_log_writer.writer.flush() catch {}; - return &android_panic_log_writer.writer; - } - } - - fn unlockAndroidLogWriter() void { - if (is_zig_014_or_less) { - android_panic_log_writer.flush(); - } else { - android_panic_log_writer.writer.flush() catch {}; - } - android_log_writer_mutex.unlock(); - } + const android_log_level: Level = switch (message_level) { + // => .ANDROID_LOG_VERBOSE, // No mapping + .debug => .debug, // android.ANDROID_LOG_DEBUG = 3, + .info => .info, // android.ANDROID_LOG_INFO = 4, + .warn => .warn, // android.ANDROID_LOG_WARN = 5, + .err => .err, // android.ANDROID_LOG_WARN = 6, }; - const posix = std.posix; - - /// Panic is a copy-paste of the panic logic from Zig but replaces usages of getStdErr with our own writer - /// - /// - Provide custom "io" namespace so we can easily customize getStdErr() to be our own writer - /// - Provide other functions from std.debug.* - fn panicImpl(first_trace_addr: ?usize, msg: []const u8) noreturn { - @branchHint(.cold); - - if (std.options.enable_segfault_handler) { - // If a segfault happens while panicking, we want it to actually segfault, not trigger - // the handler. - resetSegfaultHandler(); - } - - // Note there is similar logic in handleSegfaultPosix and handleSegfaultWindowsExtra. - nosuspend switch (panic_stage) { - 0 => { - panic_stage = 1; - - _ = panicking.fetchAdd(1, .seq_cst); - - // Make sure to release the mutex when done - { - if (builtin.single_threaded) { - _ = __android_log_print( - @intFromEnum(Level.fatal), - comptime if (log_tag.len == 0) null else log_tag.ptr, - "panic: %.*s", - msg.len, - msg.ptr, - ); - } else { - const current_thread_id: u32 = std.Thread.getCurrentId(); - _ = __android_log_print( - @intFromEnum(Level.fatal), - comptime if (log_tag.len == 0) null else log_tag.ptr, - "thread %d panic: %.*s", - current_thread_id, - msg.len, - msg.ptr, - ); - } - if (@errorReturnTrace()) |t| dumpStackTrace(t.*); - if (is_zig_014_or_less) { - dumpCurrentStackTrace_014(first_trace_addr); - } else { - const stderr = io.lockAndroidLogWriter(); - defer io.unlockAndroidLogWriter(); - std.debug.dumpCurrentStackTraceToWriter(first_trace_addr orelse @returnAddress(), stderr) catch {}; - } - } - - waitForOtherThreadToFinishPanicking(); - }, - 1 => { - panic_stage = 2; - - // A panic happened while trying to print a previous panic message, - // we're still holding the mutex but that's fine as we're going to - // call abort() - android_fatal_log("Panicked during a panic. Aborting."); - }, - else => { - // Panicked while printing "Panicked during a panic." - }, - }; - - posix.abort(); + const fields_info = args_type_info.@"struct".fields; + if (fields_info.len == 0 and + comptime std.mem.indexOfScalar(u8, format, '{') == null) + { + // If no formatting, log string directly with Android logging + _ = Logger.logString(android_log_level, format); + return; } - - fn dumpStackTrace(stack_trace: std.builtin.StackTrace) void { - nosuspend { - if (comptime builtin.target.cpu.arch.isWasm()) { - @compileError("cannot use Android logger with Wasm"); - } - if (builtin.strip_debug_info) { - android_fatal_log("Unable to dump stack trace: debug info stripped"); - } - const debug_info = std.debug.getSelfDebugInfo() catch |err| { - android_fatal_print_c_string("Unable to dump stack trace: Unable to open debug info: %s", @errorName(err)); - return; - }; - const stderr = io.lockAndroidLogWriter(); - defer io.unlockAndroidLogWriter(); - std.debug.writeStackTrace(stack_trace, stderr, debug_info, .no_color) catch |err| { - android_fatal_print_c_string("Unable to dump stack trace: %s", @errorName(err)); - return; - }; - } + var buffer: [8192]u8 = undefined; + var logger = Logger.init(android_log_level, &buffer); + nosuspend { + logger.writer.print(scope_prefix_text ++ format ++ "\n", args) catch return; + logger.writer.flush() catch return; } - - /// Deprecated: Only used for current Zig 0.14.1 stable builds, - fn dumpCurrentStackTrace_014(start_addr: ?usize) void { - nosuspend { - if (comptime builtin.target.cpu.arch.isWasm()) { - @compileError("cannot use Android logger with Wasm"); - } - if (builtin.strip_debug_info) { - android_fatal_log("Unable to dump stack trace: debug info stripped"); - return; - } - const debug_info = std.debug.getSelfDebugInfo() catch |err| { - android_fatal_print_c_string("Unable to dump stack trace: Unable to open debug info: %s", @errorName(err)); - return; - }; - const stderr = io.lockAndroidLogWriter(); - defer io.unlockAndroidLogWriter(); - std.debug.writeCurrentStackTrace(stderr, debug_info, .no_color, start_addr) catch |err| { - android_fatal_print_c_string("Unable to dump stack trace: %s", @errorName(err)); - return; - }; - } - } -}; - -fn android_fatal_log(message: [:0]const u8) void { - _ = __android_log_write( - @intFromEnum(Level.fatal), - comptime if (log_tag.len == 0) null else log_tag.ptr, - message, - ); -} - -fn android_fatal_print_c_string( - comptime fmt: [:0]const u8, - c_str: [:0]const u8, -) void { - _ = __android_log_print( - @intFromEnum(Level.fatal), - comptime if (log_tag.len == 0) null else log_tag.ptr, - fmt, - c_str.ptr, - ); -} - -fn android_log_string(android_log_level: Level, text: []const u8) void { - _ = __android_log_print( - @intFromEnum(android_log_level), - comptime if (log_tag.len == 0) null else log_tag.ptr, - "%.*s", - text.len, - text.ptr, - ); } diff --git a/src/android/ndk/ndk.zig b/src/android/ndk/ndk.zig new file mode 100644 index 0000000..fe6b715 --- /dev/null +++ b/src/android/ndk/ndk.zig @@ -0,0 +1,41 @@ +//! NDK functions as defined at: https://developer.android.com/ndk/reference/group/logging + +/// Writes the constant string text to the log, with priority prio and tag tag. +/// Returns: 1 if the message was written to the log, or -EPERM if it was not; see __android_log_is_loggable(). +/// Source: https://developer.android.com/ndk/reference/group/logging +pub extern "log" fn __android_log_write(prio: c_int, tag: [*c]const u8, text: [*c]const u8) c_int; + +/// Writes a formatted string to the log, with priority prio and tag tag. +/// The details of formatting are the same as for printf(3) +/// Returns: 1 if the message was written to the log, or -EPERM if it was not; see __android_log_is_loggable(). +/// Source: https://man7.org/linux/man-pages/man3/printf.3.html +pub extern "log" fn __android_log_print(prio: c_int, tag: [*c]const u8, text: [*c]const u8, ...) c_int; + +/// Log Levels for Android +pub const Level = enum(u8) { + // silent = 8, // Android docs: For internal use only. + // Fatal: Android only, for use when aborting + fatal = 7, // ANDROID_LOG_FATAL + /// Error: something has gone wrong. This might be recoverable or might + /// be followed by the program exiting. + err = 6, // ANDROID_LOG_ERROR + /// Warning: it is uncertain if something has gone wrong or not, but the + /// circumstances would be worth investigating. + warn = 5, // ANDROID_LOG_WARN + /// Info: general messages about the state of the program. + info = 4, // ANDROID_LOG_INFO + /// Debug: messages only useful for debugging. + debug = 3, // ANDROID_LOG_DEBUG + // verbose = 2, // ANDROID_LOG_VERBOSE + // default = 1, // ANDROID_LOG_DEFAULT + + // Returns a string literal of the given level in full text form. + // pub fn asText(comptime self: Level) []const u8 { + // return switch (self) { + // .err => "error", + // .warn => "warning", + // .info => "info", + // .debug => "debug", + // }; + // } +}; diff --git a/src/android/zig014/LogWriter_Zig014.zig b/src/android/zig014/LogWriter_Zig014.zig new file mode 100644 index 0000000..80fd6f6 --- /dev/null +++ b/src/android/zig014/LogWriter_Zig014.zig @@ -0,0 +1,91 @@ +//! LogWriter_Zig014 was was taken basically as is from: https://github.com/ikskuh/ZigAndroidTemplate +//! +//! Deprecated: To be removed when Zig 0.15.x is stable + +const std = @import("std"); +const builtin = @import("builtin"); +const ndk = @import("ndk"); +const Level = ndk.Level; + +level: Level, + +line_buffer: [8192]u8 = undefined, +line_len: usize = 0, + +const Error = error{}; +pub const GenericWriter = std.io.GenericWriter(*LogWriter_Zig014, LogWriter_Zig014.Error, LogWriter_Zig014.write); +const Writer = std.io.Writer(*LogWriter_Zig014, Error, write); + +const log_tag: [:0]const u8 = @import("android_builtin").package_name; + +pub fn logFn( + comptime message_level: std.log.Level, + comptime scope: @Type(.enum_literal), + comptime format: []const u8, + args: anytype, +) void { + // NOTE(jae): 2024-09-11 + // Zig has a colon ": " or "): " for scoped but Android logs just do that after being flushed + // So we don't do that here. + const prefix2 = if (scope == .default) "" else "(" ++ @tagName(scope) ++ ")"; // "): "; + var androidLogWriter = comptime LogWriter_Zig014{ + .level = switch (message_level) { + // => .ANDROID_LOG_VERBOSE, // No mapping + .debug => .debug, // android.ANDROID_LOG_DEBUG = 3, + .info => .info, // android.ANDROID_LOG_INFO = 4, + .warn => .warn, // android.ANDROID_LOG_WARN = 5, + .err => .err, // android.ANDROID_LOG_WARN = 6, + }, + }; + const logger = androidLogWriter.writer(); + + nosuspend { + logger.print(prefix2 ++ format ++ "\n", args) catch return; + androidLogWriter.flush(); + } +} + +fn write(self: *@This(), buffer: []const u8) Error!usize { + for (buffer) |char| { + switch (char) { + '\n' => { + self.flush(); + }, + else => { + if (self.line_len >= self.line_buffer.len - 1) { + self.flush(); + } + self.line_buffer[self.line_len] = char; + self.line_len += 1; + }, + } + } + return buffer.len; +} + +pub fn flush(self: *@This()) void { + if (self.line_len > 0) { + std.debug.assert(self.line_len < self.line_buffer.len - 1); + self.line_buffer[self.line_len] = 0; + if (log_tag.len == 0) { + _ = ndk.__android_log_write( + @intFromEnum(self.level), + null, + &self.line_buffer, + ); + } else { + _ = ndk.__android_log_write( + @intFromEnum(self.level), + log_tag.ptr, + &self.line_buffer, + ); + } + } + self.line_len = 0; +} + +pub fn writer(self: *@This()) Writer { + return Writer{ .context = self }; +} + +const LogWriter_Zig014 = @This(); diff --git a/src/android/zig014/zig014.zig b/src/android/zig014/zig014.zig new file mode 100644 index 0000000..9c0ca5a --- /dev/null +++ b/src/android/zig014/zig014.zig @@ -0,0 +1,3 @@ +//! Seperate module for Zig 0.14.X functionality as @Type() comptime directive was removed + +pub const LogWriter = @import("LogWriter_Zig014.zig"); diff --git a/src/android/zig015/zig015.zig b/src/android/zig015/zig015.zig new file mode 100644 index 0000000..acfebc2 --- /dev/null +++ b/src/android/zig015/zig015.zig @@ -0,0 +1,22 @@ +//! Seperate module for Zig 0.15.2 functionality as @Type() comptime directive was removed + +const std = @import("std"); + +const LogFunction = fn (comptime message_level: std.log.Level, comptime scope: @Type(.enum_literal), comptime format: []const u8, args: anytype) void; + +pub fn wrapLogFn(comptime logFn: fn ( + comptime message_level: std.log.Level, + comptime scope_prefix_text: [:0]const u8, + comptime format: []const u8, + args: anytype, +) void) LogFunction { + return struct { + fn standardLogFn(comptime message_level: std.log.Level, comptime scope: @Type(.enum_literal), comptime format: []const u8, args: anytype) void { + // NOTE(jae): 2024-09-11 + // Zig has a colon ": " or "): " for scoped but Android logs just do that after being flushed + // So we don't do that here. + const scope_prefix_text = if (scope == .default) "" else "(" ++ @tagName(scope) ++ ")"; // "): "; + return logFn(message_level, scope_prefix_text, format, args); + } + }.standardLogFn; +} diff --git a/src/android/zig016/zig016.zig b/src/android/zig016/zig016.zig new file mode 100644 index 0000000..413a6e4 --- /dev/null +++ b/src/android/zig016/zig016.zig @@ -0,0 +1,22 @@ +//! Seperate module for Zig 0.16.X-dev functionality as @Type() comptime directive was removed + +const std = @import("std"); + +const LogFunction = fn (comptime message_level: std.log.Level, comptime scope: @EnumLiteral(), comptime format: []const u8, args: anytype) void; + +pub fn wrapLogFn(comptime logFn: fn ( + comptime message_level: std.log.Level, + comptime scope_prefix_text: [:0]const u8, + comptime format: []const u8, + args: anytype, +) void) LogFunction { + return struct { + fn standardLogFn(comptime message_level: std.log.Level, comptime scope: @EnumLiteral(), comptime format: []const u8, args: anytype) void { + // NOTE(jae): 2024-09-11 + // Zig has a colon ": " or "): " for scoped but Android logs just do that after being flushed + // So we don't do that here. + const scope_prefix_text = if (scope == .default) "" else "(" ++ @tagName(scope) ++ ")"; // "): "; + return logFn(message_level, scope_prefix_text, format, args); + } + }.standardLogFn; +} diff --git a/src/androidbuild/BuildTools.zig b/src/androidbuild/BuildTools.zig index d06af27..b7df126 100644 --- a/src/androidbuild/BuildTools.zig +++ b/src/androidbuild/BuildTools.zig @@ -2,6 +2,7 @@ //! - $ANDROID_HOME/build-tools/35.0.0 const std = @import("std"); +const builtin = @import("builtin"); const Allocator = std.mem.Allocator; aapt2: []const u8, @@ -33,7 +34,11 @@ pub fn init(b: *std.Build, android_sdk_path: []const u8, build_tools_version: [] // 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) { + const access_wrapped_error = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + std.fs.accessAbsolute(build_tools_path, .{}) + else + std.Io.Dir.accessAbsolute(b.graph.io, build_tools_path, .{}); + access_wrapped_error 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, diff --git a/src/androidbuild/Ndk.zig b/src/androidbuild/Ndk.zig index e609a33..e7b2b4b 100644 --- a/src/androidbuild/Ndk.zig +++ b/src/androidbuild/Ndk.zig @@ -2,6 +2,7 @@ //! - $ANDROID_HOME/ndk/29.0.13113456 const std = @import("std"); +const builtin = @import("builtin"); const androidbuild = @import("androidbuild.zig"); const Allocator = std.mem.Allocator; @@ -34,7 +35,11 @@ pub fn init(b: *std.Build, android_sdk_path: []const u8, ndk_version: []const u8 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) { + const access_wrapped_error = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + std.fs.accessAbsolute(android_ndk_path, .{}) + else + std.Io.Dir.accessAbsolute(b.graph.io, android_ndk_path, .{}); + access_wrapped_error 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, @@ -76,7 +81,11 @@ pub fn init(b: *std.Build, android_sdk_path: []const u8, ndk_version: []const u8 // Check if NDK sysroot path is accessible const has_ndk_sysroot = blk: { - std.fs.accessAbsolute(ndk_sysroot, .{}) catch |err| switch (err) { + const access_wrapped_error = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + std.fs.accessAbsolute(ndk_sysroot, .{}) + else + std.Io.Dir.accessAbsolute(b.graph.io, ndk_sysroot, .{}); + access_wrapped_error catch |err| switch (err) { error.FileNotFound => { const message = b.fmt("Android NDK sysroot '{s}' had unexpected error. Missing at '{s}'", .{ ndk_version, @@ -121,7 +130,12 @@ pub fn validateApiLevel(ndk: *const Ndk, b: *std.Build, api_level: ApiLevel, err // "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) { + + const access_wrapped_error = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + std.fs.accessAbsolute(ndk_sysroot_target_api_version, .{}) + else + std.Io.Dir.accessAbsolute(b.graph.io, ndk_sysroot_target_api_version, .{}); + access_wrapped_error 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, @@ -154,7 +168,11 @@ pub fn validateApiLevel(ndk: *const Ndk, b: *std.Build, api_level: ApiLevel, err b.fmt("android-{d}", .{@intFromEnum(api_level)}), "android.jar", }); - std.fs.accessAbsolute(root_jar, .{}) catch |err| switch (err) { + const access_wrapped_error = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + std.fs.accessAbsolute(root_jar, .{}) + else + std.Io.Dir.accessAbsolute(b.graph.io, root_jar, .{}); + access_wrapped_error catch |err| switch (err) { error.FileNotFound => { const message = b.fmt("Android API level {d} not installed. Unable to find '{s}'", .{ @intFromEnum(api_level), diff --git a/src/androidbuild/WindowsSdk.zig b/src/androidbuild/WindowsSdk.zig deleted file mode 100644 index 253ca67..0000000 --- a/src/androidbuild/WindowsSdk.zig +++ /dev/null @@ -1,273 +0,0 @@ -//! 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"); - -const windows = std.os.windows; -const RRF = windows.advapi32.RRF; - -const OpenOptions = struct { - /// Sets the KEY_WOW64_32KEY access flag. - /// https://learn.microsoft.com/en-us/windows/win32/winprog64/accessing-an-alternate-registry-view - wow64_32: bool = false, -}; - -pub const RegistryWtf8 = struct { - key: windows.HKEY, - - /// Assert that `key` is valid WTF-8 string - pub fn openKey(hkey: windows.HKEY, key: []const u8, options: OpenOptions) error{KeyNotFound}!RegistryWtf8 { - const key_wtf16le: [:0]const u16 = key_wtf16le: { - var key_wtf16le_buf: [RegistryWtf16Le.key_name_max_len]u16 = undefined; - const key_wtf16le_len: usize = std.unicode.wtf8ToWtf16Le(key_wtf16le_buf[0..], key) catch |err| switch (err) { - error.InvalidWtf8 => unreachable, - }; - key_wtf16le_buf[key_wtf16le_len] = 0; - break :key_wtf16le key_wtf16le_buf[0..key_wtf16le_len :0]; - }; - - const registry_wtf16le = try RegistryWtf16Le.openKey(hkey, key_wtf16le, options); - return .{ .key = registry_wtf16le.key }; - } - - /// Closes key, after that usage is invalid - pub fn closeKey(reg: RegistryWtf8) void { - const return_code_int: windows.HRESULT = windows.advapi32.RegCloseKey(reg.key); - const return_code: windows.Win32Error = @enumFromInt(return_code_int); - switch (return_code) { - .SUCCESS => {}, - else => {}, - } - } - - /// Get string from registry. - /// Caller owns result. - pub fn getString(reg: RegistryWtf8, allocator: std.mem.Allocator, subkey: []const u8, value_name: []const u8) error{ OutOfMemory, ValueNameNotFound, NotAString, StringNotFound }![]u8 { - const subkey_wtf16le: [:0]const u16 = subkey_wtf16le: { - var subkey_wtf16le_buf: [RegistryWtf16Le.key_name_max_len]u16 = undefined; - const subkey_wtf16le_len: usize = std.unicode.wtf8ToWtf16Le(subkey_wtf16le_buf[0..], subkey) catch unreachable; - subkey_wtf16le_buf[subkey_wtf16le_len] = 0; - break :subkey_wtf16le subkey_wtf16le_buf[0..subkey_wtf16le_len :0]; - }; - - const value_name_wtf16le: [:0]const u16 = value_name_wtf16le: { - var value_name_wtf16le_buf: [RegistryWtf16Le.value_name_max_len]u16 = undefined; - const value_name_wtf16le_len: usize = std.unicode.wtf8ToWtf16Le(value_name_wtf16le_buf[0..], value_name) catch unreachable; - value_name_wtf16le_buf[value_name_wtf16le_len] = 0; - break :value_name_wtf16le value_name_wtf16le_buf[0..value_name_wtf16le_len :0]; - }; - - const registry_wtf16le: RegistryWtf16Le = .{ .key = reg.key }; - const value_wtf16le = try registry_wtf16le.getString(allocator, subkey_wtf16le, value_name_wtf16le); - defer allocator.free(value_wtf16le); - - const value_wtf8: []u8 = try std.unicode.wtf16LeToWtf8Alloc(allocator, value_wtf16le); - errdefer allocator.free(value_wtf8); - - return value_wtf8; - } - - /// Get DWORD (u32) from registry. - pub fn getDword(reg: RegistryWtf8, subkey: []const u8, value_name: []const u8) error{ ValueNameNotFound, NotADword, DwordTooLong, DwordNotFound }!u32 { - const subkey_wtf16le: [:0]const u16 = subkey_wtf16le: { - var subkey_wtf16le_buf: [RegistryWtf16Le.key_name_max_len]u16 = undefined; - const subkey_wtf16le_len: usize = std.unicode.wtf8ToWtf16Le(subkey_wtf16le_buf[0..], subkey) catch unreachable; - subkey_wtf16le_buf[subkey_wtf16le_len] = 0; - break :subkey_wtf16le subkey_wtf16le_buf[0..subkey_wtf16le_len :0]; - }; - - const value_name_wtf16le: [:0]const u16 = value_name_wtf16le: { - var value_name_wtf16le_buf: [RegistryWtf16Le.value_name_max_len]u16 = undefined; - const value_name_wtf16le_len: usize = std.unicode.wtf8ToWtf16Le(value_name_wtf16le_buf[0..], value_name) catch unreachable; - value_name_wtf16le_buf[value_name_wtf16le_len] = 0; - break :value_name_wtf16le value_name_wtf16le_buf[0..value_name_wtf16le_len :0]; - }; - - const registry_wtf16le: RegistryWtf16Le = .{ .key = reg.key }; - return registry_wtf16le.getDword(subkey_wtf16le, value_name_wtf16le); - } - - /// Under private space with flags: - /// KEY_QUERY_VALUE and KEY_ENUMERATE_SUB_KEYS. - /// After finishing work, call `closeKey`. - pub fn loadFromPath(absolute_path: []const u8) error{KeyNotFound}!RegistryWtf8 { - const absolute_path_wtf16le: [:0]const u16 = absolute_path_wtf16le: { - var absolute_path_wtf16le_buf: [RegistryWtf16Le.value_name_max_len]u16 = undefined; - const absolute_path_wtf16le_len: usize = std.unicode.wtf8ToWtf16Le(absolute_path_wtf16le_buf[0..], absolute_path) catch unreachable; - absolute_path_wtf16le_buf[absolute_path_wtf16le_len] = 0; - break :absolute_path_wtf16le absolute_path_wtf16le_buf[0..absolute_path_wtf16le_len :0]; - }; - - const registry_wtf16le = try RegistryWtf16Le.loadFromPath(absolute_path_wtf16le); - return .{ .key = registry_wtf16le.key }; - } -}; - -const RegistryWtf16Le = struct { - key: windows.HKEY, - - /// Includes root key (f.e. HKEY_LOCAL_MACHINE). - /// https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-element-size-limits - pub const key_name_max_len = 255; - /// In Unicode characters. - /// https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-element-size-limits - pub const value_name_max_len = 16_383; - - /// Under HKEY_LOCAL_MACHINE with flags: - /// KEY_QUERY_VALUE, KEY_ENUMERATE_SUB_KEYS, optionally KEY_WOW64_32KEY. - /// After finishing work, call `closeKey`. - fn openKey(hkey: windows.HKEY, key_wtf16le: [:0]const u16, options: OpenOptions) error{KeyNotFound}!RegistryWtf16Le { - var key: windows.HKEY = undefined; - var access: windows.REGSAM = windows.KEY_QUERY_VALUE | windows.KEY_ENUMERATE_SUB_KEYS; - if (options.wow64_32) access |= windows.KEY_WOW64_32KEY; - const return_code_int: windows.HRESULT = windows.advapi32.RegOpenKeyExW( - hkey, - key_wtf16le, - 0, - access, - &key, - ); - const return_code: windows.Win32Error = @enumFromInt(return_code_int); - switch (return_code) { - .SUCCESS => {}, - .FILE_NOT_FOUND => return error.KeyNotFound, - - else => return error.KeyNotFound, - } - return .{ .key = key }; - } - - /// Closes key, after that usage is invalid - fn closeKey(reg: RegistryWtf16Le) void { - const return_code_int: windows.HRESULT = windows.advapi32.RegCloseKey(reg.key); - const return_code: windows.Win32Error = @enumFromInt(return_code_int); - switch (return_code) { - .SUCCESS => {}, - else => {}, - } - } - - /// Get string ([:0]const u16) from registry. - fn getString(reg: RegistryWtf16Le, allocator: std.mem.Allocator, subkey_wtf16le: [:0]const u16, value_name_wtf16le: [:0]const u16) error{ OutOfMemory, ValueNameNotFound, NotAString, StringNotFound }![]const u16 { - var actual_type: windows.ULONG = undefined; - - // Calculating length to allocate - var value_wtf16le_buf_size: u32 = 0; // in bytes, including any terminating NUL character or characters. - var return_code_int: windows.HRESULT = windows.advapi32.RegGetValueW( - reg.key, - subkey_wtf16le, - value_name_wtf16le, - RRF.RT_REG_SZ, - &actual_type, - null, - &value_wtf16le_buf_size, - ); - - // Check returned code and type - var return_code: windows.Win32Error = @enumFromInt(return_code_int); - switch (return_code) { - .SUCCESS => std.debug.assert(value_wtf16le_buf_size != 0), - .MORE_DATA => unreachable, // We are only reading length - .FILE_NOT_FOUND => return error.ValueNameNotFound, - .INVALID_PARAMETER => unreachable, // We didn't combine RRF.SUBKEY_WOW6464KEY and RRF.SUBKEY_WOW6432KEY - else => return error.StringNotFound, - } - switch (actual_type) { - windows.REG.SZ => {}, - else => return error.NotAString, - } - - const value_wtf16le_buf: []u16 = try allocator.alloc(u16, std.math.divCeil(u32, value_wtf16le_buf_size, 2) catch unreachable); - errdefer allocator.free(value_wtf16le_buf); - - return_code_int = windows.advapi32.RegGetValueW( - reg.key, - subkey_wtf16le, - value_name_wtf16le, - RRF.RT_REG_SZ, - &actual_type, - value_wtf16le_buf.ptr, - &value_wtf16le_buf_size, - ); - - // Check returned code and (just in case) type again. - return_code = @enumFromInt(return_code_int); - switch (return_code) { - .SUCCESS => {}, - .MORE_DATA => unreachable, // Calculated first time length should be enough, even overestimated - .FILE_NOT_FOUND => return error.ValueNameNotFound, - .INVALID_PARAMETER => unreachable, // We didn't combine RRF.SUBKEY_WOW6464KEY and RRF.SUBKEY_WOW6432KEY - else => return error.StringNotFound, - } - switch (actual_type) { - windows.REG.SZ => {}, - else => return error.NotAString, - } - - const value_wtf16le: []const u16 = value_wtf16le: { - // note(bratishkaerik): somehow returned value in `buf_len` is overestimated by Windows and contains extra space - // we will just search for zero termination and forget length - // Windows sure is strange - const value_wtf16le_overestimated: [*:0]const u16 = @ptrCast(value_wtf16le_buf.ptr); - break :value_wtf16le std.mem.span(value_wtf16le_overestimated); - }; - - _ = allocator.resize(value_wtf16le_buf, value_wtf16le.len); - return value_wtf16le; - } - - /// Get DWORD (u32) from registry. - fn getDword(reg: RegistryWtf16Le, subkey_wtf16le: [:0]const u16, value_name_wtf16le: [:0]const u16) error{ ValueNameNotFound, NotADword, DwordTooLong, DwordNotFound }!u32 { - var actual_type: windows.ULONG = undefined; - var reg_size: u32 = @sizeOf(u32); - var reg_value: u32 = 0; - - const return_code_int: windows.HRESULT = windows.advapi32.RegGetValueW( - reg.key, - subkey_wtf16le, - value_name_wtf16le, - RRF.RT_REG_DWORD, - &actual_type, - ®_value, - ®_size, - ); - const return_code: windows.Win32Error = @enumFromInt(return_code_int); - switch (return_code) { - .SUCCESS => {}, - .MORE_DATA => return error.DwordTooLong, - .FILE_NOT_FOUND => return error.ValueNameNotFound, - .INVALID_PARAMETER => unreachable, // We didn't combine RRF.SUBKEY_WOW6464KEY and RRF.SUBKEY_WOW6432KEY - else => return error.DwordNotFound, - } - - switch (actual_type) { - windows.REG.DWORD => {}, - else => return error.NotADword, - } - - return reg_value; - } - - /// Under private space with flags: - /// KEY_QUERY_VALUE and KEY_ENUMERATE_SUB_KEYS. - /// After finishing work, call `closeKey`. - fn loadFromPath(absolute_path_as_wtf16le: [:0]const u16) error{KeyNotFound}!RegistryWtf16Le { - var key: windows.HKEY = undefined; - - const return_code_int: windows.HRESULT = std.os.windows.advapi32.RegLoadAppKeyW( - absolute_path_as_wtf16le, - &key, - windows.KEY_QUERY_VALUE | windows.KEY_ENUMERATE_SUB_KEYS, - 0, - 0, - ); - const return_code: windows.Win32Error = @enumFromInt(return_code_int); - switch (return_code) { - .SUCCESS => {}, - else => return error.KeyNotFound, - } - - return .{ .key = key }; - } -}; diff --git a/src/androidbuild/androidbuild.zig b/src/androidbuild/androidbuild.zig index 9d91f16..fa0d2b0 100644 --- a/src/androidbuild/androidbuild.zig +++ b/src/androidbuild/androidbuild.zig @@ -97,29 +97,58 @@ pub fn runNameContext(comptime name: []const u8) []const u8 { return "zig-android-sdk " ++ name; } -pub fn printErrorsAndExit(message: []const u8, errors: []const []const u8) noreturn { - nosuspend { - log.err("{s}", .{message}); - const stderr = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) - std.io.getStdErr().writer() - else - std.fs.File.stderr(); - std.debug.lockStdErr(); - defer std.debug.unlockStdErr(); - for (errors) |err| { - var it = std.mem.splitScalar(u8, err, '\n'); - const headline = it.next() orelse continue; - stderr.writeAll("- ") catch {}; - stderr.writeAll(headline) catch {}; - stderr.writeAll("\n") catch {}; - while (it.next()) |line| { - stderr.writeAll(" ") catch {}; - stderr.writeAll(line) catch {}; +pub fn printErrorsAndExit(b: *std.Build, message: []const u8, errors: []const []const u8) noreturn { + if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) { + nosuspend { + // Deprecated path for Zig 0.14.x and Zig 0.15.x + log.err("{s}", .{message}); + + const stderr = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) + std.io.getStdErr().writer() + else if (builtin.zig_version.major == 0 and builtin.zig_version.minor == 15) + std.fs.File.stderr() + else + @compileError("NOTE: Handled below for newer zig cases"); + + for (errors) |err| { + var it = std.mem.splitScalar(u8, err, '\n'); + const headline = it.next() orelse continue; + stderr.writeAll("- ") catch {}; + stderr.writeAll(headline) catch {}; stderr.writeAll("\n") catch {}; + while (it.next()) |line| { + stderr.writeAll(" ") catch {}; + stderr.writeAll(line) catch {}; + stderr.writeAll("\n") catch {}; + } } + stderr.writeAll("\n") catch {}; + } + std.process.exit(1); + } + + // Format errors and then use the logger to show the user + var buf = b.allocator.alloc(u8, 16384) catch @panic("OOM"); + defer b.allocator.free(buf); + var writer = std.Io.Writer.fixed(buf[0..]); + writer.writeAll(message) catch @panic("OOM"); + writer.writeByte('\n') catch @panic("OOM"); + if (errors.len == 0) { + writer.writeAll("- no errors written") catch @panic("OOM"); + } + for (errors) |err| { + var it = std.mem.splitScalar(u8, err, '\n'); + const headline = it.next() orelse continue; + writer.writeAll("- ") catch @panic("OOM"); + writer.writeAll(headline) catch @panic("OOM"); + writer.writeByte('\n') catch @panic("OOM"); + while (it.next()) |line| { + writer.writeAll(" ") catch @panic("OOM"); + writer.writeAll(line) catch @panic("OOM"); + writer.writeByte('\n') catch @panic("OOM"); } - stderr.writeAll("\n") catch {}; } + log.err("{s}", .{writer.buffered()}); std.process.exit(1); } diff --git a/src/androidbuild/apk.zig b/src/androidbuild/apk.zig index c047126..801535c 100644 --- a/src/androidbuild/apk.zig +++ b/src/androidbuild/apk.zig @@ -78,7 +78,7 @@ pub fn create(sdk: *Sdk, options: Options) *Apk { ndk.validateApiLevel(b, options.api_level, &errors); } if (errors.items.len > 0) { - printErrorsAndExit("unable to find required Android installation", errors.items); + printErrorsAndExit(sdk.b, "unable to find required Android installation", errors.items); } const apk: *Apk = b.allocator.create(Apk) catch @panic("OOM"); @@ -261,7 +261,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { // try errors.append(b.fmt("must add at least one Java file to build OR you must setup your AndroidManifest.xml to have 'android:hasCode=false'", .{})); // } if (errors.items.len > 0) { - printErrorsAndExit("misconfigured Android APK", errors.items); + printErrorsAndExit(apk.b, "misconfigured Android APK", errors.items); } } @@ -503,7 +503,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { } apk.setLibCFile(artifact); apk.addLibraryPaths(artifact.root_module); - artifact.linkLibC(); + artifact.root_module.link_libc = true; // Apply workaround for Zig 0.14.0 stable // @@ -547,6 +547,17 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { }); d8.setName(runNameContext("d8")); + // Prepend JDK bin path so d8 can always find "java", etc + if (apk.sdk.jdk_path.len > 0) { + var env_map = d8.getEnvMap(); + const path = env_map.get("PATH") orelse &[0]u8{}; + const new_path = try std.mem.join(b.allocator, &[1]u8{std.fs.path.delimiter}, &.{ + b.fmt("{s}/bin", .{apk.sdk.jdk_path}), + path, + }); + try env_map.put("PATH", new_path); + } + // ie. android_sdk/platforms/android-{api-level}/android.jar d8.addArg("--lib"); d8.addArg(root_jar); @@ -717,6 +728,7 @@ fn doInstallApk(apk: *Apk) std.mem.Allocator.Error!*Step.InstallFile { apk.build_tools.apksigner, "sign", }); + try apk.updatePathWithJdk(apksigner); apksigner.setName(runNameContext("apksigner")); apksigner.addArg("--ks"); // ks = keystore apksigner.addFileArg(key_store.file); @@ -814,7 +826,7 @@ fn applyLibLinkCppWorkaroundIssue19(apk: *Apk, artifact: *Step.Compile) void { // NOTE(jae): 2025-11-18 // Due to Android include files not being provided by Zig, we should provide them if the library is linking against C++ // This resolves an issue where if you are trying to build the openxr_loader C++ code from source, it can't find standard library includes like or - artifact.addIncludePath(.{ .cwd_relative = b.fmt("{s}/usr/include/c++/v1", .{apk.ndk.sysroot_path}) }); + artifact.root_module.addIncludePath(.{ .cwd_relative = b.fmt("{s}/usr/include/c++/v1", .{apk.ndk.sysroot_path}) }); if (artifact.root_module.link_libcpp == true) { // NOTE(jae): 2025-04-06 @@ -909,4 +921,19 @@ fn updateSharedLibraryOptions(artifact: *std.Build.Step.Compile) void { // artifact.export_table = true; } +/// Prepend JDK bin path so "d8", "apksigner", etc can always find "java" +fn updatePathWithJdk(apk: *Apk, run: *std.Build.Step.Run) !void { + if (apk.sdk.jdk_path.len == 0) return; + + const b = apk.b; + + var env_map = run.getEnvMap(); + const path = env_map.get("PATH") orelse &[0]u8{}; + const new_path = try std.mem.join(b.allocator, &[1]u8{std.fs.path.delimiter}, &.{ + b.fmt("{s}/bin", .{apk.sdk.jdk_path}), + path, + }); + try env_map.put("PATH", new_path); +} + const Apk = @This(); diff --git a/src/androidbuild/builtin_options_update.zig b/src/androidbuild/builtin_options_update.zig index c487c2d..7d1b54b 100644 --- a/src/androidbuild/builtin_options_update.zig +++ b/src/androidbuild/builtin_options_update.zig @@ -47,8 +47,15 @@ fn make(step: *Step, _: Build.Step.MakeOptions) !void { // ie. "com.zig.sdl2\n\r" const package_name_backing_buf = try b.allocator.alloc(u8, 8192); defer b.allocator.free(package_name_backing_buf); - const package_name_filedata = try package_name_path.root_dir.handle.readFile(package_name_path.sub_path, package_name_backing_buf); - const package_name_stripped = std.mem.trimRight(u8, package_name_filedata, " \r\n"); + + const package_name_filedata = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + try package_name_path.root_dir.handle.readFile(package_name_path.sub_path, package_name_backing_buf) + else + try package_name_path.root_dir.handle.readFile(b.graph.io, package_name_path.sub_path, package_name_backing_buf); + const package_name_stripped = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) + std.mem.trimRight(u8, package_name_filedata, " \r\n") + else + std.mem.trimEnd(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); diff --git a/src/androidbuild/d8glob.zig b/src/androidbuild/d8glob.zig index 9fd3000..20015a6 100644 --- a/src/androidbuild/d8glob.zig +++ b/src/androidbuild/d8glob.zig @@ -64,12 +64,22 @@ fn make(step: *Step, _: Build.Step.MakeOptions) !void { // NOTE(jae): 2025-07-23 // As of Zig 0.15.0-dev.1092+d772c0627, package_name_path.openDir("") is not possible as it assumes you're appending a sub-path - var dir = try search_dir.root_dir.handle.openDir(search_dir.sub_path, .{ .iterate = true }); - defer dir.close(); + var dir = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + try search_dir.root_dir.handle.openDir(search_dir.sub_path, .{ .iterate = true }) + else + try search_dir.root_dir.handle.openDir(b.graph.io, search_dir.sub_path, .{ .iterate = true }); + defer if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + dir.close() + else + dir.close(b.graph.io); var walker = try dir.walk(arena); defer walker.deinit(); - while (try walker.next()) |entry| { + while (if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + try walker.next() + else + try walker.next(b.graph.io)) |entry| + { if (entry.kind != .file) { continue; } diff --git a/src/androidbuild/tools.zig b/src/androidbuild/tools.zig index 68c06d8..1c6e084 100644 --- a/src/androidbuild/tools.zig +++ b/src/androidbuild/tools.zig @@ -3,10 +3,6 @@ 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 getAndroidTriple = androidbuild.getAndroidTriple; const runNameContext = androidbuild.runNameContext; @@ -66,16 +62,11 @@ pub fn create(b: *std.Build, options: Options) *Sdk { const host_os_tag = b.graph.host.result.os.tag; // Discover tool paths - var path_search = PathSearch.init(b.allocator, host_os_tag) catch |err| switch (err) { + var path_search = PathSearch.init(b, 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; - } - const configured_android_sdk_path = getAndroidSDKPath(b.allocator) catch @panic("OOM"); + const configured_android_sdk_path = getAndroidSDKPath(b) 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; @@ -98,52 +89,51 @@ pub fn create(b: *std.Build, options: Options) *Sdk { errors.append(b.allocator, \\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." + \\- Then configure your ANDROID_HOME environment variable to where you've installed it. ) catch @panic("OOM"); } if (errors.items.len > 0) { - printErrorsAndExit("unable to find required Android installation", errors.items); + printErrorsAndExit(b, "unable to find required Android installation", errors.items); } // 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(b.allocator, message) catch @panic("OOM"); - }, - else => { - const message = b.fmt("Android Command Line Tools path had unexpected error: {s} ({s})", .{ - @errorName(toolerr), - tools, - }); - errors.append(b.allocator, 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(b.allocator, message) catch @panic("OOM"); - }, - }; - break :cmdlineblk cmdline_tools; + const cmdline_tool_path_list = [_][]const u8{ + b.pathResolve(&[_][]const u8{ android_sdk_path, "cmdline-tools", "latest", "bin" }), + b.pathResolve(&[_][]const u8{ android_sdk_path, "tools", "bin" }), }; - + const cmdline_tools_path: []const u8 = cmdlineblk: { + for (cmdline_tool_path_list) |cmdline_tools_path| { + const access_wrapped_error = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + std.fs.accessAbsolute(cmdline_tools_path, .{}) + else + std.Io.Dir.accessAbsolute(b.graph.io, cmdline_tools_path, .{}); + access_wrapped_error catch |err| switch (err) { + error.FileNotFound => continue, + else => { + const message = b.fmt("Android Command Line Tools path had an unexpected error: {s} ({s})", .{ + @errorName(err), + cmdline_tools_path, + }); + errors.append(b.allocator, message) catch @panic("OOM"); + }, + }; + break :cmdlineblk cmdline_tools_path; + } + // If unable to find command line tools, return empty + break :cmdlineblk &[0]u8{}; + }; + if (cmdline_tools_path.len == 0) { + const message = b.fmt("Android SDK Command-line tools not found in SDK folder. (expected {s} or {s} to exist)\n- This can either be installed via Android Studio\n- or downloaded directly here: {s}", .{ + cmdline_tool_path_list[0], + cmdline_tool_path_list[1], + "https://developer.android.com/studio#command-line-tools-only", + }); + errors.append(b.allocator, message) catch @panic("OOM"); + } if (errors.items.len > 0) { - printErrorsAndExit("unable to find required Android installation", errors.items); + printErrorsAndExit(b, "unable to find required Android installation", errors.items); } const platform_tools_path = b.pathResolve(&[_][]const u8{ android_sdk_path, "platform-tools" }); @@ -380,77 +370,56 @@ pub fn createOrGetLibCFile(sdk: *Sdk, compile: *Step.Compile, android_api_level: return android_libc_path; } -/// Search JDK_HOME, and then JAVA_HOME -fn getJDKPath(allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 { - 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 (jdk_home.len > 0) { - return jdk_home; - } - - 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 (java_home.len > 0) { - return java_home; - } - - return &[0]u8{}; -} - /// Caller must free returned memory -fn getAndroidSDKPath(allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 { - 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 (android_home.len > 0) { +fn getAndroidSDKPath(b: *std.Build) error{OutOfMemory}![]const u8 { + const allocator = b.allocator; + const environ_map = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + &b.graph.env_map + else + &b.graph.environ_map; + + if (environ_map.get("ANDROID_HOME")) |android_home| if (android_home.len > 0) return android_home; - } // Check for Android Studio switch (builtin.os.tag) { .windows => { + // NOTE(jae): 2026-01-10 + // At least as of Android Studio Meerkat (2024.3.1), built on March 13th 2025. + // This logic will not do anything on Windows. SdkPath is empty. + // + // So let's just remove this. + // // 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" - 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, - }; - break :blk sdk_path; - } - break :blk &[0]u8{}; - }; - if (android_studio_sdk_path.len > 0) { - return android_studio_sdk_path; - } + // + // const windows = std.os.windows; + // const RegistryWtf8 = @import("WindowsSdk.zig").RegistryWtf8; + // 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, + // }; + // break :blk sdk_path; + // } + // break :blk &[0]u8{}; + // }; + // if (android_studio_sdk_path.len > 0) { + // return android_studio_sdk_path; + // } }, // NOTE(jae): 2024-09-15 // Look into auto-discovery of Android SDK for Mac // Mac: /Users//Library/Android/sdk .macos => { - const user = std.process.getEnvVarOwned(allocator, "USER") catch |err| switch (err) { - error.OutOfMemory => return error.OutOfMemory, - error.EnvironmentVariableNotFound => &[0]u8{}, - error.InvalidWtf8 => @panic("USER environment variable is invalid UTF-8"), - }; + const user = environ_map.get("USER") orelse &[0]u8{}; defer allocator.free(user); return try std.fmt.allocPrint(allocator, "/Users/{s}/Library/Android/sdk", .{user}); }, @@ -462,62 +431,46 @@ fn getAndroidSDKPath(allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 // - /Users/[USER]/Library/Android/sdk // Source: https://stackoverflow.com/a/34627928 .linux => { - { - const android_sdk_path = "/usr/lib/android-sdk"; + for ([_][]const u8{ + "/usr/lib/android-sdk", + "/Library/Android/sdk", + }) |android_sdk_path| { const has_path: bool = pathblk: { - std.fs.accessAbsolute(android_sdk_path, .{}) catch |err| switch (err) { + const access_wrapped_error = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + std.fs.accessAbsolute(android_sdk_path, .{}) + else + std.Io.Dir.accessAbsolute(b.graph.io, android_sdk_path, .{}); + access_wrapped_error catch |err| switch (err) { error.FileNotFound => break :pathblk false, // fallthrough and try next else => std.debug.panic("{s} has error: {}", .{ android_sdk_path, err }), }; break :pathblk true; }; - if (has_path) { - return android_sdk_path; - } - } - - { - const android_sdk_path = "/Library/Android/sdk"; - const has_path: bool = pathblk: { - std.fs.accessAbsolute(android_sdk_path, .{}) catch |err| switch (err) { - error.FileNotFound => break :pathblk false, // fallthrough and try next - else => std.debug.panic("{s} has error: {}", .{ android_sdk_path, err }), - }; - break :pathblk true; - }; - if (has_path) { - return android_sdk_path; + if (!has_path) { + continue; } + return android_sdk_path; } // Check user paths // - /home/AccountName/Android/Sdk // - /Users/[USER]/Library/Android/sdk - const user = std.process.getEnvVarOwned(allocator, "USER") catch |err| switch (err) { - error.OutOfMemory => return error.OutOfMemory, - error.EnvironmentVariableNotFound => &[0]u8{}, - error.InvalidWtf8 => @panic("USER environment variable is invalid UTF-8"), - }; + const user = environ_map.get("USER") orelse &[0]u8{}; if (user.len > 0) { - { - const android_sdk_path = try std.fmt.allocPrint(allocator, "/Users/{s}/Library/Android/sdk", .{user}); - const has_path: bool = pathblk: { - std.fs.accessAbsolute(android_sdk_path, .{}) catch |err| switch (err) { - error.FileNotFound => break :pathblk false, // fallthrough and try next - else => std.debug.panic("{s} has error: {}", .{ android_sdk_path, err }), - }; - break :pathblk true; - }; - if (has_path) { - return android_sdk_path; - } - } - { + inline for ([_][]const u8{ + "/Users/{s}/Library/Android/sdk", // NOTE(jae): 2025-05-11 // No idea if /AccountName/ maps to $USER but going to assume it does for now. - const android_sdk_path = try std.fmt.allocPrint(allocator, "/home/{s}/Android/Sdk", .{user}); + "/home/{s}/Android/Sdk", + }) |android_sdk_user_path_template| { + const android_sdk_path = try std.fmt.allocPrint(allocator, android_sdk_user_path_template, .{user}); + errdefer allocator.free(android_sdk_path); const has_path: bool = pathblk: { - std.fs.accessAbsolute(android_sdk_path, .{}) catch |err| switch (err) { + const access_wrapped_error = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + std.fs.accessAbsolute(android_sdk_path, .{}) + else + std.Io.Dir.accessAbsolute(b.graph.io, android_sdk_path, .{}); + access_wrapped_error catch |err| switch (err) { error.FileNotFound => break :pathblk false, // fallthrough and try next else => std.debug.panic("{s} has error: {}", .{ android_sdk_path, err }), }; @@ -526,6 +479,7 @@ fn getAndroidSDKPath(allocator: std.mem.Allocator) error{OutOfMemory}![]const u8 if (has_path) { return android_sdk_path; } + allocator.free(android_sdk_path); } } }, @@ -546,6 +500,7 @@ pub const KeyStore = struct { /// Searches your PATH environment variable directories for adb, jarsigner, etc const PathSearch = struct { + b: *std.Build, allocator: std.mem.Allocator, path_env: []const u8, path_it: std.mem.SplitIterator(u8, .scalar), @@ -556,15 +511,16 @@ const PathSearch = struct { jarsigner: []const u8, android_sdk_path: ?[]const u8 = null, - jdk_path: ?[]const u8 = null, - - pub fn init(allocator: std.mem.Allocator, host_os_tag: std.Target.Os.Tag) error{ EnvironmentVariableNotFound, OutOfMemory }!PathSearch { - const path_env = std.process.getEnvVarOwned(allocator, "PATH") catch |err| switch (err) { - error.OutOfMemory => return error.OutOfMemory, - error.EnvironmentVariableNotFound => return error.EnvironmentVariableNotFound, - // Windows-only - error.InvalidWtf8 => @panic("PATH environment variable is invalid UTF-8"), - }; + jdk_path: ?[]const u8, + + pub fn init(b: *std.Build, host_os_tag: std.Target.Os.Tag) error{ EnvironmentVariableNotFound, OutOfMemory }!PathSearch { + const allocator = b.allocator; + const environ_map = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + &b.graph.env_map + else + &b.graph.environ_map; + + const path_env = environ_map.get("PATH") orelse return error.EnvironmentVariableNotFound; if (path_env.len == 0) { return error.EnvironmentVariableNotFound; } @@ -574,19 +530,56 @@ const PathSearch = struct { const adb = try std.mem.concat(allocator, u8, &.{ "adb", exe_suffix }); const jarsigner = try std.mem.concat(allocator, u8, &.{ "jarsigner", exe_suffix }); + // setup paths + const configured_jdk_path: ?[]const u8 = jdkpath: { + const jdk_home = environ_map.get("JDK_HOME") orelse &[0]u8{}; + if (jdk_home.len > 0) { + break :jdkpath jdk_home; + } + const java_home = environ_map.get("JAVA_HOME") orelse &[0]u8{}; + if (java_home.len > 0) { + break :jdkpath java_home; + } + if (host_os_tag == .linux) { + // const environ_map = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + // &b.graph.env_map + // else + // &b.graph.environ_map; + const maybe_user: ?[]const u8 = environ_map.get("USER") orelse null; + if (maybe_user) |user| { + const jarsigner_path = b.findProgram(&.{"jarsigner"}, &.{ + // NOTE(jae): 2026-01-10 + // I manually put my install here, not standard per-se but I see no reason to not support this. + b.fmt("/home/{s}/android-studio/jbr/bin", .{user}), + // NOTE(jae): 2026-01-10 + // Suggested install locations for Android Studio from: https://developer.android.com/studio/install + "/usr/local/android-studio/jbr/bin", // for your user profile + "/opt/android-studio/jbr/bin", // for shared users + }) catch break :jdkpath null; + const jbr_bin_dir = std.fs.path.dirname(jarsigner_path) orelse break :jdkpath null; + const jbr_dir = std.fs.path.dirname(jbr_bin_dir) orelse break :jdkpath null; + break :jdkpath jbr_dir; + } + } + break :jdkpath null; + }; + const path_it = std.mem.splitScalar(u8, path_env, ';'); return .{ + .b = b, .allocator = allocator, .path_env = path_env, .path_it = path_it, .adb = adb, .jarsigner = jarsigner, + .jdk_path = configured_jdk_path, }; } - pub fn deinit(self: *PathSearch) void { - const allocator = self.allocator; - allocator.free(self.path_env); + pub fn deinit(_: *PathSearch) void { + // NOTE(jae): 2026-01-09: Using copy from "b.graph.environ_map" now + // const allocator = self.allocator; + // allocator.free(self.path_env); } /// Get the Android SDK Path, the caller owns the memory @@ -630,9 +623,10 @@ const PathSearch = struct { { const adb_binary_path = std.fs.path.join(allocator, &.{ path_item, self.adb }) catch |err| return err; defer allocator.free(adb_binary_path); - std.fs.accessAbsolute(adb_binary_path, .{}) catch { - break :blk; - }; + if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + std.fs.accessAbsolute(adb_binary_path, .{}) catch break :blk + else + std.Io.Dir.accessAbsolute(self.b.graph.io, adb_binary_path, .{}) catch break :blk; } // Transform: "Sdk\platform-tools" into "Sdk" const sdk_path = std.fs.path.dirname(path_item) orelse { @@ -655,9 +649,10 @@ const PathSearch = struct { const jarsigner_binary_path = std.fs.path.join(allocator, &.{ path_item, self.jarsigner }) catch |err| return err; defer allocator.free(jarsigner_binary_path); - std.fs.accessAbsolute(jarsigner_binary_path, .{}) catch { - break :blk; - }; + if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15) + std.fs.accessAbsolute(jarsigner_binary_path, .{}) catch break :blk + else + std.Io.Dir.accessAbsolute(self.b.graph.io, jarsigner_binary_path, .{}) catch break :blk; } // Transform: "jdk-21.0.3.9-hotspot/bin" into "jdk-21.0.3.9-hotspot" const jdk_path = std.fs.path.dirname(path_item) orelse {