blob: 4ea641e6a024c8042e4455e0b678b209190926f5 [file] [log] [blame]
mod aquery;
mod rust_project;
use std::{collections::BTreeMap, convert::TryInto, fs, process::Command};
use anyhow::{bail, Context};
use camino::{Utf8Path, Utf8PathBuf};
use runfiles::Runfiles;
use rust_project::RustProject;
pub use rust_project::{DiscoverProject, RustAnalyzerArg};
use serde::{de::DeserializeOwned, Deserialize};
pub const WORKSPACE_ROOT_FILE_NAMES: &[&str] =
&["MODULE.bazel", "REPO.bazel", "WORKSPACE.bazel", "WORKSPACE"];
pub const BUILD_FILE_NAMES: &[&str] = &["BUILD.bazel", "BUILD"];
#[allow(clippy::too_many_arguments)]
pub fn generate_rust_project(
bazel: &Utf8Path,
output_base: &Utf8Path,
workspace: &Utf8Path,
execution_root: &Utf8Path,
bazel_startup_options: &[String],
bazel_args: &[String],
rules_rust_name: &str,
targets: &[String],
) -> anyhow::Result<RustProject> {
generate_crate_info(
bazel,
output_base,
workspace,
bazel_startup_options,
bazel_args,
rules_rust_name,
targets,
)?;
let crate_specs = aquery::get_crate_specs(
bazel,
output_base,
workspace,
execution_root,
bazel_startup_options,
bazel_args,
targets,
rules_rust_name,
)?;
let path: Utf8PathBuf = runfiles::rlocation!(
Runfiles::create()?,
"rules_rust/rust/private/rust_analyzer_detect_sysroot.rust_analyzer_toolchain.json"
)
.context("toolchain runfile not found")?
.try_into()?;
let toolchain_info = deserialize_file_content(&path, output_base, workspace, execution_root)?;
rust_project::assemble_rust_project(bazel, workspace, toolchain_info, &crate_specs)
}
/// Executes `bazel info` to get a map of context information.
pub fn bazel_info(
bazel: &Utf8Path,
workspace: Option<&Utf8Path>,
output_base: Option<&Utf8Path>,
bazel_startup_options: &[String],
bazel_args: &[String],
) -> anyhow::Result<BTreeMap<String, String>> {
let output = bazel_command(bazel, workspace, output_base)
.args(bazel_startup_options)
.arg("info")
.args(bazel_args)
.output()?;
if !output.status.success() {
let status = output.status;
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("bazel info failed: ({status:?})\n{stderr}");
}
// Extract and parse the output.
let info_map = String::from_utf8(output.stdout)?
.trim()
.split('\n')
.filter_map(|line| line.split_once(':'))
.map(|(k, v)| (k.to_owned(), v.trim().to_owned()))
.collect();
Ok(info_map)
}
fn generate_crate_info(
bazel: &Utf8Path,
output_base: &Utf8Path,
workspace: &Utf8Path,
bazel_startup_options: &[String],
bazel_args: &[String],
rules_rust: &str,
targets: &[String],
) -> anyhow::Result<()> {
log::info!("running bazel build...");
log::debug!("Building rust_analyzer_crate_spec files for {:?}", targets);
let output = bazel_command(bazel, Some(workspace), Some(output_base))
.args(bazel_startup_options)
.arg("build")
.args(bazel_args)
.arg("--norun_validations")
.arg("--remote_download_all")
.arg(format!(
"--aspects={rules_rust}//rust:defs.bzl%rust_analyzer_aspect"
))
.arg("--output_groups=rust_analyzer_crate_spec,rust_generated_srcs,rust_analyzer_proc_macro_dylib,rust_analyzer_src")
.args(targets)
.output()?;
if !output.status.success() {
let status = output.status;
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("bazel build failed: ({status})\n{stderr}");
}
log::info!("bazel build finished");
Ok(())
}
fn bazel_command(
bazel: &Utf8Path,
workspace: Option<&Utf8Path>,
output_base: Option<&Utf8Path>,
) -> Command {
let mut cmd = Command::new(bazel);
cmd
// Switch to the workspace directory if one was provided.
.current_dir(workspace.unwrap_or(Utf8Path::new(".")))
.env_remove("BAZELISK_SKIP_WRAPPER")
.env_remove("BUILD_WORKING_DIRECTORY")
.env_remove("BUILD_WORKSPACE_DIRECTORY")
// Set the output_base if one was provided.
.args(output_base.map(|s| format!("--output_base={s}")));
cmd
}
fn deserialize_file_content<T>(
path: &Utf8Path,
output_base: &Utf8Path,
workspace: &Utf8Path,
execution_root: &Utf8Path,
) -> anyhow::Result<T>
where
T: DeserializeOwned,
{
let content = fs::read_to_string(path)
.with_context(|| format!("failed to read file: {path}"))?
.replace("__WORKSPACE__", workspace.as_str())
.replace("${pwd}", execution_root.as_str())
.replace("__EXEC_ROOT__", execution_root.as_str())
.replace("__OUTPUT_BASE__", output_base.as_str());
log::trace!("{}\n{}", path, content);
serde_json::from_str(&content).with_context(|| format!("failed to deserialize file: {path}"))
}
/// `rust-analyzer` associates workspaces with buildfiles. Therefore, when it passes in a
/// source file path, we use this function to identify the buildfile the file belongs to.
fn source_file_to_buildfile(file: &Utf8Path) -> anyhow::Result<Utf8PathBuf> {
// Skip the first element as it's always the full file path.
file.ancestors()
.skip(1)
.flat_map(|dir| BUILD_FILE_NAMES.iter().map(move |build| dir.join(build)))
.find(|p| p.exists())
.with_context(|| format!("no buildfile found for {file}"))
}
fn buildfile_to_targets(workspace: &Utf8Path, buildfile: &Utf8Path) -> anyhow::Result<String> {
log::info!("getting targets for buildfile: {buildfile}");
let parent_dir = buildfile
.strip_prefix(workspace)
.with_context(|| format!("{buildfile} not part of workspace"))?
.parent();
let targets = match parent_dir {
Some(p) if !p.as_str().is_empty() => format!("//{p}:all"),
_ => "//...".to_string(),
};
Ok(targets)
}
#[derive(Debug, Deserialize)]
struct ToolchainInfo {
sysroot: Utf8PathBuf,
sysroot_src: Utf8PathBuf,
}