Small project, two files in two locations

I have a small project that I'd like to work on.
While I could write this project in a language that I'm already familiar with, such as Python, I'm thinking about using this as an opportunity to learn Rust.

I want this project to consist of a main() source file and a single library source file, and I have my heart set on having them in two unrelated directories.
As I start to read about Rust, I'm starting to worry that this may be a problem.

Most of the stuff I've read indicates that I'm supposed to learn Cargo at the same time as I start learning Rust. Since I'm not planning on packaging/distributing any Rust software any time soon, and I'm running a Linux version with a good package manager, I'm hoping to skip learning about cargo-as-package-manager for the time being, while learning cargo-as-build-system as necessary.

If I'm working on a beginner's project like this, with only two source files
in two very different locations, do I want to create one package, or two packages,
one crate, or two crates?
Can I just do something like "rustc <path to source1> <path to source2>"
and put off learning about cargo altogether?

The thing about that is, compiling software (especially native software) is complex and cargo makes building the code really easy: just run cargo build in the project dir somewhere.
Without it that process will turn into a long list of error-prone steps.

You can create a library project using cargo new --lib <LIBRARY_CRATE_NAME> and a binary project with cargo NEW <BINARY_CRATE_NAME>.

I'd suggest one project with a src/lib.rs for the library and a src/main.rs for the binary, really. But if you're really that set on having them separate, a project for each.

You'll be trading learning about cargo for learning about rustc. With rustc you'll need to specify when you want a library build, the edition, optimization levels, crate names, and the location of libraries you're linking in. It might not be so bad if you have zero non-std dependencies. [1] It will quickly get more complicated if you do have dependencies.

If you go with one project and Cargo, all you really need to know at first is

  • cargo new
  • cargo run
  • cargo run --release

And you'll pick up check, build, test, and so on as you learn more.

If you go with two, you'll need one extra step. Add the following to your binary's Cargo.toml:

# Under [dependencies]

your-library-name = { path = "path/to/your/library" }

You can run cargo build -v (for example) to see how it's invoking rustc. If you end up using rustc directly, you probably won't need all the same flags, but this may help you figure things out if you're not getting the results you expected.


  1. Or perhaps if your distro is supplying rustc and an adequate set of dependencies. ↩︎

2 Likes

Don't worry about "learning" cargo yet. Just do the steps in the book for creating a new project.

cargo new projectname

and

cargo r

to build and run it.

You will need to learn some rust, the concept of modules if you want to call into another files functions. The book chapter about Packages and crates is a lot to grok at first. Just understand enough to make a src/main.rs and src/lib.rs and then later learn more cargo stuff if you need to.
1st step is you must learn what a module is to use rust. Then take the next step of making your module into a lib.rs file.

One “hack” that can solve this problem for you, (but note that this approach is unconventional and learning the very basics about using cargo and crates is usually the recommended, more proper approach), is by not considering your library as another crate, but just as another module with an explicit #[path = …] annotaiton. E.e. something like this works:

foo/main.rs

#[path = "../bar/baz.rs"]
mod baz;

fn main() {
    baz::qux();
}

bar/baz.rs

pub fn qux() {
    println!("hello from baz::qux()");
}

compile with rustc main.rs, rustc will look up the other file fo rthe baz module itself.

Note that if you include the same module twice this way, it would be duplicated. You’ll still need to learn to learn to distinguish mod …; from use …;.

There’s a second extra step: for creating the library crate, one would typically use cargo new --lib instead of cargo new.

1 Like

Does rustc not have reasonable (i.e. for a tiny little, 1000 lines-of-code project that I won't be publishing) defaults for these things?
And the idea here is that I don't care about (or want) a library build, an "edition", or "crate names". Just a quick instant-gratification binary production process.

By the way, (assuming you have the latest version of Rust, 1.62, installed, which was published 3 days ago) with the newly stabilized cargo add command, setting up two crates with a dependency is even easier, you don’t even need to touch the Cargo.toml yourself :wink:

cargo new --lib bar # create library crate "bar"
cargo new foo # create binary crate "foo" (can also be done in a different location)
cd foo # enter "foo"s directory
cargo add --path ../bar # add dependency on "bar" to "foo" (you can also specify an absolute path)

# feel free to edit main.rs to *use* functions from "bar", e.g.:
echo 'fn main() { println!("{}", bar::add(42, 1)); }' > src/main.rs; cargo fmt

cargo run # compiles "foo" and its dependencies (i.e. "bar") and runs "foo"

The main thing here is that I'm trying to decide between a language I already know, and Rust (which I have no overwhelming motivation to learn, as would be the case if this were employment-related).

If I have to spend a number of hours learning "best practices" of Rust packaging before I can start seeing my code run (especially if it seems like those "best practices" mostly only matter for a large project that I'm publishing via github and hope to have outside contributors for), that'll dampen my enthusiasm.

And if "rearrange your code to fit the Rust Way Of Doing Things" (e.g. move your files around to the standard Rust directory structure) is the only way for this to be simple and quick-starting, my own stubbornness will present a problem.

And I'm running a linux distro with a good package manager that I quite like (and in my experience, it has a good track record of having packages for various language libraries), so learning another package manager is low on the priority list.

Now that you mention editions, that’s another good reason for using cargo. You won’t wanna be using Rust in “edition 2015” mode, which is the default when manually calling rustc filename, and if you use rustc manually, this can only be changed by passing an explicit argument like --edition 2021.

Creating a new crate with cargo new will appropriately select the most recent edition (at the time of creating the crate) for you and write it into your Cargo.toml file, so that cargo check/cargo build/cargo run do the right thing.

Cargo does. With rustc you sometimes have to be more explicit.

Well, you do though. For backwards compatibility reasons, the default edition is 2015, so if you don't specify an edition you'll be missing out on some features and writing unidiomatic (modern) Rust. This is like how gcc will default to [1] C90, a 30-some year old standard. [2] And you care about building libraries or not, too. Let's walk through an example to see what I mean.

mkdir library
cd library
echo -e 'pub fn hi() {\n    println!("Hello, world!");\n}' > lib.rs
rustc --edition=2021 lib.rs 

The invocation fails with:

error[E0601]: `main` function not found in crate `lib`
 --> lib.rs:1:1
  |
1 | / pub fn hi() {
2 | |     println!("Hello, world!");
3 | | }
  | |_^ consider adding a `main` function to `lib.rs`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0601`.

And you need to do:

rustc --edition=2021 lib.rs --crate-type=lib
ls
lib.rs  liblib.rlib

(It's liblib because you didn't give it a crate name.)

Continuing on...

mkdir ../binary && cd ../binary 
echo -e 'use library;\nfn main() {\n    library::hi();\n}' > main.rs 
rustc --edition=2021 main.rs 
error[E0432]: unresolved import `library`
 --> main.rs:1:5
  |
1 | use library;
  |     ^^^^^^^ no external crate `library`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0432`.

And so:

rustc --edition=2021 main.rs --extern library=../library/liblib.rlib 
./main 
Hello, world!

And now you have a binary. (It's not optimized.)


If I have to spend a number of hours learning "best practices" of Rust packaging before I can start seeing my code run [...] that'll dampen my enthusiasm.

As I think the above demonstrates, you'll have to learn something -- a few cargo commands, or the lower-level ways to use rustc to build and link everything instead. As you said yourself, Cargo is a build system, not just a package manager.

Here's the same thing as above with Cargo:

# One project
cargo new project && cd project 
echo -e 'pub fn hi() {\n    println!("Hello, world!");\n}' > src/lib.rs
echo -e 'use project;\nfn main() {\n    project::hi();\n}' > src/main.rs 
cargo run 
# Two projects
cargo new --lib library && cd library
echo -e 'pub fn hi() {\n    println!("Hello, world!");\n}' > src/lib.rs

cd .. && cargo new binary && cd binary
echo -e 'use library;\nfn main() {\n    library::hi();\n}' > src/main.rs 
cargo add --path ../library/
# Or if you don't have `cargo add` because it's too new:
# echo 'library = { path = "../library" }' >> Cargo.toml
cargo run

Whichever way you want to go [3], hopefully there's enough here to get you started.


  1. the GNU extension of ↩︎

  2. Maybe imagine if Python had to support all versions, so you got Python 1.0 unless you told it otherwise. ↩︎

  3. but I still recommend cargo ↩︎

4 Likes

In the Rust toolchain, the “reasonable defaults” are kept in cargo, not rustc.

If I have to spend a number of hours learning "best practices" of Rust packaging

The only “practice” you need to know is that it is standard practice for projects to use Cargo. Not using Cargo is generally for extremely specialized projects that need special compilation options, or big organizations that have their own build tools.

You don't need anything more than one foo/Cargo.toml and one foo/src/main.rs for a binary project. You don't need to learn anything about how to make a good publishable package to use Cargo to compile your project.

And, if you try to use any Rust libraries at all without using Cargo (or some tool that substitutes for it), you will have to learn a lot of things about rustc and do a lot of manual work.

And I'm running a linux distro with a good package manager that I quite like … learning another package manager is low on the priority list.

For your purposes, Cargo is primarily a build tool. It has a tiny sort-of package manager (cargo install) attached for installing simple dev tools, but you don't need to use that at all. It does download libraries, but they only get used by cargo build; they're cached, not installed.

2 Likes