Skip to main content

bob/
summary.rs

1/*
2 * Copyright (c) 2026 Jonathan Perkin <jonathan@perkin.org.uk>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17/*!
18 * Generate pkg_summary.gz for binary packages.
19 *
20 * This module provides functionality to generate a `pkg_summary.gz` file
21 * containing metadata for binary packages tracked in the database.
22 */
23
24use std::fs::File;
25use std::io::Write;
26use std::path::Path;
27
28use anyhow::{Context, Result};
29use flate2::Compression;
30use flate2::write::GzEncoder;
31use pkgsrc::archive::{BinaryPackage, SummaryOptions};
32use rayon::prelude::*;
33use tracing::{debug, warn};
34
35use crate::config::PkgsrcEnv;
36use crate::db::Database;
37
38/**
39 * Generate pkg_summary.gz for all successful packages in the database.
40 *
41 * Queries the database for packages with successful build outcomes, generates
42 * Summary entries using pkgsrc::archive::BinaryPackage, and writes the
43 * concatenated output to `PACKAGES/All/pkg_summary.gz`.
44 */
45pub fn generate_pkg_summary(db: &Database, threads: usize) -> Result<()> {
46    let pkgsrc_env = db.load_pkgsrc_env()?;
47    let pkgnames = db.get_successful_packages()?;
48
49    if pkgnames.is_empty() {
50        debug!("No successful packages to include in pkg_summary");
51        return Ok(());
52    }
53
54    let packages_dir = pkgsrc_env.packages.join("All");
55
56    debug!(
57        count = pkgnames.len(),
58        dir = %packages_dir.display(),
59        "Generating pkg_summary for packages"
60    );
61
62    let pool = rayon::ThreadPoolBuilder::new()
63        .num_threads(threads)
64        .build()
65        .context("Failed to build thread pool for pkg_summary generation")?;
66
67    let results: Vec<String> = pool.install(|| {
68        pkgnames
69            .par_iter()
70            .filter_map(|pkgname| {
71                let pkgfile = packages_dir.join(format!("{}.tgz", pkgname));
72                generate_summary_entry(&pkgfile)
73            })
74            .collect()
75    });
76
77    write_pkg_summary(&pkgsrc_env, &results)
78}
79
80fn generate_summary_entry(pkgfile: &Path) -> Option<String> {
81    if !pkgfile.exists() {
82        warn!(path = %pkgfile.display(), "Package file not found");
83        return None;
84    }
85
86    let opts = SummaryOptions {
87        compute_file_cksum: true,
88    };
89
90    match BinaryPackage::open(pkgfile) {
91        Ok(pkg) => match pkg.to_summary_with_opts(&opts) {
92            Ok(summary) => Some(format!("{}\n", summary)),
93            Err(e) => {
94                warn!(
95                    path = %pkgfile.display(),
96                    error = %e,
97                    "Failed to generate summary"
98                );
99                None
100            }
101        },
102        Err(e) => {
103            warn!(
104                path = %pkgfile.display(),
105                error = %e,
106                "Failed to open package"
107            );
108            None
109        }
110    }
111}
112
113fn write_pkg_summary(pkgsrc_env: &PkgsrcEnv, entries: &[String]) -> Result<()> {
114    let summary_path = pkgsrc_env.packages.join("All/pkg_summary.gz");
115    let file = File::create(&summary_path)
116        .with_context(|| format!("Failed to create {}", summary_path.display()))?;
117    let mut encoder = GzEncoder::new(file, Compression::default());
118
119    for entry in entries {
120        encoder.write_all(entry.as_bytes())?;
121    }
122
123    encoder.finish()?;
124
125    debug!(
126        path = %summary_path.display(),
127        count = entries.len(),
128        "pkg_summary.gz written"
129    );
130
131    Ok(())
132}