Skip to content
academia.sh

Lesson 02 / 12

Setup and the Package Manager

There are three parts a source is required to write for it to turn into a runnable binary — the entry point, the crate root, the manifest; all three are items, none of them produces a value on its own, but the three together produce a single output, and a change in the manifest can change that output's name without ever touching the source.

Published Updated

In this lesson

  • Lists the three parts required for a source to run (entry point, crate root, manifest)
  • Shows that these three parts fall into the item class, and that their use, not the parts themselves, produces a value
  • Proves that the manifest stands outside the source by changing the output's name without changing the source
Contents

The previous lesson defined the three classes and split eighteen writings among them; none of them alone was sufficient for a program to run, because the question asked was already a different one. This lesson narrows the question: what, at minimum, must a source text write, with nothing missing, for it to turn into a running binary? The answer is three parts, and all three fall into a class familiar from the previous lesson — item. None of them produces a value on its own, but the three together produce a single output: an executable program.

This question looks smaller than the previous lesson’s question, but it is not independent of it. The previous lesson asked what something written in the source produces when evaluated; this lesson asks what frame must stand around the source for evaluation to even begin. The expression, statement, and item distinction is a classification valid only inside Rust source; this lesson shows where that source begins and its relationship to the data surrounding it.

Three Mandatory Items

The three parts required for a source to turn into a runnable binary are: the entry point (the fn main marking where the program will start), the crate root (the file itself where the compiler starts reading), and the manifest (the data file holding the package’s name and which language edition it compiles with). A standard project layout separates these as follows:

project/
├── Cargo.toml
└── src/
    └── main.rs

# Cargo.toml contents
[package]
name = "calculator"
version = "0.1.0"
edition = "2021"

These three fall into the item class established in the previous lesson: none of them is evaluated on its own, each only declares something. fn main itself is a function declaration — just like double in the previous lesson. The crate root is a file; the file itself is not “run,” only the declarations inside it are introduced to the compiler. The manifest, in turn, is not even Rust source; it is data in TOML format, and it is read by the package manager.

The separation in the directory tree is not arbitrary: only source files sit under src/, while Cargo.toml sits at the project’s root, outside src/. This layout keeps “source” and “data about the source” out of the same folder — a tool scanning all source files only needs src/, and reads the manifest with separate logic. Here we use a single-file source and hand it directly to the compiler, because the package manager is never run in this course. In a real project, the crate root is src/main.rs, and the compiler starts there and reaches other files through mod declarations — that expansion is the subject of the Modules and Crates lesson; here it is enough to know the root is a single file and compilation starts from there.

LB4. fn main being “used” differs from a function being used in the previous lesson: double was called from within the program itself, while main is called by no line at all — what calls it is the runtime environment that takes over when the operating system runs the compiled binary at the start of a process. The declaration (item) is the same declaration, but the calling side is outside the source.

This does not mean the source is incomplete on its own; the record is complete at the crate root, only the source cannot decide at what moment that record turns into a value. A source can also contain other items, such as double or observe — all of them stand as declarations, none runs on its own. Only one carries the name main and is recognized by the operating system as the entry point; the others produce a value only if reached from main‘s body or from another item main calls. This is the project-level counterpart of the previous lesson’s finding that items are valueless in measurement: a source tree can declare as many items as it likes, but an item unreachable from the entry point never runs.

LB5. The manifest is not Rust source at all, so the expression/statement/item classification does not truly apply to it — that classification is defined only for Rust source. But it lands functionally in the same place: the manifest itself is not “run” either, it only supplies data the package manager reads, and it contributes its name to the final output. The manifest’s own format makes this difference visible too: name = "calculator" is a key-value pairing, not a call or a branch — there is no code path to evaluate in TOML, only data to read.

The whole these three parts form together is called a package: the source tree the manifest points to, together with the manifest itself, forms a single unit of distribution and compilation. The package manager’s name comes from here too — what it manages is not a single source file, but the whole that source and identity form together. A package’s boundary is the directory in the file system where the manifest is found; every source file under that directory counts as belonging to the same package.

TOML itself is a data language designed to support this distinction: it contains only key = value lines and section headers opened with square brackets (such as [package]). There is no branching, loop, or function call — a manifest is not “run,” only read straight through as a plain set of records. This stands in direct contrast to Rust source: the source has expressions and statements to evaluate, the manifest has only data.

The difference between the crate root and the entry point is a similar distinction and should not be confused. The crate root is a file (where the compiler starts reading); the entry point is a specific item inside that file (fn main, the function where execution will start). In a small program the two can sit in the same file, but conceptually they are separate: a crate root can carry helper declarations linked to other files through mod, with no fn main in it at all — we saw this in the previous lesson with mod measure. What a package needs to be runnable is not the crate root itself, but fn main being found somewhere in the source tree.

The directory tree and the manifest text block shown throughout this lesson are not run and carry no numeric claim; they only document what a real project looks like. Every example that runs for the rest of the course will be set up to work directly with the compiler, independent of the package manager — this is a consequence of setting up a measurement environment that requires no network access, not a limitation of the language itself.

To see what the entry point does on its own, the source below runs it in its smallest form.

// our_program.rs — the least a source must write for it to be able to run
fn main() {
    println!("verification-key={}", 7 * 6);
}
verification-key=42

There is a single item in this source: fn main. The source itself produces no value — it tells the compiler “the program’s start is here.” The value arises when the program is run, in a way similar to how the previous lesson’s double(3) call’s value arose when the function was called. The difference is that the calling side is outside the source.

The print call inside the body itself falls into a class familiar from the previous lesson: a statement. The previous lesson’s table showed exactly this in its “the value of printing” row — printing returns (), and produces no value itself. The only thing that changes here is that this statement no longer stands alone, but inside the body of an item named fn main: the item itself produces no value, but when the statement in its body runs, it produces a side effect (a line printed to the screen). The expression, statement, and item classes can nest this way — an item’s body can be made of statements and expressions, but the item itself still remains an item.

The crate root being the single file the compiler starts reading is a requirement: a file without fn main can be compiled and written to disk (the measure function inside mod in the previous lesson did exactly this), but an executable the operating system can run directly needs an fn main somewhere. This is a single fact that will not appear again for the rest of the course: a source collection with no entry point can be compiled, but it is not runnable.

The separation of these three mandatory parts from one another is not a coincidence; it is itself a design decision — it stands on the same axis as the course’s title. Separating the source from the source’s identity (name, version, language edition) lets the two change independently: a project’s name can change without its logic ever changing, or its logic can change without its name ever changing. A design that kept both pieces of information in the same file could not provide this — every tool reading the source would first have to know where in the source to look for identity information.

Limiting Measurement: The Manifest Is Not Inside the Source

The manifest standing separate from the source has a concrete consequence: you can change the output’s name without ever changing the source, only by changing a field in the manifest. In a normal project, the package manager derives the binary’s name from the manifest’s name field. The measurement below shows the same principle, without running the package manager, directly with the compiler’s output-naming option — the source (our_program.rs) never changes, only the target name changes.

name_a="calculator"
name_b="calc-tool"
rustc --edition 2021 -o "$name_a" our_program.rs
rustc --edition 2021 -o "$name_b" our_program.rs
echo "output_a: $(./$name_a)"
echo "output_b: $(./$name_b)"
if [ "$name_a" != "$name_b" ]; then
  echo "binary_names_differ: yes"
else
  echo "binary_names_differ: no"
fi
output_a: verification-key=42
output_b: verification-key=42
binary_names_differ: yes

LB6. This measurement does not run the package manager — it is not run in any code block in this lesson either. What is shown is the principle of the package manager deriving the binary’s name from a data field: rustc‘s -o option stands in here for the manifest’s name field. The same source file was compiled twice, with two different target names, and both gave the exact same output; the only thing that changed was the binary’s name.

This result belongs to the same family as the previous lesson’s “class is read from the signature” finding: there, a method’s class was determined by looking at its signature, not its call site; here, a binary’s name is determined by looking at the manifest, not the source. In both cases the source itself does not carry a piece of information; that information sits somewhere outside the source, and the source runs unaware of it. The our_program.rs file does not know, in any of its lines, that it will turn into a binary named “calculator” or “calc-tool.”

The output_a and output_b lines in the measurement being exactly identical is itself an observation: when the same source is compiled twice with the same compiler options, it produces two separate binaries, but their behavior never diverges. This is the compilation-level counterpart of the previous lesson’s determinism finding — there it was shown that the same source produces the same eighteen lines on every run, here it is shown that the same source produces the same behavior on every compilation. The only thing that changes is a name left outside the compilation.

This independence is not one-directional. Once compilation is complete, the produced binary becomes independent of the source as well — even if the source file is deleted, a previously produced binary keeps working.

rustc --edition 2021 -o binary_example our_program.rs
mv our_program.rs our_program_backup.rs
echo "running: $(./binary_example)"
if [ -f our_program.rs ]; then
  echo "source_still_same_name: yes"
else
  echo "source_still_same_name: no"
fi
running: verification-key=42
source_still_same_name: no

LB7. The binary is the result of compilation; it is not the source itself, but the source’s evaluated state at a specific moment. Even if the source file is moved or deleted, the binary does not know it, because the link was established at compile time, and from that moment on the binary is a file on its own. This shows the ultimate purpose of the entry-point–crate-root–manifest trio: the three together produce a single, self-sufficient output independent of the source. This is the foundation of a situation commonly encountered in deployment practice: only the compiled binary is moved to a server, the source never goes there — because the binary can run on its own, this move carries no drawback.

This distinction has a concrete practical value: a source file carrying the same computation logic can be compiled under the name “calculator” as an internal tool in one context, and under a completely different name as part of a distributed package in another. Two separate identities are obtained without the source ever being copied or edited, only by changing the manifest around it — the measurement above shows exactly this: our_program.rs was compiled under two different names without ever being opened and edited even once.

This distinction has a practical consequence: renaming a project can be done without changing a single character in the source text. The crate root and the manifest carry two separate layers of visibility for the same project — one tells what runs, the other tells how the project is named and which language edition it compiles with. The two layers do not mix: you can read the source without reading the manifest, read the manifest without reading the source, and neither assumes anything about the other.

The same distinction also holds in reverse: the same crate root can be part of two separate projects with two separate manifests. What determines which project a source file belongs to is not the file’s content, but which manifest points to it. As seen in the our_program.rs example, this is a consequence of the source’s portability: the file’s content stays fixed while the manifest around it can change, and these two changes are independent of one another.

The other fields the manifest carries (the version and language-edition lines seen in the example above) follow the same principle: they sit in a code block and never mix into the source itself. In this lesson, the --edition 2021 option was given directly to the compiler because the package manager is never run; in a real project, the package manager reads this information from the manifest and passes it to the compiler itself. The source file carries none of this information — it only knows what it does, not what the manifest around it carries.

This lesson showed that the previous lesson’s “item” class is not limited to a single declaration line. A file itself (the crate root) and a project’s identity (the manifest) can be read by the same logic too: neither produces a value on its own, both only set up a frame, and the code running inside that frame produces an output. The next lesson descends into this frame, into the entry point’s body, and takes up that most of the first lines written there bind a value to a name.

Summary

  • Three parts are mandatory for a source to turn into a runnable binary: the entry point, the crate root, the manifest.
  • All three are items: none of them produces a value itself, and the three together produce a single output — a running program.
  • The side that “uses” the entry point is not the source itself, but the operating system’s moment of starting a process.
  • The manifest is not Rust source and stands outside the source; the output’s name can be changed by changing only the manifest, without ever changing the source.
  • The crate root and the manifest are two separate layers unaware of each other: one carries what runs, the other carries how the project is named.
  • Once produced, a compiled binary becomes independent of the source: even if the source file is deleted or moved, a previously produced binary keeps working.

Next Step

A body can now be written inside the entry point, but the first line of that body is usually to establish a name — to bind a value to a name. The next lesson asks which class this binding itself falls into, and shows by running it the difference between the same name being rebound three times and being bound once and changed twice — even though both reach the same result.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close