1mod query;
7mod rust_project;
8
9use std::collections::BTreeMap;
10use std::fs;
11use std::io::{self, Write};
12use std::process::Command;
13
14use anyhow::{Context, bail};
15use camino::{Utf8Path, Utf8PathBuf};
16use clap::Parser;
17use env_logger::fmt::Formatter;
18use env_logger::{Target, WriteStyle};
19use log::{LevelFilter, Record};
20use rust_project::RustProject;
21pub use rust_project::{DiscoverProject, RustAnalyzerArg};
22use serde::de::DeserializeOwned;
23
24pub const WORKSPACE_ROOT_FILE_NAMES: &[&str] = &["MODULE.bazel", "REPO.bazel", "WORKSPACE.bazel", "WORKSPACE"];
25
26pub const BUILD_FILE_NAMES: &[&str] = &["BUILD.bazel", "BUILD"];
27
28fn find_workspace_root_file(workspace: &Utf8Path) -> anyhow::Result<Utf8PathBuf> {
34 BUILD_FILE_NAMES
35 .iter()
36 .chain(WORKSPACE_ROOT_FILE_NAMES)
37 .map(|file| workspace.join(file))
38 .find(|p| p.exists())
39 .with_context(|| format!("no root file found for bazel workspace {workspace}"))
40}
41
42fn project_discovery() -> anyhow::Result<DiscoverProject<'static>> {
43 let Config {
44 workspace,
45 execution_root,
46 output_base,
47 bazel,
48 bazel_startup_options,
49 bazel_args,
50 rust_analyzer_argument,
51 } = Config::parse()?;
52
53 log::info!("got rust-analyzer argument: {rust_analyzer_argument:?}");
54
55 let ra_arg = match rust_analyzer_argument {
56 Some(ra_arg) => ra_arg,
57 None => RustAnalyzerArg::Buildfile(find_workspace_root_file(&workspace)?),
58 };
59
60 log::info!("resolved rust-analyzer argument: {ra_arg:?}");
61
62 let (buildfile, targets) = ra_arg.into_target_details(&workspace)?;
63
64 log::debug!("got buildfile: {buildfile}");
65 log::debug!("got targets: {targets}");
66
67 let project = generate_rust_project(
69 &bazel,
70 &output_base,
71 &workspace,
72 &execution_root,
73 &bazel_startup_options,
74 &bazel_args,
75 &[targets],
76 )?;
77
78 std::fs::write(
79 workspace.join(".rust-project.bazel.json"),
80 serde_json::to_string_pretty(&project).unwrap(),
81 )
82 .context("failed to write output")?;
83
84 Ok(DiscoverProject::Finished { buildfile, project })
85}
86
87#[allow(clippy::writeln_empty_string)]
88fn write_discovery<W>(mut writer: W, discovery: DiscoverProject) -> std::io::Result<()>
89where
90 W: Write,
91{
92 serde_json::to_writer(&mut writer, &discovery)?;
93 writeln!(writer, "")
95}
96
97fn main() -> anyhow::Result<()> {
98 let log_format_fn = |fmt: &mut Formatter, rec: &Record| {
99 let message = rec.args();
100 let discovery = DiscoverProject::Progress { message };
101 write_discovery(fmt, discovery)
102 };
103
104 env_logger::Builder::from_default_env()
106 .write_style(WriteStyle::Never)
108 .format(log_format_fn)
110 .filter_level(LevelFilter::Trace)
112 .target(Target::Stdout)
113 .init();
114
115 let discovery = match project_discovery() {
116 Ok(discovery) => discovery,
117 Err(error) => DiscoverProject::Error {
118 error: error.to_string(),
119 source: error.source().as_ref().map(ToString::to_string),
120 },
121 };
122
123 write_discovery(io::stdout(), discovery)?;
124 Ok(())
125}
126
127#[derive(Debug)]
128pub struct Config {
129 workspace: Utf8PathBuf,
131
132 execution_root: Utf8PathBuf,
134
135 output_base: Utf8PathBuf,
137
138 bazel: Utf8PathBuf,
140
141 bazel_startup_options: Vec<String>,
145
146 bazel_args: Vec<String>,
150
151 rust_analyzer_argument: Option<RustAnalyzerArg>,
153}
154
155impl Config {
156 pub fn parse() -> anyhow::Result<Self> {
158 let ConfigParser {
159 workspace,
160 bazel,
161 bazel_startup_options,
162 bazel_args,
163 rust_analyzer_argument,
164 } = ConfigParser::parse();
165
166 let mut info_map = bazel_info(&bazel, workspace.as_deref(), None, &bazel_startup_options, &bazel_args)?;
168
169 let config = Config {
170 workspace: info_map
171 .remove("workspace")
172 .expect("'workspace' must exist in bazel info")
173 .into(),
174 execution_root: info_map
175 .remove("execution_root")
176 .expect("'execution_root' must exist in bazel info")
177 .into(),
178 output_base: info_map
179 .remove("output_base")
180 .expect("'output_base' must exist in bazel info")
181 .into(),
182 bazel,
183 bazel_startup_options,
184 bazel_args,
185 rust_analyzer_argument,
186 };
187
188 Ok(config)
189 }
190}
191
192#[derive(Debug, Parser)]
193struct ConfigParser {
194 #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
196 workspace: Option<Utf8PathBuf>,
197
198 #[clap(long, default_value = "bazel", env = "BAZEL")]
200 bazel: Utf8PathBuf,
201
202 #[clap(long = "bazel_startup_option")]
206 bazel_startup_options: Vec<String>,
207
208 #[clap(long = "bazel_arg")]
212 bazel_args: Vec<String>,
213
214 rust_analyzer_argument: Option<RustAnalyzerArg>,
216}
217
218#[allow(clippy::too_many_arguments)]
219pub fn generate_rust_project(
220 bazel: &Utf8Path,
221 output_base: &Utf8Path,
222 workspace: &Utf8Path,
223 execution_root: &Utf8Path,
224 bazel_startup_options: &[String],
225 bazel_args: &[String],
226 targets: &[String],
227) -> anyhow::Result<RustProject> {
228 let crate_specs = query::get_crate_specs(
229 bazel,
230 output_base,
231 workspace,
232 execution_root,
233 bazel_startup_options,
234 bazel_args,
235 targets,
236 )?;
237
238 let path = std::env::var("RUST_ANALYZER_TOOLCHAIN_PATH").context("MISSING RUST_ANALYZER_TOOLCHAIN_PATH")?;
239
240 #[cfg(bazel_runfiles)]
241 let path: Utf8PathBuf = runfiles::rlocation!(runfiles::Runfiles::create()?, path)
242 .context("toolchain runfile not found")?
243 .try_into()?;
244
245 #[cfg(not(bazel_runfiles))]
246 let path = Utf8PathBuf::from(path);
247
248 let toolchain_info = deserialize_file_content(&path, output_base, workspace, execution_root)?;
249
250 rust_project::assemble_rust_project(bazel, workspace, output_base, toolchain_info, crate_specs)
251}
252
253pub fn bazel_info(
255 bazel: &Utf8Path,
256 workspace: Option<&Utf8Path>,
257 output_base: Option<&Utf8Path>,
258 bazel_startup_options: &[String],
259 bazel_args: &[String],
260) -> anyhow::Result<BTreeMap<String, String>> {
261 let output = bazel_command(bazel, workspace, output_base)
262 .args(bazel_startup_options)
263 .arg("info")
264 .args(bazel_args)
265 .output()?;
266
267 if !output.status.success() {
268 let status = output.status;
269 let stderr = String::from_utf8_lossy(&output.stderr);
270 bail!("bazel info failed: ({status:?})\n{stderr}");
271 }
272
273 let info_map = String::from_utf8(output.stdout)?
275 .trim()
276 .split('\n')
277 .filter_map(|line| line.split_once(':'))
278 .map(|(k, v)| (k.to_owned(), v.trim().to_owned()))
279 .collect();
280
281 Ok(info_map)
282}
283
284fn bazel_command(bazel: &Utf8Path, workspace: Option<&Utf8Path>, output_base: Option<&Utf8Path>) -> Command {
285 let mut cmd = Command::new(bazel);
286
287 cmd
288 .current_dir(workspace.unwrap_or(Utf8Path::new(".")))
290 .env_remove("BAZELISK_SKIP_WRAPPER")
291 .env_remove("BUILD_WORKING_DIRECTORY")
292 .env_remove("BUILD_WORKSPACE_DIRECTORY")
293 .args(output_base.map(|s| format!("--output_base={s}")));
295
296 cmd
297}
298
299fn deserialize_file_content<T>(
300 path: &Utf8Path,
301 output_base: &Utf8Path,
302 workspace: &Utf8Path,
303 execution_root: &Utf8Path,
304) -> anyhow::Result<T>
305where
306 T: DeserializeOwned,
307{
308 let content = fs::read_to_string(path)
309 .with_context(|| format!("failed to read file: {path}"))?
310 .replace("__WORKSPACE__", workspace.as_str())
311 .replace("${pwd}", execution_root.as_str())
312 .replace("__EXEC_ROOT__", execution_root.as_str())
313 .replace("__OUTPUT_BASE__", output_base.as_str());
314
315 log::trace!("{path}\n{content}");
316
317 serde_json::from_str(&content).with_context(|| format!("failed to deserialize file: {path}"))
318}
319
320fn source_file_to_buildfile(file: &Utf8Path) -> anyhow::Result<Utf8PathBuf> {
323 file.ancestors()
325 .skip(1)
326 .flat_map(|dir| BUILD_FILE_NAMES.iter().map(move |build| dir.join(build)))
327 .find(|p| p.exists())
328 .with_context(|| format!("no buildfile found for {file}"))
329}
330
331fn buildfile_to_targets(workspace: &Utf8Path, buildfile: &Utf8Path) -> anyhow::Result<String> {
332 log::info!("getting targets for buildfile: {buildfile}");
333
334 let parent_dir = buildfile
335 .strip_prefix(workspace)
336 .with_context(|| format!("{buildfile} not part of workspace"))?
337 .parent();
338
339 let targets = match parent_dir {
340 Some(p) if !p.as_str().is_empty() => format!("//{p}:all"),
341 _ => "//...".to_string(),
342 };
343
344 Ok(targets)
345}
346
347#[derive(Debug, serde_derive::Deserialize)]
348struct ToolchainInfo {
349 sysroot: Utf8PathBuf,
350 sysroot_src: Utf8PathBuf,
351}