Week 7: Rust Tooling

This might be one of my favorite articles to write because I immediately fell in love with the Rust tooling ecosystem.

TL;DR

Imagine if yarn, make, and a cousin of package.json merged into one behemoth of a stack. Welcome to cargo

This article is framed  as beginner guide to installing a basic Rust project as this process will expose most users to the most prominent and useful features of the Rust ecosystem.

Toolchain

First and foremost is the Rust toolchain, rustup. I mentioned this briefly in Week 3, but to add the toolchain to your computer, you can use a command like brew install rustup-init to install and setup the default toolchain.

Build Tools

With rustup comes rustc and cargo . The other language equivalents would be python and pip for Python, and node and npm for NodeJS (but neither are a compiled language like Rust). rustc is the native compiler for Rust and simply put, turns code into an executable. You can pass flags to enable optimizations and debugging symbols, just like in other compilers. You will not need to invoke rustc manually in most cases, as this is the job of cargo ! Cargo is the native package manager and build tool for Rust. A key component of the rust ecosystem are packages called “crates”, which represents projects written in rust that can be compiled. Every crate has a single Cargo.toml which is the equivalent to a package.json in a javascript project. Here is an example of a Cargo.toml:

[package]
name = "legion_prof"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4"
clap = "2.33"
csv = "1.1"
derive_more = { version = "0.99", default_features = false, features = ["add", "display", "from"] }
flate2 = "1"
nom = "7"
num_enum = "0.5"
rayon = "1.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
legion_prof_viewer = { path = "/Users/adam/legion/prof-viewer"}

[profile.dev]
opt-level = 2
debug-assertions = true

In this .toml file, you can see that we define a crate metadata under the header [package]  such as the name, version, and schema. We also declare the dependencies of our crate, which utilizes a registry called crates.io (or can point to a local path, to link local development).

One notable feature of Rust crates is that they are compiled statically, which means that all of their dependencies are included in the final binary or library. This makes it easy to distribute and use Rust code without worrying about version conflicts or external dependencies.

One of the benefits of interpreted languages is that dependencies are loaded at runtime (i.e. Node just looks up a module every time you run your program). In a compiled language like Rust, dependency management becomes more complex, however Cargo does a great job of optimizing builds by only rebuilding the components that have changed since the last build. This saves a great deal of time since you don’t have to recompile 300+ crates in a medium-sized project every time you change a line.

Crate Features

Another feature of Rust crates is that they are typically designed to be highly composable. This means that they can be easily combined with other crates to create larger, more complex programs. A feature that I enjoyed about crates is it’s use of “features”. A feature is an optional include that you can make on a dependency to enable extra features. This is extremely useful for reducing the dependencies of your project. For example, a perfect use case of this feature was splitting server-side code from the frontend egui viewer. In the same crate, we provide both the server-side rendering features that a server would need (along with all the dependencies associated with that) as well as the egui GUI that we built. Cargo is smart enough to detect the dependencies that only exist under a feature and therefore not include them if the feature is not requested.

Neat Features

  • Cargo.lock this lock file represents the dependency tree needed to build a project, and saves cargo vasts amount of computation and network lookups to download all the dependencies needed to build a project.
  • Cargo manages multiple versions of the same crate to prevent conflicts.
  • cargo test executes native Rust tests on a crate. TDD is baked into Rust!
  • cargo doc generates static html documentation pages very similar to Python package documentation.
  • Cargo can create a new project template with cargo new.
  • Cargo has a built-in system for managing and publishing crates to the crates.io registry.
Cargo docs made me feel comfortable diving into crates' codebases.

Once you have finished development on your product, deployment is the easiest part. You already are executing debug binaries locally to test your app, so you can simply bundle everything together with production-level optimizations with cargo build --release and you now have a working executable.

To deploy to a specific target (MacOS in my case), I found cargo-bundle to be extremely useful.

Tiny!

I was able to deploy prof-viewer as 5.7 MB a native .app. A more traditional way of deploying a desktop app would be Electron, which utilizes a chromium base to simply render a webpage as an app. This can lead to a several-hundred megabyte binary that users have to download for each app they download that includes Electron. Rust on the other hand is much more portable.