How to Copy Bazel Build Outputs to Your Source Directory

Copy Files from Bazel Output to Source

NOTE: This article was updated on 2022/01/31 to point to the new repository home for updatesrc.

Have you ever wanted to copy the output of a Bazel build step to your source directory (e.g., generated documentation, formatted source files)? Read on to learn how the updatesrc_update rule and the updatesrc_update_all macro in the cgrindel/bazel-starlib repository can make this super simple.

Table of Contents

Add updatesrc to Your Workspace.

The updatesrc rules live in cgrindel/bazel-starlib. Let’s add cgrindel/bazel-starlib to your WORKSPACE file.

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
    name = "cgrindel_bazel_starlib",
    sha256 = "fc2ee0fce914e3aee1a6af460d4ba1eed9d82e8125294d14e7d3f236d4a10a5d",
    strip_prefix = "bazel-starlib-0.3.2",
    urls = [
        "http://github.com/cgrindel/bazel-starlib/archive/v0.3.2.tar.gz",
    ],
)

load("@cgrindel_bazel_starlib//:deps.bzl", "bazel_starlib_dependencies")

bazel_starlib_dependencies()

load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")

bazel_skylib_workspace()

The above code will download the repository and its dependencies.

Specify the Files to Update

The updatesrc_update rule defines an executable Bazel target that copies specified files from the Bazel output to the workspace/source directory. There are two ways to select files to copy.

Option #1: Specify the Files Using the srcs and outs Attributes

This option is helpful if you are generating files with a genrule or something similar. In the same Bazel package as the genrule, you define an updatesrc_update target specifying the source files using the srcs attribute and the output files using the outs attribute. The source list and output list must result in file lists that have the same length, as the contents of the n-th index of the output list replace the contents of the n-th index of the source list.

In the following example, the genrule targets accept a source file (e.g., c.txt) and create a modified version (e.g., c.txt_modified). The updatesrc_update target defines the mapping between the output files and the source files.

load(
    "@cgrindel_bazel_starlib//updatesrc:defs.bzl",
    "updatesrc_update",
)

genrule(
    name = "c_modified",
    srcs = ["c.txt"],
    outs = ["c.txt_modified"],
    cmd = """\
echo "# Howdy" > $@
cat $(location c.txt) >> $@
""",
)

genrule(
    name = "d_modified",
    srcs = ["d.txt"],
    outs = ["d.txt_modified"],
    cmd = """\
echo "# Howdy" > $@
cat $(location d.txt) >> $@
""",
)

updatesrc_update(
    name = "update",
    srcs = ["c.txt", "d.txt"],
    outs = [":c_modified", ":d_modified"],
)

To copy the output files to the source directory, you execute the updatesrc_update target.

$ bazel run //path/to/pkg:update

A working example is in the examples directory of the repository.

Option #2: Custom Rule Provides UpdateSrcsInfo

The second option is for Bazel rule authors. This repository defines a provider called UpdateSrcsInfo. If a rule generates an output that can be copied to the source directory, the rule can return an instance of this provider. The UpdateSrcsInfo provider maps source files and with their corresponding output files.

In the following example, we define a rule called header. It adds header text to the top of a source file. In addition to returning a DefaultInfo provider, it returns an instance of an UpdateSrcsInfo provider with the output-source mapping.

# File: header/header.bzl

load(
    "@cgrindel_bazel_starlib//updatesrc:defs.bzl",
    "UpdateSrcsInfo",
    "update_srcs",
)

def _header_impl(ctx):
    outs = []
    updsrcs = []
    for src in ctx.files.srcs:
        out = ctx.actions.declare_file(src.basename + "_with_header")
        outs.append(out)
        updsrcs.append(update_srcs.create(src = src, out = out))
        ctx.actions.run(
            outputs = [out],
            inputs = [src],
            executable = ctx.executable._header_tool,
            arguments = [src.path, out.path, ctx.attr.header],
        )

    return [
        DefaultInfo(files = depset(outs)),
        UpdateSrcsInfo(update_srcs = depset(updsrcs)),
    ]

header = rule(
    implementation = _header_impl,
    attrs = {
        "srcs": attr.label_list(
            allow_files = True,
            mandatory = True,
        ),
        "header": attr.string(
            mandatory = True,
        ),
        "_header_tool": attr.label(
            default = "@simple_example//header:header.sh",
            executable = True,
            cfg = "host",
            allow_files = True,
        ),
    },
    doc = "Copies the output files to the workspace directory.",
)

A Bazel package that uses the rule looks like the following:

# File: path/to/pkg/BUILD.bazel

load("//header:header.bzl", "header")
load("@cgrindel_bazel_starlib//updatesrc:defs.bzl", "updatesrc_update")

header(
    name = "add_headers",
    srcs = glob(["*.txt"]),
    header = "# Super cool header",
)

updatesrc_update(
    name = "update",
    deps = [":add_headers"],
)

To copy the output files to the source directory, you execute the updatesrc_update target.

$ bazel run //path/to/pkg:update

A working example is in the examples directory of the repository.

Update All of the Files

As we mentioned above, each updatesrc_update rule is executable and running it will copy the files for that particular target. However, if you want to update all of the files in your repository, you will define an updatesrc_update_all target. The updatesrc_update_all macro defines an executable target that queries your repository for every updatesrc_update target and executes them one at a time.

At the root of your workspace, create a BUILD.bazel file if you don’t have one. Add the following:

load(
    "@cgrindel_bazel_starlib//updatesrc:defs.bzl",
    "updatesrc_update_all",
)

# Define a runnable target to execute all of the updatesrc_update targets
# that are defined in your workspace.
updatesrc_update_all(
    name = "update_all",
)

Then, update all of your mapped source files by executing the update_all target.

$ bazel run //:update_all

In addition to automatically executing all of the updatesrc_update targets, the updatesrc_update_all macro can be configured to run other executable targets. It is handy if you write a custom file-copy script and have everything updated with a simple command.

In the following example, we define an update_all target that executes all updatesrc_update targets and executes a custom target called //doc:update.

load(
    "@cgrindel_bazel_starlib//updatesrc:defs.bzl",
    "updatesrc_update_all",
)

updatesrc_update_all(
    name = "update_all",
    targets_to_run = [
        "//doc:update",
    ],
)

Then, as before, you run the update_all target.

$ bazel run //:update_all

Conclusion

This article demonstrated using updatesrc to copy files from Bazel output directories to your workspace/source directory.