#
cargo/ops/registry/
publish.rs

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