cargo/ops/registry/
publish.rs

1//! Interacts with the registry [publish API][1].
2//!
3//! [1]: https://doc.rust-lang.org/nightly/cargo/reference/registry-web-api.html#publish
4
5use 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 annotate_snippets::Level;
15use anyhow::Context as _;
16use anyhow::bail;
17use cargo_credential::Operation;
18use cargo_credential::Secret;
19use cargo_util::paths;
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    // Check that the specs match members.
77    for spec in &specs {
78        spec.query(member_ids.clone())?;
79    }
80    let mut pkgs = ws.members_with_features(&specs, &opts.cli_features)?;
81    // In `members_with_features_old`, it will add "current" package (determined by the cwd)
82    // So we need filter
83    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    // If `--workspace` is passed,
89    // the intent is more like "publish all publisable packages in this workspace",
90    // so skip `publish=false` packages.
91    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 = match opts.reg_or_index.clone() {
130        Some(r) => {
131            validate_registry(&just_pkgs, Some(&r))?;
132            Some(r)
133        }
134        None => {
135            let reg = super::infer_registry(&just_pkgs)?;
136            validate_registry(&just_pkgs, reg.as_ref())?;
137            if let Some(RegistryOrIndex::Registry(registry)) = &reg {
138                if registry != CRATES_IO_REGISTRY {
139                    // Don't warn for crates.io.
140                    opts.gctx.shell().note(&format!(
141                        "found `{}` as only allowed registry. Publishing to it automatically.",
142                        registry
143                    ))?;
144                }
145            }
146            reg
147        }
148    };
149
150    // This is only used to confirm that we can create a token before we build the package.
151    // This causes the credential provider to be called an extra time, but keeps the same order of errors.
152    let source_ids = super::get_source_id(opts.gctx, reg_or_index.as_ref())?;
153    let (mut registry, mut source) = super::registry(
154        opts.gctx,
155        &source_ids,
156        opts.token.as_ref().map(Secret::as_deref),
157        reg_or_index.as_ref(),
158        true,
159        Some(Operation::Read).filter(|_| !opts.dry_run),
160    )?;
161
162    {
163        let _lock = opts
164            .gctx
165            .acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
166
167        for (pkg, _) in &pkgs {
168            verify_unpublished(pkg, &mut source, &source_ids, opts.dry_run, opts.gctx)?;
169            verify_dependencies(pkg, &registry, source_ids.original).map_err(|err| {
170                ManifestError::new(
171                    err.context(format!(
172                        "failed to verify manifest at `{}`",
173                        pkg.manifest_path().display()
174                    )),
175                    pkg.manifest_path().into(),
176                )
177            })?;
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            // `package_with_dep_graph` ignores this field in favor of
192            // the already-resolved list of packages
193            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            dry_run: opts.dry_run,
200        },
201        pkgs,
202    )?;
203
204    let mut plan = PublishPlan::new(&pkg_dep_graph.graph);
205    // May contains packages from previous rounds as `wait_for_any_publish_confirmation` returns
206    // after it confirms any packages, not all packages, requiring us to handle the rest in the next
207    // iteration.
208    //
209    // As a side effect, any given package's "effective" timeout may be much larger.
210    let mut to_confirm = BTreeSet::new();
211
212    while !plan.is_empty() {
213        // There might not be any ready package, if the previous confirmations
214        // didn't unlock a new one. For example, if `c` depends on `a` and
215        // `b`, and we uploaded `a` and `b` but only confirmed `a`, then on
216        // the following pass through the outer loop nothing will be ready for
217        // upload.
218        let mut ready = plan.take_ready();
219        while let Some(pkg_id) = ready.pop_first() {
220            let (pkg, (_features, tarball)) = &pkg_dep_graph.packages[&pkg_id];
221            opts.gctx.shell().status("Uploading", pkg.package_id())?;
222
223            if !opts.dry_run {
224                let ver = pkg.version().to_string();
225
226                tarball.file().seek(SeekFrom::Start(0))?;
227                let hash = cargo_util::Sha256::new()
228                    .update_file(tarball.file())?
229                    .finish_hex();
230                let operation = Operation::Publish {
231                    name: pkg.name().as_str(),
232                    vers: &ver,
233                    cksum: &hash,
234                };
235                registry.set_token(Some(auth::auth_token(
236                    &opts.gctx,
237                    &source_ids.original,
238                    None,
239                    operation,
240                    vec![],
241                    false,
242                )?));
243            }
244
245            let workspace_context = || {
246                let mut remaining = ready.clone();
247                remaining.extend(plan.iter());
248                if !remaining.is_empty() {
249                    format!(
250                        "\n\nnote: the following crates have not been published yet:\n  {}",
251                        remaining.into_iter().join("\n  ")
252                    )
253                } else {
254                    String::new()
255                }
256            };
257
258            transmit(
259                opts.gctx,
260                ws,
261                pkg,
262                tarball.file(),
263                &mut registry,
264                source_ids.original,
265                opts.dry_run,
266                workspace_context,
267            )?;
268            to_confirm.insert(pkg_id);
269
270            if !opts.dry_run {
271                // Short does not include the registry name.
272                let short_pkg_description = format!("{} v{}", pkg.name(), pkg.version());
273                let source_description = source_ids.original.to_string();
274                ws.gctx().shell().status(
275                    "Uploaded",
276                    format!("{short_pkg_description} to {source_description}"),
277                )?;
278            }
279        }
280
281        let confirmed = if opts.dry_run {
282            to_confirm.clone()
283        } else {
284            const DEFAULT_TIMEOUT: u64 = 60;
285            let timeout = if opts.gctx.cli_unstable().publish_timeout {
286                let timeout: Option<u64> = opts.gctx.get("publish.timeout")?;
287                timeout.unwrap_or(DEFAULT_TIMEOUT)
288            } else {
289                DEFAULT_TIMEOUT
290            };
291            if 0 < timeout {
292                let source_description = source.source_id().to_string();
293                let short_pkg_descriptions = package_list(to_confirm.iter().copied(), "or");
294                if plan.is_empty() {
295                    let report = &[
296                        annotate_snippets::Group::with_title(
297                        annotate_snippets::Level::NOTE
298                            .secondary_title(format!(
299                                "waiting for {short_pkg_descriptions} to be available at {source_description}"
300                            ))),
301                            annotate_snippets::Group::with_title(annotate_snippets::Level::HELP.secondary_title(format!(
302                                "you may press ctrl-c to skip waiting; the {crate} should be available shortly",
303                                crate = if to_confirm.len() == 1 { "crate" } else {"crates"}
304                            ))),
305                    ];
306                    opts.gctx.shell().print_report(report, false)?;
307                } else {
308                    opts.gctx.shell().note(format!(
309                    "waiting for {short_pkg_descriptions} to be available at {source_description}.\n\
310                    {count} remaining {crate} to be published",
311                    count = plan.len(),
312                    crate = if plan.len() == 1 { "crate" } else {"crates"}
313                ))?;
314                }
315
316                let timeout = Duration::from_secs(timeout);
317                let confirmed = wait_for_any_publish_confirmation(
318                    opts.gctx,
319                    source_ids.original,
320                    &to_confirm,
321                    timeout,
322                )?;
323                if !confirmed.is_empty() {
324                    let short_pkg_description = package_list(confirmed.iter().copied(), "and");
325                    opts.gctx.shell().status(
326                        "Published",
327                        format!("{short_pkg_description} at {source_description}"),
328                    )?;
329                } else {
330                    let short_pkg_descriptions = package_list(to_confirm.iter().copied(), "or");
331                    let krate = if to_confirm.len() == 1 {
332                        "crate"
333                    } else {
334                        "crates"
335                    };
336                    opts.gctx.shell().print_report(
337                        &[Level::WARNING
338                            .secondary_title(format!(
339                                "timed out waiting for {short_pkg_descriptions} \
340                                    to be available in {source_description}",
341                            ))
342                            .element(Level::NOTE.message(format!(
343                                "the registry may have a backlog that is delaying making the \
344                                {krate} available. The {krate} should be available soon.",
345                            )))],
346                        false,
347                    )?;
348                }
349                confirmed
350            } else {
351                BTreeSet::new()
352            }
353        };
354        if confirmed.is_empty() {
355            // If nothing finished, it means we timed out while waiting for confirmation.
356            // We're going to exit, but first we need to check: have we uploaded everything?
357            if plan.is_empty() {
358                // It's ok that we timed out, because nothing was waiting on dependencies to
359                // be confirmed.
360                break;
361            } else {
362                let failed_list = package_list(plan.iter(), "and");
363                bail!(
364                    "unable to publish {failed_list} due to a timeout while waiting for published dependencies to be available."
365                );
366            }
367        }
368        for id in &confirmed {
369            to_confirm.remove(id);
370        }
371        plan.mark_confirmed(confirmed);
372    }
373
374    Ok(())
375}
376
377/// Poll the registry for any packages that are ready for use.
378///
379/// Returns the subset of `pkgs` that are ready for use.
380/// This will be an empty set if we timed out before confirming anything.
381fn wait_for_any_publish_confirmation(
382    gctx: &GlobalContext,
383    registry_src: SourceId,
384    pkgs: &BTreeSet<PackageId>,
385    timeout: Duration,
386) -> CargoResult<BTreeSet<PackageId>> {
387    let mut source = SourceConfigMap::empty(gctx)?.load(registry_src, &HashSet::new())?;
388    // Disable the source's built-in progress bars. Repeatedly showing a bunch
389    // of independent progress bars can be a little confusing. There is an
390    // overall progress bar managed here.
391    source.set_quiet(true);
392
393    let now = std::time::Instant::now();
394    let sleep_time = Duration::from_secs(1);
395    let max = timeout.as_secs() as usize;
396    let mut progress = Progress::with_style("Waiting", ProgressStyle::Ratio, gctx);
397    progress.tick_now(0, max, "")?;
398    let available = loop {
399        {
400            let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
401            // Force re-fetching the source
402            //
403            // As pulling from a git source is expensive, we track when we've done it within the
404            // process to only do it once, but we are one of the rare cases that needs to do it
405            // multiple times
406            gctx.updated_sources().remove(&source.replaced_source_id());
407            source.invalidate_cache();
408            let mut available = BTreeSet::new();
409            for pkg in pkgs {
410                if poll_one_package(registry_src, pkg, &mut source)? {
411                    available.insert(*pkg);
412                }
413            }
414
415            // As soon as any package is available, break this loop so we can see if another
416            // one can be uploaded.
417            if !available.is_empty() {
418                break available;
419            }
420        }
421
422        let elapsed = now.elapsed();
423        if timeout < elapsed {
424            break BTreeSet::new();
425        }
426
427        progress.tick_now(elapsed.as_secs() as usize, max, "")?;
428        std::thread::sleep(sleep_time);
429    };
430
431    Ok(available)
432}
433
434fn poll_one_package(
435    registry_src: SourceId,
436    pkg_id: &PackageId,
437    source: &mut dyn Source,
438) -> CargoResult<bool> {
439    let version_req = format!("={}", pkg_id.version());
440    let query = Dependency::parse(pkg_id.name(), Some(&version_req), registry_src)?;
441    let summaries = loop {
442        // Exact to avoid returning all for path/git
443        match source.query_vec(&query, QueryKind::Exact) {
444            std::task::Poll::Ready(res) => {
445                break res?;
446            }
447            std::task::Poll::Pending => source.block_until_ready()?,
448        }
449    };
450    Ok(!summaries.is_empty())
451}
452
453fn verify_unpublished(
454    pkg: &Package,
455    source: &mut RegistrySource<'_>,
456    source_ids: &RegistrySourceIds,
457    dry_run: bool,
458    gctx: &GlobalContext,
459) -> CargoResult<()> {
460    let query = Dependency::parse(
461        pkg.name(),
462        Some(&pkg.version().to_exact_req().to_string()),
463        source_ids.replacement,
464    )?;
465    let duplicate_query = loop {
466        match source.query_vec(&query, QueryKind::Exact) {
467            std::task::Poll::Ready(res) => {
468                break res?;
469            }
470            std::task::Poll::Pending => source.block_until_ready()?,
471        }
472    };
473    if !duplicate_query.is_empty() {
474        // Move the registry error earlier in the publish process.
475        // Since dry-run wouldn't talk to the registry to get the error, we downgrade it to a
476        // warning.
477        if dry_run {
478            gctx.shell().warn(format!(
479                "crate {}@{} already exists on {}",
480                pkg.name(),
481                pkg.version(),
482                source.describe()
483            ))?;
484        } else {
485            bail!(
486                "crate {}@{} already exists on {}",
487                pkg.name(),
488                pkg.version(),
489                source.describe()
490            );
491        }
492    }
493
494    Ok(())
495}
496
497fn verify_dependencies(
498    pkg: &Package,
499    registry: &Registry,
500    registry_src: SourceId,
501) -> CargoResult<()> {
502    for dep in pkg.dependencies().iter() {
503        if check_dep_has_version(dep, true)? {
504            continue;
505        }
506        // TomlManifest::prepare_for_publish will rewrite the dependency
507        // to be just the `version` field.
508        if dep.source_id() != registry_src {
509            if !dep.source_id().is_registry() {
510                // Consider making SourceId::kind a public type that we can
511                // exhaustively match on. Using match can help ensure that
512                // every kind is properly handled.
513                panic!("unexpected source kind for dependency {:?}", dep);
514            }
515            // Block requests to send to crates.io with alt-registry deps.
516            // This extra hostname check is mostly to assist with testing,
517            // but also prevents someone using `--index` to specify
518            // something that points to crates.io.
519            if registry_src.is_crates_io() || registry.host_is_crates_io() {
520                bail!(
521                    "crates cannot be published to crates.io with dependencies sourced from other\n\
522                       registries. `{}` needs to be published to crates.io before publishing this crate.\n\
523                       (crate `{}` is pulled from {})",
524                    dep.package_name(),
525                    dep.package_name(),
526                    dep.source_id()
527                );
528            }
529        }
530    }
531    Ok(())
532}
533
534pub(crate) fn prepare_transmit(
535    gctx: &GlobalContext,
536    ws: &Workspace<'_>,
537    local_pkg: &Package,
538    registry_id: SourceId,
539) -> CargoResult<NewCrate> {
540    let included = None; // don't filter build-targets
541    let publish_pkg = prepare_for_publish(local_pkg, ws, included)?;
542
543    let deps = publish_pkg
544        .dependencies()
545        .iter()
546        .map(|dep| {
547            // If the dependency is from a different registry, then include the
548            // registry in the dependency.
549            let dep_registry_id = match dep.registry_id() {
550                Some(id) => id,
551                None => SourceId::crates_io(gctx)?,
552            };
553            // In the index and Web API, None means "from the same registry"
554            // whereas in Cargo.toml, it means "from crates.io".
555            let dep_registry = if dep_registry_id != registry_id {
556                Some(dep_registry_id.url().to_string())
557            } else {
558                None
559            };
560
561            Ok(NewCrateDependency {
562                optional: dep.is_optional(),
563                default_features: dep.uses_default_features(),
564                name: dep.package_name().to_string(),
565                features: dep.features().iter().map(|s| s.to_string()).collect(),
566                version_req: dep.version_req().to_string(),
567                target: dep.platform().map(|s| s.to_string()),
568                kind: match dep.kind() {
569                    DepKind::Normal => "normal",
570                    DepKind::Build => "build",
571                    DepKind::Development => "dev",
572                }
573                .to_string(),
574                registry: dep_registry,
575                explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()),
576                artifact: dep.artifact().map(|artifact| {
577                    artifact
578                        .kinds()
579                        .iter()
580                        .map(|x| x.as_str().into_owned())
581                        .collect()
582                }),
583                bindep_target: dep.artifact().and_then(|artifact| {
584                    artifact.target().map(|target| target.as_str().to_owned())
585                }),
586                lib: dep.artifact().map_or(false, |artifact| artifact.is_lib()),
587            })
588        })
589        .collect::<CargoResult<Vec<NewCrateDependency>>>()?;
590    let manifest = publish_pkg.manifest();
591    let ManifestMetadata {
592        ref authors,
593        ref description,
594        ref homepage,
595        ref documentation,
596        ref keywords,
597        ref readme,
598        ref repository,
599        ref license,
600        ref license_file,
601        ref categories,
602        ref badges,
603        ref links,
604        ref rust_version,
605    } = *manifest.metadata();
606    let rust_version = rust_version.as_ref().map(ToString::to_string);
607    let readme_content = local_pkg
608        .manifest()
609        .metadata()
610        .readme
611        .as_ref()
612        .map(|readme| {
613            paths::read(&local_pkg.root().join(readme)).with_context(|| {
614                format!("failed to read `readme` file for package `{}`", local_pkg)
615            })
616        })
617        .transpose()?;
618    if let Some(ref file) = local_pkg.manifest().metadata().license_file {
619        if !local_pkg.root().join(file).exists() {
620            bail!("the license file `{}` does not exist", file)
621        }
622    }
623
624    let string_features = match manifest.normalized_toml().features() {
625        Some(features) => features
626            .iter()
627            .map(|(feat, values)| {
628                (
629                    feat.to_string(),
630                    values.iter().map(|fv| fv.to_string()).collect(),
631                )
632            })
633            .collect::<BTreeMap<String, Vec<String>>>(),
634        None => BTreeMap::new(),
635    };
636
637    Ok(NewCrate {
638        name: publish_pkg.name().to_string(),
639        vers: publish_pkg.version().to_string(),
640        deps,
641        features: string_features,
642        authors: authors.clone(),
643        description: description.clone(),
644        homepage: homepage.clone(),
645        documentation: documentation.clone(),
646        keywords: keywords.clone(),
647        categories: categories.clone(),
648        readme: readme_content,
649        readme_file: readme.clone(),
650        repository: repository.clone(),
651        license: license.clone(),
652        license_file: license_file.clone(),
653        badges: badges.clone(),
654        links: links.clone(),
655        rust_version,
656    })
657}
658
659fn transmit(
660    gctx: &GlobalContext,
661    ws: &Workspace<'_>,
662    pkg: &Package,
663    tarball: &File,
664    registry: &mut Registry,
665    registry_id: SourceId,
666    dry_run: bool,
667    workspace_context: impl Fn() -> String,
668) -> CargoResult<()> {
669    let new_crate = prepare_transmit(gctx, ws, pkg, registry_id)?;
670
671    // Do not upload if performing a dry run
672    if dry_run {
673        gctx.shell().warn("aborting upload due to dry run")?;
674        return Ok(());
675    }
676
677    let warnings = registry.publish(&new_crate, tarball).with_context(|| {
678        format!(
679            "failed to publish {} v{} to registry at {}{}",
680            pkg.name(),
681            pkg.version(),
682            registry.host(),
683            workspace_context()
684        )
685    })?;
686
687    if !warnings.invalid_categories.is_empty() {
688        let msg = format!(
689            "the following are not valid category slugs and were ignored: {}",
690            warnings.invalid_categories.join(", ")
691        );
692        gctx.shell().print_report(
693            &[Level::WARNING
694                .secondary_title(msg)
695                .element(Level::HELP.message(
696                "please see <https://crates.io/category_slugs> for the list of all category slugs",
697            ))],
698            false,
699        )?;
700    }
701
702    if !warnings.invalid_badges.is_empty() {
703        let msg = format!(
704            "the following are not valid badges and were ignored: {}",
705            warnings.invalid_badges.join(", ")
706        );
707        gctx.shell().print_report(
708            &[Level::WARNING.secondary_title(msg).elements([
709                Level::NOTE.message(
710                    "either the badge type specified is unknown or a required \
711                    attribute is missing",
712                ),
713                Level::HELP.message(
714                    "please see \
715                    <https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata> \
716                    for valid badge types and their required attributes",
717                ),
718            ])],
719            false,
720        )?;
721    }
722
723    if !warnings.other.is_empty() {
724        for msg in warnings.other {
725            gctx.shell().warn(&msg)?;
726        }
727    }
728
729    Ok(())
730}
731
732/// State for tracking dependencies during upload.
733struct PublishPlan {
734    /// Graph of publishable packages where the edges are `(dependency -> dependent)`
735    dependents: Graph<PackageId, ()>,
736    /// The weight of a package is the number of unpublished dependencies it has.
737    dependencies_count: HashMap<PackageId, usize>,
738}
739
740impl PublishPlan {
741    /// Given a package dependency graph, creates a `PublishPlan` for tracking state.
742    fn new(graph: &Graph<PackageId, ()>) -> Self {
743        let dependents = graph.reversed();
744
745        let dependencies_count: HashMap<_, _> = dependents
746            .iter()
747            .map(|id| (*id, graph.edges(id).count()))
748            .collect();
749        Self {
750            dependents,
751            dependencies_count,
752        }
753    }
754
755    fn iter(&self) -> impl Iterator<Item = PackageId> + '_ {
756        self.dependencies_count.iter().map(|(id, _)| *id)
757    }
758
759    fn is_empty(&self) -> bool {
760        self.dependencies_count.is_empty()
761    }
762
763    fn len(&self) -> usize {
764        self.dependencies_count.len()
765    }
766
767    /// Returns the set of packages that are ready for publishing (i.e. have no outstanding dependencies).
768    ///
769    /// These will not be returned in future calls.
770    fn take_ready(&mut self) -> BTreeSet<PackageId> {
771        let ready: BTreeSet<_> = self
772            .dependencies_count
773            .iter()
774            .filter_map(|(id, weight)| (*weight == 0).then_some(*id))
775            .collect();
776        for pkg in &ready {
777            self.dependencies_count.remove(pkg);
778        }
779        ready
780    }
781
782    /// Packages confirmed to be available in the registry, potentially allowing additional
783    /// packages to be "ready".
784    fn mark_confirmed(&mut self, published: impl IntoIterator<Item = PackageId>) {
785        for id in published {
786            for (dependent_id, _) in self.dependents.edges(&id) {
787                if let Some(weight) = self.dependencies_count.get_mut(dependent_id) {
788                    *weight = weight.saturating_sub(1);
789                }
790            }
791        }
792    }
793}
794
795/// Format a collection of packages as a list
796///
797/// e.g. "foo v0.1.0, bar v0.2.0, and baz v0.3.0".
798///
799/// Note: the final separator (e.g. "and" in the previous example) can be chosen.
800fn package_list(pkgs: impl IntoIterator<Item = PackageId>, final_sep: &str) -> String {
801    let mut names: Vec<_> = pkgs
802        .into_iter()
803        .map(|pkg| format!("{} v{}", pkg.name(), pkg.version()))
804        .collect();
805    names.sort();
806
807    match &names[..] {
808        [] => String::new(),
809        [a] => a.clone(),
810        [a, b] => format!("{a} {final_sep} {b}"),
811        [names @ .., last] => {
812            format!("{}, {final_sep} {last}", names.join(", "))
813        }
814    }
815}
816
817fn validate_registry(pkgs: &[&Package], reg_or_index: Option<&RegistryOrIndex>) -> CargoResult<()> {
818    let reg_name = match reg_or_index {
819        Some(RegistryOrIndex::Registry(r)) => Some(r.as_str()),
820        None => Some(CRATES_IO_REGISTRY),
821        Some(RegistryOrIndex::Index(_)) => None,
822    };
823    if let Some(reg_name) = reg_name {
824        for pkg in pkgs {
825            if let Some(allowed) = pkg.publish().as_ref() {
826                if !allowed.iter().any(|a| a == reg_name) {
827                    bail!(
828                        "`{}` cannot be published.\n\
829                         The registry `{}` is not listed in the `package.publish` value in Cargo.toml.",
830                        pkg.name(),
831                        reg_name
832                    );
833                }
834            }
835        }
836    }
837
838    Ok(())
839}
840
841#[cfg(test)]
842mod tests {
843    use crate::{
844        core::{PackageId, SourceId},
845        sources::CRATES_IO_INDEX,
846        util::{Graph, IntoUrl},
847    };
848
849    use super::PublishPlan;
850
851    fn pkg_id(name: &str) -> PackageId {
852        let loc = CRATES_IO_INDEX.into_url().unwrap();
853        PackageId::try_new(name, "1.0.0", SourceId::for_registry(&loc).unwrap()).unwrap()
854    }
855
856    #[test]
857    fn parallel_schedule() {
858        let mut graph: Graph<PackageId, ()> = Graph::new();
859        let a = pkg_id("a");
860        let b = pkg_id("b");
861        let c = pkg_id("c");
862        let d = pkg_id("d");
863        let e = pkg_id("e");
864
865        graph.add(a);
866        graph.add(b);
867        graph.add(c);
868        graph.add(d);
869        graph.add(e);
870        graph.link(a, c);
871        graph.link(b, c);
872        graph.link(c, d);
873        graph.link(c, e);
874
875        let mut order = PublishPlan::new(&graph);
876        let ready: Vec<_> = order.take_ready().into_iter().collect();
877        assert_eq!(ready, vec![d, e]);
878
879        order.mark_confirmed(vec![d]);
880        let ready: Vec<_> = order.take_ready().into_iter().collect();
881        assert!(ready.is_empty());
882
883        order.mark_confirmed(vec![e]);
884        let ready: Vec<_> = order.take_ready().into_iter().collect();
885        assert_eq!(ready, vec![c]);
886
887        order.mark_confirmed(vec![c]);
888        let ready: Vec<_> = order.take_ready().into_iter().collect();
889        assert_eq!(ready, vec![a, b]);
890
891        order.mark_confirmed(vec![a, b]);
892        let ready: Vec<_> = order.take_ready().into_iter().collect();
893        assert!(ready.is_empty());
894    }
895}