From cb4dcb2753dca13ebbb77f71604de5e741a0cccf Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Sat, 12 Apr 2025 20:43:04 +0100 Subject: [PATCH] Add function for generating customisable clippy action --- rust/private/clippy.bzl | 52 ++++++++++++++++++++++++++++----- rust/private/providers.bzl | 1 + util/process_wrapper/main.rs | 8 ++++- util/process_wrapper/options.rs | 9 ++++++ 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/rust/private/clippy.bzl b/rust/private/clippy.bzl index 55bd609b98..36b948631e 100644 --- a/rust/private/clippy.bzl +++ b/rust/private/clippy.bzl @@ -99,9 +99,30 @@ def _clippy_aspect_impl(target, ctx): if hasattr(target[OutputGroupInfo], "clippy_checks"): return [] + output_format = "stderr-to-file" if ctx.attr._capture_output[CaptureClippyOutputInfo].capture_output else "stderr-and-touch-file" + + clippy_info = rust_clippy_action(target, ctx, output_format, capture_exit_code=False) + + return [ + OutputGroupInfo(clippy_checks = clippy_info.output), + clippy_info, + ] + +def rust_clippy_action(target, ctx, output_format, capture_exit_code): + """Generate an action to run clippy. + + Args: + target (Target): The target the aspect is running on. + ctx (ctx, optional): The aspect's context object. + output_format (str): One of "stderr-and-touch-file", "stderr-to-file", or "json-to-file" + capture_exit_code (boolean): If true, write the exit code to a file and exit 0. + + Returns: + ClippyInfo: A `ClippyInfo` provider. + """ crate_info = _get_clippy_ready_crate_info(target, ctx) if not crate_info: - return [ClippyInfo(output = depset([]))] + return ClippyInfo(output = depset([])) toolchain = find_toolchain(ctx) cc_toolchain, feature_configuration = find_cc_toolchain(ctx) @@ -171,7 +192,7 @@ def _clippy_aspect_impl(target, ctx): # For remote execution purposes, the clippy_out file must be a sibling of crate_info.output # or rustc may fail to create intermediate output files because the directory does not exist. - if ctx.attr._capture_output[CaptureClippyOutputInfo].capture_output: + if output_format == "stderr-to-file": clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.out", sibling = crate_info.output) args.process_wrapper_flags.add("--stderr-file", clippy_out) @@ -181,7 +202,7 @@ def _clippy_aspect_impl(target, ctx): # If we are capturing the output, we want the build system to be able to keep going # and consume the output. Some clippy lints are denials, so we cap everything at warn. args.rustc_flags.add("--cap-lints=warn") - else: + elif output_format == "stderr-and-touch-file": # A marker file indicating clippy has executed successfully. # This file is necessary because "ctx.actions.run" mandates an output. clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.ok", sibling = crate_info.output) @@ -196,6 +217,24 @@ def _clippy_aspect_impl(target, ctx): # Bazel will consider the execution result of the aspect to be "success", # and Clippy won't be re-triggered unless the source file is modified. args.rustc_flags.add("-Dwarnings") + elif output_format == "json-to-file": + clippy_out = ctx.actions.declare_file(ctx.label.name + ".clippy.out", sibling = crate_info.output) + args.process_wrapper_flags.add("--stderr-file", clippy_out) + + if clippy_flags or lint_files: + args.rustc_flags.add_all(clippy_flags) + args.rustc_flags.add("--error-format=json") + else: + fail("clippy output_format must be one of stderr-to-file|stderr-and-touch-file|json-to-file but was {}".format(output_format)) + + outputs = [clippy_out] + + if capture_exit_code: + exit_code_file = ctx.actions.declare_file(ctx.label.name + ".clippy.exit_code", sibling = crate_info.output) + args.process_wrapper_flags.add("--exit-code-file", exit_code_file) + outputs.append(exit_code_file) + else: + exit_code_file = None # Upstream clippy requires one of these two filenames or it silently uses # the default config. Enforce the naming so users are not confused. @@ -208,7 +247,7 @@ def _clippy_aspect_impl(target, ctx): ctx.actions.run( executable = ctx.executable._process_wrapper, inputs = compile_inputs, - outputs = [clippy_out], + outputs = outputs, env = env, tools = [toolchain.clippy_driver], arguments = args.all, @@ -217,10 +256,7 @@ def _clippy_aspect_impl(target, ctx): toolchain = "@rules_rust//rust:toolchain_type", ) - return [ - OutputGroupInfo(clippy_checks = depset([clippy_out])), - ClippyInfo(output = depset([clippy_out])), - ] + return ClippyInfo(output = depset([clippy_out]), exit_code_file = exit_code_file) # Example: Run the clippy checker on all targets in the codebase. # bazel build --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect \ diff --git a/rust/private/providers.bzl b/rust/private/providers.bzl index 746370bc70..4ac2299b28 100644 --- a/rust/private/providers.bzl +++ b/rust/private/providers.bzl @@ -139,6 +139,7 @@ ClippyInfo = provider( doc = "Provides information on a clippy run.", fields = { "output": "File with the clippy output.", + "exit_code_file": "File with clippy's exit code, or None if the action is expected to fail on failure.", }, ) diff --git a/util/process_wrapper/main.rs b/util/process_wrapper/main.rs index 68551abbac..b49c81ab32 100644 --- a/util/process_wrapper/main.rs +++ b/util/process_wrapper/main.rs @@ -226,7 +226,13 @@ fn main() -> Result<(), ProcessWrapperError> { } } - exit(code) + if let Some(exit_code_file) = opts.exit_code_file { + std::fs::write(exit_code_file, format!("{code}")) + .map_err(|e| ProcessWrapperError(format!("failed to write exit code file: {}", e)))?; + Ok(()) + } else { + exit(code); + } } #[cfg(test)] diff --git a/util/process_wrapper/options.rs b/util/process_wrapper/options.rs index c0f4add011..39f1aa5181 100644 --- a/util/process_wrapper/options.rs +++ b/util/process_wrapper/options.rs @@ -44,6 +44,8 @@ pub(crate) struct Options { // If set, also logs all unprocessed output from the rustc output to this file. // Meant to be used to get json output out of rustc for tooling usage. pub(crate) output_file: Option, + // If set, output the exit code of the subprocess to this file in ASCII, and unconditionally exit 0. + pub(crate) exit_code_file: Option, // If set, it configures rustc to emit an rmeta file and then // quit. pub(crate) rustc_quit_on_rmeta: bool, @@ -64,6 +66,7 @@ pub(crate) fn options() -> Result { let mut stdout_file = None; let mut stderr_file = None; let mut output_file = None; + let mut exit_code_file = None; let mut rustc_quit_on_rmeta_raw = None; let mut rustc_output_format_raw = None; let mut flags = Flags::new(); @@ -101,6 +104,11 @@ pub(crate) fn options() -> Result { "Log all unprocessed subprocess stderr in this file.", &mut output_file, ); + flags.define_flag( + "--exit-code-file", + "Log the process's exit code to this file as ASCII. Unconditionally exit with code 0 if this is set.", + &mut exit_code_file, + ); flags.define_flag( "--rustc-quit-on-rmeta", "If enabled, this wrapper will terminate rustc after rmeta has been emitted.", @@ -210,6 +218,7 @@ pub(crate) fn options() -> Result { stdout_file, stderr_file, output_file, + exit_code_file, rustc_quit_on_rmeta, rustc_output_format, })