{"record":{"id":"12a9809bafa6a97b","repo":"gitbutlerapp/gitbutler","slug":"cli-source-path-must-be-absolute","errorCode":null,"errorMessage":"CLI source path must be absolute","messagePattern":"CLI source path must be absolute","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-action/src/cli.rs","lineNumber":175,"sourceCode":"    Ok(())\n}\n\n/// Map the error to escalate privileges if permission was denied.\n#[cfg(any(target_os = \"macos\", all(test, unix)))]\nfn escalate_privilege_error_if_permission_denied(err: anyhow::Error) -> InstallError {\n    match err.downcast_ref::<std::io::Error>() {\n        Some(io_err) if io_err.kind() == std::io::ErrorKind::PermissionDenied => {\n            InstallError::InstallationRequiresElevatedPrivileges(err)\n        }\n        _ => InstallError::Other(err),\n    }\n}\n\n#[cfg(any(target_os = \"macos\", all(test, unix)))]\nfn validate_cli_source(source: &std::path::Path) -> anyhow::Result<()> {\n    use std::os::unix::fs::PermissionsExt;\n\n    anyhow::ensure!(source.is_absolute(), \"CLI source path must be absolute\");\n    let metadata = std::fs::metadata(source)\n        .with_context(|| format!(\"Cannot access CLI executable at '{}'\", source.display()))?;\n    anyhow::ensure!(\n        metadata.is_file() && metadata.permissions().mode() & 0o111 != 0,\n        \"CLI source '{}' must be an executable file\",\n        source.display()\n    );\n    Ok(())\n}\n\n#[cfg(any(target_os = \"macos\", all(test, unix)))]\nfn verify_cli_link(source: &std::path::Path, destination: &std::path::Path) -> anyhow::Result<()> {\n    let target = std::fs::read_link(destination).context(\"Cannot read installed CLI symlink\")?;\n    anyhow::ensure!(\n        target == source,\n        \"Refusing to replace '{}', which points to '{}' instead of '{}'\",\n        destination.display(),\n        target.display(),","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-action/src/cli.rs#L157-L193","documentation":"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.","triggerScenarios":"install_cli_link called with a relative source path such as 'target/release/but' instead of an absolute path.","commonSituations":"Hardcoded relative paths in scripts; relying on CWD in tests or CI; computing the source from a relative config value.","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"],"exampleFix":"// before\ninstall_cli_link(Path::new(\"target/release/but\"), &dest, policy)\n// after\nlet src = std::fs::canonicalize(\"target/release/but\")?;\ninstall_cli_link(&src, &dest, policy)","handlingStrategy":"validation","validationCode":"let src = std::fs::canonicalize(source).map_err(|_| anyhow!(\"source must exist and be absolute\"))?;\nif !src.is_absolute() { return Err(anyhow!(\"source must be absolute\")); }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["cli","install","path","validation"],"backgroundTag":"invalid-argument-value","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}