Building Rust Code - Using Make

This series of posts is about building Rust code. In the first post I covered the current issues (and my solutions) around building Rust using external tooling. This post will cover using Make to build Rust projects.

The Example Crate

For this post, I'm going to use the rust-geom library as an example. It is a simple Rust library used by Servo to handle common geometric tasks like dealing with points, rectangles, and matrices. It is pure Rust code, has no dependencies, and includes some unit tests.

We want to build a dynamic library and the test suite, and the Makefile should be able to run the test suite by using make check. As much as possible, we'll use the same crate structure that rustpkg uses so that once rustpkg is ready for real use, the transition to it will be painless.

Makefile Abstractions

Did you know that Makefiles can define functions? It's a little clumsy, but it works and you can abstract a bunch of the tedium away. I'd never really noticed them before dealing with the Rust and Servo build systems, which use them heavily.

By using shell commands like shasum and sed, we can compute crate hashes, and by using Make's eval function, we can dynamically define new targets. I've created a rust.mk which can be included in a Makefile that makes it really easy to build Rust crates.

Magical Makefiles

Let's look at a Makefile for rust-geom which uses rust.mk.

include rust.mk

RUSTC ?= rustc
RUSTFLAGS ?=

.PHONY : all
all: rust-geom

.PHONY : check
check: check-rust-geom

$(eval $(call RUST_CRATE, .))

It includes rust.mk, sets up some basic variables that control the compiler and flags, and then defines the top level targets. The magic bit comes the call to RUST_CRATE which takes a path to where a crate's lib.rs and test.rs are located. In this case the path is the current directory, ..

RUST_CRATE finds the pkgid attribute in the crate and uses this to compute the crate's name, hash, and the output filename for the library. It then creates a target with the same name as the crate name, in this case rust-geom, and a target for the output file for the library. It uses the Rust compiler's support for dependency information so that it will know exactly when it needs to recompile things.

If the crate contains a test.rs file, it will also create a target that compiles the tests for the crates into an executable as well as a target to run the tests. The executable will be named after the crate; for rust-geom it will be named rust-geom-test. The check target is also named after the crate, check-rust-geom.

The files lib.rs and test.rs are the files rustpkg itself uses by default. This Makefile does not support the pkg.rs custom build logic, but if you need custom logic, it is easy enough to modify this example. One benefit of following in rustpkg's footsteps here is that this same crate should be buildable with rustpkg without modification.

Behind the Scenes

rust.mk is a a little ugly, but not too bad. It defines a few helper functions like RUST_CRATE_PKGID and RUST_CRATE_HASH which are used by the main RUST_CRATE function. The syntax is a bit silly because of the use of eval and the need to escape $s, but it shouldn't be too hard to follow if you're already familiar with Make syntax.

RUST_CRATE_PKGID = $(shell sed -ne 's/^#[ *pkgid *= *"(.*)" *];$$/\1/p' $(firstword $(1)))
RUST_CRATE_PATH = $(shell printf $(1) | sed -ne 's/^([^#]*)\/.*$$/\1/p')
RUST_CRATE_NAME = $(shell printf $(1) | sed -ne 's/^([^#]*\/){0,1}([^#]*).*$$/\2/p')
RUST_CRATE_VERSION = $(shell printf $(1) | sed -ne 's/^[^#]*#(.*)$$/\1/p')
RUST_CRATE_HASH = $(shell printf $(strip $(1)) | shasum -a 256 | sed -ne 's/^(.{8}).*$$/\1/p')

ifeq ($(shell uname),Darwin)
RUST_DYLIB_EXT=dylib
else
RUST_DYLIB_EXT=so
endif

define RUST_CRATE

_rust_crate_dir = $(dir $(1))
_rust_crate_lib = $$(_rust_crate_dir)lib.rs
_rust_crate_test = $$(_rust_crate_dir)test.rs

_rust_crate_pkgid = $$(call RUST_CRATE_PKGID, $$(_rust_crate_lib))
_rust_crate_name = $$(call RUST_CRATE_NAME, $$(_rust_crate_pkgid))
_rust_crate_version = $$(call RUST_CRATE_VERSION, $$(_rust_crate_pkgid))
_rust_crate_hash = $$(call RUST_CRATE_HASH, $$(_rust_crate_pkgid))
_rust_crate_dylib = lib$$(_rust_crate_name)-$$(_rust_crate_hash)-$$(_rust_crate_version).$(RUST_DYLIB_EXT)

.PHONY : $$(_rust_crate_name)
$$(_rust_crate_name) : $$(_rust_crate_dylib)

$$(_rust_crate_dylib) : $$(_rust_crate_lib)
    $$(RUSTC) $$(RUSTFLAGS) --dep-info --lib $$<

-include $$(patsubst %.rs,%.d,$$(_rust_crate_lib))

ifneq ($$(wildcard $$(_rust_crate_test)),"")

.PHONY : check-$$(_rust_crate_name)
check-$$(_rust_crate_name): $$(_rust_crate_name)-test
    ./$$(_rust_crate_name)-test

$$(_rust_crate_name)-test : $$(_rust_crate_test)
    $$(RUSTC) $$(RUSTFLAGS) --dep-info --test $$< -o $$@

-include $$(patsubst %.rs,%.d,$$(_rust_crate_test))

endif

endef

If you wanted, you could add the crate's target and the check target to the all and check targets within this function, simplifying the main Makefile. You could also have it generate an appropriate clean-rust-geom target as well.

It's not going to win a beauty contest, but it will get the job done nicely.

Next Up

In the next post, I plan to show the same example, but using CMake.

more info