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