diff --git a/src/rendering/display.rs b/src/edit/display.rs similarity index 99% rename from src/rendering/display.rs rename to src/edit/display.rs index 22bdd95..f38670c 100644 --- a/src/rendering/display.rs +++ b/src/edit/display.rs @@ -1,7 +1,7 @@ +use crate::io::save::save_dependency; use crate::project::dependency::feature::EnabledState; use crate::project::document::Document; -use crate::rendering::filter_view::FilterView; -use crate::save::save_dependency; +use crate::edit::filter_view::FilterView; use color_eyre::eyre::{Context, ContextCompat}; use color_eyre::Result; use console::{style, Emoji, Key, Term}; diff --git a/src/rendering/filter_view/item.rs b/src/edit/filter_view/item.rs similarity index 96% rename from src/rendering/filter_view/item.rs rename to src/edit/filter_view/item.rs index 8fbfb20..b66d796 100644 --- a/src/rendering/filter_view/item.rs +++ b/src/edit/filter_view/item.rs @@ -1,6 +1,6 @@ use crate::project::dependency::Dependency; use crate::project::package::Package; -use crate::rendering::search::highlight_search; +use crate::edit::search::highlight_search; use console::style; pub struct FilterViewItem { diff --git a/src/rendering/filter_view/mod.rs b/src/edit/filter_view/mod.rs similarity index 98% rename from src/rendering/filter_view/mod.rs rename to src/edit/filter_view/mod.rs index 07f7d80..f127b3b 100644 --- a/src/rendering/filter_view/mod.rs +++ b/src/edit/filter_view/mod.rs @@ -1,7 +1,7 @@ use crate::project::dependency::Dependency; use crate::project::document::Document; use crate::project::package::Package; -use crate::rendering::filter_view::item::FilterViewItem; +use crate::edit::filter_view::item::FilterViewItem; use color_eyre::eyre::ContextCompat; use fuzzy_matcher::skim::SkimMatcherV2; use itertools::Itertools; diff --git a/src/rendering/mod.rs b/src/edit/mod.rs similarity index 100% rename from src/rendering/mod.rs rename to src/edit/mod.rs diff --git a/src/rendering/search.rs b/src/edit/search.rs similarity index 100% rename from src/rendering/search.rs rename to src/edit/search.rs diff --git a/src/io/mod.rs b/src/io/mod.rs new file mode 100644 index 0000000..4343cef --- /dev/null +++ b/src/io/mod.rs @@ -0,0 +1,3 @@ +pub mod parsing; +pub mod save; +pub mod util; \ No newline at end of file diff --git a/src/parsing/dependency.rs b/src/io/parsing/dependency.rs similarity index 98% rename from src/parsing/dependency.rs rename to src/io/parsing/dependency.rs index f099941..a76f087 100644 --- a/src/parsing/dependency.rs +++ b/src/io/parsing/dependency.rs @@ -1,8 +1,8 @@ -use crate::parsing::package::get_package_from_version; +use crate::io::parsing::package::get_package_from_version; use crate::project::dependency::feature::{EnabledState, FeatureData, SubFeature, SubFeatureType}; use crate::project::dependency::util::get_path; use crate::project::dependency::{Dependency, DependencyType}; -use crate::util::get_item_from_doc; +use crate::io::util::get_item_from_doc; use cargo_metadata::PackageId; use color_eyre::eyre::{eyre, ContextCompat}; use itertools::Itertools; diff --git a/src/parsing/mod.rs b/src/io/parsing/mod.rs similarity index 100% rename from src/parsing/mod.rs rename to src/io/parsing/mod.rs diff --git a/src/parsing/package.rs b/src/io/parsing/package.rs similarity index 93% rename from src/parsing/package.rs rename to src/io/parsing/package.rs index b468ed6..0aaba03 100644 --- a/src/parsing/package.rs +++ b/src/io/parsing/package.rs @@ -1,12 +1,12 @@ use cargo_metadata::{CargoOpt, PackageId}; -use crate::parsing::workspace::parse_workspace; +use crate::io::parsing::workspace::parse_workspace; use color_eyre::Result; -use crate::parsing::dependency::parse_dependency; +use crate::io::parsing::dependency::parse_dependency; use crate::project::dependency::Dependency; use crate::project::package::Package; -use crate::util::toml_document_from_path; +use crate::io::util::toml_document_from_path; use color_eyre::eyre::ContextCompat; use semver::VersionReq; use std::collections::HashMap; diff --git a/src/parsing/workspace.rs b/src/io/parsing/workspace.rs similarity index 91% rename from src/parsing/workspace.rs rename to src/io/parsing/workspace.rs index 8ec1b51..0660383 100644 --- a/src/parsing/workspace.rs +++ b/src/io/parsing/workspace.rs @@ -1,7 +1,7 @@ -use crate::parsing::dependency::parse_dependency_from_item; +use crate::io::parsing::dependency::parse_dependency_from_item; use crate::project::dependency::Dependency; use crate::project::package::Package; -use crate::util::toml_document_from_path; +use crate::io::util::toml_document_from_path; use cargo_metadata::PackageId; use color_eyre::eyre::eyre; use color_eyre::Result; diff --git a/src/save.rs b/src/io/save.rs similarity index 98% rename from src/save.rs rename to src/io/save.rs index 5fd2d4b..3849995 100644 --- a/src/save.rs +++ b/src/io/save.rs @@ -1,6 +1,6 @@ use crate::project::dependency::util::get_path; use crate::project::document::Document; -use crate::util::{get_mut_item_from_doc, toml_document_from_path}; +use crate::io::util::{get_mut_item_from_doc, toml_document_from_path}; use color_eyre::eyre::{ContextCompat, Error}; use std::fs; use toml_edit::{Array, Formatted, InlineTable, Item, Value}; diff --git a/src/util.rs b/src/io/util.rs similarity index 100% rename from src/util.rs rename to src/io/util.rs diff --git a/src/main.rs b/src/main.rs index 5ef484a..5910de0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ #![warn(clippy::unwrap_used)] -use std::io; use std::process::exit; use clap::{arg, CommandFactory, Parser, Subcommand}; @@ -8,18 +7,16 @@ use clap_complete::{generate, Shell}; use color_eyre::Result; use console::Term; +use crate::edit::display::Display; use crate::project::document::Document; use crate::prune::prune; -use crate::rendering::display::Display; +mod edit; mod prune; -mod rendering; -mod parsing; mod project; -mod util; -mod save; +mod io; #[derive(Parser)] #[command(name = "cargo")] @@ -74,7 +71,7 @@ fn main() -> Result<()> { generator, cmd, cmd.get_name().to_string(), - &mut io::stdout(), + &mut std::io::stdout(), ); return Ok(()); } diff --git a/src/project/document.rs b/src/project/document.rs index ac4e9c2..c1fc810 100644 --- a/src/project/document.rs +++ b/src/project/document.rs @@ -3,7 +3,7 @@ use color_eyre::eyre::{bail, eyre, ContextCompat}; use color_eyre::Result; use itertools::Itertools; -use crate::parsing::package::get_packages; +use crate::io::parsing::package::get_packages; use crate::project::dependency::feature::EnabledState; use crate::project::package::Package; diff --git a/src/prune.rs b/src/prune.rs deleted file mode 100644 index 9b34ca3..0000000 --- a/src/prune.rs +++ /dev/null @@ -1,516 +0,0 @@ -use color_eyre::Result; -use std::collections::HashMap; - -use console::{style, Term}; -use std::io::Write; -use std::ops::Not; -use std::path::Path; - -use crate::project::dependency::Dependency; -use crate::project::document::Document; -use crate::save::save_dependency; -use crate::util::{get_item_from_doc, toml_document_from_path}; -use crate::CleanLevel; -use color_eyre::eyre::{eyre, ContextCompat}; -use itertools::Itertools; -use std::process::{Command, Stdio}; - -pub fn prune( - mut document: Document, - is_dry_run: bool, - skip_tests: bool, - clean: CleanLevel, -) -> Result<()> { - let mut term = Term::stdout(); - - let mut enabled_features = get_enabled_features(&document); - - let base_ignored_features = - get_ignored_features("./", "workspace.cargo-features-manager.keep")?; - remove_ignored_features(&document, &base_ignored_features, &mut enabled_features)?; - - prune_features( - &mut document, - is_dry_run, - skip_tests, - clean, - &mut term, - enabled_features, - known_features()?, - )?; - - Ok(()) -} - -//give a map of known features that do not affect completion but remove functionality -pub fn known_features() -> Result>> { - let file = include_str!("../Known-Features.toml"); - - let document: toml_edit::DocumentMut = file.parse()?; - - let mut map = HashMap::new(); - - for (dependency, features) in document.as_table() { - let features = features - .as_array() - .context("could not parse Known-Features.toml")?; - - let features = features - .iter() - .filter_map(|item| item.as_str()) - .map(|name| name.to_string()) - .collect_vec(); - - map.insert(dependency.to_string(), features); - } - - Ok(map) -} - -type FeaturesToTest = HashMap>>; - -fn get_enabled_features(document: &Document) -> FeaturesToTest { - let mut data = HashMap::new(); - - for package in document.get_packages() { - let mut package_data = HashMap::new(); - - for dependency in package.get_deps() { - let enabled_features = dependency - .features - .iter() - .filter(|(_name, data)| data.is_toggleable() && data.is_enabled()) - .map(|(name, _data)| name) - .cloned() - .collect::>(); - - if enabled_features.is_empty().not() { - package_data.insert(dependency.get_name().clone(), enabled_features); - } - } - - if package_data.is_empty().not() { - data.insert(package.name.clone(), package_data); - } - } - - data -} - -fn remove_ignored_features( - document: &Document, - base_ignored: &HashMap>, - enabled_features: &mut FeaturesToTest, -) -> Result<()> { - for (package_name, dependencies) in enabled_features { - let package = document.get_package(package_name)?; - - let ignored_features = get_ignored_features( - package.manifest_path.trim_end_matches("/Cargo.toml"), - "cargo-features-manager.keep", - )?; - - for (dependency_name, features) in dependencies { - let dependency = package.get_dep(dependency_name)?; - - if dependency.can_use_default() { - features.push("default".to_string()); - } - - for feature in ignored_features.get(&dependency.name).unwrap_or(&vec![]) { - remove_feature(feature, features, dependency); - } - for feature in base_ignored.get(&dependency.name).unwrap_or(&vec![]) { - remove_feature(feature, features, dependency); - } - - if let Some(index) = features.iter().position(|name| name == "default") { - features.remove(index); - } - } - } - - Ok(()) -} - -fn remove_feature(feature: &String, features: &mut Vec, dependency: &Dependency) { - let index = features.iter().position(|name| name == feature); - - let Some(index) = index else { - return; - }; - - features.remove(index); - - if let Some(feature) = dependency.get_feature(feature) { - for sub_feature in &feature.sub_features { - remove_feature(&sub_feature.name, features, dependency); - } - } -} - -fn prune_features( - document: &mut Document, - is_dry_run: bool, - skip_tests: bool, - should_clean: CleanLevel, - term: &mut Term, - features: FeaturesToTest, - known_features: HashMap>, -) -> Result<()> { - let feature_count = features - .values() - .flat_map(|dependencies| dependencies.values()) - .flatten() - .count(); - - let mut has_known_features_enabled = false; - let mut checked_features_count = 0; - - writeln!(term, "workspace [{}]", feature_count)?; - - let package_inset = if features.len() == 1 { 0 } else { 2 }; - let dependency_inset = if features.len() == 1 { 2 } else { 4 }; - - for (package_name, dependencies) in features - .into_iter() - .sorted_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)) - { - if dependencies.is_empty() { - continue; - } - - let package_feature_count = dependencies.values().flatten().count(); - let mut package_checked_features_count = 0; - - if document.is_workspace() { - term.clear_line()?; - writeln!(term)?; - writeln!( - term, - "{:package_inset$}{} [{}]", - "", package_name, package_feature_count - )?; - } - - for (dependency_name, features) in dependencies - .into_iter() - .sorted_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)) - { - if features.is_empty() { - continue; - } - - let mut known_features_list = vec![]; - let dependency = document - .get_package(&package_name)? - .get_dep(&dependency_name)?; - - for feature_name in known_features.get(&dependency_name).unwrap_or(&vec![]) { - set_features_to_be_keept( - dependency, - feature_name.to_string(), - &mut known_features_list, - ) - } - - let mut to_be_disabled = vec![]; - to_be_disabled.append(&mut known_features_list.clone()); - - for (id, feature) in features.iter().enumerate() { - term.clear_line()?; - writeln!( - term, - "{:dependency_inset$}{} [{}/{}]", - "", - dependency_name, - id, - features.len() - )?; - term.clear_line()?; - writeln!(term, "{:dependency_inset$} └ {}", "", feature)?; - term.move_cursor_up(2)?; - - write_progress_bar( - term, - feature_count, - checked_features_count, - document, - &package_name, - package_feature_count, - package_checked_features_count, - )?; - - document - .get_package_mut(&package_name)? - .get_dep_mut(&dependency_name)? - .disable_feature(feature)?; - - save_dependency(document, &package_name, &dependency_name)?; - - if !to_be_disabled.contains(feature) && check(skip_tests)? { - set_features_to_be_disabled( - document - .get_package(&package_name)? - .get_dep(&dependency_name)?, - feature.to_string(), - &mut to_be_disabled, - ); - } - - //reset to start - for feature in &features { - document - .get_package_mut(&package_name)? - .get_dep_mut(&dependency_name)? - .enable_feature(feature)?; - } - - save_dependency(document, &package_name, &dependency_name)?; - - checked_features_count += 1; - package_checked_features_count += 1; - - write_progress_bar( - term, - feature_count, - checked_features_count, - document, - &package_name, - package_feature_count, - package_checked_features_count, - )?; - } - - let mut disabled_count = style( - features - .iter() - .filter(|feature| to_be_disabled.contains(feature)) - .map(|feature| { - if known_features_list.contains(feature) { - has_known_features_enabled = true; - style(feature).color256(7).to_string() - } else { - style(format!("-{}", feature)).red().to_string() - } - }) - .join(","), - ); - - if to_be_disabled.is_empty() { - disabled_count = style("0".to_string()); - } - - term.clear_line()?; - writeln!( - term, - "{:dependency_inset$}{} [{}/{}]", - "", - dependency_name, - disabled_count, - features.len() - )?; - - if let CleanLevel::Dependency = should_clean { - clean()?; - } - - if is_dry_run { - continue; - } - - if to_be_disabled.is_empty().not() { - for feature in to_be_disabled { - if known_features_list.contains(&feature) { - continue; - } - - document - .get_package_mut(&package_name)? - .get_dep_mut(&dependency_name)? - .disable_feature(&feature)?; - } - - save_dependency(document, &package_name, &dependency_name)?; - } - } - - if let CleanLevel::Package = should_clean { - clean()?; - } - } - - if has_known_features_enabled { - term.clear_line()?; - writeln!(term)?; - writeln!(term, "Some features that do not affect compilation but can limit functionally where found. For more information refer to https://github.com/ToBinio/cargo-features-manager?tab=readme-ov-file#prune")?; - } - - Ok(()) -} - -fn write_progress_bar( - term: &mut Term, - workspace_feature_count: usize, - completed_workspace_feature_count: usize, - document: &Document, - package_name: &str, - package_features: usize, - completed_package_features: usize, -) -> Result<()> { - term.move_cursor_down(2)?; - term.clear_line()?; - writeln!(term)?; - term.clear_line()?; - write!( - term, - "Workspace [{}/{}]", - completed_workspace_feature_count, workspace_feature_count, - )?; - - if document.is_workspace() { - write!( - term, - " -> {} [{}/{}]", - package_name, completed_package_features, package_features - )?; - } - - writeln!(term)?; - - term.move_cursor_up(4)?; - Ok(()) -} - -fn set_features_to_be_disabled( - dependency: &Dependency, - feature: String, - to_be_disabled: &mut Vec, -) { - if to_be_disabled.contains(&feature) { - return; - } - - to_be_disabled.push(feature.clone()); - - dependency - .features - .iter() - .filter(|(_, data)| { - data.sub_features - .iter() - .any(|sub_feature| sub_feature.name == feature) - }) - .for_each(|(name, _)| { - set_features_to_be_disabled(dependency, name.to_string(), to_be_disabled); - }); -} - -fn set_features_to_be_keept( - dependency: &Dependency, - feature: String, - to_be_disabled: &mut Vec, -) { - if to_be_disabled.contains(&feature) { - return; - } - - to_be_disabled.push(feature.clone()); - - if let Some(feature) = dependency.get_feature(&feature) { - for sub_feature in &feature.sub_features { - set_features_to_be_keept(dependency, sub_feature.name.clone(), to_be_disabled); - } - } -} - -fn clean() -> Result<()> { - let mut child = Command::new("cargo") - .arg("clean") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn()?; - - let _ = child.wait()?.code().ok_or(eyre!("Could not clear"))?; - - Ok(()) -} - -fn check(skip_tests: bool) -> Result { - if !build()? { - return Ok(false); - } - - if !skip_tests && !test()? { - return Ok(false); - } - - Ok(true) -} - -fn build() -> Result { - let mut child = Command::new("cargo") - .arg("build") - .arg("--all-targets") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn()?; - - let code = child.wait()?.code().ok_or(eyre!("Could not build"))?; - - Ok(code == 0) -} - -fn test() -> Result { - let mut child = Command::new("cargo") - .arg("test") - .arg("--workspace") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn()?; - - let code = child.wait()?.code().ok_or(eyre!("Could not test"))?; - - Ok(code == 0) -} - -fn get_ignored_features>( - file_path: P, - item_path: &str, -) -> Result>> { - let result = toml_document_from_path(file_path.as_ref().join("Cargo.toml")); - - match result { - Ok(document) => { - let item = get_item_from_doc(item_path, &document); - - let Ok(item) = item else { - return Ok(HashMap::new()); - }; - - let table = item.as_table_like().context(format!( - "could not parse {} in {:?}", - item_path, - file_path.as_ref() - ))?; - - let mut map = HashMap::new(); - - for (key, value) in table.iter() { - map.insert( - key.to_string(), - value - .as_array() - .ok_or(eyre!("Invalid format to keep features"))? - .iter() - .filter_map(|value| value.as_str()) - .map(|value| value.to_string()) - .collect(), - ); - } - - Ok(map) - } - Err(_) => Ok(HashMap::new()), - } -} diff --git a/src/prune/display.rs b/src/prune/display.rs new file mode 100644 index 0000000..ddec359 --- /dev/null +++ b/src/prune/display.rs @@ -0,0 +1,180 @@ +use crate::project::document::Document; +use crate::prune::parse::{DependencyName, FeatureName, FeaturesToTest}; +use color_eyre::Result; +use console::{style, Term}; +use itertools::Itertools; +use std::collections::HashMap; +use std::io::Write; + +type IsKnownFeature = bool; + +pub struct Display { + term: Term, + + package_inset: usize, + dependency_inset: usize, + + feature_count: usize, + checked_features_count: usize, + is_workspace: bool, + + package_name: String, + package_feature_count: usize, + package_checked_features_count: usize, + + dependency_name: String, + dependency_feature_count: usize, +} + +impl Display { + pub fn new(features_to_test: &FeaturesToTest, document: &Document) -> Self { + let feature_count = features_to_test + .values() + .flat_map(|dependencies| dependencies.values()) + .flatten() + .count(); + + let package_inset = if features_to_test.len() == 1 { 0 } else { 2 }; + let dependency_inset = if features_to_test.len() == 1 { 2 } else { 4 }; + + Self { + feature_count, + package_inset, + dependency_inset, + is_workspace: document.is_workspace(), + package_name: "?".to_string(), + package_feature_count: 0, + package_checked_features_count: 0, + dependency_name: "?".to_string(), + term: Term::stdout(), + checked_features_count: 0, + dependency_feature_count: 0, + } + } + + pub fn start(&self) -> Result<()> { + writeln!(&self.term, "workspace [{}]", self.feature_count)?; + Ok(()) + } + + pub fn display_known_features_notice(&mut self) -> Result<()> { + self.term.clear_line()?; + writeln!(self.term)?; + writeln!(self.term, "Some features that do not affect compilation but can limit functionally where found. For more information refer to https://github.com/ToBinio/cargo-features-manager?tab=readme-ov-file#prune")?; + Ok(()) + } + + pub fn next_package( + &mut self, + package_name: &str, + package_features: &HashMap>, + ) -> Result<()> { + self.package_name = package_name.to_string(); + self.package_feature_count = package_features.values().flatten().count(); + self.package_checked_features_count = 0; + + if self.is_workspace { + let package_inset = self.package_inset; + + self.term.clear_line()?; + writeln!(self.term)?; + writeln!( + self.term, + "{:package_inset$}{} [{}]", + "", package_name, self.package_feature_count + )?; + } + + Ok(()) + } + + pub fn next_dependency(&mut self, name: &str, features: &Vec) { + self.dependency_feature_count = features.len(); + self.dependency_name = name.to_string(); + } + + pub fn finish_dependency( + &mut self, + features: Vec<(&FeatureName, IsKnownFeature)>, + ) -> Result<()> { + let mut disabled_count = style( + features + .iter() + .map(|(name, known)| { + if *known { + style(name).color256(7).to_string() + } else { + style(format!("-{}", name)).red().to_string() + } + }) + .join(","), + ); + + if features.is_empty() { + disabled_count = style("0".to_string()); + } + + let dependency_inset = self.dependency_inset; + + self.term.clear_line()?; + writeln!( + self.term, + "{:dependency_inset$}{} [{}/{}]", + "", self.dependency_name, disabled_count, self.dependency_feature_count + )?; + + Ok(()) + } + + pub fn next_feature(&mut self, id: usize, feature_name: &FeatureName) -> Result<()> { + let dependency_inset = self.dependency_inset; + + self.term.clear_line()?; + writeln!( + self.term, + "{:dependency_inset$}{} [{}/{}]", + "", self.dependency_name, id, self.dependency_feature_count, + )?; + self.term.clear_line()?; + writeln!(self.term, "{:dependency_inset$} └ {}", "", feature_name)?; + self.term.move_cursor_up(2)?; + + self.display_progress_bar()?; + + Ok(()) + } + + pub fn finish_feature(&mut self) -> Result<()> { + self.checked_features_count += 1; + self.package_checked_features_count += 1; + + self.display_progress_bar()?; + + Ok(()) + } + + fn display_progress_bar(&mut self) -> Result<()> { + self.term.move_cursor_down(2)?; + self.term.clear_line()?; + writeln!(self.term)?; + self.term.clear_line()?; + write!( + self.term, + "Workspace [{}/{}]", + self.checked_features_count, self.feature_count, + )?; + + if self.is_workspace { + write!( + self.term, + " -> {} [{}/{}]", + self.package_name, self.package_checked_features_count, self.package_feature_count + )?; + } + + writeln!(self.term)?; + + self.term.move_cursor_up(4)?; + Ok(()) + } +} diff --git a/src/prune/mod.rs b/src/prune/mod.rs new file mode 100644 index 0000000..6d16e2f --- /dev/null +++ b/src/prune/mod.rs @@ -0,0 +1,288 @@ +use color_eyre::Result; +use std::collections::HashMap; + +use std::ops::Not; + +use crate::io::save::save_dependency; +use crate::project::dependency::Dependency; +use crate::project::document::Document; +use crate::prune::display::Display; +use crate::prune::parse::{get_features_to_test, FeaturesToTest}; +use crate::CleanLevel; +use color_eyre::eyre::{eyre, ContextCompat}; +use itertools::Itertools; +use std::process::{Command, Stdio}; + +mod parse; + +mod display; + +pub fn prune( + mut document: Document, + is_dry_run: bool, + skip_tests: bool, + clean: CleanLevel, +) -> Result<()> { + let features_to_test = get_features_to_test(&document)?; + + prune_features( + &mut document, + is_dry_run, + skip_tests, + clean, + features_to_test, + known_features()?, + )?; + + Ok(()) +} + +//give a map of known features that do not affect completion but remove functionality +pub fn known_features() -> Result>> { + let file = include_str!("../../Known-Features.toml"); + + let document: toml_edit::DocumentMut = file.parse()?; + + let mut map = HashMap::new(); + + for (dependency, features) in document.as_table() { + let features = features + .as_array() + .context("could not parse Known-Features.toml")?; + + let features = features + .iter() + .filter_map(|item| item.as_str()) + .map(|name| name.to_string()) + .collect_vec(); + + map.insert(dependency.to_string(), features); + } + + Ok(map) +} + +fn prune_features( + document: &mut Document, + is_dry_run: bool, + skip_tests: bool, + should_clean: CleanLevel, + features: FeaturesToTest, + known_features: HashMap>, +) -> Result<()> { + let mut has_known_features_enabled = false; + + let mut display = Display::new(&features, &document); + display.start()?; + + for (package_name, dependencies) in features + .into_iter() + .sorted_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)) + { + if dependencies.is_empty() { + continue; + } + + display.next_package(&package_name, &dependencies)?; + + for (dependency_name, features) in dependencies + .into_iter() + .sorted_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)) + { + if features.is_empty() { + continue; + } + + let mut known_features_list = vec![]; + let dependency = document + .get_package(&package_name)? + .get_dep(&dependency_name)?; + + for feature_name in known_features.get(&dependency_name).unwrap_or(&vec![]) { + set_features_to_be_kept( + dependency, + feature_name.to_string(), + &mut known_features_list, + ) + } + + let mut to_be_disabled = vec![]; + to_be_disabled.append(&mut known_features_list.clone()); + + display.next_dependency(&dependency_name, &features); + + for (id, feature) in features.iter().enumerate() { + display.next_feature(id, feature)?; + + document + .get_package_mut(&package_name)? + .get_dep_mut(&dependency_name)? + .disable_feature(feature)?; + + save_dependency(document, &package_name, &dependency_name)?; + + if !to_be_disabled.contains(feature) && check(skip_tests)? { + set_features_to_be_disabled( + document + .get_package(&package_name)? + .get_dep(&dependency_name)?, + feature.to_string(), + &mut to_be_disabled, + ); + } + + //reset to start + for feature in &features { + document + .get_package_mut(&package_name)? + .get_dep_mut(&dependency_name)? + .enable_feature(feature)?; + } + + save_dependency(document, &package_name, &dependency_name)?; + + display.finish_feature()?; + } + + let features_result = features + .iter() + .filter(|feature| to_be_disabled.contains(feature)) + .map(|feature| { + if known_features_list.contains(feature) { + has_known_features_enabled = true; + (feature, true) + } else { + (feature, false) + } + }) + .collect(); + + display.finish_dependency(features_result)?; + + if let CleanLevel::Dependency = should_clean { + clean()?; + } + + if is_dry_run { + continue; + } + + if to_be_disabled.is_empty().not() { + for feature in to_be_disabled { + if known_features_list.contains(&feature) { + continue; + } + + document + .get_package_mut(&package_name)? + .get_dep_mut(&dependency_name)? + .disable_feature(&feature)?; + } + + save_dependency(document, &package_name, &dependency_name)?; + } + } + + if let CleanLevel::Package = should_clean { + clean()?; + } + } + + if has_known_features_enabled { + display.display_known_features_notice()?; + } + + Ok(()) +} + +fn set_features_to_be_disabled( + dependency: &Dependency, + feature: String, + to_be_disabled: &mut Vec, +) { + if to_be_disabled.contains(&feature) { + return; + } + + to_be_disabled.push(feature.clone()); + + dependency + .features + .iter() + .filter(|(_, data)| { + data.sub_features + .iter() + .any(|sub_feature| sub_feature.name == feature) + }) + .for_each(|(name, _)| { + set_features_to_be_disabled(dependency, name.to_string(), to_be_disabled); + }); +} + +fn set_features_to_be_kept( + dependency: &Dependency, + feature: String, + to_be_disabled: &mut Vec, +) { + if to_be_disabled.contains(&feature) { + return; + } + + to_be_disabled.push(feature.clone()); + + if let Some(feature) = dependency.get_feature(&feature) { + for sub_feature in &feature.sub_features { + set_features_to_be_kept(dependency, sub_feature.name.clone(), to_be_disabled); + } + } +} + +fn clean() -> Result<()> { + let mut child = Command::new("cargo") + .arg("clean") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + + let _ = child.wait()?.code().ok_or(eyre!("Could not clear"))?; + + Ok(()) +} + +fn check(skip_tests: bool) -> Result { + if !build()? { + return Ok(false); + } + + if !skip_tests && !test()? { + return Ok(false); + } + + Ok(true) +} + +fn build() -> Result { + let mut child = Command::new("cargo") + .arg("build") + .arg("--all-targets") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + + let code = child.wait()?.code().ok_or(eyre!("Could not build"))?; + + Ok(code == 0) +} + +fn test() -> Result { + let mut child = Command::new("cargo") + .arg("test") + .arg("--workspace") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + + let code = child.wait()?.code().ok_or(eyre!("Could not test"))?; + + Ok(code == 0) +} diff --git a/src/prune/parse.rs b/src/prune/parse.rs new file mode 100644 index 0000000..e9e37c1 --- /dev/null +++ b/src/prune/parse.rs @@ -0,0 +1,145 @@ +use color_eyre::Result; + +use crate::io::util::{get_item_from_doc, toml_document_from_path}; +use crate::project::dependency::Dependency; +use crate::project::document::Document; +use color_eyre::eyre::{eyre, ContextCompat}; +use std::collections::HashMap; +use std::ops::Not; +use std::path::Path; + +type PackageName = String; +pub type DependencyName = String; +pub type FeatureName = String; +pub type FeaturesToTest = HashMap>>; + +pub fn get_features_to_test(document: &Document) -> Result { + let base_ignored_features = + get_ignored_features("./", "workspace.cargo-features-manager.keep")?; + + let mut enabled_features = get_enabled_features(document); + remove_ignored_features(document, &base_ignored_features, &mut enabled_features)?; + + Ok(enabled_features) +} + +fn get_enabled_features(document: &Document) -> FeaturesToTest { + let mut data = HashMap::new(); + + for package in document.get_packages() { + let mut package_data = HashMap::new(); + + for dependency in package.get_deps() { + let enabled_features = dependency + .features + .iter() + .filter(|(_name, data)| data.is_toggleable() && data.is_enabled()) + .map(|(name, _data)| name) + .cloned() + .collect::>(); + + if enabled_features.is_empty().not() { + package_data.insert(dependency.get_name().clone(), enabled_features); + } + } + + if package_data.is_empty().not() { + data.insert(package.name.clone(), package_data); + } + } + + data +} + +fn get_ignored_features>( + file_path: P, + item_path: &str, +) -> color_eyre::Result>> { + let result = toml_document_from_path(file_path.as_ref().join("Cargo.toml")); + + match result { + Ok(document) => { + let item = get_item_from_doc(item_path, &document); + + let Ok(item) = item else { + return Ok(HashMap::new()); + }; + + let table = item.as_table_like().context(format!( + "could not parse {} in {:?}", + item_path, + file_path.as_ref() + ))?; + + let mut map = HashMap::new(); + + for (key, value) in table.iter() { + map.insert( + key.to_string(), + value + .as_array() + .ok_or(eyre!("Invalid format to keep features"))? + .iter() + .filter_map(|value| value.as_str()) + .map(|value| value.to_string()) + .collect(), + ); + } + + Ok(map) + } + Err(_) => Ok(HashMap::new()), + } +} + +fn remove_ignored_features( + document: &Document, + base_ignored: &HashMap>, + enabled_features: &mut FeaturesToTest, +) -> Result<()> { + for (package_name, dependencies) in enabled_features { + let package = document.get_package(package_name)?; + + let ignored_features = get_ignored_features( + package.manifest_path.trim_end_matches("/Cargo.toml"), + "cargo-features-manager.keep", + )?; + + for (dependency_name, features) in dependencies { + let dependency = package.get_dep(dependency_name)?; + + if dependency.can_use_default() { + features.push("default".to_string()); + } + + for feature in ignored_features.get(&dependency.name).unwrap_or(&vec![]) { + remove_feature(feature, features, dependency); + } + for feature in base_ignored.get(&dependency.name).unwrap_or(&vec![]) { + remove_feature(feature, features, dependency); + } + + if let Some(index) = features.iter().position(|name| name == "default") { + features.remove(index); + } + } + } + + Ok(()) +} + +fn remove_feature(feature: &String, features: &mut Vec, dependency: &Dependency) { + let index = features.iter().position(|name| name == feature); + + let Some(index) = index else { + return; + }; + + features.remove(index); + + if let Some(feature) = dependency.get_feature(feature) { + for sub_feature in &feature.sub_features { + remove_feature(&sub_feature.name, features, dependency); + } + } +}