{"record":{"id":"b74875f9a3bbfa42","repo":"jdx/mise","slug":"brew-cask-duplicate-app-target-caskroom-bundle-bundle","errorCode":null,"errorMessage":"brew-cask: duplicate app target '{}' (Caskroom bundle '{bundle}')","messagePattern":"brew-cask: duplicate app target '(.+?)' \\(Caskroom bundle '(.+?)'\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask/mod.rs","lineNumber":355,"sourceCode":"    fn app_target_paths(&self) -> Result<Vec<PathBuf>> {\n        let mut targets = Vec::with_capacity(self.apps.len());\n        let mut bundle_names = BTreeSet::new();\n        for app in &self.apps {\n            let name = app.target_name()?;\n            let target = app_target_path(name)?;\n            let bundle = app_bundle_name(name)?;\n            // Explicit targets can differ in appdir but still overwrite the\n            // same basename in the shared Caskroom staging directory. Reject\n            // Unicode caseless matches conservatively: the appdir and Caskroom\n            // may use different filesystems. Canonical caseless matching needs\n            // NFD before and after case folding (lowercasing misses e.g. ϐ/β).\n            // Normalize only the key, preserving the original install paths.\n            let bundle_key = unicase::UniCase::new(bundle.nfd().collect::<String>())\n                .to_folded_case()\n                .nfd()\n                .collect::<String>();\n            if targets.contains(&target) || !bundle_names.insert(bundle_key) {\n                bail!(\n                    \"brew-cask: duplicate app target '{}' (Caskroom bundle '{bundle}')\",\n                    target.display()\n                );\n            }\n            targets.push(target);\n        }\n        Ok(targets)\n    }\n\n    fn binary_targets(&self) -> Result<Vec<PathBuf>> {\n        let appdir = cask_appdir(&self.apps)?;\n        self.binaries\n            .iter()\n            .map(|binary| binary.target_path(&appdir))\n            .chain(\n                self.command_wrappers\n                    .iter()\n                    .map(CommandWrapperArtifact::target_path),","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/packages/brew/cask/mod.rs#L337-L373","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Remove or correct the duplicate artifact stanza in the cask definition so each artifact has a distinct target path and bundle name.","If two artifacts legitimately point to the same file, keep only one stanza.","Check for case-only differences in bundle names and unify them in the cask definition.","If the cask comes from a tap, verify against upstream or switch to the official tap."],"exampleFix":"// before (cask artifacts)\n[{\"source\":\"MyApp.app\"},{\"source\":\"myapp.app\"}]\n// after\n[{\"source\":\"MyApp.app\"}]","handlingStrategy":"validation","validationCode":"use unicase::UniCase;\nuse std::collections::HashSet;\nfn has_duplicate_bundles(names: &[String]) -> Option<&String> {\n    let mut seen: HashSet<String> = HashSet::new();\n    names.iter().find(|n| {\n        let key = UniCase::new(n.nfd().collect::<String>()).to_folded_case().nfd().collect::<String>();\n        !seen.insert(key)\n    })\n}","typeGuard":null,"tryCatchPattern":"match install_result {\n    Err(e) if e.to_string().contains(\"duplicate app target\") => {\n        eprintln!(\"cask defines duplicate artifacts; dedupe the artifact list before retrying\");\n    }\n    other => other?,\n}","preventionTips":["Dedupe artifact lists in generated cask definitions by normalized bundle name.","Run a cask lint that flags case-only or NFC/NFD duplicate bundle names.","Avoid hand-editing cask JSON; regenerate from upstream.","Test cask definitions on a case-sensitive filesystem too to catch hidden collisions."],"tags":["rust","brew","duplicate","cask-definition","validation"],"backgroundTag":"invalid-config-value","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}