diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md index 3ad29197c..abe6e55cd 100644 --- a/docs/PLUGINS.md +++ b/docs/PLUGINS.md @@ -223,6 +223,11 @@ libraries (vendored C, packed assets). See `fizzy.plugin.addCModule` and [pixi's `build.zig`](https://github.com/fizzyedit/pixi/blob/main/build.zig) for a worked example with vendored C deps. +A plugin that ships a program of its own (a helper it starts, as the agent plugin starts +`fizzy-mcp`) builds it as usual and passes it to `fizzy.plugin.installBeside(b, exe, .{ .plugin = +"" })`: `zig build install` then copies it into the plugin's own directory, beside the +library and still executable, where the plugin finds it at run time. + Under `create`, the dylib's C-ABI entry point (the five symbols the host looks up at `dlopen` time — ABI fingerprint check, the `register` entry, and injection hooks for the allocator/`*Host`/dvui-context/render-bridge — see §7 for the full list) is a tiny **generated** diff --git a/sdk/plugin_sdk.zig b/sdk/plugin_sdk.zig index 6025a3d2e..b5edb1e6b 100644 --- a/sdk/plugin_sdk.zig +++ b/sdk/plugin_sdk.zig @@ -254,6 +254,28 @@ pub fn install(b: *std.Build, lib: *std.Build.Step.Compile, opts: InstallOptions // convenience). Not for a wasm build: the browser has no plugins directory, and the desktop // editor would only find a file it cannot open. if (lib.rootModuleTarget().cpu.arch == .wasm32) return; + addDevInstall(b, lib, name, dest, b.fmt("install plugin '{s}' into fizzy plugins dir", .{name})); +} + +pub const InstallBesideOptions = struct { + /// The plugin's manifest `id`: the directory the artifact joins. + plugin: []const u8, +}; + +/// Wire `zig build install` to drop a second artifact (a helper program the plugin starts, say) +/// into the plugin's own `{plugins}/{plugin}/` directory, beside the library `install` puts there, +/// under the artifact's own file name and with its permissions. Like `install`'s copy, a dev +/// convenience: a release ships what the plugin's `release.yml` uploads. +/// +/// fizzy.plugin.installBeside(b, helper_exe, .{ .plugin = "agent" }); +pub fn installBeside(b: *std.Build, artifact: *std.Build.Step.Compile, opts: InstallBesideOptions) void { + if (artifact.rootModuleTarget().cpu.arch == .wasm32) return; + addDevInstall(b, artifact, opts.plugin, artifact.out_filename, b.fmt("install '{s}' beside plugin '{s}'", .{ artifact.out_filename, opts.plugin })); +} + +/// Copy `artifact` to `{plugins}/{dir_name}/{file_name}` on every `zig build install`, through +/// `plugin_sdk_install.zig`: Zig 0.17's build has no custom steps. +fn addDevInstall(b: *std.Build, artifact: *std.Build.Step.Compile, dir_name: []const u8, file_name: []const u8, step_name: []const u8) void { const fizzy_dep = fizzyDep(b, .{ .target = b.graph.host, .optimize = .fast }); const dev_exe = b.addExecutable(.{ .name = "fizzy-plugin-dev-install", @@ -264,13 +286,13 @@ pub fn install(b: *std.Build, lib: *std.Build.Step.Compile, opts: InstallOptions }), }); const dev = b.addRunArtifact(dev_exe); - dev.setName(b.fmt("install plugin '{s}' into fizzy plugins dir", .{name})); - dev.addArtifactArg(lib); - dev.addArgs(&.{ name, dest }); + dev.setName(step_name); + dev.addArtifactArg(artifact); + dev.addArgs(&.{ dir_name, file_name }); // A relative `FIZZY_PROFILE` is taken from the plugin's own directory. dev.setCwd(b.path(".")); - // Every install, not only when the dylib changed: the copy in the plugins dir may have been - // replaced or removed since. + // Every install, not only when the artifact changed: the copy in the plugins dir may have + // been replaced or removed since. dev.has_side_effects = true; b.getInstallStep().dependOn(&dev.step); } diff --git a/sdk/plugin_sdk_install.zig b/sdk/plugin_sdk_install.zig index df103837a..d2c11a058 100644 --- a/sdk/plugin_sdk_install.zig +++ b/sdk/plugin_sdk_install.zig @@ -1,8 +1,8 @@ -//! Copy a built plugin into fizzy's plugins directory: `zig build install`'s dev convenience, -//! wired by `fizzy.plugin.install`. A program run by a Run step, because Zig 0.17's build has no -//! custom steps. +//! Copy a built plugin, or a program it installs beside itself, into fizzy's plugins directory: +//! `zig build install`'s dev convenience, wired by `fizzy.plugin.install` and `.installBeside`. +//! A program run by a Run step, because Zig 0.17's build has no custom steps. //! -//! Usage: `plugin_sdk_install `. It lands at +//! Usage: `plugin_sdk_install `. It lands at //! `//`, where `` is `$FIZZY_PROFILE/plugins` when that is set //! (a relative profile is taken from the working directory, the plugin's own) and otherwise the //! host's fizzy plugins directory, the one the editor scans. Read here, at make time, so the @@ -34,14 +34,16 @@ pub fn main(init: std.process.Init) !void { const dest = try std.fs.path.join(arena, &.{ dir, file_name }); const data = try cwd.readFileAlloc(io, src, arena, .limited(512 * 1024 * 1024)); + // The source's permissions, so a program installed beside the plugin stays executable. + const permissions = (try cwd.statFile(io, src, .{})).permissions; // Sibling temp file + rename, not a truncating write onto `dest`: a running fizzy may be // reading `dest` right now (`PluginLoader.stableLoadCopyPath` copies from it), and on Windows // a plain overwrite of a file the editor still has open fails outright. The rename also means // a crash mid-write never leaves a half-written dylib at the load path. const tmp = try std.fmt.allocPrint(arena, "{s}.part", .{dest}); - try cwd.writeFile(io, .{ .sub_path = tmp, .data = data }); + try cwd.writeFile(io, .{ .sub_path = tmp, .data = data, .flags = .{ .permissions = permissions } }); try std.Io.Dir.rename(cwd, tmp, cwd, dest, io); - std.log.info("fizzy: installed plugin → {s}", .{dest}); + std.log.info("fizzy: installed {s} → {s}", .{ file_name, dest }); } /// `{local_config}/fizzy/plugins`, mirroring the runtime loader (`core/paths.zig`), or