rustc_plugin/
cli.rs

1use std::{
2  env, fs,
3  path::PathBuf,
4  process::{Command, Stdio, exit},
5};
6
7use cargo_metadata::camino::Utf8Path;
8
9use super::plugin::{PLUGIN_ARGS, RustcPlugin};
10use crate::CrateFilter;
11
12pub const RUN_ON_ALL_CRATES: &str = "RUSTC_PLUGIN_ALL_TARGETS";
13pub const SPECIFIC_CRATE: &str = "SPECIFIC_CRATE";
14pub const SPECIFIC_TARGET: &str = "SPECIFIC_TARGET";
15pub const CARGO_VERBOSE: &str = "CARGO_VERBOSE";
16
17/// The top-level function that should be called in your user-facing binary.
18pub fn cli_main<T: RustcPlugin>(plugin: T) {
19  if env::args().any(|arg| arg == "-V") {
20    println!("{}", plugin.version());
21    return;
22  }
23
24  let metadata = cargo_metadata::MetadataCommand::new()
25    .no_deps()
26    .other_options(["--all-features".to_string(), "--offline".to_string()])
27    .exec()
28    .unwrap();
29  let plugin_subdir = format!("plugin-{}", env!("RUSTC_CHANNEL"));
30  let target_dir = metadata.target_directory.join(plugin_subdir);
31
32  let args = plugin.args(&target_dir);
33
34  let mut cmd = Command::new("cargo");
35  cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
36
37  let mut path = env::current_exe()
38    .expect("current executable path invalid")
39    .with_file_name(plugin.driver_name().as_ref());
40
41  if cfg!(windows) {
42    path.set_extension("exe");
43  }
44
45  cmd
46    .env("RUSTC_WORKSPACE_WRAPPER", path)
47    .args(["check", "--target-dir"])
48    .arg(&target_dir);
49
50  if env::var(CARGO_VERBOSE).is_ok() {
51    cmd.arg("-vv");
52  } else {
53    cmd.arg("-q");
54  }
55
56  let workspace_members = metadata
57    .workspace_members
58    .iter()
59    .map(|pkg_id| {
60      metadata
61        .packages
62        .iter()
63        .find(|pkg| &pkg.id == pkg_id)
64        .unwrap()
65    })
66    .collect::<Vec<_>>();
67
68  match args.filter {
69    CrateFilter::CrateContainingFile(file_path) => {
70      only_run_on_file(&mut cmd, file_path, &workspace_members, &target_dir);
71    }
72    CrateFilter::AllCrates | CrateFilter::OnlyWorkspace => {
73      cmd.arg("--all");
74      match args.filter {
75        CrateFilter::AllCrates => {
76          cmd.env(RUN_ON_ALL_CRATES, "");
77        }
78        CrateFilter::OnlyWorkspace => {}
79        CrateFilter::CrateContainingFile(_) => unreachable!(),
80      }
81    }
82  }
83
84  let args_str = serde_json::to_string(&args.args).unwrap();
85  log::debug!("{PLUGIN_ARGS}={args_str}");
86  cmd.env(PLUGIN_ARGS, args_str);
87
88  // HACK: if running on the rustc codebase, this env var needs to exist
89  // for the code to compile
90  if workspace_members.iter().any(|pkg| pkg.name == "rustc-main") {
91    cmd.env("CFG_RELEASE", "");
92  }
93
94  plugin.modify_cargo(&mut cmd, &args.args);
95
96  let exit_status = cmd.status().expect("failed to wait for cargo?");
97
98  exit(exit_status.code().unwrap_or(-1));
99}
100
101fn only_run_on_file(
102  cmd: &mut Command,
103  file_path: PathBuf,
104  workspace_members: &[&cargo_metadata::Package],
105  target_dir: &Utf8Path,
106) {
107  // We compare this against canonicalized paths, so it must be canonicalized too
108  let file_path = file_path.canonicalize().unwrap();
109
110  // Find the package and target that corresponds to a given file path
111  let mut matching = workspace_members
112    .iter()
113    .filter_map(|pkg| {
114      let targets = pkg
115        .targets
116        .iter()
117        .filter(|target| {
118          let src_path = target.src_path.canonicalize().unwrap();
119          log::trace!("Package {} has src path {}", pkg.name, src_path.display());
120          file_path.starts_with(src_path.parent().unwrap())
121        })
122        .collect::<Vec<_>>();
123
124      let target = (match targets.len() {
125        0 => None,
126        1 => Some(targets[0]),
127        _ => {
128          // If there are multiple targets that match a given directory, e.g. `examples/whatever.rs`, then
129          // find the target whose name matches the file stem
130          let stem = file_path.file_stem().unwrap().to_string_lossy();
131          let name_matches_stem = targets
132            .clone()
133            .into_iter()
134            .find(|target| target.name == stem);
135
136          // Otherwise we're in a special case, e.g. "main.rs" corresponds to the bin target.
137          name_matches_stem.or_else(|| {
138            let only_bin = targets
139              .iter()
140              .all(|target| !target.kind.contains(&"lib".into()));
141            // TODO: this is a pile of hacks, and it seems like there is no reliable way to say
142            // which target a file will correspond to given only its filename. For example,
143            // if you have src/foo.rs it could either be imported by src/main.rs, or src/lib.rs, or
144            // even both!
145            if only_bin {
146              targets
147                .into_iter()
148                .find(|target| target.kind.contains(&"bin".into()))
149            } else {
150              let kind = (if stem == "main" { "bin" } else { "lib" }).to_string();
151              targets
152                .into_iter()
153                .find(|target| target.kind.contains(&kind))
154            }
155          })
156        }
157      })?;
158
159      Some((pkg, target))
160    })
161    .collect::<Vec<_>>();
162  let (pkg, target) = match matching.len() {
163    0 => panic!("Could not find target for path: {}", file_path.display()),
164    1 => matching.remove(0),
165    _ => panic!("Too many matching targets: {matching:?}"),
166  };
167
168  // Add compile filter to specify the target corresponding to the given file
169  cmd.arg("-p").arg(format!("{}:{}", pkg.name, pkg.version));
170
171  // See https://doc.rust-lang.org/cargo/commands/cargo-check.html#target-selection for possible compile kinds
172  enum CompileKind {
173    Lib,
174    Bin,
175    Example,
176    Test,
177    Bench,
178    ProcMacro,
179  }
180
181  // kind string should be one of the ones listed here:
182  // https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field
183  let kind_str = &target.kind[0];
184  let kind = match kind_str.as_str() {
185    "lib" | "rlib" | "dylib" | "staticlib" | "cdylib" => CompileKind::Lib,
186    "bin" => CompileKind::Bin,
187    "proc-macro" => CompileKind::ProcMacro,
188    "example" => CompileKind::Example,
189    "test" => CompileKind::Test,
190    "bench" => CompileKind::Bench,
191    _ => unreachable!("unexpected cargo crate type: {kind_str}"),
192  };
193
194  match kind {
195    CompileKind::Lib => {
196      // If the rmeta files were previously generated for the lib (e.g. by running the plugin
197      // on a reverse-dep), then we have to remove them or else Cargo will memoize the plugin.
198      let deps_dir = target_dir.join("debug").join("deps");
199      if let Ok(entries) = fs::read_dir(deps_dir) {
200        let prefix = format!("lib{}", pkg.name.replace('-', "_"));
201        for entry in entries {
202          let path = entry.unwrap().path();
203          if let Some(file_name) = path.file_name()
204            && file_name.to_string_lossy().starts_with(&prefix)
205          {
206            fs::remove_file(path).unwrap();
207          }
208        }
209      }
210
211      cmd.arg("--lib");
212    }
213    CompileKind::Bin => {
214      cmd.args(["--bin", &target.name]);
215    }
216    CompileKind::ProcMacro => {}
217    CompileKind::Example => {
218      cmd.args(["--example", &target.name]);
219    }
220    CompileKind::Test => {
221      cmd.args(["--test", &target.name]);
222    }
223    CompileKind::Bench => {
224      cmd.args(["--bench", &target.name]);
225    }
226  }
227
228  cmd.env(
229    SPECIFIC_CRATE,
230    match kind {
231      CompileKind::Lib => &pkg.name,
232      CompileKind::Bin => &pkg.name,
233      CompileKind::Example => &target.name,
234      CompileKind::Test => &target.name,
235      CompileKind::Bench => &target.name,
236      CompileKind::ProcMacro => &pkg.name,
237    }
238    .replace('-', "_"),
239  );
240  cmd.env(SPECIFIC_TARGET, match kind {
241    CompileKind::Bench | CompileKind::Example => "bin",
242    _ => kind_str,
243  });
244
245  log::debug!(
246    "Package: {}, target kind {}, target name {}",
247    pkg.name,
248    kind_str,
249    target.name
250  );
251}