This document explains how Bazel is configured and used in the avalanchego monorepo.
The bazel command is provided by bazelisk,
which automatically downloads the correct Bazel version from .bazelversion. Most
Taskfile targets use ./scripts/nix_run.sh bazelisk ..., which runs in the repo's
nix dev shell when needed and avoids nesting nix develop when already inside it.
In the nix dev shell (nix develop), bazel and bazelisk are both on PATH directly.
For Nix installation and repo dev shell setup, see CONTRIBUTING.md.
Some tasks (e.g. task bazel-check-metadata) only require tooling that is installed
by default on GitHub Action runners (e.g. bash, git, go, bazelisk). These
tasks can be executed without a nix shell which in CI avoids the cost of nix installation.
# Build the main binary
task bazel-build
# Build with optimizations
task bazel-build-opt
# Run unit tests
task bazel-test
# Update Bazel metadata after changing Go imports or Bazel module deps
task bazel-generate-metadata
# Clean build cache
task bazel-clean- Hermetic builds - Reproducible builds regardless of host environment
- Incremental compilation - Only rebuild what changed
- Parallel execution - Efficient use of multi-core systems
- Caching - Local and remote build caching
- Multi-language - Single build system for Go, Solidity, Rust, protobuf, etc.
The build uses go_sdk.from_file() to read the Go version from
go.mod, ensuring a single source of truth without manual syncing:
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
go_sdk.from_file(go_mod = "//:go.mod")Approaches considered:
| Approach | Pros | Cons |
|---|---|---|
go_sdk.from_file() (chosen) |
Reproducible, single source of truth via go.mod, no nix required | None significant |
go_sdk.download() |
Explicit version in MODULE.bazel | Go version must be synced manually across go.mod files |
go_sdk.host() |
Uses system Go | Requires nix shell, not hermetic outside nix |
rules_nixpkgs_go |
Bazel calls Nix directly, fully hermetic | Incompatible with rules_go v0.57+ (toolchain API mismatch) |
Note: rules_nixpkgs_go v0.13.0 proved incompatible with rules_go v0.56.0+. The rules_go toolchain API changed in ways that rules_nixpkgs_go doesn't support. See: tweag/rules_nixpkgs#667
This repo keeps version pins in the checked-in configuration consumed by the relevant tooling rather than duplicating them in documentation:
- Bazel:
.bazelversion - Go:
go.mod(read bygo_sdk.from_file()) - Bazel modules such as
rules_goand Gazelle:MODULE.bazel
When checking or updating a version, use those files as the source of truth.
Bazel CI uses two separate Gazelle go_deps extension instances:
- the main
go_depsinstance readsgo.workfor the workspace modules and the external repos they import - the isolated
tool_go_depsinstance readstools/external/go.modfor repo-owned helper tools that CI may need to launch before other Bazel tasks
That split is intentional. The CI setup path needs to fetch the Bazel-owned
//tools/external:task bootstrap target and warm external dependency caches
without also depending on whatever local workspace state happens to exist in a
particular checkout.
For the same reason, MODULE.bazel intentionally omits use_repo bindings for
workspace modules such as avalanchego and graft/*. Those modules are built
from the local source tree, so binding their generated local-path repos is not
needed for normal builds. Omitting them also keeps broad fetches such as
bazel fetch //... from traversing personal workspace state like local
symlinks or repo-adjacent directories while trying to prepare external
repositories for CI.
If future Bazel changes appear to make this split unnecessary, treat that as a behavioral change to validate rather than a cleanup to apply mechanically. The important invariant is that Bazel CI can bootstrap repo tools and prefetch the external dependencies its jobs need without coupling that setup step to machine-specific workspace state.
| Factor | Bazel 7 | Bazel 8 |
|---|---|---|
| bzlmod | Optional | Default |
| LTS status | Older | Current |
| WORKSPACE | Default | Deprecated (still works) |
Bazel 8 is the current LTS with native bzlmod support. bzlmod replaces the legacy use of WORKSPACE files.
The repository contains multiple Go modules with different licenses:
| Module | Path | License | Import Path |
|---|---|---|---|
| avalanchego | / (root) |
BSD-3 | github.com/ava-labs/avalanchego |
| coreth | graft/coreth/ |
LGPL-3 | github.com/ava-labs/avalanchego/graft/coreth |
| evm | graft/evm/ |
- | github.com/ava-labs/avalanchego/graft/evm |
| subnet-evm | graft/subnet-evm/ |
LGPL-3 | github.com/ava-labs/avalanchego/graft/subnet-evm |
Each module has its own go.mod with replace directives pointing to
sibling modules. Bazel handles cross-module imports via go.work and
gazelle prefix directives.
| File | Purpose | Safe to Delete? |
|---|---|---|
MODULE.bazel |
Bazel module definition, dependencies, patches | No |
MODULE.bazel.lock |
Locked module/dependency resolution state | Yes (regenerated) |
go.work |
Go workspace aggregating all modules (used by go_deps) |
No |
.bazelrc |
Bazel build flags and settings | No |
.bazelignore |
Directories excluded from Bazel | No |
.bazelversion |
Bazel version pin (used by bazelisk) | No |
BUILD.bazel (root) |
Gazelle config: prefix, exclusions, proto disable | No |
.bazel/patches/*.patch |
Fixes for external dependencies (some generated) | No |
.bazel/patches/build_files/ |
Source BUILD files for generated patches | No |
scripts/generate_bazel_patches.sh |
Generates .patch files from build_files/ |
No |
.bazel/defs.bzl |
Custom test macros for graft module timeouts | No |
scripts/bazel_workspace_status.sh |
Git commit stamping for releases | No |
Most BUILD.bazel files are auto-generated by Gazelle, but some have custom content that would be lost if deleted:
| File | Custom Content |
|---|---|
BUILD.bazel (root) |
Gazelle directives (prefix, exclude, proto) + gazelle() rule |
main/BUILD.bazel |
x_defs for Git commit stamping |
graft/coreth/BUILD.bazel |
gazelle:prefix, gazelle:map_kind for test timeouts |
graft/evm/BUILD.bazel |
gazelle:prefix, gazelle:map_kind for test timeouts |
graft/subnet-evm/BUILD.bazel |
gazelle:prefix, gazelle:map_kind for test timeouts |
Preserving custom content: Gazelle directives (# gazelle:prefix,
# gazelle:map_kind) are automatically preserved. For other custom
content (like x_defs), use # keep comments to prevent gazelle from
removing them:
x_defs = {
# keep
"github.com/ava-labs/avalanchego/version.GitCommit": "{STABLE_GIT_COMMIT}",
},Rule of thumb: Use # keep only for custom content that Gazelle
would otherwise remove or rewrite.
Caveat: Gazelle's fix mode does not rename existing function
calls in files with # keep comments. This means changes to
gazelle:map_kind directives (e.g., renaming a macro) won't propagate
to # keep files automatically -- they need a manual find-and-replace.
Gazelle automatically generates BUILD.bazel files from Go source code.
Gazelle is declared as a bazel_dep in MODULE.bazel. It is not
provided by Nix. The recommended entrypoint is task bazel-generate-metadata, which regenerates Bazel metadata for the repo.
Run task bazel-generate-metadata after:
- Adding new
.gofiles - Changing import statements
- Adding new packages/directories
- Modifying
go.moddependencies - Modifying
MODULE.bazel
task bazel-generate-metadata also refreshes MODULE.bazel.lock into the
same state later Bazel module commands expect. bazel mod tidy alone does not
always fully refresh MODULE.bazel.lock, so a later Bazel command may rewrite
it. Running the lockfile refresh as part of metadata generation makes that
update happen in one predictable place instead of as a later surprise.
A single root-level gazelle run handles all Go modules. Gazelle
discovers # gazelle:prefix directives in subdirectories and uses
them for import path resolution, so graft modules get correct
importpath values without needing separate gazelle targets.
Each Go module root needs a BUILD.bazel with a gazelle:prefix
directive:
# graft/coreth/BUILD.bazel
# gazelle:prefix github.com/ava-labs/avalanchego/graft/corethDependencies are similarly unified via a single declaration:
go_deps.from_file(go_work = "//:go.work")The go.work file aggregates all module dependencies, so external
dependencies from graft modules are resolved automatically.
The graft modules (coreth, evm, subnet-evm) have longer test timeouts
than the root module. Each graft module's root BUILD.bazel uses a
gazelle:map_kind directive to remap go_test to the graft_go_test
macro (defined in .bazel/defs.bzl, Bazel "long" timeout, 900s):
# graft/coreth/BUILD.bazel
# gazelle:prefix github.com/ava-labs/avalanchego/graft/coreth
# gazelle:map_kind go_test graft_go_test //.bazel:defs.bzlThis remaps all go_test targets in that subtree to use the custom
macro with the longer timeout.
Go dependencies are resolved via go_deps.from_file(go_work = ...)
(see "How Gazelle Handles Multiple Modules" above).
Some dependencies require patches for Bazel compatibility. Patches are
in .bazel/patches/ and applied in MODULE.bazel.
Ordered from least to most invasive:
-
Gazelle directives (
gazelle_overridewith directives) Guide gazelle's BUILD file generation with directives likegazelle:excludeto skip problematic directories. Use when excluding certain source files is sufficient. -
Sandbox relaxation (
tags = ["no-sandbox"]) Disable sandboxing on specific targets so they can access undeclared inputs (e.g., cross-package assembly includes). Use when the build tool needs files that can't be declared as Bazel dependencies due to tooling limitations. Trade-off: loses hermeticity on those targets; incompatible with remote execution. -
BUILD file augmentation (patch on gazelle output) Patch the gazelle-generated BUILD files to add missing sources, flags, or attributes. Use when gazelle gets most things right but misses something specific.
-
Custom BUILD files (
build_file_generation = "off"+ patch) Replace gazelle-generated BUILD files entirely. Use when the dependency has complex build requirements (CGO, assembly, non-standard layout) that gazelle can't handle.
Choosing between augmentation (3) and custom BUILD files (4):
Prefer augmentation when gazelle produces a correct BUILD file that
only needs minor additions (e.g., an extra copts flag or a missing
source file). Switch to custom BUILD files when gazelle gets less than
~70% of the target right — at that point, patching the delta becomes
harder to maintain than owning the whole file. Signs that custom BUILD
files are warranted: cc_library targets with platform-specific
select(), CGO with assembly, pre-built static libraries via
cc_import, or unity-build compilation models.
| Dependency | Issue | Solution |
|---|---|---|
ava-labs/libevm |
Missing C sources for secp256k1 | Custom BUILD files + gazelle directive excludes secp256k1 dir |
firewood-go-ethhash/ffi |
Pre-built static libraries | Custom BUILD files with cc_import |
supranational/blst |
Complex CGO with assembly | Custom BUILD files (gazelle disabled) |
consensys/gnark-crypto |
Assembly cross-package includes | no-sandbox on bls12-381 fp and fr targets |
Problem: Gazelle-generated BUILD file doesn't include libsecp256k1 C sources.
Solution: Custom BUILD files + gazelle directive excluding the
secp256k1 directory from generation. See Patch Maintenance below
for how to edit these.
Problem: Pre-built static libraries with -L paths that don't work
in Bazel's sandbox.
Solution: Custom BUILD files with cc_import for proper static
library linking. Gazelle can't generate these rules for pre-built
libraries. See Patch Maintenance below for how to edit these.
Problem: Complex CGO with assembly files that gazelle cannot handle.
Solution: Custom BUILD files replacing gazelle output entirely.
Gazelle is disabled and a patch provides custom BUILD files handling
blst's unity-build compilation model with platform-specific assembly
selection. Compiler flags are derived from the CGO directives in
blst.go. See Patch Maintenance below for how to edit these.
Problem: Assembly files use cross-package #include directives:
#include "../../../field/asm/element_4w/element_4w_arm64.s"Bazel's sandboxed builds don't allow relative includes across package boundaries.
Solution: Sandbox relaxation via tags = ["no-sandbox"] on the
bls12-381 fp and fr targets, allowing the Go assembler to resolve
relative includes from the full source tree in the execroot. Only
bls12-381 is patched since it's the only curve in the dependency graph.
This is simpler than duplicating assembly files or providing custom
BUILD files for the entire module.
See rules_go#3636).
go_deps.module_override(
patches = ["//.bazel/patches:com_github_consensys_gnark_crypto_asm_includes.patch"],
path = "github.com/consensys/gnark-crypto",
)Dependency chain:
avalanchego → libevm → kzg4844 → go-kzg-4844 → gnark-crypto
gnark-crypto is used for KZG blob commitments (EIP-4844).
Proto generation is disabled globally via the root BUILD.bazel:
# gazelle:proto disable_globalThe repository uses pre-generated .pb.go files checked into source
control rather than generating them at build time. This avoids proto
toolchain complexity in Bazel while maintaining compatibility with the
existing go generate workflow.
Bazel targets that import protobuf runtime packages depend on the Go module
google.golang.org/protobuf through go_deps.from_file(go_work = "//:go.work")
and refer to it as @org_golang_google_protobuf//.... The repo does not rely
on direct protobuf / rules_proto Bazel module dependencies for proto code
generation.
# Build main binary
task bazel-build # or: bazel build //main:avalanchego
# Build with optimizations
task bazel-build-opt # or: bazel build --compilation_mode=opt //main:avalanchego
# Build everything
bazel build //...By default, bazel test matches scripts/build_test.sh behavior,
with a few exceptions:
- The script passes
-tags testtogo test; currently there are no//go:build testfiles in this repo, so it has no effect. - The script excludes several directories via
go list | grep -v ...; Bazel instead relies ontags = ["manual"]to keep non-unit tests out ofbazel test //....
# Run all unit tests (shuffle enabled, race on)
task bazel-test # or: bazel test //...
# Run tests for a specific package
bazel test //utils/...
# Run specific test functions (target:test_name + filter)
bazel test //utils:set_test --test_filter=TestSet_Add
# Fast local iteration (no race, no shuffle)
task bazel-test-fast # or: bazel test --config=fast //...
# Collect coverage
bazel coverage //...
# Run E2E tests (requires built binary)
task bazel-test-e2e| Option | Default | Toggle with |
|---|---|---|
| Race detection | ON | --config=norace (disable) |
| Shuffle | ON | --config=noshuffle (disable) |
| Fast mode | - | --config=fast (no shuffle, no race) |
Examples:
# Disable race detection
bazel test --config=norace //...
# Disable shuffle only
bazel test --config=noshuffle //...
# Fast mode (no shuffle, no race)
bazel test --config=fast //...Bazel has four timeout categories. .bazelrc sets the durations via
--test_timeout=short,moderate,long,eternal:
| Category | Duration | Used By |
|---|---|---|
| short | 60s | - |
| moderate | 120s | Root module tests (default for go_test) |
| long | 900s | Graft module tests (via graft_go_test macro) |
| eternal | 3600s | - |
The graft_go_test macro sets timeout = "long" so graft tests
get the 900s budget. Root module tests use the default "moderate"
category (120s). See Custom Test Macros for how this is wired up.
Tests that are not unit tests (e2e tests, integration tests, load
tests) must have tags = ["manual"] in their BUILD.bazel file. This
excludes them from bazel test //... which should only run unit
tests.
This roughly mirrors the behavior of scripts/build_test.sh, which excludes these directories via grep:
grep -v tests/e2e | grep -v tests/upgrade | grep -v tests/fixture/bootstrapmonitor/e2e | ...Tests with manual tag:
| Test | BUILD.bazel Location |
|---|---|
| Main E2E tests | tests/e2e/BUILD.bazel |
| Upgrade tests | tests/upgrade/BUILD.bazel |
| Bootstrap monitor E2E | tests/fixture/bootstrapmonitor/e2e/BUILD.bazel |
| Subnet-EVM warp tests | graft/subnet-evm/tests/warp/BUILD.bazel |
| Subnet-EVM load tests | graft/subnet-evm/tests/load/BUILD.bazel |
| Coreth warp tests | graft/coreth/tests/warp/BUILD.bazel |
When adding new non-unit tests, add the manual tag with a # keep
comment. The # keep is required because gazelle does not manage the
manual tag and will strip it when regenerating BUILD files from
scratch. Use go_test in your BUILD.bazel -- in graft modules,
gazelle:map_kind will automatically rewrite it to graft_go_test:
go_test(
name = "my_e2e_test",
srcs = ["my_e2e_test.go"],
tags = ["manual"], # keep -- not a unit test
deps = [...],
)# Regenerate Bazel metadata
task bazel-generate-metadata
# Update MODULE.bazel use_repo calls
task bazel-mod-tidy # or: bazel mod tidy
# Refresh Bazel module metadata files
task bazel-sync-module-metadata
# Clean build outputs
task bazel-clean # or: bazel clean
# Full cache clean
task bazel-clean-all # or: bazel clean --expunge
# CI/local: verify Bazel metadata is current
task bazel-check-metadataAs part of bazel-check-metadata, package-local BUILD.bazel files are
expected to define at most one go_library rule. Multiple
go_library rules in one directory are usually stale metadata left
behind by a package rename or move, where Gazelle added the new rule
without removing the old checked-in one.
This repo prefers linting for that stale-rule pattern rather than
deleting and regenerating all non-curated BUILD.bazel files. The lint
is narrower and safer: it fails on the specific suspicious state we want
to prevent, without relying on a maintained list of which BUILD files
are safe to destroy and recreate from scratch.
In CI, the Bazel workflow runs bazel-check-metadata before Bazel build
and test jobs. This makes stale metadata fail with a single actionable
error instead of surfacing later as multiple downstream Bazel failures.
This is especially useful for pull requests tested against a moving base
branch, where the metadata included in the PR may be stale relative to
the current merge target.
In GitHub Actions, the Bazel jobs use the local
./.github/actions/setup-bazel composite action. It prepares cache
state for the dependencies those jobs are expected to need and sets
RUN_TASK_PREFER_BAZEL=1. With that variable set, run_task.sh uses
the Bazel-owned //tools/external:task target instead of bootstrapping
task with go tool on runners where Go is already on PATH. That
preference is only for CI; local developer use still defaults to the
Go-based task bootstrap.
See Bazel CI External Dependency Caching for the motivation, cache-key design, checked-in list of Bazel CI target patterns used to prepare the build dependency cache, and enforcement model. This keeps repo tool bootstrapping and build dependency caching inside Bazel for the lighter-weight Bazel CI jobs. The E2E Bazel job uses the same cache setup before its heavier test wrapper.
That check includes the Bazel module metadata files, so lockfile drift is caught in the metadata phase rather than showing up later as a surprising working-tree mutation.
The intent is similar to actions/setup-go: set up dependency caches once
from a small amount of checked-in metadata so later CI jobs can reuse them
instead of downloading the same things again. Bazel does not infer the right
shared CI cache contents from go.mod alone, so this repo has to be more
explicit about what it fetches ahead of time.
The primary motivation is CI reliability, not just speed. This repo has seen GitHub Actions flakes when Bazel jobs had to download external dependencies and Go module data from the network in each job. Caching as much of that setup work as possible means fewer repeated network requests during the Bazel workflow, which reduces exposure to those infrastructure failures.
The Bazel CI cache setup restores and configures two kinds of cached data:
- Bazel
repository_cache - shared Gazelle
GOMODCACHE
The shared GOMODCACHE is required because Gazelle go_repository otherwise
keeps Go module downloads in each Bazel work area. That means a later job can
still hit the network even after the setup fetch has already run. The action
therefore enables both:
GO_REPOSITORY_USE_HOST_MODCACHE=1GOMODCACHE=...
The GitHub Actions cache key is:
bazel-repo-${runner.os}-${runner.arch}-${hashFiles('.bazelversion', 'MODULE.bazel.lock', 'scripts/bazel_ci_dependency_list.sh')}
with a same-platform restore prefix of
bazel-repo-${runner.os}-${runner.arch}-.
That split is intentional:
runner.osandrunner.archseparate caches by platform.bazelversioninvalidates the cache when the Bazel version changesMODULE.bazel.lockinvalidates the cache when the pinned external dependency set changesscripts/bazel_ci_dependency_list.shinvalidates the cache when the checked-in Bazel CI target patterns used by setup change- the broader same-platform restore key still gives a useful warm start because these caches store downloaded dependency data, not per-run build outputs
This setup is similar in spirit to actions/setup-go: before the later Bazel
CI jobs run, prepare cache state for the build dependencies they are expected
to need so those jobs do not each discover missing dependencies on their own.
The setup action first restores any previously saved dependency data,
configures Bazel to use it, and fetches the Bazel-owned
//tools/external:task bootstrap target before the workflow's first
./scripts/run_task.sh ... invocation. In the per-platform setup job it is
run with initial-setup: true; in that mode it also checks Bazel metadata and
runs ./scripts/run_task.sh bazel-cache-ci-build-dependencies, which
delegates to ./scripts/cache_bazel_ci_build_dependencies.sh and uses the
checked-in list in ./scripts/bazel_ci_dependency_list.sh.
That checked-in list names both:
- the Bazel bootstrap targets needed before the first CI task launch
- the Bazel target patterns whose build dependencies the later CI jobs are expected to need
The list should cover what bazel-ci.yml actually runs, rather than trying to
fetch everything Bazel could possibly reach. This avoids missing dependencies
needed by the real CI jobs while also avoiding broader fetches that download
unrelated repos and toolchains.
A related design constraint is that this setup path must stay focused on
external dependencies, not local workspace-module discovery. The isolated
tool_go_deps extension and the omission of workspace-module use_repo
bindings in MODULE.bazel are part of the same design: they let the setup job
fetch Bazel-owned repo tools and warm caches for later jobs without making
bazel fetch walk machine-specific workspace state.
The Bazel CI tasks that consume this prepared cache state run through
./scripts/run_bazel_ci_command.sh. When BAZEL_CI_ENFORCE_DEPENDENCY_LIST=1
is set (as it is in bazel-ci.yml), that wrapper extracts the target patterns
from its Bazel invocation and fails if they are not present in
bazel_ci_dependency_list.sh.
That keeps the checked-in list aligned with the Bazel CI jobs we actually run. It makes it harder for a new or changed Bazel CI job to start depending on a different set of external build dependencies without also updating the list of target patterns used by setup to prepare the cache.
When modifying setup-bazel, run_task.sh, run_bazel_ci_command.sh,
bazel_ci_dependency_list.sh, or the Bazel module wiring that supports them,
preserve these invariants:
- CI can launch
taskwithout assuming a preinstalled repo-specific wrapper - the
setupjob prepares the dependency state later Bazel CI jobs are expected to consume - the checked-in dependency list matches the Bazel target patterns actually run
by
bazel-ci.yml - cache-prefetch behavior stays focused on external repositories and does not start depending on developer-specific workspace state
Validate changes proportionally:
- run
./scripts/test_run_task_launcher.shwhen changingrun_task.shor its Bazel bootstrap path so the launcher policy and working-directory behavior are still covered - run the affected Bazel tasks through their normal entrypoints (for example
task bazel-check-metadata,task bazel-cache-ci-build-dependencies, and the relevanttask bazel-test-*targets) so the checked-in dependency list, bootstrap target, and cache-preparation flow still agree - if you change which Bazel CI commands or target patterns the workflow runs,
update
scripts/bazel_ci_dependency_list.shin the same change rather than letting CI discover the mismatch later - if you change
MODULE.bazelorMODULE.bazel.lock, rerun the normal Bazel metadata workflow and confirm the setup path still reaches repo tools and external repos without traversing unintended local workspace state
The GitHub Actions Bazel workflow also defines a single aggregate job,
bazel-required, that depends on the other jobs in the workflow via
needs. Branch protection can require that one workflow-level job
instead of tracking each underlying Bazel job separately. This reduces
required-check maintenance to the workflow level.
If the setup job fails its metadata check in CI, rebase or merge the target
branch, run task bazel-generate-metadata, commit the resulting changes, and
rerun CI.
On macOS, .bazelrc defaults to using the Apple CommandLineTools
installed under /Library/Developer/CommandLineTools. This is the
default location for the tools installed without Xcode, and the
location used by GitHub Actions runners.
For most usage, these defaults should be sufficient. If a machine uses
Xcode or a non-default Apple developer toolchain location, the
defaults can be overridden via .bazelrc.local which is optionally
imported by .bazelrc. .bazelrc.local is intended to be generated
via task bazel-configure-local, which runs
./scripts/generate_bazelrc_local.sh under the repo's standard task
entrypoint. The script uses xcode-select -p and xcrun --sdk macosx --show-sdk-path to determine the host's active Apple developer
directory and macOS SDK and writes those values to .bazelrc.local.
The script can also be run directly and is invoked automatically by
direnv.
When invoked by direnv, generation is best-effort: failures are shown as warnings during shell entry but do not prevent entering the repo. When run directly, the script exits non-zero on discovery failures so manual setup problems remain actionable.
When adding a new Go module under graft/:
-
Create the module's go.mod and add to go.work:
go work use ./graft/newmodule -
Create the module's root BUILD.bazel with the gazelle prefix:
# graft/newmodule/BUILD.bazel # gazelle:prefix github.com/ava-labs/avalanchego/graft/newmodule
-
Generate Bazel metadata:
task bazel-generate-metadata
Dependencies are resolved automatically via go.work.
Regenerate Bazel metadata:
task bazel-generate-metadata- Check if it's in
go.mod- if not, add it - Run
task go-mod-tidy(this runsgo mod tidyin all modules, syncs the workspace, and runsbazel mod tidy)
Check .bazelrc for required CGO flags. CGO is enabled via
--action_env=CGO_ENABLED=1.
If you see errors related to CGO dependencies (blst, gnark-crypto, secp256k1), verify that:
- Patches in
.bazel/patches/are applied correctly inMODULE.bazel - The
gazelle_overridewithbuild_file_generation = "off"is set for packages with custom BUILD files
If you see errors like:
#include: open /field/asm/element_4w/element_4w_arm64.s: no such file or directory
This indicates the gnark-crypto assembly patch isn't being applied. Check:
- The patch file exists:
.bazel/patches/com_github_consensys_gnark_crypto_asm_includes.patch MODULE.bazelhas themodule_overrideapplying the patch- Try
task bazel-clean-allto clear cached BUILD files
Try a full cache clean:
task bazel-clean-allIf builds still fail after cleaning, check if MODULE.bazel.lock needs regenerating:
rm MODULE.bazel.lock
task bazel-mod-tidyUsually means gazelle created a target that conflicts with a manually-defined one. Check for custom content in the BUILD.bazel file.
CGO is enabled via environment variables in .bazelrc:
build --action_env=CGO_CFLAGS="-O2 -D__BLST_PORTABLE__"
build --action_env=CGO_ENABLED=1
Dependency-specific CGO issues are handled via patches (see Patched Dependencies above).
The main binary includes Git commit information via x_defs in
main/BUILD.bazel (see BUILD.bazel Files with Custom Content above
for the # keep annotation pattern).
Stamping is enabled via the release config in .bazelrc:
bazel build //main:avalanchego # Dev build (no stamp, cached)
bazel build --config=release //main:avalanchego # Release build (stamped)-
gnark-crypto sandbox relaxation - The
no-sandboxtags on bls12-381 targets (see "Sandbox relaxation" strategy above) mean those compilations are not hermetically sandboxed. This is acceptable for a pinned dependency but incompatible with remote execution. -
Go version sync - Go version must be kept in sync across
go.work,go.modfiles, andnix/go/default.nix. Bazel reads the version fromgo.modviago_sdk.from_file(), soMODULE.bazeldoesn't need separate updating. Usetask update-go-version -- <version>to update all files except nix (which requires SHA changes). CI enforces consistency viatask check-go-version.
The following improvements are planned or under consideration:
Remote caching would enable:
- Cache sharing between CI runs
- Faster builds for new team members
- Cross-machine cache reuse
go_sdk.download() enables remote execution since the Go toolchain is
hermetic and reproducible.
Implementation: Add BuildBuddy, Buildkite or similar remote cache service.
- Migrate CI to Bazel - Update GitHub Actions to use Bazel for builds/tests where possible
- Retain non-Bazel coverage - Keep traditional
go buildCI jobs to support third-party consumers who don't use Bazel
Patches that create new BUILD files (blst, firewood, libevm) are
generated from readable BUILD files in .bazel/patches/build_files/.
This avoids hand-maintaining patch hunk line counts, which Bazel's
internal patch parser is strict about (unlike git apply).
To modify a patch:
- Edit the BUILD file in
.bazel/patches/build_files/<module>/ - Run
task bazel-generate-patchesto regenerate.patchfiles - Verify:
./scripts/nix_run.sh bazelisk build @<module>//<target>(or plainbazelisk build @<module>//<target>when the host already has the required tools available) - Commit both the BUILD file and the generated
.patchfile
Patches that modify existing files (e.g., gnark-crypto's no-sandbox
tag) are maintained manually and are not generated by this script.
Periodic review:
- Check if upstream projects have improved Bazel support
- Test patches against dependency updates
- Consider upstreaming BUILD files where feasible
- Monitor rules_go#3636 for proper cross-package asm include support (would remove gnark-crypto's sandbox relaxation)
Graft modules already have custom test timeouts via gazelle:map_kind
(see "Custom Test Macros" section). Note that gazelle:map_kind
applies to all go_test targets in the subtree, including non-unit
tests. This is harmless today since Bazel only runs unit tests, but
may need revisiting if non-unit tests move to Bazel. Consider further
stratification:
- Integration tests: medium timeout (300s)
- E2E tests: explicit long timeout (currently use
manualtag)