Practical Bazel: Building Multiple Workspaces in CI
Practical Bazel bazel
Published: 2022-10-18
Practical Bazel: Building Multiple Workspaces in CI

For most Bazel projects, I strongly recommend using a single Bazel workspace per source code repository. However, it can be occasionally useful to nest multiple workspaces within a single repository. For example, when I’m writing Bazel rulesets, I will often create test cases that contain own workspace with a slightly different configuration in order to test various workspace-level configuration settings for the ruleset, while maintaining a root workspace which is the primary workspace for the ruleset.

For example, let’s say I’m writing a ruleset named rules_foo for compiling programs using a hypothetical programming language named foo. I want to write tests for this ruleset to see if it can successfully compile a program with a host-installed version of the compiler as well as with a version of the compiler the ruleset downloads itself. In order to do this, I’ll make one decision in the root WORKSPACE (usually to download the compiler) and then include a test with a WORKSPACE and a configuraiton that uses a host-installed compiler.

To define a separate Bazel workspace, simply create a file named WORKSPACE in the directory whcih denotes the root of the workspace. Beware of the following caveats:

  1. bazel build //... from the root workspace will build all targets, including the ones in the sub-workspaces. This means that all targets must build successfully with both the sub-workspace’s settings as well as the root workspace’s. When using this technique with Bazel rulesets, this means that the root workspace has the union of all settings necessary in order to build all targets, whereas sub-workspaces can have more specific or narrow settings.
  2. If you have paths in your .bazelrc, they will be relative to the current workspace, where instead you probably want them relative to the root workspace. This can be fixed with a build script (see below).

To make building multiple workspaces from a single CI/CD pipeline easier, I typically use a variation of the below script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/bin/bash
#
# build.sh: A single script which builds all the workspaces in
# a repository.

set -euo pipefail

cleanup() {
  if [[ -v TMP_BAZELRC ]]; then
    rm -f $TMP_BAZELRC
  fi
}
trap cleanup EXIT

# This script is used by CI which sets the BRANCH_NAME variable
if [[ ! -v BRANCH_NAME ]]; then
  echo "ERROR: Environment variable BRANCH_NAME is not set" 1>&2
  exit 1
fi

case "$BRANCH_NAME" in
  master) BUILD_CONFIG=jenkins_master ;;
  *)      BUILD_CONFIG=jenkins ;;
esac

ROOT=$(realpath $PWD)

# Build each workspace in the tree
for f in $(find . -name WORKSPACE -type f); do
  DIRNAME=$(realpath $(dirname $f))
  echo "Building $DIRNAME"

  # .bazelrc has path references to $PWD, which doesn't work when we're
  # building sub-workspaces.  If this happens, we need to create
  # a new .bazelrc with fixed-up path refernces and use that instead.
  if [[ "$DIRNAME" != "$ROOT" ]]; then
    TMP_BAZELRC=$(mktemp)
    sed -e "s#\$PWD#$ROOT#g" $ROOT/.bazelrc > $TMP_BAZELRC
    (cd $DIRNAME && bazel --bazelrc=$TMP_BAZELRC build --config=$BUILD_CONFIG //...)
  else
    (cd $DIRNAME && bazel build --config=$BUILD_CONFIG //...)
  fi
done