Skip to content

Instantly share code, notes, and snippets.

@rrbutani
Last active June 3, 2024 21:45
Show Gist options
  • Save rrbutani/61f068a5f5e263a436d7a4cebd7be626 to your computer and use it in GitHub Desktop.
Save rrbutani/61f068a5f5e263a436d7a4cebd7be626 to your computer and use it in GitHub Desktop.

Prototypes a stage-based toolchain bootstrap with transitions using "quux-lang" — a fictitious language — as an example.

bazel cquery 'filter("^//:*", deps(:foo) - filter("//:_quux_toolchain_current_bootstrap_stage_is_[0-9]*", //:*)) - //:defs.bzl - //:toolchain - //:quux_toolchain_type' --output=graph > out.dot

bazel cquery 'filter("^//:*", deps(:bar) - filter("//:_quux_toolchain_current_bootstrap_stage_is_[0-9]*", //:*)) - //:defs.bzl - //:toolchain_bootstrappable - //:quux_toolchain_type - //:my_arch - //:exec_arch - //:target_arch - //:my_target - //:cross_toolchain_bootstrappable' --output=graph > out.dot

bazel build :bar -c opt --attempt_to_print_relative_paths
DEBUG: defs.bzl:151:8: [toolc(cros)] made toolchain up to [0]: compiler
DEBUG: defs.bzl:151:8: [toolc(host)] made toolchain up to [0]: compiler
DEBUG: defs.bzl:88:8:          [lib] built builtins w/toolchain [0]: compiler
DEBUG: defs.bzl:151:8: [toolc(host)] made toolchain up to [1]: builtins
DEBUG: defs.bzl:88:8:          [lib] built core w/toolchain [1]: builtins
DEBUG: defs.bzl:151:8: [toolc(host)] made toolchain up to [2]: core
DEBUG: defs.bzl:88:8:          [lib] built std w/toolchain [2]: core
DEBUG: defs.bzl:151:8: [toolc(host)] made toolchain up to [3]: std
DEBUG: defs.bzl:88:8:          [lib] built extra w/toolchain [3]: std
DEBUG: defs.bzl:151:8: [toolc(host)] made toolchain up to [4]: extra
DEBUG: defs.bzl:88:8:          [lib] built macro_expander w/toolchain [4]: extra
DEBUG: defs.bzl:88:8:          [lib] built special_builtins w/toolchain [0]: compiler
DEBUG: defs.bzl:151:8: [toolc(cros)] made toolchain up to [1]: builtins
DEBUG: defs.bzl:88:8:          [lib] built core w/toolchain [1]: builtins
DEBUG: defs.bzl:151:8: [toolc(cros)] made toolchain up to [2]: core
DEBUG: defs.bzl:88:8:          [lib] built std w/toolchain [2]: core
DEBUG: defs.bzl:151:8: [toolc(cros)] made toolchain up to [3]: std
DEBUG: defs.bzl:88:8:          [lib] built extra w/toolchain [3]: std
DEBUG: defs.bzl:151:8: [toolc(cros)] made toolchain up to [4]: extra
DEBUG: defs.bzl:88:8:          [lib] built bar_ w/toolchain [4]: extra```
use nix -p bazel_7
/bazel-*
.direnv
MODULE.bazel.lock
load(":defs.bzl", "bootstrapping_stage_setting_defs", "quux_library", "bootstrapped_quux_toolchain")
load(":test_utils.bzl", "plat_transition")
################################################################################
bootstrapping_stage_setting_defs(name = "quux_toolchain_current_bootstrap_stage")
toolchain_type(name = "quux_toolchain_type")
################################################################################
quux_library(
name = "builtins",
val = select({ "//conditions:default": "native" }),
requires = "compiler",
tags = ["manual"],
)
quux_library(
name = "core",
val = "core",
requires = "builtins",
tags = ["manual"],
)
quux_library(
name = "std",
val = "std",
requires = "core",
tags = ["manual"],
)
quux_library(
name = "extra",
val = "extra",
requires = "std",
tags = ["manual"],
)
bootstrapped_quux_toolchain(
name = "toolchain",
compiler = ":defs.bzl",
builtins = ":builtins",
core = ":core",
std = ":std",
extra = ":extra",
# Will run anywhere but can only produce binaries for `exec_arch`.
exec_compatible_with = [],
target_compatible_with = [":exec_arch"]
)
################################################################################
quux_library(name = "foo", val = "foo")
################################################################################
## Special toolchain, cross compiles for `exec_arch` -> `target_arch`.
constraint_setting(name = "my_arch", default_constraint_value = ":exec_arch")
constraint_value(name = "exec_arch", constraint_setting = ":my_arch")
constraint_value(name = "target_arch", constraint_setting = ":my_arch")
platform(name = "my_target", constraint_values = [":target_arch"])
#-------------------------------------------------------------------------------
quux_library(name = "macro_expander", val = "m")
quux_library(
name = "special_builtins",
requires = "compiler",
val = "native",
tags = ["manual"],
# These special builtins invoke a binary at compile time (i.e. to do
# codegen, macro expansion):
host_deps = [":macro_expander"],
)
bootstrapped_quux_toolchain(
name = "cross_toolchain",
compiler = ":defs.bzl",
builtins = ":special_builtins",
core = ":core",
std = ":std",
extra = ":extra",
# Can only run on `exec_arch`, will only produce binaries for `target_arch`.
exec_compatible_with = [":exec_arch"],
target_compatible_with = [":target_arch"],
# NOTE: ^^^^ this constraint is what prevents `macro_expander` from being
# built with the bootstrap toolchain that's added to `extra_toolchains`.
#
# See the TODO on `_bootstrap_component_transition_impl` in `defs.bzl`.
)
#-------------------------------------------------------------------------------
# Build some library for `:target_arch`:
quux_library(name = "bar_", val = "bar", target_compatible_with = [":target_arch"], tags = ["manual"])
plat_transition(
name = "bar",
plat = ":my_target",
inner = ":bar_",
)
load("@range_setting.bzl", "mk_range_setting")
DEBUG = True
dbg = print if DEBUG else lambda *a, **kw: None
################################################################################
QUUX_TOOLCHAIN_TYPE = Label(":quux_toolchain_type")
# in bootstrap order
quux_built_in_libs = [
"builtins",
"core",
"std",
"extra",
]
quux_components = ["compiler"] + quux_built_in_libs
quux_component_to_bootstrap_stage_num = {
comp: i for i, comp in enumerate(quux_components)
}
_bootstrap_stage_setting_label = Label(":quux_toolchain_current_bootstrap_stage")
bootstrapping_stage_setting_defs, current_bootstrap_stage = mk_range_setting(
name = _bootstrap_stage_setting_label,
elements = quux_components,
default = quux_built_in_libs[-1],
declare_supporting_targets_separately = True,
)
# value of this setting indicates what has already been bootstrapped; i.e.
# what's available
################################################################################
QuuxLibInfo = provider(fields = dict(
label = "Label",
value = "str",
))
_quux_lib_output_template = """
Quux lib: {lib_label} => {val}
Compiler:
- stage {stage_num} ({max_component})
- compiler: {compiler_file} ({compiler_label})
""" + "\n".join([
" - " + l + ": {" + l + "}"
for l in quux_built_in_libs
]) + "\n"
def _quux_library_impl(ctx):
quux_toolchain = ctx.toolchains[QUUX_TOOLCHAIN_TYPE].quux
_stringify_lib = lambda l: "None" if l == None else (
"{} ({})".format(l.value, str(l.label))
)
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(out, content = _quux_lib_output_template.format(
lib_label = str(ctx.label), val = ctx.attr.val,
max_component = quux_toolchain.highest_stage_component_available,
stage_num = quux_toolchain.stage_num,
compiler_file = quux_toolchain.compiler.file,
compiler_label = quux_toolchain.compiler.label,
**{
lib: _stringify_lib(getattr(quux_toolchain, lib))
for lib in quux_built_in_libs
},
))
req = ctx.attr.requires
have = quux_toolchain.highest_stage_component_available
if req != None and have != req:
req_num, have_num = [
quux_component_to_bootstrap_stage_num[n] for n in [req, have]
]
if req_num > have_num: fail(
"required toolchain with {} (stage {}) but was given {} (stage {})".format(
req, req_num, have, have_num
)
)
else:
print("warn({}): given toolchain with more libs than required: needed {} ({}), got {} ({})".format(
str(ctx.label), req, req_num, have, have_num,
))
dbg(" [lib] built {} w/toolchain [{}]: {}".format(ctx.label.name, quux_toolchain.stage_num, have))
return [
DefaultInfo(files = depset([out])),
QuuxLibInfo(label = ctx.label, value = ctx.attr.val),
]
quux_library = rule(
implementation = _quux_library_impl,
attrs = dict(
val = attr.string(mandatory = True),
# Just a way for us to assert that bootstrapping is going how we expect.
requires = attr.string(default = "extra", values = quux_component_to_bootstrap_stage_num.keys()),
host_deps = attr.label_list(allow_files = True, cfg = "exec"),
),
toolchains = [QUUX_TOOLCHAIN_TYPE],
provides = [QuuxLibInfo],
)
################################################################################
QuuxToolchainInfo = provider(
# compiler: struct(label, file)
# *libs: QuuxLibInfo
# highest_stage_component_available: str in `quux_component_to_bootstrap_stage_num.keys()`
# stage_num: int
fields = list(quux_component_to_bootstrap_stage_num.keys()) + [
"highest_stage_component_available", "stage_num",
]
)
def _quux_toolchain_impl(ctx):
# sliding scale; all attrs up to a stage must be provided, no attrs beyond
# that stage must be given
remaining = []
highest_stage_component_available = None
attrs = dict(compiler = struct(label = ctx.attr.compiler.label, file = ctx.file.compiler))
for i, c in enumerate(quux_components):
comp = getattr(ctx.attr, c)
if comp != None:
highest_stage_component_available = c
if not c == "compiler":
attrs[c] = comp[QuuxLibInfo]
continue
else:
remaining = quux_components[i:]
break
for r in remaining:
if getattr(ctx.attr, r) != None:
fail(
("Component {} should not be specified; highest consecutive " +
"component provided was {}, all components above this " +
"should be `None`.").format(
r, highest_stage_component_available,
))
else:
attrs[r] = None
stage_num = quux_component_to_bootstrap_stage_num[highest_stage_component_available]
dbg("[toolc({})] made toolchain up to [{}]: {}".format(
"cros" if ctx.label.name.startswith("cross") else "host",
stage_num,
highest_stage_component_available,
))
return platform_common.ToolchainInfo(
quux = QuuxToolchainInfo(
highest_stage_component_available = highest_stage_component_available,
stage_num = stage_num,
**attrs,
),
)
quux_toolchain = rule(
implementation = _quux_toolchain_impl,
attrs = dict(
compiler = attr.label(allow_single_file = True, cfg = "exec"),
**{
lib: attr.label(
mandatory = False,
providers = [QuuxLibInfo],
cfg = "target",
)
for lib in quux_built_in_libs
}
),
provides = [platform_common.ToolchainInfo],
)
################################################################################
# TODO: would be nice if we could _remove_ the bootstrap toolchain for exec deps
# that are behind this transition but unfortunately afaik that's not possible?
#
# Danger is that if the exec constraints align, host deps used for this
# toolchain component may get built with the bootstrap toolchain... which is not
# what we want (may not even work since we're missing components).
#
# If it weren't experimental, we could maybe transition
# `--experimental_exec_config` to point to a transition that modifies the
# default starlark exec transition's behavior to remove the bootstrap toolchain
# from the extra list..
#
# We may be able to synthesize (and set w/transitions) exec and target platforms
# in the top-level macro as a way around this but for now, leaving as it;
# shouldn't come up often in typical usage I think.
def _bootstrap_component_transition_impl(settings, attr):
component = attr.lib_type
toolchain_label = str(attr.self)
extra_toolchains = settings.get("//command_line_option:extra_toolchains", [])
# to build `component`, ask for a compiler that has N - 1 components:
stage = quux_components[quux_component_to_bootstrap_stage_num[component] - 1]
# If we don't do the latter check, we'll recurse infinitely during
# configuration.
if not extra_toolchains or extra_toolchains[0] != toolchain_label:
extra_toolchains = [toolchain_label] + extra_toolchains
return {
"//command_line_option:extra_toolchains": extra_toolchains,
str(_bootstrap_stage_setting_label): stage,
}
_bootstrap_component_transition = transition(
implementation = _bootstrap_component_transition_impl,
inputs = [
"//command_line_option:extra_toolchains",
],
outputs = [
"//command_line_option:extra_toolchains",
str(_bootstrap_stage_setting_label),
],
)
# Wraps a library, builds it with the appropriate "version" of the final
# toolchain.
_bootstrap_quux_builtin_lib = rule(
implementation = lambda ctx: [ctx.attr.inner[QuuxLibInfo], ctx.attr.inner[DefaultInfo]],
attrs = dict(
inner = attr.label(mandatory = True, providers = [QuuxLibInfo]),
lib_type = attr.string(
# configurable = False, # Bazel 8; macros
mandatory = True,
values = quux_built_in_libs,
),
self = attr.label(
mandatory = True,
providers = [], # unfortunately no providers for `toolchain(...)`?
doc = "label of the final `toolchain(...)` that the lib is used in",
)
),
cfg = _bootstrap_component_transition,
provides = [QuuxLibInfo],
)
_force_fully_bootstrapped_transition = transition(
implementation = lambda settings, attr: {
str(_bootstrap_stage_setting_label): quux_components[-1],
},
inputs = [],
outputs = [str(_bootstrap_stage_setting_label)],
)
_force_fully_bootstrapped_quux_toolchain = rule(
implementation = lambda ctx: [
ctx.attr.inner[platform_common.ToolchainInfo],
],
attrs = dict(
inner = attr.label(providers = [platform_common.ToolchainInfo], mandatory = True),
),
provides = [platform_common.ToolchainInfo],
cfg = _force_fully_bootstrapped_transition,
)
def bootstrapped_quux_toolchain(
name,
compiler,
exec_compatible_with = [],
target_compatible_with = [],
target_settings = [],
visibility = None,
tags = [],
**libs,
):
common_attrs = dict(
exec_compatible_with = exec_compatible_with,
target_compatible_with = target_compatible_with,
tags = tags,
)
for lib_type in libs.keys():
if lib_type not in quux_built_in_libs: fail("unknown lib", lib_type)
if not all([l in libs for l in quux_built_in_libs]): fail(
"missing libs; need all of: ", ", ".join(quux_built_in_libs),
)
names = struct(
# wrapper targets that force the use of the appropriate bootstrap
# toolchain to build builtin libs
bootstrapped_lib = lambda lib_type: "_" + name + "_lib_" + lib_type + "_boostrapped",
# `quux_toolchain` target that gates components appropriately, according
# to the current bootstrap stage
quux_toolchain = name + "_inner_bootstrappable",
# `toolchain(...)` wrapper for ^, sensitive to bootstrap stage setting
#
# used by the _bootstrap_quux_builtin_lib` targets
toolchain_bootstrappable = name + "_bootstrappable",
# public `toolchain(...)` wrapper for ^, impervious to bootstrap stage
# setting
#
# the idea here is that if this final toolchain (i.e. fully
# bootstrapped) is selected in a context where the bootstrap stage
# setting is being altered, we don't want to re-bootstrap
#
# i.e. if another toolchain is constructed and if, in the transitive
# deps of its built-in library inputs (built while bootstrapping) it
# builds a host tool that resolves its toolchain to our toolchain
#
# in a case like this we want to force use of the fully bootstrapped
# toolchain
#
# we ensure this by — for the public toolchain target that will be
# registered — re-exporting the `quux_toolchain` definition behind
# a transition that forces the bootstrap stage setting to fully
# bootstrapped
quux_toolchain_forced_fully_bootstrapped = name + "_inner",
public_toolchain = name,
)
# Bootstrapped libs:
for lib_type, lib_val in libs.items():
_bootstrap_quux_builtin_lib(
name = names.bootstrapped_lib(lib_type),
inner = lib_val,
lib_type = lib_type,
self = ":" + names.toolchain_bootstrappable,
visibility = ["//visibility:private"],
# target-oriented; can propagate `(exec|target)_compatible_with`:
**common_attrs,
)
# Bootstrap toolchain + `toolchain(...)` (recursive):
quux_toolchain(
name = names.quux_toolchain,
compiler = compiler,
visibility = ["//visibility:private"],
**(common_attrs | {
# gate component; only available if the current bootstrapping stage
# is >= this lib:
lib_type: current_bootstrap_stage.ge(
lib_type,
then = ":" + names.bootstrapped_lib(lib_type),
else_ = None,
)
for lib_type in libs.keys()
}),
)
native.toolchain(
name = names.toolchain_bootstrappable,
toolchain_type = QUUX_TOOLCHAIN_TYPE,
toolchain = ":" + names.quux_toolchain,
target_settings = target_settings,
visibility = ["//visibility:private"],
**common_attrs,
)
# Public fully-bootstrapped toolchain:
_force_fully_bootstrapped_quux_toolchain(
name = names.quux_toolchain_forced_fully_bootstrapped,
inner = ":" + names.quux_toolchain,
visibility = ["//visibility:private"],
**common_attrs,
)
native.toolchain(
name = names.public_toolchain,
toolchain_type = QUUX_TOOLCHAIN_TYPE,
toolchain = ":" + names.quux_toolchain_forced_fully_bootstrapped,
target_settings = target_settings,
visibility = visibility,
**common_attrs,
)
module(name = "toolchain-bootstrapping-example")
bazel_dep(name = "bazel_skylib", version = "1.7.0")
bazel_dep(name = "range_setting.bzl")
git_override(
module_name = "range_setting.bzl",
remote = "[email protected]:cc3fdc5bae38b5cd18733fb6bef15fdd.git",
commit = "ee355dae2d1d97c4efe71e85157bd37cc1093155",
)
register_toolchains("//:toolchain", "//:cross_toolchain")
_plat_transition = transition(
implementation = lambda settings, attr: {
"//command_line_option:platforms": [str(attr.plat)],
},
inputs = [],
outputs = ["//command_line_option:platforms"],
)
plat_transition = rule(
implementation = lambda ctx: [ctx.attr.inner[DefaultInfo]],
attrs = dict(
inner = attr.label(mandatory = True),
plat = attr.label(allow_files = False, mandatory = True, providers = [platform_common.PlatformInfo]),
),
cfg = _plat_transition,
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment