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