| pub(crate) mod features; |
| |
| pub mod index; |
| |
| use crate::{DepKind, Edge, Error, Kid, Krates}; |
| use cargo_metadata as cm; |
| use features::{Feature, ParsedFeature}; |
| use std::{ |
| collections::{BTreeMap, BTreeSet}, |
| path::{Path, PathBuf}, |
| }; |
| |
| /// An alternative to [`cargo_metadata::MetadataCommand`] which allows correct |
| /// feature usage, as well as ensuring that the command can run successfully |
| /// regardless of where it is executed and on what. |
| #[derive(Default, Debug)] |
| pub struct Cmd { |
| cargo_path: Option<PathBuf>, |
| manifest_path: Option<PathBuf>, |
| current_dir: Option<PathBuf>, |
| features: Vec<String>, |
| other_options: Vec<String>, |
| all_features: bool, |
| no_default_features: bool, |
| frozen: bool, |
| locked: bool, |
| offline: bool, |
| } |
| |
| #[derive(Copy, Clone)] |
| pub struct LockOptions { |
| /// Requires that the Cargo.lock file is up-to-date. If the lock file is |
| /// missing, or it needs to be updated, Cargo will exit with an error. |
| /// Prevents Cargo from attempting to access the network to determine if it |
| /// is out-of-date. |
| pub frozen: bool, |
| /// Requires that the Cargo.lock file is up-to-date. If the lock file is |
| /// missing, or it needs to be updated, Cargo will exit with an error. |
| pub locked: bool, |
| /// Prevents Cargo from accessing the network for any reason. Without this |
| /// flag, Cargo will stop with an error if it needs to access the network |
| /// and the network is not available. With this flag, Cargo will attempt to |
| /// proceed without the network if possible. |
| /// |
| /// Beware that this may result in different dependency resolution than |
| /// online mode. Cargo will restrict itself to crates that are downloaded |
| /// locally, even if there might be a newer version as indicated in the |
| /// local copy of the index. See the [cargo fetch](https://doc.rust-lang.org/cargo/commands/cargo-fetch.html) |
| /// command to download dependencies before going offline. |
| pub offline: bool, |
| } |
| |
| impl Cmd { |
| 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<PathBuf>) -> &mut Self { |
| self.cargo_path = Some(path.into()); |
| self |
| } |
| |
| /// Path to a `Cargo.toml` manifest |
| pub fn manifest_path(&mut self, path: impl Into<PathBuf>) -> &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<PathBuf>) -> &mut Self { |
| self.current_dir = Some(path.into()); |
| self |
| } |
| |
| /// Disables default features. |
| /// |
| /// **NOTE**: This has **no effect** if used on a workspace. You must |
| /// specify a working directory or manifest path to a specific crate if used |
| /// on a crate inside a workspace. |
| pub fn no_default_features(&mut self) -> &mut Self { |
| self.no_default_features = true; |
| self |
| } |
| |
| /// Enables all features for all workspace crates. Usable on both individual |
| /// crates and on an entire workspace. |
| pub fn all_features(&mut self) -> &mut Self { |
| self.all_features = true; |
| self |
| } |
| |
| /// Enables specific features. See the **NOTE** for `no_default_features` |
| pub fn features(&mut self, feats: impl IntoIterator<Item = String>) -> &mut Self { |
| self.features.extend(feats); |
| self |
| } |
| |
| /// Sets the various [lock options](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html#manifest-options) |
| /// for determining if cargo can access the network and if the lockfile must |
| /// be present and can be modified |
| pub fn lock_opts(&mut self, lopts: LockOptions) -> &mut Self { |
| self.frozen = lopts.frozen; |
| self.locked = lopts.locked; |
| self.offline = lopts.offline; |
| 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 IntoIterator<Item = String>) -> &mut Self { |
| self.other_options.extend(options); |
| self |
| } |
| } |
| |
| #[allow(clippy::fallible_impl_from)] |
| impl From<Cmd> for cm::MetadataCommand { |
| fn from(mut cmd: Cmd) -> cm::MetadataCommand { |
| let mut mdc = cm::MetadataCommand::new(); |
| |
| if let Some(cp) = cmd.cargo_path { |
| mdc.cargo_path(cp); |
| } |
| |
| // If the manifest path is set, we force set the current working |
| // directory to its parent and use the relative path, this is to fix an |
| // edge case where you can run cargo metadata from a directory outside |
| // of a workspace which could fail if eg. there is a reference to a |
| // registry that is defined in the workspace's .cargo/config |
| if let Some(mp) = cmd.manifest_path { |
| cmd.current_dir = Some(mp.parent().unwrap().to_owned()); |
| mdc.manifest_path("Cargo.toml"); |
| } |
| |
| if let Some(cd) = cmd.current_dir { |
| if !cd.as_os_str().is_empty() { |
| mdc.current_dir(cd); |
| } else { |
| mdc.current_dir("."); |
| } |
| } |
| |
| // Everything else we specify as additional options, as MetadataCommand |
| // does not handle features correctly, eg. you cannot disable default |
| // and set specific ones at the same time |
| // https://github.com/oli-obk/cargo_metadata/issues/79 |
| cmd.features.sort(); |
| cmd.features.dedup(); |
| |
| let mut opts = Vec::with_capacity( |
| cmd.features.len() |
| + cmd.other_options.len() |
| + usize::from(cmd.no_default_features) |
| + usize::from(cmd.all_features), |
| ); |
| |
| if cmd.no_default_features { |
| opts.push("--no-default-features".to_owned()); |
| } |
| |
| if cmd.all_features { |
| opts.push("--all-features".to_owned()); |
| } |
| |
| if !cmd.features.is_empty() { |
| opts.push("--features".to_owned()); |
| opts.push(cmd.features.join(" ")); |
| } |
| |
| if cmd.frozen { |
| opts.push("--frozen".to_owned()); |
| } |
| |
| if cmd.locked { |
| opts.push("--locked".to_owned()); |
| } |
| |
| if cmd.offline { |
| opts.push("--offline".to_owned()); |
| } |
| |
| opts.append(&mut cmd.other_options); |
| mdc.other_options(opts); |
| |
| mdc |
| } |
| } |
| |
| #[derive(Clone)] |
| pub enum Target { |
| Builtin(&'static cfg_expr::targets::TargetInfo), |
| #[cfg(feature = "targets")] |
| Triple(cfg_expr::target_lexicon::Triple), |
| Unknown(String), |
| } |
| |
| impl<T> From<T> for Target |
| where |
| T: AsRef<str>, |
| { |
| fn from(triple: T) -> Self { |
| let triple = triple.as_ref(); |
| match cfg_expr::targets::get_builtin_target_by_triple(triple) { |
| Some(bi) => Self::Builtin(bi), |
| None => { |
| #[cfg(feature = "targets")] |
| { |
| match triple.parse() { |
| Ok(triple) => Self::Triple(triple), |
| Err(_) => Self::Unknown(triple.to_owned()), |
| } |
| } |
| |
| #[cfg(not(feature = "targets"))] |
| Self::Unknown(triple.to_owned()) |
| } |
| } |
| } |
| } |
| |
| use std::fmt; |
| |
| impl fmt::Display for Target { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| match self { |
| Self::Builtin(bi) => f.write_str(bi.triple.as_str()), |
| #[cfg(feature = "targets")] |
| Self::Triple(trip) => write!(f, "{}", trip), |
| Self::Unknown(unknown) => f.write_str(unknown), |
| } |
| } |
| } |
| |
| struct TargetFilter { |
| inner: Target, |
| features: Vec<String>, |
| } |
| |
| impl TargetFilter { |
| fn eval(&self, predicate: &cfg_expr::Predicate<'_>) -> bool { |
| match predicate { |
| cfg_expr::expr::Predicate::Target(tp) => match &self.inner { |
| Target::Builtin(bi) => tp.matches(*bi), |
| #[cfg(feature = "targets")] |
| Target::Triple(trip) => tp.matches(trip), |
| Target::Unknown(_) => false, |
| }, |
| cfg_expr::expr::Predicate::TargetFeature(feat) => { |
| // TODO: target_features are extremely rare in cargo.toml |
| // files, it might be a good idea to inform the user of this |
| // somehow, if they are unsure why a particular dependency |
| // is being filtered |
| self.features.iter().any(|f| f == feat) |
| } |
| // We *could* warn here about an invalid expression, but |
| // presumably cargo will be responsible for that, so don't bother |
| _ => false, |
| } |
| } |
| |
| fn matches_triple(&self, triple: &str) -> bool { |
| match &self.inner { |
| Target::Builtin(bi) => bi.triple.as_str() == triple, |
| #[cfg(feature = "targets")] |
| Target::Triple(trip) => { |
| let as_triple = format!("{}", trip); |
| as_triple == triple |
| } |
| Target::Unknown(unknown) => unknown == triple, |
| } |
| } |
| } |
| |
| /// The scope for which a dependency kind will be ignored |
| #[derive(Clone, Copy)] |
| pub enum Scope { |
| /// Will match a dependency from a crate in the workspace |
| Workspace, |
| /// Will match a dependency from any crate not in the workspace |
| NonWorkspace, |
| /// Will ignore a dependency from any crate |
| All, |
| } |
| |
| /// Trait used to report back any crates that are completely ignored in the |
| /// final crate graph that is built. This occurs when the crate has no |
| /// dependents any longer due to the applied filters. |
| pub trait OnFilter { |
| fn filtered(&mut self, krate: cm::Package); |
| } |
| |
| /// For when you just want to satisfy [`OnFilter`] without doing anything |
| pub struct NoneFilter; |
| impl OnFilter for NoneFilter { |
| fn filtered(&mut self, _: cm::Package) {} |
| } |
| |
| impl<F> OnFilter for F |
| where |
| F: FnMut(cm::Package), |
| { |
| fn filtered(&mut self, krate: cm::Package) { |
| self(krate); |
| } |
| } |
| |
| /// A builder used to create a Krates graph, either by running a cargo metadata |
| /// command, or using an already deserialized [`cargo_metadata::Metadata`] |
| #[derive(Default)] |
| pub struct Builder { |
| target_filters: Vec<TargetFilter>, |
| workspace_filters: Vec<PathBuf>, |
| exclude: Vec<crate::PkgSpec>, |
| ignore_kinds: u32, |
| workspace: bool, |
| crates_io_index: Option<index::BuildIndexCache>, |
| } |
| |
| impl Builder { |
| pub fn new() -> Self { |
| Self::default() |
| } |
| |
| /// Ignores a specific dependency kind in the given scope. |
| /// |
| /// ``` |
| /// # use krates::{Builder, DepKind, Scope}; |
| /// Builder::new().ignore_kind(DepKind::Build, Scope::NonWorkspace); |
| /// ``` |
| /// |
| /// In the above example, let's say we depended on `zstd`. zstd depends on |
| /// the `cc` crate (`zstd -> zstd-safe -> zstd-sys -> cc`) for building |
| /// C code. By ignoring the `build` kind for non-workspace crates, the link |
| /// from `zstd-sys` -> `cc` will be filtered out. If the same `cc` is not |
| /// depended on by a crate in the workspace, `cc` will not end up in the |
| /// final `Krates` graph. |
| /// |
| /// Note that ignoring [`DepKind::Dev`] for [`Scope::NonWorkspace`] is |
| /// meaningless as dev dependencies are not resolved by cargo for transitive |
| /// dependencies. |
| pub fn ignore_kind(&mut self, kind: DepKind, scope: Scope) -> &mut Self { |
| let kind_flag = match kind { |
| DepKind::Normal => 0x1, |
| DepKind::Dev => 0x8, |
| DepKind::Build => 0x40, |
| }; |
| |
| self.ignore_kinds |= kind_flag; |
| |
| self.ignore_kinds |= match scope { |
| Scope::Workspace => kind_flag << 1, |
| Scope::NonWorkspace => kind_flag << 2, |
| Scope::All => kind_flag << 1 | kind_flag << 2, |
| }; |
| |
| self |
| } |
| |
| /// By default, the response from `cargo metadata` determines what the |
| /// root(s) of the crate graph will be. If the Cargo.toml path used is a |
| /// virtual manifest, then each workspace member will be used as a root. If |
| /// the manifest path is for a single crate, or a non-virtual manifest |
| /// inside a workspace, then only that single crate will be used as the |
| /// root, and in the workspace case, only other workspace members that are |
| /// dependencies of that root crate, directly or indirectly, will be |
| /// included in the final graph. |
| /// |
| /// Setting workspace = true will change that default behavior, and instead |
| /// include all workspace crates (unless they are filtered via other |
| /// methods) even if the manifest path is not a virtual manifest inside |
| /// a workspace |
| /// |
| /// ``` |
| /// # use krates::Builder; |
| /// Builder::new().workspace(true); |
| /// ``` |
| pub fn workspace(&mut self, workspace: bool) -> &mut Self { |
| self.workspace = workspace; |
| self |
| } |
| |
| /// Package specification(s) to exclude from the final graph. Unlike with |
| /// cargo, each exclusion spec can apply to more than 1 instance of a |
| /// package, eg if multiple crates are sourced from the same url, or |
| /// multiple versions of the same crate |
| /// |
| /// ``` |
| /// # use krates::Builder; |
| /// Builder::new().exclude(["a-crate:0.1.0"].iter().map(|spec| spec.parse().unwrap())); |
| /// ``` |
| pub fn exclude<I>(&mut self, exclude: I) -> &mut Self |
| where |
| I: IntoIterator<Item = crate::PkgSpec>, |
| { |
| self.exclude.extend(exclude); |
| self |
| } |
| |
| /// By default, every workspace crate is treated as a root node and |
| /// implicitly added to the graph if the graph is built from a workspace |
| /// context and not a specific crate in the workspace. |
| /// |
| /// By using this method, only the workspace crates whose Cargo.toml path |
| /// matches one of the specified crates will be added as root nodes, meaning |
| /// that any workspace crate not in the list that doesn't have any |
| /// dependendents on a workspace crate that does, will no longer appear in |
| /// the graph. |
| /// |
| /// If you specify only a single path, and that path is actually to a |
| /// a workspace's virtual manifest, the graph will be the same as if |
| /// [`Builder::include_workspace_crates`] was not specified. |
| /// |
| /// ``` |
| /// # use krates::{Builder, DepKind, Scope}; |
| /// Builder::new().include_workspace_crates(&["path/to/some/crate"]); |
| /// ``` |
| pub fn include_workspace_crates<P, I>(&mut self, crates: I) -> &mut Self |
| where |
| P: AsRef<Path>, |
| I: IntoIterator<Item = P>, |
| { |
| self.workspace_filters.extend(crates.into_iter().map(|p| { |
| // It's the users's responsibility to give proper paths here, as |
| // we can't rely on eg canonicalization since we might not be on |
| // the same filesystem any more. We do fixup the ensure they point |
| // at a Cargo.toml however |
| let p = p.as_ref(); |
| |
| // Attempt to canonicalize the path, which might not work if the |
| // user is attempting to add a path to filter from another |
| // filesystem or similar |
| let p = p.canonicalize().unwrap_or_else(|_| p.to_owned()); |
| |
| if !p.ends_with("Cargo.toml") { |
| p.join("Cargo.toml") |
| } else { |
| p |
| } |
| })); |
| |
| self |
| } |
| |
| /// By default, cargo resolves all target specific dependencies. Optionally, |
| /// you can use the `--filter-platform` option on `cargo metadata` to |
| /// resolve only dependencies that match the specified target, but it can |
| /// only do this for one platlform. |
| /// |
| /// By using this method, you can specify one or more targets by their |
| /// triple, as well as any [`target_features`](https://doc.rust-lang.org/reference/attributes/codegen.html#the-target_feature-attribute) |
| /// that you [promise](https://doc.rust-lang.org/reference/behavior-considered-undefined.html) |
| /// are enabled for that target to filter dependencies by. If any of the |
| /// specified targets matches a target specific dependency, it will be |
| /// included in the graph. |
| /// |
| /// When specifying a target triple, only builtin targets of rustc can be |
| /// used to evaluate `cfg()` expressions. If the triple is not recognized, |
| /// it will only be evaluated against `[target.<triple-or-json>.<|build-|dev->dependencies]`. |
| /// |
| /// ``` |
| /// # use krates::{Builder, DepKind, Scope}; |
| /// let targets = [ |
| /// // the big 3 |
| /// "x86_64-unknown-linux-gnu", |
| /// "x86_64-pc-windows-msvc", |
| /// "x86_64-apple-darwin", |
| /// // and musl! |
| /// "x86_64-unknown-linux-musl", |
| /// // and wasm (with the fancy atomics feature!) |
| /// "wasm32-unknown-unknown", |
| /// ]; |
| /// |
| /// Builder::new().include_targets(targets.iter().map(|triple| { |
| /// if triple.starts_with("wasm32") { |
| /// (*triple, vec!["atomics".to_owned()]) |
| /// } else { |
| /// (*triple, vec![]) |
| /// } |
| /// })); |
| /// ``` |
| pub fn include_targets<S: Into<Target>>( |
| &mut self, |
| targets: impl IntoIterator<Item = (S, Vec<String>)>, |
| ) -> &mut Self { |
| self.target_filters |
| .extend(targets.into_iter().map(|(triple, features)| TargetFilter { |
| inner: triple.into(), |
| features, |
| })); |
| self |
| } |
| |
| /// Configures the index implementation |
| /// |
| /// This method allows overriding the location of your `CARGO_HOME`, but note |
| /// that no fetching from the remote index is performed by this library, so |
| /// it is your responsibility to have called `cargo fetch` or similar to have |
| /// an up to date index cache at the location provided |
| /// |
| /// This method takes into account the local environment to open the correct |
| /// crates.io registry, or replacement registry if the user has configured that |
| /// |
| /// You can force the usage of the git registry (if not overridden in the |
| /// local environment) by specifying `Some("1.69.0")` or lower semver as the |
| /// sparse registry was not made the default until `1.70.0` |
| #[inline] |
| pub fn with_crates_io_index(&mut self, index_cache_build: index::BuildIndexCache) -> &mut Self { |
| self.crates_io_index = Some(index_cache_build); |
| self |
| } |
| |
| /// Builds a [`Krates`] graph using metadata that be retrieved via the |
| /// specified metadata command. If `on_filter` is specified, it will be |
| /// called with each package that was filtered from the graph, if any. |
| /// |
| /// This method will fail if the metadata command fails for some reason, or |
| /// if the command specifies `--no-deps` which means there will be no |
| /// resolution graph to build our graph from. |
| /// |
| /// ```no_run |
| /// # use krates::cm::Package; |
| /// let mut mdc = krates::Cmd::new(); |
| /// mdc.manifest_path("path/to/Cargo.toml"); |
| /// |
| /// if /*no_default_features*/ true { |
| /// mdc.no_default_features(); |
| /// } |
| /// |
| /// if /*cfg.all_features*/ false { |
| /// mdc.all_features(); |
| /// } |
| /// |
| /// mdc.features( |
| /// ["cool-feature", "cooler-feature", "coolest-feature"] |
| /// .iter() |
| /// .map(|s| s.to_string()), |
| /// ); |
| /// |
| /// let mut builder = krates::Builder::new(); |
| /// |
| /// if /*cfg.ignore_build_dependencies*/ false { |
| /// builder.ignore_kind(krates::DepKind::Build, krates::Scope::All); |
| /// } |
| /// |
| /// if /*cfg.ignore_dev_dependencies*/ true { |
| /// builder.ignore_kind(krates::DepKind::Dev, krates::Scope::All); |
| /// } |
| /// |
| /// let graph: krates::Krates = builder.build( |
| /// mdc, |
| /// |filtered: Package| match filtered.source { |
| /// Some(src) => { |
| /// if src.is_crates_io() { |
| /// println!("filtered {} {}", filtered.name, filtered.version); |
| /// } else { |
| /// println!("filtered {} {} {}", filtered.name, filtered.version, src); |
| /// } |
| /// } |
| /// None => println!("filtered crate {} {}", filtered.name, filtered.version), |
| /// }, |
| /// ).unwrap(); |
| /// ``` |
| pub fn build<N, E, F>( |
| self, |
| cmd: impl Into<cm::MetadataCommand>, |
| on_filter: F, |
| ) -> Result<Krates<N, E>, Error> |
| where |
| N: From<cargo_metadata::Package>, |
| E: From<Edge>, |
| F: OnFilter, |
| { |
| let metadata = cmd.into().exec()?; |
| self.build_with_metadata(metadata, on_filter) |
| } |
| |
| /// Builds a [`Krates`] graph using the specified metadata. If `on_filter` is |
| /// specified, it will be called with each package that was filtered from |
| /// the graph, if any. |
| /// |
| /// The metadata **must** have resolved dependencies for the graph to be |
| /// built, so not having it is the only way this method will fail. |
| /// |
| /// ```no_run |
| /// # use krates::{Krates, Builder, DepKind, Scope, cm::Package}; |
| /// let contents = std::fs::read_to_string("metadata.json") |
| /// .map_err(|e| format!("failed to load metadata file: {}", e)).unwrap(); |
| /// |
| /// let md: krates::cm::Metadata = serde_json::from_str(&contents) |
| /// .map_err(|e| format!("failed to deserialize metadata: {}", e)).unwrap(); |
| /// |
| /// let krates: Krates = Builder::new().build_with_metadata( |
| /// md, |
| /// |pkg: Package| println!("filtered {}", pkg.id) |
| /// ).unwrap(); |
| /// |
| /// println!("found {} unique crates", krates.len()); |
| /// ``` |
| pub fn build_with_metadata<N, E, F>( |
| self, |
| md: cargo_metadata::Metadata, |
| mut on_filter: F, |
| ) -> Result<Krates<N, E>, Error> |
| where |
| N: From<cargo_metadata::Package>, |
| E: From<Edge>, |
| F: OnFilter, |
| { |
| let mut resolved = md.resolve.ok_or(Error::NoResolveGraph)?; |
| |
| let mut packages: Vec<_> = md |
| .packages |
| .into_iter() |
| .map(|pkg| (Kid::from(pkg.id.clone()), pkg)) |
| .collect(); |
| packages.sort_by(|(a, _), (b, _)| a.cmp(b)); |
| |
| // Load all the cache entries from disk for all the possible _unique_ |
| // crates in the graph so that we don't need to access disk again |
| let index = self.crates_io_index.map(|index| { |
| index::CachingIndex::new( |
| index, |
| packages.iter().map(|(_id, pkg)| pkg.name.clone()).collect(), |
| ) |
| }); |
| |
| let mut workspace_members: Vec<_> = |
| md.workspace_members.into_iter().map(Kid::from).collect(); |
| workspace_members.sort(); |
| |
| let root = resolved.root.take().map(Kid::from); |
| |
| let roots = { |
| let mut roots = BTreeSet::new(); |
| |
| // Only include workspaces members the user wants if they have specified |
| // any, this is to take into account scenarios where you have a large |
| // workspace, but only want to get the crates used by a subset of the |
| // workspace |
| if self.workspace_filters.is_empty() { |
| // If the resolve graph specifies a root, it means the user |
| // specified a particular crate in a workspace, so we'll only use |
| // that single root for the entire graph rather than a root for each |
| // workspace member crate |
| if !self.workspace { |
| if let Some(rkid) = &root { |
| roots.insert(rkid); |
| } |
| } |
| |
| if roots.is_empty() { |
| roots.extend(workspace_members.iter()); |
| } |
| } else { |
| // If the filters only contain 1 path, and it is the path to a |
| // workspace toml, then we disregard the filters |
| if self.workspace_filters.len() == 1 |
| && Some(md.workspace_root.as_ref()) == self.workspace_filters[0].parent() |
| { |
| roots.extend(workspace_members.iter()); |
| } else { |
| for wm in &workspace_members { |
| if let Ok(i) = packages.binary_search_by(|(id, _pkg)| id.cmp(wm)) { |
| if self |
| .workspace_filters |
| .iter() |
| .any(|wf| wf == &packages[i].1.manifest_path) |
| { |
| roots.insert(wm); |
| } |
| } |
| } |
| } |
| } |
| |
| roots |
| }; |
| |
| let is_root_crate = |pid: &Kid| -> bool { roots.contains(&pid) }; |
| |
| let exclude = self.exclude; |
| |
| let include_all_targets = self.target_filters.is_empty(); |
| let ignore_kinds = self.ignore_kinds; |
| let targets = self.target_filters; |
| |
| #[derive(Debug)] |
| struct DepKindInfo { |
| kind: DepKind, |
| cfg: Option<String>, |
| } |
| |
| #[derive(Debug)] |
| // We use our resolution nodes because cargo_metadata uses |
| // non-exhaustive everywhere :p |
| struct NodeDep { |
| /// The name of the dependency, which could be a different name than the crate itself |
| name: String, |
| pkg: Kid, |
| dep_kinds: Vec<DepKindInfo>, |
| } |
| |
| #[derive(Debug)] |
| struct Node { |
| id: Kid, |
| deps: Vec<NodeDep>, |
| features: Vec<String>, |
| has_default_feature: bool, |
| } |
| |
| impl Node { |
| #[inline] |
| fn feature_index(&self, feat: &str) -> usize { |
| match self.features.binary_search_by(|f| f.as_str().cmp(feat)) { |
| Ok(i) => i, |
| Err(_i) => { |
| unreachable!( |
| "unable to locate {feat} for crate {} features({:?})", |
| self.id, self.features |
| ); |
| } |
| } |
| } |
| |
| #[inline] |
| fn feature(&self, index: usize) -> &str { |
| self.features[index].as_str() |
| } |
| } |
| |
| let mut nodes: Vec<_> = resolved |
| .nodes |
| .into_iter() |
| .map(|rn| { |
| let id = Kid::from(rn.id); |
| if let Err(i) = packages.binary_search_by(|(pid, _)| pid.cmp(&id)) { |
| unreachable!( |
| "oh no, it looks we couldn't find '{id}', maybe it is this? {:#?}", |
| &packages[i] |
| ); |
| } |
| |
| let mut deps: Vec<_> = rn |
| .deps |
| .into_iter() |
| .map(|dn| { |
| // This requires 1.41+ |
| let dep_kinds = dn |
| .dep_kinds |
| .into_iter() |
| .map(|dk| DepKindInfo { |
| kind: dk.kind.into(), |
| cfg: dk.target.map(|t| t.to_string()), |
| }) |
| .collect(); |
| |
| NodeDep { |
| name: dn.name, |
| pkg: Kid::from(dn.pkg), |
| dep_kinds, |
| } |
| }) |
| .collect(); |
| |
| // These _should_ always already be sorted, but again, might be |
| // due to implementation details rather than guaranteed |
| deps.sort_by(|a, b| a.pkg.cmp(&b.pkg)); |
| |
| let mut features = rn.features; |
| |
| // Note that cargo metadata _currently_ always outputs these in |
| // lexicographic order, but I don't know if that is actually |
| // guaranteed at all and might just be due to the implementation |
| // (eg stored in a Btree), so we just perform our own sort to |
| // guarantee that this is indeed always sorted since we rely on |
| // that attribute |
| features.sort(); |
| let has_default_feature = features |
| .binary_search_by(|f| f.as_str().cmp("default")) |
| .is_ok(); |
| |
| Node { |
| id, |
| deps, |
| features, |
| has_default_feature, |
| } |
| }) |
| .collect(); |
| |
| nodes.sort_by(|a, b| a.id.cmp(&b.id)); |
| |
| // #[inline] |
| // fn crate_name_from_pid(pid: &PackageId) -> &str { |
| // if let Some(name_end) = pid.repr.find(' ') { |
| // &pid.repr[..name_end] |
| // } else { |
| // DecomposedRepr::build(pid).name |
| // } |
| // } |
| |
| let mut dep_edge_map = BTreeMap::new(); |
| let mut feature_edge_map = BTreeMap::new(); |
| |
| // The stack of pid + features that are visited until we've reached every |
| // unique leaf. |
| // |
| // This is rooted on the root packages + enabled features as returned by |
| // cargo metadata, but we (no longer) use the rest of the output as is, |
| // as we can prune edges based on cfg/build kind, or even exclude packages |
| // entirely, and thus have to basically re-resolve the crates + features |
| // to avoid adding nodes/edges that shouldn't exist based on the caller's |
| // configuration (eg. https://github.com/EmbarkStudios/krates/issues/60) |
| let mut visit_stack = Vec::with_capacity(roots.len()); |
| |
| for root in &roots { |
| let krate_index = nodes.binary_search_by(|n| n.id.cmp(root)).unwrap(); |
| let rnode = &nodes[krate_index]; |
| |
| visit_stack.push((*root, None)); |
| |
| for feat in 0..rnode.features.len() { |
| visit_stack.push((*root, Some(feat))); |
| } |
| } |
| |
| #[derive(Debug)] |
| struct DependencyEdge { |
| kind: DepKind, |
| cfg: Option<String>, |
| features: Vec<usize>, |
| } |
| |
| #[derive(Debug)] |
| struct PackageNode<'p> { |
| is_root: bool, |
| deps: BTreeMap<&'p Kid, KrateDependency>, |
| } |
| |
| #[derive(Debug)] |
| struct KrateDependency { |
| edges: Vec<DependencyEdge>, |
| features: BTreeSet<usize>, |
| } |
| |
| #[derive(Debug)] |
| enum FeatureEdgeName { |
| Feature(String), |
| Rename(String), |
| Krate, |
| } |
| |
| #[derive(Debug)] |
| struct FeatureEdge<'nodes> { |
| kid: &'nodes Kid, |
| name: FeatureEdgeName, |
| } |
| |
| #[derive(Debug)] |
| struct KrateFeatures<'p> { |
| /// The inner DAG of features, note these are _all_ resolved features, |
| /// but might not be actually be present in the final graph if they |
| /// reference pruned krates |
| graph: Vec<(String, Vec<FeatureEdge<'p>>)>, |
| /// The actual set of features enabled by 1 or more parent krates |
| actual: BTreeSet<usize>, |
| /// Weakly referenced features |
| pending_weak: BTreeMap<&'p Kid, BTreeSet<usize>>, |
| filled_non_optional: bool, |
| } |
| |
| let check = |map: &BTreeMap<&Kid, KrateFeatures<'_>>, pid: &Kid, feature: Option<usize>| { |
| map.get(pid).map_or(false, |pn| { |
| if let Some(feat) = feature { |
| pn.actual.contains(&feat) |
| } else { |
| pn.filled_non_optional |
| } |
| }) |
| }; |
| |
| let get_rnode = |pid: &Kid| &nodes[nodes.binary_search_by(|n| n.id.cmp(pid)).unwrap()]; |
| |
| while let Some((pid, feature)) = visit_stack.pop() { |
| if check(&feature_edge_map, pid, feature) { |
| continue; |
| } |
| |
| let is_in_workspace = workspace_members.binary_search(pid).is_ok(); |
| |
| let krate_index = nodes.binary_search_by(|n| n.id.cmp(pid)).unwrap(); |
| |
| let rnode = &nodes[krate_index]; |
| let (_, krate) = &mut packages[krate_index]; |
| |
| if exclude.iter().any(|exc| exc.matches(krate)) { |
| continue; |
| } |
| |
| let get_dep_id = |dep_name: &str| -> Option<&Kid> { |
| rnode.deps.iter().find_map(|ndep| { |
| if dep_names_match(dep_name, &ndep.name) || dep_name == ndep.pkg.name() { |
| Some(&ndep.pkg) |
| } else { |
| None |
| } |
| }) |
| }; |
| |
| let krate_features = feature_edge_map.entry(pid).or_insert_with(|| { |
| if let Some(index) = &index { |
| index::fix_features(index, krate); |
| } |
| |
| // Cargo puts out a flat list of the enabled features, but we need |
| // to use the declared features on the crate itself to figure out |
| // the actual chain of features from one crate to another |
| // package features: |
| // "features": { |
| // "blocking": [ |
| // "simple", |
| // "reqwest?/blocking" |
| // ], |
| // "default": [ |
| // "simple" |
| // ], |
| // "json": [ |
| // "reqwest?/json" |
| // ], |
| // "multipart": [ |
| // "reqwest?/multipart" |
| // ], |
| // "reqwest": [ |
| // "dep:reqwest" |
| // ], |
| // "rgb": [ |
| // "dep:rgb" |
| // ], |
| // "serde": [ |
| // "dep:serde", |
| // "rgb?/serde" |
| // ], |
| // "simple": [ |
| // "json" |
| // ], |
| // "ssh": [ |
| // "git/ssh", |
| // "git/ssh_key_from_memory" |
| // ], |
| // "stream": [ |
| // "reqwest/stream" |
| // ], |
| // "zlib": [ |
| // "git/zlib-ng-compat", |
| // "reqwest?/deflate" |
| // ] |
| // }, |
| // resolved features: |
| // "features": [ |
| // "blocking", |
| // "json", |
| // "reqwest", |
| // "simple", |
| // "stream" |
| // ] |
| let graph: Vec<_> = rnode |
| .features |
| .iter() |
| .filter_map(|feat| { |
| // This _should_ never fail in normal cases, however if we |
| // aren't provided with an index implementation it's possible for |
| // the resolved features to mention features that aren't in |
| // the actual crate manifest |
| let sub_feats: Vec<_> = krate |
| .features |
| .get(feat)? |
| .iter() |
| .filter_map(|sub_feat| { |
| let sf = ParsedFeature::from(sub_feat.as_str()); |
| |
| match sf.feat() { |
| Feature::Krate(krate_name) => { |
| let kid = get_dep_id(krate_name)?; |
| let real_name = kid.name(); |
| |
| Some(FeatureEdge { |
| kid, |
| name: if real_name != krate_name { |
| FeatureEdgeName::Rename(krate_name.to_owned()) |
| } else { |
| FeatureEdgeName::Krate |
| }, |
| }) |
| } |
| Feature::Simple(s) => Some(FeatureEdge { |
| kid: pid, |
| name: FeatureEdgeName::Feature(s.to_owned()), |
| }), |
| Feature::Strong { |
| krate: krate_name, |
| feature, |
| } => Some(FeatureEdge { |
| kid: get_dep_id(krate_name)?, |
| name: FeatureEdgeName::Feature(feature.to_owned()), |
| }), |
| Feature::Weak { |
| krate: krate_name, |
| feature, |
| } => { |
| if rnode.features.iter().any(|kn| kn == krate_name) { |
| Some(FeatureEdge { |
| kid: get_dep_id(krate_name)?, |
| name: FeatureEdgeName::Feature(feature.to_owned()), |
| }) |
| } else { |
| None |
| } |
| } |
| } |
| }) |
| .collect(); |
| |
| Some((feat.clone(), sub_feats)) |
| }) |
| .collect(); |
| |
| KrateFeatures { |
| graph, |
| actual: BTreeSet::new(), |
| pending_weak: BTreeMap::new(), |
| filled_non_optional: false, |
| } |
| }); |
| |
| let pn = dep_edge_map.entry(pid).or_insert_with(|| PackageNode { |
| is_root: is_root_crate(pid), |
| deps: BTreeMap::new(), |
| }); |
| |
| let mut deps = BTreeMap::<&Kid, (&NodeDep, Option<BTreeSet<usize>>)>::new(); |
| |
| if let Some(feature) = feature { |
| if !krate_features.actual.insert(feature) { |
| unreachable!(); |
| } |
| |
| if !krate_features.filled_non_optional { |
| visit_stack.push((pid, None)); |
| } |
| |
| // This _should_ never fail in normal cases, however if the |
| // an index implementation is not provided, it's possible for |
| // the resolved features to mention features that aren't in |
| // the actual crate manifest |
| let fs = if let Some(fs) = krate.features.get(rnode.feature(feature)) { |
| fs |
| } else { |
| // TODO: Show the user a warning, or just fail? |
| continue; |
| }; |
| |
| for sf in fs { |
| let pf = ParsedFeature::from(sf.as_str()); |
| |
| let (krate_name, feature) = match pf.feat() { |
| Feature::Simple(feat) => { |
| visit_stack.push((pid, Some(rnode.feature_index(feat)))); |
| continue; |
| } |
| Feature::Krate(krate) => (krate, None), |
| Feature::Strong { krate, feature } | Feature::Weak { krate, feature } => { |
| (krate, Some(feature)) |
| } |
| }; |
| |
| let Some(ndep) = rnode.deps.iter().find(|rdep| { |
| dep_names_match(krate_name, &rdep.name) || krate_name == rdep.pkg.name() |
| }) else { |
| // We can have a feature that points to a crate that isn't resolved by cargo due to it being |
| // a dev-only dependency |
| continue; |
| }; |
| |
| let rdep_node = get_rnode(&ndep.pkg); |
| let is_weak = matches!(pf.feat(), Feature::Weak { .. }); |
| |
| if is_weak && !pn.deps.contains_key(&ndep.pkg) { |
| let feature = rdep_node.feature_index(feature.unwrap()); |
| krate_features |
| .pending_weak |
| .entry(&ndep.pkg) |
| .or_default() |
| .insert(feature); |
| continue; |
| } else if let Some(mut pending) = krate_features.pending_weak.remove(&ndep.pkg) |
| { |
| deps.entry(&ndep.pkg) |
| .or_insert_with(|| (ndep, Some(BTreeSet::new()))) |
| .1 |
| .as_mut() |
| .unwrap() |
| .append(&mut pending); |
| } |
| |
| let feats = deps |
| .entry(&ndep.pkg) |
| .or_insert_with(|| (ndep, Some(BTreeSet::new()))) |
| .1 |
| .as_mut() |
| .unwrap(); |
| |
| if let Some(feature) = feature { |
| feats.insert(rdep_node.feature_index(feature)); |
| } |
| } |
| } else { |
| krate_features.filled_non_optional = true; |
| deps.extend(rnode.deps.iter().map(|ndep| (&ndep.pkg, (ndep, None)))); |
| } |
| |
| let targets = &targets; |
| |
| // Though each unique dependency can only be resolved once, it's possible |
| // for the crate to list the same dependency multiple times, with different |
| // dependency kinds, or different target configurations, so each one gets its |
| // own edge |
| for (pkg, (rdep, mut features)) in deps { |
| let rdep_node = get_rnode(pkg); |
| |
| #[derive(Debug)] |
| struct Edge<'d> { |
| kind: DepKind, |
| cfg: Option<&'d str>, |
| features: &'d [String], |
| uses_default_features: bool, |
| } |
| |
| let maybe_real_name = pkg.name(); |
| let strong = features.is_some(); |
| |
| // If there are multiple versions of the same package we use the |
| // version to disambiguate references to them, unfortunately, |
| // though (it should be) extremely rare to do this, we always |
| // check that versions match once a crate is found, as nodes can |
| // not be resolved due to features, but will still be listed as |
| // a dependency |
| // |
| // Note that cargo _should_ fail to resolve nodes if the same |
| // package is referenced with two `^` (compatible) semvers, ie, |
| // you can't reference both ">= 0.2.12" and "=0.2.7" of a package |
| // even if they could never point to the same package, this _may_ |
| // mean there could be a situation where a single crate _could_ |
| // be referenced with 0.0.x versions, but...I'll fix that if |
| // someone reports an issue |
| let rdep_version: semver::Version = |
| rdep.pkg.version().parse().expect("failed to parse semver"); |
| let has_prelease = !rdep_version.pre.is_empty(); |
| |
| let edges = rdep.dep_kinds.iter().filter_map(|dk| { |
| let mask = match dk.kind { |
| DepKind::Normal => 0x1, |
| DepKind::Dev => 0x8, |
| DepKind::Build => 0x40, |
| }; |
| |
| let mask = mask | mask << if is_in_workspace { 1 } else { 2 }; |
| if mask & ignore_kinds == mask { |
| return None; |
| } |
| |
| let dep = krate |
| .dependencies |
| .iter() |
| .find(|dep| { |
| if dk.kind != dep.kind { |
| return false; |
| } |
| |
| // Crates can rename the dependency package themselves |
| let dep_name = dep.rename.as_deref().unwrap_or(&dep.name); |
| if !dep_names_match(dep_name, &rdep.name) && maybe_real_name != dep_name { |
| return false; |
| } |
| |
| // Handle case where a dependency may not have a version requirement, which |
| // typically happens in the case of non-registry dependencies that use a pre-release |
| // semver, if the version _is_ a prelease it will never match the empty |
| // requirement |
| (has_prelease && dep.req.comparators.is_empty()) || dep.req.matches(&rdep_version) |
| }) |
| .unwrap_or_else(|| panic!("cargo metadata resolved a dependency for a dependency not specified by the crate: {rdep:?}")); |
| |
| if dep.optional && !strong { |
| return None; |
| } |
| |
| let cfg = if let Some(cfg) = dk.cfg.as_deref() { |
| 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 |
| } |
| } |
| } 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)) |
| }; |
| |
| if !matched { |
| return None; |
| } |
| } |
| |
| Some(cfg) |
| } else { |
| None |
| }; |
| |
| Some(Edge { |
| kind: dk.kind, |
| cfg, |
| features: &dep.features, |
| // Dependencies will default to saying "uses_default_features" on edges, |
| // even if the crate in question doesn't actually have a "default" feature, |
| // so check that it actually does |
| uses_default_features: dep.uses_default_features && rdep_node.has_default_feature, |
| }) |
| }); |
| |
| // Don't add the dependency unless we have at least one edge, otherwise |
| // we could include optional dependencies that are only weakly referenced |
| let mut edges = edges.peekable(); |
| if edges.peek().is_none() { |
| continue; |
| } |
| |
| let dep = pn.deps.entry(pkg).or_insert_with(|| KrateDependency { |
| edges: Vec::new(), |
| features: BTreeSet::new(), |
| }); |
| |
| let mut visit_dep = Some((pkg, None)); |
| |
| for edge in edges { |
| if let Some(features) = features.take() { |
| visit_stack.extend(visit_dep.take()); |
| for feat in features { |
| visit_stack.push((pkg, Some(feat))); |
| dep.features.insert(feat); |
| } |
| } |
| |
| if !dep |
| .edges |
| .iter() |
| .any(|d| d.kind == edge.kind && d.cfg.as_deref() == edge.cfg) |
| { |
| visit_stack.extend(visit_dep.take()); |
| |
| let mut features = Vec::new(); |
| for feat in edge |
| .features |
| .iter() |
| .map(|f| f.as_str()) |
| .chain(edge.uses_default_features.then_some("default")) |
| { |
| let feat_index = rdep_node.feature_index(feat); |
| visit_stack.push((pkg, Some(feat_index))); |
| features.push(feat_index); |
| } |
| |
| let edge = DependencyEdge { |
| kind: edge.kind, |
| cfg: edge.cfg.map(|s| s.into()), |
| features, |
| }; |
| |
| dep.edges.push(edge); |
| } |
| } |
| } |
| } |
| |
| // Sanity check, it's possible the user could exclude all of the |
| // possible workspace root nodes leaving themselves with an empty graph, |
| // which isn't much use to anyone |
| if dep_edge_map.is_empty() { |
| return Err(Error::NoRootKrates); |
| } |
| |
| let mut graph = petgraph::Graph::<crate::Node<N>, E>::new(); |
| graph.reserve_nodes(dep_edge_map.len()); |
| |
| let mut edge_count = 0; |
| |
| // Preserve the ordering of the krates when inserting them into the graph |
| // so that we can easily binary search for the crates based on their |
| // package id with just the graph and no ancillary tables |
| for (id, krate) in packages { |
| if let Some(pn) = dep_edge_map.get(&id) { |
| let rnode = get_rnode(&id); |
| |
| // If the crate is a root then the features it has enabled are |
| // accurate, however if it is not a root then we need to manually |
| // build up the list of enabled features as each edge is added |
| let features = if pn.is_root { |
| rnode.features.iter().cloned().collect() |
| } else { |
| feature_edge_map[&id] |
| .actual |
| .iter() |
| .map(|k| rnode.feature(*k).to_owned()) |
| .collect() |
| }; |
| |
| let krate = crate::Node::Krate { |
| id, |
| krate: N::from(krate), |
| features, |
| }; |
| |
| graph.add_node(krate); |
| edge_count += pn.deps.len(); |
| } else { |
| on_filter.filtered(krate); |
| } |
| } |
| |
| let krates_end = graph.node_count(); |
| |
| // Reserve space for each edge from a crate to another crate's feature(s) |
| graph.reserve_edges(edge_count); |
| |
| let get = |graph: &petgraph::Graph<crate::Node<N>, E>, |
| kid: &Kid, |
| feature: Option<&str>| |
| -> Option<crate::NodeId> { |
| if let Some(feat) = feature { |
| graph.raw_nodes()[krates_end..] |
| .iter() |
| .enumerate() |
| .find_map(|(i, n)| { |
| if let crate::Node::Feature { krate_index, name } = &n.weight { |
| if let crate::Node::Krate { id, .. } = &graph[*krate_index] { |
| if id == kid && name == feat { |
| return Some(crate::NodeId::new(i + krates_end)); |
| } |
| } |
| } |
| |
| None |
| }) |
| } else { |
| graph.raw_nodes()[..krates_end] |
| .iter() |
| .enumerate() |
| .find_map(|(i, n)| { |
| if let crate::Node::Krate { id, .. } = &n.weight { |
| if id == kid { |
| return Some(crate::NodeId::new(i)); |
| } |
| } |
| |
| None |
| }) |
| } |
| }; |
| |
| // Now that we have all of the actual crate nodes, we can link all of the |
| // features exposed by each crate |
| let (node_count, edge_count) = |
| feature_edge_map |
| .iter() |
| .fold((0usize, 0usize), |(nc, ec), (pid, feats)| { |
| let nc = nc + feats.actual.len() * 2; |
| let krate_index = nodes.binary_search_by(|n| n.id.cmp(pid)).unwrap(); |
| let rnode = &nodes[krate_index]; |
| |
| let ec = ec |
| + feats |
| .graph |
| .iter() |
| .filter_map(|(name, sf)| { |
| feats |
| .actual |
| .contains(&rnode.feature_index(name)) |
| .then_some(sf.len()) |
| }) |
| .sum::<usize>(); |
| |
| (nc, ec) |
| }); |
| |
| graph.reserve_nodes(node_count); |
| graph.reserve_edges(edge_count); |
| |
| // Keep edges between crates ordered as well, though we don't depend on this |
| for srcind in 0..graph.node_count() { |
| let srcid = crate::NodeId::new(srcind); |
| let pid = if let crate::Node::Krate { id, .. } = &graph[srcid] { |
| id |
| } else { |
| continue; |
| }; |
| |
| if let Some(pn) = dep_edge_map.remove(pid) { |
| // Attach an edge for each crate dependency, note that there might not |
| // actually be a target crate for the edge since crates can be pruned |
| // due to target configuration |
| for (dep_id, dep) in pn.deps { |
| let target_krate = if let Some(tk) = get(&graph, dep_id, None) { |
| tk |
| } else { |
| continue; |
| }; |
| |
| let attach = |graph: &mut petgraph::Graph<crate::Node<N>, E>, |
| feat: String, |
| edge: Edge| { |
| let feat_node = if let Some(feat_node) = get(graph, dep_id, Some(&feat)) { |
| feat_node |
| } else { |
| graph.add_node(crate::Node::Feature { |
| krate_index: target_krate, |
| name: feat, |
| }) |
| }; |
| |
| graph.add_edge(srcid, feat_node, edge.into()); |
| }; |
| |
| let rnode = get_rnode(dep_id); |
| |
| // Add the features that were explicitly enabled by the specific |
| // normal/dev/build dependency |
| for edge in dep.edges { |
| let attach_direct_edge = edge.features.is_empty(); |
| |
| for feat in edge.features { |
| let feat = rnode.feature(feat); |
| attach( |
| &mut graph, |
| feat.to_owned(), |
| Edge::DepFeature { |
| kind: edge.kind, |
| cfg: edge.cfg.clone(), |
| }, |
| ); |
| } |
| |
| if attach_direct_edge { |
| graph.add_edge( |
| srcid, |
| target_krate, |
| Edge::Dep { |
| kind: edge.kind, |
| cfg: edge.cfg, |
| } |
| .into(), |
| ); |
| } |
| } |
| |
| // Add the features that were toggled on via a parent crate feature |
| for feat in dep.features { |
| let feat = rnode.feature(feat); |
| attach(&mut graph, feat.to_owned(), Edge::Feature); |
| } |
| } |
| } |
| } |
| |
| // Now attach edges between all of features and their parent crate |
| for (pid, features) in feature_edge_map { |
| let (kind, mut feature_stack) = if let Some(kind) = get(&graph, pid, None) { |
| if let crate::Node::Krate { features, .. } = &graph[kind] { |
| (kind, features.iter().cloned().collect::<Vec<_>>()) |
| } else { |
| continue; |
| } |
| } else { |
| continue; |
| }; |
| |
| let get_or_insert = |graph: &mut petgraph::Graph<crate::Node<N>, E>, |
| kid: &Kid, |
| feature: &str| |
| -> crate::NodeId { |
| let node_id = |
| graph.raw_nodes()[krates_end..] |
| .iter() |
| .enumerate() |
| .find_map(|(i, n)| { |
| if let crate::Node::Feature { krate_index, name } = &n.weight { |
| if let crate::Node::Krate { id, .. } = &graph[*krate_index] { |
| if id == kid && name == feature { |
| return Some(crate::NodeId::new(i + krates_end)); |
| } |
| } |
| } |
| |
| None |
| }); |
| |
| node_id.unwrap_or_else(|| { |
| let krate_index = get(graph, kid, None).unwrap(); |
| |
| graph.add_node(crate::Node::Feature { |
| krate_index, |
| name: feature.to_owned(), |
| }) |
| }) |
| }; |
| |
| let mut enabled = features.graph; |
| |
| // Since we can prune crates either by kind, target, or user specification, |
| // the actual set of features might not be the same as those that |
| // the crate thinks they were, so we use the top level features that |
| // have been enabled by the known edges to recursively add all of |
| // the sub features |
| while let Some(feat) = feature_stack.pop() { |
| let (_parent, sub_features) = |
| if let Some(i) = enabled.iter().position(|(k, _)| *k == feat) { |
| enabled.swap_remove(i) |
| } else { |
| continue; |
| }; |
| |
| let src_id = get_or_insert(&mut graph, pid, &feat); |
| |
| // Also add an edge from each feature to the crate node it belongs to |
| graph.add_edge(src_id, kind, E::from(crate::Edge::Feature)); |
| |
| for sub_feat in sub_features { |
| if sub_feat.kid != pid && get(&graph, sub_feat.kid, None).is_none() { |
| continue; |
| } |
| |
| let feat_name = match sub_feat.name { |
| FeatureEdgeName::Feature(feat) => Some(feat), |
| FeatureEdgeName::Rename(kname) => Some(kname), |
| FeatureEdgeName::Krate => None, |
| }; |
| |
| let target_id = if let Some(target_id) = |
| get(&graph, sub_feat.kid, feat_name.as_deref()) |
| { |
| target_id |
| } else { |
| let feat_name = feat_name.unwrap_or_else(|| sub_feat.kid.name().to_owned()); |
| |
| let target_id = graph.add_node(crate::Node::Feature { |
| krate_index: kind, |
| name: feat_name.clone(), |
| }); |
| |
| // Ensure that all of the subfeatures enabled by the parent feature are added to the |
| // flat list of enabled features for the crate |
| if let crate::Node::Krate { features, .. } = &mut graph[kind] { |
| if !features.contains(&feat_name) { |
| features.insert(feat_name.clone()); |
| feature_stack.push(feat_name); |
| } |
| } |
| |
| target_id |
| }; |
| |
| graph.add_edge(src_id, target_id, E::from(crate::Edge::Feature)); |
| } |
| } |
| } |
| |
| Ok(Krates { |
| graph, |
| workspace_members, |
| workspace_root: md.workspace_root, |
| krates_end, |
| }) |
| } |
| } |
| |
| /// When renaming dependencies to something with a `-` in a Cargo.toml file, |
| /// the actual resolved name in the metadata will replace the `-` with a `_` so |
| /// we need to take that into account when comparing the names as declared in |
| /// the crate metadata with the dependencies in the resolved graph |
| #[inline] |
| fn dep_names_match(krate_dep_name: &str, resolved_name: &str) -> bool { |
| if krate_dep_name.len() != resolved_name.len() { |
| false |
| } else { |
| krate_dep_name |
| .chars() |
| .zip(resolved_name.chars()) |
| .all(|(kn, rn)| kn == rn || kn == '-' && rn == '_') |
| } |
| } |