2021-05-22 13:53:47 +00:00
|
|
|
//! 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 conserned 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`]
|
2019-09-30 08:58:53 +00:00
|
|
|
|
2021-07-19 18:20:10 +00:00
|
|
|
mod manifest_path;
|
2019-02-05 22:10:49 +00:00
|
|
|
mod cargo_workspace;
|
2021-01-05 18:02:13 +00:00
|
|
|
mod cfg_flag;
|
2020-06-24 12:57:37 +00:00
|
|
|
mod project_json;
|
2019-02-05 22:10:49 +00:00
|
|
|
mod sysroot;
|
2020-11-13 15:44:48 +00:00
|
|
|
mod workspace;
|
2021-01-18 11:52:12 +00:00
|
|
|
mod rustc_cfg;
|
2021-07-18 08:29:22 +00:00
|
|
|
mod build_scripts;
|
2019-02-05 22:10:49 +00:00
|
|
|
|
internal: add simple smoke test for project model
Our project model code is rather complicated -- the logic for lowering
from `cargo metadata` to `CrateGraph` is fiddly and special-case. So
far, we survived without testing this at all, but this increasingly
seems like a poor option.
So this PR introduces a simple tests just to detect the most obvious
failures. The idea here is that, although we rely on external processes
(cargo & rustc), we are actually using their stable interfaces, so we
might just mock out the outputs.
Long term, I would like to try to virtualize IO here, so as to do such
mocking in a more principled way, but lets start simple.
Should we forgo the mocking and just call `cargo metadata` directly
perhaps? Touch question -- I personally feel that fast, in-process tests
are more important in this case than any extra assurance we get from
running the real thing.
Super-long term, we would probably want to extend our heavy tests to
cover more use-cases, but we should figure a way to do that without
slowing the tests down for everyone.
Perhaps we need two-tiered bors system, where we pull from `master` into
`release` branch only when an additional set of tests passes?
2021-07-20 12:38:20 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests;
|
|
|
|
|
2019-03-05 21:29:23 +00:00
|
|
|
use std::{
|
2021-07-17 13:43:33 +00:00
|
|
|
fs::{self, read_dir, ReadDir},
|
2020-06-24 13:52:07 +00:00
|
|
|
io,
|
2020-07-23 16:57:27 +00:00
|
|
|
process::Command,
|
2019-03-05 21:29:23 +00:00
|
|
|
};
|
2019-02-05 22:10:49 +00:00
|
|
|
|
2021-07-19 18:20:10 +00:00
|
|
|
use anyhow::{bail, format_err, Context, Result};
|
2020-08-13 08:32:19 +00:00
|
|
|
use paths::{AbsPath, AbsPathBuf};
|
2020-11-13 15:44:48 +00:00
|
|
|
use rustc_hash::FxHashSet;
|
2019-03-21 08:43:47 +00:00
|
|
|
|
2019-02-05 22:10:49 +00:00
|
|
|
pub use crate::{
|
2021-07-18 08:29:22 +00:00
|
|
|
build_scripts::WorkspaceBuildScripts,
|
2021-01-05 18:02:13 +00:00
|
|
|
cargo_workspace::{
|
2021-02-11 16:34:56 +00:00
|
|
|
CargoConfig, CargoWorkspace, Package, PackageData, PackageDependency, RustcSource, Target,
|
2021-08-23 17:18:11 +00:00
|
|
|
TargetData, TargetKind, UnsetTestCrates,
|
2021-01-05 18:02:13 +00:00
|
|
|
},
|
2021-07-19 18:20:10 +00:00
|
|
|
manifest_path::ManifestPath,
|
2020-06-24 13:52:07 +00:00
|
|
|
project_json::{ProjectJson, ProjectJsonData},
|
2019-02-05 22:10:49 +00:00
|
|
|
sysroot::Sysroot,
|
2021-06-14 04:41:46 +00:00
|
|
|
workspace::{CfgOverrides, PackageRoot, ProjectWorkspace},
|
2019-02-05 22:10:49 +00:00
|
|
|
};
|
2020-07-23 16:57:27 +00:00
|
|
|
|
2020-06-03 09:44:51 +00:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
2020-06-03 10:05:50 +00:00
|
|
|
pub enum ProjectManifest {
|
2021-07-19 18:20:10 +00:00
|
|
|
ProjectJson(ManifestPath),
|
|
|
|
CargoToml(ManifestPath),
|
2020-04-16 20:02:10 +00:00
|
|
|
}
|
|
|
|
|
2020-06-03 10:05:50 +00:00
|
|
|
impl ProjectManifest {
|
2020-06-24 11:34:24 +00:00
|
|
|
pub fn from_manifest_file(path: AbsPathBuf) -> Result<ProjectManifest> {
|
2021-07-19 18:20:10 +00:00
|
|
|
let path = ManifestPath::try_from(path)
|
|
|
|
.map_err(|path| format_err!("bad manifest path: {}", path.display()))?;
|
2021-07-17 13:43:33 +00:00
|
|
|
if path.file_name().unwrap_or_default() == "rust-project.json" {
|
2020-06-03 10:05:50 +00:00
|
|
|
return Ok(ProjectManifest::ProjectJson(path));
|
2020-04-16 20:02:10 +00:00
|
|
|
}
|
2021-07-17 13:43:33 +00:00
|
|
|
if path.file_name().unwrap_or_default() == "Cargo.toml" {
|
2020-06-03 10:05:50 +00:00
|
|
|
return Ok(ProjectManifest::CargoToml(path));
|
2020-04-16 20:02:10 +00:00
|
|
|
}
|
|
|
|
bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display())
|
2019-08-19 12:41:18 +00:00
|
|
|
}
|
|
|
|
|
2020-06-24 11:34:24 +00:00
|
|
|
pub fn discover_single(path: &AbsPath) -> Result<ProjectManifest> {
|
2020-06-03 10:05:50 +00:00
|
|
|
let mut candidates = ProjectManifest::discover(path)?;
|
2020-04-16 20:19:38 +00:00
|
|
|
let res = match candidates.pop() {
|
|
|
|
None => bail!("no projects"),
|
|
|
|
Some(it) => it,
|
|
|
|
};
|
|
|
|
|
|
|
|
if !candidates.is_empty() {
|
|
|
|
bail!("more than one project")
|
|
|
|
}
|
|
|
|
Ok(res)
|
|
|
|
}
|
|
|
|
|
2020-06-24 11:34:24 +00:00
|
|
|
pub fn discover(path: &AbsPath) -> io::Result<Vec<ProjectManifest>> {
|
2020-05-08 23:51:59 +00:00
|
|
|
if let Some(project_json) = find_in_parent_dirs(path, "rust-project.json") {
|
2020-06-03 10:05:50 +00:00
|
|
|
return Ok(vec![ProjectManifest::ProjectJson(project_json)]);
|
2020-04-16 20:02:10 +00:00
|
|
|
}
|
2020-04-16 20:19:38 +00:00
|
|
|
return find_cargo_toml(path)
|
2020-06-03 10:05:50 +00:00
|
|
|
.map(|paths| paths.into_iter().map(ProjectManifest::CargoToml).collect());
|
2020-04-16 20:02:10 +00:00
|
|
|
|
2021-07-19 18:20:10 +00:00
|
|
|
fn find_cargo_toml(path: &AbsPath) -> io::Result<Vec<ManifestPath>> {
|
2020-05-08 23:51:59 +00:00
|
|
|
match find_in_parent_dirs(path, "Cargo.toml") {
|
|
|
|
Some(it) => Ok(vec![it]),
|
|
|
|
None => Ok(find_cargo_toml_in_child_dir(read_dir(path)?)),
|
2020-04-16 20:02:10 +00:00
|
|
|
}
|
2020-05-08 23:51:59 +00:00
|
|
|
}
|
2020-04-16 20:02:10 +00:00
|
|
|
|
2021-07-19 18:20:10 +00:00
|
|
|
fn find_in_parent_dirs(path: &AbsPath, target_file_name: &str) -> Option<ManifestPath> {
|
2021-07-17 13:43:33 +00:00
|
|
|
if path.file_name().unwrap_or_default() == target_file_name {
|
2021-07-19 18:20:10 +00:00
|
|
|
if let Ok(manifest) = ManifestPath::try_from(path.to_path_buf()) {
|
|
|
|
return Some(manifest);
|
|
|
|
}
|
2020-04-16 20:02:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let mut curr = Some(path);
|
2020-05-08 23:51:59 +00:00
|
|
|
|
2020-04-16 20:02:10 +00:00
|
|
|
while let Some(path) = curr {
|
2020-05-08 23:51:59 +00:00
|
|
|
let candidate = path.join(target_file_name);
|
2021-07-17 13:43:33 +00:00
|
|
|
if fs::metadata(&candidate).is_ok() {
|
2021-07-19 18:20:10 +00:00
|
|
|
if let Ok(manifest) = ManifestPath::try_from(candidate) {
|
|
|
|
return Some(manifest);
|
|
|
|
}
|
2020-04-16 20:02:10 +00:00
|
|
|
}
|
|
|
|
curr = path.parent();
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
2021-07-19 18:20:10 +00:00
|
|
|
fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<ManifestPath> {
|
2020-04-16 20:02:10 +00:00
|
|
|
// Only one level down to avoid cycles the easy way and stop a runaway scan with large projects
|
2020-05-08 23:51:59 +00:00
|
|
|
entities
|
|
|
|
.filter_map(Result::ok)
|
|
|
|
.map(|it| it.path().join("Cargo.toml"))
|
|
|
|
.filter(|it| it.exists())
|
2020-06-24 11:34:24 +00:00
|
|
|
.map(AbsPathBuf::assert)
|
2021-07-19 18:20:10 +00:00
|
|
|
.filter_map(|it| it.try_into().ok())
|
2020-05-08 23:51:59 +00:00
|
|
|
.collect()
|
2020-04-16 20:02:10 +00:00
|
|
|
}
|
|
|
|
}
|
2020-06-03 09:44:51 +00:00
|
|
|
|
2021-06-25 20:51:54 +00:00
|
|
|
pub fn discover_all(paths: &[AbsPathBuf]) -> Vec<ProjectManifest> {
|
2020-06-03 09:44:51 +00:00
|
|
|
let mut res = paths
|
|
|
|
.iter()
|
2020-06-03 10:05:50 +00:00
|
|
|
.filter_map(|it| ProjectManifest::discover(it.as_ref()).ok())
|
2020-06-03 09:44:51 +00:00
|
|
|
.flatten()
|
|
|
|
.collect::<FxHashSet<_>>()
|
|
|
|
.into_iter()
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
res.sort();
|
|
|
|
res
|
|
|
|
}
|
2020-04-16 20:02:10 +00:00
|
|
|
}
|
|
|
|
|
2020-07-23 16:57:27 +00:00
|
|
|
fn utf8_stdout(mut cmd: Command) -> Result<String> {
|
2020-05-08 12:54:29 +00:00
|
|
|
let output = cmd.output().with_context(|| format!("{:?} failed", cmd))?;
|
|
|
|
if !output.status.success() {
|
2020-05-08 16:53:53 +00:00
|
|
|
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),
|
|
|
|
}
|
2020-05-08 12:54:29 +00:00
|
|
|
}
|
2020-07-23 16:57:27 +00:00
|
|
|
let stdout = String::from_utf8(output.stdout)?;
|
2020-08-25 15:00:08 +00:00
|
|
|
Ok(stdout.trim().to_string())
|
2020-05-08 12:54:29 +00:00
|
|
|
}
|