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
50 changes: 50 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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}"
}
]
}
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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
}
}
13 changes: 10 additions & 3 deletions crates/cli/src/args.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use console::style;

#[derive(PartialEq)]
pub enum Mode {
X,
Help,
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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?;
Expand Down
199 changes: 182 additions & 17 deletions crates/cli/src/query.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

fn resolve_projects_for_pkgspec(
pkgspec: &mut Pkgspec,
conn: &Connection,
) -> Result<Vec<String>, Box<dyn Error>> {
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<dyn Error>> {
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<Vec<String>, Box<dyn Error>> {
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<Vec<QueryResult>, Box<dyn Error>> {
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<String> {
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<String>,
silent: bool,
conn: &Connection,
json_version: Option<isize>,
config: &Config,
) -> Result<(), Box<dyn Error>> {
let is_json = json_version == Some(2);
let mut all_results = Vec::new();

pub fn query(args: &Vec<String>, silent: bool, conn: &Connection) -> Result<(), Box<dyn Error>> {
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(())
}
4 changes: 2 additions & 2 deletions crates/cli/src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pub async fn resolve(
Ok((installations, graph))
}

enum Pkgspec {
pub enum Pkgspec {
Req(PackageReq),
Latest(String),
}
Expand Down Expand Up @@ -133,7 +133,7 @@ impl Pkgspec {
}
}

fn parse_pkgspec(pkgspec: &str) -> Result<Pkgspec, Box<dyn std::error::Error>> {
pub fn parse_pkgspec(pkgspec: &str) -> Result<Pkgspec, Box<dyn std::error::Error>> {
if let Some(project) = pkgspec.strip_suffix("@latest") {
Ok(Pkgspec::Latest(project.to_string()))
} else {
Expand Down
4 changes: 1 addition & 3 deletions crates/lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions crates/lib/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down