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
- Remove the trailing slash and spell the full destination file name: "./bin/mytool:/usr/local/bin/mytool".
- 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
- Mirror cp semantics: trailing slash only for directory targets.
- Prefer writing the full destination filename everywhere; it is unambiguous for both kinds.
- Avoid templating that unconditionally appends '/' to destinations.
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
- not a directory: {}
- cannot copy a file or symlink to the image root
- unsupported host path type: {}
- mise oci: no project mise config found in the current direct
- mise oci does not support [bootstrap.macos.*] defaults (foun
AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17).
Data as JSON: /api/errors/29c32bb56d255bba.
Report an issue: GitHub.