There comes a time when working with Bazel when we want to understand the command-line flags used to build our code. For example, you might want to see what flags are being passed to swiftc. Up until Bazel 9, we would typically rely on --subcommands, but it could get quite verbose.

Action graph query

In addition to the standard bazel query command, there are also bazel cquery (configurable query) and bazel aquery (action graph query). Each of these helps us explore different parts of the build graph. Since we’re interested in inspecting command-line flags, aquery is the right tool—it exposes all declared actions, including the exact commands being executed.

For a project like this iOS template, we can explore how Swift code is compiled by running:

bazel aquery //app:app.library --output=commands

Which produces output like:

bazel-out/darwin_arm64-opt-exec/bin/external/rules_swift+/tools/worker/worker swiftc -target arm64-apple-macos12.6 -sdk __BAZEL_XCODE_SDKROOT__ -file-prefix-map '__BAZEL_XCODE_DEVELOPER_DIR__=/PLACEHOLDER_DEVELOPER_DIR' '-Xwrapped-swift=-bazel-target-label=@@//app:app.library' -emit-object -output-file-map bazel-out/darwin_arm64-fastbuild/bin/app/app.library.output_file_map.json -Xfrontend -no-clang-module-breadcrumbs -emit-module-path bazel-out/darwin_arm64-fastbuild/bin/app/app.swiftmodule '-enforce-exclusivity=checked' -emit-const-values-path bazel-out/darwin_arm64-fastbuild/bin/app/app.library_objs/source/ContentView.swift.swiftconstvalues -Xfrontend -const-gather-protocols-file -Xfrontend external/rules_swift+/swift/toolchains/config/const_protocols_to_gather.json -DDEBUG -Onone -Xfrontend -internalize-at-link -Xfrontend -no-serialize-debugging-options -enable-testing -disable-sandbox -gline-tables-only '-Xwrapped-swift=-file-prefix-pwd-is-dot' -file-prefix-map '__BAZEL_XCODE_DEVELOPER_DIR__=/PLACEHOLDER_DEVELOPER_DIR' -file-compilation-dir . -module-cache-path bazel-out/darwin_arm64-fastbuild/bin/_swift_module_cache -Ibazel-out/darwin_arm64-fastbuild/bin/modules/Models -Ibazel-out/darwin_arm64-fastbuild/bin/modules/API '-Xwrapped-swift=-macro-expansion-dir=bazel-out/darwin_arm64-fastbuild/bin/app/app.library.macro-expansions' -Xcc -iquote. -Xcc -iquotebazel-out/darwin_arm64-fastbuild/bin -Xfrontend -color-diagnostics -enable-batch-mode -module-name app -index-store-path bazel-out/darwin_arm64-fastbuild/bin/app/app.library.indexstore -index-ignore-system-modules '-Xwrapped-swift=-global-index-store-import-path=bazel-out/_global_index_store' -enable-bare-slash-regex -Xfrontend -disable-clang-spi -enable-experimental-feature AccessLevelOnImport -parse-as-library -static -Xcc -O0 -Xcc '-DDEBUG=1' -Xfrontend '-checked-async-objc-bridging=on' app/source/ContentView.swift app/source/MainApp.swift
...

At first glance, this output looks overwhelming. But if you break it down, it’s simply Bazel invoking tools with the appropriate flags.

Doing something useful

While this output can help us understand what is being executed and how, it becomes much more powerful when used comparatively.

One practical approach is to diff this output across ruleset versions or Bazel releases. For example:

bazel aquery //app:app.library --output=commands > commands.txt

You can generate one file per version and use standard diffing tools to spot regressions or better understand what changed between versions.

Making it executable

In a Bazel 9 video by aspect.build, Alex Eagle shared an interesting idea: turning aquery output into an executable shell script.

That idea is what got me intrigued. While the output isn’t directly executable, it seems feasible to get there by replacing placeholder variables, adjusting formatting, and fiddling with cwd. With a bit of effort, this could become a powerful debugging tool.

Conclusion

This is a small quality-of-life improvement in Bazel 9, but it unlocks a very practical debugging technique.