Skip to content

Commit dbb6eab

Browse files
committed
fix mutli crate setup
1 parent 5e18c89 commit dbb6eab

File tree

31 files changed

+640
-247
lines changed

31 files changed

+640
-247
lines changed

codegen/crates/crate_feature_graph/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22
name = "crate_feature_graph"
33
version = "0.1.0"
44
edition = "2024"
5+
readme = "README.md"
6+
license = "MIT OR Apache-2.0"
7+
include = ["src/**", "bin/**", "Cargo.toml", "README.md", "example.png"]
58

69
[lib]
710
path = "src/lib.rs"
811

12+
[[bin]]
13+
name = "crate_feature_graph"
14+
path = "bin/main.rs"
15+
916
[features]
1017
dot_parser = ["petgraph/dot_parser"]
1118
serde = ["dep:serde", "dep:serde_json"]
@@ -18,6 +25,7 @@ log = { workspace = true }
1825
clap = { workspace = true, features = ["derive"] }
1926
serde = { workspace = true, features = ["derive"], optional = true }
2027
serde_json = { workspace = true, optional = true }
28+
pretty_env_logger = { workspace = true }
2129

2230
[dev-dependencies]
2331
petgraph = { workspace = true, features = ["dot_parser"] }
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Crate Feature Graph
2+
3+
![Example output graph](./example.png)
4+
5+
A tool for visualising feature flow in a rust workspace, from the perspective of build time features.
6+
7+
## Features
8+
- Compute feature flow throughout your workspace
9+
- Filter out crates you don't want to see from the graph, but retain the connections
10+
- Output as a dot graph or if the `dot_parser` feature is disabled as a text representation
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use clap::Parser;
2+
use crate_feature_graph::{CrateName, Workspace, WorkspaceGraph};
3+
4+
#[derive(Parser)]
5+
struct Args {
6+
/// Path to the Cargo.toml of the workspace to analyze
7+
#[clap(long, short, default_value = "./Cargo.toml")]
8+
manifest_path: String,
9+
10+
/// Comma-separated list of feature names to enable for the given manifest
11+
#[clap(
12+
short,
13+
long,
14+
default_value = "",
15+
use_value_delimiter = true,
16+
value_delimiter = ','
17+
)]
18+
features: Vec<String>,
19+
20+
/// Whether to disable the default features of the given manifest
21+
/// Defaults to true
22+
#[clap(long, default_value = "false")]
23+
disable_default_features: bool,
24+
25+
/// Only show crates in the output graph that match one of these names
26+
/// Only works for dot graphs
27+
/// Defaults to empty, which shows all crates
28+
#[clap(long, use_value_delimiter = true, value_delimiter = ',')]
29+
only_show_crates: Vec<String>,
30+
31+
/// The package to consider as the root of the search
32+
/// The default will use the root of the workspace
33+
/// The features provided will be applied to this package, unless a crate prefix is provided
34+
#[clap(long)]
35+
root_package: Option<String>,
36+
}
37+
38+
pub fn main() -> std::io::Result<()> {
39+
pretty_env_logger::init();
40+
let args = Args::parse();
41+
42+
let metadata = cargo_metadata::MetadataCommand::new()
43+
.manifest_path(&args.manifest_path)
44+
.exec()
45+
.expect("Failed to get cargo metadata");
46+
47+
let workspace = Workspace::from(&metadata);
48+
let mut graph = WorkspaceGraph::from(workspace);
49+
50+
let root = args.root_package;
51+
log::info!(
52+
"Calculating features for root: {:?} with features: {:?}, default features: {}",
53+
root,
54+
args.features,
55+
!args.disable_default_features
56+
);
57+
58+
// TODO: allow focusing on a non-workspace root, i.e. package
59+
graph
60+
.calculate_enabled_features_and_dependencies_parse(args.features, root.map(CrateName::new));
61+
62+
#[cfg(feature = "dot_parser")]
63+
let dot_graph = graph.visualise_feature_flow(args.only_show_crates)?;
64+
65+
#[cfg(not(feature = "dot_parser"))]
66+
let dot_graph = {
67+
log::warn!("Feature `dot_parser` not enabled, dumping debug print instead");
68+
format!("{:?}", graph.workspace)
69+
};
70+
71+
// dump the graph
72+
println!("{dot_graph}");
73+
74+
Ok(())
75+
}
65.9 KB
Loading

codegen/crates/crate_feature_graph/src/feature.rs

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ pub enum FeatureEffect {
6464
/// I.e. `foo=["dep:optional"]` is represented as `EnableOptionalDependency("optional")`
6565
EnableOptionalDependency(CrateName),
6666
/// I.e. `foo=["optional(?)/feature"]` is represented as `EnableFeatureInDependency("feature", "optional")`
67-
/// this effect by itself does not enable the dependency, only the feature in it
67+
/// this effect by itself does not enable the dependency, only the feature in it unless `enables_optionals` is true
6868
EnableFeatureInDependency(FeatureName, CrateName, bool),
6969
}
7070

@@ -263,6 +263,21 @@ impl Crate {
263263
}
264264
}
265265

266+
/// Checks if a dependency of this crate is enabled via one of its active features
267+
/// The active features must be computed first
268+
pub fn optional_dependency_is_enabled(&self, dep: &CrateName) -> bool {
269+
self.optional_dependencies.iter().any(|d| {
270+
&d.name == dep
271+
&& self.active_features.as_ref().is_some_and(|f| {
272+
f.iter().any(|f| {
273+
self.features.iter().any(|feat| {
274+
feat.name == *f && feat.effects().any(|e| e.enables_crate(dep))
275+
})
276+
})
277+
})
278+
})
279+
}
280+
266281
/// processes the feature set, given the initially enabled features, and returns the full set of enabled features.
267282
/// if `enable_default` is true, the "default" feature is considered enabled initially if it exists.
268283
pub fn compute_active_features(
@@ -324,14 +339,28 @@ impl Crate {
324339
// for default do still include the feature even if it doesn't exist
325340
all_features.insert(next);
326341
} else {
327-
error!(
328-
"Crate `{}` does not have feature `{}`. Are you using the right features?",
329-
self.name, next
330-
);
342+
// error!(
343+
// "Crate `{}` does not have feature `{}`. Are you using the right features?",
344+
// self.name, next
345+
// );
346+
// too noisy for now, figure out what's going on TODO
331347
}
332348
}
333349

334-
self.active_features = Some(all_features);
350+
if let Some(active_features) = &self.active_features {
351+
// merge, features are always additive in a workspace
352+
if self.in_workspace.is_some_and(|a| a) {
353+
log::trace!(
354+
"Merging active features for crate `{}`, as new path is being computed: {:?} + {:?}",
355+
self.name,
356+
active_features,
357+
all_features
358+
);
359+
}
360+
self.active_features = Some(active_features.union(&all_features).cloned().collect());
361+
} else {
362+
self.active_features = Some(all_features);
363+
}
335364
}
336365
}
337366
impl From<Package> for Crate {
@@ -526,31 +555,6 @@ impl From<&Metadata> for Workspace {
526555
}
527556
});
528557

529-
let existing_package_names = workspace_crates
530-
.iter()
531-
.map(|c| c.name.clone())
532-
.collect::<HashSet<_>>();
533-
534-
// also process dependency crates that are not part of the workspace
535-
for dep in meta.packages.iter() {
536-
for dependency in dep.dependencies.iter() {
537-
let dep_name = CrateName::new(&dependency.name);
538-
if !existing_package_names.contains(&dep_name) {
539-
other_crates.push(Crate {
540-
name: dep_name,
541-
features: vec![],
542-
dependencies: vec![],
543-
optional_dependencies: vec![],
544-
version: Version::new(0, 0, 0),
545-
in_workspace: Some(false),
546-
active_features: None,
547-
active_dependency_features: None,
548-
is_enabled: None,
549-
});
550-
}
551-
}
552-
}
553-
554558
for krate in workspace_crates.iter_mut() {
555559
krate.in_workspace = Some(true);
556560
}

0 commit comments

Comments
 (0)