pumpkin-api-zig/build.zig
MegalithOfficial 28c3e929ca
Add Zig bindings for the Pumpkin plugin API
The bindings under src/wit are generated from the pumpkin-plugin-wit
submodule by tools/gen.zig. The canonical ABI is handled at comptime in
src/abi.zig, so plugins are plain wasm32-freestanding modules and only
need wasm-tools to become a component.

On top of that there is a small hand written layer: plugin registration,
typed event handlers, declarative commands (cmd), text components from
struct literals (msg) and std.log support.
2026-09-19 17:46:24 +02:00

123 lines
4.6 KiB
Zig

const std = @import("std");
pub const PluginOptions = struct {
name: []const u8,
root_source_file: std.Build.LazyPath,
optimize: std.builtin.OptimizeMode = .ReleaseSmall,
};
/// Builds a plugin and installs `<name>.wasm`, ready to be dropped into the
/// server's `plugins` folder. Needs `wasm-tools` in PATH.
///
/// `api` is this package as the caller sees it:
/// const api = b.dependency("pumpkin_api_zig", .{});
pub fn addPlugin(b: *std.Build, api: *std.Build.Dependency, options: PluginOptions) *std.Build.Step.InstallFile {
return addPluginFrom(b, api.builder, options);
}
fn addPluginFrom(b: *std.Build, api: *std.Build, options: PluginOptions) *std.Build.Step.InstallFile {
const target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
const pumpkin = api.createModule(.{
.root_source_file = api.path("src/pumpkin.zig"),
.target = target,
.optimize = options.optimize,
});
const core = b.addExecutable(.{
.name = options.name,
.root_module = b.createModule(.{
.root_source_file = options.root_source_file,
.target = target,
.optimize = options.optimize,
.imports = &.{.{ .name = "pumpkin", .module = pumpkin }},
}),
});
core.entry = .disabled;
core.rdynamic = true;
// Zig has no component model target, so the core module gets its WIT
// attached and is wrapped into a component afterwards.
const embed = b.addSystemCommand(&.{ "wasm-tools", "component", "embed", "--world", "plugin" });
embed.addFileArg(api.path("src/plugin.wit"));
embed.addFileArg(core.getEmittedBin());
embed.addArg("-o");
const embedded = embed.addOutputFileArg("embedded.wasm");
const new = b.addSystemCommand(&.{ "wasm-tools", "component", "new" });
new.addFileArg(embedded);
new.addArg("-o");
const component = new.addOutputFileArg(b.fmt("{s}.wasm", .{options.name}));
const install = b.addInstallFile(component, b.fmt("{s}.wasm", .{options.name}));
b.getInstallStep().dependOn(&install.step);
return install;
}
pub fn build(b: *std.Build) void {
_ = addPluginFrom(b, b, .{
.name = "pumpkin-plugin-example",
.root_source_file = b.path("example/src/main.zig"),
});
const test_step = b.step("test", "Run unit tests");
for ([_][]const u8{ "src/abi.zig", "tools/gen.zig" }) |path| {
const tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path(path),
.target = b.graph.host,
}),
});
test_step.dependOn(&b.addRunArtifact(tests).step);
}
const check = b.addObject(.{
.name = "check",
.root_module = b.createModule(.{
.root_source_file = b.path("src/check.zig"),
.target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }),
.optimize = .ReleaseSmall,
}),
});
b.step("check", "Compile every generated binding").dependOn(&check.step);
// zig build gen: src/wit/ and src/plugin.wit from the wit submodule.
// Directory inputs aren't hashed, so these always rerun.
const dump = b.addSystemCommand(&.{ "wasm-tools", "component", "wit", "--json" });
dump.addDirectoryArg(b.path("wit/v0.1"));
dump.has_side_effects = true;
const json = dump.captureStdOut(.{});
const merge = b.addSystemCommand(&.{ "wasm-tools", "component", "wit", "-o", "src/plugin.wit" });
merge.addDirectoryArg(b.path("wit/v0.1"));
merge.setCwd(b.path("."));
merge.has_side_effects = true;
const generator = b.addExecutable(.{
.name = "gen",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/gen.zig"),
.target = b.graph.host,
}),
});
const generate = b.addRunArtifact(generator);
generate.addFileArg(json);
generate.addArg("src");
generate.setCwd(b.path("."));
generate.has_side_effects = true;
const fmt = b.addFmt(.{ .paths = &.{ "src/wit", "src/wit.zig" } });
fmt.step.dependOn(&generate.step);
fmt.step.dependOn(&merge.step);
b.step("gen", "Regenerate src/wit from wit/v0.1").dependOn(&fmt.step);
// zig build fixtures: rewrites what the generator test compares against.
// demo.json comes from `wasm-tools component wit tools/testdata/wit --json`.
const fixtures = b.addRunArtifact(generator);
fixtures.addFileArg(b.path("tools/testdata/demo.json"));
fixtures.addArg("tools/testdata/expected");
fixtures.setCwd(b.path("."));
fixtures.has_side_effects = true;
b.step("fixtures", "Update the generator test snapshots").dependOn(&fixtures.step);
}