Repository navigation
feat: labelle android build/run (#134) - #144
Conversation
#134) Add `labelle android` subcommand: - `labelle android build [--emulator] [--release]` — cross-compile .so - `labelle android run [--emulator]` — build + package APK + deploy via ADB Includes: - AndroidConfig struct (package_name, min/target SDK, orientation) - AndroidManifest.xml generation for NativeActivity - APK packaging via aapt + zipalign + apksigner - Auto-generated debug keystore at ~/.labelle/android-debug.keystore - ADB install + launch integration - SDK/NDK tool detection (ANDROID_HOME, PATH) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR SummaryMedium Risk Overview Introduces Reviewed by Cursor Bugbot for commit 8232939. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Code Review
This pull request introduces Android support to the CLI, including configuration structures, command-line integration, and build/deployment logic via ADB. The review identified several issues: silent error handling during asset copying and directory cleanup, an unused variable for the application name that should be used in the Android manifest, a memory leak in the ADB path discovery logic, and unreliable logic for selecting the latest Android build-tools version.
| defer allocator.free(assets_src); | ||
| const assets_dst = try std.fs.path.join(allocator, &.{ staging_dir, "assets" }); | ||
| defer allocator.free(assets_dst); | ||
| copyDirectory(allocator, assets_src, assets_dst) catch {}; |
There was a problem hiding this comment.
| const app_name = if (android_cfg.app_name.len > 0) android_cfg.app_name else cfg.title; | ||
| _ = app_name; |
| // Create APK staging directory | ||
| const staging_dir = try std.fs.path.join(allocator, &.{ target_dir, "apk-staging" }); | ||
| defer allocator.free(staging_dir); | ||
| std.fs.cwd().deleteTree(staging_dir) catch {}; |
There was a problem hiding this comment.
| \\ </application> | ||
| \\</manifest> | ||
| \\ | ||
| , .{ package_name, cfg.min_sdk_version, cfg.target_sdk_version, package_name, orientation }); |
| defer { | ||
| if (result.stdout.len == 0) allocator.free(result.stdout); | ||
| } |
There was a problem hiding this comment.
There is a memory leak here. result.stdout is only freed if its length is zero, but util.runCmd (which uses std.process.Child.run) always allocates this buffer. It should be unconditionally freed. The subsequent allocator.dupe at line 349 ensures the returned path remains valid even after result.stdout is freed.
defer allocator.free(result.stdout);
| var iter = dir.iterate(); | ||
| while (try iter.next()) |entry| { | ||
| if (entry.kind == .directory) { | ||
| if (latest) |prev| allocator.free(prev); | ||
| latest = try allocator.dupe(u8, entry.name); | ||
| } | ||
| } |
There was a problem hiding this comment.
The logic for finding the 'latest' build-tools version is unreliable because it simply picks the last entry returned by the directory iterator. Directory iteration order is not guaranteed to be sorted by version. If multiple versions are installed (e.g., 30.0.3 and 34.0.0), this might pick an older one depending on the filesystem and OS.
- propagate asset copy errors with try instead of catch {}
- pass app_name to generateAndroidManifest and use it as android:label
- add comment explaining why staging dir delete errors are silently ignored
- fix memory leak: defer-free result.stdout unconditionally in findAdb
- findBuildTools: pick highest version via std.mem.order comparison
- check keytool exit code before returning keystore path
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8232939. Configure here.
| defer allocator.free(assets_src); | ||
| const assets_dst = try std.fs.path.join(allocator, &.{ staging_dir, "assets" }); | ||
| defer allocator.free(assets_dst); | ||
| try copyDirectory(allocator, assets_src, assets_dst); |
There was a problem hiding this comment.
Missing assets directory fails Android deployment
High Severity
copyDirectory is called with try, which propagates the error and aborts deployment if the assets/ directory doesn't exist. The equivalent iOS code in ios.zig explicitly catches this error with catch {} and a comment explaining "No assets directory is fine — not all projects have assets." Projects without an assets/ directory will fail to deploy to Android even though the build is otherwise valid.
Reviewed by Cursor Bugbot for commit 8232939. Configure here.
| // Locate the built .so | ||
| const lib_name = if (emulator) "x86_64-linux-android" else "aarch64-linux-android"; | ||
| _ = lib_name; | ||
| const so_path = try std.fs.path.join(allocator, &.{ target_dir, "zig-out", "lib", "libgame.so" }); |
There was a problem hiding this comment.
Computed lib_name immediately discarded, unused in path
Medium Severity
lib_name is computed based on the emulator flag to distinguish between x86_64-linux-android and aarch64-linux-android, but is then immediately silenced with _ = lib_name. The so_path on the next line uses a hardcoded path without any architecture-specific component. This means the .so lookup path is identical for both emulator and device builds, which will likely fail to locate the correct binary if the build system outputs architecture-specific paths.
Reviewed by Cursor Bugbot for commit 8232939. Configure here.
| defer allocator.free(build_tools_dir); | ||
|
|
||
| const aapt2 = try std.fs.path.join(allocator, &.{ build_tools_dir, "aapt2" }); | ||
| defer allocator.free(aapt2); |
There was a problem hiding this comment.
Unused aapt2 variable allocated but never referenced
Low Severity
aapt2 is allocated via std.fs.path.join and freed via defer, but is never used anywhere in the function. The comment on line 211 explicitly states "Use aapt (v1) for simplicity", and only the aapt variable (line 212) is actually used in the packaging command. This is dead code that performs an unnecessary allocation.
Reviewed by Cursor Bugbot for commit 8232939. Configure here.


Summary
labelle android build [--emulator] [--release]command — cross-compiles shared library for Androidlabelle android run [--emulator]— builds + packages APK + deploys via ADBAndroidConfigstruct forproject.labelle(package_name, min/target SDK, orientation)~/.labelle/android-debug.keystoreANDROID_HOME/ PATHDepends on labelle-toolkit/labelle-assembler#3 for build.zig template generation.
Closes #134
Test plan
zig build+zig build testpasslabelle runregression — still workslabelle android buildon a project with Android SDK installedlabelle android run --emulatordeploys to emulator🤖 Generated with Claude Code