Skip to content

Support Rust and Cargo dependencies #260

Description

@fniessink

The problem

A Rust project gets nothing from Update-time. Its Cargo.toml is not scanned, its dependencies are not updated, and none of the four warnings reaches them — while the Dockerfile and the GitHub Actions workflow in the same repository are covered.

That is worth changing because Rust is the type where every check would work. Probed 2026-08-15:

Question Answer
What crates.io reports per version num, created_at, yanked, checksum
Does OSV match a crates.io version? yes — time@0.1.44 returns GHSA-wcg3-cvx6-7396 and RUSTSEC-2020-0071

Publication dates make the cooldown and the staleness check possible, yanked is crates.io's own concept rather than something to approximate, and OSV matches versions properly — unlike the GitHub Actions ecosystem, where a version-bearing query comes back empty. Cargo would be the second dependency type after Python to get all five checks, so its per-type documentation would have a real answer in every subsection rather than several "none: no such concept".

What would be updated

  • Cargo.toml, searched for recursively. Cargo.lock stays untouched, since cargo maintains it.
  • The dependencies in [dependencies], [dev-dependencies], [build-dependencies], [workspace.dependencies], and the target-specific tables, in both spellings: the string form serde = "1.0" and the table form serde = { version = "1.0", features = ["derive"] }.
  • Not a path or a git dependency, which crates.io does not serve. That is the same case as a dependency uv resolves through a [tool.uv.sources] entry, and it wants the same treatment: left out of the update and out of the checks, rather than looked up under a name that may belong to an unrelated crate.

The design constraint: what counts as a pin

Cargo's default requirement is a caret range. serde = "1.0" means ^1.0, so it already admits 1.9, and the exact spelling is serde = "=1.0.203". Update-time's rule elsewhere is that only an exact pin is rewritten, which would leave almost every real Rust manifest untouched, since the caret form is the convention rather than a deliberate opt-out.

So the question this type has to answer first is whether a caret requirement is a pin to move forward or a range to leave alone. npm's answer is to delegate and let the range decide; Python's answer is to rewrite the exact pins and let a looser specifier be the opt-out. Rust sits between the two, because the idiomatic spelling is a range and there is no separate convention for "keep me here".

Who rewrites the manifest

Unverified, and it needs verifying before any of this is built. Cargo is installed on the machine this was written on only through rustup, with no default toolchain, so nothing could be run against a real project.

From cargo's documented behaviour, cargo update resolves within the requirement the manifest declares, as npm update does, and bumping the requirement itself is cargo upgrade, which ships in the separate cargo-edit crate rather than in cargo. If that holds, Update-time rewrites Cargo.toml itself and then runs cargo update to bring Cargo.lock along — the shape the pyproject.toml path already has, where Update-time writes the pins and uv relocks.

The five checks

Check How
Update the versions crates.io lists, filtered by whatever the previous section settles about caret requirements
Cooldown created_at per version, applied by Update-time itself
Staleness the newest version's created_at
Yank yanked per version, so a yanked release is skipped when picking a new version and a reference left on one is warned about
Vulnerability OSV's crates.io ecosystem

Pinning

The manifest holds no hash, so Update-time adds none — the answer Python dependencies already get. crates.io reports a checksum per version and Cargo.lock records it, and cargo maintains that file.

Markers

Cargo.toml is TOML and takes # comments, and a dependency is normally one per line, so both marker placements work and the comment leads need no change. A dependency declared in the table form spans several lines when it is formatted that way, which is the one placement wrinkle worth testing.

Out of scope

  • Cargo.lock, which cargo maintains.
  • rust-toolchain.toml, which names a toolchain rather than a dependency. It is the Rust counterpart of .python-version and deserves its own issue.
  • Registries other than crates.io, declared through [registries].

Documentation

  • A new per-type section under "Details per dependency type", with all nine subsections the structure check requires.
  • A row in the first dependency-type table, which then obliges a row in each of the other tables.
  • The list of scanned files in the command-line help.
  • A changelog entry under [Unreleased].

Open questions

  1. Is a caret requirement a pin to move forward or a range to leave alone? Everything else about the type follows from this.
  2. Does Update-time rewrite the requirement itself, or shell out to cargo upgrade when cargo-edit is installed and do nothing when it is not?
  3. What happens to a member that inherits from the workspace, serde = { workspace = true }, whose version lives in the root manifest? The reference to update is in one file and the declaration naming it in another.
  4. Does a workspace get one cargo update run at its root, or one per member manifest?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions