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