//! Developer/maintenance tooling: vendoring the Zig source into the Rust crate,
//! regenerating the conformance corpora, the figl → generated-file sync, and the
//! version sync. None of these run on a normal build; they are explicit steps
//! a maintainer invokes (a few are wired into the `check` gate via the handles
//! returned in `Result`).
const std = @import("std");
const Context = @import("Context.zig");
const artifacts = @import("artifacts.zig");
/// Handles the `checks` gate needs to depend on.
pub const Result = struct {
/// The generated-file staleness guard (`build.zig.zon` + the workflows),
/// which `zig build check` runs.
check_figl_step: *std.Build.Step,
/// The `Language` contract's compile-failure cases, which `zig build check`
/// runs. Not part of `test`/`conformance`: asserting a `@compileError`
/// means compiling something that must NOT build, which a test binary
/// cannot host.
validate_check_step: *std.Build.Step,
/// The publish-path existence guard (`vendor-rust --check`), which
/// `zig build check` runs. The copy it guards only happens at release time,
/// so nothing else would notice a renamed source until a release died.
vendor_check_step: *std.Build.Step,
/// The one-version guard (`version-sync --check`): every file that carries
/// fig's version agrees with build.zig.zon. `zig build check` runs it.
version_check_step: *std.Build.Step,
};
pub fn add(ctx: Context, arts: artifacts.Result) Result {
const b = ctx.b;
const target = ctx.target;
const optimize = ctx.optimize;
const mod = arts.fig_mod;
const exe = arts.exe;
// Vendor this Zig source into fig-sys (bindings/rust/fig-sys/zig) so the
// published crate is self-contained — build.rs compiles the core from there
// when there is no repo above it. A small Zig program rather than a shell
// script, so it runs anywhere the crate's own `zig build` already does.
const vendor_rust = b.addExecutable(.{
.name = "vendor_rust",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/vendor-rust.zig"),
.target = target,
.optimize = optimize,
}),
});
const vendor_rust_run = b.addRunArtifact(vendor_rust);
vendor_rust_run.addArg(b.pathFromRoot("."));
vendor_rust_run.addArg(b.pathFromRoot("bindings/rust/fig-sys/zig"));
const vendor_rust_step = b.step("vendor-rust", "Vendor the Zig source into the Rust crate for publishing");
vendor_rust_step.dependOn(&vendor_rust_run.step);
// The read-only half: every path the copy above reads, plus every entry in
// build.zig.zon's `.paths`, still exists. The copy itself runs only at
// publish time, so without this a renamed source file is invisible until a
// release workflow dies on it — which is how `fig.md` -> `README.md` got as
// far as a tagged release. Git state isn't a declared input, so it must not
// be cached.
const vendor_check_run = b.addRunArtifact(vendor_rust);
vendor_check_run.addArg(b.pathFromRoot("."));
vendor_check_run.addArg(b.pathFromRoot("bindings/rust/fig-sys/zig"));
vendor_check_run.addArg("--check");
vendor_check_run.has_side_effects = true;
const vendor_check_step = b.step("vendor-check", "Fail if a path the published crate needs (or build.zig.zon's .paths promises) is missing");
vendor_check_step.dependOn(&vendor_check_run.step);
// Dev tool: regenerate the YAML conformance corpus from the yaml-test-suite,
// parsing the suite meta-files with fig itself (no third-party YAML library).
// zig build gen-yaml-conformance -- <path-to-yaml-test-suite> [<fig-root>]
const gen_yaml = b.addExecutable(.{
.name = "gen_yaml_conformance",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/gen_yaml_conformance.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{ .name = "fig", .module = mod }},
}),
});
const gen_yaml_run = b.addRunArtifact(gen_yaml);
if (b.args) |args| gen_yaml_run.addArgs(args);
const gen_yaml_step = b.step("gen-yaml-conformance", "Regenerate the YAML conformance corpus");
gen_yaml_step.dependOn(&gen_yaml_run.step);
// Dev tool: STRUCTURAL conformance — compare the shape of fig's parse against
// the suite's canonical `tree:` event stream (the pass/fail scoreboard can't
// see a wrong-shape accept). zig build check-yaml-trees -- <suite> [<fig-root>]
const check_trees = b.addExecutable(.{
.name = "check_yaml_trees",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/check_yaml_trees.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{ .name = "fig", .module = mod }},
}),
});
const check_trees_run = b.addRunArtifact(check_trees);
if (b.args) |args| check_trees_run.addArgs(args);
const check_trees_step = b.step("check-yaml-trees", "Structural-diff fig's parse vs the suite tree");
check_trees_step.dependOn(&check_trees_run.step);
// Dev tool: vendor the toml-lang/toml-test corpus into testdata/toml/.
// zig build gen-toml-conformance -- <path-to-toml-test> [<version>] [<fig-root>]
const gen_toml = b.addExecutable(.{
.name = "gen_toml_conformance",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/gen_toml_conformance.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{ .name = "fig", .module = mod }},
}),
});
const gen_toml_run = b.addRunArtifact(gen_toml);
if (b.args) |args| gen_toml_run.addArgs(args);
const gen_toml_step = b.step("gen-toml-conformance", "Vendor the toml-test corpus");
gen_toml_step.dependOn(&gen_toml_run.step);
// Dev tool: vendor the json5/json5-tests corpus into testdata/json5/.
// zig build gen-json5-conformance -- <path-to-json5-tests> [<fig-root>]
const gen_json5 = b.addExecutable(.{
.name = "gen_json5_conformance",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/gen_json5_conformance.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{ .name = "fig", .module = mod }},
}),
});
const gen_json5_run = b.addRunArtifact(gen_json5);
if (b.args) |args| gen_json5_run.addArgs(args);
const gen_json5_step = b.step("gen-json5-conformance", "Vendor the json5-tests corpus");
gen_json5_step.dependOn(&gen_json5_run.step);
// fig ships every artifact under one version, decided in build.zig.zon's
// `.version` (see docs/VERSIONING.md). `version-sync` copies it into every
// other file that carries it — the figl source, fig.h's FIG_VERSION_*
// macros, README.md's frontmatter, the Rust workspace and its internal
// pins, both package.json files and their lockfiles, the nushell
// plugin's pin on fig and its lockfile — and is what
// `dx release` runs as its `post_bump` (.config/release.toml).
// `version-check` is the read-only half, and `zig build check` runs it.
// Both read and write the real tree, which is not a declared input, so
// neither may be served from cache.
const version_sync = b.addExecutable(.{
.name = "version_sync",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/version-sync.zig"),
.target = target,
.optimize = optimize,
}),
});
const version_sync_run = b.addRunArtifact(version_sync);
version_sync_run.addArg(b.pathFromRoot("."));
version_sync_run.has_side_effects = true;
const version_sync_step = b.step("version-sync", "Copy build.zig.zon's version into every file that carries it");
version_sync_step.dependOn(&version_sync_run.step);
const version_check_run = b.addRunArtifact(version_sync);
version_check_run.addArg(b.pathFromRoot("."));
version_check_run.addArg("--check");
version_check_run.has_side_effects = true;
const version_check_step = b.step("version-check", "Fail if a file that carries fig's version disagrees with build.zig.zon");
version_check_step.dependOn(&version_check_run.step);
// The `.figl` files under figl/ (build.zig.figl, ci.figl, fuzz.figl,
// homebrew.figl, release-binaries.figl, release.figl, release-npm.figl,
// release-npm-wasi.figl, tangled-ci.figl, tangled-release.figl) are the
// source of truth for their generated counterparts (build.zig.zon, the six
// .github/workflows/*.yml files, the two .tangled/workflows/*.yml
// pipelines) — the inverse of the usual setup,
// dogfooding fig's own cross-format conversion for fig's own release
// infra. This tool shells out to the just-built `fig` binary (`fig get -o
// <format>`) to regenerate them; it never re-implements parsing/printing
// itself. `sync-figl` writes; `check-figl` (used by CI and the pre-commit
// hook, via `zig build check`) fails if a destination is stale, without
// writing. Git state (the destination files) isn't a declared input, so
// neither run may be served from cache.
const sync_figl = b.addExecutable(.{
.name = "sync_figl",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/sync-figl.zig"),
.target = target,
.optimize = optimize,
}),
});
const sync_figl_run = b.addRunArtifact(sync_figl);
sync_figl_run.addArtifactArg(exe);
sync_figl_run.addArg(b.pathFromRoot("."));
sync_figl_run.has_side_effects = true;
const sync_figl_step = b.step("sync-figl", "Regenerate build.zig.zon + the .github/ and .tangled/ workflow files from their .figl sources");
sync_figl_step.dependOn(&sync_figl_run.step);
const check_figl_run = b.addRunArtifact(sync_figl);
check_figl_run.addArtifactArg(exe);
check_figl_run.addArg(b.pathFromRoot("."));
check_figl_run.addArg("--check");
check_figl_run.has_side_effects = true;
const check_figl_step = b.step("check-figl", "Fail if build.zig.zon / the .github/ and .tangled/ workflow files are stale relative to their .figl sources");
check_figl_step.dependOn(&check_figl_run.step);
// The `Language` contract's negative tests. `language.validate` is a wall
// of `@compileError`, and Zig cannot assert that one fires from inside
// `zig build test` — a test that triggers a compile error doesn't fail, it
// takes the whole suite's build down. So the refusals are covered from
// outside instead: this tool writes deliberately-broken fixture languages
// to a work directory and runs `zig build-obj` over each, asserting the
// compile fails with the message the matching rule should produce. See its
// module doc, and the proposal's §6, which asked for exactly this.
//
// Shells out to the same `zig` running the build (`b.graph.zig_exe`), so
// the fixtures are checked against the toolchain in use rather than
// whatever is first on PATH. Side-effecting: it writes and re-writes one
// probe file per case, and its result depends on `src/languages/` state
// that isn't a declared input, so it must not be served from cache.
//
// Takes the format list as a module so its all-formats-off
// `build_options` stub names exactly the gates the library reads.
const validate_check = b.addExecutable(.{
.name = "validate_check",
.root_module = b.createModule(.{
.root_source_file = b.path("tools/validate-check.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "languages", .module = b.createModule(.{ .root_source_file = b.path("src/languages/list.zig") }) },
},
}),
});
const validate_check_run = b.addRunArtifact(validate_check);
validate_check_run.addArg(b.graph.zig_exe);
validate_check_run.addArg(b.pathFromRoot("."));
validate_check_run.addArg(b.pathFromRoot(".zig-cache/validate-check"));
validate_check_run.has_side_effects = true;
const validate_check_step = b.step("validate-check", "Assert language.validate REJECTS malformed Language manifests (compile-failure cases)");
validate_check_step.dependOn(&validate_check_run.step);
return .{
.check_figl_step = check_figl_step,
.validate_check_step = validate_check_step,
.vendor_check_step = vendor_check_step,
.version_check_step = version_check_step,
};
}