6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 1 | use std::{ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 2 | collections::{BTreeMap, BTreeSet}, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 3 | fs, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 4 | os::unix::fs::MetadataExt, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 5 | path::{Path, PathBuf}, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 6 | process::Command, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 7 | time::{SystemTime, UNIX_EPOCH}, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 8 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 9 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 10 | use serde::{Deserialize, Serialize}; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 11 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 12 | use crate::{ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 13 | client, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 14 | config::{Config, Rule}, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 15 | paths, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 16 | proto::{self, Event, Reader, Request}, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 17 | store, sys, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 18 | units::Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 19 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 20 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 21 | #[derive(Clone, Debug, Serialize, Deserialize)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 22 | pub struct Entry { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 23 | pub root: PathBuf, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 24 | pub rule: String, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 25 | pub last_used: u64, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 26 | pub pinned: bool, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 27 | pub size: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 28 | pub sized_at: u64, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 29 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 30 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 31 | type Registry = BTreeMap<PathBuf, Entry>; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 32 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 33 | fn now() -> u64 { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 34 | SystemTime::now() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 35 | .duration_since(UNIX_EPOCH) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 36 | .map_or(0, |d| d.as_secs()) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 37 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 38 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 39 | fn matches(rule: &Rule, name: &str) -> bool { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 40 | match rule.name.strip_suffix('*') { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 41 | Some(prefix) => name.starts_with(prefix), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 42 | None => name == rule.name, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 43 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 44 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 45 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 46 | fn discover<'a>(cfg: &'a Config, root: &Path) -> Vec<(PathBuf, &'a Rule)> { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 47 | let Ok(dir) = fs::read_dir(root) else { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 48 | return Vec::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 49 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 50 | let names: Vec<String> = dir |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 51 | .flatten() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 52 | .map(|e| e.file_name().to_string_lossy().into_owned()) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 53 | .collect(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 54 | names |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 55 | .iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 56 | .filter_map(|name| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 57 | let rule = cfg.artifact_rules().find(|r| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 58 | matches(r, name) && r.marker.as_ref().is_none_or(|m| root.join(m).exists()) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 59 | })?; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 60 | Some((root.join(name), rule)) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 61 | }) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 62 | .collect() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 63 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 64 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 65 | /// symlinks are followed at the top only, so a nix `result` counts its store |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 66 | /// path. hardlinks count once, the store is full of them |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 67 | fn size_of(path: &Path) -> Bytes { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 68 | let mut seen = BTreeSet::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 69 | let mut total = 0; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 70 | let mut stack = vec![fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())]; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 71 | while let Some(p) = stack.pop() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 72 | let Ok(meta) = fs::symlink_metadata(&p) else { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 73 | continue; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 74 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 75 | if meta.nlink() > 1 && !seen.insert((meta.dev(), meta.ino())) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 76 | continue; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 77 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 78 | total += meta.blocks() * 512; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 79 | if meta.is_dir() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 80 | && let Ok(dir) = fs::read_dir(&p) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 81 | { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 82 | stack.extend(dir.flatten().map(|e| e.path())); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 83 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 84 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 85 | Bytes(total) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 86 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 87 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 88 | /// only builds bump `last_used`. checking sizes isn't using them |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 89 | pub fn register(cfg: &Config, root: &Path, used: bool) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 90 | let found = discover(cfg, root); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 91 | if found.is_empty() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 92 | return; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 93 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 94 | let _ = store::update("artifacts", |reg: &mut Registry| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 95 | for (path, rule) in found { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 96 | reg.entry(path) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 97 | .and_modify(|e| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 98 | if used { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 99 | e.last_used = now(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 100 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 101 | }) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 102 | .or_insert_with(|| Entry { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 103 | root: root.to_path_buf(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 104 | rule: rule.name.clone(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 105 | last_used: now(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 106 | pinned: false, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 107 | size: Bytes::ZERO, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 108 | sized_at: 0, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 109 | }); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 110 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 111 | }); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 112 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 113 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 114 | pub fn pin(path: &Path, pinned: bool) -> Result<(), String> { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 115 | let path = fs::canonicalize(path).map_err(|e| format!("{}: {e}", path.display()))?; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 116 | let root = paths::project_root(path.parent().unwrap_or(&path)); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 117 | store::update("artifacts", |reg: &mut Registry| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 118 | reg.entry(path.clone()) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 119 | .or_insert_with(|| Entry { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 120 | root: root.clone(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 121 | rule: "manual".into(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 122 | last_used: now(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 123 | pinned, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 124 | size: Bytes::ZERO, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 125 | sized_at: 0, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 126 | }) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 127 | .pinned = pinned; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 128 | }) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 129 | .map_err(|e| e.to_string()) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 130 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 131 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 132 | fn active_roots() -> BTreeSet<PathBuf> { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 133 | let Some(mut s) = client::connect() else { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 134 | return BTreeSet::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 135 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 136 | if proto::send(&mut s, &Request::Status).is_err() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 137 | return BTreeSet::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 138 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 139 | match Reader::new(&s).and_then(|mut r| r.recv::<Event>()) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 140 | Ok(Some(Event::Status(st))) => st |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 141 | .leases |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 142 | .iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 143 | .map(|l| paths::project_root(&l.cwd)) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 144 | .collect(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 145 | _ => BTreeSet::new(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 146 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 147 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 148 | |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 149 | fn refresh(cfg: &Config, root: Option<&Path>, max_age: u64) -> Registry { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 150 | if let Some(root) = root { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 151 | register(cfg, root, false); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 152 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 153 | store::update("artifacts", |reg: &mut Registry| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 154 | reg.retain(|p, _| fs::symlink_metadata(p).is_ok()); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 155 | for (path, e) in reg.iter_mut() { |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 156 | if now().saturating_sub(e.sized_at) >= max_age { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 157 | e.size = size_of(path); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 158 | e.sized_at = now(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 159 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 160 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 161 | reg.clone() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 162 | }) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 163 | .unwrap_or_default() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 164 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 165 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 166 | pub struct Report { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 167 | pub project: Option<(PathBuf, Bytes)>, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 168 | pub global: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 169 | pub over_project: bool, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 170 | pub over_global: bool, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 171 | pub entries: Registry, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 172 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 173 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 174 | impl Report { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 175 | pub const fn over(&self) -> bool { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 176 | self.over_project || self.over_global |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 177 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 178 | |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 179 | pub const fn remedy(&self) -> &'static str { |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 180 | if self.over_global { |
fdad58f8133a | atagen | 2026-10-25 23:14 UTC | 181 | "roundabout artifacts gc --cap" |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 182 | } else { |
fdad58f8133a | atagen | 2026-10-25 23:14 UTC | 183 | "roundabout artifacts gc --project --cap" |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 184 | } |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 185 | } |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 186 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 187 | pub fn describe(&self, cfg: &Config) -> String { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 188 | let mut out = Vec::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 189 | if let Some((root, size)) = &self.project |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 190 | && self.over_project |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 191 | { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 192 | out.push(format!( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 193 | "{} holds {size} of build artifacts, cap is {}", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 194 | root.display(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 195 | cfg.artifacts.project_cap |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 196 | )); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 197 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 198 | if self.over_global { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 199 | out.push(format!( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 200 | "artifacts total {} across projects, cap is {}", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 201 | self.global, cfg.artifacts.global_cap |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 202 | )); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 203 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 204 | out.join(". ") |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 205 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 206 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 207 | |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 208 | const RECHECK_SECS: u64 = 60; |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 209 | |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 210 | /// the cache can predate someone's `rm`, so being over earns a second look |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 211 | /// before anyone gets nagged |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 212 | pub fn check(cfg: &Config, root: Option<&Path>, fresh: bool) -> Report { |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 213 | let ttl = if fresh { |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 214 | 0 |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 215 | } else { |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 216 | cfg.artifacts.size_ttl.0.as_secs() |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 217 | }; |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 218 | let report = measure(cfg, root, ttl); |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 219 | if report.over() && ttl > RECHECK_SECS { |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 220 | return measure(cfg, root, RECHECK_SECS); |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 221 | } |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 222 | report |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 223 | } |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 224 | |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 225 | fn measure(cfg: &Config, root: Option<&Path>, max_age: u64) -> Report { |
7770b8fcfa8b | atagen | 2026-10-25 10:50 UTC | 226 | let entries = refresh(cfg, root, max_age); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 227 | let global = entries.values().map(|e| e.size).sum(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 228 | let project = root.map(|r| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 229 | let size = entries |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 230 | .values() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 231 | .filter(|e| e.root == r) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 232 | .map(|e| e.size) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 233 | .sum(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 234 | (r.to_path_buf(), size) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 235 | }); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 236 | Report { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 237 | over_project: project |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 238 | .as_ref() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 239 | .is_some_and(|(_, s)| *s > cfg.artifacts.project_cap), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 240 | over_global: global > cfg.artifacts.global_cap, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 241 | project, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 242 | global, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 243 | entries, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 244 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 245 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 246 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 247 | fn remove(cfg: &Config, path: &Path, e: &Entry) -> Result<(), String> { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 248 | let rule = cfg.artifact_rules().find(|r| r.name == e.rule); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 249 | if let Some(clean) = rule.and_then(|r| r.clean.as_ref()) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 250 | && let Some((prog, args)) = clean.split_first() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 251 | { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 252 | let ok = Command::new(prog) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 253 | .args(args) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 254 | .current_dir(&e.root) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 255 | .status() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 256 | .is_ok_and(|s| s.success()); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 257 | return if ok { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 258 | Ok(()) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 259 | } else { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 260 | Err(format!("`{}` failed", clean.join(" "))) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 261 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 262 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 263 | let meta = fs::symlink_metadata(path).map_err(|err| err.to_string())?; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 264 | if meta.is_dir() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 265 | fs::remove_dir_all(path) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 266 | } else { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 267 | fs::remove_file(path) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 268 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 269 | .map_err(|err| format!("{}: {err}", path.display())) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 270 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 271 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 272 | pub struct Gc { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 273 | pub freed: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 274 | pub removed: Vec<PathBuf>, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 275 | pub failed: Vec<String>, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 276 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 277 | |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 278 | pub fn gc(cfg: &Config, project: Option<&Path>, to_cap: bool, dry_run: bool) -> Gc { |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 279 | let max_age = if to_cap { |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 280 | RECHECK_SECS |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 281 | } else { |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 282 | cfg.artifacts.size_ttl.0.as_secs() |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 283 | }; |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 284 | let entries = refresh(cfg, project, max_age); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 285 | let active = active_roots(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 286 | let idle_after = cfg.artifacts.idle_after.0.as_secs(); |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 287 | let movable = |
e9a5f5a41749 | atagen | 2026-10-25 11:09 UTC | 288 | |e: &Entry| !e.pinned && !active.contains(&e.root) && project.is_none_or(|r| e.root == r); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 289 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 290 | let mut doomed: Vec<PathBuf> = entries |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 291 | .iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 292 | .filter(|(_, e)| movable(e) && now().saturating_sub(e.last_used) > idle_after) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 293 | .map(|(p, _)| p.clone()) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 294 | .collect(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 295 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 296 | if to_cap { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 297 | let mut global: Bytes = entries |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 298 | .iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 299 | .filter(|(p, _)| !doomed.contains(p)) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 300 | .map(|(_, e)| e.size) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 301 | .sum(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 302 | let mut per_root: BTreeMap<&Path, Bytes> = BTreeMap::new(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 303 | for (p, e) in &entries { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 304 | if !doomed.contains(p) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 305 | *per_root.entry(&e.root).or_default() += e.size; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 306 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 307 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 308 | let mut lru: Vec<(&PathBuf, &Entry)> = entries |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 309 | .iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 310 | .filter(|(p, e)| movable(e) && !doomed.contains(p)) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 311 | .collect(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 312 | lru.sort_by_key(|(_, e)| e.last_used); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 313 | for (p, e) in lru { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 314 | let root_size = per_root.get(e.root.as_path()).copied().unwrap_or_default(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 315 | if global <= cfg.artifacts.global_cap && root_size <= cfg.artifacts.project_cap { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 316 | continue; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 317 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 318 | global = global.saturating_sub(e.size); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 319 | per_root.insert(&e.root, root_size.saturating_sub(e.size)); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 320 | doomed.push(p.clone()); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 321 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 322 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 323 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 324 | let mut out = Gc { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 325 | freed: Bytes::ZERO, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 326 | removed: Vec::new(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 327 | failed: Vec::new(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 328 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 329 | for path in doomed { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 330 | let e = &entries[&path]; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 331 | if !dry_run && let Err(err) = remove(cfg, &path, e) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 332 | out.failed.push(err); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 333 | continue; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 334 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 335 | out.freed += e.size; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 336 | out.removed.push(path); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 337 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 338 | if !dry_run { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 339 | let removed = out.removed.clone(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 340 | let _ = store::update("artifacts", |reg: &mut Registry| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 341 | for p in &removed { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 342 | reg.remove(p); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 343 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 344 | }); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 345 | nix_gc(cfg, &mut out); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 346 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 347 | out |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 348 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 349 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 350 | fn nix_gc(cfg: &Config, out: &mut Gc) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 351 | if !cfg.artifacts.nix_gc { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 352 | return; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 353 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 354 | let Some(store) = sys::fs(Path::new("/nix/store")) else { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 355 | return; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 356 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 357 | let want = cfg.artifacts.nix_min_free.saturating_sub(store.free); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 358 | if want == Bytes::ZERO { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 359 | return; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 360 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 361 | let ok = Command::new("nix") |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 362 | .args(["store", "gc", "--max", &want.0.to_string()]) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 363 | .status() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 364 | .is_ok_and(|s| s.success()); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 365 | if ok { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 366 | let after = sys::fs(Path::new("/nix/store")).map_or(store.free, |s| s.free); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 367 | out.freed += after.saturating_sub(store.free); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 368 | } else { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 369 | out.failed.push("nix store gc failed".into()); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 370 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 371 | } |