diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbd0caf79..43d1a32c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,7 @@ jobs: - run: pkgx +git - run: pkgx +git --json - run: pkgx +git --json=v1 + - run: pkgx +git --json=v2 - run: pkgx git --version - run: pkgx --silent +git - run: pkgx --quiet +git diff --git a/Cargo.lock b/Cargo.lock index d6025a9a9..e1092ea09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1074,6 +1074,7 @@ dependencies = [ "nix", "regex", "rusqlite", + "serde", "serde_json", "tokio", ] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 993998a2d..32066eb49 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -14,6 +14,7 @@ regex = "1.11.1" indicatif = "0.17.9" nix = { version = "0.29.0", features = ["process"] } serde_json = "1.0.135" +serde = { version = "1.0", features = ["derive"] } libpkgx = { version = "0.6.0", path = "../lib" } console = { version = "0.15", default-features = false, features = [ "ansi-parsing", diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index d478adf52..b888b6ed3 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -10,7 +10,7 @@ pub enum Mode { pub struct Flags { pub quiet: bool, pub silent: bool, - pub json: bool, + pub json: Option, pub version_n_continue: bool, pub shebang: bool, pub sync: bool, @@ -30,12 +30,13 @@ pub fn parse() -> Args { let mut args = Vec::new(); let mut silent: bool = false; let mut quiet: bool = false; - let mut json: bool = false; + let mut json = None; let mut find_program = false; let mut collecting_args = false; let mut version_n_continue = false; let mut shebang = false; let mut sync = false; + let json_latest_v: isize = 2; for arg in std::env::args().skip(1) { if collecting_args { @@ -47,17 +48,19 @@ pub fn parse() -> Args { collecting_args = true; } else if arg.starts_with("--") { match arg.as_str() { + "--shebang" => shebang = true, "--json" => { if !silent { eprintln!( - "{} use --json=v1", - style("warning: --json is not stable").yellow() + "{} use --json={}", + style("warning: --json is not stable").yellow(), + json_latest_v ); } - json = true + json = Some(2); } - "--shebang" => shebang = true, - "--json=v1" => json = true, + "--json=v1" => json = Some(1), + "--json=v2" => json = Some(2), "--silent" => silent = true, "--help" => mode = Mode::Help, "--version" => mode = Mode::Version, @@ -98,7 +101,7 @@ pub fn parse() -> Args { } 'h' => mode = Mode::Help, 's' => silent = true, - 'j' => json = true, + 'j' => json = Some(json_latest_v), 'v' => version_n_continue = true, '!' => shebang = true, 'Q' => mode = Mode::Query, diff --git a/crates/cli/src/dump.rs b/crates/cli/src/dump.rs index c48b60cb2..624c3fff0 100644 --- a/crates/cli/src/dump.rs +++ b/crates/cli/src/dump.rs @@ -1,17 +1,73 @@ -use std::collections::HashMap; +use std::{collections::HashMap, path::PathBuf, vec}; use libpkgx::{ + env::expand_moustaches, pantry_db, platform_case_aware_env_key::construct_platform_case_aware_env_key, types::Installation, }; +use serde::Serialize; use serde_json::json; pub fn dump( conn: rusqlite::Connection, installations: Vec, - env: HashMap>, flags: &crate::args::Flags, ) -> Result<(), Box> { - if !flags.json { + if let Some(v) = flags.json { + if v < 2 { + let env = libpkgx::env::map(&installations); + let mut runtime_env = HashMap::new(); + for pkg in installations.clone() { + let pkg_runtime_env = + libpkgx::pantry_db::runtime_env_for_project(&pkg.pkg.project, &conn)?; + if !pkg_runtime_env.is_empty() { + runtime_env.insert(pkg.pkg.project, pkg_runtime_env); + } + } + let json = json!({ + "pkgs": installations, + "env": env, + "runtime_env": runtime_env + }); + println!("{}", json); + } else { + let mut pkgs: HashMap = HashMap::new(); + for installation in installations.clone() { + let env = libpkgx::env::map(&vec![installation.clone()]); + let project = installation.pkg.project.clone(); + + let mut runtime_env = libpkgx::pantry_db::runtime_env_for_project(&project, &conn)?; + + for (installation_key, installation_value) in runtime_env.clone() { + let installation_value = + expand_moustaches(&installation_value, &installation, &installations); + runtime_env.insert(installation_key, installation_value); + } + + let programs = pantry_db::programs_for_project(&project, &conn)?; + let companions = pantry_db::companions_for_projects(&[project.clone()], &conn)? + .iter() + .map(|c| c.to_string()) + .collect::>(); + + let pkg = JsonV2Pkg { + path: installation.path, + project, + version: installation.pkg.version, + env, + runtime_env, + programs, + companions, + }; + pkgs.insert(pkg.project.clone(), pkg); + } + + let json = json!({ + "pkgs": pkgs, "env": libpkgx::env::map(&installations) + }); + println!("{}", json); + } + } else { + let env = libpkgx::env::map(&installations); let env = env .iter() .map(|(k, v)| { @@ -29,21 +85,21 @@ pub fn dump( value.replace(&format!(":${}", key), &format!("${{{}:+:${}}}", key, key)) ); } - } else { - let mut runtime_env = HashMap::new(); - for pkg in installations.clone() { - let pkg_runtime_env = - libpkgx::pantry_db::runtime_env_for_project(&pkg.pkg.project, &conn)?; - if !pkg_runtime_env.is_empty() { - runtime_env.insert(pkg.pkg.project, pkg_runtime_env); - } - } - let json = json!({ - "pkgs": installations, - "env": env, - "runtime_env": runtime_env - }); - println!("{}", json); } Ok(()) } + +#[derive(Serialize)] +struct JsonV2Pkg { + project: String, + version: libpkgx::Version, + #[serde(skip_serializing_if = "HashMap::is_empty")] + env: HashMap>, + #[serde(skip_serializing_if = "HashMap::is_empty")] + runtime_env: HashMap, + path: PathBuf, + #[serde(skip_serializing_if = "Vec::is_empty")] + programs: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + companions: Vec, +} diff --git a/crates/cli/src/help.rs b/crates/cli/src/help.rs index c894e06fc..8710a0260 100644 --- a/crates/cli/src/help.rs +++ b/crates/cli/src/help.rs @@ -31,6 +31,8 @@ flags: -q, --quiet # suppress brief informational messages -qq, --silent # no chat. no errors. just execute. -v # print version and continue + --sync # sync first (note: rarely if ever needed) + -j,--json=v2 # output JSON (if sensible) more: $ OPEN https://docs.pkgx.sh diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 0b2b338ba..06d4d36e9 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -53,9 +53,9 @@ async fn main() -> Result<(), Box> { &mut spinner, ) .await?; - let env = libpkgx::env::map(&installations); if !args.is_empty() { + let env = libpkgx::env::map(&installations); let (cmd, args, env) = x::exec(find_program, args, installations, env, flags, conn, graph).await?; spinner.finish_and_clear(); @@ -63,7 +63,7 @@ async fn main() -> Result<(), Box> { Ok(()) } else if !plus.is_empty() { spinner.finish_and_clear(); - dump::dump(conn, installations, env, &flags)?; + dump::dump(conn, installations, &flags)?; Ok(()) } else if flags.version_n_continue || flags.sync { Ok(()) diff --git a/crates/lib/src/pantry_db.rs b/crates/lib/src/pantry_db.rs index 61637dd75..04dcd9cfa 100644 --- a/crates/lib/src/pantry_db.rs +++ b/crates/lib/src/pantry_db.rs @@ -179,3 +179,16 @@ pub fn companions_for_projects( // Collect results into a Vec, propagating errors Ok(companions.collect::, _>>()?) } + +pub fn programs_for_project( + project: &String, + conn: &Connection, +) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare("SELECT program FROM provides WHERE project = ?1")?; + let mut rv = Vec::new(); + let mut rows = stmt.query(params![project])?; + while let Some(row) = rows.next()? { + rv.push(row.get(0)?); + } + Ok(rv) +}