cargo/ops/registry/
publish.rs

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