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