diff --git a/build.zig b/build.zig index a4dcdc7..5327af9 100644 --- a/build.zig +++ b/build.zig @@ -22,5 +22,12 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); + + // Create stub of builtin options. + // This is discovered and then replace in src/androidbuild/apk.zig + const android_builtin_options = std.Build.addOptions(b); + android_builtin_options.addOption([:0]const u8, "package_name", ""); + module.addImport("android_builtin", android_builtin_options.createModule()); + module.linkSystemLibrary("log", .{}); } diff --git a/examples/minimal/README.md b/examples/minimal/README.md index 50224f1..39e1bd7 100644 --- a/examples/minimal/README.md +++ b/examples/minimal/README.md @@ -29,12 +29,17 @@ adb uninstall "com.zig.minimal" ### View logs of application -Powershell +Powershell (app doesn't need to be running) ```sh adb logcat | Select-String com.zig.minimal: ``` -Bash +Bash (app doesn't need running to be running) +```sh +adb logcat com.zig.minimal:D *:S +``` + +Bash (app must be running, logs everything by the process including modules) ```sh adb logcat --pid=`adb shell pidof -s com.zig.minimal` ``` diff --git a/examples/sdl2/README.md b/examples/sdl2/README.md index ebd2267..c4b7e07 100644 --- a/examples/sdl2/README.md +++ b/examples/sdl2/README.md @@ -35,12 +35,17 @@ adb uninstall "com.zig.sdl2" ### View logs of application -Powershell +Powershell (app doesn't need to be running) ```sh adb logcat | Select-String com.zig.sdl2: ``` -Bash +Bash (app doesn't need running to be running) +```sh +adb logcat com.zig.sdl2:D *:S +``` + +Bash (app must be running, logs everything by the process including modules) ```sh adb logcat --pid=`adb shell pidof -s com.zig.sdl2` ``` diff --git a/examples/sdl2/src/sdl-zig-demo.zig b/examples/sdl2/src/sdl-zig-demo.zig index aaaa3aa..01d5e66 100644 --- a/examples/sdl2/src/sdl-zig-demo.zig +++ b/examples/sdl2/src/sdl-zig-demo.zig @@ -23,7 +23,6 @@ else /// This needs to be exported for Android builds export fn SDL_main() callconv(.C) void { if (builtin.abi == .android) { - android.set_default_tag("com.zig.sdl2"); _ = std.start.callMain(); } else { @panic("SDL_main should not be called outside of Android builds"); @@ -31,6 +30,8 @@ export fn SDL_main() callconv(.C) void { } pub fn main() !void { + log.debug("started sdl-zig-demo", .{}); + if (sdl.SDL_Init(sdl.SDL_INIT_VIDEO) < 0) { log.info("Unable to initialize SDL: {s}", .{sdl.SDL_GetError()}); return error.SDLInitializationFailed; @@ -69,7 +70,14 @@ pub fn main() !void { defer sdl.SDL_DestroyTexture(zig_texture); var quit = false; + var has_run_frame: FrameLog = .none; while (!quit) { + if (has_run_frame == .one_frame_passed) { + // NOTE(jae): 2024-10-03 + // Allow inspection of logs to see if a frame executed at least once + log.debug("has executed one frame", .{}); + has_run_frame = .logged_one_frame; + } var event: sdl.SDL_Event = undefined; while (sdl.SDL_PollEvent(&event) != 0) { switch (event.type) { @@ -83,7 +91,15 @@ pub fn main() !void { _ = sdl.SDL_RenderClear(renderer); _ = sdl.SDL_RenderCopy(renderer, zig_texture, null, null); sdl.SDL_RenderPresent(renderer); - sdl.SDL_Delay(17); + if (has_run_frame == .none) { + has_run_frame = .one_frame_passed; + } } } + +const FrameLog = enum { + none, + one_frame_passed, + logged_one_frame, +}; diff --git a/src/android/android.zig b/src/android/android.zig index 12bb6aa..7218030 100644 --- a/src/android/android.zig +++ b/src/android/android.zig @@ -1,19 +1,19 @@ const std = @import("std"); const builtin = @import("builtin"); -extern "log" fn __android_log_write(prio: c_int, tag: [*c]const u8, text: [*c]const u8) c_int; +// TODO(jae): 2024-10-03 +// Consider exposing this in the future +// pub const builtin = android_builtin; -/// Update the tag used by the Android logger, this will default to your package name unless logging occurs in a seperate -/// thread. -/// -/// ie. If using via SDL2, you'll likely want the tag set. -/// -/// NOTE(jae): 2024-09-22 -/// We should look at moving to making the "tag" default your Android package name by parsing that out of your AndroidManifest.xml -/// or perhaps even just make it use the "name" of your app -pub fn set_default_tag(tag: [:0]const u8) void { - LogWriter.tag = tag; -} +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; +}; + +extern "log" fn __android_log_write(prio: c_int, tag: [*c]const u8, text: [*c]const u8) c_int; /// Alternate panic implementation that calls __android_log_write so that you can see the logging via "adb logcat" pub const panic = Panic.panic; @@ -50,12 +50,12 @@ pub const Level = enum(u8) { /// Alternate log function implementation that calls __android_log_write so that you can see the logging via "adb logcat" pub fn logFn( comptime message_level: std.log.Level, - comptime scope: if (builtin.zig_version.minor != 13) - // Support Zig 0.14.0-dev - @Type(.enum_literal) - else + comptime scope: if (builtin.zig_version.major == 0 and builtin.zig_version.minor == 13) // Support Zig 0.13.0 - @Type(.EnumLiteral), + @Type(.EnumLiteral) + else + // Support Zig 0.14.0-dev + @Type(.enum_literal), comptime format: []const u8, args: anytype, ) void { @@ -82,9 +82,17 @@ pub fn logFn( /// LogWriter was was taken basically as is from: https://github.com/ikskuh/ZigAndroidTemplate const LogWriter = struct { - /// name of the application / log scope - /// if not set, it'll default to the "package" attribute defined in AndroidManifest.xml - var tag: [:0]const u8 = &[0:0]u8{}; + /// 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. + var tag: [:0]const u8 = android_builtin.package_name; level: Level, diff --git a/src/androidbuild/apk.zig b/src/androidbuild/apk.zig index e7845dc..ddff07e 100644 --- a/src/androidbuild/apk.zig +++ b/src/androidbuild/apk.zig @@ -2,6 +2,7 @@ const std = @import("std"); const androidbuild = @import("androidbuild.zig"); const D8Glob = @import("d8glob.zig").D8Glob; const Tools = @import("tools.zig").Tools; +const BuiltinOptionsUpdate = @import("builtin_options_update.zig").BuiltinOptionsUpdate; const KeyStore = androidbuild.KeyStore; const getAndroidTriple = androidbuild.getAndroidTriple; @@ -14,39 +15,52 @@ const ResolvedTarget = std.Build.ResolvedTarget; const LazyPath = std.Build.LazyPath; pub const APK = struct { - const AddJavaSourceFileOption = struct { + pub const AddJavaSourceFileOption = struct { file: LazyPath, // NOTE(jae): 2024-09-17 // Consider adding flags to define/declare the target Java version for this file. // Not sure what we'll need in the future. // flags: []const []const u8 = &.{}, }; - const AddJavaSourceFilesOptions = struct { + pub const AddJavaSourceFilesOptions = struct { root: LazyPath, files: []const []const u8, }; + pub const Resource = union(enum) { + // file: File, + directory: Directory, + + // pub const File = struct { + // source: LazyPath, + // }; + + pub const Directory = struct { + source: LazyPath, + }; + }; b: *std.Build, tools: *const Tools, + key_store: ?KeyStore, + android_manifest: ?LazyPath, artifacts: std.ArrayListUnmanaged(*Step.Compile), java_files: std.ArrayListUnmanaged(LazyPath), - key_store: ?KeyStore, - resource_files: *Step.WriteFile, + resources: std.ArrayListUnmanaged(Resource), pub fn create(b: *std.Build, tools: *const Tools) *@This() { - const ab: *@This() = b.allocator.create(@This()) catch @panic("OOM"); - ab.* = .{ + const apk: *@This() = b.allocator.create(@This()) catch @panic("OOM"); + apk.* = .{ .b = b, .tools = tools, + .key_store = null, .android_manifest = null, .artifacts = .{}, .java_files = .{}, - .resource_files = b.addWriteFiles(), - .key_store = null, + .resources = .{}, }; - return ab; + return apk; } /// Set the AndroidManifest.xml file to use @@ -61,7 +75,12 @@ pub const APK = struct { /// - mipmap-mdpi/ic_launcher.png /// - etc pub fn addResourceDirectory(apk: *@This(), dir: LazyPath) void { - _ = apk.resource_files.addCopyDirectory(dir, "", .{}); + const b = apk.b; + apk.resources.append(b.allocator, Resource{ + .directory = .{ + .source = dir, + }, + }) catch @panic("OOM"); } /// Add artifact to the Android build, this should be a shared library (*.so) @@ -89,7 +108,7 @@ pub const APK = struct { /// This is required run on an Android device. /// /// If you want to just use a temporary key for local development, do something like this: - /// - ab.setKeyStore(android_tools.createKeyStore(android.CreateKey.example())); + /// - apk.setKeyStore(android_tools.createKeyStore(android.CreateKey.example())); pub fn setKeyStore(apk: *@This(), key_store: KeyStore) void { apk.key_store = key_store; } @@ -203,25 +222,6 @@ pub const APK = struct { break :blk false; }; - // Make zip of compiled resource files, ie. - // - res/values/strings.xml -> values_strings.arsc.flat - // - mipmap/ic_launcher.png -> mipmap-ic_launcher.png.flat - const resources_flat_zip: LazyPath = blk: { - const aapt2compile = b.addSystemCommand(&[_][]const u8{ - apk.tools.build_tools.aapt2, - "compile", - }); - - // add directory - aapt2compile.addArg("--dir"); - aapt2compile.addDirectoryArg(apk.resource_files.getDirectory()); - - aapt2compile.addArg("-o"); - const resources_flat_zip_file = aapt2compile.addOutputFileArg("resources.flat.zip"); - aapt2compile.setName(runNameContext("aapt2 compile")); - break :blk resources_flat_zip_file; - }; - // Make resources.apk from: // - resources.flat.zip (created from "aapt2 compile") // - res/values/strings.xml -> values_strings.arsc.flat @@ -255,18 +255,24 @@ pub const APK = struct { aapt2link.addArg("--manifest"); aapt2link.addFileArg(android_manifest_file); - // TODO(jae): 2024-10-01 - // We can skip outputting to zip and extracting and just output to dir - // aapt2link.addArg("--output-to-dir"); // Requires: Android SDK Build Tools 28.0.0 or higher - // Specify output file - aapt2link.addArg("-o"); - const resources_apk_file = aapt2link.addOutputFileArg("resources.apk"); - aapt2link.addArgs(&[_][]const u8{ "--target-sdk-version", b.fmt("{d}", .{@intFromEnum(apk.tools.api_level)}), }); + // NOTE(jae): 2024-10-02 + // Explored just outputting to dir but it gets errors like: + // - error: failed to write res/mipmap-mdpi-v4/ic_launcher.png to archive: + // The system cannot find the file specified. (2). + // + // So... I'll stick with the creating an APK and extracting it approach. + // aapt2link.addArg("--output-to-dir"); // Requires: Android SDK Build Tools 28.0.0 or higher + // aapt2link.addArg("-o"); + // const resources_apk_dir = aapt2link.addOutputDirectoryArg("resources"); + + aapt2link.addArg("-o"); + const resources_apk_file = aapt2link.addOutputFileArg("resources.apk"); + // TODO(jae): 2024-09-17 // Add support for asset directories // Additional directory @@ -277,16 +283,55 @@ pub const APK = struct { // } // Add resource files - aapt2link.addFileArg(resources_flat_zip); + for (apk.resources.items) |resource| { + const resources_flat_zip = resblk: { + // Make zip of compiled resource files, ie. + // - res/values/strings.xml -> values_strings.arsc.flat + // - mipmap/ic_launcher.png -> mipmap-ic_launcher.png.flat + switch (resource) { + .directory => |resource_directory| { + const aapt2compile = b.addSystemCommand(&[_][]const u8{ + apk.tools.build_tools.aapt2, + "compile", + }); + aapt2compile.setName(runNameContext("aapt2 compile [dir]")); + + // add directory + aapt2compile.addArg("--dir"); + aapt2compile.addDirectoryArg(resource_directory.source); + + aapt2compile.addArg("-o"); + const resources_flat_zip_file = aapt2compile.addOutputFileArg("resource_dir.flat.zip"); + + break :resblk resources_flat_zip_file; + }, + } + }; + + // Add resources.flat.zip + aapt2link.addFileArg(resources_flat_zip); + } break :blk resources_apk_file; }; - // TODO(jae): 2024-10-01 - // Extract the package name from the APK and build that information - // into the Zig code - // - aapt2 dump packagename output.apk - // + const package_name_file = blk: { + const aapt2packagename = b.addSystemCommand(&[_][]const u8{ + apk.tools.build_tools.aapt2, + "dump", + "packagename", + }); + aapt2packagename.setName(runNameContext("aapt2 dump packagename")); + aapt2packagename.addFileArg(resources_apk); + break :blk aapt2packagename.captureStdOut(); + }; + + const android_builtin = blk: { + const android_builtin_options = std.Build.addOptions(b); + BuiltinOptionsUpdate.create(b, android_builtin_options, package_name_file); + break :blk android_builtin_options.createModule(); + }; + // We could also use that information to create easy to use Zig step like // - zig build adb-uninstall (adb uninstall "com.zig.sdl2") // - zig build adb-logcat @@ -308,8 +353,8 @@ pub const APK = struct { // - arm-linux-androideabi // - i686-linux-android // - x86_64-linux-android - for (apk.artifacts.items, 0..) |root_artifact, artifact_index| { - const target: ResolvedTarget = root_artifact.root_module.resolved_target orelse { + for (apk.artifacts.items, 0..) |artifact, artifact_index| { + const target: ResolvedTarget = artifact.root_module.resolved_target orelse { @panic(b.fmt("artifact[{d}] has no 'target' set", .{artifact_index})); }; @@ -321,14 +366,13 @@ pub const APK = struct { .x86 => "x86", else => @panic(b.fmt("unsupported or unhandled arch: {s}", .{@tagName(target.result.cpu.arch)})), }; - _ = apk_files.addCopyFile(root_artifact.getEmittedBin(), b.fmt("lib/{s}/libmain.so", .{so_dir})); + _ = apk_files.addCopyFile(artifact.getEmittedBin(), b.fmt("lib/{s}/libmain.so", .{so_dir})); // update artifact to: // - Be configured to work correctly on Android // - To know where C header /lib files are via setLibCFile and linkLibC // - Provide path to additional libraries to link to { - const artifact = root_artifact; if (artifact.linkage) |linkage| { if (linkage == .dynamic) { updateSharedLibraryOptions(artifact); @@ -338,6 +382,18 @@ pub const APK = struct { apk.addLibraryPaths(&artifact.root_module); artifact.linkLibC(); } + + // Add module + artifact.root_module.addImport("android_builtin", android_builtin); + + var modules_it = artifact.root_module.import_table.iterator(); + while (modules_it.next()) |entry| { + const module = entry.value_ptr.*; + if (module.import_table.get("android_builtin")) |_| { + module.addImport("android_builtin", android_builtin); + } + } + // NOTE(jae): 2024-08-09 // Try to fix compilation issues for ARM 32-bit (ie. arm-linux-androideabi) // if (target.result.cpu.arch == .arm) { @@ -350,7 +406,7 @@ pub const APK = struct { // update linked libraries that use C or C++ to: // - use Android LibC file // - add Android NDK library paths. (libandroid, liblog, etc) - apk.updateLinkObjects(root_artifact, so_dir, apk_files); + apk.updateLinkObjects(artifact, so_dir, apk_files); } // Add *.jar files @@ -424,7 +480,9 @@ pub const APK = struct { jar.addPrefixedFileArg("--file=", resources_apk); // NOTE(jae): 2024-09-30 - // Extract to directory of resources_apk and force add that to the overall apk files + // Extract to directory of resources_apk and force add that to the overall apk files. + // This currently has an issue where because we can't use "addOutputDirectoryArg" this + // step will always be executed. const extracted_apk_dir = resources_apk.dirname(); jar.setCwd(extracted_apk_dir); _ = apk_files.addCopyDirectory(extracted_apk_dir, "", .{ @@ -480,6 +538,7 @@ pub const APK = struct { var zipalign = b.addSystemCommand(&[_][]const u8{ apk.tools.build_tools.zipalign, }); + zipalign.setName(runNameContext("zipalign")); // If you use apksigner, zipalign must be used before the APK file has been signed. // If you sign your APK using apksigner and make further changes to the APK, its signature is invalidated. diff --git a/src/androidbuild/builtin_options_update.zig b/src/androidbuild/builtin_options_update.zig new file mode 100644 index 0000000..87de8e4 --- /dev/null +++ b/src/androidbuild/builtin_options_update.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const androidbuild = @import("androidbuild.zig"); +const builtin = @import("builtin"); +const Build = std.Build; +const Step = Build.Step; +const Options = Build.Step.Options; +const LazyPath = Build.LazyPath; +const fs = std.fs; +const mem = std.mem; +const assert = std.debug.assert; + +/// BuiltinOptionsUpdate will update the *Options +pub const BuiltinOptionsUpdate = struct { + pub const base_id: Step.Id = .custom; + + step: Step, + + options: *Options, + package_name_stdout: LazyPath, + + pub fn create(owner: *std.Build, options: *Options, package_name_stdout: LazyPath) void { + const builtin_options_update = owner.allocator.create(@This()) catch @panic("OOM"); + builtin_options_update.* = .{ + .step = Step.init(.{ + .id = base_id, + .name = androidbuild.runNameContext("builtin_options_update"), + .owner = owner, + .makeFn = comptime if (std.mem.eql(u8, builtin.zig_version_string, "0.13.0")) + make013 + else + makeLatest, + }), + .options = options, + .package_name_stdout = package_name_stdout, + }; + // Run step relies on this finishing + options.step.dependOn(&builtin_options_update.step); + // Depend on package name stdout before running this step + package_name_stdout.addStepDependencies(&builtin_options_update.step); + } + + /// make for zig 0.13.0 + fn make013(step: *Step, prog_node: std.Progress.Node) !void { + _ = prog_node; // autofix + try make(step); + } + + /// make for zig 0.14.0+ + fn makeLatest(step: *Step, options: Build.Step.MakeOptions) !void { + _ = options; // autofix + try make(step); + } + + fn make(step: *Step) !void { + const b = step.owner; + const builtin_options_update: *@This() = @fieldParentPtr("step", step); + const options = builtin_options_update.options; + + const package_name_path = builtin_options_update.package_name_stdout.getPath2(b, step); + + const file = try fs.openFileAbsolute(package_name_path, .{}); + + // Read package name from stdout and strip line feed / carriage return + // ie. "com.zig.sdl2\n\r" + const package_name_filedata = try file.readToEndAlloc(b.allocator, 8192); + const package_name_stripped = std.mem.trimRight(u8, package_name_filedata, " \r\n"); + const package_name: [:0]const u8 = try b.allocator.dupeZ(u8, package_name_stripped); + + options.addOption([:0]const u8, "package_name", package_name); + } +};