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
- 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"`.
- 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.
- 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().
- 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
- Use a relative resolver.lockfile-path (resolved against the config file) so it can never become a filesystem root.
- Add a parent()-is_some() + non-empty check in any wrapper that computes a lockfile path before passing it to cargo.
- Omit resolver.lockfile-path / --lockfile-path unless you genuinely need a custom lockfile location.
- On Windows, always use a fully-qualified `DRIVE:\dir\Cargo.lock` form.
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
- you can't generate a lockfile for an empty workspace.
- package collision in the lockfile: packages {} and {} are di
- local registry path is not a directory: {}
- local registry index path is not a directory: {}
- could not find `{}` in `{}` or any parent directory
AI-assisted analysis of rust-lang/cargo@0e07a15537 (2026-08-06).
Data as JSON: /data/errors/9cf9140e251f5061.json.
Report an issue: GitHub.