jdx/mise · error · eyre::Report

[dotfiles]."{}": mode symlink-each requires a directory sour

Error message

[dotfiles]."{}": mode symlink-each requires a directory source: {}

What it means

An `[oci.dotfiles]` entry with `mode = "symlink-each"` symlinks every entry inside a source directory into the image (one symlink per file, preserving names). Because it iterates directory contents with walkdir, the source must be a directory; pointing symlink-each at a regular file is a configuration error and aborts the build with the entry key and path.

Source

Thrown at src/oci/builder.rs:882

    for req in requests {
        if req.mode != FileMode::Content && !req.source.exists() {
            bail!(
                "[dotfiles].\"{}\": source does not exist: {}",
                req.target_raw,
                req.source.display()
            );
        }

        match req.mode {
            FileMode::Symlink | FileMode::Copy => {
                collect_source_as_files(&req.source, &oci_target_path(req)?, &mut entries)
                    .wrap_err_with(|| {
                        format!("adding [dotfiles].\"{}\" to OCI image", req.target_raw)
                    })?;
            }
            FileMode::SymlinkEach => {
                if !req.source.is_dir() {
                    bail!(
                        "[dotfiles].\"{}\": mode symlink-each requires a directory source: {}",
                        req.target_raw,
                        req.source.display()
                    );
                }
                let target = oci_target_path(req)?;
                entries.add_dir(target.clone())?;
                for entry in walkdir::WalkDir::new(&req.source).sort_by_file_name() {
                    let entry = entry?;
                    let ft = entry.file_type();
                    if !(ft.is_file() || ft.is_symlink()) {
                        continue;
                    }
                    let rel = entry.path().strip_prefix(&req.source)?;
                    let path = format!("{target}/{}", rel.to_string_lossy().replace('\\', "/"));
                    entries.add_file(
                        path,
                        file::read(entry.path())?,

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Point the symlink-each entry at a directory containing the files you want individually linked
  2. If you meant to link a single file, use `mode = "symlink"` (or "copy") instead
  3. Restructure the source so each linked item sits inside a dedicated directory

Example fix

# before (mise.toml)
[oci.dotfiles]
"~/bin" = { source = "./run-tool.sh", mode = "symlink-each" }

# after (mise.toml)
"~/bin/run-tool.sh" = { source = "./run-tool.sh", mode = "symlink" }
Defensive patterns

Strategy: validation

Validate before calling

python3 - <<'EOF'
import tomllib, os, sys
cfg = tomllib.load(open('mise.toml', 'rb'))
for target, spec in (cfg.get('oci', {}).get('dotfiles', {}) or {}).items():
    if isinstance(spec, dict) and spec.get('mode') == 'symlink-each':
        src = os.path.expanduser(spec.get('source', ''))
        if not os.path.isdir(src): sys.exit(f'dotfiles {target!r}: symlink-each source is not a directory: {src}')
print('symlink-each sources OK')
EOF

Prevention

When it happens

Trigger: `[oci.dotfiles]` entry with mode symlink-each whose source resolves to a file, e.g. `"bin" = { source = "./script.sh", mode = "symlink-each" }`. Also triggered when a expected directory path is occupied by a same-named file.

Common situations: Confusing symlink (whole target) with symlink-each (per-entry inside a directory); restructuring a dotfiles repo so a former directory is now a file.

Understand the failure class

Background: Config validation failed: what "invalid value for {key}" and settings-rejection errors mean across 19 open-source libraries — this error's family across 19 libraries.

Related errors


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