jdx/mise · error · eyre::Report

copy image path {image_path:?} ends with `/` but {} is not a

Error message

copy image path {image_path:?} ends with `/` but {} is not a directory; specify the exact destination file name

What it means

In tar-copy semantics a trailing '/' on the destination means 'this is a directory target' (like cp -r dir/). The guard fires when the source's symlink_metadata is NOT a directory (file or symlink) but image_path still ends with '/', because then the intended file name would be ambiguous — the entry would otherwise be placed at an unspecified name inside that directory.

Source

Thrown at src/oci/layer.rs:126

/// Directory contents are placed under `image_path`; a file or symlink is
/// placed at `image_path` itself.
pub fn build_layer_from_path(
    host_path: &Path,
    image_path: &str,
    owner: LayerOwner,
) -> Result<LayerBlob> {
    let metadata = std::fs::symlink_metadata(host_path)
        .wrap_err_with(|| format!("reading metadata for {}", host_path.display()))?;
    let target = image_path.trim_matches('/');

    if metadata.is_dir() {
        return build_layer_from_dir(host_path, target, owner);
    }
    if target.is_empty() {
        eyre::bail!("cannot copy a file or symlink to the image root");
    }
    if image_path.ends_with('/') {
        eyre::bail!(
            "copy image path {image_path:?} ends with `/` but {} is not a directory; \
             specify the exact destination file name",
            host_path.display()
        );
    }

    let kind = if metadata.file_type().is_symlink() {
        EntryKind::Symlink(
            std::fs::read_link(host_path)
                .wrap_err_with(|| format!("reading symlink {}", host_path.display()))?,
        )
    } else if metadata.is_file() {
        EntryKind::File
    } else {
        eyre::bail!("unsupported host path type: {}", host_path.display());
    };
    let mode = match &kind {
        EntryKind::Symlink(_) => 0o777,

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Remove the trailing slash and spell the full destination file name: "./bin/mytool:/usr/local/bin/mytool".
  2. Or change the source to the parent directory if you really want the whole tree copied.

Example fix

# before
files = [ "./config.toml:/etc/mise/" ]

# after
files = [ "./config.toml:/etc/mise/config.toml" ]
Defensive patterns

Strategy: validation

Validate before calling

import os, sys
for src, dst in copy_entries.items():
    if dst.endswith("/") and not os.path.isdir(src):
        sys.exit(f"{src} is a file but {dst!r} ends with '/'; give the exact file name")

Prevention

When it happens

Trigger: A copy entry pairing a regular file or symlink source with a destination like "/usr/local/bin/" — the code path is reached only when metadata.is_dir() is false, then the trailing slash is rejected.

Common situations: Copy-pasting a directory-style destination for a file, or template code that appends '/' to every destination uniformly.

Related errors


AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17). Data as JSON: /api/errors/29c32bb56d255bba. Report an issue: GitHub.