Using buck to build Rust projects

Home Blog

2023-04-13

A few days ago, Facebook/Meta/idk announced that buck2 is now open source.

Buck2 is an extensible and performant build system written in Rust and designed to make your build experience faster and more efficient.

As it just so happens, I have taken an increasing interest in build systems lately. I have mostly ignored the cool kids’ build system things, because I have never worked at a FAANG, or at a true monorepo shop. I also personally try and avoid the JVM wherever possible, and the first generation of these tools were all built on top of it. (Yes, that bias may be outdated, I’m not saying you should avoid the JVM, just stating my own bias up front.)

So this timing was perfect! Let’s explore what using buck looks like.

A brief aside: listen, I have no love for Facebook. I deleted my account almost a dozen years ago. That doesn’t mean I’m not interested in using good tools they produce. If you feel differently, fine, but that’s not what I want to talk about today, so I’m not going to.

Oh, one last bit before I begin: I’m not going to do a lot of motivating on “why would you want to use Buck?” in this post. There’s a few reasons for that, but for now, if that’s what you’re looking for, this post isn’t it. We’re doing this purely for the novelty of trying out some new tech right now. I will probably end up with a post giving better motivations at some point in the future, but I think it makes more sense once you see how it works, rather than starting there.

A series

This post is part of a series:

This post represents how to do this at the time that this was posted; future posts may update or change something that happens here. Here’s a hopefully complete but possibly incomplete list of updates and the posts that talk about it:

Getting started with buck2

The Getting Started page will give you instructions on installing buck. As of this moment, the instructions are:

$ rustup install nightly-2023-01-24
$ cargo +nightly-2023-01-24 install --git https://github.com/facebook/buck2.git buck2

This is mega-convenient for me as a Rust user, but probably not if you don’t have Rust installed. That said, this is a first release, and so I don’t expect anything fancier. This is what cargo install is good for!

Let’s make a new directory, hello:

$ mkdir buck-rust-hello
$ cd buck-rust-hello

To initialize a project, we use this command:

$ buck2 init --git

Before we move forward, let’s examine what this generated for us.

Initial project files

We now have this stuff in our directory:

$ git add .
$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   .buckconfig
        new file:   .gitignore
        new file:   .gitmodules
        new file:   BUCK
        new file:   prelude
        new file:   toolchains/BUCK

Let’s talk about each of these in turn.

.buckconfig

The .buckconfig file is… a configuration file for Buck, go figure. It looks like this:

[repositories]
root = .
prelude = prelude
toolchains = toolchains
none = none

[repository_aliases]
config = prelude
fbcode = none
fbsource = none
buck = none

[parser]
target_platform_detector_spec = target:root//...->prelude//platforms:default

That none = none is kind of amusing. Regardless of that, this file is extremely important: it configures the entire thing. In a sense, it’s like Cargo.toml: just like a package is defined by the existence of a Cargo.toml, a .buckconfig defines the existence of a… cell. Which defines a package. We’ll get there. Point is, this is the top level configuration. We say that the repository root is in the current directory, we’d like to use the default prelude and toolchains.

I uh… I don’t know what the none = none is for. It might just be a bug. I haven’t seen it in some of the other config files I’ve poked at. Let’s just leave that alone for now. I do have a suspicion though… and it involves the next section.

We also have a table for repository aliases. I couldn’t find any documentation on this, but I would imagine this means we could use the name config instead of prelude later. Looks like we don’t have any way to refer to fbcode and fbsource, which makes sense, and same with buck.

(I wonder if this is what the none = none is about above, defining a sort of “none repository” that we can then alias these to.)

Finally, we have a parser table, with one entry, pointing out where a thing exists. I know this configures Buck’s parser, but other than that… I’m sure I’ll figure it out eventually.

.gitmodules & prelude

We have a git submodule, pointing to https://github.com/facebook/buck2-prelude.git, that lives at the prelude directory. If you poke around in there, you’ll find a bunch of .bzl files that implement useful features for us to use. We’ll get into those in a moment, but the point is that this is sort of like a ‘standard library’ if you will. You could also not use it and define your own. If you’re that kind of person.

.gitignore

A very simple .gitignore will be created, that contains one line: /buck-out. This is where buck stores artifacts produced by your builds, so we don’t want that checked into version control.

BUCK

Now we get to the good stuff. Here’s the generated BUCK file:

# A list of available rules and their signatures can be found here: https://buck2.build/docs/generated/starlark/prelude/prelude.bzl

genrule(
    name = "hello_world",
    out = "out.txt",
    cmd = "echo BUILT BY BUCK2> $OUT",
)

genrule is like a function, provided by our prelude. If you’re curious, the implementation is in prelude/genrule.bzl. This command, as you may imagine, sets up a rule, named hello_world, that produces a file called out.txt. It does this by running the cmd. Nice and straightforward. We’ll give that a try in a moment, but first, one last file:

toolchains/BUCK

This file describes a toolchain. Here’s the contents:

load("@prelude//toolchains:genrule.bzl", "system_genrule_toolchain")

system_genrule_toolchain(
    name = "genrule",
    visibility = ["PUBLIC"],
)

This loads a certain rule from the prelude, and then defines this as a public toolchain. We can define as many toolchains as we want here, for example, if we wanted to build both Rust and Python, we could define both toolchains here for later use.

The “genrule” toolchain is used to generate files from a shell command, as we saw before with our rule that produces out.txt. So, in my understanding, here we are defining that we wish to actually use that. And then, in the BUCK file, we’re using this toolchain to implement our rule.

Invoking our first build

Okay, let’s actually give this a shot. To instruct Buck to build something, we invoke it with the “target pattern” as an argument. Let’s ask Buck what targets it knows how to build. To do this:

C:\Users\steve\Documents\GitHub\buck-rust-hello〉buck2 targets //...                                                     04/10/2023 02:01:50 PM
Build ID: cd778a29-2ba4-484b-8956-dc67f6fc0625
Jobs completed: 4. Time elapsed: 0.0s.
root//:hello_world

The //... is a “target pattern.” The /... means “all build targets in build files in subdirectories”, and / means our root directory, so //... means “all targets in all build files in all subdirectories.” By passing this target to buck2 targets, we can see every target in the project. This shows our one target we’ve defined, root://:hello_world. This name was defined in our BUCK file above. If you change that to

genrule(
    name = "lol",

then buck2 targets //... would show root://:lol.

Let’s actually build our target:

〉buck2 build //:hello_world
File changed: root//BUCK
Build ID: 73f4b797-2238-47bc-8e43-7ffcb2b7d9b7
Jobs completed: 36. Time elapsed: 0.0s. Cache hits: 0%. Commands: 1 (cached: 0, remote: 0, local: 1)
BUILD SUCCEEDED

Okay the build succeeded, but where is our out.txt? We can ask buck!

〉buck2 build //:hello_world --show-output
Build ID: 7ce93845-ab1e-4b0a-9274-51fed9f9e295
Jobs completed: 3. Time elapsed: 0.0s.
BUILD SUCCEEDED
root//:hello_world buck-out\v2\gen\root\fb50fd37ce946800\__hello_world__\out\out.txt

It lives in a deeply nested subdirectory of buck-out, a new top-level directory that was created for us. If you remember from before, this directory is ignored in our .gitignore.

If we look at the file, you can see it contains the text we wanted it to contain.

Let’s build a second time!

〉buck2 build //:hello_world
File changed: root//.git/index.lock
File changed: root//.git
File changed: root//.git/modules/prelude/index.lock
31 additional file change events
Build ID: c00e4bfa-a1f8-40c7-a61c-2a394dca5da5
Jobs completed: 5. Time elapsed: 0.0s.
BUILD SUCCEEDED

Buck has noticed that we’ve changed some files, but since our rule doesn’t depend on any of them, we’re good to go.

Building some Rust code

Okay, echo to a file is fun, but let’s actually build some Rust. Create a file, hello.rs:

fn main() {
    println!("Hello, world!");
}

and then update the BUCK file to this:

rust_binary(
    name = "hello_world",
    srcs = ["hello.rs"],
    crate_root = "hello.rs",
)

This says “hey, we’re building a Rust binary, it has this target name, these source files, and the crate root lives here.” Given we only have one file, there’s some reptition. It happens. Let’s build:

〉buck2 build //:hello_world
File changed: root//BUCK
Error running analysis for `root//:hello_world (prelude//platforms:default#fb50fd37ce946800)`

Caused by:
    0: Error looking up configured node root//:hello_world (prelude//platforms:default#fb50fd37ce946800)
    1: Error looking up configured node toolchains//:cxx (prelude//platforms:default#fb50fd37ce946800) (prelude//platforms:default#fb50fd37ce946800)
    2: looking up unconfigured target node `toolchains//:cxx`
    3: Unknown target `cxx` from package `toolchains//`.
       Did you mean one of the 1 targets in toolchains//:BUCK?
Build ID: f126ce07-efe8-41d3-8aae-8b7d31a4dafc
Jobs completed: 4. Time elapsed: 0.0s.
BUILD FAILED

Oops! We didn’t set up a rust toolchain! Let’s do that now. Edit toolchains/BUCK:

load("@prelude//toolchains:rust.bzl", "system_rust_toolchain")

system_rust_toolchain(
    name = "rust",
    default_edition = "2021",
    visibility = ["PUBLIC"],
)

And… when we build again, the same error. Now. I am not 100% sure what’s going on here, but this is what we need to do:

load("@prelude//toolchains:rust.bzl", "system_rust_toolchain")
load("@prelude//toolchains:genrule.bzl", "system_genrule_toolchain")
load("@prelude//toolchains:cxx.bzl", "system_cxx_toolchain")
load("@prelude//toolchains:python.bzl", "system_python_bootstrap_toolchain")

system_genrule_toolchain(
    name = "genrule",
    visibility = ["PUBLIC"],
)

system_rust_toolchain(
    name = "rust",
    default_edition = "2021",
    visibility = ["PUBLIC"],
)

system_cxx_toolchain(
    name = "cxx",
    visibility = ["PUBLIC"],
)

system_python_bootstrap_toolchain(
    name = "python_bootstrap",
    visibility = ["PUBLIC"],
)

I believe that this is because, to compile the Rust compiler, we need Python and a C++ compiler. Well, I did believe that, but after digging into things some more, it’s that the Rust toolchain from the prelude depends on the CXX toolchain in the prelude, because the Rust toolchain invokes the C compiler to invoke the linker. I’m still not 100% sure why Python needs to be in there. Anyway.

Now, when I run, I got this:

〉buck2 build //:hello_world -v 3
Action failed: prelude//python_bootstrap/tools:win_python_wrapper (symlinked_dir resources)
Internal error: symlink(original=../../../../../../../../../prelude/python_bootstrap/tools/win_python_wrapper.bat, link=C:\Users\steve\Documents\GitHub\buck-rust-hello\buck-out\v2\gen\prelude\fb50fd37ce946800\python_bootstrap\tools\__win_python_wrapper__\resources\win_python_wrapper.bat): A required privilege is not held by the client. (os error 1314)
Build ID: 57a66885-f7e7-474b-a782-b49fc4425be9
Jobs completed: 14. Time elapsed: 0.0s.
BUILD FAILED
Failed to build 'prelude//python_bootstrap/tools:win_python_wrapper (prelude//platforms:default#fb50fd37ce946800)'

I got this becuase I’m on Windows, and Windows restricts the ability to create symlinks by default. Turning on “Developer Mode” (which I’m surprised that I haven’t had to turn on so far yet), I get further:

<whole bunch of output>
  = note: 'clang++' is not recognized as an internal or external command,
          operable program or batch file.

Here’s that “invokes the compiler to get the linker” thing I was referring to above.

Now… by default, the Rust support is for the GNU version of the Windows toolchain. I never use that. Upstream has said that they want everything to be supported, so that change will come at some point, maybe by the time you read this! But in the meantime, I could get my (pure Rust) projects building with two small patches:

diff --git a/prelude/toolchains/cxx.bzl b/prelude/toolchains/cxx.bzl
index c57b7b8..dc14ca7 100644
--- a/prelude/toolchains/cxx.bzl
+++ b/prelude/toolchains/cxx.bzl
@@ -39,7 +39,7 @@ def _system_cxx_toolchain_impl(ctx):
         CxxToolchainInfo(
             mk_comp_db = ctx.attrs.make_comp_db,
             linker_info = LinkerInfo(
-                linker = RunInfo(args = ["clang++"]),
+                linker = RunInfo(args = ["link"]),
                 linker_flags = ["-fuse-ld=lld"] + ctx.attrs.link_flags,
                 archiver = RunInfo(args = ["ar", "rcs"]),
                 archiver_type = archiver_type,
diff --git a/prelude/toolchains/rust.bzl b/prelude/toolchains/rust.bzl
index 8172090..4545d55 100644
--- a/prelude/toolchains/rust.bzl
+++ b/prelude/toolchains/rust.bzl
@@ -23,7 +23,7 @@ _DEFAULT_TRIPLE = select({
         # default when we're able; but for now buck2 doesn't work with the MSVC
         # toolchain yet.
         "config//cpu:arm64": "aarch64-pc-windows-gnu",
-        "config//cpu:x86_64": "x86_64-pc-windows-gnu",
+        "config//cpu:x86_64": "x86_64-pc-windows-msvc",
     }),
 })

Now a build works!

〉buck2 build //:hello_world
File changed: root//BUCK
File changed: root//.git/index.lock
File changed: root//.git
6 additional file change events
Build ID: 65fc80aa-7bfa-433a-bfa7-57919147b550
Jobs completed: 65. Time elapsed: 1.0s. Cache hits: 0%. Commands: 1 (cached: 0, remote: 0, local: 1)
BUILD SUCCEEDED

We can run it to see the output:

〉buck2 run //:hello_world
Build ID: 78b0ca23-2c7c-4c02-a161-bba15e3b38bd
Jobs completed: 3. Time elapsed: 0.0s.
hello world

Same idea as cargo run.

Speaking of cargo run, what might this look like with Cargo? Well, we can create a Cargo.toml:

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

[[bin]]
name = "hello_world"
path = "hello.rs"

and try it out. Oh, and you’ll probably want to put target into your .gitignore.

Let’s build. The “benchmark” command in nushell is sort of like time on a UNIX system:

〉benchmark { cargo build }
   Compiling hello_world v0.1.0 (C:\Users\steve\Documents\GitHub\buck-rust-hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.34s
416ms 490us 100ns
〉benchmark { cargo build }
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
77ms 317us 200ns

Not too bad, a bit under half a second for the initial build, and near immediate on a subsequent build. What about buck?

〉benchmark { buck2 build //:hello_world -v 3 }
Running action: <snip>
Build ID: 47ebd9f1-3394-4f72-a0fb-02c936035d2b
Jobs completed: 58. Time elapsed: 0.8s. Cache hits: 0%. Commands: 1 (cached: 0, remote: 0, local: 1)
BUILD SUCCEEDED
945ms 557us
〉benchmark { buck2 build //:hello_world -v 3 }
Build ID: 5eed24e8-a540-454f-861a-855464aba3c9
Jobs completed: 3. Time elapsed: 0.0s.
BUILD SUCCEEDED
78ms 274us 100ns

Not too shabby; Buck is a teeny bit slower on the initial build, but when stuff is cached, both are the same speed. All of this is fast enough to qualify as “basically instant.”

Adding a library

Let’s up the complexity a bit, by adding a library that we want to depend on. Here’s a lib.rs:

pub fn print_hello() {
    println!("Hello, world!");
}

We want to change our top-level BUCK to add this:

+rust_library(
+    name = "print_hello",
+    srcs = ["lib.rs"],
+    edition = "2021",
+    visibility = ["PUBLIC"],
+)
+
 rust_binary(
     name = "hello_world",
     srcs = ["hello.rs"],
     crate_root = "hello.rs",
+    deps = [
+        ":print_hello",
+    ],
 )

Here, we make a new library, print_hello, and then make our binary depend on it.

Let’s change the code in main.rs to use the library:

fn main() {
    println!("hello world");
    print_hello::print_hello();
}

And that’s it! Let’s examine our targets:

〉buck2 targets //...
Build ID: 4646f2e7-0ea3-4d59-8590-3da0708ce96e
Jobs completed: 4. Time elapsed: 0.0s.
root//:hello_world
root//:print_hello

They’re both there! We can now build one, the other, or everything:

# build everything
〉buck2 build //...
# build just the library
〉buck2 build //:print_hello
# build 'just' the binary, this will of course end up building the library too
〉buck2 build //:print_hello

Let’s make sure it still prints our output:

〉buck2 run //:hello_world
Build ID: d76c80fb-dd77-463a-86a1-b6443cea10f6
Jobs completed: 3. Time elapsed: 0.0s.
Hello, world!

Nice.

Let’s compare that to Cargo. Modify Cargo.toml:

[lib]
name = "print_hello"
path = "lib.rs"

and build:

〉cargo run
   Compiling hello_world v0.1.0 (C:\Users\steve\Documents\GitHub\buck-rust-hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target\debug\hello_world.exe`
Hello, world!

Nice.

more cargo-ish

Before we move forward, this isn’t exactly an apples to apples comparison; we’ve been doing a lot of configuration for Cargo that we normally wouldn’t have to do, and also, what if you already have a Cargo project, but you want to try out Buck with it?

Do this:

> mkdir src
> mv hello.rs src/main.rs
> mv lib.rs src/main.rs

And delete the configuration from Cargo.toml, leaving just the package table. Finally, we need to change src/main.rs, given that we’re using the default crate name for the library crate, which is hello_world and not print_hello:

fn main() {
    hello_world::print_hello();
}

After this, cargo build works just fine. But what about Buck?

So. We have a bit of weirdness here, and I’m not sure if it’s actually work-around-able in Buck or not, since I’m still learning this myself. But if we do the basic translation, we’ll get an error. Let’s try it. This is how you modify the BUCK file:

rust_library(
    name = "hello_world",
    srcs = glob(["src/**/*.rs"]),
    edition = "2021",
    visibility = ["PUBLIC"],
)

rust_binary(
    name = "hello_world",
    srcs = ["src/main.rs"],
    crate_root = "src/main.rs",
    deps = [
        ":print_hello",
    ],
)

Cargo produces a binary and a library, both called hello_world, but buck doesn’t like that:

〉buck2 run //:hello_world
Error evaluating build file: `root//:BUCK`

Caused by:
    Traceback (most recent call last):
      * BUCK:8, in <module>
          rust_binary(
    error: Attempted to register target root//:hello_world twice, re-run the command with `--target-call-stacks` to obtain a call stack of the first registration
      --> BUCK:8:1
       |
     8 | / rust_binary(
     9 | |     name = "hello_world",
    10 | |     srcs = ["src/main.rs"],
    11 | |     crate_root = "src/main.rs",
    12 | |     deps = [
    13 | |         ":print_hello",
    14 | |     ],
    15 | | )
       | |_^
       |

Build ID: d6a8925d-0180-4308-bcb9-fbc888e7eca1
Jobs completed: 4. Time elapsed: 0.0s.
BUILD FAILED

It’s like hey! You have two targets named hello_world! That’s confusing! It also reveals a difference between Buck and Cargo. With Cargo, if you remember our configuration, we had to point it to the crate root. From there, Cargo just leans on rustc to load up all of the other files that may be required if you have a bunch of modules. But with Buck, we need to tell it up front which files we use. So as you can see above:

    srcs = glob(["src/**/*.rs"]),

We can use the glob command to glob up all of our files, which is nice, but it’s… it’s actually wrong. We want to glob everything except main.rs. If main.rs were to change, this would try and re-build both the binary and the library, in my understanding. So that’s annoying.

It’s not just annoying for Buck, though. Having both a src/main.rs and a src/lib.rs has led to so much confusion from beginners over the years. At some point, someone puts mod lib; into src/main.rs and everything goes to hell. The original intention of this layout, to make simple things simple, is a good idea, but I think that sadly, we missed the mark here. Luckily, Cargo also supports a bin directory:

> mkdir src/bin
> mv src/main.rs src/bin

We can still cargo run and cargo build and this all works just fine. This doesn’t fix our glob issue, though, because src/bin is still inside of src.

I think, in the abstract, I’d prefer a layout like src/{lib,bin}. You want things to not really be nested. So let’s do that. Both Cargo and Buck can handle it! It’s just not as nice as being purely default in Cargo, since that convention is so strong.

# if you didn't do this above
> mkdir src/bin
> mv src/main.rs src/bin

> mkdir src/lib
> mv src/lib.rs src/lib/print_hello.rs

We have to change src/bin/main.rs to use hello_world again

fn main() {
    hello_world::print_hello();
}

And we have to re-add some configuration into Cargo.toml:

[lib]
path = "src/lib/print_hello.rs"

Everything should build just fine. But what about Buck?

So, once you start getting into subdirectories, you can also start using multiple BUCK files. So we can empty out our root BUCK file (I’m leaving it existing but empty, if you want to delete it you can but you’ll recreate it in the next part anyway), and create two new ones. Here’s src/lib/BUCK:

rust_library(
    name = "hello_world",
    srcs = glob(["**/*.rs"]),
    crate_root = "print_hello.rs",
    edition = "2021",
    visibility = ["PUBLIC"],
)

and src/bin/BUCK:

rust_binary(
    name = "hello_world",
    srcs = ["main.rs"],
    crate_root = "main.rs",
    deps = [
        ":hello_world",
    ],
)

We added in a crate_root to the library as well. Okay, let’s try this:

〉buck2 run //:hello_world
File changed: root//src/lib/BUCK
Unknown target `hello_world` from package `root//`.
Did you mean one of the 0 targets in root//:BUCK?
Build ID: d5059fc9-8001-47c4-ba5a-6ba605a4182c
Jobs completed: 2. Time elapsed: 0.0s.
BUILD FAILED

Oops! Since we moved files around, the names of our targets have changed. Let’s examine them:

〉buck2 targets //...
Build ID: c4165964-cb87-49b4-8afe-4a3fc2c526bc
Jobs completed: 4. Time elapsed: 0.0s.
root//src/bin:hello_world
root//src/lib:hello_world

We had only seen very basic target patterns, but this is enough to show off:

root/src/bin:hello_world

can be read as

The “hello_world” target defined in /src/bin/BUCK.

Our target names changing also means we made one mistake in our new BUCK files. Let’s give it a try:

〉buck2 run //src/bin:hello_world
Error running analysis for `root//src/bin:hello_world (prelude//platforms:default#fb50fd37ce946800)`

Caused by:
    0: Error looking up configured node root//src/bin:hello_world (prelude//platforms:default#fb50fd37ce946800)
    1: Cyclic computation detected when computing key `root//src/bin:hello_world (prelude//platforms:default#fb50fd37ce946800)`, which forms a cycle in computation chain: `root//src/bin:hello_world (prelude//platforms:default#fb50fd37ce946800),root//src/bin:hello_world (prelude//platforms:default#fb50fd37ce946800)`
Build ID: 930ab541-c2dd-44f5-aef1-f6658a2b7c53
Jobs completed: 2. Time elapsed: 0.0s.
BUILD FAILED

Right. Our binary depends on :hello_world, which it is itself named hello_world, so there’s a problem. But that’s just it, we don’t want to depend on any old hello_world, we want to depend on our libary. Can you write out the target pattern that should go in src/bin/BUCK?

It looks like this:

    deps = [
        "//src/lib:hello_world",
    ],

“The hello_world target in /src/lib/BUCK. Perfect. And now it works!

〉buck2 run //src/bin:hello_world
File changed: root//src/bin/BUCK
Build ID: c6d2fdaa-298a-425a-9091-d3f6b38c4336
Jobs completed: 12. Time elapsed: 0.5s. Cache hits: 0%. Commands: 1 (cached: 0, remote: 0, local: 1)
Hello, world!

It kinda stinks to have to type all of that out. Luckily, Buck supports aliases for target patterns. Take our top-level BUCK file, and add this:

alias(
    name = "build",
    actual = "//src/bin:hello_world",
    visibility = ["PUBLIC"],
)

And now we can use it:

〉buck2 build
Build ID: a87ed1e2-cfab-47b0-830e-407217997fd7
Jobs completed: 2. Time elapsed: 0.0s.
BUILD SUCCEEDED

Fun.

Okay! This is getting a bit long, so let’s end there. We have more to learn before buck can actually replace Cargo in our normal development workflow, but I hope this helped you see how you could get started with Buck if you wanted to.

If you want to check out this on your own, I’ve published this on GitHub: https://github.com/steveklabnik/buck-rust-hello/tree/024ef54ba45627e87a65aaf2f69c6661198c336c

Next up, we’ll be tackling other features, like “using crates from crates.io.” No promises on when that’ll get published though!