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

  1. Point the call at a fresh, non-existent directory path.
  2. Remove or rename the file/symlink currently occupying the destination path.
  3. 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

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


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/f57ea16c840b1cc5. Report an issue: GitHub.