How do you build a Rust Wasm binary with Bazel?

49 views Asked by At

I'm trying to get rules_rust to build a Wasm binary for me dependent on some Rust libraries I've configured with rust_library, but I can't quite figure out the right spelling.

How do I build a Rust Wasm binary with Bazel?

1

There are 1 answers

0
akdom On

The key is to realize that you need to convince Bazel to apply a platform transition to the build graph. Please take a moment to read the Bazel configuration transition docs for context on what on earth is actually happening here.

I've built a few helpers that make this easier for my project. The core of all of this is a wasm "platform transition":

def _wasm_rust_transition_impl(settings, attr):
    return {
        "//command_line_option:platforms": "@rules_rust//rust/platform:wasm",
    }

wasm_rust_transition = transition(
    implementation = _wasm_rust_transition_impl,
    inputs = [],
    outputs = [
        "//command_line_option:platforms",
    ],
)

I apply this transition to a cheeky little helper that "just" copies the final rust binary file for me... but keep in mind that this little transition at the final step in the build graph makes everything work! I believe I learned this trick via the Envoy Rust Bazel Wasm rules... but TBH, I can't quite remember.

def _wasm_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.run(
        executable = "cp",
        arguments = [ctx.files.binary[0].path, out.path],
        outputs = [out],
        inputs = ctx.files.binary,
    )

    return [
        DefaultInfo(
            files = depset([out]),
            runfiles = ctx.runfiles([out]))]

def _wasm_attrs(transition):
    return {
        "binary": attr.label(mandatory = True, cfg = transition),
        "_whitelist_function_transition": attr.label(
            default = "@bazel_tools//tools/whitelists/function_transition_whitelist"),
    }

wasm_rust_binary_rule = rule(
    implementation = _wasm_binary_impl,
    attrs = _wasm_attrs(wasm_rust_transition),
)

And to make everything ergonomic, I provide a macro that forwards everything I need to a rust_binary and wraps it in the copier/transition-applier.

def wasm_rust_binary(name, **kwargs):
    wasm_name = "_wasm_" + name.replace(".", "_")
    # Remove double-underscores to enforce snake case in case, for example,
    # `name` was underscore prefixed.
    wasm_name = wasm_name.replace("__", "_")
    kwargs.setdefault("visibility", ["//visibility:public"])

    rust_binary(
        name = wasm_name,
        crate_type = "cdylib",
        out_binary = True,
        target_compatible_with = select({
            "//bazel/config:platform_is_rust_wasm": [],
            "//conditions:default": ["@platforms//:incompatible"],
        }),
        **kwargs
    )

    wasm_rust_binary_rule(
        name = name,
        binary = ":" + wasm_name,
    )

Then I can include any of my Rust libraries as normal rust_librarys included via deps for a wasm_rust_binary and the transition will apply through the whole build graph.

P.S. Note that there's a special little config value in that rust_binary select which forces these binaries to fail when forced into a build graph not configured for the wasm platform. Here's all that is:

config_setting(
    name = "platform_is_rust_wasm",
    values = {"platforms": "@rules_rust//rust/platform:wasm"},
)