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
5 changes: 5 additions & 0 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
.fizzy_sdk = .{
.path = "sdk/",
},
// A plugin that imports `tape` and `replay`, linked into the test build as an app would.
.tape_fixture = .{
.path = "tests/fixtures/tape-plugin",
.lazy = true,
},
// Fizzy's native backend (SDL3, its GPU renderer, the platform pieces), with SDL,
// zig-objc and zigwin32 pinned there. App-only: plugins never link it.
.fizzy_backend = .{
Expand Down
33 changes: 33 additions & 0 deletions build/app.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1513,6 +1513,39 @@ fn addTests(
const fixture_test = b.addTest(.{ .name = "rehome-fixture", .root_module = module });
test_step.dependOn(&b.addRunArtifact(fixture_test).step);
}
// What a plugin does with `tape` and `replay` (`tests/fixtures/tape-plugin/play.zig`), through
// the path an application's plugin takes: its own `staticModule`, then `rehome` onto this
// executable's framework. The fixture's own build tests the same file as a dylib.
// Fizzy's own build only: the fixture is not in `.paths`, so an app fetching fizzy lacks it.
if (cfg.app_is_fizzy) if (b.lazyDependency("tape_fixture", .{})) |fixture_dep| {
const source = b.lazyImport(@This(), "tape_fixture").?.staticModule(fixture_dep.builder, .{
.target = target,
.optimize = optimize,
.dvui = dvui_testing_dep.module("dvui_testing"),
.core = core_module_test,
.sdk = sdk_module_test,
.icons = icons_test,
});
const consumer = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true });
const module = @import("plugins.zig").rehome(b, .{
.target = target,
.optimize = optimize,
.dvui = dvui_testing_dep.module("dvui_testing"),
.core = core_module_test,
.sdk = sdk_module_test,
.icons = icons_test,
.backend = null,
.workbench_opts = workbench_opts,
.consumer = consumer,
}, "tape_fixture", source);
const play_tests = b.addTest(.{
.name = "fizzy-tape-plugin-bundled-tests",
.root_module = module,
.filters = test_filters,
});
test_integration_step.dependOn(&b.addRunArtifact(play_tests).step);
check_integration_step.dependOn(&play_tests.step);
};
const workbench_module_test = workbench_plugin.addStaticModule(b, target, optimize, .{
.dvui = dvui_testing_dep.module("dvui_testing"),
.core = core_module_test,
Expand Down
3 changes: 3 additions & 0 deletions build/sdk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ pub fn wireSdkModule(
sdk_module.addImport("dvui", dvui_module);
sdk_module.addImport("proxy_bridge", proxy_bridge_module);
sdk_module.addImport("core", core_module);
// Their data types are on the plugin boundary (`sdk/src/dylib.zig`).
sdk_module.addImport("tape", core_module.import_table.get("tape").?);
sdk_module.addImport("replay", core_module.import_table.get("replay").?);
// `sdk_version` as a *named* module rather than a relative `@import`: `sdk/sdk_version.zig`
// is the single edit site for the triplet, but it sits outside this module's root
// (`sdk/src/`), and Zig confines a module's relative imports to its own root — a named
Expand Down
5 changes: 5 additions & 0 deletions scripts/check-examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ if [[ -n $builds ]]; then
printf '%s\n' $builds | xargs -P "${EXAMPLES_JOBS:-3}" -n 1 bash -c 'build_one "$1"' _
fi

# A plugin that imports `tape` and `replay` builds and plays a tape as a dylib too
# (tests/fixtures/tape-plugin; the same file is tested linked into the app by `test-integration`).
echo "== tape-plugin fixture, as a dylib"
(cd "$root/tests/fixtures/tape-plugin" && zig build test --cache-dir "$work/tape-plugin-cache" --prefix "$work/tape-plugin-out")

# An app's list is exact: one naming none of fizzy's, workbench included, fails while build.zig
# configures, which compiles nothing. The build must fail and say which plugin is missing.
echo "== app-only fixture, expected to fail naming fizzy.plugins.workbench"
Expand Down
4 changes: 4 additions & 0 deletions sdk/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ file is the rules for changing it.
`sdk_version.zig` outside a release PR (`CONTRIBUTING.md`, "Changing the SDK").
- **A new boundary type goes in `sdk_boundary_types`** (`src/dylib.zig`), or the fingerprint never
sees it change.
- **`tape` and `replay` are boundary too.** `tape.binary.version` is folded into the shape
fingerprint and the tape data types are in `tape_boundary_types` (`src/dylib.zig`): bump the version or
change one, and the fingerprint moves. A store plugin imports both, as a bundled one does
(`plugin.create`; `tests/fixtures/tape-plugin`).
- **This directory ships on its own** as `fizzy-sdk-v*.tar.gz` (`scripts/pack-sdk.sh`), holding
only what `build.zig.zon`'s `.paths` lists. A new top-level file here goes in that list too;
`scripts/check-examples.sh` builds the example plugin from the packed tarball and catches it.
Expand Down
10 changes: 10 additions & 0 deletions sdk/plugin_sdk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ pub const Modules = struct {
sdk: *std.Build.Module,
dvui: *std.Build.Module,
proxy_bridge: *std.Build.Module,
/// `core`'s own `tape` and `replay`, the instances a statically linked plugin imports.
tape: *std.Build.Module,
replay: *std.Build.Module,
};

pub const ModulesOptions = struct {
Expand Down Expand Up @@ -164,6 +167,8 @@ fn modulesFromDep(fizzy_dep: *std.Build.Dependency) Modules {
.sdk = fizzy_dep.module("fizzy_sdk"),
.dvui = fizzy_dep.module("dvui"),
.proxy_bridge = fizzy_dep.module("proxy_bridge"),
.tape = fizzy_dep.module("tape"),
.replay = fizzy_dep.module("replay"),
};
}

Expand All @@ -176,6 +181,8 @@ pub fn addImports(mod: *std.Build.Module, plugin_modules: Modules) void {
mod.addImport("fizzy_sdk", plugin_modules.sdk);
mod.addImport("dvui", plugin_modules.dvui);
mod.addImport("proxy_bridge", plugin_modules.proxy_bridge);
mod.addImport("tape", plugin_modules.tape);
mod.addImport("replay", plugin_modules.replay);
}

fn module(
Expand Down Expand Up @@ -662,6 +669,9 @@ pub fn exportModules(
sdk_mod.addImport("dvui", dvui_proxy_mod);
sdk_mod.addImport("proxy_bridge", proxy_bridge_mod);
sdk_mod.addImport("core", core_mod);
// The fingerprint hashes `tape`'s and `replay`'s data types (`dylib.zig`).
sdk_mod.addImport("tape", core_mod.import_table.get("tape").?);
sdk_mod.addImport("replay", core_mod.import_table.get("replay").?);
// See the matching comment in `build/sdk.zig`. `sdk_version.zig` needs no `repoPath` hop:
// unlike `src/…`, it lives at this package's root in *both* layouts (in-repo `fizzy/sdk/`
// and the release tarball, whose root is that same directory).
Expand Down
34 changes: 34 additions & 0 deletions sdk/src/dylib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const builtin = @import("builtin");
const dvui = @import("dvui");
const proxy_bridge = @import("proxy_bridge");
const core = @import("core");
const tape = @import("tape");
const replay = @import("replay");
const fingerprint = @import("fingerprint.zig");
const dvui_context = @import("dvui_context.zig");
const runtime = @import("runtime.zig");
Expand Down Expand Up @@ -189,6 +191,36 @@ const sdk_boundary_types = .{
VersionTriplet,
};

/// A plugin's demo crosses the boundary as bytes (`Demo.tape`), so these are shared by what they
/// spell, not by pointer: `tape.binary.version` is folded in `sdk_shape_fingerprint`, and the
/// data types a tape is made of are hashed here. `Stage` is the seam a plugin's app fills in.
const tape_boundary_types = .{
tape.Tape,
tape.Tape.Op,
tape.Tape.Action,
tape.Tape.Button,
tape.Tape.Command,
tape.Tape.Step,
tape.Tape.Scroll,
tape.Tape.Target,
tape.Tape.Wait,
tape.Tape.Skip,
tape.Tape.Until,
tape.Tape.Caption,
tape.Tape.Chapter,
tape.Tape.Keyframe,
tape.Tape.Setting,
tape.Tape.File,
tape.Tape.Check,
tape.Sequencer.Point,
tape.Sequencer.Failure,
tape.Sequencer.Sink,
tape.Sequencer.Sink.VTable,
replay.Player,
replay.Stage,
replay.Stage.VTable,
};

const entry_symbol_types = .{
RegisterFn,
SetGlobalsFn,
Expand Down Expand Up @@ -280,6 +312,8 @@ pub const sdk_shape_fingerprint: u64 = blk: {
h = fingerprint.hashAllShape(h, sdk_boundary_types, 6);
h = fingerprint.hashAllShape(h, entry_symbol_types, 3);
h = fingerprint.hashAllShape(h, dvui_shared_state_types, 6);
h = fingerprint.hashAllShape(h, tape_boundary_types, 6);
h = fingerprint.foldString(h, std.mem.asBytes(&tape.binary.version));
break :blk h;
};

Expand Down
2 changes: 1 addition & 1 deletion sdk/src/version.zig
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub const sdk_version = @import("sdk_version").sdk_version;
/// why it is a single target/mode-invariant literal rather than a per-target table. Update this
/// value from the `@compileError` it triggers whenever it changes; leave `sdk_version` to the
/// next SDK release.
pub const recorded_sdk_shape_fingerprint: u64 = 0x68bbdb31985c4555;
pub const recorded_sdk_shape_fingerprint: u64 = 0x9bf131098b2d283c;

comptime {
if (dylib.sdk_shape_fingerprint != recorded_sdk_shape_fingerprint) {
Expand Down
22 changes: 22 additions & 0 deletions tests/fixtures/tape-plugin/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const std = @import("std");
const fizzy = @import("fizzy");

/// The plugin as an application links it (`build/app.zig` builds it this way and plays its tests).
// anytype: the app's framework struct, whose type `fizzy.plugin.staticModule` owns.
pub fn staticModule(b: *std.Build, framework: anytype) *std.Build.Module {
return fizzy.plugin.staticModule(b, framework, .{});
}

pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

const plugin = fizzy.plugin.create(b, .{ .target = target, .optimize = optimize });

// The plugin's module as the dylib sees it, tested: `tape` and `replay` resolve in a store
// plugin as they do in a bundled one (`build/app.zig` tests `play.zig` the other way).
const tests = b.addTest(.{ .name = "tape-fixture-tests", .root_module = plugin.module });
const test_step = b.step("test", "Run the fixture's tests as a dylib plugin sees its imports");
test_step.dependOn(&b.addRunArtifact(tests).step);
test_step.dependOn(&plugin.lib.step);
}
18 changes: 18 additions & 0 deletions tests/fixtures/tape-plugin/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.{
.name = .tape_fixture,
.version = "0.1.0",
.minimum_zig_version = "0.17.0",
.paths = .{
"build.zig",
"build.zig.zon",
"plugin.zig",
"plugin.zig.zon",
"play.zig",
},
.fingerprint = 0x39f10a95729a0ebe,
.dependencies = .{
.fizzy = .{
.path = "../../../sdk",
},
},
}
74 changes: 74 additions & 0 deletions tests/fixtures/tape-plugin/play.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! A plugin's use of `tape` and `replay`: write a demo with `Script`, ship it as bytes, load it and
//! play it. The same file is tested twice: linked into an app (`build/app.zig`, the imports
//! `plugins.staticModule` gives) and built as a plugin dylib (this directory's `build.zig`, the
//! imports `plugin.create` gives), which is how a plugin's one source builds both ways.
const std = @import("std");
const tape = @import("tape");
const replay = @import("replay");

const Played = struct {
commands: usize = 0,

fn sink(self: *Played) tape.Sequencer.Sink {
return .{ .ctx = self, .vtable = &.{
.locate = locate,
.moveTo = moveTo,
.button = button,
.scroll = scroll,
.key = key,
.text = text,
.command = command,
.keyframe = keyframe,
.holds = holds,
.failed = failed,
} };
}
fn locate(_: *anyopaque, _: tape.Tape.Target) ?tape.Sequencer.Point {
return null;
}
fn moveTo(_: *anyopaque, _: tape.Sequencer.Point) void {}
fn button(_: *anyopaque, _: tape.Tape.Button, _: bool) void {}
fn scroll(_: *anyopaque, _: tape.Tape.Scroll) void {}
fn key(_: *anyopaque, _: []const u8) void {}
fn text(_: *anyopaque, _: []const u8) void {}
fn command(ctx: *anyopaque, cmd: tape.Tape.Command) void {
const self: *Played = @ptrCast(@alignCast(ctx));
if (std.mem.eql(u8, cmd.id, "fixture.hello")) self.commands += 1;
}
fn keyframe(_: *anyopaque, _: *const tape.Tape.Keyframe) void {}
fn holds(_: *anyopaque, _: tape.Tape.Until) bool {
return true;
}
fn failed(_: *anyopaque, _: tape.Sequencer.Failure) void {}
};

test "a plugin writes a demo, ships it as bytes, and plays it back" {
const gpa = std.testing.allocator;
var owned = owned: {
// `finish` hands the arena to `owned`, so `script` owns it only until then.
var script: tape.Script = .init(gpa, "fixture", "Fixture");
errdefer script.deinit();
try script.keyframe(.{ .root = "demo://fixture" });
try script.command("fixture.hello");
break :owned try script.finish();
};
defer owned.deinit();

const bytes = try tape.binary.encode(gpa, owned.tape, .{});
defer gpa.free(bytes);
var loaded = try tape.Tape.load(gpa, bytes, .{});
defer loaded.deinit();

var played: Played = .{};
var seq: tape.Sequencer = .init(&loaded.tape);
var frames: usize = 0;
while (!seq.done() and frames < 1_000) : (frames += 1) {
_ = seq.advance(seq.now + 16, 16, played.sink());
}
try std.testing.expectEqual(@as(usize, 1), played.commands);
}

test "replay is the module that drives a window with a tape" {
// The player takes the tape `tape` made: one `Tape` type on both sides of the import.
try std.testing.expect(@hasDecl(replay.Player, "play"));
}
22 changes: 22 additions & 0 deletions tests/fixtures/tape-plugin/plugin.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Fixture: a plugin that imports `tape` and `replay`, built as a dylib to show a store plugin gets
//! both (`play.zig` holds what it does with them). Registers nothing but itself.
const sdk = @import("fizzy_sdk");

pub const plugin_options = @import("fizzy_plugin_options");

var plugin: sdk.Plugin = .{
.state = undefined,
.vtable = &vtable,
.id = plugin_options.id,
.display_name = plugin_options.name,
};

const vtable: sdk.Plugin.VTable = .{};

pub fn register(host: *sdk.Host) !void {
try host.registerPlugin(&plugin);
}

test {
_ = @import("play.zig");
}
8 changes: 8 additions & 0 deletions tests/fixtures/tape-plugin/plugin.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.{
.id = "tape_fixture",
.name = "Tape fixture",
.version = "0.1.0",
.min_sdk_version = "",
.description = "Imports tape and replay, as a dylib.",
.tags = .{"fixture"},
}
Loading