{"record":{"id":"99a1fc333e7ca936","repo":"jdx/mise","slug":"brew-cask-structured-symlink-target-already","errorCode":null,"errorMessage":"brew-cask: structured symlink target '{}' already exists","messagePattern":"brew-cask: structured symlink target '(.+?)' already exists","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":3642,"sourceCode":"            let target = resolve_flight_path_with_context(cask, target, staged_path, appdir)?;\n            let sources = flight_symlink_sources(cask, source, *source_glob, staged_path, appdir)?;\n            if sources.is_empty() {\n                bail!(\n                    \"brew-cask: structured symlink source '{}' did not match any paths\",\n                    source.path\n                );\n            }\n            let target_metadata = target.symlink_metadata().ok();\n            let target_is_real_dir = target_metadata\n                .as_ref()\n                .is_some_and(|metadata| metadata.is_dir());\n            let target_is_dir = target_is_real_dir || sources.len() > 1;\n            if sources.len() > 1 {\n                let created_external_directory =\n                    target_metadata.is_none() && !target.starts_with(staged_path);\n                if target_metadata.is_some() && !target_is_real_dir {\n                    if target.exists() && !force && !targets.previous_symlinks.contains(&target) {\n                        bail!(\n                            \"brew-cask: structured symlink target '{}' already exists\",\n                            target.display()\n                        );\n                    }\n                    targets.protect(&target)?;\n                } else if created_external_directory {\n                    // Record the absent directory itself so rollback removes\n                    // the container after removing the links created below.\n                    if let Some(parent) = target.parent() {\n                        create_flight_dir_all(parent, *sudo)?;\n                    }\n                    targets.protect(&target)?;\n                }\n                create_flight_dir_all(&target, *sudo)?;\n                if created_external_directory || targets.previous_directories.contains(&target) {\n                    targets.record_installed_directory(target.clone());\n                }\n            }","sourceCodeStart":3624,"sourceCodeEnd":3660,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/src/system/packages/brew/cask.rs#L3624-L3660","documentation":"During a multi-source symlink step, the target exists as a non-directory, is a real file (`target.exists()`), the step is not `force: true`, and the path is not recorded in `targets.previous_symlinks` (links this cask's own transaction created and may replace freely). The installer refuses to overwrite a file it does not own, keeping the destructive protect/remove phase reachable only for owned or forced targets.","triggerScenarios":"Reinstall where the previous symlink was never recorded in the transaction; a user-created real file at the target path; another package owning a file at the same target location.","commonSituations":"An earlier install failed mid-transaction so ownership tracking was lost; switching between casks that link the same name; the user ran a manual `ln -s` or copied a file to the target.","solutions":["Set `force: true` on the symlink step when overwriting foreign files is acceptable","Remove or rename the conflicting file at the target path and retry the install","Run the cask's uninstall flow first so receipts and `previous_symlinks` tracking are consistent","Check whether another installed cask links the same target name and uninstall it"],"exampleFix":"# before\nsymlink(source: \"bin/tool\", target: \"#{prefix}/bin/tool\")\n# after - allow replacing an unowned existing file\nsymlink(source: \"bin/tool\", target: \"#{prefix}/bin/tool\", force: true)","handlingStrategy":"validation","validationCode":"// Before a symlink step, detect unowned existing targets it would refuse\nlet target = resolve_flight_path_with_context(cask, &step.target, staged_path, appdir)?;\nlet meta = target.symlink_metadata();\nif let Some(m) = &meta {\n    let is_foreign_dir_source = sources.len() > 1 && !m.is_dir();\n    if (target.exists() || is_foreign_dir_source) && !step.force && !previous_symlinks.contains(&target) {\n        return Err(eyre!(\"target '{}' exists and is not owned by this cask\", target.display()));\n    }\n}","typeGuard":null,"tryCatchPattern":"if err.to_string().contains(\"symlink target\") && err.to_string().contains(\"already exists\") {\n    // offer: remove the conflicting path, or re-run with force semantics\n}","preventionTips":["Use `force: true` for link names the cask legitimately owns","Complete uninstalls before reinstalling so ownership receipts stay accurate","Avoid link names that collide with system or other-package files"],"tags":["brew-cask","symlink","conflict","install-artifact"],"backgroundTag":"symlink-target-already-exists","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}