The Cargo Guide

Dependency Source Types

Why Dependency Source Types Matter

When you declare a dependency in Cargo, you are not only choosing a package name and a version requirement. You are also choosing where that package comes from.

That source choice affects:

  • reproducibility
  • trust
  • team workflows
  • offline behavior
  • publication constraints
  • performance of dependency resolution

A useful mental model is:

  • version requirements answer "which version is acceptable?"
  • source types answer "where does Cargo fetch it from?"

The main source types covered here are:

  • crates.io dependencies
  • alternate registry dependencies
  • git dependencies
  • path dependencies
  • local unpublished crates
  • vendored or mirrored sources
  • source replacement configurations

The Default Case: crates.io Dependencies

The most common source is crates.io, Rust's default public package registry.

[dependencies]
regex = "1"
serde = { version = "1", features = ["derive"] }

In this form, Cargo assumes the dependency comes from the default registry.

Example Rust code:

use regex::Regex;
 
pub fn contains_digit(s: &str) -> bool {
    Regex::new(r"\d").unwrap().is_match(s)
}

For beginners, the key point is that most Cargo examples use crates.io implicitly. You usually do not have to write anything special to say "use crates.io."

Why crates.io Feels Simple

A crates.io dependency is simple because Cargo already knows the registry and index behavior for the default public ecosystem.

[dependencies]
anyhow = "1"

This means you can focus on package name and version requirement first, then learn more advanced source types only when a project actually needs them.

That default simplicity is one reason Cargo feels approachable early on.

Alternate Registries

Cargo can also pull dependencies from registries other than crates.io.

A dependency from an alternate registry looks like this:

[dependencies]
internal_utils = { version = "1.2.0", registry = "company" }

This says:

  • the package name is internal_utils
  • the acceptable version is 1.2.0-compatible according to Cargo's normal version rules
  • the package should come from the registry named company

Alternate registries are commonly used in organizations that maintain private package ecosystems.

Configuring an Alternate Registry

The registry name used in Cargo.toml is usually defined in Cargo configuration.

Illustrative configuration:

[registries.company]
index = "sparse+https://packages.example.com/index/"

That gives Cargo a registry name, company, and tells it where the registry index lives.

A dependency can then use that registry by name:

[dependencies]
internal_utils = { version = "1.2.0", registry = "company" }

This division is important:

  • Cargo.toml chooses the registry by logical name
  • Cargo config tells Cargo how to reach that registry

Git Dependencies

A git dependency tells Cargo to fetch a package from a git repository instead of a registry.

Basic form:

[dependencies]
parser_core = { git = "https://github.com/example/parser_core.git" }

You can also target a specific branch:

[dependencies]
parser_core = { git = "https://github.com/example/parser_core.git", branch = "main" }

Or a tag:

[dependencies]
parser_core = { git = "https://github.com/example/parser_core.git", tag = "v0.4.0" }

Or a specific revision:

[dependencies]
parser_core = { git = "https://github.com/example/parser_core.git", rev = "8f2a6b7" }

Git dependencies are useful when:

  • the crate is not published yet
  • you need a branch or fix not available in a registry release
  • you are temporarily testing upstream changes

When Git Dependencies Are a Good Fit

Git dependencies are powerful, but they are usually a step away from the normal registry workflow.

They are often appropriate for:

  • active collaboration across repositories
  • prototyping before publication
  • temporary dependency overrides during development

They are usually less ideal when your goal is the most conventional, stable, public dependency story.

A practical beginner rule is:

  • prefer registry dependencies when possible
  • use git dependencies when you specifically need repository state rather than registry publication

Path Dependencies

A path dependency points to another package on the local filesystem.

[dependencies]
core_utils = { path = "../core_utils" }

Suppose the filesystem looks like this:

projects/
ā”œā”€ā”€ app/
│   └── Cargo.toml
└── core_utils/
    └── Cargo.toml

Then app can depend on ../core_utils.

Example Rust usage:

use core_utils::normalize_name;
 
fn main() {
    println!("{}", normalize_name("  Alice  "));
}

Path dependencies are common during local development, especially in multi-package repositories or when code has not been published.

Local Unpublished Crates

A local unpublished crate is usually just a package you depend on through a path while it still exists only on your machine or within your repository.

Example directory layout:

workspace/
ā”œā”€ā”€ app/
│   ā”œā”€ā”€ Cargo.toml
│   └── src/main.rs
└── math_core/
    ā”œā”€ā”€ Cargo.toml
    └── src/lib.rs

app/Cargo.toml:

[dependencies]
math_core = { path = "../math_core" }

math_core/src/lib.rs:

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

app/src/main.rs:

fn main() {
    println!("{}", math_core::double(21));
}

This is often the first step before a local crate becomes either a workspace member or a published package.

Path Dependencies vs Workspaces

A path dependency and a workspace are related ideas, but they are not the same.

A path dependency simply tells Cargo where another package lives:

[dependencies]
core_utils = { path = "../core_utils" }

A workspace introduces a larger organizational structure:

[workspace]
members = ["app", "core_utils"]
resolver = "3"

A useful mental model is:

  • path dependencies solve local package linking
  • workspaces solve multi-package project organization

Many real codebases use both together.

Registry Dependencies vs Path Dependencies in Practice

It is common for a crate to begin life as a path dependency and later move to a published registry dependency.

Early local phase:

[dependencies]
core_utils = { path = "../core_utils" }

Later published phase:

[dependencies]
core_utils = "0.3"

That transition changes the dependency from local filesystem provenance to registry provenance. The code using the crate may stay almost unchanged, but the dependency source story becomes very different.

Vendored Sources

Vendoring means storing dependency source material locally so builds do not need to fetch packages live from the network.

A common workflow uses a vendored directory and source replacement.

Illustrative directory layout:

my_project/
ā”œā”€ā”€ .cargo/
│   └── config.toml
ā”œā”€ā”€ vendor/
ā”œā”€ā”€ Cargo.toml
└── src/
    └── main.rs

Illustrative config:

[source.crates-io]
replace-with = "vendored-sources"
 
[source.vendored-sources]
directory = "vendor"

This tells Cargo to replace the normal crates.io source with a local directory source.

Vendoring is useful for:

  • offline builds
  • hermetic CI
  • reviewable third-party source snapshots
  • environments with restricted network access

Local Registry Sources

Cargo's source replacement model also supports local registry sources.

Illustrative configuration:

[source.my-local-registry]
local-registry = "local-registry"

A local registry is different from a plain vendored source directory.

A useful mental distinction is:

  • directory = "vendor" points to unpacked vendored crate sources
  • local-registry = "..." points to a registry-like local structure containing crate archives and index data

Most beginners encounter vendored directories first because they are conceptually simpler.

Mirrored Sources

A mirror is a source that reproduces content from another source while changing where Cargo fetches it from.

At the Cargo configuration level, this often shows up through source replacement.

Illustrative configuration:

[source.crates-io]
replace-with = "company-mirror"
 
[source.company-mirror]
registry = "sparse+https://mirror.example.com/index/"

This means:

  • dependency declarations can still look like ordinary crates.io dependencies
  • Cargo is redirected to fetch from the configured mirror instead

Mirrors are useful for reliability, caching, internal governance, or network control.

Source Replacement

Source replacement tells Cargo to substitute one source for another.

Illustrative example:

[source.crates-io]
replace-with = "vendored-sources"
 
[source.vendored-sources]
directory = "vendor"

Or replacement with another registry-style source:

[source.crates-io]
replace-with = "company-mirror"
 
[source.company-mirror]
registry = "sparse+https://mirror.example.com/index/"

A strong mental model is:

  • dependency declarations describe dependency intent
  • source replacement rewrites where those dependencies are resolved from

This is especially important in enterprise, offline, or controlled-build environments.

What Source Replacement Is Good For

Source replacement is most useful when you want to preserve ordinary dependency declarations while changing the actual fetch location.

That can support:

  • vendored builds
  • corporate mirrors
  • restricted network environments
  • controlled provenance workflows

It lets a team keep package manifests relatively normal while centralizing source policy in Cargo configuration.

Sparse vs Git Index Protocols

At the registry protocol level, Cargo supports two main remote index styles:

  • git protocol
  • sparse protocol

Conceptually:

  • a git index behaves like a git repository containing registry metadata
  • a sparse index fetches only the metadata files needed for relevant crates over HTTP

Illustrative registry config with sparse:

[registries.company]
index = "sparse+https://packages.example.com/index/"

Illustrative registry config with git-style index:

[registries.company]
index = "https://git.example.com/company-index.git"

A good conceptual summary is:

  • git index: repository-oriented metadata distribution
  • sparse index: file-by-file HTTP metadata distribution

Why Sparse Can Feel Faster

The sparse protocol is designed to fetch only the metadata files Cargo actually needs for the dependencies being resolved.

That usually means less metadata transfer than cloning an entire registry index repository.

The main beginner takeaway is not implementation detail, but intent:

  • sparse is optimized around narrower metadata fetching
  • git index is optimized around the model of a local cloned index repository

A Conceptual Comparison of Sparse and Git

Suppose you depend on only a small number of crates.

With a sparse index, Cargo can conceptually fetch only the relevant metadata files.

With a git index, Cargo conceptually interacts with a registry metadata repository and caches that repository locally.

You do not need to memorize network internals to understand the tradeoff. The important design distinction is that sparse aims to avoid the "clone the whole index" model.

How Cargo Configures Registry Protocol Choice

Registry protocol choice appears through the registry index URL.

Sparse registry example:

[registries.company]
index = "sparse+https://packages.example.com/index/"

Git registry example:

[registries.company]
index = "https://git.example.com/company-index.git"

That sparse+ prefix is the key visible signal that a registry is using the sparse protocol.

Registry Trust and Provenance

Dependency source type is not only a technical choice. It is also a provenance choice.

Questions worth asking include:

  • who controls this source?
  • how is it authenticated?
  • can the team reproduce builds from it later?
  • is it public, private, mirrored, or local-only?
  • is the dependency coming from a reviewed release, a repository branch, or a local path?

For example, these sources imply different provenance stories:

[dependencies]
serde = "1"
[dependencies]
parser_core = { git = "https://github.com/example/parser_core.git", rev = "8f2a6b7" }
[dependencies]
core_utils = { path = "../core_utils" }

All three may work, but they answer "where did this code come from?" differently.

Trust Differences Between Common Source Types

A useful practical spectrum is:

  • crates.io release: public registry publication model
  • alternate registry: organizational or private publication model
  • git dependency: repository state model
  • path dependency: local filesystem model
  • vendored source: locally stored snapshot model

This is not a ranking of good to bad. It is a way to think clearly about provenance and operational expectations.

For example, a path dependency may be perfect for local development but poor as the long-term story for a published open-source crate. A vendored source may be excellent for hermetic builds but unnecessary for a small personal project.

A Realistic Mixed Example

Here is a package that uses several source types together.

[dependencies]
serde = "1"
internal_utils = { version = "1.2.0", registry = "company" }
parser_core = { git = "https://github.com/example/parser_core.git", tag = "v0.4.0" }
core_utils = { path = "../core_utils" }

And a possible Cargo config:

[registries.company]
index = "sparse+https://packages.example.com/index/"
 
[source.crates-io]
replace-with = "vendored-sources"
 
[source.vendored-sources]
directory = "vendor"

This setup shows how dependency source declarations and source replacement rules can work together:

  • serde is declared like a normal crates.io dependency
  • Cargo is configured to satisfy crates.io through a vendored directory
  • internal_utils comes from a named alternate registry
  • parser_core comes from a git repository
  • core_utils comes from the local filesystem

Code Example Using Mixed Sources

Suppose core_utils is a local unpublished crate and serde is a normal dependency.

use serde::Serialize;
 
#[derive(Serialize)]
pub struct Report {
    pub name: String
}
 
pub fn build_report(input: &str) -> Report {
    Report {
        name: core_utils::normalize_name(input)
    }
}

The Rust code does not usually reveal the source type directly. That source story lives mainly in the manifest and configuration.

Common Beginner Mistakes

Mistake 1: thinking every dependency source is a registry dependency.

Path and git dependencies are common and useful.

Mistake 2: treating path dependencies as if they were publish-ready external dependencies.

They are usually a local development mechanism.

Mistake 3: using git dependencies casually when a registry release would be simpler and more stable.

Mistake 4: assuming vendoring changes the manifest declarations.

Often it changes Cargo configuration instead.

Mistake 5: not noticing that source choice changes provenance and trust assumptions.

A dependency source is part of the software supply story, not just a fetch mechanism.

Hands-On Exercise

Create two local packages and connect them with a path dependency.

mkdir dep_sources_lab
cd dep_sources_lab
cargo new app
cargo new core_utils --lib

Edit app/Cargo.toml:

[package]
name = "app"
version = "0.1.0"
edition = "2024"
 
[dependencies]
core_utils = { path = "../core_utils" }
serde = { version = "1", features = ["derive"] }

Edit core_utils/src/lib.rs:

pub fn normalize_name(s: &str) -> String {
    s.trim().to_lowercase()
}

Edit app/src/main.rs:

fn main() {
    println!("{}", core_utils::normalize_name("  Alice  "));
}

Run it:

cd app
cargo run

Then, as a thought exercise, compare how the manifest would change if core_utils were later published to a registry or pulled from a git repository instead.

Mental Model Summary

A strong mental model for Cargo dependency source types is:

  • crates.io is the default public registry source
  • alternate registries support private or organizational package ecosystems
  • git dependencies fetch repository state directly
  • path dependencies point at local packages on disk
  • local unpublished crates are often consumed first through path dependencies
  • vendored and mirrored sources let teams control network usage and provenance
  • source replacement lets configuration rewrite where dependencies are fetched from
  • sparse and git index protocols are two different ways registry metadata can be distributed
  • source choice is part of trust, provenance, and reproducibility, not just syntax

Once this model is stable, dependency declarations become much easier to reason about operationally.