mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00

Default to not using LLVM for application compilation. This gives more than 2x performance improvement for compilation stage.
347 lines
11 KiB
Zig
347 lines
11 KiB
Zig
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,
|
|
};
|
|
|
|
pub const RunOptions = struct {
|
|
output: enum { stream, capture } = .capture,
|
|
wait: bool = true,
|
|
};
|
|
|
|
/// 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, options: RunOptions) !void {
|
|
const cwd_path = switch (dir) {
|
|
.path => |capture| capture,
|
|
.dir => |capture| try capture.realpathAlloc(allocator, "."),
|
|
};
|
|
defer if (dir == .dir) allocator.free(cwd_path);
|
|
|
|
const output_behaviour: std.process.Child.StdIo = switch (options.output) {
|
|
.stream => .Inherit,
|
|
.capture => .Pipe,
|
|
};
|
|
|
|
var child = std.process.Child.init(argv, allocator);
|
|
child.stdin_behavior = .Ignore;
|
|
child.stdout_behavior = output_behaviour;
|
|
child.stderr_behavior = output_behaviour;
|
|
if (options.output == .stream) {
|
|
child.stdout = std.io.getStdOut();
|
|
child.stderr = std.io.getStdErr();
|
|
}
|
|
child.cwd = cwd_path;
|
|
|
|
var stdout = try std.ArrayListUnmanaged(u8).initCapacity(allocator, 0);
|
|
var stderr = try std.ArrayListUnmanaged(u8).initCapacity(allocator, 0);
|
|
errdefer {
|
|
stdout.deinit(allocator);
|
|
stderr.deinit(allocator);
|
|
}
|
|
|
|
try child.spawn();
|
|
switch (options.output) {
|
|
.capture => try child.collectOutput(allocator, &stdout, &stderr, 50 * 1024),
|
|
.stream => {},
|
|
}
|
|
|
|
if (!options.wait) return;
|
|
|
|
const result = std.process.Child.RunResult{
|
|
.term = try child.wait(),
|
|
.stdout = try stdout.toOwnedSlice(allocator),
|
|
.stderr = try stderr.toOwnedSlice(allocator),
|
|
};
|
|
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);
|
|
}
|
|
};
|