1use std::collections::BTreeMap;
6use std::collections::BTreeSet;
7use std::collections::HashMap;
8use std::collections::HashSet;
9use std::fs::File;
10use std::io::Seek;
11use std::io::SeekFrom;
12use std::time::Duration;
13
14use anyhow::Context as _;
15use anyhow::bail;
16use cargo_credential::Operation;
17use cargo_credential::Secret;
18use cargo_util::paths;
19use cargo_util_terminal::report::Level;
20use crates_io::NewCrate;
21use crates_io::NewCrateDependency;
22use crates_io::Registry;
23use itertools::Itertools;
24
25use crate::CargoResult;
26use crate::GlobalContext;
27use crate::core::Dependency;
28use crate::core::Package;
29use crate::core::PackageId;
30use crate::core::PackageIdSpecQuery;
31use crate::core::SourceId;
32use crate::core::Workspace;
33use crate::core::dependency::DepKind;
34use crate::core::manifest::ManifestMetadata;
35use crate::core::resolver::CliFeatures;
36use crate::ops;
37use crate::ops::PackageOpts;
38use crate::ops::Packages;
39use crate::ops::RegistryOrIndex;
40use crate::ops::registry::RegistryClient;
41use crate::ops::registry::RegistrySourceIds;
42use crate::sources::CRATES_IO_REGISTRY;
43use crate::sources::RegistrySource;
44use crate::sources::SourceConfigMap;
45use crate::sources::source::QueryKind;
46use crate::sources::source::Source;
47use crate::util::Graph;
48use crate::util::Progress;
49use crate::util::ProgressStyle;
50use crate::util::VersionExt as _;
51use crate::util::auth;
52use crate::util::cache_lock::CacheLockMode;
53use crate::util::context::JobsConfig;
54use crate::util::errors::ManifestError;
55use crate::util::toml::prepare_for_publish;
56
57use super::super::check_dep_has_version;
58
59pub struct PublishOpts<'gctx> {
60 pub gctx: &'gctx GlobalContext,
61 pub token: Option<Secret<String>>,
62 pub reg_or_index: Option<RegistryOrIndex>,
63 pub verify: bool,
64 pub allow_dirty: bool,
65 pub jobs: Option<JobsConfig>,
66 pub keep_going: bool,
67 pub to_publish: ops::Packages,
68 pub targets: Vec<String>,
69 pub dry_run: bool,
70 pub cli_features: CliFeatures,
71}
72
73pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
74 let specs = opts.to_publish.to_package_id_specs(ws)?;
75
76 let member_ids: Vec<_> = ws.members().map(|p| p.package_id()).collect();
77 for spec in &specs {
79 spec.query(member_ids.clone())?;
80 }
81 let mut pkgs = ws.members_with_features(&specs, &opts.cli_features)?;
82 pkgs.retain(|(m, _)| specs.iter().any(|spec| spec.matches(m.package_id())));
85
86 let (unpublishable, pkgs): (Vec<_>, Vec<_>) = pkgs
87 .into_iter()
88 .partition(|(pkg, _)| pkg.publish() == &Some(vec![]));
89 let allow_unpublishable = match &opts.to_publish {
93 Packages::Default => ws.is_virtual(),
94 Packages::All(_) => true,
95 Packages::OptOut(_) => true,
96 Packages::Packages(_) => false,
97 };
98 if !unpublishable.is_empty() && !allow_unpublishable {
99 bail!(
100 "{} cannot be published.\n\
101 `package.publish` must be set to `true` or a non-empty list in Cargo.toml to publish.",
102 unpublishable
103 .iter()
104 .map(|(pkg, _)| format!("`{}`", pkg.name()))
105 .join(", "),
106 );
107 }
108
109 if pkgs.is_empty() {
110 if allow_unpublishable {
111 let n = unpublishable.len();
112 let plural = if n == 1 { "" } else { "s" };
113 ws.gctx().shell().print_report(
114 &[Level::WARNING
115 .secondary_title(format!(
116 "nothing to publish, but found {n} unpublishable package{plural}"
117 ))
118 .element(Level::HELP.message(
119 "to publish packages, set `package.publish` to `true` or a non-empty list",
120 ))],
121 false,
122 )?;
123 return Ok(());
124 } else {
125 unreachable!("must have at least one publishable package");
126 }
127 }
128
129 let just_pkgs: Vec<_> = pkgs.iter().map(|p| p.0).collect();
130 let reg_or_index = resolve_registry_or_index(opts, &just_pkgs)?;
131
132 let source_ids = super::get_source_id(opts.gctx, reg_or_index.as_ref())?;
135 let (mut registry, mut source) = super::registry(
136 opts.gctx,
137 &source_ids,
138 opts.token.as_ref().map(Secret::as_deref),
139 reg_or_index.as_ref(),
140 true,
141 Some(Operation::Read).filter(|_| !opts.dry_run),
142 )?;
143
144 {
145 let _lock = opts
146 .gctx
147 .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
148
149 for (pkg, _) in &pkgs {
150 verify_unpublished(pkg, &mut source, &source_ids, opts.dry_run, opts.gctx)?;
151 verify_dependencies(pkg, ®istry, source_ids.original).map_err(|err| {
152 ManifestError::new(
153 err.context(format!(
154 "failed to verify manifest at `{}`",
155 pkg.manifest_path().display()
156 )),
157 pkg.manifest_path().into(),
158 )
159 })?;
160 }
161 }
162
163 let pkg_dep_graph = ops::cargo_package::package_with_dep_graph(
164 ws,
165 &PackageOpts {
166 gctx: opts.gctx,
167 verify: opts.verify,
168 list: false,
169 fmt: ops::PackageMessageFormat::Human,
170 check_metadata: true,
171 allow_dirty: opts.allow_dirty,
172 include_lockfile: true,
173 to_package: ops::Packages::Default,
176 targets: opts.targets.clone(),
177 jobs: opts.jobs.clone(),
178 keep_going: opts.keep_going,
179 cli_features: opts.cli_features.clone(),
180 reg_or_index: reg_or_index.clone(),
181 dry_run: opts.dry_run,
182 },
183 pkgs,
184 )?;
185
186 let mut plan = PublishPlan::new(&pkg_dep_graph.graph);
187 let mut to_confirm = BTreeSet::new();
193
194 if plan.has_cycles() {
196 bail!(
197 "circular dependency detected while publishing {}\n\
198 help: to break a cycle between dev-dependencies \
199 and other dependencies, remove the version field \
200 on the dev-dependency so it will be implicitly \
201 stripped on publish",
202 package_list(plan.cycle_members(), "and")
203 );
204 }
205
206 while !plan.is_empty() {
207 let mut ready = plan.take_ready();
213
214 if ready.is_empty() {
215 return Err(crate::util::internal(format!(
218 "no packages ready to publish but {} packages remain in plan with {} awaiting confirmation: {}",
219 plan.len(),
220 to_confirm.len(),
221 package_list(plan.iter(), "and")
222 )));
223 }
224
225 while let Some(pkg_id) = ready.pop_first() {
226 let (pkg, (_features, tarball)) = &pkg_dep_graph.packages[&pkg_id];
227 opts.gctx.shell().status("Uploading", pkg.package_id())?;
228
229 if !opts.dry_run {
230 let ver = pkg.version().to_string();
231
232 tarball.file().seek(SeekFrom::Start(0))?;
233 let hash = cargo_util::Sha256::new()
234 .update_file(tarball.file())?
235 .finish_hex();
236 let operation = Operation::Publish {
237 name: pkg.name().as_str(),
238 vers: &ver,
239 cksum: &hash,
240 };
241 registry.set_token(Some(auth::auth_token(
242 &opts.gctx,
243 &source_ids.original,
244 None,
245 operation,
246 vec![],
247 false,
248 )?));
249 }
250
251 let workspace_context = || {
252 let mut remaining = ready.clone();
253 remaining.extend(plan.iter());
254 if !remaining.is_empty() {
255 format!(
256 "\n\nnote: the following crates have not been published yet:\n {}",
257 remaining.into_iter().join("\n ")
258 )
259 } else {
260 String::new()
261 }
262 };
263
264 transmit(
265 opts.gctx,
266 ws,
267 pkg,
268 tarball.file(),
269 &mut registry,
270 source_ids.original,
271 opts.dry_run,
272 workspace_context,
273 )?;
274 to_confirm.insert(pkg_id);
275
276 if !opts.dry_run {
277 let short_pkg_description = format!("{} v{}", pkg.name(), pkg.version());
279 let source_description = source_ids.original.to_string();
280 ws.gctx().shell().status(
281 "Uploaded",
282 format!("{short_pkg_description} to {source_description}"),
283 )?;
284 }
285 }
286
287 let confirmed = if opts.dry_run {
288 to_confirm.clone()
289 } else {
290 const DEFAULT_TIMEOUT: u64 = 60;
291 let timeout = if opts.gctx.cli_unstable().publish_timeout {
292 let timeout: Option<u64> = opts.gctx.get("publish.timeout")?;
293 timeout.unwrap_or(DEFAULT_TIMEOUT)
294 } else {
295 DEFAULT_TIMEOUT
296 };
297 if 0 < timeout {
298 let source_description = source.source_id().to_string();
299 let short_pkg_descriptions = package_list(to_confirm.iter().copied(), "or");
300 if plan.is_empty() {
301 let report = &[
302 cargo_util_terminal::report::Group::with_title(
303 cargo_util_terminal::report::Level::NOTE
304 .secondary_title(format!(
305 "waiting for {short_pkg_descriptions} to be available at {source_description}"
306 ))),
307 cargo_util_terminal::report::Group::with_title(cargo_util_terminal::report::Level::HELP.secondary_title(format!(
308 "you may press ctrl-c to skip waiting; the {crate} should be available shortly",
309 crate = if to_confirm.len() == 1 { "crate" } else {"crates"}
310 ))),
311 ];
312 opts.gctx.shell().print_report(report, false)?;
313 } else {
314 opts.gctx.shell().note(format!(
315 "waiting for {short_pkg_descriptions} to be available at {source_description}.\n\
316 {count} remaining {crate} to be published",
317 count = plan.len(),
318 crate = if plan.len() == 1 { "crate" } else {"crates"}
319 ))?;
320 }
321
322 let timeout = Duration::from_secs(timeout);
323 let confirmed = wait_for_any_publish_confirmation(
324 opts.gctx,
325 source_ids.original,
326 &to_confirm,
327 timeout,
328 )?;
329 if !confirmed.is_empty() {
330 let short_pkg_description = package_list(confirmed.iter().copied(), "and");
331 opts.gctx.shell().status(
332 "Published",
333 format!("{short_pkg_description} at {source_description}"),
334 )?;
335 } else {
336 let short_pkg_descriptions = package_list(to_confirm.iter().copied(), "or");
337 let krate = if to_confirm.len() == 1 {
338 "crate"
339 } else {
340 "crates"
341 };
342 opts.gctx.shell().print_report(
343 &[Level::WARNING
344 .secondary_title(format!(
345 "timed out waiting for {short_pkg_descriptions} \
346 to be available in {source_description}",
347 ))
348 .element(Level::NOTE.message(format!(
349 "the registry may have a backlog that is delaying making the \
350 {krate} available. The {krate} should be available soon.",
351 )))],
352 false,
353 )?;
354 }
355 confirmed
356 } else {
357 BTreeSet::new()
358 }
359 };
360 if confirmed.is_empty() {
361 if plan.is_empty() {
364 break;
367 } else {
368 let failed_list = package_list(plan.iter(), "and");
369 bail!(
370 "unable to publish {failed_list} due to a timeout while waiting for published dependencies to be available."
371 );
372 }
373 }
374 for id in &confirmed {
375 to_confirm.remove(id);
376 }
377 plan.mark_confirmed(confirmed);
378 }
379
380 Ok(())
381}
382
383fn wait_for_any_publish_confirmation(
388 gctx: &GlobalContext,
389 registry_src: SourceId,
390 pkgs: &BTreeSet<PackageId>,
391 timeout: Duration,
392) -> CargoResult<BTreeSet<PackageId>> {
393 let mut source = SourceConfigMap::empty(gctx)?.load(registry_src, &HashSet::new())?;
394 source.set_quiet(true);
398
399 let now = std::time::Instant::now();
400 let sleep_time = Duration::from_secs(1);
401 let max = timeout.as_secs() as usize;
402 let mut progress = Progress::with_style("Waiting", ProgressStyle::Ratio, gctx);
403 progress.tick_now(0, max, "")?;
404 let available = loop {
405 {
406 let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
407 gctx.updated_sources().remove(&source.replaced_source_id());
413 source.invalidate_cache();
414 let mut available = BTreeSet::new();
415 for pkg in pkgs {
416 if poll_one_package(registry_src, pkg, &mut *source)? {
417 available.insert(*pkg);
418 }
419 }
420
421 if !available.is_empty() {
424 break available;
425 }
426 }
427
428 let elapsed = now.elapsed();
429 if timeout < elapsed {
430 break BTreeSet::new();
431 }
432
433 progress.tick_now(elapsed.as_secs() as usize, max, "")?;
434 std::thread::sleep(sleep_time);
435 };
436
437 Ok(available)
438}
439
440fn poll_one_package(
441 registry_src: SourceId,
442 pkg_id: &PackageId,
443 source: &dyn Source,
444) -> CargoResult<bool> {
445 let version_req = format!("={}", pkg_id.version());
446 let query = Dependency::parse(pkg_id.name(), Some(&version_req), registry_src)?;
447 let summaries = crate::util::block_on(source.query_vec(&query, QueryKind::Exact))?;
449 Ok(!summaries.is_empty())
450}
451
452fn verify_unpublished(
453 pkg: &Package,
454 source: &mut RegistrySource<'_>,
455 source_ids: &RegistrySourceIds,
456 dry_run: bool,
457 gctx: &GlobalContext,
458) -> CargoResult<()> {
459 let query = Dependency::parse(
460 pkg.name(),
461 Some(&pkg.version().to_exact_req().to_string()),
462 source_ids.replacement,
463 )?;
464 let duplicate_query = crate::util::block_on(source.query_vec(&query, QueryKind::Exact))?;
465 if !duplicate_query.is_empty() {
466 if dry_run {
470 gctx.shell().warn(format!(
471 "crate {}@{} already exists on {}",
472 pkg.name(),
473 pkg.version(),
474 source.describe()
475 ))?;
476 } else {
477 bail!(
478 "crate {}@{} already exists on {}",
479 pkg.name(),
480 pkg.version(),
481 source.describe()
482 );
483 }
484 }
485
486 Ok(())
487}
488
489fn verify_dependencies(
490 pkg: &Package,
491 registry: &Registry<RegistryClient<'_>>,
492 registry_src: SourceId,
493) -> CargoResult<()> {
494 for dep in pkg.dependencies().iter() {
495 if check_dep_has_version(dep, true)? {
496 continue;
497 }
498 if dep.source_id() != registry_src {
501 if !dep.source_id().is_registry() {
502 panic!("unexpected source kind for dependency {:?}", dep);
506 }
507 if registry_src.is_crates_io() || registry.host_is_crates_io() {
512 bail!(
513 "crates cannot be published to crates.io with dependencies sourced from other\n\
514 registries. `{}` needs to be published to crates.io before publishing this crate.\n\
515 (crate `{}` is pulled from {})",
516 dep.package_name(),
517 dep.package_name(),
518 dep.source_id()
519 );
520 }
521 }
522 }
523 Ok(())
524}
525
526pub(crate) fn prepare_transmit(
527 gctx: &GlobalContext,
528 ws: &Workspace<'_>,
529 local_pkg: &Package,
530 registry_id: SourceId,
531) -> CargoResult<NewCrate> {
532 let included = None; let publish_pkg = prepare_for_publish(local_pkg, ws, included)?;
534
535 let deps = publish_pkg
536 .dependencies()
537 .iter()
538 .map(|dep| {
539 let dep_registry_id = match dep.registry_id() {
542 Some(id) => id,
543 None => SourceId::crates_io(gctx)?,
544 };
545 let dep_registry = if dep_registry_id != registry_id {
548 Some(dep_registry_id.url().to_string())
549 } else {
550 None
551 };
552
553 Ok(NewCrateDependency {
554 optional: dep.is_optional(),
555 default_features: dep.uses_default_features(),
556 name: dep.package_name().to_string(),
557 features: dep.features().iter().map(|s| s.to_string()).collect(),
558 version_req: dep.version_req().to_string(),
559 target: dep.platform().map(|s| s.to_string()),
560 kind: match dep.kind() {
561 DepKind::Normal => "normal",
562 DepKind::Build => "build",
563 DepKind::Development => "dev",
564 }
565 .to_string(),
566 registry: dep_registry,
567 explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()),
568 artifact: dep.artifact().map(|artifact| {
569 artifact
570 .kinds()
571 .iter()
572 .map(|x| x.as_str().into_owned())
573 .collect()
574 }),
575 bindep_target: dep.artifact().and_then(|artifact| {
576 artifact.target().map(|target| target.as_str().to_owned())
577 }),
578 lib: dep.artifact().map_or(false, |artifact| artifact.is_lib()),
579 })
580 })
581 .collect::<CargoResult<Vec<NewCrateDependency>>>()?;
582 let manifest = publish_pkg.manifest();
583 let ManifestMetadata {
584 ref authors,
585 ref description,
586 ref homepage,
587 ref documentation,
588 ref keywords,
589 ref readme,
590 ref repository,
591 ref license,
592 ref license_file,
593 ref categories,
594 ref badges,
595 ref links,
596 ref rust_version,
597 } = *manifest.metadata();
598 let rust_version = rust_version.as_ref().map(ToString::to_string);
599 let readme_content = local_pkg
600 .manifest()
601 .metadata()
602 .readme
603 .as_ref()
604 .map(|readme| {
605 paths::read(&local_pkg.root().join(readme)).with_context(|| {
606 format!("failed to read `readme` file for package `{}`", local_pkg)
607 })
608 })
609 .transpose()?;
610 if let Some(ref file) = local_pkg.manifest().metadata().license_file {
611 if !local_pkg.root().join(file).exists() {
612 bail!("the license file `{}` does not exist", file)
613 }
614 }
615
616 let string_features = match manifest.normalized_toml().features() {
617 Some(features) => features
618 .iter()
619 .map(|(feat, values)| {
620 (
621 feat.to_string(),
622 values.iter().map(|fv| fv.to_string()).collect(),
623 )
624 })
625 .collect::<BTreeMap<String, Vec<String>>>(),
626 None => BTreeMap::new(),
627 };
628
629 Ok(NewCrate {
630 name: publish_pkg.name().to_string(),
631 vers: publish_pkg.version().to_string(),
632 deps,
633 features: string_features,
634 authors: authors.clone(),
635 description: description.clone(),
636 homepage: homepage.clone(),
637 documentation: documentation.clone(),
638 keywords: keywords.clone(),
639 categories: categories.clone(),
640 readme: readme_content,
641 readme_file: readme.clone(),
642 repository: repository.clone(),
643 license: license.clone(),
644 license_file: license_file.clone(),
645 badges: badges.clone(),
646 links: links.clone(),
647 rust_version,
648 })
649}
650
651fn transmit(
652 gctx: &GlobalContext,
653 ws: &Workspace<'_>,
654 pkg: &Package,
655 tarball: &File,
656 registry: &mut Registry<RegistryClient<'_>>,
657 registry_id: SourceId,
658 dry_run: bool,
659 workspace_context: impl Fn() -> String,
660) -> CargoResult<()> {
661 let new_crate = prepare_transmit(gctx, ws, pkg, registry_id)?;
662
663 if dry_run {
665 gctx.shell().warn("aborting upload due to dry run")?;
666 return Ok(());
667 }
668
669 let warnings = registry.publish(&new_crate, tarball).with_context(|| {
670 format!(
671 "failed to publish {} v{} to registry at {}{}",
672 pkg.name(),
673 pkg.version(),
674 registry.host(),
675 workspace_context()
676 )
677 })?;
678
679 if !warnings.invalid_categories.is_empty() {
680 let msg = format!(
681 "the following are not valid category slugs and were ignored: {}",
682 warnings.invalid_categories.join(", ")
683 );
684 gctx.shell().print_report(
685 &[Level::WARNING
686 .secondary_title(msg)
687 .element(Level::HELP.message(
688 "please see <https://crates.io/category_slugs> for the list of all category slugs",
689 ))],
690 false,
691 )?;
692 }
693
694 if !warnings.invalid_badges.is_empty() {
695 let msg = format!(
696 "the following are not valid badges and were ignored: {}",
697 warnings.invalid_badges.join(", ")
698 );
699 gctx.shell().print_report(
700 &[Level::WARNING.secondary_title(msg).elements([
701 Level::NOTE.message(
702 "either the badge type specified is unknown or a required \
703 attribute is missing",
704 ),
705 Level::HELP.message(
706 "please see \
707 <https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata> \
708 for valid badge types and their required attributes",
709 ),
710 ])],
711 false,
712 )?;
713 }
714
715 if !warnings.other.is_empty() {
716 for msg in warnings.other {
717 gctx.shell().warn(&msg)?;
718 }
719 }
720
721 Ok(())
722}
723
724struct PublishPlan {
726 dependents: Graph<PackageId, ()>,
728 graph: Graph<PackageId, ()>,
730 dependencies_count: HashMap<PackageId, usize>,
732}
733
734impl PublishPlan {
735 fn new(graph: &Graph<PackageId, ()>) -> Self {
737 let dependents = graph.reversed();
738
739 let dependencies_count: HashMap<_, _> = dependents
740 .iter()
741 .map(|id| (*id, graph.edges(id).count()))
742 .collect();
743 Self {
744 dependents,
745 graph: graph.clone(),
746 dependencies_count,
747 }
748 }
749
750 fn iter(&self) -> impl Iterator<Item = PackageId> + '_ {
751 self.dependencies_count.iter().map(|(id, _)| *id)
752 }
753
754 fn is_empty(&self) -> bool {
755 self.dependencies_count.is_empty()
756 }
757
758 fn len(&self) -> usize {
759 self.dependencies_count.len()
760 }
761
762 fn has_cycles(&self) -> bool {
764 !self.cycle_members().is_empty()
765 }
766
767 fn cycle_members(&self) -> Vec<PackageId> {
769 let mut remaining: BTreeSet<_> = self.dependencies_count.keys().copied().collect();
770 loop {
771 let to_remove: Vec<_> = remaining
772 .iter()
773 .filter(|&id| {
774 self.graph
775 .edges(id)
776 .all(|(child, _)| !remaining.contains(child))
777 })
778 .copied()
779 .collect();
780 if to_remove.is_empty() {
781 break;
782 }
783 for id in to_remove {
784 remaining.remove(&id);
785 }
786 }
787 remaining.into_iter().collect()
788 }
789
790 fn take_ready(&mut self) -> BTreeSet<PackageId> {
794 let ready: BTreeSet<_> = self
795 .dependencies_count
796 .iter()
797 .filter_map(|(id, weight)| (*weight == 0).then_some(*id))
798 .collect();
799 for pkg in &ready {
800 self.dependencies_count.remove(pkg);
801 }
802 ready
803 }
804
805 fn mark_confirmed(&mut self, published: impl IntoIterator<Item = PackageId>) {
808 for id in published {
809 for (dependent_id, _) in self.dependents.edges(&id) {
810 if let Some(weight) = self.dependencies_count.get_mut(dependent_id) {
811 *weight = weight.saturating_sub(1);
812 }
813 }
814 }
815 }
816}
817
818fn package_list(pkgs: impl IntoIterator<Item = PackageId>, final_sep: &str) -> String {
824 let mut names: Vec<_> = pkgs
825 .into_iter()
826 .map(|pkg| format!("{} v{}", pkg.name(), pkg.version()))
827 .collect();
828 names.sort();
829
830 match &names[..] {
831 [] => String::new(),
832 [a] => a.clone(),
833 [a, b] => format!("{a} {final_sep} {b}"),
834 [names @ .., last] => {
835 format!("{}, {final_sep} {last}", names.join(", "))
836 }
837 }
838}
839
840fn resolve_registry_or_index(
841 opts: &PublishOpts<'_>,
842 just_pkgs: &[&Package],
843) -> CargoResult<Option<RegistryOrIndex>> {
844 let opt_index_or_registry = opts.reg_or_index.clone();
845
846 let res = match opt_index_or_registry {
847 ref r @ Some(ref registry_or_index) => {
848 validate_registry(just_pkgs, r.as_ref())?;
849
850 let registry_is_specified_by_any_package = just_pkgs
851 .iter()
852 .any(|pkg| pkg.publish().as_ref().map(|v| v.len()).unwrap_or(0) > 0);
853
854 if registry_is_specified_by_any_package && registry_or_index.is_index() {
855 opts.gctx.shell().warn(r#"`--index` will ignore registries set by `package.publish` in Cargo.toml, and may cause unexpected push to prohibited registry
856help: use `--registry` instead or set `publish = true` in Cargo.toml to suppress this warning"#)?;
857 }
858
859 r.clone()
860 }
861 None => {
862 let reg = super::infer_registry(&just_pkgs)?;
863 validate_registry(&just_pkgs, reg.as_ref())?;
864 if let Some(RegistryOrIndex::Registry(registry)) = ® {
865 if registry != CRATES_IO_REGISTRY {
866 opts.gctx.shell().note(&format!(
868 "found `{}` as only allowed registry. Publishing to it automatically.",
869 registry
870 ))?;
871 }
872 }
873 reg
874 }
875 };
876
877 Ok(res)
878}
879
880fn validate_registry(pkgs: &[&Package], reg_or_index: Option<&RegistryOrIndex>) -> CargoResult<()> {
881 let reg_name = match reg_or_index {
882 Some(RegistryOrIndex::Registry(r)) => Some(r.as_str()),
883 None => Some(CRATES_IO_REGISTRY),
884 Some(RegistryOrIndex::Index(_)) => None,
885 };
886 if let Some(reg_name) = reg_name {
887 for pkg in pkgs {
888 if let Some(allowed) = pkg.publish().as_ref() {
889 if !allowed.iter().any(|a| a == reg_name) {
890 bail!(
891 "`{}` cannot be published.\n\
892 The registry `{}` is not listed in the `package.publish` value in Cargo.toml.",
893 pkg.name(),
894 reg_name
895 );
896 }
897 }
898 }
899 }
900
901 Ok(())
902}
903
904#[cfg(test)]
905mod tests {
906 use crate::{
907 core::{PackageId, SourceId},
908 sources::CRATES_IO_INDEX,
909 util::{Graph, IntoUrl},
910 };
911
912 use super::PublishPlan;
913
914 fn pkg_id(name: &str) -> PackageId {
915 let loc = CRATES_IO_INDEX.into_url().unwrap();
916 PackageId::try_new(name, "1.0.0", SourceId::for_registry(&loc).unwrap()).unwrap()
917 }
918
919 #[test]
920 fn parallel_schedule() {
921 let mut graph: Graph<PackageId, ()> = Graph::new();
922 let a = pkg_id("a");
923 let b = pkg_id("b");
924 let c = pkg_id("c");
925 let d = pkg_id("d");
926 let e = pkg_id("e");
927
928 graph.add(a);
929 graph.add(b);
930 graph.add(c);
931 graph.add(d);
932 graph.add(e);
933 graph.link(a, c);
934 graph.link(b, c);
935 graph.link(c, d);
936 graph.link(c, e);
937
938 let mut order = PublishPlan::new(&graph);
939 let ready: Vec<_> = order.take_ready().into_iter().collect();
940 assert_eq!(ready, vec![d, e]);
941
942 order.mark_confirmed(vec![d]);
943 let ready: Vec<_> = order.take_ready().into_iter().collect();
944 assert!(ready.is_empty());
945
946 order.mark_confirmed(vec![e]);
947 let ready: Vec<_> = order.take_ready().into_iter().collect();
948 assert_eq!(ready, vec![c]);
949
950 order.mark_confirmed(vec![c]);
951 let ready: Vec<_> = order.take_ready().into_iter().collect();
952 assert_eq!(ready, vec![a, b]);
953
954 order.mark_confirmed(vec![a, b]);
955 let ready: Vec<_> = order.take_ready().into_iter().collect();
956 assert!(ready.is_empty());
957 }
958}