Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ FROM rust:1-trixie AS builder

WORKDIR /app

# Build-time git metadata for the footer. The build context excludes
# `.git` (see .dockerignore), so `build.rs` can't shell out to git here.
# The deploy passes these as build args instead; Coolify auto-injects
# `SOURCE_COMMIT`. Set GIT_BRANCH as a build arg in Coolify (e.g. `main`).
ARG GIT_BRANCH=unknown
ARG GIT_HASH=
ARG SOURCE_COMMIT=
ENV GIT_BRANCH=${GIT_BRANCH} \
GIT_HASH=${GIT_HASH} \
SOURCE_COMMIT=${SOURCE_COMMIT}

# Cache dependencies separately from source. Copy just the manifests
# first, build a dummy main so cargo downloads + compiles deps, then
# overwrite with the real source.
Expand Down
31 changes: 26 additions & 5 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,15 @@ fn main() {

/// Emit `GIT_BRANCH` / `GIT_HASH` as compile-time env vars for the server footer.
///
/// Best-effort: if `git` isn't on PATH or `.git` isn't present (e.g. a Docker
/// build from a tarball), we emit `unknown` and move on. `cargo:rerun-if-changed`
/// on `.git/HEAD` refreshes the values when the checked-out commit/branch changes.
/// Best-effort: prefer build-time overrides from the environment, then fall back
/// to asking `git`. Docker builds exclude `.git` (see `.dockerignore`), so the
/// deploy passes `GIT_BRANCH` / `GIT_HASH` as build args instead; Coolify also
/// injects `SOURCE_COMMIT`. If nothing is available we emit `unknown`.
fn emit_git_build_info() {
println!("cargo:rerun-if-changed=.git/HEAD");
println!("cargo:rerun-if-env-changed=GIT_BRANCH");
println!("cargo:rerun-if-env-changed=GIT_HASH");
println!("cargo:rerun-if-env-changed=SOURCE_COMMIT");

let git = |args: &[&str]| -> Option<String> {
let out = std::process::Command::new("git").args(args).output().ok()?;
Expand All @@ -76,8 +80,25 @@ fn emit_git_build_info() {
if s.is_empty() { None } else { Some(s) }
};

let branch = git(&["rev-parse", "--abbrev-ref", "HEAD"]).unwrap_or_else(|| "unknown".into());
let hash = git(&["rev-parse", "--short", "HEAD"]).unwrap_or_else(|| "unknown".into());
// A non-empty environment variable, trimmed. Used for build-arg overrides.
let env_var = |key: &str| -> Option<String> {
std::env::var(key)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
};

let branch = env_var("GIT_BRANCH")
.or_else(|| git(&["rev-parse", "--abbrev-ref", "HEAD"]))
.unwrap_or_else(|| "unknown".into());

// Hash precedence: explicit GIT_HASH, then Coolify's SOURCE_COMMIT
// (shortened to 7 chars), then local git, then unknown.
let hash = env_var("GIT_HASH")
.or_else(|| env_var("SOURCE_COMMIT").map(|s| s.chars().take(7).collect()))
.or_else(|| git(&["rev-parse", "--short", "HEAD"]))
.unwrap_or_else(|| "unknown".into());

println!("cargo:rustc-env=GIT_BRANCH={branch}");
println!("cargo:rustc-env=GIT_HASH={hash}");
}
Expand Down
5 changes: 4 additions & 1 deletion examples/01_strings_and_chars/1_intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ Before we go further, two words that show up everywhere in Rust:
Borrows are written with an `&` (or `&mut` if you also want to mutate).
The borrow has to end before the owner is dropped, and the compiler enforces that for you, ruling out use-after-free and dangling pointers.

The ownership chapter covers this in depth; for now just keep the mental picture of "one owner, many short-lived borrows."
The ownership model is why Rust has two string types in the first place: it tracks who owns each piece of data.
The 30-second version: every value has one owner, the value is dropped when that owner goes out of scope, and you can borrow a value without taking it.
The next chapter (moves and `Copy`) and the borrowing chapter make this hands-on, and a later memory chapter ties it together.
For now just keep the mental picture of "one owner, many short-lived borrows."

The split between `&str` and `String` is what makes Rust strings both fast and safe.
A function that just *reads* text takes `&str`; a function that *produces* new text returns `String`.
Expand Down
38 changes: 38 additions & 0 deletions examples/02_moves_and_copy/1_intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Moves and Copy

Most languages let you keep using a variable after you've assigned it somewhere else.
Rust usually doesn't, and it stops you at compile time.

```rust
let s = String::from("hello");
let t = s; // ownership moves from s to t
println!("{s}"); // ERROR: borrow of moved value: `s`
```

Assigning `s` to `t` *moves* the string.
There's now one owner, `t`, and `s` is dead.
Reach for `s` again and the compiler refuses, naming the exact line.
In a language with shared mutable pointers this would be a silent bug (two variables aliasing one buffer, one of them freeing it first), and Rust turns it into a compile error.

Why move instead of copy?
A `String` owns a buffer on the heap.
Copying it on every assignment would mean duplicating that buffer over and over, silently.
Rust makes the cheap thing (a move: hand over the pointer) the default and the expensive thing (a deep copy with `.clone()`) something you ask for out loud.

## Copy types

Small values that live entirely on the stack don't have this problem.
Integers, `bool`, `char`, and fixed-size arrays of them implement the `Copy` trait, so assigning one duplicates the bits instead of moving:

```rust
let a = 5;
let b = a; // a is copied, not moved
println!("{a} {b}"); // both fine
```

The rule of thumb: a type that owns nothing on the heap and is cheap to duplicate is `Copy`, and the move rules never bite it.
Everything else moves.

This is the first chapter where moves matter, because `String` is the first heap-owning type you've met.
Borrowing, using a value without taking it, is the next chapter.
Here we just get comfortable with ownership changing hands.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The value lives at the callee now, and will be dropped when the callee finishes

Note the signature: `String` in, `String` out.
Implement the body by mutating the parameter (`s.push_str(...)`) and then returning `s`.
Because you own `s`, you're free to mutate it without any `&mut` dance: ownership implies the right to modify.
Because you own `s`, you're free to mutate it directly: ownership implies the right to modify.

## Useful from the standard library

Expand Down
15 changes: 15 additions & 0 deletions examples/02_moves_and_copy/3_copy_types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copy types don't move

`take_ownership` moved a `String`.
This step shows the other half of the rule: a `Copy` type is duplicated instead, so the caller keeps its value.

`double` takes an `i32` by value.
Because `i32` is `Copy`, the caller's variable is still alive after the call.
The body is one expression; the lesson is in the test, where `x` is read again after being passed in.

## Useful from the standard library

- `i32`, the other integer types, `bool`, `char`, and fixed-size arrays of `Copy` values all implement [`Copy`](https://doc.rust-lang.org/std/marker/trait.Copy.html).
Assigning or passing one duplicates its bits instead of moving it.
- Heap-owning types like `String` and `Vec<T>` are deliberately not `Copy`.
When you need a second owner of one of those, ask for it with [`Clone::clone`](https://doc.rust-lang.org/std/clone/trait.Clone.html#tymethod.clone).
18 changes: 18 additions & 0 deletions examples/02_moves_and_copy/3_copy_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// Doubles a number.
///
/// `i32` is a `Copy` type, so the caller's value is duplicated bit-for-bit
/// when it's passed in. The original stays usable after the call (see the
/// test), which is exactly what does *not* happen with a `String`.
fn double(n: i32) -> i32 {
todo!()
}

#[test]
fn test_double() {
let x = 21;
let y = double(x);
assert_eq!(y, 42);
// x is still usable here: i32 is Copy, so `double(x)` copied it
// instead of moving it.
assert_eq!(x, 21);
}
13 changes: 13 additions & 0 deletions examples/02_moves_and_copy/4_what_we_learned.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Wrapping up moves and Copy

You moved a `String` into a function and back out, and saw that an `i32` copies instead of moving.

## What we learned

- Assigning or passing a non-`Copy` value moves it.
The old binding is dead, and using it again is a compile error, not a runtime surprise.
- A move hands over ownership cheaply (just the pointer).
Rust makes deep copies explicit through `.clone()`.
- `Copy` types (integers, `bool`, `char`, fixed-size arrays of those) duplicate bit-for-bit instead of moving, so the original stays usable.
- The owner is responsible for the value: when it goes out of scope, the value is dropped, with no garbage collector involved.
- Borrowing, coming up next, lets you hand a value to a function without giving up ownership at all.
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@

#[path = "2_take_ownership.rs"]
mod _2_take_ownership;
#[path = "3_borrow_string.rs"]
mod _3_borrow_string;
#[path = "4_mutate_string.rs"]
mod _4_mutate_string;
#[path = "5_experiments.rs"]
mod _5_experiments;
#[path = "3_copy_types.rs"]
mod _3_copy_types;

fn main() {}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Implement `ferris_mood(hunger, naps)` returning a `&'static str`, following thes
String literals like `"Hangry"` are baked into your compiled binary, so the text is around for as long as the program is running.
The `'static` *lifetime* is just the compiler's way of saying "this reference will never dangle."
If you've written C, it's the same intuition as a `const char *` pointing at a string literal.
Lifetimes get a proper introduction in the ownership and borrowing chapter; for now the only thing to take away is *"string literals are always safe to return as `&'static str`."*
Lifetimes get a proper introduction in the memory and ownership chapter; for now the only thing to take away is *"string literals are always safe to return as `&'static str`."*

## Two things to watch

Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ sum_to(0) = 0 // base case
sum_to(n) = n + sum_to(n - 1) // for n > 0
```

This is the lightbulb moment for recursion: a function's answer can be defined in terms of *its own answer to a smaller version of the same problem*.
That's the whole idea of recursion: a function's answer is defined in terms of its own answer to a smaller version of the same problem.
Once the base case is reached, every pending call finishes its addition and the final total bubbles back up.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ Read the error, it points right at it.
Once it compiles, look at the second test.
The caller's variable is untouched even though the function reassigned its parameter.
That's because `i32` is `Copy`, so the function received its own copy to mutate.
You'll see what changes for non-`Copy` types like `String` when ownership and borrowing come up later.
The moves chapter showed the other half of this: a non-`Copy` type like `String` gets moved in instead of copied.
The next chapter, borrowing, shows how to lend a value to a function without giving it up at all.
File renamed without changes.
File renamed without changes.
35 changes: 35 additions & 0 deletions examples/05_borrowing_and_references/1_intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Borrowing and references

*Why are Rust developers so frugal? They prefer to borrow.*

The functions chapter had a nuisance baked in.
Pass a `String` to a function and the move rules say you've handed it over.
Want to use it afterwards? Too bad, it's gone.
Returning it back out works but gets tedious fast, especially when the function only needed to *read* the value.

Borrowing is the fix.
A borrow lets a function use a value without taking ownership of it: `&value` for a shared, read-only borrow, and `&mut value` for an exclusive, writable one.

```rust
fn length(s: &String) -> usize { s.len() } // borrows, doesn't take

let s = String::from("rust");
let n = length(&s); // lend s out for the call
println!("{s}"); // s still owns the data
```

The caller keeps ownership the whole time.
The function gets temporary access and gives it back when it returns.

## The one rule

Borrows come with a single rule, and the borrow checker enforces it everywhere:

> At any moment you can have either any number of shared `&` references, or exactly one `&mut` reference. Never both at once.

A shared reference promises the data won't change while you're looking at it.
A mutable reference promises nobody else is looking while you write to it.
Allow both at once and you'd have someone reading a value halfway through someone else's change, which is the classic data race.
Rust rules that out at compile time instead of trusting you to get the locking right.

When the compiler rejects your code with a borrowing error, this rule is the first thing to check.
15 changes: 15 additions & 0 deletions examples/05_borrowing_and_references/5_what_we_learned.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Wrapping up borrowing

You borrowed a `String` read-only as `&str`, mutated one through `&mut String`, and triggered the borrow checker's three canonical errors on purpose.

## What we learned

- A borrow lets you read or modify a value without taking ownership.
`&T` is a shared, read-only borrow; `&mut T` is an exclusive, writable one.
- The borrow-checker rule: at any moment, either any number of `&T` borrows or exactly one `&mut T`, never both.
That's what rules out data races at compile time.
- Mutability is opt-in at every layer: the binding (`let mut x`), the parameter (`&mut T`), and the call site (`&mut x`).
- Default to `&str` over `&String` (and `&[T]` over `&Vec<T>`) for read-only parameters.
Slice types accept more callers thanks to deref coercion.
- The compiler errors are the lesson.
Once you can say in one sentence why the compiler is complaining, you've built the muscle this chapter is for.
22 changes: 22 additions & 0 deletions examples/05_borrowing_and_references/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// @generated by build.rs - do not edit by hand.
// This file aggregates the chapter's step files (`<n>_<slug>.rs`).
// To change it, edit (or add/remove) those step files and re-run `cargo build`.
#![allow(
dead_code,
unused_imports,
unused_variables,
clippy::todo,
clippy::needless_pass_by_value,
clippy::needless_pass_by_ref_mut,
clippy::ptr_arg,
clippy::boxed_local
)]

#[path = "2_borrow_string.rs"]
mod _2_borrow_string;
#[path = "3_mutate_string.rs"]
mod _3_mutate_string;
#[path = "4_experiments.rs"]
mod _4_experiments;

fn main() {}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ let r: &mut i32 = &mut n;
```

Without the `*`, you'd be trying to add `1` to a reference, which the compiler won't let you do.
References show up properly in the borrowing chapter; for now it's enough to know that when a function returns `&mut T`, you reach the `T` through `*`.
References were introduced back in the borrowing chapter; for now it's enough to know that when a function returns `&mut T`, you reach the `T` through `*`.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::collections::HashMap;
///
/// Looking up a key returns an `Option<&V>`, because the key might not
/// be there. Collapse it into a concrete value with a fallback for
/// the missing case (`Option` is covered properly in chapter 9).
/// the missing case (`Option` is covered properly in the `Option` chapter).
fn get_config_value(config: &HashMap<String, String>, key: &str) -> String {
todo!()
}
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Watch out for ownership: a tuple of `String`s is *moved* into the function, whil
"Moved" means the caller's binding is no longer usable afterwards, because the value's single owner is now the function parameter rather than the caller.
"Copied" means the value is duplicated bit-for-bit, so the caller keeps theirs and the function gets its own.
The split is decided by a trait called `Copy`: types that are tiny and have no heap data (integers, bools, `char`, fixed-size arrays of those, and tuples made entirely of `Copy` types) implement it; types that own heap data (like `String` or `Vec`) deliberately don't.
The doc-comment below has more on this, and the ownership chapter covers move semantics in depth.
The doc-comment below has more on this, and the moves chapter covered move semantics in depth.

## Useful from the standard library

Expand All @@ -17,4 +17,4 @@ The doc-comment below has more on this, and the ownership chapter covers move se
- Anything that isn't `Copy` (like `String`) is *moved* when bound by destructuring, so the caller's binding becomes unusable: ownership of the underlying heap buffer transferred into the function.
`Copy` types (integers, bools, `char`, and tuples of those) are duplicated instead, so the caller keeps their copy.
After this function returns, the caller's `(String, String)` tuple is gone.
The ownership chapter covers move semantics in depth.
The moves chapter covered this in depth.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/// aren't `Copy`). After the call, the original `full_name` is no longer
/// valid; try using it after calling this function and read the error.
/// `swap_values` below takes `(i32, i32)`, which is `Copy`, so the
/// original is still usable. Chapter 12 covers this properly.
/// original is still usable. The moves chapter covers this properly.
///
/// Hint: Use [tuple destructuring](https://doc.rust-lang.org/rust-by-example/flow_control/match/destructuring/destructure_tuple.html)
fn get_first_name(full_name: (String, String)) -> String {
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
56 changes: 0 additions & 56 deletions examples/11_ownership_and_borrowing/1_intro.md

This file was deleted.

Loading
Loading