Skip to main content

bob/
lib.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#![cfg_attr(not(doctest), doc = include_str!("../README.md"))]
18
19pub mod action;
20pub mod build;
21pub mod config;
22pub mod db;
23pub mod fmt;
24pub mod logging;
25pub mod makejobs;
26pub mod pkgstate;
27pub mod sandbox;
28pub mod scan;
29pub mod scheduler;
30pub mod vcs;
31
32mod cpu;
33mod history;
34mod init;
35mod state;
36mod summary;
37mod tui;
38
39use std::io::{self, Write};
40
41/**
42 * Column alignment for tabular output.
43 */
44#[doc(hidden)]
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46pub enum Align {
47    #[default]
48    Left,
49    Right,
50}
51
52/**
53 * Column alignment, driven by the strum `align` prop.
54 *
55 * Default to left alignment.
56 */
57#[doc(hidden)]
58pub trait ColumnAlign: strum::EnumProperty {
59    fn align(&self) -> Align {
60        match self.get_str("align") {
61            Some("right") => Align::Right,
62            _ => Align::Left,
63        }
64    }
65}
66
67/**
68 * Write a line to stdout, returning false on broken pipe.
69 *
70 * Use this in loops to gracefully handle SIGPIPE (e.g., when piped to `head`).
71 */
72#[doc(hidden)]
73pub fn try_println(s: &str) -> bool {
74    let result = writeln!(io::stdout(), "{}", s);
75    !matches!(result, Err(e) if e.kind() == io::ErrorKind::BrokenPipe)
76}
77
78/**
79 * Spawn a thread with the given name.  Thread names appear in
80 * debuggers and per-thread process listings such as prstat, allowing
81 * activity to be attributed.  Panics if the thread cannot be spawned,
82 * matching `std::thread::spawn`.
83 */
84pub(crate) fn spawn_named<T, F>(name: impl Into<String>, f: F) -> std::thread::JoinHandle<T>
85where
86    F: FnOnce() -> T + Send + 'static,
87    T: Send + 'static,
88{
89    std::thread::Builder::new()
90        .name(name.into())
91        .spawn(f)
92        .expect("failed to spawn thread")
93}
94
95/**
96 * Return the current time as seconds since the Unix epoch.
97 */
98pub(crate) fn epoch_secs() -> Result<i64, std::time::SystemTimeError> {
99    std::time::SystemTime::now()
100        .duration_since(std::time::UNIX_EPOCH)
101        .map(|d| d.as_secs() as i64)
102}
103
104/**
105 * `strftime` format for a build_id.  Build_ids are timestamp strings
106 * minted at `Database::open` and stored as the primary key in
107 * `build_metadata` and the `build_id` column of `build_history`.
108 */
109#[doc(hidden)]
110pub const BUILD_ID_FORMAT: &str = "%Y%m%dT%H%M%SZ";
111
112/**
113 * Parse a build_id string into the timestamp it encodes, or `None`
114 * if `s` is not in the expected [`BUILD_ID_FORMAT`].
115 */
116#[doc(hidden)]
117pub fn parse_build_id(s: &str) -> Option<chrono::NaiveDateTime> {
118    chrono::NaiveDateTime::parse_from_str(s, BUILD_ID_FORMAT).ok()
119}
120
121/**
122 * Parse a human-style duration like `30d`, `6w`, `12m`, `1y` into a
123 * count of seconds.  Units: `d` days, `w` weeks, `m` months (30 days),
124 * `y` years (365 days).
125 */
126#[doc(hidden)]
127pub fn parse_duration_secs(s: &str) -> Result<i64, String> {
128    let split = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
129    let (num_part, unit) = s.split_at(split);
130    let n: i64 = num_part
131        .parse()
132        .map_err(|_| format!("invalid duration '{}': expected NUMBER+UNIT", s))?;
133    let secs_per = match unit {
134        "d" => 86_400,
135        "w" => 86_400 * 7,
136        "m" => 86_400 * 30,
137        "y" => 86_400 * 365,
138        _ => {
139            return Err(format!(
140                "invalid duration unit '{}': use d, w, m, or y",
141                unit
142            ));
143        }
144    };
145    Ok(n * secs_per)
146}
147
148/// Error indicating the operation was interrupted (e.g., by Ctrl+C).
149#[doc(hidden)]
150#[derive(Debug)]
151pub struct Interrupted;
152
153impl std::fmt::Display for Interrupted {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        write!(f, "Interrupted")
156    }
157}
158
159impl std::error::Error for Interrupted {}
160
161// Re-export main types for convenience.
162//
163// The typical workflow is:
164//   Config::load() → Scan::new() → scan.start() → scan.resolve()
165//   → Build::new() → build.start()
166//
167// Hidden from rustdoc to keep the crate root page readable; items from
168// public modules are documented there instead.
169
170#[doc(hidden)]
171pub use build::{
172    Build, BuildReason, BuildResult, BuildSummary, PkgBuildStats, Stage, pkg_up_to_date,
173};
174#[doc(hidden)]
175pub use config::{Config, Summary, WrkObjKind};
176#[doc(hidden)]
177pub use cpu::{CpuSamplerHandle, start_cpu_sampler};
178#[doc(hidden)]
179pub use db::Database;
180#[doc(hidden)]
181pub use history::{History, HistoryKind};
182#[doc(hidden)]
183pub use init::Init;
184#[doc(hidden)]
185pub use pkgstate::{PackageCounts, PackageState};
186#[doc(hidden)]
187pub use sandbox::Sandbox;
188#[doc(hidden)]
189pub use scan::{Scan, ScanResult, ScanSummary};
190#[doc(hidden)]
191pub use scheduler::{PackageNode, Scheduler};
192#[doc(hidden)]
193pub use state::RunState;
194#[doc(hidden)]
195pub use summary::generate_pkg_summary;
196#[doc(hidden)]
197pub use tui::{format_duration, print_elapsed, print_failed, print_status};