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