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
- Pass an absolute source path (e.g. via std::env::current_dir()?.join(...) or std::fs::canonicalize)
- Fix scripts/config to resolve the binary location at startup (current_exe or install prefix)
- 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
- Resolve source via current_exe()/canonicalize before install
- Avoid relative paths in scripts/CI
- Pin the binary location in config as absolute
- Validate in tests that source paths are absolute
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
- CLI destination must be absolute
- CLI source ' ' must be an executable file
- Aborting due to empty branch name
- Aborting due to empty
- Automatic CLI installation is not supported on Windows. To…
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)