diff options
| author | tsne <tsne.dev@outlook.com> | 2025-01-01 14:58:28 +0100 |
|---|---|---|
| committer | tsne <tsne.dev@outlook.com> | 2025-10-30 08:32:49 +0100 |
| commit | 44e5ad763794a438ecfd50c8b7f6ea760ea82da5 (patch) | |
| tree | 575a1fc72ceb1d7f052cf582abc1e038e27f69a3 /src/main.zig | |
| download | porteur-44e5ad763794a438ecfd50c8b7f6ea760ea82da5.tar.gz | |
Diffstat (limited to 'src/main.zig')
| -rw-r--r-- | src/main.zig | 1195 |
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()); +} |