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