| use crate::util::data_structures::HashMap; | |
| use cargo_util_terminal::report::{AnnotationKind, Group, Level, Snippet}; | |
| use std::borrow::Cow; | |
| use std::cell::OnceCell; | |
| use std::collections::{BTreeMap, BTreeSet}; | |
| use std::ffi::OsStr; | |
| use std::path::{Path, PathBuf}; | |
| use std::rc::Rc; | |
| use std::str::{self, FromStr}; | |
| use std::sync::Arc; | |
| use crate::AlreadyPrintedError; | |
| use crate::workspace::summary::MissingDependencyError; | |
| use anyhow::{Context as _, anyhow, bail}; | |
| use cargo_platform::Platform; | |
| use cargo_util::paths; | |
| use cargo_util_schemas::manifest::{ | |
| self, FeatureDefinition, FeatureMetadata, FeatureName, PackageName, PathBaseName, | |
| TomlDependency, TomlDetailedDependency, TomlManifest, TomlPackageBuild, TomlWorkspace, | |
| }; | |
| use cargo_util_schemas::manifest::{RustVersion, StringOrBool}; | |
| use itertools::Itertools; | |
| use pathdiff::diff_paths; | |
| use url::Url; | |
| use crate::compiler::{CompileKind, CompileTarget}; | |
| use crate::context::{ConfigRelativePath, TOP_LEVEL_CONFIG_KEYS}; | |
| use crate::diagnostics::cwd_rel_path; | |
| use crate::diagnostics::get_key_value_span; | |
| use crate::resolver::ResolveBehavior; | |
| use crate::sources::{CRATES_IO_INDEX, CRATES_IO_REGISTRY}; | |
| use crate::util::errors::{CargoResult, ManifestError}; | |
| use crate::util::interning::InternedString; | |
| use crate::util::{self, GlobalContext, IntoUrl, OnceExt, OptVersionReq}; | |
| use crate::workspace::dependency::{Artifact, ArtifactTarget, DepKind}; | |
| use crate::workspace::manifest::{ManifestMetadata, TargetSourcePath}; | |
| use crate::workspace::{ | |
| CliUnstable, FeatureValue, Patch, PatchLocation, find_workspace_root, resolve_relative_path, | |
| }; | |
| use crate::workspace::{Dependency, Manifest, Package, PackageId, Summary, Target}; | |
| use crate::workspace::{Edition, EitherManifest, Feature, Features, VirtualManifest, Workspace}; | |
| use crate::workspace::{ | |
| GitReference, PackageIdSpec, SourceId, WorkspaceConfig, WorkspaceRootConfig, | |
| }; | |
| mod embedded; | |
| mod targets; | |
| use self::targets::to_targets; | |
| /// See also `bin/cargo/commands/run.rs`s `is_manifest_command` | |
| pub fn is_embedded(path: &Path) -> bool { | |
| let ext = path.extension(); | |
| ext == Some(OsStr::new("rs")) || ext.is_none() | |
| } | |
| /// Loads a `Cargo.toml` from a file on disk. | |
| /// | |
| /// This could result in a real or virtual manifest being returned. | |
| /// | |
| /// A list of nested paths is also returned, one for each path dependency | |
| /// within the manifest. For virtual manifests, these paths can only | |
| /// come from patched or replaced dependencies. These paths are not | |
| /// canonicalized. | |
| #[tracing::instrument(skip(gctx))] | |
| pub fn read_manifest( | |
| path: &Path, | |
| source_id: SourceId, | |
| gctx: &GlobalContext, | |
| ) -> CargoResult<EitherManifest> { | |
| let mut warnings = Default::default(); | |
| let mut errors = Default::default(); | |
| let is_embedded = is_embedded(path); | |
| let contents = read_toml_string(path, is_embedded, gctx)?; | |
| let document = parse_document(&contents) | |
| .map_err(|e| emit_toml_diagnostic(e.into(), &contents, path, gctx))?; | |
| let original_toml = deserialize_toml(&document) | |
| .map_err(|e| emit_toml_diagnostic(e.into(), &contents, path, gctx))?; | |
| let document = make_document_owned(document); | |
| let mut manifest = (|| { | |
| let empty = Vec::new(); | |
| let cargo_features = original_toml.cargo_features.as_ref().unwrap_or(&empty); | |
| let features = Features::new(cargo_features, gctx, &mut warnings, source_id.is_path())?; | |
| let workspace_config = | |
| to_workspace_config(&original_toml, path, is_embedded, &mut warnings, &features)?; | |
| if let WorkspaceConfig::Root(ws_root_config) = &workspace_config { | |
| let package_root = path.parent().unwrap(); | |
| gctx.ws_roots() | |
| .insert(package_root.to_owned(), ws_root_config.clone()); | |
| } | |
| let normalized_toml = normalize_toml( | |
| &original_toml, | |
| &features, | |
| &workspace_config, | |
| path, | |
| is_embedded, | |
| gctx, | |
| &mut warnings, | |
| &mut errors, | |
| )?; | |
| if normalized_toml.package().is_some() { | |
| to_real_manifest( | |
| Some(contents), | |
| Some(document), | |
| original_toml, | |
| normalized_toml, | |
| features, | |
| workspace_config, | |
| source_id, | |
| path, | |
| is_embedded, | |
| gctx, | |
| &mut warnings, | |
| &mut errors, | |
| ) | |
| .map(EitherManifest::Real) | |
| } else if normalized_toml.workspace.is_some() { | |
| assert!(!is_embedded); | |
| to_virtual_manifest( | |
| Some(contents), | |
| Some(document), | |
| original_toml, | |
| normalized_toml, | |
| features, | |
| workspace_config, | |
| source_id, | |
| path, | |
| gctx, | |
| &mut warnings, | |
| &mut errors, | |
| ) | |
| .map(EitherManifest::Virtual) | |
| } else { | |
| anyhow::bail!("manifest is missing either a `[package]` or a `[workspace]`") | |
| } | |
| })() | |
| .map_err(|err| { | |
| ManifestError::new( | |
| err.context(format!("failed to parse manifest at `{}`", path.display())), | |
| path.into(), | |
| ) | |
| })?; | |
| for warning in warnings { | |
| manifest.warnings_mut().add_warning(warning); | |
| } | |
| for error in errors { | |
| manifest.warnings_mut().add_critical_warning(error); | |
| } | |
| Ok(manifest) | |
| } | |
| /// Transform the parsed TOML document so that all its values are owned, so that it has a 'static | |
| /// lifetime, to make it easier to work with it. | |
| fn make_document_owned( | |
| mut document: toml::Spanned<toml::de::DeTable<'_>>, | |
| ) -> toml::Spanned<toml::de::DeTable<'static>> { | |
| document.get_mut().make_owned(); | |
| // SAFETY: `DeTable::make_owned` ensures no borrows remain and the lifetime does not affect | |
| // layout | |
| unsafe { | |
| std::mem::transmute::< | |
| toml::Spanned<toml::de::DeTable<'_>>, | |
| toml::Spanned<toml::de::DeTable<'static>>, | |
| >(document) | |
| } | |
| } | |
| #[tracing::instrument(skip_all)] | |
| fn read_toml_string(path: &Path, is_embedded: bool, gctx: &GlobalContext) -> CargoResult<String> { | |
| let mut contents = paths::read(path).map_err(|err| ManifestError::new(err, path.into()))?; | |
| if is_embedded { | |
| if !gctx.cli_unstable().script { | |
| anyhow::bail!("parsing `{}` requires `-Zscript`", path.display()); | |
| } | |
| contents = embedded::expand_manifest(&contents) | |
| .map_err(|e| emit_frontmatter_diagnostic(e, &contents, path, gctx))?; | |
| } | |
| Ok(contents) | |
| } | |
| #[tracing::instrument(skip_all)] | |
| fn parse_document(contents: &str) -> Result<toml::Spanned<toml::de::DeTable<'_>>, toml::de::Error> { | |
| toml::de::DeTable::parse(&contents) | |
| } | |
| #[tracing::instrument(skip_all)] | |
| fn deserialize_toml( | |
| document: &toml::Spanned<toml::de::DeTable<'_>>, | |
| ) -> Result<manifest::TomlManifest, toml::de::Error> { | |
| let mut unused = BTreeSet::new(); | |
| let deserializer = toml::de::Deserializer::from(document.clone()); | |
| let mut document: manifest::TomlManifest = serde_ignored::deserialize(deserializer, |path| { | |
| let mut key = String::new(); | |
| stringify(&mut key, &path); | |
| unused.insert(key); | |
| })?; | |
| document._unused_keys = unused; | |
| Ok(document) | |
| } | |
| fn stringify(dst: &mut String, path: &serde_ignored::Path<'_>) { | |
| use serde_ignored::Path; | |
| match *path { | |
| Path::Root => {} | |
| Path::Seq { parent, index } => { | |
| stringify(dst, parent); | |
| if !dst.is_empty() { | |
| dst.push('.'); | |
| } | |
| dst.push_str(&index.to_string()); | |
| } | |
| Path::Map { parent, ref key } => { | |
| stringify(dst, parent); | |
| if !dst.is_empty() { | |
| dst.push('.'); | |
| } | |
| dst.push_str(key); | |
| } | |
| Path::Some { parent } | |
| | Path::NewtypeVariant { parent } | |
| | Path::NewtypeStruct { parent } => stringify(dst, parent), | |
| } | |
| } | |
| fn to_workspace_config( | |
| original_toml: &manifest::TomlManifest, | |
| manifest_file: &Path, | |
| is_embedded: bool, | |
| warnings: &mut Vec<String>, | |
| features: &Features, | |
| ) -> CargoResult<WorkspaceConfig> { | |
| if is_embedded { | |
| let ws_root_config = to_workspace_root_config(&TomlWorkspace::default(), manifest_file); | |
| return Ok(WorkspaceConfig::Root(ws_root_config)); | |
| } | |
| let workspace_config = match ( | |
| original_toml.workspace.as_ref(), | |
| original_toml.package().and_then(|p| p.workspace.as_ref()), | |
| ) { | |
| (Some(toml_config), None) => { | |
| verify_lints(toml_config.lints.as_ref(), warnings)?; | |
| if let Some(ws_deps) = &toml_config.dependencies { | |
| for (name, dep) in ws_deps { | |
| if let TomlDependency::Detailed(d) = dep { | |
| if d.builtin { | |
| features | |
| .require(Feature::builtin_dependencies()) | |
| .with_context(|| { | |
| format!("resolving workspace dependency `{name}`") | |
| })?; | |
| } | |
| } | |
| if dep.is_optional() { | |
| bail!("{name} is optional, but workspace dependencies cannot be optional",); | |
| } | |
| if dep.is_public() { | |
| bail!("{name} is public, but workspace dependencies cannot be public",); | |
| } | |
| } | |
| for (name, dep) in ws_deps { | |
| unused_dep_keys(name, "workspace.dependencies", dep.unused_keys(), warnings); | |
| } | |
| } | |
| let ws_root_config = to_workspace_root_config(toml_config, manifest_file); | |
| 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" | |
| ), | |
| }; | |
| Ok(workspace_config) | |
| } | |
| fn to_workspace_root_config( | |
| normalized_toml: &manifest::TomlWorkspace, | |
| manifest_file: &Path, | |
| ) -> WorkspaceRootConfig { | |
| let package_root = manifest_file.parent().unwrap(); | |
| let inheritable = InheritableFields { | |
| package: normalized_toml.package.clone(), | |
| dependencies: normalized_toml.dependencies.clone(), | |
| lints: normalized_toml.lints.clone(), | |
| _ws_root: package_root.to_owned(), | |
| }; | |
| let ws_root_config = WorkspaceRootConfig::new( | |
| package_root, | |
| &normalized_toml.members, | |
| &normalized_toml.default_members, | |
| &normalized_toml.exclude, | |
| &Some(inheritable), | |
| &normalized_toml.metadata, | |
| ); | |
| ws_root_config | |
| } | |
| /// See [`Manifest::normalized_toml`] for more details | |
| #[tracing::instrument(skip_all)] | |
| fn normalize_toml( | |
| original_toml: &manifest::TomlManifest, | |
| features: &Features, | |
| workspace_config: &WorkspaceConfig, | |
| manifest_file: &Path, | |
| is_embedded: bool, | |
| gctx: &GlobalContext, | |
| warnings: &mut Vec<String>, | |
| errors: &mut Vec<String>, | |
| ) -> CargoResult<manifest::TomlManifest> { | |
| let package_root = manifest_file.parent().unwrap(); | |
| let inherit_cell: OnceCell<InheritableFields> = OnceCell::new(); | |
| let inherit = || { | |
| inherit_cell | |
| .try_borrow_with(|| load_inheritable_fields(gctx, manifest_file, &workspace_config)) | |
| }; | |
| let workspace_root = || inherit().map(|fields| fields.ws_root().as_path()); | |
| let mut normalized_toml = manifest::TomlManifest { | |
| cargo_features: original_toml.cargo_features.clone(), | |
| package: None, | |
| project: None, | |
| badges: None, | |
| features: None, | |
| lib: None, | |
| bin: None, | |
| example: None, | |
| test: None, | |
| bench: None, | |
| dependencies: None, | |
| dev_dependencies: None, | |
| dev_dependencies2: None, | |
| build_dependencies: None, | |
| build_dependencies2: None, | |
| target: None, | |
| lints: None, | |
| hints: None, | |
| workspace: original_toml.workspace.clone().or_else(|| { | |
| // Prevent looking for a workspace by `read_manifest_from_str` | |
| is_embedded.then(manifest::TomlWorkspace::default) | |
| }), | |
| profile: original_toml.profile.clone(), | |
| patch: normalize_patch( | |
| gctx, | |
| original_toml.patch.as_ref(), | |
| &workspace_root, | |
| features, | |
| )?, | |
| replace: original_toml.replace.clone(), | |
| _unused_keys: Default::default(), | |
| }; | |
| if let Some(original_package) = original_toml.package().map(Cow::Borrowed).or_else(|| { | |
| if is_embedded { | |
| Some(Cow::Owned(Box::new(manifest::TomlPackage::default()))) | |
| } else { | |
| None | |
| } | |
| }) { | |
| let normalized_package = normalize_package_toml( | |
| &original_package, | |
| manifest_file, | |
| is_embedded, | |
| gctx, | |
| &inherit, | |
| features, | |
| )?; | |
| let package_name = &normalized_package | |
| .normalized_name() | |
| .expect("previously normalized") | |
| .clone(); | |
| let edition = normalized_package | |
| .normalized_edition() | |
| .expect("previously normalized") | |
| .map_or(Edition::default(), |e| { | |
| Edition::from_str(&e).unwrap_or_default() | |
| }); | |
| normalized_toml.package = Some(normalized_package); | |
| normalized_toml.features = normalize_features(original_toml.features.as_ref())?; | |
| let auto_embedded = is_embedded.then_some(false); | |
| normalized_toml.lib = targets::normalize_lib( | |
| original_toml.lib.as_ref(), | |
| package_root, | |
| package_name, | |
| edition, | |
| original_package.autolib.or(auto_embedded), | |
| warnings, | |
| )?; | |
| let original_toml_bin = if is_embedded { | |
| let name = package_name.as_ref().to_owned(); | |
| let manifest_file_name = manifest_file | |
| .file_name() | |
| .expect("file name enforced previously"); | |
| let path = PathBuf::from(manifest_file_name); | |
| Cow::Owned(Some(vec![manifest::TomlBinTarget { | |
| name: Some(name), | |
| crate_type: None, | |
| crate_type2: None, | |
| path: Some(manifest::PathValue(path)), | |
| filename: None, | |
| test: None, | |
| doctest: None, | |
| bench: None, | |
| doc: None, | |
| doc_scrape_examples: None, | |
| proc_macro: None, | |
| proc_macro2: None, | |
| harness: None, | |
| required_features: None, | |
| edition: None, | |
| }])) | |
| } else { | |
| Cow::Borrowed(&original_toml.bin) | |
| }; | |
| normalized_toml.bin = Some(targets::normalize_bins( | |
| original_toml_bin.as_ref().as_ref(), | |
| package_root, | |
| package_name, | |
| edition, | |
| original_package.autobins.or(auto_embedded), | |
| warnings, | |
| errors, | |
| normalized_toml.lib.is_some(), | |
| )?); | |
| normalized_toml.example = Some(targets::normalize_examples( | |
| original_toml.example.as_ref(), | |
| package_root, | |
| edition, | |
| original_package.autoexamples.or(auto_embedded), | |
| warnings, | |
| errors, | |
| )?); | |
| normalized_toml.test = Some(targets::normalize_tests( | |
| original_toml.test.as_ref(), | |
| package_root, | |
| edition, | |
| original_package.autotests.or(auto_embedded), | |
| warnings, | |
| errors, | |
| )?); | |
| normalized_toml.bench = Some(targets::normalize_benches( | |
| original_toml.bench.as_ref(), | |
| package_root, | |
| edition, | |
| original_package.autobenches.or(auto_embedded), | |
| warnings, | |
| errors, | |
| )?); | |
| normalized_toml.dependencies = normalize_dependencies( | |
| gctx, | |
| edition, | |
| &features, | |
| original_toml.dependencies.as_ref(), | |
| DepKind::Normal, | |
| &inherit, | |
| &workspace_root, | |
| package_root, | |
| warnings, | |
| )?; | |
| deprecated_underscore( | |
| &original_toml.dev_dependencies2, | |
| &original_toml.dev_dependencies, | |
| "dev-dependencies", | |
| package_name, | |
| "package", | |
| edition, | |
| warnings, | |
| )?; | |
| normalized_toml.dev_dependencies = normalize_dependencies( | |
| gctx, | |
| edition, | |
| &features, | |
| original_toml.dev_dependencies(), | |
| DepKind::Development, | |
| &inherit, | |
| &workspace_root, | |
| package_root, | |
| warnings, | |
| )?; | |
| deprecated_underscore( | |
| &original_toml.build_dependencies2, | |
| &original_toml.build_dependencies, | |
| "build-dependencies", | |
| package_name, | |
| "package", | |
| edition, | |
| warnings, | |
| )?; | |
| normalized_toml.build_dependencies = normalize_dependencies( | |
| gctx, | |
| edition, | |
| &features, | |
| original_toml.build_dependencies(), | |
| DepKind::Build, | |
| &inherit, | |
| &workspace_root, | |
| package_root, | |
| warnings, | |
| )?; | |
| let mut normalized_target = BTreeMap::new(); | |
| for (name, platform) in original_toml.target.iter().flatten() { | |
| let normalized_dependencies = normalize_dependencies( | |
| gctx, | |
| edition, | |
| &features, | |
| platform.dependencies.as_ref(), | |
| DepKind::Normal, | |
| &inherit, | |
| &workspace_root, | |
| package_root, | |
| warnings, | |
| )?; | |
| deprecated_underscore( | |
| &platform.dev_dependencies2, | |
| &platform.dev_dependencies, | |
| "dev-dependencies", | |
| name, | |
| "platform target", | |
| edition, | |
| warnings, | |
| )?; | |
| let normalized_dev_dependencies = normalize_dependencies( | |
| gctx, | |
| edition, | |
| &features, | |
| platform.dev_dependencies(), | |
| DepKind::Development, | |
| &inherit, | |
| &workspace_root, | |
| package_root, | |
| warnings, | |
| )?; | |
| deprecated_underscore( | |
| &platform.build_dependencies2, | |
| &platform.build_dependencies, | |
| "build-dependencies", | |
| name, | |
| "platform target", | |
| edition, | |
| warnings, | |
| )?; | |
| let normalized_build_dependencies = normalize_dependencies( | |
| gctx, | |
| edition, | |
| &features, | |
| platform.build_dependencies(), | |
| DepKind::Build, | |
| &inherit, | |
| &workspace_root, | |
| package_root, | |
| warnings, | |
| )?; | |
| normalized_target.insert( | |
| name.clone(), | |
| manifest::TomlPlatform { | |
| dependencies: normalized_dependencies, | |
| build_dependencies: normalized_build_dependencies, | |
| build_dependencies2: None, | |
| dev_dependencies: normalized_dev_dependencies, | |
| dev_dependencies2: None, | |
| }, | |
| ); | |
| } | |
| normalized_toml.target = (!normalized_target.is_empty()).then_some(normalized_target); | |
| let normalized_lints = original_toml | |
| .lints | |
| .clone() | |
| .map(|value| lints_inherit_with(value, || inherit()?.lints())) | |
| .transpose()?; | |
| normalized_toml.lints = normalized_lints.map(|lints| manifest::InheritableLints { | |
| workspace: false, | |
| lints, | |
| }); | |
| normalized_toml.hints = original_toml.hints.clone(); | |
| normalized_toml.badges = original_toml.badges.clone(); | |
| } else { | |
| if let Some(field) = original_toml.requires_package().next() { | |
| let suggestion = if field == "lints" { | |
| "\nhelp: a similar field exists: `[workspace.lints]`" | |
| } else { | |
| "" | |
| }; | |
| bail!( | |
| "this virtual manifest specifies a `{field}` section, which is not allowed{suggestion}" | |
| ); | |
| } | |
| } | |
| Ok(normalized_toml) | |
| } | |
| fn normalize_patch<'a>( | |
| gctx: &GlobalContext, | |
| original_patch: Option<&BTreeMap<String, BTreeMap<PackageName, TomlDependency>>>, | |
| workspace_root: &dyn Fn() -> CargoResult<&'a Path>, | |
| features: &Features, | |
| ) -> CargoResult<Option<BTreeMap<String, BTreeMap<PackageName, TomlDependency>>>> { | |
| if let Some(patch) = original_patch { | |
| let mut normalized_patch = BTreeMap::new(); | |
| for (name, packages) in patch { | |
| let mut normalized_packages = BTreeMap::new(); | |
| for (pkg, dep) in packages { | |
| let dep = if let TomlDependency::Detailed(dep) = dep { | |
| let mut dep = dep.clone(); | |
| normalize_path_dependency(gctx, &mut dep, workspace_root, features) | |
| .with_context(|| { | |
| format!("resolving path for patch of ({pkg}) for source ({name})") | |
| })?; | |
| TomlDependency::Detailed(dep) | |
| } else { | |
| dep.clone() | |
| }; | |
| normalized_packages.insert(pkg.clone(), dep); | |
| } | |
| normalized_patch.insert(name.clone(), normalized_packages); | |
| } | |
| Ok(Some(normalized_patch)) | |
| } else { | |
| Ok(None) | |
| } | |
| } | |
| #[tracing::instrument(skip_all)] | |
| fn normalize_package_toml<'a>( | |
| original_package: &manifest::TomlPackage, | |
| manifest_file: &Path, | |
| is_embedded: bool, | |
| gctx: &GlobalContext, | |
| inherit: &dyn Fn() -> CargoResult<&'a InheritableFields>, | |
| features: &Features, | |
| ) -> CargoResult<Box<manifest::TomlPackage>> { | |
| let package_root = manifest_file.parent().unwrap(); | |
| let edition = original_package | |
| .edition | |
| .clone() | |
| .map(|value| field_inherit_with(value, "edition", || inherit()?.edition())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value) | |
| .or_else(|| { | |
| if is_embedded { | |
| const DEFAULT_EDITION: crate::workspace::features::Edition = | |
| crate::workspace::features::Edition::LATEST_STABLE; | |
| let mut report = vec![Group::with_title(Level::WARNING.secondary_title(format!( | |
| "`package.edition` is unspecified, defaulting to the latest edition (currently `{DEFAULT_EDITION}`)" | |
| )))]; | |
| if !matches!(gctx.shell().verbosity(), cargo_util_terminal::Verbosity::Quiet) { | |
| report.push(Group::with_title(Level::HELP.secondary_title(format!( | |
| "to pin the edition, run `cargo fix --manifest-path {}`", manifest_file.display() | |
| )))); | |
| } | |
| let _ = gctx.shell().print_report(&report, true); | |
| Some(manifest::InheritableField::Value( | |
| DEFAULT_EDITION.to_string(), | |
| )) | |
| } else { | |
| None | |
| } | |
| }); | |
| let rust_version = original_package | |
| .rust_version | |
| .clone() | |
| .map(|value| field_inherit_with(value, "rust-version", || inherit()?.rust_version())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let name = Some( | |
| original_package | |
| .name | |
| .clone() | |
| .or_else(|| { | |
| if is_embedded { | |
| let file_stem = manifest_file | |
| .file_stem() | |
| .expect("file name enforced previously") | |
| .to_string_lossy(); | |
| let name = embedded::sanitize_name(file_stem.as_ref()); | |
| let name = | |
| manifest::PackageName::new(name).expect("sanitize made the name valid"); | |
| Some(name) | |
| } else { | |
| None | |
| } | |
| }) | |
| .ok_or_else(|| anyhow::format_err!("missing field `package.name`"))?, | |
| ); | |
| let version = original_package | |
| .version | |
| .clone() | |
| .map(|value| field_inherit_with(value, "version", || inherit()?.version())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let authors = original_package | |
| .authors | |
| .clone() | |
| .map(|value| field_inherit_with(value, "authors", || inherit()?.authors())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let build = if is_embedded { | |
| Some(TomlPackageBuild::Auto(false)) | |
| } else { | |
| if let Some(TomlPackageBuild::MultipleScript(_)) = original_package.build { | |
| features.require(Feature::multiple_build_scripts())?; | |
| } | |
| targets::normalize_build(original_package.build.as_ref(), package_root)? | |
| }; | |
| let metabuild = original_package.metabuild.clone(); | |
| let default_target = original_package.default_target.clone(); | |
| let forced_target = original_package.forced_target.clone(); | |
| let links = original_package.links.clone(); | |
| let exclude = original_package | |
| .exclude | |
| .clone() | |
| .map(|value| field_inherit_with(value, "exclude", || inherit()?.exclude())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let include = original_package | |
| .include | |
| .clone() | |
| .map(|value| field_inherit_with(value, "include", || inherit()?.include())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let publish = original_package | |
| .publish | |
| .clone() | |
| .map(|value| field_inherit_with(value, "publish", || inherit()?.publish())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let workspace = original_package.workspace.clone(); | |
| let im_a_teapot = original_package.im_a_teapot.clone(); | |
| let autolib = Some(false); | |
| let autobins = Some(false); | |
| let autoexamples = Some(false); | |
| let autotests = Some(false); | |
| let autobenches = Some(false); | |
| let default_run = original_package.default_run.clone(); | |
| let description = original_package | |
| .description | |
| .clone() | |
| .map(|value| field_inherit_with(value, "description", || inherit()?.description())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let homepage = original_package | |
| .homepage | |
| .clone() | |
| .map(|value| field_inherit_with(value, "homepage", || inherit()?.homepage())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let documentation = original_package | |
| .documentation | |
| .clone() | |
| .map(|value| field_inherit_with(value, "documentation", || inherit()?.documentation())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let readme = normalize_package_readme( | |
| package_root, | |
| original_package | |
| .readme | |
| .clone() | |
| .map(|value| field_inherit_with(value, "readme", || inherit()?.readme(package_root))) | |
| .transpose()? | |
| .as_ref(), | |
| ) | |
| .map(|s| manifest::InheritableField::Value(StringOrBool::String(s))) | |
| .or(Some(manifest::InheritableField::Value(StringOrBool::Bool( | |
| false, | |
| )))); | |
| let keywords = original_package | |
| .keywords | |
| .clone() | |
| .map(|value| field_inherit_with(value, "keywords", || inherit()?.keywords())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let categories = original_package | |
| .categories | |
| .clone() | |
| .map(|value| field_inherit_with(value, "categories", || inherit()?.categories())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let license = original_package | |
| .license | |
| .clone() | |
| .map(|value| field_inherit_with(value, "license", || inherit()?.license())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let license_file = original_package | |
| .license_file | |
| .clone() | |
| .map(|value| { | |
| field_inherit_with(value, "license-file", || { | |
| inherit()?.license_file(package_root) | |
| }) | |
| }) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let repository = original_package | |
| .repository | |
| .clone() | |
| .map(|value| field_inherit_with(value, "repository", || inherit()?.repository())) | |
| .transpose()? | |
| .map(manifest::InheritableField::Value); | |
| let resolver = original_package.resolver.clone(); | |
| let metadata = original_package.metadata.clone(); | |
| let normalized_package = manifest::TomlPackage { | |
| edition, | |
| rust_version, | |
| name, | |
| version, | |
| authors, | |
| build, | |
| metabuild, | |
| default_target, | |
| forced_target, | |
| links, | |
| exclude, | |
| include, | |
| publish, | |
| workspace, | |
| im_a_teapot, | |
| autolib, | |
| autobins, | |
| autoexamples, | |
| autotests, | |
| autobenches, | |
| default_run, | |
| description, | |
| homepage, | |
| documentation, | |
| readme, | |
| keywords, | |
| categories, | |
| license, | |
| license_file, | |
| repository, | |
| resolver, | |
| metadata, | |
| _invalid_cargo_features: Default::default(), | |
| }; | |
| Ok(Box::new(normalized_package)) | |
| } | |
| /// Returns the name of the README file for a [`manifest::TomlPackage`]. | |
| fn normalize_package_readme( | |
| package_root: &Path, | |
| readme: Option<&manifest::StringOrBool>, | |
| ) -> Option<String> { | |
| match &readme { | |
| None => default_readme_from_package_root(package_root), | |
| Some(value) => match value { | |
| manifest::StringOrBool::Bool(false) => None, | |
| manifest::StringOrBool::Bool(true) => Some("README.md".to_string()), | |
| manifest::StringOrBool::String(v) => Some(v.clone()), | |
| }, | |
| } | |
| } | |
| pub 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. | |
| pub(crate) fn default_readme_from_package_root(package_root: &Path) -> Option<String> { | |
| for &readme_filename in DEFAULT_README_FILES.iter() { | |
| if package_root.join(readme_filename).is_file() { | |
| return Some(readme_filename.to_string()); | |
| } | |
| } | |
| None | |
| } | |
| #[tracing::instrument(skip_all)] | |
| fn normalize_features( | |
| original_features: Option<&BTreeMap<manifest::FeatureName, FeatureDefinition>>, | |
| ) -> CargoResult<Option<BTreeMap<manifest::FeatureName, FeatureDefinition>>> { | |
| let Some(normalized_features) = original_features.cloned() else { | |
| return Ok(None); | |
| }; | |
| Ok(Some(normalized_features)) | |
| } | |
| #[tracing::instrument(skip_all)] | |
| fn normalize_dependencies<'a>( | |
| gctx: &GlobalContext, | |
| edition: Edition, | |
| features: &Features, | |
| orig_deps: Option<&BTreeMap<manifest::PackageName, manifest::InheritableDependency>>, | |
| kind: DepKind, | |
| inherit: &dyn Fn() -> CargoResult<&'a InheritableFields>, | |
| workspace_root: &dyn Fn() -> CargoResult<&'a Path>, | |
| package_root: &Path, | |
| warnings: &mut Vec<String>, | |
| ) -> CargoResult<Option<BTreeMap<manifest::PackageName, manifest::InheritableDependency>>> { | |
| let Some(dependencies) = orig_deps else { | |
| return Ok(None); | |
| }; | |
| let mut deps = BTreeMap::new(); | |
| for (name_in_toml, v) in dependencies.iter() { | |
| let mut resolved = dependency_inherit_with( | |
| v.clone(), | |
| name_in_toml, | |
| inherit, | |
| package_root, | |
| edition, | |
| warnings, | |
| )?; | |
| if let manifest::TomlDependency::Detailed(ref mut d) = resolved { | |
| deprecated_underscore( | |
| &d.default_features2, | |
| &d.default_features, | |
| "default-features", | |
| name_in_toml, | |
| "dependency", | |
| edition, | |
| warnings, | |
| )?; | |
| if d.public.is_some() { | |
| let with_public_feature = features.require(Feature::public_dependency()).is_ok(); | |
| let with_z_public = gctx.cli_unstable().public_dependency; | |
| match kind { | |
| DepKind::Normal => { | |
| if !with_public_feature && !with_z_public { | |
| d.public = None; | |
| warnings.push(format!( | |
| "ignoring `public` on dependency {name_in_toml}, pass `-Zpublic-dependency` to enable support for it" | |
| )); | |
| } | |
| } | |
| DepKind::Development | DepKind::Build => { | |
| let kind_name = kind.kind_table(); | |
| let hint = format!( | |
| "'public' specifier can only be used on regular dependencies, not {kind_name}", | |
| ); | |
| if with_public_feature || with_z_public { | |
| bail!(hint) | |
| } else { | |
| // If public feature isn't enabled in nightly, we instead warn that. | |
| warnings.push(hint); | |
| d.public = None; | |
| } | |
| } | |
| } | |
| } | |
| if d.builtin { | |
| features | |
| .require(Feature::builtin_dependencies()) | |
| .with_context(|| format!("resolving builtin dependency {name_in_toml}"))?; | |
| } | |
| normalize_path_dependency(gctx, d, workspace_root, features) | |
| .with_context(|| format!("resolving path dependency {name_in_toml}"))?; | |
| } | |
| deps.insert( | |
| name_in_toml.clone(), | |
| manifest::InheritableDependency::Value(resolved.clone()), | |
| ); | |
| } | |
| Ok(Some(deps)) | |
| } | |
| fn normalize_path_dependency<'a>( | |
| gctx: &GlobalContext, | |
| detailed_dep: &mut TomlDetailedDependency, | |
| workspace_root: &dyn Fn() -> CargoResult<&'a Path>, | |
| features: &Features, | |
| ) -> CargoResult<()> { | |
| if let Some(base) = detailed_dep.base.take() { | |
| if let Some(path) = detailed_dep.path.as_mut() { | |
| let new_path = lookup_path_base(&base, gctx, workspace_root, features)?.join(&path); | |
| *path = new_path.to_str().unwrap().to_string(); | |
| } else { | |
| bail!("`base` can only be used with path dependencies"); | |
| } | |
| } | |
| Ok(()) | |
| } | |
| fn load_inheritable_fields( | |
| gctx: &GlobalContext, | |
| normalized_path: &Path, | |
| workspace_config: &WorkspaceConfig, | |
| ) -> CargoResult<InheritableFields> { | |
| match workspace_config { | |
| WorkspaceConfig::Root(root) => Ok(root.inheritable().clone()), | |
| WorkspaceConfig::Member { | |
| root: Some(path_to_root), | |
| } => { | |
| let path = normalized_path | |
| .parent() | |
| .unwrap() | |
| .join(path_to_root) | |
| .join("Cargo.toml"); | |
| let root_path = paths::normalize_path(&path); | |
| inheritable_from_path(gctx, root_path) | |
| } | |
| WorkspaceConfig::Member { root: None } => { | |
| match find_workspace_root(&normalized_path, gctx)? { | |
| Some(path_to_root) => inheritable_from_path(gctx, path_to_root), | |
| None => Err(anyhow!("failed to find a workspace root")), | |
| } | |
| } | |
| } | |
| } | |
| fn inheritable_from_path( | |
| gctx: &GlobalContext, | |
| workspace_path: PathBuf, | |
| ) -> CargoResult<InheritableFields> { | |
| // 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) = gctx.ws_roots().get(workspace_path_root) { | |
| return Ok(ws_root.inheritable().clone()); | |
| }; | |
| let source_id = SourceId::for_manifest_path(&workspace_path)?; | |
| let man = read_manifest(&workspace_path, source_id, gctx)?; | |
| match man.workspace_config() { | |
| WorkspaceConfig::Root(root) => { | |
| gctx.ws_roots().insert(workspace_path, root.clone()); | |
| Ok(root.inheritable().clone()) | |
| } | |
| _ => bail!( | |
| "root of a workspace inferred but wasn't a root: {}", | |
| workspace_path.display() | |
| ), | |
| } | |
| } | |
| /// Defines simple getter methods for inheritable fields. | |
| macro_rules! package_field_getter { | |
| ( $(($key:literal, $field:ident -> $ret:ty),)* ) => ( | |
| $( | |
| #[doc = concat!("Gets the field `workspace.package.", $key, "`.")] | |
| fn $field(&self) -> CargoResult<$ret> { | |
| let Some(val) = self.package.as_ref().and_then(|p| p.$field.as_ref()) else { | |
| bail!("`workspace.package.{}` was not defined", $key); | |
| }; | |
| Ok(val.clone()) | |
| } | |
| )* | |
| ) | |
| } | |
| /// A group of fields that are inheritable by members of the workspace | |
| #[derive(Clone, Debug, Default)] | |
| pub struct InheritableFields { | |
| package: Option<manifest::InheritablePackage>, | |
| dependencies: Option<BTreeMap<manifest::PackageName, manifest::TomlDependency>>, | |
| lints: Option<manifest::TomlLints>, | |
| // Bookkeeping to help when resolving values from above | |
| _ws_root: PathBuf, | |
| } | |
| impl InheritableFields { | |
| package_field_getter! { | |
| // Please keep this list lexicographically ordered. | |
| ("authors", authors -> Vec<String>), | |
| ("categories", categories -> Vec<String>), | |
| ("description", description -> String), | |
| ("documentation", documentation -> String), | |
| ("edition", edition -> String), | |
| ("exclude", exclude -> Vec<String>), | |
| ("homepage", homepage -> String), | |
| ("include", include -> Vec<String>), | |
| ("keywords", keywords -> Vec<String>), | |
| ("license", license -> String), | |
| ("publish", publish -> manifest::VecStringOrBool), | |
| ("repository", repository -> String), | |
| ("rust-version", rust_version -> RustVersion), | |
| ("version", version -> semver::Version), | |
| } | |
| /// Gets a workspace dependency with the `name`. | |
| fn get_dependency( | |
| &self, | |
| name: &str, | |
| package_root: &Path, | |
| ) -> CargoResult<manifest::TomlDependency> { | |
| 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 manifest::TomlDependency::Detailed(detailed) = &mut dep { | |
| if detailed.base.is_none() { | |
| // If this is a path dependency without a base, then update the path to be relative | |
| // to the workspace root instead. | |
| if let Some(rel_path) = &detailed.path { | |
| detailed.path = Some(resolve_relative_path( | |
| name, | |
| self.ws_root(), | |
| package_root, | |
| rel_path, | |
| )?); | |
| } | |
| } | |
| } | |
| Ok(dep) | |
| } | |
| /// Gets the field `workspace.lints`. | |
| pub fn lints(&self) -> CargoResult<manifest::TomlLints> { | |
| let Some(val) = &self.lints else { | |
| bail!("`workspace.lints` was not defined"); | |
| }; | |
| Ok(val.clone()) | |
| } | |
| /// Gets the field `workspace.package.license-file`. | |
| fn license_file(&self, package_root: &Path) -> CargoResult<String> { | |
| let Some(license_file) = self.package.as_ref().and_then(|p| p.license_file.as_ref()) 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`. | |
| fn readme(&self, package_root: &Path) -> CargoResult<manifest::StringOrBool> { | |
| let Some(readme) = normalize_package_readme( | |
| self._ws_root.as_path(), | |
| self.package.as_ref().and_then(|p| p.readme.as_ref()), | |
| ) else { | |
| bail!("`workspace.package.readme` was not defined"); | |
| }; | |
| resolve_relative_path("readme", &self._ws_root, package_root, &readme) | |
| .map(manifest::StringOrBool::String) | |
| } | |
| fn ws_root(&self) -> &PathBuf { | |
| &self._ws_root | |
| } | |
| } | |
| fn field_inherit_with<'a, T>( | |
| field: manifest::InheritableField<T>, | |
| label: &str, | |
| get_ws_inheritable: impl FnOnce() -> CargoResult<T>, | |
| ) -> CargoResult<T> { | |
| match field { | |
| manifest::InheritableField::Value(value) => Ok(value), | |
| manifest::InheritableField::Inherit(_) => get_ws_inheritable().with_context(|| { | |
| format!( | |
| "error inheriting `{label}` from workspace root manifest's `workspace.package.{label}`", | |
| ) | |
| }), | |
| } | |
| } | |
| fn lints_inherit_with( | |
| lints: manifest::InheritableLints, | |
| get_ws_inheritable: impl FnOnce() -> CargoResult<manifest::TomlLints>, | |
| ) -> CargoResult<manifest::TomlLints> { | |
| if lints.workspace { | |
| if !lints.lints.is_empty() { | |
| anyhow::bail!( | |
| "cannot override `workspace.lints` in `lints`, either remove the overrides or `lints.workspace = true` and manually specify the lints" | |
| ); | |
| } | |
| get_ws_inheritable().with_context( | |
| || "error inheriting `lints` from workspace root manifest's `workspace.lints`", | |
| ) | |
| } else { | |
| Ok(lints.lints) | |
| } | |
| } | |
| fn dependency_inherit_with<'a>( | |
| dependency: manifest::InheritableDependency, | |
| name: &str, | |
| inherit: &dyn Fn() -> CargoResult<&'a InheritableFields>, | |
| package_root: &Path, | |
| edition: Edition, | |
| warnings: &mut Vec<String>, | |
| ) -> CargoResult<manifest::TomlDependency> { | |
| match dependency { | |
| manifest::InheritableDependency::Value(value) => Ok(value), | |
| manifest::InheritableDependency::Inherit(w) => { | |
| inner_dependency_inherit_with(w, name, inherit, package_root, edition, warnings).with_context(|| { | |
| format!( | |
| "error inheriting `{name}` from workspace root manifest's `workspace.dependencies.{name}`", | |
| ) | |
| }) | |
| } | |
| } | |
| } | |
| fn inner_dependency_inherit_with<'a>( | |
| pkg_dep: manifest::TomlInheritedDependency, | |
| name: &str, | |
| inherit: &dyn Fn() -> CargoResult<&'a InheritableFields>, | |
| package_root: &Path, | |
| edition: Edition, | |
| warnings: &mut Vec<String>, | |
| ) -> CargoResult<manifest::TomlDependency> { | |
| let ws_dep = inherit()?.get_dependency(name, package_root)?; | |
| let mut merged_dep = match ws_dep { | |
| manifest::TomlDependency::Simple(ws_version) => manifest::TomlDetailedDependency { | |
| version: Some(ws_version), | |
| ..Default::default() | |
| }, | |
| manifest::TomlDependency::Detailed(ws_dep) => ws_dep.clone(), | |
| }; | |
| let manifest::TomlInheritedDependency { | |
| workspace: _, | |
| features, | |
| optional, | |
| default_features, | |
| default_features2, | |
| public, | |
| _unused_keys: _, | |
| } = &pkg_dep; | |
| let default_features = default_features.or(*default_features2); | |
| // RFC 3945: Allow workspace members to override the workspace dependency's | |
| // `default-features` setting. | |
| if edition >= Edition::Edition2024 { | |
| merged_dep.default_features = default_features.or(merged_dep.default_features); | |
| } else { | |
| match (default_features, merged_dep.default_features()) { | |
| // member: default-features = true and | |
| // workspace: default-features = false should turn on | |
| // default-features | |
| (Some(true), Some(false)) => { | |
| merged_dep.default_features = Some(true); | |
| } | |
| // member: default-features = false and | |
| // workspace: default-features = true should ignore member | |
| // default-features | |
| (Some(false), Some(true)) => { | |
| deprecated_ws_default_features(name, Some(true), warnings); | |
| } | |
| // member: default-features = false and | |
| // workspace: dep = "1.0" should ignore member default-features | |
| (Some(false), None) => { | |
| deprecated_ws_default_features(name, None, warnings); | |
| } | |
| _ => {} | |
| } | |
| } | |
| merged_dep.features = match (merged_dep.features.clone(), features.clone()) { | |
| (Some(dep_feat), Some(inherit_feat)) => Some( | |
| dep_feat | |
| .into_iter() | |
| .chain(inherit_feat) | |
| .collect::<Vec<String>>(), | |
| ), | |
| (Some(dep_fet), None) => Some(dep_fet), | |
| (None, Some(inherit_feat)) => Some(inherit_feat), | |
| (None, None) => None, | |
| }; | |
| merged_dep.optional = *optional; | |
| merged_dep.public = *public; | |
| Ok(manifest::TomlDependency::Detailed(merged_dep)) | |
| } | |
| fn deprecated_ws_default_features( | |
| label: &str, | |
| ws_def_feat: Option<bool>, | |
| warnings: &mut Vec<String>, | |
| ) { | |
| let ws_def_feat = match ws_def_feat { | |
| Some(true) => "true", | |
| Some(false) => "false", | |
| None => "not specified", | |
| }; | |
| warnings.push(format!( | |
| "`default-features` is ignored for {label}, since `default-features` was \ | |
| {ws_def_feat} for `workspace.dependencies.{label}`; \ | |
| overriding workspace `default-features` to false requires Rust 1.99+ \ | |
| and the 2024 edition" | |
| )); | |
| } | |
| #[tracing::instrument(skip_all)] | |
| pub fn to_real_manifest( | |
| contents: Option<String>, | |
| document: Option<toml::Spanned<toml::de::DeTable<'static>>>, | |
| original_toml: manifest::TomlManifest, | |
| normalized_toml: manifest::TomlManifest, | |
| features: Features, | |
| workspace_config: WorkspaceConfig, | |
| source_id: SourceId, | |
| manifest_file: &Path, | |
| is_embedded: bool, | |
| gctx: &GlobalContext, | |
| warnings: &mut Vec<String>, | |
| _errors: &mut Vec<String>, | |
| ) -> CargoResult<Manifest> { | |
| let package_root = manifest_file.parent().unwrap(); | |
| if !package_root.is_dir() { | |
| bail!( | |
| "package root '{}' is not a directory", | |
| package_root.display() | |
| ); | |
| }; | |
| let normalized_package = normalized_toml | |
| .package() | |
| .expect("previously verified to have a `[package]`"); | |
| let package_name = normalized_package | |
| .normalized_name() | |
| .expect("previously normalized"); | |
| if package_name.contains(':') { | |
| features.require(Feature::open_namespaces())?; | |
| } | |
| let rust_version = normalized_package | |
| .normalized_rust_version() | |
| .expect("previously normalized") | |
| .cloned(); | |
| let edition = if let Some(edition) = normalized_package | |
| .normalized_edition() | |
| .expect("previously normalized") | |
| { | |
| let edition: Edition = edition | |
| .parse() | |
| .context("failed to parse the `edition` key")?; | |
| if let Some(pkg_msrv) = &rust_version { | |
| if let Some(edition_msrv) = edition.first_version() { | |
| let edition_msrv = RustVersion::try_from(edition_msrv).unwrap(); | |
| if !edition_msrv.is_compatible_with(&pkg_msrv.to_partial()) { | |
| bail!( | |
| "rust-version {} is incompatible with the version ({}) required by \ | |
| the specified edition ({})", | |
| pkg_msrv, | |
| edition_msrv, | |
| edition, | |
| ) | |
| } | |
| } | |
| } | |
| edition | |
| } else { | |
| let msrv_edition = if let Some(pkg_msrv) = &rust_version { | |
| Edition::ALL | |
| .iter() | |
| .filter(|e| { | |
| e.first_version() | |
| .map(|e| { | |
| let e = RustVersion::try_from(e).unwrap(); | |
| e.is_compatible_with(&pkg_msrv.to_partial()) | |
| }) | |
| .unwrap_or_default() | |
| }) | |
| .max() | |
| .copied() | |
| } else { | |
| None | |
| } | |
| .unwrap_or_default(); | |
| let default_edition = Edition::default(); | |
| let latest_edition = Edition::LATEST_STABLE; | |
| // We're trying to help the user who might assume they are using a new edition, | |
| // so if they can't use a new edition, don't bother to tell them to set it. | |
| // This also avoids having to worry about whether `package.edition` is compatible with | |
| // their MSRV. | |
| if msrv_edition != default_edition || rust_version.is_none() { | |
| let tip = if msrv_edition == latest_edition || rust_version.is_none() { | |
| format!(" while the latest is `{latest_edition}`") | |
| } else { | |
| format!(" while {msrv_edition} is compatible with `rust-version`") | |
| }; | |
| warnings.push(format!( | |
| "`package.edition` is unspecified, defaulting to `{default_edition}`{tip}" | |
| )); | |
| } | |
| default_edition | |
| }; | |
| if !edition.is_stable() { | |
| let version = normalized_package | |
| .normalized_version() | |
| .expect("previously normalized") | |
| .map(|v| format!("@{v}")) | |
| .unwrap_or_default(); | |
| let hint = rust_version | |
| .as_ref() | |
| .map(|rv| format!("help: {package_name}{version} requires rust {rv}")); | |
| features.require_with_hint(Feature::unstable_editions(), hint.as_deref())?; | |
| } | |
| if original_toml.project.is_some() { | |
| if Edition::Edition2024 <= edition { | |
| anyhow::bail!( | |
| "`[project]` is not supported as of the 2024 Edition, please use `[package]`" | |
| ); | |
| } else { | |
| warnings.push(format!("`[project]` is deprecated in favor of `[package]`")); | |
| } | |
| } | |
| if normalized_package.metabuild.is_some() { | |
| features.require(Feature::metabuild())?; | |
| } | |
| if is_embedded { | |
| let manifest::TomlManifest { | |
| cargo_features: _, | |
| package: _, | |
| project: _, | |
| badges: _, | |
| features: _, | |
| lib, | |
| bin, | |
| example, | |
| test, | |
| bench, | |
| dependencies: _, | |
| dev_dependencies: _, | |
| dev_dependencies2: _, | |
| build_dependencies, | |
| build_dependencies2, | |
| target: _, | |
| lints: _, | |
| hints: _, | |
| workspace, | |
| profile: _, | |
| patch: _, | |
| replace: _, | |
| _unused_keys: _, | |
| } = &original_toml; | |
| let mut invalid_fields = vec![ | |
| ("`workspace`", workspace.is_some()), | |
| ("`lib`", lib.is_some()), | |
| ("`bin`", bin.is_some()), | |
| ("`example`", example.is_some()), | |
| ("`test`", test.is_some()), | |
| ("`bench`", bench.is_some()), | |
| ("`build-dependencies`", build_dependencies.is_some()), | |
| ("`build_dependencies`", build_dependencies2.is_some()), | |
| ]; | |
| if let Some(package) = original_toml.package() { | |
| let manifest::TomlPackage { | |
| edition: _, | |
| rust_version: _, | |
| name: _, | |
| version: _, | |
| authors: _, | |
| build, | |
| metabuild, | |
| default_target: _, | |
| forced_target: _, | |
| links, | |
| exclude: _, | |
| include: _, | |
| publish: _, | |
| workspace, | |
| im_a_teapot: _, | |
| autolib, | |
| autobins, | |
| autoexamples, | |
| autotests, | |
| autobenches, | |
| default_run, | |
| description: _, | |
| homepage: _, | |
| documentation: _, | |
| readme: _, | |
| keywords: _, | |
| categories: _, | |
| license: _, | |
| license_file: _, | |
| repository: _, | |
| resolver: _, | |
| metadata: _, | |
| _invalid_cargo_features: _, | |
| } = package.as_ref(); | |
| invalid_fields.extend([ | |
| ("`package.workspace`", workspace.is_some()), | |
| ("`package.build`", build.is_some()), | |
| ("`package.metabuild`", metabuild.is_some()), | |
| ("`package.links`", links.is_some()), | |
| ("`package.autolib`", autolib.is_some()), | |
| ("`package.autobins`", autobins.is_some()), | |
| ("`package.autoexamples`", autoexamples.is_some()), | |
| ("`package.autotests`", autotests.is_some()), | |
| ("`package.autobenches`", autobenches.is_some()), | |
| ("`package.default-run`", default_run.is_some()), | |
| ]); | |
| } | |
| let invalid_fields = invalid_fields | |
| .into_iter() | |
| .filter_map(|(name, invalid)| invalid.then_some(name)) | |
| .collect::<Vec<_>>(); | |
| if !invalid_fields.is_empty() { | |
| let fields = invalid_fields.join(", "); | |
| let are = if invalid_fields.len() == 1 { | |
| "is" | |
| } else { | |
| "are" | |
| }; | |
| anyhow::bail!("{fields} {are} not allowed in embedded manifests") | |
| } | |
| } | |
| let resolve_behavior = match ( | |
| normalized_package.resolver.as_ref(), | |
| normalized_toml | |
| .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 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 = to_targets( | |
| &features, | |
| &original_toml, | |
| &normalized_toml, | |
| package_root, | |
| edition, | |
| &normalized_package.metabuild, | |
| warnings, | |
| )?; | |
| if targets.iter().all(|t| t.is_custom_build()) { | |
| bail!( | |
| "no targets specified in the manifest\n\ | |
| either src/lib.rs, src/main.rs, a [lib] section, or \ | |
| [[bin]] section must be present" | |
| ) | |
| } | |
| 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(), | |
| conflicts | |
| .iter() | |
| .map(|t| format!(" * `{}` target `{}`", t.kind().description(), t.name(),)) | |
| .join("\n") | |
| )); | |
| }) | |
| } | |
| if let Some(links) = &normalized_package.links { | |
| if !targets.iter().any(|t| t.is_custom_build()) { | |
| bail!( | |
| "package specifies that it links to `{links}` but does not have a custom build script" | |
| ) | |
| } | |
| } | |
| validate_feature_definitions(&features, original_toml.features.as_ref(), warnings)?; | |
| validate_dependencies(original_toml.dependencies.as_ref(), None, None, warnings)?; | |
| validate_dependencies( | |
| original_toml.dev_dependencies(), | |
| None, | |
| Some(DepKind::Development), | |
| warnings, | |
| )?; | |
| validate_dependencies( | |
| original_toml.build_dependencies(), | |
| None, | |
| Some(DepKind::Build), | |
| warnings, | |
| )?; | |
| for (name, platform) in original_toml.target.iter().flatten() { | |
| let platform_kind: Platform = name.parse()?; | |
| platform_kind.check_cfg_attributes(warnings); | |
| platform_kind.check_cfg_keywords(warnings, manifest_file); | |
| let platform_kind = Some(platform_kind); | |
| validate_dependencies( | |
| platform.dependencies.as_ref(), | |
| platform_kind.as_ref(), | |
| None, | |
| warnings, | |
| )?; | |
| validate_dependencies( | |
| platform.build_dependencies(), | |
| platform_kind.as_ref(), | |
| Some(DepKind::Build), | |
| warnings, | |
| )?; | |
| validate_dependencies( | |
| platform.dev_dependencies(), | |
| platform_kind.as_ref(), | |
| Some(DepKind::Development), | |
| warnings, | |
| )?; | |
| } | |
| // Collect the dependencies. | |
| let mut deps = Vec::new(); | |
| let mut manifest_ctx = ManifestContext { | |
| deps: &mut deps, | |
| source_id, | |
| gctx, | |
| warnings, | |
| platform: None, | |
| file: manifest_file, | |
| }; | |
| gather_dependencies( | |
| &mut manifest_ctx, | |
| normalized_toml.dependencies.as_ref(), | |
| None, | |
| )?; | |
| gather_dependencies( | |
| &mut manifest_ctx, | |
| normalized_toml.dev_dependencies(), | |
| Some(DepKind::Development), | |
| )?; | |
| gather_dependencies( | |
| &mut manifest_ctx, | |
| normalized_toml.build_dependencies(), | |
| Some(DepKind::Build), | |
| )?; | |
| for (name, platform) in normalized_toml.target.iter().flatten() { | |
| manifest_ctx.platform = Some(name.parse()?); | |
| gather_dependencies(&mut manifest_ctx, platform.dependencies.as_ref(), None)?; | |
| gather_dependencies( | |
| &mut manifest_ctx, | |
| platform.build_dependencies(), | |
| Some(DepKind::Build), | |
| )?; | |
| gather_dependencies( | |
| &mut manifest_ctx, | |
| platform.dev_dependencies(), | |
| Some(DepKind::Development), | |
| )?; | |
| } | |
| let replace = replace(&normalized_toml, &mut manifest_ctx, &features)?; | |
| let patch = patch(&normalized_toml, &mut manifest_ctx, &features)?; | |
| { | |
| 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 | |
| ); | |
| } | |
| } | |
| } | |
| verify_lints( | |
| normalized_toml | |
| .normalized_lints() | |
| .expect("previously normalized"), | |
| warnings, | |
| )?; | |
| let default = manifest::TomlLints::default(); | |
| let rustflags = lints_to_rustflags( | |
| normalized_toml | |
| .normalized_lints() | |
| .expect("previously normalized") | |
| .unwrap_or(&default), | |
| )?; | |
| let hints = normalized_toml.hints.clone(); | |
| let metadata = ManifestMetadata { | |
| description: normalized_package | |
| .normalized_description() | |
| .expect("previously normalized") | |
| .cloned(), | |
| homepage: normalized_package | |
| .normalized_homepage() | |
| .expect("previously normalized") | |
| .cloned(), | |
| documentation: normalized_package | |
| .normalized_documentation() | |
| .expect("previously normalized") | |
| .cloned(), | |
| readme: normalized_package | |
| .normalized_readme() | |
| .expect("previously normalized") | |
| .cloned(), | |
| authors: normalized_package | |
| .normalized_authors() | |
| .expect("previously normalized") | |
| .cloned() | |
| .unwrap_or_default(), | |
| license: normalized_package | |
| .normalized_license() | |
| .expect("previously normalized") | |
| .cloned(), | |
| license_file: normalized_package | |
| .normalized_license_file() | |
| .expect("previously normalized") | |
| .cloned(), | |
| repository: normalized_package | |
| .normalized_repository() | |
| .expect("previously normalized") | |
| .cloned(), | |
| keywords: normalized_package | |
| .normalized_keywords() | |
| .expect("previously normalized") | |
| .cloned() | |
| .unwrap_or_default(), | |
| categories: normalized_package | |
| .normalized_categories() | |
| .expect("previously normalized") | |
| .cloned() | |
| .unwrap_or_default(), | |
| badges: normalized_toml.badges.clone().unwrap_or_default(), | |
| links: normalized_package.links.clone(), | |
| rust_version: rust_version.clone(), | |
| }; | |
| if let Some(profiles) = &normalized_toml.profile { | |
| let cli_unstable = gctx.cli_unstable(); | |
| validate_profiles(profiles, cli_unstable, &features, warnings)?; | |
| } | |
| let version = normalized_package | |
| .normalized_version() | |
| .expect("previously normalized"); | |
| let publish = match normalized_package | |
| .normalized_publish() | |
| .expect("previously normalized") | |
| { | |
| Some(manifest::VecStringOrBool::VecString(vecstring)) => Some(vecstring.clone()), | |
| Some(manifest::VecStringOrBool::Bool(false)) => Some(vec![]), | |
| Some(manifest::VecStringOrBool::Bool(true)) => None, | |
| None => version.is_none().then_some(vec![]), | |
| }; | |
| if version.is_none() && publish != Some(vec![]) { | |
| bail!("`package.publish` requires `package.version` be specified"); | |
| } | |
| let pkgid = PackageId::new( | |
| package_name.as_str().into(), | |
| version | |
| .cloned() | |
| .unwrap_or_else(|| semver::Version::new(0, 0, 0)), | |
| source_id, | |
| ); | |
| let summary = { | |
| let summary = Summary::new( | |
| pkgid, | |
| deps, | |
| &normalized_toml | |
| .features | |
| .as_ref() | |
| .unwrap_or(&Default::default()) | |
| .iter() | |
| .map(|(k, v)| { | |
| ( | |
| k.to_string().into(), | |
| v.enables().iter().map(InternedString::from).collect(), | |
| ) | |
| }) | |
| .collect(), | |
| normalized_package.links.as_deref(), | |
| rust_version.clone(), | |
| ); | |
| // edition2024 stops exposing implicit features, which will strip weak optional dependencies from `dependencies`, | |
| // need to check whether `dep_name` is stripped as unused dependency | |
| if let Err(ref err) = summary { | |
| if let Some(missing_dep) = err.downcast_ref::<MissingDependencyError>() { | |
| missing_dep_diagnostic( | |
| missing_dep, | |
| &original_toml, | |
| document.as_ref(), | |
| contents.as_deref(), | |
| manifest_file, | |
| gctx, | |
| )?; | |
| } | |
| } | |
| summary? | |
| }; | |
| if summary.features().contains_key("default-features") { | |
| warnings.push( | |
| "`[features]` defines a feature named `default-features` | |
| note: only a feature named `default` will be enabled by default" | |
| .to_string(), | |
| ) | |
| } | |
| if let Some(run) = &normalized_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(), | |
| "target", | |
| ); | |
| bail!("default-run target `{}` not found{}", run, suggestion); | |
| } | |
| } | |
| let default_kind = normalized_package | |
| .default_target | |
| .as_ref() | |
| .map(|t| CompileTarget::new(&*t, gctx.cli_unstable().json_target_spec)) | |
| .transpose()? | |
| .map(CompileKind::Target); | |
| let forced_kind = normalized_package | |
| .forced_target | |
| .as_ref() | |
| .map(|t| CompileTarget::new(&*t, gctx.cli_unstable().json_target_spec)) | |
| .transpose()? | |
| .map(CompileKind::Target); | |
| let include = normalized_package | |
| .normalized_include() | |
| .expect("previously normalized") | |
| .cloned() | |
| .unwrap_or_default(); | |
| let exclude = normalized_package | |
| .normalized_exclude() | |
| .expect("previously normalized") | |
| .cloned() | |
| .unwrap_or_default(); | |
| let links = normalized_package.links.clone(); | |
| let custom_metadata = normalized_package.metadata.clone(); | |
| let im_a_teapot = normalized_package.im_a_teapot; | |
| let default_run = normalized_package.default_run.clone(); | |
| let metabuild = normalized_package.metabuild.clone().map(|sov| sov.0); | |
| let manifest = Manifest::new( | |
| contents.map(Rc::new), | |
| document.map(Arc::new), | |
| Some(Rc::new(original_toml)), | |
| Rc::new(normalized_toml), | |
| summary, | |
| default_kind, | |
| forced_kind, | |
| targets, | |
| exclude, | |
| include, | |
| links, | |
| metadata, | |
| custom_metadata, | |
| publish, | |
| replace, | |
| patch, | |
| workspace_config, | |
| features, | |
| edition, | |
| rust_version, | |
| im_a_teapot, | |
| default_run, | |
| metabuild, | |
| resolve_behavior, | |
| rustflags, | |
| hints, | |
| is_embedded, | |
| ); | |
| if manifest | |
| .normalized_toml() | |
| .package() | |
| .unwrap() | |
| .license_file | |
| .is_some() | |
| && manifest | |
| .normalized_toml() | |
| .package() | |
| .unwrap() | |
| .license | |
| .is_some() | |
| { | |
| warnings.push( | |
| "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_owned(), | |
| ); | |
| } | |
| if let Some(original_toml) = manifest.original_toml() { | |
| warn_on_unused(&original_toml._unused_keys, warnings); | |
| } | |
| manifest.feature_gate()?; | |
| Ok(manifest) | |
| } | |
| fn missing_dep_diagnostic( | |
| missing_dep: &MissingDependencyError, | |
| orig_toml: &TomlManifest, | |
| document: Option<&toml::Spanned<toml::de::DeTable<'static>>>, | |
| contents: Option<&str>, | |
| manifest_file: &Path, | |
| gctx: &GlobalContext, | |
| ) -> CargoResult<()> { | |
| let dep_name = missing_dep.dep_name; | |
| let manifest_path = cwd_rel_path(manifest_file, gctx); | |
| let title = format!( | |
| "feature `{}` includes `{}`, but `{}` is not a dependency", | |
| missing_dep.feature, missing_dep.feature_value, &dep_name | |
| ); | |
| let help = format!("enable the dependency with `dep:{dep_name}`"); | |
| let info_label = format!( | |
| "`{}` is an unused optional dependency since no feature enables it", | |
| &dep_name | |
| ); | |
| let group = Group::with_title(Level::ERROR.primary_title(&title)); | |
| let group = | |
| if let Some(contents) = contents | |
| && let Some(document) = document | |
| { | |
| let feature_span = | |
| get_key_value_span(&document, &["features", missing_dep.feature.as_str()]).unwrap(); | |
| let snippet = Snippet::source(contents) | |
| .path(manifest_path) | |
| .annotation(AnnotationKind::Primary.span(feature_span.value)); | |
| if missing_dep.weak_optional { | |
| let mut orig_deps = vec![ | |
| ( | |
| orig_toml.dependencies.as_ref(), | |
| vec![DepKind::Normal.kind_table()], | |
| ), | |
| ( | |
| orig_toml.build_dependencies.as_ref(), | |
| vec![DepKind::Build.kind_table()], | |
| ), | |
| ]; | |
| for (name, platform) in orig_toml.target.iter().flatten() { | |
| orig_deps.push(( | |
| platform.dependencies.as_ref(), | |
| vec!["target", name, DepKind::Normal.kind_table()], | |
| )); | |
| orig_deps.push(( | |
| platform.build_dependencies.as_ref(), | |
| vec!["target", name, DepKind::Normal.kind_table()], | |
| )); | |
| } | |
| if let Some((_, toml_path)) = orig_deps.iter().find(|(deps, _)| { | |
| if let Some(deps) = deps { | |
| deps.keys().any(|p| *p.as_str() == *dep_name) | |
| } else { | |
| false | |
| } | |
| }) { | |
| let toml_path = toml_path | |
| .iter() | |
| .map(|s| *s) | |
| .chain(std::iter::once(dep_name.as_str())) | |
| .collect::<Vec<_>>(); | |
| let dep_span = get_key_value_span(&document, &toml_path).unwrap(); | |
| group | |
| .element(snippet.annotation( | |
| AnnotationKind::Context.span(dep_span.key).label(info_label), | |
| )) | |
| .element(Level::HELP.message(help)) | |
| } else { | |
| group.element(snippet) | |
| } | |
| } else { | |
| group.element(snippet) | |
| } | |
| } else { | |
| group | |
| }; | |
| if let Err(err) = gctx.shell().print_report(&[group], true) { | |
| return Err(err.into()); | |
| } | |
| Err(AlreadyPrintedError::new(anyhow!("").into()).into()) | |
| } | |
| fn to_virtual_manifest( | |
| contents: Option<String>, | |
| document: Option<toml::Spanned<toml::de::DeTable<'static>>>, | |
| original_toml: manifest::TomlManifest, | |
| normalized_toml: manifest::TomlManifest, | |
| features: Features, | |
| workspace_config: WorkspaceConfig, | |
| source_id: SourceId, | |
| manifest_file: &Path, | |
| gctx: &GlobalContext, | |
| warnings: &mut Vec<String>, | |
| _errors: &mut Vec<String>, | |
| ) -> CargoResult<VirtualManifest> { | |
| let mut deps = Vec::new(); | |
| let (replace, patch) = { | |
| let mut manifest_ctx = ManifestContext { | |
| deps: &mut deps, | |
| source_id, | |
| gctx, | |
| warnings, | |
| platform: None, | |
| file: manifest_file, | |
| }; | |
| ( | |
| replace(&normalized_toml, &mut manifest_ctx, &features)?, | |
| patch(&normalized_toml, &mut manifest_ctx, &features)?, | |
| ) | |
| }; | |
| if let Some(profiles) = &normalized_toml.profile { | |
| validate_profiles(profiles, gctx.cli_unstable(), &features, warnings)?; | |
| } | |
| let resolve_behavior = normalized_toml | |
| .workspace | |
| .as_ref() | |
| .and_then(|ws| ws.resolver.as_deref()) | |
| .map(|r| ResolveBehavior::from_manifest(r)) | |
| .transpose()?; | |
| if let WorkspaceConfig::Member { .. } = &workspace_config { | |
| bail!("virtual manifests must be configured with [workspace]"); | |
| } | |
| let manifest = VirtualManifest::new( | |
| contents.map(Rc::new), | |
| document.map(Rc::new), | |
| Some(Rc::new(original_toml)), | |
| Rc::new(normalized_toml), | |
| replace, | |
| patch, | |
| workspace_config, | |
| features, | |
| resolve_behavior, | |
| ); | |
| if let Some(original_toml) = manifest.original_toml() { | |
| warn_on_unused(&original_toml._unused_keys, warnings); | |
| } | |
| Ok(manifest) | |
| } | |
| fn validate_feature_definitions( | |
| cargo_features: &Features, | |
| features: Option<&BTreeMap<FeatureName, FeatureDefinition>>, | |
| warnings: &mut Vec<String>, | |
| ) -> CargoResult<()> { | |
| let Some(features) = features else { | |
| return Ok(()); | |
| }; | |
| for (feature, feature_definition) in features { | |
| match feature_definition { | |
| FeatureDefinition::Array(..) => {} | |
| FeatureDefinition::Metadata(FeatureMetadata { _unused_keys, .. }) => { | |
| cargo_features.require(Feature::feature_metadata())?; | |
| warnings.extend( | |
| _unused_keys | |
| .keys() | |
| .map(|k| format!("unused manifest key: `features.{feature}.{k}`")), | |
| ); | |
| } | |
| } | |
| } | |
| Ok(()) | |
| } | |
| #[tracing::instrument(skip_all)] | |
| fn validate_dependencies( | |
| original_deps: Option<&BTreeMap<manifest::PackageName, manifest::InheritableDependency>>, | |
| platform: Option<&Platform>, | |
| kind: Option<DepKind>, | |
| warnings: &mut Vec<String>, | |
| ) -> CargoResult<()> { | |
| let Some(dependencies) = original_deps else { | |
| return Ok(()); | |
| }; | |
| for (name_in_toml, v) in dependencies.iter() { | |
| let kind_name = match kind { | |
| Some(k) => k.kind_table(), | |
| None => "dependencies", | |
| }; | |
| let table_in_toml = if let Some(platform) = platform { | |
| format!("target.{platform}.{kind_name}") | |
| } else { | |
| kind_name.to_string() | |
| }; | |
| unused_dep_keys(name_in_toml, &table_in_toml, v.unused_keys(), warnings); | |
| } | |
| Ok(()) | |
| } | |
| struct ManifestContext<'a, 'b> { | |
| deps: &'a mut Vec<Dependency>, | |
| source_id: SourceId, | |
| gctx: &'b GlobalContext, | |
| warnings: &'a mut Vec<String>, | |
| platform: Option<Platform>, | |
| file: &'a Path, | |
| } | |
| #[tracing::instrument(skip_all)] | |
| fn gather_dependencies( | |
| manifest_ctx: &mut ManifestContext<'_, '_>, | |
| normalized_deps: Option<&BTreeMap<manifest::PackageName, manifest::InheritableDependency>>, | |
| kind: Option<DepKind>, | |
| ) -> CargoResult<()> { | |
| let Some(dependencies) = normalized_deps else { | |
| return Ok(()); | |
| }; | |
| for (n, v) in dependencies.iter() { | |
| let resolved = v.normalized().expect("previously normalized"); | |
| let dep = dep_to_dependency(&resolved, n, manifest_ctx, kind)?; | |
| manifest_ctx.deps.push(dep); | |
| } | |
| Ok(()) | |
| } | |
| fn replace( | |
| me: &manifest::TomlManifest, | |
| manifest_ctx: &mut ManifestContext<'_, '_>, | |
| features: &Features, | |
| ) -> CargoResult<Vec<(PackageIdSpec, Dependency)>> { | |
| if me.patch.is_some() && me.replace.is_some() { | |
| bail!("cannot specify both [replace] and [patch]"); | |
| } | |
| let mut replace = Vec::new(); | |
| for (spec, replacement) in me.replace.iter().flatten() { | |
| if let TomlDependency::Detailed(d) = replacement { | |
| if d.builtin { | |
| features | |
| .require(Feature::builtin_dependencies()) | |
| .with_context(|| format!("invalid replacement for `{spec}`"))?; | |
| } | |
| } | |
| 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 = dep_to_dependency(replacement, spec.name(), manifest_ctx, 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 manifest_ctx.warnings, | |
| ); | |
| dep.set_version_req(OptVersionReq::exact(&version)); | |
| replace.push((spec, dep)); | |
| } | |
| Ok(replace) | |
| } | |
| fn patch( | |
| me: &TomlManifest, | |
| manifest_ctx: &mut ManifestContext<'_, '_>, | |
| features: &Features, | |
| ) -> CargoResult<HashMap<Url, Vec<Patch>>> { | |
| let mut patch = HashMap::default(); | |
| for (toml_url, deps) in me.patch.iter().flatten() { | |
| let url = match &toml_url[..] { | |
| CRATES_IO_REGISTRY => CRATES_IO_INDEX.parse().unwrap(), | |
| _ => manifest_ctx | |
| .gctx | |
| .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, | |
| if toml_url == "crates" { | |
| "\nFor crates.io, use [patch.crates-io] (with a dash)" | |
| } else { | |
| "" | |
| } | |
| ) | |
| })?, | |
| }; | |
| patch.insert( | |
| url, | |
| deps.iter() | |
| .map(|(name, dep)| { | |
| if let manifest::TomlDependency::Detailed(d) = dep { | |
| if d.builtin { | |
| features | |
| .require(Feature::builtin_dependencies()) | |
| .with_context(|| format!("resolving patch for `{name}`"))?; | |
| } | |
| } | |
| unused_dep_keys( | |
| name, | |
| &format!("patch.{toml_url}",), | |
| dep.unused_keys(), | |
| &mut manifest_ctx.warnings, | |
| ); | |
| let dep = dep_to_dependency(dep, name, manifest_ctx, None)?; | |
| let loc = PatchLocation::Manifest(manifest_ctx.file.to_path_buf()); | |
| Ok(Patch { dep, loc }) | |
| }) | |
| .collect::<CargoResult<Vec<_>>>()?, | |
| ); | |
| } | |
| Ok(patch) | |
| } | |
| /// Transforms a `patch` entry from Cargo config to a [`Dependency`]. | |
| pub(crate) fn config_patch_to_dependency<P: ResolveToPath + Clone>( | |
| config_patch: &manifest::TomlDependency<P>, | |
| name: &str, | |
| source_id: SourceId, | |
| gctx: &GlobalContext, | |
| warnings: &mut Vec<String>, | |
| ) -> CargoResult<Dependency> { | |
| let manifest_ctx = &mut ManifestContext { | |
| deps: &mut Vec::new(), | |
| source_id, | |
| gctx, | |
| warnings, | |
| platform: None, | |
| // config path doesn't have manifest file path, and doesn't use it. | |
| file: Path::new("unused"), | |
| }; | |
| dep_to_dependency(config_patch, name, manifest_ctx, None) | |
| } | |
| fn dep_to_dependency<P: ResolveToPath + Clone>( | |
| orig: &manifest::TomlDependency<P>, | |
| name_in_toml: &str, | |
| manifest_ctx: &mut ManifestContext<'_, '_>, | |
| kind: Option<DepKind>, | |
| ) -> CargoResult<Dependency> { | |
| let orig = match orig { | |
| manifest::TomlDependency::Simple(version) => &manifest::TomlDetailedDependency::<P> { | |
| version: Some(version.clone()), | |
| ..Default::default() | |
| }, | |
| manifest::TomlDependency::Detailed(details) => details, | |
| }; | |
| if let Some(version) = &orig.version { | |
| if version.contains('+') { | |
| manifest_ctx.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 | |
| )); | |
| } | |
| } | |
| // Early detection of potentially misused feature syntax | |
| // instead of generating a "feature not found" error. | |
| if let Some(features) = &orig.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 = to_dependency_source_id(orig, name_in_toml, manifest_ctx, kind)?; | |
| let (pkg_name, explicit_name_in_toml) = match orig.package { | |
| Some(ref s) => (&s[..], Some(name_in_toml)), | |
| None => (name_in_toml, None), | |
| }; | |
| let version = orig.version.as_deref(); | |
| let mut dep = Dependency::parse(pkg_name, version, new_source_id)?; | |
| dep.set_features(orig.features.iter().flatten()) | |
| .set_default_features(orig.default_features().unwrap_or(true)) | |
| .set_optional(orig.optional.unwrap_or(false)) | |
| .set_platform(manifest_ctx.platform.clone()); | |
| if let Some(registry) = &orig.registry { | |
| let registry_id = SourceId::alt_registry(manifest_ctx.gctx, registry)?; | |
| dep.set_registry_id(registry_id); | |
| } | |
| if let Some(registry_index) = &orig.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) = orig.public { | |
| dep.set_public(p); | |
| } | |
| if let (Some(artifact), is_lib, target) = ( | |
| orig.artifact.as_ref(), | |
| orig.lib.unwrap_or(false), | |
| orig.target.as_deref(), | |
| ) { | |
| if manifest_ctx.gctx.cli_unstable().bindeps { | |
| let artifact = Artifact::parse( | |
| &artifact.0, | |
| is_lib, | |
| target, | |
| manifest_ctx.gctx.cli_unstable().json_target_spec, | |
| )?; | |
| 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 orig.lib.is_some() || orig.target.is_some() { | |
| for (is_set, specifier) in [ | |
| (orig.lib.is_some(), "lib"), | |
| (orig.target.is_some(), "target"), | |
| ] { | |
| if !is_set { | |
| continue; | |
| } | |
| bail!( | |
| "'{}' specifier cannot be used without an 'artifact = …' value ({})", | |
| specifier, | |
| name_in_toml | |
| ) | |
| } | |
| } | |
| Ok(dep) | |
| } | |
| fn to_dependency_source_id<P: ResolveToPath + Clone>( | |
| orig: &manifest::TomlDetailedDependency<P>, | |
| name_in_toml: &str, | |
| manifest_ctx: &mut ManifestContext<'_, '_>, | |
| kind: Option<DepKind>, | |
| ) -> CargoResult<SourceId> { | |
| let manifest::TomlDetailedDependency { | |
| version, | |
| registry, | |
| registry_index, | |
| path, | |
| base: _, | |
| git, | |
| branch, | |
| tag, | |
| rev, | |
| builtin, | |
| features: _, | |
| optional: _, | |
| default_features: _, | |
| default_features2: _, | |
| package: _, | |
| public: _, | |
| artifact: _, | |
| lib: _, | |
| target: _, | |
| _unused_keys: _, | |
| } = orig; | |
| let builtin_kind = if *builtin { Some(()) } else { None }; | |
| let git_kind = if let Some(git) = git { | |
| Some(git) | |
| } else { | |
| let git_only_keys = [ | |
| (&orig.branch, "branch"), | |
| (&orig.tag, "tag"), | |
| (&orig.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 | |
| ); | |
| } | |
| } | |
| None | |
| }; | |
| let path_kind = if let Some(path) = path { | |
| Some(path) | |
| } else { | |
| None | |
| }; | |
| let registry_kind = if version.is_some() { | |
| Some(()) | |
| } else { | |
| if orig.registry.is_some() { | |
| manifest_ctx.warnings.push(format!( | |
| "key `registry` is ignored for dependency `{name_in_toml}`", | |
| )); | |
| } | |
| if orig.registry_index.is_some() { | |
| manifest_ctx.warnings.push(format!( | |
| "key `registry-index` is ignored for dependency `{name_in_toml}`", | |
| )); | |
| } | |
| None | |
| }; | |
| let source = match (builtin_kind, git_kind, path_kind, registry_kind) { | |
| (Some(()), None, None, None) => { | |
| if kind == Some(DepKind::Build) { | |
| bail!("builtin dependency `{name_in_toml}` cannot be used as a build dependency") | |
| } | |
| SourceId::for_builtin()? | |
| } | |
| (None, Some(git), None, _) => { | |
| let n_details = [&branch, &tag, &rev].iter().filter(|d| d.is_some()).count(); | |
| if n_details > 1 { | |
| bail!( | |
| "dependency ({name_in_toml}) specification is ambiguous. \ | |
| Only one of `branch`, `tag` or `rev` is allowed.", | |
| ); | |
| } | |
| let reference = orig | |
| .branch | |
| .clone() | |
| .map(GitReference::Branch) | |
| .or_else(|| orig.tag.clone().map(GitReference::Tag)) | |
| .or_else(|| orig.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 `#{fragment}` in git URL is ignored for dependency ({name_in_toml}). \ | |
| If you were trying to specify a specific git revision, \ | |
| use `rev = \"{fragment}\"` in the dependency declaration.", | |
| ); | |
| manifest_ctx.warnings.push(msg); | |
| } | |
| SourceId::for_git(&loc, reference)? | |
| } | |
| (None, None, Some(path), _) => { | |
| let path = path.resolve(manifest_ctx.gctx); | |
| // 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 manifest_ctx.source_id.is_path() { | |
| let path = manifest_ctx.file.parent().unwrap().join(path); | |
| let path = paths::normalize_path(&path); | |
| SourceId::for_path(&path)? | |
| } else { | |
| manifest_ctx.source_id | |
| } | |
| } | |
| (None, None, None, Some(())) => match (registry, registry_index) { | |
| (Some(_), Some(_)) => { | |
| bail!( | |
| "dependency ({name_in_toml}) specification is ambiguous. \ | |
| Only one of `registry` or `registry-index` is allowed.", | |
| ) | |
| } | |
| (Some(registry), None) => SourceId::alt_registry(manifest_ctx.gctx, registry)?, | |
| (None, Some(registry_index)) => { | |
| let url = registry_index.into_url()?; | |
| SourceId::for_registry(&url)? | |
| } | |
| (None, None) => SourceId::crates_io(manifest_ctx.gctx)?, | |
| }, | |
| (None, None, None, None) => { | |
| bail!( | |
| "dependency ({name_in_toml}) specified without \ | |
| providing a local path, Git repository, version, or \ | |
| workspace dependency to use" | |
| ) | |
| } | |
| (builtin_kind, git_kind, path_kind, registry_kind) => { | |
| if builtin_kind.is_some() { | |
| if registry_kind.is_some() { | |
| bail!( | |
| "builtin dependency `{name_in_toml}` cannot be combined with a version requirement\n\ | |
| Builtin dependencies are unversioned." | |
| ) | |
| } | |
| bail!( | |
| "dependency ({name_in_toml}) specification is ambiguous. \ | |
| `builtin = true` cannot be combined with any other dependency source" | |
| ) | |
| } | |
| if git_kind.is_some() && path_kind.is_some() { | |
| bail!( | |
| "dependency ({name_in_toml}) specification is ambiguous. \ | |
| Only one of `git` or `path` is allowed.", | |
| ) | |
| } | |
| bail!("dependency ({name_in_toml}) specification is ambiguous") | |
| } | |
| }; | |
| Ok(source) | |
| } | |
| pub(crate) fn lookup_path_base<'a>( | |
| base: &PathBaseName, | |
| gctx: &GlobalContext, | |
| workspace_root: &dyn Fn() -> CargoResult<&'a Path>, | |
| features: &Features, | |
| ) -> CargoResult<PathBuf> { | |
| features.require(Feature::path_bases())?; | |
| // Look up the relevant base in the Config and use that as the root. | |
| if let Some(path_bases) = | |
| gctx.get::<Option<ConfigRelativePath>>(["path-bases", base.as_str()])? | |
| { | |
| Ok(path_bases.resolve_path(gctx)) | |
| } else { | |
| // Otherwise, check the built-in bases. | |
| match base.as_str() { | |
| "workspace" => Ok(workspace_root()?.to_path_buf()), | |
| _ => bail!( | |
| "path base `{base}` is undefined. \ | |
| You must add an entry for `{base}` in the Cargo configuration [path-bases] table." | |
| ), | |
| } | |
| } | |
| } | |
| pub trait ResolveToPath { | |
| fn resolve(&self, gctx: &GlobalContext) -> PathBuf; | |
| } | |
| impl ResolveToPath for String { | |
| fn resolve(&self, _: &GlobalContext) -> PathBuf { | |
| self.into() | |
| } | |
| } | |
| impl ResolveToPath for ConfigRelativePath { | |
| fn resolve(&self, gctx: &GlobalContext) -> PathBuf { | |
| self.resolve_path(gctx) | |
| } | |
| } | |
| /// 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. | |
| #[tracing::instrument(skip_all)] | |
| fn unique_build_targets( | |
| targets: &[Target], | |
| package_root: &Path, | |
| ) -> Result<(), HashMap<PathBuf, Vec<Target>>> { | |
| let mut source_targets = HashMap::<_, Vec<_>>::default(); | |
| 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::<HashMap<_, _>>(); | |
| if !conflict_targets.is_empty() { | |
| return Err(conflict_targets); | |
| } | |
| Ok(()) | |
| } | |
| /// 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`. | |
| fn validate_profiles( | |
| profiles: &manifest::TomlProfiles, | |
| cli_unstable: &CliUnstable, | |
| features: &Features, | |
| warnings: &mut Vec<String>, | |
| ) -> CargoResult<()> { | |
| for (name, profile) in &profiles.0 { | |
| validate_profile(profile, name, cli_unstable, features, warnings)?; | |
| } | |
| Ok(()) | |
| } | |
| /// Checks syntax validity and unstable feature gate for a given profile. | |
| pub fn validate_profile( | |
| root: &manifest::TomlProfile, | |
| name: &str, | |
| cli_unstable: &CliUnstable, | |
| features: &Features, | |
| warnings: &mut Vec<String>, | |
| ) -> CargoResult<()> { | |
| validate_profile_layer(root, cli_unstable, features)?; | |
| if let Some(ref profile) = root.build_override { | |
| validate_profile_override(profile, "build-override")?; | |
| validate_profile_layer(profile, cli_unstable, features)?; | |
| } | |
| if let Some(ref packages) = root.package { | |
| for profile in packages.values() { | |
| validate_profile_override(profile, "package")?; | |
| validate_profile_layer(profile, cli_unstable, features)?; | |
| } | |
| } | |
| if let Some(dir_name) = &root.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 | |
| ); | |
| } | |
| match name { | |
| "doc" => { | |
| warnings.push("profile `doc` is deprecated and has no effect".to_string()); | |
| } | |
| "test" | "bench" => { | |
| if root.panic.is_some() { | |
| warnings.push(format!("`panic` setting is ignored for `{}` profile", name)) | |
| } | |
| } | |
| _ => {} | |
| } | |
| if let Some(panic) = &root.panic { | |
| if panic != "unwind" && panic != "abort" && panic != "immediate-abort" { | |
| bail!( | |
| "`panic` setting of `{}` is not a valid setting, \ | |
| must be `unwind`, `abort`, or `immediate-abort`.", | |
| panic | |
| ); | |
| } | |
| } | |
| if let Some(manifest::StringOrBool::String(arg)) = &root.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.", | |
| ); | |
| } | |
| } | |
| Ok(()) | |
| } | |
| /// Validates a profile. | |
| /// | |
| /// This is a shallow check, which is reused for the profile itself and any overrides. | |
| fn validate_profile_layer( | |
| profile: &manifest::TomlProfile, | |
| cli_unstable: &CliUnstable, | |
| features: &Features, | |
| ) -> CargoResult<()> { | |
| if profile.codegen_backend.is_some() { | |
| match ( | |
| features.require(Feature::codegen_backend()), | |
| cli_unstable.codegen_backend, | |
| ) { | |
| (Err(e), false) => return Err(e), | |
| _ => {} | |
| } | |
| } | |
| if profile.rustflags.is_some() { | |
| match ( | |
| features.require(Feature::profile_rustflags()), | |
| cli_unstable.profile_rustflags, | |
| ) { | |
| (Err(e), false) => return Err(e), | |
| _ => {} | |
| } | |
| } | |
| if profile.panic.as_deref() == Some("immediate-abort") { | |
| match ( | |
| features.require(Feature::panic_immediate_abort()), | |
| cli_unstable.panic_immediate_abort, | |
| ) { | |
| (Err(e), false) => return Err(e), | |
| _ => {} | |
| } | |
| } | |
| Ok(()) | |
| } | |
| /// Validation that is specific to an override. | |
| fn validate_profile_override(profile: &manifest::TomlProfile, which: &str) -> CargoResult<()> { | |
| if profile.package.is_some() { | |
| bail!("package-specific profiles cannot be nested"); | |
| } | |
| if profile.build_override.is_some() { | |
| bail!("build-override profiles cannot be nested"); | |
| } | |
| if profile.panic.is_some() { | |
| bail!("`panic` may not be specified in a `{}` profile", which) | |
| } | |
| if profile.lto.is_some() { | |
| bail!("`lto` may not be specified in a `{}` profile", which) | |
| } | |
| if profile.rpath.is_some() { | |
| bail!("`rpath` may not be specified in a `{}` profile", which) | |
| } | |
| Ok(()) | |
| } | |
| fn verify_lints( | |
| lints: Option<&manifest::TomlLints>, | |
| warnings: &mut Vec<String>, | |
| ) -> CargoResult<()> { | |
| let Some(lints) = lints else { | |
| return Ok(()); | |
| }; | |
| for (tool, lints) in lints { | |
| let supported = ["cargo", "clippy", "rust", "rustdoc"]; | |
| if !supported.contains(&tool.as_str()) { | |
| let message = format!( | |
| "unrecognized lint tool `lints.{tool}`, specifying unrecognized tools may break in the future. | |
| supported tools: {}", | |
| supported.join(", "), | |
| ); | |
| warnings.push(message); | |
| continue; | |
| } | |
| let mut seen_normalized: HashMap<String, String> = HashMap::default(); | |
| for (name, config) in lints { | |
| let normalized = name.replace('-', "_"); | |
| if name.contains('-') { | |
| warnings.push(format!( | |
| "`lints.{tool}.{name}` is deprecated in favor of \ | |
| `lints.{tool}.{normalized}` and will not work in a \ | |
| future edition" | |
| )); | |
| } | |
| if let Some(existing) = seen_normalized.get(&normalized) { | |
| warnings.push(format!( | |
| "duplicate lint `{existing}` in `[lints.{tool}]`, \ | |
| conflicts with `{name}` and will not work in a future edition" | |
| )); | |
| } | |
| seen_normalized.insert(normalized.clone(), name.to_string()); | |
| 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") | |
| } | |
| } else if let Some(config) = config.config() { | |
| for config_name in config.keys() { | |
| // manually report unused manifest key warning since we collect all the "extra" | |
| // keys and values inside the config table | |
| let expected = EXPECTED_LINT_CONFIG.contains(&(tool, name, config_name)); | |
| if !expected { | |
| let message = | |
| format!("unused manifest key: `lints.{tool}.{name}.{config_name}`"); | |
| warnings.push(message); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Ok(()) | |
| } | |
| static EXPECTED_LINT_CONFIG: &[(&str, &str, &str)] = &[ | |
| // forwarded to rustc/rustdoc | |
| ("rust", "unexpected_cfgs", "check-cfg"), | |
| ]; | |
| fn lints_to_rustflags(lints: &manifest::TomlLints) -> CargoResult<Vec<String>> { | |
| let mut rustflags = lints | |
| .iter() | |
| // We don't want to pass any of the `cargo` lints to `rustc` | |
| .filter(|(tool, _)| tool != &"cargo") | |
| .flat_map(|(tool, lints)| { | |
| lints.iter().map(move |(name, config)| { | |
| let flag = match config.level() { | |
| manifest::TomlLintLevel::Forbid => "--forbid", | |
| manifest::TomlLintLevel::Deny => "--deny", | |
| manifest::TomlLintLevel::Warn => "--warn", | |
| manifest::TomlLintLevel::Allow => "--allow", | |
| }; | |
| 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::<Vec<_>>(); | |
| rustflags.sort(); | |
| let mut rustflags: Vec<_> = rustflags.into_iter().map(|(_, _, option)| option).collect(); | |
| // Also include the custom arguments specified in `[lints.rust.unexpected_cfgs.check_cfg]` | |
| if let Some(rust_lints) = lints.get("rust") { | |
| if let Some(unexpected_cfgs) = rust_lints.get("unexpected_cfgs") { | |
| if let Some(config) = unexpected_cfgs.config() { | |
| if let Some(check_cfg) = config.get("check-cfg") { | |
| if let Ok(check_cfgs) = toml::Value::try_into::<Vec<String>>(check_cfg.clone()) | |
| { | |
| for check_cfg in check_cfgs { | |
| rustflags.push("--check-cfg".to_string()); | |
| rustflags.push(check_cfg); | |
| } | |
| // error about `check-cfg` not being a list-of-string | |
| } else { | |
| bail!("`lints.rust.unexpected_cfgs.check-cfg` must be a list of string"); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Ok(rustflags) | |
| } | |
| fn emit_frontmatter_diagnostic( | |
| e: crate::util::frontmatter::FrontmatterError, | |
| contents: &str, | |
| manifest_file: &Path, | |
| gctx: &GlobalContext, | |
| ) -> anyhow::Error { | |
| let primary_span = e.primary_span(); | |
| // Get the path to the manifest, relative to the cwd | |
| let manifest_path = diff_paths(manifest_file, gctx.cwd()) | |
| .unwrap_or_else(|| manifest_file.to_path_buf()) | |
| .display() | |
| .to_string(); | |
| let group = Group::with_title(Level::ERROR.primary_title(e.message())).element( | |
| Snippet::source(contents) | |
| .path(manifest_path) | |
| .annotation(AnnotationKind::Primary.span(primary_span)) | |
| .annotations( | |
| e.visible_spans() | |
| .iter() | |
| .map(|s| AnnotationKind::Visible.span(s.clone())), | |
| ), | |
| ); | |
| if let Err(err) = gctx.shell().print_report(&[group], true) { | |
| return err.into(); | |
| } | |
| return AlreadyPrintedError::new(e.into()).into(); | |
| } | |
| fn emit_toml_diagnostic( | |
| e: toml::de::Error, | |
| contents: &str, | |
| manifest_file: &Path, | |
| gctx: &GlobalContext, | |
| ) -> anyhow::Error { | |
| let Some(span) = e.span() else { | |
| return e.into(); | |
| }; | |
| // Get the path to the manifest, relative to the cwd | |
| let manifest_path = diff_paths(manifest_file, gctx.cwd()) | |
| .unwrap_or_else(|| manifest_file.to_path_buf()) | |
| .display() | |
| .to_string(); | |
| let group = Group::with_title(Level::ERROR.primary_title(e.message())).element( | |
| Snippet::source(contents) | |
| .path(manifest_path) | |
| .annotation(AnnotationKind::Primary.span(span)), | |
| ); | |
| if let Err(err) = gctx.shell().print_report(&[group], true) { | |
| return err.into(); | |
| } | |
| return AlreadyPrintedError::new(e.into()).into(); | |
| } | |
| /// Warn about paths that have been deprecated and may conflict. | |
| fn deprecated_underscore<T>( | |
| old: &Option<T>, | |
| new: &Option<T>, | |
| new_path: &str, | |
| name: &str, | |
| kind: &str, | |
| edition: Edition, | |
| warnings: &mut Vec<String>, | |
| ) -> CargoResult<()> { | |
| let old_path = new_path.replace("-", "_"); | |
| if old.is_some() && Edition::Edition2024 <= edition { | |
| anyhow::bail!( | |
| "`{old_path}` is unsupported as of the 2024 edition; instead use `{new_path}`\n(in the `{name}` {kind})" | |
| ); | |
| } else if old.is_some() && new.is_some() { | |
| warnings.push(format!( | |
| "`{old_path}` is redundant with `{new_path}`, preferring `{new_path}` in the `{name}` {kind}" | |
| )) | |
| } else if old.is_some() { | |
| warnings.push(format!( | |
| "`{old_path}` is deprecated in favor of `{new_path}` and will not work in the 2024 edition\n(in the `{name}` {kind})" | |
| )) | |
| } | |
| Ok(()) | |
| } | |
| fn warn_on_unused(unused: &BTreeSet<String>, warnings: &mut Vec<String>) { | |
| use std::fmt::Write as _; | |
| for key in unused { | |
| let mut message = format!("unused manifest key: {}", key); | |
| if TOP_LEVEL_CONFIG_KEYS.iter().any(|c| c == key) { | |
| write!( | |
| &mut message, | |
| "\nhelp: {key} is a valid .cargo/config.toml key" | |
| ) | |
| .unwrap(); | |
| } | |
| warnings.push(message); | |
| } | |
| } | |
| fn unused_dep_keys( | |
| dep_name: &str, | |
| kind: &str, | |
| unused_keys: Vec<String>, | |
| warnings: &mut Vec<String>, | |
| ) { | |
| for unused in unused_keys { | |
| let key = format!("unused manifest key: {kind}.{dep_name}.{unused}"); | |
| warnings.push(key); | |
| } | |
| } | |
| /// Make the [`Package`] self-contained so its ready for packaging | |
| pub fn prepare_for_publish( | |
| me: &Package, | |
| ws: &Workspace<'_>, | |
| packaged_files: Option<&[PathBuf]>, | |
| ) -> CargoResult<Package> { | |
| let contents = me.manifest().contents(); | |
| let document = me.manifest().document(); | |
| let original_toml = prepare_toml_for_publish( | |
| me.manifest().normalized_toml(), | |
| ws, | |
| me.root(), | |
| packaged_files, | |
| )?; | |
| let normalized_toml = original_toml.clone(); | |
| let features = me.manifest().unstable_features().clone(); | |
| let workspace_config = me.manifest().workspace_config().clone(); | |
| let source_id = me.package_id().source_id(); | |
| let mut warnings = Default::default(); | |
| let mut errors = Default::default(); | |
| let gctx = ws.gctx(); | |
| let manifest = to_real_manifest( | |
| contents.map(|c| c.to_owned()), | |
| document.cloned(), | |
| original_toml, | |
| normalized_toml, | |
| features, | |
| workspace_config, | |
| source_id, | |
| me.manifest_path(), | |
| me.manifest().is_embedded(), | |
| gctx, | |
| &mut warnings, | |
| &mut errors, | |
| )?; | |
| let new_pkg = Package::new(manifest, me.manifest_path()); | |
| Ok(new_pkg) | |
| } | |
| /// Prepares the manifest for publishing. | |
| // - Path and git components of dependency specifications are removed. | |
| // - License path is updated to point within the package. | |
| fn prepare_toml_for_publish( | |
| me: &manifest::TomlManifest, | |
| ws: &Workspace<'_>, | |
| package_root: &Path, | |
| packaged_files: Option<&[PathBuf]>, | |
| ) -> CargoResult<manifest::TomlManifest> { | |
| let gctx = ws.gctx(); | |
| if me | |
| .cargo_features | |
| .iter() | |
| .flat_map(|f| f.iter()) | |
| .any(|f| f == "open-namespaces") | |
| { | |
| anyhow::bail!("cannot publish with `open-namespaces`") | |
| } | |
| let mut package = me.package().unwrap().clone(); | |
| package.workspace = None; | |
| // Validates if build script file is included in package. If not, warn and ignore. | |
| if let Some(custom_build_scripts) = package.normalized_build().expect("previously normalized") { | |
| let mut included_scripts = Vec::new(); | |
| for script in custom_build_scripts { | |
| let path = Path::new(script).to_path_buf(); | |
| let included = packaged_files.map(|i| i.contains(&path)).unwrap_or(true); | |
| if included { | |
| let path = path | |
| .into_os_string() | |
| .into_string() | |
| .map_err(|_err| anyhow::format_err!("non-UTF8 `package.build`"))?; | |
| let path = normalize_path_string_sep(path); | |
| included_scripts.push(path); | |
| } else { | |
| ws.gctx().shell().warn(format!( | |
| "ignoring `package.build` entry `{}` as it is not included in the published package", | |
| path.display() | |
| ))?; | |
| } | |
| } | |
| package.build = Some(match included_scripts.len() { | |
| 0 => TomlPackageBuild::Auto(false), | |
| 1 => TomlPackageBuild::SingleScript(included_scripts[0].clone()), | |
| _ => TomlPackageBuild::MultipleScript(included_scripts), | |
| }); | |
| } | |
| 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_value()) | |
| .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()); | |
| } | |
| if let Some(license_file) = &package.license_file { | |
| let license_file = license_file | |
| .as_value() | |
| .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 let Ok(license_file) = abs_license_path.strip_prefix(package_root) { | |
| package.license_file = Some(manifest::InheritableField::Value( | |
| normalize_path_string_sep( | |
| license_file | |
| .to_str() | |
| .ok_or_else(|| anyhow::format_err!("non-UTF8 `package.license-file`"))? | |
| .to_owned(), | |
| ), | |
| )); | |
| } else { | |
| // 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(manifest::InheritableField::Value( | |
| license_path | |
| .file_name() | |
| .unwrap() | |
| .to_str() | |
| .unwrap() | |
| .to_string(), | |
| )); | |
| } | |
| } | |
| if let Some(readme) = &package.readme { | |
| let readme = readme | |
| .as_value() | |
| .context("readme should have been resolved before `prepare_for_publish()`")?; | |
| match readme { | |
| manifest::StringOrBool::String(readme) => { | |
| let readme_path = Path::new(&readme); | |
| let abs_readme_path = paths::normalize_path(&package_root.join(readme_path)); | |
| if let Ok(readme_path) = abs_readme_path.strip_prefix(package_root) { | |
| package.readme = Some(manifest::InheritableField::Value(StringOrBool::String( | |
| normalize_path_string_sep( | |
| readme_path | |
| .to_str() | |
| .ok_or_else(|| { | |
| anyhow::format_err!("non-UTF8 `package.license-file`") | |
| })? | |
| .to_owned(), | |
| ), | |
| ))); | |
| } else { | |
| // 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(manifest::InheritableField::Value( | |
| manifest::StringOrBool::String( | |
| readme_path | |
| .file_name() | |
| .unwrap() | |
| .to_str() | |
| .unwrap() | |
| .to_string(), | |
| ), | |
| )); | |
| } | |
| } | |
| manifest::StringOrBool::Bool(_) => {} | |
| } | |
| } | |
| let lib = if let Some(target) = &me.lib { | |
| prepare_target_for_publish(target, packaged_files, "library", ws.gctx())? | |
| } else { | |
| None | |
| }; | |
| let bin = prepare_targets_for_publish(me.bin.as_ref(), packaged_files, "binary", ws.gctx())?; | |
| let example = | |
| prepare_targets_for_publish(me.example.as_ref(), packaged_files, "example", ws.gctx())?; | |
| let test = prepare_targets_for_publish(me.test.as_ref(), packaged_files, "test", ws.gctx())?; | |
| let bench = | |
| prepare_targets_for_publish(me.bench.as_ref(), packaged_files, "benchmark", ws.gctx())?; | |
| let all = |_d: &manifest::TomlDependency| true; | |
| let mut manifest = manifest::TomlManifest { | |
| cargo_features: me.cargo_features.clone(), | |
| package: Some(package), | |
| project: None, | |
| badges: me.badges.clone(), | |
| features: me.features.clone(), | |
| lib, | |
| bin, | |
| example, | |
| test, | |
| bench, | |
| dependencies: map_deps(gctx, me.dependencies.as_ref(), all)?, | |
| dev_dependencies: map_deps( | |
| gctx, | |
| me.dev_dependencies(), | |
| manifest::TomlDependency::is_version_specified, | |
| )?, | |
| dev_dependencies2: None, | |
| build_dependencies: map_deps(gctx, me.build_dependencies(), all)?, | |
| build_dependencies2: None, | |
| target: match me.target.as_ref().map(|target_map| { | |
| target_map | |
| .iter() | |
| .map(|(k, v)| { | |
| Ok(( | |
| k.clone(), | |
| manifest::TomlPlatform { | |
| dependencies: map_deps(gctx, v.dependencies.as_ref(), all)?, | |
| dev_dependencies: map_deps( | |
| gctx, | |
| v.dev_dependencies(), | |
| manifest::TomlDependency::is_version_specified, | |
| )?, | |
| dev_dependencies2: None, | |
| build_dependencies: map_deps(gctx, v.build_dependencies(), all)?, | |
| build_dependencies2: None, | |
| }, | |
| )) | |
| }) | |
| .collect() | |
| }) { | |
| Some(Ok(v)) => Some(v), | |
| Some(Err(e)) => return Err(e), | |
| None => None, | |
| }, | |
| lints: me.lints.clone(), | |
| hints: me.hints.clone(), | |
| workspace: None, | |
| profile: me.profile.clone(), | |
| patch: None, | |
| replace: None, | |
| _unused_keys: Default::default(), | |
| }; | |
| strip_features(&mut manifest); | |
| return Ok(manifest); | |
| fn strip_features(manifest: &mut TomlManifest) { | |
| fn insert_dep_name( | |
| dep_name_set: &mut BTreeSet<manifest::PackageName>, | |
| deps: Option<&BTreeMap<manifest::PackageName, manifest::InheritableDependency>>, | |
| ) { | |
| let Some(deps) = deps else { | |
| return; | |
| }; | |
| deps.iter().for_each(|(k, _v)| { | |
| dep_name_set.insert(k.clone()); | |
| }); | |
| } | |
| let mut dep_name_set = BTreeSet::new(); | |
| insert_dep_name(&mut dep_name_set, manifest.dependencies.as_ref()); | |
| insert_dep_name(&mut dep_name_set, manifest.dev_dependencies()); | |
| insert_dep_name(&mut dep_name_set, manifest.build_dependencies()); | |
| if let Some(target_map) = manifest.target.as_ref() { | |
| target_map.iter().for_each(|(_k, v)| { | |
| insert_dep_name(&mut dep_name_set, v.dependencies.as_ref()); | |
| insert_dep_name(&mut dep_name_set, v.dev_dependencies()); | |
| insert_dep_name(&mut dep_name_set, v.build_dependencies()); | |
| }); | |
| } | |
| let features = manifest.features.as_mut(); | |
| let Some(features) = features else { | |
| return; | |
| }; | |
| for feature in features.values_mut() { | |
| let enables = match feature { | |
| FeatureDefinition::Array(enables) => enables, | |
| FeatureDefinition::Metadata(metadata) => &mut metadata.enables, | |
| }; | |
| enables.retain(|feature_dep| { | |
| let feature_value = FeatureValue::new(feature_dep.as_str().into()); | |
| match feature_value { | |
| FeatureValue::Dep { dep_name } | FeatureValue::DepFeature { dep_name, .. } => { | |
| let k = &manifest::PackageName::new(dep_name.to_string()).unwrap(); | |
| dep_name_set.contains(k) | |
| } | |
| _ => true, | |
| } | |
| }); | |
| } | |
| } | |
| fn map_deps( | |
| gctx: &GlobalContext, | |
| deps: Option<&BTreeMap<manifest::PackageName, manifest::InheritableDependency>>, | |
| filter: impl Fn(&manifest::TomlDependency) -> bool, | |
| ) -> CargoResult<Option<BTreeMap<manifest::PackageName, manifest::InheritableDependency>>> { | |
| let Some(deps) = deps else { | |
| return Ok(None); | |
| }; | |
| let deps = deps | |
| .iter() | |
| .filter(|(_k, v)| { | |
| if let manifest::InheritableDependency::Value(def) = v { | |
| filter(def) | |
| } else { | |
| false | |
| } | |
| }) | |
| .map(|(k, v)| Ok((k.clone(), map_dependency(gctx, v)?))) | |
| .collect::<CargoResult<BTreeMap<_, _>>>()?; | |
| Ok(Some(deps)) | |
| } | |
| fn map_dependency( | |
| gctx: &GlobalContext, | |
| dep: &manifest::InheritableDependency, | |
| ) -> CargoResult<manifest::InheritableDependency> { | |
| let dep = match dep { | |
| manifest::InheritableDependency::Value(manifest::TomlDependency::Detailed(d)) => { | |
| let mut d = d.clone(); | |
| // Path dependencies become crates.io deps. | |
| d.path.take(); | |
| d.base.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(gctx.get_registry_index(®istry)?.to_string()); | |
| } | |
| Ok(d) | |
| } | |
| manifest::InheritableDependency::Value(manifest::TomlDependency::Simple(s)) => { | |
| Ok(manifest::TomlDetailedDependency { | |
| version: Some(s.clone()), | |
| ..Default::default() | |
| }) | |
| } | |
| _ => unreachable!(), | |
| }; | |
| dep.map(manifest::TomlDependency::Detailed) | |
| .map(manifest::InheritableDependency::Value) | |
| } | |
| } | |
| pub fn prepare_targets_for_publish( | |
| targets: Option<&Vec<manifest::TomlTarget>>, | |
| packaged_files: Option<&[PathBuf]>, | |
| context: &str, | |
| gctx: &GlobalContext, | |
| ) -> CargoResult<Option<Vec<manifest::TomlTarget>>> { | |
| let Some(targets) = targets else { | |
| return Ok(None); | |
| }; | |
| let mut prepared = Vec::with_capacity(targets.len()); | |
| for target in targets { | |
| let Some(target) = prepare_target_for_publish(target, packaged_files, context, gctx)? | |
| else { | |
| continue; | |
| }; | |
| prepared.push(target); | |
| } | |
| if prepared.is_empty() { | |
| Ok(None) | |
| } else { | |
| Ok(Some(prepared)) | |
| } | |
| } | |
| pub fn prepare_target_for_publish( | |
| target: &manifest::TomlTarget, | |
| packaged_files: Option<&[PathBuf]>, | |
| context: &str, | |
| gctx: &GlobalContext, | |
| ) -> CargoResult<Option<manifest::TomlTarget>> { | |
| let path = target.path.as_ref().expect("previously normalized"); | |
| let path = &path.0; | |
| if let Some(packaged_files) = packaged_files { | |
| if !packaged_files.contains(&path) { | |
| let name = target.name.as_ref().expect("previously normalized"); | |
| gctx.shell().warn(format!( | |
| "ignoring {context} `{name}` as `{}` is not included in the published package", | |
| path.display() | |
| ))?; | |
| return Ok(None); | |
| } | |
| } | |
| let mut target = target.clone(); | |
| let path = normalize_path_sep(path.to_path_buf(), context)?; | |
| target.path = Some(manifest::PathValue(path.into())); | |
| Ok(Some(target)) | |
| } | |
| fn normalize_path_sep(path: PathBuf, context: &str) -> CargoResult<PathBuf> { | |
| let path = path | |
| .into_os_string() | |
| .into_string() | |
| .map_err(|_err| anyhow::format_err!("non-UTF8 path for {context}"))?; | |
| let path = normalize_path_string_sep(path); | |
| Ok(path.into()) | |
| } | |
| pub fn normalize_path_string_sep(path: String) -> String { | |
| if std::path::MAIN_SEPARATOR != '/' { | |
| path.replace(std::path::MAIN_SEPARATOR, "/") | |
| } else { | |
| path | |
| } | |
| } |