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}