Skip to main content

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