06. Cargo publish flow
This note documents the Mochi-as-Rust-library publish path: how mochi pkg publish --to=crates.io lowers a Mochi package to a publishable Rust library crate and uploads it.
The library crate shape
A Rust library crate intended for crates.io distribution has the following structure:
mochi-emitted/
Cargo.toml
Cargo.lock # only when publishing a workspace
README.md # mandatory for crates.io
LICENSE-APACHE # license file
LICENSE-MIT
src/
lib.rs # the root of the public API
<other modules>
build.rs # optional; only when cbindgen is configured
include/ # optional; cbindgen-generated C header
mochi_emitted.h
The user writes Mochi in <package>/src/*.mochi. The bridge lowers via TargetRustLibrary (a new MEP-53 target) to the above tree.
Cargo.toml minimum:
[package]
name = "mochi-example"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0 OR MIT"
description = "An example Mochi package, published as a Rust library crate."
repository = "https://github.com/example/mochi-example"
documentation = "https://docs.rs/mochi-example"
readme = "README.md"
keywords = ["mochi", "example"]
categories = ["data-structures"]
rust-version = "1.78"
[lib]
crate-type = ["rlib", "cdylib"]
[dependencies]
mochi-runtime = "0.6"
serde = { version = "1.0", features = ["derive"] }
[build-dependencies]
cbindgen = "0.27" # only when [rust.publish] cbindgen = true
The metadata fields come from the Mochi mochi.toml [package] table (description, repository, license, etc.) with no transformation. The Rust crate name is the Mochi package name with @scope/name flattened to scope-name (crates.io does not support @scope/ syntax; the bridge emits a flat name and records the mapping in mochi.lock).
src/lib.rs is generated by lowering the Mochi package's public API. Mochi fun items become pub fn. Mochi record items become pub struct. Mochi type T = A | B items become pub enum. Private items (those not in the package's pub set, per the Mochi visibility model) become pub(crate).
cbindgen.toml, when [rust.publish] cbindgen = true:
language = "C"
header = "/* Generated by cbindgen from Mochi sources */"
include_guard = "MOCHI_EXAMPLE_H"
braces = "NextLine"
line_length = 80
tab_width = 4
[export]
include = []
exclude = []
[export.rename]
"i64" = "int64_t"
"u8" = "uint8_t"
The build.rs invokes cbindgen at build time:
extern crate cbindgen;
fn main() {
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
cbindgen::generate(&crate_dir)
.expect("cbindgen failed")
.write_to_file("include/mochi_example.h");
}
crates.io publish protocol
The crates.io upload protocol is documented at https://doc.rust-lang.org/cargo/reference/registries.html#publishing-protocol. A publish operation is:
- Build a
.cratetarball:cargo package --no-verify --allow-dirty --target-dir <tmp>. - Compute SHA-256 of the tarball.
- POST to
https://crates.io/api/v1/crates/newwith:- Header:
Authorization: <token>(legacy) orAuthorization: Sigstore <signed-bundle>(RFC #3724). - Body: a multipart-encoded blob containing a JSON metadata block plus the tarball.
- Header:
- crates.io validates the metadata, deduplicates against the existing index, stores the tarball at
https://static.crates.io/crates/<name>/<name>-<version>.crate, and appends to the sparse index.
The JSON metadata block at step 3:
{
"name": "mochi-example",
"vers": "0.1.0",
"deps": [
{ "name": "mochi-runtime", "version_req": "^0.6", "features": [], "optional": false, "default_features": true, "target": null, "kind": "normal", "registry": null, "explicit_name_in_toml": null }
],
"features": {},
"description": "An example Mochi package, published as a Rust library crate.",
"documentation": "https://docs.rs/mochi-example",
"homepage": null,
"readme": "(README content)",
"readme_file": "README.md",
"keywords": ["mochi", "example"],
"categories": ["data-structures"],
"license": "Apache-2.0 OR MIT",
"license_file": null,
"repository": "https://github.com/example/mochi-example",
"badges": {},
"links": null
}
The bridge composes this block from mochi.toml + the lowered Mochi-to-Rust dependency graph + the README content.
Sparse index format
After a successful publish, crates.io appends a single JSON line to the sparse index at https://index.crates.io/<bucket>/<bucket>/<name>:
{"name":"mochi-example","vers":"0.1.0","deps":[...],"cksum":"<sha256-hex>","features":{},"yanked":false,"links":null,"v":2,"features2":{},"rust_version":"1.78"}
<bucket>/<bucket> is the prefix-hashing scheme Cargo uses for sparse-index path locality: a 4-char crate name uses 4/<name>, others use <first2>/<chars3-4>/<name>.
The bridge does NOT touch the sparse index directly. crates.io's server-side handler appends to it on a successful upload.
Publish-side gate
MEP-53 phase 15 established a publish-side gate for the runtime crate: cargo publish --dry-run --no-verify --allow-dirty, opt-in via MOCHI_RUN_PUBLISH_DRYRUN=1. MEP-73 extends this gate to user packages:
$ mochi pkg publish --to=crates.io --dry-run
[1/5] Lowering package via TargetRustLibrary ... (1.2s)
[2/5] Packaging via cargo package --no-verify --allow-dirty ... (3.5s)
[3/5] Validating crates.io metadata ... OK
[4/5] Obtaining OIDC token from CI environment ... OK (GitHub Actions id-token: write)
[5/5] DRY RUN: would POST to https://crates.io/api/v1/crates/new
Bundle SHA-256: <hash>
Rekor would log entry index: (simulated) 12345678
A real publish (without --dry-run) replaces step 5 with the actual POST and Rekor log write.
Metadata validation
Before upload the bridge validates:
[package].namematches crates.io's name regex[a-zA-Z0-9_-]{1,64}(the bridge flattens@scope/nametoscope-name).[package].versionparses as semver 2.0.0.[package].licenseparses as an SPDX expression (Apache-2.0 OR MIT,MIT,BSD-3-Clause, etc.).[package].descriptionis between 1 and 1024 characters.[package].repositoryis a valid URL.- The README file referenced by
[package].readmeexists. - The licence files referenced (LICENSE-APACHE, LICENSE-MIT) exist.
A validation failure exits before the upload step.
Licence compatibility walk
When the Mochi package depends on Rust crates (via [rust-dependencies]), the published Mochi-as-library crate effectively re-exports their licence terms. The bridge walks the transitive dep graph and computes the SPDX expression union; if the union is incompatible with the declared [package] license, publish fails:
$ mochi pkg publish --to=crates.io
ERROR: licence incompatibility
Declared: Apache-2.0 OR MIT
Transitive: GPL-3.0 (via [email protected] from [rust-dependencies])
Resolution: either remove my-strange-dep, or change [package].license to a GPL-compatible expression.
The bridge consults the licence-compat table in ~/.cache/mochi/licence-compat.json (refreshed quarterly from the SPDX licence list and OSI compatibility matrix). Common incompatibilities (Apache-2.0 + GPL-2.0-only, Apache-2.0 + AGPL) are detected.
Cross-references
- 01-language-surface for the
mochi pkg publishCLI surface. - 07-sigstore-cargo-rfc3724 for the trusted-publishing OIDC path.
- 09-abi-stability for the cdylib ABI shape downstream consumers link against.
- MEP-53 phase 15 for the existing publish-dry-run gate.
- MEP-73 §5 for the normative CLI surface.