diff options
| author | tsne <tsne.dev@outlook.com> | 2026-05-13 11:33:08 +0200 |
|---|---|---|
| committer | tsne <tsne.dev@outlook.com> | 2026-05-22 08:25:27 +0200 |
| commit | c5b88c66ca8956df26ed87a7397862009e18cfc7 (patch) | |
| tree | 5a862bdacdeaa7fff73bf1bb2a423eee38f9b6e5 /src | |
| parent | initial commit (diff) | |
| download | porteur-c5b88c66ca8956df26ed87a7397862009e18cfc7.tar.gz | |
upgrade to zig 0.16
Diffstat (limited to 'src')
| -rw-r--r-- | src/cli.zig | 543 | ||||
| -rw-r--r-- | src/conf.zig | 102 | ||||
| -rw-r--r-- | src/env.zig | 30 | ||||
| -rw-r--r-- | src/fs.zig | 500 | ||||
| -rw-r--r-- | src/git.zig | 54 | ||||
| -rw-r--r-- | src/main.zig | 256 | ||||
| -rw-r--r-- | src/port.zig | 472 | ||||
| -rw-r--r-- | src/shell.zig | 271 | ||||
| -rw-r--r-- | src/template.zig | 359 | ||||
| -rw-r--r-- | src/time.zig | 8 | ||||
| -rw-r--r-- | src/tree.zig | 29 |
11 files changed, 1660 insertions, 964 deletions
diff --git a/src/cli.zig b/src/cli.zig index 8c5e37a..5b43e96 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -26,16 +26,150 @@ //! during compile time. There are two versions for a help text: a short //! and a long version. The short version displays the usage pattern of //! the command whereas the long version displays a detailed help text. +//! +//! The CLI contains global variables for argument parsing. These variables +//! need to be initialized with the `init` function before the package can be +//! used. const std = @import("std"); const assert = std.debug.assert; -var stdout_buf: [1024]u8 = undefined; -var stdout_printer: Printer = .init(std.fs.File.stdout().writer(&stdout_buf)); -var stderr_printer: Printer = .init(std.fs.File.stderr().writer(&.{})); +var stdout_buf: [256]u8 = undefined; +var stderr_buf: [0]u8 = undefined; +var stdout_printer: Printer = .init(std.posix.STDOUT_FILENO, &stdout_buf); +var stderr_printer: Printer = .init(std.posix.STDERR_FILENO, &stderr_buf); + +pub const stdout = &stdout_printer; +pub const stderr = &stderr_printer; + +/// The command we are currently in or null if we reached the end or positional arguments. +var current_cmd: ?[]const u8 = null; +var arg_iter: std.process.Args.Iterator = undefined; +var exe_name: []const u8 = ""; + +/// Initialize the global CLI variables and return the program (i.e. the main +/// entry command). +pub fn init(comptime Program: type, args: std.process.Args) Program { + const cmd_info = comptime Command_Info.from_command(Program); + var program: Program = undefined; + + arg_iter = args.iterate(); + _ = arg_iter.skip(); // skip executable name + current_cmd = parse_opts(cmd_info, &program); + exe_name = cmd_info.name; + return program; +} + +/// Return the next subcommand of the given command type. The next subcommand +/// is the name of the next argument. It also parses all options and prefills +/// them in the returned subcommand. +pub fn next_subcommand(comptime Command: type) ?Command.Subcommands { + const subcommands = comptime Command_Info.from_subcommand(Command); + comptime assert(subcommands.len > 0); + + const arg = current_cmd orelse return null; + inline for (subcommands) |subcmd| { + if (std.mem.eql(u8, arg, subcmd.name)) { + var cmd: subcmd.type = undefined; + current_cmd = parse_opts(subcmd, &cmd); + return @unionInit(Command.Subcommands, subcmd.tag.?, cmd); + } + } + fatal_with_usage(Command, "unknown command '{s}' (see '{s} --help')", .{ arg, exe_name }); +} + +/// Return the next positional argument or `null` if there are no more arguments. +pub fn next_positional() ?[]const u8 { + if (current_cmd) |cmd| { + current_cmd = null; + return cmd; + } + return arg_iter.next(); +} + +/// Parse and fill all command options and return the current command. +fn parse_opts(comptime cmd_info: Command_Info, cmd: *cmd_info.type) ?[]const u8 { + inline for (cmd_info.options) |opt| { + @field(cmd.options, opt.field).init(); + } + + args_loop: while (arg_iter.next()) |arg| { + assert(arg.len > 0); + if (arg.len < 2 or arg[0] != '-') { + // We have either a single dash or a non-option here. Treat these as + // a positional or subcommand (however the executor chooses to use it). + return arg; + } -pub var stdout = &stdout_printer; -pub var stderr = &stderr_printer; + var opt_name: []const u8 = undefined; + if (arg[1] != '-') { + // We check for a short option name. + // If we have a boolean flag or an option name longer than a + // single character, we have both the name and the value in + // a single argument. Otherwise we fetch the next argument. + opt_name = arg[1..2]; + if (opt_name[0] == 'h') { + if (arg.len > 2) { + fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{arg}); + } + cmd_info.print_usage(stderr); + std.process.exit(0); + } + + inline for (cmd_info.options) |opt| { + if (opt.short_name) |short_name| { + if (short_name == opt_name[0]) { + const value: [:0]const u8 = if (arg.len > 2 or opt.arg_name.len == 0) arg[2..] else next: { + const next_arg = arg_iter.next(); + if (next_arg == null or next_arg.?[0] == '-') { + fatal("missing value for option '{c}'", .{opt_name[0]}); + } + break :next next_arg.?; + }; + @field(cmd.options, opt.field).parse(opt_name, value); + continue :args_loop; + } + } + } + } else if (arg.len > 2) { + // We check for a long option name. + // If we don't have the format `name=value`, we get the + // next argument lazily, because for boolean flags we + // don't need any extra argument. + opt_name = arg[2..]; + var value: ?[:0]const u8 = null; + if (std.mem.findScalar(u8, arg, '=')) |eq| { + opt_name = arg[2..eq]; + value = arg[eq + 1 ..]; + } + + if (std.mem.eql(u8, opt_name, "help")) { + cmd_info.print_help(); + } + + inline for (cmd_info.options) |opt| { + if (std.mem.eql(u8, opt_name, opt.long_name)) { + value = value orelse if (opt.arg_name.len == 0) "" else next_arg: { + const next = arg_iter.next(); + if (next == null or next.?[0] == '-') { + fatal("missing value for option '{s}'", .{opt_name}); + } + break :next_arg next.?; + }; + @field(cmd.options, opt.field).parse(opt_name, value.?); + continue :args_loop; + } + } + } else { + // A single "--" was detected which means the rest of the arguments + // are interpreted as positional. We are done parsing. + return null; + } + + fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{opt_name}); + } + return null; +} pub const Command_Help = struct { pub const Section = struct { @@ -215,145 +349,6 @@ pub fn Option(comptime T: type, comptime opt_help: Option_Help(T)) type { }; } -pub const Args = struct { - /// The command we are currently in or null if we reached the end or positional arguments. - current_cmd: ?[]const u8, - iter: std.process.ArgIterator, - exe_name: []const u8, - - pub fn init(allocator: std.mem.Allocator, comptime exe_name: []const u8, program: anytype) !Args { - comptime assert(exe_name.len > 0); - - var iter = try std.process.ArgIterator.initWithAllocator(allocator); - errdefer iter.deinit(); - _ = iter.skip(); // skip executable name - const cmd_info = Command_Info.from_command(switch (@typeInfo(@TypeOf(program))) { - .pointer => |info| info.child, - else => @compileError("expected pointer, got " ++ @typeName(@TypeOf(program))), - }); - const current_cmd = parse_opts(&iter, cmd_info, program); - return Args{ - .current_cmd = current_cmd, - .iter = iter, - .exe_name = exe_name, - }; - } - - pub fn deinit(self: *Args, allocator: std.mem.Allocator) void { - _ = allocator; - self.iter.deinit(); - } - - pub fn next_subcommand(self: *Args, comptime Command: type) ?Command.Subcommands { - const subcommands = comptime Command_Info.from_subcommand(Command); - comptime assert(subcommands.len > 0); - - const arg = self.current_cmd orelse return null; - inline for (subcommands) |subcmd| { - if (std.mem.eql(u8, arg, subcmd.name)) { - var cmd: subcmd.type = undefined; - self.current_cmd = parse_opts(&self.iter, subcmd, &cmd); - return @unionInit(Command.Subcommands, subcmd.tag.?, cmd); - } - } - fatal_with_usage(Command, "unknown command '{s}' (see '{s} --help')", .{ arg, self.exe_name }); - } - - pub fn next_positional(self: *Args) ?[]const u8 { - if (self.current_cmd) |cmd| { - self.current_cmd = null; - return cmd; - } - self.current_cmd = null; - return self.iter.next(); - } - - /// Parse and fill all command options and return the current command. - fn parse_opts(iter: *std.process.ArgIterator, comptime cmd_info: Command_Info, cmd: *cmd_info.type) ?[]const u8 { - inline for (cmd_info.options) |opt| { - @field(cmd.options, opt.field).init(); - } - - args_loop: while (iter.next()) |arg| { - assert(arg.len > 0); - if (arg.len < 2 or arg[0] != '-') { - // We have either a single dash or a non-option here. Treat these as - // a positional or subcommand (however the executor chooses to use it). - return arg; - } - - var opt_name: []const u8 = undefined; - if (arg[1] != '-') { - // We check for a short option name. - // If we have a boolean flag or an option name longer than a - // single character, we have both the name and the value in - // a single argument. Otherwise we fetch the next argument. - opt_name = arg[1..2]; - if (opt_name[0] == 'h') { - if (arg.len > 2) { - fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{arg}); - } - cmd_info.print_usage(stderr); - std.process.exit(0); - } - - inline for (cmd_info.options) |opt| { - if (opt.short_name) |short_name| { - if (short_name == opt_name[0]) { - const value: [:0]const u8 = if (arg.len > 2 or opt.arg_name.len == 0) arg[2..] else next: { - const next_arg = iter.next(); - if (next_arg == null or next_arg.?[0] == '-') { - fatal("missing value for option '{c}'", .{opt_name[0]}); - } - break :next next_arg.?; - }; - @field(cmd.options, opt.field).parse(opt_name, value); - continue :args_loop; - } - } - } - } else if (arg.len > 2) { - // We check for a long option name. - // If we don't have the format `name=value`, we get the - // next argument lazily, because for boolean flags we - // don't need any extra argument. - opt_name = arg[2..]; - var value: ?[:0]const u8 = null; - if (std.mem.indexOfScalar(u8, arg, '=')) |eq| { - opt_name = arg[2..eq]; - value = arg[eq + 1 ..]; - } - - if (std.mem.eql(u8, opt_name, "help")) { - cmd_info.print_help(); - } - - inline for (cmd_info.options) |opt| { - if (std.mem.eql(u8, opt_name, opt.long_name)) { - value = value orelse if (opt.arg_name.len == 0) "" else next_arg: { - const next = iter.next(); - if (next == null or next.?[0] == '-') { - fatal("missing value for option '{s}'", .{opt_name}); - } - break :next_arg next.?; - }; - @field(cmd.options, opt.field).parse(opt_name, value.?); - continue :args_loop; - } - } - } else { - // A single "--" was detected which means the rest of the arguments - // are interpreted as positional. We are done parsing. - return null; - } - - fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{opt_name}); - } - - return null; - } -}; - pub const Print_Options = struct { /// Indentation of the first line. margin: u16 = 0, @@ -369,25 +364,30 @@ const Usage_Column = struct { }; pub const Printer = struct { - writer: std.fs.File.Writer, + fd: std.posix.fd_t, max_width: u16, + writer: std.Io.Writer, - fn init(w: std.fs.File.Writer) Printer { + fn init(fd: std.posix.fd_t, buf: []u8) Printer { return Printer{ - .writer = w, + .fd = fd, .max_width = 80, + .writer = .{ + .vtable = &.{ .drain = drain }, + .buffer = buf, + }, }; } - /// Drain the remaining buffered data and puts it to the sink. + /// Drain the remaining buffered data and put it to the sink. pub fn flush(self: *Printer) void { - self.writer.interface.flush() catch {}; + self.writer.flush() catch {}; } /// Write a value directly without splitting. This uses the formatting capabilities /// of `std.fmt`. pub fn write(self: *Printer, comptime fmt: []const u8, value: anytype) void { - self.writer.interface.print(fmt, value) catch {}; + self.writer.print(fmt, value) catch {}; } /// Write a value directly without splitting and append a newline. This uses the @@ -420,7 +420,7 @@ pub const Printer = struct { self.put('\n'); } - /// Print a formatted text and split it into multiple lines if if exceeds + /// Print a formatted text and split it into multiple lines if it exceeds /// a certain width. The splitting behaviour can be configured using the /// given print options. pub fn printf(self: *Printer, comptime format: []const u8, args: anytype, opts: Print_Options) void { @@ -435,7 +435,7 @@ pub const Printer = struct { buf[buf.len - 3] = '.'; buf[buf.len - 2] = '.'; buf[buf.len - 1] = '.'; - break :dots buf[0..]; + break :dots &buf; }, }; self.print(text, opts); @@ -452,7 +452,7 @@ pub const Printer = struct { /// given line of text. pub fn print_lines(self: *Printer, text: []const u8, opts: Print_Options) void { var rest = text; - while (std.mem.indexOfScalar(u8, rest, '\n')) |eol| { + while (std.mem.findScalar(u8, rest, '\n')) |eol| { self.print_line(rest[0..eol], opts); rest = rest[eol + 1 ..]; } @@ -463,11 +463,11 @@ pub const Printer = struct { } fn put(self: *Printer, c: u8) void { - self.writer.interface.writeByte(c) catch {}; + self.writer.writeByte(c) catch {}; } fn put_str(self: *Printer, text: []const u8) void { - self.writer.interface.writeAll(text) catch {}; + self.writer.writeAll(text) catch {}; } fn print_usage_table(self: *Printer, comptime left_column: Usage_Column, comptime right_column: Usage_Column, opts: Print_Options) void { @@ -521,12 +521,12 @@ pub const Printer = struct { if (std.mem.allEqual(u8, text[0..last_split], line_split)) { // There are leading line splits here, so we consider an indentation. // We want to keep indentation intact and print to the next split. - eol = std.mem.indexOfScalarPos(u8, text, width, line_split) orelse text.len; + eol = std.mem.findScalarPos(u8, text, width, line_split) orelse text.len; } } else { // We don't have a split char in the line. So we extend the line - // to the next space. This hopefully will not happen in the wild. - eol = std.mem.indexOfScalarPos(u8, text, width, line_split) orelse text.len; + // to the next space. This will hopefully not happen in the wild. + eol = std.mem.findScalarPos(u8, text, width, line_split) orelse text.len; } self.put_str(text[0..eol]); @@ -536,11 +536,51 @@ pub const Printer = struct { fn put_indent(self: *Printer, n: usize) void { const buf = [_]u8{' '} ** 32; var remaining = n; - while (remaining > buf.len) : (remaining -= buf.len) { + while (remaining > buf.len) { self.put_str(&buf); + remaining -= buf.len; } self.put_str(buf[0..remaining]); } + + pub fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { + const self: *Printer = @alignCast(@fieldParentPtr("writer", w)); + var iovec: [8]std.posix.iovec_const = undefined; + var iovlen: usize = 0; + if (w.end > 0) { + iovec[iovlen] = .{ .base = w.buffer.ptr, .len = w.end }; + iovlen += 1; + } + var off: usize = 0; + while (iovlen < iovec.len and off < data.len - 1) { + if (data[off].len > 0) { + iovec[iovlen] = .{ .base = data[off].ptr, .len = data[off].len }; + iovlen += 1; + } + off += 1; + } + if (data[data.len - 1].len > 0) { + off = 0; + const buf = data[data.len - 1]; + while (iovlen < iovec.len and off < splat) { + iovec[iovlen] = .{ .base = buf.ptr, .len = buf.len }; + iovlen += 1; + off += 1; + } + } + + if (iovlen == 0) return 0; + + const n: usize = while (true) { + const res = std.posix.system.writev(self.fd, &iovec, @intCast(iovlen)); + if (res < 0) { + if (std.posix.errno(res) == .INTR) continue; + return error.WriteFailed; + } + break @intCast(res); + }; + return w.consume(n); + } }; pub fn fatal(comptime format: []const u8, args: anytype) noreturn { @@ -570,21 +610,32 @@ pub fn print_help(comptime Command: type) noreturn { } pub fn ask(comptime prompt: []const u8, args: anytype, buf: []u8) ?[]const u8 { - var stdin = std.fs.File.stdin().reader(buf); + assert(buf.len > 0); + + const stdin: std.posix.fd_t = std.posix.STDIN_FILENO; + var off: usize = 0; while (true) { stderr.printf(prompt, args, .{}); stderr.flush(); - const line = stdin.interface.takeDelimiterExclusive('\n') catch |err| switch (err) { - error.StreamTooLong => { - stderr.printf_line("ERROR: The input exceeds the maximum of {} characters.", .{buf.len}, .{}); - continue; - }, - else => unreachable, - }; - const input = std.mem.trim(u8, line, " \t\r"); - return if (input.len == 0) null else input; + const res = std.posix.system.read(stdin, buf.ptr + off, buf.len - off); + assert(res >= 0); + if (res == 0) break; + + const n: usize = @intCast(res); + if (std.mem.findScalar(u8, buf[off .. off + n], '\n')) |idx| { + off += idx; + break; + } + off += n; + if (off >= buf.len) { + stderr.printf_line("ERROR: The input exceeds the maximum of {} characters.", .{buf.len}, .{}); + off = 0; + } } + + const input = std.mem.trim(u8, buf[0..off], " \t\r"); + return if (input.len == 0) null else input; } pub fn ask_confirmation(comptime prompt: []const u8, args: anytype, comptime default_answer: enum { yes, no }) bool { @@ -612,7 +663,7 @@ const Pager = struct { text: []const u8, dumb: bool, - pub fn format(self: Bold, w: *std.io.Writer) std.io.Writer.Error!void { + pub fn format(self: Bold, w: *std.Io.Writer) std.Io.Writer.Error!void { if (!self.dumb) try w.writeAll("\x1b[1m"); try w.writeAll(self.text); if (!self.dumb) try w.writeAll("\x1b[m"); @@ -624,56 +675,85 @@ const Pager = struct { dumb: bool, fn init(out_buf: []u8) Pager { - if (!stdout.writer.file.isTty()) { - return .{ .pid = null, .printer = stdout_printer, .dumb = true }; - } + if (std.posix.system.isatty(stdout_printer.fd) != 1) return .init_dumb(); - const fds = std.posix.pipe() catch return .{ .pid = null, .printer = stdout_printer, .dumb = true }; - const fdin = fds[0]; - const fdout = fds[1]; + var stdin_pipe: [2]std.posix.fd_t = undefined; + if (std.posix.system.pipe(&stdin_pipe) != 0) return .init_dumb(); - const pid = std.posix.fork() catch { - std.posix.close(fdin); - std.posix.close(fdout); - return .{ .pid = null, .printer = stdout_printer, .dumb = true }; - }; + const pid = std.posix.system.fork(); + switch (pid) { + -1 => { + _ = std.posix.system.close(stdin_pipe[0]); + _ = std.posix.system.close(stdin_pipe[1]); + return .init_dumb(); + }, + 0 => { + // pager process + if (std.posix.system.dup2(stdin_pipe[0], std.posix.STDIN_FILENO) == -1) unreachable; + _ = std.posix.system.close(stdin_pipe[0]); + _ = std.posix.system.close(stdin_pipe[1]); - if (pid == 0) { - // pager process - std.posix.close(fdout); - std.posix.dup2(fdin, std.posix.STDIN_FILENO) catch unreachable; + var env: [512:null]?[*:0]const u8 = undefined; + var envlen: usize = 0; + inline for (&[_][*:0]const u8{ + "LESSCHARSET=utf-8", + }) |predefined_env| { + env[envlen] = predefined_env; + envlen += 1; + } - var env: [512:null]?[*:0]const u8 = undefined; - var envlen: usize = 0; - const environ = if (std.os.environ.len < 512) std.os.environ else std.os.environ[0..511]; - for (environ) |e| { - env[envlen] = e; - envlen += 1; - } - env[envlen] = null; + while (std.posix.system.environ[envlen]) |e| { + env[envlen] = e; + envlen += 1; + if (envlen >= env.len - 1) break; + } + env[envlen] = null; - // TODO: Allow environment PAGER to define custom pagers. - const pager_argv: [*:null]const ?[*:0]const u8 = &.{ "less", "-R", null }; + // TODO: Allow environment PAGER to define custom pagers. + const Pager_Prog = struct { + path: [:0]const u8, + argv: [*:null]const ?[*:0]const u8, + }; - std.posix.execvpeZ(pager_argv[0].?, pager_argv, &env) catch unreachable; - std.process.exit(1); - } else { - // application process - std.posix.close(fdin); - var out: std.fs.File = .{ .handle = fdout }; - return .{ - .pid = pid, - .printer = Printer.init(out.writer(out_buf)), - .dumb = if (std.posix.getenv("TERM")) |term| std.mem.eql(u8, term, "dumb") else true, - }; + for ([_]Pager_Prog{ + .{ .path = "/usr/bin/less", .argv = &.{ "less", "-R", null } }, + .{ .path = "/bin/cat", .argv = &.{ "cat", null } }, + }) |pager| { + const res = std.posix.system.execve(pager.path, pager.argv, &env); + switch (std.posix.errno(res)) { + .NOENT => continue, + else => |err| fatal("error executing pager: {}", .{err}), + } + } + fatal("no pager found", .{}); + }, + else => { + // application process + _ = std.posix.system.close(stdin_pipe[0]); + var dumb = true; + if (std.posix.system.getenv("TERM")) |term| { + dumb = term[0] == 'd' and term[1] == 'u' and term[2] == 'm' and term[3] == 'b'; + } + + return .{ + .pid = pid, + .printer = .init(stdin_pipe[1], out_buf), + .dumb = dumb, + }; + }, } } + fn init_dumb() Pager { + return .{ .pid = null, .printer = stdout_printer, .dumb = true }; + } + fn deinit(self: *Pager) noreturn { self.printer.flush(); if (self.pid) |pid| { - self.printer.writer.file.close(); - _ = std.posix.waitpid(pid, 0); + var status: c_int = undefined; + _ = std.posix.system.close(self.printer.fd); + _ = std.posix.system.waitpid(pid, &status, 0); } std.process.exit(0); } @@ -808,7 +888,7 @@ const Command_Info = struct { }; } - fn from_subcommand(comptime Command: type) []Command_Info { + fn from_subcommand(comptime Command: type) []const Command_Info { if (!@hasDecl(Command, "Subcommands")) { return &.{}; } @@ -819,12 +899,15 @@ const Command_Info = struct { }; assert(union_info.tag_type != null); - var subcmds: [union_info.fields.len]Command_Info = undefined; - for (union_info.fields, 0..) |field, i| { - subcmds[i] = Command_Info.from_command(field.type); - subcmds[i].tag = field.name; - } - return &subcmds; + const infos = comptime sub: { + var subcmds: [union_info.fields.len]Command_Info = undefined; + for (union_info.fields, 0..) |field, i| { + subcmds[i] = Command_Info.from_command(field.type); + subcmds[i].tag = field.name; + } + break :sub subcmds; + }; + return &infos; } /// Flushes the printer @@ -863,7 +946,7 @@ const Command_Info = struct { p.put('\n'); // subcommands - const subcmds = Command_Info.from_subcommand(cmd.type); + const subcmds = comptime Command_Info.from_subcommand(cmd.type); if (subcmds.len > 0) { const name_col = comptime Usage_Table.column(Command_Info, subcmds, struct { fn extract(comptime subcmd: Command_Info) []const u8 { @@ -886,7 +969,7 @@ const Command_Info = struct { if (cmd.options.len > 0) { const title_col = comptime Usage_Table.column(Option_Info, cmd.options, struct { fn extract(comptime opt: Option_Info) []const u8 { - const argname = if (opt.arg_name.len == 0) "" else " <" ++ opt.arg_name ++ ">"; + const argname = if (opt.arg_name.len == 0) "" else " ⟨" ++ opt.arg_name ++ "⟩"; return if (opt.short_name) |short| std.fmt.comptimePrint("-{c}, --{s}{s}", .{ short, opt.long_name, argname }) else @@ -940,7 +1023,7 @@ const Command_Info = struct { if (cmd.options.len > 0) { p.printf_line("{f}", .{pager.bold("OPTIONS")}, .{ .margin = margin0 }); inline for (cmd.options) |opt| { - const argname = if (opt.arg_name.len == 0) "" else " <" ++ opt.arg_name ++ ">"; + const argname = if (opt.arg_name.len == 0) "" else " ⟨" ++ opt.arg_name ++ "⟩"; if (opt.short_name) |short| { p.printf_line("-{f}, --{f}{s}", .{ pager.bold(&[1]u8{short}), pager.bold(opt.long_name), argname }, .{ .margin = margin1 }); } else { @@ -952,7 +1035,7 @@ const Command_Info = struct { } // subcommands - const subcmds = Command_Info.from_subcommand(cmd.type); + const subcmds = comptime Command_Info.from_subcommand(cmd.type); if (subcmds.len > 0) { p.printf_line("{f}", .{pager.bold("COMMANDS")}, .{ .margin = margin0 }); inline for (subcmds) |subcmd| { @@ -1065,16 +1148,6 @@ const Option_Info = struct { }; return &options; } - - fn contains_whitespace(comptime text: []const u8) bool { - for (text) |c| { - switch (c) { - ' ', '\t', '\r', '\n' => return true, - else => {}, - } - } - return false; - } }; fn sanitize_name(comptime name: []const u8) []const u8 { @@ -1138,6 +1211,16 @@ fn contains_eol(comptime text: []const u8) bool { return false; } +fn contains_whitespace(comptime text: []const u8) bool { + for (text) |c| { + switch (c) { + ' ', '\t', '\r', '\n' => return true, + else => {}, + } + } + return false; +} + fn is_uppercase(comptime text: []const u8) bool { for (text) |c| { if ('a' <= c and c <= 'z') return false; @@ -1151,7 +1234,7 @@ fn join_enum_values(comptime E: type) []const u8 { 0 => unreachable, 1 => "'" ++ fields[0].name ++ "'", 2 => "'" ++ fields[0].name ++ "' or '" ++ fields[1].name ++ "'", - 3 => str: { + else => str: { var text: []const u8 = ""; for (fields[0 .. fields.len - 1]) |field| { text = text ++ "'" ++ field.name ++ "', "; diff --git a/src/conf.zig b/src/conf.zig index 419c369..97238c3 100644 --- a/src/conf.zig +++ b/src/conf.zig @@ -1,21 +1,23 @@ const std = @import("std"); const assert = std.debug.assert; +const fs = @import("fs.zig"); + pub const Var = struct { key: []const u8, val: []const u8, - pub fn format(self: Var, w: *std.io.Writer) !void { + pub fn format(self: Var, w: *std.Io.Writer) !void { const spacebuf = [_]u8{' '} ** 32; const equal_sign = " = "; try w.writeAll(self.key); try w.writeAll(equal_sign); - if (std.mem.indexOfScalar(u8, self.val, '\n')) |eol0| { + if (std.mem.findScalar(u8, self.val, '\n')) |eol0| { try w.writeAll(self.val[0 .. eol0 + 1]); var val = self.val[eol0 + 1 ..]; while (val.len > 0) { - const eol = std.mem.indexOfScalar(u8, val, '\n') orelse val.len - 1; + const eol = std.mem.findScalar(u8, val, '\n') orelse val.len - 1; const line = val[0 .. eol + 1]; var spaces = self.key.len; while (spaces > 0) { @@ -86,45 +88,44 @@ pub const Config = struct { return .{ .allocator = allocator, .buf = &.{}, .len = 0 }; } - pub fn from_file(allocator: std.mem.Allocator, path: []const u8) !Parse_Result { - var file = std.fs.openFileAbsolute(path, .{}) catch |err| { - return switch (err) { - error.FileNotFound => .{ .conf = .init(allocator) }, - else => err, - }; + pub fn from_file(allocator: std.mem.Allocator, path: *const fs.Path) !Parse_Result { + var read_buf: [512]u8 = undefined; + var r = fs.file_reader(path, &read_buf) catch |err| { + if (err == error.FileNotFound) return .{ .conf = .init(allocator) }; + return err; }; - defer file.close(); - - var read_buf: [1024]u8 = undefined; - var reader = file.reader(&read_buf); - return parse(allocator, &reader.interface, read_buf.len); + defer r.deinit(); + return parse(allocator, &r.interface, read_buf.len); } - fn parse(allocator: std.mem.Allocator, reader: *std.io.Reader, comptime max_line_len: usize) !Parse_Result { + /// Parse the string in the given buffer. The buffer is reused to store the raw config data. + fn parse(allocator: std.mem.Allocator, reader: *std.Io.Reader, comptime max_line_len: usize) !Parse_Result { const spaces = " \t\r"; - var allocating: std.io.Writer.Allocating = .init(allocator); - + var allocating: std.Io.Writer.Allocating = .init(allocator); var header: Var_Header = undefined; var header_off: usize = 0; var need_key = true; var file_line: usize = 0; while (true) { file_line += 1; - const raw_line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) { - error.StreamTooLong => { - return .{ - .err = .{ - .msg = std.fmt.comptimePrint("key/value line exceeds the maximum of {} bytes", .{max_line_len}), - .line = file_line, - }, - }; - }, - error.EndOfStream => { - break; - }, - else => unreachable, + const raw_line = reader.takeDelimiterExclusive('\n') catch |err| { + switch (err) { + error.StreamTooLong => { + return .{ + .err = .{ + .msg = std.fmt.comptimePrint("key/value line exceeds the maximum of {} bytes", .{max_line_len}), + .line = file_line, + }, + }; + }, + error.EndOfStream => { + break; + }, + else => unreachable, + } }; + if (reader.bufferedLen() > 0) reader.toss(1); const line = std.mem.trim(u8, raw_line, spaces); if (line.len == 0 or line[0] == '#') { @@ -142,8 +143,8 @@ pub const Config = struct { }; }; - const key = std.mem.trimRight(u8, line[0..eq], spaces); - const val = std.mem.trimLeft(u8, line[eq + 1 ..], spaces); + const key = std.mem.trimEnd(u8, line[0..eq], spaces); + const val = std.mem.trimStart(u8, line[eq + 1 ..], spaces); if (key.len == 0) { // We have a value continuation here. So we append a newline @@ -235,11 +236,10 @@ pub const Config = struct { }; test "parse config - empty" { - var input = std.io.Reader.fixed(""); - var buf: [32]u8 = undefined; - var reader = std.io.Reader.limited(&input, std.io.Limit.limited(input.buffer.len), &buf); + const input = ""; + var input_r = std.Io.Reader.fixed(input); - var result = try Config.parse(std.testing.allocator, &reader.interface, buf.len); + var result = try Config.parse(std.testing.allocator, &input_r, input.len); try std.testing.expect(result == .conf); try std.testing.expectEqualSlices(u8, "", result.conf.buf[0..result.conf.len]); defer result.conf.deinit(); @@ -248,12 +248,11 @@ test "parse config - empty" { try std.testing.expect(vars.next() == null); } -test "parse config - singe line" { - var input = std.io.Reader.fixed("foo one = bar one"); - var buf: [32]u8 = undefined; - var reader = std.io.Reader.limited(&input, std.io.Limit.limited(input.buffer.len), &buf); +test "parse config - single line" { + const input = "foo one = bar one"; + var input_r = std.Io.Reader.fixed(input); - var result = try Config.parse(std.testing.allocator, &reader.interface, buf.len); + var result = try Config.parse(std.testing.allocator, &input_r, input.len); try std.testing.expect(result == .conf); try std.testing.expectEqualSlices( u8, @@ -272,8 +271,8 @@ test "parse config - singe line" { } test "parse config - mixed" { - var input = std.io.Reader.fixed( - \\foo_one = bar one + const input = + \\ foo_one = bar one \\foo_two = bar two 1 \\ = bar two 2 \\ = @@ -292,11 +291,10 @@ test "parse config - mixed" { \\ \\foo_five = \\ - ); - var buf: [32]u8 = undefined; - var reader = std.io.Reader.limited(&input, std.io.Limit.limited(input.buffer.len), &buf); + ; + var input_r = std.Io.Reader.fixed(input); - var result = try Config.parse(std.testing.allocator, &reader.interface, buf.len); + var result = try Config.parse(std.testing.allocator, &input_r, input.len); try std.testing.expect(result == .conf); try std.testing.expectEqualSlices( u8, @@ -336,7 +334,8 @@ test "parse config - mixed" { } test "parse config - with comment" { - var input = std.io.Reader.fixed( + const input = + \\ \\# first var: \\foo_one = bar one \\# second var: @@ -348,11 +347,10 @@ test "parse config - with comment" { \\foo_three = \\ = bar three 2 \\ = bar three 3 - ); - var buf: [32]u8 = undefined; - var reader = std.io.Reader.limited(&input, std.io.Limit.limited(input.buffer.len), &buf); + ; + var input_r = std.Io.Reader.fixed(input); - var result = try Config.parse(std.testing.allocator, &reader.interface, buf.len); + var result = try Config.parse(std.testing.allocator, &input_r, input.len); try std.testing.expect(result == .conf); try std.testing.expectEqualSlices( u8, diff --git a/src/env.zig b/src/env.zig index 7e8b355..bb1e8bc 100644 --- a/src/env.zig +++ b/src/env.zig @@ -11,7 +11,6 @@ const Self = @This(); allocator: std.mem.Allocator, prefix: []const u8, etc: []const u8, // ${PREFIX}/etc/ -args: *cli.Args, config: conf.Config, ports_dir: []const u8, @@ -21,26 +20,30 @@ default_category: ?[]const u8, default_repo_branch: []const u8, editor_cmd: ?[]const u8, -pub fn init(allocator: std.mem.Allocator, etc: []const u8, args: *cli.Args) Self { +_proc_env: std.process.Environ, + +pub fn init(allocator: std.mem.Allocator, env: std.process.Environ, etc: []const u8) Self { const prefix = if (options.path_prefix[options.path_prefix.len - 1] == '/') options.path_prefix[0 .. options.path_prefix.len - 1] else options.path_prefix; - const config = read_config(allocator, .join(.{ etc, "porteur/porteur.conf" })); + const path: fs.Path = .join(.{ etc, "porteur/porteur.conf" }); + const config = read_config(allocator, &path); var self: Self = .{ .allocator = allocator, .prefix = prefix, .etc = etc, - .args = args, .config = config, - .ports_dir = std.posix.getenvZ("PORTSDIR") orelse "/usr/ports", + .ports_dir = env.getPosix("PORTSDIR") orelse "/usr/ports", .distfiles_dir = prefix ++ "/porteur/distfiles/{{TREENAME}}", .distfiles_history = 1, .default_category = null, .default_repo_branch = "main", .editor_cmd = null, + + ._proc_env = env, }; var iter = config.iterate(); @@ -64,7 +67,7 @@ pub fn init(allocator: std.mem.Allocator, etc: []const u8, args: *cli.Args) Self } res = a[0]; } - break :history res; + break :history @max(1, res); }; } else if (std.mem.eql(u8, v.key, "category") and v.val.len > 0) { self.default_category = v.val; @@ -105,7 +108,7 @@ pub fn ports_dist_dir(self: Self, tree_name: []const u8, port_name: []const u8) .{ .key = "{{TREENAME}}", .val = tree_name }, .{ .key = "{{PORTNAME}}", .val = port_name }, }) |v| { - if (std.mem.indexOf(u8, src, v.key)) |idx| { + if (std.mem.find(u8, src, v.key)) |idx| { @memcpy(dest.buf[dest.len .. dest.len + idx], src[0..idx]); dest.len += idx; @memcpy(dest.buf[dest.len .. dest.len + v.val.len], v.val); @@ -139,6 +142,11 @@ pub fn work_path(self: Self, subpath: anytype) fs.Path { return p; } +/// Return the value of process' environment variable with the given name. +pub fn get(self: Self, name: []const u8) ?[:0]const u8 { + return self._proc_env.getPosix(name); +} + pub fn alloc(self: Self, size: usize) []u8 { return self.allocator.alloc(u8, size) catch cli.fatal("out of memory", .{}); } @@ -147,6 +155,10 @@ pub fn dealloc(self: Self, bytes: []const u8) void { self.allocator.free(bytes); } +pub fn dupe(self: Self, bytes: []const u8) []u8 { + return self.allocator.dupe(u8, bytes) catch cli.fatal("out of memory", .{}); +} + pub fn err(self: Self, comptime fmt: []const u8, args: anytype) void { _ = self; cli.stderr.write_line("Error: " ++ fmt, args); @@ -165,8 +177,8 @@ pub fn info(self: Self, comptime fmt: []const u8, args: anytype) void { cli.stdout.flush(); } -fn read_config(allocator: std.mem.Allocator, conf_file: fs.Path) conf.Config { - if (conf.Config.from_file(allocator, conf_file.name())) |res| { +fn read_config(allocator: std.mem.Allocator, conf_file: *const fs.Path) conf.Config { + if (conf.Config.from_file(allocator, conf_file)) |res| { return switch (res) { .conf => |c| c, .err => |e| empty: { @@ -3,8 +3,13 @@ const assert = std.debug.assert; const fatal = @import("cli.zig").fatal; +pub const max_path_bytes = std.Io.Dir.max_path_bytes; + +const default_file_mode: std.posix.mode_t = 0o644; +const default_dir_mode: std.posix.mode_t = 0o755; + pub const Path = struct { - buf: [std.fs.max_path_bytes]u8, + buf: [max_path_bytes]u8, len: usize, pub fn init(path: []const u8) Path { @@ -29,12 +34,12 @@ pub const Path = struct { inline for (info.fields) |field| { var seg: []const u8 = @field(segments, field.name); if (seg.len > 0) { - if (self.len > 0 and seg[0] == std.fs.path.sep) seg = seg[1..]; - if (seg[seg.len - 1] == std.fs.path.sep) seg = seg[0 .. seg.len - 1]; + if (self.len > 0 and seg[0] == '/') seg = seg[1..]; + if (seg[seg.len - 1] == '/') seg = seg[0 .. seg.len - 1]; } if (seg.len > 0) { if (self.len > 0) { - self.buf[self.len] = std.fs.path.sep; + self.buf[self.len] = '/'; self.len += 1; } @memcpy(self.buf[self.len .. self.len + seg.len], seg); @@ -48,146 +53,189 @@ pub const Path = struct { return self.buf[0..self.len :0]; } - pub fn basename(self: *const Path) []const u8 { - return std.fs.path.basename(self.name()); - } - pub fn dir(self: *const Path) ?Path { - const dirname = std.fs.path.dirname(self.name()) orelse return null; - return .init(dirname); + if (self.len == 0) return null; + const path = self.buf[0..if (self.buf[self.len - 1] == '/') self.len - 1 else self.len]; + const off = std.mem.findScalarLast(u8, path, '/') orelse return .init("/"); + return .init(path[0..off]); } /// Returns the relative name of `self` regarding to `root`. /// Both paths must be absolute. - pub fn relname(self: *const Path, root: Path) ?[]const u8 { - assert(std.fs.path.isAbsolute(root.name())); - assert(std.fs.path.isAbsolute(self.name())); + pub fn relname(self: *const Path, root: *const Path) ?[]const u8 { + assert(std.Io.Dir.path.isAbsolute(root.name())); + assert(std.Io.Dir.path.isAbsolute(self.name())); if (!std.mem.startsWith(u8, self.name(), root.name())) return null; if (self.len == root.len) return ""; var rel = self.buf[root.len..self.len]; - if (rel[0] == std.fs.path.sep) rel = rel[1..]; + if (rel[0] == '/') rel = rel[1..]; return rel; } }; -pub fn mkdir_all(path: Path) !void { - try std.fs.cwd().makePath(path.name()); +pub fn mkdir(path: *const Path) !void { + const res = std.posix.system.mkdirat(std.posix.AT.FDCWD, path.name(), default_dir_mode); + try result_to_error(res); } -pub fn touch(path: Path) !void { - const cwd = std.fs.cwd(); - if (std.fs.path.dirname(path.name())) |dir| { - try cwd.makePath(dir); - } - const f = try cwd.createFileZ(path.name(), .{ .mode = 0o644 }); - f.close(); +pub fn mkdir_all(path: *const Path) !void { + const info = try dir_info(path); + if (info.exists) return; + if (path.dir()) |parent| try mkdir_all(&parent); + try mkdir(path); } -pub fn rm(path: Path) !void { - std.fs.deleteTreeAbsolute(path.name()) catch |err| { - if (err != error.FileNotFound) return err; - }; -} +pub fn touch(path: *const Path) !void { + if (path.dir()) |dir| try mkdir_all(&dir); -pub fn cp_file(from_path: Path, to_path: Path) !void { - const from = from_path.name(); - const to = to_path.name(); - std.fs.deleteTreeAbsolute(to) catch |err| { - if (err != error.FileNotFound) return err; - }; - if (to_path.dir()) |to_parent| try mkdir_all(to_parent); - try std.fs.copyFileAbsolute(from, to, .{}); + const res = std.posix.system.openat(std.posix.AT.FDCWD, path.name(), .{ .CREAT = true }, default_file_mode); + try result_to_error(res); + _ = std.posix.system.close(@intCast(res)); } -pub fn cp_dir(allocator: std.mem.Allocator, from_path: Path, to_path: Path) !void { - const from = from_path.name(); - const to = to_path.name(); - std.fs.deleteTreeAbsolute(to) catch |err| { - if (err != error.FileNotFound) return err; +pub fn rm(path: *const Path) !void { + var iter = iterate(path) catch |err| { + switch (err) { + error.NotDir => { + const res = std.posix.system.unlinkat(std.posix.AT.FDCWD, path.name(), 0); + try result_to_error(res); + return; + }, + error.FileNotFound => return, + else => return err, + } }; - try mkdir_all(to_path); - try cp_tree(allocator, from, to); + defer iter.deinit(); + + while (try iter.next()) |entry| { + const child_path: Path = .join(.{ path.name(), entry.name }); + switch (entry.type) { + .dir => { + try rm(&child_path); + }, + else => { + const res = std.posix.system.unlinkat(std.posix.AT.FDCWD, child_path.name(), 0); + if (res == -1) return result_to_error(res); + }, + } + } + + const res = std.posix.system.rmdir(path.name()); + try result_to_error(res); } -pub fn mv_dir(allocator: std.mem.Allocator, from_path: Path, to_path: Path) !void { - const from = from_path.name(); - const to = to_path.name(); - std.fs.deleteTreeAbsolute(to) catch |err| { - if (err != error.FileNotFound) return err; - }; - if (to_path.dir()) |to_parent| try mkdir_all(to_parent); +pub fn cp_file(from: *const Path, to: *const Path) !void { + try rm(to); + if (to.dir()) |to_parent| try mkdir_all(&to_parent); - std.fs.renameAbsoluteZ(from, to) catch |err| { - if (err != error.RenameAcrossMountPoints) return err; - try std.fs.makeDirAbsoluteZ(to); - try cp_tree(allocator, from, to); - try rm(from_path); - }; + var res = std.posix.system.openat(std.posix.AT.FDCWD, from.name(), .{}); + try result_to_error(res); + const from_fd: std.posix.fd_t = @intCast(res); + defer _ = std.posix.system.close(from_fd); + + res = std.posix.system.openat(std.posix.AT.FDCWD, to.name(), .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, default_file_mode); + try result_to_error(res); + const to_fd: std.posix.fd_t = @intCast(res); + defer _ = std.posix.system.close(to_fd); + + var buf: [4096]u8 = undefined; + while (true) { + var rres = std.posix.system.read(from_fd, &buf, buf.len); + if (rres < 0) { + const err = result_to_error(rres); + if (err == error.Interrupted) continue; + return err; + } + if (rres == 0) break; + const bytes_read: usize = @intCast(rres); + var bytes_written: usize = 0; + while (bytes_written < bytes_read) { + const wbuf = buf[bytes_written..bytes_read]; + rres = std.posix.system.write(to_fd, wbuf.ptr, wbuf.len); + if (rres < 0) { + const err = result_to_error(rres); + if (err == error.Interrupted) continue; + } + bytes_written += @intCast(rres); + } + } } -pub fn mv_file(from_path: Path, to_path: Path) !void { - const from = from_path.name(); - const to = to_path.name(); - std.fs.deleteTreeAbsolute(to) catch |err| { - if (err != error.FileNotFound) return err; - }; - if (to_path.dir()) |to_parent| try mkdir_all(to_parent); +pub fn mv_dir(from: *const Path, to: *const Path) !void { + try rm(to); + if (to.dir()) |to_parent| try mkdir_all(&to_parent); - std.fs.renameAbsoluteZ(from, to) catch |err| { - if (err != error.RenameAcrossMountPoints) return err; - try std.fs.copyFileAbsolute(from, to, .{}); - try rm(from_path); - }; + const res = std.posix.system.renameat(std.posix.AT.FDCWD, from.name(), std.posix.AT.FDCWD, to.name()); + if (res == 0) return; + + const err = result_to_error(res); + if (err != error.CrossDevice) return err; + try mkdir(to); + try cp_tree(from, to); + try rm(from); } -fn cp_tree(allocator: std.mem.Allocator, from: [:0]const u8, to: [:0]const u8) !void { - var from_dir = try std.fs.openDirAbsoluteZ(from, .{ .iterate = true }); - defer from_dir.close(); +pub fn mv_file(from: *const Path, to: *const Path) !void { + try rm(to); + if (to.dir()) |to_parent| try mkdir_all(&to_parent); - var walker = try from_dir.walk(allocator); - defer walker.deinit(); - while (try walker.next()) |entry| { - const dest: Path = .join(.{ to, entry.path }); - switch (entry.kind) { - .directory => try std.fs.makeDirAbsoluteZ(dest.name()), + const res = std.posix.system.renameat(std.posix.AT.FDCWD, from.name(), std.posix.AT.FDCWD, to.name()); + if (res == 0) return; + + const err = result_to_error(res); + if (err != error.CrossDevice) return err; + try cp_file(from, to); + try rm(from); +} + +fn cp_tree(from: *const Path, to: *const Path) !void { + var iter = try iterate(from); + defer iter.deinit(); + while (try iter.next()) |entry| { + const from_child: Path = .join(.{ from.name(), entry.name }); + const to_child: Path = .join(.{ to.name(), entry.name }); + switch (entry.type) { + .dir => { + try mkdir(&to_child); + try cp_tree(&from_child, &to_child); + }, .file => { - const src: Path = .join(.{ from, entry.path }); - try std.fs.copyFileAbsolute(src.name(), dest.name(), .{}); + try cp_file(&from_child, &to_child); + }, + else => { + // Ignore unsupported types. }, - else => |k| fatal("cannot copy {s}", .{@tagName(k)}), } } } -pub const File_Iterator = struct { - iter: std.fs.Dir.Iterator, - - pub fn deinit(self: *File_Iterator) void { - self.iter.dir.close(); - } +/// Check if a path is relative and does not break out its root. +pub fn is_relative(path: []const u8) bool { + if (std.Io.Dir.path.isAbsolute(path)) return false; - pub fn next(self: *File_Iterator) ?[]const u8 { - while (true) { - const entry = self.iter.next() catch unreachable orelse return null; - if (entry.kind == .file) return entry.name; + var rest = path; + var depth: usize = 0; + while (rest.len > 0) { + var segment: []const u8 = undefined; + if (std.mem.findScalar(u8, rest, '/')) |sep| { + segment = rest[0..sep]; + rest = rest[sep + 1 ..]; + } else { + segment = rest; + rest = rest[rest.len..]; + } + if (segment.len == 0 or (segment.len == 1 and segment[0] == '.')) continue; + if (segment.len == 2 and segment[0] == '.' and segment[1] == '.') { + if (depth == 0) return false; + depth -= 1; + } else { + depth += 1; + } + if (segment.len != 1 or segment[0] != '.') { + continue; } } -}; - -/// Iterate files of a directory with the given path. The path must be absolute. -/// If the directory does not exist, null will be returned. -pub fn iterate_files(path: Path) !?File_Iterator { - var dir = std.fs.openDirAbsoluteZ(path.name(), .{ .iterate = true }) catch |err| { - return switch (err) { - error.FileNotFound => null, - error.BadPathName, error.InvalidUtf8 => error.BadPathName, - error.NameTooLong => unreachable, - else => err, - }; - }; - return .{ - .iter = dir.iterate(), - }; + return true; } pub const Dir_Info = struct { @@ -195,18 +243,15 @@ pub const Dir_Info = struct { empty: bool, }; -pub fn dir_info(path: Path) !Dir_Info { - var dir = std.fs.openDirAbsoluteZ(path.name(), .{ .iterate = true }) catch |err| { +pub fn dir_info(path: *const Path) !Dir_Info { + var iter = iterate(path) catch |err| { return switch (err) { error.FileNotFound => .{ .exists = false, .empty = false }, - error.BadPathName, error.InvalidUtf8 => error.BadPathName, - error.NameTooLong => unreachable, else => err, }; }; - defer dir.close(); + defer iter.deinit(); - var iter = dir.iterate(); const child = iter.next() catch unreachable; return .{ .exists = true, @@ -214,44 +259,212 @@ pub fn dir_info(path: Path) !Dir_Info { }; } -pub fn file_exists(path: Path) !bool { - std.fs.accessAbsoluteZ(path.name(), .{}) catch |err| { - switch (err) { - error.FileNotFound => return false, - error.PermissionDenied => return false, - error.NameTooLong => unreachable, - else => return err, - } - }; - return true; +pub fn path_exists(path: *const Path) bool { + return std.posix.system.access(path.name(), std.posix.R_OK) == 0; } /// Read the whole file into a buffer and returns the buffer. The caller is responsible /// to deallocate the memory with the given allocator. /// If the file does not exist, null will be returned. -pub fn read_file(allocator: std.mem.Allocator, path: Path) !?[]const u8 { - var f = std.fs.openFileAbsoluteZ(path.name(), .{}) catch |err| { - return switch (err) { - error.FileNotFound => null, - error.NameTooLong => unreachable, - else => err, - }; - }; - defer f.close(); +pub fn read_file(allocator: std.mem.Allocator, path: *const Path) ![]u8 { + const res = std.posix.system.open(path.name(), .{}); + try result_to_error(res); + + const fd: std.posix.fd_t = @intCast(res); + defer _ = std.posix.system.close(fd); - const stats = try f.stat(); - const buf = try allocator.alloc(u8, @intCast(stats.size)); + var stat: std.posix.Stat = undefined; + if (std.posix.system.fstat(fd, &stat) == -1) { + return error.Unexpected; + } + + const buf = try allocator.alloc(u8, @intCast(stat.size)); errdefer allocator.free(buf); - var r = f.reader(buf); - var off: usize = 0; - while (off < buf.len) { - const n = try r.readStreaming(buf[off..]); - off += n; + var bytes_read: usize = 0; + while (bytes_read < buf.len) { + const rbuf = buf[bytes_read..]; + const n = std.posix.system.read(fd, rbuf.ptr, rbuf.len); + result_to_error(n) catch |err| { + if (err == error.Interrupted) continue; + return err; + }; + bytes_read += @intCast(n); } return buf; } +pub const Iterator = struct { + pub const Entry = struct { + name: []const u8, + type: enum { dir, file, unsupported }, + }; + + fd: std.posix.fd_t, + buf: [4096]u8 = undefined, + len: usize = 0, + off: usize = 0, + + pub fn deinit(self: *Iterator) void { + _ = std.posix.system.close(self.fd); + } + + pub fn next(self: *Iterator) !?Entry { + while (true) { + if (self.off < self.len) { + const dirent = @as(*align(1) std.posix.system.dirent, @ptrCast(&self.buf[self.off])); + const name = @as([*]u8, @ptrCast(&dirent.name))[0..dirent.namlen]; + + self.off += @as(usize, @intCast(dirent.reclen)); + if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) { + continue; + } + + return .{ + .name = name, + .type = switch (dirent.type) { + std.posix.DT.DIR => .dir, + std.posix.DT.REG => .file, + else => .unsupported, + }, + }; + } + + var base: i64 = undefined; + const res = std.posix.system.getdirentries(self.fd, &self.buf, self.buf.len, &base); + try result_to_error(res); + if (res == 0) return null; + self.len = @intCast(res); + self.off = 0; + } + } +}; + +pub fn iterate(path: *const Path) !Iterator { + const res = std.posix.system.openat(std.posix.AT.FDCWD, path.name(), .{ .DIRECTORY = true }); + try result_to_error(res); + return .{ .fd = @intCast(res) }; +} + +pub const File_Writer = struct { + fd: std.posix.fd_t, + interface: std.Io.Writer, + + pub fn deinit(self: *File_Writer) void { + _ = std.posix.system.close(self.fd); + } + + fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { + const self: *File_Writer = @alignCast(@fieldParentPtr("interface", w)); + var iovec: [8]std.posix.iovec_const = undefined; + var iovlen: usize = 0; + if (w.end > 0) { + iovec[iovlen] = .{ .base = w.buffer.ptr, .len = w.end }; + iovlen += 1; + } + var off: usize = 0; + while (iovlen < iovec.len and off < data.len - 1) { + if (data[off].len > 0) { + iovec[iovlen] = .{ .base = data[off].ptr, .len = data[off].len }; + iovlen += 1; + } + off += 1; + } + if (data[data.len - 1].len > 0) { + off = 0; + const buf = data[data.len - 1]; + while (iovlen < iovec.len and off < splat) { + iovec[iovlen] = .{ .base = buf.ptr, .len = buf.len }; + iovlen += 1; + off += 1; + } + } + + if (iovlen == 0) return 0; + while (true) { + const res = std.posix.system.writev(self.fd, &iovec, @intCast(iovlen)); + if (res < 0) { + switch (std.posix.errno(res)) { + .INTR => continue, + else => return error.WriteFailed, + } + } + return w.consume(@intCast(res)); + } + } +}; + +pub fn file_writer(path: *const Path, buf: []u8, opts: struct { create: bool = false }) !File_Writer { + const res = std.posix.system.openat(std.posix.AT.FDCWD, path.name(), .{ .ACCMODE = .WRONLY, .CREAT = opts.create }, default_file_mode); + try result_to_error(res); + return .{ + .fd = @intCast(res), + .interface = .{ + .vtable = &.{ .drain = File_Writer.drain }, + .buffer = buf, + }, + }; +} + +pub const File_Reader = struct { + fd: std.posix.fd_t, + interface: std.Io.Reader, + + pub fn deinit(self: *File_Reader) void { + _ = std.posix.system.close(self.fd); + } + + fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { + const self: *File_Reader = @alignCast(@fieldParentPtr("interface", r)); + const buf = limit.slice(try w.writableSliceGreedy(1)); + const n: usize = while (true) { + const res = std.posix.system.read(self.fd, buf.ptr, buf.len); + if (res < 0) { + switch (std.posix.errno(res)) { + .SUCCESS => unreachable, + .INTR => continue, + else => return error.ReadFailed, + } + } + if (res == 0) return error.EndOfStream; + break @intCast(res); + }; + w.advance(n); + return n; + } +}; + +pub fn file_reader(path: *const Path, buf: []u8) !File_Reader { + const res = std.posix.system.openat(std.posix.AT.FDCWD, path.name(), .{ .ACCMODE = .RDONLY }); + try result_to_error(res); + return .{ + .fd = @intCast(res), + .interface = .{ + .vtable = &.{ .stream = File_Reader.stream }, + .buffer = buf, + .seek = 0, + .end = 0, + }, + }; +} + +fn result_to_error(res: anytype) !void { + return switch (std.posix.system.errno(res)) { + .SUCCESS => {}, + .NAMETOOLONG => unreachable, + .NOENT => error.FileNotFound, + .EXIST => error.FileAlreadyExists, + .ACCES => error.AccessDenied, + .LOOP => error.TooManySymlinks, + .MFILE, .NFILE => error.TooManyFiles, + .ISDIR => error.IsDir, + .NOTDIR => error.NotDir, + .INTR => error.Interrupted, + .XDEV => error.CrossDevice, + else => error.Unexpected, + }; +} + test "path join" { var path: Path = undefined; @@ -267,3 +480,14 @@ test "path join" { path = .join(.{ "/absolute", "local/dir" }); try std.testing.expectEqualStrings("/absolute/local/dir", path.name()); } + +test "is relative" { + try std.testing.expectEqual(true, is_relative(".")); + try std.testing.expectEqual(false, is_relative("..")); + try std.testing.expectEqual(true, is_relative("foo/..")); + try std.testing.expectEqual(true, is_relative("foo/../.")); + try std.testing.expectEqual(true, is_relative("foo/../.bar/./..")); + try std.testing.expectEqual(true, is_relative("./.foo/")); + try std.testing.expectEqual(false, is_relative("./foo/../..")); + try std.testing.expectEqual(false, is_relative("/.foo/")); +} diff --git a/src/git.zig b/src/git.zig index 82e19b0..fc0d81b 100644 --- a/src/git.zig +++ b/src/git.zig @@ -19,14 +19,14 @@ pub const Commit = struct { const s = std.mem.trim(u8, version_str, " \t"); if (s.len == 0) return .init_empty(); - const pos = std.mem.indexOfScalar(u8, s, '@') orelse return error.invalid_commit; + const pos = std.mem.findScalar(u8, s, '@') orelse return error.invalid_commit; return .{ .hash = s[0..pos], .timestamp = std.fmt.parseInt(u64, s[pos + 1 ..], 10) catch return error.invalid_commit, }; } - pub fn format(self: Commit, w: *std.io.Writer) std.io.Writer.Error!void { + pub fn format(self: Commit, w: *std.Io.Writer) std.Io.Writer.Error!void { if (self.hash.len > 0) { try w.print("{s}@{d}", .{ self.hash, self.timestamp }); } @@ -38,8 +38,8 @@ pub const Repo = struct { url: []const u8, branch: []const u8, - pub fn clone(self: Repo, env: Env) !void { - const dir = try fs.dir_info(self.path); + pub fn clone(self: Repo, env: *const Env) !void { + const dir = try fs.dir_info(&self.path); if (!dir.exists or dir.empty) { var branch_buf: [std.fs.max_name_bytes]u8 = undefined; const branch_param = branch_buf[0 .. 9 + self.branch.len]; @@ -50,58 +50,58 @@ pub const Repo = struct { .argv = &.{ "git", "clone", "--quiet", "--depth=1", branch_param, self.url, self.path.name() }, }); switch (res) { - .success => |out| env.dealloc(out), + .success => |out| if (out) |o| env.dealloc(o), .failure => |out| { env.err("failed to clone {s}\n", .{self.url}); - fatal("{s}", .{out}); + fatal("{s}", .{out orelse ""}); }, } } } - pub fn reset(self: Repo, env: Env) void { + pub fn reset(self: Repo, env: *const Env) void { var res = shell.exec(.{ .env = env, .argv = &.{ "git", "reset", "--hard" }, - .cwd = self.path, + .cwd = &self.path, }); switch (res) { - .success => |out| env.dealloc(out), + .success => |out| if (out) |o| env.dealloc(o), .failure => |out| { env.err("failed to reset repository: {s}\n", .{self.path.name()}); - fatal("{s}", .{out}); + fatal("{s}", .{out orelse ""}); }, } res = shell.exec(.{ .env = env, .argv = &.{ "git", "clean", "-fdx" }, - .cwd = self.path, + .cwd = &self.path, }); switch (res) { - .success => |out| env.dealloc(out), + .success => |out| if (out) |o| env.dealloc(o), .failure => |out| { env.err("failed to clean repository: {s}\n", .{self.path.name()}); - fatal("{s}", .{out}); + fatal("{s}", .{out orelse ""}); }, } } - pub fn update(self: Repo, env: Env) !Commit { - const dir = try fs.dir_info(self.path); + pub fn update(self: Repo, env: *const Env) !Commit { + const dir = try fs.dir_info(&self.path); if (!dir.exists or dir.empty) { try self.clone(env); } else { var res = shell.exec(.{ .env = env, .argv = &.{ "git", "fetch", "--quiet", "--depth=1", "origin", self.branch }, - .cwd = self.path, + .cwd = &self.path, }); switch (res) { - .success => |out| env.dealloc(out), + .success => |out| if (out) |o| env.dealloc(o), .failure => |out| { env.err("failed to fetch branch {s}: {s}\n", .{ self.branch, self.url }); - fatal("{s}", .{out}); + fatal("{s}", .{out orelse ""}); }, } @@ -109,20 +109,20 @@ pub const Repo = struct { res = shell.exec(.{ .env = env, .argv = &.{ "git", "reset", "--hard", branch_spec.name() }, - .cwd = self.path, + .cwd = &self.path, }); switch (res) { - .success => |out| env.dealloc(out), + .success => |out| if (out) |o| env.dealloc(o), .failure => |out| { env.err("failed to reset branch {s}: {s}\n", .{ self.branch, self.path.name() }); - fatal("{s}", .{out}); + fatal("{s}", .{out orelse ""}); }, } } // We leak `out` intentionally here. - const out = self.must_run(env, &.{ "git", "show", "--no-patch", "--format=format:%H %at", self.branch }); - const space = std.mem.indexOfScalar(u8, out, ' ') orelse { + const out = self.must_run(env, &.{ "git", "show", "--no-patch", "--format=format:%H %at", self.branch }).?; + const space = std.mem.findScalar(u8, out, ' ') orelse { fatal("unexpected output for 'git show'", .{}); }; return .{ @@ -141,17 +141,17 @@ pub const Repo = struct { }; } - fn must_run(self: Repo, env: Env, argv: []const []const u8) []const u8 { + fn must_run(self: Repo, env: *const Env, argv: []const []const u8) ?[]const u8 { const res = shell.exec(.{ .env = env, .argv = argv, - .cwd = self.path, + .cwd = &self.path, }); switch (res) { .success => |out| return out, .failure => |out| { - var errmsg = out; - if (std.mem.indexOfScalar(u8, errmsg, '\n')) |eol| { + var errmsg = out orelse ""; + if (std.mem.findScalar(u8, errmsg, '\n')) |eol| { errmsg = errmsg[0..eol]; if (errmsg.len > 0 and errmsg[errmsg.len - 1] == '\r') { errmsg = errmsg[0 .. errmsg.len - 1]; diff --git a/src/main.zig b/src/main.zig index bafcbd8..55e7604 100644 --- a/src/main.zig +++ b/src/main.zig @@ -14,14 +14,12 @@ const Time = @import("time.zig").Time; const default_tree_name = "default"; -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); +pub fn main(init: std.process.Init.Minimal) !void { + var arena_allocator: std.heap.ArenaAllocator = .init(std.heap.page_allocator); + const allocator = arena_allocator.allocator(); - var main_cmd: Main_Command = undefined; - var args: cli.Args = try .init(allocator, "porteur", &main_cmd); - defer args.deinit(allocator); - try main_cmd.execute(allocator, &args); + var main_cmd = cli.init(Main_Command, init.args); + main_cmd.execute(allocator, init.environ); } const Main_Command = struct { @@ -31,6 +29,7 @@ const Main_Command = struct { "porteur [--etc=⟨path⟩] tree ⟨command⟩ [options] [⟨args⟩]", "porteur [--etc=⟨path⟩] tmpl ⟨command⟩ [options] [⟨args⟩]", "porteur [--etc=⟨path⟩] port ⟨command⟩ [options] [⟨args⟩]", + "porteur version", "porteur help [⟨command⟩ [⟨subcommand⟩]]", }, .short_description = "Build your own ports.", @@ -69,11 +68,11 @@ const Main_Command = struct { \\`{{v}}`. It is also possible to use variables in filenames. For more \\information how to define variables, see the VARIABLES section. \\ - \\Normally, the processed template files are used to create the port - \\files, the distribution file is created, and a distinfo is generated. - \\To have more control over this process, a template can define a file - \\named `.config` in its root directory. For a documentation of all - \\configurable options, see the sample configuration file (see FILES). + \\Normally, the port files are generated from the template files, the + \\distribution file is created, and a distinfo is generated. To have + \\more control over this process, a template can define a `.config` + \\file in its root directory for fine-tuning. For a documentation of + \\all configurable options, see the sample configuration file (see FILES). , }, .{ @@ -107,30 +106,30 @@ const Main_Command = struct { \\ \\List of predefined variables: \\ - \\ .portname - Name of the port - \\ .portversion - Current version of the port - \\ .category - Category of the port - \\ .distdir - Directory containing all distfiles - \\ .distname - Name of the archive file containing the distribution + \\ .portname - Name of the port + \\ .portversion - Current version of the port + \\ .category - Category of the port + \\ .distdir - Directory containing all distfiles + \\ .distname - Name of the unnamed distribution file + \\ .distname.⟨name⟩ - Name of a named distribution file , }, .{ .heading = "DISTFILES", .body = - \\To compile a port, a distribution is necessary that contains the source - \\code. Porteur creates a gzipped archive of the Git repository and puts - \\it in the configured `distfiles-dir`. The filename of this archive is - \\provided in the `.dist.name` variable and the directory where its put in - \\is provided in the `.dist.dir` variable (see VARIABLES). So the template - \\can reference the archive's location and tell FreeBSD where to find the - \\source code. + \\To compile a port, a distribution is necessary that contains everything + \\the port needs to be built. By default porteur creates a gzipped archive + \\of the whole Git repository and puts it in the configured `distfiles-dir` + \\(see `porteur.conf.sample`). The filename of this archive is provided + \\in the `.distname` variable and its directory in `.distdir` (see + \\VARIABLES). \\ - \\Sometimes it is necessary to do extra steps before the distribution is - \\created (e.g. vendoring). These steps can be defined as a target in the - \\port Makefile of the template. When porteur executes this target it will - \\set ${WRKSRC} to the port's source tree. The target name can be defined - \\with `dist-prepare-target` in the template's configuration file (see - \\TEMPLATES). + \\This behaviour, however, can be configured. With the `dist.prepare-target` + \\and the `dist.source-path` options in the template config it is possible + \\to prepare a source tree executing a Makefile target and defining the + \\contents of the archive respectively. It is furthermore possible to + \\define multiple archive files for the distribution. For more information + \\see the `template.conf.sample` file. , }, .{ @@ -188,24 +187,26 @@ const Main_Command = struct { pub const Subcommands = union(enum) { help: Help_Command, + version: Version_Command, tmpl: Tmpl_Command, tree: Tree_Command, port: Port_Command, }; - pub fn execute(self: *Main_Command, allocator: std.mem.Allocator, args: *cli.Args) !void { - const subcmd = args.next_subcommand(Main_Command) orelse { + pub fn execute(self: *Main_Command, allocator: std.mem.Allocator, penv: std.process.Environ) void { + const subcmd = cli.next_subcommand(Main_Command) orelse { cli.print_usage(cli.stdout, Main_Command); return; }; const etc_path = self.options.etc.value_or_default(); - const env: Env = .init(allocator, etc_path, args); + const env: Env = .init(allocator, penv, etc_path); switch (subcmd) { - .help => |cmd| cmd.execute(env), - .tmpl => |cmd| cmd.execute(env), - .tree => |cmd| cmd.execute(env), - .port => |cmd| cmd.execute(env), + .help => |cmd| cmd.execute(), + .version => |cmd| cmd.execute(), + .tmpl => |cmd| cmd.execute(&env), + .tree => |cmd| cmd.execute(&env), + .port => |cmd| cmd.execute(&env), } } }; @@ -231,26 +232,26 @@ const Help_Command = struct { , }; - pub const Subcommands = Help_Subcommands(Main_Command.Subcommands); + pub const Subcommands = Main_Command.Subcommands; - fn execute(self: Help_Command, env: Env) void { + fn execute(self: Help_Command) void { _ = self; - const subcmd = env.args.next_subcommand(Help_Command) orelse { + const subcmd = cli.next_subcommand(Help_Command) orelse { cli.print_usage(cli.stdout, Main_Command); return; }; switch (subcmd) { - inline else => |cmd| execute_subcmd(env.args, @TypeOf(cmd)), + inline else => |cmd| execute_subcmd(@TypeOf(cmd)), } } - fn execute_subcmd(args: *cli.Args, comptime Command: type) void { + fn execute_subcmd(comptime Command: type) void { if (@hasDecl(Command, "Subcommands")) { - if (args.next_subcommand(Command)) |subcmd| { + if (cli.next_subcommand(Command)) |subcmd| { switch (subcmd) { inline else => |cmd| { - execute_subcmd(args, @TypeOf(cmd)); + execute_subcmd(@TypeOf(cmd)); return; }, } @@ -258,27 +259,24 @@ const Help_Command = struct { } cli.print_help(Command); } +}; - fn Help_Subcommands(comptime Subcmds: type) type { - assert(@typeInfo(Subcmds) == .@"union"); - const union_info = @typeInfo(Subcmds).@"union"; +const Version_Command = struct { + pub const help = cli.Command_Help{ + .name = "porteur version", + .synopsis = &.{ + "porteur version", + }, + .short_description = "Display the version of porteur.", + .long_description = + \\Print the current version of porteur to stdout. + , + }; - var fields: [union_info.fields.len]std.builtin.Type.UnionField = undefined; - for (union_info.fields, 0..) |field, i| { - fields[i] = .{ - .name = field.name, - .alignment = field.alignment, - .type = struct { - pub const help = field.type.help; - }, - }; - } - return @Type(.{ .@"union" = .{ - .layout = union_info.layout, - .tag_type = union_info.tag_type, - .fields = &fields, - .decls = &.{}, - } }); + fn execute(self: Version_Command) void { + _ = self; + cli.stdout.write_line("{s}", .{options.version}); + cli.stdout.flush(); } }; @@ -310,15 +308,15 @@ const Tmpl_Command = struct { conf: Tmpl_Conf, }; - pub fn execute(self: Tmpl_Command, env: Env) void { + pub fn execute(self: Tmpl_Command, env: *const Env) void { _ = self; - const subcmd = env.args.next_subcommand(Tmpl_Command) orelse { + const subcmd = cli.next_subcommand(Tmpl_Command) orelse { cli.fatal_with_usage(Tmpl_Command, "missing subcommand", .{}); }; switch (subcmd) { - .list => |cmd| cmd.execute(env) catch |err| fatal_err(err, "failed to list templates"), - .conf => |cmd| cmd.execute(env) catch |err| fatal_err(err, "failed to update template configuration"), + .list => |cmd| cmd.execute(env) catch |e| fatal_err(e, "failed to list templates"), + .conf => |cmd| cmd.execute(env) catch |e| fatal_err(e, "failed to update template configuration"), } } @@ -348,7 +346,7 @@ const Tmpl_Command = struct { }), }, - fn execute(self: Tmpl_Ls, env: Env) !void { + fn execute(self: Tmpl_Ls, env: *const Env) !void { const long = self.options.long.value_or_default(); const print_opts: cli.Print_Options = .{ .hanging_indent = 4 }; var iter = try tmpl.iterate(env); @@ -411,8 +409,8 @@ const Tmpl_Command = struct { }), }, - fn execute(self: Tmpl_Conf, env: Env) !void { - const template_name = env.args.next_positional() orelse { + fn execute(self: Tmpl_Conf, env: *const Env) !void { + const template_name = cli.next_positional() orelse { cli.fatal_with_usage(@This(), "missing template name", .{}); }; @@ -425,7 +423,8 @@ const Tmpl_Command = struct { if (edit) |_| { try tmpl.edit_config(env, template_name); } else if (import) |filename| { - try tmpl.import_config(env, template_name, .init(filename)); + const path: fs.Path = .init(filename); + try tmpl.import_config(env, template_name, &path); } else { const template = try tmpl.must_get(env, template_name); const config = try tmpl.read_config(env, template); @@ -459,15 +458,15 @@ const Tree_Command = struct { rm: Tree_Rm, }; - pub fn execute(self: Tree_Command, env: Env) void { + pub fn execute(self: Tree_Command, env: *const Env) void { _ = self; - const subcmd = env.args.next_subcommand(Tree_Command) orelse { + const subcmd = cli.next_subcommand(Tree_Command) orelse { cli.fatal_with_usage(Tree_Command, "missing subcommand", .{}); }; switch (subcmd) { - .list => |cmd| cmd.execute(env) catch |err| fatal_err(err, "failed to iterate ports trees"), - .rm => |cmd| cmd.execute(env) catch |err| fatal_err(err, "failed to remove ports trees"), + .list => |cmd| cmd.execute(env) catch |e| fatal_err(e, "failed to iterate ports trees"), + .rm => |cmd| cmd.execute(env) catch |e| fatal_err(e, "failed to remove ports trees"), } } @@ -488,7 +487,7 @@ const Tree_Command = struct { , }; - fn execute(self: Tree_Ls, env: Env) !void { + fn execute(self: Tree_Ls, env: *const Env) !void { _ = self; var iter = try tree.iterate(env); defer iter.deinit(); @@ -524,9 +523,9 @@ const Tree_Command = struct { }), }, - fn execute(self: Tree_Rm, env: Env) !void { + fn execute(self: Tree_Rm, env: *const Env) !void { const force = self.options.force.value_or_default(); - const name = env.args.next_positional() orelse { + const name = cli.next_positional() orelse { if (force) std.process.exit(0); cli.fatal_with_usage(@This(), "missing tree name", .{}); }; @@ -582,6 +581,7 @@ const Port_Command = struct { pub const Subcommands = union(enum) { list: Port_Ls, info: Port_Info, + version: Port_Version, add: Port_Add, vars: Port_Vars, update: Port_Update, @@ -591,13 +591,13 @@ const Port_Command = struct { rm: Port_Rm, }; - pub fn execute(self: Port_Command, env: Env) void { - const subcmd = env.args.next_subcommand(Port_Command) orelse { + pub fn execute(self: Port_Command, env: *const Env) void { + const subcmd = cli.next_subcommand(Port_Command) orelse { cli.fatal_with_usage(Port_Command, "missing subcommand", .{}); }; const tree_name = self.options.tree.value_or_default(); - const ports_tree = tree.get(env, tree_name) catch |err| fatal_err(err, "failed to get tree"); + const ports_tree = tree.get(env, tree_name) catch |e| fatal_err(e, "failed to get tree"); if (ports_tree == null) cli.fatal("ports tree not found: {s}", .{tree_name}); const ctx = port.Ctx{ @@ -606,15 +606,16 @@ const Port_Command = struct { }; switch (subcmd) { - .list => |cmd| cmd.execute(ctx) catch |err| fatal_err(err, "failed to list ports"), - .info => |cmd| cmd.execute(ctx) catch |err| fatal_err(err, "failed to fetch port"), - .add => |cmd| cmd.execute(ctx) catch |err| fatal_err(err, "failed to write port"), - .vars => |cmd| cmd.execute(ctx) catch |err| fatal_err(err, "failed to update port variables"), - .update => |cmd| cmd.execute(ctx) catch |err| fatal_err(err, "failed to update port"), - .bump_version => |cmd| cmd.execute(ctx) catch |err| fatal_err(err, "failed to bump the port version"), - .history => |cmd| cmd.execute(ctx) catch |err| fatal_err(err, "failed to list distfile history"), - .rollback => |cmd| cmd.execute(ctx) catch |err| fatal_err(err, "failed to roll back the port version"), - .rm => |cmd| cmd.execute(ctx) catch |err| fatal_err(err, "failed to delete port"), + .list => |cmd| cmd.execute(ctx) catch |e| fatal_err(e, "failed to list ports"), + .info => |cmd| cmd.execute(ctx) catch |e| fatal_err(e, "failed to fetch port"), + .version => |cmd| cmd.execute(ctx) catch |e| fatal_err(e, "failed to fetch port version"), + .add => |cmd| cmd.execute(ctx) catch |e| fatal_err(e, "failed to write port"), + .vars => |cmd| cmd.execute(ctx) catch |e| fatal_err(e, "failed to update port variables"), + .update => |cmd| cmd.execute(ctx) catch |e| fatal_err(e, "failed to update port"), + .bump_version => |cmd| cmd.execute(ctx) catch |e| fatal_err(e, "failed to bump the port version"), + .history => |cmd| cmd.execute(ctx) catch |e| fatal_err(e, "failed to list distfile history"), + .rollback => |cmd| cmd.execute(ctx) catch |e| fatal_err(e, "failed to roll back the port version"), + .rm => |cmd| cmd.execute(ctx) catch |e| fatal_err(e, "failed to delete port"), } } @@ -680,7 +681,7 @@ const Port_Command = struct { }; fn execute(self: Port_Info, ctx: port.Ctx) !void { _ = self; - const port_name = ctx.env.args.next_positional() orelse { + const port_name = cli.next_positional() orelse { cli.fatal_with_usage(@This(), "missing port name", .{}); }; const p = try port.must_get(ctx, port_name); @@ -707,6 +708,32 @@ const Port_Command = struct { } }; + const Port_Version = struct { + pub const help = cli.Command_Help{ + .name = "porteur port version", + .synopsis = &.{ + "porteur port [options] version ⟨port⟩", + }, + .short_description = "Show the current version of a port.", + .long_description = + \\This command prints the current version of the specified port of a + \\ports tree. If the port cannot be found in the ports tree an error + \\will be reported. + , + }; + fn execute(self: Port_Version, ctx: port.Ctx) !void { + _ = self; + const port_name = cli.next_positional() orelse { + cli.fatal_with_usage(@This(), "missing port name", .{}); + }; + const p = try port.must_get(ctx, port_name); + defer p.deinit(); + const version = try port.read_version(ctx, p); + cli.stdout.printf_line("{f}", .{version}, .{}); + cli.stdout.flush(); + } + }; + const Port_Add = struct { pub const help = cli.Command_Help{ .name = "porteur port add", @@ -730,11 +757,22 @@ const Port_Command = struct { fn execute(self: Port_Add, ctx: port.Ctx) !void { _ = self; - if (!try tmpl.exists_any(ctx.env)) { - const tmpl_dir = tmpl.etc_dir(ctx.env); - cli.stderr.print_line("No templates found.", .{}); - cli.fatal("Please define a template in `{s}`.", .{tmpl_dir.name()}); - } + + var templatebuf: [std.posix.NAME_MAX]u8 = undefined; + const the_only_template = blk: { + var iter = try tmpl.iterate(ctx.env); + defer iter.deinit(); + const first_tmpl = try iter.next() orelse { + const tmpl_dir = tmpl.etc_dir(ctx.env); + cli.stderr.print_line("No templates found.", .{}); + cli.fatal("Please define a template in `{s}`.", .{tmpl_dir.name()}); + }; + const first_tmpl_name = templatebuf[0..first_tmpl.name.len]; + @memcpy(first_tmpl_name, first_tmpl.name); + + _ = try iter.next() orelse break :blk first_tmpl_name; + break :blk null; + }; const namebuf = try ctx.env.allocator.alloc(u8, 64); defer ctx.env.allocator.free(namebuf); @@ -744,7 +782,7 @@ const Port_Command = struct { cli.stderr.flush(); continue; }; - if (std.mem.indexOfAny(u8, input, "/\\\t ")) |_| { + if (std.mem.findAny(u8, input, "/\\\t ")) |_| { cli.stderr.print_line("ERROR: The port name must not contain slash, backslash, or space.", .{}); cli.stderr.flush(); continue; @@ -765,7 +803,7 @@ const Port_Command = struct { cli.stderr.flush(); continue; }; - if (std.mem.indexOfAny(u8, input, "/\\\t ")) |_| { + if (std.mem.findAny(u8, input, "/\\\t ")) |_| { cli.stderr.print_line("ERROR: The port category must not contain slash, backslash, or space.", .{}); cli.stderr.flush(); continue; @@ -784,7 +822,7 @@ const Port_Command = struct { break ver; }; - var repourlbuf: [std.fs.max_path_bytes]u8 = undefined; + var repourlbuf: [fs.max_path_bytes]u8 = undefined; const repo_url = while (true) { const input = cli.ask("Git Repository: ", .{}, &repourlbuf) orelse { cli.stderr.print_line("ERROR: The Git repository is required.", .{}); @@ -806,8 +844,7 @@ const Port_Command = struct { break input; }; - var templatebuf: [64]u8 = undefined; - const template = while (true) { + const template = the_only_template orelse while (true) { const input = cli.ask("Template: ", .{}, &templatebuf) orelse { cli.stderr.print_line("ERROR: The template is required.", .{}); cli.stderr.flush(); @@ -894,7 +931,7 @@ const Port_Command = struct { }, fn execute(self: Port_Vars, ctx: port.Ctx) !void { - const port_name = ctx.env.args.next_positional() orelse { + const port_name = cli.next_positional() orelse { cli.fatal_with_usage(@This(), "missing port name", .{}); }; @@ -907,7 +944,8 @@ const Port_Command = struct { if (edit) |_| { try port.edit_vars(ctx, port_name); } else if (import) |filename| { - try port.import_vars(ctx, port_name, .init(filename)); + const path: fs.Path = .init(filename); + try port.import_vars(ctx, port_name, &path); } else { const p = try port.must_get(ctx, port_name); defer p.deinit(); @@ -959,7 +997,7 @@ const Port_Command = struct { .force = self.options.force.value_or_default(), }; var count: usize = 0; - while (ctx.env.args.next_positional()) |port_name| { + while (cli.next_positional()) |port_name| { count += 1; try update_single_port(ctx, port_name, opts); } @@ -1035,7 +1073,7 @@ const Port_Command = struct { else => cli.fatal("can either bump major, minor, or patch version", .{}), } - const port_name = ctx.env.args.next_positional() orelse { + const port_name = cli.next_positional() orelse { cli.fatal_with_usage(@This(), "missing port name", .{}); }; @@ -1075,7 +1113,7 @@ const Port_Command = struct { fn execute(self: Port_History, ctx: port.Ctx) !void { const long = self.options.long.value_or_default(); - const port_name = ctx.env.args.next_positional() orelse { + const port_name = cli.next_positional() orelse { cli.fatal_with_usage(@This(), "missing port name", .{}); }; var distfiles = try port.list_distfiles(ctx, port_name); @@ -1120,10 +1158,10 @@ const Port_Command = struct { fn execute(self: Port_Rollback, ctx: port.Ctx) !void { _ = self; - const port_name = ctx.env.args.next_positional() orelse { + const port_name = cli.next_positional() orelse { cli.fatal_with_usage(@This(), "missing port name", .{}); }; - const version_str = ctx.env.args.next_positional() orelse { + const version_str = cli.next_positional() orelse { cli.fatal_with_usage(@This(), "missing version", .{}); }; const version = port.Version.parse(version_str) catch { @@ -1164,7 +1202,7 @@ const Port_Command = struct { var count: usize = 0; var exit_code: u8 = 0; - while (ctx.env.args.next_positional()) |port_name| { + while (cli.next_positional()) |port_name| { count += 1; if (!force) { const confirmed = cli.ask_confirmation("Remove port {s}?", .{port_name}, .no); diff --git a/src/port.zig b/src/port.zig index 81aa034..a7d53e4 100644 --- a/src/port.zig +++ b/src/port.zig @@ -1,5 +1,5 @@ //! A port's configuration is stored file-based with the following -//! directory structure: +//! files: //! //! * info - category, template //! * src - repo, branch, commit hash, commit timestamp @@ -24,7 +24,7 @@ const fatal = cli.fatal; const distname_ext = ".tar.gz"; pub const Ctx = struct { - env: Env, + env: *const Env, tree: tree.Tree, }; @@ -57,9 +57,9 @@ pub const Info = struct { fn from_file(ctx: Ctx, port_name: []const u8) !?Info { const port_root = ctx.tree.port_path(port_name); - const info_file: fs.Path = .join(.{ port_root.name(), "info" }); + const info_path: fs.Path = .join(.{ port_root.name(), "info" }); - const info_res = conf.Config.from_file(ctx.env.allocator, info_file.name()) catch { + const info_res = conf.Config.from_file(ctx.env.allocator, &info_path) catch { port_corrupted(port_name, "invalid info file"); }; const info_conf = switch (info_res) { @@ -88,13 +88,11 @@ pub const Info = struct { }; } - fn to_file(self: Info) !void { - const path: fs.Path = .join(.{ self.root.name(), "info" }); - const file = try std.fs.createFileAbsoluteZ(path.name(), .{ .mode = 0o644 }); - defer file.close(); - + fn to_file(self: Info, path: *const fs.Path) !void { var write_buf: [256]u8 = undefined; - var writer = file.writer(&write_buf); + var writer = try fs.file_writer(path, &write_buf, .{ .create = true }); + defer writer.deinit(); + inline for (&[_]conf.Var{ .{ .key = "category", .val = self.category }, .{ .key = "template", .val = self.template }, @@ -105,6 +103,10 @@ pub const Info = struct { try writer.interface.flush(); } + fn info_file(self: Info) fs.Path { + return .join(.{ self.root.name(), "info" }); + } + fn version_file(self: Info) fs.Path { return .join(.{ self.root.name(), "version" }); } @@ -139,10 +141,10 @@ pub const Version = packed struct { var major: []const u8 = text; var minor: []const u8 = ""; var patch: []const u8 = ""; - if (std.mem.indexOfScalar(u8, major, '.')) |dot1| { + if (std.mem.findScalar(u8, major, '.')) |dot1| { minor = major[dot1 + 1 ..]; major = major[0..dot1]; - if (std.mem.indexOfScalar(u8, minor, '.')) |dot2| { + if (std.mem.findScalar(u8, minor, '.')) |dot2| { patch = minor[dot2 + 1 ..]; minor = minor[0..dot2]; } @@ -155,63 +157,46 @@ pub const Version = packed struct { }; } - /// Parse the version as it is written to a file: 0.0.0[.d00000000[.r0]] + /// Parse the version as it is written to a file: 0.0.0[.00000000r0] pub fn parse(text: []const u8) !Version { - const dot1 = std.mem.indexOfScalar(u8, text, '.') orelse return error.invalid_version; - const dot2 = std.mem.indexOfScalarPos(u8, text, dot1 + 1, '.') orelse return error.invalid_version; - const dot3 = std.mem.indexOfScalarPos(u8, text, dot2 + 1, '.') orelse text.len; + const minor_pos = std.mem.findScalar(u8, text, '.') orelse return error.invalid_version; + const patch_pos = std.mem.findScalarPos(u8, text, minor_pos + 1, '.') orelse return error.invalid_version; + const date_pos = std.mem.findScalarPos(u8, text, patch_pos + 1, '.') orelse text.len; - const major = text[0..dot1]; - const minor = text[dot1 + 1 .. dot2]; - const patch = text[dot2 + 1 .. dot3]; - var date: ?[]const u8 = null; - var revision: ?[]const u8 = null; - if (dot3 < text.len) { - if (dot3 + 1 == text.len or text[dot3 + 1] != 'd') return error.invalid_version; - date = text[dot3 + 2 ..]; - if (std.mem.indexOfScalarPos(u8, text, dot3 + 2, '.')) |dot4| { - if (dot4 + 1 == text.len or text[dot4 + 1] != 'r') return error.invalid_version; - date = text[dot3 + 2 .. dot4]; - revision = text[dot4 + 2 ..]; - } + const major = parse_int(u16, text[0..minor_pos]) orelse return error.invalid_version; + const minor = parse_int(u16, text[minor_pos + 1 .. patch_pos]) orelse return error.invalid_version; + const patch = parse_int(u32, text[patch_pos + 1 .. date_pos]) orelse return error.invalid_version; + var date: u32 = 0; + var revision: u32 = 0; + if (date_pos < text.len) { + const rev_pos = std.mem.findScalarPos(u8, text, date_pos + 1, 'r') orelse return error.invalid_version; + date = parse_int(u32, text[date_pos + 1 .. rev_pos]) orelse return error.invalid_version; + revision = parse_int(u32, text[rev_pos + 1 ..]) orelse return error.invalid_version; } return .{ - .major = parse_int(u16, major) orelse return error.invalid_version, - .minor = parse_int(u16, minor) orelse return error.invalid_version, - .patch = parse_int(u32, patch) orelse return error.invalid_version, - .date = if (date) |d| parse_int(u32, d) orelse return error.invalid_version else 0, - .revision = if (revision) |r| parse_int(u32, r) orelse return error.invalid_version else 0, + .major = major, + .minor = minor, + .patch = patch, + .date = date, + .revision = revision, }; } fn from_file(ctx: Ctx, info: Info) !Version { - const text = try fs.read_file(ctx.env.allocator, info.version_file()) orelse return error.missing_version; + const version_file = info.version_file(); + const text = fs.read_file(ctx.env.allocator, &version_file) catch |err| { + return if (err == error.FileNotFound) error.missing_version else err; + }; defer ctx.env.dealloc(text); - - const s = std.mem.trim(u8, text, " \t\n"); - - var major: []const u8 = s; - var minor: []const u8 = ""; - var patch: []const u8 = ""; - if (std.mem.indexOfScalar(u8, major, '.')) |dot1| { - minor = major[dot1 + 1 ..]; - major = major[0..dot1]; - if (std.mem.indexOfScalar(u8, minor, '.')) |dot2| { - patch = minor[dot2 + 1 ..]; - minor = minor[0..dot2]; - } - } - return try parse(std.mem.trim(u8, text, " \t\n")); } - fn to_file(self: Version, path: fs.Path) !void { - const file = try std.fs.createFileAbsoluteZ(path.name(), .{ .mode = 0o644 }); - defer file.close(); - + fn to_file(self: Version, path: *const fs.Path) !void { var write_buf: [64]u8 = undefined; - var writer = file.writer(&write_buf); + var writer = try fs.file_writer(path, &write_buf, .{ .create = true }); + defer writer.deinit(); + try self.format(&writer.interface); try writer.interface.writeByte('\n'); try writer.interface.flush(); @@ -224,16 +209,17 @@ pub const Version = packed struct { return str; } - pub fn format(self: Version, w: *std.io.Writer) std.io.Writer.Error!void { + pub fn format(self: Version, w: *std.Io.Writer) std.Io.Writer.Error!void { try w.print("{}.{}.{}", .{ self.major, self.minor, self.patch }); if (self.date != 0) { - try w.print(".d{}", .{self.date}); - if (self.revision != 0) { - try w.print(".r{}", .{self.revision}); - } + try w.print(".{}r{}", .{ self.date, self.revision }); } } + fn eq(left: Version, right: Version) bool { + return @as(u128, @bitCast(left)) == @as(u128, @bitCast(right)); + } + fn less(left: Version, right: Version) bool { return @as(u128, @bitCast(left)) < @as(u128, @bitCast(right)); } @@ -255,7 +241,7 @@ pub const Source = struct { fn from_file(ctx: Ctx, info: Info) !Source { const src_file = info.src_file(); - const raw_res = try conf.Config.from_file(ctx.env.allocator, src_file.name()); + const raw_res = try conf.Config.from_file(ctx.env.allocator, &src_file); const raw = switch (raw_res) { .conf => |c| c, .err => port_corrupted(info.name, "invalid source file"), @@ -289,16 +275,15 @@ pub const Source = struct { }; } - fn to_file(self: Source, path: fs.Path) !void { + fn to_file(self: Source, path: *const fs.Path) !void { var commit_buf: [64]u8 = undefined; - var commit_str: std.io.Writer = .fixed(&commit_buf); + var commit_str: std.Io.Writer = .fixed(&commit_buf); try commit_str.print("{f}", .{self.commit}); - const file = try std.fs.createFileAbsoluteZ(path.name(), .{ .mode = 0o644 }); - defer file.close(); - var write_buf: [256]u8 = undefined; - var writer = file.writer(&write_buf); + var writer = try fs.file_writer(path, &write_buf, .{ .create = true }); + defer writer.deinit(); + inline for (&[_]conf.Var{ .{ .key = "repo", .val = self.repo.url }, .{ .key = "branch", .val = self.repo.branch }, @@ -317,12 +302,11 @@ pub const Source = struct { pub const Iterator = struct { ctx: Ctx, - dir: ?std.fs.Dir = null, - iter: ?std.fs.Dir.Iterator = null, + iter: ?fs.Iterator = null, current: ?Info = null, pub fn deinit(self: *Iterator) void { - if (self.dir) |*dir| dir.close(); + if (self.iter) |*it| it.deinit(); if (self.current) |p| p.deinit(); } @@ -333,7 +317,7 @@ pub const Iterator = struct { } if (self.iter) |*iter| { while (try iter.next()) |entry| { - if (entry.kind == .directory) { + if (entry.type == .dir) { self.current = try .from_file(self.ctx, entry.name); return self.current; } @@ -344,17 +328,14 @@ pub const Iterator = struct { }; pub fn iterate(ctx: Ctx) !Iterator { - const dir = std.fs.openDirAbsoluteZ(ctx.tree.root.name(), .{ .iterate = true }) catch |err| { - if (err == error.FileNotFound) { - return .{ .ctx = ctx }; - } + const iter = fs.iterate(&ctx.tree.root) catch |err| { + if (err == error.FileNotFound) return .{ .ctx = ctx }; return err; }; return .{ .ctx = ctx, - .dir = dir, - .iter = dir.iterate(), + .iter = iter, }; } @@ -384,7 +365,7 @@ pub fn read_source(ctx: Ctx, info: Info) !Source { pub fn read_vars(ctx: Ctx, info: Info) !conf.Config { const vars_file = info.vars_file(); - const conf_res = try conf.Config.from_file(ctx.env.allocator, vars_file.name()); + const conf_res = try conf.Config.from_file(ctx.env.allocator, &vars_file); return switch (conf_res) { .conf => |c| c, .err => |e| { @@ -407,8 +388,9 @@ pub fn create(ctx: Ctx, info: Info, opts: Create_Options) !void { .branch = opts.repo_branch, }; + ctx.env.info("===> cloning repository: {s}", .{repo.path.name()}); try repo.clone(ctx.env); - errdefer fs.rm(repo.path) catch {}; + errdefer fs.rm(&repo.path) catch {}; const source: Source = .{ .raw = undefined, @@ -417,23 +399,30 @@ pub fn create(ctx: Ctx, info: Info, opts: Create_Options) !void { }; const port_root = ctx.tree.port_path(info.name); - try fs.mkdir_all(port_root); - try info.to_file(); - try opts.initial_version.to_file(info.version_file()); - try source.to_file(info.src_file()); + const info_file = info.info_file(); + const version_file = info.version_file(); + const src_file = info.src_file(); + + try fs.mkdir_all(&port_root); + ctx.env.info("===> writing port data: {s}", .{info.root.name()}); + try info.to_file(&info_file); + try opts.initial_version.to_file(&version_file); + try source.to_file(&src_file); } pub fn edit_vars(ctx: Ctx, port_name: []const u8) !void { const info = try must_get(ctx, port_name); defer info.deinit(); - try shell.edit_config_file_noverify(ctx.env, info.vars_file()); + + const vars_file = info.vars_file(); + try shell.edit_config_file_noverify(ctx.env, &vars_file); } -pub fn import_vars(ctx: Ctx, port_name: []const u8, new_vars_file: fs.Path) !void { +pub fn import_vars(ctx: Ctx, port_name: []const u8, new_vars_file: *const fs.Path) !void { const info = try must_get(ctx, port_name); defer info.deinit(); - const parsed = try conf.Config.from_file(ctx.env.allocator, new_vars_file.name()); + const parsed = try conf.Config.from_file(ctx.env.allocator, new_vars_file); switch (parsed) { .conf => |c| c.deinit(), .err => |e| { @@ -441,7 +430,8 @@ pub fn import_vars(ctx: Ctx, port_name: []const u8, new_vars_file: fs.Path) !voi }, } - try fs.mv_file(new_vars_file, info.vars_file()); + const vars_file = info.vars_file(); + try fs.mv_file(new_vars_file, &vars_file); } pub const Update_Options = struct { @@ -456,6 +446,7 @@ pub fn update_version(ctx: Ctx, port_name: []const u8, options: Update_Options) var source: Source = try .from_file(ctx, info); defer source.deinit(); + ctx.env.info("===> {s}: updating repository", .{info.name}); const old_version: Version = try .from_file(ctx, info); const old_commit = source.commit; const new_commit = try source.repo.update(ctx.env); @@ -475,13 +466,17 @@ pub fn update_version(ctx: Ctx, port_name: []const u8, options: Update_Options) } source.commit = new_commit; - try source.to_file(info.src_file()); - try new_version.to_file(info.version_file()); + + ctx.env.info("===> {s}: updating port data", .{info.name}); + const src_file = info.src_file(); + const version_file = info.version_file(); + try source.to_file(&src_file); + try new_version.to_file(&version_file); try render_port(ctx, .{ .info = info, .version = new_version, - .source = source, + .source = &source, }); return new_version; } @@ -504,12 +499,13 @@ pub fn bump_version(ctx: Ctx, port_name: []const u8, options: Bump_Options) !Ver .minor => .{ .major = version.major, .minor = version.minor + 1, .patch = 0 }, .patch => .{ .major = version.major, .minor = version.minor, .patch = version.patch + 1 }, }; - try new_version.to_file(info.version_file()); + const version_file = info.version_file(); + try new_version.to_file(&version_file); try render_port(ctx, .{ .info = info, .version = new_version, - .source = source, + .source = &source, }); return new_version; } @@ -520,23 +516,22 @@ pub const Distfile = struct { }; pub fn list_distfiles(ctx: Ctx, port_name: []const u8) !std.ArrayList(Distfile) { - const dist_dir = ctx.env.ports_dist_dir(ctx.tree.name, port_name); - return try list_distfiles_sorted(ctx.env.allocator, port_name, dist_dir); + const distdir = ctx.env.ports_dist_dir(ctx.tree.name, port_name); + return try fetch_port_distfiles_sorted(ctx.env.allocator, port_name, &distdir); } pub fn rollback(ctx: Ctx, port_name: []const u8, old_version: Version) !void { const info = try must_get(ctx, port_name); defer info.deinit(); - var version_buf: [64]u8 = undefined; - const version_str = std.fmt.bufPrint(&version_buf, "{f}", .{old_version}) catch unreachable; - - var distname_buf: [std.fs.max_name_bytes]u8 = undefined; - const distname = assemble_distname(&distname_buf, info.name, version_str); - var dist_file = ctx.env.ports_dist_dir(ctx.tree.name, info.name); - dist_file.append(.{distname}); - if (!try fs.file_exists(dist_file)) { - fatal("distfile {s} cannot be found", .{distname}); + const distdir = ctx.env.ports_dist_dir(ctx.tree.name, port_name); + var distfiles = try fetch_distfiles(ctx.env.allocator, &distdir, .{ + .port_name = port_name, + .version = old_version, + }); + defer distfiles.deinit(ctx.env.allocator); + if (distfiles.items.len == 0) { + fatal("no distfiles found for port {s}", .{port_name}); } try render_port(ctx, .{ @@ -553,19 +548,14 @@ pub fn remove(ctx: Ctx, port_name: []const u8) !bool { } pub inline fn remove_all(ctx: Ctx) !void { - var dir = std.fs.openDirAbsoluteZ(ctx.tree.root.name(), .{ .iterate = true }) catch |err| { - return switch (err) { - error.FileNotFound => return, - error.BadPathName, error.InvalidUtf8 => error.BadPathName, - error.NameTooLong => unreachable, - else => err, - }; + var iter = fs.iterate(&ctx.tree.root) catch |err| { + if (err == error.FileNotFound) return; + return err; }; - defer dir.close(); + defer iter.deinit(); - var iter = dir.iterate(); while (try iter.next()) |entry| { - if (entry.kind == .directory) { + if (entry.type == .dir) { if (try get(ctx, entry.name)) |p| { try remove_existing_port(ctx, p); } @@ -577,16 +567,17 @@ fn remove_existing_port(ctx: Ctx, info: Info) !void { const port_dir = ctx.env.ports_tree_path(ctx.tree.name, .{ info.category, info.name }); const dist_dir = ctx.env.ports_dist_dir(ctx.tree.name, info.name); const src_dir = ctx.env.ports_src_path(ctx.tree.name, .{info.name}); - try fs.rm(port_dir); - try fs.rm(dist_dir); - try fs.rm(src_dir); - try fs.rm(ctx.tree.port_path(info.name)); + const port_path = ctx.tree.port_path(info.name); + try fs.rm(&port_dir); + try fs.rm(&dist_dir); + try fs.rm(&src_dir); + try fs.rm(&port_path); } const Render_Options = struct { info: Info, version: Version, - source: ?Source, // null if it already exists + source: ?*const Source, // null if it already exists }; fn render_port(ctx: Ctx, opts: Render_Options) !void { @@ -600,23 +591,19 @@ fn render_port(ctx: Ctx, opts: Render_Options) !void { var version_buf: [64]u8 = undefined; const version_str = std.fmt.bufPrint(&version_buf, "{f}", .{opts.version}) catch unreachable; - var distname_buf: [std.fs.max_name_bytes]u8 = undefined; - const distname = assemble_distname(&distname_buf, opts.info.name, version_str); - // While generating the port files we write to the following working directories: // // * port files: <wrk>/<tree>/ports/<port>/* - // * dist file: <wrk>/<tree>/dists/<distname> + // * dist files: <wrk>/<tree>/dists/* // const port_dir = ctx.env.ports_tree_path(ctx.tree.name, .{ opts.info.category, opts.info.name }); const dist_dir = ctx.env.ports_dist_dir(ctx.tree.name, opts.info.name); - const dist_file: fs.Path = .join(.{ dist_dir.name(), distname }); const wrk_port_dir = ctx.env.work_path(.{ ctx.tree.name, "ports", opts.info.name }); - const wrk_dist_file = ctx.env.work_path(.{ ctx.tree.name, "dist", distname }); - try fs.rm(wrk_port_dir); - try fs.rm(wrk_dist_file.dir().?); - try fs.mkdir_all(wrk_port_dir); - try fs.mkdir_all(wrk_dist_file.dir().?); + const wrk_dist_dir = ctx.env.work_path(.{ ctx.tree.name, "dist" }); + try fs.rm(&wrk_port_dir); + try fs.rm(&wrk_dist_dir); + try fs.mkdir_all(&wrk_port_dir); + try fs.mkdir_all(&wrk_dist_dir); const template: tmpl.Template = .init(ctx.env, opts.info.template); const template_conf = try tmpl.read_config(ctx.env, template); @@ -631,125 +618,230 @@ fn render_port(ctx: Ctx, opts: Render_Options) !void { try predefined_vars.add_var(".portversion", version_str); try predefined_vars.add_var(".category", opts.info.category); try predefined_vars.add_var(".distdir", dist_dir.name()); - try predefined_vars.add_var(".distname", distname); + for (template_conf.archives[0..template_conf.archives_len]) |*archive| { + var distname_buf: [std.fs.max_name_bytes]u8 = undefined; + const distname = assemble_distname(&distname_buf, .{ + .dist_id = archive.id, + .port_name = opts.info.name, + .version = version_str, + }); + if (archive.id.len > 0) { + var key_buf: [512]u8 = undefined; + try predefined_vars.add_var(concat(&key_buf, &.{ ".distname.", archive.id }), distname); + } else { + try predefined_vars.add_var(".distname", distname); + } + } - var make_portsdir_buf: [std.fs.max_path_bytes]u8 = undefined; - var make_distdir_buf: [std.fs.max_path_bytes]u8 = undefined; + var make_portsdir_buf: [fs.max_path_bytes]u8 = undefined; + var make_distdir_buf: [fs.max_path_bytes]u8 = undefined; const make_portsdir = concat(&make_portsdir_buf, &.{ "PORTSDIR=", ctx.env.ports_dir }); const make_distdir = concat(&make_distdir_buf, &.{ "DISTDIR=", dist_dir.name() }); ctx.env.info("===> {s}: writing port files", .{opts.info.name}); const vars = [_]conf.Config{ predefined_vars, port_vars }; - try template.render(ctx.env, &vars, wrk_port_dir); - try fs.mv_dir(ctx.env.allocator, wrk_port_dir, port_dir); + try template.render(ctx.env, &vars, &wrk_port_dir); + try fs.mv_dir(&wrk_port_dir, &port_dir); + var new_distfiles_created = false; if (opts.source) |src| { var prepared_dist = false; - if (template_conf.prepare_dist_target) |prepare_dist_target| { + defer { + if (prepared_dist) src.repo.reset(ctx.env); + } + + if (template_conf.prepare_target) |prepare_target| { ctx.env.info("===> {s}: prepare distribution", .{opts.info.name}); var make_target_buf: [std.fs.max_name_bytes]u8 = undefined; const res = shell.exec(.{ .env = ctx.env, - .argv = &.{ "make", "-V", concat(&make_target_buf, &.{ "${.ALLTARGETS:M", prepare_dist_target, "}" }), make_portsdir }, - .cwd = port_dir, + .argv = &.{ "make", "-V", concat(&make_target_buf, &.{ "${.ALLTARGETS:M", prepare_target, "}" }), make_portsdir }, + .cwd = &port_dir, }); const has_target = switch (res) { .success => |out| has: { - defer ctx.env.dealloc(out); - const target_found = std.mem.trim(u8, out, " \t\n"); - break :has std.mem.eql(u8, target_found, prepare_dist_target); + const output = out orelse break :has false; + defer ctx.env.dealloc(output); + const target_found = std.mem.trim(u8, output, " \t\n"); + break :has std.mem.eql(u8, target_found, prepare_target); }, .failure => |out| { - ctx.env.err("failed to check for makefile target `{s}`\n", .{prepare_dist_target}); - fatal("{s}", .{out}); + ctx.env.err("failed to check for makefile target `{s}`\n", .{prepare_target}); + fatal("{s}", .{out orelse ""}); }, }; if (has_target) { - var make_wrksrc_buf: [std.fs.max_path_bytes]u8 = undefined; + var make_wrksrc_buf: [fs.max_path_bytes]u8 = undefined; const make_wrksrc = concat(&make_wrksrc_buf, &.{ "WRKSRC=", src.repo.path.name() }); shell.run(.{ .env = ctx.env, - .argv = &.{ "make", make_portsdir, make_distdir, make_wrksrc, prepare_dist_target }, - .cwd = port_dir, + .argv = &.{ "make", make_portsdir, make_distdir, make_wrksrc, prepare_target }, + .cwd = &port_dir, }); prepared_dist = true; } else { - ctx.env.info("Target `{s}` not found (skipping)", .{prepare_dist_target}); + ctx.env.info("Target `{s}` not found (skipping)", .{prepare_target}); } } - ctx.env.info("===> {s}: creating distfiles", .{opts.info.name}); - var tar_rename_buf: [std.fs.max_path_bytes]u8 = undefined; - const tar_rename = concat(&tar_rename_buf, &.{ ",^,", distname[0 .. distname.len - distname_ext.len], "/," }); - shell.run(.{ - .env = ctx.env, - .argv = &.{ "tar", "-czf", wrk_dist_file.name(), "-C", src.repo.path.name(), "--exclude-vcs", "-s", tar_rename, "." }, - }); - if (prepared_dist) src.repo.reset(ctx.env); - try fs.mv_file(wrk_dist_file, dist_file); - ctx.env.info("distfile: {s}", .{dist_file.name()}); + for (template_conf.archives[0..template_conf.archives_len]) |*archive| { + var distname_buf: [std.fs.max_name_bytes]u8 = undefined; + const distname = assemble_distname(&distname_buf, .{ + .dist_id = archive.id, + .port_name = opts.info.name, + .version = version_str, + }); + const wrk_dist_file: fs.Path = .join(.{ wrk_dist_dir.name(), distname }); + const dist_file: fs.Path = .join(.{ dist_dir.name(), distname }); + + if (archive.id.len > 0) { + ctx.env.info("===> {s}: creating distfile [{s}]", .{ opts.info.name, archive.id }); + } else { + ctx.env.info("===> {s}: creating distfile", .{opts.info.name}); + } + var tar_rename_buf: [fs.max_path_bytes]u8 = undefined; + const tar_rename = concat(&tar_rename_buf, &.{ ",^,", distname[0 .. distname.len - distname_ext.len], "/," }); + + var root = src.repo.path; + if (archive.root) |archive_root| root.append(.{archive_root}); + + var tar_cmd: [8 + archive.paths.len][]const u8 = undefined; + var tar_cmd_len: usize = 8; + tar_cmd[0] = "tar"; + tar_cmd[1] = "-czf"; + tar_cmd[2] = wrk_dist_file.name(); + tar_cmd[3] = "-C"; + tar_cmd[4] = root.name(); + tar_cmd[5] = "-s"; + tar_cmd[6] = tar_rename; + tar_cmd[7] = "--exclude-vcs"; + if (archive.paths_len == 0) { + tar_cmd[tar_cmd_len] = "."; + tar_cmd_len += 1; + } else { + var existings_paths: usize = 0; + for (archive.paths[0..archive.paths_len]) |path| { + const abs_path: fs.Path = .join(.{ root.name(), path }); + if (fs.path_exists(&abs_path)) { + tar_cmd[tar_cmd_len] = path; + tar_cmd_len += 1; + existings_paths += 1; + } else { + ctx.env.warn("cannot find path in archive root : {s}", .{path}); + } + } + if (existings_paths == 0) { + tar_cmd[tar_cmd_len] = "--files-from"; + tar_cmd_len += 1; + tar_cmd[tar_cmd_len] = "/dev/null"; + tar_cmd_len += 1; + } + } + + shell.run(.{ + .env = ctx.env, + .argv = tar_cmd[0..tar_cmd_len], + }); + + try fs.mv_file(&wrk_dist_file, &dist_file); + ctx.env.info("distfile: {s}", .{dist_file.name()}); + new_distfiles_created = true; + } } - ctx.env.info("===> {s}: creating distinfo", .{opts.info.name}); + ctx.env.info("===> {s}: creating port's distinfo", .{opts.info.name}); shell.run(.{ .env = ctx.env, .argv = &.{ "make", make_portsdir, make_distdir, "makesum" }, - .cwd = port_dir, + .cwd = &port_dir, }); - if (opts.source != null) { + if (new_distfiles_created) { ctx.env.info("===> {s}: cleaning up old distfiles", .{opts.info.name}); - var history = list_distfiles_sorted(ctx.env.allocator, opts.info.name, dist_dir) catch |err| { - ctx.env.warn("cannot remove old distfiles from {s} ({s})", .{ dist_dir.name(), @errorName(err) }); + var history = fetch_port_distfiles_sorted(ctx.env.allocator, opts.info.name, &dist_dir) catch |err| { + ctx.env.warn("cannot fetch distfiles from {s} ({s})", .{ dist_dir.name(), @errorName(err) }); return; }; defer history.deinit(ctx.env.allocator); - - if (history.items.len > ctx.env.distfiles_history) { - var to_delete: usize = history.items.len - ctx.env.distfiles_history; - var i: usize = history.items.len; - while (i > 0 and to_delete > 0) { - i -= 1; - const dist: *const Distfile = &history.items[i]; - if (!std.mem.endsWith(u8, dist.path.name(), distname)) { - fs.rm(dist.path) catch |err| { - ctx.env.warn("cannot remove {s} ({s})", .{ dist.path.name(), @errorName(err) }); + if (history.items.len > 0) { + var version = history.items[0].version; + var visited: usize = 1; + for (history.items[1..]) |*distfile| { + if (!Version.eq(distfile.version, version)) { + visited += 1; + version = distfile.version; + } + if (visited > ctx.env.distfiles_history) { + fs.rm(&distfile.path) catch |err| { + ctx.env.warn("cannot remove {s} ({s})", .{ distfile.path.name(), @errorName(err) }); }; - to_delete -= 1; } } } } } -/// Get a list of all distfiles sorted from new to old. -fn list_distfiles_sorted(allocator: std.mem.Allocator, port_name: []const u8, dist_dir: fs.Path) !std.ArrayList(Distfile) { - var iter = try fs.iterate_files(dist_dir) orelse return .empty; +/// Get a list of all distfiles of a single port sorted from new to old. +fn fetch_port_distfiles_sorted(allocator: std.mem.Allocator, port_name: []const u8, dist_dir: *const fs.Path) !std.ArrayList(Distfile) { + const distfiles = try fetch_distfiles(allocator, dist_dir, .{ .port_name = port_name }); + std.mem.sortUnstable(Distfile, distfiles.items, {}, struct { + pub fn gt(_: void, left: Distfile, right: Distfile) bool { + return if (Version.eq(right.version, left.version)) + std.mem.lessThan(u8, right.path.name(), left.path.name()) + else + Version.less(right.version, left.version); + } + }.gt); + return distfiles; +} + +const Distfile_Filter = struct { + port_name: []const u8, + version: ?Version = null, +}; + +fn fetch_distfiles(allocator: std.mem.Allocator, dist_dir: *const fs.Path, filter: Distfile_Filter) !std.ArrayList(Distfile) { + var iter = fs.iterate(dist_dir) catch |err| { + if (err == error.FileNotFound) return .empty; + return err; + }; defer iter.deinit(); var distfiles: std.ArrayList(Distfile) = .empty; - while (iter.next()) |distfile| { - if (std.mem.startsWith(u8, distfile, port_name) and std.mem.endsWith(u8, distfile, distname_ext)) { - const version = Version.parse(distfile[port_name.len + 1 .. distfile.len - distname_ext.len]) catch continue; + while (try iter.next()) |entry| { + if (entry.type != .file) continue; + + const distfile = entry.name; + if (std.mem.startsWith(u8, distfile, filter.port_name) and std.mem.endsWith(u8, distfile, distname_ext)) { + const last_dash = std.mem.lastIndexOfScalar(u8, distfile, '-') orelse continue; + const version = Version.parse(distfile[last_dash + 1 .. distfile.len - distname_ext.len]) catch continue; + if (filter.version) |expected_version| { + if (!Version.eq(expected_version, version)) { + continue; + } + } try distfiles.append(allocator, .{ .version = version, .path = .join(.{ dist_dir.name(), distfile }), }); } } - - std.mem.sortUnstable(Distfile, distfiles.items, {}, struct { - pub fn gt(_: void, left: Distfile, right: Distfile) bool { - return Version.less(right.version, left.version); - } - }.gt); return distfiles; } +const Distname_Parts = struct { + dist_id: []const u8, + port_name: []const u8, + version: []const u8, +}; + /// Build a distname that matches FreeBSD's default value for ${DISTNAME}. -fn assemble_distname(buf: []u8, port_name: []const u8, version: []const u8) []const u8 { - return concat(buf, &.{ port_name, "-", version, distname_ext }); +fn assemble_distname(buf: []u8, parts: Distname_Parts) []const u8 { + return if (parts.dist_id.len > 0) + concat(buf, &.{ parts.port_name, "-", parts.dist_id, "-", parts.version, distname_ext }) + else + return concat(buf, &.{ parts.port_name, "-", parts.version, distname_ext }); } fn parse_int(comptime Int: type, text: []const u8) ?Int { @@ -785,19 +877,21 @@ test "parse version" { try std.testing.expectEqual(@as(u32, 0), version.date); try std.testing.expectEqual(@as(u32, 0), version.revision); - version = try .parse("3.2.1.d20180709"); + version = try .parse("3.2.1.20250709r0"); try std.testing.expectEqual(@as(u16, 3), version.major); try std.testing.expectEqual(@as(u16, 2), version.minor); try std.testing.expectEqual(@as(u16, 1), version.patch); - try std.testing.expectEqual(@as(u32, 20180709), version.date); + try std.testing.expectEqual(@as(u32, 20250709), version.date); try std.testing.expectEqual(@as(u32, 0), version.revision); - version = try .parse("2.1.3.d20210429.r7"); + version = try .parse("2.1.3.20250429r7"); try std.testing.expectEqual(@as(u16, 2), version.major); try std.testing.expectEqual(@as(u16, 1), version.minor); try std.testing.expectEqual(@as(u16, 3), version.patch); - try std.testing.expectEqual(@as(u32, 20210429), version.date); + try std.testing.expectEqual(@as(u32, 20250429), version.date); try std.testing.expectEqual(@as(u32, 7), version.revision); + + try std.testing.expectError(error.invalid_version, Version.parse("3.1.2.20250511")); } test "compare version" { diff --git a/src/shell.zig b/src/shell.zig index d361b09..f933777 100644 --- a/src/shell.zig +++ b/src/shell.zig @@ -4,86 +4,43 @@ const assert = std.debug.assert; const cli = @import("cli.zig"); const fs = @import("fs.zig"); const conf = @import("conf.zig"); +const time = @import("time.zig"); const Env = @import("env.zig"); const fatal = @import("cli.zig").fatal; pub const Exec_Args = struct { - env: Env, + env: *const Env, argv: []const []const u8, - cwd: ?fs.Path = null, + cwd: ?*const fs.Path = null, }; pub const Exec_Result = union(enum) { - success: []const u8, - failure: []const u8, + success: ?[]const u8, + failure: ?[]const u8, }; pub fn exec(args: Exec_Args) Exec_Result { - assert(args.argv.len > 0); - - const res = std.process.Child.run(.{ - .allocator = args.env.allocator, - .argv = args.argv, - .cwd = if (args.cwd) |cwd| cwd.name() else null, - .cwd_dir = if (args.cwd == null) std.fs.cwd() else null, - }) catch |err| { - fatal("error executing {s}: {s}", .{ args.argv[0], @errorName(err) }); - }; - switch (res.term) { - .Exited => |code| { - if (code == 0) { - args.env.dealloc(res.stderr); - return .{ .success = res.stdout }; - } else if (res.stderr.len > 0) { - args.env.dealloc(res.stdout); - return .{ .failure = res.stderr }; - } else { - args.env.dealloc(res.stderr); - return .{ .failure = res.stdout }; - } - }, - .Signal => fatal("signal received executing {s}", .{args.argv[0]}), - .Stopped => fatal("stopped executing {s}", .{args.argv[0]}), - .Unknown => fatal("unknown error executing {s}", .{args.argv[0]}), - } + return spawn_proc(args, true); } pub fn run(args: Exec_Args) void { - assert(args.argv.len > 0); - - var child = std.process.Child.init(args.argv, args.env.allocator); - if (args.cwd) |cwd| child.cwd = cwd.name(); - child.cwd_dir = if (args.cwd == null) std.fs.cwd() else null; - child.spawn() catch |err| { - fatal("error executing {s}: {s}", .{ args.argv[0], @errorName(err) }); - }; - errdefer _ = child.kill() catch {}; - - const term = child.wait() catch |err| { - fatal("error waiting for {s}: {s}", .{ args.argv[0], @errorName(err) }); - }; - switch (term) { - .Exited => |code| if (code != 0) std.process.exit(code), - .Signal => fatal("signal received executing {s}", .{args.argv[0]}), - .Stopped => fatal("stopped executing {s}", .{args.argv[0]}), - .Unknown => fatal("unknown error executing {s}", .{args.argv[0]}), - } + _ = spawn_proc(args, false); } -pub fn edit_config_file(env: Env, file: fs.Path, comptime verify: fn (env: Env, vars: conf.Config) bool) !void { +pub fn edit_config_file(env: *const Env, file: *const fs.Path, comptime verify: fn (env: *const Env, vars: conf.Config) bool) !void { const tmp_file = tmp_edit_file(env, file); - fs.cp_file(file, tmp_file) catch |err| { + fs.cp_file(file, &tmp_file) catch |err| { switch (err) { - error.FileNotFound => try fs.touch(tmp_file), + error.FileNotFound => try fs.touch(&tmp_file), else => return err, } }; edit: while (true) { const vars: ?conf.Config = parse: while (true) { - try exec_editor(env, tmp_file); - const parsed = try conf.Config.from_file(env.allocator, file.name()); + try exec_editor(env, &tmp_file); + const parsed = try conf.Config.from_file(env.allocator, file); switch (parsed) { .conf => |c| break :parse c, .err => |e| { @@ -99,7 +56,7 @@ pub fn edit_config_file(env: Env, file: fs.Path, comptime verify: fn (env: Env, if (vars) |v| { defer v.deinit(); if (verify(env, v)) { - try fs.mv_file(tmp_file, file); + try fs.mv_file(&tmp_file, file); return; } switch (ask_edit_again()) { @@ -110,12 +67,12 @@ pub fn edit_config_file(env: Env, file: fs.Path, comptime verify: fn (env: Env, break :edit; } } - try fs.rm(tmp_file); + try fs.rm(&tmp_file); } -pub fn edit_config_file_noverify(env: Env, file: fs.Path) !void { +pub fn edit_config_file_noverify(env: *const Env, file: *const fs.Path) !void { try edit_config_file(env, file, struct { - fn noverify(_: Env, _: conf.Config) bool { + fn noverify(_: *const Env, _: conf.Config) bool { return true; } }.noverify); @@ -137,49 +94,45 @@ fn ask_edit_again() enum { again, cancel } { } } -fn exec_editor(env: Env, file: fs.Path) !void { - const pid = std.posix.fork() catch |err| { - fatal("cannot fork editor: {s}", .{@errorName(err)}); - }; +fn exec_editor(env: *const Env, file: *const fs.Path) !void { + var configured_editor_buf: [fs.max_path_bytes]u8 = undefined; + var configured_editor: ?[:0]u8 = null; + if (env.editor_cmd) |cmd| { + @memcpy(configured_editor_buf[0..cmd.len], cmd); + configured_editor_buf[cmd.len] = 0; + configured_editor = configured_editor_buf[0..cmd.len :0]; + } - if (pid == 0) { - // editor process - var editor_env: [512:null]?[*:0]const u8 = undefined; - var editor_envlen: usize = 0; - const environ = if (std.os.environ.len < 512) std.os.environ else std.os.environ[0..511]; - for (environ) |e| { - editor_env[editor_envlen] = e; - editor_envlen += 1; - } - editor_env[editor_envlen] = null; + const editor = configured_editor orelse + env.get("VISUAL") orelse + env.get("EDITOR") orelse + "vi"; - var configured_editor_buf: [std.fs.max_path_bytes]u8 = undefined; - var configured_editor: ?[:0]u8 = null; - if (env.editor_cmd) |cmd| { - @memcpy(configured_editor_buf[0..cmd.len], cmd); - configured_editor_buf[cmd.len] = 0; - configured_editor = configured_editor_buf[0..cmd.len :0]; + const args: Exec_Args = .{ + .env = env, + .argv = &.{ editor, file.name() }, + }; + const res = spawn_proc(args, false); + if (res.failure) |failure| { + if (failure.len == 0) { + fatal("failed to open editor", .{}); + } else { + fatal("{s}", .{failure}); } - - const editor = configured_editor orelse std.posix.getenv("VISUAL") orelse std.posix.getenv("EDITOR") orelse "vi"; - const editor_argv: [*:null]const ?[*:0]const u8 = &.{ editor, file.name(), null }; - std.posix.execvpeZ(editor_argv[0].?, editor_argv, &editor_env) catch unreachable; - std.process.exit(1); } - - _ = std.posix.waitpid(pid, 0); } /// Derive the filename for the temporary file of `file`. /// The filename is "{relname}.{timestamp}", where `relname` is the relative /// part of file regarding to etc. -fn tmp_edit_file(env: Env, file: fs.Path) fs.Path { - const relname = file.relname(env.etc_path(.{})).?; +fn tmp_edit_file(env: *const Env, file: *const fs.Path) fs.Path { + const etc = env.etc_path(.{}); + const relname = file.relname(&etc).?; var tmp_filename_buf: [std.fs.max_name_bytes]u8 = undefined; var tmp_filename_len: usize = 0; - tmp_filename_len += std.fmt.printInt(tmp_filename_buf[tmp_filename_len..], std.time.timestamp(), 16, .lower, .{}); + tmp_filename_len += std.fmt.printInt(tmp_filename_buf[tmp_filename_len..], time.now(), 16, .lower, .{}); tmp_filename_buf[tmp_filename_len] = '.'; tmp_filename_len += 1; for (relname) |c| { @@ -189,3 +142,143 @@ fn tmp_edit_file(env: Env, file: fs.Path) fs.Path { return env.work_path(.{ "edit", tmp_filename_buf[0..tmp_filename_len] }); } + +fn spawn_proc(args: Exec_Args, need_stdout: bool) Exec_Result { + assert(args.argv.len > 0); + + var stdout_pipe: [2]std.posix.fd_t = undefined; + if (need_stdout) { + if (std.posix.system.pipe(&stdout_pipe) != 0) { + fatal("error executing {s}: unable to create pipe", .{args.argv[0]}); + } + } + + const pid = std.posix.system.fork(); + switch (pid) { + -1 => { + if (need_stdout) { + _ = std.posix.system.close(stdout_pipe[0]); + _ = std.posix.system.close(stdout_pipe[1]); + } + fatal("error executing {s}: unable to fork", .{args.argv[0]}); + }, + 0 => { + // child process + if (need_stdout) { + if (std.posix.system.dup2(stdout_pipe[1], std.posix.STDOUT_FILENO) == -1) unreachable; + if (std.posix.system.dup2(stdout_pipe[1], std.posix.STDERR_FILENO) == -1) unreachable; + _ = std.posix.system.close(stdout_pipe[0]); + _ = std.posix.system.close(stdout_pipe[1]); + } + + const argv = args.env.allocator.allocSentinel(?[*:0]const u8, args.argv.len, null) catch { + fatal("error executing {s}: out of memory", .{args.argv[0]}); + }; + for (args.argv, 0..) |arg, i| { + argv[i] = args.env.allocator.dupeZ(u8, arg) catch { + fatal("error executing {s}: out of memory", .{args.argv[0]}); + }; + } + + var env: [512:null]?[*:0]const u8 = undefined; + var envlen: usize = 0; + while (std.posix.system.environ[envlen]) |e| { + env[envlen] = e; + envlen += 1; + if (envlen >= env.len - 1) break; + } + env[envlen] = null; + + if (args.cwd) |cwd| { + if (std.posix.system.chdir(cwd.name()) == -1) { + fatal("error executing {s}: chdir failed", .{args.argv[0]}); + } + } + + const sent = std.mem.findSentinel(u8, 0, argv[0].?); + const name = argv[0].?[0..sent :0]; + + var eaccess = false; + if (std.mem.findScalar(u8, name, '/')) |_| { + // We have a relative or absolute path. No need to consult the PATH env. + const res = std.posix.system.execve(name, argv, &env); + switch (std.posix.errno(res)) { + .NOENT => {}, + .ACCES => eaccess = true, + else => |err| fatal("error executing {s}: {}", .{ name, err }), + } + } else { + var path_env = args.env.get("PATH") orelse ""; + var path: fs.Path = undefined; + while (path_env.len > 0) { + const colon = std.mem.findScalar(u8, path_env, ':') orelse path_env.len; + path = .join(.{ if (colon == 0) "." else path_env[0..colon], name }); + path_env = if (colon == path_env.len) "" else path_env[colon + 1 ..]; + + const res = std.posix.system.execve(path.name(), argv, &env); + switch (std.posix.errno(res)) { + .NOENT => {}, + .ACCES => eaccess = true, + else => |err| fatal("error executing {s}: {}", .{ name, err }), + } + } + } + + if (eaccess) { + fatal("permission denied: {s}", .{name}); + } else { + fatal("not found: {s}", .{name}); + } + }, + else => { + // parent process + var stdout_buf: []u8 = &[0]u8{}; + var stdout_len: usize = 0; + defer if (stdout_buf.len > 0) args.env.dealloc(stdout_buf); + if (need_stdout) { + _ = std.posix.system.close(stdout_pipe[1]); + defer _ = std.posix.system.close(stdout_pipe[0]); + + while (true) { + if (stdout_len >= stdout_buf.len) { + const newbuf = args.env.alloc(stdout_buf.len + 1024); + @memcpy(newbuf[0..stdout_len], stdout_buf[0..stdout_len]); + args.env.dealloc(stdout_buf); + stdout_buf = newbuf; + } + + const res = std.posix.system.read(stdout_pipe[0], stdout_buf.ptr + stdout_len, stdout_buf.len - stdout_len); + if (res < 0) { + if (std.posix.errno(res) == .INTR) continue; + fatal("error executing {s}: cannot read stdout", .{args.argv[0]}); + } + if (res == 0) break; + const n: usize = @intCast(res); + stdout_len += n; + } + } + + var status: c_int = undefined; + while (true) { + const res = std.posix.system.waitpid(pid, &status, 0); + if (res < 0) { + if (std.posix.errno(res) == std.posix.E.INTR) continue; + fatal("error executing {s}: failed waiting for process", .{args.argv[0]}); + } + + const s: u32 = @intCast(status); + if (std.posix.W.IFEXITED(s)) { + const code = std.posix.W.EXITSTATUS(s); + const output: ?[]const u8 = if (stdout_len > 0) args.env.dupe(stdout_buf[0..stdout_len]) else null; + return if (code == 0) .{ .success = output } else .{ .failure = output }; + } else if (std.posix.W.IFSIGNALED(s)) { + fatal("signal received executing {s}", .{args.argv[0]}); + } else if (std.posix.W.IFSTOPPED(s)) { + unreachable; + } else { + fatal("unknown error executing {s}", .{args.argv[0]}); + } + } + }, + } +} 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]); } diff --git a/src/time.zig b/src/time.zig index 290c9de..9c162e2 100644 --- a/src/time.zig +++ b/src/time.zig @@ -83,7 +83,7 @@ pub const Time = struct { }; } - pub fn format(self: Time, w: *std.io.Writer) std.io.Writer.Error!void { + pub fn format(self: Time, w: *std.Io.Writer) std.Io.Writer.Error!void { try w.print( "{d:0>4}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2} UTC", .{ self.year, self.month, self.day, self.hour, self.minute, self.second }, @@ -92,8 +92,10 @@ pub const Time = struct { }; pub fn now() u64 { - const ts = std.posix.clock_gettime(.REALTIME) catch return 0; - return if (ts.sec < 0) 0 else @intCast(ts.sec); + var ts: std.posix.timespec = undefined; + const res = std.posix.system.clock_gettime(std.posix.CLOCK.REALTIME, &ts); + if (res < 0 or ts.sec < 0) return 0; + return @intCast(ts.sec); } test "time from timestamp" { diff --git a/src/tree.zig b/src/tree.zig index 8599c7c..75efa9c 100644 --- a/src/tree.zig +++ b/src/tree.zig @@ -15,17 +15,16 @@ pub const Tree = struct { pub const Iterator = struct { root: fs.Path, - dir: ?std.fs.Dir = null, - iter: ?std.fs.Dir.Iterator = null, + iter: ?fs.Iterator = null, pub fn deinit(self: *Iterator) void { - if (self.dir) |*dir| dir.close(); + if (self.iter) |*it| it.deinit(); } pub fn next(self: *Iterator) !?Tree { if (self.iter) |*iter| { while (try iter.next()) |entry| { - if (entry.kind == .directory) { + if (entry.type == .dir) { return .{ .name = entry.name, .root = .join(.{ self.root.name(), entry.name }), @@ -37,31 +36,33 @@ pub const Iterator = struct { } }; -pub inline fn iterate(env: Env) !Iterator { +pub inline fn iterate(env: *const Env) !Iterator { const root = tree_root_dir(env); - const dir = std.fs.openDirAbsoluteZ(root.name(), .{ .iterate = true }) catch |err| { + const iter = fs.iterate(&root) catch |err| { if (err == error.FileNotFound) return .{ .root = undefined }; return err; }; + return .{ .root = root, - .dir = dir, - .iter = dir.iterate(), + .iter = iter, }; } -pub fn get(env: Env, tree_name: []const u8) !?Tree { +pub fn get(env: *const Env, tree_name: []const u8) !?Tree { var tree_root = tree_root_dir(env); tree_root.append(.{tree_name}); return .{ .name = tree_name, .root = tree_root }; } -pub fn remove(env: Env, tree: Tree) !void { - try fs.rm(tree.root); - fs.rm(env.ports_tree_path(tree.name, .{})) catch {}; - fs.rm(env.ports_src_path(tree.name, .{})) catch {}; +pub fn remove(env: *const Env, tree: Tree) !void { + const tree_path = env.ports_tree_path(tree.name, .{}); + const src_path = env.ports_src_path(tree.name, .{}); + try fs.rm(&tree.root); + fs.rm(&tree_path) catch {}; + fs.rm(&src_path) catch {}; } -fn tree_root_dir(env: Env) fs.Path { +fn tree_root_dir(env: *const Env) fs.Path { return env.etc_path(.{"ports"}); } |