Skip to content
Draft
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
110 changes: 103 additions & 7 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ load("@rules_rust//rust:defs.bzl", "rust_clippy", "rustfmt_test")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
load("@rules_shellcheck//:def.bzl", "shellcheck_test")
load(":cargo_build.bzl", "cargo_run", "cargo_test", "cargo_vendor", "cargo_vendor_provider", "python_venv", "python_venv_provider", "uv_python_install", "uv_python_venv")
load(":cargo_build.bzl", "cargo_run", "cargo_test", "cargo_vendor", "cargo_vendor_provider", "hack_command", "python_venv", "python_venv_provider", "uv_python_install", "uv_python_venv")
load("//crates/cli:hack.bzl", cli_hack = "HACK", cli_manifest = "MANIFEST")
load("//crates/cli-lib:hack.bzl", cli_lib_hack = "HACK", cli_lib_manifest = "MANIFEST")
load("//crates/cli-python:hack.bzl", cli_python_hack = "HACK", cli_python_manifest = "MANIFEST")
load("//crates/lib:hack.bzl", lib_hack = "HACK", lib_manifest = "MANIFEST")
load("//crates/lib-core:hack.bzl", lib_core_hack = "HACK", lib_core_manifest = "MANIFEST")
load("//crates/lib-dialects:hack.bzl", lib_dialects_hack = "HACK", lib_dialects_manifest = "MANIFEST")
load("//crates/lib-wasm:hack.bzl", lib_wasm_hack = "HACK", lib_wasm_manifest = "MANIFEST")
load("//crates/lineage:hack.bzl", lineage_hack = "HACK", lineage_manifest = "MANIFEST")
load("//crates/lsp:hack.bzl", lsp_hack = "HACK", lsp_manifest = "MANIFEST")
load("//crates/sqlinference:hack.bzl", sqlinference_hack = "HACK", sqlinference_manifest = "MANIFEST")

# Link all npm packages into node_modules
npm_link_all_packages(name = "node_modules")
Expand Down Expand Up @@ -232,6 +242,19 @@ RUST_TARGETS = [
"//crates/sqlinference:sqruff-sqlinference",
]

# Workspace-level manifests shared by every per-crate hack target. Combined
# with a crate's dependency-closure machete_srcs, this is the scoped input set
# that lets unrelated crate edits stay cache hits (see cargo_hack_suite).
filegroup(
name = "workspace_manifest",
srcs = [
"Cargo.lock",
"Cargo.toml",
"rust-toolchain.toml",
],
visibility = ["//visibility:public"],
)

# Common Cargo source files for cargo-based checks
# Used by cargo_machete, cargo_check, cargo_hack_check, rust_lint
filegroup(
Expand All @@ -251,6 +274,7 @@ filegroup(
"//crates/lsp:machete_srcs",
"//crates/sqlinference:machete_srcs",
],
visibility = ["//visibility:public"],
)

# Vendor cargo dependencies and install Rust toolchain (cached until Cargo.lock changes)
Expand All @@ -269,6 +293,7 @@ cargo_vendor(
cargo_vendor_provider(
name = "cargo_deps",
vendor = ":cargo_vendor",
visibility = ["//visibility:public"],
)

# Pre-cache Python venv with all dev dependencies (cached until pyproject.toml changes)
Expand Down Expand Up @@ -460,16 +485,87 @@ cargo test --all --all-features --exclude sqruff --exclude sqruff-lib-core --exc
""",
)

# Cargo hack check - verify each feature compiles in isolation
# This ensures no feature combination is broken
# Uses vendored dependencies for hermetic builds
# Cargo hack check - verify each feature compiles in isolation.
#
# Instead of one monolithic `cargo hack check --each-feature` action (serial,
# all-or-nothing caching), the sweep is decomposed into one `cargo check`
# target per feature per crate. Each crate owns its variation map in its
# hack.bzl (see cargo_hack_suite); the targets run in parallel and give
# per-feature failure attribution. The whole sweep is the union of every
# crate's hack suite.
#
# hack_reconcile (below) guarantees that decomposition stays a one-to-one
# match with what cargo-hack itself would enumerate, so no feature is silently
# dropped.

# Expected command list derived from every crate's hack.bzl map. Compared
# against `cargo hack ... --print-command-list` by hack_reconcile.
_HACK_MODULES = [
(cli_manifest, cli_hack),
(cli_lib_manifest, cli_lib_hack),
(cli_python_manifest, cli_python_hack),
(lib_manifest, lib_hack),
(lib_core_manifest, lib_core_hack),
(lib_dialects_manifest, lib_dialects_hack),
(lib_wasm_manifest, lib_wasm_hack),
(lineage_manifest, lineage_hack),
(lsp_manifest, lsp_hack),
(sqlinference_manifest, sqlinference_hack),
]

_EXPECTED_HACK_COMMANDS = [
hack_command(manifest, args)
for manifest, hack in _HACK_MODULES
for args in hack.values()
]

_HACK_WANT = "\n".join(sorted(_EXPECTED_HACK_COMMANDS))

# Reconciliation test: the per-crate hack.bzl maps must match, one-to-one, the
# commands `cargo hack check --each-feature` would run. Uses --print-command-list
# so nothing is compiled; CARGO_NET_OFFLINE (not --offline) keeps the printed
# lines clean for diffing.
cargo_test(
name = "cargo_hack_check",
size = "enormous",
name = "hack_reconcile",
size = "medium",
srcs = [":cargo_srcs"],
tools = ["@cargo_hack//:cargo-hack"],
vendor = ":cargo_deps",
script = "cargo hack check --each-feature --exclude-features=codegen-docs --offline",
script = (
"cat > want.txt <<'HACK_WANT_EOF'\n" +
_HACK_WANT + "\n" +
"HACK_WANT_EOF\n" +
"sort -o want.txt want.txt\n" +
"CARGO_NET_OFFLINE=true cargo hack check --each-feature --exclude-features=codegen-docs --print-command-list | sort > got.txt\n" +
"if ! diff -u want.txt got.txt; then\n" +
" echo ''\n" +
" echo 'ERROR: the per-crate hack.bzl maps are out of sync with cargo-hack.'\n" +
" echo ' < want.txt = enumerated from crates/*/hack.bzl FEATURES lists'\n" +
" echo ' > got.txt = what cargo hack check --each-feature enumerates'\n" +
" echo 'Update the FEATURES lists in the relevant crates/*/hack.bzl to match.'\n" +
" exit 1\n" +
"fi\n" +
"echo 'OK: hack.bzl maps match cargo hack --each-feature one-to-one.'\n"
),
)

# Aggregates the whole feature sweep plus the reconciliation guard.
# Usage: bazel test //:cargo_hack_check
test_suite(
name = "cargo_hack_check",
tests = [
"//crates/cli:hack",
"//crates/cli-lib:hack",
"//crates/cli-python:hack",
"//crates/lib:hack",
"//crates/lib-core:hack",
"//crates/lib-dialects:hack",
"//crates/lib-wasm:hack",
"//crates/lineage:hack",
"//crates/lsp:hack",
"//crates/sqlinference:hack",
":hack_reconcile",
],
)

# Zensical docs build - verify documentation builds successfully
Expand Down
80 changes: 80 additions & 0 deletions cargo_build.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -616,3 +616,83 @@ cargo_run = rule(
executable = True,
attrs = _cargo_attrs,
)

def each_feature(features):
"""Builds the `--each-feature` variation map for a crate.

Mirrors `cargo hack check --each-feature`: one `--no-default-features` run
plus one `--features X` run per declared feature. For a crate with no
features, pass `[]` to get a single plain `cargo check` (matching what
cargo-hack emits for featureless crates).

Returns a map of {target-name-suffix: cargo feature arguments}. The keys are
used as Bazel target name suffixes, so they must be valid target names.

The resulting set is reconciled one-to-one against cargo-hack itself by
//:hack_reconcile, which fails if a crate's hack.bzl drifts from the
features cargo-hack actually enumerates.
"""
if not features:
return {"check": ""}
variations = {"none": "--no-default-features"}
for f in features:
variations[f] = "--no-default-features --features " + f
return variations

def hack_command(manifest, args):
"""Formats a single cargo-hack-equivalent `cargo check` command line.

Matches the exact form printed by `cargo hack ... --print-command-list`
(no `--offline`), so the reconciliation diff stays byte-for-byte.
"""
cmd = "cargo check --manifest-path " + manifest
if args:
cmd += " " + args
return cmd

def cargo_hack_suite(name, manifest, variations, closure, vendor, size = "large"):
"""Generates one `cargo check` test per feature variation, plus a suite.

Each generated target runs exactly the `cargo check` invocation that
`cargo hack check --each-feature` would run for `manifest` and one feature,
using the hermetic vendored toolchain. Splitting cargo-hack's serial sweep
into individual targets lets Bazel run them in parallel and attribute
failures per feature.

`closure` is the crate's own directory plus the directories of its
transitive in-workspace dependencies (e.g. ["crates/lib-dialects",
"crates/lib-core"]). The action's inputs are scoped to just those crates'
sources, and the workspace `members`/`default-members` are trimmed to the
same set in-sandbox so cargo only loads the closure (rather than validating
every workspace member). This way an edit to an unrelated crate does not
invalidate this crate's targets, maximising Bazel cache hits.
"""
srcs = ["//:workspace_manifest"] + ["//{}:machete_srcs".format(d) for d in closure]

members_toml = "[" + ", ".join(['"{}"'.format(d) for d in closure]) + "]"

# Restrict the workspace to the dependency closure before running cargo, so
# missing (out-of-closure) crate sources don't fail workspace loading.
# Portable in-place edit (works with both GNU and BSD sed).
rewrite = (
"sed -e 's|^members = \\[\"crates/\\*\"\\]|members = " + members_toml + "|'" +
" -e 's|^default-members = .*|default-members = " + members_toml + "|'" +
" Cargo.toml > Cargo.toml.scoped && mv Cargo.toml.scoped Cargo.toml\n"
)

tests = []
for suffix, args in variations.items():
tname = "{}_{}".format(name, suffix)
cargo_test(
name = tname,
size = size,
srcs = srcs,
vendor = vendor,
script = rewrite + hack_command(manifest, args) + " --offline",
)
tests.append(":" + tname)
native.test_suite(
name = name,
tests = tests,
visibility = ["//visibility:public"],
)
Comment thread
Copilot marked this conversation as resolved.
13 changes: 13 additions & 0 deletions crates/cli-lib/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
load("@crates//:defs.bzl", "all_crate_deps")
load("@rules_rust//rust:defs.bzl", "rust_library")
load("//:cargo_build.bzl", "cargo_hack_suite")
load(":hack.bzl", "CLOSURE", "HACK", "MANIFEST")

# Per-feature `cargo check` targets mirroring `cargo hack --each-feature`.
# The HACK map lives in hack.bzl and is reconciled against cargo-hack by
# //:hack_reconcile. Run the whole crate sweep via `bazel test //crates/cli-lib:hack`.
cargo_hack_suite(
name = "hack",
closure = CLOSURE,
manifest = MANIFEST,
variations = HACK,
vendor = "//:cargo_deps",
)

rust_library(
name = "sqruff-cli-lib",
Expand Down
29 changes: 29 additions & 0 deletions crates/cli-lib/hack.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Feature variations for `cargo hack --each-feature` on this crate.

`FEATURES` lists the features declared in Cargo.toml that cargo-hack checks in
isolation (excluding `codegen-docs`, which is excluded from the sweep). The
derived `HACK` map drives the per-feature `cargo check` targets in BUILD.bazel
and is reconciled one-to-one against cargo-hack by //:hack_reconcile.
"""

load("//:cargo_build.bzl", "each_feature")

MANIFEST = "crates/cli-lib/Cargo.toml"

FEATURES = [
"parser",
"python",
]

HACK = each_feature(FEATURES)

# This crate plus its transitive in-workspace dependencies. Scopes the Bazel
# action inputs (and the in-sandbox workspace) so edits to unrelated crates
# remain cache hits. See cargo_hack_suite.
CLOSURE = [
"crates/cli-lib",
"crates/lib",
"crates/lib-core",
"crates/lib-dialects",
"crates/lsp",
]
14 changes: 14 additions & 0 deletions crates/cli-python/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
load("//:cargo_build.bzl", "cargo_hack_suite")
load(":hack.bzl", "CLOSURE", "HACK", "MANIFEST")

exports_files([
"Cargo.toml",
"pyproject.toml",
])

# Per-feature `cargo check` targets mirroring `cargo hack --each-feature`.
# The HACK map lives in hack.bzl and is reconciled against cargo-hack by
# //:hack_reconcile. Run the whole crate sweep via `bazel test //crates/cli-python:hack`.
cargo_hack_suite(
name = "hack",
closure = CLOSURE,
manifest = MANIFEST,
variations = HACK,
vendor = "//:cargo_deps",
)

# Python source files for linting
filegroup(
name = "python_srcs",
Expand Down
26 changes: 26 additions & 0 deletions crates/cli-python/hack.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Feature variations for `cargo hack --each-feature` on this crate.

This crate declares no Cargo features, so cargo-hack emits a single plain
`cargo check`. The derived `HACK` map drives the `cargo check` target in
BUILD.bazel and is reconciled one-to-one against cargo-hack by //:hack_reconcile.
"""

load("//:cargo_build.bzl", "each_feature")

MANIFEST = "crates/cli-python/Cargo.toml"

FEATURES = []

HACK = each_feature(FEATURES)

# This crate plus its transitive in-workspace dependencies. Scopes the Bazel
# action inputs (and the in-sandbox workspace) so edits to unrelated crates
# remain cache hits. See cargo_hack_suite.
CLOSURE = [
"crates/cli-python",
"crates/cli-lib",
"crates/lib",
"crates/lib-core",
"crates/lib-dialects",
"crates/lsp",
]
13 changes: 13 additions & 0 deletions crates/cli/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
load("@crates//:defs.bzl", "all_crate_deps")
load("@rules_rust//rust:defs.bzl", "rust_binary")
load("//:cargo_build.bzl", "cargo_hack_suite")
load(":hack.bzl", "CLOSURE", "HACK", "MANIFEST")

# Per-feature `cargo check` targets mirroring `cargo hack --each-feature`.
# The HACK map lives in hack.bzl and is reconciled against cargo-hack by
# //:hack_reconcile. Run the whole crate sweep via `bazel test //crates/cli:hack`.
cargo_hack_suite(
name = "hack",
closure = CLOSURE,
manifest = MANIFEST,
variations = HACK,
vendor = "//:cargo_deps",
)

rust_binary(
name = "sqruff",
Expand Down
34 changes: 34 additions & 0 deletions crates/cli/hack.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Feature variations for `cargo hack --each-feature` on this crate.

`FEATURES` lists the features declared in Cargo.toml that cargo-hack checks in
isolation (excluding `codegen-docs`, which is excluded from the sweep). The
derived `HACK` map drives the per-feature `cargo check` targets in BUILD.bazel
and is reconciled one-to-one against cargo-hack by //:hack_reconcile.
"""

load("//:cargo_build.bzl", "each_feature")

MANIFEST = "crates/cli/Cargo.toml"

FEATURES = [
"bench",
"default",
"dhat-heap",
"mimalloc",
"parser",
"python",
]

HACK = each_feature(FEATURES)

# This crate plus its transitive in-workspace dependencies. Scopes the Bazel
# action inputs (and the in-sandbox workspace) so edits to unrelated crates
# remain cache hits. See cargo_hack_suite.
CLOSURE = [
"crates/cli",
"crates/cli-lib",
"crates/lib",
"crates/lib-core",
"crates/lib-dialects",
"crates/lsp",
]
Loading
Loading