The Publishing Project

Bazel Build System: Backend code, even more choices

Just like with the frontend, the backend gives you multiple choices of languages and feature sets for working with Bazel. C/C++, Go and Rust are the three languages that I tested generating WASM code with so it may be interesting to see how well they work with Bazel and how they abstract WASM generation.

We can keep all the code for our project in a single monorepo and let Bazel sort out what tools to use to compile which part.

I use C and C++ as the baseline for testing Bazel with backend languages. It also works slightly differently from other languages as it is built into Bazel and doesn’t require the rules to be installed manually.

As far as backend code goes, Go is my favorite language. However, the Bazel toolchain’s way of compiling Go code to WASM is not intuitive.

I’m also exploring Rust toolchains for Bazel because it provides better WebAssembly tooling than Go, but it’s still a challenge to get it right every time.

Backend code: Go

I’ve always been interested in Go as a backend language or something that will allow me to create WASM code to run on the web.

The first step, as usual, is to load the rules in the WORKSPACE.

http_archive(
  name = "io_bazel_rules_go",
  sha256 = "6f111c57fd50baf5b8ee9d63024874dd2a014b069426156c55adbf6d3d22cb7b",
  urls = [
    "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.25.0/rules_go-v0.25.0.tar.gz",
    "https://github.com/bazelbuild/rules_go/releases/download/v0.25.0/rules_go-v0.25.0.tar.gz",
  ],
)

load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")

go_rules_dependencies()

go_register_toolchains(version = "1.15.5")

This must be the first set of rules to appear in the global WORKSPACE or be in its own WORKSPACE otherwise, it will cause an error.

The ruleset provides the following core rules

For more information see the rules_go README.

The BUILDFILE is simple. We load the go_binary rule and use it to compile the file we want. All the source files must be in the main package.

load("@io_bazel_rules_go//go:def.bzl", "go_binary")

go_binary(
  name = "hello_go",
  srcs = ["hello.go"],
)

The code for hello.go is the basic ‘hello world` example that builds a single binary executable.

package main

import "fmt"

func main() {
  fmt.Println("hello world")
  fmt.Println("I'm learning Blaze and Go")
}

To create packages we use go_library instead of go_binary. All the source files must be part of the same package for this to work.

go_library(
  name = "go_default_library",
  srcs = [
    "foo.go",
    "bar.go",
  ],
  deps = [
    "//tools:go_default_library",
    "@org_golang_x_utils//stuff:go_default_library",
  ],
  importpath = "github.com/example/project/foo",
  visibility = ["//visibility:public"],
)

These libraries will not generate an executable and are meant to be consumed by binaries or other libraries.

Cross-compiling to WASM

While not as intuitive as Rust, Go also allows you to compile code into WASM.

rules_go can cross-compile Go projects to any platform the Go toolchain supports. The simplest way to do this is by setting the --platforms flag on the command line.

bazel build [email protected]_bazel_rules_go//go/toolchain:linux_amd64 //my/project

You can replace linux_amd64 in the example above with any valid GOOS / GOARCH pair. To list all platforms, run this command:

bazel query 'kind(platform, @io_bazel_rules_go//go/toolchain:all)'

By default, cross-compilation will cause Go targets to be built in “pure mode”, which disables cgo; cgo files will not be compiled, and C/C++ dependencies will not be compiled or linked.

To compile a Go target into WASM, run the following command

bazel build [email protected]_bazel_rules_go//go/toolchain:js_wasm //go:hello_go

Note that this will change all future builds of that particular Go package to WASM. To change it back you’ll have to build again with a toolchain matching the GOOS / GOARCH you want to build for.

bazel build \
[email protected]_bazel_rules_go//go/toolchain:darwin_amd64 \
//go:hello_go

Rust

Bazel also provides a set of rules to work with Rust and its ecosystem. The idea, as with Go and C/C++, is to be able to code backend software in the same repository as the frontend or be able to create WASM modules to address Javascript hot path bottlenecks in the frontend.

To get started add the following block to the WORKSPACE file:

http_archive(
  name = "io_bazel_rules_rust",
  sha256 = "0e2e633bf0f7f25392ffb477d677c88eb34fe70ffae05e3ad92fdd9f8d6579db",
  strip_prefix = "rules_rust-bc0578798f50d018ca4278ad5610598c400992c9",
  urls = [
    # Master branch as of 2020-12-05
    "https://github.com/bazelbuild/rules_rust/archive/bc0578798f50d018ca4278ad5610598c400992c9.tar.gz",
  ],
)

load("@io_bazel_rules_rust//rust:repositories.bzl", "rust_repositories")

rust_repositories()

The file we’re using to test the build is simple, it’s a hello world example in Rust.

// This is the main function
fn main() {
    // Print text to the console
    println!("Hello World!");
    println!("I'm a Rustacean in training!")
}

The BUILD command is simple.

load("@io_bazel_rules_rust//rust:rust.bzl", "rust_binary")

rust_binary(
    name = "hello_rust",
    srcs = ["src/main.rs"],
)

However, there is a problem with the build. I generated the repository with Cargo using cargo init. But the documentation for rust_rules proposes a different system, one that doesn’t use the language’s built-in tools, at least not directly.

This means that the project either works with the standard Rust tools or with Bazel, not with both.

If you look at the example on Github you will see that it has no Cargo.toml or Cargo.lock but it still build the binaries and libraries when using Bazel.

The project also depends on a library crate also located as a top-level child on the workspace. Moving them to a directory under the workspace (meaning they are not at the top level) will result in an error because Bazel cannot find the BUILDFILE for either crate.

To run the code in rust2/hello_rust from the root of the project, use the following command:

bazel build //rust2/hello_rust:all

Bazel has no concept of default build tasks so the :all component is important. Using a command like this means you can skip a BUILD file at the root of every package 🙂

Using Rust External Dependencies

Currently, the most common approach to managing external dependencies is using cargo-raze to generate BUILD files for Cargo crates.

This sounds like the best way to bring an existing Rust project into Bazel.

WebAssembly

To build a rust_binary for wasm32-unknown-unknown target add the [email protected]_bazel_rules_rust//rust/platform:wasm flag to your bazel build command.

bazel build @examples//hello_world_wasm /
[email protected]_bazel_rules_rust//rust/platform:wasm

To build a rust_binary for wasm32-wasi target add the –[email protected]_bazel_rules_rust//rust/platform:wasi flag.

bazel build @examples//hello_world_wasm /
[email protected]_bazel_rules_rust//rust/platform:wasi

rust_wasm_bindgen will automatically transition to the wasm platform and can be used when building WebAssembly code for the host target.

C++

If you will work with C++ code on Bazel it’s slightly different than working with Go or Rust.

Loading rules for C++ is not necessary. The rules are built into Bazel so we can safely skip loading them in the WORKSPACE we want to work on so we can concentrate on the code and the BUILD files.

The first example builds a binary from a single file without dependencies.

This is the BUILD file we use to build the package:

load("@rules_cc//cc:defs.bzl", "cc_binary")

cc_binary(
  name = "hello-world",
  srcs = ["hello-world.cc"],
)

Note that we load the cc_binary rule from the rules_cc ruleset, even though we didn’t load it into the workspace, it just works.

The second example builds a library and integrates it with a binary.

The BUILDFILE defines two targets, a library, and a binary.

The hello_greet library target uses a C++ file and a header.

The hello-world binary target uses a single C++ file and depends on the library we created.

load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")

cc_library(
    name = "hello-greet",
    srcs = ["hello-greet.cc"],
    hdrs = ["hello-greet.h"],
)

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
    deps = [
        ":hello-greet",
    ],
)

See the cpp_tutorial in the Bazel examples repository for the code for these examples.