From 02c17c4d71b47478c321ef704c310ee8dab02ee1 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Tue, 1 Apr 2025 08:45:20 -0700 Subject: [PATCH] make it compile --- build.zig | 131 ++++++++++++++++++++++++++++++++++----------------- src/main.zig | 20 +++----- src/root.zig | 99 ++++++++++++++++++++------------------ 3 files changed, 147 insertions(+), 103 deletions(-) diff --git a/build.zig b/build.zig index e465655..1471aa6 100644 --- a/build.zig +++ b/build.zig @@ -1,81 +1,126 @@ const std = @import("std"); +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); - // Add mvzr dependency - const mvzr_dep = b.dependency("mvzr", .{ + const mvzr_dep = b.dependency("mvzr", .{}); + // This creates a "module", which represents a collection of source files alongside + // some compilation options, such as optimization mode and linked system libraries. + // Every executable or library we compile will be based on one or more modules. + const lib_mod = b.createModule(.{ + // `root_source_file` is the Zig "entry point" of the module. If a module + // only contains e.g. external object files, you can make this `null`. + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }); - // Create the library module - const lib_mod = b.addModule("syncthing_events_lib", .{ - .source_file = .{ .path = "src/root.zig" }, - .dependencies = &.{ - .{ .name = "mvzr", .module = mvzr_dep.module("mvzr") }, - }, + lib_mod.addImport("mvzr", mvzr_dep.module("mvzr")); + + // We will also create a module for our other entry point, 'main.zig'. + const exe_mod = b.createModule(.{ + // `root_source_file` is the Zig "entry point" of the module. If a module + // only contains e.g. external object files, you can make this `null`. + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, }); - // Create the executable module - const exe_mod = b.addModule("syncthing_events_exe", .{ - .source_file = .{ .path = "src/main.zig" }, - .dependencies = &.{ - .{ .name = "syncthing_events_lib", .module = lib_mod }, - }, - }); + // Modules can depend on one another using the `std.Build.Module.addImport` function. + // This is what allows Zig source code to use `@import("foo")` where 'foo' is not a + // file path. In this case, we set up `exe_mod` to import `lib_mod`. + exe_mod.addImport("syncthing_events_lib", lib_mod); - // Create the library - const lib = b.addStaticLibrary(.{ + // Now, we will create a static library based on the module we created above. + // This creates a `std.Build.Step.Compile`, which is the build step responsible + // for actually invoking the compiler. + const lib = b.addLibrary(.{ + .linkage = .static, .name = "syncthing_events", - .root_source_file = .{ .path = "src/root.zig" }, - .target = target, - .optimize = optimize, + .root_module = lib_mod, }); - lib.addModule("mvzr", mvzr_dep.module("mvzr")); + + // This declares intent for the library to be installed into the standard + // location when the user invokes the "install" step (the default step when + // running `zig build`). b.installArtifact(lib); - // Create the executable + // This creates another `std.Build.Step.Compile`, but this one builds an executable + // rather than a static library. const exe = b.addExecutable(.{ .name = "syncthing_events", - .root_source_file = .{ .path = "src/main.zig" }, - .target = target, - .optimize = optimize, + .root_module = exe_mod, }); - exe.addModule("syncthing_events_lib", lib_mod); - b.installArtifact(exe); - // Create run command + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + const no_bin = b.option(bool, "no-bin", "skip emitting binary") orelse false; + const no_llvm = b.option(bool, "no-llvm", "skip use of llvm") orelse false; + lib.use_llvm = !no_llvm; + exe.use_llvm = !no_llvm; + if (no_bin) { + b.getInstallStep().dependOn(&exe.step); + } else { + b.installArtifact(exe); + } + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` if (b.args) |args| { run_cmd.addArgs(args); } + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); - // Create test step + // Creates a step for unit testing. This only builds the test executable + // but does not run it. const lib_unit_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/root.zig" }, - .target = target, - .optimize = optimize, + .root_module = lib_mod, }); - lib_unit_tests.addModule("mvzr", mvzr_dep.module("mvzr")); - - const exe_unit_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/main.zig" }, - .target = target, - .optimize = optimize, - }); - exe_unit_tests.addModule("syncthing_events_lib", lib_mod); const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + const exe_unit_tests = b.addTest(.{ + .root_module = exe_mod, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_lib_unit_tests.step); test_step.dependOn(&run_exe_unit_tests.step); } - - diff --git a/src/main.zig b/src/main.zig index bbe3cda..2ee82fc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,21 +9,21 @@ const Args = struct { }; pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.GeneralPurposeAllocator(.{}).init; defer _ = gpa.deinit(); const allocator = gpa.allocator(); const args = try parseArgs(allocator); - var config = try loadConfig(allocator, args.config_path); - defer config.deinit(allocator); + var parsed_config = try loadConfig(allocator, args.config_path); + defer parsed_config.deinit(); + var config = parsed_config.value; if (args.syncthing_url) |url| { config.syncthing_url = url; } var poller = try EventPoller.init(allocator, config); - defer poller.deinit(); const stdout = std.io.getStdOut().writer(); try stdout.print("Monitoring Syncthing events at {s}\n", .{config.syncthing_url}); @@ -83,7 +83,7 @@ fn parseArgs(allocator: std.mem.Allocator) !Args { return args; } -fn loadConfig(allocator: Allocator, path: []const u8) !Config { +fn loadConfig(allocator: std.mem.Allocator, path: []const u8) !std.json.Parsed(Config) { const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); @@ -93,13 +93,7 @@ fn loadConfig(allocator: Allocator, path: []const u8) !Config { const ext = std.fs.path.extension(path); if (std.mem.eql(u8, ext, ".json")) { - var parser = std.json.Parser.init(allocator, false); - defer parser.deinit(); - - var tree = try parser.parse(content); - defer tree.deinit(); - - return try std.json.parse(Config, &tree, .{ .allocator = allocator }); + return try std.json.parseFromSlice(Config, allocator, content, .{}); } else if (std.mem.eql(u8, ext, ".zon")) { // TODO: Implement ZON parsing return error.UnsupportedConfigFormat; @@ -164,4 +158,4 @@ test "config loading" { try testing.expectEqual(@as(u32, 3), config.max_retries); try testing.expectEqual(@as(u32, 2000), config.retry_delay_ms); try testing.expectEqual(@as(usize, 1), config.watchers.len); -} \ No newline at end of file +} diff --git a/src/root.zig b/src/root.zig index 0e78c45..dbc6118 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,16 +1,14 @@ const std = @import("std"); -const json = std.json; -const Allocator = std.mem.Allocator; const testing = std.testing; const mvzr = @import("mvzr"); pub const Config = struct { syncthing_url: []const u8 = "http://localhost:8384", - max_retries: u32 = 5, + max_retries: usize = std.math.maxInt(usize), retry_delay_ms: u32 = 1000, watchers: []Watcher, - pub fn deinit(self: *Config, allocator: Allocator) void { + pub fn deinit(self: *Config, allocator: std.mem.Allocator) void { for (self.watchers) |*watcher| { watcher.deinit(allocator); } @@ -22,9 +20,9 @@ pub const Watcher = struct { folder: []const u8, path_pattern: []const u8, command: []const u8, - compiled_pattern: ?mvzr.Pattern = null, + compiled_pattern: ?mvzr.Regex = null, - pub fn init(allocator: Allocator, folder: []const u8, path_pattern: []const u8, command: []const u8) !Watcher { + pub fn init(allocator: std.mem.Allocator, folder: []const u8, path_pattern: []const u8, command: []const u8) !Watcher { var watcher = Watcher{ .folder = try allocator.dupe(u8, folder), .path_pattern = try allocator.dupe(u8, path_pattern), @@ -35,7 +33,7 @@ pub const Watcher = struct { return watcher; } - pub fn deinit(self: *Watcher, allocator: Allocator) void { + pub fn deinit(self: *Watcher, allocator: std.mem.Allocator) void { if (self.compiled_pattern) |*pattern| { pattern.deinit(); } @@ -49,7 +47,7 @@ pub const Watcher = struct { return false; } if (self.compiled_pattern) |pattern| { - return pattern.match(path); + return pattern.match(path) != null; } return false; } @@ -60,8 +58,8 @@ pub const SyncthingEvent = struct { type: []const u8, folder: []const u8, path: []const u8, - - pub fn fromJson(allocator: Allocator, value: json.Value) !SyncthingEvent { + + pub fn fromJson(allocator: std.mem.Allocator, value: std.json.Value) !SyncthingEvent { return SyncthingEvent{ .id = value.object.get("id").?.integer, .type = try allocator.dupe(u8, value.object.get("type").?.string), @@ -70,7 +68,7 @@ pub const SyncthingEvent = struct { }; } - pub fn deinit(self: *SyncthingEvent, allocator: Allocator) void { + pub fn deinit(self: *SyncthingEvent, allocator: std.mem.Allocator) void { allocator.free(self.type); allocator.free(self.folder); allocator.free(self.path); @@ -78,36 +76,44 @@ pub const SyncthingEvent = struct { }; pub const EventPoller = struct { - allocator: Allocator, + allocator: std.mem.Allocator, config: Config, - last_id: i64, - client: std.http.Client, + last_id: ?i64, - pub fn init(allocator: Allocator, config: Config) !EventPoller { - return EventPoller{ + pub fn init(allocator: std.mem.Allocator, config: Config) !EventPoller { + return .{ .allocator = allocator, .config = config, - .last_id = 0, - .client = std.http.Client.init(allocator), + .last_id = null, }; } - pub fn deinit(self: *EventPoller) void { - self.client.deinit(); - } - pub fn poll(self: *EventPoller) ![]SyncthingEvent { - var retry_count: u32 = 0; + var client = std.http.Client{ .allocator = self.allocator }; + var arena = std.heap.ArenaAllocator.init(self.allocator); + defer arena.deinit(); + const aa = arena.allocator(); + try client.initDefaultProxies(aa); + + var retry_count: usize = self.config.max_retries; while (retry_count < self.config.max_retries) : (retry_count += 1) { - var url_buf: [256]u8 = undefined; - const url = try std.fmt.bufPrint(&url_buf, "{s}/rest/events?events=ItemFinished&since={d}", .{ - self.config.syncthing_url, self.last_id, + var url_buf: [1024]u8 = undefined; + var since_buf: [100]u8 = undefined; + const since = if (self.last_id) |id| + try std.fmt.bufPrint(&since_buf, "&since={d}", .{id}) + else + ""; + const url = try std.fmt.bufPrint(&url_buf, "{s}/rest/events?events=ItemFinished{s}", .{ + self.config.syncthing_url, since, }); - var events = std.ArrayList(SyncthingEvent).init(self.allocator); - errdefer events.deinit(); + var al = std.ArrayList(u8).init(self.allocator); + defer al.deinit(); - var response = self.client.get(url) catch |err| { + const response = client.fetch(.{ + .location = .{ .url = url }, + .response_storage = .{ .dynamic = &al }, + }) catch |err| { std.log.err("HTTP request failed: {s}", .{@errorName(err)}); if (retry_count + 1 < self.config.max_retries) { std.time.sleep(self.config.retry_delay_ms * std.time.ns_per_ms); @@ -115,10 +121,9 @@ pub const EventPoller = struct { } return err; }; - defer response.deinit(); - if (response.status_code != 200) { - std.log.err("HTTP status code: {d}", .{response.status_code}); + if (response.status != .ok) { + std.log.err("HTTP status code: {}", .{response.status}); if (retry_count + 1 < self.config.max_retries) { std.time.sleep(self.config.retry_delay_ms * std.time.ns_per_ms); continue; @@ -126,39 +131,39 @@ pub const EventPoller = struct { return error.HttpError; } - var parser = json.Parser.init(self.allocator, false); - defer parser.deinit(); + var events = std.ArrayList(SyncthingEvent).init(self.allocator); + errdefer events.deinit(); - var tree = try parser.parse(response.body); - defer tree.deinit(); + const parsed = try std.json.parseFromSliceLeaky(std.json.Value, aa, al.items, .{}); - const array = tree.root.array; + const array = parsed.array; for (array.items) |item| { const event = try SyncthingEvent.fromJson(self.allocator, item); try events.append(event); - if (event.id > self.last_id) { + if (self.last_id == null or event.id > self.last_id.?) { self.last_id = event.id; } } - return events.toOwnedSlice(); + return try events.toOwnedSlice(); } return error.MaxRetriesExceeded; } }; -pub fn executeCommand(allocator: Allocator, command: []const u8, event: SyncthingEvent) !void { - var expanded_cmd = try expandCommandVariables(allocator, command, event); +pub fn executeCommand(allocator: std.mem.Allocator, command: []const u8, event: SyncthingEvent) !void { + const expanded_cmd = try expandCommandVariables(allocator, command, event); defer allocator.free(expanded_cmd); - var process = std.ChildProcess.init(&[_][]const u8{ "sh", "-c", expanded_cmd }, allocator); + // TODO: Should this spawn sh like this, or exec directly? + var process = std.process.Child.init(&[_][]const u8{ "sh", "-c", expanded_cmd }, allocator); process.stdout_behavior = .Inherit; process.stderr_behavior = .Inherit; _ = try process.spawnAndWait(); } -fn expandCommandVariables(allocator: Allocator, command: []const u8, event: SyncthingEvent) ![]const u8 { +fn expandCommandVariables(allocator: std.mem.Allocator, command: []const u8, event: SyncthingEvent) ![]const u8 { var result = std.ArrayList(u8).init(allocator); defer result.deinit(); @@ -203,13 +208,13 @@ test "config parsing" { \\} ; - var parser = json.Parser.init(testing.allocator, false); + var parser = std.json.Parser.init(testing.allocator, false); defer parser.deinit(); var tree = try parser.parse(config_json); defer tree.deinit(); - const config = try json.parse(Config, &tree, .{ .allocator = testing.allocator }); + const config = try std.json.parse(Config, &tree, .{ .allocator = testing.allocator }); defer config.deinit(testing.allocator); try testing.expectEqualStrings("http://test:8384", config.syncthing_url); @@ -231,7 +236,7 @@ test "event parsing" { \\} ; - var parser = json.Parser.init(testing.allocator, false); + var parser = std.json.Parser.init(testing.allocator, false); defer parser.deinit(); var tree = try parser.parse(event_json); @@ -277,4 +282,4 @@ test "watcher pattern matching" { try testing.expect(watcher.matches("photos", "test.jpeg")); try testing.expect(!watcher.matches("photos", "test.png")); try testing.expect(!watcher.matches("documents", "test.jpg")); -} \ No newline at end of file +}