Add update command to CLI tool

Automatically update to latest version of Jetzig with `jetzig update`.
This commit is contained in:
Bob Farrell 2024-03-09 19:45:24 +00:00
parent 2a25084de6
commit bec5f9c905
4 changed files with 165 additions and 81 deletions

View File

@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const args = @import("args"); const args = @import("args");
const init = @import("commands/init.zig"); const init = @import("commands/init.zig");
const update = @import("commands/update.zig");
const generate = @import("commands/generate.zig"); const generate = @import("commands/generate.zig");
const Options = struct { const Options = struct {
@ -14,6 +15,7 @@ const Options = struct {
.usage_summary = "[COMMAND]", .usage_summary = "[COMMAND]",
.option_docs = .{ .option_docs = .{
.init = "Initialize a new project", .init = "Initialize a new project",
.update = "Update current project to latest version of Jetzig",
.generate = "Generate scaffolding", .generate = "Generate scaffolding",
.help = "Print help and exit", .help = "Print help and exit",
}, },
@ -22,6 +24,7 @@ const Options = struct {
const Verb = union(enum) { const Verb = union(enum) {
init: init.Options, init: init.Options,
update: update.Options,
generate: generate.Options, generate: generate.Options,
g: generate.Options, g: generate.Options,
}; };
@ -52,6 +55,7 @@ pub fn main() !void {
\\Commands: \\Commands:
\\ \\
\\ init Initialize a new project. \\ init Initialize a new project.
\\ update Update current project to latest version of Jetzig.
\\ generate Generate scaffolding. \\ generate Generate scaffolding.
\\ \\
\\ Pass --help to any command for more information, e.g. `jetzig init --help` \\ Pass --help to any command for more information, e.g. `jetzig init --help`
@ -77,6 +81,13 @@ fn run(allocator: std.mem.Allocator, options: args.ParseArgsResult(Options, Verb
options.positionals, options.positionals,
.{ .help = options.options.help }, .{ .help = options.options.help },
), ),
.update => |opts| update.run(
allocator,
opts,
writer,
options.positionals,
.{ .help = options.options.help },
),
}; };
} }
} }

View File

@ -47,7 +47,7 @@ pub fn run(
install_path = arg; install_path = arg;
} }
const github_url = try githubUrl(allocator); const github_url = try util.githubUrl(allocator);
defer allocator.free(github_url); defer allocator.free(github_url);
if (other_options.help) { if (other_options.help) {
@ -87,10 +87,10 @@ pub fn run(
install_dir = try std.fs.cwd().makeOpenPath(input_install_path, .{}); install_dir = try std.fs.cwd().makeOpenPath(input_install_path, .{});
} }
const real_path = try install_dir.realpathAlloc(allocator, "."); const realpath = try install_dir.realpathAlloc(allocator, ".");
defer allocator.free(real_path); defer allocator.free(realpath);
const output = try std.fmt.allocPrint(allocator, "Creating new project in {s}\n\n", .{real_path}); const output = try std.fmt.allocPrint(allocator, "Creating new project in {s}\n\n", .{realpath});
defer allocator.free(output); defer allocator.free(output);
try writer.writeAll(output); try writer.writeAll(output);
@ -184,7 +184,7 @@ pub fn run(
null, null,
); );
try runCommand(allocator, real_path, &[_][]const u8{ try util.runCommand(allocator, realpath, &[_][]const u8{
"zig", "zig",
"fetch", "fetch",
"--save", "--save",
@ -208,38 +208,7 @@ pub fn run(
\\And then browse to http://localhost:8080/ \\And then browse to http://localhost:8080/
\\ \\
\\ \\
, .{real_path}); , .{realpath});
}
fn runCommand(allocator: std.mem.Allocator, install_path: []const u8, argv: []const []const u8) !void {
const result = try std.process.Child.run(.{ .allocator = allocator, .argv = argv, .cwd = install_path });
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
const command = try std.mem.join(allocator, " ", argv);
defer allocator.free(command);
std.debug.print("[exec] {s}", .{command});
if (result.term.Exited != 0) {
util.printFailure();
std.debug.print(
\\
\\Error running command: {s}
\\
\\[stdout]:
\\
\\{s}
\\
\\[stderr]:
\\
\\{s}
\\
, .{ command, result.stdout, result.stderr });
return error.JetzigCommandError;
} else {
util.printSuccess();
}
} }
const Replace = struct { const Replace = struct {
@ -308,47 +277,6 @@ fn writeSourceFile(install_dir: std.fs.Dir, path: []const u8, content: []const u
} }
} }
// Generate a full GitHub URL for passing to `zig fetch`.
fn githubUrl(allocator: std.mem.Allocator) ![]const u8 {
var client = std.http.Client{ .allocator = allocator };
defer client.deinit();
const url = "https://api.github.com/repos/jetzig-framework/jetzig/branches/main";
const extra_headers = &[_]std.http.Header{.{ .name = "X-GitHub-Api-Version", .value = "2022-11-28" }};
var response_storage = std.ArrayList(u8).init(allocator);
defer response_storage.deinit();
const fetch_result = try client.fetch(.{
.location = .{ .url = url },
.extra_headers = extra_headers,
.response_storage = .{ .dynamic = &response_storage },
});
if (fetch_result.status != .ok) {
std.debug.print("Error fetching from GitHub: {s}\n", .{url});
return error.JetzigCommandError;
}
const parsed_response = try std.json.parseFromSlice(
struct { commit: struct { sha: []const u8 } },
allocator,
response_storage.items,
.{ .ignore_unknown_fields = true },
);
defer parsed_response.deinit();
return try std.mem.concat(
allocator,
u8,
&[_][]const u8{
"https://github.com/jetzig-framework/jetzig/archive/",
parsed_response.value.commit.sha,
".tar.gz",
},
);
}
// Prompt a user for input and return the result. Accepts an optional default value. // Prompt a user for input and return the result. Accepts an optional default value.
fn promptInput( fn promptInput(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
@ -384,19 +312,19 @@ fn promptInput(
// Initialize a new Git repository when setting up a new project (optional). // Initialize a new Git repository when setting up a new project (optional).
fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void { fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
try runCommand(allocator, install_dir, &[_][]const u8{ try util.runCommand(allocator, install_dir, &[_][]const u8{
"git", "git",
"init", "init",
".", ".",
}); });
try runCommand(allocator, install_dir, &[_][]const u8{ try util.runCommand(allocator, install_dir, &[_][]const u8{
"git", "git",
"add", "add",
".", ".",
}); });
try runCommand(allocator, install_dir, &[_][]const u8{ try util.runCommand(allocator, install_dir, &[_][]const u8{
"git", "git",
"commit", "commit",
"-m", "-m",

72
cli/commands/update.zig Normal file
View File

@ -0,0 +1,72 @@
const std = @import("std");
const args = @import("args");
const util = @import("../util.zig");
/// Command line options for the `update` command.
pub const Options = struct {
pub const meta = .{
.usage_summary = "[NAME=jetzig]",
.full_text =
\\Updates the current project to the latest version of Jetzig.
\\
\\Optionally pass a positional argument to save the dependency to `build.zig.zon` with an
\\alternative name.
\\
\\Equivalent to running `zig fetch --save=jetzig https://github.com/jetzig-framework/jetzig/archive/<latest-commit>.tar.gz`
\\
\\Example:
\\
\\ jetzig update
\\ jetzig update web
,
.option_docs = .{
.path = "Set the output path relative to the current directory (default: current directory)",
},
};
};
/// Run the `jetzig update` command.
pub fn run(
allocator: std.mem.Allocator,
options: Options,
writer: anytype,
positionals: [][]const u8,
other_options: struct { help: bool },
) !void {
_ = options;
if (other_options.help) {
try args.printHelp(Options, "jetzig update", writer);
return;
}
if (positionals.len > 1) {
std.debug.print("Expected at most 1 positional argument, found {}\n", .{positionals.len});
return error.JetzigCommandError;
}
const name = if (positionals.len > 0) positionals[0] else "jetzig";
const github_url = try util.githubUrl(allocator);
defer allocator.free(github_url);
const save_arg = try std.mem.concat(allocator, u8, &[_][]const u8{ "--save=", name });
defer allocator.free(save_arg);
var cwd = try util.detectJetzigProjectDir();
defer cwd.close();
const realpath = try std.fs.realpathAlloc(allocator, ".");
defer allocator.free(realpath);
try util.runCommand(allocator, realpath, &[_][]const u8{
"zig",
"fetch",
save_arg,
github_url,
});
std.debug.print(
\\Update complete.
\\
, .{});
}

View File

@ -94,3 +94,76 @@ pub fn isCamelCase(input: []const u8) bool {
return true; return true;
} }
/// Runs a command as a child process and verifies successful exit code.
pub fn runCommand(allocator: std.mem.Allocator, install_path: []const u8, argv: []const []const u8) !void {
const result = try std.process.Child.run(.{ .allocator = allocator, .argv = argv, .cwd = install_path });
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
const command = try std.mem.join(allocator, " ", argv);
defer allocator.free(command);
std.debug.print("[exec] {s}", .{command});
if (result.term.Exited != 0) {
printFailure();
std.debug.print(
\\
\\Error running command: {s}
\\
\\[stdout]:
\\
\\{s}
\\
\\[stderr]:
\\
\\{s}
\\
, .{ command, result.stdout, result.stderr });
return error.JetzigCommandError;
} else {
printSuccess();
}
}
/// Generate a full GitHub URL for passing to `zig fetch`.
pub fn githubUrl(allocator: std.mem.Allocator) ![]const u8 {
var client = std.http.Client{ .allocator = allocator };
defer client.deinit();
const url = "https://api.github.com/repos/jetzig-framework/jetzig/branches/main";
const extra_headers = &[_]std.http.Header{.{ .name = "X-GitHub-Api-Version", .value = "2022-11-28" }};
var response_storage = std.ArrayList(u8).init(allocator);
defer response_storage.deinit();
const fetch_result = try client.fetch(.{
.location = .{ .url = url },
.extra_headers = extra_headers,
.response_storage = .{ .dynamic = &response_storage },
});
if (fetch_result.status != .ok) {
std.debug.print("Error fetching from GitHub: {s}\n", .{url});
return error.JetzigCommandError;
}
const parsed_response = try std.json.parseFromSlice(
struct { commit: struct { sha: []const u8 } },
allocator,
response_storage.items,
.{ .ignore_unknown_fields = true },
);
defer parsed_response.deinit();
return try std.mem.concat(
allocator,
u8,
&[_][]const u8{
"https://github.com/jetzig-framework/jetzig/archive/",
parsed_response.value.commit.sha,
".tar.gz",
},
);
}