Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jobs:
- uses: actions/checkout@v4
- name: Build
run: cargo build
- name: Test
run: cargo test

test-windows:
runs-on: windows-latest
Expand All @@ -29,3 +31,5 @@ jobs:
- uses: actions/checkout@v4
- name: Build
run: cargo build
- name: Test
run: cargo test
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- Show an actionable hint when `bootit` is installed to a user-local directory (e.g. via `cargo install`), where `sudo bootit` cannot find the binary.

## [0.1.0] - 2026-02-05

### Added
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,34 @@ it windows # No sudo required!
cargo install bootit
```

## Troubleshooting

### `sudo: bootit: command not found` after `cargo install`

`cargo install` puts `bootit` in `~/.cargo/bin`, which is on your user PATH
but not on sudo's `secure_path` (the list of directories sudo searches when
you run `sudo <command>`). As a result, the first command fails and the
second one cannot even find the binary:

```bash
$ bootit scan
Error: This program must be run as root (try: sudo bootit ...)

$ sudo bootit scan
sudo: bootit: command not found
```

Run it with the full path instead:

```bash
sudo "$(which bootit)" scan
```

or link it into a system directory once so plain `sudo bootit` works:

```bash
sudo ln -s "$(which bootit)" /usr/local/bin/bootit
```

## Contributing
Contributions are welcome! Feel free to open issues or submit pull requests on the GitHub repository
82 changes: 80 additions & 2 deletions src/util.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};

use miette::miette;
use which::which;
Expand All @@ -8,8 +8,15 @@ pub fn check_privileges() -> miette::Result<()> {
{
let euid = unsafe { libc::geteuid() };
if euid != 0 {
let hint = sudo_hint();
if hint.is_empty() {
return Err(miette!(
"This program must be run as root (try: sudo bootit ...)"
));
}
return Err(miette!(
"This program must be run as root (try: sudo bootit ...)"
help = hint,
"This program must be run as root (try: sudo bootit ...)",
));
}
}
Expand All @@ -26,6 +33,44 @@ pub fn check_privileges() -> miette::Result<()> {
Ok(())
}

/// Builds a hint for the non-root error message.
///
/// `cargo install` places binaries in `~/.cargo/bin`, which is on the user's
/// PATH but not on sudo's `secure_path`. In that case `sudo bootit ...` fails
/// with "command not found" before the program even runs, so the generic
/// "try: sudo bootit ..." advice in the error message is misleading.
///
/// Returns an empty string when the binary lives in a system directory that
/// sudo can already find, so the error message stays short.
#[cfg(unix)]
fn sudo_hint() -> String {
let exe = std::env::current_exe().ok();
let home = std::env::var_os("HOME").map(PathBuf::from);
match (exe, home) {
(Some(exe), Some(home)) => sudo_hint_for(&exe, &home),
_ => String::new(),
}
}

#[cfg(unix)]
fn sudo_hint_for(exe: &Path, home: &Path) -> String {
let installed_to_user_dir =
exe.starts_with(home) || exe.to_string_lossy().contains(".cargo/bin");
if !installed_to_user_dir {
return String::new();
}

format!(
"bootit is installed in a user-local directory ({}) which sudo does not search, so \
`sudo bootit ...` will fail with \"command not found\".\n\
Run it with the full path:\n sudo {} ...\n\
or link it into a system directory once:\n sudo ln -s {} /usr/local/bin/bootit",
exe.display(),
exe.display(),
exe.display(),
)
}

#[cfg(windows)]
fn is_elevated() -> bool {
use std::ptr::null_mut;
Expand Down Expand Up @@ -67,3 +112,36 @@ pub fn find_it() -> miette::Result<PathBuf> {
Err(miette!("Could not find 'it' in PATH. Please install it."))
}
}

#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::path::Path;

#[test]
fn no_hint_when_installed_in_system_dir() {
let hint = sudo_hint_for(Path::new("/usr/local/bin/bootit"), Path::new("/home/user"));
assert_eq!(hint, "");
}

#[test]
fn hint_for_cargo_install_under_home() {
let hint = sudo_hint_for(
Path::new("/home/user/.cargo/bin/bootit"),
Path::new("/home/user"),
);
assert!(hint.contains("/home/user/.cargo/bin/bootit"));
assert!(hint.contains("sudo /home/user/.cargo/bin/bootit ..."));
assert!(hint.contains("ln -s"));
}

#[test]
fn hint_for_cargo_install_under_other_home() {
// Installed via cargo but for a different user than the one running it.
let hint = sudo_hint_for(
Path::new("/home/other/.cargo/bin/bootit"),
Path::new("/home/user"),
);
assert!(hint.contains("ln -s"));
}
}
Loading