From e476251168fab96ae3c7544ee1a9f3ae3b7f885f Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 2 Oct 2023 16:27:34 -0500 Subject: [PATCH 1/3] refactor(toml): Consistently lay out fmt/serde impls The theory of organization - Built-in impls first (`Display` in this case) - `Serialize` next to `Display` since they are related --- src/cargo/util/toml/mod.rs | 82 +++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 2e730b4e9fc..c9d3697c75d 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -373,6 +373,18 @@ impl TomlProfiles { #[derive(Clone, Debug, Eq, PartialEq)] pub struct TomlOptLevel(pub String); +impl ser::Serialize for TomlOptLevel { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + match self.0.parse::() { + Ok(n) => n.serialize(serializer), + Err(_) => self.0.serialize(serializer), + } + } +} + impl<'de> de::Deserialize<'de> for TomlOptLevel { fn deserialize(d: D) -> Result where @@ -397,18 +409,6 @@ impl<'de> de::Deserialize<'de> for TomlOptLevel { } } -impl ser::Serialize for TomlOptLevel { - fn serialize(&self, serializer: S) -> Result - where - S: ser::Serializer, - { - match self.0.parse::() { - Ok(n) => n.serialize(serializer), - Err(_) => self.0.serialize(serializer), - } - } -} - #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] pub enum TomlDebugInfo { None, @@ -418,6 +418,18 @@ pub enum TomlDebugInfo { Full, } +impl Display for TomlDebugInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TomlDebugInfo::None => f.write_char('0'), + TomlDebugInfo::Limited => f.write_char('1'), + TomlDebugInfo::Full => f.write_char('2'), + TomlDebugInfo::LineDirectivesOnly => f.write_str("line-directives-only"), + TomlDebugInfo::LineTablesOnly => f.write_str("line-tables-only"), + } + } +} + impl ser::Serialize for TomlDebugInfo { fn serialize(&self, serializer: S) -> Result where @@ -483,18 +495,6 @@ impl<'de> de::Deserialize<'de> for TomlDebugInfo { } } -impl Display for TomlDebugInfo { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - TomlDebugInfo::None => f.write_char('0'), - TomlDebugInfo::Limited => f.write_char('1'), - TomlDebugInfo::Full => f.write_char('2'), - TomlDebugInfo::LineDirectivesOnly => f.write_str("line-directives-only"), - TomlDebugInfo::LineTablesOnly => f.write_str("line-tables-only"), - } - } -} - #[derive(Deserialize, Serialize, Clone, Debug, Default, Eq, PartialEq)] #[serde(default, rename_all = "kebab-case")] pub struct TomlProfile { @@ -526,6 +526,15 @@ pub enum ProfilePackageSpec { All, } +impl fmt::Display for ProfilePackageSpec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProfilePackageSpec::Spec(spec) => spec.fmt(f), + ProfilePackageSpec::All => f.write_str("*"), + } + } +} + impl ser::Serialize for ProfilePackageSpec { fn serialize(&self, s: S) -> Result where @@ -551,15 +560,6 @@ impl<'de> de::Deserialize<'de> for ProfilePackageSpec { } } -impl fmt::Display for ProfilePackageSpec { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ProfilePackageSpec::Spec(spec) => spec.fmt(f), - ProfilePackageSpec::All => f.write_str("*"), - } - } -} - impl TomlProfile { /// Checks stytax validity and unstable feature gate for a given profile. pub fn validate( @@ -3255,21 +3255,21 @@ struct TomlTarget { #[derive(Clone)] struct PathValue(PathBuf); -impl<'de> de::Deserialize<'de> for PathValue { - fn deserialize(deserializer: D) -> Result +impl ser::Serialize for PathValue { + fn serialize(&self, serializer: S) -> Result where - D: de::Deserializer<'de>, + S: ser::Serializer, { - Ok(PathValue(String::deserialize(deserializer)?.into())) + self.0.serialize(serializer) } } -impl ser::Serialize for PathValue { - fn serialize(&self, serializer: S) -> Result +impl<'de> de::Deserialize<'de> for PathValue { + fn deserialize(deserializer: D) -> Result where - S: ser::Serializer, + D: de::Deserializer<'de>, { - self.0.serialize(serializer) + Ok(PathValue(String::deserialize(deserializer)?.into())) } } From b784bafaa7df86d739db6c970da3a62f750428f1 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 2 Oct 2023 16:33:54 -0500 Subject: [PATCH 2/3] refactor(toml): Move impl bodies next to types At minimum, we should have "everything" for a type next to it but it was previously scattered. When collecting the related impl's, I decided to order based on how I tend to do it. Generally the methods are the "core" of the type so I put those first. The order I went with 1. Basic impl body 2. Complex impl body 3. Trait impls 4. Helper free-functions --- src/cargo/util/toml/mod.rs | 5388 ++++++++++++++++++------------------ 1 file changed, 2694 insertions(+), 2694 deletions(-) diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index c9d3697c75d..790beef9959 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -206,6 +206,75 @@ pub enum TomlDependency { Detailed(DetailedTomlDependency

), } +impl TomlDependency { + fn unused_keys(&self) -> Vec { + match self { + TomlDependency::Simple(_) => vec![], + TomlDependency::Detailed(detailed) => detailed.other.keys().cloned().collect(), + } + } +} + +impl TomlDependency

{ + pub(crate) fn to_dependency_split( + &self, + name: &str, + source_id: SourceId, + nested_paths: &mut Vec, + config: &Config, + warnings: &mut Vec, + platform: Option, + root: &Path, + features: &Features, + kind: Option, + ) -> CargoResult { + self.to_dependency( + name, + &mut Context { + deps: &mut Vec::new(), + source_id, + nested_paths, + config, + warnings, + platform, + root, + features, + }, + kind, + ) + } + + fn to_dependency( + &self, + name: &str, + cx: &mut Context<'_, '_>, + kind: Option, + ) -> CargoResult { + match *self { + TomlDependency::Simple(ref version) => DetailedTomlDependency::

{ + version: Some(version.clone()), + ..Default::default() + } + .to_dependency(name, cx, kind), + TomlDependency::Detailed(ref details) => details.to_dependency(name, cx, kind), + } + } + + fn is_version_specified(&self) -> bool { + match self { + TomlDependency::Detailed(d) => d.version.is_some(), + TomlDependency::Simple(..) => true, + } + } + + fn is_optional(&self) -> bool { + match self { + TomlDependency::Detailed(d) => d.optional.unwrap_or(false), + TomlDependency::Simple(..) => false, + } + } +} + impl<'de, P: Deserialize<'de> + Clone> de::Deserialize<'de> for TomlDependency

{ fn deserialize(deserializer: D) -> Result where @@ -222,15 +291,6 @@ impl<'de, P: Deserialize<'de> + Clone> de::Deserialize<'de> for TomlDependency

Vec { - match self { - TomlDependency::Simple(_) => vec![], - TomlDependency::Detailed(detailed) => detailed.other.keys().cloned().collect(), - } - } -} - pub trait ResolveToPath { fn resolve(&self, config: &Config) -> PathBuf; } @@ -286,2940 +346,2880 @@ pub struct DetailedTomlDependency { other: BTreeMap, } -// Explicit implementation so we avoid pulling in P: Default -impl Default for DetailedTomlDependency

{ - fn default() -> Self { - Self { - version: Default::default(), - registry: Default::default(), - registry_index: Default::default(), - path: Default::default(), - git: Default::default(), - branch: Default::default(), - tag: Default::default(), - rev: Default::default(), - features: Default::default(), - optional: Default::default(), - default_features: Default::default(), - default_features2: Default::default(), - package: Default::default(), - public: Default::default(), - artifact: Default::default(), - lib: Default::default(), - target: Default::default(), - other: Default::default(), - } - } -} - -/// This type is used to deserialize `Cargo.toml` files. -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct TomlManifest { - cargo_features: Option>, - package: Option>, - project: Option>, - profile: Option, - lib: Option, - bin: Option>, - example: Option>, - test: Option>, - bench: Option>, - dependencies: Option>, - dev_dependencies: Option>, - #[serde(rename = "dev_dependencies")] - dev_dependencies2: Option>, - build_dependencies: Option>, - #[serde(rename = "build_dependencies")] - build_dependencies2: Option>, - features: Option>>, - target: Option>, - replace: Option>, - patch: Option>>, - workspace: Option, - badges: Option, - lints: Option, -} - -#[derive(Deserialize, Serialize, Clone, Debug, Default)] -pub struct TomlProfiles(BTreeMap); - -impl TomlProfiles { - pub fn get_all(&self) -> &BTreeMap { - &self.0 +impl DetailedTomlDependency { + fn add_features(&mut self, features: Option>) { + self.features = match (self.features.clone(), features.clone()) { + (Some(dep_feat), Some(inherit_feat)) => Some( + dep_feat + .into_iter() + .chain(inherit_feat) + .collect::>(), + ), + (Some(dep_fet), None) => Some(dep_fet), + (None, Some(inherit_feat)) => Some(inherit_feat), + (None, None) => None, + }; } - pub fn get(&self, name: &str) -> Option<&TomlProfile> { - self.0.get(name) + fn update_optional(&mut self, optional: Option) { + self.optional = optional; } - /// Checks syntax validity and unstable feature gate for each profile. - /// - /// It's a bit unfortunate both `-Z` flags and `cargo-features` are required, - /// because profiles can now be set in either `Cargo.toml` or `config.toml`. - pub fn validate( - &self, - cli_unstable: &CliUnstable, - features: &Features, - warnings: &mut Vec, + fn resolve_path( + &mut self, + name: &str, + root_path: &Path, + package_root: &Path, ) -> CargoResult<()> { - for (name, profile) in &self.0 { - profile.validate(name, cli_unstable, features, warnings)?; + if let Some(rel_path) = &self.path { + self.path = Some(resolve_relative_path( + name, + root_path, + package_root, + rel_path, + )?) } Ok(()) } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct TomlOptLevel(pub String); +impl DetailedTomlDependency

{ + fn to_dependency( + &self, + name_in_toml: &str, + cx: &mut Context<'_, '_>, + kind: Option, + ) -> CargoResult { + if self.version.is_none() && self.path.is_none() && self.git.is_none() { + let msg = format!( + "dependency ({}) specified without \ + providing a local path, Git repository, version, or \ + workspace dependency to use. This will be considered an \ + error in future versions", + name_in_toml + ); + cx.warnings.push(msg); + } -impl ser::Serialize for TomlOptLevel { - fn serialize(&self, serializer: S) -> Result - where - S: ser::Serializer, - { - match self.0.parse::() { - Ok(n) => n.serialize(serializer), - Err(_) => self.0.serialize(serializer), + if let Some(version) = &self.version { + if version.contains('+') { + cx.warnings.push(format!( + "version requirement `{}` for dependency `{}` \ + includes semver metadata which will be ignored, removing the \ + metadata is recommended to avoid confusion", + version, name_in_toml + )); + } } - } -} -impl<'de> de::Deserialize<'de> for TomlOptLevel { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - use serde::de::Error as _; - UntaggedEnumVisitor::new() - .expecting("an optimization level") - .i64(|value| Ok(TomlOptLevel(value.to_string()))) - .string(|value| { - if value == "s" || value == "z" { - Ok(TomlOptLevel(value.to_string())) - } else { - Err(serde_untagged::de::Error::custom(format!( - "must be `0`, `1`, `2`, `3`, `s` or `z`, \ - but found the string: \"{}\"", - value - ))) + if self.git.is_none() { + let git_only_keys = [ + (&self.branch, "branch"), + (&self.tag, "tag"), + (&self.rev, "rev"), + ]; + + for &(key, key_name) in &git_only_keys { + if key.is_some() { + bail!( + "key `{}` is ignored for dependency ({}).", + key_name, + name_in_toml + ); } - }) - .deserialize(d) - } -} + } + } -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] -pub enum TomlDebugInfo { - None, - LineDirectivesOnly, - LineTablesOnly, - Limited, - Full, -} - -impl Display for TomlDebugInfo { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - TomlDebugInfo::None => f.write_char('0'), - TomlDebugInfo::Limited => f.write_char('1'), - TomlDebugInfo::Full => f.write_char('2'), - TomlDebugInfo::LineDirectivesOnly => f.write_str("line-directives-only"), - TomlDebugInfo::LineTablesOnly => f.write_str("line-tables-only"), + // Early detection of potentially misused feature syntax + // instead of generating a "feature not found" error. + if let Some(features) = &self.features { + for feature in features { + if feature.contains('/') { + bail!( + "feature `{}` in dependency `{}` is not allowed to contain slashes\n\ + If you want to enable features of a transitive dependency, \ + the direct dependency needs to re-export those features from \ + the `[features]` table.", + feature, + name_in_toml + ); + } + if feature.starts_with("dep:") { + bail!( + "feature `{}` in dependency `{}` is not allowed to use explicit \ + `dep:` syntax\n\ + If you want to enable an optional dependency, specify the name \ + of the optional dependency without the `dep:` prefix, or specify \ + a feature from the dependency's `[features]` table that enables \ + the optional dependency.", + feature, + name_in_toml + ); + } + } } - } -} -impl ser::Serialize for TomlDebugInfo { - fn serialize(&self, serializer: S) -> Result - where - S: ser::Serializer, - { - match self { - Self::None => 0.serialize(serializer), - Self::LineDirectivesOnly => "line-directives-only".serialize(serializer), - Self::LineTablesOnly => "line-tables-only".serialize(serializer), - Self::Limited => 1.serialize(serializer), - Self::Full => 2.serialize(serializer), - } - } -} + let new_source_id = match ( + self.git.as_ref(), + self.path.as_ref(), + self.registry.as_ref(), + self.registry_index.as_ref(), + ) { + (Some(_), _, Some(_), _) | (Some(_), _, _, Some(_)) => bail!( + "dependency ({}) specification is ambiguous. \ + Only one of `git` or `registry` is allowed.", + name_in_toml + ), + (_, _, Some(_), Some(_)) => bail!( + "dependency ({}) specification is ambiguous. \ + Only one of `registry` or `registry-index` is allowed.", + name_in_toml + ), + (Some(git), maybe_path, _, _) => { + if maybe_path.is_some() { + bail!( + "dependency ({}) specification is ambiguous. \ + Only one of `git` or `path` is allowed.", + name_in_toml + ); + } -impl<'de> de::Deserialize<'de> for TomlDebugInfo { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - use serde::de::Error as _; - let expecting = "a boolean, 0, 1, 2, \"line-tables-only\", or \"line-directives-only\""; - UntaggedEnumVisitor::new() - .expecting(expecting) - .bool(|value| { - Ok(if value { - TomlDebugInfo::Full - } else { - TomlDebugInfo::None - }) - }) - .i64(|value| { - let debuginfo = match value { - 0 => TomlDebugInfo::None, - 1 => TomlDebugInfo::Limited, - 2 => TomlDebugInfo::Full, - _ => { - return Err(serde_untagged::de::Error::invalid_value( - Unexpected::Signed(value), - &expecting, - )) - } - }; - Ok(debuginfo) - }) - .string(|value| { - let debuginfo = match value { - "none" => TomlDebugInfo::None, - "limited" => TomlDebugInfo::Limited, - "full" => TomlDebugInfo::Full, - "line-directives-only" => TomlDebugInfo::LineDirectivesOnly, - "line-tables-only" => TomlDebugInfo::LineTablesOnly, - _ => { - return Err(serde_untagged::de::Error::invalid_value( - Unexpected::Str(value), - &expecting, - )) - } - }; - Ok(debuginfo) - }) - .deserialize(d) - } -} + let n_details = [&self.branch, &self.tag, &self.rev] + .iter() + .filter(|d| d.is_some()) + .count(); -#[derive(Deserialize, Serialize, Clone, Debug, Default, Eq, PartialEq)] -#[serde(default, rename_all = "kebab-case")] -pub struct TomlProfile { - pub opt_level: Option, - pub lto: Option, - pub codegen_backend: Option, - pub codegen_units: Option, - pub debug: Option, - pub split_debuginfo: Option, - pub debug_assertions: Option, - pub rpath: Option, - pub panic: Option, - pub overflow_checks: Option, - pub incremental: Option, - pub dir_name: Option, - pub inherits: Option, - pub strip: Option, - // Note that `rustflags` is used for the cargo-feature `profile_rustflags` - pub rustflags: Option>, - // These two fields must be last because they are sub-tables, and TOML - // requires all non-tables to be listed first. - pub package: Option>, - pub build_override: Option>, -} + if n_details > 1 { + bail!( + "dependency ({}) specification is ambiguous. \ + Only one of `branch`, `tag` or `rev` is allowed.", + name_in_toml + ); + } -#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] -pub enum ProfilePackageSpec { - Spec(PackageIdSpec), - All, -} + let reference = self + .branch + .clone() + .map(GitReference::Branch) + .or_else(|| self.tag.clone().map(GitReference::Tag)) + .or_else(|| self.rev.clone().map(GitReference::Rev)) + .unwrap_or(GitReference::DefaultBranch); + let loc = git.into_url()?; -impl fmt::Display for ProfilePackageSpec { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ProfilePackageSpec::Spec(spec) => spec.fmt(f), - ProfilePackageSpec::All => f.write_str("*"), - } - } -} + if let Some(fragment) = loc.fragment() { + let msg = format!( + "URL fragment `#{}` in git URL is ignored for dependency ({}). \ + If you were trying to specify a specific git revision, \ + use `rev = \"{}\"` in the dependency declaration.", + fragment, name_in_toml, fragment + ); + cx.warnings.push(msg) + } -impl ser::Serialize for ProfilePackageSpec { - fn serialize(&self, s: S) -> Result - where - S: ser::Serializer, - { - self.to_string().serialize(s) - } -} + SourceId::for_git(&loc, reference)? + } + (None, Some(path), _, _) => { + let path = path.resolve(cx.config); + cx.nested_paths.push(path.clone()); + // If the source ID for the package we're parsing is a path + // source, then we normalize the path here to get rid of + // components like `..`. + // + // The purpose of this is to get a canonical ID for the package + // that we're depending on to ensure that builds of this package + // always end up hashing to the same value no matter where it's + // built from. + if cx.source_id.is_path() { + let path = cx.root.join(path); + let path = paths::normalize_path(&path); + SourceId::for_path(&path)? + } else { + cx.source_id + } + } + (None, None, Some(registry), None) => SourceId::alt_registry(cx.config, registry)?, + (None, None, None, Some(registry_index)) => { + let url = registry_index.into_url()?; + SourceId::for_registry(&url)? + } + (None, None, None, None) => SourceId::crates_io(cx.config)?, + }; -impl<'de> de::Deserialize<'de> for ProfilePackageSpec { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - let string = String::deserialize(d)?; - if string == "*" { - Ok(ProfilePackageSpec::All) - } else { - PackageIdSpec::parse(&string) - .map_err(de::Error::custom) - .map(ProfilePackageSpec::Spec) - } - } -} + let (pkg_name, explicit_name_in_toml) = match self.package { + Some(ref s) => (&s[..], Some(name_in_toml)), + None => (name_in_toml, None), + }; -impl TomlProfile { - /// Checks stytax validity and unstable feature gate for a given profile. - pub fn validate( - &self, - name: &str, - cli_unstable: &CliUnstable, - features: &Features, - warnings: &mut Vec, - ) -> CargoResult<()> { - self.validate_profile(name, cli_unstable, features)?; - if let Some(ref profile) = self.build_override { - profile.validate_override("build-override")?; - profile.validate_profile(&format!("{name}.build-override"), cli_unstable, features)?; + let version = self.version.as_deref(); + let mut dep = Dependency::parse(pkg_name, version, new_source_id)?; + if self.default_features.is_some() && self.default_features2.is_some() { + warn_on_deprecated("default-features", name_in_toml, "dependency", cx.warnings); } - if let Some(ref packages) = self.package { - for (override_name, profile) in packages { - profile.validate_override("package")?; - profile.validate_profile( - &format!("{name}.package.{override_name}"), - cli_unstable, - features, - )?; - } + dep.set_features(self.features.iter().flatten()) + .set_default_features( + self.default_features + .or(self.default_features2) + .unwrap_or(true), + ) + .set_optional(self.optional.unwrap_or(false)) + .set_platform(cx.platform.clone()); + if let Some(registry) = &self.registry { + let registry_id = SourceId::alt_registry(cx.config, registry)?; + dep.set_registry_id(registry_id); } - - // Profile name validation - Self::validate_name(name)?; - - if let Some(dir_name) = self.dir_name { - // This is disabled for now, as we would like to stabilize named - // profiles without this, and then decide in the future if it is - // needed. This helps simplify the UI a little. - bail!( - "dir-name=\"{}\" in profile `{}` is not currently allowed, \ - directory names are tied to the profile name for custom profiles", - dir_name, - name - ); + if let Some(registry_index) = &self.registry_index { + let url = registry_index.into_url()?; + let registry_id = SourceId::for_registry(&url)?; + dep.set_registry_id(registry_id); } - // `inherits` validation - if matches!(self.inherits.map(|s| s.as_str()), Some("debug")) { - bail!( - "profile.{}.inherits=\"debug\" should be profile.{}.inherits=\"dev\"", - name, - name - ); + if let Some(kind) = kind { + dep.set_kind(kind); } - - match name { - "doc" => { - warnings.push("profile `doc` is deprecated and has no effect".to_string()); - } - "test" | "bench" => { - if self.panic.is_some() { - warnings.push(format!("`panic` setting is ignored for `{}` profile", name)) - } - } - _ => {} + if let Some(name_in_toml) = explicit_name_in_toml { + dep.set_explicit_name_in_toml(name_in_toml); } - if let Some(panic) = &self.panic { - if panic != "unwind" && panic != "abort" { - bail!( - "`panic` setting of `{}` is not a valid setting, \ - must be `unwind` or `abort`", - panic - ); + if let Some(p) = self.public { + cx.features.require(Feature::public_dependency())?; + + if dep.kind() != DepKind::Normal { + bail!("'public' specifier can only be used on regular dependencies, not {:?} dependencies", dep.kind()); } + + dep.set_public(p); } - if let Some(StringOrBool::String(arg)) = &self.lto { - if arg == "true" || arg == "false" { + if let (Some(artifact), is_lib, target) = ( + self.artifact.as_ref(), + self.lib.unwrap_or(false), + self.target.as_deref(), + ) { + if cx.config.cli_unstable().bindeps { + let artifact = Artifact::parse(&artifact.0, is_lib, target)?; + if dep.kind() != DepKind::Build + && artifact.target() == Some(ArtifactTarget::BuildDependencyAssumeTarget) + { + bail!( + r#"`target = "target"` in normal- or dev-dependencies has no effect ({})"#, + name_in_toml + ); + } + dep.set_artifact(artifact) + } else { + bail!("`artifact = …` requires `-Z bindeps` ({})", name_in_toml); + } + } else if self.lib.is_some() || self.target.is_some() { + for (is_set, specifier) in [ + (self.lib.is_some(), "lib"), + (self.target.is_some(), "target"), + ] { + if !is_set { + continue; + } bail!( - "`lto` setting of string `\"{arg}\"` for `{name}` profile is not \ - a valid setting, must be a boolean (`true`/`false`) or a string \ - (`\"thin\"`/`\"fat\"`/`\"off\"`) or omitted.", - ); + "'{}' specifier cannot be used without an 'artifact = …' value ({})", + specifier, + name_in_toml + ) } } - - Ok(()) + Ok(dep) } +} - /// Validate dir-names and profile names according to RFC 2678. - pub fn validate_name(name: &str) -> CargoResult<()> { - if let Some(ch) = name - .chars() - .find(|ch| !ch.is_alphanumeric() && *ch != '_' && *ch != '-') - { - bail!( - "invalid character `{}` in profile name `{}`\n\ - Allowed characters are letters, numbers, underscore, and hyphen.", - ch, - name - ); +// Explicit implementation so we avoid pulling in P: Default +impl Default for DetailedTomlDependency

{ + fn default() -> Self { + Self { + version: Default::default(), + registry: Default::default(), + registry_index: Default::default(), + path: Default::default(), + git: Default::default(), + branch: Default::default(), + tag: Default::default(), + rev: Default::default(), + features: Default::default(), + optional: Default::default(), + default_features: Default::default(), + default_features2: Default::default(), + package: Default::default(), + public: Default::default(), + artifact: Default::default(), + lib: Default::default(), + target: Default::default(), + other: Default::default(), } + } +} - const SEE_DOCS: &str = "See https://doc.rust-lang.org/cargo/reference/profiles.html \ - for more on configuring profiles."; +/// This type is used to deserialize `Cargo.toml` files. +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct TomlManifest { + cargo_features: Option>, + package: Option>, + project: Option>, + profile: Option, + lib: Option, + bin: Option>, + example: Option>, + test: Option>, + bench: Option>, + dependencies: Option>, + dev_dependencies: Option>, + #[serde(rename = "dev_dependencies")] + dev_dependencies2: Option>, + build_dependencies: Option>, + #[serde(rename = "build_dependencies")] + build_dependencies2: Option>, + features: Option>>, + target: Option>, + replace: Option>, + patch: Option>>, + workspace: Option, + badges: Option, + lints: Option, +} - let lower_name = name.to_lowercase(); - if lower_name == "debug" { - bail!( - "profile name `{}` is reserved\n\ - To configure the default development profile, use the name `dev` \ - as in [profile.dev]\n\ - {}", - name, - SEE_DOCS - ); - } - if lower_name == "build-override" { - bail!( - "profile name `{}` is reserved\n\ - To configure build dependency settings, use [profile.dev.build-override] \ - and [profile.release.build-override]\n\ - {}", - name, - SEE_DOCS - ); +impl TomlManifest { + /// Prepares the manifest for publishing. + // - Path and git components of dependency specifications are removed. + // - License path is updated to point within the package. + pub fn prepare_for_publish( + &self, + ws: &Workspace<'_>, + package_root: &Path, + ) -> CargoResult { + let config = ws.config(); + let mut package = self + .package + .as_ref() + .or_else(|| self.project.as_ref()) + .unwrap() + .clone(); + package.workspace = None; + let current_resolver = package + .resolver + .as_ref() + .map(|r| ResolveBehavior::from_manifest(r)) + .unwrap_or_else(|| { + package + .edition + .as_ref() + .and_then(|e| e.as_defined()) + .map(|e| Edition::from_str(e)) + .unwrap_or(Ok(Edition::Edition2015)) + .map(|e| e.default_resolve_behavior()) + })?; + if ws.resolve_behavior() != current_resolver { + // This ensures the published crate if built as a root (e.g. `cargo install`) will + // use the same resolver behavior it was tested with in the workspace. + // To avoid forcing a higher MSRV we don't explicitly set this if it would implicitly + // result in the same thing. + package.resolver = Some(ws.resolve_behavior().to_manifest()); } - - // These are some arbitrary reservations. We have no plans to use - // these, but it seems safer to reserve a few just in case we want to - // add more built-in profiles in the future. We can also uses special - // syntax like cargo:foo if needed. But it is unlikely these will ever - // be used. - if matches!( - lower_name.as_str(), - "build" - | "check" - | "clean" - | "config" - | "fetch" - | "fix" - | "install" - | "metadata" - | "package" - | "publish" - | "report" - | "root" - | "run" - | "rust" - | "rustc" - | "rustdoc" - | "target" - | "tmp" - | "uninstall" - ) || lower_name.starts_with("cargo") - { - bail!( - "profile name `{}` is reserved\n\ - Please choose a different name.\n\ - {}", - name, - SEE_DOCS - ); - } - - Ok(()) - } - - /// Validates a profile. - /// - /// This is a shallow check, which is reused for the profile itself and any overrides. - fn validate_profile( - &self, - name: &str, - cli_unstable: &CliUnstable, - features: &Features, - ) -> CargoResult<()> { - if let Some(codegen_backend) = &self.codegen_backend { - match ( - features.require(Feature::codegen_backend()), - cli_unstable.codegen_backend, - ) { - (Err(e), false) => return Err(e), - _ => {} - } - - if codegen_backend.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_') { - bail!( - "`profile.{}.codegen-backend` setting of `{}` is not a valid backend name.", - name, - codegen_backend, - ); - } - } - if self.rustflags.is_some() { - match ( - features.require(Feature::profile_rustflags()), - cli_unstable.profile_rustflags, - ) { - (Err(e), false) => return Err(e), - _ => {} + if let Some(license_file) = &package.license_file { + let license_file = license_file + .as_defined() + .context("license file should have been resolved before `prepare_for_publish()`")?; + let license_path = Path::new(&license_file); + let abs_license_path = paths::normalize_path(&package_root.join(license_path)); + if abs_license_path.strip_prefix(package_root).is_err() { + // This path points outside of the package root. `cargo package` + // will copy it into the root, so adjust the path to this location. + package.license_file = Some(MaybeWorkspace::Defined( + license_path + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(), + )); } } - Ok(()) - } - - /// Validation that is specific to an override. - fn validate_override(&self, which: &str) -> CargoResult<()> { - if self.package.is_some() { - bail!("package-specific profiles cannot be nested"); - } - if self.build_override.is_some() { - bail!("build-override profiles cannot be nested"); - } - if self.panic.is_some() { - bail!("`panic` may not be specified in a `{}` profile", which) - } - if self.lto.is_some() { - bail!("`lto` may not be specified in a `{}` profile", which) - } - if self.rpath.is_some() { - bail!("`rpath` may not be specified in a `{}` profile", which) - } - Ok(()) - } - /// Overwrite self's values with the given profile. - pub fn merge(&mut self, profile: &TomlProfile) { - if let Some(v) = &profile.opt_level { - self.opt_level = Some(v.clone()); + if let Some(readme) = &package.readme { + let readme = readme + .as_defined() + .context("readme should have been resolved before `prepare_for_publish()`")?; + match readme { + StringOrBool::String(readme) => { + let readme_path = Path::new(&readme); + let abs_readme_path = paths::normalize_path(&package_root.join(readme_path)); + if abs_readme_path.strip_prefix(package_root).is_err() { + // This path points outside of the package root. `cargo package` + // will copy it into the root, so adjust the path to this location. + package.readme = Some(MaybeWorkspace::Defined(StringOrBool::String( + readme_path + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(), + ))); + } + } + StringOrBool::Bool(_) => {} + } } + let all = |_d: &TomlDependency| true; + return Ok(TomlManifest { + package: Some(package), + project: None, + profile: self.profile.clone(), + lib: self.lib.clone(), + bin: self.bin.clone(), + example: self.example.clone(), + test: self.test.clone(), + bench: self.bench.clone(), + dependencies: map_deps(config, self.dependencies.as_ref(), all)?, + dev_dependencies: map_deps( + config, + self.dev_dependencies + .as_ref() + .or_else(|| self.dev_dependencies2.as_ref()), + TomlDependency::is_version_specified, + )?, + dev_dependencies2: None, + build_dependencies: map_deps( + config, + self.build_dependencies + .as_ref() + .or_else(|| self.build_dependencies2.as_ref()), + all, + )?, + build_dependencies2: None, + features: self.features.clone(), + target: match self.target.as_ref().map(|target_map| { + target_map + .iter() + .map(|(k, v)| { + Ok(( + k.clone(), + TomlPlatform { + dependencies: map_deps(config, v.dependencies.as_ref(), all)?, + dev_dependencies: map_deps( + config, + v.dev_dependencies + .as_ref() + .or_else(|| v.dev_dependencies2.as_ref()), + TomlDependency::is_version_specified, + )?, + dev_dependencies2: None, + build_dependencies: map_deps( + config, + v.build_dependencies + .as_ref() + .or_else(|| v.build_dependencies2.as_ref()), + all, + )?, + build_dependencies2: None, + }, + )) + }) + .collect() + }) { + Some(Ok(v)) => Some(v), + Some(Err(e)) => return Err(e), + None => None, + }, + replace: None, + patch: None, + workspace: None, + badges: self.badges.clone(), + cargo_features: self.cargo_features.clone(), + lints: self.lints.clone(), + }); - if let Some(v) = &profile.lto { - self.lto = Some(v.clone()); + fn map_deps( + config: &Config, + deps: Option<&BTreeMap>, + filter: impl Fn(&TomlDependency) -> bool, + ) -> CargoResult>> { + let Some(deps) = deps else { return Ok(None) }; + let deps = deps + .iter() + .filter(|(_k, v)| { + if let MaybeWorkspace::Defined(def) = v { + filter(def) + } else { + false + } + }) + .map(|(k, v)| Ok((k.clone(), map_dependency(config, v)?))) + .collect::>>()?; + Ok(Some(deps)) } - if let Some(v) = profile.codegen_backend { - self.codegen_backend = Some(v); + fn map_dependency( + config: &Config, + dep: &MaybeWorkspaceDependency, + ) -> CargoResult { + let dep = match dep { + MaybeWorkspace::Defined(TomlDependency::Detailed(d)) => { + let mut d = d.clone(); + // Path dependencies become crates.io deps. + d.path.take(); + // Same with git dependencies. + d.git.take(); + d.branch.take(); + d.tag.take(); + d.rev.take(); + // registry specifications are elaborated to the index URL + if let Some(registry) = d.registry.take() { + d.registry_index = Some(config.get_registry_index(®istry)?.to_string()); + } + Ok(d) + } + MaybeWorkspace::Defined(TomlDependency::Simple(s)) => Ok(DetailedTomlDependency { + version: Some(s.clone()), + ..Default::default() + }), + _ => unreachable!(), + }; + dep.map(TomlDependency::Detailed) + .map(MaybeWorkspace::Defined) } + } - if let Some(v) = profile.codegen_units { - self.codegen_units = Some(v); - } - - if let Some(v) = profile.debug { - self.debug = Some(v); + pub fn to_real_manifest( + me: &Rc, + embedded: bool, + source_id: SourceId, + package_root: &Path, + config: &Config, + ) -> CargoResult<(Manifest, Vec)> { + fn get_ws( + config: &Config, + resolved_path: &Path, + workspace_config: &WorkspaceConfig, + ) -> CargoResult { + match workspace_config { + WorkspaceConfig::Root(root) => Ok(root.inheritable().clone()), + WorkspaceConfig::Member { + root: Some(ref path_to_root), + } => { + let path = resolved_path + .parent() + .unwrap() + .join(path_to_root) + .join("Cargo.toml"); + let root_path = paths::normalize_path(&path); + inheritable_from_path(config, root_path) + } + WorkspaceConfig::Member { root: None } => { + match find_workspace_root(&resolved_path, config)? { + Some(path_to_root) => inheritable_from_path(config, path_to_root), + None => Err(anyhow!("failed to find a workspace root")), + } + } + } } - if let Some(v) = profile.debug_assertions { - self.debug_assertions = Some(v); - } + if !package_root.is_dir() { + bail!( + "package root '{}' is not a directory", + package_root.display() + ); + }; - if let Some(v) = &profile.split_debuginfo { - self.split_debuginfo = Some(v.clone()); - } + let mut nested_paths = vec![]; + let mut warnings = vec![]; + let mut errors = vec![]; - if let Some(v) = profile.rpath { - self.rpath = Some(v); - } + // Parse features first so they will be available when parsing other parts of the TOML. + let empty = Vec::new(); + let cargo_features = me.cargo_features.as_ref().unwrap_or(&empty); + let features = Features::new(cargo_features, config, &mut warnings, source_id.is_path())?; - if let Some(v) = &profile.panic { - self.panic = Some(v.clone()); - } + let mut package = match (&me.package, &me.project) { + (Some(_), Some(project)) => { + if source_id.is_path() { + config.shell().warn(format!( + "manifest at `{}` contains both `project` and `package`, \ + this could become a hard error in the future", + package_root.display() + ))?; + } + project.clone() + } + (Some(package), None) => package.clone(), + (None, Some(project)) => { + if source_id.is_path() { + config.shell().warn(format!( + "manifest at `{}` contains `[project]` instead of `[package]`, \ + this could become a hard error in the future", + package_root.display() + ))?; + } + project.clone() + } + (None, None) => bail!("no `package` section found"), + }; - if let Some(v) = profile.overflow_checks { - self.overflow_checks = Some(v); - } + let workspace_config = match (me.workspace.as_ref(), package.workspace.as_ref()) { + (Some(toml_config), None) => { + let mut inheritable = toml_config.package.clone().unwrap_or_default(); + inheritable.update_ws_path(package_root.to_path_buf()); + inheritable.update_deps(toml_config.dependencies.clone()); + let lints = toml_config.lints.clone(); + let lints = verify_lints(lints)?; + inheritable.update_lints(lints); + if let Some(ws_deps) = &inheritable.dependencies { + for (name, dep) in ws_deps { + unused_dep_keys( + name, + "workspace.dependencies", + dep.unused_keys(), + &mut warnings, + ); + } + } + let ws_root_config = WorkspaceRootConfig::new( + package_root, + &toml_config.members, + &toml_config.default_members, + &toml_config.exclude, + &Some(inheritable), + &toml_config.metadata, + ); + config + .ws_roots + .borrow_mut() + .insert(package_root.to_path_buf(), ws_root_config.clone()); + WorkspaceConfig::Root(ws_root_config) + } + (None, root) => WorkspaceConfig::Member { + root: root.cloned(), + }, + (Some(..), Some(..)) => bail!( + "cannot configure both `package.workspace` and \ + `[workspace]`, only one can be specified" + ), + }; - if let Some(v) = profile.incremental { - self.incremental = Some(v); + let package_name = package.name.trim(); + if package_name.is_empty() { + bail!("package name cannot be an empty string") } - if let Some(v) = &profile.rustflags { - self.rustflags = Some(v.clone()); + validate_package_name(package_name, "package name", "")?; + + let resolved_path = package_root.join("Cargo.toml"); + + let inherit_cell: LazyCell = LazyCell::new(); + let inherit = + || inherit_cell.try_borrow_with(|| get_ws(config, &resolved_path, &workspace_config)); + + let version = package + .version + .clone() + .resolve("version", || inherit()?.version())?; + + package.version = MaybeWorkspace::Defined(version.clone()); + + let pkgid = package.to_package_id(source_id, version)?; + + let edition = if let Some(edition) = package.edition.clone() { + let edition: Edition = edition + .resolve("edition", || inherit()?.edition())? + .parse() + .with_context(|| "failed to parse the `edition` key")?; + package.edition = Some(MaybeWorkspace::Defined(edition.to_string())); + edition + } else { + Edition::Edition2015 + }; + // Add these lines if start a new unstable edition. + // ``` + // if edition == Edition::Edition20xx { + // features.require(Feature::edition20xx))?; + // } + // ``` + if !edition.is_stable() { + // Guard in case someone forgets to add .require() + return Err(util::errors::internal(format!( + "edition {} should be gated", + edition + ))); } - if let Some(other_package) = &profile.package { - match &mut self.package { - Some(self_package) => { - for (spec, other_pkg_profile) in other_package { - match self_package.get_mut(spec) { - Some(p) => p.merge(other_pkg_profile), - None => { - self_package.insert(spec.clone(), other_pkg_profile.clone()); - } - } - } + let rust_version = if let Some(rust_version) = &package.rust_version { + let rust_version = rust_version + .clone() + .resolve("rust_version", || inherit()?.rust_version())?; + let req = rust_version.caret_req(); + if let Some(first_version) = edition.first_version() { + let unsupported = + semver::Version::new(first_version.major, first_version.minor - 1, 9999); + if req.matches(&unsupported) { + bail!( + "rust-version {} is older than first version ({}) required by \ + the specified edition ({})", + rust_version, + first_version, + edition, + ) } - None => self.package = Some(other_package.clone()), } - } + Some(rust_version) + } else { + None + }; - if let Some(other_bo) = &profile.build_override { - match &mut self.build_override { - Some(self_bo) => self_bo.merge(other_bo), - None => self.build_override = Some(other_bo.clone()), - } + if package.metabuild.is_some() { + features.require(Feature::metabuild())?; } - if let Some(v) = &profile.inherits { - self.inherits = Some(*v); - } + let resolve_behavior = match ( + package.resolver.as_ref(), + me.workspace.as_ref().and_then(|ws| ws.resolver.as_ref()), + ) { + (None, None) => None, + (Some(s), None) | (None, Some(s)) => Some(ResolveBehavior::from_manifest(s)?), + (Some(_), Some(_)) => { + bail!("cannot specify `resolver` field in both `[workspace]` and `[package]`") + } + }; - if let Some(v) = &profile.dir_name { - self.dir_name = Some(*v); - } + // If we have no lib at all, use the inferred lib, if available. + // If we have a lib with a path, we're done. + // If we have a lib with no path, use the inferred lib or else the package name. + let targets = targets( + &features, + me, + package_name, + package_root, + edition, + &package.build, + &package.metabuild, + &mut warnings, + &mut errors, + )?; - if let Some(v) = &profile.strip { - self.strip = Some(v.clone()); + if targets.is_empty() { + debug!("manifest has no build targets"); } - } -} - -/// A StringOrVec can be parsed from either a TOML string or array, -/// but is always stored as a vector. -#[derive(Clone, Debug, Serialize, Eq, PartialEq, PartialOrd, Ord)] -pub struct StringOrVec(Vec); -impl<'de> de::Deserialize<'de> for StringOrVec { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - UntaggedEnumVisitor::new() - .expecting("string or list of strings") - .string(|value| Ok(StringOrVec(vec![value.to_owned()]))) - .seq(|value| value.deserialize().map(StringOrVec)) - .deserialize(deserializer) - } -} - -impl StringOrVec { - pub fn iter<'a>(&'a self) -> std::slice::Iter<'a, String> { - self.0.iter() - } -} - -#[derive(Clone, Debug, Serialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum StringOrBool { - String(String), - Bool(bool), -} - -impl<'de> Deserialize<'de> for StringOrBool { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - UntaggedEnumVisitor::new() - .bool(|b| Ok(StringOrBool::Bool(b))) - .string(|s| Ok(StringOrBool::String(s.to_owned()))) - .deserialize(deserializer) - } -} - -#[derive(PartialEq, Clone, Debug, Serialize)] -#[serde(untagged)] -pub enum VecStringOrBool { - VecString(Vec), - Bool(bool), -} + if let Err(conflict_targets) = unique_build_targets(&targets, package_root) { + conflict_targets + .iter() + .for_each(|(target_path, conflicts)| { + warnings.push(format!( + "file `{}` found to be present in multiple \ + build targets:\n{}", + target_path.display().to_string(), + conflicts + .iter() + .map(|t| format!( + " * `{}` target `{}`", + t.kind().description(), + t.name(), + )) + .join("\n") + )); + }) + } -impl<'de> de::Deserialize<'de> for VecStringOrBool { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - UntaggedEnumVisitor::new() - .expecting("a boolean or vector of strings") - .bool(|value| Ok(VecStringOrBool::Bool(value))) - .seq(|value| value.deserialize().map(VecStringOrBool::VecString)) - .deserialize(deserializer) - } -} + if let Some(links) = &package.links { + if !targets.iter().any(|t| t.is_custom_build()) { + bail!( + "package `{}` specifies that it links to `{}` but does not \ + have a custom build script", + pkgid, + links + ) + } + } -fn version_trim_whitespace<'de, D>(deserializer: D) -> Result -where - D: de::Deserializer<'de>, -{ - UntaggedEnumVisitor::new() - .expecting("SemVer version") - .string( - |value| match value.trim().parse().map_err(de::Error::custom) { - Ok(parsed) => Ok(MaybeWorkspace::Defined(parsed)), - Err(e) => Err(e), - }, - ) - .map(|value| value.deserialize().map(MaybeWorkspace::Workspace)) - .deserialize(deserializer) -} + let mut deps = Vec::new(); -/// This Trait exists to make [`MaybeWorkspace::Workspace`] generic. It makes deserialization of -/// [`MaybeWorkspace`] much easier, as well as making error messages for -/// [`MaybeWorkspace::resolve`] much nicer -/// -/// Implementors should have a field `workspace` with the type of `bool`. It is used to ensure -/// `workspace` is not `false` in a `Cargo.toml` -pub trait WorkspaceInherit { - /// This is the workspace table that is being inherited from. - /// For example `[workspace.dependencies]` would be the table "dependencies" - fn inherit_toml_table(&self) -> &str; + let mut cx = Context { + deps: &mut deps, + source_id, + nested_paths: &mut nested_paths, + config, + warnings: &mut warnings, + features: &features, + platform: None, + root: package_root, + }; - /// This is used to output the value of the implementors `workspace` field - fn workspace(&self) -> bool; -} + fn process_dependencies( + cx: &mut Context<'_, '_>, + new_deps: Option<&BTreeMap>, + kind: Option, + workspace_config: &WorkspaceConfig, + inherit_cell: &LazyCell, + ) -> CargoResult>> { + let Some(dependencies) = new_deps else { + return Ok(None); + }; -/// An enum that allows for inheriting keys from a workspace in a Cargo.toml. -#[derive(Serialize, Copy, Clone, Debug)] -#[serde(untagged)] -pub enum MaybeWorkspace { - /// The "defined" type, or the type that that is used when not inheriting from a workspace. - Defined(T), - /// The type when inheriting from a workspace. - Workspace(W), -} + let inheritable = || { + inherit_cell.try_borrow_with(|| { + get_ws(cx.config, &cx.root.join("Cargo.toml"), &workspace_config) + }) + }; -impl MaybeWorkspace { - fn resolve<'a>( - self, - label: &str, - get_ws_inheritable: impl FnOnce() -> CargoResult, - ) -> CargoResult { - match self { - MaybeWorkspace::Defined(value) => Ok(value), - MaybeWorkspace::Workspace(w) => get_ws_inheritable().with_context(|| { - format!( - "error inheriting `{label}` from workspace root manifest's `workspace.{}.{label}`", - w.inherit_toml_table(), - ) - }), + let mut deps: BTreeMap = BTreeMap::new(); + for (n, v) in dependencies.iter() { + let resolved = v + .clone() + .resolve_with_self(n, |dep| dep.resolve(n, inheritable, cx))?; + let dep = resolved.to_dependency(n, cx, kind)?; + let name_in_toml = dep.name_in_toml().as_str(); + validate_package_name(name_in_toml, "dependency name", "")?; + let kind_name = match kind { + Some(k) => k.kind_table(), + None => "dependencies", + }; + let table_in_toml = if let Some(platform) = &cx.platform { + format!("target.{}.{kind_name}", platform.to_string()) + } else { + kind_name.to_string() + }; + unused_dep_keys(name_in_toml, &table_in_toml, v.unused_keys(), cx.warnings); + cx.deps.push(dep); + deps.insert(n.to_string(), MaybeWorkspace::Defined(resolved.clone())); + } + Ok(Some(deps)) } - } - fn resolve_with_self<'a>( - self, - label: &str, - get_ws_inheritable: impl FnOnce(&W) -> CargoResult, - ) -> CargoResult { - match self { - MaybeWorkspace::Defined(value) => Ok(value), - MaybeWorkspace::Workspace(w) => get_ws_inheritable(&w).with_context(|| { - format!( - "error inheriting `{label}` from workspace root manifest's `workspace.{}.{label}`", - w.inherit_toml_table(), - ) - }), + // Collect the dependencies. + let dependencies = process_dependencies( + &mut cx, + me.dependencies.as_ref(), + None, + &workspace_config, + &inherit_cell, + )?; + if me.dev_dependencies.is_some() && me.dev_dependencies2.is_some() { + warn_on_deprecated("dev-dependencies", package_name, "package", cx.warnings); } - } - - fn as_defined(&self) -> Option<&T> { - match self { - MaybeWorkspace::Workspace(_) => None, - MaybeWorkspace::Defined(defined) => Some(defined), + let dev_deps = me + .dev_dependencies + .as_ref() + .or_else(|| me.dev_dependencies2.as_ref()); + let dev_deps = process_dependencies( + &mut cx, + dev_deps, + Some(DepKind::Development), + &workspace_config, + &inherit_cell, + )?; + if me.build_dependencies.is_some() && me.build_dependencies2.is_some() { + warn_on_deprecated("build-dependencies", package_name, "package", cx.warnings); } - } -} - -type MaybeWorkspaceDependency = MaybeWorkspace; + let build_deps = me + .build_dependencies + .as_ref() + .or_else(|| me.build_dependencies2.as_ref()); + let build_deps = process_dependencies( + &mut cx, + build_deps, + Some(DepKind::Build), + &workspace_config, + &inherit_cell, + )?; -impl<'de> de::Deserialize<'de> for MaybeWorkspaceDependency { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let value = serde_value::Value::deserialize(deserializer)?; + let lints = me + .lints + .clone() + .map(|mw| mw.resolve(|| inherit()?.lints())) + .transpose()?; + let lints = verify_lints(lints)?; + let default = TomlLints::default(); + let rustflags = lints_to_rustflags(lints.as_ref().unwrap_or(&default)); - if let Ok(w) = TomlWorkspaceDependency::deserialize(serde_value::ValueDeserializer::< - D::Error, - >::new(value.clone())) - { - return if w.workspace() { - Ok(MaybeWorkspace::Workspace(w)) - } else { - Err(de::Error::custom("`workspace` cannot be false")) - }; - } - TomlDependency::deserialize(serde_value::ValueDeserializer::::new(value)) - .map(MaybeWorkspace::Defined) - } -} - -impl MaybeWorkspaceDependency { - fn unused_keys(&self) -> Vec { - match self { - MaybeWorkspaceDependency::Defined(d) => d.unused_keys(), - MaybeWorkspaceDependency::Workspace(w) => w.other.keys().cloned().collect(), - } - } -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -pub struct TomlWorkspaceDependency { - workspace: bool, - features: Option>, - default_features: Option, - #[serde(rename = "default_features")] - default_features2: Option, - optional: Option, - /// This is here to provide a way to see the "unused manifest keys" when deserializing - #[serde(skip_serializing)] - #[serde(flatten)] - other: BTreeMap, -} - -impl WorkspaceInherit for TomlWorkspaceDependency { - fn inherit_toml_table(&self) -> &str { - "dependencies" - } - - fn workspace(&self) -> bool { - self.workspace - } -} - -impl TomlWorkspaceDependency { - fn resolve<'a>( - &self, - name: &str, - inheritable: impl FnOnce() -> CargoResult<&'a InheritableFields>, - cx: &mut Context<'_, '_>, - ) -> CargoResult { - fn default_features_msg(label: &str, ws_def_feat: Option, cx: &mut Context<'_, '_>) { - let ws_def_feat = match ws_def_feat { - Some(true) => "true", - Some(false) => "false", - None => "not specified", + let mut target: BTreeMap = BTreeMap::new(); + for (name, platform) in me.target.iter().flatten() { + cx.platform = { + let platform: Platform = name.parse()?; + platform.check_cfg_attributes(cx.warnings); + Some(platform) }; - cx.warnings.push(format!( - "`default-features` is ignored for {label}, since `default-features` was \ - {ws_def_feat} for `workspace.dependencies.{label}`, \ - this could become a hard error in the future" - )) - } - if self.default_features.is_some() && self.default_features2.is_some() { - warn_on_deprecated("default-features", name, "dependency", cx.warnings); - } - inheritable()?.get_dependency(name, cx.root).map(|d| { - match d { - TomlDependency::Simple(s) => { - if let Some(false) = self.default_features.or(self.default_features2) { - default_features_msg(name, None, cx); - } - if self.optional.is_some() || self.features.is_some() { - TomlDependency::Detailed(DetailedTomlDependency { - version: Some(s), - optional: self.optional, - features: self.features.clone(), - ..Default::default() - }) - } else { - TomlDependency::Simple(s) - } - } - TomlDependency::Detailed(d) => { - let mut d = d.clone(); - match ( - self.default_features.or(self.default_features2), - d.default_features.or(d.default_features2), - ) { - // member: default-features = true and - // workspace: default-features = false should turn on - // default-features - (Some(true), Some(false)) => { - d.default_features = Some(true); - } - // member: default-features = false and - // workspace: default-features = true should ignore member - // default-features - (Some(false), Some(true)) => { - default_features_msg(name, Some(true), cx); - } - // member: default-features = false and - // workspace: dep = "1.0" should ignore member default-features - (Some(false), None) => { - default_features_msg(name, None, cx); - } - _ => {} - } - d.add_features(self.features.clone()); - d.update_optional(self.optional); - TomlDependency::Detailed(d) - } + let deps = process_dependencies( + &mut cx, + platform.dependencies.as_ref(), + None, + &workspace_config, + &inherit_cell, + )?; + if platform.build_dependencies.is_some() && platform.build_dependencies2.is_some() { + warn_on_deprecated("build-dependencies", name, "platform target", cx.warnings); } - }) - } -} - -//. This already has a `Deserialize` impl from version_trim_whitespace -type MaybeWorkspaceSemverVersion = MaybeWorkspace; - -type MaybeWorkspaceString = MaybeWorkspace; -impl<'de> de::Deserialize<'de> for MaybeWorkspaceString { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - struct Visitor; - - impl<'de> de::Visitor<'de> for Visitor { - type Value = MaybeWorkspaceString; - - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - f.write_str("a string or workspace") + let build_deps = platform + .build_dependencies + .as_ref() + .or_else(|| platform.build_dependencies2.as_ref()); + let build_deps = process_dependencies( + &mut cx, + build_deps, + Some(DepKind::Build), + &workspace_config, + &inherit_cell, + )?; + if platform.dev_dependencies.is_some() && platform.dev_dependencies2.is_some() { + warn_on_deprecated("dev-dependencies", name, "platform target", cx.warnings); } + let dev_deps = platform + .dev_dependencies + .as_ref() + .or_else(|| platform.dev_dependencies2.as_ref()); + let dev_deps = process_dependencies( + &mut cx, + dev_deps, + Some(DepKind::Development), + &workspace_config, + &inherit_cell, + )?; + target.insert( + name.clone(), + TomlPlatform { + dependencies: deps, + build_dependencies: build_deps, + build_dependencies2: None, + dev_dependencies: dev_deps, + dev_dependencies2: None, + }, + ); + } - fn visit_string(self, value: String) -> Result - where - E: de::Error, - { - Ok(MaybeWorkspaceString::Defined(value)) - } + let target = if target.is_empty() { + None + } else { + Some(target) + }; + let replace = me.replace(&mut cx)?; + let patch = me.patch(&mut cx)?; - fn visit_map(self, map: V) -> Result - where - V: de::MapAccess<'de>, - { - let mvd = de::value::MapAccessDeserializer::new(map); - TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) + { + let mut names_sources = BTreeMap::new(); + for dep in &deps { + let name = dep.name_in_toml(); + let prev = names_sources.insert(name, dep.source_id()); + if prev.is_some() && prev != Some(dep.source_id()) { + bail!( + "Dependency '{}' has different source paths depending on the build \ + target. Each dependency must have a single canonical source path \ + irrespective of build target.", + name + ); + } } } - d.deserialize_any(Visitor) - } -} - -type MaybeWorkspaceRustVersion = MaybeWorkspace; -impl<'de> de::Deserialize<'de> for MaybeWorkspaceRustVersion { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - struct Visitor; - - impl<'de> de::Visitor<'de> for Visitor { - type Value = MaybeWorkspaceRustVersion; + let exclude = package + .exclude + .clone() + .map(|mw| mw.resolve("exclude", || inherit()?.exclude())) + .transpose()? + .unwrap_or_default(); + let include = package + .include + .clone() + .map(|mw| mw.resolve("include", || inherit()?.include())) + .transpose()? + .unwrap_or_default(); + let empty_features = BTreeMap::new(); - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - f.write_str("a semver or workspace") - } + let summary = Summary::new( + pkgid, + deps, + me.features.as_ref().unwrap_or(&empty_features), + package.links.as_deref(), + rust_version.clone(), + )?; - fn visit_string(self, value: String) -> Result - where - E: de::Error, - { - let value = value.parse::().map_err(|e| E::custom(e))?; - Ok(MaybeWorkspaceRustVersion::Defined(value)) - } - - fn visit_map(self, map: V) -> Result - where - V: de::MapAccess<'de>, - { - let mvd = de::value::MapAccessDeserializer::new(map); - TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) - } - } - - d.deserialize_any(Visitor) - } -} - -type MaybeWorkspaceVecString = MaybeWorkspace, TomlWorkspaceField>; -impl<'de> de::Deserialize<'de> for MaybeWorkspaceVecString { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - struct Visitor; - - impl<'de> de::Visitor<'de> for Visitor { - type Value = MaybeWorkspaceVecString; - - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - f.write_str("a vector of strings or workspace") - } - fn visit_seq(self, v: A) -> Result - where - A: de::SeqAccess<'de>, - { - let seq = de::value::SeqAccessDeserializer::new(v); - Vec::deserialize(seq).map(MaybeWorkspace::Defined) - } - - fn visit_map(self, map: V) -> Result - where - V: de::MapAccess<'de>, - { - let mvd = de::value::MapAccessDeserializer::new(map); - TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) - } - } - - d.deserialize_any(Visitor) - } -} - -type MaybeWorkspaceStringOrBool = MaybeWorkspace; -impl<'de> de::Deserialize<'de> for MaybeWorkspaceStringOrBool { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - struct Visitor; - - impl<'de> de::Visitor<'de> for Visitor { - type Value = MaybeWorkspaceStringOrBool; - - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - f.write_str("a string, a bool, or workspace") - } - - fn visit_bool(self, v: bool) -> Result - where - E: de::Error, - { - let b = de::value::BoolDeserializer::new(v); - StringOrBool::deserialize(b).map(MaybeWorkspace::Defined) - } - - fn visit_string(self, v: String) -> Result - where - E: de::Error, - { - let string = de::value::StringDeserializer::new(v); - StringOrBool::deserialize(string).map(MaybeWorkspace::Defined) - } - - fn visit_map(self, map: V) -> Result - where - V: de::MapAccess<'de>, - { - let mvd = de::value::MapAccessDeserializer::new(map); - TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) - } - } - - d.deserialize_any(Visitor) - } -} - -type MaybeWorkspaceVecStringOrBool = MaybeWorkspace; -impl<'de> de::Deserialize<'de> for MaybeWorkspaceVecStringOrBool { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - struct Visitor; - - impl<'de> de::Visitor<'de> for Visitor { - type Value = MaybeWorkspaceVecStringOrBool; - - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - f.write_str("a boolean, a vector of strings, or workspace") - } - - fn visit_bool(self, v: bool) -> Result - where - E: de::Error, - { - let b = de::value::BoolDeserializer::new(v); - VecStringOrBool::deserialize(b).map(MaybeWorkspace::Defined) - } - - fn visit_seq(self, v: A) -> Result - where - A: de::SeqAccess<'de>, - { - let seq = de::value::SeqAccessDeserializer::new(v); - VecStringOrBool::deserialize(seq).map(MaybeWorkspace::Defined) - } - - fn visit_map(self, map: V) -> Result - where - V: de::MapAccess<'de>, - { - let mvd = de::value::MapAccessDeserializer::new(map); - TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) - } - } - - d.deserialize_any(Visitor) - } -} - -type MaybeWorkspaceBtreeMap = - MaybeWorkspace>, TomlWorkspaceField>; - -impl<'de> de::Deserialize<'de> for MaybeWorkspaceBtreeMap { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let value = serde_value::Value::deserialize(deserializer)?; - - if let Ok(w) = TomlWorkspaceField::deserialize( - serde_value::ValueDeserializer::::new(value.clone()), - ) { - return if w.workspace() { - Ok(MaybeWorkspace::Workspace(w)) - } else { - Err(de::Error::custom("`workspace` cannot be false")) - }; - } - BTreeMap::deserialize(serde_value::ValueDeserializer::::new(value)) - .map(MaybeWorkspace::Defined) - } -} - -#[derive(Deserialize, Serialize, Copy, Clone, Debug)] -pub struct TomlWorkspaceField { - #[serde(deserialize_with = "bool_no_false")] - workspace: bool, -} - -fn bool_no_false<'de, D: de::Deserializer<'de>>(deserializer: D) -> Result { - let b: bool = Deserialize::deserialize(deserializer)?; - if b { - Ok(b) - } else { - Err(de::Error::custom("`workspace` cannot be false")) - } -} - -impl WorkspaceInherit for TomlWorkspaceField { - fn inherit_toml_table(&self) -> &str { - "package" - } - - fn workspace(&self) -> bool { - self.workspace - } -} - -/// Represents the `package`/`project` sections of a `Cargo.toml`. -/// -/// Note that the order of the fields matters, since this is the order they -/// are serialized to a TOML file. For example, you cannot have values after -/// the field `metadata`, since it is a table and values cannot appear after -/// tables. -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -pub struct TomlPackage { - edition: Option, - rust_version: Option, - name: InternedString, - #[serde(deserialize_with = "version_trim_whitespace")] - version: MaybeWorkspaceSemverVersion, - authors: Option, - build: Option, - metabuild: Option, - default_target: Option, - forced_target: Option, - links: Option, - exclude: Option, - include: Option, - publish: Option, - workspace: Option, - im_a_teapot: Option, - autobins: Option, - autoexamples: Option, - autotests: Option, - autobenches: Option, - default_run: Option, - - // Package metadata. - description: Option, - homepage: Option, - documentation: Option, - readme: Option, - keywords: Option, - categories: Option, - license: Option, - license_file: Option, - repository: Option, - resolver: Option, - - // Provide a helpful error message for a common user error. - #[serde(rename = "cargo-features", skip_serializing)] - _invalid_cargo_features: Option, - - // Note that this field must come last due to the way toml serialization - // works which requires tables to be emitted after all values. - metadata: Option, -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct TomlWorkspace { - members: Option>, - #[serde(rename = "default-members")] - default_members: Option>, - exclude: Option>, - resolver: Option, - - // Properties that can be inherited by members. - package: Option, - dependencies: Option>, - lints: Option, - - // Note that this field must come last due to the way toml serialization - // works which requires tables to be emitted after all values. - metadata: Option, -} - -/// A group of fields that are inheritable by members of the workspace -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub struct InheritableFields { - // We use skip here since it will never be present when deserializing - // and we don't want it present when serializing - #[serde(skip)] - dependencies: Option>, - #[serde(skip)] - lints: Option, - - version: Option, - authors: Option>, - description: Option, - homepage: Option, - documentation: Option, - readme: Option, - keywords: Option>, - categories: Option>, - license: Option, - #[serde(rename = "license-file")] - license_file: Option, - repository: Option, - publish: Option, - edition: Option, - badges: Option>>, - exclude: Option>, - include: Option>, - #[serde(rename = "rust-version")] - rust_version: Option, - // We use skip here since it will never be present when deserializing - // and we don't want it present when serializing - #[serde(skip)] - ws_root: PathBuf, -} - -/// Defines simple getter methods for inheritable fields. -macro_rules! inheritable_field_getter { - ( $(($key:literal, $field:ident -> $ret:ty),)* ) => ( - $( - #[doc = concat!("Gets the field `workspace.", $key, "`.")] - pub fn $field(&self) -> CargoResult<$ret> { - let Some(val) = &self.$field else { - bail!("`workspace.{}` was not defined", $key); - }; - Ok(val.clone()) - } - )* - ) -} - -impl InheritableFields { - inheritable_field_getter! { - // Please keep this list lexicographically ordered. - ("dependencies", dependencies -> BTreeMap), - ("lints", lints -> TomlLints), - ("package.authors", authors -> Vec), - ("package.badges", badges -> BTreeMap>), - ("package.categories", categories -> Vec), - ("package.description", description -> String), - ("package.documentation", documentation -> String), - ("package.edition", edition -> String), - ("package.exclude", exclude -> Vec), - ("package.homepage", homepage -> String), - ("package.include", include -> Vec), - ("package.keywords", keywords -> Vec), - ("package.license", license -> String), - ("package.publish", publish -> VecStringOrBool), - ("package.repository", repository -> String), - ("package.rust-version", rust_version -> RustVersion), - ("package.version", version -> semver::Version), - } - - /// Gets a workspace dependency with the `name`. - pub fn get_dependency(&self, name: &str, package_root: &Path) -> CargoResult { - let Some(deps) = &self.dependencies else { - bail!("`workspace.dependencies` was not defined"); - }; - let Some(dep) = deps.get(name) else { - bail!("`dependency.{name}` was not found in `workspace.dependencies`"); - }; - let mut dep = dep.clone(); - if let TomlDependency::Detailed(detailed) = &mut dep { - detailed.resolve_path(name, self.ws_root(), package_root)?; - } - Ok(dep) - } - - /// Gets the field `workspace.package.license-file`. - pub fn license_file(&self, package_root: &Path) -> CargoResult { - let Some(license_file) = &self.license_file else { - bail!("`workspace.package.license-file` was not defined"); - }; - resolve_relative_path("license-file", &self.ws_root, package_root, license_file) - } - - /// Gets the field `workspace.package.readme`. - pub fn readme(&self, package_root: &Path) -> CargoResult { - let Some(readme) = readme_for_package(self.ws_root.as_path(), self.readme.as_ref()) else { - bail!("`workspace.package.readme` was not defined"); + let metadata = ManifestMetadata { + description: package + .description + .clone() + .map(|mw| mw.resolve("description", || inherit()?.description())) + .transpose()?, + homepage: package + .homepage + .clone() + .map(|mw| mw.resolve("homepage", || inherit()?.homepage())) + .transpose()?, + documentation: package + .documentation + .clone() + .map(|mw| mw.resolve("documentation", || inherit()?.documentation())) + .transpose()?, + readme: readme_for_package( + package_root, + package + .readme + .clone() + .map(|mw| mw.resolve("readme", || inherit()?.readme(package_root))) + .transpose()? + .as_ref(), + ), + authors: package + .authors + .clone() + .map(|mw| mw.resolve("authors", || inherit()?.authors())) + .transpose()? + .unwrap_or_default(), + license: package + .license + .clone() + .map(|mw| mw.resolve("license", || inherit()?.license())) + .transpose()?, + license_file: package + .license_file + .clone() + .map(|mw| mw.resolve("license", || inherit()?.license_file(package_root))) + .transpose()?, + repository: package + .repository + .clone() + .map(|mw| mw.resolve("repository", || inherit()?.repository())) + .transpose()?, + keywords: package + .keywords + .clone() + .map(|mw| mw.resolve("keywords", || inherit()?.keywords())) + .transpose()? + .unwrap_or_default(), + categories: package + .categories + .clone() + .map(|mw| mw.resolve("categories", || inherit()?.categories())) + .transpose()? + .unwrap_or_default(), + badges: me + .badges + .clone() + .map(|mw| mw.resolve("badges", || inherit()?.badges())) + .transpose()? + .unwrap_or_default(), + links: package.links.clone(), + rust_version: package + .rust_version + .map(|mw| mw.resolve("rust-version", || inherit()?.rust_version())) + .transpose()?, }; - resolve_relative_path("readme", &self.ws_root, package_root, &readme) - .map(StringOrBool::String) - } - - pub fn ws_root(&self) -> &PathBuf { - &self.ws_root - } - - pub fn update_deps(&mut self, deps: Option>) { - self.dependencies = deps; - } - - pub fn update_lints(&mut self, lints: Option) { - self.lints = lints; - } - - pub fn update_ws_path(&mut self, ws_root: PathBuf) { - self.ws_root = ws_root; - } -} - -impl TomlPackage { - pub fn to_package_id( - &self, - source_id: SourceId, - version: semver::Version, - ) -> CargoResult { - PackageId::new(self.name, version, source_id) - } -} - -struct Context<'a, 'b> { - deps: &'a mut Vec, - source_id: SourceId, - nested_paths: &'a mut Vec, - config: &'b Config, - warnings: &'a mut Vec, - platform: Option, - root: &'a Path, - features: &'a Features, -} - -impl TomlManifest { - /// Prepares the manifest for publishing. - // - Path and git components of dependency specifications are removed. - // - License path is updated to point within the package. - pub fn prepare_for_publish( - &self, - ws: &Workspace<'_>, - package_root: &Path, - ) -> CargoResult { - let config = ws.config(); - let mut package = self - .package + package.description = metadata + .description + .clone() + .map(|description| MaybeWorkspace::Defined(description)); + package.homepage = metadata + .homepage + .clone() + .map(|homepage| MaybeWorkspace::Defined(homepage)); + package.documentation = metadata + .documentation + .clone() + .map(|documentation| MaybeWorkspace::Defined(documentation)); + package.readme = metadata + .readme + .clone() + .map(|readme| MaybeWorkspace::Defined(StringOrBool::String(readme))); + package.authors = package + .authors + .as_ref() + .map(|_| MaybeWorkspace::Defined(metadata.authors.clone())); + package.license = metadata + .license + .clone() + .map(|license| MaybeWorkspace::Defined(license)); + package.license_file = metadata + .license_file + .clone() + .map(|license_file| MaybeWorkspace::Defined(license_file)); + package.repository = metadata + .repository + .clone() + .map(|repository| MaybeWorkspace::Defined(repository)); + package.keywords = package + .keywords + .as_ref() + .map(|_| MaybeWorkspace::Defined(metadata.keywords.clone())); + package.categories = package + .categories + .as_ref() + .map(|_| MaybeWorkspace::Defined(metadata.categories.clone())); + package.rust_version = rust_version.clone().map(|rv| MaybeWorkspace::Defined(rv)); + package.exclude = package + .exclude .as_ref() - .or_else(|| self.project.as_ref()) - .unwrap() - .clone(); - package.workspace = None; - let current_resolver = package - .resolver + .map(|_| MaybeWorkspace::Defined(exclude.clone())); + package.include = package + .include .as_ref() - .map(|r| ResolveBehavior::from_manifest(r)) - .unwrap_or_else(|| { - package - .edition - .as_ref() - .and_then(|e| e.as_defined()) - .map(|e| Edition::from_str(e)) - .unwrap_or(Ok(Edition::Edition2015)) - .map(|e| e.default_resolve_behavior()) - })?; - if ws.resolve_behavior() != current_resolver { - // This ensures the published crate if built as a root (e.g. `cargo install`) will - // use the same resolver behavior it was tested with in the workspace. - // To avoid forcing a higher MSRV we don't explicitly set this if it would implicitly - // result in the same thing. - package.resolver = Some(ws.resolve_behavior().to_manifest()); + .map(|_| MaybeWorkspace::Defined(include.clone())); + + let profiles = me.profile.clone(); + if let Some(profiles) = &profiles { + let cli_unstable = config.cli_unstable(); + profiles.validate(cli_unstable, &features, &mut warnings)?; } - if let Some(license_file) = &package.license_file { - let license_file = license_file - .as_defined() - .context("license file should have been resolved before `prepare_for_publish()`")?; - let license_path = Path::new(&license_file); - let abs_license_path = paths::normalize_path(&package_root.join(license_path)); - if abs_license_path.strip_prefix(package_root).is_err() { - // This path points outside of the package root. `cargo package` - // will copy it into the root, so adjust the path to this location. - package.license_file = Some(MaybeWorkspace::Defined( - license_path - .file_name() - .unwrap() - .to_str() - .unwrap() - .to_string(), - )); - } + + let publish = package + .publish + .clone() + .map(|publish| publish.resolve("publish", || inherit()?.publish()).unwrap()); + + package.publish = publish.clone().map(|p| MaybeWorkspace::Defined(p)); + + let publish = match publish { + Some(VecStringOrBool::VecString(ref vecstring)) => Some(vecstring.clone()), + Some(VecStringOrBool::Bool(false)) => Some(vec![]), + None | Some(VecStringOrBool::Bool(true)) => None, + }; + + if summary.features().contains_key("default-features") { + warnings.push( + "`default-features = [\"..\"]` was found in [features]. \ + Did you mean to use `default = [\"..\"]`?" + .to_string(), + ) } - if let Some(readme) = &package.readme { - let readme = readme - .as_defined() - .context("readme should have been resolved before `prepare_for_publish()`")?; - match readme { - StringOrBool::String(readme) => { - let readme_path = Path::new(&readme); - let abs_readme_path = paths::normalize_path(&package_root.join(readme_path)); - if abs_readme_path.strip_prefix(package_root).is_err() { - // This path points outside of the package root. `cargo package` - // will copy it into the root, so adjust the path to this location. - package.readme = Some(MaybeWorkspace::Defined(StringOrBool::String( - readme_path - .file_name() - .unwrap() - .to_str() - .unwrap() - .to_string(), - ))); - } - } - StringOrBool::Bool(_) => {} + if let Some(run) = &package.default_run { + if !targets + .iter() + .filter(|t| t.is_bin()) + .any(|t| t.name() == run) + { + let suggestion = + util::closest_msg(run, targets.iter().filter(|t| t.is_bin()), |t| t.name()); + bail!("default-run target `{}` not found{}", run, suggestion); } } - let all = |_d: &TomlDependency| true; - return Ok(TomlManifest { - package: Some(package), + + let default_kind = package + .default_target + .as_ref() + .map(|t| CompileTarget::new(&*t)) + .transpose()? + .map(CompileKind::Target); + let forced_kind = package + .forced_target + .as_ref() + .map(|t| CompileTarget::new(&*t)) + .transpose()? + .map(CompileKind::Target); + let custom_metadata = package.metadata.clone(); + let resolved_toml = TomlManifest { + cargo_features: me.cargo_features.clone(), + package: Some(package.clone()), project: None, - profile: self.profile.clone(), - lib: self.lib.clone(), - bin: self.bin.clone(), - example: self.example.clone(), - test: self.test.clone(), - bench: self.bench.clone(), - dependencies: map_deps(config, self.dependencies.as_ref(), all)?, - dev_dependencies: map_deps( - config, - self.dev_dependencies - .as_ref() - .or_else(|| self.dev_dependencies2.as_ref()), - TomlDependency::is_version_specified, - )?, + profile: me.profile.clone(), + lib: me.lib.clone(), + bin: me.bin.clone(), + example: me.example.clone(), + test: me.test.clone(), + bench: me.bench.clone(), + dependencies, + dev_dependencies: dev_deps, dev_dependencies2: None, - build_dependencies: map_deps( - config, - self.build_dependencies - .as_ref() - .or_else(|| self.build_dependencies2.as_ref()), - all, - )?, + build_dependencies: build_deps, build_dependencies2: None, - features: self.features.clone(), - target: match self.target.as_ref().map(|target_map| { - target_map - .iter() - .map(|(k, v)| { - Ok(( - k.clone(), - TomlPlatform { - dependencies: map_deps(config, v.dependencies.as_ref(), all)?, - dev_dependencies: map_deps( - config, - v.dev_dependencies - .as_ref() - .or_else(|| v.dev_dependencies2.as_ref()), - TomlDependency::is_version_specified, - )?, - dev_dependencies2: None, - build_dependencies: map_deps( - config, - v.build_dependencies - .as_ref() - .or_else(|| v.build_dependencies2.as_ref()), - all, - )?, - build_dependencies2: None, - }, - )) - }) - .collect() - }) { - Some(Ok(v)) => Some(v), - Some(Err(e)) => return Err(e), - None => None, - }, - replace: None, - patch: None, - workspace: None, - badges: self.badges.clone(), - cargo_features: self.cargo_features.clone(), - lints: self.lints.clone(), - }); - - fn map_deps( - config: &Config, - deps: Option<&BTreeMap>, - filter: impl Fn(&TomlDependency) -> bool, - ) -> CargoResult>> { - let Some(deps) = deps else { return Ok(None) }; - let deps = deps - .iter() - .filter(|(_k, v)| { - if let MaybeWorkspace::Defined(def) = v { - filter(def) - } else { - false - } - }) - .map(|(k, v)| Ok((k.clone(), map_dependency(config, v)?))) - .collect::>>()?; - Ok(Some(deps)) + features: me.features.clone(), + target, + replace: me.replace.clone(), + patch: me.patch.clone(), + workspace: me.workspace.clone(), + badges: me + .badges + .as_ref() + .map(|_| MaybeWorkspace::Defined(metadata.badges.clone())), + lints: lints.map(|lints| MaybeWorkspaceLints { + workspace: false, + lints, + }), + }; + let mut manifest = Manifest::new( + summary, + default_kind, + forced_kind, + targets, + exclude, + include, + package.links.clone(), + metadata, + custom_metadata, + profiles, + publish, + replace, + patch, + workspace_config, + features, + edition, + rust_version, + package.im_a_teapot, + package.default_run.clone(), + Rc::new(resolved_toml), + package.metabuild.clone().map(|sov| sov.0), + resolve_behavior, + rustflags, + embedded, + ); + if package.license_file.is_some() && package.license.is_some() { + manifest.warnings_mut().add_warning( + "only one of `license` or `license-file` is necessary\n\ + `license` should be used if the package license can be expressed \ + with a standard SPDX expression.\n\ + `license-file` should be used if the package uses a non-standard license.\n\ + See https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields \ + for more information." + .to_string(), + ); } - - fn map_dependency( - config: &Config, - dep: &MaybeWorkspaceDependency, - ) -> CargoResult { - let dep = match dep { - MaybeWorkspace::Defined(TomlDependency::Detailed(d)) => { - let mut d = d.clone(); - // Path dependencies become crates.io deps. - d.path.take(); - // Same with git dependencies. - d.git.take(); - d.branch.take(); - d.tag.take(); - d.rev.take(); - // registry specifications are elaborated to the index URL - if let Some(registry) = d.registry.take() { - d.registry_index = Some(config.get_registry_index(®istry)?.to_string()); - } - Ok(d) - } - MaybeWorkspace::Defined(TomlDependency::Simple(s)) => Ok(DetailedTomlDependency { - version: Some(s.clone()), - ..Default::default() - }), - _ => unreachable!(), - }; - dep.map(TomlDependency::Detailed) - .map(MaybeWorkspace::Defined) + for warning in warnings { + manifest.warnings_mut().add_warning(warning); } + for error in errors { + manifest.warnings_mut().add_critical_warning(error); + } + + manifest.feature_gate()?; + + Ok((manifest, nested_paths)) } - pub fn to_real_manifest( + fn to_virtual_manifest( me: &Rc, - embedded: bool, source_id: SourceId, - package_root: &Path, + root: &Path, config: &Config, - ) -> CargoResult<(Manifest, Vec)> { - fn get_ws( - config: &Config, - resolved_path: &Path, - workspace_config: &WorkspaceConfig, - ) -> CargoResult { - match workspace_config { - WorkspaceConfig::Root(root) => Ok(root.inheritable().clone()), - WorkspaceConfig::Member { - root: Some(ref path_to_root), - } => { - let path = resolved_path - .parent() - .unwrap() - .join(path_to_root) - .join("Cargo.toml"); - let root_path = paths::normalize_path(&path); - inheritable_from_path(config, root_path) - } - WorkspaceConfig::Member { root: None } => { - match find_workspace_root(&resolved_path, config)? { - Some(path_to_root) => inheritable_from_path(config, path_to_root), - None => Err(anyhow!("failed to find a workspace root")), - } - } - } + ) -> CargoResult<(VirtualManifest, Vec)> { + if me.project.is_some() { + bail!("this virtual manifest specifies a [project] section, which is not allowed"); + } + if me.package.is_some() { + bail!("this virtual manifest specifies a [package] section, which is not allowed"); + } + if me.lib.is_some() { + bail!("this virtual manifest specifies a [lib] section, which is not allowed"); + } + if me.bin.is_some() { + bail!("this virtual manifest specifies a [[bin]] section, which is not allowed"); + } + if me.example.is_some() { + bail!("this virtual manifest specifies a [[example]] section, which is not allowed"); + } + if me.test.is_some() { + bail!("this virtual manifest specifies a [[test]] section, which is not allowed"); + } + if me.bench.is_some() { + bail!("this virtual manifest specifies a [[bench]] section, which is not allowed"); + } + if me.dependencies.is_some() { + bail!("this virtual manifest specifies a [dependencies] section, which is not allowed"); + } + if me.dev_dependencies.is_some() || me.dev_dependencies2.is_some() { + bail!("this virtual manifest specifies a [dev-dependencies] section, which is not allowed"); + } + if me.build_dependencies.is_some() || me.build_dependencies2.is_some() { + bail!("this virtual manifest specifies a [build-dependencies] section, which is not allowed"); + } + if me.features.is_some() { + bail!("this virtual manifest specifies a [features] section, which is not allowed"); + } + if me.target.is_some() { + bail!("this virtual manifest specifies a [target] section, which is not allowed"); + } + if me.badges.is_some() { + bail!("this virtual manifest specifies a [badges] section, which is not allowed"); } - if !package_root.is_dir() { - bail!( - "package root '{}' is not a directory", - package_root.display() - ); - }; - - let mut nested_paths = vec![]; - let mut warnings = vec![]; - let mut errors = vec![]; - - // Parse features first so they will be available when parsing other parts of the TOML. + let mut nested_paths = Vec::new(); + let mut warnings = Vec::new(); + let mut deps = Vec::new(); let empty = Vec::new(); let cargo_features = me.cargo_features.as_ref().unwrap_or(&empty); let features = Features::new(cargo_features, config, &mut warnings, source_id.is_path())?; - let mut package = match (&me.package, &me.project) { - (Some(_), Some(project)) => { - if source_id.is_path() { - config.shell().warn(format!( - "manifest at `{}` contains both `project` and `package`, \ - this could become a hard error in the future", - package_root.display() - ))?; - } - project.clone() - } - (Some(package), None) => package.clone(), - (None, Some(project)) => { - if source_id.is_path() { - config.shell().warn(format!( - "manifest at `{}` contains `[project]` instead of `[package]`, \ - this could become a hard error in the future", - package_root.display() - ))?; - } - project.clone() - } - (None, None) => bail!("no `package` section found"), + let (replace, patch) = { + let mut cx = Context { + deps: &mut deps, + source_id, + nested_paths: &mut nested_paths, + config, + warnings: &mut warnings, + platform: None, + features: &features, + root, + }; + (me.replace(&mut cx)?, me.patch(&mut cx)?) }; - - let workspace_config = match (me.workspace.as_ref(), package.workspace.as_ref()) { - (Some(toml_config), None) => { + let profiles = me.profile.clone(); + if let Some(profiles) = &profiles { + profiles.validate(config.cli_unstable(), &features, &mut warnings)?; + } + let resolve_behavior = me + .workspace + .as_ref() + .and_then(|ws| ws.resolver.as_deref()) + .map(|r| ResolveBehavior::from_manifest(r)) + .transpose()?; + let workspace_config = match me.workspace { + Some(ref toml_config) => { let mut inheritable = toml_config.package.clone().unwrap_or_default(); - inheritable.update_ws_path(package_root.to_path_buf()); + inheritable.update_ws_path(root.to_path_buf()); inheritable.update_deps(toml_config.dependencies.clone()); let lints = toml_config.lints.clone(); let lints = verify_lints(lints)?; inheritable.update_lints(lints); - if let Some(ws_deps) = &inheritable.dependencies { - for (name, dep) in ws_deps { + let ws_root_config = WorkspaceRootConfig::new( + root, + &toml_config.members, + &toml_config.default_members, + &toml_config.exclude, + &Some(inheritable), + &toml_config.metadata, + ); + config + .ws_roots + .borrow_mut() + .insert(root.to_path_buf(), ws_root_config.clone()); + WorkspaceConfig::Root(ws_root_config) + } + None => { + bail!("virtual manifests must be configured with [workspace]"); + } + }; + Ok(( + VirtualManifest::new( + replace, + patch, + workspace_config, + profiles, + features, + resolve_behavior, + ), + nested_paths, + )) + } + + fn replace(&self, cx: &mut Context<'_, '_>) -> CargoResult> { + if self.patch.is_some() && self.replace.is_some() { + bail!("cannot specify both [replace] and [patch]"); + } + let mut replace = Vec::new(); + for (spec, replacement) in self.replace.iter().flatten() { + let mut spec = PackageIdSpec::parse(spec).with_context(|| { + format!( + "replacements must specify a valid semver \ + version to replace, but `{}` does not", + spec + ) + })?; + if spec.url().is_none() { + spec.set_url(CRATES_IO_INDEX.parse().unwrap()); + } + + if replacement.is_version_specified() { + bail!( + "replacements cannot specify a version \ + requirement, but found one for `{}`", + spec + ); + } + + let mut dep = replacement.to_dependency(spec.name().as_str(), cx, None)?; + let version = spec.version().ok_or_else(|| { + anyhow!( + "replacements must specify a version \ + to replace, but `{}` does not", + spec + ) + })?; + unused_dep_keys( + dep.name_in_toml().as_str(), + "replace", + replacement.unused_keys(), + &mut cx.warnings, + ); + dep.set_version_req(OptVersionReq::exact(&version)) + .lock_version(&version); + replace.push((spec, dep)); + } + Ok(replace) + } + + fn patch(&self, cx: &mut Context<'_, '_>) -> CargoResult>> { + let mut patch = HashMap::new(); + for (toml_url, deps) in self.patch.iter().flatten() { + let url = match &toml_url[..] { + CRATES_IO_REGISTRY => CRATES_IO_INDEX.parse().unwrap(), + _ => cx + .config + .get_registry_index(toml_url) + .or_else(|_| toml_url.into_url()) + .with_context(|| { + format!( + "[patch] entry `{}` should be a URL or registry name", + toml_url + ) + })?, + }; + patch.insert( + url, + deps.iter() + .map(|(name, dep)| { unused_dep_keys( name, - "workspace.dependencies", + &format!("patch.{toml_url}",), dep.unused_keys(), - &mut warnings, + &mut cx.warnings, ); - } + dep.to_dependency(name, cx, None) + }) + .collect::>>()?, + ); + } + Ok(patch) + } + + /// Returns the path to the build script if one exists for this crate. + fn maybe_custom_build( + &self, + build: &Option, + package_root: &Path, + ) -> Option { + let build_rs = package_root.join("build.rs"); + match *build { + // Explicitly no build script. + Some(StringOrBool::Bool(false)) => None, + Some(StringOrBool::Bool(true)) => Some(build_rs), + Some(StringOrBool::String(ref s)) => Some(PathBuf::from(s)), + None => { + // If there is a `build.rs` file next to the `Cargo.toml`, assume it is + // a build script. + if build_rs.is_file() { + Some(build_rs) + } else { + None } - let ws_root_config = WorkspaceRootConfig::new( - package_root, - &toml_config.members, - &toml_config.default_members, - &toml_config.exclude, - &Some(inheritable), - &toml_config.metadata, - ); - config - .ws_roots - .borrow_mut() - .insert(package_root.to_path_buf(), ws_root_config.clone()); - WorkspaceConfig::Root(ws_root_config) } - (None, root) => WorkspaceConfig::Member { - root: root.cloned(), - }, - (Some(..), Some(..)) => bail!( - "cannot configure both `package.workspace` and \ - `[workspace]`, only one can be specified" - ), - }; - - let package_name = package.name.trim(); - if package_name.is_empty() { - bail!("package name cannot be an empty string") } + } - validate_package_name(package_name, "package name", "")?; - - let resolved_path = package_root.join("Cargo.toml"); - - let inherit_cell: LazyCell = LazyCell::new(); - let inherit = - || inherit_cell.try_borrow_with(|| get_ws(config, &resolved_path, &workspace_config)); + pub fn has_profiles(&self) -> bool { + self.profile.is_some() + } - let version = package - .version - .clone() - .resolve("version", || inherit()?.version())?; + pub fn features(&self) -> Option<&BTreeMap>> { + self.features.as_ref() + } +} - package.version = MaybeWorkspace::Defined(version.clone()); +struct Context<'a, 'b> { + deps: &'a mut Vec, + source_id: SourceId, + nested_paths: &'a mut Vec, + config: &'b Config, + warnings: &'a mut Vec, + platform: Option, + root: &'a Path, + features: &'a Features, +} - let pkgid = package.to_package_id(source_id, version)?; +fn verify_lints(lints: Option) -> CargoResult> { + let Some(lints) = lints else { + return Ok(None); + }; - let edition = if let Some(edition) = package.edition.clone() { - let edition: Edition = edition - .resolve("edition", || inherit()?.edition())? - .parse() - .with_context(|| "failed to parse the `edition` key")?; - package.edition = Some(MaybeWorkspace::Defined(edition.to_string())); - edition - } else { - Edition::Edition2015 - }; - // Add these lines if start a new unstable edition. - // ``` - // if edition == Edition::Edition20xx { - // features.require(Feature::edition20xx))?; - // } - // ``` - if !edition.is_stable() { - // Guard in case someone forgets to add .require() - return Err(util::errors::internal(format!( - "edition {} should be gated", - edition - ))); + for (tool, lints) in &lints { + let supported = ["rust", "clippy", "rustdoc"]; + if !supported.contains(&tool.as_str()) { + let supported = supported.join(", "); + anyhow::bail!("unsupported `{tool}` in `[lints]`, must be one of {supported}") } - - let rust_version = if let Some(rust_version) = &package.rust_version { - let rust_version = rust_version - .clone() - .resolve("rust_version", || inherit()?.rust_version())?; - let req = rust_version.caret_req(); - if let Some(first_version) = edition.first_version() { - let unsupported = - semver::Version::new(first_version.major, first_version.minor - 1, 9999); - if req.matches(&unsupported) { - bail!( - "rust-version {} is older than first version ({}) required by \ - the specified edition ({})", - rust_version, - first_version, - edition, + for name in lints.keys() { + if let Some((prefix, suffix)) = name.split_once("::") { + if tool == prefix { + anyhow::bail!( + "`lints.{tool}.{name}` is not valid lint name; try `lints.{prefix}.{suffix}`" + ) + } else if tool == "rust" && supported.contains(&prefix) { + anyhow::bail!( + "`lints.{tool}.{name}` is not valid lint name; try `lints.{prefix}.{suffix}`" ) + } else { + anyhow::bail!("`lints.{tool}.{name}` is not a valid lint name") } } - Some(rust_version) - } else { - None - }; + } + } - if package.metabuild.is_some() { - features.require(Feature::metabuild())?; + Ok(Some(lints)) +} + +fn lints_to_rustflags(lints: &TomlLints) -> Vec { + let mut rustflags = lints + .iter() + .flat_map(|(tool, lints)| { + lints.iter().map(move |(name, config)| { + let flag = config.level().flag(); + let option = if tool == "rust" { + format!("{flag}={name}") + } else { + format!("{flag}={tool}::{name}") + }; + ( + config.priority(), + // Since the most common group will be `all`, put it last so people are more + // likely to notice that they need to use `priority`. + std::cmp::Reverse(name), + option, + ) + }) + }) + .collect::>(); + rustflags.sort(); + rustflags.into_iter().map(|(_, _, option)| option).collect() +} + +fn unused_dep_keys( + dep_name: &str, + kind: &str, + unused_keys: Vec, + warnings: &mut Vec, +) { + for unused in unused_keys { + let key = format!("unused manifest key: {kind}.{dep_name}.{unused}"); + warnings.push(key); + } +} + +fn inheritable_from_path( + config: &Config, + workspace_path: PathBuf, +) -> CargoResult { + // Workspace path should have Cargo.toml at the end + let workspace_path_root = workspace_path.parent().unwrap(); + + // Let the borrow exit scope so that it can be picked up if there is a need to + // read a manifest + if let Some(ws_root) = config.ws_roots.borrow().get(workspace_path_root) { + return Ok(ws_root.inheritable().clone()); + }; + + let source_id = SourceId::for_path(workspace_path_root)?; + let (man, _) = read_manifest(&workspace_path, source_id, config)?; + match man.workspace_config() { + WorkspaceConfig::Root(root) => { + config + .ws_roots + .borrow_mut() + .insert(workspace_path, root.clone()); + Ok(root.inheritable().clone()) } + _ => bail!( + "root of a workspace inferred but wasn't a root: {}", + workspace_path.display() + ), + } +} - let resolve_behavior = match ( - package.resolver.as_ref(), - me.workspace.as_ref().and_then(|ws| ws.resolver.as_ref()), - ) { - (None, None) => None, - (Some(s), None) | (None, Some(s)) => Some(ResolveBehavior::from_manifest(s)?), - (Some(_), Some(_)) => { - bail!("cannot specify `resolver` field in both `[workspace]` and `[package]`") - } - }; +/// Returns the name of the README file for a [`TomlPackage`]. +pub fn readme_for_package(package_root: &Path, readme: Option<&StringOrBool>) -> Option { + match &readme { + None => default_readme_from_package_root(package_root), + Some(value) => match value { + StringOrBool::Bool(false) => None, + StringOrBool::Bool(true) => Some("README.md".to_string()), + StringOrBool::String(v) => Some(v.clone()), + }, + } +} - // If we have no lib at all, use the inferred lib, if available. - // If we have a lib with a path, we're done. - // If we have a lib with no path, use the inferred lib or else the package name. - let targets = targets( - &features, - me, - package_name, - package_root, - edition, - &package.build, - &package.metabuild, - &mut warnings, - &mut errors, - )?; +const DEFAULT_README_FILES: [&str; 3] = ["README.md", "README.txt", "README"]; - if targets.is_empty() { - debug!("manifest has no build targets"); +/// Checks if a file with any of the default README file names exists in the package root. +/// If so, returns a `String` representing that name. +fn default_readme_from_package_root(package_root: &Path) -> Option { + for &readme_filename in DEFAULT_README_FILES.iter() { + if package_root.join(readme_filename).is_file() { + return Some(readme_filename.to_string()); } + } - if let Err(conflict_targets) = unique_build_targets(&targets, package_root) { - conflict_targets - .iter() - .for_each(|(target_path, conflicts)| { - warnings.push(format!( - "file `{}` found to be present in multiple \ - build targets:\n{}", - target_path.display().to_string(), - conflicts - .iter() - .map(|t| format!( - " * `{}` target `{}`", - t.kind().description(), - t.name(), - )) - .join("\n") - )); - }) - } + None +} - if let Some(links) = &package.links { - if !targets.iter().any(|t| t.is_custom_build()) { - bail!( - "package `{}` specifies that it links to `{}` but does not \ - have a custom build script", - pkgid, - links - ) - } +/// Checks a list of build targets, and ensures the target names are unique within a vector. +/// If not, the name of the offending build target is returned. +fn unique_build_targets( + targets: &[Target], + package_root: &Path, +) -> Result<(), HashMap>> { + let mut source_targets = HashMap::<_, Vec<_>>::new(); + for target in targets { + if let TargetSourcePath::Path(path) = target.src_path() { + let full = package_root.join(path); + source_targets.entry(full).or_default().push(target.clone()); } + } - let mut deps = Vec::new(); + let conflict_targets = source_targets + .into_iter() + .filter(|(_, targets)| targets.len() > 1) + .collect::>(); - let mut cx = Context { - deps: &mut deps, - source_id, - nested_paths: &mut nested_paths, - config, - warnings: &mut warnings, - features: &features, - platform: None, - root: package_root, - }; + if !conflict_targets.is_empty() { + return Err(conflict_targets); + } - fn process_dependencies( - cx: &mut Context<'_, '_>, - new_deps: Option<&BTreeMap>, - kind: Option, - workspace_config: &WorkspaceConfig, - inherit_cell: &LazyCell, - ) -> CargoResult>> { - let Some(dependencies) = new_deps else { - return Ok(None); - }; + Ok(()) +} - let inheritable = || { - inherit_cell.try_borrow_with(|| { - get_ws(cx.config, &cx.root.join("Cargo.toml"), &workspace_config) - }) - }; +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +pub struct TomlProfiles(BTreeMap); - let mut deps: BTreeMap = BTreeMap::new(); - for (n, v) in dependencies.iter() { - let resolved = v - .clone() - .resolve_with_self(n, |dep| dep.resolve(n, inheritable, cx))?; - let dep = resolved.to_dependency(n, cx, kind)?; - let name_in_toml = dep.name_in_toml().as_str(); - validate_package_name(name_in_toml, "dependency name", "")?; - let kind_name = match kind { - Some(k) => k.kind_table(), - None => "dependencies", - }; - let table_in_toml = if let Some(platform) = &cx.platform { - format!("target.{}.{kind_name}", platform.to_string()) - } else { - kind_name.to_string() - }; - unused_dep_keys(name_in_toml, &table_in_toml, v.unused_keys(), cx.warnings); - cx.deps.push(dep); - deps.insert(n.to_string(), MaybeWorkspace::Defined(resolved.clone())); - } - Ok(Some(deps)) - } +impl TomlProfiles { + pub fn get_all(&self) -> &BTreeMap { + &self.0 + } - // Collect the dependencies. - let dependencies = process_dependencies( - &mut cx, - me.dependencies.as_ref(), - None, - &workspace_config, - &inherit_cell, - )?; - if me.dev_dependencies.is_some() && me.dev_dependencies2.is_some() { - warn_on_deprecated("dev-dependencies", package_name, "package", cx.warnings); - } - let dev_deps = me - .dev_dependencies - .as_ref() - .or_else(|| me.dev_dependencies2.as_ref()); - let dev_deps = process_dependencies( - &mut cx, - dev_deps, - Some(DepKind::Development), - &workspace_config, - &inherit_cell, - )?; - if me.build_dependencies.is_some() && me.build_dependencies2.is_some() { - warn_on_deprecated("build-dependencies", package_name, "package", cx.warnings); - } - let build_deps = me - .build_dependencies - .as_ref() - .or_else(|| me.build_dependencies2.as_ref()); - let build_deps = process_dependencies( - &mut cx, - build_deps, - Some(DepKind::Build), - &workspace_config, - &inherit_cell, - )?; + pub fn get(&self, name: &str) -> Option<&TomlProfile> { + self.0.get(name) + } - let lints = me - .lints - .clone() - .map(|mw| mw.resolve(|| inherit()?.lints())) - .transpose()?; - let lints = verify_lints(lints)?; - let default = TomlLints::default(); - let rustflags = lints_to_rustflags(lints.as_ref().unwrap_or(&default)); + /// Checks syntax validity and unstable feature gate for each profile. + /// + /// It's a bit unfortunate both `-Z` flags and `cargo-features` are required, + /// because profiles can now be set in either `Cargo.toml` or `config.toml`. + pub fn validate( + &self, + cli_unstable: &CliUnstable, + features: &Features, + warnings: &mut Vec, + ) -> CargoResult<()> { + for (name, profile) in &self.0 { + profile.validate(name, cli_unstable, features, warnings)?; + } + Ok(()) + } +} - let mut target: BTreeMap = BTreeMap::new(); - for (name, platform) in me.target.iter().flatten() { - cx.platform = { - let platform: Platform = name.parse()?; - platform.check_cfg_attributes(cx.warnings); - Some(platform) - }; - let deps = process_dependencies( - &mut cx, - platform.dependencies.as_ref(), - None, - &workspace_config, - &inherit_cell, - )?; - if platform.build_dependencies.is_some() && platform.build_dependencies2.is_some() { - warn_on_deprecated("build-dependencies", name, "platform target", cx.warnings); - } - let build_deps = platform - .build_dependencies - .as_ref() - .or_else(|| platform.build_dependencies2.as_ref()); - let build_deps = process_dependencies( - &mut cx, - build_deps, - Some(DepKind::Build), - &workspace_config, - &inherit_cell, - )?; - if platform.dev_dependencies.is_some() && platform.dev_dependencies2.is_some() { - warn_on_deprecated("dev-dependencies", name, "platform target", cx.warnings); +#[derive(Deserialize, Serialize, Clone, Debug, Default, Eq, PartialEq)] +#[serde(default, rename_all = "kebab-case")] +pub struct TomlProfile { + pub opt_level: Option, + pub lto: Option, + pub codegen_backend: Option, + pub codegen_units: Option, + pub debug: Option, + pub split_debuginfo: Option, + pub debug_assertions: Option, + pub rpath: Option, + pub panic: Option, + pub overflow_checks: Option, + pub incremental: Option, + pub dir_name: Option, + pub inherits: Option, + pub strip: Option, + // Note that `rustflags` is used for the cargo-feature `profile_rustflags` + pub rustflags: Option>, + // These two fields must be last because they are sub-tables, and TOML + // requires all non-tables to be listed first. + pub package: Option>, + pub build_override: Option>, +} + +impl TomlProfile { + /// Checks stytax validity and unstable feature gate for a given profile. + pub fn validate( + &self, + name: &str, + cli_unstable: &CliUnstable, + features: &Features, + warnings: &mut Vec, + ) -> CargoResult<()> { + self.validate_profile(name, cli_unstable, features)?; + if let Some(ref profile) = self.build_override { + profile.validate_override("build-override")?; + profile.validate_profile(&format!("{name}.build-override"), cli_unstable, features)?; + } + if let Some(ref packages) = self.package { + for (override_name, profile) in packages { + profile.validate_override("package")?; + profile.validate_profile( + &format!("{name}.package.{override_name}"), + cli_unstable, + features, + )?; } - let dev_deps = platform - .dev_dependencies - .as_ref() - .or_else(|| platform.dev_dependencies2.as_ref()); - let dev_deps = process_dependencies( - &mut cx, - dev_deps, - Some(DepKind::Development), - &workspace_config, - &inherit_cell, - )?; - target.insert( - name.clone(), - TomlPlatform { - dependencies: deps, - build_dependencies: build_deps, - build_dependencies2: None, - dev_dependencies: dev_deps, - dev_dependencies2: None, - }, + } + + // Profile name validation + Self::validate_name(name)?; + + if let Some(dir_name) = self.dir_name { + // This is disabled for now, as we would like to stabilize named + // profiles without this, and then decide in the future if it is + // needed. This helps simplify the UI a little. + bail!( + "dir-name=\"{}\" in profile `{}` is not currently allowed, \ + directory names are tied to the profile name for custom profiles", + dir_name, + name ); } - let target = if target.is_empty() { - None - } else { - Some(target) - }; - let replace = me.replace(&mut cx)?; - let patch = me.patch(&mut cx)?; + // `inherits` validation + if matches!(self.inherits.map(|s| s.as_str()), Some("debug")) { + bail!( + "profile.{}.inherits=\"debug\" should be profile.{}.inherits=\"dev\"", + name, + name + ); + } - { - let mut names_sources = BTreeMap::new(); - for dep in &deps { - let name = dep.name_in_toml(); - let prev = names_sources.insert(name, dep.source_id()); - if prev.is_some() && prev != Some(dep.source_id()) { - bail!( - "Dependency '{}' has different source paths depending on the build \ - target. Each dependency must have a single canonical source path \ - irrespective of build target.", - name - ); + match name { + "doc" => { + warnings.push("profile `doc` is deprecated and has no effect".to_string()); + } + "test" | "bench" => { + if self.panic.is_some() { + warnings.push(format!("`panic` setting is ignored for `{}` profile", name)) } } + _ => {} } - let exclude = package - .exclude - .clone() - .map(|mw| mw.resolve("exclude", || inherit()?.exclude())) - .transpose()? - .unwrap_or_default(); - let include = package - .include - .clone() - .map(|mw| mw.resolve("include", || inherit()?.include())) - .transpose()? - .unwrap_or_default(); - let empty_features = BTreeMap::new(); + if let Some(panic) = &self.panic { + if panic != "unwind" && panic != "abort" { + bail!( + "`panic` setting of `{}` is not a valid setting, \ + must be `unwind` or `abort`", + panic + ); + } + } - let summary = Summary::new( - pkgid, - deps, - me.features.as_ref().unwrap_or(&empty_features), - package.links.as_deref(), - rust_version.clone(), - )?; + if let Some(StringOrBool::String(arg)) = &self.lto { + if arg == "true" || arg == "false" { + bail!( + "`lto` setting of string `\"{arg}\"` for `{name}` profile is not \ + a valid setting, must be a boolean (`true`/`false`) or a string \ + (`\"thin\"`/`\"fat\"`/`\"off\"`) or omitted.", + ); + } + } - let metadata = ManifestMetadata { - description: package - .description - .clone() - .map(|mw| mw.resolve("description", || inherit()?.description())) - .transpose()?, - homepage: package - .homepage - .clone() - .map(|mw| mw.resolve("homepage", || inherit()?.homepage())) - .transpose()?, - documentation: package - .documentation - .clone() - .map(|mw| mw.resolve("documentation", || inherit()?.documentation())) - .transpose()?, - readme: readme_for_package( - package_root, - package - .readme - .clone() - .map(|mw| mw.resolve("readme", || inherit()?.readme(package_root))) - .transpose()? - .as_ref(), - ), - authors: package - .authors - .clone() - .map(|mw| mw.resolve("authors", || inherit()?.authors())) - .transpose()? - .unwrap_or_default(), - license: package - .license - .clone() - .map(|mw| mw.resolve("license", || inherit()?.license())) - .transpose()?, - license_file: package - .license_file - .clone() - .map(|mw| mw.resolve("license", || inherit()?.license_file(package_root))) - .transpose()?, - repository: package - .repository - .clone() - .map(|mw| mw.resolve("repository", || inherit()?.repository())) - .transpose()?, - keywords: package - .keywords - .clone() - .map(|mw| mw.resolve("keywords", || inherit()?.keywords())) - .transpose()? - .unwrap_or_default(), - categories: package - .categories - .clone() - .map(|mw| mw.resolve("categories", || inherit()?.categories())) - .transpose()? - .unwrap_or_default(), - badges: me - .badges - .clone() - .map(|mw| mw.resolve("badges", || inherit()?.badges())) - .transpose()? - .unwrap_or_default(), - links: package.links.clone(), - rust_version: package - .rust_version - .map(|mw| mw.resolve("rust-version", || inherit()?.rust_version())) - .transpose()?, - }; - package.description = metadata - .description - .clone() - .map(|description| MaybeWorkspace::Defined(description)); - package.homepage = metadata - .homepage - .clone() - .map(|homepage| MaybeWorkspace::Defined(homepage)); - package.documentation = metadata - .documentation - .clone() - .map(|documentation| MaybeWorkspace::Defined(documentation)); - package.readme = metadata - .readme - .clone() - .map(|readme| MaybeWorkspace::Defined(StringOrBool::String(readme))); - package.authors = package - .authors - .as_ref() - .map(|_| MaybeWorkspace::Defined(metadata.authors.clone())); - package.license = metadata - .license - .clone() - .map(|license| MaybeWorkspace::Defined(license)); - package.license_file = metadata - .license_file - .clone() - .map(|license_file| MaybeWorkspace::Defined(license_file)); - package.repository = metadata - .repository - .clone() - .map(|repository| MaybeWorkspace::Defined(repository)); - package.keywords = package - .keywords - .as_ref() - .map(|_| MaybeWorkspace::Defined(metadata.keywords.clone())); - package.categories = package - .categories - .as_ref() - .map(|_| MaybeWorkspace::Defined(metadata.categories.clone())); - package.rust_version = rust_version.clone().map(|rv| MaybeWorkspace::Defined(rv)); - package.exclude = package - .exclude - .as_ref() - .map(|_| MaybeWorkspace::Defined(exclude.clone())); - package.include = package - .include - .as_ref() - .map(|_| MaybeWorkspace::Defined(include.clone())); + Ok(()) + } + + /// Validate dir-names and profile names according to RFC 2678. + pub fn validate_name(name: &str) -> CargoResult<()> { + if let Some(ch) = name + .chars() + .find(|ch| !ch.is_alphanumeric() && *ch != '_' && *ch != '-') + { + bail!( + "invalid character `{}` in profile name `{}`\n\ + Allowed characters are letters, numbers, underscore, and hyphen.", + ch, + name + ); + } - let profiles = me.profile.clone(); - if let Some(profiles) = &profiles { - let cli_unstable = config.cli_unstable(); - profiles.validate(cli_unstable, &features, &mut warnings)?; + const SEE_DOCS: &str = "See https://doc.rust-lang.org/cargo/reference/profiles.html \ + for more on configuring profiles."; + + let lower_name = name.to_lowercase(); + if lower_name == "debug" { + bail!( + "profile name `{}` is reserved\n\ + To configure the default development profile, use the name `dev` \ + as in [profile.dev]\n\ + {}", + name, + SEE_DOCS + ); + } + if lower_name == "build-override" { + bail!( + "profile name `{}` is reserved\n\ + To configure build dependency settings, use [profile.dev.build-override] \ + and [profile.release.build-override]\n\ + {}", + name, + SEE_DOCS + ); } - let publish = package - .publish - .clone() - .map(|publish| publish.resolve("publish", || inherit()?.publish()).unwrap()); + // These are some arbitrary reservations. We have no plans to use + // these, but it seems safer to reserve a few just in case we want to + // add more built-in profiles in the future. We can also uses special + // syntax like cargo:foo if needed. But it is unlikely these will ever + // be used. + if matches!( + lower_name.as_str(), + "build" + | "check" + | "clean" + | "config" + | "fetch" + | "fix" + | "install" + | "metadata" + | "package" + | "publish" + | "report" + | "root" + | "run" + | "rust" + | "rustc" + | "rustdoc" + | "target" + | "tmp" + | "uninstall" + ) || lower_name.starts_with("cargo") + { + bail!( + "profile name `{}` is reserved\n\ + Please choose a different name.\n\ + {}", + name, + SEE_DOCS + ); + } - package.publish = publish.clone().map(|p| MaybeWorkspace::Defined(p)); + Ok(()) + } - let publish = match publish { - Some(VecStringOrBool::VecString(ref vecstring)) => Some(vecstring.clone()), - Some(VecStringOrBool::Bool(false)) => Some(vec![]), - None | Some(VecStringOrBool::Bool(true)) => None, - }; + /// Validates a profile. + /// + /// This is a shallow check, which is reused for the profile itself and any overrides. + fn validate_profile( + &self, + name: &str, + cli_unstable: &CliUnstable, + features: &Features, + ) -> CargoResult<()> { + if let Some(codegen_backend) = &self.codegen_backend { + match ( + features.require(Feature::codegen_backend()), + cli_unstable.codegen_backend, + ) { + (Err(e), false) => return Err(e), + _ => {} + } - if summary.features().contains_key("default-features") { - warnings.push( - "`default-features = [\"..\"]` was found in [features]. \ - Did you mean to use `default = [\"..\"]`?" - .to_string(), - ) + if codegen_backend.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_') { + bail!( + "`profile.{}.codegen-backend` setting of `{}` is not a valid backend name.", + name, + codegen_backend, + ); + } } - - if let Some(run) = &package.default_run { - if !targets - .iter() - .filter(|t| t.is_bin()) - .any(|t| t.name() == run) - { - let suggestion = - util::closest_msg(run, targets.iter().filter(|t| t.is_bin()), |t| t.name()); - bail!("default-run target `{}` not found{}", run, suggestion); + if self.rustflags.is_some() { + match ( + features.require(Feature::profile_rustflags()), + cli_unstable.profile_rustflags, + ) { + (Err(e), false) => return Err(e), + _ => {} } } + Ok(()) + } - let default_kind = package - .default_target - .as_ref() - .map(|t| CompileTarget::new(&*t)) - .transpose()? - .map(CompileKind::Target); - let forced_kind = package - .forced_target - .as_ref() - .map(|t| CompileTarget::new(&*t)) - .transpose()? - .map(CompileKind::Target); - let custom_metadata = package.metadata.clone(); - let resolved_toml = TomlManifest { - cargo_features: me.cargo_features.clone(), - package: Some(package.clone()), - project: None, - profile: me.profile.clone(), - lib: me.lib.clone(), - bin: me.bin.clone(), - example: me.example.clone(), - test: me.test.clone(), - bench: me.bench.clone(), - dependencies, - dev_dependencies: dev_deps, - dev_dependencies2: None, - build_dependencies: build_deps, - build_dependencies2: None, - features: me.features.clone(), - target, - replace: me.replace.clone(), - patch: me.patch.clone(), - workspace: me.workspace.clone(), - badges: me - .badges - .as_ref() - .map(|_| MaybeWorkspace::Defined(metadata.badges.clone())), - lints: lints.map(|lints| MaybeWorkspaceLints { - workspace: false, - lints, - }), - }; - let mut manifest = Manifest::new( - summary, - default_kind, - forced_kind, - targets, - exclude, - include, - package.links.clone(), - metadata, - custom_metadata, - profiles, - publish, - replace, - patch, - workspace_config, - features, - edition, - rust_version, - package.im_a_teapot, - package.default_run.clone(), - Rc::new(resolved_toml), - package.metabuild.clone().map(|sov| sov.0), - resolve_behavior, - rustflags, - embedded, - ); - if package.license_file.is_some() && package.license.is_some() { - manifest.warnings_mut().add_warning( - "only one of `license` or `license-file` is necessary\n\ - `license` should be used if the package license can be expressed \ - with a standard SPDX expression.\n\ - `license-file` should be used if the package uses a non-standard license.\n\ - See https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields \ - for more information." - .to_string(), - ); + /// Validation that is specific to an override. + fn validate_override(&self, which: &str) -> CargoResult<()> { + if self.package.is_some() { + bail!("package-specific profiles cannot be nested"); } - for warning in warnings { - manifest.warnings_mut().add_warning(warning); + if self.build_override.is_some() { + bail!("build-override profiles cannot be nested"); + } + if self.panic.is_some() { + bail!("`panic` may not be specified in a `{}` profile", which) + } + if self.lto.is_some() { + bail!("`lto` may not be specified in a `{}` profile", which) + } + if self.rpath.is_some() { + bail!("`rpath` may not be specified in a `{}` profile", which) + } + Ok(()) + } + + /// Overwrite self's values with the given profile. + pub fn merge(&mut self, profile: &TomlProfile) { + if let Some(v) = &profile.opt_level { + self.opt_level = Some(v.clone()); + } + + if let Some(v) = &profile.lto { + self.lto = Some(v.clone()); + } + + if let Some(v) = profile.codegen_backend { + self.codegen_backend = Some(v); + } + + if let Some(v) = profile.codegen_units { + self.codegen_units = Some(v); + } + + if let Some(v) = profile.debug { + self.debug = Some(v); + } + + if let Some(v) = profile.debug_assertions { + self.debug_assertions = Some(v); } - for error in errors { - manifest.warnings_mut().add_critical_warning(error); + + if let Some(v) = &profile.split_debuginfo { + self.split_debuginfo = Some(v.clone()); } - manifest.feature_gate()?; + if let Some(v) = profile.rpath { + self.rpath = Some(v); + } - Ok((manifest, nested_paths)) - } + if let Some(v) = &profile.panic { + self.panic = Some(v.clone()); + } - fn to_virtual_manifest( - me: &Rc, - source_id: SourceId, - root: &Path, - config: &Config, - ) -> CargoResult<(VirtualManifest, Vec)> { - if me.project.is_some() { - bail!("this virtual manifest specifies a [project] section, which is not allowed"); + if let Some(v) = profile.overflow_checks { + self.overflow_checks = Some(v); } - if me.package.is_some() { - bail!("this virtual manifest specifies a [package] section, which is not allowed"); + + if let Some(v) = profile.incremental { + self.incremental = Some(v); } - if me.lib.is_some() { - bail!("this virtual manifest specifies a [lib] section, which is not allowed"); + + if let Some(v) = &profile.rustflags { + self.rustflags = Some(v.clone()); } - if me.bin.is_some() { - bail!("this virtual manifest specifies a [[bin]] section, which is not allowed"); + + if let Some(other_package) = &profile.package { + match &mut self.package { + Some(self_package) => { + for (spec, other_pkg_profile) in other_package { + match self_package.get_mut(spec) { + Some(p) => p.merge(other_pkg_profile), + None => { + self_package.insert(spec.clone(), other_pkg_profile.clone()); + } + } + } + } + None => self.package = Some(other_package.clone()), + } } - if me.example.is_some() { - bail!("this virtual manifest specifies a [[example]] section, which is not allowed"); + + if let Some(other_bo) = &profile.build_override { + match &mut self.build_override { + Some(self_bo) => self_bo.merge(other_bo), + None => self.build_override = Some(other_bo.clone()), + } } - if me.test.is_some() { - bail!("this virtual manifest specifies a [[test]] section, which is not allowed"); + + if let Some(v) = &profile.inherits { + self.inherits = Some(*v); } - if me.bench.is_some() { - bail!("this virtual manifest specifies a [[bench]] section, which is not allowed"); + + if let Some(v) = &profile.dir_name { + self.dir_name = Some(*v); } - if me.dependencies.is_some() { - bail!("this virtual manifest specifies a [dependencies] section, which is not allowed"); + + if let Some(v) = &profile.strip { + self.strip = Some(v.clone()); } - if me.dev_dependencies.is_some() || me.dev_dependencies2.is_some() { - bail!("this virtual manifest specifies a [dev-dependencies] section, which is not allowed"); + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub enum ProfilePackageSpec { + Spec(PackageIdSpec), + All, +} + +impl fmt::Display for ProfilePackageSpec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProfilePackageSpec::Spec(spec) => spec.fmt(f), + ProfilePackageSpec::All => f.write_str("*"), } - if me.build_dependencies.is_some() || me.build_dependencies2.is_some() { - bail!("this virtual manifest specifies a [build-dependencies] section, which is not allowed"); + } +} + +impl ser::Serialize for ProfilePackageSpec { + fn serialize(&self, s: S) -> Result + where + S: ser::Serializer, + { + self.to_string().serialize(s) + } +} + +impl<'de> de::Deserialize<'de> for ProfilePackageSpec { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + let string = String::deserialize(d)?; + if string == "*" { + Ok(ProfilePackageSpec::All) + } else { + PackageIdSpec::parse(&string) + .map_err(de::Error::custom) + .map(ProfilePackageSpec::Spec) } - if me.features.is_some() { - bail!("this virtual manifest specifies a [features] section, which is not allowed"); + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TomlOptLevel(pub String); + +impl ser::Serialize for TomlOptLevel { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + match self.0.parse::() { + Ok(n) => n.serialize(serializer), + Err(_) => self.0.serialize(serializer), } - if me.target.is_some() { - bail!("this virtual manifest specifies a [target] section, which is not allowed"); + } +} + +impl<'de> de::Deserialize<'de> for TomlOptLevel { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + use serde::de::Error as _; + UntaggedEnumVisitor::new() + .expecting("an optimization level") + .i64(|value| Ok(TomlOptLevel(value.to_string()))) + .string(|value| { + if value == "s" || value == "z" { + Ok(TomlOptLevel(value.to_string())) + } else { + Err(serde_untagged::de::Error::custom(format!( + "must be `0`, `1`, `2`, `3`, `s` or `z`, \ + but found the string: \"{}\"", + value + ))) + } + }) + .deserialize(d) + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub enum TomlDebugInfo { + None, + LineDirectivesOnly, + LineTablesOnly, + Limited, + Full, +} + +impl Display for TomlDebugInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TomlDebugInfo::None => f.write_char('0'), + TomlDebugInfo::Limited => f.write_char('1'), + TomlDebugInfo::Full => f.write_char('2'), + TomlDebugInfo::LineDirectivesOnly => f.write_str("line-directives-only"), + TomlDebugInfo::LineTablesOnly => f.write_str("line-tables-only"), } - if me.badges.is_some() { - bail!("this virtual manifest specifies a [badges] section, which is not allowed"); + } +} + +impl ser::Serialize for TomlDebugInfo { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + match self { + Self::None => 0.serialize(serializer), + Self::LineDirectivesOnly => "line-directives-only".serialize(serializer), + Self::LineTablesOnly => "line-tables-only".serialize(serializer), + Self::Limited => 1.serialize(serializer), + Self::Full => 2.serialize(serializer), } + } +} - let mut nested_paths = Vec::new(); - let mut warnings = Vec::new(); - let mut deps = Vec::new(); - let empty = Vec::new(); - let cargo_features = me.cargo_features.as_ref().unwrap_or(&empty); - let features = Features::new(cargo_features, config, &mut warnings, source_id.is_path())?; +impl<'de> de::Deserialize<'de> for TomlDebugInfo { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + use serde::de::Error as _; + let expecting = "a boolean, 0, 1, 2, \"line-tables-only\", or \"line-directives-only\""; + UntaggedEnumVisitor::new() + .expecting(expecting) + .bool(|value| { + Ok(if value { + TomlDebugInfo::Full + } else { + TomlDebugInfo::None + }) + }) + .i64(|value| { + let debuginfo = match value { + 0 => TomlDebugInfo::None, + 1 => TomlDebugInfo::Limited, + 2 => TomlDebugInfo::Full, + _ => { + return Err(serde_untagged::de::Error::invalid_value( + Unexpected::Signed(value), + &expecting, + )) + } + }; + Ok(debuginfo) + }) + .string(|value| { + let debuginfo = match value { + "none" => TomlDebugInfo::None, + "limited" => TomlDebugInfo::Limited, + "full" => TomlDebugInfo::Full, + "line-directives-only" => TomlDebugInfo::LineDirectivesOnly, + "line-tables-only" => TomlDebugInfo::LineTablesOnly, + _ => { + return Err(serde_untagged::de::Error::invalid_value( + Unexpected::Str(value), + &expecting, + )) + } + }; + Ok(debuginfo) + }) + .deserialize(d) + } +} + +/// A StringOrVec can be parsed from either a TOML string or array, +/// but is always stored as a vector. +#[derive(Clone, Debug, Serialize, Eq, PartialEq, PartialOrd, Ord)] +pub struct StringOrVec(Vec); - let (replace, patch) = { - let mut cx = Context { - deps: &mut deps, - source_id, - nested_paths: &mut nested_paths, - config, - warnings: &mut warnings, - platform: None, - features: &features, - root, - }; - (me.replace(&mut cx)?, me.patch(&mut cx)?) - }; - let profiles = me.profile.clone(); - if let Some(profiles) = &profiles { - profiles.validate(config.cli_unstable(), &features, &mut warnings)?; - } - let resolve_behavior = me - .workspace - .as_ref() - .and_then(|ws| ws.resolver.as_deref()) - .map(|r| ResolveBehavior::from_manifest(r)) - .transpose()?; - let workspace_config = match me.workspace { - Some(ref toml_config) => { - let mut inheritable = toml_config.package.clone().unwrap_or_default(); - inheritable.update_ws_path(root.to_path_buf()); - inheritable.update_deps(toml_config.dependencies.clone()); - let lints = toml_config.lints.clone(); - let lints = verify_lints(lints)?; - inheritable.update_lints(lints); - let ws_root_config = WorkspaceRootConfig::new( - root, - &toml_config.members, - &toml_config.default_members, - &toml_config.exclude, - &Some(inheritable), - &toml_config.metadata, - ); - config - .ws_roots - .borrow_mut() - .insert(root.to_path_buf(), ws_root_config.clone()); - WorkspaceConfig::Root(ws_root_config) - } - None => { - bail!("virtual manifests must be configured with [workspace]"); - } - }; - Ok(( - VirtualManifest::new( - replace, - patch, - workspace_config, - profiles, - features, - resolve_behavior, - ), - nested_paths, - )) +impl StringOrVec { + pub fn iter<'a>(&'a self) -> std::slice::Iter<'a, String> { + self.0.iter() } +} - fn replace(&self, cx: &mut Context<'_, '_>) -> CargoResult> { - if self.patch.is_some() && self.replace.is_some() { - bail!("cannot specify both [replace] and [patch]"); - } - let mut replace = Vec::new(); - for (spec, replacement) in self.replace.iter().flatten() { - let mut spec = PackageIdSpec::parse(spec).with_context(|| { - format!( - "replacements must specify a valid semver \ - version to replace, but `{}` does not", - spec - ) - })?; - if spec.url().is_none() { - spec.set_url(CRATES_IO_INDEX.parse().unwrap()); - } +impl<'de> de::Deserialize<'de> for StringOrVec { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .expecting("string or list of strings") + .string(|value| Ok(StringOrVec(vec![value.to_owned()]))) + .seq(|value| value.deserialize().map(StringOrVec)) + .deserialize(deserializer) + } +} - if replacement.is_version_specified() { - bail!( - "replacements cannot specify a version \ - requirement, but found one for `{}`", - spec - ); - } +#[derive(Clone, Debug, Serialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum StringOrBool { + String(String), + Bool(bool), +} - let mut dep = replacement.to_dependency(spec.name().as_str(), cx, None)?; - let version = spec.version().ok_or_else(|| { - anyhow!( - "replacements must specify a version \ - to replace, but `{}` does not", - spec - ) - })?; - unused_dep_keys( - dep.name_in_toml().as_str(), - "replace", - replacement.unused_keys(), - &mut cx.warnings, - ); - dep.set_version_req(OptVersionReq::exact(&version)) - .lock_version(&version); - replace.push((spec, dep)); - } - Ok(replace) +impl<'de> Deserialize<'de> for StringOrBool { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .bool(|b| Ok(StringOrBool::Bool(b))) + .string(|s| Ok(StringOrBool::String(s.to_owned()))) + .deserialize(deserializer) } +} - fn patch(&self, cx: &mut Context<'_, '_>) -> CargoResult>> { - let mut patch = HashMap::new(); - for (toml_url, deps) in self.patch.iter().flatten() { - let url = match &toml_url[..] { - CRATES_IO_REGISTRY => CRATES_IO_INDEX.parse().unwrap(), - _ => cx - .config - .get_registry_index(toml_url) - .or_else(|_| toml_url.into_url()) - .with_context(|| { - format!( - "[patch] entry `{}` should be a URL or registry name", - toml_url - ) - })?, - }; - patch.insert( - url, - deps.iter() - .map(|(name, dep)| { - unused_dep_keys( - name, - &format!("patch.{toml_url}",), - dep.unused_keys(), - &mut cx.warnings, - ); - dep.to_dependency(name, cx, None) - }) - .collect::>>()?, - ); +#[derive(PartialEq, Clone, Debug, Serialize)] +#[serde(untagged)] +pub enum VecStringOrBool { + VecString(Vec), + Bool(bool), +} + +impl<'de> de::Deserialize<'de> for VecStringOrBool { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .expecting("a boolean or vector of strings") + .bool(|value| Ok(VecStringOrBool::Bool(value))) + .seq(|value| value.deserialize().map(VecStringOrBool::VecString)) + .deserialize(deserializer) + } +} + +/// This Trait exists to make [`MaybeWorkspace::Workspace`] generic. It makes deserialization of +/// [`MaybeWorkspace`] much easier, as well as making error messages for +/// [`MaybeWorkspace::resolve`] much nicer +/// +/// Implementors should have a field `workspace` with the type of `bool`. It is used to ensure +/// `workspace` is not `false` in a `Cargo.toml` +pub trait WorkspaceInherit { + /// This is the workspace table that is being inherited from. + /// For example `[workspace.dependencies]` would be the table "dependencies" + fn inherit_toml_table(&self) -> &str; + + /// This is used to output the value of the implementors `workspace` field + fn workspace(&self) -> bool; +} + +/// An enum that allows for inheriting keys from a workspace in a Cargo.toml. +#[derive(Serialize, Copy, Clone, Debug)] +#[serde(untagged)] +pub enum MaybeWorkspace { + /// The "defined" type, or the type that that is used when not inheriting from a workspace. + Defined(T), + /// The type when inheriting from a workspace. + Workspace(W), +} + +impl MaybeWorkspace { + fn resolve<'a>( + self, + label: &str, + get_ws_inheritable: impl FnOnce() -> CargoResult, + ) -> CargoResult { + match self { + MaybeWorkspace::Defined(value) => Ok(value), + MaybeWorkspace::Workspace(w) => get_ws_inheritable().with_context(|| { + format!( + "error inheriting `{label}` from workspace root manifest's `workspace.{}.{label}`", + w.inherit_toml_table(), + ) + }), } - Ok(patch) } - /// Returns the path to the build script if one exists for this crate. - fn maybe_custom_build( - &self, - build: &Option, - package_root: &Path, - ) -> Option { - let build_rs = package_root.join("build.rs"); - match *build { - // Explicitly no build script. - Some(StringOrBool::Bool(false)) => None, - Some(StringOrBool::Bool(true)) => Some(build_rs), - Some(StringOrBool::String(ref s)) => Some(PathBuf::from(s)), - None => { - // If there is a `build.rs` file next to the `Cargo.toml`, assume it is - // a build script. - if build_rs.is_file() { - Some(build_rs) - } else { - None - } - } + fn resolve_with_self<'a>( + self, + label: &str, + get_ws_inheritable: impl FnOnce(&W) -> CargoResult, + ) -> CargoResult { + match self { + MaybeWorkspace::Defined(value) => Ok(value), + MaybeWorkspace::Workspace(w) => get_ws_inheritable(&w).with_context(|| { + format!( + "error inheriting `{label}` from workspace root manifest's `workspace.{}.{label}`", + w.inherit_toml_table(), + ) + }), } } - pub fn has_profiles(&self) -> bool { - self.profile.is_some() + fn as_defined(&self) -> Option<&T> { + match self { + MaybeWorkspace::Workspace(_) => None, + MaybeWorkspace::Defined(defined) => Some(defined), + } } +} - pub fn features(&self) -> Option<&BTreeMap>> { - self.features.as_ref() +type MaybeWorkspaceDependency = MaybeWorkspace; + +impl MaybeWorkspaceDependency { + fn unused_keys(&self) -> Vec { + match self { + MaybeWorkspaceDependency::Defined(d) => d.unused_keys(), + MaybeWorkspaceDependency::Workspace(w) => w.other.keys().cloned().collect(), + } } } -fn verify_lints(lints: Option) -> CargoResult> { - let Some(lints) = lints else { - return Ok(None); - }; +impl<'de> de::Deserialize<'de> for MaybeWorkspaceDependency { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let value = serde_value::Value::deserialize(deserializer)?; - for (tool, lints) in &lints { - let supported = ["rust", "clippy", "rustdoc"]; - if !supported.contains(&tool.as_str()) { - let supported = supported.join(", "); - anyhow::bail!("unsupported `{tool}` in `[lints]`, must be one of {supported}") - } - for name in lints.keys() { - if let Some((prefix, suffix)) = name.split_once("::") { - if tool == prefix { - anyhow::bail!( - "`lints.{tool}.{name}` is not valid lint name; try `lints.{prefix}.{suffix}`" - ) - } else if tool == "rust" && supported.contains(&prefix) { - anyhow::bail!( - "`lints.{tool}.{name}` is not valid lint name; try `lints.{prefix}.{suffix}`" - ) - } else { - anyhow::bail!("`lints.{tool}.{name}` is not a valid lint name") - } - } + if let Ok(w) = TomlWorkspaceDependency::deserialize(serde_value::ValueDeserializer::< + D::Error, + >::new(value.clone())) + { + return if w.workspace() { + Ok(MaybeWorkspace::Workspace(w)) + } else { + Err(de::Error::custom("`workspace` cannot be false")) + }; } + TomlDependency::deserialize(serde_value::ValueDeserializer::::new(value)) + .map(MaybeWorkspace::Defined) } +} - Ok(Some(lints)) +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct TomlWorkspaceDependency { + workspace: bool, + features: Option>, + default_features: Option, + #[serde(rename = "default_features")] + default_features2: Option, + optional: Option, + /// This is here to provide a way to see the "unused manifest keys" when deserializing + #[serde(skip_serializing)] + #[serde(flatten)] + other: BTreeMap, } -fn lints_to_rustflags(lints: &TomlLints) -> Vec { - let mut rustflags = lints - .iter() - .flat_map(|(tool, lints)| { - lints.iter().map(move |(name, config)| { - let flag = config.level().flag(); - let option = if tool == "rust" { - format!("{flag}={name}") - } else { - format!("{flag}={tool}::{name}") - }; - ( - config.priority(), - // Since the most common group will be `all`, put it last so people are more - // likely to notice that they need to use `priority`. - std::cmp::Reverse(name), - option, - ) - }) +impl TomlWorkspaceDependency { + fn resolve<'a>( + &self, + name: &str, + inheritable: impl FnOnce() -> CargoResult<&'a InheritableFields>, + cx: &mut Context<'_, '_>, + ) -> CargoResult { + fn default_features_msg(label: &str, ws_def_feat: Option, cx: &mut Context<'_, '_>) { + let ws_def_feat = match ws_def_feat { + Some(true) => "true", + Some(false) => "false", + None => "not specified", + }; + cx.warnings.push(format!( + "`default-features` is ignored for {label}, since `default-features` was \ + {ws_def_feat} for `workspace.dependencies.{label}`, \ + this could become a hard error in the future" + )) + } + if self.default_features.is_some() && self.default_features2.is_some() { + warn_on_deprecated("default-features", name, "dependency", cx.warnings); + } + inheritable()?.get_dependency(name, cx.root).map(|d| { + match d { + TomlDependency::Simple(s) => { + if let Some(false) = self.default_features.or(self.default_features2) { + default_features_msg(name, None, cx); + } + if self.optional.is_some() || self.features.is_some() { + TomlDependency::Detailed(DetailedTomlDependency { + version: Some(s), + optional: self.optional, + features: self.features.clone(), + ..Default::default() + }) + } else { + TomlDependency::Simple(s) + } + } + TomlDependency::Detailed(d) => { + let mut d = d.clone(); + match ( + self.default_features.or(self.default_features2), + d.default_features.or(d.default_features2), + ) { + // member: default-features = true and + // workspace: default-features = false should turn on + // default-features + (Some(true), Some(false)) => { + d.default_features = Some(true); + } + // member: default-features = false and + // workspace: default-features = true should ignore member + // default-features + (Some(false), Some(true)) => { + default_features_msg(name, Some(true), cx); + } + // member: default-features = false and + // workspace: dep = "1.0" should ignore member default-features + (Some(false), None) => { + default_features_msg(name, None, cx); + } + _ => {} + } + d.add_features(self.features.clone()); + d.update_optional(self.optional); + TomlDependency::Detailed(d) + } + } }) - .collect::>(); - rustflags.sort(); - rustflags.into_iter().map(|(_, _, option)| option).collect() + } } -fn unused_dep_keys( - dep_name: &str, - kind: &str, - unused_keys: Vec, - warnings: &mut Vec, -) { - for unused in unused_keys { - let key = format!("unused manifest key: {kind}.{dep_name}.{unused}"); - warnings.push(key); +impl WorkspaceInherit for TomlWorkspaceDependency { + fn inherit_toml_table(&self) -> &str { + "dependencies" + } + + fn workspace(&self) -> bool { + self.workspace } } -fn inheritable_from_path( - config: &Config, - workspace_path: PathBuf, -) -> CargoResult { - // Workspace path should have Cargo.toml at the end - let workspace_path_root = workspace_path.parent().unwrap(); +//. This already has a `Deserialize` impl from version_trim_whitespace +type MaybeWorkspaceSemverVersion = MaybeWorkspace; - // Let the borrow exit scope so that it can be picked up if there is a need to - // read a manifest - if let Some(ws_root) = config.ws_roots.borrow().get(workspace_path_root) { - return Ok(ws_root.inheritable().clone()); - }; +type MaybeWorkspaceString = MaybeWorkspace; +impl<'de> de::Deserialize<'de> for MaybeWorkspaceString { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; - let source_id = SourceId::for_path(workspace_path_root)?; - let (man, _) = read_manifest(&workspace_path, source_id, config)?; - match man.workspace_config() { - WorkspaceConfig::Root(root) => { - config - .ws_roots - .borrow_mut() - .insert(workspace_path, root.clone()); - Ok(root.inheritable().clone()) + impl<'de> de::Visitor<'de> for Visitor { + type Value = MaybeWorkspaceString; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_str("a string or workspace") + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + Ok(MaybeWorkspaceString::Defined(value)) + } + + fn visit_map(self, map: V) -> Result + where + V: de::MapAccess<'de>, + { + let mvd = de::value::MapAccessDeserializer::new(map); + TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) + } } - _ => bail!( - "root of a workspace inferred but wasn't a root: {}", - workspace_path.display() - ), - } -} -/// Returns the name of the README file for a [`TomlPackage`]. -pub fn readme_for_package(package_root: &Path, readme: Option<&StringOrBool>) -> Option { - match &readme { - None => default_readme_from_package_root(package_root), - Some(value) => match value { - StringOrBool::Bool(false) => None, - StringOrBool::Bool(true) => Some("README.md".to_string()), - StringOrBool::String(v) => Some(v.clone()), - }, + d.deserialize_any(Visitor) } } -const DEFAULT_README_FILES: [&str; 3] = ["README.md", "README.txt", "README"]; +type MaybeWorkspaceRustVersion = MaybeWorkspace; +impl<'de> de::Deserialize<'de> for MaybeWorkspaceRustVersion { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = MaybeWorkspaceRustVersion; -/// Checks if a file with any of the default README file names exists in the package root. -/// If so, returns a `String` representing that name. -fn default_readme_from_package_root(package_root: &Path) -> Option { - for &readme_filename in DEFAULT_README_FILES.iter() { - if package_root.join(readme_filename).is_file() { - return Some(readme_filename.to_string()); - } - } + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_str("a semver or workspace") + } - None -} + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + let value = value.parse::().map_err(|e| E::custom(e))?; + Ok(MaybeWorkspaceRustVersion::Defined(value)) + } -/// Checks a list of build targets, and ensures the target names are unique within a vector. -/// If not, the name of the offending build target is returned. -fn unique_build_targets( - targets: &[Target], - package_root: &Path, -) -> Result<(), HashMap>> { - let mut source_targets = HashMap::<_, Vec<_>>::new(); - for target in targets { - if let TargetSourcePath::Path(path) = target.src_path() { - let full = package_root.join(path); - source_targets.entry(full).or_default().push(target.clone()); + fn visit_map(self, map: V) -> Result + where + V: de::MapAccess<'de>, + { + let mvd = de::value::MapAccessDeserializer::new(map); + TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) + } } - } - - let conflict_targets = source_targets - .into_iter() - .filter(|(_, targets)| targets.len() > 1) - .collect::>(); - if !conflict_targets.is_empty() { - return Err(conflict_targets); + d.deserialize_any(Visitor) } - - Ok(()) } -impl TomlDependency

{ - pub(crate) fn to_dependency_split( - &self, - name: &str, - source_id: SourceId, - nested_paths: &mut Vec, - config: &Config, - warnings: &mut Vec, - platform: Option, - root: &Path, - features: &Features, - kind: Option, - ) -> CargoResult { - self.to_dependency( - name, - &mut Context { - deps: &mut Vec::new(), - source_id, - nested_paths, - config, - warnings, - platform, - root, - features, - }, - kind, - ) - } +type MaybeWorkspaceVecString = MaybeWorkspace, TomlWorkspaceField>; +impl<'de> de::Deserialize<'de> for MaybeWorkspaceVecString { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; - fn to_dependency( - &self, - name: &str, - cx: &mut Context<'_, '_>, - kind: Option, - ) -> CargoResult { - match *self { - TomlDependency::Simple(ref version) => DetailedTomlDependency::

{ - version: Some(version.clone()), - ..Default::default() + impl<'de> de::Visitor<'de> for Visitor { + type Value = MaybeWorkspaceVecString; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.write_str("a vector of strings or workspace") + } + fn visit_seq(self, v: A) -> Result + where + A: de::SeqAccess<'de>, + { + let seq = de::value::SeqAccessDeserializer::new(v); + Vec::deserialize(seq).map(MaybeWorkspace::Defined) } - .to_dependency(name, cx, kind), - TomlDependency::Detailed(ref details) => details.to_dependency(name, cx, kind), - } - } - fn is_version_specified(&self) -> bool { - match self { - TomlDependency::Detailed(d) => d.version.is_some(), - TomlDependency::Simple(..) => true, + fn visit_map(self, map: V) -> Result + where + V: de::MapAccess<'de>, + { + let mvd = de::value::MapAccessDeserializer::new(map); + TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) + } } - } - fn is_optional(&self) -> bool { - match self { - TomlDependency::Detailed(d) => d.optional.unwrap_or(false), - TomlDependency::Simple(..) => false, - } + d.deserialize_any(Visitor) } } -impl DetailedTomlDependency

{ - fn to_dependency( - &self, - name_in_toml: &str, - cx: &mut Context<'_, '_>, - kind: Option, - ) -> CargoResult { - if self.version.is_none() && self.path.is_none() && self.git.is_none() { - let msg = format!( - "dependency ({}) specified without \ - providing a local path, Git repository, version, or \ - workspace dependency to use. This will be considered an \ - error in future versions", - name_in_toml - ); - cx.warnings.push(msg); - } +type MaybeWorkspaceStringOrBool = MaybeWorkspace; +impl<'de> de::Deserialize<'de> for MaybeWorkspaceStringOrBool { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; - if let Some(version) = &self.version { - if version.contains('+') { - cx.warnings.push(format!( - "version requirement `{}` for dependency `{}` \ - includes semver metadata which will be ignored, removing the \ - metadata is recommended to avoid confusion", - version, name_in_toml - )); - } - } + impl<'de> de::Visitor<'de> for Visitor { + type Value = MaybeWorkspaceStringOrBool; - if self.git.is_none() { - let git_only_keys = [ - (&self.branch, "branch"), - (&self.tag, "tag"), - (&self.rev, "rev"), - ]; + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.write_str("a string, a bool, or workspace") + } - for &(key, key_name) in &git_only_keys { - if key.is_some() { - bail!( - "key `{}` is ignored for dependency ({}).", - key_name, - name_in_toml - ); - } + fn visit_bool(self, v: bool) -> Result + where + E: de::Error, + { + let b = de::value::BoolDeserializer::new(v); + StringOrBool::deserialize(b).map(MaybeWorkspace::Defined) } - } - // Early detection of potentially misused feature syntax - // instead of generating a "feature not found" error. - if let Some(features) = &self.features { - for feature in features { - if feature.contains('/') { - bail!( - "feature `{}` in dependency `{}` is not allowed to contain slashes\n\ - If you want to enable features of a transitive dependency, \ - the direct dependency needs to re-export those features from \ - the `[features]` table.", - feature, - name_in_toml - ); - } - if feature.starts_with("dep:") { - bail!( - "feature `{}` in dependency `{}` is not allowed to use explicit \ - `dep:` syntax\n\ - If you want to enable an optional dependency, specify the name \ - of the optional dependency without the `dep:` prefix, or specify \ - a feature from the dependency's `[features]` table that enables \ - the optional dependency.", - feature, - name_in_toml - ); - } + fn visit_string(self, v: String) -> Result + where + E: de::Error, + { + let string = de::value::StringDeserializer::new(v); + StringOrBool::deserialize(string).map(MaybeWorkspace::Defined) } - } - let new_source_id = match ( - self.git.as_ref(), - self.path.as_ref(), - self.registry.as_ref(), - self.registry_index.as_ref(), - ) { - (Some(_), _, Some(_), _) | (Some(_), _, _, Some(_)) => bail!( - "dependency ({}) specification is ambiguous. \ - Only one of `git` or `registry` is allowed.", - name_in_toml - ), - (_, _, Some(_), Some(_)) => bail!( - "dependency ({}) specification is ambiguous. \ - Only one of `registry` or `registry-index` is allowed.", - name_in_toml - ), - (Some(git), maybe_path, _, _) => { - if maybe_path.is_some() { - bail!( - "dependency ({}) specification is ambiguous. \ - Only one of `git` or `path` is allowed.", - name_in_toml - ); - } + fn visit_map(self, map: V) -> Result + where + V: de::MapAccess<'de>, + { + let mvd = de::value::MapAccessDeserializer::new(map); + TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) + } + } - let n_details = [&self.branch, &self.tag, &self.rev] - .iter() - .filter(|d| d.is_some()) - .count(); + d.deserialize_any(Visitor) + } +} - if n_details > 1 { - bail!( - "dependency ({}) specification is ambiguous. \ - Only one of `branch`, `tag` or `rev` is allowed.", - name_in_toml - ); - } +type MaybeWorkspaceVecStringOrBool = MaybeWorkspace; +impl<'de> de::Deserialize<'de> for MaybeWorkspaceVecStringOrBool { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; - let reference = self - .branch - .clone() - .map(GitReference::Branch) - .or_else(|| self.tag.clone().map(GitReference::Tag)) - .or_else(|| self.rev.clone().map(GitReference::Rev)) - .unwrap_or(GitReference::DefaultBranch); - let loc = git.into_url()?; + impl<'de> de::Visitor<'de> for Visitor { + type Value = MaybeWorkspaceVecStringOrBool; - if let Some(fragment) = loc.fragment() { - let msg = format!( - "URL fragment `#{}` in git URL is ignored for dependency ({}). \ - If you were trying to specify a specific git revision, \ - use `rev = \"{}\"` in the dependency declaration.", - fragment, name_in_toml, fragment - ); - cx.warnings.push(msg) - } + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.write_str("a boolean, a vector of strings, or workspace") + } - SourceId::for_git(&loc, reference)? + fn visit_bool(self, v: bool) -> Result + where + E: de::Error, + { + let b = de::value::BoolDeserializer::new(v); + VecStringOrBool::deserialize(b).map(MaybeWorkspace::Defined) } - (None, Some(path), _, _) => { - let path = path.resolve(cx.config); - cx.nested_paths.push(path.clone()); - // If the source ID for the package we're parsing is a path - // source, then we normalize the path here to get rid of - // components like `..`. - // - // The purpose of this is to get a canonical ID for the package - // that we're depending on to ensure that builds of this package - // always end up hashing to the same value no matter where it's - // built from. - if cx.source_id.is_path() { - let path = cx.root.join(path); - let path = paths::normalize_path(&path); - SourceId::for_path(&path)? - } else { - cx.source_id - } + + fn visit_seq(self, v: A) -> Result + where + A: de::SeqAccess<'de>, + { + let seq = de::value::SeqAccessDeserializer::new(v); + VecStringOrBool::deserialize(seq).map(MaybeWorkspace::Defined) } - (None, None, Some(registry), None) => SourceId::alt_registry(cx.config, registry)?, - (None, None, None, Some(registry_index)) => { - let url = registry_index.into_url()?; - SourceId::for_registry(&url)? + + fn visit_map(self, map: V) -> Result + where + V: de::MapAccess<'de>, + { + let mvd = de::value::MapAccessDeserializer::new(map); + TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) } - (None, None, None, None) => SourceId::crates_io(cx.config)?, - }; + } - let (pkg_name, explicit_name_in_toml) = match self.package { - Some(ref s) => (&s[..], Some(name_in_toml)), - None => (name_in_toml, None), - }; + d.deserialize_any(Visitor) + } +} - let version = self.version.as_deref(); - let mut dep = Dependency::parse(pkg_name, version, new_source_id)?; - if self.default_features.is_some() && self.default_features2.is_some() { - warn_on_deprecated("default-features", name_in_toml, "dependency", cx.warnings); - } - dep.set_features(self.features.iter().flatten()) - .set_default_features( - self.default_features - .or(self.default_features2) - .unwrap_or(true), - ) - .set_optional(self.optional.unwrap_or(false)) - .set_platform(cx.platform.clone()); - if let Some(registry) = &self.registry { - let registry_id = SourceId::alt_registry(cx.config, registry)?; - dep.set_registry_id(registry_id); - } - if let Some(registry_index) = &self.registry_index { - let url = registry_index.into_url()?; - let registry_id = SourceId::for_registry(&url)?; - dep.set_registry_id(registry_id); +type MaybeWorkspaceBtreeMap = + MaybeWorkspace>, TomlWorkspaceField>; + +impl<'de> de::Deserialize<'de> for MaybeWorkspaceBtreeMap { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let value = serde_value::Value::deserialize(deserializer)?; + + if let Ok(w) = TomlWorkspaceField::deserialize( + serde_value::ValueDeserializer::::new(value.clone()), + ) { + return if w.workspace() { + Ok(MaybeWorkspace::Workspace(w)) + } else { + Err(de::Error::custom("`workspace` cannot be false")) + }; } + BTreeMap::deserialize(serde_value::ValueDeserializer::::new(value)) + .map(MaybeWorkspace::Defined) + } +} + +#[derive(Deserialize, Serialize, Copy, Clone, Debug)] +pub struct TomlWorkspaceField { + #[serde(deserialize_with = "bool_no_false")] + workspace: bool, +} + +impl WorkspaceInherit for TomlWorkspaceField { + fn inherit_toml_table(&self) -> &str { + "package" + } + + fn workspace(&self) -> bool { + self.workspace + } +} + +fn bool_no_false<'de, D: de::Deserializer<'de>>(deserializer: D) -> Result { + let b: bool = Deserialize::deserialize(deserializer)?; + if b { + Ok(b) + } else { + Err(de::Error::custom("`workspace` cannot be false")) + } +} + +/// Represents the `package`/`project` sections of a `Cargo.toml`. +/// +/// Note that the order of the fields matters, since this is the order they +/// are serialized to a TOML file. For example, you cannot have values after +/// the field `metadata`, since it is a table and values cannot appear after +/// tables. +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct TomlPackage { + edition: Option, + rust_version: Option, + name: InternedString, + #[serde(deserialize_with = "version_trim_whitespace")] + version: MaybeWorkspaceSemverVersion, + authors: Option, + build: Option, + metabuild: Option, + default_target: Option, + forced_target: Option, + links: Option, + exclude: Option, + include: Option, + publish: Option, + workspace: Option, + im_a_teapot: Option, + autobins: Option, + autoexamples: Option, + autotests: Option, + autobenches: Option, + default_run: Option, + + // Package metadata. + description: Option, + homepage: Option, + documentation: Option, + readme: Option, + keywords: Option, + categories: Option, + license: Option, + license_file: Option, + repository: Option, + resolver: Option, + + // Provide a helpful error message for a common user error. + #[serde(rename = "cargo-features", skip_serializing)] + _invalid_cargo_features: Option, + + // Note that this field must come last due to the way toml serialization + // works which requires tables to be emitted after all values. + metadata: Option, +} + +impl TomlPackage { + pub fn to_package_id( + &self, + source_id: SourceId, + version: semver::Version, + ) -> CargoResult { + PackageId::new(self.name, version, source_id) + } +} + +fn version_trim_whitespace<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + UntaggedEnumVisitor::new() + .expecting("SemVer version") + .string( + |value| match value.trim().parse().map_err(de::Error::custom) { + Ok(parsed) => Ok(MaybeWorkspace::Defined(parsed)), + Err(e) => Err(e), + }, + ) + .map(|value| value.deserialize().map(MaybeWorkspace::Workspace)) + .deserialize(deserializer) +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct TomlWorkspace { + members: Option>, + #[serde(rename = "default-members")] + default_members: Option>, + exclude: Option>, + resolver: Option, + + // Properties that can be inherited by members. + package: Option, + dependencies: Option>, + lints: Option, - if let Some(kind) = kind { - dep.set_kind(kind); - } - if let Some(name_in_toml) = explicit_name_in_toml { - dep.set_explicit_name_in_toml(name_in_toml); - } + // Note that this field must come last due to the way toml serialization + // works which requires tables to be emitted after all values. + metadata: Option, +} - if let Some(p) = self.public { - cx.features.require(Feature::public_dependency())?; +/// A group of fields that are inheritable by members of the workspace +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InheritableFields { + // We use skip here since it will never be present when deserializing + // and we don't want it present when serializing + #[serde(skip)] + dependencies: Option>, + #[serde(skip)] + lints: Option, - if dep.kind() != DepKind::Normal { - bail!("'public' specifier can only be used on regular dependencies, not {:?} dependencies", dep.kind()); + version: Option, + authors: Option>, + description: Option, + homepage: Option, + documentation: Option, + readme: Option, + keywords: Option>, + categories: Option>, + license: Option, + #[serde(rename = "license-file")] + license_file: Option, + repository: Option, + publish: Option, + edition: Option, + badges: Option>>, + exclude: Option>, + include: Option>, + #[serde(rename = "rust-version")] + rust_version: Option, + // We use skip here since it will never be present when deserializing + // and we don't want it present when serializing + #[serde(skip)] + ws_root: PathBuf, +} + +/// Defines simple getter methods for inheritable fields. +macro_rules! inheritable_field_getter { + ( $(($key:literal, $field:ident -> $ret:ty),)* ) => ( + $( + #[doc = concat!("Gets the field `workspace.", $key, "`.")] + pub fn $field(&self) -> CargoResult<$ret> { + let Some(val) = &self.$field else { + bail!("`workspace.{}` was not defined", $key); + }; + Ok(val.clone()) } + )* + ) +} - dep.set_public(p); - } +impl InheritableFields { + inheritable_field_getter! { + // Please keep this list lexicographically ordered. + ("dependencies", dependencies -> BTreeMap), + ("lints", lints -> TomlLints), + ("package.authors", authors -> Vec), + ("package.badges", badges -> BTreeMap>), + ("package.categories", categories -> Vec), + ("package.description", description -> String), + ("package.documentation", documentation -> String), + ("package.edition", edition -> String), + ("package.exclude", exclude -> Vec), + ("package.homepage", homepage -> String), + ("package.include", include -> Vec), + ("package.keywords", keywords -> Vec), + ("package.license", license -> String), + ("package.publish", publish -> VecStringOrBool), + ("package.repository", repository -> String), + ("package.rust-version", rust_version -> RustVersion), + ("package.version", version -> semver::Version), + } - if let (Some(artifact), is_lib, target) = ( - self.artifact.as_ref(), - self.lib.unwrap_or(false), - self.target.as_deref(), - ) { - if cx.config.cli_unstable().bindeps { - let artifact = Artifact::parse(&artifact.0, is_lib, target)?; - if dep.kind() != DepKind::Build - && artifact.target() == Some(ArtifactTarget::BuildDependencyAssumeTarget) - { - bail!( - r#"`target = "target"` in normal- or dev-dependencies has no effect ({})"#, - name_in_toml - ); - } - dep.set_artifact(artifact) - } else { - bail!("`artifact = …` requires `-Z bindeps` ({})", name_in_toml); - } - } else if self.lib.is_some() || self.target.is_some() { - for (is_set, specifier) in [ - (self.lib.is_some(), "lib"), - (self.target.is_some(), "target"), - ] { - if !is_set { - continue; - } - bail!( - "'{}' specifier cannot be used without an 'artifact = …' value ({})", - specifier, - name_in_toml - ) - } + /// Gets a workspace dependency with the `name`. + pub fn get_dependency(&self, name: &str, package_root: &Path) -> CargoResult { + let Some(deps) = &self.dependencies else { + bail!("`workspace.dependencies` was not defined"); + }; + let Some(dep) = deps.get(name) else { + bail!("`dependency.{name}` was not found in `workspace.dependencies`"); + }; + let mut dep = dep.clone(); + if let TomlDependency::Detailed(detailed) = &mut dep { + detailed.resolve_path(name, self.ws_root(), package_root)?; } Ok(dep) } -} -impl DetailedTomlDependency { - fn add_features(&mut self, features: Option>) { - self.features = match (self.features.clone(), features.clone()) { - (Some(dep_feat), Some(inherit_feat)) => Some( - dep_feat - .into_iter() - .chain(inherit_feat) - .collect::>(), - ), - (Some(dep_fet), None) => Some(dep_fet), - (None, Some(inherit_feat)) => Some(inherit_feat), - (None, None) => None, + /// Gets the field `workspace.package.license-file`. + pub fn license_file(&self, package_root: &Path) -> CargoResult { + let Some(license_file) = &self.license_file else { + bail!("`workspace.package.license-file` was not defined"); }; + resolve_relative_path("license-file", &self.ws_root, package_root, license_file) } - fn update_optional(&mut self, optional: Option) { - self.optional = optional; + /// Gets the field `workspace.package.readme`. + pub fn readme(&self, package_root: &Path) -> CargoResult { + let Some(readme) = readme_for_package(self.ws_root.as_path(), self.readme.as_ref()) else { + bail!("`workspace.package.readme` was not defined"); + }; + resolve_relative_path("readme", &self.ws_root, package_root, &readme) + .map(StringOrBool::String) } - fn resolve_path( - &mut self, - name: &str, - root_path: &Path, - package_root: &Path, - ) -> CargoResult<()> { - if let Some(rel_path) = &self.path { - self.path = Some(resolve_relative_path( - name, - root_path, - package_root, - rel_path, - )?) - } - Ok(()) + pub fn ws_root(&self) -> &PathBuf { + &self.ws_root + } + + pub fn update_deps(&mut self, deps: Option>) { + self.dependencies = deps; + } + + pub fn update_lints(&mut self, lints: Option) { + self.lints = lints; + } + + pub fn update_ws_path(&mut self, ws_root: PathBuf) { + self.ws_root = ws_root; } } @@ -3252,41 +3252,6 @@ struct TomlTarget { edition: Option, } -#[derive(Clone)] -struct PathValue(PathBuf); - -impl ser::Serialize for PathValue { - fn serialize(&self, serializer: S) -> Result - where - S: ser::Serializer, - { - self.0.serialize(serializer) - } -} - -impl<'de> de::Deserialize<'de> for PathValue { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - Ok(PathValue(String::deserialize(deserializer)?.into())) - } -} - -/// Corresponds to a `target` entry, but `TomlTarget` is already used. -#[derive(Serialize, Deserialize, Debug, Clone)] -struct TomlPlatform { - dependencies: Option>, - #[serde(rename = "build-dependencies")] - build_dependencies: Option>, - #[serde(rename = "build_dependencies")] - build_dependencies2: Option>, - #[serde(rename = "dev-dependencies")] - dev_dependencies: Option>, - #[serde(rename = "dev_dependencies")] - dev_dependencies2: Option>, -} - impl TomlTarget { fn new() -> TomlTarget { TomlTarget::default() @@ -3339,12 +3304,47 @@ impl TomlTarget { } } +#[derive(Clone)] +struct PathValue(PathBuf); + impl fmt::Debug for PathValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } +impl ser::Serialize for PathValue { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + self.0.serialize(serializer) + } +} + +impl<'de> de::Deserialize<'de> for PathValue { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + Ok(PathValue(String::deserialize(deserializer)?.into())) + } +} + +/// Corresponds to a `target` entry, but `TomlTarget` is already used. +#[derive(Serialize, Deserialize, Debug, Clone)] +struct TomlPlatform { + dependencies: Option>, + #[serde(rename = "build-dependencies")] + build_dependencies: Option>, + #[serde(rename = "build_dependencies")] + build_dependencies2: Option>, + #[serde(rename = "dev-dependencies")] + dev_dependencies: Option>, + #[serde(rename = "dev_dependencies")] + dev_dependencies2: Option>, +} + #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(expecting = "a lints table")] pub struct MaybeWorkspaceLints { From 571e6cf3c55cdddf0b299906cd4df703b32d5dbf Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 3 Oct 2023 11:41:25 -0500 Subject: [PATCH 3/3] refactor(toml): Move related structs next to each other --- src/cargo/util/toml/mod.rs | 3128 ++++++++++++++++++------------------ 1 file changed, 1564 insertions(+), 1564 deletions(-) diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 790beef9959..f2143a91638 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -188,472 +188,6 @@ fn warn_on_deprecated(new_path: &str, name: &str, kind: &str, warnings: &mut Vec )) } -type TomlLibTarget = TomlTarget; -type TomlBinTarget = TomlTarget; -type TomlExampleTarget = TomlTarget; -type TomlTestTarget = TomlTarget; -type TomlBenchTarget = TomlTarget; - -#[derive(Clone, Debug, Serialize)] -#[serde(untagged)] -pub enum TomlDependency { - /// In the simple format, only a version is specified, eg. - /// `package = ""` - Simple(String), - /// The simple format is equivalent to a detailed dependency - /// specifying only a version, eg. - /// `package = { version = "" }` - Detailed(DetailedTomlDependency

), -} - -impl TomlDependency { - fn unused_keys(&self) -> Vec { - match self { - TomlDependency::Simple(_) => vec![], - TomlDependency::Detailed(detailed) => detailed.other.keys().cloned().collect(), - } - } -} - -impl TomlDependency

{ - pub(crate) fn to_dependency_split( - &self, - name: &str, - source_id: SourceId, - nested_paths: &mut Vec, - config: &Config, - warnings: &mut Vec, - platform: Option, - root: &Path, - features: &Features, - kind: Option, - ) -> CargoResult { - self.to_dependency( - name, - &mut Context { - deps: &mut Vec::new(), - source_id, - nested_paths, - config, - warnings, - platform, - root, - features, - }, - kind, - ) - } - - fn to_dependency( - &self, - name: &str, - cx: &mut Context<'_, '_>, - kind: Option, - ) -> CargoResult { - match *self { - TomlDependency::Simple(ref version) => DetailedTomlDependency::

{ - version: Some(version.clone()), - ..Default::default() - } - .to_dependency(name, cx, kind), - TomlDependency::Detailed(ref details) => details.to_dependency(name, cx, kind), - } - } - - fn is_version_specified(&self) -> bool { - match self { - TomlDependency::Detailed(d) => d.version.is_some(), - TomlDependency::Simple(..) => true, - } - } - - fn is_optional(&self) -> bool { - match self { - TomlDependency::Detailed(d) => d.optional.unwrap_or(false), - TomlDependency::Simple(..) => false, - } - } -} - -impl<'de, P: Deserialize<'de> + Clone> de::Deserialize<'de> for TomlDependency

{ - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - UntaggedEnumVisitor::new() - .expecting( - "a version string like \"0.9.8\" or a \ - detailed dependency like { version = \"0.9.8\" }", - ) - .string(|value| Ok(TomlDependency::Simple(value.to_owned()))) - .map(|value| value.deserialize().map(TomlDependency::Detailed)) - .deserialize(deserializer) - } -} - -pub trait ResolveToPath { - fn resolve(&self, config: &Config) -> PathBuf; -} - -impl ResolveToPath for String { - fn resolve(&self, _: &Config) -> PathBuf { - self.into() - } -} - -impl ResolveToPath for ConfigRelativePath { - fn resolve(&self, c: &Config) -> PathBuf { - self.resolve_path(c) - } -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -pub struct DetailedTomlDependency { - version: Option, - registry: Option, - /// The URL of the `registry` field. - /// This is an internal implementation detail. When Cargo creates a - /// package, it replaces `registry` with `registry-index` so that the - /// manifest contains the correct URL. All users won't have the same - /// registry names configured, so Cargo can't rely on just the name for - /// crates published by other users. - registry_index: Option, - // `path` is relative to the file it appears in. If that's a `Cargo.toml`, it'll be relative to - // that TOML file, and if it's a `.cargo/config` file, it'll be relative to that file. - path: Option

, - git: Option, - branch: Option, - tag: Option, - rev: Option, - features: Option>, - optional: Option, - default_features: Option, - #[serde(rename = "default_features")] - default_features2: Option, - package: Option, - public: Option, - - /// One or more of `bin`, `cdylib`, `staticlib`, `bin:`. - artifact: Option, - /// If set, the artifact should also be a dependency - lib: Option, - /// A platform name, like `x86_64-apple-darwin` - target: Option, - /// This is here to provide a way to see the "unused manifest keys" when deserializing - #[serde(skip_serializing)] - #[serde(flatten)] - other: BTreeMap, -} - -impl DetailedTomlDependency { - fn add_features(&mut self, features: Option>) { - self.features = match (self.features.clone(), features.clone()) { - (Some(dep_feat), Some(inherit_feat)) => Some( - dep_feat - .into_iter() - .chain(inherit_feat) - .collect::>(), - ), - (Some(dep_fet), None) => Some(dep_fet), - (None, Some(inherit_feat)) => Some(inherit_feat), - (None, None) => None, - }; - } - - fn update_optional(&mut self, optional: Option) { - self.optional = optional; - } - - fn resolve_path( - &mut self, - name: &str, - root_path: &Path, - package_root: &Path, - ) -> CargoResult<()> { - if let Some(rel_path) = &self.path { - self.path = Some(resolve_relative_path( - name, - root_path, - package_root, - rel_path, - )?) - } - Ok(()) - } -} - -impl DetailedTomlDependency

{ - fn to_dependency( - &self, - name_in_toml: &str, - cx: &mut Context<'_, '_>, - kind: Option, - ) -> CargoResult { - if self.version.is_none() && self.path.is_none() && self.git.is_none() { - let msg = format!( - "dependency ({}) specified without \ - providing a local path, Git repository, version, or \ - workspace dependency to use. This will be considered an \ - error in future versions", - name_in_toml - ); - cx.warnings.push(msg); - } - - if let Some(version) = &self.version { - if version.contains('+') { - cx.warnings.push(format!( - "version requirement `{}` for dependency `{}` \ - includes semver metadata which will be ignored, removing the \ - metadata is recommended to avoid confusion", - version, name_in_toml - )); - } - } - - if self.git.is_none() { - let git_only_keys = [ - (&self.branch, "branch"), - (&self.tag, "tag"), - (&self.rev, "rev"), - ]; - - for &(key, key_name) in &git_only_keys { - if key.is_some() { - bail!( - "key `{}` is ignored for dependency ({}).", - key_name, - name_in_toml - ); - } - } - } - - // Early detection of potentially misused feature syntax - // instead of generating a "feature not found" error. - if let Some(features) = &self.features { - for feature in features { - if feature.contains('/') { - bail!( - "feature `{}` in dependency `{}` is not allowed to contain slashes\n\ - If you want to enable features of a transitive dependency, \ - the direct dependency needs to re-export those features from \ - the `[features]` table.", - feature, - name_in_toml - ); - } - if feature.starts_with("dep:") { - bail!( - "feature `{}` in dependency `{}` is not allowed to use explicit \ - `dep:` syntax\n\ - If you want to enable an optional dependency, specify the name \ - of the optional dependency without the `dep:` prefix, or specify \ - a feature from the dependency's `[features]` table that enables \ - the optional dependency.", - feature, - name_in_toml - ); - } - } - } - - let new_source_id = match ( - self.git.as_ref(), - self.path.as_ref(), - self.registry.as_ref(), - self.registry_index.as_ref(), - ) { - (Some(_), _, Some(_), _) | (Some(_), _, _, Some(_)) => bail!( - "dependency ({}) specification is ambiguous. \ - Only one of `git` or `registry` is allowed.", - name_in_toml - ), - (_, _, Some(_), Some(_)) => bail!( - "dependency ({}) specification is ambiguous. \ - Only one of `registry` or `registry-index` is allowed.", - name_in_toml - ), - (Some(git), maybe_path, _, _) => { - if maybe_path.is_some() { - bail!( - "dependency ({}) specification is ambiguous. \ - Only one of `git` or `path` is allowed.", - name_in_toml - ); - } - - let n_details = [&self.branch, &self.tag, &self.rev] - .iter() - .filter(|d| d.is_some()) - .count(); - - if n_details > 1 { - bail!( - "dependency ({}) specification is ambiguous. \ - Only one of `branch`, `tag` or `rev` is allowed.", - name_in_toml - ); - } - - let reference = self - .branch - .clone() - .map(GitReference::Branch) - .or_else(|| self.tag.clone().map(GitReference::Tag)) - .or_else(|| self.rev.clone().map(GitReference::Rev)) - .unwrap_or(GitReference::DefaultBranch); - let loc = git.into_url()?; - - if let Some(fragment) = loc.fragment() { - let msg = format!( - "URL fragment `#{}` in git URL is ignored for dependency ({}). \ - If you were trying to specify a specific git revision, \ - use `rev = \"{}\"` in the dependency declaration.", - fragment, name_in_toml, fragment - ); - cx.warnings.push(msg) - } - - SourceId::for_git(&loc, reference)? - } - (None, Some(path), _, _) => { - let path = path.resolve(cx.config); - cx.nested_paths.push(path.clone()); - // If the source ID for the package we're parsing is a path - // source, then we normalize the path here to get rid of - // components like `..`. - // - // The purpose of this is to get a canonical ID for the package - // that we're depending on to ensure that builds of this package - // always end up hashing to the same value no matter where it's - // built from. - if cx.source_id.is_path() { - let path = cx.root.join(path); - let path = paths::normalize_path(&path); - SourceId::for_path(&path)? - } else { - cx.source_id - } - } - (None, None, Some(registry), None) => SourceId::alt_registry(cx.config, registry)?, - (None, None, None, Some(registry_index)) => { - let url = registry_index.into_url()?; - SourceId::for_registry(&url)? - } - (None, None, None, None) => SourceId::crates_io(cx.config)?, - }; - - let (pkg_name, explicit_name_in_toml) = match self.package { - Some(ref s) => (&s[..], Some(name_in_toml)), - None => (name_in_toml, None), - }; - - let version = self.version.as_deref(); - let mut dep = Dependency::parse(pkg_name, version, new_source_id)?; - if self.default_features.is_some() && self.default_features2.is_some() { - warn_on_deprecated("default-features", name_in_toml, "dependency", cx.warnings); - } - dep.set_features(self.features.iter().flatten()) - .set_default_features( - self.default_features - .or(self.default_features2) - .unwrap_or(true), - ) - .set_optional(self.optional.unwrap_or(false)) - .set_platform(cx.platform.clone()); - if let Some(registry) = &self.registry { - let registry_id = SourceId::alt_registry(cx.config, registry)?; - dep.set_registry_id(registry_id); - } - if let Some(registry_index) = &self.registry_index { - let url = registry_index.into_url()?; - let registry_id = SourceId::for_registry(&url)?; - dep.set_registry_id(registry_id); - } - - if let Some(kind) = kind { - dep.set_kind(kind); - } - if let Some(name_in_toml) = explicit_name_in_toml { - dep.set_explicit_name_in_toml(name_in_toml); - } - - if let Some(p) = self.public { - cx.features.require(Feature::public_dependency())?; - - if dep.kind() != DepKind::Normal { - bail!("'public' specifier can only be used on regular dependencies, not {:?} dependencies", dep.kind()); - } - - dep.set_public(p); - } - - if let (Some(artifact), is_lib, target) = ( - self.artifact.as_ref(), - self.lib.unwrap_or(false), - self.target.as_deref(), - ) { - if cx.config.cli_unstable().bindeps { - let artifact = Artifact::parse(&artifact.0, is_lib, target)?; - if dep.kind() != DepKind::Build - && artifact.target() == Some(ArtifactTarget::BuildDependencyAssumeTarget) - { - bail!( - r#"`target = "target"` in normal- or dev-dependencies has no effect ({})"#, - name_in_toml - ); - } - dep.set_artifact(artifact) - } else { - bail!("`artifact = …` requires `-Z bindeps` ({})", name_in_toml); - } - } else if self.lib.is_some() || self.target.is_some() { - for (is_set, specifier) in [ - (self.lib.is_some(), "lib"), - (self.target.is_some(), "target"), - ] { - if !is_set { - continue; - } - bail!( - "'{}' specifier cannot be used without an 'artifact = …' value ({})", - specifier, - name_in_toml - ) - } - } - Ok(dep) - } -} - -// Explicit implementation so we avoid pulling in P: Default -impl Default for DetailedTomlDependency

{ - fn default() -> Self { - Self { - version: Default::default(), - registry: Default::default(), - registry_index: Default::default(), - path: Default::default(), - git: Default::default(), - branch: Default::default(), - tag: Default::default(), - rev: Default::default(), - features: Default::default(), - optional: Default::default(), - default_features: Default::default(), - default_features2: Default::default(), - package: Default::default(), - public: Default::default(), - artifact: Default::default(), - lib: Default::default(), - target: Default::default(), - other: Default::default(), - } - } -} - /// This type is used to deserialize `Cargo.toml` files. #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] @@ -1921,1308 +1455,1695 @@ fn inheritable_from_path( } } -/// Returns the name of the README file for a [`TomlPackage`]. -pub fn readme_for_package(package_root: &Path, readme: Option<&StringOrBool>) -> Option { - match &readme { - None => default_readme_from_package_root(package_root), - Some(value) => match value { - StringOrBool::Bool(false) => None, - StringOrBool::Bool(true) => Some("README.md".to_string()), - StringOrBool::String(v) => Some(v.clone()), - }, - } +/// Returns the name of the README file for a [`TomlPackage`]. +pub fn readme_for_package(package_root: &Path, readme: Option<&StringOrBool>) -> Option { + match &readme { + None => default_readme_from_package_root(package_root), + Some(value) => match value { + StringOrBool::Bool(false) => None, + StringOrBool::Bool(true) => Some("README.md".to_string()), + StringOrBool::String(v) => Some(v.clone()), + }, + } +} + +const DEFAULT_README_FILES: [&str; 3] = ["README.md", "README.txt", "README"]; + +/// Checks if a file with any of the default README file names exists in the package root. +/// If so, returns a `String` representing that name. +fn default_readme_from_package_root(package_root: &Path) -> Option { + for &readme_filename in DEFAULT_README_FILES.iter() { + if package_root.join(readme_filename).is_file() { + return Some(readme_filename.to_string()); + } + } + + None +} + +/// Checks a list of build targets, and ensures the target names are unique within a vector. +/// If not, the name of the offending build target is returned. +fn unique_build_targets( + targets: &[Target], + package_root: &Path, +) -> Result<(), HashMap>> { + let mut source_targets = HashMap::<_, Vec<_>>::new(); + for target in targets { + if let TargetSourcePath::Path(path) = target.src_path() { + let full = package_root.join(path); + source_targets.entry(full).or_default().push(target.clone()); + } + } + + let conflict_targets = source_targets + .into_iter() + .filter(|(_, targets)| targets.len() > 1) + .collect::>(); + + if !conflict_targets.is_empty() { + return Err(conflict_targets); + } + + Ok(()) +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct TomlWorkspace { + members: Option>, + #[serde(rename = "default-members")] + default_members: Option>, + exclude: Option>, + resolver: Option, + + // Properties that can be inherited by members. + package: Option, + dependencies: Option>, + lints: Option, + + // Note that this field must come last due to the way toml serialization + // works which requires tables to be emitted after all values. + metadata: Option, +} + +/// A group of fields that are inheritable by members of the workspace +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct InheritableFields { + // We use skip here since it will never be present when deserializing + // and we don't want it present when serializing + #[serde(skip)] + dependencies: Option>, + #[serde(skip)] + lints: Option, + + version: Option, + authors: Option>, + description: Option, + homepage: Option, + documentation: Option, + readme: Option, + keywords: Option>, + categories: Option>, + license: Option, + #[serde(rename = "license-file")] + license_file: Option, + repository: Option, + publish: Option, + edition: Option, + badges: Option>>, + exclude: Option>, + include: Option>, + #[serde(rename = "rust-version")] + rust_version: Option, + // We use skip here since it will never be present when deserializing + // and we don't want it present when serializing + #[serde(skip)] + ws_root: PathBuf, +} + +/// Defines simple getter methods for inheritable fields. +macro_rules! inheritable_field_getter { + ( $(($key:literal, $field:ident -> $ret:ty),)* ) => ( + $( + #[doc = concat!("Gets the field `workspace.", $key, "`.")] + pub fn $field(&self) -> CargoResult<$ret> { + let Some(val) = &self.$field else { + bail!("`workspace.{}` was not defined", $key); + }; + Ok(val.clone()) + } + )* + ) } -const DEFAULT_README_FILES: [&str; 3] = ["README.md", "README.txt", "README"]; +impl InheritableFields { + inheritable_field_getter! { + // Please keep this list lexicographically ordered. + ("dependencies", dependencies -> BTreeMap), + ("lints", lints -> TomlLints), + ("package.authors", authors -> Vec), + ("package.badges", badges -> BTreeMap>), + ("package.categories", categories -> Vec), + ("package.description", description -> String), + ("package.documentation", documentation -> String), + ("package.edition", edition -> String), + ("package.exclude", exclude -> Vec), + ("package.homepage", homepage -> String), + ("package.include", include -> Vec), + ("package.keywords", keywords -> Vec), + ("package.license", license -> String), + ("package.publish", publish -> VecStringOrBool), + ("package.repository", repository -> String), + ("package.rust-version", rust_version -> RustVersion), + ("package.version", version -> semver::Version), + } -/// Checks if a file with any of the default README file names exists in the package root. -/// If so, returns a `String` representing that name. -fn default_readme_from_package_root(package_root: &Path) -> Option { - for &readme_filename in DEFAULT_README_FILES.iter() { - if package_root.join(readme_filename).is_file() { - return Some(readme_filename.to_string()); + /// Gets a workspace dependency with the `name`. + pub fn get_dependency(&self, name: &str, package_root: &Path) -> CargoResult { + let Some(deps) = &self.dependencies else { + bail!("`workspace.dependencies` was not defined"); + }; + let Some(dep) = deps.get(name) else { + bail!("`dependency.{name}` was not found in `workspace.dependencies`"); + }; + let mut dep = dep.clone(); + if let TomlDependency::Detailed(detailed) = &mut dep { + detailed.resolve_path(name, self.ws_root(), package_root)?; } + Ok(dep) } - None -} + /// Gets the field `workspace.package.license-file`. + pub fn license_file(&self, package_root: &Path) -> CargoResult { + let Some(license_file) = &self.license_file else { + bail!("`workspace.package.license-file` was not defined"); + }; + resolve_relative_path("license-file", &self.ws_root, package_root, license_file) + } -/// Checks a list of build targets, and ensures the target names are unique within a vector. -/// If not, the name of the offending build target is returned. -fn unique_build_targets( - targets: &[Target], - package_root: &Path, -) -> Result<(), HashMap>> { - let mut source_targets = HashMap::<_, Vec<_>>::new(); - for target in targets { - if let TargetSourcePath::Path(path) = target.src_path() { - let full = package_root.join(path); - source_targets.entry(full).or_default().push(target.clone()); - } + /// Gets the field `workspace.package.readme`. + pub fn readme(&self, package_root: &Path) -> CargoResult { + let Some(readme) = readme_for_package(self.ws_root.as_path(), self.readme.as_ref()) else { + bail!("`workspace.package.readme` was not defined"); + }; + resolve_relative_path("readme", &self.ws_root, package_root, &readme) + .map(StringOrBool::String) } - let conflict_targets = source_targets - .into_iter() - .filter(|(_, targets)| targets.len() > 1) - .collect::>(); + pub fn ws_root(&self) -> &PathBuf { + &self.ws_root + } - if !conflict_targets.is_empty() { - return Err(conflict_targets); + pub fn update_deps(&mut self, deps: Option>) { + self.dependencies = deps; } - Ok(()) + pub fn update_lints(&mut self, lints: Option) { + self.lints = lints; + } + + pub fn update_ws_path(&mut self, ws_root: PathBuf) { + self.ws_root = ws_root; + } } -#[derive(Deserialize, Serialize, Clone, Debug, Default)] -pub struct TomlProfiles(BTreeMap); +/// Represents the `package`/`project` sections of a `Cargo.toml`. +/// +/// Note that the order of the fields matters, since this is the order they +/// are serialized to a TOML file. For example, you cannot have values after +/// the field `metadata`, since it is a table and values cannot appear after +/// tables. +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct TomlPackage { + edition: Option, + rust_version: Option, + name: InternedString, + #[serde(deserialize_with = "version_trim_whitespace")] + version: MaybeWorkspaceSemverVersion, + authors: Option, + build: Option, + metabuild: Option, + default_target: Option, + forced_target: Option, + links: Option, + exclude: Option, + include: Option, + publish: Option, + workspace: Option, + im_a_teapot: Option, + autobins: Option, + autoexamples: Option, + autotests: Option, + autobenches: Option, + default_run: Option, -impl TomlProfiles { - pub fn get_all(&self) -> &BTreeMap { - &self.0 - } + // Package metadata. + description: Option, + homepage: Option, + documentation: Option, + readme: Option, + keywords: Option, + categories: Option, + license: Option, + license_file: Option, + repository: Option, + resolver: Option, - pub fn get(&self, name: &str) -> Option<&TomlProfile> { - self.0.get(name) - } + // Provide a helpful error message for a common user error. + #[serde(rename = "cargo-features", skip_serializing)] + _invalid_cargo_features: Option, - /// Checks syntax validity and unstable feature gate for each profile. - /// - /// It's a bit unfortunate both `-Z` flags and `cargo-features` are required, - /// because profiles can now be set in either `Cargo.toml` or `config.toml`. - pub fn validate( + // Note that this field must come last due to the way toml serialization + // works which requires tables to be emitted after all values. + metadata: Option, +} + +impl TomlPackage { + pub fn to_package_id( &self, - cli_unstable: &CliUnstable, - features: &Features, - warnings: &mut Vec, - ) -> CargoResult<()> { - for (name, profile) in &self.0 { - profile.validate(name, cli_unstable, features, warnings)?; - } - Ok(()) + source_id: SourceId, + version: semver::Version, + ) -> CargoResult { + PackageId::new(self.name, version, source_id) } } -#[derive(Deserialize, Serialize, Clone, Debug, Default, Eq, PartialEq)] -#[serde(default, rename_all = "kebab-case")] -pub struct TomlProfile { - pub opt_level: Option, - pub lto: Option, - pub codegen_backend: Option, - pub codegen_units: Option, - pub debug: Option, - pub split_debuginfo: Option, - pub debug_assertions: Option, - pub rpath: Option, - pub panic: Option, - pub overflow_checks: Option, - pub incremental: Option, - pub dir_name: Option, - pub inherits: Option, - pub strip: Option, - // Note that `rustflags` is used for the cargo-feature `profile_rustflags` - pub rustflags: Option>, - // These two fields must be last because they are sub-tables, and TOML - // requires all non-tables to be listed first. - pub package: Option>, - pub build_override: Option>, +fn version_trim_whitespace<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + UntaggedEnumVisitor::new() + .expecting("SemVer version") + .string( + |value| match value.trim().parse().map_err(de::Error::custom) { + Ok(parsed) => Ok(MaybeWorkspace::Defined(parsed)), + Err(e) => Err(e), + }, + ) + .map(|value| value.deserialize().map(MaybeWorkspace::Workspace)) + .deserialize(deserializer) } -impl TomlProfile { - /// Checks stytax validity and unstable feature gate for a given profile. - pub fn validate( - &self, - name: &str, - cli_unstable: &CliUnstable, - features: &Features, - warnings: &mut Vec, - ) -> CargoResult<()> { - self.validate_profile(name, cli_unstable, features)?; - if let Some(ref profile) = self.build_override { - profile.validate_override("build-override")?; - profile.validate_profile(&format!("{name}.build-override"), cli_unstable, features)?; - } - if let Some(ref packages) = self.package { - for (override_name, profile) in packages { - profile.validate_override("package")?; - profile.validate_profile( - &format!("{name}.package.{override_name}"), - cli_unstable, - features, - )?; - } - } +/// This Trait exists to make [`MaybeWorkspace::Workspace`] generic. It makes deserialization of +/// [`MaybeWorkspace`] much easier, as well as making error messages for +/// [`MaybeWorkspace::resolve`] much nicer +/// +/// Implementors should have a field `workspace` with the type of `bool`. It is used to ensure +/// `workspace` is not `false` in a `Cargo.toml` +pub trait WorkspaceInherit { + /// This is the workspace table that is being inherited from. + /// For example `[workspace.dependencies]` would be the table "dependencies" + fn inherit_toml_table(&self) -> &str; - // Profile name validation - Self::validate_name(name)?; + /// This is used to output the value of the implementors `workspace` field + fn workspace(&self) -> bool; +} - if let Some(dir_name) = self.dir_name { - // This is disabled for now, as we would like to stabilize named - // profiles without this, and then decide in the future if it is - // needed. This helps simplify the UI a little. - bail!( - "dir-name=\"{}\" in profile `{}` is not currently allowed, \ - directory names are tied to the profile name for custom profiles", - dir_name, - name - ); - } +/// An enum that allows for inheriting keys from a workspace in a Cargo.toml. +#[derive(Serialize, Copy, Clone, Debug)] +#[serde(untagged)] +pub enum MaybeWorkspace { + /// The "defined" type, or the type that that is used when not inheriting from a workspace. + Defined(T), + /// The type when inheriting from a workspace. + Workspace(W), +} - // `inherits` validation - if matches!(self.inherits.map(|s| s.as_str()), Some("debug")) { - bail!( - "profile.{}.inherits=\"debug\" should be profile.{}.inherits=\"dev\"", - name, - name - ); +impl MaybeWorkspace { + fn resolve<'a>( + self, + label: &str, + get_ws_inheritable: impl FnOnce() -> CargoResult, + ) -> CargoResult { + match self { + MaybeWorkspace::Defined(value) => Ok(value), + MaybeWorkspace::Workspace(w) => get_ws_inheritable().with_context(|| { + format!( + "error inheriting `{label}` from workspace root manifest's `workspace.{}.{label}`", + w.inherit_toml_table(), + ) + }), } + } - match name { - "doc" => { - warnings.push("profile `doc` is deprecated and has no effect".to_string()); - } - "test" | "bench" => { - if self.panic.is_some() { - warnings.push(format!("`panic` setting is ignored for `{}` profile", name)) - } - } - _ => {} + fn resolve_with_self<'a>( + self, + label: &str, + get_ws_inheritable: impl FnOnce(&W) -> CargoResult, + ) -> CargoResult { + match self { + MaybeWorkspace::Defined(value) => Ok(value), + MaybeWorkspace::Workspace(w) => get_ws_inheritable(&w).with_context(|| { + format!( + "error inheriting `{label}` from workspace root manifest's `workspace.{}.{label}`", + w.inherit_toml_table(), + ) + }), } + } - if let Some(panic) = &self.panic { - if panic != "unwind" && panic != "abort" { - bail!( - "`panic` setting of `{}` is not a valid setting, \ - must be `unwind` or `abort`", - panic - ); - } + fn as_defined(&self) -> Option<&T> { + match self { + MaybeWorkspace::Workspace(_) => None, + MaybeWorkspace::Defined(defined) => Some(defined), } + } +} - if let Some(StringOrBool::String(arg)) = &self.lto { - if arg == "true" || arg == "false" { - bail!( - "`lto` setting of string `\"{arg}\"` for `{name}` profile is not \ - a valid setting, must be a boolean (`true`/`false`) or a string \ - (`\"thin\"`/`\"fat\"`/`\"off\"`) or omitted.", - ); - } - } +//. This already has a `Deserialize` impl from version_trim_whitespace +type MaybeWorkspaceSemverVersion = MaybeWorkspace; - Ok(()) - } +type MaybeWorkspaceString = MaybeWorkspace; +impl<'de> de::Deserialize<'de> for MaybeWorkspaceString { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; - /// Validate dir-names and profile names according to RFC 2678. - pub fn validate_name(name: &str) -> CargoResult<()> { - if let Some(ch) = name - .chars() - .find(|ch| !ch.is_alphanumeric() && *ch != '_' && *ch != '-') - { - bail!( - "invalid character `{}` in profile name `{}`\n\ - Allowed characters are letters, numbers, underscore, and hyphen.", - ch, - name - ); - } + impl<'de> de::Visitor<'de> for Visitor { + type Value = MaybeWorkspaceString; - const SEE_DOCS: &str = "See https://doc.rust-lang.org/cargo/reference/profiles.html \ - for more on configuring profiles."; + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_str("a string or workspace") + } - let lower_name = name.to_lowercase(); - if lower_name == "debug" { - bail!( - "profile name `{}` is reserved\n\ - To configure the default development profile, use the name `dev` \ - as in [profile.dev]\n\ - {}", - name, - SEE_DOCS - ); - } - if lower_name == "build-override" { - bail!( - "profile name `{}` is reserved\n\ - To configure build dependency settings, use [profile.dev.build-override] \ - and [profile.release.build-override]\n\ - {}", - name, - SEE_DOCS - ); - } + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + Ok(MaybeWorkspaceString::Defined(value)) + } - // These are some arbitrary reservations. We have no plans to use - // these, but it seems safer to reserve a few just in case we want to - // add more built-in profiles in the future. We can also uses special - // syntax like cargo:foo if needed. But it is unlikely these will ever - // be used. - if matches!( - lower_name.as_str(), - "build" - | "check" - | "clean" - | "config" - | "fetch" - | "fix" - | "install" - | "metadata" - | "package" - | "publish" - | "report" - | "root" - | "run" - | "rust" - | "rustc" - | "rustdoc" - | "target" - | "tmp" - | "uninstall" - ) || lower_name.starts_with("cargo") - { - bail!( - "profile name `{}` is reserved\n\ - Please choose a different name.\n\ - {}", - name, - SEE_DOCS - ); + fn visit_map(self, map: V) -> Result + where + V: de::MapAccess<'de>, + { + let mvd = de::value::MapAccessDeserializer::new(map); + TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) + } } - Ok(()) + d.deserialize_any(Visitor) } +} - /// Validates a profile. - /// - /// This is a shallow check, which is reused for the profile itself and any overrides. - fn validate_profile( - &self, - name: &str, - cli_unstable: &CliUnstable, - features: &Features, - ) -> CargoResult<()> { - if let Some(codegen_backend) = &self.codegen_backend { - match ( - features.require(Feature::codegen_backend()), - cli_unstable.codegen_backend, - ) { - (Err(e), false) => return Err(e), - _ => {} +type MaybeWorkspaceRustVersion = MaybeWorkspace; +impl<'de> de::Deserialize<'de> for MaybeWorkspaceRustVersion { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = MaybeWorkspaceRustVersion; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_str("a semver or workspace") } - if codegen_backend.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_') { - bail!( - "`profile.{}.codegen-backend` setting of `{}` is not a valid backend name.", - name, - codegen_backend, - ); + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + let value = value.parse::().map_err(|e| E::custom(e))?; + Ok(MaybeWorkspaceRustVersion::Defined(value)) } - } - if self.rustflags.is_some() { - match ( - features.require(Feature::profile_rustflags()), - cli_unstable.profile_rustflags, - ) { - (Err(e), false) => return Err(e), - _ => {} + + fn visit_map(self, map: V) -> Result + where + V: de::MapAccess<'de>, + { + let mvd = de::value::MapAccessDeserializer::new(map); + TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) } } - Ok(()) - } - /// Validation that is specific to an override. - fn validate_override(&self, which: &str) -> CargoResult<()> { - if self.package.is_some() { - bail!("package-specific profiles cannot be nested"); - } - if self.build_override.is_some() { - bail!("build-override profiles cannot be nested"); - } - if self.panic.is_some() { - bail!("`panic` may not be specified in a `{}` profile", which) - } - if self.lto.is_some() { - bail!("`lto` may not be specified in a `{}` profile", which) - } - if self.rpath.is_some() { - bail!("`rpath` may not be specified in a `{}` profile", which) - } - Ok(()) + d.deserialize_any(Visitor) } +} - /// Overwrite self's values with the given profile. - pub fn merge(&mut self, profile: &TomlProfile) { - if let Some(v) = &profile.opt_level { - self.opt_level = Some(v.clone()); - } +type MaybeWorkspaceVecString = MaybeWorkspace, TomlWorkspaceField>; +impl<'de> de::Deserialize<'de> for MaybeWorkspaceVecString { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; - if let Some(v) = &profile.lto { - self.lto = Some(v.clone()); - } + impl<'de> de::Visitor<'de> for Visitor { + type Value = MaybeWorkspaceVecString; - if let Some(v) = profile.codegen_backend { - self.codegen_backend = Some(v); - } + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.write_str("a vector of strings or workspace") + } + fn visit_seq(self, v: A) -> Result + where + A: de::SeqAccess<'de>, + { + let seq = de::value::SeqAccessDeserializer::new(v); + Vec::deserialize(seq).map(MaybeWorkspace::Defined) + } - if let Some(v) = profile.codegen_units { - self.codegen_units = Some(v); + fn visit_map(self, map: V) -> Result + where + V: de::MapAccess<'de>, + { + let mvd = de::value::MapAccessDeserializer::new(map); + TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) + } } - if let Some(v) = profile.debug { - self.debug = Some(v); - } + d.deserialize_any(Visitor) + } +} - if let Some(v) = profile.debug_assertions { - self.debug_assertions = Some(v); - } +type MaybeWorkspaceStringOrBool = MaybeWorkspace; +impl<'de> de::Deserialize<'de> for MaybeWorkspaceStringOrBool { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; - if let Some(v) = &profile.split_debuginfo { - self.split_debuginfo = Some(v.clone()); - } + impl<'de> de::Visitor<'de> for Visitor { + type Value = MaybeWorkspaceStringOrBool; - if let Some(v) = profile.rpath { - self.rpath = Some(v); - } + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.write_str("a string, a bool, or workspace") + } - if let Some(v) = &profile.panic { - self.panic = Some(v.clone()); - } + fn visit_bool(self, v: bool) -> Result + where + E: de::Error, + { + let b = de::value::BoolDeserializer::new(v); + StringOrBool::deserialize(b).map(MaybeWorkspace::Defined) + } - if let Some(v) = profile.overflow_checks { - self.overflow_checks = Some(v); - } + fn visit_string(self, v: String) -> Result + where + E: de::Error, + { + let string = de::value::StringDeserializer::new(v); + StringOrBool::deserialize(string).map(MaybeWorkspace::Defined) + } - if let Some(v) = profile.incremental { - self.incremental = Some(v); + fn visit_map(self, map: V) -> Result + where + V: de::MapAccess<'de>, + { + let mvd = de::value::MapAccessDeserializer::new(map); + TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) + } } - if let Some(v) = &profile.rustflags { - self.rustflags = Some(v.clone()); - } + d.deserialize_any(Visitor) + } +} - if let Some(other_package) = &profile.package { - match &mut self.package { - Some(self_package) => { - for (spec, other_pkg_profile) in other_package { - match self_package.get_mut(spec) { - Some(p) => p.merge(other_pkg_profile), - None => { - self_package.insert(spec.clone(), other_pkg_profile.clone()); - } - } - } - } - None => self.package = Some(other_package.clone()), +type MaybeWorkspaceVecStringOrBool = MaybeWorkspace; +impl<'de> de::Deserialize<'de> for MaybeWorkspaceVecStringOrBool { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = MaybeWorkspaceVecStringOrBool; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.write_str("a boolean, a vector of strings, or workspace") } - } - if let Some(other_bo) = &profile.build_override { - match &mut self.build_override { - Some(self_bo) => self_bo.merge(other_bo), - None => self.build_override = Some(other_bo.clone()), + fn visit_bool(self, v: bool) -> Result + where + E: de::Error, + { + let b = de::value::BoolDeserializer::new(v); + VecStringOrBool::deserialize(b).map(MaybeWorkspace::Defined) } - } - if let Some(v) = &profile.inherits { - self.inherits = Some(*v); - } + fn visit_seq(self, v: A) -> Result + where + A: de::SeqAccess<'de>, + { + let seq = de::value::SeqAccessDeserializer::new(v); + VecStringOrBool::deserialize(seq).map(MaybeWorkspace::Defined) + } - if let Some(v) = &profile.dir_name { - self.dir_name = Some(*v); + fn visit_map(self, map: V) -> Result + where + V: de::MapAccess<'de>, + { + let mvd = de::value::MapAccessDeserializer::new(map); + TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) + } } - if let Some(v) = &profile.strip { - self.strip = Some(v.clone()); + d.deserialize_any(Visitor) + } +} + +type MaybeWorkspaceBtreeMap = + MaybeWorkspace>, TomlWorkspaceField>; + +impl<'de> de::Deserialize<'de> for MaybeWorkspaceBtreeMap { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let value = serde_value::Value::deserialize(deserializer)?; + + if let Ok(w) = TomlWorkspaceField::deserialize( + serde_value::ValueDeserializer::::new(value.clone()), + ) { + return if w.workspace() { + Ok(MaybeWorkspace::Workspace(w)) + } else { + Err(de::Error::custom("`workspace` cannot be false")) + }; } + BTreeMap::deserialize(serde_value::ValueDeserializer::::new(value)) + .map(MaybeWorkspace::Defined) } } -#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] -pub enum ProfilePackageSpec { - Spec(PackageIdSpec), - All, +#[derive(Deserialize, Serialize, Copy, Clone, Debug)] +pub struct TomlWorkspaceField { + #[serde(deserialize_with = "bool_no_false")] + workspace: bool, } -impl fmt::Display for ProfilePackageSpec { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ProfilePackageSpec::Spec(spec) => spec.fmt(f), - ProfilePackageSpec::All => f.write_str("*"), - } +impl WorkspaceInherit for TomlWorkspaceField { + fn inherit_toml_table(&self) -> &str { + "package" + } + + fn workspace(&self) -> bool { + self.workspace } } -impl ser::Serialize for ProfilePackageSpec { - fn serialize(&self, s: S) -> Result - where - S: ser::Serializer, - { - self.to_string().serialize(s) +fn bool_no_false<'de, D: de::Deserializer<'de>>(deserializer: D) -> Result { + let b: bool = Deserialize::deserialize(deserializer)?; + if b { + Ok(b) + } else { + Err(de::Error::custom("`workspace` cannot be false")) } } -impl<'de> de::Deserialize<'de> for ProfilePackageSpec { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - let string = String::deserialize(d)?; - if string == "*" { - Ok(ProfilePackageSpec::All) - } else { - PackageIdSpec::parse(&string) - .map_err(de::Error::custom) - .map(ProfilePackageSpec::Spec) +type MaybeWorkspaceDependency = MaybeWorkspace; + +impl MaybeWorkspaceDependency { + fn unused_keys(&self) -> Vec { + match self { + MaybeWorkspaceDependency::Defined(d) => d.unused_keys(), + MaybeWorkspaceDependency::Workspace(w) => w.other.keys().cloned().collect(), } } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct TomlOptLevel(pub String); - -impl ser::Serialize for TomlOptLevel { - fn serialize(&self, serializer: S) -> Result +impl<'de> de::Deserialize<'de> for MaybeWorkspaceDependency { + fn deserialize(deserializer: D) -> Result where - S: ser::Serializer, + D: de::Deserializer<'de>, { - match self.0.parse::() { - Ok(n) => n.serialize(serializer), - Err(_) => self.0.serialize(serializer), + let value = serde_value::Value::deserialize(deserializer)?; + + if let Ok(w) = TomlWorkspaceDependency::deserialize(serde_value::ValueDeserializer::< + D::Error, + >::new(value.clone())) + { + return if w.workspace() { + Ok(MaybeWorkspace::Workspace(w)) + } else { + Err(de::Error::custom("`workspace` cannot be false")) + }; } + TomlDependency::deserialize(serde_value::ValueDeserializer::::new(value)) + .map(MaybeWorkspace::Defined) } } -impl<'de> de::Deserialize<'de> for TomlOptLevel { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - use serde::de::Error as _; - UntaggedEnumVisitor::new() - .expecting("an optimization level") - .i64(|value| Ok(TomlOptLevel(value.to_string()))) - .string(|value| { - if value == "s" || value == "z" { - Ok(TomlOptLevel(value.to_string())) - } else { - Err(serde_untagged::de::Error::custom(format!( - "must be `0`, `1`, `2`, `3`, `s` or `z`, \ - but found the string: \"{}\"", - value - ))) +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct TomlWorkspaceDependency { + workspace: bool, + features: Option>, + default_features: Option, + #[serde(rename = "default_features")] + default_features2: Option, + optional: Option, + /// This is here to provide a way to see the "unused manifest keys" when deserializing + #[serde(skip_serializing)] + #[serde(flatten)] + other: BTreeMap, +} + +impl TomlWorkspaceDependency { + fn resolve<'a>( + &self, + name: &str, + inheritable: impl FnOnce() -> CargoResult<&'a InheritableFields>, + cx: &mut Context<'_, '_>, + ) -> CargoResult { + fn default_features_msg(label: &str, ws_def_feat: Option, cx: &mut Context<'_, '_>) { + let ws_def_feat = match ws_def_feat { + Some(true) => "true", + Some(false) => "false", + None => "not specified", + }; + cx.warnings.push(format!( + "`default-features` is ignored for {label}, since `default-features` was \ + {ws_def_feat} for `workspace.dependencies.{label}`, \ + this could become a hard error in the future" + )) + } + if self.default_features.is_some() && self.default_features2.is_some() { + warn_on_deprecated("default-features", name, "dependency", cx.warnings); + } + inheritable()?.get_dependency(name, cx.root).map(|d| { + match d { + TomlDependency::Simple(s) => { + if let Some(false) = self.default_features.or(self.default_features2) { + default_features_msg(name, None, cx); + } + if self.optional.is_some() || self.features.is_some() { + TomlDependency::Detailed(DetailedTomlDependency { + version: Some(s), + optional: self.optional, + features: self.features.clone(), + ..Default::default() + }) + } else { + TomlDependency::Simple(s) + } } - }) - .deserialize(d) + TomlDependency::Detailed(d) => { + let mut d = d.clone(); + match ( + self.default_features.or(self.default_features2), + d.default_features.or(d.default_features2), + ) { + // member: default-features = true and + // workspace: default-features = false should turn on + // default-features + (Some(true), Some(false)) => { + d.default_features = Some(true); + } + // member: default-features = false and + // workspace: default-features = true should ignore member + // default-features + (Some(false), Some(true)) => { + default_features_msg(name, Some(true), cx); + } + // member: default-features = false and + // workspace: dep = "1.0" should ignore member default-features + (Some(false), None) => { + default_features_msg(name, None, cx); + } + _ => {} + } + d.add_features(self.features.clone()); + d.update_optional(self.optional); + TomlDependency::Detailed(d) + } + } + }) } } -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] -pub enum TomlDebugInfo { - None, - LineDirectivesOnly, - LineTablesOnly, - Limited, - Full, -} +impl WorkspaceInherit for TomlWorkspaceDependency { + fn inherit_toml_table(&self) -> &str { + "dependencies" + } -impl Display for TomlDebugInfo { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - TomlDebugInfo::None => f.write_char('0'), - TomlDebugInfo::Limited => f.write_char('1'), - TomlDebugInfo::Full => f.write_char('2'), - TomlDebugInfo::LineDirectivesOnly => f.write_str("line-directives-only"), - TomlDebugInfo::LineTablesOnly => f.write_str("line-tables-only"), - } + fn workspace(&self) -> bool { + self.workspace } } -impl ser::Serialize for TomlDebugInfo { - fn serialize(&self, serializer: S) -> Result - where - S: ser::Serializer, - { +#[derive(Clone, Debug, Serialize)] +#[serde(untagged)] +pub enum TomlDependency { + /// In the simple format, only a version is specified, eg. + /// `package = ""` + Simple(String), + /// The simple format is equivalent to a detailed dependency + /// specifying only a version, eg. + /// `package = { version = "" }` + Detailed(DetailedTomlDependency

), +} + +impl TomlDependency { + fn unused_keys(&self) -> Vec { match self { - Self::None => 0.serialize(serializer), - Self::LineDirectivesOnly => "line-directives-only".serialize(serializer), - Self::LineTablesOnly => "line-tables-only".serialize(serializer), - Self::Limited => 1.serialize(serializer), - Self::Full => 2.serialize(serializer), + TomlDependency::Simple(_) => vec![], + TomlDependency::Detailed(detailed) => detailed.other.keys().cloned().collect(), } } } -impl<'de> de::Deserialize<'de> for TomlDebugInfo { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - use serde::de::Error as _; - let expecting = "a boolean, 0, 1, 2, \"line-tables-only\", or \"line-directives-only\""; - UntaggedEnumVisitor::new() - .expecting(expecting) - .bool(|value| { - Ok(if value { - TomlDebugInfo::Full - } else { - TomlDebugInfo::None - }) - }) - .i64(|value| { - let debuginfo = match value { - 0 => TomlDebugInfo::None, - 1 => TomlDebugInfo::Limited, - 2 => TomlDebugInfo::Full, - _ => { - return Err(serde_untagged::de::Error::invalid_value( - Unexpected::Signed(value), - &expecting, - )) - } - }; - Ok(debuginfo) - }) - .string(|value| { - let debuginfo = match value { - "none" => TomlDebugInfo::None, - "limited" => TomlDebugInfo::Limited, - "full" => TomlDebugInfo::Full, - "line-directives-only" => TomlDebugInfo::LineDirectivesOnly, - "line-tables-only" => TomlDebugInfo::LineTablesOnly, - _ => { - return Err(serde_untagged::de::Error::invalid_value( - Unexpected::Str(value), - &expecting, - )) - } - }; - Ok(debuginfo) - }) - .deserialize(d) +impl TomlDependency

{ + pub(crate) fn to_dependency_split( + &self, + name: &str, + source_id: SourceId, + nested_paths: &mut Vec, + config: &Config, + warnings: &mut Vec, + platform: Option, + root: &Path, + features: &Features, + kind: Option, + ) -> CargoResult { + self.to_dependency( + name, + &mut Context { + deps: &mut Vec::new(), + source_id, + nested_paths, + config, + warnings, + platform, + root, + features, + }, + kind, + ) } -} - -/// A StringOrVec can be parsed from either a TOML string or array, -/// but is always stored as a vector. -#[derive(Clone, Debug, Serialize, Eq, PartialEq, PartialOrd, Ord)] -pub struct StringOrVec(Vec); -impl StringOrVec { - pub fn iter<'a>(&'a self) -> std::slice::Iter<'a, String> { - self.0.iter() + fn to_dependency( + &self, + name: &str, + cx: &mut Context<'_, '_>, + kind: Option, + ) -> CargoResult { + match *self { + TomlDependency::Simple(ref version) => DetailedTomlDependency::

{ + version: Some(version.clone()), + ..Default::default() + } + .to_dependency(name, cx, kind), + TomlDependency::Detailed(ref details) => details.to_dependency(name, cx, kind), + } } -} -impl<'de> de::Deserialize<'de> for StringOrVec { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - UntaggedEnumVisitor::new() - .expecting("string or list of strings") - .string(|value| Ok(StringOrVec(vec![value.to_owned()]))) - .seq(|value| value.deserialize().map(StringOrVec)) - .deserialize(deserializer) + fn is_version_specified(&self) -> bool { + match self { + TomlDependency::Detailed(d) => d.version.is_some(), + TomlDependency::Simple(..) => true, + } } -} -#[derive(Clone, Debug, Serialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum StringOrBool { - String(String), - Bool(bool), + fn is_optional(&self) -> bool { + match self { + TomlDependency::Detailed(d) => d.optional.unwrap_or(false), + TomlDependency::Simple(..) => false, + } + } } -impl<'de> Deserialize<'de> for StringOrBool { +impl<'de, P: Deserialize<'de> + Clone> de::Deserialize<'de> for TomlDependency

{ fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { UntaggedEnumVisitor::new() - .bool(|b| Ok(StringOrBool::Bool(b))) - .string(|s| Ok(StringOrBool::String(s.to_owned()))) + .expecting( + "a version string like \"0.9.8\" or a \ + detailed dependency like { version = \"0.9.8\" }", + ) + .string(|value| Ok(TomlDependency::Simple(value.to_owned()))) + .map(|value| value.deserialize().map(TomlDependency::Detailed)) .deserialize(deserializer) } } -#[derive(PartialEq, Clone, Debug, Serialize)] -#[serde(untagged)] -pub enum VecStringOrBool { - VecString(Vec), - Bool(bool), +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct DetailedTomlDependency { + version: Option, + registry: Option, + /// The URL of the `registry` field. + /// This is an internal implementation detail. When Cargo creates a + /// package, it replaces `registry` with `registry-index` so that the + /// manifest contains the correct URL. All users won't have the same + /// registry names configured, so Cargo can't rely on just the name for + /// crates published by other users. + registry_index: Option, + // `path` is relative to the file it appears in. If that's a `Cargo.toml`, it'll be relative to + // that TOML file, and if it's a `.cargo/config` file, it'll be relative to that file. + path: Option

, + git: Option, + branch: Option, + tag: Option, + rev: Option, + features: Option>, + optional: Option, + default_features: Option, + #[serde(rename = "default_features")] + default_features2: Option, + package: Option, + public: Option, + + /// One or more of `bin`, `cdylib`, `staticlib`, `bin:`. + artifact: Option, + /// If set, the artifact should also be a dependency + lib: Option, + /// A platform name, like `x86_64-apple-darwin` + target: Option, + /// This is here to provide a way to see the "unused manifest keys" when deserializing + #[serde(skip_serializing)] + #[serde(flatten)] + other: BTreeMap, } -impl<'de> de::Deserialize<'de> for VecStringOrBool { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - UntaggedEnumVisitor::new() - .expecting("a boolean or vector of strings") - .bool(|value| Ok(VecStringOrBool::Bool(value))) - .seq(|value| value.deserialize().map(VecStringOrBool::VecString)) - .deserialize(deserializer) +impl DetailedTomlDependency { + fn add_features(&mut self, features: Option>) { + self.features = match (self.features.clone(), features.clone()) { + (Some(dep_feat), Some(inherit_feat)) => Some( + dep_feat + .into_iter() + .chain(inherit_feat) + .collect::>(), + ), + (Some(dep_fet), None) => Some(dep_fet), + (None, Some(inherit_feat)) => Some(inherit_feat), + (None, None) => None, + }; } -} -/// This Trait exists to make [`MaybeWorkspace::Workspace`] generic. It makes deserialization of -/// [`MaybeWorkspace`] much easier, as well as making error messages for -/// [`MaybeWorkspace::resolve`] much nicer -/// -/// Implementors should have a field `workspace` with the type of `bool`. It is used to ensure -/// `workspace` is not `false` in a `Cargo.toml` -pub trait WorkspaceInherit { - /// This is the workspace table that is being inherited from. - /// For example `[workspace.dependencies]` would be the table "dependencies" - fn inherit_toml_table(&self) -> &str; + fn update_optional(&mut self, optional: Option) { + self.optional = optional; + } - /// This is used to output the value of the implementors `workspace` field - fn workspace(&self) -> bool; + fn resolve_path( + &mut self, + name: &str, + root_path: &Path, + package_root: &Path, + ) -> CargoResult<()> { + if let Some(rel_path) = &self.path { + self.path = Some(resolve_relative_path( + name, + root_path, + package_root, + rel_path, + )?) + } + Ok(()) + } } -/// An enum that allows for inheriting keys from a workspace in a Cargo.toml. -#[derive(Serialize, Copy, Clone, Debug)] -#[serde(untagged)] -pub enum MaybeWorkspace { - /// The "defined" type, or the type that that is used when not inheriting from a workspace. - Defined(T), - /// The type when inheriting from a workspace. - Workspace(W), -} +impl DetailedTomlDependency

{ + fn to_dependency( + &self, + name_in_toml: &str, + cx: &mut Context<'_, '_>, + kind: Option, + ) -> CargoResult { + if self.version.is_none() && self.path.is_none() && self.git.is_none() { + let msg = format!( + "dependency ({}) specified without \ + providing a local path, Git repository, version, or \ + workspace dependency to use. This will be considered an \ + error in future versions", + name_in_toml + ); + cx.warnings.push(msg); + } -impl MaybeWorkspace { - fn resolve<'a>( - self, - label: &str, - get_ws_inheritable: impl FnOnce() -> CargoResult, - ) -> CargoResult { - match self { - MaybeWorkspace::Defined(value) => Ok(value), - MaybeWorkspace::Workspace(w) => get_ws_inheritable().with_context(|| { - format!( - "error inheriting `{label}` from workspace root manifest's `workspace.{}.{label}`", - w.inherit_toml_table(), - ) - }), + if let Some(version) = &self.version { + if version.contains('+') { + cx.warnings.push(format!( + "version requirement `{}` for dependency `{}` \ + includes semver metadata which will be ignored, removing the \ + metadata is recommended to avoid confusion", + version, name_in_toml + )); + } } - } - fn resolve_with_self<'a>( - self, - label: &str, - get_ws_inheritable: impl FnOnce(&W) -> CargoResult, - ) -> CargoResult { - match self { - MaybeWorkspace::Defined(value) => Ok(value), - MaybeWorkspace::Workspace(w) => get_ws_inheritable(&w).with_context(|| { - format!( - "error inheriting `{label}` from workspace root manifest's `workspace.{}.{label}`", - w.inherit_toml_table(), - ) - }), + if self.git.is_none() { + let git_only_keys = [ + (&self.branch, "branch"), + (&self.tag, "tag"), + (&self.rev, "rev"), + ]; + + for &(key, key_name) in &git_only_keys { + if key.is_some() { + bail!( + "key `{}` is ignored for dependency ({}).", + key_name, + name_in_toml + ); + } + } } - } - fn as_defined(&self) -> Option<&T> { - match self { - MaybeWorkspace::Workspace(_) => None, - MaybeWorkspace::Defined(defined) => Some(defined), + // Early detection of potentially misused feature syntax + // instead of generating a "feature not found" error. + if let Some(features) = &self.features { + for feature in features { + if feature.contains('/') { + bail!( + "feature `{}` in dependency `{}` is not allowed to contain slashes\n\ + If you want to enable features of a transitive dependency, \ + the direct dependency needs to re-export those features from \ + the `[features]` table.", + feature, + name_in_toml + ); + } + if feature.starts_with("dep:") { + bail!( + "feature `{}` in dependency `{}` is not allowed to use explicit \ + `dep:` syntax\n\ + If you want to enable an optional dependency, specify the name \ + of the optional dependency without the `dep:` prefix, or specify \ + a feature from the dependency's `[features]` table that enables \ + the optional dependency.", + feature, + name_in_toml + ); + } + } } - } -} - -type MaybeWorkspaceDependency = MaybeWorkspace; -impl MaybeWorkspaceDependency { - fn unused_keys(&self) -> Vec { - match self { - MaybeWorkspaceDependency::Defined(d) => d.unused_keys(), - MaybeWorkspaceDependency::Workspace(w) => w.other.keys().cloned().collect(), - } - } -} + let new_source_id = match ( + self.git.as_ref(), + self.path.as_ref(), + self.registry.as_ref(), + self.registry_index.as_ref(), + ) { + (Some(_), _, Some(_), _) | (Some(_), _, _, Some(_)) => bail!( + "dependency ({}) specification is ambiguous. \ + Only one of `git` or `registry` is allowed.", + name_in_toml + ), + (_, _, Some(_), Some(_)) => bail!( + "dependency ({}) specification is ambiguous. \ + Only one of `registry` or `registry-index` is allowed.", + name_in_toml + ), + (Some(git), maybe_path, _, _) => { + if maybe_path.is_some() { + bail!( + "dependency ({}) specification is ambiguous. \ + Only one of `git` or `path` is allowed.", + name_in_toml + ); + } -impl<'de> de::Deserialize<'de> for MaybeWorkspaceDependency { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let value = serde_value::Value::deserialize(deserializer)?; + let n_details = [&self.branch, &self.tag, &self.rev] + .iter() + .filter(|d| d.is_some()) + .count(); - if let Ok(w) = TomlWorkspaceDependency::deserialize(serde_value::ValueDeserializer::< - D::Error, - >::new(value.clone())) - { - return if w.workspace() { - Ok(MaybeWorkspace::Workspace(w)) - } else { - Err(de::Error::custom("`workspace` cannot be false")) - }; - } - TomlDependency::deserialize(serde_value::ValueDeserializer::::new(value)) - .map(MaybeWorkspace::Defined) - } -} + if n_details > 1 { + bail!( + "dependency ({}) specification is ambiguous. \ + Only one of `branch`, `tag` or `rev` is allowed.", + name_in_toml + ); + } -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -pub struct TomlWorkspaceDependency { - workspace: bool, - features: Option>, - default_features: Option, - #[serde(rename = "default_features")] - default_features2: Option, - optional: Option, - /// This is here to provide a way to see the "unused manifest keys" when deserializing - #[serde(skip_serializing)] - #[serde(flatten)] - other: BTreeMap, -} + let reference = self + .branch + .clone() + .map(GitReference::Branch) + .or_else(|| self.tag.clone().map(GitReference::Tag)) + .or_else(|| self.rev.clone().map(GitReference::Rev)) + .unwrap_or(GitReference::DefaultBranch); + let loc = git.into_url()?; -impl TomlWorkspaceDependency { - fn resolve<'a>( - &self, - name: &str, - inheritable: impl FnOnce() -> CargoResult<&'a InheritableFields>, - cx: &mut Context<'_, '_>, - ) -> CargoResult { - fn default_features_msg(label: &str, ws_def_feat: Option, cx: &mut Context<'_, '_>) { - let ws_def_feat = match ws_def_feat { - Some(true) => "true", - Some(false) => "false", - None => "not specified", - }; - cx.warnings.push(format!( - "`default-features` is ignored for {label}, since `default-features` was \ - {ws_def_feat} for `workspace.dependencies.{label}`, \ - this could become a hard error in the future" - )) - } - if self.default_features.is_some() && self.default_features2.is_some() { - warn_on_deprecated("default-features", name, "dependency", cx.warnings); - } - inheritable()?.get_dependency(name, cx.root).map(|d| { - match d { - TomlDependency::Simple(s) => { - if let Some(false) = self.default_features.or(self.default_features2) { - default_features_msg(name, None, cx); - } - if self.optional.is_some() || self.features.is_some() { - TomlDependency::Detailed(DetailedTomlDependency { - version: Some(s), - optional: self.optional, - features: self.features.clone(), - ..Default::default() - }) - } else { - TomlDependency::Simple(s) - } + if let Some(fragment) = loc.fragment() { + let msg = format!( + "URL fragment `#{}` in git URL is ignored for dependency ({}). \ + If you were trying to specify a specific git revision, \ + use `rev = \"{}\"` in the dependency declaration.", + fragment, name_in_toml, fragment + ); + cx.warnings.push(msg) } - TomlDependency::Detailed(d) => { - let mut d = d.clone(); - match ( - self.default_features.or(self.default_features2), - d.default_features.or(d.default_features2), - ) { - // member: default-features = true and - // workspace: default-features = false should turn on - // default-features - (Some(true), Some(false)) => { - d.default_features = Some(true); - } - // member: default-features = false and - // workspace: default-features = true should ignore member - // default-features - (Some(false), Some(true)) => { - default_features_msg(name, Some(true), cx); - } - // member: default-features = false and - // workspace: dep = "1.0" should ignore member default-features - (Some(false), None) => { - default_features_msg(name, None, cx); - } - _ => {} - } - d.add_features(self.features.clone()); - d.update_optional(self.optional); - TomlDependency::Detailed(d) + + SourceId::for_git(&loc, reference)? + } + (None, Some(path), _, _) => { + let path = path.resolve(cx.config); + cx.nested_paths.push(path.clone()); + // If the source ID for the package we're parsing is a path + // source, then we normalize the path here to get rid of + // components like `..`. + // + // The purpose of this is to get a canonical ID for the package + // that we're depending on to ensure that builds of this package + // always end up hashing to the same value no matter where it's + // built from. + if cx.source_id.is_path() { + let path = cx.root.join(path); + let path = paths::normalize_path(&path); + SourceId::for_path(&path)? + } else { + cx.source_id } } - }) - } -} - -impl WorkspaceInherit for TomlWorkspaceDependency { - fn inherit_toml_table(&self) -> &str { - "dependencies" - } + (None, None, Some(registry), None) => SourceId::alt_registry(cx.config, registry)?, + (None, None, None, Some(registry_index)) => { + let url = registry_index.into_url()?; + SourceId::for_registry(&url)? + } + (None, None, None, None) => SourceId::crates_io(cx.config)?, + }; - fn workspace(&self) -> bool { - self.workspace - } -} + let (pkg_name, explicit_name_in_toml) = match self.package { + Some(ref s) => (&s[..], Some(name_in_toml)), + None => (name_in_toml, None), + }; -//. This already has a `Deserialize` impl from version_trim_whitespace -type MaybeWorkspaceSemverVersion = MaybeWorkspace; + let version = self.version.as_deref(); + let mut dep = Dependency::parse(pkg_name, version, new_source_id)?; + if self.default_features.is_some() && self.default_features2.is_some() { + warn_on_deprecated("default-features", name_in_toml, "dependency", cx.warnings); + } + dep.set_features(self.features.iter().flatten()) + .set_default_features( + self.default_features + .or(self.default_features2) + .unwrap_or(true), + ) + .set_optional(self.optional.unwrap_or(false)) + .set_platform(cx.platform.clone()); + if let Some(registry) = &self.registry { + let registry_id = SourceId::alt_registry(cx.config, registry)?; + dep.set_registry_id(registry_id); + } + if let Some(registry_index) = &self.registry_index { + let url = registry_index.into_url()?; + let registry_id = SourceId::for_registry(&url)?; + dep.set_registry_id(registry_id); + } -type MaybeWorkspaceString = MaybeWorkspace; -impl<'de> de::Deserialize<'de> for MaybeWorkspaceString { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - struct Visitor; + if let Some(kind) = kind { + dep.set_kind(kind); + } + if let Some(name_in_toml) = explicit_name_in_toml { + dep.set_explicit_name_in_toml(name_in_toml); + } - impl<'de> de::Visitor<'de> for Visitor { - type Value = MaybeWorkspaceString; + if let Some(p) = self.public { + cx.features.require(Feature::public_dependency())?; - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - f.write_str("a string or workspace") + if dep.kind() != DepKind::Normal { + bail!("'public' specifier can only be used on regular dependencies, not {:?} dependencies", dep.kind()); } - fn visit_string(self, value: String) -> Result - where - E: de::Error, - { - Ok(MaybeWorkspaceString::Defined(value)) - } + dep.set_public(p); + } - fn visit_map(self, map: V) -> Result - where - V: de::MapAccess<'de>, - { - let mvd = de::value::MapAccessDeserializer::new(map); - TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) + if let (Some(artifact), is_lib, target) = ( + self.artifact.as_ref(), + self.lib.unwrap_or(false), + self.target.as_deref(), + ) { + if cx.config.cli_unstable().bindeps { + let artifact = Artifact::parse(&artifact.0, is_lib, target)?; + if dep.kind() != DepKind::Build + && artifact.target() == Some(ArtifactTarget::BuildDependencyAssumeTarget) + { + bail!( + r#"`target = "target"` in normal- or dev-dependencies has no effect ({})"#, + name_in_toml + ); + } + dep.set_artifact(artifact) + } else { + bail!("`artifact = …` requires `-Z bindeps` ({})", name_in_toml); + } + } else if self.lib.is_some() || self.target.is_some() { + for (is_set, specifier) in [ + (self.lib.is_some(), "lib"), + (self.target.is_some(), "target"), + ] { + if !is_set { + continue; + } + bail!( + "'{}' specifier cannot be used without an 'artifact = …' value ({})", + specifier, + name_in_toml + ) } } - - d.deserialize_any(Visitor) + Ok(dep) } } -type MaybeWorkspaceRustVersion = MaybeWorkspace; -impl<'de> de::Deserialize<'de> for MaybeWorkspaceRustVersion { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - struct Visitor; +// Explicit implementation so we avoid pulling in P: Default +impl Default for DetailedTomlDependency

{ + fn default() -> Self { + Self { + version: Default::default(), + registry: Default::default(), + registry_index: Default::default(), + path: Default::default(), + git: Default::default(), + branch: Default::default(), + tag: Default::default(), + rev: Default::default(), + features: Default::default(), + optional: Default::default(), + default_features: Default::default(), + default_features2: Default::default(), + package: Default::default(), + public: Default::default(), + artifact: Default::default(), + lib: Default::default(), + target: Default::default(), + other: Default::default(), + } + } +} - impl<'de> de::Visitor<'de> for Visitor { - type Value = MaybeWorkspaceRustVersion; +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +pub struct TomlProfiles(BTreeMap); - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - f.write_str("a semver or workspace") - } +impl TomlProfiles { + pub fn get_all(&self) -> &BTreeMap { + &self.0 + } - fn visit_string(self, value: String) -> Result - where - E: de::Error, - { - let value = value.parse::().map_err(|e| E::custom(e))?; - Ok(MaybeWorkspaceRustVersion::Defined(value)) - } + pub fn get(&self, name: &str) -> Option<&TomlProfile> { + self.0.get(name) + } - fn visit_map(self, map: V) -> Result - where - V: de::MapAccess<'de>, - { - let mvd = de::value::MapAccessDeserializer::new(map); - TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) - } + /// Checks syntax validity and unstable feature gate for each profile. + /// + /// It's a bit unfortunate both `-Z` flags and `cargo-features` are required, + /// because profiles can now be set in either `Cargo.toml` or `config.toml`. + pub fn validate( + &self, + cli_unstable: &CliUnstable, + features: &Features, + warnings: &mut Vec, + ) -> CargoResult<()> { + for (name, profile) in &self.0 { + profile.validate(name, cli_unstable, features, warnings)?; } - - d.deserialize_any(Visitor) + Ok(()) } } -type MaybeWorkspaceVecString = MaybeWorkspace, TomlWorkspaceField>; -impl<'de> de::Deserialize<'de> for MaybeWorkspaceVecString { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - struct Visitor; - - impl<'de> de::Visitor<'de> for Visitor { - type Value = MaybeWorkspaceVecString; - - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - f.write_str("a vector of strings or workspace") - } - fn visit_seq(self, v: A) -> Result - where - A: de::SeqAccess<'de>, - { - let seq = de::value::SeqAccessDeserializer::new(v); - Vec::deserialize(seq).map(MaybeWorkspace::Defined) - } +#[derive(Deserialize, Serialize, Clone, Debug, Default, Eq, PartialEq)] +#[serde(default, rename_all = "kebab-case")] +pub struct TomlProfile { + pub opt_level: Option, + pub lto: Option, + pub codegen_backend: Option, + pub codegen_units: Option, + pub debug: Option, + pub split_debuginfo: Option, + pub debug_assertions: Option, + pub rpath: Option, + pub panic: Option, + pub overflow_checks: Option, + pub incremental: Option, + pub dir_name: Option, + pub inherits: Option, + pub strip: Option, + // Note that `rustflags` is used for the cargo-feature `profile_rustflags` + pub rustflags: Option>, + // These two fields must be last because they are sub-tables, and TOML + // requires all non-tables to be listed first. + pub package: Option>, + pub build_override: Option>, +} - fn visit_map(self, map: V) -> Result - where - V: de::MapAccess<'de>, - { - let mvd = de::value::MapAccessDeserializer::new(map); - TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) +impl TomlProfile { + /// Checks stytax validity and unstable feature gate for a given profile. + pub fn validate( + &self, + name: &str, + cli_unstable: &CliUnstable, + features: &Features, + warnings: &mut Vec, + ) -> CargoResult<()> { + self.validate_profile(name, cli_unstable, features)?; + if let Some(ref profile) = self.build_override { + profile.validate_override("build-override")?; + profile.validate_profile(&format!("{name}.build-override"), cli_unstable, features)?; + } + if let Some(ref packages) = self.package { + for (override_name, profile) in packages { + profile.validate_override("package")?; + profile.validate_profile( + &format!("{name}.package.{override_name}"), + cli_unstable, + features, + )?; } } - d.deserialize_any(Visitor) - } -} + // Profile name validation + Self::validate_name(name)?; -type MaybeWorkspaceStringOrBool = MaybeWorkspace; -impl<'de> de::Deserialize<'de> for MaybeWorkspaceStringOrBool { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - struct Visitor; + if let Some(dir_name) = self.dir_name { + // This is disabled for now, as we would like to stabilize named + // profiles without this, and then decide in the future if it is + // needed. This helps simplify the UI a little. + bail!( + "dir-name=\"{}\" in profile `{}` is not currently allowed, \ + directory names are tied to the profile name for custom profiles", + dir_name, + name + ); + } - impl<'de> de::Visitor<'de> for Visitor { - type Value = MaybeWorkspaceStringOrBool; + // `inherits` validation + if matches!(self.inherits.map(|s| s.as_str()), Some("debug")) { + bail!( + "profile.{}.inherits=\"debug\" should be profile.{}.inherits=\"dev\"", + name, + name + ); + } - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - f.write_str("a string, a bool, or workspace") + match name { + "doc" => { + warnings.push("profile `doc` is deprecated and has no effect".to_string()); } - - fn visit_bool(self, v: bool) -> Result - where - E: de::Error, - { - let b = de::value::BoolDeserializer::new(v); - StringOrBool::deserialize(b).map(MaybeWorkspace::Defined) + "test" | "bench" => { + if self.panic.is_some() { + warnings.push(format!("`panic` setting is ignored for `{}` profile", name)) + } } + _ => {} + } - fn visit_string(self, v: String) -> Result - where - E: de::Error, - { - let string = de::value::StringDeserializer::new(v); - StringOrBool::deserialize(string).map(MaybeWorkspace::Defined) + if let Some(panic) = &self.panic { + if panic != "unwind" && panic != "abort" { + bail!( + "`panic` setting of `{}` is not a valid setting, \ + must be `unwind` or `abort`", + panic + ); } + } - fn visit_map(self, map: V) -> Result - where - V: de::MapAccess<'de>, - { - let mvd = de::value::MapAccessDeserializer::new(map); - TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) + if let Some(StringOrBool::String(arg)) = &self.lto { + if arg == "true" || arg == "false" { + bail!( + "`lto` setting of string `\"{arg}\"` for `{name}` profile is not \ + a valid setting, must be a boolean (`true`/`false`) or a string \ + (`\"thin\"`/`\"fat\"`/`\"off\"`) or omitted.", + ); } } - d.deserialize_any(Visitor) + Ok(()) } -} -type MaybeWorkspaceVecStringOrBool = MaybeWorkspace; -impl<'de> de::Deserialize<'de> for MaybeWorkspaceVecStringOrBool { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - struct Visitor; + /// Validate dir-names and profile names according to RFC 2678. + pub fn validate_name(name: &str) -> CargoResult<()> { + if let Some(ch) = name + .chars() + .find(|ch| !ch.is_alphanumeric() && *ch != '_' && *ch != '-') + { + bail!( + "invalid character `{}` in profile name `{}`\n\ + Allowed characters are letters, numbers, underscore, and hyphen.", + ch, + name + ); + } - impl<'de> de::Visitor<'de> for Visitor { - type Value = MaybeWorkspaceVecStringOrBool; + const SEE_DOCS: &str = "See https://doc.rust-lang.org/cargo/reference/profiles.html \ + for more on configuring profiles."; - fn expecting(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - f.write_str("a boolean, a vector of strings, or workspace") - } + let lower_name = name.to_lowercase(); + if lower_name == "debug" { + bail!( + "profile name `{}` is reserved\n\ + To configure the default development profile, use the name `dev` \ + as in [profile.dev]\n\ + {}", + name, + SEE_DOCS + ); + } + if lower_name == "build-override" { + bail!( + "profile name `{}` is reserved\n\ + To configure build dependency settings, use [profile.dev.build-override] \ + and [profile.release.build-override]\n\ + {}", + name, + SEE_DOCS + ); + } - fn visit_bool(self, v: bool) -> Result - where - E: de::Error, - { - let b = de::value::BoolDeserializer::new(v); - VecStringOrBool::deserialize(b).map(MaybeWorkspace::Defined) - } + // These are some arbitrary reservations. We have no plans to use + // these, but it seems safer to reserve a few just in case we want to + // add more built-in profiles in the future. We can also uses special + // syntax like cargo:foo if needed. But it is unlikely these will ever + // be used. + if matches!( + lower_name.as_str(), + "build" + | "check" + | "clean" + | "config" + | "fetch" + | "fix" + | "install" + | "metadata" + | "package" + | "publish" + | "report" + | "root" + | "run" + | "rust" + | "rustc" + | "rustdoc" + | "target" + | "tmp" + | "uninstall" + ) || lower_name.starts_with("cargo") + { + bail!( + "profile name `{}` is reserved\n\ + Please choose a different name.\n\ + {}", + name, + SEE_DOCS + ); + } - fn visit_seq(self, v: A) -> Result - where - A: de::SeqAccess<'de>, - { - let seq = de::value::SeqAccessDeserializer::new(v); - VecStringOrBool::deserialize(seq).map(MaybeWorkspace::Defined) + Ok(()) + } + + /// Validates a profile. + /// + /// This is a shallow check, which is reused for the profile itself and any overrides. + fn validate_profile( + &self, + name: &str, + cli_unstable: &CliUnstable, + features: &Features, + ) -> CargoResult<()> { + if let Some(codegen_backend) = &self.codegen_backend { + match ( + features.require(Feature::codegen_backend()), + cli_unstable.codegen_backend, + ) { + (Err(e), false) => return Err(e), + _ => {} } - fn visit_map(self, map: V) -> Result - where - V: de::MapAccess<'de>, - { - let mvd = de::value::MapAccessDeserializer::new(map); - TomlWorkspaceField::deserialize(mvd).map(MaybeWorkspace::Workspace) + if codegen_backend.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_') { + bail!( + "`profile.{}.codegen-backend` setting of `{}` is not a valid backend name.", + name, + codegen_backend, + ); + } + } + if self.rustflags.is_some() { + match ( + features.require(Feature::profile_rustflags()), + cli_unstable.profile_rustflags, + ) { + (Err(e), false) => return Err(e), + _ => {} } } + Ok(()) + } - d.deserialize_any(Visitor) + /// Validation that is specific to an override. + fn validate_override(&self, which: &str) -> CargoResult<()> { + if self.package.is_some() { + bail!("package-specific profiles cannot be nested"); + } + if self.build_override.is_some() { + bail!("build-override profiles cannot be nested"); + } + if self.panic.is_some() { + bail!("`panic` may not be specified in a `{}` profile", which) + } + if self.lto.is_some() { + bail!("`lto` may not be specified in a `{}` profile", which) + } + if self.rpath.is_some() { + bail!("`rpath` may not be specified in a `{}` profile", which) + } + Ok(()) } -} -type MaybeWorkspaceBtreeMap = - MaybeWorkspace>, TomlWorkspaceField>; + /// Overwrite self's values with the given profile. + pub fn merge(&mut self, profile: &TomlProfile) { + if let Some(v) = &profile.opt_level { + self.opt_level = Some(v.clone()); + } -impl<'de> de::Deserialize<'de> for MaybeWorkspaceBtreeMap { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let value = serde_value::Value::deserialize(deserializer)?; + if let Some(v) = &profile.lto { + self.lto = Some(v.clone()); + } - if let Ok(w) = TomlWorkspaceField::deserialize( - serde_value::ValueDeserializer::::new(value.clone()), - ) { - return if w.workspace() { - Ok(MaybeWorkspace::Workspace(w)) - } else { - Err(de::Error::custom("`workspace` cannot be false")) - }; + if let Some(v) = profile.codegen_backend { + self.codegen_backend = Some(v); } - BTreeMap::deserialize(serde_value::ValueDeserializer::::new(value)) - .map(MaybeWorkspace::Defined) - } -} -#[derive(Deserialize, Serialize, Copy, Clone, Debug)] -pub struct TomlWorkspaceField { - #[serde(deserialize_with = "bool_no_false")] - workspace: bool, -} + if let Some(v) = profile.codegen_units { + self.codegen_units = Some(v); + } -impl WorkspaceInherit for TomlWorkspaceField { - fn inherit_toml_table(&self) -> &str { - "package" - } + if let Some(v) = profile.debug { + self.debug = Some(v); + } - fn workspace(&self) -> bool { - self.workspace - } -} + if let Some(v) = profile.debug_assertions { + self.debug_assertions = Some(v); + } -fn bool_no_false<'de, D: de::Deserializer<'de>>(deserializer: D) -> Result { - let b: bool = Deserialize::deserialize(deserializer)?; - if b { - Ok(b) - } else { - Err(de::Error::custom("`workspace` cannot be false")) - } -} + if let Some(v) = &profile.split_debuginfo { + self.split_debuginfo = Some(v.clone()); + } + + if let Some(v) = profile.rpath { + self.rpath = Some(v); + } -/// Represents the `package`/`project` sections of a `Cargo.toml`. -/// -/// Note that the order of the fields matters, since this is the order they -/// are serialized to a TOML file. For example, you cannot have values after -/// the field `metadata`, since it is a table and values cannot appear after -/// tables. -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "kebab-case")] -pub struct TomlPackage { - edition: Option, - rust_version: Option, - name: InternedString, - #[serde(deserialize_with = "version_trim_whitespace")] - version: MaybeWorkspaceSemverVersion, - authors: Option, - build: Option, - metabuild: Option, - default_target: Option, - forced_target: Option, - links: Option, - exclude: Option, - include: Option, - publish: Option, - workspace: Option, - im_a_teapot: Option, - autobins: Option, - autoexamples: Option, - autotests: Option, - autobenches: Option, - default_run: Option, + if let Some(v) = &profile.panic { + self.panic = Some(v.clone()); + } - // Package metadata. - description: Option, - homepage: Option, - documentation: Option, - readme: Option, - keywords: Option, - categories: Option, - license: Option, - license_file: Option, - repository: Option, - resolver: Option, + if let Some(v) = profile.overflow_checks { + self.overflow_checks = Some(v); + } - // Provide a helpful error message for a common user error. - #[serde(rename = "cargo-features", skip_serializing)] - _invalid_cargo_features: Option, + if let Some(v) = profile.incremental { + self.incremental = Some(v); + } - // Note that this field must come last due to the way toml serialization - // works which requires tables to be emitted after all values. - metadata: Option, -} + if let Some(v) = &profile.rustflags { + self.rustflags = Some(v.clone()); + } -impl TomlPackage { - pub fn to_package_id( - &self, - source_id: SourceId, - version: semver::Version, - ) -> CargoResult { - PackageId::new(self.name, version, source_id) - } -} + if let Some(other_package) = &profile.package { + match &mut self.package { + Some(self_package) => { + for (spec, other_pkg_profile) in other_package { + match self_package.get_mut(spec) { + Some(p) => p.merge(other_pkg_profile), + None => { + self_package.insert(spec.clone(), other_pkg_profile.clone()); + } + } + } + } + None => self.package = Some(other_package.clone()), + } + } -fn version_trim_whitespace<'de, D>(deserializer: D) -> Result -where - D: de::Deserializer<'de>, -{ - UntaggedEnumVisitor::new() - .expecting("SemVer version") - .string( - |value| match value.trim().parse().map_err(de::Error::custom) { - Ok(parsed) => Ok(MaybeWorkspace::Defined(parsed)), - Err(e) => Err(e), - }, - ) - .map(|value| value.deserialize().map(MaybeWorkspace::Workspace)) - .deserialize(deserializer) -} + if let Some(other_bo) = &profile.build_override { + match &mut self.build_override { + Some(self_bo) => self_bo.merge(other_bo), + None => self.build_override = Some(other_bo.clone()), + } + } -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct TomlWorkspace { - members: Option>, - #[serde(rename = "default-members")] - default_members: Option>, - exclude: Option>, - resolver: Option, + if let Some(v) = &profile.inherits { + self.inherits = Some(*v); + } - // Properties that can be inherited by members. - package: Option, - dependencies: Option>, - lints: Option, + if let Some(v) = &profile.dir_name { + self.dir_name = Some(*v); + } - // Note that this field must come last due to the way toml serialization - // works which requires tables to be emitted after all values. - metadata: Option, + if let Some(v) = &profile.strip { + self.strip = Some(v.clone()); + } + } } -/// A group of fields that are inheritable by members of the workspace -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub struct InheritableFields { - // We use skip here since it will never be present when deserializing - // and we don't want it present when serializing - #[serde(skip)] - dependencies: Option>, - #[serde(skip)] - lints: Option, - - version: Option, - authors: Option>, - description: Option, - homepage: Option, - documentation: Option, - readme: Option, - keywords: Option>, - categories: Option>, - license: Option, - #[serde(rename = "license-file")] - license_file: Option, - repository: Option, - publish: Option, - edition: Option, - badges: Option>>, - exclude: Option>, - include: Option>, - #[serde(rename = "rust-version")] - rust_version: Option, - // We use skip here since it will never be present when deserializing - // and we don't want it present when serializing - #[serde(skip)] - ws_root: PathBuf, +#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub enum ProfilePackageSpec { + Spec(PackageIdSpec), + All, } -/// Defines simple getter methods for inheritable fields. -macro_rules! inheritable_field_getter { - ( $(($key:literal, $field:ident -> $ret:ty),)* ) => ( - $( - #[doc = concat!("Gets the field `workspace.", $key, "`.")] - pub fn $field(&self) -> CargoResult<$ret> { - let Some(val) = &self.$field else { - bail!("`workspace.{}` was not defined", $key); - }; - Ok(val.clone()) - } - )* - ) +impl fmt::Display for ProfilePackageSpec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProfilePackageSpec::Spec(spec) => spec.fmt(f), + ProfilePackageSpec::All => f.write_str("*"), + } + } } -impl InheritableFields { - inheritable_field_getter! { - // Please keep this list lexicographically ordered. - ("dependencies", dependencies -> BTreeMap), - ("lints", lints -> TomlLints), - ("package.authors", authors -> Vec), - ("package.badges", badges -> BTreeMap>), - ("package.categories", categories -> Vec), - ("package.description", description -> String), - ("package.documentation", documentation -> String), - ("package.edition", edition -> String), - ("package.exclude", exclude -> Vec), - ("package.homepage", homepage -> String), - ("package.include", include -> Vec), - ("package.keywords", keywords -> Vec), - ("package.license", license -> String), - ("package.publish", publish -> VecStringOrBool), - ("package.repository", repository -> String), - ("package.rust-version", rust_version -> RustVersion), - ("package.version", version -> semver::Version), +impl ser::Serialize for ProfilePackageSpec { + fn serialize(&self, s: S) -> Result + where + S: ser::Serializer, + { + self.to_string().serialize(s) } +} - /// Gets a workspace dependency with the `name`. - pub fn get_dependency(&self, name: &str, package_root: &Path) -> CargoResult { - let Some(deps) = &self.dependencies else { - bail!("`workspace.dependencies` was not defined"); - }; - let Some(dep) = deps.get(name) else { - bail!("`dependency.{name}` was not found in `workspace.dependencies`"); - }; - let mut dep = dep.clone(); - if let TomlDependency::Detailed(detailed) = &mut dep { - detailed.resolve_path(name, self.ws_root(), package_root)?; +impl<'de> de::Deserialize<'de> for ProfilePackageSpec { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + let string = String::deserialize(d)?; + if string == "*" { + Ok(ProfilePackageSpec::All) + } else { + PackageIdSpec::parse(&string) + .map_err(de::Error::custom) + .map(ProfilePackageSpec::Spec) } - Ok(dep) } +} - /// Gets the field `workspace.package.license-file`. - pub fn license_file(&self, package_root: &Path) -> CargoResult { - let Some(license_file) = &self.license_file else { - bail!("`workspace.package.license-file` was not defined"); - }; - resolve_relative_path("license-file", &self.ws_root, package_root, license_file) +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TomlOptLevel(pub String); + +impl ser::Serialize for TomlOptLevel { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + match self.0.parse::() { + Ok(n) => n.serialize(serializer), + Err(_) => self.0.serialize(serializer), + } } +} - /// Gets the field `workspace.package.readme`. - pub fn readme(&self, package_root: &Path) -> CargoResult { - let Some(readme) = readme_for_package(self.ws_root.as_path(), self.readme.as_ref()) else { - bail!("`workspace.package.readme` was not defined"); - }; - resolve_relative_path("readme", &self.ws_root, package_root, &readme) - .map(StringOrBool::String) +impl<'de> de::Deserialize<'de> for TomlOptLevel { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + use serde::de::Error as _; + UntaggedEnumVisitor::new() + .expecting("an optimization level") + .i64(|value| Ok(TomlOptLevel(value.to_string()))) + .string(|value| { + if value == "s" || value == "z" { + Ok(TomlOptLevel(value.to_string())) + } else { + Err(serde_untagged::de::Error::custom(format!( + "must be `0`, `1`, `2`, `3`, `s` or `z`, \ + but found the string: \"{}\"", + value + ))) + } + }) + .deserialize(d) } +} - pub fn ws_root(&self) -> &PathBuf { - &self.ws_root - } +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub enum TomlDebugInfo { + None, + LineDirectivesOnly, + LineTablesOnly, + Limited, + Full, +} - pub fn update_deps(&mut self, deps: Option>) { - self.dependencies = deps; +impl Display for TomlDebugInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TomlDebugInfo::None => f.write_char('0'), + TomlDebugInfo::Limited => f.write_char('1'), + TomlDebugInfo::Full => f.write_char('2'), + TomlDebugInfo::LineDirectivesOnly => f.write_str("line-directives-only"), + TomlDebugInfo::LineTablesOnly => f.write_str("line-tables-only"), + } } +} - pub fn update_lints(&mut self, lints: Option) { - self.lints = lints; +impl ser::Serialize for TomlDebugInfo { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + match self { + Self::None => 0.serialize(serializer), + Self::LineDirectivesOnly => "line-directives-only".serialize(serializer), + Self::LineTablesOnly => "line-tables-only".serialize(serializer), + Self::Limited => 1.serialize(serializer), + Self::Full => 2.serialize(serializer), + } } +} - pub fn update_ws_path(&mut self, ws_root: PathBuf) { - self.ws_root = ws_root; +impl<'de> de::Deserialize<'de> for TomlDebugInfo { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + use serde::de::Error as _; + let expecting = "a boolean, 0, 1, 2, \"line-tables-only\", or \"line-directives-only\""; + UntaggedEnumVisitor::new() + .expecting(expecting) + .bool(|value| { + Ok(if value { + TomlDebugInfo::Full + } else { + TomlDebugInfo::None + }) + }) + .i64(|value| { + let debuginfo = match value { + 0 => TomlDebugInfo::None, + 1 => TomlDebugInfo::Limited, + 2 => TomlDebugInfo::Full, + _ => { + return Err(serde_untagged::de::Error::invalid_value( + Unexpected::Signed(value), + &expecting, + )) + } + }; + Ok(debuginfo) + }) + .string(|value| { + let debuginfo = match value { + "none" => TomlDebugInfo::None, + "limited" => TomlDebugInfo::Limited, + "full" => TomlDebugInfo::Full, + "line-directives-only" => TomlDebugInfo::LineDirectivesOnly, + "line-tables-only" => TomlDebugInfo::LineTablesOnly, + _ => { + return Err(serde_untagged::de::Error::invalid_value( + Unexpected::Str(value), + &expecting, + )) + } + }; + Ok(debuginfo) + }) + .deserialize(d) } } +type TomlLibTarget = TomlTarget; +type TomlBinTarget = TomlTarget; +type TomlExampleTarget = TomlTarget; +type TomlTestTarget = TomlTarget; +type TomlBenchTarget = TomlTarget; + #[derive(Default, Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] struct TomlTarget { @@ -3304,33 +3225,6 @@ impl TomlTarget { } } -#[derive(Clone)] -struct PathValue(PathBuf); - -impl fmt::Debug for PathValue { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl ser::Serialize for PathValue { - fn serialize(&self, serializer: S) -> Result - where - S: ser::Serializer, - { - self.0.serialize(serializer) - } -} - -impl<'de> de::Deserialize<'de> for PathValue { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - Ok(PathValue(String::deserialize(deserializer)?.into())) - } -} - /// Corresponds to a `target` entry, but `TomlTarget` is already used. #[derive(Serialize, Deserialize, Debug, Clone)] struct TomlPlatform { @@ -3462,3 +3356,109 @@ impl<'de> de::Deserialize<'de> for InvalidCargoFeatures { )) } } + +pub trait ResolveToPath { + fn resolve(&self, config: &Config) -> PathBuf; +} + +impl ResolveToPath for String { + fn resolve(&self, _: &Config) -> PathBuf { + self.into() + } +} + +impl ResolveToPath for ConfigRelativePath { + fn resolve(&self, c: &Config) -> PathBuf { + self.resolve_path(c) + } +} + +/// A StringOrVec can be parsed from either a TOML string or array, +/// but is always stored as a vector. +#[derive(Clone, Debug, Serialize, Eq, PartialEq, PartialOrd, Ord)] +pub struct StringOrVec(Vec); + +impl StringOrVec { + pub fn iter<'a>(&'a self) -> std::slice::Iter<'a, String> { + self.0.iter() + } +} + +impl<'de> de::Deserialize<'de> for StringOrVec { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .expecting("string or list of strings") + .string(|value| Ok(StringOrVec(vec![value.to_owned()]))) + .seq(|value| value.deserialize().map(StringOrVec)) + .deserialize(deserializer) + } +} + +#[derive(Clone, Debug, Serialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum StringOrBool { + String(String), + Bool(bool), +} + +impl<'de> Deserialize<'de> for StringOrBool { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .bool(|b| Ok(StringOrBool::Bool(b))) + .string(|s| Ok(StringOrBool::String(s.to_owned()))) + .deserialize(deserializer) + } +} + +#[derive(PartialEq, Clone, Debug, Serialize)] +#[serde(untagged)] +pub enum VecStringOrBool { + VecString(Vec), + Bool(bool), +} + +impl<'de> de::Deserialize<'de> for VecStringOrBool { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .expecting("a boolean or vector of strings") + .bool(|value| Ok(VecStringOrBool::Bool(value))) + .seq(|value| value.deserialize().map(VecStringOrBool::VecString)) + .deserialize(deserializer) + } +} + +#[derive(Clone)] +struct PathValue(PathBuf); + +impl fmt::Debug for PathValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl ser::Serialize for PathValue { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + self.0.serialize(serializer) + } +} + +impl<'de> de::Deserialize<'de> for PathValue { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + Ok(PathValue(String::deserialize(deserializer)?.into())) + } +}