Accept Crubit features on the command line, and thread them through to the bindings generator.

PiperOrigin-RevId: 665967698
Change-Id: I45453129bbb717717885504496d0390d5bcd75c7
diff --git a/cc_bindings_from_rs/BUILD b/cc_bindings_from_rs/BUILD
index d94ddfa..3e8c7c0 100644
--- a/cc_bindings_from_rs/BUILD
+++ b/cc_bindings_from_rs/BUILD
@@ -23,8 +23,10 @@
         ":toposort",
         "//common:arc_anyhow",
         "//common:code_gen_utils",
+        "//common:crubit_feature",
         "//common:error_report",
         "//common:memoized",
+        "@crate_index//:flagset",
         "@crate_index//:itertools",
         "@crate_index//:proc-macro2",
         "@crate_index//:quote",
@@ -58,9 +60,11 @@
         ":run_compiler",
         "//common:arc_anyhow",
         "//common:code_gen_utils",
+        "//common:crubit_feature",
         "//common:error_report",
         "//common:token_stream_printer",
         "@crate_index//:clap",
+        "@crate_index//:flagset",
         "@crate_index//:itertools",
     ],
 )
@@ -95,8 +99,10 @@
     ],
     rustc_flags = ["-Zallow-features=rustc_private"],
     deps = [
+        "//common:crubit_feature",
         "@crate_index//:anyhow",
         "@crate_index//:clap",
+        "@crate_index//:flagset",
     ],
 )
 
diff --git a/cc_bindings_from_rs/bindings.rs b/cc_bindings_from_rs/bindings.rs
index a0da3b3..963c109 100644
--- a/cc_bindings_from_rs/bindings.rs
+++ b/cc_bindings_from_rs/bindings.rs
@@ -67,6 +67,12 @@
         #[input]
         fn crate_name_to_include_paths(&self) -> Rc<HashMap<Rc<str>, Vec<CcInclude>>>;
 
+        /// A map from a crate name to the features enabled on that crate.
+        // TODO(b/271857814): A crate name might not be globally unique - the key needs to also cover
+        // a "hash" of the crate version and compilation flags.
+        #[input]
+        fn crate_name_to_features(&self) -> Rc<HashMap<Rc<str>, flagset::FlagSet<crubit_feature::CrubitFeature>>>;
+
         /// Error collector for generating reports of errors encountered during the generation of bindings.
         #[input]
         fn errors(&self) -> Rc<dyn ErrorReporting>;
@@ -74,10 +80,6 @@
         #[input]
         fn no_thunk_name_mangling(&self) -> bool;
 
-        // TODO(b/262878759): Provide a set of enabled/disabled Crubit features.
-        #[input]
-        fn _features(&self) -> ();
-
         fn support_header(&self, suffix: &'tcx str) -> CcInclude;
 
         fn repr_attrs(&self, did: DefId) -> Rc<[rustc_attr::ReprAttr]>;
@@ -9262,9 +9264,9 @@
             tcx,
             /* crubit_support_path_format= */ "<crubit/support/for/tests/{header}>".into(),
             /* crate_name_to_include_paths= */ Default::default(),
+            /* crate_name_to_features= */ Default::default(),
             /* errors = */ Rc::new(IgnoreErrors),
             /* no_thunk_name_mangling= */ false,
-            /* _features= */ (),
         )
     }
 
diff --git a/cc_bindings_from_rs/cc_bindings_from_rs.rs b/cc_bindings_from_rs/cc_bindings_from_rs.rs
index 30eb716..82598d6 100644
--- a/cc_bindings_from_rs/cc_bindings_from_rs.rs
+++ b/cc_bindings_from_rs/cc_bindings_from_rs.rs
@@ -42,13 +42,21 @@
         paths.push(CcInclude::user_header(include_path.as_str().into()));
     }
 
+    let mut crate_name_to_features =
+        <HashMap<Rc<str>, flagset::FlagSet<crubit_feature::CrubitFeature>>>::new();
+    for (crate_name, features) in &cmdline.crate_features {
+        let accumulated_features =
+            crate_name_to_features.entry(crate_name.as_str().into()).or_default();
+        *accumulated_features |= *features
+    }
+
     Database::new(
         tcx,
         crubit_support_path_format,
         crate_name_to_include_paths.into(),
+        crate_name_to_features.into(),
         errors,
         cmdline.no_thunk_name_mangling,
-        /* _features= */ (),
     )
 }
 
diff --git a/cc_bindings_from_rs/cmdline.rs b/cc_bindings_from_rs/cmdline.rs
index 4cbd656..44995c5 100644
--- a/cc_bindings_from_rs/cmdline.rs
+++ b/cc_bindings_from_rs/cmdline.rs
@@ -48,6 +48,16 @@
     // a "hash" of the crate version and compilation flags.
     pub crate_headers: Vec<(String, String)>,
 
+    /// Feature flags enabled for a given crate. Keys are crate names, and
+    /// values are feature flags. All crates must have their features fully
+    /// specified, and consistently across rustc invocations, or the
+    /// behavior is undefined.
+    ///
+    /// Example: "--crate-feature=foo=experimental".
+    #[clap(long = "crate-feature", value_parser = parse_crate_feature,
+           value_name = "CRATE_NAME=CRUBIT_FEATURE")]
+    pub crate_features: Vec<(String, flagset::FlagSet<crubit_feature::CrubitFeature>)>,
+
     /// Path to a rustfmt executable that will be used to format the
     /// Rust source files generated by the tool.
     #[clap(long, value_parser, value_name = "FILE")]
@@ -129,6 +139,25 @@
     Ok((crate_name.to_string(), include.to_string()))
 }
 
+/// Parse cmdline arguments of the following form:`"crate_name=crubit_feature"`.
+fn parse_crate_feature(
+    s: &str,
+) -> Result<(String, flagset::FlagSet<crubit_feature::CrubitFeature>)> {
+    let Some(pos) = s.find('=') else {
+        bail!("Expected KEY=VALUE syntax but no `=` found in `{s}`");
+    };
+
+    let crate_name = &s[..pos];
+    ensure!(!crate_name.is_empty(), "Empty crate names are invalid");
+
+    let feature_name = &s[(pos + 1)..];
+    let Some(features) = crubit_feature::named_features(feature_name.as_bytes()) else {
+        bail!("Invalid Crubit feature name: {feature_name:?}");
+    };
+
+    Ok((crate_name.to_string(), features))
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -222,29 +251,46 @@
 Usage: cc_bindings_from_rs_unittest_executable [OPTIONS] --h-out <FILE> --rs-out <FILE> --crubit-support-path-format <STRING> --clang-format-exe-path <FILE> --rustfmt-exe-path <FILE> [-- <RUSTC_ARGS>...]
 
 Arguments:
-  [RUSTC_ARGS]...  Command line arguments of the Rust compiler
+  [RUSTC_ARGS]...
+          Command line arguments of the Rust compiler
 
 Options:
       --h-out <FILE>
           Output path for C++ header file with bindings
+
       --rs-out <FILE>
           Output path for Rust implementation of the bindings
+
       --crubit-support-path-format <STRING>
           This is the format to `#include` Crubit C++ support library headers, using `{header}` as the  placeholder. Example: `<crubit/support/{header}>` will produce `#include <crubit/support/hdr.h>`
+
       --clang-format-exe-path <FILE>
           Path to a clang-format executable that will be used to format the C++ header files generated by the tool
+
       --crate-header <CRATE_NAME=INCLUDE_PATH>
-          Include paths of bindings for dependency crates, generated by previous invocations of Crubit. Keys are crate names, and values are include paths. Example: "--crate-header=foo=some/path/foo_cc_api.h" [aliases: bindings-from-dependency]
+          Include paths of bindings for dependency crates, generated by previous invocations of Crubit. Keys are crate names, and values are include paths. Example: "--crate-header=foo=some/path/foo_cc_api.h"
+          
+          [aliases: bindings-from-dependency]
+
+      --crate-feature <CRATE_NAME=CRUBIT_FEATURE>
+          Feature flags enabled for a given crate. Keys are crate names, and values are feature flags. All crates must have their features fully specified, and consistently across rustc invocations, or the behavior is undefined.
+          
+          Example: "--crate-feature=foo=experimental".
+
       --rustfmt-exe-path <FILE>
           Path to a rustfmt executable that will be used to format the Rust source files generated by the tool
+
       --rustfmt-config-path <FILE>
           Path to a rustfmt.toml file that should replace the default formatting of the .rs files generated by the tool
+
       --error-report-out <FILE>
           Path to the error reporting output file
+
       --no-thunk-name-mangling
           This is for golden tests only. Using this in production may cause undefined behavior
+
   -h, --help
-          Print help
+          Print help (see a summary with '-h')
 "#;
         let actual_msg = clap_err.to_string();
         assert_eq!(
@@ -338,6 +384,73 @@
     }
 
     #[test]
+    fn test_crate_features_as_multiple_separate_cmdline_args() {
+        let cmdline = new_cmdline([
+            "--h-out=foo.h",
+            "--rs-out=foo_impl.rs",
+            "--crubit-support-path-format=<crubit/support/{header}>",
+            "--clang-format-exe-path=clang-format.exe",
+            "--rustfmt-exe-path=rustfmt.exe",
+            "--crate-feature=dep1=supported",
+            "--crate-feature=dep1=experimental",
+            "--crate-feature=dep2=experimental",
+        ])
+        .unwrap();
+
+        assert_eq!(3, cmdline.crate_features.len());
+        assert_eq!("dep1", cmdline.crate_features[0].0);
+        assert_eq!(
+            flagset::FlagSet::<crubit_feature::CrubitFeature>::from(
+                crubit_feature::CrubitFeature::Supported
+            ),
+            cmdline.crate_features[0].1
+        );
+        assert_eq!("dep1", cmdline.crate_features[1].0);
+        assert_eq!(
+            flagset::FlagSet::<crubit_feature::CrubitFeature>::from(
+                crubit_feature::CrubitFeature::Experimental
+            ),
+            cmdline.crate_features[1].1
+        );
+        assert_eq!("dep2", cmdline.crate_features[2].0);
+        assert_eq!(
+            flagset::FlagSet::<crubit_feature::CrubitFeature>::from(
+                crubit_feature::CrubitFeature::Experimental
+            ),
+            cmdline.crate_features[2].1
+        );
+    }
+
+    #[test]
+    fn test_parse_crate_feature() {
+        assert_eq!(
+            parse_crate_feature("foo=supported").unwrap(),
+            (
+                "foo".into(),
+                flagset::FlagSet::<crubit_feature::CrubitFeature>::from(
+                    crubit_feature::CrubitFeature::Supported
+                )
+            ),
+        );
+        assert_eq!(
+            parse_crate_feature("").unwrap_err().to_string(),
+            "Expected KEY=VALUE syntax but no `=` found in ``",
+        );
+        assert_eq!(
+            parse_crate_feature("no-equal-char").unwrap_err().to_string(),
+            "Expected KEY=VALUE syntax but no `=` found in `no-equal-char`",
+        );
+        assert_eq!(
+            parse_crate_feature("=bar").unwrap_err().to_string(),
+            "Empty crate names are invalid",
+        );
+        assert_eq!(
+            parse_crate_feature("foo=").unwrap_err().to_string(),
+            "Invalid Crubit feature name: \"\"",
+        );
+    }
+
+    #[test]
     fn test_crubit_support_path_format_arg_happy_path() {
         let cmdline = new_cmdline([
             "--h-out=foo.h",
diff --git a/common/crubit_feature.rs b/common/crubit_feature.rs
index c6a78d4..e259527 100644
--- a/common/crubit_feature.rs
+++ b/common/crubit_feature.rs
@@ -33,6 +33,17 @@
     }
 }
 
+/// Returns the set of features named by this short name.
+pub fn named_features(name: &[u8]) -> Option<flagset::FlagSet<CrubitFeature>> {
+    let features = match name {
+        b"all" => flagset::FlagSet::<CrubitFeature>::full(),
+        b"supported" => CrubitFeature::Supported.into(),
+        b"experimental" => CrubitFeature::Experimental.into(),
+        _ => return None,
+    };
+    Some(features)
+}
+
 /// A newtype around a single named feature flagset, so that it can be
 /// deserialized from a string instead of an integer.
 #[derive(Debug, Default, PartialEq, Eq, Clone)]
@@ -60,16 +71,11 @@
             where
                 E: serde::de::Error,
             {
-                let features = match bytes {
-                    b"all" => flagset::FlagSet::<CrubitFeature>::full(),
-                    b"supported" => CrubitFeature::Supported.into(),
-                    b"experimental" => CrubitFeature::Experimental.into(),
-                    other => {
-                        return Err(E::custom(format!(
-                            "Unexpected Crubit feature: {:?}",
-                            String::from_utf8_lossy(other)
-                        )));
-                    }
+                let Some(features) = named_features(bytes) else {
+                    return Err(E::custom(format!(
+                        "Unexpected Crubit feature: {:?}",
+                        String::from_utf8_lossy(bytes)
+                    )));
                 };
 
                 Ok(SerializedCrubitFeature(features))