rust-lang/cargo · error

Lockfile path can't be root

Error message

Lockfile path can't be root

What it means

Panic in Workspace::lock_root (src/workspace/workspace.rs:733). When a custom lockfile location is set (via the resolver.lockfile-path config key or the deprecated --lockfile-path flag), lock_root() returns the directory containing the lockfile by calling requested.parent().expect("Lockfile path can't be root"). Path::parent() returns None when the path is a filesystem root (e.g. "/", "C:\", or a UNC root), so the panic fires when the configured/derived lockfile path resolves to a root that has no parent directory.

Source

Thrown at src/workspace/workspace.rs:733

        self
    }

    pub fn ignore_lock(&self) -> bool {
        self.ignore_lock
    }

    pub fn set_ignore_lock(&mut self, ignore_lock: bool) -> &mut Workspace<'gctx> {
        self.ignore_lock = ignore_lock;
        self
    }

    /// Returns the directory where the lockfile is in.
    pub fn lock_root(&self) -> Filesystem {
        if let Some(requested) = self.requested_lockfile_path.as_ref() {
            return Filesystem::new(
                requested
                    .parent()
                    .expect("Lockfile path can't be root")
                    .to_owned(),
            );
        }
        self.default_lock_root()
    }

    fn default_lock_root(&self) -> Filesystem {
        if self.root_maybe().is_embedded() {
            // Include a workspace hash in case the user requests a shared build-dir so that
            // scripts don't fight over the `Cargo.lock` content
            let workspace_manifest_path = self.root_manifest();
            let real_path = std::fs::canonicalize(workspace_manifest_path)
                .unwrap_or_else(|_err| workspace_manifest_path.to_owned());
            let hash = crate::util::hex::short_hash(&real_path);
            self.build_dir().join(hash)
        } else {
            Filesystem::new(self.root().to_owned())
        }

View on GitHub (pinned to 0e07a15537)

Solutions

  1. Point resolver.lockfile-path at a real subdirectory, not a filesystem root: use a relative path (relative to the config file) or an absolute path under an existing directory, e.g. `lockfile-path = "target/Cargo.lock"`.
  2. Before relying on the value, verify the resolved path has a parent: in tooling that drives cargo, assert path.parent().is_some() and that the parent is an existing writable directory.
  3. If you do not actually need a custom lockfile location, remove the resolver.lockfile-path key (and the --lockfile-path flag) entirely so cargo falls back to default_lock_root().
  4. On Windows, double-check the path is a fully-qualified `C:\dir\Cargo.lock` form, not a prefix-only or UNC-root value.

Example fix

# before (panics):
#   # .cargo/config.toml
#   [resolver]
#   lockfile-path = "/Cargo.lock"     # parent() degenerate => panic in lock_root

# after (safe):
#   [resolver]
#   lockfile-path = "target/Cargo.lock"   # real parent dir => lock_root() ok
Defensive patterns

Strategy: validation

Validate before calling

use std::path::Path;

// Validate the configured lockfile path BEFORE letting cargo call lock_root().
// Mirrors the in-cargo checks (ends_with Cargo.lock, not a dir) and adds the missing parent() guard.
fn valid_lockfile_path(p: &Path) -> Result<(), String> {
    if !p.ends_with("Cargo.lock") {
        return Err("lockfile-path must end with Cargo.lock".into());
    }
    let parent = p.parent().ok_or_else(|| "lockfile-path resolves to a filesystem root (no parent)".to_string())?;
    if parent.as_os_str().is_empty() {
        return Err("lockfile-path needs an explicit parent directory".into());
    }
    if p.is_dir() {
        return Err("lockfile-path is a directory".into());
    }
    Ok(())
}

Type guard

use std::path::{Path, PathBuf};

// Narrow to a path guaranteed not to trip lock_root()'s .expect.
struct NonRootLockfilePath(PathBuf);

impl NonRootLockfilePath {
    fn new(p: PathBuf) -> Result<Self, &'static str> {
        match (p.ends_with("Cargo.lock"), p.parent()) {
            (true, Some(parent)) if !parent.as_os_str().is_empty() && !p.is_dir() => Ok(Self(p)),
            _ => Err("lockfile-path must be a Cargo.lock inside a real directory"),
        }
    }
    fn as_path(&self) -> &Path { &self.0 }
}

Try / catch

// Not recommended: this is a deterministic config error, not a runtime fault.
// Prefer validation (above). If you must defend at the boundary:
use std::panic;
let root = panic::catch_unwind(|| workspace.lock_root());
match root {
    Ok(fs) => { /* use fs */ }
    Err(_)  => { /* fall back to ws.set_requested_lockfile_path(None) and use default_lock_root() */ }
}

Prevention

When it happens

Trigger: Setting resolver.lockfile-path in .cargo/config.toml (or passing --lockfile-path) to a value that, after ConfigRelativePath resolution and the ends_with("Cargo.lock") + non-directory checks at lines 395-403, still ends up at a path whose parent() is None - e.g. a root-level absolute path or a degenerate Windows prefix path. The existing validation does NOT reject parent()==None, so such a value slips through to lock_root() and panics on the next resolution.

Common situations: Trying to force the lockfile to a root-mounted location; a misbehaving wrapper/script that computes lockfile-path and accidentally yields a root; Windows path quirks where a prefix-only path has no directory parent; copy-pasting an absolute lockfile path that normalizes to root after config-relative resolution.

Related errors


AI-assisted analysis of rust-lang/cargo@0e07a15537 (2026-08-06). Data as JSON: /data/errors/9cf9140e251f5061.json. Report an issue: GitHub.