cargo_update/ops/
config.rs

1use std::fmt::{Formatter as FFormatter, Result as FResult, Write as FWrite};
2use serde::{Deserializer, Deserialize, Serializer, Serialize};
3use std::collections::{BTreeMap, BTreeSet};
4use std::io::ErrorKind as IoErrorKind;
5use std::process::Command;
6use std::default::Default;
7use semver::VersionReq;
8use serde_json as json;
9use std::borrow::Cow;
10use std::path::Path;
11use serde::de;
12use std::fs;
13use toml;
14
15
16/// A single operation to be executed upon configuration of a package.
17#[derive(Debug, Clone, Hash, PartialEq, Eq)]
18pub enum ConfigOperation {
19    /// Set the toolchain to use to compile the package.
20    SetToolchain(String),
21    /// Use the default toolchain to use to compile the package.
22    RemoveToolchain,
23    /// Whether to compile the package with the default features.
24    DefaultFeatures(bool),
25    /// Compile the package with the specified feature.
26    AddFeature(String),
27    /// Remove the feature from the list of features to compile with.
28    RemoveFeature(String),
29    /// Set build profile (`dev`/`release`/*~/.cargo/config.toml* `[profile.gaming]`/&c.)
30    SetBuildProfile(Cow<'static, str>),
31    /// Set allowing to install prereleases to the specified value.
32    SetInstallPrereleases(bool),
33    /// Set enforcing Cargo.lock to the specified value.
34    SetEnforceLock(bool),
35    /// Set installing only the pre-set binaries.
36    SetRespectBinaries(bool),
37    /// Constrain the installed to the specified one.
38    SetTargetVersion(VersionReq),
39    /// Always install latest package version.
40    RemoveTargetVersion,
41    /// Set environment variable to given value for `cargo install`.
42    SetEnvironment(String, String),
43    /// Remove environment variable for `cargo install`.
44    ClearEnvironment(String),
45    /// Remove configuration for an environment variable.
46    InheritEnvironment(String),
47    /// Reset configuration to default values.
48    ResetConfig,
49}
50
51
52/// Compilation configuration for one crate.
53///
54/// # Examples
55///
56/// Reading a configset, adding an entry to it, then writing it back.
57///
58/// ```
59/// # use cargo_update::ops::PackageConfig;
60/// # use std::fs::{File, create_dir_all};
61/// # use std::path::Path;
62/// # use std::env::temp_dir;
63/// # let td = temp_dir().join("cargo_update-doctest").join("PackageConfig-0");
64/// # create_dir_all(&td).unwrap();
65/// # let config_file = td.join(".install_config.toml");
66/// # let operations = [];
67/// let mut configuration = PackageConfig::read(&config_file, Path::new("/ENOENT")).unwrap();
68/// configuration.insert("cargo_update".to_string(), PackageConfig::from(&operations));
69/// PackageConfig::write(&configuration, &config_file).unwrap();
70/// ```
71#[derive(Debug, Clone, Hash, Eq, Serialize, Deserialize)]
72pub struct PackageConfig {
73    /// Toolchain to use to compile the package, or `None` for default.
74    pub toolchain: Option<String>,
75    /// Whether to compile the package with the default features.
76    pub default_features: bool,
77    /// Features to compile the package with.
78    pub features: BTreeSet<String>,
79    /// Equivalent to `build_profile = Some("dev")` but binds stronger
80    pub debug: Option<bool>,
81    /// The build profile (`test` or `bench` or one from *~/.cargo/config.toml* `[profile.gaming]`); CANNOT be `dev` (`debug =
82    /// Some(true)`) or `release` (`debug = build_profile = None`)
83    pub build_profile: Option<Cow<'static, str>>,
84    /// Whether to install pre-release versions.
85    pub install_prereleases: Option<bool>,
86    /// Whether to enforce Cargo.lock versions.
87    pub enforce_lock: Option<bool>,
88    /// Whether to install only the pre-configured binaries.
89    pub respect_binaries: Option<bool>,
90    /// Versions to constrain to.
91    pub target_version: Option<VersionReq>,
92    /// Environment variables to alter for cargo. `None` to remove.
93    pub environment: Option<BTreeMap<String, EnvironmentOverride>>,
94    /// Read in from `.crates2.json`, shouldn't be saved
95    #[serde(skip)]
96    pub from_transient: bool,
97}
98impl PartialEq for PackageConfig {
99    fn eq(&self, other: &Self) -> bool {
100        self.toolchain /************/ == other.toolchain && // !
101        self.default_features /*****/ == other.default_features && // !
102        self.features /*************/ == other.features && // !
103        self.debug /****************/ == other.debug && // !
104        self.build_profile /********/ == other.build_profile && // !
105        self.install_prereleases /**/ == other.install_prereleases && // !
106        self.enforce_lock /*********/ == other.enforce_lock && // !
107        self.respect_binaries /*****/ == other.respect_binaries && // !
108        self.target_version /*******/ == other.target_version && // !
109        self.environment /**********/ == other.environment
110        // No from_transient
111    }
112}
113
114
115impl PackageConfig {
116    /// Create a package config based on the default settings and modified according to the specified operations.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// # extern crate cargo_update;
122    /// # extern crate semver;
123    /// # fn main() {
124    /// # use cargo_update::ops::{EnvironmentOverride, ConfigOperation, PackageConfig};
125    /// # use std::collections::BTreeSet;
126    /// # use std::collections::BTreeMap;
127    /// # use semver::VersionReq;
128    /// # use std::str::FromStr;
129    /// assert_eq!(PackageConfig::from(&[ConfigOperation::SetToolchain("nightly".to_string()),
130    ///                                  ConfigOperation::DefaultFeatures(false),
131    ///                                  ConfigOperation::AddFeature("rustc-serialize".to_string()),
132    ///                                  ConfigOperation::SetBuildProfile("dev".into()),
133    ///                                  ConfigOperation::SetInstallPrereleases(false),
134    ///                                  ConfigOperation::SetEnforceLock(true),
135    ///                                  ConfigOperation::SetRespectBinaries(true),
136    ///                                  ConfigOperation::SetTargetVersion(VersionReq::from_str(">=0.1").unwrap()),
137    ///                                  ConfigOperation::SetEnvironment("RUSTC_WRAPPER".to_string(), "sccache".to_string()),
138    ///                                  ConfigOperation::ClearEnvironment("CC".to_string())]),
139    ///            PackageConfig {
140    ///                toolchain: Some("nightly".to_string()),
141    ///                default_features: false,
142    ///                features: {
143    ///                    let mut feats = BTreeSet::new();
144    ///                    feats.insert("rustc-serialize".to_string());
145    ///                    feats
146    ///                },
147    ///                debug: Some(true),
148    ///                build_profile: None,
149    ///                install_prereleases: Some(false),
150    ///                enforce_lock: Some(true),
151    ///                respect_binaries: Some(true),
152    ///                target_version: Some(VersionReq::from_str(">=0.1").unwrap()),
153    ///                environment: Some({
154    ///                    let mut vars = BTreeMap::new();
155    ///                    vars.insert("RUSTC_WRAPPER".to_string(), EnvironmentOverride(Some("sccache".to_string())));
156    ///                    vars.insert("CC".to_string(), EnvironmentOverride(None));
157    ///                    vars
158    ///                }),
159    ///                from_transient: false,
160    ///            });
161    /// # }
162    /// ```
163    pub fn from<'o, O: IntoIterator<Item = &'o ConfigOperation>>(ops: O) -> PackageConfig {
164        let mut def = PackageConfig::default();
165        def.execute_operations(ops);
166        def
167    }
168
169    /// Generate cargo arguments from this configuration.
170    ///
171    /// Executable names are stripped of their trailing `".exe"`, if any.
172    ///
173    /// # Examples
174    ///
175    /// ```no_run
176    /// # use cargo_update::ops::PackageConfig;
177    /// # use std::collections::BTreeMap;
178    /// # use std::process::Command;
179    /// # let name = "cargo-update".to_string();
180    /// # let mut configuration = BTreeMap::new();
181    /// # configuration.insert(name.clone(), PackageConfig::from(&[]));
182    /// let cmd = Command::new("cargo")
183    ///               .args(configuration.get(&name).unwrap().cargo_args(&["racer"]).iter().map(AsRef::as_ref))
184    ///               .arg(&name)
185    /// // Process the command further -- run it, for example.
186    /// # .status().unwrap();
187    /// # let _ = cmd;
188    /// ```
189    pub fn cargo_args<S: AsRef<str>, I: IntoIterator<Item = S>>(&self, executables: I) -> Vec<Cow<'static, str>> {
190        let mut res = vec![];
191        if let Some(ref t) = self.toolchain {
192            res.push(format!("+{}", t).into());
193        }
194        res.push("install".into());
195        res.push("-f".into());
196        if !self.default_features {
197            res.push("--no-default-features".into());
198        }
199        if !self.features.is_empty() {
200            res.push("--features".into());
201            let mut a = String::new();
202            for f in &self.features {
203                write!(a, "{} ", f).unwrap();
204            }
205            res.push(a.into());
206        }
207        if let Some(true) = self.enforce_lock {
208            res.push("--locked".into());
209        }
210        if let Some(true) = self.respect_binaries {
211            for x in executables {
212                let x = x.as_ref();
213
214                res.push("--bin".into());
215                res.push(x.strip_suffix(".exe").unwrap_or(x).to_string().into());
216            }
217        }
218        if let Some(true) = self.debug {
219            res.push("--debug".into());
220        } else if let Some(prof) = self.build_profile.as_ref() {
221            res.push("--profile".into());
222            res.push(prof.clone());
223        }
224        res
225    }
226
227    /// Apply transformations from `self.environment` to `cmd`.
228    pub fn environmentalise<'c>(&self, cmd: &'c mut Command) -> &'c mut Command {
229        if let Some(env) = self.environment.as_ref() {
230            for (var, val) in env {
231                match val {
232                    EnvironmentOverride(Some(val)) => cmd.env(var, val),
233                    EnvironmentOverride(None) => cmd.env_remove(var),
234                };
235            }
236        }
237        cmd
238    }
239
240    /// Modify `self` according to the specified set of operations.
241    ///
242    /// If this config was transient (read in from `.crates2.json`), it is made real and will be saved.
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// # extern crate cargo_update;
248    /// # extern crate semver;
249    /// # fn main() {
250    /// # use cargo_update::ops::{ConfigOperation, PackageConfig};
251    /// # use std::collections::BTreeSet;
252    /// # use semver::VersionReq;
253    /// # use std::str::FromStr;
254    /// let mut cfg = PackageConfig {
255    ///     toolchain: Some("nightly".to_string()),
256    ///     default_features: false,
257    ///     features: {
258    ///         let mut feats = BTreeSet::new();
259    ///         feats.insert("rustc-serialize".to_string());
260    ///         feats
261    ///     },
262    ///     debug: None,
263    ///     build_profile: None,
264    ///     install_prereleases: None,
265    ///     enforce_lock: None,
266    ///     respect_binaries: None,
267    ///     target_version: Some(VersionReq::from_str(">=0.1").unwrap()),
268    ///     environment: None,
269    ///     from_transient: false,
270    /// };
271    /// cfg.execute_operations(&[ConfigOperation::RemoveToolchain,
272    ///                          ConfigOperation::AddFeature("serde".to_string()),
273    ///                          ConfigOperation::RemoveFeature("rustc-serialize".to_string()),
274    ///                          ConfigOperation::SetBuildProfile("dev".into()),
275    ///                          ConfigOperation::RemoveTargetVersion]);
276    /// assert_eq!(cfg,
277    ///            PackageConfig {
278    ///                toolchain: None,
279    ///                default_features: false,
280    ///                features: {
281    ///                    let mut feats = BTreeSet::new();
282    ///                    feats.insert("serde".to_string());
283    ///                    feats
284    ///                },
285    ///                debug: Some(true),
286    ///                build_profile: None,
287    ///                install_prereleases: None,
288    ///                enforce_lock: None,
289    ///                respect_binaries: None,
290    ///                target_version: None,
291    ///                environment: None,
292    ///                from_transient: false,
293    ///            });
294    /// # }
295    /// ```
296    pub fn execute_operations<'o, O: IntoIterator<Item = &'o ConfigOperation>>(&mut self, ops: O) {
297        self.from_transient = false;
298        for op in ops {
299            self.execute_operation(op)
300        }
301    }
302
303    fn execute_operation(&mut self, op: &ConfigOperation) {
304        match op {
305            ConfigOperation::SetToolchain(ref tchn) => self.toolchain = Some(tchn.clone()),
306            ConfigOperation::RemoveToolchain => self.toolchain = None,
307            ConfigOperation::DefaultFeatures(f) => self.default_features = *f,
308            ConfigOperation::AddFeature(ref feat) => {
309                self.features.insert(feat.clone());
310            }
311            ConfigOperation::RemoveFeature(ref feat) => {
312                self.features.remove(feat);
313            }
314            ConfigOperation::SetBuildProfile(d) => {
315                self.debug = None;
316                self.build_profile = Some(d.clone());
317                self.normalise();
318            }
319            ConfigOperation::SetInstallPrereleases(pr) => self.install_prereleases = Some(*pr),
320            ConfigOperation::SetEnforceLock(el) => self.enforce_lock = Some(*el),
321            ConfigOperation::SetRespectBinaries(rb) => self.respect_binaries = Some(*rb),
322            ConfigOperation::SetTargetVersion(ref vr) => self.target_version = Some(vr.clone()),
323            ConfigOperation::RemoveTargetVersion => self.target_version = None,
324            ConfigOperation::SetEnvironment(ref var, ref val) => {
325                self.environment.get_or_insert(Default::default()).insert(var.clone(), EnvironmentOverride(Some(val.clone())));
326            }
327            ConfigOperation::ClearEnvironment(ref var) => {
328                self.environment.get_or_insert(Default::default()).insert(var.clone(), EnvironmentOverride(None));
329            }
330            ConfigOperation::InheritEnvironment(ref var) => {
331                self.environment.get_or_insert(Default::default()).remove(var);
332            }
333            ConfigOperation::ResetConfig => *self = Default::default(),
334        }
335    }
336
337    /// Read a configset from the specified file, or from the given `.cargo2.json`.
338    ///
339    /// The first file (usually `.install_config.toml`) is used by default for each package;
340    /// `.cargo2.json`, if any, is used to backfill existing data from cargo.
341    ///
342    /// If the specified file doesn't exist an empty configset is returned.
343    ///
344    /// # Examples
345    ///
346    /// ```
347    /// # use std::collections::{BTreeSet, BTreeMap};
348    /// # use cargo_update::ops::PackageConfig;
349    /// # use std::fs::{self, create_dir_all};
350    /// # use std::env::temp_dir;
351    /// # use std::path::Path;
352    /// # use std::io::Write;
353    /// # let td = temp_dir().join("cargo_update-doctest").join("PackageConfig-read-0");
354    /// # create_dir_all(&td).unwrap();
355    /// # let config_file = td.join(".install_config.toml");
356    /// fs::write(&config_file, &b"\
357    ///    [cargo-update]\n\
358    ///    default_features = true\n\
359    ///    features = [\"serde\"]\n"[..]).unwrap();
360    /// assert_eq!(PackageConfig::read(&config_file, Path::new("/ENOENT")), Ok({
361    ///     let mut pkgs = BTreeMap::new();
362    ///     pkgs.insert("cargo-update".to_string(), PackageConfig {
363    ///         toolchain: None,
364    ///         default_features: true,
365    ///         features: {
366    ///             let mut feats = BTreeSet::new();
367    ///             feats.insert("serde".to_string());
368    ///             feats
369    ///         },
370    ///         debug: None,
371    ///         build_profile: None,
372    ///         install_prereleases: None,
373    ///         enforce_lock: None,
374    ///         respect_binaries: None,
375    ///         target_version: None,
376    ///         environment: None,
377    ///         from_transient: false,
378    ///     });
379    ///     pkgs
380    /// }));
381    /// ```
382    pub fn read(p: &Path, cargo2_json: &Path) -> Result<BTreeMap<String, PackageConfig>, (String, i32)> {
383        let mut base = match fs::read_to_string(p) {
384            Ok(s) => toml::from_str(&s).map_err(|e| (e.to_string(), 2))?,
385            Err(e) if e.kind() == IoErrorKind::NotFound => BTreeMap::new(),
386            Err(e) => Err((e.to_string(), 1))?,
387        };
388        // {
389        //   "installs": {
390        //     "pixelmatch 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)": {
391        //       "version_req": null,
392        //       "bins": [
393        //         "pixelmatch"
394        //       ],
395        //       "features": [
396        //         "build-binary"
397        //       ],
398        //       "all_features": false,
399        //       "no_default_features": false,
400        //       "profile": "release",
401        //       "target": "x86_64-unknown-linux-gnu",
402        //       "rustc": "rustc 1.54.0 (a178d0322 2021-07-26)\nbinary: ..."
403        //     },
404        if let Ok(cargo2_data) = fs::read(cargo2_json) {
405            if let Ok(json::Value::Object(mut cargo2)) = json::from_slice(&cargo2_data[..]) {
406                if let Some(json::Value::Object(installs)) = cargo2.remove("installs") {
407                    for (k, v) in installs {
408                        if let json::Value::Object(v) = v {
409                            if let Some((name, _, _)) = super::parse_registry_package_ident(&k).or_else(|| super::parse_git_package_ident(&k)) {
410                                if !base.contains_key(name) {
411                                    base.insert(name.to_string(), PackageConfig::cargo2_package_config(v));
412                                }
413                            }
414                        }
415                    }
416                }
417            }
418        }
419        for (_, v) in &mut base {
420            v.normalise();
421        }
422        Ok(base)
423    }
424
425    fn normalise(&mut self) {
426        if self.debug.unwrap_or(false) && self.build_profile.is_none() {
427            self.build_profile = Some("dev".into());
428        }
429
430        match self.build_profile.as_deref().unwrap_or("release") {
431            "dev" => {
432                self.debug = Some(true);
433                self.build_profile = None;
434            }
435            "release" => {
436                self.debug = None;
437                self.build_profile = None;
438            }
439            _ => {
440                self.debug = None;
441                // self.build_profile unchanged
442            }
443        }
444    }
445
446    fn cargo2_package_config(mut blob: json::Map<String, json::Value>) -> PackageConfig {
447        let mut ret = PackageConfig::default();
448        ret.from_transient = true;
449
450        // Nothing to parse PackageConfig::toolchain from
451        if let Some(json::Value::Bool(ndf)) = blob.get("no_default_features") {
452            ret.default_features = !ndf;
453        }
454        if let Some(json::Value::Array(fs)) = blob.remove("features") {
455            ret.features = fs.into_iter()
456                .filter_map(|f| match f {
457                    json::Value::String(s) => Some(s),
458                    _ => None,
459                })
460                .collect();
461        }
462        // Nothing to parse "all_features" into
463        if let Some(json::Value::String(prof)) = blob.remove("profile") {
464            ret.build_profile = Some(prof.into());
465        }
466        // Nothing to parse PackageConfig::install_prereleases from
467        // Nothing to parse PackageConfig::enforce_lock from
468        // "bins" is kinda like PackageConfig::respect_binaries but no really
469        // "version_req" is set by cargo install --version, so we'd lock after the first update if we parsed it like this
470        // Nothing to parse PackageConfig::environment from
471        ret
472    }
473
474    /// Save a configset to the specified file, transient (`.crates2.json`) configs are removed.
475    ///
476    /// # Examples
477    ///
478    /// ```
479    /// # use std::collections::{BTreeSet, BTreeMap};
480    /// # use cargo_update::ops::PackageConfig;
481    /// # use std::fs::{self, create_dir_all};
482    /// # use std::env::temp_dir;
483    /// # use std::io::Read;
484    /// # let td = temp_dir().join("cargo_update-doctest").join("PackageConfig-write-0");
485    /// # create_dir_all(&td).unwrap();
486    /// # let config_file = td.join(".install_config.toml");
487    /// PackageConfig::write(&{
488    ///     let mut pkgs = BTreeMap::new();
489    ///     pkgs.insert("cargo-update".to_string(), PackageConfig {
490    ///         toolchain: None,
491    ///         default_features: true,
492    ///         features: {
493    ///             let mut feats = BTreeSet::new();
494    ///             feats.insert("serde".to_string());
495    ///             feats
496    ///         },
497    ///         debug: None,
498    ///         build_profile: None,
499    ///         install_prereleases: None,
500    ///         enforce_lock: None,
501    ///         respect_binaries: None,
502    ///         target_version: None,
503    ///         environment: None,
504    ///         from_transient: false,
505    ///     });
506    ///     pkgs
507    /// }, &config_file).unwrap();
508    ///
509    /// assert_eq!(&fs::read_to_string(&config_file).unwrap(),
510    ///            "[cargo-update]\n\
511    ///             default_features = true\n\
512    ///             features = [\"serde\"]\n");
513    /// ```
514    pub fn write(configuration: &BTreeMap<String, PackageConfig>, p: &Path) -> Result<(), (String, i32)> {
515        fs::write(p, &toml::to_string(&FilteredPackageConfigMap(configuration)).map_err(|e| (e.to_string(), 2))?).map_err(|e| (e.to_string(), 3))
516    }
517}
518
519impl Default for PackageConfig {
520    fn default() -> PackageConfig {
521        PackageConfig {
522            toolchain: None,
523            default_features: true,
524            features: BTreeSet::new(),
525            debug: None,
526            build_profile: None,
527            install_prereleases: None,
528            enforce_lock: None,
529            respect_binaries: None,
530            target_version: None,
531            environment: None,
532            from_transient: false,
533        }
534    }
535}
536
537struct FilteredPackageConfigMap<'a>(pub &'a BTreeMap<String, PackageConfig>);
538impl<'a> Serialize for FilteredPackageConfigMap<'a> {
539    #[inline]
540    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
541        serializer.collect_map(self.0.iter().filter(|(_, v)| !v.from_transient))
542    }
543}
544
545
546/// Wrapper that serialises `None` as a boolean.
547///
548/// serde's default `BTreeMap<String, Option<String>>` implementation simply loses `None` values.
549#[derive(Debug, Clone, Hash, PartialEq, Eq)]
550pub struct EnvironmentOverride(pub Option<String>);
551
552impl<'de> Deserialize<'de> for EnvironmentOverride {
553    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
554        deserializer.deserialize_any(EnvironmentOverrideVisitor)
555    }
556}
557
558impl Serialize for EnvironmentOverride {
559    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
560        match &self.0 {
561            Some(data) => serializer.serialize_str(&data),
562            None => serializer.serialize_bool(false),
563        }
564    }
565}
566
567struct EnvironmentOverrideVisitor;
568
569impl<'de> de::Visitor<'de> for EnvironmentOverrideVisitor {
570    type Value = EnvironmentOverride;
571
572    fn expecting(&self, formatter: &mut FFormatter) -> FResult {
573        write!(formatter, "A string or boolean")
574    }
575
576    fn visit_bool<E: de::Error>(self, _: bool) -> Result<Self::Value, E> {
577        Ok(EnvironmentOverride(None))
578    }
579
580    fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
581        Ok(EnvironmentOverride(Some(s.to_string())))
582    }
583}