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