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