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::bail;
15use anyhow::Context as _;
16use cargo_credential::Operation;
17use cargo_credential::Secret;
18use cargo_util::paths;
19use crates_io::NewCrate;
20use crates_io::NewCrateDependency;
21use crates_io::Registry;
22use itertools::Itertools;
23
24use crate::core::dependency::DepKind;
25use crate::core::manifest::ManifestMetadata;
26use crate::core::resolver::CliFeatures;
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::ops;
34use crate::ops::registry::RegistrySourceIds;
35use crate::ops::PackageOpts;
36use crate::ops::Packages;
37use crate::ops::RegistryOrIndex;
38use crate::sources::source::QueryKind;
39use crate::sources::source::Source;
40use crate::sources::RegistrySource;
41use crate::sources::SourceConfigMap;
42use crate::sources::CRATES_IO_REGISTRY;
43use crate::util::auth;
44use crate::util::cache_lock::CacheLockMode;
45use crate::util::context::JobsConfig;
46use crate::util::toml::prepare_for_publish;
47use crate::util::Graph;
48use crate::util::Progress;
49use crate::util::ProgressStyle;
50use crate::util::VersionExt as _;
51use crate::CargoResult;
52use crate::GlobalContext;
53
54use super::super::check_dep_has_version;
55
56pub struct PublishOpts<'gctx> {
57 pub gctx: &'gctx GlobalContext,
58 pub token: Option<Secret<String>>,
59 pub reg_or_index: Option<RegistryOrIndex>,
60 pub verify: bool,
61 pub allow_dirty: bool,
62 pub jobs: Option<JobsConfig>,
63 pub keep_going: bool,
64 pub to_publish: ops::Packages,
65 pub targets: Vec<String>,
66 pub dry_run: bool,
67 pub cli_features: CliFeatures,
68}
69
70pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
71 let multi_package_mode = ws.gctx().cli_unstable().package_workspace;
72 let specs = opts.to_publish.to_package_id_specs(ws)?;
73
74 if !multi_package_mode {
75 if specs.len() > 1 {
76 bail!("the `-p` argument must be specified to select a single package to publish")
77 }
78 if Packages::Default == opts.to_publish && ws.is_virtual() {
79 bail!("the `-p` argument must be specified in the root of a virtual workspace")
80 }
81 }
82
83 let member_ids: Vec<_> = ws.members().map(|p| p.package_id()).collect();
84 for spec in &specs {
86 spec.query(member_ids.clone())?;
87 }
88 let mut pkgs = ws.members_with_features(&specs, &opts.cli_features)?;
89 pkgs = pkgs
92 .into_iter()
93 .filter(|(m, _)| specs.iter().any(|spec| spec.matches(m.package_id())))
94 .collect();
95
96 let just_pkgs: Vec<_> = pkgs.iter().map(|p| p.0).collect();
97 let reg_or_index = match opts.reg_or_index.clone() {
98 Some(r) => {
99 validate_registry(&just_pkgs, Some(&r))?;
100 Some(r)
101 }
102 None => {
103 let reg = super::infer_registry(&just_pkgs)?;
104 validate_registry(&just_pkgs, reg.as_ref())?;
105 if let Some(RegistryOrIndex::Registry(ref registry)) = ® {
106 if registry != CRATES_IO_REGISTRY {
107 opts.gctx.shell().note(&format!(
109 "found `{}` as only allowed registry. Publishing to it automatically.",
110 registry
111 ))?;
112 }
113 }
114 reg
115 }
116 };
117
118 let source_ids = super::get_source_id(opts.gctx, reg_or_index.as_ref())?;
121 let (mut registry, mut source) = super::registry(
122 opts.gctx,
123 &source_ids,
124 opts.token.as_ref().map(Secret::as_deref),
125 reg_or_index.as_ref(),
126 true,
127 Some(Operation::Read).filter(|_| !opts.dry_run),
128 )?;
129
130 {
131 let _lock = opts
132 .gctx
133 .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
134
135 for (pkg, _) in &pkgs {
136 verify_unpublished(pkg, &mut source, &source_ids, opts.dry_run, opts.gctx)?;
137 verify_dependencies(pkg, ®istry, source_ids.original)?;
138 }
139 }
140
141 let pkg_dep_graph = ops::cargo_package::package_with_dep_graph(
142 ws,
143 &PackageOpts {
144 gctx: opts.gctx,
145 verify: opts.verify,
146 list: false,
147 fmt: ops::PackageMessageFormat::Human,
148 check_metadata: true,
149 allow_dirty: opts.allow_dirty,
150 include_lockfile: true,
151 to_package: ops::Packages::Default,
154 targets: opts.targets.clone(),
155 jobs: opts.jobs.clone(),
156 keep_going: opts.keep_going,
157 cli_features: opts.cli_features.clone(),
158 reg_or_index: reg_or_index.clone(),
159 },
160 pkgs,
161 )?;
162
163 let mut plan = PublishPlan::new(&pkg_dep_graph.graph);
164 let mut to_confirm = BTreeSet::new();
170
171 while !plan.is_empty() {
172 for pkg_id in plan.take_ready() {
178 let (pkg, (_features, tarball)) = &pkg_dep_graph.packages[&pkg_id];
179 opts.gctx.shell().status("Uploading", pkg.package_id())?;
180
181 if !opts.dry_run {
182 let ver = pkg.version().to_string();
183
184 tarball.file().seek(SeekFrom::Start(0))?;
185 let hash = cargo_util::Sha256::new()
186 .update_file(tarball.file())?
187 .finish_hex();
188 let operation = Operation::Publish {
189 name: pkg.name().as_str(),
190 vers: &ver,
191 cksum: &hash,
192 };
193 registry.set_token(Some(auth::auth_token(
194 &opts.gctx,
195 &source_ids.original,
196 None,
197 operation,
198 vec![],
199 false,
200 )?));
201 }
202
203 transmit(
204 opts.gctx,
205 ws,
206 pkg,
207 tarball.file(),
208 &mut registry,
209 source_ids.original,
210 opts.dry_run,
211 )?;
212 to_confirm.insert(pkg_id);
213
214 if !opts.dry_run {
215 let short_pkg_description = format!("{} v{}", pkg.name(), pkg.version());
217 let source_description = source_ids.original.to_string();
218 ws.gctx().shell().status(
219 "Uploaded",
220 format!("{short_pkg_description} to {source_description}"),
221 )?;
222 }
223 }
224
225 let confirmed = if opts.dry_run {
226 to_confirm.clone()
227 } else {
228 const DEFAULT_TIMEOUT: u64 = 60;
229 let timeout = if opts.gctx.cli_unstable().publish_timeout {
230 let timeout: Option<u64> = opts.gctx.get("publish.timeout")?;
231 timeout.unwrap_or(DEFAULT_TIMEOUT)
232 } else {
233 DEFAULT_TIMEOUT
234 };
235 if 0 < timeout {
236 let timeout = Duration::from_secs(timeout);
237 wait_for_any_publish_confirmation(
238 opts.gctx,
239 source_ids.original,
240 &to_confirm,
241 timeout,
242 )?
243 } else {
244 BTreeSet::new()
245 }
246 };
247 if confirmed.is_empty() {
248 if plan.is_empty() {
251 break;
254 } else {
255 let failed_list = package_list(plan.iter(), "and");
256 bail!("unable to publish {failed_list} due to time out while waiting for published dependencies to be available.");
257 }
258 }
259 for id in &confirmed {
260 to_confirm.remove(id);
261 }
262 plan.mark_confirmed(confirmed);
263 }
264
265 Ok(())
266}
267
268fn wait_for_any_publish_confirmation(
273 gctx: &GlobalContext,
274 registry_src: SourceId,
275 pkgs: &BTreeSet<PackageId>,
276 timeout: Duration,
277) -> CargoResult<BTreeSet<PackageId>> {
278 let mut source = SourceConfigMap::empty(gctx)?.load(registry_src, &HashSet::new())?;
279 source.set_quiet(true);
283 let source_description = source.source_id().to_string();
284
285 let now = std::time::Instant::now();
286 let sleep_time = Duration::from_secs(1);
287 let max = timeout.as_secs() as usize;
288 let short_pkg_descriptions = package_list(pkgs.iter().copied(), "or");
290 gctx.shell().note(format!(
291 "waiting for {short_pkg_descriptions} to be available at {source_description}.\n\
292 You may press ctrl-c to skip waiting; the crate should be available shortly."
293 ))?;
294 let mut progress = Progress::with_style("Waiting", ProgressStyle::Ratio, gctx);
295 progress.tick_now(0, max, "")?;
296 let available = loop {
297 {
298 let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
299 gctx.updated_sources().remove(&source.replaced_source_id());
305 source.invalidate_cache();
306 let mut available = BTreeSet::new();
307 for pkg in pkgs {
308 if poll_one_package(registry_src, pkg, &mut source)? {
309 available.insert(*pkg);
310 }
311 }
312
313 if !available.is_empty() {
316 break available;
317 }
318 }
319
320 let elapsed = now.elapsed();
321 if timeout < elapsed {
322 gctx.shell().warn(format!(
323 "timed out waiting for {short_pkg_descriptions} to be available in {source_description}",
324 ))?;
325 gctx.shell().note(
326 "the registry may have a backlog that is delaying making the \
327 crate available. The crate should be available soon.",
328 )?;
329 break BTreeSet::new();
330 }
331
332 progress.tick_now(elapsed.as_secs() as usize, max, "")?;
333 std::thread::sleep(sleep_time);
334 };
335 if !available.is_empty() {
336 let short_pkg_description = available
337 .iter()
338 .map(|pkg| format!("{} v{}", pkg.name(), pkg.version()))
339 .sorted()
340 .join(", ");
341 gctx.shell().status(
342 "Published",
343 format!("{short_pkg_description} at {source_description}"),
344 )?;
345 }
346
347 Ok(available)
348}
349
350fn poll_one_package(
351 registry_src: SourceId,
352 pkg_id: &PackageId,
353 source: &mut dyn Source,
354) -> CargoResult<bool> {
355 let version_req = format!("={}", pkg_id.version());
356 let query = Dependency::parse(pkg_id.name(), Some(&version_req), registry_src)?;
357 let summaries = loop {
358 match source.query_vec(&query, QueryKind::Exact) {
360 std::task::Poll::Ready(res) => {
361 break res?;
362 }
363 std::task::Poll::Pending => source.block_until_ready()?,
364 }
365 };
366 Ok(!summaries.is_empty())
367}
368
369fn verify_unpublished(
370 pkg: &Package,
371 source: &mut RegistrySource<'_>,
372 source_ids: &RegistrySourceIds,
373 dry_run: bool,
374 gctx: &GlobalContext,
375) -> CargoResult<()> {
376 let query = Dependency::parse(
377 pkg.name(),
378 Some(&pkg.version().to_exact_req().to_string()),
379 source_ids.replacement,
380 )?;
381 let duplicate_query = loop {
382 match source.query_vec(&query, QueryKind::Exact) {
383 std::task::Poll::Ready(res) => {
384 break res?;
385 }
386 std::task::Poll::Pending => source.block_until_ready()?,
387 }
388 };
389 if !duplicate_query.is_empty() {
390 if dry_run {
394 gctx.shell().warn(format!(
395 "crate {}@{} already exists on {}",
396 pkg.name(),
397 pkg.version(),
398 source.describe()
399 ))?;
400 } else {
401 bail!(
402 "crate {}@{} already exists on {}",
403 pkg.name(),
404 pkg.version(),
405 source.describe()
406 );
407 }
408 }
409
410 Ok(())
411}
412
413fn verify_dependencies(
414 pkg: &Package,
415 registry: &Registry,
416 registry_src: SourceId,
417) -> CargoResult<()> {
418 for dep in pkg.dependencies().iter() {
419 if check_dep_has_version(dep, true)? {
420 continue;
421 }
422 if dep.source_id() != registry_src {
425 if !dep.source_id().is_registry() {
426 panic!("unexpected source kind for dependency {:?}", dep);
430 }
431 if registry_src.is_crates_io() || registry.host_is_crates_io() {
436 bail!("crates cannot be published to crates.io with dependencies sourced from other\n\
437 registries. `{}` needs to be published to crates.io before publishing this crate.\n\
438 (crate `{}` is pulled from {})",
439 dep.package_name(),
440 dep.package_name(),
441 dep.source_id());
442 }
443 }
444 }
445 Ok(())
446}
447
448pub(crate) fn prepare_transmit(
449 gctx: &GlobalContext,
450 ws: &Workspace<'_>,
451 local_pkg: &Package,
452 registry_id: SourceId,
453) -> CargoResult<NewCrate> {
454 let included = None; let publish_pkg = prepare_for_publish(local_pkg, ws, included)?;
456
457 let deps = publish_pkg
458 .dependencies()
459 .iter()
460 .map(|dep| {
461 let dep_registry_id = match dep.registry_id() {
464 Some(id) => id,
465 None => SourceId::crates_io(gctx)?,
466 };
467 let dep_registry = if dep_registry_id != registry_id {
470 Some(dep_registry_id.url().to_string())
471 } else {
472 None
473 };
474
475 Ok(NewCrateDependency {
476 optional: dep.is_optional(),
477 default_features: dep.uses_default_features(),
478 name: dep.package_name().to_string(),
479 features: dep.features().iter().map(|s| s.to_string()).collect(),
480 version_req: dep.version_req().to_string(),
481 target: dep.platform().map(|s| s.to_string()),
482 kind: match dep.kind() {
483 DepKind::Normal => "normal",
484 DepKind::Build => "build",
485 DepKind::Development => "dev",
486 }
487 .to_string(),
488 registry: dep_registry,
489 explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()),
490 artifact: dep.artifact().map(|artifact| {
491 artifact
492 .kinds()
493 .iter()
494 .map(|x| x.as_str().into_owned())
495 .collect()
496 }),
497 bindep_target: dep.artifact().and_then(|artifact| {
498 artifact.target().map(|target| target.as_str().to_owned())
499 }),
500 lib: dep.artifact().map_or(false, |artifact| artifact.is_lib()),
501 })
502 })
503 .collect::<CargoResult<Vec<NewCrateDependency>>>()?;
504 let manifest = publish_pkg.manifest();
505 let ManifestMetadata {
506 ref authors,
507 ref description,
508 ref homepage,
509 ref documentation,
510 ref keywords,
511 ref readme,
512 ref repository,
513 ref license,
514 ref license_file,
515 ref categories,
516 ref badges,
517 ref links,
518 ref rust_version,
519 } = *manifest.metadata();
520 let rust_version = rust_version.as_ref().map(ToString::to_string);
521 let readme_content = local_pkg
522 .manifest()
523 .metadata()
524 .readme
525 .as_ref()
526 .map(|readme| {
527 paths::read(&local_pkg.root().join(readme)).with_context(|| {
528 format!("failed to read `readme` file for package `{}`", local_pkg)
529 })
530 })
531 .transpose()?;
532 if let Some(ref file) = local_pkg.manifest().metadata().license_file {
533 if !local_pkg.root().join(file).exists() {
534 bail!("the license file `{}` does not exist", file)
535 }
536 }
537
538 let string_features = match manifest.normalized_toml().features() {
539 Some(features) => features
540 .iter()
541 .map(|(feat, values)| {
542 (
543 feat.to_string(),
544 values.iter().map(|fv| fv.to_string()).collect(),
545 )
546 })
547 .collect::<BTreeMap<String, Vec<String>>>(),
548 None => BTreeMap::new(),
549 };
550
551 Ok(NewCrate {
552 name: publish_pkg.name().to_string(),
553 vers: publish_pkg.version().to_string(),
554 deps,
555 features: string_features,
556 authors: authors.clone(),
557 description: description.clone(),
558 homepage: homepage.clone(),
559 documentation: documentation.clone(),
560 keywords: keywords.clone(),
561 categories: categories.clone(),
562 readme: readme_content,
563 readme_file: readme.clone(),
564 repository: repository.clone(),
565 license: license.clone(),
566 license_file: license_file.clone(),
567 badges: badges.clone(),
568 links: links.clone(),
569 rust_version,
570 })
571}
572
573fn transmit(
574 gctx: &GlobalContext,
575 ws: &Workspace<'_>,
576 pkg: &Package,
577 tarball: &File,
578 registry: &mut Registry,
579 registry_id: SourceId,
580 dry_run: bool,
581) -> CargoResult<()> {
582 let new_crate = prepare_transmit(gctx, ws, pkg, registry_id)?;
583
584 if dry_run {
586 gctx.shell().warn("aborting upload due to dry run")?;
587 return Ok(());
588 }
589
590 let warnings = registry
591 .publish(&new_crate, tarball)
592 .with_context(|| format!("failed to publish to registry at {}", registry.host()))?;
593
594 if !warnings.invalid_categories.is_empty() {
595 let msg = format!(
596 "the following are not valid category slugs and were \
597 ignored: {}. Please see https://crates.io/category_slugs \
598 for the list of all category slugs. \
599 ",
600 warnings.invalid_categories.join(", ")
601 );
602 gctx.shell().warn(&msg)?;
603 }
604
605 if !warnings.invalid_badges.is_empty() {
606 let msg = format!(
607 "the following are not valid badges and were ignored: {}. \
608 Either the badge type specified is unknown or a required \
609 attribute is missing. Please see \
610 https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata \
611 for valid badge types and their required attributes.",
612 warnings.invalid_badges.join(", ")
613 );
614 gctx.shell().warn(&msg)?;
615 }
616
617 if !warnings.other.is_empty() {
618 for msg in warnings.other {
619 gctx.shell().warn(&msg)?;
620 }
621 }
622
623 Ok(())
624}
625
626struct PublishPlan {
628 dependents: Graph<PackageId, ()>,
630 dependencies_count: HashMap<PackageId, usize>,
632}
633
634impl PublishPlan {
635 fn new(graph: &Graph<PackageId, ()>) -> Self {
637 let dependents = graph.reversed();
638
639 let dependencies_count: HashMap<_, _> = dependents
640 .iter()
641 .map(|id| (*id, graph.edges(id).count()))
642 .collect();
643 Self {
644 dependents,
645 dependencies_count,
646 }
647 }
648
649 fn iter(&self) -> impl Iterator<Item = PackageId> + '_ {
650 self.dependencies_count.iter().map(|(id, _)| *id)
651 }
652
653 fn is_empty(&self) -> bool {
654 self.dependencies_count.is_empty()
655 }
656
657 fn take_ready(&mut self) -> BTreeSet<PackageId> {
661 let ready: BTreeSet<_> = self
662 .dependencies_count
663 .iter()
664 .filter_map(|(id, weight)| (*weight == 0).then_some(*id))
665 .collect();
666 for pkg in &ready {
667 self.dependencies_count.remove(pkg);
668 }
669 ready
670 }
671
672 fn mark_confirmed(&mut self, published: impl IntoIterator<Item = PackageId>) {
675 for id in published {
676 for (dependent_id, _) in self.dependents.edges(&id) {
677 if let Some(weight) = self.dependencies_count.get_mut(dependent_id) {
678 *weight = weight.saturating_sub(1);
679 }
680 }
681 }
682 }
683}
684
685fn package_list(pkgs: impl IntoIterator<Item = PackageId>, final_sep: &str) -> String {
691 let mut names: Vec<_> = pkgs
692 .into_iter()
693 .map(|pkg| format!("`{} v{}`", pkg.name(), pkg.version()))
694 .collect();
695 names.sort();
696
697 match &names[..] {
698 [] => String::new(),
699 [a] => a.clone(),
700 [a, b] => format!("{a} {final_sep} {b}"),
701 [names @ .., last] => {
702 format!("{}, {final_sep} {last}", names.join(", "))
703 }
704 }
705}
706
707fn validate_registry(pkgs: &[&Package], reg_or_index: Option<&RegistryOrIndex>) -> CargoResult<()> {
708 let unpublishable = pkgs
709 .iter()
710 .filter(|pkg| pkg.publish() == &Some(Vec::new()))
711 .map(|pkg| format!("`{}`", pkg.name()))
712 .collect::<Vec<_>>();
713 if !unpublishable.is_empty() {
714 bail!(
715 "{} cannot be published.\n\
716 `package.publish` must be set to `true` or a non-empty list in Cargo.toml to publish.",
717 unpublishable.join(", ")
718 );
719 }
720
721 let reg_name = match reg_or_index {
722 Some(RegistryOrIndex::Registry(r)) => Some(r.as_str()),
723 None => Some(CRATES_IO_REGISTRY),
724 Some(RegistryOrIndex::Index(_)) => None,
725 };
726 if let Some(reg_name) = reg_name {
727 for pkg in pkgs {
728 if let Some(allowed) = pkg.publish().as_ref() {
729 if !allowed.iter().any(|a| a == reg_name) {
730 bail!(
731 "`{}` cannot be published.\n\
732 The registry `{}` is not listed in the `package.publish` value in Cargo.toml.",
733 pkg.name(),
734 reg_name
735 );
736 }
737 }
738 }
739 }
740
741 Ok(())
742}
743
744#[cfg(test)]
745mod tests {
746 use crate::{
747 core::{PackageId, SourceId},
748 sources::CRATES_IO_INDEX,
749 util::{Graph, IntoUrl},
750 };
751
752 use super::PublishPlan;
753
754 fn pkg_id(name: &str) -> PackageId {
755 let loc = CRATES_IO_INDEX.into_url().unwrap();
756 PackageId::try_new(name, "1.0.0", SourceId::for_registry(&loc).unwrap()).unwrap()
757 }
758
759 #[test]
760 fn parallel_schedule() {
761 let mut graph: Graph<PackageId, ()> = Graph::new();
762 let a = pkg_id("a");
763 let b = pkg_id("b");
764 let c = pkg_id("c");
765 let d = pkg_id("d");
766 let e = pkg_id("e");
767
768 graph.add(a);
769 graph.add(b);
770 graph.add(c);
771 graph.add(d);
772 graph.add(e);
773 graph.link(a, c);
774 graph.link(b, c);
775 graph.link(c, d);
776 graph.link(c, e);
777
778 let mut order = PublishPlan::new(&graph);
779 let ready: Vec<_> = order.take_ready().into_iter().collect();
780 assert_eq!(ready, vec![d, e]);
781
782 order.mark_confirmed(vec![d]);
783 let ready: Vec<_> = order.take_ready().into_iter().collect();
784 assert!(ready.is_empty());
785
786 order.mark_confirmed(vec![e]);
787 let ready: Vec<_> = order.take_ready().into_iter().collect();
788 assert_eq!(ready, vec![c]);
789
790 order.mark_confirmed(vec![c]);
791 let ready: Vec<_> = order.take_ready().into_iter().collect();
792 assert_eq!(ready, vec![a, b]);
793
794 order.mark_confirmed(vec![a, b]);
795 let ready: Vec<_> = order.take_ready().into_iter().collect();
796 assert!(ready.is_empty());
797 }
798}