rules_dx
rules_dx is a Bazel developer platform with one dx CLI for build, test, lint, typecheck, format, generate, audit, and release tasks. Install Bazel via Bazelisk, then run dx through Bazel:
bazel run @rules_dx//:dx -- --help
Documentation
Start here to use dx. There is no dx install step: dx runs through Bazel.
Install Bazel via Bazelisk (see .bazelversion), then follow the command and CI
guides below.
Commands
- Command reference: behavior of each
dxcommand. - Supported surface: labels and
.bzlfiles covered by the compatibility promise. - Scope defaults: labels, files, dirs, and
--here. - Build, test, coverage: build, test, run, deploy, coverage.
- Quality: lint, typecheck, format.
- Check, fix, clean: fix-up loops and cleanup.
- Generate: refresh
BUILDfiles. - Docs: build, check, and preview this site.
- Environment and codegen: env and codegen.
- Audit and update: security, license, update, bump, add.
- Init: set up a workspace.
- Status and version: health report and the version pin.
- List tests: list test targets.
- Completion: shell completions.
- Migrate: move to the current layout.
- New, upgrade, rollback: create modules and change the pin.
- Offline bundle: build a vendored bundle and install from it.
- Watch: rerun on file changes.
- Worktree: worktree slots.
CI And Examples
- GitHub CI: run
dxin GitHub Actions. - Examples: starter callers and per-language adoption workspaces.
- Docs site: rendered reference.
Command Reference
Every dx command runs through Bazel. Put dx flags after the command,
Bazel flags after --. @rules_dx//:dx is the public launcher; inside this
checkout //:dx is the same target.
Locally, Bazel reads ~/.bazelrc and the system rc for every dx call. Under
CI=true and inside Bazel tests, dx passes --nohome_rc --nosystem_rc.
bazel run @rules_dx//:dx -- build //...
bazel run @rules_dx//:dx -- lint --check //... -- --jobs=4
dx build,dx test,dx coverage,dx run,dx deploydx lint,dx typecheck,dx formatdx check,dx fix,dx cleandx generatedx env,dx codegendx security,dx license,dx update,dx bump,dx adddx docsdx initdx status,dx versiondx list-testsdx completiondx migratedx new,dx upgrade,dx rollbackdx watchdx worktree
Scope rules live in Scope Defaults.
Airgapped hosts install from a vendored bundle: see Offline Bundle.
A .dx/version pin that disagrees with the rules_dx version dx was built
from stops most commands. See Version Skew.
Build Slots
dx build, test, coverage, deploy, lint, typecheck,
format, check, fix, generate, codegen, env, security, license,
docs and list-tests take a build slot before they call Bazel.
dx run holds a slot only while it builds and frees it before the app starts.
dx watch takes one slot per iteration and holds none while it waits for
changes. dx update takes one slot at its first updater that runs Bazel and
holds it until its updaters finish. dx update --check and updaters that do
not run Bazel take none. Slots are shared by every
worktree and every user session on the machine. The slot count is the machine
memory, or the cgroup memory limit if lower, divided by 3 GiB. There is always
at least one and never more than 16. The count has no setting.
When the Bazel config sets a non-empty --remote_executor, dx offers more
slots: the memory divided by 1.5 GiB, at most 16. dx reads this from the rc
files Bazel reads, their --config sections, and the flags you pass. A run
takes an extra slot only when no regular slot is free, the host is not under
memory pressure, and no other run took an extra slot in the last 10 seconds.
On Linux, memory pressure means /proc/pressure/memory some avg10 is 10 or
more, or MemAvailable is below 1.5 GiB when that file is missing. macOS and
Windows get no extra slots. A run that waits only because of memory pressure
says so in its waiting notice.
While a slot is held, dx adds two flags right after build, test,
coverage and run:
--local_resources=cpu=<n> --local_resources=memory=<mb>
n is the CPU count divided by the slots held right now, extra slots and
this one included, and never less than 2. mb is the memory, less the memory kept for nested
Bazel test servers, divided by the same number, in MiB. The values are fixed
when dx takes the slot. Nested dx calls reuse them through DX_BUILD_SHARE.
dx adds neither flag when an rc file Bazel reads sets that resource to a value
other than the vendored preset default. It also skips a resource you pass after
--:
dx build //... -- --local_resources=cpu=4
CI runs and dx runs inside a Bazel test add neither flag.
When all slots are taken, or other runs already wait, dx waits and prints one line to stderr:
dx: waiting for a build slot (held by /path/to/worktree and 5 more, 2 ahead in line)
The line names one current holder, how many other slots are held, and how many
runs wait ahead. Waiting runs get a slot in the order they started waiting. A
waiter that exits or is stopped leaves the line. A waiter suspended for over 30
seconds does not hold up the line until it resumes. If dx cannot use the
queue directory next to the slot files, it waits without a line.
With --output=json, dx also writes one notice event with code
waiting_for_build_slot right after command_started. The slot frees when the
Bazel work ends or dx exits, including when it crashes.
While dx waits, it keeps the worktree's running Bazel server from shutting down for idleness. It never starts a server.
If dx cannot write a slot file, it locks it read-only. If it still cannot use
a slot file, it prints dx: skipped build slot <path>: <error> once and runs
with one slot fewer. With --output=json it also writes one warning notice
event with code build_slot_skipped.
If dx can use no slot file, it prints dx: build slots disabled: <error> and
runs without a slot. With
--output=json it also writes one notice event with code
build_slots_disabled.
Ctrl-C or SIGTERM during the wait stops dx at once. dx prints
dx: cancelled: stopped by signal N and exits 128 plus the signal number.
status, version, worktree, completion and the other commands that do not
run Bazel never take a slot. A dx call started while DX_BUILD_SLOT is set, or
from inside a Bazel test, takes no second slot. While dx holds a slot, the
Bazel and tool processes it starts see DX_BUILD_SLOT=1 and DX_BUILD_SHARE
set to cpu=<n>,memory=<mb>. dx run apps start
after the slot frees.
Disk Space
Before each Bazel build it starts, dx checks free space on the workspace disk, the Bazel output base, the temp dir and the home dir. When one has under 1 GiB free, dx prints one line to stderr and keeps going:
dx: disk_low: 512.0 MiB free at /path; free disk space with `dx worktree prune --apply`
With --output=json, dx also writes one notice event with code disk_low
and level warning.
dx run and dx deploy check before the build, not before the app starts, and
an app's exit code is never reported as disk_full.
When the disk holding the worktree pool has under 20 GiB and under 10% free,
or under 1 GiB free,
dx drops orphaned output bases, repository cache downloads unused for 30 days,
and free slots' output bases each time it
takes a build slot: once for each command above, for each watch iteration,
for the build run does before the app starts, and for update when it runs
Bazel. This also happens when build slots are disabled. It never drops the
output base of the workspace being built. dx prints one of these lines to
stderr only when it removed something or could not reclaim:
dx: low disk: <free> free; removed <what>; <free> free now
dx: low disk, cannot reclaim output bases: <error>
With --output=json, dx also writes one notice event with code disk_low
and level warning, after command_started and before that build's output.
dx worktree add and dx worktree hook create also refuse with code
disk_full when the pool's disk has under 1 GiB free after that reclaim.
Inspecting The Graph
dx has no owners, deps, or why command. Use native Bazel. Quote every
expression.
bazel query 'rdeps(//..., //pkg:target, 1)'
bazel query 'deps(//pkg:target)'
bazel query 'somepath(//app:server, //lib:core)'
bazel cquery 'deps(//pkg:target)' --platforms=//platforms:linux_x86_64
bazel query 'kind(rule, rdeps(//..., @repo//pkg:target, 1))'
query reads the unconfigured graph. cquery reads the graph for the
selected platform and flags. An empty result is not a pass: check the exit
code.
Global Flags
Every command takes these:
--help(-h): print help.--version(-V): print the dx version.--workspace <dir>: run in another workspace. A relative dir starts from the current directory.--quiet[=bool](-q),--verbose[=bool](-v): less or more output. Bare meanstrue, and=falseturns off a default inherited from the environment or a config file. Short flags cluster (-qv); repeating-vchanges nothing.--color auto|always|never: color output. Defaultauto.--bazel-startup-option <token>: Bazel startup option, repeatable. Takes--output_base=<path>or--output_user_root=<path>. Inserted before the Bazel verb for every managed invocation. Isolated output bases isolate concurrent runs.
Every other flag belongs to one command family. dx <command> --help lists
only the flags that command takes, and a flag from another family is a usage
error.
Read commands take --output text|json (-o). -o json, -ojson and short
clusters such as -qojson all work. Commands that can print a patch also take diff. Commands that only
print text take no --output.
Check commands take --fail-on info|warning|error (default warning) and
--report <format>=<dest>. Repeat --report for more files. A relative
<dest> anchors at the workspace root wherever dx runs; an absolute <dest>
writes outside the workspace as given. Two flags resolving to the same file fail
before anything runs, including ./ and parent/../ spellings of one path.
Parent directories must already exist. A failed write names the resolved file and
the cause, exits nonzero, and keeps the previous file bytes. Each command page
lists its report formats. Coverage also takes --min-coverage <percent> and
fails below it.
Network commands take --offline and --frozen. --offline means no network:
use pinned cached snapshots or fail. --frozen means no manifest or lock
resolution changes and may fetch already pinned content. Combine them for both
policies. Older dx versions spelled offline as --frozen: pass --offline for
that behavior.
--check, --apply, --debug, --release, --here, and --affected are
documented on the command pages that take them. --here limits a command to the
current directory tree. --affected limits it to targets your changes reach. --apply never combines with --check. Bare --debug or
--release means dev, except for deploy, where it means release.
Exit codes: 0 success, 2 usage error, 1 failed check.
A usage error names the offending token, prints a tip: a similar ... line when
a near match exists, then prints the usage line above. With no command, the usage
error prints the command list and a see dx <command> --help hint.
JSON Output
--output=json writes one JSON object per line to stdout. Every object has an
event name and a schema with major and minor. The schema is 3.0.
Readers accept any 3.x and reject other majors. The stream starts with one
command_started and ends with one command_finished. Diagnostics and logs go to
stderr. A failure before the command starts emits error and then
command_finished with no command_started; that command_finished carries
the process exit code and results_complete: false. If dx cannot build an
event, it writes error with code invalid_result, then command_finished,
and exits 1. DX_OUTPUT=json and
output = "json" in dx.toml give the same stream, also when the rest of
dx.toml is invalid. A stop signal ends the run with no error and no
command_finished; dx exits 128 plus the signal number. A closed stdout
exits 141.
bazel run @rules_dx//:dx -- lint --check //... --output=json
Agent sessions
When CLAUDECODE, CODEX_THREAD_ID, CURSOR_AGENT, GEMINI_CLI, or
AI_AGENT is set, the default output is json for commands that support it,
and Bazel progress stays off stderr. --output and DX_OUTPUT still win, and
so does output in dx.toml. In CI these variables are ignored.
Empty optional fields are left out of test_outcome and build_failure events. A plain
pass is not an event. dx test and dx coverage count passed, failed, cached, no_cases, and skipped tests
in command_finished and write every test to the results report. Each count is
per result, so a target with four shards counts four. Equal
build_failure and diagnostic events from different targets and
configurations appear once, with a count.
command_started: the command and its mode.build_failure: one failure Bazel reported while building, fromdx build,dx test, anddx coverage. It hasrecord_version,target,configuration,target_kind,stage,category,action,tool,message, anddiagnostics, whose items may carry arelatedlist. Facts Bazel did not report readunknown. The same failure from several targets or configurations is one event with acountand up to 20 othertargets.capability: one declared capability, withname,source,availability,detail,flags,outputs,reports,scope_policy,effect, andpassthrough.operation: a phase, with the scopes it resolved to.selection: the resolved environment, assetup_id,environment_id, andcodegen_iddigests.status: one check, withname,status,detail, andhint.slot: one worktree slot, withaction,path,state, andserver, plus optionalslot,branch,holder,reason,base, anddisk_bytes.reasonsays why a slot or worktree was kept.summary: the pool totals fromdx worktree list, withslots,servers, andservers_rss_bytes, plus optionaldisk_bytesanddisk_cache_bytes.diagnostic: one finding, withseverity,tool,message,location,snapshot,fixable, andcount, plus optionalrule,path,range,package,resolution,baseline,origins, andrelated.locationisunknown,file,bytes, orlines. Arangewithlineslocation addsstart_line,start_column,end_line, andend_column, all 1-based.countis how many identical findings this one stands for.baselineissuppressedwhen the baseline holds the finding. Each oforiginsnames theproducer,target, andconfigurationthat reported it.packagenames the dependency of avulnorlicensefinding, withnameandversion, plusfixedfor a vulnerability andlicensefor a license finding. An emptyfixedlist means no fixed version exists.change: one file edit, withpath,kind,edit_count, andeffect.effectisproposedin check mode, andappliedorrefusedwith--apply. Arefusedchange carries areason.--include-editsaddssource_digestand the fulledits.phase_started: adx checkordx fixphase began, withcommand,phase,mode, andcorrelation. Every event of the phase carries the samecommand,phase, andcorrelation.phase_finished: a phase ended, with itsexit_codeand counts.notice: a note, withlevel,code,message, and optionalrelated_command,scope,path,language, andimport.report: a report written, withformat,path, andresults_complete.formatisjunit,lcov,failures, orresults.test_outcome: one test result except a plain pass, withtargetandoutcome, plus optionalconfiguration,status,cached,run,shard,attempt,final,duration_millis,cases_total,cases_failed,cases_error,cases_skipped,cases_omitted,artifacts, andevidence_complete.evidence_completeandartifactsappear only when evidence is missing.finalisfalseonly on an attempt that a later attempt replaced. A failing result addsfailing_cases(up to 10, each withclassname,name,outcome, and amessagecut to 1024 bytes),failing_cases_omitted,log_path, andlog_tail(the last 40 lines, at most 8 KiB of the test log).error: a failure, withcode,message, and optionalcause,status,excerpt,truncated,path,flag, andphase.causeis one ofinput,defaults,query_launch,query,analysis,bazel,test,application,tool_unavailable,collection,report,host,operational, orunknown.bazel_failedhas causebazel;tests_failedhas causetest;application_failedis adx runapplication that exited nonzero and has causeapplication.disk_fullhas causehost: a Bazel run or query exited 36 and a disk it writes to has under 1 GiB free. dx keeps exit 36 and prints anext:line suggestingdx worktree prune --apply.statusis the exit code of the failed Bazel run or application.excerptholds the last lines Bazel printed to stderr, andtruncatedistruewhen lines were cut.command_finished: theexit_code, plus anyresults_complete,run_id,diagnostics,tests, andchangescounts.testshaspassed,failed,cached,no_cases, andskipped. Each counts results (per target, configuration, run and shard), so a target with four shards counts four.failedcounts results whose final attempt has outcomefailedortimeout; earlier failed attempts of a retried test still appear astest_outcomeevents withfinal: falseand are not counted.cachedcounts the passed tests that came from the cache.no_casescounts passed tests whose report had no test cases; they are also inpassed.skippedcounts tests with outcomeskippedorfiltered.changeshascreateandmodify, and with--applyalsoappliedandrefused.
dx run and dx update add correlation to their events, so you can group
them by target or set.
Environment
Every variable below supplies a default. A flag on the command line wins, and
--quiet=false, and --verbose=false turn an inherited
default off.
DX_WORKSPACE=<dir>: default for--workspace.DX_QUIET=<bool>: default for--quiet.DX_VERBOSE=<bool>: default for--verbose.DX_COLOR=<mode>: default for--color.DX_OUTPUT=<mode>: default for--output.DX_FAIL_ON=<level>: default for--fail-on.NO_COLOR=<any>: disables color inautomode, even when empty.--color alwaysstill colors.BUILD_WORKSPACE_DIRECTORY=<dir>: workspace start underbazel run.BUILD_WORKING_DIRECTORY=<dir>: current directory underbazel run.
A <bool> is on for true and off for false, case-insensitive and trimmed.
Any other non-empty value is a usage error. An empty value is unset.
A DX_OUTPUT or DX_FAIL_ON value that a command does not support is ignored
for that command, as in the config file.
Config File
dx.toml (committed) and dx.local.toml (local-only, gitignored) set the
same defaults. A key
is the flag name without --.
[dx]
workspace = "/path/to/repo"
quiet = true
verbose = true
color = "never"
output = "json"
fail-on = "error"
Values are TOML, so a boolean key takes true or false. An empty value is
unset. Defaults live only under [dx] with the hyphenated flag names. A
top-level default or fail_on is a usage error.
An unknown key is a usage error that names the key and the keys it accepts.
Other supported sections in the same file stay valid.
Unsupported defaults are ignored per command. An output default that a
command does not support falls back to the command's default output: json in
an agent session for commands with JSON output, else text. For example, with
output = "diff" an agent session runs dx test with json. An unsupported
fail-on default falls back to warning. The same value as a flag is a usage
error. An invalid value is always a usage error.
Commit shared
defaults in dx.toml and keep personal overrides in dx.local.toml. Local
beats committed. No other file is read for defaults.
The nearest file to the working directory wins, and its values sit below the environment. The search
reads the working directory and each directory above it, so a neighboring or
nested workspace tree never supplies defaults. Under bazel run the search
starts at the workspace root. In CI the local file and DX_ variables are
ignored. Flags still win. CI never implies --apply.
The command line wins over the environment, and the environment wins over
the file. Defaults load from the selected workspace: --workspace or
DX_WORKSPACE selects it before the file is read. A workspace key in the
file redirects once to that workspace. A relative workspace key starts
from the workspace start. -h, --help, help, -V, and
--version print without reading a broken config. dx new and
dx completion run outside a workspace. Deleting .dx keeps committed
behavior. Read-only commands never rewrite config.
Supported Surface
@rules_dx//:dx runs every command.
Language rules:
@rules_dx//android/rules:defs.bzl: Android libraries.@rules_dx//apple/ios:app.bzl: iOS apps, tests and frameworks.@rules_dx//apple/ios:defs.bzl: iOS libraries.@rules_dx//cc/rules:defs.bzl: C and C++.@rules_dx//csharp/rules:defs.bzl: C#.@rules_dx//cuda/rules:defs.bzl: CUDA.@rules_dx//cue/rules:defs.bzl: CUE.@rules_dx//elixir/rules:defs.bzl: Elixir.@rules_dx//erlang/rules:defs.bzl: Erlang.@rules_dx//fsharp/rules:defs.bzl: F#.@rules_dx//gherkin/rules:defs.bzl: Gherkin.@rules_dx//gleam/rules:defs.bzl: Gleam.@rules_dx//go/rules:defs.bzl: Go.@rules_dx//html_template/rules:defs.bzl: HTML templates.@rules_dx//java/rules:defs.bzl: Java.@rules_dx//javascript/rules:defs.bzl: JavaScript.@rules_dx//javascript/rules:web.bzl: web apps and dev servers.@rules_dx//jsonnet/rules:defs.bzl: Jsonnet.@rules_dx//kotlin/rules:defs.bzl: Kotlin.@rules_dx//lua/rules:defs.bzl: Lua.@rules_dx//perl/rules:defs.bzl: Perl.@rules_dx//pkl/rules:defs.bzl: Pkl.@rules_dx//powershell/rules:defs.bzl: PowerShell.@rules_dx//python/rules:defs.bzl: Python.@rules_dx//qt/rules:defs.bzl: Qt.@rules_dx//qt/rules:rust.bzl: Rust libraries for Qt.@rules_dx//ruby/rules:defs.bzl: Ruby.@rules_dx//rust/rules:defs.bzl: Rust.@rules_dx//scala/rules:defs.bzl: Scala.@rules_dx//sql/rules:defs.bzl: SQL.@rules_dx//terraform/rules:defs.bzl: Terraform.@rules_dx//typescript/rules:defs.bzl: TypeScript.@rules_dx//vue/rules:defs.bzl: Vue.@rules_dx//xml/rules:defs.bzl: XML.@rules_dx//yaml/rules:defs.bzl: YAML.@rules_dx//zig/rules:defs.bzl: Zig.
Packaging and deploy:
@rules_dx//android/packaging:defs.bzl: APKs, App Bundles and adb.@rules_dx//appimage:defs.bzl: AppImage bundles.@rules_dx//apple/ios:device.bzl: signed iOS IPA export.@rules_dx//apple/provisioning:defs.bzl: Apple provisioning.@rules_dx//apple/provisioning:probe.bzl: Apple toolchain probe.@rules_dx//deploy/offline:offline.bzl: offline bundles.@rules_dx//deploy/rules:archive.bzl: release archives.@rules_dx//deploy/rules:cloudflare.bzl: Cloudflare deploys.@rules_dx//deploy/rules:crates.bzl: crates.io publish.@rules_dx//deploy/rules:github.bzl: GitHub releases.@rules_dx//deploy/rules:maven.bzl: Maven publish.@rules_dx//deploy/rules:npm.bzl: npm publish.@rules_dx//deploy/rules:nuget.bzl: NuGet publish.@rules_dx//deploy/rules:oci.bzl: OCI images.@rules_dx//deploy/rules:octopus.bzl: Octopus Deploy.@rules_dx//deploy/rules:promotion.bzl: release promotion.@rules_dx//deploy/rules:pypi.bzl: PyPI publish.@rules_dx//msix:defs.bzl: MSIX packages.
SDK and toolchain extensions:
@rules_dx//android/sdk:repos.bzl: Android SDK.@rules_dx//android/toolchain:repos.bzl: Android NDK toolchain.@rules_dx//apple/ios:simulator.bzl: iOS simulator.@rules_dx//elixir/rules:hex.bzl: Hex packages for Elixir.@rules_dx//gleam/rules:hex.bzl: Hex packages for Gleam.@rules_dx//powershell/rules:gallery.bzl: PowerShell Gallery modules.@rules_dx//msix:repos.bzl: MSIX tools.@rules_dx//qt/sdk:repos.bzl: Qt SDK.
API docs:
@rules_dx//tools/docs/typescript:defs.bzl: TypeScript API doc shards.
Quality config:
@rules_dx//quality:native_config.bzl: native linter and formatter configs.@rules_dx//quality:policy.bzl: quality families and workspace policy.
Every other file stays private. A private file is not a dependency. Patch releases keep every supported label and flag. Minor releases add without breaking old uses. A breaking change needs a new major version and a migration. Readers tolerate added fields and fail on a new major schema. Bazel 9.2.0 is the tested baseline.
rules_dx does not provide debugger acquisition, launch or attach integration, Debug Adapter Protocol adapters, or generated debugger configurations. These are unsupported and not planned. Debug build profiles and editor language-server settings do not imply debugger support. You can set up external debuggers yourself.
Build, Test, And Coverage Commands
bazel run @rules_dx//:dx -- build //...
bazel run @rules_dx//:dx -- test //...
bazel run @rules_dx//:dx -- coverage //...
No scope means //.... Pass a label, pattern, file, or dir to narrow it.
Use --here for the current dir tree, or --affected for the targets your
git changes reach (see Scope Defaults). File scopes
resolve to the owning targets. For dx test and dx coverage, a file runs
the tests that depend on its owners. A test tagged manual runs only when
it owns the file. Args after -- go to Bazel unchanged.
dx build, dx test, dx coverage, dx run and dx deploy also pass
--@rules_dx//config:validate=false, --@rules_dx//config:clippy_output_diagnostics=true
and --@rules_dx//config:rustc_output_diagnostics=true. Another value for one of them,
or for the matching @rules_rust//rust/settings flag, fails before Bazel starts with exit 2.
dx spells them with the name your MODULE.bazel gives rules_dx, for example
--@my_dx//config:validate=false when repo_name = "my_dx".
Ctrl-C, SIGTERM or SIGHUP stops a run. A SIGHUP that was ignored when
dx started, as under nohup, stays ignored. Ctrl-C, and a signal sent to dx itself,
even from outside a container, reach Bazel or the app once.
On macOS in a terminal, a SIGINT or SIGHUP sent to dx alone is not passed on,
and dx stops once Bazel or the app ends; send SIGTERM to stop it right away.
Outside a terminal, every process dx started gets it too, and any still running after
10 seconds is killed. A stopped run prints dx: cancelled: stopped by signal N, exits 128 plus the signal number, and emits no
command_finished event. The shared Bazel server keeps running. If Bazel
dies by a signal dx did not get, dx reports a bazel_signalled error and
exits 1.
Outside a terminal, if dx is killed with SIGKILL, Linux stops the process dx
started. Anything else in that process group keeps running until the next dx
run in that worktree stops it, prints dx: stopped leftover process group <id> from dx run <pid>, which is gone, and with --output=json emits a notice
event with code orphan_process_group_stopped, after command_started. On
macOS the next run stops the whole tree. In a terminal, a SIGKILL to dx alone
leaves its children running. Outside a terminal, when the process dx started
exits, dx stops anything still running in its process group. A dx that exits
on its own never leaves anything for the next run to stop.
dx build And dx test
dx build [--here] [--affected] [--debug|--release] [--report failures=<path>] [scope...] [-- bazel-options...]
dx test [--here] [--affected] [--debug|--release] [--report junit=<path>] [--report failures=<path>] [--report results=<path>] [scope...] [-- bazel-options...]
Builds or tests the scope. --debug uses the dx_debug profile,
--release uses dx_release. No flag uses dx_dev. The two flags conflict. --debug changes compilation
only. It does not start or attach a debugger. See
Supported Surface.
Exec tools build at fastbuild; --release builds them at opt.
Extra --config values pass through after the dx profile in their original
order. Only a conflicting dx profile is rejected.
dx adds its own report flags after your Bazel options, so a --config cannot
change them. dx test and dx coverage reject --remote_download_minimal,
--remote_download_toplevel and any --remote_download_outputs other than
all. dx coverage also rejects any --combined_report other than lcov.
dx writes its build event file with
--build_event_json_file_path_conversion=false, so it rejects
--build_event_json_file and any other --build_event_json_file_path_conversion
value. A rejected flag fails before Bazel starts with exit 2.
dx forces these flags and treats them as protected: --skip_incompatible_explicit_targets
(build, test and coverage), the three --@rules_dx//config: settings for validation and
rustc and clippy diagnostics, --remote_download_outputs=all (test and coverage),
--combined_report=lcov (coverage), and --build_event_json_file. Another value
for one of them, and Bazel startup options after --, exit 2.
Output: --output text|json. Reports: dx test writes
--report junit=<path> JUnit reports. Repeat the flag for more files. Use -
for stdout. A relative path anchors at the workspace root; an absolute one writes
as given. Colliding destinations fail before anything runs, parents must exist,
and a failed write keeps the previous file bytes. JUnit case names gain
[run=N] or [config=<id>] when a target has several runs or configurations,
and [shard=N,attempt=N] for later shards and attempts. A test.xml with
invalid UTF-8 still counts; dx replaces the bad bytes and prints a
dx: warning: on stderr. dx test --output=json adds one
test_outcome event per test result except a plain pass (passed or
no_cases with complete evidence), with its Bazel status,
run, shard, attempt, cached state, duration, and case counts. A failing result
also carries failing_cases (up to 10, each with classname, name,
outcome and message), log_path, and log_tail (the last 40 lines, at most 8 KiB of
test.log). artifacts and evidence_complete appear only when evidence is
missing. final is false on an attempt that a later attempt of the same
target, configuration, run and shard replaced, and appears only then. Plain
passes are not events; passed_after_retry is. When dx collected any test result,
command_finished has tests, counted once per result (per target,
configuration, run and shard, so a target with four shards counts four), with passed (every result that passed,
including passed_after_retry and no_cases, matching the passed entries in
the results report), failed (results whose final attempt has outcome
failed or timeout), cached (passed results that came from the
cache), and no_cases (passed results whose report had no test cases; part of
passed), and skipped (tests with outcome skipped or filtered). Earlier
attempts of a retried test still appear as test_outcome events with
final: false and are not counted.
A test that Bazel skipped has outcome skipped. A test that a tag or size
filter removed from the run has outcome filtered. Neither changes how the
run ends. When any test was filtered, dx emits one notice event with code
tests_filtered that gives the count and the first labels. In text mode it
prints dx: tests_filtered: ... on stderr.
--report results=<path> writes one JSON file with every test result, passed
or not. It follows the same path rules as the other reports. With
--output=json and no results report, dx writes it to
.dx/results.json in the workspace and emits a report
event with the path. A failed write of that default file fails the run, like a failed explicit
--report results=<path>: dx keeps Bazel's nonzero code or else exits nonzero,
results_complete is false, and a report_failed error is emitted. To write
the file somewhere else, pass --report results=<path>. Such a run removes the old .dx/results.json first, even
when it stops before Bazel starts. Text runs and runs with a results report
leave it alone.
The file has:
record_version:1,exit_code: Bazel's code.run_id: identifies the run that wrote the file;command_finishedcarries the samerun_id. Concurrent runs share.dx/results.json, so check thatrun_idmatchescommand_finished.run_id, or give each run its own path with--report results=<path>.complete:falsewhen results are incomplete.results: one entry per test result, with thetest_outcomefields, every case asclassname,name,outcomeandmessage, andlog_pathwhen the result did not pass. A replaced attempt carriesfinal: false.
dx build, dx test and dx coverage report each failure Bazel records
when it exits nonzero. --output=text prints one dx: build failure: line
per failure on stderr. A repeated failure lists its target and up to twenty others
in that line, then xN, the total across targets and configurations. Its rustc
diagnostics follow it. Each diagnostic lists its labels, notes and helps indented below it.
--output=json adds one build_failure event per failure, before
command_finished. The same failure from several targets or configurations is
one event with a count and up to 20 other targets. Each event has:
record_version:1. New fields are only ever added.target,configuration,target_kind(test,otherorunknown).stage:analysis,tool_acquisition,compilation,linking,test_execution,otherorunknown.countandtargets: only when the failure repeats.category:analysis_failed,loading_failed,tool_unavailable,tool_missing,compile_failed,link_failed,test_execution_failed,action_failed,target_not_built,skipped,cancelledoraborted.action:mnemonic,primary_outputand, when known,exit_code.tool:clippyforClippyactions andrustcfor other Rust actions.message: the first line of Bazel's reason, for failures with no action.diagnostics:status,totaland up to 50itemswithseverity,code,message,path,line,columnand an orderedrelatedlist. Eachrelateditem hasrole(label,noteorhelp),message, and when knownpath,line,column,replacementandapplicability. Labels read from rendered output have alinebut nocolumn. Related items are not counted intotal. Link errors have none.
Anything Bazel did not report reads unknown. An analysis failure has no
action, so its action fields are unknown. A test target that fails to build
emits a build_failure with target_kind test and no test case in the JUnit
report. It also gets a test_outcome with outcome failed_to_build, no case
counts, and status FAILED_TO_BUILD only when Bazel reports that summary.
The run's results are incomplete. Environment variables and command lines are
never reported.
Diagnostics come from rustc's JSON or rendered output in the failed action's
stderr. status is parsed when diagnostics were read, none when the stderr
held none, unavailable when the stderr is unreadable or named only on a remote
cache, and unknown
for non-Rust actions. A Rust action whose diagnostics include a linking with
error is reported as linking.
The failures report: --report failures=<path> on dx build, dx test and dx coverage writes
one JSON file with the failures of this run. It follows the same path rules
as the other reports and may share a run with junit or lcov. Use - for
stdout in text mode. The file is written on success too, with an empty
failures list. A failed write is a report_failed error. It fails a run Bazel
passed and never changes Bazel's own code.
The file has:
record_version:1,exit_code: Bazel's code.complete:falsewhen the build events end early or cannot be read, with the reason inproblem.truncated:truewhen any stderr was cut at a limit.failures_omitted: failures past the first 200.failures: thebuild_failurefields above, each with astderrobject.
stderr has status, bytes, truncated, redacted_lines and, for
captured output, text. status is:
structuredfor Rust actions whose diagnostics were parsed. Readdiagnosticsinstead.capturedortruncatedfor other actions. Each failure keeps at most 64 KiB and the file at most 1 MiB. Later failures readomitted_limit.remotewhen the build events still name the stderr only on a remote cache. dx asks Bazel to name local stderr files, so this is rare.absentwhen Bazel named no stderr,unreadablewhen the local file is gone.
Only this run's failed actions are read. No directory is scanned and no
history is kept. Lines that start a command line ((cd or exec env) are
dropped and counted in redacted_lines. Environment variables and command
lines are never reported. Positions come only from rustc diagnostics.
bazel run @rules_dx//:dx -- build --report failures=build-failures.json //...
bazel run @rules_dx//:dx -- test --report junit=junit.xml --report failures=failures.json
bazel run @rules_dx//:dx -- test --report results=results.json //...
In an agent session (CLAUDECODE, CODEX_THREAD_ID, CURSOR_AGENT,
GEMINI_CLI or AI_AGENT set), dx build, dx test, dx coverage and
dx run default to --output=json. They pass --noshow_progress,
--noshow_loading_progress, --noshow_timestamps, --curses=no,
--ui_event_filters=-info,-debug and --test_summary=none to Bazel before
your own Bazel options, so Bazel prints no progress on stderr. Those quiet
Bazel flags apply only when the output is JSON. For dx run,
args after -- still go only to the app. --output, DX_OUTPUT and the output setting in dx.toml still
win. In CI these variables are ignored.
If Bazel fails and its event file is missing or ends early, dx still returns
Bazel's code and reports a build_events_incomplete notice. dx test and
dx coverage write the reports they can, and the results report has
complete: false. If Bazel succeeds and dx test or dx coverage cannot
read the event file, dx exits 1 with unreadable_bep or invalid_bep.
Exit codes: 0 success. 2 usage, scope or owner errors. 1 operational
failures, including no_tests and query_launch_failed. Bazel failures keep Bazel's code. Results are incomplete when a
result has no test.xml, a test.xml is missing, truncated or invalid,
Bazel announces a result it never reports, a target has a summary but no
result, or the build events end before Bazel's last event. Another test's
evidence never makes up for it. Incomplete results fail the run, even when
Bazel passes. --output=text prints dx: incomplete_results: <reason> on
stderr, --output=json emits an incomplete_results error, the JUnit report
gets a dx.infrastructure error case, and results_complete reads false.
Failing tests with every result collected keep Bazel's code 3 and read
results_complete: true. Other Bazel failures read false.
With exit 3 and at least one failed result, --output=json emits a
tests_failed error with cause test and Bazel's status. Other Bazel failures
emit bazel_failed. dx coverage does the same.
bazel run @rules_dx//:dx -- build //cli/...
bazel run @rules_dx//:dx -- test --here
bazel run @rules_dx//:dx -- test --affected
bazel run @rules_dx//:dx -- test //... -- --jobs=4
bazel run @rules_dx//:dx -- test //cli/process/... -- --test_filter=workflow_argv
--affected builds or tests the changed targets and their reverse
dependencies. A Cargo.lock or crate_universe lockfile change selects the
users of the changed crates. Other lock files and patches named in
MODULE.bazel widen to //...; see scope defaults.
dx test --affected and dx coverage --affected run only the
affected test targets. With nothing affected they print no affected targets
and exit 0. Usage errors exit 2. Affected targets that do not fit the host
platform are skipped; see Scope Defaults.
Args after -- reach the test binary through Bazel. dx test and
dx coverage accept repeatable --test_arg tokens, in equals and
two-token form, with spaces and Unicode kept verbatim. --test_filter
stays a Bazel option. When the selected tests are rust_test targets,
optionally with their rustfmt_test and rust_clippy_test checks, dx also
passes the last --test_filter value to the test binaries as a libtest
filter, so only matching cases run. Other commands
reject --test_arg; run those cases with bazel directly.
Tests run once by default. Only tests marked flaky in their build
definition retry, up to three attempts. Diagnose one test with retries
through dx test <label> -- --flaky_test_attempts=3, or through
bazel test --config=retry <label>. A test that passes after retry is
reported as passed_after_retry with its attempt number.
dx run
dx run [--debug|--release] <label...> [-- args...]
Builds the runnable targets in scope, then launches them in scope
order. Explicit labels and patterns run sequentially. File and directory
scopes must resolve to exactly one runnable target. Args after -- go to
the app. The default profile is development; --release selects release.
dx run works with CI=true. dx run --apply is a usage error.
The build holds a build slot. The slot frees before
the app starts.
Output: --output text|json. dx run has no report format. With
--output=json, an app that exits nonzero emits an error with code
application_failed, cause application, the app's exit code as status,
and correlation run:<label>.
A failed build emits an error with code bazel_failed and starts no app.
Exit codes: 0 success, 2 usage or scope errors, 1 no runnable target or
a launch failure, or a file or directory scope that matches more than one runnable
target (ambiguous). A failed build keeps Bazel's exit code. The app's own code
is the exit code, and the first app that exits nonzero stops the later targets.
bazel run @rules_dx//:dx -- run @rules_dx//:dx -- --help
bazel run @rules_dx//:dx -- run --release @rules_dx//:dx -- --help
dx deploy
dx deploy [--apply] [--debug|--release] <label> [-- args...]
Builds one deployable target without publishing it. Pass --apply to run
the deployment. Takes exactly one label. The profile is the flag if given, else the target's
profile attribute, else the command default. With --apply, dx sets
DX_PROFILE for the deploy tool.
Args after -- go to the app, and only reach it on an --apply launch.
Output: text only.
Exit codes: 0 success, 2 usage or scope errors, 1 a build or launch
failure. The target's own code is the exit code. A passing check names
the --apply command that publishes.
dx coverage
dx coverage [--here] [--affected] [--min-coverage <percent>] [--report lcov=<path>] [--report failures=<path>] [--report results=<path>] [scope...] [-- bazel-options...]
Collects LCOV coverage over the scope. --min-coverage takes a whole number
from 0 to 100 and fails below that percent. The gate compares one line rate
(covered over executable lines, across all files) against it. Without the flag,
coverage collects without enforcing. A passing gate prints
coverage <percent>% (<covered>/<eligible> lines) meets minimum <n>%. A failing one
prints coverage_below_minimum: coverage <percent>% (<covered>/<eligible> lines) below minimum <n>%
on stderr, or emits an error event with code coverage_below_minimum with
--output=json. A collected LCOV with zero executable lines also fails.
C and C++ tests report line coverage through LLVM, with no setup needed.
Output: --output text|json. Reports: --report lcov=<path> writes
combined LCOV. Repeat the flag for more files. Use - for stdout. A relative
path anchors at the workspace root; an absolute one writes as given. Colliding
destinations fail before anything runs, parents must exist, and a failed write
keeps the previous file bytes.
dx coverage --output=json adds one test_outcome event per test result except
a plain pass, as for dx test. A failing result carries failing_cases,
log_path and log_tail as in dx test; a missing or invalid test.xml
there does not make coverage incomplete. command_finished counts tests as for
dx test.
--report failures=<path> and --report results=<path> work here too, as
described above.
Coverage sources
dx coverage also lists the production sources each cell owns. The source
list comes from the instrumented files of the targets in scope, so
--instrumentation_filter decides which files count. Each source gets one
disposition:
eligible: a production source in a covered language that the LCOV reports with executable lines.support: a test ortestonlysource, a TypeScript declaration file (.d.ts,.d.mts,.d.cts), a source the LCOV reports with no executable lines, or a C or C++ header the LCOV does not report.excluded: a source the LCOV does not report, with one validLCOV_EXCL_START/LCOV_EXCL_STOPrange over every line that is not blank or a comment.unsupported: a source in a language the gate does not read. These are counted, never dropped.
In text mode dx prints one counts line on stderr. With --output=json the
same text is the message of a notice event with code coverage_sources and
level info:
dx: coverage sources <cell>: <raw> raw, <excluded> excluded, <eligible> eligible lines; <n> eligible, <n> support, <n> excluded, <n> unsupported files
<cell> is <os>_<cpu>, such as linux_x86_64. A production source missing
from the LCOV, or an LCOV path no target owns, prints
dx: coverage_sources_incomplete: <message> on stderr, or emits a notice
event with code coverage_sources_incomplete and level warning with
--output=json. These notices do not change the exit code.
--build_tests_only is refused, because it hides library targets from the
source list. dx coverage also passes --build_tests_only=false after your
options, so a .bazelrc entry or --config that turns it on has no effect.
Exit codes: 0 success. 2 usage, scope or owner errors. 1 operational
failures, coverage below minimum, or incomplete coverage. Bazel failures
keep Bazel's code. Coverage is incomplete when Bazel reports a
coverage.dat artifact that is missing or invalid, when no coverage
artifacts are reported, when a target's coverage source list is missing or
unreadable, or when the build events are incomplete as for dx test. Incomplete coverage fails the run, blocks
--min-coverage, and marks a written LCOV report incomplete. A retried
test counts only its final attempt. Tests without coverage outputs are
not expected to produce coverage. Empty but valid coverage.dat files
are accepted and contribute no lines.
bazel run @rules_dx//:dx -- coverage //...
bazel run @rules_dx//:dx -- coverage --min-coverage 100 //...
Coverage ignores use LCOV_EXCL_LINE or LCOV_EXCL_START / LCOV_EXCL_STOP
with a short reason: and issue:. Bare ignores without a reason fail the
gate. reason: goes on the marker line or the line before and holds at most 120
characters. issue: must contain a digit.
The comment syntax depends on the language: // for Rust, Go, JS/TS, the JVM
languages (including Kotlin scripts, .kts), .NET and C/C++; # for Python
and Ruby; % for Erlang (.erl and .hrl). Markers in other sources
(.bzl, .pyi, .cu/.cuh, .d.ts) are not read.
Line And Branch Metrics
This section describes //tools/coverage:coverage_bin, not dx coverage.
The coverage gate enforces exact executable-line coverage. Branch outcomes
are measured separately and never change the gate. Each file with branch
records reports raw, ignored, eligible and taken counts with its language.
Branches on lines covered by LCOV_EXCL_LINE or a START/STOP range are
counted as ignored and are left out of eligible, taken, missed and the rate.
The declared branch summary is still checked against all records. Files without
branch records report unavailable instead of a rate. A declared branch
summary that does not match its records fails the report as incomplete.
Quality Commands
bazel run @rules_dx//:dx -- lint //...
bazel run @rules_dx//:dx -- typecheck //...
bazel run @rules_dx//:dx -- format //...
No scope means //.... Use --here for the current dir tree, or --affected for the targets your
git changes reach (see Scope Defaults). With nothing
affected they print no affected targets and exit 0. All three
check files by default and report drift without writing. Pass --apply to
write validated fixes. Args after -- go to Bazel, and dx always adds
--keep_going and --remote_download_outputs=all after them. All three also pass
--@rules_dx//config:validate=false, --@rules_dx//config:clippy_output_diagnostics=true
and --@rules_dx//config:rustc_output_diagnostics=true. Another value for one of them,
or for the matching @rules_rust//rust/settings flag, fails before Bazel starts with exit 2.
dx spells them with the name your MODULE.bazel gives rules_dx, for example
--@my_dx//config:validate=false when repo_name = "my_dx".
These args fail the same way: --aspects, --output_groups,
--build_event_json_file, any --build_event_json_file_path_conversion other
than false, --nokeep_going, any --keep_going other than bare
--keep_going, --remote_download_minimal, --remote_download_toplevel, any
--remote_download_outputs other than all, --test_arg, and the startup
options --bazelrc, --home_rc, --nohome_rc, --system_rc, --nosystem_rc,
--output_base, --output_user_root, --host_jvm_args and --server_jvm_out.
Pass --output_base or --output_user_root with
--bazel-startup-option instead.
With --@rules_rust//rust/settings:pipelined_compilation=true in your .bazelrc,
dx typecheck reads library diagnostics from the metadata build and skips their codegen.
dx lint [--here] [--affected] [--check] [--apply] [--fail-on info|warning|error] [--output text|diff|json] [--include-edits] [--report sarif=<dest>] [scope...] [-- bazel-options ...]
dx typecheck [--here] [--affected] [--check] [--apply] [--fail-on info|warning|error] [--output text|diff|json] [--include-edits] [--report sarif=<dest>] [scope...] [-- bazel-options ...]
dx format [--here] [--affected] [--check] [--apply] [--fail-on info|warning|error] [--output text|diff|json] [--include-edits] [scope...] [-- bazel-options ...]
--check: report findings without writing files. Same as the default.--apply: write validated fixes and refresh the configured baseline. Without it files stay untouched. After--apply, findings point at files as written. Unwritten files keep their checked positions.--fail-on info|warning|error: severity that fails. Defaultwarning.--report sarif=<dest>: write a SARIF report forlintandtypecheck. Repeatable.dx formathas no report format. A relative destination anchors at the workspace root; an absolute one writes as given. Colliding destinations fail before anything runs, parents must exist, and a failed write keeps the previous file bytes. Use-for stdout. Only one stdout report is allowed, and only with text output:--output jsonor--output diffexits2. With a stdout report, text findings and summaries go to stderr. SARIF results carrypartialFingerprints(dx/v1),ruleIndex, andfixesfor machine-applicable replacements. Runs setcolumnKindtounicodeCodePoints.dx lintwithout--applyalso fails with exit1when the checkout holds build output or a file over 1 MiB, as described fordx check.--output text|diff|json: result shape.--include-edits: addsource_digestand fulleditsto JSONchangeevents. Needs--output json; otherwise exits2.
Text prints one line per finding:
path:line:column: severity: message [tool/rule]. Lines and columns count
Unicode characters in the checked file. A finding without a verified position
prints path:; a finding without a file prints (no file):. Identical
findings print once with (xN). --quiet never hides findings or failures.
--output diff prints a unified patch of the proposed fixes on stdout and no
finding lines. Not applied:, Conflict: and Baseline stale: lines go to
stderr.
When a check finds changes, text prints
N file(s) need changes. Apply them with: dx <command> --apply ... with your
workspace, scope, and Bazel options quoted for the shell. Credential-bearing
options are left out. Failures print a next: line with the fix and a check
command to run again. Inside dx check and dx fix, these lines name
dx fix --apply or the umbrella command with your scope instead.
JSON prints one compact change per file with path, kind, edit_count,
and effect (proposed, applied, or refused with a reason). The
reason is stale_source, unreadable_source, invalid_edits,
incomplete_collection, conflicting_digests or conflicting_candidates. Identical
findings from several targets print once, with a count and every target in
origins.
When several targets fix the same file, identical fixes apply once. Fixes that
disagree leave the file unchanged and print a Conflict: line naming the
targets on stderr. JSON reports a change with effect refused and reason
conflicting_candidates or conflicting_digests.
Other files still apply.
The result set is incomplete when a target fails, a result is invalid, Bazel
announces a target or aspect it never reports, an output group is marked
incomplete, or the build events end before Bazel's last event. Targets Bazel
skips as incompatible are not missing. lint, typecheck, and format skip targets that
do not fit the target platform, even named or affected ones; see
Scope Defaults. An incomplete run prints
dx: incomplete_results: <reason> on stderr, or an incomplete_results
error event in JSON, and never applies fixes.
A failed Bazel build lists each failed action the way dx build does: a
build_failure event in JSON, or a dx: build failure: line on stderr. A Rust
compile error in a Clippy or Rustc action carries its path, line,
column and message; see dx build. When every
failed action is a compile error, the run reports bazel_failed with
<command> build failed with Bazel exit <n>; results are incomplete, and text
output adds next: fix the compile errors above, then run: .... Other failed
builds report analysis_failed. The results are incomplete either way.
A target whose fixes oscillate or hit the ten-round limit did not converge.
The run fails even without findings and prints
dx: not_converged: <target> in configuration <id> did not converge: <cause> after <n> round(s) of <tools>
on stderr, or a not_converged error event in JSON. The cause is
OSCILLATION or ITERATION_LIMIT. SARIF marks that tool's run
executionSuccessful: false with the same message. Such a target proposes no
fixes, and --apply does not refresh the baseline. Fixes from other targets
still apply.
Exit codes: 0 success, 2 usage or scope errors, 1 findings at or above
--fail-on, a check (no --apply) with any proposed change, whatever
--fail-on says, an incomplete result set, a target that did not converge, a
conflicting fix, a failed report write, or a missing, malformed, or stale
baseline. Bazel failures report 1, not Bazel's code.
bazel run @rules_dx//:dx -- lint --check //...
bazel run @rules_dx//:dx -- format --here
bazel run @rules_dx//:dx -- lint --affected
bazel run @rules_dx//:dx -- typecheck --fail-on error //cli/...
bazel run @rules_dx//:dx -- format --apply //cli/...
Policy
The three commands check with the workspace policy that
--@rules_dx//config:workspace names. Without that flag the policy is
@rules_dx//quality:default_workspace_policy. Pass another target after --
to select your own.
bazel run @rules_dx//:dx -- lint -- --@rules_dx//config:workspace=//quality:my_policy
bazel run @rules_dx//:dx -- format --check //... -- --@rules_dx//config:workspace=//quality:my_policy
A policy is a workspace_policy of quality_family sections, loaded from
@rules_dx//quality:policy.bzl. Every family states each capability it uses:
a tool ID, or the capability in disabled.
load("@rules_dx//quality:policy.bzl", "quality_family", "workspace_policy")
quality_family(
name = "toml_family",
family_id = "toml",
disabled = ["typecheck"],
format = ["taplo"],
lint = ["taplo"],
)
workspace_policy(
name = "my_policy",
families = [":toml_family"],
)
- A family that leaves a capability neither selected nor disabled fails the analysis with the capability named.
- A family that selects a tool with no wired executable fails with the wired tools listed for that capability.
disabled = ["format"]runs no formatter for that family.
Default policy
default_workspace_policy selects these tools. A capability marked
disabled runs no tool for that family.
| Family | Format | Lint | Typecheck |
|---|---|---|---|
cc | clang_format | disabled | disabled |
graphql | prettier | disabled | disabled |
html | prettier | disabled | disabled |
java | google_java_format | checkstyle, pmd, spotbugs | disabled |
javascript | biome | biome | disabled |
json | prettier | biome | disabled |
kotlin | ktfmt | ktlint | disabled |
markdown | disabled | markdown_check, vale | disabled |
python | ruff | pydoclint, ruff | ty |
rust | rustfmt | clippy | rustc |
scala | scalafmt | disabled | disabled |
shell | shfmt | shellcheck | disabled |
starlark | buildifier | buildifier | disabled |
text | disabled | keep_sorted | disabled |
toml | taplo | taplo | disabled |
typescript | biome | biome | disabled |
Wired tools
Every wired tool, with the verbs it serves and the families its classes belong to.
| Tool ID | Verbs | Families |
|---|---|---|
biome | format, lint | javascript, json, typescript |
buf | format, lint | protobuf |
buildifier | format, lint | starlark |
checkstyle | lint | java |
clang_format | format | cc, cuda |
clang_tidy | lint | cc |
clippy | lint | rust |
cppcheck | lint | cc |
csharpier | format | csharp |
cue | format | cue |
djlint | format, lint | html_template |
erlfmt | format | erlang |
errcheck | lint | go |
eslint | lint | javascript |
fantomas | format | fsharp |
flake8 | lint | python |
fsharplint | lint | fsharp |
gleam_format | format | gleam |
gofumpt | format | go |
google_java_format | format | java |
govet | lint | go |
jsonnetfmt | format | jsonnet |
keep_sorted | lint | text |
ktfmt | format | kotlin |
ktlint | lint | kotlin |
markdown_check | lint | markdown |
mix_format | format | elixir |
modfmt | format | go_module |
pkl | format | pkl |
pmd | lint | java |
prettier | format | css, gherkin, graphql, html, javascript, json, sql, typescript, xml |
psscriptanalyzer | lint | powershell |
pydoclint | lint | python |
pylint | lint | python |
qmlformat | format | qml |
qmllint | lint | qml |
roslyn | lint | csharp |
rubocop | lint | ruby |
ruff | audit, format, lint | python |
rustc | typecheck | rust |
rustfmt | format | rust |
scalafix | lint | scala |
scalafmt | format | scala |
shellcheck | lint | shell |
shfmt | format | shell |
spotbugs | lint | java |
standardrb | format | ruby |
staticcheck | lint | go |
stylelint | lint | css |
taplo | format, lint | toml |
terraform | format | terraform |
ty | typecheck | python |
vale | lint | markdown |
yamlfmt | format | yaml |
yamllint | lint | yaml |
zig_fmt | format | zig |
OCaml (ocaml) has no formatter and no linter. OCaml targets are tagged no-format, no-lint and no-typecheck, so dx format, dx lint and dx typecheck skip them.
Tool overrides
The three commands run every wired tool from its managed executable. Pass
another target after -- to select your own build of one tool. The override
keeps the tool ID, so findings read the same.
bazel run @rules_dx//:dx -- lint -- --@rules_dx//config:tool_ruff=//tools:my_ruff
ruff,terraform,qmlformat,qmllint,zig_fmt,erlfmt,mix_formatandgleam_formathave override flags.- The
ruffandterraformalternates must be a single executable file. Anything else fails the analysis with the tool named. - The
qmlformat,qmllint,zig_fmt,erlfmt,mix_formatandgleam_formatalternates must be executable targets, such as@dx_qt//:qmlformat,@dx_qt//:qmllint,@rules_dx//zig/tools:zig,@rules_dx//erlang/tools:erlfmt,@rules_dx//elixir/tools:mix_formatand@rules_dx//gleam/tools:gleam_format. A target without an executable fails the analysis. - An override that prints another diagnostic shape fails the run with the tool named.
QML formatting
qmlformat has no default executable. Enable the managed Qt SDK from
qt/README.md, select qmlformat in a qml family, and point the flag at
the SDK tool.
bazel run @rules_dx//:dx -- format -- --@rules_dx//config:tool_qmlformat=@dx_qt//:qmlformat
bazel run @rules_dx//:dx -- format --apply -- --@rules_dx//config:tool_qmlformat=@dx_qt//:qmlformat
Without the flag, a policy that selects qmlformat fails the analysis and
names the flag. To set formatting options, bind a qmlformat_config to the
QML targets with aspect_hints. The file must be named .qmlformat.ini. dx
passes it to qmlformat with -s.
Zig formatting
zig_fmt uses the pinned Zig compiler that zig_library, zig_binary and
zig_test carry. Select zig_fmt in a zig family. No flag is needed.
quality_family(
name = "zig_family",
family_id = "zig",
disabled = ["audit", "lint", "typecheck"],
format = ["zig_fmt"],
)
bazel run @rules_dx//:dx -- format
bazel run @rules_dx//:dx -- format --apply
To use your own compiler, set the flag:
bazel run @rules_dx//:dx -- format -- --@rules_dx//config:tool_zig_fmt=@rules_dx//zig/tools:zig
A Zig-class target that is not one of those three wrappers fails the analysis
and names the flag. The check runs zig fmt --check and never writes.
--apply runs zig fmt. A file with a syntax error reports an error at its
line and column and stays untouched. Use the wrappers from
@rules_dx//zig/rules:defs.bzl.
Erlang formatting
erlfmt has no default executable. Select erlfmt in an erlang family and
point the flag at the pinned erlfmt 1.8.0 escript.
quality_family(
name = "erlang_family",
family_id = "erlang",
disabled = ["audit", "lint", "typecheck"],
format = ["erlfmt"],
)
bazel run @rules_dx//:dx -- format -- --@rules_dx//config:tool_erlfmt=@rules_dx//erlang/tools:erlfmt
bazel run @rules_dx//:dx -- format --apply -- --@rules_dx//config:tool_erlfmt=@rules_dx//erlang/tools:erlfmt
Without the flag, a policy that selects erlfmt fails the analysis and names
the flag. The check runs erlfmt --check and never writes. --apply runs
erlfmt --write. A file with a syntax error reports an error at its line and
column and stays untouched. A %% erlfmt-ignore comment keeps the next form
as written. Erlang sources come from the rules in
@rules_dx//erlang/rules:defs.bzl.
Elixir formatting
mix_format has no default executable. Select mix_format in an elixir
family and point the flag at the pinned Elixir 1.20.4 formatter.
quality_family(
name = "elixir_family",
family_id = "elixir",
disabled = ["audit", "lint", "typecheck"],
format = ["mix_format"],
)
bazel run @rules_dx//:dx -- format -- --@rules_dx//config:tool_mix_format=@rules_dx//elixir/tools:mix_format
bazel run @rules_dx//:dx -- format --apply -- --@rules_dx//config:tool_mix_format=@rules_dx//elixir/tools:mix_format
Without the flag, a policy that selects mix_format fails the analysis and
names the flag. The check runs mix format --check-formatted with the default
formatter options and never writes. --apply runs mix format. A file with a
syntax error reports an error at its line and column and stays untouched. The
formatter reads no .formatter.exs, Mix project or Hex home. Elixir sources
come from the rules in @rules_dx//elixir/rules:defs.bzl.
Gleam formatting
gleam_format has no default executable. Select gleam_format in a gleam
family and point the flag at the pinned Gleam 1.19.2 formatter.
quality_family(
name = "gleam_family",
family_id = "gleam",
disabled = ["audit", "lint", "typecheck"],
format = ["gleam_format"],
)
bazel run @rules_dx//:dx -- format -- --@rules_dx//config:tool_gleam_format=@rules_dx//gleam/tools:gleam_format
bazel run @rules_dx//:dx -- format --apply -- --@rules_dx//config:tool_gleam_format=@rules_dx//gleam/tools:gleam_format
Without the flag, a policy that selects gleam_format fails the analysis and
names the flag. The check runs gleam format --check and never writes.
--apply runs gleam format. A file with a syntax error reports an error at
its line and column and stays untouched. The formatter reads no gleam.toml
or Hex cache. Gleam sources come from the rules in
@rules_dx//gleam/rules:defs.bzl.
QML linting
qmllint has no default executable. Enable the managed Qt SDK from
qt/README.md, select qmllint in a qml family, and point the flag at the
SDK tool.
bazel run @rules_dx//:dx -- lint -- --@rules_dx//config:tool_qmllint=@dx_qt//:qmllint
Without the flag, a policy that selects qmllint fails the analysis and names
the flag. Findings keep the line and column qmllint reports. Info, warning
and critical messages show as info, warning and error. A file that qmllint
does not report fails the run.
Import context comes from the qmldir files in the QML target and all its
transitive deps, including external repositories. dx stages them and passes
their module roots with -I. A qmldir whose directory does not end in its
module URI is not importable and is skipped. QML_IMPORT_PATH is not used. To
set lint options, bind a config ending in .qmllint.ini to the QML targets
with aspect_hints. dx copies it next to each linted file as .qmllint.ini.
Without one, dx runs qmllint with --ignore-settings.
Java lint
Java lint runs checkstyle, pmd and spotbugs.
checkstyle has no default ruleset. Bind a checkstyle_config to each Java
target, or lint fails. pmd uses rulesets/java/quickstart.xml unless a
pmd_config is bound. For both, src must be a checked-in .xml file and
data lists any extra files it reads. A target takes at most one config per
tool.
load("@rules_java//java:defs.bzl", "java_library")
load("@rules_dx//quality:native_config.bzl", "checkstyle_config", "pmd_config")
checkstyle_config(
name = "checkstyle_cfg",
src = "checkstyle.xml",
)
pmd_config(
name = "pmd_cfg",
src = "pmd-ruleset.xml",
)
java_library(
name = "lib",
srcs = ["Lib.java"],
aspect_hints = [
":checkstyle_cfg",
":pmd_cfg",
],
)
Scala linting
scalafix runs from the managed pin. It is not in the default Scala lint set.
Select it in a scala family.
quality_family(
name = "scala_family",
family_id = "scala",
disabled = ["audit", "format", "typecheck"],
lint = ["scalafix"],
)
Register the SemanticDB toolchain in MODULE.bazel:
register_toolchains("@rules_dx//scala/toolchain:semanticdb")
That toolchain is a plain scala_toolchain. If you already register your own,
keep it and set these two attributes on it instead:
scala_toolchain(
name = "my_scala_toolchain",
enable_semanticdb = True,
semanticdb_bundle_in_jar = True,
)
Without one of the two, the analysis fails and names the fix. Bind a scalafix_config
to every Scala target with aspect_hints. The file is your .scalafix.conf.
A Scala target without one fails the analysis.
bazel run @rules_dx//:dx -- lint //app/...
bazel run @rules_dx//:dx -- lint --apply //app/...
dx gives scalafix the compiled SemanticDB of the target and its
dependencies, so semantic rules such as ExplicitResultTypes work. This
includes compile-only (neverlink) dependencies and test targets. Lint
messages keep the rule ID, line and column. A rule that rewrites code shows as
a Patch error at the first changed line, and --apply writes the rewrite. A
run that exits with a parse error, missing SemanticDB, an unknown rule or an
empty rule list fails with the cause named. It never counts as clean.
C# formatting
csharpier runs from the managed pin. Select it in a csharp family.
quality_family(
name = "csharp_family",
family_id = "csharp",
disabled = ["audit", "lint", "typecheck"],
format = ["csharpier"],
)
bazel run @rules_dx//:dx -- format //app/...
bazel run @rules_dx//:dx -- format --apply //app/...
A check never writes. --apply rewrites only the C# files the selected
targets own. To change options, bind a csharpier_config to the C# targets
with aspect_hints. An unformatted file is reported at the start of the file. A syntax
error is reported at its line and column with its compiler code and never
counts as clean.
C# lint
roslyn compiles the target again with the SDK analyzers and reports their
findings. The compile uses the target's own sources, references, defines and
compiler options. It needs no install step. Select it in a csharp family.
quality_family(
name = "csharp_family",
family_id = "csharp",
disabled = ["audit", "typecheck"],
format = ["csharpier"],
lint = ["roslyn"],
)
bazel run @rules_dx//:dx -- lint //app/...
The analysis uses the target framework, references, and analyzers the target
builds with. The default is the SDK analysis level for that framework. To
change severities, pass a .globalconfig file in analyzer_configs on the
C# target. A compile error is reported at its line and column with its
compiler code and never counts as clean. Findings in generated files under
bazel-out or in external repositories are not reported, but a compile error
there still fails the check. An analyzer that fails to run fails
the check. Diagnostics suppressed in source with #pragma warning disable or
[SuppressMessage] are not reported.
Roslyn is check only. lint --apply changes no files. Supported on
linux_x86_64.
F# formatting and lint
fantomas formats and fsharplint lints. Both run from the managed pin
with no install step. Select them in an fsharp family.
quality_family(
name = "fsharp_family",
family_id = "fsharp",
disabled = ["audit", "typecheck"],
format = ["fantomas"],
lint = ["fsharplint"],
)
bazel run @rules_dx//:dx -- format //app/...
bazel run @rules_dx//:dx -- format --apply //app/...
bazel run @rules_dx//:dx -- lint //app/...
A format check never writes. --apply rewrites only the F# files the
selected targets own. An unformatted file is reported at the start of the
file. fsharplint has no fixes, so lint --apply changes nothing.
fsharplint checks each target with its sources in compile order, its
dependency assemblies, its defines and its langversion. Rules that need
type information see the same code the compiler sees. Findings keep the
FL rule ID, line and column.
To change lint rules, bind an fsharplint_config to the F# targets with
aspect_hints.
load("@rules_dx//quality:native_config.bzl", "fsharplint_config")
fsharplint_config(
name = "relaxed",
src = "relaxed.fsharplint.json",
visibility = ["//visibility:public"],
)
fsharp_library(
name = "core",
aspect_hints = ["//:relaxed"],
srcs = ["Greeter.fs"],
)
A syntax error is reported by both tools as an error at its line and column
with its FS compiler code. It never counts as clean. A tool that fails to
run, or prints output dx cannot read, fails the run with the cause named.
Protocol Buffers
buf runs from the managed pin for format and lint. Select it in a
protobuf family. It runs on Linux x86_64 and needs diff in /usr/bin or
/bin.
quality_family(
name = "protobuf_family",
family_id = "protobuf",
format = ["buf"],
lint = ["buf"],
)
bazel run @rules_dx//:dx -- format --apply //api/...
bazel run @rules_dx//:dx -- lint //api/...
Targets are proto_library rules. Imports resolve through the target's
deps. A check never writes. --apply rewrites only the .proto files the
selected targets own. Lint uses the upstream STANDARD rules and reports
each finding at its line and column with the rule ID. A syntax error or an
import that does not resolve fails format and lint with the cause named and
never counts as clean. A syntax error in an imported .proto fails lint of
every target that imports it, with that file, line and column named. An
unformatted file is reported at the start of the file. To change the rules, bind a buf_config to the proto_library targets
with aspect_hints. The file is your buf.yaml.
CUE
cue fmt runs from the managed pin for format. Select it in a cue family.
It runs on Linux x86_64.
quality_family(
name = "cue_family",
family_id = "cue",
format = ["cue"],
)
bazel run @rules_dx//:dx -- format //config/...
bazel run @rules_dx//:dx -- format --apply //config/...
Own CUE files with cue_library from @rules_dx//cue/rules:defs.bzl. List the
package files in srcs and the libraries it imports in deps. Put the
cue.mod/module.cue of your module in the srcs of a cue_library and list
that library in deps. Both are staged as import context. A check never
writes. --apply rewrites only the .cue files the selected targets own. An
unformatted file is reported at the start of the file. A syntax error fails the
run with the file, line and column. It never counts as clean. cue takes no
config file.
Jsonnet
jsonnetfmt runs from the managed pin for format. Select it in a jsonnet
family. It runs on Linux x86_64.
quality_family(
name = "jsonnet_family",
family_id = "jsonnet",
format = ["jsonnetfmt"],
)
bazel run @rules_dx//:dx -- format //config/...
bazel run @rules_dx//:dx -- format --apply //config/...
Own Jsonnet files with jsonnet_library from
@rules_dx//jsonnet/rules:defs.bzl. List the .jsonnet and .libsonnet files
of the package in srcs and the libraries it imports in deps. The imported
libraries are staged beside the entry file. A check never writes. --apply
rewrites only the files the selected targets own. An unformatted file is
reported at the start of the file. A syntax error fails the run with the file,
line and column. It never counts as clean. jsonnetfmt stops at the first
syntax error, so fix it and run again to see the next one. jsonnetfmt takes
no config file.
Pkl
pkl format runs from the managed pin for format. Select it in a pkl family.
It runs on Linux x86_64.
quality_family(
name = "pkl_family",
family_id = "pkl",
format = ["pkl"],
)
bazel run @rules_dx//:dx -- format //config/...
bazel run @rules_dx//:dx -- format --apply //config/...
Own Pkl files with pkl_library from @rules_dx//pkl/rules:defs.bzl. List the
.pkl files of the package in srcs and the libraries it imports in deps. A
check never writes. --apply rewrites only the files the selected targets own.
An unformatted file is reported at the start of the file. A syntax error fails
the run with the file, line and column. It never counts as clean. pkl format
checks syntax only and does not resolve imports. It takes no config file.
To use another Pkl executable, point tool_pkl at it.
bazel run @rules_dx//:dx -- format -- --@rules_dx//config:tool_pkl=//tools:pkl
Terraform
terraform fmt runs from the managed pin for format. Select it in a
terraform family. It runs on Linux x86_64. It never runs init, reads state
or contacts a backend.
quality_family(
name = "terraform_family",
family_id = "terraform",
format = ["terraform"],
)
bazel run @rules_dx//:dx -- format //infra/...
bazel run @rules_dx//:dx -- format --apply //infra/...
Own Terraform files with terraform_module from
@rules_dx//terraform/rules:defs.bzl. List the .tf, .tfvars, .tftest.hcl and
.tfmock.hcl files of the module in srcs and the modules it calls in deps. A check never
writes. --apply rewrites only the files the selected targets own. An
unformatted file is reported at the start of the file. A syntax error fails the
run with the file and line. It never counts as clean. terraform fmt takes no
config file.
To use OpenTofu, point tool_terraform at a tofu executable file. The
findings keep the terraform tool ID.
bazel run @rules_dx//:dx -- format -- --@rules_dx//config:tool_terraform=@tofu//:tofu
CSS, SCSS and Less
stylelint runs from the managed pin for lint. It runs on Linux x86_64. Select
it in the css family. Format stays with prettier.
quality_family(
name = "css_family",
family_id = "css",
format = ["prettier"],
lint = ["stylelint"],
)
bazel run @rules_dx//:dx -- lint //web/...
bazel run @rules_dx//:dx -- format --apply //web/...
Own stylesheets with javascript_library. It accepts .css, .scss and
.less in srcs. Lint is check-only: lint --apply never rewrites a
stylesheet. Each finding shows its line and column with the rule name. The
default rules are the Stylelint correctness checks. They accept CSS Modules
:global, :local, :export, :import and composes. SCSS and Less files
parse with postcss-scss and postcss-less.
A syntax error, an invalid config or a tool that fails to run fails the run
with the cause named. It never counts as clean. To change the rules, bind a
stylelint_config to the library with aspect_hints. The file is a JSON
config. It replaces the default rules, so add an overrides entry with
customSyntax for *.scss and *.less.
HTML, GraphQL, JSON5 and JSONC
prettier runs from the managed pin for format. It runs on Linux x86_64. Select
it in the html, graphql and json families. JSON5 and JSONC files belong
to the json family.
quality_family(
name = "graphql_family",
disabled = ["lint", "typecheck"],
family_id = "graphql",
format = ["prettier"],
)
bazel run @rules_dx//:dx -- format //web/...
bazel run @rules_dx//:dx -- format --apply //web/...
Own these files with javascript_library. It accepts .html, .graphql,
.gql, .json5 and .jsonc in srcs. A check never writes. --apply
rewrites only the files the selected targets own. Comments in JSON5 and JSONC
stay, and both gain trailing commas. Scripts inside HTML are formatted too.
An unformatted file is reported at the start of the file. A syntax error fails the run with the file, line and
column. It never counts as clean. prettier runs with its default options for
these files. .prettierrc, .editorconfig and other user config files are
ignored.
SQL, XML and Gherkin
prettier runs from the managed pin with the prettier-plugin-sql,
@prettier/plugin-xml and prettier-plugin-gherkin plugins for format. It runs
on Linux x86_64. Select it in the sql, xml and gherkin families.
quality_family(
name = "sql_family",
disabled = ["lint", "typecheck"],
family_id = "sql",
format = ["prettier"],
)
bazel run @rules_dx//:dx -- format //db/...
bazel run @rules_dx//:dx -- format --apply //db/...
Own the files with sql_library from @rules_dx//sql/rules:defs.bzl,
xml_library from @rules_dx//xml/rules:defs.bzl and gherkin_library from
@rules_dx//gherkin/rules:defs.bzl. They accept .sql; .xml, .xsd, .xsl,
.xslt and .wsdl; and .feature in srcs, and other libraries in deps. A
check never writes. --apply rewrites only the files the selected targets own.
An unformatted file is reported at the start of the file. A syntax error fails
the run with the file, line and column where the tool gives one. It never counts
as clean.
SQL files use the generic sql dialect. To pick a dialect, put it before the
extension: schema.postgresql.sql. The dialects are bigquery, clickhouse,
db2, db2i, hive, mariadb, mysql, n1ql, plsql, postgresql,
redshift, singlestoredb, snowflake, spark, sqlite, transactsql,
trino and tsql. The pinned options keep SQL keywords in the case you
wrote them. XML namespaces, prefixes, attribute quotes, attribute order and
text stay as written. Gherkin keeps its step text and tags. .prettierrc,
.editorconfig and other user config files are ignored.
PowerShell
psscriptanalyzer runs from the managed PSScriptAnalyzer 1.25.0 pin for lint.
It runs on Linux x86_64. Select it in the powershell family.
quality_family(
name = "powershell_family",
family_id = "powershell",
lint = ["psscriptanalyzer"],
)
bazel run @rules_dx//:dx -- lint //scripts/...
Own scripts and modules with pwsh_library and pwsh_binary from
@rules_dx//powershell/rules:defs.bzl. pwsh_library accepts .ps1, .psm1
and .psd1 in srcs. pwsh_binary accepts .ps1. A using module or
Import-Module of a module in deps resolves in lint against the target's
declared modules only. Lint never searches modules installed on the host. When
you run or test the target, the host's module path is still searched after the
declared modules. For Pester
tests, add @rules_dx//powershell/gallery:pester to the deps of a
pwsh_test.
Lint is check-only: lint --apply never rewrites a script. Each finding shows
its line and column with the rule name. A script that does not parse is
reported as an error.
An invalid config or a tool that fails to run fails the run with the cause
named. It never counts as clean. To change the rules, bind a
psscriptanalyzer_config to the target with aspect_hints. The file is a
.psd1 settings file. Unknown rule names in IncludeRules or ExcludeRules
are errors.
Ruby formatting and lint
standardrb formats and rubocop lints. Both run from the gems of a
rules_ruby bundle you declare, never from ambient gems or Bundler. Linux x86_64
only. Select them in a ruby family. Only standardrb formats.
Copy third_party/ruby/Gemfile, Gemfile.lock and its gem_checksums into your
module, then point the tools at the bundle in .bazelrc.
ruby = use_extension("@rules_ruby//ruby:extensions.bzl", "ruby")
ruby.bundle_fetch(
name = "dx_bundle",
gem_checksums = {...},
gemfile = "//:Gemfile",
gemfile_lock = "//:Gemfile.lock",
)
use_repo(ruby, "dx_bundle")
build --@rules_dx//quality/tools/ruby:rubocop=@dx_bundle//bin:rubocop
build --@rules_dx//quality/tools/ruby:standardrb=@dx_bundle//bin:standardrb
Without these flags the Ruby tools exit 2 and say which flag is missing.
Other languages are unaffected.
quality_family(
name = "ruby_family",
family_id = "ruby",
disabled = ["audit", "typecheck"],
format = ["standardrb"],
lint = ["rubocop"],
)
bazel run @rules_dx//:dx -- format //app/...
bazel run @rules_dx//:dx -- format --apply //app/...
bazel run @rules_dx//:dx -- lint //app/...
A check never writes. --apply rewrites only the Ruby files the selected
targets own. rubocop has no fixes, so lint --apply changes nothing.
Lint findings keep the cop name and line. A syntax error is reported as an
error at its line and never counts as clean. An unreadable config fails the run.
A cop crash or a configuration error fails the target, even when every
offense is baselined.
To change options, bind a rubocop_config or standardrb_config to the Ruby
targets with aspect_hints.
load("@rules_dx//quality:native_config.bzl", "rubocop_config")
rubocop_config(
name = "relaxed",
src = "relaxed.rubocop.yml",
visibility = ["//visibility:public"],
)
ruby_library(
name = "core",
aspect_hints = ["//:relaxed"],
srcs = ["greeter.rb"],
)
YAML
yamlfmt formats and yamllint lints. Both run from managed pins on Linux
x86_64. Select them in a yaml family. Only yamlfmt has fixes.
quality_family(
name = "yaml_family",
family_id = "yaml",
format = ["yamlfmt"],
lint = ["yamllint"],
)
yamllint runs from a Python 3.12 toolchain. Declare the same toolchain in your
MODULE.bazel as rules_dx does: python_interpreters.toolchain and
python.toolchain with python_version = "3.12", plus rules_python and
aspect_rules_py.
bazel run @rules_dx//:dx -- format //config/...
bazel run @rules_dx//:dx -- format --apply //config/...
bazel run @rules_dx//:dx -- lint //config/...
Own YAML files with yaml_library from @rules_dx//yaml/rules:defs.bzl. List
the .yaml and .yml files of the package in srcs. A check never writes.
--apply rewrites only the files the selected targets own. yamllint is
check-only. Multiple documents and anchors are supported.
An unformatted file is a warning. A parse error is an error with the file and
line, and it never counts as clean. yamllint findings keep the rule name,
line and column.
To change options, bind a yamlfmt_config or yamllint_config to the YAML
targets with aspect_hints.
load("@rules_dx//quality:native_config.bzl", "yamlfmt_config", "yamllint_config")
yamlfmt_config(
name = "indent4",
src = "indent4.yamlfmt",
visibility = ["//visibility:public"],
)
yamllint_config(
name = "relaxed",
src = "relaxed.yamllint",
visibility = ["//visibility:public"],
)
yaml_library(
name = "app",
aspect_hints = [
"//:indent4",
"//:relaxed",
],
srcs = ["app.yaml"],
)
HTML templates
djlint formats and lints HTML templates. It runs from a managed pin on Linux
x86_64. Select it in an html_template family. Only the format capability has
fixes. Lint is check-only.
quality_family(
name = "html_template_family",
family_id = "html_template",
format = ["djlint"],
lint = ["djlint"],
)
djlint runs from a Python 3.12 toolchain. Declare the same toolchain in your
MODULE.bazel as rules_dx does: python_interpreters.toolchain and
python.toolchain with python_version = "3.12", plus rules_python and
aspect_rules_py.
bazel run @rules_dx//:dx -- format //web/...
bazel run @rules_dx//:dx -- format --apply //web/...
bazel run @rules_dx//:dx -- lint //web/...
Own templates with html_template_library from
@rules_dx//html_template/rules:defs.bzl. List the .html, .htm, .jinja,
.jinja2, .j2, .njk, .hbs, .gohtml and .tmpl files of the package in
srcs. Use deps for libraries that your templates include from. A check never
writes. --apply rewrites only the files the selected targets own.
An unformatted file is a warning. Lint findings keep the rule code, line and
column. A profile that djlint does not support, or a config file it cannot
load, is an error and never counts as clean.
To change options, bind a djlint_config to the template targets with
aspect_hints. The file is named djlint.toml or .djlintrc. Set the dialect with profile: all, angular,
askama, django, golang, handlebars, html, jinja, liquid,
nunjucks or tera. Without a bound config, configs in the workspace are
ignored.
load("@rules_dx//quality:native_config.bzl", "djlint_config")
djlint_config(
name = "indent2",
src = ".djlintrc",
visibility = ["//visibility:public"],
)
html_template_library(
name = "web",
aspect_hints = ["//:indent2"],
srcs = ["templates/page.html"],
)
Shell
shfmt formats and shellcheck lints shell sources. Both run in the shell
family.
quality_family(
name = "shell_family",
family_id = "shell",
format = ["shfmt"],
lint = ["shellcheck"],
)
Today only targets that feed the shell family explicitly (for example through
real_source_target) reach shellcheck and shfmt. Plain rules_shell
sh_binary, sh_library and sh_test sources are not classified as shell
yet, so dx lint does not lint them.
shellcheck lint is check-only: lint --apply never rewrites a script.
shfmt formatting is the part that --apply rewrites.
shellcheck never reads a .shellcheckrc from the host. A file in a parent
directory of the script is ignored, so a lint result does not depend on the
machine it runs on. To change the rules, bind a shellcheck_config to the
target with aspect_hints. The file is passed with --rcfile. Without a config, --norc is passed.
The user-level ~/.shellcheckrc and XDG config are ignored too. Name the file
something other than .shellcheckrc.
load("@rules_dx//quality:native_config.bzl", "shellcheck_config")
shellcheck_config(
name = "shellcheck_rc",
src = "lint.shellcheckrc",
)
Baselines
Opt in to a baseline to keep known findings while new ones fail. Set the
file in dx.toml:
[quality]
baseline = "quality/baseline.json"
dx.local.toml can override the selection; CI reads only dx.toml.
The path stays inside the workspace and names a .json file. Commit the
file with the selection. A missing file fails checks. Pass --apply once to
write it.
The file lists known diagnostics with tool, rule, path, message, context, and count:
{
"schema_version": 1,
"entries": [
{
"tool": "ruff",
"rule": "F401",
"path": "src/app.py",
"message": "unused import",
"context": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"count": 1
}
]
}
A finding without a path records the Bazel target that produced it:
{
"tool": "audit",
"rule": "",
"path": "",
"target": "//app:lint",
"message": "known advisory",
"context": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"count": 1
}
Identity is tool, rule, path, message, and source context, plus the target for a finding without a path. Line numbers alone never match, so moving a line keeps its entry. A rename or a rule change needs a refresh instead.
Checks print Baseline <file>: <total> total, <new> new, <suppressed> suppressed. Suppressed findings print with (baselined) and stay in JSON
and SARIF output. JSON marks them with "baseline": "suppressed" and
carries the counts in command_finished. SARIF carries the counts in each
run. New findings fail as usual. Fixed entries turn stale and fail. Entries
outside the checked tool and file scope, or tool and target scope for a
finding without a path, stay unevaluated. An incomplete run never goes
green through a baseline.
Pass --apply to refresh the file. Refresh adds new findings, prunes fixed
entries in scope, and keeps the rest. An entry is in scope only for the
tool and file that run analyzed, or the tool and target for an entry without a
path. Refresh never writes after an incomplete
run, a result that did not converge, or a finding whose source file cannot
be read; the file stays byte for byte as it was. A malformed file fails
instead of being rewritten.
Adopt in small steps. Run the check, review the findings, record them with
--apply, then keep the file current on later runs.
dx check, dx fix, And dx clean
bazel run @rules_dx//:dx -- check //...
bazel run @rules_dx//:dx -- fix //...
bazel run @rules_dx//:dx -- clean
dx check And dx fix
dx check [--here] [--affected] [--check] [--fail-on info|warning|error] [--report sarif=<dest>] [scope...] [-- bazel-options...]
dx fix [--here] [--affected] [--check] [--apply] [--fail-on info|warning|error] [--report sarif=<dest>] [scope...] [-- bazel-options...]
Before any phase, dx check and dx fix fail when your change adds build
output: a new file under target/, bazel-*, node_modules/, __pycache__/,
.gradle/, or a build/ directory outside a source tree, or a file over
1 MiB. The error names each file and exits 1.
- Your change is untracked unignored files, staged and unstaged edits, and
commits since the merge base with the default branch (
origin/HEAD,origin/main,main,origin/master,master). - Ignored files never count.
- A file that already exists at the merge base is only checked for size.
- List a path or a
dir/prefix you want to keep in.dx-large-files, one per line. Listed paths skip both rules. - Symlinks and Git LFS files are skipped.
- With no default branch or merge base (a shallow clone), only the working
tree is checked. In text output
dxprints a note on stderr. - Outside a Git checkout the check is skipped.
- If Git fails, the command exits
2with codebuild_output_git_failed.
Runs format, then lint, then typecheck, then generate in order.
--affected limits the first three to targets your git changes reach (see
Scope Defaults), runs generate on the packages of those
targets, or on the whole repo when the scope widens to //..., and makes
dx check run the affected tests as a last test phase. It skips that phase when no test is affected. With
--output=json, affected.tests on command_started lists the tests dx
chose. With nothing affected, dx check --affected and dx fix --affected
run no phase, generate included, and skip the build-output check. They print
no affected targets and exit 0. With --output=json they emit only
command_started (with affected) and command_finished; mode is check
unless --apply is given. Affected targets that do not fit the host platform
are skipped.
Every phase, including test, passes the same Bazel settings:
--@rules_dx//config:validate=false, --@rules_dx//config:clippy_output_diagnostics=true
and --@rules_dx//config:rustc_output_diagnostics=true. Another value for one of them,
or for the matching @rules_rust//rust/settings flag, fails before Bazel starts with exit 2.
dx spells them with the name your MODULE.bazel gives rules_dx, for example
--@my_dx//config:validate=false when repo_name = "my_dx".
--check passes through to every phase. --fail-on applies to format,
lint, and typecheck. --report sarif=<dest> merges the lint and
typecheck phases into one document. generate and test take neither. A
relative destination anchors at the workspace root; an absolute one writes as
given. Colliding destinations fail before anything runs, parents must exist,
and a failed write keeps the previous file bytes. dx check is always a check,
so --check is implied. - for stdout is rejected because the phases share
one document.
dx check and dx fix run every phase. A phase with findings or drift, a
failed build or tool, a target that does not converge, incomplete results, or
a report that cannot be written does not stop the next phase, so one run
reports every phase. The run still exits nonzero, and a later clean phase
never clears it. The run stops, and marks the remaining phases skipped, only
on a bad scope or configuration, when Bazel cannot run (it fails to launch, is
killed by a signal, or the disk is full), or on a cancel. A phase whose only
failures are compile errors does not stop dx check, but it stops dx fix,
generate included: the remaining phases are skipped with
<phase> skipped after <phase> failed to compile. The skipped phases are named
in the JSON umbrella_phase_skipped notice events. In text output, a run with a failed
phase ends with a dx: phases: line on stderr that says how each phase ended.
Every phase that ran and owed a capture must leave a readable SARIF document
behind. A missing, unreadable, or malformed capture is a collection_failed
error: the run fails and the report is marked incomplete. Findings from the
phases that did land are still merged. The written line names every phase
that failed, was skipped, or lost its capture.
A phase that fails still keeps the findings it wrote: its SARIF is merged and
the report is marked incomplete. A phase that only reports findings leaves the
report complete. The run exits with the code of the phase that stopped it, or
of the first failed phase, even when the report cannot be written. A
failed phase that wrote nothing adds no collection_failed error.
Output: --output text|diff|json. JSON keeps one command_started and one
command_finished for the run. Each phase sits between a phase_started and
a phase_finished, and its events carry command, phase, and a
correlation such as check/format. dx fix --apply verification phases use
verify/<phase>. A verify/<phase> that writes events dx cannot read adds a
correlated collection_failed error and fails the run. A phase that reported findings adds a correlated notice
(umbrella_phase_findings), a skipped phase adds a correlated notice
(umbrella_phase_skipped), and a capture the run could not collect adds a
correlated error. --include-edits adds full edits to JSON change events
and needs --output json.
Findings keep the tool's labels, notes, help, and related locations. Text
prints them under the finding, JSON adds an ordered related list, and SARIF
adds relatedLocations. They are never counted as findings or applied as
edits.
SARIF results carry partialFingerprints (dx/v1), ruleIndex, and fixes for machine-applicable replacements. Runs set columnKind to unicodeCodePoints.
dx check only reports. dx fix checks by default; dx fix --apply
writes validated fixes, then re-checks read-only every phase up to the last
one that changed or refused a file. The re-check runs only when every phase
passed. When no phase changed anything, no re-check runs. dx fix --apply succeeds only when that re-check passes;
remaining findings, a target that did not converge, or a failed verification
stay visible and fail the run.
When a phase finds changes, text prints
N file(s) need changes. Apply them with: dx fix --apply ... with the scope
you passed, --affected included. next: lines name dx check or dx fix
with that scope.
Exit codes: 0 success, 2 usage or scope errors, 1 a phase failed or
reported findings, a capture could not be collected, the post-apply
verification failed, or a report could not be written. A phase that stopped
the run keeps its code, so a generate phase keeps Bazel's code. Otherwise the
first failed phase sets the code.
The test phase also keeps Bazel's code. 3 with every test result collected
means some affected tests failed: the run counts that as findings and its
results are complete. 3 or 1 with results incomplete is an incomplete
phase, not findings: the reason is test failed and the results are
incomplete. Results are incomplete when the build failed, a result has no
test.xml, a test.xml is missing, truncated or invalid, Bazel announces a
result it never reports, a target has a summary but no result, or the build
events end before Bazel's last event. 2 and 4 mean the scope selected no
runnable tests.
bazel run @rules_dx//:dx -- check //...
bazel run @rules_dx//:dx -- fix --here
bazel run @rules_dx//:dx -- check --affected
bazel run @rules_dx//:dx -- fix --apply //...
dx clean
dx clean [--check] [--apply] [--prune-unobserved]
Reports unselected managed state under .dx by default and deletes
nothing. --check is that default: it reports and deletes nothing. Pass --apply to prune it. A generation a live reader still
holds is never pruned. When process observation is unavailable, dx keeps
unobserved state and reports it; pass --prune-unobserved to prune it
anyway; this never overrides a held lease. Never touches Bazel outputs; run bazel clean for those. Takes no scopes.
Output: --output text|json. --quiet silences text output.
Text, check mode, when something can be pruned or is preserved:
dx clean: reclaimable managed stateprocess observation unavailable: unobserved state is preserved, never assumed idleprune setup record: .dx/setups/<hex> (<n> bytes)prune generation: .dx/<environments|generated>/<hex> (<n> bytes)preserve current: .dx/setups/<hex>, orno current selectionpreserve unobserved setup record: .dx/setups/<hex>andpreserve unobserved generation: .dx/<environments|generated>/<hex>refuse unmanaged path: <path>reclaimable total: <n> bytesrun `dx clean --apply` to prune it, orrun `dx clean --apply --prune-unobserved` to prune it anywaywhen only preserved state is listed
When nothing can be pruned or preserved it prints dx clean: nothing to prune.
Text, --apply: dx clean: pruned <n> setup records and <m> generations (<b> bytes reclaimed), followed by ; preserved <k> leased generations
when a live reader holds one. When process observation is unavailable it
prints dx clean: preserved <n> unobserved setup records and <m> unobserved generations (process observation unavailable) and
run `dx clean --apply --prune-unobserved` to prune it anyway. With
nothing to prune it prints dx clean: nothing to prune.
JSON: command_started has mode check, or default with --apply.
Then one operation event, collect. Then one info notice per path.
Each notice has a path under .dx/ and the byte size in message:
clean_planned: would be pruned. Check mode.clean_preserved_unobserved: kept because process observation is unavailable. Check mode and--apply, when process observation is unavailable.clean_pruned: pruned.--apply.clean_preserved_leased: kept because a live reader holds it.--apply.
A failure emits an error event with code clean_failed.
command_finished ends the stream.
Exit codes: 0 success, 2 usage errors including any scope,
1 drift or a prune failure. State kept only because process observation
is unavailable exits 0.
bazel run @rules_dx//:dx -- clean --check
bazel run @rules_dx//:dx -- clean --apply
bazel run @rules_dx//:dx -- clean --apply --prune-unobserved
dx generate
bazel run @rules_dx//:dx -- generate
bazel run @rules_dx//:dx -- generate --apply //...
bazel run @rules_dx//:dx -- generate --here
bazel run @rules_dx//:dx -- generate --output json //pkg/...
bazel run @rules_dx//:dx -- generate --apply //... -- --jobs=4
Updates BUILD declarations from source with Gazelle over the scope. To
materialize declared generated source, use dx codegen. No scope means the whole
repo. Use --here for the current dir tree. Checks by default and reports
stale files without writing. Pass --apply to write them. Bazel options go
after --.
dx generate [--here] [--check] [--apply] [scope...] [-- bazel-options...]
--check: fail if files are stale instead of writing them. Same as the default.--apply: write the updated BUILD files. Without it files stay untouched.--fail-on,--report,--affected: rejected as usage errors (exit2).--output text|diff|json: result shape.diffprints one unified patch per stale file, or per file--applywrote or did not apply.
Exit codes: 0 success, 2 usage or scope errors, 1 stale files with
--check or the default, a file --apply did not write, incomplete results, a
missing or invalid generation result (invalid_result), or a patch that fails
to render (diff_failed). Bazel failures keep Bazel's code.
Output
Text starts with Running generate for <scope>. --quiet hides it. Then it
prints Created <path> or Modified <path> for each written file, or each
stale file with --check, sorted by path. Each ignored import prints
Ignored import '<import>' in <path> [<language>]: Static import intentionally contributes no Bazel dependency.
Files that were not written go to stderr as Not applied: <path> (<reason>).
An incomplete --apply run still prints the files it wrote and the
Not applied lines. An incomplete --check run, or a Bazel failure that
leaves no result, prints no file lines.
JSON starts with command_started with mode check, or default with
--apply. Then one change per file with path, kind (create or
modify), edit_count, and effect (proposed, applied, or refused).
A refused change has a reason: write_mismatch, missing_file, or
unreadable_file. Each ignored import emits a notice with code
ignored_import, level warning, path, language, and import. A Bazel
failure with --apply, or one that leaves results incomplete, emits a
bazel_failed error. So does a Gazelle failure with --check, including a
nonzero exit that does not come with a Gazelle diff. command_finished ends the stream with
results_complete, exit_code, and changes with create and modify
counts, plus applied and refused with --apply. An incomplete --apply
run still emits its change events and changes with applied and refused.
An incomplete --check run, or a Bazel failure that leaves no result, emits no
change events and no changes.
Directives
# gazelle:dx_ignore_import <language> <import> skips one import in this
directory and below. dx generate fails when no literal reference matches it,
when the language is unknown, or when the value is malformed.
Accepted languages: astro, cc, csharp, fsharp, gleam, go, java,
javascript, kotlin, mdx, python, ruby, rust, scala, svelte,
typescript, vue, zig.
# gazelle:dx_cuda_archs <spec> <spec>... sets the archs of a generated
cuda_library. It applies to this directory and below, and a deeper directive
replaces it. Separate specs with spaces, because a spec can contain commas, as
in compute_89:compute_89,sm_89. The cc language generates a <dir>_cuda
target for .cu and .cuh files that no hand-written CUDA rule lists, and it
fails when the directive is missing. Gazelle never touches hand-written
cuda_library, cuda_binary, cuda_test or cuda_gpu_test targets. It sets
archs only when it creates the target, so a hand-edited value survives later
runs. Sources named *_test.cu and sources that define main are not
generated, and main fails the run.
# gazelle:lang must list dx_dispatch (for example
# gazelle:lang go,dx_dispatch), or dx generate fails.
# gazelle:resolve <language> <import> <label> maps one import to an exact
label.
# gazelle:dx_ignore_import python requests
# gazelle:resolve python yaml @pypi//pyyaml
For java, kotlin and scala, a resolve mapping names the fully qualified
import or the import's own package, and a dx_ignore_import names the fully
qualified import. A wildcard import such as kotlinx.coroutines.* resolves
through its package. A bare class name such as ImmutableList no longer
matches, so a stale bare-name entry fails the run.
# gazelle:resolve kotlin kotlinx.coroutines @maven//:org_jetbrains_kotlinx_kotlinx_coroutines_core
# gazelle:resolve kotlin com.google.common.collect.ImmutableList @maven//:com_google_guava_guava
dx docs
dx docs [--check] [--serve [--port <n>] [--host <addr>] [--open]] [--here] [scope...] [-- bazel-options...]
Builds the documentation site with Bazel and the pinned upstream mdBook:
every dx command page, the GitHub CI guide, and the examples.
With no scope it builds the repository site (//tools/docs/site:user_site,
//tools/docs/site:user_site_check in --check). Args after -- go to Bazel unchanged.
The build also passes
--@rules_dx//config:validate=false, --@rules_dx//config:clippy_output_diagnostics=true
and --@rules_dx//config:rustc_output_diagnostics=true. Another value for one of them,
or for the matching @rules_rust//rust/settings flag, fails before Bazel starts with exit 2.
dx spells them with the name your MODULE.bazel gives rules_dx, for example
--@my_dx//config:validate=false when repo_name = "my_dx".
Scopes: Bazel labels or patterns (//..., //pkg:target, @repo//...), or
workspace-relative files or dirs resolved through Bazel query. --here builds
the current directory tree (//path/..., or //... at the root) and cannot be
combined with explicit scopes (exit 2).
--check: validate the book without rendering it. Builds the generated summary and landing page only.--serve: build, then preview the built site tree locally withpython3 -m http.server. Needspython3onPATHand exactly one built site tree with anindex.htmlentry page.--port <n>: preview port,1-65535, default8000.0is a usage error.--host <addr>: preview bind address, non-empty, default127.0.0.1.--open: open the preview URL in a browser throughpython3. If that fails,dxprints a warning and the preview keeps running.
--port, --host, and --open need --serve.
Output: --output text|json. --output diff is a usage error (exit 2).
Text prints Running docs build for <scope> (mdBook render), or
Running docs check for <scope> (no render) with --check. With --serve
it then prints Serving docs at <url> (<dir>), and Opening <url> with
--open. JSON starts with command_started (mode check or default),
then operation events (summary, plus render without --check). On
failure one error follows: launch_failed when Bazel cannot be launched,
bazel_failed, disk_full, bazel_signalled, or serve_failed. The stream
ends with command_finished. A usage, option, or scope error fails before the
command starts: one error with invalid_arguments, then command_finished,
and no command_started. Under --serve, command_finished comes when the
preview server exits.
Reports: none. --report is a usage error.
Exit codes: 0 success, 2 usage or scope errors, including --here with
an explicit scope, --output diff, and --report. 1 a launch, signal, or
preview failure, including launch_failed (Bazel cannot be launched),
serve_failed (no single site tree, a missing index.html, or a preview that
fails to launch) and a signalled preview. Bazel failures keep Bazel's code.
disk_full keeps exit 36: Bazel exited 36 and a disk it writes to has under
1 GiB free. dx then suggests dx worktree prune --apply. Under --serve the
preview server's code is the exit code.
bazel run @rules_dx//:dx -- docs --check
bazel run @rules_dx//:dx -- docs
bazel run @rules_dx//:dx -- docs --serve --port 8080
bazel run @rules_dx//:dx -- docs --here
bazel run @rules_dx//:dx -- docs --serve --host 0.0.0.0 --port 8080 --open //tools/docs/site:user_site
bazel run @rules_dx//:dx -- docs --check --output=json
bazel run @rules_dx//:dx -- docs --check -- --config=ci
The build never changes sources. Serve runs a local preview only. The site renders Markdown to HTML with syntax-highlighted code, a sidebar, a search index, and one route per page.
TypeScript API reference
typescript_doc_shard from @rules_dx//tools/docs/typescript:defs.bzl runs the pinned
TypeDoc over one TypeScript project and produces a docs IR shard with language
typescript. Pass the shard to docs_site beside any Rust shards, and the shared
pipeline renders the pages. Symbol ids have the form typescript:<package>:<name>.
load("@rules_dx//tools/docs/typescript:defs.bzl", "typescript_doc_shard")
typescript_doc_shard(
name = "api_docs",
srcs = glob(["src/*.ts"]),
entry_points = ["src/index.ts"],
package = "mylib",
tsconfig = "tsconfig.json",
)
Build the shard's site with the scope of the target:
bazel run @rules_dx//:dx -- docs //lib:api_docs
Exit codes: 0 success, 2 usage or scope errors, 1 a launch, signal or preview
failure. Bazel failures keep Bazel's code, so a TypeDoc error in the shard fails the build.
The rule owns the options plugin, options, tsconfig, entryPoints, json,
out and basePath, and fails at load time if options sets one of them. It also
passes an empty --options file, so a stray typedoc.json is never read.
typedocOptions in tsconfig.json or package.json is still loaded by TypeDoc,
and it can load JavaScript plugins. Keep plugin out of those files.
Audit And Update Commands
dx security And dx license
dx security [--here] [--affected] [--offline] [--frozen] [--fail-on info|warning|error] [--report sarif=<dest>] [scope...]
dx license [--here] [--affected] [--offline] [--frozen] [--fail-on info|warning|error] [--report sarif=<dest>|spdx=<dest>] [scope...]
dx security checks secrets plus dependency vulnerabilities. dx license
checks dependency licenses. No scope means //.... Use --here for the
current dir tree, or --affected for the sets that own targets your git
changes reach (see Scope Defaults). With no changes, or
for dx license no owned set, they print no affected targets and exit 0.
When files changed but no set owns them, dx security runs only the secrets
scan. Neither command changes files. --check and Bazel options do not apply.
dx security reads the lockfile of each set it owns and matches it against
.dx/advisory/<family>.json. Sets: cargo, go, maven, npm, npm-adopt,
npm-adopt-polyglot, npm-tools, nuget, powershell, ruby. The
npm-adopt, npm-adopt-polyglot, and npm-tools sets share the npm snapshot,
and the ruby set uses the rubygems snapshot. The powershell set has no
advisory snapshot. A run that selects it fails with failed to assess powershell
when PSGallery.lock.json lists modules, unless security.toml accepts it with
an [[uncovered]] entry, and lists it under no advisory coverage otherwise.
Sets without a snapshot are listed as no advisory coverage for <sets>.
Create or refresh the snapshots from the workspace root:
bazel run @rules_dx//cli/advisory_prep --
A snapshot is fresh only on the day it was prepared. A missing or stale
snapshot fails with advisory_refresh_failed and names this command. See
Advisory Snapshots for its flags.
A scope selects sets by path, and a path can select more than one. A ... scope
selects the set of the nearest path above it in the table plus every set nested
under it, so //quality/... selects cargo, npm-tools, and uv-tools, and
//third_party/..., a path the table does not name, selects go, maven,
nuget, powershell, and ruby. A package under a path in the table selects
that path's set, so //cli/cli selects cargo. Each set then reads its own
lockfile, so a scope narrows which sets run, never which packages inside a
lockfile.
//cli/..., //tools/docs/..., //env/..., //generation/..., //quality/..., //rust/... cargo
//examples/adopt-rust/... cargo
//csharp/..., //fsharp/..., //third_party/dotnet/... nuget
//examples/adopt-csharp/..., //examples/adopt-fsharp/... nuget
//go/..., //third_party/go/... go
//examples/adopt-go/... go
//powershell/..., //third_party/powershell/..., //examples/adopt-powershell/... powershell
//ruby/..., //third_party/ruby/..., //examples/adopt-ruby/... ruby
//java/..., //kotlin/..., //scala/..., //third_party/jvm/... maven
//examples/adopt-java/..., //examples/adopt-kotlin/..., //examples/adopt-scala/... maven
//javascript/..., //typescript/..., //astro/..., //svelte/..., //vue/..., //mdx/... npm
//quality/tools/javascript/... npm-tools
//examples/adopt-js-ts/... npm-adopt
//examples/adopt-polyglot/... npm-adopt-polyglot
//examples/adopt-polyglot/... uv-adopt-polyglot
//python/..., //python/tests/fixtures/hello/... uv
//examples/adopt-python/... uv-adopt
//quality/tools/python/... uv-tools
A scope that owns no set exits 2 and names the audited sets. //... and
MODULE.bazel select every set. The four uv sets resolve but have no
snapshot, so they report no advisory coverage. A path the table does not name,
and that has no path from the table under it, owns no set.
dx security and dx license read pnpm-lock.yaml, package-lock.json, and
yarn.lock for the npm set, and the lockfile of every other set. The ruby
set reads third_party/ruby/Gemfile.lock and
examples/adopt-ruby/Gemfile.lock. The powershell set reads
third_party/powershell/PSGallery.lock.json. Each finding names the lockfile it
was read from.
dx security reads security.toml from the workspace root. Without the file
no finding is exempted. Each [[exception]] needs advisory, package, set,
versions, reason, and expires. schema_version is 1.
[[exception]]
advisory = "GHSA-aaaa-bbbb-cccc"
package = "some-package"
set = "npm"
versions = ">=1.2.0, <2.0.0"
reason = "Reviewed; no reachable code path."
expires = "2027-03-01"
An expires date on or before the audit date fails the run. An exception that
matches no current finding in its own set fails the run, so remove it when the
finding goes away. versions uses the range syntax of the set ecosystem.
An [[uncovered]] entry accepts a set that has no advisory source. It needs
set, reason, and expires. The run stays complete and still lists the set
under no advisory coverage. An expires date on or before the audit date
fails the run.
[[uncovered]]
set = "powershell"
reason = "PowerShell Gallery publishes no advisories."
expires = "2027-04-24"
Advisory freshness and exception windows are judged against today's UTC date. Set
DX_AUDIT_TODAY to YYYY-MM-DD to pin that date and make a run reproducible.
A value that is not ten digits and dashes is ignored.
dx security scans secrets with the pinned @dx_tools//:gitleaks artifact.
When DX_GITLEAKS_BIN holds an absolute path, dx runs that file. When it is
unset, dx resolves the label through Bazel in the workspace. A relative path is
rejected and PATH is never searched. If no artifact is found, the run exits
1 with secrets auditor unavailable and prints this command, which exports
the path in the current shell:
export "$(bazel run @rules_dx//cli/tool_resolve -- --label @dx_tools//:gitleaks --var DX_GITLEAKS_BIN --print)"
dx license does not read it.
A .gitleaks.toml in the workspace root is passed to the scan. It is the only
config the scan reads. Each scan that uses it reports a warning naming the file,
because the config carries no hash pin. A secret finding is an error whose rule
is the Gitleaks rule id, for example gitleaks/aws-key.
dx license reads licenses.toml from the workspace root. Without the file
every scope is distributed and only MIT, Apache-2.0, BSD-2-Clause,
BSD-3-Clause, ISC, and Unlicense pass. schema_version is 1 and an
unknown key fails the run.
[policy] holds blocked, [policy.distributed] holds allow, review, and
deny, and [policy.sets.<set>] adds review ids for one set. An id in two
lists fails the run. [distribution] internal lists label patterns. //...
covers every scope in the main repo, //pkg/... covers pkg and everything
under it, //pkg:all covers the targets in pkg, and any other entry matches
one label. A run is internal only when an entry covers every scope, including
the --here and --affected scopes, and every other run is distributed. An internal scope fails only on blocked, so
review, deny, and unlisted ids pass. A distributed scope fails on all of
them. Its SARIF rule is license/<license> and its message names the package,
version, license, and tier.
MIT, Apache-2.0, BSD-2-Clause, and BSD-3-Clause also need their text.
Without it a distributed scope fails with the SARIF rule
license/missing-notice-text.
schema_version = 1
[policy]
blocked = ["AGPL-3.0-only", "SSPL-1.0"]
[policy.distributed]
allow = [
"MIT",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Unlicense",
]
review = ["LGPL-3.0-only", "MPL-2.0"]
deny = ["GPL-3.0-only", "GPL-3.0-or-later"]
[policy.sets.ruby]
review = ["Ruby"]
[distribution]
internal = ["//..."]
[[inventory]]
package = "some-gem"
set = "ruby"
license = "MIT"
versions = ">=1.0.0, <3.0.0"
text_present = true
Each [[inventory]] needs package, set, license, and versions, and
text_present says the license text ships with the package. It overrides the
license the cargo and npm sets read from cargo-bazel-lock.json and
package-lock.json, and it is the only license source for every other set, so
a package with no entry is UNKNOWN.
Each [[exception]] needs package, set, license, versions, reason,
and expires, and covers a finding only when package, set, and license
all match. An expires date on or before the audit date fails the run. An
exception that matches no finding in its own set fails the run too. Exceptions
for sets outside the run are still checked for fields and expiry, then skipped.
versions uses the range syntax of
the set ecosystem.
Output: --output text|json. Text prints one line per finding:
<path>: <severity>: <message> [<tool>/<rule>]. The lines go to stderr when a
report goes to stdout. JSON emits one diagnostic event per finding with
tool, rule, path, severity, and message. A vulnerability has tool
vuln, rule vuln/<advisory>, the lockfile as path, and a message that
names <package>@<version>, the advisory, its severity, and the fixed
version. Its package has name, version, and fixed, the list of fixed
versions, empty when none exists. A license finding has tool license, rule
license/<license>, a message that starts with
<package>@<version> license <license>, and a package with name,
version, and license. A failed family emits an error event. Its code is advisory_refresh_failed for a
missing, stale, or corrupt advisory snapshot, offline_required when
--offline blocks a refresh, and audit_failed for findings or any other
incomplete assessment. Reports: dx security writes
--report sarif=<dest>. dx license writes --report sarif=<dest> or
--report spdx=<dest>. Repeat the flag for more files. Use - for stdout.
A relative destination anchors at the workspace root; an absolute one writes as
given. Colliding destinations fail before anything runs, parents must exist, and
a failed write keeps the previous file bytes.
bazel run @rules_dx//:dx -- security //...
bazel run @rules_dx//:dx -- license --fail-on error //...
--offline forbids network: the run uses pinned cached snapshots and fails
with offline_required when they are missing. --frozen forbids manifest
and lock resolution changes while still allowing already pinned fetches.
Combine both flags for both policies. Exit codes: 0 success, 2 usage or scope
errors, 1 findings at or above --fail-on, an incomplete assessment, or an
operational failure.
dx update
dx update [--check] [--apply] [--offline] [--frozen] [selector ...]
Updates dependencies per set through the qualified resolvers. Checks by
default; --apply writes updates. Sets: cargo, go, maven, npm,
npm-adopt, npm-adopt-polyglot, npm-tools, nuget, powershell, ruby, uv,
uv-adopt, uv-adopt-polyglot, uv-tools. Selectors are set, set:package,
or a label/path. go, powershell, and ruby pins are manual: update
reports them pinned and changes nothing. ruby pins come from bundle lock
on the seed host, powershell pins are hand-written in
third_party/powershell/PSGallery.lock.json, and go pins track Gazelle and
widen through dx bump with a go selector.
--check validates the selected sets without writing. The uv set runs a
read-only lockfile check and reports current, and a stale lock reports failed.
go, powershell, and ruby report pinned. Every other resolvable set reports
unsupported and fails the run; refresh those with dx update --apply <set>.
No selector checks all sets, so a bare dx update reports the unsupported sets
and exits 1. Checks never take a build slot.
--apply takes one only for cargo, maven, npm, npm-tools and nuget,
whose updaters run Bazel. update never touches the Bazelrc
preset fragment: verify it with
bazel run //tools/bazelrc:preset_update -- --verify-only and regenerate it
with bazel run //tools/bazelrc:preset_update.
The preset sets these resource defaults:
startup --host_jvm_args=-XX:MaxHeapFreeRatio=30and-XX:MinHeapFreeRatio=10: idle servers return heap to the OS.startup --host_jvm_args=-XX:MaxRAMPercentage=25: the server heap is up to 25% of RAM, or of the cgroup limit when there is one.common --local_resources=memory=HOST_RAM*.5andcommon --local_resources=cpu=HOST_CPUS*.75: actions use half the RAM and three quarters of the cores.startup --max_idle_secs=900: a server shuts down after 15 minutes idle.startup --shutdown_on_low_sys_mem: an idle server shuts down when system memory runs low. It needs--max_idle_secs.common --experimental_disk_cache_gc_max_age=14d: prunes disk cache entries older than 14 days. It only applies when a disk cache is set.common --experimental_disk_cache_gc_max_size=20G: caps the disk cache at20G. It only applies when a disk cache is set.common --repo_contents_cache_gc_max_age=60d: keeps repo contents for 60 days.dx worktree prunecleans the downloads in the repository cache.build --experimental_shrink_worker_poolandbuild --experimental_total_worker_memory_limit_mb=4096: cap worker memory per server.
Override any of them in user.bazelrc.
--offline forbids network: the run uses pinned cached snapshots and fails
with offline_required when they are missing. --frozen forbids manifest
and lock resolution changes: --apply fails with frozen_locked when a
resolver would rewrite them. --check writes nothing, so --frozen does not
change it. Combine both flags for both policies.
--fail-on, --report, and Bazel options do not apply.
--bazel-startup-option applies to the cargo, maven, npm, npm-tools, and
nuget sets, which run Bazel.
Output: --output text|json. json emits command_started, then per set
either one notice (update_set_success, update_set_current,
update_set_pinned, or update_set_blocked) or one error (update_failed,
update_set_unsupported, offline_required, frozen_locked, or
backend_unavailable). An applied set also emits one change event per edited
file, before its success notice. A failed run then emits an update_recovery
notice, and every run ends with command_finished. Text prints
dx: <code>: <message> on stderr for every failure plus a summary count of updated, current, pinned, unsupported, failed, and blocked sets.
A failed run prints an update_recovery notice with the rerun command. Text
goes to stderr; JSON is a notice whose scope lists the sets to retry. After
--apply the command is dx update --apply <sets>, plus
git checkout -- <locks> to discard kept successes. After --check it is
dx update <sets>. In a consumer workspace the restore command uses the lock
paths declared in dx.toml.
Exit codes: 0 success, 2 usage or scope errors, 1 when any set is unsupported,
failed, or blocked, or on an operational failure.
bazel run @rules_dx//:dx -- update --check
bazel run @rules_dx//:dx -- update --apply go
bazel run @rules_dx//:dx -- update --apply ruby
bazel run @rules_dx//:dx -- update --apply powershell
bazel run @rules_dx//:dx -- update --apply uv uv-tools
bazel run @rules_dx//:dx -- update --apply npm-tools
bazel run @rules_dx//:dx -- update --apply npm-adopt npm-adopt-polyglot uv-adopt uv-adopt-polyglot
dx bump
dx bump [--apply] [--offline] [--frozen] <set:package> <version>
Widens one declared requirement to a new version. Checks by default;
--apply widens. Without --apply, bump exits 0 when the manifest already
declares the version and exits 1 with bump drift when a widen is pending.
Takes exactly one
set:package plus one version. Sets: bazel, cargo, github-actions,
go, maven, npm, nuget. The package must already be declared in the
manifest, or the run fails without writing. A bazel bump rewrites the one
literal version of the matching top-level bazel_dep in MODULE.bazel
and keeps every other byte. Duplicate declarations, a missing or computed
version or name, and a MODULE.bazel that does not parse fail without
writing. A github-actions version is
the resolved commit SHA, never a tag, and the pin is rewritten in every
.github/workflows file that declares the action.
A configured bazel set from dx.toml is selected as <set>:<module>. It
rewrites the one literal version of that bazel_dep in the set's own
MODULE.bazel. A read-only set and <set>:.bazelversion fail without
writing. Configured uv and npm sets fail with bump is not supported.
cargo, go, maven, npm, and nuget refresh automatically through
dx update --apply <set>. A failed refresh keeps the widen and exits 1. bazel
and github-actions are file-only and refresh nothing. Review the pin diff
and run bazel build //....
A set that needs a refresh under --offline fails before widening with
offline_required. --frozen always fails before widening with
frozen_locked, because a bump changes resolution by definition. A major
bump also needs dx migrate --from <old> --to <new>.
--check, --fail-on, --report, and Bazel options do not apply.
--bazel-startup-option applies to the Bazel run that refreshes the lock.
Output: --output text|json. Exit codes: 0 success, 2 usage or scope errors,
1 a pending widen without --apply or an operational failure.
bazel run @rules_dx//:dx -- bump --apply cargo:anyhow 1.0.100
bazel run @rules_dx//:dx -- bump --apply go:github.com/google/go-cmp 0.7.0
bazel run @rules_dx//:dx -- bump --apply bazel-modules:rules_cc 0.3.0
dx add
dx add [--apply] [--offline] [--frozen] <set:package> <version>
Declares one new requirement at an exact version. Checks by default;
--apply adds. Without --apply, add exits 1 with add pending and
writes nothing. Takes exactly one set:package plus one version, never a
batch. Sets: bazel, cargo, go, maven, npm, nuget. The package
must not be declared yet: a declared package fails in both modes and names
the dx bump command that changes its version.
Each set writes the same manifest dx bump widens and keeps every other byte:
bazel: abazel_depafter the last top-levelbazel_depinMODULE.bazel.cargo: a[dependencies]entry, kept sorted when the table already is.go: arequireline in the first directrequireblock ofgo.mod.maven: a coordinate at the end of theartifactsof themaven.installthat feeds the default@mavenrepository.npm: adependenciesentry inpackage.json.nuget: anugetline in the main group ofpaket.dependencies.
A configured bazel set from dx.toml is selected as <set>:<module> and
adds to the set's own MODULE.bazel. A read-only set fails without writing.
Configured uv and npm sets fail with add is not supported.
github-actions and bazel:.bazelversion are usage errors: add the uses:
step by hand, or change the pin with dx bump.
cargo, maven, npm, and nuget refresh automatically through
dx update --apply <set>. go refreshes as a no-op and does not update
go.sum. bazel is file-only. A failed refresh keeps the add and exits 1.
A set that needs a refresh under --offline fails before writing with
offline_required. --frozen always fails before writing with
frozen_locked.
--check, --fail-on, --report, and Bazel options do not apply.
--bazel-startup-option applies to the Bazel run that refreshes the lock.
Output: --output text|json. Exit codes: 0 success, 2 usage or scope errors,
1 a pending add without --apply, a declared package, or an operational
failure.
bazel run @rules_dx//:dx -- add --apply cargo:serde 1.0.200
bazel run @rules_dx//:dx -- add --apply npm:vue 3.5.42
bazel run @rules_dx//:dx -- add --apply bazel-modules:rules_go 0.50.0
Consumer dependency sets (dx.toml)
A workspace outside rules_dx declares its own dependency sets in a
committed dx.toml at the workspace root. When dx.toml declares at least
one [[dependency_set]], update, security, license, and bump resolve
names and scopes from it instead of the built-in sets. No built-in set is
selected. A dx.toml with only [dx] or [quality] tables keeps the built-in
sets. A dx.toml with neither sets nor those tables fails with exit 2.
schema_version = 1
[[dependency_set]]
name = "frontend"
ecosystem = "uv"
manifests = ["apps/frontend/pyproject.toml"]
locks = ["apps/frontend/uv.lock"]
scopes = ["apps/frontend"]
Each record names one set: a stable name, an ecosystem backend kind
(uv, npm, or bazel), workspace-relative manifests, locks, and
owning scopes. Manifests are named for the ecosystem: pyproject.toml for
uv, package.json for npm, MODULE.bazel for bazel; any other name
fails with exit 2. Two sets may share one ecosystem. Paths stay inside the workspace: absolute
paths, .., and \ fail. A manifest or lock whose symlink resolves outside
the workspace fails security and license for that set with exit 1.
Two writable sets cannot own the same manifest or lock; mark shared read-only ownership with writable = false.
schema_version must be 1. Unknown keys, unknown ecosystems, duplicate
names, empty records, and manifests spread across directories fail with
exit 2 before anything runs.
A bazel set audits the resolved Bazel module graph. Point manifests at
MODULE.bazel and locks at MODULE.bazel.lock.
[[dependency_set]]
name = "bazel-modules"
ecosystem = "bazel"
manifests = ["MODULE.bazel"]
locks = ["MODULE.bazel.lock"]
scopes = ["."]
dx security and dx license list every selected module from the lock's
source.json entries, plus archive_override, git_override,
local_path_override, and literal http_archive, http_file, and
http_jar calls in MODULE.bazel and the files it pulls in with include.
Downloads through any other use_repo_rule, such as git_repository, are
listed as repo_rule inputs named by commit, sha256, or tag, and
unpinned when none is set.
Each input is named by registry and source.json digest, archive sha256
or integrity, git commit, or local path. Modules that lost version selection
are not listed.
Repos that module extensions record under moduleExtensions in
MODULE.bazel.lock, from any module in the graph, are listed as
module_extension inputs under their repo name. Each is named by sha256,
integrity, or commit, and unpinned when none is set. Generated repos with
no URL are skipped. Extensions marked reproducible are not in the lock, so
their downloads are not listed.
No advisory source maps Bazel modules. A run with only bazel sets exits
0 with clean; no advisory coverage for <name> and an info diagnostic with
the input counts per source, including the module extension count. Read the diagnostic: nothing was checked for advisories. A missing or unreadable lock, a
bazel_dep absent from the lock, a declared version above the selected one,
an override still resolved from a registry, a git_override without a
commit, an include that is not a // label naming a .MODULE.bazel file,
cannot be read, or points outside the workspace,
an archive without sha256 or integrity, a computed module_name, a
malformed sha256 or integrity on an extension repo, and two extensions
that generate one repo at the same version from different URLs fail the set with failed to assess <name> and exit 1.
dx license reports UNKNOWN for every input unless licenses.toml
declares it with [[inventory]] and set = "bazel". The SPDX report carries
each input's origin as an external reference. A module that two bazel
sets share is listed once with every origin.
[[inventory]]
package = "rules_cc"
set = "bazel"
license = "Apache-2.0"
versions = "0.2.1"
text_present = true
dx update --check reports a bazel set as pinned. dx update --apply
changes nothing; change a pin with dx bump <set>:<module> <version> and
refresh the lock with bazel mod deps.
An npm set locks one package.json with the workspace's own pnpm. The
root MODULE.bazel must load it:
bazel_dep(name = "aspect_rules_js", version = "3.5.2")
pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm")
use_repo(pnpm, "pnpm")
[[dependency_set]]
name = "web"
ecosystem = "npm"
manifests = ["apps/web/package.json"]
locks = ["apps/web/pnpm-lock.yaml"]
scopes = ["apps/web"]
dx update --apply runs
bazel run @pnpm//:pnpm -- --dir <absolute set directory> install --lockfile-only
from the workspace root and writes that set's pnpm-lock.yaml only; no
node_modules is created. set:package selectors such as
web:@types/node run update <packages> --lockfile-only instead.
dx update --check runs install --lockfile-only --frozen-lockfile, writes
nothing, and fails the set with updater exited <code> when the lock is stale.
--check --offline adds pnpm --offline and Bazel
--repository_disable_download. --apply --offline fails with
offline_required, --apply --frozen fails with frozen_locked, and
--check with a set:package selector is unsupported. --bazel-startup-option
values reach the bazel call. pnpm runs with npm_config_globalconfig and
npm_config_userconfig set to /dev/null (NUL on Windows), so user and
global .npmrc files are ignored; a .npmrc in the set directory still
applies. The set is unsupported before pnpm starts when locks does not list
<set directory>/pnpm-lock.yaml or when a parent directory inside the
workspace holds a pnpm-workspace.yaml.
dx security matches every pnpm-lock.yaml package against
.dx/advisory/npm.json; a missing snapshot fails the set with
failed to assess <name>. dx license reports UNKNOWN for npm packages
unless licenses.toml declares them with [[inventory]] and set = "npm".
Selectors are set names (frontend), set:package (frontend:anyio),
or scopes (apps/frontend, //apps/frontend/..., //...). No selector
selects every configured set. A scope selects every set whose scope covers
it; overlapping scopes select every owner and the run names them all.
Unknown names and unowned paths fail with exit 2.
dx update and dx update --check run the pinned uv 0.12.23. Set
DX_UV_BIN to the absolute path of the @dx_tools//:uv artifact and
DX_UV_PYTHON to the absolute path of the @rules_dx//cli/update:python_interpreter
interpreter. A relative path is rejected and PATH is never searched. A uv
that does not report 0.12.23 is rejected. Without a usable DX_UV_BIN or
DX_UV_PYTHON the set fails with backend_unavailable and the run exits 1;
JSON reports an error event with that code. It is never reported current. In CI, bazel run @rules_dx//cli/tool_resolve -- --label @dx_tools//:uv --var DX_UV_BIN exports the uv path and the same
command with --label @rules_dx//cli/update:python_interpreter --var DX_UV_PYTHON exports the interpreter.
dx update --apply runs uv lock in the set directory and writes that set's
lock only. dx update --check runs uv lock --check, writes nothing, and
fails the set with updater exited 1 when the lock is stale. Both pass
--no-config --no-cache --no-python-downloads --python <DX_UV_PYTHON> and
start uv with an empty environment, so HOME, XDG_CONFIG_HOME, UV_*
variables, and user uv.toml files are ignored. --check --offline adds
--offline; a lock that needs a download to verify then fails the set with
updater exited 2. --apply --offline fails with offline_required and
--apply --frozen fails with frozen_locked; --check --frozen runs.
A uv.toml in the set directory or any parent inside the workspace, or a
[tool.uv] key other than sources, workspace, dev-dependencies,
default-groups, conflicts, package, managed, override-dependencies,
or constraint-dependencies, makes the set unsupported before uv starts.
Selective set:package updates are unsupported for uv: the set refreshes
as a whole. Updating a read-only set fails; checking one works.
dx security and dx license assess the configured locks. dx security
matches every uv.lock package, direct and transitive, against
.dx/advisory/pypi.json, using PEP 440 versions and PEP 503 names. Workspace
members are skipped. Packages from git, path, url, directory, or a registry
other than pypi.org are reported as unassessed and the run exits 1, as does
an unparseable locked version. A missing lock fails the family. A stale,
missing, or failed advisory snapshot fails the set with
failed to assess <name>, and --offline fails with offline_required when
the snapshot is missing. A security.toml exception uses set = "<name>" and a
PEP 440 versions range. dx bump refuses configured uv and npm sets;
configured bazel sets widen as described under dx bump.
bazel run @rules_dx//:dx -- update --check frontend
bazel run @rules_dx//:dx -- update --apply worker
bazel run @rules_dx//:dx -- update --apply web:@types/node
bazel run @rules_dx//:dx -- security apps/frontend
bazel run @rules_dx//:dx -- license --output=json
Environment And Codegen Commands
bazel run @rules_dx//:dx -- env
bazel run @rules_dx//:dx -- codegen
dx env [--check] [--apply] [<label>] [-- bazel-options...]
dx codegen [--check] [--apply] [<label>] [-- bazel-options...]
dx env collects the managed tools and env values. It upgrades nothing.
dx codegen materializes declared generated source. Both are
repository-wide by default.
With one exact // or @ label, dx env acts on that target only.
dx codegen <label> builds that label plus every codegen shard that
depends on it. For example, dx codegen //proto:api also builds each
codegen shard that depends on //proto:api. dx finds the shards in the
workspace rule graph (bazel query "kind('rule', //...)") and does not
load external repos.
A pattern, a relative label, a path, an empty value or a second label is
a usage error and exits 2.
Both check the intended selection by default and report drift without
writing. Pass --apply to select it. A missing selection is drift, not
success. Checking never selects a new generation, populates managed
bins, or commits records.
Both commands also pass
--@rules_dx//config:validate=false, --@rules_dx//config:clippy_output_diagnostics=true
and --@rules_dx//config:rustc_output_diagnostics=true. Another value for one of them,
or for the matching @rules_rust//rust/settings flag, fails before Bazel starts with exit 2.
These args fail the same way: --aspects, --output_groups,
--remote_download_minimal, --remote_download_outputs=minimal,
--build_event_json_file, any --build_event_json_file_path_conversion other
than false, --test_arg, and the startup options --bazelrc, --home_rc,
--nohome_rc, --system_rc, --nosystem_rc, --output_base,
--output_user_root, --host_jvm_args and --server_jvm_out.
Pass --output_base or --output_user_root with
--bazel-startup-option instead.
They pass --remote_download_outputs=toplevel after your options, so a --config
cannot stop the plan outputs from downloading. Pass --remote_download_outputs=all
or --remote_download_all to keep downloading all outputs.
These commands keep managed state under .dx/, which is gitignored.
dx clean reports unselected managed state. dx clean --apply prunes it.
They take no --fail-on or --report.
To run both, run env first, then codegen. Stop on the first failure.
bazel run @rules_dx//:dx -- env --apply
bazel run @rules_dx//:dx -- codegen --apply
Output: --output text|json.
Text output starts with Running <command> for <roots>. Then dx prints
one result line, where <pair> is (environment <id>, generated <id>):
- Up to date:
dx <command>: already selected setup <setup> <pair>. - Drift with a selection:
dx <command>: stale selection: current setup <setup> want setup <setup> <pair>. - Drift with no selection:
dx <command>: no current selection: intended setup <setup> <pair>. - With
--apply:dx <command>: selected setup <setup> <pair>, oralready selected setupwhen nothing changed.
Drift adds run `dx <command> --apply [<label>]` to select it.
--quiet hides all of these lines, drift included. The exit status stays
the same.
JSON output writes these events in order:
command_started, with modecheckordefault(--apply).operation, with phasecollect. Itsscopeis the label, or the codegen roots after expansion. The whole repository has noscope.- On drift, a
noticewith codemanaged_drift. - With
--apply, aselectionwithsetup,environmentandgenerated. - When Bazel fails, an
errorwith codebazel_failedand phasecollect. - Last,
command_finished.
Other failures write an error with its code, then command_finished.
Codes include invalid_arguments, invalid_result, no_capability,
unreadable_bep and managed_commit_failed.
Exit codes: 0 success. 2 usage or scope errors: a bad scope, a
conflicting validate or diagnostics flag, a rejected arg after --
(--aspects, --output_groups, --build_event_json_file, --test_arg or a
startup option), or a failed codegen
label query. The JSON error keeps the query status. 1 drift, an
invalid collection result (invalid_result), no capability
(no_capability), a non-UTF-8 temporary event path (unreadable_bep),
a commit failure (managed_commit_failed), or a Bazel launch failure.
Bazel failures keep Bazel's code.
bazel run @rules_dx//:dx -- env //a:one
bazel run @rules_dx//:dx -- codegen //proto:api
bazel run @rules_dx//:dx -- codegen --apply
bazel run @rules_dx//:dx -- env --apply
dx init
bazel run @rules_dx//:dx -- init
bazel run @rules_dx//:dx -- init --apply
bazel run @rules_dx//:dx -- init --apply my_module
tools/dx init
dx init
dx init [--apply] [module-name]
Adopts dx into an existing tree. Use dx new to start a new project. Checks by default; --apply scaffolds. Takes an optional module name, which
defaults to my_project. The module name starts with [a-z0-9] and uses
[a-z0-9._-] only. An invalid module name fails before any file is written.
Writes each file only when it is missing. The exceptions are the marked
block in AGENTS.md and stale launchers (see below). It writes .dx/version, .gitignore, .bazelignore, .envrc, .devcontainer/devcontainer.json,
.vscode/settings.json, .vscode/extensions.json,
.github/workflows/ci.yml, MODULE.bazel.snippet, the launcher
(tools/dx and tools/dx.cmd), and .claude/settings.json
with the dx worktree hooks. The hooks run tools/dx, which also runs in a Git Bash or MSYS2 shell on Windows. It also writes a marked block to AGENTS.md,
creating the file when it is missing (see below). If .claude/settings.json
already exists, it is left alone and the missing hook entries are printed to
stderr. The CI caller pins the
rules_dx reusable workflow at a reviewed commit and passes
rules_dx_version matching the snippet. When the tree has .cu or .cuh files, the snippet also carries the pinned CUDA toolkit and host-compiler stanza and the opt-in cuda-eula license line (see dx new cuda). The .gitignore lists target/, /bazel-*, node_modules/,
__pycache__/, *.pyc, .gradle/, /build/, /.dx/ide/ and
/.vscode/.rules_rust_analyzer/. The .bazelignore lists .dx, so Bazel skips
the IDE output bases. When a .bazelignore already exists it is left alone, and
dx init --apply prints dx: .bazelignore lacks .dx; add it so Bazel skips the IDE output base
to stderr if it does not list .dx. Editor settings point at the managed
interpreter and tools with host-native names. For Rust they set up
rust-analyzer with the rules_rust discover and flycheck targets. Both run
with bazel run on separate output bases under .dx/ide/, and the file
watcher and search exclude bazel-* and .dx/ide/. They configure language servers, not debuggers. See
Supported Surface.
Flags: --apply.
Scopes: optional single module name.
Output: text only. Check mode looks for .dx/version,
.devcontainer/devcontainer.json, .gitignore, .bazelignore, .envrc,
.vscode/settings.json, .vscode/extensions.json,
.github/workflows/ci.yml, MODULE.bazel.snippet, tools/dx and
tools/dx.cmd. It does not inspect .claude/settings.json or AGENTS.md.
It also flags a dx-written launcher whose version differs from .dx/version,
and an empty or non-semver .dx/version.
When all of them exist, no launcher is stale and the pin is valid, it prints
init current: <module> to stdout. Otherwise it prints
dx: init_failed: init drift: missing <paths>; stale <paths>; invalid .dx/version ("<pin>")
to stderr (only the parts that apply), followed by the dx init <module> --apply
command to run. An invalid pin is not fixed by apply: write the module version to
.dx/version, which the message names.
Apply prints wrote <path> to stdout for each file it creates or refreshes,
and wrote AGENTS.md when the block changes. For each scaffold or launcher file
that already exists and is kept, it prints dx: <path> (absent-only, left untouched) to
stderr. --quiet hides the stdout lines. The stderr lines still print.
Exit codes: 0 check passed or apply succeeded, 2 usage error, including
extra positionals or an invalid module name, 1 check found missing files or
apply failed.
Agent commands block
dx init --apply writes this block between <!-- dx:begin --> and
<!-- dx:end --> markers in AGENTS.md:
<!-- dx:begin -->
- Start: `tools/dx worktree add <branch>`. Finish: `tools/dx worktree release`.
- Fix first: `tools/dx fix --apply --affected`.
- Then verify: `tools/dx test --affected`.
- Read the `--output=json` result.
<!-- dx:end -->
An existing file gets the block appended. A block that is out of date is
replaced. Text outside the markers is never touched, and CRLF files keep CRLF.
A begin marker without an end marker is an error and the file is left as is.
dx status reports the agents check as warn when the block is missing or
out of date, or a hook is missing from .claude/settings.json.
Launcher
tools/dx (sh) and tools/dx.cmd run dx without Bazel. They read .dx/version,
download the matching standalone binary and its .sha256 file from the GitHub
release into ~/.cache/dx/bin/<version>/ (%USERPROFILE%\.cache\dx\bin\<version>\
on Windows), check the binary against that sha256, and run it. A cached binary
is only run. No Bazel server starts.
tools/dx version
tools/dx check //...
Binaries exist for Linux x86_64 and arm64, macOS arm64, and Windows x86_64 and
arm64. The download launcher exits 1 and prints the bazel run @rules_dx//:dx -- ...
command to use instead when the download fails or the platform has no binary.
A binary that does not match its sha256 is deleted and never run.
When .dx/version differs from the launcher's version, the launcher exits 1
and prints:
dx: tools/dx is pinned to dx <v> but .dx/version is '<pin>'; refresh it: bazel run @rules_dx//:dx -- init --apply
dx init --apply writes a missing launcher and rewrites one whose VERSION
line differs from .dx/version. With no .dx/version it uses the running dx
version. dx treats a launcher as its own when it starts with the template's
first three lines (#!/bin/sh, set -eu, VERSION="<v>", or @echo off,
setlocal, set "VERSION=<v>"). Any other launcher is yours and is never
touched. A dx launcher is rewritten when stale, so hand edits to it are lost.
When a launcher would change and .dx/version is not semver, init fails with
init_failed before writing any file. dx rollback --apply refreshes existing launchers the
same way.
Git hooks
dx does not install or manage Git hooks. For a commit or push hook, write a plain hook or use a hook tool, and have it run:
bazel run @rules_dx//:dx -- check //...
Enforce the same check in CI. See GitHub CI.
dx migrate
dx migrate [--apply] --from <version> --to <version> [scope...]
Rewrites breaking changes between releases. Checks by default; --apply
migrates. Both versions are Cargo semver.
Target must be newer than source. A major dx bump names dx migrate.
No manifests exist yet, so check and apply fail closed.
Flags: --from <version> --to <version>, --apply.
--check, --fail-on, --report, --offline, --frozen, --here,
--affected, and Bazel options do not apply.
Scopes: explicit Bazel labels/patterns or workspace-relative files/dirs.
No scope selects //.... Scopes are accepted but not read until a migration
ships.
Output: --output text|json. Text prints the failure on stderr:
dx: migrate_failed: no qualified migration <from> -> <to>: manifest <manifest> is not shipped.
The manifest is migrate-vA-to-vB.json for a major bump and
migrate-v<from>-to-v<to>.json for a minor or patch bump. -v first prints
Would migrate <from> -> <to> via <manifest> on stdout. json emits
command_started (mode check, or default with --apply), one error
event (migrate_failed), and command_finished. A usage error prints
dx: <message> and the usage line on stderr; json adds an error event
(invalid_arguments) and command_finished.
Reports: none. --report is a usage error.
Exit codes: 0 success, which no run reaches until a migration ships. 2
usage errors: missing, empty, or non-semver versions, a target that is not
newer (including equal versions), --report, and flags or Bazel options that
do not apply. 1 migrate_failed, returned by every valid upgrade today.
bazel run @rules_dx//:dx -- migrate --from 1.0.0 --to 2.0.0
bazel run @rules_dx//:dx -- migrate --from 1.0.0 --to 2.0.0 --output=json
dx new, dx upgrade And dx rollback
bazel run @rules_dx//:dx -- new --apply rust my_project
bazel run @rules_dx//:dx -- upgrade --from 1.0.0 --to 2.0.0
bazel run @rules_dx//:dx -- rollback
dx new
dx new [--apply] <language> [name]
Starts a new project. Use dx init to adopt an existing tree. Checks by default; --apply scaffolds. Scaffolds a minimal project for one language: rust, python,
javascript, typescript, go, java, kotlin, scala, csharp,
fsharp, c, cpp, rust-web, ruby, powershell, astro, svelte, vue, mdx, zig, elixir, gleam, erlang, or cuda. Never overwrites existing files.
There is no --force. Runs outside a workspace.
c scaffolds a standalone C consumer and cpp a standalone C++ consumer.
Both build with the managed LLVM toolchain.
Removed spellings fail with exit 2 and name the replacement:
ccis nowcpp.c#andc-sharpare nowcsharp.f#andf-sharpare nowfsharp.
Every starter also writes .dx/version, .devcontainer/devcontainer.json,
.gitignore, .bazelignore, .envrc, .vscode/settings.json, .vscode/extensions.json,
.github/workflows/ci.yml and MODULE.bazel.snippet. Check mode reports
them when they are missing.
Check mode prints this and exits 0 when every file exists:
new current: <language> <name>
When files are missing it fails with new_failed on stderr and exits 1:
dx: new_failed: new drift: missing <files> (run `dx new <language> <name> --apply`)
--apply prints one line per file it writes, and one stderr line per file
that already exists:
wrote <path>
dx: <path> (absent-only, left untouched)
Output: text only.
Exit codes: 0 success, 2 usage errors including an unknown language, an
invalid destination or package identity, or extra positionals, 1 missing
files in check mode or scaffolding failed.
The [name] is the destination directory and defaults to my_project.
Destinations stay inside the workspace. Absolute paths, .. segments, empty
segments, backslashes, control characters, and the characters < > : " | ? *
are rejected. Segments never end with a space or ., and reserved device
names such as CON and NUL are rejected. Nested destinations such as
teams/demo are allowed. Spaces and Unicode are allowed in the destination.
The package identity derives from the final destination segment. It is
lowercased. ASCII letters, digits, ., - and _ are kept. Every other run
of characters folds to one -. Leading and trailing -, _ and . are
trimmed. My App scaffolds the my-app package. The derived identity must satisfy the
language rule:
rusttakes Cargo names.pythontakes dotted names.javascripttakes npm names.typescripttakes npm names.gotakes module paths.javatakes Maven artifact ids.kotlintakes Maven artifact ids.scalatakes Maven artifact ids.csharptakes NuGet ids.fsharptakes NuGet ids.ctakes Bazel module names.cpptakes Bazel module names.rust-webtakes Cargo names.rubytakes Bazel module names.powershelltakes Bazel module names.astrotakes Bazel module names.sveltetakes Bazel module names.vuetakes Bazel module names.mdxtakes Bazel module names.zigtakes Bazel module names.elixirtakes Bazel module names.gleamtakes Bazel module names.erlangtakes Bazel module names.cudatakes Bazel module names.
A Bazel module name uses lowercase letters, digits, dots, hyphens and underscores, starts with a letter, and ends with a letter or digit. Rejected names fail before any file is written.
rust-web scaffolds a shared core with native and browser targets:
Cargo.toml, src/lib.rs, src/main.rs, BUILD.bazel, MODULE.bazel,
.bazelversion, and README.md.
dx new ruby, dx new powershell, dx new zig, dx new elixir, dx new gleam And dx new erlang
dx new [--apply] ruby [name]
dx new [--apply] powershell [name]
dx new [--apply] zig [name]
dx new [--apply] elixir [name]
dx new [--apply] gleam [name]
dx new [--apply] erlang [name]
Scaffolds a standalone consumer with a library, a binary and a test:
MODULE.bazel, .bazelversion, .bazelrc, BUILD.bazel, sources, and
README.md. Ruby, PowerShell, Zig, Elixir, Gleam and Erlang come from the rules_dx toolchains, so nothing needs to be
installed. The PowerShell targets are Linux only. The Elixir, Gleam and Erlang targets need Linux x86_64. The Zig library imports as hello.
The Elixir starter is the OTP application hello with sources in lib/, an
escript, and an ExUnit test in test/.
The Gleam starter has the module hello in src/, a gleeunit test in test/,
and a binary, library and test for both Erlang and JavaScript. The JavaScript
targets end in _js and run on Node.js.
The Erlang starter is the OTP application hello in src/, with an escript
and an EUnit test in test/.
bazel run @rules_dx//:dx -- new --apply ruby demo
Build, run and test inside the new project:
bazel build //...
bazel run :demo_bin
bazel test //...
Output: text only.
Exit codes: 0 success, 2 usage errors including an invalid
destination or package identity, 1 missing files in check mode or
scaffolding failed.
Scaffolding into an existing directory adds absent files only and leaves custom files untouched.
dx new cuda
dx new [--apply] cuda [name]
Scaffolds a standalone CUDA consumer: MODULE.bazel, .bazelversion,
.bazelrc, BUILD.bazel, scale.cu, scale.cuh, scale.h, main.cc,
scale_test.cc, and README.md. Linux x86_64 only.
rules_cuda and toolchains_llvm only read their tags from the root module, so
MODULE.bazel carries the pinned CUDA 13.4 toolkit components and the LLVM 22
host compiler for nvcc. No CUDA install is needed to build.
The stanza also pins cuBLAS, cuFFT and cuRAND. Bazel fetches them only when a
target depends on //cuda/components:cublas, :cufft or :curand. A
workspace with an older stanza must copy the stanza again to use them.
The stanza also contains cuda_license.license(accept = ["cuda-eula"]), which
accepts the NVIDIA CUDA Toolkit EULA
for the build. Remove that line to decline; CUDA targets then fail to build.
The test checks host-side logic and launches no kernel, so bazel test //...
passes without a GPU. The binary runs the kernel when a GPU is present.
dx init adds the same stanza to MODULE.bazel.snippet when the workspace has
.cu or .cuh files.
Output: text only.
Exit codes: 0 success, 2 usage errors including an invalid destination or
module name, 1 missing files in check mode or scaffolding failed.
dx new c And dx new cpp
dx new [--apply] c [name]
dx new [--apply] cpp [name]
Scaffolds a standalone consumer with a library, a binary and a test:
MODULE.bazel, .bazelversion, .bazelrc, BUILD.bazel, hello.h,
README.md, and the sources. c writes hello.c, main.c and
hello_test.c and compiles them as C. cpp writes hello.cc, main.cc and
hello_test.cc and compiles them as C++. Both use the standard library only.
The compiler comes from the managed LLVM toolchain, so nothing needs to be
installed. A destination such as 1demo is rejected because the module name
must start with a letter.
bazel run @rules_dx//:dx -- new --apply c demo
Build, run and test inside the new project:
bazel build //...
bazel run :demo_bin
bazel test //...
Output: text only.
Exit codes: 0 success, 2 usage errors including a removed spelling such as
cc, an invalid destination or package identity, 1 missing files in check
mode or scaffolding failed.
Scaffolding into an existing directory adds absent files only and leaves custom files untouched.
Framework Starters
dx new [--apply] astro [name]
dx new [--apply] svelte [name]
dx new [--apply] vue [name]
dx new [--apply] mdx [name]
Scaffolds a standalone consumer with one component, a helper and a test:
MODULE.bazel, .bazelversion, .bazelrc, .bazelignore, .npmrc,
BUILD.bazel, package.json, pnpm-lock.yaml, pnpm-workspace.yaml,
sources, and README.md. The only npm dependency is the framework package, pinned to the version
rules_dx qualifies.
Node comes from the rules_dx toolchain, so nothing needs to be installed.
bazel run @rules_dx//:dx -- new --apply astro demo
Build and test inside the new project:
bazel build //...
bazel test //...
Output: text only.
Exit codes: 0 success, 2 usage errors including an invalid
destination or package identity, 1 missing files in check mode or
scaffolding failed.
Scaffolding into an existing directory adds absent files only and leaves custom files untouched.
dx new rust-web
dx new [--apply] rust-web [name]
Scaffolds a standalone shared-Rust consumer: one core library, one native binary, one unit test suite, one web bundle, and one manual browser test. Versions come from the rules_dx pins. Never overwrites existing files.
bazel run @rules_dx//:dx -- new --apply rust-web demo
Build and test inside the new project:
bazel build //...
bazel test //...
Run the browser test with a local driver:
geckodriver --port 4444 &
bazel test :demo_browser_test --test_env=GECKODRIVER_REMOTE=http://127.0.0.1:4444
Output: text only.
Exit codes: 0 success, 2 usage errors including an invalid
destination or package identity, 1 missing files in check mode or
scaffolding failed.
The browser test is manual. It needs Firefox and geckodriver on a qualified host. Scaffolding into an existing directory adds absent files only and leaves custom files untouched.
dx upgrade
dx upgrade [--apply] --from <version> --to <version>
Runs the pin, migrate, env, and codegen steps in one go with a recovery pointer. A failed step prints the commands to rerun or undo it, with your --workspace and --apply choices kept. Checks by
default; --apply upgrades. Both
versions are required.
No manifests exist yet, so check and apply fail closed and change nothing.
Flags: --from <version> --to <version>, --apply.
Scopes: none.
A failure prints dx: upgrade_failed: <message> on stderr. With
--output=json, an upgrade_failed error event follows command_started,
then command_finished.
Output: --output text|json.
Exit codes: 0 success, 2 usage errors including missing versions or a
target that is not newer, 1 a step failed.
dx rollback
dx rollback [--apply]
Restores the previous recorded pin in .dx/version. Checks by default;
--apply restores the last recorded pin, or removes .dx/version when no
earlier pin was recorded. The check reports the pin it would restore and
exits 1 until you apply. It runs even when the pin and MODULE.bazel
disagree. Refuses without a record, after manual pin edits, or after the
record is deleted. A second --apply undoes the rollback.
Flags: --apply.
Scopes: none.
A pending rollback fails with status_pin_mismatch on stderr and exits 1:
dx: status_pin_mismatch: pin "<v>" can roll back to "<previous>" (run `dx rollback --apply`)
dx: status_pin_mismatch: pin "<v>" has no earlier pin (run `dx rollback --apply` to remove it)
Refusals use the same status_pin_mismatch code. When the recorded previous
pin is the current pin, the check prints pin current: rollback <v> and exits
0. --apply prints one of:
pinned <v> (rollback)
removed pin <v> (rollback)
After restoring a pin, --apply rewrites each existing dx-written launcher
(tools/dx, tools/dx.cmd) whose version differs, and prints
wrote <path> for each one. It never creates a launcher, never touches one
without a VERSION line, and leaves launchers alone when it removes the pin.
A previous pin that is not semver refuses before anything is written when a
launcher would change. If the refresh fails, the pin and its record stay
restored and it refuses with:
dx: status_pin_mismatch: pinned <v> but launchers not refreshed: <error> (run `bazel run @rules_dx//:dx -- init --apply`)
With --output=json, dx streams command_started, one status event named
pin (status ok, detail the pin, hint dx rollback), then
command_finished. Refusals and pending rollbacks emit a
status_pin_mismatch error event instead of the pin event.
Output: --output text|json.
Exit codes: 0 success, 2 usage errors, 1 the pin can roll back or the
rollback was refused.
bazel run @rules_dx//:dx -- rollback
bazel run @rules_dx//:dx -- rollback --apply
Offline Bundle
offline_bundle assembles a vendored bundle for an airgapped host. The
bootstrap_offline tool reads that bundle, installs Bazel from it, and
populates the advisory snapshots under .dx/advisory. It is not a dx
command; the rest of dx runs through @rules_dx//:dx:
bazel run @rules_dx//:dx -- version
The Macro
load("@rules_dx//deploy/offline:offline.bzl", "offline_bundle")
offline_bundle(
name = "offline_demo",
advisory = {"cargo": ":demo/cargo.json"},
launchers = [":demo/bazelisk-linux-amd64"],
)
name: the bundle target and the tree directory it produces.advisory: a dict of advisory set to snapshot label. Sets arecargo,npm,maven,nuget,go,rubygems. Another set fails at load time.launchers: Bazelisk launcher labels. Each file must be named for the host it serves:bazelisk-darwin-amd64,bazelisk-darwin-arm64,bazelisk-linux-amd64,bazelisk-linux-arm64orbazelisk-windows-amd64.exe. Another name fails at load time. Ship every host you install on.
The Output Tree
Build the bundle and read it from bazel-bin/<pkg>/<name>/:
bazel build //deploy/offline:offline_demo
<name>/advisory/<set>.json
<name>/advisory/SHA256SUMS
<name>/bazelisk/<launcher>
<name>/bazelisk/SHA256SUMS
Each SHA256SUMS lists the files next to it as <sha256> <file>.
Installing From A Bundle
bootstrap_offline --bundle DIR [--install-dir DIR] [--workspace DIR]
--bundle DIR: the bundle tree. Required.--install-dir DIR: wherebazel(bazel.exeon Windows) is installed. Defaults to$HOME/.local/bin.--workspace DIR: the workspace that receives.dx/advisory. Defaults to the current directory.-h,--help: print the usage line on stdout and exit 0.
The tool checks every SHA256SUMS, installs the launcher for the host, copies
each snapshot to .dx/advisory/<set>.json, and writes
.dx/advisory/<set>.meta.json with the set, the file:// URL of the snapshot
in the bundle, its sha256, the retrieval date and the workspace path. The URL
is percent-encoded, so any path the host allows is recorded faithfully.
It prints three bootstrap-offline: lines on stdout: the installed launcher
with its sha256, the number of snapshots populated, and a note that the first
Bazel module fetch still needs network once.
Refusals And Exit Codes
- 0: success, or
--help. - 1: a refusal or a bad argument. The reason goes to stderr as
bootstrap-offline: <reason>; a bad argument adds the usage line. Nothing goes to stdout.
Refusals include: a missing or absent --bundle, a bundle without
bazelisk/ or advisory/, an unsupported host, a missing launcher for the
host, a manifest that names a missing file or no files, a checksum mismatch,
an advisory snapshot the manifest does not pin, and an unknown advisory set.
dx status And dx version
bazel run @rules_dx//:dx -- status
bazel run @rules_dx//:dx -- status --output=json
bazel run @rules_dx//:dx -- version
dx status
dx status
Reports toolchain, Bazel, module, host, exec_tools, tools, pin, and agents
health. With --verbose it adds one capability record per dx command. Takes no scopes and no
per-command flags. Runs anywhere with a workspace. It is read-only: it
downloads, installs, repairs, and runs nothing.
Output: --output text|json. --verbose adds the capability records.
Exit codes: 0 all checks pass, 2 usage error, 1 a check failed or
.dx/version is missing.
Failure Explainer
dx status prints one line per check, for example:
toolchain: ok (available: rust 1.98.0 via rules_rust 0.74.0 declared in MODULE.bazel) hint: bazel build //...
Inside a dx worktree slot it adds a lease check that names the holder and branch.
The checks are toolchain, bazel, module, host, exec_tools,
tools, pin, and agents. With a dx launcher there is also launcher.
Inside a worktree slot there is also lease.
toolchain: reads the rust toolchain versions and therules_rustversion declared inMODULE.bazel.okwhen they match the versionsdxis tested with.warnwhenMODULE.bazelis missing, does not parse, declares no rust toolchain, takesrules_rustfrom an override, or declares other versions. It never failsdx status, and it reports whatMODULE.bazeldeclares, not what Bazel resolves.bazel: reads.bazelversionand looks forbazelorbazeliskonPATH.module: compares therules_dxversion inMODULE.bazel, or in a file itinclude()s, with thedxversion. It reportserrorwith the parser message when either cannot be read or parsed. Asingle_version_overrideversion wins overbazel_dep. An override without a version iswarn.host: checks the machine is a v1 execution platform: linux_x86_64, linux_arm64, macos_arm64, or windows_x86_64. The target platform never selects tools. This is not native qualification.exec_tools: reads.bazelrcand the vendored preset.warnwhen--host_compilation_mode=fastbuildis not set forbuildordx_dev, so exec tools build at opt.tools: reads the managed toolsdx env --applyselected under.dx/setups/current.warnwhen none is selected.errorwhen the selection is foreign, its environment orvalues.jsonis missing or corrupt, or a tool link is missing, is not a link, or points at a missing Bazel artifact. Pinned quality tools resolve at build time and are not probed.pin: compares.dx/versionwith the rules_dx module version thisdxwas built from. An empty or invalid (non-semver) pin hintswrite <module version> to .dx/version.launcher: shown only whentools/dxortools/dx.cmdis a dx launcher (seedx init).errorwhen that version differs from.dx/version, with detailtools/dx pins dx <v>, .dx/version is <pin>and hintbazel run @rules_dx//:dx -- init --apply. It failsdx statuswithstatus_check_failed.agents:warnwhenAGENTS.mdhas no dx block or an out-of-date one, or.claude/settings.jsonlacks adx worktreehook.dx init --applywrites it.
ok passes. warn means unknown and does not fail. error fails, and
the hint is the fix, for example dx upgrade --from <version> --to <version>.
The toolchain, bazel, module, host, exec_tools, and tools detail text starts with
available:, unavailable:, unknown:, or not applicable:. Unknown is
never healthy.
With --verbose, text output then prints one capability <name> line per
command, then capability workspace:tools.
--output=json streams command_started, one status event per check with
name, status, detail, and hint, then command_finished. With
--verbose it adds one capability event per command and one for
workspace:tools before command_finished. A capability event
has name, source, availability, detail, flags, outputs,
reports, scope_policy, effect, and passthrough. source is
cli-grammar for commands and workspace-record for workspace:tools.
availability is available, unavailable, or unknown, and
workspace:tools follows the tools check. A grammar record says the
command is accepted, not that its tools are installed.
A failure emits exactly one error event and exits 1. If pin fails, the
event is status_pin_mismatch, even when other checks fail too. Otherwise
any failed check adds status_check_failed, which names the failed checks.
An empty .dx/version fails the pin check.
A missing .dx/version stops dx status with status_pin_mismatch
(read version pin: ...) before any check runs. It prints no check lines.
JSON output has only command_started, error, and command_finished.
dx: status_check_failed: failed checks: module, tools (see hint)
dx version
dx version [--check]
Prints the version. Read-only: it never writes the pin. Bare dx version
also prints the rules_dx dependency the workspace declares, including any
override and its source.
--check: verify the pin without changing it.
To change the pin, run dx upgrade. To restore the previous pin, run
dx rollback.
Flags: --check.
Scopes: none.
Output: --output text|json. Bare text prints one line each:
dx <version>, rules_dx <version>, pin <pin>, and
dependency <dependency>.
Bare JSON emits binary, module, pin, and dependency status events.
pin is warn when the pin differs from the module version. dependency is
warn when MODULE.bazel is missing or unreadable, has no rules_dx
dependency, or declares or overrides a different version. When MODULE.bazel or an included file cannot be parsed, dependency prints unknown (<reason>). Bare dx version
still exits 0 on these warnings.
dx version --check prints version ok: <pin> on stdout. On drift it prints
dx: status_pin_mismatch: version drift: <pin> != <module> on stderr and
exits 1. JSON emits command_started, one pin status event (ok or
error), then command_finished. On drift a status_pin_mismatch error
event comes before command_finished.
A missing .dx/version makes both forms print no version lines and fail
with status_pin_mismatch.
Exit codes: 0 success, 2 usage errors including removed flags, 1 pin
drift under --check or a missing .dx/version.
bazel run @rules_dx//:dx -- version
bazel run @rules_dx//:dx -- version --check
Version Skew
Every command compares the pin in .dx/version with the rules_dx module
version built into dx before it runs. A mismatch is version skew. For
example, MODULE.bazel and .dx/version say 1.2.0 and the binary is
1.3.0, so dx build stops with
version skew: binary 1.3.0 pin 1.2.0 module 1.3.0.
- Runs anyway:
version,status,rollback,completion,worktree. - Warns and runs:
check,security,license,list-tests. - Stops with exit code
1: every other command.
watch answers for the command it
wraps. A missing or empty .dx/version is not skew, so other commands run.
An empty pin fails the pin check in dx status and dx version --check.
A missing file makes dx status and dx version exit 1 with
status_pin_mismatch.
A stop prints dx: version skew: binary <version> pin <pin> module <version>
on stderr and names the fix. Under --output=json it adds a version_skew
error event, then command_finished with exit_code 1 and
results_complete false.
bazel run @rules_dx//:dx -- version --check
bazel run @rules_dx//:dx -- rollback
bazel run @rules_dx//:dx -- build //...
dx watch
dx watch [--apply] [--debug|--release] [flags] <build|test|run|lint|typecheck|format|check|fix> [scope ...] [-- bazel-options|--app-args ...]
Reruns one command when files change. Scope is re-resolved each iteration.
Flags go to the wrapped command and are checked as dx <command> checks
them: dx watch test --release, dx watch lint --report sarif=out.sarif.
A flag the wrapped command rejects fails before watching starts. Short
options work wherever the long option does. Bare lint, typecheck, format, and
fix only check; pass --apply to write on every iteration. Args after --
reach the wrapped command unchanged.
- Wraps only
build,test,run,lint,typecheck,format,check, andfix. Other commands fail withnot watchable. - Takes
--output,--check,--apply,--fail-on,--report,--include-edits,--here,--affected,--debug, and--release, plus the global flags, when the wrapped command takes them. --hereresolves once from the current directory.--affectedre-resolves each iteration.- Local only. Refuses
CI=true. No daemon, cache, or remote execution. - Ignores changes under
.dx/,.git/, andbazel-*inside the workspace. Parent directories with those names do not matter. Also ignores the wrapped command's--reportfiles and their temp files, matched by path relative to the workspace. Edits todx.toml,dx.local.toml,BUILDandMODULE.bazelfiles rerun the command.
Except under run, changes made while the command runs queue one more
iteration. Iterations never overlap. A failing command does not stop the watch.
Each iteration takes a build slot and, on low disk, drops free slots' output
bases first. Watch holds none while it waits for changes. See
Build Slots and Disk Space.
watch run restarts the app on each change. The running app and every
process it started get SIGTERM. Any still running after 10 seconds is
killed. The replacement builds and launches only after the old app is gone.
An app that exits on its own is relaunched on the next change. The app gets
no stdin.
Output: --output text|diff|json, when the wrapped command takes that mode.
Each iteration writes the wrapped command's own output. Watch adds one line
per iteration and one line per trigger, naming the first changed absolute path.
--quiet drops these lines. With --output json, --output diff, or
--report <format>=-, they go to stderr so stdout holds only the wrapped
command's output: under JSON, one event stream per iteration, each ending in
command_finished. For a restarted app, command_finished reports 143.
scope= shows the given scopes, //... when none are given, or --affected.
watch:test:debounce=200ms scope=//... iteration=1
watching (Ctrl-C to stop)
changed /work/repo/src/main.rs (+1 more)
watch:test:debounce=200ms scope=//... iteration=2
stopped
Ctrl-C, SIGTERM or SIGHUP stops the watch. No new iteration starts after the
signal. A running command gets the signal and is awaited first. Removing the
workspace directory stops the watch with watch_failed.
Watch runs until stopped and has no success exit.
Exit codes: 2 usage error, not watchable, or CI=true; 1 operational
failures such as watch_failed; 141 when stdout closes; 129 after
SIGHUP, 130 after Ctrl-C, 143 after SIGTERM.
bazel run @rules_dx//:dx -- watch build //...
bazel run @rules_dx//:dx -- watch test //cli/...
bazel run @rules_dx//:dx -- watch test --affected
bazel run @rules_dx//:dx -- watch lint --output json
bazel run @rules_dx//:dx -- watch run //app:bin
dx worktree
dx worktree add <branch> [--create=<ref>] [--force] [--output=path]
dx worktree release [<path>] [--force]
dx worktree list [--disk]
dx worktree path <branch>
dx worktree prune [--apply] [--bases]
dx worktree hook create|remove|stop|guard
Keeps a small pool of warm checkouts, called slots, so a new worktree reuses
a slot that already has a Bazel server, output base and caches. Slots live at
<cache>/dx/worktrees/<repo>/<n>. <cache> is $XDG_CACHE_HOME, else
$HOME/.cache, else %LOCALAPPDATA%. Lease records live in
<cache>/dx/worktrees/<repo>/.dx/. <repo> is the basename of the main
checkout, or <repo>-2, <repo>-3 when another repository already uses that
name.
A bare dx worktree runs list.
Flags: --create=<ref> (add only), --force (add and release), --disk
(list only), --apply (prune only). A flag on the wrong subcommand exits 2.
Scopes: none. dx worktree takes a subcommand and its arguments.
Reports: none.
--output text|json|path. path works with add only. Text prints the lines
above. add prints dx worktree: <branch> is in slot <n> at <path>. When
add starts a new branch, it first prints
dx worktree: started <branch> from <base>, on stderr with --output=path.
<base> is the --create ref, else origin/HEAD, origin/main, or
origin/master.
release prints dx worktree: released slot <n> at <path>, or
dx worktree: removed <path> for an external worktree. Releasing a slot that
is already free prints dx worktree: slot <n> is already free and exits 0.
JSON reports it as a slot event with slot, action release, and state
free.
--output=json streams command_started, one slot event per slot, and
command_finished. list adds a summary event after the slots. prune
emits slot events with action prune, action prune_base for output
bases, and action prune_downloads for repository cache downloads.
When git fetch origin fails, add still uses local refs. Text prints
dx worktree: cannot fetch origin, using local refs: <error> before the slot
line. JSON emits a notice event with level warning and code fetch_failed
after command_started. hook create prints the same line on stderr.
When the caller already holds a slot on <branch>, add returns that slot
without fetching origin.
When the pool's disk or the disk of a Bazel output root has under 20 GiB and
under 10% free, or under 1 GiB free whatever its size, add and hook create
(after they fetch and pick a slot) and release drop orphaned output bases,
then repository cache downloads unused for 30 days, then free slots' output
bases, each oldest first, and print one of these lines before
the slot line. add and hook create keep the output base of the slot they
lease, so it stays warm for the build that follows.
dx worktree: low disk: <free> free; removed <what>; <free> free nowdx worktree: low disk: <free> free; nothing to reclaimdx worktree: low disk, cannot reclaim output bases: <error>
The first two end with ; run dx worktree prune --apply while disk stays low,
so a pool with nothing to reclaim prints
low disk: <free> free; nothing to reclaim; run dx worktree prune --apply.
All three use the disk_low notice code. JSON emits a notice event with
level warning and code disk_low before the slot event.
Reclaim stops the idle Bazel server of a free slot before it deletes that
slot's output base; a leased slot, one with a build running, and one whose
server will not stop are never touched.
When the pool's disk still has under 1 GiB free after that reclaim and the
slot they picked is a free slot, add and hook create stop that slot's idle
Bazel server and drop its output bases as a last resort. The disk_low line
then ends with
disk full: <free> free; removed the output bases of slot <n>, which this lease reuses; <free> free now.
When the disk is still that full, they refuse before they check anything out, with code disk_full,
exit 1, and the message
disk full: <free> free at <path>; cannot check out <branch>; free disk space with `dx worktree prune --apply --bases` .
A refusal emits no slot event. A branch you already hold still resolves to
its slot.
slot fields: action (add, release, list, path, prune,
prune_base, prune_downloads), path, state, server, and optional
slot, branch, holder, reason, base, disk_bytes, and entries.
base is set on add when it starts a new branch. disk_bytes is set for
prune, prune_base, and prune_downloads events other than kept and
skipped, and for list with --disk. entries is set for
prune_downloads: the number of downloads, and path is the repository
cache's content_addressable directory.
state values:
held:add,path, andlistfor a leased slot.free:releaseandpruneof a slot, andlistfor an unleased slot.reclaimable:listfor a held slot thataddmay take over.external:listandprunefor a worktree outside the pool.removed:releaseof an external worktree.free: alsoprune_basefor the output base of a free slot, with itsslot.kept:prune --applyfor a worktree it skipped, andprune_basefor an orphaned output base it keeps, with itsreason.orphaned:prune_basefor an output base whose workspace is gone.skipped:prunefor a slot, andprune_basefor an output base (with itsslotwhen it belongs to a free slot), thatprune --applycould not delete, with areason(server,held, orbusy).pruneevents in this state have nodisk_bytes.stale:prune_downloadsfor repository cache downloads unused for 30 days.
summary fields: slots, servers, servers_rss_bytes, plus disk_bytes and
disk_cache_bytes with --disk.
Exit codes: 0 success, 2 usage errors (unknown subcommand or hook name,
wrong arguments, an invalid branch name (invalid_branch), a path that is not
a slot (not_a_slot, including a bare release from the main checkout),
a flag on the wrong subcommand, an unknown --create ref,
--create on an existing branch, release of a slot another holder holds
without --force, any other dx command in a slot leased to another
session or in a slot that add is switching, and running outside a git repository, in a bare repository, or where
the main checkout cannot be found), 1 operational failures (a branch already
checked out elsewhere or held by another holder without --force, a dirty or
unpushed slot or external worktree on release without --force, slot_busy
when a dx command runs in the slot that add --force or release wants, even
with --force, a busy pool lock, no_home when no cache directory can be found,
lease_changed when another holder took the slot during the command, path finds
no checkout, prune finds work without --apply, no default branch to start
a new branch from, and any hook create, hook remove, or hook stop
failure), and 128 plus the signal number when Ctrl-C, SIGTERM, or SIGHUP
stops list --disk or prune (for example 143 after SIGTERM), with
stderr dx: cancelled: stopped by signal N.
Subcommands
add <branch> leases a slot, checks out <branch>, and prints where it is.
It picks the slot that last held the branch, then a slot with the same
MODULE.bazel.lock, then the most recently used, whatever the free disk.
When disk is low, under 20 GiB and under 10% free or under 1 GiB free, it then
deletes orphaned output bases, then repository cache downloads unused for 30
days, then the output bases of the other free slots,
least recently used first, until each output base's disk is no longer low. It
keeps the output base of the slot it picked. It reuses a free slot before it creates one. Files
matched by .worktreeinclude in the main checkout are copied in. --create=<ref>
starts a new branch from <ref>. Without it, add uses the local <branch>,
then origin/<branch>, and otherwise starts <branch> from the default branch
(origin/HEAD, else origin/main, else origin/master). --output=path prints
only the path on stdout and writes the dx worktree: ... warning lines to
stderr. Submodule checkouts and --separate-git-dir clones work too.
add refuses when the branch is already checked out in a slot held by another
holder or in a worktree outside the pool, and when --create names a branch
that exists. add returns your own slot for
<branch> as is. add --force hands a slot that holds <branch> for another
holder to you as is, without resetting it.
add takes over a held slot, including one that holds <branch>, only when
its holder is gone, no dx command runs in it, the tree is clean, and no
commit is unpushed. It does not wait. Otherwise it refuses and says which
condition fails. add never hands out, resets, or deletes a slot while a dx
command runs in it, free or held.
add also deletes free slots idle for 30 days.
release [<path>] frees the slot at <path>, or the slot you are in. A
relative <path> is read from the directory you ran dx in, including under
bazel run. It
stops the slot's Bazel server and detaches the checkout. The branch and its
commits stay in the repository. It refuses a dirty tree or unpushed commits
unless you pass --force; uncommitted changes are then wiped by the next add
of that slot. Only the holder can release a held slot, unless it is
reclaimable or you pass --force. Another agent in the same session is
another holder. While a dx command runs in the slot, release exits 1 with
slot_busy, even with --force. When disk is low, release deletes orphaned
output bases, then the output bases of free slots, least recently used first,
until each output base's disk is no longer low.
release <path> on an external worktree of this repository, such as an old
.claude/worktrees/<task> checkout, stops its Bazel server, removes the
checkout and its output base, and keeps the branch. It refuses a dirty tree or
unpushed commits unless you pass --force, which removes it anyway. The main
checkout is never removed.
Each refusal of a subcommand ends with the next step, such as pass --create=<ref>,
run the command again, pass a path from dx worktree list, or
run dx worktree add <branch>.
list prints one line per slot and a totals line:
1 feature-x session 7f3a agent a91c pid 4121 held server - /home/me/.cache/dx/worktrees/app/1
2 (detached) - free no server - /home/me/.cache/dx/worktrees/app/2
3 review-y pid 4180 (gone) held no server - /home/me/.cache/dx/worktrees/app/3
1 bazel server(s) running, 2.1 GiB resident
A holder that has exited shows (gone) after its name.
The columns are slot, branch, holder, state, server, disk use, and path. The
disk column shows - unless you pass --disk, which measures each slot and
its output base and adds the total to the last line, followed by the size of
the shared disk cache (0 B when it does not exist). It is slow on a large
pool.
$ dx worktree list --disk
1 feature-x session 7f3a agent a91c pid 4121 held server 1.2 GiB /home/me/.cache/dx/worktrees/app/1
2 (detached) - free no server 900.0 MiB /home/me/.cache/dx/worktrees/app/2
1 bazel server(s) running, 2.1 GiB resident, 2.1 GiB across slots, 3.4 GiB disk cache
state is held, free, reclaimable (holder gone, and the reclaim rule
above would allow taking it), or external (a worktree outside the pool). A
held slot whose holder is gone but which has uncommitted changes or unpushed
commits shows why after held, such as held (holder gone, commits are not pushed).
path <branch> prints the path of the checkout that holds <branch>, and
fails with exit 1 when none does.
prune lists free slots idle for 30 days, external worktrees that are clean,
fully pushed, not locked, without a running Bazel server, and on a branch that
tracks origin/<branch> and is merged into the default branch, or whose
origin/<branch> is deleted, and Bazel output bases of removed worktrees,
including checkouts removed from .claude/worktrees. It also lists the output
bases of the other free slots whose disk is low, least recently used first, as
output base of free slot <n>, <size>: <dir>. --bases lists the output bases of every free slot, whatever the disk state, least recently used first, except the slots already listed as idle. --bases works with prune only. It also lists the downloads in
Bazel's repository cache that no build used for 30 days. Only the default
cache, cache/repos/v1/content_addressable under each Bazel output root, is
checked. A cache moved with --repository_cache is not cleaned, and the
contents directory next to it is not touched. A deleted
download is fetched again when a build needs it. --apply deletes the idle free slots and stops each slot's
Bazel server and deletes those output bases; the other slots stay. --apply removes the
external worktrees with their output bases and keeps the branches. It deletes nothing without --apply. It exits 1 when something
would be pruned and --apply is absent, and 0 when there is nothing to prune.
Without --apply, text prints one line per item, and a closing line
dx worktree: <size> to reclaim; run dx worktree prune --apply. The item
lines are slot <n> idle <days> days, <size>: <path>,
external worktree <branch>, <size>: <path>,
output base of free slot <n>, <size>: <dir>,
output base of removed <workspace>, <size>: <dir>, and
repository cache downloads unused for 30 days, <n> entries, <size>: <dir>,
one per cache. With --apply the closing
line is dx worktree: pruned <size>, and dx worktree: nothing to prune
replaces everything when nothing qualifies.
--apply finds and deletes everything under the pool lock. It checks each
external worktree again. One that
became locked, got a Bazel server, uncommitted changes, or unpushed commits is
kept and printed as kept external worktree <branch> (<reason>): <path>, with
reason locked, server, dirty, unpushed, or busy (the delete failed and
part of it is still on disk). JSON reports it with state
kept and that reason. It still exits 0.
An output base whose workspace is gone but that a Bazel server still uses is
kept and printed as kept output base of removed <workspace> (server): <dir>.
JSON reports it as prune_base with state kept and reason server.
A free slot's output base whose Bazel server will not stop, whose slot was
taken, or whose slot runs a dx command, is kept and printed as
skipped output base of free slot <n> (<reason>): <dir>, with reason server,
held, or busy. An orphaned output base that cannot be deleted is printed as
skipped output base of removed <workspace> (busy): <dir>; this also covers
the output base of a slot that prune deleted while the base stayed. A slot
that was taken after the plan or is in use is printed as
skipped slot <n> (<reason>): <path>. So is a slot whose checkout is still
partly on disk after the delete; dx worktree add no longer leases it, and the
next prune or add tries to delete it again. Skipped items are not counted in
pruned <size>, and the command still exits 0.
--apply checks each download again right before it deletes it. One that a
build used since the check is kept without a line and is not counted in
pruned <size>.
Shared disk cache
The main checkout and every slot share one Bazel disk cache at
<cache>/dx/disk-cache, so a branch built in one checkout is not rebuilt in
another. dx build, test, coverage, run, and deploy add
--disk_cache=<cache>/dx/disk-cache after the Bazel verb. They skip it when
the workspace .bazelrc, a file it imports, ~/.bazelrc, or the system rc
sets --disk_cache or --nodisk_cache. Under CI=true and inside Bazel
tests no disk cache is added. Turn it off for one run with -- --disk_cache=.
bazel run @rules_dx//:dx -- build //... -- --disk_cache=
Disk cache size
When dx adds the disk cache, it also bounds it, using the same values as the vendored preset:
--experimental_disk_cache_gc_max_size=20G
--experimental_disk_cache_gc_max_age=14d
Bazel then deletes the oldest entries once the cache is over 20G, and entries
older than 14 days. dx adds both flags or neither. It leaves them out when an
rc file it reads sets either flag, or when the run's arguments set either flag
or --disk_cache. Your own values always win.
Bazel cleans the cache only while a server is idle. The cleanup starts after
--experimental_disk_cache_gc_idle_delay, which is 5 minutes by default, and
only if the server's last command carried the bound. A server that shuts down
sooner, because of a short startup --max_idle_secs, never cleans. So when an
rc file sets startup --max_idle_secs below 900, dx also adds
--experimental_disk_cache_gc_idle_delay=<max_idle_secs / 3>s. It does this
even when you set your own cache and bound, and skips it when an rc file or
the run's arguments set the idle delay.
Change the bound for every run in ~/.bazelrc:
common --experimental_disk_cache_gc_max_size=40G
common --experimental_disk_cache_gc_max_age=30d
Or for one run:
bazel run @rules_dx//:dx -- build //... -- --experimental_disk_cache_gc_max_size=40G
Hooks
hook create, hook remove, hook stop, and hook guard read one JSON
object from stdin for Claude Code hooks.
Run as exactly dx worktree hook <kind>, hooks ignore dx.toml and DX_*
variables and always print text. create, remove, and stop exit 1 on
any failure, including empty or invalid stdin. When create, remove, or
stop gets input without its field, the message names the field and the hook
event that runs it.
createreadsnameandcwd. Withoutcwd, it uses the current directory. It leases a slot on branchworktree-<name>, where characters other than letters, digits,-,_, and.become-, somy taskgivesworktree-my-task. It prints only the path on stdout. Warnings such asdx worktree: cannot fetch origin, using local refs: <error>go to stderr and do not change the exit code. It reuses a local branch with that name. A new branch starts from the default branch. If onlyoriginhasworktree-<name>, it usesworktree-<name>-2(or the next free number). Subagent worktrees get noremovehook, socreatefirst frees the session's earlierworktree-*slots that are at least a minute old, clean, fully pushed, and not running adxcommand. A slot freed this way goes to a latercreate, never the one that freed it. A slot that another agent leased in the meantime is left alone. The slot is held byworktree-<name>until the firstdx worktree addorreleasethat the same session runs inside the slot. That agent then becomes the holder.removereadsworktree_pathand releases that slot. Paths outside the pool are ignored.stopreadsagent_idfor theSubagentStopevent. It marks the slots that agent holds as held by a gone holder. Slots no agent has claimed are not marked; a latercreatefrees them.guardreadstool_input.command. It exits2with a message on stderr when the command runsgit worktree add,remove, orprune. Whenagent_idis set and the command runsdx worktree, it prints anupdatedInputthat exportsDX_WORKTREE_HOLDER=<agent_id>before the command. When stdin is empty or not valid JSON, it prints a note on stderr and exits0. It exits0otherwise.
Commit .claude/settings.json. The hooks run the repo launcher, so dx does
not need to be on PATH:
{
"hooks": {
"WorktreeCreate": [{"hooks": [{"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}\"/tools/dx worktree hook create"}]}],
"WorktreeRemove": [{"hooks": [{"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}\"/tools/dx worktree hook remove"}]}],
"SubagentStop": [{"hooks": [{"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}\"/tools/dx worktree hook stop"}]}],
"PreToolUse": [{"matcher": "Bash", "hooks": [{"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}\"/tools/dx worktree hook guard"}]}]
}
}
dx init --apply writes this file when it is absent and never overwrites it. If
.claude/settings.json exists, it prints the missing entries. It also writes
the launcher and the agent commands block in AGENTS.md; see
dx init. dx status reports the agents check as warn when a
hook is missing.
Leases
The holder is the agent session (CLAUDE_CODE_SESSION_ID or
CODEX_THREAD_ID), the agent (DX_WORKTREE_HOLDER, set by hook guard), and
a process: CLAUDE_PID when it is an ancestor of dx, else the nearest
ancestor that is not a shell or a wrapper such as timeout, env, nice,
nohup, setsid, stdbuf, time, or xargs. Two agents in one session are
different holders, and list shows each one. A slot from hook create goes
to the first agent of the same session that runs dx worktree add or
release inside it. A holder is gone when its process
exits or hook stop marks it. Any other dx command run inside a slot leased
to a different session exits 2 and names the holder. dx status
inside a slot shows the lease. Every other dx command marks the slot busy
while it runs, so no add, release, or hook create resets or frees it.
While add switches a slot, such a command waits, and exits 2 with slot N is being switched by dx worktree; run the command again if the switch takes
longer than two minutes. worktree, version, and completion always run.
Examples
bazel run @rules_dx//:dx -- worktree add feature-x
bazel run @rules_dx//:dx -- worktree add feature-x --create=origin/main
bazel run @rules_dx//:dx -- worktree add feature-x --output=path
bazel run @rules_dx//:dx -- worktree release
bazel run @rules_dx//:dx -- worktree list --output=json
bazel run @rules_dx//:dx -- worktree prune
bazel run @rules_dx//:dx -- worktree prune --apply
dx completion
dx completion <shell>
Prints the completion script for one shell: bash, zsh, fish, or
powershell. Runs outside a workspace.
- Takes exactly one shell and prints its script to stdout.
- No shell, more than one shell,
--check,--output=json,--output=diff, or--Bazel options is a usage error. - Unknown shells fail with
unknown-shell.
The script asks dx for candidates as you type, so keep dx on your PATH.
Candidates cover every command, every flag, the per-command slots, the
package labels of the selected workspace, and the targets of one package.
Candidates come from the workspace a run would use: --workspace, then
DX_WORKSPACE (ignored in CI), then workspace in dx.toml, then the
current workspace. While you type, a broken dx.toml falls back to the
current workspace without output. dx completion <shell> itself fails with
invalid_defaults when dx.toml is invalid. Nested workspaces and
.bazelignore entries never offer labels.
Target completion runs one bazel query for the typed package, with the
typed --bazel-startup-option tokens before query. It gives up after about
1.5 seconds or when another Bazel command holds the server, and then offers
package patterns only. A cold bazel query can start a Bazel server that
keeps running after completion gives up. On Windows that server outlives
the terminal or tool that ran dx, unless that tool's job forbids breakaway. The query's client and any wrapper
such as Bazelisk stop when completion gives up. Completion prints no errors
while you type.
Output: text only. --output=json and --output=diff are usage errors.
DX_OUTPUT=json and output = "json" in dx.toml fall back to text.
Exit codes: 0 success, 2 usage errors (missing or unknown shell, rejected
flags) or an invalid dx.toml, 1 stdout could not be written, 141 stdout
was closed early.
bazel run @rules_dx//:dx -- completion bash > ~/.cache/dx-completion.bash
To load completion in every new shell:
- Bash: add
source <(dx completion bash)to~/.bashrc. - Zsh: add
source <(dx completion zsh)to~/.zshrcaftercompinit. - Fish: add
dx completion fish | sourceto~/.config/fish/config.fish. - PowerShell: add
dx completion powershell | Out-String | Invoke-Expressionto$PROFILE.
The script calls dx from your PATH.
dx list-tests
dx list-tests [scope ...] [-- bazel-options ...]
dx list-tests --cases [scope ...] [-- bazel-options ...]
dx list-tests --configured [scope ...] [-- bazel-options ...]
dx list-tests --here [-- bazel-options ...]
dx list-tests --affected [-- bazel-options ...]
Lists the configured test targets for a scope. It never builds or runs
them unless you pass --cases. Use dx test to run tests. The inventory reads Bazel facts
through the tests() query function, so a custom test rule counts when Bazel marks it a test, even when its name
does not end in _test. An explicit label that is not a test, and a
pattern that matches no tests, fail instead of reporting an empty suite.
A bare run lists tests(//...). Label and pattern scopes pass through.
A workspace-relative file resolves to the tests that own it or depend on its
owners; a test tagged manual is kept only when it owns the file.
Workspace-relative directories resolve to their owning tests first. --here lists the current directory tree instead
and never combines with explicit scopes. --affected lists only the tests your
git changes reach (see Scope Defaults); with none it
prints no affected targets and exits 0. It never combines with scopes or
--here. Trailing -- options reach every
Bazel invocation behind the command, including the inventory query.
The default inventory is unconfigured (bazel query). --configured
reads configured targets through bazel cquery instead. File ownership
resolution stays unconfigured in both modes, so owners are found without
configuration. With --configured, a file scope keeps only the tests that
reach its owners in the configured graph (cquery rdeps). Such a scope can
drop tests or fail with no_tests.
--cases enumerates test cases for Rust targets (rust_test and the
dx_wrap forwarder) by building each test and running it with --list.
Listing never runs test bodies, but --cases builds the selected tests
and executes their binaries and initialization code; pass it only for
targets you trust. The --list run also passes
--@rules_dx//config:validate=false, --@rules_dx//config:clippy_output_diagnostics=true
and --@rules_dx//config:rustc_output_diagnostics=true. Another value for one of them,
or for the matching @rules_rust//rust/settings flag, fails before Bazel starts with exit 2.
Every other
framework reports unsupported_framework with its rule kind, never an
empty suite. Case output is sorted and deterministic, with no cap. It ends with
a completeness line (complete or incomplete) on stderr, or a
list-tests-summary status event (ok or warn) with --output json. Inventory stays useful without case enumeration.
Output: --output text|json. dx list-tests has no report format. Text prints one label per line, or one
label: case line per case with --cases; problems go to stderr. JSON
starts with command_started, adds one status event per label
(detail is the label, hint names the scope and the query verb), one
status event per case with --cases (detail is the case, hint is
the label), one error event per problem, and ends with
command_finished. Each error event carries code, message, cause,
phase, and status when a Bazel run exited (bazel_failed, case_failed).
Exit codes: 0 success, 2 usage errors including an external scope, a
relative label, an unreadable path, a missing path (invalid_scope), a file no target owns (no_owner), or a Bazel option --cases cannot pass to
bazel run, 1 operational failures including
no_tests, bazel_failed, query_failed, query_launch_failed, launch_failed,
unsupported_framework, no_cases, or case_failed. A partial match still exits 1 when
any scope contributed no tests.
bazel run @rules_dx//:dx -- list-tests //cli/test_scratch/...
bazel run @rules_dx//:dx -- list-tests --cases //cli/test_scratch:dx_test_scratch_test_upstream
bazel run @rules_dx//:dx -- list-tests --cases //cli/test_scratch:dx_test_scratch_test_upstream --output=json
bazel run @rules_dx//:dx -- list-tests --configured //cli/test_scratch/... -- --config=ci
Scope Defaults
Most dx commands take Bazel scopes. This page lists what each command does
with a scope.
No Scope Means //...
dx build,dx test,dx coverage,dx lint,dx typecheck,dx format,dx check,dx fix,dx security,dx license,dx migrate,dx list-tests
No Scope Means The Repository
dx generate: runs Gazelle over the whole repo.dx docs: builds//tools/docs/site:user_site, or//tools/docs/site:user_site_checkin--check.
Repository-Wide Or One Exact Label
dx env,dx codegen
Patterns, paths, and multiple labels are usage errors.
Set Selectors
dx update: set,set:package, label, or path selectors. No selector checks every set;--applywrites.dx bump: exactly oneset:packageplus one version.dx add: exactly one undeclaredset:packageplus one version.
Required Arguments
dx run: needs a runnable scope.dx deploy: exactly one main-workspace label.dx watch: needs a watchable command plus its scopes.dx new: needs a language, plus an optional name.dx init: optional module name.dx completion: exactly one shell.
No Scopes
dx clean,dx status,dx version,dx upgrade,dx rollback,dx worktree
Scope Shapes
- Labels and patterns:
//...,//pkg:target,@repo//.... - Files and dirs: workspace-relative paths, also under
bazel run. A file resolves through onebazel queryof the workspace rules to the rules that use it directly. Owners in other packages are included. External repos are not loaded. A dir becomes//dir/.... - Paths with control characters in a file or directory name are refused.
--here: the current directory tree, orBUILD_WORKING_DIRECTORYunderbazel run(//path/...,//...at the root). Never combines with explicit scopes. Accepted bydx build,dx test,dx coverage,dx lint,dx typecheck,dx format,dx generate,dx check,dx fix,dx security,dx license,dx list-tests, anddx docs.--affected: the targets your changes reach, described below. Never combines with explicit scopes or--here.- Targets that do not fit the target platform are skipped in
dx build,dx test,dx coverage,dx lint,dx typecheck,dx format,dx check, anddx fix, even when named, reached through files, or picked by--affected. Skipped tests report the outcomeskipped. Pass-- --noskip_incompatible_explicit_targetsto fail on them instead.dx runstill fails on them.
bazel run @rules_dx//:dx -- lint --here
bazel run @rules_dx//:dx -- build //cli/...
--affected
--affected selects only what your changes can reach. It is accepted by
dx build, dx test, dx coverage, dx list-tests, dx lint,
dx typecheck, dx format, dx check, dx fix, dx security, and
dx license. dx generate and dx docs reject --affected (usage error,
exit 2).
- Changes are the diff from the merge-base with the default branch
(
origin/HEAD, elseorigin/main,main,origin/master,master), plus staged, unstaged, and untracked files. A rename counts both paths. - When the workspace is a subdirectory of the Git repo, paths are relative to
the workspace and changes outside it are ignored. A change under a
local_path_overridepath outside the workspace widens to//.... - Each changed file maps to its owning targets, then to their reverse
dependencies inside
//.... Targets taggedmanualare left out. dx test,dx coverage, anddx list-testskeep only test targets.dx securityanddx licensekeep only targets a dependency set owns.dx check --affectedruns format, lint, typecheck, and generate as usual, then the affected tests.dx fix --affectedruns the same phases without tests and only reports fixes;dx fix --apply --affectedwrites them. Generate and its re-check run Gazelle on the directories of the affected targets.- A change to
MODULE.bazel,MODULE.bazel.lock,WORKSPACE,WORKSPACE.bazel,.bazelversion, any*.MODULE.bazelfile, or any.bazelrc*file, at any depth, widens the scope to//.... So does a file the root.bazelrcreaches throughimportortry-import %workspace%/..., including nested imports. Other*.bazelrcfiles do not widen. So does a file thatMODULE.bazelor an included segment names by label in an extension tag, repo rule, or override, such aspnpm-lock.yaml,go.modand itsgo.sum,maven_install.json,requirements_lock.txt, or a patch, and the included segment itself. Acrate.from_cargolockfile in an included segment counts as a lock input. When dx cannot read every value inMODULE.bazel, anypnpm-lock.yaml,package-lock.json,go.mod,go.sum,go.work,go.work.sum,maven_install.json, orrequirements_lock.txt, at any depth, widens to//.... A deleted, removed, or renamed-from file selects the targets that still name it. If no target names it, it widens to its package (for example//pkg:all) plus that package's reverse dependencies. A.bzlfile follows the.bzlrule below. An unowned file, or one outside any package, widens to the package tree around it, such as//pkg/.... - A changed
BUILDorBUILD.bazelfile selects every target in its package, plus its reverse dependencies. An addedBUILDfile widens to//...; a deleted one, or a deleted file outside any package, widens to//.... - A change to a
Cargo.lockor crate_universelockfilenamed by acrate.from_cargocall inMODULE.bazelselects the targets that use the changed crates, plus the crates that depend on them, and their reverse dependencies. Any other*.lockfile, an added or deleted lock, a lock dx cannot read, or a crate_universe lockfile change outsidecrateswidens to//.... A changed crate_universe manifest such asCargo.tomldoes not widen; it selects its owners like any other file. - A changed
.bzlor.sclfile selects the packages whose BUILD files load it, directly or through other.bzlfiles, plus their reverse dependencies. dx reads the BUILD and.bzlfiles to find them. Targets that own the file, such as abzl_library, are selected too. It widens to//...when the file is used by aMODULE.bazelextension or repo rule, by an aspect named in.bazelrcor one dx runs, or by the BUILD file of a toolchain or platform package. Those packages are the ones named byregister_toolchainsorregister_execution_platformsinMODULE.bazel, or by--extra_toolchains,--extra_execution_platforms,--platforms, or--host_platformin.bazelrc. It also widens to//...when a BUILD or.bzlfile does not parse. A.bzlor.sclfile nothing loads selects nothing. A deleted.bzlor.sclfile selects the packages that still load it, or nothing. - dx prints each widening and its reason on stderr:
dx: affected: 12 targets from 3 changed files since 1a2b3c4d5e6f; widened to //... (workspace input MODULE.bazel)ordx: affected: 4 targets from 1 changed files since 1a2b3c4d5e6f; widened to //app:all (loads tools/defs.bzl)ordx: affected: 4 targets from 1 changed files since 1a2b3c4d5e6f; widened to //app:all (build file app/BUILD.bazel). The line lists//...first and names at most three scopes, thenand N more, for exampledx: affected: 0 targets from 1 changed files since 1a2b3c4d5e6f; widened to //p0:all (loads q/f.bzl), //p1:all (loads q/f.bzl), //p2:all (loads q/f.bzl) and 477 more.--output=jsonlists every widened scope. - Before the queries run, dx prints
dx: affected: resolving changes since 1a2b3c4d5e6fon stderr, then the summary.--quiet,--output=diff, and--output=jsonprint neither. An empty selection prints no summary. - No changes, or no affected targets, prints
no affected targetson stdout and exits0.dx securityis the exception: when any file changed, it runs the secrets scan. With--output=jsonit emitscommand_started(withaffected) andcommand_finished;modeischeckfordx check, and fordx fix,lint,typecheckandformatunless--applyis given;dx build,test,coverage,list-tests,securityandlicensereportdefault. --affectedtakes no value. It never combines with explicit scopes or--here: both are usage errors with exit2.--output=jsonaddsaffectedto thecommand_startedevent:{"base": "<sha>", "changed_files": [...], "targets": [...], "tests": [...], "widened": [{"scope": "//...", "reason": "..."}]}.baseis the merge-base commit,targetsis the selection, andwidenedis empty when nothing widened. A changed.bzlfile adds one//pkg:allentry per loading package. A lock file change adds one entry per lock withrepos, the changed crates:{"scope": "rdeps(//..., @crates//:serde)", "reason": "changed crates in Cargo.lock", "repos": ["@crates//:serde"]}. With more than five crates the scope readsrdeps(//..., <n> repos).testsappears only fordx check --affected. It lists the tests the last phase runs://...when widened, empty when no test is affected. dx writescommand_startedonly after planning finishes.- A checkout without a default branch exits
2withcannot find the default branch: expected origin/HEAD, main, or master. - A shallow checkout exits
2withgit history is shallow: fetch full history with `git fetch --unshallow` or pass explicit scopes. - A failed
bazel queryexits2. Theerrorevent has codequery_failedand keeps Bazel's exit status and output excerpt. A query that cannot start has codequery_launch_failed. - dx runs at most one
bazel query,kind('rule', //...), to find the owning targets of the changed files and their reverse dependencies.dx test,dx coverage,dx list-tests, anddx checkthen run onetests(set(...))query to find the affected tests. A change that widens to//...skips the tests query. dx security --affectedruns the secrets scan whenever a file changed. When no dependency set owns a reached target, it checks no set and printsRunning audit security for no dependency sets. With no changes it printsno affected targets.
bazel run @rules_dx//:dx -- test --affected
bazel run @rules_dx//:dx -- check --affected --output=json
GitHub CI
Run dx in GitHub Actions with the reusable workflows. Copy the starters into
your repo and pin a reviewed commit.
cp examples/consumer-ci/caller.yml .github/workflows/ci.yml
cp examples/docs-ci/caller.yml .github/workflows/docs.yml
bazel_dep(name = "rules_dx", version = "0.0.0")
Pin Check
Every job runs bazel run @rules_dx//tools/ci:module_pin_bin before dx.
It checks the rules_dx declaration in MODULE.bazel. Pass that same version as rules_dx_version. A repository that
calls the workflow from its own tree declares itself instead:
module(
name = "rules_dx",
version = "0.0.0",
)
Bazel resolves rules_dx first. It stops with its own error before the
check runs if MODULE.bazel declares no rules_dx, declares it twice, or
pins a release without //tools/ci:module_pin_bin.
- A multiline call, reordered arguments and comments are fine.
- A commented-out pin never counts.
- Only the
rules_dxdeclaration counts. - A mismatched version, or one that is not a quoted string, fails with the line, the declared version and the expected one.
Consumer Workflow
examples/consumer-ci/ calls reusable-consumer.yml. Nine checks run on
every platform in platforms:
lint:dx lint --check //...typecheck:dx typecheck --check //...format:dx format --check //...generate:dx generate --check //...security-audit:dx security //...license-audit:dx license //...test:dx test //...build:dx build //...coverage:dx coverage //..., plus--min-coverage <percent>whenmin_coverageis set. Withmin_changed_line_coverageit also writes the LCOV report and gates the lines a pull request adds.
--check applies to the four quality checks. dx security, dx license,
dx test, dx build, and dx coverage reject it.
The validate (inputs) job runs first. It reads every input, rejects a bad one with the
accepted values, and starts no check.
Inputs:
rules_dx_version: the version yourrules_dxdeclaration carries. The pin check compares it withMODULE.bazel.disabled_checks: job IDs above to skip, comma-separated. Empty runs all nine. Surrounding spaces are ignored. An unknown ID, a repeated ID and an empty ID fail.platforms: JSON array of platform labels, such as["linux_x86_64"]. Omit it to run["linux_x86_64"]. Valid labels:linux_x86_64,linux_arm64,macos_arm64,windows_x86_64,windows_arm64. Malformed JSON, a JSON value that is not an array, an empty array, a repeated label, an unknown label and a label carrying spaces all fail.scheduling_mode:parallelonly.sequentialfails; checks always run in parallel.code_scanning_opt_in:falseonly.truefails; no SARIF upload step ships in this workflow.min_coverage: fails below that percent, a whole number from 0 to 100 such as80. Empty collects without enforcing a threshold.80%,80.5,-1,101,eightyand a leading space fail. Coverage comes fromdx coverage.min_changed_line_coverage: fails when fewer than that percent of the executable lines a pull request adds are covered, a whole number from 0 to 100. Empty skips the check. It accepts and rejects the same values asmin_coverage, and it never changesmin_coverage.invocation_id: 1 to 40 letters, digits, underscores or hyphens. Empty uploadsadvisory-snapshots, which suits one call per workflow run. Two calls in one workflow run, including matrix cells, each need their own ID, such asapi-linux; a repeated ID makes the second upload fail. Anything else fails. The ID also keeps the calls' concurrency groups apart, so one call never cancels another. A rerun reuses the same ID.
Changed-Line Coverage
Set min_changed_line_coverage to require coverage on new code without raising
the whole-repo threshold. The coverage job fetches full history, runs
dx coverage --report lcov=<path>, then runs diff-cover 10.6.0 over that
report. It runs on pull_request events only. Other events print a notice and
skip it.
The base is the pull request base commit. The head is the checked out HEAD.
Lines come from the diff between their merge-base and HEAD. Renames follow
the file, so a pure rename adds no lines. Deleted lines are not counted. Report
paths are mapped to workspace-relative paths and must match a changed file
exactly. An absolute path maps to the longest tracked path it ends with.
The check prints the base, merge-base and head commits, the threshold, the
covered and eligible line counts, whether the evidence is complete, and each
uncovered path:line. Lines the report does not list, such as comments and
docs, are not eligible. With no eligible line the result is not-applicable,
not 100 percent.
It fails when:
- fewer than the threshold percent of eligible lines are covered. The compare is exact, with no rounding.
- the base commit is not available, or the history is too shallow to reach a merge-base.
- the report is missing, empty, or not valid LCOV.
- the
diff-coveroutput lacks its line totals or per-file stats. - a changed file has the extension of files in the report but is not in it.
The result is
incomplete.
with:
rules_dx_version: "0.0.0"
min_changed_line_coverage: "80"
A rejected input fails in the validate job before any check starts. The nine
checks need it, so none of them run. dx-ci (aggregate) names the validation
failure and fails with them.
dx-ci (aggregate) passes only when every check succeeds. It fails when a check
fails or is cancelled, when a declared check never reports a result, and when a
check is skipped without being listed in disabled_checks. A check you disabled
stays distinguishable from a check that never ran.
When a pull request or push run is cancelled, the gate is skipped and uses no runner. In a merge queue the gate still runs and fails.
The workflow needs contents: read plus checks: write for check runs.
coverage also needs pull-requests: write for its pull-request comment.
The comment shows PASS or FAIL for the coverage threshold and the changed-line
gate, and every pull-request run updates it, including failed runs.
Pull requests use the read-only cache config automatically.
A called workflow can only narrow the calling job's token, so grant those
scopes on the calling job. examples/consumer-ci/caller.yml does.
Evidence Workflow
evidence.yml runs Bazel target patterns on hosted runners for native evidence
this repository cannot produce locally. Dispatch it per run; it never runs on
push:
gh workflow run evidence.yml --ref <branch> -f scope=//cli/cli/...
gh workflow run evidence.yml --ref <branch> -f scope="//cli/process/... //tools/testing/process_probe/..."
scope: space-separated target patterns such as//pkg/...or//pkg:target.task:test(default) orbuild.platforms: JSON array of platform labels. Accepted:linux_x86_64,linux_arm64,macos_arm64,windows_x86_64,windows_arm64. List each platform once.test_timeout: whole seconds each test may run. Default600.displays: Linux only.xcb,waylandorxcb,wayland. Starts Xvfb and a headless Weston and setsDX_QT_DISPLAYS. Default empty: no displays.
A validate job rejects a bad input naming the accepted values. Each platform
cell runs the task, uploads its log as the evidence-<platform> artifact, and
writes the tail to the job summary.
Apple Workflow
apple.yml runs the Apple toolchain tests on a hosted macOS runner with Xcode
preinstalled. Dispatch it per run:
gh workflow run apple.yml --ref <branch> -f command=test \
-f targets="//apple/... //apple/tests:macos_consumer_test //apple/tests:macos_app_consumer_test //apple/tests:macos_framework_consumer_test"
command:test(default) orbuild.targets: space-separated Bazel targets. Each must start with//.runner:macos-15(arm64, default).
Each run uploads the
apple-evidence-<runner> artifact with the executor's macOS, Xcode, and SDK
identities, the Bazel log, and the test logs and outputs.
Advisory Snapshots
security-audit reads .dx/advisory/. One
advisory snapshots (one run, every platform) job fills that directory once
per run, uploads it as the advisory-snapshots artifact
(advisory-snapshots-<invocation_id> when set), and every platform cell
downloads the same
bytes. No cell downloads a database of its own.
bazel run @rules_dx//cli/advisory_prep --
The command converts one archive per advisory family the dependency locks need,
so a repository with only Cargo.lock prepares the cargo snapshot alone. It
writes <out>/<family>.json and <out>/<family>.meta.json. The sidecar holds
the set, the source URL, the sha256 of the payload, the stamped date and the
payload path. The payload is the same for the same advisories whatever order
the archive lists them in.
Relative paths anchor at the workspace root.
--workspace <dir>: dependency set locks to read. Default the workspace root.--out <dir>: where the snapshots are written. Default.dx/advisoryunder it.--date YYYY-MM-DD: date every sidecar carries. Default today, UTC.--max-time <seconds>: wall clock limit for one download. Default 600.--connect-timeout <seconds>: connect limit for one download. Default 30.--retries <count>: attempts one download may spend. Default 3.--retry-delay <seconds>: pause between two attempts. Default 5.--archive FAMILY=PATH: convert a local archive instead of fetching it. Repeatable.--status-file <file>: appendstatus,ecosystemsandfamiliesaskey=valuelines. The workflow passes$GITHUB_OUTPUT.
A repository whose locks need no advisory ecosystem prepares nothing. The
command prints no advisory ecosystems needed, exits 0 and reports
status=empty and ecosystems=0. The job then uploads no artifact, the
security-audit cells download none, and secret scanning still runs. Dependency
sets without a lock are not audited. A missing artifact for a needed ecosystem
still fails the audit and dx-ci (aggregate).
Only https and file URLs are fetched, a redirect stays on https, and an
archive entry is refused when its length or checksum disagrees with the
archive. Exit codes: 0 success, 2 usage error, 1 preparation failure. A
failure prints one line starting with advisory_prepare_failed: naming the
family and the archive entry, path or limit it could not use, and writes no
snapshot for that family.
The artifact is kept for one day. Each security-audit cell judges freshness
against the date the job stamped, passed to dx security as DX_AUDIT_TODAY,
so every cell accepts or rejects the same snapshot. security-audit needs the
job, so a failed preparation skips every cell and dx-ci (aggregate) fails. A
missing, stale or corrupt snapshot fails the audit with advisory_refresh_failed
naming the set and the file. The job needs contents: read and no secret, so
fork pull requests run it.
Hermetic Tool Resolution
security-audit scans secrets with the pinned Gitleaks artifact, and dx
refuses ambient PATH lookup. One Resolve hermetic Gitleaks step resolves
the artifact path with a native command instead of shell parsing, so every
platform cell exports the same variable without host utilities.
bazel run @rules_dx//cli/tool_resolve -- --label @dx_tools//:gitleaks --var DX_GITLEAKS_BIN
The command runs bazel cquery --output=files for the label, takes the last
output line as the artifact path, joins it with bazel info output_base, and
refuses a missing file. It appends VAR=path to the file GITHUB_ENV names.
--label <label>: pinned tool label to resolve. Required.--var <name>: environment variable receiving the absolute path. Required.--bazel <path>: Bazel binary the resolution runs through. Defaultbazel.--workspace <dir>: directory the inner Bazel commands run in. DefaultBUILD_WORKSPACE_DIRECTORY, thebazel runinvocation root, else the current directory.--env-file <path>: environment file receivingVAR=path. DefaultGITHUB_ENV.--print: writeVAR=pathto standard output instead of a file.- Arguments after
--reach the inner cquery unchanged.
Exit codes: 0 success, 2 usage error, 1 resolution failure. A failure
prints one line starting with tool_resolve_failed: naming the label and the
cquery, output-base, missing-file or environment write it could not use, and
exports nothing.
In a local shell, export the same variable with --print:
export "$(bazel run @rules_dx//cli/tool_resolve -- --label @dx_tools//:gitleaks --var DX_GITLEAKS_BIN --print)"
Docs Workflow
examples/docs-ci/ calls reusable-docs.yml. It runs dx lint --check over
docs_scope (default //...), runs dx docs --check on site_check when
you set it, then renders your site label with dx docs and checks the
rendered tree for missing pages, styles, and search index, so broken links
fail before deploy.
Inputs:
rules_dx_version: the version yourrules_dxdeclaration carries. The pin check compares it withMODULE.bazel.docs_scope: target patterns covered by the docs check, separated by spaces, such as//... //docs:all. Default//.... Each pattern reachesdxas one argument. A value with quotes, shell characters, wildcards, a leading-or anything but a target pattern fails with one error naming it.publish: build the rendered site and deploy to Pages. Default false.environment: Pages environment name. Defaultgithub-pages.site: required. One target label in your repository whose output is the rendered mdBook tree, withindex.htmlandsearchindex.jsat its root.site_check: target label thatdx docs --checkruns. Default empty, which skips the source check.forbid: text the rendered site must not contain, one entry per line. Default empty.landing: also require a landing page in the rendered site. Default false.smoke_test: test label run withDX_PAGES_URLset to the deployed page URL. Default empty, which runs only the built-in page fetch.
A site, site_check or smoke_test value that is not a single target label
fails the docs check with one dx-ci: line naming the input.
Publishing needs pages: write plus id-token: write. The publish job stages
and validates the whole site tree, deploys it, fetches index.html and
searchindex.js from the reported page URL, then runs smoke_test if you set
one, all before the clean-checkout proof. Enable Pages with source GitHub
Actions before the first publishing run.
Bump Workflow
.github/workflows/bump.yml opens one dependency PR per run.
Create a GitHub App with contents: write and pull-requests: write.
Install it on the repository. Set two secrets:
BUMP_APP_ID: the App ID.BUMP_APP_PRIVATE_KEY: the App private key.
Dispatch the workflow with selector and version:
gh workflow run bump.yml -f selector=cargo:anyhow -f version=1.2.3
A run without both inputs lists outdated dependencies and changes nothing.
A dispatched run fails before changing files when the secrets are missing.
It pushes the branch and opens the PR as the App, so the PR runs the
required checks. Only the manifest, lock and pin files for the selected set
are staged. Other changed files fail the run. One run never batches two
dependencies. Set automerge to true to merge on the full required-check
set.
Repository Settings
Set these once in the repository. Each caller already grants its job permissions, so a read-only repository default is fine.
- Approval policy:
all_external_contributors, so fork pull requests run. - Branch protection for the consumer workflow: require
dx-ci (aggregate). One green gate covers the whole matrix. GitHub counts a skipped required check as passing, so rerun a cancelled pull request run before merging, or use a merge queue. - Branch protection for the docs workflow: require
docs-check. Adddocs-publish (GitHub Pages)oncepublishis true. - Merge gate: require branches to be up to date, or use a merge queue. Both
callers run on
merge_group(checks_requested), so the queue gets the required checks for its own SHA. Merge-group runs post no PR comment and never publish Pages. - Pages source: GitHub Actions, before the first publishing run.
- Workflow permissions: read-only by default. Each calling job then grants
what its reusable jobs request: the consumer caller
checks: writepluspull-requests: write, the docs callerpages: writeplusid-token: write.
Caching
Both callers forward repository secrets with secrets: inherit. Set
BUILDBUDDY_API_KEY to share the BuildBuddy remote cache. Codecov is
optional.
Every job restores the Bazel repository cache that Bazel and dx use. Each
platform shares one copy, keyed on MODULE.bazel and MODULE.bazel.lock.
On the default branch, coverage saves it. If coverage is disabled, test
saves it. If both are disabled, build saves it. With all three disabled,
nothing saves it. The saved copy holds only the downloads the saving job
used. Pull requests, forks included, merge queues and other branches only
restore it. Without BUILDBUDDY_API_KEY, build and test outputs
are not cached between runs.
With that secret set, every Bazel and dx call in both workflows runs with
--config=ci, plus --config=ci-pr on pull requests. Define both configs in
the repository .bazelrc before setting the secret. An undefined config fails
the run: Config value 'ci' is not defined in any .rc file.
common:ci --remote_cache=grpcs://remote.buildbuddy.io
common:ci --remote_cache_compression
common:ci --remote_timeout=10m
common:ci --remote_download_outputs=all
common:ci-pr --noremote_upload_local_results
Without the secret, no config flag is passed and no stanza is needed.
Examples
Start a new consumer, adopt an existing tree, or mix frameworks. Each
workspace below records exact commands and expected evidence in its own
README.md.
Start From CI Templates
- consumer-ci starter caller for the reusable consumer workflow.
- docs-ci starter caller for the reusable docs workflow.
- gitlab-ci starter pipeline for GitLab CI.
Adopt An Existing Tree
- adopt-rust foreign Cargo workspace adopted by
dx generate. - adopt-python foreign Python tree adopted by the Python Gazelle extension.
- adopt-js-ts foreign JS/TS tree adopted by the JavaScript/TypeScript Gazelle extensions.
- adopt-go foreign Go module adopted by the Go Gazelle extension.
- adopt-cpp foreign C++ tree adopted by the C++ Gazelle extension.
- adopt-java foreign Maven-layout tree adopted by the Java Gazelle extension.
- adopt-kotlin foreign Maven-layout tree adopted by the Kotlin Gazelle extension.
- adopt-scala foreign sbt-layout tree adopted by the Scala Gazelle extension.
- adopt-csharp foreign SDK-style tree adopted by the C# Gazelle extension.
- adopt-fsharp foreign SDK-style tree adopted by the F# Gazelle extension.
- adopt-ruby foreign Bundler-layout tree adopted by the Ruby Gazelle extension.
- adopt-powershell foreign PowerShell tree adopted by handwritten wrappers.
- adopt-polyglot foreign Python+Rust+JS/TS+C++ tree adopted package by package.
Mix Frameworks
- mixed Vue, Svelte, Astro, and MDX containers over one shared JavaScript helper.
Share One Core
- shared-rust one Rust task board driving native, browser, WASI, and Android outputs.
Build an Application
- appimage-app multi-package Linux C app with a shared library and a packaged resource, built as an AppImage and run without FUSE in tests.
- c-app multi-package pure C stock CLI with a pinned
cJSONdependency, a generated header and packaged data. - cpp-app multi-package C++17 menu report with templates, a pinned
fmtdependency, a generated header and a runfiles resource. - elixir-app multi-package Elixir application with a generated unit table, a supervised ledger, a pinned
jasonHex package and an escript. - erlang-app multi-package Erlang/OTP key-value store with a leex and yecc parser, a supervised application, a pinned
thoasdependency and an escript. - gleam-app multi-package Gleam stock CLI for Erlang and JavaScript with a generated unit table, a typed ledger and a pinned
gleam_jsonHex package. - java-app multi-package Java invoice CLI with pinned
gsonandguavadependencies, a generated class and a packaged resource. - kotlin-app multi-package Kotlin gradebook CLI with pinned
gsonandguavadependencies, a generated object and a packaged resource. - msix-app Windows console app with a DLL and a packaged resource, built as an MSIX, installed and signed in tests.
- node-app multi-package TypeScript and JavaScript registry CLI with a pinned
semverdependency, a generated module and a packaged resource. - perl-app multi-package Perl stock CLI with a typed ledger, a runfiles CSV table, a pinned
Text::CSVCPAN dependency and aselect()flag. - python-service multi-package Python CLI and local service with a pinned
clickdependency, a generated module and packaged data. - qt-app headless Qt app with a QML module, a C++ QML type, a Qt resource and a QtTest suite.
- qt-rust headless Qt/QML ledger backed by a Rust model through CXX-Qt bindings, with a pinned
adler2dependency, a QtTest suite and a relocatable archive. - qt-tasks multi-module Qt/QML task browser with C++ models, a Qt Quick board and Quick Test suite, a pinned
fmtdependency, translations, JSON storage and relocatable archives. - zig-app multi-package Zig manifest CLI with a pinned
xxHashC dependency, a generated module and embedded data.
Consumer CI example
cp caller.yml .github/workflows/ci.yml
Starter caller for the reusable consumer workflow. Pin to a reviewed commit.
bazel_dep(name = "rules_dx", version = "0.0.0")
Every job reads that declaration before dx runs. The version must be a
quoted string, and no other dependency may declare rules_dx.
A validate job runs before the checks and rejects a bad input with the
accepted values. Only parallel scheduling, false for code_scanning_opt_in
and a whole min_coverage or min_changed_line_coverage from 0 to 100 are
accepted.
Set approval policy, branch protection, and least-privilege permissions manually.
Default C/C++ and Rust setup
Generate the setup snippet:
bazel run @rules_dx//:dx -- init --apply my_project
Copy MODULE.bazel.snippet into MODULE.bazel. The snippet pins the
hermetic LLVM and Rust toolchains and registers them:
bazel_dep(name = "rules_dx", version = "0.0.0")
bazel_dep(name = "rules_cc", version = "0.2.27")
bazel_dep(name = "llvm", version = "0.8.23")
bazel_dep(name = "rules_rust", version = "0.74.0")
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
rust.toolchain(
edition = "2021",
rustfmt_version = "1.98.0",
versions = ["1.98.0"],
)
use_repo(rust, "rust_toolchains")
register_toolchains("@llvm//toolchain:all")
register_toolchains("@rust_toolchains//:all")
Only Bazel/Bazelisk is required. Ordinary cc_* and rust_* rules need
no linker paths or per-target flags. Native host builds are the default.
Cross-target and Windows ABI choices are explicit advanced setup.
Windows ARM64 builds stay unqualified.
Own toolchains
Keep the bazel_dep lines and drop the register_toolchains lines, then
register selected toolchains. Loading the rules never replaces consumer
toolchains:
cc_configure = use_extension("@rules_cc//cc:extensions.bzl", "cc_configure_extension")
use_repo(cc_configure, "local_config_cc", "local_config_cc_toolchains")
register_toolchains("@local_config_cc_toolchains//:all")
A consumer that renames the module keeps working. Declare the rename once:
bazel_dep(name = "rules_dx", version = "0.0.0", repo_name = "my_dx")
Then load through the new name:
load("@my_dx//cc/rules:defs.bzl", "cc_binary")
Minimal targets
Load the public rules through the module name. A Rust library looks like this:
load("@rules_dx//rust/rules:defs.bzl", "rust_library")
rust_library(
name = "greet",
srcs = ["lib.rs"],
crate_name = "greet",
crate_root = "lib.rs",
)
A C++ binary that depends on another repository looks like this:
load("@rules_dx//cc/rules:defs.bzl", "cc_binary")
cc_binary(
name = "greet",
srcs = ["main.cc"],
deps = ["@greet_lib//:greeting"],
)
Run the checks from the module root with the public launcher:
bazel run @rules_dx//:dx -- lint --check //...
bazel run @rules_dx//:dx -- format --check //...
The full input contract lives in the GitHub CI docs.
Docs CI Example
Copy caller.yml to .github/workflows/docs.yml in the consumer repo.
Never copy the check or deploy steps. The caller pin is a reviewed commit;
bumps are deliberate and reviewed.
cp caller.yml .github/workflows/docs.yml
The consumer module pins the same version the caller passes as
rules_dx_version (0.0.0 today):
bazel_dep(name = "rules_dx", version = "0.0.0")
Both jobs read that declaration before dx runs.
docs-check runs dx lint --check over docs_scope (space-separated target
patterns, each passed as one argument), dx docs --check, and
a render of the site whose pages, styles, and search index are validated.
publish: true builds the rendered site via dx docs, validates the staged
tree, deploys it to the github-pages environment, then smoke-tests the
reported page URL. Enable Pages with source GitHub Actions before the first
publishing run.
GitLab CI example
cp .gitlab-ci.yml <your-repo>/.gitlab-ci.yml
Starter pipeline for GitLab. It runs four jobs on Linux runners: check,
build, test and coverage. Each job runs one dx line:
bazel run @rules_dx//:dx -- check --report sarif=reports/dx.sarif //...
bazel run @rules_dx//:dx -- build --report failures=reports/failures.json //...
bazel run @rules_dx//:dx -- test --report junit=reports/junit.xml //...
bazel run @rules_dx//:dx -- coverage --report lcov=reports/coverage.lcov //...
The same recipe works on any CI provider:
- Install Bazelisk 1.29.0 as
bazeland check its SHA-256. - Commit
.bazelversion. - Set
XDG_CACHE_HOMEto.cacheinside the checkout. - Run
mkdir -p reports, then thedxlines above.
Consumer setup
Declare the module in MODULE.bazel:
bazel_dep(name = "rules_dx", version = "0.0.0")
Commit MODULE.bazel.lock and .bazelversion. The cache key reads both.
Add to .gitignore:
.cache/
reports/
bazel-*
.dx/
Add to .bazelignore:
.cache
Reports
Every job keeps reports/ as an artifact, also when the job fails. A job
that hits its timeout keeps nothing.
reports/junit.xmlshows in the merge request test widget.reports/coverage.lcov,reports/dx.sarifandreports/failures.jsonare plain artifacts. GitLab coverage reports take only Cobertura or JaCoCo, and the SARIF widget needs GitLab Ultimate.
Exit status
A job exits with the dx exit code. When dx passes, the job still fails
with 1 if its report is missing or empty, or if the run changed tracked
or untracked files. The job prints the missing path or the changed files.
Caching
Each job caches only the Bazel repository cache under .cache/bazel/. The
key is the job name plus MODULE.bazel.lock and .bazelversion. GitLab
keeps separate caches for protected and unprotected branches, so merge
request pipelines never write the cache that default branch pipelines read.
Bazel checks every cached file by its hash. Bazelisk is downloaded fresh in
every job.
Remote cache
Opt in with a GitLab CI/CD variable:
- Add a variable named
DX_REMOTE_BAZELRCof type File. Mark it protected. Put your remote cache flags in it. GitLab cannot mask a multi-line value, so never print the file in a job. - Add this line to the consumer
.bazelrc:
try-import %workspace%/.cache/remote.bazelrc
Only protected branches get the file. Merge request pipelines run without it.
Image and runner
The pipeline uses buildpack-deps:noble-scm@sha256:59d4f4d8581d741bb2c31f4bfd00a557ca94d182d082c81bd56d32afbad8dfc9
on the saas-linux-small-amd64 runner.
Adopt C++ example
C++ tree with a greet package and a stdlib-only solo package.
bazel run @rules_dx//:dx -- init //examples/adopt-cpp/...
bazel run @rules_dx//:dx -- generate //examples/adopt-cpp/...
bazel build //examples/adopt-cpp/...
bazel test //examples/adopt-cpp/...
Both tests pass (greet_test, pure_test).
copts reaches the compiler exactly as written, so -std=c++17 here stays C++17. Warnings are errors because the repository selects the treat_warnings_as_errors toolchain feature, not because the wrapper adds a flag. The same feature can also be selected on the command line:
bazel build --features=treat_warnings_as_errors //examples/adopt-cpp/...
See cc/rules/README.md for the wrapper contract.
Adopt C# example
C# SDK-style tree with a greet package and a stdlib-only solo package.
bazel run @rules_dx//:dx -- init //examples/adopt-csharp/...
bazel run @rules_dx//:dx -- generate //examples/adopt-csharp/...
bazel build //examples/adopt-csharp/...
bazel test //examples/adopt-csharp/...
Both tests pass (greet_test, pure_test).
Adopt F# example
F# SDK-style tree with a greet package and a stdlib-only solo package.
bazel run @rules_dx//:dx -- init //examples/adopt-fsharp/...
bazel run @rules_dx//:dx -- generate //examples/adopt-fsharp/...
bazel build //examples/adopt-fsharp/...
bazel test //examples/adopt-fsharp/...
Both tests pass (greet_test, pure_test).
Adopt Go example
Go module with a greet package and a stdlib-only solo package.
bazel run @rules_dx//:dx -- init //examples/adopt-go/...
bazel run @rules_dx//:dx -- generate //examples/adopt-go/...
bazel build //examples/adopt-go/...
bazel test //examples/adopt-go/...
Both tests pass (greet_test, solo_test).
Adopt Java example
Java Maven-layout tree with a greet package and a stdlib-only solo package.
bazel run @rules_dx//:dx -- init //examples/adopt-java/...
bazel run @rules_dx//:dx -- generate //examples/adopt-java/...
bazel build //examples/adopt-java/...
bazel test //examples/adopt-java/...
Both tests pass (greet_test, pure_test).
Adopt JS/TS example
JS/TS tree with an app package and a web TypeScript package.
bazel run @rules_dx//:dx -- init //examples/adopt-js-ts/...
bazel run @rules_dx//:dx -- generate //examples/adopt-js-ts/...
bazel build //examples/adopt-js-ts/...
bazel test //examples/adopt-js-ts/...
All 3 tests pass (greet_test, plain_test, app_test). The TypeScript wrapper emits its
own typecheck tests, so bazel test runs more targets than the example
declares.
Adopt Kotlin example
Kotlin Maven-layout tree with a greet package and a stdlib-only solo package.
bazel run @rules_dx//:dx -- init //examples/adopt-kotlin/...
bazel run @rules_dx//:dx -- generate //examples/adopt-kotlin/...
bazel build //examples/adopt-kotlin/...
bazel test //examples/adopt-kotlin/...
Both tests pass (greet_test, pure_test).
Adopt polyglot example
Mixed tree with a Python package, a Rust crate, a JavaScript package, a TypeScript package, and a C++ library with a handwritten binary in its own child package.
bazel run @rules_dx//:dx -- init //examples/adopt-polyglot/...
bazel run @rules_dx//:dx -- generate //examples/adopt-polyglot/...
bazel build //examples/adopt-polyglot/...
bazel test //examples/adopt-polyglot/...
All 4 tests pass (shapes_test, widgets_test, totals_test, native_test).
The TypeScript wrapper emits its own typecheck tests, so bazel test runs more
targets than the example declares.
Adopt PowerShell example
PowerShell tree with a greet package and a stdlib-only solo package.
bazel run @rules_dx//:dx -- init //examples/adopt-powershell/...
bazel run @rules_dx//:dx -- generate //examples/adopt-powershell/...
bazel build //examples/adopt-powershell/...
bazel test //examples/adopt-powershell/...
Both tests pass (greet_test, pure_test).
Adopt Python example
Python tree with an app package, a stdlib-only solo directory, and a tests/ directory.
bazel run @rules_dx//:dx -- init //examples/adopt-python/...
bazel run @rules_dx//:dx -- generate //examples/adopt-python/...
bazel build //examples/adopt-python/...
bazel test //examples/adopt-python/...
All 4 tests pass (handlers_test, pure_test, pure_unittest_test, login_test).
Adopt Ruby example
Ruby tree with a greet package and a stdlib-only solo package.
bazel run @rules_dx//:dx -- init //examples/adopt-ruby/...
bazel run @rules_dx//:dx -- generate //examples/adopt-ruby/...
bazel build //examples/adopt-ruby/...
bazel test //examples/adopt-ruby/...
Both tests pass (greet_spec, solo_test).
Adopt Rust example
Rust workspace with three path crates (api, support, worker) and a manifest-free solo directory.
bazel run @rules_dx//:dx -- init //examples/adopt-rust/...
bazel run @rules_dx//:dx -- generate //examples/adopt-rust/...
bazel build //examples/adopt-rust/...
bazel test //examples/adopt-rust/...
All 5 tests pass (api_test, api_roundtrip_test, common_test, helper_test, solo_test).
Adopt Scala example
Scala sbt-layout tree with a greet package and a stdlib-only solo package.
bazel run @rules_dx//:dx -- init //examples/adopt-scala/...
bazel run @rules_dx//:dx -- generate //examples/adopt-scala/...
bazel build //examples/adopt-scala/...
bazel test //examples/adopt-scala/...
Both tests pass (greet_test, pure_test).
For Scalafix lint, see docs/cli/commands/quality.md.
Mixed hello example
Vue, Svelte, Astro, and MDX containers over one shared JavaScript helper.
bazel build //examples/mixed/hello/...
bazel test //examples/mixed/hello/...
hello_test covers the shared helper edge.
AppImage
Package a target binary as a Linux AppImage. The tools are pinned and fetched
by Bazel. No FUSE or distro packages are needed to build or run. The build
step is a pinned Rust binary run with an empty PATH. It uses no tool from
/bin or /usr/bin.
Cells: linux_x86_64 and linux_arm64. Any other target fails analysis with
target cell this platform is unsupported.
Pins
| Tool | Version | License |
|---|---|---|
| appimagetool | 1.9.1 | MIT |
| type 2 runtime (x86_64, aarch64) | 20251108 | MIT |
| mksquashfs | 4.6.1 | GPL-2.0-or-later |
Use
MODULE.bazel needs nothing extra. The tools come from @dx_appimage_tools,
declared by rules_dx and fetched on first use.
load("@rules_dx//appimage:defs.bzl", "dx_appimage")
dx_appimage(
name = "app",
app_name = "app",
binary = ":app_bin",
data = ["greeting.txt"],
desktop = "app.desktop",
icon = "app.png",
libs = [":native_shared"],
notice = ["NOTICE.txt"],
version = "1.2.3",
)
Attributes:
binary: the executable. Its target-configuration bytes are packaged.app_name: defaults to the target name.desktopmust be<app_name>.desktopwithExec=<app_name>.iconmust be<app_name>.pngor.svg.libs: shared libraries copied tousr/lib. Link the binary with-Wl,-rpath,$ORIGIN/../lib.data: files copied tousr/share/<app_name>. Files keep their path relative to their data target's package. Files a data target collects from another package or repo keep only their file name.notice: license and NOTICE files copied tousr/share/licenses/<app_name>.version: recorded in the identity file.tools,runtime: replace the pinned tools and runtime. The runtime must match the target arch.- Two different
libs,noticeordatafiles that map to the same path fail analysis. The same file listed twice is packaged once.
Outputs:
<name>.AppImage<name>.AppImage.identity.json: app name and version, tool and runtime versions, arch (x86_64oraarch64), runtime digest, interpreter, minimum glibc, image digest and a digest for every packaged file.
AppRun is a link to usr/bin/<app_name>. The runtime sets $APPDIR.
Build and run
bazel build //pkg:app
Building signs nothing, uploads nothing and needs no credentials.
Run the image without FUSE:
APPIMAGE_EXTRACT_AND_RUN=1 bazel-bin/pkg/app.AppImage
bazel-bin/pkg/app.AppImage --appimage-extract
APPDIR="$PWD/squashfs-root" squashfs-root/AppRun
Supported tuples
| Build host | Target cell | Run host | Run route | Install | Sign |
|---|---|---|---|---|---|
| Linux x86_64 or arm64 | linux_x86_64 | Linux x86_64, glibc at least min_glibc | FUSE mount, APPIMAGE_EXTRACT_AND_RUN=1 or --appimage-extract | none, copy the file | unsupported, images are unsigned |
| Linux x86_64 or arm64 | linux_arm64 | Linux arm64, glibc at least min_glibc | FUSE mount, APPIMAGE_EXTRACT_AND_RUN=1 or --appimage-extract | none, copy the file | unsupported, images are unsigned |
| macOS or Windows | any | none | none | none | unsupported, the targets are incompatible |
min_glibc is in the identity file.
Failures:
- A missing resource or library fails in the app or the loader, not silently.
- A truncated or corrupted image exits non-zero.
- A wrong desktop or icon name fails analysis.
- Two different files that map to the same path in the image fail analysis.
- A target that is not Linux x86_64 or arm64 fails analysis.
- A desktop file without
Exec=<app_name>fails the build.
Build for arm64 from any Linux host:
bazel build //pkg:app --platforms=//appimage/platforms:linux_arm64
The image is packaged from the arm64 target bytes. Running it needs an arm64 Linux host.
Test
bazel test //appimage/... //examples/appimage-app/...
# Consumer qualification (manual; needs network and a native bazelisk):
bazel test //tools/testing/hermetic:appimage
examples/appimage-app is a two-package app that is built, relocated and run without FUSE in its tests.
AppImage example
A Linux C app packaged as an AppImage. The app reads a packaged resource and calls a function from a shared library in another package.
| Package | Role |
|---|---|
app | dxappimage, greeting.txt, desktop file, icon, notice, the app image and run_test |
greet | libgreet_shared.so |
Build on a Linux host:
bazel build //examples/appimage-app/app
Run the image without FUSE:
APPIMAGE_EXTRACT_AND_RUN=1 bazel-bin/examples/appimage-app/app/app.AppImage
bazel-bin/examples/appimage-app/app/app.AppImage --appimage-extract
APPDIR="$PWD/squashfs-root" squashfs-root/AppRun
dxappimage prints:
resource=hello from the packaged resource
library=greeting from libgreet_shared.so
probe=off
Building with --copt=-DDX_CONSUMER_PROBE prints probe=on.
Exit codes: 0 ok, 2 APPDIR is not set, 3 the resource is missing, 4
the resource is empty. Without libgreet_shared.so the loader stops the app
before it starts.
app.AppImage.identity.json records the app name and version, arch, minimum
glibc, image digest and a digest for every packaged file.
Test:
bazel test //examples/appimage-app/...
run_test copies the image to a new directory and runs it both ways. It checks
every extracted file against the identity file, exit codes 2 and 3, a
missing library and a truncated image.
Build for arm64:
bazel build //examples/appimage-app/app --platforms=//appimage/platforms:linux_arm64
Supported hosts and run routes: AppImage supported tuples.
C stock example
A multi-package pure C app. It has a stock CLI, a store library that reads JSON through cJSON, a generated store_build.h header and a packaged stock.json.
| Package | Role |
|---|---|
store | JSON loader and totals, extern "C" header, generated store_build.h |
cli | stock binary with list, total and get NAME |
bazel build //examples/c-app/...
bazel test //examples/c-app/...
bazel run //examples/c-app/cli:stock -- total
bazel run //examples/c-app/cli:stock -- get bolt-m6
bazel run //examples/c-app/cli:stock -- --file path/to/stock.json list
Without --file or STOCK_FILE, stock reads the packaged stock.json through runfiles, so the binary runs from any directory. Relative --file paths resolve against the directory you ran bazel run from. Quantities and prices must be whole numbers from 0 to 1000000.
Exit codes: 0 ok, 2 usage, 3 unreadable or malformed stock file, 4 unknown item. exit_codes_test runs the binary through each code.
Sources build as C11 with -std=c11. store_test and cli_test are C programs and fail to compile as C++. linkage_test includes the same header from C++ and links the C library. cJSON is a pinned http_archive declared in MODULE.bazel. format_test checks the sources against the repository .clang-format.
diagnostics_test compiles and lints files with a syntax error, a type error and a lint finding, and expects each to fail.
Another workspace builds, tests, runs and copies both apps with bazel test //cc/tests/consumer:app_consumer_test.
C++ pantry example
A multi-package C++17 app. It has a menu report binary, a templated group_sum, a fmt dependency, a generated build_config.h header and a menu.csv resource found through runfiles.
| Package | Role |
|---|---|
core | Menu parser, group_sum template, generated build_config.h, menu.csv |
app | pantry binary and report formatter |
bazel build //examples/cpp-app/...
bazel test //examples/cpp-app/...
bazel run //examples/cpp-app/app:pantry
bazel run //examples/cpp-app/app:pantry -- path/to/menu.csv
Without a path, pantry reads the packaged menu.csv through runfiles, so the binary runs from any directory. A relative path resolves against the directory you ran bazel run from.
Exit codes: 0 ok, 3 runfiles unavailable, 4 menu file missing or malformed. exit_codes_test runs the binary through each code.
fmt is a pinned http_archive declared in MODULE.bazel. Tests use GoogleTest. pantry_output_test runs the binary and compares its report. format_test checks the sources against the repository .clang-format.
diagnostics_test compiles and lints files with a syntax error, a type error and a lint finding, and expects each to fail.
Another workspace builds, tests, runs and copies both apps with bazel test //cc/tests/consumer:app_consumer_test.
Elixir app example
An Elixir application split into three packages. It builds and tests on the pinned Elixir and Erlang/OTP runtimes with nothing installed on the host.
| Package | Role |
|---|---|
catalog | catalog library: item parsing, a unit table generated from units.txt, and JSON through jason |
stock | stock application: a supervised GenServer ledger with a per-item limit from app_env |
cli | stockctl escript that runs commands against the ledger |
bazel build //examples/elixir-app/...
bazel test //examples/elixir-app/...
bazel run //examples/elixir-app/cli:stockctl -- "add 2 dozen pears" "count pears"
The run prints 24, 24 and the snapshot [{"name":"pears","units":24}]. A command that fails prints error: ... and exits 1.
catalog:catalog_test covers parsing, the generated table and the JSON output. stock:ledger_test covers the limit from app_env, the application graph, and supervisor restarts after a kill and after a crash. cli:stockctl_test runs the commands in process and through the stockctl launcher.
jason is a pinned hex_package declared in MODULE.bazel.
Erlang app example
An OTP application split into three packages. It builds and tests on the pinned Erlang/OTP runtime with nothing installed on the host.
| Package | Role |
|---|---|
proto | kv_proto library: a leex lexer and a yecc parser for put, get and del commands |
store | kv_store application: a supervised gen_server with a key limit, watchers and a JSON snapshot through thoas |
cli | kvctl escript that runs commands against the store |
bazel build //examples/erlang-app/...
bazel test //examples/erlang-app/...
bazel run //examples/erlang-app/cli:kvctl -- "put apples 3" "get apples"
The run prints ok, 3 and the snapshot {"apples":3}. A command that does not parse prints error: ... and exits 1.
proto:eunit runs the parser tests. store:kv_store_SUITE is a Common Test suite for the limit from app_env, watcher events, supervisor restarts and the snapshot. cli:kvctl_SUITE runs the commands in process and through the kvctl launcher.
thoas is a pinned http_archive declared in MODULE.bazel.
Gleam app example
A Gleam application split into three packages. Each package builds for Erlang on the pinned OTP runtime and for JavaScript on the managed Node.js runtime, with nothing installed on the host.
| Package | Role |
|---|---|
catalog | Item parsing, a unit table generated from units.txt, and JSON through gleam_json |
stock | Typed stock ledger with a per-item limit, sources in lib/ and the JavaScript package stock_ledger |
cli | stockctl command line on Erlang and stockctl_js on Node.js |
bazel build //examples/gleam-app/...
bazel test //examples/gleam-app/...
bazel run //examples/gleam-app/cli:stockctl -- "add 2 dozen pears" "count pears"
bazel run //examples/gleam-app/cli:stockctl_js -- "add 2 dozen pears" "count pears"
Both runs print 24, 24 and the snapshot [{"name":"pears","units":24}].
A command that fails prints error: ... and exits 1.
Every package has a gleeunit test for each target. cli:launchers_test runs
both launchers and checks their output and exit codes.
gleam_json is a pinned gleam_hex_package declared in MODULE.bazel.
Java invoice example
A multi-package Java app. It has an invoice CLI, a billing library that reads JSON through gson and groups sums through guava, a generated BuildInfo class and a packaged invoices.json resource.
| Package | Role |
|---|---|
billing | Invoice model, JSON reader, totals, generated BuildInfo, invoices.json |
cli | invoice binary with list, total, customers, schema and show ID |
bazel build //examples/java-app/...
bazel test //examples/java-app/...
bazel run //:dx -- generate --check //examples/java-app/...
bazel run //examples/java-app/cli:invoice -- total
bazel run //examples/java-app/cli:invoice -- show INV-1001
bazel run //examples/java-app/cli:invoice -- --file path/to/invoices.json list
Without --file or INVOICE_FILE, invoice reads the invoices.json packaged in the billing jar, so the binary runs from any directory. Relative --file paths resolve against the directory you ran bazel run from. Quantities and prices must be whole numbers from 0 to 1000000.
dx generate owns the billing and cli libraries. Main.java, tests and diagnostics stay handwritten.
Exit codes: 0 ok, 2 usage, 3 unreadable or malformed invoice file, 4 unknown invoice. exit_codes_test runs the binary through each code.
gson and guava come from the locked @maven hub. classpath_test checks that the app classes load from the billing jar and the library classes from their own jars. Tests use JUnit 4. Sources build at the repository Java language level with -Xlint:all -Werror.
format_test checks the sources against the pinned google-java-format. diagnostics_test compiles, lints and formats files with a syntax error, a type error, a compiler warning, an unused import and bad formatting, and expects each to fail. It also compiles the billing sources: they fail without gson or guava on the classpath and pass with both.
Kotlin gradebook example
A multi-package Kotlin app. It has a gradebook CLI, a grades library that reads JSON through gson and groups students through guava, a generated BuildInfo object and a packaged grades.json resource.
| Package | Role |
|---|---|
grades | Student model, JSON reader, ranking, generated BuildInfo, grades.json |
cli | grades binary with list, ranks, letters, schema and show NAME |
bazel build //examples/kotlin-app/...
bazel test //examples/kotlin-app/...
bazel run //:dx -- generate --check //examples/kotlin-app/...
bazel run //examples/kotlin-app/cli:grades -- ranks
bazel run //examples/kotlin-app/cli:grades -- show ada
bazel run //examples/kotlin-app/cli:grades -- --file path/to/grades.json list
Without --file or GRADES_FILE, grades reads the grades.json packaged in the grades jar, so the binary runs from any directory. Relative --file paths resolve against the directory you ran bazel run from. Scores must be whole numbers from 0 to 100, and every student needs at least one.
dx generate owns the grades and cli libraries. Main.kt, tests and diagnostics stay handwritten.
Exit codes: 0 ok, 2 usage, 3 unreadable or malformed grade file, 4 unknown student. exit_codes_test runs the binary through each code.
gson and guava come from the locked @maven hub. classpath_test checks that the app classes load from the grades jar and the library classes from their own jars. Tests use JUnit 4. Kotlin warnings are errors.
format_test checks the sources against the pinned ktfmt in Kotlin-lang style. diagnostics_test compiles, lints and formats files with a syntax error, a type error, a compiler warning, a wildcard import and bad formatting, and expects each to fail. It also compiles the grades sources: they fail without gson or guava on the classpath and pass with both.
MSIX example
A Windows console app packaged as an MSIX. The app reads a packaged resource, calls a function from a DLL in another package and prints its package identity.
| Package | Role |
|---|---|
app | dxmsix.exe, greeting.txt, logo, notice, the app package and the app_sign signer |
greet | greet.dll |
Build on a Windows host:
bazel build //examples/msix-app/app
app needs the Windows SDK Build Tools license accepted. rules_dx accepts it
for its own builds.
Install and run it with Developer Mode on, in Windows PowerShell:
mkdir $env:TEMP\dxmsix
tar -xf bazel-bin\examples\msix-app\app\app.msix -C $env:TEMP\dxmsix
Add-AppxPackage -Register $env:TEMP\dxmsix\AppxManifest.xml
dxmsix.exe
Get-AppxPackage -Name DxMsix.Example | Remove-AppxPackage
dxmsix.exe prints:
resource=hello from the packaged resource
library=greeting from greet.dll
package=DxMsix.Example_1.2.3.0_x64__kg15hasbevky6
probe=off
Building with --copt=-DDX_CONSUMER_PROBE prints probe=on.
Exit codes: 0 ok, 2 the executable path is unknown, 3 the resource is
missing, 4 the resource is empty. Without greet.dll Windows stops the app
before it starts.
Sign a copy with a test certificate:
bazel run //examples/msix-app/app:app_sign -- -Pfx C:\certs\test.pfx -Out C:\out\dxmsix.msix
//msix/tests builds, inspects, installs and signs this example.
Node app example
A multi-package TypeScript and JavaScript app. It has a package registry CLI, a registry library that checks versions through semver, a generated build_info module and a packaged packages.json resource.
| Package | Role |
|---|---|
registry | Typed registry model, semver helpers, generated build_info.ts, packages.json |
cli | registry binary with list, latest, next, satisfy, schema and where |
diagnostics | Files with deliberate errors, and the checks that must reject them |
bazel build //examples/node-app/...
bazel test //examples/node-app/...
bazel run //examples/node-app/cli:registry -- list
bazel run //examples/node-app/cli:registry -- next core minor
bazel run //examples/node-app/cli:registry -- satisfy net '^2.0.0'
bazel run //examples/node-app/cli:registry -- --file path/to/packages.json list
Without --file or REGISTRY_FILE, registry reads the packaged packages.json, so the binary runs from any directory. Relative --file paths resolve against the directory you ran bazel run from. latest and next ignore prerelease versions.
Exit codes: 0 ok, 2 usage or invalid range, 3 unreadable or malformed registry file, 4 unknown package or no matching version. exit_codes_test runs the binary through each code. registry where prints the entry script, the data file and the semver directory the binary resolved.
Tests use both runners. registry_test and cli_test run Jest. plain_test runs node:test. Every TypeScript target also gets a typecheck test with isolatedDeclarations. semver and the test tools come from the locked npm_node_app hub. cli imports registry as the workspace package @node-app/registry, so the lock covers both package.json files. Refresh it with pnpm install --lockfile-only in a copy of package.json, registry/package.json and pnpm-workspace.yaml.
Sources use the repo Biome settings. format_test checks every source and config file against the pinned Biome. diagnostics_test runs tsc and Biome on files with a syntax error, a type error, an inferred export under isolatedDeclarations, an import of a package that is not installed, a lint finding in .ts and .js, and bad formatting, and expects each to fail.
bazel coverage //examples/node-app/... writes LCOV for the Jest tests: registry.js, build_info.js and cli.js. coverage_test checks each report names those files with executed lines. plain_test runs node:test and reports none.
Perl app example
A Perl stock CLI split into three packages. Every target runs on the relocatable Perl 5.40 that rules_perl pins, with nothing installed on the host.
| Package | Role |
|---|---|
inventory | Inventory::Item and Inventory::Ledger, a stock ledger with a per-item limit |
report | Unit table from data/units.csv read through runfiles, and CSV output through Text::CSV |
cli | stockctl command line and a launcher test |
bazel build //examples/perl-app/...
bazel test //examples/perl-app/...
bazel run //examples/perl-app/cli:stockctl -- "add 2 dozen pears" "count pears"
bazel test //examples/perl-app/... --//examples/perl-app/cli:limit=144
The run prints 24, 24 and the report name,units / pears,24. A command
that fails prints error: ... and exits 1. The limit flag selects the
per-item limit through select() on env, and the launcher test follows it.
Text::CSV 2.06 comes from CPAN through the rules_perl cpan extension. Its
lock is third_party/cpanfile.snapshot.lock.json, and every distribution
carries a sha256. Text::CSV runs on its pure-Perl Text::CSV_PP backend. To
change the pin, edit third_party/cpanfile and run
bazel run //examples/perl-app/third_party:compiler.
| Platform | Status |
|---|---|
| linux_x86_64 | Qualified |
| linux_arm64, macos_arm64, windows_x86_64 | Not qualified |
| windows_arm64 | Unsupported |
Python service example
A multi-package Python app. It has a click command line, a local JSON service, a generated build_info module, packaged item data, and a pinned uv.lock.
| Package | Role |
|---|---|
ledger/core | Catalog, config, packaged items.json, generated build_info.py |
ledger/service | JSON server on 127.0.0.1 |
ledger/cli | ledger binary with info, list, quote and serve |
bazel build //examples/python-service/...
bazel test //examples/python-service/...
bazel run //examples/python-service/ledger/cli:ledger -- list
bazel run //examples/python-service/ledger/cli:ledger -- quote bolt-m6 --qty 4
bazel run //examples/python-service/ledger/cli:ledger -- serve --port 8080
Config comes from --config FILE or LEDGER_CONFIG. Keys: currency, tax_percent, items_path.
Tests use both runners: catalog_test, server_test, cli_test and broken_test run pytest. build_info_test and cli_unittest_test run unittest. build_info_test sets PIP_NO_INDEX and UV_OFFLINE, and click comes from the locked pypi hub.
Refresh the lock with uv lock --directory examples/python-service.
bazel test //python/tests/consumer:service_consumer_test
Qt app example
A headless Qt 6 app with a QML module, a C++ QML type, a Qt resource, a
Widgets form and QtTest and Qt Quick Test suites. It builds against the pinned @dx_qt SDK on Linux x86_64.
| Package | Role |
|---|---|
tally | qt_qml_module with URI Tally: Counter C++ type, Main.qml and Badge.qml |
app | tally binary, counter_test and panel_test QtTest suites, tally_quick_test Qt Quick Test suite |
bazel build //examples/qt-app/...
bazel test //examples/qt-app/...
bazel run //examples/qt-app/app:tally -- 3
The run prints the banner.txt resource, loads Tally/Main, bumps the
counter three times and prints tally 46.
counter_test checks the C++ type, evaluates the Tally module from its
embedded resources and loads QtQml.Models from the pinned import tree.
panel_test loads the panel.ui form, clicks its button and renders the
label offscreen. tally_quick_test runs tst_tally.qml, which drives the
Tally module and grabs a rendered frame with the software backend.
Qt Rust example
A headless Qt 6 ledger whose model is Rust. QML calls the Rust type,
reads its properties and handles its signal. It builds against the pinned
@dx_qt SDK on Linux x86_64 and packages into a relocatable archive.
| Package | Role |
|---|---|
ledger | ledger crate: positive amounts, a running total and an Adler-32 checksum from the pinned adler2 crate |
model | Ledger.Model: LedgerModel, a CXX-Qt QObject with read-only count, total and checksum properties, an add invokable and a rejected signal |
ui | Ledger.Ui: Main.qml calls add and records each rejected signal |
app | ledger binary, ledger_test QtTest suite, runtime bundle and ledger_release archive |
bazel build //examples/qt-rust/...
bazel test //examples/qt-rust/...
bazel run //examples/qt-rust/app:ledger -- 5 -2 7
ledger takes one or more integer amounts. It prints added <n> or
rejected <n> per amount, then a summary such as
2 entries, total 12, adler32 004c000d.
Exit codes: 0 every amount added, 1 the QML module failed to load, 2
usage error, 3 at least one amount was rejected.
Package
bazel build //examples/qt-rust/app:ledger_release_archive
mkdir /tmp/ledger && tar -xzf bazel-bin/examples/qt-rust/app/ledger_release.tar.gz -C /tmp/ledger
env -i /tmp/ledger/ledger 5 7
The archive holds ledger, NOTICE, IDENTITY.txt and resources/ with
the Qt libraries, the QML and QtQml import modules and the license texts.
The Rust code is linked into ledger. Only glibc 2.34 or newer comes from
the host.
Qt tasks example
A Qt 6 task browser. Four QML modules, C++ models, a Qt Quick board, a pinned
fmt dependency, a German translation and JSON storage. It builds against the
pinned @dx_qt SDK on Linux x86_64 and packages into relocatable archives.
| Package | Role |
|---|---|
store | Tasks.Store: TaskModel, a list model with signals and JSON load and save |
catalog | Tasks.Catalog: AssetCatalog, read from the embedded assets.json resource |
browser | Tasks.Browser: Main.qml drives both models; tasks_de.ts compiles to :/i18n/tasks_de.qm |
board | Tasks.Board: Board.qml and TaskRow.qml, a Qt Quick list of tasks with click and keyboard toggling |
app | tasks and board binaries, browser_test QtTest suite, board_quick_test Qt Quick Test suite, runtime and board_runtime bundles, tasks_release and board_release archives |
bazel build //examples/qt-tasks/...
bazel test //examples/qt-tasks/...
bazel run //examples/qt-tasks/app:tasks -- --store /tmp/tasks.json add "Draw logo" logo
bazel run //examples/qt-tasks/app:tasks -- --lang de --store /tmp/tasks.json list
tasks takes [--lang <code>] --store <path> and one command:
add <title> <asset>adds a task for a catalog asset.done <row>toggles a task.listprints every task.
Each command prints the summary line, such as 1 of 2 tasks open.
Exit codes: 0 success, 1 the QML module failed to load, 2 usage error,
unknown asset or row, missing translation or missing catalog resource, 3
storage read or write failure.
board --store <path> <image> renders the task board to an image, such as
board.png or board.ppm, and prints <image> <width>x<height> <platform>.
It always uses software rendering. It uses the offscreen platform
unless QT_QPA_PLATFORM is set.
bazel run //examples/qt-tasks/app:board -- --store /tmp/tasks.json /tmp/board.png
Board exit codes: 0 success, 1 the QML module failed to load, 2 usage
error, 3 storage read failure, 4 image write failure.
board_quick_test runs tst_board.qml. It clicks the boxes, moves the
highlight with the arrow keys, toggles with Space, checks the rendered pixels
and reloads the saved JSON from a temporary directory.
Package
bazel build //examples/qt-tasks/app:tasks_release_archive
mkdir /tmp/tasks && tar -xzf bazel-bin/examples/qt-tasks/app/tasks_release.tar.gz -C /tmp/tasks
env -i /tmp/tasks/tasks --store /tmp/tasks/tasks.json list
The archive holds tasks, NOTICE, IDENTITY.txt and resources/ with
the libraries tasks links and needs, the QML and QtQml import modules and
the license texts for Qt, fmt and LLVM. Only glibc 2.34 or newer comes from the host.
bazel build //examples/qt-tasks/app:board_release_archive
mkdir /tmp/board && tar -xzf bazel-bin/examples/qt-tasks/app/board_release.tar.gz -C /tmp/board
env -i /tmp/board/board --store /tmp/tasks.json /tmp/board/board.png
The board archive also holds the minimal, offscreen, xcb and wayland
platform plugins under resources/plugins/, with the libraries they need, the
DejaVu Sans fonts under resources/fonts/ and the QtQuick import modules.
board loads those fonts, so it needs no fontconfig setup on the host.
To render on a display, set the platform. It needs a running X or Wayland server:
DISPLAY=:0 QT_QPA_PLATFORM=xcb /tmp/board/board --store /tmp/tasks.json /tmp/board/xcb.png
WAYLAND_DISPLAY=wayland-0 QT_QPA_PLATFORM=wayland /tmp/board/board --store /tmp/tasks.json /tmp/board/wayland.png
Run the display check on a host with a display:
DX_QT_DISPLAYS=xcb,wayland bazel test //qt/tests:qt_display_test
Shared Rust task board
One Rust task board runs as a native CLI, a browser app, a WASI module, and an Android NativeActivity. Every output loads the same board and asset files, applies the same commands, and prints the same replies.
bazel build //examples/shared-rust/... @shared_consumer//...
bazel test //examples/shared-rust/... @shared_consumer//...
Layout
| Crate or target | Role |
|---|---|
tasks_model | Task and board parsing, title and asset checks. |
tasks_core | Board commands, summaries, and asset digests through the pinned adler2 2.0.1 crate. |
tasks | Native and WASI CLI: tasks [--asset=PATH]... BOARD [COMMAND].... |
shared_tasks | wasm-bindgen interface (WebBoard, asset_report, core_version, build_profile). |
tasks_ffi, tasks_interface | C interface (ffi/tasks.h) over the same core. |
tasks_activity, tasks_apk | Android NativeActivity and APK. |
assets/board.txt, assets/notes.txt | Resources every output loads. |
Commands are add TITLE, done ID, reopen ID, attach ID ASSET, and
remove ID. Replies are ok:<event> or err:<kind>.
The CLI exits 0 on success, 1 when a command is rejected, and 2 for a usage error, an unreadable or invalid board, or a missing asset.
Native
bazel run //examples/shared-rust:tasks -- \
--asset=$PWD/examples/shared-rust/assets/notes.txt \
$PWD/examples/shared-rust/assets/board.txt "add Ship it" "done 2"
bazel test //examples/shared-rust:native_test //examples/shared-rust:interface_test
native_test copies the binary and assets to a scratch directory and
checks output, exit codes, and error text. interface_test drives the
C interface.
Browser
bazel run //examples/shared-rust:serve
bazel test //examples/shared-rust:asset_test //examples/shared-rust:browser_test
browser_test serves the relocated release app in Firefox 157.0. It
checks the build profile is release, the rendered tasks, summary,
asset digest, and load error, then
applies commands through the input and button.
WASI
bazel test //examples/shared-rust:wasi_test \
//examples/shared-rust:wasi_error_test \
//examples/shared-rust:wasi_capability_test
tasks_wasi is tasks built for wasm32-wasip1 and run under
Wasmtime 49.0.2. wasi_error_test expects exit 1 for a rejected
command. wasi_capability_test expects exit 2 and
err:unreadable: /board/board.txt when the board is not preopened.
Android
export ANDROID_HOME=$HOME/Android/Sdk ANDROID_NDK_REVISION=27.2.12479018
NDK=--extra_toolchains=@android_ndk_toolchain//:ndk_cc_toolchain_device,@android_ndk_toolchain//:ndk_cc_toolchain_emulator
bazel test $NDK --test_env=ANDROID_HOME \
//examples/shared-rust:inspect_apk_test \
//examples/shared-rust:inspect_consumer_apk_test
bazel run $NDK //examples/shared-rust:install
bazel run $NDK //examples/shared-rust:start
bazel run $NDK //examples/shared-rust:log
The inspect tests check the package, SDK floor, label, launch activity,
byte-identical assets, exported symbols per ABI, no extra ABIs, and the
signature. The activity logs under the DxSharedTasks tag:
lifecycle=create through lifecycle=destroy, board=loaded tasks=4
or board=invalid, the summary or summary none, the asset report, and input=touch|key <reply> after a
tap or key press.
Tools: NDK 27.2.12479018, build-tools 34.0.0, platform-tools 37.0.0,
emulator 37.2.12.0, system image android-34;default;x86_64.
Release
bazel test //examples/shared-rust:release_test
tasks_release, tasks_wasi_release, and app_release build with
-c opt. release_test relocates the native binary and WASI module and
expects identical output plus build=release. It also runs the WASI
module with no preopen and expects err:unreadable: /board/board.txt
with exit code 2.
Deploy
dx deploy //examples/shared-rust:tasks_deploy
dx deploy --apply //examples/shared-rust:tasks_deploy
bazel test //examples/shared-rust:tasks_deploy_verify
tasks_deploy is an
archive_deploy of tasks.
The first command builds it and publishes nothing. --apply checks the
tarball against its checksum and copies tasks_deploy.tar.gz and
tasks_deploy.tar.gz.sha256 into the workspace root, or into the directory
given after --:
archive_deploy: released tasks (profile: release)
tarball: /path/to/rules_dx/tasks_deploy.tar.gz
checksum: /path/to/rules_dx/tasks_deploy.tar.gz.sha256
tasks_deploy_verify checks that the tarball holds tasks and matches the
checksum.
Independent consumer
@shared_consumer is a separate workspace that owns its HTML, board,
notes, Android resources, permissions, features, rustc flags, and
WASI runtime wrapper. It reuses tasks_core, shared_tasks,
tasks_activity, and the public toolchains with no SDK or linker setup
of its own.
bazel test @shared_consumer//... \
//examples/shared-rust:consumer_browser_test \
//examples/shared-rust:consumer_native_test
Offline and errors
bazel fetch //examples/shared-rust/... @shared_consumer//...
bazel test //examples/shared-rust/... @shared_consumer//... \
--repository_disable_download
Tests tagged block-network run without network. With an empty
repository cache, --repository_disable_download fails with
download is disabled. missing_interface_test expects a web app
without its bindgen interface to fail with
requires bindgen to provide RustWasmBindgenInfo.
missing_asset_test and bad_asset_path_test cover asset errors.
Pending
- Custom toolchain override in the consumer: it overrides features,
rustcflags, and the WASI runtime wrapper only. - Automated offline check: the fetch and
--repository_disable_downloadcommands above are not run by any test. - Emulator run: needs KVM. Without it the emulator boot loops.
- Physical arm64 device run.
- Multi-ABI install on a device.
- Apple simulator and device outputs.
Debugger integration and game-engine portability are not supported.
Zig manifest example
A multi-package Zig app. It has a manifest CLI, a catalog module with a generated schema module and an embedded items.tsv, and a digest module that calls C through a pinned xxHash dependency.
| Package | Role |
|---|---|
catalog | TSV parser and totals, generated schema.zig, embedded items.tsv |
digest | C wrapper over xxHash, imported from Zig as @import("c") |
cli | manifest binary and report formatter |
diagnostics | Compiler and zig fmt failures |
bazel build //examples/zig-app/...
bazel test //examples/zig-app/...
bazel run //examples/zig-app/cli:manifest
bazel run //examples/zig-app/cli:manifest -- get cable
manifest prints each item, the total, the schema version and an XXH64 digest of the item lines. get NAME prints one item.
Exit codes: 0 ok, 2 usage, 4 unknown item. exit_codes_test runs the binary through each code.
Zig comes from the pinned rules_zig toolchain. xxHash is a pinned http_archive declared in MODULE.bazel. catalog_test, digest_test and report_test are Zig tests. catalog_test covers malformed input.
diagnostics_test compiles files with a syntax error, a type error and an unused local, and expects each to fail. It also checks every example source with zig fmt --check.