I quite often use Bazel to build Go command line applications. One of the annoying aspects of the bazel workflow is that the binary is built in a sandbox - a completely separate directory structure. Although we can run the binary using bazel the current working directory of the running app is in the sandbox making it harder to use the app in your current directory.

Adding a command line flag to change the current working directory is another way to run your app through bazel in the right directory but this means changing the application to work around the issue.

Bazel builds and runs everything in a sandbox that is connected to the project workspace with a symlink (on linux and mac). This approach makes it a lot easier to keep the source tree clean of build artifacts but makes it hader to just run the built binary.

In go for example go build main.go generates an executable binary main in the current directory making it really easy to add this to source control, or some other inadvertent action. By working in a sandbox this is avoided. It does make running that generated binary a lot harder.

One solution to this problem is to have Bazel install the built binary onto the local path as an additional target for the command line binary BUILD.bzl script.

The local deploy rule takes the generated binary in the sandbox and copies it to a convenient location on your local path e.g. /usr/local/bin

Example BUILD.bzl

In this Go example build file as well as building a go library and binary the script also defines an 'instal' target that copies the binary to an installation directory.

load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
load("//rules:local-deploy.bzl", "local_deploy")    (1)
go_library(
    name = "golang_lib",
    srcs = ["main.go"],
    importpath = "github.com/grahambrooks/building-with-bazel/book/examples/golang",
    visibility = ["//visibility:private"],
)

go_binary(
    name = "golang",
    embed = [":golang_lib"],
    visibility = ["//visibility:public"],
)

local_deploy(
    name = "install", (2)
    srcs = [":golang"], (3)
)
1 Include the local deploy rule definition function into our build script
2 Call the rule something unique
3 Reference the built binary that is to be deployed to the target folder.

Defining a Bazel Rule

Bazel works with rules that are defined by calling the rule() method. The 'local_deploy' rule definition links the implementation method _local_deploy_impl and the expected input sources and target with a default value and type.

local_deploy = rule(
    executable = True, (1)
    implementation = _local_deploy_impl, (2)
    attrs = {
        "srcs": attr.label_list(allow_files = True), (3)
        "target": attr.string(default = "/usr/local/bin", doc = "Deployment target directory"), (4)
    },
)
1 Mark to be run using bazel run [label]. This attribute means that the rule implementation generates an executable that is run by bazel
2 Reference to the implementation function (see below)
3 The list of files to be deployed - typically the binaries generated by another rule
4 Overridable parameter that is built into the executable. The directory for the deployment

Local Deploy Implementation method

The bazel implementation function creates an executable script that is then run by bazel to do the actual installation. This is the heart of the implementation. The rule is reusable and parameterized in each BUILD.bzl file that uses it. The implementation method is run by bazel for each use and ends up generating a shell script that is run by bazel when the binary is (re)built.

def _local_deploy_impl(ctx):
    target = ctx.attr.target (1)
    shell_commands = ""

    for s in ctx.files.srcs:
        shell_commands += "echo Copying %s to %s\n" % (s.short_path, target)
        shell_commands += "sudo cp %s %s\n" % (s.short_path, target) (2)

    ctx.actions.write(  (3)
        output = ctx.outputs.executable,
        is_executable = True,
        content = shell_commands,
    )
    runfiles = ctx.runfiles(files = ctx.files.srcs) (4)
    return DefaultInfo( (5)
        executable = ctx.outputs.executable,
        runfiles = runfiles,
    )
1 Sets the rule to be executable.
2 The shell command that actually does the work copying the built binary into the given target
3 Generates a script that is run to install the built binary.
4 Sets the run files for the given input
5 The result of the rule - a scrpt that is run by bazel to install the binary.

Running the installation

Once the local_deploy target is added to the BUILD.bzl file we can call the installation script by adding the :install target.

> bazel run //golang:install

For some systems we have to use sudo to write to /usr/local/bin so you will be prompted for your password. Once installed and assuming that /usr/local/bin is on your path

> golang

runs the built application

Summary

Bazel is slightly different than most build systems that generate libraries and binaries into the local filesystem with the source. If we build something in the local filesystem we typically wrap the build in something like a makefile that then copies the generated files to install them locally. We could copy the files from the symlinked sandbox but this creates some pretty tight coupling between the bazel owned sandbox. A makefile for example would need to know the bazel sandbox directory structure which is based on the operating system architecture.

Having Bazel do the installation work is a lot simpler and avoids this coupling.