Skip to main content

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