use anyhow::Result;
use std::path::Path;
use crate::args::RustcArgs;
use crate::cache_key::compute_cache_key;
use crate::compile;
use super::{
ArtifactKind, CompileResult, Compiler, CompilerAdapter, CompilerId, KeyCtx, RefuseReason,
classify_by_filename,
};
pub const RUSTC_ID: CompilerId = CompilerId::new("rustc");
pub const ADAPTER: CompilerAdapter =
CompilerAdapter::new(RUSTC_ID, "Rust compiler", RustcCompiler::recognizes);
pub fn classify_crate_type(crate_type: &str) -> ArtifactKind {
match crate_type {
"bin" => ArtifactKind::Executable,
"dylib" | "cdylib" | "proc-macro" => ArtifactKind::DynamicLibrary,
"lib" | "rlib" | "staticlib" => ArtifactKind::Library,
_ => ArtifactKind::Other("unknown-crate-type"),
}
}
#[derive(Default)]
pub struct RustcCompiler;
impl RustcCompiler {
pub fn new() -> Self {
Self
}
pub fn recognizes(args: &[String]) -> bool {
let Some(arg0) = args.first() else {
return false;
};
let path = Path::new(arg0);
match path.file_name() {
Some(name) => {
let name = name.to_string_lossy();
name == "rustc" || name.starts_with("rustc") || name == "clippy-driver"
}
None => false,
}
}
}
impl Compiler for RustcCompiler {
type Parsed = RustcArgs;
fn id(&self) -> CompilerId {
RUSTC_ID
}
fn parse(&self, args: &[String]) -> Result<RustcArgs> {
RustcArgs::parse(args)
}
fn refuse_reasons(&self, parsed: &RustcArgs) -> Vec<RefuseReason> {
let build_script_out_dir = std::env::var_os("OUT_DIR").map(std::path::PathBuf::from);
rustc_refuse_reasons(parsed, build_script_out_dir.as_deref())
}
fn cache_key(&self, parsed: &RustcArgs, ctx: &KeyCtx<'_, '_>) -> Result<String> {
compute_cache_key(parsed, ctx.file_hasher, ctx.path_normalizer)
}
fn execute(&self, parsed: &RustcArgs) -> Result<CompileResult> {
let workspace_root = parsed.workspace_root();
let path_normalizer =
crate::path_normalizer::PathNormalizer::from_env(workspace_root.as_deref());
compile::run_rustc(
&parsed.rustc,
parsed.inner_rustc.as_deref(),
&parsed.all_args,
parsed.output.as_deref(),
parsed.out_dir.as_deref(),
parsed.crate_name.as_deref(),
parsed.extra_filename.as_deref(),
parsed.has_coverage_instrumentation(),
&path_normalizer,
)
}
fn classify_output(&self, parsed: &RustcArgs, name: &str) -> ArtifactKind {
match classify_by_filename(name) {
ArtifactKind::Other("extensionless") => {
let any_executable_crate_type = parsed
.crate_types
.iter()
.any(|t| matches!(classify_crate_type(t), ArtifactKind::Executable));
if parsed.is_test || any_executable_crate_type {
ArtifactKind::Executable
} else {
ArtifactKind::Other("rustc:unknown")
}
}
ArtifactKind::Other(_) => ArtifactKind::Other("rustc:unknown"),
kind => kind,
}
}
}
fn rustc_refuse_reasons(
parsed: &RustcArgs,
build_script_out_dir: Option<&std::path::Path>,
) -> Vec<RefuseReason> {
let mut reasons = Vec::new();
if !parsed.is_primary {
reasons.push(RefuseReason::NotPrimary);
}
if parsed.is_build_script_probe(build_script_out_dir) {
reasons.push(RefuseReason::Unsupported(
"rustc build-script probe (not yet supported)",
));
}
reasons
}
#[cfg(test)]
mod tests {
use super::*;
fn s(args: &[&str]) -> Vec<String> {
args.iter().map(|a| a.to_string()).collect()
}
#[test]
fn recognizes_rustc_and_clippy_driver() {
assert!(RustcCompiler::recognizes(&s(&["rustc"])));
assert!(RustcCompiler::recognizes(&s(&["/usr/bin/rustc"])));
assert!(RustcCompiler::recognizes(&s(&[
"/home/user/.rustup/toolchains/stable/bin/rustc"
])));
assert!(RustcCompiler::recognizes(&s(&["clippy-driver"])));
assert!(RustcCompiler::recognizes(&s(&[
"/path/to/bin/clippy-driver"
])));
assert!(!RustcCompiler::recognizes(&s(&["gcc"])));
assert!(!RustcCompiler::recognizes(&s(&["--crate-name"])));
assert!(!RustcCompiler::recognizes(&[]));
}
#[test]
fn id_is_rustc() {
assert_eq!(RustcCompiler::new().id(), RUSTC_ID);
}
#[test]
fn adapter_descriptor_uses_rustc_recognizer() {
assert_eq!(ADAPTER.id(), RUSTC_ID);
assert!(ADAPTER.recognizes(&s(&["rustc"])));
assert!(!ADAPTER.recognizes(&s(&["cc"])));
}
#[test]
fn refuse_reasons_returns_not_primary_for_query_invocations() {
let parsed = RustcCompiler::new().parse(&s(&["rustc", "-vV"])).unwrap();
let reasons = RustcCompiler::new().refuse_reasons(&parsed);
assert!(matches!(reasons.as_slice(), [RefuseReason::NotPrimary]));
}
#[test]
fn refuse_reasons_empty_for_primary_compilation() {
let parsed = RustcCompiler::new()
.parse(&s(&[
"rustc",
"src/lib.rs",
"--crate-name",
"foo",
"--crate-type",
"lib",
]))
.unwrap();
assert!(parsed.is_primary);
let reasons = RustcCompiler::new().refuse_reasons(&parsed);
assert!(reasons.is_empty());
}
#[test]
fn refuse_reasons_identifies_build_script_probe() {
let parsed = RustcCompiler::new()
.parse(&s(&[
"rustc",
"--edition=2018",
"--crate-name=thiserror",
"--crate-type=lib",
"--emit=dep-info,metadata",
"--out-dir",
"/work/proj/target/release/build/thiserror-abc/out/probe",
"build/probe.rs",
]))
.unwrap();
let reasons = rustc_refuse_reasons(
&parsed,
Some(std::path::Path::new(
"/work/proj/target/release/build/thiserror-abc/out",
)),
);
assert_eq!(reasons.len(), 1);
assert_eq!(
reasons[0].description(),
"rustc build-script probe (not yet supported)"
);
}
fn lib_args() -> RustcArgs {
RustcCompiler::new()
.parse(&s(&[
"rustc",
"src/lib.rs",
"--crate-name",
"foo",
"--crate-type",
"lib",
]))
.unwrap()
}
fn bin_args() -> RustcArgs {
RustcCompiler::new()
.parse(&s(&[
"rustc",
"src/main.rs",
"--crate-name",
"foo",
"--crate-type",
"bin",
]))
.unwrap()
}
#[test]
fn classify_output_recognizes_rust_library_artifacts() {
let c = RustcCompiler::new();
let args = lib_args();
assert_eq!(
c.classify_output(&args, "libfoo-abc123.rlib"),
ArtifactKind::Library
);
assert_eq!(
c.classify_output(&args, "libfoo-abc123.rmeta"),
ArtifactKind::Metadata
);
assert_eq!(
c.classify_output(&args, "foo-abc123.d"),
ArtifactKind::DepInfo
);
}
#[test]
fn classify_output_recognizes_object_files_including_rcgu() {
let c = RustcCompiler::new();
let args = bin_args();
assert_eq!(
c.classify_output(&args, "foo-abc.123.rcgu.o"),
ArtifactKind::Object
);
assert_eq!(c.classify_output(&args, "foo.o"), ArtifactKind::Object);
}
#[test]
fn classify_output_recognizes_dynamic_libraries_per_platform() {
let c = RustcCompiler::new();
let args = lib_args();
assert_eq!(
c.classify_output(&args, "libfoo.dylib"),
ArtifactKind::DynamicLibrary
);
assert_eq!(
c.classify_output(&args, "libfoo.so"),
ArtifactKind::DynamicLibrary
);
assert_eq!(
c.classify_output(&args, "foo.dll"),
ArtifactKind::DynamicLibrary
);
}
#[test]
fn classify_output_recognizes_debug_sidecars() {
let c = RustcCompiler::new();
let args = bin_args();
assert_eq!(
c.classify_output(&args, "foo-abc.dwo"),
ArtifactKind::DebugSidecar
);
assert_eq!(
c.classify_output(&args, "foo.pdb"),
ArtifactKind::DebugSidecar
);
}
#[test]
fn classify_output_treats_extensionless_bin_outputs_as_executable() {
let c = RustcCompiler::new();
let args = bin_args();
assert_eq!(
c.classify_output(&args, "foo-abc123"),
ArtifactKind::Executable
);
assert_eq!(
c.classify_output(&args, "foo.exe"),
ArtifactKind::Executable
);
}
#[test]
fn classify_output_falls_back_to_other_for_unrecognized_in_lib_build() {
let c = RustcCompiler::new();
let args = lib_args();
match c.classify_output(&args, "mystery-file") {
ArtifactKind::Other(_) => {}
other => panic!("expected Other, got {other:?}"),
}
}
#[test]
fn classify_crate_type_maps_known_rustc_types() {
assert_eq!(classify_crate_type("bin"), ArtifactKind::Executable);
assert_eq!(classify_crate_type("dylib"), ArtifactKind::DynamicLibrary);
assert_eq!(classify_crate_type("cdylib"), ArtifactKind::DynamicLibrary);
assert_eq!(
classify_crate_type("proc-macro"),
ArtifactKind::DynamicLibrary
);
assert_eq!(classify_crate_type("lib"), ArtifactKind::Library);
assert_eq!(classify_crate_type("rlib"), ArtifactKind::Library);
assert_eq!(classify_crate_type("staticlib"), ArtifactKind::Library);
match classify_crate_type("future-rustc-type-2030") {
ArtifactKind::Other(_) => {}
other => panic!("expected Other, got {other:?}"),
}
}
#[test]
fn classify_crate_type_link_strategy_matches_is_executable_output() {
use crate::link::LinkStrategy;
let executable_types = ["bin", "dylib", "cdylib", "proc-macro"];
for t in executable_types {
assert_eq!(
classify_crate_type(t).link_strategy(),
LinkStrategy::Copy,
"{t} should be Copy strategy"
);
}
let library_types = ["lib", "rlib", "staticlib"];
for t in library_types {
assert_eq!(
classify_crate_type(t).link_strategy(),
LinkStrategy::Hardlink,
"{t} should be Hardlink strategy"
);
}
}
}