mc_build_rs/
cargo_build.rs

1// Copyright (c) 2023 The MobileCoin Foundation
2
3//! This module contains a cargo invoker
4
5#[cfg_attr(test, double)]
6use crate::env::Environment;
7use crate::vars::{
8    ENV_CARGO_BUILD_DEP_INFO_BASEDIR, ENV_CARGO_BUILD_JOBS, ENV_CARGO_BUILD_PIPELINING,
9    ENV_CARGO_BUILD_TARGET, ENV_CARGO_CACHE_RUSTC_INFO, ENV_CARGO_HOME, ENV_CARGO_HTTP_CAINFO,
10    ENV_CARGO_HTTP_CHECK_REVOKE, ENV_CARGO_HTTP_DEBUG, ENV_CARGO_HTTP_LOW_SPEED_LIMIT,
11    ENV_CARGO_HTTP_MULTIPLEXING, ENV_CARGO_HTTP_SSL_VERSION, ENV_CARGO_HTTP_USER_AGENT,
12    ENV_CARGO_INCREMENTAL, ENV_CARGO_NET_GIT_FETCH_WITH_CLI, ENV_CARGO_NET_OFFLINE,
13    ENV_CARGO_NET_RETRY, ENV_CARGO_TARGET_DIR, ENV_CARGO_TERM_COLOR, ENV_CARGO_TERM_VERBOSE,
14    ENV_HTTPS_PROXY, ENV_HTTP_TIMEOUT, ENV_RUSTC, ENV_RUSTC_WRAPPER, ENV_RUSTDOC, ENV_RUSTDOCFLAGS,
15    ENV_RUSTFLAGS, ENV_TERM,
16};
17#[cfg(test)]
18use mockall_double::double;
19use std::{
20    collections::HashMap,
21    ffi::OsStr,
22    path::{Path, PathBuf},
23    process::Command,
24    time::Duration,
25};
26use url::Url;
27
28/// A helper method to clear and/or inject new values into a command's
29/// environment
30fn str_env(command: &mut Command, clean_env: bool, value: Option<&impl AsRef<OsStr>>, env: &str) {
31    if clean_env || value.is_some() {
32        command.env_remove(env);
33    }
34
35    if let Some(v) = value {
36        command.env(env, v);
37    }
38}
39
40/// A helper method to clear and/or inject a separated array of strings into the
41/// given command's environment
42fn strv_env(command: &mut Command, clean_env: bool, values: &[String], env: &str, sep: &str) {
43    if clean_env || !values.is_empty() {
44        command.env_remove(env);
45    }
46
47    if values.is_empty() {
48        str_env(command, clean_env, None as Option<&String>, env);
49    } else {
50        let v = values.join(sep);
51        str_env(command, clean_env, Some(&v), env);
52    }
53}
54
55/// A helper method to clear and/or inject an optional boolean value as a "0" or
56/// "1" into the given command's environment
57fn onezero_env(command: &mut Command, clean_env: bool, value: Option<bool>, env: &str) {
58    if clean_env || value.is_some() {
59        command.env_remove(env);
60    }
61
62    if let Some(val) = value {
63        let v = val as u8;
64        str_env(command, clean_env, Some(&v.to_string()), env);
65    }
66}
67
68/// A helper method to clear and/or injected an optional duration value as
69/// seconds into the given command's environment
70fn duration_env(command: &mut Command, clean_env: bool, value: Option<&Duration>, env: &str) {
71    if clean_env || value.is_some() {
72        command.env_remove(env);
73    }
74
75    if let Some(val) = value {
76        str_env(command, clean_env, Some(&val.as_secs().to_string()), env);
77    }
78}
79
80/// A helper method to clear and/or injected an optional integer value into the
81/// given command's environment
82fn u64_env(command: &mut Command, clean_env: bool, value: Option<&u64>, env: &str) {
83    if clean_env || value.is_some() {
84        command.env_remove(env);
85    }
86
87    if let Some(val) = value {
88        str_env(command, clean_env, Some(&val.to_string()), env);
89    }
90}
91
92/// A builder-pattern which constructs a command to invoke cargo build
93#[derive(Clone, Debug)]
94pub struct CargoBuilder {
95    working_dir: PathBuf,
96    clean_env: bool,
97    cargo_path: PathBuf,
98
99    home: Option<PathBuf>,
100    target_dir: Option<PathBuf>,
101    rustc: Option<PathBuf>,
102    rustc_wrapper: Option<PathBuf>,
103    rustdoc: Option<PathBuf>,
104    rustdocflags: Vec<String>,
105    rustflags: Vec<String>,
106    incremental: Option<bool>,
107    cache_rustc_info: Option<bool>,
108    term: Option<String>,
109
110    build_jobs: Option<u64>,
111    target: Option<String>,
112    dep_info_basedir: Option<PathBuf>,
113    pipelining: Option<bool>,
114
115    http_debug: Option<bool>,
116    http_proxy: Option<String>,
117    http_timeout: Option<Duration>,
118    http_cainfo: Option<PathBuf>,
119    http_check_revoke: Option<bool>,
120    http_ssl_version: Option<String>,
121    http_low_speed_limit: Option<u64>,
122    http_multiplexing: Option<bool>,
123    http_user_agent: Option<String>,
124    net_retry: Option<u64>,
125    net_git_fetch_with_cli: Option<bool>,
126    net_offline: Option<bool>,
127    registries: HashMap<String, Url>,
128    term_verbose: Option<bool>,
129    term_color: Option<bool>,
130
131    profile: String,
132    locked: bool,
133}
134
135impl CargoBuilder {
136    /// Construct a new builder instance to run cargo in the given directory.
137    ///
138    /// If clean_env is set, cargo configuration variables will not be passed
139    /// through from the command.
140    pub fn new(env: &Environment, working_dir: &Path, clean_env: bool) -> Self {
141        let cargo_path = env.cargo().to_owned();
142        let profile = env.profile().to_owned();
143        Self {
144            working_dir: working_dir.to_owned(),
145            clean_env,
146            cargo_path,
147            home: None,
148            target_dir: None,
149            rustc: None,
150            rustc_wrapper: None,
151            rustdoc: None,
152            rustdocflags: Vec::default(),
153            rustflags: Vec::default(),
154            incremental: None,
155            cache_rustc_info: None,
156            term: None,
157            build_jobs: None,
158            target: None,
159            dep_info_basedir: None,
160            pipelining: None,
161            http_debug: None,
162            http_proxy: None,
163            http_timeout: None,
164            http_cainfo: None,
165            http_check_revoke: None,
166            http_ssl_version: None,
167            http_low_speed_limit: None,
168            http_multiplexing: None,
169            http_user_agent: None,
170            net_retry: None,
171            net_git_fetch_with_cli: None,
172            net_offline: None,
173            registries: HashMap::default(),
174            term_verbose: None,
175            term_color: None,
176            profile,
177            locked: env.locked(),
178        }
179    }
180
181    /// Constructs the command which will execute cargo
182    pub fn construct(&mut self) -> Command {
183        let mut command = Command::new(&self.cargo_path);
184
185        command.current_dir(&self.working_dir);
186
187        // Environment variables Cargo reads
188
189        str_env(
190            &mut command,
191            self.clean_env,
192            self.home.as_ref(),
193            ENV_CARGO_HOME,
194        );
195        str_env(
196            &mut command,
197            self.clean_env,
198            self.target_dir.as_ref(),
199            ENV_CARGO_TARGET_DIR,
200        );
201        str_env(&mut command, self.clean_env, self.rustc.as_ref(), ENV_RUSTC);
202        str_env(
203            &mut command,
204            self.clean_env,
205            self.rustc_wrapper.as_ref(),
206            ENV_RUSTC_WRAPPER,
207        );
208        str_env(
209            &mut command,
210            self.clean_env,
211            self.rustdoc.as_ref(),
212            ENV_RUSTDOC,
213        );
214        strv_env(
215            &mut command,
216            self.clean_env,
217            &self.rustdocflags,
218            ENV_RUSTDOCFLAGS,
219            " ",
220        );
221        strv_env(
222            &mut command,
223            self.clean_env,
224            &self.rustflags,
225            ENV_RUSTFLAGS,
226            " ",
227        );
228        onezero_env(
229            &mut command,
230            self.clean_env,
231            self.incremental,
232            ENV_CARGO_INCREMENTAL,
233        );
234        onezero_env(
235            &mut command,
236            self.clean_env,
237            self.cache_rustc_info,
238            ENV_CARGO_CACHE_RUSTC_INFO,
239        );
240
241        str_env(
242            &mut command,
243            self.clean_env,
244            self.http_proxy.as_ref(),
245            ENV_HTTPS_PROXY,
246        );
247        duration_env(
248            &mut command,
249            self.clean_env,
250            self.http_timeout.as_ref(),
251            ENV_HTTP_TIMEOUT,
252        );
253        str_env(&mut command, self.clean_env, self.term.as_ref(), ENV_TERM);
254
255        // Configuration environment variables
256
257        u64_env(
258            &mut command,
259            self.clean_env,
260            self.build_jobs.as_ref(),
261            ENV_CARGO_BUILD_JOBS,
262        );
263        str_env(
264            &mut command,
265            self.clean_env,
266            self.target.as_ref(),
267            ENV_CARGO_BUILD_TARGET,
268        );
269        str_env(
270            &mut command,
271            self.clean_env,
272            self.dep_info_basedir.as_ref(),
273            ENV_CARGO_BUILD_DEP_INFO_BASEDIR,
274        );
275        onezero_env(
276            &mut command,
277            self.clean_env,
278            self.pipelining,
279            ENV_CARGO_BUILD_PIPELINING,
280        );
281
282        onezero_env(
283            &mut command,
284            self.clean_env,
285            self.http_debug,
286            ENV_CARGO_HTTP_DEBUG,
287        );
288        str_env(
289            &mut command,
290            self.clean_env,
291            self.http_cainfo.as_ref(),
292            ENV_CARGO_HTTP_CAINFO,
293        );
294        onezero_env(
295            &mut command,
296            self.clean_env,
297            self.http_check_revoke,
298            ENV_CARGO_HTTP_CHECK_REVOKE,
299        );
300        str_env(
301            &mut command,
302            self.clean_env,
303            self.http_ssl_version.as_ref(),
304            ENV_CARGO_HTTP_SSL_VERSION,
305        );
306        u64_env(
307            &mut command,
308            self.clean_env,
309            self.http_low_speed_limit.as_ref(),
310            ENV_CARGO_HTTP_LOW_SPEED_LIMIT,
311        );
312        onezero_env(
313            &mut command,
314            self.clean_env,
315            self.http_multiplexing,
316            ENV_CARGO_HTTP_MULTIPLEXING,
317        );
318        str_env(
319            &mut command,
320            self.clean_env,
321            self.http_user_agent.as_ref(),
322            ENV_CARGO_HTTP_USER_AGENT,
323        );
324        u64_env(
325            &mut command,
326            self.clean_env,
327            self.net_retry.as_ref(),
328            ENV_CARGO_NET_RETRY,
329        );
330        onezero_env(
331            &mut command,
332            self.clean_env,
333            self.net_git_fetch_with_cli,
334            ENV_CARGO_NET_GIT_FETCH_WITH_CLI,
335        );
336        onezero_env(
337            &mut command,
338            self.clean_env,
339            self.net_offline,
340            ENV_CARGO_NET_OFFLINE,
341        );
342
343        // Note, we don't remove any other registries which are part of the env here
344        for (name, index) in self.registries.iter() {
345            str_env(
346                &mut command,
347                self.clean_env,
348                Some(&index.as_str()),
349                &format!(
350                    "CARGO_REGISTRY_{}_INDEX",
351                    name.to_ascii_uppercase().replace('-', "_")
352                ),
353            );
354        }
355
356        onezero_env(
357            &mut command,
358            self.clean_env,
359            self.term_verbose,
360            ENV_CARGO_TERM_VERBOSE,
361        );
362        onezero_env(
363            &mut command,
364            self.clean_env,
365            self.term_color,
366            ENV_CARGO_TERM_COLOR,
367        );
368
369        command.arg("build").arg("-vv");
370
371        if self.profile == "release" {
372            command.arg("--release");
373        }
374
375        if self.locked {
376            command.arg("--locked");
377        }
378
379        command
380    }
381
382    /// Set the path to the cargo executable
383    pub fn cargo_path(&mut self, cargo: &Path) -> &mut Self {
384        self.cargo_path = cargo.to_owned();
385        self
386    }
387
388    /// Set the CARGO_HOME variable for invoking cargo
389    pub fn home(&mut self, home: &Path) -> &mut Self {
390        self.home = Some(home.to_owned());
391        self
392    }
393
394    /// Set the CARGO_TARGET_DIR variable for invoking cargo
395    pub fn target_dir(&mut self, target_dir: &Path) -> &mut Self {
396        self.target_dir = Some(target_dir.to_owned());
397        self
398    }
399
400    /// Set the RUSTC variable for invoking cargo
401    pub fn rustc(&mut self, rustc: &Path) -> &mut Self {
402        self.rustc = Some(rustc.to_owned());
403        self
404    }
405
406    /// Set the RUSTC_WRAPPER variable for invoking cargo
407    pub fn rustc_wrapper(&mut self, rustc_wrapper: &Path) -> &mut Self {
408        self.rustc_wrapper = Some(rustc_wrapper.to_owned());
409        self
410    }
411
412    /// Set the RUSTDOC variable when invoking cargo
413    pub fn rustdoc(&mut self, rustdoc: &Path) -> &mut Self {
414        self.rustdoc = Some(rustdoc.to_owned());
415        self
416    }
417
418    /// Add an item to the RUSTDOCFLAGS environment string
419    pub fn add_rustdoc_flag(&mut self, rustdoc_flag: &str) -> &mut Self {
420        self.rustdocflags.push(rustdoc_flag.to_owned());
421        self
422    }
423
424    /// Add multiple items to the RUSTDOCFLAGS environment string
425    pub fn add_rustdoc_flags(&mut self, rustdoc_flags: &[&str]) -> &mut Self {
426        for flag in rustdoc_flags {
427            self.rustdocflags.push((*flag).to_owned());
428        }
429        self
430    }
431
432    /// Add an item to the RUSTFLAGS environment string
433    pub fn add_rust_flag(&mut self, rust_flag: &str) -> &mut Self {
434        self.rustflags.push(rust_flag.to_owned());
435        self
436    }
437
438    /// Add multiple items to the RUSTFLAGS environment string
439    pub fn add_rust_flags(&mut self, rust_flags: &[&str]) -> &mut Self {
440        for flag in rust_flags {
441            self.rustflags.push((*flag).to_owned());
442        }
443        self
444    }
445
446    /// Explicitly set whether incremental builds are enabled or disabled
447    pub fn incremental(&mut self, incremental: bool) -> &mut Self {
448        self.incremental = Some(incremental);
449        self
450    }
451
452    /// Enable/disable whether or not cargo should cache rust info
453    pub fn cache_rustc_info(&mut self, cache_rustc_info: bool) -> &mut Self {
454        self.cache_rustc_info = Some(cache_rustc_info);
455        self
456    }
457
458    /// Set the terminal environment variable.
459    pub fn term(&mut self, term: &str) -> &mut Self {
460        self.term = Some(term.to_owned());
461        self
462    }
463
464    /// Override the `build.jobs` configuration option
465    pub fn build_jobs(&mut self, build_jobs: u64) -> &mut Self {
466        self.build_jobs = Some(build_jobs);
467        self
468    }
469
470    /// Override the `build.target` configuration option
471    pub fn target(&mut self, target: &str) -> &mut Self {
472        self.target = Some(target.to_owned());
473        self
474    }
475
476    /// Override the `build.dep-info-basedir` configuration option
477    pub fn dep_info_basedir(&mut self, dep_info_basedir: &Path) -> &mut Self {
478        self.dep_info_basedir = Some(dep_info_basedir.to_owned());
479        self
480    }
481
482    /// Override the `build.pipelining` configuration option
483    pub fn pipelining(&mut self, pipelining: bool) -> &mut Self {
484        self.pipelining = Some(pipelining);
485        self
486    }
487
488    /// Override the `http.debug` configuration option
489    pub fn http_debug(&mut self, http_debug: bool) -> &mut Self {
490        self.http_debug = Some(http_debug);
491        self
492    }
493
494    /// Override the `http.proxy` configuration option
495    pub fn http_proxy(&mut self, http_proxy: &str) -> &mut Self {
496        self.http_proxy = Some(http_proxy.to_owned());
497        self
498    }
499
500    /// Override the `http.debug` configuration option
501    pub fn http_timeout(&mut self, http_timeout: Duration) -> &mut Self {
502        self.http_timeout = Some(http_timeout);
503        self
504    }
505
506    /// Override the `http.cainfo` file configuration option
507    pub fn http_cainfo(&mut self, http_cainfo: &Path) -> &mut Self {
508        self.http_cainfo = Some(http_cainfo.to_owned());
509        self
510    }
511
512    /// Override the `http.check-revoke` configuration option
513    pub fn http_check_revoke(&mut self, http_check_revoke: bool) -> &mut Self {
514        self.http_check_revoke = Some(http_check_revoke);
515        self
516    }
517
518    /// Override the `http.ssl-version` configuration option
519    pub fn http_ssl_version(&mut self, http_ssl_version: &str) -> &mut Self {
520        self.http_ssl_version = Some(http_ssl_version.to_owned());
521        self
522    }
523
524    /// Override the `http.low-speed-limit` configuration option
525    pub fn http_low_speed_limit(&mut self, http_low_speed_limit: u64) -> &mut Self {
526        self.http_low_speed_limit = Some(http_low_speed_limit);
527        self
528    }
529
530    /// Override the `http.multiplexing` configuration option
531    pub fn http_multiplexing(&mut self, http_multiplexing: bool) -> &mut Self {
532        self.http_multiplexing = Some(http_multiplexing);
533        self
534    }
535
536    /// Override the `http.user-agent` configuration option
537    pub fn http_user_agent(&mut self, http_user_agent: String) -> &mut Self {
538        self.http_user_agent = Some(http_user_agent);
539        self
540    }
541
542    /// Override the `net.retry` configuration option
543    pub fn net_retry(&mut self, net_retry: u64) -> &mut Self {
544        self.net_retry = Some(net_retry);
545        self
546    }
547
548    /// Override the `net.get_fetch_with_cli` configuration option
549    pub fn net_git_fetch_with_cli(&mut self, net_git_fetch_with_cli: bool) -> &mut Self {
550        self.net_git_fetch_with_cli = Some(net_git_fetch_with_cli);
551        self
552    }
553
554    /// Override the `net.offline` configuration option
555    pub fn net_offline(&mut self, net_offline: bool) -> &mut Self {
556        self.net_offline = Some(net_offline);
557        self
558    }
559
560    /// Add a new crates.io-style registry to this invocation of cargo
561    pub fn add_registry(&mut self, name: String, index: Url) -> &mut Self {
562        self.registries.insert(name, index);
563        self
564    }
565
566    /// Sets whether to use verbose stdout in the cargo run
567    pub fn term_verbose(&mut self, term_verbose: bool) -> &mut Self {
568        self.term_verbose = Some(term_verbose);
569        self
570    }
571
572    /// Sets whether to output terminal colors
573    pub fn term_color(&mut self, term_color: bool) -> &mut Self {
574        self.term_color = Some(term_color);
575        self
576    }
577
578    /// Override the inherited profile (i.e. `--release`)
579    pub fn profile(&mut self, profile: String) -> &mut Self {
580        self.profile = profile;
581        self
582    }
583
584    /// Override the inherited locked argument (i.e. `--locked`)
585    pub fn locked(&mut self, locked: bool) -> &mut Self {
586        self.locked = locked;
587        self
588    }
589}
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594    use std::str::FromStr;
595
596    fn init_mock_env(cargo_path: &str, profile: &str, cargo_locked: bool) -> Environment {
597        let mut mock = Environment::default();
598
599        mock.expect_cargo()
600            .return_const(PathBuf::from_str(cargo_path).expect("Fail"));
601        mock.expect_profile().return_const(profile.to_string());
602        mock.expect_locked().return_const(cargo_locked);
603        mock
604    }
605
606    #[test]
607    fn cargo_builder() {
608        const EXPECTED_OUTPUT_DIR: &str = "/path_to_output_directory";
609        const EXPECTED_CARGO_PATH: &str = "/path_to_cargo";
610        const EXPECTED_PROFILE: &str = "debug";
611        const EXPECTED_CARGO_LOCKED: bool = true;
612
613        let mock = init_mock_env(EXPECTED_CARGO_PATH, EXPECTED_PROFILE, EXPECTED_CARGO_LOCKED);
614        let cargo_builder = CargoBuilder::new(&mock, &Path::new(EXPECTED_OUTPUT_DIR), false);
615        let actual_dir = cargo_builder.working_dir.to_str().expect("Fail");
616        let actual_cargo_path = cargo_builder.cargo_path.to_str().expect("Fail");
617
618        assert_eq!(actual_dir, EXPECTED_OUTPUT_DIR);
619        assert_eq!(actual_cargo_path, EXPECTED_CARGO_PATH);
620        assert_eq!(cargo_builder.profile, EXPECTED_PROFILE);
621        assert_eq!(cargo_builder.locked, true);
622    }
623
624    #[test]
625    fn construct_cargo_command() {
626        let mock = init_mock_env("/path_to_cargo", "debug", true);
627        let mut cargo_builder =
628            CargoBuilder::new(&mock, &Path::new("/path_to_output_directory"), false);
629        let cmd = cargo_builder.construct();
630        let actual_cmd = format!("{:?}", cmd);
631        let expected_cmd =
632            "cd \"/path_to_output_directory\" && \"/path_to_cargo\" \"build\" \"-vv\" \"--locked\"";
633
634        assert_eq!(actual_cmd, expected_cmd);
635    }
636
637    #[test]
638    fn change_cargo_path() {
639        let expected_cargo_path = "/path_to_cargo";
640        let expected_changed_cargo_path = "/changed/path_to_cargo";
641        let mock = init_mock_env(expected_cargo_path, "debug", true);
642        let mut cargo_builder =
643            CargoBuilder::new(&mock, &Path::new("/path_to_output_directory"), false);
644        let mut actual_cargo_path = cargo_builder.cargo_path.to_str().expect("Fail");
645
646        assert_eq!(actual_cargo_path, expected_cargo_path);
647
648        cargo_builder.cargo_path(Path::new(expected_changed_cargo_path));
649        actual_cargo_path = cargo_builder.cargo_path.to_str().expect("Fail");
650
651        assert_eq!(actual_cargo_path, expected_changed_cargo_path);
652    }
653
654    #[test]
655    fn change_target() {
656        let expected_target = "x86_64-unknown-linux-gnu";
657        let mock = init_mock_env("/path_to_cargo", "debug", true);
658        let mut cargo_builder =
659            CargoBuilder::new(&mock, &Path::new("/path_to_output_directory"), false);
660        let mut actual_target = cargo_builder.target.as_ref();
661
662        assert_eq!(actual_target, None);
663
664        cargo_builder.target(expected_target);
665        actual_target = cargo_builder.target.as_ref();
666
667        assert_eq!(actual_target.expect("Fail"), expected_target);
668    }
669
670    #[test]
671    fn change_rust_flags() {
672        let expected_rust_flags = ["-D", "warnings", "-C"];
673        let mock = init_mock_env("/path_to_cargo", "debug", true);
674        let mut cargo_builder =
675            CargoBuilder::new(&mock, &Path::new("/path_to_output_directory"), false);
676        let mut rust_flags: &Vec<String> = cargo_builder.rustflags.as_ref();
677
678        assert_eq!(rust_flags.is_empty(), true);
679
680        cargo_builder.add_rust_flags(&expected_rust_flags);
681        rust_flags = cargo_builder.rustflags.as_ref();
682
683        assert_eq!(rust_flags.as_slice(), expected_rust_flags);
684    }
685}