atagen

roundabout

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/roundabout.git

Blame: src/artifacts.rs

6f5b84fad3efatagen2026-09-27 03:59 UTC1
use std::{
6f5b84fad3efatagen2026-09-27 03:59 UTC2
    collections::{BTreeMap, BTreeSet},
6f5b84fad3efatagen2026-09-27 03:59 UTC3
    fs,
6f5b84fad3efatagen2026-09-27 03:59 UTC4
    os::unix::fs::MetadataExt,
6f5b84fad3efatagen2026-09-27 03:59 UTC5
    path::{Path, PathBuf},
6f5b84fad3efatagen2026-09-27 03:59 UTC6
    process::Command,
6f5b84fad3efatagen2026-09-27 03:59 UTC7
    time::{SystemTime, UNIX_EPOCH},
6f5b84fad3efatagen2026-09-27 03:59 UTC8
};
6f5b84fad3efatagen2026-09-27 03:59 UTC9
6f5b84fad3efatagen2026-09-27 03:59 UTC10
use serde::{Deserialize, Serialize};
6f5b84fad3efatagen2026-09-27 03:59 UTC11
6f5b84fad3efatagen2026-09-27 03:59 UTC12
use crate::{
6f5b84fad3efatagen2026-09-27 03:59 UTC13
    client,
6f5b84fad3efatagen2026-09-27 03:59 UTC14
    config::{Config, Rule},
6f5b84fad3efatagen2026-09-27 03:59 UTC15
    paths,
6f5b84fad3efatagen2026-09-27 03:59 UTC16
    proto::{self, Event, Reader, Request},
6f5b84fad3efatagen2026-09-27 03:59 UTC17
    store, sys,
6f5b84fad3efatagen2026-09-27 03:59 UTC18
    units::Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC19
};
6f5b84fad3efatagen2026-09-27 03:59 UTC20
6f5b84fad3efatagen2026-09-27 03:59 UTC21
#[derive(Clone, Debug, Serialize, Deserialize)]
6f5b84fad3efatagen2026-09-27 03:59 UTC22
pub struct Entry {
6f5b84fad3efatagen2026-09-27 03:59 UTC23
    pub root: PathBuf,
6f5b84fad3efatagen2026-09-27 03:59 UTC24
    pub rule: String,
6f5b84fad3efatagen2026-09-27 03:59 UTC25
    pub last_used: u64,
6f5b84fad3efatagen2026-09-27 03:59 UTC26
    pub pinned: bool,
6f5b84fad3efatagen2026-09-27 03:59 UTC27
    pub size: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC28
    pub sized_at: u64,
6f5b84fad3efatagen2026-09-27 03:59 UTC29
}
6f5b84fad3efatagen2026-09-27 03:59 UTC30
6f5b84fad3efatagen2026-09-27 03:59 UTC31
type Registry = BTreeMap<PathBuf, Entry>;
6f5b84fad3efatagen2026-09-27 03:59 UTC32
6f5b84fad3efatagen2026-09-27 03:59 UTC33
fn now() -> u64 {
6f5b84fad3efatagen2026-09-27 03:59 UTC34
    SystemTime::now()
6f5b84fad3efatagen2026-09-27 03:59 UTC35
        .duration_since(UNIX_EPOCH)
6f5b84fad3efatagen2026-09-27 03:59 UTC36
        .map_or(0, |d| d.as_secs())
6f5b84fad3efatagen2026-09-27 03:59 UTC37
}
6f5b84fad3efatagen2026-09-27 03:59 UTC38
6f5b84fad3efatagen2026-09-27 03:59 UTC39
fn matches(rule: &Rule, name: &str) -> bool {
6f5b84fad3efatagen2026-09-27 03:59 UTC40
    match rule.name.strip_suffix('*') {
6f5b84fad3efatagen2026-09-27 03:59 UTC41
        Some(prefix) => name.starts_with(prefix),
6f5b84fad3efatagen2026-09-27 03:59 UTC42
        None => name == rule.name,
6f5b84fad3efatagen2026-09-27 03:59 UTC43
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC44
}
6f5b84fad3efatagen2026-09-27 03:59 UTC45
6f5b84fad3efatagen2026-09-27 03:59 UTC46
fn discover<'a>(cfg: &'a Config, root: &Path) -> Vec<(PathBuf, &'a Rule)> {
6f5b84fad3efatagen2026-09-27 03:59 UTC47
    let Ok(dir) = fs::read_dir(root) else {
6f5b84fad3efatagen2026-09-27 03:59 UTC48
        return Vec::new();
6f5b84fad3efatagen2026-09-27 03:59 UTC49
    };
6f5b84fad3efatagen2026-09-27 03:59 UTC50
    let names: Vec<String> = dir
6f5b84fad3efatagen2026-09-27 03:59 UTC51
        .flatten()
6f5b84fad3efatagen2026-09-27 03:59 UTC52
        .map(|e| e.file_name().to_string_lossy().into_owned())
6f5b84fad3efatagen2026-09-27 03:59 UTC53
        .collect();
6f5b84fad3efatagen2026-09-27 03:59 UTC54
    names
6f5b84fad3efatagen2026-09-27 03:59 UTC55
        .iter()
6f5b84fad3efatagen2026-09-27 03:59 UTC56
        .filter_map(|name| {
6f5b84fad3efatagen2026-09-27 03:59 UTC57
            let rule = cfg.artifact_rules().find(|r| {
6f5b84fad3efatagen2026-09-27 03:59 UTC58
                matches(r, name) && r.marker.as_ref().is_none_or(|m| root.join(m).exists())
6f5b84fad3efatagen2026-09-27 03:59 UTC59
            })?;
6f5b84fad3efatagen2026-09-27 03:59 UTC60
            Some((root.join(name), rule))
6f5b84fad3efatagen2026-09-27 03:59 UTC61
        })
6f5b84fad3efatagen2026-09-27 03:59 UTC62
        .collect()
6f5b84fad3efatagen2026-09-27 03:59 UTC63
}
6f5b84fad3efatagen2026-09-27 03:59 UTC64
6f5b84fad3efatagen2026-09-27 03:59 UTC65
/// symlinks are followed at the top only, so a nix `result` counts its store
6f5b84fad3efatagen2026-09-27 03:59 UTC66
/// path. hardlinks count once, the store is full of them
6f5b84fad3efatagen2026-09-27 03:59 UTC67
fn size_of(path: &Path) -> Bytes {
6f5b84fad3efatagen2026-09-27 03:59 UTC68
    let mut seen = BTreeSet::new();
6f5b84fad3efatagen2026-09-27 03:59 UTC69
    let mut total = 0;
6f5b84fad3efatagen2026-09-27 03:59 UTC70
    let mut stack = vec![fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())];
6f5b84fad3efatagen2026-09-27 03:59 UTC71
    while let Some(p) = stack.pop() {
6f5b84fad3efatagen2026-09-27 03:59 UTC72
        let Ok(meta) = fs::symlink_metadata(&p) else {
6f5b84fad3efatagen2026-09-27 03:59 UTC73
            continue;
6f5b84fad3efatagen2026-09-27 03:59 UTC74
        };
6f5b84fad3efatagen2026-09-27 03:59 UTC75
        if meta.nlink() > 1 && !seen.insert((meta.dev(), meta.ino())) {
6f5b84fad3efatagen2026-09-27 03:59 UTC76
            continue;
6f5b84fad3efatagen2026-09-27 03:59 UTC77
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC78
        total += meta.blocks() * 512;
6f5b84fad3efatagen2026-09-27 03:59 UTC79
        if meta.is_dir()
6f5b84fad3efatagen2026-09-27 03:59 UTC80
            && let Ok(dir) = fs::read_dir(&p)
6f5b84fad3efatagen2026-09-27 03:59 UTC81
        {
6f5b84fad3efatagen2026-09-27 03:59 UTC82
            stack.extend(dir.flatten().map(|e| e.path()));
6f5b84fad3efatagen2026-09-27 03:59 UTC83
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC84
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC85
    Bytes(total)
6f5b84fad3efatagen2026-09-27 03:59 UTC86
}
6f5b84fad3efatagen2026-09-27 03:59 UTC87
6f5b84fad3efatagen2026-09-27 03:59 UTC88
/// only builds bump `last_used`. checking sizes isn't using them
6f5b84fad3efatagen2026-09-27 03:59 UTC89
pub fn register(cfg: &Config, root: &Path, used: bool) {
6f5b84fad3efatagen2026-09-27 03:59 UTC90
    let found = discover(cfg, root);
6f5b84fad3efatagen2026-09-27 03:59 UTC91
    if found.is_empty() {
6f5b84fad3efatagen2026-09-27 03:59 UTC92
        return;
6f5b84fad3efatagen2026-09-27 03:59 UTC93
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC94
    let _ = store::update("artifacts", |reg: &mut Registry| {
6f5b84fad3efatagen2026-09-27 03:59 UTC95
        for (path, rule) in found {
6f5b84fad3efatagen2026-09-27 03:59 UTC96
            reg.entry(path)
6f5b84fad3efatagen2026-09-27 03:59 UTC97
                .and_modify(|e| {
6f5b84fad3efatagen2026-09-27 03:59 UTC98
                    if used {
6f5b84fad3efatagen2026-09-27 03:59 UTC99
                        e.last_used = now();
6f5b84fad3efatagen2026-09-27 03:59 UTC100
                    }
6f5b84fad3efatagen2026-09-27 03:59 UTC101
                })
6f5b84fad3efatagen2026-09-27 03:59 UTC102
                .or_insert_with(|| Entry {
6f5b84fad3efatagen2026-09-27 03:59 UTC103
                    root: root.to_path_buf(),
6f5b84fad3efatagen2026-09-27 03:59 UTC104
                    rule: rule.name.clone(),
6f5b84fad3efatagen2026-09-27 03:59 UTC105
                    last_used: now(),
6f5b84fad3efatagen2026-09-27 03:59 UTC106
                    pinned: false,
6f5b84fad3efatagen2026-09-27 03:59 UTC107
                    size: Bytes::ZERO,
6f5b84fad3efatagen2026-09-27 03:59 UTC108
                    sized_at: 0,
6f5b84fad3efatagen2026-09-27 03:59 UTC109
                });
6f5b84fad3efatagen2026-09-27 03:59 UTC110
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC111
    });
6f5b84fad3efatagen2026-09-27 03:59 UTC112
}
6f5b84fad3efatagen2026-09-27 03:59 UTC113
6f5b84fad3efatagen2026-09-27 03:59 UTC114
pub fn pin(path: &Path, pinned: bool) -> Result<(), String> {
6f5b84fad3efatagen2026-09-27 03:59 UTC115
    let path = fs::canonicalize(path).map_err(|e| format!("{}: {e}", path.display()))?;
6f5b84fad3efatagen2026-09-27 03:59 UTC116
    let root = paths::project_root(path.parent().unwrap_or(&path));
6f5b84fad3efatagen2026-09-27 03:59 UTC117
    store::update("artifacts", |reg: &mut Registry| {
6f5b84fad3efatagen2026-09-27 03:59 UTC118
        reg.entry(path.clone())
6f5b84fad3efatagen2026-09-27 03:59 UTC119
            .or_insert_with(|| Entry {
6f5b84fad3efatagen2026-09-27 03:59 UTC120
                root: root.clone(),
6f5b84fad3efatagen2026-09-27 03:59 UTC121
                rule: "manual".into(),
6f5b84fad3efatagen2026-09-27 03:59 UTC122
                last_used: now(),
6f5b84fad3efatagen2026-09-27 03:59 UTC123
                pinned,
6f5b84fad3efatagen2026-09-27 03:59 UTC124
                size: Bytes::ZERO,
6f5b84fad3efatagen2026-09-27 03:59 UTC125
                sized_at: 0,
6f5b84fad3efatagen2026-09-27 03:59 UTC126
            })
6f5b84fad3efatagen2026-09-27 03:59 UTC127
            .pinned = pinned;
6f5b84fad3efatagen2026-09-27 03:59 UTC128
    })
6f5b84fad3efatagen2026-09-27 03:59 UTC129
    .map_err(|e| e.to_string())
6f5b84fad3efatagen2026-09-27 03:59 UTC130
}
6f5b84fad3efatagen2026-09-27 03:59 UTC131
6f5b84fad3efatagen2026-09-27 03:59 UTC132
fn active_roots() -> BTreeSet<PathBuf> {
6f5b84fad3efatagen2026-09-27 03:59 UTC133
    let Some(mut s) = client::connect() else {
6f5b84fad3efatagen2026-09-27 03:59 UTC134
        return BTreeSet::new();
6f5b84fad3efatagen2026-09-27 03:59 UTC135
    };
6f5b84fad3efatagen2026-09-27 03:59 UTC136
    if proto::send(&mut s, &Request::Status).is_err() {
6f5b84fad3efatagen2026-09-27 03:59 UTC137
        return BTreeSet::new();
6f5b84fad3efatagen2026-09-27 03:59 UTC138
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC139
    match Reader::new(&s).and_then(|mut r| r.recv::<Event>()) {
6f5b84fad3efatagen2026-09-27 03:59 UTC140
        Ok(Some(Event::Status(st))) => st
6f5b84fad3efatagen2026-09-27 03:59 UTC141
            .leases
6f5b84fad3efatagen2026-09-27 03:59 UTC142
            .iter()
6f5b84fad3efatagen2026-09-27 03:59 UTC143
            .map(|l| paths::project_root(&l.cwd))
6f5b84fad3efatagen2026-09-27 03:59 UTC144
            .collect(),
6f5b84fad3efatagen2026-09-27 03:59 UTC145
        _ => BTreeSet::new(),
6f5b84fad3efatagen2026-09-27 03:59 UTC146
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC147
}
6f5b84fad3efatagen2026-09-27 03:59 UTC148
7770b8fcfa8batagen2026-10-25 10:50 UTC149
fn refresh(cfg: &Config, root: Option<&Path>, max_age: u64) -> Registry {
6f5b84fad3efatagen2026-09-27 03:59 UTC150
    if let Some(root) = root {
6f5b84fad3efatagen2026-09-27 03:59 UTC151
        register(cfg, root, false);
6f5b84fad3efatagen2026-09-27 03:59 UTC152
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC153
    store::update("artifacts", |reg: &mut Registry| {
6f5b84fad3efatagen2026-09-27 03:59 UTC154
        reg.retain(|p, _| fs::symlink_metadata(p).is_ok());
6f5b84fad3efatagen2026-09-27 03:59 UTC155
        for (path, e) in reg.iter_mut() {
7770b8fcfa8batagen2026-10-25 10:50 UTC156
            if now().saturating_sub(e.sized_at) >= max_age {
6f5b84fad3efatagen2026-09-27 03:59 UTC157
                e.size = size_of(path);
6f5b84fad3efatagen2026-09-27 03:59 UTC158
                e.sized_at = now();
6f5b84fad3efatagen2026-09-27 03:59 UTC159
            }
6f5b84fad3efatagen2026-09-27 03:59 UTC160
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC161
        reg.clone()
6f5b84fad3efatagen2026-09-27 03:59 UTC162
    })
6f5b84fad3efatagen2026-09-27 03:59 UTC163
    .unwrap_or_default()
6f5b84fad3efatagen2026-09-27 03:59 UTC164
}
6f5b84fad3efatagen2026-09-27 03:59 UTC165
6f5b84fad3efatagen2026-09-27 03:59 UTC166
pub struct Report {
6f5b84fad3efatagen2026-09-27 03:59 UTC167
    pub project: Option<(PathBuf, Bytes)>,
6f5b84fad3efatagen2026-09-27 03:59 UTC168
    pub global: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC169
    pub over_project: bool,
6f5b84fad3efatagen2026-09-27 03:59 UTC170
    pub over_global: bool,
6f5b84fad3efatagen2026-09-27 03:59 UTC171
    pub entries: Registry,
6f5b84fad3efatagen2026-09-27 03:59 UTC172
}
6f5b84fad3efatagen2026-09-27 03:59 UTC173
6f5b84fad3efatagen2026-09-27 03:59 UTC174
impl Report {
6f5b84fad3efatagen2026-09-27 03:59 UTC175
    pub const fn over(&self) -> bool {
6f5b84fad3efatagen2026-09-27 03:59 UTC176
        self.over_project || self.over_global
6f5b84fad3efatagen2026-09-27 03:59 UTC177
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC178
e9a5f5a41749atagen2026-10-25 11:09 UTC179
    pub const fn remedy(&self) -> &'static str {
e9a5f5a41749atagen2026-10-25 11:09 UTC180
        if self.over_global {
fdad58f8133aatagen2026-10-25 23:14 UTC181
            "roundabout artifacts gc --cap"
e9a5f5a41749atagen2026-10-25 11:09 UTC182
        } else {
fdad58f8133aatagen2026-10-25 23:14 UTC183
            "roundabout artifacts gc --project --cap"
e9a5f5a41749atagen2026-10-25 11:09 UTC184
        }
e9a5f5a41749atagen2026-10-25 11:09 UTC185
    }
e9a5f5a41749atagen2026-10-25 11:09 UTC186
6f5b84fad3efatagen2026-09-27 03:59 UTC187
    pub fn describe(&self, cfg: &Config) -> String {
6f5b84fad3efatagen2026-09-27 03:59 UTC188
        let mut out = Vec::new();
6f5b84fad3efatagen2026-09-27 03:59 UTC189
        if let Some((root, size)) = &self.project
6f5b84fad3efatagen2026-09-27 03:59 UTC190
            && self.over_project
6f5b84fad3efatagen2026-09-27 03:59 UTC191
        {
6f5b84fad3efatagen2026-09-27 03:59 UTC192
            out.push(format!(
6f5b84fad3efatagen2026-09-27 03:59 UTC193
                "{} holds {size} of build artifacts, cap is {}",
6f5b84fad3efatagen2026-09-27 03:59 UTC194
                root.display(),
6f5b84fad3efatagen2026-09-27 03:59 UTC195
                cfg.artifacts.project_cap
6f5b84fad3efatagen2026-09-27 03:59 UTC196
            ));
6f5b84fad3efatagen2026-09-27 03:59 UTC197
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC198
        if self.over_global {
6f5b84fad3efatagen2026-09-27 03:59 UTC199
            out.push(format!(
6f5b84fad3efatagen2026-09-27 03:59 UTC200
                "artifacts total {} across projects, cap is {}",
6f5b84fad3efatagen2026-09-27 03:59 UTC201
                self.global, cfg.artifacts.global_cap
6f5b84fad3efatagen2026-09-27 03:59 UTC202
            ));
6f5b84fad3efatagen2026-09-27 03:59 UTC203
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC204
        out.join(". ")
6f5b84fad3efatagen2026-09-27 03:59 UTC205
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC206
}
6f5b84fad3efatagen2026-09-27 03:59 UTC207
7770b8fcfa8batagen2026-10-25 10:50 UTC208
const RECHECK_SECS: u64 = 60;
7770b8fcfa8batagen2026-10-25 10:50 UTC209
7770b8fcfa8batagen2026-10-25 10:50 UTC210
/// the cache can predate someone's `rm`, so being over earns a second look
7770b8fcfa8batagen2026-10-25 10:50 UTC211
/// before anyone gets nagged
6f5b84fad3efatagen2026-09-27 03:59 UTC212
pub fn check(cfg: &Config, root: Option<&Path>, fresh: bool) -> Report {
7770b8fcfa8batagen2026-10-25 10:50 UTC213
    let ttl = if fresh {
7770b8fcfa8batagen2026-10-25 10:50 UTC214
        0
7770b8fcfa8batagen2026-10-25 10:50 UTC215
    } else {
7770b8fcfa8batagen2026-10-25 10:50 UTC216
        cfg.artifacts.size_ttl.0.as_secs()
7770b8fcfa8batagen2026-10-25 10:50 UTC217
    };
7770b8fcfa8batagen2026-10-25 10:50 UTC218
    let report = measure(cfg, root, ttl);
7770b8fcfa8batagen2026-10-25 10:50 UTC219
    if report.over() && ttl > RECHECK_SECS {
7770b8fcfa8batagen2026-10-25 10:50 UTC220
        return measure(cfg, root, RECHECK_SECS);
7770b8fcfa8batagen2026-10-25 10:50 UTC221
    }
7770b8fcfa8batagen2026-10-25 10:50 UTC222
    report
7770b8fcfa8batagen2026-10-25 10:50 UTC223
}
7770b8fcfa8batagen2026-10-25 10:50 UTC224
7770b8fcfa8batagen2026-10-25 10:50 UTC225
fn measure(cfg: &Config, root: Option<&Path>, max_age: u64) -> Report {
7770b8fcfa8batagen2026-10-25 10:50 UTC226
    let entries = refresh(cfg, root, max_age);
6f5b84fad3efatagen2026-09-27 03:59 UTC227
    let global = entries.values().map(|e| e.size).sum();
6f5b84fad3efatagen2026-09-27 03:59 UTC228
    let project = root.map(|r| {
6f5b84fad3efatagen2026-09-27 03:59 UTC229
        let size = entries
6f5b84fad3efatagen2026-09-27 03:59 UTC230
            .values()
6f5b84fad3efatagen2026-09-27 03:59 UTC231
            .filter(|e| e.root == r)
6f5b84fad3efatagen2026-09-27 03:59 UTC232
            .map(|e| e.size)
6f5b84fad3efatagen2026-09-27 03:59 UTC233
            .sum();
6f5b84fad3efatagen2026-09-27 03:59 UTC234
        (r.to_path_buf(), size)
6f5b84fad3efatagen2026-09-27 03:59 UTC235
    });
6f5b84fad3efatagen2026-09-27 03:59 UTC236
    Report {
6f5b84fad3efatagen2026-09-27 03:59 UTC237
        over_project: project
6f5b84fad3efatagen2026-09-27 03:59 UTC238
            .as_ref()
6f5b84fad3efatagen2026-09-27 03:59 UTC239
            .is_some_and(|(_, s)| *s > cfg.artifacts.project_cap),
6f5b84fad3efatagen2026-09-27 03:59 UTC240
        over_global: global > cfg.artifacts.global_cap,
6f5b84fad3efatagen2026-09-27 03:59 UTC241
        project,
6f5b84fad3efatagen2026-09-27 03:59 UTC242
        global,
6f5b84fad3efatagen2026-09-27 03:59 UTC243
        entries,
6f5b84fad3efatagen2026-09-27 03:59 UTC244
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC245
}
6f5b84fad3efatagen2026-09-27 03:59 UTC246
6f5b84fad3efatagen2026-09-27 03:59 UTC247
fn remove(cfg: &Config, path: &Path, e: &Entry) -> Result<(), String> {
6f5b84fad3efatagen2026-09-27 03:59 UTC248
    let rule = cfg.artifact_rules().find(|r| r.name == e.rule);
6f5b84fad3efatagen2026-09-27 03:59 UTC249
    if let Some(clean) = rule.and_then(|r| r.clean.as_ref())
6f5b84fad3efatagen2026-09-27 03:59 UTC250
        && let Some((prog, args)) = clean.split_first()
6f5b84fad3efatagen2026-09-27 03:59 UTC251
    {
6f5b84fad3efatagen2026-09-27 03:59 UTC252
        let ok = Command::new(prog)
6f5b84fad3efatagen2026-09-27 03:59 UTC253
            .args(args)
6f5b84fad3efatagen2026-09-27 03:59 UTC254
            .current_dir(&e.root)
6f5b84fad3efatagen2026-09-27 03:59 UTC255
            .status()
6f5b84fad3efatagen2026-09-27 03:59 UTC256
            .is_ok_and(|s| s.success());
6f5b84fad3efatagen2026-09-27 03:59 UTC257
        return if ok {
6f5b84fad3efatagen2026-09-27 03:59 UTC258
            Ok(())
6f5b84fad3efatagen2026-09-27 03:59 UTC259
        } else {
6f5b84fad3efatagen2026-09-27 03:59 UTC260
            Err(format!("`{}` failed", clean.join(" ")))
6f5b84fad3efatagen2026-09-27 03:59 UTC261
        };
6f5b84fad3efatagen2026-09-27 03:59 UTC262
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC263
    let meta = fs::symlink_metadata(path).map_err(|err| err.to_string())?;
6f5b84fad3efatagen2026-09-27 03:59 UTC264
    if meta.is_dir() {
6f5b84fad3efatagen2026-09-27 03:59 UTC265
        fs::remove_dir_all(path)
6f5b84fad3efatagen2026-09-27 03:59 UTC266
    } else {
6f5b84fad3efatagen2026-09-27 03:59 UTC267
        fs::remove_file(path)
6f5b84fad3efatagen2026-09-27 03:59 UTC268
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC269
    .map_err(|err| format!("{}: {err}", path.display()))
6f5b84fad3efatagen2026-09-27 03:59 UTC270
}
6f5b84fad3efatagen2026-09-27 03:59 UTC271
6f5b84fad3efatagen2026-09-27 03:59 UTC272
pub struct Gc {
6f5b84fad3efatagen2026-09-27 03:59 UTC273
    pub freed: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC274
    pub removed: Vec<PathBuf>,
6f5b84fad3efatagen2026-09-27 03:59 UTC275
    pub failed: Vec<String>,
6f5b84fad3efatagen2026-09-27 03:59 UTC276
}
6f5b84fad3efatagen2026-09-27 03:59 UTC277
e9a5f5a41749atagen2026-10-25 11:09 UTC278
pub fn gc(cfg: &Config, project: Option<&Path>, to_cap: bool, dry_run: bool) -> Gc {
e9a5f5a41749atagen2026-10-25 11:09 UTC279
    let max_age = if to_cap {
e9a5f5a41749atagen2026-10-25 11:09 UTC280
        RECHECK_SECS
e9a5f5a41749atagen2026-10-25 11:09 UTC281
    } else {
e9a5f5a41749atagen2026-10-25 11:09 UTC282
        cfg.artifacts.size_ttl.0.as_secs()
e9a5f5a41749atagen2026-10-25 11:09 UTC283
    };
e9a5f5a41749atagen2026-10-25 11:09 UTC284
    let entries = refresh(cfg, project, max_age);
6f5b84fad3efatagen2026-09-27 03:59 UTC285
    let active = active_roots();
6f5b84fad3efatagen2026-09-27 03:59 UTC286
    let idle_after = cfg.artifacts.idle_after.0.as_secs();
e9a5f5a41749atagen2026-10-25 11:09 UTC287
    let movable =
e9a5f5a41749atagen2026-10-25 11:09 UTC288
        |e: &Entry| !e.pinned && !active.contains(&e.root) && project.is_none_or(|r| e.root == r);
6f5b84fad3efatagen2026-09-27 03:59 UTC289
6f5b84fad3efatagen2026-09-27 03:59 UTC290
    let mut doomed: Vec<PathBuf> = entries
6f5b84fad3efatagen2026-09-27 03:59 UTC291
        .iter()
6f5b84fad3efatagen2026-09-27 03:59 UTC292
        .filter(|(_, e)| movable(e) && now().saturating_sub(e.last_used) > idle_after)
6f5b84fad3efatagen2026-09-27 03:59 UTC293
        .map(|(p, _)| p.clone())
6f5b84fad3efatagen2026-09-27 03:59 UTC294
        .collect();
6f5b84fad3efatagen2026-09-27 03:59 UTC295
6f5b84fad3efatagen2026-09-27 03:59 UTC296
    if to_cap {
6f5b84fad3efatagen2026-09-27 03:59 UTC297
        let mut global: Bytes = entries
6f5b84fad3efatagen2026-09-27 03:59 UTC298
            .iter()
6f5b84fad3efatagen2026-09-27 03:59 UTC299
            .filter(|(p, _)| !doomed.contains(p))
6f5b84fad3efatagen2026-09-27 03:59 UTC300
            .map(|(_, e)| e.size)
6f5b84fad3efatagen2026-09-27 03:59 UTC301
            .sum();
6f5b84fad3efatagen2026-09-27 03:59 UTC302
        let mut per_root: BTreeMap<&Path, Bytes> = BTreeMap::new();
6f5b84fad3efatagen2026-09-27 03:59 UTC303
        for (p, e) in &entries {
6f5b84fad3efatagen2026-09-27 03:59 UTC304
            if !doomed.contains(p) {
6f5b84fad3efatagen2026-09-27 03:59 UTC305
                *per_root.entry(&e.root).or_default() += e.size;
6f5b84fad3efatagen2026-09-27 03:59 UTC306
            }
6f5b84fad3efatagen2026-09-27 03:59 UTC307
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC308
        let mut lru: Vec<(&PathBuf, &Entry)> = entries
6f5b84fad3efatagen2026-09-27 03:59 UTC309
            .iter()
6f5b84fad3efatagen2026-09-27 03:59 UTC310
            .filter(|(p, e)| movable(e) && !doomed.contains(p))
6f5b84fad3efatagen2026-09-27 03:59 UTC311
            .collect();
6f5b84fad3efatagen2026-09-27 03:59 UTC312
        lru.sort_by_key(|(_, e)| e.last_used);
6f5b84fad3efatagen2026-09-27 03:59 UTC313
        for (p, e) in lru {
6f5b84fad3efatagen2026-09-27 03:59 UTC314
            let root_size = per_root.get(e.root.as_path()).copied().unwrap_or_default();
6f5b84fad3efatagen2026-09-27 03:59 UTC315
            if global <= cfg.artifacts.global_cap && root_size <= cfg.artifacts.project_cap {
6f5b84fad3efatagen2026-09-27 03:59 UTC316
                continue;
6f5b84fad3efatagen2026-09-27 03:59 UTC317
            }
6f5b84fad3efatagen2026-09-27 03:59 UTC318
            global = global.saturating_sub(e.size);
6f5b84fad3efatagen2026-09-27 03:59 UTC319
            per_root.insert(&e.root, root_size.saturating_sub(e.size));
6f5b84fad3efatagen2026-09-27 03:59 UTC320
            doomed.push(p.clone());
6f5b84fad3efatagen2026-09-27 03:59 UTC321
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC322
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC323
6f5b84fad3efatagen2026-09-27 03:59 UTC324
    let mut out = Gc {
6f5b84fad3efatagen2026-09-27 03:59 UTC325
        freed: Bytes::ZERO,
6f5b84fad3efatagen2026-09-27 03:59 UTC326
        removed: Vec::new(),
6f5b84fad3efatagen2026-09-27 03:59 UTC327
        failed: Vec::new(),
6f5b84fad3efatagen2026-09-27 03:59 UTC328
    };
6f5b84fad3efatagen2026-09-27 03:59 UTC329
    for path in doomed {
6f5b84fad3efatagen2026-09-27 03:59 UTC330
        let e = &entries[&path];
6f5b84fad3efatagen2026-09-27 03:59 UTC331
        if !dry_run && let Err(err) = remove(cfg, &path, e) {
6f5b84fad3efatagen2026-09-27 03:59 UTC332
            out.failed.push(err);
6f5b84fad3efatagen2026-09-27 03:59 UTC333
            continue;
6f5b84fad3efatagen2026-09-27 03:59 UTC334
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC335
        out.freed += e.size;
6f5b84fad3efatagen2026-09-27 03:59 UTC336
        out.removed.push(path);
6f5b84fad3efatagen2026-09-27 03:59 UTC337
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC338
    if !dry_run {
6f5b84fad3efatagen2026-09-27 03:59 UTC339
        let removed = out.removed.clone();
6f5b84fad3efatagen2026-09-27 03:59 UTC340
        let _ = store::update("artifacts", |reg: &mut Registry| {
6f5b84fad3efatagen2026-09-27 03:59 UTC341
            for p in &removed {
6f5b84fad3efatagen2026-09-27 03:59 UTC342
                reg.remove(p);
6f5b84fad3efatagen2026-09-27 03:59 UTC343
            }
6f5b84fad3efatagen2026-09-27 03:59 UTC344
        });
6f5b84fad3efatagen2026-09-27 03:59 UTC345
        nix_gc(cfg, &mut out);
6f5b84fad3efatagen2026-09-27 03:59 UTC346
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC347
    out
6f5b84fad3efatagen2026-09-27 03:59 UTC348
}
6f5b84fad3efatagen2026-09-27 03:59 UTC349
6f5b84fad3efatagen2026-09-27 03:59 UTC350
fn nix_gc(cfg: &Config, out: &mut Gc) {
6f5b84fad3efatagen2026-09-27 03:59 UTC351
    if !cfg.artifacts.nix_gc {
6f5b84fad3efatagen2026-09-27 03:59 UTC352
        return;
6f5b84fad3efatagen2026-09-27 03:59 UTC353
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC354
    let Some(store) = sys::fs(Path::new("/nix/store")) else {
6f5b84fad3efatagen2026-09-27 03:59 UTC355
        return;
6f5b84fad3efatagen2026-09-27 03:59 UTC356
    };
6f5b84fad3efatagen2026-09-27 03:59 UTC357
    let want = cfg.artifacts.nix_min_free.saturating_sub(store.free);
6f5b84fad3efatagen2026-09-27 03:59 UTC358
    if want == Bytes::ZERO {
6f5b84fad3efatagen2026-09-27 03:59 UTC359
        return;
6f5b84fad3efatagen2026-09-27 03:59 UTC360
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC361
    let ok = Command::new("nix")
6f5b84fad3efatagen2026-09-27 03:59 UTC362
        .args(["store", "gc", "--max", &want.0.to_string()])
6f5b84fad3efatagen2026-09-27 03:59 UTC363
        .status()
6f5b84fad3efatagen2026-09-27 03:59 UTC364
        .is_ok_and(|s| s.success());
6f5b84fad3efatagen2026-09-27 03:59 UTC365
    if ok {
6f5b84fad3efatagen2026-09-27 03:59 UTC366
        let after = sys::fs(Path::new("/nix/store")).map_or(store.free, |s| s.free);
6f5b84fad3efatagen2026-09-27 03:59 UTC367
        out.freed += after.saturating_sub(store.free);
6f5b84fad3efatagen2026-09-27 03:59 UTC368
    } else {
6f5b84fad3efatagen2026-09-27 03:59 UTC369
        out.failed.push("nix store gc failed".into());
6f5b84fad3efatagen2026-09-27 03:59 UTC370
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC371
}