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