From eac24d52e672c0a9c118e8969bf1b839c3e7f1f3 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Sunkara Date: Thu, 13 Aug 2020 12:05:30 +0200 Subject: Rename ra_project_model -> project_model --- crates/project_model/src/cargo_workspace.rs | 362 ++++++++++++++++++ crates/project_model/src/cfg_flag.rs | 51 +++ crates/project_model/src/lib.rs | 544 ++++++++++++++++++++++++++++ crates/project_model/src/project_json.rs | 143 ++++++++ crates/project_model/src/sysroot.rs | 173 +++++++++ 5 files changed, 1273 insertions(+) create mode 100644 crates/project_model/src/cargo_workspace.rs create mode 100644 crates/project_model/src/cfg_flag.rs create mode 100644 crates/project_model/src/lib.rs create mode 100644 crates/project_model/src/project_json.rs create mode 100644 crates/project_model/src/sysroot.rs (limited to 'crates/project_model/src') diff --git a/crates/project_model/src/cargo_workspace.rs b/crates/project_model/src/cargo_workspace.rs new file mode 100644 index 000000000..abf8dca96 --- /dev/null +++ b/crates/project_model/src/cargo_workspace.rs @@ -0,0 +1,362 @@ +//! FIXME: write short doc here + +use std::{ + ffi::OsStr, + ops, + path::{Path, PathBuf}, + process::Command, +}; + +use anyhow::{Context, Result}; +use arena::{Arena, Idx}; +use cargo_metadata::{BuildScript, CargoOpt, Message, MetadataCommand, PackageId}; +use paths::{AbsPath, AbsPathBuf}; +use ra_db::Edition; +use rustc_hash::FxHashMap; + +use crate::cfg_flag::CfgFlag; + +/// `CargoWorkspace` represents the logical structure of, well, a Cargo +/// workspace. It pretty closely mirrors `cargo metadata` output. +/// +/// Note that internally, rust analyzer uses a different structure: +/// `CrateGraph`. `CrateGraph` is lower-level: it knows only about the crates, +/// while this knows about `Packages` & `Targets`: purely cargo-related +/// concepts. +/// +/// We use absolute paths here, `cargo metadata` guarantees to always produce +/// abs paths. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct CargoWorkspace { + packages: Arena, + targets: Arena, + workspace_root: AbsPathBuf, +} + +impl ops::Index for CargoWorkspace { + type Output = PackageData; + fn index(&self, index: Package) -> &PackageData { + &self.packages[index] + } +} + +impl ops::Index for CargoWorkspace { + type Output = TargetData; + fn index(&self, index: Target) -> &TargetData { + &self.targets[index] + } +} + +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub struct CargoConfig { + /// Do not activate the `default` feature. + pub no_default_features: bool, + + /// Activate all available features + pub all_features: bool, + + /// List of features to activate. + /// This will be ignored if `cargo_all_features` is true. + pub features: Vec, + + /// Runs cargo check on launch to figure out the correct values of OUT_DIR + pub load_out_dirs_from_check: bool, + + /// rustc target + pub target: Option, +} + +pub type Package = Idx; + +pub type Target = Idx; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct PackageData { + pub version: String, + pub name: String, + pub manifest: AbsPathBuf, + pub targets: Vec, + pub is_member: bool, + pub dependencies: Vec, + pub edition: Edition, + pub features: Vec, + pub cfgs: Vec, + pub out_dir: Option, + pub proc_macro_dylib_path: Option, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct PackageDependency { + pub pkg: Package, + pub name: String, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct TargetData { + pub package: Package, + pub name: String, + pub root: AbsPathBuf, + pub kind: TargetKind, + pub is_proc_macro: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TargetKind { + Bin, + /// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...). + Lib, + Example, + Test, + Bench, + Other, +} + +impl TargetKind { + fn new(kinds: &[String]) -> TargetKind { + for kind in kinds { + return match kind.as_str() { + "bin" => TargetKind::Bin, + "test" => TargetKind::Test, + "bench" => TargetKind::Bench, + "example" => TargetKind::Example, + "proc-macro" => TargetKind::Lib, + _ if kind.contains("lib") => TargetKind::Lib, + _ => continue, + }; + } + TargetKind::Other + } +} + +impl PackageData { + pub fn root(&self) -> &AbsPath { + self.manifest.parent().unwrap() + } +} + +impl CargoWorkspace { + pub fn from_cargo_metadata( + cargo_toml: &AbsPath, + cargo_features: &CargoConfig, + ) -> Result { + let mut meta = MetadataCommand::new(); + meta.cargo_path(toolchain::cargo()); + meta.manifest_path(cargo_toml.to_path_buf()); + if cargo_features.all_features { + meta.features(CargoOpt::AllFeatures); + } else { + if cargo_features.no_default_features { + // FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures` + // https://github.com/oli-obk/cargo_metadata/issues/79 + meta.features(CargoOpt::NoDefaultFeatures); + } + if !cargo_features.features.is_empty() { + meta.features(CargoOpt::SomeFeatures(cargo_features.features.clone())); + } + } + if let Some(parent) = cargo_toml.parent() { + meta.current_dir(parent.to_path_buf()); + } + if let Some(target) = cargo_features.target.as_ref() { + meta.other_options(vec![String::from("--filter-platform"), target.clone()]); + } + let mut meta = meta.exec().with_context(|| { + format!("Failed to run `cargo metadata --manifest-path {}`", cargo_toml.display()) + })?; + + let mut out_dir_by_id = FxHashMap::default(); + let mut cfgs = FxHashMap::default(); + let mut proc_macro_dylib_paths = FxHashMap::default(); + if cargo_features.load_out_dirs_from_check { + let resources = load_extern_resources(cargo_toml, cargo_features)?; + out_dir_by_id = resources.out_dirs; + cfgs = resources.cfgs; + proc_macro_dylib_paths = resources.proc_dylib_paths; + } + + let mut pkg_by_id = FxHashMap::default(); + let mut packages = Arena::default(); + let mut targets = Arena::default(); + + let ws_members = &meta.workspace_members; + + meta.packages.sort_by(|a, b| a.id.cmp(&b.id)); + for meta_pkg in meta.packages { + let cargo_metadata::Package { id, edition, name, manifest_path, version, .. } = + meta_pkg; + let is_member = ws_members.contains(&id); + let edition = edition + .parse::() + .with_context(|| format!("Failed to parse edition {}", edition))?; + let pkg = packages.alloc(PackageData { + name, + version: version.to_string(), + manifest: AbsPathBuf::assert(manifest_path), + targets: Vec::new(), + is_member, + edition, + dependencies: Vec::new(), + features: Vec::new(), + cfgs: cfgs.get(&id).cloned().unwrap_or_default(), + out_dir: out_dir_by_id.get(&id).cloned(), + proc_macro_dylib_path: proc_macro_dylib_paths.get(&id).cloned(), + }); + let pkg_data = &mut packages[pkg]; + pkg_by_id.insert(id, pkg); + for meta_tgt in meta_pkg.targets { + let is_proc_macro = meta_tgt.kind.as_slice() == ["proc-macro"]; + let tgt = targets.alloc(TargetData { + package: pkg, + name: meta_tgt.name, + root: AbsPathBuf::assert(meta_tgt.src_path.clone()), + kind: TargetKind::new(meta_tgt.kind.as_slice()), + is_proc_macro, + }); + pkg_data.targets.push(tgt); + } + } + let resolve = meta.resolve.expect("metadata executed with deps"); + for mut node in resolve.nodes { + let source = match pkg_by_id.get(&node.id) { + Some(&src) => src, + // FIXME: replace this and a similar branch below with `.unwrap`, once + // https://github.com/rust-lang/cargo/issues/7841 + // is fixed and hits stable (around 1.43-is probably?). + None => { + log::error!("Node id do not match in cargo metadata, ignoring {}", node.id); + continue; + } + }; + node.deps.sort_by(|a, b| a.pkg.cmp(&b.pkg)); + for dep_node in node.deps { + let pkg = match pkg_by_id.get(&dep_node.pkg) { + Some(&pkg) => pkg, + None => { + log::error!( + "Dep node id do not match in cargo metadata, ignoring {}", + dep_node.pkg + ); + continue; + } + }; + let dep = PackageDependency { name: dep_node.name, pkg }; + packages[source].dependencies.push(dep); + } + packages[source].features.extend(node.features); + } + + let workspace_root = AbsPathBuf::assert(meta.workspace_root); + Ok(CargoWorkspace { packages, targets, workspace_root: workspace_root }) + } + + pub fn packages<'a>(&'a self) -> impl Iterator + ExactSizeIterator + 'a { + self.packages.iter().map(|(id, _pkg)| id) + } + + pub fn target_by_root(&self, root: &AbsPath) -> Option { + self.packages() + .filter_map(|pkg| self[pkg].targets.iter().find(|&&it| &self[it].root == root)) + .next() + .copied() + } + + pub fn workspace_root(&self) -> &AbsPath { + &self.workspace_root + } + + pub fn package_flag(&self, package: &PackageData) -> String { + if self.is_unique(&*package.name) { + package.name.clone() + } else { + format!("{}:{}", package.name, package.version) + } + } + + fn is_unique(&self, name: &str) -> bool { + self.packages.iter().filter(|(_, v)| v.name == name).count() == 1 + } +} + +#[derive(Debug, Clone, Default)] +pub struct ExternResources { + out_dirs: FxHashMap, + proc_dylib_paths: FxHashMap, + cfgs: FxHashMap>, +} + +pub fn load_extern_resources( + cargo_toml: &Path, + cargo_features: &CargoConfig, +) -> Result { + let mut cmd = Command::new(toolchain::cargo()); + cmd.args(&["check", "--message-format=json", "--manifest-path"]).arg(cargo_toml); + if cargo_features.all_features { + cmd.arg("--all-features"); + } else { + if cargo_features.no_default_features { + // FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures` + // https://github.com/oli-obk/cargo_metadata/issues/79 + cmd.arg("--no-default-features"); + } + if !cargo_features.features.is_empty() { + cmd.arg("--features"); + cmd.arg(cargo_features.features.join(" ")); + } + } + + let output = cmd.output()?; + + let mut res = ExternResources::default(); + + for message in cargo_metadata::Message::parse_stream(output.stdout.as_slice()) { + if let Ok(message) = message { + match message { + Message::BuildScriptExecuted(BuildScript { package_id, out_dir, cfgs, .. }) => { + let cfgs = { + let mut acc = Vec::new(); + for cfg in cfgs { + match cfg.parse::() { + Ok(it) => acc.push(it), + Err(err) => { + anyhow::bail!("invalid cfg from cargo-metadata: {}", err) + } + }; + } + acc + }; + // cargo_metadata crate returns default (empty) path for + // older cargos, which is not absolute, so work around that. + if out_dir != PathBuf::default() { + let out_dir = AbsPathBuf::assert(out_dir); + res.out_dirs.insert(package_id.clone(), out_dir); + res.cfgs.insert(package_id, cfgs); + } + } + Message::CompilerArtifact(message) => { + if message.target.kind.contains(&"proc-macro".to_string()) { + let package_id = message.package_id; + // Skip rmeta file + if let Some(filename) = message.filenames.iter().find(|name| is_dylib(name)) + { + let filename = AbsPathBuf::assert(filename.clone()); + res.proc_dylib_paths.insert(package_id, filename); + } + } + } + Message::CompilerMessage(_) => (), + Message::Unknown => (), + Message::BuildFinished(_) => {} + Message::TextLine(_) => {} + } + } + } + Ok(res) +} + +// FIXME: File a better way to know if it is a dylib +fn is_dylib(path: &Path) -> bool { + match path.extension().and_then(OsStr::to_str).map(|it| it.to_string().to_lowercase()) { + None => false, + Some(ext) => matches!(ext.as_str(), "dll" | "dylib" | "so"), + } +} diff --git a/crates/project_model/src/cfg_flag.rs b/crates/project_model/src/cfg_flag.rs new file mode 100644 index 000000000..e92962cf6 --- /dev/null +++ b/crates/project_model/src/cfg_flag.rs @@ -0,0 +1,51 @@ +//! Parsing of CfgFlags as command line arguments, as in +//! +//! rustc main.rs --cfg foo --cfg 'feature="bar"' +use std::str::FromStr; + +use cfg::CfgOptions; +use stdx::split_once; + +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum CfgFlag { + Atom(String), + KeyValue { key: String, value: String }, +} + +impl FromStr for CfgFlag { + type Err = String; + fn from_str(s: &str) -> Result { + let res = match split_once(s, '=') { + Some((key, value)) => { + if !(value.starts_with('"') && value.ends_with('"')) { + return Err(format!("Invalid cfg ({:?}), value should be in quotes", s)); + } + let key = key.to_string(); + let value = value[1..value.len() - 1].to_string(); + CfgFlag::KeyValue { key, value } + } + None => CfgFlag::Atom(s.into()), + }; + Ok(res) + } +} + +impl<'de> serde::Deserialize<'de> for CfgFlag { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + String::deserialize(deserializer)?.parse().map_err(serde::de::Error::custom) + } +} + +impl Extend for CfgOptions { + fn extend>(&mut self, iter: T) { + for cfg_flag in iter { + match cfg_flag { + CfgFlag::Atom(it) => self.insert_atom(it.into()), + CfgFlag::KeyValue { key, value } => self.insert_key_value(key.into(), value.into()), + } + } + } +} diff --git a/crates/project_model/src/lib.rs b/crates/project_model/src/lib.rs new file mode 100644 index 000000000..ee42198f3 --- /dev/null +++ b/crates/project_model/src/lib.rs @@ -0,0 +1,544 @@ +//! FIXME: write short doc here + +mod cargo_workspace; +mod project_json; +mod sysroot; +mod cfg_flag; + +use std::{ + fs::{self, read_dir, ReadDir}, + io, + process::Command, +}; + +use anyhow::{bail, Context, Result}; +use cfg::CfgOptions; +use paths::{AbsPath, AbsPathBuf}; +use ra_db::{CrateGraph, CrateId, CrateName, Edition, Env, FileId}; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::cfg_flag::CfgFlag; + +pub use crate::{ + cargo_workspace::{CargoConfig, CargoWorkspace, Package, Target, TargetKind}, + project_json::{ProjectJson, ProjectJsonData}, + sysroot::Sysroot, +}; + +pub use ra_proc_macro::ProcMacroClient; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ProjectWorkspace { + /// Project workspace was discovered by running `cargo metadata` and `rustc --print sysroot`. + Cargo { cargo: CargoWorkspace, sysroot: Sysroot }, + /// Project workspace was manually specified using a `rust-project.json` file. + Json { project: ProjectJson }, +} + +/// `PackageRoot` describes a package root folder. +/// Which may be an external dependency, or a member of +/// the current workspace. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct PackageRoot { + /// Is a member of the current workspace + pub is_member: bool, + pub include: Vec, + pub exclude: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub enum ProjectManifest { + ProjectJson(AbsPathBuf), + CargoToml(AbsPathBuf), +} + +impl ProjectManifest { + pub fn from_manifest_file(path: AbsPathBuf) -> Result { + if path.ends_with("rust-project.json") { + return Ok(ProjectManifest::ProjectJson(path)); + } + if path.ends_with("Cargo.toml") { + return Ok(ProjectManifest::CargoToml(path)); + } + bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display()) + } + + pub fn discover_single(path: &AbsPath) -> Result { + 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> { + 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> { + 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 { + if path.ends_with(target_file_name) { + return Some(path.to_path_buf()); + } + + let mut curr = Some(path); + + while let Some(path) = curr { + let candidate = path.join(target_file_name); + if candidate.exists() { + return Some(candidate); + } + curr = path.parent(); + } + + None + } + + fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec { + // 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(AbsPathBuf::assert) + .collect() + } + } + + pub fn discover_all(paths: &[impl AsRef]) -> Vec { + let mut res = paths + .iter() + .filter_map(|it| ProjectManifest::discover(it.as_ref()).ok()) + .flatten() + .collect::>() + .into_iter() + .collect::>(); + res.sort(); + res + } +} + +impl ProjectWorkspace { + pub fn load( + manifest: ProjectManifest, + cargo_config: &CargoConfig, + with_sysroot: bool, + ) -> Result { + let res = match manifest { + ProjectManifest::ProjectJson(project_json) => { + let file = fs::read_to_string(&project_json).with_context(|| { + format!("Failed to read json file {}", project_json.display()) + })?; + let data = serde_json::from_str(&file).with_context(|| { + format!("Failed to deserialize json file {}", project_json.display()) + })?; + let project_location = project_json.parent().unwrap().to_path_buf(); + let project = ProjectJson::new(&project_location, data); + ProjectWorkspace::Json { project } + } + ProjectManifest::CargoToml(cargo_toml) => { + let cargo = CargoWorkspace::from_cargo_metadata(&cargo_toml, cargo_config) + .with_context(|| { + format!( + "Failed to read Cargo metadata from Cargo.toml file {}", + cargo_toml.display() + ) + })?; + let sysroot = if with_sysroot { + Sysroot::discover(&cargo_toml).with_context(|| { + format!( + "Failed to find sysroot for Cargo.toml file {}. Is rust-src installed?", + cargo_toml.display() + ) + })? + } else { + Sysroot::default() + }; + ProjectWorkspace::Cargo { cargo, sysroot } + } + }; + + Ok(res) + } + + /// Returns the roots for the current `ProjectWorkspace` + /// The return type contains the path and whether or not + /// the root is a member of the current workspace + pub fn to_roots(&self) -> Vec { + match self { + ProjectWorkspace::Json { project } => project + .crates + .iter() + .map(|krate| PackageRoot { + is_member: krate.is_workspace_member, + include: krate.include.clone(), + exclude: krate.exclude.clone(), + }) + .collect::>() + .into_iter() + .collect::>(), + ProjectWorkspace::Cargo { cargo, sysroot } => cargo + .packages() + .map(|pkg| { + let is_member = cargo[pkg].is_member; + let pkg_root = cargo[pkg].root().to_path_buf(); + + let mut include = vec![pkg_root.clone()]; + include.extend(cargo[pkg].out_dir.clone()); + + let mut exclude = vec![pkg_root.join(".git")]; + if is_member { + exclude.push(pkg_root.join("target")); + } else { + exclude.push(pkg_root.join("tests")); + exclude.push(pkg_root.join("examples")); + exclude.push(pkg_root.join("benches")); + } + PackageRoot { is_member, include, exclude } + }) + .chain(sysroot.crates().map(|krate| PackageRoot { + is_member: false, + include: vec![sysroot[krate].root_dir().to_path_buf()], + exclude: Vec::new(), + })) + .collect(), + } + } + + pub fn proc_macro_dylib_paths(&self) -> Vec { + match self { + ProjectWorkspace::Json { project } => project + .crates + .iter() + .filter_map(|krate| krate.proc_macro_dylib_path.as_ref()) + .cloned() + .collect(), + ProjectWorkspace::Cargo { cargo, sysroot: _sysroot } => cargo + .packages() + .filter_map(|pkg| cargo[pkg].proc_macro_dylib_path.as_ref()) + .cloned() + .collect(), + } + } + + pub fn n_packages(&self) -> usize { + match self { + ProjectWorkspace::Json { project, .. } => project.crates.len(), + ProjectWorkspace::Cargo { cargo, sysroot } => { + cargo.packages().len() + sysroot.crates().len() + } + } + } + + pub fn to_crate_graph( + &self, + target: Option<&str>, + proc_macro_client: &ProcMacroClient, + load: &mut dyn FnMut(&AbsPath) -> Option, + ) -> CrateGraph { + let mut crate_graph = CrateGraph::default(); + match self { + ProjectWorkspace::Json { project } => { + let mut cfg_cache: FxHashMap, Vec> = FxHashMap::default(); + let crates: FxHashMap<_, _> = project + .crates + .iter() + .enumerate() + .filter_map(|(seq_index, krate)| { + let file_path = &krate.root_module; + let file_id = load(&file_path)?; + + let env = krate.env.clone().into_iter().collect(); + let proc_macro = krate + .proc_macro_dylib_path + .clone() + .map(|it| proc_macro_client.by_dylib_path(&it)); + + let target = krate.target.as_deref().or(target); + let target_cfgs = cfg_cache + .entry(target) + .or_insert_with(|| get_rustc_cfg_options(target)); + + let mut cfg_options = CfgOptions::default(); + cfg_options.extend(target_cfgs.iter().chain(krate.cfg.iter()).cloned()); + + // FIXME: No crate name in json definition such that we cannot add OUT_DIR to env + Some(( + CrateId(seq_index as u32), + crate_graph.add_crate_root( + file_id, + krate.edition, + // FIXME json definitions can store the crate name + None, + cfg_options, + env, + proc_macro.unwrap_or_default(), + ), + )) + }) + .collect(); + + for (id, krate) in project.crates.iter().enumerate() { + for dep in &krate.deps { + let from_crate_id = CrateId(id as u32); + let to_crate_id = dep.crate_id; + if let (Some(&from), Some(&to)) = + (crates.get(&from_crate_id), crates.get(&to_crate_id)) + { + if crate_graph.add_dep(from, dep.name.clone(), to).is_err() { + log::error!( + "cyclic dependency {:?} -> {:?}", + from_crate_id, + to_crate_id + ); + } + } + } + } + } + ProjectWorkspace::Cargo { cargo, sysroot } => { + let mut cfg_options = CfgOptions::default(); + cfg_options.extend(get_rustc_cfg_options(target)); + + let sysroot_crates: FxHashMap<_, _> = sysroot + .crates() + .filter_map(|krate| { + let file_id = load(&sysroot[krate].root)?; + + let env = Env::default(); + let proc_macro = vec![]; + let name = sysroot[krate].name.clone(); + let crate_id = crate_graph.add_crate_root( + file_id, + Edition::Edition2018, + Some(name), + cfg_options.clone(), + env, + proc_macro, + ); + Some((krate, crate_id)) + }) + .collect(); + + for from in sysroot.crates() { + for &to in sysroot[from].deps.iter() { + let name = &sysroot[to].name; + if let (Some(&from), Some(&to)) = + (sysroot_crates.get(&from), sysroot_crates.get(&to)) + { + if crate_graph.add_dep(from, CrateName::new(name).unwrap(), to).is_err() + { + log::error!("cyclic dependency between sysroot crates") + } + } + } + } + + let libcore = sysroot.core().and_then(|it| sysroot_crates.get(&it).copied()); + let liballoc = sysroot.alloc().and_then(|it| sysroot_crates.get(&it).copied()); + let libstd = sysroot.std().and_then(|it| sysroot_crates.get(&it).copied()); + let libproc_macro = + sysroot.proc_macro().and_then(|it| sysroot_crates.get(&it).copied()); + + let mut pkg_to_lib_crate = FxHashMap::default(); + let mut pkg_crates = FxHashMap::default(); + + // Add test cfg for non-sysroot crates + cfg_options.insert_atom("test".into()); + cfg_options.insert_atom("debug_assertions".into()); + + // Next, create crates for each package, target pair + for pkg in cargo.packages() { + let mut lib_tgt = None; + for &tgt in cargo[pkg].targets.iter() { + let root = cargo[tgt].root.as_path(); + if let Some(file_id) = load(root) { + let edition = cargo[pkg].edition; + let cfg_options = { + let mut opts = cfg_options.clone(); + for feature in cargo[pkg].features.iter() { + opts.insert_key_value("feature".into(), feature.into()); + } + opts.extend(cargo[pkg].cfgs.iter().cloned()); + opts + }; + let mut env = Env::default(); + if let Some(out_dir) = &cargo[pkg].out_dir { + // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!() + if let Some(out_dir) = out_dir.to_str().map(|s| s.to_owned()) { + env.set("OUT_DIR", out_dir); + } + } + let proc_macro = cargo[pkg] + .proc_macro_dylib_path + .as_ref() + .map(|it| proc_macro_client.by_dylib_path(&it)) + .unwrap_or_default(); + + let crate_id = crate_graph.add_crate_root( + file_id, + edition, + Some(cargo[pkg].name.clone()), + cfg_options, + env, + proc_macro.clone(), + ); + if cargo[tgt].kind == TargetKind::Lib { + lib_tgt = Some((crate_id, cargo[tgt].name.clone())); + pkg_to_lib_crate.insert(pkg, crate_id); + } + if cargo[tgt].is_proc_macro { + if let Some(proc_macro) = libproc_macro { + if crate_graph + .add_dep( + crate_id, + CrateName::new("proc_macro").unwrap(), + proc_macro, + ) + .is_err() + { + log::error!( + "cyclic dependency on proc_macro for {}", + &cargo[pkg].name + ) + } + } + } + + pkg_crates.entry(pkg).or_insert_with(Vec::new).push(crate_id); + } + } + + // Set deps to the core, std and to the lib target of the current package + for &from in pkg_crates.get(&pkg).into_iter().flatten() { + if let Some((to, name)) = lib_tgt.clone() { + if to != from + && crate_graph + .add_dep( + from, + // For root projects with dashes in their name, + // cargo metadata does not do any normalization, + // so we do it ourselves currently + CrateName::normalize_dashes(&name), + to, + ) + .is_err() + { + { + log::error!( + "cyclic dependency between targets of {}", + &cargo[pkg].name + ) + } + } + } + // core is added as a dependency before std in order to + // mimic rustcs dependency order + if let Some(core) = libcore { + if crate_graph + .add_dep(from, CrateName::new("core").unwrap(), core) + .is_err() + { + log::error!("cyclic dependency on core for {}", &cargo[pkg].name) + } + } + if let Some(alloc) = liballoc { + if crate_graph + .add_dep(from, CrateName::new("alloc").unwrap(), alloc) + .is_err() + { + log::error!("cyclic dependency on alloc for {}", &cargo[pkg].name) + } + } + if let Some(std) = libstd { + if crate_graph + .add_dep(from, CrateName::new("std").unwrap(), std) + .is_err() + { + log::error!("cyclic dependency on std for {}", &cargo[pkg].name) + } + } + } + } + + // Now add a dep edge from all targets of upstream to the lib + // target of downstream. + for pkg in cargo.packages() { + for dep in cargo[pkg].dependencies.iter() { + if let Some(&to) = pkg_to_lib_crate.get(&dep.pkg) { + for &from in pkg_crates.get(&pkg).into_iter().flatten() { + if crate_graph + .add_dep(from, CrateName::new(&dep.name).unwrap(), to) + .is_err() + { + log::error!( + "cyclic dependency {} -> {}", + &cargo[pkg].name, + &cargo[dep.pkg].name + ) + } + } + } + } + } + } + } + crate_graph + } +} + +fn get_rustc_cfg_options(target: Option<&str>) -> Vec { + let mut res = Vec::new(); + + // Some nightly-only cfgs, which are required for stdlib + res.push(CfgFlag::Atom("target_thread_local".into())); + for &ty in ["8", "16", "32", "64", "cas", "ptr"].iter() { + for &key in ["target_has_atomic", "target_has_atomic_load_store"].iter() { + res.push(CfgFlag::KeyValue { key: key.to_string(), value: ty.into() }); + } + } + + let rustc_cfgs = { + let mut cmd = Command::new(toolchain::rustc()); + cmd.args(&["--print", "cfg", "-O"]); + if let Some(target) = target { + cmd.args(&["--target", target]); + } + utf8_stdout(cmd) + }; + + match rustc_cfgs { + Ok(rustc_cfgs) => res.extend(rustc_cfgs.lines().map(|it| it.parse().unwrap())), + Err(e) => log::error!("failed to get rustc cfgs: {:#}", e), + } + + res +} + +fn utf8_stdout(mut cmd: Command) -> Result { + let output = cmd.output().with_context(|| format!("{:?} failed", cmd))?; + 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) +} diff --git a/crates/project_model/src/project_json.rs b/crates/project_model/src/project_json.rs new file mode 100644 index 000000000..e3f3163f6 --- /dev/null +++ b/crates/project_model/src/project_json.rs @@ -0,0 +1,143 @@ +//! FIXME: write short doc here + +use std::path::PathBuf; + +use paths::{AbsPath, AbsPathBuf}; +use ra_db::{CrateId, CrateName, Dependency, Edition}; +use rustc_hash::FxHashMap; +use serde::{de, Deserialize}; + +use crate::cfg_flag::CfgFlag; + +/// Roots and crates that compose this Rust project. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProjectJson { + pub(crate) crates: Vec, +} + +/// A crate points to the root module of a crate and lists the dependencies of the crate. This is +/// useful in creating the crate graph. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Crate { + pub(crate) root_module: AbsPathBuf, + pub(crate) edition: Edition, + pub(crate) deps: Vec, + pub(crate) cfg: Vec, + pub(crate) target: Option, + pub(crate) env: FxHashMap, + pub(crate) proc_macro_dylib_path: Option, + pub(crate) is_workspace_member: bool, + pub(crate) include: Vec, + pub(crate) exclude: Vec, +} + +impl ProjectJson { + pub fn new(base: &AbsPath, data: ProjectJsonData) -> ProjectJson { + ProjectJson { + crates: data + .crates + .into_iter() + .map(|crate_data| { + let is_workspace_member = crate_data.is_workspace_member.unwrap_or_else(|| { + crate_data.root_module.is_relative() + && !crate_data.root_module.starts_with("..") + || crate_data.root_module.starts_with(base) + }); + let root_module = base.join(crate_data.root_module); + let (include, exclude) = match crate_data.source { + Some(src) => { + let absolutize = |dirs: Vec| { + dirs.into_iter().map(|it| base.join(it)).collect::>() + }; + (absolutize(src.include_dirs), absolutize(src.exclude_dirs)) + } + None => (vec![root_module.parent().unwrap().to_path_buf()], Vec::new()), + }; + + Crate { + root_module, + edition: crate_data.edition.into(), + deps: crate_data + .deps + .into_iter() + .map(|dep_data| Dependency { + crate_id: CrateId(dep_data.krate as u32), + name: dep_data.name, + }) + .collect::>(), + cfg: crate_data.cfg, + target: crate_data.target, + env: crate_data.env, + proc_macro_dylib_path: crate_data + .proc_macro_dylib_path + .map(|it| base.join(it)), + is_workspace_member, + include, + exclude, + } + }) + .collect::>(), + } + } +} + +#[derive(Deserialize)] +pub struct ProjectJsonData { + crates: Vec, +} + +#[derive(Deserialize)] +struct CrateData { + root_module: PathBuf, + edition: EditionData, + deps: Vec, + #[serde(default)] + cfg: Vec, + target: Option, + #[serde(default)] + env: FxHashMap, + proc_macro_dylib_path: Option, + is_workspace_member: Option, + source: Option, +} + +#[derive(Deserialize)] +#[serde(rename = "edition")] +enum EditionData { + #[serde(rename = "2015")] + Edition2015, + #[serde(rename = "2018")] + Edition2018, +} + +impl From for Edition { + fn from(data: EditionData) -> Self { + match data { + EditionData::Edition2015 => Edition::Edition2015, + EditionData::Edition2018 => Edition::Edition2018, + } + } +} + +#[derive(Deserialize)] +struct DepData { + /// Identifies a crate by position in the crates array. + #[serde(rename = "crate")] + krate: usize, + #[serde(deserialize_with = "deserialize_crate_name")] + name: CrateName, +} + +#[derive(Deserialize)] +struct CrateSource { + include_dirs: Vec, + exclude_dirs: Vec, +} + +fn deserialize_crate_name<'de, D>(de: D) -> Result +where + D: de::Deserializer<'de>, +{ + let name = String::deserialize(de)?; + CrateName::new(&name).map_err(|err| de::Error::custom(format!("invalid crate name: {:?}", err))) +} diff --git a/crates/project_model/src/sysroot.rs b/crates/project_model/src/sysroot.rs new file mode 100644 index 000000000..8239797b6 --- /dev/null +++ b/crates/project_model/src/sysroot.rs @@ -0,0 +1,173 @@ +//! FIXME: write short doc here + +use std::{convert::TryFrom, env, ops, path::Path, process::Command}; + +use anyhow::{bail, format_err, Result}; +use arena::{Arena, Idx}; +use paths::{AbsPath, AbsPathBuf}; + +use crate::utf8_stdout; + +#[derive(Default, Debug, Clone, Eq, PartialEq)] +pub struct Sysroot { + crates: Arena, +} + +pub type SysrootCrate = Idx; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SysrootCrateData { + pub name: String, + pub root: AbsPathBuf, + pub deps: Vec, +} + +impl ops::Index for Sysroot { + type Output = SysrootCrateData; + fn index(&self, index: SysrootCrate) -> &SysrootCrateData { + &self.crates[index] + } +} + +impl Sysroot { + pub fn core(&self) -> Option { + self.by_name("core") + } + + pub fn alloc(&self) -> Option { + self.by_name("alloc") + } + + pub fn std(&self) -> Option { + self.by_name("std") + } + + pub fn proc_macro(&self) -> Option { + self.by_name("proc_macro") + } + + pub fn crates<'a>(&'a self) -> impl Iterator + ExactSizeIterator + 'a { + self.crates.iter().map(|(id, _data)| id) + } + + pub fn discover(cargo_toml: &AbsPath) -> Result { + let src = get_or_install_rust_src(cargo_toml)?; + let mut sysroot = Sysroot { crates: Arena::default() }; + for name in SYSROOT_CRATES.trim().lines() { + // FIXME: remove this path when 1.47 comes out + // https://github.com/rust-lang/rust/pull/73265 + let root = src.join(format!("lib{}", name)).join("lib.rs"); + if root.exists() { + sysroot.crates.alloc(SysrootCrateData { + name: name.into(), + root, + deps: Vec::new(), + }); + } else { + let root = src.join(name).join("src/lib.rs"); + if root.exists() { + sysroot.crates.alloc(SysrootCrateData { + name: name.into(), + root, + deps: Vec::new(), + }); + } + } + } + if let Some(std) = sysroot.std() { + for dep in STD_DEPS.trim().lines() { + if let Some(dep) = sysroot.by_name(dep) { + sysroot.crates[std].deps.push(dep) + } + } + } + if let Some(alloc) = sysroot.alloc() { + if let Some(core) = sysroot.core() { + sysroot.crates[alloc].deps.push(core); + } + } + Ok(sysroot) + } + + fn by_name(&self, name: &str) -> Option { + self.crates.iter().find(|(_id, data)| data.name == name).map(|(id, _data)| id) + } +} + +fn get_or_install_rust_src(cargo_toml: &AbsPath) -> Result { + if let Ok(path) = env::var("RUST_SRC_PATH") { + let path = AbsPathBuf::try_from(path.as_str()) + .map_err(|path| format_err!("RUST_SRC_PATH must be absolute: {}", path.display()))?; + return Ok(path); + } + let current_dir = cargo_toml.parent().unwrap(); + let mut rustc = Command::new(toolchain::rustc()); + rustc.current_dir(current_dir).args(&["--print", "sysroot"]); + let stdout = utf8_stdout(rustc)?; + let sysroot_path = AbsPath::assert(Path::new(stdout.trim())); + let mut src = get_rust_src(sysroot_path); + if src.is_none() { + let mut rustup = Command::new(toolchain::rustup()); + rustup.current_dir(current_dir).args(&["component", "add", "rust-src"]); + utf8_stdout(rustup)?; + src = get_rust_src(sysroot_path); + } + match src { + Some(r) => Ok(r), + None => bail!( + "can't load standard library from sysroot\n\ + {}\n\ + (discovered via `rustc --print sysroot`)\n\ + try running `rustup component add rust-src` or set `RUST_SRC_PATH`", + sysroot_path.display(), + ), + } +} + +fn get_rust_src(sysroot_path: &AbsPath) -> Option { + // try the new path first since the old one still exists + let mut src_path = sysroot_path.join("lib/rustlib/src/rust/library"); + if !src_path.exists() { + // FIXME: remove this path when 1.47 comes out + // https://github.com/rust-lang/rust/pull/73265 + src_path = sysroot_path.join("lib/rustlib/src/rust/src"); + } + if src_path.exists() { + Some(src_path) + } else { + None + } +} + +impl SysrootCrateData { + pub fn root_dir(&self) -> &AbsPath { + self.root.parent().unwrap() + } +} + +const SYSROOT_CRATES: &str = " +alloc +core +panic_abort +panic_unwind +proc_macro +profiler_builtins +rtstartup +std +stdarch +term +test +unwind"; + +const STD_DEPS: &str = " +alloc +core +panic_abort +panic_unwind +profiler_builtins +rtstartup +proc_macro +stdarch +term +test +unwind"; -- cgit v1.2.3