cargo_flux/
cargo-flux.rs

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