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}