|  | 
|  | 1 | +use std::collections::BTreeSet; | 
|  | 2 | +use std::env; | 
|  | 3 | +use std::path::Path; | 
|  | 4 | + | 
|  | 5 | +use crate::config::split_to_cloned_by_ws; | 
|  | 6 | +use crate::errors::*; | 
|  | 7 | +use crate::file; | 
|  | 8 | + | 
|  | 9 | +type Table = toml::value::Table; | 
|  | 10 | +type Value = toml::value::Value; | 
|  | 11 | + | 
|  | 12 | +// the strategy is to merge, with arrays merging together | 
|  | 13 | +// and the deeper the config file is, the higher its priority. | 
|  | 14 | +// arrays merge, numbers/strings get replaced, objects merge in. | 
|  | 15 | +// we don't want to make any assumptions about the cargo | 
|  | 16 | +// config data, in case we need to use it later. | 
|  | 17 | +#[derive(Debug, Clone, Default)] | 
|  | 18 | +pub struct CargoToml(Table); | 
|  | 19 | + | 
|  | 20 | +impl CargoToml { | 
|  | 21 | +    fn parse(path: &Path) -> Result<CargoToml> { | 
|  | 22 | +        let contents = file::read(&path) | 
|  | 23 | +            .wrap_err_with(|| format!("could not read cargo config file at `{path:?}`"))?; | 
|  | 24 | +        Ok(CargoToml(toml::from_str(&contents)?)) | 
|  | 25 | +    } | 
|  | 26 | + | 
|  | 27 | +    pub fn to_toml(&self) -> Result<String> { | 
|  | 28 | +        toml::to_string(&self.0).map_err(Into::into) | 
|  | 29 | +    } | 
|  | 30 | + | 
|  | 31 | +    // finding cargo config files actually runs from the | 
|  | 32 | +    // current working directory the command is invoked, | 
|  | 33 | +    // not from the project root. same is true with work | 
|  | 34 | +    // spaces: the project layout does not matter. | 
|  | 35 | +    pub fn read() -> Result<Option<CargoToml>> { | 
|  | 36 | +        // note: cargo supports both `config` and `config.toml` | 
|  | 37 | +        // `config` exists for compatibility reasons, but if | 
|  | 38 | +        // present, only it will be read. | 
|  | 39 | +        let read = |dir: &Path| -> Result<Option<CargoToml>> { | 
|  | 40 | +            let noext = dir.join("config"); | 
|  | 41 | +            let ext = dir.join("config.toml"); | 
|  | 42 | +            if noext.exists() { | 
|  | 43 | +                Ok(Some(CargoToml::parse(&noext)?)) | 
|  | 44 | +            } else if ext.exists() { | 
|  | 45 | +                Ok(Some(CargoToml::parse(&ext)?)) | 
|  | 46 | +            } else { | 
|  | 47 | +                Ok(None) | 
|  | 48 | +            } | 
|  | 49 | +        }; | 
|  | 50 | + | 
|  | 51 | +        let read_and_merge = |result: &mut Option<CargoToml>, dir: &Path| -> Result<()> { | 
|  | 52 | +            let parent = read(dir)?; | 
|  | 53 | +            // can't use a match, since there's a use-after-move issue | 
|  | 54 | +            match (result.as_mut(), parent) { | 
|  | 55 | +                (Some(r), Some(p)) => r.merge(&p)?, | 
|  | 56 | +                (None, Some(p)) => *result = Some(p), | 
|  | 57 | +                (Some(_), None) | (None, None) => (), | 
|  | 58 | +            } | 
|  | 59 | + | 
|  | 60 | +            Ok(()) | 
|  | 61 | +        }; | 
|  | 62 | + | 
|  | 63 | +        let mut result = None; | 
|  | 64 | +        let cwd = env::current_dir()?; | 
|  | 65 | +        let mut dir: &Path = &cwd; | 
|  | 66 | +        loop { | 
|  | 67 | +            read_and_merge(&mut result, &dir.join(".cargo"))?; | 
|  | 68 | +            let parent_dir = dir.parent(); | 
|  | 69 | +            match parent_dir { | 
|  | 70 | +                Some(path) => dir = path, | 
|  | 71 | +                None => break, | 
|  | 72 | +            } | 
|  | 73 | +        } | 
|  | 74 | + | 
|  | 75 | +        read_and_merge(&mut result, &home::cargo_home()?)?; | 
|  | 76 | + | 
|  | 77 | +        Ok(result) | 
|  | 78 | +    } | 
|  | 79 | + | 
|  | 80 | +    fn merge(&mut self, parent: &CargoToml) -> Result<()> { | 
|  | 81 | +        // can error on mismatched-data | 
|  | 82 | + | 
|  | 83 | +        fn validate_types(x: &Value, y: &Value) -> Option<()> { | 
|  | 84 | +            match x.same_type(y) { | 
|  | 85 | +                true => Some(()), | 
|  | 86 | +                false => None, | 
|  | 87 | +            } | 
|  | 88 | +        } | 
|  | 89 | + | 
|  | 90 | +        // merge 2 tables. x has precedence over y. | 
|  | 91 | +        fn merge_tables(x: &mut Table, y: &Table) -> Option<()> { | 
|  | 92 | +            // we need to iterate over both keys, so we need a full deduplication | 
|  | 93 | +            let keys: BTreeSet<String> = x.keys().chain(y.keys()).cloned().collect(); | 
|  | 94 | +            for key in keys { | 
|  | 95 | +                let in_x = x.contains_key(&key); | 
|  | 96 | +                let in_y = y.contains_key(&key); | 
|  | 97 | +                match (in_x, in_y) { | 
|  | 98 | +                    (true, true) => { | 
|  | 99 | +                        // need to do our merge strategy | 
|  | 100 | +                        let xk = x.get_mut(&key)?; | 
|  | 101 | +                        let yk = y.get(&key)?; | 
|  | 102 | +                        validate_types(xk, yk)?; | 
|  | 103 | + | 
|  | 104 | +                        // now we've filtered out missing keys and optional values | 
|  | 105 | +                        // all key/value pairs should be same type. | 
|  | 106 | +                        if xk.is_table() { | 
|  | 107 | +                            merge_tables(xk.as_table_mut()?, yk.as_table()?)?; | 
|  | 108 | +                        } else if xk.is_array() { | 
|  | 109 | +                            xk.as_array_mut()?.extend_from_slice(yk.as_array()?); | 
|  | 110 | +                        } | 
|  | 111 | +                    } | 
|  | 112 | +                    (false, true) => { | 
|  | 113 | +                        // key in y is not in x: copy it over | 
|  | 114 | +                        let yk = y[&key].clone(); | 
|  | 115 | +                        x.insert(key, yk); | 
|  | 116 | +                    } | 
|  | 117 | +                    // key isn't present in y: can ignore it | 
|  | 118 | +                    (_, false) => (), | 
|  | 119 | +                } | 
|  | 120 | +            } | 
|  | 121 | + | 
|  | 122 | +            Some(()) | 
|  | 123 | +        } | 
|  | 124 | + | 
|  | 125 | +        merge_tables(&mut self.0, &parent.0).ok_or_else(|| eyre::eyre!("could not merge")) | 
|  | 126 | +    } | 
|  | 127 | + | 
|  | 128 | +    pub fn alias(&self, name: &str) -> Result<Option<Vec<String>>> { | 
|  | 129 | +        let parse_alias = |value: &Value| -> Result<Vec<String>> { | 
|  | 130 | +            if let Some(s) = value.as_str() { | 
|  | 131 | +                Ok(split_to_cloned_by_ws(s)) | 
|  | 132 | +            } else if let Some(a) = value.as_array() { | 
|  | 133 | +                a.iter() | 
|  | 134 | +                    .map(|i| { | 
|  | 135 | +                        i.as_str() | 
|  | 136 | +                            .map(ToOwned::to_owned) | 
|  | 137 | +                            .ok_or_else(|| eyre::eyre!("invalid alias type, got {value}")) | 
|  | 138 | +                    }) | 
|  | 139 | +                    .collect() | 
|  | 140 | +            } else { | 
|  | 141 | +                eyre::bail!("invalid alias type, got {}", value.type_str()); | 
|  | 142 | +            } | 
|  | 143 | +        }; | 
|  | 144 | + | 
|  | 145 | +        let alias = match self.0.get("alias") { | 
|  | 146 | +            Some(a) => a, | 
|  | 147 | +            None => return Ok(None), | 
|  | 148 | +        }; | 
|  | 149 | +        let table = match alias.as_table() { | 
|  | 150 | +            Some(t) => t, | 
|  | 151 | +            None => eyre::bail!("cargo config aliases must be a table"), | 
|  | 152 | +        }; | 
|  | 153 | + | 
|  | 154 | +        match table.get(name) { | 
|  | 155 | +            Some(v) => Ok(Some(parse_alias(v)?)), | 
|  | 156 | +            None => Ok(None), | 
|  | 157 | +        } | 
|  | 158 | +    } | 
|  | 159 | +} | 
|  | 160 | + | 
|  | 161 | +#[cfg(test)] | 
|  | 162 | +mod tests { | 
|  | 163 | +    use super::*; | 
|  | 164 | + | 
|  | 165 | +    macro_rules! s { | 
|  | 166 | +        ($s:literal) => { | 
|  | 167 | +            $s.to_owned() | 
|  | 168 | +        }; | 
|  | 169 | +    } | 
|  | 170 | + | 
|  | 171 | +    #[test] | 
|  | 172 | +    fn test_parse() -> Result<()> { | 
|  | 173 | +        let config1 = CargoToml(toml::from_str(CARGO_TOML1)?); | 
|  | 174 | +        let config2 = CargoToml(toml::from_str(CARGO_TOML2)?); | 
|  | 175 | +        assert_eq!(config1.alias("foo")?, Some(vec![s!("build"), s!("foo")])); | 
|  | 176 | +        assert_eq!(config1.alias("bar")?, Some(vec![s!("check"), s!("bar")])); | 
|  | 177 | +        assert_eq!(config2.alias("baz")?, Some(vec![s!("test"), s!("baz")])); | 
|  | 178 | +        assert_eq!(config2.alias("bar")?, Some(vec![s!("init"), s!("bar")])); | 
|  | 179 | +        assert_eq!(config1.alias("far")?, None); | 
|  | 180 | +        assert_eq!(config2.alias("far")?, None); | 
|  | 181 | + | 
|  | 182 | +        let mut merged = config1; | 
|  | 183 | +        merged.merge(&config2)?; | 
|  | 184 | +        assert_eq!(merged.alias("foo")?, Some(vec![s!("build"), s!("foo")])); | 
|  | 185 | +        assert_eq!(merged.alias("baz")?, Some(vec![s!("test"), s!("baz")])); | 
|  | 186 | +        assert_eq!(merged.alias("bar")?, Some(vec![s!("check"), s!("bar")])); | 
|  | 187 | + | 
|  | 188 | +        // check our merge went well, with arrays, etc. | 
|  | 189 | +        assert_eq!( | 
|  | 190 | +            merged | 
|  | 191 | +                .0 | 
|  | 192 | +                .get("build") | 
|  | 193 | +                .and_then(|x| x.get("jobs")) | 
|  | 194 | +                .and_then(|x| x.as_integer()), | 
|  | 195 | +            Some(2), | 
|  | 196 | +        ); | 
|  | 197 | +        assert_eq!( | 
|  | 198 | +            merged | 
|  | 199 | +                .0 | 
|  | 200 | +                .get("build") | 
|  | 201 | +                .and_then(|x| x.get("rustflags")) | 
|  | 202 | +                .and_then(|x| x.as_array()) | 
|  | 203 | +                .and_then(|x| x.iter().map(|i| i.as_str()).collect()), | 
|  | 204 | +            Some(vec!["-C lto", "-Zbuild-std", "-Zdoctest-xcompile"]), | 
|  | 205 | +        ); | 
|  | 206 | + | 
|  | 207 | +        Ok(()) | 
|  | 208 | +    } | 
|  | 209 | + | 
|  | 210 | +    #[test] | 
|  | 211 | +    fn test_read() -> Result<()> { | 
|  | 212 | +        let config = CargoToml::read()?.expect("cross must have cargo config."); | 
|  | 213 | +        assert_eq!( | 
|  | 214 | +            config.alias("build-docker-image")?, | 
|  | 215 | +            Some(vec![s!("xtask"), s!("build-docker-image")]) | 
|  | 216 | +        ); | 
|  | 217 | +        assert_eq!( | 
|  | 218 | +            config.alias("xtask")?, | 
|  | 219 | +            Some(vec![s!("run"), s!("-p"), s!("xtask"), s!("--")]) | 
|  | 220 | +        ); | 
|  | 221 | + | 
|  | 222 | +        Ok(()) | 
|  | 223 | +    } | 
|  | 224 | + | 
|  | 225 | +    const CARGO_TOML1: &str = r#" | 
|  | 226 | +[alias] | 
|  | 227 | +foo = "build foo" | 
|  | 228 | +bar = "check bar" | 
|  | 229 | +
 | 
|  | 230 | +[build] | 
|  | 231 | +jobs = 2 | 
|  | 232 | +rustc-wrapper = "sccache" | 
|  | 233 | +target = "x86_64-unknown-linux-gnu" | 
|  | 234 | +rustflags = ["-C lto", "-Zbuild-std"] | 
|  | 235 | +incremental = true | 
|  | 236 | +
 | 
|  | 237 | +[doc] | 
|  | 238 | +browser = "firefox" | 
|  | 239 | +
 | 
|  | 240 | +[env] | 
|  | 241 | +VAR1 = "VAL1" | 
|  | 242 | +VAR2 = { value = "VAL2", force = true } | 
|  | 243 | +VAR3 = { value = "relative/path", relative = true } | 
|  | 244 | +"#; | 
|  | 245 | + | 
|  | 246 | +    const CARGO_TOML2: &str = r#" | 
|  | 247 | +# want to check tables merge | 
|  | 248 | +# want to check arrays concat | 
|  | 249 | +# want to check rest override | 
|  | 250 | +[alias] | 
|  | 251 | +baz = "test baz" | 
|  | 252 | +bar = "init bar" | 
|  | 253 | +
 | 
|  | 254 | +[build] | 
|  | 255 | +jobs = 4 | 
|  | 256 | +rustc-wrapper = "sccache" | 
|  | 257 | +target = "x86_64-unknown-linux-gnu" | 
|  | 258 | +rustflags = ["-Zdoctest-xcompile"] | 
|  | 259 | +incremental = true | 
|  | 260 | +
 | 
|  | 261 | +[doc] | 
|  | 262 | +browser = "chromium" | 
|  | 263 | +
 | 
|  | 264 | +[env] | 
|  | 265 | +VAR1 = "NEW1" | 
|  | 266 | +VAR2 = { value = "VAL2", force = false } | 
|  | 267 | +"#; | 
|  | 268 | +} | 
0 commit comments