forked from LordMZTE/confgen
feat: add --watch argument
This commit is contained in:
parent
190dfa2695
commit
5a88ad31cf
8 changed files with 287 additions and 48 deletions
|
@ -1,6 +1,6 @@
|
|||
.{
|
||||
.name = "confgen",
|
||||
.version = "0.4.1",
|
||||
.version = "0.5.0",
|
||||
|
||||
.dependencies = .{
|
||||
.zig_args = .{
|
||||
|
|
122
confgen/Notifier.zig
Normal file
122
confgen/Notifier.zig
Normal 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");
|
||||
}
|
187
confgen/main.zig
187
confgen/main.zig
|
@ -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, .{});
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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=()
|
||||
;;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 "$@"
|
||||
|
|
Loading…
Reference in a new issue