bob 1.0.0

Fast, robust, powerful, user-friendly pkgsrc package builder
Documentation
/*
 * Copyright (c) 2026 Jonathan Perkin <jonathan@perkin.org.uk>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*!
 * Build history types and display formatting.
 *
 * [`HistoryKind`] is the single source of truth for history column
 * definitions, including database schema and display formatting.
 * [`History`] holds the data for a single build history record, used
 * for both database writes and reads.
 */

use std::time::Duration;

use strum::{EnumMessage, VariantArray};

use crate::build::Stage;
use crate::{ColumnAlign, PackageState};

/// Prefix for selecting CPU time instead of wall time for a stage.
pub(crate) const CPU_PREFIX: &str = "cpu:";

/**
 * Columns in the `build_history` table.
 *
 * Variant names map 1:1 to [`History`] struct fields.  The snake_case
 * serialization provides both the database column name and the CLI
 * display name.  Per-stage duration columns come from [`Stage`]
 * variants via a separate table.
 */
#[derive(
    Clone,
    Copy,
    Debug,
    PartialEq,
    Eq,
    strum::IntoStaticStr,
    strum::VariantArray,
    strum::EnumMessage,
    strum::EnumProperty,
)]
#[strum(serialize_all = "snake_case")]
pub enum HistoryKind {
    #[strum(message = "Build start time")]
    Timestamp,
    #[strum(message = "Package path in pkgsrc")]
    Pkgpath,
    #[strum(message = "Package name and version")]
    Pkgname,
    #[strum(message = "Package name without version")]
    Pkgbase,
    #[strum(message = "Build result")]
    Outcome,
    #[strum(message = "Last stage attempted (for failures)")]
    Stage,
    #[strum(message = "MAKE_JOBS used", props(align = "right"))]
    MakeJobs,
    #[strum(message = "Total wall-clock duration", props(align = "right"))]
    Duration,
    #[strum(message = "WRKDIR size at end of build", props(align = "right"))]
    DiskUsage,
    #[strum(message = "WRKOBJDIR type (tmpfs or disk)")]
    Wrkobjdir,
    #[strum(message = "Build session identifier")]
    BuildId,
}

/**
 * Alignment from per-variant `align` props.
 */
impl crate::ColumnAlign for HistoryKind {}

impl HistoryKind {
    /// All valid column names with alignment.
    pub fn all_columns() -> Vec<(String, crate::Align)> {
        Self::VARIANTS
            .iter()
            .map(|v| (<&str>::from(v).to_string(), v.align()))
            .chain(
                Stage::VARIANTS
                    .iter()
                    .map(|s| (s.into_str().to_string(), s.align())),
            )
            .chain(
                Stage::VARIANTS
                    .iter()
                    .map(|s| (format!("{CPU_PREFIX}{}", s.into_str()), s.align())),
            )
            .collect()
    }

    /// Column names shown by default.
    pub fn default_names() -> Vec<&'static str> {
        use HistoryKind::*;
        [
            Timestamp, Pkgname, Outcome, MakeJobs, Wrkobjdir, DiskUsage, Duration,
        ]
        .iter()
        .map(|v| v.into())
        .collect()
    }

    /// Generate the `after_long_help` text for `bob history`.
    pub fn columns_help() -> String {
        use std::fmt::Write as _;
        let all_cols = Self::all_columns();
        let max_name = all_cols.iter().map(|(n, _)| n.len()).max().unwrap_or(0);

        let mut help = String::from("Columns to display (comma-separated)\n\nColumns:\n");
        for col in Self::VARIANTS {
            let name: &str = col.into();
            let desc = col.get_message().unwrap_or("");
            let _ = writeln!(help, "  {:<width$}  {}", name, desc, width = max_name);
        }
        for s in Stage::VARIANTS {
            let name = s.into_str();
            let _ = writeln!(
                help,
                "  {:<width$}  Wall time for {} stage",
                name,
                name,
                width = max_name
            );
        }
        for s in Stage::VARIANTS {
            let name = s.into_str();
            let _ = writeln!(
                help,
                "  {:<width$}  CPU time for {} stage",
                format!("{CPU_PREFIX}{name}"),
                name,
                width = max_name
            );
        }
        let _ = write!(
            help,
            "\nDefault columns: {}",
            Self::default_names().join(",")
        );

        help
    }

    pub fn after_help() -> String {
        "Examples:\n  \
         bob history                                        Show all build history\n  \
         bob history rust                                   Show history matching 'rust'\n  \
         bob history -o pkgname,build,cpu:build,duration    Show build wall+cpu time\n  \
         bob history -Ho pkgpath                            Show pkgpaths only, no header"
            .to_string()
    }
}

/**
 * A single build history record.
 *
 * Field names match [`HistoryKind`] variant names and are used
 * directly as database column names.
 */
pub struct History {
    pub timestamp: i64,
    pub pkgpath: String,
    pub pkgname: String,
    pub pkgbase: String,
    pub outcome: PackageState,
    pub stage: Option<Stage>,
    pub make_jobs: Option<usize>,
    pub duration: Duration,
    pub disk_usage: Option<u64>,
    pub wrkobjdir: Option<crate::config::WrkObjKind>,
    pub stage_durations: Vec<(Stage, Duration)>,
    pub stage_cpu_times: Vec<(Stage, Duration)>,
    pub build_id: Option<String>,
}