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