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