aboutsummaryrefslogtreecommitdiff
path: root/src/main.zig
diff options
context:
space:
mode:
authortsne <tsne.dev@outlook.com>2025-01-01 14:58:28 +0100
committertsne <tsne.dev@outlook.com>2025-10-30 08:32:49 +0100
commit44e5ad763794a438ecfd50c8b7f6ea760ea82da5 (patch)
tree575a1fc72ceb1d7f052cf582abc1e038e27f69a3 /src/main.zig
downloadporteur-44e5ad763794a438ecfd50c8b7f6ea760ea82da5.tar.gz
initial commitHEADmain
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig1195
1 files changed, 1195 insertions, 0 deletions
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..bafcbd8
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,1195 @@
+const options = @import("options");
+const std = @import("std");
+const assert = std.debug.assert;
+
+const cli = @import("cli.zig");
+const fs = @import("fs.zig");
+const conf = @import("conf.zig");
+const tmpl = @import("template.zig");
+const tree = @import("tree.zig");
+const port = @import("port.zig");
+const git = @import("git.zig");
+const Env = @import("env.zig");
+const Time = @import("time.zig").Time;
+
+const default_tree_name = "default";
+
+pub fn main() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ const allocator = gpa.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);
+}
+
+const Main_Command = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur",
+ .synopsis = &.{
+ "porteur [--etc=⟨path⟩] tree ⟨command⟩ [options] [⟨args⟩]",
+ "porteur [--etc=⟨path⟩] tmpl ⟨command⟩ [options] [⟨args⟩]",
+ "porteur [--etc=⟨path⟩] port ⟨command⟩ [options] [⟨args⟩]",
+ "porteur help [⟨command⟩ [⟨subcommand⟩]]",
+ },
+ .short_description = "Build your own ports.",
+ .long_description = std.fmt.comptimePrint(
+ \\Porteur is a tool for managing and creating custom ports from a Git repository.
+ \\Each port is part of a ports tree and results in a FreeBSD port usable by
+ \\`make` or `poudriere`. It uses a template to generate the files that describe
+ \\how to build the application from source code.
+ \\
+ \\To add a new port some basic information (e.g. name, category) needs to be
+ \\provided including a Git repository where the source code can be found. Porteur
+ \\always uses the configured branch of the Git repository and operates only on
+ \\that version of the source code. When a port is updated, porteur checks the
+ \\latest commit on this branch and updates the port information accordingly.
+ \\
+ \\Each port lives in a ports tree which can be used by the FreeBSD ports system.
+ \\When operating on ports there must always be a tree specified. If no tree name
+ \\is provided explicitly the default tree `{[default_tree]s}` will be used.
+ , .{ .default_tree = default_tree_name }),
+ .extra_sections = &.{
+ .{
+ .heading = "TEMPLATES",
+ .body =
+ \\Each FreeBSD port consists of a collection of files that automate the
+ \\compilation of an application. Since each port in the FreeBSD ports
+ \\tree requires its own set of files, maintaining them can be tedious,
+ \\especially when large parts of the content is identical.
+ \\
+ \\To make the definition of such files reusable, porteur uses templates,
+ \\located in the `⟨etc⟩/porteur/tmpl/` directory (see FILES). Each port
+ \\must reference a template and defines the variables used within it.
+ \\
+ \\A template is a collection of files that will used to render a port
+ \\usable by FreeBSD. Each file can use variables to render port specific
+ \\data, where a variable `v` can be referenced with the placeholder
+ \\`{{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).
+ ,
+ },
+ .{
+ .heading = "VARIABLES",
+ .body =
+ \\Variables are used to substitute port specific values in templates.
+ \\Defining variables is file-based and has the following syntax:
+ \\
+ \\ key = value
+ \\
+ \\Keys define the variable name that can later be referenced (e.g. in
+ \\a template file) and the assigned value is the string that replaces
+ \\this key. Leading and trailing whitespace in a value is trimmed.
+ \\Normally, a key/value pair is defined on a single line, but multi-line
+ \\values are also supported:
+ \\
+ \\ key = value line one
+ \\ = value line two
+ \\ =
+ \\ = value line three
+ \\
+ \\Each continuation line starts with an empty key and is rendered as a
+ \\separate line in the template. This also allows empty lines to be
+ \\part of the value. Again, leading and trailing whitespace of each line
+ \\is trimmed.
+ \\
+ \\To avoid redefining common information already known (e.g., port name,
+ \\port version), porteur predefines certain variables. All predefined
+ \\variable names start with a dot (`.`), so it is recommended not to use
+ \\custom variables starting with a dot.
+ \\
+ \\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
+ ,
+ },
+ .{
+ .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.
+ \\
+ \\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).
+ ,
+ },
+ .{
+ .heading = "FILES",
+ .body = std.fmt.comptimePrint(
+ \\Porteur uses two locations where data will be stored. The first location
+ \\is `{[prefix]s}/etc/` and contains all configuration files (ports and
+ \\templates). This location can be overwritten with the --etc flag. The
+ \\second location is `{[prefix]s}/porteur/` and contains a copy of the
+ \\sources and all generated port files that FreeBSD uses.
+ \\
+ \\The following directory structure is used:
+ \\
+ \\ {[prefix]s}/etc/porteur/porteur.conf
+ \\ Configuration file for porteur.
+ \\
+ \\ {[prefix]s}/etc/porteur/porteur.conf.sample
+ \\ Sample configuration file for porteur.
+ \\
+ \\ {[prefix]s}/etc/porteur/template.conf.sample
+ \\ Sample configuration file for a template.
+ \\
+ \\ {[prefix]s}/etc/porteur/ports/<tree>/
+ \\ Configuration for each port.
+ \\
+ \\ {[prefix]s}/etc/porteur/tmpl/⟨template⟩/
+ \\ Template definitions used by the ports.
+ \\
+ \\ {[prefix]s}/porteur/ports/⟨tree⟩/
+ \\ Generated ports tree used by FreeBSD.
+ \\
+ \\ {[prefix]s}/porteur/src/⟨tree⟩/
+ \\ Copies of the sources for each port.
+ \\
+ \\ {[prefix]s}/porteur/.wrk/
+ \\ Working directory for porteur.
+ , .{ .prefix = options.path_prefix }),
+ },
+ },
+ };
+
+ options: struct {
+ etc: cli.Option([]const u8, .{
+ .name = "etc",
+ .argument_name = "path",
+ .short_description = "The path to porteur's configuration directory.",
+ .long_description = std.fmt.comptimePrint(
+ \\The path of the directory porteur is storing its configuration.
+ \\Defaults to `{[prefix]s}/etc/`. See the FILES section for more
+ \\information.
+ , .{ .prefix = options.path_prefix }),
+ .default_value = options.path_prefix ++ "/etc/",
+ }),
+ },
+
+ pub const Subcommands = union(enum) {
+ help: Help_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 {
+ 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);
+ switch (subcmd) {
+ .help => |cmd| cmd.execute(env),
+ .tmpl => |cmd| cmd.execute(env),
+ .tree => |cmd| cmd.execute(env),
+ .port => |cmd| cmd.execute(env),
+ }
+ }
+};
+
+const Help_Command = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur help",
+ .synopsis = &.{
+ "porteur help [⟨command⟩ [⟨subcommand⟩]]",
+ },
+ .short_description = "Display help information.",
+ .long_description =
+ \\Without ⟨command⟩ and ⟨subcommand⟩ given, the synopsis of the porteur command
+ \\will be printed. This is equivalent to using `porteur` without any arguments.
+ \\
+ \\When ⟨command⟩ is specified without ⟨subcommand⟩, a detailed description of this
+ \\command is printed. An additional ⟨subcommand⟩ prints the detailed description of
+ \\the subcommand.
+ \\
+ \\The `porteur help ...` and `porteur ... --help` commands are identical.
+ \\
+ \\This help text can be displayed with `porteur help help` or `porteur help --help`.
+ ,
+ };
+
+ pub const Subcommands = Help_Subcommands(Main_Command.Subcommands);
+
+ fn execute(self: Help_Command, env: Env) void {
+ _ = self;
+ const subcmd = env.args.next_subcommand(Help_Command) orelse {
+ cli.print_usage(cli.stdout, Main_Command);
+ return;
+ };
+
+ switch (subcmd) {
+ inline else => |cmd| execute_subcmd(env.args, @TypeOf(cmd)),
+ }
+ }
+
+ fn execute_subcmd(args: *cli.Args, comptime Command: type) void {
+ if (@hasDecl(Command, "Subcommands")) {
+ if (args.next_subcommand(Command)) |subcmd| {
+ switch (subcmd) {
+ inline else => |cmd| {
+ execute_subcmd(args, @TypeOf(cmd));
+ return;
+ },
+ }
+ }
+ }
+ cli.print_help(Command);
+ }
+
+ fn Help_Subcommands(comptime Subcmds: type) type {
+ assert(@typeInfo(Subcmds) == .@"union");
+ const union_info = @typeInfo(Subcmds).@"union";
+
+ 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 = &.{},
+ } });
+ }
+};
+
+const Tmpl_Command = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur tmpl",
+ .synopsis = &.{
+ "porteur tmpl ⟨command⟩ [options] [⟨args⟩]",
+ },
+ .short_description = "Provide management of port templates.",
+ .long_description =
+ \\This command manages port templates. A port template defines the files
+ \\a port uses to automate the compilation of its source code. To use a
+ \\template with multiple ports variables can be used which are defined
+ \\for each port and later replaced when the port will be updated.
+ \\
+ \\A variable definition within a template file has the form `{{key}}`,
+ \\where the value of `key` can be defined for each port using this
+ \\template. If no such value is defined for a port, it will simply be
+ \\replaced by an empty string.
+ \\
+ \\For more information about defining and using templates, see the
+ \\TEMPLATES section in `porteur --help`.
+ ,
+ };
+
+ pub const Subcommands = union(enum) {
+ list: Tmpl_Ls,
+ conf: Tmpl_Conf,
+ };
+
+ pub fn execute(self: Tmpl_Command, env: Env) void {
+ _ = self;
+ const subcmd = env.args.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"),
+ }
+ }
+
+ const Tmpl_Ls = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur tmpl ls",
+ .synopsis = &.{
+ "porteur tmpl [options] ls",
+ },
+ .short_description = "List all port templates.",
+ .long_description =
+ \\This command lists the names of all port templates that are known by
+ \\porteur.
+ ,
+ };
+ options: struct {
+ long: cli.Option(bool, .{
+ .name = "long",
+ .short_name = 'l',
+ .short_description = "List templates in the long format.",
+ .long_description =
+ \\List the templates in a longer format that includes more information
+ \\about each template. In the long format a template is listed with its
+ \\path.
+ ,
+ .default_value = false,
+ }),
+ },
+
+ fn execute(self: Tmpl_Ls, env: 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);
+ defer iter.deinit();
+ if (long) {
+ while (try iter.next()) |template| {
+ cli.stdout.printf_line("{s} {s}", .{ template.name, template.path.name() }, print_opts);
+ }
+ } else {
+ while (try iter.next()) |template| {
+ cli.stdout.print_line(template.name, print_opts);
+ }
+ }
+ cli.stdout.flush();
+ }
+ };
+
+ const Tmpl_Conf = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur tmpl conf",
+ .synopsis = &.{
+ "porteur tmpl [options] conf ⟨template⟩",
+ "porteur tmpl [options] conf (--edit | -e) ⟨template⟩",
+ "porteur tmpl [options] conf --import=⟨path⟩ ⟨template⟩",
+ },
+ .short_description = "Manage the configuration for a template.",
+ .long_description =
+ \\Manage the configuration for a template. Without a flag specified it lists
+ \\the current configuration values for this template. To edit the configuration
+ \\either use the --edit or the --import option. With the --edit option set,
+ \\the existing configuration can be modified interactively using an editor.
+ \\The --import flag imports an external configuration from a file and overwrites
+ \\the existing one. The configuration file consists of a list of variables (see
+ \\the VARIABLES section in `porteur --help`). A documentation of possible
+ \\configuration values can be found in `⟨etc⟩/porteur/template.conf.sample`.
+ ,
+ };
+ options: struct {
+ edit: cli.Option(bool, .{
+ .name = "edit",
+ .short_name = 'e',
+ .short_description = "Edit the template configuration interactively.",
+ .long_description =
+ \\Edit the existing configuration for the template interactively. An
+ \\editor will be invoked where the existing configuration can be updated.
+ \\After exiting the editor the updates need to be confirmed. This
+ \\flag is mutual exclusive with --import.
+ ,
+ }),
+ import: cli.Option([]const u8, .{
+ .name = "import",
+ .argument_name = "path",
+ .short_description = "Import new configuration from the given file.",
+ .long_description =
+ \\Import the new configuration for the template from the specified file.
+ \\This file must contain valid variable definitions and will overwrite
+ \\the existing configuration for the template. This flag is mutual
+ \\exclusive with --edit.
+ ,
+ }),
+ },
+
+ fn execute(self: Tmpl_Conf, env: Env) !void {
+ const template_name = env.args.next_positional() orelse {
+ cli.fatal_with_usage(@This(), "missing template name", .{});
+ };
+
+ const edit = self.options.edit.value;
+ const import = self.options.import.value;
+ if (edit != null and import != null) {
+ cli.fatal("can either edit or import configuration", .{});
+ }
+
+ if (edit) |_| {
+ try tmpl.edit_config(env, template_name);
+ } else if (import) |filename| {
+ try tmpl.import_config(env, template_name, .init(filename));
+ } else {
+ const template = try tmpl.must_get(env, template_name);
+ const config = try tmpl.read_config(env, template);
+ defer config.deinit();
+ var iter = config.raw.iterate();
+ while (iter.next()) |c| {
+ cli.stdout.write_line("{f}", .{c});
+ }
+ cli.stdout.flush();
+ }
+ }
+ };
+};
+
+const Tree_Command = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur tree",
+ .synopsis = &.{
+ "porteur tree ⟨command⟩ [options] [⟨args⟩]",
+ },
+ .short_description = "Provide management of ports trees.",
+ .long_description =
+ \\This command manages ports trees. A ports tree is a collection of your own
+ \\ports which can be used with the FreeBSD ports system. A port is a collection
+ \\of files that automate the compilation of an application from its source code.
+ ,
+ };
+
+ pub const Subcommands = union(enum) {
+ list: Tree_Ls,
+ rm: Tree_Rm,
+ };
+
+ pub fn execute(self: Tree_Command, env: Env) void {
+ _ = self;
+ const subcmd = env.args.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"),
+ }
+ }
+
+ const Tree_Ls = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur tree ls",
+ .synopsis = &.{
+ "porteur tree ls",
+ },
+ .short_description = "List all existing ports trees.",
+ .long_description =
+ \\This command lists the names of all ports trees that are initialized with
+ \\porteur. The name is used to specify a specific tree when dealing with
+ \\ports.
+ \\
+ \\It simply prints all tree names line by line. For more information about
+ \\a specific tree use the `porteur tree info` command.
+ ,
+ };
+
+ fn execute(self: Tree_Ls, env: Env) !void {
+ _ = self;
+ var iter = try tree.iterate(env);
+ defer iter.deinit();
+ while (try iter.next()) |t| {
+ cli.stdout.print_line(t.name, .{});
+ }
+ cli.stdout.flush();
+ }
+ };
+
+ const Tree_Rm = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur tree rm",
+ .synopsis = &.{
+ "porteur tree rm ⟨tree⟩",
+ },
+ .short_description = "Remove an existing ports tree.",
+ .long_description =
+ \\This command removes an existing ports tree and deletes all its ports.
+ \\It asks for confirmation before deleting a tree.
+ ,
+ };
+ options: struct {
+ force: cli.Option(bool, .{
+ .name = "force",
+ .short_name = 'f',
+ .short_description = "Do not prompt for confirmation.",
+ .long_description =
+ \\Attempt to remove a ports tree without prompting for confirmation.
+ \\If the tree does not exist, do not report an error.
+ ,
+ .default_value = false,
+ }),
+ },
+
+ fn execute(self: Tree_Rm, env: Env) !void {
+ const force = self.options.force.value_or_default();
+ const name = env.args.next_positional() orelse {
+ if (force) std.process.exit(0);
+ cli.fatal_with_usage(@This(), "missing tree name", .{});
+ };
+
+ const ports_tree = try tree.get(env, name) orelse {
+ if (force) return;
+ cli.fatal("tree not found", .{});
+ };
+
+ if (!force) {
+ const confirmed = cli.ask_confirmation("Remove tree {s}?", .{ports_tree.name}, .no);
+ if (!confirmed) return;
+ }
+
+ try port.remove_all(.{
+ .env = env,
+ .tree = ports_tree,
+ });
+ tree.remove(env, ports_tree) catch {};
+ }
+ };
+};
+
+const Port_Command = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur port",
+ .synopsis = &.{
+ "porteur port [--tree=⟨tree⟩ | -t ⟨tree⟩] ⟨command⟩ [options] [⟨args⟩]",
+ },
+ .short_description = "Provide management of ports in a ports tree.",
+ .long_description =
+ \\This command manages the ports of a ports tree. A port is a collection of
+ \\files that automate the compilation of an application from its source code.
+ \\It references a Git repository that contains the source code and a template
+ \\which is used to generate the port files from.
+ ,
+ };
+
+ options: struct {
+ tree: cli.Option([]const u8, .{
+ .name = "tree",
+ .short_name = 't',
+ .argument_name = "tree",
+ .short_description = "The name of the ports tree.",
+ .long_description = std.fmt.comptimePrint(
+ \\The name of the ports tree. If this option is not specified the default
+ \\tree `{[default_tree]s}` will be used.
+ , .{ .default_tree = default_tree_name }),
+ .default_value = default_tree_name,
+ }),
+ },
+
+ pub const Subcommands = union(enum) {
+ list: Port_Ls,
+ info: Port_Info,
+ add: Port_Add,
+ vars: Port_Vars,
+ update: Port_Update,
+ bump_version: Port_Bump_Version,
+ history: Port_History,
+ rollback: Port_Rollback,
+ rm: Port_Rm,
+ };
+
+ pub fn execute(self: Port_Command, env: Env) void {
+ const subcmd = env.args.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");
+ if (ports_tree == null) cli.fatal("ports tree not found: {s}", .{tree_name});
+
+ const ctx = port.Ctx{
+ .env = env,
+ .tree = ports_tree.?,
+ };
+
+ 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"),
+ }
+ }
+
+ const Port_Ls = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur port ls",
+ .synopsis = &.{
+ "porteur port [options] ls",
+ },
+ .short_description = "List all ports of a ports tree.",
+ .long_description =
+ \\This command lists all ports the ports tree contains. It simply prints
+ \\all port names along with the port versions line by line. For more
+ \\information about a specific port see the `porteur port info` command.
+ ,
+ };
+ options: struct {
+ long: cli.Option(bool, .{
+ .name = "long",
+ .short_name = 'l',
+ .short_description = "List ports in the long format.",
+ .long_description =
+ \\List the ports in a longer format that includes more information about
+ \\each port. In the long format a port is listed with its current version
+ \\and its category.
+ ,
+ .default_value = false,
+ }),
+ },
+
+ fn execute(self: Port_Ls, ctx: port.Ctx) !void {
+ const long = self.options.long.value_or_default();
+ const print_opts: cli.Print_Options = .{ .hanging_indent = 4 };
+ var iter = try port.iterate(ctx);
+ defer iter.deinit();
+ if (long) {
+ while (try iter.next()) |p| {
+ const version = try port.read_version(ctx, p);
+ cli.stdout.printf_line("{s}@{f} {s}", .{ p.name, version, p.category }, print_opts);
+ }
+ } else {
+ while (try iter.next()) |p| {
+ cli.stdout.print_line(p.name, print_opts);
+ }
+ }
+ cli.stdout.flush();
+ }
+ };
+
+ const Port_Info = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur port info",
+ .synopsis = &.{
+ "porteur port [options] info ⟨port⟩",
+ },
+ .short_description = "Show information about a port.",
+ .long_description =
+ \\This command prints information about the specified port of a ports
+ \\tree. Variables for this port are not listed here. To show them use
+ \\the `porteur port vars` command. If the port cannot be found in the
+ \\ports tree an error will be reported.
+ ,
+ };
+ fn execute(self: Port_Info, ctx: port.Ctx) !void {
+ _ = self;
+ const port_name = ctx.env.args.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);
+ const source = try port.read_source(ctx, p);
+ defer source.deinit();
+ const distdir = ctx.env.ports_dist_dir(ctx.tree.name, p.name);
+ cli.stdout.printf_line("Name: {s}", .{p.name}, .{});
+ cli.stdout.printf_line("Category: {s}", .{p.category}, .{});
+ cli.stdout.printf_line("Template: {s}", .{p.template}, .{});
+ cli.stdout.printf_line("Version: {f}", .{version}, .{});
+ cli.stdout.printf_line("Git Repo: {s}", .{source.repo.url}, .{});
+ cli.stdout.printf_line("Git Branch: {s}", .{source.repo.branch}, .{});
+ if (source.commit.hash.len > 0) {
+ cli.stdout.printf_line("Commit Hash: {s}", .{source.commit.hash}, .{});
+ cli.stdout.printf_line("Commit Time: {f}", .{Time.from_timestamp(source.commit.timestamp)}, .{});
+ } else {
+ cli.stdout.printf_line("Commit Hash: -", .{}, .{});
+ cli.stdout.printf_line("Commit Time: -", .{}, .{});
+ }
+ cli.stdout.printf_line("Distdir: {s}", .{distdir.name()}, .{});
+ cli.stdout.flush();
+ }
+ };
+
+ const Port_Add = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur port add",
+ .synopsis = &.{
+ "porteur port [options] add",
+ },
+ .short_description = "Add a new port to the ports tree.",
+ .long_description =
+ \\This command adds a new port to the ports tree. A port needs to reference a
+ \\Git repository that holds the source code for distribution and compilation.
+ \\Porteur creates a local clone of this repository.
+ \\
+ \\When adding a new port there will be no variables defined and no FreeBSD
+ \\ports tree files generated. To add variables for the new port, use the
+ \\`porteur port vars` command. If everything is set up correctly, execute
+ \\a `porteur port update` to initially render the port.
+ \\
+ \\This command is interactive.
+ ,
+ };
+
+ 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()});
+ }
+
+ const namebuf = try ctx.env.allocator.alloc(u8, 64);
+ defer ctx.env.allocator.free(namebuf);
+ const name = while (true) {
+ const input = cli.ask("Name: ", .{}, namebuf) orelse {
+ cli.stderr.print_line("ERROR: The port name is required.", .{});
+ cli.stderr.flush();
+ continue;
+ };
+ if (std.mem.indexOfAny(u8, input, "/\\\t ")) |_| {
+ cli.stderr.print_line("ERROR: The port name must not contain slash, backslash, or space.", .{});
+ cli.stderr.flush();
+ continue;
+ }
+ break input;
+ };
+
+ var catbuf: [128]u8 = undefined;
+ var default_cat_buf: [128]u8 = undefined;
+ const default_cat = if (ctx.env.default_category) |cat|
+ try std.fmt.bufPrint(&default_cat_buf, " [{s}]", .{cat})
+ else
+ "";
+ const category = while (true) {
+ const input = cli.ask("Category{s}: ", .{default_cat}, &catbuf) orelse default_cat: {
+ if (ctx.env.default_category) |c| break :default_cat c;
+ cli.stderr.print_line("ERROR: The port category is required.", .{});
+ cli.stderr.flush();
+ continue;
+ };
+ if (std.mem.indexOfAny(u8, input, "/\\\t ")) |_| {
+ cli.stderr.print_line("ERROR: The port category must not contain slash, backslash, or space.", .{});
+ cli.stderr.flush();
+ continue;
+ }
+ break input;
+ };
+
+ var verbuf: [64]u8 = undefined;
+ const version = while (true) {
+ const input = cli.ask("Initial Version [0.1.0]: ", .{}, &verbuf) orelse "0.1.0";
+ const ver = port.Version.parse_semver(input) catch {
+ cli.stderr.print_line("ERROR: The port version must be in the format `⟨major⟩[[.⟨minor⟩[.⟨patch⟩]`.", .{});
+ cli.stderr.flush();
+ continue;
+ };
+ break ver;
+ };
+
+ var repourlbuf: [std.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.", .{});
+ cli.stderr.flush();
+ continue;
+ };
+ break input;
+ };
+
+ var repobranchbuf: [64]u8 = undefined;
+ const repo_branch = while (true) {
+ const default_branch = ctx.env.default_repo_branch;
+ const input = cli.ask("Git Branch [{s}]: ", .{default_branch}, &repobranchbuf) orelse default_branch;
+ if (!git.is_valid_branch_name(input)) {
+ cli.stderr.print_line("ERROR: Invalid Git branch name.", .{});
+ cli.stderr.flush();
+ continue;
+ }
+ break input;
+ };
+
+ var templatebuf: [64]u8 = undefined;
+ const template = while (true) {
+ const input = cli.ask("Template: ", .{}, &templatebuf) orelse {
+ cli.stderr.print_line("ERROR: The template is required.", .{});
+ cli.stderr.flush();
+ continue;
+ };
+ if (!try tmpl.exists(ctx.env, input)) {
+ cli.stderr.print_line("ERROR: Template does not exist.", .{});
+ cli.stderr.flush();
+ continue;
+ }
+ break input;
+ };
+
+ cli.stderr.print_line("", .{});
+ cli.stderr.printf_line("Name: {s}", .{name}, .{});
+ cli.stderr.printf_line("Category: {s}", .{category}, .{});
+ cli.stderr.printf_line("Initial Version: {f}", .{version}, .{});
+ cli.stderr.printf_line("Git Repository: {s}", .{repo_url}, .{});
+ cli.stderr.printf_line("Git Branch: {s}", .{repo_branch}, .{});
+ cli.stderr.printf_line("Template: {s}", .{template}, .{});
+ cli.stderr.flush();
+
+ const ok = cli.ask_confirmation("OK?", .{}, .yes);
+ if (ok) {
+ const new_port: port.Info = .init(ctx.tree, name, category, template);
+ try port.create(ctx, new_port, .{
+ .repo_url = repo_url,
+ .repo_branch = repo_branch,
+ .initial_version = version,
+ });
+
+ cli.stderr.print_line("", .{});
+ cli.stderr.printf_line("port created: {s}", .{new_port.name}, .{});
+ cli.stderr.print_line("", .{});
+ cli.stderr.print_line("Next steps:", .{});
+ cli.stderr.print_line("(1) use `porteur port vars` to define variables", .{ .margin = 2 });
+ cli.stderr.print_line("(2) use `porteur port update` to initially render the port", .{ .margin = 2 });
+ cli.stderr.flush();
+ }
+ }
+ };
+
+ const Port_Vars = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur port vars",
+ .synopsis = &.{
+ "porteur port [options] vars ⟨port⟩",
+ "porteur port [options] vars (--edit | -e) ⟨port⟩",
+ "porteur port [options] vars --import=⟨path⟩ ⟨port⟩",
+ },
+ .short_description = "Manage variables for a port.",
+ .long_description =
+ \\Manage the variables for a port. Without a flag specified it lists all
+ \\existing variables for this port. To edit the variables either use the
+ \\--edit or the --import option. With the --edit option set, all existing
+ \\variables can be modified interactively using an editor. The --import
+ \\flag imports the variables from a file and overwrites all existing ones.
+ \\For more information on how to format the variables see the VARIABLES
+ \\section in `porteur --help`.
+ ,
+ };
+ options: struct {
+ edit: cli.Option(bool, .{
+ .name = "edit",
+ .short_name = 'e',
+ .short_description = "Edit the port variables interactively.",
+ .long_description =
+ \\Edit the existing variables for the port interactively. An editor
+ \\will be invoked where the existing variables can be updated. After
+ \\exiting the editor the updates need to be confirmed. This flag is
+ \\mutual exclusive with --import.
+ ,
+ }),
+ import: cli.Option([]const u8, .{
+ .name = "import",
+ .argument_name = "path",
+ .short_description = "Import new port variables from the given file.",
+ .long_description =
+ \\Import the new variables for the port from the specified file. This
+ \\file must contain valid variable definitions. It will overwrite all
+ \\existing port variables. This flag is mutual exclusive with --edit.
+ ,
+ }),
+ },
+
+ fn execute(self: Port_Vars, ctx: port.Ctx) !void {
+ const port_name = ctx.env.args.next_positional() orelse {
+ cli.fatal_with_usage(@This(), "missing port name", .{});
+ };
+
+ const edit = self.options.edit.value;
+ const import = self.options.import.value;
+ if (edit != null and import != null) {
+ cli.fatal("can either edit or import variables", .{});
+ }
+
+ if (edit) |_| {
+ try port.edit_vars(ctx, port_name);
+ } else if (import) |filename| {
+ try port.import_vars(ctx, port_name, .init(filename));
+ } else {
+ const p = try port.must_get(ctx, port_name);
+ defer p.deinit();
+ const vars = try port.read_vars(ctx, p);
+ defer vars.deinit();
+ var iter = vars.iterate();
+ while (iter.next()) |v| {
+ cli.stdout.write_line("{f}", .{v});
+ }
+ cli.stdout.flush();
+ }
+ }
+ };
+
+ const Port_Update = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur port update",
+ .synopsis = &.{
+ "porteur port [options] update [⟨port⟩ ...]",
+ },
+ .short_description = "Update ports in a ports tree.",
+ .long_description =
+ \\This command updates one or more ports in the ports tree. For each port, it
+ \\checks if there is a new commit for the configured Git branch. If so, the
+ \\files in the ports tree will be generated from the template and the new
+ \\distribution files will be created. A Git commit is considered new if it has
+ \\a newer date than the last commit from which the port files were generated
+ \\or if the date is the same, but the commit hash is different.
+ \\
+ \\If no port name is provided it updates all existing ports in the ports tree.
+ ,
+ };
+ options: struct {
+ force: cli.Option(bool, .{
+ .name = "force",
+ .short_name = 'f',
+ .short_description = "Force the port update.",
+ .long_description =
+ \\Attempt to do the port update although the current commit for the branch
+ \\is not newer than the last one. This option forces a rerender for the
+ \\port based on the branch's latest commit.
+ ,
+ .default_value = false,
+ }),
+ },
+
+ fn execute(self: Port_Update, ctx: port.Ctx) !void {
+ const opts: port.Update_Options = .{
+ .force = self.options.force.value_or_default(),
+ };
+ var count: usize = 0;
+ while (ctx.env.args.next_positional()) |port_name| {
+ count += 1;
+ try update_single_port(ctx, port_name, opts);
+ }
+ if (count == 0) {
+ var iter = try port.iterate(ctx);
+ defer iter.deinit();
+ while (try iter.next()) |p| {
+ try update_single_port(ctx, p.name, opts);
+ }
+ }
+ }
+
+ fn update_single_port(ctx: port.Ctx, port_name: []const u8, opts: port.Update_Options) !void {
+ if (try port.update_version(ctx, port_name, opts)) |new_version| {
+ cli.stderr.printf_line("updated {s} to {f}", .{ port_name, new_version }, .{});
+ } else {
+ cli.stderr.printf_line("already up to date: {s}", .{port_name}, .{});
+ }
+ cli.stderr.flush();
+ }
+ };
+
+ const Port_Bump_Version = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur port bump-version",
+ .synopsis = &.{
+ "porteur port [options] bump-version [--major | --minor | --patch] ⟨port⟩",
+ },
+ .short_description = "Increment the version of a port.",
+ .long_description =
+ \\This command increments the version of a port without updating its sources.
+ \\It is possible to increment the major version, the minor version, or the
+ \\patch version. By default, the patch version will be incremented.
+ ,
+ };
+ options: struct {
+ major: cli.Option(bool, .{
+ .name = "major",
+ .short_description = "Increment the major version.",
+ .long_description =
+ \\Increment the major version of the port by one and reset the minor and patch
+ \\version to zero. This flag in mutual exclusive with --minor and --patch.
+ ,
+ .default_value = false,
+ }),
+ minor: cli.Option(bool, .{
+ .name = "minor",
+ .short_description = "Increment the minor version.",
+ .long_description =
+ \\Increment the minor version of the port by one and reset the patch version
+ \\to zero. The major version stays untouched. This flag in mutual exclusive
+ \\with --major and --patch.
+ ,
+ .default_value = false,
+ }),
+ patch: cli.Option(bool, .{
+ .name = "patch",
+ .short_description = "Increment the patch version.",
+ .long_description =
+ \\Increment the patch version of the port by one. The major and minior version
+ \\stays untouched. This flag in mutual exclusive with --major and --minor.
+ ,
+ .default_value = false,
+ }),
+ },
+
+ fn execute(self: Port_Bump_Version, ctx: port.Ctx) !void {
+ const bump_major: u8 = @intFromBool(self.options.major.value_or_default());
+ const bump_minor: u8 = @intFromBool(self.options.minor.value_or_default());
+ const bump_patch: u8 = @intFromBool(self.options.patch.value_or_default());
+ switch (bump_major + bump_minor + bump_patch) {
+ 0, 1 => {},
+ else => cli.fatal("can either bump major, minor, or patch version", .{}),
+ }
+
+ const port_name = ctx.env.args.next_positional() orelse {
+ cli.fatal_with_usage(@This(), "missing port name", .{});
+ };
+
+ const new_version = try port.bump_version(ctx, port_name, .{
+ .which = if (bump_major != 0) .major else if (bump_minor != 0) .minor else .patch,
+ });
+
+ cli.stderr.printf_line("port version bumped to {f}", .{new_version}, .{});
+ cli.stderr.flush();
+ }
+ };
+
+ const Port_History = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur port history",
+ .synopsis = &.{
+ "porteur port [options] history ⟨port⟩",
+ },
+ .short_description = "List all existing distfile version of a port.",
+ .long_description =
+ \\This command lists all existing distfile versions for the given port.
+ \\The number of distfiles kept can be configured in `porteur.conf`.
+ ,
+ };
+ options: struct {
+ long: cli.Option(bool, .{
+ .name = "long",
+ .short_name = 'l',
+ .short_description = "List distfile version in the long format.",
+ .long_description =
+ \\List the distfile version in a longer format that also includes the
+ \\distfile locations.
+ ,
+ .default_value = false,
+ }),
+ },
+
+ 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 {
+ cli.fatal_with_usage(@This(), "missing port name", .{});
+ };
+ var distfiles = try port.list_distfiles(ctx, port_name);
+ defer distfiles.deinit(ctx.env.allocator);
+
+ if (long) {
+ const version_strings = try ctx.env.allocator.alloc(port.Version_String, distfiles.items.len);
+ defer ctx.env.allocator.free(version_strings);
+ var max_version_len: usize = 0;
+ for (distfiles.items, 0..) |distfile, i| {
+ version_strings[i] = distfile.version.to_string();
+ max_version_len = @max(max_version_len, version_strings[i].len);
+ }
+
+ const spaces = [_]u8{' '} ** 64;
+ for (distfiles.items, version_strings) |distfile, version| {
+ const fill = spaces[0 .. max_version_len - version.len];
+ cli.stdout.write_line("{s}{s} {s}", .{ version.slice(), fill, distfile.path.name() });
+ }
+ } else {
+ for (distfiles.items) |d| {
+ cli.stdout.write_line("{f}", .{d.version});
+ }
+ }
+ cli.stdout.flush();
+ }
+ };
+
+ const Port_Rollback = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur port rollback",
+ .synopsis = &.{
+ "porteur port [options] rollback ⟨port⟩ ⟨version⟩",
+ },
+ .short_description = "Roll back a port to a specific version.",
+ .long_description =
+ \\This command rolls back an existing ports to the given version.
+ \\The distfile of this version must still be in the port's history.
+ \\It asks for confirmation before rolling back a port.
+ ,
+ };
+
+ fn execute(self: Port_Rollback, ctx: port.Ctx) !void {
+ _ = self;
+ const port_name = ctx.env.args.next_positional() orelse {
+ cli.fatal_with_usage(@This(), "missing port name", .{});
+ };
+ const version_str = ctx.env.args.next_positional() orelse {
+ cli.fatal_with_usage(@This(), "missing version", .{});
+ };
+ const version = port.Version.parse(version_str) catch {
+ cli.fatal("incorrect version: {s}", .{version_str});
+ };
+
+ try port.rollback(ctx, port_name, version);
+ }
+ };
+
+ const Port_Rm = struct {
+ pub const help = cli.Command_Help{
+ .name = "porteur port rm",
+ .synopsis = &.{
+ "porteur port [options] rm ⟨port⟩ [⟨port⟩ ...]",
+ },
+ .short_description = "Remove ports from a ports tree.",
+ .long_description =
+ \\This command removes one or more existing ports from the ports tree.
+ \\It asks for confirmation before deleting a port.
+ ,
+ };
+ options: struct {
+ force: cli.Option(bool, .{
+ .name = "force",
+ .short_name = 'f',
+ .short_description = "Do not prompt for confirmation.",
+ .long_description =
+ \\Attempt to remove ports without prompting for confirmation. If the port
+ \\does not exist, do not report an error.
+ ,
+ .default_value = false,
+ }),
+ },
+
+ fn execute(self: Port_Rm, ctx: port.Ctx) !void {
+ const force = self.options.force.value_or_default();
+
+ var count: usize = 0;
+ var exit_code: u8 = 0;
+ while (ctx.env.args.next_positional()) |port_name| {
+ count += 1;
+ if (!force) {
+ const confirmed = cli.ask_confirmation("Remove port {s}?", .{port_name}, .no);
+ if (!confirmed) continue;
+ }
+ const deleted = try port.remove(ctx, port_name);
+ if (!deleted and !force) {
+ cli.stderr.printf_line("port not found: {s}", .{port_name}, .{});
+ cli.stderr.flush();
+ exit_code = 1;
+ }
+ }
+ if (count == 0 and !force) {
+ cli.fatal_with_usage(Port_Rm, "missing port name", .{});
+ } else if (exit_code != 0) {
+ std.process.exit(exit_code);
+ }
+ }
+ };
+};
+
+fn fatal_err(err: anyerror, comptime errmsg: []const u8) noreturn {
+ cli.fatal(errmsg ++ ": {s}", .{@errorName(err)});
+}
+
+test {
+ std.testing.refAllDecls(@This());
+}