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