Skip to content
Open
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: 0 additions & 4 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
name: Package and Publish

on:
push:
branches:
- master
- release/*
workflow_dispatch:

permissions:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "needs"
description = "Check if given bin(s) are available in the PATH"
version = "0.6.0"
version = "0.7.0"
edition = "2024"
authors = ["Noah <noahbuergler@proton.me>"]
license = "GPL-3.0-or-later"
Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ release_build := "./target/release/needs"

@_default:
just --list
needs gum freeze hr
needs gum freeze hr agg

@gif:
agg demo.cast --font-family "JetBrainsMono Nerd Font Mono" --speed 2 demo.gif
Expand Down
29 changes: 22 additions & 7 deletions src/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,23 @@ pub struct Binary<'a> {
pub name: Cow<'a, str>,
// TODO: use a custom version type
pub version: Option<SemVersion>,
pub package_manager: Option<String>,
}

impl<'a> Binary<'a> {
pub fn new(name: Cow<'a, str>) -> Self {
Self {
name,
version: None,
package_manager: None,
}
}

pub fn new_with_package_manager(name: Cow<'a, str>, package_manager: Option<String>) -> Self {
Self {
name,
version: None,
package_manager,
}
}
}
Expand All @@ -25,21 +35,26 @@ impl Default for Binary<'_> {
Self {
name: Cow::borrowed(""),
version: Some(unknown_version()),
package_manager: None,
}
}
}

impl Display for Binary<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.version.is_none() {
write!(f, "{} ?", self.name)
if let Some(ref pm) = self.package_manager {
write!(f, "{} ? ({})", self.name, pm)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont use () but just prefix the name with "via"

} else {
write!(f, "{} ?", self.name)
}
} else {
write!(
f,
"{} {}",
self.name,
format_version(self.version.as_ref().unwrap(), false)
)
let version_str = format_version(self.version.as_ref().unwrap(), false);
if let Some(ref pm) = self.package_manager {
write!(f, "{} {} ({})", self.name, version_str, pm)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont use () but just prefix the name with "via"

} else {
write!(f, "{} {}", self.name, version_str)
}
}
}
}
Expand Down
150 changes: 147 additions & 3 deletions src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,97 @@ use crate::binary::Binary;
use crate::error::DiscoveryError;
use log::{info, warn};
use miette::Result;
use std::path::Path;

/// Detect which package manager is responsible for managing a binary based on its path
fn detect_package_manager(binary_path: &Path) -> Option<String> {
let path_str = binary_path.to_string_lossy();
let path_str_lower = path_str.to_lowercase();

// Check if it's a cargo binary specifically
if path_str_lower.contains("/.cargo/bin/") {
return Some("cargo".to_string());
}
// Check if it's other rustup toolchain binaries
if path_str_lower.contains("/.rustup/") {
return Some("rustup".to_string());
}

// Check for Homebrew (macOS/Linux)
if path_str_lower.starts_with("/opt/homebrew/")
|| path_str_lower.starts_with("/usr/local/cellar/")
|| (path_str_lower.contains("/usr/local/") && path_str_lower.contains("homebrew"))
{
return Some("homebrew".to_string());
}

// Check for npm global installs
if path_str_lower.contains("/node_modules/.bin/")
|| path_str_lower.contains("/npm/")
|| path_str_lower.contains("/.npm/")
|| path_str_lower.contains("/npm-global/")
{
return Some("npm".to_string());
}

// Check for Go binaries
if path_str_lower.contains("/go/bin/") || path_str_lower.contains("/gopath/bin/") {
return Some("go".to_string());
}

// Check for Python pip/pipx installs
if path_str_lower.contains("/.local/bin/")
|| path_str_lower.contains("/python")
|| path_str_lower.contains("/pip/")
|| path_str_lower.contains("/pipx/")
{
return Some("pip".to_string());
}

// DEV: adding ? to these because i havnt tested them yet
// Check for snap packages
if path_str_lower.contains("/snap/") {
return Some("snap?".to_string());
}

// Check for flatpak
if path_str_lower.contains("/flatpak/") {
return Some("flatpak?".to_string());
}

// Check for AppImage
if path_str_lower.contains("/appimage/") || path_str_lower.ends_with(".appimage") {
return Some("appimage?".to_string());
}

// check for bun
if path_str_lower.contains("/bun/") || path_str_lower.contains("/.bun/") {
return Some("bun".to_string());
}

// check for deno
if path_str_lower.contains("/deno/") || path_str_lower.contains("/.deno/") {
return Some("deno".to_string());
}

// check for yarn
if path_str_lower.contains("/yarn/") || path_str_lower.contains("/.yarn/") {
return Some("yarn".to_string());
}

// Check for system package managers (dpkg/apt on Debian/Ubuntu, etc.)
// This should be last as it's the most generic
// if path_str_lower.starts_with("/usr/bin/")
// || path_str_lower.starts_with("/bin/")
// || path_str_lower.starts_with("/usr/local/bin/")
// || path_str_lower.starts_with("/sbin/")
// || path_str_lower.starts_with("/usr/sbin/")
// {
// return Some("system".to_string());
// }

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add more managers, like uv, bun, yarn and pnpm

None
}

pub fn partition_binaries(
binaries_to_check: Vec<Binary<'_>>,
Expand All @@ -16,9 +107,11 @@ pub fn partition_binaries(
for binary in binaries_to_check {
let name = binary.name.as_ref();
match which::which(name) {
Ok(_) => {
Ok(path) => {
info!(SCOPE = "which", bin = name; "found");
available.push(binary);
let package_manager = detect_package_manager(&path);
let updated_binary = Binary::new_with_package_manager(binary.name, package_manager);
available.push(updated_binary);
}
Err(err) => {
info!(SCOPE = "which", bin = name; "not found");
Expand All @@ -30,7 +123,7 @@ pub fn partition_binaries(
return Err(
DiscoveryError::BinaryCheck {
name: name.to_string(),
source: std::io::Error::new(std::io::ErrorKind::Other, err),
source: std::io::Error::other(err),
}
.into(),
);
Expand All @@ -44,9 +137,57 @@ pub fn partition_binaries(
#[cfg(test)]
mod tests {
use beef::Cow;
use std::path::Path;

use super::*;

#[test]
fn test_detect_package_manager() {
// Test rustup/cargo detection
assert_eq!(
detect_package_manager(Path::new("/home/user/.cargo/bin/cargo")),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here...

Some("cargo".to_string())
);
assert_eq!(
detect_package_manager(Path::new("/home/user/.rustup/toolchains/stable/bin/rustc")),
Some("rustup".to_string())
);

// Test system package detection
assert_eq!(detect_package_manager(Path::new("/usr/bin/grep")), None);
assert_eq!(detect_package_manager(Path::new("/bin/ls")), None);

// Test homebrew detection
assert_eq!(
detect_package_manager(Path::new("/opt/homebrew/bin/brew")),
Some("homebrew".to_string())
);

// Test npm detection
assert_eq!(
detect_package_manager(Path::new("/usr/local/lib/node_modules/.bin/npm")),
Some("npm".to_string())
);

// Test go detection
assert_eq!(
detect_package_manager(Path::new("/home/user/go/bin/gofmt")),
Some("go".to_string())
);

// Test pip detection
assert_eq!(
detect_package_manager(Path::new("/home/user/.local/bin/pip")),
Some("pip".to_string())
);

// Test unknown path
assert_eq!(
detect_package_manager(Path::new("/some/unknown/path/binary")),
None
);
}

#[test]
fn test_partition_binaries() {
let cargo_exists = which::which("cargo").is_ok();
Expand All @@ -69,6 +210,9 @@ mod tests {
if cargo_exists {
assert_eq!(available.len(), 1);
assert_eq!(available[0].name, "cargo");
// Check that package manager was detected
assert!(available[0].package_manager.is_some());
assert_eq!(available[0].package_manager.as_ref().unwrap(), "cargo");
assert_eq!(not_available.len(), 1);
assert_eq!(
not_available[0].name,
Expand Down
30 changes: 26 additions & 4 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,28 @@ pub fn print_center_aligned(
let padding_needed = max_len.saturating_sub(bin.name.len());
let padding = " ".repeat(padding_needed);
let version_display = if always_found {
"found".to_string()
if let Some(ref pm) = bin.package_manager {
format!("found {}", format!("via {}", pm).dimmed())
} else {
"found".to_string()
}
} else {
match bin.version {
Some(ref version) => format!("{}", format_version(version, full_versions)),
None => "?".to_string(),
Some(ref version) => {
let version_str = format!("{}", format_version(version, full_versions));
if let Some(ref pm) = bin.package_manager {
format!("{} {}", version_str, format!("via {}", pm).dimmed())
} else {
version_str
}
}
None => {
if let Some(ref pm) = bin.package_manager {
format!("? {}", format!("via {}", pm).dimmed())
} else {
"?".to_string()
}
}
}
};
println!("{}{} {}", padding, bin.name.green(), version_display);
Expand All @@ -32,7 +49,12 @@ pub fn print_center_aligned(binaries: Vec<Binary>, max_len: usize) -> Result<()>
for bin in &binaries {
let padding_needed = max_len.saturating_sub(bin.name.len());
let padding = " ".repeat(padding_needed);
println!("{}{} found", padding, bin.name.green());
let display_text = if let Some(ref pm) = bin.package_manager {
format!("found {}", format!("via {}", pm).dimmed())
} else {
"found".to_string()
};
println!("{}{} {}", padding, bin.name.green(), display_text);
}
Ok(())
}
4 changes: 3 additions & 1 deletion src/versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ pub fn execute_binary<'a>(binary_name: &str) -> Result<Cow<'a, str>> {
return Err(
VersionError::Execution {
name: binary_name.to_string(),
source: std::io::Error::new(std::io::ErrorKind::Other, e),
source: std::io::Error::other(e),
}
.into(),
);
Expand Down Expand Up @@ -317,6 +317,7 @@ pub fn get_versions_for_bins(binaries: Vec<Binary>) -> Vec<Binary> {
return Binary {
name: binary.name,
version: None,
package_manager: binary.package_manager,
};
}

Expand All @@ -332,6 +333,7 @@ pub fn get_versions_for_bins(binaries: Vec<Binary>) -> Vec<Binary> {
Binary {
name: binary.name,
version,
package_manager: binary.package_manager,
}
})
.collect()
Expand Down