Skip to main content

mur_common/config/
turn_track.rs

1//! Per-turn copy-on-write track tuning (`turn_track:`).
2
3use super::*;
4
5/// Commands (first word) that trigger the warm clone.
6pub const TURN_TRACK_DEFAULT_TRIGGERS: &[&str] = &["cargo"];
7/// Project-relative build-cache dirs that get a per-turn target.
8pub const TURN_TRACK_DEFAULT_CACHE_DIRS: &[&str] = &["target"];
9/// Per-profile subdirs the warm clone leaves out (path-keyed, useless in a track).
10pub const TURN_TRACK_DEFAULT_SKIP_SUBDIRS: &[&str] = &["incremental"];
11/// Below this many GiB free, the warm clone is skipped (the build runs cold).
12pub const TURN_TRACK_DEFAULT_MIN_FREE_GIB: u64 = 20;
13/// `<project>/target/<this>/<track-name>/` holds each turn's target dir.
14pub const TURN_TRACK_DEFAULT_TURN_TARGET_SUBDIR: &str = "turns";
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
17pub struct TurnTrackConfig {
18    #[serde(default)]
19    pub warm_build_cache: WarmBuildCacheConfig,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct WarmBuildCacheConfig {
24    #[serde(default = "turn_track_default_enabled")]
25    pub enabled: bool,
26    #[serde(default = "turn_track_default_triggers")]
27    pub triggers: Vec<String>,
28    #[serde(default = "turn_track_default_cache_dirs")]
29    pub cache_dirs: Vec<String>,
30    #[serde(default = "turn_track_default_skip_subdirs")]
31    pub skip_subdirs: Vec<String>,
32    #[serde(default = "turn_track_default_min_free_gib")]
33    pub min_free_gib: u64,
34    /// Joined under `<project>/target/`; must be one normal path component.
35    /// Read it through [`WarmBuildCacheConfig::validated_subdir`].
36    #[serde(default = "turn_track_default_turn_target_subdir")]
37    pub turn_target_subdir: String,
38}
39
40impl WarmBuildCacheConfig {
41    /// `turn_target_subdir` if it is a single normal path component,
42    /// otherwise the default — a bad value must not escape `target/`.
43    pub fn validated_subdir(&self) -> &str {
44        let mut parts = std::path::Path::new(&self.turn_target_subdir).components();
45        match (parts.next(), parts.next()) {
46            (Some(std::path::Component::Normal(_)), None) => &self.turn_target_subdir,
47            _ => TURN_TRACK_DEFAULT_TURN_TARGET_SUBDIR,
48        }
49    }
50}
51
52fn strings(v: &[&str]) -> Vec<String> {
53    v.iter().map(|s| s.to_string()).collect()
54}
55
56fn turn_track_default_enabled() -> bool {
57    true
58}
59
60fn turn_track_default_triggers() -> Vec<String> {
61    strings(TURN_TRACK_DEFAULT_TRIGGERS)
62}
63
64fn turn_track_default_cache_dirs() -> Vec<String> {
65    strings(TURN_TRACK_DEFAULT_CACHE_DIRS)
66}
67
68fn turn_track_default_skip_subdirs() -> Vec<String> {
69    strings(TURN_TRACK_DEFAULT_SKIP_SUBDIRS)
70}
71
72fn turn_track_default_min_free_gib() -> u64 {
73    TURN_TRACK_DEFAULT_MIN_FREE_GIB
74}
75
76fn turn_track_default_turn_target_subdir() -> String {
77    TURN_TRACK_DEFAULT_TURN_TARGET_SUBDIR.to_string()
78}
79
80impl Default for WarmBuildCacheConfig {
81    fn default() -> Self {
82        Self {
83            enabled: turn_track_default_enabled(),
84            triggers: turn_track_default_triggers(),
85            cache_dirs: turn_track_default_cache_dirs(),
86            skip_subdirs: turn_track_default_skip_subdirs(),
87            min_free_gib: turn_track_default_min_free_gib(),
88            turn_target_subdir: turn_track_default_turn_target_subdir(),
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn turn_track_defaults_when_absent() {
99        let c: Config = serde_yaml::from_str("{}").unwrap();
100        let w = &c.turn_track.warm_build_cache;
101        assert!(w.enabled);
102        assert_eq!(w.triggers, vec!["cargo".to_string()]);
103        assert_eq!(w.cache_dirs, vec!["target".to_string()]);
104        assert_eq!(w.skip_subdirs, vec!["incremental".to_string()]);
105        assert_eq!(w.min_free_gib, 20);
106        assert_eq!(w.turn_target_subdir, "turns");
107    }
108
109    #[test]
110    fn turn_track_partial_override() {
111        let c: Config =
112            serde_yaml::from_str("turn_track:\n  warm_build_cache:\n    min_free_gib: 5\n")
113                .unwrap();
114        let w = &c.turn_track.warm_build_cache;
115        assert_eq!(w.min_free_gib, 5);
116        assert!(w.enabled);
117        assert_eq!(w.triggers, vec!["cargo".to_string()]);
118        assert_eq!(w.cache_dirs, vec!["target".to_string()]);
119        assert_eq!(w.skip_subdirs, vec!["incremental".to_string()]);
120        assert_eq!(w.turn_target_subdir, "turns");
121    }
122
123    #[test]
124    fn turn_target_subdir_rejects_path_escape() {
125        let with = |s: &str| WarmBuildCacheConfig {
126            turn_target_subdir: s.to_string(),
127            ..Default::default()
128        };
129        for bad in ["", "..", ".", "a/b", "/abs", "../x"] {
130            assert_eq!(with(bad).validated_subdir(), "turns", "input {bad:?}");
131        }
132        assert_eq!(with("x").validated_subdir(), "x");
133    }
134}