diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..80543ff01 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,50 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'pkgx'", + "cargo": { + "args": ["build", "--bin=pkgx", "--package=pkgx"], + "filter": { + "name": "pkgx", + "kind": "bin" + } + }, + "args": ["+git", "--json=v2"], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'pkgx'", + "cargo": { + "args": ["test", "--no-run", "--bin=pkgx", "--package=pkgx"], + "filter": { + "name": "pkgx", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'libpkgx'", + "cargo": { + "args": ["test", "--no-run", "--lib", "--package=libpkgx"], + "filter": { + "name": "libpkgx", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..550daddd0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "rust-analyzer.check.command": "clippy", + "rust-analyzer.rustfmt.rangeFormatting.enable": true, + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer", + "editor.formatOnSave": true + } +} diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 3b0ebae20..a740746e1 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -1,5 +1,6 @@ use console::style; +#[derive(PartialEq)] pub enum Mode { X, Help, @@ -114,9 +115,15 @@ pub fn parse() -> Args { } } } else { - find_program = !arg.contains('/'); - collecting_args = true; - args.push(arg); + // Only start collecting args if not in query mode, or if we're already collecting + if mode == Mode::Query && !collecting_args { + // In query mode, continue processing flags until we hit a non-flag argument + args.push(arg); + } else { + find_program = !arg.contains('/'); + collecting_args = true; + args.push(arg); + } } } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a8cea4a91..504430e78 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -42,8 +42,8 @@ async fn main() -> Result<(), Box> { Ok(()) } args::Mode::Query => { - let (conn, _, _, _) = setup(&flags).await?; - query::query(&args, flags.silent, &conn) + let (conn, _, config, _) = setup(&flags).await?; + query::query(&args, flags.silent, &conn, flags.json, &config).await } args::Mode::X => { let (mut conn, did_sync, config, mut spinner) = setup(&flags).await?; diff --git a/crates/cli/src/query.rs b/crates/cli/src/query.rs index 93133785f..4560dbce0 100644 --- a/crates/cli/src/query.rs +++ b/crates/cli/src/query.rs @@ -1,32 +1,197 @@ use std::error::Error; -use libpkgx::pantry_db; +use libpkgx::{config::Config, inventory, pantry_db}; use rusqlite::{params, Connection}; +use serde::Serialize; + +use crate::resolve::{parse_pkgspec, Pkgspec}; + +#[derive(Serialize, Clone)] +struct QueryResult { + project: String, + programs: Vec, +} + +fn resolve_projects_for_pkgspec( + pkgspec: &mut Pkgspec, + conn: &Connection, +) -> Result, Box> { + match pkgspec { + Pkgspec::Req(pkgreq) => { + // Check if this looks like a program name (no dots and wildcard constraint) + if !pkgreq.project.contains('.') && pkgreq.constraint.raw == "*" { + // Handle as program lookup + Ok(pantry_db::which(&pkgreq.project, conn)?) + } else { + // Handle as package spec - resolve project name and return single project + let (project, _) = resolve_project_name(&pkgreq.project, conn)?; + pkgreq.project = project.clone(); + Ok(vec![project]) + } + } + Pkgspec::Latest(program_or_project) => { + let (project, _) = resolve_project_name(program_or_project, conn)?; + Ok(vec![project]) + } + } +} + +fn resolve_project_name( + input: &str, + conn: &Connection, +) -> Result<(String, String), Box> { + let original = input.to_string(); + + // First, try to resolve as a program name + let projects = pantry_db::which(&input.to_string(), conn)?; + match projects.len() { + 0 => { + // If not found as a program and contains a dot, check if it exists as a project + if input.contains('.') { + let mut stmt = conn.prepare("SELECT COUNT(*) FROM provides WHERE project = ?")?; + let count: i64 = stmt.query_row(params![input], |row| row.get(0))?; + if count > 0 { + return Ok((input.to_string(), original)); + } + } + Err(format!("Package '{}' not found", original).into()) + } + 1 => Ok((projects[0].clone(), original)), + _ => Err(format!( + "Package '{}' is ambiguous: {}", + original, + projects.join(", ") + ) + .into()), + } +} + +fn get_programs_for_project( + project: &str, + conn: &Connection, +) -> Result, Box> { + let mut stmt = + conn.prepare("SELECT program FROM provides WHERE project = ? ORDER BY program")?; + let mut rows = stmt.query(params![project])?; + let mut programs = Vec::new(); + while let Some(row) = rows.next()? { + programs.push(row.get(0)?); + } + Ok(programs) +} + +async fn process_query_arg( + arg: &str, + conn: &Connection, + config: &Config, +) -> Result, Box> { + let mut pkgspec = parse_pkgspec(arg)?; + let projects = resolve_projects_for_pkgspec(&mut pkgspec, conn)?; + + if projects.is_empty() { + let name = match &pkgspec { + Pkgspec::Req(req) => &req.project, + Pkgspec::Latest(project) => project, + }; + return Err(format!("{} not found", name).into()); + } + + let mut results = Vec::new(); + + // Determine which projects to process + let projects_to_process = match &pkgspec { + Pkgspec::Req(pkgreq) if !pkgreq.project.contains('.') && pkgreq.constraint.raw == "*" => { + // For program lookups (no dots and wildcard), process all matching projects + &projects + } + _ => { + // For package specs and latest, process first project only + &projects[0..1] + } + }; + + // Process each project + for project in projects_to_process { + // For version specs with constraints, check if any matching versions are available + if let Pkgspec::Req(pkgreq) = &pkgspec { + if pkgreq.constraint.raw != "*" { + match inventory::ls(project, config).await { + Ok(versions) => { + let matching_versions: Vec<_> = versions + .iter() + .filter(|v| pkgreq.constraint.satisfies(v)) + .collect(); + + if matching_versions.is_empty() { + return Err(format!( + "No versions matching {} found for {}", + pkgreq.constraint.raw, project + ) + .into()); + } + } + Err(_) => { + return Err(format!("Failed to get versions for {}", project).into()); + } + } + } + } + + let programs = get_programs_for_project(project, conn)?; + results.push(QueryResult { + project: project.clone(), + programs, + }); + } + + Ok(results) +} + +fn format_standard_output(results: &[QueryResult]) -> Vec { + results + .iter() + .map(|result| result.project.clone()) + .collect() +} + +fn format_json_output(results: &[QueryResult]) -> String { + serde_json::to_string_pretty(results).unwrap_or_else(|_| "[]".to_string()) +} + +pub async fn query( + args: &Vec, + silent: bool, + conn: &Connection, + json_version: Option, + config: &Config, +) -> Result<(), Box> { + let is_json = json_version == Some(2); + let mut all_results = Vec::new(); -pub fn query(args: &Vec, silent: bool, conn: &Connection) -> Result<(), Box> { if args.is_empty() { - let mut stmt = conn.prepare("SELECT program FROM provides")?; + let mut stmt = conn.prepare("SELECT DISTINCT project FROM provides ORDER BY project")?; let mut rows = stmt.query(params![])?; + while let Some(row) = rows.next()? { - let program: String = row.get(0)?; - println!("{}", program); + let project: String = row.get(0)?; + let programs = get_programs_for_project(&project, conn)?; + all_results.push(QueryResult { project, programs }); } } else { - let mut fail = false; for arg in args { - let projects = pantry_db::which(arg, conn)?; - if projects.is_empty() && silent { - std::process::exit(1); - } else if projects.is_empty() { - println!("{} not found", arg); - fail = true; - } else if !silent { - println!("{}", projects.join(", ")); - } + let results = process_query_arg(arg, conn, config).await?; + all_results.extend(results); } - if fail { - std::process::exit(1); + } + + if is_json { + println!("{}", format_json_output(&all_results)); + } else if !silent { + let output_lines = format_standard_output(&all_results); + for line in output_lines { + println!("{}", line); } } + Ok(()) } diff --git a/crates/cli/src/resolve.rs b/crates/cli/src/resolve.rs index 37327ecee..38246c25a 100644 --- a/crates/cli/src/resolve.rs +++ b/crates/cli/src/resolve.rs @@ -91,7 +91,7 @@ pub async fn resolve( Ok((installations, graph)) } -enum Pkgspec { +pub enum Pkgspec { Req(PackageReq), Latest(String), } @@ -133,7 +133,7 @@ impl Pkgspec { } } -fn parse_pkgspec(pkgspec: &str) -> Result> { +pub fn parse_pkgspec(pkgspec: &str) -> Result> { if let Some(project) = pkgspec.strip_suffix("@latest") { Ok(Pkgspec::Latest(project.to_string())) } else { diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index bf097ea5f..c58e3bb6d 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -98,9 +98,7 @@ where } }); - let stream = stream - .map_err(|e| futures::io::Error::new(futures::io::ErrorKind::Other, e)) - .into_async_read(); + let stream = stream.map_err(futures::io::Error::other).into_async_read(); let stream = stream.compat(); // Step 2: Create a XZ decoder diff --git a/crates/lib/src/sync.rs b/crates/lib/src/sync.rs index 6d75138c5..f5df20954 100644 --- a/crates/lib/src/sync.rs +++ b/crates/lib/src/sync.rs @@ -58,9 +58,7 @@ async fn download_and_extract_pantry(url: &str, dest: &PathBuf) -> Result<(), Bo let stream = rsp.bytes_stream(); - let stream = stream - .map_err(|e| futures::io::Error::new(futures::io::ErrorKind::Other, e)) - .into_async_read(); + let stream = stream.map_err(futures::io::Error::other).into_async_read(); let stream = stream.compat(); let decoder = XzDecoder::new(stream);