Compare commits

...

13 commits

27 changed files with 624 additions and 111 deletions

View file

@ -0,0 +1,35 @@
on:
push:
branches:
- master
jobs:
push-nix:
runs-on: docker-x86_64
steps:
- name: Install Packages
run: |
apt update
apt install -y sudo
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v27
with:
extra_nix_config: |
substituters = https://nix.mzte.de/mzte https://cache.nixos.org
trusted-public-keys = mzte:nH2vGx119m6VyU6we113jgo+RVEBlj+1oYWvOXcwGFM= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
- name: Build Derivation
run: |
nix build
- name: Install & Push to Attic
run: |
# Cached in MZTE cache
nix profile install github:zhaofengli/attic
attic login mzte https://nix.mzte.de ${{ secrets.ATTIC_TOKEN }}
attic push mzte result

View file

@ -13,7 +13,7 @@ jobs:
- name: Setup Zig
uses: https://github.com/goto-bus-stop/setup-zig@v2
with:
version: 0.12.0
version: 0.13.0
- name: Setup Packages
run: |

View file

@ -1,4 +1,4 @@
on: [push]
on: [push, pull_request]
jobs:
test:
@ -10,7 +10,7 @@ jobs:
- name: Setup Zig
uses: https://github.com/goto-bus-stop/setup-zig@v2
with:
version: 0.12.0
version: 0.13.0
- name: Setup Packages
run: |

5
.gitignore vendored
View file

@ -1,5 +1,4 @@
zig-cache/
.zig-cache/
zig-out/
deps.zig
gyro.lock
.gyro
result*/

View file

@ -1,9 +1,20 @@
# confgen
# Confgen
confgen is a tool to generate config files using a custom template language.
Confgen is a tool to generate config files using a custom template language.
The system is backed by a Lua runtime that is used to configure the tool.
## Installation
You can find binaries built on a Debian box in the releases tab, or build yourself like any other
Zig project.
### Nix
This project includes a Nix flake you can depend on. Additionally, the Confgen derivation as well as
derivations of other projects of mine are automatically built and pushed to an attic cache at
`https://nix.mzte.de/mzte` on every commit.
## Usage
Start by creating `confgen.lua` in your dotfiles. It should look something like this:

View file

@ -16,7 +16,7 @@ pub fn build(b: *std.Build) void {
}).module("args");
const libcg = b.createModule(.{
.root_source_file = .{ .path = "libcg/main.zig" },
.root_source_file = b.path("libcg/main.zig"),
.link_libc = true,
.target = target,
.optimize = optimize,
@ -28,7 +28,7 @@ pub fn build(b: *std.Build) void {
const confgen_exe = b.addExecutable(.{
.name = "confgen",
.root_source_file = .{ .path = "confgen/main.zig" },
.root_source_file = b.path("confgen/main.zig"),
.target = target,
.optimize = optimize,
});
@ -39,7 +39,7 @@ pub fn build(b: *std.Build) void {
b.installArtifact(confgen_exe);
b.installDirectory(.{
.source_dir = .{ .path = "share" },
.source_dir = b.path("share"),
.install_dir = .{ .custom = "share" },
.install_subdir = ".",
});
@ -54,7 +54,7 @@ pub fn build(b: *std.Build) void {
run_confgen_step.dependOn(&run_confgen_cmd.step);
const exe_confgen_tests = b.addTest(.{
.root_source_file = .{ .path = "confgen/main.zig" },
.root_source_file = b.path("confgen/main.zig"),
.link_libc = true,
.target = target,
.optimize = optimize,
@ -65,7 +65,7 @@ pub fn build(b: *std.Build) void {
if (confgenfs) {
const confgenfs_exe = b.addExecutable(.{
.name = "confgenfs",
.root_source_file = .{ .path = "confgenfs/main.zig" },
.root_source_file = b.path("confgenfs/main.zig"),
.link_libc = true,
.target = target,
.optimize = optimize,

View file

@ -1,11 +1,11 @@
.{
.name = "confgen",
.version = "0.3.1",
.version = "0.5.0",
.dependencies = .{
.zig_args = .{
.url = "git+https://git.mzte.de/mirrors/zig-args.git#89f18a104d9c13763b90e97d6b4ce133da8a3e2b",
.hash = "12203ded54c85878eea7f12744066dcb4397177395ac49a7b2aa365bf6047b623829",
.url = "git+https://git.mzte.de/mirrors/zig-args.git#872272205d95bdba33798c94e72c5387a31bc806",
.hash = "1220fe6ae56b668cc4a033282b5f227bfbb46a67ede6d84e9f9493fea9de339b5f37",
},
},

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

@ -582,13 +582,13 @@ fn generateOptsJSON(self: *FileSystem) ![]const u8 {
fn eval(self: *FileSystem, code: []const u8) !void {
if (libcg.c.luaL_loadbuffer(self.l, code.ptr, code.len, "<cgfs-eval>") != 0) {
std.log.err("unable to load eval code: {s}", .{libcg.ffi.luaToString(self.l, -1)});
std.log.err("unable to load eval code: {?s}", .{libcg.ffi.luaToString(self.l, -1)});
libcg.c.lua_pop(self.l, 1);
return error.InvalidEvalCode;
}
if (libcg.c.lua_pcall(self.l, 0, 0, 0) != 0) {
std.log.err("unable to run eval code: {s}", .{libcg.ffi.luaToString(self.l, -1)});
std.log.err("unable to run eval code: {?s}", .{libcg.ffi.luaToString(self.l, -1)});
libcg.c.lua_pop(self.l, 1);
return error.InvalidEvalCode;
}

View file

@ -2,11 +2,11 @@
pkgs.linkFarm "zig-packages" [
# zig-args
{
name = "12203ded54c85878eea7f12744066dcb4397177395ac49a7b2aa365bf6047b623829";
name = "1220fe6ae56b668cc4a033282b5f227bfbb46a67ede6d84e9f9493fea9de339b5f37";
path = pkgs.fetchgit {
url = "https://git.mzte.de/mirrors/zig-args.git";
rev = "89f18a104d9c13763b90e97d6b4ce133da8a3e2b";
hash = "sha256-JY0UDJSKOh1Cg46/GnhVTNmgr6TJKoHXgt8FponPCPM=";
rev = "872272205d95bdba33798c94e72c5387a31bc806";
hash = "sha256-H/sT6JHun+jR37fJSbsauE9K3igV/frcnD/w4Pngzc4=";
};
}
]

View file

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

View file

@ -23,7 +23,7 @@
dontConfigure = true;
nativeBuildInputs = with pkgs; [
zig_0_12.hook
zig_0_13.hook
pkg-config
luajit
fuse3

View file

@ -1,3 +1,6 @@
// FIXME: This whole thing badly needs to be reworked! It barely works, has some nonsensical design
// in places, misses some errors like incorrect delimiters and tends to crash.
const std = @import("std");
const c = @import("ffi.zig").c;
@ -49,6 +52,7 @@ pub fn next(self: *Parser) !?Token {
self.pos = i + 1;
current_type = .lua_literal;
} else if (std.mem.eql(u8, charpair, "%>")) {
// FIXME: underflow on mismatched delimiters
depth -= 1;
if (depth > 0)
continue;
@ -121,6 +125,8 @@ pub fn next(self: *Parser) !?Token {
self.pos = i;
// FIXME: Unclosed delimeters not properly detected! Example: "<%"
return tok;
}

View file

@ -11,14 +11,17 @@ pub fn luaFunc(comptime func: anytype) c.lua_CFunction {
return &struct {
fn f(l: ?*c.lua_State) callconv(.C) c_int {
return func(l.?) catch |e| {
var buf: [128]u8 = undefined;
const err_s = std.fmt.bufPrint(
&buf,
"Zig Error: {s}",
.{@errorName(e)},
) catch unreachable;
c.lua_pushlstring(l, err_s.ptr, err_s.len);
_ = c.lua_error(l);
// If error.LuaError is returned, an error value must be on the stack.
if (e != error.LuaError) {
var buf: [128]u8 = undefined;
const err_s = std.fmt.bufPrint(
&buf,
"Zig Error: {s}",
.{@errorName(e)},
) catch unreachable;
luaPushString(l.?, err_s);
}
_ = c.lua_error(l.?);
unreachable;
};
}
@ -46,7 +49,44 @@ pub fn luaCheckString(l: *c.lua_State, idx: c_int) []const u8 {
return c.luaL_checklstring(l, idx, &len)[0..len];
}
pub fn luaToString(l: *c.lua_State, idx: c_int) []const u8 {
pub fn luaToString(l: *c.lua_State, idx: c_int) ?[]const u8 {
var len: usize = 0;
return c.lua_tolstring(l, idx, &len)[0..len];
return (@as(?[*]const u8, c.lua_tolstring(l, idx, &len)) orelse return null)[0..len];
}
pub fn luaConvertString(l: *c.lua_State, idx: c_int) []const u8 {
c.lua_pushvalue(l, idx);
c.lua_getglobal(l, "tostring");
c.lua_insert(l, -2);
c.lua_call(l, 1, 1);
const s = luaToString(l, -1) orelse unreachable;
c.lua_pop(l, 1);
return s;
}
pub inline fn luaPushString(l: *c.lua_State, s: []const u8) void {
c.lua_pushlstring(l, s.ptr, s.len);
}
const StackWriter = struct {
l: *c.lua_State,
written: u31 = 0,
const Writer = std.io.Writer(*StackWriter, error{}, write);
fn write(self: *StackWriter, bytes: []const u8) error{}!usize {
luaPushString(self.l, bytes);
self.written += 1;
return bytes.len;
}
fn writer(self: *StackWriter) Writer {
return .{ .context = self };
}
};
pub fn luaFmtString(l: *c.lua_State, comptime fmt: []const u8, args: anytype) !void {
var ctx = StackWriter{ .l = l };
try std.fmt.format(ctx.writer(), fmt, args);
c.lua_concat(l, ctx.written);
}

View file

@ -57,7 +57,7 @@ pub fn luaToJSON(l: *c.lua_State, stream: anytype) !void {
// Need to duplicate the key in order to call luaToString.
// Direct call may break lua_next
c.lua_pushvalue(l, -2);
try stream.objectField(ffi.luaToString(l, -1));
try stream.objectField(ffi.luaConvertString(l, -1));
c.lua_pop(l, 1);
}
try luaToJSON(l, stream);

View file

@ -8,11 +8,18 @@ const TemplateCode = luagen.TemplateCode;
pub const state_key = "cg_state";
pub const on_done_callbacks_key = "on_done_callbacks";
const iters_alive_errmsg = "Cannot add file as file iterators are still alive!";
pub const CgState = struct {
rootpath: []const u8,
files: std.StringHashMap(CgFile),
/// Number of currently alive iterators over the files map. This is in place to cause an error
/// when the user attempts concurrent modification.
nfile_iters: usize = 0,
pub fn deinit(self: *CgState) void {
std.debug.assert(self.nfile_iters == 0);
var iter = self.files.iterator();
while (iter.next()) |kv| {
self.files.allocator.free(kv.key_ptr.*);
@ -88,6 +95,9 @@ pub fn initLuaState(cgstate: *CgState) !*c.lua_State {
c.lua_pushcfunction(l, ffi.luaFunc(lToJSON));
c.lua_setfield(l, -2, "toJSON");
c.lua_pushcfunction(l, ffi.luaFunc(lFileIter));
c.lua_setfield(l, -2, "fileIter");
// add cg table to globals
c.lua_setglobal(l, "cg");
@ -100,6 +110,7 @@ pub fn initLuaState(cgstate: *CgState) !*c.lua_State {
c.lua_setfield(l, c.LUA_REGISTRYINDEX, on_done_callbacks_key);
LTemplate.initMetatable(l);
LFileIter.initMetatable(l);
TemplateCode.initMetatable(l);
return l;
@ -107,23 +118,23 @@ pub fn initLuaState(cgstate: *CgState) !*c.lua_State {
pub fn loadCGFile(l: *c.lua_State, cgfile: [*:0]const u8) !void {
if (c.luaL_loadfile(l, cgfile) != 0) {
std.log.err("loading confgen file: {s}", .{ffi.luaToString(l, -1)});
std.log.err("loading confgen file: {?s}", .{ffi.luaToString(l, -1)});
return error.RootfileExec;
}
if (c.lua_pcall(l, 0, 0, 0) != 0) {
std.log.err("running confgen file: {s}", .{ffi.luaToString(l, -1)});
std.log.err("running confgen file: {?s}", .{ffi.luaToString(l, -1)});
return error.RootfileExec;
}
}
pub fn evalUserCode(l: *c.lua_State, code: []const u8) !void {
if (c.luaL_loadbuffer(l, code.ptr, code.len, "<eval>") != 0) {
std.log.err("loading user code: {s}", .{ffi.luaToString(l, -1)});
std.log.err("loading user code: {?s}", .{ffi.luaToString(l, -1)});
return error.Explained;
}
if (c.lua_pcall(l, 0, 0, 0) != 0) {
std.log.err("evaluating user code: {s}", .{ffi.luaToString(l, -1)});
std.log.err("evaluating user code: {?s}", .{ffi.luaToString(l, -1)});
return error.Explained;
}
}
@ -148,7 +159,7 @@ pub fn generate(l: *c.lua_State, code: TemplateCode) !GeneratedFile {
errdefer code.deinit();
if (c.luaL_loadbuffer(l, code.content.ptr, code.content.len, code.name) != 0) {
std.log.err("failed to load template: {s}", .{ffi.luaToString(l, -1)});
std.log.err("failed to load template: {?s}", .{ffi.luaToString(l, -1)});
return error.LoadTemplate;
}
@ -175,7 +186,7 @@ pub fn generate(l: *c.lua_State, code: TemplateCode) !GeneratedFile {
_ = c.lua_setfenv(l, -2);
if (c.lua_pcall(l, 0, 0, 0) != 0) {
std.log.err("failed to run template: {s}", .{ffi.luaToString(l, -1)});
std.log.err("failed to run template: {?s}", .{ffi.luaToString(l, -1)});
return error.RunTemplate;
}
@ -197,7 +208,7 @@ pub fn callOnDoneCallbacks(l: *c.lua_State, errors: bool) void {
c.lua_pushboolean(l, @intFromBool(errors));
if (c.lua_pcall(l, 1, 0, 0) != 0) {
const err_s = ffi.luaToString(l, -1);
std.log.err("running onDone callback: {s}", .{err_s});
std.log.err("running onDone callback: {?s}", .{err_s});
c.lua_pop(l, 1);
}
}
@ -213,7 +224,7 @@ fn lPrint(l: *c.lua_State) !c_int {
try writer.writeAll("\x1b[1;34mL:\x1b[0m ");
for (0..@intCast(nargs)) |i| {
const s = ffi.luaToString(l, @intCast(i + 1));
const s = ffi.luaConvertString(l, @intCast(i + 1));
try writer.writeAll(s);
if (i + 1 != nargs)
try writer.writeByte('\t');
@ -231,6 +242,11 @@ fn lAddString(l: *c.lua_State) !c_int {
const state = getState(l);
if (state.nfile_iters != 0) {
ffi.luaPushString(l, iters_alive_errmsg);
return error.LuaError;
}
const outpath_d = try state.files.allocator.dupe(u8, outpath);
errdefer state.files.allocator.free(outpath_d);
@ -253,6 +269,11 @@ fn lAddPath(l: *c.lua_State) !c_int {
const state = getState(l);
if (state.nfile_iters != 0) {
ffi.luaPushString(l, iters_alive_errmsg);
return error.LuaError;
}
var dir = try std.fs.cwd().openDir(path, .{ .iterate = true });
defer dir.close();
@ -289,6 +310,11 @@ fn lAddPath(l: *c.lua_State) !c_int {
fn lAddFile(l: *c.lua_State) !c_int {
const state = getState(l);
if (state.nfile_iters != 0) {
ffi.luaPushString(l, iters_alive_errmsg);
return error.LuaError;
}
const argc = c.lua_gettop(l);
const inpath = ffi.luaCheckString(l, 1);
@ -346,7 +372,7 @@ fn lDoTemplate(l: *c.lua_State) !c_int {
c.lua_getfield(l, 2, "name");
if (!c.lua_isnil(l, -1)) {
source_name = ffi.luaToString(l, -1);
source_name = ffi.luaToString(l, -1) orelse return error.InvalidArgument;
}
{
@ -362,9 +388,8 @@ fn lDoTemplate(l: *c.lua_State) !c_int {
tmpl_code.content.len,
tmpl_code.name.ptr,
) != 0) {
// TODO: turn this into a lua error
std.log.err("loading template: {s}", .{ffi.luaToString(l, -1)});
return error.LoadTemplate;
try ffi.luaFmtString(l, "loading template:\n{?s}", .{ffi.luaToString(l, -1)});
return error.LuaError;
}
// create env table
@ -387,9 +412,8 @@ fn lDoTemplate(l: *c.lua_State) !c_int {
_ = c.lua_setfenv(l, -2);
if (c.lua_pcall(l, 0, 0, 0) != 0) {
// TODO: turn this into a lua error
std.log.err("failed to run template: {s}", .{ffi.luaToString(l, -1)});
return error.RunTemplate;
try ffi.luaFmtString(l, "failed to run template:\n{?s}", .{ffi.luaToString(l, -1)});
return error.LuaError;
}
const output = try tmpl.getOutput(l);
@ -432,9 +456,8 @@ fn lDoTemplateFile(l: *c.lua_State) !c_int {
tmpl_code.content.len,
tmpl_code.name.ptr,
) != 0) {
// TODO: turn this into a lua error
std.log.err("loading template: {s}", .{ffi.luaToString(l, -1)});
return error.LoadTemplate;
try ffi.luaFmtString(l, "loading template:\n{?s}", .{ffi.luaToString(l, -1)});
return error.LuaError;
}
// create env
@ -456,9 +479,8 @@ fn lDoTemplateFile(l: *c.lua_State) !c_int {
_ = c.lua_setfenv(l, -2);
if (c.lua_pcall(l, 0, 0, 0) != 0) {
// TODO: turn this into a lua error
std.log.err("failed to run template: {s}", .{ffi.luaToString(l, -1)});
return error.RunTemplate;
try ffi.luaFmtString(l, "failed to run template:\n{?s}", .{ffi.luaToString(l, -1)});
return error.LuaError;
}
const output = try tmpl.getOutput(l);
@ -481,7 +503,7 @@ fn lOnDone(l: *c.lua_State) !c_int {
return 0;
}
pub fn lToJSON(l: *c.lua_State) !c_int {
fn lToJSON(l: *c.lua_State) !c_int {
c.luaL_checkany(l, 1);
const pretty = if (c.lua_gettop(l) >= 2) c.lua_toboolean(l, 2) != 0 else false;
@ -507,6 +529,83 @@ pub fn lToJSON(l: *c.lua_State) !c_int {
return 1;
}
fn lFileIter(l: *c.lua_State) !c_int {
const state = getState(l);
state.nfile_iters += 1;
_ = (LFileIter{ .iter = state.files.iterator() }).push(l);
return 1;
}
pub const LFileIter = struct {
pub const lua_registry_key = "confgen_file_iter";
iter: std.StringHashMap(CgFile).Iterator,
pub fn push(self: LFileIter, l: *c.lua_State) *LFileIter {
const self_ptr = ffi.luaPushUdata(l, LFileIter);
self_ptr.* = self;
return self_ptr;
}
fn lGC(l: *c.lua_State) !c_int {
const state = getState(l);
state.nfile_iters -= 1;
return 0;
}
fn lCall(l: *c.lua_State) !c_int {
const self = ffi.luaGetUdata(LFileIter, l, 1);
if (self.iter.next()) |kv| {
c.lua_createtable(l, 0, 3);
c.lua_pushlstring(l, kv.key_ptr.ptr, kv.key_ptr.len);
c.lua_setfield(l, -2, "path");
c.lua_pushboolean(l, @intFromBool(kv.value_ptr.copy));
c.lua_setfield(l, -2, "copy");
{ // content
c.lua_createtable(l, 0, 1);
switch (kv.value_ptr.content) {
.path => |p| {
c.lua_pushlstring(l, p.ptr, p.len);
c.lua_setfield(l, -2, "path");
},
.string => |s| {
c.lua_pushlstring(l, s.ptr, s.len);
c.lua_setfield(l, -2, "string");
},
}
c.lua_setfield(l, -2, "content");
}
return 1;
} else {
c.lua_pushnil(l);
return 1;
}
}
fn initMetatable(l: *c.lua_State) void {
_ = c.luaL_newmetatable(l, lua_registry_key);
c.lua_pushcfunction(l, ffi.luaFunc(lGC));
c.lua_setfield(l, -2, "__gc");
c.lua_pushcfunction(l, ffi.luaFunc(lCall));
c.lua_setfield(l, -2, "__call");
c.lua_pop(l, 1);
}
};
pub const LTemplate = struct {
pub const lua_registry_key = "confgen_template";
@ -567,12 +666,11 @@ pub const LTemplate = struct {
// call post processor
if (c.lua_pcall(l, 1, 1, 0) != 0) {
// TODO: return this instead of logging
std.log.err("running post processor: {s}", .{ffi.luaToString(l, -1)});
return error.PostProcessor;
try ffi.luaFmtString(l, "running post processor: {?s}", .{ffi.luaToString(l, -1)});
return error.LuaError;
}
const out = ffi.luaToString(l, -1);
const out = ffi.luaConvertString(l, -1);
return try self.output.allocator.dupe(u8, out);
}
@ -604,8 +702,11 @@ pub const LTemplate = struct {
}
fn lPushValue(l: *c.lua_State) !c_int {
c.luaL_checkany(l, 2);
if (c.lua_isnil(l, 2)) return 0; // do nothing if passed nil
const self = ffi.luaGetUdata(LTemplate, l, 1);
try self.output.appendSlice(ffi.luaCheckString(l, 2));
try self.output.appendSlice(ffi.luaConvertString(l, 2));
return 0;
}
@ -632,7 +733,7 @@ pub const LTemplate = struct {
const mode = mode: {
if (c.lua_isstring(l, 2) != 0) {
const s = ffi.luaToString(l, 2);
const s = ffi.luaToString(l, 2) orelse unreachable;
if (s.len != 3) break :mode null;
break :mode std.fmt.parseInt(u24, s, 8) catch null;
} else if (c.lua_isnumber(l, 2) != 0) {
@ -684,5 +785,7 @@ pub const LTemplate = struct {
c.lua_pushvalue(l, -1);
c.lua_setfield(l, -2, "__index");
c.lua_pop(l, 1);
}
};

View file

@ -40,6 +40,8 @@ pub const TemplateCode = struct {
c.lua_pushcfunction(l, ffi.luaFunc(lGC));
c.lua_setfield(l, -2, "__gc");
c.lua_pop(l, 1);
}
};

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" -- "${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

@ -1,6 +1,6 @@
function __confgenfs_completion() {
if [ "${COMP_CWORD}" -eq 1 ]; then
COMPREPLY=($(compgen -A file -W "--help -h" -- "${COMP_WORDS[1]}"))
COMPREPLY=($(compgen -A file -W "--help -h --eval -e --post-eval -p" -- "${COMP_WORDS[1]}"))
elif [ "${COMP_CWORD}" -eq 2 ]; then
case "${COMP_WORDS[1]}" in
"--help" | "-h") COMPREPLY=() ;;

View file

@ -5,3 +5,6 @@ complete -c confgen -s h -l help -d "Show help"
complete -c confgen -s c -l compile -d "Compile a template to Lua instead of running" -Fr
complete -c confgen -s j -l json-opt -d "Write the given or all fields from cg.opt to stdout as JSON after running the given confgenfile instead of running" -Fr
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

@ -2,3 +2,5 @@
complete -c confgenfs -e
complete -c confgenfs -s h -l help -d "Show help"
complete -c confgenfs -s e -l eval -r -d "Evaluate the given lua code before loading the confgenfile"
complete -c confgenfs -s p -l post-eval -r -d "Evaluate the given lua code after loading the confgenfile"

View file

@ -7,6 +7,9 @@
.B confgen
.RI [ CONFGENFILE ]
.RI [ OUTPATH ]
.RI < --eval\ [CODE] >
.RI < --post-eval\ [CODE] >
.RI < --watch >
.br
.B confgen --compile
.RI [ TEMPLATE_FILE ]
@ -81,6 +84,28 @@ The
.I opt
table will be empty and functions that normally add files to be generated will have no effect.
.TP
.B --eval [CODE]
Evaluate the given
.I lua code
before loading the
.IR confgenfile .
.TP
.B --post-eval [CODE]
Evaluate the given
.I lua code
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

@ -9,6 +9,8 @@ template engine.
.B confgenfs
.RI [ CONFGENFILE ]
.RI [ MOUNTPOINT ]
.RI < --eval\ [CODE] >
.RI < --post-eval\ [CODE] >
.br
.B confgenfs --help
@ -63,6 +65,21 @@ parameter.
.B --help
Show a help message.
.TP
.B --eval [CODE]
Evaluate the given
.I lua code
before loading the
.IR confgenfile .
.TP
.B --post-eval [CODE]
Evaluate the given
.I lua code
after loading the
.IR confgenfile .
.SH EXAMPLES
Change an option representing the currently in-use wayland compositor at runtime by executing
the following shellcode in a startup script. The code assumes

View file

@ -146,6 +146,37 @@ will be serialized as
Example:
.B local json = cg.toJSON({ x = \(dqy\(dq }) -- '{\(dqx\(dq: \(dqy\(dq}'
.TP
.B cg.fileIter()
Obtain an
.I iterator function
over the files that have been added to confgen. Upon each invocation, it will return a table with
the following fields:
.IP \(bu
The
.I path
field will contain the path of the output file relative to the
.IR destination\ directory .
.IP \(bu
The
.I copy
field will be set to
.I true
if the file is not a template and will be simply copied to the destination and
.I false
if the file is a template and will have generation done first.
.IP \(bu
The
.I content
field will be a table, either containing the file's source code in its
.I string
field, or the path to the source file in its
.I path
field.
.TP
.B tmpl
The global value
@ -170,7 +201,9 @@ Example:
.B tmpl:pushValue(value)
Push a Lua value to the output buffer. The values will be converted to strings the same as Lua's
.I tostring
would. This function is rarely called manually and is instead what
would, except that
.I nil
is ignored and nothing is appended. This function is rarely called manually and is instead what
.I Confgen
generates for
.IR Lua\ expression\ blocks .

View file

@ -10,8 +10,14 @@ _confgen() {
'--compile[Compile a template to Lua instead of running]:template_file:_files' \
'-j[Write the given or all fields from cg.opt to stdout as JSON after running the given confgenfile instead of running]:confgenfile:_files' \
'--json-opt[Write the given or all fields from cg.opt to stdout as JSON after running the given confgenfile instead of running]:confgenfile:_files' \
"-f[Evaluate a single template and write the output instead of running]:template_file:_files" \
"--file[Evaluate a single template and write the output instead of running]:template_file:_files"
'-f[Evaluate a single template and write the output instead of running]:template_file:_files' \
'--file[Evaluate a single template and write the output instead of running]:template_file:_files' \
'-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:' \
'-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 "$@"

View file

@ -7,6 +7,10 @@ _confgenfs() {
'*:fuse_opts:' \
'-h[Show help]' \
'--help[Show help]'
'-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:'
}
_confgenfs "$@"