- Purely functional
- Different versions of packages can coexist
- Each package must have a UUID, so packages with conflicting names can coexist
- Build logic
- Each package's metadata and build logic is written in a central Rust source file,
package.rs
- Dependencies are declared in the package file and will be fetched and installed before
- Packages are built in a separate directory before installing into the package store to ensure that broken packages are not installed
- Configuration flags can be passed to the package file via the CLI allowing the developer to configure for special opt-in cases
- Multiple crates (libraries and binaries) to build can be defined in the package file
- Package files can define flags and configuration options to be passed to crates being compiled
- Each package's metadata and build logic is written in a central Rust source file,
- Supports symlinking binaries from one version of a package at a time into a directory, allowing you to add that directory to your path
Haul is a concept for a revamp of Cargo (with the final product still being named Cargo, Haul is a "codename" of sorts). Whether or not Mozilla folk want it is another story.
Haul is a purely functional package manager and build system for the Rust
programming language inspired by Nix and Leiningen. Like Leiningen, each package
(with a package consisting of single or multiple libraries and binaries, a.k.a. crates)
has its build logic written in the host language itself, Rust. The simplest
Haul package file (named package.rs
in the root directory) is such:
#[pkg(uuid = "ad8e8d02-a537-418b-b1f9-6b3d8380e726",
name = "haul",
vers = "0.5.2")];
#[pkg_crate("src/haul.rc")];
#[pkg_dep("git://github.com/rapter-jesus/semver", target("0.1.0"))];
This simple configuration figures out how to build and install the package (and dependencies) from the declarative attributes. If you want to do custom build logic (such as probing out to the system for configuration), you can add a build listener:
#[pkg(uuid = "ad8e8d02-a537-418b-b1f9-6b3d8380e726",
name = "haul",
vers = "0.5.2")];
#[pkg_dep("git://github.com/rapter-jesus/semver", target("0.1.0"))];
#[pkg_build]
fn build() {
// Every package.rs automatically has `extern mod rustpkg;` injected
// at the top of the file, much like how core is injected
let platform = if os::is_toaster() { ~"toaster" } else { ~"robot" };
let crate = rustpkg::Crate(~"src/haul.rc").cfg(~"platform=" + platform);
rustpkg::build(~[crate]);
}
The frontend of the build API follows a FP style (to fit in with Haul's label) and uses core::workcache
to only recompile code when it has changed.
Generally, it is frowned upon to use custom build logic unless you really, really need it.
One use case is to run configuration and Makefiles for native library dependencies, to which the build API provides handy wrappers
(around some build systems, mostly automake) which also use workcache to ensure they are only
ran when they need to be.
#[pkg(uuid = "ea9ae194-eb20-4027-ab77-7835962094b6",
name = "cairo",
vers = "1.3.3")];
#[pkg_build]
fn build() {
use rustpkg::util;
let cairo_dir = os::getcwd().push_many(~[~"src", ~"cairo"]);
let crate = rustpkg::Crate(~"src/cairo.rc");
util::configure(cairo_dir, ~[]); // run configure <args> in src/cairo (only if configure has changed or hasn't been run yet)
util::make(cairo_dir, ~[]); // run make -C <args> src/cairo (will always run, relies on the makefile to cache itself)
rustpkg::build(~[crate]);
}
Haul is described as a purely functional package manager. It installs
packages (a collection of binaries and libraries) to a unique directory
based on the package UUID, name and version (<haul-dir>/store/<name>-<hash>-<version>
),
allowing packages to coexist. This is very analagous to the way Rust's
libraries work by default. When a Rust library is built, it has its name,
version and a cryptographic hash tagged into its output filename. This
allows multiple versions of the same library to coexist and be linked in.
Haul allows one package to consist of multiple libraries and the library
names can be called anything (so they could conflict with other packages),
which means Rust's default system doesn't work out in some cases. So Haul
allocates a unique directory for each package where its libraries and
binaries are installed. When you specify a dependency for a package to be compiled,
Haul automatically adds the link flag to search for libraries in the package's library
directory (<haul-dir>/store/<name>-<hash>-<version>/lib
). Of course, binaries
can not be handled as elegantly.
When you install a package with binaries,
it will install it to <haul-dir>/store/<name>-<hash>-<version>/bin
. This doesn't
allow you to easily run the binary unless you add all specific binary store
directories that you want to use to your path, which is simply impractical.
Instead of resorting to this nastiness, Haul provides "using" functionality,
which will symlink a package's binaries into <haul-dir>/bin
which can then
be added to the path. This is of course not purely functional and only one
version of the package can be used at a time, but it is the price to pay for
usability.
There is no central repository. All packages are installed from URLs where HTTP, FTP and Git are supported in a fashion similar to Go.
You can install a package with haul in [options] <url>
or from the current working directory using
only haul in
.
haul in # from the cwd
haul in git://github.com/raptor-jesus/regex
haul in git://github.com/raptor-jesus/regex -t v3.0.1
haul in http://raptor-jesus.me/regex-0.1.0.tar.gz
haul in --cfg waffles=1
-c, --cfg
- pass a cfg flag to thepackage.rs
file-u, --use
- use the package's binaries (see introduction) after installation, asking for confirmation on conflictions-t, --target
- if installing via Git, it will checkout this branch/tag before installing as their is no central repository, it is standard to tag the Git master for each release so that users can download certain versions
You can uninstall a package using haul out <name>[@<version>]
.
This will remove all binaries and libraries installed into the store for a
specific version if the package is not dependended on by another package.
If version
is omitted, all versions of that package that are removed,
except ones which are depended on by other packages. The user is asked to confirm
if the package is currently has it's binaries symlinked / used (see using).
You can symlink a specific package's binaries to <haul-dir>/bin
(see introduction) using haul use <name>@<version>
. If version
is omitted, it will use the latest installed version of that package.
haul unuse <name>
will unuse any used package's binaries
(removes symlinks of binaries from that package placed in <haul-dir>/bin
).
haul in [email protected] # install an older version of the machine package (provides machine)
haul in -u machine # install the latest (v0.1.3) machine package and use
machine -v
v0.1.3
haul use [email protected]
machine -v
v0.1.2
haul unuse machine
machine -v
< no such file >
You can build a Haul package from the current directory using haul build [options]
.
haul clean
will clean the package's build directory (i.e. it will all need to be rebuilt).
It will be built into <haul-dir>/build/<name>-<hash>-<version>
.
haul build --cfg platform=toaster
haul clean
-c, --cfg
- pass a cfg flag to thepackage.rs
file
If you want to run all unit tests in all the source files across a package (i.e. pass
--test
to all libraries and binaries when building) use haul [options] test
.
Haul will build the bootstrapped test executables into <haul-dir>/test/<name>-<hash>-<version>
and then run them. All output of the tests will redirected to stdout
.
-c, --cfg
- pass a cfg flag to thepackage.rs
file
Q. Rust has a meta language built in, why not implement the build logic in that and directly extract it from the Rust code?
A. Rust's meta macro language is bloody awesome, but it's not powerful enough for implementing a build process and in my opinion any attempt in that direction will not turn out as elegant as writing it in straight Rust. I'd love to stand corrected, though.
Q. Why did you label this has purely functional if it doesn't strictly follow a purely functional system?
A. Because I'm a dirty rotten liar.
Q. Rust already has a package manager, Cargo. Why are you reinventing the wheel?
A. As of the creation of this project, Cargo wasn't polished enough for general usage and didn't really work well. Also, this is mainly an experiment for fun and might not get anywhere.
Hey Zack,
Thanks for this. We definitely need to improve the build/packaging story in Rust. I hope you don't mind if I offer up a constructive comparison with the go tool for the Go language:
go
— easy to remember.$ go build $ go clean $ go install $ go test
Makefile
,package.json
,project.clj
,setup.py
or other crap. All metadata is implicit.go
tool automatically figures out, downloads and builds all dependencies by just parsing the source files for import statements. Simples!http://
orgit://
. The package names are actually just strings which map to local directories on the$GOPATH
. The build mechanism just happens to be clever enough to download it over the internet when building/installing.github.com/tav/html5
package whilst developing thegithub.com/tav/html-sanitiser
tool by just putting them as subdirectories of my local$GOPATH/github.com/tav
directory. And once I'm happy to release them to the world, I can just push to GitHub without having to change a single line of code or config.--cfg
params. Just setGOARCH
andGOOS
, e.g.// +build !windows
go get
it, e.g.go get github.com/nsf/gocode
. Compiled binaries are installed to the$GOBIN
directory. No need to fudge around with symlinks. Binaries are simply over-written by newer versions — which, arguably, is what most users want.go doc
on the path. Or just append the path to a third-party service, e.g. http://godoc.org/github.com/agl/pandaThe success of Go — in that there are thousands of packages available for such a young language — is very much due to the quality of this tooling around it. It makes it effortless for developers to both experiment with the language as well as publish and use code.
Do you see any reason why we can't simply mimic this wholesale for a
rust
tool? My thoughts from a previous discussion with @graydon was to simply leverageextern mod
statements in a similar manner to Go's import paths, e.g.The
rust
build logic can then parse source files for these to figure out dependencies. The main addition which seemed worthwhile was to add support for specific versions which could map to branches/tags in version control, e.g.To this I would also now add your idea of a build listener, i.e.
Whilst I see no need for that in pure-Rust code, it makes sense for code depending on external C libraries, etc. Anyway, that's enough rambling from me for a Saturday morning. I hope this has been useful in some way. What do you think?
—
All the best, tav