├── .gitignore ├── src ├── io │ ├── mod.rs │ ├── parsing │ │ ├── mod.rs │ │ ├── workspace.rs │ │ ├── package.rs │ │ └── dependency.rs │ ├── save.rs │ └── util.rs ├── edit │ ├── mod.rs │ ├── search.rs │ ├── filter_view │ │ ├── item.rs │ │ └── mod.rs │ └── display.rs ├── project │ ├── mod.rs │ ├── package.rs │ ├── dependency │ │ ├── feature.rs │ │ └── mod.rs │ └── document.rs ├── main.rs └── prune │ ├── parse.rs │ ├── display.rs │ └── mod.rs ├── resources ├── greenMark.png ├── greyFeature.png ├── greyDependency.png ├── featureDependency.png ├── featureSelector.png ├── workspaceFeatures.png ├── dependencySelector.png └── featurePackageDependency.png ├── .idea ├── vcs.xml ├── .gitignore ├── modules.xml └── crates-feature-manager.iml ├── Known-Features.toml ├── LICENSE ├── Cargo.toml ├── CHANGELOG.md ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/io/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod parsing; 2 | pub mod save; 3 | pub mod util; 4 | -------------------------------------------------------------------------------- /src/edit/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod display; 2 | pub mod search; 3 | 4 | pub mod filter_view; 5 | -------------------------------------------------------------------------------- /src/project/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dependency; 2 | pub mod document; 3 | pub mod package; 4 | -------------------------------------------------------------------------------- /src/io/parsing/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dependency; 2 | pub mod package; 3 | pub mod workspace; 4 | -------------------------------------------------------------------------------- /resources/greenMark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToBinio/cargo-features-manager/HEAD/resources/greenMark.png -------------------------------------------------------------------------------- /resources/greyFeature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToBinio/cargo-features-manager/HEAD/resources/greyFeature.png -------------------------------------------------------------------------------- /resources/greyDependency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToBinio/cargo-features-manager/HEAD/resources/greyDependency.png -------------------------------------------------------------------------------- /resources/featureDependency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToBinio/cargo-features-manager/HEAD/resources/featureDependency.png -------------------------------------------------------------------------------- /resources/featureSelector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToBinio/cargo-features-manager/HEAD/resources/featureSelector.png -------------------------------------------------------------------------------- /resources/workspaceFeatures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToBinio/cargo-features-manager/HEAD/resources/workspaceFeatures.png -------------------------------------------------------------------------------- /resources/dependencySelector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToBinio/cargo-features-manager/HEAD/resources/dependencySelector.png -------------------------------------------------------------------------------- /resources/featurePackageDependency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToBinio/cargo-features-manager/HEAD/resources/featurePackageDependency.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Known-Features.toml: -------------------------------------------------------------------------------- 1 | clap = ["default"] 2 | color-eyre = ["default"] 3 | regex = ["default"] 4 | serde_json = ["preserve_order"] 5 | rocket = ["http2"] 6 | hyper = ["http2"] 7 | hyper-util = ["http2"] 8 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/crates-feature-manager.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/edit/search.rs: -------------------------------------------------------------------------------- 1 | use console::style; 2 | 3 | pub fn highlight_search(text: &str, highlighted_letters: &[usize], is_dark: bool) -> String { 4 | text.chars() 5 | .enumerate() 6 | .map(|(index, c)| { 7 | match (is_dark, highlighted_letters.contains(&index)) { 8 | (false, true) => style(c).red().to_string(), 9 | (false, false) => c.to_string(), 10 | //dark red 11 | (true, true) => style(c).color256(1).to_string(), 12 | //light gray 13 | (true, false) => style(c).color256(8).to_string(), 14 | } 15 | }) 16 | .collect() 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ToBinio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-features-manager" 3 | version = "0.10.2" 4 | edition = "2024" 5 | authors = ["ToBinio"] 6 | license = "MIT" 7 | description = "A tui tool to enable/disable & prune dependency features" 8 | categories = ["command-line-utilities"] 9 | repository = "https://github.com/ToBinio/cargo-features-manager" 10 | keywords = ["cli", "manager", "cargo", "crates", "feature"] 11 | homepage = "https://github.com/ToBinio/cargo-features-manager.git" 12 | readme = "README.md" 13 | rust-version = "1.85.0" 14 | include = ["src/**/*", "LICENSE", "README.md", "Known-Features.toml"] 15 | 16 | [dependencies] 17 | color-eyre = "0.6.3" 18 | cargo_metadata = "0.23.0" 19 | clap = { version = "4.5.31", features = ["derive"] } 20 | clap_complete = "4.5.46" 21 | console = { version = "0.16.1", features = ["std"], default-features = false } 22 | ctrlc = "3.4.5" 23 | fuzzy-matcher = "0.3.7" 24 | itertools = { version = "0.14.0", default-features = false, features = ["use_alloc"] } 25 | semver = { version = "1.0.25", default-features = false } 26 | toml_edit = "0.23.7" 27 | tempfile = { version = "3.20.0", default-features = false } 28 | copy_dir = "0.1.3" 29 | 30 | [[bin]] 31 | name = "cargo-features" 32 | path = "src/main.rs" 33 | -------------------------------------------------------------------------------- /src/io/parsing/workspace.rs: -------------------------------------------------------------------------------- 1 | use crate::io::parsing::dependency::parse_dependency_from_item; 2 | use crate::io::util::toml_document_from_path; 3 | use crate::project::dependency::Dependency; 4 | use crate::project::package::Package; 5 | use cargo_metadata::PackageId; 6 | use color_eyre::Result; 7 | use color_eyre::eyre::eyre; 8 | use console::Emoji; 9 | use std::collections::HashMap; 10 | 11 | pub fn parse_workspace( 12 | path: &str, 13 | packages: &HashMap, 14 | ) -> Result> { 15 | let path = format!("{}/Cargo.toml", path); 16 | 17 | let document = toml_document_from_path(&path)?; 18 | let Some(workspace) = document.get("workspace") else { 19 | return Ok(None); 20 | }; 21 | 22 | let Some(dependencies) = workspace.get("dependencies") else { 23 | return Ok(None); 24 | }; 25 | 26 | let dependencies_table = dependencies.as_table_like().ok_or(eyre!( 27 | "failed to parse workspace.dependencies - not a table" 28 | ))?; 29 | 30 | let dependencies: Result> = dependencies_table 31 | .iter() 32 | .map(|(name, data)| parse_dependency_from_item(packages, name, data)) 33 | .collect(); 34 | 35 | let package = Package { 36 | dependencies: dependencies?, 37 | name: format!("{} Workspace", Emoji("🗃️", "")).to_string(), 38 | manifest_path: path, 39 | }; 40 | 41 | Ok(Some(package)) 42 | } 43 | -------------------------------------------------------------------------------- /src/project/package.rs: -------------------------------------------------------------------------------- 1 | use crate::project::dependency::Dependency; 2 | use color_eyre::eyre::{bail, eyre}; 3 | 4 | pub struct Package { 5 | pub dependencies: Vec, 6 | pub name: String, 7 | // path include the Cargo.toml 8 | pub manifest_path: String, 9 | } 10 | 11 | impl Package { 12 | pub fn get_deps(&self) -> &Vec { 13 | &self.dependencies 14 | } 15 | 16 | pub fn get_dep(&self, name: &str) -> color_eyre::Result<&Dependency> { 17 | let dep = self.dependencies.iter().find(|dep| dep.get_name().eq(name)); 18 | 19 | match dep { 20 | None => bail!("could not find dependency with name {}", name), 21 | Some(some) => Ok(some), 22 | } 23 | } 24 | 25 | pub fn get_dep_index(&self, name: &String) -> color_eyre::Result { 26 | Ok(self 27 | .dependencies 28 | .iter() 29 | .enumerate() 30 | .find(|(_, dep)| dep.get_name() == *name) 31 | .ok_or(eyre!("dependency \"{}\" could not be found", name))? 32 | .0) 33 | } 34 | 35 | pub fn get_dep_mut(&mut self, name: &str) -> color_eyre::Result<&mut Dependency> { 36 | let dep = self 37 | .dependencies 38 | .iter_mut() 39 | .find(|dep| dep.get_name().eq(name)); 40 | 41 | match dep { 42 | None => bail!("could not find dependency with name {}", name), 43 | Some(some) => Ok(some), 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/edit/filter_view/item.rs: -------------------------------------------------------------------------------- 1 | use crate::edit::search::highlight_search; 2 | use crate::project::dependency::Dependency; 3 | use crate::project::package::Package; 4 | use console::style; 5 | 6 | pub struct FilterViewItem { 7 | name: String, 8 | display_name: String, 9 | } 10 | 11 | impl FilterViewItem { 12 | pub fn from_package(dep: &Package, highlighted_letters: Vec) -> Self { 13 | Self { 14 | name: dep.name.to_string(), 15 | display_name: highlight_search( 16 | &dep.name, 17 | &highlighted_letters, 18 | dep.dependencies.is_empty(), 19 | ), 20 | } 21 | } 22 | 23 | pub fn from_dependency(dep: &Dependency, highlighted_letters: Vec) -> Self { 24 | let mut display_name = 25 | highlight_search(&dep.get_name(), &highlighted_letters, !dep.has_features()); 26 | 27 | if let Some(rename) = &dep.rename { 28 | display_name.push_str(&style(format!(" ({})", rename)).color256(8).to_string()); 29 | } 30 | 31 | if let Some(comment) = &dep.comment { 32 | display_name.push_str(&style(format!(" ({})", comment)).color256(8).to_string()); 33 | } 34 | 35 | Self { 36 | name: dep.get_name(), 37 | display_name, 38 | } 39 | } 40 | 41 | pub fn from_feature(name: &str, highlighted_letters: Vec) -> Self { 42 | Self { 43 | name: name.to_string(), 44 | display_name: highlight_search(name, &highlighted_letters, false), 45 | } 46 | } 47 | 48 | pub fn name(&self) -> &str { 49 | &self.name 50 | } 51 | 52 | pub fn display_name(&self) -> &str { 53 | &self.display_name 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/project/dependency/feature.rs: -------------------------------------------------------------------------------- 1 | use console::Emoji; 2 | use std::fmt::{Display, Formatter}; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct FeatureData { 6 | pub sub_features: Vec, 7 | pub is_default: bool, 8 | pub enabled_state: EnabledState, 9 | } 10 | 11 | #[derive(Clone, Debug, PartialEq)] 12 | pub enum EnabledState { 13 | Normal(bool), 14 | Workspace, 15 | } 16 | 17 | impl FeatureData { 18 | pub fn is_enabled(&self) -> bool { 19 | match self.enabled_state { 20 | EnabledState::Normal(is_enabled) => is_enabled, 21 | EnabledState::Workspace => true, 22 | } 23 | } 24 | 25 | pub fn is_toggleable(&self) -> bool { 26 | match self.enabled_state { 27 | EnabledState::Normal(_) => true, 28 | EnabledState::Workspace => false, 29 | } 30 | } 31 | } 32 | 33 | #[derive(Clone, Debug)] 34 | pub struct SubFeature { 35 | pub name: String, 36 | pub kind: SubFeatureType, 37 | } 38 | 39 | #[derive(Clone, PartialEq, Debug)] 40 | pub enum SubFeatureType { 41 | Normal, 42 | Dependency, 43 | DependencyFeature, 44 | } 45 | 46 | impl Display for SubFeature { 47 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 48 | if self.kind == SubFeatureType::Dependency { 49 | f.write_str(&format!( 50 | "{}{}", 51 | Emoji("📦", "dep:"), 52 | self.name.trim_start_matches("dep:") 53 | ))? 54 | } else { 55 | f.write_str(&self.name)? 56 | } 57 | 58 | Ok(()) 59 | } 60 | } 61 | 62 | impl From<&str> for SubFeatureType { 63 | fn from(s: &str) -> Self { 64 | if s.starts_with("dep:") { 65 | return SubFeatureType::Dependency; 66 | } 67 | 68 | if s.contains('/') { 69 | return SubFeatureType::DependencyFeature; 70 | } 71 | 72 | SubFeatureType::Normal 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/io/parsing/package.rs: -------------------------------------------------------------------------------- 1 | use cargo_metadata::{CargoOpt, PackageId}; 2 | 3 | use crate::io::parsing::workspace::parse_workspace; 4 | use color_eyre::Result; 5 | 6 | use crate::io::parsing::dependency::parse_dependency; 7 | use crate::io::util::toml_document_from_path; 8 | use crate::project::dependency::Dependency; 9 | use crate::project::package::Package; 10 | use color_eyre::eyre::ContextCompat; 11 | use semver::VersionReq; 12 | use std::collections::HashMap; 13 | use std::path::PathBuf; 14 | 15 | pub fn get_packages(path: impl Into) -> Result<(Vec, Option, PathBuf)> { 16 | let metadata = cargo_metadata::MetadataCommand::new() 17 | .current_dir(path) 18 | .features(CargoOpt::AllFeatures) 19 | .exec()?; 20 | 21 | let metadata_packages: HashMap = metadata 22 | .packages 23 | .into_iter() 24 | .map(|package| (package.id.clone(), package)) 25 | .collect(); 26 | 27 | let packages = metadata 28 | .workspace_members 29 | .iter() 30 | .map(|package| parse_package(package, &metadata_packages)) 31 | .collect::>>()?; 32 | 33 | Ok(( 34 | packages, 35 | parse_workspace(metadata.workspace_root.as_str(), &metadata_packages)?, 36 | metadata.workspace_root.into(), 37 | )) 38 | } 39 | 40 | pub fn parse_package( 41 | package: &PackageId, 42 | packages: &HashMap, 43 | ) -> Result { 44 | let package = packages.get(package).context("package not found")?; 45 | 46 | let toml_doc = toml_document_from_path(package.manifest_path.as_str())?; 47 | 48 | let dependencies: Result> = package 49 | .dependencies 50 | .iter() 51 | .map(|dep| parse_dependency(dep, packages, &toml_doc)) 52 | .collect(); 53 | 54 | Ok(Package { 55 | dependencies: dependencies?, 56 | name: package.name.to_string(), 57 | manifest_path: package.manifest_path.to_string(), 58 | }) 59 | } 60 | 61 | pub fn get_package_from_version<'a>( 62 | name: &str, 63 | version_req: &VersionReq, 64 | packages: &'a HashMap, 65 | ) -> Result<&'a cargo_metadata::Package> { 66 | packages 67 | .values() 68 | .filter(|package| package.name == name) 69 | .find(|package| version_req.matches(&package.version) || version_req.to_string() == "*") 70 | .context(format!( 71 | "could not find version for {} {}", 72 | name, version_req 73 | )) 74 | } 75 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::unwrap_used)] 2 | 3 | use std::process::exit; 4 | 5 | use clap::{CommandFactory, Parser, Subcommand, arg}; 6 | use clap_complete::{Shell, generate}; 7 | use color_eyre::Result; 8 | use console::Term; 9 | 10 | use crate::edit::display::Display; 11 | use crate::prune::prune; 12 | 13 | mod edit; 14 | mod prune; 15 | 16 | mod project; 17 | 18 | mod io; 19 | 20 | #[derive(Parser)] 21 | #[command(name = "cargo")] 22 | #[command(bin_name = "cargo")] 23 | enum CargoCli { 24 | Features(FeaturesArgs), 25 | } 26 | 27 | #[derive(Parser)] 28 | #[command(author, version, about, long_about = None)] 29 | struct FeaturesArgs { 30 | #[arg(long = "generate", value_enum)] 31 | generator: Option, 32 | 33 | #[arg(long, short)] 34 | dependency: Option, 35 | 36 | #[command(subcommand)] 37 | sub: Option, 38 | } 39 | 40 | #[derive(Subcommand)] 41 | enum FeaturesSubCommands { 42 | Prune { 43 | #[arg(long, short)] 44 | dry_run: bool, 45 | #[arg(long, short)] 46 | skip_tests: bool, 47 | /// do not copy the project into a temporary directory 48 | #[arg(long, short = 't')] 49 | no_tmp: bool, 50 | /// `cargo clean` will run after each 51 | #[arg(long, short, default_value_t, value_enum)] 52 | clean: CleanLevel, 53 | }, 54 | } 55 | 56 | #[derive(clap::ValueEnum, Clone, Default, Debug)] 57 | enum CleanLevel { 58 | #[default] 59 | Never, 60 | Package, 61 | Dependency, 62 | } 63 | 64 | fn main() -> Result<()> { 65 | color_eyre::install()?; 66 | 67 | let CargoCli::Features(args) = CargoCli::parse(); 68 | 69 | if let Some(generator) = args.generator { 70 | let cmd = &mut FeaturesArgs::command(); 71 | eprintln!("Generating completion file for {generator:?}..."); 72 | generate( 73 | generator, 74 | cmd, 75 | cmd.get_name().to_string(), 76 | &mut std::io::stdout(), 77 | ); 78 | return Ok(()); 79 | } 80 | 81 | run(args) 82 | } 83 | 84 | fn run(args: FeaturesArgs) -> Result<()> { 85 | let _ = ctrlc::set_handler(|| { 86 | let term = Term::stdout(); 87 | term.show_cursor().expect("could not enable cursor"); 88 | 89 | exit(0); 90 | }); 91 | 92 | if let Some(sub) = args.sub { 93 | match sub { 94 | FeaturesSubCommands::Prune { 95 | dry_run, 96 | skip_tests, 97 | clean, 98 | no_tmp, 99 | } => { 100 | prune(dry_run, skip_tests, clean, no_tmp)?; 101 | } 102 | } 103 | } else { 104 | let mut display = Display::new()?; 105 | 106 | if let Some(name) = args.dependency { 107 | display.set_selected_dep(name)? 108 | } 109 | 110 | display.start()?; 111 | } 112 | 113 | Ok(()) 114 | } 115 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.10.2 2 | 3 | * move to tempfile from deprecated tempdir 4 | 5 | ## 0.10.1 6 | 7 | * add `--no-tmp` to prune 8 | 9 | ## 0.10.0 10 | 11 | * run prune in temp folder 12 | * hide cursor while pruning 13 | 14 | ## 0.9.1 15 | 16 | * Add http2 to known false positives for 17 | rocket/hyper/hyper-util - [RivenSkaye](https://github.com/ToBinio/cargo-features-manager/pull/41) 18 | 19 | ## 0.9.0 20 | 21 | * move prune progress display to bottom 22 | * add `cargo features prune --clean ` 23 | 24 | ## 0.8.4 25 | 26 | * fix handling of renamed dependencies in 27 | workspaces - [the-wondersmith](https://github.com/ToBinio/cargo-features-manager/pull/39) 28 | 29 | ## 0.8.3 30 | 31 | * `cargo features prune` now runs all test 32 | * add `--skip-tests` to prune 33 | 34 | ## 0.8.2 35 | 36 | * fix `features-manager.keep` only being applied to normal dependencies 37 | 38 | ## 0.8.1 39 | 40 | * fix search for features not working 41 | 42 | ## 0.8.0 43 | 44 | * #### BREAKING - move Features.toml into Cargo.toml see [README.md](README.md#prune) 45 | 46 | * use `color-eyre` instead of `anyhow` 47 | * handle unused workspace dependencies 48 | * allow `default` to be a sub_feature 49 | * sort dependencies and packages alphabetically if no filter is set 50 | * when running `cargo features prune` correctly handle sub features 51 | * improved progress display while running `cargo features prune` 52 | * `cargo features prune` now displays which features get disabled 53 | * add list of known features to ignore when running `cargo features prune` 54 | 55 | ## 0.7.1 56 | 57 | * make `*` as a version be a wildcard for `any` - always find prerelease versions 58 | * handle dependency renames via `package = ""` 59 | 60 | ## 0.7.0 61 | 62 | * workspace dependency support 63 | * highlight empty packages 64 | * search for packages 65 | * support custom targets 66 | * fix bug where it could not differentiate between dependencies 67 | 68 | ## 0.6.0 69 | 70 | * use `cargo metadata` instead of custom parsing. This helps a lot for edge cases e.g. git 71 | * handle git dependencies 72 | * fix bug where changes where not saved when filtering dependencies 73 | * handle build-dependencies and dev-dependencies 74 | * display which dependencies a feature will enable 75 | * run `cargo test` for `cargo features prune` 76 | 77 | ## 0.5.3 78 | 79 | * display dependency-parsing-error next to dependency instead of crashing 80 | * ignore `no dependencies were found` when working in a workspace 81 | 82 | ## 0.5.2 83 | 84 | * don't crash when using workspace dependencies 85 | * display workspace dependencies as package 86 | * handle one cargo.toml being a workspace and a package 87 | 88 | ## 0.5.1 89 | 90 | * allow * to be in workspace path 91 | * fix local dependencies resolution 92 | 93 | ## 0.5.0 94 | 95 | * handle workspaces 96 | * always sort features 97 | * add basic terminal autocompletion 98 | 99 | ## 0.4.0 100 | 101 | * `cargo features prune` see [README.md](README.md#prune) 102 | * move from `crossterm` to `console` 103 | 104 | ## 0.3.3 105 | 106 | * only fetch crates when needed 107 | 108 | ## 0.3.2 109 | 110 | * update sparse-cache 111 | 112 | ## 0.3.1 113 | 114 | * sparse index 115 | 116 | ## 0.3.0 117 | 118 | * better search algorithm 119 | * change navigation keys 120 | 121 | ## 0.2.0 122 | 123 | * `search` see [README.md](README.md#search-mode) 124 | 125 | ## 0.1.2 126 | 127 | * support optional features 128 | 129 | ## 0.1.1 130 | 131 | * only save changed features 132 | * better gray color 133 | 134 | ## 0.1.0 135 | 136 | * initial release -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cargo Features Manger 2 | 3 | A TUI-like cli tool to manage the features of your rust-projects dependencies. 4 | 5 | You can view all available features and easily toggle (enable & disable) them with one button click. All of your changes 6 | will directly be reflected in your Cargo.toml file. 7 | 8 | --- 9 | 10 | ## install 11 | 12 | `cargo install cargo-features-manager` 13 | 14 | --- 15 | 16 | ## usage 17 | 18 | To start the tool run `cargo features` in your project root dir. 19 | 20 | This will open the dependency-selector: 21 | 22 | ![dependencySelector](resources/dependencySelector.png) 23 | 24 | Now you can select the dependency for which you want to change the enabled features. 25 | 26 | Selecting a dependency will open the feature-selector: 27 | 28 | ![featureSelector](resources/featureSelector.png) 29 | 30 | When using `cargo features -d ` it will directly open the corresponding feature-selector. 31 | 32 | ### navigation 33 | 34 | to move up 35 | 36 | to move down 37 | 38 | Space | Enter | to select 39 | 40 | ESC | to move back 41 | 42 | ### dependency selector 43 | 44 | Dependency which do not have any features are marked grey.
45 | Dev-Dependency are marked with 🧪.
46 | Build-Dependency are marked with 🛠️.
47 | Workspace-Dependency are marked with 🗃️️. 48 | 49 | ![dependencySelector](resources/dependencySelector.png) 50 | 51 | ### feature selector 52 | 53 | All default features are marked Green. 54 | 55 | ![greenMark](resources/greenMark.png) 56 | 57 | When hovering above a feature it shows other features which the selected feature requires. 58 | 59 | ![featureDependency](resources/featureDependency.png) 60 | 61 | Features marked with 📦 mean that they require an optional dependency. 62 | 63 | ![featurePackageDependency](resources/featurePackageDependency.png) 64 | 65 | Features which an active feature requires are marked grey. 66 | 67 | ![greyFeature](resources/greyFeature.png) 68 | 69 | Features marked with 🗃️️ are enabled by the workspace dependency and can only be disabled by the workspace dependency 70 | 71 | ![workspaceFeatures](resources/workspaceFeatures.png) 72 | 73 | ### search mode 74 | 75 | At any point you can start typing like normal. 76 | This will start using your input as a search query. 77 | 78 | --- 79 | 80 | ## prune 81 | 82 | You can run prune with `cargo features prune` 83 | 84 | this will disable all features which are not required to compile. 85 | 86 | ### false positives 87 | 88 | Some features may not cause the compilation to fail but still remove functionality. To limit the extent of such cases we 89 | keep a [file](Known-Features.toml) including all known false positives. These features will not be disabled 90 | by `cargo features prune` and instead be display gray letting you know that you should consider if you really need the 91 | feature. 92 | 93 | If you know of any other features that fall under this category fell free to open an Issue or PR! 94 | 95 | If your project requires additional features to be always kept. You can add a section to your `Cargo.toml` 96 | named `cargo-features-manager.keep` in there you can define which features will be kept. 97 | 98 | ```toml 99 | # Cargo.toml 100 | 101 | # for individial packages 102 | [cargo-features-manager.keep] 103 | clap = ["default"] 104 | color-eyre = ["capture-spantrace", "track-caller"] 105 | 106 | # for the whole workspace 107 | [workspace.cargo-features-manager.keep] 108 | clap = ["default"] 109 | color-eyre = ["capture-spantrace", "track-caller"] 110 | ``` -------------------------------------------------------------------------------- /src/io/save.rs: -------------------------------------------------------------------------------- 1 | use crate::io::util::{get_mut_dependecy_item_from_doc, toml_document_from_path}; 2 | use crate::project::document::Document; 3 | use color_eyre::eyre::{ContextCompat, Error}; 4 | use std::fs; 5 | use toml_edit::{Array, Formatted, InlineTable, Item, Value}; 6 | 7 | pub fn save_dependency( 8 | document: &mut Document, 9 | package_name: &str, 10 | dep_name: &str, 11 | ) -> color_eyre::Result<()> { 12 | let package = document.get_package_mut(package_name)?; 13 | let dependency = package.get_dep(dep_name)?; 14 | 15 | let features_to_enable = dependency.get_features_to_enable(); 16 | 17 | let mut doc = toml_document_from_path(&package.manifest_path)?; 18 | let deps = get_mut_dependecy_item_from_doc(&dependency.kind, &dependency.target, &mut doc)?; 19 | 20 | let deps = deps.as_table_mut().context(format!( 21 | "could not parse dependencies as a table - {}", 22 | package.name 23 | ))?; 24 | 25 | let table = match deps 26 | .get_mut(dependency.rename.as_ref().unwrap_or(&dependency.name)) 27 | .context("dependency not found")? 28 | .as_table_like_mut() 29 | { 30 | None => { 31 | deps.insert( 32 | &dependency.name, 33 | Item::Value(Value::InlineTable(InlineTable::new())), 34 | ); 35 | 36 | deps.get_mut(&dependency.name) 37 | .context(format!( 38 | "could not find {} in dependency", 39 | dependency.get_name() 40 | ))? 41 | .as_table_like_mut() 42 | .context(format!( 43 | "could not parse {} as a table", 44 | dependency.get_name() 45 | ))? 46 | } 47 | Some(table) => table, 48 | }; 49 | 50 | let has_custom_attributes = table 51 | .get_values() 52 | .iter() 53 | .map(|(name, _)| name.first().map(|key| key.to_string()).unwrap_or_default()) 54 | .any(|name| !["features", "default-features", "version"].contains(&&*name)); 55 | 56 | //check if entry has to be table or can just be string with version 57 | if dependency.can_use_default() && features_to_enable.is_empty() && !has_custom_attributes { 58 | deps.insert( 59 | &dependency.name, 60 | Item::Value(Value::String(Formatted::new(dependency.get_version()))), 61 | ); 62 | } else { 63 | //version 64 | if !dependency.version.is_empty() && !table.contains_key("git") && !dependency.workspace { 65 | table.insert( 66 | "version", 67 | Item::Value(Value::String(Formatted::new(dependency.get_version()))), 68 | ); 69 | } 70 | 71 | //features 72 | let mut features = Array::new(); 73 | 74 | for name in features_to_enable { 75 | features.push(Value::String(Formatted::new(name))); 76 | } 77 | 78 | if features.is_empty() { 79 | table.remove("features"); 80 | } else { 81 | table.insert("features", Item::Value(Value::Array(features))); 82 | } 83 | 84 | //default-feature 85 | if dependency.can_use_default() || dependency.workspace { 86 | table.remove("default-features"); 87 | } else { 88 | table.insert( 89 | "default-features", 90 | Item::Value(Value::Boolean(Formatted::new(false))), 91 | ); 92 | } 93 | } 94 | 95 | // update workspace deps 96 | if let Some(workspace_package) = document.get_workspace_package() { 97 | let workspace = workspace_package?; 98 | 99 | if workspace.name == package_name { 100 | document.update_workspace_deps()?; 101 | } 102 | } 103 | 104 | //write updates 105 | let package = document.get_package(package_name)?; 106 | 107 | fs::write(&package.manifest_path, doc.to_string()).map_err(Error::from) 108 | } 109 | -------------------------------------------------------------------------------- /src/prune/parse.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::Result; 2 | 3 | use crate::io::util::{get_item_from_doc, toml_document_from_path}; 4 | use crate::project::dependency::Dependency; 5 | use crate::project::document::Document; 6 | use crate::prune::FeaturesMap; 7 | use color_eyre::eyre::{ContextCompat, eyre}; 8 | use std::collections::HashMap; 9 | use std::ops::Not; 10 | use std::path::Path; 11 | 12 | pub fn get_features_to_test(document: &Document) -> Result { 13 | let base_ignored_features = 14 | get_ignored_features("./", "workspace.cargo-features-manager.keep")?; 15 | 16 | let mut enabled_features = get_enabled_features(document); 17 | remove_ignored_features(document, &base_ignored_features, &mut enabled_features)?; 18 | 19 | Ok(enabled_features) 20 | } 21 | 22 | fn get_enabled_features(document: &Document) -> FeaturesMap { 23 | let mut data = HashMap::new(); 24 | 25 | for package in document.get_packages() { 26 | let mut package_data = HashMap::new(); 27 | 28 | for dependency in package.get_deps() { 29 | let enabled_features = dependency 30 | .features 31 | .iter() 32 | .filter(|(_name, data)| data.is_toggleable() && data.is_enabled()) 33 | .map(|(name, _data)| name) 34 | .cloned() 35 | .collect::>(); 36 | 37 | if enabled_features.is_empty().not() { 38 | package_data.insert(dependency.get_name().clone(), enabled_features); 39 | } 40 | } 41 | 42 | if package_data.is_empty().not() { 43 | data.insert(package.name.clone(), package_data); 44 | } 45 | } 46 | 47 | data 48 | } 49 | 50 | fn get_ignored_features>( 51 | file_path: P, 52 | item_path: &str, 53 | ) -> Result>> { 54 | let result = toml_document_from_path(file_path.as_ref().join("Cargo.toml")); 55 | 56 | match result { 57 | Ok(document) => { 58 | let item = get_item_from_doc(item_path, &document); 59 | 60 | let Ok(item) = item else { 61 | return Ok(HashMap::new()); 62 | }; 63 | 64 | let table = item.as_table_like().context(format!( 65 | "could not parse {} in {:?}", 66 | item_path, 67 | file_path.as_ref() 68 | ))?; 69 | 70 | let mut map = HashMap::new(); 71 | 72 | for (key, value) in table.iter() { 73 | map.insert( 74 | key.to_string(), 75 | value 76 | .as_array() 77 | .ok_or(eyre!("Invalid format to keep features"))? 78 | .iter() 79 | .filter_map(|value| value.as_str()) 80 | .map(|value| value.to_string()) 81 | .collect(), 82 | ); 83 | } 84 | 85 | Ok(map) 86 | } 87 | Err(_) => Ok(HashMap::new()), 88 | } 89 | } 90 | 91 | fn remove_ignored_features( 92 | document: &Document, 93 | base_ignored: &HashMap>, 94 | enabled_features: &mut FeaturesMap, 95 | ) -> Result<()> { 96 | for (package_name, dependencies) in enabled_features { 97 | let package = document.get_package(package_name)?; 98 | 99 | let ignored_features = get_ignored_features( 100 | package.manifest_path.trim_end_matches("/Cargo.toml"), 101 | "cargo-features-manager.keep", 102 | )?; 103 | 104 | for (dependency_name, features) in dependencies { 105 | let dependency = package.get_dep(dependency_name)?; 106 | 107 | if dependency.can_use_default() { 108 | features.push("default".to_string()); 109 | } 110 | 111 | for feature in ignored_features.get(&dependency.name).unwrap_or(&vec![]) { 112 | remove_feature(feature, features, dependency); 113 | } 114 | for feature in base_ignored.get(&dependency.name).unwrap_or(&vec![]) { 115 | remove_feature(feature, features, dependency); 116 | } 117 | 118 | if let Some(index) = features.iter().position(|name| name == "default") { 119 | features.remove(index); 120 | } 121 | } 122 | } 123 | 124 | Ok(()) 125 | } 126 | 127 | fn remove_feature(feature: &String, features: &mut Vec, dependency: &Dependency) { 128 | let index = features.iter().position(|name| name == feature); 129 | 130 | let Some(index) = index else { 131 | return; 132 | }; 133 | 134 | features.remove(index); 135 | 136 | if let Some(feature) = dependency.get_feature(feature) { 137 | for sub_feature in &feature.sub_features { 138 | remove_feature(&sub_feature.name, features, dependency); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/project/document.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::eyre::{ContextCompat, bail, eyre}; 2 | use std::path::PathBuf; 3 | 4 | use color_eyre::Result; 5 | use itertools::Itertools; 6 | 7 | use crate::io::parsing::package::get_packages; 8 | use crate::project::dependency::feature::EnabledState; 9 | use crate::project::package::Package; 10 | 11 | pub struct Document { 12 | packages: Vec, 13 | workspace_index: Option, 14 | root_path: PathBuf, 15 | } 16 | 17 | impl Document { 18 | pub fn new(path: impl Into) -> Result { 19 | let (mut packages, workspace, root_path) = get_packages(path)?; 20 | 21 | if packages.len() == 1 22 | && packages 23 | .first() 24 | .context("no package found")? 25 | .dependencies 26 | .is_empty() 27 | { 28 | bail!("no dependencies were found") 29 | } 30 | 31 | let mut workspace_index = None; 32 | 33 | if let Some(workspace) = workspace { 34 | packages.push(workspace); 35 | 36 | workspace_index = Some(packages.len() - 1); 37 | } 38 | 39 | let mut document = Document { 40 | packages, 41 | workspace_index, 42 | root_path, 43 | }; 44 | 45 | document.update_workspace_deps()?; 46 | 47 | Ok(document) 48 | } 49 | 50 | pub fn root_path(&self) -> &PathBuf { 51 | &self.root_path 52 | } 53 | 54 | pub fn update_workspace_deps(&mut self) -> Result<()> { 55 | let Some(workspace_index) = self.workspace_index else { 56 | return Ok(()); 57 | }; 58 | 59 | for index in 0..self.packages.len() { 60 | if index == workspace_index { 61 | continue; 62 | }; 63 | 64 | for dep_index in 0..self.packages[index].dependencies.len() { 65 | let dep = &self.packages[index].dependencies[dep_index]; 66 | 67 | if !dep.workspace { 68 | continue; 69 | } 70 | 71 | let workspace = &self.packages[workspace_index]; 72 | let workspace_dep = workspace 73 | .dependencies 74 | .iter() 75 | .find(|workspace_dep| { 76 | workspace_dep.name == dep.name 77 | || workspace_dep 78 | .rename 79 | .as_ref() 80 | .is_some_and(|name| name == dep.name.as_str()) 81 | }) 82 | .ok_or(eyre!( 83 | "could not find workspace dep - {:#?}", 84 | dep.get_name() 85 | ))?; 86 | 87 | let enabled_workspace_features = workspace_dep 88 | .features 89 | .iter() 90 | .filter(|(_, data)| data.is_enabled()) 91 | .map(|(name, _)| name.to_string()) 92 | .collect_vec(); 93 | 94 | let dep = &mut self.packages[index].dependencies[dep_index]; 95 | 96 | let workspace_deps = dep 97 | .features 98 | .iter() 99 | .filter(|(_, data)| data.enabled_state == EnabledState::Workspace) 100 | .map(|(name, _)| name.to_string()) 101 | .collect_vec(); 102 | 103 | for feature in workspace_deps { 104 | dep.disable_feature(feature.as_str())?; 105 | } 106 | 107 | for name in enabled_workspace_features { 108 | dep.enable_feature(&name)?; 109 | dep.set_feature_to_workspace(&name)?; 110 | } 111 | } 112 | } 113 | 114 | Ok(()) 115 | } 116 | 117 | pub fn get_packages(&self) -> &Vec { 118 | &self.packages 119 | } 120 | 121 | pub fn get_package_by_index(&self, package_id: usize) -> Result<&Package> { 122 | self.packages 123 | .get(package_id) 124 | .context(format!("no package for id {} found", package_id)) 125 | } 126 | 127 | pub fn get_package(&self, package: &str) -> Result<&Package> { 128 | self.packages 129 | .iter() 130 | .find(|pkg| pkg.name == package) 131 | .context(format!("no package with name {} found", package)) 132 | } 133 | 134 | pub fn get_package_mut(&mut self, package: &str) -> Result<&mut Package> { 135 | self.packages 136 | .iter_mut() 137 | .find(|pkg| pkg.name == package) 138 | .context(format!("no package with name {} found", package)) 139 | } 140 | 141 | pub fn get_workspace_package(&self) -> Option> { 142 | self.workspace_index 143 | .map(|workspace_index| self.get_package_by_index(workspace_index)) 144 | } 145 | 146 | pub fn is_workspace(&self) -> bool { 147 | self.packages.len() > 1 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/edit/filter_view/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::edit::filter_view::item::FilterViewItem; 2 | use crate::project::dependency::Dependency; 3 | use crate::project::document::Document; 4 | use crate::project::package::Package; 5 | use color_eyre::eyre::ContextCompat; 6 | use fuzzy_matcher::skim::SkimMatcherV2; 7 | use itertools::Itertools; 8 | use std::cmp::Ordering; 9 | 10 | pub mod item; 11 | 12 | pub struct FilterView { 13 | pub selected_index: usize, 14 | pub data: Vec, 15 | } 16 | 17 | impl FilterView { 18 | pub fn shift(&mut self, shift: isize) { 19 | if !self.has_data() { 20 | self.selected_index = 0; 21 | return; 22 | } 23 | 24 | let mut selected_temp = self.selected_index as isize; 25 | 26 | selected_temp += self.data.len() as isize; 27 | selected_temp += shift; 28 | 29 | selected_temp %= self.data.len() as isize; 30 | 31 | self.selected_index = selected_temp as usize; 32 | } 33 | 34 | pub fn get_selected(&self) -> color_eyre::Result<&FilterViewItem> { 35 | self.data 36 | .get(self.selected_index) 37 | .context("nothing selected") 38 | } 39 | 40 | pub fn has_data(&self) -> bool { 41 | !self.data.is_empty() 42 | } 43 | 44 | pub fn data_from_dependency(dependency: &Dependency, filter: &str) -> Vec { 45 | let features = dependency 46 | .features 47 | .iter() 48 | .filter(|feature| feature.0 != "default"); 49 | 50 | if filter.is_empty() { 51 | features 52 | .sorted_by(|(name_a, data_a), (name_b, data_b)| { 53 | if data_a.is_default && !data_b.is_default { 54 | return Ordering::Less; 55 | } 56 | 57 | if data_b.is_default && !data_a.is_default { 58 | return Ordering::Greater; 59 | } 60 | 61 | name_a.cmp(name_b) 62 | }) 63 | .map(|(name, _)| FilterViewItem::from_feature(name, vec![])) 64 | .collect() 65 | } else { 66 | let matcher = SkimMatcherV2::default(); 67 | 68 | features 69 | .filter_map(|(name, _)| matcher.fuzzy(name, filter, true).map(|some| (name, some))) 70 | .sorted_by(|(_, fuzzy_a), (_, fuzzy_b)| fuzzy_a.0.cmp(&fuzzy_b.0).reverse()) 71 | .map(|(name, fuzzy)| (name, fuzzy.1)) 72 | .map(|(name, indexes)| FilterViewItem::from_feature(name, indexes)) 73 | .collect() 74 | } 75 | } 76 | 77 | pub fn data_from_package( 78 | package: &Package, 79 | filter: &str, 80 | ) -> color_eyre::Result> { 81 | let deps = if filter.is_empty() { 82 | package 83 | .dependencies 84 | .iter() 85 | .sorted_by(|dependency_a, dependency_b| dependency_a.name.cmp(&dependency_b.name)) 86 | .map(|dependency| FilterViewItem::from_dependency(dependency, vec![])) 87 | .collect() 88 | } else { 89 | let matcher = SkimMatcherV2::default(); 90 | 91 | package 92 | .dependencies 93 | .iter() 94 | .filter_map(|dependency| { 95 | matcher 96 | .fuzzy(&dependency.get_name(), filter, true) 97 | .map(|fuzzy_result| (dependency, fuzzy_result)) 98 | }) 99 | .sorted_by(|(_, fuzzy_a), (_, fuzzy_b)| fuzzy_a.0.cmp(&fuzzy_b.0).reverse()) 100 | .map(|(dependency, fuzzy)| (dependency, fuzzy.1)) 101 | .map(|(dependency, indexes)| FilterViewItem::from_dependency(dependency, indexes)) 102 | .collect() 103 | }; 104 | 105 | Ok(deps) 106 | } 107 | 108 | pub fn data_from_document( 109 | document: &Document, 110 | filter: &str, 111 | ) -> color_eyre::Result> { 112 | let packages = if filter.is_empty() { 113 | document 114 | .get_packages() 115 | .iter() 116 | .sorted_by(|package_a, package_b| package_a.name.cmp(&package_b.name)) 117 | .map(|package| FilterViewItem::from_package(package, vec![])) 118 | .collect() 119 | } else { 120 | let matcher = SkimMatcherV2::default(); 121 | 122 | document 123 | .get_packages() 124 | .iter() 125 | .filter_map(|package| { 126 | matcher 127 | .fuzzy(&package.name, filter, true) 128 | .map(|fuzzy_result| (package, fuzzy_result)) 129 | }) 130 | .sorted_by(|(_, fuzzy_a), (_, fuzzy_b)| fuzzy_a.0.cmp(&fuzzy_b.0).reverse()) 131 | .map(|(package, fuzzy)| (package, fuzzy.1)) 132 | .map(|(package, indexes)| FilterViewItem::from_package(package, indexes)) 133 | .collect() 134 | }; 135 | 136 | Ok(packages) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/prune/display.rs: -------------------------------------------------------------------------------- 1 | use crate::project::document::Document; 2 | use crate::prune::{DependencyName, FeatureName, FeaturesMap}; 3 | use color_eyre::Result; 4 | use console::{Term, style}; 5 | use itertools::Itertools; 6 | use std::collections::HashMap; 7 | use std::io::Write; 8 | 9 | type IsKnownFeature = bool; 10 | 11 | pub struct Display { 12 | term: Term, 13 | 14 | package_inset: usize, 15 | dependency_inset: usize, 16 | 17 | feature_count: usize, 18 | checked_features_count: usize, 19 | is_workspace: bool, 20 | 21 | package_name: String, 22 | package_feature_count: usize, 23 | package_checked_features_count: usize, 24 | 25 | dependency_name: String, 26 | dependency_feature_count: usize, 27 | } 28 | 29 | impl Display { 30 | pub fn new(features_to_test: &FeaturesMap, document: &Document) -> Self { 31 | let feature_count = features_to_test 32 | .values() 33 | .flat_map(|dependencies| dependencies.values()) 34 | .flatten() 35 | .count(); 36 | 37 | let package_inset = if features_to_test.len() == 1 { 0 } else { 2 }; 38 | let dependency_inset = if features_to_test.len() == 1 { 2 } else { 4 }; 39 | 40 | Self { 41 | feature_count, 42 | package_inset, 43 | dependency_inset, 44 | is_workspace: document.is_workspace(), 45 | package_name: "?".to_string(), 46 | package_feature_count: 0, 47 | package_checked_features_count: 0, 48 | dependency_name: "?".to_string(), 49 | term: Term::stdout(), 50 | checked_features_count: 0, 51 | dependency_feature_count: 0, 52 | } 53 | } 54 | 55 | pub fn start(&self) -> Result<()> { 56 | writeln!(&self.term, "workspace [{}]", self.feature_count)?; 57 | self.term.hide_cursor()?; 58 | Ok(()) 59 | } 60 | 61 | pub fn finish(&self) -> Result<()> { 62 | self.term.show_cursor()?; 63 | Ok(()) 64 | } 65 | 66 | pub fn display_known_features_notice(&mut self) -> Result<()> { 67 | self.term.clear_line()?; 68 | writeln!(self.term)?; 69 | writeln!( 70 | self.term, 71 | "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#prune" 72 | )?; 73 | Ok(()) 74 | } 75 | 76 | pub fn next_package( 77 | &mut self, 78 | package_name: &str, 79 | package_features: &HashMap>, 80 | ) -> Result<()> { 81 | self.package_name = package_name.to_string(); 82 | self.package_feature_count = package_features.values().flatten().count(); 83 | self.package_checked_features_count = 0; 84 | 85 | if self.is_workspace { 86 | let package_inset = self.package_inset; 87 | 88 | self.term.clear_line()?; 89 | writeln!(self.term)?; 90 | writeln!( 91 | self.term, 92 | "{:package_inset$}{} [{}]", 93 | "", package_name, self.package_feature_count 94 | )?; 95 | } 96 | 97 | Ok(()) 98 | } 99 | 100 | pub fn next_dependency(&mut self, name: &str, features: &[FeatureName]) { 101 | self.dependency_feature_count = features.len(); 102 | self.dependency_name = name.to_string(); 103 | } 104 | 105 | pub fn finish_dependency( 106 | &mut self, 107 | features: Vec<(&FeatureName, IsKnownFeature)>, 108 | ) -> Result<()> { 109 | let mut disabled_count = style( 110 | features 111 | .iter() 112 | .map(|(name, known)| { 113 | if *known { 114 | style(name).color256(7).to_string() 115 | } else { 116 | style(format!("-{}", name)).red().to_string() 117 | } 118 | }) 119 | .join(","), 120 | ); 121 | 122 | if features.is_empty() { 123 | disabled_count = style("0".to_string()); 124 | } 125 | 126 | let dependency_inset = self.dependency_inset; 127 | 128 | self.term.clear_line()?; 129 | writeln!( 130 | self.term, 131 | "{:dependency_inset$}{} [{}/{}]", 132 | "", self.dependency_name, disabled_count, self.dependency_feature_count 133 | )?; 134 | 135 | Ok(()) 136 | } 137 | 138 | pub fn next_feature(&mut self, id: usize, feature_name: &FeatureName) -> Result<()> { 139 | let dependency_inset = self.dependency_inset; 140 | 141 | self.term.clear_line()?; 142 | writeln!( 143 | self.term, 144 | "{:dependency_inset$}{} [{}/{}]", 145 | "", self.dependency_name, id, self.dependency_feature_count, 146 | )?; 147 | self.term.clear_line()?; 148 | writeln!(self.term, "{:dependency_inset$} └ {}", "", feature_name)?; 149 | self.term.move_cursor_up(2)?; 150 | 151 | self.display_progress_bar()?; 152 | 153 | Ok(()) 154 | } 155 | 156 | pub fn finish_feature(&mut self) -> Result<()> { 157 | self.checked_features_count += 1; 158 | self.package_checked_features_count += 1; 159 | 160 | self.display_progress_bar()?; 161 | 162 | Ok(()) 163 | } 164 | 165 | fn display_progress_bar(&mut self) -> Result<()> { 166 | self.term.move_cursor_down(2)?; 167 | self.term.clear_line()?; 168 | writeln!(self.term)?; 169 | self.term.clear_line()?; 170 | write!( 171 | self.term, 172 | "Workspace [{}/{}]", 173 | self.checked_features_count, self.feature_count, 174 | )?; 175 | 176 | if self.is_workspace { 177 | write!( 178 | self.term, 179 | " -> {} [{}/{}]", 180 | self.package_name, self.package_checked_features_count, self.package_feature_count 181 | )?; 182 | } 183 | 184 | writeln!(self.term)?; 185 | 186 | self.term.move_cursor_up(4)?; 187 | Ok(()) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/io/parsing/dependency.rs: -------------------------------------------------------------------------------- 1 | use crate::io::parsing::package::get_package_from_version; 2 | use crate::io::util::get_dependecy_item_from_doc; 3 | use crate::project::dependency::feature::{EnabledState, FeatureData, SubFeature, SubFeatureType}; 4 | use crate::project::dependency::{Dependency, DependencyType}; 5 | use cargo_metadata::PackageId; 6 | use color_eyre::eyre::{ContextCompat, eyre}; 7 | use itertools::Itertools; 8 | use semver::VersionReq; 9 | use std::collections::HashMap; 10 | use toml_edit::Item; 11 | 12 | pub fn parse_dependency( 13 | dependency: &cargo_metadata::Dependency, 14 | packages: &HashMap, 15 | document: &toml_edit::DocumentMut, 16 | ) -> color_eyre::Result { 17 | let package = get_package_from_version(&dependency.name, &dependency.req, packages)?; 18 | 19 | let kind: DependencyType = dependency.kind.into(); 20 | let mut workspace = false; 21 | 22 | let deps = get_dependecy_item_from_doc(&kind, &dependency.target, document)?; 23 | 24 | let deps = deps.as_table().context(format!( 25 | "could not parse dependencies as a table - {}", 26 | package.name 27 | ))?; 28 | 29 | let dep = if let Some(name) = &dependency.rename { 30 | deps.get(name).ok_or(eyre!( 31 | "could not find - dep:{} - {} - {:?}", 32 | name, 33 | deps, 34 | kind 35 | ))? 36 | } else { 37 | deps.get(&dependency.name).ok_or(eyre!( 38 | "could not find - dep:{} - {} - {:?}", 39 | dependency.name, 40 | deps, 41 | kind 42 | ))? 43 | }; 44 | 45 | if let Some(dep) = dep.as_table_like() { 46 | if let Some(workspace_item) = dep.get("workspace") { 47 | workspace = workspace_item.as_bool().unwrap_or(false); 48 | } 49 | } 50 | 51 | let mut new_dependency = Dependency { 52 | name: dependency.name.to_string(), 53 | rename: dependency.rename.clone(), 54 | target: dependency.target.clone(), 55 | version: dependency 56 | .req 57 | .to_string() 58 | .trim_start_matches('^') 59 | .to_owned(), 60 | kind, 61 | workspace, 62 | features: HashMap::new(), 63 | comment: None, 64 | }; 65 | 66 | set_features( 67 | &mut new_dependency, 68 | package, 69 | dependency.uses_default_features, 70 | &dependency.features, 71 | )?; 72 | 73 | Ok(new_dependency) 74 | } 75 | 76 | pub fn parse_dependency_from_item( 77 | packages: &HashMap, 78 | name: &str, 79 | data: &Item, 80 | ) -> color_eyre::Result { 81 | let mut version = "*"; 82 | let mut enabled_features = vec![]; 83 | let mut uses_default_features = true; 84 | let mut rename = None; 85 | 86 | if let Some(data) = data.as_table_like() { 87 | //parse version 88 | if let Some(version_data) = data.get("version") { 89 | version = version_data 90 | .as_str() 91 | .ok_or(eyre!("could not parse version"))?; 92 | } 93 | 94 | //parse enabled features 95 | if let Some(features) = data.get("features") { 96 | let features = features 97 | .as_array() 98 | .ok_or(eyre!("could not parse features"))?; 99 | 100 | enabled_features = features 101 | .iter() 102 | .filter_map(|feature| feature.as_str()) 103 | .map(|feature| feature.to_string()) 104 | .collect(); 105 | } 106 | 107 | //parse uses_default_features 108 | if let Some(uses_default) = data.get("default-features") { 109 | let uses_default = uses_default 110 | .as_bool() 111 | .ok_or(eyre!("could not parse default-features"))?; 112 | 113 | uses_default_features = uses_default; 114 | } 115 | 116 | //parse rename - package 117 | if let Some(package) = data.get("package") { 118 | let package = package.as_str().ok_or(eyre!("could not parse package"))?; 119 | 120 | rename = Some(package.to_string()); 121 | } 122 | } else { 123 | version = data.as_str().ok_or(eyre!("could not parse version"))?; 124 | } 125 | 126 | let mut dependency = Dependency { 127 | name: name.to_string(), 128 | rename, 129 | comment: None, 130 | version: version.to_string(), 131 | workspace: false, 132 | kind: DependencyType::Workspace, 133 | target: None, 134 | features: Default::default(), 135 | }; 136 | 137 | if let Ok(package) = get_package_from_version(name, &VersionReq::parse(version)?, packages) { 138 | set_features( 139 | &mut dependency, 140 | package, 141 | uses_default_features, 142 | &enabled_features, 143 | )?; 144 | } else { 145 | dependency.comment = Some("unused".to_string()); 146 | } 147 | 148 | Ok(dependency) 149 | } 150 | 151 | pub fn set_features( 152 | dependency: &mut Dependency, 153 | package: &cargo_metadata::Package, 154 | uses_default_features: bool, 155 | enabled_features: &Vec, 156 | ) -> color_eyre::Result<()> { 157 | let default_features = package.features.get("default").cloned().unwrap_or(vec![]); 158 | 159 | let features = package 160 | .features 161 | .iter() 162 | .map(|(feature, sub_features)| { 163 | ( 164 | feature.to_string(), 165 | FeatureData { 166 | sub_features: sub_features 167 | .iter() 168 | .map(|name| SubFeature { 169 | name: name.to_string(), 170 | kind: name.as_str().into(), 171 | }) 172 | .filter(|sub_feature| sub_feature.kind != SubFeatureType::DependencyFeature) 173 | .collect_vec(), 174 | is_default: default_features.contains(feature), 175 | enabled_state: EnabledState::Normal(false), 176 | }, 177 | ) 178 | }) 179 | .collect(); 180 | 181 | dependency.features = features; 182 | 183 | for feature in enabled_features { 184 | if Into::::into(feature.as_str()) == SubFeatureType::Normal { 185 | dependency.enable_feature(feature)?; 186 | } 187 | } 188 | 189 | if uses_default_features { 190 | for feature in &default_features { 191 | if Into::::into(feature.as_str()) == SubFeatureType::Normal { 192 | dependency.enable_feature(feature)?; 193 | } 194 | } 195 | } 196 | 197 | Ok(()) 198 | } 199 | -------------------------------------------------------------------------------- /src/io/util.rs: -------------------------------------------------------------------------------- 1 | use cargo_metadata::cargo_platform::Platform; 2 | use color_eyre::eyre::{ContextCompat, bail, eyre}; 3 | use std::fs; 4 | use std::path::Path; 5 | use std::str::FromStr; 6 | 7 | use crate::project::dependency::DependencyType; 8 | 9 | pub fn toml_document_from_path>( 10 | dir_path: P, 11 | ) -> color_eyre::Result { 12 | let file_content = fs::read_to_string(&dir_path).map_err(|err| { 13 | eyre!( 14 | "could not find Cargo.toml at {:?} - {err}", 15 | dir_path.as_ref() 16 | ) 17 | })?; 18 | 19 | Ok(file_content.parse()?) 20 | } 21 | 22 | pub fn get_mut_dependecy_item_from_doc<'a>( 23 | kind: &DependencyType, 24 | target: &Option, 25 | document: &'a mut toml_edit::DocumentMut, 26 | ) -> color_eyre::Result<&'a mut toml_edit::Item> { 27 | let path = get_dependency_path(kind, target); 28 | get_mut_item_from_doc(&path, document) 29 | } 30 | 31 | pub fn get_mut_item_from_doc<'a>( 32 | path: &str, 33 | document: &'a mut toml_edit::DocumentMut, 34 | ) -> color_eyre::Result<&'a mut toml_edit::Item> { 35 | let mut item = document.as_item_mut(); 36 | 37 | let mut is_target = false; 38 | 39 | 'outer: for key in path.split('.') { 40 | if is_target { 41 | is_target = false; 42 | 43 | let target = Platform::from_str(key.trim_start_matches('\'').trim_end_matches('\''))?; 44 | 45 | let table = item 46 | .as_table_like_mut() 47 | .context(eyre!("could not find - {} - no table", path))?; 48 | 49 | for (key, next_item) in table.iter_mut() { 50 | let platform = 51 | Platform::from_str(key.trim_start_matches('\'').trim_end_matches('\''))?; 52 | 53 | if platform.eq(&target) { 54 | item = next_item; 55 | continue 'outer; 56 | } 57 | } 58 | 59 | bail!("could not find - {} - no table", path) 60 | } 61 | 62 | item = item 63 | .get_mut(key) 64 | .context(eyre!("could not find - {}", path))?; 65 | 66 | if key == "target" { 67 | is_target = true; 68 | } 69 | } 70 | 71 | Ok(item) 72 | } 73 | 74 | pub fn get_dependecy_item_from_doc<'a>( 75 | kind: &DependencyType, 76 | target: &Option, 77 | document: &'a toml_edit::DocumentMut, 78 | ) -> color_eyre::Result<&'a toml_edit::Item> { 79 | let path = get_dependency_path(kind, target); 80 | get_item_from_doc(&path, document) 81 | } 82 | 83 | pub fn get_item_from_doc<'a>( 84 | path: &str, 85 | document: &'a toml_edit::DocumentMut, 86 | ) -> color_eyre::Result<&'a toml_edit::Item> { 87 | let mut item = document.as_item(); 88 | 89 | let mut is_target = false; 90 | 91 | 'outer: for key in path.split('.') { 92 | if is_target { 93 | is_target = false; 94 | 95 | let target = Platform::from_str(key.trim_start_matches('\'').trim_end_matches('\''))?; 96 | 97 | let table = item 98 | .as_table() 99 | .context(eyre!("could not find - {} - no table", path))?; 100 | 101 | for (key, next_item) in table.iter() { 102 | let platform = 103 | Platform::from_str(key.trim_start_matches('\'').trim_end_matches('\''))?; 104 | 105 | if platform.eq(&target) { 106 | item = next_item; 107 | continue 'outer; 108 | } 109 | } 110 | 111 | bail!("could not find - {} - no table", path) 112 | } 113 | 114 | item = item.get(key).context(eyre!("could not find - {}", path))?; 115 | 116 | if key == "target" { 117 | is_target = true; 118 | } 119 | } 120 | 121 | Ok(item) 122 | } 123 | 124 | fn get_dependency_path(kind: &DependencyType, target: &Option) -> String { 125 | let path = match kind { 126 | DependencyType::Normal => "dependencies", 127 | DependencyType::Development => "dev-dependencies", 128 | DependencyType::Build => "build-dependencies", 129 | DependencyType::Workspace => "workspace.dependencies", 130 | DependencyType::Unknown => "dependencies", 131 | }; 132 | 133 | if let Some(target) = target { 134 | return match target { 135 | Platform::Name(name) => format!("target.{}.{}", name, path), 136 | Platform::Cfg(cfg) => format!("target.'cfg({})'.{}", cfg, path), 137 | }; 138 | } 139 | 140 | path.to_string() 141 | } 142 | 143 | #[cfg(test)] 144 | mod test { 145 | use cargo_metadata::cargo_platform::{Cfg, CfgExpr, Ident, Platform}; 146 | use std::io::Write; 147 | use tempfile::NamedTempFile; 148 | 149 | use crate::{ 150 | io::util::{get_dependecy_item_from_doc, get_dependency_path, toml_document_from_path}, 151 | project::dependency::DependencyType, 152 | }; 153 | 154 | fn simple_test_toml() -> NamedTempFile { 155 | let mut tmpfile = NamedTempFile::new().unwrap(); 156 | write!( 157 | tmpfile, 158 | r#"[package] 159 | name = "Test" 160 | version = "0.0.1" 161 | edition = "2024" 162 | 163 | [dependencies] 164 | clap_complete = "4.5.46" 165 | console = {{ version = "0.16.1", features = ["std"], default-features = false }} 166 | ctrlc = "3.4.5" 167 | 168 | [dev-dependencies] 169 | color-eyre = "0.6.3" 170 | cargo_metadata = "0.23.0" 171 | clap = {{ version = "4.5.31", features = ["derive"] }} 172 | "# 173 | ) 174 | .unwrap(); 175 | 176 | tmpfile 177 | } 178 | 179 | #[test] 180 | fn toml_document_from_path_works() { 181 | let tmpfile = simple_test_toml(); 182 | let doc = toml_document_from_path(tmpfile.path()).unwrap(); 183 | assert!(doc.contains_table("package")); 184 | } 185 | 186 | #[test] 187 | fn get_dependecy_item_from_doc_works() { 188 | let tmpfile = simple_test_toml(); 189 | let doc = toml_document_from_path(tmpfile.path()).unwrap(); 190 | 191 | let item = get_dependecy_item_from_doc(&DependencyType::Development, &None, &doc).unwrap(); 192 | assert!(item.as_table().unwrap().contains_key("color-eyre")); 193 | } 194 | 195 | #[test] 196 | fn get_dependency_path_works() { 197 | assert_eq!( 198 | get_dependency_path( 199 | &DependencyType::Normal, 200 | &Some(Platform::Name("x86_64".to_string())), 201 | ), 202 | "target.x86_64.dependencies" 203 | ); 204 | 205 | assert_eq!( 206 | get_dependency_path(&DependencyType::Development, &None), 207 | "dev-dependencies" 208 | ); 209 | 210 | assert_eq!( 211 | get_dependency_path( 212 | &DependencyType::Build, 213 | &Some(Platform::Cfg(CfgExpr::Value(Cfg::Name(Ident { 214 | name: "x86_64".to_string(), 215 | raw: false, 216 | })))), 217 | ), 218 | "target.'cfg(x86_64)'.build-dependencies" 219 | ); 220 | 221 | assert_eq!( 222 | get_dependency_path( 223 | &DependencyType::Workspace, 224 | &Some(Platform::Name("x86_64".to_string())), 225 | ), 226 | "target.x86_64.workspace.dependencies" 227 | ); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/project/dependency/mod.rs: -------------------------------------------------------------------------------- 1 | use color_eyre::eyre::{ContextCompat, Result, eyre}; 2 | use std::collections::HashMap; 3 | 4 | use crate::project::dependency::feature::{EnabledState, FeatureData, SubFeatureType}; 5 | use cargo_metadata::{DependencyKind, cargo_platform::Platform}; 6 | use console::{Emoji, style}; 7 | use itertools::Itertools; 8 | 9 | pub mod feature; 10 | 11 | #[derive(Debug)] 12 | pub struct Dependency { 13 | pub name: String, 14 | pub rename: Option, 15 | pub comment: Option, 16 | pub version: String, 17 | 18 | pub workspace: bool, 19 | pub kind: DependencyType, 20 | pub target: Option, 21 | 22 | pub features: HashMap, 23 | } 24 | 25 | impl Dependency { 26 | pub fn get_name(&self) -> String { 27 | let mut name = if let Some(target) = &self.target { 28 | format!("{}.{}", target, self.name) 29 | } else { 30 | self.name.to_string() 31 | }; 32 | 33 | name = match self.kind { 34 | DependencyType::Normal | DependencyType::Workspace => name, 35 | DependencyType::Development => format!( 36 | "{} {}", 37 | Emoji("🧪", &style("dev").color256(8).to_string()), 38 | name 39 | ) 40 | .to_string(), 41 | DependencyType::Build => format!( 42 | "{} {}", 43 | Emoji("🛠️", &style("build").color256(8).to_string()), 44 | name 45 | ) 46 | .to_string(), 47 | DependencyType::Unknown => format!( 48 | "{} {}", 49 | Emoji("❔", &style("unknown").color256(8).to_string()), 50 | name 51 | ) 52 | .to_string(), 53 | }; 54 | 55 | if self.workspace { 56 | name = format!("{} {}", Emoji("🗃️", "W"), name).to_string(); 57 | } 58 | 59 | name 60 | } 61 | 62 | pub fn get_version(&self) -> String { 63 | self.version.to_string() 64 | } 65 | 66 | pub fn get_feature(&self, feature_name: &str) -> Option<&FeatureData> { 67 | self.features.get(feature_name) 68 | } 69 | 70 | pub fn has_features(&self) -> bool { 71 | !self.features.is_empty() 72 | } 73 | 74 | pub fn can_use_default(&self) -> bool { 75 | if self.workspace { 76 | return false; 77 | } 78 | 79 | for data in self.features.values() { 80 | if data.is_default && !data.is_enabled() { 81 | return false; 82 | } 83 | } 84 | 85 | true 86 | } 87 | 88 | pub fn get_features_to_enable(&self) -> Vec { 89 | let can_use_default = self.can_use_default(); 90 | 91 | self.features 92 | .iter() 93 | .filter(|(_, data)| data.is_enabled()) 94 | .filter(|(_, data)| !can_use_default || !data.is_default) 95 | .filter(|(_, data)| data.enabled_state != EnabledState::Workspace) 96 | .map(|(name, _)| name.clone()) 97 | .filter(|name| name != "default") 98 | .filter(|name| self.get_currently_dependent_features(name).is_empty()) 99 | .sorted() 100 | .collect() 101 | } 102 | 103 | pub fn toggle_feature(&mut self, feature_name: &str) -> Result<()> { 104 | let data = self 105 | .features 106 | .get(feature_name) 107 | .context(format!("could not find {}", feature_name))?; 108 | 109 | if let EnabledState::Normal(is_enabled) = data.enabled_state { 110 | if is_enabled { 111 | self.disable_feature(feature_name)?; 112 | } else { 113 | self.enable_feature(feature_name)?; 114 | } 115 | } 116 | 117 | Ok(()) 118 | } 119 | 120 | pub fn set_feature_to_workspace(&mut self, feature_name: &str) -> Result<()> { 121 | let data = self.features.get_mut(feature_name).ok_or(eyre!( 122 | "couldnt find feature {} trying to set as workspace feature for {}", 123 | feature_name, 124 | self.name 125 | ))?; 126 | 127 | data.enabled_state = EnabledState::Workspace; 128 | 129 | Ok(()) 130 | } 131 | 132 | pub fn enable_feature(&mut self, feature_name: &str) -> Result<()> { 133 | let data = self.features.get_mut(feature_name).ok_or(eyre!( 134 | "couldnt find feature {} trying to enable for {}", 135 | feature_name, 136 | self.name 137 | ))?; 138 | 139 | if data.is_enabled() { 140 | //early return to prevent loop 141 | return Ok(()); 142 | } 143 | 144 | data.enabled_state = EnabledState::Normal(true); 145 | 146 | //enable sub features 147 | let sub_features = data 148 | .sub_features 149 | .iter() 150 | .filter(|sub_feature| sub_feature.kind == SubFeatureType::Normal) 151 | .map(|sub_feature| sub_feature.name.to_string()) 152 | .collect_vec(); 153 | 154 | for sub_feature_name in sub_features { 155 | self.enable_feature(&sub_feature_name)?; 156 | } 157 | 158 | Ok(()) 159 | } 160 | 161 | pub fn disable_feature(&mut self, feature_name: &str) -> Result<()> { 162 | let data = self 163 | .features 164 | .get_mut(feature_name) 165 | .context(format!("could not find {}", feature_name))?; 166 | 167 | if !data.is_enabled() { 168 | //early return to prevent loop 169 | return Ok(()); 170 | } 171 | 172 | data.enabled_state = EnabledState::Normal(false); 173 | 174 | for name in self.get_dependent_features(feature_name) { 175 | self.disable_feature(&name)? 176 | } 177 | 178 | Ok(()) 179 | } 180 | 181 | /// returns all features which require the feature to be enabled 182 | fn get_dependent_features(&self, feature_name: &str) -> Vec { 183 | let mut dep_features = vec![]; 184 | 185 | for (name, data) in &self.features { 186 | if data 187 | .sub_features 188 | .iter() 189 | .any(|sub_feature| sub_feature.name == *feature_name) 190 | { 191 | dep_features.push(name.to_string()) 192 | } 193 | } 194 | 195 | dep_features 196 | } 197 | 198 | /// returns all features which are currently enabled and require the feature to be enabled 199 | pub fn get_currently_dependent_features(&self, feature_name: &str) -> Vec { 200 | self.get_dependent_features(feature_name) 201 | .iter() 202 | .filter_map(|name| self.features.get(name).map(|feature| (name, feature))) 203 | .filter(|(_, feature)| feature.is_enabled()) 204 | .map(|(name, _)| name.to_string()) 205 | .collect() 206 | } 207 | } 208 | 209 | #[derive(Debug)] 210 | pub enum DependencyType { 211 | Normal, 212 | Development, 213 | Build, 214 | Workspace, 215 | Unknown, 216 | } 217 | 218 | impl From for DependencyType { 219 | fn from(value: DependencyKind) -> Self { 220 | match value { 221 | DependencyKind::Normal => DependencyType::Normal, 222 | DependencyKind::Development => DependencyType::Development, 223 | DependencyKind::Build => DependencyType::Build, 224 | DependencyKind::Unknown => DependencyType::Unknown, 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/prune/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::CleanLevel; 2 | use crate::io::save::save_dependency; 3 | use crate::project::dependency::Dependency; 4 | use crate::project::document::Document; 5 | use crate::prune::display::Display; 6 | use crate::prune::parse::get_features_to_test; 7 | use color_eyre::Result; 8 | use color_eyre::eyre::{ContextCompat, eyre}; 9 | use copy_dir::copy_dir; 10 | use itertools::Itertools; 11 | use std::collections::HashMap; 12 | use std::ops::Not; 13 | use std::path::Path; 14 | use std::process::{Command, Stdio}; 15 | use tempfile::TempDir; 16 | 17 | mod parse; 18 | 19 | mod display; 20 | 21 | type PackageName = String; 22 | pub type DependencyName = String; 23 | pub type FeatureName = String; 24 | pub type FeaturesMap = HashMap>>; 25 | 26 | pub fn prune(is_dry_run: bool, skip_tests: bool, clean: CleanLevel, no_tmp: bool) -> Result<()> { 27 | let mut main_document = Document::new(".")?; 28 | 29 | //needed to be set here so the temp_dir lives long enough 30 | let tmp_dir = TempDir::with_suffix("cargo-features-manager")?; 31 | 32 | let mut document = if no_tmp { 33 | Document::new(".")? 34 | } else { 35 | let project_path = tmp_dir.path().join("project"); 36 | copy_dir(main_document.root_path(), &project_path)?; 37 | 38 | match Document::new(project_path) { 39 | Ok(document) => {document} 40 | Err(err) => { 41 | return Err(err.wrap_err("Failed to create the temporary project - try to use cargo `features prune --no-tmp`")) 42 | } 43 | } 44 | }; 45 | 46 | let features_to_test = get_features_to_test(&document)?; 47 | let to_be_disabled = prune_features( 48 | &mut document, 49 | skip_tests, 50 | clean, 51 | features_to_test, 52 | known_features()?, 53 | )?; 54 | 55 | if is_dry_run { 56 | return Ok(()); 57 | } 58 | 59 | for (package_name, dependency) in to_be_disabled { 60 | for (dependency_name, features) in dependency { 61 | for feature in features { 62 | main_document 63 | .get_package_mut(&package_name)? 64 | .get_dep_mut(&dependency_name)? 65 | .disable_feature(&feature)?; 66 | } 67 | 68 | save_dependency(&mut main_document, &package_name, &dependency_name)?; 69 | } 70 | } 71 | 72 | Ok(()) 73 | } 74 | 75 | //give a map of known features that do not affect completion but remove functionality 76 | pub fn known_features() -> Result>> { 77 | let file = include_str!("../../Known-Features.toml"); 78 | 79 | let document: toml_edit::DocumentMut = file.parse()?; 80 | 81 | let mut map = HashMap::new(); 82 | 83 | for (dependency, features) in document.as_table() { 84 | let features = features 85 | .as_array() 86 | .context("could not parse Known-Features.toml")?; 87 | 88 | let features = features 89 | .iter() 90 | .filter_map(|item| item.as_str()) 91 | .map(|name| name.to_string()) 92 | .collect_vec(); 93 | 94 | map.insert(dependency.to_string(), features); 95 | } 96 | 97 | Ok(map) 98 | } 99 | 100 | fn prune_features( 101 | document: &mut Document, 102 | skip_tests: bool, 103 | should_clean: CleanLevel, 104 | features: FeaturesMap, 105 | known_features: HashMap>, 106 | ) -> Result { 107 | let mut features_map = HashMap::new(); 108 | 109 | let mut has_known_features_enabled = false; 110 | 111 | let mut display = Display::new(&features, document); 112 | display.start()?; 113 | 114 | for (package_name, dependencies) in features 115 | .into_iter() 116 | .sorted_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)) 117 | { 118 | if dependencies.is_empty() { 119 | continue; 120 | } 121 | 122 | display.next_package(&package_name, &dependencies)?; 123 | 124 | for (dependency_name, features) in dependencies 125 | .into_iter() 126 | .sorted_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)) 127 | { 128 | if features.is_empty() { 129 | continue; 130 | } 131 | 132 | let mut known_features_list = vec![]; 133 | let dependency = document 134 | .get_package(&package_name)? 135 | .get_dep(&dependency_name)?; 136 | 137 | for feature_name in known_features.get(&dependency_name).unwrap_or(&vec![]) { 138 | set_features_to_be_kept( 139 | dependency, 140 | feature_name.to_string(), 141 | &mut known_features_list, 142 | ) 143 | } 144 | 145 | let mut to_be_disabled = vec![]; 146 | to_be_disabled.append(&mut known_features_list.clone()); 147 | 148 | display.next_dependency(&dependency_name, &features); 149 | 150 | for (id, feature) in features.iter().enumerate() { 151 | display.next_feature(id, feature)?; 152 | 153 | document 154 | .get_package_mut(&package_name)? 155 | .get_dep_mut(&dependency_name)? 156 | .disable_feature(feature)?; 157 | 158 | save_dependency(document, &package_name, &dependency_name)?; 159 | 160 | if !to_be_disabled.contains(feature) && check(skip_tests, document.root_path())? { 161 | set_features_to_be_disabled( 162 | document 163 | .get_package(&package_name)? 164 | .get_dep(&dependency_name)?, 165 | feature.to_string(), 166 | &mut to_be_disabled, 167 | ); 168 | } 169 | 170 | //reset to start 171 | for feature in &features { 172 | document 173 | .get_package_mut(&package_name)? 174 | .get_dep_mut(&dependency_name)? 175 | .enable_feature(feature)?; 176 | } 177 | 178 | save_dependency(document, &package_name, &dependency_name)?; 179 | 180 | display.finish_feature()?; 181 | } 182 | 183 | let features_result = features 184 | .iter() 185 | .filter(|feature| to_be_disabled.contains(feature)) 186 | .map(|feature| { 187 | if known_features_list.contains(feature) { 188 | has_known_features_enabled = true; 189 | (feature, true) 190 | } else { 191 | (feature, false) 192 | } 193 | }) 194 | .collect(); 195 | 196 | display.finish_dependency(features_result)?; 197 | 198 | if let CleanLevel::Dependency = should_clean { 199 | clean(document.root_path())?; 200 | } 201 | 202 | let to_be_disabled = to_be_disabled 203 | .into_iter() 204 | .filter(|feature| known_features_list.contains(feature).not()) 205 | .collect_vec(); 206 | 207 | features_map 208 | .entry(package_name.to_string()) 209 | .or_insert_with(HashMap::new) 210 | .insert(dependency_name, to_be_disabled); 211 | } 212 | 213 | if let CleanLevel::Package = should_clean { 214 | clean(document.root_path())?; 215 | } 216 | } 217 | 218 | if has_known_features_enabled { 219 | display.display_known_features_notice()?; 220 | } 221 | 222 | display.finish()?; 223 | 224 | Ok(features_map) 225 | } 226 | 227 | fn set_features_to_be_disabled( 228 | dependency: &Dependency, 229 | feature: String, 230 | to_be_disabled: &mut Vec, 231 | ) { 232 | if to_be_disabled.contains(&feature) { 233 | return; 234 | } 235 | 236 | to_be_disabled.push(feature.clone()); 237 | 238 | dependency 239 | .features 240 | .iter() 241 | .filter(|(_, data)| { 242 | data.sub_features 243 | .iter() 244 | .any(|sub_feature| sub_feature.name == feature) 245 | }) 246 | .for_each(|(name, _)| { 247 | set_features_to_be_disabled(dependency, name.to_string(), to_be_disabled); 248 | }); 249 | } 250 | 251 | fn set_features_to_be_kept( 252 | dependency: &Dependency, 253 | feature: String, 254 | to_be_disabled: &mut Vec, 255 | ) { 256 | if to_be_disabled.contains(&feature) { 257 | return; 258 | } 259 | 260 | to_be_disabled.push(feature.clone()); 261 | 262 | if let Some(feature) = dependency.get_feature(&feature) { 263 | for sub_feature in &feature.sub_features { 264 | set_features_to_be_kept(dependency, sub_feature.name.clone(), to_be_disabled); 265 | } 266 | } 267 | } 268 | 269 | fn clean>(path: P) -> Result<()> { 270 | let mut child = Command::new("cargo") 271 | .current_dir(path) 272 | .arg("clean") 273 | .stdout(Stdio::null()) 274 | .stderr(Stdio::null()) 275 | .spawn()?; 276 | 277 | let _ = child.wait()?.code().ok_or(eyre!("Could not clear"))?; 278 | 279 | Ok(()) 280 | } 281 | 282 | fn check>(skip_tests: bool, path: P) -> Result { 283 | if !build(&path)? { 284 | return Ok(false); 285 | } 286 | 287 | if !skip_tests && !test(&path)? { 288 | return Ok(false); 289 | } 290 | 291 | Ok(true) 292 | } 293 | 294 | fn build>(path: P) -> Result { 295 | let mut child = Command::new("cargo") 296 | .current_dir(path) 297 | .arg("build") 298 | .arg("--all-targets") 299 | .stdout(Stdio::null()) 300 | .stderr(Stdio::null()) 301 | .spawn()?; 302 | 303 | let code = child.wait()?.code().ok_or(eyre!("Could not build"))?; 304 | 305 | Ok(code == 0) 306 | } 307 | 308 | fn test>(path: P) -> Result { 309 | let mut child = Command::new("cargo") 310 | .current_dir(path) 311 | .arg("test") 312 | .arg("--workspace") 313 | .stdout(Stdio::null()) 314 | .stderr(Stdio::null()) 315 | .spawn()?; 316 | 317 | let code = child.wait()?.code().ok_or(eyre!("Could not test"))?; 318 | 319 | Ok(code == 0) 320 | } 321 | -------------------------------------------------------------------------------- /src/edit/display.rs: -------------------------------------------------------------------------------- 1 | use crate::edit::filter_view::FilterView; 2 | use crate::io::save::save_dependency; 3 | use crate::project::dependency::feature::EnabledState; 4 | use crate::project::document::Document; 5 | use color_eyre::Result; 6 | use color_eyre::eyre::{Context, ContextCompat}; 7 | use console::{Emoji, Key, Term, style}; 8 | use std::io::Write; 9 | use std::ops::{Not, Range}; 10 | 11 | pub struct Display { 12 | term: Term, 13 | 14 | document: Document, 15 | 16 | package_selector: FilterView, 17 | dep_selector: FilterView, 18 | feature_selector: FilterView, 19 | 20 | state: DisplayState, 21 | 22 | search_text: String, 23 | } 24 | 25 | impl Display { 26 | pub fn new() -> Result { 27 | let document = Document::new(".")?; 28 | 29 | Ok(Display { 30 | term: Term::buffered_stdout(), 31 | package_selector: FilterView { 32 | selected_index: 0, 33 | data: FilterView::data_from_document(&document, "")?, 34 | }, 35 | dep_selector: FilterView { 36 | selected_index: 0, 37 | data: FilterView::data_from_package(document.get_package_by_index(0)?, "")?, 38 | }, 39 | feature_selector: FilterView { 40 | selected_index: 0, 41 | data: vec![], 42 | }, 43 | state: if document.is_workspace() { 44 | DisplayState::Package 45 | } else { 46 | DisplayState::Dep 47 | }, 48 | search_text: "".to_string(), 49 | document, 50 | }) 51 | } 52 | 53 | fn select_selected_package(&mut self) -> Result<()> { 54 | self.state = DisplayState::Dep; 55 | 56 | // update selector 57 | self.dep_selector.data = FilterView::data_from_package( 58 | self.document 59 | .get_package(self.package_selector.get_selected()?.name())?, 60 | "", 61 | )?; 62 | 63 | Ok(()) 64 | } 65 | 66 | pub fn set_selected_dep(&mut self, dep_name: String) -> Result<()> { 67 | match self 68 | .document 69 | .get_package(self.package_selector.get_selected()?.name())? 70 | .get_dep_index(&dep_name) 71 | { 72 | Ok(index) => { 73 | self.dep_selector.selected_index = index; 74 | 75 | self.select_selected_dep()?; 76 | Ok(()) 77 | } 78 | Err(err) => Err(err), 79 | } 80 | } 81 | 82 | fn select_selected_dep(&mut self) -> Result<()> { 83 | self.state = DisplayState::Feature; 84 | 85 | let dep = self 86 | .document 87 | .get_package(self.package_selector.get_selected()?.name())? 88 | .get_dep(self.dep_selector.get_selected()?.name())?; 89 | 90 | // update selector 91 | self.feature_selector.data = FilterView::data_from_dependency(dep, &self.search_text); 92 | 93 | Ok(()) 94 | } 95 | 96 | pub fn start(&mut self) -> Result<()> { 97 | //setup 98 | self.term.hide_cursor()?; 99 | 100 | for _ in 1..self.term.size().0 { 101 | writeln!(self.term)?; 102 | } 103 | 104 | self.term.move_cursor_to(0, 0)?; 105 | self.term.flush()?; 106 | 107 | loop { 108 | match self.state { 109 | DisplayState::Dep => self.display_deps()?, 110 | DisplayState::Feature => self.display_features()?, 111 | DisplayState::Package => self.display_packages()?, 112 | } 113 | 114 | self.term.flush()?; 115 | 116 | //clear previous screen 117 | self.term.clear_last_lines(self.term.size().0 as usize)?; 118 | if let RunningState::Finished = self.input_event()? { 119 | break; 120 | } 121 | } 122 | 123 | self.term.show_cursor()?; 124 | self.term.flush()?; 125 | 126 | Ok(()) 127 | } 128 | 129 | fn display_packages(&mut self) -> Result<()> { 130 | write!(self.term, "Packages")?; 131 | self.display_search_header()?; 132 | 133 | let dep_range = self.get_max_range()?; 134 | 135 | let mut line_index = 1; 136 | let mut index = dep_range.start; 137 | 138 | for selected in &self.package_selector.data[dep_range] { 139 | if index == self.package_selector.selected_index { 140 | self.term.move_cursor_to(0, line_index)?; 141 | write!(self.term, ">")?; 142 | } 143 | 144 | self.term.move_cursor_to(2, line_index)?; 145 | write!(self.term, "{}", selected.display_name())?; 146 | 147 | index += 1; 148 | line_index += 1; 149 | } 150 | 151 | Ok(()) 152 | } 153 | 154 | fn display_deps(&mut self) -> Result<()> { 155 | write!(self.term, "Dependencies")?; 156 | self.display_search_header()?; 157 | 158 | let dep_range = self.get_max_range()?; 159 | 160 | let mut line_index = 1; 161 | let mut index = dep_range.start; 162 | 163 | for selector in &self.dep_selector.data[dep_range] { 164 | if index == self.dep_selector.selected_index { 165 | self.term.move_cursor_to(0, line_index)?; 166 | write!(self.term, ">")?; 167 | } 168 | 169 | self.term.move_cursor_to(2, line_index)?; 170 | 171 | write!(self.term, "{}", selector.display_name())?; 172 | 173 | index += 1; 174 | line_index += 1; 175 | } 176 | 177 | Ok(()) 178 | } 179 | 180 | fn display_features(&mut self) -> Result<()> { 181 | let dep = self 182 | .document 183 | .get_package(self.package_selector.get_selected()?.name())? 184 | .get_dep(self.dep_selector.get_selected()?.name()) 185 | .context(format!( 186 | "couldn't find {}", 187 | self.dep_selector.get_selected()?.name() 188 | ))?; 189 | 190 | let feature_range = self.get_max_range()?; 191 | 192 | let mut line_index = 1; 193 | let mut index = feature_range.start; 194 | 195 | write!(self.term, "{} {}", dep.get_name(), dep.get_version())?; 196 | 197 | self.display_search_header()?; 198 | 199 | let dep = self 200 | .document 201 | .get_package(self.package_selector.get_selected()?.name())? 202 | .get_dep(self.dep_selector.get_selected()?.name()) 203 | .context(format!( 204 | "could not find {}", 205 | self.dep_selector.get_selected()?.name() 206 | ))?; 207 | 208 | for feature in &self.feature_selector.data[self.get_max_range()?] { 209 | let data = dep 210 | .get_feature(feature.name()) 211 | .context(format!("couldn't find {}", feature.name()))?; 212 | 213 | self.term.move_cursor_to(2, line_index)?; 214 | 215 | let marker = match data.enabled_state { 216 | EnabledState::Normal(is_enabled) => { 217 | if is_enabled { 218 | "[X]".to_string() 219 | } else { 220 | "[ ]".to_string() 221 | } 222 | } 223 | EnabledState::Workspace => format!("{}", Emoji("🗃️", "W")), 224 | }; 225 | 226 | if data.is_default { 227 | write!(self.term, "{}", style(marker).green())?; 228 | } else { 229 | write!(self.term, "{}", marker)?; 230 | } 231 | 232 | let mut feature_name = style(feature.display_name()); 233 | 234 | if !dep 235 | .get_currently_dependent_features(feature.name()) 236 | .is_empty() 237 | || data.enabled_state == EnabledState::Workspace 238 | { 239 | //gray 240 | feature_name = feature_name.color256(8); 241 | } 242 | 243 | self.term.move_cursor_right(1)?; 244 | write!(self.term, "{}", feature_name)?; 245 | 246 | if index == self.feature_selector.selected_index { 247 | self.term.move_cursor_to(0, line_index)?; 248 | write!(self.term, ">")?; 249 | 250 | let sub_features = &data.sub_features; 251 | 252 | if sub_features.is_empty().not() { 253 | line_index += 1; 254 | 255 | self.term.move_cursor_to(6, line_index)?; 256 | write!(self.term, "└")?; 257 | 258 | self.term.move_cursor_to(8, line_index)?; 259 | 260 | for sub in sub_features { 261 | write!(self.term, "{} ", sub)?; 262 | } 263 | } 264 | } 265 | 266 | line_index += 1; 267 | index += 1; 268 | } 269 | 270 | Ok(()) 271 | } 272 | 273 | fn display_search_header(&mut self) -> Result<()> { 274 | if !self.search_text.is_empty() { 275 | write!(self.term, " - {}", self.search_text)?; 276 | } 277 | 278 | Ok(()) 279 | } 280 | 281 | fn input_event(&mut self) -> Result { 282 | match (self.term.read_key()?, &self.state) { 283 | //movement 284 | //up 285 | (Key::ArrowUp, DisplayState::Package) => { 286 | self.package_selector.shift(-1); 287 | } 288 | (Key::ArrowUp, DisplayState::Dep) => { 289 | self.dep_selector.shift(-1); 290 | } 291 | (Key::ArrowUp, DisplayState::Feature) => { 292 | if self.feature_selector.has_data() { 293 | self.feature_selector.shift(-1); 294 | } 295 | } 296 | //down 297 | (Key::ArrowDown, DisplayState::Package) => { 298 | self.package_selector.shift(1); 299 | } 300 | (Key::ArrowDown, DisplayState::Dep) => { 301 | self.dep_selector.shift(1); 302 | } 303 | (Key::ArrowDown, DisplayState::Feature) => { 304 | if self.feature_selector.has_data() { 305 | self.feature_selector.shift(1); 306 | } 307 | } 308 | 309 | //selection 310 | (Key::Enter, DisplayState::Package) 311 | | (Key::ArrowRight, DisplayState::Package) 312 | | (Key::Char(' '), DisplayState::Package) => { 313 | if self.package_selector.has_data() { 314 | let name = self.package_selector.get_selected()?.name(); 315 | 316 | if !self 317 | .document 318 | .get_package(name) 319 | .context(format!("package not found - {}", name))? 320 | .dependencies 321 | .is_empty() 322 | { 323 | self.search_text = "".to_string(); 324 | 325 | self.select_selected_package()?; 326 | 327 | //needed to wrap 328 | self.dep_selector.shift(0); 329 | } 330 | } 331 | } 332 | (Key::Enter, DisplayState::Dep) 333 | | (Key::ArrowRight, DisplayState::Dep) 334 | | (Key::Char(' '), DisplayState::Dep) => { 335 | if self.dep_selector.has_data() 336 | && self 337 | .document 338 | .get_package(self.package_selector.get_selected()?.name())? 339 | .get_dep(self.dep_selector.get_selected()?.name())? 340 | .has_features() 341 | { 342 | self.search_text = "".to_string(); 343 | 344 | self.select_selected_dep()?; 345 | 346 | //needed to wrap 347 | self.feature_selector.shift(0); 348 | } 349 | } 350 | (Key::Enter, DisplayState::Feature) 351 | | (Key::ArrowRight, DisplayState::Feature) 352 | | (Key::Char(' '), DisplayState::Feature) => { 353 | if self.feature_selector.has_data() { 354 | let dep_name = self.dep_selector.get_selected()?.name(); 355 | 356 | let dep = self 357 | .document 358 | .get_package_mut(self.package_selector.get_selected()?.name())? 359 | .get_dep_mut(dep_name)?; 360 | 361 | dep.toggle_feature(self.feature_selector.get_selected()?.name())?; 362 | 363 | save_dependency( 364 | &mut self.document, 365 | self.package_selector.get_selected()?.name(), 366 | dep_name, 367 | )?; 368 | } 369 | } 370 | 371 | //search 372 | (Key::Char(char), _) => { 373 | if char == ' ' { 374 | return Ok(RunningState::Running); 375 | } 376 | 377 | self.search_text += char.to_string().as_str(); 378 | 379 | self.update_selected_data()?; 380 | 381 | match self.state { 382 | DisplayState::Dep => self.dep_selector.shift(0), 383 | DisplayState::Feature => self.feature_selector.shift(0), 384 | DisplayState::Package => self.package_selector.shift(0), 385 | } 386 | } 387 | (Key::Backspace, _) => { 388 | let _ = self.search_text.pop(); 389 | 390 | self.update_selected_data()?; 391 | } 392 | 393 | //back 394 | (Key::Escape, _) | (Key::ArrowLeft, _) => { 395 | return self.move_back(); 396 | } 397 | 398 | _ => {} 399 | } 400 | 401 | Ok(RunningState::Running) 402 | } 403 | 404 | fn get_max_range(&self) -> Result> { 405 | let current_selected = match self.state { 406 | DisplayState::Dep => self.dep_selector.selected_index, 407 | DisplayState::Feature => self.feature_selector.selected_index, 408 | DisplayState::Package => self.package_selector.selected_index, 409 | } as isize; 410 | 411 | let max_range = match self.state { 412 | DisplayState::Dep => self.dep_selector.data.len(), 413 | DisplayState::Feature => self.feature_selector.data.len(), 414 | DisplayState::Package => self.package_selector.data.len(), 415 | }; 416 | 417 | let mut offset = 0; 418 | 419 | if let DisplayState::Feature = self.state { 420 | if self.feature_selector.has_data() { 421 | let dep = self 422 | .document 423 | .get_package(self.package_selector.get_selected()?.name())? 424 | .get_dep(self.dep_selector.get_selected()?.name())?; 425 | 426 | let feature = self.feature_selector.get_selected()?; 427 | let data = dep 428 | .get_feature(feature.name()) 429 | .context(format!("coundt find {}", feature.name()))?; 430 | 431 | if !data.sub_features.is_empty() { 432 | offset = 1; 433 | } 434 | } 435 | } 436 | 437 | let height = self.term.size().0 as usize; 438 | 439 | let start = (current_selected - height as isize / 2 + 1) 440 | .min(max_range as isize - height as isize + 1 + offset as isize) 441 | .max(0) as usize; 442 | 443 | Ok(start..max_range.min(start + height - 1 - offset)) 444 | } 445 | 446 | fn update_selected_data(&mut self) -> Result<()> { 447 | match self.state { 448 | DisplayState::Package => { 449 | self.package_selector.data = 450 | FilterView::data_from_document(&self.document, &self.search_text)?; 451 | } 452 | DisplayState::Dep => { 453 | let package = self 454 | .document 455 | .get_package(self.package_selector.get_selected()?.name())?; 456 | 457 | self.dep_selector.data = FilterView::data_from_package(package, &self.search_text)?; 458 | } 459 | DisplayState::Feature => { 460 | let dep = self 461 | .document 462 | .get_package(self.package_selector.get_selected()?.name())? 463 | .get_dep(self.dep_selector.get_selected()?.name())?; 464 | 465 | self.feature_selector.data = 466 | FilterView::data_from_dependency(dep, &self.search_text); 467 | } 468 | } 469 | 470 | Ok(()) 471 | } 472 | 473 | fn move_back(&mut self) -> Result { 474 | match self.state { 475 | DisplayState::Package => Ok(RunningState::Finished), 476 | DisplayState::Dep => { 477 | if !self.document.is_workspace() { 478 | return Ok(RunningState::Finished); 479 | } 480 | 481 | self.search_text = "".to_string(); 482 | 483 | self.state = DisplayState::Package; 484 | 485 | self.update_selected_data()?; 486 | Ok(RunningState::Running) 487 | } 488 | DisplayState::Feature => { 489 | self.search_text = "".to_string(); 490 | 491 | self.state = DisplayState::Dep; 492 | 493 | self.update_selected_data()?; 494 | Ok(RunningState::Running) 495 | } 496 | } 497 | } 498 | } 499 | 500 | enum RunningState { 501 | Running, 502 | Finished, 503 | } 504 | 505 | enum DisplayState { 506 | Package, 507 | Dep, 508 | Feature, 509 | } 510 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.25.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.21" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.13" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.7" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.4" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 55 | dependencies = [ 56 | "windows-sys 0.60.2", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.10" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 64 | dependencies = [ 65 | "anstyle", 66 | "once_cell_polyfill", 67 | "windows-sys 0.60.2", 68 | ] 69 | 70 | [[package]] 71 | name = "backtrace" 72 | version = "0.3.76" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" 75 | dependencies = [ 76 | "addr2line", 77 | "cfg-if", 78 | "libc", 79 | "miniz_oxide", 80 | "object", 81 | "rustc-demangle", 82 | "windows-link", 83 | ] 84 | 85 | [[package]] 86 | name = "bitflags" 87 | version = "2.9.4" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 90 | 91 | [[package]] 92 | name = "camino" 93 | version = "1.2.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" 96 | dependencies = [ 97 | "serde_core", 98 | ] 99 | 100 | [[package]] 101 | name = "cargo-features-manager" 102 | version = "0.10.2" 103 | dependencies = [ 104 | "cargo_metadata", 105 | "clap", 106 | "clap_complete", 107 | "color-eyre", 108 | "console", 109 | "copy_dir", 110 | "ctrlc", 111 | "fuzzy-matcher", 112 | "itertools", 113 | "semver", 114 | "tempfile", 115 | "toml_edit", 116 | ] 117 | 118 | [[package]] 119 | name = "cargo-platform" 120 | version = "0.3.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "8abf5d501fd757c2d2ee78d0cc40f606e92e3a63544420316565556ed28485e2" 123 | dependencies = [ 124 | "serde", 125 | ] 126 | 127 | [[package]] 128 | name = "cargo_metadata" 129 | version = "0.23.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "981a6f317983eec002839b90fae7411a85621410ae591a9cab2ecf5cb5744873" 132 | dependencies = [ 133 | "camino", 134 | "cargo-platform", 135 | "semver", 136 | "serde", 137 | "serde_json", 138 | "thiserror", 139 | ] 140 | 141 | [[package]] 142 | name = "cfg-if" 143 | version = "1.0.4" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 146 | 147 | [[package]] 148 | name = "cfg_aliases" 149 | version = "0.2.1" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 152 | 153 | [[package]] 154 | name = "clap" 155 | version = "4.5.49" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" 158 | dependencies = [ 159 | "clap_builder", 160 | "clap_derive", 161 | ] 162 | 163 | [[package]] 164 | name = "clap_builder" 165 | version = "4.5.49" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" 168 | dependencies = [ 169 | "anstream", 170 | "anstyle", 171 | "clap_lex", 172 | "strsim", 173 | ] 174 | 175 | [[package]] 176 | name = "clap_complete" 177 | version = "4.5.59" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "2348487adcd4631696ced64ccdb40d38ac4d31cae7f2eec8817fcea1b9d1c43c" 180 | dependencies = [ 181 | "clap", 182 | ] 183 | 184 | [[package]] 185 | name = "clap_derive" 186 | version = "4.5.49" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" 189 | dependencies = [ 190 | "heck", 191 | "proc-macro2", 192 | "quote", 193 | "syn", 194 | ] 195 | 196 | [[package]] 197 | name = "clap_lex" 198 | version = "0.7.6" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" 201 | 202 | [[package]] 203 | name = "color-eyre" 204 | version = "0.6.5" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" 207 | dependencies = [ 208 | "backtrace", 209 | "color-spantrace", 210 | "eyre", 211 | "indenter", 212 | "once_cell", 213 | "owo-colors", 214 | "tracing-error", 215 | ] 216 | 217 | [[package]] 218 | name = "color-spantrace" 219 | version = "0.3.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" 222 | dependencies = [ 223 | "once_cell", 224 | "owo-colors", 225 | "tracing-core", 226 | "tracing-error", 227 | ] 228 | 229 | [[package]] 230 | name = "colorchoice" 231 | version = "1.0.4" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 234 | 235 | [[package]] 236 | name = "console" 237 | version = "0.16.1" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" 240 | dependencies = [ 241 | "encode_unicode", 242 | "libc", 243 | "once_cell", 244 | "windows-sys 0.61.2", 245 | ] 246 | 247 | [[package]] 248 | name = "copy_dir" 249 | version = "0.1.3" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3" 252 | dependencies = [ 253 | "walkdir", 254 | ] 255 | 256 | [[package]] 257 | name = "ctrlc" 258 | version = "3.5.0" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3" 261 | dependencies = [ 262 | "dispatch", 263 | "nix", 264 | "windows-sys 0.61.2", 265 | ] 266 | 267 | [[package]] 268 | name = "dispatch" 269 | version = "0.2.0" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" 272 | 273 | [[package]] 274 | name = "either" 275 | version = "1.15.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 278 | 279 | [[package]] 280 | name = "encode_unicode" 281 | version = "1.0.0" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 284 | 285 | [[package]] 286 | name = "equivalent" 287 | version = "1.0.2" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 290 | 291 | [[package]] 292 | name = "errno" 293 | version = "0.3.14" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 296 | dependencies = [ 297 | "libc", 298 | "windows-sys 0.61.2", 299 | ] 300 | 301 | [[package]] 302 | name = "eyre" 303 | version = "0.6.12" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 306 | dependencies = [ 307 | "indenter", 308 | "once_cell", 309 | ] 310 | 311 | [[package]] 312 | name = "fastrand" 313 | version = "2.3.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 316 | 317 | [[package]] 318 | name = "fuzzy-matcher" 319 | version = "0.3.7" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" 322 | dependencies = [ 323 | "thread_local", 324 | ] 325 | 326 | [[package]] 327 | name = "gimli" 328 | version = "0.32.3" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" 331 | 332 | [[package]] 333 | name = "hashbrown" 334 | version = "0.16.0" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" 337 | 338 | [[package]] 339 | name = "heck" 340 | version = "0.5.0" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 343 | 344 | [[package]] 345 | name = "indenter" 346 | version = "0.3.4" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" 349 | 350 | [[package]] 351 | name = "indexmap" 352 | version = "2.12.0" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" 355 | dependencies = [ 356 | "equivalent", 357 | "hashbrown", 358 | ] 359 | 360 | [[package]] 361 | name = "is_terminal_polyfill" 362 | version = "1.70.1" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 365 | 366 | [[package]] 367 | name = "itertools" 368 | version = "0.14.0" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 371 | dependencies = [ 372 | "either", 373 | ] 374 | 375 | [[package]] 376 | name = "itoa" 377 | version = "1.0.15" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 380 | 381 | [[package]] 382 | name = "lazy_static" 383 | version = "1.5.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 386 | 387 | [[package]] 388 | name = "libc" 389 | version = "0.2.177" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 392 | 393 | [[package]] 394 | name = "linux-raw-sys" 395 | version = "0.11.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 398 | 399 | [[package]] 400 | name = "memchr" 401 | version = "2.7.6" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 404 | 405 | [[package]] 406 | name = "miniz_oxide" 407 | version = "0.8.9" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 410 | dependencies = [ 411 | "adler2", 412 | ] 413 | 414 | [[package]] 415 | name = "nix" 416 | version = "0.30.1" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 419 | dependencies = [ 420 | "bitflags", 421 | "cfg-if", 422 | "cfg_aliases", 423 | "libc", 424 | ] 425 | 426 | [[package]] 427 | name = "object" 428 | version = "0.37.3" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" 431 | dependencies = [ 432 | "memchr", 433 | ] 434 | 435 | [[package]] 436 | name = "once_cell" 437 | version = "1.21.3" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 440 | 441 | [[package]] 442 | name = "once_cell_polyfill" 443 | version = "1.70.1" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 446 | 447 | [[package]] 448 | name = "owo-colors" 449 | version = "4.2.3" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" 452 | 453 | [[package]] 454 | name = "pin-project-lite" 455 | version = "0.2.16" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 458 | 459 | [[package]] 460 | name = "proc-macro2" 461 | version = "1.0.101" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 464 | dependencies = [ 465 | "unicode-ident", 466 | ] 467 | 468 | [[package]] 469 | name = "quote" 470 | version = "1.0.41" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 473 | dependencies = [ 474 | "proc-macro2", 475 | ] 476 | 477 | [[package]] 478 | name = "rustc-demangle" 479 | version = "0.1.26" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 482 | 483 | [[package]] 484 | name = "rustix" 485 | version = "1.1.2" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 488 | dependencies = [ 489 | "bitflags", 490 | "errno", 491 | "libc", 492 | "linux-raw-sys", 493 | "windows-sys 0.61.2", 494 | ] 495 | 496 | [[package]] 497 | name = "ryu" 498 | version = "1.0.20" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 501 | 502 | [[package]] 503 | name = "same-file" 504 | version = "1.0.6" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 507 | dependencies = [ 508 | "winapi-util", 509 | ] 510 | 511 | [[package]] 512 | name = "semver" 513 | version = "1.0.27" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 516 | dependencies = [ 517 | "serde", 518 | "serde_core", 519 | ] 520 | 521 | [[package]] 522 | name = "serde" 523 | version = "1.0.228" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 526 | dependencies = [ 527 | "serde_core", 528 | "serde_derive", 529 | ] 530 | 531 | [[package]] 532 | name = "serde_core" 533 | version = "1.0.228" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 536 | dependencies = [ 537 | "serde_derive", 538 | ] 539 | 540 | [[package]] 541 | name = "serde_derive" 542 | version = "1.0.228" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 545 | dependencies = [ 546 | "proc-macro2", 547 | "quote", 548 | "syn", 549 | ] 550 | 551 | [[package]] 552 | name = "serde_json" 553 | version = "1.0.145" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 556 | dependencies = [ 557 | "itoa", 558 | "memchr", 559 | "ryu", 560 | "serde", 561 | "serde_core", 562 | ] 563 | 564 | [[package]] 565 | name = "sharded-slab" 566 | version = "0.1.7" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 569 | dependencies = [ 570 | "lazy_static", 571 | ] 572 | 573 | [[package]] 574 | name = "strsim" 575 | version = "0.11.1" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 578 | 579 | [[package]] 580 | name = "syn" 581 | version = "2.0.107" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" 584 | dependencies = [ 585 | "proc-macro2", 586 | "quote", 587 | "unicode-ident", 588 | ] 589 | 590 | [[package]] 591 | name = "tempfile" 592 | version = "3.23.0" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" 595 | dependencies = [ 596 | "fastrand", 597 | "once_cell", 598 | "rustix", 599 | "windows-sys 0.61.2", 600 | ] 601 | 602 | [[package]] 603 | name = "thiserror" 604 | version = "2.0.17" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 607 | dependencies = [ 608 | "thiserror-impl", 609 | ] 610 | 611 | [[package]] 612 | name = "thiserror-impl" 613 | version = "2.0.17" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 616 | dependencies = [ 617 | "proc-macro2", 618 | "quote", 619 | "syn", 620 | ] 621 | 622 | [[package]] 623 | name = "thread_local" 624 | version = "1.1.9" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 627 | dependencies = [ 628 | "cfg-if", 629 | ] 630 | 631 | [[package]] 632 | name = "toml_datetime" 633 | version = "0.7.3" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" 636 | dependencies = [ 637 | "serde_core", 638 | ] 639 | 640 | [[package]] 641 | name = "toml_edit" 642 | version = "0.23.7" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" 645 | dependencies = [ 646 | "indexmap", 647 | "toml_datetime", 648 | "toml_parser", 649 | "toml_writer", 650 | "winnow", 651 | ] 652 | 653 | [[package]] 654 | name = "toml_parser" 655 | version = "1.0.4" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" 658 | dependencies = [ 659 | "winnow", 660 | ] 661 | 662 | [[package]] 663 | name = "toml_writer" 664 | version = "1.0.4" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" 667 | 668 | [[package]] 669 | name = "tracing" 670 | version = "0.1.41" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 673 | dependencies = [ 674 | "pin-project-lite", 675 | "tracing-core", 676 | ] 677 | 678 | [[package]] 679 | name = "tracing-core" 680 | version = "0.1.34" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 683 | dependencies = [ 684 | "once_cell", 685 | "valuable", 686 | ] 687 | 688 | [[package]] 689 | name = "tracing-error" 690 | version = "0.2.1" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" 693 | dependencies = [ 694 | "tracing", 695 | "tracing-subscriber", 696 | ] 697 | 698 | [[package]] 699 | name = "tracing-subscriber" 700 | version = "0.3.20" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" 703 | dependencies = [ 704 | "sharded-slab", 705 | "thread_local", 706 | "tracing-core", 707 | ] 708 | 709 | [[package]] 710 | name = "unicode-ident" 711 | version = "1.0.19" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 714 | 715 | [[package]] 716 | name = "utf8parse" 717 | version = "0.2.2" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 720 | 721 | [[package]] 722 | name = "valuable" 723 | version = "0.1.1" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 726 | 727 | [[package]] 728 | name = "walkdir" 729 | version = "2.5.0" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 732 | dependencies = [ 733 | "same-file", 734 | "winapi-util", 735 | ] 736 | 737 | [[package]] 738 | name = "winapi-util" 739 | version = "0.1.11" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 742 | dependencies = [ 743 | "windows-sys 0.61.2", 744 | ] 745 | 746 | [[package]] 747 | name = "windows-link" 748 | version = "0.2.1" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 751 | 752 | [[package]] 753 | name = "windows-sys" 754 | version = "0.60.2" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 757 | dependencies = [ 758 | "windows-targets", 759 | ] 760 | 761 | [[package]] 762 | name = "windows-sys" 763 | version = "0.61.2" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 766 | dependencies = [ 767 | "windows-link", 768 | ] 769 | 770 | [[package]] 771 | name = "windows-targets" 772 | version = "0.53.5" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 775 | dependencies = [ 776 | "windows-link", 777 | "windows_aarch64_gnullvm", 778 | "windows_aarch64_msvc", 779 | "windows_i686_gnu", 780 | "windows_i686_gnullvm", 781 | "windows_i686_msvc", 782 | "windows_x86_64_gnu", 783 | "windows_x86_64_gnullvm", 784 | "windows_x86_64_msvc", 785 | ] 786 | 787 | [[package]] 788 | name = "windows_aarch64_gnullvm" 789 | version = "0.53.1" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 792 | 793 | [[package]] 794 | name = "windows_aarch64_msvc" 795 | version = "0.53.1" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 798 | 799 | [[package]] 800 | name = "windows_i686_gnu" 801 | version = "0.53.1" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 804 | 805 | [[package]] 806 | name = "windows_i686_gnullvm" 807 | version = "0.53.1" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 810 | 811 | [[package]] 812 | name = "windows_i686_msvc" 813 | version = "0.53.1" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 816 | 817 | [[package]] 818 | name = "windows_x86_64_gnu" 819 | version = "0.53.1" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 822 | 823 | [[package]] 824 | name = "windows_x86_64_gnullvm" 825 | version = "0.53.1" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 828 | 829 | [[package]] 830 | name = "windows_x86_64_msvc" 831 | version = "0.53.1" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 834 | 835 | [[package]] 836 | name = "winnow" 837 | version = "0.7.13" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 840 | dependencies = [ 841 | "memchr", 842 | ] 843 | --------------------------------------------------------------------------------