cargo_flux/
cargo-flux.rs

1use std::{
2    self, env,
3    io::{BufWriter, Write},
4    process::{Command, exit},
5};
6
7use anyhow::anyhow;
8use cargo_metadata::{Metadata, MetadataCommand, camino::Utf8Path};
9use flux_bin::{
10    FluxMetadata,
11    utils::{
12        EXIT_ERR, flux_sysroot_dir, get_flux_driver_path, get_rust_toolchain,
13        print_version_and_exit,
14    },
15};
16use itertools::Itertools;
17use tempfile::NamedTempFile;
18
19fn main() {
20    if env::args().any(|arg| arg == "--version" || arg == "-V") {
21        print_version_and_exit("cargo-flux");
22    }
23    let exit_code = match run() {
24        Ok(code) => code,
25        Err(e) => {
26            println!("Failed to run `cargo-flux`, error={e}");
27            EXIT_ERR
28        }
29    };
30    exit(exit_code)
31}
32
33fn run() -> anyhow::Result<i32> {
34    let toolchain = get_rust_toolchain()?;
35
36    let metadata = MetadataCommand::new().exec()?;
37    let config_file = write_cargo_config(metadata)?;
38
39    // Cargo can be called like `cargo [OPTIONS] flux`, so we skip all arguments until `flux` is found.
40    let mut args = env::args()
41        .skip_while(|arg| arg != "flux")
42        .skip(1)
43        .collect::<Vec<_>>();
44
45    // Unless we are calling `cargo flux clean` add a `check`
46    match &args[..] {
47        [subcommand, ..] if subcommand == "clean" => {}
48        _ => {
49            args.insert(0, "check".to_string());
50        }
51    }
52    args.extend(["--profile".to_string(), "flux".to_string()]);
53
54    // We set `RUSTC` as an environment variable and not in in the [build]
55    // section of the config file to make sure we run flux even when the
56    // variable is already set. We also unset `RUSTC_WRAPPER` to avoid
57    // conflicts, e.g., see https://github.com/flux-rs/flux/issues/1155
58    let sysroot = flux_sysroot_dir();
59    let flux_driver_path = get_flux_driver_path(&sysroot)?;
60    let exit_code = Command::new("cargo")
61        .env("RUSTC", flux_driver_path)
62        .env("RUSTC_WRAPPER", "")
63        .arg(format!("+{toolchain}"))
64        .arg("--config")
65        .arg(config_file.path())
66        .args(args)
67        .status()?
68        .code();
69
70    Ok(exit_code.unwrap_or(EXIT_ERR))
71}
72
73fn write_cargo_config(metadata: Metadata) -> anyhow::Result<NamedTempFile> {
74    let flux_flags: Option<Vec<String>> = if let Ok(flags) = env::var("FLUXFLAGS") {
75        Some(flags.split(" ").map(Into::into).collect())
76    } else {
77        None
78    };
79
80    let flux_toml = config::Config::builder()
81        .add_source(config::File::with_name("flux.toml").required(false))
82        .build()?;
83
84    if flux_toml.get_bool("enabled").is_ok() {
85        return Err(anyhow!("`enabled` cannot be set in `flux.toml`"));
86    }
87
88    let mut file = NamedTempFile::new()?;
89    {
90        let mut w = BufWriter::new(&mut file);
91        write!(
92            w,
93            r#"
94[unstable]
95profile-rustflags = true
96
97[env]
98FLUX_BUILD_SYSROOT = "1"
99FLUX_CARGO = "1"
100
101[profile.flux]
102inherits = "dev"
103incremental = false
104        "#
105        )?;
106
107        for package in metadata.packages {
108            let flux_metadata: FluxMetadata = config::Config::builder()
109                .add_source(FluxMetadataSource::new(
110                    package.manifest_path.to_string(),
111                    package.metadata,
112                ))
113                .add_source(flux_toml.clone())
114                .build()?
115                .try_deserialize()?;
116
117            if flux_metadata.enabled {
118                // For workspace members, cargo sets the workspace's root as the working dir
119                // when running flux. Paths will be relative to that, so we must normalize
120                // glob patterns to be relative to the workspace's root.
121                let manifest_dir_relative_to_workspace = package
122                    .manifest_path
123                    .strip_prefix(&metadata.workspace_root)
124                    .ok()
125                    .and_then(Utf8Path::parent);
126                write!(
127                    w,
128                    r#"
129[profile.flux.package."{}"]
130rustflags = [{:?}]
131                        "#,
132                    package.id,
133                    flux_metadata
134                        .into_flags(&metadata.target_directory, manifest_dir_relative_to_workspace)
135                        .iter()
136                        .chain(flux_flags.iter().flatten())
137                        .map(|s| s.as_ref())
138                        .chain(["-Fverify=on", "-Ffull-compilation=on"])
139                        .format(", ")
140                )?;
141            }
142        }
143    }
144    Ok(file)
145}
146
147#[derive(Clone, Debug)]
148struct FluxMetadataSource {
149    origin: String,
150    value: serde_json::Value,
151}
152
153impl FluxMetadataSource {
154    fn new(origin: String, value: serde_json::Value) -> Self {
155        Self { origin, value }
156    }
157}
158
159impl config::Source for FluxMetadataSource {
160    fn clone_into_box(&self) -> Box<dyn config::Source + Send + Sync> {
161        Box::new(self.clone())
162    }
163
164    fn collect(&self) -> Result<config::Map<String, config::Value>, config::ConfigError> {
165        if let serde_json::Value::Object(metadata) = &self.value
166            && let Some(flux_metadata) = metadata.get("flux")
167        {
168            let config_value = serde_json_to_config(flux_metadata, &self.origin)?;
169            if let config::ValueKind::Table(table) = config_value.kind {
170                Ok(table)
171            } else {
172                Err(config::ConfigError::Message("expected a table".to_string()))
173            }
174        } else {
175            Ok(Default::default())
176        }
177    }
178}
179
180fn serde_json_to_config(
181    value: &serde_json::Value,
182    origin: &String,
183) -> Result<config::Value, config::ConfigError> {
184    let kind = match value {
185        serde_json::Value::Null => config::ValueKind::Nil,
186        serde_json::Value::Bool(b) => config::ValueKind::Boolean(*b),
187        serde_json::Value::Number(number) => {
188            if let Some(n) = number.as_u128() {
189                config::ValueKind::U128(n)
190            } else if let Some(n) = number.as_i128() {
191                config::ValueKind::I128(n)
192            } else if let Some(n) = number.as_u64() {
193                config::ValueKind::U64(n)
194            } else if let Some(n) = number.as_i64() {
195                config::ValueKind::I64(n)
196            } else if let Some(n) = number.as_f64() {
197                config::ValueKind::Float(n)
198            } else {
199                return Err(config::ConfigError::Message("invalid number".to_string()));
200            }
201        }
202        serde_json::Value::String(s) => config::ValueKind::String(s.clone()),
203        serde_json::Value::Array(values) => {
204            config::ValueKind::Array(
205                values
206                    .iter()
207                    .map(|v| serde_json_to_config(v, origin))
208                    .try_collect()?,
209            )
210        }
211        serde_json::Value::Object(map) => {
212            config::ValueKind::Table(
213                map.iter()
214                    .map(|(k, v)| Ok((k.clone(), serde_json_to_config(v, origin)?)))
215                    .try_collect()?,
216            )
217        }
218    };
219    Ok(config::Value::new(Some(origin), kind))
220}