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
42 changes: 34 additions & 8 deletions apps/native/src-tauri/src/evolve/search_packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub enum SearchResultInstallTarget {
System,
// Package is not available on the host platform (e.g. no Darwin support, etc.)
UnavailableOnHostPlatform,
// Don't try to install -- package is broken etc.
None,
}

#[derive(Debug, serde::Serialize, Clone, PartialEq, Eq)]
Expand All @@ -30,6 +32,7 @@ pub struct SearchPackageResult {
pub version: String,
pub description: String,
pub install_via: SearchResultInstallTarget,
pub additional_info: Option<String>,
}

/// Search a single channel and return a list of SearchPackageResult
Expand Down Expand Up @@ -89,10 +92,11 @@ fn process_search_output(
if let Some(value) = parsed.as_object() {
for (attr_path, pkg) in value {
let name = attr_path.split('.').last().unwrap_or(attr_path).to_string();
let package_type = if let Some(classifier) = package_classifier {
classifier(&name)
let (package_type, additional_info) = if let Some(classifier) = package_classifier {
(classifier(&name), None)
} else {
classify_package(channel, attr_path)
let (pkg_type, info) = classify_package(channel, attr_path);
(pkg_type, info)
};

// If the package is unavailable on the host platform, skip it.
Expand All @@ -115,6 +119,7 @@ fn process_search_output(
.and_then(|v| v.as_str())
.unwrap_or("No description")
.to_string(),
additional_info,
});
}
}
Expand Down Expand Up @@ -240,7 +245,7 @@ fn classify_derivation(drv: &str) -> SearchResultInstallTarget {

/// Heuristically classify whether a nix package behaves like a GUI app
/// (Homebrew Cask-like) or a CLI / nix-native package.
fn classify_package(channel: &str, attr_path: &str) -> SearchResultInstallTarget {
fn classify_package(channel: &str, attr_path: &str) -> (SearchResultInstallTarget, Option<String>) {
let mut cmd = Command::new("nix");
cmd.args(&[
"derivation",
Expand All @@ -252,7 +257,7 @@ fn classify_package(channel: &str, attr_path: &str) -> SearchResultInstallTarget
Ok(output) => output,
Err(e) => {
log::error!("Failed to execute nix derivation show: {}", e);
return SearchResultInstallTarget::Either;
return (SearchResultInstallTarget::Either, None);
}
};

Expand All @@ -262,7 +267,23 @@ fn classify_package(channel: &str, attr_path: &str) -> SearchResultInstallTarget
// If the error message contains "not available on the requested hostPlatform",
// set to "unavailable".
if stderr.contains("not available on the requested hostPlatform") {
return SearchResultInstallTarget::UnavailableOnHostPlatform;
return (SearchResultInstallTarget::UnavailableOnHostPlatform, None);
}

// If the package is broken, set to "none" to avoid trying to install it at all.
if stderr.contains("broken: This package is broken.") {
return (SearchResultInstallTarget::None, Some("package is broken".to_string()));
}

// If this error occurs because the package is "unfree" and allowUnfree
// is not enabled, we can't do a type determination but the package
// is technically installable via nix if the user enables allowUnfree,
// so we return "either".
// CONSIDER: We may do something additional in the future like offer
// to enable allowUnfree for the user or something like that, but for now
// we'll leave things up to the agent.
if stderr.contains("Refusing to evaluate package") && stderr.contains("because it has an unfree license") {
return (SearchResultInstallTarget::Either, Some("needs allowUnfree enabled".to_string()));
}

// Else assume the error is on our side and let the agent decide what to
Expand All @@ -273,12 +294,12 @@ fn classify_package(channel: &str, attr_path: &str) -> SearchResultInstallTarget
output.status.code(),
truncated
);
return SearchResultInstallTarget::Either;
return (SearchResultInstallTarget::Either, Some(truncated));
}

let drv = String::from_utf8_lossy(&output.stdout);

classify_derivation(&drv)
(classify_derivation(&drv), None)
}

#[cfg(test)]
Expand Down Expand Up @@ -329,13 +350,15 @@ mod tests {
version: "30.2".to_string(),
description: "Extensible, customizable GNU text editor".to_string(),
install_via: SearchResultInstallTarget::Either,
additional_info: None,
})), ("emacs-fulltext", 55, Some(SearchPackageResult{
name: "auctex".to_string(),
attr_path: "legacyPackages.aarch64-darwin.auctex".to_string(),
channel: "test-channel".to_string(),
version: "13.2".to_string(),
description: "Extensible package for writing and formatting TeX files in GNU Emacs and XEmacs".to_string(),
install_via: SearchResultInstallTarget::Either,
additional_info: None,
})), ("empty", 0, None )];
let fake_package_classifier = |_package_name: &str| SearchResultInstallTarget::Either;

Expand Down Expand Up @@ -372,6 +395,7 @@ mod tests {
version: "30.2".to_string(),
description: "Extensible, customizable GNU text editor".to_string(),
install_via: SearchResultInstallTarget::Either,
additional_info: None,
},
SearchPackageResult {
name: "auctex".to_string(),
Expand All @@ -382,6 +406,7 @@ mod tests {
"Extensible package for writing and formatting TeX files in GNU Emacs and XEmacs"
.to_string(),
install_via: SearchResultInstallTarget::Either,
additional_info: None,
},
];
let channel2_results = vec![SearchPackageResult {
Expand All @@ -391,6 +416,7 @@ mod tests {
version: "30.2".to_string(),
description: "Extensible, customizable GNU text editor".to_string(),
install_via: SearchResultInstallTarget::Either,
additional_info: None,
}];

process_channel_results(&mut structured, channel1_results, 10).unwrap();
Expand Down
9 changes: 5 additions & 4 deletions apps/native/src-tauri/src/evolve/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,14 @@ IMPORTANT: The generated Nix code is syntax-validated before writing. Edits with
description: "Search for Nix packages by name or description. This is a convenient \
wrapper around 'nix search' that returns a list of structured JSON results. \
Output format: Array of SearchPackageResult objects, each containing \
{\"name\": string, \"attr_path\": string, \"version\": string, \"description\": string, \"channel\": string, \"installTarget\": SearchResultInstallTarget}. \
{\"name\": string, \"attrPath\": string, \"version\": string, \"description\": string, \"channel\": string, \"installTarget\": SearchResultInstallTarget, \"additionalInfo\": string?}. \
The installTarget field indicates whether a package should be installed via Homebrew (Homebrew), \
Nix (System), or either (Either). IMPORTANT: When making nix package edits later, try to respect \
Nix (System), either (Either), or not at all (None or UnavailableOnHostPlatform). \
The additionalInfo field is an optional string containing any other relevant clues for how to install the package. IMPORTANT: When making nix package edits later, try to respect \
1) any expressed user preference, followed by 2) the installTarget value to guide installation method recommendations. \
Example: [{\"name\": \"wget\", \"attr_path\": \"wget\", \"version\": \"1.21.3\", \"description\": \"retrieves files from the web\", \"channel\": \"nixpkgs-unstable\", \"installTarget\": \"System\"}]. \
Example: [{\"name\": \"wget\", \"attrPath\": \"wget\", \"version\": \"1.21.3\", \"description\": \"retrieves files from the web\", \"channel\": \"nixpkgs-unstable\", \"installTarget\": \"System\"}]. \
Parameters: \
use_regex enables regex patterns for advanced matching; \
useRegex enables regex patterns for advanced matching; \
channels lets you search in different flakes (one or more of nixpkgs, nixpkgs-unstable, etc.), max 5".to_string(),
parameters: serde_json::json!({
"type": "object",
Expand Down
Loading