1

I'd like to migrate away from --define flags and to build settings per: https://docs.bazel.build/versions/5.0.0/skylark/config.html

Here's the rule to which I'd like to pass command line values.

  • Can this be done in practice when using loaded rules?
  • Can one access build setting values in rule fields of .bazel files, or are they only accessible to Starlark configurations?
  • Is there a way to effectively "subclass" a loaded rule without having access to a published implementation? If _impl were public, it seems like I might be able to wrap it with my own implementation that passes it the flags.

I'm somewhat new to Bazel and still figuring out the right way to conceptualize this stuff. Any guidance is appreciated!

Current Approach

backend/BUILD.bazel:

load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_push")

# container_image :run_server definition

container_push(
    name = "push_server",
    format = "Docker",
    image = ":run_server",
    registry = "gcr.io",
    repository = "$(PROJECT_ID)/chat/server",
    tag = "$(CONTAINER_TAG)",
)

Then I run:

bazel run \
  --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 \
  --define PROJECT_ID=$(gcloud config get-value project) \
  --define CONTAINER_TAG=some_feature_branch \
  -- //backend:push_server

What I've Tried

A few variations of:

load("//backend:rules.bzl", "gcr_container_push")
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
load("@io_bazel_rules_docker//container:container.bzl", "container_image")

string_flag(
    name = "container_tag",
    build_setting_default = "latest",
    visibility = ["//visibility:public"],
)

string_flag(
    name = "project_id",
    build_setting_default = "",
    visibility = ["//visibility:public"],
)

# container_image :run_server definition

gcr_container_push(
    name = "push_server",
    image = ":run_server", 
    path = "chat/server",
)

backend/rules.bzl:

load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
load("@bazel_skylib//lib:paths.bzl", "paths")
load("@io_bazel_rules_docker//container:container.bzl", "container_push")

def _gcr_container_push_impl(ctx):
    project_id = ctx.attr._project_id[BuildSettingInfo].value
    if len(project_id) == 0:
        fail("Please provide a GCP project ID via --//backend:project_id=<PROJECT ID>.")
    container_push(
        name = ctx.label.name,
        format = "Docker",
        image = ctx.attr.image,
        registry = "gcr.io",
        repository = paths.join(project_id, ctx.attr.path),
        tag = ctx.attr._container_tag[BuildSettingInfo].value,
    )

_gcr_container_push_attrs = {
    "image": attr.label(
        allow_single_file = [".tar"],
        mandatory = True,
        doc = "The label of the image to push.",
    ),
    "path": attr.string(
        mandatory = True,
        doc = "The name of the image within the repository. Ex. gcr.io/project_id/<PATH>:tag.",
    ),
    "_container_tag": attr.label(default = Label("//backend:container_tag")),
    "_project_id": attr.label(default = Label("//backend:project_id")),
}

gcr_container_push = rule(
    implementation = _gcr_container_push_impl,
    attrs = _gcr_container_push_attrs,
    executable = True,
)

Then I run:

bazel run \
  --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 \
  --//backend:project_id=ggx-prototype \
  -- //backend:push_server  

Which returns:

Error in container_push_: 'container_push_' can only be called during the loading phase

1 Answers1

1

rules_docker has attrs like repository_file and tag_file for exactly this kind of thing. You can generate those files however you want, including a custom rule that uses your user-defined flags. I'd do it like this:

def gcr_container_push(name, image, path, **kwargs):
  if 'tag' in kwargs or 'repository' in kwargs:
    fail('Not allowed to set these')
  _gcr_container_repository(
    name = name + '_repository',
    visibility = ['//visibility:private'],
    path = path,
  )
  _gcr_container_tag(
    name = name + '_tag',
    visibility = ['//visibility:private'],
    path = path,
  )
  container_push(
    name = name,
    format = 'Docker',
    image = image,
    registry = 'gcr.io',
    repository = '',
    repository_file = ':%s_repository' % name,
    tag_file = ':%s_tag' % name,
    **kwargs
  )

def _gcr_container_repository_impl(ctx):
    project_id = ctx.attr._project_id[BuildSettingInfo].value
    if len(project_id) == 0:
        fail("Please provide a GCP project ID via --//backend:project_id=<PROJECT ID>.")
    output = ctx.actions.declare_file(ctx.label.name + '_file')
    ctx.actions.write(output, paths.join(project_id, ctx.attr.path))
    return [DefaultInfo(files = depset([output]))]

_gcr_container_repository = rule(
  impl = _gcr_container_repository_impl,
  attrs = {
    "path": attr.string(mandatory = True),
    "_project_id": attr.label(default = Label("//backend:project_id")),
  },
)

def _gcr_container_tag_impl(ctx):
    output = ctx.actions.declare_file(ctx.label.name + '_file')
    ctx.actions.write(output, ctx.attr._container_tag[BuildSettingInfo].value)
    return [DefaultInfo(files = depset([output]))]

_gcr_container_tag = rule(
  impl = _gcr_container_tag_impl,
  attrs = {
    "path": attr.string(mandatory = True),
    "_container_tag": attr.label(default = Label("//backend:container_tag")),
  },
)

Your attempt is mixing a rule and a macro. Rules have attrs and an _impl vs macros can use other rules. My approach uses custom rules to generate the files, and a macro to tie those rules to container_push.

The general answer to your question is that this requires modifying the rule to perform new kinds of substitutions based on a user-defined flag. I can see some kind of --@rules_docker//flags:docker_info=MY_PROJECT=foo configured with allow_multiple = True that would get substituted, but it definitely requires rule modifications. Wrapping the _impl is going to be tricky, because you have to reach in and change some of the actions.

Brian Silverman
  • 3,085
  • 11
  • 13
  • Thanks Brian! This answer works great in this instance. To clarify, your general answer would involve modifying the underlying rule, e.g., adding the flags to https://github.com/bazelbuild/rules_docker? It's surprising there wouldn't be a more straightforward way to augment builds with flags. Google loves command line flags! :) – Spencer Connaughton Feb 01 '22 at 21:26
  • Yes, that is correct. Having the alternative set of `_file` attrs is the best way I've seen of doing this with bazel, and other sets of rules that want to support this level of flexibility typically do something similar (`rules_pkg` for example). – Brian Silverman Feb 01 '22 at 22:12