From 60597f78d8057d134fc92eed8b7762bb27a74fbf Mon Sep 17 00:00:00 2001 From: Ali Chraghi <63465728+alichraghi@users.noreply.github.com> Date: Sun, 2 Apr 2023 23:07:05 +0330 Subject: [PATCH] app: add mach CLI and integrate wasmserve (#738) --- app/Builder.zig | 359 ++++++++++++++++++ app/Builder/mime.zig | 59 +++ .../Builder}/www/ansi_to_html.js | 0 app/Builder/www/favicon.ico | Bin 0 -> 1150 bytes app/Builder/www/index.html | 55 +++ .../Builder}/www/wasmserve.js | 7 +- app/main.zig | 177 +++++++++ app/target.zig | 52 +++ build.zig | 29 +- libs/core/sdk.zig | 96 +---- tools/wasmserve/.gitattributes | 2 - tools/wasmserve/.gitignore | 18 - tools/wasmserve/LICENSE | 13 - tools/wasmserve/LICENSE-APACHE | 202 ---------- tools/wasmserve/LICENSE-MIT | 25 -- tools/wasmserve/README.md | 31 -- tools/wasmserve/build.zig | 18 - tools/wasmserve/mime.zig | 55 --- tools/wasmserve/test/main.zig | 7 - tools/wasmserve/wasmserve.zig | 339 ----------------- 20 files changed, 731 insertions(+), 813 deletions(-) create mode 100644 app/Builder.zig create mode 100644 app/Builder/mime.zig rename {tools/wasmserve => app/Builder}/www/ansi_to_html.js (100%) create mode 100644 app/Builder/www/favicon.ico create mode 100644 app/Builder/www/index.html rename {tools/wasmserve => app/Builder}/www/wasmserve.js (91%) create mode 100644 app/main.zig create mode 100644 app/target.zig delete mode 100644 tools/wasmserve/.gitattributes delete mode 100644 tools/wasmserve/.gitignore delete mode 100644 tools/wasmserve/LICENSE delete mode 100644 tools/wasmserve/LICENSE-APACHE delete mode 100644 tools/wasmserve/LICENSE-MIT delete mode 100644 tools/wasmserve/README.md delete mode 100644 tools/wasmserve/build.zig delete mode 100644 tools/wasmserve/mime.zig delete mode 100644 tools/wasmserve/test/main.zig delete mode 100644 tools/wasmserve/wasmserve.zig diff --git a/app/Builder.zig b/app/Builder.zig new file mode 100644 index 00000000..9491f74c --- /dev/null +++ b/app/Builder.zig @@ -0,0 +1,359 @@ +const std = @import("std"); +const mime_map = @import("Builder/mime.zig").mime_map; +const Target = @import("target.zig").Target; +const OptimizeMode = std.builtin.OptimizeMode; +const allocator = @import("root").allocator; + +const Builder = @This(); + +const out_dir_path = "zig-out/www"; +const @"www/index.html" = @embedFile("Builder/www/index.html"); +const @"www/ansi_to_html.js" = @embedFile("Builder/www/ansi_to_html.js"); +const @"www/wasmserve.js" = @embedFile("Builder/www/wasmserve.js"); +const @"www/favicon.ico" = @embedFile("Builder/www/favicon.ico"); + +steps: []const []const u8 = &.{}, +serve: bool = false, +target: ?Target = null, +optimize: OptimizeMode = .Debug, +zig_path: []const u8 = "zig", +zig_build_args: []const []const u8 = &.{}, + +status: Status = .building, +listen_port: u16 = 1717, +subscribers: std.ArrayListUnmanaged(std.net.Stream) = .{}, +watch_paths: ?[][]const u8 = null, +mtimes: std.AutoHashMapUnmanaged(std.fs.File.INode, i128) = .{}, +@"formated-www/index.html": []const u8 = undefined, + +const Status = union(enum) { + building, + built, + stopped, + compile_error: []const u8, +}; + +pub fn run(self: *Builder) !void { + if (self.serve) { + const child_pid = try std.os.fork(); + if (child_pid == 0) try self.exec(); + + const wait_res = std.os.waitpid(child_pid, 0); + if (wait_res.status != 0) std.os.exit(1); + + var out_dir = std.fs.cwd().openIterableDir(out_dir_path, .{}) catch |err| { + std.log.err("cannot open '{s}': {s}", .{ out_dir_path, @errorName(err) }); + std.os.exit(1); + }; + defer out_dir.close(); + + var wasm_file_name: ?[]const u8 = null; + var out_dir_iter = out_dir.iterate(); + while (try out_dir_iter.next()) |entry| { + if (entry.kind != .File) continue; + if (std.mem.eql(u8, std.fs.path.extension(entry.name), ".wasm")) { + wasm_file_name = try allocator.dupe(u8, entry.name); + } + } + if (wasm_file_name == null) { + std.log.err("no WASM binary found at '{s}'", .{out_dir_path}); + std.os.exit(1); + } + + self.@"formated-www/index.html" = try std.fmt.allocPrint( + allocator, + @"www/index.html", + .{ + .app_name = std.fs.path.stem(wasm_file_name.?), + .wasm_path = wasm_file_name.?, + }, + ); + + const watch_thread = if (self.watch_paths) |_| + try std.Thread.spawn(.{}, watch, .{self}) + else + null; + defer if (watch_thread) |wt| wt.detach(); + + var server = std.net.StreamServer.init(.{ .reuse_address = true }); + defer server.deinit(); + try server.listen(std.net.Address.initIp4(.{ 127, 0, 0, 1 }, self.listen_port)); + std.log.info("started listening at http://127.0.0.1:{d}...", .{self.listen_port}); + + var pool = try allocator.create(std.Thread.Pool); + try pool.init(.{ .allocator = allocator }); + defer pool.deinit(); + + while (true) { + const conn = try server.accept(); + try pool.spawn(handleConn, .{ self, conn }); + } + } else { + try self.exec(); + } +} + +fn exec(self: Builder) !void { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const argv = try self.buildArgs(arena.allocator()); + + return std.os.execvpeZ( + argv[0].?, + @ptrCast([*:null]const ?[*:0]const u8, argv), + @ptrCast([*:null]const ?[*:0]const u8, std.os.environ.ptr), + ); +} + +fn buildArgs(self: Builder, arena: std.mem.Allocator) ![*:null]const ?[*:0]const u8 { + var argv = std.ArrayList(?[*:0]const u8).init(arena); + try argv.ensureTotalCapacity(self.steps.len + self.zig_build_args.len + 7); + + argv.appendAssumeCapacity(try arena.dupeZ(u8, self.zig_path)); + argv.appendAssumeCapacity("build"); + + for (self.steps) |step| { + argv.appendAssumeCapacity(try arena.dupeZ(u8, step)); + } + + argv.appendAssumeCapacity("--color"); + argv.appendAssumeCapacity("on"); + argv.appendAssumeCapacity(try std.fmt.allocPrintZ(arena, "-Doptimize={s}", .{@tagName(self.optimize)})); + if (self.target) |target| { + argv.appendAssumeCapacity(try std.fmt.allocPrintZ(arena, "-Dtarget={s}", .{try target.toZigTriple()})); + } + + for (self.zig_build_args) |arg| { + argv.appendAssumeCapacity(try arena.dupeZ(u8, arg)); + } + + argv.appendAssumeCapacity(null); + + return @ptrCast([*:null]const ?[*:0]const u8, try argv.toOwnedSlice()); +} + +fn watch(self: *Builder) void { + _ = self; + // TODO: use std.fs.Watch once async implemented +} + +const path_handlers = std.ComptimeStringMap(*const fn (*Builder, std.net.Stream) void, .{ + .{ "/", struct { + fn h(self: *Builder, stream: std.net.Stream) void { + sendData(stream, .ok, .close, mime_map.get(".html").?, self.@"formated-www/index.html"); + } + }.h }, + .{ "/notify", struct { + fn h(builder: *Builder, stream: std.net.Stream) void { + sendData(stream, .ok, .keep_alive, "text/event-stream", null); + builder.subscribers.append(allocator, stream) catch { + stream.close(); + return; + }; + builder.notify(stream); + } + }.h }, + .{ "/wasmserve.js", struct { + fn h(_: *Builder, stream: std.net.Stream) void { + sendData(stream, .ok, .close, mime_map.get(".js").?, @"www/wasmserve.js"); + } + }.h }, + .{ "/ansi_to_html.js", struct { + fn h(_: *Builder, stream: std.net.Stream) void { + sendData(stream, .ok, .close, mime_map.get(".js").?, @"www/ansi_to_html.js"); + } + }.h }, + .{ "/favicon.ico", struct { + fn h(_: *Builder, stream: std.net.Stream) void { + sendData(stream, .ok, .close, mime_map.get(".ico").?, @"www/favicon.ico"); + } + }.h }, +}); + +fn sendData( + stream: std.net.Stream, + status: std.http.Status, + connection: std.http.Connection, + content_type: []const u8, + data: ?[]const u8, +) void { + if (data) |d| { + stream.writer().print( + "HTTP/1.1 {d} {s}\r\n" ++ + "Connection: {s}\r\n" ++ + "Content-Length: {d}\r\n" ++ + "Content-Type: {s}\r\n" ++ + "\r\n{s}", + .{ + @enumToInt(status), + status.phrase() orelse "N/A", + switch (connection) { + .close => "close", + .keep_alive => "keep-alive", + }, + d.len, + content_type, + d, + }, + ) catch { + stream.close(); + return; + }; + } else { + stream.writer().print( + "HTTP/1.1 {d} {s}\r\n" ++ + "Connection: {s}\r\n" ++ + "Content-Type: {s}\r\n" ++ + "\r\n", + .{ + @enumToInt(status), + status.phrase() orelse "N/A", + switch (connection) { + .close => "close", + .keep_alive => "keep-alive", + }, + content_type, + }, + ) catch { + stream.close(); + return; + }; + } +} + +fn handleConn(self: *Builder, conn: std.net.StreamServer.Connection) void { + errdefer { + sendError(conn.stream, .internal_server_error); + conn.stream.close(); + } + + var buf: [2048]u8 = undefined; + const first_line = conn.stream.reader().readUntilDelimiter(&buf, '\n') catch |err| { + defer conn.stream.close(); + return switch (err) { + error.StreamTooLong => sendError(conn.stream, .uri_too_long), + else => sendError(conn.stream, .bad_request), + }; + }; + var first_line_iter = std.mem.split(u8, first_line, " "); + _ = first_line_iter.next(); // skip method + if (first_line_iter.next()) |uri_str| { + const uri = std.Uri.parseWithoutScheme(uri_str) catch { + defer conn.stream.close(); + return sendError(conn.stream, .bad_request); + }; + + const handler = path_handlers.get(uri.path) orelse { + // no handlers found. search in files + const rel_path = uri.path[1..]; + + const ext = std.fs.path.extension(rel_path); + const file_mime = mime_map.get(ext) orelse "text/plain"; + + const out_dir = std.fs.cwd().openDir(out_dir_path, .{}) catch |err| { + std.log.err("cannot open '{s}': {s}", .{ out_dir_path, @errorName(err) }); + std.os.exit(1); + }; + + const file = out_dir.openFile(rel_path, .{}) catch |err| { + if (err == error.FileNotFound) { + sendError(conn.stream, .not_found); + } else { + sendError(conn.stream, .internal_server_error); + } + return; + }; + defer file.close(); + + const file_size = file.getEndPos() catch { + sendError(conn.stream, .internal_server_error); + return; + }; + + conn.stream.writer().print( + "HTTP/1.1 200 OK\r\n" ++ + "Connection: close\r\n" ++ + "Content-Length: {d}\r\n" ++ + "Content-Type: {s}\r\n" ++ + "\r\n", + .{ file_size, file_mime }, + ) catch return; + std.fs.File.writeFileAll(.{ .handle = conn.stream.handle }, file, .{}) catch return; + + return; + }; + handler(self, conn.stream); + } else { + defer conn.stream.close(); + return sendError(conn.stream, .bad_request); + } +} + +fn sendError(stream: std.net.Stream, status: std.http.Status) void { + sendData(stream, status, .close, mime_map.get(".txt").?, status.phrase() orelse "N/A"); +} + +fn notify(self: *Builder, stream: std.net.Stream) void { + stream.writer().print("event: {s}\n", .{@tagName(self.status)}) catch { + stream.close(); + return; + }; + switch (self.status) { + .compile_error => |msg| { + var lines = std.mem.split(u8, msg, "\n"); + while (lines.next()) |line| { + stream.writer().print("data: {s}\n", .{line}) catch { + stream.close(); + return; + }; + } + }, + .built, .building, .stopped => {}, + } + _ = stream.write("\n") catch { + stream.close(); + return; + }; +} + +fn compile(self: *Builder) void { + std.log.info("building...", .{}); + + var pipes = std.os.pipe() catch unreachable; + const child_pid = std.os.fork() catch unreachable; + + if (child_pid == 0) { + std.os.close(pipes[0]); + std.os.dup2(pipes[1], std.os.STDERR_FILENO) catch @panic("OOM"); + std.os.close(pipes[1]); + return self.exec() catch unreachable; + } + + std.os.close(pipes[1]); + const wait_result = std.os.waitpid(child_pid, 0); + + const stderr_file = std.fs.File{ .handle = pipes[0] }; + const stderr = stderr_file.reader().readAllAlloc(allocator, std.math.maxInt(usize)) catch @panic("OOM"); + + std.io.getStdErr().writeAll(stderr) catch unreachable; + + switch (wait_result.status) { + 0 => { + allocator.free(stderr); + self.status = .built; + std.log.info("built", .{}); + }, + 1 => { + std.log.warn("compile error", .{}); + self.status = .{ .compile_error = stderr }; + }, + else => { + allocator.free(stderr); + self.status = .stopped; + std.log.warn("the build process has stopped unexpectedly", .{}); + }, + } + for (self.subscribers.items) |sub| { + self.notify(sub); + } +} diff --git a/app/Builder/mime.zig b/app/Builder/mime.zig new file mode 100644 index 00000000..a4222d9e --- /dev/null +++ b/app/Builder/mime.zig @@ -0,0 +1,59 @@ +const std = @import("std"); + +pub const mime_map = std.ComptimeStringMap([]const u8, .{ + .{ ".aac", "audio/aac" }, + .{ ".avif", "image/avif" }, + .{ ".avi", "video/x-msvideo" }, + .{ ".bin", "application/octet-stream" }, + .{ ".bmp", "image/bmp" }, + .{ ".bz", "application/x-bzip" }, + .{ ".bz2", "application/x-bzip2" }, + .{ ".css", "text/css" }, + .{ ".csv", "text/csv" }, + .{ ".eot", "application/vnd.ms-fontobject" }, + .{ ".gz", "application/gzip" }, + .{ ".gif", "image/gif" }, + .{ ".htm", "text/html" }, + .{ ".html", "text/html" }, + .{ ".ico", "image/x-icon" }, + .{ ".ics", "text/calendar" }, + .{ ".jar", "application/java-archive" }, + .{ ".jpeg", "image/jpeg" }, + .{ ".jpg", "image/jpeg" }, + .{ ".js", "text/javascript" }, + .{ ".json", "application/json" }, + .{ ".md", "text/x-markdown" }, + .{ ".mjs", "text/javascript" }, + .{ ".mp3", "audio/mpeg" }, + .{ ".mp4", "video/mp4" }, + .{ ".mpeg", "video/mpeg" }, + .{ ".oga", "audio/ogg" }, + .{ ".ogv", "video/ogg" }, + .{ ".ogx", "application/ogg" }, + .{ ".opus", "audio/opus" }, + .{ ".otf", "font/otf" }, + .{ ".png", "image/png" }, + .{ ".pdf", "application/pdf" }, + .{ ".rar", "application/vnd.rar" }, + .{ ".rtf", "application/rtf" }, + .{ ".sh", "application/x-sh" }, + .{ ".svg", "image/svg+xml" }, + .{ ".tar", "application/x-tar" }, + .{ ".tif", ".tiff", "image/tiff" }, + .{ ".toml", "text/toml" }, + .{ ".ts", "video/mp2t" }, + .{ ".ttf", "font/ttf" }, + .{ ".txt", "text/plain" }, + .{ ".wasm", "application/wasm" }, + .{ ".wav", "audio/wav" }, + .{ ".weba", "audio/webm" }, + .{ ".webm", "video/webm" }, + .{ ".webp", "image/webp" }, + .{ ".woff", "font/woff" }, + .{ ".woff2", "font/woff2" }, + .{ ".yml", "application/x-yaml" }, + .{ ".xhtml", "application/xhtml+xml" }, + .{ ".xml", "application/xml" }, + .{ ".zip", "application/zip" }, + .{ ".7z", "application/x-7z-compressed" }, +}); diff --git a/tools/wasmserve/www/ansi_to_html.js b/app/Builder/www/ansi_to_html.js similarity index 100% rename from tools/wasmserve/www/ansi_to_html.js rename to app/Builder/www/ansi_to_html.js diff --git a/app/Builder/www/favicon.ico b/app/Builder/www/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0f885fc5c5b809e36dee710aa80202a8084215d3 GIT binary patch literal 1150 zcmZQzU}Ruq5D);-3Je)63=Con3=A3!3=9Gc3=9ek5OD?&U}0czV1r-~7c1rxfe2g0 z`Z74Cmor#Igfcj!#WN^ca5AV^L*&p&W>y9Uy95RXZ5IXx`#1&$mtaAL_{K8~F%5qh zf=fOygx7vz@JMoI2*^d($-p2a0}-=NW?<0s;AJoib6{|aFJN7E`~}C#lm9uVZv4-& z?%aR2-sM@GTdqRXB9s6BGcfqqFff>h88KK!mNA%z1u=M}xfK49WNNxMi zkk|j8Vg2?042zE~Q8%_QN?>4M6c>etF-X6AJ_CbIoCkw#LKTB~cov64;(F)2t~o({ ztKLPoE&Lx*JM+I&>%#xSrIY{5I0j9%{r{iA!q$~x_VQf}+}ykj46>#S41(&+3`RlD z!oK+z8W-*Rf8_ed|L5<1|G#_7lmF`%-1vX^?5qDxGuQn$im!RX=bGx7>}bZaYTIds zd++}I#8q!)yjZa6Y=^2&q%C6fC8 zw=FsFf5GZ~|L3mU^Z(e@xBqXw`v1SVXU=~I7w`Y858wZ9Ry5=PvYi+HuikNH0!Z(Q zttS~K&ROTWV*8nQ{*`_I7wfgArjColfL^YHoq z?zz=(YNxL3p0RMV&W6307(${_8N35ySbC?ga#}sfb69PFl5lxH-Kqlc)-uV f(7?~YzyQMR3=9k)V?h|M0z0FDk%8d=GXnzvKQfon literal 0 HcmV?d00001 diff --git a/app/Builder/www/index.html b/app/Builder/www/index.html new file mode 100644 index 00000000..50cd435f --- /dev/null +++ b/app/Builder/www/index.html @@ -0,0 +1,55 @@ + + + + + + {[app_name]s} + + + + + + + + \ No newline at end of file diff --git a/tools/wasmserve/www/wasmserve.js b/app/Builder/www/wasmserve.js similarity index 91% rename from tools/wasmserve/www/wasmserve.js rename to app/Builder/www/wasmserve.js index 5443ec55..0c0781b3 100644 --- a/tools/wasmserve/www/wasmserve.js +++ b/app/Builder/www/wasmserve.js @@ -3,10 +3,13 @@ import ansi_to_html from "./ansi_to_html.js"; let evtSource = new EventSource("/notify"); function setup() { + evtSource.addEventListener("building", function (e) { + // TODO + }); evtSource.addEventListener("built", function (e) { window.location.reload(); }); - evtSource.addEventListener("build_error", function (e) { + evtSource.addEventListener("compile_error", function (e) { createErrorScreen("An error occurred while building:", e.data) }); evtSource.addEventListener("stopped", function (e) { @@ -62,4 +65,4 @@ const error_screen_pre_css = "white-space: pre-wrap;" + "overflow: hidden;"; -export default setup; +export default setup; \ No newline at end of file diff --git a/app/main.zig b/app/main.zig new file mode 100644 index 00000000..3e1ecd22 --- /dev/null +++ b/app/main.zig @@ -0,0 +1,177 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Builder = @import("Builder.zig"); +const Target = @import("target.zig").Target; + +const default_zig_path = "zig"; + +var args: []const [:0]u8 = undefined; +var arg_i: usize = 1; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +pub const allocator = gpa.allocator(); + +pub fn main() !void { + defer _ = gpa.deinit(); + + args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + if (args.len == 1) return; + + if (std.mem.eql(u8, args[arg_i], "build")) { + arg_i += 1; + var builder = Builder{}; + var steps = std.ArrayList([]const u8).init(allocator); + var build_args = std.ArrayList([]const u8).init(allocator); + + while (arg_i < args.len) : (arg_i += 1) { + if (argOption("-zig-path")) |value| { + builder.zig_path = value; + } else if (std.mem.eql(u8, args[arg_i], "--serve")) { + if (builder.target == null) builder.target = .wasm32; + if (builder.target.? != .wasm32) { + std.log.err("--serve requires -target=wasm32", .{}); + try printHelp(.build); + std.os.exit(1); + } + builder.serve = true; + } else if (argOption("-target")) |value| { + builder.target = Target.parse(value) orelse { + std.log.err("invalid target '{s}'", .{args[arg_i]}); + try printHelp(.build); + std.os.exit(1); + }; + } else if (argOption("-listen-port")) |value| { + builder.listen_port = std.fmt.parseInt(u16, value, 10) catch { + std.log.err("invalid port '{s}'", .{args[arg_i]}); + try printHelp(.build); + std.os.exit(1); + }; + } else if (argOption("-watch-path")) |value| { + var paths = std.mem.split(u8, value, ","); + builder.watch_paths = try allocator.alloc([]const u8, std.mem.count(u8, value, ",") + 1); + for (0..255) |i| { + const path = paths.next() orelse break; + builder.watch_paths.?[i] = std.mem.trim(u8, path, &std.ascii.whitespace); + } + } else if (argOption("-optimize")) |value| { + builder.optimize = std.meta.stringToEnum(std.builtin.OptimizeMode, value) orelse { + std.log.err("invalid optimize mode '{s}'", .{args[arg_i]}); + try printHelp(.build); + std.os.exit(1); + }; + } else if (std.mem.eql(u8, args[arg_i], "--")) { + arg_i += 1; + while (arg_i < args.len) : (arg_i += 1) { + try build_args.append(args[arg_i]); + } + } else { + try steps.append(args[arg_i]); + } + } + + builder.steps = try steps.toOwnedSlice(); + builder.zig_build_args = try build_args.toOwnedSlice(); + + return builder.run(); + } else if (std.mem.eql(u8, args[arg_i], "help")) { + arg_i += 1; + var subcommand = SubCommand.help; + + if (arg_i < args.len) { + if (std.mem.eql(u8, args[arg_i], "build")) { + subcommand = .build; + } else { + std.log.err("unknown command name '{s}'", .{args[arg_i]}); + try printHelp(.help); + std.os.exit(1); + } + } + return printHelp(subcommand); + } else { + std.log.err("invalid command '{s}'", .{args[arg_i]}); + try printHelp(.help); + std.os.exit(1); + } +} + +pub const SubCommand = enum { + build, + help, +}; + +fn printHelp(subcommand: SubCommand) !void { + const stdout = std.io.getStdOut(); + switch (subcommand) { + .build => { + try stdout.writeAll( + \\Usage: + \\ mach build [steps] [options] [-- [zig-build-options]] + \\ + \\General Options: + \\ + \\ -zig-path [path] Override path to zig binary + \\ + \\ -target [target] The CPU architecture and OS to build for + \\ Default is native target + \\ Supported targets: + \\ linux-x86_64, linux-aarch64, + \\ macos-x86_64, macos-aarch64, + \\ windows-x86_64, windows-aarch64, + \\ wasm32, + \\ + \\ -optimize [optimize] Prioritize performance, safety, or binary size + \\ Default is Debug + \\ Supported values: + \\ Debug + \\ ReleaseSafe + \\ ReleaseFast + \\ ReleaseSmall + \\ + \\Serve Options: + \\ + \\ --serve Starts a development server + \\ for testing WASM applications/games + \\ + \\ -listen-port [port] The development server port + \\ + \\ -watch-path [paths] Watches for changes in specified directory + \\ and automatically builds and reloads + \\ development server + \\ Separate each path with comma (,) + \\ + \\ + ); + }, + .help => { + try stdout.writeAll( + \\Usage: + \\ mach [command] + \\ + \\Commands: + \\ build Build current project + \\ help Print this mesage or the help of the given command + \\ + \\ + ); + }, + } +} + +pub fn argOption(name: []const u8) ?[]const u8 { + const cmd_arg = args[arg_i]; + if (std.mem.startsWith(u8, cmd_arg, name)) { + if (cmd_arg.len > name.len + 1 and cmd_arg[name.len] == '=') { + return cmd_arg[name.len + 1 ..]; + } else if (cmd_arg.len == name.len) { + arg_i += 1; + if (arg_i < args.len) { + return args[arg_i]; + } else { + std.log.err("expected value after '{s}' option", .{cmd_arg}); + std.os.exit(1); + } + } + } + return null; +} diff --git a/app/target.zig b/app/target.zig new file mode 100644 index 00000000..78672f00 --- /dev/null +++ b/app/target.zig @@ -0,0 +1,52 @@ +const std = @import("std"); +const allocator = @import("root").allocator; + +pub const Target = enum { + @"linux-x86_64", + @"linux-aarch64", + @"macos-x86_64", + @"macos-aarch64", + @"windows-x86_64", + @"windows-aarch64", + wasm32, + + // TODO + // android, + // ios, + + pub fn parse(str: []const u8) ?Target { + return if (std.mem.eql(u8, str, "linux")) + .@"linux-x86_64" + else if (std.mem.eql(u8, str, "windows")) + .@"windows-x86_64" + else if (std.mem.eql(u8, str, "macos")) + .@"macos-aarch64" + else if (std.mem.eql(u8, str, "wasm")) + .wasm32 + else + std.meta.stringToEnum(Target, str) orelse return null; + } + + pub fn toZigTriple(self: Target) ![]const u8 { + const zig_target = std.zig.CrossTarget{ + .cpu_arch = switch (self) { + .@"linux-x86_64", + .@"macos-x86_64", + .@"windows-x86_64", + => .x86_64, + .@"linux-aarch64", + .@"macos-aarch64", + .@"windows-aarch64", + => .aarch64, + .wasm32 => .wasm32, + }, + .os_tag = switch (self) { + .@"linux-x86_64", .@"linux-aarch64" => .linux, + .@"macos-x86_64", .@"macos-aarch64" => .macos, + .@"windows-x86_64", .@"windows-aarch64" => .windows, + .wasm32 => .freestanding, + }, + }; + return zig_target.zigTriple(allocator); + } +}; diff --git a/build.zig b/build.zig index 8500f88d..dfa86ed2 100644 --- a/build.zig +++ b/build.zig @@ -10,7 +10,6 @@ const earcut = @import("libs/earcut/build.zig"); const gamemode = @import("libs/gamemode/build.zig"); const model3d = @import("libs/model3d/build.zig"); const dusk = @import("libs/dusk/build.zig"); -const wasmserve = @import("tools/wasmserve/wasmserve.zig"); pub const gpu_dawn = @import("libs/gpu-dawn/sdk.zig").Sdk(.{ .glfw_include_dir = sdkPath("/libs/glfw/upstream/glfw/include"), .system_sdk = system_sdk, @@ -27,7 +26,6 @@ const core = @import("libs/core/sdk.zig").Sdk(.{ .gpu_dawn = gpu_dawn, .glfw = glfw, .gamemode = gamemode, - .wasmserve = wasmserve, .sysjs = sysjs, }); @@ -57,6 +55,22 @@ pub fn build(b: *std.Build) !void { const optimize = b.standardOptimizeOption(.{}); const target = b.standardTargetOptions(.{}); + const app = b.addExecutable(.{ + .name = "mach", + .root_source_file = .{ .path = "app/main.zig" }, + .version = .{ .major = 0, .minor = 1, .patch = 0 }, + .optimize = optimize, + .target = target, + }); + app.addModule("mach", module(b)); + if (app.target.getOsTag() == .windows) app.linkLibC(); + app.install(); + + const app_run_cmd = app.run(); + if (b.args) |args| app_run_cmd.addArgs(args); + const app_run_step = b.step("run", "Run Mach Engine Application"); + app_run_step.dependOn(&app_run_cmd.step); + const gpu_dawn_options = gpu_dawn.Options{ .from_source = b.option(bool, "dawn-from-source", "Build Dawn from source") orelse false, .debug = b.option(bool, "dawn-debug", "Use a debug build of Dawn") orelse false, @@ -107,10 +121,10 @@ pub fn build(b: *std.Build) !void { const shaderexp_compile_step = b.step("shaderexp", "Compile shaderexp"); shaderexp_compile_step.dependOn(&shaderexp_app.getInstallStep().?.step); - const shaderexp_run_cmd = try shaderexp_app.run(); - shaderexp_run_cmd.dependOn(&shaderexp_app.getInstallStep().?.step); + const shaderexp_run_cmd = shaderexp_app.run(); + shaderexp_run_cmd.step.dependOn(&shaderexp_app.getInstallStep().?.step); const shaderexp_run_step = b.step("run-shaderexp", "Run shaderexp"); - shaderexp_run_step.dependOn(shaderexp_run_cmd); + shaderexp_run_step.dependOn(&shaderexp_run_cmd.step); } const compile_all = b.step("compile-all", "Compile Mach"); @@ -144,7 +158,6 @@ pub const App = struct { pub const InitError = core.App.InitError; pub const LinkError = core.App.LinkError; - pub const RunError = core.App.RunError; pub fn init( b: *std.Build, @@ -202,8 +215,8 @@ pub const App = struct { app.core.install(); } - pub fn run(app: *const App) RunError!*std.build.Step { - return try app.core.run(); + pub fn run(app: *const App) *std.build.RunStep { + return app.core.run(); } pub fn getInstallStep(app: *const App) ?*std.build.InstallArtifactStep { diff --git a/libs/core/sdk.zig b/libs/core/sdk.zig index 2f1b6636..e0600482 100644 --- a/libs/core/sdk.zig +++ b/libs/core/sdk.zig @@ -89,9 +89,6 @@ pub fn Sdk(comptime deps: anytype) type { pub const InitError = error{OutOfMemory} || std.zig.system.NativeTargetInfo.DetectError; pub const LinkError = deps.glfw.LinkError; - pub const RunError = error{ - ParsingIpFailed, - } || deps.wasmserve.Error || std.fmt.ParseIntError; pub const Platform = enum { native, @@ -181,7 +178,7 @@ pub fn Sdk(comptime deps: anytype) type { pub fn install(app: *const App) void { app.step.install(); - // Install additional files (src/mach.js and template.html) + // Install additional files (mach.js and mach-sysjs.js) // in case of wasm if (app.platform == .web) { // Set install directory to '{prefix}/www' @@ -195,10 +192,6 @@ pub fn Sdk(comptime deps: anytype) type { ); app.getInstallStep().?.step.dependOn(&install_js.step); } - - genHtml(app.b.allocator, app.b.getInstallPath(web_install_dir, "index.html"), app.name) catch |err| { - std.log.err("unable to generate html: {s}", .{@errorName(err)}); - }; } // Install resources @@ -215,96 +208,13 @@ pub fn Sdk(comptime deps: anytype) type { } } - pub fn run(app: *const App) RunError!*std.build.Step { - if (app.platform == .web) { - const address = std.process.getEnvVarOwned(app.b.allocator, "MACH_ADDRESS") catch try app.b.allocator.dupe(u8, "127.0.0.1"); - const port = std.process.getEnvVarOwned(app.b.allocator, "MACH_PORT") catch try app.b.allocator.dupe(u8, "8080"); - const address_parsed = std.net.Address.parseIp4(address, try std.fmt.parseInt(u16, port, 10)) catch return error.ParsingIpFailed; - const serve_step = try deps.wasmserve.serve( - app.b, - .{ - // .step_name = - .install_dir = web_install_dir, - .watch_paths = app.watch_paths, - .listen_address = address_parsed, - }, - ); - return &serve_step.step; - } else { - return &app.step.run().step; - } + pub fn run(app: *const App) *std.build.RunStep { + return app.step.run(); } pub fn getInstallStep(app: *const App) ?*std.build.InstallArtifactStep { return app.step.install_step; } }; - - pub fn genHtml(allocator: std.mem.Allocator, output_name: []const u8, app_name: []const u8) !void { - const file = try std.fs.cwd().createFile(output_name, .{}); - defer file.close(); - - var buf = try std.fmt.allocPrint(allocator, html_template, .{ .app_name = app_name }); - defer allocator.free(buf); - - _ = try file.write(buf); - } - - const html_template = - \\ - \\ - \\ - \\ - \\ - \\ {[app_name]s} - \\ - \\ - \\ - \\ - \\ - \\ - \\ - ; }; } diff --git a/tools/wasmserve/.gitattributes b/tools/wasmserve/.gitattributes deleted file mode 100644 index ba1273bd..00000000 --- a/tools/wasmserve/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -* text=auto eol=lf -upstream/** linguist-vendored diff --git a/tools/wasmserve/.gitignore b/tools/wasmserve/.gitignore deleted file mode 100644 index feda423c..00000000 --- a/tools/wasmserve/.gitignore +++ /dev/null @@ -1,18 +0,0 @@ -# This file is for zig-specific build artifacts. -# If you have OS-specific or editor-specific files to ignore, -# such as *.swp or .DS_Store, put those in your global -# ~/.gitignore and put this in your ~/.gitconfig: -# -# [core] -# excludesfile = ~/.gitignore -# -# Cheers! -# -andrewrk - -zig-cache/ -zig-out/ -/release/ -/debug/ -/build/ -/build-*/ -/docgen_tmp/ diff --git a/tools/wasmserve/LICENSE b/tools/wasmserve/LICENSE deleted file mode 100644 index ba6099da..00000000 --- a/tools/wasmserve/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2021, Hexops Contributors (given via the Git commit history). - -All documentation, image, sound, font, and 2D/3D model files are CC-BY-4.0 licensed unless -otherwise noted. You may get a copy of this license at https://creativecommons.org/licenses/by/4.0 - -Files in a directory with a separate LICENSE file may contain files under different license terms, -described within that LICENSE file. - -All other files are licensed under the Apache License, Version 2.0 (see LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) -or the MIT license (see LICENSE-MIT or http://opensource.org/licenses/MIT), at your option. - -All files in the project without exclusions may not be copied, modified, or distributed except -according to the terms above. \ No newline at end of file diff --git a/tools/wasmserve/LICENSE-APACHE b/tools/wasmserve/LICENSE-APACHE deleted file mode 100644 index d6456956..00000000 --- a/tools/wasmserve/LICENSE-APACHE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/tools/wasmserve/LICENSE-MIT b/tools/wasmserve/LICENSE-MIT deleted file mode 100644 index 14cd7973..00000000 --- a/tools/wasmserve/LICENSE-MIT +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2021 Hexops Contributors (given via the Git commit history). - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/tools/wasmserve/README.md b/tools/wasmserve/README.md deleted file mode 100644 index 73df0a31..00000000 --- a/tools/wasmserve/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# mach/wasmserve - -Small web server specifically for serving Zig WASM applications in development - -## Getting started - -### Adding dependency - -In a `libs` subdirectory of the root of your project: - -```sh -git clone https://github.com/machlibs/wasmserve -``` - -Then in your `build.zig` add: - -```zig -... -const wasmserve = @import("libs/wasmserve/wasmserve.zig"); - -pub fn build(b: *Build) void { - ... - const serve_step = try wasmserve.serve(exe, .{ .watch_paths = &.{"src/main.zig"} }); - const run_step = b.step("run", "Run development web server"); - run_step.dependOn(&serve_step.step); -} -``` - -## Join the community - -Join the Mach community [on Discord](https://discord.gg/XNG3NZgCqp) to discuss this project, ask questions, get help, etc. diff --git a/tools/wasmserve/build.zig b/tools/wasmserve/build.zig deleted file mode 100644 index d9ec51fd..00000000 --- a/tools/wasmserve/build.zig +++ /dev/null @@ -1,18 +0,0 @@ -const std = @import("std"); -const wasmserve = @import("wasmserve.zig"); - -pub fn build(b: *std.Build) !void { - const optimize = b.standardOptimizeOption(.{}); - - const exe = b.addSharedLibrary(.{ - .name = "test", - .root_source_file = .{ .path = "test/main.zig" }, - .target = .{ .cpu_arch = .wasm32, .os_tag = .freestanding, .abi = .none }, - .optimize = optimize, - }); - exe.install(); - - const serve_step = try wasmserve.serve(exe, .{ .watch_paths = &.{"wasmserve.zig"} }); - const run_step = b.step("test", "Start a testing server"); - run_step.dependOn(&serve_step.step); -} diff --git a/tools/wasmserve/mime.zig b/tools/wasmserve/mime.zig deleted file mode 100644 index be35d099..00000000 --- a/tools/wasmserve/mime.zig +++ /dev/null @@ -1,55 +0,0 @@ -pub const mime_list = [_]struct { ext: []const []const u8, mime: []const u8 }{ - .{ .ext = &.{".aac"}, .mime = "audio/aac" }, - .{ .ext = &.{".avif"}, .mime = "image/avif" }, - .{ .ext = &.{".avi"}, .mime = "video/x-msvideo" }, - .{ .ext = &.{".bin"}, .mime = "application/octet-stream" }, - .{ .ext = &.{".bmp"}, .mime = "image/bmp" }, - .{ .ext = &.{".bz"}, .mime = "application/x-bzip" }, - .{ .ext = &.{".bz2"}, .mime = "application/x-bzip2" }, - .{ .ext = &.{".css"}, .mime = "text/css" }, - .{ .ext = &.{".csv"}, .mime = "text/csv" }, - .{ .ext = &.{".eot"}, .mime = "application/vnd.ms-fontobject" }, - .{ .ext = &.{".gz"}, .mime = "application/gzip" }, - .{ .ext = &.{".gif"}, .mime = "image/gif" }, - .{ .ext = &.{ ".htm", ".html" }, .mime = "text/html" }, - .{ .ext = &.{".ico"}, .mime = "image/vnd.microsoft.icon" }, - .{ .ext = &.{".ics"}, .mime = "text/calendar" }, - .{ .ext = &.{".jar"}, .mime = "application/java-archive" }, - .{ .ext = &.{ ".jpeg", ".jpg" }, .mime = "image/jpeg" }, - .{ .ext = &.{".js"}, .mime = "text/javascript" }, - .{ .ext = &.{".json"}, .mime = "application/json" }, - .{ .ext = &.{".md"}, .mime = "text/x-markdown" }, - .{ .ext = &.{".mjs"}, .mime = "text/javascript" }, - .{ .ext = &.{".mp3"}, .mime = "audio/mpeg" }, - .{ .ext = &.{".mp4"}, .mime = "video/mp4" }, - .{ .ext = &.{".mpeg"}, .mime = "video/mpeg" }, - .{ .ext = &.{".oga"}, .mime = "audio/ogg" }, - .{ .ext = &.{".ogv"}, .mime = "video/ogg" }, - .{ .ext = &.{".ogx"}, .mime = "application/ogg" }, - .{ .ext = &.{".opus"}, .mime = "audio/opus" }, - .{ .ext = &.{".otf"}, .mime = "font/otf" }, - .{ .ext = &.{".png"}, .mime = "image/png" }, - .{ .ext = &.{".pdf"}, .mime = "application/pdf" }, - .{ .ext = &.{".rar"}, .mime = "application/vnd.rar" }, - .{ .ext = &.{".rtf"}, .mime = "application/rtf" }, - .{ .ext = &.{".sh"}, .mime = "application/x-sh" }, - .{ .ext = &.{".svg"}, .mime = "image/svg+xml" }, - .{ .ext = &.{".tar"}, .mime = "application/x-tar" }, - .{ .ext = &.{ ".tif", ".tiff" }, .mime = "image/tiff" }, - .{ .ext = &.{".toml"}, .mime = "text/toml" }, - .{ .ext = &.{".ts"}, .mime = "video/mp2t" }, - .{ .ext = &.{".ttf"}, .mime = "font/ttf" }, - .{ .ext = &.{".txt"}, .mime = "text/plain" }, - .{ .ext = &.{".wasm"}, .mime = "application/wasm" }, - .{ .ext = &.{".wav"}, .mime = "audio/wav" }, - .{ .ext = &.{".weba"}, .mime = "audio/webm" }, - .{ .ext = &.{".webm"}, .mime = "video/webm" }, - .{ .ext = &.{".webp"}, .mime = "image/webp" }, - .{ .ext = &.{".woff"}, .mime = "font/woff" }, - .{ .ext = &.{".woff2"}, .mime = "font/woff2" }, - .{ .ext = &.{".yml"}, .mime = "application/x-yaml" }, - .{ .ext = &.{".xhtml"}, .mime = "application/xhtml+xml" }, - .{ .ext = &.{".xml"}, .mime = "application/xml" }, - .{ .ext = &.{".zip"}, .mime = "application/zip" }, - .{ .ext = &.{".7z"}, .mime = "application/x-7z-compressed" }, -}; diff --git a/tools/wasmserve/test/main.zig b/tools/wasmserve/test/main.zig deleted file mode 100644 index 74aad7f3..00000000 --- a/tools/wasmserve/test/main.zig +++ /dev/null @@ -1,7 +0,0 @@ -const std = @import("std"); - -pub fn main() void { - var x: i16 = 1; - x += 1; - std.testing.expect(x == 2) catch unreachable; -} diff --git a/tools/wasmserve/wasmserve.zig b/tools/wasmserve/wasmserve.zig deleted file mode 100644 index 6e0a5c10..00000000 --- a/tools/wasmserve/wasmserve.zig +++ /dev/null @@ -1,339 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const mime = @import("mime.zig"); -const net = std.net; -const mem = std.mem; -const fs = std.fs; -const build = std.build; - -const www_dir_path = sdkPath("/www"); -const buffer_size = 2048; -const esc = struct { - pub const reset = "\x1b[0m"; - pub const bold = "\x1b[1m"; - pub const underline = "\x1b[4m"; - pub const red = "\x1b[31m"; - pub const yellow = "\x1b[33m"; - pub const cyan = "\x1b[36m"; - pub const gray = "\x1b[90m"; -}; - -pub const Options = struct { - step_name: []const u8 = "install", - install_dir: ?build.InstallDir = null, - watch_paths: ?[]const []const u8 = null, - listen_address: ?net.Address = null, -}; - -pub const Error = error{CannotOpenDirectory} || mem.Allocator.Error; - -pub fn serve(b: *std.Build.Builder, options: Options) Error!*Wasmserve { - const self = try b.allocator.create(Wasmserve); - const install_dir = options.install_dir orelse build.InstallDir{ .lib = {} }; - const install_dir_iter = fs.cwd().makeOpenPathIterable(b.getInstallPath(install_dir, ""), .{}) catch - return error.CannotOpenDirectory; - self.* = Wasmserve{ - .b = b, - .step = build.Step.init(.{ .id = .run, .name = "wasmserve", .owner = b, .makeFn = Wasmserve.make }), - .step_name = options.step_name, - .install_dir = install_dir, - .install_dir_iter = install_dir_iter, - .address = options.listen_address orelse net.Address.initIp4([4]u8{ 127, 0, 0, 1 }, 8080), - .subscriber = null, - .watch_paths = options.watch_paths orelse &.{"src"}, - .mtimes = std.AutoHashMap(fs.File.INode, i128).init(b.allocator), - .notify_msg = null, - }; - return self; -} - -const Wasmserve = struct { - b: *build.Builder, - step: build.Step, - step_name: []const u8, - install_dir: build.InstallDir, - install_dir_iter: fs.IterableDir, - address: net.Address, - subscriber: ?*net.StreamServer.Connection, - watch_paths: []const []const u8, - mtimes: std.AutoHashMap(fs.File.INode, i128), - notify_msg: ?NotifyMessage, - - const NotifyMessage = struct { - const Event = enum { - built, - build_error, - stopped, - }; - - event: Event, - data: []const u8, - }; - - pub fn make(step: *build.Step, prog_node: *std.Progress.Node) !void { - std.debug.print("Really!\n", .{}); - - const self = @fieldParentPtr(Wasmserve, "step", step); - - try self.compile(); - // std.debug.assert(mem.eql(u8, fs.path.extension(self.compile_step.out_filename), ".wasm")); - - var www_dir = try fs.cwd().openIterableDir(www_dir_path, .{}); - defer www_dir.close(); - var www_dir_iter = www_dir.iterate(); - while (try www_dir_iter.next()) |file| { - const path = try fs.path.join(self.b.allocator, &.{ www_dir_path, file.name }); - defer self.b.allocator.free(path); - const install_www = self.b.addInstallFileWithDir( - .{ .path = path }, - self.install_dir, - file.name, - ); - try install_www.step.make(prog_node); - } - - const watch_thread = try std.Thread.spawn(.{}, watch, .{self}); - defer watch_thread.detach(); - try self.runServer(); - } - - fn runServer(self: *Wasmserve) !void { - var server = net.StreamServer.init(.{ .reuse_address = true }); - defer server.deinit(); - try server.listen(self.address); - - var addr_buf = @as([45]u8, undefined); - var fbs = std.io.fixedBufferStream(&addr_buf); - if (self.address.format("", .{}, fbs.writer())) { - std.log.info("Started listening at " ++ esc.cyan ++ esc.underline ++ "http://{s}" ++ esc.reset ++ "...", .{fbs.getWritten()}); - } else |err| logErr(err, @src()); - - while (server.accept()) |conn| { - self.respond(conn) catch |err| { - logErr(err, @src()); - continue; - }; - } else |err| logErr(err, @src()); - } - - fn respond(self: *Wasmserve, conn: net.StreamServer.Connection) !void { - errdefer respondError(conn.stream, 500, "Internal Server Error") catch |err| logErr(err, @src()); - - var recv_buf: [buffer_size]u8 = undefined; - const first_line = conn.stream.reader().readUntilDelimiter(&recv_buf, '\n') catch |err| { - switch (err) { - error.StreamTooLong => try respondError(conn.stream, 414, "Too Long Request"), - else => try respondError(conn.stream, 400, "Bad Request"), - } - return; - }; - var first_line_iter = mem.split(u8, first_line, " "); - _ = first_line_iter.next(); // skip method - if (first_line_iter.next()) |uri| { - if (uri[0] != '/') { - try respondError(conn.stream, 400, "Bad Request"); - return; - } - - const url = dropFragment(uri)[1..]; - if (mem.eql(u8, url, "notify")) { - _ = try conn.stream.write( - "HTTP/1.1 200 OK\r\n" ++ - "Connection: Keep-Alive\r\n" ++ - "Content-Type: text/event-stream\r\n" ++ - "Cache-Control: No-Cache\r\n\r\n", - ); - self.subscriber = try self.b.allocator.create(net.StreamServer.Connection); - self.subscriber.?.* = conn; - if (self.notify_msg) |msg| - if (msg.event != .built) - self.notify(); - return; - } - if (self.researchPath(url)) |file_path| { - try self.respondFile(conn.stream, file_path); - return; - } else |_| {} - - try respondError(conn.stream, 404, "Not Found"); - } else { - try respondError(conn.stream, 400, "Bad Request"); - } - } - - fn respondFile(self: Wasmserve, stream: net.Stream, path: []const u8) !void { - const ext = fs.path.extension(path); - var file_mime: []const u8 = "text/plain"; - inline for (mime.mime_list) |entry| { - for (entry.ext) |ext_entry| { - if (std.mem.eql(u8, ext, ext_entry)) - file_mime = entry.mime; - } - } - - const file = try self.install_dir_iter.dir.openFile(path, .{}); - defer file.close(); - const file_size = try file.getEndPos(); - - try stream.writer().print( - "HTTP/1.1 200 OK\r\n" ++ - "Connection: close\r\n" ++ - "Content-Length: {d}\r\n" ++ - "Content-Type: {s}\r\n" ++ - "\r\n", - .{ file_size, file_mime }, - ); - try fs.File.writeFileAll(.{ .handle = stream.handle }, file, .{}); - } - - fn respondError(stream: net.Stream, code: u32, desc: []const u8) !void { - try stream.writer().print( - "HTTP/1.1 {d} {s}\r\n" ++ - "Connection: close\r\n" ++ - "Content-Length: {d}\r\n" ++ - "Content-Type: text/html\r\n" ++ - "\r\n

{s}

", - .{ code, desc, desc.len + 50, desc }, - ); - } - - fn researchPath(self: Wasmserve, path: []const u8) ![]const u8 { - var walker = try self.install_dir_iter.walk(self.b.allocator); - defer walker.deinit(); - while (try walker.next()) |walk_entry| { - if (walk_entry.kind != .File) continue; - if (mem.eql(u8, walk_entry.path, path) or (path.len == 0 and mem.eql(u8, walk_entry.path, "index.html"))) - return try self.b.allocator.dupe(u8, walk_entry.path); - } - return error.FileNotFound; - } - - fn watch(self: *Wasmserve) void { - timer_loop: while (true) : (std.time.sleep(500 * std.time.ns_per_ms)) { - for (self.watch_paths) |path| { - var dir = fs.cwd().openIterableDir(path, .{}) catch continue; - defer dir.close(); - var walker = dir.walk(self.b.allocator) catch |err| { - logErr(err, @src()); - continue; - }; - defer walker.deinit(); - while (walker.next() catch |err| { - logErr(err, @src()); - continue; - }) |walk_entry| { - if (walk_entry.kind != .File) continue; - if (self.checkForUpdate(dir.dir, walk_entry.path)) |is_updated| { - if (is_updated) - continue :timer_loop; - } else |err| { - logErr(err, @src()); - continue; - } - } - } - } - } - - fn checkForUpdate(self: *Wasmserve, p_dir: fs.Dir, path: []const u8) !bool { - const stat = try p_dir.statFile(path); - const entry = try self.mtimes.getOrPut(stat.inode); - if (entry.found_existing and stat.mtime > entry.value_ptr.*) { - std.log.info(esc.yellow ++ esc.underline ++ "{s}" ++ esc.reset ++ " updated", .{path}); - try self.compile(); - entry.value_ptr.* = stat.mtime; - return true; - } - entry.value_ptr.* = stat.mtime; - return false; - } - - fn notify(self: *Wasmserve) void { - if (self.subscriber) |s| { - if (self.notify_msg) |msg| { - s.stream.writer().print("event: {s}\n", .{@tagName(msg.event)}) catch |err| logErr(err, @src()); - - var lines = std.mem.split(u8, msg.data, "\n"); - while (lines.next()) |line| - s.stream.writer().print("data: {s}\n", .{line}) catch |err| logErr(err, @src()); - _ = s.stream.write("\n") catch |err| logErr(err, @src()); - } - } - } - - fn compile(self: *Wasmserve) !void { - std.log.info("Building...", .{}); - - var res = std.ChildProcess.exec(.{ .argv = &.{ self.b.zig_exe, "build", self.step_name, "-Dtarget=wasm32-freestanding-none" }, .allocator = self.b.allocator }) catch |err| { - logErr(err, @src()); - return; - }; - defer self.b.allocator.free(res.stdout); - if (self.notify_msg) |msg| - if (msg.event == .build_error) - self.b.allocator.free(msg.data); - - switch (res.term) { - .Exited => |code| { - if (code == 0) { - std.log.info("Built", .{}); - self.notify_msg = .{ - .event = .built, - .data = "", - }; - } else { - std.log.err("Compile error", .{}); - self.notify_msg = .{ - .event = .build_error, - .data = res.stderr, - }; - } - }, - .Signal, .Stopped, .Unknown => { - std.log.err("The build process has stopped unexpectedly", .{}); - self.notify_msg = .{ - .event = .stopped, - .data = "", - }; - }, - } - std.io.getStdErr().writeAll(res.stderr) catch |err| logErr(err, @src()); - self.notify(); - } -}; - -fn dropFragment(input: []const u8) []const u8 { - for (input, 0..) |c, i| - if (c == '?' or c == '#') - return input[0..i]; - - return input; -} - -fn logErr(err: anyerror, src: std.builtin.SourceLocation) void { - if (@errorReturnTrace()) |et| { - std.log.err(esc.red ++ esc.bold ++ "{s}" ++ esc.reset ++ " >>>\n{s}", .{ @errorName(err), et }); - } else { - var file_name_buf: [1024]u8 = undefined; - var fba = std.heap.FixedBufferAllocator.init(&file_name_buf); - const allocator = fba.allocator(); - const file_path = fs.path.relative(allocator, ".", src.file) catch @as([]const u8, src.file); - std.log.err(esc.red ++ esc.bold ++ "{s}" ++ esc.reset ++ - " at " ++ esc.underline ++ "{s}:{d}:{d}" ++ esc.reset ++ - esc.gray ++ " fn {s}()" ++ esc.reset, .{ - @errorName(err), - file_path, - src.line, - src.column, - src.fn_name, - }); - } -} - -fn sdkPath(comptime suffix: []const u8) []const u8 { - if (suffix[0] != '/') @compileError("suffix must be an absolute path"); - return comptime blk: { - const root_dir = std.fs.path.dirname(@src().file) orelse "."; - break :blk root_dir ++ suffix; - }; -}