Skip to content

Commit

Permalink
feat: Add JSON output (implements #3)
Browse files Browse the repository at this point in the history
  • Loading branch information
VorpalBlade committed Jun 8, 2024
1 parent c173971 commit 5253a99
Show file tree
Hide file tree
Showing 17 changed files with 249 additions and 60 deletions.
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ ring = "0.17.8"
rust-ini = "0.21.0"
smallvec = "1.13.2"
strum = "0.26.2"
serde = "1.0.203"
serde_json = "1.0.117"

[workspace.lints.rust]
rust-2018-idioms = "warn"
Expand Down
8 changes: 7 additions & 1 deletion crates/paketkoll/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ version = "0.2.1"

[features]
# Default features
default = ["debian", "arch_linux"]
default = ["debian", "arch_linux", "json"]

# Include the Arch Linux backend
arch_linux = ["paketkoll_core/arch_linux"]

# Include support for the Debian backend
debian = ["paketkoll_core/debian"]

# Include support for exporting to JSON
json = ["dep:serde_json", "paketkoll_core/serde", "serde"]
serde = ["dep:serde"]

[dependencies]
ahash = { workspace = true }
anyhow = { workspace = true, features = ["backtrace"] }
Expand All @@ -30,6 +34,8 @@ os_info = { workspace = true }
paketkoll_core = { version = "0.3.1", path = "../paketkoll_core" }
proc-exit = { workspace = true }
rayon = { workspace = true }
serde = { workspace = true, optional = true, features = ["serde_derive"] }
serde_json = { workspace = true, optional = true }

[target.'cfg(target_env = "musl")'.dependencies]
# The allocator on musl is attrociously slow, so we use a custom one.
Expand Down
4 changes: 3 additions & 1 deletion crates/paketkoll/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub(crate) struct Cli {
#[arg(short, long, default_value_t = Backend::Auto)]
pub(crate) backend: Backend,
/// Output format to use
#[arg(short, long, default_value_t = Format::Human, hide = true)]
#[arg(short, long, default_value_t = Format::Human)]
pub(crate) format: Format,
/// Operation to perform
#[command(subcommand)]
Expand Down Expand Up @@ -52,13 +52,15 @@ pub(crate) enum Format {
/// Human readable output
Human,
/// JSON formatted output
#[cfg(feature = "json")]
Json,
}

impl std::fmt::Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Format::Human => write!(f, "human"),
#[cfg(feature = "json")]
Format::Json => write!(f, "json"),
}
}
Expand Down
90 changes: 63 additions & 27 deletions crates/paketkoll/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use paketkoll_core::{
types::{Issue, PackageRef},
};
use proc_exit::{Code, Exit};
use rayon::slice::ParallelSliceMut;
use rayon::prelude::*;

#[cfg(target_env = "musl")]
use mimalloc::MiMalloc;
Expand All @@ -38,24 +38,38 @@ fn main() -> anyhow::Result<Exit> {
cli::Commands::InstalledPackages => {
let (interner, packages) = package_ops::installed_packages(&(&cli).try_into()?)?;
let mut stdout = BufWriter::new(stdout().lock());
for pkg in packages {
let pkg_name = interner
.try_resolve(&pkg.name.as_interner_ref())
.ok_or_else(|| anyhow::anyhow!("No package name for package"))?;
match pkg.reason {
Some(paketkoll_core::types::InstallReason::Explicit) => {
writeln!(stdout, "{} {}", pkg_name, pkg.version)?;
}
Some(paketkoll_core::types::InstallReason::Dependency) => {
writeln!(stdout, "{} {} (as dep)", pkg_name, pkg.version)?;

match cli.format {
cli::Format::Human => {
for pkg in packages {
let pkg_name = interner
.try_resolve(&pkg.name.as_interner_ref())
.ok_or_else(|| anyhow::anyhow!("No package name for package"))?;
match pkg.reason {
Some(paketkoll_core::types::InstallReason::Explicit) => {
writeln!(stdout, "{} {}", pkg_name, pkg.version)?;
}
Some(paketkoll_core::types::InstallReason::Dependency) => {
writeln!(stdout, "{} {} (as dep)", pkg_name, pkg.version)?;
}
None => writeln!(
stdout,
"{} {} (unknown install reason)",
pkg_name, pkg.version
)?,
}
}
None => writeln!(
stdout,
"{} {} (unknown install reason)",
pkg_name, pkg.version
)?,
}
#[cfg(feature = "json")]
cli::Format::Json => {
let packages: Vec<_> = packages
.into_par_iter()
.map(|pkg| pkg.into_direct(&interner))
.collect();
serde_json::to_writer_pretty(&mut stdout, &packages)?;
}
}

Ok(Exit::new(Code::SUCCESS))
}
}
Expand Down Expand Up @@ -91,28 +105,50 @@ fn run_file_checks(cli: &Cli) -> Result<Exit, anyhow::Error> {

let has_issues = !found_issues.is_empty();

{
let mut stdout = BufWriter::new(stdout().lock());
for (pkg, issue) in found_issues.iter() {
let pkg = pkg.and_then(|e| interner.try_resolve(&e.as_interner_ref()));
for kind in issue.kinds() {
if let Some(pkg) = pkg {
write!(stdout, "{pkg}: ")?;
match cli.format {
cli::Format::Human => {
let mut stdout = BufWriter::new(stdout().lock());
for (pkg, issue) in found_issues.iter() {
let pkg = pkg.and_then(|e| interner.try_resolve(&e.as_interner_ref()));
for kind in issue.kinds() {
if let Some(pkg) = pkg {
write!(stdout, "{pkg}: ")?;
}
// Prefer to not do any escaping. This doesn't assume unicode.
// Also it is faster.
stdout.write_all(issue.path().as_os_str().as_bytes())?;
writeln!(stdout, " {kind}")?;
}
// Prefer to not do any escaping. This doesn't assume unicode.
// Also it is faster.
stdout.write_all(issue.path().as_os_str().as_bytes())?;
writeln!(stdout, " {kind}")?;
}
}
#[cfg(feature = "json")]
cli::Format::Json => {
let mut stdout = BufWriter::new(stdout().lock());
let found_issues: Vec<_> = found_issues
.into_par_iter()
.map(|(package, issue)| {
let package = package.and_then(|e| interner.try_resolve(&e.as_interner_ref()));
IssueReport { package, issue }
})
.collect();
serde_json::to_writer_pretty(&mut stdout, &found_issues)?;
}
}

Ok(if has_issues {
Exit::new(Code::FAILURE)
} else {
Exit::new(Code::SUCCESS)
})
}

#[cfg(feature = "json")]
#[derive(Debug, serde::Serialize)]
struct IssueReport<'interner> {
package: Option<&'interner str>,
issue: Issue,
}

impl TryFrom<Backend> for paketkoll_core::config::Backend {
type Error = anyhow::Error;

Expand Down
4 changes: 4 additions & 0 deletions crates/paketkoll_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ arch_linux = [
# Include support for the Debian backend
debian = ["__md5", "dep:dashmap", "dashmap/rayon"]

# Include support for serde on public datatypes
serde = ["dep:serde", "bitflags/serde", "smallvec/serde", "compact_str/serde"]

# Internal feature: Enable MD5 support
__md5 = ["dep:md-5"]
# Internal feature: Enable SHA-256 support
Expand Down Expand Up @@ -65,6 +68,7 @@ rayon = { workspace = true }
regex = { workspace = true }
ring = { workspace = true, optional = true }
rust-ini = { workspace = true, optional = true }
serde = { workspace = true, optional = true, features = ["derive"] }
smallvec = { workspace = true, features = [
"const_generics",
"const_new",
Expand Down
2 changes: 1 addition & 1 deletion crates/paketkoll_core/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub(crate) trait Packages: Name {
fn packages(
&self,
interner: &crate::types::Interner,
) -> anyhow::Result<Vec<crate::types::Package>>;
) -> anyhow::Result<Vec<crate::types::PackageInterned>>;
}

// TODO: Operations to add
Expand Down
11 changes: 4 additions & 7 deletions crates/paketkoll_core/src/backend/arch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::{

use crate::{
config::PackageFilter,
types::{FileEntry, Interner, Package, PackageRef},
types::{FileEntry, Interner, Package, PackageInterned, PackageRef},
};
use anyhow::{Context, Result};
use dashmap::DashSet;
Expand Down Expand Up @@ -116,12 +116,9 @@ impl Files for ArchLinux {
}

impl Packages for ArchLinux {
fn packages(
&self,
interner: &crate::types::Interner,
) -> anyhow::Result<Vec<crate::types::Package>> {
fn packages(&self, interner: &crate::types::Interner) -> anyhow::Result<Vec<PackageInterned>> {
let db_root = Path::new(&self.pacman_config.db_path).join("local");
let results: anyhow::Result<Vec<Package>> = std::fs::read_dir(db_root)
let results: anyhow::Result<Vec<PackageInterned>> = std::fs::read_dir(db_root)
.context("Failed to read pacman database directory")?
.par_bridge()
.filter_map(|entry| {
Expand Down Expand Up @@ -208,7 +205,7 @@ fn load_pkg_for_file_listing(

/// Process a single packaging, parsing the desc and files entries
#[inline]
fn load_pkg(entry: &std::fs::DirEntry, interner: &Interner) -> Result<Option<Package>> {
fn load_pkg(entry: &std::fs::DirEntry, interner: &Interner) -> Result<Option<PackageInterned>> {
if !entry.file_type()?.is_dir() {
return Ok(None);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/paketkoll_core/src/backend/arch/desc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::types::{
ArchitectureRef, Dependency, InstallReason, Interner, PackageInstallStatus, PackageRef,
};

impl crate::types::Package {
impl crate::types::PackageInterned {
pub(super) fn from_arch_linux_desc(
mut readable: impl std::io::BufRead,
interner: &Interner,
Expand Down
2 changes: 1 addition & 1 deletion crates/paketkoll_core/src/backend/deb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ impl Packages for Debian {
fn packages(
&self,
interner: &crate::types::Interner,
) -> anyhow::Result<Vec<crate::types::Package>> {
) -> anyhow::Result<Vec<crate::types::PackageInterned>> {
// Parse status
log::debug!(target: "paketkoll_core::backend::deb", "Loading status to installed packages");
let (_, mut packages) = {
Expand Down
12 changes: 6 additions & 6 deletions crates/paketkoll_core/src/backend/deb/parsers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use bstr::{io::BufReadExt, ByteSlice, ByteVec};
use smallvec::SmallVec;

use crate::types::{
ArchitectureRef, Checksum, Dependency, FileEntry, FileFlags, InstallReason, Interner, Package,
PackageBuilder, PackageRef, Properties, RegularFileBasic,
ArchitectureRef, Checksum, Dependency, FileEntry, FileFlags, InstallReason, Interner,
PackageBuilder, PackageInterned, PackageRef, Properties, RegularFileBasic,
};

/// Load lines from a readable as `PathBufs`
Expand Down Expand Up @@ -78,7 +78,7 @@ pub(super) fn parse_md5sums(

/// Parse depends lines like:
/// Depends: libc6 (>= 2.34), libice6 (>= 1:1.0.0), libx11-6, libxaw7 (>= 2:1.0.14), libxcursor1 (>> 1.1.2), libxext6, libxi6, libxmu6 (>= 2:1.1.3), libxmuu1 (>= 2:1.1.3), libxrandr2 (>= 2:1.5.0), libxt6, libxxf86vm1, cpp
fn parse_depends(interner: &Interner, input: &str) -> Vec<Dependency> {
fn parse_depends(interner: &Interner, input: &str) -> Vec<Dependency<PackageRef>> {
let mut result = vec![];
for segment in input.split(',') {
let segment = segment.trim_start();
Expand Down Expand Up @@ -127,13 +127,13 @@ fn dependency_name(segment: &str, interner: &lasso::ThreadedRodeo) -> PackageRef
pub(super) fn parse_status(
interner: &Interner,
input: &mut impl BufRead,
) -> anyhow::Result<(Vec<FileEntry>, Vec<Package>)> {
) -> anyhow::Result<(Vec<FileEntry>, Vec<PackageInterned>)> {
let mut state = StatusParsingState::Start;

let mut config_files = vec![];
let mut packages = vec![];

let mut package_builder: Option<PackageBuilder> = None;
let mut package_builder: Option<PackageBuilder<PackageRef, ArchitectureRef>> = None;

// This file is UTF-8 at least
for line in input.lines() {
Expand All @@ -142,7 +142,7 @@ pub(super) fn parse_status(
if let Some(builder) = package_builder {
packages.push(builder.build()?);
}
package_builder = Some(Package::builder());
package_builder = Some(PackageInterned::builder());
// This will be updated later with the correct reason when we parse extended status
package_builder
.as_mut()
Expand Down
4 changes: 2 additions & 2 deletions crates/paketkoll_core/src/backend/flatpak.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl Packages for Flatpak {
fn packages(
&self,
interner: &crate::types::Interner,
) -> anyhow::Result<Vec<crate::types::Package>> {
) -> anyhow::Result<Vec<crate::types::PackageInterned>> {
let cmd = Command::new("flatpak")
.arg("list")
.arg("--system")
Expand All @@ -58,7 +58,7 @@ impl Packages for Flatpak {
fn parse_flatpak_output(
output: &str,
interner: &crate::types::Interner,
) -> Result<Vec<crate::types::Package>, anyhow::Error> {
) -> Result<Vec<crate::types::PackageInterned>, anyhow::Error> {
let mut packages = Vec::new();

for line in output.lines() {
Expand Down
Loading

0 comments on commit 5253a99

Please sign in to comment.