blob: 0242444828792cdb1f04a1836483654c0fff37ca [file]
"""
multitool
Multitool takes as input a JSON lockfile and emits the following repos:
- [hub].[os]_[cpu], for each [os]/[cpu] combo in _SUPPORTED_ENVS:
This repository holds os/cpu specific binaries for all tools in the provided
lockfile(s) and constructs clean symlinks to their content for inclusion in
toolchains defined in the [hub] repo.
The structure of this repo is, very simply:
tools/
[tool-name]/
BUILD.bazel (export all *_executable files)
[os]_[cpu]_executable (a downloaded file or a symlink to a file in a
downloaded and extracted archive)
- [hub]:
This repository holds toolchain definitions for all tools in the provided
lockfile(s), as well as an executable tool target that will pick the
appropriate toolchain.
The structure of this repo is:
toolchains/
BUILD.bazel (a single file containing all declared toolchains for easy registration)
tools/
[tool-name]/
BUILD.bazel (declares the toolchain_type and the executable tool target)
tool.bzl (scaffolding for the tool target and toolchain declarations in toolchains/BUILD.bazel)
toolchain_info.bzl (common scaffolding for toolchain declarations)
(additional BUILD.bazel and a WORKSPACE file are included as required by Bazel)
To keep things orderly, we keep all the toolchain Bazel goo in the [hub] repo and only stash
the binaries in the [hub].[os]_[cpu] repos. It's a conscious decision not to place some fragments
of the toolchain definitions in the latter repos to make the dependencies run exactly one way:
[hub] -> [hub].[os]_[cpu].
This implementation depends on rendering a number of templates, which are defined in sibling
folders and managed by the templates starlark file.
Note that we intend to support both bzlmod and non-bzlmod setups, so `hub` intentionally avoids
a register_toolchains call.
"""
load("@bazel_features//:features.bzl", "bazel_features")
load(":templates.bzl", "templates")
_SUPPORTED_ENVS = [
("linux", "arm64"),
("linux", "x86_64"),
("macos", "arm64"),
("macos", "x86_64"),
]
def _check(condition, message):
"fails iff condition is False and emits message"
if not condition:
fail(message)
def _load_tools(rctx):
tools = {}
for lockfile in rctx.attr.lockfiles:
# TODO: validate no conflicts from multiple hub declarations and/or
# fix toolchains to also declare their versions and enable consumers
# to use constraints to pick the right one.
# (this is also a very naive merge at the tool level)
tools = tools | json.decode(rctx.read(lockfile))
# a special key says this JSON document conforms to a schema
tools.pop("$schema", None)
# validation
for tool_name, tool in tools.items():
for binary in tool["binaries"]:
_check(
binary["os"] in ["linux", "macos"],
"{tool_name}: Unknown os '{os}'".format(
tool_name = tool_name,
os = binary["os"],
),
)
_check(
binary["cpu"] in ["x86_64", "arm64"],
"{tool_name}: Unknown cpu '{cpu}'".format(
tool_name = tool_name,
cpu = binary["cpu"],
),
)
return tools
def _feature_sensitive_args(binary):
args = {}
if bazel_features.external_deps.download_has_headers_param:
args["headers"] = binary.get("headers", {})
return args
def _env_specific_tools_impl(rctx):
tools = _load_tools(rctx)
for tool_name, tool in tools.items():
for binary in tool["binaries"]:
if binary["os"] != rctx.attr.os or binary["cpu"] != rctx.attr.cpu:
continue
target_executable = "tools/{tool_name}/{os}_{cpu}_executable".format(
tool_name = tool_name,
cpu = binary["cpu"],
os = binary["os"],
)
if binary["kind"] == "file":
rctx.download(
url = binary["url"],
sha256 = binary["sha256"],
output = target_executable,
executable = True,
**_feature_sensitive_args(binary)
)
elif binary["kind"] == "archive":
archive_path = "tools/{tool_name}/{os}_{cpu}_archive".format(
tool_name = tool_name,
cpu = binary["cpu"],
os = binary["os"],
)
rctx.download_and_extract(
url = binary["url"],
sha256 = binary["sha256"],
output = archive_path,
type = binary.get("type", ""),
**_feature_sensitive_args(binary)
)
# link to the executable
rctx.symlink(
"{archive_path}/{file}".format(archive_path = archive_path, file = binary["file"]),
target_executable,
)
elif binary["kind"] == "pkg":
# Check if pkgutil is on the path, and if not fail silently.
# repository rules execute irrespective of platform/OS, so this
# check is required for `pkg_archive` to not fail on Linux.
pkgutil_cmd = rctx.which("pkgutil")
if not pkgutil_cmd:
continue
archive_path = "tools/{tool_name}/{os}_{cpu}_pkg".format(
tool_name = tool_name,
cpu = binary["cpu"],
os = binary["os"],
)
rctx.download(
url = binary["url"],
sha256 = binary["sha256"],
output = archive_path + ".pkg",
**_feature_sensitive_args(binary)
)
rctx.execute([pkgutil_cmd, "--expand-full", archive_path + ".pkg", archive_path])
# link to the executable
rctx.symlink(
"{archive_path}/{file}".format(archive_path = archive_path, file = binary["file"]),
target_executable,
)
else:
fail("Unknown 'kind' {kind}".format(kind = binary["kind"]))
templates.env_tool(rctx, tool_name, "BUILD.bazel")
templates.env(rctx, "tools/BUILD.bazel")
templates.env(rctx, "BUILD.bazel")
_env_specific_tools = repository_rule(
attrs = {
"lockfiles": attr.label_list(mandatory = True, allow_files = True),
"os": attr.string(),
"cpu": attr.string(),
},
implementation = _env_specific_tools_impl,
)
def _sort_fn(tup):
return tup[0]
def _multitool_hub_impl(rctx):
tools = _load_tools(rctx)
loads = []
defines = []
for tool_name, tool in sorted(tools.items(), key = _sort_fn):
toolchains = []
for binary in tool["binaries"]:
toolchains.append('\n declare_toolchain(name="{name}", os="{os}", cpu="{cpu}", toolchain_type=_TOOLCHAIN_TYPE)'.format(
name = tool_name,
cpu = binary["cpu"],
os = binary["os"],
))
templates.hub_tool(rctx, tool_name, "BUILD.bazel")
templates.hub_tool(rctx, tool_name, "tool.bzl", {
"{toolchains}": "\n".join(toolchains),
})
clean_name = tool_name.replace("-", "_")
loads.append('load("//tools/{tool_name}:tool.bzl", declare_{clean_name}_toolchains = "declare_toolchains")'.format(
tool_name = tool_name,
clean_name = clean_name,
))
defines.append("declare_{clean_name}_toolchains()".format(clean_name = clean_name))
templates.hub(rctx, "BUILD.bazel")
templates.hub(rctx, "toolchain_info.bzl", {
"{hub_name}": rctx.attr.name,
})
templates.hub(rctx, "tools/BUILD.bazel")
templates.hub(rctx, "toolchains/BUILD.bazel", {
"{loads}": "\n".join(loads),
"{defines}": "\n".join(defines),
})
_multitool_hub = repository_rule(
attrs = {
"lockfiles": attr.label_list(mandatory = True, allow_files = True),
},
implementation = _multitool_hub_impl,
)
def hub(name, lockfiles):
"Create a multitool hub."
for env in _SUPPORTED_ENVS:
_env_specific_tools(
name = "{name}.{os}_{cpu}".format(name = name, os = env[0], cpu = env[1]),
lockfiles = lockfiles,
os = env[0],
cpu = env[1],
)
_multitool_hub(name = name, lockfiles = lockfiles)