Skip to main content

cargo/sources/
config.rs

1//! Implementation of configuration for various sources
2//!
3//! This module will parse the various `source.*` TOML configuration keys into a
4//! structure usable by Cargo itself. Currently this is primarily used to map
5//! sources to one another via the `replace-with` key in `.cargo/config`.
6
7use crate::core::{GitReference, PackageId, Source, SourceId};
8use crate::sources::{ReplacedSource, CRATES_IO_REGISTRY};
9use crate::util::config::{self, ConfigRelativePath, OptValue};
10use crate::util::errors::{CargoResult, CargoResultExt};
11use crate::util::{Config, IntoUrl};
12use anyhow::bail;
13use log::debug;
14use std::collections::{HashMap, HashSet};
15use url::Url;
16
17#[derive(Clone)]
18pub struct SourceConfigMap<'cfg> {
19    /// Mapping of source name to the toml configuration.
20    cfgs: HashMap<String, SourceConfig>,
21    /// Mapping of `SourceId` to the source name.
22    id2name: HashMap<SourceId, String>,
23    config: &'cfg Config,
24}
25
26/// Definition of a source in a config file.
27#[derive(Debug, serde::Deserialize)]
28#[serde(rename_all = "kebab-case")]
29struct SourceConfigDef {
30    /// Indicates this source should be replaced with another of the given name.
31    replace_with: OptValue<String>,
32    /// A directory source.
33    directory: Option<ConfigRelativePath>,
34    /// A registry source. Value is a URL.
35    registry: OptValue<String>,
36    /// A local registry source.
37    local_registry: Option<ConfigRelativePath>,
38    /// A git source. Value is a URL.
39    git: OptValue<String>,
40    /// The git branch.
41    branch: OptValue<String>,
42    /// The git tag.
43    tag: OptValue<String>,
44    /// The git revision.
45    rev: OptValue<String>,
46}
47
48/// Configuration for a particular source, found in TOML looking like:
49///
50/// ```toml
51/// [source.crates-io]
52/// registry = 'https://github.com/rust-lang/crates.io-index'
53/// replace-with = 'foo'    # optional
54/// ```
55#[derive(Clone)]
56struct SourceConfig {
57    /// `SourceId` this source corresponds to, inferred from the various
58    /// defined keys in the configuration.
59    id: SourceId,
60
61    /// Whether or not this source is replaced with another.
62    ///
63    /// This field is a tuple of `(name, location)` where `location` is where
64    /// this configuration key was defined (such as the `.cargo/config` path
65    /// or the environment variable name).
66    replace_with: Option<(String, String)>,
67}
68
69impl<'cfg> SourceConfigMap<'cfg> {
70    pub fn new(config: &'cfg Config) -> CargoResult<SourceConfigMap<'cfg>> {
71        let mut base = SourceConfigMap::empty(config)?;
72        let sources: Option<HashMap<String, SourceConfigDef>> = config.get("source")?;
73        if let Some(sources) = sources {
74            for (key, value) in sources.into_iter() {
75                base.add_config(key, value)?;
76            }
77        }
78        Ok(base)
79    }
80
81    pub fn empty(config: &'cfg Config) -> CargoResult<SourceConfigMap<'cfg>> {
82        let mut base = SourceConfigMap {
83            cfgs: HashMap::new(),
84            id2name: HashMap::new(),
85            config,
86        };
87        base.add(
88            CRATES_IO_REGISTRY,
89            SourceConfig {
90                id: SourceId::crates_io(config)?,
91                replace_with: None,
92            },
93        )?;
94        Ok(base)
95    }
96
97    pub fn config(&self) -> &'cfg Config {
98        self.config
99    }
100
101    /// Get the `Source` for a given `SourceId`.
102    pub fn load(
103        &self,
104        id: SourceId,
105        yanked_whitelist: &HashSet<PackageId>,
106    ) -> CargoResult<Box<dyn Source + 'cfg>> {
107        debug!("loading: {}", id);
108
109        let mut name = match self.id2name.get(&id) {
110            Some(name) => name,
111            None => return Ok(id.load(self.config, yanked_whitelist)?),
112        };
113        let mut cfg_loc = "";
114        let orig_name = name;
115        let new_id;
116        loop {
117            let cfg = match self.cfgs.get(name) {
118                Some(cfg) => cfg,
119                None => bail!(
120                    "could not find a configured source with the \
121                     name `{}` when attempting to lookup `{}` \
122                     (configuration in `{}`)",
123                    name,
124                    orig_name,
125                    cfg_loc
126                ),
127            };
128            match &cfg.replace_with {
129                Some((s, c)) => {
130                    name = s;
131                    cfg_loc = c;
132                }
133                None if id == cfg.id => return Ok(id.load(self.config, yanked_whitelist)?),
134                None => {
135                    new_id = cfg.id.with_precise(id.precise().map(|s| s.to_string()));
136                    break;
137                }
138            }
139            debug!("following pointer to {}", name);
140            if name == orig_name {
141                bail!(
142                    "detected a cycle of `replace-with` sources, the source \
143                     `{}` is eventually replaced with itself \
144                     (configuration in `{}`)",
145                    name,
146                    cfg_loc
147                )
148            }
149        }
150
151        let new_src = new_id.load(
152            self.config,
153            &yanked_whitelist
154                .iter()
155                .map(|p| p.map_source(id, new_id))
156                .collect(),
157        )?;
158        let old_src = id.load(self.config, yanked_whitelist)?;
159        if !new_src.supports_checksums() && old_src.supports_checksums() {
160            bail!(
161                "\
162cannot replace `{orig}` with `{name}`, the source `{orig}` supports \
163checksums, but `{name}` does not
164
165a lock file compatible with `{orig}` cannot be generated in this situation
166",
167                orig = orig_name,
168                name = name
169            );
170        }
171
172        if old_src.requires_precise() && id.precise().is_none() {
173            bail!(
174                "\
175the source {orig} requires a lock file to be present first before it can be
176used against vendored source code
177
178remove the source replacement configuration, generate a lock file, and then
179restore the source replacement configuration to continue the build
180",
181                orig = orig_name
182            );
183        }
184
185        Ok(Box::new(ReplacedSource::new(id, new_id, new_src)))
186    }
187
188    fn add(&mut self, name: &str, cfg: SourceConfig) -> CargoResult<()> {
189        if let Some(old_name) = self.id2name.insert(cfg.id, name.to_string()) {
190            // The user is allowed to redefine the built-in crates-io
191            // definition from `empty()`.
192            if name != CRATES_IO_REGISTRY {
193                bail!(
194                    "source `{}` defines source {}, but that source is already defined by `{}`\n\
195                     note: Sources are not allowed to be defined multiple times.",
196                    name,
197                    cfg.id,
198                    old_name
199                );
200            }
201        }
202        self.cfgs.insert(name.to_string(), cfg);
203        Ok(())
204    }
205
206    fn add_config(&mut self, name: String, def: SourceConfigDef) -> CargoResult<()> {
207        let mut srcs = Vec::new();
208        if let Some(registry) = def.registry {
209            let url = url(&registry, &format!("source.{}.registry", name))?;
210            srcs.push(SourceId::for_registry(&url)?);
211        }
212        if let Some(local_registry) = def.local_registry {
213            let path = local_registry.resolve_path(self.config);
214            srcs.push(SourceId::for_local_registry(&path)?);
215        }
216        if let Some(directory) = def.directory {
217            let path = directory.resolve_path(self.config);
218            srcs.push(SourceId::for_directory(&path)?);
219        }
220        if let Some(git) = def.git {
221            let url = url(&git, &format!("source.{}.git", name))?;
222            let reference = match def.branch {
223                Some(b) => GitReference::Branch(b.val),
224                None => match def.tag {
225                    Some(b) => GitReference::Tag(b.val),
226                    None => match def.rev {
227                        Some(b) => GitReference::Rev(b.val),
228                        None => GitReference::Branch("master".to_string()),
229                    },
230                },
231            };
232            srcs.push(SourceId::for_git(&url, reference)?);
233        } else {
234            let check_not_set = |key, v: OptValue<String>| {
235                if let Some(val) = v {
236                    bail!(
237                        "source definition `source.{}` specifies `{}`, \
238                         but that requires a `git` key to be specified (in {})",
239                        name,
240                        key,
241                        val.definition
242                    );
243                }
244                Ok(())
245            };
246            check_not_set("branch", def.branch)?;
247            check_not_set("tag", def.tag)?;
248            check_not_set("rev", def.rev)?;
249        }
250        if name == "crates-io" && srcs.is_empty() {
251            srcs.push(SourceId::crates_io(self.config)?);
252        }
253
254        match srcs.len() {
255            0 => bail!(
256                "no source location specified for `source.{}`, need \
257                 `registry`, `local-registry`, `directory`, or `git` defined",
258                name
259            ),
260            1 => {}
261            _ => bail!(
262                "more than one source location specified for `source.{}`",
263                name
264            ),
265        }
266        let src = srcs[0];
267
268        let replace_with = def
269            .replace_with
270            .map(|val| (val.val, val.definition.to_string()));
271
272        self.add(
273            &name,
274            SourceConfig {
275                id: src,
276                replace_with,
277            },
278        )?;
279
280        return Ok(());
281
282        fn url(val: &config::Value<String>, key: &str) -> CargoResult<Url> {
283            let url = val.val.into_url().chain_err(|| {
284                format!(
285                    "configuration key `{}` specified an invalid \
286                     URL (in {})",
287                    key, val.definition
288                )
289            })?;
290
291            Ok(url)
292        }
293    }
294}