---
title: Publishing
source: 'https://academia.sh/en/courses/rust-advanced/publishing'
course: 'The Rust Ecosystem and Advanced Topics'
language: en
updated: '2026-09-15T04:58:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows, by running a submission model, that a version string sits in the source but never passes any check against the body'
    - 'Measures where version compatibility stops at the signature level by compiling the same caller source against library versions whose signature stays the same and whose signature changes'
    - 'Explains, by comparing two versions that carry the same signature, that they can produce different behavior — that the promise stops at the signature, not at the behavior'
---

# Publishing

It is shown by running code that a version string sits in the source without ever being checked against the body, that version compatibility is modeled at the signature level, and that the same signature can produce different behavior — that the promise stops at the signature, not at the behavior.

The Documentation lesson showed that a passing documentation example does not mean the
explanation above it is correct: the documentation tool tests only the example, it never
reads the sentence above it at all. This final lesson carries the same distinction to two
other surfaces a package exposes to the outside — a version string and a signature. Both
look like a promise: the version string says "this package is in this state," the signature
says "this function takes this, returns that." Across the Rust Ecosystem and Advanced Topics
course, eleven lessons asked the same question in eleven different setups — where does the
compiler's guarantee stop here? — and the answer fell into one of four classes. This final
lesson applies those four classes to two promises and shows that the two do not fall into
the same class.

## The Meaning of the Version String

The guarantee core run at the course's opening set aside one of its twelve observations for
this: a version string sitting in the source corresponded to nothing. The run below extends
that observation with a package manifest and a function that submits it to a package
registry — the registry's name is never written anywhere, the submission is only modeled.

```rust
// package_manifest.rs — the version string sits in the source, the function that models the submission never looks at it
struct PackageManifest {
    name: &'static str,
    version: &'static str,
}

const VERSION_STRING: &str = "0.1.0";

fn double(x: i32) -> i32 {
    x * 2
}

// The function that models the submission: takes the manifest, produces a confirmation string.
// It does not check the version field's content in any way - it accepts any string.
fn model_submission(manifest: &PackageManifest) -> String {
    format!("the {} package is marked as {} without changing the body", manifest.name, manifest.version)
}

fn main() {
    let manifest = PackageManifest { name: "measurement_crate", version: VERSION_STRING };
    println!("{}", model_submission(&manifest));
    println!("double(21) = {}", double(21));

    let second_manifest = PackageManifest { name: "measurement_crate", version: "0.2.0" };
    println!("{}", model_submission(&second_manifest));
    println!("double(21) = {}", double(21));
}
```

```
the measurement_crate package is marked as 0.1.0 without changing the body
double(21) = 42
the measurement_crate package is marked as 0.2.0 without changing the body
double(21) = 42
```

**PL16.** Both manifests point to the same `double` function, both produce the same call,
and both return `42` — but the `version` field changed from `"0.1.0"` to `"0.2.0"`.
`model_submission`'s signature never looks at what string the `version` field carries at
all: it accepts every value of type `&'static str`. While the body stays unchanged in the
source down to a single character, the version field can be moved up a digit by hand, and
the compiler looks for no consistency at all between the two.

This is the exact repetition of the observation from the course's opening guarantee core:
a version string sits in the source and nobody compares it against the body. The class is
**nobody checks it** — neither the compiler, nor a tool, nor a running program tests whether
the version field stays consistent with the body. Submitting to a package registry carries
the same limit: the registry accepts a submitted version string, but it never checks that
the string correctly reflects the change in the body — that check is left entirely to the
writer's judgment.

## Version Compatibility and the Signature

**The distinguishing sentence:** the Production Go: Testing, Tooling and Performance course
measured module publishing and version compatibility along the axis of a piece of
knowledge's source — asking whether a version declaration came from the source, a tool, or
a run. What this lesson asks is different: not a version declaration, but where the
compiler's own checking stops when a signature is at stake.

The version string passed through no check at all, but a function's signature does not. The
run below compiles the same caller source against three separate library versions: the
first is the base version, the second is a next version written with the same signature but
a different body, the third is a version whose signature has changed. This is measured from
within the program — the sources are kept as strings, the compiler is invoked with
`std::process::Command`, only compiled/did-not-compile is printed, the error text is never
read.

```rust
// version_compatibility.rs — the same caller is tried against three library versions, only compiled/did-not-compile is printed
use std::env;
use std::fs;
use std::process::Command;

const CALLER: &str = r#"
fn main() {
    let result = convert(10);
    println!("result={}", result);
}
"#;

const VERSION_ONE: &str = "fn convert(x: i32) -> i32 { x + 1 }\n";
const VERSION_TWO_SAME_SIGNATURE: &str = "fn convert(x: i32) -> i32 { x * 2 }\n";
const VERSION_TWO_CHANGED_SIGNATURE: &str = "fn convert(x: i32, step: i32) -> i32 { x + step }\n";

fn compiles(source: &str, workdir: &std::path::Path, name: &str) -> bool {
    let path = workdir.join(format!("{}.rs", name));
    fs::write(&path, source).unwrap();
    let binary = workdir.join(name);
    Command::new("rustc")
        .args(["--edition", "2021"])
        .arg("-o").arg(&binary)
        .arg(&path)
        .output()
        .map(|c| c.status.success())
        .unwrap_or(false)
}

fn classify(success: bool) -> &'static str {
    if success { "compiled" } else { "did not compile" }
}

fn main() {
    let workdir = env::temp_dir().join(format!("version-compatibility-{}", std::process::id()));
    fs::create_dir_all(&workdir).unwrap();

    for (name, body) in [
        ("version_one", VERSION_ONE),
        ("version_two_same_signature", VERSION_TWO_SAME_SIGNATURE),
        ("version_two_changed_signature", VERSION_TWO_CHANGED_SIGNATURE),
    ] {
        let combined = format!("{}{}", body, CALLER);
        let success = compiles(&combined, &workdir, name);
        println!("{}: {}", name, classify(success));
    }

    let _ = fs::remove_dir_all(&workdir);
}
```

```
version_one: compiled
version_two_same_signature: compiled
version_two_changed_signature: did not compile
```

**PL17.** The same caller source — the `convert(10)` call and the line that prints the
result — stayed unchanged against all three versions. It compiled against the first and
second versions, because both carried the signature the caller expected; it did not compile
against the third version, because the third version had added a second parameter and the
caller passed only a single argument. The compiler genuinely tested a form of version
compatibility here — but what it tested was not the version number, it was the signature.

This stands in direct contrast to the version string falling into the nobody-checks-it
class. The class here is **still holds**: the compiler retests, at every compilation,
whether a library is still type-compatible with the caller, and this test is entirely
independent of the version number itself — the result would not change whether the second
version carried the same number as the first or a different one, because the compiler never
looks at the version field at all, only at the signature.

## A Bounding Measurement

The compiler looking at the signature genuinely tests which version the caller keeps
compiling against — but this test never reaches into behavior at all. The run below calls
two versions carrying the exact same signature side by side.

```rust
// signature_and_behavior.rs — the same signature can give a different result; the promise stops at the signature, not at the behavior
fn convert_v1(x: i32) -> i32 {
    x + 1
}

fn convert_v2(x: i32) -> i32 {
    x * 2
}

fn caller<F: Fn(i32) -> i32>(convert: F, input: i32) -> i32 {
    convert(input)
}

fn main() {
    let input = 10;
    println!("v1: {}", caller(convert_v1, input));
    println!("v2: {}", caller(convert_v2, input));
}
```

```
v1: 11
v2: 20
```

**PL18.** `convert_v1` and `convert_v2` carry the exact same signature; `caller`'s generic
bound accepts both in the same way. But when called with the same input, they produce a
completely different result: one gives `11`, the other `20`. The measurement in the
previous section would never have caught this difference at all — what it tested there was
only the question "does the caller still compile," and this question would answer "yes" for
both versions.

This limit echoes the observation from the course's first lesson: there, a macro compiled
both when called with two semantically distant types, because it checked type fitness at
the syntax level, not at the level of meaning. Here, the same signature carries two
semantically distant bodies, and the compiler accepts both, because it checks type fitness
at the signature level, not at behavior. The promise — the guarantee that saying "version
compatible" gives — stops at the signature; it does not stop at behavior. When a new version
is submitted to a package registry, the compiler accepting that version only shows that the
signature stayed consistent, it does not show that the body does the same work as before.

## Closing Table

| Lesson | What was measured | Class of guarantee | Bounding measurement |
|---|---|---|---|
| Declarative Macros | The body the macro produces passing type-checking, macro hygiene | still holds | The macro looks only at type fitness; the same macro called with `i32` and `bool` compiles in both |
| Procedural Macros | The generated output also being checked in a separate compilation step | still holds | A procedural macro must compile in its own crate — not a choice, a constraint of the language |
| Domain-Specific Languages | The macro's output also being checked again through an interface | still holds | Readable syntax does not bring correctness; a wrong route-handler binding compiles too, as long as the type fits |
| `unsafe` Rust | One operation staying unchecked while another in the same block stays checked | still holds · handed to the writer | A handed-off guarantee is invisible when used correctly; the difference lies in the responsibility for misuse |
| The Foreign Function Interface | Two C functions being called by trusting only their declared signature | handed to the writer · outside the language | Had the declaration been written wrong, the compiler could not have noticed |
| The WebAssembly Target | A decision that changes by target being separated out in the source | outside the language | Source looking portable does not mean it is portable |
| Embedded Targets | A `no_std` environment taking allocation and panic behavior into its own hands | outside the language | Allocation-free code is not always possible; a fixed buffer demands an upper bound |
| Dependencies and Feature Flags | A closed flag's body never entering compilation at all | nobody checks it | Shows it is not compiled today, does not show whether it would compile once the flag is turned on |
| Testing and Property-Based Testing | A property being tested with a fixed-seed generator | handed to the writer | A passing test is true only for the tested input set; anything outside the range is never tested |
| Benchmarking and Profiling | Two sorting implementations' comparison and copy counts | nobody checks it | Operation count does not mean duration; an implementation doing fewer operations can in fact take longer |
| Documentation | The documentation example being tested, the explanation above it not being tested | still holds · nobody checks it | The documentation tool tests the example, not the explanation; the same function can carry both classes at once |
| Publishing | The meaning of the version string and version compatibility being tested at the signature level | nobody checks it · still holds | The same signature can give different behavior — the promise stops at the signature, not at the behavior |

## Summary

- The version string sits in the source and never passes any check against the body; the
  class is **nobody checks it** — even the model that submits the version declaration to a
  package registry never looks at the version field's content.
- The same caller source compiled against three library versions: it compiled against the
  two versions whose signature stayed the same, and did not compile against the version
  whose signature changed; the class is **still holds** — the compiler looks at the
  signature, not at the version number.
- Two versions carrying the same signature gave different results when called with the
  exact same input (`11` versus `20`); the signature-level check of version compatibility
  never reaches into behavior — the promise stops at the signature, not at the behavior.
- Four classes stayed unchanged throughout the course: still holds, handed to the writer,
  outside the language, nobody checks it; the twelve observations split 4/3/2/3, and this
  final lesson added one example each to the still-holds and nobody-checks-it ends of that
  split.
- The submission to a package registry was modeled here without carrying an actual network
  call, using only a manifest and a function that accepts it; the registry's name was never
  written anywhere.

## Course Wrap-Up

The Rust Ecosystem and Advanced Topics course asked one question in twelve different setups
across twelve lessons: where does the compiler's guarantee stop here? The answer fell into
one of four classes, and these four names stayed unchanged throughout the course — still
holds, handed to the writer, outside the language, nobody checks it; the twelve observations
split four, three, two, three. The Macros and Metaprogramming topic showed that generated
source is checked like ordinary source, but that this check works only at the type level.
The Low-Level and Interoperability topic showed where checking is handed off — to the writer
or to another language — and that a handed-off guarantee stays invisible when used
correctly. The Project Lifecycle topic showed, one after another, that the source sometimes
has corners that are never checked at all — a closed flag's body, an operation count turned
into a duration, a documentation sentence's correctness, a version string's meaning. This
final lesson placed two of those corners side by side: a version string is never checked at
all, but that same version's signature is still checked.

The Rust curriculum closes with five courses, and each asked the same language a completely
different question. Rust Fundamentals asked whether a written line produces a value — the
expression, statement, item distinction. Ownership and the Memory Model asked where that
value's lifetime ends. Traits, Generics and Error Handling asked how many times a body runs.
Concurrency and Asynchronous Rust asked what a task needs in order to make progress. The
Rust Ecosystem and Advanced Topics course asked where the compiler's guarantee stops. The
five questions looked at five different layers of the same language: first the written line
itself, then a value's lifetime in the source, then how many times a body runs, then what a
task needs in order to make progress, and finally where the checking that provides all of
this itself stops.

The next curriculum moves on to C and C++; which measurement axis will be chosen for these
two languages is not yet settled in this lesson.
