1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
use redb::Database;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::runtime::Runtime;
use tokio::sync::watch;
use crate::errors;
/// Owns the per-build runtime state that was previously stored in process
/// globals: the tokio runtime, the interrupt flag, the interrupt broadcast
/// channel, and the checksum/mtime caches. Creating a fresh `BuildContext`
/// gives an isolated build environment — the prerequisite for daemon mode,
/// LSP integration, and parallel test harnesses.
pub struct BuildContext {
runtime: Runtime,
interrupted: AtomicBool,
interrupt_tx: watch::Sender<bool>,
interrupt_rx: watch::Receiver<bool>,
/// In-memory checksum cache — avoids re-reading and re-hashing the same
/// file multiple times within a single build run.
pub(crate) checksum_cache: Mutex<Option<HashMap<PathBuf, String>>>,
/// Persistent mtime database — maps (path, mtime) → checksum across builds.
pub(crate) mtime_db: Mutex<Option<Database>>,
/// Mtime entries computed this session and not yet written to `mtime_db`.
/// See `checksum::flush_mtime_cache`.
pub(crate) mtime_pending: Mutex<crate::checksum::PendingMtime>,
/// Whether mtime pre-check is enabled. Set to false by `--no-mtime-cache`.
pub(crate) mtime_enabled: AtomicBool,
/// Max-arg-length threshold for `run_checker` (sourced from `build.max_arg_len`).
/// Stored on the context so any processor can read it without having to be
/// handed the full `Config`.
pub(crate) max_arg_len: std::sync::atomic::AtomicUsize,
/// TTL for cached HTTP responses (sourced from `cache.webcache_ttl_secs`).
/// On the context for the same reason as `max_arg_len`: the iyamlschema
/// checker needs it and has no route to the full `Config`.
pub(crate) webcache_ttl_secs: std::sync::atomic::AtomicU64,
/// `[build] command_timeout_secs`; 0 means no limit (the default).
pub(crate) command_timeout_secs: std::sync::atomic::AtomicU64,
/// Every path some product of the current graph declares as an output.
/// Filled by the executor before the first product runs. A processor
/// that inspects a shared output directory after its tool ran (the
/// mass generator's plan-vs-build check) needs it to tell a neighbor's
/// declared file from a file its tool wrote off-plan — `execute` has no
/// other route to the graph.
pub(crate) declared_outputs: Mutex<HashSet<PathBuf>>,
/// Every product's declared output directories: a path under one of
/// them belongs to that product, whatever its name.
pub(crate) declared_output_dirs: Mutex<Vec<PathBuf>>,
/// In-session memo of combined input checksums for large input lists.
/// See `checksum::combined_input_checksum`.
pub(crate) combined_memo: Mutex<crate::checksum::CombinedMemo>,
}
impl BuildContext {
pub(crate) fn new() -> Self {
let runtime = Runtime::new().expect(errors::TOKIO_RUNTIME);
let (interrupt_tx, interrupt_rx) = watch::channel(false);
Self {
runtime,
interrupted: AtomicBool::new(false),
interrupt_tx,
interrupt_rx,
checksum_cache: Mutex::new(None),
mtime_db: Mutex::new(None),
mtime_pending: Mutex::new(crate::checksum::PendingMtime::default()),
mtime_enabled: AtomicBool::new(true),
max_arg_len: std::sync::atomic::AtomicUsize::new(1_000_000),
webcache_ttl_secs: std::sync::atomic::AtomicU64::new(7 * 24 * 60 * 60),
command_timeout_secs: std::sync::atomic::AtomicU64::new(0),
declared_outputs: Mutex::new(HashSet::new()),
declared_output_dirs: Mutex::new(Vec::new()),
combined_memo: Mutex::new(crate::checksum::CombinedMemo::default()),
}
}
/// Record the declared outputs of every product in the graph about to
/// be executed. Replaces any earlier set: a context outlives one graph
/// in watch mode.
pub(crate) fn set_declared_outputs(&self, paths: HashSet<PathBuf>, dirs: Vec<PathBuf>) {
*self.declared_outputs.lock().unwrap() = paths;
*self.declared_output_dirs.lock().unwrap() = dirs;
}
/// Whether some product of the current graph declares `path` as an output.
/// Whether some product owns `path`: it is a declared output file, or it
/// lies under a declared output directory. A step that writes a
/// directory of files (`output_dirs = ["_site/logos"]`) owns every file
/// in it, so a mass generator sharing `_site/` must not report them as
/// written off its plan when the two run at the same time.
pub fn is_declared_output(&self, path: &std::path::Path) -> bool {
if self.declared_outputs.lock().unwrap().contains(path) {
return true;
}
self.declared_output_dirs
.lock()
.unwrap()
.iter()
.any(|dir| path.starts_with(dir))
}
pub(crate) fn set_command_timeout_secs(&self, n: u64) {
self.command_timeout_secs.store(n, Ordering::Relaxed);
}
/// The build-wide command timeout, or None when `[build]
/// command_timeout_secs` is 0 (the default: no limit).
pub fn command_timeout(&self) -> Option<std::time::Duration> {
match self.command_timeout_secs.load(Ordering::Relaxed) {
0 => None,
secs => Some(std::time::Duration::from_secs(secs)),
}
}
pub(crate) fn set_mtime_check(&self, enabled: bool) {
self.mtime_enabled.store(enabled, Ordering::Relaxed);
}
pub(crate) fn set_max_arg_len(&self, n: usize) {
self.max_arg_len.store(n, Ordering::Relaxed);
}
pub(crate) fn set_webcache_ttl_secs(&self, n: u64) {
self.webcache_ttl_secs.store(n, Ordering::Relaxed);
}
pub(crate) fn webcache_ttl_secs(&self) -> u64 {
self.webcache_ttl_secs.load(Ordering::Relaxed)
}
pub fn max_arg_len(&self) -> usize {
self.max_arg_len.load(Ordering::Relaxed)
}
pub(crate) const fn runtime(&self) -> &Runtime {
&self.runtime
}
pub(crate) fn is_interrupted(&self) -> bool {
self.interrupted.load(Ordering::SeqCst)
}
pub(crate) fn interrupt(&self) {
self.interrupted.store(true, Ordering::SeqCst);
let _ = self.interrupt_tx.send(true);
}
pub(crate) fn interrupt_receiver(&self) -> watch::Receiver<bool> {
self.interrupt_rx.clone()
}
}
#[cfg(test)]
mod declared_output_tests {
use super::*;
use std::path::Path;
#[test]
fn a_file_under_a_declared_output_dir_is_owned() {
let ctx = BuildContext::new();
ctx.set_declared_outputs(
HashSet::from([PathBuf::from("_site/data/companies.json.gz")]),
vec![PathBuf::from("_site/logos")],
);
assert!(ctx.is_declared_output(Path::new("_site/data/companies.json.gz")));
assert!(ctx.is_declared_output(Path::new("_site/logos/redhat.svg")));
assert!(ctx.is_declared_output(Path::new("_site/logos/deep/er.svg")));
assert!(!ctx.is_declared_output(Path::new("_site/logos-old/x.svg")));
assert!(!ctx.is_declared_output(Path::new("_site/en/index.html")));
}
}