Skip to main content

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:

  1. Build a .crate tarball: cargo package --no-verify --allow-dirty --target-dir <tmp>.
  2. Compute SHA-256 of the tarball.
  3. POST to https://crates.io/api/v1/crates/new with:
    • Header: Authorization: <token> (legacy) or Authorization: Sigstore <signed-bundle> (RFC #3724).
    • Body: a multipart-encoded blob containing a JSON metadata block plus the tarball.
  4. 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": {},
"authors": ["Tam Nguyen Duc <[email protected]>"],
"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].name matches crates.io's name regex [a-zA-Z0-9_-]{1,64} (the bridge flattens @scope/name to scope-name).
  • [package].version parses as semver 2.0.0.
  • [package].license parses as an SPDX expression (Apache-2.0 OR MIT, MIT, BSD-3-Clause, etc.).
  • [package].description is between 1 and 1024 characters.
  • [package].repository is a valid URL.
  • The README file referenced by [package].readme exists.
  • 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