11//! Small shared utilities for xtask subcommands.
22
33use std:: path:: { Path , PathBuf } ;
4+ use std:: process:: Command ;
45
56/// Return a copy of `path` with forward slashes replaced by the platform's
67/// main separator. No-op on POSIX where `MAIN_SEPARATOR == '/'`.
@@ -18,9 +19,128 @@ pub fn with_native_separators(path: &Path) -> PathBuf {
1819 PathBuf :: from ( s. replace ( '/' , std:: path:: MAIN_SEPARATOR_STR ) )
1920}
2021
22+ /// The package-scoped environment variables that `cargo` injects into a
23+ /// process it runs — including `cargo xtask` itself. When an xtask subcommand
24+ /// shells out to a *nested* `cargo`, these leak from the outer invocation into
25+ /// the child unless stripped.
26+ ///
27+ /// This matters because some build scripts fingerprint these vars. Most
28+ /// importantly, `ring` (the base of q2's rustls/reqwest TLS stack) fingerprints
29+ /// `CARGO_MANIFEST_DIR`: when the nested cargo inherits
30+ /// `CARGO_MANIFEST_DIR=.../crates/xtask` from the outer `cargo xtask`, the
31+ /// build script reads as dirty (`EnvVarChanged { name: "CARGO_MANIFEST_DIR",
32+ /// old_value: None, new_value: Some(".../crates/xtask") }`) and the entire
33+ /// dependency closure rebuilds — a multi-minute compile on *every* nested
34+ /// invocation whenever the previous build came from a plain shell or
35+ /// `cargo build --workspace`. Stripping these vars makes the nested cargo
36+ /// fingerprint exactly as a fresh shell would. Cargo sets the correct
37+ /// per-crate values for the crates it actually builds. See bd-awchm8w7.
38+ ///
39+ /// Only package-scoped vars are listed; environment essentials the child still
40+ /// needs (`PATH`, `CARGO_HOME`, `RUSTUP_*`, `RUSTFLAGS`, …) are deliberately
41+ /// left untouched.
42+ const INHERITED_CARGO_PKG_VARS : & [ & str ] = & [
43+ "CARGO_MANIFEST_DIR" ,
44+ "CARGO_MANIFEST_PATH" ,
45+ "CARGO_PKG_NAME" ,
46+ "CARGO_PKG_VERSION" ,
47+ "CARGO_PKG_VERSION_MAJOR" ,
48+ "CARGO_PKG_VERSION_MINOR" ,
49+ "CARGO_PKG_VERSION_PATCH" ,
50+ "CARGO_PKG_VERSION_PRE" ,
51+ "CARGO_PKG_AUTHORS" ,
52+ "CARGO_PKG_DESCRIPTION" ,
53+ "CARGO_PKG_REPOSITORY" ,
54+ "CARGO_PKG_HOMEPAGE" ,
55+ "CARGO_PKG_LICENSE" ,
56+ "CARGO_PKG_LICENSE_FILE" ,
57+ "CARGO_PKG_RUST_VERSION" ,
58+ "CARGO_PKG_README" ,
59+ "CARGO_CRATE_NAME" ,
60+ "CARGO_BIN_NAME" ,
61+ "CARGO_PRIMARY_PACKAGE" ,
62+ "OUT_DIR" ,
63+ ] ;
64+
65+ /// Mark the inherited cargo package env vars (see [`INHERITED_CARGO_PKG_VARS`])
66+ /// for removal on `cmd`, so a *nested* `cargo`/`npm` invocation spawned from an
67+ /// xtask subcommand fingerprints exactly as a fresh shell would — avoiding the
68+ /// spurious full rebuild of the TLS-stack dependency closure that an inherited
69+ /// `CARGO_MANIFEST_DIR` triggers.
70+ ///
71+ /// Use this for any nested tool invocation from xtask; prefer
72+ /// [`nested_command`] when constructing the `Command` from scratch.
73+ pub fn strip_inherited_cargo_env ( cmd : & mut Command ) -> & mut Command {
74+ for var in INHERITED_CARGO_PKG_VARS {
75+ cmd. env_remove ( var) ;
76+ }
77+ cmd
78+ }
79+
80+ /// Construct a [`Command`] for `program` with the inherited cargo package env
81+ /// vars already stripped (see [`strip_inherited_cargo_env`]). This is the
82+ /// preferred constructor for any nested `cargo`/`npm` invocation from an xtask
83+ /// subcommand.
84+ pub fn nested_command ( program : & str ) -> Command {
85+ let mut cmd = Command :: new ( program) ;
86+ strip_inherited_cargo_env ( & mut cmd) ;
87+ cmd
88+ }
89+
2190#[ cfg( test) ]
2291mod tests {
2392 use super :: * ;
93+ use std:: collections:: HashMap ;
94+
95+ /// Collect a `Command`'s env overrides into a map of name -> Option<value>,
96+ /// where `None` means the variable is marked for removal in the child.
97+ fn env_overrides ( cmd : & Command ) -> HashMap < String , Option < String > > {
98+ cmd. get_envs ( )
99+ . map ( |( k, v) | {
100+ (
101+ k. to_string_lossy ( ) . into_owned ( ) ,
102+ v. map ( |v| v. to_string_lossy ( ) . into_owned ( ) ) ,
103+ )
104+ } )
105+ . collect ( )
106+ }
107+
108+ #[ test]
109+ fn strip_marks_inherited_cargo_pkg_vars_for_removal ( ) {
110+ let mut cmd = Command :: new ( "cargo" ) ;
111+ strip_inherited_cargo_env ( & mut cmd) ;
112+ let env = env_overrides ( & cmd) ;
113+ // The variable that actually caused the ring/rustls rebuild thrash.
114+ assert_eq ! ( env. get( "CARGO_MANIFEST_DIR" ) , Some ( & None ) ) ;
115+ // A representative sample of the rest of the package-scoped vars.
116+ for var in [ "CARGO_PKG_NAME" , "CARGO_PKG_VERSION" , "OUT_DIR" ] {
117+ assert_eq ! (
118+ env. get( var) ,
119+ Some ( & None ) ,
120+ "{var} should be marked for removal"
121+ ) ;
122+ }
123+ }
124+
125+ #[ test]
126+ fn strip_does_not_touch_unrelated_or_cargo_home ( ) {
127+ // CARGO_HOME / PATH must survive — only package-scoped vars are stripped.
128+ let mut cmd = Command :: new ( "cargo" ) ;
129+ strip_inherited_cargo_env ( & mut cmd) ;
130+ let env = env_overrides ( & cmd) ;
131+ assert ! (
132+ !env. contains_key( "CARGO_HOME" ) ,
133+ "CARGO_HOME must not be stripped"
134+ ) ;
135+ assert ! ( !env. contains_key( "PATH" ) , "PATH must not be stripped" ) ;
136+ }
137+
138+ #[ test]
139+ fn nested_command_strips_pkg_vars ( ) {
140+ let cmd = nested_command ( "cargo" ) ;
141+ let env = env_overrides ( & cmd) ;
142+ assert_eq ! ( env. get( "CARGO_MANIFEST_DIR" ) , Some ( & None ) ) ;
143+ }
24144
25145 #[ test]
26146 fn with_native_separators_is_noop_on_posix_like_paths ( ) {
0 commit comments