astrid-runtime/astrid · error
capsule materialization destination is not a directory
Error message
capsule materialization destination is not a directory
What it means
materialize_capsule_package checks the destination with fs::symlink_metadata before extracting. If the path exists but is a symlink or a non-directory (e.g. a regular file), extraction cannot proceed and this error is thrown. It exists so the function never writes through a symlink or clobbers an existing file.
Source
Thrown at crates/astrid-capsule-install/src/storage.rs:542
.with_context(|| format!("resolve durable uid for principal {principal}"))?;
Ok(read_verified_durable_package(store, uid, id)?.map(|package| package.metadata().clone()))
}
/// Materialize a verified durable package into a fresh disposable directory.
///
/// This helper is for loaders whose current host ABI still accepts a path. It
/// never establishes authority: callers must retain the package snapshot and
/// digest, and the destination must be a new cache generation. Archive paths,
/// links, special files, duplicate entries, and symlinked parents are rejected
/// before any bytes are written. Exact metadata and authority sidecars are
/// restored from the package bytes, not trusted from the archive.
pub fn materialize_capsule_package(
package: &CapsulePackage,
destination: &Path,
) -> anyhow::Result<()> {
match fs::symlink_metadata(destination) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => {
bail!("capsule materialization destination is not a directory")
},
Ok(_) => bail!("capsule materialization destination already exists"),
Err(error) if error.kind() == io::ErrorKind::NotFound => {},
Err(error) => {
return Err(error).with_context(|| format!("inspect {}", destination.display()));
},
}
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create materialization parent {}", parent.display()))?;
}
fs::create_dir(destination)
.with_context(|| format!("create materialization {}", destination.display()))?;
let decoder = flate2::read::GzDecoder::new(Cursor::new(&package.archive));
let mut archive = tar::Archive::new(decoder);
let mut names = std::collections::BTreeSet::new();
for entry in archive.entries().context("read durable capsule archive")? {
let mut entry = entry.context("read durable capsule archive entry")?;View on GitHub (pinned to affd8760f4)
Solutions
- Point the call at a fresh, non-existent directory path.
- Remove or rename the file/symlink currently occupying the destination path.
- If a symlink was intended, resolve it (fs::canonicalize) and materialize into the resolved parent directory instead.
Example fix
// before
materialize_capsule_package(&package, &Path::new("/opt/capsule/current"))?; // symlink
// after
let dest = Path::new("/opt/capsule/releases/v42");
materialize_capsule_package(&package, dest)?;
fs::remove_file("/opt/capsule/current");
fs::symlink(dest, "/opt/capsule/current")?; Defensive patterns
Strategy: validation
Validate before calling
let dest = Path::new("/opt/capsule/materialized");
match fs::symlink_metadata(dest) {
Ok(m) if m.file_type().is_symlink() || !m.is_dir() => {
fs::remove_file(dest)?;
}
Ok(_) => anyhow::bail!("destination already exists"),
Err(_) => {}
}
materialize_capsule_package(&package, dest)?; Try / catch
match materialize_capsule_package(&pkg, &dest) {
Ok(()) => {}
Err(e) if e.to_string().contains("not a directory") => {
fs::remove_file(&dest).ok();
materialize_capsule_package(&pkg, &dest)?;
}
Err(e) => return Err(e),
} Prevention
- Pass a path that does not yet exist (unique per run).
- Never pass symlinks (e.g. 'current' release symlinks) as the destination.
- Check with symlink_metadata (not exists()) so dangling symlinks are caught too.
When it happens
Trigger: Calling materialize_capsule_package(&package, dest) where dest exists as a symlink (even dangling) or as a regular file/socket/device rather than a directory.
Common situations: A leftover file from a previous run at the destination path; a symlinked 'current release' directory passed as the destination; a temp path pointing at a file instead of a directory; passing $HOME or a path that is actually a dotfile.
Related errors
- capsule projection contains a symbolic link: {}
- projected path is redirected or not a regular file: {}
- leftover capsule authority receipt is not a regular file: {}
- legacy capsule authority root is not a regular directory: {}
- legacy capsule authority root contains a non-regular entry:
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/f57ea16c840b1cc5.
Report an issue: GitHub.