{"record":{"id":"23d91830a225fc94","repo":"jdx/mise","slug":"brew-cask-appdir-must-prefix-a-binary-target","errorCode":null,"errorMessage":"brew-cask: $APPDIR must prefix a binary target","messagePattern":"brew-cask: \\$APPDIR must prefix a binary target","errorType":"validation","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":6148,"sourceCode":"    if target_name.contains('\\0') {\n        bail!(\"brew-cask: binary target contains NUL\");\n    }\n    if let Some(relative) = target_name.strip_prefix(\"$APPDIR/\") {\n        let relative = Path::new(relative);\n        if relative.components().next().is_none()\n            || relative\n                .components()\n                .any(|component| !matches!(component, Component::Normal(_)))\n        {\n            bail!(\"brew-cask: binary $APPDIR target '{target_name}' must stay below Applications\");\n        }\n        if !allowed_appdir_roots()?.iter().any(|root| root == appdir) {\n            bail!(\"brew-cask: invalid appdir '{}'\", appdir.display());\n        }\n        return Ok(appdir.join(relative));\n    }\n    if target_name.contains(\"$APPDIR\") {\n        bail!(\"brew-cask: $APPDIR must prefix a binary target\");\n    }\n    let prefix = prefix::prefix();\n    let prefix_str = prefix.to_string_lossy();\n    let target_name = target_name.replace(\"$HOMEBREW_PREFIX\", prefix_str.as_ref());\n    let path = PathBuf::from(&target_name);\n    let target = if path.is_absolute() {\n        path\n    } else if target_name.contains('/') {\n        prefix.join(path)\n    } else {\n        prefix.join(\"bin\").join(path)\n    };\n    if target\n        .components()\n        .any(|component| matches!(component, Component::ParentDir))\n    {\n        bail!(\n            \"brew-cask: binary target '{}' must not contain '..'\",","sourceCodeStart":6130,"sourceCodeEnd":6166,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/packages/brew/cask.rs#L6130-L6166","documentation":"The $APPDIR placeholder is only meaningful as a prefix: binary_target_path handles targets starting with \"$APPDIR/\" and rejects any other occurrence of the literal \"$APPDIR\" in a binary target. A placeholder buried mid-path (\"bin/$APPDIR/tool\") has no defined expansion point, so mise refuses it instead of linking to an unintended location.","triggerScenarios":"A cask binary artifact target containing \"$APPDIR\" anywhere other than the start, e.g. \"bin/$APPDIR/tool\" or \"foo$APPDIR\".","commonSituations":"Hand-written or templated casks misusing the placeholder; tap generators interpolating variables into the middle of target strings.","solutions":["Move the placeholder to the front: \"$APPDIR/tool\" — or drop it and use a normal target relative to the brew prefix","For prefix-anchored binaries use \"$HOMEBREW_PREFIX/bin/tool\" or plain \"tool\"","Report the malformed stanza upstream if it comes from a tap"],"exampleFix":"// before (cask binary target)\n\"target\": \"bin/$APPDIR/tool\"\n// after\n\"target\": \"$APPDIR/tool\"","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"fn uses_appdir_prefix_correctly(name: &str) -> bool {\n    match name.find(\"$APPDIR\") {\n        Some(pos) => name[pos..].starts_with(\"$APPDIR/\"),\n        None => true,\n    }\n}","tryCatchPattern":"match binary_target_path(name, &appdir) {\n    Ok(p) => p,\n    Err(e) if e.to_string().contains(\"$APPDIR must prefix\") => {\n        warn!(\"skipping malformed $APPDIR target: {name}\");\n        continue;\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Place $APPDIR only at the very start of a binary target, immediately followed by '/'","Use $HOMEBREW_PREFIX or bare names for prefix-relative binaries"],"tags":["brew-cask","placeholder","path-validation","binary"],"backgroundTag":"invalid-placeholder-syntax","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}