mirror of
https://github.com/rust-lang/rust-analyzer
synced 2025-01-15 06:33:58 +00:00
3cf28f1fc6
`linkedProjects` is owned by the user's configuration, so when users update this setting, `linkedProjects` is reset. This is problematic when `linkedProjects` also contains projects discovered with `discoverCommand`. The buggy behaviour occurred when: (1) The user configures `discoverCommand` and loads a Rust project. (2) The user changes any setting in VS Code, so rust-analyzer receives `workspace/didChangeConfiguration`. (3) `handle_did_change_configuration` ultimately calls `Client::apply_change_with_sink()`, which updates `config.user_config` and discards any items we added in `linkedProjects`. Instead, separate out `discovered_projects_from_filesystem` and `discovered_projects_from_command` from user configuration, so user settings cannot affect any type of discovered project. This fixes the subtle issue mentioned here: https://github.com/rust-lang/rust-analyzer/pull/17246#issuecomment-2185259122
242 lines
8.1 KiB
Rust
242 lines
8.1 KiB
Rust
//! In rust-analyzer, we maintain a strict separation between pure abstract
|
|
//! semantic project model and a concrete model of a particular build system.
|
|
//!
|
|
//! Pure model is represented by the [`base_db::CrateGraph`] from another crate.
|
|
//!
|
|
//! In this crate, we are concerned with "real world" project models.
|
|
//!
|
|
//! Specifically, here we have a representation for a Cargo project
|
|
//! ([`CargoWorkspace`]) and for manually specified layout ([`ProjectJson`]).
|
|
//!
|
|
//! Roughly, the things we do here are:
|
|
//!
|
|
//! * Project discovery (where's the relevant Cargo.toml for the current dir).
|
|
//! * Custom build steps (`build.rs` code generation and compilation of
|
|
//! procedural macros).
|
|
//! * Lowering of concrete model to a [`base_db::CrateGraph`]
|
|
|
|
mod build_dependencies;
|
|
mod cargo_workspace;
|
|
mod env;
|
|
mod manifest_path;
|
|
pub mod project_json;
|
|
mod rustc_cfg;
|
|
mod sysroot;
|
|
pub mod target_data_layout;
|
|
mod workspace;
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
use std::{
|
|
fmt,
|
|
fs::{self, read_dir, ReadDir},
|
|
io,
|
|
process::Command,
|
|
};
|
|
|
|
use anyhow::{bail, format_err, Context};
|
|
use paths::{AbsPath, AbsPathBuf, Utf8PathBuf};
|
|
use rustc_hash::FxHashSet;
|
|
|
|
pub use crate::{
|
|
build_dependencies::WorkspaceBuildScripts,
|
|
cargo_workspace::{
|
|
CargoConfig, CargoFeatures, CargoWorkspace, Package, PackageData, PackageDependency,
|
|
RustLibSource, Target, TargetData, TargetKind,
|
|
},
|
|
manifest_path::ManifestPath,
|
|
project_json::{ProjectJson, ProjectJsonData},
|
|
sysroot::Sysroot,
|
|
workspace::{FileLoader, PackageRoot, ProjectWorkspace, ProjectWorkspaceKind},
|
|
};
|
|
pub use cargo_metadata::Metadata;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ProjectJsonFromCommand {
|
|
/// The data describing this project, such as its dependencies.
|
|
pub data: ProjectJsonData,
|
|
/// The build system specific file that describes this project,
|
|
/// such as a `my-project/BUCK` file.
|
|
pub buildfile: AbsPathBuf,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
|
pub enum ProjectManifest {
|
|
ProjectJson(ManifestPath),
|
|
CargoToml(ManifestPath),
|
|
CargoScript(ManifestPath),
|
|
}
|
|
|
|
impl ProjectManifest {
|
|
pub fn from_manifest_file(path: AbsPathBuf) -> anyhow::Result<ProjectManifest> {
|
|
let path = ManifestPath::try_from(path)
|
|
.map_err(|path| format_err!("bad manifest path: {path}"))?;
|
|
if path.file_name().unwrap_or_default() == "rust-project.json" {
|
|
return Ok(ProjectManifest::ProjectJson(path));
|
|
}
|
|
if path.file_name().unwrap_or_default() == ".rust-project.json" {
|
|
return Ok(ProjectManifest::ProjectJson(path));
|
|
}
|
|
if path.file_name().unwrap_or_default() == "Cargo.toml" {
|
|
return Ok(ProjectManifest::CargoToml(path));
|
|
}
|
|
if path.extension().unwrap_or_default() == "rs" {
|
|
return Ok(ProjectManifest::CargoScript(path));
|
|
}
|
|
bail!("project root must point to a Cargo.toml, rust-project.json or <script>.rs file: {path}");
|
|
}
|
|
|
|
pub fn discover_single(path: &AbsPath) -> anyhow::Result<ProjectManifest> {
|
|
let mut candidates = ProjectManifest::discover(path)?;
|
|
let res = match candidates.pop() {
|
|
None => bail!("no projects"),
|
|
Some(it) => it,
|
|
};
|
|
|
|
if !candidates.is_empty() {
|
|
bail!("more than one project");
|
|
}
|
|
Ok(res)
|
|
}
|
|
|
|
pub fn discover(path: &AbsPath) -> io::Result<Vec<ProjectManifest>> {
|
|
if let Some(project_json) = find_in_parent_dirs(path, "rust-project.json") {
|
|
return Ok(vec![ProjectManifest::ProjectJson(project_json)]);
|
|
}
|
|
if let Some(project_json) = find_in_parent_dirs(path, ".rust-project.json") {
|
|
return Ok(vec![ProjectManifest::ProjectJson(project_json)]);
|
|
}
|
|
return find_cargo_toml(path)
|
|
.map(|paths| paths.into_iter().map(ProjectManifest::CargoToml).collect());
|
|
|
|
fn find_cargo_toml(path: &AbsPath) -> io::Result<Vec<ManifestPath>> {
|
|
match find_in_parent_dirs(path, "Cargo.toml") {
|
|
Some(it) => Ok(vec![it]),
|
|
None => Ok(find_cargo_toml_in_child_dir(read_dir(path)?)),
|
|
}
|
|
}
|
|
|
|
fn find_in_parent_dirs(path: &AbsPath, target_file_name: &str) -> Option<ManifestPath> {
|
|
if path.file_name().unwrap_or_default() == target_file_name {
|
|
if let Ok(manifest) = ManifestPath::try_from(path.to_path_buf()) {
|
|
return Some(manifest);
|
|
}
|
|
}
|
|
|
|
let mut curr = Some(path);
|
|
|
|
while let Some(path) = curr {
|
|
let candidate = path.join(target_file_name);
|
|
if fs::metadata(&candidate).is_ok() {
|
|
if let Ok(manifest) = ManifestPath::try_from(candidate) {
|
|
return Some(manifest);
|
|
}
|
|
}
|
|
curr = path.parent();
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<ManifestPath> {
|
|
// Only one level down to avoid cycles the easy way and stop a runaway scan with large projects
|
|
entities
|
|
.filter_map(Result::ok)
|
|
.map(|it| it.path().join("Cargo.toml"))
|
|
.filter(|it| it.exists())
|
|
.map(Utf8PathBuf::from_path_buf)
|
|
.filter_map(Result::ok)
|
|
.map(AbsPathBuf::try_from)
|
|
.filter_map(Result::ok)
|
|
.filter_map(|it| it.try_into().ok())
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
pub fn discover_all(paths: &[AbsPathBuf]) -> Vec<ProjectManifest> {
|
|
let mut res = paths
|
|
.iter()
|
|
.filter_map(|it| ProjectManifest::discover(it.as_ref()).ok())
|
|
.flatten()
|
|
.collect::<FxHashSet<_>>()
|
|
.into_iter()
|
|
.collect::<Vec<_>>();
|
|
res.sort();
|
|
res
|
|
}
|
|
|
|
pub fn manifest_path(&self) -> &ManifestPath {
|
|
match self {
|
|
ProjectManifest::ProjectJson(it)
|
|
| ProjectManifest::CargoToml(it)
|
|
| ProjectManifest::CargoScript(it) => it,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for ProjectManifest {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
fmt::Display::fmt(self.manifest_path(), f)
|
|
}
|
|
}
|
|
|
|
fn utf8_stdout(mut cmd: Command) -> anyhow::Result<String> {
|
|
let output = cmd.output().with_context(|| format!("{cmd:?} failed"))?;
|
|
if !output.status.success() {
|
|
match String::from_utf8(output.stderr) {
|
|
Ok(stderr) if !stderr.is_empty() => {
|
|
bail!("{:?} failed, {}\nstderr:\n{}", cmd, output.status, stderr)
|
|
}
|
|
_ => bail!("{:?} failed, {}", cmd, output.status),
|
|
}
|
|
}
|
|
let stdout = String::from_utf8(output.stdout)?;
|
|
Ok(stdout.trim().to_owned())
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub enum InvocationStrategy {
|
|
Once,
|
|
#[default]
|
|
PerWorkspace,
|
|
}
|
|
|
|
/// A set of cfg-overrides per crate.
|
|
#[derive(Default, Debug, Clone, Eq, PartialEq)]
|
|
pub struct CfgOverrides {
|
|
/// A global set of overrides matching all crates.
|
|
pub global: cfg::CfgDiff,
|
|
/// A set of overrides matching specific crates.
|
|
pub selective: rustc_hash::FxHashMap<String, cfg::CfgDiff>,
|
|
}
|
|
|
|
impl CfgOverrides {
|
|
pub fn len(&self) -> usize {
|
|
self.global.len() + self.selective.values().map(|it| it.len()).sum::<usize>()
|
|
}
|
|
|
|
pub fn apply(&self, cfg_options: &mut cfg::CfgOptions, name: &str) {
|
|
if !self.global.is_empty() {
|
|
cfg_options.apply_diff(self.global.clone());
|
|
};
|
|
if let Some(diff) = self.selective.get(name) {
|
|
cfg_options.apply_diff(diff.clone());
|
|
};
|
|
}
|
|
}
|
|
|
|
fn parse_cfg(s: &str) -> Result<cfg::CfgAtom, String> {
|
|
let res = match s.split_once('=') {
|
|
Some((key, value)) => {
|
|
if !(value.starts_with('"') && value.ends_with('"')) {
|
|
return Err(format!("Invalid cfg ({s:?}), value should be in quotes"));
|
|
}
|
|
let key = intern::Symbol::intern(key);
|
|
let value = intern::Symbol::intern(&value[1..value.len() - 1]);
|
|
cfg::CfgAtom::KeyValue { key, value }
|
|
}
|
|
None => cfg::CfgAtom::Flag(intern::Symbol::intern(s)),
|
|
};
|
|
Ok(res)
|
|
}
|