|  | 
|  | 1 | +// This Source Code Form is subject to the terms of the Mozilla Public | 
|  | 2 | +// License, v. 2.0. If a copy of the MPL was not distributed with this | 
|  | 3 | +// file, You can obtain one at https://mozilla.org/MPL/2.0/. | 
|  | 4 | + | 
|  | 5 | +//! External xtasks. (extasks?) | 
|  | 6 | +
 | 
|  | 7 | +use std::ffi::OsString; | 
|  | 8 | +use std::os::unix::process::CommandExt; | 
|  | 9 | +use std::process::Command; | 
|  | 10 | + | 
|  | 11 | +use anyhow::{Context, Result}; | 
|  | 12 | +use clap::Parser; | 
|  | 13 | + | 
|  | 14 | +/// Argument parser for external xtasks. | 
|  | 15 | +/// | 
|  | 16 | +/// In general we want all developer tasks to be discoverable simply by running | 
|  | 17 | +/// `cargo xtask`, but some development tools end up with a particularly | 
|  | 18 | +/// large dependency tree. It's not ideal to have to pay the cost of building | 
|  | 19 | +/// our release engineering tooling if all the user wants to do is check for | 
|  | 20 | +/// workspace dependency issues. | 
|  | 21 | +/// | 
|  | 22 | +/// `External` provides a pattern for creating xtasks that live in other crates. | 
|  | 23 | +/// An external xtask is defined on `crate::Cmds` as a tuple variant containing | 
|  | 24 | +/// `External`, which captures all arguments and options (even `--help`) as | 
|  | 25 | +/// a `Vec<OsString>`. The main function then calls `External::exec` with the | 
|  | 26 | +/// appropriate bin target name and any additional Cargo arguments. | 
|  | 27 | +#[derive(Debug, Parser)] | 
|  | 28 | +#[clap( | 
|  | 29 | +    disable_help_flag(true), | 
|  | 30 | +    disable_help_subcommand(true), | 
|  | 31 | +    disable_version_flag(true) | 
|  | 32 | +)] | 
|  | 33 | +pub struct External { | 
|  | 34 | +    #[clap(trailing_var_arg(true), allow_hyphen_values(true))] | 
|  | 35 | +    args: Vec<OsString>, | 
|  | 36 | + | 
|  | 37 | +    // This stores an in-progress Command builder. `cargo_args` appends args | 
|  | 38 | +    // to it, and `exec` consumes it. Clap does not treat this as a command | 
|  | 39 | +    // (`skip`), but fills in this field by calling `new_command`. | 
|  | 40 | +    #[clap(skip = new_command())] | 
|  | 41 | +    command: Command, | 
|  | 42 | +} | 
|  | 43 | + | 
|  | 44 | +impl External { | 
|  | 45 | +    pub fn exec_bin( | 
|  | 46 | +        self, | 
|  | 47 | +        package: impl AsRef<str>, | 
|  | 48 | +        bin_target: impl AsRef<str>, | 
|  | 49 | +    ) -> Result<()> { | 
|  | 50 | +        self.exec_common(&[ | 
|  | 51 | +            "--package", | 
|  | 52 | +            package.as_ref(), | 
|  | 53 | +            "--bin", | 
|  | 54 | +            bin_target.as_ref(), | 
|  | 55 | +        ]) | 
|  | 56 | +    } | 
|  | 57 | + | 
|  | 58 | +    fn exec_common(mut self, args: &[&str]) -> Result<()> { | 
|  | 59 | +        let error = self.command.args(args).arg("--").args(self.args).exec(); | 
|  | 60 | +        Err(error).context("failed to exec `cargo run`") | 
|  | 61 | +    } | 
|  | 62 | +} | 
|  | 63 | + | 
|  | 64 | +fn new_command() -> Command { | 
|  | 65 | +    let mut command = cargo_command(CargoLocation::FromEnv); | 
|  | 66 | +    command.arg("run"); | 
|  | 67 | +    command | 
|  | 68 | +} | 
|  | 69 | + | 
|  | 70 | +/// Creates and prepares a `std::process::Command` for the `cargo` executable. | 
|  | 71 | +pub fn cargo_command(location: CargoLocation) -> Command { | 
|  | 72 | +    let mut command = location.resolve(); | 
|  | 73 | + | 
|  | 74 | +    for (key, _) in std::env::vars_os() { | 
|  | 75 | +        let Some(key) = key.to_str() else { continue }; | 
|  | 76 | +        if SANITIZED_ENV_VARS.matches(key) { | 
|  | 77 | +            command.env_remove(key); | 
|  | 78 | +        } | 
|  | 79 | +    } | 
|  | 80 | + | 
|  | 81 | +    command | 
|  | 82 | +} | 
|  | 83 | + | 
|  | 84 | +/// How to determine the location of the `cargo` executable. | 
|  | 85 | +#[derive(Clone, Copy, Debug)] | 
|  | 86 | +pub enum CargoLocation { | 
|  | 87 | +    /// Use the `CARGO` environment variable, and fall back to `"cargo"` if it | 
|  | 88 | +    /// is not set. | 
|  | 89 | +    FromEnv, | 
|  | 90 | +} | 
|  | 91 | + | 
|  | 92 | +impl CargoLocation { | 
|  | 93 | +    fn resolve(self) -> Command { | 
|  | 94 | +        match self { | 
|  | 95 | +            CargoLocation::FromEnv => { | 
|  | 96 | +                let cargo = std::env::var_os("CARGO") | 
|  | 97 | +                    .unwrap_or_else(|| OsString::from("cargo")); | 
|  | 98 | +                Command::new(&cargo) | 
|  | 99 | +            } | 
|  | 100 | +        } | 
|  | 101 | +    } | 
|  | 102 | +} | 
|  | 103 | + | 
|  | 104 | +#[derive(Debug)] | 
|  | 105 | +struct SanitizedEnvVars { | 
|  | 106 | +    // At the moment we only ban some prefixes, but we may also want to ban env | 
|  | 107 | +    // vars by exact name in the future. | 
|  | 108 | +    prefixes: &'static [&'static str], | 
|  | 109 | +} | 
|  | 110 | + | 
|  | 111 | +impl SanitizedEnvVars { | 
|  | 112 | +    const fn new() -> Self { | 
|  | 113 | +        // Remove many of the environment variables set in | 
|  | 114 | +        // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts. | 
|  | 115 | +        // This is done to avoid recompilation with crates like ring between | 
|  | 116 | +        // `cargo clippy` and `cargo xtask clippy`. (This is really a bug in | 
|  | 117 | +        // both ring's build script and in Cargo.) | 
|  | 118 | +        // | 
|  | 119 | +        // The current list is informed by looking at ring's build script, so | 
|  | 120 | +        // it's not guaranteed to be exhaustive and it may need to grow over | 
|  | 121 | +        // time. | 
|  | 122 | +        let prefixes = &["CARGO_PKG_", "CARGO_MANIFEST_", "CARGO_CFG_"]; | 
|  | 123 | +        Self { prefixes } | 
|  | 124 | +    } | 
|  | 125 | + | 
|  | 126 | +    fn matches(&self, key: &str) -> bool { | 
|  | 127 | +        self.prefixes.iter().any(|prefix| key.starts_with(prefix)) | 
|  | 128 | +    } | 
|  | 129 | +} | 
|  | 130 | + | 
|  | 131 | +static SANITIZED_ENV_VARS: SanitizedEnvVars = SanitizedEnvVars::new(); | 
0 commit comments