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