add file watcher
This commit is contained in:
parent
8ed6347466
commit
9217965ec1
207
build.zig
207
build.zig
@ -1,156 +1,87 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Although this function looks imperative, it does not perform the build
|
||||
// directly and instead it mutates the build graph (`b`) that will be then
|
||||
// executed by an external runner. The functions in `std.Build` implement a DSL
|
||||
// for defining build steps and express dependencies between them, allowing the
|
||||
// build runner to parallelize the build automatically (and the cache system to
|
||||
// know when a step doesn't need to be re-run).
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Standard target options allow the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
// means any target is allowed, and the default is native. Other options
|
||||
// for restricting supported target set are available.
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
// Standard optimization options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
// It's also possible to define more custom flags to toggle optional features
|
||||
// of this build script using `b.option()`. All defined flags (including
|
||||
// target and optimize options) will be listed when running `zig build --help`
|
||||
// in this directory.
|
||||
|
||||
// This creates a module, which represents a collection of source files alongside
|
||||
// some compilation options, such as optimization mode and linked system libraries.
|
||||
// Zig modules are the preferred way of making Zig code available to consumers.
|
||||
// addModule defines a module that we intend to make available for importing
|
||||
// to our consumers. We must give it a name because a Zig package can expose
|
||||
// multiple modules and consumers will need to be able to specify which
|
||||
// module they want to access.
|
||||
const mod = b.addModule("sokol_template_v2", .{
|
||||
// The root source file is the "entry point" of this module. Users of
|
||||
// this module will only be able to access public declarations contained
|
||||
// in this file, which means that if you have declarations that you
|
||||
// intend to expose to consumers that were defined in other files part
|
||||
// of this module, you will have to make sure to re-export them from
|
||||
// the root file.
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
// Later on we'll use this module as the root module of a test executable
|
||||
// which requires us to specify a target.
|
||||
const plugin_dynamic_enabled = b.option(bool, "plugin-dynamic", "Enable loading plugin through a dynamic library") orelse true;
|
||||
const plugin_static_enabled = b.option(bool, "plugin-static", "Enable loading plugin through a dynamic library") orelse true;
|
||||
|
||||
const build_options = b.addOptions();
|
||||
build_options.addOption(bool, "plugin_dynamic", plugin_dynamic_enabled);
|
||||
build_options.addOption(bool, "plugin_static", plugin_static_enabled);
|
||||
|
||||
const plugin_api_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/plugin_api.zig"),
|
||||
});
|
||||
|
||||
const exe_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize
|
||||
});
|
||||
exe_mod.addOptions("build_options", build_options);
|
||||
exe_mod.addImport("plugin_api", plugin_api_mod);
|
||||
|
||||
// Here we define an executable. An executable needs to have a root module
|
||||
// which needs to expose a `main` function. While we could add a main function
|
||||
// to the module defined above, it's sometimes preferable to split business
|
||||
// logic and the CLI into two separate modules.
|
||||
//
|
||||
// If your goal is to create a Zig library for others to use, consider if
|
||||
// it might benefit from also exposing a CLI tool. A parser library for a
|
||||
// data serialization format could also bundle a CLI syntax checker, for example.
|
||||
//
|
||||
// If instead your goal is to create an executable, consider if users might
|
||||
// be interested in also being able to embed the core functionality of your
|
||||
// program in their own executable in order to avoid the overhead involved in
|
||||
// subprocessing your CLI tool.
|
||||
//
|
||||
// If neither case applies to you, feel free to delete the declaration you
|
||||
// don't need and to put everything under a single module.
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "sokol_template_v2",
|
||||
.root_module = b.createModule(.{
|
||||
// b.createModule defines a new module just like b.addModule but,
|
||||
// unlike b.addModule, it does not expose the module to consumers of
|
||||
// this package, which is why in this case we don't have to give it a name.
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
// Target and optimization levels must be explicitly wired in when
|
||||
// defining an executable or library (in the root module), and you
|
||||
// can also hardcode a specific target for an executable or library
|
||||
// definition if desireable (e.g. firmware for embedded devices).
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
// List of modules available for import in source files part of the
|
||||
// root module.
|
||||
.imports = &.{
|
||||
// Here "sokol_template_v2" is the name you will use in your source code to
|
||||
// import this module (e.g. `@import("sokol_template_v2")`). The name is
|
||||
// repeated because you are allowed to rename your imports, which
|
||||
// can be extremely useful in case of collisions (which can happen
|
||||
// importing modules from different packages).
|
||||
.{ .name = "sokol_template_v2", .module = mod },
|
||||
},
|
||||
}),
|
||||
const plugin_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/plugin.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize
|
||||
});
|
||||
|
||||
// This declares intent for the executable to be installed into the
|
||||
// install prefix when running `zig build` (i.e. when executing the default
|
||||
// step). By default the install prefix is `zig-out/` but can be overridden
|
||||
// by passing `--prefix` or `-p`.
|
||||
b.installArtifact(exe);
|
||||
|
||||
// This creates a top level step. Top level steps have a name and can be
|
||||
// invoked by name when running `zig build` (e.g. `zig build run`).
|
||||
// This will evaluate the `run` step rather than the default step.
|
||||
// For a top level step to actually do something, it must depend on other
|
||||
// steps (e.g. a Run step, as we will see in a moment).
|
||||
const run_step = b.step("run", "Run the app");
|
||||
|
||||
// This creates a RunArtifact step in the build graph. A RunArtifact step
|
||||
// invokes an executable compiled by Zig. Steps will only be executed by the
|
||||
// runner if invoked directly by the user (in the case of top level steps)
|
||||
// or if another step depends on it, so it's up to you to define when and
|
||||
// how this Run step will be executed. In our case we want to run it when
|
||||
// the user runs `zig build run`, so we create a dependency link.
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
// By making the run step depend on the default step, it will be run from the
|
||||
// installation directory rather than directly from within the cache directory.
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
|
||||
// This allows the user to pass arguments to the application in the build
|
||||
// command itself, like this: `zig build run -- arg1 arg2 etc`
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
plugin_mod.addImport("plugin_api", plugin_api_mod);
|
||||
if (plugin_static_enabled) {
|
||||
exe_mod.addImport("plugin", plugin_mod);
|
||||
}
|
||||
|
||||
// Creates an executable that will run `test` blocks from the provided module.
|
||||
// Here `mod` needs to define a target, which is why earlier we made sure to
|
||||
// set the releative field.
|
||||
const mod_tests = b.addTest(.{
|
||||
.root_module = mod,
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "sokol_template_v2",
|
||||
.root_module = exe_mod,
|
||||
});
|
||||
|
||||
// A run step that will run the test executable.
|
||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||
|
||||
// Creates an executable that will run `test` blocks from the executable's
|
||||
// root module. Note that test executables only test one module at a time,
|
||||
// hence why we have to create two separate ones.
|
||||
const exe_tests = b.addTest(.{
|
||||
.root_module = exe.root_module,
|
||||
const plugin_dynamic = b.addLibrary(.{
|
||||
.name = "plugin",
|
||||
.linkage = .dynamic,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/plugin_dynamic.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize
|
||||
})
|
||||
});
|
||||
plugin_dynamic.root_module.addImport("plugin_api", plugin_api_mod);
|
||||
plugin_dynamic.root_module.addImport("plugin", plugin_mod);
|
||||
|
||||
// A run step that will run the second test executable.
|
||||
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||
const plugin_install_artifact = b.addInstallArtifact(plugin_dynamic, .{ });
|
||||
if (plugin_dynamic_enabled) {
|
||||
exe.step.dependOn(&plugin_install_artifact.step);
|
||||
}
|
||||
|
||||
// A top level step for running all tests. dependOn can be called multiple
|
||||
// times and since the two run steps do not depend on one another, this will
|
||||
// make the two of them run in parallel.
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_mod_tests.step);
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
b.installArtifact(exe);
|
||||
|
||||
// Just like flags, top level steps are also listed in the `--help` menu.
|
||||
//
|
||||
// The Zig build system is entirely implemented in userland, which means
|
||||
// that it cannot hook into private compiler APIs. All compilation work
|
||||
// orchestrated by the build system will result in other Zig compiler
|
||||
// subcommands being invoked with the right flags defined. You can observe
|
||||
// these invocations when one fails (or you pass a flag to increase
|
||||
// verbosity) to validate assumptions and diagnose problems.
|
||||
//
|
||||
// Lastly, the Zig build system is relatively simple and self-contained,
|
||||
// and reading its source code will allow you to master it.
|
||||
{
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (plugin_dynamic_enabled) {
|
||||
run_cmd.addArg("--plugin-path");
|
||||
run_cmd.addArg(b.getInstallPath(.lib, plugin_dynamic.out_lib_filename));
|
||||
}
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}
|
||||
|
||||
{
|
||||
const run_step = b.step("build-plugin", "Build plugin");
|
||||
run_step.dependOn(&plugin_install_artifact.step);
|
||||
}
|
||||
|
||||
{
|
||||
const exe_tests = b.addTest(.{ .root_module = exe_mod, });
|
||||
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,81 +1,12 @@
|
||||
.{
|
||||
// This is the default name used by packages depending on this one. For
|
||||
// example, when a user runs `zig fetch --save <url>`, this field is used
|
||||
// as the key in the `dependencies` table. Although the user can choose a
|
||||
// different name, most users will stick with this provided value.
|
||||
//
|
||||
// It is redundant to include "zig" in this name because it is already
|
||||
// within the Zig package namespace.
|
||||
.name = .sokol_template_v2,
|
||||
// This is a [Semantic Version](https://semver.org/).
|
||||
// In a future version of Zig it will be used for package deduplication.
|
||||
.version = "0.0.0",
|
||||
// Together with name, this represents a globally unique package
|
||||
// identifier. This field is generated by the Zig toolchain when the
|
||||
// package is first created, and then *never changes*. This allows
|
||||
// unambiguous detection of one package being an updated version of
|
||||
// another.
|
||||
//
|
||||
// When forking a Zig project, this id should be regenerated (delete the
|
||||
// field and run `zig build`) if the upstream project is still maintained.
|
||||
// Otherwise, the fork is *hostile*, attempting to take control over the
|
||||
// original project's identity. Thus it is recommended to leave the comment
|
||||
// on the following line intact, so that it shows up in code reviews that
|
||||
// modify the field.
|
||||
.fingerprint = 0xf440b4e6e51f5206, // Changing this has security and trust implications.
|
||||
// Tracks the earliest Zig version that the package considers to be a
|
||||
// supported use case.
|
||||
.minimum_zig_version = "0.16.0",
|
||||
// This field is optional.
|
||||
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
|
||||
// Once all dependencies are fetched, `zig build` no longer requires
|
||||
// internet connectivity.
|
||||
.dependencies = .{
|
||||
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
|
||||
//.example = .{
|
||||
// // When updating this field to a new URL, be sure to delete the corresponding
|
||||
// // `hash`, otherwise you are communicating that you expect to find the old hash at
|
||||
// // the new URL. If the contents of a URL change this will result in a hash mismatch
|
||||
// // which will prevent zig from using it.
|
||||
// .url = "https://example.com/foo.tar.gz",
|
||||
//
|
||||
// // This is computed from the file contents of the directory of files that is
|
||||
// // obtained after fetching `url` and applying the inclusion rules given by
|
||||
// // `paths`.
|
||||
// //
|
||||
// // This field is the source of truth; packages do not come from a `url`; they
|
||||
// // come from a `hash`. `url` is just one of many possible mirrors for how to
|
||||
// // obtain a package matching this `hash`.
|
||||
// //
|
||||
// // Uses the [multihash](https://multiformats.io/multihash/) format.
|
||||
// .hash = "...",
|
||||
//
|
||||
// // When this is provided, the package is found in a directory relative to the
|
||||
// // build root. In this case the package's hash is irrelevant and therefore not
|
||||
// // computed. This field and `url` are mutually exclusive.
|
||||
// .path = "foo",
|
||||
//
|
||||
// // When this is set to `true`, a package is declared to be lazily
|
||||
// // fetched. This makes the dependency only get fetched if it is
|
||||
// // actually used.
|
||||
// .lazy = false,
|
||||
//},
|
||||
},
|
||||
// Specifies the set of files and directories that are included in this package.
|
||||
// Only files and directories listed here are included in the `hash` that
|
||||
// is computed for this package. Only files listed here will remain on disk
|
||||
// when using the zig package manager. As a rule of thumb, one should list
|
||||
// files required for compilation plus any license(s).
|
||||
// Paths are relative to the build root. Use the empty string (`""`) to refer to
|
||||
// the build root itself.
|
||||
// A directory listed here means that all files within, recursively, are included.
|
||||
.dependencies = .{},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
// For example...
|
||||
//"LICENSE",
|
||||
//"README.md",
|
||||
},
|
||||
}
|
||||
|
||||
301
src/cli.zig
Normal file
301
src/cli.zig
Normal file
@ -0,0 +1,301 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const log = std.log.scoped(.engine);
|
||||
|
||||
const CLI = @This();
|
||||
|
||||
gpa: std.mem.Allocator,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
options: std.ArrayList(Option),
|
||||
commands: std.ArrayList(Command),
|
||||
|
||||
stdout: Channel,
|
||||
stderr: Channel,
|
||||
|
||||
pub const Command = struct {
|
||||
name: []const u8,
|
||||
description: []const u8,
|
||||
|
||||
const Id = enum (u32) { _ };
|
||||
};
|
||||
|
||||
pub const Option = struct {
|
||||
long_name: []const u8,
|
||||
description: []const u8,
|
||||
kind: Kind,
|
||||
|
||||
pub const Kind = enum {
|
||||
toggle,
|
||||
single_argument
|
||||
};
|
||||
|
||||
pub const Id = enum (u32) { _ };
|
||||
|
||||
const Value = struct {
|
||||
argument: ?[]const u8 = null
|
||||
};
|
||||
};
|
||||
|
||||
const Channel = struct {
|
||||
file: Io.File,
|
||||
buffer: [4 * 4096]u8,
|
||||
writer: Io.File.Writer,
|
||||
|
||||
pub fn init(self: *Channel, io: Io, file: Io.File) void {
|
||||
self.* = Channel{
|
||||
.file = file,
|
||||
.buffer = undefined,
|
||||
.writer = file.writer(io, &self.buffer)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(self: *Channel, comptime fmt: []const u8, args: anytype) void {
|
||||
self.writer.interface.print(fmt, args) catch |e| {
|
||||
log.err("Failed to write to channel: {}", .{e});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn flush(self: *Channel) void {
|
||||
self.writer.interface.flush() catch |e| {
|
||||
log.err("Failed to flush channel: {}", .{e});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(self: *CLI, io: Io, gpa: std.mem.Allocator) void {
|
||||
self.* = CLI{
|
||||
.gpa = gpa,
|
||||
.arena = .init(gpa),
|
||||
.options = .empty,
|
||||
.commands = .empty,
|
||||
.stdout = undefined,
|
||||
.stderr = undefined
|
||||
};
|
||||
self.stdout.init(io, Io.File.stdout());
|
||||
self.stderr.init(io, Io.File.stderr());
|
||||
}
|
||||
|
||||
pub fn deinit(self: *CLI) void {
|
||||
self.stderr.flush();
|
||||
self.stdout.flush();
|
||||
|
||||
self.arena.deinit();
|
||||
|
||||
self.options.deinit(self.gpa);
|
||||
self.commands.deinit(self.gpa);
|
||||
}
|
||||
|
||||
pub const AddOptionOptions = struct {
|
||||
long_name: []const u8,
|
||||
description: ?[]const u8 = null,
|
||||
kind: Option.Kind = .toggle,
|
||||
};
|
||||
|
||||
pub fn addOption(self: *CLI, opts: AddOptionOptions) !Option.Id {
|
||||
const arena = self.arena.allocator();
|
||||
|
||||
var owned_description: []const u8 = "";
|
||||
if (opts.description) |description| {
|
||||
owned_description = try arena.dupe(u8, description);
|
||||
}
|
||||
|
||||
const index = self.options.items.len;
|
||||
try self.options.append(self.gpa, .{
|
||||
.long_name = try arena.dupe(u8, opts.long_name),
|
||||
.description = owned_description,
|
||||
.kind = opts.kind
|
||||
});
|
||||
|
||||
return @enumFromInt(index);
|
||||
}
|
||||
|
||||
fn getOptionByLongName(self: *CLI, long_name: []const u8) ?Option.Id {
|
||||
for (0.., self.options.items) |i, option| {
|
||||
if (std.mem.eql(u8, long_name, option.long_name)) {
|
||||
return @enumFromInt(i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub const AddCommandOptions = struct {
|
||||
name: []const u8,
|
||||
description: ?[]const u8 = null
|
||||
};
|
||||
|
||||
pub fn addCommand(self: *CLI, opts: AddCommandOptions) !Command.Id {
|
||||
const arena = self.arena.allocator();
|
||||
|
||||
var owned_description: []const u8 = "";
|
||||
if (opts.description) |description| {
|
||||
owned_description = try arena.dupe(u8, description);
|
||||
}
|
||||
|
||||
const index = self.commands.items.len;
|
||||
try self.commands.append(self.gpa, Command{
|
||||
.name = try arena.dupe(u8, opts.name),
|
||||
.description = owned_description,
|
||||
});
|
||||
|
||||
return @enumFromInt(index);
|
||||
}
|
||||
|
||||
pub fn showUsage(self: *CLI, program_name: []const u8) void {
|
||||
self.stderr.write("Usage: {s}", .{program_name});
|
||||
if (self.options.items.len > 0) {
|
||||
self.stderr.write(" [options]", .{});
|
||||
}
|
||||
if (self.commands.items.len > 0) {
|
||||
self.stderr.write(" <command>", .{});
|
||||
}
|
||||
self.stderr.write("\n", .{});
|
||||
|
||||
if (self.commands.items.len > 0) {
|
||||
self.stderr.write("\n", .{});
|
||||
self.stderr.write("Commands:\n", .{});
|
||||
for (self.commands.items) |command| {
|
||||
self.stderr.write(" {s} {s}\n", .{command.name, command.description});
|
||||
}
|
||||
}
|
||||
|
||||
if (self.options.items.len > 0) {
|
||||
self.stderr.write("\n", .{});
|
||||
self.stderr.write("Options:\n", .{});
|
||||
|
||||
for (0.., self.options.items) |i, option| {
|
||||
if (i > 0) {
|
||||
self.stderr.write("\n", .{});
|
||||
}
|
||||
|
||||
self.stderr.write(" --{s}", .{option.long_name});
|
||||
if (option.kind == .single_argument) {
|
||||
// TODO: Make this hint renameable
|
||||
self.stderr.write(" <value>", .{});
|
||||
}
|
||||
self.stderr.write("\n", .{});
|
||||
if (option.description.len > 0) {
|
||||
self.stderr.write(" {s}\n", .{option.description});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.stderr.flush();
|
||||
}
|
||||
|
||||
pub const ParseResult = struct {
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
command: ?Command.Id,
|
||||
options: []?Option.Value,
|
||||
|
||||
pub fn init(gpa: std.mem.Allocator, options_len: usize) !ParseResult {
|
||||
var arena = std.heap.ArenaAllocator.init(gpa);
|
||||
errdefer arena.deinit();
|
||||
|
||||
const options = try arena.allocator().alloc(?Option.Value, options_len);
|
||||
@memset(options, null);
|
||||
|
||||
return ParseResult{
|
||||
.arena = arena,
|
||||
.command = null,
|
||||
.options = options
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ParseResult) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
pub fn getOption(self: ParseResult, id: Option.Id) ?Option.Value {
|
||||
return self.options[@intFromEnum(id)];
|
||||
}
|
||||
|
||||
pub fn isSet(self: ParseResult, id: Option.Id) bool {
|
||||
return self.getOption(id) != null;
|
||||
}
|
||||
|
||||
// TODO: I don't like this function name
|
||||
pub fn getOptionArgument(self: ParseResult, id: Option.Id) ?[]const u8 {
|
||||
if (self.getOption(id)) |option| {
|
||||
return option.argument;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const Parser = struct {
|
||||
args: []const []const u8,
|
||||
cursor: u32,
|
||||
|
||||
pub fn peek(self: *Parser) ?[]const u8 {
|
||||
if (self.cursor < self.args.len) {
|
||||
return self.args[self.cursor];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn take(self: *Parser) ?[]const u8 {
|
||||
if (self.peek()) |value| {
|
||||
self.cursor += 1;
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
fn printErrorAndExit(self: *CLI, comptime fmt: []const u8, args: anytype) noreturn {
|
||||
self.stderr.write("ERROR: " ++ fmt ++ "\n", args);
|
||||
self.stderr.flush();
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
// TODO: I don't like the fact that this call `std.process.exit()`
|
||||
// But for now this is the most convenient thing.
|
||||
pub fn parse(self: *CLI, gpa: std.mem.Allocator, args: []const []const u8) !ParseResult {
|
||||
var result = try ParseResult.init(gpa, self.options.items.len);
|
||||
errdefer result.deinit();
|
||||
|
||||
var parser = Parser{
|
||||
.args = args,
|
||||
.cursor = 1
|
||||
};
|
||||
|
||||
next_arg: while (parser.take()) |arg| {
|
||||
if (std.mem.startsWith(u8, arg, "--")) {
|
||||
const long_option_name = arg[2..];
|
||||
if (self.getOptionByLongName(long_option_name)) |option_id| {
|
||||
const option = self.options.items[@intFromEnum(option_id)];
|
||||
var result_value = Option.Value{};
|
||||
|
||||
if (option.kind == .single_argument) {
|
||||
const argument = parser.take() orelse {
|
||||
self.printErrorAndExit("Missing required argument for '{s}'", .{arg});
|
||||
};
|
||||
|
||||
result_value.argument = argument;
|
||||
}
|
||||
|
||||
result.options[@intFromEnum(option_id)] = result_value;
|
||||
continue :next_arg;
|
||||
}
|
||||
|
||||
self.printErrorAndExit("Unknown option '{s}'", .{arg});
|
||||
}
|
||||
|
||||
if (result.command == null) {
|
||||
for (0.., self.commands.items) |i, command| {
|
||||
if (std.mem.eql(u8, command.name, arg)) {
|
||||
result.command = @enumFromInt(i);
|
||||
continue :next_arg;
|
||||
}
|
||||
}
|
||||
|
||||
self.printErrorAndExit("Unknown command '{s}'", .{arg});
|
||||
} else {
|
||||
self.printErrorAndExit("Unknown argument '{s}'", .{arg});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
312
src/file_watcher.zig
Normal file
312
src/file_watcher.zig
Normal file
@ -0,0 +1,312 @@
|
||||
const std = @import("std");
|
||||
const INotify = @import("inotify.zig");
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const FileWatcher = @This();
|
||||
|
||||
root_dir: []u8,
|
||||
inotify: INotify,
|
||||
files: std.ArrayList(File),
|
||||
watches: std.ArrayList(Watch),
|
||||
|
||||
const File = struct {
|
||||
path: []u8,
|
||||
debounce: Io.Duration,
|
||||
last_event_at: ?Io.Timestamp,
|
||||
|
||||
pub fn deinit(self: File, gpa: Allocator) void {
|
||||
gpa.free(self.path);
|
||||
}
|
||||
};
|
||||
|
||||
const Watch = struct {
|
||||
wd: ?u32,
|
||||
path: []u8,
|
||||
|
||||
pub fn deinit(self: Watch, gpa: Allocator) void {
|
||||
gpa.free(self.path);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(gpa: Allocator, root_dir: []const u8) !FileWatcher {
|
||||
if (!std.fs.path.isAbsolute(root_dir)) {
|
||||
return error.PathIsNotAbsolute;
|
||||
}
|
||||
|
||||
const inotify_buf = try gpa.alloc(u8, INotify.max_event_size * 10);
|
||||
errdefer gpa.free(inotify_buf);
|
||||
|
||||
const root_dir_owned = try gpa.dupe(u8, root_dir);
|
||||
errdefer gpa.free(root_dir_owned);
|
||||
|
||||
return FileWatcher{
|
||||
.root_dir = root_dir_owned,
|
||||
.inotify = try .init(inotify_buf),
|
||||
.files = .empty,
|
||||
.watches = .empty
|
||||
};
|
||||
}
|
||||
|
||||
fn findFileIndex(self: *FileWatcher, path: []const u8) ?usize {
|
||||
for (0.., self.files.items) |i, file| {
|
||||
if (std.mem.eql(u8, file.path, path)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn findFile(self: *FileWatcher, path: []const u8) ?*File {
|
||||
if (self.findFileIndex(path)) |file_index| {
|
||||
return &self.files.items[file_index];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
fn findWatchByPath(self: *FileWatcher, path: []const u8) ?usize {
|
||||
for (0.., self.watches.items) |i, watch| {
|
||||
if (std.mem.eql(u8, watch.path, path)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn findWatchByDescriptor(self: *FileWatcher, wd: u32) ?usize {
|
||||
for (0.., self.watches.items) |i, watch| {
|
||||
if (watch.wd == wd) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn bufJoinPaths(path_buff: []u8, paths: []const []const u8) ![:0]u8 {
|
||||
return try std.fmt.bufPrintZ(path_buff, "{f}", .{
|
||||
std.fs.path.fmtJoin(paths)
|
||||
});
|
||||
}
|
||||
|
||||
fn attemptRegisteringWatch(self: *FileWatcher, watch: *Watch) !void {
|
||||
if (watch.wd != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
|
||||
const path = try bufJoinPaths(&path_buff, &.{ self.root_dir, watch.path });
|
||||
|
||||
watch.wd = self.inotify.add(path, INotify.Mask{
|
||||
.create = true,
|
||||
.modify = true,
|
||||
.delete_self = true,
|
||||
.move_self = true,
|
||||
.moved_to = true,
|
||||
.moved_from = true,
|
||||
}) catch |err| switch (err) {
|
||||
error.FileNotFound => null,
|
||||
else => return err
|
||||
};
|
||||
}
|
||||
|
||||
fn isInside(outer_path: []const u8, inner_path: []const u8) bool {
|
||||
if (inner_path.len == outer_path.len) {
|
||||
return std.mem.eql(u8, inner_path, outer_path);
|
||||
} else if (inner_path.len > outer_path.len) {
|
||||
return std.mem.startsWith(u8, inner_path, outer_path) and inner_path[outer_path.len] == std.fs.path.sep;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fn unregisterWatchesRecursively(self: *FileWatcher, dir: []const u8) !void {
|
||||
for (self.watches.items) |*watch| {
|
||||
const wd = watch.wd orelse continue;
|
||||
|
||||
if (isInside(dir, watch.path)) {
|
||||
try self.inotify.remove(wd);
|
||||
watch.wd = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn registerWatchesRecursively(self: *FileWatcher, dir: []const u8) !void {
|
||||
for (self.watches.items) |*watch| {
|
||||
if (watch.wd == null and isInside(dir, watch.path)) {
|
||||
try self.attemptRegisteringWatch(watch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensureWatchExists(self: *FileWatcher, gpa: Allocator, path: []const u8) !usize {
|
||||
var watch_index: usize = undefined;
|
||||
if (self.findWatchByPath(path)) |found_watch_index| {
|
||||
watch_index = found_watch_index;
|
||||
} else {
|
||||
try self.watches.ensureUnusedCapacity(gpa, 1);
|
||||
|
||||
const path_owned = try gpa.dupe(u8, path);
|
||||
errdefer gpa.free(path_owned);
|
||||
|
||||
watch_index = self.watches.items.len;
|
||||
self.watches.appendAssumeCapacity(Watch{
|
||||
.wd = null,
|
||||
.path = path_owned
|
||||
});
|
||||
}
|
||||
|
||||
try self.attemptRegisteringWatch(&self.watches.items[watch_index]);
|
||||
|
||||
return watch_index;
|
||||
}
|
||||
|
||||
fn removeWatch(self: *FileWatcher, gpa: Allocator, index: usize) void {
|
||||
const watch = self.watches.swapRemove(index);
|
||||
watch.deinit(gpa);
|
||||
}
|
||||
|
||||
pub fn add(self: *FileWatcher, gpa: Allocator, path: []const u8, debounce: Io.Duration) !void {
|
||||
if (std.fs.path.isAbsolute(path)) {
|
||||
return error.PathIsAbsolute;
|
||||
}
|
||||
|
||||
if (self.findFileIndex(path) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dir_iter: ?[]const u8 = path;
|
||||
while (dir_iter) |current| {
|
||||
_ = try self.ensureWatchExists(gpa, current);
|
||||
|
||||
dir_iter = std.fs.path.dirname(current);
|
||||
}
|
||||
|
||||
_ = try self.ensureWatchExists(gpa, "");
|
||||
|
||||
try self.files.ensureUnusedCapacity(gpa, 1);
|
||||
|
||||
const path_owned = try gpa.dupe(u8, path);
|
||||
errdefer gpa.free(path_owned);
|
||||
|
||||
self.files.appendAssumeCapacity(File{
|
||||
.path = path_owned,
|
||||
.debounce = debounce,
|
||||
.last_event_at = null
|
||||
});
|
||||
}
|
||||
|
||||
pub fn remove(self: *FileWatcher, gpa: Allocator, path: []const u8) void {
|
||||
const index = self.findFileIndex(path);
|
||||
if (index == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = self.files.items[index];
|
||||
self.files.swapRemove(index);
|
||||
|
||||
file.deinit(gpa);
|
||||
}
|
||||
|
||||
fn nextEvents(self: *FileWatcher) !?*File {
|
||||
while (try self.inotify.next()) |e| {
|
||||
const watch_index = self.findWatchByDescriptor(e.wd) orelse continue;
|
||||
var watch = &self.watches.items[watch_index];
|
||||
|
||||
if (e.mask.ignored) {
|
||||
watch.wd = null;
|
||||
|
||||
} else if (e.mask.q_overflow) {
|
||||
return error.QueueOverflow;
|
||||
|
||||
} else if (e.mask.delete_self or e.mask.modify) {
|
||||
if (self.findFile(watch.path)) |file| {
|
||||
return file;
|
||||
}
|
||||
|
||||
} else if (e.mask.create) {
|
||||
assert(e.name != null);
|
||||
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
|
||||
const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) });
|
||||
if (self.findWatchByPath(path)) |child_watch_index| {
|
||||
try self.attemptRegisteringWatch(&self.watches.items[child_watch_index]);
|
||||
}
|
||||
|
||||
if (self.findFile(path)) |file| {
|
||||
return file;
|
||||
}
|
||||
|
||||
} else if (e.mask.moved_from) {
|
||||
assert(e.name != null);
|
||||
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
|
||||
const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) });
|
||||
try self.unregisterWatchesRecursively(path);
|
||||
|
||||
if (self.findFile(path)) |file| {
|
||||
return file;
|
||||
}
|
||||
|
||||
} else if (e.mask.moved_to) {
|
||||
assert(e.name != null);
|
||||
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
|
||||
const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) });
|
||||
try self.registerWatchesRecursively(path);
|
||||
|
||||
if (self.findFile(path)) |file| {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn next(self: *FileWatcher, io: Io) !?[]const u8 {
|
||||
const now = Io.Clock.real.now(io);
|
||||
|
||||
var queue_overflow = false;
|
||||
while (true) {
|
||||
const file = (self.nextEvents() catch |err| switch (err) {
|
||||
error.QueueOverflow => {
|
||||
queue_overflow = true;
|
||||
break;
|
||||
},
|
||||
else => return err
|
||||
}) orelse break;
|
||||
file.last_event_at = now;
|
||||
}
|
||||
|
||||
if (queue_overflow) {
|
||||
for (self.files.items) |*file| {
|
||||
file.last_event_at = now;
|
||||
}
|
||||
}
|
||||
|
||||
for (self.files.items) |*file| {
|
||||
const last_event_at = file.last_event_at orelse continue;
|
||||
if (now.nanoseconds > last_event_at.addDuration(file.debounce).nanoseconds) {
|
||||
file.last_event_at = null;
|
||||
return file.path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *FileWatcher, gpa: Allocator) void {
|
||||
gpa.free(self.root_dir);
|
||||
|
||||
gpa.free(self.inotify.buf);
|
||||
self.inotify.deinit();
|
||||
|
||||
for (self.files.items) |file| {
|
||||
file.deinit(gpa);
|
||||
}
|
||||
self.files.deinit(gpa);
|
||||
|
||||
for (self.watches.items) |watch| {
|
||||
watch.deinit(gpa);
|
||||
}
|
||||
self.watches.deinit(gpa);
|
||||
}
|
||||
186
src/inotify.zig
Normal file
186
src/inotify.zig
Normal file
@ -0,0 +1,186 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const assert = std.debug.assert;
|
||||
const linux = std.os.linux;
|
||||
|
||||
const INotify = @This();
|
||||
|
||||
fd: i32,
|
||||
|
||||
buf: []u8,
|
||||
buf_offset: usize,
|
||||
buf_used: usize,
|
||||
|
||||
pub const Mask = packed struct(u32) {
|
||||
access: bool = false,
|
||||
modify: bool = false,
|
||||
attrib: bool = false,
|
||||
close_write: bool = false,
|
||||
close_nowrite: bool = false,
|
||||
open: bool = false,
|
||||
moved_from: bool = false,
|
||||
moved_to: bool = false,
|
||||
create: bool = false,
|
||||
delete: bool = false,
|
||||
delete_self: bool = false,
|
||||
move_self: bool = false,
|
||||
|
||||
_unused1: u1 = 0,
|
||||
|
||||
unmount: bool = false,
|
||||
q_overflow: bool = false,
|
||||
ignored: bool = false,
|
||||
|
||||
_unused2: u8 = 0,
|
||||
|
||||
onlydir: bool = false,
|
||||
dont_follow: bool = false,
|
||||
excl_unlink: bool = false,
|
||||
|
||||
_unused3: u1 = 0,
|
||||
|
||||
mask_create: bool = false,
|
||||
mask_add: bool = false,
|
||||
isdir: bool = false,
|
||||
oneshot: bool = false,
|
||||
|
||||
pub fn format(self: Mask, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
try writer.writeAll(".{");
|
||||
var first = true;
|
||||
inline for (@typeInfo(Mask).@"struct".fields) |field| {
|
||||
if (@typeInfo(field.type) == .bool and @field(self, field.name)) {
|
||||
if (!first) {
|
||||
try writer.writeAll(",");
|
||||
}
|
||||
try writer.print(" .{s} = true", .{field.name});
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
|
||||
fn testMask(expected: u32, actual: Mask) !void {
|
||||
try std.testing.expectEqual(expected, @as(u32, @bitCast(actual)));
|
||||
}
|
||||
|
||||
test {
|
||||
try testMask(linux.IN.ACCESS, Mask{ .access = true });
|
||||
try testMask(linux.IN.MODIFY, Mask{ .modify = true });
|
||||
try testMask(linux.IN.ATTRIB, Mask{ .attrib = true });
|
||||
try testMask(linux.IN.CLOSE_WRITE, Mask{ .close_write = true });
|
||||
try testMask(linux.IN.CLOSE_NOWRITE, Mask{ .close_nowrite = true });
|
||||
try testMask(linux.IN.OPEN, Mask{ .open = true });
|
||||
try testMask(linux.IN.MOVED_FROM, Mask{ .moved_from = true });
|
||||
try testMask(linux.IN.MOVED_TO, Mask{ .moved_to = true });
|
||||
try testMask(linux.IN.CREATE, Mask{ .create = true });
|
||||
try testMask(linux.IN.DELETE, Mask{ .delete = true });
|
||||
try testMask(linux.IN.DELETE_SELF, Mask{ .delete_self = true });
|
||||
try testMask(linux.IN.MOVE_SELF, Mask{ .move_self = true });
|
||||
try testMask(linux.IN.UNMOUNT, Mask{ .unmount = true });
|
||||
try testMask(linux.IN.Q_OVERFLOW, Mask{ .q_overflow = true });
|
||||
try testMask(linux.IN.IGNORED, Mask{ .ignored = true });
|
||||
try testMask(linux.IN.ONLYDIR, Mask{ .onlydir = true });
|
||||
try testMask(linux.IN.DONT_FOLLOW, Mask{ .dont_follow = true });
|
||||
try testMask(linux.IN.EXCL_UNLINK, Mask{ .excl_unlink = true });
|
||||
try testMask(linux.IN.MASK_CREATE, Mask{ .mask_create = true });
|
||||
try testMask(linux.IN.MASK_ADD, Mask{ .mask_add = true });
|
||||
try testMask(linux.IN.ISDIR, Mask{ .isdir = true });
|
||||
try testMask(linux.IN.ONESHOT, Mask{ .oneshot = true });
|
||||
}
|
||||
};
|
||||
|
||||
pub const Event = struct {
|
||||
wd: u32,
|
||||
mask: Mask,
|
||||
cookie: u32,
|
||||
name: ?[*:0]const u8,
|
||||
};
|
||||
|
||||
pub const max_event_size = @sizeOf(linux.inotify_event) + linux.NAME_MAX + 1;
|
||||
|
||||
pub fn init(buf: []u8) !INotify {
|
||||
const fd = linux.inotify_init1(linux.IN.NONBLOCK);
|
||||
if (linux.errno(fd) != .SUCCESS) {
|
||||
return error.InotifyInit;
|
||||
}
|
||||
return INotify{
|
||||
.fd = @intCast(fd),
|
||||
.buf = buf,
|
||||
.buf_offset = 0,
|
||||
.buf_used = 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn add(self: INotify, path: [*:0]const u8, mask: Mask) !u32 {
|
||||
const wd = linux.inotify_add_watch(
|
||||
self.fd,
|
||||
path,
|
||||
@bitCast(mask)
|
||||
);
|
||||
|
||||
switch (linux.errno(wd)) {
|
||||
.SUCCESS => return @intCast(wd),
|
||||
.ACCES => return error.AccessDenied,
|
||||
.BADF => unreachable, // The given file descriptor is not valid.
|
||||
.EXIST => return error.MarkAlreadyExists,
|
||||
.FAULT => unreachable, // path points outside of the process's accessible address space.
|
||||
.INVAL => unreachable, // The given event mask contains no valid events;
|
||||
.NAMETOOLONG => return error.NameTooLong,
|
||||
.NOENT => return error.FileNotFound,
|
||||
.NOMEM => return error.SystemResources,
|
||||
.NOSPC => return error.UserMarkQuotaExceeded,
|
||||
.NOTDIR => return error.NotDir,
|
||||
else => |err| return std.posix.unexpectedErrno(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(self: INotify, wd: u32) !void {
|
||||
const rc = linux.inotify_rm_watch(self.fd, @bitCast(wd));
|
||||
if (linux.errno(rc) != .SUCCESS) {
|
||||
return error.InotifyRemoveWatch;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(self: *INotify) !?Event {
|
||||
if (self.buf_used <= self.buf.len) {
|
||||
const n = std.posix.read(self.fd, self.buf[self.buf_used..]) catch |err| switch(err) {
|
||||
error.WouldBlock => 0,
|
||||
else => return err
|
||||
};
|
||||
if (n > 0) {
|
||||
self.buf_used += n;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.buf_offset < self.buf_used) {
|
||||
const event = std.mem.bytesAsValue(
|
||||
linux.inotify_event,
|
||||
self.buf[self.buf_offset..][0..@sizeOf(linux.inotify_event)]
|
||||
);
|
||||
self.buf_offset += @sizeOf(linux.inotify_event) + event.len;
|
||||
assert(self.buf_offset <= self.buf_used);
|
||||
|
||||
if (self.buf_used == self.buf_offset) {
|
||||
self.buf_offset = 0;
|
||||
self.buf_used = 0;
|
||||
}
|
||||
|
||||
var name: ?[*:0]const u8 = null;
|
||||
if (event.len > 0) {
|
||||
name = std.mem.span(@as([*:0]const u8, @ptrCast(event)) + @sizeOf(linux.inotify_event));
|
||||
}
|
||||
|
||||
return Event{
|
||||
.wd = @bitCast(event.wd),
|
||||
.mask = @bitCast(event.mask),
|
||||
.cookie = event.cookie,
|
||||
.name = name
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: INotify) void {
|
||||
_ = linux.close(self.fd);
|
||||
}
|
||||
138
src/main.zig
138
src/main.zig
@ -1,71 +1,95 @@
|
||||
const std = @import("std");
|
||||
const CLI = @import("cli.zig");
|
||||
const build_options = @import("build_options");
|
||||
const builtin = @import("builtin");
|
||||
const FileWatcher = @import("file_watcher.zig");
|
||||
const Io = std.Io;
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const sokol_template_v2 = @import("sokol_template_v2");
|
||||
// TODO: zig build --watch --color off --summary none
|
||||
|
||||
pub fn main(init: std.process.Init) !void {
|
||||
// Prints to stderr, unbuffered, ignoring potential errors.
|
||||
std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
|
||||
const Plugin = @import("plugin_api").Plugin;
|
||||
|
||||
// This is appropriate for anything that lives as long as the process.
|
||||
const arena: std.mem.Allocator = init.arena.allocator();
|
||||
|
||||
// Accessing command line arguments:
|
||||
const args = try init.minimal.args.toSlice(arena);
|
||||
for (args) |arg| {
|
||||
std.log.info("arg: {s}", .{arg});
|
||||
pub fn getStaticPlugin() !Plugin {
|
||||
if (!build_options.plugin_static) {
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
// In order to do I/O operations need an `Io` instance.
|
||||
return @import("plugin").api;
|
||||
}
|
||||
|
||||
const DynamicPlugin = struct {
|
||||
lib: std.DynLib,
|
||||
plugin: Plugin,
|
||||
|
||||
pub fn init(path: []const u8) !DynamicPlugin {
|
||||
var lib = try std.DynLib.open(path);
|
||||
errdefer lib.close();
|
||||
|
||||
var plugin: Plugin = undefined;
|
||||
inline for (@typeInfo(Plugin).@"struct".fields) |field| {
|
||||
@field(plugin, field.name) = lib.lookup(field.type, field.name) orelse return error.SymbolNotFound;
|
||||
}
|
||||
|
||||
return DynamicPlugin{
|
||||
.lib = lib,
|
||||
.plugin = plugin
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *DynamicPlugin) void {
|
||||
self.lib.close();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main(init: std.process.Init) !void {
|
||||
// This is appropriate for anything that lives as long as the process.
|
||||
const arena: std.mem.Allocator = init.arena.allocator();
|
||||
const gpa = init.gpa;
|
||||
|
||||
const io = init.io;
|
||||
|
||||
// Stdout is for the actual output of your application, for example if you
|
||||
// are implementing gzip, then only the compressed bytes should be sent to
|
||||
// stdout, not any debugging messages.
|
||||
var stdout_buffer: [1024]u8 = undefined;
|
||||
var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
|
||||
const stdout_writer = &stdout_file_writer.interface;
|
||||
var cli: CLI = undefined;
|
||||
cli.init(io, init.gpa);
|
||||
defer cli.deinit();
|
||||
|
||||
try sokol_template_v2.printAnotherMessage(stdout_writer);
|
||||
const opt_plugin_path = try cli.addOption(.{
|
||||
.kind = .single_argument,
|
||||
.long_name = "plugin-path"
|
||||
});
|
||||
|
||||
try stdout_writer.flush(); // Don't forget to flush!
|
||||
const args = try init.minimal.args.toSlice(arena);
|
||||
var result = try cli.parse(gpa, args);
|
||||
defer result.deinit();
|
||||
|
||||
const plugin_path: ?[]const u8 = result.getOptionArgument(opt_plugin_path);
|
||||
|
||||
if (plugin_path != null) {
|
||||
var foo = try DynamicPlugin.init(plugin_path.?);
|
||||
defer foo.deinit();
|
||||
}
|
||||
|
||||
var cwd = Io.Dir.cwd();
|
||||
|
||||
const root_dir = try cwd.realPathFileAlloc(io, ".", gpa);
|
||||
defer gpa.free(root_dir);
|
||||
|
||||
var file_watcher = try FileWatcher.init(gpa, root_dir);
|
||||
defer file_watcher.deinit(gpa);
|
||||
|
||||
try file_watcher.add(gpa, "test.txt", .fromMilliseconds(150));
|
||||
try file_watcher.add(gpa, "foo/test.txt", .fromMilliseconds(150));
|
||||
|
||||
while (true) {
|
||||
while (try file_watcher.next(io)) |path| {
|
||||
std.debug.print("path: {s}\n", .{path});
|
||||
}
|
||||
|
||||
try io.sleep(.fromMilliseconds(100), .real);
|
||||
}
|
||||
}
|
||||
|
||||
test "simple test" {
|
||||
const gpa = std.testing.allocator;
|
||||
var list: std.ArrayList(i32) = .empty;
|
||||
defer list.deinit(gpa); // Try commenting this out and see if zig detects the memory leak!
|
||||
try list.append(gpa, 42);
|
||||
try std.testing.expectEqual(@as(i32, 42), list.pop());
|
||||
}
|
||||
|
||||
test "fuzz example" {
|
||||
try std.testing.fuzz({}, testOne, .{});
|
||||
}
|
||||
|
||||
fn testOne(context: void, smith: *std.testing.Smith) !void {
|
||||
_ = context;
|
||||
// Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
|
||||
|
||||
const gpa = std.testing.allocator;
|
||||
var list: std.ArrayList(u8) = .empty;
|
||||
defer list.deinit(gpa);
|
||||
while (!smith.eos()) switch (smith.value(enum { add_data, dup_data })) {
|
||||
.add_data => {
|
||||
const slice = try list.addManyAsSlice(gpa, smith.value(u4));
|
||||
smith.bytes(slice);
|
||||
},
|
||||
.dup_data => {
|
||||
if (list.items.len == 0) continue;
|
||||
if (list.items.len > std.math.maxInt(u32)) return error.SkipZigTest;
|
||||
const len = smith.valueRangeAtMost(u32, 1, @min(32, list.items.len));
|
||||
const off = smith.valueRangeAtMost(u32, 0, @intCast(list.items.len - len));
|
||||
try list.appendSlice(gpa, list.items[off..][0..len]);
|
||||
try std.testing.expectEqualSlices(
|
||||
u8,
|
||||
list.items[off..][0..len],
|
||||
list.items[list.items.len - len ..],
|
||||
);
|
||||
},
|
||||
};
|
||||
test {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
11
src/plugin.zig
Normal file
11
src/plugin.zig
Normal file
@ -0,0 +1,11 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const PluginAPI = @import("plugin_api");
|
||||
|
||||
fn hello() callconv(.c) u32 {
|
||||
return 1;
|
||||
}
|
||||
|
||||
pub const api = PluginAPI.Plugin{
|
||||
.hello = hello
|
||||
};
|
||||
26
src/plugin_api.zig
Normal file
26
src/plugin_api.zig
Normal file
@ -0,0 +1,26 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
pub const Plugin = struct {
|
||||
hello: *const fn() callconv(.c) u32,
|
||||
|
||||
comptime {
|
||||
for (@typeInfo(Plugin).@"struct".fields) |field| {
|
||||
assert(isPointerToCFunction(field.type));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn isPointerToCFunction(T: type) bool {
|
||||
if (@typeInfo(T) != .pointer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fnInfo = @typeInfo(@typeInfo(T).pointer.child);
|
||||
if (fnInfo != .@"fn") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const calling_convention = std.meta.activeTag(fnInfo.@"fn".calling_convention);
|
||||
return calling_convention == std.builtin.CallingConvention.c;
|
||||
}
|
||||
16
src/plugin_dynamic.zig
Normal file
16
src/plugin_dynamic.zig
Normal file
@ -0,0 +1,16 @@
|
||||
const plugin = @import("plugin");
|
||||
const PluginAPI = @import("plugin_api");
|
||||
|
||||
comptime {
|
||||
if (@hasField(plugin, "api")) {
|
||||
@compileError("Missing 'api' field");
|
||||
}
|
||||
|
||||
if (@TypeOf(plugin.api) != PluginAPI.Plugin) {
|
||||
@compileError("Incorrect 'api' field type");
|
||||
}
|
||||
|
||||
for (@typeInfo(PluginAPI.Plugin).@"struct".fields) |field| {
|
||||
@export(@field(plugin.api, field.name), .{ .name = field.name });
|
||||
}
|
||||
}
|
||||
18
src/root.zig
18
src/root.zig
@ -1,18 +0,0 @@
|
||||
//! By convention, root.zig is the root source file when making a package.
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
|
||||
/// This is a documentation comment to explain the `printAnotherMessage` function below.
|
||||
///
|
||||
/// Accepting an `Io.Writer` instance is a handy way to write reusable code.
|
||||
pub fn printAnotherMessage(writer: *Io.Writer) Io.Writer.Error!void {
|
||||
try writer.print("Run `zig build test` to run the tests.\n", .{});
|
||||
}
|
||||
|
||||
pub fn add(a: i32, b: i32) i32 {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
test "basic add functionality" {
|
||||
try std.testing.expect(add(3, 7) == 10);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user