feat: add --watch argument

This commit is contained in:
LordMZTE 2024-07-02 12:20:18 +02:00
parent 190dfa2695
commit 5a88ad31cf
Signed by untrusted user: LordMZTE
GPG key ID: B64802DC33A64FF6
8 changed files with 287 additions and 48 deletions

View file

@ -1,6 +1,6 @@
.{
.name = "confgen",
.version = "0.4.1",
.version = "0.5.0",
.dependencies = .{
.zig_args = .{

122
confgen/Notifier.zig Normal file
View file

@ -0,0 +1,122 @@
const std = @import("std");
const sigset = sigs: {
var set = std.posix.empty_sigset;
std.os.linux.sigaddset(&set, std.posix.SIG.INT);
std.os.linux.sigaddset(&set, std.posix.SIG.TERM);
break :sigs set;
};
inotifyfd: std.os.linux.fd_t,
sigfd: std.os.linux.fd_t,
watches: WatchesMap,
inotifyrd: std.io.BufferedReader(1024 * 4, std.fs.File.Reader),
const WatchesMap = std.AutoHashMap(i32, []const u8);
const Notifier = @This();
pub const Event = union(enum) {
quit,
file_changed: []const u8,
};
pub fn init(alloc: std.mem.Allocator) !Notifier {
std.posix.sigprocmask(std.posix.SIG.BLOCK, &sigset, null);
const inotifyfd = try std.posix.inotify_init1(0);
errdefer std.posix.close(inotifyfd);
const sigfd = try std.posix.signalfd(-1, &sigset, 0);
errdefer std.posix.close(sigfd);
return .{
.inotifyfd = inotifyfd,
.sigfd = sigfd,
.watches = WatchesMap.init(alloc),
.inotifyrd = std.io.bufferedReader((std.fs.File{ .handle = inotifyfd }).reader()),
};
}
pub fn deinit(self: *Notifier) void {
std.posix.sigprocmask(std.posix.SIG.UNBLOCK, &sigset, null);
std.posix.close(self.inotifyfd);
std.posix.close(self.sigfd);
var w_iter = self.watches.iterator();
while (w_iter.next()) |wkv| {
self.watches.allocator.free(wkv.value_ptr.*);
}
self.watches.deinit();
}
pub fn addDir(self: *Notifier, dirname: []const u8) !void {
const fd = std.posix.inotify_add_watch(
self.inotifyfd,
dirname,
std.os.linux.IN.MASK_CREATE | std.os.linux.IN.ONLYDIR | std.os.linux.IN.CLOSE_WRITE,
) catch |e| switch (e) {
error.WatchAlreadyExists => return,
else => return e,
};
errdefer std.posix.inotify_rm_watch(self.inotifyfd, fd);
const dir_d = try self.watches.allocator.dupe(u8, dirname);
errdefer self.watches.allocator.free(dir_d);
// SAFETY: This cannot cause UB. We have checked if the dir is already watched.
std.debug.assert(!self.watches.contains(fd));
try self.watches.putNoClobber(fd, dir_d);
}
/// Caller must free returned memory.
pub fn next(self: *Notifier) !Event {
var pollfds = [2]std.posix.pollfd{
.{ .fd = self.inotifyfd, .events = std.posix.POLL.IN, .revents = 0 },
.{ .fd = self.sigfd, .events = std.posix.POLL.IN, .revents = 0 },
};
const pending_data = self.inotifyrd.start != self.inotifyrd.end;
if (!pending_data)
_ = try std.posix.poll(&pollfds, -1);
if (pending_data or pollfds[0].revents == std.posix.POLL.IN) {
var ev: std.os.linux.inotify_event = undefined;
try self.inotifyrd.reader().readNoEof(std.mem.asBytes(&ev));
// The inotify_event struct is optionally followed by ev.len bytes for the path name of
// the watched file. We must read them here to avoid clobbering the next event.
var name_buf: [std.fs.max_path_bytes]u8 = undefined;
std.debug.assert(ev.len <= name_buf.len);
if (ev.len > 0)
try self.inotifyrd.reader().readNoEof(name_buf[0..ev.len]);
const dirpath = self.watches.get(ev.wd) orelse
@panic("inotifyfd returned invalid handle");
// Required as padding bytes may be included in read value
const name = std.mem.sliceTo(&name_buf, 0);
return .{
.file_changed =
// This avoids inconsistent naming in the edge-case that we're observing the CWD
if (std.mem.eql(u8, dirpath, "."))
try self.watches.allocator.dupe(u8, name)
else
try std.fs.path.join(
self.watches.allocator,
&.{ dirpath, name },
),
};
}
if (pollfds[1].revents == std.posix.POLL.IN) {
var ev: std.os.linux.signalfd_siginfo = undefined;
std.debug.assert(try std.posix.read(self.sigfd, std.mem.asBytes(&ev)) ==
@sizeOf(std.os.linux.signalfd_siginfo));
return .quit;
}
@panic("poll returned incorrectly");
}

View file

@ -2,6 +2,8 @@ const std = @import("std");
const args = @import("args");
const libcg = @import("libcg");
const Notifier = @import("Notifier.zig");
comptime {
if (@import("builtin").is_test) {
std.testing.refAllDeclsRecursive(@This());
@ -23,6 +25,7 @@ const Args = struct {
help: bool = false,
eval: ?[]const u8 = null,
@"post-eval": ?[]const u8 = null,
watch: bool = false,
pub const shorthands = .{
.c = "compile",
@ -31,6 +34,7 @@ const Args = struct {
.h = "help",
.e = "eval",
.p = "post-eval",
.w = "watch",
};
};
@ -42,9 +46,10 @@ const usage =
\\ --compile, -c [TEMPLATE_FILE] Compile a template to Lua instead of running. Useful for debugging.
\\ --json-opt, -j [CONFGENFILE] Write the given or all fields from cg.opt to stdout as JSON after running the given confgenfile instead of running.
\\ --file, -f [TEMPLATE_FILE] [OUTFILE] Evaluate a single template and write the output instead of running.
\\ --eval, -e [CODE] Evaluate code before the confgenfile
\\ --post-eval, -p [CODE] Evaluate code after the confgenfile
\\ --help, -h Show this help
\\ --eval, -e [CODE] Evaluate code before the confgenfile .
\\ --post-eval, -p [CODE] Evaluate code after the confgenfile.
\\ --watch, -w Watch for changes of input files and re-generate them if changed.
\\ --help, -h Show this help.
\\
\\Usage:
\\ confgen [CONFGENFILE] [OUTPATH] Generate configs according the the supplied configuration file.
@ -177,24 +182,49 @@ pub fn run() !void {
const l = try libcg.luaapi.initLuaState(&state);
defer libcg.c.lua_close(l);
const tmplsrc = try std.fs.cwd().readFileAlloc(
alloc,
arg.positionals[0],
std.math.maxInt(usize),
);
const tmplcode = try libcg.luagen.generateLua(
alloc,
tmplsrc,
arg.positionals[0],
);
const genf = try libcg.luaapi.generate(l, tmplcode);
defer alloc.free(genf.content);
var content_buf = std.ArrayList(u8).init(alloc);
defer content_buf.deinit();
const outfile = try std.fs.cwd().createFile(arg.positionals[1], .{ .mode = genf.mode });
defer outfile.close();
try outfile.writeAll(genf.content);
const cgfile = libcg.luaapi.CgFile{
.content = .{ .path = arg.positionals[0] },
.copy = false,
};
try genfile(
alloc,
l,
cgfile,
&content_buf,
".",
arg.positionals[1],
);
libcg.luaapi.callOnDoneCallbacks(l, false);
if (arg.options.watch) {
var notif = try Notifier.init(alloc);
defer notif.deinit();
try notif.addDir(std.fs.path.dirname(arg.positionals[0]) orelse ".");
while (true) switch (try notif.next()) {
.quit => break,
.file_changed => |p| {
defer alloc.free(p);
if (!std.mem.eql(u8, p, arg.positionals[0])) continue;
genfile(
alloc,
l,
cgfile,
&content_buf,
".",
arg.positionals[1],
) catch |e| {
std.log.err("generating {s}: {}", .{ arg.positionals[1], e });
};
},
};
}
return;
}
@ -228,7 +258,7 @@ pub fn run() !void {
try std.posix.chdir(state.rootpath);
const l = try libcg.luaapi.initLuaState(&state);
var l = try libcg.luaapi.initLuaState(&state);
defer libcg.c.lua_close(l);
if (arg.options.eval) |code| {
@ -244,31 +274,100 @@ pub fn run() !void {
var content_buf = std.ArrayList(u8).init(alloc);
defer content_buf.deinit();
var errors = false;
var iter = state.files.iterator();
while (iter.next()) |kv| {
const outpath = kv.key_ptr.*;
const file = kv.value_ptr.*;
{
var errors = false;
var iter = state.files.iterator();
while (iter.next()) |kv| {
const outpath = kv.key_ptr.*;
const file = kv.value_ptr.*;
if (file.copy) {
std.log.info("copying {s}", .{outpath});
} else {
std.log.info("generating {s}", .{outpath});
genfile(
alloc,
l,
file,
&content_buf,
output_abs,
outpath,
) catch |e| {
errors = true;
std.log.err("generating {s}: {}", .{ outpath, e });
};
}
genfile(
alloc,
l,
file,
&content_buf,
output_abs,
outpath,
) catch |e| {
errors = true;
std.log.err("generating {s}: {}", .{ outpath, e });
};
libcg.luaapi.callOnDoneCallbacks(l, errors);
}
libcg.luaapi.callOnDoneCallbacks(l, errors);
if (arg.options.watch) {
var notif = try Notifier.init(alloc);
defer notif.deinit();
{
try notif.addDir(std.fs.path.dirname(cgfile) orelse ".");
var iter = state.files.iterator();
while (iter.next()) |kv| {
try notif.addDir(std.fs.path.dirname(kv.key_ptr.*) orelse ".");
}
}
while (true) switch (try notif.next()) {
.quit => break,
.file_changed => |p| {
defer alloc.free(p);
if (std.mem.eql(u8, p, cgfile)) {
std.log.info("Confgenfile changed; re-evaluating", .{});
// Destroy Lua state
libcg.c.lua_close(l);
l = try libcg.luaapi.initLuaState(&state);
// Reset CgState
state.nfile_iters = 0; // old Lua state is dead, so no iterators.
{
var iter = state.files.iterator();
while (iter.next()) |kv| {
alloc.free(kv.key_ptr.*);
kv.value_ptr.deinit(alloc);
}
state.files.clearRetainingCapacity();
}
// Evaluate cgfile and eval args
if (arg.options.eval) |code| {
try libcg.luaapi.evalUserCode(l, code);
}
try libcg.luaapi.loadCGFile(l, cgfile.ptr);
if (arg.options.@"post-eval") |code| {
try libcg.luaapi.evalUserCode(l, code);
}
continue;
}
// We need to iterate here because the key of the map corresponds to the file's
// output path. The input path may be entirely different.
var iter = state.files.iterator();
while (iter.next()) |kv| {
if (kv.value_ptr.content != .path) continue;
if (std.mem.eql(u8, kv.value_ptr.content.path, p)) {
genfile(
alloc,
l,
kv.value_ptr.*,
&content_buf,
output_abs,
kv.key_ptr.*,
) catch |e| {
std.log.err("generating {s}: {}", .{ p, e });
};
}
}
},
};
}
}
fn genfile(
@ -281,6 +380,12 @@ fn genfile(
) !void {
const state = libcg.luaapi.getState(l);
if (file.copy) {
std.log.info("copying {s}", .{file_outpath});
} else {
std.log.info("generating {s}", .{file_outpath});
}
if (file.copy) {
const to_path = try std.fs.path.join(
alloc,
@ -321,7 +426,7 @@ fn genfile(
.string => |s| content = s,
.path => |p| {
fname = std.fs.path.basename(p);
const path = try std.fs.path.join(alloc, &.{ state.rootpath, p });
const path = try std.fs.path.resolve(alloc, &.{ state.rootpath, p });
defer alloc.free(path);
const f = try std.fs.cwd().openFile(path, .{});

View file

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1718530797,
"narHash": "sha256-pup6cYwtgvzDpvpSCFh1TEUjw2zkNpk8iolbKnyFmmU=",
"lastModified": 1719690277,
"narHash": "sha256-0xSej1g7eP2kaUF+JQp8jdyNmpmCJKRpO12mKl/36Kc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b60ebf54c15553b393d144357375ea956f89e9a9",
"rev": "2741b4b489b55df32afac57bc4bfd220e8bf617e",
"type": "github"
},
"original": {

View file

@ -1,10 +1,10 @@
function __confgen_completion() {
if [ "${COMP_CWORD}" -eq 1 ]; then
COMPREPLY=($(compgen -A file -W "--compile -c --json-opt -j --help -h --file -f --eval -e --post-eval -p" -- "${COMP_WORDS[1]}"))
COMPREPLY=($(compgen -A file -W "--compile -c --json-opt -j --help -h --file -f --eval -e --post-eval -p --watch -w" -- "${COMP_WORDS[1]}"))
elif [ "${COMP_CWORD}" -eq 2 ]; then
case "${COMP_WORDS[1]}" in
"--help" | "-h") COMPREPLY=() ;;
"--compile" | "-c" | "--json-opt" | "-j" | "--file" | "-f")
"--compile" | "-c" | "--json-opt" | "-j" | "--file" | "-f" | "--watch" | "-w")
compopt -o default
COMPREPLY=()
;;

View file

@ -7,3 +7,4 @@ complete -c confgen -s j -l json-opt -d "Write the given or all fields from cg.o
complete -c confgen -s f -l file -d "Evaluate a single template and write the output instead of running"
complete -c confgen -s e -l eval -r -d "Evaluate the given lua code before loading the confgenfile"
complete -c confgen -s p -l post-eval -r -d "Evaluate the given lua code after loading the confgenfile"
complete -c confgen -s w -l watch -r -d "Watch for changes of input files and re-generate them if changed"

View file

@ -9,6 +9,7 @@
.RI [ OUTPATH ]
.RI < --eval\ [CODE] >
.RI < --post-eval\ [CODE] >
.RI < --watch >
.br
.B confgen --compile
.RI [ TEMPLATE_FILE ]
@ -97,6 +98,14 @@ Evaluate the given
after loading the
.IR confgenfile .
.TP
.B --watch
Watch for changes on
.I input files
and regenerate them when they're edited. If the
.I confgenfile
is changed, the entire state will be re-loaded.
.SH SEE ALSO
.BR confgen (3),
.BR confgen.lua (5),

View file

@ -15,7 +15,9 @@ _confgen() {
'-e[Evaluate the given lua code before loading the confgenfile]:code:' \
'--eval[Evaluate the given lua code before loading the confgenfile]:code:' \
'-p[Evaluate the given lua code after loading the confgenfile]:code:' \
'--post-eval[Evaluate the given lua code after loading the confgenfile]:code:'
'--post-eval[Evaluate the given lua code after loading the confgenfile]:code:' \
'-w[Watch for changes of input files and re-generate them if changed]' \
'--watch[Watch for changes of input files and re-generate them if changed]'
}
_confgen "$@"