]> git.proxmox.com Git - cargo.git/blob - src/cargo/ops/cargo_add/mod.rs
Extract finding a workspace dependency into its own function
[cargo.git] / src / cargo / ops / cargo_add / mod.rs
1 //! Core of cargo-add command
2
3 mod crate_spec;
4 mod dependency;
5 mod manifest;
6
7 use anyhow::Context;
8 use std::collections::BTreeSet;
9 use std::collections::VecDeque;
10 use std::path::Path;
11
12 use cargo_util::paths;
13 use indexmap::IndexSet;
14 use termcolor::Color::Green;
15 use termcolor::Color::Red;
16 use termcolor::ColorSpec;
17 use toml_edit::Item as TomlItem;
18
19 use crate::core::dependency::DepKind;
20 use crate::core::registry::PackageRegistry;
21 use crate::core::Package;
22 use crate::core::Registry;
23 use crate::core::Shell;
24 use crate::core::Workspace;
25 use crate::CargoResult;
26 use crate::Config;
27 use crate_spec::CrateSpec;
28 use dependency::Dependency;
29 use dependency::GitSource;
30 use dependency::PathSource;
31 use dependency::RegistrySource;
32 use dependency::Source;
33 use manifest::LocalManifest;
34
35 use crate::ops::cargo_add::dependency::MaybeWorkspace;
36 pub use manifest::DepTable;
37
38 /// Information on what dependencies should be added
39 #[derive(Clone, Debug)]
40 pub struct AddOptions<'a> {
41 /// Configuration information for cargo operations
42 pub config: &'a Config,
43 /// Package to add dependencies to
44 pub spec: &'a Package,
45 /// Dependencies to add or modify
46 pub dependencies: Vec<DepOp>,
47 /// Which dependency section to add these to
48 pub section: DepTable,
49 /// Act as if dependencies will be added
50 pub dry_run: bool,
51 }
52
53 /// Add dependencies to a manifest
54 pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<()> {
55 let dep_table = options
56 .section
57 .to_table()
58 .into_iter()
59 .map(String::from)
60 .collect::<Vec<_>>();
61
62 let manifest_path = options.spec.manifest_path().to_path_buf();
63 let mut manifest = LocalManifest::try_new(&manifest_path)?;
64 let legacy = manifest.get_legacy_sections();
65 if !legacy.is_empty() {
66 anyhow::bail!(
67 "Deprecated dependency sections are unsupported: {}",
68 legacy.join(", ")
69 );
70 }
71
72 let mut registry = PackageRegistry::new(options.config)?;
73
74 let deps = {
75 let _lock = options.config.acquire_package_cache_lock()?;
76 registry.lock_patches();
77 options
78 .dependencies
79 .iter()
80 .map(|raw| {
81 resolve_dependency(
82 &manifest,
83 raw,
84 workspace,
85 &options.section,
86 options.config,
87 &mut registry,
88 )
89 })
90 .collect::<CargoResult<Vec<_>>>()?
91 };
92
93 let was_sorted = manifest
94 .get_table(&dep_table)
95 .map(TomlItem::as_table)
96 .map_or(true, |table_option| {
97 table_option.map_or(true, |table| is_sorted(table.iter().map(|(name, _)| name)))
98 });
99 for dep in deps {
100 print_msg(&mut options.config.shell(), &dep, &dep_table)?;
101 if let Some(Source::Path(src)) = dep.source() {
102 if src.path == manifest.path.parent().unwrap_or_else(|| Path::new("")) {
103 anyhow::bail!(
104 "cannot add `{}` as a dependency to itself",
105 manifest.package_name()?
106 )
107 }
108 }
109
110 let available_features = dep
111 .available_features
112 .keys()
113 .map(|s| s.as_ref())
114 .collect::<BTreeSet<&str>>();
115 let mut unknown_features: Vec<&str> = Vec::new();
116 if let Some(req_feats) = dep.features.as_ref() {
117 let req_feats: BTreeSet<_> = req_feats.iter().map(|s| s.as_str()).collect();
118 unknown_features.extend(req_feats.difference(&available_features).copied());
119 }
120 if let Some(inherited_features) = dep.inherited_features.as_ref() {
121 let inherited_features: BTreeSet<_> =
122 inherited_features.iter().map(|s| s.as_str()).collect();
123 unknown_features.extend(inherited_features.difference(&available_features).copied());
124 }
125 unknown_features.sort();
126 if !unknown_features.is_empty() {
127 anyhow::bail!("unrecognized features: {unknown_features:?}");
128 }
129
130 manifest.insert_into_table(&dep_table, &dep)?;
131 manifest.gc_dep(dep.toml_key());
132 }
133
134 if was_sorted {
135 if let Some(table) = manifest
136 .get_table_mut(&dep_table)
137 .ok()
138 .and_then(TomlItem::as_table_like_mut)
139 {
140 table.sort_values();
141 }
142 }
143
144 if options.dry_run {
145 options.config.shell().warn("aborting add due to dry run")?;
146 } else {
147 manifest.write()?;
148 }
149
150 Ok(())
151 }
152
153 /// Dependency entry operation
154 #[derive(Clone, Debug, PartialEq, Eq)]
155 pub struct DepOp {
156 /// Describes the crate
157 pub crate_spec: Option<String>,
158 /// Dependency key, overriding the package name in crate_spec
159 pub rename: Option<String>,
160
161 /// Feature flags to activate
162 pub features: Option<IndexSet<String>>,
163 /// Whether the default feature should be activated
164 pub default_features: Option<bool>,
165
166 /// Whether dependency is optional
167 pub optional: Option<bool>,
168
169 /// Registry for looking up dependency version
170 pub registry: Option<String>,
171
172 /// Git repo for dependency
173 pub path: Option<String>,
174 /// Git repo for dependency
175 pub git: Option<String>,
176 /// Specify an alternative git branch
177 pub branch: Option<String>,
178 /// Specify a specific git rev
179 pub rev: Option<String>,
180 /// Specify a specific git tag
181 pub tag: Option<String>,
182 }
183
184 fn resolve_dependency(
185 manifest: &LocalManifest,
186 arg: &DepOp,
187 ws: &Workspace<'_>,
188 section: &DepTable,
189 config: &Config,
190 registry: &mut PackageRegistry<'_>,
191 ) -> CargoResult<Dependency> {
192 let crate_spec = arg
193 .crate_spec
194 .as_deref()
195 .map(CrateSpec::resolve)
196 .transpose()?;
197 let mut selected_dep = if let Some(url) = &arg.git {
198 let mut src = GitSource::new(url);
199 if let Some(branch) = &arg.branch {
200 src = src.set_branch(branch);
201 }
202 if let Some(tag) = &arg.tag {
203 src = src.set_tag(tag);
204 }
205 if let Some(rev) = &arg.rev {
206 src = src.set_rev(rev);
207 }
208
209 let selected = if let Some(crate_spec) = &crate_spec {
210 if let Some(v) = crate_spec.version_req() {
211 // crate specifier includes a version (e.g. `docopt@0.8`)
212 anyhow::bail!("cannot specify a git URL (`{url}`) with a version (`{v}`).");
213 }
214 let dependency = crate_spec.to_dependency()?.set_source(src);
215 let selected = select_package(&dependency, config, registry)?;
216 if dependency.name != selected.name {
217 config.shell().warn(format!(
218 "translating `{}` to `{}`",
219 dependency.name, selected.name,
220 ))?;
221 }
222 selected
223 } else {
224 let mut source = crate::sources::GitSource::new(src.source_id()?, config)?;
225 let packages = source.read_packages()?;
226 let package = infer_package(packages, &src)?;
227 Dependency::from(package.summary())
228 };
229 selected
230 } else if let Some(raw_path) = &arg.path {
231 let path = paths::normalize_path(&std::env::current_dir()?.join(raw_path));
232 let src = PathSource::new(&path);
233
234 let selected = if let Some(crate_spec) = &crate_spec {
235 if let Some(v) = crate_spec.version_req() {
236 // crate specifier includes a version (e.g. `docopt@0.8`)
237 anyhow::bail!("cannot specify a path (`{raw_path}`) with a version (`{v}`).");
238 }
239 let dependency = crate_spec.to_dependency()?.set_source(src);
240 let selected = select_package(&dependency, config, registry)?;
241 if dependency.name != selected.name {
242 config.shell().warn(format!(
243 "translating `{}` to `{}`",
244 dependency.name, selected.name,
245 ))?;
246 }
247 selected
248 } else {
249 let source = crate::sources::PathSource::new(&path, src.source_id()?, config);
250 let packages = source.read_packages()?;
251 let package = infer_package(packages, &src)?;
252 Dependency::from(package.summary())
253 };
254 selected
255 } else if let Some(crate_spec) = &crate_spec {
256 crate_spec.to_dependency()?
257 } else {
258 anyhow::bail!("dependency name is required");
259 };
260 selected_dep = populate_dependency(selected_dep, arg);
261
262 let old_dep = get_existing_dependency(manifest, selected_dep.toml_key(), section)?;
263 let mut dependency = if let Some(mut old_dep) = old_dep.clone() {
264 if old_dep.name != selected_dep.name {
265 // Assuming most existing keys are not relevant when the package changes
266 if selected_dep.optional.is_none() {
267 selected_dep.optional = old_dep.optional;
268 }
269 selected_dep
270 } else {
271 if selected_dep.source().is_some() {
272 // Overwrite with `crate_spec`
273 old_dep.source = selected_dep.source;
274 }
275 old_dep = populate_dependency(old_dep, arg);
276 old_dep.available_features = selected_dep.available_features;
277 old_dep
278 }
279 } else {
280 selected_dep
281 };
282
283 if dependency.source().is_none() {
284 if let Some(package) = ws.members().find(|p| p.name().as_str() == dependency.name) {
285 // Only special-case workspaces when the user doesn't provide any extra
286 // information, otherwise, trust the user.
287 let mut src = PathSource::new(package.root());
288 // dev-dependencies do not need the version populated
289 if section.kind() != DepKind::Development {
290 let op = "";
291 let v = format!("{op}{version}", version = package.version());
292 src = src.set_version(v);
293 }
294 dependency = dependency.set_source(src);
295 } else {
296 let latest = get_latest_dependency(&dependency, false, config, registry)?;
297
298 if dependency.name != latest.name {
299 config.shell().warn(format!(
300 "translating `{}` to `{}`",
301 dependency.name, latest.name,
302 ))?;
303 dependency.name = latest.name; // Normalize the name
304 }
305 dependency = dependency
306 .set_source(latest.source.expect("latest always has a source"))
307 .set_available_features(latest.available_features);
308 }
309 }
310
311 let version_required = dependency.source().and_then(|s| s.as_registry()).is_some();
312 let version_optional_in_section = section.kind() == DepKind::Development;
313 let preserve_existing_version = old_dep
314 .as_ref()
315 .map(|d| d.version().is_some())
316 .unwrap_or(false);
317 if !version_required && !preserve_existing_version && version_optional_in_section {
318 // dev-dependencies do not need the version populated
319 dependency = dependency.clear_version();
320 }
321
322 dependency = populate_available_features(dependency, config, registry, ws)?;
323
324 Ok(dependency)
325 }
326
327 /// Provide the existing dependency for the target table
328 ///
329 /// If it doesn't exist but exists in another table, let's use that as most likely users
330 /// want to use the same version across all tables unless they are renaming.
331 fn get_existing_dependency(
332 manifest: &LocalManifest,
333 dep_key: &str,
334 section: &DepTable,
335 ) -> CargoResult<Option<Dependency>> {
336 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
337 enum Key {
338 Error,
339 Dev,
340 Build,
341 Normal,
342 Existing,
343 }
344
345 let mut possible: Vec<_> = manifest
346 .get_dependency_versions(dep_key)
347 .map(|(path, dep)| {
348 let key = if path == *section {
349 (Key::Existing, true)
350 } else if dep.is_err() {
351 (Key::Error, path.target().is_some())
352 } else {
353 let key = match path.kind() {
354 DepKind::Normal => Key::Normal,
355 DepKind::Build => Key::Build,
356 DepKind::Development => Key::Dev,
357 };
358 (key, path.target().is_some())
359 };
360 (key, dep)
361 })
362 .collect();
363 possible.sort_by_key(|(key, _)| *key);
364 let (key, dep) = if let Some(item) = possible.pop() {
365 item
366 } else {
367 return Ok(None);
368 };
369 let mut dep = dep?;
370
371 if key.0 != Key::Existing {
372 // When the dep comes from a different section, we only care about the source and not any
373 // of the other fields, like `features`
374 let unrelated = dep;
375 dep = Dependency::new(&unrelated.name);
376 dep.source = unrelated.source.clone();
377 dep.registry = unrelated.registry.clone();
378
379 // dev-dependencies do not need the version populated when path is set though we
380 // should preserve it if the user chose to populate it.
381 let version_required = unrelated.source().and_then(|s| s.as_registry()).is_some();
382 let version_optional_in_section = section.kind() == DepKind::Development;
383 if !version_required && version_optional_in_section {
384 dep = dep.clear_version();
385 }
386 }
387
388 Ok(Some(dep))
389 }
390
391 fn get_latest_dependency(
392 dependency: &Dependency,
393 _flag_allow_prerelease: bool,
394 config: &Config,
395 registry: &mut PackageRegistry<'_>,
396 ) -> CargoResult<Dependency> {
397 let query = dependency.query(config)?;
398 match query {
399 MaybeWorkspace::Workspace(_) => {
400 unreachable!("registry dependencies required, found a workspace dependency");
401 }
402 MaybeWorkspace::Other(query) => {
403 let possibilities = loop {
404 let fuzzy = true;
405 match registry.query_vec(&query, fuzzy) {
406 std::task::Poll::Ready(res) => {
407 break res?;
408 }
409 std::task::Poll::Pending => registry.block_until_ready()?,
410 }
411 };
412 let latest = possibilities
413 .iter()
414 .max_by_key(|s| {
415 // Fallback to a pre-release if no official release is available by sorting them as
416 // less.
417 let stable = s.version().pre.is_empty();
418 (stable, s.version())
419 })
420 .ok_or_else(|| {
421 anyhow::format_err!(
422 "the crate `{dependency}` could not be found in registry index."
423 )
424 })?;
425 let mut dep = Dependency::from(latest);
426 if let Some(reg_name) = dependency.registry.as_deref() {
427 dep = dep.set_registry(reg_name);
428 }
429 Ok(dep)
430 }
431 }
432 }
433
434 fn select_package(
435 dependency: &Dependency,
436 config: &Config,
437 registry: &mut PackageRegistry<'_>,
438 ) -> CargoResult<Dependency> {
439 let query = dependency.query(config)?;
440 match query {
441 MaybeWorkspace::Workspace(_) => {
442 unreachable!("path or git dependency expected, found workspace dependency");
443 }
444 MaybeWorkspace::Other(query) => {
445 let possibilities = loop {
446 let fuzzy = false; // Returns all for path/git
447 match registry.query_vec(&query, fuzzy) {
448 std::task::Poll::Ready(res) => {
449 break res?;
450 }
451 std::task::Poll::Pending => registry.block_until_ready()?,
452 }
453 };
454 match possibilities.len() {
455 0 => {
456 let source = dependency
457 .source()
458 .expect("source should be resolved before here");
459 anyhow::bail!("the crate `{dependency}` could not be found at `{source}`")
460 }
461 1 => {
462 let mut dep = Dependency::from(&possibilities[0]);
463 if let Some(reg_name) = dependency.registry.as_deref() {
464 dep = dep.set_registry(reg_name);
465 }
466 Ok(dep)
467 }
468 _ => {
469 let source = dependency
470 .source()
471 .expect("source should be resolved before here");
472 anyhow::bail!(
473 "unexpectedly found multiple copies of crate `{dependency}` at `{source}`"
474 )
475 }
476 }
477 }
478 }
479 }
480
481 fn infer_package(mut packages: Vec<Package>, src: &dyn std::fmt::Display) -> CargoResult<Package> {
482 let package = match packages.len() {
483 0 => {
484 anyhow::bail!("no packages found at `{src}`");
485 }
486 1 => packages.pop().expect("match ensured element is present"),
487 _ => {
488 let mut names: Vec<_> = packages
489 .iter()
490 .map(|p| p.name().as_str().to_owned())
491 .collect();
492 names.sort_unstable();
493 anyhow::bail!("multiple packages found at `{src}`: {}", names.join(", "));
494 }
495 };
496 Ok(package)
497 }
498
499 fn populate_dependency(mut dependency: Dependency, arg: &DepOp) -> Dependency {
500 if let Some(registry) = &arg.registry {
501 if registry.is_empty() {
502 dependency.registry = None;
503 } else {
504 dependency.registry = Some(registry.to_owned());
505 }
506 }
507 if let Some(value) = arg.optional {
508 if value {
509 dependency.optional = Some(true);
510 } else {
511 dependency.optional = None;
512 }
513 }
514 if let Some(value) = arg.default_features {
515 if value {
516 dependency.default_features = None;
517 } else {
518 dependency.default_features = Some(false);
519 }
520 }
521 if let Some(value) = arg.features.as_ref() {
522 dependency = dependency.extend_features(value.iter().cloned());
523 }
524
525 if let Some(rename) = &arg.rename {
526 dependency = dependency.set_rename(rename);
527 }
528
529 dependency
530 }
531
532 /// Lookup available features
533 fn populate_available_features(
534 mut dependency: Dependency,
535 config: &Config,
536 registry: &mut PackageRegistry<'_>,
537 ws: &Workspace<'_>,
538 ) -> CargoResult<Dependency> {
539 if !dependency.available_features.is_empty() {
540 return Ok(dependency);
541 }
542
543 let query = dependency.query(config)?;
544 let query = match query {
545 MaybeWorkspace::Workspace(_workspace) => {
546 let dep = find_workspace_dep(dependency.toml_key(), ws.root_manifest())?;
547 if let Some(features) = dep.features.clone() {
548 dependency = dependency.set_inherited_features(features);
549 }
550 let query = dep.query(config)?;
551 match query {
552 MaybeWorkspace::Workspace(_) => {
553 unreachable!("This should have been caught when parsing a workspace root")
554 }
555 MaybeWorkspace::Other(query) => query,
556 }
557 }
558 MaybeWorkspace::Other(query) => query,
559 };
560 let possibilities = loop {
561 match registry.query_vec(&query, true) {
562 std::task::Poll::Ready(res) => {
563 break res?;
564 }
565 std::task::Poll::Pending => registry.block_until_ready()?,
566 }
567 };
568 // Ensure widest feature flag compatibility by picking the earliest version that could show up
569 // in the lock file for a given version requirement.
570 let lowest_common_denominator = possibilities
571 .iter()
572 .min_by_key(|s| {
573 // Fallback to a pre-release if no official release is available by sorting them as
574 // more.
575 let is_pre = !s.version().pre.is_empty();
576 (is_pre, s.version())
577 })
578 .ok_or_else(|| {
579 anyhow::format_err!("the crate `{dependency}` could not be found in registry index.")
580 })?;
581 dependency = dependency.set_available_features_from_cargo(lowest_common_denominator.features());
582
583 Ok(dependency)
584 }
585
586 fn print_msg(shell: &mut Shell, dep: &Dependency, section: &[String]) -> CargoResult<()> {
587 use std::fmt::Write;
588
589 if matches!(shell.verbosity(), crate::core::shell::Verbosity::Quiet) {
590 return Ok(());
591 }
592
593 let mut message = String::new();
594 write!(message, "{}", dep.name)?;
595 match dep.source() {
596 Some(Source::Registry(src)) => {
597 if src.version.chars().next().unwrap_or('0').is_ascii_digit() {
598 write!(message, " v{}", src.version)?;
599 } else {
600 write!(message, " {}", src.version)?;
601 }
602 }
603 Some(Source::Path(_)) => {
604 write!(message, " (local)")?;
605 }
606 Some(Source::Git(_)) => {
607 write!(message, " (git)")?;
608 }
609 Some(Source::Workspace(_)) => {
610 write!(message, " (workspace)")?;
611 }
612 None => {}
613 }
614 write!(message, " to")?;
615 if dep.optional().unwrap_or(false) {
616 write!(message, " optional")?;
617 }
618 let section = if section.len() == 1 {
619 section[0].clone()
620 } else {
621 format!("{} for target `{}`", &section[2], &section[1])
622 };
623 write!(message, " {section}")?;
624 write!(message, ".")?;
625 shell.status("Adding", message)?;
626
627 let mut activated: IndexSet<_> = dep.features.iter().flatten().map(|s| s.as_str()).collect();
628 if dep.default_features().unwrap_or(true) {
629 activated.insert("default");
630 }
631 activated.extend(dep.inherited_features.iter().flatten().map(|s| s.as_str()));
632 let mut walk: VecDeque<_> = activated.iter().cloned().collect();
633 while let Some(next) = walk.pop_front() {
634 walk.extend(
635 dep.available_features
636 .get(next)
637 .into_iter()
638 .flatten()
639 .map(|s| s.as_str()),
640 );
641 activated.extend(
642 dep.available_features
643 .get(next)
644 .into_iter()
645 .flatten()
646 .map(|s| s.as_str()),
647 );
648 }
649 activated.remove("default");
650 activated.sort();
651 let mut deactivated = dep
652 .available_features
653 .keys()
654 .filter(|f| !activated.contains(f.as_str()) && *f != "default")
655 .collect::<Vec<_>>();
656 deactivated.sort();
657 if !activated.is_empty() || !deactivated.is_empty() {
658 let prefix = format!("{:>13}", " ");
659 shell.write_stderr(format_args!("{}Features:\n", prefix), &ColorSpec::new())?;
660 for feat in activated {
661 shell.write_stderr(&prefix, &ColorSpec::new())?;
662 shell.write_stderr('+', &ColorSpec::new().set_bold(true).set_fg(Some(Green)))?;
663 shell.write_stderr(format_args!(" {}\n", feat), &ColorSpec::new())?;
664 }
665 for feat in deactivated {
666 shell.write_stderr(&prefix, &ColorSpec::new())?;
667 shell.write_stderr('-', &ColorSpec::new().set_bold(true).set_fg(Some(Red)))?;
668 shell.write_stderr(format_args!(" {}\n", feat), &ColorSpec::new())?;
669 }
670 }
671
672 Ok(())
673 }
674
675 // Based on Iterator::is_sorted from nightly std; remove in favor of that when stabilized.
676 fn is_sorted(mut it: impl Iterator<Item = impl PartialOrd>) -> bool {
677 let mut last = match it.next() {
678 Some(e) => e,
679 None => return true,
680 };
681
682 for curr in it {
683 if curr < last {
684 return false;
685 }
686 last = curr;
687 }
688
689 true
690 }
691
692 fn find_workspace_dep(toml_key: &str, root_manifest: &Path) -> CargoResult<Dependency> {
693 let manifest = LocalManifest::try_new(root_manifest)?;
694 let manifest = manifest
695 .data
696 .as_item()
697 .as_table_like()
698 .context("could not make `manifest.data` into a table")?;
699 let workspace = manifest
700 .get("workspace")
701 .context("could not find `workspace`")?
702 .as_table_like()
703 .context("could not make `manifest.data.workspace` into a table")?;
704 let dependencies = workspace
705 .get("dependencies")
706 .context("could not find `dependencies` table in `workspace`")?
707 .as_table_like()
708 .context("could not make `dependencies` into a table")?;
709 let dep_item = dependencies.get(toml_key).context(format!(
710 "could not find {} in `workspace.dependencies`",
711 toml_key
712 ))?;
713 Dependency::from_toml(root_manifest.parent().unwrap(), toml_key, dep_item)
714 }