gitbutlerapp/gitbutler · error

CLI source path must be absolute

Error message

CLI source path must be absolute

What it means

validate_cli_source checks the CLI executable path before a symlink is created. It requires an absolute source path; relative paths are rejected because the escalated installer may run with a different working directory, producing a broken link.

Solutions

  1. Pass an absolute source path (e.g. via std::env::current_dir()?.join(...) or std::fs::canonicalize)
  2. Fix scripts/config to resolve the binary location at startup (current_exe or install prefix)
  3. Ensure the path exists and is executable too, since the next checks enforce that

Example fix

// before
install_cli_link(Path::new("target/release/but"), &dest, policy)
// after
let src = std::fs::canonicalize("target/release/but")?;
install_cli_link(&src, &dest, policy)
Defensive patterns

Strategy: validation

Validate before calling

let src = std::fs::canonicalize(source).map_err(|_| anyhow!("source must exist and be absolute"))?;
if !src.is_absolute() { return Err(anyhow!("source must be absolute")); }

Prevention

When it happens

Trigger: install_cli_link called with a relative source path such as 'target/release/but' instead of an absolute path.

Common situations: Hardcoded relative paths in scripts; relying on CWD in tests or CI; computing the source from a relative config value.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/12a9809bafa6a97b. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-action/src/cli.rs:175

    Ok(())
}

/// Map the error to escalate privileges if permission was denied.
#[cfg(any(target_os = "macos", all(test, unix)))]
fn escalate_privilege_error_if_permission_denied(err: anyhow::Error) -> InstallError {
    match err.downcast_ref::<std::io::Error>() {
        Some(io_err) if io_err.kind() == std::io::ErrorKind::PermissionDenied => {
            InstallError::InstallationRequiresElevatedPrivileges(err)
        }
        _ => InstallError::Other(err),
    }
}

#[cfg(any(target_os = "macos", all(test, unix)))]
fn validate_cli_source(source: &std::path::Path) -> anyhow::Result<()> {
    use std::os::unix::fs::PermissionsExt;

    anyhow::ensure!(source.is_absolute(), "CLI source path must be absolute");
    let metadata = std::fs::metadata(source)
        .with_context(|| format!("Cannot access CLI executable at '{}'", source.display()))?;
    anyhow::ensure!(
        metadata.is_file() && metadata.permissions().mode() & 0o111 != 0,
        "CLI source '{}' must be an executable file",
        source.display()
    );
    Ok(())
}

#[cfg(any(target_os = "macos", all(test, unix)))]
fn verify_cli_link(source: &std::path::Path, destination: &std::path::Path) -> anyhow::Result<()> {
    let target = std::fs::read_link(destination).context("Cannot read installed CLI symlink")?;
    anyhow::ensure!(
        target == source,
        "Refusing to replace '{}', which points to '{}' instead of '{}'",
        destination.display(),
        target.display(),

View on GitHub (pinned to 58e5313667)