Skip to content

Instantly share code, notes, and snippets.

@rrbutani
Last active October 28, 2023 22:32
Show Gist options
  • Save rrbutani/ee5adf677b7b7b2ea9ef0bb2c0071326 to your computer and use it in GitHub Desktop.
Save rrbutani/ee5adf677b7b7b2ea9ef0bb2c0071326 to your computer and use it in GitHub Desktop.
.bazel-toolchain-transitions

September 5th, 2023

having a bit of a crisis

runfiles are broken

using platforms to model build environments is broken because of how strategies interact with platforms

bzlmod is conceptually pretty excellent but still has tons of warts and weird limitations that require lots of creative workarounds


my newest complaint is that toolchain resolution is actually not nearly as powerful as I thought

ignoring that registration via aliases just silently does nothing (an annoyance but a solvable one), i learned that only the constraints on the actual toolchain rule are consulted during resolution, not the constraints on the underlying toolchain target or its deps

it makes sense that this is the way it is because otherwise you'd have to load and do some analysis for huge dep graphs for potentially many toolchains just to do toolchain resolution (and it'd also be annoying to communicate to users why a particular toolchain was ruled out)

but the end result is that you have to manually hoist every exec/target constraint that your toolchain and its deps have to the toolchain wrapper to accurately convey to bazel when the toolchain can be used — if you don't do this, bazel will select the toolchain (even if there are fallbacks that would have worked in this situation) and then give you an error about how the target is incompatible


the other crisis I had last night is that tool transitions w.r.t. toolchains seem broken? i.e. for a random label I can choose to depend on it via cfg = "exec" (exec transition, so that my execution platform becomes its target platform; so that I can run the tool at build time) but no equivalent exists for toolchains; they are always in the target configuration as far as I can tell

this is a problem because some toolchains that are "build oriented" like C++ give you their tools in exec configuration (so that the tools' target platform is your exec platform) but toolchains that are "runtime oriented" like Python will give you the python interpreter in target configuration; the idea is that you py_binary rule wants to have a python that runnable on the target's target platform

this makes sense to a degree unlike in nixpkgs where we have a rich vocabulary of cross-compilation stage relationships and can pretty much arbitrarily reach forwards and backwards, in Bazel we only have target and exec and pretty much only have one tool to move between stages: cfg = "exec" a.k.a. move this dep "backwards" please; build it for my exec platform

so given ^, it kind of makes sense that runtime toolchains and "build tool" toolchains are the way they are

if the C++ toolchain tried to specify the runtime platform for clang as its target platform, it would have no way to talk about the platform for which the clang binary generates code (also then the exec platform would be the platform where clang is built which really is well and truly irrelevant to all users of the toolchain)

if the python toolchain described the platform the python interpreter can run on as its execution platform (and had no target constraints since python does not produce machine code), targets that wish to invoke the python interpreter at the runtime of their target now have to ask for the python toolchain where it's execution platform is the target's target platform; i.e. we want to slide the python toolchain dep "forwards". afaik this is not something we can do (I don't think there's syntax for it and also this seems to pose new problems like: what would the python toolchain's target platform be under such a transition? nixpkgs handles this problem by "saturating")

so that's all fine; my problem though is that the "slide backwards" behavior of cfg = "exec" that I can normally apply to labels does not seem to have an analogue for toolchains i.e. afaik I cannot ask for a python toolchain that I want to invoke at build time in my rule

it's especially annoying because if I depend on targets explicitly (without the late binding shenanigans of toolchains) I can totally express this I can even try to (badly) express the "slide forward" type of dependency by writing my own transitions and passing them to cfg

Really what I want is config_common.toolchain_type("//some/toolchain:type", cfg = "exec") but that does not exist

I think I can emulate ^ by setting up a target that just re-exports the current toolchain that toolchain resolution resolves to (like this: https://github.com/bazelbuild/rules_perl/blob/d458b41dd15a086721dcf663317f2c121bba8984/BUILD#L47-L56) and then by putting that target behind a cfg = "exec" (see below)


the short of it is that whenever I dig deeper into how some bazel features work I end up disappointed; feels like everything is hacks, approximations, best-effort, only covers some use cases

doesn't feel like there's a rigorous underlying Model that I can reason about (this is also how I feel about the rust type system...)

i think it's particularly jarring because learning about cross in nixpkgs was largely the opposite experience -- there's definitely warts but the underpinnings are elegant and robust


Example

# BUILD.bazel

# Note: this (incorrectly) declares a target dependency on `perl` instead of an
# execution dependency; we want `cfg = "exec"` for `perl` here since we don't
# need a perl that's compatible with our target platform — just one that we can
# use at build time.
#
# This makes it so that
# `bazel build //:perl_test --platforms=@local_config_platform//:host` (i.e.
# building for a non-EC target platform, with EC execution platforms available
# when there are only EC perl toolchains regsitered) fails.
#
# Unfortunately we have no way to communicate that we want a toolchain
# dependency "for" the execution platform (i.e. `cfg = "exec"`).
#
# For interpreters like `python` and `perl` a way we'd get around this (I
# think) is to depend on a `py_binary`, etc. with `cfg = "exec"` (i.e. in
# `tools` below); this lets us move a dependency "backwards".
#
# As far as I can tell there's no collary for toolchains that are tool-oriented;
# if I want to have a target that invokes `gcc` at runtime I don't think I have
# any way to express that I wish to depend on `cc_toolchain` where its execution
# platform will be my target's target platform. i.e. we have no way to move a
# dependency "forwards". This makes sense, I think (no way to specify what
# `gcc`'s new target platform would be in this case; i.e. when my target invokes
# `gcc` what platform will it be targeting? no way to express this in Bazel; we
# can't reach further forward than `target platform` (this isn't nix))
genrule(
    name = "perl_test",
    cmd = "$(PERL) --version > $@",
    # cmd = "echo yo > $@",
    outs = ["perl_version"],
    srcs = [
        # This also asks for the toolchain configured for the target, as you'd
        # expect:
        # "@rules_perl//:current_toolchain",

    ],
    tools = [
        "@host_bash",
        # "@@_main~configure_deps~ec_perl_toolchain//:ec_perl_toolchain",

        # This works fine but we can't use make vars:
        # "@rules_perl//:current_toolchain",
    ],
    toolchains = [
        # Is configured for the target; errors.
        # "@rules_perl//:current_toolchain",

        # Works fine (wrapper rule that transitions the toolchain for us):
        ":exec_perl",
    ],
)

load(":transitions.bzl", "exec_perl_toolchain")

exec_perl_toolchain(
    name = "exec_perl",
)
# transitions.bzl

def _exec_perl_toolchain_impl(ctx):
    # toolchain = ctx.toolchains["@rules_perl//:toolchain_type"]
    # print(toolchain)

    toolchain = ctx.attr._perl_toolchain_label

    return [
        toolchain[platform_common.ToolchainInfo],
        toolchain[DefaultInfo],
        toolchain[platform_common.TemplateVariableInfo],
    ]

# NOTE: not allowed!
# buildozer: disable=no-effect
"""
exec_perl_toolchain = rule(
    implementation = _exec_perl_toolchain_impl,
    toolchains = ["@rules_perl//:toolchain_type"],

    # can't use `exec` here, needs to be a user defined transition function
    # (makes sense, we don't know what exec for this target will be yet)
    # cfg = "exec",
)
"""

exec_perl_toolchain = rule(
    implementation =  _exec_perl_toolchain_impl,
    attrs = {
        "_perl_toolchain_label": attr.label(
            default = "@rules_perl//:current_toolchain",
            providers = [platform_common.ToolchainInfo],
            # this is how we're asking for the toolchain in exec configuration
            cfg = "exec",
        )
    },
    provides = [platform_common.ToolchainInfo],
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment