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