11

Say I have a custom rule, my_object. It looks like:

my_object(
  name = "foo",
  deps = [
    //services/image-A:push,
    //services/image-B:push,
  ]
)

Where the labels in deps are rules_docker's container_push rules.

I want to be able to bazel run //:foo and have it push the Docker images within the deps list. How do I do this?

This seems to be a specific case of just generally wanting to run the executables of other rules within the executable of a custom rule.

thundergolfer
  • 527
  • 1
  • 5
  • 18

2 Answers2

11

The thing to do here is to have my_object output an executable that executes the other executables.

Consider this example:

def _impl1(ctx):
  ctx.actions.write(
    output = ctx.outputs.executable,
    is_executable = True,
    content = "echo %s 123" % ctx.label.name)
  return DefaultInfo(executable = ctx.outputs.executable)


exec_rule1 = rule(
  implementation = _impl1,
  executable = True,
)


def _impl2(ctx):

  executable_paths = []
  runfiles = ctx.runfiles()
  for dep in ctx.attr.deps:
    # the "./" is needed if the executable is in the current directory
    # (i.e. in the workspace root)
    executable_paths.append("./" + dep.files_to_run.executable.short_path)
    # collect the runfiles of the other executables so their own runfiles
    # will be available when the top-level executable runs
    runfiles = runfiles.merge(dep.default_runfiles)

  ctx.actions.write(
    output = ctx.outputs.executable,
    is_executable = True,
    content = "\n".join(executable_paths))

  return DefaultInfo(
    executable = ctx.outputs.executable,
    runfiles = runfiles)


exec_rule2 = rule(
  implementation = _impl2,
  executable = True,
  attrs = {
    "deps": attr.label_list(),
  },
)

BUILD.bazel:

load(":defs.bzl", "exec_rule1", "exec_rule2")

exec_rule1(name = "foo")
exec_rule1(name = "bar")
exec_rule2(name = "baz", deps = [":foo", ":bar"])

and then running it:

$ bazel run //:baz
INFO: Analyzed target //:baz (4 packages loaded, 19 targets configured).
INFO: Found 1 target...
Target //:baz up-to-date:
  bazel-bin/baz
INFO: Elapsed time: 0.211s, Critical Path: 0.01s
INFO: 0 processes.
INFO: Build completed successfully, 6 total actions
INFO: Build completed successfully, 6 total actions
foo 123
bar 123
ahumesky
  • 4,203
  • 8
  • 12
  • Relevant guide in Bazel docs, https://docs.bazel.build/versions/3.4.0/skylark/rules.html#runfiles – user7610 Aug 06 '20 at 12:36
  • There is one caveat with this approach, though. This way you won't get the arguments defined in the target. See the note from https://docs.bazel.build/versions/main/be/common-definitions.html#binary.args. `NOTE: The arguments are not passed when you run the target outside of Bazel (for example, by manually executing the binary in bazel-bin/).` – hisener Jun 16 '22 at 13:23
  • For many rules, the value of the `args` attribute isn't embedded into the output binary, but in your rule implementation you can read `ctx.attr.args` and put that into the output binary / output wrapper as needed. – ahumesky Jun 16 '22 at 18:34
  • Though, really, it might be better to add a separate attribute, like `embedded_args` or something, to distinguish it from `args` and its documented behavior. – ahumesky Jun 16 '22 at 18:48
  • `ctx.attr.args` would give the args for the `baz` target, right? `embedded_args` might be a solution, but since there are multiple `deps`, I am not sure it is a good solution. – hisener Jun 29 '22 at 09:07
  • `ctx.attr.args` gives the value of the args attribute for that target, correct. There are different ways to architect it. The most straight forward is probably for those targets in the top-level deps to have their own embeded_args to embed in their own wrapper scripts, which get called at the top-level. Another way is to add a provider to the rules in the top-level deps which contains the args from those targets, which the top-level target reads and assembles into its wrapper scripts. Probably a lot of args to manage and assemble. – ahumesky Jun 29 '22 at 20:34
0

I managed to achieve this by implementing DefaultInfo in rule.

def build_all_impl(ctx):
    targets = ctx.attr.targets
    run_files = []
    for target in targets:
        run_files = run_files + target.files.to_list()
    DefaultInfo(
        runfiles = ctx.runfiles(run_files),
    )

build_all = rule(
    implementation = build_all_impl,
    attrs = {
        "targets": attr.label_list(
            doc = "target to build",
        ),
    },
)

And then by running build_all rule

build_all(
    name = "all",
    targets = [
        ":target-1",
        ":target-2",
        ...
    ],
)
anuj kosambi
  • 194
  • 1
  • 6