Debug Bazel rules_go projects
Case study: Buildozer
The other day I needed to improve buildozer
a little
and naturally wanted to run buildozer
in the dlv
debugger.
I learn by doing and think through debugging.
Where I would previously use the execroot trick
to debug programs that operate on absolute paths
it did not work for this.
buildozer
operates on the current working directory
so executing it from within Bazel's output directory does not work.
Instead I used the substitute path technique from rules_go,
thanks Danny!
This article is a little show-case
for how you can easily do the same in your terminal
and with configuration your IDE too.
It leverages several cool dlv
features.
I will show you how to:
- Debug
buildozer
- Find all the debug symbols
- Manage debug symbols for multiple go projects in different bazel projects.
The techniques are general and work for many languages,
only the tooling differs.
You can do the same for gdb
and rr
when building C/C++ code.
Substitute path
Substitute path is a common debugger command to change where to look for debug symbols. Programs compiled with debug symbols have paths to the source files, which often works well. But binaries built by Bazel need some extra care. For its sandbox and execroot directory structure does not look like the source code tree.
One big source of confusion is the external/
directory
where all external code is put in a flat hierarchy.
The code could come from any remote repository,
or even be bundled with the source code of the program
at an arbitrary location in the source tree.
For the buildtools project we use the following substitute paths
with the <BUILDTOOLS>
placeholder for the repository path.
bazel-buildtools
is the convenience symlink to the execroot,
if you do not build them instead use $(bazel info output_path)/..
to find the path.
With WORKSPACE:
(dlv) config substitute-path external/ <BUILDTOOLS>/bazel-buildtools/external
(dlv) config substitute-path GOROOT/ <BUILDTOOLS>/bazel-buildtools/external/go-sdk
(dlv) config substitute-path "" <BUILDTOOLS>/bazel-buildtools/
With MODULE.bazel the go sdk has a qualified name: rules_go~<VERSION>~go_sdk~go_default_sdk
(dlv) config substitute-path GOROOT/ <BUILDTOOLS>/bazel-buildtools/external/rules_go~0.46.0~go_sdk~go_default_sdk
Initialize delve with project-specific settings
You could use dlv
's configuration file to save these.
If you only work with one project that works fine.
The config file is singular for your computer:
$XDG_CONFIG_HOME/dlv/config.yml
or ~/.dlv/config.yml
.
It is a natural location for your debugging preferences,
but not to describe how individual projects work.
The config file is documented here.
To set settings for a specific project
use the --init <command file>
flag.
You can provide any debugger commands here,
it will initialize the debugging session
as if they were written interactively.
First, compile and store the path information
we need to debug buildozer
.
buildtools $ export BUILDTOOLS=$PWD
buildtools $ bazel build -c dbg //buildozer
Target //buildozer:buildozer up-to-date:
bazel-bin/buildozer/buildozer_/buildozer
buildtools $ export BUILDOZER="$(readlink bazel-bin/buildozer/buildozer_/buildozer)"
# We can now run 'buildozer' in our project.
buildtools $ cd /path/to/project
Then, open buildozer
in the debugger.
This uses the shell pseudo-file redirection to in-line the commands.
You can also save them to a file for the project.
- Bash
- Fish
project $ dlv exec --init <(echo '
config substitute-path external/ '"$BUILDTOOLS"'/bazel-buildtools/external
config substitute-path GOROOT/ '"$BUILDTOOLS"'/bazel-buildtools/external/go-sdk
config substitute-path "" '"$BUILDTOOLS"'/bazel-buildtools/
print "You can also set break points and automatically start the program."
break main.main
continue
') "$BUILDOZER" -- -h
# Create and use 'substitute-path.dlv' for the buildtools project.
project $ dlv exec --init "$BUILDTOOLS"/substitute-path.dlv "$BUILDOZER" -- -h
# Augment it with ad-hoc commands.
project $ dlv exec --init <( {
cat "$BUILDTOOLS"/substitute-path.dlv
echo 'break main.main
continue'
} ) $BUILDOZER -- -h
project $ dlv exec --init (echo '
config substitute-path external/ '"$BUILDTOOLS"'/bazel-buildtools/external
config substitute-path GOROOT/ '"$BUILDTOOLS"'/bazel-buildtools/external/go-sdk
config substitute-path "" '"$BUILDTOOLS"'/bazel-buildtools/
print "You can also set break points and automatically start the program."
break main.main
continue
' | psub) "$BUILDOZER" -- -h
# Create and use 'substitute-path.dlv' for the buildtools project.
project $ dlv exec --init "$BUILDTOOLS"/substitute-path.dlv "$BUILDOZER" -- -h
# Augment it with ad-hoc commands.
project $ dlv exec --init ( begin
cat "$BUILDTOOLS"/substitute-path.dlv
echo 'break main.main
continue'
end | psub ) $BUILDOZER -- -h
Execroot trick
If the execution directory of the program does not matter
you can execute it directly from the execroot,
where all source files are available as they were during compilation in the sandbox.
So the debug symbol paths are correct.
First build, then translate the bazel-bin
convenience symlink target
to the real bazel-out
path
(bazel-bin
is not available in the execroot).
$ bazel build -c dbg //buildozer
Target //buildozer:buildozer up-to-date:
bazel-bin/buildozer/buildozer_/buildozer
$ cd bazel-buildtools
$ dlv exec bazel-out/k8-dbg/bin/buildozer/buildozer_/buildozer -- -h
(dlv) b main.main
Breakpoint 1 set at 0x6e0a32 for main.main() buildozer/main.go:80
(dlv) c
main.main() buildozer/main.go:80 (hits goroutine(1):1 total:1) (PC: 0x6e0a32)
...
=> 80: func main() {
81: flag.Var(&commandsFiles, "f", "file name(s) to read commands from, use '-' for stdin (format:|-separated command line arguments to buildozer, excluding flags)")
No settings are needed for dlv
.
Finding the bazel-out
path is easy with readlink
.
This is the permanent output location,
until you change the program or clean away all artifacts.
bazel-bin
is just the last build and is frequently rewritten.
buildtools $ readlink bazel-bin
.../9cc0cee4950d14fcd8254d5df1538d8e/execroot/buildtools/bazel-out/k8-dbg/bin
We need bazel-out/k8-dbg/bin
.