1use 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 cfgs: HashMap<String, SourceConfig>,
21 id2name: HashMap<SourceId, String>,
23 config: &'cfg Config,
24}
25
26#[derive(Debug, serde::Deserialize)]
28#[serde(rename_all = "kebab-case")]
29struct SourceConfigDef {
30 replace_with: OptValue<String>,
32 directory: Option<ConfigRelativePath>,
34 registry: OptValue<String>,
36 local_registry: Option<ConfigRelativePath>,
38 git: OptValue<String>,
40 branch: OptValue<String>,
42 tag: OptValue<String>,
44 rev: OptValue<String>,
46}
47
48#[derive(Clone)]
56struct SourceConfig {
57 id: SourceId,
60
61 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 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 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(®istry, &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}