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