aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortsne <tsne.dev@outlook.com>2026-05-13 11:33:08 +0200
committertsne <tsne.dev@outlook.com>2026-05-22 08:25:27 +0200
commitc5b88c66ca8956df26ed87a7397862009e18cfc7 (patch)
tree5a862bdacdeaa7fff73bf1bb2a423eee38f9b6e5 /src
parentinitial commit (diff)
downloadporteur-c5b88c66ca8956df26ed87a7397862009e18cfc7.tar.gz
upgrade to zig 0.16
Diffstat (limited to 'src')
-rw-r--r--src/cli.zig543
-rw-r--r--src/conf.zig102
-rw-r--r--src/env.zig30
-rw-r--r--src/fs.zig500
-rw-r--r--src/git.zig54
-rw-r--r--src/main.zig256
-rw-r--r--src/port.zig472
-rw-r--r--src/shell.zig271
-rw-r--r--src/template.zig359
-rw-r--r--src/time.zig8
-rw-r--r--src/tree.zig29
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: {
diff --git a/src/fs.zig b/src/fs.zig
index f1bee2d..8519ac8 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -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"});
}