jdx/mise · error

brew-cask: duplicate app target

Error message

brew-cask: duplicate app target '{}' (Caskroom bundle '{bundle}')

What it means

During brew-cask app artifact target planning, the code collects the set of install targets and the set of normalized Caskroom bundle names. It rejects an install plan where two artifacts resolve to the same target path or where two different artifacts have case/Unicode-normalization-equivalent bundle names, since the second install would clobber or double-map the first on case-insensitive macOS filesystems.

Solutions

  1. Remove or correct the duplicate artifact stanza in the cask definition so each artifact has a distinct target path and bundle name.
  2. If two artifacts legitimately point to the same file, keep only one stanza.
  3. Check for case-only differences in bundle names and unify them in the cask definition.
  4. If the cask comes from a tap, verify against upstream or switch to the official tap.

Example fix

// before (cask artifacts)
[{"source":"MyApp.app"},{"source":"myapp.app"}]
// after
[{"source":"MyApp.app"}]
Defensive patterns

Strategy: validation

Validate before calling

use unicase::UniCase;
use std::collections::HashSet;
fn has_duplicate_bundles(names: &[String]) -> Option<&String> {
    let mut seen: HashSet<String> = HashSet::new();
    names.iter().find(|n| {
        let key = UniCase::new(n.nfd().collect::<String>()).to_folded_case().nfd().collect::<String>();
        !seen.insert(key)
    })
}

Try / catch

match install_result {
    Err(e) if e.to_string().contains("duplicate app target") => {
        eprintln!("cask defines duplicate artifacts; dedupe the artifact list before retrying");
    }
    other => other?,
}

Prevention

When it happens

Trigger: A cask defines two app (or app-like) artifacts whose install targets are identical, or whose bundle basenames differ only in case or Unicode normalization (e.g. 'MyApp.app' vs 'myapp.app'). Detected via parse/expand of nested-bundle artifacts.

Common situations: Hand-edited cask JSON duplicating an app stanza; a tap with a typo'd duplicate artifact; nested-archive sources that both expand to the same bundle basename; NFD/NFC filename discrepancies from extractors.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at src/system/packages/brew/cask/mod.rs:355

    fn app_target_paths(&self) -> Result<Vec<PathBuf>> {
        let mut targets = Vec::with_capacity(self.apps.len());
        let mut bundle_names = BTreeSet::new();
        for app in &self.apps {
            let name = app.target_name()?;
            let target = app_target_path(name)?;
            let bundle = app_bundle_name(name)?;
            // Explicit targets can differ in appdir but still overwrite the
            // same basename in the shared Caskroom staging directory. Reject
            // Unicode caseless matches conservatively: the appdir and Caskroom
            // may use different filesystems. Canonical caseless matching needs
            // NFD before and after case folding (lowercasing misses e.g. ϐ/β).
            // Normalize only the key, preserving the original install paths.
            let bundle_key = unicase::UniCase::new(bundle.nfd().collect::<String>())
                .to_folded_case()
                .nfd()
                .collect::<String>();
            if targets.contains(&target) || !bundle_names.insert(bundle_key) {
                bail!(
                    "brew-cask: duplicate app target '{}' (Caskroom bundle '{bundle}')",
                    target.display()
                );
            }
            targets.push(target);
        }
        Ok(targets)
    }

    fn binary_targets(&self) -> Result<Vec<PathBuf>> {
        let appdir = cask_appdir(&self.apps)?;
        self.binaries
            .iter()
            .map(|binary| binary.target_path(&appdir))
            .chain(
                self.command_wrappers
                    .iter()
                    .map(CommandWrapperArtifact::target_path),

View on GitHub (pinned to 533346cc37)