#
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}