jdx/mise · error

brew-cask: generic artifact target

Error message

brew-cask: generic artifact target '{}' must stay below {}

What it means

generic_artifact_target_path parses a cask artifact's target string after substituting $HOMEBREW_PREFIX with the active prefix. The result must be absolute, start with the prefix, contain at least one component below it, contain no ParentDir ('..') component, and pass path_starts_with_resolved_root so symlinked segments cannot route the real destination outside the prefix. Violations abort before anything is staged or copied.

Solutions

  1. Use an absolute target strictly below the active prefix, e.g. $HOMEBREW_PREFIX/etc/foo.conf
  2. Use the literal $HOMEBREW_PREFIX spelling for substitution — ${HOMEBREW_PREFIX} is not expanded
  3. Strip any '..' segments and ensure at least one component follows the prefix
  4. Verify the prefix mise resolves (HOMEBREW_PREFIX, mise settings) matches the cask's assumption

Example fix

# before
artifact "lib/libfoo.dylib", target: "/usr/local/lib/libfoo.dylib"

# after
artifact "lib/libfoo.dylib", target: "$HOMEBREW_PREFIX/lib/libfoo.dylib"
Defensive patterns

Strategy: validation

Validate before calling

// Validate cask artifact target strings before running the install.
fn valid_generic_target(target: &str, prefix: &Path) -> bool {
    let expanded = target.replace("$HOMEBREW_PREFIX", &prefix.to_string_lossy());
    let p = Path::new(&expanded);
    p.is_absolute()
        && p.starts_with(prefix)
        && p.strip_prefix(prefix).map(|r| r.components().next().is_some()).unwrap_or(false)
        && !p.components().any(|c| matches!(c, std::path::Component::ParentDir))
}

Type guard

fn is_expanded_absolute_under_prefix(target: &str, prefix: &Path) -> bool {
    valid_generic_target(target, prefix)
}

Prevention

When it happens

Trigger: artifact target: "/etc/foo" or "~/Library/x" (not under the prefix); "$HOMEBREW_PREFIX" alone; "$HOMEBREW_PREFIX/opt/../etc/x"; a target under a symlink that resolves outside the prefix; unexpanded ${HOMEBREW_PREFIX} spelling (only the bare $HOMEBREW_PREFIX form is replaced).

Common situations: Casks ported from Homebrew ruby that install to system paths; hardcoded /usr/local targets on arm64 machines whose prefix is /opt/homebrew; templating that emits relative paths or unexpanded variables.

Related errors


AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22). Data as JSON: /api/errors/a0117444eb7ace2f. Report an issue: GitHub.

Appendix: source

Thrown at src/system/packages/brew/cask.rs:2501

        run_installer_artifact(stage, installer, targets.copied_files())?;
        completed(index)?;
    }
    durabilize_staged_symlink_targets(stage, temporary_caskroom, targets)
}

fn generic_artifact_target_path(target: &str) -> Result<PathBuf> {
    let prefix = prefix::prefix();
    let expanded = target.replace("$HOMEBREW_PREFIX", &prefix.to_string_lossy());
    let target = PathBuf::from(expanded);
    if !target.is_absolute()
        || !target.starts_with(&prefix)
        || target.strip_prefix(&prefix)?.components().next().is_none()
        || target
            .components()
            .any(|component| matches!(component, Component::ParentDir))
        || !path_starts_with_resolved_root(&target, &prefix)
    {
        bail!(
            "brew-cask: generic artifact target '{}' must stay below {}",
            target.display(),
            prefix.display()
        );
    }
    Ok(target)
}

fn generic_artifact_targets(artifacts: &CaskArtifacts) -> Result<Vec<PathBuf>> {
    artifacts
        .generic
        .iter()
        .map(|artifact| generic_artifact_target_path(&artifact.target))
        .collect()
}

fn previous_generic_targets(cask: &Cask) -> Result<Vec<CaskTargetRecord>> {
    let Some(version) = installed_version(&cask.token) else {

View on GitHub (pinned to 6f52dcdf99)