diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb660a05c..169f7b585 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,6 +167,18 @@ jobs: ! pkgx -Qqq flubber-flubber pkgx -Q + - name: query --json + run: | + pkgx -Q --json=v2 git | pkgx jq -e '.[0].project == "git-scm.org"' + pkgx -Q --json=v2 git | pkgx jq -e '.[0].programs | length > 0' + pkgx -Q --json=v2 | pkgx jq -e 'length > 0' + + - name: query pkgspecs + run: | + pkgx -Q git-scm.org | grep -q git-scm.org + pkgx -Q node git | grep -q nodejs.org + ! pkgx -Q git-scm.org@99999 + - run: if [ $(find ~/.pkgx -name .tmp\* -type d | wc -l) -gt 0 ]; then exit 1; fi diff --git a/Cargo.lock b/Cargo.lock index 6922dff3d..7a7101052 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1059,7 +1059,7 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "pkgx" -version = "2.7.1" +version = "2.8.0" dependencies = [ "console", "indicatif", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 4d584b2d3..085f1d976 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -3,7 +3,7 @@ name = "pkgx" description = "Run anything" authors = ["Max Howell ", "Jacob Heider "] license = "Apache-2.0" -version = "2.7.1" +version = "2.8.0" edition = "2021" repository = "https://github.com/pkgxdev/pkgx" diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 3b0ebae20..325da147b 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,8 +115,10 @@ pub fn parse() -> Args { } } } else { - find_program = !arg.contains('/'); - collecting_args = true; + if mode != Mode::Query { + 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..c7a63f3f2 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, &conn, &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..57fa08808 100644 --- a/crates/cli/src/query.rs +++ b/crates/cli/src/query.rs @@ -1,32 +1,168 @@ use std::error::Error; -use libpkgx::pantry_db; +use libpkgx::{config::Config, inventory, pantry_db}; use rusqlite::{params, Connection}; +use serde::Serialize; -pub fn query(args: &Vec, silent: bool, conn: &Connection) -> Result<(), Box> { +use crate::{ + args::Flags, + resolve::{parse_pkgspec, Pkgspec}, +}; + +#[derive(Serialize)] +struct QueryResult { + project: String, + programs: Vec, +} + +pub async fn query( + args: &Vec, + flags: &Flags, + conn: &Connection, + config: &Config, +) -> Result<(), Box> { + let is_json = flags.json == Some(2); + let silent = flags.silent; + + // print out the whole list if no args if args.is_empty() { - let mut stmt = conn.prepare("SELECT program FROM provides")?; - let mut rows = stmt.query(params![])?; - while let Some(row) = rows.next()? { - let program: String = row.get(0)?; - println!("{}", program); + // if they requested json, output full mapping of project -> programs + if is_json { + let mut stmt = + conn.prepare("SELECT DISTINCT project FROM provides ORDER BY project")?; + let mut rows = stmt.query(params![])?; + let mut results = Vec::new(); + while let Some(row) = rows.next()? { + let project: String = row.get(0)?; + let programs = get_programs(conn, &project)?; + results.push(QueryResult { project, programs }); + } + println!("{}", serde_json::to_string_pretty(&results)?); + // if not, just list all programs + } else { + let mut stmt = conn.prepare("SELECT program FROM provides")?; + let mut rows = stmt.query(params![])?; + while let Some(row) = rows.next()? { + let program: String = row.get(0)?; + println!("{}", program); + } } - } else { - let mut fail = false; - for arg in args { - let projects = pantry_db::which(arg, conn)?; - if projects.is_empty() && silent { + return Ok(()); + } + + let mut results = Vec::new(); + let mut fail = false; + + for arg in args { + let mut pkgspec = parse_pkgspec(arg)?; + + let projects = match &mut pkgspec { + Pkgspec::Req(req) if !req.project.contains('.') && req.constraint.raw == "*" => { + pantry_db::which(&req.project, conn)? + } + Pkgspec::Req(req) => match resolve_project(&req.project, conn) { + Ok(project) => { + req.project = project.clone(); + vec![project] + } + Err(e) => { + if silent { + std::process::exit(1); + } + println!("{}", e); + fail = true; + continue; + } + }, + Pkgspec::Latest(name) => match resolve_project(name, conn) { + Ok(project) => vec![project], + Err(e) => { + if silent { + std::process::exit(1); + } + println!("{}", e); + fail = true; + continue; + } + }, + }; + + if projects.is_empty() { + if silent { std::process::exit(1); - } else if projects.is_empty() { - println!("{} not found", arg); - fail = true; - } else if !silent { - println!("{}", projects.join(", ")); + } + println!("{} not found", arg); + fail = true; + continue; + } + + // validate version constraint if specified + if let Pkgspec::Req(req) = &pkgspec { + if req.constraint.raw != "*" { + let versions = inventory::ls(&projects[0], config).await?; + if !versions.iter().any(|v| req.constraint.satisfies(v)) { + if silent { + std::process::exit(1); + } + println!( + "no versions matching {} found for {}", + req.constraint.raw, projects[0] + ); + fail = true; + continue; + } } } - if fail { - std::process::exit(1); + + if is_json { + for project in &projects { + let programs = get_programs(conn, project)?; + results.push(QueryResult { + project: project.clone(), + programs, + }); + } + } else if !silent { + println!("{}", projects.join(", ")); } } + + if is_json { + println!("{}", serde_json::to_string_pretty(&results)?); + } + + if fail { + std::process::exit(1); + } + Ok(()) } + +fn resolve_project(input: &str, conn: &Connection) -> Result> { + let projects = pantry_db::which(&input.to_string(), conn)?; + match projects.len() { + 1 => Ok(projects[0].clone()), + 0 => { + 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()); + } + } + Err(format!("{} not found", input).into()) + } + _ => Err(format!("{} is ambiguous: {}", input, projects.join(", ")).into()), + } +} + +fn get_programs(conn: &Connection, project: &str) -> 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) +} 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 {