const std = @import("std"); const builtin = @import("builtin"); const cli = @import("cli.zig"); const colors = @import("colors.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; } const icons = .{ .check = "✅", .cross = "❌", }; /// Print a success confirmation. pub fn printSuccess() void { std.debug.print(" " ++ icons.check ++ "\n", .{}); } /// Print a failure confirmation. pub fn printFailure() void { std.debug.print(" " ++ icons.cross ++ "\n", .{}); } const PrintContext = enum { success, failure }; /// Print some output in with a given context to stderr. pub fn print(comptime context: PrintContext, comptime message: []const u8, args: anytype) !void { const writer = std.io.getStdErr().writer(); switch (context) { .success => try writer.print( std.fmt.comptimePrint("{s} {s}\n", .{ icons.check, colors.green(message) }), args, ), .failure => try writer.print( std.fmt.comptimePrint("{s} {s}\n", .{ icons.cross, colors.red(message) }), args, ), } } /// Detects a Jetzig project directory either in the current directory or one of its parent /// directories. 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 in Jetzig project directory and verifies successful exit /// code. pub fn runCommand(allocator: std.mem.Allocator, argv: []const []const u8) !void { var dir = try detectJetzigProjectDir(); defer dir.close(); try runCommandInDir(allocator, argv, .{ .dir = dir }); } const Dir = union(enum) { path: []const u8, dir: std.fs.Dir, }; /// Runs a command as a child process in the given directory and verifies successful exit code. pub fn runCommandInDir(allocator: std.mem.Allocator, argv: []const []const u8, dir: Dir) !void { const cwd_path = switch (dir) { .path => |capture| capture, .dir => |capture| try capture.realpathAlloc(allocator, "."), }; defer if (dir == .dir) allocator.free(cwd_path); const result = std.process.Child.run(.{ .allocator = allocator, .argv = argv, .cwd = cwd_path, }) catch |err| { switch (err) { error.FileNotFound => { printFailure(); const cmd_str = try std.mem.join(allocator, " ", argv); defer allocator.free(cmd_str); std.debug.print( \\Error: Could not execute command - executable '{s}' not found \\Command: {s} \\Working directory: {s} \\ , .{ argv[0], cmd_str, cwd_path }); return error.JetzigCommandError; }, else => return err, } }; defer allocator.free(result.stdout); defer allocator.free(result.stderr); if (result.term.Exited != 0) { printFailure(); if (result.stdout.len > 0) { std.debug.print("\n[stdout]:\n{s}\n", .{result.stdout}); } if (result.stderr.len > 0) { std.debug.print("\n[stderr]:\n{s}\n", .{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), }; } pub fn unicodePrint(comptime fmt: []const u8, args: anytype) !void { if (builtin.os.tag == .windows) { // Windows-specific code const cp_out = try UTF8ConsoleOutput.init(); defer cp_out.deinit(); std.debug.print(comptime fmt, args); } else { // Non-Windows platforms just print normally std.debug.print(fmt, args); } } const UTF8ConsoleOutput = struct { original: c_uint, fn init() !UTF8ConsoleOutput { const original = std.os.windows.kernel32.GetConsoleOutputCP(); if (original == 0) { return error.FailedToGetConsoleOutputCP; } const result = std.os.windows.kernel32.SetConsoleOutputCP(65001); // UTF-8 code page if (result == 0) { return error.FailedToSetConsoleOutputCP; } return .{ .original = original }; } fn deinit(self: UTF8ConsoleOutput) void { _ = std.os.windows.kernel32.SetConsoleOutputCP(self.original); } };