Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,13 @@ Full contract: **[`docs/PLUGINS.md`](docs/PLUGINS.md)**. Living reshape plan: **

Which plugins an executable links in is build data: `build/sdk.zig`'s `bundledPluginsModule`
generates `bundled_plugins` (`pub const modules = .{ @import("workbench"), … }`) and the
runtime iterates it — nothing in `app/` or `src/` names a plugin except the workbench (its
state still lives on the Editor). An app built on fizzy adds its own with `defer-app` +
`fizzy.buildApp(fizzy_dep, &.{ .{ .name, .module = dep.module("plugin") } })`; see
runtime iterates it. Nothing in `app/` or `src/` registers a plugin by name, though two are
imported as libraries and so always built: workbench (its state still lives on the Editor, so
every list must include it) and markdown (the store draws READMEs with it). An app built on
fizzy lists every plugin it links with `defer-app` + `fizzy.buildApp(fizzy_dep, &.{
fizzy.plugins.workbench, fizzy.plugins.text, .{ .name, .module = dep.module("plugin") } })`:
fizzy's own are entries with no module, built for each executable (`build/plugins.zig`'s
`link`), and only those listed register or get a dylib. See
[`fizzyedit/example-app`](https://github.com/fizzyedit/example-app), which bundles
`fizzyedit/example-plugin`.

Expand Down
25 changes: 20 additions & 5 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ var deferred: ?struct {

pub const BundledPlugin = @import("build/sdk.zig").BundledPlugin;

/// Fizzy's own plugins, as entries in the list `buildApp` links: `fizzy.plugins.text`, or
/// `fizzy.plugins.all` for every one in fizzy's own order. Fizzy builds each for every executable.
/// `workbench` must be listed for now, because the editor still holds its state
/// (fizzyedit/plans#13).
pub const plugins = @import("build/plugins.zig").entries;

/// Build the application in a consumer's graph, with the plugins the consumer bundles.
///
/// ```zig
Expand All @@ -75,18 +81,27 @@ pub const BundledPlugin = @import("build/sdk.zig").BundledPlugin;
/// .@"app-layout" = b.path("src/layout.zig"),
/// });
/// try fizzy.buildApp(fizzy_dep, &.{
/// fizzy.plugins.workbench,
/// fizzy.plugins.text,
/// .{ .name = "pixi", .module = b.dependency("pixi", .{ .target = target, .optimize = optimize }).module("plugin") },
/// });
/// b.installArtifact(fizzy_dep.artifact("myapp"));
/// ```
///
/// `name` is the plugin's id (its root's `plugin_id`); `module` its static module, which the
/// plugin package exports and which fizzy wires to `dvui`, `core`, `fizzy_sdk` and `icons`.
/// Fizzy's own four are always bundled ahead of these.
pub fn buildApp(fizzy_dep: *std.Build.Dependency, plugins: []const BundledPlugin) !void {
/// `linked` is everything the app links, in registration order (the order of the sidebar's
/// views): fizzy's own (`fizzy.plugins`) and the app's. For an app's, `name` is the plugin's id
/// (its root's `plugin_id`) and `module` its static module, which the plugin package exports and
/// which fizzy wires to `dvui`, `core`, `fizzy_sdk` and `icons`.
pub fn buildApp(fizzy_dep: *std.Build.Dependency, linked: []const BundledPlugin) !void {
const d = deferred orelse @panic("fizzy.buildApp: pass `.@\"defer-app\" = true` to b.dependency(\"fizzy\", …) first");
var opts = d.base;
opts.app_plugins = plugins;
// Until fizzyedit/example-app lists fizzy's plugins itself (its CI and fizzy's build each
// other's main): a list naming none of fizzy's gets all of them ahead of its own, as before.
// Goes in fizzyedit/plans#13 step 2.
const names_fizzys = for (linked) |p| {
if (p.module == null) break true;
} else false;
opts.app_plugins = if (names_fizzys) linked else try std.mem.concat(fizzy_dep.builder.allocator, BundledPlugin, &.{ &plugins.all, linked });
try @import("build/app.zig").construct(fizzy_dep.builder, d.target, d.optimize, opts, d.cfg);
}

Expand Down
4 changes: 2 additions & 2 deletions build/app.zig
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ pub const WebPluginDir = struct {
};

pub const Options = struct {
/// Plugins the application bundles beyond fizzy's own — see `build.zig`'s `buildApp`.
app_plugins: []const @import("sdk.zig").BundledPlugin = &.{},
/// Every plugin the application links, fizzy's own among them — see `build.zig`'s `buildApp`.
app_plugins: []const @import("sdk.zig").BundledPlugin = &@import("plugins.zig").entries.all,
/// Dependencies (by the name in `build.zig.zon`) whose `"plugin"` module the **web** build
/// links in. The browser cannot `dlopen`, so a plugin exists there only if the application
/// bundles it; this is fizzy-the-app's list, resolved for the wasm target. Each is looked
Expand Down
103 changes: 20 additions & 83 deletions build/exe.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ const workbench_plugin = plugins.workbench;
const text_plugin = plugins.text;
const markdown_plugin = plugins.markdown;
const image_plugin = plugins.image;
const archive_plugin = plugins.archive;
const devtools_plugin = plugins.devtools;
const MacosSdlPaths = common.MacosSdlPaths;

/// Install stripped exe + built-in plugin dylibs for `vpk pack --packDir`.
Expand Down Expand Up @@ -201,9 +199,10 @@ pub fn addFizzyExecutableForTarget(
/// `Host.layout_ctx`. The file imports `dvui`, `app`, `core`, and `fizzy_sdk` — not
/// `fizzy` / `Editor` — so the module graph does not cycle.
app_layout: ?std.Build.LazyPath,
/// Plugins the application bundles beyond fizzy's own four: each a static module from a
/// plugin package (`plugin_dep.module("plugin")`), linked in and listed in the generated
/// `bundled_plugins` module under its plugin id.
/// Every plugin the application links, in registration order: fizzy's own
/// (`fizzy.plugins`, built here) and the application's, each a static module from a plugin
/// package (`plugin_dep.module("plugin")`). Listed in the generated `bundled_plugins` module
/// under its plugin id.
app_plugins: []const sdk.BundledPlugin,
) !FizzyExecutable {
const native = nativeDvui(b, native_backend, resolved_target, optimize, accesskit, macos_sdl_paths);
Expand Down Expand Up @@ -292,86 +291,24 @@ pub fn addFizzyExecutableForTarget(

const sdk_module = sdk.wireSdkModule(b, resolved_target, optimize, dvui_mod, proxy_bridge_host_mod, core_module, exe.root_module);
const sdk_proxy_module = sdk.wireSdkModule(b, resolved_target, optimize, dvui_proxy_mod, proxy_bridge_plugin_mod, core_proxy_module, null);
const workbench_module = workbench_plugin.addStaticModule(b, resolved_target, optimize, .{
// What this application links, as build data: a generated module whose `modules` tuple
// names every statically linked plugin, so the runtime registers, probes and falls back
// to whatever is listed here and never a plugin by name. An app built on fizzy lists its
// own (`buildApp`), fizzy's among them or not.
const linked = plugins.link(b, .{
.target = resolved_target,
.optimize = optimize,
.dvui = dvui_mod,
.core = core_module,
.sdk = sdk_module,
.icons = icons_module,
.backend = native.backend,
}, workbench_opts, exe.root_module);
const text_module = text_plugin.addStaticModule(b, resolved_target, optimize, .{
.dvui = dvui_mod,
.core = core_module,
.sdk = sdk_module,
.icons = icons_module,
}, exe.root_module);
const image_module = image_plugin.addStaticModule(b, resolved_target, optimize, .{
.dvui = dvui_mod,
.core = core_module,
.sdk = sdk_module,
}, exe.root_module);
const archive_module = archive_plugin.addStaticModule(b, resolved_target, optimize, .{
.dvui = dvui_mod,
.core = core_module,
.sdk = sdk_module,
}, exe.root_module);
const devtools_module = devtools_plugin.addStaticModule(b, resolved_target, optimize, .{
.dvui = dvui_mod,
.core = core_module,
.sdk = sdk_module,
}, exe.root_module);
const markdown_module: ?*std.Build.Module = if (resolved_target.result.cpu.arch != .wasm32)
markdown_plugin.addStaticModule(b, resolved_target, optimize, .{
.dvui = dvui_mod,
.core = core_module,
.sdk = sdk_module,
}, exe.root_module)
else
null;

// What this application bundles, as build data: a generated module whose `modules` tuple
// names every statically linked plugin, so the runtime registers, probes and falls back
// to whatever is listed here and never a plugin by name. An app built on fizzy lists its
// own.
var bundled_list: std.ArrayList(sdk.BundledPlugin) = .empty;
try bundled_list.appendSlice(b.allocator, &.{
.{ .name = "workbench", .module = workbench_module },
.{ .name = "text", .module = text_module },
.{ .name = "image", .module = image_module },
.{ .name = "archive", .module = archive_module },
.{ .name = "markdown", .module = markdown_module },
.{ .name = "devtools", .module = devtools_module },
});
for (app_plugins) |p| {
const src_mod = p.module orelse {
try bundled_list.append(b.allocator, p);
continue;
};
// A copy per executable, not the plugin package's module itself: this function builds
// more than one exe (the packaged, Velopack-linked one beside the plain one), each with
// its own `dvui`/`core`/`fizzy_sdk`, and one module cannot import both sets. The copy
// keeps the plugin's own imports — its manifest options, its dependencies — and gets
// this exe's framework modules.
const m = b.createModule(.{
.target = resolved_target,
.optimize = optimize,
.root_source_file = src_mod.root_source_file,
.link_libc = resolved_target.result.cpu.arch != .wasm32,
});
var it = src_mod.import_table.iterator();
while (it.next()) |kv| {
const name = kv.key_ptr.*;
if (std.mem.eql(u8, name, "dvui") or std.mem.eql(u8, name, "core") or std.mem.eql(u8, name, "fizzy_sdk") or std.mem.eql(u8, name, "icons")) continue;
m.addImport(name, kv.value_ptr.*);
}
m.addImport("dvui", dvui_mod);
m.addImport("core", core_module);
m.addImport("fizzy_sdk", sdk_module);
if (icons_module) |icons| m.addImport("icons", icons);
exe.root_module.addImport(p.name, m);
try bundled_list.append(b.allocator, .{ .name = p.name, .module = m });
}
const bundled = sdk.bundledPluginsModule(b, resolved_target, optimize, bundled_list.items);
.workbench_opts = workbench_opts,
.consumer = exe.root_module,
.markdown = resolved_target.result.cpu.arch != .wasm32,
}, app_plugins);
const markdown_module = linked.markdown;
const bundled = sdk.bundledPluginsModule(b, resolved_target, optimize, linked.plugins);
exe.root_module.addImport("bundled_plugins", bundled);

const singleton_app_dep = b.dependency("dvui_singleton_app", .{
Expand Down Expand Up @@ -415,7 +352,7 @@ pub fn addFizzyExecutableForTarget(
}, workbench_opts);
} else null;

const text_dylib: ?*std.Build.Step.Compile = if (resolved_target.result.cpu.arch != .wasm32) blk: {
const text_dylib: ?*std.Build.Step.Compile = if (resolved_target.result.cpu.arch != .wasm32 and plugins.linksFizzys(app_plugins, "text")) blk: {
break :blk text_plugin.addDylib(b, resolved_target, optimize, .{
.dvui = dvui_proxy_mod,
.core = core_proxy_module,
Expand All @@ -425,7 +362,7 @@ pub fn addFizzyExecutableForTarget(
});
} else null;

const markdown_dylib: ?*std.Build.Step.Compile = if (resolved_target.result.cpu.arch != .wasm32) blk: {
const markdown_dylib: ?*std.Build.Step.Compile = if (resolved_target.result.cpu.arch != .wasm32 and plugins.linksFizzys(app_plugins, "markdown")) blk: {
break :blk markdown_plugin.addDylib(b, resolved_target, optimize, .{
.dvui = dvui_proxy_mod,
.core = core_proxy_module,
Expand All @@ -434,7 +371,7 @@ pub fn addFizzyExecutableForTarget(
});
} else null;

const image_dylib: ?*std.Build.Step.Compile = if (resolved_target.result.cpu.arch != .wasm32) blk: {
const image_dylib: ?*std.Build.Step.Compile = if (resolved_target.result.cpu.arch != .wasm32 and plugins.linksFizzys(app_plugins, "image")) blk: {
break :blk image_plugin.addDylib(b, resolved_target, optimize, .{
.dvui = dvui_proxy_mod,
.core = core_proxy_module,
Expand Down
119 changes: 118 additions & 1 deletion build/plugins.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,127 @@
//! Each built-in plugin keeps its fizzy-internal static-embed glue self-contained in
//! `plugins/<name>/static/integration.zig`, separate from the canonical third-party files
//! at the plugin-folder root (fizzy's `@import("<name>")` resolves to the root
//! `<name>.zig`). Fizzy root aggregates those integration files here.
//! `<name>.zig`). Fizzy root aggregates those integration files here, and `link` turns the list
//! an application gives into modules for one executable.
const std = @import("std");
const BundledPlugin = @import("sdk.zig").BundledPlugin;

pub const workbench = @import("../plugins/workbench/static/integration.zig");
pub const text = @import("../plugins/text/static/integration.zig");
pub const markdown = @import("../plugins/markdown/static/integration.zig");
pub const image = @import("../plugins/image/static/integration.zig");
pub const archive = @import("../plugins/archive/static/integration.zig");
pub const devtools = @import("../plugins/devtools/static/integration.zig");

/// Fizzy's own plugins, as entries in the list an application links (`fizzy.plugins`). An entry
/// with no module is built by fizzy for each executable, wired to that executable's framework.
pub const entries = struct {
pub const workbench: BundledPlugin = .{ .name = "workbench" };
pub const text: BundledPlugin = .{ .name = "text" };
pub const image: BundledPlugin = .{ .name = "image" };
pub const archive: BundledPlugin = .{ .name = "archive" };
pub const markdown: BundledPlugin = .{ .name = "markdown" };
pub const devtools: BundledPlugin = .{ .name = "devtools" };

/// What fizzy itself links, in its sidebar order.
pub const all = [_]BundledPlugin{ @This().workbench, @This().text, @This().image, @This().archive, @This().markdown, @This().devtools };
};

/// Whether `plugins` links fizzy's own `name`, rather than leaving it out or bringing a module of
/// its own under that id.
pub fn linksFizzys(plugins: []const BundledPlugin, name: []const u8) bool {
for (plugins) |p| if (p.module == null and std.mem.eql(u8, p.name, name)) return true;
return false;
}

/// What one executable gives the plugins it links.
pub const Wiring = struct {
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
dvui: *std.Build.Module,
core: *std.Build.Module,
sdk: *std.Build.Module,
icons: ?*std.Build.Module,
/// workbench's: the native backend, null on the web.
backend: ?*std.Build.Module,
workbench_opts: *std.Build.Step.Options,
/// The executable's root module, which imports each plugin by its name.
consumer: *std.Build.Module,
/// False where markdown does not build (the native exe for wasm).
markdown: bool = true,
};

pub const Linked = struct {
/// The modules for `bundledPluginsModule`, in the order the application listed them.
plugins: []const BundledPlugin,
/// Built whether or not they are listed: fizzy's own code imports both. The editor holds
/// workbench's state, and the store draws a plugin's README with markdown. A module is made
/// once for its consumer, so a listed one is this one.
workbench: *std.Build.Module,
markdown: ?*std.Build.Module,
};

/// Make the modules for `plugins`: fizzy's own built for this executable, an application's
/// re-homed onto it. workbench must be listed, because the editor still holds its state
/// (fizzyedit/plans#13).
pub fn link(b: *std.Build, w: Wiring, plugins: []const BundledPlugin) Linked {
if (!linksFizzys(plugins, "workbench")) std.debug.panic("fizzy: the plugin list must include `fizzy.plugins.workbench`; the editor still holds its state (fizzyedit/plans#13)", .{});

const workbench_mod = workbench.addStaticModule(b, w.target, w.optimize, .{
.dvui = w.dvui,
.core = w.core,
.sdk = w.sdk,
.icons = w.icons,
.backend = w.backend,
}, w.workbench_opts, w.consumer);
const markdown_mod: ?*std.Build.Module = if (w.markdown) markdown.addStaticModule(b, w.target, w.optimize, .{ .dvui = w.dvui, .core = w.core, .sdk = w.sdk }, w.consumer) else null;

var out: std.ArrayList(BundledPlugin) = .empty;
for (plugins) |p| {
const m: ?*std.Build.Module = if (p.module) |src| rehome(b, w, p.name, src) else if (std.mem.eql(u8, p.name, "workbench"))
workbench_mod
else if (std.mem.eql(u8, p.name, "markdown"))
markdown_mod
else if (std.mem.eql(u8, p.name, "text"))
text.addStaticModule(b, w.target, w.optimize, .{ .dvui = w.dvui, .core = w.core, .sdk = w.sdk, .icons = w.icons }, w.consumer)
else if (std.mem.eql(u8, p.name, "image"))
image.addStaticModule(b, w.target, w.optimize, .{ .dvui = w.dvui, .core = w.core, .sdk = w.sdk }, w.consumer)
else if (std.mem.eql(u8, p.name, "archive"))
archive.addStaticModule(b, w.target, w.optimize, .{ .dvui = w.dvui, .core = w.core, .sdk = w.sdk }, w.consumer)
else if (std.mem.eql(u8, p.name, "devtools"))
devtools.addStaticModule(b, w.target, w.optimize, .{ .dvui = w.dvui, .core = w.core, .sdk = w.sdk }, w.consumer)
else
std.debug.panic("fizzy: plugin '{s}' has no module and is not one of fizzy's own (`fizzy.plugins`)", .{p.name});
// A plugin this target does not build (markdown for the native exe on wasm) is left out.
if (m) |module| out.append(b.allocator, .{ .name = p.name, .module = module }) catch @panic("OOM");
}
return .{ .plugins = out.items, .workbench = workbench_mod, .markdown = markdown_mod };
}

/// A copy of an application's plugin module for this executable, not the plugin package's module
/// itself: one application builds several executables (the packaged, Velopack-linked one beside
/// the plain one, the web one), each with its own `dvui`/`core`/`fizzy_sdk`, and one module cannot
/// import both sets. The copy keeps the plugin's own imports (its manifest options, its
/// dependencies) and gets this executable's framework modules.
pub fn rehome(b: *std.Build, w: Wiring, name: []const u8, src: *std.Build.Module) *std.Build.Module {
const wasm = w.target.result.cpu.arch == .wasm32;
const m = b.createModule(.{
.target = w.target,
.optimize = w.optimize,
.root_source_file = src.root_source_file,
.link_libc = !wasm,
.single_threaded = if (wasm) true else null,
});
var it = src.import_table.iterator();
while (it.next()) |kv| {
const import = kv.key_ptr.*;
if (std.mem.eql(u8, import, "dvui") or std.mem.eql(u8, import, "core") or std.mem.eql(u8, import, "fizzy_sdk") or std.mem.eql(u8, import, "icons")) continue;
m.addImport(import, kv.value_ptr.*);
}
m.addImport("dvui", w.dvui);
m.addImport("core", w.core);
m.addImport("fizzy_sdk", w.sdk);
if (w.icons) |icons| m.addImport("icons", icons);
w.consumer.addImport(name, m);
return m;
}
5 changes: 3 additions & 2 deletions build/sdk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@ pub fn buildOptsModule(opts: *std.Build.Step.Options) *std.Build.Module {
pub const BundledPlugin = struct {
/// The plugin's import name and id — `plugin_id` in its root, `plugin.zig.zon`'s `.id`.
name: []const u8,
/// Null for a plugin this target does not build (markdown on wasm); it is left out.
module: ?*std.Build.Module,
/// The plugin's static module. Null for one of fizzy's own (`fizzy.plugins`), which fizzy
/// builds for each executable.
module: ?*std.Build.Module = null,
};

/// The generated `bundled_plugins` module: `pub const modules = .{ @import("a"), @import("b") … }`
Expand Down
Loading
Loading