# About Aspect Build
Source: https://site.aspect.build/about
Aspect Build commercializes Bazel, Google's open source build system, through expert support and the Aspect Workflows developer productivity platform.
Aspect Build was founded in 2021 to commercialize Bazel, Google's open sourced build system, now stewarded by the Linux Foundation. Our team includes early Bazel contributors and maintainers who built and operated Bazel at Google scale.
Our insight: Bazel provides a highly efficient and scalable core build engine, but organizations outside Google need a richer ecosystem of tools, integrations, and services to adopt it successfully. We began with Bazel services and then released the Aspect Workflows developer productivity platform.
Our company name is a nod to Bazel's core concepts. In Bazel, an "aspect" is a powerful extension that enriches the build graph with additional insights, actions, and dependencies, unlocking deeper analysis and automation.
Meanwhile, code and builds are structured in "BUILD" files. Just like Bazel aspects enhance the build process, Aspect Build enhances your developer experience with expert services and our developer productivity platform.
# RBE is fast enough for AI agents. But, is it safe enough?
Source: https://site.aspect.build/blog/ai-agents-rbe
Everyone's solving 'make RBE fast enough for agents.' Nobody's solving 'make RBE safe enough to let agents near it.' Self-hosted Workflows gives your AI its own execution plane — in your own VPC, without extending the trust boundary of your human CI.
AI coding agents and remote build execution (RBE) are a natural fit. Agents iterate fast: run a build, watch it fail, fix it, then rebuild. Everyone always asks if their executor fleet can keep up. Almost always, they find that it can. After all, RBE was built exactly for these kinds of parallel, high-frequency builds.
The real question is should agents have access to that infrastructure at all?
When an AI coding agent runs on your RBE cluster, it’s working in the same environment as your engineers, with access to the same executor pool, the same secrets, and the same artifact cache your release builds depend on.
Your RBE cluster was designed with human engineers in mind. Someone authenticated, accountable, with a job title and an oncall rotation is lurking on the other end. An agent doesn’t have any of that. Yet most teams have quietly handed it full CI-level trust anyway.
We ran into this with a healthcare technology company. Their fix: a second Aspect Workflows deployment that runs nothing but AI coding agents, isolated from their human CI behind its own network boundary, with its own access audit trail.
## When agents cross the trust threshold
When you’re a small team, running a handful of agents occasionally, sharing a cluster is no big deal. The risk is low because the scope is narrow. It’s not fine when you realize your agent has unfettered access to infrastructure that’s not set up to be audited that way.
There are a few concrete signs it’s time to isolate your agents:
* **Agents are running non-stop:** Your nightly batch job touches the cluster for an hour then clocks out. An agent loop, on the other hand, is active during business hours. It spawns builds on every commit and hangs out on your executor fleet all day.
* **Build load is blending:** Your human CI builds and agent retry loops compete for the same executors. When agent-driven builds gang up on your human builds and push them back in the queue, it’s not an executor problem. It’s actually two workloads that were never meant to share a queue — you just found out the hard way when your PR is stuck behind an agent’s umpteenth retry.
* **Your audit trail can’t tell the difference:** Your logs show that a build ran on executor X. They don’t tell you whether X was a person or an agent (at least not without additional tools). Do you know what an agent touched last week? Or even two days ago? That gap in logging is a compliance nightmare in regulated environments and a forensic dead-end anywhere else.
* **Compliance isn’t reactive:** Some teams have security conversations after something breaks or an audit asks a question you can’t answer. For teams in certain industries (healthcare, finance, defense, cybersecurity) compliance comes before an incident. Either way, the need will arrive. And the infrastructure you’ve been running on was never built to answer those questions.
## Blast radius: what an agent can actually get its hands on
The trust argument is abstract. The blast radius is concrete. When your agent is unruly, gets compromised, or just operates with the speed and energy of an overcaffeinated toddler, here’s what it can reach on a shared cluster.
### The artifact cache
Bazel’s remote cache is keyed by action hash. An agent looking around a large change set generates action hashes across a wide slice of the build graph. Every query fills the cache’s hot tier with entries your engineers will probably never need and pushes out the ones they probably do. A build that should’ve been fast turns into a partial cold rebuild, not because of any changes in the code, but because an agent went on a querying quest an hour earlier. That’s a correctness-adjacent problem: your cache is now shaped by how agents wander, not by how your team actually builds.
Without any changes to infrastructure you can create separate cache namespaces, set with `--remote_instance_name` in your Bazel config. Your human CI reads and writes to one namespace and agents get their own. Take it even further and tier your storage: fast, low-latency storage for humans and a cheaper tier for agents, where a cache miss costs less because the agent rebuilds anyway.
### The executor pool
The agent retry loop is a tight one: build, fail, fix, rebuild. Enough of those running at once and your executor pool is at 100%, leaving your human CI waiting in line. The fix is queue prioritization: human builds get a dedicated capacity reservation, agent builds get burst capacity. Most RBE platforms support priority classes, but somebody has to configure it. It doesn’t happen by default. On a dedicated cluster, this doesn’t come up, because agents physically can’t reach the pool human builds depend on.
### The secrets those executors can reach
RBE executors have access to things like API tokens, signing keys, registry credentials, and other secrets your build actions ask for. An agent running wild on the same fleet has the same reach as your engineers. If you haven’t explicitly scoped what an agent can touch, you’ve implicitly given it access to all the things. You ask it to "fix the failing test" and it queries the whole graph, hitting targets that touch credentials, and burning signing resources, all without leaving a trace that tells you it wasn’t a person.
A dedicated cluster means two IAM configurations instead of one, and the agent one is short. Access to the cache. That’s it — no signing keys, no registry writes, nothing that touches a deploy. The scope is explicit because you made it that way, not because you’re hoping no one asks.
## Compliance: the audit trail problem
For a healthcare technology company, the whole thing didn’t start as a performance conversation. It started with an audit.
When it comes to regulated industries the concerns are specific: who had access to the executor that ran this build, and what could they reach? On a shared cluster, the honest answer is "whoever was in the CI IAM policy at the time, including any agents that happened to be running." That just doesn’t satisfy an auditor and it doesn’t tend to hold up in a security incident review either.
A dedicated agent cluster makes the audit trail clean. Every action lands in your cloud’s native audit log, tagged to that cluster automatically. No one has to build a special agent-tracking dashboard. Agent activity is visibly separate from human activity because there are two clusters, not with a tool that’s cobbled together. When an auditor asks what the agent accessed, the answer is bound by what that cluster was allowed to reach and that’s a short list.
This carries over to data residency. If you have requirements for where code artifacts live or get processed, an air-gapped or single-region Workflows deployment keeps agent build data inside the same jurisdictional lines as everything else, regardless of what the agent is doing.
As a practical side benefit, it’s simpler to reason about. CI executors need write access to your artifact registry, signing keys for release builds, and deployment credentials. Agents don’t typically need any of that. Scoping those permissions differently on one cluster is awkward. With two clusters, there are two policies, and the agent policy is short.
## Cost attribution: governance follows isolation
Once you’ve split execution planes, you can actually see what agents cost. On a shared cluster, agent and human CI builds are a blended bill. You can watch the spend as agents become more active, but you can’t easily separate what drove it.
It’s easily fixed by tagging compute resources by workload. Compute resources in the agent cluster get a tag like `workload: agent`; human CI gets `workload: ci`. Most cloud billing dashboards already group by tag, so per-workload cost shows up without additional tooling. The same pattern works if multiple teams are running their own agent clusters — each team’s infrastructure cost is a separate line.
The more useful number is cost per unit of output rather than raw compute cost. For human CI that’s cost per merged PR or per successful build. For agents, it’s cost per accepted commit or cost per resolved issue. Those numbers take more manual work to pull together, but worth computing once to establish a baseline. Dedicated infrastructure adds a bit of fixed overhead, but it also makes the comparison honest. You can’t do that with a blended bill.
Chargeback models differ from team to team. Some teams bill agent infrastructure back to the team running the agents; others treat it as a shared platform cost. Either way, attribution only works if the workloads were separated at the infrastructure level. You can’t make a governance decision about agent adoption off a number that mixes two different things together.
## What this looks like in practice
The healthcare technology company runs two [Aspect Workflows](https://aspect.build/platform) deployments in their AWS account. One handles all their human CI. The other handles nothing but agent-driven builds, behind its own network boundary, with its own IAM policies and its own audit trail.
The agent cluster reads from the same cache the human CI fills, so agents aren’t starting from nothing every time. Writes go to a separate namespace, so they can’t step on your CI cache. Executor pools are separate. IAM policies are separate. Billing tags are separate.
Running two clusters costs the same operational overhead as one. Aspect engineers deploy and operate both either way. What the company walks away with is a compliance posture they can describe to an auditor and a trust boundary they can point to.
## Get your agents off shared CI
Everyone’s busy solving fast enough. We’ve got that part down. The trust boundary problem has been largely ignored. It’s the one that shows up in audits, in incident postmortems, and on the day an agent-driven build quietly touches a signing key it shouldn’t have.
Give agents their own Workflows deployment and their own execution plane: separate VPC, separate IAM, separate audit trail. It still reads from the same artifact cache your human CI fills. It just doesn’t get to write to it, or touch anything else.
If you want to see what this looks like for your infrastructure, [reach out and we’ll walk through it](https://aspect.build/request-demo).
# Angular with Bazel
Source: https://site.aspect.build/blog/angular-with-bazel
Learn how to integrate Angular and Bazel using rules_js for better performance and compatibility.
We recently released [rules\_js](https://github.com/aspect-build/rules_js/) 1.0.0, a faster and more compatible approach to integrating JavaScript tooling under Bazel.
Now that ng-conf 2022 is kicking off and all our friends are back in Salt Lake City, it's a perfect time to update how Angular and Bazel work well together using rules\_js.
## First a bit of background… the Angular CLI
The [Angular CLI](https://angular.io/cli) is the standard developer tool for Angular applications. From dev server to production bundling, the CLI takes an "all-in-one" approach for a good, lowest-effort developer experience. Under the hood the CLI is primarily a thin wrapper over the [Angular Architect library](https://github.com/angular/angular-cli/tree/14.2.x/packages/angular_devkit/architect). Angular Architect allows various tools to be integrated into the CLI via plugins such as a devserver, webpack bundling, sass integration, and test frameworks, along with the core Angular Compiler (`ngc`). Architect combines those tools into one for the full Angular CLI experience.
## Angular Architect in Bazel
The simplest method of building an Angular application under Bazel is the same as under the Angular CLI: use Angular Architect. Simply invoke the Angular Architect tool from Bazel for the same all-in-one experience as the Angular CLI.
For example, a single Bazel target to compile an Angular application (named `my-app`, created by the Angular CLI):
```python theme={null}
load("@npm//:@angular-devkit/architect-cli/package_json.bzl", architect_cli = "bin")
architect_cli.architect(
name = "my-app",
args = ["my-app:build"],
srcs = glob(["**/*.ts", "**/*.sass", "**/*.html"]),
)
```
See the [Angular Architect example](https://github.com/aspect-build/bazel-examples/tree/main/angular) for a full example.
But all-in-one is not the idiomatic Bazel style: if anything in the application changes the entire application must recompile. While easier to implement, Angular Architect is not the ideal Bazel experience.
## `ngc` in Bazel
To truly benefit from Bazel the compilation must be split into independent actions that can be individually executed and cached. Each action coordinated by Angular Architect can instead be coordinated by Bazel. When Bazel is coordinating the actions the real benefits of Bazel will be seen such as parallelization, caching, test caching, remote execution/caching and all the other benefits that come with Bazel.
The primary action is compiling Angular TypeScript including component templates, css and the various annotations such as `@Injectable`. The Angular Compiler (`ngc`), is a drop-in replacement for the TypeScript compiler (`tsc`). The `ts_project` rule can be customized to use `ngc` as the compiler binary.
Bazel's macros provide a simple way to define your own "syntax sugar". We'll start by declaring the `ngc` compiler target, and an `ng_project` macro that makes it easy to declare these.
**tools/BUILD.bazel**
```python theme={null}
load("@npm//:@angular/compiler-cli/package_json.bzl", compiler_cli = "bin")
compiler_cli.ngc_binary(name = "ngc")
```
**tools/ng.bzl**
```python theme={null}
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
# Macro to wrap Angular's ngc compiler
def ng_project(name, **kwargs):
ts_project(
name = name,
# NGC compiler, do not use the standard tsc worker
tsc = "//tools:ngc",
supports_workers = False,
# Any other ts_project() or generic args
**kwargs
)
```
Now Angular code can be fully compiled with the `ng_project` macro including TypeScript, component HTML and CSS, directives, `@Injectable`s etc.
An example of a `ng_project` target:
**my-app/BUILD.bazel**
```python theme={null}
load("//tools:ng.bzl", "ng_project")
ng_project(
name = "my-app",
srcs = glob(["**/*.ts", "**/*.css", "**/*.html"]),
deps = [
"//:node_modules/@angular/core",
...
],
)
```
This is a single `ng_project` rule, but an application will most likely be divided into many BUILD files, creating many independently compiled and cached Bazel targets.
Other features of the Angular CLI such as Sass preprocessing, webpack bundling, a devserver, testing etc. will be configured as independent Bazel targets. Under the hood the tools will be the same as Angular Architect but now coordinated by Bazel. Or, you can swap out some of the tools, essentially making your own custom, incremental build system for Angular just with a few lines of Bazel's macros.
For example, bundling the application with [rules\_webpack](https://github.com/aspect-build/rules_webpack)
```python theme={null}
load("@aspect_rules_webpack//webpack:defs.bzl", "webpack_bundle")
webpack_bundle(
name = "bundle",
entry_point = "main.js",
srcs = [":my-app"],
)
```
For a complete example see the angular-ngc Bazel example: [https://github.com/jbedard/bazel-examples/tree/angular-ngc/angular-ngc](https://github.com/jbedard/bazel-examples/tree/angular-ngc/angular-ngc)
# Announcing Remote Build Execution
Source: https://site.aspect.build/blog/announcing-remote-build-execution
Aspect Workflows now includes Remote Build Execution, speeding development by offloading computation to worker machines
Remote Build Execution (RBE) is a technique for off-loading computation of a wide build and test graph to a farm of worker machines. It can vastly speed up development when changes affect a large subgraph of a monorepo.
Aspect is pleased to announce that this is now a supported feature of our Workflows platform on both GCP and AWS, in use on our OSS rulesets and rolling out to users today!
## What is Remote Build Execution (RBE)?
Bazel has a built-in scheduler. It tries to parallelize build steps as much as possible given the estimated available resources. For example, on an eight-core machine, it might run eight different test actions concurrently, if it determines other resources can allow it (for example, available system memory).
Bazel will queue actions which otherwise might have been able to run immediately when resources on the “local” machine are exhausted. Remote Build Execution allows additional resources on other computers to be added to the build. Instead of queuing, Bazel then uses a remote API to send RPC calls to that “farm” of computers. The inputs are identified (and uploaded, if needed), then an RPC call schedules the action to run remotely.
In some cases this makes the overall build faster. In this post we’ll discuss which cases those are, so you can decide if RBE is right for your organization.
## Remote Build Execution is misunderstood
Aspect’s competitors have offered Remote Build Execution from the beginning. Their pricing is based on usage (either on the upper bound on the number of executors or on Cloud Compute resource consumption.) Perhaps due to this perverse incentive, they have positioned RBE as the way to accelerate build and test and encourage every user to adopt it. From reading their website, an engineer can reasonably come away with the false impression that RBE is the first step to speeding up a slow build. However this naive view usually results in much higher costs. You may have heard me in a conference talk describe RBE as “the performance optimization of last resort”.
Our first goal is “Minimal Execution”, which is where a build is incremental thanks to a very high cache hit rate. Of course “Minimal Execution” is faster and cheaper than either Local or Remote execution. That’s why Aspect doesn’t have a usage-based pricing model! Later in this article, I’ll dig more into the reasons that a small-to-medium sized codebase and team might not get enough benefits from RBE to make it worthwhile. For now, just be aware that the highest-order factor for ANY Bazel project is the remote cache, which is how highly incremental builds can skip work, and is the bottleneck for Bazel to look up cache hits to avoid re-work.
Aspect’s largest users like Airtable do benefit from Remote Build Execution. These companies have hundreds of engineers working on a codebase with millions of lines of code. Even after minimizing the amount of execution, their build graph shape still lends itself to wide parallelism on a typical product engineer’s change.
## About Aspect’s RBE
Aspect has open-source in our DNA. So it was obvious from the beginning that we’d build on excellent, well-maintained and battle-tested Remote Cache and Execution software. We chose Buildbarn! It powers hundreds of developers at Apple and has a strong Slack community. (Apple’s open-source office doesn’t like to publicize their projects, so it’s not obvious that Buildbarn is built there!)
Our Buildbarn remote cache deployments have been live at every Workflows customer for the last 18 months. The cache is highly available and scalable, and has been rock-solid. So adding RBE was “just” a matter of enabling another Buildbarn component.
In practice, it was not that easy. Although Buildbarn has an “[example deployments](https://github.com/buildbarn/bb-deployments)” repository, there’s a lot of missing documentation. Users are forced to read comments on the protocol buffer definitions to understand a lot of the fields. If you decide to deploy Buildbarn yourself, and run into problems, we recommend our partner [Meroton](https://meroton.com/) for professional services.
Moving execution off of Bazel’s host machine has the architectural benefit of separation of concerns. You can choose compute instance types to match the profile of your Bazel actions, rather than needing to bend the actions to fit the available resources on the machine where Bazel runs. Separating the environment where the CI system executes steps from the environment where actions execute also adds security and maintainability benefits. This is especially true when the remote cache and execution is accessed from developer machines.
As a special case of this separation, some builds need to execute actions on various hardware platforms. Aspect has several autonomous driving customers with custom circuit boards. Remote execution allows Bazel to run on a standard and inexpensive instance type like AWS Graviton, while some test logic can execute on a runner process on the board. Bazel itself understands execution platforms, and Buildbarn allows workers on multiple platforms within a single cluster. Aspect Workflows includes the configuration needed to connect Bazel and Buildbarn ensuring that each action runs on the right hardware and operating system.
Customers may wish to distribute a "platform-compatible" workload over a variety of compute options, with various scaling parameters and compute specificity. Buildbarn accounts for this with a concept called "size classes". With no effort required by the user, Buildbarn may schedule a single action across the gamut of size class options available, and uses this data to schedule that action in the future on the least expensive size class where it is likely to succeed. This has the advantage of still delivering fast results, but optimizing the workload over time to reduce costs.
RBE also serves as a stop-gap when a build is non-incremental, typically when the Remote Cache is unavailable or has been intentionally cleared as part of an infrastructure rollout.
Watch for a formal Case Study on Aspect Workflows RBE at some of our customers, coming soon!
## When to consider Local rather than RBE
As I've said at conference talks, we consider RBE to be the "performance optimization of last resort". We believe strongly that reducing execution is the first step, to minimize cloud compute costs.
Here are other factors that make Bazel’s Local execution strategy a better choice than RBE for small-to-medium sized repositories:
1. When typical developer activity results in invalidating many expensive test actions, compute costs will be high (regardless of whether execution is local or remote). If your organization’s budget doesn’t allow for re-computing all these tests, you may want to run them less frequently (i.e. only after merge, or nightly). We often see this in robotics or autonomous driving, where physics simulations run as part of a test. This “test selection” strategy is in contrast to the typical Bazel idiom which is to “test everything affected”.
2. RBE requires strictness in configuring Bazel’s toolchains and hermeticity to account for the “host” and “execution” platforms differing. This can be a big task, and gets harder as you work through the “long tail” of atypical build actions. As our competitor writes on one of their case studies:
> Migrating a Bazel project to remote execution can be a daunting task
3. A wide cache miss is often caused by an infra engineer who changed an SDK version, or some other configuration change that invalidates a lot of the graph. In that use case, the engineer probably doesn't expect the same performance as a product engineer making routine changes.
4. It’s easy to add more Local resources. By provisioning a larger machine for Bazel, the built-in scheduling will parallelize build actions over the available resources such as multiple CPU cores. By occupying such a machine for short time periods, we can avoid the high cost implications of these expensive instance types.
5. Your build graph may not be amenable to parallelization. An action cannot run until its inputs are available, so when a build is slow due to the “critical path” of such serialized actions, adding more compute resources doesn’t make the build faster.
## Try Aspect Workflows
Try Aspect's RBE solution by requesting a free trial of Aspect Workflows. Our engineering team will deploy an instance to try out with your real workload, and gather the data to help decide whether RBE is a net benefit for your team!
# Aspect Build, built on Bazel
Source: https://site.aspect.build/blog/aspect-build
Aspect Build offers Bazel solutions, including a rules docsite, BUILD file automation, and a more user-friendly CLI. Explore more at aspect.build
We've been hard at work building a great consulting business at [Aspect](https://aspect.build). We are helping some large companies like Robinhood and Boston Dynamics migrate their build, test, and CI systems to Bazel. However, it's clear from all our engagements that Bazel itself is incomplete. We are excited to announce some products we've built to help fill in this gap.
## Rules Docsite
Bazel doesn't come with "batteries included". It only understands a few languages in the built-in distribution like Java, Python, and C++. For everything else, it relies on plugins called "rulesets".
The first problem our customers found with rulesets is that the documentation is lacking. There is usually a very simple example of usage, and then as soon as you need something more real, you are stuck reading sources. So we've launched the first Bazel rules docsite, at
[https://aspect.build/docs](https://aspect.build/docs)
Our docsite scrapes the generated API output of canonical rulesets, using the resulting protocol buffers to drive a custom site. It's blazing fast because the content is all build-time pre-rendered. Other features of our docsite:
* **Unified**: All the Bazel rules documented in a single place.
* **Search**: You can search across the documentation for all rulesets.
* **Versioned**: Every documentation page is permalinked to an exact version and won't change. You can select the version of a ruleset that you use.
* **Deep-links**: You can link directly to an attribute of a rule, so it's quicker to help out your fellow humans.
Ultimately our goal is to improve the upstream documentation across the rulesets by making the documentation easy to edit in-place, with the convenience of a wiki, through a standard pull request process.
## `BUILD` file generation
Bazel's `BUILD` files are wonderfully explicit and self-contained, but largely mirror dependency information that was already evident in the source files. At most clients we've observed that Product Engineers don't want to learn about build systems, and we think they shouldn't need to.
Aspect is making a big investment in [Gazelle](https://github.com/bazelbuild/bazel-gazelle), which automates maintenance of `BUILD` files. We upstreamed the [Python Gazelle plugin](https://github.com/bazelbuild/rules_python/tree/main/gazelle/README.md) to rules\_python and have started work on plugins for TypeScript and other languages.
## Rules Authors SIG
We've long been the core maintainers of [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) and also help to maintain [rules\_python](https://github.com/bazelbuild/rules_python) and [rules\_docker](https://github.com/bazelbuild/rules_docker). After working with our clients, we have a much better appreciation for how much companies depend on these rules, yet how little maintenance effort goes into them.
Sadly many corporate contributors haven't been able to get their changes merged, and maintainers haven't benefitted from the resources these large companies can provide. For this reason, we led the creation of the first Special Interest Group (SIG) under Bazel's [new SIG program](https://bazel.build/sig.html). Find it at [SIG Rules Authors](https://github.com/bazelbuild/community/blob/main/sigs/rules-authors/CHARTER.md).
## More usable CLI
We see that most engineers interact with Bazel on the command-line, and many are baffled by its complex error messages and unfamiliar terminology. It's an expert tool, being used by engineers who would rather not become build system experts.
`aspect` is a new command-line interface wrapping Bazel. The `aspect` CLI is interactive, and customizable for your organization's developer workflows. It's compatible with `bazel` so you can switch back and forth easily. While it's currently a pre-release, we are excited for the future of this tool.
Read more at [https://aspect.build](https://aspect.build).
## We can't wait to build a better Bazel!
If you're as excited about build tooling as we are, then let's work together! Visit aspect.build or participate in the SIG as a funder or a coder.
# Start a new Bazel project with aspect init
Source: https://site.aspect.build/blog/aspect-init-and-starters
aspect init is now a native Aspect CLI command, and the language starter templates live at github.com/aspect-starters with a Use this template button.
Standing up a Bazel workspace takes more boilerplate than
it should. `.bazelversion`, `.bazelrc` with sane flags, toolchains,
formatting and linting wiring, package-manager wiring, and CI that *maybe works*. It's a rite of passage no one asked for.
So we packed the whole setup into a single command.
## `aspect init` is now built into the CLI
`aspect init` is a native [Aspect CLI](/docs/cli) command (requires
[v2026.25.11](https://github.com/aspect-build/aspect-cli/releases/tag/v2026.25.11)
or newer). No separate tool, nothing to download by hand. Just install the CLI and run:
```bash theme={null}
aspect init my-project --preset go
cd my-project
aspect build //...
```
That's it. You have a fresh bazel setup.
Omit `--preset` and you get an interactive picker. Available presets: `minimal`, `shell`, `go`, `js`, `py`, `java`, `kotlin`, `cpp`, `rust`, `ruby`,
`scala`, and `kitchen-sink` (every language plus OCI containers, protobuf, and
release stamping).
What generated project comes wired up:
* 🧱 The latest Bazel (bzlmod) with curated flags via [`bazelrc-preset.bzl`](https://github.com/bazel-contrib/bazelrc-preset.bzl)
* 🧰 A hermetic dev environment via [`bazel_env.bzl`](https://github.com/buildbuddy-io/bazel_env.bzl) and [`rules_multitool`](https://github.com/theoremlp/rules_multitool)
* 🎨 Formatting and linting with [`rules_lint`](https://github.com/aspect-build/rules_lint)
* 📦 Native package-manager integration for the chosen languages
* ⚙️ Working GitHub Actions CI that runs `aspect build`/`test`/`lint`/`format` on ephemeral runners
* 📌 A pinned Aspect CLI version so your whole team and CI use identical tooling.
## The template repos, if you prefer
Each preset is also published as a GitHub *template repository* under
[github.com/aspect-starters](https://github.com/aspect-starters). Go to the
language you want — say [aspect-starters/go](https://github.com/aspect-starters/go) —
and hit **Use this template** button (or fork it, or just `git clone`).
Super handy if you want a playground, a training repo, or a link to
send to the next person who asks "how do I set up Bazel for X?"
Building a polyglot monorepo? `aspect init --preset kitchen-sink` or start from
[aspect-starters/kitchen-sink](https://github.com/aspect-starters/kitchen-sink).
## One source of truth
The CLI command and the template repos render from the same templates, so they
never drift. The source of truth is
[aspect-build/aspect-workflows-template](https://github.com/aspect-build/aspect-workflows-template):
CI there renders and builds every preset before anything lands, then republishes the
`aspect-starters` repos on each release.
Found a rough edge or want to improve a template? File issues and PRs there.
This replaces the older wizard-based `init` (which relied on an external
scaffolding tool) and Aspect's previous starter repos. Same idea, much better execution,
and now it's part of our CLI.
[Install the Aspect CLI](/docs/cli/install) and run `aspect init`.
# Automated testing of each commit != CI
Source: https://site.aspect.build/blog/automated-testing-of-each-commit-ci
Explore how microservice architectures hinder true Continuous Integration and why API contracts alone can't prevent integration issues. Learn how to fix it.
> Cross-posted from my [Medium blog from 2019](https://medium.com/@Jakeherringbone/automated-testing-of-each-commit-ci-6f718d93d0da)
I’ve had some chances on a recent trip to get more first-hand engagement with some enterprise-scale Angular customers. What I learned reinforces the impression I’ve gotten about our industry as micro-service architecture rolls out: we no longer do Continuous Integration (CI).
These companies seem self-assured that they have good testing practices: they have an automated test suite, and they run it continuously. That’s really great, and catches some bugs earlier and makes happier developers who don’t have to go back as frequently to debug through the sludge of years-old code. So you have the C in CI, no problem.
We must remember what the I in CI stands for. What are we integrating?
Any large organization requires breaking down work into departments. Ever wondered why a space agency has a control room with such a huge number of desks with controllers? Rocketry and space flight is such a massive technical undertaking, with different scientific and engineering disciplines working together, that they’ve built an operations process which gives them immediate access to information from each.
Imagine if instead of the big control room, the astronauts were just talking to a “frontend” team who then had to consult what the “backend” team thought, sometimes with a several day round-trip, and that in turn was using outdated manuals for the spacecraft. Each department might be doing the right thing on their own, but they are not “integrated”. They don’t act as a single unit.
The problem I see in companies adopting a microservice architecture is similar. Each team has tested their own components and are ready for deployment. Then when it’s time to get some new software up there to our astronauts (or other users), we discover that it doesn’t work in the QA environment — taking a day to trace the change in some other department. This is really bad for the business. If it takes weeks to integrate the software each time we want to deploy, then critical business initiatives have to build in extra time budget for software changes.
This delay in shipping causes a terrible feedback loop in the organization. As it takes weeks to release a change through “the process”, teams want to have more autonomy to have their own release schedule. It seems like it should accelerate the process, but of course this exacerbates the problem. These new autonomous units only run their own tests, and so the interactions are untested when used with the other systems it will have to integrate with in production.
The usual software architect or consultant replies, “This is not a problem because rigorous API contracts are drawn at the boundaries of each service. Each part is tested against that API”. It sounds like a great answer. Does it work?
There’s a guy who worked on the C++ team at Google named Hyrum Wright. He had to make global changes across Google’s monorepo to migrate everyone to newer versions of some base library API. That API had been specified, and was tested against, and the API was not being changed. Yet library changes would inevitably cause a bunch of test failures across Google. What were we doing wrong?
There’s now an observation called [“Hyrum’s Law”](https://www.hyrumslaw.com/) based on this experience:
> With a sufficient number of users of an API,\
> it does not matter what you promise in the contract:\
> all observable behaviors of your system\
> will be depended on by somebody.
Another way to say this is, while your API surface is constrained in spirit, it is unconstrained in practice. Change how a result is sorted? Someone relied on the prior ordering.
Now comes the incorrect conclusion from some architects: “So then that was a bug in the client of the API. The contract never guaranteed that the data would be sorted. Shame on them”. What is the business to make of this? The client software team made an avoidable error and is therefore negligent? Passing the blame this way doesn’t actually solve the business problem: these integration errors happen and are preventable. API contracts are not a sufficient way to prevent them.
As software engineers, we know that even if we are quite diligent, we’ll make accidental assumptions that happen to work today. The solution to developers making human mistakes is to add QA which catches it. Therefore, we should be writing (and continuously running) tests that exercise the *entire stack* we’ll deploy on: integrating it continuously. Only this can assure reliable delivery of our new code. And remember, the astronauts are depending on us.
## Let's fix it!
In my article [https://blog.aspect.dev/cboi-continuous-build-occasional-integration](https://blog.aspect.dev/cboi-continuous-build-occasional-integration) I talk about the technical details of how to get Continuous Integration back.
# Bazel: Avoiding eager fetches
Source: https://site.aspect.build/blog/avoid-eager-fetches
Learn how to identify and prevent eager fetches in Bazel builds, optimizing dependency management for efficient development workflows
Bazel manages your dependencies, and fetches them to a users machine when they are needed for a build. That's great, as it ensures all developers on the project have the same dependencies installed without having to think about it. When working well, these fetches are lazy and fine-grained: users only download what's needed for the specific targets they requested to build or test.
However it's easy to de-optimize by introducing an "eager fetch". This is when Bazel downloads some dependencies which aren't actually needed for the current build, just because they are referenced during the analysis phase when the BUILD files are read. These fetches are only a problem on the first build, since the resulting "external repositories" are reused by Bazel for subsequent re-builds. However they are still annoying that first time, and if the repository gets invalidated (maybe because the user switches branches to one that's rebased before some change to the dependency listing) then they have to wait again. They are extra-annoying when the "fetch" includes some subsequent slow install steps, like compiling a program that was just downloaded.
> Bazel has a ["repository cache"](https://bazel.build/docs/build#repository-cache) but this is often misunderstood. It does *not* cache the external repository that Bazel installed on the disk - rather it only caches certain network fetches which had a sha256 sum or integrity hash and were fetched by the Bazel built-in downloader. Tools like `npm` and `pip` do their own fetches, so those aren't cached by Bazel (though they might be cached somewhere else on disk by those tools). Even if you avoid a network fetch, any computation performed to "install" those dependencies is never cached by Bazel and has to be re-done if the external repository is invalidated. If you change branches back to the original one, the invalidation is just as expensive; there's no re-use of the prior state in a X -> Y -> X sequence.
This article explains how these eager fetches get triggered, how to remediate them and how to prevent regressions.
## WORKSPACE eager fetches
These are the worst kind, because they happen for every single build regardless of the dependency graph or which targets the user requests. Bazel must evaluate the complete `WORKSPACE` file to understand what third-party dependencies exist for the build. Let's say the WORKSPACE file contains this content:
```python theme={null}
load("@rules_python//python:pip.bzl", "pip_parse")
pip_parse(
name = "my_deps",
requirements_lock = "//path/to:requirements_lock.txt",
)
load("@my_deps//:requirements.bzl", "install_deps")
install_deps()
```
The penultimate line loads from the `@my_deps` repository, which means that repository must be eagerly fetched. Whatever work happens in `pip_parse` will happen for every single build, even for developers who aren't doing anything Python-related. In this case, `pip_parse` does need to fetch metadata about Python dependencies, so this isn't free. Use the Bazel profile to help you determine whether an eager fetch is a problem in your builds.
## Mitigating
In some cases, you can refactor the WORKSPACE to remove the fetch. One approach is to "vendor" - check in the result of the expensive computation rather than perform it on-the-fly, and add a test to the repo ensuring it stays up-to-date. That test will still need to fetch the external repository, but other builds won't.
Continuing the example above, I added documentation for `pip_parse` showing how you could `load` the `requirements.bzl` file from within your repo: [https://github.com/bazelbuild/rules\\\_python/blob/main/docs/pip.md#vendoring-the-requirementsbzl-file](https://github.com/bazelbuild/rules\\_python/blob/main/docs/pip.md#vendoring-the-requirementsbzl-file) If you do this, there will no longer be a `load` statement from `@my_deps` in the WORKSPACE, which should fix the eager fetch.
Another approach is to defer the work from a repository rule to an action that runs later in the BUILD graph. This generally requires changes to the rules you're using, so I won't try to give an example for end-users to follow.
Again, you should first profile your build to understand which eager fetches are really a problem in practice. External repositories are locally cached by Bazel and shouldn't be invalidated often.
## BUILD eager fetches
BUILD files also contain `load` statements, causing the external repository being loaded to be fetched. Unlike the WORKSPACE case above, the behavior depends on whether Bazel needs to analyze the BUILD file, which is the case if it is transitively referenced from targets the user requests to build or test.
For example, in this BUILD file, we load from under the `@npm` repository:
```python theme={null}
# Content of //pkg1:BUILD
load("@npm//@bazel/typescript:index.bzl", "ts_project")
package(default_visibility = ["//visibility:public"])
ts_project(
name = "a",
srcs = glob(["*.ts"]),
declaration = True,
tsconfig = "//:tsconfig.json",
deps = [
"@npm//@types/node",
"@npm//tslib",
],
)
filegroup(name = "b")
```
As a result, if the user asks to build `a`, or *any target* in the `pkg1` package such as `b`, then the full fetch of `@npm` will be eager. This also happens if the user asks for a target which directly or indirectly load's *any target* in this package, such as `//pkg2:c` shown here:
```python theme={null}
# Content of //pkg2:BUILD
filegroup(name = "c", srcs = ["//pkg1:b"])
```
Another example comes from using the `requirements` helper provided by rules\_python. If you use the suggested pattern
```python theme={null}
load("@pip//:requirements.bzl", "requirement")
py_library(
name = "foo",
...
deps = [
requirement("requests"),
],
)
```
this also causes an eager-fetch of whatever is in `@pip` which might cause all Python dependencies to be downloaded!
I added this warning to the [documentation for pip\_install](https://github.com/bazelbuild/rules_python/blob/main/docs/pip.md#pip_install):
> Note that this convenience comes with a cost. Analysis of any BUILD file which loads the requirements helper in this way will cause an eager-fetch of all the pip dependencies, even if no python targets are requested to be built. In a multi-language repo, this may cause developers to fetch dependencies they don't need, so consider using the long form for dependencies if this happens.
## Mitigating
For BUILD fetches, the shape of the BUILD file graph matters. As with any programming language, it's a design smell when your imports come from many different unrelated places. Try to avoid BUILD files that load from external repositories and *also* contain other targets which don't use those repositories.
Another mitigation is to reduce the size of the external repository being fetched. In the npm example example, we fetched `@npm` which might have a large number of packages, and if it uses `npm_install` or `yarn_install` from `build_bazel_rules_nodejs`, then *all* of those packages had to be installed just to get the `@bazel/typescript` one actually needed by this build. You could have a separate `package.json` listing file with a small number of dependencies loaded by BUILD files, with another `npm_install` repository rule fetching a repo like `@npm_bazel_deps`. This would still be eager-fetched, but it would be much faster.
In some cases the eager fetch is just for syntax sugar. In the rules\_python `requirement` example, we could have just used `@pypi__requests//:pkg` in the `deps`, with no `load` statement at all.
You can file issues as well. This takes longer to resolve, but it's healthier for the ecosystem to report these problems. Often the maintainers of the ruleset you use don't know about the problem, because they only build targets that require fetching the repo and have a smaller project, so they just don't observe the negative effects. [https://github.com/bazelbuild/rules\\\_nodejs/issues/3193](https://github.com/bazelbuild/rules\\_nodejs/issues/3193) is the issue for the example above.
## Preventing regression
Eager fetches are subtle, and easy to introduce into your Bazel build. You only notice them when the external repository is invalidated, and are only outraged enough to file issues when you're building something you feel is unrelated. "Why am I re-compiling a python interpreter from source just to run my Go test??"
You can't write a test within Bazel to catch this condition (as far as I know). But you can formulate something outside of Bazel, and then run this as a separate step/pipeline on CI. There are a couple methods.
The first is clumsy but reproduces exactly what you observe: write a test to the effect of "If I do a clean Bazel build of target `//:foo`, then look in the external directory, I should not observe the presence of the unrelated repo `bar`." See [https://github.com/aspect-build/bazel-examples/tree/main/eager-fetch](https://github.com/aspect-build/bazel-examples/tree/main/eager-fetch) for both the working example, and a sample `test_no_eager_fetch.sh` script which makes the assertion.
The second is to use a `bazel query` to detect a path through the dependency graph from a target to an undesired external repo. We'll just query for all the packages that some targets depend on, then grep for those in a repo, and if the result is non-empty then we found an eager fetch:
```plaintext theme={null}
bazel query --output=package 'let targets = set(//some:target //some/other:target) in buildfiles(deps($targets))' | uniq | sort | grep @slow_repo
```
# Bazel 9 Upstream Prebuilt Protobuf
Source: https://site.aspect.build/blog/bazel-9-protobuf
Bazel 9 includes a prebuilt upstream protobuf compiler.
Bazel 9.0 is a major long-term support (LTS) release. It contains new features and backwards incompatible changes.
* Bzlmod is now always enabled, and all WORKSPACE logic has been removed from Bazel ([#26131](https://github.com/bazelbuild/bazel/issues/26131)). The [Bzlmod migration tool is available](https://bazel.build/external/migration_tool).
* All C++-related [rules](https://github.com/bazelbuild/bazel/issues/26131) are [removed](https://bazel.build/external/migration_tool) from Bazel and must be loaded from @rules\_cc, as part of the [Starlark effort](https://github.com/bazelbuild/bazel/issues/23043).
* [Bazel 9](https://github.com/bazelbuild/bazel/issues/23043) ships with the latest protobuf module (version 33.4), which includes support for a prebuilt protobuf compiler.
This prebuilt protobuf compiler follows up on our blog article [never compile protoc again](/blog/never-compile-protoc-again) which ended with the promise “We’re hoping to upstream our `toolchains_protoc` to the protobuf repository, so that the default Bazel experience will be fast.”
This is now (nearly) true as of the [v33.4 release](https://github.com/protocolbuffers/protobuf/releases/tag/v33.4).
We thank the [Google Protobuf](https://github.com/protocolbuffers/protobuf) team for sponsoring this work and reviewing the implementation, including changes to the release process.
## Enabling the Protobufs toolchains feature
Bazel 9 flips the [`--incompatible_enable_proto_toolchain_resolution`](https://registry.build/flag/bazel/?flag=incompatible_enable_proto_toolchain_resolution) flag to true. This means Bazel is responsible for resolving the symbol `@protobuf//bazel/private:proto_toolchain_type` to a “concrete” toolchain that provides the right `protoc` binary for your execution platform. This is also the case for a toolchain\_type for each language stub generator.
If you’re on Bazel 9, there’s nothing to do, but earlier Bazels require you set the flag.
## Opting-in
Until [https://github.com/protocolbuffers/protobuf/pull/25313](https://github.com/protocolbuffers/protobuf/pull/25313) lands and is released, you need to opt-in to using the `protoc` binaries from [https://github.com/protocolbuffers/protobuf/releases](https://github.com/protocolbuffers/protobuf/releases). Add to your `.bazelrc`:
```bash theme={null}
common --@protobuf//bazel/toolchains:prefer_prebuilt_protoc
```
> note, if you have a `repo_name=com_google_protobuf` you’ll have to adapt the `@protobuf` name
## Enforcing
You can enforce that no rules do the “Wrong Thing” of directly referencing the `cc_binary` target `@protobuf//:protoc` by following our snippets: [https://github.com/aspect-build/toolchains\_protoc#ensure-protobuf-and-grpc-never-built](https://github.com/aspect-build/toolchains_protoc#ensure-protobuf-and-grpc-never-built)
If this fails to build, it means that there’s a bug you should find or report.
The [https://github.com/aspect-build/toolchains\_protoc](https://github.com/aspect-build/toolchains_protoc) and [https://github.com/bazelbuild/rules\_proto](https://github.com/bazelbuild/rules_proto) repositories are now archived, completing the deprecation period.
## Next
Need help migrating from WORKSPACE to Bzlmod or other steps to move to Bazel 9? View [Aspect Build services](http://www.aspect.build/services) and email us at [hello@aspect.build](mailto:hello@aspect.build) or ping us on Bazel Slack. We’re happy to support you.
# Bazel can write to the source folder!
Source: https://site.aspect.build/blog/bazel-can-write-to-the-source-folder
Bazel can write to the source folder for specific needs, using `bazel run` and `bazel test` to maintain consistency
Bazel is Google's open-sourced build tool. When used internally at Google, it comes along with a bunch of idioms which Googlers naturally take for granted, and associate with Bazel. These can accidentally become part of the accepted dogma around Bazel migration.
Most frequently, the accident I see is a false perception "Bazel cannot write to the source folder, so you can no longer check in generated files, nor have them in the sources but ignored from VCS".
## Typically you shouldn't do it
Intermediate outputs in Bazel are meant to be used directly as inputs to another target in the build. For example, if you generate language-specific client stubs from a `.proto` file, those stay in the `bazel-out` folder and a later compiler step should be configured to read them from there.
However there are plenty of cases where outputs do need to go in the source folder:
* workaround for an editor plugin that only knows to read in the source folder and can't be configured to look in bazel-out
* "golden" or "snapshot" files used for tests
* generated documentation that's checked in next to sources
* files that you need to be able to search or browse from your version control GUI
## Yes you can do it
If you restrict yourself to only `bazel build` and `bazel test`, then it's true that neither of these commands can mutate the source tree. Bazel is strictly a transform tool from the sources to its own bazel-out folder. However, `bazel run` has no such limitation, and in fact always sets an environment variable `BUILD_WORKSPACE_DIRECTORY` which makes it easy to find your sources and modify them.
This leads us to the "Write to Sources" pattern for Bazel. We'll use `bazel run` to make the updates, and `bazel test` to make sure developers don't allow the file in the source folder to drift from what Bazel generates.
Note that this pattern does have one downside, compared with build tools that allow a build to directly output into the source tree. Until you run the tests, it's possible that you're working against an out-of-date file in the source folder. This could mean you spend some time developing, only to find on CI that the generated file needs to be updated, and then after updating it, you have to make some fixes to the code you wrote.
The easiest way to use this pattern is with rules that already exist for this purpose. Aspect has a [`write_source_files`](https://registry.bazel.build/modules/bazel_lib#lib-write_source_files-bzl) rule, and another option is [`updatesrc`](https://github.com/cgrindel/bazel-starlib/tree/main/updatesrc) from Chuck Grindel.
You can also assemble the parts yourself, directly in a `BUILD.bazel` file. Here's the basic recipe, which I've adapted to many scenarios. For example, many of the core Bazel rulesets now use this pattern to keep their generated API markdown files in sync with the sources.
```python theme={null}
load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
load("@bazel_skylib//rules:write_file.bzl", "write_file")
# Config:
# Map from some source file to a target that produces it.
# This recipe assumes you already have some such targets.
_GENERATED = {
"some-source": "//:generated.txt",
# ...
}
# Create a test target for each file that Bazel should
# write to the source tree.
[
diff_test(
name = "check_" + k,
# Make it trivial for devs to understand that if
# this test fails, they just need to run the updater
# Note, you need bazel-skylib version 1.1.1 or greater
# to get the failure_message attribute
failure_message = "Please run: bazel run //:update",
file1 = k,
file2 = v,
)
for [k, v] in _GENERATED.items()
]
# Generate the updater script so there's only one target for devs to run,
# even if many generated files are in the source folder.
write_file(
name = "gen_update",
out = "update.sh",
content = [
# This depends on bash, would need tweaks for Windows
"#!/usr/bin/env bash",
# Bazel gives us a way to access the source folder!
"cd $BUILD_WORKSPACE_DIRECTORY",
] + [
# Paths are now relative to the workspace.
# We can copy files from bazel-bin to the sources
"cp -fv bazel-bin/{1} {0}".format(
k,
# Convert label to path
v.replace(":", "/"),
)
for [k, v] in _GENERATED.items()
],
)
# This is what you can `bazel run` and it can write to the source folder
sh_binary(
name = "update",
srcs = ["update.sh"],
data = _GENERATED.values(),
)
```
You may want to tweak the recipe, for example if the output files are markdown I'll append ".md" to the keys. If your files follow a convention you might be able to configure it with just a list rather than a dictionary.
# Device management: tools on your developers PATH
Source: https://site.aspect.build/blog/bazel-devenv
Explore easier developer tool distribution with Bazel and direnv for seamless environment setup and management in your development workflow
“Device Management”, or MDM, is that thing which forces your work computer to have security software installed. It has the ability to push tools to developer machines too - however it’s owned by the security team at your company. While it would be convenient, I’ve found it’s difficult for the Developer Platform team to use that to distribute the "canonical developer environment”.
Some folks use a devcontainer or VDI (Virtual Desktop Infrastructure) to describe the tooling you need installed, but that’s heavy and slow. If we’re using Bazel, it already has features to give a hermetic environment, right?
A year ago, I wrote an article describing how to get tools on your developers local machine using Bazel, and today I have an update.
In the technique from my original post ([run tools installed by bazel](/blog/run-tools-installed-by-bazel)), users have to change their behavior, typing `./tools/my-tool` rather than just `my-tool`. Retraining developers to have a different command under their fingers is hard, especially for things they run all the time.
Shortly after I wrote that post, Fabian Meumertzheim created [https://github.com/buildbuddy-io/bazel\_env.bzl](https://github.com/buildbuddy-io/bazel_env.bzl). This is an alternative technique I’ll write about today. It puts the tools on the `$PATH` instead, fixing the ergonomic issue from the first technique — however there are always trade-offs! This one requires engineers manually install the `direnv` tool before they get setup.
## How direnv works
**direnv** is a command-line tool that automatically sets and unsets environment variables when you `cd` into or out of a directory. It’s especially useful for managing project-specific environment variables, such as secrets, configuration settings, or language versions (like Python or Node.js versions).
* You place a `.envrc` file in your project directory.
* This `.envrc` file contains shell commands to export environment variables or run setup scripts.
* When you enter the directory, `direnv` loads the `.envrc` file and applies the environment changes.
* When you leave the directory, it automatically reverts those changes.
Here’s how it looks when I enter Aspect’s monorepo (named “silo”):
```plaintext theme={null}
alexeagle@aspect-build ~ % cd Projects/silo
direnv: loading ~/Projects/silo/.envrc
direnv: export ~PATH
```
Installing `direnv` isn’t just a matter of getting the program on your machine. It also needs to hook into your shell (it supports `bash`, `zsh` and many others). And finally, you must explicitly allow each `.envrc` to be trusted.
To follow this pattern, you’ll need to instruct your developers to install this first tool manually. Fortunately they’ll get a reminder of the instructions in the next step.
## bazel\_env creates a .envrc
After installing `bazel_env.bzl` you’ll have a runnable Bazel target, typically `bazel run //:_bazel_env` or `bazel run //tools:bazel_env`.
The `bazel_env` target is defined with a dictionary that maps a tool name to put on the PATH, to some other target that provides it. What kinds of targets can those be? Take a look at the example: [https://github.com/buildbuddy-io/bazel\_env.bzl/blob/main/examples/BUILD.bazel](https://github.com/buildbuddy-io/bazel_env.bzl/blob/main/examples/BUILD.bazel)
1. Binary targets for programs you author yourself in the monorepo
2. Tools provided by a toolchain, like `go`, `node`, `pnpm`, `cargo`, etc
3. With [https://github.com/theoremlp/rules\_multitool](https://github.com/theoremlp/rules_multitool) you can run `multitool` to update a `tools.lock.json` file, and all of these tools are installed.
4. CLI utilities distributed by a package manager, like console scripts from PyPI, `bin` entries from the `package.json` of NPM packages, Go utilities (see `scaffold` example here), and so on.
```plaintext theme={null}
% bazel run //tools:bazel_env
INFO: Analyzed target //tools:bazel_env (580 packages loaded, 72963 targets configured).
INFO: Found 1 target...
Target //tools:bazel_env up-to-date:
bazel-bin/tools/bazel_env_all_tools
INFO: Elapsed time: 9.399s, Critical Path: 0.45s
INFO: Running command line: bazel-bin/tools/bazel_env.sh
====== bazel_env ======
✅ direnv is installed
✅ direnv added bazel-out/bazel_env-opt/bin/tools/bazel_env/bin to PATH
Tools available in PATH:
* aws: @aws
* pnpm: @pnpm
* gofumpt: @@rules_multitool~~multitool~multitool//tools/gofumpt:gofumpt
* jsonnetfmt: @@rules_multitool~~multitool~multitool//tools/jsonnetfmt:jsonnetfmt
* shfmt: @@rules_multitool~~multitool~multitool//tools/shfmt:shfmt
* terraform: //tools:terraform
* yamlfmt: @@rules_multitool~~multitool~multitool//tools/yamlfmt:yamlfmt
* ruff: @@rules_multitool~~multitool~multitool//tools/ruff:ruff
* shellcheck: @@rules_multitool~~multitool~multitool//tools/shellcheck:shellcheck
* buf: @@rules_multitool~~multitool~multitool//tools/buf:buf
* buildozer: @@rules_multitool~~multitool~multitool//tools/buildozer:buildozer
* docker-compose: @@rules_multitool~~multitool~multitool//tools/docker-compose:docker-compose
* diesel-cli: @@rules_multitool~~multitool~multitool//tools/diesel-cli:diesel-cli
* etcdctl: @@rules_multitool~~multitool~multitool//tools/etcdctl:etcdctl
* grpcurl: @@rules_multitool~~multitool~multitool//tools/grpcurl:grpcurl
* ibazel: @@rules_multitool~~multitool~multitool//tools/ibazel:ibazel
* multitool: @@rules_multitool~~multitool~multitool//tools/multitool:multitool
* otel-cli: @@rules_multitool~~multitool~multitool//tools/otel-cli:otel-cli
* otelcol-contrib: @@rules_multitool~~multitool~multitool//tools/otelcol-contrib:otelcol-contrib
* promtool: @@rules_multitool~~multitool~multitool//tools/promtool:promtool
* pyrra: @@rules_multitool~~multitool~multitool//tools/pyrra:pyrra
* tflint: @@rules_multitool~~multitool~multitool//tools/tflint:tflint
* tfsec: @@rules_multitool~~multitool~multitool//tools/tfsec:tfsec
* buildifier: @buildifier_prebuilt//:buildifier
* scaffold: @com_github_hay_kot_scaffold//:scaffold
* node: $(NODE_PATH)
* cargo: $(CARGO)
* rustfmt: $(RUSTFMT)
Toolchains available at stable relative paths:
* nodejs: bazel-out/bazel_env-opt/bin/tools/bazel_env/toolchains/nodejs
* rust: bazel-out/bazel_env-opt/bin/tools/bazel_env/toolchains/rust
direnv: loading ~/Projects/silo/.envrc
direnv: export ~PATH
```
## Guardrails
There are a few things that can go wrong, which are worth pointing out.
You’ll note that the tools are actually installed under a named output folder, `bazel-out/bazel_env-opt` - what if the user runs a `bazel clean`? This case is well handled, since `direnv` is able to report errors in the `.envrc` file with a custom message:
```plaintext theme={null}
alexeagle@aspect-build silo % bazel clean
INFO: Starting clean (this may take a while). Consider using --async if the clean takes more than several minutes.
direnv: loading ~/Projects/silo/.envrc
direnv: ERROR[bazel_env.bzl]: Run 'bazel run //tools:bazel_env' to regenerate bazel-out/bazel_env-opt/bin/tools/bazel_env/bin
direnv: export ~PATH
```
Any time the tools change, everyone has to run it again.
It’s also not lazy (at least not yet). When you run `bazel_env` it needs to fetch all the tools, even those you never plan to run. See [https://github.com/buildbuddy-io/bazel\_env.bzl/issues/14](https://github.com/buildbuddy-io/bazel_env.bzl/issues/14)
As a workaround, you can have multiple `bazel_env` targets, but users have to choose the right one for the work they intend to do.
# Bazel for SONiC: What We've Learned and Contributed
Source: https://site.aspect.build/blog/bazel-for-sonic
We explore the case for adopting Bazel across SONiC Foundation projects and update the SONiC and Bazel community on Aspect Build contributions.
As the [Linux Foundation](https://www.linuxfoundation.org/)’s [**SONiC Foundation**](https://sonicfoundation.dev/) continues to drive forward an open, standards-based network operating system for the industry, one challenge has become increasingly visible across the community: **how to build, test, and release SONiC components with speed, consistency, and confidence**. The project has grown in complexity—diverse hardware platforms, multiple languages and toolchains, distributed teams, and the rising expectation that networking software behave like modern cloud software.
This is exactly where **Bazel**, the open-source, high-performance build and test system originally created at Google, can play a transformative role. Bazel offers **SONiC** (**S**oftware for **O**pen **N**etworking **i**n the **C**loud) contributors, adopters and vendors the reliability, scalability, and repeatability needed to sustain a world-class network OS at global scale.
Below, we explore the case for adopting Bazel across SONiC Foundation projects and how it can meaningfully improve developer productivity, platform compatibility, security posture, and release engineering.
***
## 1. Reproducible Builds Are No Longer Optional
SONiC today supports an expanding matrix of devices, ASICs, and software packages. That's its strength—but also its build challenge. Makefiles, ad-hoc scripts, and hand-rolled toolchains multiply variability and increase onboarding time, especially for new vendors.
Bazel provides hermetic, reproducible builds, ensuring that:
* The same inputs always produce the same outputs
* Dependencies are fetched deterministically and cached
* Toolchains and container images are versioned and immutable
* Builds can run anywhere—from a developer's laptop to CI runners to cloud build farms
This consistency dramatically reduces "it works on my machine" issues. For SONiC's multi-vendor ecosystem, it means any contributor can build and test with confidence against a shared standard.
***
## 2. A Universal Build System for a Polyglot Codebase
SONiC involves C++, Python, Go, Rust, Docker, kernel modules, switch-vendor SDKs, and more. Bazel excels here:
* First-class multi-language support
* Extensibility through Starlark rules
* Deterministic container and image builds
* Cross-compilation support for ARM, x86, PowerPC, and ASIC-specific toolchains
Instead of maintaining fragmented build logic across repositories, SONiC developers can standardize on a single system that handles everything from low-level DPDK components to high-level services.
***
## 3. Cloud-Native CI/CD That Scales With the SONiC Community
Bazel was designed for massive scale and parallelism. For SONiC maintainers, this directly translates into:
* Faster builds through fine-grained caching
* Incremental rebuilds that recompile only what changed
* Remote execution to distribute builds across clusters
* Remote caching to avoid duplicated work between developers and CI jobs
* Unified pipelines across all repos and languages
As the community continues to expand, this infrastructure lets new contributors ramp up quickly and ensures that CI is fast, stable, and cost-efficient.
***
## 4. Stronger Security and Compliance
Networking software is increasingly subject to regulatory scrutiny. SONiC vendors must prove the provenance, integrity, and patch level of every component.
Bazel strengthens security by:
* Locking down external dependencies
* Ensuring reproducibility (critical for supply-chain audits)
* Providing deterministic SBOM generation
* Enforcing hermetic builds that eliminate environment drift
* Integrating easily with tools for SLSA, sigstore, and in-toto
For organizations integrating SONiC into large-scale infrastructure, this reduces risk and simplifies compliance workflows.
***
## 5. Better Collaboration Between Vendors and the Community
One of SONiC's greatest strengths is its vendor-neutral ecosystem. Bazel reinforces this mission:
* All vendors compile with the same rules and toolchains
* Contributors can share Starlark extensions for SDKs or hardware targets
* Build logic becomes collaborative, reviewable, and testable—just like code
* Onboarding new vendors becomes faster and less error-prone
Instead of each party maintaining private build scripts, SONiC can establish a shared, open-standard build vocabulary.
***
## 6. Future-Proofing SONiC for the Next Decade
SONiC is evolving quickly—into new form factors, new silicon, new service models, and new deployment patterns. With Bazel, SONiC gains a foundation that:
* Scales horizontally as code volume increases
* Easily supports new languages or frameworks
* Provides deterministic releases that downstream integrators can trust
* Enables advanced workflows like distributed testing or reproducible builds in the cloud
Bazel is not a short-term patch; it is an investment in SONiC's long-term velocity and stability.
***
## The Legacy Build System: Complexity at Scale
To understand why Bazel is necessary, it's worth examining what SONiC's build process looked like before: a Makefile-based system built around a "slave" container image that encapsulated system dependencies.
### Arbitrary Commands, Arbitrary Problems
The legacy system consists of **317 Makefile rules** (`.mk` files) and dependency files (`.dep` files), one for nearly every package, container, and component in SONiC. Each rule is allowed to invoke arbitrary commands: `apt-get install`, `pip install`, `dget`, shell scripts, custom build logic. On the surface, this sounds reasonable—the "slave" container provides isolation. But at SONiC's scale, this approach becomes a liability:
**Brittleness**: Each recipe fetches dependencies from the internet on-demand. A network hiccup, a removed package from Debian archives, a deprecated Python module on PyPI, and the entire build fails. There's no guarantee that tomorrow's build will work the same way as today's. No pinning, no snapshot repositories, no explicit version control of dependencies.
**Non-Hermeticity**: Because recipes are allowed to fetch and execute arbitrary commands, the build's output depends on what's available on the internet *right now*. Build two identical SONiC commits a week apart, and they may produce different binaries. The build machine's OS and installed packages are implicit dependencies—if you upgrade Ubuntu on your build server, you risk silently changing SONiC's outputs.
**Unmaintainability at Scale**: With 317 separate rule files, coordinating changes is nightmarish. A Debian package update might require changes to multiple recipes. Dependency injection during build (where rules can declare new dependencies at build time) means the full dependency graph is opaque until runtime. You don't know what's actually going to be built until you run `make`.
**Hidden Complexity**: The "slave" container approach obscures the real problem. Developers think they're building in a consistent environment because it's Docker-based, but the container itself is built by the same brittle, non-hermetic process. You're wrapping chaos in a box and calling it reproducibility.
### Recipes Inject Dependencies During Build
Making matters worse, the legacy system allows rules to **inject dependencies dynamically during the build process**. A recipe isn't just a fixed input-output mapping; it's a script that can decide at runtime what to depend on. This makes the dependency graph fundamentally unknowable until build execution. You can't reason about what the artifact will contain—you have to run it and see.
### The Scale Problem
SONiC spans dozens of C/C++ components, multiple Python packages, Go binaries, container images, kernel modules, and ASIC-specific toolchains. The legacy system required maintainers to write and maintain Makefiles for all of this, with no unifying framework. Each component has its own recipe, its own build logic, its own fragile internet-based dependency fetching. When SONiC grew to support ARM architectures, multiple Debian versions (Stretch, Buster, Bullseye, Bookworm), and dozens of ASIC vendors, the number of recipes multiplied.
The build system became a maintenance burden that grew faster than the codebase itself.
***
## Bazel is decades ahead as a build system—but it’s not autopilot. You still need a good engineer to make it fit the problem.
Bazel is uniquely suited to address the intricate build and test challenges faced by the SONiC project. As SONiC evolves to support a growing range of devices, languages, and architectures, the need for a robust build system becomes paramount. Bazel’s hermetic, reproducible builds help ensure that the same inputs consistently yield the same outputs—an essential property in a diverse ecosystem. With deterministic dependency management and strong cross-compilation support, Bazel aligns well with SONiC’s goals of consistency across platforms and contributors.
But Bazel isn’t a silver bullet. It’s decades ahead as a build system, yet its architecture doesn’t solve the problem automagically—real success still depends on good engineering: clear rule boundaries, disciplined dependencies, and a toolchain/packaging strategy that makes Bazel’s guarantees real in practice.
## The Deep Technical Challenge: Hermetic Package Dependency Resolution
Building a polyglot, multi-architecture system like SONiC using Bazel exposed a fundamental tension between how traditional Linux package managers work and how Bazel is designed to operate. The solution has required solving multiple interconnected problems that most build systems never encounter.
The sections below summarize some of the Bazel for SONiC issues that [Aspect Build](http://www.aspect.build) and has helped to identify and address.
### The .deb Package Overlay Problem
Debian packages are designed to be installed sequentially into a shared filesystem. When you run `apt-get install`, each package unpacks its files into the same root directory—creating a virtual "overlay" of files from potentially hundreds of packages. Circular dependencies are resolved through this overlay: library A might contain a symlink to library B, which is provided by a completely different package. The order of installation handles conflicts, and the running system sees a unified merged filesystem.
Bazel, by contrast, abhors large, opaque target outputs. Each target should be hermetic and reproducible, with explicit dependencies and minimal side effects. Representing "unpack all these packages and overlay them" as a single Bazel target creates an enormous, unwieldy build artifact that defeats caching, incremental builds, and reproducibility. Yet SONiC needs precisely this—a complete, consistent sysroot with hundreds of interdependent Debian packages.
The solution: *Bazel needed to compute the transitive closure of package dependencies, resolve symlinks ahead of time, and produce a metadata representation of the merged filesystem without materializing the entire overlay*\*\*.\*\* Rather than creating a giant unpacked sysroot target, `rules_distroless` generates a "Contents" file—a snapshot mapping every filename to the package that provides it. This allows Bazel to:
* Resolve symlink chains before build time (avoiding "dangling symlink" errors that plague container builds)
* Determine the final location of each library or header file despite circular package dependencies
* Share the metadata across builds without replicating hundreds of MB of unpacked packages
* Enable fine-grained caching at the package level, not the sysroot level
### The RPATH Nightmare
The ELF dynamic linker, [`ld.so`](http://ld.so), searches for shared libraries in a precise order:
1. Directories in the binary's `DT_RPATH` attribute (if `DT_RUNPATH` is absent)
2. `LD_LIBRARY_PATH` environment variable
3. Directories in `DT_RUNPATH` (the modern preferred approach)
4. System cache (`/etc/`[`ld.so`](http://ld.so)`.cache`)
5. Default paths (`/lib`, `/usr/lib`, and architecture-specific variants)
By default, binaries built in SONiC expect their dependencies in `/usr/lib`, `/lib`, and architecture-specific variants like `/usr/lib/aarch64-linux-gnu`. But Bazel builds place outputs in `bazel-out/`, a completely different path structure. The linker has no idea where to find [`libyang.so`](http://libyang.so)`.2.0` when it's buried in `bazel-out/aarch64-linux-gnu/bin/external/com_github_sonic_net_sonic_mgmt_common/lib/`[`libyang.so`](http://libyang.so)`.2.0`.
The fix requires embedding `RPATH` entries directly into the binary at link time—telling the linker "look in `$ORIGIN/../lib` for my dependencies." But this introduces new challenges:
* **Relocation**: A binary linked with `RPATH=$ORIGIN/../lib` expects a specific directory structure. If that binary is later used as a tool in another Bazel action (where it's relocated to a different path), the `RPATH` becomes invalid.
* **Transitive Dependencies**: When a binary depends on library A, which depends on library B, the linker must find all three—but each may have different `RPATH` settings. Bazel must ensure the transitive closure is visible to the linker without creating massive, monolithic targets.
SONiC solved this through **Bazel Configurations and Transitions** —a mechanism that applies different compiler flags to different parts of the dependency graph. Some targets (like Python's C extensions) are compiled with `-fPIC` and packaged into a sysroot. Everything else uses the standard compilation model. Bazel's transitions ensure these different compilation modes never mix, preventing linker errors where position-dependent code tries to reference position-independent code.
### The RPATH Nightmare Continues: Finding Dependencies in the Sandbox
When Bazel runs a test target, it constructs a temporary sandbox directory containing only the files that test explicitly depends on. A test might link against a dynamic C++ library, which depends on [`libc.so.6`](http://libc.so), which transitively depends on [`ld-linux-x86-64.so.2`](http://ld-linux-x86-64.so). At test runtime, none of these libraries are in the system `/usr/lib`—they're scattered across `bazel-out/` in the test's sandbox.
**The breakthrough**: Rather than relying on `LD_LIBRARY_PATH` (which is fragile and defeats reproducibility), `rules_distroless` ensures that when a test binary is linked, its `RPATH` contains the exact paths to all transitive dependencies *within that test's sandbox*. The binary becomes self-contained—it knows exactly where to find every library it needs, relative to its own location. A test can now be run anywhere, on any machine, and it will find its dependencies without environment variable manipulation. This is genuinely hermetic testing: the test's success or failure depends only on the code and its declared dependencies, not on what happens to be installed on the developer's machine.
### Container Runtime: Binaries Finding Dependencies Across Layers
When a SONiC container starts, thousands of binaries are present—p4rt, swss, gnmi, monitoring agents, and hundreds of utilities. None of them are statically linked. Each binary must find its shared libraries at runtime.
In a traditional Docker/container image built with a Dockerfile, everything is installed into standard locations (`/lib`, `/usr/lib`). The dynamic linker searches these paths by default. Simple and naive.
But SONiC containers built with Bazel take a different approach. Multiple packages provide the same functionality (e.g., multiple versions of [`libprotoc.so`](http://libprotoc.so)), but only one should be "selected" for the final image. Furthermore, if SONiC switches from Debian bookworm to Debian trixie, or moves between Ubuntu LTS versions, the exact libraries available change. Hardcoding `/usr/lib/x86_64-linux-gnu/`[`libfoo.so.1`](http://libfoo.so) in a binary's `RPATH` would break when the library moves or changes versions.
**The solution**: Bazel generates a container image where *the RPATH of every binary is already computed to find the exact libraries that will be present in that specific image*. When p4rt starts in the container, its `RPATH` contains the paths where `rules_distroless` placed [`libprotoc.so`](http://libprotoc.so), [`libyang.so`](http://libyang.so), and everything else. The binary finds its dependencies through the RPATH, not through the system's default search paths. This means:
* The container is self-describing: a binary's dependencies are "baked in" rather than discovered at runtime
* Moving from Debian bookworm to trixie doesn't break existing binaries—Bazel recomputes the RPATH for the new distro
* Libraries can be placed anywhere in the image without breaking binaries
* Container images are reproducible: given the same Bazel build configuration, the same binaries will find the same libraries every time
### Liberation from Host Distro Dependencies
Perhaps the most profound impact: `rules_distroless` decouples the build machine's operating system from the built artifact.
Traditionally, if you build SONiC on Ubuntu 24.04 and then try to run the binaries on Ubuntu 22.04, you hit subtle glibc incompatibilities. Some binaries depend on functions only available in Ubuntu 24's glibc version. Others link against the "wrong" version of OpenSSL. Developers spend weeks debugging "works on my machine" failures.
With `rules_distroless`, the build process explicitly pins every single Debian package using Debian snapshot repositories. When you build SONiC on an Ubuntu 22.04 machine, the build ignores the system's installed packages and fetches exact, pinned versions from the snapshots. The build output contains binaries linked against those exact packages, regardless of what's on the build machine.
This means:
* A developer on Ubuntu 20.04 can build SONiC targeting Debian bookworm without installing bookworm libraries locally
* CI/CD runners can be any modern Linux distro—the build is isolated from the host
* Build results are reproducible across machines because dependency versions are explicit, not implicit
* Upgrading the build machine's OS doesn't accidentally change SONiC's binaries
The build machine becomes nearly irrelevant. You're no longer asking "how do I build this on my machine?" You're asking "given a Bazel configuration and a snapshot of the Debian archive, what do I build?" The answer is the same everywhere, because Bazel controls everything.
### Radical Hermeticity: Explicit Dependencies, Zero Implicit Assumptions
The commitment to hermeticity goes far beyond RPATH tuning and package pinning. SONiC's C/C++ compilation is built with compiler flags that reject the very concept of "system libraries":
```plaintext theme={null}
-nostdinc -nostdinc++ -nostdlib
```
These flags tell the compiler: "There are no standard libraries. There are no default include paths. Everything must be explicitly declared." Without these flags, a compiler would silently fall back to the build machine's `/usr/include` and `/usr/lib`. A developer on Ubuntu 24.04 might accidentally use headers from Ubuntu 24's glibc, even if the target is Debian bookworm. With `-nostdinc -nostdinc++ -nostdlib`, that accident becomes impossible—the build fails loudly if a dependency is missing.
Every header file, every standard library function, every bit of C runtime must be explicitly provided as a Bazel dependency. This guarantees that:
* A build on any machine produces identical binaries
* Adding a dependency automatically updates the build configuration (nothing hidden)
* Removing unused dependencies is impossible—they're explicitly declared
* Switching between libc implementations or versions is straightforward—just change the declared dependency
### Auto-Generated cc\_library Targets: Turning .deb Packages into Bazel Dependencies
But declaring every libc header and every system library explicitly would require thousands of manual `cc_library` rules. That's where `rules_distroless` shines.
When `rules_distroless` processes a Debian package, it doesn't just extract files. It **automatically generates Bazel** `cc_library` targets that encapsulate:
* All header files from the package (C standard library headers, C++ standard library headers, architecture-specific headers)
* All shared object libraries
* The correct include paths and linker settings
* All transitive dependencies, automatically resolved
From a SONiC developer's perspective, instead of hoping that libc headers and libraries exist somewhere on the system, they explicitly declare:
```python theme={null}
cc_binary(
name = "my_tool",
srcs = ["main.cc"],
deps = [
"@debian//libc6",
"@debian//libstdc++",
],
)
```
The Bazel build system now understands exactly which Debian packages this binary depends on. If you want to upgrade libc, you change one line in MODULE.bazel. If you want to use a different libc or switch distributions entirely, the build system tracks it explicitly.
**This auto-generation is the key to polyrepo hermetic builds.** In a monorepo, one team can write all the libc rules. But SONiC spans multiple GitHub repositories. Each repo's BUILD files can independently declare which Debian packages it needs. The `rules_distroless` machinery automatically generates compatible `cc_library` targets, and Bazel's module system ensures they all use the same versions across all repositories.
No more silent fallback to system libraries. No more "it works on my machine but not in CI." Every dependency is explicit, versionable, and traceable.
### The Performance Paradox
Computing transitive closure of dependencies across a polyrepo, resolving symlink chains, and managing RPATH dynamically sounds expensive — and it is. But doing it at build time, once, and caching the result is far cheaper than doing it at container runtime or — worse — debugging "symbol not found" errors weeks later in production.
The trick is recognizing what can and cannot be cached:
* **Package metadata** (which files each package provides, symlink targets) is stable and highly cacheable
* **Sysroot layout** (which package provides which file when all dependencies are considered) is computed once and reused
* **Binary relocations** (embedding RPATH in binaries) happens at link time and is reproducible
* **Container assembly** (final layer selection) happens downstream and doesn't affect upstream caching
SONiC's Bazel infrastructure treats these as separate concerns, allowing massive parallelism and cache reuse even though the final build is complex.
***
## Solving the Python Dependency Maze: Explicit, Versioned, Cross-Compilable
Python presents a unique challenge in reproducible builds. Unlike C/C++ where dependencies are system packages installed via a package manager, Python's ecosystem fetches from PyPI—a centralized repository where package availability and versioning can change. The standard approach to managing Python dependencies in Bazel has been `rules_python`, but it has limitations that made it unsuitable for SONiC's polyglot, multi-architecture environment.
### The PyPI Problem at Scale
SONiC uses Python extensively: configuration generation tools, management daemons, testing frameworks, and utilities. Each Python package has transitive dependencies on other packages, many of which are compiled extensions (`.so` files). The challenge is that `rules_python`'s traditional `pip_parse` implementation:
* Assumes a single target architecture (no cross-compilation support)
* Doesn't pin package versions deterministically
* Struggles with compiled Python extensions that depend on system libraries
* Doesn't integrate well with polyrepo ecosystems where multiple repositories have different Python dependency requirements
In SONiC's case, you might need to build the same Python package for x86-64 and ARM64 simultaneously. The standard tooling wasn't built for that.
### Enter Aspect Rules Python: Modern Dependency Management
To solve these problems, [**Aspect Rules Python**](https://github.com/aspect-build/rules_py) (`aspect_rules_py`) provides a modern reimplementation of Python dependency management in Bazel. It replaces the traditional `pip_parse` with a new implementation based on **uv**, a fast, production-grade Python package resolver.
Key improvements:
**Explicit Versioning**: Dependencies are pinned in a lock file (similar to `requirements.lock`), ensuring that every build uses the exact same package versions. No surprises from PyPI changes.
**Cross-Compilation Support**: Unlike the original `rules_python`, `aspect_rules_py` can build Python packages for different architectures simultaneously. This is critical for SONiC: the same Python source code can be compiled as a wheel for x86-64, then recompiled for ARM64, with all dependencies resolved correctly for each target.
**Integration with Hermetic Sysroots**: When a Python package has compiled extensions that depend on system libraries (e.g., a C extension that links against `libyang`), the sysroot provided by `rules_distroless` makes the correct headers and libraries available. `aspect_rules_py` integrates seamlessly with this sysroot, so the package's build automatically uses the right C compiler flags, header locations, and link paths for the target architecture.
**Polyrepo-Friendly**: Each SONiC repository declares and resolves its own Python dependency closure independently. `aspect_rules_py` uses uv to compute the complete transitive dependency graph for each repository, generating a lock file that pins all transitive dependencies. Because each repo has its own lock file, different repositories can use different versions of shared dependencies without conflict—Bazel's module system keeps them isolated.
### Enabling Container Cross-Compilation
Combined with the Debian package management improvements (`rules_distroless`), `aspect_rules_py` enables a powerful capability: **building complete container images for different architectures within a single Bazel build**.
Before, cross-compiling a SONiC container for ARM64 required:
1. Running the build on an ARM64 machine (or emulating it, which was slow)
2. Managing different Python dependency versions for different architectures
3. Handling the mismatch between the build machine's Python environment and the target architecture
With `aspect_rules_py` and `rules_distroless`, Bazel can:
1. Resolve Python dependencies for the target architecture (e.g., ARM64)
2. Build wheels for that architecture using the sysroot's C compiler and libraries
3. Layer those wheels into the container image alongside the pinned Debian packages
4. All within a single Bazel invocation on any machine
This means a developer on an x86-64 laptop can type `bazel build //path/to:docker-swss-arm64` and get a fully cross-compiled container image without any special setup, emulation, or native ARM64 hardware.
***
## A Call to Action for the SONiC Foundation
The SONiC community is at an inflection point. As deployments reach hyperscale and the contributor base grows more diverse, the project needs a build and test system that matches its ambitions.
Bazel offers exactly that: a modern, reproducible, cloud-scale platform that empowers contributors, accelerates releases, and strengthens the entire ecosystem.
Adopting Bazel across SONiC Foundation projects will:
* Reduce fragmentation
* Increase developer productivity
* Improve security and auditing
* Enable faster and more reliable releases
* Provide a consistent experience for every vendor and contributor
The benefits compound over time—and they're aligned with SONiC's vision of an open, interoperable, high-performance network OS. **Let's give SONiC the build system it deserves**.
### Accelerating Innovation Through Reproducible, Scalable, Cloud-Native Build Systems
In this article, we have explored the case for adopting Bazel across SONiC Foundation projects and how it can meaningfully improve developer productivity, platform compatibility, security posture, and release engineering. We’ve also updated the Bazel and SONiC communities on some of our recent contributions to empower Bazel for SONiC.
As the SONiC Foundation continues to drive forward an open, standards-based network operating system for the industry, one challenge has become increasingly visible across the community: **how to build, test, and release SONiC components with speed, consistency, and confidence**. The project has grown in complexity—diverse hardware platforms, multiple languages and toolchains, distributed teams, and the rising expectation that networking software behave like modern cloud software.
This is exactly where Bazel, the open-source, high-performance build and test system originally created at Google, can play a transformative role. Bazel offers SONiC contributors and vendors the reliability, scalability, and repeatability needed to sustain a world-class network OS at global scale.
## Next Steps
Interested in learning more about how to succeed with Bazel for SONiC? [Schedule time to talk with us](https://calendly.com/aspect-build/intro?back=1\&month=2026-01) or email us at [hello@aspect.build](mailto:hello@aspect.build).
# Bazel market growth, year over year
Source: https://site.aspect.build/blog/bazel-market-2023
Bazel adoption grew by 56% in 2023, with around 950 companies now using it. Learn more about the growth and its implications for businesses and developers.
A year ago, I wrote about how Google hadn't produced any adoption numbers for Bazel. However, companies like ours must make informed investment decisions, and so must a lot of individuals deciding whether the Bazel ecosystem is "big enough". This could be as simple as "should I convince my husband that it's worth me spending my nights and weekends to write a book about Bazel", or "is it economical for my consulting company to advertise our services by hosting a Bazel podcast". On the other hand, it could involve institutional investors who are putting millions of dollars into a business plan that is counting on projected revenue from users to make Bazel more usable by enterprises.
Here's that post from last year: [https://blog.aspect.dev/estimating-bazel-adoption](https://blog.aspect.dev/estimating-bazel-adoption). The summary: in the absence of data from Google, we used estimated **600 companies were using Bazel**.
## This year
Fast-forward to BazelCon this year. In the keynote, there was a wide open opportunity for the Bazel team to provide some adoption numbers. Even though Bazel still doesn't have telemetry built-in, there's a simple proxy - the same one we always used for the Angular JavaScript framework: distinct active users of the documentation site. Sadly, while the Bazel team is interested in providing this data, there are bureaucratic obstacles in privacy policy and legal standing in the way, so they still haven't published anything. I remain optimistic that we'll get it eventually!
Fortunately, my co-founder Greg is willing to do all the work to reproduce our analysis from last year, and our result is that **there are now about 950 companies using Bazel, a 56% increase from last year.**
## Details
I won't repeat the methodology we used, as you can read about it in the post from last year. Essentially, we take two different approaches to list which companies use Bazel, then see how many companies appear in both lists. Assuming a uniform "density", this gives us an approximation of how many Bazel users appear on *neither* list, and so we can extrapolate a total.
Last year we published this data:
```plaintext theme={null}
TBA = [# public list] * [#private list] / [# overlap] = 218 * 127 / 46 = 602
```
Updating for November 2023, we now arrive at:
```plaintext theme={null}
TBA = [# public list] * [#private list] / [# overlap] = 311 * 228 / 75 = 945
```
# Fixing Bazel out-of-memory problems
Source: https://site.aspect.build/blog/bazel-oom
Troubleshoot and fix Bazel's out-of-memory issues in both JVM and system contexts with practical solutions and tips.
Memory management is a generally hard topic in computer systems operations. Debugging it inside a cloud-hosted build system is even worse!
There are two potential problems:
* The Bazel server runs in a JVM, and it internally tries to allocate more objects than the max heap size its allowed.
* Bazel spawns subprocesses (called "actions", including test actions) and they collectively exhaust the memory in the machine or VM that Bazel runs in.
I'll cover these scenarios separately since they're mostly unrelated.
> Of course, the Bazel JVM heap does occupy system memory, so they're related in the sense that a smaller Bazel server footprint would allow for more actions to run, but I've never considered that to be a potential remediation.
## Bazel server out-of-memory
How to tell this is happening:
* Bazel exits with code 33 (see [`ExitCodes.java`](https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/util/ExitCode.java#L58))
* Check the output of `bazel info | grep heap` if the Bazel server is still running, see if it is near the max.
Some things you can do about it:
* Give it more RAM! Assuming the system has some available, you can use the [`host_jvm_args` startup flag](https://docs.bazel.build/versions/main/command-line-reference.html#flag--host_jvm_args) to adjust the usual JVM parameters like `-Xmx2g`.
* Always turn on the [`--heap_dump_on_oom` flag](https://bazel.build/reference/command-line-reference#flag--heap_dump_on_oom) so that you get extra information in this case.
* "Memory saving mode" can be useful if you're hosting Bazel in ephemeral CI workers where you expect every build to be cold, however that's slow and not recommended. If you do that, you can avoid Bazel tracking incremental state which saves some memory. [https://bazel.build/configure/memory](https://bazel.build/configure/memory)
* Roll up your sleeves and figure out what's consuming so much memory in Bazel's JVM. Start from [Bazel's documentation on memory profiling](https://bazel.build/rules/performance#memory-profiling). An example can be rulesets where data is repeated rather than using depsets, an example analysis: [https://github.com/aspect-build/rules\_js/pull/391](https://github.com/aspect-build/rules_js/pull/391)
## System out-of-memory
Bazel schedules actions (build steps and test runners) based on the amount of system resources it thinks are available, and using some heuristic about how much RAM a typical action requires. Two kinds of things can go wrong, either Bazel thinks more RAM is available than the system actually has free, or Bazel underestimates the resources to be reserved for a given action.
By default, Bazel's max concurrency is based on the heuristic that each action needs one CPU core, so the `--jobs` flag default is the number of (maybe virtual) CPUs on the machine. Note that Bazel reports progress with a "X running" indicator which might lead you to believe that the concurrency is actually higher, but that's a misleading message because it can include actions that are queued waiting for resources
Did you know when @bazelbuild prints progress like
\[12 / 100] 32 actions, 30 running
That "running" count includes "remote-cache" spawns! If you have --jobs=16 (the default on 16 core) the other 14 of them aren't actually running, they're queued for "local" spawn. [https://t.co/a3w7TWZVTM](https://t.co/a3w7TWZVTM)
— Alex 🦅 Eagle (@Jakeherringbone) August 17, 2022
How to know this is happening:
* The Bazel server gets killed by the operating system like `Bazel server terminated abruptly (error code: 14, error message: 'Socket Closed', log file: ...)`
Some things you can do about it:
* If Bazel is running inside a container, it may calculate available RAM based on the host system rather than what is allocated for the container, due to [Default local resources should respect cgroup limits on Linux](https://github.com/bazelbuild/bazel/issues/3886). The remediation is to explicitly tell Bazel's scheduler what the container limits are by setting the [`--local_ram_resources` flag](https://bazel.build/docs/user-manual#local-resources) to match the container runtime.
* Reduce `--jobs` so that fewer things run concurrently. This is a blunt approach and makes all builds take longer, but saves you the effort of figuring out which actions didn't get enough resource reservation.
* Figure out which actions consume a lot of RAM, and tell Bazel's scheduler to reserve more resources for them. For tests, use the [`test_size` attribute](https://bazel.build/reference/be/common-definitions#common-attributes-tests) - a larger size gets more reserved memory per the table in that documentation. For build actions, the Bazel team [recommends](https://github.com/bazelbuild/bazel/issues/14601) using [execution properties](https://docs.bazel.build/versions/main/exec-groups.html#using-execution-groups-to-set-execution-properties) though to be honest, that looks really complex and I haven't used it myself.
* Try out the [`--experimental_local_memory_estimate` flag](https://docs.bazel.build/versions/main/command-line-reference.html#flag--experimental_local_memory_estimate) to make Bazel smarter about knowing the available system resources at the time it's scheduling the subprocess to spawn.
* Investigate using [Remote Build Execution](https://bazel.build/remote/rbe) so that heavy workloads move off the machine and run on a cloud of executors.
# Bazel technique for Continuous Delivery
Source: https://site.aspect.build/blog/bazel-technique-for-continuous-delivery
Learn about using Bazel for Continuous Delivery, distinguishing between CI, CD, and Deployment, and optimizing artifact delivery processes
The term "CD" is ambiguous. Some engineers use it to mean "Continuous Deployment", in which changes are automatically released, e.g. into a "dev" environment.
Aspect recommends that Continuous Delivery is modeled as the step of the pipeline where built artifacts are uploaded from the build machine to a well-known repository location. This could be a container image registry like Docker Hub, a blob store like AWS S3, or even a database.
This makes a clear separation of responsibilities between CI, CD and Deployment:
* The CI pipeline runs all the tests to confirm the repo is in a shippable state
* The CD pipeline should then upload only the artifacts that are:
* configured with `BUILD.bazel` files. Product engineers don't need to worry about setting up CD
* green: it can prove that all relevant tests are passing
* changed from a previous build
* The deployment system
* locates and "promotes" artifacts to the next environment, such as "dev", "staging", or "prod".
## Build vs. Buy
The recommendations in this guide can be applied in two ways:
* DevInfra teams may wish to implement and operate a custom system for their organization, or
* Use [Aspect Workflows](https://aspect.build/workflows), which provides this feature out-of-the-box.
## What is "deliverable"
A deliverable artifact is one that contains both the binary or files to push, as well as the "pushing" logic that knows how to perform the upload. It might also send a message to the deployment system to trigger an auto-deployment of the new artifact.
In Bazel terms, this means a deliverable should be an executable program that can be `bazel run`.
### Container Images
The rules\_oci [`oci_push`](https://github.com/bazel-contrib/rules_oci/blob/main/docs/push.md#oci_push) or rules\_docker [`container_push`](https://github.com/bazelbuild/rules_docker/blob/master/docs/container.md#container_push) rules can both be executed with `bazel run` to push a Docker image to a registry like Docker Hub.
Therefore these rules are considered "deliverable".
### Git Push
Sometimes artifacts belong in a separate code repository. For example, an SDK built from the API definitions in a monorepo needs to be published.
See an example `git_push` executable [in this repository](https://github.com/aspect-build/bazel-examples/tree/main/git_push).
### S3 upload
See [s3\_sync](https://github.com/aspect-build/rules_aws/tree/main/examples/release_to_s3).
## Which targets to deliver
A `bazel query` expression is the most convenient way to locate deliverable targets. Users may choose a tagging scheme for their workspace (i.e. "all targets with `tags = ['artifact']`"), or deliver well-known rule kinds (i.e. `oci_push`), or both.
## Which changes to deliver
To optimize time and money, it's best to deliver only "changed" targets. This avoids wasted time and resources uploading the same artifact repeatedly. It also means that release engineers won't have to sort through a massive list of duplicates when choosing a release.
There are two approaches for choosing "changed" targets:
1. Predict the changes based on a version control delta. For example you could `git diff` between the hash being delivered and the "prior successful" delivery hash, then use a tool like [bazel-diff](https://github.com/Tinder/bazel-diff) or [target-determinator](https://github.com/bazel-contrib/target-determinator) to produce a list of targets that might be affected by those changes.
2. Determine empirically based on what is actually different. This requires determinism, so it must only use [unstamped](https://github.com/bazel-contrib/bazel-lib/blob/main/docs/stamping.md) build results (`--nostamp`). In most cases a green CI run just completed, so these unstamped outputs are easily available.
Aspect recommends following the second approach because the first has some downsides:
* [bazel-diff](https://github.com/Tinder/bazel-diff) is incorrect and will sometimes miss affected targets, so they aren't delivered.
* [target-determinator](https://github.com/bazel-contrib/target-determinator) is slow and may hurt the "service level indicator" of time between pushing a hotfix and being able to release that fix.
* It will over-deliver, because sometimes a source change doesn't actually factor into whether the release binary changes, such as for a comment-only change.
The rest of this section provides more details about the second approach (determine empirically).
To determine whether Workflows should deliver that executable target on a particular commit, it is first hashed using the [`aspect outputs` command](https://docs.aspect.build/cli/commands/aspect_outputs/) with a special pseudo-mnemonic "ExecutableHash", for example:
```sh theme={null}
$ aspect outputs 'attr("tags", "\bdeliverable\b", //...)' ExecutableHash
//cli:release h1:cj8OUC3l3fIr3Zxnffk6y7gukLOJmiWRCAQoqadg66Y=
//workflows/rosetta:release h1:kjHVajw+Nta2kh3Epcd32DkZxTE1NHA8b5N7hCNFNSM=
```
You need a lookup database to store previously delivered hashes. If the hash value matches one previously seen, then skip delivery of that target.
### Debugging changes to deliver
You can run the `aspect outputs` command locally to understand whether a given change to a source file results in a new executable. Sometimes the result may be surprising. For example, if a comment in a `.go` source file is changed, the compiler produces the same `.a` file as a result, so the hash seen on the uploader executable is unchanged.
Another scenario that won't change the executable is when some production configuration is changed. For example, you may use Helm charts to deploy to Kubernetes. If these aren't included inside the image, then changes to these files won't cause a new delivery.
## Perform the delivery
Run each deliverable target with [stamping](https://registry.bazel.build/modules/bazel_lib#lib-stamping-bzl) enabled. You can do this in a script which reads the targets from a manifest file, essentially `cat $delivery_manifest | xargs -N1 bazel run --stamp`.
# Bazel: what you give, what you get
Source: https://site.aspect.build/blog/bazel-what-you-give-what-you-get
Bazel: Efficient, incremental, and correct outputs by describing dependencies accurately. Keep your outputs up-to-date with minimal work
There are a few ways I like to describe Bazel to an engineer who hasn't used it. If they have used similar build tools like Gradle or Make, I'll usually start with comparing the configuration affordances or differences in execution strategies. But most engineers have only used the canonical tooling for the language they write, and only superficially interact with it. After all, they're busy writing the code, and the build system normally tries to hide behind the scenes. With Bazel, we're asking engineers to understand a bit more, and here's where I like to start:
Bazel offers you this proposition: you describe the dependencies of your application, and Bazel will keep your outputs up-to-date.
# Describe your dependencies
Most build systems allow any code to depend on anything. As a result, they are limited in how aggressively they can minimize re-build times. This is extra work you'll need to do, to get Bazel's benefits.
Your job is to describe your sources, by grouping them into "targets". For example, "a TypeScript library", "a Go package", "a dynamic-linked Swift library", etc. You say which source files in your repo are part of each target, and then what other targets it "depends" on. Sometimes you can give some other bits of description, like the module name this code should be imported by, or options for compiling it. Sometimes you'll have to indicate runtime dependencies as well, such as some data file read during one of your tests.
That's it - you don't have to tell Bazel what to do with these sources.
The amount of work varies. Since your source code generally hints at the dependencies (like with `import` statements) it's possible for tooling to automate 80% of the work, and such BUILD file generators exist for a small, increasing number of languages. It's also up to you how detailed to be - you can just make one coarse-grained target saying "all the Java files in this whole directory tree are one big library", or you could make fine-grained ones for each subdirectory, or something in the middle.
The more correct your dependency graph, the more guarantees Bazel provides. If your graph is missing some inputs, then Bazel can't know to invalidate caches when those inputs change. This is called non-hermeticity. If your tools produce different outputs for the same inputs (like including a timestamp or non-stable ordering), then Bazel will be less incremental than it should since dependent targets will have to re-build. This is called non-determinism.
As a side benefit of describing your dependencies, sometimes you'll also discover undesired dependencies, so you can fix those and/or add constraints to prevent bad dependencies from being introduced in your code.
# Keeps your outputs up-to-date
In exchange for your work in describing your dependencies, you get a fantastic property: fast, incremental, and correct outputs.
Your outputs are a filesystem tree, usually in the `bazel-out` folder. Bazel populates some subset of this tree depending what you ask for. If you ask for the default outputs of a Java library, Bazel places a `.jar` file in the output tree. If you ask for a test to be run, Bazel places the exit code of that test runner in the output tree (representing the pass/fail status).
Bazel does the minimum work required to update the output tree. In the trivial case, Bazel queries the dependency graph and determines that the inputs to a given step are the same as a previous build, and does no work. This "cache hit" is the common case. If you don't have a cache hit locally on your machine, Bazel will fetch one from a remote cache.
If you change one file, then any nodes in the dependency graph that directly depend on it must be re-evaluated. That might mean a compiler is re-run. However if the result is the same as a previous run, then there is no more work to be done. This avoids "cascading re-builds" where a whole spine of the tree is re-evaluated.
There are a lot of things you can do with an incrementally-updated output tree. For example, you can set up your CI to just run `bazel test //...` (test everything) and then rely on Bazel incrementality and caching to be sure only the minimal build\&test work happens for each change.
There's a lot more to Bazel, but I find this description fits well in a two-minute attention span and conveys the basic value proposition.
# Aspect at BazelCon 2023
Source: https://site.aspect.build/blog/bazelcon-2023
Discover Aspect's BazelCon updates: bazel lint, bazel-lib 2.0, rules_py, partnership with Chainguard, and more. Watch our talks and explore new features.
We had a great time visiting Google's Munich office for the BazelCon conference. This annual event is always a big milestone for us, so I've written up a summary of the product announcements and links to our talks.
### bazel lint
We announced the first release of our `bazel lint` support. This is in two parts:
1. an OSS repository [aspect-build/rules\_lint](https://github.com/aspect-build/rules_lint) which integrates many formatting and linting tools under Bazel, and
2. a new `lint` [command](https://aspect.build/docs/cli/tasks/lint) in the Aspect CLI which allows you to run linters without requiring they have the same "hard error" semantics as tests.
Here's a quick demo:
In the next release of [Aspect Workflows](https://www.aspect.build/workflows) you'll be able to see these lint warnings as "expert code review comments" on your GitHub code reviews. This matches how Google wires up linters in the internal developer workflow. It allows you to turn on a new check for newly introduced code without being forced to fix or suppress all existing occurrences first.
### bazel-lib 2.0
[bazel-lib](https://github.com/aspect-build/bazel-lib) is the "standard library" Aspect uses to write Bazel rules and BUILD files for our clients. With over 20 Bazel rulesets, we have learned a lot of common patterns and encoded them for you to re-use.
Our 2.0 release adds a `tar` rule, which is a simpler alternative to the `pkg_tar` rule in rules\_pkg. You can find details in the [tar rule documentation](https://github.com/aspect-build/bazel-lib/blob/main/docs/tar.md).
The lead engineer for bazel-lib, Derek Cormier, gave a BazelCon talk *bazel-lib for BUILD and rules authors* which you can watch here: [https://www.youtube.com/watch?v=IXimf4DCAoY\&t=27947s](https://www.youtube.com/watch?v=IXimf4DCAoY\&t=27947s)
### rules\_py
Aspect engineers have been among the most [active maintainers](https://github.com/bazelbuild/rules_python/graphs/contributors) of the rules\_python ruleset. We added the hermetic interpreter, Gazelle extension, wheel publishing, and much more.
However, we have found that the "Google internal" approach for laying out Python programs on the disk is too far from the Python ecosystem standard of creating a virtual environment (virtualenv). rules\_py aims to solve this with re-implementations of the `py_*` rules that are more compatible, and therefore have fewer bugs and work with your editor.
Our Staff Engineer, Matt Mackay gave a BazelCon talk *Python & Bazel: Aspect's rules\_py* which you can watch here: [https://www.youtube.com/watch?v=IXimf4DCAoY\&t=28536s](https://www.youtube.com/watch?v=IXimf4DCAoY\&t=28536s)
### Partnership with Chainguard
[Chainguard](https://www.chainguard.dev/) is solving the "supply-chain security" problem of base images for containers. They funded our work on [rules\_apko](https://registry.bazel.build/modules/rules_apko), which integrates their Wolfi "un-distro" with Bazel, and we just released v1.0! This lets engineers easily and securely assemble a base image using the same Bazel idioms they already understand, and without the downsides of a Dockerfile that can contain arbitrary code and executes non-reproducible commands within a container runtime.
Read more in our Blog post: [https://blog.aspect.dev/rules-apko](https://blog.aspect.dev/rules-apko)
### Day 2 "Keynote"
Alex kicked off the second day of BazelCon with *I'm an Imposter and So Can You! Working in multiple languages at once*. This was not a technical talk, rather it explored the feelings of being out of place in a Developer Infrastructure team that's supporting and steering the work of product engineers who use unfamiliar languages. Watch the talk: [https://www.youtube.com/watch?v=IXimf4DCAoY\&t=118s](https://www.youtube.com/watch?v=IXimf4DCAoY\&t=118s)
### Aspect Workflows and Rules
Finally, Alex presented Aspect's products, both paid services and OSS rulesets. You can watch the *Aspect Workflows* talk here: [https://www.youtube.com/watch?v=8Dc8R\\\_Zrf6M\&t=10680s](https://www.youtube.com/watch?v=8Dc8R\\_Zrf6M\&t=10680s)
# What's New at BazelCon 2025
Source: https://site.aspect.build/blog/bazelcon-2025
Wow, it’s been an exciting year in Bazel-land. See highlights from the BazelCon 2025 keynotes, Hackathon, Aspect Build product releases and more.
Wow, it’s been an exciting year in Bazel-land, and Aspect has made the most of it. BazelCon is our yearly cadence for “conference-driven development”.
Here’s a retrospective of the conference and our highlights including from my Monday morning keynote.
## Travel Was a Pain
U.S. flight cancellations and delays brought many attendees to Atlanta in the wee hours of the morning. Either that, or it was a convenient excuse for feeling hungover during the morning sessions.
All told, BazelCon 2025 attracted over 330 in-person attendees in Atlanta. Thankfully everyone arrived safely.
## Aspect Extension Language
Bazel is great at two things: Loading & Analysis of the Dependency/Action graphs, and using plugins (“rulesets”) to populate the `bazel-out` folder from your sources.
A broad class of extensibility has been missing, and as a result most teams have written their own local dev scripts wrapping `bazel` and also some CI/CD YAML for their pipelines.
Our solution: a new Starlark dialect we call “Aspect Extension Language (AXL)”. Similarly to how `.bzl` files have some Bazel-specific standard libraries available as global symbols in starlark code, `.axl` files give you extension points to hook your developer workflows!
Check out my BazelCon 2025 [10 minute lightning talk about AXL](https://www.youtube.com/watch?v=j7-IMZ2q5W4\&list=PLak8-7eFSpowmNiR2lhvJEomLA140yban\&index=21) and our [Marvin Saves the BUILD comic](https://cdn.prod.website-files.com/62fe361319fc7d5a70696095/690f4cd660ffe62f2a6e21e9_Marvin%20Saves%20the%20BUILD.pdf) (as a pdf file).
We host a collection of extensions at [https://github.com/aspect-extensions](https://github.com/aspect-extensions) and you can easily write your own. Start at [https://aspect.build/axl](https://aspect.build/axl).
## Notable BazelCon 2025 Content
Check out the [conference session recordings](https://www.youtube.com/playlist?list=PLak8-7eFSpowmNiR2lhvJEomLA140yban). Our [Bazel 102 course on Python](https://www.youtube.com/watch?v=MB6Txen7rUk\&list=PLak8-7eFSpowmNiR2lhvJEomLA140yban\&index=2) was recorded, and has lots of updated content.
We recorded Aspect Insights video podcasts with Mícheál, Yun, and Xudong from Google. These episodes will drop soon!
We hosted a Hackathon the day after the conference. It felt like a hit! Notably Juan from Verkada presented an AXL extension he wrote, which will post soon.
We want to continue getting stuff done around Bazel, so we are going to host monthly meetups in the San Francisco Bay Area starting next month at Figma. See [https://luma.com/build-meetup-sf](https://luma.com/build-meetup-sf) for events in San Francisco and [https://luma.com/98o72bge](https://luma.com/98o72bge) for events in Palo Alto and Silly Valley, aka Silicon Valley.
Next year at BazelCon 2026 we’ll host another Hackathon event like this too.
Malte gave a talk on rules\_img. We’ve supported his work and intend to make an easy transition for users of rules\_oci.
The BUILD Foundation is forming. Uber, Spotify, and Canva already committed to be founding members. The first meeting will be December 4, email [foundation@bazel.build](mailto:foundation@bazel.build) if your company would consider participating.
## Orion and gazelle-prebuilt
Jason Bedard has worked with our awesome partners at Adobe to extract the Starlark BUILD file generator to a standalone Gazelle extension called Orion. You can now include this in your own custom Gazelle binary, along with custom extensions you wrote in Go.
We also extracted the pre-compiled Gazelle out of Aspect CLI to a standalone repo: aspect-gazelle. This way your engineers can skip the slow Go source builds and we can reliably depend on C extensions like tree-sitter.
## Community Contributions
As a leader in the Bazel ecosystem and Aspect’s Developer Evangelist, I’ve been active behind the scenes.
* Suggested that Cloudflare host a mirror of the BCR, since our customers tripped on unreliable hosting from `ftp.gnu.org` and `gitlab.arm.com`. They did!
* Bazel docs are the #1 user-reported problem. For initial progress, the Bazel Central Registry (BCR) has added features to improve transparency:
* Starlark API docs are now visible in the registry for modules that publish their documentation.
* Deprecation flags now clearly mark deprecated or archived modules in the registry.
* Next step for documentation: move hosting to a place the community can edit more easily, for example by having a preview of docs PRs (tentatively `bazel.online`). I encouraged Alan Mond to pick up the torch of improving Bazel’s docsite after he wrote an excellent blog post and coordinated with Bazel team to launch it. I arranged for the Rules Authors SIG to pay a third-party doc hosting service, [https://mintlify.com](https://mintlify.com). You can see [https://preview.bazel.build](https://preview.bazel.build) for what’s coming!
* We donated our bazel-lib library to Linux Foundation. The 3.0 release completes the rename of the module, removing Aspect branding from the name.
* I had a LOT of meetings to sell the BUILD Foundation mission to large enterprises and the community, connecting their resources to the OSS projects that need it.
* In addition, Jason contributed to upstream Gazelle.
## Free Marvin Plush
Including free U.S. shipping. While supplies last. [Get yours here](https://share.hsforms.com/137ta79VISxWWSrUao8ysyQrr96h).
## See you at BazelCon 2026!
We participate in the BazelCon 2026 planning meetings. Little birds chirp that it may be in Europe. My colleague Brett Sheppard suggested Bazel, Belgium. He lived nearby in Flanders as a student. No word yet on the final location for next year’s get together.
Happy coding from the Aspect Build team at BazelCon 2025.
To learn more about our Bazel support and platform, visit [www.aspect.build](http://www.aspect.build) or [pick a time to talk with us](https://calendly.com/aspect-build/).
# .bazelrc flags you should enable
Source: https://site.aspect.build/blog/bazelrc-flags
Discover essential .bazelrc flags for optimal project setup, performance, and debugging.
> This post has been converted to a guide: [https://github.com/bazel-contrib/bazelrc-presets](https://github.com/bazel-contrib/bazelrc-presets)
I suggest you add these lines to your `.bazelrc` file as early in your project as possible. Add one at a time and let the dust settle, as they can be breaking.
See a complete `.bazelrc` file here: [https://github.com/aspect-build/bazel-examples/blob/main/bazelrc/.bazelrc](https://github.com/aspect-build/bazel-examples/blob/main/bazelrc/.bazelrc)
* `build --sandbox_default_allow_network=false`: ensure that you don't accidentally make non-hermetic actions/tests which depend on remote services. Tag an individual target with `tags=["requires-network"]` to opt-out of the enforcement.
* `common --incompatible_allow_tags_propagation`: ensure that tags applied in your BUILD file, like `tags = ["no-remote"]` get propagated to actions created by the rule. Without this option, you rely on rules authors to manually check the tags you passed and apply relevant ones to the actions they create. See [https://github.com/bazelbuild/bazel/issues/7766](https://github.com/bazelbuild/bazel/issues/7766)
* `test --incompatible_exclusive_test_sandboxed`: fix a bug where Bazel didn't enable sandboxing for tests with `tags=["exclusive"]`
* `build --incompatible_strict_action_env`: don't let environment variables like `$PATH` sneak into the build, which can cause massive cache misses when they change.
* `build --modify_execution_info=PackageTar=+no-remote`: Some actions are always IO-intensive but require little compute. It's wasteful to put the output in the remote cache, it just saturates the network and fills the cache storage causing earlier evictions. It's also not worth sending them for remote execution. For actions like `PackageTar` it's faster to just re-run the work locally every time. You'll have to look at an execution log to figure out which action mnemonics you care about.
* `build --nolegacy_external_runfiles`: improve performance of sandbox by skipping the older `my_repo/external/other_repo` symlinks. Note, some rules may fail under this flag, please file issues with the rule author.
* `startup --host_jvm_args=-DBAZEL_TRACK_SOURCE_DIRECTORIES=1`: ensure that the Bazel server notices when a directory changes, if you have a directory listed in the `srcs` of some target.
* `build --experimental_remote_merkle_tree_cache --experimental_remote_merkle_tree_cache_size=[XX]`: Improve remote cache checking speed by memorizing merkle tree calculations, and tweak the amount of memory allocated to it
* `build --remote_local_fallback`: If the grpc remote cache connection fails, it will fail the build, add this so it falls back to the local cache.
* `build --heap_dump_on_oom`: helps you debug when Bazel runs out of memory
* `build --incompatible_remote_results_ignore_disk`: If you have both `--noremote_upload_local_results` and `--disk_cache`, then this fixes a bug where Bazel doesn't write to the local disk cache as it treats as a remote cache.
* `build --incompatible_default_to_explicit_init_py`: fix the wrong default that comes from Google's internal monorepo by using `__init__.py` to delimit a Python package
* `build --noexperimental_check_output_files --noexperimental_check_external_repository_files`: Speed up *all builds* by not checking if output files have been modified. Also lets you hack around the output tree for local debugging. Note, the second one is only in Bazel 6.0 nightlies at present.
* `test --test_verbose_timeout_warnings`: Bazel's default for `test_timeout` is `medium` (5 min), but most tests should instead be `short` (1 min).
* `build --incompatible_remote_build_event_upload_respect_no_cache`: Don't upload artifacts referenced in the BEP if the action can't be cached remotely.
* `--bes_upload_mode=fully_async`: Don't make the user wait for uploads, instead allow the `bazel` command to complete and exit.
* `build --experimental_reuse_sandbox_directories`: Save time on Sandbox creation and deletion when many of the same kind of action run during the build.
As the very last line in `.bazelrc`:
* `try-import %workspace%/.bazelrc.user`: allow developers to add repo-specific overrides in their own personal `.bazelrc.user` file. Make sure this is git-ignored.
## Maybe
* `build --experimental_inprocess_symlink_creation`: allows spaces in filenames which are inputs to actions.
* `test --build_tests_only`: change the behavior of `bazel test` to not bother building targets which aren't dependencies of the tests. Matches some developer expectations.
* Need to broadcast a message to all your Bazel users? You can use `build --unconditional_warning="You can just get it to print stuff" --unconditional_warning="<0001f92f>, anything"` to get a UI like
```plaintext theme={null}
WARNING: You can just get it to print stuff
WARNING: 🤯, anything
```
## Remove
These flags are not a good idea:
* `build --workspace_status_command=...`: this should only be enabled for release builds where stamping is desirable, otherwise you spend time running `git` commands on every build. Consider `build:release --workspace_status_command=...` instead, so it's only active under `--config=release`.
# Presets for bazelrc
Source: https://site.aspect.build/blog/bazelrc-presets
Discover how Bazel presets can help configure sensible default options, improve efficiency, and avoid common bugs in new repositories.
Bazel has a TON of options - over 1500 of them! \[1] It has so many obscure options that even experts like myself are often surprised to learn about a new one.
Many of the options have the wrong default value for new repositories. This means new users re-experience some bug only to find that they just never "enabled the bugfix".
There's now an easy way for users to avoid learning about so many flags and get more sensible values by default: presets.
### What is a preset?
A preset is just a named `.bazelrc` file with a collection of flags set, and a bunch of comments explaining the behavior change and links to documentation or issues filed.
An example preset entry looks like
```python theme={null}
# Allow exclusive tests to run in the sandbox. Fixes a bug where Bazel doesn't enable sandboxing for
# tests with `tags=["exclusive"]`.
# Docs: https://bazel.build/reference/command-line-reference#flag--incompatible_exclusive_test_sandboxed
test --incompatible_exclusive_test_sandboxed
```
We group them into preset files. The flag above appears in `correctness.bazelrc` .
### Using presets
If you don't have a dependency on bazel-lib yet, add it. This module provides a ton of carefully curated starlark libraries, rules, and other utilities that are helpful across languages. Install instructions appear on each release: [https://github.com/aspect-build/bazel-lib/releases](https://github.com/aspect-build/bazel-lib/releases)
Next, copy the presets into your repository. The easiest way to do this is with our macro:
```python theme={null}
load("@aspect_bazel_lib//lib:bazelrc_presets.bzl", "write_aspect_bazelrc_presets")
write_aspect_bazelrc_presets(name = "update_aspect_bazelrc_presets")
```
This creates a test target checking that the copies are up-to-date, so as you upgrade bazel-lib, it will print a `bazel run` command to update your copy. Since the presets can't be guaranteed to work with every Bazel setup, it's important for you to code review the changes. This is why the presets get copied to your source repo.
Finally, update the `/.bazelrc` file in your repo to import the presets you use. Add an `import` statement like
```python theme={null}
import %workspace%/.aspect/bazelrc/correctness.bazelrc
```
Complete documentation and the contents of the preset files are all on [https://github.com/bazel-contrib/bazelrc-presets](https://github.com/bazel-contrib/bazelrc-presets).
### Improving the presets
The presets live in an Apache 2 licensed open-source repo: [https://github.com/aspect-build/bazel-lib/tree/main/.aspect/bazelrc](https://github.com/aspect-build/bazel-lib/tree/main/.aspect/bazelrc) where the community can suggest more improvements. Bazel is always adding flags, so we expect this to grow over time.
### Why not fix the wrong defaults?
We have been working on this! The rules authors SIG has put up bug bounties asking for community help to flip some Bazel flags: [https://github.com/bazel-contrib/SIG-rules-authors/issues?q=is%3Aissue+is%3Aopen+label%3Abounty-1000USD](https://github.com/bazel-contrib/SIG-rules-authors/issues?q=is%3Aissue+is%3Aopen+label%3Abounty-1000USD)
However, so far only one volunteer has stepped up to work on flipping one of them.
\[1] `bazel% git checkout 6.0.0; find . -name '\*.java' -type f -exec fgrep "@Option(" {} ; | wc -l ` -> 1537
# Starlark linter: Buildifier
Source: https://site.aspect.build/blog/buildifier
How to set up the Bazel Buildifier tool by Aspect Build
Bazel uses its own configuration language called Starlark: [https://starlark-lang.org](https://starlark-lang.org). It’s a Python dialect that allows parallel evaluation to make builds faster.
[Linting](https://aspect.build/docs/cli/tasks/lint) is the process of using a static code analysis tool, known as a "linter," to identify and flag potential programming errors, bugs, stylistic issues, and suspicious constructs in source code. It essentially examines code without executing it.
Of course every language needs linting and formatting. Starlark has one too! It was originally created because the Go team at Google wanted to machine-edit `BUILD` files, but didn’t want to get into code reviews with teams who liked their hand-formatting of files. Read [https://laurent.le-brun.eu/blog/the-story-of-reformatting-100k-files-at-google-in-2011](https://laurent.le-brun.eu/blog/the-story-of-reformatting-100k-files-at-google-in-2011) for more on this back-story.
Buildifier is a tool for formatting Bazel BUILD and .bzl files with a standard convention. Buildifier works on the Aspect Extension Language too! Here’s how that looks in VSCode.
## Setting it up
There are several ways to install [Buildifier](https://aspect.build/docs/cli/tasks/buildifier) for developers. After working with over 50 companies on their Bazel config, we encoded our learnings into our Starter repos: [github.com/aspect-starters](https://github.com/aspect-starters). To save you a bunch of browsing, here’s a summary of what Aspect recommends:
1. Buildifier is written in Go, but most engineers don’t want to wait to compile it when it’s a cache miss. This is why [the “official” instructions](https://github.com/bazelbuild/buildtools/tree/main/buildifier#setup-and-usage-via-bazel) look HORRIBLE. We like [https://github.com/keith/buildifier-prebuilt](https://github.com/keith/buildifier-prebuilt) as an easy way to get pre-built binaries, along with a build rule to run it. Note that you could fetch binaries directly from the [project releases](https://github.com/bazelbuild/buildtools/releases) as well.
2. Developers will want to run `buildifier` from their PATH. We recommend [https://direnv.net](https://direnv.net) to hook the shell to update PATH as you `cd` into the workspace folder, then [https://github.com/buildbuddy-io/bazel\_env.bzl](https://github.com/buildbuddy-io/bazel_env.bzl) to add tools to the PATH. [Here’s the spot in the example](https://github.com/buildbuddy-io/bazel_env.bzl/blob/85e57cfd869cbbcc412c79db050191d88eef554a/examples/BUILD.bazel#L38) that sets up Buildifier.
3. bazel\_env.bzl also provides a stable path for the tool that editors can reference. For example in VSCode, add this to your `.vscode/settings.json`:\
`"bazel.buildifierExecutable": "./bazel-out/bazel_env-opt/bin/tools/bazel_env/bin/buildifier",`\
you probably also want to enable it on save:\
`"bazel.buildifierFixOnFormat": true,`
## Buildifier formatting
We want to make developers productive! What’s not productive? Discussion of whitespace, or waiting to re-run your CI job because of a formatter nit. We can make these basically disappear.
First, setup Aspect rules\_lint, following [https://github.com/aspect-build/rules\_lint/blob/main/docs/formatting.md](https://github.com/aspect-build/rules_lint/blob/main/docs/formatting.md). You don’t need this for buildifier itself, but assuming you also want to run other formatters for other languages, it gives you a single setup that formats all files in the repo.
We setup the editor earlier to run buildifier on save, but engineers have a lot of editor choices. As a fallback, add a `pre-commit` hook so any unformatted files get fixed at `git commit` time. A couple options for this are documented in formatting.md.
Finally we do want to enforce that files in the repo are formatted. This isn’t because we hate reading code with different whitespace - it’s just to avoid the next engineer who touches the file ending up with spurious deltas in their pull request. You can run the `format.check` target from rules\_lint on your CI. Give developers a nice error message when it fails, guiding them to setup their dev environment so they don’t need to hit a red CI job in the future.
When you first format the repo, it’s good practice to list your commit hash in the `.git-blame-ignore-revs` - see [https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-filefile](https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-filefile). This way you don’t pollute the blame layer.
## Buildifier linting
Buildifier has about 100 checks that catch certain coding issues in Starlark files, though many of them are specific to Bazel’s standard library. The list is here: [https://github.com/bazelbuild/buildtools/blob/main/WARNINGS.md](https://github.com/bazelbuild/buildtools/blob/main/WARNINGS.md)
Using rules\_lint only runs linters over the dependency graph, and you probably didn’t want to have to add your `BUILD` files to a `filegroup` in the `BUILD` file. Too self-referential! So we recommend running buildifier linting as a standalone step, such as this [GitHub Actions workflow](https://github.com/aspect-build/rules_jasmine/blob/fdfda3c75ff986c82dbb486d9ed93ee9caa9ff6f/.github/workflows/buildifier.yaml). Note that this is not incremental - it runs the linter across all the code in the repository, every time it’s run.
When you first setup the linter, it will point out a big pile of issues, and this may result in a massive PR that is hard to rebase and disruptive to engineers when it lands. We recommend enabling a single check at a time, and slowly rinse-and-repeat following the “[Ratchet principle](https://qntm.org/ratchet)”.
## It’s easier on our platform!
The Aspect Workflows developer productivity platform includes buildifier as a first-class task type. This lets you skip some of the setup steps above, and get our recommendations running automatically on your continuous integration (CI) system. Check it out at [https://aspect.build/platform](https://aspect.build/platform).
Or [talk with us](https://calendly.com/aspect-build/intro) to learn more.
# Adopting Bazel's new package manager
Source: https://site.aspect.build/blog/bzlmod
Simplify Bazel package management with the new \"bzlmod\" feature for easier dependency handling and improved user experience
Bazel packages (called "modules") have historically been distributed with a long "WORKSPACE snippet", which required users to install and configure the module and *also* its dependencies. This caused a lot of headache for users, since the first declaration of some dependency wins, and so a wrong order of some transitive initialization code results in baffling errors which can be traced back (often with significant effort) to a root cause of version skew.
As a result, rules authors had to be very careful about taking dependencies. To ensure an easier user experience, rules\_nodejs has always refused to add any dependencies at all, even on common modules like bazel-skylib.
Yun and Xùdōng from the Bazel team at Google have been hard at work on fixing this, in a new feature called "bzlmod". The problem has actually had a few false starts in the past (the Bazel "Federation" was one) so it's awesome that this is nearly ready for adoption.
You can read about the design of the feature in the [Bzlmod User Guide](https://docs.bazel.build/versions/main/bzlmod.html) so I won't repeat that here. Instead we'll just dive into how you can use it!
## What it looks like
As an example, I'll use our SWC rule from [rules\_swc](https://github.com/aspect-build/rules_swc/blob/main/README.md) which runs a super-fast JavaScript/TypeScript transpiler written in Rust.
If you use this rule today, there's a complex bunch of code to copy-paste into your project's `WORKSPACE` file, as illustrated by the install documentation on a release: [https://github.com/aspect-build/rules\_swc/releases/tag/v0.2.1](https://github.com/aspect-build/rules_swc/releases/tag/v0.2.1)
Not only is that code that every user of the rule has to maintain, but all there's that headache mentioned before: this code interacts with other install code the user copied into that file. rules\_swc depends on a nodejs toolchain, a rust binding, a bunch of npm packages, bazel-skylib, and Aspect's bazel-lib of helper functions. That graph is pretty complex to leak through to the end-user!
Under bzlmod, you can depend on rules\_swc with just one line in your `MODULE.bazel` file:
```plaintext theme={null}
bazel_dep(name = "aspect_rules_swc", version = "0.2.0")
```
The [example repo](https://github.com/aspect-build/bazel-examples/tree/e2d805683afdfcc1747621e7e9fc80b4ad71b6bb/bzlmod) shows that an swc target works correctly, using [MODULE.bazel](https://github.com/aspect-build/bazel-examples/blob/e2d805683afdfcc1747621e7e9fc80b4ad71b6bb/bzlmod/MODULE.bazel) to declare the dependencies.
## Kicking the tires yourself
First, you'll need to use Bazel 5.0 or greater. As of this writing, you write this in your `.bazelversion` file:
```plaintext theme={null}
5.0.0rc3
```
> Follow [https://github.com/bazelbuild/bazel/issues/14013](https://github.com/bazelbuild/bazel/issues/14013) to find out when 5.0.0 final has shipped.
This should update your local Bazel version. If it doesn't, your install is tied to one version, which you should fix following [https://docs.bazel.build/versions/main/install-bazelisk.html](https://docs.bazel.build/versions/main/install-bazelisk.html).
Next you need to opt-in to the bzlmod feature. Add this to your `.bazelrc` file:
```plaintext theme={null}
common --experimental_enable_bzlmod
```
If you run `bazel info` now, you'll immediately get an error that you must create a `MODULE.bazel` file in the repository root, next to WORKSPACE. This file contains a syntactic subset Starlark, similar to `BUILD.bazel` files.
If you want to create your own registry, you'll probably start by forking bazelbuild/bazel-central-registry. You'll then want to use the `--registry` flag, note that the value has to be a raw github server.
For this example, I'll add Aspect's registry to `.bazelrc` with the line
```plaintext theme={null}
common --registry=https://raw.githubusercontent.com/aspect-build/bazel-central-registry/main/
```
> Note, `main` is a floating reference there. If you're pushing commits to your registry, you'll find that GitHub's CDN has too long of a Time-to-live, and your edits won't show up. Replace `main` with a commit SHA to fix this.
If you add a dependency to your `MODULE.bazel`, for example
```plaintext theme={null}
bazel_dep(name = "aspect_bazel_lib", version = "0.3.0")
```
and do a build, you'll find there's now an external repository with that name and version in the external folder in your output\_base. (Look in `$(bazel info output_base)/external`)
You can have a mix of dependencies declared in `WORKSPACE` and in `MODULE.bazel`. That's nice so you can do the migration one package at a time.
However in practice you'll probably find many modules you depend on which aren't present on the registry at all. You'll have to bug their authors to add a MODULE.bazel file in their repo and publish to the registry. You could also contribute this yourself; see the next section.
## As a Rule Author
If you write your own rules, you'll have to interact with bzlmod as a publisher too.
You'll probably start with a local clone of the registry. The bazel server process caches the fetches from the registry (even if you use a `file:///` uri, sadly). So while testing your registration, you'll have to `bazel shutdown` before every bazel command.
In your clone of `bazel-central-registry`, you can run the wizard `./tools/add_module.py`. This will prompt you for the info it needs. Delete any cruft (as of writing, it creates temp .json files in the working directory) and commit and push changes. You can push them to your own fork of the registry before making a PR to the upstream. Note that the BCR presubmit tests will only work when you open that PR.
It's common to need patches on the upstream rule, at least to start with. Create it with a command like `git diff > ../bcr/modules/my_module/0.1.0/patches/bzlmod.patch` (or `git show` if the changes are already committed). You'll have to create a subresource integrity hash of the content of that patch, with a command like
```shell theme={null}
shasum -a 256 modules/aspect_rules_js/0.3.0/patches/bzlmod.patch | awk '{print $1}' | xxd -r -p | base64`
```
Be careful, if the patch file contains edits to the project's `WORKSPACE` it will fail to apply, because at the time bzlmod applies it, the WORKSPACE file is auto-generated 2 lines of code instead of what came from your rules repo. Then register the patch in your `source.json` like other examples in BCR.
Things that will need to be patched or changed include:
* **toolchain registration**: Extensions can't call native modules (they are evaluated on a different thread for performance). bzlmod has a different syntax for this anyhow, so repository rules shouldn't need to register toolchains. See [this patch on rules\_sh](https://github.com/aspect-build/bazel-central-registry/blob/main/modules/rules_sh/0.2.0/patches/add_module_extension.patch#L68-L82) as the example I followed.
* **fixed repository names**: bzlmod maps the repositories in order to namespace them for the strict visibility feature. If your rules expect to create an external repository `@foo` and then use `@foo//some:target` in a BUILD file or as a default for an attribute, you'll find that repo doesn't exist. Or in my case I had BUILD files assuming `@foo//:foo` target would exist, and needed to make it `@foo//:pkg` so the target is predictable without knowing the repository name.
* **result of one repository rule used by another**: [https://github.com/bazelbuild/bazel/issues/14445](https://github.com/bazelbuild/bazel/issues/14445) - I had to refactor rules not to require this feature
## More resources
By default, the registry at [https://github.com/bazelbuild/bazel-central-registry](https://github.com/bazelbuild/bazel-central-registry) is used to discover Bazel modules. Other registries may be created, in particular large companies will likely run a private registry for their own modules.
Yun created a bunch of examples you can reference: [https://github.com/meteorcloudy/bzlmod-examples](https://github.com/meteorcloudy/bzlmod-examples).
Open issues with bzlmod: [https://github.com/bazelbuild/bazel/issues?q=is%3Aopen+is%3Aissue+project%3Abazelbuild%2Fbazel%2F9](https://github.com/bazelbuild/bazel/issues?q=is%3Aopen+is%3Aissue+project%3Abazelbuild%2Fbazel%2F9)
There's a listing of resources, including the BazelCon talk: [https://docs.bazel.build/versions/main/bzlmod.html#external-links](https://docs.bazel.build/versions/main/bzlmod.html#external-links)
# Aspect Workflows Case Study: Sourcegraph
Source: https://site.aspect.build/blog/case-study-sourcegraph
Learn how Sourcegraph improved CI/CD performance and reduced costs by implementing Aspect Workflows.
[Sourcegraph](https://sourcegraph.com/) is a code intelligence platform known for their AI coding assistant, Cody.
Their software is developed in an [Open-Source repository](https://github.com/sourcegraph/sourcegraph-public-snapshot) by a large team of engineers, primarily in Go, TypeScript, and Rust. They were building \~50 different docker container images, which were not optimized. Optimizing them individually was considered less attractive than using a build system that naturally produces correct images. The build for the frontend client bundle was non-incremental and rebuilt on every change. This was a big pain point and made CI very slow, and also flaky.
Aspect Build Systems is a [Bazel product partner](https://bazel.build/community/partners) and provides a monorepo developer platform called Aspect Workflows. By adopting Workflows, Sourcegraph was able to resolve these problems, making CI **2-3x faster** while **reducing cloud compute costs by 40%.**
## History
An ex-Googler at Sourcegraph wrote a small internal position paper advocating for a move to [Bazel](https://bazel.build), the open-source Build & Test tool from Google. His experience in Google’s monorepo with a giant Go and React app convinced him and the team that “it really works.”
Sourcegraph was confident in migrating Go and Rust code to build with Bazel, but the frontend code was perceived as a risk due to limited resources and the complexity of the code and the migration path. This led them to work with Aspect, the author of Bazel’s JavaScript rules.
Aspect and Sourcegraph began working together in November 2022, as Sourcegraph started their Bazel migration journey. On December 2, the setup began:
```plaintext theme={null}
commit b1a56385e5659043adc38af53cfd5a131c98b98b
Author: Jean-Hadrien Chabran
Date: Fri Dec 2 12:57:14 2022 +0100
Initial Bazel Setup (#45052)
* bazel: initial bazel workspace and rules_js setup
* bazel: generate types for schema files
* bazel: generate graphql schema file
* Fix typo
Co-authored-by: Derek Cormier
```
## Migrating to Bazel
Bazel adoption is complex. This is partially due to the inherent difficulty in migrating build systems, which are deeply integrated with the codebase. Also, Bazel is known for a steep learning curve and limited available expertise. Aspect is recognized as the community leader and provides professional services. In this case we began with hourly consulting.
The work was divided into **two phases to mitigate risk**. The first was a “Frontend Bazel POC”. We agreed on the following deliverables:
* Prove that Bazel is a viable and performant build system for Sourcegraph’s frontend code.
* Migrate a single React Frontend app to Bazel, using Webpack for bundling. This was expected to be a “tricky” case that would mitigate risk early.
* Existing Jest tests run under Bazel.
* Demonstrate use of the Aspect CLI to generate BUILD files for TypeScript sources.
* Demonstrate a CI build that is incremental, fast, and robust to network failures.
Following the success of the POC phase, we entered a **second phase**: “rules\_js Bazel migration”, scoped to include:
* Fine-grained SASS and Postcss build targets
* Complete sourcegraph/web bundle
* Mocha integration tests
* Review and optimize Sourcegraph’s golang Bazel configuration
*This was especially important because releases to non-Cloud customers were quarterly, making it impossible to expedite fixes.*
* Documentation & Handoff
While Backend engineers were accustomed to maintaining `Makefile`'s, BUILD file generation for JavaScript and TypeScript was particularly critical so that frontend engineers don’t have a new task of manually configuring Bazel as they change source files. This is integrated into Aspect’s CLI tool: [https://aspect.build/cli](https://aspect.build/cli)
## Aspect Workflows
Meanwhile, in February 2023, Aspect presented a demo of our monorepo developer platform, Aspect Workflows. The first reaction from engineers on the call: “If we showed this to all the engineers at Sourcegraph there would be a mutiny if we didn't buy it.”
Aspect provided a free trial of Workflows, giving Sourcegraph time to establish target Key Performance Indicators:
1. Simple PRs should spend under 2 minutes in build & test.
2. Median (50%ile) build\&test should be 2-3x faster.
3. The cost for CI compute should be significantly reduced.
Aspect started performing the install for Workflows in July. As part of the trial, we provided guidance to Sourcegraph with graph optimization, non-determinism fixes, and build-without-the-bytes. These all improved the codebase, reducing the workload needed for Bazel.
By November we were reporting excellent results. Even Quinn, Sourcegraph’s CEO “warmed up” to Bazel:
%\[[https://www.linkedin.com/posts/quinnslack\_im-really-warming-up-to-bazel-after-we-started-activity-7123725994049339392-lyi1?utm\_source=share\&utm\_medium=member\_desktop](https://www.linkedin.com/posts/quinnslack_im-really-warming-up-to-bazel-after-we-started-activity-7123725994049339392-lyi1?utm_source=share\&utm_medium=member_desktop)]
To calculate the return on investment, Sourcegraph ran Aspect Workflows side-by-side with the legacy Bazel build. During the period from November 27 to December 18 2023:
1. **3.7 times as many builds ran in under 2 minutes** (172 of build\&test jobs (12%) on the legacy build, compared to 649 (44%) on Aspect Workflows)
2. **Median (50%ile) build\&test was 2.4x faster** (12 minutes on legacy build, 5 minutes on Aspect Workflows)
3. Google Compute Engine **costs were reduced 40%** (December cost for legacy build $4607, Aspect Workflows $2810)
Anecdotally, engineers reported getting their fastest build on main with a 1 minute Bazel test of the whole repo, thanks to a high cache hit rate. This is the first time engineers on the team have experienced such fast builds!
## Conclusion
Aspect Workflows delivered on the promise to make builds significantly faster, while also reducing compute costs.
Workflows also enabled a couple of key features. The Continuous Delivery pipeline pushes dozens of artifacts for each green main build, but only those which were modified. Also, we augmented the Buildkite user interface with annotations showing real-time test results from Bazel’s Build Events stream, so that engineers don’t need to wait for the build to complete before learning of a problem.
As of March 2024, Sourcegraph is fully relying on Aspect Workflows to run Bazel in their CI/CD pipeline.
# CBOI: Continuous Build, Occasional Integration
Source: https://site.aspect.build/blog/cboi-continuous-build-occasional-integration
\"Continuous Build, Occasional Integration\" fails in software development and how to switch to effective Continuous Integration
Is your organization practicing CBOI? If you haven't heard this hot new industry acronym, it stands for "Continuous Build, Occasional Integration." A lot of big companies are using this technique. It's a different way of approaching Continuous Integration (CI).
By different, I mean a lot worse.
In fact, your organization should *not* practice CBOI. So why write an article about it? Because, sadly, most organizations who claim to do CI are actually doing CBOI. I'll explain why that is, and how you can stop.
## What is CI?
Let's break down the terms a bit to start. "Continuous" is just a way of saying "infinite loop" - we trigger on every change or on a regular interval, and give feedback to the development cycle, such as alerting developers that they broke an automated test. Easy, and not controversial.
"Integration" is a much more nuanced term. In most software shops, what we mean here is that we bring together the artifacts from independent engineering teams into a functioning system. A common example that I'll use in this article is a Frontend and a Backend.
In a small organization, with only a few developers, Integration isn't much of a problem. Every engineer develops on the whole stack, and runs the complete system locally. As the organization scales, however, teams break up and specialize. The full system is eventually too complex to fit in one person's head, though the Architect tries mightily. The more the org structure gets broken up, the more different software systems diverge and the harder it is to guarantee that the code they're writing works when integrated.
In order to perform Continuous Integration, then, you need an automated way to integrate the full stack. In working with a number large companies, I've rarely observed this automation. Instead, individual developers just work on their code (not surprising since they would prefer to work in isolation, reducing their cognitive load and learning curve). They aren't able to bring up other parts of the system, for a variety of reasons I'll list later. However, the engineers know (or their managers instruct them) to set up a "CI" for their code. So they take the build and test system they use locally, and put it on a server running in a loop. In our example, the backend team runs their backend tests on Jenkins.
Is that CI? There's an easy litmus test to determine that.
## How to tell if you're doing CBOI rather than CI
Let's say the backend team makes a change, that will break the frontend code. To avoid certain objections, I'll add that this change isn't something we expected to be part of the API contract between these layers: let's say we just caused the ordering of results from a query to change. At what point in your development cycle will you discover the problem?
In organizations doing CBOI, the answer is that they'll find out in production when customers discover the defect. That's because the automation couldn't run the frontend tests against the HEAD version of the backend, and since the change appeared API-compatible, no one tried to manually verify it either. When you're discovering your bugs in prod, you should start asking the hard questions in your post-mortem: why didn't our CI catch this? And in our example, the answer shocks our engineers: they didn't have CI after all.
Instead of CI, their setup was individual teams testing their code in a loop, which is a Continuous Build (CB). Then when they released to prod, the Release Engineer performed the actual integration, by putting the code from different teams together in the finished system. They only do those releases on a less-frequent cadence. That's Occasional Integration (OI).
If a developer wanted to debug the problem, they'd be forced to "code in production". With no way to reproduce the full stack, they have to push speculative changes and look at production logs to see if they've fixed it. SSH'ing into a production box to make edits is the opposite of what we want. For space, I won't go into details on this as it merits a separate article (and is maybe obvious to you).
So we've finally defined what CBOI is, and seen how it causes production outages and scary engineering practices. Ouch!
## How to stop doing CBOI
I have to start this section with a warning: it isn't going to be easy. The Continuous Build was setup because it was trivial: take the build/test tool the developers were running for their code and put it on a server in a loop. There isn't a similarly easy way to integrate the full stack. It may even require some changes to your build/test tools, or to the entry-point of your software. However if your organization has a problem with defects in production (or wants to avoid such a problem), this work is worth doing.
Also, although the example so far was a Frontend and a Backend, which are runnable applications, CI is just as important for other vertices of your dependency graph, such as shared libraries or data model schemas.
I'll break this down into a series of problems:
1. developers can't run the full stack
2. no integration test fixture exists that can detect the defect
3. resource constraints make it uneconomical to run all the tests
Along the way (spoiler alert) I'll explain how one Integration tool ([http://bazel.build](http://bazel.build)) solves the technical problems.
However we'll conclude with a final problem, the people problem:
4. the organization is averse to integrating dev processes
> People problems are always harder than software problems, as I learned from early Google luminary Bill Coughran.
## Why devs can't run the full stack
As I mentioned earlier, our ideal integration happens on the developers machine. After making that non-order-preserving backend change, you'd just run the frontend tests to discover the breakage. In practice this is much harder than it should be.
First, you might need your machine in a very particular state. You need compilers and toolchains installed, at just the right versions, statically linked against the right system headers, and running on an OS that's compatible with prod. Most teams don't have an up-to-date "onboarding" instructions that carefully covers this, and since the underlying systems are always churning, you don't even know whether your instructions will work for the next person trying to run your code.
Next, many systems require shared runtime infrastructure ("the staging environment") or credentials. These either aren't made available to engineers, or they're a contended resource where only one person can have their changes running at a time.
It's also common that knowledge of how to bring up a fresh copy of the system isn't written down anywhere, and hasn't been scripted. Only the sysadmin has the steps roughly documented in an unsaved notepad.exe buffer, so when you need to bring up a server, that person clicks around the AWS UI to do so.
To solve these problems, and unlock your developers ability to run the whole system, you need:
* A tool like Bazel that manages the toolchains and keeps the configuration roughly hermetic, so a dev can "parachute" into someone else's code and run it at HEAD without any setup to maintain.
* The ability to cheaply spin up a new environment anywhere. For example if you deploy to a Kubernetes cluster, use something like minikube to make a miniature local environment that mimics production and re-uses most of the same configs.
* Robust scripting that automates the release engineer's job. It should be possible for a test to run the same setup logic to make a fresh copy of the system under test.
The configurations need to be "democratized" for this to work well. Under Jenkins you might have had some centralized Groovy code that looks at changed directories or repositories and determines tests to run. This doesn't scale in a big org where many engineers have to edit these files. Instead, you should push configuration out to the leaves as much as possible: co-locate the description of build\&test for some code at the nearest common ancestor directory of those inputs. Bazel's `BUILD.bazel` files are a great example of how to do this.
## Integration test fixtures
Remember that tests are written in three parts, sometimes called "Arrange, Act, Assert". The first part is to bring up the "System under test" (SUT). ( [https://en.wikipedia.org/wiki/Test\\\_fixture#Software](https://en.wikipedia.org/wiki/Test\\_fixture#Software) and other links )
In order to assert that the frontend and backend work together, our automated test first needs to integrate the frontend and backend, by building both of them at HEAD and running them in a suitable environment, with the wiring performed so they can reach each other for API calls. You'll need a high-level, language-agnostic tool to orchestrate these builds, in order to build dependencies from head. Again, Bazel is great for this.
You'll find there is natural resistance here: the "first mover" cost is very high. An engineer could easily spend a week writing one test to catch the ordering defect I mentioned earlier. In the scope of that post-mortem, someone will object "we can't possibly make time for that." But of course, the fixture is reusable, and once it's written you can add more true "integration tests", even writing them at the same time you make software changes rather than as regression tests for a post-mortem.
If the code is in many repositories, that also introduces a burden. You'll either need some "meta-versioning" scheme that says what SHA of each repo to fetch when integrating, or you'll need to co-locate the code into a single monorepo (which has its own cost/benefit analysis).
## Not economical to run all the tests
The last technical problem I'll mention is test triggering. In the CBOI model, you only needed to run the backend tests when the backend changed, and the frontend tests when the frontend changed. And they were smaller tests that only required a single system in their test fixture. CI is going to require that we write tests with heavier fixtures, and run them on more changes.
Triggering across projects is tricky. Our goal is to avoid running all the tests every time, but to run the "necessary" ones. You could write some logic that says "last time we touched that backend we broke something, so those changes also trigger this other CI". This logic is likely flawed and quickly rusts, so I don't think it's a good strategy. You could automate that logic using some heuristics, like [Launchable](https://www.launchableinc.com/) does (now CloudBees Smart Tests). But to make this calculation reliably correct, ensuring that *all* affected tests are run for a given change, you need a dependency graph. Bazel is great for expressing and querying that graph, for example finding every test that transitively depends on the changed sources.
In a naive solution, it's also too slow to build everything from HEAD. You need a shared cache of intermediate build artifacts. Bazel has a great remote caching layer that can scale to a large monorepo, ensuring that you keep good incrementality.
## Organization Averse to Integrating
Lastly, I mentioned there's a non-technical problem as well. Even with clever engineers and the right tools, like Bazel, this might be what sinks your effort.
Engineers want to work in isolation from each other. For example, the backend engineers think JavaScript is a mess and don't want to learn anything about frontend code. Engineers are amazingly tribal! Try asking a Mac user to develop on Windows or vice-versa.
To do CI, we're asking that the backend engineers have to look at the frontend test results when something is red, to determine if their changes caused a regression. We're asking the frontend engineers to wait for a build of the backend to run their tests against. These teams never had to work closely together in the past.
Worse, we're also asking the managers to act differently. This is an infrastructure investment for the future, requiring some plumbing changes in the build system. So only an organization willing to make strategic decisions will be able to prioritize and consistently staff their CI project. Also, the managers from different parts of the org will have to reach some technical agreement between their teams about standardizing on build/test tooling that can span across projects. This may run into the same friction you always have when making shared technical decisions.
## Epilogue: coverage
I like to beat up on test coverage as a metric, because it weights entirely on executing lines of code, but not on making assertions. In the context of CBOI, test coverage is also misleading. You might have 100% test coverage of the frontend, and 100% test coverage of the backend, but 0% test coverage of defects seen when integrating the two. I think this contributes to the misunderstanding among engineering managers.
# CODEOWNERS and Bazel
Source: https://site.aspect.build/blog/codeowners-and-bazel
Learn how to manage code ownership in monorepos using CODEOWNERS, OWNERS files, and tools like Bazel for better code review workflows.
Gating submission of code changes on the right reviewers is a critical and nuanced problem.
Most companies are using GitHub for code review. It supports a single file named `CODEOWNERS` in the root of the repository. This clearly wasn't designed for monorepos, where you want each org to maintain its own "Ownership" semantics, and beneath that each team may want overrides. It ought to follow the example of many linters which treat any source file as governed by the nearest ancestor configuration file.
Bazel is a monorepo build and test tool, but it's closely related to code review as well, so this is the sort of problem we could expect it to solve. The naive answer is to encode ownership in Bazel's dependency graph, like with [https://github.com/zegl/rules\_codeowners](https://github.com/zegl/rules_codeowners) (disclaimer, I'm a contributor there)
However, `rules_codeowners` layering on top of the dependency graph is not ideal. It requires a parent folder to list its children, and then those are listed in a big registration block in `generate_codeowners`. You don't want to declare such a dependency graph, because it inverts the graph and causes "eager fetches" - in order to load the root package you accidentally have to load `//my_org/your_slow_team` and `//other_org/made_bad_choices` - making builds slow for everyone.
Note that Google's monorepo has a separate file, which is just a textproto called `OWNERS`. Bazel (aka. blaze) is not involved. It's similar to [https://www.kubernetes.dev/docs/guide/owners/](https://www.kubernetes.dev/docs/guide/owners/) from what I can tell.
What you really want is a "whole-repo operation" that reads the data files spread around the repository, and Bazel isn't a good choice for such operations since any given node in the dependency graph should have a limited transitive reachable scope based on the dependencies in the source code.
### Paid options
I've used a standalone service before like [https://www.pullapprove.com/](https://www.pullapprove.com/) - this gives you a great deal of expressiveness in policies around code changes. However it just integrates with GitHub as an additional status on PRs, the same as a CI system. It doesn't understand your GitHub teams or play with the built-in "Owned by" feature in the GitHub user interface [https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#about-code-owners](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#about-code-owners)
So let's say we really want `CODEOWNERS` but for it to work with monorepo.
### Okay so what could we do instead
There's a key observation: in reviewing a commit that modifies an OWNERS file, we don't need the new values to be "live" in evaluating the policies of who reviews that change. Quite the opposite: if I make a PR to remove your team from the set of required owners of some file, your team should be required to approve that change. This means we're fine with the OWNERS semantics applying only *after* the commit gets merged to the `main` branch.
This means we can treat CODEOWNERS as a continuous delivery problem. For any green commit on `main` we can aggregate OWNERS files from the whole repository into a correct CODEOWNERS file, then "deliver" that with a bot commit back into the repository whenever it changes.
# Configuring Bazel's Downloader
Source: https://site.aspect.build/blog/configuring-bazels-downloader
Configure Bazel's downloader for efficient, secure package management, improving resilience and enhancing security
Bazel has a built-in downloader that's used for many things. It has a separate cache from the repository cache, so even if some repository rule re-runs, you won't have to fetch from the internet. It's also configurable, though this is undocumented.
Some repository rules run external programs like `yarn install`, these aren't aware of Bazel's downloader and do their own network fetches. The downloader is used from WORKSPACE with rules like `http_archive` or `http_file`, and can also be used in repository rules with `repository_ctx.download[_and_extract]`.
Repository rules are a source of non-hermeticity in builds. They also present a security vulnerability when fetching untrusted third-party code. For these reasons we recommend using a local read-through cache like Artifactory for better resilience against network outages, with some security scanner to help identify known vulnerabilities in packages stored in the cache.
You can tell Bazel to redirect all your downloads through that read-through cache. We recommend doing this on CI to start with, using `--experimental_downloader_config=bazel_downloader.cfg` in `.bazelrc`.
Now you need to create that config file. Though there isn't documentation, the [source code for UrlRewriterConfig](https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriterConfig.java#L66) will get you pretty close.
Here's an example config to get you started.
```plaintext theme={null}
# This file works by going through each URL and matching it to any rewrite line, and if it matches, adding the second
# parameter as a candidate to the pool. If the candidate set is empty it uses the original. We order this file so that
# the first match should work so that we don't get any `Warnings:` in the logs.
allow s3.amazonaws.com
# For some reason the bazel team decided that mirror.bazel.build should be the source of truth for these 3 files, let
# those through
rewrite (mirror.bazel.build/bazel_coverage_output_generator/.*) artifactory.internal.net/artifactory/$1
rewrite (mirror.bazel.build/bazel_java_tools/.*) artifactory.internal.net/artifactory/$1
rewrite (mirror.bazel.build/openjdk/.*) artifactory.internal.net/artifactory/$1
# For everything else, our urls exactly match what mirror.bazel.build gives so skip the indirection
rewrite mirror.bazel.build/(.*) artifactory.internal.net/artifactory/$1
# Use any of our remote repositories
rewrite (dl.google.com)/(.*) artifactory.internal.net/artifactory/$1/$2
rewrite (files.pythonhosted.org)/(.*) artifactory.internal.net/artifactory/$1/$2
rewrite (github.com)/(.*) artifactory.internal.net/artifactory/$1/$2
rewrite (pypi.python.org)/(.*) artifactory.internal.net/artifactory/$1/$2
rewrite (raw.githubusercontent.com)/(.*) artifactory.internal.net/artifactory/$1/$2
rewrite (releases.llvm.org)/(.*) artifactory.internal.net/artifactory/$1/$2
rewrite (repo.maven.apache.org)/(.*) artifactory.internal.net/artifactory/$1/$2
rewrite (s3.amazonaws.com)/(.*) artifactory.internal.net/artifactory/$1/$2
rewrite (storage.googleapis.com)/(.*) artifactory.internal.net/artifactory/$1/$2
rewrite (www.python.org)/(.*) artifactory.internal.net/artifactory/$1/$2
rewrite (zlib.net)/(.*) artifactory.internal.net/artifactory/$1/$2
# These are identical URLs so instead of making more remote repositories we just alias the others
rewrite pypi.org/(.*) artifactory.internal.net/artifactory/pypi.python.org/$1
rewrite repo1.maven.org/(.*) artifactory.internal.net/artifactory/repo.maven.apache.org/$1
# Improved security: only allow stuff from artifactory
allow artifactory.internal.net
block *
```
# Containerizing JavaScript Applications with Bazel
Source: https://site.aspect.build/blog/containerizing-javascript-applications-with-bazel
Learn how to optimize JavaScript container builds with rules_js's JsImageLayer. Discover layer groups for better build times and deployment efficiency.
Containerizing JavaScript applications is controversial because they come in so many flavors. They could be bundled into a single file or the original layout of the source tree could be kept intact. There is not a one-size-fits-all approach to creating a container out of your JavaScript application.
With Bazel, this story is different. All [\*\_binary tar](https://bazel.build/extending/rules#executable_rules_and_test_rules)[get](https://en.wikipedia.org/wiki/Tar_/\(computing/\))[s](https://bazel.build/extending/rules#executable_rules_and_test_rules) have a well known directory structure called Runfiles which makes it insanely easy to decide what structure the Javascript container will have. You just take the Runfiles directory tree, put it into a [tar](https://en.wikipedia.org/wiki/Tar_/\(computing/\)) archive, add it to your `oci_image` and call it a day, right?
Though this is a fine approach for small applications (\<50MB), it does not scale well beyond a few gigabytes because of increased build and deploy times. You could take a nap waiting for the whole layer to be uploaded and redeployed after a single line change.
💡
If you don’t know about how the default Docker storage OverlayFS works, give
this fun page a try.
The [`JsImageLayer` rule from r](https://github.com/aspect-build/rules_js/blob/main/docs/js_image_layer.md)[ules\_js keeps y](https://github.com/aspect-build/rules_js/tree/main)ou moving. It is a packaging rule that efficiently creates JavaScript containers using the Runfiles structure.
In the early days, `JsImageLayer` created two layers for the whole container. The `node_modules` layer contained everything that changed infrequently such as npm dependencies and node interpreter (yes, `rules_js` includes a hermetic Node.js interpreter) and the app layer contained first party JavaScript code.
This worked fairly well because a single line change did not cause `node` and `node_modules` to be uploaded and redeployed. However, we realized that it was not good enough. node binary rarely changes and `node_modules` changes more frequently than `node`, so it is not economical to bundle them together as a single layer.
That’s exactly what prompted the `rules_js` maintainers (us) to add more layers, ordered from infrequently changed to frequently changed.
In version 2.0 of `rules_js`, `js_image_layer` created more layers for better build and deploy time performance. It worked well for most JavaScript containers, but there is no one-size-fits-all approach. People reached the limit of that optimization too.
$\[ \begin{array}{ccccc} \text{node} & \text{package_store_3p} & \text{package_store_1p} & \text{node_modules} & \text{app} \\ \uparrow & \uparrow & \uparrow & \uparrow & \uparrow \\ \text{interpreter} & \text{3rd party npm} & \text{1st party npm} & \text{symlinks} & \text{application code} \\ \end{array} \]$
What happens if you have 150,000 files from 3rd party npm packages? Changing one npm package led to the whole layer being rebuilt and sent over the network, causing unwelcome flashbacks to the early days of `js_image_layer`. The problem was even worse if you had npm packages that shipped with prebuilt binaries or .node bindings. In my [consulting work](https://www.aspect.build/services) with AI companies, I learned how monstrous `pip` packages can be (👀 CUDA). I knew what I had to do.
Introducing `JsImageLayer` layer groups, a new `rules_js` feature that allows fine grained control over the number of layers created. Users can create additional layers to further optimize `JsImageLayer` by supplying a dictionary of names and regex that is evaluated against the path.
An example of putting `@huge/pkg` into its own layer can be written as follows.
$\[ \begin{array}{cc} \text{layer_groups} & \text{default layers} \\ \uparrow & \uparrow \\ \text{any number of additional layers} & \text{the layers shown above} \\ \end{array} \]$
💡
JsImageLayer creates 5 default layers for an easy out-of-the-box experience even if they are empty due to preceding layer\_groups.
We also took this as an opportunity to optimize how we generate layers. Previously, `js_image_layer` had a custom Node.js program to create layers (`.tar` archives). Though it worked great for medium-size archives (\<= 200MB), [streaming backpressure](https://nodejs.org/en/learn/modules/backpressuring-in-streams) greatly reduced its efficiency. Fixing this was as easy as building the archives with good ol’ **libarchive** (also known as bsdtar).
One of our customers saw a **40%** speed improvement with no additional configuration change. With some additional layers, it became **50% faster** due to better parallelization of build actions.
[A benchmark](https://github.com/thesayyn/js_image_layer_bench) with the cold build times for a `js_image_layer` target with **no change** to the BUILD file demonstrates **52%** speed improvement for overall build time.
You can now add as many layers as you want based on size, how frequently they change, or any other criteria. You can even override the default layers by using the same keys in the dictionary.
The [Layer Groups feature](https://github.com/aspect-build/rules_js/blob/main/docs/js_image_layer.md#js_image_layer-layer_groups) is now available in `rules_js` version [v2.3.5](https://github.com/aspect-build/rules_js/releases/tag/v2.3.5)!
# rctx.download custom headers coming to Bazel 7.1
Source: https://site.aspect.build/blog/custom-download-headers
Learn how to use HTTP headers in Bazel's downloader for fetching Docker layers, Alpine packages, and more. Improve caching and avoid common HTTP issues.
Bazel is responsible for fetching files from the internet in most cases. From a `WORKSPACE` file you may have seen this as `http_archive` or `http_file`. Behind the scenes, when writing a repository rule, this uses the `repository_ctx#download` API, [https://bazel.build/rules/lib/builtins/repository\_ctx#download](https://bazel.build/rules/lib/builtins/repository_ctx#download) (or the `download_and_extract` variant).
> Note that some rulesets decided not to use the Bazel downloader. For example, rules\_go uses `go mod download` [https://pkg.go.dev/cmd/go#hdr-Download\_modules\_to\_local\_cache](https://pkg.go.dev/cmd/go#hdr-Download_modules_to_local_cache) and rules\_python uses `pip download`.
The Bazel Downloader API is great because the files are always cached in the Bazel repository\_cache. If you enable `--experimental_remote_downloader` you can even use your Remote Cache to serve as a read-through proxy of the files. The Downloader also has a rich configuration ability.
As ruleset authors, we've relied heavily on this Bazel API. But, we ran into some cases that didn't work. For example, the Docker registry is an HTTP server with lots of surprises. At least that has been our experience implementing a [repository rule](https://bazel.build/extending/repo) for rules\_oci for fetching container images from registries like DockerHub.
## Docker pull and HTTP Headers
In rules\_oci, we don't want to run `docker pull`. We want to use the Bazel downloader to get the benefits listed above.
Internally, container images are just a few tar archives called "layers" which are put together via a "Manifest" file, which is a straightforward `json` file. Like everything else in the software industry, this json file also has its [variations](https://github.com/moby/moby/tree/master/image/spec). We are mostly interested in the [v2](https://distribution.github.io/distribution/spec/manifest-v2-2/) version as it is widely used, while v1 is deprecated and not in use.
However, some people don't like to move with everybody else. So this encouraged Docker to implement a fallback behavior where if you are trying to request a manifest from a registry, and you don't have `Accept` HTTP header set with specific mime types, then the registry will happily assume that you are an old person trying to fetch an image that has the v2 manifest and gladly downgrade the manifest to v1.
Unfortunately Bazel's downloader had no way to set headers, so it was not suitable for fetching Docker layers. We had to workaround this by reverting to `curl` in our repository rule. But then the downloaded files are no longer cached, and so this is not a principled solution.
Thanks to Bazel being open-source, this was correctable! I filed the issue for this: [https://github.com/bazelbuild/bazel/issues/17829](https://github.com/bazelbuild/bazel/issues/17829). Then I worked with the Bazel team to land a fix for it. Thanks to Fabian and Tiago for reviewing the PR and getting it landed!
The feature that allows setting arbitrary HTTP headers is already on HEAD and available on the [documentation](https://bazel.build/rules/lib/builtins/repository_ctx#download) page but has not been cherry-picked into Bazel's 7.1-release branch yet. However, it can be used today by putting `2b8885ed954e58d09d47290d47882a7298c88334` into `.bazelversion` file.
## Alpine
This problem of setting headers is not unique to Docker registries. Alpine packages also have a property that also requires us to send some headers. However, this case is a little different than Docker one. Alpine uses an [archive format](https://wiki.alpinelinux.org/wiki/Apk_spec) which is simply multiple gzip streams of tars combined. It is a lot like \`.deb\` [archives](https://en.wikipedia.org/wiki/Deb_\(file_format\)).
These three segments are;
* `signature`: contains signature
* `control`: contains metadata information about the package.
* `data`: contains files such as libs and executables.
The problem with `.apk` packages is that unlike the `control` , `data` segments, the `signature` segment is not guaranteed to be stable, meaning its contents might change without notice. This is a problem in the Bazel world because in order to be reproducible everything we fetch from the internet has to be identical even after five years from now.
Luckily the signature segment is optional, some packages don't even have it, and can be omitted if needed. However, since an `.apk` is a single archive consisting of three `.tar` archives we can't just get the last two. This is where we need to use [HTTP range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests) to only fetch the last two segments of an archive so that it's always stable. In order to do that, we need to send a `Range` header to tell the HTTP server to stream only the range of bytes we specified.
## Trying it out
Here's an example of an oci\_pull rule that demonstrates how setting the `Accept` header changes the response of the registry.
```python theme={null}
def _simple_pull_impl(rctx):
url = "https://index.docker.io/v2/fluxcd/flux/manifests/1.25.4"
# Get the token: curl -fsSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:fluxcd/flux:pull"
auth_pattern = {
url: { "type": "pattern", "pattern": "Bearer ", "password": "" }
}
headers = {
"Accept": "application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json",
}
rctx.download(
url = url,
auth = auth_pattern,
# If you comment out the headers argument, the schema version will be 1.
headers = headers,
output = "manifest.json"
)
manifest = json.decode(rctx.read("manifest.json"))
print("schemaVersion: %s" % manifest["schemaVersion"])
rctx.file("BUILD.bazel", """filegroup(name = "manifest", srcs = ["manifest.json"])""")
simple_pull = repository_rule(
implementation = _simple_pull_impl,
)
```
With HTTP headers the sky is the limit. We have only talked about a few specific problems, but this feature allows Bazel users to do much more than that.
Imagine if you want to download a big archive from the Artifactory but it takes too much time. Well, you can accelerate the fetching by downloading different parts of the archive in parallel by making multiple HTTP requests simultaneously. I'll post more on that later.
# Customize Bazel with Aspect CLI plugins
Source: https://site.aspect.build/blog/customize-bazel-with-aspect-cli
Customize Bazel effortlessly with Aspect CLI plugins for tailored developer workflows and enhanced control over Bazel deployments
A consistent theme of Bazel deployments at our clients is the steep on-ramp: how difficult it is for developers to understand and for DevInfra teams to integrate. On the other hand, Bazel's popularity is partly because Googlers have experienced how well it can work, and have told their story other companies who want that experience too.
What's the missing piece? Bazel was customized for Google's developer workflow, and now you can customize it for yours!
The `aspect` CLI is a wrapper around Bazel. If you've ever installed `bazelisk` following the [Bazel recommended install](https://bazel.build/install/bazelisk) you already run a wrapper around Bazel. In fact, `aspect` includes `bazelisk` so it's an even better wrapper.
`aspect` supports plugins, which give you the missing "control point" to solve for issues in local developer workflows, such as:
* amend error messages to point to your internal documentation
* offer to apply auto-fixes to incorrect source code
* add commands for deploying, linting, rebasing, or other common developer workflows
* "cookiecutter": stamp out new Bazel projects following your local conventions
Plugins also run on Continuous Integration servers where you might want to:
* "fail fast" by reporting a red status as soon as the first build/test step fails
* feed systems that manage flakiness or trigger buildcop actions
A plugin is any program that serves our gRPC protocol. While you can write a plugin from scratch in any language, in practice there's a much faster way to get started: clone our [template repository](https://github.com/aspect-build/aspect-cli-plugin-template) and write your plugin using our Go SDK.
This article shows how to write your first plugin.
> Note: the plugin API is still in alpha, so breaking changes are likely.
## Tour of the plugin
The template repository includes a full Bazel setup and CI/CD using GitHub actions, so all you need to look at is one file: [`plugin.go`](https://github.com/aspect-build/aspect-cli-plugin-template/blob/main/plugin.go). It serves as an example of how to write your own plugin.
After the imports, you'll see the `main` one-liner. This just uses the excellent [`go-plugin` library from Hashicorp](https://github.com/hashicorp/go-plugin) to start up your plugin and connect it to the CLI.
```go theme={null}
func main() {
goplugin.Serve(config.NewConfigFor(&HelloWorldPlugin{}))
}
```
Next we declare the plugin type, `HelloWorldPlugin` so we have an instance to store state. The `Base` field lets us inherit implementations so we only need to implement the functions we care about.
After that, we implement the `CustomCommands` function, so we can declare that `aspect hello-world` is available to users. Our command appears in the output of `aspect help` and when run, it just prints "Hello World!". Your custom command can do anything you like, including spawning sub-processes to interact with your other tools.
The next function in the example is `BEPEventCallback`. This is called for each BuildEvent received from Bazel. Your editor will give type-completion here, making it pretty easy to navigate the available events in Bazel's [`build_event_stream` protocol buffer](https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto). This is all you have to do to consume the Build Event Protocol! In this example we just store some useful information, but you could also immediately call an API like telling your CI server that the build is going to go red.
Finally we implement the `PostBuildHook` which is called after `bazel build` exits. In that function we're using the supplied `PromptRunner` interface to ask the user for input, when we're running in interactive mode.
## A Full-featured Example
The [fix-visibility plugin](https://github.com/aspect-build/aspect-cli/blob/v0.3.0/plugins/fix-visibility/plugin.go) shows a very useful real-world use case. We've seen developers new to Bazel struggle with the concept of "visibility" and it's often the first error they run into requiring manual `BUILD` file edits.
Here's what it looks like in action:
This plugin uses the same functions we saw in the template. It subscribes to the Build Event Protocol to watch for Bazel analysis failures including the `is not visible from target` message, storing them for after the build finishes.
When running interactively, it prompts the user if they'd like to have the problem auto-fixed. Note that since our plugin is written in Go, and most of the tooling ecosystem around Bazel is also written in Go, it's easy to reuse [buildozer](https://github.com/bazelbuild/buildtools/blob/master/buildozer/README.md) to apply the edits to the BUILD file.
If running non-interactive, like on CI, it prints the equivalent buildozer command the user could run locally to fix their `BUILD` file.
As a result, users feel that Bazel is "easy to use", and put less support/training burden on your DevInfra team.
## What plugins can you imagine?
We can't wait to see how you extend Bazel! As more plugins are available, we'll put together a plugin gallery to help users discover ones that solve their own local workflow woes.
# Dagger and Bazel
Source: https://site.aspect.build/blog/dagger-and-bazel
Compare Dagger and Bazel: explore key differences in containerization, configuration, language support, reproducibility, and community impact for build tool
For those of you in the Bazel ecosystem, when you hear "Dagger", you probably think of [the dependency injection framework for Java, Kotlin, and Android](https://dagger.dev/). And for good reason. Dependencies are frequently part of a build process. But [there's another Dagger in the wild](https://dagger.io/), a tool and platform for ephemerally building and testing multi-language projects. Sound familiar?
In this post, I look at what (the new) Dagger is, how it works, and how it compares to Bazel.
## Dagger history and concepts
Dagger, started in 2022 by Solomon Hyke, the same creator as Docker, is part of the ecosystem of tools I call "code as infrastructure." In this model, you use your programming language of choice to define your infrastructure and test and build pipelines.
Under the hood, everything runs in containers, making Dagger pipelines portable and moveable between local runs or running on CI.
Dagger defines every task and workflow (including "infrastructure" setup) as a Dagger Function written in one of the currently available SDKs: Go, Typescript, and Python. Yes, this means you write functions to create Functions. It's a little confusing.
You can extend the core Dagger functionality with [Daggerverse modules](https://daggerverse.dev/), which include a wide variety of use cases, from using helm charts to spinning up Kubernetes servers and various package managers.
At the center of everything is the Dagger Engine, [which is open source](https://github.com/dagger/dagger) and handles maintaining the connections between functions, caching, state, telemetry, and more.
There's also the Dagger Cloud, which currently provides a browser-based interface for tracing and debugging issues with Dagger Functions. This is Dagger's monetization strategy and is free for one user.
## Example
This is the TypeScript example Daggerized application pipeline from [the Dagger documentation](https://docs.dagger.io/quickstart/daggerize). It builds two containers, runs tests, and then publishes one of the built containers.
```ts theme={null}
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class HelloDagger {
/**
* Publish the application container after building and testing it on-the-fly
*/
@func()
async publish(source: Directory): Promise {
await this.test(source)
return await this.build(source).publish(
"ttl.sh/myapp-" + Math.floor(Math.random() * 10000000),
)
}
/**
* Build the application container
*/
@func()
build(source: Directory): Container {
const build = this.buildEnv(source)
.withExec(["npm", "run", "build"])
.directory("./dist")
return dag
.container()
.from("nginx:1.25-alpine")
.withDirectory("/usr/share/nginx/html", build)
.withExposedPort(80)
}
/**
* Return the result of running unit tests
*/
@func()
async test(source: Directory): Promise {
return this.buildEnv(source)
.withExec(["npm", "run", "test:unit", "run"])
.stdout()
}
/**
* Build a ready-to-use development environment
*/
@func()
buildEnv(source: Directory): Container {
const nodeCache = dag.cacheVolume("node")
return dag
.container()
.from("node:21-slim")
.withDirectory("/src", source)
.withMountedCache("/root/.npm", nodeCache)
.withWorkdir("/src")
.withExec(["npm", "install"])
}
}
```
You chain Functions together, meaning other Functions can call them. To run this example, you call the `publish` Function, passing a local code directory:
```sh theme={null}
dagger call publish --source=.
```
The `publish` Function, in turn, calls `test`, which calls `build`, which calls `buildEnv`. You can pass variables between them, such as the code directory, and use any other aspect of the programming language you're using should you need them.
The Dagger-specific methods in each Function are fairly self-explanatory and often mirror their name and function in a Dockerfile. Again, they use chaining from the base `dag` client and any of its core types, such as a `Container`.
Once you run the pipeline and it is complete, besides anything retained for caching, the Dagger engine tears everything down. However, like with Docker, you can maintain state by mounting local volumes from the host system.
## Bazel and/or Dagger
I initially set out to write this post looking at how to use Bazel and Dagger together. But I almost immediately came across [this line in the documentation](https://docs.dagger.io/adopting#:~:text=You%20are%20happily%20using%20a%20monolithic%20toolchain%2C%20such%20as%20Gradle%2C%20Nix%20or%20Bazel%2C%20with%20no%20exception%20and%20no%20fragmentation%20within%20the%20team):
> Before going any further, you should look for reasons not to adopt Dagger. Your project may not be a good fit for Dagger if: …
>
> * You are happily using a monolithic toolchain, such as Gradle, Nix or Bazel, with no exception and no fragmentation within the team
There are also many discussions within the community on how the tools compare and contrast, often coming down to "stick with what works for you".
So, instead, what are the key differences and overlaps? In the general spirit of technology and developer tools, there are no hard and fast "best" answers. Often, it depends.
### Native verses Containers
Dagger runs tasks in containers, which makes its pipelines portable. However, not everything can run in containers. They add their own overhead, such as container runtimes.
Bazel uses native environments to run tasks which are more performant, but can have inconsistencies between platforms and require accommodations for portability.
### Configuration language
Bazel uses Starlark, which is Python-like but requires understanding some complex new concepts. It has widespread support for plugins and linters in IDEs.
Dagger uses Python, Go, or TypeScript with the relevant SDK, making it simpler to start. The world of "code as infrastructure" tools is interesting and includes something like [Pulumi](https://www.pulumi.com/). I'm unsure if it has widespread adoption as a concept or if it's something that dev and DevOps teams actually want to mix together.
### Language support
Support for most aspects of Bazel comes in the form of "rules". By default, Bazel has support for C, C++, Java, Objective-C, Proto Buffer, Python, and Shell. Through 3rd party community rules, Bazel can support many other languages natively.
While you can only write pipelines in one of the supported SDKs (or call the Dagger API directly), Dagger can build, test, and run whatever programming language you can run in containers.
### Reproducibility
One of Bazel's main features is its reliable reproducibility no matter when or where you run a build or test.
Dagger relies on the reproducibility of containers for its own guarantees. As it has this reproducibility, it can add caching, helping speed up subsequent runs.
### Community
Bazel originates in Google, but now has a large community of users, contributors, and a large ecosystem of rulesets and plugins around it to extend core functionality. Bazel is about nine years old. This is relatively new in the grand scheme of similar tools such as Nix or Gradle, but also perhaps considered "old" by some cutting-edge development teams.
Dagger is new and largely directed by one company. However, it is open source and has a healthy mix of contributors. The module ecosystem is reasonable, but it's always hard to predict how much maintainers will keep those up to date.
### DevEx
Bazel treats most things as artifacts that depend on each other, which can require some rethinking for your processes but is also similar to Nix. Bazel often needs a reasonable amount of configuration files and rulesets to start.
While it uses common language patterns and containers (which are fairly established now), Dagger also requires some rethinking, especially if you're coming from "as code" tools. I also found that whilst the practice of chaining was conceptually straightforward, I found myself getting lost in it sometimes and passing lots of variables back and forth. Perhaps the biggest blocker is if you don't use Python, Go, or TypeScript, writing pipelines is more challenging. It's still possible to use it by calling the GraphQL API, but then you lose a lot of the advantages. And while containers are fairly flexible, there are still certain tasks that you can't or don't want to run in them.
## Stick with what you know
Dagger and other "code as infrastructure" tools like it are interesting and tend to attract a lot of tech media attention. However, they are still new and often developed largely by one company funded by venture capital. This can mean the tool won't stick around, no matter how promising it is.
Echoing the statements I read in the Dagger community, but also those of any pragmatic developer tool community. There are always new shiny tools to try. They may offer benefits over what you use already, but if what you have works reliably, and you have staff who understand it, is it worth the effort? That's time you could spend on servicing and improving customer's needs.
# Diagnosing Bazel Cache Misses
Source: https://site.aspect.build/blog/diagnose-cache-misses-1
Learn how to diagnose and fix Bazel cache misses by investigating repository rule determinism and file differences between executions
Are you building with Bazel but having caching woes for a particular test target? Perhaps it's not getting as many cache hits as you'd expect, or even worse, none at all!
Fear not! In this blog series, we are going to take a look at a number of different ways that we can investigate and get to the bottom of these cache misses. We'll also walk through a few of the common patterns to help fix troublesome files.
In this post, we'll focus on looking at repository rule determinism and one of the ways we can query for file differences between executions.
#### Deterministic Repository Rules
Firstly, let's define what we mean by a deterministic repository rule.
> Given the same setup and install of a dependency, we should get the same output.
Rules such as `http_archive` and `http_file` are always going to be deterministic. They download a file, check it against a checksum and expose targets to access the resulting downloaded file.
However, for repository rules that may call out to package managers, such as `pip`, `npm` or `cargo` (to name a few), we can not guarantee that the output of the install process is consistent.
While these package managers have lock files which ensure our direct and transitive dependencies are pinned (ie, we get the same *set* of dependencies each time), what they don’t tell us is if a package itself is going to run some sort of "post install" script or process. For example, it’s quite common for Python distributions to build a native extensions from source during install, and we may end up with differing `.so` files each time. As this happens during the repository rule run, the output of this process can then become inputs to the rest of the build, causing the cache misses and unnecessary rebuilds that we are experiencing.
## Example: Python wheels compiled from source distribution
The example below shows the output of two separate `pip install` commands, where `psycopg2` was built from source.
From diffing the `.so` files here with [diffoscope](https://diffoscope.org/), it's clear that when building the wheel and other artifacts, the absolute path to a temporary directory that pip uses has made its way into the resulting artifacts. This then becomes an input file to our build graph, resulting in cache misses both locally and on CI.
As previously mentioned, there are a few methods to help find these differences. One method we can use is querying for the dependencies of the target in question, getting paths to the files, hashing them and diffing the resulting shasums over two runs of the query (seperated with a `bazel clean --expunge`). We will be using the command below to generate the shasum file. Make sure to replace `//foo` with the target that you are investigating.
```bash theme={null}
bazel query "kind('source file', deps(//foo))" --output xml |
xq '.query."source-file"[]."@location"' --raw-output |
awk -F ':' '{print $1}' |
sort |
xargs shasum -a 256 {} |
tee shas.txt
```
Some parts of this are optional, eg the use of `tee`, but it can be useful to see what is being printed in some cases.
> The `xq` tool is included with `yq`, see [https://kislyuk.github.io/yq/#xml-support](https://kislyuk.github.io/yq/#xml-support) for more info.
Now that we have our two lists of shasums for each input file, we can work on applying the necessary fixes. In some cases the solution may be simply to exclude those files from the inputs to the build graph, (perhaps in the case of leftover temporary files from invoking another build tool in a post install), other cases may require slightly more complex fixes.
We will dive into those cases further in a later blog post in this series.
# Documentation in source control
Source: https://site.aspect.build/blog/docs-in-vcs
Store developer documentation in source control for easy access, versioning, and collaboration, but consider presentation and ease of editing
Writing developer documentation is a great way to show your coworkers that you care about them. Where should you put it though?
There are a few choices:
* wiki software like Confluence from Atlassian
* Google Docs
* hackmd.io
* checked into source control
I think the last one is usually the right option. Here's why:
1. You can find the documentation using the same codesearch tool you use for code.
2. The documentation is versioned along with the code, so you can see what it looked like for last month's release.
3. You can ask for documentation updates in the code review and ensure they're added atomically along with related code changes.
4. Code reviewers can help make docs correct and readable while the content is still fresh in your mind.
5. The documentation can be executable, by adding a test next to it that extracts some content and runs it.
6. Code generator tools in the repo can produce documentation. For example, your API schema can be included in the docs along with prose.
7. All the familiar tooling for diffing, bisecting, and blaming are available.
8. Your CODEOWNERS setup is re-used to ensure docs are reviewed by the right team.
HOWEVER there are downsides to documentation-in-code, so you should be sure to mitigate these in your environment:
1. Make the **presentation pretty**. A README.md in GitHub is rendered okay, but it appears below the source code listing for the folder. Something like GitHub Pages is a pretty easy way to show it. To be fancier, you could use a static rendering like [https://gohugo.io/](https://gohugo.io/) as a post-commit hook on CI.
2. Make sure the **"pencil" icon is enabled** in your source browser. It should be as easy to suggest changes as it would be for a wiki, or else the "activation energy" is too high and engineers will tend to avoid the context switching required to make changes.
3. Update your **code review approval requirements** so that pure docs changes are not difficult to land. For example you might skip testing and allow commits with no review at all, if you want a true "wiki" experience.
Note that there is a case where docs shouldn't go in version control: when non-engineers need convenient edit access as well. It's not fair to make someone learn git and Markdown to edit non-technical documentation.
# Easier merges on lockfiles
Source: https://site.aspect.build/blog/easier-merges-on-lockfiles
Resolve lockfile merge conflicts automatically with Git's 'ours' merge driver, streamlining your workflow and avoiding manual fixes.
Lockfiles are a strange case of code which is checked into your repository, but not really editable. When you change your third-party dependencies, you'll typically be forced to update a corresponding lockfile at the same time. What happens when you rebase your changes on upstream, and someone else also updated the lockfile? Merge conflicts! Yuck!
Resolving a merge conflict in some generated file is annoying. Do you always accept your changes? Always accept the "incoming"? Some package managers know how to resolve merge conflicts on their own, if you remember this feature then your happy path is just to run the package manager again (e.g. `pnpm install`) and then the file is fixed. But not all engineers know that this works, and not all package managers know to do the resolution. (For example, Bazel's bzlmod, see [https://github.com/bazelbuild/bazel/issues/20272#issuecomment-1819889397](https://github.com/bazelbuild/bazel/issues/20272#issuecomment-1819889397))
Git already has a way to improve the situation though. The `.gitattributes` file has a bunch of helpful bits you should know about, such as `linguist-generated=true` (mark some files as generated so GitHub doesn't show diffs in their content) and `export-ignore` (omit some files when packaging up an archive of the repo). Let's look at another one: `merge=`
Git has a bunch of strategies for merging file contents, called "drivers". You can register some of these in your personal git configuration, see [https://git-scm.com/docs/merge-config](https://git-scm.com/docs/merge-config). The driver is a program that is expected to perform the merge. Again from the [docs](https://git-scm.com/docs/gitattributes#_defining_a_custom_merge_driver):
> a command to run to merge ancestor’s version (`%O`), current version (`%A`) and the other branches' version (`%B`)\
> ...\
> The merge driver is expected to leave the result of the merge in the file named with `%A` by overwriting it, and exit with zero status if it managed to merge them cleanly, or non-zero if there were conflicts.
Let's say we want to take the "current version" (whatever we have in our local source tree) and make that the merge result. It's already the `%A` file, so there's nothing to do. All we have to do is "exit with zero status" and that's what the `true` builtin does. So while the merge driver could be a complex topic, there's a trivial way to define one that always takes "our" file as the merge result:
```plaintext theme={null}
git config --global merge.ours.driver true
```
Now that you've got that in your git configuration (and every other developer on your team does as well...) you can specify that the lockfiles in your repo are meant to always use this merge driver, by adding to your `.gitattributes` file like
```plaintext theme={null}
path/to/LOCKFILE merge=ours
```
Now you shouldn't be bothered with merge conflicts like the default driver would report.
# Estimating Bazel's Adoption
Source: https://site.aspect.build/blog/estimating-bazel-adoption
We estimate 600 companies currently use Bazel, with potential growth to 3,750 at full market saturation. Learn more about our methodology.
The Bazel team has not instrumented Bazel with telemetry, so Google can't publish any numbers regarding Bazel usage. This means the industry is lacking a metric of the total market penetration, which is useful information when you're planning a career or business around the Bazel ecosystem. Let's fix that!
> Historical anecdote: At the same time the Bazel team considered adding telemetry, I was tech lead for Angular CLI and we added telemetry there. We figured out all the Google policies surrounding this kind of data collection and requirements for opt-out flow and disclosure to users. We tried to persuade the Bazel team to simply replicate what we did, and there was no technical or policy problem preventing them from doing so. Rather, the team decided that philosophically, Bazel should act like a compiler which has no reason to reach out on the network.
## Our estimate
We estimate that around 600 companies use Bazel, as of Q3 2022.
Projecting forward, we think that when Bazel is more mature and reaches full market saturation, it will be adopted by about 3750 companies.
## Methodology
We have two separate datasources for Bazel adoption. One is the publicly disclosed users, for example those listed on [https://bazel.build/community/users](https://bazel.build/community/users). The other is Bazel adopters who we've interacted with as a consulting company, which we call our "private users" list.
Our private users list captures some sample of the Total Bazel Adoption (TBA), say we know χ% of them. This means for every 100 actual Bazel adopting companies, we happen to have talked to χ of them.
Similarly, the public list has some sample of the Total Bazel Adoption. We make a critical assumption to make our analysis work: both lists are random samples of the total population, so for every 100 Bazel adopters, χ of them have announced themselves as adopters in one way or another.
```plaintext theme={null}
TBA * (χ/100) = [# private list]
TBA * (χ/100) = [# public list]
```
Critically, the χ private listed companies are partially overlapped with the χ publicly listed ones. The overlap is companies that we've talked to privately who have **also** publicly announced. If the samples are random, then the chance of a company appearing on both lists is given by simple probability. The chance of the event (company on both lists) is the product of each event happening independently.
```plaintext theme={null}
[# overlap]/TBA = [# public list]/TBA * [# private list]/TBA
```
Now we just need a bit of linear algebra to solve for our desired Total Bazel Adoption.
```plaintext theme={null}
TBA = [# public list] * [#private list] / [# overlap] = 218 * 127 / 46 = 602
```
## Projecting to saturation
We think that Bazel is currently in the "Chasm" between early adoption and early majority, a well-documented phase of a new product where some effort is required to use it. Read [https://a16z.com/2018/12/27/endless-chasm-enterprise-startups/](https://a16z.com/2018/12/27/endless-chasm-enterprise-startups/).
Typically, 2.5% of adoption is by the "Innovators" and 13.5% by the "Early Adopters." So we simply divide our TBA by this percentage to arrive at our estimate of Bazel adoption after the "Laggards" have come on-board:
```plaintext theme={null}
Saturation = TBA / 0.16 = 3750
```
# Estimating the effort to build a Bazel CI/CD
Source: https://site.aspect.build/blog/estimating-bazel-cicd
Learn the key challenges of integrating Bazel into CI/CD pipelines and the effort required.
At Aspect, we've consulted for many companies, helping several of them to run Bazel on their CI/CD infrastructure. Since we've been through this migration several times we can report on the typical obstacles we've seen, and the engineering effort that was required to overcome those.
The goal of this post is to help engineering managers who are tasked with a project like this to understand the complexity involved and estimate the effort that will be required to implement and operate Bazel on CI. Our conclusion is that a medium-sized engineering org will need 12-24 senior-engineer-months of work, with 0.75-1.0 FTE required for maintenance and operations.
## Avoid accidental discards of Bazel's analysis cache
In a large repo, Bazel spends a bunch of time on the ["Analysis Phase"](https://bazel.build/extending/concepts#evaluation-model), where all the rule implementation functions are run. The result is cached in-memory in the Bazel server, but can easily be discarded, just by running a `bazel` command with some flags that differ from the previous command.
The failure mode here is subtle - performance is degraded with a message like "options have changed, discarding analysis cache". Often this problem is introduced just because someone changed a CI script to add another command, say they did a `bazel query` somewhere to support a user request. Now the **following Bazel command** becomes slow. It's hard to detect that this has happened.
> Aspect is working on [an upstream PR](https://github.com/bazelbuild/bazel/pull/16805) in Bazel to just fail the build when the analysis cache is discarded.
To prevent this, you have to add some layer in your CI design wrapping Bazel calls, to ensure that the same flags are always passed. Note that having `.bazelrc` isn't always enough, because you might configure a flag such that it applies to only a subset of the commands it should. There are also Bazel bugs that cause analysis discards, such as `bazel coverage` always doing so.
## Persistent runners
As Kubernetes became popular, the industry as a whole moved to ephemeral CI instances running in their own pods. For most build systems, that came with the added benefit of isolating build and test from other PRs or builds, overcoming incorrectness issues with the build system.
However, Bazel is the opposite. The correctness guarantee is built-into the tool (to the extent that the build definition is hermetic). Worse, since Bazel typically manages a hermetic toolchain for each language, an ephemeral runner has all of the up-front work to download these toolchains, and set them up.
Your first reaction is to use the CI system's caching mechanism. For example an article titled [Caching dependencies to speed up workflows](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) seems like just what you want! And used correctly, you can improve the situation with Bazel somewhat, by using the `--repository_cache` flag to avoid re-downloading toolchains and library dependencies.
However on each run, Bazel still has to execute the repository rules (the "Repository Cache" is actually a "Downloader Cache" of the **inputs** to repository rules, not a cache of the resulting external repository. The Bazel team has indicated they're investigating a cache of the latter, so maybe someday this will be easier.)
It also has to re-execute a lot of work which is normally saved in-memory in the running Bazel server (a JVM process which is meant to be long-lived). For example, all the starlark code which turns rule definitions into the Action graph. There is no way to export this state from a Bazel server and then restore it into a new ephemeral instance.
You'll want your CI runner pool to be responsive to demand. You want to scale near zero during off-peak, to stay within your operation budget, but scale up during peak load to avoid developers waiting in a queue.
Fortunately, this isn't just a Bazel-specific problem. Many organizations need CI runners within their private network, and might want to keep them running, so there are some resources to get you past this step.
* Buildkite: [Launching and Running Elastic CI Stack for AWS](https://buildkite.com/docs/tutorials/elastic-ci-stack-aws)
* CircleCI: [Automatically scale self-hosted runners in AWS to meet demand](https://circleci.com/blog/autoscale-self-hosted-runners-aws/)
* GitHub Actions: [Autoscaling with self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/autoscaling-with-self-hosted-runners)
## Warm persistent runners
Going beyond the section above, we still have more obstacles even with persistent runners.
1. The builds during the ramp-up period of the day are slow since they run on a fresh machine, just as bad as an ephemeral one. The same developers are affected by this every day: whichever ones have a typically earlier schedule than their coworkers.
2. The Bazel server and output tree (`bazel-out`) are sensitive to whatever workload was performed last. Pull Requests can have widely varying base commits, and as soon as a worker has to "sync backwards" through the commit history, it's likely to run over a change that invalidates Bazel caches and outputs. Then, the next request is closer to `HEAD` and has to re-do all this work again.
You'll need to allocate a lot of time for reasoning about how to improve the 95th percentile builds that fall into these de-optimizations.
## Runner health checking
When we stop using ephemeral build runners, we invite the possibility of resource leaks. For example, a test might start up docker containers and then fail to shut them down. After this runner performs enough builds, it will may run out of file handles, disk space, memory, or other finite resources.
You'll need some way to defend against poorly behaved workloads. This could include monitoring how many builds a runner has done, or the available resources remaining, and take that runner out of the pool proactively. Or, you might add some cleanup logic to find leaked resources and close them after builds are complete.
## Choose and deploy a remote cache
There are [quite a few options](https://bazel.build/remote/caching#external-links) and it's pretty challenging to understand the trade-offs for all of these. Typical problems with your remote cache include:
* Network saturation on a single machine. AWS goes up to 25G network on the largest instance size, which isn't enough in a medium-sized company to serve hundreds of CI worker machines. You may need a cache that can horizontally scale to additional shards.
* You may need replication for better uptime. Updates to the remote cache software will otherwise cause significant delays for CI users and therefore require nighttime/weekend maintenance windows.
You might also get bogged down in a decision for remote caching when you want to select a remote execution service at the same time, even if you're not likely to need that anytime soon. As long as your build inputs are deterministic, you should expect a very high cache hit rate, so most builds are fast even with any actions being run on the CI worker machine. The correct thing to tackle first is to dive into any non-determinism in your inputs and fix them.
## Mirror files from the internet
Depending on external services degrades your uptime. For example, sometimes GitHub or other CDN providers have outages or partial outages, and your pipeline fails because it's unable to fetch these files.
You'll want to setup Bazel's downloader, using `--experimental_downloader_config` to point at some read-through mirror you maintain.
It's a good practice to lock down the ability of your users to introduce new dependencies on the internet, not only during Bazel's downloading phase, but also in build actions or tests. You should consider network-firewalling the agents that run build actions to prevent these.
## Define SLAs and monitor them
Your stakeholders want assurances that you're making CI faster and keeping it fast.
You need critical path developer metrics, like how long developers perceive they wait in the CI queue for a runner to be available, how long it takes from getting scheduled onto a runner before the first Bazel action spawns, how long it takes to be notified of a failing unit test. You want to support the health of the pipeline by monitoring the greenness ratio of `main` and watch for flakiness exceeding a tolerable threshold.
You also probably want some underlying metrics for diagnosing slowness, such as the rate of invalidations for external repository and analysis caches.
As with any monitoring project, you have to start from data collection, then make a robust pipeline to store, aggregate, and visualize, and then create alerts for your on-call devops engineer.
This might involve working with your production oncall support, if you want to share common instances of monitoring software like Prometheus and Grafana. Alternatively they might ask the DevOps team to deploy their own instances of these services, so plan ahead for both options.
## Keep the build green
You'll have to decide what "green" even means. See our earlier article, [Monorepo Shared Green](https://hashnode.com/post/cl6lb0mrb00beacnvgs46957i).
Then, you have to have policies and mechanisms in-place to repair a red master branch. This needs to be quick - product teams will howl if they're blocked from shipping, and a red master can pick up a compounding breakage or developers may already be writing code that depends on the culprit.
You'll need to identify breakages, alert a build cop, and communicate the status of the red build. Later engineers who sync into the red region may ask "why is this broken", not realizing they picked up a bad base commit, so you may want a `stable` git ref that engineers clone. When you found the culprit, you need to quickly revert, ideally not waiting on CI and code review processes. You must ensure that the breakage is escalated until its resolved, as the buildcop isn't always reliable. A policy should be in-place to defend the buildcop from angry engineers who think they should have been given time to "fix forward" their broken commit.
In some cases, the breakage needs a post-mortem. Why wasn't it caught in pre-commit testing? What follow-up actions are needed to reduce the buildcop burden and keep main green more of the time?
## Reduce cost
Everyone's budgets are being closely watched in 2023. Your Bazel rollout should come with decreased costs. This means it's not good enough to patch over cache misses by throwing more machines at it, like with Bazel remote build execution (RBE). Instead, you have to monitor determinism so that the cache hit rate stays high.
Constant tuning is required to keep the scale-up/scale-down curve closely matched with demand. Are agents running idle for too long? Could work be better "bin-packed" onto available machines?
## Remote Execution
The previous section says to avoid RBE when it's just a workaround for a poor cache hit rate. Even for changes that really do invalidate much of the cache (a Node.js or Go SDK version change for example) the engineer working on that change is willing to tolerate a slower CI roundtrip since it's expected to be a heavy migration project.
However, in larger scale orgs (maybe over 250 engineers in a highly-connected monorepo) then you may have regular product engineering work which invalidates an expensive part of the graph, AND that graph is "wide" - lots of work could run in parallel. In this scenario, you want to add more compute power to each Bazel worker, and this is what Remote Build Execution is for.
There are [SaaS offerings](https://bazel.build/community/remote-execution-services), or you might be using a remote cache system that includes RBE like BuildBarn.
## Keep delivery and deploys working
Your CI pipeline is also responsible for creating release artifacts. Engineers should not ship releases from their local machines, containing whatever state their git branch is in, as this is not reproducible.
You have to decide what artifacts to deliver for a given build. See our article on [Selective Delivery](https://hashnode.com/post/ckvtvrayr0g3c8as15n49cgze) for one approach you can take.
The Continuous Delivery has to stamp builds with version control info, so that monitoring systems can report whether the server crash-loop is correlated with a new version of the app. However, stamped artifacts are non-deterministic and ruin the cache hit rate.
Also, you want to lock down the credentials available to the test running machines, while still allowing CD to push to your artifact store.
For these security and performance reasons, you need a second pipeline to build these artifacts.
## This sounds hard. How long will it take?
Yes, it really is hard. If you'd like another opinion, take a look at Son's excellent post: [https://sluongng.hashnode.dev/bazel-in-ci-part-2-worker-setup](https://sluongng.hashnode.dev/bazel-in-ci-part-2-worker-setup) which covers these topics in more technical detail.
From what we've seen, a "medium" sized company should expect to spend 6-12 months of engineering effort for 2 full time senior build / dev-infra engineers (with Bazel & CI/CD experience), and then ongoing maintenance and tuning equivalent to 0.75-1 full time build / dev-infra engineer.
## Can I just use Remote Build Execution with a simple CI runner?
Answering this requires understanding how Bazel works.
The Bazel host machine (the CI runner) first has a lot of work to do to execute repository rules (the code it saved in the Repository Cache) and analyze & build the action graph. All that has to happen before any individual actions can be sent to remote executors to be run. On a naive setup like ephemeral workers, that can add minutes to each CI job and that time goes up the larger the repository is.
So, adding RBE to a naive setup will probably increase overall costs and not give the best-case performance of Bazel's incremental model.
## What if you can't budget that much time for this project?
No surprise, as the authors of this post we've already built all these lessons into our product, Aspect Workflows. Everything in this post can be handled for you.
Learn more, and sign up for a free trial at [https://aspect.build/workflows](https://aspect.build/workflows) .
# Fetching ML models under Bazel
Source: https://site.aspect.build/blog/fetching-ml-models-under-bazel
Learn how to use Bazel to fetch and cache NLTK data hermetically, ensuring reproducible builds and reliable Python tests without network dependencies.
This applies the same lazy-fetch technique as [Avoiding eager fetches](/blog/avoid-eager-fetches) — separating an external tool's *fetch* step from its *build* step — but this time to a Python package used for Machine Learning tasks, NLTK Data [https://www.nltk.org/data.html](https://www.nltk.org/data.html)
Like the previous article, we can read the install documentation provided by the tool we want to run as a Bazel action. When we do, we find they suggest a non-hermetic "installation" path that creates a `/usr/local/share/nltk_data` folder on your machine. Here's the error printed when the data isn't found:
```plaintext theme={null}
Resource punkt not found.
Please use the NLTK Downloader to obtain the resource:
>>> import nltk
>>> nltk.download('punkt')
For more information see: https://www.nltk.org/data.html
```
We don't want to follow this guidance. It also forces us to add `tags = ["requires-network"]` on a target, which is a smell of something wrong. The installed state is unmanaged by the build system, and won't exist on the CI machine where your tests run. You could prepare the CI machines in the same way of course, but this isn't what we want under Bazel. It makes the build non-reproducible since it depends on machine state, and not portable to other executors.
Instead, we'll use the Bazel Downloader to prepare the package. Like before, the key observation is that these "lazy fetching" patterns always have a cache or install folder layout the tool expects to read. So long as we can construct a cache folder that meets all the assumptions of the tool, we can just stitch our result folder into the tool's runtime using the affordance provided, in this case the `NLTK_DATA` environment variable.
## Fetching NLTK data
For the sake of example, let's say our NLTK usage assumes that the Punkt tokenizer is "installed". We can see that it's distributed in their GitHub repo: [https://github.com/nltk/nltk\_data/tree/gh-pages/packages/tokenizers](https://github.com/nltk/nltk_data/tree/gh-pages/packages/tokenizers)
Our first step is to fetch the data. We want to use the Bazel Downloader, for reasons described in my earlier post: [https://blog.aspect.dev/configuring-bazels-downloader](https://blog.aspect.dev/configuring-bazels-downloader). That can use the `repository_ctx.download*` functions, but this case is simple, so we can just use the `http_archive` helper from our `WORKSPACE` or `MODULE.bazel` file:
```python theme={null}
# Bazel download from https://www.nltk.org/nltk_data/ rather than follow their lame instructions
http_archive(
name = "nltk_data_punkt",
build_file_content = """exports_files(["punkt"], visibility = ["//visibility:public"])""",
sha256 = "51c3078994aeaf650bfc8e028be4fb42b4a0d177d41c012b6a983979653660ec",
# note: 'gh-pages' branch replaced by a commit hash for determinism
urls = ["https://raw.githubusercontent.com/nltk/nltk_data/1d3c34b4cfd6059986bf4bc604e5929335ab92ff/packages/tokenizers/punkt.zip"],
)
```
Now, we need to prepare a folder that mimics the cache folder structure nltk's downloader creates. It's sufficient to do a single `copy_to_directory` action. To make it look pretty in a Bazel context, we'll wrap it with a trivial Macro that adds some documentation. Let's put this content in `nltk.bzl`:
```python theme={null}
"Helpers for https://www.nltk.org/"
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
def nltk_data(name, corpora):
"""Assemble a folder following instructions at https://www.nltk.org/data.html#manual-installation
Args:
name: name of resulting target
corpora: list of packages from https://github.com/nltk/nltk_data/tree/gh-pages/packages
e.g. ["tokenizers/punkt", "sentiment/vader_lexicon"]
Note that these need to be fetched by Bazel, see /tools/bazel/fetch.bzl to add more.
"""
copy_to_directory(
name = name,
# Prohibit this data being used in production,
# which would pose a vulnerability issue and could cause outages.
testonly = True,
# convention for external repos is using the last segment of the name, e.g.
# tokenizers/punkt is in @nltk_data_punkt
srcs = ["@nltk_data_{0}//:{0}".format(each.split("/")[-1]) for each in corpora],
include_external_repositories = ["nltk_data_*"],
replace_prefixes = {each.split("/")[-1]: each for each in corpora},
)
```
## Using in a BUILD target
Now we'll edit our Python target to be able to resolve the data. This is likely a `py_binary` or `py_test` target. First, load the `nltk.bzl` file and call the macro, for example:
```python theme={null}
nltk_data(
name = "nltk_data",
corpora = ["tokenizers/punkt"],
)
```
This `:nltk_data` target looks like an nltk-installed data folder.
So in our Python target, we just need to set an environment variable pointing to it:
```python theme={null}
py_test(
...
data = [
":nltk_data",
"//path/to/tests/words",
],
env = {"NLTK_DATA": "$(rootpath :nltk_data)"},
)
```
Now the data is downloaded by Bazel and then hermetically provided to the test action at runtime with no network access.
# What's better than a genrule?
Source: https://site.aspect.build/blog/genrule-bestrule
Learn why genrule is the best rule for your Bazel build process, simplifying command execution and enhancing developer experience
Bazel is pretty confusing. One confusion I've seen a lot is, when do I need to write a custom rule? When can I use a macro? When can I use a genrule?
To make matters worse, Xooglers tend to have a biased answer to this question because of what they saw in google3. Because Google doesn't really use third-party tooling much, there was little usage of genrule to run tools. In a BazelCon talk I once said "we all know genrule is the bestrule" and the Bazel TL gave me a skeptical look.
> While the Bazel user guide and user manual preach the benefits of giving Bazel full control over your build process by rewriting all build processes using Bazel-native rulesets (as Google reportedly does internally), this is an immense amount of work.
> [https://www.stevenengelhardt.com/2020/10/21/practical-bazel-start-with-genrules/](https://www.stevenengelhardt.com/2020/10/21/practical-bazel-start-with-genrules/)
Also the API is kinda lame, and Google hires "only the smartest engineers" so it's not considered a burden to tell someone they must learn Starlark and Bazel's obscure analysis-loading-execution phase semantics.
In reality, genrule semantics are what you want.
* Point to an existing binary
* say what command line flags it requires
* list inputs, outputs, and dependencies
This should be doable entirely in a `BUILD` file by a regular developer who isn't interested in a half-day meander through build system internals.
## Yeah but genrule is not great?
Sadly, the "canonical" genrule [https://bazel.build/reference/be/general#genrule](https://bazel.build/reference/be/general#genrule) is pretty lacking, and requires that you give it a bash one-liner or script.
[`run_binary`](https://github.com/bazelbuild/bazel-skylib/blob/main/docs/run_binary_doc.md) from bazel-skylib is a little better since it supports cmd.exe on Windows and has a smaller sane API. [`run_binary`](https://github.com/aspect-build/bazel-lib/blob/main/docs/run_binary.md) from Aspect bazel-lib is even better: it can output directories, and supports custom progress messages, mnemonics (bazel's action tagging system) and execution requirements (hints about how to spawn).
The best `genrule` ought to provide a bunch more features. We built one for rules\_js users [`js_run_binary`](https://github.com/bazel-contrib/bazel-lib/blob/main/lib/private/run_binary.bzl) which can do more things:
* collect stdout or stderr as output files (great for stubborn tools that insist on putting outputs on stdio when Bazel requires files for everything)
* intercept the exit code and write to an output file (Bazel will immediately fail if any action exits non-zero, even if the tool is just being cute and returning information via exit code when successful)
* allow both output files and output directories
* `chdir` to a different folder at the start of the action. Some tools expect users to `cd` into a folder containing some config file and run there.
* throw away logspam when the action succeeds
## Generated Genrules
Okay so if you have a great "genrule" API, the next logical step is for the package manager rules to get involved. Those rules read the metadata for third-party packages, and can see which ones provide developer tool "binaries".
rules\_python, rules\_nodejs, and rules\_js all support auto-generated rules for these. For example if you just wanted to use `yamllint` in your BUILD file, you can do it like this:
[https://github.com/bazelbuild/rules\_python/blob/main/examples/pip\_parse/BUILD#L53-L56](https://github.com/bazelbuild/rules_python/blob/main/examples/pip_parse/BUILD#L53-L56)
```python theme={null}
load("@pypi//:requirements.bzl", "entry_point")
alias(
name = "yamllint",
actual = entry_point("yamllint"),
)
```
This gives you a genrule-like API for calling the tool. Even better, there's no eager fetch: loading this BUILD file won't make Bazel download the yamllint wheel from pypi. (Same for rules\_js generated bins)
`@pypi_yamllint//:rules_python_wheel_entry_point_yamllint` is what this alias points to, and that's a regular `py_binary` rule which could be used with the aspect\_bazel\_lib `run_binary` above, and you're already done.
## So, genrule, macro, or custom rule?
So far everything I showed is just genrule. But it can get unwieldy for developers to directly call the CLI of third-party tooling from the BUILD file. It's a pretty nice pattern to wrap these generated `bin` entry points with a macro, which is just a preprocessor definition Bazel will resolve early (in the loading phase). When you run `bazel query`, you're looking at BUILD files after macros expand, so they're really just a bit of syntax sugar.
> Be careful not to make a mess with macros - they are leaky abstractions. Read [https://docs.bazel.build/versions/main/skylark/bzl-style.html#macros](https://docs.bazel.build/versions/main/skylark/bzl-style.html#macros) before you write your first one.
Now users BUILD files look just the way you'd wish they did. We still didn't write custom rules. If you've gotten this far, here's my main takeaway:
## You should rarely need to write a custom rule
Here are some good reasons to write your own custom Bazel rule:
* You need to interoperate with other rules using richer information than the built-in providers like `DefaultInfo` and `OutputGroupInfo` provide.
* The underlying tool is too slow on a cold start (ahem JVM and Node applications), and everyone works around this with "watch mode", so you need to write a "Bazel persistent worker" binary and wrap that as a rule.
* You want to write a `toolchain` that is platform-aware, to manage Bazel's fetching that tool or to cross-compile for the target platform.
* The actions to run depend on what outputs a user requests. For example, you can run a quicker transpiler for TypeScript if the user only wants JavaScript outputs and not "declaration" interface files.
# GitHub Actions Dynamic Matrix
Source: https://site.aspect.build/blog/github-actions-dynamic-matrix
Guide to configuring dynamic matrices in GitHub Actions using bash scripts and JSON arrays for flexible job handling.
We needed to configure GitHub actions to run a matrix of jobs, but the values weren't static. For example:
* one job requires a secret auth token, thus it can't be run on untrusted code from pull requests (for example, a private NPM registry is used, or Bazel's Remote Build Execution)
* we might want to read a line from a config file like `.bazelversion` and use that value in a matrix dimension
GitHub actions themselves [don't document this at all](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs). Maybe they should!
The answer here is fantastic, but really long:
[https://stackoverflow.com/questions/65384420/how-to-make-a-github-action-matrix-element-conditional](https://stackoverflow.com/questions/65384420/how-to-make-a-github-action-matrix-element-conditional)
It's also a bit out-of-date since GitHub is deprecating the syntax it uses:
[https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/](https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/)
And it uses a separate JSON file which felt like overkill for my case. Here's a quicker recipe:
## Define one or more "matrix-prep" jobs
Each of these contributes the values needed by one dimension of the matrix. It just runs bash one-liners, then the results are aggregated into a JSON array. For example to make a value conditional on having some secret available:
```yaml theme={null}
jobs:
matrix-prep-config:
# Prepares the 'config' axis of the test matrix
runs-on: ubuntu-latest
env:
# Grab a secret from the GitHub environment, will be empty string if the secret isn't
# visible such as for untrusted code in a Pull Request
ENGFLOW_PRIVATE_KEY: ${{ secrets.ENGFLOW_PRIVATE_KEY }}
steps:
- id: local
run: echo "config=local" >> $GITHUB_OUTPUT
- id: rbe
run: echo "config=rbe" >> $GITHUB_OUTPUT
# Don't run RBE if there are no EngFlow creds which is the case on forks
if: ${{ env.ENGFLOW_PRIVATE_KEY != '' }}
outputs:
# Result will look like '["local", "rbe"]' if the secret was present or
# '["local"]' otherwise
configs: ${{ toJSON(steps.*.outputs.config) }}
```
or another example where we need to read a file from the repo to find the values:
```yaml theme={null}
jobs:
matrix-prep-bazelversion:
# Prepares the 'bazelversion' axis of the test matrix
runs-on: ubuntu-latest
steps:
# Need the repo checked out in order to read the file
- uses: actions/checkout@v3
- id: bazel_6
run: echo "bazelversion=$(head -n 1 .bazelversion)" >> $GITHUB_OUTPUT
- id: bazel_5
run: echo "bazelversion=5.3.2" >> $GITHUB_OUTPUT
outputs:
# Will look like '["6.0.0rc1", "5.3.2"]'
bazelversions: ${{ toJSON(steps.*.outputs.bazelversion) }}
```
## Use that JSON value in the matrix definition
We'll use `needs` to wait for the above jobs to complete and to read the values produced.
```yaml theme={null}
jobs:
[...]
test:
runs-on: ubuntu-latest
needs:
- matrix-prep-config
- matrix-prep-bazelversion
strategy:
matrix:
# Reads the value saved by "outputs" of the jobs above
config: ${{ fromJSON(needs.matrix-prep-config.outputs.configs) }}
bazelversion: ${{ fromJSON(needs.matrix-prep-bazelversion.outputs.bazelversions) }}
# Another dimension with static values
folder:
- "."
- "e2e/bzlmod"
- "e2e/copy_to_directory"
# Exclusions work like normal
exclude:
- config: rbe
bazelversion: 5.3.2
folder: e2e/bzlmod
```
Here's the full example: [https://github.com/aspect-build/bazel-lib/blob/0c8ef86684d5a3335bb5e911a51d64e5fab39f9b/.github/workflows/ci.yaml](https://github.com/aspect-build/bazel-lib/blob/0c8ef86684d5a3335bb5e911a51d64e5fab39f9b/.github/workflows/ci.yaml)
# Why would you want a hermetic C++ toolchain?
Source: https://site.aspect.build/blog/hermetic-c-toolchain
Bazel is usually thought of as a hermetic build system. But the default behaviour you get with Bazel's C/C++ built-in rules is not hermetic! Why does this matte
Bazel is usually thought of as a hermetic build system. But the default behaviour you get with Bazel's C/C++ built-in rules is not hermetic! Why does this matter? The short answer: Reproducibility and portability.
Developers want their code to compile correctly and be reproducible on CI systems and other developers' machines. C++ toolchains usually function by relying on the host system libraries to work. There are two primary components of this: compiling and linking.
* Compiling: the compiler will produce individual binaries for the language files. They often have the `.o` extension. The compiler will rely on header files it finds on the host machine (for C and C++) to collect information about the dependencies API.
* Linking: the linker will collect all the compiled files and assemble them together to produce the final executable, either the main program that can be executed standalone or a shared library with the `.so` extension. The linker will connect the final binary to the correct symbols during linking, and those symbols are again found in files on the host machine. There are two ways it can be accomplished: static or dynamic linking. They are not mutually exclusive and often are mixed during linking, hence the usage of terms like "fully static" and "mostly static." Some libraries like `glibc` and `libstdc++` have side effects when linked statically and are preferred to be linked dynamically (see below for more).
Aspect offers the [https://github.com/aspect-build/gcc-toolchain](https://github.com/aspect-build/gcc-toolchain) to configure Bazel with a hermetic C/C++ toolchain.
### Why is it bad to rely on system libraries?
Relying on system libraries during **build** is bad for reproducibility and portability. To solve this, we can use a "sysroot".
#### Why reproducibility?
There will always be a version skew on libraries between different machines. Even if it were true that "everyone is on the same OS version," there is no guarantee that the system libraries are the same. E.g. someone may have installed a slightly different GCC on the system, enough to have a new symbol added to the `libstdc++.so` file, and the linker will gladly use that new symbol. The output of the Bazel action will be different and produce, at best, a cache miss and, at worst (but rarely), a different runtime result.
When we use a sysroot with a `libstdc++.so` during build, the binary will always require the symbols linked against that `libstdc++.so` at runtime. The same is true for any other library in the sysroot used by the linker. The hermetic characteristic of the sysroot leads to a deterministic output that is reproducible between machines.
#### Why portability?
Take the example from "Why reproducibility?" and apply it here to `libc.so`. Every Linux system will have a standard libc. This is one of the most important libraries in the system. While we don't want to *link* against the system libc, it's stable enough to rely on it at runtime. To accomplish this, we rely on the `runtime search path` (or `rpath` for short). I.e. during building, we pass a `-L` flag to the linker to find the `libc.so` in the sysroot, but at runtime, the elf binary will find `libc.so` in the rpath (usually under `/usr/lib//libc.so`). Because of how glibc handles API evolution, a binary linked to the symbols of an old glibc will be compatible with a new version of glibc. The opposite is not true; linking against new symbols will throw exceptions at runtime if those symbols are not present. To solve portability, we include an old-enough version of glibc in the sysroot contained in this repository that will broaden the portability of the binaries produced.
## Side effects of static linking
### Large outputs
Every time we link a static archive `.a` to a binary, that binary will contain all symbols from the static archive, increasing the size of the final output. Even when stripping the binaries correctly, when the necessary symbols are duplicated multiple times, the outputs tend to be much larger than when dynamically linking against the shared object version of that library. This has a special impact on remote caching and remote build execution under Bazel. Unless the performance gain of statically linking surpasses the losses in build time (and costs), shared linking is preferable.
### glibc
The first feature a binary will lose when statically linking libc is the ability to load other
shared objects at runtime using `dlopen`. Since glibc uses `dlopen` extensively, it's not
recommended statically linking it. For extra context, when it comes to muslc, it supports static linking, but `dlopen` will still not be possible.
### libstdc++
The standard C++ library is widely depended upon and often will be dynamically linked by many programs in the build graph under Bazel, e.g. tools and language interpreters. When it comes to language interpreters, it's common that it will allow native extensions, and more common yet is that those native extensions are shipped as shared objects, subsequently loaded at runtime using `dlopen`.
Any shared object loaded that has been dynamically linked to libstdc++ (external pre-built
binaries), will make the static linking effort useless. For the users who understand the nuances well, static linking can be done by adding `static_libstdcxx` to the `features` attribute. See [hello\_world\_cpp/BUILD.bazel](https://github.com/aspect-build/gcc-toolchain/blob/3365b1abbfbd2f8565147f80d3d9ba309478360e/examples/hello_world_cpp/BUILD.bazel#L55).
### Other libraries
Always check if static linking is supported or advised for other libraries.
# Aspect Build Blog
Source: https://site.aspect.build/blog/index
Insights on Bazel, developer productivity, build systems, and software engineering at scale.
# Integration testing your container images with Bazel
Source: https://site.aspect.build/blog/integration-testing-oci
Explore two approaches for migrating Docker Compose tests to Bazel for better integration and test management.
Before Bazel, many teams were using Docker Compose to manage the workflow of running tests which need to do the following:
1. Build a docker/OCI image containing your packaged application
2. Launch a locally running container with that image, and maybe some others like a database
3. Make sure the applications running inside the containers have the right network and filesystem mounts and are able to locate each other
4. Execute the test runner to interact with the containers and make assertions about their behavior
5. Tear everything down so you don't leak resources on your machine, for example shutting down containers and pruning the image data from the docker daemon
When migrating such tests to Bazel, I've seen a lot of developers struggle with how to model this. There are two high-level approaches.
## 1. Bazel on the "inside"
This is the simpler approach. We'll just take those same steps above, and keep that code approximately the same. Whatever is orchestrating that, whether it's Groovy code in a Jenkins pipeline, or a docker-compose.yml file, we leave it alone. If developers are expected to manually do these steps when debugging the test locally on their dev machine, they keep doing that.
We simply replace "Build a docker/OCI image" with the equivalent `bazel run //my:image_target` command, so that Bazel will build the image and *also* load that image into the container runtime (i.e. the docker daemon) with some tag like `latest`.
Then we replace "Execute the test runner" with the equivalent `bazel test //my:tests` command, possibly using something like `--test_env=MY_SERVICE_PORT=9876` so that the test runner process is able to locate the services it is meant to interact with.
I call this "Bazel on the inside" because the legacy testing scripts still govern the top-level execution flow, and Bazel is just invoked by that flow at a couple points.
The benefit of this approach is that it reduces risk during a Bazel migration: we're changing fewer things at a time. But it has some downsides, so I generally recommend this only as an interim solution:
* It's non-hermetic. Bazel can't guarantee that the code under test is actually built from HEAD, your scripting has to take care of that. It also won't know when to invalidate the cache entry for the test. You could supply the image as an additional (unused) input to the test target to remediate this.
* Even if the `bazel test` invocation gets 100% cache hits, so none of the tests actually execute, you've already spent the time of setting up and tearing down the test fixture. This means you can never get the 5-second "no-op" CI run.
* It's less portable, if you used CI scripts to orchestrate the steps, it's hard for developers to run it identically on their machine.
## 2. Bazel on the "outside"
This is the more "idiomatic" approach, and fixes all those downsides. However it's a bigger refactoring project, and changes the mental model for engineers.
In this approach, `bazel test` is the outermost block in the diagram. The test has a normal dependency on the system-under-test to guarantee it's built from HEAD, in our case that means `data = ["//my:image_target"]`. Then during the test execution, the test runner has the responsibility of the "lifecycle" methods for setup and teardown, which means launching and stopping the containers.
This is less work than it sounds like, thanks to the excellent [testcontainers](https://testcontainers.com/) library. It's available in most languages and there is plenty of documentation on how to interact with it from your `setup` lifecycle hook. It can also automatically perform the teardown steps so you don't have to worry about your careless coworker exhausting resources on the CI machine.
## Example
I'll illustrate "Bazel on the outside" with an example in Python, though you can do this from most languages.
`BUILD.bazel`
```python theme={null}
# Follow https://github.com/bazel-contrib/rules_oci#usage to instruct Bazel how to build an image from your application.
oci_image(
name = image_target,
base = "@distroless_base",
tars = [layer_target],
)
# Package the image into a 'tar' format suitable for the 'docker load' command
oci_tarball(
name = tarball_target,
image = image_target,
repotags = ["bazel/my_app:latest"],
)
# Our integration test target gains a `data` (runtime) dependency on the tar
py_test(
name = "integration_test",
srcs = ["integration_test.py"],
data = [tarball_target],
main = "integration_test.py",
tags = [
"requires-docker",
"requires-network",
],
deps = [
"@pypi_docker//:pkg",
"@pypi_testcontainers//:pkg",
],
)
```
Now, inside our test we can access that tar file. We could use the Bazel runfiles library to resolve the location, here I just rely on the symlinks that Bazel creates relative to the test's working directory. Sadly testcontainers doesn't know how to load it into the docker daemon, so we have to do that ourselves:
`integration_test.py`
```plaintext theme={null}
import json
import docker
import requests
from testcontainers.core.container import DockerContainer
TAR_PATH = "my_wksp/path/to/my.tarball/tarball.tar"
# Match the 'repotags' we applied from the BUILD file
IMAGE_NAME = "bazel/my_app:latest"
def _load_latest_tarball():
client = docker.from_env()
with open(TAR_PATH, "rb") as f:
client.images.load(f)
```
With that bit out of the way, we can write our test case. This test is expecting that the container exposes an AWS lambda function, and we're just calling it with some dummy data:
```plaintext theme={null}
def test_thing():
_load_latest_tarball()
with DockerContainer(
IMAGE_NAME,
).with_bind_ports(
container=8080,
host=9000,
) as container:
# waits for the container to be ready
port = container.get_exposed_port(8080)
data = json.dumps({})
res = requests.post(
f"http://localhost:{port}/2015-03-31/functions/function/invocations",
data=data,
)
assert res.json() == "ok"
```
You can find a complete code listing in this PR: [https://github.com/aspect-build/bazel-examples/pull/223/files](https://github.com/aspect-build/bazel-examples/pull/223/files)
## Applications
We use this in several places. One of them is for [testing rules\_oci itself](https://github.com/bazel-contrib/rules_oci/blob/1fce184ebb51fbd7a51268007473e33b9fd3d75f/.github/workflows/ci.yaml#L122-L124). There, we are using [Testcontainers Cloud](https://testcontainers.com/cloud/) so that the test fixture container doesn't have to run on the same machine where the test process is executing, which allows our test to be declared as `size="small"`, meaning that we only need one local CPU and a small amount of RAM to be reserved, and so Bazel can schedule lots of these tests in parallel.
Another application that I'm really excited about uses [localstack](https://github.com/localstack/localstack) to provide a high-fidelity mock of AWS so that we can run tests that want to interact with Amazon services. This means we don't need to wire user's real AWS credentials into our tests (a common source of cache misses as these differ between engineers) and we don't have to endure the extra time and potential flakiness that comes from tests that need to create real cloud resources, or the non-hermeticity and test isolation failures that come from tests accessing existing cloud resources that are in an unknown state.
When I find some time, I hope to make this `aws_localstack_test` target available in our [https://github.com/aspect-build/rules\\\_aws](https://github.com/aspect-build/rules\\_aws) so it's trivial for you to adopt. If your team would benefit from such a thing, and could fund the engineering work, please reach out!
# Keeping main green in a monorepo
Source: https://site.aspect.build/blog/keeping-main-green
Explore how to prevent red main branches with merge queues, on-call policies, and better CI practices to improve development workflows and team efficiency.
I touched on Merge Queues in an earlier post: /blog/monorepo-shared-green. Today I'll expand on the requirements that lead to considering them, and the alternative design we recommend instead.
When `main` is red, it causes problems:
* Can teams release their application if tests are failing?
* Developers who rebase into the red region wonder if they broke something.
* Someone (let's call them the the "Build Cop") needs to take action to repair it. There is often no clear ownership, leading to the slack thread "hey is anyone looking at the red CI?"
* New pull requests are also red, and if merged can cause a "compound" breakage, making repair harder to reason about.
How do we prevent these problems? What policy decisions should we make, and how do we avoid causing new problems?
# Reasons why `main` goes red
In a naive repository, the `main` branch goes red when someone merges broken code. Most teams now have some policies around what may be merged, which include some CI status. This prevents the majority of broken changes from being merged, but not all:
1. Two changes which were green independently may be broken when combined, even though the `git merge` operation doesn't cause a merge conflict. This "in-flight collision" is usually rare, but in some cases when many engineers are working on closely adjacent code it can be more common.
2. "Stale Green" statuses from PRs which aren't rebased "close" to main are more likely to cause an in-flight collision. This can happen for two reasons: 2a) the PR was originally tested with an old "base" commit, so the status was stale when it was reported, or 2b) there was a delay between the status being reported and the PR merged, e.g. after vacation a developer merges a week-old PR which was green at the time.
3. Tests are non-hermetic. They depend on external factors such as what OS version the CI machines have installed, or some hosted service being available, or a package manager serving some files. Whatever commit happens to execute on CI after one of these preconditions breaks is now red, with no fault of the changes in the commit.
4. If the CI is unreliable, the policy may allow a "break the glass" to merge anyway. Developers may reason "my change is surely safe, this status was a flake" but such reasoning is often flawed.
I'll refer back to these reasons as we look at solutions.
# Solution? Merge queues
A merge queue is an extra step where the CI system is triggered at merge-time, on the presumed . This can prevent #1 and #2 above, since the PR is first re-based on "current" HEAD and then tested on top of other changes. It's called a "queue" because the simplest implementation requires each PR to wait until the prior ones are merged, so that it may be rebased and tested in isolation. Some systems fudge on this by batching up PRs, introducing a ton of complexity around determining which member of the batch caused a red status, then replaying them individually, or "smart batching" by re-ordering the queue first based on some heuristic of which changes are least likely to collide.
I think this is a bad solution for a few reasons.
* Developers now have to wait for their changes to merge. They have to "stay at their desk" in case some action is required of them, and their teammates can't pull HEAD to build on top of their work until the merge queue runs.
* It increases costs. The CI system already runs tests for each PR snapshot and then again after merge. Adding a third trigger increases load on CI by up to 50%, and in many orgs this is a substantial increase on their Cloud Compute bill.
* It doesn't address reasons #3 and #4, so you still need a second solution anyway.
# Better Solution: On-call plus policy
Here we approach the the problem with two tactics:
* other ways to avoid `main` going red
* reduce the impact of red `main`
## Avoiding red `main`
**Improve reliability**: To deal with reason #4, we don't want developers to have a "break glass". But we shouldn't just take away something they have a legitimate need for. Instead we should "post-mortem" every time the CI gave a developer the wrong status - that means mitigating the effects of flaky tests, or refactoring tests to rely less on unreliable external services they expect to connect to.
**freshness policy**: A green PR status has an expiration date. If it has been more than a couple days, or a hundred commits behind HEAD, or whatever cutoff you choose, then that status no longer satisfies the policy requirement of "green status required to merge". This policy can also be fancier, using some knowledge of the dependency graph or "we think these are dangerous folders" to adjust the policy requirements to merge.
**test the prospective merge**: an easy solution for reason #2a is to ignore whatever Base commit the developer chose (or got by accident because they forgot to `pull` before they started work), and instead test the result of rebasing their changes onto current HEAD. In cases where the rebase fails, you could choose to either force the engineer to rebase or proceed testing with their original base commit and warn. We prefer the latter because forcing developers to rebase can take them out of their "flow" by throwing merge conflict resolution into their product development.
## Reduced impact of red `main`
**`stable` ref**: Also called "Latest Known Green" (`lkg`). Whenever `main` CI has a green status, and you'd perform next steps like Continuous Delivery, also advance a "pointer" into the git history. A `ref` is a lightweight, named pointer. You could have it be an actual branch, although that's not necessary since it will never have a diverging history from `main`. Now update typical developer workflows to pull/rebase from `stable` rather than `main`. (It's also possible to rename the branches so that `main` represents latest-known-green and `unstable` or `bleeding-edge` represents whatever has merged, though this is a bigger change to our mental model of version control).
This avoids developers accidentally rebasing into a `red` region, but of course the trade-off is that they may need the "unstable" commit history if they want to pick up from work their teammate just merged.
**BuildCop as on-call**. Someone is quite literally on-call for reverting broken commits from `main`. They require the policy approval from all teams in the repo that they are permitted to "revert first, ask questions later". In regulation-compliant repositories, you also need to inform your regulators that unreviewed commits on `main` should be permitted if they simply "rewind history" to a previously-reviewed state.
This makes more sense in bigger monorepos where the burden of carrying a pager is offset by having one person perform this responsibility for a bunch of teams at once. At first, engineers are skeptical of having someone outside their own project or codebase doing this, but they are quickly relieved that `main` stays green and they don't really need to be involved.
# Aspect Workflows
If you're a Bazel user, you should know that we are building our recommended workflow into our product: [https://aspect.build/docs/aspect-workflows/overview](https://aspect.build/docs/aspect-workflows/overview)
I highly recommend taking a look at this, and comment or reach out to me for a demo if you think this can help your team stay green!
# Announcing Linting for Bazel
Source: https://site.aspect.build/blog/lint
Bazel now offers linting with Aspect, boosting developer productivity through rules_lint and the Aspect CLI for seamless code analysis
Aspect's mission is to make developers productive in large-scale polyglot repositories. We largely rely on the Bazel build system to power that productivity gain. But what happens when Bazel has a major missing feature that all developers need? There is a `bazel coverage` command for collecting Test Coverage, but no `bazel lint` command for running code analysis tools. Why?
At Google, we built a separate system for this called Tricorder which integrates with the code review tool, Critique. You can read an academic paper about it:
However, most engineers and team leads aren't looking for research; they need solutions which are ready to deploy. When Bazel was open-sourced, this ecosystem for running linters wasn't included. Aspect has put these parts back together and we're very excited to share what we've built.
The solution is in several layers, so I'll explore them one at a time.
## rules\_lint
[rules\_lint](https://github.com/aspect-build/rules_lint) is the lowest layer in Aspect's linting support is a "ruleset" which you can think of as a plugin for Bazel. It's open-sourced under an Apache 2.0 license, because we want everyone in the Bazel ecosystem to be able to use and contribute to it!
First we split out formatting as a special case. Formatters run on individual files, and the modifications they make are always safe to apply. In fact, engineers shouldn't even need to think about formatting: because they're guaranteed to be fast and only run on modified files, they can be in a pre-commit version control hook so they're run automatically by `git commit`. rules\_lint includes an aggregator rule, `format_multirun` that gives you a simple runnable command that formats any files you pass to it, regardless of language.
Remaining linters may need to operate on a whole program, and their suggested fixes may require human review. For these we use Bazel's aspect feature, which is like Aspect-oriented programming: you can apply some logic across an existing object model. In Bazel's case this means running some tools over the existing dependency graph. That's perfect for linting, since you already declared your "library targets" to Bazel. We just need to visit them.
The [1.0 release](https://github.com/aspect-build/rules_lint/releases) of rules\_lint includes a TON of tool integrations already, and more are added all the time, thanks to our design that requires a minimal layer of Bazel idioms on top of the tools themselves:
The rules\_lint layer concludes with a bare-bones developer experience for library code in all languages:
1. Run `bazel build --config=lint //...` to produce lint reports.\
(See the `build:lint` lines in [this `.bazelrc`](https://github.com/aspect-build/bazel-examples/blob/5c5cb8b1fae29d03ad802196be0e0b253bc9b4a4/.bazelrc#L11-L19) for the flags this config option expands to.)
2. Print the resulting reports, for example a simplistic one-liner using `find` looks like: `find $(bazel info bazel-bin) -name "*AspectRulesLint*report" -exec cat {} \;`
A slightly better way to view just the newly-produced reports is to make a script like [https://github.com/aspect-build/rules\_lint/blob/main/example/lint.sh](https://github.com/aspect-build/rules_lint/blob/main/example/lint.sh). However we can do much better, so keep reading.
## `aspect lint`
[Aspect CLI](https://github.com/aspect-build/aspect-cli) is a replacement for the Bazel command-line interface. It has better usability, and a plugin model. It is available with an Apache-2 license, and the behavior matches `bazel` so it's safe to simply drop into your `.bazeliskrc` file to switch over for your whole team. In fact, our Homebrew formula installs the Aspect CLI as `bazel` on your path, just like the [officially recommended](https://bazel.build/install) Bazelisk package which we modeled after.
It also lets us add the `lint` command that Bazel is missing!
`aspect lint` picks up where we left off in rules\_lint. It can read the report files similarly to the `lint.sh` wrapper I linked to - but it can **also** read the suggested fixes produced by linter tools. When `lint` is running in an interactive terminal, it will prompt the user to accept the proposed patches from the linter tool (they can be previewed before apply if desired.) Alternatively like most linter tools, you can run `lint --fix` to request the fixes be applied.
### Try it
The easiest way to try rules\_lint and the `lint` command is to run `aspect init` to get a blank Bazel repository with formatting and linting already configured for the language(s) you code in.
## Lint in Code Review
The two layers described so far give developers a local experience for linting their code and applying fixes. However most of us don't remember to run `lint` on our changes before we send them for review. In fact, it's often a waste of time, because there's no reason to polish code that's still a work-in-progress: we often refactor several times before getting to a shape that's ready to request feedback.
The **typical options for integrating linting into the code review process are terrible**:
1. Print warnings to the terminal during the build and test step on CI. The developer ignores them, especially when existing warnings are mixed with new ones. The code reviewer is unlikely to click into the CI logs to discover that warnings were printed. New warnings continue to be added to the codebase. I've been [writing about this](https://medium.com/@Jakeherringbone/warning-warnings-may-be-distracting-e6112cfb7cca) since 2016.
2. Promote all lint warnings to errors. Developers are now forced to fix any linting violation, even if it is trivial, regardless of whether their code reviewer agrees. They quickly learn how to suppress the error by adding `//ignore` lines in their code. When you want to enable a new linter check, you're also forced to `//ignore` the existing violations in the codebase, or take the risk of editing code you don't understand. Linting is now equivalent to tests, which block merge when red.
What if I told you there's a third way: the way we did it at Google. The linter should actually be re-thought, not as warnings, not as tests, but as code review comments.
> Of course, you might still promote certain lint rules to errors, such as data tainting rules for security, and our tooling respects that and blocks the developer. Our goal is to support both warnings AND errors well.
## Marvin
Marvin is adorable, and he's your Bazel buddy. For linting, he acts like a code review bot. He uses rules\_lint but requests the machine-readable outputs. Then he shows up in your code review and presents the lint warnings on the lines you've changed, and suggests the fixes offered by the linter tool. Deciding how to act (or maybe not act) on the warnings is now a human task for the author and reviewer to perform, along with other things that come up during a review.
This works for any language rules\_lint supports! Follow the links to the live PR on our examples repo where I made the code change.
[Java, using PMD](https://github.com/aspect-build/bazel-examples/pull/342):
[TypeScript, using ESLint](https://github.com/aspect-build/bazel-examples/pull/343/files#annotation_25632591416):\
This demonstrates what happens if the user configures this rule as an error in the eslint config.
[Python, using Ruff:](https://github.com/aspect-build/bazel-examples/pull/344/checks?check_run_id=29395134023)
[C++, using Clang-Tidy](https://github.com/aspect-build/bazel-examples/pull/354/files):
### Try it
The screenshots above are from our "kitchen sink" monorepo, [github.com/aspect-build/bazel-examples](https://github.com/aspect-build/bazel-examples). Try editing some code to produce linter warnings. You can interact with Marvin by sending a PR to the repository, and watch the automation run!
## Sign up today!
We offer a free trial of our Workflows solution for Bazel CI/CD which includes Marvin in the latest release.
# Many Python versions, one Bazel build
Source: https://site.aspect.build/blog/many-python-versions-one-bazel-build
Manage multiple Python versions efficiently in a Bazel build for seamless migration and execution. Achieve this with custom scripts and toolchains
During a migration from one version of the Python runtime to another, or the migration to Bazel itself, it can be useful to have more than one version of the Python interpreter in the build. With the current rule set for Python, and Bazel's built in `py_runtime_pair`, this is tricky to achieve. This post describes a recipe that allows for many Python interpreters in one build graph.
This works well if there are no edges between two targets which require different versions, for example, a library project designed to work under Python 3.6 isn't depended on by an application whose interpreter version is set to Python 3.9.
To support the multiple interpreters, we are going to intercept the executions of the default interpreter and if required, swap out the interpreter used for the `py_binary` or `py_test` with the one that's required.
To get started, we are going to generate a stub script that will perform the swapping and execution of the interpreter. For this, we'll use the `expand_template` rule from [aspect\_bazel\_lib](https://github.com/aspect-build/bazel-lib) library rule set which is loaded into our `WORKSPACE` file.
We also need a stub template, which can be found in [this gist](https://gist.github.com/mattem/57019db2e4e37e495879e734eaaa843a). It will attempt to find the requested Python interpreter that's set at the path in the environment variable `WHICH_PYTHON`. If this can't be found, it will then attempt to fallback to the default version. It's at this stage we can also apply some default flags and arguments.
In our BUILD file, we fill in the template:
```python theme={null}
load("@aspect_bazel_lib//lib:expand_make_vars.bzl", "expand_template")
expand_template(
name = "interpreter_stub",
out = "stub.py",
data = [
# Reference to the binary generally found in bin/python3
"@python38//:bin/python3",
],
is_executable = True,
substitutions = {
# Template the path to the default interpreter
"%DEFAULT_PYTHON_INTERPRETER_PATH%": "$(execpath @python38//:bin/python3)",
},
template = "//python:stub.py.tpl",
visibility = ["//visibility:public"],
)
```
Next, we'll define a `py_runtime` that uses our generated interpreter stub. We also need to add the default interpreter versions files into the `files` attribute of the `py_runtime`.
The interpreters files can either come from a [static build of Python](https://github.com/indygreg/python-build-standalone), be built from source as part of the build, or live on the system's `PATH`.
```python theme={null}
# This py_runtime defines the files needed to run our "default" version, however the interpreter may swap out and delegate to another
py_runtime(
name = "python_stub_runtime",
files = [
# Need the default python interpreter here as
# a fallback version for external py_binary targets that don't set the 'WHICH_PYTHON' env var.
"@python38//:files",
# Need the runfiles helper for when looking up the files needed for other Python versions defined
"@bazel_tools//tools/python/runfiles",
],
interpreter = "//bazel/python/interpreter:stub.py",
python_version = "PY3",
visibility = ["//visibility:public"],
)
```
Now, define a `py_runtime_pair` and the final `toolchain` that we will register.
> Note that `py_runtime_pair` was designed only for the Python 2 to 3 migration, and isn't useful for defining a pair of different Python 3 interpreters.
```python theme={null}
py_runtime_pair(
name = "py_stub_runtime_pair",
py2_runtime = None,
py3_runtime = ":python_stub_runtime",
)
# Used to register a default toolchain in /WORKSPACE.bazel
toolchain(
name = "py_stub_toolchain",
toolchain = ":py_stub_runtime_pair",
toolchain_type = "@bazel_tools//tools/python:toolchain_type",
)
```
Finally, we need to intercept all our calls to `py_binary` and `py_test`. To do this, define the macros `py_binary` and `py_test` that users will load instead. You'll have to update all the `load()` sites for rules\_python in your codebase to come from this macro.
```python theme={null}
def py_test(name, py3_version, data = [], env = {}, **kwargs):
(pyenv, runfiles) = env_and_runfiles_for_python(py3_version)
_py_test(
name = name,
data = data + runfiles,
env = dict(env, **pyenv),
**kwargs
)
```
The `env_and_runfiles_for_python` returns a tuple that contains the interpreters' runfiles and environment variables needed for the requested interpreter version.
```python theme={null}
PYTHON_VERSION_INFO = struct(
"PY37": struct(
workspace_name: "python36",
interpreter: struct(
major_version: "3",
version: "3.7.8",
)
),
....
)
# convenience export of a struct containing the version keys
PY3 = struct(**{
key: key
for key in PYTHON_VERSION_INFO.keys()
})
def env_and_runfiles_for_python(version):
info = PYTHON_VERSON_INFO.get(version)
env = {
"WHICH_PYTHON": "$(execpath @%s//:bin/python3)" % info.workspace_name,
"PYTHON_VERSION": info.interpreter.version,
}
runfiles = [
"@%s//:bin/python3" % info.workspace_name,
"@%s//:files" % info.workspace_name,
],
return (env, runfiles)
```
Now, when users use our `py_test` macro, they can set the new attribute `py3_version` to set the version of the Python interpreter that they need for the given target.
More interpreter versions can be added to the struct `PY3` struct, and it can be used to hold other information too, for example base image labels needed for `container_image`.
For users who wish to run their tests under different interpreter version from the default, their `py_test` load and usage now looks like:
```python theme={null}
# load from the bzl file containing our macro
load("//bazel:defaults.bzl", "py_test")
# we can also load in the PY3 symbol from where we defined our versions
load("//bazel:python.bzl", "PY3")
# run the test under the 3.7 interpreter
py_test(
name = "lib_test",
srcs = [
...
],
deps = [
...
],
py3_version = PY3.PY37,
)
```
### Fetching external dependencies
When fetching external Python dependencies for a project, we must ensure that `pip_install` rule is called with the right interpreter version, as we may end up with the wrong dependencies. For this, we are going to set `python_interpreter_target` using the data we stored on the version struct above.
```python theme={null}
info = PYTHON_VERSION_INFO.PY37
pip_install(
name = ...,
python_interpreter_target = "@%s//:bin/python%s" % (info.workspace_name, info.interpreter.major_version),
)
```
Also, ensure that if the dependencies are locked via something like [`compile_pip_requirements`](https://github.com/bazelbuild/rules_python/blob/main/docs/pip.md#compile_pip_requirements), that the interpreter used to lock the dependencies is the correct version for the project, as again this can result in different versions of external dependencies.
# Monorepo Shared Green
Source: https://site.aspect.build/blog/monorepo-shared-green
Learn about the benefits of implementing a \"shared green\" monobuild model in a monorepo for improved code sharing and consistency
The journey to monorepo shouldn't stop when all the code is in a single Version Control repository. After all, much of the touted benefit of monorepos is the increased code sharing and consistency between projects. If each team continues to use the term "repo" for their top-level folder in the monorepo, and works in isolation from other folders in the monorepo, then there was no benefit of moving them in! Instead, we want to continue the "mono" effort, bringing the concept to more of the developer workflow.
Aspect writes a lot about Bazel, which is the "monobuild" for your monorepo, allowing for code sharing between projects. In this post, I want to cover "mono-CI/CD". That is, how many continuous integration pipelines do you have in a monorepo, and how many continuous delivery mechanisms. I'll advocate for the "shared green" model.
First some concepts are needed:
## Buildcopping
Each red/green status in the repo needs to be kept green. Discovering a breakage (green->red) and repairing it quickly is the job of a "Build Cop".
Some teams are not so great at being build cops. Usually the responsibility is unclear ("hey, anyone looking at why master is red?") or is assigned to someone who has more pressing product development work and who isn't really proficient at reading logs and reasoning about what broke. They often don't have authority to revert bad commits, instead asking the commit author to resolve it. And too often, authors are attached to what they landed and spend precious time repairing the problem by rolling-forward (adding new commits) rather than reverting.
In a large organization, it's more economical to have a small rotation of people who are well-trained and have an obvious runbook for keeping the pipeline green, than for each team to do this themselves.
It's very risky to deploy a service when tests are failing. In some cases, it's even a violation of regulatory compliance. So, when it's time to release, any brokenness on CI is finally a critical issue to product owners. Thus, a red repo halts deployments and can cause real cost to the business.
## The Hard Way: each project has their own pipeline and status
To gate commits, we need to decide which pipelines to run a change against. We could use the dependency graph to determine which targets are potentially affected by a change, but this is incorrect and/or slow.
* tinder/bazel-diff is [very incorrect](https://github.com/Tinder/bazel-diff/issues/134)
* target-determinator is [very slow](https://github.com/bazel-contrib/target-determinator/issues/3)
* the SkyFrame system Google uses for this is occasionally incorrect, as Ulf recently reminded me
It's even harder to do CD. Can we release our service? Which tests should be green? Do you release if a library you depend on has a failing test? How do you communicate to engineers why the CD system didn't produce an output for their commit? There are no satisfying solutions on the market today.
## The Easy Way: shared green
Shared green simply says, there is one build\&test pipeline for the monorepo. This "monostatus" applies across all libraries, applications, and services in the repository. If anything is red, nothing can release.
It's easy for engineers to reason about and requires no code - all CI systems have a way to gate the delivery step on the success of the testing step.
## Making shared green scale
The fundamental requirement of a shared green is that it has to be almost always green. Red regions block teams from releasing, and the more they are blocked, the more severe the pushback from those teams. Worse, if your time-to-repair is longer than the interval between breakages landing, they will compound and be much harder to reason about and resolve. Also, another team may have landed a change that depends on the commit that needs to be reverted, so that the oncall now has to revert more than one commit.
How can we stay green more of the time to avoid these shared-green failure modes?
* Reduce the time between a bad commit landing and the breakage being reported. Perhaps introduce a "Failing" status in CI, where the build and test is still running, but is known that it will go red later.
* Reduce the time it takes the oncall to respond. Make sure the paging system works well, and escalate to secondary. Avoid "false positive" pages where oncall is paged for flakes, as this makes them less likely to respond to a real breakage.
* Reduce the time it takes the oncall to repair the build. Point directly to the commit and give instructions for how to revert that commit, or build a button into your UI that reverts immediately.
* Reduce the time it takes for the fix commit to be reported as green. This is simply a matter of keeping the master pipeline fast.
* Give the oncall authority. No one may question a revert that's performed to keep the CI green.
* Post-mortem each breakage. In-flight semantic collisions occur when two PRs are green individually but red when combined - if this becomes frequent, you may need a Merge Queue to re-test green PRs when landing (especially those which are further behind HEAD)
* Allow a "break the glass" in CD for teams who want to release despite red. Audit when this happens and work to reduce the frequency this is needed.
## Can it really work at my scale?
This is a tough question. Here are data-points that I know:
* Google circa 2015 had [2 billion SLOC](https://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext) and 50k engineers. There was no snapshot of the `google3` monorepo where all the starlark code could even successfully parse, let alone be analyzed, built and tested. No chance for shared green.
* A large Aspect customer has 2 million SLOC and 500 engineers. They are still on shared green, but without the "break the glass" for CD. On-call is sometimes hard, and there are stretches of redness on master which prevents deployment. More investment in on-call responsiveness as suggested above could provide some relief for better experience and more growth.
* All other Aspect customers have a shared green.
This suggests that if your company is 2-3 orders of magnitude smaller than 2015 Google (100-1000 times smaller in SLOC \* number of engineers) then you may be able to keep a shared green.
## Merge Queues
If you encounter in-flight collisions a lot, then you may need to run tests a third time. In addition to testing the developer's snapshot and the result of the merge, you can also run the tests right before merging. This uses more resources of course, and also slows down the developer, because you need to test linearly, one commit (or batch of commits) at a time.
A "Merge Queue" is a separate developer workflow system that manages this. GitHub offers one: [Managing a merge queue](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue). There's also a great research paper from Uber describing their fancy Merge Queue design: ["Keeping Master Green at Scale"](https://dl.acm.org/doi/pdf/10.1145/3302424.3303970).
However, if your rate of in-flight collisions is rare, then we recommend you just allow the build cop to revert a colliding commit. The build cop has to monitor for other failures on main, and the cost for that person to revert the occasional commit a few times a month is typically less than a Merge Queue.
# Moving TypeScript code into a Bazel monorepo
Source: https://site.aspect.build/blog/moving-typescript-to-bazel-monorepo
Step-by-step guide for migrating TypeScript projects into a Bazel monorepo, ensuring smooth workflow and effective governance.
> note, this article is still a work-in-progress as of September 2022
Aspect's rules\_js and rules\_ts finally make Bazel work well with TypeScript projects. If you're a Dev Infra team who's ready to do a migration, here's a practical step-by-step guide.
# 1. Planning
Sorry, I know you want to get right into the code. However, in a larger organization there are some things you may trip on later.
1. Research your options. If you're a Bazel enthusiast, chances are that some of your co-workers are not, so you'll want to be prepared to justify your choices. [https://monorepo.tools](https://monorepo.tools) is one nice resource for comparing across the frontend ecosystem.
2. Bazel introduces risk. It's not the most popular build tool for TypeScript. You might want to budget for OSS support in case you need help, which we offer: [https://www.aspect.dev/services#support](https://www.aspect.dev/services#support)
3. Like [https://rushjs.io](https://rushjs.io), rules\_js uses pnpm under the hood. rules\_js does support dynamically importing npm or yarn lockfiles, however it adds complexity, a performance hit, and some long-tail bugs both in [how it's implemented](https://github.com/aspect-build/rules_js/commit/d2a277c4b70ddba1880cc8267aa6e637841c9b1f) and how developers might get different results outside of Bazel. Will you change to pnpm as a "pre-factoring" before Bazel? Will you re-train your developers to run `pnpm` rather than `npm` or `yarn`?
4. Skim all the documentation, so that you at least have a sense of where to search when you have questions. I won't try to repeat it here.
5. Is it time yet? You might want to do a small-scale Proof-of-Concept and dry-run the migration with a friendly team first, to see what's missing. For example, Aspect is working on auto-configuration of BUILD files (as a [JS extension for Gazelle](https://github.com/bazelbuild/bazel-gazelle)) so if your team strongly dislikes editing these by hand, you might need to wait.
6. Who will own the monorepo? If you never thought about the term "governance" as applied to source code, you really need to do so now. There will be decisions about code consistency, policies like rollbacks, how to keep it green, etc. If you have no Dev Infra team, you may need to do some political work first to staff a couple of positions so that this is actually someone's job. Once your monorepo becomes a "tragedy of the commons", it's very hard to repair.
# 2. Setup TS monorepo
Your DevInfra team or "governance group" should be closely involved in decision-making.
Follow the READMEs from rules\_ts, or look in Bazel examples. Here are some things to look out for:
* Probably use [workspaces](https://pnpm.io/workspaces) to make a dependency graph among the first-party npm packages that will make up your monorepo. You'll want developers to stay comfortable with their existing configurations, which are likely in `package.json` files.
* Dependencies should always stay local. If some TS code in package `foo` depends on `pkg-a`, then that dependency should stay in `foo/package.json`. (Note that Google has a "single version policy" and you can too - rules\_js removes the performance penalty that rules\_nodejs had when all dependencies appear in a root /package.json. However it shifts the burden for upgrades to one engineer applying to the whole monorepo. That's a benefit to the organization but a cost to that developer and her project manager who just wants to ship features, so it's a political "hot potato".)
* The root BUILD file will have the `npm_link_all_packages()` call to create the `node_modules` tree under `bazel-out`. Nested npm packages should make exactly the same call to create their nested `node_modules` trees.
* Setup code formatting with prettier. To make it consistent for everyone and to work across languages, we suggest using [https://github.com/aspect-build/bazel-super-formatter](https://github.com/aspect-build/bazel-super-formatter)
# 3. "Slurp" commits to the monorepo
You want to minimize disruption as you move code from many repositories into the monorepo.
First, make sure the monorepo is a good developer experience. You don't want to create detractors who decide they hate Bazel and will never support your migration.
To actually move the code, you should be careful to preserve git history. Let's say you have a repo named `other-repo`, and we want to bring all of its commits into the monorepo under a folder called `other`. First install [git-filter-repo](https://github.com/newren/git-filter-repo), then run it like so:
```sh theme={null}
$ cd other-repo
# Make sure you are carrying the current history of the default branch, don't lose anything!
other-repo$ git fetch; git reset --hard origin/main
# Rewrite the history as if it always had been authored under the other/ folder
other-repo$ git filter-repo --to-subdirectory=other
# make sure you're at HEAD of monorepo as well
$ cd ../mono-repo
$ git fetch; git reset --hard origin/main
# Make the other-repo history visible to the monorepo,
mono-repo$ git remote add other ../other-repo
mono-repo$ git fetch other
# Make a "merge" commit that has one parent at monorepo HEAD and another at other-repo rewritten HEAD
mono-repo$ git merge other/main --allow-unrelated-histories
```
At this point you can test as usual and send the merge commit as a PR. Note that you might have to change the monorepo branch protection to allow merge commits, if you normally permit only linear history. At this point, you CANNOT REBASE. If you need to make changes, just do these steps again.
As soon as you land this merge commit to the monorepo, you're in danger of the code diverging. You should use whatever means you have to avoid new commits landing in other-repo, as you'll need another recipe to rewrite-and-sync those commits and they might not merge cleanly. As quickly as possible, get the owning team to verify their CI and release workflows, and archive the other-repo. It's easier to un-archive it again later if needed than to deal with consequences from it being a "fork" of the code.
# 4. Train your developers that code sharing is now a thing
In many-repo workflows, it's a pain to share code. As an application developer, you have to bump the version of your dependencies, deal with conflicts between libraries, and many other things. As a library owner, you have users of old versions who post bugs, but don't want to move to latest.
Monorepo isn't just putting the code together, it should also mean you move to trunk-based development. Dependencies are "at HEAD" and library changes take effect in applications immediately. This sounds scary, but that's what "Continuous Integration" means. You may need to update projects as you bring them into the repo so that they stop fetching your first-party library code from Artifactory or npm, and instead reference the local copy in the monorepo.
In particular, make sure that the Bazel dependency graph makes sense, and that application developers can easily re-use packages across the repository. Be prepared to govern! For example, use Bazel's [package visibility](https://bazel.build/concepts/visibility) to enforce SLAs - a casually-developed (or abandonware) library shouldn't permit dependencies from a business-critical applications.
# 5. Setup CI/CD workflows
Bazel is supposed to make your Build and Test fast. However it has setup steps (Repository rule execution and Analysis phase over the target graph) and these always run before any cache hits can be looked up. Therefore a cold Bazel worker may be slower than your legacy build.
We suggest looking at our new [Aspect Workflows](https://www.aspect.build/workflows) product and read our other blog posts about making Bazel fast on CI.
# The best tool for the Bazel job might be older than you
Source: https://site.aspect.build/blog/mtree
Learn how to use mtree specification to reuse older code. Sometimes best tool may be an older one, promoting stability and reliability for your projects.
A lot of engineering is tribal: you know a few languages, use the technology that they enable, and follow the ideas and opinions of that community. Bazel is a cross-language build tool, and that means as Bazel experts, we end up cross-pollinating a lot of ideas between tribes. We don’t even make fun of COBOL or Fortran!
Some tribes are dismissive of old technology. However, a lot of old projects are not abandoned: they are stable, and in many cases are the bedrock on which a whole stack of tools have been developed. So in this article I’ll take you back to the turn of the decade - and not the most recent one. I suggest you listen to 🎶 “*Poison*” by Bell Biv DeVoe while you read this article — here’s the music video [https://www.youtube.com/watch?v=hgnhVcyLy1I](https://www.youtube.com/watch?v=hgnhVcyLy1I). That’s because *Poison* came out in March 1990, a couple months before 4.3BSD-Reno, which introduced a tool called `mtree(5)`.
From the archive:
[https://man.archlinux.org/man/mtree.5.en](https://man.archlinux.org/man/mtree.5.en)
> The `mtree` format is a textual format that describes a collection of filesystem objects. Such files are typically used to create or verify directory hierarchies.
Well, that sounds useful to use in a build system like Bazel. We love textual formats for the ease of manipulation, and Bazel is obsessed with small files produced in one place being inputs to various tools.
## The mtree format
Here’s a simple mtree file:
```bash theme={null}
usr/bin uid=0 gid=0 mode=0755 time=1672560000 type=dir
usr/bin/bazelisk uid=0 gid=0 mode=0755 time=1672560000 type=file contents=/path/to/download
usr/bin/bazel uid=0 gid=0 mode=0755 time=1672560000 type=link link=bazelisk
```
The format includes everything we need for reproducible builds, especially that `time` attribute. We can describe any arbitrary filesystem structure along with permissions, symlinks, and a bunch more.
Now, following the Unix philosophy, Bazel enables you to compose tools or random bash one-liners with ease. So let’s say we need `/usr/bin/bazel` to be owned by the right user since we’ll stick this filesystem into a Docker container image. It can be as simple as `sed`:
```python theme={null}
genrule(
name = "change_owner",
cmd = "sed 's/uid=0/uid=1000/;s/gid=0/gid=500/' <$< >$@",
...
)
```
## Where mtree’s are used
Since the `mtree` format is 35 years old, it can run for President of the United States! And with age comes wisdom — it also has a lot of useful utilities, for example [https://github.com/vbatts/go-mtree](https://github.com/vbatts/go-mtree) is a Go utility to interact with manifests. The most useful one, however is `tar` - at least BSD tar (the GNU tar is not very reproducible and doesn’t have an intermediate representation of the filesystem being archived)
We wrote a Bazel rule, [https://registry.bazel.build/modules/tar.bzl](https://registry.bazel.build/modules/tar.bzl), which is a simple starlark wrapper around a hermetic BSD tar toolchain and the mtree format. This rule doesn’t have to be smart, because it just takes an mtree to describe how the inputs should be laid out into the archive.
I gave that `sed` example earlier - it turns out another Very Old tool is a great way to make more general edits to mtrees - `awk`. We built this into `tar.bzl` to provide various mutations of the filesystem layout:
[https://github.com/bazel-contrib/tar.bzl/blob/main/tar/private/modify\_mtree.awk](https://github.com/bazel-contrib/tar.bzl/blob/main/tar/private/modify_mtree.awk)
And wrapping that with a minimal `mutate` API lets us replace most of `rules_pkg` in a simple way:
[https://github.com/bazel-contrib/tar.bzl/blob/main/examples/migrate-rules\_pkg/BUILD](https://github.com/bazel-contrib/tar.bzl/blob/main/examples/migrate-rules_pkg/BUILD)
## The philosophy
This post isn’t just about `mtree`. It turns out many problems were solved long ago in compilers, optimization, packaging, verification, and so on. It’s easy to be tribal and only look for solutions in the ecosystem you’re familiar with, like GitHub repos with a bunch of social followers and recent commits. Often the best tool for the job is on a website that looks like the Wayback Machine, and will serve you a lot better than The Latest Rewrite in Rust.
Shoutout to [http://github.com/thesayyn](http://github.com/thesayyn) for finding all the clever ideas I’ve described in this post!
## What’s next
Meet the Aspect Build team at [BazelCon 2025](https://events.linuxfoundation.org/bazelcon/).
# Multiple external dependency closures in Bazel
Source: https://site.aspect.build/blog/multiple-deps
Discover strategies for managing dependencies in Bazel monorepos, including single and multiple version approaches, with key trade-offs.
I frequently see organizations moving to a monorepo, where applications or services depend on different versions of third-party libraries, and face a decision. Should they align these versions, following a "single version policy". Should they allow every application to manage its own separate list of dependencies? Or is there some approach in the middle? What are the tradeoffs between these solutions, and how does Bazel affect the decision?
Here's someone asking this recently, which prompted me to finally post about this:
[https://www.reddit.com/r/bazel/comments/115tqh0/why\_there\_is\_no\_multiple\_version\_managing\_of/](https://www.reddit.com/r/bazel/comments/115tqh0/why_there_is_no_multiple_version_managing_of/)
I'll give some quick answers here. If you'd like to get detailed answers for your codebase, you can book directly on my calendar: [https://calendly.com/alexeagle](https://calendly.com/alexeagle)
## Dependencies
First of all, I cannot recommend this article enough, by my college CS teaching assistant and co-author of the Go language: [https://research.swtch.com/deps](https://research.swtch.com/deps)
The short takeaway: taking a dependency on an external library seems like a convenient and obvious shortcut for developers ("never try to write your own datetime parsing code") but in practice the hidden, deferred costs mean it's often the wrong choice.
For the rest of this article, let's assume that the external dependencies are legitimately needed and need to be fetched and made available at build-time and/or run-time for the application.
## Language details
At a high level, all languages look the same:
1. The developer expresses the dependencies they take and the version constraint, which could be "any version" or "at least this version" or "a version that is semver-compatible like 2.*.*" or sometimes "exactly the following version". That last one is incorrect because it pretends to "pin" the dependency for reproducibility, but you have to pin transitive dependencies too, which leads to:
2. You run a "constraint solver" to determine a complete "transitive closure" of dependencies, which satisfies all the developer's constraints as well as those of the external libraries. You could do this on-the-fly, but for reproducibility you should write the result as a "lockfile" in the source tree, ideally including integrity hashes of those files to defend against supply-chain attacks.
3. The dependency lockfile is provided to Bazel. In some cases it's translated to Starlark, so that Bazel's downloader fetches the packages. This allows Bazel's [downloader configuration](https://blog.aspect.dev/configuring-bazels-downloader) to handle things like providing a read-through proxy, and also lets the Bazel repository cache hold onto these. Some rulesets just rely on the package manager tool to do the downloads instead.
4. The Bazel rules expose each direct dependency as a "label" so you can include it in the `deps` of the code that imports from that dependency, bringing the external libraries into your dependency graph. That graph spans both first-party and third-party libraries, which is where the trouble is going to start.
I'll write about the problem in general, but first I'll translate these for each language I've studied closely.
### Python
There are many ways to express your dependencies because the ecosystem has lots of competing standards. Bazel's [rules\_python](https://github.com/bazelbuild/rules_python) prefers the `requirements.txt` format for expressing the dependencies and their constraints, and expects a lockfile which is also in that format. It provides a rule to run `pip-compile` from [https://pypi.org/project/pip-tools/](https://pypi.org/project/pip-tools/) to run the constraint solver and a test that verifies the lockfile is up-to-date. Aspect's [rules\_py](https://github.com/aspect-build/rules_py) depends on rules\_python to do this.
When the third-party dependency is a "source distribution" for the platform/architecture you install on, then `pip install` is run as a repository rule in Bazel. This is pretty terrible and ought to be avoided, but the recipe for doing so today is "use binary wheels only" which in practice means you have to supply those yourself. There's good work going on here in the community so better answers may be coming.
Finally, the `pip_parse` repository rule (or module extension in bzlmod) converts the locked requirements to BUILD files. It uses the `pip install` tool to do the downloads, not the Bazel downloader. Only the packages needed for the requested build outputs are installed, so this is incremental.
You can have multiple `pip_parse` calls with different names like `my_pypi_deps` and `your_pypi_deps` within a single Bazel workspace, so it's trivially possible to have multiple dependency "transitive closures".
### JavaScript
Everyone has standardized on `package.json` to express your dependencies. Each package manager has its own lockfile format. Aspect's [rules\_js](https://github.com/aspect-build/rules_js) supports the [pnpm-lock.yaml](https://pnpm.io/git#lockfiles) file directly, and also allows on-the-fly import of npm or yarn lockfiles.
rules\_js uses Bazel's downloader to fetch these packages.
You can have multiple `npm_translate_lock` calls with different names like `my_npm_deps` and `your_npm_deps` within a single Bazel workspace, so it's trivially possible to have multiple dependency "transitive closures".
Node.js is unique among language runtimes in that it supports multiple versions of the same library in a single application. The resolution spec of `require` walks up the `node_modules` tree *starting from the callsite of `require`* and takes the first result, so that code in two different locations can get different results.
### Go
Go introduced a ["module" system](https://go.dev/blog/using-go-modules) in version 1.11, which is the dependency manager used under Bazel. This expects a `go.mod` file in the root of a Go module. A `go.sum` file provides the lockfile, however unlike other rulesets, [rules\_go](https://github.com/bazelbuild/rules_go) doesn't read `go.sum` when installing dependencies. Instead, typical usage runs the `update-deps` command from \[Gazelle] which independently solves the version constraints and writes the result as `go_repository` calls in Starlark, typically into a macro living in `go.bzl`, forming a triple (`go.{mod,sum,bzl}`).
Go has different semantic versioning than other languages: a major (v2.0) will have a different module name (my.com/module/v2) so it's easier to have a single version policy: if different applications use different major versions of the same library, those can live side-by-side since they're seen as two different modules.
Gazelle update-repos does allow multiple transitive closures to be installed.
### OCI (Open Container Initiative)
[rules\_oci](https://github.com/bazel-contrib/rules_oci) is an alternative to rules\_docker. OCI (and docker) already use a content-address based scheme using digests to refer to remote images, so a lockfile isn't required. rules\_oci will warn you if you use a tag like `latest` to refer to your dependencies. It uses Bazel's downloader to fetch manifests and layers for your base image.
This ruleset is still pre-1.0 so I won't go into much detail yet, as things are subject to change.
## Many-versions policy
Here we just model the many-repo world, where each application has its own set of dependencies. As one case study, a large finance company I worked with has about 80 different requirements.txt files and transitive dependency closures.
* *Skew* is the downside of this approach. A given external dependency at different versions will likely be reachable following multiple dependency paths, and it's hard to predict which version you end up with. rules\_python, for example, constructs a `sys.path` in the runtime stub with dependencies in an arbitrary order, and the interpreter will end up with whichever one happens to be first. This can easily violate dependency constraints - you use library X\@1 which needs Y>=2, but you have Y\@1 picked up first, probably making library X misbehave or crash. In practice, it will often work out okay, but when it doesn't, you'll spend a long time figuring out why.
* *Management* is harder. You'll have many dependency files and lockfiles, many calls to the Bazel repository rule to translate them to starlark, and many external `@path_to_myapp_deps` repositories to depend on. Tools like Gazelle may not understand which transitive dependency closure should be added to `deps` to satisfy an `import` statement.
* *Speed* of migration is an advantage here. You can reduce the effort required to migrate to a Bazel monorepo.
## Single-version policy
This approach to external dependencies is based on a philosophy that there should be a single transitive closure of external dependencies for the entire Bazel workspace. The monorepo governance group (`CODEOWNERS` of the root folder) are responsible for making dependencies work. This is how Google does things.
In practice, there is usually some need for an exception for "big breaking changes" where a second version has to be made available during a migration window. Applications are switched over one-at-a-time, then finally the old version is removed.
* *Updates* are a big deal, since changing an external dependency version will immediately make all applications in the workspace pick up the change. This can only work when applications have decent automated test coverage. This is a cultural change from multi-repo, because the engineer who does the upgrade is now responsible for any fixes needed across the whole workspace. At the organization-level, this is a feature: you get a better economy of scale if only one engineer needs to learn the details of the upgrade, and other teams get the benefits for free. At the team-level this is a bug, because it takes longer for this engineer to do the upgrade than it would have in a multi-repo.
* *Aligning dependencies* is a first migration step. We've written one-off tools to walk a multi-repo and find the greatest-common version (Go uses [MVS](https://research.swtch.com/vgo-mvs) and you can take a similar approach). Then you do pre-factoring steps to change application dependency versions to match the single version policy and roll out the application. If the change sticks, then you've reduced the mismatch. When the mismatch goes to zero, you can drop the separate transitive dependency closure for that application.
* *Solving constraints* gets harder. In a huge repository, in theory it's not possible to have a single version file that includes many external libraries because they don't have any version of a common dependency that satisfies both. In practice, we've always found that it's possible in a medium-sized repository to wiggle the situation loose, though sometimes it does require getting a fix upstream in some library to relax their constraint. (Like when they have meaningless [upper-bound constraints](https://iscinumpy.dev/post/bound-version-constraints/))
## A couple versions policy
This is a useful middle ground. There is still a governance group preventing divergence ("you cannot make a new `requirements.txt` file until you make your case to us of why you need it").
* *Disjoint dependency graphs*: this approach works best when there are truly disjoint graphs, meaning that applications in subgraph A don't depend on any of the libraries in subgraph B, and they don't share any external dependencies either. In this case you won't run into any of the version skew bugs from the "many-versions policy".
* *External hosted runtimes* can force you into this approach. For example you may deploy code to an external service like a Cloud Lambda or a Snowflake data warehouse. They may constrain the language version or version of a library that you must use. This extra constraint can make your "single version" policy unsolvable.
# Never Compile protoc Again
Source: https://site.aspect.build/blog/never-compile-protoc-again
Learn how to optimize Bazel builds by avoiding protoc compilation. Discover toolchain alternatives and solutions for faster, more efficient development.
## Background
Protocol Buffers needs a code generation step, turning your `.proto` files into “stub” code in your language of choice which provides marshaling/unmarshaling of proto messages (binary or JSON typically) into language-idiomatic data structures. If you use `services`, then you also want “stub” code to implement clients and/or servers that use gRPC.
This code generation needs a “compiler” for the protobuf language and the idiomatic compiler provided by the Protobuf team at Google is `protoc`. They provide binary releases of this tool on [https://github.com/protocolbuffers/protobuf/releases](https://github.com/protocolbuffers/protobuf/releases) and that’s where most protobuf users get it (maybe via a plugin for their Build system, such as [https://github.com/google/protobuf-gradle-plugin](https://github.com/google/protobuf-gradle-plugin))
However under Bazel, the common means of getting the tool is to compile it yourself from [this `cc_binary` target](https://github.com/protocolbuffers/protobuf/blob/5014f131760523702281f3e05c1e539f08f450c9/BUILD.bazel#L348-L354). This is what Googlers expect, having lived in an extreme monorepo where everything is vendored, and security is paramount. See an [interesting comment](https://github.com/bazelbuild/bazel-central-registry/pull/3843#issuecomment-2675948147) I got in a discussion:
> Interesting. I wasn't aware there was a contingent of Bazel rules maintainers who are opposed to google3-style source-only builds.
Yes! I’m one of those maintainers. That’s because most of us don’t have perfect caching. Ideally you don’t notice `protoc` compilation other than “the first time”. In practice, I wait for `protoc` to build all the time, and it spams my CI log with `gcc` warnings that are irrelevant to me. And on some machines, the compilation fails. If you haven’t listened to the Aspect Insights podcast, this topic was all the way back at [episode 1](https://www.youtube.com/watch?v=s0i_Ra_mG9U\&list=PLLU28e_DRwdtpojOqWM5UeFyxad7m9gCF\&index=12).
## Avoiding it
The exciting news is that many Bazel projects can stop compiling `protoc` TODAY.
Aspect engineer Sahin worked with the Bazel team long ago to help [introduce a new flag](https://github.com/bazelbuild/rules_proto/discussions/213), allowing the `protoc` binary to be registered as a [toolchain](https://bazel.build/extending/toolchains). This gives us the flexibility of registering something other than the `cc_binary` target. In fact, you’re free to register `/bin/false` as your protocol buffer compiler, if you want something very fast and very broken.
I then wrote [https://github.com/aspect-build/toolchains\_protoc](https://github.com/aspect-build/toolchains_protoc) which gives the convenience of downloading the official binary release, and registering that. See the docs on that repo, and especially the `examples` folder.
Unfortunately we haven’t made as much progress as we’d like, because there’s an ecosystem-wide change required. Everywhere that references the protobuf compiler needs to use the toolchain to resolve it. As of this writing, [https://github.com/protocolbuffers/protobuf/pull/19679](https://github.com/protocolbuffers/protobuf/pull/19679) is pending as a fix for the worst culprit.
Then there is a long, long tail of issues. For example, I was waiting 10 minutes to cut releases of our Bazel OSS rulesets, just because the stardoc documentation generator is a `java_binary` target with a hard-coded transitive dep on the `cc_binary(name=”protoc”)` target and the CD step wasn’t getting a cache hit for it. So I worked around that with another pre-build: [https://github.com/alexeagle/stardoc-prebuilt](https://github.com/alexeagle/stardoc-prebuilt) and then applied a small patch to our rulesets to override the `renderer` attribute, for example: [https://github.com/aspect-build/rules\_js/pull/2156](https://github.com/aspect-build/rules_js/pull/2156). It’s unfortunate to have to work around the problem, but in this case it’s worth it to fix the slowest step in our releases.
## Making it official
We’re hoping to upstream our `toolchains_protoc` to the protobuf repository, so that the default Bazel experience will be fast. Of course, you can still choose to register the `cc_binary` target as your proto compiler toolchain, if you want to build it from source.
# Deterministic npm dependencies with Bazel
Source: https://site.aspect.build/blog/npm-determinism
Use Bazel with npm for deterministic dependencies, faster builds, reduced CI times, and cost savings; fix non-determinism for efficient development
Determinism in a build system is the property that the outputs are identical for given input files. A build tool might embed a timestamp in its output, or use a non-stable sorting, or include local filesystem paths. All of these are non-deterministic and will ruin build performance in a build system like Bazel. This article discusses a typical source of de-optimized frontend builds we've seen at our clients who use npm packages. In later articles we'll see how similar problems exist in many language ecosystems, like in Python.
## How non-determinism impacts your build
Under the hood, Bazel hashes the contents of each installed npm package that is an input to your build. These hashes are used to determine if a target needs to rebuild and to check for cache hits before rebuilding.
Diverging input hashes in npm dependencies from different npm & yarn installs means that you won't share remote cache hits between machines and you will rebuild targets on every machine each time your npm repository rule(s) run. Since npm dependencies are inputs to most, if not all, of your NodeJS targets, this problem can effect a large part of your build graph leading to unexpected rebuilds, long CI times and frustrated developers.
Developers that are affected by this may get frustrated and perceive Bazel as slow. On CI, workers that can't share cache hits for targets will have to do duplicate work to build these targets which is both slow and expensive at scale.
Fixing this problem will reduce the frequency of long local builds, improve CI times and reduce your CI compute costs. At scale, the productivity gains and cost savings can be significant. Even for small to medium sized projects, frequent rebuilds due to non-determinism in your npm dependencies can be frustrating and break your focus.
## Non-determinism in npm installs
While npm packages downloaded from the registry are deterministic, the post-install steps of many npm packages can generate non-deterministic files. These files may contain absolute paths and/or timestamps or have other non-determinism bytes baked-in when generated during postinstall. "Native" packages which use a system like Make to compile non-JS sources at install time are a typical source of trouble, for example:
```text theme={null}
Diffing @npm repositories' node_modules
Files f1/node_modules/ssh2/build/Makefile and f2/node_modules/ssh2/build/Makefile differ
Files f1/node_modules/ssh2/build/config.gypi and f2/node_modules/ssh2/build/config.gypi differ
```
Vernacular NodeJS build systems typically don't take the file contents of npm dependencies into account when caching, so non-determinism in these files does not affect incrementally.
When building with Bazel, which provides much stronger guarantees for correctness and reproducibility, these generated files are accounted as inputs and non-determinism can lead to mass cache misses which can significantly impact the incrementally of your build.
## Checking for non-determinism
If you're seeing full rebuilds of large parts of your build graph every time your npm repository rule(s) run and lower than expected remote cache hit rates, non-determinism in your npm dependencies could be the cause.
A simple way to determine if your build is suffering from non-determistic npm dependencies is to diff the results to two separate runs of your npm repository rules at different output bases. Running on different output bases is important so that the absolute path of the npm or yarn installs differs between the two runs.
If your build runs on multiple platforms then you should also run this check on all platforms you use, since postinstall steps will generate different files on different platforms. This may be the case, for example, if your developers build on MacOS but your CI runs Linux workers
In some cases, there may be non-determinism that creeps in due to non-hermetic system inputs such as the version of gcc or xcode install on the system. To check for this you'd have to diff separate runs of your npm repository rules from machines with different configurations.
## Example
To illustrate this process, I created an example [repository](https://github.com/aspect-build/bazel-examples/tree/main/check-npm-determinism) with a [check\_npm\_determinism.sh](https://github.com/aspect-build/bazel-examples/blob/main/check-npm-determinism/check_npm_determinism.sh) script that performs a diff of two different npm installs to different output bases.
The script runs `bazel --output_base= fetch @npm//...` twice on different output bases and then compares the resulting node\_modules folders with diff,
```text theme={null}
diff -qr "$node_modules_1" "$node_modules_2"
```
If there is non-determinism in the npm install the output may look something like this,
```text theme={null}
$ ./check_npm_determinism.sh
Fetching first @npm
Starting local Bazel server and connecting to it...
INFO: All external dependencies fetched successfully.
Loading: 7 packages loaded
Fetching second @npm
Starting local Bazel server and connecting to it...
INFO: All external dependencies fetched successfully.
Loading: 7 packages loaded
Diffing @npm repositories' node_modules
Files tmp/fetch_1/external/npm/_/node_modules/ssh2/lib/protocol/crypto/build/Makefile and tmp/fetch_2/external/npm/_/node_modules/ssh2/lib/protocol/crypto/build/Makefile differ
Files tmp/fetch_1/external/npm/_/node_modules/ssh2/lib/protocol/crypto/build/config.gypi and tmp/fetch_2/external/npm/_/node_modules/ssh2/lib/protocol/crypto/build/config.gypi differ
```
In this case, the postinstall step of ssh2 generates a `build` folder with a `Makefile` and a `config.gypi` file that contain absolute paths.
> *NB: If you want to see the contents on the diff result just drop the -q flag on the diff call.*
## Resolving non-determinism
In the simple case, if the files that are non-deterministic are not needed for your build, you can simply delete them in a postinstall script. In the example repository [WORKSPACE](https://github.com/aspect-build/bazel-examples/blob/main/check-npm-determinism/WORKSPACE) file, I simply delete the `node_modules/ssh2/lib/protocol/crypto/build` folder as it is the only one that contains problematic files.
In this case, I did this by adding `&& rm -rf ./node_modules/ssh2/lib/protocol/crypto/build` to `npm install` using the `args` attribute of `npm_install`,
```plaintext theme={null}
npm_install(
name = "npm",
package_json = "//:package.json",
package_lock_json = "//:package-lock.json",
args = [
"&&",
"rm -rf ./node_modules/ssh2/lib/protocol/crypto/build"
],
)
```
but you could also do this in the package.json postinstall script or bash script called from the postinstall script.
In more complex cases, where the problematic files are needed for the build, you may need to patch the offending npm dependency so it doesn't generate non-deterministic files or land a fix upstream that does the same.
## Catching regressions
Whether or not you've found and fixed non-determinism in your npm dependencies, your build could regress in the future when new npm dependencies are added or existing ones are upgraded.
It is best practice to periodically run this check on CI (or manually if you must). Running this check on every change set could be too slow if becomes the bottleneck on no-op change sets, but setting up a periodic cron job on CI that runs once a day or more could save you and your developers time by catching regressions in npm dependency determinism when they happen so you can keep your build times fast and your developers productive.
# Mechanics of moving an other-repo to the monorepo
Source: https://site.aspect.build/blog/otherrepo-to-monorepo
Learn the step-by-step process of merging projects into a Bazel monorepo, preserving git history, and setting up smooth CI/CD workflows.
Once a Bazel monorepo has been setup, a next task is to slowly consume other projects, one-at-a-time. We'll walk through the details of how you perform that operation.
This post assumes you've already done some preliminary work:
* You have a monorepo with a folder structure and "governance": a set of policies and a group that enforces them.
* The other-repo has a similar Bazel setup, so that it can merge into the monorepo Bazel workspace. Alternatively, you can add the new folder to `.bazelignore` and "Bazelify" it *after* performing the migration.
## Preparation
Make sure the monorepo is not a regression for the other-repo developers. It should have a good developer experience, and feature-parity for common workflows.
Communicate this process with the developers in the other-repo. For example, open PRs in that repo should be landed before the move, or else they'll have to follow a similar process to rebase those changes into the monorepo.
## Create a merge commit
It is always recommended to preserve the git history of the other-repo, so that we can use the blame layer for future forensics about who changed what. Fortunately this is easy to do.
First, install a git plugin: [https://github.com/newren/git-filter-repo](https://github.com/newren/git-filter-repo)
Now you can run these commands:
```bash theme={null}
$ cd other-repo
# Make sure you are carrying the current history of the default branch, don't lose anything!
other-repo$ git fetch; git reset --hard origin/main
# Rewrite the history as if it always had been authored under the other/ folder
other-repo$ git filter-repo --to-subdirectory=other
# make sure you're at HEAD of monorepo as well
$ cd ../monorepo
$ git fetch; git reset --hard origin/main
# Make the other-repo history visible to the monorepo
monorepo$ git remote add other ../other-repo
monorepo$ git fetch other
# Make a "merge" commit that has one parent at monorepo HEAD and another at other-repo rewritten HEAD
mono-repo$ git merge other/main --allow-unrelated-histories
```
At this point you have some local commits, and you can test as usual and send the merge commit as a PR. At this point, you CANNOT REBASE. If you need to make changes, for example because monorepo HEAD has new commits, just do these steps again. In a busy monorepo, you may need to wait until outside working hours, or even freeze commits landing on the monorepo, in order to perform the operations without new commits being added.
## Make the move
Now it's time to merge onto `main`.
> Note that you might have to change the monorepo branch protection to allow merge commits, if you normally permit only linear history.
As soon as you land the merge commit to the monorepo, you're in danger of the code diverging between the two copies. You should use whatever means you have to avoid new commits landing in other-repo, as you'll need another recipe to rewrite-and-sync those commits and they might not merge cleanly. As quickly as possible, get the owning team to verify their CI and release workflows, and archive the other-repo. It's easier to un-archive it again later if needed than to deal with consequences from it being a "fork" of the code which diverges as new commits land there rather than in the monorepo.
## Re-train developers
Now that the other-repo is archived, developers who worked there need to be oriented to working in the monorepo. They should be aware of "trunk-based development" - if they work on a library which was previously versioned through a release process, they aren't used to their changes being "live" as soon as they land them.
## Setup CI/CD workflows
Bazel is supposed to make your Build and Test fast. However it has setup steps (Repository rule execution and Analysis phase over the target graph) and these always run before any cache hits can be looked up. Therefore a cold Bazel worker may be slower than your legacy build.
We suggest looking at our [Aspect Workflows](https://aspect.build/workflows) product and read our other blog posts about making Bazel fast on CI.
# The 'outside of Bazel' pattern
Source: https://site.aspect.build/blog/outside-of-bazel-pattern
The Bazel build tool is fantastic for taking a well-defined dependency graph, which is really a tree coming up from the root (the artifact to be built or test t
The Bazel build tool is fantastic for taking a well-defined dependency graph, which is really a tree coming up from the root (the artifact to be built or test to be run), and progressing through increasingly wide branches of direct and transitive dependencies, all the way up to the leaves, which are source files that live in your repository, or possibly even third-party sources.
However, I often see developers struggling to model something that’s not a tree. Sometimes it’s a bush, or a chandelier. This is usually a sign that Bazel’s dependency + action graphs won’t work well, due to bad ergonomics and ruined incrementality.
Bazel dogma teaches us that all logic should be in Starlark (Bazel’s extension language) and described in `BUILD` files. I’ll show a few examples where this isn’t the “Right Tool for the Job”.
However, I’ll make a stronger case: Bazel is the inner core of a wider system. The core really only performs two jobs well:
1. Inspect the dependency and action graphs (`aquery` and `cquery`)
2. Populate a subset of `bazel-bin` and `bazel-testlogs` (`build` and `test` - though the latter can really be thought of as “build text files containing all the test runner exit codes”)
The wider system is a “task runner”. `Makefile` commonly serves this purpose, surrounding Bazel commands, but it has a trap: it overlaps with Bazel’s capabilities and makes it impossible to ensure you don’t have Build steps sneaking into the outer layer. At BazelCon this year, I’ll present a better task runner that lets you write tasks in Starlark. In the meantime, I’ll just illustrate the task runner layer with some Bash one-liners.
## An archive of the whole repo
Our first example is common in Bazel rulesets. You want an archive that represents the whole source repository, so the shape is “take all the leaves and connect them directly to the root”.
This doesn’t work well in Bazel because packages are encapsulated, so a `glob([**/*])` doesn’t gather up all the sources in subpackages. Instead you need an awkward tree of `filegroup` targets in every package that are linked together.
The alternative is to use the **Right Tool for the Job**: `git archive`. It has some lesser-known configuration options you can set in the `.gitattributes`file (see [https://git-scm.com/docs/git-archive#ATTRIBUTES](https://git-scm.com/docs/git-archive#ATTRIBUTES)) that help a bunch:
* Filtering out contents: instead of Bazel `glob(excludes=[])` you can use `export-ignore` patterns
* Version Stamping the result: use `export-subst` to configure which file is stamped, then something like the following in that file:
* ```plaintext theme={null}
_VERSION_PRIVATE = "$Format:%(describe:tags=true)$"
VERSION = "0.0.0" if _VERSION_PRIVATE.startswith("$Format") else _VERSION_PRIVATE.replace("v", "", 1)
```
In an earlier post I covered this in more detail, including real-life examples: [releasing bazel rulesets rust](/blog/releasing-bazel-rulesets-rust)
## All the “Something” targets
This one comes up a lot. Recently I’ve been working on distributing API documentation which is generated for code across the repo. You’ll recognize this pattern whenever it seems like ``bazel query 'some expression` | xargs bazel build`` is the model of what you want to ask Bazel for.
It’s tempting to model this is a “collector” target that’s just a long list of `deps` and then wonder “how am I going to keep this list of deps up-to-date as we add more `something` targets?” You can’t and shouldn’t.
The biggest reason to avoid this one is the shape of dependency graph you end up with. Developers will commonly trip over analyzing this target (just doing a query over the repo, or `load`ing that package will do it). Then Bazel goes from incremental “only do the minimal work for the targets I requested” to performing a whole-repo step that downloads gigabytes of irrelevant tooling.
This time, the **Right Tool for the Job** is a small workflow outside of Bazel. Create a query expression that matches the targets you care about, and selects the outputs you need from them. We do this for the `lint` command to select “all the report files” for example. Here’s a full code listing for the API docgen task (note that we don’t literally use `xargs` because of the length of the command line can exceed `ARG_MAX` and spill into multiple spawns):
```bash theme={null}
docs="$(mktemp -d)"; targets="$(mktemp)"
bazel --output_base="$docs" query --output=label --output_file="$targets" 'kind("starlark_doc_extract rule", //...)'
bazel --output_base="$docs" build --target_pattern_file="$targets"
tar --create --auto-compress \
--directory "$(bazel --output_base="$docs" info bazel-bin)" \
--file "$GITHUB_WORKSPACE/${ARCHIVE%.tar.gz}.docs.tar.gz" .
```
## Compare with another version of the code
[buf\_breaking](https://buf.build/docs/cli/build-systems/bazel/?h=bazel#breaking-change-detection) is a good example. It wants to see the prior state of the output (say at the Base commit of a Pull Request), and compare with the current one. Bazel sees a single snapshot of the source code for a given build. I’ve seen some customers write a repository rule to clone a different commit of the repository, which seems very brittle to me. Checking in the prior output is too hard to automate.
The Right Tool for this Job is to find CI artifacts from the Base commit for a given change, and run a comparison/validation tool after the build runs. Then write an updated artifact from builds on the `main` branch for subsequent comparisons.
## `gazelle`
BUILD file generation is in this category, because Bazel has always refused to allow dynamic dependency graphs based on file contents. The team argues that the “no-op” build has to remain fast.
But it doesn’t matter how fast their tool is, if every user is then forced to wrap it in something slower. In this case, we always want something to run **before** Bazel’s loading phase: a step like how C++ builds run `autoconf` with a `./configure && make` workflow.
Today engineers mostly have to discover their BUILD files are outdated (maybe there’s a compilation error about a missing dependency) and then do a manual `bazel run //:gazelle` - but if we had a Task Runner layer around Bazel, we’d just setup a step to run ahead of time.
*(By the way, at BazelCon I’ll present two things: we can use Starlark to write the task that invokes Gazelle, and we can also extend Gazelle’s BUILD generation logic in Starlark!)*
## Coverage
Bazel has a `coverage` command, so why is this example here? Well, in my experience, it was a mistake - this command wanted to live in the task runner layer, but Google never wrote one. The coverage system is bad mostly because of things like lcov transformation and merging, and how difficult it is to configure.
Coverage really should have been formulated as a Task Runner that:
1. Builds the code under a Transition that enables an Instrumentation Configuration (pokes counters into the executable to track how many times a line or statement executes)
2. Runs the tests as usual. The coverage data files are configured to be additional outputs (using the `TEST_UNDECLARED_OUTPUTS` feature my intern added (hi John!))
3. After the tests are complete, collects the resulting data files. They might be LCOV format, or something else that needs to be transformed.
4. Presents the results, frequently by consulting the VCS so you can show incremental coverage (how many of the added/edited lines were tested)
## Run
Even the `bazel run` command is a mistake in my opinion. It’s a nice “syntax sugar” for building a single target, then spawning it as a subprocess. But it’s missing things like `watch` mode, we needed a separate `rules_multirun` to get multiple servers to start, and has a ton of bugs around how the working directory is selected.
If we had a Task Runner, it would clearly not be Bazel’s job to do these things.
## `print`
Buildozer is a great tool for machine-editing BUILD files, but also for quickly inspecting their contents in a purely syntactic pass that doesn’t trigger Bazel’s fetching, Loading, and Analysis phases. With a Task Runner layer, we can easily expose these Bazel-adjacent tools through the same interface engineers use to request build outputs, instead of making them install and learn about a variety of other tools. So we’d add a `print` task that doesn’t even invoke Bazel at all!
## Next
To learn more about Aspect Build's Bazel developer workflow platform and professional services, visit [aspect.build](http://aspect.build).
# Pnpm v10 and rules_js: Better Alignment and Improved Build Determinism
Source: https://site.aspect.build/blog/pnpm-v10-and-rulesjs
Pnpm v10 improves build determinism with rules_js, offering enhanced performance and reliability for Bazel-based workflows
**Pnpm v10** was released [earlier this year](https://github.com/pnpm/pnpm/releases/tag/v10.0.0). After some [initial issues](https://github.com/pnpm/pnpm/issues/9531) release [10.11.1](https://github.com/pnpm/pnpm/releases/tag/v10.11.1) now fully passes regression tests with [rules\_js](https://github.com/aspect-build/rules_js).
This release brings notable improvements in **hermeticity**, **performance**, and **reliability**, aligning `pnpm` more closely with the expectations and requirements of Bazel-based workflows that use `rules_js`.
### (Only) Built Dependencies
Since pnpm v9 rules\_js has required packages with build steps to be manually declared in the `pnpm.onlyBuiltDependencies` field of `package.json`. Now pnpm v10 has the exact same requirements as rules\_js, see pnpm [#8897](https://github.com/pnpm/pnpm/pull/8897) (as well as [#7710](https://github.com/pnpm/pnpm/pull/7710) and [#7716](https://github.com/pnpm/pnpm/pull/7715) for more background).
Explicitly declaring which packages require a build step improves determinism and hermeticity in both pnpm and rules\_js. This allows `rules_js` to model Bazel build actions without first needing to download and inspect package contents.
This change was followed-up with `pnpm.neverBuiltDependencies` ([#8958](https://github.com/pnpm/pnpm/pull/8958)) in pnpm 10.1 to suppress `pnpm install` warnings about packages containing install logic without being listed in `pnpm.onlyBuiltDependencies`.
### Secure SHA256 Hashing
Pnpm v10 has switched to more secure **SHA256** hashing of content in the `pnpm-lock.yaml` file, see [#8530](https://github.com/pnpm/pnpm/pull/8530). Bazel and rules\_js already use sha256/512 for integrity checks, and rules\_js will continue to align with pnpm lockfiles where pnpm v10 has upgraded to sha256.
### Configuration changes
Pnpm v10 has made many other configuration related changed that do not directly effect integration with rules\_js, but may effect your experience when upgrading such as:
* default hoisting has changed [#8378](https://github.com/pnpm/pnpm/issues/8378)
* `NODE_ENV` is now ignored on install [#8827](https://github.com/pnpm/pnpm/issues/8827)
* the `@yarnpkg/extensions` package was upgraded, this may alter resolved dependencies in edge cases
### Catalogs
While actually a pnpm v9.5 feature, [pnpm catalogs](https://pnpm.io/catalogs) is a feature worth mentioning again. Catalogs have provided a way to stop repeating version numbers throughout your `package.json` files and declare a single version for a package in a single location, while keeping fine grained dependencies in your projects.
Catalogs are especially useful in large monorepos where Bazel and rules\_js are normally used.
Catalogs are used by pnpm when generating the `pnpm-lock.yaml` file and does not change the underlying lockfile format, so rules\_js supports comes for free.
### Final Thoughts
Like pnpm v9 last year, pnpm v10 continues to move toward more deterministic builds that continue to align with Bazel and rules\_js, further proving pnpm was the right choice for the Bazel and rules\_js ecosystems.
See the [pnpm v10 release](https://github.com/pnpm/pnpm/releases/tag/v10.0.0) and [followup releases](https://github.com/pnpm/pnpm/releases) for a full list of changes.
# Preventing production code depending on experiments
Source: https://site.aspect.build/blog/preventing-production-code-depending-on-experiments
Learn how to manage experimental code in Bazel monorepos by restricting dependencies and ensuring faster iteration without risking production builds.
At Google we had an `/experimental` folder in source control. This is a nice way to be able to check in code in a monorepo that you're just "spiking" on, with all the usual developer ergonomics and access to shared library code. There's also no expectation that you have to maintain code in this folder.
These could go on an `experimental` feature branch. However, having this on the `main` branch lets you get CI feedback and easily point co-workers at your experiments, and collaborate with others on them, without having anyone forced to rebase the `experimental` branch on the latest `main` and deal with merge conflicts.
You can go a step further and configure the code review and merging requirements to be relaxed for commits that only modify `experimental/` so that this folder has much faster iteration times, with correspondingly lower quality expectations.
The danger of having this low-quality code in the `main` branch of a Bazel-built monorepo is that it's so easy for a production service to take a (transitive) dependency on it. This article presents a simple way to prevent that.
### Bazel can disallow dependency edges
We'll build on top of a Bazel feature called `testonly`. Bazel enforces that production code cannot depend on tests. It also prints a good error message when a developer violates the policy. So our first step is to mark all our experimental code as `testonly` - that is, you're free to use that code for your testing, but it won't be allowed to depend on it from anything that's *not* marked with `testonly`.
There's a convenient tool to machine-edit Bazel's BUILD files, `buildozer`. We'll use that to mechanically set all packages beneath `/experimental` to default their targets to be `testonly`. We'll also add a helpful comment, since developers might not be used to seeing this configuration.
```bash theme={null}
# Download buildozer if needed: https://github.com/bazelbuild/buildtools/releases/
buildozer 'set default_testonly True' //experimental/...:__pkg__
buildozer 'comment code\ in\ experimental\ may\ only\ be\ used\ for\ testing' //experimental/...:__pkg__
```
As a result, all BUILD files under experimental will now contain
```python theme={null}
# code in experimental may only be used for testing
package(default_testonly = True)
```
Now we can check what happens when production code tries to add a `dep` on something in this folder:
```bash theme={null}
$ bazel build --nobuild //myservice/...
ERROR: proj/myservice/BUILD.bazel:3:18:
in js_binary rule server:
non-test target '//myservice:server' depends on testonly target '//experimental/subdir:proto_test'
and doesn't have testonly attribute set
```
### Making sure it stays this way
That's a good start, but our setup is brittle for a couple reasons:
* we set the default\_testonly property for each package, but individual targets can override that with `testonly=False`
* new packages can be introduced under `/experimental` and the authors won't know to apply this again
As a fix, we should setup a CI task. It simply needs to query for any non-`testonly` targets under `/experimental` like so:
`bazel query 'attr(testonly, 0, //experimental/...)'`
If there's any stdout from this command, then we report that as a failure in CI.
Also, let's give developers an easy way to repair a red build if they get one. We can just print those same buildozer commands we used above - they are idempotent and safe to re-run on all the packages to update newly-added `BUILD` files.
# Principles of a Bazel Migration
Source: https://site.aspect.build/blog/principles
Successful Bazel migration: prioritize psychology, avoid workflow disruption, change gradually, maintain a ratchet mechanism
At Aspect we've helped with a bunch of build system migrations at a few scales, and have been working under our own set of guiding principles.
# Incubate net promoters
Software is written by humans, so human psychology matters. Humans are tribal, drawn to cosmetics, and jump to conclusions with their ["system one"](https://en.wikipedia.org/wiki/Thinking,_Fast_and_Slow) mind.
The initial reaction to Bazel will form a first impression that’s hard to correct later. People will feel their tribe is offended: "that might work in language X but us Y language developers would never do it that way". They'll mistake something cosmetic for something inherent: "the way the errors get presented in CI don't make any sense". They'll infer that one slow step means the whole thing is slower.
Even uninformed opinions can matter a lot. Early influential users will spread these around the engineering culture, causing people who had no strong opinion to become entrenched, making your later job either easier or harder.
For example, when rolling out a new invariant as a CI check, make the "how to fix" instruction obvious. If the person feels that the check caught a real issue that was trivial to get past, they'll feel it was an enjoyable experience. If they think you're throwing a roadblock in their way, they'll resent it.
# Don't disrupt workflows
Keep the Makefile!
If the developer types `make test` or `yarn build` or `npm run serve` today, maybe they still can. Avoid changing the topmost user-facing part of the tooling where possible.
Most product developers aren't interested in build system details and don't care about whatever change you've made, they don't want it reflected in what they need to type. Retraining is expensive and burns goodwill.
Also remember that many workflows take place in an editor. Bazel disrupts the paths on disk where editor extensions look for libraries. Be aware of this problem and be proactive to find and advertise workarounds for keeping editors happy.
# Change one thing at a time
Build system changes can cause subtle regressions, where cause and effect are at a distance, and the developer encountering the problem and the build system engineer who caused it are totally unfamiliar with the domain of the other. This makes it hard to diagnose problems.
Just like a `git bisect` workflow, a sane, linear history of events makes it much easier to reason about what happened. Based on the delta of what changed, you can make assertions about what is the possible blast radius, and whether that can explain the problem.
So, we try to change only one thing at a time. Use pre-factoring changes to the old build system to do things like break up a cycle in the dependency graph (but avoid such code changes if they're not load-bearing for Bazel migration, see "don't change the code" below). Use post-factorings to make related cleanups you noticed during the migration. Resist the urge to combine these at all costs!
One ideal outcome from this principle: you can use `bazel build --subcommands` to see the flags passed to some tool X, then compare with how that tool was called by the legacy build system, and any differences should be intentional and required by this migration step.
# Ratchet mechanism
The [ratchet](https://en.wikipedia.org/wiki/Ratchet_mechanism) is a mechanical tool to ensure "no backsliding".
Whenever you tighten the semantics of the build, by ensuring some new invariant holds, you should have a ratchet in place to make sure it stays that way. For example, if you fix some type-check errors, you should make sure the CI system will mark any subsequent changes red if they re-introduce those type-check errors.
Combined with "change one thing at a time" this can give you incredible power to work in a huge codebase. For example if you just introduce one type-check error code at a time, you can fix only those, and use your ratchet to make sure those don't come back. You can then rinse-and-repeat with low risk changes, while ensuring that the system as a whole is eventually converging on the correct behavior.
# Gradient ascent
A migration can't leave a developer experience in a bad state before improving it.
Alex explains this in a BazelCon talk: [https://youtu.be/UwuRGpVpmbo?t=398](https://youtu.be/UwuRGpVpmbo?t=398)
As explained in that talk, we want to maximize the benefits of Bazel, while deferring costs and known risks. We have to keep making improvements.
Don't say "we'll just leave a TODO here to come back and fix the performance regression". That TODO will be there longer than you think, maybe forever. In the meantime dissatisfaction might cause escalation to decision makers who de-fund the migration work.
This is related to the Youtube results for "changing a tire while driving". Even though a big migration is underway, it's critical to the business that it be non-disruptive.
# Close the loop
Before calling a task "done", think about "acceptance criteria". What does "done" mean, and did you fix the root cause, or only one proximate cause?
As an example, answering a technical question for one user helps that user today. They may ask the same question again later, along with a hundred of their colleagues. Answering their question doesn't close the loop, instead you should figure out what documentation they would have naturally consulted, or what error message they were presented with. Go fix those things, then just send them a pointer to that fix.
For Bazel this often means adding validation steps, constraining legal values for attributes, and fixing error messages, in addition to getting into a habit of always improving documentation.
# Leave the code alone
Sometimes it’s Bazel that should change - things like writing to the source folder, the choice of working directory for a test, having a filesystem layout in a certain way.
We want to avoid changes that break the legacy build, and don’t want developers to have an impression that Bazel requires code changes that are really just different idioms.
If the code does have to change, maybe it can be in a superficial way. For example you could add comments like Gazelle directives that inform the tooling without making any load-bearing changes that could break things.
Dependencies are an important case as well. We shouldn't change versions of any third-party library just because Bazel is managing them.
# Leave few fingerprints
As a consultant, you want to make sure clients own their own code during and after a migration. You also want to touch only the build system, while leaving the owners of the code to make modifications.
You can run a tool which changes the code in known-safe ways (like running a formatter), then attribute changes to that tool rather than yourself.
If you're editing a bunch of code, you probably didn't follow the "leave the code alone" principle.
# Python toolchains in rules_python
Source: https://site.aspect.build/blog/python-toolchains
Bazel is supposed to be repeatable, right? Your teammate runs the tests on her computer and should get the same result as you did. A build from a commit last mo
Bazel is supposed to be repeatable, right? Your teammate runs the tests on her computer and should get the same result as you did. A build from a commit last month should be the same if you re-build it today. But in practice, this only works when we declare a hermetic build, where all the tools used are declared to Bazel using some pinned version. Bazel fetches these tools for us so that developers don't even have to think about whether they have the right version.
This has not been the case for Python, where we've seen clients trip over having different python interpreter versions in different environments.
Toolchains play a critical role in Bazel for achieving deterministic and hermetic builds. Today the team at Aspect has reached an important goal in rules\_python when we landed the support for toolchains in the latest [0.7.0 release](https://github.com/bazelbuild/rules_python/releases/tag/0.7.0).
Here's how you start using it, based on the [upstream example](https://github.com/bazelbuild/rules_python/blob/0.7.0/examples/pip_parse/WORKSPACE):
```python theme={null}
load(
"@rules_python//python:repositories.bzl",
"python_register_toolchains",
)
# Build steps will discover the right python via
# Bazel's toolchain support
python_register_toolchains(
name = "python310",
# Available versions are in
# https://github.com/bazelbuild/rules_python/blob/0.7.0/python/versions.bzl#L39
python_version = "3.10",
)
# However, repository rules run before toolchain resolution,
# so we explicitly load the "resolved" interpreter
# for the host platform...
load("@python310_resolved_interpreter//:defs.bzl", "interpreter")
load("@rules_python//python:pip.bzl", "pip_parse")
pip_parse(
...
# ...and pass that interpreter to be used when
# dependencies are resolved.
python_interpreter_target = interpreter,
...
)
```
Any build or test actions using `py_binary` or `py_test` will pick up the hermetic toolchain automatically via [https://bazel.build/docs/toolchains#toolchain-resolution](https://bazel.build/docs/toolchains#toolchain-resolution). However, repository rules like `pip_parse` run prior to that resolution, so we've used a trick here to load the resolved toolchain for the host platform, where the repository rules run.
It relies on the fantastic work from [https://github.com/indygreg](https://github.com/indygreg) in [https://github.com/indygreg/python-build-standalone](https://github.com/indygreg/python-build-standalone). Consider sponsoring him on GitHub!
We encourage everyone to try the feature out.
A special thanks to [https://github.com/UebelAndre](https://github.com/UebelAndre) and [https://github.com/alexeagle](https://github.com/alexeagle) for helping out.
# Releasing Bazel rulesets that publish tools
Source: https://site.aspect.build/blog/releasing-bazel-rulesets
Learn how Aspect automates Bazel rule releases, ensuring secure, reproducible toolchains and seamless user experiences, with minimal manual effort.
This post is a bit of a behind-the-scenes look at how we make Aspect's Bazel rules great. Unless you're a Bazel rule author and distribute your rules to third-parties, there's probably nothing for you to take away here. But if you're a fan of the thoughtfulness and polish we put into our developer experience, soak it up!
Here's a quick problem statement:
* To make it trivial to perform good maintenance, releases must be fully automated, just by pushing a tag to the repository. (This is also for supply-chain security: we guarantee that the release is made from the sources which were tagged.)
* Where possible, our rules just spawn actions that call existing third-party tools we wrap. But sometimes we're forced to spawn a program that we wrote, and thus need to distribute.
* We don't want to leak our toolchain dependency to users. It's never as easy as it sounds to force your users to compile your C++ or Go code. Even tools written in Python incur a penalty, such as rules\_pkg requiring a python interpreter installed on the machine. Users shouldn't wait to build our tools (looking at you, `protoc`).
* We want to make it simple for users to switch between a tagged release and a `git_override` or equivalent where they depend on a source archive, based on an un-released SHA of our repo (or their fork of it).
Putting it together, what we need to do is build our binaries and publish them with each release, while also updating our toolchain definition to be able to download them, while ensuring that anyone using a "HEAD" or other source archive of the rules gets the toolchains needed to build the tools.
What's more, it needs to be stupidly simple. We are mostly doing OSS Bazel rules in our spare time: we rarely get paid to work on them. We have over 20 rulesets now. 1/3 of the releases on the Bazel Central Registry are from us \[1]. We don't want to spend time performing releases or fixing release machinery.
This puzzled us for a long time, and we've finally found a recipe that works, which I'll share here. I'll use our fantastic Bazel "standard library" as an example: bazel-lib, and step through chronologically what happens.
### Before the release
As development is going on in the repository, we always maintain a green CI. This gives us the invariant to make sure we know the integrity hash of our own binaries all the time. This means anytime a developer makes a PR that changes the sources of our tools (written in Go), they'll also be asked to `bazel run //tools:releases_versions_check_in` so that this file stays current: [/tools/integrity.bzl](https://github.com/aspect-build/bazel-lib/blob/v2.0.0/tools/integrity.bzl)
It's worth the small inconvenience of vendoring this info into the repo. It means we don't have to create any commits on the repo when we perform a release. That's important - once the tag has been pushed, we'd have to add a commit to the repo, and then re-tag (or move an existing tag which is pretty naughty).
We use these integrity hashes to declare our "release" toolchains, such as [/lib/private/copy\_directory\_toolchain.bzl](https://github.com/aspect-build/bazel-lib/blob/v2.0.0/lib/private/copy_directory_toolchain.bzl). These are ready to be registered for users to be on the "happy path" of just downloading our release artifacts from GitHub's static content distribution network.
### A tag is pushed
[/.github/workflows/release.yml](https://github.com/aspect-build/bazel-lib/blob/v2.0.0/.github/workflows/release.yml#L6-L9) shows our GitHub Actions handler that is triggered by pushing a tag. This mechanism comes from [https://github.com/bazel-contrib/rules-template](https://github.com/bazel-contrib/rules-template) which is now the recommended way to start a Bazel ruleset.
These are the steps in the workflow:
1. Run tests again, just as a check that we didn't tag a red commit. We could look it up from the run on `main` but thanks to remote caching it's cheap to just run them.
2. Run the release build. We are paranoid and check that there are no stray changes in the repo.
3. Run the `release_prep.sh` script. It has a few jobs:
* The rules are distributed as an archive file, which we build by running `git archive`. We're careful to produce an archive that's the same structure as what GitHub serves from their source archive endpoint, so that users can switch from releases to source archives with the fewest needed edits.
* We make use of a little-known configuration affordance for `git archive` in [/.gitattributes](https://github.com/aspect-build/bazel-lib/blob/v2.0.0/.gitattributes#L4C1-L12) which lets us substitute a placeholder in the code. It can also exclude some folders that don't need to be shipped. This is WAY simpler than teaching Bazel how to produce our release artifact as a build output, because that requires a tree of `filegroup` rules that are a PITA to update. (We know, we've done it a lot).
* That placeholder goes into [/tools/version.bzl](https://github.com/aspect-build/bazel-lib/blob/v2.0.0/tools/version.bzl) which is how our starlark code will be able to sense whether we are running from a release artifact or a source archive.
* Also produce the release notes, which is the "how to install" snippet that will end up on our user documentation.
4. Use the [https://github.com/softprops/action-gh-release](https://github.com/softprops/action-gh-release) reusable workflow to publish the release to our GitHub repo, including auto-generated release notes and all our artifacts published to be downloaded.
5. Rely on [https://github.com/bazel-contrib/publish-to-bcr](https://github.com/bazel-contrib/publish-to-bcr) to automatically mirror our new release to the Bazel Central Registry at [https://registry.bazel.build](https://registry.bazel.build)
### The user runs toolchain resolution
Users are instructed to register toolchains using one of a couple APIs. Here's where we can check whether the VERSION is `0.0.0` which means it's a source archive: [/lib/repositories.bzl](https://github.com/aspect-build/bazel-lib/blob/v2.0.0/lib/repositories.bzl#L141)
If it is, then we register a toolchain that builds the tools from source. If not, then we register a toolchain that downloads the binaries.
### Hiding our dependencies
There was one hidden complexity of this plan, in bzlmod. Bazel's `MODULE.bazel` file is supposed to list our dependencies and differentiates between development-only dependencies, vs. those that should be exposed to users.
Our file [/MODULE.bazel](https://github.com/aspect-build/bazel-lib/blob/v2.0.0/MODULE.bazel#L46-L72) has to declare that the go toolchain, and several go libraries, AND gazelle are real dependencies. That's because a source archive will have this file in it, and if you use a source archive, you'll need to be able to build the tools from source.
When our releases get published to the Bazel registry with Publish to BCR, the `MODULE.bazel` file will be patched, converting those Go dependencies to `dev_dependency` since users of a release artifact won't need them: [/.bcr/patches/go\_dev\_dep.patch](https://github.com/aspect-build/bazel-lib/blob/v2.0.0/.bcr/patches/go_dev_dep.patch)
We do this because we like our users! Even though bzlmod improves the story for Bazel transitive dependencies a bunch, we don't want users to accidentally get the version of these modules bumped due to the MVS algorithm taking our module's dependencies into account, nor do we want them to wait for Bazel to build them from source.
So if you look at our entry on BCR [https://registry.bazel.build/modules/aspect\_bazel\_lib/2.0.0](https://registry.bazel.build/modules/aspect_bazel_lib/2.0.0) you'll see we take only three direct dependencies, and the rest are dev dependencies.
### Credits
Thanks to Sahin and Derek from Aspect for iterating through our options for making something so elegant!
\[1] Clone bazelbuild/bazel-central-registry to see we pushed 213 of the 655 total releases on the registry:\
`$ for m in $(ls modules/*/*.json); do echo "$m $(jq '.versions | length' < $m)"; done | egrep "aspect|oci|nodejs|structure_test" | awk '{sum += $2 } END { print sum }'`
# Publishing Bazel rules that depend on tools: take 2
Source: https://site.aspect.build/blog/releasing-bazel-rulesets-rust
Learn how Aspect uses Bazel toolchains to automate Rust and Go binary releases, ensuring efficient cross-compilation and smooth user experiences.
Previously I wrote about a pattern we developed for [https://github.com/aspect-build/bazel-lib](https://github.com/aspect-build/bazel-lib) to publish our Go binaries on each release, then use Bazel's [toolchains](https://bazel.build/extending/toolchains) support to fetch those on users machines. This ensures that the Go SDK and libraries are only a development dependency of bazel-lib, not leaked to users.
We recently needed to follow this pattern in [https://github.com/aspect-build/rules\_py](https://github.com/aspect-build/rules_py) - but it didn't work quite the same way. That's because this time we wrote the tools in Rust, so that we could take advantage of some great OSS work from leaders in the Python ecosystem recently: [https://astral.sh](https://astral.sh) and [https://prefix.dev](https://prefix.dev). In particular, we wanted to use [https://docs.rs/rattler\_installs\_packages/latest/rattler\_installs\_packages/](https://docs.rs/rattler_installs_packages/latest/rattler_installs_packages/) to create our virtualenv.
The difference between Go and Rust is in the support for cross-compilation. In Go, it's quite easy for every platform to compile the complete set of release binaries. As a result, we were able to check in the integrity hashes of the binaries at every commit of the repo. If a contributors changes a Go source file, they'll be forced to update the corresponding integrity hashes as part of their PR. As a result, every commit in the git history is "releasable" - the sources include the information needed to safely fetch pre-compiled binaries from the releases page.
Rust is a bit trickier. This is both at the language level, and also because of how rules\_rust behaves. We were not able to get all the different cross-compiles working, so the source repo CANNOT always have the integrity hashes. Well... that's okay because we didn't really enjoy having to check those in anyway.
## The Fix
After some debate, we formed the requirements:
* The release instructions should still have one step: "push a tag to the repo". This ensures that developers don't push from local machine (non-reproducible, cannot make SLSA attestation that our binaries are built from the tagged sources, etc). It also reduces the maintenance burden which is critical for a small team maintaining lots of rulesets.
* For packaging the ruleset, we're allergic to the complexity of teaching Bazel about a "tree of `filegroup` targets called `release_files` as that's too much maintenance work. We prefer just `git archive` especially because of `.gitattributes` support for things like "exclude the examples folder from the distribution" and "stamp the release tag into one of the source files".
The solution: the GitHub Actions automation has to build the binaries, then we take those integrity hashes and modify the `.tar` file produced by `git archive` to insert them.
Here's a quick code walk-through in case you need to build something similar:
1. [https://github.com/aspect-build/rules\_py/blob/main/tools/release/BUILD.bazel#L31](https://github.com/aspect-build/rules_py/blob/main/tools/release/BUILD.bazel#L31) is an `sh_binary` that "delivers" the Rust tools to a `$DEST` folder
2. [https://github.com/aspect-build/rules\_py/blob/main/.github/workflows/release.yml#L12-L38](https://github.com/aspect-build/rules_py/blob/main/.github/workflows/release.yml#L12-L38) ensures that when a release tag is pushed to the repo, GitHub Actions spins up a machine for each OS to run that "deliver" tool. Then, [https://github.com/aspect-build/rules\_py/blob/main/.github/workflows/release.yml#L46-L52](https://github.com/aspect-build/rules_py/blob/main/.github/workflows/release.yml#L46-L52) we fetch those artifacts to our release job.
3. [https://github.com/aspect-build/rules\_py/blob/main/.github/workflows/release\_prep.sh#L16-L44](https://github.com/aspect-build/rules_py/blob/main/.github/workflows/release_prep.sh#L16-L44) The release script runs `git archive` like usual, but then we modify the `tools/integrity.bzl` file to contain the new hashes. Note that `git archive` HAS to be run in a pristine folder so that the release tag ends up stamped as our version in [https://github.com/aspect-build/rules\_py/blob/main/tools/version.bzl#L3-L5](https://github.com/aspect-build/rules_py/blob/main/tools/version.bzl#L3-L5). So we can't do any messing around with files until after we've run it.
4. [https://github.com/aspect-build/rules\_py/releases/tag/v0.7.1](https://github.com/aspect-build/rules_py/releases/tag/v0.7.1) shows how an automated release looks after I pushed the `v0.7.1` tag. Inside the [`rules_py-v0.7.1.tar.gz`](https://github.com/aspect-build/rules_py/releases/download/v0.7.1/rules_py-v0.7.1.tar.gz) artifact, we have all our features:
1. `tools/version.bzl` contains `_VERSION_PRIVATE = "v0.7.1"` thanks to [https://github.com/aspect-build/rules\_py/blob/78832372c4a8c17259882f9c27715f8ef6cf4451/.gitattributes#L8-L9](https://github.com/aspect-build/rules_py/blob/78832372c4a8c17259882f9c27715f8ef6cf4451/.gitattributes#L8-L9)
2. the `examples` folder is absent, to keep the release artifact from getting huge as we pile on more example usage, thanks to [https://github.com/aspect-build/rules\_py/blob/78832372c4a8c17259882f9c27715f8ef6cf4451/.gitattributes#L5-L6](https://github.com/aspect-build/rules_py/blob/78832372c4a8c17259882f9c27715f8ef6cf4451/.gitattributes#L5-L6)
3. `tools/integrity.bzl` contains the result of that release script, i.e. `RELEASED_BINARY_INTEGRITY = { "unpack-aarch64-apple-darwin": "12502bad22e0725baeb37b531322fe1d12dd053ae6716bcead88cad5e26f0dab",`\
`...`
5. Cool, now we just need to ensure that rules\_py users are setup to fetch those binaries rather than need rules\_rust. Thanks to the git-stamping in `version.bzl`, the macro users call can tell whether you're using a prerelease (developing on rules\_py or fetching a SHA of the repo: [https://github.com/aspect-build/rules\_py/blob/78832372c4a8c17259882f9c27715f8ef6cf4451/py/repositories.bzl#L61](https://github.com/aspect-build/rules_py/blob/78832372c4a8c17259882f9c27715f8ef6cf4451/py/repositories.bzl#L61)\
This way users of a release get a different set of toolchains registered.
6. That pre-built binary toolchain is just the usual incantation: one repo that contains `toolchain` calls for every platform: [https://github.com/aspect-build/rules\_py/blob/78832372c4a8c17259882f9c27715f8ef6cf4451/py/private/toolchain/repo.bzl](https://github.com/aspect-build/rules_py/blob/78832372c4a8c17259882f9c27715f8ef6cf4451/py/private/toolchain/repo.bzl) and then one repo for each platform that actually does the tool fetching: [https://github.com/aspect-build/rules\_py/blob/78832372c4a8c17259882f9c27715f8ef6cf4451/py/private/toolchain/tools.bzl](https://github.com/aspect-build/rules_py/blob/78832372c4a8c17259882f9c27715f8ef6cf4451/py/private/toolchain/tools.bzl). That ensures you only download tools for the platforms Bazel selected in toolchain resolution. Note that one tool is for the `exec` platform (we unpack wheels in an action) and the other is for the `target` platform (we create the virtualenv inside the `py_binary` runtime). Bazel handles this nicely, e.g. if you build a `py_image` on a Mac, you'll fetch the unpack binary only for darwin\_arm64 and the venv binary only for linux\_x86.
7. Finally, we've got a test in [https://github.com/aspect-build/rules\_py/tree/main/e2e/use\_release](https://github.com/aspect-build/rules_py/tree/main/e2e/use_release) that asserts that the release works as we expected: the pre-built binary toolchains were selected, and we only downloaded tools for the toolchain-resolved platform.
That's a pretty deep-dive into what we do in Bazel rules to provide an awesome end-user experience. More to come on rules\_py as we ship our 1.0!
# Build Wolfi images with Bazel: Introducing rules_apko
Source: https://site.aspect.build/blog/rules-apko
Learn about rules_apko, a Bazel plugin for building secure, minimal Wolfi-based container images with reproducible, air-gapped builds.
> Co-author: **Adam Dawson, Principal Product Manager at Chainguard**\
> Cross-posted with Chainguard: [https://www.chainguard.dev/unchained/announcing-bazel-rules-for-extending-chainguard-images](https://www.chainguard.dev/unchained/announcing-bazel-rules-for-extending-chainguard-images)
Yesterday during [BazelCon 2023](https://conf.bazel.build/), in partnership with [Chainguard](https://www.chainguard.dev/), I announced the general availability of rules\_apko, an open source plugin for Bazel, which makes it possible to build secure, minimal [Wolfi-based](https://wolfi.dev/) container images using the popular Bazel build system. This plugin allows Bazel users to build OCI container images with the open source community un-distro, [Wolfi](https://www.chainguard.dev/unchained/small-cctopus-and-a-big-idea-the-story-of-how-a-one-year-old-linux-un-distro-is-improving-the-clouds-software-supply-chain), using their existing pipelines and workflows in Bazel.
### **Apko is for more secure, distroless container images based on the Wolfi un-distro**
[Apko](https://edu.chainguard.dev/open-source/apko/getting-started-with-apko/?__hstc=1638499.71f16f5dd1368de0901e703ea3789e0a.1698301063806.1698301063806.1698301063806.1&__hssc=1638499.3.1698301063806&__hsfp=1457483275) is an open-source project developed by Chainguard for producing minimal, low-CVE, distroless container images using the Wolfi un-distro. Apko is used to assemble distroless base images and Wolfi's extensive library of APK packages (or packages you create) into an OCI-compliant container image that is fully reproducible and has a complete software bill of materials (SBOM).
### **Bazel is for fast, reproducible builds**
[Bazel](https://bazel.build/) is the open-sourced version of Google’s internal build tool, commonly used in multi-language monorepos to get faster and more reproducible builds. Bazel relies on plugins, called “rulesets,” to understand how to build images. Since Bazel can understand most languages, it’s a single tool that can produce images containing any application code. It also provides hermeticity and determinism guarantees, allowing a secure software supply chain to propagate from the package manager all the way to your production images.
### **Introducing rules\_apko**
rules\_apko is a new Bazel ruleset for building OCI images using Wolfi-base images and APKs within existing Bazel workflows.
Previously under Bazel, users had to build base images outside of Bazel and manually update them in the Bazel configuration, or use the non-performant and now deprecated `container_run_and_*` APIs in rules\_docker.
rules\_apko generates a fully locked and verifiable description of all transitive dependencies. Bazel then downloads individual APK packages needed for the requested build targets, and creates an OCI-format base image containing the installed packages. This base image can then be further extended by rules\_oci to append artifacts built from sources in the repository.
Benefits of using apko and Wolfi-base images with Bazel include:
* Supply chain security assurances in Bazel that the APK packages fetched have the same integrity hashes as the lock file.
* Bazel can build any application code in any language and add to the image.
* Bazel coordinates test runners where container images are required as inputs.
* Bazel can enable fully-offline (“air gapped”) builds with rules\_apko.
* Assurances that the resulting image is fully reproducible and has a complete SBOM.
### **Getting Started with rules\_apko**
rules\_apko is available today and it's easy to get started building secure, minimal container images in Bazel:
* Run the `apko resolve` command to produce the `apko.resolved.json` file. Note: the resolve command is undocumented and is available in the [newest release](https://github.com/chainguard-dev/apko/releases/tag/v0.11.0) of apko.
* Follow the [install instructions](https://github.com/chainguard-dev/rules_apko#installation) to add rules\_apko to your Bazel project.
* Call the `translate_apko_lock` Bazel API to import the `apko.lock.json` file so that Bazel can download and verify the integrity of remote assets.
* Add `apko_image` targets to your BUILD files to create base images.
Take a look at the [https://github.com/chainguard-dev/rules\_apko/tree/main/examples](https://github.com/chainguard-dev/rules_apko/tree/main/examples) for more ideas of how to use rules\_apko to create secure, reproducible container images for your enterprise applications.
### **Resources**
To learn more about using rules\_apko for distroless container images, check out the following additional resources:
* rules\_apko project on [GitHub](https://github.com/chainguard-dev/rules_apko)
* Bazel rules for apko [documentation](https://edu.chainguard.dev/open-source/apko/bazel-rules/?__hstc=1638499.71f16f5dd1368de0901e703ea3789e0a.1698301063806.1698301063806.1698301063806.1&__hssc=1638499.3.1698301063806&__hsfp=1457483275) on Chainguard Academy
You can try [Chainguard Images](https://www.chainguard.dev/chainguard-images) for [free today](https://console.enforce.dev/auth/login) to see for yourself how we're working to improve the container image landscape with a secure-by-default design. Our free and public Images are available on the `:latest` and `:latest-dev` versions only. If you're interested in learning more or have additional questions regarding our Chainguard Images Enterprise features and capabilities, [please reach out](https://www.chainguard.dev/contact) to our team for more information.
Aspect would like to extend our special thanks to the team at Chainguard for sponsoring the work of developing rules\_apko!
# Bazel for Frontend: Introducing rules_js
Source: https://site.aspect.build/blog/rules-js
Transform your frontend workflow with rules_js: faster builds, simplified tooling, and improved performance. Get started now!
At Aspect we've been working on supporting the [Bazel build tool](https://bazel.build) in the frontend/JS ecosystem for [five years](https://github.com/bazelbuild/rules_nodejs/graphs/contributors). We started from how Node.js programs are run internally at Google. It worked okay, but to be honest, it was never great.
A few of the problems our users have endured:
* Too slow to run a full package manager install whenever a file like `package.json` changes
* Having separate source and output folders breaks expectations of Node.js tooling. For example, TypeScript required a tricky `rootDirs` setting to resolve everything.
* No support for "workspaces" so every `package.json` had to be independently installed
* Performance: treating npm packages as directories rather than thousands of files was bolted-on late in the project and hard to adopt.
rules\_js solves these problems! There's lots of info in our README: [https://github.com/aspect-build/rules\\\_js](https://github.com/aspect-build/rules\\_js). Note that in the best case scenario, Bazel only needs to install a single npm package if that's the only one in the transitive dependency closure of the requested build/test targets!
The first Beta release of rules\_js is out: [v1.0.0-beta.0](https://github.com/aspect-build/rules_js/releases/tag/v1.0.0-beta.0) along with accompanying rulesets for some common tools like TypeScript, ESBuild, SWC, Rollup, Terser, Jest, and Webpack, listed at [https://github.com/aspect-build](https://github.com/aspect-build).
So now is a great time to do some prototyping. Follow our [migration guide](https://github.com/aspect-build/rules_js/blob/main/docs/migrate.md) and let us know how we can improve our docs to help you get up and running quickly.
I'll be giving a talk on it next week: [https://events.skillsmatter.com/bazelx2022](https://events.skillsmatter.com/bazelx2022) and we hope to publish the 1.0.0 final around that time.
Thank you to so many of our partners:
* Our awesome coworkers at [http://aspect.dev](http://aspect.dev) for working through design and public API review over the last year
* Adobe for being an early adopter to help us shake out the bugs
* Our OSS community for evaluating the design as it has evolved
* [Pete](https://twitter.com/octogonz_) from [Rush.js](https://rushjs.io/) for showing us how pnpm is the perfect fit
* Ryan Day at Google for pushing us to use npm-native abstractions
# rules_js 3.0 - out with the old (and default to the new)
Source: https://site.aspect.build/blog/rules-js-3
What’s new in rules_js 3.0: default MODULE.bazel, removed old APIs and pnpm 8, and improved maintainability and correctness. Ideal upgrade path
`rules_js` 3.0 is now available. This release simplifies the internals by dropping support for older Bazel, pnpm, and Node versions while keeping the public `rules_js` APIs stable, prioritizing maintainability, performance, and correctness over new surface features.
We've also launched new documentation with this release, at [https://aspect.build/docs/bazel/javascript](https://aspect.build/docs/bazel/javascript)
If you're already on Bazel 7+, loading `rules_js` via `MODULE.bazel`, and using pnpm 9+, the upgrade should be straightforward.
## What’s Changed?
* 🚫 Remove Bazel 6 support
* 🚫 Remove pnpm 8 support
* 🚫 Remove WORKSPACE support
* 🧹 Remove some rarely used APIs
* ✅ Enable enhancements that were previously opt-in
## Ecosystem Compatibility
Related rulesets including `rules_ts`, `rules_swc`, `rules_jest`, and `rules_webpack` have been tested with `rules_js` 3.0 and are compatible with their current releases.
The dependency on `aspect_bazel_lib` 2.x has been replaced with `bazel_lib` 3.0 ([2567](https://github.com/aspect-build/rules_js/pull/2567)). With this change, `rules_js` ensures any `aspect_bazel_lib` usage remains forward-compatible with `bazel_lib`, as long as your repository does not override the `aspect_bazel_lib` module.
***
The most significant change is the removal of pnpm 8 and WORKSPACE support.
Early versions of `rules_js` were designed around:
* The pnpm lockfile format at the time
* Limitations of Bazel repository rules in WORKSPACE mode
To avoid diverging code paths:
* When pnpm updated its lockfile format, `rules_js` downgraded it internally.
* When Bazel MODULEs (bzlmod) were introduced, `rules_js` delegated much of the logic back to WORKSPACE-based implementations.
That approach kept behavior consistent, but added complexity and limited adoption of newer pnpm and bzlmod features.
With pnpm 8 and WORKSPACE removed:
* The core architecture is simpler
* Legacy compatibility layers are gone
* A single modern code path replaces multiple branches
* Bugs such as the pnpm lockfile being parsed twice under bzlmod ([2480](https://github.com/aspect-build/rules_js/pull/2480)) are eliminated
***
## Notable CHANGELOG
**📦 Platform-specific optional npm dependencies (**[**2538**](https://github.com/aspect-build/rules_js/pull/2538)**)**
That annoying `@esbuild/android-arm64` you always see being fetched and know you *really* don't need will no longer be fetched. Other common examples are platform specific `@swc/*`, `@rollup/*` or the upcoming `@typescript/native-preview-*` packages.
**🔗** `proto_library` **support in JS deps (**[**2721**](https://github.com/aspect-build/rules_js/pull/2721)**)**
Experimental support for directly depending on `proto_library` targets in `js_library(deps)` or `ts_project(deps)`. You register a `protoc` plugin toolchain for your choice of codegen tooling.
**🗂 Package exclusion presets + default exclusion list (**[**2652**](https://github.com/aspect-build/rules_js/pull/2652)**)**
That 10mb `CHANGELOG.md` you always notice? Gone!
The `npm_exclude_package_contents` API now has multiple presets instead the previous single `use_defaults = True` which was opt-in. The previous `use_defaults = True` has been replaced with the `"yarn_autoclean"` preset which mimics the `yarn autoclean` command. A new `"basic"` preset is now available and enabled by default - this is less aggressive then the yarn list while still excluding common unnecessary files often shipped in npm packages.
**⚡ Better Bazel 9 compatibility and performance**
Bazel 9 has some new features such as the [facts API](https://bazel.build/rules/lib/builtins/module_ctx#facts) which will now be used to make things like pnpm downloads reproducible even if you don't manually provide SHAs ([2698](https://github.com/aspect-build/rules_js/pull/2698)).
Path-mapping support has also been expanded ([2575](https://github.com/aspect-build/rules_js/pull/2575)).
**🛣 Initial support for Bazel path mapping (**[**2575**](https://github.com/aspect-build/rules_js/pull/2575)**)**
Bazel path mapping allows some actions to be cached and reused across compilation modes such as `dbg` and `release`.
**🟢 Default Node version bumped to v22 (**[**2649**](https://github.com/aspect-build/rules_js/pull/2649)**)**
Along with upgrading the minimum `rules_nodejs` version the default node version is now also updated to the LTS v22.
***
## What's Next
`rules_js` 3.0 is launched and ready for you to upgrade. Check out the [updated docs](https://aspect.build/docs/bazel/javascript) or try a new project using the [https://github.com/aspect-starters/js](https://github.com/aspect-starters/js) repo which has all the latest versions.
Having problems? Aspect offers a paid support plan with a dedicated Slack channel for your team under an SLA.
# Aspect's rules_lint Reaches 2.0
Source: https://site.aspect.build/blog/rules-lint-2
Explore rules_lint 2.0 featuring AXL, Python ty support, Rust Clippy integration, and full Bazel 9 compatibility for streamlined coding and review
You’ll find the project and examples at [https://github.com/aspect-build/rules\_lint](https://github.com/aspect-build/rules_lint).
We’ve been incredibly gratified by the [more than 60 open-source contributors](https://github.com/aspect-build/rules_lint/graphs/contributors) who have improved the ruleset, and everyone who has filed or answered issues. Thank you! We support 30 languages now and continue to grow.
## Highlights of Aspect rules\_lint release version 2.0
* The Aspect Extension Language (AXL) is now used to provide the `lint` and `format` tasks on your command-line. Just install the [Aspect CLI](https://aspect.build/docs/cli/overview) and run `aspect lint`.
* Most of the [aspect-starters](https://github.com/aspect-starters) demonstrate rules\_lint 2.0, and give an easy playground to try it out.
* The rules\_lint [examples folder](https://github.com/aspect-build/rules_lint/tree/main/examples) is now broken out into standalone Bazel modules per-language, making it much easier to find the bits you need for your own repo.
* Pythonistas will love the new `ty` linter support. Ty is a fast Python type-checker written in Rust from [Astral](https://astral.sh) - and we’re working with them to add incremental type-check support to avoid quadratic runtime. Thank you to [https://github.com/whoahbot](https://github.com/whoahbot) for the contribution!
* We’ve added Rust’s `clippy` tool so you can get auto-fixes like unused imports applied automatically during code review. We expect `rules_rust` to decrease scope, possibly removing their Clippy support. Thank you to [https://github.com/blorente](https://github.com/blorente) for adding this support!
* Full Bazel 9 support, including Bzlmod `module_extension` support for fetching all tools, obviating the need for any `WORKSPACE` file or `http_archive` rules.
## About rules\_lint
`aspect-build/rules_lint` is a Bazel ruleset that makes **linting** and **formatting** “first-class” in Bazel — so you can run common static analysis tools via Bazel without wrapping your existing BUILD targets or changing the rulesets you already use.
As before, rules\_lint v2 is really two rulesets in one:
### Formatting
* Typically *one formatter per language*; deterministic output; “just apply the changes.”
* Formatting tools are run as side-effects outside of Bazel actions and wired with a `git` pre-commit hook.
* That means it can **run on files not modeled in Bazel’s dependency graph**: formatting runs on the file tree (helpful for scripts, docs, etc.).
Get started at [https://github.com/aspect-build/rules\_lint/blob/main/docs/formatting.md](https://github.com/aspect-build/rules_lint/blob/main/docs/formatting.md)
### Linting
* Can run *multiple linters per language*; may propose fixes; results can be shown as terminal output, failing tests, or code-review feedback.
* **No BUILD-file clutter**: you lint existing `*_library` targets rather than adding special wrapper macros.
* **Incremental & cache-friendly**: lint runs as Bazel actions (works with remote execution/cache).
* **Practical for legacy repos**: supports “lint only what changed” workflows so you can start without fixing all historic issues. We refer to the “Water Leak Principle” which says to stop the leak before mopping the spill.
Get started at [https://github.com/aspect-build/rules\_lint/blob/main/docs/linting.md](https://github.com/aspect-build/rules_lint/blob/main/docs/linting.md)
## Wiring into code review
Aspect’s platform includes Marvin, our mascot and helpful Bazel bot. Marvin comments on your pull requests, and includes lint results displayed as GitHub Checks. Even better, when the linter tool has a `—-fix` mode, Marvin will provide the Suggested Fixes in the GitHub code review so you can just accept the improvements to your code.
## Next steps
To learn more about our [Aspect Workflows developer productivity platform](https://www.aspect.build/platform) and [expert Bazel support services](https://www.aspect.build/services), talk to us on Bazel Slack or email us at [hello@aspect.build](mailto:hello@aspect.build).
# Introducing rules_oci
Source: https://site.aspect.build/blog/rules-oci
Discover how rules_oci improves secure container builds with Bazel, offering multi-platform support and code signing.
> Cross-posted from the [Google Security Blog](https://security.googleblog.com)
Today, we are announcing the General Availability 1.0 version of rules\_oci, an open-sourced Bazel plugin (“ruleset”) that makes it simpler and more secure to build container images with Bazel. This effort was a collaboration between Aspect, the [Rules Authors Special Interest Group](https://bazel-contrib.github.io/SIG-rules-authors/), and Google. In this post we’ll explain how rules\_oci differs from its predecessor, rules\_docker, and describe the benefits it offers for both container image security and the container community.
*Update August 2023: rules\_oci is now the* [*officially recommended ruleset*](https://bazel.build/rules#recommended-rules) *for Docker/OCI.*
See the video from my talk at Bazel Community Day SF:
%\[[https://www.youtube.com/watch?v=rLDKcNv7xAY](https://www.youtube.com/watch?v=rLDKcNv7xAY)]
## Bazel and Distroless for supply chain security
Bazel is gaining fast adoption within enterprises thanks to its ability to scale to the largest codebases and handle builds in almost any language. Because Bazel manages and caches dependencies by their integrity hash, it is uniquely suited to make guarantees about the supply chain based on the Trust-on-First-Use principle. One way Google uses Bazel is to build their widely used Distroless base images for Docker.
Distroless is a series of minimal base images which improve supply-chain security. They restrict what's in your runtime container to precisely what's necessary for your app, which is a best practice employed by Google and other tech companies that have used containers in production for many years. Using minimal base images reduces the burden of managing risks associated with security vulnerabilities, licensing, and governance issues in the supply chain for building applications.
## rules\_oci vs rules\_docker
Historically, building container images was supported by **rules\_docker**, which is [now in maintenance mode](https://github.com/bazelbuild/rules_docker#status). The new ruleset, called [rules\_oci](https://github.com/bazel-contrib/rules_oci), is better suited for Distroless as well as most Bazel container builds for several reasons:
* The [Open Container Initiative](https://opencontainers.org/) standard has changed the playing field, and there are now multiple container runtimes and image formats. rules\_oci is not tied to running a docker daemon already installed on the machine.
* rules\_docker was created before many excellent container manipulation tools existed, such as Crane, Skopeo, and Zot. rules\_oci can simply rely on trusted third-party toolchains and avoid building or maintaining any Bazel-specific tools.
* rules\_oci doesn’t include any language-specific rules, which makes it much more maintainable than rules\_docker. Also, it avoids the pitfalls of stale dependencies on other language rulesets.
# Other benefits of rules\_oci
There are other great features of rules\_oci to highlight as well. For example, it uses Bazel’s downloader to fetch layers from a remote registry, improving caching and allowing transparent use of a private registry. Multi-architecture images make it more convenient to target platforms like ARM-based servers, and support Windows Containers as well. Code signing allows users to verify that a container image they use was created by the developer who signed it, and was not modified by any third-party along the way (e.g. “person-in-the-middle attack”). In combination with the work on the [Bazel team’s roadmap](https://bazel.build/about/roadmap#software_bill_of_materials_data_generation_sboms_oss_license_compliance_tools), you’ll also get a Software Bill of Materials (SBOM) showing what went into the container you use.
Since adopting rules\_oci and Bazel 6, the Distroless team has seen several improvements to their build processes, image outputs, and security metadata:
* Native support for signing allows them to eliminate a race condition that could have left some images unsigned. They now sign on immutable digest references to images during the build instead of tags after the build.
* Native support for oci indexes (multi platform images) allowed them to remove the dependency on docker during build. This also means more natural and debuggable failures when something goes wrong with multi platform builds.
* Improvements to fetching and caching means their CI builds are faster and fail less when using flaky remote repositories.
* Distroless images are now accompanied by SBOMs embedded in a signed attestation which you can view with `cosign` and some `jq` magic:
`cosign download attestation` [`gcr.io/distroless/base:latest-amd64`](http://gcr.io/distroless/base:latest-amd64) `| jq -rcs '.[0].payload' | base64 -d | jq -r '.predicate' | jq`
In the end, rules\_oci allowed the Distroless team to modernize their build while also adding necessary supply chain security metadata to allow organizations to make better decisions about the images they consume.
# Get started with rules\_oci
Today we’re happy to announce that rules\_oci is now a 1.0 version. This stability guarantee follows the semver standard, and promises that future releases won’t include breaking public API changes. Aspect provides resources for using rules\_oci, such as a [Migration guide](https://github.com/bazel-contrib/rules_oci/blob/main/docs/migrate_from_rules_docker.md) from rules\_docker. Aspect also provides support, training, and consulting services for effectively adopting rules\_oci for building containers in all languages.
If you use rules\_docker today, or are considering using Bazel to build your containers, this is a great time to give rules\_oci a try. You can help us out by filing actionable issues, contributing code, or donating to the [Rules Authors SIG OpenCollective](https://opencollective.com/bazel-rules-authors-sig). Since the project is developed and maintained entirely as community-driven open source, your support is essential to keep the project healthy and responsive to your needs.
# rules_ts benchmarks
Source: https://site.aspect.build/blog/rules-ts-benchmarks
Discover the performance benchmarks of rules_ts, comparing ts_project with older rules and vanilla TypeScript compiler for faster builds
Aspect's [rules\_ts](https://github.com/aspect-build/rules_ts) is a port of [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs)'s [@bazel/typescript](https://www.npmjs.com/package/@bazel/typescript) package that provides a `ts_project` rule layered on top of [rules\_js](https://github.com/aspect-build/rules_js), Aspect's new high-performance Bazel rule set for JavaScript, designed from the ground up with performance in mind.
The `ts_project` rule from [rules\_ts](https://github.com/aspect-build/rules_ts) has the same API as its predecessor from [@bazel/typescript](https://www.npmjs.com/package/@bazel/typescript), but it leaves the gate with performance improvements that were not possible under [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs).
In this post, we'll compare build times for those two `ts_project` implementations, as well as against `ts_library` from [@bazel/concatjs](https://www.npmjs.com/package/@bazel/concatjs) (the original TypeScript rule from Google), and against the vanilla TypeScript compiler, `tsc`.
> To learn about how rules\_js also makes npm dependencies fast with Bazel check out our [rules\_js npm benchmarks](https://blog.aspect.dev/rulesjs-npm-benchmarks)
## Methodology
These benchmarks were run against a [generated TypeScript code base](https://github.com/aspect-build/bazel-benchmarks/tree/main/rules_ts) of 5 features, 10 modules per feature, 10 components per module and 1001 lines of code per component. This makes for a total of 555 TypeScript files containing 500995 lines of TypeScript code in aggregate.
For the Bazel build, each module maps to one Bazel target for a total of 55 `ts_project/ts_library` targets.
## Configuration
These benchmarks were run on a MacBook Pro (16-inch 2019), 2.4 GHz 8-Core Intel Core i9, 64 GB 2667 MHz DDR4 running macOS Monterey 12.3.1
Versions of typescript and rule sets used were,
* [TypeScript](https://www.typescriptlang.org/) 4.6.3
* [build\_bazel\_rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) 5.5.0
* [@bazel/typescript](https://www.npmjs.com/package/@bazel/typescript) 5.5.0
* [@bazel/concatjs](https://www.npmjs.com/package/@bazel/concatjs) 5.5.0
* [aspect\_rules\_ts](https://github.com/aspect-build/rules_ts) 0.7.0
* [aspect\_rules\_swc](https://github.com/aspect-build/rules_swc) [PR#57](https://github.com/aspect-build/rules_swc/pull/57) - this is an upcoming performance fix which uses a [new pure rust CLI](https://github.com/swc-project/swc/issues/1589) for [swc](https://swc.rs/).
## Full builds vs. "devserver" builds
In these benchmarks we measure two different scenarios:
1. A full clean build (`bazel build ...`) followed by an incremental `bazel build ...` after making a change to a leaf TypeScript file. This scenario includes type-checking.
2. A clean "devserver" build (`bazel build :devserver`), which emulates a typical developer workflow of building while running a tool such as a devserver, followed by an incremental `bazel build :devserver` after making a change to a leaf TypeScript file.
The "devserver" scenario is an important measure that emulates the typical local development workflow of coding while running tools such as a devserver or a test runner such as jest. These tools are often run in watch mode while making changes to source code. Faster build times on such changes are critical to reduce the round-trip-time to get feedback on those changes. Type-checking is not included, because it's assumed the developer already got such feedback in their editor, and type-checks will be run along with tests in CI.
Ideal build times to maximize developer productivity are less than 1 second on changes to leaf nodes and less than 10 seconds on changes that affect large parts of the graph. Ideally these ideal times are sustained even on large projects.
## ts\_project vs. ts\_library
`ts_project` was originally developed in [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) as an alternative to `ts_library` to provide a cleaner API better suited for the many ways TypeScript is used outside of Google. While the API was better suited for the wild, it could not compete with `ts_library` on performance, because the latter uses a heavily optimized and deeply integrated wrapper around the TypeScript compiler.
The new `ts_project` from [rules\_ts](https://github.com/aspect-build/rules_ts) has significantly reduced the performance gap with `ts_library` by adding first-class support for Bazel workers.
> [rules\_js](https://github.com/aspect-build/rules_js), which [rules\_ts](https://github.com/aspect-build/rules_ts) is layered on, has made first-class worker support in [rules\_ts](https://github.com/aspect-build/rules_ts) possible by doing away with the dynamic runtime node\_modules linking that [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) uses.
While `ts_library` can still slightly outpace `ts_project` with worker mode in full clean build times, in the "devserver" scenario `ts_project` has a significant advantage over `ts_library` by allowing you to configure a separate tool for transpiling TS -> JS.
In these benchmarks, we'll measure `ts_project` configured with [swc](https://swc.rs/) as the transpiler. [swc](https://swc.rs/) is an order of magnitude faster than TypeScript for pure transpilation but it does not type check, so TypeScript is still used for type checking in this split configuration.
The split configuration also removes type checking from the build graph for devserver and test targets so only transpilation is needed to build them, reducing the round-trip-time on changes when running such targets by an order of magnitude. Type-checking is handled in separate targets that can be run explicitly or with the catch-all `bazel build ...`.
## Results
Without further ado, here are the results of the benchmarks.
### Full clean builds
`ts_library` leads the pack for full (transpilation & type checking) clean build times. It has been heavily optimized inside Google and is integrated deeply with TypeScript compiler internals. The `ts_library` API, however, is not well suited for the many ways that TypeScript projects are configured outside of Google and it does not integrate well with many other rules and tools in the frontend ecosystem.
[rules\_ts](https://github.com/aspect-build/rules_ts)'s `ts_project` is a competitive runner up. It makes significant performance gains over its predecessor from [@bazel/typescript](https://www.npmjs.com/package/@bazel/typescript) by adding first-class support for worker mode.
> This is only our initial pass at worker mode for `ts_project` and we believe we can optimize it further in the future by taking advantage of Bazel features such as [multiplexed workers](https://docs.bazel.build/versions/main/multiplex-worker.html). Stay tuned for future performance improvements in this rule.
### Incremental full builds
All the Bazel rules measured are relatively close in incremental full build times with `ts_library` taking the lead and `ts_project` from [rules\_ts](https://github.com/aspect-build/rules_ts) with worker-mode and swc for transpilation runner up. In this benchmark, vanilla `tsc` is slowest but in smaller projects it can be quite fast.
### Clean "devserver" builds
Clean "devserver" builds are where `ts_project`, configured with [swc](https://swc.rs/) as the transpiler, really stands out and is an order of magnitude faster than the rest. The 500,000+ lines TypeScript code in this benchmark take even the heavily optimized `ts_library` more than 40 seconds to build while [swc](https://swc.rs/) can transpile the same in 3 seconds flat.
[swc](https://swc.rs/) is fast enough to spawn to be configured as one action per TypeScript file, which means that with remote execution the 555 `.ts` file targets in this benchmark could be distributed across 555 remote executors and transpile nearly instantly. `ts_library`, on the other hand, does not split into one target per file so it could parallelize into only 55 actions with remote execution in this benchmark.
### Incremental "devserver" builds
On incremental "devserver" builds, where `ts_library` and even `ts_project` from [@bazel/typescript](https://www.npmjs.com/package/@bazel/typescript) are fairly fast, `ts_project` configured with [swc](https://swc.rs/) for transpilation is still an order of magnitude faster.
## The bottom line
With [rules\_ts](https://github.com/aspect-build/rules_ts), front-end developers can finally get the near instant round-trip-times they are used to with optimized front-end build systems such as [Vite](https://vitejs.dev/) with Bazel.
We feel this improvement, along with [fast npm dependency management](https://blog.aspect.dev/rulesjs-npm-benchmarks) will get more web developers on board with building with Bazel.
Bazel & JavaScript is about to get a whole lot better!
# rules_js 2.0
Source: https://site.aspect.build/blog/rulesjs-2
Upgrade to Aspect's rules_js 2.0 for faster Bazel builds, secure pnpm v9 support, direct js_library linking, and enhanced ESLint integration.
Two years ago, we [launched](/blog/rulesjs-launch) the semver-stable release of Aspect's JavaScript ruleset for Bazel, rules\_js. Today I'm happy to announce that we've wrapped up development on the 2.0 release, and it's time to upgrade!
While you're poking at your Bazel setup, we also encourage users to try our monorepo developer platform, Aspect Workflows. This is the easiest way to achieve Bazel's promised speed and cost savings benefits in your Continuous Integration and Delivery pipeline. It includes Remote Cache and Remote Build Execution, along with a host of other [features](https://aspect.build/docs/aspect-workflows/features/overview). Talk to us if you'd like to run a free trial to compare with your current setup.
## What's new
pnpm is the package manager we ported into Bazel. We now support [pnpm version 9](https://github.com/pnpm/pnpm/releases/tag/v9.0.0). We also now support the [`onlyBuiltDependencies`](https://pnpm.io/package_json#pnpmonlybuiltdependencies) option, which improves security by disallowing arbitrary code execution of `postInstall` lifecycle hooks from packages you depend on.
Previously to "link" one library in your monorepo to the `node_modules` of some application, you needed an `npm_package` rule in the middle, regardless of whether that library was intended to be published outside the Bazel build. With rules\_js 2.0, a `js_library` target may be directly linked into consuming packages. This is faster, as it avoids some copying. It's also more compatible, fixing an issue with peer dependencies of first-party packages. And, it avoids an undesired "eager type-check" where `.d.ts` files in the npm package target triggered TypeScript to run, even if the `.js` files could be produced by a faster transpiler.
ESLint is the de-facto linter for JavaScript and TypeScript code. Previously users had to grab the `js_binary` target from the `eslint` package, and then wire up their own method for executing it. With Aspect's [rules\_lint](https://github.com/aspect-build/rules_lint) in 1.0 release candidate, we can confidently recommend that you use the [eslint support](https://github.com/aspect-build/rules_lint/blob/main/docs/eslint.md) we provide, which includes the ability to treat lint warnings as code review comments, rather than forcing them to be treated like errors. It has the added benefit that you can lint most other languages too, using a single configuration and with the same developer workflow.
For Docker container users, we've updated `js_image_layer` to produce 5 individual layers, with your application code only in the upper-most layer. This improves time to `docker push` since only the changed layer has a novel digest and needs to be uploaded to a remote registry.
We've made some performance improvements too, that will make Bazel's analysis phase faster. We optimized some starlark codepaths and data structures, and also reduced the size of the depset we use for representing third-party libraries. `npm_translate_lock` runs faster too.
In addition, this release is simply our chance to clean up a ton of TODO's from the codebase. As we've maintained rules\_js over the last two years, we kept our promise of semver compatibility, which meant a lot of changes had to be deferred until now.
Keeping the codebase clean makes it more mutable, so we're able to continue our long-term support commitment.
## How to upgrade
Aspect has upgraded many of our clients, and found that it's pretty quick and painless in most cases.
We have written a [comprehensive migration guide](https://github.com/aspect-build/rules_js/blob/main/docs/migrate_2.md). Please give us feedback in `#javascript` on Bazel Slack if you run into anything missing, and we'll be able to improve the migration guide for others. We don't want anyone stuck on rules\_js 1.x - that increases our support burden!
Finally, if your company is getting value from our OSS rules, consider using the Sponsor button on the repo, or add your adoption testimonial to [https://github.com/aspect-build/rules\_js/discussions/1000](https://github.com/aspect-build/rules_js/discussions/1000).
Thanks for being valued Aspect customers!
* Love from the rules\_js maintainer team
# rules_js 1.0.0
Source: https://site.aspect.build/blog/rulesjs-launch
Upgrade to rules_js 1.0.0 for faster JavaScript build, test, and release tooling under Bazel. Get support from Aspect
After a few weeks in Release Candidate, we've just released the first semver-major version of rules\_js. 🚀 For those who have been waiting for this milestone before investing time in the upgrade - wait no longer!
[github.com/aspect-build/rules\_js#v1.0.0](https://github.com/aspect-build/rules_js/releases/tag/v1.0.0)
rules\_js is a faster and more compatible approach to integrating JavaScript build, test, and release tooling under Bazel, compared with the earlier rules\_nodejs.
Learn more:
* Our original post on rules\_js: [https://blog.aspect.dev/rules-js](https://blog.aspect.dev/rules-js)
* My conference talk: [https://www.aspect.dev/resources](https://www.aspect.dev/resources)
* npm benchmarks: [https://blog.aspect.dev/rulesjs-npm-benchmarks](https://blog.aspect.dev/rulesjs-npm-benchmarks)
Aspect Development is a Bazel consulting agency and we're releasing this under an Apache 2.0 license because we believe in the community power of open-source. Engineers working at Figma, Aurora, Robinhood, and Adobe have made contributions, however there is no large company behind rules\_js who is paying for hours spent on development. Since we have to pay our engineers, we need your support!
1. Book a paid support meeting with our lead engineers on Calendly: [https://calendly.com/alexeagle](https://calendly.com/alexeagle) or [https://calendly.com/gregmagolan](https://calendly.com/gregmagolan). We'll unblock you right away and can give in-depth answers to technical questions, explain design trade-offs, and more.
2. Pay a "bug bounty" for specific OSS fixes or features you need right away, rather than wait for us to prioritize them based on our client's needs.
3. Sign up for OSS support for Bazel, including Aspect's Bazel rules, billed monthly.
4. Hire our Bazel experts as hourly consultants. Start with a free consult: [https://calendly.com/alexeagle/consult](https://calendly.com/alexeagle/consult)
You can contact [mailto:hello@aspect.dev](mailto:hello@aspect.dev) to sign up for the last three options.
We're excited to partner with you and can't wait to see what your organization can build!
# rules_js npm benchmarks
Source: https://site.aspect.build/blog/rulesjs-npm-benchmarks
Discover the performance benchmarks of rules_js npm tool compared to rules_nodejs and non-Bazel npm tools
How fast is [rules\_js](https://github.com/aspect-build/rules_js)? Why is it better than [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs)? How does it compare to non-Bazel npm tools?
If you're reading this you may be considering using [rules\_js](https://github.com/aspect-build/rules_js) for a new project, migrating your existing project to Bazel with [rules\_js](https://github.com/aspect-build/rules_js), or switching your existing project from [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) to [rules\_js](https://github.com/aspect-build/rules_js). If so, you've come to the right place. [rules\_js](https://github.com/aspect-build/rules_js) was designed from the ground up with performance in mind. The foundational piece to a performant Bazel rule set for javascript is fast fetching & linking of npm dependencies, which is what we'll be measuring here.
In this benchmark we will compare the performance of fetching, linking, and running tools with [rules\_js](https://github.com/aspect-build/rules_js) against the competition. We'll compare the following tools & rules,
* [yarn](https://yarnpkg.com/)
* [npm](https://docs.npmjs.com/cli/v8)
* [pnpm](https://pnpm.io/)
* rules\_nodejs [yarn\_install](https://github.com/bazelbuild/rules_nodejs/blob/stable/docs/dependencies.md)
* rules\_nodejs [npm\_install](https://github.com/bazelbuild/rules_nodejs/blob/stable/docs/dependencies.md)
* rules\_js [npm\_translate\_lock](https://github.com/aspect-build/rules_js/blob/main/docs/npm_import.md#npm_translate_lock)
We've chosen a package.json from [elastic/kibana](https://github.com/elastic/kibana/blob/main/package.json) as representative large Node.js monorepo. This package.json has approximately 3750 downloaded npm packages in its transitive closure.
> We will consider `node_modules` style linking only in this benchmark. Yarn's [plug'n'play](https://yarnpkg.com/features/pnp) linking style isn't widely supported at this time. Plug'n'play linking is very fast since it doesn't need to layout a `node_modules` tree on disk, however, it isn't a useful comparison for most users since it doesn't work well with many tools in the ecosystem.
## Considerations
There are a few important things to keep in mind when benchmarking npm package management tools so we don't compare apples to oranges.
### Fetching from registry vs. using local caches
When running a package manager for the first time on a project you'll likely need to fetch 3rd party packages from somewhere external, usually from the yarn or npm registries. On the first fetch, these packages are stored locally in your npm, yarn, pnpm and bazel caches so the next time you run the same command the cache will be used and the command will run much faster.
We'll compare both fetching from the internet and using locally cached packages in this benchmark as both are common scenarios users will encounter.
> Unlike yarn & npm which cache npm package archives, pnpm's cache is a very performant per-file CAS (content addressable store) under the hood. This gives pnpm a slight advantage compared to the other package managers for time to pull packages from its local cache.
>
> Bazel's external repository cache (better thought of as a downloader cache), also caches the downloaded npm package archives. [rules\_js](https://github.com/aspect-build/rules_js) makes use of Bazel's external repository cache for caching downloaded npm package archives. Although pnpm can beat [rules\_js](https://github.com/aspect-build/rules_js) in full linking time when pulling from the cache, [rules\_js](https://github.com/aspect-build/rules_js) adds the ability to lazy fetch and lazy link which will make it faster than pnpm for many common workflows.
### Bazel rules use npm, yarn and pnpm lockfiles
The `yarn_install`, `npm_install` and `npm_translate_lock` Bazel rules use pre-generated yarn, npm and pnpm locks files respectively. Users will typically generate the lockfiles outside of bazel using the package managers directly.
We will benchmark how long it take to generate lockfiles with yarn, npm and pnpm since Bazel users will need to run these tools to generate & update their lockfiles.
For all direct comparisons of package managers against Bazel rules, lockfiles will have been pre-generated.
## Configuration
These benchmarks were run on a MacBook Pro (16-inch 2019), 2.4 GHz 8-Core Intel Core i9, 64 GB 2667 MHz DDR4 running macOS Monterey 12.3.1
Internet throughput (relevant for fetching dependencies from the registry) was approximately 60 Mbps download & 25 Mbps upload.
Versions of package managers used were,
* pnpm 7.1.7
* npm 8.11.0
* yarn 1.22.19
Versions of package managers used by Bazel `npm_install` and `yarn_install` rules, fetch hermetically by Bazel, were,
* npm 8.5.5
* yarn 1.22.11
## Lockfile generation / dependency resolution
Lets start by measuring how long lockfile generation (also known as dependency resolution) takes with [yarn](https://yarnpkg.com/), [npm](https://docs.npmjs.com/cli/v8) and [pnpm](https://pnpm.io/). Bazel doesn't come into this comparison since the Bazel package management rules use pre-generated lockfiles.
We run each tool twice; once with an empty cache and once with its cache populated from the previous run. In both cases there is no lockfile pre-generated and no `node_modules` folder present.
pnpm is the fastest of the three tools for generating lockfiles in part because it has a `--lockfile-only` flag that we set so that it can generate a lockfile without linking a `node_modules` folder.
Since `npm` and `yarn` always link a `node_modules` tree we can't tell how much time just the lockfile generation takes with these tools. However, since neither `npm` or `yarn` have a `--lockfile-only` flag, that measurement is not useful since to generate a lockfile, a user will be forced to also link a `node_modules` tree with these tools.
## Linking node\_modules with lockfile pre-generated
With a pre-generated lockfile, we can bring Bazel rules into the comparisons.
The `yarn_install` and `npm_install` rules from [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) have been around for many years. These rules run the `yarn` and `npm` package managers under the hood and rely on them for fetching and linking. Caching is handled by `yarn` and `npm` as well for these rules. As such, `yarn_install` and `npm_install` do not offer performance improvements over running these package managers outside of Bazel.
The `npm_translate_lock` rule from the new [rules\_js](https://github.com/aspect-build/rules_js) rules, on the other hand, does not use any package managers under the hood. It consumes a pre-generated pnpm lockfile but internally it uses pure Bazel to fetch npm dependencies and to link the `node_modules` tree. Since fetching is handled by Bazel, npm packages fetched from the registry are cached in Bazel's external repository cache.
Since pnpm lockfiles can be generated faster than yarn and npm lockfiles, as seen above, the `npm_translate_lock` rule also has the least amount of lockfile generation overhead of the Bazel rules measured, which is a pre-req to fetching, linking and building.
Linking with [rules\_js](https://github.com/aspect-build/rules_js) using `npm_translate_lock` is beat only by linking with `pnpm` itself. As we'll see shortly, however, that is not the whole story as [rules\_js](https://github.com/aspect-build/rules_js) still has a few other tricks up its sleeve.
## Incremental node\_modules linking
One of the major deficiencies in the `npm_install` and `yarn_install` rules from [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) is that they don't support incremental `node_modules` linking. On any change to either the `package.json` or the lockfile, the external repository where the `node_modules` is located is invalidated and the entire `node_modules` tree must be re-linked with the same long install times seen above.
This is a major performance penalty for both local development and CI. Locally you can hit this often when switching branches and rebasing in repositories with a large number of 3rd party npm deps. Some developers will go so far as to change their local workflows to avoid this penalty as much as possible. On CI, persistent workers for change sets and landed commits can also hit this often, leading to slow CI times even on trivial changes.
With the `npm_translate_lock` rule from [rules\_js](https://github.com/aspect-build/rules_js), we set out to resolve this deficiency from the start. We chose to use the pnpm lockfile format because it allows for both fetching npm packages individually from the registry with Bazel's downloader and for incrementally linking the `node_modules` tree with fine grained outputs.
To illustate this we start with a full install to fetch, populate the cache & link `node_modules`. We then make an arbitrary change to a package.json dependency (in this case upgrade chai\@3.5.0 to chai\@4.3.6) and re-run install. For the Bazel rules we first re-generate the lockfile, which is not included in the measurement. This scenario would be similar to rebasing to HEAD and getting an updated package.json & lockfile from remote on the rebase.
Unlike `yarn_install` and `npm_insall` which must re-link the entire `node_modules` tree when there are any changes to dependencies, `npm_translate_lock` from [rules\_js](https://github.com/aspect-build/rules_js) matches the incremental linking performance web developers are used to outside of Bazel.
We feel this improvement will get more web developers on board with building with Bazel as their local workflow build times won't regress as they can with [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs). This is especially true at companies with large monorepos and/or a large set of npm dependencies.
> rules\_js, like pnpm, links a [symlinked node\_modules structure](https://pnpm.io/symlinked-node-modules-structure) which allows for incremental and lazy fetching and linking with Bazel. Not all tools currently work with this linking style, however, many major ones do and and many open source projects such as Next.js, Vue and Vite have already migrated to pnpm and are compatible with this linking style. There are also a growing number of tools in the ecosystem with pnpm support: [https://pnpm.io/community/tools](https://pnpm.io/community/tools).
>
> We plan to add support for yarn & npm to [rules\_js](https://github.com/aspect-build/rules_js) post 1.0. The yarn & npm rules won't support incremental linking or lazy fetching and linking since, like in [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs), they will just run the `yarn` and `npm` package managers under the hood. These rules are meant to be used for migrations from [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) to [rules\_js](https://github.com/aspect-build/rules_js) so you can migrate to [rules\_js](https://github.com/aspect-build/rules_js) without switching your package manager. Once on [rules\_js](https://github.com/aspect-build/rules_js), the switch to pnpm to unlock incremental and lazy fetching and linking can be tackled separately.
>
> For projects that are unable to switch to pnpm for lockfile generation and/or are not compatible with the symlinked `node_modules` structure, the yarn & npm rules can also be used to migrate to Bazel with [rules\_js](https://github.com/aspect-build/rules_js), however, the recommended approach is to first migrate to pnpm outside of Bazel and then migrate to Bazel with [rules\_js](https://github.com/aspect-build/rules_js) so your DX for npm dependencies does not regress.
## Lazy fetching and linking
Of all the package management solutions in this benchmark, [rules\_js](https://github.com/aspect-build/rules_js) alone allows for lazy fetching and lazy linking of npm dependencies. This is a feature that Bazel's action graph allows for that npm, yarn and pnpm are not able to reproduce and [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) was not designed to do.
Unlike npm, yarn, pnpm, npm\_install and yarn\_install, which must link the entire `node_modules` tree, [rules\_js](https://github.com/aspect-build/rules_js) using `npm_translate_lock` is able to fetch and link *only* the subset of the `node_modules` tree that is required for building and/or running one or more targets.
To illustate this we start with populated caches and no `node_modules` folder linked. For each tool we'll do the bare minimum to run the "uuid" npm package CLI tool, which is pulled in as a direct dependency.
For npm, yarn and pnpm, we'll have to fully link and then run `./node_modules/.bin/uuid`.
For `npm_install` and `yarn_install`, we will run the `nodejs_binary` target with `bazel run //:uuid_bin`.
For `npm_translate_lock`, we will run the `js_binary` target with `bazel run //:uuid_bin`.
Only `js_binary` from [rules\_js](https://github.com/aspect-build/rules_js) is able to fetch and link *only* the uuid npm package and its transitive dependencies to run the tool. The same principle of lazy fetching and linking holds for all rules\_js-based build and test targets: **you only need to fetch and link the npm dependencies that are required for the targets your are building and testing**.
In a large monorepo with many projects sharing a package.json or a workspace of many package.json files, this can lead to significant time savings by not having to fetch & link npm dependencies whenever there are changes to npm dependencies that don't affect you. *Bazel will determine which changes affect you so you don't have to.*
# Running local tools installed by Bazel
Source: https://site.aspect.build/blog/run-tools-installed-by-bazel
Manage command-line tools efficiently with Bazel. Streamline workflows, ensure version consistency, and boost development productivity with this approach.
It's a common pattern that developers in your repo are expected to run some command-line tools as part of interacting with the code. For example, maybe they need to run `terraform plan` when working with Terraform files.
However, it's a non-stop hassle to get the *right version* of Terraform installed on everyone's machine. There's always someone asking in Slack "hey it seems like there's some unsupported syntax in this code can you help me" and after some debugging you realize they installed the tool three years ago and that version isn't supported by the current code.
Bazel is great for this! However, the pattern to set it up has always been fiddly. I recently had a client who needed a better answer, so here it is!
## Fetching the tools
This post assumes the tools you need to run are statically-linked as an executable. If you're instead meant to `gem install` or `pip install` or `npm install` the tool, then this pattern is too simple to work.
I recently contributed to a nice [utility ruleset](https://github.com/theoremlp/rules_multitool) for Bazel that gives you a JSON-format lockfile for the tools you want to fetch on their various platforms. Continuing with the `terraform` example, we could have a `tools/multitool.lock.json` file containing:
```json theme={null}
{
"$schema": "https://raw.githubusercontent.com/theoremlp/rules_multitool/main/lockfile.schema.json",
"terraform": {
"binaries": [
{
"kind": "archive",
"url": "https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_darwin_arm64.zip",
"sha256": "99c4d4feafb0183af2f7fbe07beeea6f83e5f5a29ae29fee3168b6810e37ff98",
"os": "macos",
"cpu": "arm64",
"file": "terraform"
},
{
"kind": "archive",
"url": "https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_darwin_amd64.zip",
"sha256": "0eaf64e28f82e2defd06f7a6f3187d8cea03d5d9fcd2af54f549a6c32d6833f7",
"os": "macos",
"cpu": "x86_64",
"file": "terraform"
},
{
"kind": "archive",
"url": "https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_linux_amd64.zip",
"sha256": "3ff056b5e8259003f67fd0f0ed7229499cfb0b41f3ff55cc184088589994f7a5",
"os": "linux",
"cpu": "x86_64",
"file": "terraform"
},
{
"kind": "archive",
"url": "https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_linux_arm64.zip",
"sha256": "08631c385667dd28f03b3a3f77cb980393af4a2fcfc2236c148a678ad9150c8c",
"os": "linux",
"cpu": "arm64",
"file": "terraform"
}
]
}
}
```
> NB: I'd love to work on some tooling to generate this file for an arbitrary list of released binaries. Please fund us!
Now we just need to point Bazel at this JSON lockfile to get the tools installed. Paste the `MODULE.bazel` or `WORKSPACE` snippet from the latest release: [https://github.com/theoremlp/rules\_multitool/releases](https://github.com/theoremlp/rules_multitool/releases)
## Making it nice for developers
The desired end-user experience shouldn't involve Bazel and should be as simple as running the tool in your terminal:
```bash theme={null}
$ ./tools/terraform -version
Terraform v1.7.5
on linux_amd64
```
First, as always, is to find a workaround for a Bazel footgun: `bazel run` cannot possibly behave correctly because it will change the working directory. (It's designed with the assumption that you'd only want to run programs you wrote yourself...) If the working directory is wrong, then we run `./tools/terraform plan` it can't found the Terraform files: `Error: No configuration files`. Oops! Come visit [https://github.com/bazelbuild/bazel/issues/3325](https://github.com/bazelbuild/bazel/issues/3325) and give it a thumbs up.
You might see `—-run_under` suggested for this purpose, with a simple value to change the working directory like `—-run_under=”cd $PWD &&”`. However this is broken under Bazel as well, as it discards the analysis cache. Oops! Come visit [https://github.com/bazelbuild/bazel/issues/10782](https://github.com/bazelbuild/bazel/issues/10782) and ask for this to happen only when the value of `run_under` looks like a label.
To work around these issues, we're forced to use a mix of `bazel build` to fetch the tool into `bazel-out`, `bazel cquery` to know what path it's installed under, and `bazel info` to convert that relative path. Sigh.
Create a helper script in `tools/_multirun_under_cwd.sh` , make it executable and add this content:
```bash theme={null}
#!/bin/sh
target="@multitool//tools/$(basename "$0")"
bazel 2>/dev/null build "$target" && exec $(bazel info execution_root)/$(bazel 2>/dev/null cquery --output=files "$target") "$@"
```
This script assumes that the name of the program being run (`basename "$0"`) is the name we gave in the JSON file earlier `"terraform"`. So the final step is just a symlink:
`cd tools; ln -s _multitool_run_under_cwd.sh terraform`
Now the tool behaves the way users expect, running in whatever working directory they're in. Let's try under our `/infrastructure` folder:
```bash theme={null}
/infrastructure$ ../tools/terraform init
Initializing the backend...
Initializing modules...
```
Nice! Now you can repeat this for other tools you need to run, just by adding an entry to the JSON file, and the corresponding symlink in the `tools` folder.
**UPDATE May 2024**: rules\_multitool now has a dedicated `cwd` target: [https://github.com/theoremlp/rules\\\_multitool/pull/29](https://github.com/theoremlp/rules\\_multitool/pull/29) However the pattern above is still useful if you fetched your tools in some other way.
# Securing Bazel's Module Registry
Source: https://site.aspect.build/blog/securing-bcr
Bazel rulesets can now include attestations—cryptographic proof that release artifacts were built from trusted sources on secure infrastructure.
As a developer, I view most posts about Supply-Chain Security with skepticism. Fear, uncertainty, and doubt (FUD) are a great way for security vendors to get leads and make sales, whether or not the underlying problem represents a real vulnerability.
But a couple weeks ago, there was yet another wake-up call: a [vulnerability in tj-actions](https://thehackernews.com/2025/03/cisa-warns-of-active-exploitation-in.html) infected an unknown number of open-source projects, whose CI pipelines probably exposed tokens that can be used to hijack their projects.
Like the [`xz` vulnerability](https://en.wikipedia.org/wiki/XZ_Utils_backdoor) last year, this happened in an OSS repo where a tiny number of individuals (often hobbyists) are responsible for holding up the security posture of enterprise users. And many Bazel rulesets are similarly-staffed projects! Google is transferring repositories to the Linux Foundation (under the bazel-contrib GitHub org) and this comes with no resources for maintenance.
What would happen if a Bazel ruleset author leaks a Personal Access Token? Rules do things like download toolchains, and invoke them to produce your build outputs. A malicious release artifact could easily inject code into the binaries you build and ship. This attack vector is outside of your code, and outside of the third-party packages that your security scanner runs on. I don’t know of any companies who are taking steps to prevent this today.
## What can you do about it?
Whenever taking a new dependency, an enterprise security team will scan the sources for exploits, because they cannot trust the maintainers have a security posture that meets their requirements. When the dependency is updated by dependabot or renovate, hopefully they’re reviewing the delta to the sources since the prior trusted version.
It’s convenient that rulesets (and C++ packages) are published on the [Bazel Central Registry](https://registry.bazel.build) so users don’t have to vendor the sources of all the modules they depend on. However as a consequence of GitHub’s [checksum stability outage](https://blog.bazel.build/2023/02/15/github-archive-checksum.html) two years ago, Bazel modules generally publish a release artifact, which is constructed from the sources. How can an enterprise consumer know whether that artifact is truly built from the sources they’ve reviewed?
This is the assurance we can get from a framework like [https://slsa.dev/](https://slsa.dev/). Thanks to a grant from Google and a bunch of help from [Appu](https://github.com/loosebazooka) on the Distroless team, Aspect has upgraded the supply-chain for Bazel rulesets to optionally include an attestation, which is a cryptographic proof-of-trust. It allows an end-user of the module to place their trust in the build system vendor (GitHub in this case) that the build artifacts are provably constructed on a secure machine, given the sources and build scripts that exist in the repository for that release.
## Attestations of BCR modules
As an example, let’s look at [https://registry.bazel.build/modules/aspect\_rules\_lint/1.3.4](https://registry.bazel.build/modules/aspect_rules_lint/1.3.4). To be secure, one should follow the “Release Notes” link to navigate to the GitHub repo, and use the “Full Changelog” link to scan through the source code changes since the prior release we trusted. Okay, nothing looks malicious. How can we trust the release artifact on BCR?
That release also has three new files, with a [`.intoto.jsonl`](https://github.com/aspect-build/rules_lint/releases/download/v1.3.4/MODULE.bazel.intoto.jsonl) extension. These are attestations of provenance. What does that mean? Since the release was built on GitHub Actions, using GitHub-hosted runners, GitHub is providing a proof that the release artifact is constructed from the sources in the repo, using the workflow definition in the repo.
GitHub CLI provides the `attestation verify` command. If we download the release artifact we can try it out:
```plaintext theme={null}
% gh attestation verify ~/Downloads/rules_lint-v1.3.4.tar.gz --owner aspect-build --signer-repo bazel-contrib/.github
Loaded digest sha256:a7dfbfe0aa2fb960911a1589fa2ebc4f9fd0e25b0090edb54a9dfac73fdd6444 for file:///Users/alexeagle/Downloads/rules_lint-v1.3.4.tar.gz
Loaded 1 attestation from GitHub API
The following policy criteria will be enforced:
- Predicate type must match:................ https://slsa.dev/provenance/v1
- Source Repository Owner URI must match:... https://github.com/aspect-build
- Subject Alternative Name must match regex: (?i)^https://github.com/bazel-contrib/.github/
- OIDC Issuer must match:................... https://token.actions.githubusercontent.com
✓ Verification succeeded!
The following 1 attestation matched the policy criteria
- Attestation #1
- Build repo:..... aspect-build/rules_lint
- Build workflow:. .github/workflows/release.yml@refs/tags/v1.3.4
- Signer repo:.... bazel-contrib/.github
- Signer workflow: .github/workflows/release_ruleset.yaml@refs/tags/v7.1.0
```
A similar verification is built-in to the BCR presubmit process as well, ensuring that the attestations in the repository ([https://github.com/bazelbuild/bazel-central-registry/blob/main/modules/aspect\_rules\_lint/1.3.4/attestations.json](https://github.com/bazelbuild/bazel-central-registry/blob/main/modules/aspect_rules_lint/1.3.4/attestations.json)) may be trusted. Ultimately we expect that the Bazel client itself will transparently verify modules as they are downloaded, which will also support private registries.
## Rolling out to the ecosystem
We didn’t just make this work for rules\_lint. The changes needed have been made to the [Publish to BCR](https://github.com/bazel-contrib/publish-to-bcr) helper, which is now a reusable workflow rather than a GitHub App. That means we’ll soon be able to trust releases from all the rules that reuse this release and publish workflow.
Thanks to [Derek](http://github.com/kormide) for doing the engineering work! If you’re a module author, follow that link to the docs for Publish to BCR.
# Bootstrap Complete: Aspect Build Raises $3.85M to Enable Developers in Massive, Multi-Language Codebases
Source: https://site.aspect.build/blog/seed-round
Aspect Build raises $3.85M to enhance dev workflows for multi-language codebases, driving faster, reliable, and scalable software development
Today we are thrilled to be featured in [TechCrunch](https://techcrunch.com/2024/10/01/aspect-build-gets-3-85m-to-help-developers-create-software-with-bazel/), announcing that **Aspect Build** has raised \$3.85M in pre-seed and seed round funding led by **FirstMark**, with participation from **Preston-Werner Ventures** and several angel investors. This funding will accelerate the growth of our product, **Aspect Workflows** as we continue to empower software teams to overcome the challenges of modern development, enabling them to ship faster, more reliably, and at larger scale.
**Addressing the Productivity Bottlenecks**
It’s 2024, and many engineering organizations still struggle with slow, costly, and unpredictable developer workflows! The causes vary from unreliable dependency management, brittle IDE configuration, slow builds, and the impracticality of automated integration testing. Despite advancements in the DevOps stack like containerization, CI/CD, and infrastructure as code, the increasing complexity of software projects is driving up build times and slowing down teams. As projects grow, particularly with the rise of **monorepos** and reliance on **third-party dependencies**, idiomatic language-specific tooling is becoming a major bottleneck for companies striving to deliver features quickly and securely at scale.
At Aspect, we are uncomfortably excited to solve these problems. By mastering advanced techniques such as **remote execution and** **caching** and **hermetic dependency management**, we help teams eliminate the delays caused by slow and flaky builds. We bring developers along with us thanks to unrelenting focus on ergonomics, documentation, and support. Our system optimizes every step of the build process, ensuring that development teams can focus on creating and shipping great products.
**Our Journey: bootstrapping from Google**
**Greg Magolan** and I [first worked together](https://github.com/gregmagolan/abc-demo-build-with-aot-universal) at Google on the Angular team adopting **Bazel**, a large-scale multi-language build system that pioneered how massive organizations like Google handled complex software builds. We loved that Google decided to open-source the tool, but it was very clear that companies would have difficulty achieving the benefits promised in the [Bazel 1.0 blog post](https://blog.bazel.build/2019/10/17/bazel-reaches-10-milestone.html).
Rather than start with venture funding, we bootstrapped for three years by providing Bazel consulting services to over 50 companies. This gave us a front-row seat to witness painful bottlenecks that processes like dependency management, builds and Continuous Integration can create, and also the first taste of success in helping early customers overcome them.
Aspect placed a big bet on our [Open Source Software](https://github.com/aspect-build), which for us is both a passion and our code of ethics. We’ve been the authors of Bazel’s JavaScript and TypeScript support from the beginning, and have added Docker (OCI), Python, and more. Just recently, the [Bazel Central Registry](https://registry.bazel.build/) got its 2,000th entry, and we’re proud that 23% of those are from Aspect! I was also the catalyst for moving Bazel’s community and yearly conference to the Linux Foundation, setting the stage for meaningful community governance.
With **Aspect Workflows**, we’re layering on our open-source and bringing that same level of optimization from our consulting days to a wider audience, helping software teams scale their workflows to manage scale and complexity.
Since launching, Aspect Workflows has deployed at numerous customers, provided 40-60% cost savings, accelerated builds 10x and tests 2-3x, all while maintaining the reproducibility and security necessary for complex, large-scale development.
**Fueling Our Growth with Seed Funding**
We met [David Waltcher](https://www.linkedin.com/in/davidwaltcher/) over two years ago. His insight was immediately obvious, as he wrote in his introduction to us, “I’ve been spending a lot of time around Bazel… as the Google family has permeated other organizations, everyone in our network has been raving about the multi-language support and caching.” We’re glad he followed us through our bootstrap journey and our gradual conversion to recurring revenue, and we’re proud to partner with **FirstMark** and **Preston-Werner Ventures**, who bring a wealth of experience in developer tools and share our vision for transforming how software teams operate. With their support, we’re excited to accelerate our product roadmap, expand our team, and push the boundaries of what’s possible in large-scale multi-language software development.
We frequently share exciting announcements, so follow us at [https://www.linkedin.com/company/aspect-build/](https://www.linkedin.com/company/aspect-build/)
-Alex Eagle
CEO, Aspect Build
# Which tests are affected? Ask the cache, run nothing.
Source: https://site.aspect.build/blog/selective-testing-with-cache-diff
How aspect cache diff figures out which tests a change can break, using only a remote cache and Bazel's gRPC log, without executing anything.
Your remote cache already knows which tests should run. You're just not asking it yet.
[`bazel-diff`](https://github.com/Tinder/bazel-diff) is the tool most teams reach for.
It hashes every target from its rule, its attributes, and the hashes of its inputs
(a Merkle tree), then diffs two revisions. It works, and we've used it. But it's not without its limitations:
it sits one level above the action cache key, which is what actually decides whether an action re-runs. `bazel-diff`
only sees target definitions and source file contents. It never notices that two different inputs can still produce a byte-identical output that's already in the cache.
It overreports.
Lockfiles make this especially obvious, and it isn't just a Python problem. By default, `bazel-diff` treats
each external repo as an opaque blob. Anything that re-runs a repository rule re-hashes
the whole repo, flagging every target under it. Bump one package in `uv.lock` and all of
`@pypi` is dirty. Touch `pnpm-lock.yaml` and `@npm` goes with it. Edit `go.mod` and the
generated Go module repos follow.
The [`--fineGrainedHashExternalRepos`](https://github.com/Tinder/bazel-diff) flag narrows that to individual changed
targets, which helps a lot. Even so, `bazel-diff` is still hashing targets. But the deeper problem remains: you can change a
target's inputs, Bazel rebuilds the action, and the output comes out byte-identical. The action cache would have served it.
`bazel-diff` still flags it.
We wanted to catch that case (target changed, output didn't) by reading the cache directly. That's what pushed us
to build `aspect cache diff`.
Like any analysis tool, `aspect cache diff` overreports by default — there is no cheap way to rule out every false positive without actually building something. It ships two modes: **overreport** (the default) runs nothing; **precise** runs `bazel build` on the changed frontier first and produces an exact answer. Both are described [below](#inverting-the-hits).
Two design constraints: use only a remote cache with no
remote-execution cluster, and read the verdict straight out of Bazel's
[`--remote_grpc_log`](https://bazel.build/reference/command-line-reference#flag--remote_grpc_log),
no `cquery` and no `aquery`.
## Why not just run `bazel test //...`?
Bazel already serves cached test results. Why not just run everything and let it skip what hasn't changed?
A single Bazel server can only parallelize within one machine. [`--jobs`](https://bazel.build/reference/command-line-reference#flag--jobs) parallelizes across local cores,
or across an RBE (Remote Build Execution) cluster if you have one. Without RBE, every affected test runs on one box,
bounded by its core count. You could scale your machine, but you'd get sub-linear returns, have an oversized machine sitting idle for 99% of small-scale builds, and
pay for cores whether you use them or not. You can't run more tests concurrently than one Bazel server can schedule.
The way you scale without RBE is to shard across many CI runners, each running a slice.
To shard intelligently, you need the affected set as data first — a list of labels
you can partition before anything runs. That's what `aspect cache diff` produces, cheaply, without executing anything.
The probe runs on a small coordinator, prints the affected labels, and a scheduler fans
them out. Running `bazel test //...` to completion on one machine is the serialization bottleneck you're trying to break.
## Probe the cache, run nothing
Bazel has a little-known flag, [`--experimental_remote_require_cached`](https://bazel.build/reference/command-line-reference#flag--experimental_remote_require_cached),
that intercepts the remote-execution decision (the moment Bazel is about to dispatch an action to a remote executor).
With it, when an action isn't already in the remote cache, Bazel refuses to run it. It returns `EXECUTION_DENIED`
before scheduling any work, local or remote. Pair it with `--remote_grpc_log` and Bazel writes one `LogEntry` per RPC,
including a `GetActionResult` for every action it checked, tagged with the target label and action mnemonic.
So the probe is a single invocation that executes nothing:
```sh theme={null}
bazel test //... \
--experimental_remote_require_cached \
--remote_grpc_log=probe.binpb \
--keep_going
```
Every action is either a cache hit (served, nothing run) or a miss (denied, nothing run).
Then we check the log.
One problem: with a cache but no executor, the remote-execution decision never fires.
A cache miss falls back to local execution, ungated, and the flag does nothing. So we give
Bazel an executor to gate against. It's an in-process dummy that implements just enough of the
[Remote Execution API](https://github.com/bazelbuild/remote-apis) for Bazel to route
actions through the remote-execution path. `--experimental_remote_require_cached`
intercepts each miss and returns `EXECUTION_DENIED` before anything reaches the dummy.
## Inverting the hits
Under `--keep_going`, a cache miss deep in a test's dependency tree trips require-cached at
that action. The test's own `TestRunner` lookup never gets reached. The flat log
shows the miss frontier, the deepest missed action on each branch, not a clean per-test
verdict.
The trick is inverting the hits instead of chasing the misses:
> A test's `TestRunner` action is a cache hit if and only if its entire transitive closure
> was cached, which means the test is unaffected.
So `affected = tests(//...) − {tests whose TestRunner lookup hit}`. And since every log
entry carries the target label, the tests are self-labeling. No sentinel actions to wire up.
Two modes come out of this:
* **`overreport`** (the default): runs nothing and reverse-deps from each missed target to
its dependent tests. Cheap, but it flags tests that may actually be cached if
their direct inputs haven't changed.
* **`precise`**: first runs `bazel build` on the missing non-test actions to resolve deps that are
merely uncached, then counts a test as affected only if its `TestRunner` action misses, a genuine input change.
There's no blind spot: a `TestRunner` action key already folds in the content
digests of its runfiles, so a data dep that rebuilt to a different output moves the test's key (correctly affected),
and one that rebuilt to a byte-identical output leaves it alone (correctly skipped). Same
transitive-closure guarantee, read off a single action. It builds the changed frontier
but still never runs a test, since `bazel build` can't execute `TestRunner`.
`overreport` is the default because it runs nothing. `precise` has to `bazel build` the
changed frontier first. The coordinator running the probe doesn't have to be small — you can size it up
or point it at RBE to keep build times fast. Tests are often the long tail of CI: wide end-to-end suites
that need to be sharded across many runners to finish in time. The build frontier in `precise` mode
is bounded to a single pass; the false positives `overreport` emits fan out across all your shards.
Reach for `precise` only when a falsely-flagged test costs more to run than the frontier costs to build.
Even in `overreport` mode, `aspect cache diff` handles repository rule changes more precisely than
`bazel-diff`. Where `bazel-diff` marks an entire external repo as dirty when its lockfile changes,
`aspect cache diff` lets Bazel re-evaluate the repository rules and propagates only the actual
downstream effects — touch one package in `uv.lock` and only what was genuinely affected gets flagged,
not all of `@pypi`. In a large monorepo with a single directory change, `overreport` resolves most of
the graph exactly; only the targets blocked by an uncached upstream action are unknowable without
building first.
## What the probe has to get right
Two things have to be true for the probe to produce a correct affected set, and both took us longer to pin
down than we'd like to admit.
**Flag parity between the probe and your baseline.** Local and remote execution compute the same action digest
given the same flags. But any flag that touches an action's environment, [`--action_env`](https://bazel.build/reference/command-line-reference#flag--action_env)
for instance, changes that digest. If the CI run that populates the cache uses a flag the probe doesn't
(or vice versa), every action looks like a miss and the probe reports everything as affected.
The fix is flag hygiene: the run that fills the cache and the run that reads it must see identical flags.
**Defeating two layers of local short-circuiting.** Run the probe twice in the same output base
and the second run reports nothing, because Bazel never queries the remote cache at all.
The Bazel server is long-lived and keeps its [Skyframe](https://bazel.build/reference/skyframe) graph alive
between invocations. On a re-run, Skyframe sees an action's node is already evaluated with unchanged inputs
and returns the memoized result without re-entering the execution path. The remote cache lookup lives inside
that path, so it never fires.
Underneath Skyframe is the on-disk action cache: a persistent record in the output base of
which actions Bazel has run. It survives server restarts. Even on a cold server, if the on-disk
cache says an action is up-to-date, Bazel marks it as done and skips it. Again, no remote lookup.
So we open both gates:
```sh theme={null}
--nokeep_state_after_build --nouse_action_cache
```
[`--nokeep_state_after_build`](https://bazel.build/reference/command-line-reference#flag--keep_state_after_build)
discards the in-memory Skyframe state when the build finishes, so the next probe
re-evaluates from scratch. [`--nouse_action_cache`](https://bazel.build/reference/command-line-reference#flag--use_action_cache)
ignores the on-disk action cache, so no action is presumed up-to-date. Every action
falls through to its normal execution path, where the first step (with a remote cache
configured) is a `GetActionResult` keyed by the action digest.
That lookup is the whole point, and it's cheap: a single small gRPC call that returns
hit-or-miss from metadata alone. No outputs downloaded, nothing executed. Neither flag is part
of the action key, so the digest we look up is byte-identical to the one your baseline
cached.
Clearing these two caches instead of running `bazel clean` is also what keeps the probe fast on a warm runner:
the output base survives, so external repos stay extracted across runs.
Opening those gates isn't free. Throwing away Skyframe and bypassing the action cache
forces Bazel to redo analysis and re-check every action from scratch. The probe's
analysis phase runs slower than a warm `bazel test //...` that reuses all of that incremental state. We're
deliberately discarding the incrementality Bazel works hard to preserve. For a normal
build, that's a regression; for the probe it's the entire point, because that same
short-circuiting is what hides the cache lookups we need to read.
It still beats the multiple Bazel invocations `bazel-diff` requires. To diff two revisions,
`bazel-diff` checks out the base, runs `generate-hashes`, checks out your change, runs
`generate-hashes` again, then diffs the two JSON files. Two full Bazel invocations across two checkouts, and
the checkout in the middle invalidates Skyframe, so there's nothing incremental to reuse
between them. The probe runs once, against your working tree, with the cache your mainline
CI already filled as the baseline. No second checkout, no second invocation,
and the result is keyed on action digests instead of target hashes.
## What you get
Nothing to install separately. `aspect cache diff` ships in the
[Aspect CLI](/docs/cli/install), the same `aspect` binary you already run Bazel through.
```
$ aspect cache diff
//services/api:integration_test ← cache miss in //services/api:lib (CppCompile)
//services/api:smoke_test ← cache miss in //services/api:lib (CppCompile)
Affected 2 of 318 test target(s).
```
Affected labels stream to stdout, so you can pipe them straight into
`xargs aspect test`. The reasons, the "caused by" target, and the summary go to stderr, so
the two streams stay separable. `--format=json` emits a structured output for a
scheduler to consume. `--exec` runs the affected set in one shot:
```sh theme={null}
aspect cache diff --exec="aspect test"
```
On a very large graph, passing labels through `xargs` or `--exec` can hit the shell's
argument-length ceiling. Redirect stdout to a file and hand it to `aspect test` via
`--target-pattern-file` instead — no argument-length limit:
```sh theme={null}
aspect cache diff > affected.txt
aspect test --target-pattern-file=affected.txt
```
## Grouping the affected set into CI jobs
Because the affected labels come out as plain data, you aren't stuck running them as one
undifferentiated batch on one runner. You can partition the set along boundaries your team already
reasons about and schedule each group as its own CI job, a Buildkite step or a GitHub
Actions matrix leg:
* **By language:** Go tests on a runner sized for them, the TypeScript suite on another,
the slow integration tests on a third.
* **By label hierarchy:** `//services/...` on one job, `//libs/...` on another, so a change
confined to one area only spins up the runners it actually needs.
This is the sharding from earlier, made deliberate. Instead of slicing the affected set
into arbitrary chunks, you slice along ownership and test behavior boundaries. Each group gets
its own resource profile, and its own pass/fail signal, all running in parallel. The probe is cheap enough to run first
on a small coordinator, so you only pay for the runners a given change requires.
## The honest caveats
This is a cache-shaped tool, so it inherits the cache's limits.
* **Baseline coverage bounds accuracy.** A cache hit definitively means unaffected. A miss means
changed, never-uploaded, or evicted. Against a fully-populated baseline it's exact;
against a sparse cache it overreports. Your mainline CI's normal cache uploads are the
baseline, no extra job to run.
* **Always-local actions are always affected.** Anything tagged `no-remote`, `local`, or
`no-cache` bypasses the remote path; require-cached can't gate it, so it runs locally
during the probe and reads as affected.
The payoff is a different failure mode from `bazel-diff`. Instead of a Merkle tree amplifying
every leaf change up the graph, you get exactly what the cache knows changed, computed from
the same action keys your build already uses, with nothing executed to find out.
# Self-hosting your CI/CD infra
Source: https://site.aspect.build/blog/self-hosting-your-cicd-infra
Discover how Aspect Workflows powers Bazel CI/CD with a BYOC model, boosting security, compliance, cost control, and seamless integration in the cloud stack
Aspect Workflows is the Software-as-a-Service product that runs [Bazel](https://bazel.build) developer workflows, such as continuous integration and delivery, with the speed and cost benefits promised by this advanced tool. But it’s not like most Cloud-hosted SaaS that runs on an account the vendor provides. Instead we deploy into our customers cloud accounts, sometimes called “Bring Your Own Cloud” (BYOC). In this post I’ll explain why we do it this way, and how our customers benefit.
### Enhanced Security
Customer cloud accounts have security protocols to isolate networks on Virtual Private Clouds (VPCs), enforce custom IAM roles, and firewall applications to prevent unintended access. CI/CD systems are core to the software supply-chain, so vulnerabilities matter! Self-hosted CI infrastructure is subject to the same policies.
It requires less trust from the vendor. While Aspect is SOC2 certified of course, there are always other security and business risks of relying on a vendor to operate the infrastructure. The reduced risk in self-hosting has advantages in legal and procurement processes as well.
Self-hosted infrastructure-as-code (IaC) also allows your security scanning tools to operate over the vendor’s infrastructure definitions, including any co-maintenance policies granted to the vendor’s on-call engineers.
### Data Control and Compliance
Data is retained in the customer’s cloud, and is subject to the access auditing, encryption, and retention policies enforced by their platform team. This is especially useful in industries where regulations like GDPR, HIPAA, FINRA require constraints around data management.
### Cloud Pricing
Many companies have a cloud contract that reserves some capacity to get a better pricing agreement. Others like startups have credits given by the cloud sales team or from their investors. Sometimes there’s a requirement to utilize all the compute credits or reservations.
Hosting CI infrastructure in the customers cloud account lets them save on hosting costs, compared with a vendor-hosted model.
### Honest Billing
Running tests can be resource-intensive, and you want to be able to run as many as you need. When a vendor meters their service based on usage, it’s difficult to budget for predicted SaaS costs.
In the case of Build & Test, the vendor should be expected to reduce the cloud compute costs by optimizing use of caching and right-sized, elastic-scaling instances. If the vendor is rewarded for consuming compute, they have a perverse incentive to use more resources rather than less.
Self-hosting infrastructure puts the resource billing under the customers control, and allows them to scrutinize the optimizations the vendor provides.
### Low-latency, high-bandwidth integrations
Running infrastructure in the customers cloud makes it much more straightforward to introduce components developed in-house or by other vendors. For example, a Build system infrastructure should be network-adjacent to a remote development cluster or remote IDE backends.
# Stamping Bazel builds with selective delivery
Source: https://site.aspect.build/blog/stamping-bazel-builds-with-selective-delivery
Stamping Bazel builds for selective delivery, ensuring version control integration and efficient artifact release in CI/CD pipelines
The obvious next step after building a nice CI pipeline around Bazel is Continuous Deployment. So no surprise that one of the frequent questions on Bazel slack is
> How do I release the artifacts built by Bazel?
and the answer is really not well documented anywhere. Here's what I've learned.
# Stamping
Bazel is mostly unaware of version control, and that's good because coupling causes intended feature interactions. But sometimes you want the git SHA to appear in the binary so your monitoring system can tell which version is crash-looping. This is where stamping is used. Bazel keeps two files sitting in `bazel-out` all the time, `stable-status.txt` and `volatile-status.txt`, which are populated from local environment info like the hostname, and can be inputs to build actions.
The files are just sitting there in the output tree after any build:
```plaintext theme={null}
$ cat bazel-out/stable-status.txt
BUILD_EMBED_LABEL
BUILD_HOST system76-pc
BUILD_USER alexeagle
$ cat bazel-out/volatile-status.txt
BUILD_TIMESTAMP 1634865540
```
> You can fill in more values in this file by adding `--workspace_status_command=path/to/my_script.sh` to your .bazelrc and writing a script that emits values, often by calling `git`. Note that adding this flag to every build can mean slow git operations slowing down developers, so you might want to include this flag only on CI. As an aside, instead of just a git SHA let me recommend [https://twitter.com/jakeherringbone/status/1324871225898749953](https://twitter.com/jakeherringbone/status/1324871225898749953)
The "stable" statuses are meant to be relatively constant over rebuilds on your machine. So your username is stable. The stable status file is part of Bazel's cache key for actions. So if your value of `--embed_label` changes, it will be reflected in the BUILD\_EMBED\_LABEL line of stable-status.txt and you'll get a cache miss for every stamped action. They will be re-run to find out the new value.
The "volatile" statuses change all the time, like the timestamp. These are not part of an action key, as that would make the cache useless.
Bazel only rebuilds an artifact if the stable stamp or one of the declared inputs changes. Otherwise you can get a cache hit, with a stale value of a volatile stamp.
> Due to using a volatile stamp, we had a bug when we made Angular's release process. As a workaround, to make sure all the artifacts were versioned together, we had to do a clean build when releasing. I always felt bad for whoever was doing the push on their laptop and waiting. This was the wrong approach, it should have used stable stamping.
# When to stamp
Bazel has a flag `--stamp`. Very sadly, it is not exposed to Bazel rules in any consistent way, and so many rules have a fixed boolean attribute `stamp = True|False`. This inconsistency is too bad, and causes a lot of friction around correct stamping.
You should not enable `--stamp` on your CI builds. When any stable status value changes, you'll bust the cache and re-do a lot of work. Even if you don't use stable status values, some ruleset you depend on might.
This is also a key element of how we'll find the changed artifacts later. We don't want any stamp info in them at all, so their content hash is deterministic.
# Finding the releasable artifacts
Use a Bazel rule to describe your release artifacts. I think it's easiest if this rule is executable, so you can `bazel run //my:artifact.push` for example. Delivery styles vary a lot, so I haven't seen one of these that works for everyone. You could write a custom rule that produces a manifest file of whatever info your continuous delivery system needs to know.
After a green CI build and test step, your pipeline should use bazel query to find all of the release artifacts.
# Selective release
We could release everything all the time, but
* we don't want to push duplicate artifacts
* stamped artifacts should always reflect the version info of the last change that affected them
* downstream systems will be confusing for users to operate since there are too many versions to pick from
Here's the recipe:
1. CI already ran without `--stamp`, so the release artifacts are deterministic from sources, and are in `bazel-out` (or may be only in the remote cache, so you'll need the [Remote Output Service](https://github.com/bazelbuild/bazel/pull/12823))
2. Query for the release artifacts and loop over them
3. For each artifact, compute the content hash (or just take the existing .digest output from sth like docker that supplies it)
4. run a reliable key/value store to act like a bloom filter (Redis SETNX is good for this) which quickly tells you that the content hash is different than before
5. loop over these newly-seen artifacts labels and run again with `bazel run --stamp thing.deploy` or whatever you need to do to promote them to the next stage in the CD pipeline
Since most of the actions in the dependency graph shouldn't be stamp-aware, the last step here should still be fairly incremental.
## Update: Do it for me!
Hey, great news. In the two years since I wrote this post, we've fully-baked this solution into our Bazel CI/CD product, Aspect Workflows. You can find details on our approach in our [Bazel Delivery Guide](https://aspect.build/docs/aspect-workflows/features/selective-delivery) and how it's configured in our product: [https://aspect.build/docs/aspect-workflows/features/selective-delivery](https://aspect.build/docs/aspect-workflows/features/selective-delivery) - Sign up for a free month and experience how it works on your own codebase!
# Bazel Starlark Docs on the Registry
Source: https://site.aspect.build/blog/stardocs-on-bcr
Explore Bazel Starlark API docs on the Registry for updated ruleset documentation, publishing guides, and community contribution opportunities
About a year ago, I experimented with buf and starlark docgen. I didn’t make any public announcement at the time. This past year I’ve been continuing to evolve the design, and I’m excited to have finally launched it!
For example here is [https://registry.bazel.build/modules/tar.bzl](https://registry.bazel.build/modules/tar.bzl):
## Documenting Bazel APIs
Some documentation for the Bazel “core” appears on [https://bazel.build](https://bazel.build). However Bazel is extended by rulesets which are published as modules. How do developers find the API documentation for these?
The answer has been quite fragmented, with the community taking several approaches:
1. Markdown or ReStructured Text files checked into the repo like [https://github.com/bazel-contrib/rules\_go/tree/master/docs/go/core](https://github.com/bazel-contrib/rules_go/tree/master/docs/go/core). These rely on some testing that the checked-in files match the stardoc output. So a `stardoc_with_diff_test` is commonly included, like [this one for the Go example](https://github.com/bazel-contrib/rules_go/blob/master/docs/doc_helpers.bzl). This is not great for contributors who submit a pull request (PR) with a minor correction to some Starlark code — they are met with a red PR and have to update the codegen. Worse, that process uses a Java program in the stardoc module which has to be built from source. This has a bunch of dependencies like protoc which also builds from source. It can take 10min to update the codegen following a 10sec minor correction. This leads to contributors abandoning the fix.
2. Using GitHub Pages like [https://bazelbuild.github.io/rules\_rust/](https://bazelbuild.github.io/rules_rust/) or adding a versioning scheme like [https://bazelbuild.github.io/rules\_pkg/](https://bazelbuild.github.io/rules_pkg/)
3. Using Sphinx and a [custom Bazel publishing pipeline](https://rules-python.readthedocs.io/en/latest/sphinxdocs/sphinx-bzl.html) like [https://rules-python.readthedocs.io/en/latest/](https://rules-python.readthedocs.io/en/latest/)
4. Aspect had a legacy docsite where we rendered API docs.
Most of these were not great, missing features like versioning, full-text search, a button to copy a code sample, or nav-to-edit.
And most importantly, there was no one place to look. As a developer, if you don’t know which Bazel Module contains the doc you need, you end up searching around all these places. Bazel Modules are commonly published on the Bazel Central Registry (BCR) at [https://registry.bazel.build](https://registry.bazel.build). So, that’s where I added them!
## How it Works
### Generating the Docs
Behind the scenes, Bazel has a [built-in rule `starlark_doc_extract`](https://bazel.build/versions/8.3.0/reference/be/general#starlark_doc_extract), in the Java core code, which runs Bazel’s Starlark interpreter over a given Starlark file. The interpreter is required because `.bzl` files use a standard library which is Bazel-specific and not part of the Starlark language spec, and the documentation needs to be aware of that. It also needs to read from transitively-loaded files in the `deps` of a `bzl_library` target.
As a Starlark author, you don’t really want to think about generating the API docs, it should just work! So writing `bzl_library` in your BUILD files is sufficient - and there’s a Bazel Gazelle extension to write those for you.
The implementation of `bzl_library` in bazel-skylib doesn’t actually do any documentation generation, or even [validation of the inputs](https://github.com/bazelbuild/bazel-skylib/issues/568).
So we have an improved one in bazel-lib. Here’s my first opportunity to link you to the documentation on the BCR: [https://registry.bazel.build/modules/bazel\_lib#-bzl\_library-bzl](https://registry.bazel.build/modules/bazel_lib#-bzl_library-bzl)
At this point, a rule author can `bazel query ‘kind(starlark_doc_extract, //...)’` to see what APIs have their documentation available.
### Publishing the Docs
We want to include these docs in the releases, next to the artifact users download. Most rules use the [Publish-to-BCR](https://github.com/bazel-contrib/publish-to-bcr) workflow or app which require a `release_prep.sh` script, so we can just add a snippet there:
```bash theme={null}
# Add generated API docs to the release
# See https://github.com/bazelbuild/bazel-central-registry/blob/main/docs/stardoc.md
docs="$(mktemp -d)"; targets="$(mktemp)"
bazel --output_base="$docs" query --output=label --output_file="$targets" 'kind("starlark_doc_extract rule", //...)'
bazel --output_base="$docs" build --target_pattern_file="$targets"
tar --create --auto-compress \
--directory "$(bazel --output_base="$docs" info bazel-bin)" \
--file "$GITHUB_WORKSPACE/${ARCHIVE%.tar.gz}.docs.tar.gz" .
```
> Note: the `--output_file` flag was added in Bazel 7.5.0
Next, we want the module's metadata to point to that docs.tar.gz file, by editing the `source.template.json` to include \`"docs\_url": "[https://github.com///releases/download//-.docs.tar.gz](https://github.com/%7BOWNER%7D/%7BREPO%7D/releases/download/%7BTAG%7D/%7BREPO%7D-%7BTAG%7D.docs.tar.gz)",\`
> Note: you need publish-to-bcr version v0.2.3 or greater to pick up [a fix](https://github.com/bazel-contrib/publish-to-bcr/pull/290) for multiple replacements in source.template.json
Now the module just gets published as usual. The docs\_url property points to an archive of all the stardocs in their binaryproto format.
### Rendering the Docs
Google maintains [https://github.com/bazelbuild/stardoc](https://github.com/bazelbuild/stardoc) which is a Java implementation of a Velocity template renderer for stardoc binaryprotos. However the BCR UI is a TypeScript Next.js application. We don’t want to introduce a Java source dependency. Not only that — but look at the “Legacy WORKSPACE setup” on [https://github.com/bazelbuild/stardoc/releases/tag/0.8.0](https://github.com/bazelbuild/stardoc/releases/tag/0.8.0) for an idea of the mass of transitive dependencies it requires at runtime.
As I pointed out in that blog post I linked at the start, we can just use the `@buf/bazel_bazel.bufbuild_es` NPM package to parse the binaryproto’s, so we get a short implementation of data fetching: [https://github.com/bazel-contrib/bcr-ui/blob/main/data/stardoc.ts](https://github.com/bazel-contrib/bcr-ui/blob/main/data/stardoc.ts)
Now that we have the Stardocs in a TypeScript object, we just need a standard React component to render them: [https://github.com/bazel-contrib/bcr-ui/blob/main/components/Stardoc.tsx](https://github.com/bazel-contrib/bcr-ui/blob/main/components/Stardoc.tsx) — this one is a little longer, but it’s mostly written and maintained by AI.
## Come Use It and Contribute!
The BCR-UI project is governed by the Rules Authors SIG under Linux Foundation: [https://github.com/bazel-contrib/bcr-ui](https://github.com/bazel-contrib/bcr-ui)
Your help is greatly appreciated!
* Update a ruleset to publish its docs
* Improve ruleset documentation, like by adding more copy-paste examples
* Suggest usability improvements to the Next.js rendering
# What's new at Aspect: Summer 2023
Source: https://site.aspect.build/blog/summer-2023
Check out Aspect's latest updates: new clients, open-source releases, and upcoming conferences. Improving Bazel workflows for the community.
Since our [last update in December](https://blog.aspect.dev/winter-2022), a lot has been going on at Aspect, and we’re delighted to welcome you back from Summer Vacation! Our kids are going back to school and maybe yours are too. We’re ready to build some great software with Bazel!
Let’s start with some numbers. Our professional services division, [Aspect Development](https://www.aspect.dev/), has now worked for 42 companies, where we've remediated a lot of common problems across the Bazel ecosystem. Our [blog](https://blog.aspect.dev/) has 4,600 monthly visitors, and on our [documentation site](https://aspect.build/docs) we serve 2,400 of you a month. We plan to make these resources even more useful to the entire Bazel community. To start, we’ve added a live chat box on our docsite so you can ask us questions directly from the documentation. We are also offering one [free week of Bazel Open Source Support](https://www.aspect.dev/bazel-open-source-support) to help keep your team unblocked.
We’ve seen strong interest in our products as well. Aspect Workflows is the missing ingredient to make Bazel incredibly fast in your current CI/CD platform, on your cloud. We are now installed at 8 companies. Here's an unscripted response when a customer saw that first 4-second build:
In addition to Buildkite, CircleCI, and GitHub Actions, we now support GitLab. And we’ve added a second cloud provider: Google Cloud. This increases the reach of Aspect Workflows, which you can also see from the live demo of Workflows for our open-source repositories: [https://buildkite.com/aspect](https://buildkite.com/aspect) .
Aspect continues to lead the way in Bazel’s Open Source community. Thanks to funding from the Bazel Rules Authors SIG and the Distroless team at Google, we launched a better alternative to the unmaintained bazelbuild/rules\_docker repo. We released a 1.0 version of rules\_oci in May. There are more details and a meetup talk on our [rules\_oci blog post](https://blog.aspect.dev/rules-oci). [Testimonials from users](https://github.com/bazel-contrib/rules_oci/discussions/299) are now rolling in, and the feedback has been even more positive than we hoped! In the rules\_js ecosystem, we reached a 1.0 release for rollup, terser, jasmine, and swc. We continue to get [strong user testimonials](https://github.com/aspect-build/rules_js/discussions/1000) for rules\_js as well.
A huge thank you to these [financial sponsors](https://opencollective.com/aspect-build) of our Open Source projects:
[Aspect CLI](https://www.aspect.build/cli) is a better frontend for running `bazel` in your terminal. The “configure” command now generates BUILD files for JavaScript/TypeScript and has preliminary support for Kotlin.You can see real-life usage in Sourcegraph’s repo [https://github.com/sourcegraph/sourcegraph](https://github.com/sourcegraph/sourcegraph) - try cloning the repo and run "bazel configure".
That's it for now! We’re looking forward to the fall conference season, where you can come meet us at the [DPE Summit](https://dpesummit.com/) in San Francisco Sep 20-21, [BazelCon](https://conf.bazel.build/) in Munich Oct 24-25, and [PackagingCon](https://packaging-con.org/) in Berlin Oct 26-28.
# Blog Tags — Aspect Build
Source: https://site.aspect.build/blog/tags
Browse the Aspect Build blog by topic: Bazel, rules_js, Gazelle, CI/CD, remote execution, and more.
# AI — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/ai
1 AI post from the Aspect Build engineering blog on Bazel and developer productivity.
# Aspect CLI — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/aspect-cli
6 Aspect CLI posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Bazel — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/bazel
71 Bazel posts from the Aspect Build engineering blog on Bazel and developer productivity.
# CI/CD — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/ci-cd
15 CI/CD posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Company — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/company
10 Company posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Containers & OCI — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/containers-oci
5 Containers & OCI posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Gazelle — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/gazelle
3 Gazelle posts from the Aspect Build engineering blog on Bazel and developer productivity.
# JavaScript — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/javascript
11 JavaScript posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Linting — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/linting
3 Linting posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Migration — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/migration
8 Migration posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Monorepo — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/monorepo
10 Monorepo posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Performance — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/performance
9 Performance posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Python — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/python
3 Python posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Releases — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/releases
13 Releases posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Remote Cache — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/remote-cache
2 Remote Cache posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Remote Execution — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/remote-execution
4 Remote Execution posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Selective Delivery — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/selective-delivery
2 Selective Delivery posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Supply Chain Security — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/supply-chain-security
1 Supply Chain Security post from the Aspect Build engineering blog on Bazel and developer productivity.
# TypeScript — Aspect Build Blog
Source: https://site.aspect.build/blog/tags/typescript
5 TypeScript posts from the Aspect Build engineering blog on Bazel and developer productivity.
# Things a program must not do under Bazel
Source: https://site.aspect.build/blog/things-a-program-must-not-do-under-bazel
Ensure Bazel program compliance: no implicit environment assumptions, write only to output directory, avoid stdin/stdout reliance, and more
Don't expect the environment to have implicit things like tools in the PATH. Bazel builds are meant to be portable so you can invoke them remotely or share cache hits with your coworkers. Be explicit about getting tools you need as inputs, or using Bazel's toolchain feature to locate them on the disk.
Don't rely on the working directory to be next to the inputs. Bazel always sets the working directory in the root of the workspace, next to the WORKSPACE file. (Of course you can always chdir inside the process after Bazel starts it. In NodeJS, for example, you can do this with a [`--require` script](https://github.com/bazelbuild/rules_nodejs/issues/1840#issuecomment-619277667).)
Don't try to write to the sources. The input directory will be in a read-only filesystem by default. For example Angular CLI runs the `ngcc` program which tries to edit `package.json` files in the inputs: [https://github.com/angular/angular/pull/33366](https://github.com/angular/angular/pull/33366)
Don't try to write in a subdirectory of the sources either. Only write to the output directory.
Don't resolve symlinks of the inputs. Node programs often try to read symlinks because of a common pattern of linking build outputs of one package to be dependencies of another. Bazel creates an execroot (for build actions) and a runfiles root (for test/run) filled with symlinks to sources or other outputs. Resolving the symlinks causes non-hermeticity by finding undeclared input files, or causes logic bugs in the program if it compares a symlink target path and expects them to be the same.
Don't rely on stdin/stdout to communicate inputs and outputs. Bazel will sometimes use stdin for a protocol that communicates with the program, like worker mode or ibazel watch mode. It's possible to work around this with a genrule but that introduces a bash dependency.
Don't rely on accepting all the configuration over command line arguments. You will likely run into argv length limits. Consider accepting a configuration file or params file with the CLI flags written into it.
My earlier post about getting Nativescript tools to run under Bazel: [https://medium.com/@Jakeherringbone/running-tools-under-bazel-8aa416e7090c](https://medium.com/@Jakeherringbone/running-tools-under-bazel-8aa416e7090c)
# 10-20x speedup for TypeScript transpilation in Bazel
Source: https://site.aspect.build/blog/typescript-speedup
Achieve 10-20x faster TypeScript transpilation in Bazel using the new `ts_project` with a customizable transpiler like SWC
When I first started working on TypeScript in Bazel, we borrowed Google's approach to running the compiler, called `ts_library`. It's fast, but it comes with a ton of complexity since it needs a custom compiler binary which hacks into a lot of TypeScript's internal APIs. It was also not compatible with a lot of existing code, since it makes assumptions about module formats to work with Google's closure compiler, which almost no one uses.
So when I left Google at the start of 2020, I worked with [OJ Kwon](https://twitter.com/_ojkwon), who I knew from RxJS, to make a new Bazel plugin ("rule") for running TypeScript. This is called `ts_project` and it's just a thin wrapper around the `tsc` binary distributed by the TS team at Microsoft.
We gained a ton of simplicity and compatibility, which has been great to allow us to maintain this tooling with a band of OSS volunteers. However, we lost speed because we start a new, cold `tsc` process for each compilation, rather than using a "watch mode". One of our [OSS contributors](https://github.com/mrmeku) hooked up a watch mode ("persistent worker" in Bazel lingo) but it didn't scale well since a separate compiler had to be warm for each library in the project. As a result, we've never deprecated the `ts_library` rule, and users have been stuck with a choice between a poorly-maintained thing and a slow thing.
I'm happy to report that this problem is fixed!
Amusingly, OJ and I got to meet again, now that he works on [SWC](https://swc.rs). This is a very fast TS -> JS transpiler written in Rust. Its own benchmarks show that it is 20x faster than alternatives. Even better from Bazel's perspective: swc performs just as well when it is "cold", running as a new process, as it does in a "watch mode".
Thus our change to Bazel's `ts_project` is to introduce a `transpiler` attribute where you can choose any tool you like. SWC is a good choice, but you might need to use another tool due to specific transforms your code relies on. For example, many projects use [Babel](https://babeljs.io/).
Note that Bazel will still use `tsc` to type-check the code when requested, however this isn't triggered when you just want to build a JS bundle or run a devserver. We observed most developers had a TypeScript Language Service running in their editor, so they already got the "red squiggles" on their mistakes and don't need the build system to repeat that before running the program. If you'd like, you can still run the checker under Bazel by building the `_typecheck` target (or use a wildcard like `my_package:all`). You'd probably do that only when you're done with a change, and then of course that wildcard is used on CI to ensure code is checked before it's merged.
## Demo
Here is a Bazel config to run `tsc`:
```python theme={null}
load("@npm//@bazel/typescript:index.bzl", "ts_project")
# Uses TypeScript (tsc) for both type-checking and transpilation
# % bazel build tsc
# INFO: Elapsed time: 6.798s, Critical Path: 5.24s
ts_project(
name = "tsc",
srcs = ["big.ts"],
out_dir = "build-tsc",
tsconfig = _TSCONFIG,
)
```
Over 5 seconds is too slow for a project with a single file!
Here's what it looks like to use `swc` instead:
```python theme={null}
load("@aspect_rules_swc//swc:swc.bzl", swc = "swc_rule")
# Runs swc to transpile ts -> js
# and tsc to type-check.
# % bazel build swc
# INFO: Elapsed time: 0.745s, Critical Path: 0.54s
#
# Optionally, or on CI, you can explicitly do the slow type-check:
# $ bazel build swc_typecheck
# INFO: Elapsed time: 3.330s, Critical Path: 3.19s
ts_project(
name = "swc",
transpiler = swc,
srcs = ["big.ts"],
out_dir = "build-swc",
tsconfig = _TSCONFIG,
)
```
That's 10x faster. In real world cases we've seen improvements over 20x.
Just for completeness, here is `babel`:
```python theme={null}
# This is <20 SLOC Bazel macro to adapt the Babel CLI to the ts_project API
load("babel.bzl", "babel")
# Runs babel to transpile ts -> js
# and tsc to type-check
# % bazel build babel
# INFO: Elapsed time: 3.928s, Critical Path: 3.73s
#
# Like the swc example, you could build babel_typecheck to run tsc.
ts_project(
name = "babel",
transpiler = babel,
srcs = ["big.ts"],
out_dir = "build-babel",
tsconfig = _TSCONFIG,
)
```
That's only a mild speed improvement, however it's still an improvement over earlier `ts_project` since you can use the same Bazel config to declare both transpile and type-check for the TS sources.
The full example is here: [https://github.com/aspect-build/bazel-examples/tree/main/ts\_project\_transpiler](https://github.com/aspect-build/bazel-examples/tree/main/ts_project_transpiler)
Thank you to our friends at [EngFlow](https://www.engflow.com/) for sponsoring the OSS work in rules\_nodejs to add this attribute! I hope that after this feature "bakes" for a few weeks, we'll finally be able to deprecate the old `ts_library`.
# Bazel + TypeScript: faster with Remote Execution
Source: https://site.aspect.build/blog/typescript-with-rbe
See how Bazel's remote execution speeds up TypeScript builds by 8.4x on a 10M LOC project. Explore benchmarks and benefits.
This post will show how much faster TypeScript builds can be when using [remote execution](https://bazel.build/remote/rbe), Bazel's unique ability to parallelize transpile and type-check work across a farm of machines. We hope that Bazel 6.0 will include fixes for [symlinks support](https://github.com/aspect-build/rules_js/issues/308), making it possible to use [remote execution](https://bazel.build/remote/rbe) with Aspect's [rules\_ts](https://github.com/aspect-build/rules_ts).
## How much faster?
Using a remote execution cluster of 100 executors provided by our partners at [EngFlow](https://www.engflow.com/), we benchmarked a large TypeScript application with 10M lines of code, representing a large-scale enterprise application, to be **8.4x** faster with remote execution than when building locally on a 16 core MacBook Pro. The build took **2 minutes and 13 seconds** with remote execution vs. **18 minutes 53 seconds** locally. Full benchmark results are found further down in this post.
## Scale Horizontally
With remote execution your build is **no longer bound by the resources on your local or CI machines** making it easy to horizontally scale your build compute. You can now scale your build compute to keep build times fast for even the largest TypeScript code bases by increasing the number of remote executors.
The benchmarks in this post used 100 remote executors. Had we increased the number of remote executors by a factor of 2 we would expect to see a similar 2x reduction in build times. How much you gain from increasing remote execution compute depends only on how wide your build graph is and how many actions can be run in parallel.
## Fixes coming in Bazel core
Why doesn't this work with Bazel 5?
[rules\_ts](https://github.com/aspect-build/rules_ts) is built on top of [rules\_js](https://github.com/aspect-build/rules_js), which uses a [symlinked node\_modules structure](https://pnpm.io/symlinked-node-modules-structure) for linking. This means it inherently relies on [Node.js](https://nodejs.org/en/) tools, such as [TypeScript](https://www.typescriptlang.org/), following symlinks to resolve npm dependencies.
Historically, [Bazel](https://bazel.build/) turned symlink inputs into actual files on remote executors. This made it incompatible with any actions that depend on symlinks when executing, such as [rules\_js](https://github.com/aspect-build/rules_js) actions when resolving transitive npm dependencies.
Thanks to recent work by [Fabian Meumertzheim](https://github.com/fmeum), symlinks are [now supported](https://github.com/bazelbuild/bazel/commit/ca95fecde07a28736ea815ec64bcd639a234d79c) with remote execution in Bazel [5.3.0](https://github.com/bazelbuild/bazel/releases/tag/5.3.0). The last change outstanding for [rules\_js](https://github.com/aspect-build/rules_js) to work with remote execution is currently in review. This is a [fix](https://github.com/fmeum/bazel/commit/a2fdd4ad3963aec805d45ae1664a415bcdf2a3c3#diff-5735787cb0cd600c97d3b645c63e48429f198355b53979b724a61e07683785b2R144) to keep unresolved symlinks relative in the sandbox & the runfiles trees.
> If you want to try remote execution with [rules\_ts](https://github.com/aspect-build/rules_ts) before the last fix lands in Bazel core, follow the instructions [here](https://github.com/aspect-build/rules_js/issues/308#issuecomment-1237499019).
## Developer Productivity and Build Times
To benchmark remote execution with [rules\_ts](https://github.com/aspect-build/rules_ts) we increased the number of actions used in the original rules\_ts benchmarks by 20x so that a full clean build took around 20 minutes locally on a MacBook Pro. This is meant to represent a large-scale enterprise application.
Twenty minutes of waiting on build & test is a representative threshold at which many companies might start to consider bringing remote execution into their Bazel configuration to decrease build & test times and make their developers more productive.
> Long waits on build & test can really kill developer productivity at an organization. At twenty minutes, a developer can only iterate three times an hour at best. When working on a difficult problem, quick iterations are crucial to flow and result in faster & better solutions. At twenty minutes, a developer is likely to context switch to other work rather than waiting for the next results. A developer may also settle on a less than ideal solution just so she can move past a problem rather than continuing to iterate slowly.
## Benchmarks
The benchmarks used for this post were run against a generated TypeScript code base that mimics a large enterprise scale. It has 100 features, 10 modules per feature, 10 components per module and 1001 lines of code per component. This makes for a total of 11,100 TypeScript files containing over **10 million lines** of TypeScript in aggregate. That is a lot of TypeScript code!
For the Bazel build, each module maps to one Bazel target, for a total of 1100 `ts_project/ts_library` targets.
> A timestamp is written to each generated TypeScript source file in this benchmark to intentionally cause cache misses, so that actions are forced to re-run.
### Hardware
These benchmarks were run on a MacBook Pro (16-inch 2019), 2.4 GHz 8-Core Intel Core i9, 64 GB 2667 MHz DDR4 running macOS Monterey 12.5.1
The remote execution cluster was made up of 100 executors on AWS c6i.xlarge instances.
Versions of TypeScript and rule sets used were,
* [TypeScript](https://www.typescriptlang.org/) 4.8.2
* [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) 5.5.0
* [@bazel/typescript](https://www.npmjs.com/package/@bazel/typescript) 5.5.0
* [@bazel/concatjs](https://www.npmjs.com/package/@bazel/concatjs) 5.5.0
* [aspect\_rules\_js](https://github.com/aspect-build/rules_js) 1.1.2
* [aspect\_rules\_ts](https://github.com/aspect-build/rules_ts) 1.0.0-rc2
* [aspect\_rules\_swc](https://github.com/aspect-build/rules_swc) [PR#57](https://github.com/aspect-build/rules_swc/pull/57) (a soon-to-be-landed performance enhancement which uses a new pure rust CLI for [swc](https://swc.rs/))
### GitHub Actions Hosts
We also ran the benchmark on standard [GitHub Actions](https://docs.github.com/en/actions) machines with 2 cores and 7 GB ram. These machines were not powerful enough to run local actions in comparable times or without OOM'ing, so only remote execution actions were benchmarked on [GitHub Actions](https://docs.github.com/en/actions) hosts.
> The ability to run a large Bazel build on relatively small machines is one of the benefits of Bazel remote execution. Instead of allocating CI machines with hundreds of cores, you can instead run your build on very small CI hosts backed by a large auto-scaling remote execution cluster. If tuned well, this configuration can result in significant cost savings on compute.
## Javascript Rule Sets
### rules\_js
[rules\_js](https://github.com/aspect-build/rules_js) is a high-performance and more compatible [spin-off](https://blog.aspect.dev/rules-js) from [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) and is the result everything the maintainers of [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) learned over the years.
[rules\_js](https://github.com/aspect-build/rules_js), which reach [1.0.0](https://blog.aspect.dev/rulesjs-launch) less than a month ago, is already in use by many companies that we've talked to. We've seen a lot of interest on the `#javascript` channel on [Bazel slack](https://bazelbuild.slack.com/) and the rule set already has over 100 stars on GitHub.
Now, [rules\_ts](https://github.com/aspect-build/rules_ts) and [rules\_js](https://github.com/aspect-build/rules_js) finally make remote execution possible in a high-performance Javascript rule set, while taking a more compatible approach to integrating with Node.js tools.
### rules\_nodejs
For historical context, while remote execution was possible in some configurations with [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs), it has never worked well.
Originally, the Node.js toolchain was difficult to use if the host & execution platform did not match. This is the case, for example, if you run Bazel locally on a MacBook but the remote execution cluster uses Linux executors. This issue has now been fixed in the [rules\_nodejs toolchain layer](https://github.com/bazelbuild/rules_nodejs/blob/stable/nodejs/toolchain.bzl), which is shared between [rules\_js](https://github.com/aspect-build/rules_js) and [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) rules.
Next, [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) historically enumerated all files in every npm dependency as individual inputs. This resulted in hundreds of thousands of input files to actions in large projects that noticeably slowed down sandbox and runfiles tree creation. When [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) was updated to use source directories for npm dependency inputs, this resolved the excessive number of inputs, but the optimization was not compatible with remote execution since remote execution does not support source directory inputs.
Finally, [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) was updated to use [declared directories](https://bazel.build/rules/lib/actions#declare_directory) for npm dependencies. This made it compatible with remote execution but the additional overhead of making a directory copy of each npm dependency was a noticeable performance hit to already slow, eager npm dependency fetching & linking and [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) still suffered from all the other problems inherent with its in-action runtime linker.
## Full builds vs. "devserver" builds
In these benchmarks we measure two different scenarios:
1. A full clean build (`bazel build ...`) followed by an incremental `bazel build ...` after making a change to a leaf TypeScript file.
2. A clean "devserver" build (`bazel build :devserver`), which emulates a typical developer workflow of building while running a tool such as a devserver, followed by an incremental `bazel build :devserver` after making a change to a leaf TypeScript file.
The "devserver" scenario is an important measure that emulates the typical local development workflow of coding while running tools such as a devserver or a test runner such as jest. These tools are often run in watch mode while making changes to source code. The faster build times are on changes the shorter the round-trip-time is to get feedback on those changes.
Ideal build times to maximize developer productivity are less than 1 second on changes to leaf nodes and less than 10 seconds on changes that affect large parts of the graph. With a good dependency graph, these ideal times can be preserved even as the project grows.
## ts\_project vs. ts\_library
`ts_project` was originally developed in [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) as an alternative to `ts_library` to provide a cleaner API better suited for the many ways TypeScript is used outside of Google. While the API was better suited for the wild, it could not compete with `ts_library`, a heavily optimized and deeply integrated wrapper around the TypeScript compiler, on performance.
The new `ts_project` from [rules\_ts](https://github.com/aspect-build/rules_ts) has significantly reduced the performance gap with `ts_library` by adding first-class support for Bazel workers and now support for remote execution. We'll refer to `ts_project` from [rules\_ts](https://github.com/aspect-build/rules_ts) as simply `ts_project` in this blog post. The original `ts_project` from [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) we'll refer to as the [@bazel/typescript](https://www.npmjs.com/package/@bazel/typescript) `ts_project`.
> [rules\_js](https://github.com/aspect-build/rules_js), which [rules\_ts](https://github.com/aspect-build/rules_ts) is layered on, made first-class worker support in [rules\_ts](https://github.com/aspect-build/rules_ts) possible by doing away with the dynamic runtime node\_modules linking that [rules\_nodejs](https://github.com/bazelbuild/rules_nodejs) uses.
In these benchmarks, we'll measure both `ts_project` rules configured with [swc](https://swc.rs/) as the transpiler. [swc](https://swc.rs/) is an order of magnitude faster that TypeScript for pure transpilation but it does not type-check, so TypeScript is still used for type checking in this split configuration.
The split configuration also removes type checking from the build graph for devserver and test targets, so only transpilation is needed to build them, reducing the round-trip-time on changes when running such targets by an order of magnitude. Type checking is handled in separate targets that can be run explicitly or with the catch-all `bazel build ...`.
## Results
Here are the results of the benchmarks.
### Full clean build
#### Fastest: ts\_project + swc with remote execution
The fastest full clean build we were observed is on the MacBook Pro host with `ts_project` + [swc](https://swc.rs/) and remote execution. This full transpile & type-check of 10M lines of TypeScript code took just 2 minutes and 13 seconds, **8.4x faster** times faster than the equivalent build without remote execution running on the MacBook host, which took 18 minutes 35.
> In the `ts_project` + [swc](https://swc.rs/) configuration, we ran the 11,100 [swc](https://swc.rs/) transpile actions (one per TypeScript file) locally on the 16 MacBook Pro cores while the remote execution cluster ran the TypeScript type-check and declaration file emit actions. Transpiling with [swc](https://swc.rs/) is so fast and short-lived that running locally is faster than using remote execution due to network latency and overhead of uploading inputs and downloading outputs.
#### Runners-up: ts\_project & ts\_library with remote execution on GitHub actions host
Tied for 2nd fastest builds are `ts_project` (without [swc](https://swc.rs/)) with remote execution on a standard [GitHub Actions](https://docs.github.com/en/actions) 2 core host and `ts_library` with remote execution on the same host.
The `ts_project` build took only 2 minutes and 29 seconds, **7.5x faster** than the equivalent build without remote execution running on the MacBook host, which took 18 minutes and 45 seconds. The `ts_library` build was virtually identical at 2 minutes and 31 seconds.
> Running on [GitHub Actions](https://docs.github.com/en/actions), with all actions executing remotely, was faster than running the same configuration on the MacBook host, which took 3 minutes and 2 seconds. The faster build times on GitHub actions were due to the superior network connection on GitHub actions machines compared to the MacBook host running in my home office. TL;DR is that when you're using remote execution, your uplink speed to the remote execution cluster matters.
### Incremental full builds
Interestingly, `ts_library` has the best incremental full build time at 4.1s with its heavily optimized workers. [@bazel/typescript](https://www.npmjs.com/package/@bazel/typescript) `ts_project` with [swc](https://swc.rs/) is runner up at 7.5s. `ts_project` with [swc](https://swc.rs/) was a close third taking 8.0s. In the smaller orginal rules\_ts benchmark `ts_project` with [swc](https://swc.rs/) was 2nd fastest after `ts_library`.
Incremental builds with remote execution were slower than local in this scenario as there were not many actions to run and the network overhead of using remote execution made the overall time slower.
> tsc is quite a bit slower than every other configuration in this benchmark for incremental builds. It is configured as a single project. In a real world scenario, you would likely split it up to multiple invocations and use TypeScript project references between them which may result in faster build times.
### Clean "devserver" builds
As with the original rules\_ts benchmark, clean "devserver" transpile-only builds are fastest when the build is configured to use [swc](https://swc.rs/) as local actions.
`ts_project` + [swc](https://swc.rs/) was observed to take just 47 seconds. With [@bazel/typescript](https://www.npmjs.com/package/@bazel/typescript) `ts_project`, which can also be configured to use [swc](https://swc.rs/), the time was measured was comparable at 57s.
Running [swc](https://swc.rs/) actions on remote executors is a de-optimization since the added time of network latency and upload/download time adds more overhead than the benefit of more executors when on the MacBook Pro host with 16 cores.
### Incremental "devserver" builds
Like clean "devserver" builds, the most performant incremental "devserver" builds are the ones that use [swc](https://swc.rs/) for transpilation with local actions. Both `ts_project` rules with [swc](https://swc.rs/) clocked around 2 seconds. `ts_library` without remote execution was comparably fast at 3.2s.
Remote execution for incremental "devserver" builds was slower than for local builds due to the additional network lately and upload/download time.
## The Bottom Line
Remote execution with [rules\_ts](https://github.com/aspect-build/rules_ts) on large TypeScript projects can make an order magnitude impact on large build times. Your developers will be more productive and thank you when their wait time for large builds is reduced by 10x or more. As your project grows, remote execution makes it possible to easily scale your build compute horizontally to keep large builds fast.
For very fast incremental builds with few actions, however, developers may get faster builds times by keeping actions running locally depending on how fast their connection is to the remote execution cluster. Cloud hosted development environments may solve this discrepancy in the future since the developer machines can be located very close to the remote execution cluster to keep network overhead at a minimum.
# Versioning releases from a monorepo
Source: https://site.aspect.build/blog/versioning-releases-from-a-monorepo
Guide to versioning releases from a monorepo, exploring trunk-based development and monoversion strategies for streamlined software development.
Moving code into a monorepo is just the beginning of a transition. Ideally, each team gives up control and responsibility for governing their development process, and in exchange they get a consistent, centrally-supported experience where they are ultimately glad they don't have to do that "DevInfra" work anymore and can focus on product. Their management is happy too, and they spread monorepo enthusiasm through the organization.
"Monorepo" sounds like a version control concern, but I've written before about things that become "mono" in addition to the git repository, such as the CI build ([https://blog.aspect.dev/monorepo-shared-green](https://blog.aspect.dev/monorepo-shared-green)). Today is a simpler topic: how to apply a version to the artifacts you release from a monorepo.
## Trunk-based development
Teams usually arrive in the monorepo with their prior workflow of tagging their own releases. Their version scheme is specific to their project, and may include Semantic Versioning. Their first-party dependencies come from other repos in the organization and have their own versions, which are handled exactly as if they were third-party packages from the internet. When both ends of such a dependency edge live together in the same repo, this causes a strange time-travel effect: an application released at a given commit depends on a library in `../some-lib` at a version many commits in the past. When browsing the source repo, you can't just follow the `../some-lib` link to reason about how the application interacts with the library. And the developers of `some-lib` make releases without knowing if they will work with applications at HEAD. This is like branching, even if there's never a release branch for some-lib, and eventually there is "merge hell".
There's also a performance penalty in Git when you have a large number of tags on the repo. Even non-tag-related operations like `git status` slow down. We can end up with a classic "[tragedy of the commons](https://en.wikipedia.org/wiki/Tragedy_of_the_commons)" if developers keep cutting releases of their libraries and apps by pushing tags to the shared remote. Someone at Stripe knows all the details; if you'd like them just let me know.
The solution to this is sometimes called "trunk-based development", which means that dependencies should be at HEAD. When the last of a library's dependents moves into the monorepo, no one needs to reference that library by a version, and it can stop doing versioned releases altogether. What should it do instead?
## Monoversion
The maintainers of the monorepo (e.g. a DevInfra team) can offer a single version across the entire monorepo. You won't convince every team to change their ways, but over time you can make this experience so simple and effective that you overcome change aversion.
There are two options to choose from:
1. **Automated**. Every commit in the monorepo gets a version.
2. **Manual**. Someone still chooses a tag to apply at a commit and pushes it.
## Automated
We'll make versions that look like
```plaintext theme={null}
2020.44.123+abc1234
[year].[week].[# commits so far that week]+[git SHA]
```
Benefits:
* We can still have a version that "looks like semver", which means that any tools which parse version numbers will work. For example your monitoring software will still show that your app started to crash-loop only after the new release was deployed.
* We can make it easy to reason about "when is this release from" and "which of these two versions is later".
* The short git SHA still appears in the ["build metadata" field](https://semver.org/#spec-item-10) so you can navigate to the sources as they existing when the release was built.
* You can cut a release from anywhere without having to push commits or tags to the repo.
* Avoids proliferation of refs that slow down git.
Downsides:
* This isn't semantic versioning: we don't attempt to indicate what's a breaking change. We assume that it's infeasible to coordinate development across the monorepo to force teams to operate on a common cadence.
How to do it:
1. Tag the repo at the beginning of each week. If you use GitHub, the easy way is to drop a `.github/workflows/weekly-tag.yaml` file that just calls an API endpoint to add the tag. (This avoids the expense of cloning the repo in order to use `git push`). Here's my solution: [https://gist.github.com/alexeagle/ad3f1f4f90a5394a866bbb3a3b8d1de9](https://gist.github.com/alexeagle/ad3f1f4f90a5394a866bbb3a3b8d1de9)
2. Use this command to determine the version:
`git describe --long --match="[0-9][0-9][0-9][0-9].[0-9][0-9]" | sed -e 's/-/./;s/-g/+/'`
If you use Bazel, you can tuck this into your `workspace_status_command` and then Bazel's stamping feature will always put the right value in your `--stamp`ed build outputs. Also note that the build metadata (the plus character and following bits) aren't legal in Docker tags, so you might need to use a hyphen instead. This is technically indicating a pre-release, per the spec: [https://semver.org/#spec-item-9](https://semver.org/#spec-item-9)
## Manual
You might still have a reason to tag releases yourself. For example, you may be shipping a product that your users will expect to follow Semantic Versioning.
Downsides:
* You have to coordinate features and breaking changes across the entire repository, so that you follow Semantic Versioning.
* You have to tag the repo, which requires knowing what tag to apply (bump the major/minor/patch?) and for users to have write permission.
How to do it:
We use [https://github.com/choffmeister/git-describe-semver](https://github.com/choffmeister/git-describe-semver) in Aspect's monorepo. You just need to install that package and use it when cutting a release. We wrapped it with a Bash script ([https://gist.github.com/alexeagle/041f116ecb576aed7bc87c44120a0c9c](https://gist.github.com/alexeagle/041f116ecb576aed7bc87c44120a0c9c)) so that Bazel's workspace\_status\_command can call it without making users install it themselves.
# What is a build system and what is CI?
Source: https://site.aspect.build/blog/what-is-a-build-system-and-what-is-ci
Discover the essence of build systems and CI in software development, unraveling their interconnected roles and impact on development processes
For a long time, I thought I knew the answer to that question. A build system understands your software, how to build and test it. And a CI is a loop that runs the build system on a server.
When I was the tech lead for Angular CLI, I asked a lot of our big corporate users "what build system do you currently use" and the most common response was "Jenkins". Of course with my preconception of what these terms mean, I thought they were just wrong.
It turns out they were right, because they turned Jenkins into the build system. They probably started in the small scale with a build system for the frontend code (let's say npm scripts) and a build system for the backend (let's say Maven), and at that time Jenkins would have run these independently. As things got complex and interconnected, they'd need integration tests, so no surprise, the one common place these could be added is using some Groovy code or a plugin to Jenkins (There should be a software aphorism "any tool with sufficient adoption grows a plugin ecosystem, thereby rendering it redundant with tools it should have complimented".)
Now they created a build system you can't run locally on your machine, only in the CI environment, and kinda ruined CI for everyone. Now engineers have to wait forever to go through a CI loop to get something green, because it's too hard to reproduce the failure locally to fix it. It's sad, but understandable the way this evolved.
There are build systems which are meant to generalize across the stack and are locally-reproducable ([Bazel](http://bazel.build) of course) - but what I've learned is that to sell that solution, you have to frame it as replacing CI, not replacing the build system. After switching to Bazel, you actually have a "CI" you can run locally on your machine, with a server that runs that thing in a loop. And you try not to get too hung up on how the term "CI" lost all its meaning in the process.
# Happy Holidays from Aspect: Winter 2022
Source: https://site.aspect.build/blog/winter-2022
Aspect reflects on a busy 2022: 20+ companies consulted, Aspect Workflows in Alpha, Aspect CLI 1.0 launch, and rules_js rollouts.
We've been very busy this year! Before we all take a well-deserved winter break, here's a wrap-up of our announcements from the last few months.
Let's start with the numbers to close out 2022. We've consulted for more than 20 companies now! It's been enlightening to see so much variability in DevInfra approaches to Bazel, but also how many problems are common. We've spread the knowledge around on our blog, which has 4,200 monthly views. Working in the OSS community has always been our passion, so we have operated the documentation site for Bazel rules at [docs.aspect.build](https://aspect.build/docs) for over 2,000 monthly active users. The community of Bazel users is now estimated at 600 companies and 50,000 engineers.
As Bazel consultants for the last two years, we've helped many of our clients achieve the promised benefits of Bazel on CI by solving the most common problems, such as keeping build agents warm, tuning cloud deployment parameters, and avoiding cache misses. Now, instead of paying our hourly rate to make your CI hum, we offer this as a turn-key, infrastructure-as-code product. At Bazelcon in November, we announced **Aspect Workflows** reached Alpha, and we've rolled it out to our first customer. They saw an immediate 70% drop in overall CI times, compared with their prior ephemeral, cold-start Bazel solution. Visit [aspect.build/workflows](https://aspect.build/workflows) for more details and to book a demo.
**Aspect CLI** is now 1.0! This is an open-source wrapper for Bazel that does so much more than Bazelisk. We pour consulting wisdom into this tool to reduce the support burden for us (and your DevX team) when product engineers trip over Bazel's usability issues. It also has a rich, gRPC-based plugin API, so you can integrate Bazel into your unique developer workflows. You can try it right now, starting at [aspect.build/cli](https://aspect.build/cli).
We have a long history with JavaScript and Node.js under Bazel. Thanks to a breakthrough new approach, we launched a game-changing new plugin for Bazel: [rules\_js](https://www.aspect.build/rules). In December we've been busy migrating three large client codebases to use it and have tuned our tooling and approaches. We can do your migration at a fraction of the cost it would take for full-time engineers on staff. Book with us at [aspect.dev](https://aspect.dev)
Speaking of professional services, with Bazel 6.0.0 about to land, we're also your best resource for upgrading to the **new Bazel module manager**, Bzlmod. A year ago, we were the first to write about it: [https://blog.aspect.dev/bzlmod](https://blog.aspect.dev/bzlmod). Our engineers partnered with the core Bazel team at Google to write infrastructure that [automatically mirrors new releases](https://github.com/bazel-contrib/publish-to-bcr) to the Bazel Central Registry and we added bzlmod support for JavaScript and Python. We can bring this expertise to your project and finally eliminate the maintenance nightmare of the `WORKSPACE` file.
What's coming in 2023? We are excited to see more Bazel adoption and happier responses in developer productivity surveys.
# Get in Touch with Aspect Build
Source: https://site.aspect.build/contact
Have questions about Bazel migration, build optimization, or Aspect Build's tools? Contact us today to connect with our experts and improve your development process.
How can we help?
Connect with our team to see how we can help your organization deliver software faster, at scale.
Accelerate your team's delivery
Build confidently with Aspect. Reduce time and complexity to operate Bazel at scale.
✓
Aspect Workflows
The developer productivity platform for Bazel.
✓
Bazel Support
Expert assistance and consulting.
Try Aspect Workflows for Free
30-day free trial on a dedicated, single-tenant deployment.
Just want the free tools?
The Aspect CLI, AXL, and Marvin are free for every developer. No trial needed.
# Aspect Build Customers
Source: https://site.aspect.build/customers
See how teams use Aspect Build to improve Bazel performance, simplify migrations, and optimize their development workflows.
Accelerating AI Robot Development: Physical Intelligence's Success with Aspect Workflows
18x
Faster robot code deployment
7%
Reduced CI costs despite 2.6x build growth
Read the case study →
Sourcegraph is fully relying on Aspect Workflows in their CI/CD pipeline
3.7x
More sub-2min builds
2.4x
Speedup of median build & test
40%
Reduced compute costs
Read the case study →
Optimizing Build and Test Times: Coda's Success with Aspect
10x
Faster no-op build, 11 min to 1 min
2-3x
Speedup of typical build & test
67%
Reduced compute costs
Read the case study →
# AssemblyAI's Success with Aspect Workflows
Source: https://site.aspect.build/customers/assemblyai
AssemblyAI, a leader in AI-driven speech-to-text, cut CI compute costs 10x and decreased developer wait time by 60% with a Bazel-based Python monorepo and self-hosted Aspect Workflows.
AssemblyAI's Success with Aspect Workflows
10x
Reduced compute costs
AssemblyAI provides advanced AI-driven speech-to-text services. They offer tools for audio
transcription, sentiment analysis, topic detection, and other leading capabilities. They are known for
high accuracy, scalability, and easy API integrations.
AssemblyAI enables businesses to rapidly build voice-based applications. Needless to say, keeping their
builds fast and their engineers moving is critical for providing world-class products to their
customers.
Challenges:
AssemblyAI's Bazel story began in early 2022, when they were using over 250 source repositories, many
of which were branched from the history of others. The resulting forks diverged, preventing
improvements or bugfixes from being applied consistently. New and old engineers alike struggled to find
the correct source code. Releases often took over a week and rollbacks were commonplace. Even versions
of libraries such as ffmpeg would vary between a researcher's machine where model training occurred and
the production deployment, resulting in subtle transcoding bugs.
A monorepo existed at this time, but it was not consistent. Top-level folders had their own tooling
choices. Dependencies and common approaches were not shared between them. AssemblyAI's team evaluated
Bazel, Pants, and Buck2. The Head of Technology chose to start a revived monorepo effort with Bazel
because of its high level of community support and his familiarity with the core team.
Solutions:
AssemblyAI contacted Aspect in December 2022. They were seeking help with executing py\_binary via
gunicorn in a Python docker image. During a brief consulting engagement, Aspect quickly resolved
AssemblyAI's issue and then migrated the company to a new monorepo. Backed by industry-standard SLAs,
we supported AssemblyAI engineers in a shared Slack channel.
AssemblyAI became interested in our self-hosted Workflows product to speed up their Continuous
Integration. They were focused on support for Github Actions and custom security groups along with
optimizing AI model packaging, training, and inference workflows. AssemblyAI's trial began in April
2023\.
The trial was evaluated on the following criteria:
- Availability and scalability of Aspect's CI runners
- High action cache hit rate and low analysis cache discard rate (based on provided Grafana dashboards)
- Cost effectiveness
- Selectively stamping and delivering only changed artifacts
- Auditable back-references from artifact to the monorepo state where it was produced
- Alerting on-call engineers to CI breakage via our "buildcop" service
- Providing enough information to enable Continuous Deployment
Results:
Deployments sped up with fewer rollbacks, new engineers onboarded faster, and AI researchers and
engineers improved collaboration by working together in a shared repository.
AssemblyAI saw significant improvements in build consistency. Optimized dependencies led to fast and
cost-effective AI training and inference cycles. Developer CI wait time decreased by 60%. The cost to
operate CI plummeted; the team estimates a 10x reduction. During one week in particular, AssemblyAI's
legacy runners cost \$600 while the self-hosted Aspect Workflows runners cost \$5.
Why It Worked:
In a machine learning environment, it is critical for dependencies to be consistent between researchers
conducting training and production systems performing inference. A Bazel-based Python monorepo with
self-hosted Aspect Workflows ensures that AssemblyAI reliably manages dependency versions, leading to
better performance and reduced overhead.
However, our proudest achievement was receiving an email from a former AssemblyAI engineer asking us to
set up Workflows at his new startup!
# Optimizing Build and Test Times: Coda's Success with Aspect
Source: https://site.aspect.build/customers/coda
Coda reduced CI build times by 10x and cut compute costs by 67% with Aspect Workflows, streamlining their development process and boosting efficiency.
Optimizing Build and Test Times: Coda's Success with Aspect
10x
Faster no-op build from 11 to 1 min
2-3x
Speedup of typical build & test
67%
Reduced compute costs
Challenges Faced
After adopting Bazel, Coda had a powerful tool for scaling up the size of their TypeScript codebase,
and spent some effort resolving cycles in the dependency graph to improve incrementality.
However, the build and test process on CircleCI were consistently exceeding 10 minutes on average,
often stretching beyond 30 minutes. They were spending the equivalent of two software engineer salaries
to cover the cloud costs required to run their build and test processes.
The prolonged build and test times were hindering Coda's development efficiency, causing delays in
project timelines, and impacting overall productivity. The challenge was to find a solution that could
significantly reduce these times and streamline their continuous integration process.
Selection Process
After careful consideration, Coda selected Aspect Workflows, developed by Aspect, as the solution that
met their criteria for efficiency, scalability, and ease of integration. One of the key criteria was to
remain on their existing CI system (CircleCI). The decision to use Workflows was influenced by their
experience having worked at Google.
Implementation
The implementation process was a testament to the user-friendly nature of Aspect Workflows. Coda
reported that adopting our product was "a pretty smooth transition." This speaks to the ease of
onboarding and the effectiveness of Aspect Workflows in aligning with Coda's existing workflows.
Results and Benefits
The impact of implementing Aspect Workflows was substantial. After the implementation, Coda achieved
not only a boost in development efficiency but also a remarkable cost savings in CI cloud spend.
Customer Case Study
Challenge
- Bazel build & test on CircleCI taking over 10 min average, often over 30 min.
- Cost too high: the equivalent of two software engineer salaries
Solution
- Aspect Workflows trial for one month.
- Improve stability and uptime, evaluate spot instances.
- "Pretty smooth transition" for developers.
- Regular improvement through monthly releases.
Results
- 10x faster no-op build from 11 min to 1 min
- 2-3x speedup of typical build & test
- 67% reduced compute cost (despite higher usage)
We went from having significant limits in CI and tools to where the limits are now just due to our code
The implementation of Aspect Workflows yielded tangible and impressive results for Coda. Their no-op
fully-cached build, which previously took 11 minutes, was optimized to an impressive 1-minute duration.
The typical build and test cycle at Coda, representing a critical component of their development
workflow, saw a remarkable improvement, now operating 2-3 times faster than before. This acceleration
has significantly increased the team's agility and ability to iterate swiftly.
Notably, Coda achieved a remarkable 67% reduction in compute costs. This is despite the codebase
growing during this period, so the workload increased. This cost efficiency demonstrates the
effectiveness of Aspect Workflows in not only enhancing performance but also in optimizing resource
utilization. This reduction in compute costs allows Coda to allocate resources more strategically,
contributing to a more cost-effective and streamlined development process.
Notably, the 10x speedup in build and 2-3x faster tests exactly match the user testimonial presented in
the Bazel 1.0 blog post.
Client's Perspective
-
The optimization journey at Coda provides valuable insights into their historical and ongoing efforts
to enhance their software development pipeline. In 2018/2019, the introduction of Bazel initiated a
gradual process of improving build and test. By January 2022, about half of the code base could be
compiled under Bazel.
-
Additional targets and tests were added to address scaling issues. The team considered Bazel a
natural fit based on their experiences with using it internally at Google. However, limited
familiarity with JS/TS Bazel integrations presented a learning curve, and efforts were made part-time
until a year ago.
-
The contributor investigated alternatives to Bazel but found none that met Coda's specific needs.
The team, not married to Bazel, remains open to exploration but faces challenges due to their
existing setup with Webpack and plugins. Aspect plans to provide better off-ramps for Webpack that
leverage Bazel's ability to serve as the build orchestration.
-
As long as Bazel and the TypeScript ecosystem provide sufficient value, Coda plans to continue using
them. Ongoing evaluations will be crucial to ensuring the selected tool remains the best fit.
# Accelerating AI Robot Development: Physical Intelligence's Success with Aspect Workflows
Source: https://site.aspect.build/customers/physical-intelligence
AI robotics leader Physical Intelligence reduced production code time-to-robot from 1.5 hours to 5 minutes with Aspect Workflows, while cutting CI costs 7% despite a 2.6x increase in build files.
Accelerating AI Robot Development: Physical Intelligence's Success with Aspect Workflows
18x
Faster Robot Production Code Deployment
7%
Reduced CI Costs Despite 2.6x Build File Increase
Founded in 2024, Physical Intelligence is a startup dedicated to building general-purpose AI for robots.
In November 2024, the company announced it had raised \$400 million in early-stage funding from OpenAI,
Thrive Capital, Lux Capital, and Jeff Bezos. Their mission is to develop software that can run on any
robot. To achieve this, they are creating foundational models and learning algorithms to power both
current and future generations of robots.
Background
Jimmy Tanner, Senior Software Engineer at Physical Intelligence, first connected with Aspect in
September 2024. His team had chosen Bazel for their infrastructure and they were seeking expert guidance
and support. They were also exploring options for hosted Remote Build Execution (RBE). Their
multi-language codebase included Python, TypeScript, and C/C++, and they were using GitHub Actions for
CI along with Docker for containerization.
Even with a few ex-Googlers on their team, Physical Intelligence recognized Bazel's complexity and
sought expert guidance from the outset to avoid costly technical debt. In addition to implementation
support, they were also interested in experiencing the benefits of Aspect's platform firsthand.
After evaluating Aspect alongside BuildBuddy and other solutions, they agreed to a 30-day trial.
Challenges
Physical Intelligence aimed to transition their robot development pipeline to Bazel to achieve faster,
more efficient workflows. Their objectives were twofold:
- Migrate their Continuous Integration (CI) pipeline to Bazel for significantly faster execution.
-
Shift their robot builds, primarily Python binaries packaged in Docker containers, to Bazel for
smaller, more hermetic builds with improved cache hits and faster deployment.
Their starting point was a monorepo that was only partially integrated with Bazel. While the team had
begun onboarding to Bazel and setting up its structure, their robot systems operated entirely outside
of Bazel. In CI, Bazel was used experimentally with a remote cache, but the tests were optional and run
for comparison, not yet fully replacing their Python pytest and GitHub Actions setup. CI times averaged
15 minutes, slowed by heavy load phases that diminished cache benefits. Robot builds were similarly
inefficient, with build times on GitHub Actions' larger runners averaging 10 minutes due to repeated
downloading of external dependencies. A subset of low-level C++ hardware code was built with Bazel into
binaries, which were then copied into Docker containers alongside Python code managed outside Bazel.
Unifying these disparate processes under Bazel was a key goal.
The prolonged time from pull request (PR) to code deployment on robots hindered rapid iteration cycles
critical for researchers. Additionally, GitHub Actions costs, approximately \$12,000 per month,
highlighted considerable inefficiencies due to the lack of a Bazel-based workflow. The team explored
setting up their own runners but found Aspect Workflows to be an easier to use "out of the box"
solution. Their goals were to reduce CI and build times, decrease workflow costs, and avoid trade-offs
between speed and expense.
During the 30-day trial with Aspect, the focus was on fully migrating CI and robot builds to Bazel to
achieve these performance gains. The team also aimed to move away from Docker to rules\_oci to create
container images more directly, particularly for complex CUDA-based images required for GPU-enabled
robot components. While they could build basic images, they needed expert guidance for advanced
configurations. Aspect believed that by onboarding the team to Bazel and leveraging Aspect Workflows,
Physical Intelligence would achieve a strong return on investment, with faster builds and reduced costs
effectively paying for the solution itself.
Results
By the end of the trial, Physical Intelligence successfully migrated their Continuous Integration (CI)
pipeline and robot builds to Bazel, achieving significant performance improvements despite a
substantial increase in build complexity. With Aspect's expert guidance, the team transitioned their
Docker containers to rules\_oci, enabling faster and more direct creation of container images. When the
collaboration began in November, Physical Intelligence was in a Bazel dark launch, with a repository
containing 138 BUILD.bazel files. By the trial's end, this number had grown to 364, reflecting a more
than twofold increase in build complexity. Despite this, median build times for the main branch
improved from 10-15 minutes to 5.5 minutes, and the entire pipeline median reached approximately 10
minutes. This represents a significant efficiency gain given the expanded build graph. Notably, prior
to the collaboration, Physical Intelligence had no builds completing in under 2 minutes; in the last
month of the trial, 21 such builds were recorded, showcasing Aspect's impact on performance.
A key benefit, as highlighted by Jimmy Tanner, was the creation of a bespoke Docker build process that
optimized every step of the build, push, and deployment workflow. This reduced the "time-to-robot" for
production code from 90 minutes to 5 minutes, enabling Physical Intelligence to deploy productionized
code to most robots. Previously, the 90-minute delay was intolerable, leading researchers to
self-deploy code to robots manually using git. This improvement transformed developer iteration cycles,
aligning with the team's priority of maximizing speed.
Aspect Workflows reduced GitHub Actions costs by 7% despite heightened CI usage and a more than twofold
increase in build files (from 138 to 364). This cost savings, achieved despite increased complexity,
highlights substantial efficiency gains. Jimmy Tanner noted that the team currently values developer
iteration speed most, though additional cost savings could be achieved through further build
optimizations. He praised the collaboration: "Aspect Workflows has made Bazel an order of magnitude
easier to adopt and significantly more valuable for our team." The trial's success fostered an ongoing
partnership between Physical Intelligence and Aspect, with both teams satisfied with the outcomes and
committed to continued collaboration.
# Sourcegraph is fully relying on Aspect Workflows in their CI/CD pipeline.
Source: https://site.aspect.build/customers/sourcegraph
Aspect Workflows helped Sourcegraph accelerate CI builds by 2-3x and cut cloud compute costs by 40%, enhancing performance and efficiency.
Sourcegraph is fully relying on Aspect Workflows in their CI/CD pipeline.
3.7x
More sub-2min builds
2.4x
Speedup of median build & test
40%
Reduced compute costs
Sourcegraph is a code intelligence platform known for their AI coding assistant, Cody.
Their software is developed in an Open-Source repository by a large team of engineers, primarily in Go,
TypeScript, and Rust.
They were building \~50 different docker container images, which were not optimized. Optimizing them
individually was considered less attractive than using a build system that naturally produces correct
images.
The build for the frontend client bundle was non-incremental and rebuilt on every change. This was a
big pain point and made CI very slow. It was also flaky.
Aspect Build Systems is a Bazel product partner and provides a monorepo developer platform called
Aspect Workflows. By adopting Workflows Sourcegraph was able to resolve these problems, making CI 2-3x
faster while reducing cloud compute costs by 40%.
History
An ex-Googler at Sourcegraph wrote a small internal position paper advocating for a move to Bazel, the
open-source Build & Test tool from Google. His experience in Google's monorepo with a giant Go and
React app convinced him and the team that "it really works."
Sourcegraph was confident in migrating Go and Rust code to build with Bazel, but the frontend code was
perceived as a risk due to limited resources and the complexity of the code and the migration path.
This led them to work with Aspect, the author of Bazel's JavaScript rules.
Aspect and Sourcegraph began working together in November 2022, as Sourcegraph started their Bazel
migration journey. On December 2, the setup began:
Migrating to Bazel
Bazel adoption is complex. This is partially due to the inherent difficulty in migrating build systems,
which are deeply integrated with the codebase. Also, Bazel is known for a steep learning curve and
limited available expertise. Aspect is recognized as the community leader and provides professional
services. In this case we began with hourly consulting.
The work was divided into two phases to mitigate risk. The first was a "Frontend Bazel POC". We agreed
on the following deliverables:
- Prove that Bazel is a viable and performant build system for Sourcegraph's frontend code.
- Migrate a single React Frontend app to Bazel, using Webpack for bundling.
- Existing Jest tests run under Bazel.
- Demonstrate use of the Aspect CLI to generate BUILD files for TypeScript sources.
- Demonstrate a CI build that is incremental, fast, and robust to network failures.
Following the success of the POC phase, we entered a second phase: "rules\_js Bazel migration", scoped
to include:
- Fine-grained SASS and Postcss build targets
- Complete @sourcegraph/web bundle
- Mocha integration tests
- Review and optimize Sourcegraph's golang Bazel configuration
- Documentation & Handoff
While Backend engineers were accustomed to maintaining Makefiles, BUILD file generation for JavaScript
and TypeScript was particularly critical so that frontend engineers don't have a new task of manually
configuring Bazel as they change source files.
This is integrated into Aspect's CLI tool.
Aspect Workflows
Meanwhile, in February 2023, Aspect presented a demo of our turnkey CI/CD solution, Aspect Workflows.
The first reaction from engineers on the call:
If we showed this to all engineers at Sourcegraph there would be mutiny if we didn't buy it.
Aspect provided a free trial of Workflows, giving Sourcegraph time to establish target Key Performance
Indicators:
- Simple PRs should spend under 2 min in build & test.
- Median (50%ile) build\&test should be 2-3x faster.
- The cost for CI compute should be significantly reduced.
Aspect started performing the install for Workflows in July. As part of the trial, we provided guidance
to Sourcegraph with graph optimization, non-determinism fixes, and build-without-the-bytes. These all
improved the codebase, reducing the workload needed for Bazel.
By November we were reporting excellent results. Even Quinn, Sourcegraph's CEO "warmed up" to Bazel:
I'm really warming up to Bazel after we started using it for our monorepo at Sourcegraph. I trusted our
team when they decided to set it up, but only recently did I really become a believer. I trust that I
can run any build/test steps in a reproducible manner.
To calculate the return on investment, Sourcegraph ran Aspect Workflows side-by-side with the legacy
Bazel build. During the period from November 27 to December 18 2023:
-
3.7 times as many builds ran in under 2 minutes (172 of build\&test jobs (12%) on the legacy
build, compared to 649 (44%) on Aspect Workflows)
-
Google Compute Engine costs were reduced 40% (December cost for legacy build \$4607, Aspect Workflows
\$2810)
-
Median (50%ile) build\&test was 2.4x faster (12 minutes on legacy build, 5 minutes on Aspect
Workflows)
Anecdotally, engineers reported getting their fastest build on main with a 1 minute Bazel test of the
whole repo, thanks to a high cache hit rate. This is the first time engineers on the team have
experienced such fast builds!
Conclusion
Aspect Workflows delivered on the promise to make builds significantly faster, while also reducing
compute costs.
Workflows also enabled a couple of key features. The Continuous Delivery pipeline pushes dozens of
artifacts for each green main build, but only those which were modified. Also, we augmented the
Buildkite user interface with annotations showing real-time test results from Bazel's Build Events
stream, so that engineers don't need to wait for the build to complete before learning of a problem.
As of March 2024, Sourcegraph is fully relying on Aspect Workflows to run Bazel in their CI/CD
pipeline.
# Open Source Software Licenses
Source: https://site.aspect.build/docs/aspect-workflows/additional-materials/license
Open source software license details for components used in Aspect Workflows, covering MIT, Apache, and other permissive licenses for transparency.
This page provides details about the open source software licenses for components used in Aspect Workflows. This documentation ensures transparency and compliance.
### 1. MIT license
The MIT License is a permissive free software license. It is a short and simple license that allows reuse of the code with minimal restrictions.
| Packages Covered | | |
| :------------------------ | :------------------ | :--------------- |
| **@nestjs/cache-manager** | **@nestjs/common** | **@nestjs/core** |
| **@redis/client** | **cache-manager** | **chalk** |
| **class-transformer** | **class-validator** | **hot-shots** |
| **node-fetch** | **yargs** | |
```plaintext theme={null}
(The MIT License)
Copyright (c) 2017-2023 Kamil Mysliwiec
(and other copyright holders, including Redis, inc., MOG Inc., Sindre Sorhus, TypeStack, Steve Ivy, David Frank, and yargs Contributors)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
### 2. Apache license version 2.0
The Apache License is a permissive free software license from the Apache Software Foundation. It grants the user a copyright and patent license.
| Packages Covered | | |
| :-------------------------------- | :-------------------------------------------- | :------------------------------------------ |
| **@opentelemetry/api** | **@opentelemetry/exporter-metrics-otlp-grpc** | **@opentelemetry/exporter-trace-otlp-grpc** |
| **@opentelemetry/resources** | **@opentelemetry/sdk-metrics** | **@opentelemetry/sdk-trace-base** |
| **@opentelemetry/sdk-trace-node** | **@opentelemetry/semantic-conventions** | **reflect-metadata** |
| **typedoc** | | |
```plaintext theme={null}
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
```
### 3. ISC license
The ISC License is a modern, permissive open source software license. It's functionally equivalent to the MIT License, but is notably shorter and simpler.
| Packages Covered |
| :--------------- |
| **minimatch** |
```plaintext theme={null}
The ISC License
Copyright (c) 2011-2023 Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
```
### 4. Custom permissive license (YAML)
The **yaml** package uses a custom permissive license that grants broad rights to use, copy, and modify the software, provided the copyright notice and permission notice are maintained.
| Packages Covered |
| :--------------- |
| **yaml** |
```plaintext theme={null}
Copyright Eemeli Aro
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
```
# AOSP build benchmark on Aspect Workflows
Source: https://site.aspect.build/docs/aspect-workflows/aosp-remote-execution-benchmark
Measured results for a full AOSP android-14.0.0_r22 build on self-hosted Aspect Workflows, with complete infrastructure configuration and links to the public CI runs.
Aspect Workflows runs Android Open Source Project builds on remote execution. This page
reports measured results for a full AOSP build, the infrastructure it ran on, and links to
the public CI runs so every number can be checked against its source.
**Build under test:** AOSP `android-14.0.0_r22`, target `aosp_arm64-userdebug`, 176,702
ninja actions. Deployed on Aspect Workflows self-hosted in AWS (`us-east-2`).
## Executive summary
Four configurations, spanning the full spectrum a team actually experiences, from the worst
case (a fresh machine with nothing warm) to the steady state (no changes):
| | Configuration | Build time |
| ---------------- | -------------------------------------------------------- | ----------- |
| Worst case | Fresh runner: cold source sync + full local build | **1h 16m** |
| Baseline | Warm runner, full uncached build, single 96-vCPU machine | **34m 07s** |
| Remote execution | Warm runner, full uncached build, 512-slot fleet | **24m 38s** |
| Steady state | Warm runner, no changes | **2m 38s** |
* **Full, fully-uncached build: 24m 38s** with remote execution, versus **34m 07s** on a
96-vCPU machine alone: **81,463 of 81,481 actions executed on the remote fleet, zero
failures**. On the part of the build that can be parallelized, remote execution is
**3.9x faster** (14m 12s down to 3m 36s); the end-to-end ratio is diluted by AOSP's
serial packaging tail, which no platform can parallelize.
* **Incremental build with no changes: 2m 38s**, most of which is `repo sync`. Warm,
persistent runners keep the source tree between builds: the first-ever run on a fresh
machine paid **39m 21s** of `repo sync`; every run since has paid about 90 seconds.
* **The open-source AOSP build is the worst case for remote execution, not the best.** It
is small enough that fixed costs and the serial tail make up \~58% of wall-clock.
Production AOSP-derived builds are typically several times larger, and nearly all of the
added volume is parallelizable compile and link work, which is exactly what the fleet
accelerates. Modeled on the measured phase profile, a build that takes \~4 hours locally
lands around **\~1.5 hours with remote execution (\~2.5x)** at the measured fleet size, and
**toward \~1 hour (\~3.3–3.9x)** with the fleet scaled 2–4x to match the larger build. See
*Extrapolating to larger AOSP builds* below.
**Why self-hosted.** The deployment measured here runs entirely inside one AWS account:
source, artifacts, and caches never leave the VPC, which matters when the tree contains
vendor IP. Because the platform is yours, every knob behind these numbers is tunable per
workload: fleet size, per-action memory ceilings, worker pools, and the exact container
image AOSP's build system expects. The focus of this page is **build time**; cost economics
depend on usage pattern and are treated separately in *Cost considerations* below. Every
number on this page links to a public CI run and its uploaded logs.
## Results
The four benchmarks below use the same build on the same infrastructure. What varies is
what was already warm (the source tree, the output directory, the caches) and whether
remote execution was enabled.
| Benchmark | Build step | soong timer | Actions executed |
| ------------------------------------------------- | -------------- | ----------- | ---------------- |
| First build: fresh runner, cold sync (worst case) | **1h 16m 06s** | 35:58 | 176,702 |
| Full build, single 96-vCPU machine | **34m 07s** | 32:25 | 176,702 |
| Full build, remote execution | **24m 38s** | 22:38 | 176,702 |
| Incremental build, nothing changed | **2m 38s** | 01:01 | 576 |
The remote-execution row is the honest full-build number: cache reuse was explicitly
disabled, so every action was genuinely executed. **81,463 actions ran on the remote
fleet**, 18 fell back to the local machine, and none failed.
The two warm full builds differ in exactly one thing: whether remote execution is on. It
takes the build step from 34m 07s to 24m 38s, a **1.38x** end-to-end improvement. Isolating ninja, it goes from 29m 10s to
19m 24s, a **1.50x** improvement.
Those figures understate what remote execution does to the work it can actually affect.
Roughly 5 minutes of every run is source sync and soong analysis, and 15 minutes is a serial
tail that runs on the CI machine in both configurations. **On the parallelizable part of the
build, remote execution is 3.9x faster**: 14m 12s down to 3m 36s.
### Phase-by-phase comparison
The two warm full builds execute an identical action graph, and the last 10% is the same
17,669 actions in each, so the phases compare directly.
| Component | Remote execution | Single machine | Ratio |
| ------------------------------ | ---------------- | -------------- | ----------------- |
| `repo sync` and soong analysis | 5m 02s | 4m 47s | 1.0x (fixed cost) |
| **First 90% of actions** | **3m 36s** | **14m 12s** | **3.9x faster** |
| Last 10%: the serial tail | 15m 36s | 14m 54s | 0.95x |
| **Build step total** | **24m 38s** | **34m 07s** | **1.38x** |
Remote execution is roughly **4x on the parallelizable part of the build**, the portion that
scales with fleet size. The serial tail is marginally slower under remote execution, because
those actions form a dependency chain in which each one pays a network round trip that no
amount of parallelism recovers. The tail accounts for about 44% of the remote-execution run's
wall-clock and is unaffected by fleet size, cache state, or worker count.
### Execution profile
Within the remote-execution run, the fleet absorbs the parallel bulk of the build almost
immediately:
| Progress | Elapsed from ninja start | Sustained rate |
| -------- | ------------------------ | ----------------- |
| 10% | 0.3 min | \~1,100 actions/s |
| 30% | 0.8 min | \~1,600 actions/s |
| 50% | 1.7 min | \~490 actions/s |
| 70% | 2.5 min | \~770 actions/s |
| 90% | 3.6 min | \~430 actions/s |
| 100% | 19.4 min | — |
**90% of all actions complete in the first 3.6 minutes.** The remaining time is AOSP's
serial tail (R8/dex, APEX hiddenapi encoding, image assembly, and signing), a dependency
chain of large, mostly single-threaded steps that runs on the CI machine.
The serial tail is a property of the AOSP build graph, not of the execution platform. It is
unaffected by fleet size or cache state, and is present in every configuration, including a
purely local build. It is the reason a larger fleet doesn't reduce total build time beyond
a point; see Sizing guidance below.
All four benchmarks are public:
| Scenario | Public CI run |
| -------------------------------------- | -------------------------------------------------------------------------------------------- |
| First build, fresh runner (worst case) | [run 31758099068](https://github.com/aspect-build/basic-aosp-build/actions/runs/31758099068) |
| Full build, remote execution | [run 31847444276](https://github.com/aspect-build/basic-aosp-build/actions/runs/31847444276) |
| Incremental build | [run 31850749685](https://github.com/aspect-build/basic-aosp-build/actions/runs/31850749685) |
| Full build, single machine | [run 31851206170](https://github.com/aspect-build/basic-aosp-build/actions/runs/31851206170) |
## First build on a fresh runner (worst case)
The cold-start benchmark: a newly-launched runner with no source tree, no output directory,
and nothing warm anywhere. This is what a team's first build looks like, and what every
build would look like without persistent runners.
| Phase | Duration |
| ----------------------------------- | -------------- |
| `repo sync` (from nothing) | **39m 21s** |
| soong analysis and ninja generation | 3m 27s |
| ninja execution | 32m 40s |
| **Build step total** | **1h 16m 06s** |
More than half the wall-clock is the cold `repo sync`: the cost runner persistence
removes. Every subsequent run on this page synced the same tree in under two minutes.
## Full build with remote execution
Every action executed remotely, with no action-cache reuse of any kind.
| Phase | Duration |
| ----------------------------------------- | ----------- |
| Prerequisites | 3s |
| `repo sync` (source tree already present) | 1m 49s |
| lunch / build configuration | 8s |
| soong analysis and ninja generation | 3m 13s |
| ninja execution | 19m 24s |
| **Build step total** | **24m 38s** |
### Remote execution outcome
Measured from `rbe_metrics.txt`, uploaded as a workflow artifact on the run:
| Metric | Value |
| ------------------------------ | ------------------------- |
| Actions dispatched to reclient | 81,481 |
| Executed remotely | **81,463** (99.98%) |
| Local fallback | 18 (0.02%) |
| Failures | 0 |
| Cache hits | 0 (disabled for this run) |
Action mix by ninja edge description: approximately 47,600 `clang++`, 18,300 `clang`, 1,100
`javac`, 1,000 `turbine`, plus `jar`, `d8`, `signapk` and `zip` steps.
The 18 local fallbacks are actions that exceeded the configured per-action memory ceiling
(large C++ links) and were retried on the CI machine, a deliberate trade-off described
under *Sizing guidance*.
## Full build on a single machine
The control: an identical source tree, an identical emptied output directory and the same
96-vCPU CI machine, with remote execution disabled. All 176,702 actions ran locally.
| Phase | Duration |
| ----------------------------------- | ----------- |
| `repo sync` | 1m 32s |
| soong analysis and ninja generation | 3m 15s |
| ninja execution | 29m 10s |
| **Build step total** | **34m 07s** |
Sync and analysis cost the same as the remote-execution run, to within a few seconds: they're
unaffected by where actions execute. The difference is entirely in ninja: 29m 10s
locally against 19m 24s with the fleet.
A 96-vCPU machine is already a strong local baseline. The comparison understates
what remote execution offers a team whose CI machines are smaller, since the remote figure
barely depends on the size of the machine driving it; see *Sizing guidance*.
## Incremental build
The tree is fully built and nothing has changed. soong detects no work and only the
packaging ninja re-runs.
| Phase | Duration |
| -------------------------------------------- | ---------- |
| Prepare build directory | 8s |
| `repo sync` | 1m 34s |
| soong analysis | 2s |
| ninja (`no work to do`, 576 packaging edges) | 57s |
| **Build step total** | **2m 38s** |
No actions reached the remote fleet; there was nothing to execute. That leaves `repo sync`
as 60% of a build that did nothing: in the incremental loop the cost is syncing source, not
compiling.
## Configuration
### CI runner
The machine that drives the build, runs soong, and executes the serial tail.
| Property | Value |
| ------------- | ---------------------------------------- |
| Instance type | `c6id.24xlarge` |
| vCPU / memory | 96 vCPU / 192 GiB |
| Local storage | 2 x 1,425 GB NVMe SSD (instance store) |
| Root volume | 2,048 GB EBS |
| Network | 37.5 Gbps |
| OS | Ubuntu 24.04.4 LTS |
| CI system | GitHub Actions, self-hosted runner group |
The AOSP workspace lives on the instance-store NVMe rather than EBS, so source sync, the
output tree and all local I/O are on directly-attached storage.
### Remote execution fleet
| Property | Value |
| ----------------------------- | ------------------------------ |
| Worker instance type | `m6id.4xlarge` |
| Per worker | 16 vCPU / 64 GiB / 950 GB NVMe |
| Workers (maximum) | 32 |
| Concurrent actions per worker | 16 |
| **Total execution slots** | **512** |
| Memory ceiling per action | 3,584 MiB |
| Warm pool | 2 pre-initialized workers |
| Scaling | 0 to 32, demand-driven |
Workers run the container image AOSP's build system requests, so remote actions execute in
the toolchain environment they expect.
### Remote cache and storage tier
| Property | Value |
| ------------- | -------------------------------------------- |
| Engine | BuildBarn storage nodes |
| Shards | 4, mirrored |
| Instance type | `im4gn.large` (2 vCPU / 8 GiB / 937 GB NVMe) |
| Usable cache | \~3.7 TiB |
| Frontend | 3–18 tasks, auto-scaled |
A full cold AOSP build writes roughly 250–400 GB into the content-addressable store.
Repeated builds deduplicate to nearly nothing.
### AOSP build configuration
| Setting | Value |
| ------------------------ | ---------------------------------------------------------------------------------------- |
| Branch | `android-14.0.0_r22` |
| Target | `aosp_arm64-userdebug` |
| Platform version | Android 14 (`UQ1A.240205.002`) |
| `repo sync` parallelism | 4 jobs |
| Local ninja parallelism | 96 |
| Remote ninja parallelism | 800 |
| ccache | Disabled (`USE_CCACHE=0`) on the full-build benchmarks; enabled on the incremental build |
Tools routed to remote execution through reclient: C++ compile, C++ link, javac, turbine,
R8, D8, jar, zip, signapk, ABI dumper, ABI linker and clang-tidy. Metalava runs locally.
Every tool is configured for local fallback, so any action the fleet cannot serve is retried
on the CI machine rather than failing the build.
## Sizing guidance
**Fleet size stops mattering after the parallel phase, for a build this size.** With 90%
of actions complete in the first 3.6 minutes, adding workers shortens that window, not the
serial tail that follows. For the OSS build, 512 slots is already past the point of
diminishing returns and a smaller fleet would produce a similar total time. The economics
invert for larger builds: a production build's parallel phase runs for hours rather than
minutes, keeping a bigger fleet busy long enough to pay for itself; see *Scaling the fleet
beyond 512 slots* under the extrapolation section.
**The CI machine can be much smaller.** The measurements above use a 96-vCPU runner for both
configurations, which flatters the local baseline: it is close to the largest single machine
worth pointing at an AOSP build. With remote execution, that machine's job is to run soong,
drive the graph and execute the serial tail, none of which needs 96 cores. A substantially
smaller runner should land near the same total time.
**Memory ceiling versus slot count.** Each worker provides 64 GiB across 16 concurrent
actions. A 3.5 GB per-action ceiling keeps all 16 slots usable, at the cost of a small
number of very large C++ links falling back to the CI machine (18 of 81,481 actions here).
Workloads with many large links can trade slots for headroom: for example, 8 slots at 7 GB.
**Cold-start behaviour.** Scaling a fleet from zero costs several minutes before full
capacity is available, because cloud capacity provisioning ramps progressively. A small
minimum fleet size or a warm pool removes this from the critical path. The runs above were
measured with the fleet already warm.
**Warm, persistent runners eliminate the largest fixed cost.** The very first build on this
deployment ran on a fresh machine and paid **39m 21s** of `repo sync`
([run 31758099068](https://github.com/aspect-build/basic-aosp-build/actions/runs/31758099068));
cold syncs against AOSP's servers range 35–50 minutes with rate limiting. Aspect Workflows
runners persist between builds (configurable idle timeout; 60 minutes here), so every
benchmark run on this page paid 1m 32s to 1m 49s instead: the source tree survives on the
runner's local NVMe and syncs incrementally. For the incremental loop, where `repo sync` is
60% of a 2m 38s build, runner persistence is the optimization that pays off most.
## Extrapolating to larger AOSP builds
The open-source AOSP build measured above is, counterintuitively, close to the worst case
for remote execution. It is small enough that the two components a fleet can't accelerate,
source sync plus soong analysis (\~5 min) and the serial packaging tail (\~15 min), account
for **58% of the remote run's wall-clock**. Production AOSP-derived builds (vendor HALs,
platform apps, product-specific stacks) are commonly three to five times the compile volume,
and nearly all of that added volume is parallelizable compile and link work. The serial tail
does not grow with it: a product build still assembles one set of images, and its
packaging-phase action count is similar to the OSS build's, growing only modestly with the
number of APKs.
That inverts the proportions. Modelling a production build that takes **\~4 hours** on the
same class of CI hardware where the OSS build takes \~1 hour, using the phase behaviour
measured above:
| Phase | OSS build (\~1 h local) | Production build (\~4 h local) | Production with RBE |
| ---------------------------- | ----------------------- | ------------------------------ | ----------------------------------- |
| Source sync + soong analysis | \~8 min | \~15 min | \~15 min |
| Parallelisable actions | \~25 min (42%) | \~195 min (**81%**) | **\~50 min** (at the measured 3.9x) |
| Serial packaging tail | \~26 min | \~30 min | \~31 min |
| **Total** | \~60 min | **\~4 h** | **\~1.5 h (\~2.5x)** |
Model assumptions, stated so they can be challenged:
1. **Added volume is parallel-phase work.** What distinguishes a product build from OSS is
compile and link volume: the exact work measured at 3.9x above.
2. **The serial tail is roughly constant in absolute terms**, give or take 15–20% growth for
additional APK dexing and signing, because packaging structure does not scale with source
volume.
3. **The parallel-phase speedup holds at 3.9x.** This is conservative: in the OSS run the
512-slot fleet was idle after the first 3.6 minutes, so a longer parallel phase amortizes
the same fleet better, and the fleet itself can be sized to the workload. With a fleet
matched to a 4-hour build and output-download pruning enabled, the parallel-phase ratio
has room above 3.9x.
Sensitivity: doubling the assumed tail still yields \~2.0x; holding the tail constant and
achieving 5x on the parallel phase yields \~2.8x. The justified range is **2.2–2.8x
end-to-end for a 4-hour-class build, \~2.5x central**, against the 1.38x measured on the
small OSS build. The direction of the relationship is the important part: **the larger the
AOSP build, the larger the share of it that remote execution accelerates.**
### Scaling the fleet beyond 512 slots
For the small OSS build, a larger fleet buys nothing: the 512 slots were already idle after
3.6 minutes. A 4-hour-class build is different: its \~195-minute parallel phase keeps a fleet
busy long enough that fleet size becomes a real lever. Scaling the measured configuration by
2–4x, with a stated scheduling-efficiency discount per step (90% / 85% / 80% of linear, to
account for narrowing graph width, cache-tier load, and client pipeline depth):
| Fleet | Slots | Parallel phase | Build total | vs \~4 h local |
| --------------------------- | ----- | -------------- | ------------ | -------------- |
| 1x (measured configuration) | 512 | \~50 min | **\~1h 36m** | **2.5x** |
| 2x | 1,024 | \~28 min | **\~1h 14m** | **3.3x** |
| 3x | 1,536 | \~20 min | **\~1h 06m** | **3.6x** |
| 4x | 2,048 | \~16 min | **\~1h 02m** | **3.9x** |
| Limit (unbounded fleet) | — | 0 | \~46 min | \~5.2x |
Two readings of that table matter equally. First, **a fleet sized to the build pushes a
4-hour build toward one hour**: the 2x step alone recovers another 22 minutes. Second, the
returns diminish exactly as Amdahl predicts: the fixed \~46 minutes of sync, analysis and
serial tail is the floor no fleet reaches past, so each doubling buys roughly half what the
previous one did. The economically sensible operating point for a 4-hour-class build is
likely 2–3x the measured fleet; beyond that, effort is better spent on the serial tail
itself.
Scaling prerequisites, all configuration rather than architecture: the client's remote
pipeline depth (`NINJA_REMOTE_NUM_JOBS`) scales with slot count; the cache/storage tier is
sized at deployment time to match (its shard topology is fixed once data is resident); and
output-download pruning keeps the CI machine's network from becoming the funnel at higher
action rates.
Two further effects compound this in production use, both measured above rather than
modeled: real CI runs are not fully uncached, so unchanged subtrees hit the shared remote
cache and land well under the uncached figure; and no-change runs complete in minutes,
independent of build size.
## Cost considerations
This page's focus is build time; cost depends on fleet utilization and cache hit
rate, so what follows is a planning model with its assumptions stated. Two facts frame it: a
remote fleet adds compute that a local build does not consume, and a large dedicated runner
spends most of a long build under-utilized: dozens of vCPUs busy for the few minutes of
wide parallelism, then near-idle through the serial tail. Which effect wins is a question
of utilization, and utilization is set by build concurrency and caching.
### A modeled scenario: one production Android repo
Assume caching is already in place: warm persistent trees and a shared build cache. That's
the right baseline, because caching is table stakes; what this models is what *adding
remote execution* is worth on top of it.
The load is a typical day for a large organization shipping a production Android platform:
**\~300 presubmit delta builds, \~40 postsubmit integration builds, and \~15 full builds**
(the nightly target matrix plus release candidates), roughly 355 builds a day with dozens
in flight at peak hours.
Today's architecture is dedicated 64-vCPU runners. The comparison moves the same load to
16-vCPU runners plus one shared, demand-scaled fleet (up to 2,048 slots, the 4x row of the
fleet-scaling table above). Percentages are computed from on-demand list prices for the
instance types in this report; only the relative results are shown:
| Build type | Per day | Today (64-vCPU runners, cached) | Adding RBE (16-vCPU runners + shared fleet) |
| -------------------------------- | ------- | ------------------------------- | ------------------------------------------- |
| Presubmit (delta, warm tree) | \~300 | \~25 min median | **\~15 min median** |
| Postsubmit integration | \~40 | \~40 min | **\~22 min** |
| Full build (nightly matrix, RCs) | \~15 | \~4 h | **\~1 h 10 m** |
The gains concentrate where work actually executes. A cached presubmit only compiles its
delta, so remote execution trims it rather than transforms it; a full build executes
everything, and that's where 4 hours becomes \~70 minutes.
The outcome, on this load:
* **Compute cost: \~50% lower** than the same load on fully autoscaled large runners, and
**\~90% lower** than a static pool sized for peak. Real AOSP runner pools sit between
those bounds: a cold runner pays a \~39-minute source sync before its first build
(measured above), which pushes teams toward keeping runners warm rather than scaling
to zero.
* **One demand-scaled fleet serves everything.** The whole day is \~2,500–3,000 slot-hours
of remote execution, absorbed at 60–70% fleet utilization; peak presubmit bursts queue
for minutes, not hours.
* **\~100 hours of cumulative build waiting removed per day** across the team.
The basis: the OSS benchmark measured \~31 slot-hours of remote execution for a full build
(512 slots busy for 3.6 minutes), so a 4x-volume full build is \~125 slot-hours cold, and
\~60% of that on typical nightlies since the existing cache covers unchanged subtrees.
Cached presubmit deltas average \~4 slot-hours. The runner shrinks from 64 to 16 vCPU
because under remote execution it only runs soong, drives the graph, and executes the
serial tail.
What the model depends on:
* **The fleet only pays for executed actions.** Caching already covers repeated work in
both columns; remote execution buys speed on the work that remains, and its cost scales
with that work rather than with the fleet's size on paper.
* **Load keeps the fleet utilized.** 355 builds a day is comfortably enough; the same fleet
serving a handful of builds a week would be idle-heavy, and the comparison inverts.
* **Runner downsizing compounds across the pool**: every concurrent build needs a runner,
and each one is a quarter the size.
At this load, engineer time waiting on builds dwarfs the compute in either column, and
it's exactly the number this page measures.
## Verifying these results
Every run is public and reproducible:
* **Build logs**: the "Build AOSP" step of each linked run contains full ninja output with
per-action timestamps.
* **Artifacts**: every run of the
[AOSP Build workflow](https://github.com/aspect-build/basic-aosp-build/actions/workflows/aosp-build.yaml)
uploads its full build logs as downloadable artifacts: the build log and the complete
reclient logs, including `rbe_metrics.txt` with per-action completion statuses and a
per-action record log. Download them from any run's page for full build details beyond
what this page summarizes.
* **Build definition**: [aspect-build/basic-aosp-build](https://github.com/aspect-build/basic-aosp-build)
contains the build script, the reclient configuration and the CI workflow.
# CI runners
Source: https://site.aspect.build/docs/aspect-workflows/features/ci-runners
Persistent, NVMe-backed self-hosted CI runners from Aspect Workflows that keep Bazel's output base warm across jobs for faster CI builds.
Aspect Workflows provides auto-scaling, self-hosted CI runners tuned for optimal Bazel performance. They register automatically with the CI system you already use (GitHub Actions, CircleCI, GitLab, or Buildkite), run on the exact hardware you choose, and cost a fraction of provider-hosted runners.
## Tuned for Bazel
Generic cloud VMs, like GitHub Actions' `ubuntu-latest`, provision a fresh, empty filesystem for every job. Bazel's output base is gone. Every action must either hit the remote cache or be re-executed from scratch. Even with a warm remote cache, the round-trip cost of fetching outputs adds up, and the Bazel JVM starts cold with no in-process state.
Aspect's CI runners are designed around Bazel's specific requirements. They are persistent VMs that accept multiple jobs in sequence rather than terminating after one. The Bazel JVM stays running between jobs, so in-memory state, including the analysis cache, carries over. Each runner mounts a RAID0 array across multiple NVMe drives to maximize filesystem IOPS, which matters for Bazel's heavily file-intensive output base operations.
When a runner starts up, it can optionally be pre-warmed from an on-disk archive: Bazel's fetch phase is already done, with repository rule fetches, extracted external dependencies, and a populated output base all restored before the first job runs. This eliminates the cold-start penalty for new machines joining the pool: even a fresh runner's first job skips the dependency downloads. Cached builds routinely complete in under one minute.
## Your hardware, your choice
Runners automatically register with your CI provider as self-hosted runners, so your pipelines target them like any other runner label. And because they are your machines, you choose the exact configuration: instance type, architecture, CPU, memory, GPUs, and NVMe storage.
Multiple runner groups are supported, so different CI jobs run on different machine types: x86, Arm, and GPU instances side by side. If your tests need hardware your CI provider doesn't offer, like the GPU instance types AI teams need, runner groups are how you get it. Docker-based tests are fully supported, on runners and on remote execution workers alike.
## Auto-scaling, down to zero
The runner pool auto-scales based on queue depth and scales back down to zero when there is no work, so you are not paying for idle machines between CI runs.
## Health checks, not babysitting
The classic foot-gun with persistent runners is a machine that drifts into a bad state, stays in the pool, and fails every job it picks up until a human notices and drains it.
Workflows runners take care of this themselves. Built-in health checks keep every runner healthy, and a runner that hits an unrecoverable failure is automatically taken out of service before it accepts another job. The pool scales replacement capacity as usual, so one bad machine never turns into a string of red builds, and nobody gets paged to go find it.
## A fraction of provider-hosted cost
Self-hosted Workflows CI runners are substantially cheaper than the per-minute pricing on GitHub Actions, Buildkite, CircleCI, and GitLab provider-hosted runners: typically 40–80% lower CI compute spend, with the gap widening on larger instance types. Because the runners live in your own AWS or GCP account, any committed-use discounts, savings plans, or enterprise agreements you already have with your cloud provider apply directly.
To learn more about CI runners, see the [Estimating the effort to build a Bazel CI/CD](/blog/estimating-bazel-cicd) article.
# Marvin: GitHub & GitLab bot
Source: https://site.aspect.build/docs/aspect-workflows/features/code-review
How Aspect Workflows surfaces Bazel build, test, and lint results on GitHub pull requests and GitLab merge requests using the Marvin bot.
Build and test results buried in raw CI logs don't help reviewers. A developer has to hunt through job output to find out whether the build passed and which tests failed, by which point context has already switched.
Aspect Workflows includes a bot called **Marvin** that bridges CI results and your pull requests on GitHub and merge requests on GitLab. Marvin is one of the developer-experience improvements the [Aspect CLI](/docs/cli/overview) brings to Workflows — vanilla `bazel` has no equivalent. The CLI streams structured task events (build status, test outcomes, lint findings, suggested fixes) to the Aspect API as each `aspect ` runs; Marvin consumes that stream and writes the matching status checks, PR and MR comments, review annotations, and suggested-change blocks live. To get these surfaces, run your builds through `aspect ` rather than plain `bazel`.
### Build and test status
Marvin posts a status check for each Aspect Workflows job, as a GitHub Check or a GitLab pipeline status. The check updates as the job progresses, not just at the end, so authors and reviewers see partial results, like the first failing test, without waiting for the entire pipeline to finish.
### Lint annotations
When the [`aspect lint`](/docs/cli/tasks/lint) task reports a warning or error, Marvin annotates the offending line, via GitHub's Checks API or as a GitLab merge request annotation:
When the linter can suggest a fix, Marvin attaches the recommendation as a suggestion so the author can apply it with one click:
### PR and MR summary
Marvin posts a summary comment to the pull request or merge request thread with links to detailed logs and uploaded artifacts. You can jump directly from the comment to the relevant BEP, test logs, or build profile without navigating the CI UI.
Lint annotations and one-click suggested fixes are powered by the [`aspect lint`](/docs/cli/tasks/lint) AXL task. See that page for how to declare linters, configure severity, and tune output.
# Custom tasks
Source: https://site.aspect.build/docs/aspect-workflows/features/custom-tasks
# External remote cache and execution
Source: https://site.aspect.build/docs/aspect-workflows/features/external-cache-exec
Expose a second Aspect Workflows remote cache and execution endpoint so developer workstations get the same Bazel cache hits CI runners enjoy.
Aspect Workflows can expose a second cache and execution endpoint, separate from the CI-internal cluster, that developer workstations reach over the internet. Developers configure their `~/.bazelrc` to point at this endpoint and benefit from the same remote cache hits and remote execution workers that CI uses, without accessing the internal VPC.
The external remote cache is a separate cache cluster that complies with the [Bazel Remote Execution Protocol v2](https://docs.bazel.build/versions/2.0.0/remote-execution.html). The external remote execution cluster accepts build actions from developer machines and runs them on dedicated workers for the target platform architecture (x86-64, arm64, or custom).
The external cache can be configured in either of two modes:
* **Independent storage** (the default): the external cache has its own backing storage, sized and scaled separately from the internal CI cache. Use this when you want to isolate CI's cache from developer traffic.
* **Pass-through**: the external cache reuses the internal CI cache as its upstream storage, so a cache entry written by a CI runner becomes available to developer workstations immediately, and vice versa. This is the lowest-friction setup when you want CI and developer caches to share hits without operating two storage layers.
Access to both clusters is authenticated via **OpenID Connect** or **Personal Access Tokens (PATs)**. PATs are issued from the Aspect admin portal and presented to Bazel via `--remote_header=X-Aspect=aw_pat_XXXX`, convenient for local development on a workstation where an interactive OIDC flow isn't a fit.
# Remote cache
Source: https://site.aspect.build/docs/aspect-workflows/features/remote-cache
A managed Bazel remote cache in your own VPC, included with every Aspect Workflows deployment, with Build without the Bytes support.
Every Aspect Workflows deployment includes a managed remote cache: REv2-compliant, deployed inside your own VPC right next to your CI runners, and operated by Aspect engineers under our SLA.
## How caching works
Bazel skips re-executing any action whose inputs, command, and environment match a prior run; it fetches the cached output instead. The `--remote_cache` flag points Bazel at a content-addressable store so that cache hits are shared across every developer and CI job on your team, not just within a single machine.
## In your VPC, next to your runners
A remote cache is only as fast as the network between it and your builds. Hosted caches put the internet in that path: every artifact pays round-trip latency, and every byte pays egress. The Workflows cache runs inside a dedicated Virtual Private Cloud, colocated with your runners, so reads and writes stay on your network at your network's speed. It never shares quota or storage with other Workflows customers, and your build traffic never leaves the private network boundary.
## Build without the Bytes
The cache supports Bazel's `--remote_download_minimal` mode: instead of downloading every artifact in the build graph, Bazel fetches only those strictly required to complete the build, typically just the leaf nodes. This dramatically reduces network transfer and improves overall build efficiency.
## Developer-facing access
Build clients outside the Workflows-managed runners, such as developer laptops, a secondary CI provider, or ad-hoc jobs, can connect to the same cache, either privately (VPC peering) or over the internet via the [external remote cache and execution](/docs/aspect-workflows/features/external-cache-exec) feature. External access authenticates through your SSO provider using OIDC/OAuth, with fine-grained authorization options such as read-only access for end users.
## Standard protocol, no lock-in
The cache implements the Bazel Remote Execution Protocol v2 over gRPC. Any REv2-compatible client works with it, and nothing about your build depends on the cache to keep working.
## Observability
Grafana dashboards in your deployment track cache hit rates, throughput, and storage over time, so you can catch performance regressions and validate optimizations.
# Remote build execution (RBE)
Source: https://site.aspect.build/docs/aspect-workflows/features/remote-execution
Aspect Workflows remote build execution (RBE) cluster implementing the Bazel REv2 protocol to parallelize actions across remote workers.
A local Bazel build is limited by the number of CPU cores on the machine running it. Remote Build Execution (RBE) removes that ceiling: Bazel distributes individual actions across a pool of remote workers via `--remote_executor`, so hundreds of compile and test actions run in parallel regardless of local hardware.
Every Workflows deployment includes an optional remote execution cluster that implements the [Bazel Remote Execution Protocol v2](https://docs.bazel.build/versions/2.0.0/remote-execution.html). The cluster is highly configurable: you define one or more *executors*, each pinned to a specific container image (and optionally a custom Bazel platform), and each executor backed by one or more worker pools with their own instance type (including GPU instance types), CPU/memory shape, network access toggle, max concurrency, and auto-scaling policy (minimum, warm, and maximum pool sizes; target-tracking scaling; and schedule-based rules to pre-scale ahead of known peaks). This means you can size each executor to exactly the workload it handles, mix architectures (`amd64`, `arm64`) and toolchain images within a single deployment, and scale down to zero when the queue is empty so you pay nothing for idle capacity.
## What RBE gives you
**Action-level parallelism.** Bazel's action graph is already a DAG. With RBE, every action that has no unmet dependencies runs immediately on the worker pool, not queued behind other actions on a single machine.
**Multiple platforms, your hardware.** Define several RBE platforms side by side, the same way you define runner groups: each pinned to its own container image and instance type, so you control architecture, CPU, memory, NVMe, and GPUs per platform. Docker-based tests are fully supported on workers.
**Right-sized actions.** Each platform supports multiple worker size classes, and actions are matched to a size class and memory automatically, so heavyweight link and test actions land on big machines without reserving them for every small compile.
**Per-action platform constraints.** Each action can declare the execution platform it needs. You can mix container images, OS versions, and toolchains within a single build without splitting it into separate jobs.
**Cross-platform builds.** The worker pool supports arm64 and amd64 simultaneously. Cross-compilation, multi-arch container publishing, and other cross-platform workflows run as native builds on the right architecture rather than through emulation.
**Cost-proportional resource allocation.** Provision large workers for link steps and test shards that need memory; use smaller workers for cheap compile actions. Workers scale down when the queue is empty, so you pay only for what you use.
## Standard protocol, no lock-in
The execution cluster implements the Bazel Remote Execution Protocol v2. Any REv2-compatible client works with it, and nothing about your build depends on RBE to keep working.
## Adopt at your own pace
RBE is in every Workflows deployment from day one for when you're ready for it. Most teams start with the warm [CI runners](/docs/aspect-workflows/features/ci-runners) and [remote cache](/docs/aspect-workflows/features/remote-cache), which deliver most of the speedup with no migration; turning RBE on later is a configuration change on the same deployment, tuned by Aspect engineers under our SLA.
# Selective delivery
Source: https://site.aspect.build/docs/aspect-workflows/features/selective-delivery
Push only artifacts whose Bazel output digests changed with Aspect Workflows Selective Delivery, to your bucket or container registry.
Selective Delivery delivers only the targets whose Bazel-built outputs actually changed, driven by the same build graph you already trust. In a monorepo with hundreds of deliverable artifacts, that turns "push everything on every green build" into a handful of uploads per commit.
## The problem
Pushing every artifact on every green build is slow and introduces risk: you may push a binary whose inputs did not change, overwriting a known-good artifact with an identical but unverified one. It also wastes CI time and registry storage on uploads that change nothing. You want to push only the artifacts whose build outputs actually changed since the last release.
## How it works
Selective Delivery determines what changed using the content hash of each build output, not the git SHA or a timestamp. If Bazel produces the same output digest for a target, that target is not pushed, even if unrelated files changed in the same commit. This is the same cache-key logic Bazel uses internally, applied at delivery time.
From trigger to upload, a delivery run:
* **Triggers on green builds of release branches.** Running on `main` is typical, but you have full control over which branches and tags delivery runs on in your CI configuration, and you declare the build and test jobs it depends on there too, so delivery only runs when they pass.
* **Compares output digests** against the prior release to find the targets whose outputs actually changed. With Build without the Bytes, the comparison works from hashes alone and never downloads an artifact, so determining what to deliver costs almost nothing, even across hundreds of deliverable targets.
* **Builds resolved targets** with version-control stamping enabled, so the pushed artifact carries the correct build metadata.
* **Runs push logic in parallel** across the delivery targets, uploading each changed artifact to its configured destination: a container registry, object store, or other target.
* **Produces a delivery manifest**: a structured record of every target's outcome and CI metadata, uploaded as a CI artifact. The manifest is [customizable and can be enriched with AXL hooks](/docs/cli/guides/delivery-manifest), for example to attach OCI image digests or feed downstream deployment tooling.
## Why digests, not git SHAs
Version stamping is the usual obstacle here: if every artifact embeds the commit SHA, then every artifact differs on every commit and nothing can be skipped. Selective Delivery sidesteps the trap by comparing *unstamped* outputs to detect change, then rebuilding only the changed targets *with* stamping for the actual push. You get accurate change detection and correctly-stamped artifacts.
## What you get
* **Faster delivery.** Only changed targets rebuild and upload, so release pipelines finish in a fraction of the time.
* **Lower cost.** No redundant uploads burning CI minutes, registry storage, and network egress.
* **Safer releases.** Known-good artifacts are never overwritten by unverified rebuilds of identical content.
* **Simple configuration.** Adding a new delivery target is as easy as adding a new Bazel target; teams configure and maintain their own.
* **Auditable.** Each delivered artifact traces back to the monorepo state that produced it.
To learn more about Selective Delivery, see the [Stamping Bazel builds with selective delivery](/blog/stamping-bazel-builds-with-selective-delivery) article.
Selective Delivery is implemented as the [`aspect delivery`](/docs/cli/tasks/delivery) AXL task. The task page covers configuration, the delivery script contract, and how to wire it into your CI pipeline.
# Build & Test UI
Source: https://site.aspect.build/docs/aspect-workflows/features/webui
The Aspect Workflows Build & Test UI for inspecting Bazel CI invocations, test results, and logs, running in your own AWS or GCP account.
The Aspect Workflows Build & Test UI is where you triage build and test failures fast. It shows recent build and test runs across your CI environment, lets you drill into individual invocations for test results and raw logs, and surfaces the metadata needed to diagnose failures without navigating the CI provider UI.
It ships with every Workflows deployment, running in your own AWS or GCP account alongside the rest of the Workflows services, exposed at a subdomain on your own DNS, and authenticated through your existing identity provider with SSO over OIDC or SAML. SCIM keeps user provisioning and deprovisioning in sync with your directory, and the same SSO that gates the rest of your engineering tooling gates the Build & Test UI too.
## Targets without the noise
The targets view collapses the invocation's build graph to what matters: which targets built, which failed, and how long each took.
## Test inspection
All test targets with status and duration, filterable and sortable, with per-test logs one click away.
## Every build, captured in full
Every invocation is captured in full: build logs with full-text search, per-test logs, target outcomes with timings, and the cache hit counts that explain why a build was fast or slow. View logs in place, raw, or downloaded, and share a deep link to the exact invocation instead of pasting log fragments into chat.
## The Details tab
Every invocation carries a Details tab with the full story of the build:
* **Build profile**: download the full, concurrent timeline of the build, showing the true critical path, idle gaps, and scheduling delays. Open it in Perfetto or `chrome://tracing` to see exactly where wall-clock time went.
* **Target outcomes**: tests passed and cached, targets built and skipped, charted at a glance.
* **Invocation details**: status and exit code, start, end, and elapsed time, Bazel version, command and pattern, workspace, and counts of targets configured, packages loaded, and actions executed.
* **Target configuration**: identifiers and Make variables for the targets in the invocation.
## Detail at the level you need
Developer and focus modes adjust how much detail an invocation shows, so a product engineer chasing one red test and a build engineer profiling the whole invocation each see the right view.
# Aspect Workflows
Source: https://site.aspect.build/docs/aspect-workflows/overview
Overview of Aspect Workflows, a managed Bazel CI acceleration platform offering self-hosted runners, remote cache, and remote execution — working with vanilla Bazel, with the Aspect CLI as an optional enhanced DX.
Aspect Workflows is a managed CI acceleration platform for Bazel monorepos. It deploys into your own cloud account and automatically registers auto-scaling, self-hosted runners with the CI system you already use: GitHub Actions, CircleCI, GitLab, or Buildkite. Self-hosted runners cost a fraction of provider-hosted ones, and you choose their exact configuration (instance type, architecture, CPU, memory, GPUs, NVMe storage), with multiple runner groups so different jobs run on different hardware: x86, Arm, and GPU instances side by side. On top of the runners: a remote cache and remote execution fleet, and a [Build & Test UI](/docs/aspect-workflows/features/webui) for triaging failures fast. Your pipelines keep calling `bazel` directly — Workflows wires the runner's cache, RBE, and NVMe output base into every `bazel` invocation automatically. Customers typically see cached builds complete in under a minute, overall CI time cut by 2–3×, and cloud compute costs drop 40–80%.
The optional [Aspect CLI](/docs/cli/overview) — a free, open-source [Starlark task system](/docs/cli/tasks) — layers an enhanced developer experience on top for teams who want it, replacing hand-rolled CI scripting with tasks that run identically on a laptop and in CI. It is never required to use Workflows.
## Why it's different
Most CI platforms assume the build system is incorrect: incremental state might be stale, so every job gets a clean, cold worker that re-does all the work. Bazel guarantees correctness, so Workflows hosts it the opposite way: runners stay warm, with Bazel's analysis cache in memory and the output base on local NVMe, and only genuinely new work executes.
What that means for you day to day:
* **Your PR feedback loop shrinks.** A cached `bazel test //...` returns in seconds, not the 10+ minutes a cold runner spends re-fetching and re-analyzing the world.
* **It runs in your infrastructure.** Source code, secrets, and artifacts never leave your network. That includes locked-down environments: GovCloud, air-gapped, and infrastructure with strict security requirements.
* **Big CI savings, before adopting RBE.** Runners use the same self-hosted runner mechanism your CI provider already supports (GitHub Actions, CircleCI, GitLab, Buildkite). If you're on ephemeral runners with long build times today, the warm pool cuts CI time and compute costs immediately, with no remote-execution migration. RBE is in the deployment from day one for when you're ready for it; until then you don't have to touch it.
* **Your hardware, your choice.** Pick the exact runner configuration: instance type, architecture, CPU, memory, GPUs, NVMe. Multiple runner groups let different CI jobs run on different machine types. If your tests need hardware your CI provider doesn't offer, like GPU instance types for AI workloads, this is how you get it.
* **Works with your existing Bazel setup.** Point your pipeline's `runs-on:` at a Workflows runner and your current `bazel build` / `bazel test` steps run as-is, now backed by a warm cache and NVMe output base. No rewrite required to get the speedup.
* **Optional: delete your CI scripting.** For teams that want it, the free, open-source [AXL task system](/docs/cli/tasks) replaces the pile of shell scripts and YAML. `aspect lint` does the same thing on your laptop as in CI, so "works on my machine" stops being a category of bug. It's opt-in — plain `bazel` is always a first-class path.
* **No lock-in.** The cache and execution speak standard REv2, your pipelines run on plain Bazel, and the [Aspect CLI is free and open source](/docs/cli/overview). Nothing about your build depends on us to keep working.
* **Tuned by Bazel experts.** The engineers operating your deployment maintain `rules_py`, `rules_js`, `rules_lint`, and a [broad portfolio of Bazel open source](/docs/bazel/open-source).
## Deployment options
**Self-hosted (your cloud):** Workflows deploys into your AWS or GCP account via Terraform. Your source code, secrets, and build artifacts never leave your infrastructure. A dedicated cloud project keeps cost attribution and access policies isolated from other systems.
**Aspect Cloud (hosted):** Aspect operates a fully isolated, single-tenant deployment on your behalf. You get the same performance and feature set without managing cloud infrastructure.
## Core components
**[CI runners](/docs/aspect-workflows/features/ci-runners)**: Auto-scaling, self-hosted runners that automatically register with your existing CI system (GitHub Actions, CircleCI, GitLab, or Buildkite) and keep Bazel's in-memory state and a populated NVMe-backed output base warm across jobs. Multiple runner groups run different jobs on different machine types. New runners restore from a warming archive with Bazel's fetch phase already done, so even a first job skips dependency downloads. The pool scales up to demand and back down to zero when idle.
**[Remote cache](/docs/aspect-workflows/features/remote-cache)**: A content-addressable cache co-located with your runners in the same VPC. Bazel checks the cache before executing any action; on a hit, the output is fetched and the action is skipped.
**[Remote Build Execution](/docs/aspect-workflows/features/remote-execution)**: An auto-scaling worker fleet that executes Bazel actions in parallel across many machines, scaling up to demand and back down to zero when idle. On a cache miss, work fans out horizontally rather than serializing on a single runner. Define multiple RBE platforms the same way you define runner groups: each with its own container image and instance type (arch, CPU, memory, NVMe, GPUs), and multiple worker size classes per platform with automatic size and memory matching per action. Docker-based tests are fully supported on workers.
**[External remote cache and execution](/docs/aspect-workflows/features/external-cache-exec)**: Expose the same cache and execution clusters to developer workstations and external build clients, authenticated via your SSO provider.
**[Selective delivery](/docs/aspect-workflows/features/selective-delivery)**: Delivers only the targets whose Bazel-built outputs actually changed, driven by the same build graph you already trust.
**[AXL tasks](/docs/cli/tasks) (optional enhanced DX)**: `aspect build`, `aspect test`, `aspect format`, `aspect lint`, `aspect gazelle`, and `aspect delivery` are an opt-in layer from the free, open-source Aspect CLI that replaces hand-written CI scripts. Each task handles Bazel flag configuration, artifact upload, and CI platform integrations (GitHub Status Checks and PR comments, GitLab job annotations and MR comments, Buildkite Annotations) internally. Your pipelines can keep calling plain `bazel` instead — the tasks are never required.
**[Marvin: GitHub & GitLab bot](/docs/aspect-workflows/features/code-review)**: Marvin adds status checks, posts PR comments, and annotates code reviews on GitHub and GitLab, streaming build results, test failures, and lint findings as the job runs, with one-click suggested fixes.
**[Build & Test UI](/docs/aspect-workflows/features/webui)**: Triage build and test failures fast, with full invocation capture: searchable logs, target outcomes, and downloadable build profiles. Sits behind your SSO (OIDC or SAML), with SCIM provisioning.
## CI platform support
Aspect Workflows integrates with GitHub Actions, Buildkite, GitLab CI/CD, and CircleCI. Your pipeline targets a Workflows runner queue and calls `bazel` in each CI step as it does today; the runner's setup step wires the remote cache, RBE, and NVMe output base into every `bazel` invocation automatically. Teams who adopt the optional Aspect CLI can call `aspect ` instead for the enhanced developer experience, and those tasks self-configure based on the runner environment and CI provider.
## Browse live examples
The `aspect-build/bazel-examples` repo runs on Aspect Workflows across all four supported CI providers. Source lives in two places: [GitHub](https://github.com/aspect-build/bazel-examples) (used by the GitHub Actions, Buildkite, and CircleCI pipelines) and [GitLab](https://gitlab.com/aspect-build/bazel-examples) (used by the GitLab CI pipeline).
## Getting started
* **Evaluating Workflows?** [Start a free trial](/trial). We deploy into your cloud (or ours) and you measure the results on your own repository during the 30-day trial.
* **Want a feel for the tooling first?** [Install the free Aspect CLI](/quickstart). It's the same task system Workflows runs in CI, and it works in any Bazel workspace in minutes, no account required.
* **Ready to deploy?** Aspect handles the deployment end to end, into a dedicated cloud account on your cloud or on Aspect Cloud, and operates it from there: provisioning, monitoring, upgrades, and 24/7 expert support. We deploy into high-compliance environments too: GovCloud, air-gapped, and infrastructure with strict security requirements.
# Workflows alerts
Source: https://site.aspect.build/docs/aspect-workflows/using-workflows/support/workflows-alerts
Reference for alerts the Aspect Workflows system raises, with the underlying concepts and triage steps for Bazel CI runner operators.
This document systematically outlines the technical concepts and theoretical basis for the alert types generated by the Workflows system, along with the initial steps for triage and debugging. These concepts are foundational for understanding the operational health and behavior of the Workflows CI runners and associated infrastructure.
## Understanding workflows alerts
The Workflows system triggers alerts when critical operational thresholds experience breaches or when essential components fail their health checks. These alerts provide immediate notification of a deviation from expected behavior, allowing operators to diagnose and triage the underlying cause.
The Workflows alerts cover key aspects of the system, including:
* Storage resource capacity.
* Runner initialization and boot sequence.
* Core app process health.
* Scaling mechanism integrity.
* External resource performance.
## Key alert categories and associated concepts
The system categorizes Workflows alerts based on the component or process that fails, which dictates the scope of the required investigation.
## Runner initialization alerts
These alerts relate to the fundamental processes required for a runner to become operational. The primary log group for investigating these is generally `/aw/runner/cloud-init/output`.
* **Bootstrap failure**
The Bootstrap Failure alert indicates that a significant number of runners have failed the initial boot sequence and are repeatedly cycling due to failing health checks.
Concept: At this stage, the system is unable to bring any new runners online, which directly impacts scaling and job execution capacity.
Triage Concept: Determine the cause by searching for the string BOOTSTRAP ERROR within the designated log group.
* **Warming restore error**
The system triggers the Warming Restore Error when the process of restoring the cache fails on multiple runners, even if the runner successfully completes the bootstrap phase.
Concept: This alert may indicate cache corruption in the primary or secondary warming caches, or a transient failure during the retrieval of cache archives from storage. A runner affected by this is either running cold or has warmed from the previous cache pointer.
Triage Concept: Diagnosis involves searching for WARMING ERROR in the log group and verifying the successful execution and upload of the warming archive creation job.
### Runner process alerts
These alerts signify an issue with the core app processes running on an already-bootstrapped runner.
* **CI agent error**
The system triggers this alert when the CI agent process within the runner encounters an error.
Triage Concept: Determining the cause requires searching the `/aw/runner/ci-agent` log group on Amazon Web Services using CloudWatch log filtering.
* **Workflows CI runner error**
This alert occurs when the main runner process logs an error at a level of CRITICAL or higher.
Triage Concept: You can determine the specific cause by filtering the `/aw/runner/workflows` log group on AWS for the corresponding high log level. The higher log levels in this context are EMERGENCY and ALERT.
## Storage and resource alerts
These concepts relate to the size and state of the storage buckets used by Workflows.
* **Warming bucket size alert**
This alert triggers when the size of the warming bucket exceeds a 500 GB threshold.
Concept: The warming bucket stores objects that are frequently accessed or recently added to the archive bucket to speed up the cache warming process for runners.
Lifecycle Policy: A lifecycle policy governs object management and deletes objects in the *archive/* directory that have the tag DeleteMark set to a value of `1`.
Critical Requirement: Don't enable bucket versioning on the warming bucket. If you find it enabled, turn it off and safely remove all versions of deleted objects.
### Scaling mechanism alerts
These alerts point to issues within the scaling infrastructure, which is responsible for managing runner deployment.
* **Scaling lambda error rate**
The system triggers this alert when the error rate for the scaling lambda component breaches a defined threshold.
Triage Concept: Diagnosis requires viewing the Lambda’s logs in the dedicated log group, which has the format `/aws/lambda/aw__scaling___`.
* **Malicious lambda event**
The system triggers this specific alert when the scaling lambda receives an event where the checksum key doesn’t match the expected value.
Concept: This disparity suggests that someone is sending a malicious payload to the lambda endpoint.
Triage Concept: The lambda logs the string **Potentially malicious event detected** along with additional event details. You can find the corresponding logs in the `/aws/lambda/aw__scaling___` log group.
### Infrastructure performance alerts
This alert relates to the performance of external load balancing components.
* **Application load balancer response time (Amazon Web Services only)**
This alert triggers when the **Application Load Balancer (ALB)** records **high response times** from the underlying cluster resources.
Concept: High ALB response times are frequently correlated with issues in the **remote cache**, such as excessive churn or other process-consuming activity within the cluster.
Triage Concept: Triage begins by checking the service statistics for each service in the remote cluster to isolate the source of the latency. Additionally, validating the symlink integrity of the various cache drive locations, for example `/dev/cas`, is necessary to ensure that the underlying storage is accessible by the cache services.
# Builtins
Source: https://site.aspect.build/docs/axl/builtins
`module` [base64](/axl/builtins/base64)
`module` [hash](/axl/builtins/hash)
`module` [time](/axl/builtins/time)
# Base64
Source: https://site.aspect.build/docs/axl/builtins/base64
load("@std//base64.axl", "base64")
`property` **base64**
base64: struct(decode = function, decode\_url = function, encode = function, encode\_url = function)
# Hash
Source: https://site.aspect.build/docs/axl/builtins/hash
load("@std//hash.axl", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "blake2b", "blake2s")
`function` **md5**
Creates a new MD5 hash object.
Call `update(data)` to feed bytes or strings into the hash, then
`digest()` for raw bytes or `hexdigest()` for the hex-encoded string.
# Examples
```python theme={null}
load("@std//hash.axl", "md5")
h = md5()
h.update("hello world")
h.hexdigest() # "5eb63bbbe01eeed093cb22bb8f5acdc3"
```
`function` **sha1**
Creates a new SHA-1 hash object.
Call `update(data)` to feed bytes or strings into the hash, then
`digest()` for raw bytes or `hexdigest()` for the hex-encoded string.
# Examples
```python theme={null}
load("@std//hash.axl", "sha1")
h = sha1()
h.update("hello world")
h.hexdigest() # "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"
```
`function` **sha224**
Creates a new SHA-224 hash object.
Call `update(data)` to feed bytes or strings into the hash, then
`digest()` for raw bytes or `hexdigest()` for the hex-encoded string.
# Examples
```python theme={null}
load("@std//hash.axl", "sha224")
h = sha224()
h.update("hello world")
h.hexdigest() # "2f05477fc24bb4faefd86517156dafdecec45b8a..."
```
`function` **sha256**
Creates a new SHA-256 hash object.
Call `update(data)` to feed bytes or strings into the hash, then
`digest()` for raw bytes or `hexdigest()` for the hex-encoded string.
# Examples
```python theme={null}
load("@std//hash.axl", "sha256")
h = sha256()
h.update("hello world")
h.hexdigest()
# "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
```
`function` **sha384**
Creates a new SHA-384 hash object.
Call `update(data)` to feed bytes or strings into the hash, then
`digest()` for raw bytes or `hexdigest()` for the hex-encoded string.
# Examples
```python theme={null}
load("@std//hash.axl", "sha384")
h = sha384()
h.update("hello world")
h.hexdigest() # 96-char hex string
```
`function` **sha512**
Creates a new SHA-512 hash object.
Call `update(data)` to feed bytes or strings into the hash, then
`digest()` for raw bytes or `hexdigest()` for the hex-encoded string.
# Examples
```python theme={null}
load("@std//hash.axl", "sha512")
h = sha512()
h.update("hello ")
h.update("world")
h.hexdigest() # 128-char hex string
```
`function` **blake2b**
Creates a new BLAKE2b-512 hash object.
Call `update(data)` to feed bytes or strings into the hash, then
`digest()` for raw bytes or `hexdigest()` for the hex-encoded string.
# Examples
```python theme={null}
load("@std//hash.axl", "blake2b")
h = blake2b()
h.update("hello world")
h.digest() # raw 64-byte digest as bytes
```
`function` **blake2s**
Creates a new BLAKE2s-256 hash object.
Call `update(data)` to feed bytes or strings into the hash, then
`digest()` for raw bytes or `hexdigest()` for the hex-encoded string.
# Examples
```python theme={null}
load("@std//hash.axl", "blake2s")
h = blake2s()
h.update("hello world")
h.digest() # raw 32-byte digest as bytes
```
# Time
Source: https://site.aspect.build/docs/axl/builtins/time
load("@std//time.axl", "sleep", "sleep\_iter", "monotonic", "monotonic\_ns", "time")
`function` **sleep**
Blocks the current thread for `ms` milliseconds.
Returns `None`. The sleep is synchronous; the calling task's thread
is parked for the full duration.
# Examples
```python theme={null}
load("@std//time.axl", "sleep")
sleep(250) # pause for 250 ms
```
`function` **sleep\_iter**
Returns an infinite iterator that yields a monotonically increasing integer every `ms` milliseconds.
Each call to `next` sleeps for `ms` milliseconds, then returns the
next tick (starting at `0`). Use `break` to stop iteration.
# Examples
```python theme={null}
load("@std//time.axl", "sleep_iter")
for tick in sleep_iter(1000): # poll once per second
if check_done():
break
```
`function` **monotonic**
Returns a monotonically non-decreasing time in seconds as a float.
The reference epoch is fixed at process start, so values are only
meaningful relative to other `monotonic()` readings within the same
process. Suitable for measuring elapsed time.
# Examples
```python theme={null}
load("@std//time.axl", "monotonic")
start = monotonic()
do_work()
elapsed = monotonic() - start # seconds, as float
```
`function` **monotonic\_ns**
def monotonic\_ns() -> int
Returns a monotonically non-decreasing time in nanoseconds as an int.
The reference epoch is fixed at process start, so values are only
meaningful relative to other `monotonic_ns()` readings within the
same process. Higher precision than `monotonic()`.
# Examples
```python theme={null}
load("@std//time.axl", "monotonic_ns")
start = monotonic_ns()
do_work()
elapsed_ns = monotonic_ns() - start # nanoseconds, as int
```
`property` **time**
# Types
Source: https://site.aspect.build/docs/axl/types
`type` [Arg](/axl/types/arg)
`type` [Arguments](/axl/types/arguments)
`type` [ConfigContext](/axl/types/config_context)
`type` [ExporterSpec](/axl/types/exporter_spec)
`type` [Exporters](/axl/types/exporters)
`type` [FeatureContext](/axl/types/feature_context)
`type` [Future](/axl/types/future)
`type` [Hash](/axl/types/hash)
`type` [Http](/axl/types/http)
`type` [HttpResponse](/axl/types/http_response)
`type` [Task](/axl/types/task)
`type` [TaskConclusion](/axl/types/task_conclusion)
`type` [TaskContext](/axl/types/task_context)
`type` [TaskInfo](/axl/types/task_info)
`type` [Telemetry](/axl/types/telemetry)
`type` [Template](/axl/types/template)
`type` [bool](/axl/types/bool)
`type` [bytes](/axl/types/bytes)
`type` [dict](/axl/types/dict)
`type` [float](/axl/types/float)
`type` [int](/axl/types/int)
`type` [list](/axl/types/list)
`type` [namespace](/axl/types/namespace)
`type` [range](/axl/types/range)
`type` [set](/axl/types/set)
`type` [str](/axl/types/str)
`type` [struct](/axl/types/struct)
`type` [tuple](/axl/types/tuple)
`type` [type](/axl/types/type)
`module` [args](/axl/types/args)
`module` [aspect](/axl/types/aspect)
`module` [bazel](/axl/types/bazel)
`module` [futures](/axl/types/futures)
`module` [json](/axl/types/json)
`module` [remote](/axl/types/remote)
`module` [std](/axl/types/std)
`module` [typing](/axl/types/typing)
`module` [wasm](/axl/types/wasm)
`function` **abs**
Take the absolute value of an int.
```
abs(0) == 0
abs(-10) == 10
abs(10) == 10
abs(10.0) == 10.0
abs(-12.34) == 12.34
```
`function` **all**
[all](https://github.com/bazelbuild/starlark/blob/master/spec.md#all): returns true if all values in the iterable object have a truth value of true.
```
all([1, True]) == True
all([1, 1]) == True
all([0, 1, True]) == False
all([True, 1, True]) == True
all([0, 0]) == False
all([0, False]) == False
all([True, 0]) == False
all([1, False]) == False
```
`function` **any**
[any](https://github.com/bazelbuild/starlark/blob/master/spec.md#any): returns true if any value in the iterable object have a truth value of true.
```
any([0, True]) == True
any([0, 1]) == True
any([0, 1, True]) == True
any([0, 0]) == False
any([0, False]) == False
```
`function` **attr**
Creates a field definition for a trait, with a type, optional default value, and optional description.
`default` must match the declared type. Mutable defaults (lists, dicts) are deep-copied
when a trait instance is created, so each instance gets its own independent copy.
Example:
```starlark theme={null}
BazelTrait = trait(host=str, port=attr(int, default = 80))
r = BazelTrait(host="localhost") # port defaults to 80
```
`function` **breakpoint**
When a debugger is available, breaks into the debugger.
`function` **call\_stack**
def call\_stack(
\*,
strip\_frames: int = 0
) -> str
Get a textual representation of the call stack.
This is intended only for debugging purposes to display to a human and
should not be considered stable or parseable.
strip\_frames will pop N frames from the top of the call stack, which can
be useful to hide non-interesting lines - for example, strip\_frames=1
will hide the call to and location of `call_stack()` itself.
`function` **call\_stack\_frame**
def call\_stack\_frame(
n: int,
/
) -> None | StackFrame
Get a structural representation of the n-th call stack frame.
With `n=0` returns `call_stack_frame` itself.
Returns `None` if `n` is greater than or equal to the stack size.
`function` **chr**
[chr](https://github.com/bazelbuild/starlark/blob/master/spec.md#bool): returns a string encoding a codepoint.
`chr(i)` returns a string that encodes the single Unicode code
point whose value is specified by the integer `i`. `chr` fails
unless `0 ≤ i ≤ 0x10FFFF`.
```
chr(65) == 'A'
chr(1049) == 'Й'
chr(0x1F63F) == '😿'
```
`function` **debug**
Print the value with full debug formatting. The result may not be stable over time. Intended for debugging purposes and guaranteed to produce verbose output not suitable for user display.
`function` **dir**
[dir](https://github.com/bazelbuild/starlark/blob/master/spec.md#dir): list attributes of a value.
`dir(x)` returns a list of the names of the attributes (fields and
methods) of its operand. The attributes of a value `x` are the names
`f` such that `x.f` is a valid expression.
```
"capitalize" in dir("abc")
```
`function` **enum**
The `enum` type represents one value picked from a set of values.
For example:
```python theme={null}
MyEnum = enum("option1", "option2", "option3")
```
This statement defines an enumeration `MyEnum` that consists of the three values `"option1"`, `"option2"` and `option3`.
Now `MyEnum` is defined, it's possible to do the following:
* Create values of this type with `MyEnum("option2")`. It is a runtime error if the argument is not one of the predeclared values of the enumeration.
* Get the type of the enum suitable for a type annotation with `MyEnum`.
* Given a value of the enum (for example, `v = MyEnum("option2")`), get the underlying value `v.value == "option2"` or the index in the enumeration `v.index == 1`.
* Get a list of the values that make up the array with `MyEnum.values() == ["option1", "option2", "option3"]`.
* Treat `MyEnum` a bit like an array, with `len(MyEnum) == 3`, `MyEnum[1] == MyEnum("option2")` and iteration over enums `[x.value for x in MyEnum] == ["option1", "option2", "option3"]`.
Enumeration types store each value once, which are then efficiently referenced by enumeration values.
`function` **enumerate**
[enumerate](https://github.com/bazelbuild/starlark/blob/master/spec.md#enumerate): return a list of (index, element) from an iterable.
`enumerate(x)` returns a list of `(index, value)` pairs, each containing
successive values of the iterable sequence and the index of the
value within the sequence.
The optional second parameter, `start`, specifies an integer value to
add to each index.
```
enumerate(["zero", "one", "two"]) == [(0, "zero"), (1, "one"), (2, "two")]
enumerate(["one", "two"], 1) == [(1, "one"), (2, "two")]
```
`function` **eval\_type**
Create a runtime type object which can be used to check if a value matches the given type.
`function` **fail**
fail: fail the execution
```
fail("this is an error") # fail: this is an error
fail("oops", 1, False) # fail: oops 1 False
```
`function` **feature**
Declares a feature — a composable behavior injector for the fragment system.
The `implementation` function receives a `FeatureContext` and runs after all
config.axl files have been evaluated. It can inject closures into fragment
hook lists via `ctx.fragments[FragmentType].hook.append(...)`.
Feature CLI args are injected into every task subcommand on the CLI. Only named
optional flags are supported — positional args and `required = true` are not
allowed because features apply globally and would break any task that doesn't
supply the flag.
Every feature automatically gets an `enabled` CLI arg. It shows up as
`--{name}:enabled` on the command line and is accessible as `ctx.args.enabled`
in the implementation. Set `enabled = False` for opt-in features.
## Naming
Features must be exported as **CamelCase** (`ArtifactUpload`).
This is enforced at definition time. Features are referenced as type keys
(`ctx.features[ArtifactUpload]`), mirroring Bazel's provider convention
(`dep[CcInfo]`); CamelCase signals this type-key role.
The `name` field sets the kebab-case slug used as a prefix for every CLI arg this
feature declares: a feature named `"artifact-upload"` with arg `mode` exposes
`--artifact-upload:mode`. The name is auto-derived from the CamelCase export name
via `to_command_name` (`ArtifactUpload` → `artifact-upload`) if not set explicitly.
`display_name` overrides the Title Case name shown in CLI help section headings.
## Arg names
Arg keys must be `snake_case` (`[a-z][a-z0-9_]*`). There are two kinds:
* **CLI args** (`args.string(...)`, `args.boolean(...)`, etc.) — exposed as
`--{name}-{arg}` flags on every task subcommand; must be optional.
* **Config-only args** (`args.custom(type, default = …)`) — set in `config.axl`
only, not shown in help.
Both kinds are accessible as `ctx.args.arg_name` in the implementation.
## Example
```starlark theme={null}
def _impl(ctx: FeatureContext):
ctx.fragments[BazelFragment].build_end.append(
lambda task_ctx, state: upload_artifacts(
task_ctx, ctx.args.bucket, ctx.args.mode
)
)
ArtifactUpload = feature(
summary = "Upload build artifacts to S3 storage",
implementation = _impl,
args = {
"bucket": args.custom(str | None, default = None), # config.axl only
"mode": args.string(default = "auto"), # CLI flag: --artifact-upload-mode
},
)
```
`function` **field**
Creates a field record. Used as an argument to the `record` function.
```
rec_type = record(host=field(str), port=field(int), mask=field(int, default=255))
rec = rec_type(host="localhost", port=80)
rec.port == 80
rec.mask == 255
```
`function` **filter**
Apply a predicate to each element of the iterable, returning those that match. As a special case if the function is `None` then removes all the `None` values.
```
filter(bool, [0, 1, False, True]) == [1, True]
filter(lambda x: x > 2, [1, 2, 3, 4]) == [3, 4]
filter(None, [True, None, False]) == [True, False]
```
`function` **getattr**
[getattr](https://github.com/bazelbuild/starlark/blob/master/spec.md#getattr): returns the value of an attribute
`getattr(x, name)` returns the value of the attribute (field or method)
of x named `name`. It is a dynamic error if x has no such attribute.
`getattr(x, "f")` is equivalent to `x.f`.
`getattr(x, "f", d)` is equivalent to `x.f if hasattr(x, "f") else d`
and will never raise an error.
```
getattr("banana", "split")("a") == ["b", "n", "n", ""] # equivalent to "banana".split("a")
```
`function` **hasattr**
[hasattr](https://github.com/bazelbuild/starlark/blob/master/spec.md#hasattr): test if an object has an attribute
`hasattr(x, name)` reports whether x has an attribute (field or method)
named `name`.
`function` **hash**
[hash](https://github.com/bazelbuild/starlark/blob/master/spec.md#hash): returns the hash number of a value.
`hash(x)` returns an integer hash value for x such that `x == y`
implies `hash(x) == hash(y)`.
`hash` fails if x, or any value upon which its hash depends, is
unhashable.
```
hash("hello") != hash("world")
```
`function` **isinstance**
Check if a value matches the given type.
This operation can be very fast or very slow depending on how it is used.
`isinstance(x, list)` is very fast,
because it is compiled to a special bytecode instruction.
`isinstance(x, list[str])` is `O(N)` operation
because it checks every element in this list.
`L = list; [isinstance(x, L) for x in y]` is slow when `L` is not a constant:
`isinstance()` first converts `list` to a type in a loop, which is slow.
But last operation can be optimized like this:
`L = eval_type(list); [isinstance(x, L) for x in y]`:
`eval_type()` converts `list` value into prepared type matcher.
`function` **len**
[len](https://github.com/bazelbuild/starlark/blob/master/spec.md#len): get the length of a sequence
`len(x)` returns the number of elements in its argument.
It is a dynamic error if its argument is not a sequence.
```
len(()) == 0
len({}) == 0
len([]) == 0
len([1]) == 1
len([1,2]) == 2
len({'16': 10}) == 1
len(True) # error: not supported
```
`function` **map**
Apply a function to each element of the iterable, returning the results.
```
map(abs, [7, -5, -6]) == [7, 5, 6]
map(lambda x: x * 2, [1, 2, 3, 4]) == [2, 4, 6, 8]
```
`function` **max**
[max](https://github.com/bazelbuild/starlark/blob/master/spec.md#max): returns the maximum of a sequence.
`max(x)` returns the greatest element in the iterable sequence x.
It is an error if any element does not support ordered comparison,
or if the sequence is empty.
The optional named parameter `key` specifies a function to be applied
to each element prior to comparison.
```
max([3, 1, 4, 1, 5, 9]) == 9
max("two", "three", "four") == "two" # the lexicographically greatest
max("two", "three", "four", key=len) == "three" # the longest
```
`function` **min**
[min](https://github.com/bazelbuild/starlark/blob/master/spec.md#min): returns the minimum of a sequence.
`min(x)` returns the least element in the iterable sequence x.
It is an error if any element does not support ordered comparison,
or if the sequence is empty.
```
min([3, 1, 4, 1, 5, 9]) == 1
min("two", "three", "four") == "four" # the lexicographically least
min("two", "three", "four", key=len) == "two" # the shortest
```
`function` **ord**
[ord](https://github.com/bazelbuild/starlark/blob/master/spec.md#ord): returns the codepoint of a character
`ord(s)` returns the integer value of the sole Unicode code point
encoded by the string `s`.
If `s` does not encode exactly one Unicode code point, `ord` fails.
Each invalid code within the string is treated as if it encodes the
Unicode replacement character, U+FFFD.
Example:
```
ord("A") == 65
ord("Й") == 1049
ord("😿") == 0x1F63F
```
`function` **partial**
Construct a partial application. In almost all cases it is simpler to use a `lamdba`.
`function` **pprint**
`function` **prepr**
Like `repr`, but produces more verbose pretty-printed output
`function` **print**
Print some values to the output.
`function` **pstr**
Like `str`, but produces more verbose pretty-printed output
`function` **record**
def record(
\*\*kwargs: typing.Any
) -> function
A `record` type represents a set of named values, each with their own type.
For example:
```python theme={null}
MyRecord = record(host=str, port=int)
```
This above statement defines a record `MyRecord` with 2 fields, the first named `host` that must be of type `str`, and the second named `port` that must be of type `int`.
Now `MyRecord` is defined, it's possible to do the following:
* Create values of this type with `MyRecord(host="localhost", port=80)`. It is a runtime error if any arguments are missed, of the wrong type, or if any unexpected arguments are given.
* Get the type of the record suitable for a type annotation with `MyRecord.type`.
* Get the fields of the record. For example, `v = MyRecord(host="localhost", port=80)` will provide `v.host == "localhost"` and `v.port == 80`. Similarly, `dir(v) == ["host", "port"]`.
It is also possible to specify default values for parameters using the `field` function.
For example:
```python theme={null}
MyRecord = record(host=str, port=field(int, 80))
```
Now the `port` field can be omitted, defaulting to `80` is not present (for example, `MyRecord(host="localhost").port == 80`).
Records are stored deduplicating their field names, making them more memory efficient than dictionaries.
`function` **repr**
[repr](https://github.com/bazelbuild/starlark/blob/master/spec.md#repr): formats its argument as a string.
All strings in the result are double-quoted.
```
repr(1) == '1'
repr("x") == "\"x\""
repr([1, "x"]) == "[1, \"x\"]"
repr("test \"'") == "\"test \\\"'\""
repr("x\"y😿 \\'") == "\"x\\\"y\\U0001f63f \\\\'\""
```
`function` **reversed**
[reversed](https://github.com/bazelbuild/starlark/blob/master/spec.md#reversed): reverse a sequence
`reversed(x)` returns a new list containing the elements of the iterable
sequence x in reverse order.
```
reversed(['a', 'b', 'c']) == ['c', 'b', 'a']
reversed(range(5)) == [4, 3, 2, 1, 0]
reversed("stressed".elems()) == ["d", "e", "s", "s", "e", "r", "t", "s"]
reversed({"one": 1, "two": 2}.keys()) == ["two", "one"]
```
`function` **sorted**
[sorted](https://github.com/bazelbuild/starlark/blob/master/spec.md#sorted): sort a sequence
`sorted(x)` returns a new list containing the elements of the iterable
sequence x, in sorted order. The sort algorithm is stable.
The optional named parameter `reverse`, if true, causes `sorted` to
return results in reverse sorted order.
The optional named parameter `key` specifies a function of one
argument to apply to obtain the value's sort key.
The default behavior is the identity function.
```
sorted([3, 1, 4, 1, 5, 9]) == [1, 1, 3, 4, 5, 9]
sorted([3, 1, 4, 1, 5, 9], reverse=True) == [9, 5, 4, 3, 1, 1]
sorted(["two", "three", "four"], key=len) == ["two", "four", "three"] # shortest to longest
sorted(["two", "three", "four"], key=len, reverse=True) == ["three", "four", "two"] # longest to shortest
```
`function` **task**
def task(
\*,
implementation: typing.Callable\[\[TaskContext], None],
args: dict\[str, typing.Any] = ...,
summary: str = ...,
description: str = ...,
display\_name: str = ...,
group: list\[str] = \[],
name: str = ...,
traits: list = \[]
) -> Task
Declares a task — a named CLI command with an implementation function.
## Naming
Assign the result to a **snake\_case** variable. The CLI command name is derived
automatically by converting `_` to `-` (`axl_add` → `axl-add`).
Use `name = "explicit-name"` to override.
Command names must match `[a-z][a-z0-9-]*`.
## Args
Arg keys must be `snake_case` (`[a-z][a-z0-9_]*`). There are two kinds:
* **CLI args** (`args.string(...)`, `args.int(...)`, etc.) — exposed as `--kebab-flags` on
the CLI and accessible as `ctx.args.arg_name` in the implementation. Can be overridden
in `config.axl`; an explicit CLI flag always wins over a config override.
* **Config-only args** (`args.custom(type, default = …)`) — not shown in help; set by repo
maintainers in `config.axl` via `ctx.tasks["group/name"].args.arg_name = value`.
All args are read as `ctx.args.arg_name` in the implementation regardless of kind.
## Help text
* `summary` — one-liner shown in the task list; falls back to `" task defined in "`.
* `description` — extended prose shown in `--help` (replaces summary in that view).
* `friendly_kind` — Title Case label for help section headings; auto-derived from the kind.
## Aliases
Call `.alias(defaults = {...})` to declare a new top-level command
that shares this task's implementation but exposes overridden defaults
for one or more args. See the `task.alias` docstring for details.
## Example
```starlark theme={null}
def _impl(ctx: TaskContext) -> int:
ctx.std.io.stdout.write("Hello, " + ctx.args.recipient + "\n")
return 0
greet = task(
group = ["utils"],
summary = "Say hello",
implementation = _impl,
args = {
"recipient": args.string(default = "world", description = "Who to greet"),
"greeting": args.custom(str, default = "Hello", description = "Greeting word (config.axl only)"),
},
)
```
`function` **trait**
Creates a trait type — a shared configuration object that tasks opt into.
## Naming
Traits must be exported as **CamelCase** (`MyConfig`, `BazelTrait`). This is
enforced at definition time.
## Fields
Each field must be an `attr()` definition with a `default` value. The default is used
to construct the initial trait instance lazily on first access — there is no mechanism
to inject values before that construction, so all fields must have defaults.
## Example
```starlark theme={null}
BazelTrait = trait(
extra_flags = attr(list[str], default = [], description = "Extra Bazel flags for every build"),
profile_upload = attr(bool, default = False, description = "Upload Bazel profile after build"),
)
```
`function` **zip**
[zip](https://github.com/bazelbuild/starlark/blob/master/spec.md#zip): zip several iterables together
`zip()` returns a new list of n-tuples formed from corresponding
elements of each of the n iterable sequences provided as arguments to
`zip`. That is, the first tuple contains the first element of each of
the sequences, the second element contains the second element of each
of the sequences, and so on. The result list is only as long as the
shortest of the input sequences.
```
zip() == []
zip(range(5)) == [(0,), (1,), (2,), (3,), (4,)]
zip(range(5), "abc".elems()) == [(0, "a"), (1, "b"), (2, "c")]
```
`property` **False**
`property` **None**
`property` **True**
`property` **trace**
# Arg
Source: https://site.aspect.build/docs/axl/types/arg
`Arg` is the opaque value returned by the [`args`](/axl/types/args) builders
(`args.string`, `args.int`, `args.boolean`, `args.string_list`,
`args.trailing_var_args`, `args.positional`, `args.custom`, and their
list variants).
An `Arg` is a declaration — a schema entry that tells a task how to parse,
validate, and default a single argument. You do not construct or mutate
an `Arg` directly; you obtain one from an `args.*` builder and place it
in a task's `args` map. At invocation time, the parsed value is delivered
to your implementation through the [`Arguments`](/axl/types/arguments)
object, keyed by the map's name.
## Usage
Use an `Arg` as the value in a task's `args` dictionary. The dictionary
key becomes the argument's name — that name is derived to kebab-case for
the CLI flag (`snake_case_name` → `--snake-case-name`) unless you pass
`long = "override-name"` on the builder.
```starlark theme={null}
def _impl(ctx: TaskContext):
mode = ctx.args.get("mode")
tags = ctx.args.get("tags")
my_task = task(
implementation = _impl,
args = {
"mode": args.string(default = "auto", values = ["auto", "on", "off"]),
"tags": args.string_list(),
"verbose": args.boolean(default = False, short = "v"),
},
)
```
## Related
* [`args`](/axl/types/args) — builders that produce an `Arg`.
* [`Arguments`](/axl/types/arguments) — the parsed args passed to a task
implementation, including `is_explicit` to detect CLI-supplied values.
* [`Task.alias`](/axl/types/task) — override an existing `Arg`'s default
without redefining it.
# Args
Source: https://site.aspect.build/docs/axl/types/args
`type` [Args](/axl/types/args/args)
`function` **string\_list**
Defines a string list flag that can be specified multiple times.
Use `long = "override-name"` to override the default kebab-case derivation.
`function` **uint\_list**
Defines an unsigned integer list flag that can be specified multiple times.
Use `long = "override-name"` to override the default kebab-case derivation.
`function` **trailing\_var\_args**
def trailing\_var\_args(
\*,
description: None | str = None
) -> args.Arg
Defines a trailing variable argument that captures the remaining arguments without further parsing. Only one such argument is permitted, and it must be the last in the sequence.
`function` **boolean**
Defines a boolean flag. Use `--flag_name` (true) or `--flag_name=false`.
Use `long = "override-name"` to override the default kebab-case derivation.
`function` **int**
Defines an integer flag.
Use `long = "override-name"` to override the default kebab-case derivation.
`function` **uint**
Defines an unsigned integer flag.
Use `long = "override-name"` to override the default kebab-case derivation.
`function` **int\_list**
Defines an integer list flag that can be specified multiple times.
Use `long = "override-name"` to override the default kebab-case derivation.
`function` **custom**
Defines a config-only arg — not exposed on the CLI. Set via config.axl only.
The `type` argument must be a built-in or otherwise frozen type (e.g. `str`, `int`,
`bool`, `list[str]`). If provided, `default` must match the declared type.
Example:
```starlark theme={null}
my_task = task(
implementation = _impl,
args = {
"mode": args.string(default = "auto"),
"bucket": args.custom(str | None, default = None), # config.axl only
},
)
```
`function` **string**
Defines a string flag that can be specified as `--flag_name=flag_value`.
Use `long = "override-name"` to override the default kebab-case derivation.
`function` **boolean\_list**
Defines a boolean list flag that can be specified multiple times.
Use `long = "override-name"` to override the default kebab-case derivation.
`function` **positional**
Defines a positional argument that accepts a range of values.
# Args
Source: https://site.aspect.build/docs/axl/types/args/args
# Arguments
Source: https://site.aspect.build/docs/axl/types/arguments
`function` **Arguments.is\_explicit**
def Arguments.is\_explicit(
name: str,
/
) -> bool
Return `True` iff the user passed `--=...` (or its short form) on the CLI for the current invocation.
Returns `False` for args that were resolved from the task's
default, an alias's overridden default, or a `config.axl`
override — anything that wasn't typed at the command line.
Used by repro / fix command builders to skip echoing flags
whose values would just reproduce the task's default. The
developer copying the rendered repro gets back exactly what
they (or CI) typed.
# Aspect
Source: https://site.aspect.build/docs/axl/types/aspect
`type` [Aspect](/axl/types/aspect/aspect)
`module` [auth](/axl/types/aspect/auth)
# Aspect
Source: https://site.aspect.build/docs/axl/types/aspect/aspect
`property` **Aspect.auth**
# Auth
Source: https://site.aspect.build/docs/axl/types/aspect/auth
`type` [AuthSession](/axl/types/aspect/auth/auth_session)
`type` [AuthCredentials](/axl/types/aspect/auth/auth_credentials)
`type` [Auth](/axl/types/aspect/auth/auth)
# Auth
Source: https://site.aspect.build/docs/axl/types/aspect/auth/auth
`function` **Auth.credentials**
`function` **Auth.login**
def Auth.login(
\*,
token: str = ...,
api\_token: str = ...
) -> typing.Any
`function` **Auth.logout**
`function` **Auth.persist**
`property` **Auth.api\_url**
# Auth credentials
Source: https://site.aspect.build/docs/axl/types/aspect/auth/auth_credentials
`property` **AuthCredentials.access\_token**
AuthCredentials.access\_token: str
`property` **AuthCredentials.email**
AuthCredentials.email: str
`property` **AuthCredentials.name**
AuthCredentials.name: str
`property` **AuthCredentials.tenant\_id**
AuthCredentials.tenant\_id: str
`property` **AuthCredentials.token\_status**
AuthCredentials.token\_status: str
# Auth session
Source: https://site.aspect.build/docs/axl/types/aspect/auth/auth_session
`function` **AuthSession.wait**
`property` **AuthSession.url**
# Bazel
Source: https://site.aspect.build/docs/axl/types/bazel
`type` [SandboxRecoveryResult](/axl/types/bazel/sandbox_recovery_result)
`type` [BazelRC](/axl/types/bazel/bazel_r_c)
`type` [Bazel](/axl/types/bazel/bazel)
`type` [HealthCheckResult](/axl/types/bazel/health_check_result)
`module` [query](/axl/types/bazel/query)
`module` [build](/axl/types/bazel/build)
`module` [build\_events](/axl/types/bazel/build_events)
`module` [execution\_log](/axl/types/bazel/execution_log)
# Bazel
Source: https://site.aspect.build/docs/axl/types/bazel/bazel
`function` **Bazel.build**
def Bazel.build(
\*targets: str,
build\_events: bool | list\[bazel.build.BuildEventIter | bazel.build.BuildEventSink] = ...,
workspace\_events: bool = False,
execution\_log: bool | list\[bazel.execlog.ExecLogSink] = ...,
flags: list\[str | (str, str)] = \[],
stdout: None | std.io.Writable = ...,
stderr: None | std.io.Writable = ...,
stdio: None | std.io.Stdio = None,
current\_dir: None | str = None,
announce\_version: bool = False,
announce\_command: bool = False
) -> bazel.build.Build
Build one or more Bazel targets.
Returns a `Build` object. The call does not block — use `.wait()` to
wait for the invocation to finish and retrieve its exit status.
**Parameters**
* `execution_log`: Enable Bazel execution log collection. Pass `True` to enable the in-memory decoded iterator (accessible via `build.execution_logs()`), or pass a list of sinks such as `[execution_log.compact_file(path = "out.binpb.zst")]` to write the log to one or more files. Sinks and the iterator can be combined: passing a list of sinks still allows calling `build.execution_logs()` to iterate entries in-process.
"//my/pkg:target",
flags = \[
"--config=release",
("--notmp\_sandbox", ">=8"),
("--some\_legacy\_flag", "\<7"),
],
stdout = None, # discard child stdout
stderr = ctx.std.fs.create("bazel.err"), # redirect stderr to a file
`function` **Bazel.cancel\_invocation**
Cancel whatever invocation is currently running on the Bazel server.
Finds the bazel client process holding the server lock and sends it
SIGINT (graceful cancellation, like Ctrl+C). The client then forwards
a CancelRequest RPC to the server. Returns a `Cancellation` with
status and control methods.
**Parameters**
* `force_kill_after_ms`: - If the build is still running after this many milliseconds, `wait()` will automatically escalate by sending the 2nd and 3rd SIGINT to the Bazel client (the 3rd triggers Bazel's built-in server kill, equivalent to Ctrl+C three times). If the client still doesn't exit, falls back to SIGKILL on both client and server. Defaults to 5000ms. Set to 0 to disable auto-escalation and manage cancellation manually via `wait(timeout_ms=...)` and `force()`.
`function` **Bazel.health\_check**
Probe the Bazel server to determine whether it is responsive.
Runs `bazel --noblock_for_lock info server_pid`. If the server is
unresponsive, attempts recovery by killing the server process and
re-checking.
Returns a `HealthCheckResult` with `.success`, `.healthy`, `.message`,
and `.exit_code` attributes.
**Examples**
```python theme={null}
def _health_probe_impl(ctx):
result = ctx.bazel.health_check()
if not result.healthy:
fail("Bazel server is unhealthy")
```
`function` **Bazel.info**
Run `bazel info` and return all key/value pairs as a dict.
Blocks until the command completes. Raises an error if Bazel exits
with a non-zero code.
`function` **Bazel.parse\_rc**
Parse `.bazelrc` files rooted at `root` and return a `BazelRC` object.
**Parameters**
* `root`: - Bazel workspace root directory. Defaults to `env.bazel_root_dir` — the deepest `MODULE.bazel` / `WORKSPACE` ancestor of `cwd`. Pass an explicit `root` only to read a bazelrc outside the surrounding workspace; passing the Aspect root here in a sub-workspace layout would read the outer `.bazelrc` and leak the parent project's flags.
* `startup_flags`: - Startup flags (e.g. `["--bazelrc=/path/to/extra.bazelrc"]`).
* `flags`: - Command-line flags to inject as synthetic `always` options (e.g. `["--config=opt"]`).
rc = ctx.bazel.parse\_rc(flags = \["--config=opt"])
build = ctx.bazel.build("//...", flags = rc.expand(command = "build"))
build.wait()
`function` **Bazel.query**
The query system provides a programmatic interface for analyzing build dependencies and target relationships. Queries are constructed using a chain API and are lazily evaluated only when `.eval()` is explicitly called.
The entry point is `ctx.bazel.query()`, which returns a `query` for creating initial
query expressions. Most operations operate on `query` objects, which represent
sets of targets that can be filtered, transformed, and combined.
**Example**
```starlark theme={null}
**Query** dependencies of a target
deps = ctx.bazel.query().targets("//myapp:main").deps()
all_deps = deps.eval()
**Chain** multiple operations
sources = ctx.bazel.query().targets("//myapp:main")
.deps()
.kind("source file")
.eval()
```
`function` **Bazel.recover\_poisoned\_sandbox**
Detect and best-effort repair runner-poisoning sandbox state described by bazelbuild/bazel#23880.
Inspects `/sandbox/` for entries outside Bazel's
`SANDBOX_BASE_PERSISTENT_DIRS` whitelist
(`{.DS_Store, sandbox_stash, sandbox_stash_temp, _moved_trash_dir}`).
Anything else present after a bazel command exited is the
poisoning signature: Bazel's own `afterCommand` cleanup left
state behind because its spawn-runner registry didn't include
the strategy that owned that subtree (see `LinuxSandboxedStrategy.
create` IOException path; fixed upstream by `abe8d6090` in 9.0+).
When poisoning is detected, every offending entry is
`rm -rf`'d. The result distinguishes three cases:
* `"clean"`: nothing to remove. Either the sandbox dir didn't
exist or contained only whitelisted entries.
* `"repaired"`: at least one non-whitelisted entry was found
and all of them were successfully removed. The runner is
safe to keep serving jobs.
* `"still_poisoned"`: at least one entry survived the removal
attempt (e.g. EPERM on the underlying filesystem). The runner
will keep crashing every bazel command on the same output
base — the caller should mark it unhealthy.
Reads `--output_base=` from `ctx.bazel.startup_flags`. When
`--output_base` is not present (e.g. local dev), returns `"clean"`
— there's no way to know which outputabase to inspect, and the
failure mode is a Workflows-runner-only concern in practice.
Safe to call only AFTER the most recent bazel client invocation
has fully exited — a live `bazel build/test` against the same
outputabase would race the removal.
**Examples**
```python theme={null}
def _post_bazel_hook(ctx, exit_code):
if exit_code == 0:
return
r = ctx.bazel.recover_poisoned_sandbox()
if r.outcome == "repaired":
print("Recovered from bazel#23880 poisoning: " + ", ".join(r.removed))
elif r.outcome == "still_poisoned":
signal_instance_unhealthy()
```
`function` **Bazel.test**
def Bazel.test(
\*targets: str,
build\_events: bool | list\[bazel.build.BuildEventIter | bazel.build.BuildEventSink] = ...,
workspace\_events: bool = False,
execution\_log: bool | list\[bazel.execlog.ExecLogSink] = ...,
flags: list\[str | (str, str)] = \[],
stdout: None | std.io.Writable = ...,
stderr: None | std.io.Writable = ...,
stdio: None | std.io.Stdio = None,
current\_dir: None | str = None,
announce\_version: bool = False,
announce\_command: bool = False
) -> bazel.build.Build
Build and test one or more Bazel targets.
Returns a `Build` object. The call does not block — use `.wait()` to
wait for the invocation to finish and retrieve its exit status.
**Parameters**
* `execution_log`: Enable Bazel execution log collection. Pass `True` to enable the in-memory decoded iterator (accessible via `build.execution_logs()`), or pass a list of sinks such as `[execution_log.compact_file(path = "out.binpb.zst")]` to write the log to one or more files. Sinks and the iterator can be combined: passing a list of sinks still allows calling `build.execution_logs()` to iterate entries in-process.
"//my/pkg:test",
flags = \[
"--test\_output=errors",
("--notmp\_sandbox", ">=8"),
("--some\_legacy\_flag", "\<7"),
],
`property` **Bazel.startup\_flags**
Mutable list of startup flags prepended to every Bazel invocation on this context.
# Bazel r c
Source: https://site.aspect.build/docs/axl/types/bazel/bazel_r_c
`function` **BazelRC.announce**
def BazelRC.announce(
\*,
command: str,
ansi: bool = False,
max\_width: int = 120
) -> str
Return a human-readable summary of all options loaded for `command`.
Options are grouped by source file and wrapped at `max_width` columns (default 120).
Pass `ansi = True` to enable bold/dim ANSI styling on headers and section names.
Useful for debugging which flags were picked up and from which rc file:
```python theme={null}
print(rc.announce(command = "build"))
print(rc.announce(command = "build", ansi = True))
```
`function` **BazelRC.expand**
def BazelRC.expand(
\*,
command: str
) -> list
Expand all `--config=` flags for `command` and return the fully-resolved list.
Each item is either a plain `str` (unconditional) or a `(str, str)` tuple
`(flag, version_condition)` for version-gated flags. This format is directly
compatible with `ctx.bazel.build(flags=...)`.
`function` **BazelRC.expand\_all**
def BazelRC.expand\_all(
\*,
command: str
) -> (list, list)
Expand all `--config=` flags for `command` and split results by origin section.
Options from `common` sections cannot be passed directly on the Bazel CLI — they must be
injected via startup flags so Bazel applies the correct silent-ignore semantics. All other
options (`always`, `build`, etc.) are safe to pass as regular command flags.
Returns a `(startup_flags, flags)` tuple:
* `startup_flags`: `["--default_override=0:common=", ...]`
* `flags`: direct command flags (same `str | (str, str)` format as `expand()`)
# Example
```python theme={null}
startup_flags, flags = rc.expand_all(command = "build")
ctx.bazel.build("//...", flags = flags, startup_flags = startup_flags)
```
`function` **BazelRC.options\_for**
def BazelRC.options\_for(
\*,
command: str
) -> list
Return all options applicable to `command` without expanding `--config=` flags.
Each item is either a plain `str` (unconditional) or a `(str, str)` tuple
`(flag, version_condition)` for version-gated flags.
`function` **BazelRC.sources**
def BazelRC.sources() -> list
Return the list of source file paths that were loaded.
# Build
Source: https://site.aspect.build/docs/axl/types/bazel/build
`type` [BuildStatus](/axl/types/bazel/build/build_status)
`type` [Cancellation](/axl/types/bazel/build/cancellation)
`type` [ExecutionLogIterator](/axl/types/bazel/build/execution_log_iterator)
`type` [BuildEventSink](/axl/types/bazel/build/build_event_sink)
`type` [BuildEventIter](/axl/types/bazel/build/build_event_iter)
`type` [WorkspaceEventIterator](/axl/types/bazel/build/workspace_event_iterator)
`type` [Build](/axl/types/bazel/build/build)
`module` [build\_event](/axl/types/bazel/build/build_event/build_event)
`module` [execution\_log](/axl/types/bazel/execution_log)
`module` [workspace\_event](/axl/types/bazel/build/workspace_event/workspace_event)
# Build
Source: https://site.aspect.build/docs/axl/types/bazel/build/build
`function` **Build.execution\_logs**
`function` **Build.try\_wait**
`function` **Build.wait**
Block until the Bazel invocation finishes and return a `BuildStatus`.
After `wait()` returns, the execution log pipe has been closed and the
producer thread has exited. Calling `execution_logs()` after `wait()`
will fail — the stream is consumed as part of the wait. Iterate
`execution_logs()` **before** calling `wait()` if you need to process
entries.
`build_events()` remains usable after `wait()` for replaying historical
events, because the build event stream retains its buffer.
`function` **Build.workspace\_events**
# Build event
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event
`module` [build\_event\_id](/axl/types/bazel/build/build_event/build_event_id)
`module` [aborted](/axl/types/bazel/build/build_event/aborted)
`module` [workspace\_status](/axl/types/bazel/build/build_event/workspace_status)
`module` [pattern\_expanded](/axl/types/bazel/build/build_event/pattern_expanded)
`module` [file](/axl/types/bazel/execution_log/file)
`module` [test\_result](/axl/types/bazel/build/build_event/test_result)
`module` [build\_finished](/axl/types/bazel/build/build_event/build_finished)
`module` [build\_metrics](/axl/types/bazel/build/build_event/build_metrics)
`module` [convenience\_symlink](/axl/types/bazel/build/build_event/convenience_symlink)
`module` [build\_event](/axl/types/bazel/build/build_event/build_event)
`function` **WorkspaceStatus**
`function` **Aborted**
`function` **OptionsParsed**
def OptionsParsed(
\*\*kwargs: typing.Any
) -> options\_parsed
`function` **Configuration**
def Configuration(
\*\*kwargs: typing.Any
) -> configuration
`function` **ExecRequestConstructed**
def ExecRequestConstructed(
\*\*kwargs: typing.Any
) -> exec\_request\_constructed
`function` **BuildMetrics**
`function` **BuildEventId**
`function` **TargetComplete**
def TargetComplete(
\*\*kwargs: typing.Any
) -> target\_complete
`function` **PatternExpanded**
`function` **BuildToolLogs**
def BuildToolLogs(
\*\*kwargs: typing.Any
) -> build\_tool\_logs
`function` **ConvenienceSymlinksIdentified**
def ConvenienceSymlinksIdentified(
\*\*kwargs: typing.Any
) -> convenience\_symlinks\_identified
`function` **BuildMetadata**
def BuildMetadata(
\*\*kwargs: typing.Any
) -> build\_metadata
`function` **BuildStarted**
def BuildStarted(
\*\*kwargs: typing.Any
) -> build\_started
`function` **NamedSetOfFiles**
def NamedSetOfFiles(
\*\*kwargs: typing.Any
) -> named\_set\_of\_files
`function` **ConvenienceSymlink**
`function` **BuildFinished**
`function` **TestResult**
`function` **BuildEvent**
`function` **TestProgress**
def TestProgress(
\*\*kwargs: typing.Any
) -> test\_progress
`function` **File**
`function` **ActionExecuted**
def ActionExecuted(
\*\*kwargs: typing.Any
) -> action\_executed
`function` **TargetSummary**
def TargetSummary(
\*\*kwargs: typing.Any
) -> target\_summary
`function` **UnstructuredCommandLine**
def UnstructuredCommandLine(
\*\*kwargs: typing.Any
) -> unstructured\_command\_line
`function` **OutputGroup**
def OutputGroup(
\*\*kwargs: typing.Any
) -> output\_group
`function` **TargetConfigured**
def TargetConfigured(
\*\*kwargs: typing.Any
) -> target\_configured
`function` **TestSummary**
def TestSummary(
\*\*kwargs: typing.Any
) -> test\_summary
`function` **Progress**
def Progress(
\*\*kwargs: typing.Any
) -> progress
`function` **WorkspaceConfig**
def WorkspaceConfig(
\*\*kwargs: typing.Any
) -> workspace\_config
`function` **Fetch**
`function` **EnvironmentVariable**
def EnvironmentVariable(
\*\*kwargs: typing.Any
) -> environment\_variable
# Aborted
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/aborted
# Build event
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/build_event
# Build event id
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/build_event_id
`function` **StructuredCommandLineId**
def StructuredCommandLineId(
\*\*kwargs: typing.Any
) -> structured\_command\_line\_id
`function` **BuildToolLogsId**
def BuildToolLogsId(
\*\*kwargs: typing.Any
) -> build\_tool\_logs\_id
`function` **WorkspaceStatusId**
def WorkspaceStatusId(
\*\*kwargs: typing.Any
) -> workspace\_status\_id
`function` **TestProgressId**
def TestProgressId(
\*\*kwargs: typing.Any
) -> test\_progress\_id
`function` **TestResultId**
def TestResultId(
\*\*kwargs: typing.Any
) -> test\_result\_id
`function` **OptionsParsedId**
def OptionsParsedId(
\*\*kwargs: typing.Any
) -> options\_parsed\_id
`function` **BuildStartedId**
def BuildStartedId(
\*\*kwargs: typing.Any
) -> build\_started\_id
`function` **TargetSummaryId**
def TargetSummaryId(
\*\*kwargs: typing.Any
) -> target\_summary\_id
`function` **TargetConfiguredId**
def TargetConfiguredId(
\*\*kwargs: typing.Any
) -> target\_configured\_id
`function` **ActionCompletedId**
def ActionCompletedId(
\*\*kwargs: typing.Any
) -> action\_completed\_id
`function` **TestSummaryId**
def TestSummaryId(
\*\*kwargs: typing.Any
) -> test\_summary\_id
`function` **BuildMetricsId**
def BuildMetricsId(
\*\*kwargs: typing.Any
) -> build\_metrics\_id
`function` **ExecRequestId**
def ExecRequestId(
\*\*kwargs: typing.Any
) -> exec\_request\_id
`function` **PatternExpandedId**
def PatternExpandedId(
\*\*kwargs: typing.Any
) -> pattern\_expanded\_id
`function` **NamedSetOfFilesId**
def NamedSetOfFilesId(
\*\*kwargs: typing.Any
) -> named\_set\_of\_files\_id
`function` **BuildFinishedId**
def BuildFinishedId(
\*\*kwargs: typing.Any
) -> build\_finished\_id
`function` **ConfiguredLabelId**
def ConfiguredLabelId(
\*\*kwargs: typing.Any
) -> configured\_label\_id
`function` **ConvenienceSymlinksIdentifiedId**
def ConvenienceSymlinksIdentifiedId(
\*\*kwargs: typing.Any
) -> convenience\_symlinks\_identified\_id
`function` **BuildMetadataId**
def BuildMetadataId(
\*\*kwargs: typing.Any
) -> build\_metadata\_id
`function` **UnstructuredCommandLineId**
def UnstructuredCommandLineId(
\*\*kwargs: typing.Any
) -> unstructured\_command\_line\_id
`function` **UnconfiguredLabelId**
def UnconfiguredLabelId(
\*\*kwargs: typing.Any
) -> unconfigured\_label\_id
`function` **ProgressId**
def ProgressId(
\*\*kwargs: typing.Any
) -> progress\_id
`function` **FetchId**
def FetchId(
\*\*kwargs: typing.Any
) -> fetch\_id
`function` **TargetCompletedId**
def TargetCompletedId(
\*\*kwargs: typing.Any
) -> target\_completed\_id
`function` **ConfigurationId**
def ConfigurationId(
\*\*kwargs: typing.Any
) -> configuration\_id
`function` **WorkspaceConfigId**
def WorkspaceConfigId(
\*\*kwargs: typing.Any
) -> workspace\_config\_id
`function` **UnknownBuildEventId**
def UnknownBuildEventId(
\*\*kwargs: typing.Any
) -> unknown\_build\_event\_id
# Build finished
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/build_finished
`function` **ExitCode**
def ExitCode(
\*\*kwargs: typing.Any
) -> exit\_code
`function` **AnomalyReport**
def AnomalyReport(
\*\*kwargs: typing.Any
) -> anomaly\_report
# Build metrics
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/build_metrics
`module` [action\_summary](/axl/types/bazel/build/build_event/build_metrics/action_summary)
`module` [memory\_metrics](/axl/types/bazel/build/build_event/build_metrics/memory_metrics)
`module` [artifact\_metrics](/axl/types/bazel/build/build_event/build_metrics/artifact_metrics)
`module` [build\_graph\_metrics](/axl/types/bazel/build/build_event/build_metrics/build_graph_metrics)
`module` [worker\_metrics](/axl/types/bazel/build/build_event/build_metrics/worker_metrics)
`module` [network\_metrics](/axl/types/bazel/build/build_event/build_metrics/network_metrics)
`module` [worker\_pool\_metrics](/axl/types/bazel/build/build_event/build_metrics/worker_pool_metrics)
`module` [dynamic\_execution\_metrics](/axl/types/bazel/build/build_event/build_metrics/dynamic_execution_metrics)
`function` **MemoryMetrics**
`function` **NetworkMetrics**
`function` **ArtifactMetrics**
`function` **EvaluationStat**
def EvaluationStat(
\*\*kwargs: typing.Any
) -> evaluation\_stat
`function` **WorkerMetrics**
`function` **TimingMetrics**
def TimingMetrics(
\*\*kwargs: typing.Any
) -> timing\_metrics
`function` **WorkerPoolMetrics**
`function` **TargetMetrics**
def TargetMetrics(
\*\*kwargs: typing.Any
) -> target\_metrics
`function` **ActionSummary**
`function` **CumulativeMetrics**
def CumulativeMetrics(
\*\*kwargs: typing.Any
) -> cumulative\_metrics
`function` **PackageMetrics**
def PackageMetrics(
\*\*kwargs: typing.Any
) -> package\_metrics
`function` **BuildGraphMetrics**
`function` **DynamicExecutionMetrics**
# Action summary
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/build_metrics/action_summary
`function` **ActionData**
def ActionData(
\*\*kwargs: typing.Any
) -> action\_data
`function` **RunnerCount**
def RunnerCount(
\*\*kwargs: typing.Any
) -> runner\_count
# Artifact metrics
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/build_metrics/artifact_metrics
`function` **FilesMetric**
def FilesMetric(
\*\*kwargs: typing.Any
) -> files\_metric
# Build graph metrics
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/build_metrics/build_graph_metrics
`function` **RuleClassCount**
def RuleClassCount(
\*\*kwargs: typing.Any
) -> rule\_class\_count
`function` **AspectCount**
def AspectCount(
\*\*kwargs: typing.Any
) -> aspect\_count
# Dynamic execution metrics
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/build_metrics/dynamic_execution_metrics
`function` **RaceStatistics**
def RaceStatistics(
\*\*kwargs: typing.Any
) -> race\_statistics
# Memory metrics
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/build_metrics/memory_metrics
`function` **GarbageMetrics**
def GarbageMetrics(
\*\*kwargs: typing.Any
) -> garbage\_metrics
# Network metrics
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/build_metrics/network_metrics
`function` **SystemNetworkStats**
def SystemNetworkStats(
\*\*kwargs: typing.Any
) -> system\_network\_stats
# Worker metrics
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/build_metrics/worker_metrics
`function` **WorkerStats**
def WorkerStats(
\*\*kwargs: typing.Any
) -> worker\_stats
# Worker pool metrics
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/build_metrics/worker_pool_metrics
`function` **WorkerPoolStats**
def WorkerPoolStats(
\*\*kwargs: typing.Any
) -> worker\_pool\_stats
# Convenience symlink
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/convenience_symlink
# File
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/file
# Pattern expanded
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/pattern_expanded
`function` **TestSuiteExpansion**
def TestSuiteExpansion(
\*\*kwargs: typing.Any
) -> test\_suite\_expansion
# Test result
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/test_result
`module` [execution\_info](/axl/types/bazel/build/build_event/test_result/execution_info)
`function` **ExecutionInfo**
# Execution info
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/test_result/execution_info
`function` **TimingBreakdown**
def TimingBreakdown(
\*\*kwargs: typing.Any
) -> timing\_breakdown
`function` **ResourceUsage**
def ResourceUsage(
\*\*kwargs: typing.Any
) -> resource\_usage
# Workspace status
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event/workspace_status
`function` **Item**
# Build event iter
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event_iter
`function` **BuildEventIter.drain**
def BuildEventIter.drain() -> None | bool
Stop iterating: unsubscribe, drop buffered events. Idempotent.
`function` **BuildEventIter.try\_pop**
Non-blocking pop. Returns `None` when empty or disconnected. Honors the `kinds=` filter.
# Build event iterator
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event_iterator
`function` **iterator.drain**
Stop iterating: unsubscribe, drop buffered events. Idempotent.
`function` **iterator.try\_pop**
Non-blocking pop. Returns `None` when empty or disconnected. Honors the `kinds=` filter.
# Build event sink
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_event_sink
`function` **BuildEventSink.wait**
def BuildEventSink.wait() -> None | bool
Block until this sink finishes flushing. Idempotent.
# Build status
Source: https://site.aspect.build/docs/axl/types/bazel/build/build_status
`property` **BuildStatus.code**
`property` **BuildStatus.success**
BuildStatus.success: bool
# Cancellation
Source: https://site.aspect.build/docs/axl/types/bazel/build/cancellation
`function` **Cancellation.force**
def Cancellation.force() -> bool
Forcefully cancel the invocation.
Sends the 2nd and 3rd SIGINT to the Bazel client, following Bazel's
3-stage cancellation protocol (the 1st SIGINT was already sent by
`cancel_invocation()`). The 3rd SIGINT triggers Bazel's built-in
`KillServerProcess` which kills the server and exits the client.
If the client doesn't exit within 5 seconds, falls back to SIGKILL
on both the client and server. If no client is found holding the lock
(e.g. the client crashed), sends SIGKILL directly to the server daemon.
Returns `True` if a signal was sent, `False` if neither the client nor
server could be found (the build may have already finished).
`function` **Cancellation.wait**
def Cancellation.wait(
\*,
poll\_ms: int = 200,
timeout\_ms: int = 0
) -> bool
Block until the cancelled invocation finishes.
Polls until the server is no longer busy. If the `force_kill_after_ms`
deadline (set on `cancel_invocation`) is reached while still busy,
automatically escalates by calling `force()`.
Returns `True` if the server became free (either gracefully or after
force-kill). Returns `False` only if `timeout_ms` is set and reached
before the server became free (in this case no automatic escalation
occurs — use `force()` manually).
`property` **Cancellation.busy**
Whether the bazel server is currently busy (lock held by another client). Queries in real time via `bazel --noblock_for_lock info`.
# Execution log
Source: https://site.aspect.build/docs/axl/types/bazel/build/execution_log
`module` [platform](/axl/types/remote/execution/platform)
`module` [exec\_log\_entry](/axl/types/bazel/build/execution_log/exec_log_entry)
`function` **Platform**
`function` **File**
`function` **EnvironmentVariable**
def EnvironmentVariable(
\*\*kwargs: typing.Any
) -> environment\_variable
`function` **SpawnMetrics**
def SpawnMetrics(
\*\*kwargs: typing.Any
) -> spawn\_metrics
`function` **SpawnExec**
def SpawnExec(
\*\*kwargs: typing.Any
) -> spawn\_exec
`function` **Digest**
def Digest(
\*\*kwargs: typing.Any
) -> digest
`function` **ExecLogEntry**
# Exec log entry
Source: https://site.aspect.build/docs/axl/types/bazel/build/execution_log/exec_log_entry
`module` [output](/axl/types/bazel/build/execution_log/exec_log_entry/output)
`function` **Directory**
def Directory(
\*\*kwargs: typing.Any
) -> directory
`function` **Output**
`function` **File**
`function` **Spawn**
`function` **SymlinkAction**
def SymlinkAction(
\*\*kwargs: typing.Any
) -> symlink\_action
`function` **UnresolvedSymlink**
def UnresolvedSymlink(
\*\*kwargs: typing.Any
) -> unresolved\_symlink
`function` **RunfilesTree**
def RunfilesTree(
\*\*kwargs: typing.Any
) -> runfiles\_tree
`function` **Invocation**
def Invocation(
\*\*kwargs: typing.Any
) -> invocation
`function` **SymlinkEntrySet**
def SymlinkEntrySet(
\*\*kwargs: typing.Any
) -> symlink\_entry\_set
`function` **InputSet**
def InputSet(
\*\*kwargs: typing.Any
) -> input\_set
# Output
Source: https://site.aspect.build/docs/axl/types/bazel/build/execution_log/exec_log_entry/output
# Platform
Source: https://site.aspect.build/docs/axl/types/bazel/build/execution_log/platform
`function` **Property**
def Property(
\*\*kwargs: typing.Any
) -> property
# Execution log iterator
Source: https://site.aspect.build/docs/axl/types/bazel/build/execution_log_iterator
`function` **ExecutionLogIterator.done**
def ExecutionLogIterator.done() -> bool
Returns `True` if stream is complete and all the events are received via `for` or calling `try_pop` repeatedly.
`function` **ExecutionLogIterator.try\_pop**
Returns `ExecLogEntry` if event buffer is not empty. Maximum `1000` events is buffered at once.
# Workspace event
Source: https://site.aspect.build/docs/axl/types/bazel/build/workspace_event
`module` [workspace\_event](/axl/types/bazel/build/workspace_event/workspace_event)
`function` **ExtractEvent**
def ExtractEvent(
\*\*kwargs: typing.Any
) -> extract\_event
`function` **RenameEvent**
def RenameEvent(
\*\*kwargs: typing.Any
) -> rename\_event
`function` **WhichEvent**
def WhichEvent(
\*\*kwargs: typing.Any
) -> which\_event
`function` **ReadEvent**
def ReadEvent(
\*\*kwargs: typing.Any
) -> read\_event
`function` **DeleteEvent**
def DeleteEvent(
\*\*kwargs: typing.Any
) -> delete\_event
`function` **DownloadAndExtractEvent**
def DownloadAndExtractEvent(
\*\*kwargs: typing.Any
) -> download\_and\_extract\_event
`function` **OsEvent**
def OsEvent(
\*\*kwargs: typing.Any
) -> os\_event
`function` **FileEvent**
def FileEvent(
\*\*kwargs: typing.Any
) -> file\_event
`function` **SymlinkEvent**
def SymlinkEvent(
\*\*kwargs: typing.Any
) -> symlink\_event
`function` **PatchEvent**
def PatchEvent(
\*\*kwargs: typing.Any
) -> patch\_event
`function` **WorkspaceEvent**
`function` **TemplateEvent**
def TemplateEvent(
\*\*kwargs: typing.Any
) -> template\_event
`function` **DownloadEvent**
def DownloadEvent(
\*\*kwargs: typing.Any
) -> download\_event
`function` **ExecuteEvent**
def ExecuteEvent(
\*\*kwargs: typing.Any
) -> execute\_event
# Workspace event
Source: https://site.aspect.build/docs/axl/types/bazel/build/workspace_event/workspace_event
# Workspace event iterator
Source: https://site.aspect.build/docs/axl/types/bazel/build/workspace_event_iterator
`function` **WorkspaceEventIterator.done**
def WorkspaceEventIterator.done() -> bool
Returns `True` if stream is complete and all the events are received via `for` or calling `pop` repeatedly.
`function` **WorkspaceEventIterator.try\_pop**
Returns `WorkspaceEvent` if event buffer is not empty. Maximum `1000` events is buffered at once.
# Build events
Source: https://site.aspect.build/docs/axl/types/bazel/build_events
`type` [grpc](/axl/types/bazel/build_events/grpc)
`type` [iterator](/axl/types/bazel/build_events/iterator)
`function` **file**
# Grpc
Source: https://site.aspect.build/docs/axl/types/bazel/build_events/grpc
`function` **grpc.wait**
Block until this sink finishes flushing. Idempotent.
# Iterator
Source: https://site.aspect.build/docs/axl/types/bazel/build_events/iterator
`function` **iterator.drain**
Stop iterating: unsubscribe, drop buffered events. Idempotent.
`function` **iterator.try\_pop**
Non-blocking pop. Returns `None` when empty or disconnected. Honors the `kinds=` filter.
# Execution log
Source: https://site.aspect.build/docs/axl/types/bazel/execution_log
`type` [ExecLogSink](/axl/types/bazel/execution_log/exec_log_sink)
`type` [file](/axl/types/bazel/execution_log/file)
`function` **compact\_file**
# Exec log sink
Source: https://site.aspect.build/docs/axl/types/bazel/execution_log/exec_log_sink
# File
Source: https://site.aspect.build/docs/axl/types/bazel/execution_log/file
# Health check result
Source: https://site.aspect.build/docs/axl/types/bazel/health_check_result
`property` **HealthCheckResult.exit\_code**
HealthCheckResult.exit\_code: None | int
The original Bazel exit code, if available.
`property` **HealthCheckResult.message**
HealthCheckResult.message: None | str
Diagnostic message, if any.
`property` **HealthCheckResult.outcome**
HealthCheckResult.outcome: str
The server health state: `"healthy"`, `"unhealthy"`, or `"inconclusive"`.
# Query
Source: https://site.aspect.build/docs/axl/types/bazel/query
`type` [TargetSet](/axl/types/bazel/query/target_set)
`type` [Query](/axl/types/bazel/query/query)
`module` [fileset\_entry](/axl/types/bazel/query/fileset_entry)
`module` [attribute](/axl/types/bazel/query/attribute)
`module` [target](/axl/types/bazel/query/target)
`module` [allowed\_rule\_class\_info](/axl/types/bazel/query/allowed_rule_class_info)
`module` [attribute\_value](/axl/types/bazel/query/attribute_value)
`function` **RuleDefinition**
def RuleDefinition(
\*\*kwargs: typing.Any
) -> rule\_definition
`function` **FilesetEntry**
`function` **LabelListDictEntry**
def LabelListDictEntry(
\*\*kwargs: typing.Any
) -> label\_list\_dict\_entry
`function` **BuildLanguage**
def BuildLanguage(
\*\*kwargs: typing.Any
) -> build\_language
`function` **AttributeDefinition**
def AttributeDefinition(
\*\*kwargs: typing.Any
) -> attribute\_definition
`function` **GeneratedFile**
def GeneratedFile(
\*\*kwargs: typing.Any
) -> generated\_file
`function` **AllowedRuleClassInfo**
`function` **AttributeValue**
`function` **ConfiguredRuleInput**
def ConfiguredRuleInput(
\*\*kwargs: typing.Any
) -> configured\_rule\_input
`function` **PackageGroup**
def PackageGroup(
\*\*kwargs: typing.Any
) -> package\_group
`function` **RuleSummary**
def RuleSummary(
\*\*kwargs: typing.Any
) -> rule\_summary
`function` **EnvironmentGroup**
def EnvironmentGroup(
\*\*kwargs: typing.Any
) -> environment\_group
`function` **StringListDictEntry**
def StringListDictEntry(
\*\*kwargs: typing.Any
) -> string\_list\_dict\_entry
`function` **StringDictEntry**
def StringDictEntry(
\*\*kwargs: typing.Any
) -> string\_dict\_entry
`function` **LabelDictUnaryEntry**
def LabelDictUnaryEntry(
\*\*kwargs: typing.Any
) -> label\_dict\_unary\_entry
`function` **QueryResult**
def QueryResult(
\*\*kwargs: typing.Any
) -> query\_result
`function` **Target**
`function` **LabelKeyedStringDictEntry**
def LabelKeyedStringDictEntry(
\*\*kwargs: typing.Any
) -> label\_keyed\_string\_dict\_entry
`function` **SourceFile**
def SourceFile(
\*\*kwargs: typing.Any
) -> source\_file
`function` **Rule**
`function` **Attribute**
`function` **License**
def License(
\*\*kwargs: typing.Any
) -> license
# Allowed rule class info
Source: https://site.aspect.build/docs/axl/types/bazel/query/allowed_rule_class_info
# Attribute
Source: https://site.aspect.build/docs/axl/types/bazel/query/attribute
`function` **Selector**
def Selector(
\*\*kwargs: typing.Any
) -> selector
`function` **SelectorEntry**
def SelectorEntry(
\*\*kwargs: typing.Any
) -> selector\_entry
`function` **SelectorList**
def SelectorList(
\*\*kwargs: typing.Any
) -> selector\_list
# Attribute value
Source: https://site.aspect.build/docs/axl/types/bazel/query/attribute_value
`function` **DictEntry**
def DictEntry(
\*\*kwargs: typing.Any
) -> dict\_entry
# Fileset entry
Source: https://site.aspect.build/docs/axl/types/bazel/query/fileset_entry
# Query
Source: https://site.aspect.build/docs/axl/types/bazel/query/query
`function` **Query.eval**
def Query.eval(
\*,
flags: list\[str | (str, str)] = \[],
announce\_version: bool = False,
announce\_command: bool = False
) -> typing.Iterable\[generated\_file | package\_group | rule | source\_file]
The query system provides a programmatic interface for analyzing build dependencies and target relationships. Queries are constructed using a chain API and are lazily evaluated only when `.eval()` is explicitly called.
The entry point is `ctx.bazel.query()`, which returns a `query` for creating initial
query expressions. Most operations operate on `query` objects, which represent
sets of targets that can be filtered, transformed, and combined.
**Example**
```starlark theme={null}
**Query** dependencies of a target
deps = ctx.bazel.query().targets("//myapp:main").deps()
all_deps: target_set = deps.eval()
**Chain** multiple operations
sources = ctx.bazel.query().targets("//myapp:main")
.deps()
.kind("source file")
.eval()
```
Fails with Bazel's own stderr if the query exits non-zero (bad
expression, BUILD evaluation error, …), rather than returning an
empty target set — a failed query is not the same as one that
matched nothing.
**Parameters**
* `flags`: - Command flags to pass to `bazel query` (between the expression and `--output`). Callers that run `query` alongside `build` / `test` under `--ignore_all_rc_files` MUST forward the rc-expanded command flags here, so the query resolves external repositories the same way the build does (e.g. an rc-set `--noenable_bzlmod` / `--enable_workspace`). Omitting them lets the query diverge from the build and fail on repos the build can see. Accepts the same `str | (str, version-constraint)` shape as `ctx.bazel.build` / `.test`; version-gated flags are filtered against the running Bazel version identically.
* `announce_version`: - Print an `INFO: Bazel ` line before spawning. Resolved from the `--announce-bazel-version` task flag.
* `announce_command`: - Print an `INFO: Spawning: ` line before spawning. Resolved from the `--announce-bazel-command` task flag. Both mirror the `ctx.bazel.build` / `.test` disclosure.
`function` **Query.raw**
Replaces the query `expression` with a raw query expression string.
This escape hatch allows direct use of the underlying query language for complex cases,
while still supporting further chaining.
```starlark theme={null}
**Complex** intersection query
complex = ctx.bazel.query().raw("deps(//foo) intersect kind('test', //bar:*)")
**Path**-based query
path_query = ctx.bazel.query().raw("somepath(//start, //end)")
**Chaining** after raw
filtered = complex.kind("source file")
```
# Target
Source: https://site.aspect.build/docs/axl/types/bazel/query/target
# Target set
Source: https://site.aspect.build/docs/axl/types/bazel/query/target_set
# Sandbox recovery result
Source: https://site.aspect.build/docs/axl/types/bazel/sandbox_recovery_result
`property` **SandboxRecoveryResult.outcome**
SandboxRecoveryResult.outcome: str
Recovery outcome: `"clean"`, `"repaired"`, or `"still_poisoned"`.
* `clean`: nothing to do. Either the sandbox dir didn't exist or
contained only whitelisted entries.
* `repaired`: non-whitelisted entries were found and successfully
removed. The runner is usable for the next job.
* `still_poisoned`: non-whitelisted entries were found and at least
one could not be removed. Caller should signal the runner
unhealthy.
`property` **SandboxRecoveryResult.remaining**
SandboxRecoveryResult.remaining: list\[str]
Sorted names of sandbox-base entries that survived the removal attempt. Only non-empty when `outcome == "still_poisoned"`.
`property` **SandboxRecoveryResult.removed**
SandboxRecoveryResult.removed: list\[str]
Sorted names of sandbox-base entries that were successfully removed. Empty when `outcome == "clean"`.
# Bool
Source: https://site.aspect.build/docs/axl/types/bool
# Bytes
Source: https://site.aspect.build/docs/axl/types/bytes
`function` **bytes.elems**
Returns an iterable over the individual bytes as 1-byte bytes objects.
```
list(b"abc".elems()) == [b"a", b"b", b"c"]
```
# Config context
Source: https://site.aspect.build/docs/axl/types/config_context
`function` **ConfigContext.http**
def ConfigContext.http() -> Http
The `http` attribute provides a programmatic interface for making HTTP requests. It is used to fetch data from remote servers and can be used in conjunction with other aspects to perform complex data processing tasks.
**Example**
```starlark theme={null}
**Fetch** data from a remote server
data = ctx.http().get("https://example.com/data.json").block()
```
`property` **ConfigContext.features**
Access to the feature map for configuring feature instances.
Usage:
```starlark theme={null}
ctx.features[ArtifactUpload].enabled = False
```
`property` **ConfigContext.std**
Standard library is the foundation of powerful AXL tasks.
`property` **ConfigContext.tasks**
`property` **ConfigContext.telemetry**
Telemetry handle. Use `ctx.telemetry.exporters.add(url=..., ...)` to register OTLP exporters before any task runs.
`property` **ConfigContext.template**
Expand template files.
`property` **ConfigContext.traits**
Access to the trait map for configuring trait instances.
Usage:
```starlark theme={null}
ctx.traits[BazelTrait].extra_flags = ["--config=ci"]
```
`property` **ConfigContext.wasm**
EXPERIMENTAL! Run wasm programs within tasks.
# Dict
Source: https://site.aspect.build/docs/axl/types/dict
`function` **dict.clear**
[dict.clear](https://github.com/bazelbuild/starlark/blob/master/spec.md#dict·clear): clear a dictionary
`D.clear()` removes all the entries of dictionary D and returns `None`.
It fails if the dictionary is frozen or if there are active iterators.
```
x = {"one": 1, "two": 2}
x.clear()
x == {}
```
`function` **dict.get**
[dict.get](https://github.com/bazelbuild/starlark/blob/master/spec.md#dict·get): return an element from the dictionary.
`D.get(key[, default])` returns the dictionary value corresponding to
the given key. If the dictionary contains no such value, `get`
returns `None`, or the value of the optional `default` parameter if
present.
`get` fails if `key` is unhashable.
```
x = {"one": 1, "two": 2}
x.get("one") == 1
x.get("three") == None
x.get("three", 0) == 0
```
`function` **dict.items**
[dict.items](https://github.com/bazelbuild/starlark/blob/master/spec.md#dict·items): get list of (key, value) pairs.
`D.items()` returns a new list of key/value pairs, one per element in
dictionary D, in the same order as they would be returned by a `for`
loop.
```
x = {"one": 1, "two": 2}
x.items() == [("one", 1), ("two", 2)]
```
`function` **dict.keys**
[dict.keys](https://github.com/bazelbuild/starlark/blob/master/spec.md#dict·keys): get the list of keys of the dictionary.
`D.keys()` returns a new list containing the keys of dictionary D, in
the same order as they would be returned by a `for` loop.
```
x = {"one": 1, "two": 2}
x.keys() == ["one", "two"]
```
`function` **dict.pop**
[dict.pop](https://github.com/bazelbuild/starlark/blob/master/spec.md#dict·pop): return an element and remove it from a dictionary.
`D.pop(key[, default])` returns the value corresponding to the specified
key, and removes it from the dictionary. If the dictionary contains no
such value, and the optional `default` parameter is present, `pop`
returns that value; otherwise, it fails.
`pop` fails if `key` is unhashable, or the dictionary is frozen or has
active iterators.
```
x = {"one": 1, "two": 2}
x.pop("one") == 1
x == {"two": 2}
x.pop("three", 0) == 0
x.pop("three", None) == None
```
Failure:
```
{'one': 1}.pop('four') # error: not found
```
`function` **dict.popitem**
[dict.popitem](https://github.com/bazelbuild/starlark/blob/master/spec.md#dict·popitem): returns and removes the first key/value pair of a dictionary.
`D.popitem()` returns the first key/value pair, removing it from the
dictionary.
`popitem` fails if the dictionary is empty, frozen, or has active
iterators.
```
x = {"one": 1, "two": 2}
x.popitem() == ("one", 1)
x.popitem() == ("two", 2)
x == {}
```
Failure:
```
{}.popitem() # error: empty dict
```
`function` **dict.setdefault**
[dict.setdefault](https://github.com/bazelbuild/starlark/blob/master/spec.md#dict·setdefault): get a value from a dictionary, setting it to a new value if not present.
`D.setdefault(key[, default])` returns the dictionary value
corresponding to the given key. If the dictionary contains no such
value, `setdefault`, like `get`, returns `None` or the value of the
optional `default` parameter if present; `setdefault` additionally
inserts the new key/value entry into the dictionary.
`setdefault` fails if the key is unhashable or if the dictionary is
frozen.
```
x = {"one": 1, "two": 2}
x.setdefault("one") == 1
x.setdefault("three", 0) == 0
x == {"one": 1, "two": 2, "three": 0}
x.setdefault("four") == None
x == {"one": 1, "two": 2, "three": 0, "four": None}
```
`function` **dict.update**
[dict.update](https://github.com/bazelbuild/starlark/blob/master/spec.md#dict·update): update values in the dictionary.
`D.update([pairs][, name=value[, ...])` makes a sequence of key/value
insertions into dictionary D, then returns `None.`
If the positional argument `pairs` is present, it must be
another `dict`, or some other iterable.
If it is another `dict`, then its key/value pairs are inserted into D.
If it is an iterable, it must provide a sequence of pairs (or other
iterables of length 2), each of which is treated as a key/value pair
to be inserted into D.
For each `name=value` argument present, the name is converted to a
string and used as the key for an insertion into D, with its
corresponding value being `value`.
`update` fails if the dictionary is frozen.
```
x = {}
x.update([("a", 1), ("b", 2)], c=3)
x.update({"d": 4})
x.update(e=5)
x == {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}
```
`function` **dict.values**
def dict.values() -> list
[dict.values](https://github.com/bazelbuild/starlark/blob/master/spec.md#dict·values): get the list of values of the dictionary.
`D.values()` returns a new list containing the dictionary's values, in
the same order as they would be returned by a `for` loop over the
dictionary.
```
x = {"one": 1, "two": 2}
x.values() == [1, 2]
```
# Exporter spec
Source: https://site.aspect.build/docs/axl/types/exporter_spec
# Exporters
Source: https://site.aspect.build/docs/axl/types/exporters
`function` **Exporters.add**
Register an exporter. The runtime builds the corresponding sink after phase 3 and replays any buffered spans/logs into it.
Exactly one of `url` (OTLP exporter) or `file` (local file/stderr/stdout
exporter) must be provided.
Args (OTLP shape):
url: OTLP collector URL
protocol: "grpc" (default) or "http/protobuf"
headers: optional dict\[str,str] for auth/tenant routing
Args (file shape):
file: filesystem path, or "stderr" / "stdout"
format: "compact" (default, one human-readable line per record) or
"jsonl" (one JSON object per line, machine-parseable)
Common args:
signals: optional list, subset of \["traces", "logs", "metrics"];
default is all three
resource\_attributes: optional dict\[str,str] merged into the exporter's
Resource
# Feature context
Source: https://site.aspect.build/docs/axl/types/feature_context
`function` **FeatureContext.http**
def FeatureContext.http() -> Http
HTTP client for making requests during feature initialization.
`property` **FeatureContext.args**
Resolved args for this feature: config-only values and CLI args merged. Access via `ctx.args.arg_name`.
`property` **FeatureContext.aspect**
Aspect platform APIs (auth, etc.).
`property` **FeatureContext.std**
Standard library — same as `ctx.std` in config and task functions.
`property` **FeatureContext.telemetry**
Telemetry handle. Use `ctx.telemetry.exporters.add(url=..., ...)` to register OTLP exporters that the runtime installs after phase 3. Buffered spans/logs from earlier phases are replayed to them.
`property` **FeatureContext.traits**
The full mutable trait map. Inject into traits via `ctx.traits[TraitType].hook.append(...)`.
# Float
Source: https://site.aspect.build/docs/axl/types/float
# Future
Source: https://site.aspect.build/docs/axl/types/future
`function` **Future.block**
`function` **Future.map\_err**
`function` **Future.map\_ok**
`function` **Future.map\_ok\_or\_else**
# Futures
Source: https://site.aspect.build/docs/axl/types/futures
`function` **iter**
def iter(
\*futures: Future
) -> FutureIterator
# Hash
Source: https://site.aspect.build/docs/axl/types/hash
`function` **Hash.digest**
`function` **Hash.hexdigest**
def Hash.hexdigest() -> str
`function` **Hash.update**
# Http
Source: https://site.aspect.build/docs/axl/types/http
`function` **Http.delete**
`function` **Http.download**
Downloads a file from a URL to a local path.
If both `integrity` and `sha256` are specified, `integrity` takes precedence.
The checksum is verified in a streaming fashion during download.
`function` **Http.get**
`function` **Http.patch**
`function` **Http.post**
`function` **Http.put**
# Http response
Source: https://site.aspect.build/docs/axl/types/http_response
`property` **HttpResponse.body**
`property` **HttpResponse.headers**
`property` **HttpResponse.status**
# Int
Source: https://site.aspect.build/docs/axl/types/int
# Json
Source: https://site.aspect.build/docs/axl/types/json
`function` **encode**
Encode a value to a JSON string. Mirrors the Starlark stdlib's `json.encode`, with an additional optional `indent` parameter: pass a non-negative integer to pretty-print the output with that many spaces of indentation per nesting level (newlines inserted between elements). Omitting `indent` produces the stdlib's compact single-line form.
`function` **decode**
Decode a JSON string. Raises on parse failure. Mirrors the Starlark stdlib's `json.decode`. Use `try_decode` instead at I/O boundaries where a malformed input would otherwise crash the caller.
`function` **try\_decode**
Decode a JSON string. Returns `default` (None by default) on parse failure instead of raising. Distinguishing "parse failed" from "valid `null` parse": both produce None unless the caller passes a sentinel `default`.
# Examples
```python theme={null}
json.try_decode('{"a": 1}') # {"a": 1}
json.try_decode("not json") # None
json.try_decode("not json", {}) # {}
json.try_decode("null") # None (valid parse)
json.try_decode("null", "MISS") # None (still valid; not the sentinel)
```
# List
Source: https://site.aspect.build/docs/axl/types/list
`function` **list.append**
[list.append](https://github.com/bazelbuild/starlark/blob/master/spec.md#list·append): append an element to a list.
`L.append(x)` appends `x` to the list L, and returns `None`.
`append` fails if the list is frozen or has active iterators.
```
x = []
x.append(1)
x.append(2)
x.append(3)
x == [1, 2, 3]
```
`function` **list.clear**
[list.clear](https://github.com/bazelbuild/starlark/blob/master/spec.md#list·clear): clear a list
`L.clear()` removes all the elements of the list L and returns `None`.
It fails if the list is frozen or if there are active iterators.
```
x = [1, 2, 3]
x.clear()
x == []
```
`function` **list.extend**
[list.extend](https://github.com/bazelbuild/starlark/blob/master/spec.md#list·extend): extend a list with another iterable's content.
`L.extend(x)` appends the elements of `x`, which must be iterable, to
the list L, and returns `None`.
`extend` fails if `x` is not iterable, or if the list L is frozen or has
active iterators.
```
x = []
x.extend([1, 2, 3])
x.extend(["foo"])
x == [1, 2, 3, "foo"]
```
`function` **list.index**
[list.index](https://github.com/bazelbuild/starlark/blob/master/spec.md#list·index): get the index of an element in the list.
`L.index(x[, start[, end]])` finds `x` within the list L and returns its
index.
The optional `start` and `end` parameters restrict the portion of
list L that is inspected. If provided and not `None`, they must be list
indices of type `int`. If an index is negative, `len(L)` is effectively
added to it, then if the index is outside the range `[0:len(L)]`, the
nearest value within that range is used; see [Indexing](#indexing).
`index` fails if `x` is not found in L, or if `start` or `end`
is not a valid index (`int` or `None`).
```
x = ["b", "a", "n", "a", "n", "a"]
x.index("a") == 1 # bAnana
x.index("a", 2) == 3 # banAna
x.index("a", -2) == 5 # bananA
```
`function` **list.insert**
[list.insert](https://github.com/bazelbuild/starlark/blob/master/spec.md#list·insert): insert an element in a list.
`L.insert(i, x)` inserts the value `x` in the list L at index `i`,
moving higher-numbered elements along by one. It returns `None`.
As usual, the index `i` must be an `int`. If its value is negative,
the length of the list is added, then its value is clamped to the
nearest value in the range `[0:len(L)]` to yield the effective index.
`insert` fails if the list is frozen or has active iterators.
```
x = ["b", "c", "e"]
x.insert(0, "a")
x.insert(-1, "d")
x == ["a", "b", "c", "d", "e"]
```
`function` **list.pop**
[list.pop](https://github.com/bazelbuild/starlark/blob/master/spec.md#list·pop): removes and returns the last element of a list.
`L.pop([index])` removes and returns the last element of the list L, or,
if the optional index is provided, at that index.
`pop` fails if the index is negative or not less than the length of
the list, of if the list is frozen or has active iterators.
```
x = [1, 2, 3]
x.pop() == 3
x.pop() == 2
x == [1]
```
`function` **list.remove**
[list.remove](https://github.com/bazelbuild/starlark/blob/master/spec.md#list·remove): remove a value from a list
`L.remove(x)` removes the first occurrence of the value `x` from the
list L, and returns `None`.
`remove` fails if the list does not contain `x`, is frozen, or has
active iterators.
```
x = [1, 2, 3, 2]
x.remove(2)
x == [1, 3, 2]
x.remove(2)
x == [1, 3]
```
A subsequent call to `x.remove(2)` would yield an error because the
element won't be found.
```
x = [1, 2, 3, 2]
x.remove(2)
x.remove(2)
x.remove(2) # error: not found
```
# Namespace
Source: https://site.aspect.build/docs/axl/types/namespace
# Range
Source: https://site.aspect.build/docs/axl/types/range
# Remote
Source: https://site.aspect.build/docs/axl/types/remote
`module` [execution](/axl/types/remote/execution)
`module` [logging](/axl/types/remote/logging)
# Execution
Source: https://site.aspect.build/docs/axl/types/remote/execution
`module` [command](/axl/types/remote/execution/command)
`module` [platform](/axl/types/remote/execution/platform)
`module` [execution\_stage](/axl/types/remote/execution/execution_stage)
`module` [batch\_update\_blobs\_request](/axl/types/remote/execution/batch_update_blobs_request)
`module` [batch\_update\_blobs\_response](/axl/types/remote/execution/batch_update_blobs_response)
`module` [batch\_read\_blobs\_response](/axl/types/remote/execution/batch_read_blobs_response)
`module` [digest\_function](/axl/types/remote/execution/digest_function)
`module` [priority\_capabilities](/axl/types/remote/execution/priority_capabilities)
`module` [symlink\_absolute\_path\_strategy](/axl/types/remote/execution/symlink_absolute_path_strategy)
`module` [compressor](/axl/types/remote/execution/compressor)
`module` [execution\_client](/axl/types/remote/execution/execution_client)
`module` [action\_cache\_client](/axl/types/remote/execution/action_cache_client)
`module` [content\_addressable\_storage\_client](/axl/types/remote/execution/content_addressable_storage_client)
`module` [capabilities\_client](/axl/types/remote/execution/capabilities_client)
`function` **ActionCache**
`function` **Execution**
`function` **ContentAddressableStorage**
`function` **Compressor**
`function` **CacheCapabilities**
def CacheCapabilities(
\*\*kwargs: typing.Any
) -> cache\_capabilities
`function` **ActionCacheUpdateCapabilities**
def ActionCacheUpdateCapabilities(
\*\*kwargs: typing.Any
) -> action\_cache\_update\_capabilities
`function` **RequestMetadata**
def RequestMetadata(
\*\*kwargs: typing.Any
) -> request\_metadata
`function` **OutputFile**
def OutputFile(
\*\*kwargs: typing.Any
) -> output\_file
`function` **ActionResult**
def ActionResult(
\*\*kwargs: typing.Any
) -> action\_result
`function` **ExecuteRequest**
def ExecuteRequest(
\*\*kwargs: typing.Any
) -> execute\_request
`function` **ExecutionPolicy**
def ExecutionPolicy(
\*\*kwargs: typing.Any
) -> execution\_policy
`function` **FileNode**
def FileNode(
\*\*kwargs: typing.Any
) -> file\_node
`function` **Directory**
def Directory(
\*\*kwargs: typing.Any
) -> directory
`function` **ExecutedActionMetadata**
def ExecutedActionMetadata(
\*\*kwargs: typing.Any
) -> executed\_action\_metadata
`function` **FindMissingBlobsRequest**
def FindMissingBlobsRequest(
\*\*kwargs: typing.Any
) -> find\_missing\_blobs\_request
`function` **OutputSymlink**
def OutputSymlink(
\*\*kwargs: typing.Any
) -> output\_symlink
`function` **FindMissingBlobsResponse**
def FindMissingBlobsResponse(
\*\*kwargs: typing.Any
) -> find\_missing\_blobs\_response
`function` **Command**
`function` **BatchUpdateBlobsResponse**
`function` **ExecuteOperationMetadata**
def ExecuteOperationMetadata(
\*\*kwargs: typing.Any
) -> execute\_operation\_metadata
`function` **Platform**
`function` **Tree**
`function` **GetActionResultRequest**
def GetActionResultRequest(
\*\*kwargs: typing.Any
) -> get\_action\_result\_request
`function` **BatchUpdateBlobsRequest**
`function` **GetTreeRequest**
def GetTreeRequest(
\*\*kwargs: typing.Any
) -> get\_tree\_request
`function` **NodeProperty**
def NodeProperty(
\*\*kwargs: typing.Any
) -> node\_property
`function` **LogFile**
def LogFile(
\*\*kwargs: typing.Any
) -> log\_file
`function` **SymlinkAbsolutePathStrategy**
`function` **ExecutionCapabilities**
def ExecutionCapabilities(
\*\*kwargs: typing.Any
) -> execution\_capabilities
`function` **ToolDetails**
def ToolDetails(
\*\*kwargs: typing.Any
) -> tool\_details
`function` **DigestFunction**
`function` **BatchReadBlobsResponse**
`function` **OutputDirectory**
def OutputDirectory(
\*\*kwargs: typing.Any
) -> output\_directory
`function` **PriorityCapabilities**
`function` **ResultsCachePolicy**
def ResultsCachePolicy(
\*\*kwargs: typing.Any
) -> results\_cache\_policy
`function` **NodeProperties**
def NodeProperties(
\*\*kwargs: typing.Any
) -> node\_properties
`function` **DirectoryNode**
def DirectoryNode(
\*\*kwargs: typing.Any
) -> directory\_node
`function` **WaitExecutionRequest**
def WaitExecutionRequest(
\*\*kwargs: typing.Any
) -> wait\_execution\_request
`function` **UpdateActionResultRequest**
def UpdateActionResultRequest(
\*\*kwargs: typing.Any
) -> update\_action\_result\_request
`function` **BatchReadBlobsRequest**
def BatchReadBlobsRequest(
\*\*kwargs: typing.Any
) -> batch\_read\_blobs\_request
`function` **GetTreeResponse**
def GetTreeResponse(
\*\*kwargs: typing.Any
) -> get\_tree\_response
`function` **SymlinkNode**
def SymlinkNode(
\*\*kwargs: typing.Any
) -> symlink\_node
`function` **Action**
def Action(
\*\*kwargs: typing.Any
) -> action
`function` **ExecuteResponse**
def ExecuteResponse(
\*\*kwargs: typing.Any
) -> execute\_response
`function` **ServerCapabilities**
def ServerCapabilities(
\*\*kwargs: typing.Any
) -> server\_capabilities
`function` **ExecutionStage**
`function` **Digest**
def Digest(
\*\*kwargs: typing.Any
) -> digest
`function` **GetCapabilitiesRequest**
def GetCapabilitiesRequest(
\*\*kwargs: typing.Any
) -> get\_capabilities\_request
# Action cache client
Source: https://site.aspect.build/docs/axl/types/remote/execution/action_cache_client
# Batch read blobs response
Source: https://site.aspect.build/docs/axl/types/remote/execution/batch_read_blobs_response
`function` **Response**
def Response(
\*\*kwargs: typing.Any
) -> response
# Batch update blobs request
Source: https://site.aspect.build/docs/axl/types/remote/execution/batch_update_blobs_request
`function` **Request**
def Request(
\*\*kwargs: typing.Any
) -> request
# Batch update blobs response
Source: https://site.aspect.build/docs/axl/types/remote/execution/batch_update_blobs_response
`function` **Response**
def Response(
\*\*kwargs: typing.Any
) -> response
# Capabilities client
Source: https://site.aspect.build/docs/axl/types/remote/execution/capabilities_client
# Command
Source: https://site.aspect.build/docs/axl/types/remote/execution/command
`function` **EnvironmentVariable**
def EnvironmentVariable(
\*\*kwargs: typing.Any
) -> environment\_variable
# Compressor
Source: https://site.aspect.build/docs/axl/types/remote/execution/compressor
# Content addressable storage client
Source: https://site.aspect.build/docs/axl/types/remote/execution/content_addressable_storage_client
# Digest function
Source: https://site.aspect.build/docs/axl/types/remote/execution/digest_function
# Execution client
Source: https://site.aspect.build/docs/axl/types/remote/execution/execution_client
# Execution stage
Source: https://site.aspect.build/docs/axl/types/remote/execution/execution_stage
# Platform
Source: https://site.aspect.build/docs/axl/types/remote/execution/platform
`function` **Property**
def Property(
\*\*kwargs: typing.Any
) -> property
# Priority capabilities
Source: https://site.aspect.build/docs/axl/types/remote/execution/priority_capabilities
`function` **PriorityRange**
def PriorityRange(
\*\*kwargs: typing.Any
) -> priority\_range
# Symlink absolute path strategy
Source: https://site.aspect.build/docs/axl/types/remote/execution/symlink_absolute_path_strategy
# Logging
Source: https://site.aspect.build/docs/axl/types/remote/logging
`module` [rpc\_call\_details](/axl/types/remote/logging/rpc_call_details)
`function` **LogEntry**
def LogEntry(
\*\*kwargs: typing.Any
) -> log\_entry
`function` **GetActionResultDetails**
def GetActionResultDetails(
\*\*kwargs: typing.Any
) -> get\_action\_result\_details
`function` **UpdateActionResultDetails**
def UpdateActionResultDetails(
\*\*kwargs: typing.Any
) -> update\_action\_result\_details
`function` **ReadDetails**
def ReadDetails(
\*\*kwargs: typing.Any
) -> read\_details
`function` **QueryWriteStatusDetails**
def QueryWriteStatusDetails(
\*\*kwargs: typing.Any
) -> query\_write\_status\_details
`function` **GetCapabilitiesDetails**
def GetCapabilitiesDetails(
\*\*kwargs: typing.Any
) -> get\_capabilities\_details
`function` **WaitExecutionDetails**
def WaitExecutionDetails(
\*\*kwargs: typing.Any
) -> wait\_execution\_details
`function` **WriteDetails**
def WriteDetails(
\*\*kwargs: typing.Any
) -> write\_details
`function` **RpcCallDetails**
`function` **ExecuteDetails**
def ExecuteDetails(
\*\*kwargs: typing.Any
) -> execute\_details
`function` **FindMissingBlobsDetails**
def FindMissingBlobsDetails(
\*\*kwargs: typing.Any
) -> find\_missing\_blobs\_details
# Rpc call details
Source: https://site.aspect.build/docs/axl/types/remote/logging/rpc_call_details
# Set
Source: https://site.aspect.build/docs/axl/types/set
`function` **set.add**
Add an item to the set. `# starlark::assert::is_true(r#" x = set([1, 2, 3]) x.add(4) x == set([1, 2, 3, 4]) # "#);`
`function` **set.clear**
`function` **set.difference**
Returns a new set with elements unique the set when compared to the specified iterable. `# starlark::assert::is_true(r#" x = set([1, 2, 3]) y = [3, 4, 5] x.difference(y) == set([1, 2]) # "#);`
`function` **set.discard**
Remove the item from the set. It does nothing if there is no such item.
`discard` fails if the key is unhashable or if the dictionary is
frozen.
Time complexity of this operation is *O(N)* where *N* is the number of entries in the set.
```
x = set([1, 2, 3])
x.discard(2)
x == set([1, 3])
```
A subsequent call to `x.discard(2)` would do nothing.
```
x = set([1, 2, 3])
x.discard(2)
x.discard(2)
x == set([1, 3])
```
`function` **set.intersection**
Return a new set with elements common to the set and all others. Unlike Python does not support variable number of arguments. `# starlark::assert::is_true(r#" x = set([1, 2, 3]) y = [3, 4, 5] x.intersection(y) == set([3]) # "#);`
`function` **set.issubset**
Test whether every element in the set is in other iterable. `# starlark::assert::is_true(r#" x = set([1, 2, 3]) y = [3, 1, 2] x.issubset(y) # "#);`
`function` **set.issuperset**
Test whether every element other iterable is in the set. `# starlark::assert::is_true(r#" x = set([1, 2, 3]) y = [1, 3] x.issuperset(y) == True # "#);`
`function` **set.pop**
Removes and returns the **last** element of a set.
`S.pop()` removes and returns the last element of the set S.
`pop` fails if the set is empty, or if the set is frozen or has active iterators.
Time complexity of this operation is *O(1)*.
```
x = set([1, 2, 3])
x.pop() == 3
x.pop() == 2
x == set([1])
```
`function` **set.remove**
Remove the item from the set. It raises an error if there is no such item.
`remove` fails if the key is unhashable or if the dictionary is
frozen.
Time complexity of this operation is *O(N)* where *N* is the number of entries in the set.
```
x = set([1, 2, 3])
x.remove(2)
x == set([1, 3])
```
A subsequent call to `x.remove(2)` would yield an error because the
element won't be found.
```
x = set([1, 2, 3])
x.remove(2)
x.remove(2) # error: not found
```
`function` **set.symmetric\_difference**
Returns a new set with elements in either the set or the specified iterable but not both. `# starlark::assert::is_true(r#" x = set([1, 2, 3]) y = [3, 4, 5] x.symmetric_difference(y) == set([1, 2, 4, 5]) # "#);`
`function` **set.union**
Return a new set with elements from the set and all others. Unlike Python does not support variable number of arguments. `# starlark::assert::is_true(r#" x = set([1, 2, 3]) y = [3, 4, 5] x.union(y) == set([1, 2, 3, 4, 5]) # "#);`
`function` **set.update**
Update the set by adding items from an iterable. `# starlark::assert::is_true(r#" x = set([1, 3, 2]) x.update([4, 3]) list(x) == [1, 3, 2, 4] # "#);`
# Std
Source: https://site.aspect.build/docs/axl/types/std
`type` [Net](/axl/types/std/net)
`type` [Std](/axl/types/std/std)
`type` [FileSystem](/axl/types/std/file_system)
`type` [Env](/axl/types/std/env)
`module` [process](/axl/types/std/process)
`module` [io](/axl/types/std/io)
# Env
Source: https://site.aspect.build/docs/axl/types/std/env
`function` **Env.arch**
Returns the CPU architecture.
Returns a string describing the CPU architecture, such as
"x86\_64", "aarch64", etc.
`function` **Env.aspect\_cli\_version**
def Env.aspect\_cli\_version() -> str
Returns the version of the Aspect CLI.
`function` **Env.aspect\_root\_dir**
def Env.aspect\_root\_dir() -> str
Returns the Aspect project root directory — the anchor for axl / config loading.
Found by walking upwards from the current working directory looking
for an `.aspect/version.axl` or `MODULE.aspect` marker. Falls back
to the deepest Bazel workspace marker (`MODULE.bazel`, `WORKSPACE`,
etc.) so a pure-Bazel monorepo still resolves to a sane project
anchor, and finally to the current working directory if no marker
exists anywhere.
Distinct from the Bazel workspace root — bazelrc discovery and
`bazel info workspace` use the deepest Bazel marker, which can
differ when a Bazel sub-workspace sits under an Aspect root.
`function` **Env.bazel\_root\_dir**
def Env.bazel\_root\_dir() -> str
Returns the Bazel workspace root directory — the anchor for bazelrc discovery, `bazel info workspace`, and BES output paths.
Found by walking upwards from the current working directory looking
for a `MODULE.bazel` / `WORKSPACE` marker. Falls back to the deepest
`.aspect/version.axl` or `MODULE.aspect` marker so a pure-Aspect
workspace still resolves, and finally to the current working
directory if no marker exists anywhere.
Distinct from the Aspect project root — axl / config loading uses
the deepest `.aspect/` marker, which can differ when a Bazel
sub-workspace sits under an Aspect root.
`function` **Env.current\_dir**
def Env.current\_dir() -> str
Returns the current working directory as a path.
**Platform**-specific behavior
This function currently corresponds to the `getcwd` function on Unix
and the `GetCurrentDirectoryW` function on Windows.
**Errors**
Fails if the current working directory value is invalid.
Possible cases:
* Current directory does not exist.
* There are insufficient permissions to access the current directory.
`function` **Env.current\_exe**
def Env.current\_exe() -> str
`function` **Env.git\_root\_dir**
def Env.git\_root\_dir() -> None | str
Returns the git repository root — the directory containing `.git` — or `None` when not inside a git repository.
`function` **Env.home\_dir**
Returns the path of the current user's home directory if known.
This may return `None` if getting the directory fails or if the platform does not have user home directories.
For storing user data and configuration it is often preferable to use more specific directories.
For example, [XDG Base Directories] on Unix or the `LOCALAPPDATA` and `APPDATA` environment variables on Windows.
[XDG Base Directories]: https://specifications.freedesktop.org/basedir-spec/latest/
**Unix**
* Returns the value of the 'HOME' environment variable if it is set
(including to an empty string).
* Otherwise, it tries to determine the home directory by invoking the `getpwuid_r` function
using the UID of the current user. An empty home directory field returned from the
`getpwuid_r` function is considered to be a valid value.
* Returns `None` if the current user has no entry in the /etc/passwd file.
**Windows**
* Returns the value of the 'USERPROFILE' environment variable if it is set, and is not an empty string.
* Otherwise, [`GetUserProfileDirectory`][msdn] is used to return the path. This may change in the future.
[msdn]: https://docs.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-getuserprofiledirectorya
In UWP (Universal Windows Platform) targets this function is unimplemented and always returns `None`.
`function` **Env.os**
Returns the operating system name.
Returns a string describing the operating system in use, such as
"linux", "macos", "windows", etc.
`function` **Env.remove\_var**
def Env.remove\_var(
key: str,
/
) -> None
Removes an environment variable from the current process.
Has no effect if the variable is not set. Subsequent `var()` calls will
return `None` for the removed variable.
`function` **Env.set\_var**
def Env.set\_var(
key: str,
value: str,
/
) -> None
Sets an environment variable for the current process.
This affects all subsequent `var()` calls and any child processes spawned
after this call. Use with care — environment variables are global process
state.
`function` **Env.temp\_dir**
def Env.temp\_dir() -> str
Returns the path of a temporary directory.
The temporary directory may be shared among users, or between processes
with different privileges; thus, the creation of any files or directories
in the temporary directory must use a secure method to create a uniquely
named file. Creating a file or directory with a fixed or predictable name
may result in "insecure temporary file" security vulnerabilities. Consider
using a crate that securely creates temporary files or directories.
Note that the returned value may be a symbolic link, not a directory.
**Platform**-specific behavior
On Unix, returns the value of the `TMPDIR` environment variable if it is
set, otherwise the value is OS-specific:
* On Darwin-based OSes (macOS, iOS, etc) it returns the directory provided
by `confstr(_CS_DARWIN_USER_TEMP_DIR, ...)`, as recommended by [Apple's
security guidelines][appledoc].
* On all other unix-based OSes, it returns `/tmp`.
On Windows, the behavior is equivalent to that of [`GetTempPath2`][GetTempPath2] /
[`GetTempPath`][GetTempPath], which this function uses internally.
[GetTempPath2]: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppath2a
[GetTempPath]: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppatha
[appledoc]: https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/RaceConditions.html#//apple_ref/doc/uid/TP40002585-SW10
`function` **Env.var**
Fetches the environment variable key from the current process.
`function` **Env.vars**
Returns an iterator of (variable, value) pairs of strings, for all the environment variables of the current process.
The returned iterator contains a snapshot of the process's environment
variables at the time of this invocation. Modifications to environment
variables afterwards will not be reflected in the returned iterator.
# File system
Source: https://site.aspect.build/docs/axl/types/std/file_system
`function` **FileSystem.copy**
def FileSystem.copy(
from: str,
to: str,
/
) -> int
Copies the contents of one file to another. This function will also copy the permission bits of the original file to the destination file.
This function will overwrite the contents of to.
Note that if from and to both point to the same file, then the file will likely get truncated by this operation.
On success, the total number of bytes copied is returned and it is equal to the length of the to file as reported by metadata.
This function will return an error in the following situations, but is not limited to just these cases:
* from is neither a regular file nor a symlink to a regular file.
* from does not exist.
* The current process does not have the permission rights to read from or write to.
* The parent directory of to doesn’t exist.
`function` **FileSystem.create**
Creates (or truncates) a file for writing and returns it as a writable stream.
Mirrors `std::fs::File::create` — the file is opened write-only and
truncated to zero length, or created if it does not exist.
`function` **FileSystem.create\_dir**
def FileSystem.create\_dir(
path: str,
/
) -> None
Creates a new, empty directory at the provided path.
This function will return an error in the following situations, but is not limited to just these cases:
* User lacks permissions to create directory at path.
* A parent of the given path doesn’t exist. (To create a directory and all its missing parents at the same time, use the createidirlall function.)
* path already exists.
NOTE: If a parent of the given path doesn’t exist, this function will return an error. To create a directory and all its missing parents at the same time, use the createidirlall function.
`function` **FileSystem.create\_dir\_all**
def FileSystem.create\_dir\_all(
path: str,
/
) -> None
Recursively create a directory and all of its parent components if they are missing.
This function is not atomic. If it returns an error, any parent components it was able to create will remain.
If the empty path is passed to this function, it always succeeds without creating any directories.
The function will return an error if any directory specified in path does not exist and could not be created. There may be other error conditions; see create\_dir for specifics.
`function` **FileSystem.exists**
def FileSystem.exists(
path: str,
/
) -> bool
Returns `true` if the path points at an existing entity.
This function will traverse symbolic links to query information about the
destination file. In case of broken symbolic links this will return `false`.
Note that while this avoids some pitfalls of the `exists()` method, it still can not
prevent time-of-check to time-of-use (TOCTOU) bugs. You should only use it in scenarios
where those bugs are not an issue.
`function` **FileSystem.hard\_link**
def FileSystem.hard\_link(
original: str,
link: str,
/
) -> None
Creates a new hard link on the filesystem.
The link path will be a link pointing to the original path. Note that systems often require these two paths to both be located on the same filesystem.
If original names a symbolic link, it is platform-specific whether the symbolic link is followed. On platforms where it’s possible to not follow it, it is not followed, and the created hard link points to the symbolic link itself.
This function will return an error in the following situations, but is not limited to just these cases:
* The original path is not a file or doesn’t exist.
* The ‘link’ path already exists.
`function` **FileSystem.is\_dir**
def FileSystem.is\_dir(
path: str,
/
) -> bool
Returns true if this path is for a directory.
This function will return an error in the following situations, but is not limited to just these cases:
* The user lacks permissions to perform metadata call on path.
* path does not exist.
`function` **FileSystem.is\_file**
def FileSystem.is\_file(
path: str,
/
) -> bool
Returns true if this path is for a regular file.
This function will return an error in the following situations, but is not limited to just these cases:
* The user lacks permissions to perform metadata call on path.
* path does not exist.
`function` **FileSystem.metadata**
Returns the metadata about the given file or directory.
This function will return an error in the following situations, but is not limited to just these cases:
* The user lacks permissions to perform metadata call on path.
* path does not exist.
The modified, accessed, created fields of the Metadata result might not be available on all platforms, and will
be set to None on platforms where they is not available.
The executable field reflects the Unix execute bit; it is always false on non-Unix platforms.
`function` **FileSystem.mkdtemp**
def FileSystem.mkdtemp(
\*,
prefix: str = "",
parent: str = ""
) -> str
Creates a new temporary directory with a unique name and returns its path.
The directory is created inside `parent` (defaults to the system temp dir).
The caller is responsible for removing it when done.
`function` **FileSystem.open**
Opens a file for reading and returns it as a readable stream.
The returned stream can be passed directly as the `data` argument to
`ctx.http().post()` or `ctx.http().put()` for streaming uploads, or
iterated over / read directly.
`function` **FileSystem.read\_dir**
Returns an iterator over the entries within a directory.
This function will return an error in the following situations, but is not limited to just these cases:
* The provided path doesn’t exist.
* The process lacks permissions to view the contents.
* The path points at a non-directory file.
`function` **FileSystem.read\_link**
def FileSystem.read\_link(
path: str,
/
) -> str
Reads a symbolic link, returning the file that the link points to.
This function will return an error in the following situations, but is not limited to just these cases:
* path is not a symbolic link.
* path does not exist.
`function` **FileSystem.read\_to\_string**
def FileSystem.read\_to\_string(
path: str,
/
) -> str
Reads the entire contents of a file into a string.
This function will return an error under a number of different circumstances. Some of these error conditions are:
* The specified file does not exist.
* The user lacks permission to get the specified access rights for the file.
* The user lacks permission to open one of the directory components of the specified path.
`function` **FileSystem.remove\_dir**
def FileSystem.remove\_dir(
path: str,
/
) -> None
Removes an empty directory.
If you want to remove a directory that is not empty, as well as all
of its contents recursively, consider using remove\_dir\_all instead.
This function will return an error in the following situations, but is not limited to just these cases:
* path doesn’t exist.
* path isn’t a directory.
* The user lacks permissions to remove the directory at the provided path.
* The directory isn’t empty.
`function` **FileSystem.remove\_dir\_all**
def FileSystem.remove\_dir\_all(
path: str,
/
) -> None
Removes a directory at this path, after removing all its contents. Use carefully!
This function does not follow symbolic links and it will simply remove the symbolic link itself.
See remove\_file and remove\_dir for possible errors.
`function` **FileSystem.remove\_file**
def FileSystem.remove\_file(
path: str,
/
) -> None
Removes a file from the filesystem.
Note that there is no guarantee that the file is immediately deleted
(e.g., depending on platform, other open file descriptors may prevent immediate removal).
This function will return an error in the following situations, but is not limited to just these cases:
* path points to a directory.
* The file doesn’t exist.
* The user lacks permissions to remove the file.
`function` **FileSystem.rename**
def FileSystem.rename(
from: str,
to: str,
/
) -> None
Renames a file or directory to a new name, replacing the original file if to already exists.
This will not work if the new name is on a different mount point.
This function will return an error in the following situations, but is not limited to just these cases:
* from does not exist.
* The user lacks permissions to view contents.
* from and to are on separate filesystems.
`function` **FileSystem.symlink\_metadata**
Queries the metadata about a file without following symlinks.
This function will return an error in the following situations, but is not limited to just these cases:
* The user lacks permissions to perform metadata call on path.
* path does not exist.
The modified, accessed, created fields of the Metadata result might not be available on all platforms, and will
be set to None on platforms where they is not available.
The executable field reflects the Unix execute bit; it is always false on non-Unix platforms.
`function` **FileSystem.try\_append**
def FileSystem.try\_append(
path: str,
content: str,
/
) -> bool
Appends a string to the end of a file, creating it if it does not exist.
Opens the file with `OpenOptions::append(true)` so concurrent writers
see their bytes interleaved at record boundaries rather than racing.
POSIX `O_APPEND` is atomic for writes ≤ `PIPE_BUF`, which covers the
short single-line records this is designed for (the
`runner_job_history` lines fit comfortably). The parent directory
must exist; this function will not create intermediate directories.
Returns `True` on success and `False` on any I/O error (parent
directory missing, permissions denied, target is a directory, etc.).
Errors are swallowed because the primary caller is the runner job
history hook, where a write failure must never fail the task.
`function` **FileSystem.try\_read\_to\_string**
def FileSystem.try\_read\_to\_string(
path: str,
/
) -> str
Reads the entire contents of a file into a string, or returns `""` on any I/O error (missing file, permission denied, non-UTF-8 content, transient read failure). Companion to `try_append`: the silent fall-through lets callers handle never-fail invariants without try/except — e.g. the runner job history dedup read, where a failed read must degrade to "assume empty file" rather than fail the task.
`function` **FileSystem.write**
def FileSystem.write(
path: str,
content: str,
/
) -> None
Writes a string as the entire contents of a file.
This function will create a file if it does not exist, and will entirely replace its contents if it does.
Depending on the platform, this function may fail if the full directory path does not exist.
This is a convenience function for using fs.create and \[write\_all] with fewer imports.
# Io
Source: https://site.aspect.build/docs/axl/types/std/io
`type` [Readable](/axl/types/std/io/readable)
`type` [Stdio](/axl/types/std/io/stdio)
`type` [Writable](/axl/types/std/io/writable)
# Readable
Source: https://site.aspect.build/docs/axl/types/std/io/readable
`function` **Readable.read**
def Readable.read(
size: int = ...,
/
) -> bytes
Reads bytes from this source.
If `size` is provided, reads up to that many bytes.
If `size` is not provided, reads until EOF.
Returns the bytes read.
`function` **Readable.read\_to\_string**
def Readable.read\_to\_string() -> str
Reads all bytes until EOF in this source and returns a string.
If successful, this function will return all bytes as a string.
`property` **Readable.is\_tty**
Returns true if the underlying stream is connected to a terminal/tty.
# Stdio
Source: https://site.aspect.build/docs/axl/types/std/io/stdio
`property` **Stdio.stderr**
Returns a writable stream for the standard error of the current process.
`property` **Stdio.stdin**
Returns a readable stream for the standard input of the current process.
`property` **Stdio.stdout**
Returns a writable stream for the standard output of the current process.
# Writable
Source: https://site.aspect.build/docs/axl/types/std/io/writable
`function` **Writable.close**
def Writable.close() -> None
Closes this output stream. This drops the underlying handle, and subsequent writes will fail with "stream is closed".
`function` **Writable.flush**
def Writable.flush() -> None
Flushes this output stream, ensuring that all intermediately buffered contents reach their destination.
`function` **Writable.write**
Writes a buffer into this writer, returning how many bytes were written.
`property` **Writable.is\_tty**
Returns true if the underlying stream is connected to a terminal/tty.
# Net
Source: https://site.aspect.build/docs/axl/types/std/net
`function` **Net.try\_unix\_request**
def Net.try\_unix\_request(
path: str,
/,
\*,
send: None | str = None
) -> str
One-shot Unix-domain-socket request/response.
Connects to `path`, writes `send` if provided, shuts down the write
side (so peers that wait for EOF before replying see end-of-stream
promptly), reads all bytes until EOF, and returns them as a string.
Returns `""` on any I/O error (socket missing, connect refused,
non-UTF-8 response, transient read failure) so callers can treat
"no response" and "error" uniformly without try/except scaffolding.
Use a non-empty return as the signal that the peer replied.
Unix only — returns `""` on other platforms.
# Process
Source: https://site.aspect.build/docs/axl/types/std/process
`type` [Output](/axl/types/std/process/output)
`type` [Process](/axl/types/std/process/process)
`type` [Child](/axl/types/std/process/child)
`type` [Command](/axl/types/std/process/command)
`type` [ExitStatus](/axl/types/std/process/exit_status)
# Child
Source: https://site.aspect.build/docs/axl/types/std/process/child
`function` **Child.kill**
Forces the child process to exit. If the child has already exited, its a no-op.
This is equivalent to sending a SIGKILL on Unix platforms.
`function` **Child.stderr**
The handle for reading from the child’s standard error (stderr), if it has been captured. Calling this function more than once will yield error.
`function` **Child.stdin**
The handle for writing to the child’s standard input (stdin), if it has been captured. Calling this function more than once will yield error.
`function` **Child.stdout**
The handle for reading from the child’s standard output (stdout), if it has been captured. Calling this function more than once will yield error.
`function` **Child.try\_wait**
Non-blocking check for child exit. Returns None if the child is still running, or ExitStatus if it has exited. Does not consume the child — stdout/stderr stream accessors remain callable after this returns a status, allowing pipe contents to be drained via child.stdout().readototstring() etc.
`function` **Child.wait**
Waits for the child to exit completely, returning the status that it exited with. This function will continue to have the same return value after it has been called at least once.
The stdin handle to the child process, if any, will be closed
before waiting. This helps avoid deadlock: it ensures that the
child does not block waiting for input from the parent, while
the parent waits for the child to exit.
`function` **Child.wait\_with\_output**
WARNING: Calling `wait_with_output` consumes the child instance, causing errors on subsequent calls to other methods.
Simultaneously waits for the child to exit and collect all remaining
output on the stdout/stderr handles, returning an `Output`
instance.
The stdin handle to the child process, if any, will be closed
before waiting. This helps avoid deadlock: it ensures that the
child does not block waiting for input from the parent, while
the parent waits for the child to exit.
By default, stdin, stdout and stderr are inherited from the parent.
In order to capture the output into this `Result