jdx/mise · error

mode symlink-each requires the source to be a directory

Error message

mode symlink-each requires the source to be a directory: {}

What it means

check_symlink_each validates that an entry with `mode = "symlink-each"` has a directory as its source, since symlink-each links every file inside the source directory individually. When the source is missing or a regular file, it bails; callers surface this in the status row or apply error list.

Solutions

  1. Point `source` at the directory containing the files you want individually linked.
  2. If linking a single file, switch to `mode = "symlink"` or `mode = "copy"`.
  3. Ensure the dotfiles repository is cloned/checked out so the source directory exists.

Example fix

// before
"~/.config/app" = { source = "dotfiles/app.conf", mode = "symlink-each" }
// after
"~/.config/app" = { source = "dotfiles/app", mode = "symlink-each" }
Defensive patterns

Strategy: validation

Validate before calling

let src = shellexpand(source);
if !src.is_dir() {
    return Err(format!("symlink-each source must be an existing directory: {src:?}"));
}

Prevention

When it happens

Trigger: Applying a dotfile entry with `mode = "symlink-each"` whose `source` does not exist or points at a file, e.g. `{ source = "dotfiles/single.conf", mode = "symlink-each" }` or a source directory that was deleted/renamed.

Common situations: Dotfiles repo reorganized so the source folder no longer exists; typo in the source path; intending per-file linking but pointing at a single file; source not yet cloned on a fresh machine.

Related errors


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/e303b82793bf145e. Report an issue: GitHub.

Appendix: source

Thrown at src/system/files.rs:1574

        }
    } else if target.exists() {
        Ok(FileState::Differs("exists but is not a symlink".into()))
    } else {
        Ok(FileState::Missing)
    }
}

fn points_at_same_file(target: &Path, source: &Path) -> bool {
    match (target.canonicalize(), source.canonicalize()) {
        (Ok(a), Ok(b)) => a == b,
        _ => false,
    }
}

fn check_symlink_each(req: &FileRequest) -> Result<FileState> {
    if !req.source.is_dir() {
        // callers add the entry's context (status row / apply error list)
        bail!(
            "mode symlink-each requires the source to be a directory: {}",
            req.source.display_user()
        );
    }
    let stale = stale_links(req)?;
    let stale_reason = || format!("{} stale link(s)", stale.len());
    let files = walk_source_files(req)?;
    // with no files to link the desired state is just the target directory —
    // a blocking non-directory must still surface (and be --force-able)
    if files.is_empty() {
        return if req.target.is_dir() {
            if stale.is_empty() {
                Ok(FileState::Applied)
            } else {
                Ok(FileState::Differs(stale_reason()))
            }
        } else if req.target.exists() || req.target.is_symlink() {
            Ok(FileState::Differs("exists but is not a directory".into()))

View on GitHub (pinned to 533346cc37)