diff options
Diffstat (limited to 'src/template.zig')
| -rw-r--r-- | src/template.zig | 359 |
1 files changed, 255 insertions, 104 deletions
diff --git a/src/template.zig b/src/template.zig index 210e77d..89521e3 100644 --- a/src/template.zig +++ b/src/template.zig @@ -10,13 +10,30 @@ const Env = @import("env.zig"); const fatal = cli.fatal; pub const Config = struct { + const max_archives = 16; + + pub const Archive = struct { + /// The id of the distribution file. + /// Default: empty (no distname suffix) + id: []const u8, + /// The path that should be the root of the archive (relative to the repository's root). + root: ?[]const u8 = null, + /// A list of paths that should be included in the archive. + /// All paths are relative to the root's root. + /// Default: empty (everything is included) + paths: [64][]const u8 = undefined, + paths_len: usize = 0, + }; + raw: conf.Config, - prepare_dist_target: ?[]const u8, + prepare_target: ?[]const u8, + archives: [max_archives]Archive, + archives_len: usize, - fn from_file(env: Env, tmpl: Template) !Config { + fn from_file(env: *const Env, tmpl: Template) !Config { const config_file = tmpl.config_file(); - const conf_res = try conf.Config.from_file(env.allocator, config_file.name()); + const conf_res = try conf.Config.from_file(env.allocator, &config_file); switch (conf_res) { .conf => |c| return .init(env, c), .err => |e| { @@ -26,21 +43,69 @@ pub const Config = struct { } } - fn init(env: Env, raw: conf.Config) Config { - var prepare_dist_target: ?[]const u8 = null; + fn init(env: *const Env, raw: conf.Config) Config { + var dist_prepare_target: ?[]const u8 = null; + var archives: [max_archives]Archive = undefined; + var n_archives: usize = 0; var iter = raw.iterate(); while (iter.next()) |v| { - if (std.mem.eql(u8, v.key, "prepare-dist-target") and v.val.len > 0) { - prepare_dist_target = v.val; + if (std.mem.eql(u8, v.key, "dist.prepare-target")) { + dist_prepare_target = if (v.val.len > 0) v.val else null; + } else if (std.mem.startsWith(u8, v.key, "dist.archive")) { + var archive_key = v.key[12..]; + if (std.mem.findScalar(u8, archive_key, '.')) |dot| { + var archive_id: []const u8 = undefined; + if (dot == 0) { + archive_id = ""; + } else if (archive_key[0] != '[' or archive_key[dot - 1] != ']') { + env.warn("unknown template configuration `{s}`", .{v.key}); + continue; + } else { + archive_id = archive_key[1 .. dot - 1]; + } + archive_key = archive_key[dot + 1 ..]; + + var archive: ?*Archive = null; + for (0..n_archives) |i| { + if (std.mem.eql(u8, archives[i].id, archive_id)) { + archive = &archives[i]; + break; + } + } + if (archive == null) { + if (n_archives == max_archives) { + fatal("maximum number of distribution files exceeded", .{}); + } + archive = &archives[n_archives]; + archives[n_archives] = .{ .id = archive_id }; + n_archives += 1; + } + if (std.mem.eql(u8, archive_key, "root")) { + archive.?.root = if (v.val.len > 0) v.val else null; + } else if (std.mem.eql(u8, archive_key, "paths")) { + archive.?.paths_len = split_paths(&archive.?.paths, v.val); + } else { + env.warn("unknown template configuration `{s}`", .{v.key}); + } + } else { + env.warn("unknown template configuration `{s}`", .{v.key}); + } } else { env.warn("unknown template configuration `{s}`", .{v.key}); } } + if (n_archives == 0) { + archives[0] = .{ .id = "" }; + n_archives = 1; + } + return .{ .raw = raw, - .prepare_dist_target = prepare_dist_target, + .prepare_target = dist_prepare_target, + .archives = archives, + .archives_len = n_archives, }; } @@ -55,7 +120,7 @@ pub const Template = struct { name: []const u8, path: fs.Path, - pub fn init(env: Env, template_name: []const u8) Template { + pub fn init(env: *const Env, template_name: []const u8) Template { var template_path = etc_dir(env); template_path.append(.{template_name}); return .{ @@ -64,47 +129,45 @@ pub const Template = struct { }; } - pub fn render(self: *const Template, env: Env, tmpl_conf: []const conf.Config, target: fs.Path) !void { - var dir = try std.fs.openDirAbsoluteZ(self.path.name(), .{ .iterate = true }); - defer dir.close(); - - var walker = try dir.walk(env.allocator); - defer walker.deinit(); - - while (try walker.next()) |entry| { - if (std.mem.eql(u8, entry.path, config_filename)) { - continue; - } + pub fn render(self: *const Template, env: *const Env, tmpl_confs: []const conf.Config, target: *const fs.Path) !void { + try render_files_recursively(env, target, &self.path, "", tmpl_confs); + } - const entry_path = try replace_vars_in_path(target, entry.path, tmpl_conf); - switch (entry.kind) { - .directory => { - try fs.mkdir_all(entry_path); + fn render_files_recursively(env: *const Env, dest_root: *const fs.Path, src_root: *const fs.Path, src_subpath: []const u8, tmpl_confs: []const conf.Config) !void { + const path: fs.Path = .join(.{ src_root.name(), src_subpath }); + var iter = try fs.iterate(&path); + defer iter.deinit(); + while (try iter.next()) |entry| { + const entry_subpath: fs.Path = .join(.{ src_subpath, entry.name }); + const src_path: fs.Path = .join(.{ src_root.name(), entry_subpath.name() }); + const dest_path = try replace_vars_in_path(dest_root, entry_subpath.name(), tmpl_confs); + switch (entry.type) { + .dir => { + try fs.mkdir(&dest_path); + try render_files_recursively(env, dest_root, src_root, entry_subpath.name(), tmpl_confs); }, .file => { - env.info("writing template file {s}", .{entry_path.name()[target.len + 1 ..]}); - const input = try fs.read_file(env.allocator, .join(.{ self.path.name(), entry.path })); + env.info("writing template file {s}", .{dest_path.name()[dest_root.len + 1 ..]}); + const input = try fs.read_file(env.allocator, &src_path); - var output_file = try std.fs.createFileAbsoluteZ(entry_path.name(), .{ .mode = 0o644 }); - defer output_file.close(); + var output_buf: [512]u8 = undefined; + var output_writer = try fs.file_writer(&dest_path, &output_buf, .{ .create = true }); + defer output_writer.deinit(); - var write_buf: [1024]u8 = undefined; - var output_writer = output_file.writer(&write_buf); const output = &output_writer.interface; - - try replace_vars(output, input.?, tmpl_conf); + try replace_vars(output, input, tmpl_confs); try output.flush(); }, - else => {}, + else => {}, // skip } } } /// Does not flush the writer. - fn replace_vars(w: *std.io.Writer, text: []const u8, tmpl_confs: []const conf.Config) !void { + fn replace_vars(w: *std.Io.Writer, text: []const u8, tmpl_confs: []const conf.Config) !void { var rest = text; while (rest.len > 0) { - const pos = std.mem.indexOfScalar(u8, rest, '{') orelse { + const pos = std.mem.findScalar(u8, rest, '{') orelse { try w.writeAll(rest); return; }; @@ -132,7 +195,7 @@ pub const Template = struct { begin += 1; } - const end = std.mem.indexOfScalarPos(u8, rest, begin, '}') orelse return error.missing_variable_closing; + const end = std.mem.findScalarPos(u8, rest, begin, '}') orelse return error.missing_variable_closing; if (end == rest.len - 1 or rest[end + 1] != '}') return error.missing_variable_closing; const var_name = rest[begin..end]; @@ -155,31 +218,32 @@ pub const Template = struct { } } - fn replace_vars_in_path(root: fs.Path, subpath: []const u8, tmpl_confs: []const conf.Config) !fs.Path { + fn replace_vars_in_path(root: *const fs.Path, subpath: []const u8, tmpl_confs: []const conf.Config) !fs.Path { assert(root.len > 0); assert(subpath.len > 0); const slash = std.fs.path.sep; - var res: fs.Path = root; + var res: fs.Path = root.*; res.buf[res.len] = slash; res.len += 1; - var res_writer = std.io.Writer.fixed(res.buf[res.len..]); + var res_writer = std.Io.Writer.fixed(res.buf[res.len..]); var name = subpath; while (name.len > 0) { - const idx = std.mem.indexOfScalar(u8, name, slash) orelse name.len - 1; + const idx = std.mem.findScalar(u8, name, slash) orelse name.len - 1; const part = name[0 .. idx + 1]; const pos = res_writer.end; replace_vars(&res_writer, part, tmpl_confs) catch |err| switch (err) { error.missing_variable_closing, error.missing_variable_name => { res_writer.undo(res_writer.end - pos); - try res_writer.writeAll(part); + res_writer.writeAll(part) catch unreachable; }, - else => return err, + error.WriteFailed => return error.PathTooLong, + else => unreachable, }; name = name[idx + 1 ..]; } - try res_writer.flush(); + res_writer.flush() catch unreachable; res.len += res_writer.end; res.buf[res.len] = 0; @@ -193,22 +257,19 @@ pub const Template = struct { pub const Iterator = struct { path: fs.Path, - dir: ?std.fs.Dir = null, - iter: ?std.fs.Dir.Iterator = null, + iter: ?fs.Iterator, pub fn deinit(self: *Iterator) void { - if (self.dir) |*dir| dir.close(); + if (self.iter) |*iter| iter.deinit(); } pub fn next(self: *Iterator) !?Template { if (self.iter) |*iter| { while (try iter.next()) |entry| { - if (entry.kind == .directory) { - var path = self.path; - path.append(.{entry.name}); + if (entry.type == .dir) { return .{ .name = entry.name, - .path = path, + .path = .join(.{ self.path.name(), entry.name }), }; } } @@ -217,59 +278,45 @@ pub const Iterator = struct { } }; -pub inline fn iterate(env: Env) !Iterator { - const path = etc_dir(env); - const dir = std.fs.openDirAbsoluteZ(path.name(), .{ .iterate = true }) catch |err| { - if (err == error.FileNotFound) { - return .{ .path = path }; - } +pub inline fn iterate(env: *const Env) !Iterator { + var tmpl_iter: Iterator = .{ .path = etc_dir(env), .iter = undefined }; + tmpl_iter.iter = fs.iterate(&tmpl_iter.path) catch |err| it: { + if (err == error.FileNotFound) break :it null; return err; }; - return .{ - .path = path, - .dir = dir, - .iter = dir.iterate(), - }; + return tmpl_iter; } -pub fn exists(env: Env, template_name: []const u8) !bool { +pub fn exists(env: *const Env, template_name: []const u8) !bool { return try get(env, template_name) != null; } -pub fn exists_any(env: Env) !bool { - var iter = try iterate(env); - defer iter.deinit(); - if (try iter.next()) |_| { - return true; - } - return false; -} - -pub fn get(env: Env, template_name: []const u8) !?Template { +pub fn get(env: *const Env, template_name: []const u8) !?Template { const tmpl: Template = .init(env, template_name); - const info = try fs.dir_info(tmpl.path); + const info = try fs.dir_info(&tmpl.path); return if (info.exists) tmpl else null; } -pub fn must_get(env: Env, template_name: []const u8) !Template { +pub fn must_get(env: *const Env, template_name: []const u8) !Template { return try get(env, template_name) orelse { fatal("template not found: {s}", .{template_name}); }; } -pub fn read_config(env: Env, tmpl: Template) !Config { +pub fn read_config(env: *const Env, tmpl: Template) !Config { return Config.from_file(env, tmpl); } -pub fn edit_config(env: Env, template_name: []const u8) !void { +pub fn edit_config(env: *const Env, template_name: []const u8) !void { const tmpl = try must_get(env, template_name); - try shell.edit_config_file(env, tmpl.config_file(), verify_config); + const config_file = tmpl.config_file(); + try shell.edit_config_file(env, &config_file, verify_config); } -pub fn import_config(env: Env, template_name: []const u8, new_config_file: fs.Path) !void { +pub fn import_config(env: *const Env, template_name: []const u8, new_config_file: *const fs.Path) !void { const tmpl = try must_get(env, template_name); - const parsed = try conf.Config.from_file(env.allocator, new_config_file.name()); + const parsed = try conf.Config.from_file(env.allocator, new_config_file); const config = switch (parsed) { .conf => |c| c, .err => |e| { @@ -282,29 +329,101 @@ pub fn import_config(env: Env, template_name: []const u8, new_config_file: fs.Pa if (!verify_config(env, config)) { std.process.exit(1); } - try fs.mv_file(new_config_file, tmpl.config_file()); + + const config_file = tmpl.config_file(); + try fs.mv_file(new_config_file, &config_file); } -pub fn etc_dir(env: Env) fs.Path { +pub fn etc_dir(env: *const Env) fs.Path { return env.etc_path(.{"tmpl"}); } -fn verify_config(env: Env, raw: conf.Config) bool { +fn verify_config(env: *const Env, raw: conf.Config) bool { const tmpl_conf: Config = .init(env, raw); var valid = true; - if (tmpl_conf.prepare_dist_target) |prepare_dist_target| { - if (std.mem.indexOfAny(u8, prepare_dist_target, " \n\t\r")) |_| { - cli.stderr.write_line("Error: invalid value `prepare-dist-target`", .{}); + if (tmpl_conf.prepare_target) |prepare_target| { + if (std.mem.findAny(u8, prepare_target, " \n\t\r")) |_| { + cli.stderr.write_line("Error: invalid value `dist.prepare-target`", .{}); valid = false; } } + for (tmpl_conf.archives[0..tmpl_conf.archives_len]) |archive| { + if (archive.root) |root| { + if (!fs.is_relative(root)) { + if (archive.id.len > 0) { + cli.stderr.write_line("Error: value `dist.archive[{s}].root` is not a relative path", .{archive.id}); + } else { + cli.stderr.write_line("Error: value `dist.archive.root` is not a relative path", .{}); + } + valid = false; + } + } + for (archive.paths[0..archive.paths_len]) |path| { + if (!fs.is_relative(path)) { + if (archive.id.len > 0) { + cli.stderr.write_line("Error: value `dist.archive[{s}].paths` contains a non-relative path", .{archive.id}); + } else { + cli.stderr.write_line("Error: value `dist.archive.paths` contains a non-relative path", .{}); + } + valid = false; + } + } + } return valid; } +fn split_paths(buf: [][]const u8, str: []const u8) usize { + var path_count: usize = 0; + var pos: usize = 0; + while (pos < str.len) { + pos = std.mem.findNonePos(u8, str, pos, " \t\r\n") orelse break; + var space = pos; + while (space < str.len) : (space += 1) { + switch (str[space]) { + ' ' => if (str[space - 1] != '\\') break, + '\t', '\r', '\n' => break, + else => {}, + } + } + + buf[path_count] = str[pos..space]; + path_count += 1; + pos = space + 1; + } + return path_count; +} + fn tmpl_corrupted(tmpl_name: []const u8, comptime reason: []const u8) noreturn { fatal("template {s} corrupted (" ++ reason ++ ")", .{tmpl_name}); } +test "parse config" { + var raw: conf.Config = .init(std.testing.allocator); + try raw.add_var("dist.prepare-target", "prepare-everything"); + try raw.add_var("dist.archive.root", "foo"); + try raw.add_var("dist.archive.paths", "foo"); + try raw.add_var("dist.archive[bar].root", "foo"); + try raw.add_var("dist.archive[bar].paths", "bar\nbaz"); + + const env: Env = undefined; + const config: Config = .init(&env, raw); + defer config.deinit(); + + try std.testing.expectEqualStrings("prepare-everything", config.prepare_target.?); + try std.testing.expectEqual(2, config.archives_len); + + try std.testing.expectEqualStrings("", config.archives[0].id); + try std.testing.expectEqualStrings("foo", config.archives[0].root.?); + try std.testing.expectEqual(1, config.archives[0].paths_len); + try std.testing.expectEqualStrings("foo", config.archives[0].paths[0]); + + try std.testing.expectEqualStrings("bar", config.archives[1].id); + try std.testing.expectEqualStrings("foo", config.archives[1].root.?); + try std.testing.expectEqual(2, config.archives[1].paths_len); + try std.testing.expectEqualStrings("bar", config.archives[1].paths[0]); + try std.testing.expectEqualStrings("baz", config.archives[1].paths[1]); +} + test "render simple template - variable at end" { const tmpl = "This is a simple {{test}} with multiple {{vars}} in different {{places}}"; @@ -316,7 +435,7 @@ test "render simple template - variable at end" { try config.add_var("vars", "variables"); try config.add_var("places", "positions"); - var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, 2 * tmpl.len); + var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, 2 * tmpl.len); defer rendered.deinit(); try Template.replace_vars(&rendered.writer, tmpl, &.{config}); @@ -335,7 +454,7 @@ test "render simple template - variable at beginning" { try config.add_var("vars", "variables"); try config.add_var("places", "positions"); - var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, 2 * tmpl.len); + var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, 2 * tmpl.len); defer rendered.deinit(); try Template.replace_vars(&rendered.writer, tmpl, &.{config}); @@ -348,7 +467,7 @@ test "render empty template" { const config: conf.Config = .init(std.testing.allocator); defer config.deinit(); - var rendered: std.io.Writer.Allocating = .init(std.testing.allocator); + var rendered: std.Io.Writer.Allocating = .init(std.testing.allocator); defer rendered.deinit(); try Template.replace_vars(&rendered.writer, tmpl, &.{config}); @@ -364,7 +483,7 @@ test "render template without braces" { try config.add_var("a", "foo"); try config.add_var("b", "bar"); - var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); + var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); defer rendered.deinit(); try Template.replace_vars(&rendered.writer, tmpl, &.{config}); @@ -380,7 +499,7 @@ test "render template without variable and brace" { try config.add_var("a", "foo"); try config.add_var("b", "bar"); - var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); + var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); defer rendered.deinit(); try Template.replace_vars(&rendered.writer, tmpl, &.{config}); @@ -395,7 +514,7 @@ test "render template with multiple opening braces" { try config.add_var("foo", "FOOBAR"); - var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); + var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); defer rendered.deinit(); try Template.replace_vars(&rendered.writer, tmpl, &.{config}); @@ -411,7 +530,7 @@ test "render template with missing variable closing" { try config.add_var("a", "foo"); try config.add_var("b", "bar"); - var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); + var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); defer rendered.deinit(); const res = Template.replace_vars(&rendered.writer, tmpl, &.{config}); @@ -427,7 +546,7 @@ test "render template with missing variable closing at the end" { try config.add_var("a", "foo"); try config.add_var("b", "bar"); - var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); + var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); defer rendered.deinit(); const res = Template.replace_vars(&rendered.writer, tmpl, &.{config}); @@ -443,7 +562,7 @@ test "render template with incomplete variable closing" { try config.add_var("a", "foo"); try config.add_var("b", "bar"); - var rendered: std.io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); + var rendered: std.Io.Writer.Allocating = try .initCapacity(std.testing.allocator, tmpl.len); defer rendered.deinit(); const res = Template.replace_vars(&rendered.writer, tmpl, &.{config}); @@ -459,18 +578,50 @@ test "replace variables in path" { var replaced: fs.Path = undefined; - replaced = try Template.replace_vars_in_path(.init("root"), ".config/foo", &.{config}); - try std.testing.expectEqualStrings("root/.config/foo", replaced.name()); + inline for ([_]struct { + root: []const u8, + subpath: []const u8, + expected: []const u8, + }{ + .{ .root = "root", .subpath = ".config/foo", .expected = "root/.config/foo" }, + .{ .root = "/usr/local", .subpath = "etc", .expected = "/usr/local/etc" }, + .{ .root = "/usr/local", .subpath = "etc/{{a}}.{{b}}", .expected = "/usr/local/etc/foo.bar" }, + .{ .root = "/usr/local", .subpath = "etc/{{/foo", .expected = "/usr/local/etc/{{/foo" }, + .{ .root = "/usr/local", .subpath = "etc/{{}}/foo", .expected = "/usr/local/etc/{{}}/foo" }, + }) |t| { + const root: fs.Path = .init(t.root); + replaced = try Template.replace_vars_in_path(&root, t.subpath, &.{config}); + try std.testing.expectEqualStrings(t.expected, replaced.name()); + } +} + +test "split paths" { + var paths: [8][]const u8 = undefined; + var n: usize = undefined; + + n = split_paths(&paths, ""); + try std.testing.expectEqual(0, n); + + n = split_paths(&paths, "dir"); + try std.testing.expectEqual(1, n); + try std.testing.expectEqualStrings("dir", paths[0]); - replaced = try Template.replace_vars_in_path(.init("/usr/local"), "etc", &.{config}); - try std.testing.expectEqualStrings("/usr/local/etc", replaced.name()); + n = split_paths(&paths, "dir/subdir"); + try std.testing.expectEqual(1, n); + try std.testing.expectEqualStrings("dir/subdir", paths[0]); - replaced = try Template.replace_vars_in_path(.init("/usr/local"), "etc/{{a}}.{{b}}", &.{config}); - try std.testing.expectEqualStrings("/usr/local/etc/foo.bar", replaced.name()); + n = split_paths(&paths, "dir/sub\\ dir"); + try std.testing.expectEqual(1, n); + try std.testing.expectEqualStrings("dir/sub\\ dir", paths[0]); - replaced = try Template.replace_vars_in_path(.init("/usr/local"), "etc/{{/foo", &.{config}); - try std.testing.expectEqualStrings("/usr/local/etc/{{/foo", replaced.name()); + n = split_paths(&paths, "dir_one/subdir dir_two"); + try std.testing.expectEqual(2, n); + try std.testing.expectEqualStrings("dir_one/subdir", paths[0]); + try std.testing.expectEqualStrings("dir_two", paths[1]); - replaced = try Template.replace_vars_in_path(.init("/usr/local"), "etc/{{}}/foo", &.{config}); - try std.testing.expectEqualStrings("/usr/local/etc/{{}}/foo", replaced.name()); + n = split_paths(&paths, "dir_one/subdir dir_two\ndir_three/foo\\ bar"); + try std.testing.expectEqual(3, n); + try std.testing.expectEqualStrings("dir_one/subdir", paths[0]); + try std.testing.expectEqualStrings("dir_two", paths[1]); + try std.testing.expectEqualStrings("dir_three/foo\\ bar", paths[2]); } |