Building Rust Code - Using Make
December 12, 2013
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 Makefile
s 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.