- 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.
And, oh, @graydon also came up with a cool idea for a
rustpkg.org
redirection service which we could all use. It would allow for packages to be moved from one service to another without having to update any source files, e.g. code could refer to:So I could initially have it pointing to
github.com/tav/spdy
and should I get tired of maintaining it, I could update it to point togithub.com/stooge/spdy
😉To add to this, it might also be useful to have a
rust map
command which updates a~/.rustmap.yaml
file, e.g.The
rust
tool would then rewrite references when building. This would be especially useful in production environments when you want to perhaps refer to local mirrors instead of public sources which may change underneath you or not be accessible due to some outage.