feat: initial commit

This commit is contained in:
LordMZTE 2023-01-20 21:30:02 +01:00
commit b26942dc4e
Signed by: LordMZTE
GPG Key ID: B64802DC33A64FF6
10 changed files with 736 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
zig-cache/
zig-out/
deps.zig
gyro.lock
.gyro

46
build.zig Normal file
View File

@ -0,0 +1,46 @@
const std = @import("std");
pub fn build(b: *std.build.Builder) 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 release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("confgen", "src/main.zig");
exe.setTarget(target);
exe.setBuildMode(mode);
exe.strip = mode != .Debug and mode != .ReleaseSafe;
setupExe(exe);
exe.install();
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const exe_tests = b.addTest("src/main.zig");
exe_tests.setTarget(target);
exe_tests.setBuildMode(mode);
setupExe(exe_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&exe_tests.step);
}
fn setupExe(exe: *std.build.LibExeObjStep) void {
exe.linkLibC();
exe.linkSystemLibrary("luajit");
exe.unwind_tables = true;
}

157
src/Parser.zig Normal file
View File

@ -0,0 +1,157 @@
const std = @import("std");
const c = @import("ffi.zig").c;
str: []const u8,
pos: usize,
pub const Parser = @This();
pub const Token = struct {
token_type: TokenType,
str: []const u8,
};
pub const TokenType = enum {
text,
lua,
lua_literal,
};
pub fn next(self: *Parser) !?Token {
var current_type = TokenType.text;
// TODO: this approach allows for stuff like <% <! %> %> to be considered valid.
// that sucks, but I cannot be bothered
var depth: usize = 0;
var i = self.pos;
while (i < self.str.len) : (i += 1) {
const charpair = self.str[i..@min(i + 2, self.str.len)];
if (std.mem.eql(u8, charpair, "<%")) {
if (current_type == .text and self.pos != i) {
const tok = Token{
.token_type = .text,
.str = self.str[self.pos..i],
};
self.pos = i;
return tok;
}
defer depth += 1;
if (depth > 0)
continue;
i += 1;
self.pos = i + 1;
current_type = .lua_literal;
} else if (std.mem.eql(u8, charpair, "%>")) {
depth -= 1;
if (depth > 0)
continue;
// Can't check for != .lua_literal here, as that would require using
// sort of a stack for nested blocks
if (current_type == .text)
return error.UnexpectedClose;
const tok = Token{
.token_type = .lua_literal,
.str = std.mem.trim(u8, self.str[self.pos..i], &std.ascii.whitespace),
};
self.pos = i + 2;
return tok;
} else if (std.mem.eql(u8, charpair, "<!")) {
if (current_type == .text and self.pos != i) {
const tok = Token{
.token_type = .text,
.str = self.str[self.pos..i],
};
self.pos = i;
return tok;
}
defer depth += 1;
if (depth > 0)
continue;
i += 1;
self.pos = i + 1;
current_type = .lua;
} else if (std.mem.eql(u8, charpair, "!>")) {
depth -= 1;
if (depth > 0)
continue;
if (current_type == .text)
return error.UnexpectedClose;
const tok = Token{
.token_type = .lua,
.str = std.mem.trim(u8, self.str[self.pos..i], &std.ascii.whitespace),
};
self.pos = i + 2;
return tok;
}
}
if (self.pos == i) {
return null;
}
if (current_type != .text) {
return error.UnclosedDelimeter;
}
const tok = Token{
.token_type = .text,
.str = self.str[self.pos..],
};
self.pos = i;
return tok;
}
test "lua literal" {
const input =
\\bla
\\<% <% test %> %>
\\bla
;
var parser = Parser{ .str = input, .pos = 0 };
try std.testing.expectEqual(TokenType.text, (try parser.next()).?.token_type);
try std.testing.expectEqual(TokenType.lua_literal, (try parser.next()).?.token_type);
try std.testing.expectEqual(TokenType.text, (try parser.next()).?.token_type);
try std.testing.expectEqual(@as(?Token, null), try parser.next());
}
test "lua" {
const input =
\\bla
\\<! test !>
\\bla
;
var parser = Parser{ .str = input, .pos = 0 };
try std.testing.expectEqual(TokenType.text, (try parser.next()).?.token_type);
try std.testing.expectEqual(TokenType.lua, (try parser.next()).?.token_type);
try std.testing.expectEqual(TokenType.text, (try parser.next()).?.token_type);
try std.testing.expectEqual(@as(?Token, null), try parser.next());
}

42
src/ffi.zig Normal file
View File

@ -0,0 +1,42 @@
const std = @import("std");
pub const c = @cImport({
@cInclude("lua.h");
@cInclude("lauxlib.h");
@cInclude("lualib.h");
});
/// Generates a wrapper function with error handling for a lua CFunction
pub fn luaFunc(comptime func: fn (*c.lua_State) anyerror!c_int) 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.bufPrintZ(
&buf,
"Zig Error: {s}",
.{@errorName(e)},
) catch unreachable;
c.lua_pushstring(l, err_s.ptr);
_ = c.lua_error(l);
unreachable;
};
}
}.f;
}
/// Convenience function for pushing some full userdata onto the lua stack
pub fn luaPushUdata(l: *c.lua_State, udata: anytype, tname: [*:0]const u8) void {
const T = @TypeOf(udata);
// create and set data
@ptrCast(*T, @alignCast(@alignOf(*T), c.lua_newuserdata(l, @sizeOf(T)).?)).* = udata;
// set metatable
c.luaL_getmetatable(l, tname);
_ = c.lua_setmetatable(l, -2);
}
pub fn luaGetUdata(comptime T: type, l: *c.lua_State, param: c_int, tname: [*:0]const u8) *T {
return @ptrCast(*T, @alignCast(@alignOf(*T), c.luaL_checkudata(l, param, tname)));
}

265
src/lua_api.zig Normal file
View File

@ -0,0 +1,265 @@
const std = @import("std");
const ffi = @import("ffi.zig");
const c = ffi.c;
const TemplateCode = @import("luagen.zig").TemplateCode;
pub const state_key = "cg_state";
pub const CgState = struct {
outpath: []const u8,
rootpath: []const u8,
files: std.ArrayList(CgFile),
pub fn deinit(self: *CgState) void {
for (self.files.items) |*file| {
file.deinit();
}
self.files.deinit();
}
};
pub const CgFile = struct {
outpath: []const u8,
content: CgFileContent,
/// If set, this is a normal file that should just be copied.
copy: bool = false,
pub fn deinit(self: *CgFile) void {
std.heap.c_allocator.free(self.outpath);
switch (self.content) {
.path => |x| std.heap.c_allocator.free(x),
.string => |x| std.heap.c_allocator.free(x),
}
}
};
pub const CgFileContent = union(enum) {
path: []const u8,
string: []const u8,
};
pub fn initLuaState(cgstate: *CgState) !*c.lua_State {
const l = c.luaL_newstate().?;
// open all lua libs
c.luaL_openlibs(l);
// create opt table
c.lua_newtable(l);
// init cg table
c.lua_newtable(l);
c.lua_setfield(l, -2, "opt");
c.lua_pushcfunction(l, ffi.luaFunc(lAddString));
c.lua_setfield(l, -2, "addString");
c.lua_pushcfunction(l, ffi.luaFunc(lAddPath));
c.lua_setfield(l, -2, "addPath");
c.lua_pushcfunction(l, ffi.luaFunc(lAddFile));
c.lua_setfield(l, -2, "addFile");
// add cg table to globals
c.lua_setglobal(l, "cg");
// add state to registry
c.lua_pushlightuserdata(l, cgstate);
c.lua_setfield(l, c.LUA_REGISTRYINDEX, state_key);
LTemplate.initMetatable(l);
return l;
}
pub fn getState(l: *c.lua_State) *CgState {
c.lua_getfield(l, c.LUA_REGISTRYINDEX, state_key);
const state_ptr = c.lua_touserdata(l, -1);
c.lua_pop(l, 1);
return @ptrCast(*CgState, @alignCast(@alignOf(*CgState), state_ptr));
}
pub fn generate(l: *c.lua_State, code: TemplateCode) ![]const u8 {
const prevtop = c.lua_gettop(l);
defer c.lua_settop(l, prevtop);
if (c.luaL_loadbuffer(l, code.content.ptr, code.content.len, code.name) != 0) {
std.log.err("failed to load template: {s}", .{c.lua_tolstring(l, -1, null)});
return error.LoadTemplate;
}
// create template environment
c.lua_newtable(l);
// initialize environment
c.lua_getglobal(l, "_G");
c.lua_setfield(l, -2, "_G");
// add cg.opt to context
c.lua_getglobal(l, "cg");
c.lua_getfield(l, -1, "opt");
c.lua_setfield(l, -3, "opt");
c.lua_pop(l, 1);
// initialize template
const tmpl = try LTemplate.init(code);
tmpl.push(l);
c.lua_setfield(l, -2, "tmpl");
_ = c.lua_setfenv(l, -2);
if (c.lua_pcall(l, 0, 0, 0) != 0) {
std.log.err("failed to run template: {s}", .{c.lua_tolstring(l, -1, null)});
return error.RunTemplate;
}
return try tmpl.output.toOwnedSlice();
}
fn lAddString(l: *c.lua_State) !c_int {
var outpath_len: usize = 0;
const outpath = c.luaL_checklstring(l, 1, &outpath_len);
var data_len: usize = 0;
const data = c.luaL_checklstring(l, 2, &data_len);
const state = getState(l);
try state.files.append(CgFile{
.outpath = try std.heap.c_allocator.dupe(u8, outpath[0..outpath_len]),
.content = .{ .string = try std.heap.c_allocator.dupe(u8, data[0..data_len]) },
});
return 0;
}
fn lAddPath(l: *c.lua_State) !c_int {
var path_len: usize = 0;
const path = c.luaL_checklstring(l, 1, &path_len);
const state = getState(l);
var dir = try std.fs.cwd().openIterableDir(path[0..path_len], .{});
defer dir.close();
var iter = try dir.walk(std.heap.c_allocator);
defer iter.deinit();
while (try iter.next()) |entry| {
if (entry.kind == .Directory)
continue;
const outpath = if (std.mem.endsWith(u8, entry.path, ".cgt"))
entry.path[0 .. entry.path.len - 4]
else
entry.path;
try state.files.append(.{
.outpath = try std.heap.c_allocator.dupe(u8, outpath),
.content = .{ .path = try std.heap.c_allocator.dupe(u8, entry.path) },
.copy = !std.mem.endsWith(u8, entry.path, ".cgt"),
});
}
return 0;
}
fn lAddFile(l: *c.lua_State) !c_int {
const argc = c.lua_gettop(l);
var inpath_len: usize = 0;
const inpath = c.luaL_checklstring(l, 1, &inpath_len)[0..inpath_len];
const outpath = if (argc >= 2) blk: {
var outpath_len: usize = 0;
break :blk c.luaL_checklstring(l, 2, &outpath_len)[0..outpath_len];
} else blk: {
if (std.mem.endsWith(u8, inpath, ".cgt")) {
break :blk inpath[0 .. inpath.len - 4];
}
break :blk inpath;
};
const state = getState(l);
try state.files.append(.{
.outpath = try std.heap.c_allocator.dupe(u8, outpath),
.content = .{ .path = try std.heap.c_allocator.dupe(u8, inpath) },
.copy = !std.mem.endsWith(u8, inpath, ".cgt"),
});
return 0;
}
pub const LTemplate = struct {
pub const registry_key = "confgen_template";
code: TemplateCode,
output: std.ArrayList(u8),
pub fn init(code: TemplateCode) !*LTemplate {
const self = try std.heap.c_allocator.create(LTemplate);
self.* = .{
.output = std.ArrayList(u8).init(std.heap.c_allocator),
.code = code,
};
return self;
}
pub fn deinit(self: *LTemplate) void {
self.output.deinit();
std.heap.c_allocator.destroy(self);
}
pub fn push(self: *LTemplate, l: *c.lua_State) void {
ffi.luaPushUdata(l, self, registry_key);
}
fn lGC(l: *c.lua_State) !c_int {
const self = ffi.luaGetUdata(*LTemplate, l, 1, registry_key).*;
self.deinit();
return 0;
}
fn lPushLitIdx(l: *c.lua_State) !c_int {
const self = ffi.luaGetUdata(*LTemplate, l, 1, registry_key).*;
const idx = std.math.cast(usize, c.luaL_checkint(l, 2)) orelse return error.InvalidIndex;
if (idx >= self.code.literals.len)
return error.InvalidIndex;
try self.output.appendSlice(self.code.literals[idx]);
return 0;
}
fn lPushValue(l: *c.lua_State) !c_int {
const self = ffi.luaGetUdata(*LTemplate, l, 1, registry_key).*;
const val = c.luaL_checklstring(l, 2, null);
try self.output.appendSlice(std.mem.span(val));
return 0;
}
fn initMetatable(l: *c.lua_State) void {
_ = c.luaL_newmetatable(l, registry_key);
c.lua_pushcfunction(l, ffi.luaFunc(lGC));
c.lua_setfield(l, -2, "__gc");
c.lua_pushcfunction(l, ffi.luaFunc(lPushLitIdx));
c.lua_setfield(l, -2, "pushLitIdx");
c.lua_pushcfunction(l, ffi.luaFunc(lPushValue));
c.lua_setfield(l, -2, "pushValue");
c.lua_pushvalue(l, -1);
c.lua_setfield(l, -2, "__index");
}
};

50
src/luagen.zig Normal file
View File

@ -0,0 +1,50 @@
const std = @import("std");
const Parser = @import("Parser.zig");
/// A compiled lua file for a template.
/// Contains references to template input string!
pub const TemplateCode = struct {
name: [:0]const u8,
content: []const u8,
literals: []const []const u8,
pub fn deinit(self: TemplateCode) void {
std.heap.c_allocator.free(self.name);
std.heap.c_allocator.free(self.literals);
std.heap.c_allocator.free(self.content);
}
};
/// Generates a lua script that allows getting the output from a given parser.
pub fn generateLua(parser: *Parser, name: []const u8) !TemplateCode {
var outbuf = std.ArrayList(u8).init(std.heap.c_allocator);
var literals = std.ArrayList([]const u8).init(std.heap.c_allocator);
while (try parser.next()) |token| {
switch (token.token_type) {
.text => {
try literals.append(token.str);
try outbuf.writer().print(
"tmpl:pushLitIdx({d})\n",
.{literals.items.len - 1},
);
},
.lua => {
try outbuf.appendSlice(token.str);
try outbuf.append('\n');
},
.lua_literal => {
try outbuf.writer().print(
"tmpl:pushValue({s})\n",
.{token.str},
);
},
}
}
return .{
.name = try std.heap.c_allocator.dupeZ(u8, name),
.content = try outbuf.toOwnedSlice(),
.literals = try literals.toOwnedSlice(),
};
}

145
src/main.zig Normal file
View File

@ -0,0 +1,145 @@
const std = @import("std");
const c = @import("ffi.zig").c;
const luagen = @import("luagen.zig");
const lapi = @import("lua_api.zig");
const rootfile = @import("rootfile.zig");
const Parser = @import("Parser.zig");
comptime {
if (@import("builtin").is_test) {
std.testing.refAllDeclsRecursive(@This());
}
}
pub fn main() !void {
// TODO: add flag to emit generated lua files
if (std.os.argv.len != 2) {
// TODO: print usage
std.log.err("Expected one argument.", .{});
return error.InvalidArgs;
}
const conf_dir = (try rootfile.findRootDir()) orelse {
std.log.err("Couldn't find confgen.lua file!", .{});
return error.RootfileNotFound;
};
defer std.heap.c_allocator.free(conf_dir);
var state = lapi.CgState{
.outpath = std.mem.span(std.os.argv[1]),
.rootpath = conf_dir,
.files = std.ArrayList(lapi.CgFile).init(std.heap.c_allocator),
};
defer state.deinit();
const l = try lapi.initLuaState(&state);
defer c.lua_close(l);
const conf_file_path = try std.fs.path.joinZ(
std.heap.c_allocator,
&.{ conf_dir, "confgen.lua" },
);
defer std.heap.c_allocator.free(conf_file_path);
if (c.luaL_loadfile(l, conf_file_path.ptr) != 0) {
std.log.err("loading confgen.lua: {s}", .{c.lua_tolstring(l, -1, null)});
return error.RootfileExec;
}
if (c.lua_pcall(l, 0, 0, 0) != 0) {
std.log.err("running confgen.lua: {s}", .{c.lua_tolstring(l, -1, null)});
return error.RootfileExec;
}
var content_buf = std.ArrayList(u8).init(std.heap.c_allocator);
defer content_buf.deinit();
for (state.files.items) |file| {
if (file.copy) {
std.log.info("copying {s}", .{file.outpath});
} else {
std.log.info("generating {s}", .{file.outpath});
}
genfile(l, file, &content_buf) catch |e| {
std.log.err("generating {s}: {}", .{ file.outpath, e });
};
}
}
fn genfile(
l: *c.lua_State,
file: lapi.CgFile,
content_buf: *std.ArrayList(u8),
) !void {
const state = lapi.getState(l);
if (file.copy) {
const from_path = try std.fs.path.join(
std.heap.c_allocator,
&.{ state.rootpath, file.content.path },
);
defer std.heap.c_allocator.free(from_path);
const to_path = try std.fs.path.join(
std.heap.c_allocator,
&.{ state.outpath, file.outpath },
);
defer std.heap.c_allocator.free(to_path);
if (std.fs.path.dirname(to_path)) |dir| {
try std.fs.cwd().makePath(dir);
}
try std.fs.cwd().copyFile(from_path, std.fs.cwd(), to_path, .{});
return;
}
content_buf.clearRetainingCapacity();
var content: []const u8 = undefined;
var fname: ?[]const u8 = null;
switch (file.content) {
.string => |s| content = s,
.path => |p| {
fname = std.fs.path.basename(p);
const path = try std.fs.path.join(std.heap.c_allocator, &.{ state.rootpath, p });
defer std.heap.c_allocator.free(path);
const f = try std.fs.cwd().openFile(path, .{});
defer f.close();
try f.reader().readAllArrayList(content_buf, std.math.maxInt(usize));
content = content_buf.items;
},
}
var parser = Parser{
.str = content,
.pos = 0,
};
const tmpl = try luagen.generateLua(&parser, fname orelse file.outpath);
defer tmpl.deinit();
const out = try lapi.generate(l, tmpl);
defer std.heap.c_allocator.free(out);
const path = try std.fs.path.join(
std.heap.c_allocator,
&.{ state.outpath, file.outpath },
);
defer std.heap.c_allocator.free(path);
if (std.fs.path.dirname(path)) |dir| {
try std.fs.cwd().makePath(dir);
}
var outfile = try std.fs.cwd().createFile(path, .{});
defer outfile.close();
try outfile.writeAll(out);
}

18
src/rootfile.zig Normal file
View File

@ -0,0 +1,18 @@
const std = @import("std");
const c = @import("ffi.zig").c;
/// Tries to find the confgen.lua file by walking up the directory tree.
/// Returns the directory path, NOT THE FILE PATH.
/// Returned path is malloc'd
pub fn findRootDir() !?[]const u8 {
// TODO: walk upwards
_ = std.fs.cwd().statFile("confgen.lua") catch |e| {
if (e == error.FileNotFound) {
return null;
}
return e;
};
return try std.heap.c_allocator.dupe(u8, ".");
}

4
testing/confgen.lua Normal file
View File

@ -0,0 +1,4 @@
cg.addString("test1.cfg", [[foo <% opt.test %> bar]])
cg.addPath(".")
cg.opt.test = "I'm a test option!"

4
testing/test.cfg.cgt Normal file
View File

@ -0,0 +1,4 @@
Hello, I'm a config file
<! for i=0, 4 do !>test string #<% i + 1 %>: <% opt.test %>
<! end !>