blob: 65ecfa5fafbe8e8ccbf1d142dc071d3eae4d36ea [file]
//! Cargo configuration schemas.
//!
//! This module contains types that define the schema for various configuration
//! sections found in Cargo configuration.
//!
//! These types are mostly used by [`GlobalContext::get`](super::GlobalContext::get)
//! to deserialize configuration values from TOML files, environment variables,
//! and CLI arguments.
//!
//! Schema types here should only contain data and simple accessor methods.
//! Avoid depending on [`GlobalContext`](super::GlobalContext) directly.
use crate::sources::CRATES_IO_REGISTRY;
use crate::util::data_structures::HashMap;
use std::borrow::Cow;
use std::ffi::OsStr;
use std::str::FromStr;
use std::{fmt, hash};
use cargo_credential::Secret;
use cargo_util_schemas::manifest::ProfileName;
use serde::Deserialize;
use serde::Serialize;
use serde::de;
use serde_untagged::UntaggedEnumVisitor;
use std::path::Path;
use crate::CargoResult;
use super::OptValue;
use super::PathAndArgs;
use super::StringList;
use super::Value;
use super::path::ConfigRelativePath;
#[derive(Debug, Default, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct CargoCacheConfig {
/// How often to automatically clean unused cache data.
pub auto_clean_frequency: Option<String>,
/// Settings for cleaning the global cache.
pub global_clean: Option<GlobalCleanConfig>,
}
/// Cache cleaning settings from the `cache.global-clean` config table.
///
/// NOTE: Not all of these options may get stabilized. Some of them are very
/// low-level details, and may not be something typical users need.
///
/// If any of these options are `None`, the built-in default is used.
#[derive(Debug, Default, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct GlobalCleanConfig {
/// Anything older than this duration will be deleted in the source cache.
pub max_src_age: Option<String>,
/// Anything older than this duration will be deleted in the compressed crate cache.
pub max_crate_age: Option<String>,
/// Any index older than this duration will be deleted from the index cache.
pub max_index_age: Option<String>,
/// Any git checkout older than this duration will be deleted from the checkout cache.
pub max_git_co_age: Option<String>,
/// Any git clone older than this duration will be deleted from the git cache.
pub max_git_db_age: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct CargoNewConfig {
#[deprecated = "cargo-new no longer supports adding the authors field"]
#[expect(dead_code, reason = "deprecated")]
name: Option<String>,
#[deprecated = "cargo-new no longer supports adding the authors field"]
#[expect(dead_code, reason = "deprecated")]
email: Option<String>,
#[serde(rename = "vcs")]
pub version_control: Option<VersionControl>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum VersionControl {
Git,
Hg,
Pijul,
Fossil,
NoVcs,
}
impl VersionControl {
pub const VALUES: &[Self] = &[Self::Git, Self::Hg, Self::Pijul, Self::Fossil, Self::NoVcs];
pub fn as_str(&self) -> &'static str {
match self {
VersionControl::Git => "git",
VersionControl::Hg => "hg",
VersionControl::Pijul => "pijul",
VersionControl::Fossil => "fossil",
VersionControl::NoVcs => "none",
}
}
}
impl FromStr for VersionControl {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, anyhow::Error> {
match s {
"git" => Ok(VersionControl::Git),
"hg" => Ok(VersionControl::Hg),
"pijul" => Ok(VersionControl::Pijul),
"fossil" => Ok(VersionControl::Fossil),
"none" => Ok(VersionControl::NoVcs),
other => anyhow::bail!("unknown vcs specification: `{}`", other),
}
}
}
impl<'de> de::Deserialize<'de> for VersionControl {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(de::Error::custom)
}
}
/// Definition of a source in a config file.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct SourceConfigDef {
/// Indicates this source should be replaced with another of the given name.
pub replace_with: OptValue<String>,
/// A directory source.
pub directory: Option<ConfigRelativePath>,
/// A registry source. Value is a URL.
pub registry: OptValue<String>,
/// A local registry source.
pub local_registry: Option<ConfigRelativePath>,
/// A git source. Value is a URL.
pub git: OptValue<String>,
/// The git branch.
pub branch: OptValue<String>,
/// The git tag.
pub tag: OptValue<String>,
/// The git revision.
pub rev: OptValue<String>,
}
/// A map of registry names to URLs where documentations are hosted.
/// This is for unstable feature [`-Zrustdoc-map`][1].
///
/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
#[derive(serde::Deserialize, Debug)]
#[serde(default)]
pub struct RustdocExternMap {
#[serde(deserialize_with = "default_crates_io_to_docs_rs")]
/// * Key is the registry name in the configuration `[registries.<name>]`.
/// * Value is the URL where the documentation is hosted.
pub registries: HashMap<String, String>,
pub std: Option<RustdocExternMode>,
}
impl Default for RustdocExternMap {
fn default() -> Self {
Self {
registries: HashMap::from_iter([(CRATES_IO_REGISTRY.into(), DOCS_RS_URL.into())]),
std: None,
}
}
}
const DOCS_RS_URL: &'static str = "https://docs.rs/";
fn default_crates_io_to_docs_rs<'de, D: serde::Deserializer<'de>>(
de: D,
) -> Result<HashMap<String, String>, D::Error> {
let mut registries = HashMap::deserialize(de)?;
if !registries.contains_key(CRATES_IO_REGISTRY) {
registries.insert(CRATES_IO_REGISTRY.into(), DOCS_RS_URL.into());
}
Ok(registries)
}
impl hash::Hash for RustdocExternMap {
fn hash<H: hash::Hasher>(&self, into: &mut H) {
self.std.hash(into);
for (key, value) in &self.registries {
key.hash(into);
value.hash(into);
}
}
}
/// Mode used for `std`. This is for unstable feature [`-Zrustdoc-map`][1].
///
/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
#[derive(Debug, Hash)]
pub enum RustdocExternMode {
/// Use a local `file://` URL.
Local,
/// Use a remote URL to <https://doc.rust-lang.org/> (default).
Remote,
/// An arbitrary URL.
Url(String),
}
impl From<String> for RustdocExternMode {
fn from(s: String) -> RustdocExternMode {
match s.as_ref() {
"local" => RustdocExternMode::Local,
"remote" => RustdocExternMode::Remote,
_ => RustdocExternMode::Url(s),
}
}
}
impl fmt::Display for RustdocExternMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RustdocExternMode::Local => "local".fmt(f),
RustdocExternMode::Remote => "remote".fmt(f),
RustdocExternMode::Url(s) => s.fmt(f),
}
}
}
impl<'de> serde::de::Deserialize<'de> for RustdocExternMode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(s.into())
}
}
/// The `[http]` table.
///
/// Example configuration:
///
/// ```toml
/// [http]
/// proxy = "host:port"
/// timeout = 30
/// cainfo = "/path/to/ca-bundle.crt"
/// check-revoke = true
/// multiplexing = true
/// ssl-version = "tlsv1.3"
/// ```
#[derive(Debug, Default, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct CargoHttpConfig {
pub proxy: Option<String>,
pub low_speed_limit: Option<u32>,
pub timeout: Option<u64>,
pub cainfo: Option<ConfigRelativePath>,
pub proxy_cainfo: Option<ConfigRelativePath>,
pub check_revoke: Option<bool>,
pub user_agent: Option<String>,
pub debug: Option<bool>,
pub multiplexing: Option<bool>,
pub ssl_version: Option<SslVersionConfig>,
}
/// The `[future-incompat-report]` stable
///
/// Example configuration:
///
/// ```toml
/// [future-incompat-report]
/// frequency = "always"
/// ```
#[derive(Debug, Default, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct CargoFutureIncompatConfig {
frequency: Option<CargoFutureIncompatFrequencyConfig>,
}
#[derive(Debug, Default, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum CargoFutureIncompatFrequencyConfig {
#[default]
Always,
Never,
}
impl CargoFutureIncompatConfig {
pub fn should_display_message(&self) -> bool {
use CargoFutureIncompatFrequencyConfig::*;
let frequency = self.frequency.as_ref().unwrap_or(&Always);
match frequency {
Always => true,
Never => false,
}
}
}
/// Configuration for `ssl-version` in `http` section
/// There are two ways to configure:
///
/// ```text
/// [http]
/// ssl-version = "tlsv1.3"
/// ```
///
/// ```text
/// [http]
/// ssl-version.min = "tlsv1.2"
/// ssl-version.max = "tlsv1.3"
/// ```
#[derive(Clone, Debug, PartialEq)]
pub enum SslVersionConfig {
Single(String),
Range(SslVersionConfigRange),
}
impl<'de> Deserialize<'de> for SslVersionConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
UntaggedEnumVisitor::new()
.string(|single| Ok(SslVersionConfig::Single(single.to_owned())))
.map(|map| map.deserialize().map(SslVersionConfig::Range))
.deserialize(deserializer)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct SslVersionConfigRange {
pub min: Option<String>,
pub max: Option<String>,
}
/// The `[net]` table.
///
/// Example configuration:
///
/// ```toml
/// [net]
/// retry = 2
/// offline = false
/// git-fetch-with-cli = true
/// ```
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct CargoNetConfig {
pub retry: Option<u32>,
pub offline: Option<bool>,
pub git_fetch_with_cli: Option<bool>,
pub ssh: Option<CargoSshConfig>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct CargoSshConfig {
pub known_hosts: Option<Vec<Value<String>>>,
}
/// Configuration for `jobs` in `build` section. There are two
/// ways to configure: An integer or a simple string expression.
///
/// ```toml
/// [build]
/// jobs = 1
/// ```
///
/// ```toml
/// [build]
/// jobs = "default" # Currently only support "default".
/// ```
#[derive(Debug, Clone)]
pub enum JobsConfig {
Integer(i32),
String(String),
}
impl<'de> Deserialize<'de> for JobsConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
UntaggedEnumVisitor::new()
.i32(|int| Ok(JobsConfig::Integer(int)))
.string(|string| Ok(JobsConfig::String(string.to_owned())))
.deserialize(deserializer)
}
}
/// The `[build]` table.
///
/// Example configuration:
///
/// ```toml
/// [build]
/// jobs = 4
/// target = "x86_64-unknown-linux-gnu"
/// target-dir = "target"
/// rustflags = ["-C", "link-arg=-fuse-ld=lld"]
/// incremental = true
/// ```
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct CargoBuildConfig {
// deprecated, but preserved for compatibility
pub pipelining: Option<bool>,
pub dep_info_basedir: Option<ConfigRelativePath>,
pub target_dir: Option<ConfigRelativePath>,
pub build_dir: Option<ConfigRelativePath>,
pub incremental: Option<bool>,
pub target: Option<BuildTargetConfig>,
pub profile: Option<ProfileName>,
pub jobs: Option<JobsConfig>,
pub rustflags: Option<StringList>,
pub rustdocflags: Option<StringList>,
pub rustc_wrapper: Option<ConfigRelativePath>,
pub rustc_workspace_wrapper: Option<ConfigRelativePath>,
pub rustc: Option<ConfigRelativePath>,
pub rustdoc: Option<ConfigRelativePath>,
pub artifact_dir: Option<ConfigRelativePath>,
pub warnings: Option<WarningHandling>,
/// Unstable feature `-Zsbom`.
pub sbom: Option<bool>,
/// Unstable feature `-Zbuild-analysis`.
pub analysis: Option<CargoBuildAnalysis>,
/// Unstable feature `-Zchecksum-freshness`.
pub fingerprint: Option<FingerprintMethod>,
pub build_std: Option<BuildStdConfig>,
}
/// Metrics collection for build analysis.
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct CargoBuildAnalysis {
pub enabled: bool,
}
/// Whether warnings should warn, be allowed, or cause an error.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum WarningHandling {
#[default]
/// Output warnings.
Warn,
/// Allow warnings (do not output them).
Allow,
/// Error if warnings are emitted.
Deny,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum FingerprintMethod {
#[default]
Mtime,
Content,
}
impl FingerprintMethod {
pub fn as_str(&self) -> &'static str {
match self {
Self::Mtime => "mtime",
Self::Content => "content",
}
}
}
impl std::fmt::Display for FingerprintMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_str().fmt(f)
}
}
/// Configuration for `build.build-std`.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct BuildStdConfig {
pub when: Option<BuildStdWhen>,
pub crates: Option<BuildStdCrates>,
}
/// When to build the standard library.
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum BuildStdWhen {
Never,
Always,
}
/// Which standard library crates to build.
/// Valid values are "core", "alloc", "std", or an array of crate names
#[derive(Debug, PartialEq, Eq)]
pub enum BuildStdCrates {
Core,
Alloc,
Std,
// Custom list of crates - permanently unstable
List(Vec<String>),
}
impl<'de> Deserialize<'de> for BuildStdCrates {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
UntaggedEnumVisitor::new()
.string(|s| match s {
"core" => Ok(Self::Core),
"alloc" => Ok(Self::Alloc),
"std" => Ok(Self::Std),
_ => Err(serde::de::Error::unknown_variant(
s,
&["core", "alloc", "std"],
)),
})
.seq(|seq| seq.deserialize().map(Self::List))
.deserialize(deserializer)
}
}
/// Configuration for `build.target`.
///
/// Accepts in the following forms:
///
/// ```toml
/// target = "a"
/// target = ["a"]
/// target = ["a", "b"]
/// ```
#[derive(Debug, Deserialize)]
#[serde(transparent)]
pub struct BuildTargetConfig {
inner: Value<BuildTargetConfigInner>,
}
#[derive(Debug)]
enum BuildTargetConfigInner {
One(String),
Many(Vec<String>),
}
impl<'de> Deserialize<'de> for BuildTargetConfigInner {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
UntaggedEnumVisitor::new()
.string(|one| Ok(BuildTargetConfigInner::One(one.to_owned())))
.seq(|many| many.deserialize().map(BuildTargetConfigInner::Many))
.deserialize(deserializer)
}
}
impl BuildTargetConfig {
/// Gets values of `build.target` as a list of strings.
pub fn values(&self, cwd: &Path) -> CargoResult<Vec<String>> {
let map = |s: &String| {
if s.ends_with(".json") {
// Path to a target specification file (in JSON).
// <https://doc.rust-lang.org/rustc/targets/custom.html>
self.inner
.definition
.root(cwd)
.join(s)
.to_str()
.expect("must be utf-8 in toml")
.to_string()
} else {
// A string. Probably a target tuple.
s.to_string()
}
};
let values = match &self.inner.val {
BuildTargetConfigInner::One(s) => vec![map(s)],
BuildTargetConfigInner::Many(v) => v.iter().map(map).collect(),
};
Ok(values)
}
}
/// The `[resolver]` table.
///
/// Example configuration:
///
/// ```toml
/// [resolver]
/// incompatible-rust-versions = "fallback"
/// incompatible-publish-age = "deny"
/// feature-unification = "workspace"
/// lockfile-path = "my/Cargo.lock"
/// ```
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct CargoResolverConfig {
pub incompatible_rust_versions: Option<IncompatibleRustVersions>,
pub incompatible_publish_age: Option<IncompatiblePublishAge>,
pub feature_unification: Option<FeatureUnification>,
pub lockfile_path: Option<ConfigRelativePath>,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum IncompatibleRustVersions {
Allow,
Fallback,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum IncompatiblePublishAge {
Allow,
Deny,
}
#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum FeatureUnification {
Package,
Selected,
Workspace,
}
/// The `[term]` table.
///
/// Example configuration:
///
/// ```toml
/// [term]
/// verbose = false
/// quiet = false
/// color = "auto"
/// progress.when = "auto"
/// ```
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct TermConfig {
pub verbose: Option<bool>,
pub quiet: Option<bool>,
pub color: Option<String>,
pub hyperlinks: Option<bool>,
pub unicode: Option<bool>,
pub progress: Option<ProgressConfig>,
}
/// The `term.progress` configuration.
///
/// Example configuration:
///
/// ```toml
/// [term]
/// progress.when = "never" # or "auto"
/// ```
///
/// ```toml
/// # `when = "always"` requires a `width` field
/// [term]
/// progress = { when = "always", width = 80 }
/// ```
#[derive(Debug, Default)]
pub struct ProgressConfig {
pub when: ProgressWhen,
pub width: Option<usize>,
/// Communicate progress status with a terminal
pub term_integration: Option<bool>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProgressWhen {
#[default]
Auto,
Never,
Always,
}
// We need this custom deserialization for validadting the rule of
// `when = "always"` requiring a `width` field.
impl<'de> Deserialize<'de> for ProgressConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
struct ProgressConfigInner {
#[serde(default)]
when: ProgressWhen,
width: Option<usize>,
term_integration: Option<bool>,
}
let pc = ProgressConfigInner::deserialize(deserializer)?;
if let ProgressConfigInner {
when: ProgressWhen::Always,
width: None,
..
} = pc
{
return Err(serde::de::Error::custom(
"\"always\" progress requires a `width` key",
));
}
Ok(ProgressConfig {
when: pc.when,
width: pc.width,
term_integration: pc.term_integration,
})
}
}
#[derive(Debug)]
enum EnvConfigValueInner {
Simple(String),
WithOptions {
value: ConfigRelativePath,
force: bool,
relative: bool,
},
}
impl<'de> Deserialize<'de> for EnvConfigValueInner {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct WithOptions {
value: ConfigRelativePath,
#[serde(default)]
force: bool,
#[serde(default)]
relative: bool,
}
UntaggedEnumVisitor::new()
.string(|simple| Ok(EnvConfigValueInner::Simple(simple.to_owned())))
.map(|map| {
let with_options: WithOptions = map.deserialize()?;
Ok(EnvConfigValueInner::WithOptions {
value: with_options.value,
force: with_options.force,
relative: with_options.relative,
})
})
.deserialize(deserializer)
}
}
/// Configuration value for environment variables in `[env]` section.
///
/// Supports two formats: simple string and with options.
///
/// ```toml
/// [env]
/// FOO = "value"
/// ```
///
/// ```toml
/// [env]
/// BAR = { value = "relative/path", relative = true }
/// BAZ = { value = "override", force = true }
/// ```
#[derive(Debug, Deserialize)]
#[serde(transparent)]
pub struct EnvConfigValue {
inner: EnvConfigValueInner,
}
impl EnvConfigValue {
/// Whether this value should override existing environment variables.
pub fn is_force(&self) -> bool {
match self.inner {
EnvConfigValueInner::Simple(_) => false,
EnvConfigValueInner::WithOptions { force, .. } => force,
}
}
/// Resolves the environment variable value.
///
/// If `relative = true`,
/// the value is interpreted as a [`ConfigRelativePath`]-like path.
pub fn resolve<'a>(&'a self, cwd: &Path) -> Cow<'a, OsStr> {
match self.inner {
EnvConfigValueInner::Simple(ref s) => Cow::Borrowed(OsStr::new(s.as_str())),
EnvConfigValueInner::WithOptions {
ref value,
relative,
..
} => {
if relative {
let p = value.value().definition.root(cwd).join(value.raw_value());
Cow::Owned(p.into_os_string())
} else {
Cow::Borrowed(OsStr::new(value.raw_value()))
}
}
}
}
}
pub type EnvConfig = HashMap<String, EnvConfigValue>;
/// Credentials for a registry.
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct RegistryCredential {
pub token: OptValue<Secret<String>>,
}
/// `[registries.NAME]` tables.
///
/// The values here should be kept in sync with `GlobalRegistryConfig`
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct RegistryConfig {
pub index: Option<String>,
#[deprecated = "see `RegistryCredential::token` instead"]
pub token: OptValue<Secret<String>>,
pub credential_provider: Option<PathAndArgs>,
/// Minimum publish age threshold for RFC 3923
pub min_publish_age: Option<String>,
#[serde(rename = "protocol")]
_protocol: Option<String>,
}
/// The `[registry]` table, which has more keys than the `[registries.NAME]` tables.
///
/// Note: nesting `RegistryConfig` inside this struct and using `serde(flatten)` *should* work
/// but fails with "invalid type: sequence, expected a value" when attempting to deserialize.
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct GlobalRegistryConfig {
pub index: Option<String>,
#[deprecated = "see `RegistryCredential::token` instead"]
pub token: OptValue<Secret<String>>,
pub credential_provider: Option<PathAndArgs>,
/// Global default Minimum publish age threshold for RFC 3923
pub global_min_publish_age: Option<String>,
#[serde(rename = "default")]
_default: Option<String>,
#[serde(rename = "global-credential-providers")]
_global_credential_providers: Option<Vec<String>>,
}
impl GlobalRegistryConfig {
pub fn to_registry_config(self) -> RegistryConfig {
RegistryConfig {
index: self.index,
#[expect(deprecated)]
token: self.token,
credential_provider: self.credential_provider,
// `min-publish-age` is per-registry config only.
min_publish_age: None,
_protocol: None,
}
}
}