jdx/mise · error
brew-cask: structured copy source must resolve to exactly…
Error message
brew-cask: structured copy source must resolve to exactly one path
What it means
A structured `copy` artifact step requires its source to resolve to exactly one path. The step resolves sources through `flight_symlink_sources` (honoring `source_glob` and appdir context), then destructures with `let [source] = sources`; zero matches or multiple matches both fail this check and abort the cask install before any file is copied.
Solutions
- Pin the glob to the exact file name so it matches exactly one path
- Split into one copy step per file when several files must be copied
- If the glob matched zero, inspect the staged payload and re-stage (fresh download) to rule out partial extraction
- Guard genuinely optional files with `if_exists` so the step skips instead of failing
Example fix
# before
copy(source: "payload/**/*", target: "#{staged}/out", source_glob: true)
# after - exactly one concrete source per step
copy(source: "payload/MyApp.app", target: "#{staged}/out/MyApp.app") Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight: a copy step must resolve to exactly one source
let sources = flight_symlink_sources(cask, &step.source, step.source_glob, staged_path, appdir)?;
match sources.as_slice() {
[single] => Ok(single.clone()),
[] => Err(eyre!("copy source '{}' matched nothing; check payload layout", step.source.path)),
many => Err(eyre!("copy source '{}' matched {} paths; split into per-file steps", step.source.path, many.len())),
} Try / catch
if err.to_string().contains("copy source must resolve to exactly one path") {
// report the staged directory listing next to the error to speed up diagnosis
} Prevention
- Prefer concrete single-file sources over globs in copy steps
- Test artifact stanzas against a freshly staged payload after every upstream release
- Guard optional copies with `if_exists` so empty matches skip instead of fail
When it happens
Trigger: `copy` with `source_glob: true` where the staged glob matches zero or 2+ paths; a source string whose template expansion resolves to several staged locations. All `guards` must have matched first, so this fires on the normal path of a mis-scoped glob.
Common situations: Upstream payload gains or loses a file so a previously exact glob becomes ambiguous or empty; overly broad patterns like `**/*.app`; version-stamped file names changing between releases.
Related errors
- brew-cask: structured copy source
- brew-cask: structured copy target
- brew-cask: structured directory copy requires recursive=true
- brew-cask: structured move with multiple sources requires a…
- brew-cask: structured symlink source
AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22).
Data as JSON: /api/errors/70653fe9062f4476.
Report an issue: GitHub.
Appendix: source
Thrown at src/system/packages/brew/cask.rs:3563
source,
target,
recursive,
overwrite,
source_glob,
guards,
} => {
if !guards
.iter()
.map(|guard| flight_guard_matches(cask, guard, staged_path, appdir))
.collect::<Result<Vec<_>>>()?
.into_iter()
.all(|matches| matches)
{
return Ok(());
}
let sources = flight_symlink_sources(cask, source, *source_glob, staged_path, appdir)?;
let [source] = sources.as_slice() else {
bail!("brew-cask: structured copy source must resolve to exactly one path");
};
if !source.exists() {
bail!(
"brew-cask: structured copy source '{}' was not found",
source.display()
);
}
if source.is_dir() && !recursive {
bail!("brew-cask: structured directory copy requires recursive=true");
}
let target = resolve_flight_path_with_context(cask, target, staged_path, appdir)?;
let external = !target.starts_with(staged_path);
let target_metadata = target.symlink_metadata().ok();
if target_metadata.is_some() {
if !overwrite {
bail!(
"brew-cask: structured copy target '{}' already exists",
target.display()View on GitHub (pinned to 6f52dcdf99)