jetzig/cli/util.zig
Bob Farrell dc11412587 WIP
2024-11-09 17:13:32 +00:00

246 lines
7.8 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const cli = @import("cli.zig");
/// Decode a base64 string, used for parsing out build artifacts generated by the CLI program's
/// build.zig which are stored in the executable as a module.
pub fn base64Decode(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
const decoder = std.base64.Base64Decoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,
);
const size = try decoder.calcSizeForSlice(input);
const ptr = try allocator.alloc(u8, size);
try decoder.decode(ptr, input);
return ptr;
}
/// Print a success confirmation.
pub fn printSuccess() void {
std.debug.print("\n", .{});
}
/// Print a failure confirmation.
pub fn printFailure() void {
std.debug.print("\n", .{});
}
/// Verifies that cwd is the root of a Jetzig project
pub fn detectJetzigProjectDir() !std.fs.Dir {
var dir = try std.fs.cwd().openDir(".", .{});
const max_parent_dirs: usize = 100; // Prevent symlink loops or other weird stuff.
for (0..max_parent_dirs) |_| {
if (try isPath(dir, "build.zig", .file) and try isPath(dir, "src/app/views", .dir)) return dir;
dir = dir.openDir("..", .{}) catch |err| {
switch (err) {
error.FileNotFound, error.NotDir => {
std.debug.print(
"Encountered unexpected detecting Jetzig project directory: {s}\n",
.{@errorName(err)},
);
return error.JetzigCommandError;
},
else => return err,
}
};
continue;
}
std.debug.print(
\\Exceeded maximum parent directory depth.
\\Unable to detect Jetzig project directory.
\\
,
.{},
);
return error.JetzigCommandError;
}
fn isPath(dir: std.fs.Dir, sub_path: []const u8, path_type: enum { file, dir }) !bool {
switch (path_type) {
.file => {
_ = dir.statFile(sub_path) catch |err| {
switch (err) {
error.FileNotFound => return false,
else => return err,
}
};
return true;
},
.dir => {
var test_dir = dir.openDir(sub_path, .{}) catch |err| {
switch (err) {
error.FileNotFound, error.NotDir => return false,
else => return err,
}
};
test_dir.close();
return true;
},
}
}
// Strip leading and trailing whitespace from a u8 slice.
pub inline fn strip(input: []const u8) []const u8 {
return std.mem.trim(u8, input, &std.ascii.whitespace);
}
/// Attempts to confirm if a string input is in CamelCase. False if the first character is
/// not alphabetic lower-case or if the input contains underscores.
pub fn isCamelCase(input: []const u8) bool {
if (input.len == 0) return false;
if (!std.mem.containsAtLeast(u8, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1, &[_]u8{input[0]})) return false;
if (std.mem.containsAtLeast(u8, input, 1, "_")) return false;
return true;
}
pub fn execCommand(allocator: std.mem.Allocator, argv: []const []const u8) !void {
std.debug.print("[exec]", .{});
for (argv) |arg| std.debug.print(" {s}", .{arg});
std.debug.print("\n", .{});
if (std.process.can_execv) {
return std.process.execv(allocator, argv);
} else {
var dir = try detectJetzigProjectDir();
defer dir.close();
const path = try dir.realpathAlloc(allocator, ".");
defer allocator.free(path);
try runCommandStreaming(allocator, path, argv);
}
}
pub fn runCommandStreaming(allocator: std.mem.Allocator, install_path: []const u8, argv: []const []const u8) !void {
var child = std.process.Child.init(argv, allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Inherit;
child.stderr_behavior = .Inherit;
child.cwd = install_path;
try child.spawn();
_ = try child.wait();
}
/// 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",
},
);
}
/// Attempt to locate the main application executable in `zig-out/bin/`
pub fn locateExecutable(
allocator: std.mem.Allocator,
dir: std.fs.Dir,
options: struct { relative: bool = false },
) !?[]const u8 {
const file = dir.openFile(".jetzig", .{}) catch |err| {
switch (err) {
error.FileNotFound => return null,
else => return err,
}
};
const content = try file.readToEndAlloc(allocator, 1024);
defer allocator.free(content);
const exe_name = strip(content);
const suffix = if (builtin.os.tag == .windows) ".exe" else "";
const full_name = try std.mem.concat(allocator, u8, &[_][]const u8{ exe_name, suffix });
defer allocator.free(full_name);
// XXX: Will fail if user sets a custom install path.
var bin_dir = try dir.openDir("zig-out/bin", .{ .iterate = true });
defer bin_dir.close();
var walker = try bin_dir.walk(allocator);
defer walker.deinit();
while (try walker.next()) |entry| {
if (entry.kind == .file and std.mem.eql(u8, entry.path, full_name)) {
if (options.relative) {
return try std.fs.path.join(allocator, &[_][]const u8{ "zig-out", "bin", entry.path });
} else {
return try bin_dir.realpathAlloc(allocator, entry.path);
}
}
}
return null;
}
pub fn environmentBuildOption(environment: cli.Environment) []const u8 {
return switch (environment) {
inline else => |tag| "-Denvironment=" ++ @tagName(tag),
};
}