#
cargo/ops/registry/
publish.rs

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