From fe294e69cb366b589024ef04f4eef684c30c70a2 Mon Sep 17 00:00:00 2001 From: Michael Belousov Date: Sat, 24 Oct 2026 15:04:34 -0400 Subject: [PATCH 1/2] build: an app builds its plugins, web and package with fizzy's build API An app built on fizzy can now: - build a plugin against an executable's own modules: `fizzy.framework(fizzy_dep, .native | .web)` and the SDK's `plugin.staticModule(b, framework, .{})`. A plugin's own dependencies, C ones included, are then built by its build for each target rather than copied by fizzy's, so a plugin can bring its own tree-sitter grammar (graphl does). Unwired `"plugin"` modules are still copied, now with `tape` and `replay` too. - say which executables link each plugin, with a module per target: `.{ .name, .targets = &.{ .{ .target = .web, .module = ... } } }`. - get what it built from `fizzy.addApp`: the executables and the web build (`web`, `web_page`, `web_files`); `fizzy.addWebSteps(b, fizzy_dep)` adds a `web` step. - add to its package: `package-dirs` (files beside the executable) and `vpk-args` (appended to `vpk pack`), as dependency options. --- AGENTS.md | 19 ++++++- build.zig | 137 +++++++++++++++++++++++++++++++++++++++++---- build/app.zig | 63 +++++++++++++++++---- build/exe.zig | 69 ++++++++++++++++------- build/package.zig | 16 +++++- build/plugins.zig | 26 +++++++-- build/sdk.zig | 41 ++++++++++++++ build/web.zig | 128 ++++++++++++++++++++++++++++-------------- sdk/plugin_sdk.zig | 60 ++++++++++++++++++++ 9 files changed, 469 insertions(+), 90 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c336104d2..e55708e6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,10 +76,27 @@ all decided at comptime (`App.loads_plugins`). What is fizzy's own (bundle id, u `VERSION`, its plist and resource script) only fizzy's root `build.zig` passes (`Options.own`); an app sets its identity through fizzy's options (`app-name`, `app-bundle-id`, `app_version`, `repo-url`, `app-package-title`, `app-icon`, `app-icon-ico`, `app-icon-icns`, …), and -`try fizzy.addPackageSteps(b, fizzy_dep)` gives its own build `package` and `packageall`. See +`try fizzy.addPackageSteps(b, fizzy_dep)` gives its own build `package` and `packageall`, which +ship the files of each directory in `package-dirs` beside the executable and pass `vpk-args` to +`vpk pack`. See [`fizzyedit/example-app`](https://github.com/fizzyedit/example-app), which bundles `fizzyedit/example-plugin`. +A plugin package with dependencies of its own builds its static module against the executables +that link it: it exposes a function over `fizzy.plugin.staticModule(b, framework, .{})`, and the +app lists what that returns for `fizzy.framework(fizzy_dep, .native)`. Such a module is linked as +it is, its dependencies with the targets, modules and C sources its build gave them. An unwired +`"plugin"` module is copied for each executable instead (`build/plugins.zig`'s `rehome`), and +gets each of what the SDK exports a plugin (`dvui`, `core`, `fizzy_sdk`, `icons`, `tape`, +`replay`) that its build did not wire, the executable's own. +`fizzy.addApp` is `buildApp` returning the executables, for steps of the app's own. An entry's +`targets` names the kinds of executable that link it, each with the module built against that +kind's framework (`.{ .target = .web, .module = ... }`); one the browser cannot run names only +`.native`. +It returns the web build too, one directory (`web`; the page and the rest apart, `web_page` and +`web_files`), and `fizzy.addWebSteps(b, fizzy_dep)` gives an app built with `buildApp` a `web` +step that installs it. + **The example repos move with fizzy.** `fizzyedit/example-app` (fizzy as a library, in every shape) and `fizzyedit/example-plugin` (the plugin template) live outside this repo, and CI builds both against each pull request's own tree (`scripts/check-examples.sh`, which you can run diff --git a/build.zig b/build.zig index fa9ff2827..47016d552 100644 --- a/build.zig +++ b/build.zig @@ -88,7 +88,8 @@ pub fn build(b: *std.Build) !void { deferred = .{ .target = target, .optimize = optimize, .base = base, .cfg = cfg }; return; } - try app.construct(b, target, optimize, base, cfg); + var frameworks: app.Frameworks = .{}; + _ = try app.construct(b, target, optimize, base, cfg, &frameworks); // On the configure pass that fetches Velopack there is no `package` step to run. if (cfg.vz != null) @import("build/package.zig").addPackageAllStep(b, cfg.no_emit); } @@ -102,6 +103,8 @@ var deferred: ?struct { cfg: @import("build/app.zig").Config, /// `buildApp` has run (`addPackageSteps` needs its steps). built: bool = false, + /// What `framework` made before `buildApp`, which builds the executables with it. + frameworks: @import("build/app.zig").Frameworks = .{}, } = null; pub const BundledPlugin = @import("build/sdk.zig").BundledPlugin; @@ -112,6 +115,41 @@ pub const BundledPlugin = @import("build/sdk.zig").BundledPlugin; /// (fizzyedit/plans#13). pub const plugins = @import("build/plugins.zig").entries; +/// One of the app's executables as the plugins it links see it: its target and framework modules +/// (`framework`). +pub const Framework = plugin.Framework; + +pub const Platform = @import("build/sdk.zig").Platform; + +/// The framework modules the app's `platform` executables link, to build a plugin against before +/// `buildApp` lists it. A plugin module built against them is linked as it is: its dependencies +/// keep the modules, target and C sources its own build gave them, so a plugin can bring a C +/// library of its own, such as a tree-sitter grammar. A module that is not (the +/// unwired `"plugin"` module a plugin package exports) is copied for each executable instead. +/// +/// ```zig +/// const hello = b.dependency("hello", .{}); +/// try fizzy.buildApp(fizzy_dep, &.{ +/// fizzy.plugins.workbench, +/// .{ .name = "hello", .module = @import("hello").staticModule(hello.builder, fizzy.framework(fizzy_dep, .native)) }, +/// }); +/// ``` +pub fn framework(fizzy_dep: *std.Build.Dependency, platform: Platform) Framework { + if (deferred == null) @panic("fizzy.framework: pass `.@\"defer-app\" = true` to b.dependency(\"fizzy\", …) first"); + const d = &deferred.?; + const b = fizzy_dep.builder; + switch (platform) { + .native => { + const fw = d.frameworks.getNative(b, d.target, d.optimize, d.cfg); + return .{ .target = d.target, .optimize = d.optimize, .dvui = fw.native.dvui, .core = fw.core, .sdk = fw.sdk, .icons = fw.icons }; + }, + .web => { + const fw = d.frameworks.getWeb(b, d.optimize); + return .{ .target = @import("build/web.zig").target(b), .optimize = d.optimize, .dvui = fw.dvui, .core = fw.core, .sdk = fw.sdk, .icons = fw.icons }; + }, + } +} + /// Build the application in a consumer's graph, with the plugins the consumer bundles. /// /// ```zig @@ -136,17 +174,60 @@ pub const plugins = @import("build/plugins.zig").entries; /// (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"); + _ = try addApp(fizzy_dep, linked); +} + +/// What `addApp` made, for steps of the app's own: `b.installArtifact(app.exe)`, say. +pub const App = struct { + /// The desktop executable `zig build run` runs. + exe: *std.Build.Step.Compile, + /// The one `package` packs: `exe`, or the same linked with Velopack where the target has it. + packaged: *std.Build.Step.Compile, + /// The web executable, `web.wasm`. + wasm: *std.Build.Step.Compile, + /// The web build, a directory to deploy as it is: `web_page` and `web_files`. + web: std.Build.LazyPath, + /// Its `index.html`, and everything else in it, for an app that serves a page of its own. + web_page: std.Build.LazyPath, + web_files: std.Build.LazyPath, +}; + +/// `buildApp`, giving back what it made. An entry's `targets` names the kinds of executable that +/// link it, each with the module built against that kind's framework; a plugin the browser cannot +/// run names only `.native`. +/// +/// ```zig +/// const app = try fizzy.addApp(fizzy_dep, &.{ +/// fizzy.plugins.workbench, +/// .{ .name = "hello", .targets = &.{ +/// .{ .target = .native, .module = hello.staticModule(hello_dep.builder, fizzy.framework(fizzy_dep, .native)) }, +/// .{ .target = .web, .module = hello.staticModule(hello_dep.builder, fizzy.framework(fizzy_dep, .web)) }, +/// } }, +/// .{ .name = "shell", .module = shell_dep.module("plugin"), .targets = &.{.{ .target = .native }} }, +/// }); +/// ``` +pub fn addApp(fizzy_dep: *std.Build.Dependency, linked: []const BundledPlugin) !App { + const d = deferred orelse @panic("fizzy.addApp: pass `.@\"defer-app\" = true` to b.dependency(\"fizzy\", …) first"); var opts = d.base; - // 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); + opts.app_plugins = try withFizzys(fizzy_dep.builder, linked); + const built = try @import("build/app.zig").construct(fizzy_dep.builder, d.target, d.optimize, opts, d.cfg, &deferred.?.frameworks); deferred.?.built = true; + return .{ + .exe = built.native.main.exe, + .packaged = built.native.package.exe, + .wasm = built.web.wasm, + .web = built.web.site, + .web_page = built.web.page, + .web_files = built.web.files, + }; +} + +/// 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. +fn withFizzys(b: *std.Build, linked: []const BundledPlugin) ![]const BundledPlugin { + for (linked) |p| if (p.isFizzys()) return linked; + return std.mem.concat(b.allocator, BundledPlugin, &.{ &plugins.all, linked }); } /// `zig build package` (and its alias `desktop`) and `zig build packageall` for an app built with @@ -167,7 +248,19 @@ pub fn buildApp(fizzy_dep: *std.Build.Dependency, linked: []const BundledPlugin) /// `repo-url` (the update feed; none by default), `app-package-title`, `app-icon` (PNG: the /// window, the infobar, Linux), `app-icon-ico`, `app-windows-rc`, `app-icon-icns`, /// `app-macos-plist`. The icons default to fizzy's art; the resource script is generated and -/// vpk writes the plist. +/// vpk writes the plist. `package-dirs` (a `[]const LazyPath`) adds each directory's files beside +/// the executable, and `vpk-args` (a `[]const []const u8`) is passed to `vpk pack` after fizzy's +/// own arguments: +/// +/// ```zig +/// const helpers = b.addWriteFiles(); +/// _ = helpers.addCopyFile(helper_exe.getEmittedBin(), helper_exe.out_filename); +/// const fizzy_dep = b.dependency("fizzy", .{ +/// // … +/// .@"package-dirs" = &[_]std.Build.LazyPath{helpers.getDirectory()}, +/// .@"vpk-args" = &[_][]const u8{ "--releaseNotes", "notes.md" }, +/// }); +/// ``` /// /// `packageall` passes each run the options the app's own command line was given, so the /// Windows MSVC ones reach fizzy only if the app declares `windows-msvc-libc` / `fetch-msvc` and @@ -197,6 +290,28 @@ pub fn addPackageSteps(b: *std.Build, fizzy_dep: *std.Build.Dependency) !void { @import("build/package.zig").addPackageAllStep(b, d.cfg.no_emit); } +/// `zig build web` for an app built with `buildApp`, as a step of the app's own build `b`: fizzy's +/// web build installed into the app's `zig-out/web`. An app that already has a `web` step gets +/// `error.WebStepTaken`. +/// +/// ```zig +/// try fizzy.buildApp(fizzy_dep, &.{ … }); +/// try fizzy.addWebSteps(b, fizzy_dep); +/// ``` +pub fn addWebSteps(b: *std.Build, fizzy_dep: *std.Build.Dependency) !void { + if (b.top_level_steps.contains("web")) { + std.log.err("fizzy.addWebSteps: this build already has a `web` step", .{}); + return error.WebStepTaken; + } + const d = deferred orelse @panic("fizzy.addWebSteps: pass `.@\"defer-app\" = true` to b.dependency(\"fizzy\", …) and call fizzy.buildApp first"); + if (!d.built) @panic("fizzy.addWebSteps: call fizzy.buildApp first"); + b.step("web", "Build the web (wasm) app into zig-out/web/").dependOn(&b.addInstallDirectory(.{ + .source_dir = fizzy_dep.namedLazyPath("web"), + .install_dir = .{ .custom = "web" }, + .install_subdir = "", + }).step); +} + /// What a dvui app of its own builds on (`addDvui`): fizzy's backend, with OS windows besides the /// main one, or dvui's own SDL3 backend, with the main window only. pub const NativeBackend = @import("build/exe.zig").NativeBackend; diff --git a/build/app.zig b/build/app.zig index 51bae113c..96421a37b 100644 --- a/build/app.zig +++ b/build/app.zig @@ -10,6 +10,7 @@ const dvui = fizzy_sdk.dvui; const velopack = @import("velopack.zig"); const rehome_fixture = @import("rehome_fixture.zig"); const FizzyExecutable = @import("exe.zig").FizzyExecutable; +const NativeFramework = @import("exe.zig").NativeFramework; const web = @import("web.zig"); /// A plugin checkout for the web build, with the modules its own `build.zig` would have added @@ -30,7 +31,8 @@ pub const WebPluginDir = struct { }; pub const Options = struct { - /// Every plugin the application links, fizzy's own among them — see `build.zig`'s `buildApp`. + /// Every plugin the application links, fizzy's own among them, each in the executables its + /// `targets` names — 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 @@ -57,7 +59,8 @@ pub const Options = struct { pub fn build(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.lang.OptimizeMode, opts: Options) !void { const cfg = try readConfig(b, target, opts); - try construct(b, target, optimize, opts, cfg); + var frameworks: Frameworks = .{}; + _ = try construct(b, target, optimize, opts, cfg, &frameworks); } /// Everything `build` reads before it constructs anything — the options, the version, the @@ -102,6 +105,9 @@ pub const Config = struct { /// What the native executable draws with (`-Dnative-backend`, `build/exe.zig`). native_backend: @import("exe.zig").NativeBackend, test_filters: []const []const u8, + /// `-Dpackage-dirs`, `-Dvpk-args` (`package.zig`). + package_dirs: []const std.Build.LazyPath, + vpk_args: []const []const u8, macos_sign_app_identity: ?[]const u8, macos_sign_install_identity: ?[]const u8, macos_notary_profile: ?[]const u8, @@ -450,6 +456,11 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options "test-filter", "Skip tests that do not match any filter", ) orelse &[0][]const u8{}; + + // What an app's installers carry besides the executable and the plugin dylibs (a helper + // program it starts, say), and what it tells vpk besides fizzy's own arguments. + const package_dirs = b.option([]const std.Build.LazyPath, "package-dirs", "Directories whose files `package` ships beside the executable") orelse &.{}; + const vpk_args = b.option([]const []const u8, "vpk-args", "Arguments `package` passes `vpk pack` after its own") orelse &.{}; return .{ .vz = vz, .macos_sdl_paths = macos_sdl_paths, @@ -485,6 +496,8 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options .accesskit = accesskit, .native_backend = native_backend, .test_filters = test_filters, + .package_dirs = package_dirs, + .vpk_args = vpk_args, .macos_sign_app_identity = macos_sign_app_identity, .macos_sign_install_identity = macos_sign_install_identity, .macos_notary_profile = macos_notary_profile, @@ -492,8 +505,15 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options }; } -/// Phase two of `build`: the executables, the web build, tests, packaging. -pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.lang.OptimizeMode, opts: Options, cfg: Config) !void { +/// What `construct` made, for an app built on fizzy to build steps of its own over. +pub const Built = struct { + native: Native, + web: web.Web, +}; + +/// Phase two of `build`: the executables, the web build, tests, packaging. `frameworks` holds any +/// an app asked for first (`fizzy.framework`), and gets the rest. +pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.lang.OptimizeMode, opts: Options, cfg: Config, frameworks: *Frameworks) !Built { const assetpack = @import("assetpack"); const assets_module = assetpack.pack(b, b.path("assets"), .{}); // The window's icon, apart from `assets` so an app built on fizzy can give its own. @@ -510,20 +530,39 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. // `zig build web` produces `zig-out/web/{web.wasm, web.js, index.html, NotoSansKR-Regular.ttf}`, // deployable as-is to a static host. - const w = web.build(b, optimize, cfg.build_opts, cfg.workbench_opts, assets_module, app_icon_module, cfg.app_layout_path, opts.app_plugins, opts.web_plugin_deps, opts.web_plugin_dirs, opts.own != null); - const built = try native(b, target, optimize, opts, cfg, assets_module, app_icon_module); + const w = web.build(b, frameworks.getWeb(b, optimize), optimize, cfg.build_opts, cfg.workbench_opts, assets_module, app_icon_module, cfg.app_layout_path, try @import("sdk.zig").pluginsFor(b, opts.app_plugins, .web), opts.web_plugin_deps, opts.web_plugin_dirs, opts.own != null); + const built = try native(b, target, optimize, opts, cfg, frameworks.getNative(b, target, optimize, cfg), assets_module, app_icon_module); install(b, target, optimize, cfg, w, built); try addTests(b, target, optimize, cfg, built, assets_module, app_icon_module); + return .{ .native = built, .web = w }; } +/// Each kind of executable's framework, made the first time it is asked for: by `construct`, or +/// before it by an app (`fizzy.framework`), whose plugins are then built against the very modules +/// its executables link. +pub const Frameworks = struct { + native: ?NativeFramework = null, + web: ?web.Framework = null, + + pub fn getNative(self: *Frameworks, b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.lang.OptimizeMode, cfg: Config) NativeFramework { + if (self.native == null) self.native = @import("exe.zig").nativeFramework(b, target, optimize, cfg.accesskit, cfg.native_backend, cfg.macos_sdl_paths); + return self.native.?; + } + + pub fn getWeb(self: *Frameworks, b: *std.Build, optimize: std.lang.OptimizeMode) web.Framework { + if (self.web == null) self.web = web.framework(b, optimize); + return self.web.?; + } +}; + /// What `native` made: the executable, and the one `package` packs. -const Native = struct { +pub const Native = struct { main: FizzyExecutable, /// `main`, unless it has no Velopack and the target supports it: then a second, with it. package: FizzyExecutable, }; -fn native(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.lang.OptimizeMode, opts: Options, cfg: Config, assets_module: *std.Build.Module, app_icon_module: *std.Build.Module) !Native { +fn native(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.lang.OptimizeMode, opts: Options, cfg: Config, framework: NativeFramework, assets_module: *std.Build.Module, app_icon_module: *std.Build.Module) !Native { const fizzy_exe = @import("exe.zig"); const vz = cfg.vz; const macos_sdl_paths = cfg.macos_sdl_paths; @@ -544,10 +583,10 @@ fn native(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.lang.Op const static_image = cfg.static_image; const dynamic_plugins = cfg.dynamic_plugins; const workbench_opts = cfg.workbench_opts; - const accesskit = cfg.accesskit; const native_backend = cfg.native_backend; + const app_plugins = try @import("sdk.zig").pluginsFor(b, opts.app_plugins, .native); - const main_fizzy = try fizzy_exe.addFizzyExecutableForTarget(b, vz, target, optimize, accesskit, native_backend, build_opts, workbench_opts, assets_module, app_icon_module, cfg.branding.rc, macos_sdl_paths, velopack_enabled, app_name, app_layout_path, opts.app_plugins, dynamic_plugins); + const main_fizzy = try fizzy_exe.addFizzyExecutableForTarget(b, vz, target, optimize, framework, native_backend, build_opts, workbench_opts, assets_module, app_icon_module, cfg.branding.rc, macos_sdl_paths, velopack_enabled, app_name, app_layout_path, app_plugins, dynamic_plugins); const package_fizzy: FizzyExecutable = package_blk: { if (velopack_enabled) break :package_blk main_fizzy; @@ -573,7 +612,7 @@ fn native(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.lang.Op pack_opts.addOption(bool, "static_image", static_image); pack_opts.addOption(bool, "dynamic_plugins", dynamic_plugins); pack_opts.addOption(bool, "has_app_layout", app_layout_path != null); - break :package_blk try fizzy_exe.addFizzyExecutableForTarget(b, vz, target, optimize, accesskit, native_backend, pack_opts, workbench_opts, assets_module, app_icon_module, cfg.branding.rc, macos_sdl_paths, true, app_name, app_layout_path, opts.app_plugins, dynamic_plugins); + break :package_blk try fizzy_exe.addFizzyExecutableForTarget(b, vz, target, optimize, framework, native_backend, pack_opts, workbench_opts, assets_module, app_icon_module, cfg.branding.rc, macos_sdl_paths, true, app_name, app_layout_path, app_plugins, dynamic_plugins); }; return .{ .main = main_fizzy, .package = package_fizzy }; @@ -661,6 +700,8 @@ fn install(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.lang.O .velopack_required_fail = velopack_required_fail, .exe_for_package = built.package.exe, .package_fizzy = built.package, + .package_dirs = cfg.package_dirs, + .vpk_args = cfg.vpk_args, .macos_sign_app_identity = macos_sign_app_identity, .macos_sign_install_identity = macos_sign_install_identity, .macos_notary_profile = macos_notary_profile, diff --git a/build/exe.zig b/build/exe.zig index 1d097cd53..1039e2f80 100644 --- a/build/exe.zig +++ b/build/exe.zig @@ -167,13 +167,56 @@ pub fn platformModule( return fizzy_backend.platformModule(backendDependency(b), native.dvui, native.backend, backendOptions(target, optimize, macos_sdl_paths)); } +/// What every native executable of one target links and builds its plugins against: dvui on the +/// native backend, `core`, the SDK and `icons`. Made once, so the plain executable and the +/// packaged one share it. +pub const NativeFramework = struct { + native: NativeDvui, + core: *std.Build.Module, + sdk: *std.Build.Module, + icons: ?*std.Build.Module, + /// The glass look the backend dresses OS windows with; the dylibs' `core` takes it too. + glass_look: *std.Build.Module, +}; + +pub fn nativeFramework( + b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.lang.OptimizeMode, + accesskit: dvui.AccesskitOptions, + native_backend: NativeBackend, + macos_sdl_paths: ?MacosSdlPaths, +) NativeFramework { + const native = nativeDvui(b, native_backend, target, optimize, accesskit, macos_sdl_paths); + const glass_look = fizzy_backend.glassLookModule(backendDependency(b)); + // Its in-app glass, the backend's (`glassModule`): `core` over the same dvui takes the same one. + const glass = fizzy_backend.glassModule(backendDependency(b), native.dvui); + + // Shared `core` module (gfx/math/fs/generated atlas/platform/paths/dvui hub + + // generic widgets). Import set is shared with the plugin SDK path — see sdk/core_module.zig. + const core_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("core/core.zig"), + }); + const icons = core_mod.addImports(b, core_module, native.dvui, b.dependency("fizzy_sdk", .{}).builder, target, optimize, glass_look, glass); + const proxy_bridge_host_mod = sdk.addProxyBridgeModule(b, target, optimize, native.dep, native.dvui); + return .{ + .native = native, + .core = core_module, + .sdk = sdk.wireSdkModule(b, target, optimize, native.dvui, proxy_bridge_host_mod, core_module, null), + .icons = icons, + .glass_look = glass_look, + }; +} + pub fn addFizzyExecutableForTarget( b: *std.Build, /// Null on the configure pass that fetches Velopack (`app.readConfig`). vz: ?velopack.Dep, resolved_target: std.Build.ResolvedTarget, optimize: std.lang.OptimizeMode, - accesskit: dvui.AccesskitOptions, + framework: NativeFramework, native_backend: NativeBackend, build_opts: *std.Build.Step.Options, workbench_opts: *std.Build.Step.Options, @@ -200,8 +243,7 @@ pub fn addFizzyExecutableForTarget( /// False for a link-only app (`-Ddynamic-plugins=false`): no plugin dylib is built. dynamic_plugins: bool, ) !FizzyExecutable { - const native = nativeDvui(b, native_backend, resolved_target, optimize, accesskit, macos_sdl_paths); - const dvui_dep = native.dep; + const native = framework.native; const dvui_mod = native.dvui; const dvui_proxy_dep = sdk.dvuiDependency(b, .{ @@ -211,7 +253,6 @@ pub fn addFizzyExecutableForTarget( .accesskit = .off, }); const dvui_proxy_mod = dvui_proxy_dep.module("dvui_proxy"); - const proxy_bridge_host_mod = sdk.addProxyBridgeModule(b, resolved_target, optimize, dvui_dep, dvui_mod); const proxy_bridge_plugin_mod = dvui_proxy_dep.module("proxy_bridge"); const exe = b.addExecutable(.{ @@ -259,19 +300,8 @@ pub fn addFizzyExecutableForTarget( native_backend_opts.addOption(bool, "fizzy", native_backend == .fizzy); exe.root_module.addImport("native_backend", native_backend_opts.createModule()); - // The glass look the backend dresses OS windows with; `core` takes the same module. - const glass_look = fizzy_backend.glassLookModule(backendDependency(b)); - // Its in-app glass, the backend's (`glassModule`): `core` over the same dvui takes the same one. - const glass = fizzy_backend.glassModule(backendDependency(b), dvui_mod); - - // Shared `core` module (gfx/math/fs/generated atlas/platform/paths/dvui hub + - // generic widgets). Import set is shared with the plugin SDK path — see sdk/core_module.zig. - const core_module = b.createModule(.{ - .target = resolved_target, - .optimize = optimize, - .root_source_file = b.path("core/core.zig"), - }); - const icons_module = core_mod.addImports(b, core_module, dvui_mod, b.dependency("fizzy_sdk", .{}).builder, resolved_target, optimize, glass_look, glass); + const core_module = framework.core; + const icons_module = framework.icons; exe.root_module.addImport("core", core_module); if (icons_module) |icons| exe.root_module.addImport("icons", icons); @@ -280,7 +310,7 @@ pub fn addFizzyExecutableForTarget( .optimize = optimize, .root_source_file = b.path("core/core.zig"), }); - _ = core_mod.addImports(b, core_proxy_module, dvui_proxy_mod, b.dependency("fizzy_sdk", .{}).builder, resolved_target, optimize, glass_look, null); + _ = core_mod.addImports(b, core_proxy_module, dvui_proxy_mod, b.dependency("fizzy_sdk", .{}).builder, resolved_target, optimize, framework.glass_look, null); // `macos_fsevents` is load-bearing for `FolderWatcher`: it watches a whole project folder, // and the kqueue fallback needs a file descriptor per directory *and* per file — exactly the @@ -294,7 +324,8 @@ pub fn addFizzyExecutableForTarget( exe.root_module.addImport("nightwatch", dep.module("nightwatch")); } - const sdk_module = sdk.wireSdkModule(b, resolved_target, optimize, dvui_mod, proxy_bridge_host_mod, core_module, exe.root_module); + const sdk_module = framework.sdk; + exe.root_module.addImport("fizzy_sdk", sdk_module); const sdk_proxy_module = sdk.wireSdkModule(b, resolved_target, optimize, dvui_proxy_mod, proxy_bridge_plugin_mod, core_proxy_module, null); // 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 diff --git a/build/package.zig b/build/package.zig index cbe909b9d..4d264f985 100644 --- a/build/package.zig +++ b/build/package.zig @@ -21,6 +21,10 @@ pub const Options = struct { velopack_required_fail: ?*std.Build.Step, exe_for_package: *std.Build.Step.Compile, package_fizzy: exe.FizzyExecutable, + /// Directories whose files go beside the executable, after it and the plugin dylibs. + package_dirs: []const std.Build.LazyPath, + /// Passed to `vpk pack` after fizzy's own arguments. + vpk_args: []const []const u8, macos_sign_app_identity: ?[]const u8, macos_sign_install_identity: ?[]const u8, macos_notary_profile: ?[]const u8, @@ -133,7 +137,7 @@ pub fn addSteps(opts: Options) *std.Build.Step { // so vpk ships plugins/ next to the main binary. const pack_input_subdir = b.fmt("{s}/.pack-input", .{zig_out_subdir}); const pack_plugins_subdir = b.fmt("{s}/.pack-input/plugins", .{zig_out_subdir}); - const pack_stage_tail = exe.addVelopackPackDirInstall( + var pack_stage_tail = exe.addVelopackPackDirInstall( b, exe_for_package, package_fizzy, @@ -141,6 +145,15 @@ pub fn addSteps(opts: Options) *std.Build.Step { pack_plugins_subdir, &strip_release_sh.step, ); + for (opts.package_dirs) |dir| { + const install_dir = b.addInstallDirectory(.{ + .source_dir = dir, + .install_dir = .{ .custom = pack_input_subdir }, + .install_subdir = "", + }); + install_dir.step.dependOn(pack_stage_tail); + pack_stage_tail = &install_dir.step; + } vpk_pkg_sh.addArg("--packDir"); vpk_pkg_sh.addDirectoryArg(build_common.installDirPath(.{ .custom = pack_input_subdir })); switch (target.result.os.tag) { @@ -196,6 +209,7 @@ pub fn addSteps(opts: Options) *std.Build.Step { }, else => unreachable, } + vpk_pkg_sh.addArgs(opts.vpk_args); // Stream vpk's stdout/stderr live so failures surface their actual // diagnostic instead of just an exit-code-N message from the build // runner. With `addOutputDirectoryArg` in play, `infer_from_args` diff --git a/build/plugins.zig b/build/plugins.zig index 674541236..d001fee6a 100644 --- a/build/plugins.zig +++ b/build/plugins.zig @@ -141,16 +141,22 @@ const Rehome = struct { self.stale.deinit(self.b.allocator); } + /// What the SDK package exports a plugin (`sdk/plugin_sdk.zig`'s `exportModules`), as this + /// consumer has it. `tape` and `replay` are the ones `core` was built with, so a `Tape` the + /// plugin makes is the one fizzy plays. + const framework_names = [_][]const u8{ "dvui", "core", "fizzy_sdk", "icons", "tape", "replay" }; + fn framework(self: *const Rehome, name: []const u8) ?*std.Build.Module { if (std.mem.eql(u8, name, "dvui")) return self.w.dvui; if (std.mem.eql(u8, name, "core")) return self.w.core; if (std.mem.eql(u8, name, "fizzy_sdk")) return self.w.sdk; if (std.mem.eql(u8, name, "icons")) return self.w.icons; + if (std.mem.eql(u8, name, "tape") or std.mem.eql(u8, name, "replay")) return self.w.core.import_table.get(name); return null; } fn isFrameworkName(name: []const u8) bool { - for ([_][]const u8{ "dvui", "core", "fizzy_sdk", "icons" }) |n| if (std.mem.eql(u8, name, n)) return true; + for (framework_names) |n| if (std.mem.eql(u8, name, n)) return true; return false; } @@ -183,7 +189,12 @@ const Rehome = struct { } for (fresh.items) |m| { var direct = self.pinnedElsewhere(m); - for (m.import_table.keys()) |name| direct = direct or isFrameworkName(name); + // A module already built against this consumer's framework (`fizzy.framework`) is + // not stale for importing it. + var it = m.import_table.iterator(); + while (it.next()) |kv| if (isFrameworkName(kv.key_ptr.*)) { + direct = direct or self.framework(kv.key_ptr.*) != kv.value_ptr.*; + }; if (direct) self.stale.put(gpa, m, {}) catch @panic("OOM"); } var changed = true; @@ -279,14 +290,17 @@ const Rehome = struct { return m; } - /// A plugin's root module is always copied and always gets this consumer's framework modules, - /// whether or not its own table named them: its build need not wire what its source imports. + /// A plugin's root module always gets this consumer's framework modules, whether or not its + /// own table named them: its build need not wire what its source imports. It is copied unless + /// it was built against this consumer's framework (it imports this `dvui`), along with all it + /// imports; only then are the missing ones added to it in place, as they are this framework's. fn copy(self: *Rehome, name: []const u8, src: *std.Build.Module) *std.Build.Module { self.analyze(src); - self.stale.put(self.b.allocator, src, {}) catch @panic("OOM"); + const own = src.import_table.get("dvui") == self.w.dvui and !self.stale.contains(src); + if (!own) self.stale.put(self.b.allocator, src, {}) catch @panic("OOM"); self.root = src; const m = self.copyAnalyzed(name, src); - inline for (.{ "dvui", "core", "fizzy_sdk", "icons" }) |import_name| { + for (framework_names) |import_name| { if (m.import_table.get(import_name) == null) if (self.framework(import_name)) |fw| m.addImport(import_name, fw); } return m; diff --git a/build/sdk.zig b/build/sdk.zig index 6da00e346..3fb5337de 100644 --- a/build/sdk.zig +++ b/build/sdk.zig @@ -152,14 +152,55 @@ pub fn buildOptsModule(opts: *std.Build.Step.Options) *std.Build.Module { return m; } +/// The kinds of executable an app builds: the desktop ones (the plain executable and the one +/// `package` packs share a framework) and the web one. +pub const Platform = enum { native, web }; + pub const BundledPlugin = struct { /// The plugin's import name and id — `plugin_id` in its root, `plugin.zig.zon`'s `.id`. name: []const u8, /// 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 kinds of executable that link it, each with its own module if it has one (built + /// against that kind's framework). Null: every kind, with `module`. + targets: ?[]const Target = null, + + pub const Target = struct { + target: Platform, + /// Null: the plugin's `module`. + module: ?*std.Build.Module = null, + }; + + /// One of fizzy's own: no module for any executable. + pub fn isFizzys(self: BundledPlugin) bool { + if (self.module != null) return false; + for (self.targets orelse &.{}) |t| if (t.module != null) return false; + return true; + } + + /// The entry as `platform`'s executables link it, or null if they do not. + pub fn forPlatform(self: BundledPlugin, platform: Platform) ?BundledPlugin { + const targets = self.targets orelse return self; + for (targets) |t| if (t.target == platform) return .{ .name = self.name, .module = t.module orelse self.module }; + return null; + } }; +/// The plugins `platform`'s executables link, in order, each with that kind's module. +pub fn pluginsFor(b: *std.Build, plugins: []const BundledPlugin, platform: Platform) ![]const BundledPlugin { + var linked: std.ArrayList(BundledPlugin) = .empty; + for (plugins) |p| { + const entry = p.forPlatform(platform) orelse continue; + for (linked.items) |l| if (std.mem.eql(u8, l.name, entry.name)) { + std.log.err("fizzy: plugin '{s}' is listed twice", .{entry.name}); + return error.PluginListedTwice; + }; + try linked.append(b.allocator, entry); + } + return linked.items; +} + /// The generated `bundled_plugins` module: `pub const modules = .{ @import("a"), @import("b") … }` /// over the statically linked plugins, in order. The runtime (`app.App`) iterates it at /// comptime for registration, the bundled-id check and each built-in's manifest; nothing in diff --git a/build/web.zig b/build/web.zig index 401c12df9..84b9b6368 100644 --- a/build/web.zig +++ b/build/web.zig @@ -6,13 +6,17 @@ const plugins = @import("plugins.zig"); const sdk = @import("sdk.zig"); const rehome_fixture = @import("rehome_fixture.zig"); -/// What `build` made: the wasm module, and the site beside it. +/// What `build` made: the wasm module, and the site around it. pub const Web = struct { wasm: *std.Build.Step.Compile, - /// Everything but the wasm: the stamped `index.html`, `web.js`, the worker, the font, the OAuth - /// page, the hosted demo tapes under `demos/` (fizzy's own build) and each plugin's pages - /// under `plugins//`. - dir: std.Build.LazyPath, + /// The whole site, deployable as it is: `files` and `page`. + site: std.Build.LazyPath, + /// Everything but the page: the wasm, `web.js`, the worker, the font, the OAuth page, the + /// hosted demo tapes under `demos/` (fizzy's own build) and each plugin's pages under + /// `plugins//`. + files: std.Build.LazyPath, + /// `index.html`, stamped with the wasm's hash and the storage name. + page: std.Build.LazyPath, /// Fizzy's own build: a native-built plugin graph re-homed to wasm32 and compiled apart from /// the app, for `check-web`. rehome_fixture: ?*std.Build.Step.Compile, @@ -21,25 +25,8 @@ pub const Web = struct { demos: ?std.Build.LazyPath, }; -/// The web (wasm) build, apart from the native exe so it cannot disturb packaging, SDL or -/// Velopack. Adds no step of its own; `install` puts it under `zig-out/web/`. -pub fn build( - b: *std.Build, - optimize: std.lang.OptimizeMode, - build_opts: *std.Build.Step.Options, - workbench_opts: *std.Build.Step.Options, - assets_module: *std.Build.Module, - /// The window's icon (`app.Branding.png`); `src/Entry.zig` imports it. - app_icon_module: *std.Build.Module, - app_layout: ?std.Build.LazyPath, - app_plugins: []const sdk.BundledPlugin, - web_plugin_deps: []const []const u8, - web_plugin_dirs: []const @import("app.zig").WebPluginDir, - /// Fizzy's own build (`Options.own`): the hosted demo tapes come from `docs/demos/` and - /// `scripts/`, which fizzy's package leaves out, so an app built on fizzy has none. - own: bool, -) Web { - const web_target = b.resolveTargetQuery(.{ +pub fn target(b: *std.Build) std.Build.ResolvedTarget { + return b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding, .cpu_features_add = std.Target.wasm.featureSet(&.{ @@ -48,7 +35,20 @@ pub fn build( .bulk_memory, }), }); +} + +/// What the web executable links and builds its plugins against (`exe.NativeFramework`'s twin). +pub const Framework = struct { + dvui_dep: *std.Build.Dependency, + dvui: *std.Build.Module, + backend: *std.Build.Module, + core: *std.Build.Module, + sdk: *std.Build.Module, + icons: ?*std.Build.Module, +}; +pub fn framework(b: *std.Build, optimize: std.lang.OptimizeMode) Framework { + const web_target = target(b); // The web backend is fizzy's own (`src/backend/WebBackend.zig` + `web/web.js`), started as a // copy of dvui's: dvui is built in its `custom` mode and our backend linked to it, as dvui's // build does for its own (`linkBackend`). Every option is the one dvui's `web` mode sets, so @@ -78,6 +78,49 @@ pub fn build( dvui_web.addImport("backend", web_backend); const dvui_web_proxy_bridge = sdk.addProxyBridgeModule(b, web_target, optimize, dvui_web_dep, dvui_web); + // Shared `core` module for the wasm build (dvui web backend variant). + const core_module_web = b.createModule(.{ + .target = web_target, + .optimize = optimize, + .root_source_file = b.path("core/core.zig"), + .link_libc = false, + .single_threaded = true, + }); + const icons_web = core_mod.addImports(b, core_module_web, dvui_web, b.dependency("fizzy_sdk", .{}).builder, web_target, optimize, null, null); + return .{ + .dvui_dep = dvui_web_dep, + .dvui = dvui_web, + .backend = web_backend, + .core = core_module_web, + .sdk = sdk.wireSdkModule(b, web_target, optimize, dvui_web, dvui_web_proxy_bridge, core_module_web, null), + .icons = icons_web, + }; +} + +/// The web (wasm) build, apart from the native exe so it cannot disturb packaging, SDL or +/// Velopack. Adds no step of its own; `install` puts it under `zig-out/web/`. +pub fn build( + b: *std.Build, + fw: Framework, + optimize: std.lang.OptimizeMode, + build_opts: *std.Build.Step.Options, + workbench_opts: *std.Build.Step.Options, + assets_module: *std.Build.Module, + /// The window's icon (`app.Branding.png`); `src/Entry.zig` imports it. + app_icon_module: *std.Build.Module, + app_layout: ?std.Build.LazyPath, + app_plugins: []const sdk.BundledPlugin, + web_plugin_deps: []const []const u8, + web_plugin_dirs: []const @import("app.zig").WebPluginDir, + /// Fizzy's own build (`Options.own`): the hosted demo tapes come from `docs/demos/` and + /// `scripts/`, which fizzy's package leaves out, so an app built on fizzy has none. + own: bool, +) Web { + const web_target = target(b); + const dvui_web_dep = fw.dvui_dep; + const dvui_web = fw.dvui; + const web_backend = fw.backend; + const web_exe = b.addExecutable(.{ .name = "web", .root_module = b.createModule(.{ @@ -176,18 +219,12 @@ pub fn build( // wasm path is gated by `arch != .wasm32` in `auto_update.impl`. web_exe.root_module.addImport("build_opts", sdk.buildOptsModule(build_opts)); - // Shared `core` module for the wasm build (dvui web backend variant). - const core_module_web = b.createModule(.{ - .target = web_target, - .optimize = optimize, - .root_source_file = b.path("core/core.zig"), - .link_libc = false, - .single_threaded = true, - }); - const icons_web = core_mod.addImports(b, core_module_web, dvui_web, b.dependency("fizzy_sdk", .{}).builder, web_target, optimize, null, null); + const core_module_web = fw.core; + const icons_web = fw.icons; web_exe.root_module.addImport("core", core_module_web); if (icons_web) |icons| web_exe.root_module.addImport("icons", icons); - const sdk_module_web = sdk.wireSdkModule(b, web_target, optimize, dvui_web, dvui_web_proxy_bridge, core_module_web, web_exe.root_module); + const sdk_module_web = fw.sdk; + web_exe.root_module.addImport("fizzy_sdk", sdk_module_web); // Three editor files have `const sdl3 = @import("backend").c;` at file // scope. After refactoring all `sdl3.SDL_DialogFileFilter` references @@ -286,7 +323,7 @@ pub fn build( const index_html_with_hash = cb_run.captureStdOut(.{}); const dir = b.addWriteFiles(); - _ = dir.addCopyFile(index_html_with_hash, "index.html"); + _ = dir.addCopyFile(web_exe.getEmittedBin(), web_exe.out_filename); _ = dir.addCopyFile(b.path("web/web.js"), "web.js"); // Worker mode (`?worker=1`): the editor in a worker, drawing through an OffscreenCanvas. _ = dir.addCopyFile(b.path("web/fizzy-worker.js"), "fizzy-worker.js"); @@ -324,24 +361,33 @@ pub fn build( break :blk b.addObject(.{ .name = "rehome-fixture-web", .root_module = module }); } else null; - return .{ .wasm = web_exe, .dir = dir.getDirectory(), .rehome_fixture = rehome_check, .demos = demos }; + const site = b.addWriteFiles(); + _ = site.addCopyDirectory(dir.getDirectory(), "", .{}); + _ = site.addCopyFile(index_html_with_hash, "index.html"); + // For an app built on fizzy, which installs it into its own prefix: a dependency's prefix is a + // directory in the cache. + b.addNamedLazyPath("web", site.getDirectory()); + return .{ + .wasm = web_exe, + .site = site.getDirectory(), + .files = dir.getDirectory(), + .page = index_html_with_hash, + .rehome_fixture = rehome_check, + .demos = demos, + }; } /// The steps over `build`'s result: `web` installs it into `zig-out/web/`, and the ones that /// serve, copy or only compile it. pub fn install(b: *std.Build, web: Web) void { const web_install_dir: std.Build.InstallDir = .{ .custom = "web" }; - const install_wasm = b.addInstallArtifact(web.wasm, .{ - .dest_dir = .{ .override = web_install_dir }, - }); const install_site = b.addInstallDirectory(.{ - .source_dir = web.dir, + .source_dir = web.site, .install_dir = web_install_dir, .install_subdir = "", }); const web_step = b.step("web", "Build the fizzy web (wasm) app into zig-out/web/"); - web_step.dependOn(&install_wasm.step); web_step.dependOn(&install_site.step); if (web.demos) |tapes| { const install_demos = b.addInstallDirectory(.{ diff --git a/sdk/plugin_sdk.zig b/sdk/plugin_sdk.zig index 97312162c..7ff534818 100644 --- a/sdk/plugin_sdk.zig +++ b/sdk/plugin_sdk.zig @@ -559,6 +559,66 @@ pub fn create(b: *std.Build, opts: CreateOptions) PluginArtifact { return .{ .lib = lib, .module = plugin_module, .static = static_module }; } +/// One of an application's executables, as a plugin it links statically sees it: the target and +/// the framework modules (`fizzy.framework` in the application's build). A plugin package whose +/// build has dependencies of its own builds them against these, so the application links the +/// module it is given as it is, without copying anything for each executable. +/// +/// A plugin package that pins its own SDK has its own copy of this type, so its build takes the +/// application's by field: `anytype`, as `staticModule` does. +pub const Framework = struct { + target: std.Build.ResolvedTarget, + optimize: std.lang.OptimizeMode, + dvui: *std.Build.Module, + core: *std.Build.Module, + /// Imported as `fizzy_sdk`. + sdk: *std.Build.Module, + icons: ?*std.Build.Module, +}; + +pub const StaticModuleOptions = struct { + /// Defaults to `b.path("plugin.zig")`, as in `create`. + root_source_file: ?std.Build.LazyPath = null, + link_libc: bool = true, +}; + +/// The plugin's static module for one executable (`framework`, a `Framework` by field): its +/// `plugin.zig` with its manifest, importing that executable's `dvui`, `core`, `fizzy_sdk`, +/// `icons`, and `core`'s `tape` and `replay`. The plugin's own dependencies are the caller's to +/// add, built for `framework.target` and against `framework.dvui` where they draw. One that +/// `core` imports as well (zf) is `core`'s module, `framework.core.import_table.get("zf")`: a +/// file belongs to one module of a compilation. +/// +/// pub fn staticModule(b: *std.Build, framework: anytype) *std.Build.Module { +/// const m = fizzy.plugin.staticModule(b, framework, .{}); +/// m.addImport("my_lib", myLib(b, framework.target, framework.optimize, framework.dvui)); +/// return m; +/// } +/// +/// The application calls it with the plugin's builder (`dep.builder`) for each kind of +/// executable it builds, and lists the result in `fizzy.buildApp`. +pub fn staticModule(b: *std.Build, framework: anytype, opts: StaticModuleOptions) *std.Build.Module { + const wasm = framework.target.result.cpu.arch == .wasm32; + const m = readManifest(b); + const mod = b.createModule(.{ + .target = framework.target, + .optimize = framework.optimize, + .root_source_file = opts.root_source_file orelse b.path("plugin.zig"), + .link_libc = opts.link_libc and !wasm, + .single_threaded = if (wasm) true else null, + }); + mod.addAnonymousImport("plugin_zon", .{ .root_source_file = b.path("plugin.zig.zon") }); + mod.addOptions(plugin_options_import, pluginOptions(b, m.id, m.name, m.version, m.min_sdk_version, m.raw)); + mod.addImport("dvui", framework.dvui); + mod.addImport("core", framework.core); + mod.addImport("fizzy_sdk", framework.sdk); + if (framework.icons) |icons| mod.addImport("icons", icons); + for ([_][]const u8{ "tape", "replay" }) |name| { + if (framework.core.import_table.get(name)) |dep| mod.addImport(name, dep); + } + return mod; +} + pub fn exportModules( b: *std.Build, target: std.Build.ResolvedTarget, From 6b7f323670e3af6e58d06ec74816acc9b2e2a222 Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:21:08 -0500 Subject: [PATCH 2/2] sdk: give staticModule's anytype its reason Agent-Team: linux Worker: claude Co-Authored-By: Claude Sonnet 5.5 --- sdk/plugin_sdk.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/plugin_sdk.zig b/sdk/plugin_sdk.zig index 7ff534818..102399055 100644 --- a/sdk/plugin_sdk.zig +++ b/sdk/plugin_sdk.zig @@ -597,6 +597,7 @@ pub const StaticModuleOptions = struct { /// /// The application calls it with the plugin's builder (`dep.builder`) for each kind of /// executable it builds, and lists the result in `fizzy.buildApp`. +// anytype: the Framework of fizzy's build, a build-time duck type pub fn staticModule(b: *std.Build, framework: anytype, opts: StaticModuleOptions) *std.Build.Module { const wasm = framework.target.result.cpu.arch == .wasm32; const m = readManifest(b);