list_features/
lib.rs

1//! Returns the list of enable features when building your crate.
2//!
3//! The [`list_enabled_as_string`] function lists enabled features during the Cargo build process,
4//! in a format that can be directly saved in build artifacts, which can be then included
5//! elsewhere in the program and read at run time.
6//! 
7//! Other functions are made available in case you prefer obtaining intermediate data
8//! or want to provide more parameters, but they’re probably not what you’re looking for.
9//! 
10//! # Examples
11//!
12//! See the example included with the [`list_enabled_as_string`] function and the
13//! [example crate](https://framagit.org/dder/list-features/-/tree/master/example_crate)
14
15
16use std::collections::HashSet;
17use std::io::{self, BufRead};
18use std::fmt::Write;
19
20
21/// Returns the list of enabled features as a `Vec<String>`.
22/// 
23/// Reads from `std::env::vars` and filters the features based on those listed in `Cargo.toml`.
24/// This function should only be called in build scripts or code executed during a Cargo build process, as
25/// the required `CARGO_FEATURE_*` environment variables will be missing otherwise.
26/// 
27/// Unless its output format doesn’t suit you, you’ll probably want to use [`list_enabled_as_string`] instead.
28/// 
29/// See also [`list_enabled_with_path`].
30/// 
31/// # Panics
32/// 
33/// Panics if the `Cargo.toml` file cannot be read.
34/// 
35/// # Returns
36///
37/// A `Vec<String>` containing the names of the enabled features, ordered with `default` first and then sorted alphabetically.
38pub fn list_enabled() -> Vec<String> {
39  list_enabled_with_path("Cargo.toml")
40}
41
42/// Returns the list of enabled features as a `Vec<String>`.
43/// 
44/// Same as [`list_enabled`] but allows specifying a custom path to `Cargo.toml`.
45/// 
46/// # Panics
47/// 
48/// Panics if the specified file cannot be read.
49/// 
50/// # Arguments
51///
52/// * `cargo_toml_path` - Path to the `Cargo.toml` file
53/// 
54/// # Returns
55///
56/// A `Vec<String>` containing the names of the enabled features, ordered with `default` first and then sorted alphabetically.
57pub fn list_enabled_with_path(cargo_toml_path: &str) -> Vec<String> {
58  let all_features = list_all(cargo_toml_path).unwrap();
59  list_enabled_among(&all_features)
60}
61
62/// Generates a constant declaration containing enabled Cargo features.
63/// 
64/// It’s a wrapper around [`list_enabled`] that provides a `String` that should be usable as is in an output file of the build script.
65/// This function should only be called in build scripts or code executed during a Cargo build process, as
66/// the required `CARGO_FEATURE_*` environment variables will be missing otherwise.
67/// 
68/// See also [`list_enabled_as_string_with_path`].
69/// 
70/// # Panics
71/// 
72/// Panics if the `Cargo.toml` file cannot be read.
73/// 
74/// # Arguments
75///
76/// * `const_name` - Name of the constant to generate.
77/// 
78/// # Returns
79/// A `String` containing the code for the constant declaration, like:
80/// ```
81/// String::from(r#"pub const CONST_NAME: &[&str] = &[
82/// "feature1",
83/// "feature2",
84/// ];"#);
85/// ```
86/// 
87/// # Examples
88///
89/// ```ignore
90/// // in build.rs
91/// let out_dir = std::env::var("OUT_DIR").unwrap();
92/// let file_path = format!("{out_dir}/build_info.rs");
93/// let features = list_features::list_enabled_as_string("ENABLED_FEATURES");
94/// std::fs::write(file_path, features).unwrap();
95///
96/// // in main.rs
97/// include!(concat!(env!("OUT_DIR"), "/build_info.rs"));
98/// for feature in ENABLED_FEATURES {
99///   println!(output, " {feature}");
100/// }
101/// ```
102pub fn list_enabled_as_string(const_name: &str) -> String {
103  list_enabled_as_string_with_path(const_name, "Cargo.toml")
104}
105
106/// Generates a constant declaration containing enabled Cargo features.
107/// 
108/// Same as [`list_enabled_as_string`] but allows specifying a custom path to `Cargo.toml`.
109/// 
110/// # Panics
111/// 
112/// Panics if the specified file cannot be read.
113/// 
114/// # Arguments  
115/// * `const_name` - Name of the constant to generate
116/// * `cargo_toml_path` - Path to the `Cargo.toml` file
117pub fn list_enabled_as_string_with_path(const_name: &str, cargo_toml_path: &str) -> String {
118  let enabled_features = list_enabled_with_path(cargo_toml_path);
119  let mut buf = String::new();
120  writeln!(buf, "pub const {const_name}: &[&str] = &[").unwrap();
121  for feature in enabled_features {
122    writeln!(buf, r#""{feature}","#).unwrap();
123  }
124  writeln!(buf, "];").unwrap();
125  buf
126}
127
128/// Parses a `Cargo.toml` file and returns the set of declared feature names.
129/// 
130/// Only the `[features]` section is considered. While it should be able handle reasonable edge cases, this function also tries to
131/// keep things simple and is not a replacement for a full parser such as the [toml crate](https://crates.io/crates/toml).
132///
133/// # Arguments
134///
135/// * `cargo_toml_path` - Path to the `Cargo.toml` file used as the source for the available features list.
136/// 
137/// # Returns
138///
139/// A `HashSet<String>` containing the names of the declared features.
140pub fn list_all<S: AsRef<str>>(cargo_toml_path: S) -> Result<HashSet<String>, io::Error> {
141  let file = std::fs::File::open(cargo_toml_path.as_ref())?;
142  let reader = io::BufReader::new(file);
143  let lines: Result<Vec<String>, io::Error> = reader.lines().collect();
144  let lines = lines?;
145  Ok(parse_feature_keys_from_lines(lines))
146}
147
148// Core parser logic that works on any line iterator.
149fn parse_feature_keys_from_lines<I>(lines: I) -> HashSet<String>
150where
151  I: IntoIterator<Item = String>,
152{
153  let mut in_features = false;
154  let mut features = HashSet::new();
155
156  for line in lines {
157    let stripped = line.split('#').next().unwrap_or("").trim();
158
159    if stripped.starts_with('[') {
160      in_features = stripped == "[features]";
161      continue;
162    }
163
164    if in_features && !stripped.is_empty() {
165      if let Some((key, _)) = stripped.split_once('=') {
166        let key = key.trim().trim_matches('"');
167        if !key.is_empty() {
168          features.insert(key.to_string());
169        }
170      }
171    }
172  }
173
174  features
175}
176#[cfg(feature = "test")]
177pub fn test_parse_feature_keys_from_lines<I>(lines: I) -> HashSet<String>
178where
179  I: IntoIterator<Item = String>,
180{
181  parse_feature_keys_from_lines(lines)
182}
183
184// Returns the list of enabled features that are present in `all_features`.
185//
186// This reads from `std::env::vars` and filters against the provided set.
187// It should only be called in build scripts or code executed during a Cargo build.
188fn list_enabled_among(all_features: &std::collections::HashSet<String>) -> Vec<String> {
189  let normalize = |s: &str| s.to_lowercase().replace('_', "-");
190
191  let mut enabled: Vec<String> = std::env::vars()
192    .filter_map(|(k, _)| {
193      if let Some(name) = k.strip_prefix("CARGO_FEATURE_") {
194        let norm_name = normalize(name);
195        if let Some(matched) = all_features
196          .iter()
197          .find(|feat| normalize(feat) == norm_name)
198        {
199          return Some(matched.clone());
200        }
201      }
202      None
203    })
204    .collect();
205  
206  // reorder and put default at front
207  enabled.sort();
208  if let Some(pos) = enabled.iter().position(|f| f == "default") {
209    let default_feature = enabled.remove(pos);
210    enabled.insert(0, default_feature);
211  }
212
213  enabled
214}
215#[cfg(feature = "test")]
216pub fn test_list_enabled_among(all_features: &std::collections::HashSet<String>) -> Vec<String> {
217  list_enabled_among(all_features)
218}