blob: 0489ea4d299009969b6ab7695bef007cdcf78af0 [file] [edit]
# rules/rbe_config.bzl
def _resolve_preset(repository_ctx, preset_name, container_image, cpp_env):
"""Downloads and resolves standard RBE presets.
Returns:
Tuple of (container_image, cpp_env_dict).
"""
if not preset_name:
return container_image, cpp_env
presets_json_path = "rbe_presets.json"
# Download dynamic presets from GitHub master branch
repository_ctx.download(
output = presets_json_path,
url = ["https://raw.githubusercontent.com/bazelbuild/continuous-integration/master/rules/rbe_presets.json"],
)
presets = json.decode(repository_ctx.read(presets_json_path))
resolved_preset = presets.get(preset_name)
if not resolved_preset:
fail("rbe_config: Preset configuration not found for '{}'.".format(preset_name))
return resolved_preset["container"], resolved_preset["cpp_env"]
def _download_bazel_toolchains(repository_ctx):
"""Downloads and extracts the bazel-toolchains source repository."""
repo_zip_url = "https://github.com/bazelbuild/bazel-toolchains/archive/refs/heads/master.zip"
if repository_ctx.attr.bazel_toolchains_url:
repo_zip_url = repository_ctx.attr.bazel_toolchains_url
print("rbe_config: Downloading bazel-toolchains source from {}...".format(repo_zip_url))
repository_ctx.download_and_extract(
url = [repo_zip_url],
output = "bazel-toolchains-src",
stripPrefix = "bazel-toolchains-master",
)
return repository_ctx.path("bazel-toolchains-src")
def _compile_generator(repository_ctx, src_dir):
"""Compiles rbe_configs_gen inside a sibling Go container.
Returns:
Path to the compiled generator executable.
"""
rbe_gen_path = repository_ctx.path("bazel-toolchains-src/rbe_configs_gen")
print("rbe_config: Compiling rbe_configs_gen via Go container...")
compile_res = repository_ctx.execute([
"docker", "run", "--rm",
"-v", "{}:/srcdir".format(src_dir),
"-w", "/srcdir",
"golang:1.21",
"go", "build", "-o", "/srcdir/rbe_configs_gen", "./cmd/rbe_configs_gen"
])
if compile_res.return_code != 0:
fail("rbe_config: compilation of rbe_configs_gen failed:\nStdout: {}\nStderr: {}".format(compile_res.stdout, compile_res.stderr))
return rbe_gen_path
def _generate_toolchains(repository_ctx, rbe_gen_path, bazel_version, bazel_path, container_image, cpp_env):
"""Runs the generator inside the sandboxed container and extracts RBE files."""
# Serialize C++ environment dict to JSON inside sandbox
cpp_env_json = "cpp_env.json"
repository_ctx.file(cpp_env_json, json.encode(cpp_env))
output_tarball = "rbe_default.tar"
output_manifest = "manifest.json"
args = [
rbe_gen_path,
"--toolchain_container=" + container_image,
"--cpp_env_json=" + str(repository_ctx.path(cpp_env_json)),
"--output_tarball=" + str(repository_ctx.path(output_tarball)),
"--output_manifest=" + str(repository_ctx.path(output_manifest)),
"--exec_os=linux",
"--target_os=linux",
]
if bazel_version:
args.append("--bazel_version=" + bazel_version)
elif bazel_path:
args.append("--host_bazel_path=" + str(bazel_path))
else:
fail("rbe_config: Neither bazel_version nor bazel_path is available.")
print("rbe_config: Executing generator to detect toolchains inside {}...".format(container_image))
exec_res = repository_ctx.execute(args)
if exec_res.return_code != 0:
fail("rbe_config: Dynamic generation failed:\nStdout: {}\nStderr: {}".format(exec_res.stdout, exec_res.stderr))
# Extract the generated configs directly into the repository's directory
repository_ctx.extract(archive = output_tarball)
# --- Private Repository Rule Entrypoint ---
def _rbe_config_impl(repository_ctx):
# 1. Resolve presets/custom image container and environment
container_image, cpp_env = _resolve_preset(
repository_ctx,
repository_ctx.attr.preset_name,
repository_ctx.attr.container,
repository_ctx.attr.cpp_env
)
# 2. Download bazel-toolchains source code
src_dir = _download_bazel_toolchains(repository_ctx)
# 3. Compile rbe_configs_gen
rbe_gen_path = _compile_generator(repository_ctx, src_dir)
# 4. Resolve Bazel version or locate host Bazel
bazel_version = None
bazel_path = repository_ctx.os.environ.get("RBE_CONFIG_BAZEL_PATH")
if bazel_path:
print("rbe_config: RBE_CONFIG_BAZEL_PATH env var is set, using host bazel path: {}".format(bazel_path))
else:
bazel_path = repository_ctx.os.environ.get("BAZEL_REAL")
if bazel_path:
print("rbe_config: BAZEL_REAL env var is set, using host bazel path: {}".format(bazel_path))
else:
bazel_version = getattr(native, "bazel_version", None)
if bazel_version:
print("rbe_config: Using detected bazel_version: {}".format(bazel_version))
else:
print("rbe_config: native.bazel_version not available, falling back to host bazel from PATH")
bazel_path = repository_ctx.which("bazel")
if bazel_path:
print("rbe_config: Using Bazel binary at {} for toolchain generation".format(bazel_path))
else:
fail("rbe_config: Bazel executable not found in PATH. If you are using a custom-named binary or non-standard layout, please export RBE_CONFIG_BAZEL_PATH=/path/to/your/binary or use a tools/bazel wrapper script.")
# 5. Run the generator and extract configurations
_generate_toolchains(repository_ctx, rbe_gen_path, bazel_version, bazel_path, container_image, cpp_env)
# Private repository rule
_rbe_config = repository_rule(
implementation = _rbe_config_impl,
attrs = {
"preset_name": attr.string(mandatory = False),
"container": attr.string(mandatory = False),
"cpp_env": attr.string_dict(mandatory = False),
"bazel_toolchains_url": attr.string(mandatory = False),
},
environ = [
"RBE_CONFIG_BAZEL_PATH",
"BAZEL_REAL",
], # Propagate custom Bazel path environment variables
)
# --- Public WORKSPACE wrapper macro ---
def rbe_config(name, preset = None, spec = None, bazel_toolchains_url = None):
"""Unified RBE toolchain generator rule (for WORKSPACE).
Args:
name: The name of the repository (usually 'rbe_ubuntu').
preset: The string name of a standard preset (e.g. "ubuntu").
spec: A custom config dictionary containing "container" and "cpp_env" keys.
bazel_toolchains_url: Optional URL override for fetching the bazel-toolchains repository source.
"""
if preset and spec:
fail("rbe_config: 'preset' and 'spec' are mutually exclusive. Please specify only one of them.")
if not preset and not spec:
fail("rbe_config: Either 'preset' or 'spec' must be specified.")
if preset:
_rbe_config(
name = name,
preset_name = preset,
bazel_toolchains_url = bazel_toolchains_url,
)
else:
if "container" not in spec or "cpp_env" not in spec:
fail("rbe_config: 'spec' dictionary must contain both 'container' and 'cpp_env' keys.")
_rbe_config(
name = name,
container = spec["container"],
cpp_env = spec["cpp_env"],
bazel_toolchains_url = bazel_toolchains_url,
)
# --- Bzlmod Module Extension ---
def _rbe_config_extension_impl(module_ctx):
for mod in module_ctx.modules:
for tag in mod.tags.config:
if tag.preset and (tag.container or tag.cpp_env):
fail("rbe_config extension: 'preset' and custom specifications are mutually exclusive.")
if not tag.preset and not tag.container:
fail("rbe_config extension: Either 'preset' or custom specifications must be provided.")
if tag.preset:
_rbe_config(
name = tag.name,
preset_name = tag.preset,
)
else:
_rbe_config(
name = tag.name,
container = tag.container,
cpp_env = tag.cpp_env,
)
rbe_config_extension = module_extension(
implementation = _rbe_config_extension_impl,
tag_classes = {
"config": tag_class(
attrs = {
"name": attr.string(mandatory = True),
"preset": attr.string(mandatory = False),
"container": attr.string(mandatory = False),
"cpp_env": attr.string_dict(mandatory = False),
}
)
}
)