diff --git a/cli/cli.zig b/cli/cli.zig index ca62b78..8171e78 100644 --- a/cli/cli.zig +++ b/cli/cli.zig @@ -4,6 +4,7 @@ const init = @import("commands/init.zig"); const update = @import("commands/update.zig"); const generate = @import("commands/generate.zig"); const server = @import("commands/server.zig"); +const bundle = @import("commands/bundle.zig"); const Options = struct { help: bool = false, @@ -19,6 +20,7 @@ const Options = struct { .update = "Update current project to latest version of Jetzig", .generate = "Generate scaffolding", .server = "Run a development server", + .bundle = "Create a deployment bundle", .help = "Print help and exit", }, }; @@ -29,8 +31,10 @@ const Verb = union(enum) { update: update.Options, generate: generate.Options, server: server.Options, + bundle: bundle.Options, g: generate.Options, s: server.Options, + b: bundle.Options, }; /// Main entrypoint for `jetzig` executable. Parses command line args and generates a new @@ -52,7 +56,7 @@ pub fn main() !void { } }; - if (options.options.help or options.verb == null) { + if ((!options.options.help and options.verb == null) or (options.options.help and options.verb == null)) { try args.printHelp(Options, "jetzig", writer); try writer.writeAll( \\ @@ -62,6 +66,7 @@ pub fn main() !void { \\ update Update current project to latest version of Jetzig. \\ generate Generate scaffolding. \\ server Run a development server. + \\ bundle Create a deployment bundle. \\ \\ Pass --help to any command for more information, e.g. `jetzig init --help` \\ @@ -100,6 +105,13 @@ fn run(allocator: std.mem.Allocator, options: args.ParseArgsResult(Options, Verb options.positionals, .{ .help = options.options.help }, ), + .b, .bundle => |opts| bundle.run( + allocator, + opts, + writer, + options.positionals, + .{ .help = options.options.help }, + ), }; } } diff --git a/cli/commands/bundle.zig b/cli/commands/bundle.zig new file mode 100644 index 0000000..7435a7e --- /dev/null +++ b/cli/commands/bundle.zig @@ -0,0 +1,139 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const args = @import("args"); + +const util = @import("../util.zig"); + +/// Command line options for the `bundle` command. +pub const Options = struct { + optimize: enum { Debug, ReleaseFast, ReleaseSmall } = .ReleaseFast, + arch: enum { x86_64, aarch64, default } = .default, + os: enum { linux, macos, windows, default } = .default, + + pub const meta = .{ + .full_text = + \\Creates a deployment bundle. + \\ + \\On Windows, `tar.exe` is used to generate a `.zip` file. + \\ + \\On other operating systems, `tar` is used to generate a `.tar.gz` file. + \\ + \\The deployment bundle contains a compiled executable with the `public/` and `static/` + \\directories included. This bundle can be copied to a deployment server, unpacked, and + \\launched in place. + , + .option_docs = .{ + .optimize = "Set optimization level, must be one of { Debug, ReleaseFast, ReleaseSmall } (default: ReleaseFast)", + .arch = "Set build target CPU architecture, must be one of { x86_64, aarch64 } (default: Current CPU arch)", + .os = "Set build target operating system, must be one of { linux, macos, windows } (default: Current OS)", + }, + }; +}; + +/// Run the deployment bundle generator. Create an archive containing the Jetzig executable, +/// with `public/` and `static/` directories. +pub fn run( + allocator: std.mem.Allocator, + options: Options, + writer: anytype, + positionals: [][]const u8, + other_options: struct { help: bool }, +) !void { + _ = positionals; + if (other_options.help) { + try args.printHelp(Options, "jetzig bundle", writer); + return; + } + + std.debug.print("Compiling bundle...\n", .{}); + var cwd = try util.detectJetzigProjectDir(); + defer cwd.close(); + + const path = try cwd.realpathAlloc(allocator, "."); + defer allocator.free(path); + + if (try util.locateExecutable(allocator, cwd, .{ .relative = true })) |executable| { + defer allocator.free(executable); + + var tar_argv = std.ArrayList([]const u8).init(allocator); + defer tar_argv.deinit(); + + var install_argv = std.ArrayList([]const u8).init(allocator); + defer install_argv.deinit(); + + try install_argv.appendSlice(&[_][]const u8{ "zig", "build" }); + + switch (builtin.os.tag) { + .windows => try tar_argv.appendSlice(&[_][]const u8{ + "tar.exe", + "-a", + "-c", + "-f", + "bundle.zip", + executable, + }), + else => try tar_argv.appendSlice(&[_][]const u8{ + "tar", + "--transform=s,^,jetzig/,", + "--transform=s,^jetzig/zig-out/bin/,jetzig/,", + "-zcf", + "bundle.tar.gz", + executable, + }), + } + + switch (options.optimize) { + .ReleaseFast => try install_argv.append("-Doptimize=ReleaseFast"), + .ReleaseSmall => try install_argv.append("-Doptimize=ReleaseSmall"), + .Debug => try install_argv.append("-Doptimize=Debug"), + } + + var target_buf = std.ArrayList([]const u8).init(allocator); + defer target_buf.deinit(); + + try target_buf.append("-Dtarget="); + switch (options.arch) { + .x86_64 => try target_buf.append("x86_64"), + .aarch64 => try target_buf.append("aarch64"), + .default => try target_buf.append(@tagName(builtin.cpu.arch)), + } + + try target_buf.append("-"); + + switch (options.os) { + .linux => try target_buf.append("linux"), + .macos => try target_buf.append("macos"), + .windows => try target_buf.append("windows"), + .default => try target_buf.append(@tagName(builtin.os.tag)), + } + + const target = try std.mem.concat(allocator, u8, target_buf.items); + defer allocator.free(target); + + try install_argv.append(target); + try install_argv.append("install"); + + var public_dir: ?std.fs.Dir = cwd.openDir("public", .{}) catch null; + defer if (public_dir) |*dir| dir.close(); + + var static_dir: ?std.fs.Dir = cwd.openDir("static", .{}) catch null; + defer if (static_dir) |*dir| dir.close(); + + if (public_dir != null) try tar_argv.append("public"); + if (static_dir != null) try tar_argv.append("static"); + + try util.runCommand(allocator, path, install_argv.items); + try util.runCommand(allocator, path, tar_argv.items); + + switch (builtin.os.tag) { + .windows => std.debug.print("Bundle `bundle.zip` generated successfully.", .{}), + else => std.debug.print("Bundle `bundle.tar.gz` generated successfully.", .{}), + } + util.printSuccess(); + } else { + std.debug.print("Unable to locate compiled executable. Exiting.", .{}); + util.printFailure(); + std.os.exit(1); + } +} diff --git a/cli/commands/server.zig b/cli/commands/server.zig index 2e01e43..f38aca7 100644 --- a/cli/commands/server.zig +++ b/cli/commands/server.zig @@ -1,11 +1,12 @@ const std = @import("std"); + const args = @import("args"); + const util = @import("../util.zig"); -const builtin = @import("builtin"); pub const watch_changes_pause_duration = 1 * 1000 * 1000 * 1000; -/// Command line options for the `update` command. +/// Command line options for the `server` command. pub const Options = struct { reload: bool = true, @@ -68,7 +69,7 @@ pub fn run( &[_][]const u8{ "zig", "build", "-Djetzig_runner=true", "install" }, ); - const exe_path = try locateExecutable(allocator, cwd); + const exe_path = try util.locateExecutable(allocator, cwd, .{}); if (exe_path == null) { std.debug.print("Unable to locate compiled executable. Exiting.\n", .{}); std.os.exit(1); @@ -137,34 +138,3 @@ fn totalMtime(allocator: std.mem.Allocator, cwd: std.fs.Dir, sub_path: []const u return sum; } - -fn locateExecutable(allocator: std.mem.Allocator, dir: std.fs.Dir) !?[]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 = util.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)) { - return try bin_dir.realpathAlloc(allocator, entry.path); - } - } - - return null; -} diff --git a/cli/util.zig b/cli/util.zig index 2350911..9ff49d9 100644 --- a/cli/util.zig +++ b/cli/util.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); /// 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. @@ -167,3 +168,43 @@ pub fn githubUrl(allocator: std.mem.Allocator) ![]const u8 { }, ); } + +/// 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; +}