diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76548be..f48b404 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: strategy: matrix: os: [ubuntu-22.04] - features: ["--features targets", null] + features: ["--features targets", "--features metadata", null] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/Cargo.lock b/Cargo.lock index fef1822..ff92ecb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -567,6 +567,7 @@ dependencies = [ name = "krates" version = "0.16.10" dependencies = [ + "camino", "cargo-platform", "cargo_metadata", "cfg-expr", @@ -574,6 +575,7 @@ dependencies = [ "insta", "petgraph", "semver", + "serde", "serde_json", "similar-asserts", "tame-index", diff --git a/Cargo.toml b/Cargo.toml index f187c45..b4639ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,19 +21,24 @@ rust-version = "1.65.0" default = [] # Adds support for filtering target specific dependencies targets = ["cfg-expr/targets"] +# Enables interop with cargo_metadata, if not enabled uses types defined within krates +metadata = ["dep:cargo_metadata", "dep:cargo-platform"] [dependencies] +camino = { version = "1.1", features = ["serde1"] } # Used for acquiring and/or deserializing `cargo metadata` output -cargo_metadata = "0.18" +cargo_metadata = { version = "0.18", default-features = false, optional = true } # We need to use a type from this because it use part of the public API of cargo_metadata # ...but it's not actually in the public API :p -cargo-platform = "0.1" +cargo-platform = { version = "0.1", default-features = false, optional = true } # Used to parse and evaluate cfg() expressions for dependencies cfg-expr = "0.15" # Used to create and traverse graph structures petgraph = "0.6" # Used for checking version requirements semver = "1.0" +serde = "1.0" +serde_json = "1.0" [dev-dependencies] # Example CLI diff --git a/src/builder.rs b/src/builder.rs index 06d603d..a8aecd5 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2,8 +2,7 @@ pub(crate) mod features; pub mod index; -use crate::{DepKind, Edge, Error, Kid, Krates}; -use cargo_metadata as cm; +use crate::{cm, DepKind, Edge, Error, Kid, Krates, MdTarget}; use features::{Feature, ParsedFeature}; use std::{ collections::{BTreeMap, BTreeSet}, @@ -568,7 +567,7 @@ impl Builder { on_filter: F, ) -> Result, Error> where - N: From, + N: From, E: From, F: OnFilter, { @@ -600,11 +599,11 @@ impl Builder { /// ``` pub fn build_with_metadata( self, - md: cargo_metadata::Metadata, + md: crate::Metadata, mut on_filter: F, ) -> Result, Error> where - N: From, + N: From, E: From, F: OnFilter, { @@ -689,7 +688,7 @@ impl Builder { #[derive(Debug)] struct DepKindInfo { kind: DepKind, - cfg: Option<(String, cargo_platform::Platform)>, + cfg: Option, } #[derive(Debug)] @@ -756,7 +755,7 @@ impl Builder { .into_iter() .map(|dk| DepKindInfo { kind: dk.kind.into(), - cfg: dk.target.map(|t| (t.to_string(), t)), + cfg: dk.target.map(MdTarget::from), }) .collect(); @@ -1195,7 +1194,7 @@ impl Builder { return false; } - dk.cfg.as_ref().map(|(_, p)| p) == dep.target.as_ref() + crate::targets_eq(&dk.cfg, &dep.target) }).count() > 1; let (dep_index, dep) = krate @@ -1221,7 +1220,7 @@ impl Builder { return false; } - if dk.cfg.as_ref().map(|(_, p)| p) != dep.target.as_ref() { + if !crate::targets_eq(&dk.cfg, &dep.target) { return false; } @@ -1261,51 +1260,42 @@ impl Builder { return None; } - let cfg = if let Some(cfg) = dk.cfg.as_ref().map(|(c, _)| c.as_str()) { + let cfg = if let Some(cfg) = &dk.cfg { if !include_all_targets { - let matched = if cfg.starts_with("cfg(") { - match cfg_expr::Expression::parse(cfg) { - Ok(expr) => { - // We only need to focus on target predicates because they are - // the only type of predicate allowed by cargo at the moment - - // While it might be nicer to evaluate all the targets for each predicate - // it would lead to weird situations where an expression could evaluate to true - // (or false) with a combination of platform, that would otherwise be impossible, - // eg cfg(all(windows, target_env = "musl")) could evaluate to true - targets - .iter() - .any(|target| expr.eval(|pred| target.eval(pred))) - } - Err(_pe) => { - // TODO: maybe log a warning if we somehow fail to parse the cfg? - true - } - } + let matched = if let Some(expr) = &cfg.cfg { + // We only need to focus on target predicates because they are + // the only type of predicate allowed by cargo at the moment + + // While it might be nicer to evaluate all the targets for each predicate + // it would lead to weird situations where an expression could evaluate to true + // (or false) with a combination of platform, that would otherwise be impossible, + // eg cfg(all(windows, target_env = "musl")) could evaluate to true + targets + .iter() + .any(|target| expr.eval(|pred| target.eval(pred))) } else { // If it's not a cfg expression, it's just a fully specified target triple, // so we just do a string comparison - targets.iter().any(|target| target.matches_triple(cfg)) + targets.iter().any(|target| target.matches_triple(&cfg.inner)) }; if !matched { return None; } - } else if cfg.starts_with("cfg(") { + } else if let Some(expr) = &cfg.cfg { // This is _basically_ a tortured way to evaluate `cfg(any())`, which is always false but // is used by eg. serde -> serde_derive. If not filtering targets this would mean that // serde_derive and all of its dependencies would be pulled into the graph, even if the // only edge was the cfg(any()). - if let Ok(expr) = cfg_expr::Expression::parse(cfg) { - // We can't just do an eval and always return true, as that then would cause any - // not() expressions to evaluate to false - if expr.predicates().count() == 0 && !expr.eval(|_| true) { - return None; - } + + // We can't just do an eval and always return true, as that then would cause any + // not() expressions to evaluate to false + if expr.predicates().count() == 0 && !expr.eval(|_| true) { + return None; } } - Some(cfg) + Some(cfg.inner.as_str()) } else { None }; diff --git a/src/builder/index.rs b/src/builder/index.rs index 87d4f58..58a9377 100644 --- a/src/builder/index.rs +++ b/src/builder/index.rs @@ -40,10 +40,12 @@ impl CachingIndex { } } +/// Correct features with index information +/// /// Due to , we can't actually /// trust cargo to give us the correct package metadata, so we instead use the /// (presumably) correct data from the the index -pub(super) fn fix_features(index: &CachingIndex, krate: &mut cargo_metadata::Package) { +pub(super) fn fix_features(index: &CachingIndex, krate: &mut crate::Package) { if krate .source .as_ref() @@ -62,10 +64,9 @@ pub(super) fn fix_features(index: &CachingIndex, krate: &mut cargo_metadata::Pac } } - // The index entry features might not have the `dep:` - // used with weak features if the crate version was - // published with cargo <1.60.0 version, so we need to - // manually fix that up since we depend on that format + // The index entry features might not have the `dep:` used with weak + // features if the crate version was published with cargo <1.60.0 version, + // so we need to manually fix that up since we depend on that format let missing_deps: Vec<_> = krate .features .iter() diff --git a/src/cm.rs b/src/cm.rs new file mode 100644 index 0000000..1939497 --- /dev/null +++ b/src/cm.rs @@ -0,0 +1,706 @@ +//! Internal version of [`cargo_metadata`](https://github.com/oli-obk/cargo_metadata) + +pub use camino::Utf8PathBuf as PathBuf; +use semver::Version; +use serde::Deserialize; +use std::{collections::BTreeMap, fmt}; + +mod cmd; +mod dependency; +mod errors; + +pub use cmd::MetadataCommand; +pub use dependency::{Dependency, DependencyKind}; +pub use errors::Error; + +/// An "opaque" identifier for a package. +/// +/// It is possible to inspect the `repr` field, if the need arises, but its +/// precise format is an implementation detail and is subject to change. +/// +/// `Metadata` can be indexed by `PackageId`. +#[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[serde(transparent)] +pub struct PackageId { + /// The underlying string representation of id. + pub repr: String, +} + +impl fmt::Display for PackageId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.repr, f) + } +} + +#[derive(Clone, Deserialize, Debug)] +/// Starting point for metadata returned by `cargo metadata` +pub struct Metadata { + /// A list of all crates referenced by this crate (and the crate itself) + pub packages: Vec, + /// A list of all workspace members + pub workspace_members: Vec, + /// The list of default workspace members + /// + /// This not available if running with a version of Cargo older than 1.71. + #[serde(skip_serializing_if = "workspace_default_members_is_missing")] + pub workspace_default_members: WorkspaceDefaultMembers, + /// Dependencies graph + pub resolve: Option, + /// Workspace root + pub workspace_root: PathBuf, + /// Build directory + pub target_directory: PathBuf, + /// The workspace-level metadata object. Null if non-existent. + #[serde(rename = "metadata", default)] + pub workspace_metadata: serde_json::Value, + /// The metadata format version + pub version: usize, +} + +impl Metadata { + /// Get the workspace's root package of this metadata instance. + pub fn root_package(&self) -> Option<&Package> { + match &self.resolve { + Some(resolve) => { + // if dependencies are resolved, use Cargo's answer + let root = resolve.root.as_ref()?; + self.packages.iter().find(|pkg| &pkg.id == root) + } + None => { + // if dependencies aren't resolved, check for a root package manually + let root_manifest_path = self.workspace_root.join("Cargo.toml"); + self.packages + .iter() + .find(|pkg| pkg.manifest_path == root_manifest_path) + } + } + } + + /// Get the workspace packages. + pub fn workspace_packages(&self) -> Vec<&Package> { + self.packages + .iter() + .filter(|&p| self.workspace_members.contains(&p.id)) + .collect() + } + + /// Get the workspace default packages. + /// + /// # Panics + /// + /// This will panic if running with a version of Cargo older than 1.71. + pub fn workspace_default_packages(&self) -> Vec<&Package> { + self.packages + .iter() + .filter(|&p| self.workspace_default_members.contains(&p.id)) + .collect() + } +} + +impl<'a> std::ops::Index<&'a PackageId> for Metadata { + type Output = Package; + + fn index(&self, idx: &'a PackageId) -> &Self::Output { + self.packages + .iter() + .find(|p| p.id == *idx) + .unwrap_or_else(|| panic!("no package with this id: {:?}", idx)) + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(transparent)] +/// A list of default workspace members. +/// +/// See [`Metadata::workspace_default_members`]. +/// +/// It is only available if running a version of Cargo of 1.71 or newer. +/// +/// # Panics +/// +/// Dereferencing when running an older version of Cargo will panic. +pub struct WorkspaceDefaultMembers(Option>); + +impl core::ops::Deref for WorkspaceDefaultMembers { + type Target = [PackageId]; + + fn deref(&self) -> &Self::Target { + self.0 + .as_ref() + .expect("WorkspaceDefaultMembers should only be dereferenced on Cargo versions >= 1.71") + } +} + +/// Return true if a valid value for [`WorkspaceDefaultMembers`] is missing, and +/// dereferencing it would panic. +/// +/// Internal helper for `skip_serializing_if` and test code. Might be removed in +/// the future. +#[doc(hidden)] +pub fn workspace_default_members_is_missing( + workspace_default_members: &WorkspaceDefaultMembers, +) -> bool { + workspace_default_members.0.is_none() +} + +#[derive(Clone, Deserialize, Debug)] +/// A dependency graph +pub struct Resolve { + /// Nodes in a dependencies graph + pub nodes: Vec, + + /// The crate for which the metadata was read. + pub root: Option, +} + +impl<'a> std::ops::Index<&'a PackageId> for Resolve { + type Output = Node; + + fn index(&self, idx: &'a PackageId) -> &Self::Output { + self.nodes + .iter() + .find(|p| p.id == *idx) + .unwrap_or_else(|| panic!("no Node with this id: {:?}", idx)) + } +} + +#[derive(Clone, Deserialize, Debug)] +/// A node in a dependencies graph +pub struct Node { + /// An opaque identifier for a package + pub id: PackageId, + /// Dependencies in a structured format. + /// + /// `deps` handles renamed dependencies whereas `dependencies` does not. + #[serde(default)] + pub deps: Vec, + + /// List of opaque identifiers for this node's dependencies. + /// It doesn't support renamed dependencies. See `deps`. + pub dependencies: Vec, + + /// Features enabled on the crate + #[serde(default)] + pub features: Vec, +} + +#[derive(Clone, Deserialize, Debug)] +/// A dependency in a node +pub struct NodeDep { + /// The name of the dependency's library target. + /// If the crate was renamed, it is the new name. + pub name: String, + /// Package ID (opaque unique identifier) + pub pkg: PackageId, + /// The kinds of dependencies. + /// + /// This field was added in Rust 1.41. + #[serde(default)] + pub dep_kinds: Vec, +} + +#[derive(Clone, Deserialize, Debug)] +/// Information about a dependency kind. +pub struct DepKindInfo { + /// The kind of dependency. + #[serde(deserialize_with = "dependency::parse_dependency_kind")] + pub kind: DependencyKind, + /// The target platform for the dependency. + /// + /// This is `None` if it is not a target dependency. + pub target: Option, +} + +#[derive(Clone, Deserialize, Debug)] +/// One or more crates described by a single `Cargo.toml` +/// +/// Each [`target`][Package::targets] of a `Package` will be built as a crate. +/// For more information, see . +pub struct Package { + /// The [`name` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field) as given in the `Cargo.toml` + // (We say "given in" instead of "specified in" since the `name` key cannot be inherited from the workspace.) + pub name: String, + /// The [`version` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-version-field) as specified in the `Cargo.toml` + pub version: Version, + /// The [`authors` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-authors-field) as specified in the `Cargo.toml` + #[serde(default)] + pub authors: Vec, + /// An opaque identifier for a package + pub id: PackageId, + /// The source of the package, e.g. + /// crates.io or `None` for local projects. + pub source: Option, + /// The [`description` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-description-field) as specified in the `Cargo.toml` + pub description: Option, + /// List of dependencies of this particular package + pub dependencies: Vec, + /// The [`license` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields) as specified in the `Cargo.toml` + pub license: Option, + /// The [`license-file` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields) as specified in the `Cargo.toml`. + /// If the package is using a nonstandard license, this key may be specified instead of + /// `license`, and must point to a file relative to the manifest. + pub license_file: Option, + /// Targets provided by the crate (lib, bin, example, test, ...) + pub targets: Vec, + /// Features provided by the crate, mapped to the features required by that feature. + pub features: BTreeMap>, + /// Path containing the `Cargo.toml` + pub manifest_path: PathBuf, + /// The [`categories` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-categories-field) as specified in the `Cargo.toml` + #[serde(default)] + pub categories: Vec, + /// The [`keywords` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-keywords-field) as specified in the `Cargo.toml` + #[serde(default)] + pub keywords: Vec, + /// The [`readme` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-readme-field) as specified in the `Cargo.toml` + pub readme: Option, + /// The [`repository` URL](https://doc.rust-lang.org/cargo/reference/manifest.html#the-repository-field) as specified in the `Cargo.toml` + // can't use `url::Url` because that requires a more recent stable compiler + pub repository: Option, + /// The [`homepage` URL](https://doc.rust-lang.org/cargo/reference/manifest.html#the-homepage-field) as specified in the `Cargo.toml`. + /// + /// On versions of cargo before 1.49, this will always be [`None`]. + pub homepage: Option, + /// The [`documentation` URL](https://doc.rust-lang.org/cargo/reference/manifest.html#the-documentation-field) as specified in the `Cargo.toml`. + /// + /// On versions of cargo before 1.49, this will always be [`None`]. + pub documentation: Option, + /// The default Rust edition for the package (either what's specified in the [`edition` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-edition-field) + /// or defaulting to [`Edition::E2015`]). + /// + /// Beware that individual targets may specify their own edition in + /// [`Target::edition`]. + #[serde(default)] + pub edition: Edition, + /// Contents of the free form [`package.metadata` section](https://doc.rust-lang.org/cargo/reference/manifest.html#the-metadata-table). + /// + /// This contents can be serialized to a struct using serde: + /// + /// ```rust + /// use serde::Deserialize; + /// use serde_json::json; + /// + /// #[derive(Debug, Deserialize)] + /// struct SomePackageMetadata { + /// some_value: i32, + /// } + /// + /// let value = json!({ + /// "some_value": 42, + /// }); + /// + /// let package_metadata: SomePackageMetadata = serde_json::from_value(value).unwrap(); + /// assert_eq!(package_metadata.some_value, 42); + /// + /// ``` + #[serde(default, skip_serializing_if = "is_null")] + pub metadata: serde_json::Value, + /// The name of a native library the package is linking to. + pub links: Option, + /// List of registries to which this package may be published (derived from the [`publish` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field)). + /// + /// Publishing is unrestricted if `None`, and forbidden if the `Vec` is empty. + /// + /// This is always `None` if running with a version of Cargo older than 1.39. + pub publish: Option>, + /// The [`default-run` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-default-run-field) as given in the `Cargo.toml` + // (We say "given in" instead of "specified in" since the `default-run` key cannot be inherited from the workspace.) + /// The default binary to run by `cargo run`. + /// + /// This is always `None` if running with a version of Cargo older than 1.55. + pub default_run: Option, + /// The [`rust-version` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) as specified in the `Cargo.toml`. + /// The minimum supported Rust version of this package. + /// + /// This is always `None` if running with a version of Cargo older than 1.58. + #[serde(default)] + #[serde(deserialize_with = "deserialize_rust_version")] + pub rust_version: Option, +} + +impl Package { + /// Full path to the license file if one is present in the manifest + pub fn license_file(&self) -> Option { + self.license_file.as_ref().map(|file| { + self.manifest_path + .parent() + .unwrap_or(&self.manifest_path) + .join(file) + }) + } + + /// Full path to the readme file if one is present in the manifest + pub fn readme(&self) -> Option { + self.readme.as_ref().map(|file| { + self.manifest_path + .parent() + .unwrap_or(&self.manifest_path) + .join(file) + }) + } +} + +/// The source of a package such as crates.io. +/// +/// It is possible to inspect the `repr` field, if the need arises, but its +/// precise format is an implementation detail and is subject to change. +#[derive(Clone, Deserialize, Debug, PartialEq, Eq)] +#[serde(transparent)] +pub struct Source { + /// The underlying string representation of a source. + pub repr: String, +} + +impl Source { + /// Returns true if the source is crates.io. + pub fn is_crates_io(&self) -> bool { + self.repr == "registry+https://github.com/rust-lang/crates.io-index" + } +} + +impl fmt::Display for Source { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.repr, f) + } +} + +#[derive(Clone, Deserialize, Debug)] +/// A single target (lib, bin, example, ...) provided by a crate +pub struct Target { + /// Name as given in the `Cargo.toml` or generated from the file name + pub name: String, + /// Kind of target. + /// + /// The possible values are `example`, `test`, `bench`, `custom-build` and + /// [Cargo crate types](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field): + /// `bin`, `lib`, `rlib`, `dylib`, `cdylib`, `staticlib`, `proc-macro`. + /// + /// Other possible values may be added in the future. + pub kind: Vec, + /// Similar to `kind`, but only reports the + /// [Cargo crate types](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field): + /// `bin`, `lib`, `rlib`, `dylib`, `cdylib`, `staticlib`, `proc-macro`. + /// Everything that's not a proc macro or a library of some kind is reported as "bin". + /// + /// Other possible values may be added in the future. + #[serde(default)] + #[cfg_attr(feature = "builder", builder(default))] + pub crate_types: Vec, + + #[serde(default)] + #[cfg_attr(feature = "builder", builder(default))] + #[serde(rename = "required-features")] + /// This target is built only if these features are enabled. + /// It doesn't apply to `lib` targets. + pub required_features: Vec, + /// Path to the main source file of the target + pub src_path: PathBuf, + /// Rust edition for this target + #[serde(default)] + #[cfg_attr(feature = "builder", builder(default))] + pub edition: Edition, + /// Whether or not this target has doc tests enabled, and the target is + /// compatible with doc testing. + /// + /// This is always `true` if running with a version of Cargo older than 1.37. + #[serde(default = "default_true")] + #[cfg_attr(feature = "builder", builder(default = "true"))] + pub doctest: bool, + /// Whether or not this target is tested by default by `cargo test`. + /// + /// This is always `true` if running with a version of Cargo older than 1.47. + #[serde(default = "default_true")] + #[cfg_attr(feature = "builder", builder(default = "true"))] + pub test: bool, + /// Whether or not this target is documented by `cargo doc`. + /// + /// This is always `true` if running with a version of Cargo older than 1.50. + #[serde(default = "default_true")] + #[cfg_attr(feature = "builder", builder(default = "true"))] + pub doc: bool, +} + +impl Target { + fn is_kind(&self, name: TargetKind) -> bool { + self.kind.iter().any(|kind| kind == &name) + } + + /// Return true if this target is of kind "lib". + pub fn is_lib(&self) -> bool { + self.is_kind(TargetKind::Lib) + } + + /// Return true if this target is of kind "bin". + pub fn is_bin(&self) -> bool { + self.is_kind(TargetKind::Bin) + } + + /// Return true if this target is of kind "example". + pub fn is_example(&self) -> bool { + self.is_kind(TargetKind::Example) + } + + /// Return true if this target is of kind "test". + pub fn is_test(&self) -> bool { + self.is_kind(TargetKind::Test) + } + + /// Return true if this target is of kind "bench". + pub fn is_bench(&self) -> bool { + self.is_kind(TargetKind::Bench) + } + + /// Return true if this target is of kind "custom-build". + pub fn is_custom_build(&self) -> bool { + self.is_kind(TargetKind::CustomBuild) + } + + /// Return true if this target is of kind "proc-macro". + pub fn is_proc_macro(&self) -> bool { + self.is_kind(TargetKind::ProcMacro) + } +} + +/// Kind of target. +/// +/// The possible values are `example`, `test`, `bench`, `custom-build` and +/// [Cargo crate types](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field): +/// `bin`, `lib`, `rlib`, `dylib`, `cdylib`, `staticlib`, `proc-macro`. +/// +/// Other possible values may be added in the future. +#[derive(Clone, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum TargetKind { + /// `cargo bench` target + #[serde(rename = "bench")] + Bench, + /// Binary executable target + #[serde(rename = "bin")] + Bin, + /// Custom build target + #[serde(rename = "custom-build")] + CustomBuild, + /// Dynamic system library target + #[serde(rename = "cdylib")] + CDyLib, + /// Dynamic Rust library target + #[serde(rename = "dylib")] + DyLib, + /// Example target + #[serde(rename = "example")] + Example, + /// Rust library + #[serde(rename = "lib")] + Lib, + /// Procedural Macro + #[serde(rename = "proc-macro")] + ProcMacro, + /// Rust library for use as an intermediate artifact + #[serde(rename = "rlib")] + RLib, + /// Static system library + #[serde(rename = "staticlib")] + StaticLib, + /// Test target + #[serde(rename = "test")] + Test, +} + +impl From<&str> for TargetKind { + fn from(value: &str) -> Self { + match value { + "example" => TargetKind::Example, + "test" => TargetKind::Test, + "bench" => TargetKind::Bench, + "custom-build" => TargetKind::CustomBuild, + "bin" => TargetKind::Bin, + "lib" => TargetKind::Lib, + "rlib" => TargetKind::RLib, + "dylib" => TargetKind::DyLib, + "cdylib" => TargetKind::CDyLib, + "staticlib" => TargetKind::StaticLib, + "proc-macro" => TargetKind::ProcMacro, + x => panic!("unknown target kind {x}"), + } + } +} + +/// Similar to `kind`, but only reports the +/// [Cargo crate types](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field): +/// `bin`, `lib`, `rlib`, `dylib`, `cdylib`, `staticlib`, `proc-macro`. +/// Everything that's not a proc macro or a library of some kind is reported as "bin". +/// +/// Other possible values may be added in the future. +#[derive(Clone, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum CrateType { + /// Binary executable target + #[serde(rename = "bin")] + Bin, + /// Dynamic system library target + #[serde(rename = "cdylib")] + CDyLib, + /// Dynamic Rust library target + #[serde(rename = "dylib")] + DyLib, + /// Rust library + #[serde(rename = "lib")] + Lib, + /// Procedural Macro + #[serde(rename = "proc-macro")] + ProcMacro, + /// Rust library for use as an intermediate artifact + #[serde(rename = "rlib")] + RLib, + /// Static system library + #[serde(rename = "staticlib")] + StaticLib, +} + +impl From<&str> for CrateType { + fn from(value: &str) -> Self { + match value { + "bin" => CrateType::Bin, + "lib" => CrateType::Lib, + "rlib" => CrateType::RLib, + "dylib" => CrateType::DyLib, + "cdylib" => CrateType::CDyLib, + "staticlib" => CrateType::StaticLib, + "proc-macro" => CrateType::ProcMacro, + x => panic!("unknown crate type {x}"), + } + } +} + +/// The Rust edition +/// +/// As of writing this comment rust editions 2024, 2027 and 2030 are not actually a thing yet but are parsed nonetheless for future proofing. +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum Edition { + /// Edition 2015 + #[serde(rename = "2015")] + E2015, + /// Edition 2018 + #[serde(rename = "2018")] + E2018, + /// Edition 2021 + #[serde(rename = "2021")] + E2021, + #[doc(hidden)] + #[serde(rename = "2024")] + _E2024, + #[doc(hidden)] + #[serde(rename = "2027")] + _E2027, + #[doc(hidden)] + #[serde(rename = "2030")] + _E2030, +} + +impl Edition { + /// Return the string representation of the edition + pub fn as_str(&self) -> &'static str { + use Edition::*; + match self { + E2015 => "2015", + E2018 => "2018", + E2021 => "2021", + _E2024 => "2024", + _E2027 => "2027", + _E2030 => "2030", + } + } +} + +impl fmt::Display for Edition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl Default for Edition { + fn default() -> Self { + Self::E2015 + } +} + +fn default_true() -> bool { + true +} + +/// As per the Cargo Book the [`rust-version` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) must: +/// +/// > be a bare version number with two or three components; +/// > it cannot include semver operators or pre-release identifiers. +/// +/// [`semver::Version`] however requires three components. This function takes +/// care of appending `.0` if the provided version number only has two components +/// and ensuring that it does not contain a pre-release version or build metadata. +fn deserialize_rust_version<'de, D>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: serde::de::Deserializer<'de>, +{ + let mut buf = match Option::::deserialize(deserializer)? { + None => return Ok(None), + Some(buf) => buf, + }; + + for char in buf.chars() { + if char == '-' { + return Err(serde::de::Error::custom( + "pre-release identifiers are not supported in rust-version", + )); + } else if char == '+' { + return Err(serde::de::Error::custom( + "build metadata is not supported in rust-version", + )); + } + } + + if buf.matches('.').count() == 1 { + // e.g. 1.0 -> 1.0.0 + buf.push_str(".0"); + } + + Ok(Some( + Version::parse(&buf).map_err(serde::de::Error::custom)?, + )) +} + +#[cfg(test)] +mod test { + use semver::Version; + + #[derive(Debug, serde::Deserialize)] + struct BareVersion( + #[serde(deserialize_with = "super::deserialize_rust_version")] Option, + ); + + fn bare_version(str: &str) -> Version { + serde_json::from_str::(&format!(r#""{}""#, str)) + .unwrap() + .0 + .unwrap() + } + + fn bare_version_err(str: &str) -> String { + serde_json::from_str::(&format!(r#""{}""#, str)) + .unwrap_err() + .to_string() + } + + #[test] + fn test_deserialize_rust_version() { + assert_eq!(bare_version("1.2"), Version::new(1, 2, 0)); + assert_eq!(bare_version("1.2.0"), Version::new(1, 2, 0)); + assert_eq!( + bare_version_err("1.2.0-alpha"), + "pre-release identifiers are not supported in rust-version" + ); + assert_eq!( + bare_version_err("1.2.0+123"), + "build metadata is not supported in rust-version" + ); + } +} diff --git a/src/cm/cmd.rs b/src/cm/cmd.rs new file mode 100644 index 0000000..bfc87e6 --- /dev/null +++ b/src/cm/cmd.rs @@ -0,0 +1,235 @@ +use super::Error; +use std::path::PathBuf; +use std::{ffi::OsString, process::Command}; + +/// Cargo features flags +#[derive(Debug, Clone)] +pub enum CargoOpt { + /// Run cargo with `--features-all` + AllFeatures, + /// Run cargo with `--no-default-features` + NoDefaultFeatures, + /// Run cargo with `--features ` + SomeFeatures(Vec), +} + +/// A builder for configurating `cargo metadata` invocation. +#[derive(Debug, Clone, Default)] +pub struct MetadataCommand { + /// Path to `cargo` executable. If not set, this will use the + /// the `$CARGO` environment variable, and if that is not set, will + /// simply be `cargo`. + cargo_path: Option, + /// Path to `Cargo.toml` + manifest_path: Option, + /// Current directory of the `cargo metadata` process. + current_dir: Option, + /// Output information only about workspace members and don't fetch dependencies. + no_deps: bool, + /// Collections of `CargoOpt::SomeFeatures(..)` + features: Vec, + /// Latched `CargoOpt::AllFeatures` + all_features: bool, + /// Latched `CargoOpt::NoDefaultFeatures` + no_default_features: bool, + /// Arbitrary command line flags to pass to `cargo`. These will be added + /// to the end of the command line invocation. + other_options: Vec, + /// Arbitrary environment variables to set when running `cargo`. These will be merged into + /// the calling environment, overriding any which clash. + env: std::collections::BTreeMap, + /// Show stderr + verbose: bool, +} + +impl MetadataCommand { + /// Creates a default `cargo metadata` command, which will look for + /// `Cargo.toml` in the ancestors of the current directory. + pub fn new() -> Self { + Self::default() + } + /// Path to `cargo` executable. If not set, this will use the + /// the `$CARGO` environment variable, and if that is not set, will + /// simply be `cargo`. + pub fn cargo_path(&mut self, path: impl Into) -> &mut Self { + self.cargo_path = Some(path.into()); + self + } + /// Path to `Cargo.toml` + pub fn manifest_path(&mut self, path: impl Into) -> &mut Self { + self.manifest_path = Some(path.into()); + self + } + /// Current directory of the `cargo metadata` process. + pub fn current_dir(&mut self, path: impl Into) -> &mut Self { + self.current_dir = Some(path.into()); + self + } + /// Output information only about workspace members and don't fetch dependencies. + pub fn no_deps(&mut self) -> &mut Self { + self.no_deps = true; + self + } + /// Which features to include. + /// + /// Call this multiple times to specify advanced feature configurations: + /// + /// ```no_run + /// # use cargo_metadata::{CargoOpt, MetadataCommand}; + /// MetadataCommand::new() + /// .features(CargoOpt::NoDefaultFeatures) + /// .features(CargoOpt::SomeFeatures(vec!["feat1".into(), "feat2".into()])) + /// .features(CargoOpt::SomeFeatures(vec!["feat3".into()])) + /// // ... + /// # ; + /// ``` + /// + /// # Panics + /// + /// `cargo metadata` rejects multiple `--no-default-features` flags. Similarly, the `features()` + /// method panics when specifying multiple `CargoOpt::NoDefaultFeatures`: + /// + /// ```should_panic + /// # use cargo_metadata::{CargoOpt, MetadataCommand}; + /// MetadataCommand::new() + /// .features(CargoOpt::NoDefaultFeatures) + /// .features(CargoOpt::NoDefaultFeatures) // <-- panic! + /// // ... + /// # ; + /// ``` + /// + /// The method also panics for multiple `CargoOpt::AllFeatures` arguments: + /// + /// ```should_panic + /// # use cargo_metadata::{CargoOpt, MetadataCommand}; + /// MetadataCommand::new() + /// .features(CargoOpt::AllFeatures) + /// .features(CargoOpt::AllFeatures) // <-- panic! + /// // ... + /// # ; + /// ``` + pub fn features(&mut self, features: CargoOpt) -> &mut Self { + match features { + CargoOpt::SomeFeatures(features) => self.features.extend(features), + CargoOpt::NoDefaultFeatures => { + assert!( + !self.no_default_features, + "Do not supply CargoOpt::NoDefaultFeatures more than once!" + ); + self.no_default_features = true; + } + CargoOpt::AllFeatures => { + assert!( + !self.all_features, + "Do not supply CargoOpt::AllFeatures more than once!" + ); + self.all_features = true; + } + } + self + } + /// Arbitrary command line flags to pass to `cargo`. These will be added + /// to the end of the command line invocation. + pub fn other_options(&mut self, options: impl Into>) -> &mut Self { + self.other_options = options.into(); + self + } + + /// Arbitrary environment variables to set when running `cargo`. These will be merged into + /// the calling environment, overriding any which clash. + /// + /// Some examples of when you may want to use this: + /// 1. Setting cargo config values without needing a .cargo/config.toml file, e.g. to set + /// `CARGO_NET_GIT_FETCH_WITH_CLI=true` + /// 2. To specify a custom path to RUSTC if your rust toolchain components aren't laid out in + /// the way cargo expects by default. + /// + /// ```no_run + /// # use cargo_metadata::{CargoOpt, MetadataCommand}; + /// MetadataCommand::new() + /// .env("CARGO_NET_GIT_FETCH_WITH_CLI", "true") + /// .env("RUSTC", "/path/to/rustc") + /// // ... + /// # ; + /// ``` + pub fn env, V: Into>( + &mut self, + key: K, + val: V, + ) -> &mut MetadataCommand { + self.env.insert(key.into(), val.into()); + self + } + + /// Set whether to show stderr + pub fn verbose(&mut self, verbose: bool) -> &mut MetadataCommand { + self.verbose = verbose; + self + } + + /// Builds a command for `cargo metadata`. This is the first + /// part of the work of `exec`. + pub fn cargo_command(&self) -> Command { + let cargo = self + .cargo_path + .clone() + .or_else(|| std::env::var("CARGO").map(PathBuf::from).ok()) + .unwrap_or_else(|| PathBuf::from("cargo")); + let mut cmd = Command::new(cargo); + cmd.args(["metadata", "--format-version", "1"]); + + if self.no_deps { + cmd.arg("--no-deps"); + } + + if let Some(path) = self.current_dir.as_ref() { + cmd.current_dir(path); + } + + if !self.features.is_empty() { + cmd.arg("--features").arg(self.features.join(",")); + } + if self.all_features { + cmd.arg("--all-features"); + } + if self.no_default_features { + cmd.arg("--no-default-features"); + } + + if let Some(manifest_path) = &self.manifest_path { + cmd.arg("--manifest-path").arg(manifest_path.as_os_str()); + } + cmd.args(&self.other_options); + + cmd.envs(&self.env); + + cmd + } + + /// Parses `cargo metadata` output. `data` must have been + /// produced by a command built with `cargo_command`. + pub fn parse>(data: T) -> Result { + let meta = serde_json::from_str(data.as_ref())?; + Ok(meta) + } + + /// Runs configured `cargo metadata` and returns parsed `Metadata`. + pub fn exec(&self) -> Result { + let mut command = self.cargo_command(); + if self.verbose { + command.stderr(std::process::Stdio::inherit()); + } + let output = command.output()?; + if !output.status.success() { + return Err(Error::CargoMetadata { + stderr: String::from_utf8(output.stderr)?, + }); + } + + let stdout = std::str::from_utf8(&output.stdout)? + .lines() + .find(|line| line.starts_with('{')) + .ok_or(Error::NoJson)?; + Self::parse(stdout) + } +} diff --git a/src/cm/dependency.rs b/src/cm/dependency.rs new file mode 100644 index 0000000..e5ba8e1 --- /dev/null +++ b/src/cm/dependency.rs @@ -0,0 +1,72 @@ +//! This module contains `Dependency` and the types/functions it uses for deserialization. + +use std::fmt; + +use semver::VersionReq; +use serde::{Deserialize, Deserializer}; + +#[derive(Eq, PartialEq, Clone, Debug, Copy, Hash, Deserialize, Default)] +/// Dependencies can come in three kinds +pub enum DependencyKind { + #[serde(rename = "normal")] + #[default] + /// The 'normal' kind + Normal, + #[serde(rename = "dev")] + /// Those used in tests only + Development, + #[serde(rename = "build")] + /// Those used in build scripts only + Build, +} + +impl fmt::Display for DependencyKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Normal => f.write_str("normal"), + Self::Development => f.write_str("dev"), + Self::Build => f.write_str("build"), + } + } +} + +/// The `kind` can be `null`, which is interpreted as the default - `Normal`. +pub(super) fn parse_dependency_kind<'de, D>(d: D) -> Result +where + D: Deserializer<'de>, +{ + Deserialize::deserialize(d).map(|x: Option<_>| x.unwrap_or_default()) +} + +#[derive(Clone, Deserialize, Debug)] +/// A dependency of the main crate +pub struct Dependency { + /// Name as given in the `Cargo.toml` + pub name: String, + /// The source of dependency + pub source: Option, + /// The required version + pub req: VersionReq, + /// The kind of dependency this is + #[serde(deserialize_with = "parse_dependency_kind")] + pub kind: DependencyKind, + /// Whether this dependency is required or optional + pub optional: bool, + /// Whether the default features in this dependency are used. + pub uses_default_features: bool, + /// The list of features enabled for this dependency. + pub features: Vec, + /// The target this dependency is specific to. + pub target: Option, + /// If the dependency is renamed, this is the new name for the dependency + /// as a string. None if it is not renamed. + pub rename: Option, + /// The URL of the index of the registry where this dependency is from. + /// + /// If None, the dependency is from crates.io. + pub registry: Option, + /// The file system path for a local path dependency. + /// + /// Only produced on cargo 1.51+ + pub path: Option, +} diff --git a/src/cm/errors.rs b/src/cm/errors.rs new file mode 100644 index 0000000..0958617 --- /dev/null +++ b/src/cm/errors.rs @@ -0,0 +1,104 @@ +use std::{fmt, io, str::Utf8Error, string::FromUtf8Error}; + +/// Error returned when executing/parsing `cargo metadata` fails. +/// +/// # Note about Backtraces +/// +/// This error type does not contain backtraces, but each error variant +/// comes from _one_ specific place, so it's not really needed for the +/// inside of this crate. If you need a backtrace down to, but not inside +/// of, a failed call of `cargo_metadata` you can do one of multiple thinks: +/// +/// 1. Convert it to a `failure::Error` (possible using the `?` operator), +/// which is similar to a `Box<::std::error::Error + 'static + Send + Sync>`. +/// 2. Have appropriate variants in your own error type. E.g. you could wrap +/// a `failure::Context` or add a `failure::Backtrace` field (which +/// is empty if `RUST_BACKTRACE` is not set, so it's simple to use). +/// 3. You still can place a failure based error into a `error_chain` if you +/// really want to. (Either through foreign_links or by making it a field +/// value of a `ErrorKind` variant). +/// +#[derive(Debug)] +pub enum Error { + /// Error during execution of `cargo metadata` + CargoMetadata { + /// stderr returned by the `cargo metadata` command + stderr: String, + }, + + /// IO Error during execution of `cargo metadata` + Io(io::Error), + + /// Output of `cargo metadata` was not valid utf8 + Utf8(Utf8Error), + + /// Error output of `cargo metadata` was not valid utf8 + ErrUtf8(FromUtf8Error), + + /// Deserialization error (structure of json did not match expected structure) + Json(serde_json::Error), + + /// The output did not contain any json + NoJson, +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Io(io) => Some(io), + Self::Utf8(err) => Some(err), + Self::ErrUtf8(err) => Some(err), + Self::Json(err) => Some(err), + _ => None, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CargoMetadata { stderr } => { + write!(f, "`cargo metadata` exited with an error: {stderr}") + } + Self::Io(io) => { + write!(f, "failed to start `cargo metadata`: {io}") + } + Self::Utf8(err) => { + write!(f, "cannot convert the stdout of `cargo metadata`: {err}") + } + Self::ErrUtf8(err) => { + write!(f, "cannot convert the stderr of `cargo metadata`: {err}") + } + Self::Json(err) => { + write!(f, "failed to interpret `cargo metadata`'s json: {err}") + } + Self::NoJson => { + f.write_str("could not find any json in the output of `cargo metadata`") + } + } + } +} + +impl From for Error { + fn from(value: io::Error) -> Self { + Self::Io(value) + } +} + +impl From for Error { + fn from(value: Utf8Error) -> Self { + Self::Utf8(value) + } +} + +impl From for Error { + fn from(value: FromUtf8Error) -> Self { + Self::ErrUtf8(value) + } +} + +impl From for Error { + fn from(value: serde_json::Error) -> Self { + Self::Json(value) + } +} diff --git a/src/errors.rs b/src/errors.rs index 9a85871..6e94f39 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,6 @@ +#[cfg(not(feature = "metadata"))] +use crate::cm::Error as CMErr; +#[cfg(feature = "metadata")] use cargo_metadata::Error as CMErr; use std::fmt; diff --git a/src/lib.rs b/src/lib.rs index a168a9f..5d12d40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,17 +25,23 @@ //! } //! ``` +#[cfg(feature = "metadata")] pub use cargo_metadata as cm; +#[cfg(not(feature = "metadata"))] +pub mod cm; + pub use cfg_expr; #[cfg(feature = "targets")] pub use cfg_expr::target_lexicon; use cm::DependencyKind as DK; +pub use cm::{Metadata, Package, PackageId}; + pub use petgraph; pub use semver; -pub use cm::camino::{self, Utf8Path, Utf8PathBuf}; +pub use camino::{self, Utf8Path, Utf8PathBuf}; use petgraph::{graph::EdgeIndex, graph::NodeIndex, visit::EdgeRef, Direction}; mod builder; @@ -83,8 +89,8 @@ impl Kid { } #[allow(clippy::fallible_impl_from)] -impl From for Kid { - fn from(pid: cargo_metadata::PackageId) -> Self { +impl From for Kid { + fn from(pid: PackageId) -> Self { let mut repr = pid.repr; let mut gen = || { @@ -296,6 +302,7 @@ impl From for DepKind { DK::Normal => Self::Normal, DK::Build => Self::Build, DK::Development => Self::Dev, + #[cfg(feature = "metadata")] DK::Unknown => unreachable!(), } } @@ -570,7 +577,7 @@ impl Krates { /// Note that `dep_index` is the index of [`cargo_metadata::Package::dependencies`] #[inline] pub fn resolved_dependency(&self, nid: NodeId, dep_index: usize) -> Option<&N> { - if nid.index() < self.krates_end { + if nid.index() >= self.krates_end { return None; } @@ -866,6 +873,59 @@ impl std::ops::Index for Krates { } } +#[derive(Debug)] +struct MdTarget { + inner: String, + cfg: Option, + #[cfg(feature = "metadata")] + platform: cargo_platform::Platform, +} + +#[cfg(feature = "metadata")] +impl From for MdTarget { + fn from(platform: cargo_platform::Platform) -> Self { + let inner = platform.to_string(); + let cfg = inner + .starts_with("cfg(") + .then(|| cfg_expr::Expression::parse(&inner).ok()) + .flatten(); + Self { + inner, + cfg, + platform, + } + } +} + +#[cfg(not(feature = "metadata"))] +impl From for MdTarget { + fn from(inner: String) -> Self { + let cfg = inner + .starts_with("cfg(") + .then(|| cfg_expr::Expression::parse(&inner).ok()) + .flatten(); + Self { inner, cfg } + } +} + +#[cfg(feature = "metadata")] +fn targets_eq(target: &Option, other: &Option) -> bool { + match (target, other) { + (None, None) => true, + (Some(a), Some(b)) => a.platform.eq(b), + _ => false, + } +} + +#[cfg(not(feature = "metadata"))] +fn targets_eq(target: &Option, other: &Option) -> bool { + match (target, other) { + (None, None) => true, + (Some(a), Some(b)) => a.inner.eq(b), + _ => false, + } +} + #[cfg(test)] mod tests { #[test] @@ -894,7 +954,7 @@ mod tests { ]; for (repr, name, version, source) in ids { - let kid = super::Kid::from(cargo_metadata::PackageId { + let kid = super::Kid::from(super::PackageId { repr: repr.to_owned(), });