1use std::cell::{RefCell, RefMut};
53use std::collections::hash_map::Entry::{Occupied, Vacant};
54use std::collections::{HashMap, HashSet};
55use std::env;
56use std::fmt;
57use std::fs::{self, File};
58use std::io::prelude::*;
59use std::io::{self, SeekFrom};
60use std::mem;
61use std::path::{Path, PathBuf};
62use std::str::FromStr;
63use std::sync::Once;
64use std::time::Instant;
65
66use anyhow::{anyhow, bail};
67use curl::easy::Easy;
68use lazycell::LazyCell;
69use serde::Deserialize;
70use url::Url;
71
72use self::ConfigValue as CV;
73use crate::core::shell::Verbosity;
74use crate::core::{nightly_features_allowed, CliUnstable, Shell, SourceId, Workspace};
75use crate::ops;
76use crate::util::errors::{CargoResult, CargoResultExt};
77use crate::util::toml as cargo_toml;
78use crate::util::{paths, validate_package_name};
79use crate::util::{FileLock, Filesystem, IntoUrl, IntoUrlWithBase, Rustc};
80
81mod de;
82use de::Deserializer;
83
84mod value;
85pub use value::{Definition, OptValue, Value};
86
87mod key;
88use key::ConfigKey;
89
90mod path;
91pub use path::{ConfigRelativePath, PathAndArgs};
92
93mod target;
94pub use target::{TargetCfgConfig, TargetConfig};
95
96macro_rules! get_value_typed {
98 ($name:ident, $ty:ty, $variant:ident, $expected:expr) => {
99 fn $name(&self, key: &ConfigKey) -> Result<OptValue<$ty>, ConfigError> {
101 let cv = self.get_cv(key)?;
102 let env = self.get_env::<$ty>(key)?;
103 match (cv, env) {
104 (Some(CV::$variant(val, definition)), Some(env)) => {
105 if definition.is_higher_priority(&env.definition) {
106 Ok(Some(Value { val, definition }))
107 } else {
108 Ok(Some(env))
109 }
110 }
111 (Some(CV::$variant(val, definition)), None) => Ok(Some(Value { val, definition })),
112 (Some(cv), _) => Err(ConfigError::expected(key, $expected, &cv)),
113 (None, Some(env)) => Ok(Some(env)),
114 (None, None) => Ok(None),
115 }
116 }
117 };
118}
119
120#[derive(Debug)]
123pub struct Config {
124 home_path: Filesystem,
126 shell: RefCell<Shell>,
128 values: LazyCell<HashMap<String, ConfigValue>>,
130 cli_config: Option<Vec<String>>,
132 cwd: PathBuf,
134 cargo_exe: LazyCell<PathBuf>,
136 rustdoc: LazyCell<PathBuf>,
138 extra_verbose: bool,
140 frozen: bool,
143 locked: bool,
146 offline: bool,
149 jobserver: Option<jobserver::Client>,
151 unstable_flags: CliUnstable,
153 easy: LazyCell<RefCell<Easy>>,
155 crates_io_source_id: LazyCell<SourceId>,
157 cache_rustc_info: bool,
159 creation_time: Instant,
161 target_dir: Option<Filesystem>,
163 env: HashMap<String, String>,
165 updated_sources: LazyCell<RefCell<HashSet<SourceId>>>,
167 package_cache_lock: RefCell<Option<(Option<FileLock>, usize)>>,
170 http_config: LazyCell<CargoHttpConfig>,
172 net_config: LazyCell<CargoNetConfig>,
173 build_config: LazyCell<CargoBuildConfig>,
174 target_cfgs: LazyCell<Vec<(String, TargetCfgConfig)>>,
175}
176
177impl Config {
178 pub fn new(shell: Shell, cwd: PathBuf, homedir: PathBuf) -> Config {
186 static mut GLOBAL_JOBSERVER: *mut jobserver::Client = 0 as *mut _;
187 static INIT: Once = Once::new();
188
189 INIT.call_once(|| unsafe {
192 if let Some(client) = jobserver::Client::from_env() {
193 GLOBAL_JOBSERVER = Box::into_raw(Box::new(client));
194 }
195 });
196
197 let env: HashMap<_, _> = env::vars_os()
198 .filter_map(|(k, v)| {
199 match (k.into_string(), v.into_string()) {
201 (Ok(k), Ok(v)) => Some((k, v)),
202 _ => None,
203 }
204 })
205 .collect();
206
207 let cache_rustc_info = match env.get("CARGO_CACHE_RUSTC_INFO") {
208 Some(cache) => cache != "0",
209 _ => true,
210 };
211
212 Config {
213 home_path: Filesystem::new(homedir),
214 shell: RefCell::new(shell),
215 cwd,
216 values: LazyCell::new(),
217 cli_config: None,
218 cargo_exe: LazyCell::new(),
219 rustdoc: LazyCell::new(),
220 extra_verbose: false,
221 frozen: false,
222 locked: false,
223 offline: false,
224 jobserver: unsafe {
225 if GLOBAL_JOBSERVER.is_null() {
226 None
227 } else {
228 Some((*GLOBAL_JOBSERVER).clone())
229 }
230 },
231 unstable_flags: CliUnstable::default(),
232 easy: LazyCell::new(),
233 crates_io_source_id: LazyCell::new(),
234 cache_rustc_info,
235 creation_time: Instant::now(),
236 target_dir: None,
237 env,
238 updated_sources: LazyCell::new(),
239 package_cache_lock: RefCell::new(None),
240 http_config: LazyCell::new(),
241 net_config: LazyCell::new(),
242 build_config: LazyCell::new(),
243 target_cfgs: LazyCell::new(),
244 }
245 }
246
247 pub fn default() -> CargoResult<Config> {
252 let shell = Shell::new();
253 let cwd =
254 env::current_dir().chain_err(|| "couldn't get the current directory of the process")?;
255 let homedir = homedir(&cwd).ok_or_else(|| {
256 anyhow!(
257 "Cargo couldn't find your home directory. \
258 This probably means that $HOME was not set."
259 )
260 })?;
261 Ok(Config::new(shell, cwd, homedir))
262 }
263
264 pub fn home(&self) -> &Filesystem {
266 &self.home_path
267 }
268
269 pub fn git_path(&self) -> Filesystem {
271 self.home_path.join("git")
272 }
273
274 pub fn registry_index_path(&self) -> Filesystem {
276 self.home_path.join("registry").join("index")
277 }
278
279 pub fn registry_cache_path(&self) -> Filesystem {
281 self.home_path.join("registry").join("cache")
282 }
283
284 pub fn registry_source_path(&self) -> Filesystem {
286 self.home_path.join("registry").join("src")
287 }
288
289 pub fn default_registry(&self) -> CargoResult<Option<String>> {
291 Ok(match self.get_string("registry.default")? {
292 Some(registry) => Some(registry.val),
293 None => None,
294 })
295 }
296
297 pub fn shell(&self) -> RefMut<'_, Shell> {
299 self.shell.borrow_mut()
300 }
301
302 pub fn rustdoc(&self) -> CargoResult<&Path> {
304 self.rustdoc
305 .try_borrow_with(|| Ok(self.get_tool("rustdoc", &self.build_config()?.rustdoc)))
306 .map(AsRef::as_ref)
307 }
308
309 pub fn load_global_rustc(&self, ws: Option<&Workspace<'_>>) -> CargoResult<Rustc> {
311 let cache_location = ws.map(|ws| {
312 ws.target_dir()
313 .join(".rustc_info.json")
314 .into_path_unlocked()
315 });
316 let wrapper = self.maybe_get_tool("rustc_wrapper", &self.build_config()?.rustc_wrapper);
317 let rustc_workspace_wrapper = self.maybe_get_tool(
318 "rustc_workspace_wrapper",
319 &self.build_config()?.rustc_workspace_wrapper,
320 );
321
322 if !self.cli_unstable().unstable_options && rustc_workspace_wrapper.is_some() {
323 bail!("Usage of `RUSTC_WORKSPACE_WRAPPER` requires `-Z unstable-options`")
324 }
325
326 Rustc::new(
327 self.get_tool("rustc", &self.build_config()?.rustc),
328 wrapper,
329 rustc_workspace_wrapper,
330 &self
331 .home()
332 .join("bin")
333 .join("rustc")
334 .into_path_unlocked()
335 .with_extension(env::consts::EXE_EXTENSION),
336 if self.cache_rustc_info {
337 cache_location
338 } else {
339 None
340 },
341 )
342 }
343
344 pub fn cargo_exe(&self) -> CargoResult<&Path> {
346 self.cargo_exe
347 .try_borrow_with(|| {
348 fn from_current_exe() -> CargoResult<PathBuf> {
349 let exe = env::current_exe()?.canonicalize()?;
354 Ok(exe)
355 }
356
357 fn from_argv() -> CargoResult<PathBuf> {
358 let argv0 = env::args_os()
367 .map(PathBuf::from)
368 .next()
369 .ok_or_else(|| anyhow!("no argv[0]"))?;
370 paths::resolve_executable(&argv0)
371 }
372
373 let exe = from_current_exe()
374 .or_else(|_| from_argv())
375 .chain_err(|| "couldn't get the path to cargo executable")?;
376 Ok(exe)
377 })
378 .map(AsRef::as_ref)
379 }
380
381 pub fn updated_sources(&self) -> RefMut<'_, HashSet<SourceId>> {
383 self.updated_sources
384 .borrow_with(|| RefCell::new(HashSet::new()))
385 .borrow_mut()
386 }
387
388 pub fn values(&self) -> CargoResult<&HashMap<String, ConfigValue>> {
394 self.values.try_borrow_with(|| self.load_values())
395 }
396
397 pub fn values_mut(&mut self) -> CargoResult<&mut HashMap<String, ConfigValue>> {
404 match self.values.borrow_mut() {
405 Some(map) => Ok(map),
406 None => bail!("config values not loaded yet"),
407 }
408 }
409
410 pub fn set_values(&self, values: HashMap<String, ConfigValue>) -> CargoResult<()> {
412 if self.values.borrow().is_some() {
413 bail!("config values already found")
414 }
415 match self.values.fill(values) {
416 Ok(()) => Ok(()),
417 Err(_) => bail!("could not fill values"),
418 }
419 }
420
421 pub fn reload_rooted_at<P: AsRef<Path>>(&mut self, path: P) -> CargoResult<()> {
424 let values = self.load_values_from(path.as_ref())?;
425 self.values.replace(values);
426 self.merge_cli_args()?;
427 Ok(())
428 }
429
430 pub fn cwd(&self) -> &Path {
432 &self.cwd
433 }
434
435 pub fn target_dir(&self) -> CargoResult<Option<Filesystem>> {
441 if let Some(dir) = &self.target_dir {
442 Ok(Some(dir.clone()))
443 } else if let Some(dir) = env::var_os("CARGO_TARGET_DIR") {
444 Ok(Some(Filesystem::new(self.cwd.join(dir))))
445 } else if let Some(val) = &self.build_config()?.target_dir {
446 let val = val.resolve_path(self);
447 Ok(Some(Filesystem::new(val)))
448 } else {
449 Ok(None)
450 }
451 }
452
453 fn get_cv(&self, key: &ConfigKey) -> CargoResult<Option<ConfigValue>> {
458 log::trace!("get cv {:?}", key);
459 let vals = self.values()?;
460 let mut parts = key.parts().enumerate();
461 let mut val = match vals.get(parts.next().unwrap().1) {
462 Some(val) => val,
463 None => return Ok(None),
464 };
465 for (i, part) in parts {
466 match val {
467 CV::Table(map, _) => {
468 val = match map.get(part) {
469 Some(val) => val,
470 None => return Ok(None),
471 }
472 }
473 CV::Integer(_, def)
474 | CV::String(_, def)
475 | CV::List(_, def)
476 | CV::Boolean(_, def) => {
477 let key_so_far: Vec<&str> = key.parts().take(i).collect();
478 bail!(
479 "expected table for configuration key `{}`, \
480 but found {} in {}",
481 key_so_far.join("."),
483 val.desc(),
484 def
485 )
486 }
487 }
488 }
489 Ok(Some(val.clone()))
490 }
491
492 pub fn set_env(&mut self, env: HashMap<String, String>) {
494 self.env = env;
495 }
496
497 fn get_env<T>(&self, key: &ConfigKey) -> Result<OptValue<T>, ConfigError>
498 where
499 T: FromStr,
500 <T as FromStr>::Err: fmt::Display,
501 {
502 match self.env.get(key.as_env_key()) {
503 Some(value) => {
504 let definition = Definition::Environment(key.as_env_key().to_string());
505 Ok(Some(Value {
506 val: value
507 .parse()
508 .map_err(|e| ConfigError::new(format!("{}", e), definition.clone()))?,
509 definition,
510 }))
511 }
512 None => Ok(None),
513 }
514 }
515
516 fn has_key(&self, key: &ConfigKey, env_prefix_ok: bool) -> bool {
517 if self.env.contains_key(key.as_env_key()) {
518 return true;
519 }
520 if env_prefix_ok {
522 let env_prefix = format!("{}_", key.as_env_key());
523 if self.env.keys().any(|k| k.starts_with(&env_prefix)) {
524 return true;
525 }
526 }
527 if let Ok(o_cv) = self.get_cv(key) {
528 if o_cv.is_some() {
529 return true;
530 }
531 }
532 false
533 }
534
535 pub fn get_string(&self, key: &str) -> CargoResult<OptValue<String>> {
539 self.get::<Option<Value<String>>>(key)
540 }
541
542 pub fn get_path(&self, key: &str) -> CargoResult<OptValue<PathBuf>> {
548 self.get::<Option<Value<ConfigRelativePath>>>(key).map(|v| {
549 v.map(|v| Value {
550 val: v.val.resolve_program(self),
551 definition: v.definition,
552 })
553 })
554 }
555
556 fn string_to_path(&self, value: String, definition: &Definition) -> PathBuf {
557 let is_path = value.contains('/') || (cfg!(windows) && value.contains('\\'));
558 if is_path {
559 definition.root(self).join(value)
560 } else {
561 PathBuf::from(value)
563 }
564 }
565
566 pub fn get_list(&self, key: &str) -> CargoResult<OptValue<Vec<(String, Definition)>>> {
574 let key = ConfigKey::from_str(key);
575 self._get_list(&key)
576 }
577
578 fn _get_list(&self, key: &ConfigKey) -> CargoResult<OptValue<Vec<(String, Definition)>>> {
579 match self.get_cv(key)? {
580 Some(CV::List(val, definition)) => Ok(Some(Value { val, definition })),
581 Some(val) => self.expected("list", key, &val),
582 None => Ok(None),
583 }
584 }
585
586 fn get_list_or_string(&self, key: &ConfigKey) -> CargoResult<Vec<(String, Definition)>> {
588 let mut res = Vec::new();
589 match self.get_cv(key)? {
590 Some(CV::List(val, _def)) => res.extend(val),
591 Some(CV::String(val, def)) => {
592 let split_vs = val.split_whitespace().map(|s| (s.to_string(), def.clone()));
593 res.extend(split_vs);
594 }
595 Some(val) => {
596 return self.expected("string or array of strings", key, &val);
597 }
598 None => {}
599 }
600
601 self.get_env_list(key, &mut res)?;
602 Ok(res)
603 }
604
605 fn get_env_list(
607 &self,
608 key: &ConfigKey,
609 output: &mut Vec<(String, Definition)>,
610 ) -> CargoResult<()> {
611 let env_val = match self.env.get(key.as_env_key()) {
612 Some(v) => v,
613 None => return Ok(()),
614 };
615
616 let def = Definition::Environment(key.as_env_key().to_string());
617 if self.cli_unstable().advanced_env && env_val.starts_with('[') && env_val.ends_with(']') {
618 let toml_s = format!("value={}", env_val);
620 let toml_v: toml::Value = toml::de::from_str(&toml_s).map_err(|e| {
621 ConfigError::new(format!("could not parse TOML list: {}", e), def.clone())
622 })?;
623 let values = toml_v
624 .as_table()
625 .unwrap()
626 .get("value")
627 .unwrap()
628 .as_array()
629 .expect("env var was not array");
630 for value in values {
631 let s = value.as_str().ok_or_else(|| {
633 ConfigError::new(
634 format!("expected string, found {}", value.type_str()),
635 def.clone(),
636 )
637 })?;
638 output.push((s.to_string(), def.clone()));
639 }
640 } else {
641 output.extend(
642 env_val
643 .split_whitespace()
644 .map(|s| (s.to_string(), def.clone())),
645 );
646 }
647 Ok(())
648 }
649
650 fn get_table(&self, key: &ConfigKey) -> CargoResult<OptValue<HashMap<String, CV>>> {
654 match self.get_cv(key)? {
655 Some(CV::Table(val, definition)) => Ok(Some(Value { val, definition })),
656 Some(val) => self.expected("table", key, &val),
657 None => Ok(None),
658 }
659 }
660
661 get_value_typed! {get_integer, i64, Integer, "an integer"}
662 get_value_typed! {get_bool, bool, Boolean, "true/false"}
663 get_value_typed! {get_string_priv, String, String, "a string"}
664
665 fn expected<T>(&self, ty: &str, key: &ConfigKey, val: &CV) -> CargoResult<T> {
667 val.expected(ty, &key.to_string())
668 .map_err(|e| anyhow!("invalid configuration for key `{}`\n{}", key, e))
669 }
670
671 pub fn configure(
677 &mut self,
678 verbose: u32,
679 quiet: bool,
680 color: Option<&str>,
681 frozen: bool,
682 locked: bool,
683 offline: bool,
684 target_dir: &Option<PathBuf>,
685 unstable_flags: &[String],
686 cli_config: &[String],
687 ) -> CargoResult<()> {
688 self.unstable_flags.parse(unstable_flags)?;
689 if !cli_config.is_empty() {
690 self.unstable_flags.fail_if_stable_opt("--config", 6699)?;
691 self.cli_config = Some(cli_config.iter().map(|s| s.to_string()).collect());
692 self.merge_cli_args()?;
693 }
694 let extra_verbose = verbose >= 2;
695 let verbose = verbose != 0;
696
697 #[derive(Deserialize, Default)]
698 struct TermConfig {
699 verbose: Option<bool>,
700 color: Option<String>,
701 }
702
703 let term = self.get::<TermConfig>("term").unwrap_or_default();
705
706 let color = color.or_else(|| term.color.as_deref());
707
708 let verbosity = match (verbose, term.verbose, quiet) {
709 (true, _, false) | (_, Some(true), false) => Verbosity::Verbose,
710
711 (false, _, true) => Verbosity::Quiet,
714
715 (true, _, true) => {
718 bail!("cannot set both --verbose and --quiet");
719 }
720
721 (false, _, false) => Verbosity::Normal,
722 };
723
724 let cli_target_dir = match target_dir.as_ref() {
725 Some(dir) => Some(Filesystem::new(dir.clone())),
726 None => None,
727 };
728
729 self.shell().set_verbosity(verbosity);
730 self.shell().set_color_choice(color)?;
731 self.extra_verbose = extra_verbose;
732 self.frozen = frozen;
733 self.locked = locked;
734 self.offline = offline
735 || self
736 .net_config()
737 .ok()
738 .and_then(|n| n.offline)
739 .unwrap_or(false);
740 self.target_dir = cli_target_dir;
741
742 if nightly_features_allowed() {
743 if let Some(val) = self.get::<Option<bool>>("unstable.mtime_on_use")? {
744 self.unstable_flags.mtime_on_use |= val;
745 }
746 }
747
748 Ok(())
749 }
750
751 pub fn cli_unstable(&self) -> &CliUnstable {
752 &self.unstable_flags
753 }
754
755 pub fn extra_verbose(&self) -> bool {
756 self.extra_verbose
757 }
758
759 pub fn network_allowed(&self) -> bool {
760 !self.frozen() && !self.offline()
761 }
762
763 pub fn offline(&self) -> bool {
764 self.offline
765 }
766
767 pub fn frozen(&self) -> bool {
768 self.frozen
769 }
770
771 pub fn lock_update_allowed(&self) -> bool {
772 !self.frozen && !self.locked
773 }
774
775 pub fn load_values(&self) -> CargoResult<HashMap<String, ConfigValue>> {
777 self.load_values_from(&self.cwd)
778 }
779
780 fn load_values_from(&self, path: &Path) -> CargoResult<HashMap<String, ConfigValue>> {
781 let mut cfg = CV::Table(HashMap::new(), Definition::Path(PathBuf::from(".")));
784 let home = self.home_path.clone().into_path_unlocked();
785
786 self.walk_tree(path, &home, |path| {
787 let value = self.load_file(path)?;
788 cfg.merge(value, false)
789 .chain_err(|| format!("failed to merge configuration at `{}`", path.display()))?;
790 Ok(())
791 })
792 .chain_err(|| "could not load Cargo configuration")?;
793
794 match cfg {
795 CV::Table(map, _) => Ok(map),
796 _ => unreachable!(),
797 }
798 }
799
800 fn load_file(&self, path: &Path) -> CargoResult<ConfigValue> {
801 let mut seen = HashSet::new();
802 self._load_file(path, &mut seen)
803 }
804
805 fn _load_file(&self, path: &Path, seen: &mut HashSet<PathBuf>) -> CargoResult<ConfigValue> {
806 if !seen.insert(path.to_path_buf()) {
807 bail!(
808 "config `include` cycle detected with path `{}`",
809 path.display()
810 );
811 }
812 let contents = fs::read_to_string(path)
813 .chain_err(|| format!("failed to read configuration file `{}`", path.display()))?;
814 let toml = cargo_toml::parse(&contents, path, self)
815 .chain_err(|| format!("could not parse TOML configuration in `{}`", path.display()))?;
816 let value = CV::from_toml(Definition::Path(path.to_path_buf()), toml).chain_err(|| {
817 format!(
818 "failed to load TOML configuration from `{}`",
819 path.display()
820 )
821 })?;
822 let value = self.load_includes(value, seen)?;
823 Ok(value)
824 }
825
826 fn load_includes(&self, mut value: CV, seen: &mut HashSet<PathBuf>) -> CargoResult<CV> {
832 let (includes, def) = match &mut value {
834 CV::Table(table, _def) => match table.remove("include") {
835 Some(CV::String(s, def)) => (vec![(s, def.clone())], def),
836 Some(CV::List(list, def)) => (list, def),
837 Some(other) => bail!(
838 "`include` expected a string or list, but found {} in `{}`",
839 other.desc(),
840 other.definition()
841 ),
842 None => {
843 return Ok(value);
844 }
845 },
846 _ => unreachable!(),
847 };
848 if !self.cli_unstable().config_include {
850 self.shell().warn(format!("config `include` in `{}` ignored, the -Zconfig-include command-line flag is required",
851 def))?;
852 return Ok(value);
853 }
854 let mut root = CV::Table(HashMap::new(), value.definition().clone());
856 for (path, def) in includes {
857 let abs_path = match &def {
858 Definition::Path(p) => p.parent().unwrap().join(&path),
859 Definition::Environment(_) | Definition::Cli => self.cwd().join(&path),
860 };
861 self._load_file(&abs_path, seen)
862 .and_then(|include| root.merge(include, true))
863 .chain_err(|| format!("failed to load config include `{}` from `{}`", path, def))?;
864 }
865 root.merge(value, true)?;
866 Ok(root)
867 }
868
869 fn merge_cli_args(&mut self) -> CargoResult<()> {
871 let cli_args = match &self.cli_config {
872 Some(cli_args) => cli_args,
873 None => return Ok(()),
874 };
875 let mut loaded_args = CV::Table(HashMap::new(), Definition::Cli);
876 for arg in cli_args {
877 let arg_as_path = self.cwd.join(arg);
878 let tmp_table = if !arg.is_empty() && arg_as_path.exists() {
879 let str_path = arg_as_path
881 .to_str()
882 .ok_or_else(|| {
883 anyhow::format_err!("config path {:?} is not utf-8", arg_as_path)
884 })?
885 .to_string();
886 let mut map = HashMap::new();
887 let value = CV::String(str_path, Definition::Cli);
888 map.insert("include".to_string(), value);
889 CV::Table(map, Definition::Cli)
890 } else {
891 let toml_v: toml::Value = toml::de::from_str(arg)
894 .chain_err(|| format!("failed to parse --config argument `{}`", arg))?;
895 let toml_table = toml_v.as_table().unwrap();
896 if toml_table.len() != 1 {
897 bail!(
898 "--config argument `{}` expected exactly one key=value pair, got {} keys",
899 arg,
900 toml_table.len()
901 );
902 }
903 CV::from_toml(Definition::Cli, toml_v)
904 .chain_err(|| format!("failed to convert --config argument `{}`", arg))?
905 };
906 let mut seen = HashSet::new();
907 let tmp_table = self
908 .load_includes(tmp_table, &mut seen)
909 .chain_err(|| "failed to load --config include".to_string())?;
910 loaded_args
911 .merge(tmp_table, true)
912 .chain_err(|| format!("failed to merge --config argument `{}`", arg))?;
913 }
914 let _ = self.values()?;
916 let values = self.values_mut()?;
917 let loaded_map = match loaded_args {
918 CV::Table(table, _def) => table,
919 _ => unreachable!(),
920 };
921 for (key, value) in loaded_map.into_iter() {
922 match values.entry(key) {
923 Vacant(entry) => {
924 entry.insert(value);
925 }
926 Occupied(mut entry) => entry.get_mut().merge(value, true).chain_err(|| {
927 format!(
928 "failed to merge --config key `{}` into `{}`",
929 entry.key(),
930 entry.get().definition(),
931 )
932 })?,
933 };
934 }
935 Ok(())
936 }
937
938 fn get_file_path(
944 &self,
945 dir: &Path,
946 filename_without_extension: &str,
947 warn: bool,
948 ) -> CargoResult<Option<PathBuf>> {
949 let possible = dir.join(filename_without_extension);
950 let possible_with_extension = dir.join(format!("{}.toml", filename_without_extension));
951
952 if fs::metadata(&possible).is_ok() {
953 if warn && fs::metadata(&possible_with_extension).is_ok() {
954 let skip_warning = if let Ok(target_path) = fs::read_link(&possible) {
960 target_path == possible_with_extension
961 } else {
962 false
963 };
964
965 if !skip_warning {
966 self.shell().warn(format!(
967 "Both `{}` and `{}` exist. Using `{}`",
968 possible.display(),
969 possible_with_extension.display(),
970 possible.display()
971 ))?;
972 }
973 }
974
975 Ok(Some(possible))
976 } else if fs::metadata(&possible_with_extension).is_ok() {
977 Ok(Some(possible_with_extension))
978 } else {
979 Ok(None)
980 }
981 }
982
983 fn walk_tree<F>(&self, pwd: &Path, home: &Path, mut walk: F) -> CargoResult<()>
984 where
985 F: FnMut(&Path) -> CargoResult<()>,
986 {
987 let mut stash: HashSet<PathBuf> = HashSet::new();
988
989 for current in paths::ancestors(pwd) {
990 if let Some(path) = self.get_file_path(¤t.join(".cargo"), "config", true)? {
991 walk(&path)?;
992 stash.insert(path);
993 }
994 }
995
996 if let Some(path) = self.get_file_path(home, "config", true)? {
1000 if !stash.contains(&path) {
1001 walk(&path)?;
1002 }
1003 }
1004
1005 Ok(())
1006 }
1007
1008 pub fn get_registry_index(&self, registry: &str) -> CargoResult<Url> {
1010 validate_package_name(registry, "registry name", "")?;
1011 Ok(
1012 match self.get_string(&format!("registries.{}.index", registry))? {
1013 Some(index) => self.resolve_registry_index(index)?,
1014 None => bail!("No index found for registry: `{}`", registry),
1015 },
1016 )
1017 }
1018
1019 pub fn get_default_registry_index(&self) -> CargoResult<Option<Url>> {
1021 Ok(match self.get_string("registry.index")? {
1022 Some(index) => Some(self.resolve_registry_index(index)?),
1023 None => None,
1024 })
1025 }
1026
1027 fn resolve_registry_index(&self, index: Value<String>) -> CargoResult<Url> {
1028 let base = index
1029 .definition
1030 .root(self)
1031 .join("truncated-by-url_with_base");
1032 let _parsed = index.val.into_url()?;
1034 let url = index.val.into_url_with_base(Some(&*base))?;
1035 if url.password().is_some() {
1036 bail!("Registry URLs may not contain passwords");
1037 }
1038 Ok(url)
1039 }
1040
1041 pub fn load_credentials(&mut self) -> CargoResult<()> {
1043 let home_path = self.home_path.clone().into_path_unlocked();
1044 let credentials = match self.get_file_path(&home_path, "credentials", true)? {
1045 Some(credentials) => credentials,
1046 None => return Ok(()),
1047 };
1048
1049 let mut value = self.load_file(&credentials)?;
1050 {
1052 let (value_map, def) = match value {
1053 CV::Table(ref mut value, ref def) => (value, def),
1054 _ => unreachable!(),
1055 };
1056
1057 if let Some(token) = value_map.remove("token") {
1058 if let Vacant(entry) = value_map.entry("registry".into()) {
1059 let mut map = HashMap::new();
1060 map.insert("token".into(), token);
1061 let table = CV::Table(map, def.clone());
1062 entry.insert(table);
1063 }
1064 }
1065 }
1066
1067 if let CV::Table(map, _) = value {
1068 let base_map = self.values_mut()?;
1069 for (k, v) in map {
1070 match base_map.entry(k) {
1071 Vacant(entry) => {
1072 entry.insert(v);
1073 }
1074 Occupied(mut entry) => {
1075 entry.get_mut().merge(v, true)?;
1076 }
1077 }
1078 }
1079 }
1080
1081 Ok(())
1082 }
1083
1084 fn maybe_get_tool(&self, tool: &str, from_config: &Option<PathBuf>) -> Option<PathBuf> {
1087 let var = tool.to_uppercase();
1088
1089 match env::var_os(&var) {
1090 Some(tool_path) => {
1091 let maybe_relative = match tool_path.to_str() {
1092 Some(s) => s.contains('/') || s.contains('\\'),
1093 None => false,
1094 };
1095 let path = if maybe_relative {
1096 self.cwd.join(tool_path)
1097 } else {
1098 PathBuf::from(tool_path)
1099 };
1100 Some(path)
1101 }
1102
1103 None => from_config.clone(),
1104 }
1105 }
1106
1107 fn get_tool(&self, tool: &str, from_config: &Option<PathBuf>) -> PathBuf {
1110 self.maybe_get_tool(tool, from_config)
1111 .unwrap_or_else(|| PathBuf::from(tool))
1112 }
1113
1114 pub fn jobserver_from_env(&self) -> Option<&jobserver::Client> {
1115 self.jobserver.as_ref()
1116 }
1117
1118 pub fn http(&self) -> CargoResult<&RefCell<Easy>> {
1119 let http = self
1120 .easy
1121 .try_borrow_with(|| ops::http_handle(self).map(RefCell::new))?;
1122 {
1123 let mut http = http.borrow_mut();
1124 http.reset();
1125 let timeout = ops::configure_http_handle(self, &mut http)?;
1126 timeout.configure(&mut http)?;
1127 }
1128 Ok(http)
1129 }
1130
1131 pub fn http_config(&self) -> CargoResult<&CargoHttpConfig> {
1132 self.http_config
1133 .try_borrow_with(|| Ok(self.get::<CargoHttpConfig>("http")?))
1134 }
1135
1136 pub fn net_config(&self) -> CargoResult<&CargoNetConfig> {
1137 self.net_config
1138 .try_borrow_with(|| Ok(self.get::<CargoNetConfig>("net")?))
1139 }
1140
1141 pub fn build_config(&self) -> CargoResult<&CargoBuildConfig> {
1142 self.build_config
1143 .try_borrow_with(|| Ok(self.get::<CargoBuildConfig>("build")?))
1144 }
1145
1146 pub fn target_cfgs(&self) -> CargoResult<&Vec<(String, TargetCfgConfig)>> {
1150 self.target_cfgs
1151 .try_borrow_with(|| target::load_target_cfgs(self))
1152 }
1153
1154 pub fn target_cfg_triple(&self, target: &str) -> CargoResult<TargetConfig> {
1156 target::load_target_triple(self, target)
1157 }
1158
1159 pub fn crates_io_source_id<F>(&self, f: F) -> CargoResult<SourceId>
1160 where
1161 F: FnMut() -> CargoResult<SourceId>,
1162 {
1163 Ok(*(self.crates_io_source_id.try_borrow_with(f)?))
1164 }
1165
1166 pub fn creation_time(&self) -> Instant {
1167 self.creation_time
1168 }
1169
1170 pub fn get<'de, T: serde::de::Deserialize<'de>>(&self, key: &str) -> CargoResult<T> {
1185 let d = Deserializer {
1186 config: self,
1187 key: ConfigKey::from_str(key),
1188 env_prefix_ok: true,
1189 };
1190 T::deserialize(d).map_err(|e| e.into())
1191 }
1192
1193 pub fn assert_package_cache_locked<'a>(&self, f: &'a Filesystem) -> &'a Path {
1194 let ret = f.as_path_unlocked();
1195 assert!(
1196 self.package_cache_lock.borrow().is_some(),
1197 "package cache lock is not currently held, Cargo forgot to call \
1198 `acquire_package_cache_lock` before we got to this stack frame",
1199 );
1200 assert!(ret.starts_with(self.home_path.as_path_unlocked()));
1201 ret
1202 }
1203
1204 pub fn acquire_package_cache_lock(&self) -> CargoResult<PackageCacheLock<'_>> {
1210 let mut slot = self.package_cache_lock.borrow_mut();
1211 match *slot {
1212 Some((_, ref mut cnt)) => {
1215 *cnt += 1;
1216 }
1217 None => {
1218 let path = ".package-cache";
1219 let desc = "package cache";
1220
1221 match self.home_path.open_rw(path, self, desc) {
1238 Ok(lock) => *slot = Some((Some(lock), 1)),
1239 Err(e) => {
1240 if maybe_readonly(&e) {
1241 let lock = self.home_path.open_ro(path, self, desc).ok();
1242 *slot = Some((lock, 1));
1243 return Ok(PackageCacheLock(self));
1244 }
1245
1246 Err(e).chain_err(|| "failed to acquire package cache lock")?;
1247 }
1248 }
1249 }
1250 }
1251 return Ok(PackageCacheLock(self));
1252
1253 fn maybe_readonly(err: &anyhow::Error) -> bool {
1254 err.chain().any(|err| {
1255 if let Some(io) = err.downcast_ref::<io::Error>() {
1256 if io.kind() == io::ErrorKind::PermissionDenied {
1257 return true;
1258 }
1259
1260 #[cfg(unix)]
1261 return io.raw_os_error() == Some(libc::EROFS);
1262 }
1263
1264 false
1265 })
1266 }
1267 }
1268
1269 pub fn release_package_cache_lock(&self) {}
1270}
1271
1272#[derive(Debug)]
1274pub struct ConfigError {
1275 error: anyhow::Error,
1276 definition: Option<Definition>,
1277}
1278
1279impl ConfigError {
1280 fn new(message: String, definition: Definition) -> ConfigError {
1281 ConfigError {
1282 error: anyhow::Error::msg(message),
1283 definition: Some(definition),
1284 }
1285 }
1286
1287 fn expected(key: &ConfigKey, expected: &str, found: &ConfigValue) -> ConfigError {
1288 ConfigError {
1289 error: anyhow!(
1290 "`{}` expected {}, but found a {}",
1291 key,
1292 expected,
1293 found.desc()
1294 ),
1295 definition: Some(found.definition().clone()),
1296 }
1297 }
1298
1299 fn missing(key: &ConfigKey) -> ConfigError {
1300 ConfigError {
1301 error: anyhow!("missing config key `{}`", key),
1302 definition: None,
1303 }
1304 }
1305
1306 fn with_key_context(self, key: &ConfigKey, definition: Definition) -> ConfigError {
1307 ConfigError {
1308 error: anyhow::Error::from(self)
1309 .context(format!("could not load config key `{}`", key)),
1310 definition: Some(definition),
1311 }
1312 }
1313}
1314
1315impl std::error::Error for ConfigError {
1316 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1317 self.error.source()
1318 }
1319}
1320
1321impl fmt::Display for ConfigError {
1322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1323 if let Some(definition) = &self.definition {
1324 write!(f, "error in {}: {}", definition, self.error)
1325 } else {
1326 self.error.fmt(f)
1327 }
1328 }
1329}
1330
1331impl serde::de::Error for ConfigError {
1332 fn custom<T: fmt::Display>(msg: T) -> Self {
1333 ConfigError {
1334 error: anyhow::Error::msg(msg.to_string()),
1335 definition: None,
1336 }
1337 }
1338}
1339
1340impl From<anyhow::Error> for ConfigError {
1341 fn from(error: anyhow::Error) -> Self {
1342 ConfigError {
1343 error,
1344 definition: None,
1345 }
1346 }
1347}
1348
1349#[derive(Eq, PartialEq, Clone)]
1350pub enum ConfigValue {
1351 Integer(i64, Definition),
1352 String(String, Definition),
1353 List(Vec<(String, Definition)>, Definition),
1354 Table(HashMap<String, ConfigValue>, Definition),
1355 Boolean(bool, Definition),
1356}
1357
1358impl fmt::Debug for ConfigValue {
1359 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1360 match self {
1361 CV::Integer(i, def) => write!(f, "{} (from {})", i, def),
1362 CV::Boolean(b, def) => write!(f, "{} (from {})", b, def),
1363 CV::String(s, def) => write!(f, "{} (from {})", s, def),
1364 CV::List(list, def) => {
1365 write!(f, "[")?;
1366 for (i, (s, def)) in list.iter().enumerate() {
1367 if i > 0 {
1368 write!(f, ", ")?;
1369 }
1370 write!(f, "{} (from {})", s, def)?;
1371 }
1372 write!(f, "] (from {})", def)
1373 }
1374 CV::Table(table, _) => write!(f, "{:?}", table),
1375 }
1376 }
1377}
1378
1379impl ConfigValue {
1380 fn from_toml(def: Definition, toml: toml::Value) -> CargoResult<ConfigValue> {
1381 match toml {
1382 toml::Value::String(val) => Ok(CV::String(val, def)),
1383 toml::Value::Boolean(b) => Ok(CV::Boolean(b, def)),
1384 toml::Value::Integer(i) => Ok(CV::Integer(i, def)),
1385 toml::Value::Array(val) => Ok(CV::List(
1386 val.into_iter()
1387 .map(|toml| match toml {
1388 toml::Value::String(val) => Ok((val, def.clone())),
1389 v => bail!("expected string but found {} in list", v.type_str()),
1390 })
1391 .collect::<CargoResult<_>>()?,
1392 def,
1393 )),
1394 toml::Value::Table(val) => Ok(CV::Table(
1395 val.into_iter()
1396 .map(|(key, value)| {
1397 let value = CV::from_toml(def.clone(), value)
1398 .chain_err(|| format!("failed to parse key `{}`", key))?;
1399 Ok((key, value))
1400 })
1401 .collect::<CargoResult<_>>()?,
1402 def,
1403 )),
1404 v => bail!(
1405 "found TOML configuration value of unknown type `{}`",
1406 v.type_str()
1407 ),
1408 }
1409 }
1410
1411 fn into_toml(self) -> toml::Value {
1412 match self {
1413 CV::Boolean(s, _) => toml::Value::Boolean(s),
1414 CV::String(s, _) => toml::Value::String(s),
1415 CV::Integer(i, _) => toml::Value::Integer(i),
1416 CV::List(l, _) => {
1417 toml::Value::Array(l.into_iter().map(|(s, _)| toml::Value::String(s)).collect())
1418 }
1419 CV::Table(l, _) => {
1420 toml::Value::Table(l.into_iter().map(|(k, v)| (k, v.into_toml())).collect())
1421 }
1422 }
1423 }
1424
1425 fn merge(&mut self, from: ConfigValue, force: bool) -> CargoResult<()> {
1434 match (self, from) {
1435 (&mut CV::List(ref mut old, _), CV::List(ref mut new, _)) => {
1436 let new = mem::replace(new, Vec::new());
1437 old.extend(new.into_iter());
1438 }
1439 (&mut CV::Table(ref mut old, _), CV::Table(ref mut new, _)) => {
1440 let new = mem::replace(new, HashMap::new());
1441 for (key, value) in new {
1442 match old.entry(key.clone()) {
1443 Occupied(mut entry) => {
1444 let new_def = value.definition().clone();
1445 let entry = entry.get_mut();
1446 entry.merge(value, force).chain_err(|| {
1447 format!(
1448 "failed to merge key `{}` between \
1449 {} and {}",
1450 key,
1451 entry.definition(),
1452 new_def,
1453 )
1454 })?;
1455 }
1456 Vacant(entry) => {
1457 entry.insert(value);
1458 }
1459 };
1460 }
1461 }
1462 (expected @ &mut CV::List(_, _), found)
1464 | (expected @ &mut CV::Table(_, _), found)
1465 | (expected, found @ CV::List(_, _))
1466 | (expected, found @ CV::Table(_, _)) => {
1467 return Err(anyhow!(
1468 "failed to merge config value from `{}` into `{}`: expected {}, but found {}",
1469 found.definition(),
1470 expected.definition(),
1471 expected.desc(),
1472 found.desc()
1473 ));
1474 }
1475 (old, mut new) => {
1476 if force || new.definition().is_higher_priority(old.definition()) {
1477 mem::swap(old, &mut new);
1478 }
1479 }
1480 }
1481
1482 Ok(())
1483 }
1484
1485 pub fn i64(&self, key: &str) -> CargoResult<(i64, &Definition)> {
1486 match self {
1487 CV::Integer(i, def) => Ok((*i, def)),
1488 _ => self.expected("integer", key),
1489 }
1490 }
1491
1492 pub fn string(&self, key: &str) -> CargoResult<(&str, &Definition)> {
1493 match self {
1494 CV::String(s, def) => Ok((s, def)),
1495 _ => self.expected("string", key),
1496 }
1497 }
1498
1499 pub fn table(&self, key: &str) -> CargoResult<(&HashMap<String, ConfigValue>, &Definition)> {
1500 match self {
1501 CV::Table(table, def) => Ok((table, def)),
1502 _ => self.expected("table", key),
1503 }
1504 }
1505
1506 pub fn list(&self, key: &str) -> CargoResult<&[(String, Definition)]> {
1507 match self {
1508 CV::List(list, _) => Ok(list),
1509 _ => self.expected("list", key),
1510 }
1511 }
1512
1513 pub fn boolean(&self, key: &str) -> CargoResult<(bool, &Definition)> {
1514 match self {
1515 CV::Boolean(b, def) => Ok((*b, def)),
1516 _ => self.expected("bool", key),
1517 }
1518 }
1519
1520 pub fn desc(&self) -> &'static str {
1521 match *self {
1522 CV::Table(..) => "table",
1523 CV::List(..) => "array",
1524 CV::String(..) => "string",
1525 CV::Boolean(..) => "boolean",
1526 CV::Integer(..) => "integer",
1527 }
1528 }
1529
1530 pub fn definition(&self) -> &Definition {
1531 match self {
1532 CV::Boolean(_, def)
1533 | CV::Integer(_, def)
1534 | CV::String(_, def)
1535 | CV::List(_, def)
1536 | CV::Table(_, def) => def,
1537 }
1538 }
1539
1540 fn expected<T>(&self, wanted: &str, key: &str) -> CargoResult<T> {
1541 bail!(
1542 "expected a {}, but found a {} for `{}` in {}",
1543 wanted,
1544 self.desc(),
1545 key,
1546 self.definition()
1547 )
1548 }
1549}
1550
1551pub fn homedir(cwd: &Path) -> Option<PathBuf> {
1552 ::home::cargo_home_with_cwd(cwd).ok()
1553}
1554
1555pub fn save_credentials(cfg: &Config, token: String, registry: Option<String>) -> CargoResult<()> {
1556 let home_path = cfg.home_path.clone().into_path_unlocked();
1560 let filename = match cfg.get_file_path(&home_path, "credentials", false)? {
1561 Some(path) => match path.file_name() {
1562 Some(filename) => Path::new(filename).to_owned(),
1563 None => Path::new("credentials").to_owned(),
1564 },
1565 None => Path::new("credentials").to_owned(),
1566 };
1567
1568 let mut file = {
1569 cfg.home_path.create_dir()?;
1570 cfg.home_path
1571 .open_rw(filename, cfg, "credentials' config file")?
1572 };
1573
1574 let (key, mut value) = {
1575 let key = "token".to_string();
1576 let value = ConfigValue::String(token, Definition::Path(file.path().to_path_buf()));
1577 let mut map = HashMap::new();
1578 map.insert(key, value);
1579 let table = CV::Table(map, Definition::Path(file.path().to_path_buf()));
1580
1581 if let Some(registry) = registry.clone() {
1582 let mut map = HashMap::new();
1583 map.insert(registry, table);
1584 (
1585 "registries".into(),
1586 CV::Table(map, Definition::Path(file.path().to_path_buf())),
1587 )
1588 } else {
1589 ("registry".into(), table)
1590 }
1591 };
1592
1593 let mut contents = String::new();
1594 file.read_to_string(&mut contents).chain_err(|| {
1595 format!(
1596 "failed to read configuration file `{}`",
1597 file.path().display()
1598 )
1599 })?;
1600
1601 let mut toml = cargo_toml::parse(&contents, file.path(), cfg)?;
1602
1603 if let Some(token) = toml.as_table_mut().unwrap().remove("token") {
1605 let mut map = HashMap::new();
1606 map.insert("token".to_string(), token);
1607 toml.as_table_mut()
1608 .unwrap()
1609 .insert("registry".into(), map.into());
1610 }
1611
1612 if registry.is_some() {
1613 if let Some(table) = toml.as_table_mut().unwrap().remove("registries") {
1614 let v = CV::from_toml(Definition::Path(file.path().to_path_buf()), table)?;
1615 value.merge(v, false)?;
1616 }
1617 }
1618 toml.as_table_mut().unwrap().insert(key, value.into_toml());
1619
1620 let contents = toml.to_string();
1621 file.seek(SeekFrom::Start(0))?;
1622 file.write_all(contents.as_bytes())?;
1623 file.file().set_len(contents.len() as u64)?;
1624 set_permissions(file.file(), 0o600)?;
1625
1626 return Ok(());
1627
1628 #[cfg(unix)]
1629 fn set_permissions(file: &File, mode: u32) -> CargoResult<()> {
1630 use std::os::unix::fs::PermissionsExt;
1631
1632 let mut perms = file.metadata()?.permissions();
1633 perms.set_mode(mode);
1634 file.set_permissions(perms)?;
1635 Ok(())
1636 }
1637
1638 #[cfg(not(unix))]
1639 #[allow(unused)]
1640 fn set_permissions(file: &File, mode: u32) -> CargoResult<()> {
1641 Ok(())
1642 }
1643}
1644
1645pub struct PackageCacheLock<'a>(&'a Config);
1646
1647impl Drop for PackageCacheLock<'_> {
1648 fn drop(&mut self) {
1649 let mut slot = self.0.package_cache_lock.borrow_mut();
1650 let (_, cnt) = slot.as_mut().unwrap();
1651 *cnt -= 1;
1652 if *cnt == 0 {
1653 *slot = None;
1654 }
1655 }
1656}
1657
1658#[derive(Debug, Default, Deserialize, PartialEq)]
1659#[serde(rename_all = "kebab-case")]
1660pub struct CargoHttpConfig {
1661 pub proxy: Option<String>,
1662 pub low_speed_limit: Option<u32>,
1663 pub timeout: Option<u64>,
1664 pub cainfo: Option<ConfigRelativePath>,
1665 pub check_revoke: Option<bool>,
1666 pub user_agent: Option<String>,
1667 pub debug: Option<bool>,
1668 pub multiplexing: Option<bool>,
1669 pub ssl_version: Option<SslVersionConfig>,
1670}
1671
1672#[derive(Clone, Debug, Deserialize, PartialEq)]
1686#[serde(untagged)]
1687pub enum SslVersionConfig {
1688 Single(String),
1689 Range(SslVersionConfigRange),
1690}
1691
1692#[derive(Clone, Debug, Deserialize, PartialEq)]
1693pub struct SslVersionConfigRange {
1694 pub min: Option<String>,
1695 pub max: Option<String>,
1696}
1697
1698#[derive(Debug, Deserialize)]
1699#[serde(rename_all = "kebab-case")]
1700pub struct CargoNetConfig {
1701 pub retry: Option<u32>,
1702 pub offline: Option<bool>,
1703 pub git_fetch_with_cli: Option<bool>,
1704}
1705
1706#[derive(Debug, Deserialize)]
1707#[serde(rename_all = "kebab-case")]
1708pub struct CargoBuildConfig {
1709 pub pipelining: Option<bool>,
1710 pub dep_info_basedir: Option<ConfigRelativePath>,
1711 pub target_dir: Option<ConfigRelativePath>,
1712 pub incremental: Option<bool>,
1713 pub target: Option<ConfigRelativePath>,
1714 pub jobs: Option<u32>,
1715 pub rustflags: Option<StringList>,
1716 pub rustdocflags: Option<StringList>,
1717 pub rustc_wrapper: Option<PathBuf>,
1718 pub rustc_workspace_wrapper: Option<PathBuf>,
1719 pub rustc: Option<PathBuf>,
1720 pub rustdoc: Option<PathBuf>,
1721 pub out_dir: Option<ConfigRelativePath>,
1722}
1723
1724#[derive(Debug, Deserialize)]
1735pub struct StringList(Vec<String>);
1736
1737impl StringList {
1738 pub fn as_slice(&self) -> &[String] {
1739 &self.0
1740 }
1741}