Skip to content

Commit 81128b5

Browse files
improve zig build --watch compatibility for BuiltinUpdateOptions step and DirectoryFileInput (#73)
- update `BuiltinUpdateOptions` to be --watch compatible by clearing the output content data. This allows you to now modify `AndroidManifest.xml` with --watch - update `DirectoryFileInput` to be --watch compatible, so now if you have an asset directory and you add/remove/edit files, the build step will re-run - fix D8Glob behaviour to also trim the `file_inputs` list and avoid growing it Fixes #71
1 parent 2e8c837 commit 81128b5

4 files changed

Lines changed: 158 additions & 60 deletions

File tree

src/androidbuild/DirectoryFileInput.zig

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
//! DirectoryFileInput adds files within a directory to the dependencies of the given Step.Run command
22
//! This is required so that generated directories will work.
33

4-
const std = @import("std");
54
const androidbuild = @import("androidbuild.zig");
65
const builtin = @import("builtin");
7-
const Build = std.Build;
6+
const Build = @import("std").Build;
87
const Step = Build.Step;
98
const Run = Build.Step.Run;
109
const LazyPath = Build.LazyPath;
11-
const fs = std.fs;
12-
const mem = std.mem;
13-
const assert = std.debug.assert;
10+
const fs = @import("std").fs;
11+
const mem = @import("std").mem;
12+
const debug = @import("std").debug;
1413

1514
step: Step,
1615

@@ -20,7 +19,15 @@ run: *Build.Step.Run,
2019
/// The directory that will contain the files to glob
2120
dir: LazyPath,
2221

23-
pub fn create(owner: *std.Build, run: *Run, dir: LazyPath) void {
22+
/// Track the files added to the Run step for --watch
23+
file_input_range: ?FileInputRange,
24+
25+
const FileInputRange = struct {
26+
start_value: []const u8,
27+
len: u32,
28+
};
29+
30+
pub fn create(owner: *Build, run: *Run, dir: LazyPath) void {
2431
const self = owner.allocator.create(DirectoryFileInput) catch @panic("OOM");
2532
self.* = .{
2633
.step = Step.init(.{
@@ -31,19 +38,47 @@ pub fn create(owner: *std.Build, run: *Run, dir: LazyPath) void {
3138
}),
3239
.run = run,
3340
.dir = dir,
41+
.file_input_range = null,
3442
};
3543
// Run step relies on DirectoryFileInput finishing
3644
run.step.dependOn(&self.step);
3745
// If dir is generated then this will wait for that dir to generate
3846
dir.addStepDependencies(&self.step);
3947
}
4048

41-
fn make(step: *Step, _: Build.Step.MakeOptions) !void {
49+
fn make(step: *Step, options: Build.Step.MakeOptions) !void {
4250
const b = step.owner;
51+
const gpa = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14)
52+
// Deprecated: Zig 0.14.X doesn't have options.gpa
53+
b.allocator
54+
else
55+
options.gpa;
4356
const arena = b.allocator;
4457
const self: *DirectoryFileInput = @fieldParentPtr("step", step);
45-
4658
const run = self.run;
59+
60+
// Add the directory to --watch input so that if any files are updated or changed
61+
// this step will re-trigger
62+
const need_derived_inputs = try step.addDirectoryWatchInput(self.dir);
63+
64+
// triggers on --watch if a file is modified.
65+
if (self.file_input_range) |file_input_range| {
66+
const start_index: usize = blk: {
67+
for (run.file_inputs.items, 0..) |lp, file_input_index| {
68+
switch (lp) {
69+
.cwd_relative => |cwd_relative| {
70+
if (mem.eql(u8, file_input_range.start_value, cwd_relative)) {
71+
break :blk file_input_index;
72+
}
73+
},
74+
else => continue,
75+
}
76+
}
77+
return error.MissingFileInputWatchArgument;
78+
};
79+
try run.file_inputs.replaceRange(run.step.owner.allocator, start_index, file_input_range.len, &.{});
80+
}
81+
4782
const dir_path = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15)
4883
self.dir.getPath3(b, step)
4984
else
@@ -60,19 +95,43 @@ fn make(step: *Step, _: Build.Step.MakeOptions) !void {
6095
else
6196
dir.close(b.graph.io);
6297

98+
var optional_file_input_value: ?[]const u8 = null;
99+
var optional_file_input_start_index: ?usize = null;
63100
var walker = try dir.walk(arena);
64101
defer walker.deinit();
65102
while (if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15)
66103
try walker.next()
67104
else
68105
try walker.next(b.graph.io)) |entry|
69106
{
70-
if (entry.kind != .file) continue;
71-
72-
// Add file as dependency to run command
73-
run.addFileInput(LazyPath{
74-
.cwd_relative = try dir_path.root_dir.join(b.allocator, &.{ dir_path.sub_path, entry.path }),
75-
});
107+
switch (entry.kind) {
108+
.directory => {
109+
if (need_derived_inputs) {
110+
const entry_path = try dir_path.join(arena, entry.path);
111+
try step.addDirectoryWatchInputFromPath(entry_path);
112+
}
113+
},
114+
.file => {
115+
// Add file as dependency to run command
116+
const file_path = try dir_path.root_dir.join(gpa, &.{ dir_path.sub_path, entry.path });
117+
if (optional_file_input_value == null) {
118+
// Set index and value of first file
119+
optional_file_input_start_index = run.file_inputs.items.len;
120+
optional_file_input_value = file_path;
121+
}
122+
run.addFileInput(LazyPath{
123+
.cwd_relative = file_path,
124+
});
125+
},
126+
else => continue,
127+
}
128+
}
129+
if (optional_file_input_value) |file_input_value| {
130+
const file_input_start_index = optional_file_input_start_index orelse unreachable;
131+
self.file_input_range = .{
132+
.start_value = file_input_value,
133+
.len = @intCast(run.file_inputs.items.len - file_input_start_index),
134+
};
76135
}
77136
}
78137

src/androidbuild/apk.zig

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,8 +423,7 @@ fn doInstallApk(apk: *Apk) Allocator.Error!*Step.InstallFile {
423423
};
424424

425425
const android_builtin = blk: {
426-
const android_builtin_options = std.Build.addOptions(b);
427-
BuiltinOptionsUpdate.create(b, android_builtin_options, package_name_file);
426+
const android_builtin_options = BuiltinOptionsUpdate.create(b, package_name_file);
428427
break :blk android_builtin_options.createModule();
429428
};
430429

src/androidbuild/builtin_options_update.zig

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
//! BuiltinOptionsUpdate will update the *Options
22

3-
const std = @import("std");
43
const androidbuild = @import("androidbuild.zig");
54
const builtin = @import("builtin");
6-
const Build = std.Build;
5+
const Build = @import("std").Build;
76
const Step = Build.Step;
87
const Options = Build.Step.Options;
98
const LazyPath = Build.LazyPath;
10-
const fs = std.fs;
11-
const mem = std.mem;
12-
const assert = std.debug.assert;
9+
const fs = @import("std").fs;
10+
const mem = @import("std").mem;
11+
const assert = @import("std").debug.assert;
1312

1413
pub const base_id: Step.Id = .custom;
1514

@@ -18,7 +17,9 @@ step: Step,
1817
options: *Options,
1918
package_name_stdout: LazyPath,
2019

21-
pub fn create(owner: *std.Build, options: *Options, package_name_stdout: LazyPath) void {
20+
pub fn create(owner: *Build, package_name_stdout: LazyPath) *BuiltinOptionsUpdate {
21+
const options = Build.addOptions(owner);
22+
2223
const builtin_options_update = owner.allocator.create(BuiltinOptionsUpdate) catch @panic("OOM");
2324
builtin_options_update.* = .{
2425
.step = Step.init(.{
@@ -34,13 +35,26 @@ pub fn create(owner: *std.Build, options: *Options, package_name_stdout: LazyPat
3435
options.step.dependOn(&builtin_options_update.step);
3536
// Depend on package name stdout before running this step
3637
package_name_stdout.addStepDependencies(&builtin_options_update.step);
38+
return builtin_options_update;
39+
}
40+
41+
pub fn createModule(self: *BuiltinOptionsUpdate) *Build.Module {
42+
return self.options.createModule();
3743
}
3844

3945
fn make(step: *Step, _: Build.Step.MakeOptions) !void {
4046
const b = step.owner;
4147
const builtin_options_update: *BuiltinOptionsUpdate = @fieldParentPtr("step", step);
4248
const options = builtin_options_update.options;
4349

50+
// If using --watch and the user updated AndroidManifest.xml, this step can be re-triggered.
51+
//
52+
// To avoid appending multiple "package_name = " lines to the output module, we need to clear it if
53+
// the options step has any contents
54+
if (options.contents.items.len > 0) {
55+
options.contents.clearRetainingCapacity();
56+
}
57+
4458
const package_name_path = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15)
4559
builtin_options_update.package_name_stdout.getPath3(b, step)
4660
else
@@ -56,9 +70,9 @@ fn make(step: *Step, _: Build.Step.MakeOptions) !void {
5670
else
5771
try package_name_path.root_dir.handle.readFile(b.graph.io, package_name_path.sub_path, package_name_backing_buf);
5872
const package_name_stripped = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14)
59-
std.mem.trimRight(u8, package_name_filedata, " \r\n")
73+
mem.trimRight(u8, package_name_filedata, " \r\n")
6074
else
61-
std.mem.trimEnd(u8, package_name_filedata, " \r\n");
75+
mem.trimEnd(u8, package_name_filedata, " \r\n");
6276
const package_name: [:0]const u8 = try b.allocator.dupeZ(u8, package_name_stripped);
6377

6478
options.addOption([:0]const u8, "package_name", package_name);

src/androidbuild/d8glob.zig

Lines changed: 62 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ argv_range: ?ArgRange,
2727
const file_ext = ".class";
2828

2929
const ArgRange = struct {
30-
start: usize,
31-
end: usize,
30+
argv_start: usize,
31+
file_input_start: usize,
32+
len: u32,
3233
};
3334

3435
/// Creates a D8Glob step which is used to collect all *.class output files after a javac process generates them
@@ -57,21 +58,28 @@ fn make(step: *Step, options: Build.Step.MakeOptions) !void {
5758
_ = options;
5859
const b = step.owner;
5960
const arena = b.allocator;
60-
const gpa = b.allocator;
61-
const glob: *@This() = @fieldParentPtr("step", step);
61+
const self: *D8Glob = @fieldParentPtr("step", step);
6262

63-
const d8 = glob.run;
63+
const d8 = self.run;
64+
65+
// NOTE(jae): 2026-03-01
66+
// This step uses the output of various Java files from a .zig-cache folder so
67+
// in theory we shouldn't need to handle addDirectoryWatchInput
68+
//
69+
// ie. .zig-cache/o/078c6c3d6e14d425d58e182be74b7006/android_classes
70+
// const need_derived_inputs = try step.addDirectoryWatchInput(self.dir);
6471

6572
// Triggers on --watch if a Java file is modified.
6673
// For example: ZigSDLActivity.java
67-
if (glob.argv_range) |argv_range| {
68-
try d8.argv.replaceRange(d8.step.owner.allocator, argv_range.start, argv_range.end - argv_range.start, &.{});
74+
if (self.argv_range) |argv_range| {
75+
try d8.file_inputs.replaceRange(d8.step.owner.allocator, argv_range.file_input_start, argv_range.len, &.{});
76+
try d8.argv.replaceRange(d8.step.owner.allocator, argv_range.argv_start, argv_range.len, &.{});
6977
}
7078

7179
const search_dir = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15)
72-
glob.dir.getPath3(b, step)
80+
self.dir.getPath3(b, step)
7381
else
74-
try glob.dir.getPath4(b, step);
82+
try self.dir.getPath4(b, step);
7583

7684
// NOTE(jae): 2024-09-22
7785
// Change current working directory to where the Java classes are
@@ -83,7 +91,7 @@ fn make(step: *Step, options: Build.Step.MakeOptions) !void {
8391
// A deeper fix to this problem could be:
8492
// - Zip up all the *.class files and just provide that as ONE argument or alternatively
8593
// - If "d8" has the ability to pass a file of command line parameters, that would work too but I haven't seen any in the docs
86-
d8.setCwd(glob.dir);
94+
d8.setCwd(self.dir);
8795

8896
// NOTE(jae): 2025-07-23
8997
// 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
@@ -97,44 +105,62 @@ fn make(step: *Step, options: Build.Step.MakeOptions) !void {
97105
dir.close(b.graph.io);
98106

99107
var optional_argv_start: ?usize = null;
108+
var optional_file_input_start: ?usize = null;
100109
var walker = try dir.walk(arena);
101110
defer walker.deinit();
102111
while (if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 15)
103112
try walker.next()
104113
else
105114
try walker.next(b.graph.io)) |entry|
106115
{
107-
if (entry.kind != .file) {
108-
continue;
109-
}
110-
// NOTE(jae): 2024-10-01
111-
// Initially ignored classes with alternate API postfixes / etc but
112-
// that did not work with SDL2 so no longer do that.
113-
// - !std.mem.containsAtLeast(u8, entry.basename, 1, "$") and
114-
// - !std.mem.containsAtLeast(u8, entry.basename, 1, "_API")
115-
if (std.mem.endsWith(u8, entry.path, file_ext)) {
116-
const absolute_file_path = try search_dir.root_dir.join(gpa, &.{ search_dir.sub_path, entry.path });
117-
const relative_to_dir_path = absolute_file_path[search_dir.sub_path.len + 1 ..];
118-
// NOTE(jae): 2024-09-22
119-
// We set the current working directory to "glob.Dir" and then make arguments be
120-
// relative to that directory.
121-
//
122-
// This is to avoid the Java error "command line too long" that can occur with d8
123-
if (optional_argv_start == null) {
124-
optional_argv_start = d8.argv.items.len;
125-
}
126-
d8.addFileInput(LazyPath{
127-
.cwd_relative = absolute_file_path,
128-
});
129-
d8.addArg(relative_to_dir_path);
116+
switch (entry.kind) {
117+
.directory => {
118+
// NOTE(jae): 2026-03-01
119+
// This step uses the output of various Java files from a .zig-cache folder so
120+
// in theory we shouldn't need to handle addDirectoryWatchInput
121+
//
122+
// if (need_derived_inputs) {
123+
// const entry_path = try search_dir.join(arena, entry.path);
124+
// try step.addDirectoryWatchInputFromPath(entry_path);
125+
// }
126+
},
127+
.file => {
128+
// NOTE(jae): 2024-10-01
129+
// Initially ignored classes with alternate API postfixes / etc but
130+
// that did not work with SDL2 so no longer do that.
131+
// - !std.mem.containsAtLeast(u8, entry.basename, 1, "$") and
132+
// - !std.mem.containsAtLeast(u8, entry.basename, 1, "_API")
133+
if (std.mem.endsWith(u8, entry.path, file_ext)) {
134+
const absolute_file_path = try search_dir.root_dir.join(arena, &.{ search_dir.sub_path, entry.path });
135+
const relative_to_dir_path = absolute_file_path[search_dir.sub_path.len + 1 ..];
136+
// NOTE(jae): 2024-09-22
137+
// We set the current working directory to "glob.Dir" and then make arguments be
138+
// relative to that directory.
139+
//
140+
// This is to avoid the Java error "command line too long" that can occur with d8
141+
if (optional_argv_start == null) {
142+
optional_argv_start = d8.argv.items.len;
143+
optional_file_input_start = d8.file_inputs.items.len;
144+
}
145+
d8.addArg(relative_to_dir_path);
146+
d8.addFileInput(LazyPath{
147+
.cwd_relative = absolute_file_path,
148+
});
149+
}
150+
},
151+
else => continue,
130152
}
131153
}
132154

133155
// Track arguments added to "d8" so that we can remove them if "make" is re-run in --watch mode
134156
if (optional_argv_start) |argv_start| {
135-
glob.argv_range = .{
136-
.start = argv_start,
137-
.end = d8.argv.items.len,
157+
const file_input_start = optional_file_input_start orelse unreachable;
158+
const len: u32 = @intCast(d8.argv.items.len - argv_start);
159+
assert(len == d8.file_inputs.items.len - file_input_start);
160+
self.argv_range = .{
161+
.argv_start = argv_start,
162+
.file_input_start = file_input_start,
163+
.len = len,
138164
};
139165
}
140166
}

0 commit comments

Comments
 (0)