Skip to content
Open
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
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ Plugins depend on the **`sdk/` package** (its own `build.zig` + `build.zig.zon`)
Pattern:

- **Plugins** (built-in + third-party): `.fizzy = .{ .path = ".../sdk" }` locally, or the `fizzy-sdk-v*` **release asset** URL from the matching `sdk-v*` tag (not the git archive — that is the monorepo root zon with Velopack). Call `fizzy.plugin.create` / `.install` as before; `b.dependency("fizzy", .{ .plugin_sdk = true })` still works (the option is accepted and ignored — `sdk/` always exports modules). Packing: `scripts/pack-sdk.sh` / `.github/workflows/sdk-tag.yml`.
- **App**: repo-root `zig build` as usual. Velopack stays `.lazy = true` in the root zon; never `@import("velopack_zig")` — the helper surface is vendored in `build/velopack.zig` and resolved only in `build/app.zig` via `lazyDependency`.
- **App**: repo-root `zig build` as usual. The app **consumes `sdk/` as a dependency** (`.fizzy_sdk = .{ .path = "sdk/" }`), so build scripts reach `plugin`/`core_module`/`sdk_version` through `@import("fizzy_sdk")` and never by relative path into `sdk/` — a file may belong to only one module, so a path import claims it for the root build module and breaks the dependency outright. The same applies in reverse: nothing under `src/` may relative-import an `sdk/` file. Velopack stays `.lazy = true` in the root zon; never `@import("velopack_zig")` — the helper surface is vendored in `build/velopack.zig` and resolved only in `build/app.zig` via `lazyDependency`.
- **dvui is pinned in exactly one place — `sdk/build.zig.zon` — and is deliberately absent from the root zon.** The app borrows it via `build/sdk.zig`'s `dvuiDependency` (which forwards backend/target/optimize normally), and build scripts get dvui's build API from `@import("fizzy_sdk").dvui`. Do **not** "fix" the missing root dep by re-adding `.dvui`: two pins that drift make `recorded_sdk_shape_fingerprint` unsatisfiable by *both* the app and plugin-SDK builds at once, and the resulting error tells you to bump `sdk_version`, which cannot help. Bump or swap to a local checkout in `sdk/build.zig.zon` only.
- Shared `core` import wiring lives in `sdk/core_module.zig` and is called from the app build *and* `sdk/plugin_sdk.zig`'s `exportModules` so the import set can't drift. Note the `with_tui = false` on the zf dependency: without it, zf's standalone terminal binary drags `libvaxis` into every plugin build.

Acceptance test after any build-graph change:
Expand Down
6 changes: 5 additions & 1 deletion build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ const std = @import("std");

/// App-side re-export of the plugin build API (lives in `sdk/`). Plugins should depend on
/// the `sdk/` package directly — see CLAUDE.md — not this root package.
pub const plugin = @import("sdk/plugin_sdk.zig");
///
/// Reached through the dependency rather than by path (`sdk/plugin_sdk.zig`): the app consumes
/// `sdk/` as a package so the two can share one dvui pin, and a file may belong to only one module,
/// so claiming these for the root's build module would make that impossible.
pub const plugin = @import("fizzy_sdk").plugin;

pub fn build(b: *std.Build) !void {
const windows_msvc_libc_opt = b.option([]const u8, "windows-msvc-libc", "zig libc manifest for *-windows-msvc when cross-compiling; forwarded by packageall for Windows children") orelse null;
Expand Down
6 changes: 2 additions & 4 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,8 @@
.hash = "icons-0.0.0-iJxA-VvGMwAgiKSXRe_Y0O7RpasdtEJhBfVx8IGGEBl_",
.lazy = true,
},
.dvui = .{
.url = "https://github.com/foxnne/dvui-dev/archive/ed2f1c67f0316184783c8dba7d79ed4c49d26f97.tar.gz",
.hash = "dvui-0.5.0-dev-AQFJmX1d_QA2wHjWCweU26ZxqIrA9LwWeysGFbfVMc7y",
//.path = "../dvui-dev",
.fizzy_sdk = .{
.path = "sdk/",
},
.assetpack = .{
.url = "https://github.com/foxnne/assetpack/archive/ac7592f3f5988857840d0df4610e1e1fad690e2e.tar.gz",
Expand Down
66 changes: 60 additions & 6 deletions build/app.zig
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
const std = @import("std");

const plugin = @import("../sdk/plugin_sdk.zig");
const core_mod = @import("../sdk/core_module.zig");
const dvui = @import("dvui");
// Through the `sdk/` dependency, not by relative path — see `build/sdk.zig`'s `dvuiDependency` for
// why the app consumes the SDK as a package, and `sdk/build.zig` for what it exposes. dvui's build
// API arrives the same way because `sdk/` owns the repo's only dvui pin.
const fizzy_sdk = @import("fizzy_sdk");
const plugin = fizzy_sdk.plugin;
const core_mod = fizzy_sdk.core_module;
const dvui = fizzy_sdk.dvui;
const velopack = @import("velopack.zig");

pub const Options = struct {
Expand Down Expand Up @@ -367,6 +371,10 @@ pub fn build(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
// below never reaches it (nothing in the graph forces `sdk.manifest`), so it
// needs its own root either way.
.{ "fizzy-sdk-manifest-tests", "src/sdk/manifest.zig" },
// The `[[wikilink]]` tokenizer. std-only on purpose: it's shared verbatim by the
// markdown renderer and by out-of-tree indexers, so it must not depend on dvui or
// anything else the SDK-rooted artifact drags in.
.{ "fizzy-sdk-wikilink-tests", "src/sdk/services/wikilink.zig" },
// The text plugin's headless editing model. Lives under src/plugins/ but is
// deliberately dvui-free (see textcore.zig), so it tests as pure logic from the
// app build. One root covers every file below it — they're relative imports.
Expand All @@ -386,6 +394,9 @@ pub fn build(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
// Content-swap reveal phase machine. std-only by design (see reveal.zig) — the dvui
// half is the thin wrapper in core/dvui.zig.
.{ "fizzy-reveal-tests", "src/core/reveal.zig" },
// Ring buffering and dot-segment filtering for the folder watcher. std-only so it can
// be tested here; FolderWatcher.zig itself needs a live editor.
.{ "fizzy-folder-events-tests", "src/editor/folder_events.zig" },
}) |entry| {
try unit_test_artifacts.append(b.allocator, b.addTest(.{
.name = entry[0],
Expand Down Expand Up @@ -448,7 +459,7 @@ pub fn build(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
return;
}

const dvui_testing_dep = b.dependency("dvui", .{
const dvui_testing_dep = sdk.dvuiDependency(b, .{
.target = target,
.optimize = optimize,
.backend = .testing,
Expand Down Expand Up @@ -481,7 +492,12 @@ pub fn build(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
const icons_test = core_mod.addImports(b, core_module_test, dvui_testing_dep.module("dvui_testing"), target, optimize);
fizzy_test_module.addImport("core", core_module_test);
if (icons_test) |icons| fizzy_test_module.addImport("icons", icons);
if (b.lazyDependency("nightwatch", .{ .target = target, .optimize = optimize })) |dep| {
// See `exe.zig` for why macOS needs the FSEvents backend.
const nightwatch_test_dep = if (target.result.os.tag == .macos)
b.lazyDependency("nightwatch", .{ .target = target, .optimize = optimize, .macos_fsevents = true })
else
b.lazyDependency("nightwatch", .{ .target = target, .optimize = optimize });
if (nightwatch_test_dep) |dep| {
fizzy_test_module.addImport("nightwatch", dep.module("nightwatch"));
}

Expand All @@ -499,7 +515,7 @@ pub fn build(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
.sdk = sdk_module_test,
.icons = icons_test,
}, fizzy_test_module);
_ = plugins.markdown.addStaticModule(b, target, optimize, .{
const markdown_module_test = plugins.markdown.addStaticModule(b, target, optimize, .{
.dvui = dvui_testing_dep.module("dvui_testing"),
.core = core_module_test,
.sdk = sdk_module_test,
Expand Down Expand Up @@ -536,6 +552,13 @@ pub fn build(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
// built above rather than rooting a second one at the widget — a file may belong to only
// one module per compilation, and the plugin's own module already owns it.
integration_module.addImport("text", text_module_test);
// Same reasoning for the markdown preview: its block virtualization is a claim about what
// gets *drawn*, which only a real headless frame can check.
integration_module.addImport("markdown", markdown_module_test);
integration_module.addAnonymousImport("markdown_sample", .{ .root_source_file = b.path("docs/PLUGINS.md") });
// The document with the 45KB table — the case table-row culling exists for, and the one it
// could get wrong.
integration_module.addAnonymousImport("markdown_sample_tables", .{ .root_source_file = b.path("docs/PLUGIN_MANIFEST_PLAN.md") });

const integration_tests = b.addTest(.{
.name = "fizzy-integration-tests",
Expand Down Expand Up @@ -596,6 +619,37 @@ pub fn build(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
bench_step.dependOn(&run_bench.step);
}

// `zig build bench-markdown` — markdown preview frame-cost benchmark. Same rules as
// `bench-text` above: its own step, prints timings instead of asserting, only comparable at
// equal `-Doptimize` (cmark and freetype build at the app's optimize level).
{
const bench_module = b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("tests/bench/bench_markdown.zig"),
});
bench_module.addImport("dvui", dvui_testing_dep.module("dvui_testing"));
bench_module.addImport("markdown", markdown_module_test);
// This repo's own docs, as anonymous imports rather than checked-in fixtures — the same
// reasoning as `bench-text`'s samples. `PLUGINS.md` is the document that prompted the
// benchmark.
bench_module.addAnonymousImport("sample_huge", .{ .root_source_file = b.path("docs/PLUGINS.md") });
bench_module.addAnonymousImport("sample_prose", .{ .root_source_file = b.path("docs/PLUGIN_MANIFEST_PLAN.md") });
bench_module.addAnonymousImport("sample_medium", .{ .root_source_file = b.path("CLAUDE.md") });
bench_module.addAnonymousImport("sample_small", .{ .root_source_file = b.path("docs/MODULARIZATION_RELEASE_NOTES.md") });

const bench_markdown = b.addTest(.{ .name = "fizzy-bench-markdown", .root_module = bench_module });
bench_markdown.root_module.link_libcpp = !target_is_windows_msvc;
if (target.result.os.tag == .windows) {
bench_markdown.root_module.linkSystemLibrary("comctl32", .{});
}

const bench_step = b.step("bench-markdown", "Benchmark the markdown preview's per-frame draw cost (prints timings)");
const run_bench = b.addRunArtifact(bench_markdown);
run_bench.has_side_effects = true;
bench_step.dependOn(&run_bench.step);
}

// Pure-logic tests that nevertheless sit in a file importing `dvui` (or the SDK)
// can't join the unit layer, so they get their own roots here. Rooting at
// `src/sdk/sdk.zig` collects every SDK file reachable from it by relative
Expand Down
2 changes: 1 addition & 1 deletion build/common.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const std = @import("std");

const plugin = @import("../sdk/plugin_sdk.zig");
const plugin = @import("fizzy_sdk").plugin;
const update = @import("../update.zig");
const GitDependency = update.GitDependency;

Expand Down
23 changes: 16 additions & 7 deletions build/exe.zig
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const std = @import("std");
const dvui = @import("dvui");
// dvui's build API via the SDK package, which owns the repo's only dvui pin.
const dvui = @import("fizzy_sdk").dvui;
// Vendored Velopack glue — see build/velopack.zig header (never `@import("velopack_zig")`).
const velopack = @import("velopack.zig");
const plugin = @import("../sdk/plugin_sdk.zig");
const core_mod = @import("../sdk/core_module.zig");
const plugin = @import("fizzy_sdk").plugin;
const core_mod = @import("fizzy_sdk").core_module;
const common = @import("common.zig");
const plugins = @import("plugins.zig");
const sdk = @import("sdk.zig");
Expand Down Expand Up @@ -79,7 +80,7 @@ pub fn addFizzyExecutableForTarget(
velopack_enabled: bool,
) !FizzyExecutable {
const dvui_dep = if (macos_sdl_paths) |p|
b.dependency("dvui", .{
sdk.dvuiDependency(b, .{
.target = resolved_target,
.optimize = optimize,
.backend = .sdl3,
Expand All @@ -89,9 +90,9 @@ pub fn addFizzyExecutableForTarget(
.library_path = p.lib,
})
else
b.dependency("dvui", .{ .target = resolved_target, .optimize = optimize, .backend = .sdl3, .accesskit = accesskit });
sdk.dvuiDependency(b, .{ .target = resolved_target, .optimize = optimize, .backend = .sdl3, .accesskit = accesskit });

const dvui_proxy_dep = b.dependency("dvui", .{
const dvui_proxy_dep = sdk.dvuiDependency(b, .{
.target = resolved_target,
.optimize = optimize,
.backend = .proxy,
Expand Down Expand Up @@ -149,7 +150,15 @@ pub fn addFizzyExecutableForTarget(
});
_ = core_mod.addImports(b, core_proxy_module, dvui_proxy_mod, resolved_target, optimize);

if (b.lazyDependency("nightwatch", .{ .target = resolved_target, .optimize = optimize })) |dep| {
// `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
// shape that exhausts the fd limit on a real repo. FSEvents covers the subtree with one
// stream. The option only exists when nightwatch is built for macOS, hence the split.
const nightwatch_dep = if (resolved_target.result.os.tag == .macos)
b.lazyDependency("nightwatch", .{ .target = resolved_target, .optimize = optimize, .macos_fsevents = true })
else
b.lazyDependency("nightwatch", .{ .target = resolved_target, .optimize = optimize });
if (nightwatch_dep) |dep| {
exe.root_module.addImport("nightwatch", dep.module("nightwatch"));
}

Expand Down
24 changes: 24 additions & 0 deletions build/sdk.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
const std = @import("std");

/// The repo's one dvui, borrowed from the `sdk/` package instead of pinned by the app.
///
/// dvui is not a dependency of the root package at all: `sdk/build.zig.zon` declares the only pin
/// and this reaches through to it, so there is a single place to bump a version or point at a local
/// checkout. `args` is forwarded to dvui's own build untouched (backend, target, optimize, …), so
/// callers keep full control of *how* it is built; only *which* dvui is shared.
///
/// Worth the indirection because the two are not free to disagree. dvui types reachable from the
/// plugin boundary feed `dylib.sdk_shape_fingerprint`, which both the app build and the plugin-SDK
/// build check against the single `recorded_sdk_shape_fingerprint` literal in `src/sdk/version.zig`.
/// When each build compiled a different dvui, they computed different fingerprints from that one
/// literal and no value satisfied both — every fix broke the other side, and the error blamed
/// `sdk_version`, which a bump cannot repair. One pin makes that state unreachable rather than
/// merely discouraged.
///
/// The direction is forced: `sdk/` ships standalone as `fizzy-sdk-v*.tar.gz` for third-party
/// plugins, so it must carry its own pin and can never read anything above its own root. The app
/// can always reach down into it.
pub fn dvuiDependency(b: *std.Build, args: anytype) *std.Build.Dependency {
// Only the SDK package's resolved dependency table is wanted here, not its artifacts, so its
// own target/optimize are left at default; `args` carries the target dvui is really built for.
return b.dependency("fizzy_sdk", .{}).builder.dependency("dvui", args);
}

pub fn addProxyBridgeModule(
b: *std.Build,
target: std.Build.ResolvedTarget,
Expand Down
4 changes: 2 additions & 2 deletions build/web.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const std = @import("std");
const core_mod = @import("../sdk/core_module.zig");
const core_mod = @import("fizzy_sdk").core_module;
const plugins = @import("plugins.zig");
const sdk = @import("sdk.zig");

Expand All @@ -24,7 +24,7 @@ pub fn addSteps(
}),
});

const dvui_web_dep = b.dependency("dvui", .{
const dvui_web_dep = sdk.dvuiDependency(b, .{
.target = web_target,
.optimize = optimize,
.backend = .web,
Expand Down
Loading
Loading