Skip to main content

xtask_no_warnings/
lib.rs

1#![allow(clippy::needless_doctest_main)]
2
3//! Silence warnings in [xtask][xtask] builds without invalidating the dependency cache.
4//!
5//! # Purpose
6//!
7//! This is a micro crate with zero dependencies for use with xtask during development.
8//!
9//! The standard way to silence compiler warnings during development is to set
10//! `RUSTFLAGS=-Awarnings`. It works, but it has a painful side effect: `RUSTFLAGS` is part of the
11//! compiler fingerprint for **every** crate in the build graph. Toggling it forces Cargo to
12//! recompile the entire project from scratch, including all dependencies. On machines with limited
13//! resources (e.g. low-specs laptops, handheld devices, ...) this means minutes of wasted build
14//! time every single time you flip the flag.
15//!
16//! This crate solves the problem by using [`RUSTC_WORKSPACE_WRAPPER`][workspace_wrapper] instead.
17//! `RUSTC_WORKSPACE_WRAPPER` routes `rustc` invocations through a wrapper binary but **only for
18//! workspace members**. Dependencies are compiled by `rustc` directly and their cached artifacts
19//! remain valid regardless of whether the wrapper is active.
20//!
21//! The wrapper here is the xtask binary itself. At startup, [`init`] checks for a sentinel
22//! environment variable. When Cargo invokes the xtask as a rustc wrapper, `init` forwards all
23//! arguments to the real `rustc` with `-Awarnings` prepended and then exits. When the developer
24//! invokes the xtask normally, `init` is a no-op and the rest of the `main` function runs as
25//! usual.
26//!
27//! Because `RUSTC_WORKSPACE_WRAPPER` produces artifacts under a **separate fingerprint** from a
28//! plain `rustc` run, the two modes (warning on or off) maintain independent caches for workspace
29//! members. The very first toggle in each direction recompiles those crates, every subsequent
30//! toggle hits the cache immediately.
31//!
32//! # Usage
33//!
34//! ## 1. Add the dependency to your xtask
35//!
36//! `xtask/Cargo.toml`
37//! ```toml
38//! [dependencies]
39//! xtask-no-warnings = "0.1"
40//! ```
41//!
42//! ## 2. Call init at the top of main
43//!
44//! `xtask/src/main.rs`
45//! ```rust,no_run
46//! fn main() {
47//!     xtask_no_warnings::init();
48//!
49//!     // Your xtask logic here.
50//! }
51//! ```
52//!
53//! `init` must be the very first statement so that when Cargo invokes the xtask as a rustc
54//! wrapper, it exits immediately before any of your setup code runs.
55//!
56//! ## 3. Spawn Cargo with or without warnings
57//!
58//! ### Option A - `cargo_command`
59//!
60//! This function returns a `Command` for Cargo with the wrapper environment variable already set.
61//! Append your subcommand and flags before running it.
62//!
63//! ```rust,no_run
64//! fn build(no_warnings: bool) {
65//!     let mut cmd = if no_warnings {
66//!         xtask_no_warnings::cargo_command()
67//!     } else {
68//!         std::process::Command::new(std::env::var_os("CARGO").unwrap_or("cargo".into()))
69//!     };
70//!
71//!     cmd.args(["build", "--release"])
72//!         .status()
73//!         .expect("cargo failed");
74//! }
75//! ```
76//!
77//! ### Option B - `setup`
78//!
79//! This function configures the current process to act as a workspace wrapper. Useful when you are
80//! building the `Command` yourself and only want to add the wrapper conditionally.
81//!
82//!
83//! ```rust,no_run
84//! fn build(no_warnings: bool) {
85//!     let mut cmd = std::process::Command::new("cargo");
86//!     cmd.args(["build", "--release"]);
87//!
88//!     if no_warnings {
89//!         unsafe { xtask_no_warnings::setup(); }
90//!     }
91//!
92//!     cmd.status().expect("cargo failed");
93//! }
94//! ```
95//!
96//! ## Basic xtask setup
97//!
98//! A typical project using a xtask workspace member looks like this:
99//! ```toml
100//! my-project/
101//!   Cargo.toml
102//!   .cargo/
103//!     config.toml
104//!   src/
105//!     lib.rs
106//!   xtask/
107//!     Cargo.toml
108//!     src/main.rs
109//! ```
110//!
111//! To create it the `xtask`, you can use `cargo new xtask` in the root of your project, you can
112//! then create the `.cargo/config.toml` that should contains the following:
113//! ```toml
114//! [alias]
115//! xtask = "run --package xtask --"
116//! ```
117//!
118//! You should be able to invoke your xtask with `cargo xtask <task>`. For more information, check
119//! the [xtask][xtask] repository.
120//!
121//! [xtask]: https://github.com/matklad/cargo-xtask
122//! [workspace_wrapper]: https://doc.rust-lang.org/cargo/reference/config.html#buildrustc-workspace-wrapper
123
124use std::process::Command;
125
126/// Sentinel environment variable used to distinguish wrapper invocations from normal xtask
127/// invocations.
128const ENV_KEY: &str = "XTASK_RUSTC_WRAPPER";
129
130/// Handle a potential rustc wrapper invocation, then return.
131///
132/// Call this at the very **first** statement in your xtask `main` function. When Cargo is invoking
133/// the xtask binary as a `RUSTC_WORKSPACE_WRAPPER`, this function runs `rustc -Awarnings
134/// <original-args>` and terminates the process. When the xtask is invoked normally by the
135/// developer, this function is a no-op and returns immediately, so the rest of `main` executes as
136/// usual.
137///
138/// # Panics
139///
140/// Panics if the process is running as a rustc wrapper but the rustc path argument is missing or
141/// if the rustc subprocess cannot be spawned.
142///
143/// # Example
144///
145/// ```rust,no_run
146/// fn main() {
147///     xtask_no_warnings::init();
148///
149///     // Your xtask logic starts here.
150/// }
151/// ```
152pub fn init() {
153    if std::env::var_os(ENV_KEY).is_none() {
154        return;
155    }
156
157    let mut args = std::env::args_os().skip(1);
158    let rustc = args.next().expect("no rustc path was provided");
159
160    let status = Command::new(&rustc)
161        .arg("-Awarnings")
162        .args(args)
163        .status()
164        .unwrap_or_else(|e| panic!("failed to spawn rustc (`{}`): {e}", rustc.to_string_lossy()));
165    std::process::exit(status.code().unwrap_or(1));
166}
167
168/// Configures the current process to act as a workspace wrapper.
169///
170/// This sets two environment variables on the current process:
171///
172/// - `RUSTC_WORKSPACE_WRAPPER` - points to the current xtask executable so that Cargo routes
173///   workspace member compilation through it.
174/// - `XTASK_RUSTC_WRAPPER` - a sentinel that `init` uses to detect wrapper invocations.
175///
176/// Dependencies are **not** wrapped and their cached artifacts remain valid regardless of whether
177/// you call this function.
178///
179/// # Safety
180///
181/// This function uses [`std::env::set_var`] which is unsafe if called concurrently from multiple
182/// threads. Callers must ensure this function is not invoked from multiple threads simultaneously.
183/// See https://doc.rust-lang.org/std/env/fn.set_var.html
184///
185/// # Panics
186///
187/// Panics if the path to the current executable cannot be determined.
188///
189/// # Example
190///
191/// ```rust,no_run
192/// fn build(no_warnings: bool) {
193///     let mut cmd = std::process::Command::new("cargo");
194///     cmd.args(["build", "--release"]);
195///
196///     if no_warnings {
197///         unsafe { xtask_no_warnings::setup(); }
198///     }
199///
200///     cmd.status().expect("cargo failed");
201/// }
202/// ```
203pub unsafe fn setup() {
204    let wrapper =
205        std::env::current_exe().expect("cannot determine the path to the current executable");
206
207    unsafe {
208        std::env::set_var("RUSTC_WORKSPACE_WRAPPER", wrapper);
209        std::env::set_var(ENV_KEY, "1");
210    }
211}
212
213/// Return a Cargo `Command` pre-configured to suppress warnings in workspace members.
214///
215/// The returned command already has `RUSTC_WORKSPACE_WRAPPER` and `XTASK_RUSTC_WRAPPER` set, you
216/// only need to append subcommand and flags.
217///
218/// The Cargo executable is taken from the `CARGO` environment variable when available (which Cargo
219/// sets automatically), falling back to `cargo` if not set.
220///
221/// # Panics
222///
223/// Panics if the path to the current executable cannot be determined.
224///
225/// # Example
226///
227/// ```rust,no_run
228/// fn build_without_warning() {
229///     xtask_no_warnings::cargo_command()
230///         .args(["build", "--release"])
231///         .status()
232///         .expect("cargo command failed");
233/// }
234/// ```
235pub fn cargo_command() -> Command {
236    let wrapper =
237        std::env::current_exe().expect("cannot determine the path to the current executable");
238    let cargo = std::env::var_os("CARGO").unwrap_or("cargo".into());
239    let mut cmd = Command::new(cargo);
240    cmd.env("RUSTC_WORKSPACE_WRAPPER", wrapper);
241    cmd.env(ENV_KEY, "1");
242    cmd
243}