Search code examples
bazel

Idiomatic retrieval of the Bazel execution path


I'm working on my first custom Bazel rules. The rules allow the running of bats command line tests.

I've included the rule definition below verbatim. I'm pretty happy with it so far but there's one part which feels really ugly and non-standard. If the rule user adds a binary dependency to the rule then I make sure that the binary appears on the PATH so that it can be tested. At the moment I do this by making a list of the binary paths and then appending them with $PWD which is expanded inside the script to the complete execution path. This feels hacky and error prone.

Is there a more idiomatic way to do this? I don't believe I can access the execution path in the rule due to it not being created until the execution phase.

Thanks for your help!

BATS_REPOSITORY_BUILD_FILE = """
package(default_visibility = [ "//visibility:public" ])
sh_binary(
  name = "bats",
  srcs = ["libexec/bats"],
  data = [
    "libexec/bats-exec-suite",
    "libexec/bats-exec-test",
    "libexec/bats-format-tap-stream",
    "libexec/bats-preprocess",
  ],
)
"""

def bats_repositories(version="v0.4.0"):
    native.new_git_repository(
      name = "bats",
      remote = "https://github.com/sstephenson/bats",
      tag = version,
      build_file_content = BATS_REPOSITORY_BUILD_FILE
    )

BASH_TEMPLATE = """
#!/usr/bin/env bash
set -e
export TMPDIR="$TEST_TMPDIR"
export PATH="{bats_bins_path}":$PATH
"{bats}" "{test_paths}"
"""

def _dirname(path):
  prefix, _, _ = path.rpartition("/")
  return prefix.rstrip("/")

def _bats_test_impl(ctx):
  runfiles = ctx.runfiles(
      files = ctx.files.srcs,
      collect_data = True,
  )

  tests = [f.short_path for f in ctx.files.srcs]
  path = ["$PWD/" + _dirname(b.short_path) for b in ctx.files.deps]

  sep = ctx.configuration.host_path_separator

  ctx.file_action(
      output = ctx.outputs.executable,
      executable = True,
      content = BASH_TEMPLATE.format(
          bats = ctx.executable._bats.short_path,
          test_paths = " ".join(tests),
          bats_bins_path = sep.join(path),
      ),
  )

  runfiles = runfiles.merge(ctx.attr._bats.default_runfiles)

  return DefaultInfo(
      runfiles = runfiles,
  )

bats_test = rule(
    attrs = {
        "srcs": attr.label_list(
            allow_files = True,
        ),
        "deps": attr.label_list(),
        "_bats": attr.label(
            default = Label("@bats//:bats"),
            executable = True,
            cfg = "host",
        ),
    },
    test = True,
    implementation = _bats_test_impl,
)

Solution

  • This should be easy to support from Bazel 0.8.0 which will be released in ~2 weeks.
    In your skylark implementation you should do ctx.expand_location(binary) where binary should be something like $(execpath :some-label) so you might want to just format the label you got from the user with the $(execpath) and bazel will make sure to give you the execution location of that label.

    Some relevant resources: $location expansion in Bazel
    https://github.com/bazelbuild/bazel/issues/2475 https://github.com/bazelbuild/bazel/commit/cff0dc94f6a8e16492adf54c88d0b26abe903d4c