Today I released librsvg 2.41.1, and it's a big release! Apart from all the Rust goodness, and the large number of bug fixes, I am very happy with the way the build system works these days. I've found it invaluable to have good examples of Autotools incantations to copy&paste, so hopefully this will be useful to someone else.
There are some subtleties that a "good" autotools setup demands, and so far I think librsvg is doing well:
-
The
configurescript checks forcargoandrustc. -
"
make distcheck" works. This means that the build can be performed withbuilddir != srcdir, and also thatmake checkruns the available tests and they all pass. -
The
rsvg_internalslibrary is built with Rust, and ourMakefile.amcallscargo buildwith the correct options. It is able to handle debug and release builds. -
"
make clean" cleans up the Rust build directories as well. -
If you change a
.rsfile and typemake, only the necessary stuff gets rebuilt. -
Etcetera. I think librsvg feels like a normal autotool'ed library. Let's see how this is done.
Librsvg's basic autotools setup
Librsvg started out with a fairly traditional autotools setup with a
configure.ac and Makefile.am. For
historical reasons the .[ch] source files live in the toplevel
librsvg/ directory, not in a src subdirectory or something like
that.
librsvg
├ configure.ac
├ Makefile.am
├ *.[ch]
├ src/
├ doc/
├ tests/
└ win32/
Adding Rust to the build
The Rust source code lives in librsvg/rust; that's
where Cargo.toml lives, and of course there is the conventional
src subdirectory with the *.rs files.
librsvg
├ configure.ac
├ Makefile.am
├ *.[ch]
├ src/
├ rust/ <--- this is new!
│ ├ Cargo.toml
│ └ src/
├ doc/
├ tests/
└ win32/
Detecting the presence of cargo and rustc in configure.ac
This goes in configure.ac:
AC_CHECK_PROG(CARGO, [cargo], [yes], [no])
AS_IF(test x$CARGO = xno,
AC_MSG_ERROR([cargo is required. Please install the Rust toolchain from https://www.rust-lang.org/])
)
AC_CHECK_PROG(RUSTC, [rustc], [yes], [no])
AS_IF(test x$RUSTC = xno,
AC_MSG_ERROR([rustc is required. Please install the Rust toolchain from https://www.rust-lang.org/])
)
These two try to execute cargo and rustc, respectively, and abort
with an error message if they are not present.
Supporting debug or release mode for the Rust build
One can call cargo like "cargo build --release" to turn on expensive
optimizations, or normally like just "cargo build" to build with
debug information. That is, the latter is the default: if you don't
pass any options, cargo does a debug build.
Autotools and C compilers normally work a bit differently; one must
call the configure script like "CFLAGS='-g -O0' ./configure" for a
debug build, or "CFLAGS='-O2 -fomit-frame-pointer' ./configure" for
a release build.
Linux distros already have all the infrastructure to pass the
appropriate CFLAGS to configure. We need to be able to pass the
appropriate flag to Cargo. My main requirement for this was:
- Distros shouldn't have to substantially change their RPM specfiles (or whatever) to accomodate the Rust build.
- I assume that distros will want to make release builds by default.
- I as a developer am comfortable with passing extra options to make debug builds on my machine.
The scheme in librsvg lets you run "configure --enable-debug" to
make it call a plain cargo build, or a plain "configure" to make
it use cargo build --release instead. The CFLAGS are passed as
usual through an environment variable. This way, distros don't have
to change their packaging to keep on making release builds as usual.
This goes in configure.ac:
dnl Specify --enable-debug to make a development release. By default,
dnl we build in public release mode.
AC_ARG_ENABLE(debug,
AC_HELP_STRING([--enable-debug],
[Build Rust code with debugging information [default=no]]),
[debug_release=$enableval],
[debug_release=no])
AC_MSG_CHECKING(whether to build Rust code with debugging information)
if test "x$debug_release" = "xyes" ; then
AC_MSG_RESULT(yes)
RUST_TARGET_SUBDIR=debug
else
AC_MSG_RESULT(no)
RUST_TARGET_SUBDIR=release
fi
AM_CONDITIONAL([DEBUG_RELEASE], [test "x$debug_release" = "xyes"])
AC_SUBST([RUST_TARGET_SUBDIR])
This defines an Automake conditional called DEBUG_RELEASE, which we
will use in Makefile.am later.
It also causes @RUST_TARGET_SUBDIR@ to be substituted in Makefile.am
with either debug or release; we will see what these are about.
Adding Rust source files
The librsvg/rust/src directory has all the *.rs files, and cargo
tracks their dependencies and whether they need to be rebuilt if one changes.
However, since that directory is not tracked by make, it won't
rebuild things if a Rust source file changes! So, we need to tell our
Makefile.am about those files:
RUST_SOURCES = \
rust/build.rs \
rust/Cargo.toml \
rust/src/aspect_ratio.rs \
rust/src/bbox.rs \
rust/src/cnode.rs \
rust/src/color.rs \
...
RUST_EXTRA = \
rust/Cargo.lock
EXTRA_DIST += $(RUST_SOURCES) $(RUST_EXTRA)
It's a bit unfortunate that the change tracking is duplicated in the
Makefile, but we are already used to listing all the C source files
in there, anyway.
Most notably, the rust subdirectory is not listed in the SUBDIRS
in Makefile.am, since there is no rust/Makefile at all!
Cargo release or debug build?
if DEBUG_RELEASE
CARGO_RELEASE_ARGS=
else
CARGO_RELEASE_ARGS=--release
endif
We will call cargo build with that argument later.
Verbose or quiet build?
Librsvg uses AM_SILENT_RULES([yes]) in configure.ac. This lets
you just run "make" for a quiet build, or "make V=1" to get the
full command lines passed to the compiler. Cargo supports something
similar, so let's add it to Makefile.am:
CARGO_VERBOSE = $(cargo_verbose_$(V))
cargo_verbose_ = $(cargo_verbose_$(AM_DEFAULT_VERBOSITY))
cargo_verbose_0 =
cargo_verbose_1 = --verbose
This expands the V variable to empty, 0, or 1. The result of
expanding that gives us the final command-line argument in the
CARGO_VERBOSE variable.
What's the filename of the library we are building?
RUST_LIB=@abs_top_builddir@/rust/target/@RUST_TARGET_SUBDIR@/librsvg_internals.a
Remember our @RUST_TARGET_SUBDIR@ from configure.ac? If you call
plain "cargo build", it will put the binaries in
rust/target/debug. But if you call "cargo build --release", it
will put the binaries in rust/target/release.
With the bit above, the RUST_LIB variable now has the correct path
for the built library. The @abs_top_builddir@ makes it work when
the build directory is not the same as the source directory.
Okay, so how do we call cargo?
@abs_top_builddir@/rust/target/@RUST_TARGET_SUBDIR@/librsvg_internals.a: $(RUST_SOURCES)
cd $(top_srcdir)/rust && \
CARGO_TARGET_DIR=@abs_top_builddir@/rust/target cargo build $(CARGO_VERBOSE) $(CARGO_RELEASE_ARGS)
We make the funky library filename depend on $(RUST_SOURCES).
That's what will cause make to rebuild the Rust library if one of
the Rust source files changes.
We override the CARGO_TARGET_DIR with Automake's preference, and
call cargo build with the correct arguments.
Linking into the main C library
librsvg_@RSVG_API_MAJOR_VERSION@_la_LIBADD = \
$(LIBRSVG_LIBS) \
$(LIBM) \
$(RUST_LIB)
This expands our $(RUST_LIB) from above into our linker line, along
with librsvg's other dependencies.
make check
This is our hook so that make check will cause cargo test to run:
check-local:
cd $(srcdir)/rust && \
CARGO_TARGET_DIR=@abs_top_builddir@/rust/target cargo test
make clean
Same thing for make clean and cargo clean:
clean-local:
cd $(top_srcdir)/rust && \
CARGO_TARGET_DIR=@abs_top_builddir@/rust/target cargo clean
Vendoring dependencies
Linux distros probably want Rust packages to come bundled with their dependencies, so that they can replace them later with newer/patched versions.
Here is a hook so that make dist will cause cargo vendor to be
run before making the tarball. That command will creates a
rust/vendor directory with a copy of all the Rust crates that
librsvg depends on.
RUST_EXTRA += rust/cargo-vendor-config
dist-hook:
(cd $(distdir)/rust && \
cargo vendor -q && \
mkdir .cargo && \
cp cargo-vendor-config .cargo/config)
The tarball needs to have a rust/.cargo/config to know where to find
the vendored sources (i.e. the embedded dependencies), but we don't
want that in our development source tree. Instead, we generate it
from a rust/cargo-vendor-config file in our
source tree:
# This is used after `cargo vendor` is run from `make dist`.
#
# In the distributed tarball, this file should end up in
# rust/.cargo/config
[source.crates-io]
registry = 'https://github.com/rust-lang/crates.io-index'
replace-with = 'vendored-sources'
[source.vendored-sources]
directory = './vendor'
One last thing
If you put this in your Cargo.toml, release binaries will be a lot
smaller. This turns on link-time optimizations (LTO), which removes
unused functions from the binary.
[profile.release]
lto = true
Summary and thanks
I think the above is some good boilerplate that you can put in your
configure.ac / Makefile.am to integrate a Rust sub-library into
your C code. It handles make-y things like make clean and make
check; debug and release builds; verbose and quiet builds;
builddir != srcdir; all the goodies.
I think the only thing I'm missing is to check for the cargo-vendor
binary. I'm not sure how to only check for that if I'm the one making
tarballs... maybe an --enable-maintainer-mode flag?
This would definitely not have been possible without prior work. Thanks to everyone who figured out Autotools before me, so I could cut&paste your goodies:
Update 2017/Nov/11: Fixed the initialization of RUST_EXTRA; thanks
to Tobias Mueller for catching this.