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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ jobs:
if: matrix.name == 'macOS'
run: bash scripts/ci-retry.sh zig build -Dnative-backend=sdl3 -Dno-emit --summary all

# A link-only app (`-Ddynamic-plugins=false`): its gates on loading plugins at runtime are
# comptime, so only a build with it off shows that build still compiles.
- name: Compile a link-only app
if: matrix.name == 'Linux'
run: bash scripts/ci-retry.sh zig build -Ddynamic-plugins=false -Dno-emit --summary all

# The suites for SDL's patches on macOS (FizzyGPU's Metal tests; the rest are Linux's), when
# SDL's patches or pins changed: see the `sdl` job. Is there a Metal device? The probe says,
# and the suite then requires Metal, so a runner with one can't skip the Metal tests. (On
Expand Down
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ every list must include it) and markdown (the store draws READMEs with it). An a
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
`link`), and only those listed register or get a dylib. `.@"dynamic-plugins" = false` (or
`-Ddynamic-plugins=false`) makes an app link-only: no plugin dylib is built, and nothing loads
at runtime (fizzy's dylibs, the user plugins directory, the store, a web page's `?plugin=`),
all decided at comptime (`App.loads_plugins`). See
[`fizzyedit/example-app`](https://github.com/fizzyedit/example-app), which bundles
`fizzyedit/example-plugin`.

Expand Down
9 changes: 7 additions & 2 deletions app/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1141,12 +1141,17 @@ pub fn syncLoadedPluginRenderBridge(app: *App) void {
}
}

/// Whether this build loads plugins at runtime (`-Ddynamic-plugins`, default on): fizzy's own
/// dylibs, the user plugins directory, the store, and on the web a page's `?plugin=`. Off, the
/// app runs exactly the plugins it links.
pub const loads_plugins = if (@hasDecl(build_opts, "dynamic_plugins")) build_opts.dynamic_plugins else true;

/// Whether a bundled plugin should be loaded from `{exe_dir}/plugins/<id>/` rather than the
/// copy linked in: never on wasm, not when the build pinned it static
/// copy linked in: never on wasm or in a link-only build, not when the build pinned it static
/// (`build_opts.static_<id>`), and not when `FIZZY_STATIC_<ID>=1` is set — the bisection
/// switch for dylib loading trouble.
pub fn bundledDylibEnabled(gpa: std.mem.Allocator, comptime id: []const u8) bool {
if (comptime builtin.target.cpu.arch == .wasm32) return false;
if (comptime builtin.target.cpu.arch == .wasm32 or !loads_plugins) return false;
if (comptime @hasDecl(build_opts, "static_" ++ id)) {
if (comptime @field(build_opts, "static_" ++ id)) return false;
}
Expand Down
5 changes: 5 additions & 0 deletions app/store/PluginStore.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const store = @import("registry/store.zig");
const Page = @import("PluginPage.zig");
const PluginLoader = @import("PluginLoader.zig");
const PluginManager = @import("PluginManager.zig");
const App = @import("../App.zig");
/// Pretend two installed plugins have store updates waiting, so the update flow (the app's
/// offer, or the silent path's logging) can be exercised without an actually out-of-date
/// plugin. A build-time flag: flip it, run, flip it back.
Expand Down Expand Up @@ -406,6 +407,9 @@ const app = &PluginManager.current;

pub fn register(manager: PluginManager) !void {
PluginManager.current = manager;
// A link-only build has no store: nothing to fetch, nowhere to install to. The manager is
// still set, because settings name plugins through it (`displayName`).
if (comptime !App.loads_plugins) return;
const host = manager.host;
const url = resolveRegistryUrl();
const fp_hex = try std.fmt.allocPrint(manager.gpa, "0x{x}", .{dylib.abi_fingerprint});
Expand Down Expand Up @@ -1333,6 +1337,7 @@ fn offerPendingUpdates() void {
/// before the Host-registry iterations, so a freshly-registered or unloaded plugin never
/// mutates a list mid-iteration.
pub fn tick() void {
if (comptime !App.loads_plugins) return;
if (comptime builtin.target.cpu.arch == .wasm32) {
if (catalog) |*c| c.pump(web_fetch);
StoreIcon.pump();
Expand Down
13 changes: 11 additions & 2 deletions build/app.zig
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub const Config = struct {
static_workbench: bool,
static_text: bool,
static_image: bool,
dynamic_plugins: bool,
workbench_opts: *std.Build.Step.Options,
/// Null on the configure pass that fetches Velopack (see `readConfig`).
msvcup_before_compile: ?*std.Build.Step.Run,
Expand Down Expand Up @@ -227,6 +228,11 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options
build_opts.addOption([]const u8, "app_repo_url", app_repo_url);
build_opts.addOption([]const u8, "app_repo_url_fallback", app_repo_url_fallback);
build_opts.addOption(bool, "velopack_enabled", velopack_enabled);
// Off, the app runs only the plugins it links (`buildApp`'s list): no plugin dylib is built,
// and nothing loads at runtime — not fizzy's own dylibs, not the user plugins directory, not
// the store, not a web page's `?plugin=`. Comptime, so none of it is checked per frame.
const dynamic_plugins = b.option(bool, "dynamic-plugins", "Load plugins at runtime: fizzy's own dylibs, the user plugins directory, the store (default: true)") orelse true;
build_opts.addOption(bool, "dynamic_plugins", dynamic_plugins);

// A consumer that wants its own shape passes `-Dapp-layout=` (a LazyPath to
// `pub fn layout(?*anyopaque, *Layout)`). Fizzy itself uses `src/editor/layout.zig`.
Expand Down Expand Up @@ -303,6 +309,7 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options
.static_workbench = static_workbench,
.static_text = static_text,
.static_image = static_image,
.dynamic_plugins = dynamic_plugins,
.workbench_opts = workbench_opts,
.msvcup_before_compile = msvcup_before_compile,
.accesskit = accesskit,
Expand Down Expand Up @@ -354,6 +361,7 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.
const static_workbench = cfg.static_workbench;
const static_text = cfg.static_text;
const static_image = cfg.static_image;
const dynamic_plugins = cfg.dynamic_plugins;
const workbench_opts = cfg.workbench_opts;
const msvcup_before_compile = cfg.msvcup_before_compile;
const accesskit = cfg.accesskit;
Expand All @@ -377,7 +385,7 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.

web.addSteps(b, optimize, build_opts, workbench_opts, assets_module, opts.app_plugins, opts.web_plugin_deps, opts.web_plugin_dirs);

const main_fizzy = try fizzy_exe.addFizzyExecutableForTarget(b, vz, target, optimize, accesskit, native_backend, build_opts, workbench_opts, assets_module, macos_sdl_paths, velopack_enabled, app_name, app_layout_path, opts.app_plugins);
const main_fizzy = try fizzy_exe.addFizzyExecutableForTarget(b, vz, target, optimize, accesskit, native_backend, build_opts, workbench_opts, assets_module, macos_sdl_paths, velopack_enabled, app_name, app_layout_path, opts.app_plugins, dynamic_plugins);
const exe = main_fizzy.exe;

const package_fizzy: FizzyExecutable = package_blk: {
Expand All @@ -399,8 +407,9 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.
pack_opts.addOption(bool, "static_workbench", static_workbench);
pack_opts.addOption(bool, "static_text", static_text);
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, macos_sdl_paths, true, app_name, app_layout_path, opts.app_plugins);
break :package_blk try fizzy_exe.addFizzyExecutableForTarget(b, vz, target, optimize, accesskit, native_backend, pack_opts, workbench_opts, assets_module, macos_sdl_paths, true, app_name, app_layout_path, opts.app_plugins, dynamic_plugins);
};
const exe_for_package = package_fizzy.exe;

Expand Down
10 changes: 6 additions & 4 deletions build/exe.zig
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ pub fn addFizzyExecutableForTarget(
/// package (`plugin_dep.module("plugin")`). Listed in the generated `bundled_plugins` module
/// under its plugin id.
app_plugins: []const sdk.BundledPlugin,
/// 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;
Expand Down Expand Up @@ -341,7 +343,7 @@ pub fn addFizzyExecutableForTarget(
exe.root_module.addImport("app_layout", app_layout_mod);
}

const workbench_dylib: ?*std.Build.Step.Compile = if (resolved_target.result.cpu.arch != .wasm32) blk: {
const workbench_dylib: ?*std.Build.Step.Compile = if (dynamic_plugins and resolved_target.result.cpu.arch != .wasm32) blk: {
break :blk workbench_plugin.addDylib(b, resolved_target, optimize, .{
.dvui = dvui_proxy_mod,
.core = core_proxy_module,
Expand All @@ -352,7 +354,7 @@ pub fn addFizzyExecutableForTarget(
}, workbench_opts);
} else null;

const text_dylib: ?*std.Build.Step.Compile = if (resolved_target.result.cpu.arch != .wasm32 and plugins.linksFizzys(app_plugins, "text")) blk: {
const text_dylib: ?*std.Build.Step.Compile = if (dynamic_plugins and 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 @@ -362,7 +364,7 @@ pub fn addFizzyExecutableForTarget(
});
} else null;

const markdown_dylib: ?*std.Build.Step.Compile = if (resolved_target.result.cpu.arch != .wasm32 and plugins.linksFizzys(app_plugins, "markdown")) blk: {
const markdown_dylib: ?*std.Build.Step.Compile = if (dynamic_plugins and 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 @@ -371,7 +373,7 @@ pub fn addFizzyExecutableForTarget(
});
} else null;

const image_dylib: ?*std.Build.Step.Compile = if (resolved_target.result.cpu.arch != .wasm32 and plugins.linksFizzys(app_plugins, "image")) blk: {
const image_dylib: ?*std.Build.Step.Compile = if (dynamic_plugins and 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
6 changes: 3 additions & 3 deletions src/editor/Editor.zig
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ pub const view_settings = "fizzy.settings";
pub const workbench_files_view = workbench_mod.view_files;

pub fn loadUserPlugins(editor: *Editor, config_folder: []const u8) void {
if (comptime builtin.target.cpu.arch == .wasm32) return;
if (comptime builtin.target.cpu.arch == .wasm32 or !App.loads_plugins) return;

const plugins_dir = std.fs.path.join(editor.app.gpa, &.{ config_folder, "plugins" }) catch return;
defer editor.app.gpa.free(plugins_dir);
Expand Down Expand Up @@ -1424,7 +1424,7 @@ pub const UnloadError = error{ NotUnloadable, DirtyDocuments };
/// dvui/render-bridge sync path as `loadUserPlugins`. Caller ensures `id` is not already
/// registered. On success the lib is appended to `loaded_plugin_libs`.
pub fn loadUserPluginById(editor: *Editor, id: []const u8) !void {
if (comptime builtin.target.cpu.arch == .wasm32) return error.NotUnloadable;
if (comptime builtin.target.cpu.arch == .wasm32 or !App.loads_plugins) return error.NotUnloadable;
const path = try App.userPluginPath(editor.app.gpa, &editor.app, id);
const opened = PluginLoader.open(editor.app.gpa, path, id) catch |err| {
editor.userPluginLoadFailed(id, path, err);
Expand Down Expand Up @@ -1632,7 +1632,7 @@ const WebPluginRequest = struct {
/// The page asks for a plugin: `?plugin=<id>` on the URL (`plugins/<id>/<id>.wasm` beside
/// the app), or one it remembered from a store install (by its URL).
export fn FizzyWebPluginRequest(id_ptr: [*]const u8, id_len: usize, url_ptr: [*]const u8, url_len: usize) void {
if (comptime builtin.target.cpu.arch != .wasm32) return;
if (comptime builtin.target.cpu.arch != .wasm32 or !App.loads_plugins) return;
const editor = web_editor orelse return;
const id = id_ptr[0..id_len];
// The id reaches this from the page's query string, and with no URL it is interpolated
Expand Down
Loading