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::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 if plan.has_cycles() {
195 bail!(
196 "circular dependency detected while publishing {}\n\
197 help: to break a cycle between dev-dependencies \
198 and other dependencies, remove the version field \
199 on the dev-dependency so it will be implicitly \
200 stripped on publish",
201 package_list(plan.cycle_members(), "and")
202 );
203 }
204
205 while !plan.is_empty() {
206 let mut ready = plan.take_ready();
212
213 if ready.is_empty() {
214 return Err(crate::util::internal(format!(
217 "no packages ready to publish but {} packages remain in plan with {} awaiting confirmation: {}",
218 plan.len(),
219 to_confirm.len(),
220 package_list(plan.iter(), "and")
221 )));
222 }
223
224 while let Some(pkg_id) = ready.pop_first() {
225 let (pkg, (_features, tarball)) = &pkg_dep_graph.packages[&pkg_id];
226 opts.gctx.shell().status("Uploading", pkg.package_id())?;
227
228 if !opts.dry_run {
229 let ver = pkg.version().to_string();
230
231 tarball.file().seek(SeekFrom::Start(0))?;
232 let hash = cargo_util::Sha256::new()
233 .update_file(tarball.file())?
234 .finish_hex();
235 let operation = Operation::Publish {
236 name: pkg.name().as_str(),
237 vers: &ver,
238 cksum: &hash,
239 };
240 registry.set_token(Some(auth::auth_token(
241 &opts.gctx,
242 &source_ids.original,
243 None,
244 operation,
245 vec![],
246 false,
247 )?));
248 }
249
250 let workspace_context = || {
251 let mut remaining = ready.clone();
252 remaining.extend(plan.iter());
253 if !remaining.is_empty() {
254 format!(
255 "\n\nnote: the following crates have not been published yet:\n {}",
256 remaining.into_iter().join("\n ")
257 )
258 } else {
259 String::new()
260 }
261 };
262
263 transmit(
264 opts.gctx,
265 ws,
266 pkg,
267 tarball.file(),
268 &mut registry,
269 source_ids.original,
270 opts.dry_run,
271 workspace_context,
272 )?;
273 to_confirm.insert(pkg_id);
274
275 if !opts.dry_run {
276 let short_pkg_description = format!("{} v{}", pkg.name(), pkg.version());
278 let source_description = source_ids.original.to_string();
279 ws.gctx().shell().status(
280 "Uploaded",
281 format!("{short_pkg_description} to {source_description}"),
282 )?;
283 }
284 }
285
286 let confirmed = if opts.dry_run {
287 to_confirm.clone()
288 } else {
289 const DEFAULT_TIMEOUT: u64 = 60;
290 let timeout = if opts.gctx.cli_unstable().publish_timeout {
291 let timeout: Option<u64> = opts.gctx.get("publish.timeout")?;
292 timeout.unwrap_or(DEFAULT_TIMEOUT)
293 } else {
294 DEFAULT_TIMEOUT
295 };
296 if 0 < timeout {
297 let source_description = source.source_id().to_string();
298 let short_pkg_descriptions = package_list(to_confirm.iter().copied(), "or");
299 if plan.is_empty() {
300 let report = &[
301 cargo_util_terminal::report::Group::with_title(
302 cargo_util_terminal::report::Level::NOTE
303 .secondary_title(format!(
304 "waiting for {short_pkg_descriptions} to be available at {source_description}"
305 ))),
306 cargo_util_terminal::report::Group::with_title(cargo_util_terminal::report::Level::HELP.secondary_title(format!(
307 "you may press ctrl-c to skip waiting; the {crate} should be available shortly",
308 crate = if to_confirm.len() == 1 { "crate" } else {"crates"}
309 ))),
310 ];
311 opts.gctx.shell().print_report(report, false)?;
312 } else {
313 opts.gctx.shell().note(format!(
314 "waiting for {short_pkg_descriptions} to be available at {source_description}.\n\
315 {count} remaining {crate} to be published",
316 count = plan.len(),
317 crate = if plan.len() == 1 { "crate" } else {"crates"}
318 ))?;
319 }
320
321 let timeout = Duration::from_secs(timeout);
322 let confirmed = wait_for_any_publish_confirmation(
323 opts.gctx,
324 source_ids.original,
325 &to_confirm,
326 timeout,
327 )?;
328 if !confirmed.is_empty() {
329 let short_pkg_description = package_list(confirmed.iter().copied(), "and");
330 opts.gctx.shell().status(
331 "Published",
332 format!("{short_pkg_description} at {source_description}"),
333 )?;
334 } else {
335 let short_pkg_descriptions = package_list(to_confirm.iter().copied(), "or");
336 let krate = if to_confirm.len() == 1 {
337 "crate"
338 } else {
339 "crates"
340 };
341 opts.gctx.shell().print_report(
342 &[Level::WARNING
343 .secondary_title(format!(
344 "timed out waiting for {short_pkg_descriptions} \
345 to be available in {source_description}",
346 ))
347 .element(Level::NOTE.message(format!(
348 "the registry may have a backlog that is delaying making the \
349 {krate} available. The {krate} should be available soon.",
350 )))],
351 false,
352 )?;
353 }
354 confirmed
355 } else {
356 BTreeSet::new()
357 }
358 };
359 if confirmed.is_empty() {
360 if plan.is_empty() {
363 break;
366 } else {
367 let failed_list = package_list(plan.iter(), "and");
368 bail!(
369 "unable to publish {failed_list} due to a timeout while waiting for published dependencies to be available."
370 );
371 }
372 }
373 for id in &confirmed {
374 to_confirm.remove(id);
375 }
376 plan.mark_confirmed(confirmed);
377 }
378
379 Ok(())
380}
381
382fn wait_for_any_publish_confirmation(
387 gctx: &GlobalContext,
388 registry_src: SourceId,
389 pkgs: &BTreeSet<PackageId>,
390 timeout: Duration,
391) -> CargoResult<BTreeSet<PackageId>> {
392 let mut source = SourceConfigMap::empty(gctx)?.load(registry_src, &HashSet::new())?;
393 source.set_quiet(true);
397
398 let now = std::time::Instant::now();
399 let sleep_time = Duration::from_secs(1);
400 let max = timeout.as_secs() as usize;
401 let mut progress = Progress::with_style("Waiting", ProgressStyle::Ratio, gctx);
402 progress.tick_now(0, max, "")?;
403 let available = loop {
404 {
405 let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
406 gctx.updated_sources().remove(&source.replaced_source_id());
412 source.invalidate_cache();
413 let mut available = BTreeSet::new();
414 for pkg in pkgs {
415 if poll_one_package(registry_src, pkg, &mut *source)? {
416 available.insert(*pkg);
417 }
418 }
419
420 if !available.is_empty() {
423 break available;
424 }
425 }
426
427 let elapsed = now.elapsed();
428 if timeout < elapsed {
429 break BTreeSet::new();
430 }
431
432 progress.tick_now(elapsed.as_secs() as usize, max, "")?;
433 std::thread::sleep(sleep_time);
434 };
435
436 Ok(available)
437}
438
439fn poll_one_package(
440 registry_src: SourceId,
441 pkg_id: &PackageId,
442 source: &dyn Source,
443) -> CargoResult<bool> {
444 let version_req = format!("={}", pkg_id.version());
445 let query = Dependency::parse(pkg_id.name(), Some(&version_req), registry_src)?;
446 let summaries = crate::util::block_on(source.query_vec(&query, QueryKind::Exact))?;
448 Ok(!summaries.is_empty())
449}
450
451fn verify_unpublished(
452 pkg: &Package,
453 source: &mut RegistrySource<'_>,
454 source_ids: &RegistrySourceIds,
455 dry_run: bool,
456 gctx: &GlobalContext,
457) -> CargoResult<()> {
458 let query = Dependency::parse(
459 pkg.name(),
460 Some(&pkg.version().to_exact_req().to_string()),
461 source_ids.replacement,
462 )?;
463 let duplicate_query = crate::util::block_on(source.query_vec(&query, QueryKind::Exact))?;
464 if !duplicate_query.is_empty() {
465 if dry_run {
469 gctx.shell().warn(format!(
470 "crate {}@{} already exists on {}",
471 pkg.name(),
472 pkg.version(),
473 source.describe()
474 ))?;
475 } else {
476 bail!(
477 "crate {}@{} already exists on {}",
478 pkg.name(),
479 pkg.version(),
480 source.describe()
481 );
482 }
483 }
484
485 Ok(())
486}
487
488fn verify_dependencies(
489 pkg: &Package,
490 registry: &Registry,
491 registry_src: SourceId,
492) -> CargoResult<()> {
493 for dep in pkg.dependencies().iter() {
494 if check_dep_has_version(dep, true)? {
495 continue;
496 }
497 if dep.source_id() != registry_src {
500 if !dep.source_id().is_registry() {
501 panic!("unexpected source kind for dependency {:?}", dep);
505 }
506 if registry_src.is_crates_io() || registry.host_is_crates_io() {
511 bail!(
512 "crates cannot be published to crates.io with dependencies sourced from other\n\
513 registries. `{}` needs to be published to crates.io before publishing this crate.\n\
514 (crate `{}` is pulled from {})",
515 dep.package_name(),
516 dep.package_name(),
517 dep.source_id()
518 );
519 }
520 }
521 }
522 Ok(())
523}
524
525pub(crate) fn prepare_transmit(
526 gctx: &GlobalContext,
527 ws: &Workspace<'_>,
528 local_pkg: &Package,
529 registry_id: SourceId,
530) -> CargoResult<NewCrate> {
531 let included = None; let publish_pkg = prepare_for_publish(local_pkg, ws, included)?;
533
534 let deps = publish_pkg
535 .dependencies()
536 .iter()
537 .map(|dep| {
538 let dep_registry_id = match dep.registry_id() {
541 Some(id) => id,
542 None => SourceId::crates_io(gctx)?,
543 };
544 let dep_registry = if dep_registry_id != registry_id {
547 Some(dep_registry_id.url().to_string())
548 } else {
549 None
550 };
551
552 Ok(NewCrateDependency {
553 optional: dep.is_optional(),
554 default_features: dep.uses_default_features(),
555 name: dep.package_name().to_string(),
556 features: dep.features().iter().map(|s| s.to_string()).collect(),
557 version_req: dep.version_req().to_string(),
558 target: dep.platform().map(|s| s.to_string()),
559 kind: match dep.kind() {
560 DepKind::Normal => "normal",
561 DepKind::Build => "build",
562 DepKind::Development => "dev",
563 }
564 .to_string(),
565 registry: dep_registry,
566 explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()),
567 artifact: dep.artifact().map(|artifact| {
568 artifact
569 .kinds()
570 .iter()
571 .map(|x| x.as_str().into_owned())
572 .collect()
573 }),
574 bindep_target: dep.artifact().and_then(|artifact| {
575 artifact.target().map(|target| target.as_str().to_owned())
576 }),
577 lib: dep.artifact().map_or(false, |artifact| artifact.is_lib()),
578 })
579 })
580 .collect::<CargoResult<Vec<NewCrateDependency>>>()?;
581 let manifest = publish_pkg.manifest();
582 let ManifestMetadata {
583 ref authors,
584 ref description,
585 ref homepage,
586 ref documentation,
587 ref keywords,
588 ref readme,
589 ref repository,
590 ref license,
591 ref license_file,
592 ref categories,
593 ref badges,
594 ref links,
595 ref rust_version,
596 } = *manifest.metadata();
597 let rust_version = rust_version.as_ref().map(ToString::to_string);
598 let readme_content = local_pkg
599 .manifest()
600 .metadata()
601 .readme
602 .as_ref()
603 .map(|readme| {
604 paths::read(&local_pkg.root().join(readme)).with_context(|| {
605 format!("failed to read `readme` file for package `{}`", local_pkg)
606 })
607 })
608 .transpose()?;
609 if let Some(ref file) = local_pkg.manifest().metadata().license_file {
610 if !local_pkg.root().join(file).exists() {
611 bail!("the license file `{}` does not exist", file)
612 }
613 }
614
615 let string_features = match manifest.normalized_toml().features() {
616 Some(features) => features
617 .iter()
618 .map(|(feat, values)| {
619 (
620 feat.to_string(),
621 values.iter().map(|fv| fv.to_string()).collect(),
622 )
623 })
624 .collect::<BTreeMap<String, Vec<String>>>(),
625 None => BTreeMap::new(),
626 };
627
628 Ok(NewCrate {
629 name: publish_pkg.name().to_string(),
630 vers: publish_pkg.version().to_string(),
631 deps,
632 features: string_features,
633 authors: authors.clone(),
634 description: description.clone(),
635 homepage: homepage.clone(),
636 documentation: documentation.clone(),
637 keywords: keywords.clone(),
638 categories: categories.clone(),
639 readme: readme_content,
640 readme_file: readme.clone(),
641 repository: repository.clone(),
642 license: license.clone(),
643 license_file: license_file.clone(),
644 badges: badges.clone(),
645 links: links.clone(),
646 rust_version,
647 })
648}
649
650fn transmit(
651 gctx: &GlobalContext,
652 ws: &Workspace<'_>,
653 pkg: &Package,
654 tarball: &File,
655 registry: &mut Registry,
656 registry_id: SourceId,
657 dry_run: bool,
658 workspace_context: impl Fn() -> String,
659) -> CargoResult<()> {
660 let new_crate = prepare_transmit(gctx, ws, pkg, registry_id)?;
661
662 if dry_run {
664 gctx.shell().warn("aborting upload due to dry run")?;
665 return Ok(());
666 }
667
668 let warnings = registry.publish(&new_crate, tarball).with_context(|| {
669 format!(
670 "failed to publish {} v{} to registry at {}{}",
671 pkg.name(),
672 pkg.version(),
673 registry.host(),
674 workspace_context()
675 )
676 })?;
677
678 if !warnings.invalid_categories.is_empty() {
679 let msg = format!(
680 "the following are not valid category slugs and were ignored: {}",
681 warnings.invalid_categories.join(", ")
682 );
683 gctx.shell().print_report(
684 &[Level::WARNING
685 .secondary_title(msg)
686 .element(Level::HELP.message(
687 "please see <https://crates.io/category_slugs> for the list of all category slugs",
688 ))],
689 false,
690 )?;
691 }
692
693 if !warnings.invalid_badges.is_empty() {
694 let msg = format!(
695 "the following are not valid badges and were ignored: {}",
696 warnings.invalid_badges.join(", ")
697 );
698 gctx.shell().print_report(
699 &[Level::WARNING.secondary_title(msg).elements([
700 Level::NOTE.message(
701 "either the badge type specified is unknown or a required \
702 attribute is missing",
703 ),
704 Level::HELP.message(
705 "please see \
706 <https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata> \
707 for valid badge types and their required attributes",
708 ),
709 ])],
710 false,
711 )?;
712 }
713
714 if !warnings.other.is_empty() {
715 for msg in warnings.other {
716 gctx.shell().warn(&msg)?;
717 }
718 }
719
720 Ok(())
721}
722
723struct PublishPlan {
725 dependents: Graph<PackageId, ()>,
727 graph: Graph<PackageId, ()>,
729 dependencies_count: HashMap<PackageId, usize>,
731}
732
733impl PublishPlan {
734 fn new(graph: &Graph<PackageId, ()>) -> Self {
736 let dependents = graph.reversed();
737
738 let dependencies_count: HashMap<_, _> = dependents
739 .iter()
740 .map(|id| (*id, graph.edges(id).count()))
741 .collect();
742 Self {
743 dependents,
744 graph: graph.clone(),
745 dependencies_count,
746 }
747 }
748
749 fn iter(&self) -> impl Iterator<Item = PackageId> + '_ {
750 self.dependencies_count.iter().map(|(id, _)| *id)
751 }
752
753 fn is_empty(&self) -> bool {
754 self.dependencies_count.is_empty()
755 }
756
757 fn len(&self) -> usize {
758 self.dependencies_count.len()
759 }
760
761 fn has_cycles(&self) -> bool {
763 !self.cycle_members().is_empty()
764 }
765
766 fn cycle_members(&self) -> Vec<PackageId> {
768 let mut remaining: BTreeSet<_> = self.dependencies_count.keys().copied().collect();
769 loop {
770 let to_remove: Vec<_> = remaining
771 .iter()
772 .filter(|&id| {
773 self.graph
774 .edges(id)
775 .all(|(child, _)| !remaining.contains(child))
776 })
777 .copied()
778 .collect();
779 if to_remove.is_empty() {
780 break;
781 }
782 for id in to_remove {
783 remaining.remove(&id);
784 }
785 }
786 remaining.into_iter().collect()
787 }
788
789 fn take_ready(&mut self) -> BTreeSet<PackageId> {
793 let ready: BTreeSet<_> = self
794 .dependencies_count
795 .iter()
796 .filter_map(|(id, weight)| (*weight == 0).then_some(*id))
797 .collect();
798 for pkg in &ready {
799 self.dependencies_count.remove(pkg);
800 }
801 ready
802 }
803
804 fn mark_confirmed(&mut self, published: impl IntoIterator<Item = PackageId>) {
807 for id in published {
808 for (dependent_id, _) in self.dependents.edges(&id) {
809 if let Some(weight) = self.dependencies_count.get_mut(dependent_id) {
810 *weight = weight.saturating_sub(1);
811 }
812 }
813 }
814 }
815}
816
817fn package_list(pkgs: impl IntoIterator<Item = PackageId>, final_sep: &str) -> String {
823 let mut names: Vec<_> = pkgs
824 .into_iter()
825 .map(|pkg| format!("{} v{}", pkg.name(), pkg.version()))
826 .collect();
827 names.sort();
828
829 match &names[..] {
830 [] => String::new(),
831 [a] => a.clone(),
832 [a, b] => format!("{a} {final_sep} {b}"),
833 [names @ .., last] => {
834 format!("{}, {final_sep} {last}", names.join(", "))
835 }
836 }
837}
838
839fn resolve_registry_or_index(
840 opts: &PublishOpts<'_>,
841 just_pkgs: &[&Package],
842) -> CargoResult<Option<RegistryOrIndex>> {
843 let opt_index_or_registry = opts.reg_or_index.clone();
844
845 let res = match opt_index_or_registry {
846 ref r @ Some(ref registry_or_index) => {
847 validate_registry(just_pkgs, r.as_ref())?;
848
849 let registry_is_specified_by_any_package = just_pkgs
850 .iter()
851 .any(|pkg| pkg.publish().as_ref().map(|v| v.len()).unwrap_or(0) > 0);
852
853 if registry_is_specified_by_any_package && registry_or_index.is_index() {
854 opts.gctx.shell().warn(r#"`--index` will ignore registries set by `package.publish` in Cargo.toml, and may cause unexpected push to prohibited registry
855help: use `--registry` instead or set `publish = true` in Cargo.toml to suppress this warning"#)?;
856 }
857
858 r.clone()
859 }
860 None => {
861 let reg = super::infer_registry(&just_pkgs)?;
862 validate_registry(&just_pkgs, reg.as_ref())?;
863 if let Some(RegistryOrIndex::Registry(registry)) = ® {
864 if registry != CRATES_IO_REGISTRY {
865 opts.gctx.shell().note(&format!(
867 "found `{}` as only allowed registry. Publishing to it automatically.",
868 registry
869 ))?;
870 }
871 }
872 reg
873 }
874 };
875
876 Ok(res)
877}
878
879fn validate_registry(pkgs: &[&Package], reg_or_index: Option<&RegistryOrIndex>) -> CargoResult<()> {
880 let reg_name = match reg_or_index {
881 Some(RegistryOrIndex::Registry(r)) => Some(r.as_str()),
882 None => Some(CRATES_IO_REGISTRY),
883 Some(RegistryOrIndex::Index(_)) => None,
884 };
885 if let Some(reg_name) = reg_name {
886 for pkg in pkgs {
887 if let Some(allowed) = pkg.publish().as_ref() {
888 if !allowed.iter().any(|a| a == reg_name) {
889 bail!(
890 "`{}` cannot be published.\n\
891 The registry `{}` is not listed in the `package.publish` value in Cargo.toml.",
892 pkg.name(),
893 reg_name
894 );
895 }
896 }
897 }
898 }
899
900 Ok(())
901}
902
903#[cfg(test)]
904mod tests {
905 use crate::{
906 core::{PackageId, SourceId},
907 sources::CRATES_IO_INDEX,
908 util::{Graph, IntoUrl},
909 };
910
911 use super::PublishPlan;
912
913 fn pkg_id(name: &str) -> PackageId {
914 let loc = CRATES_IO_INDEX.into_url().unwrap();
915 PackageId::try_new(name, "1.0.0", SourceId::for_registry(&loc).unwrap()).unwrap()
916 }
917
918 #[test]
919 fn parallel_schedule() {
920 let mut graph: Graph<PackageId, ()> = Graph::new();
921 let a = pkg_id("a");
922 let b = pkg_id("b");
923 let c = pkg_id("c");
924 let d = pkg_id("d");
925 let e = pkg_id("e");
926
927 graph.add(a);
928 graph.add(b);
929 graph.add(c);
930 graph.add(d);
931 graph.add(e);
932 graph.link(a, c);
933 graph.link(b, c);
934 graph.link(c, d);
935 graph.link(c, e);
936
937 let mut order = PublishPlan::new(&graph);
938 let ready: Vec<_> = order.take_ready().into_iter().collect();
939 assert_eq!(ready, vec![d, e]);
940
941 order.mark_confirmed(vec![d]);
942 let ready: Vec<_> = order.take_ready().into_iter().collect();
943 assert!(ready.is_empty());
944
945 order.mark_confirmed(vec![e]);
946 let ready: Vec<_> = order.take_ready().into_iter().collect();
947 assert_eq!(ready, vec![c]);
948
949 order.mark_confirmed(vec![c]);
950 let ready: Vec<_> = order.take_ready().into_iter().collect();
951 assert_eq!(ready, vec![a, b]);
952
953 order.mark_confirmed(vec![a, b]);
954 let ready: Vec<_> = order.take_ready().into_iter().collect();
955 assert!(ready.is_empty());
956 }
957}