blob: 6e57423d0b678694414b5e6d94c479410597f550 [file] [edit]
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, 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, gctx, &mut warnings)?;
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,
gctx: &GlobalContext,
warnings: &mut Vec<String>,
) -> 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(), gctx, warnings)?;
if let Some(ws_deps) = &toml_config.dependencies {
for (name, dep) in ws_deps {
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() {
bail!("this virtual manifest specifies a `{field}` section, which is not allowed");
}
}
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.
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, Vec<String>>>,
) -> CargoResult<Option<BTreeMap<manifest::FeatureName, Vec<String>>>> {
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;
}
}
}
}
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);
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), edition, warnings)?;
}
// member: default-features = false and
// workspace: dep = "1.0" should ignore member default-features
(Some(false), None) => {
deprecated_ws_default_features(name, None, edition, 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>,
edition: Edition,
warnings: &mut Vec<String>,
) -> CargoResult<()> {
let ws_def_feat = match ws_def_feat {
Some(true) => "true",
Some(false) => "false",
None => "not specified",
};
if Edition::Edition2024 <= edition {
anyhow::bail!("`default-features = false` cannot override workspace's `default-features`");
} else {
warnings.push(format!(
"`default-features` is ignored for {label}, since `default-features` was \
{ws_def_feat} for `workspace.dependencies.{label}`, \
this could become a hard error in the future"
));
}
Ok(())
}
#[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_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)?;
let patch = patch(&normalized_toml, &mut manifest_ctx)?;
{
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"),
gctx,
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.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)?,
patch(&normalized_toml, &mut manifest_ctx)?,
)
};
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)
}
#[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<'_, '_>,
) -> 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() {
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<'_, '_>,
) -> 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)| {
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 orig.version.is_none() && orig.path.is_none() && orig.git.is_none() {
anyhow::bail!(
"dependency ({name_in_toml}) specified without \
providing a local path, Git repository, version, or \
workspace dependency to use"
);
}
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
));
}
}
if orig.git.is_none() {
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
);
}
}
}
// 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)?;
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<'_, '_>,
) -> CargoResult<SourceId> {
match (
orig.git.as_ref(),
orig.path.as_ref(),
orig.registry.as_deref(),
orig.registry_index.as_ref(),
) {
(Some(_git), Some(_path), _, _) => {
bail!(
"dependency ({name_in_toml}) specification is ambiguous. \
Only one of `git` or `path` is allowed.",
);
}
(_, _, Some(_registry), Some(_registry_index)) => bail!(
"dependency ({name_in_toml}) specification is ambiguous. \
Only one of `registry` or `registry-index` is allowed.",
),
(Some(git), None, _, _) => {
let n_details = [&orig.branch, &orig.tag, &orig.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, 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 {
Ok(manifest_ctx.source_id)
}
}
(None, None, Some(registry), None) => SourceId::alt_registry(manifest_ctx.gctx, registry),
(None, None, None, Some(registry_index)) => {
let url = registry_index.into_url()?;
SourceId::for_registry(&url)
}
(None, None, None, None) => SourceId::crates_io(manifest_ctx.gctx),
}
}
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())?;
// HACK: The `base` string is user controlled, but building the path is safe from injection
// attacks since the `PathBaseName` type restricts the characters that can be used to exclude `.`
let base_key = format!("path-bases.{base}");
// Look up the relevant base in the Config and use that as the root.
if let Some(path_bases) = gctx.get::<Option<ConfigRelativePath>>(&base_key)? {
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
);
}
// `inherits` validation
if matches!(root.inherits.as_deref(), Some("debug")) {
bail!(
"profile.{}.inherits=\"debug\" should be profile.{}.inherits=\"dev\"",
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.trim_paths.is_some() {
match (
features.require(Feature::trim_paths()),
cli_unstable.trim_paths,
) {
(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>,
gctx: &GlobalContext,
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;
}
if tool == "cargo" && !gctx.cli_unstable().cargo_lints {
warn_for_cargo_lint_feature(gctx, warnings);
}
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 warn_for_cargo_lint_feature(gctx: &GlobalContext, warnings: &mut Vec<String>) {
use std::fmt::Write as _;
let key_name = "lints.cargo";
let feature_name = "cargo-lints";
let mut message = String::new();
let _ = write!(
message,
"unused manifest key `{key_name}` (may be supported in a future version)"
);
if gctx.nightly_features_allowed {
let _ = write!(
message,
"
consider passing `-Z{feature_name}` to enable this feature."
);
} else {
let _ = write!(
message,
"
this Cargo does not support nightly features, but if you
switch to nightly channel you can pass
`-Z{feature_name}` to enable this feature.",
);
}
warnings.push(message);
}
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;
};
features.values_mut().for_each(|feature_deps| {
feature_deps.retain(|feature_dep| {
let feature_value = FeatureValue::new(feature_dep.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(&registry)?.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
}
}