]> git.lizzy.rs Git - rust.git/blob - crates/ra_project_model/src/lib.rs
dd9c80691f3de78016456d5cc038fbbade75cb36
[rust.git] / crates / ra_project_model / src / lib.rs
1 //! FIXME: write short doc here
2
3 mod cargo_workspace;
4 mod json_project;
5 mod sysroot;
6
7 use std::{
8     error::Error,
9     fs::{read_dir, File, ReadDir},
10     io::BufReader,
11     path::{Path, PathBuf},
12     process::Command,
13 };
14
15 use anyhow::{bail, Context, Result};
16 use ra_cfg::CfgOptions;
17 use ra_db::{CrateGraph, CrateName, Edition, Env, ExternSource, ExternSourceId, FileId};
18 use rustc_hash::FxHashMap;
19 use serde_json::from_reader;
20
21 pub use crate::{
22     cargo_workspace::{CargoConfig, CargoWorkspace, Package, Target, TargetKind},
23     json_project::JsonProject,
24     sysroot::Sysroot,
25 };
26 pub use ra_proc_macro::ProcMacroClient;
27
28 #[derive(Clone, PartialEq, Eq, Hash, Debug)]
29 pub struct CargoTomlNotFoundError {
30     pub searched_at: PathBuf,
31     pub reason: String,
32 }
33
34 impl std::fmt::Display for CargoTomlNotFoundError {
35     fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36         write!(
37             fmt,
38             "can't find Cargo.toml at {}, due to {}",
39             self.searched_at.display(),
40             self.reason
41         )
42     }
43 }
44
45 impl Error for CargoTomlNotFoundError {}
46
47 #[derive(Debug, Clone)]
48 pub enum ProjectWorkspace {
49     /// Project workspace was discovered by running `cargo metadata` and `rustc --print sysroot`.
50     Cargo { cargo: CargoWorkspace, sysroot: Sysroot },
51     /// Project workspace was manually specified using a `rust-project.json` file.
52     Json { project: JsonProject },
53 }
54
55 /// `PackageRoot` describes a package root folder.
56 /// Which may be an external dependency, or a member of
57 /// the current workspace.
58 #[derive(Clone)]
59 pub struct PackageRoot {
60     /// Path to the root folder
61     path: PathBuf,
62     /// Is a member of the current workspace
63     is_member: bool,
64 }
65
66 impl PackageRoot {
67     pub fn new(path: PathBuf, is_member: bool) -> PackageRoot {
68         PackageRoot { path, is_member }
69     }
70
71     pub fn path(&self) -> &PathBuf {
72         &self.path
73     }
74
75     pub fn is_member(&self) -> bool {
76         self.is_member
77     }
78 }
79
80 impl ProjectWorkspace {
81     pub fn discover(path: &Path, cargo_features: &CargoConfig) -> Result<ProjectWorkspace> {
82         ProjectWorkspace::discover_with_sysroot(path, true, cargo_features)
83     }
84
85     pub fn discover_with_sysroot(
86         path: &Path,
87         with_sysroot: bool,
88         cargo_features: &CargoConfig,
89     ) -> Result<ProjectWorkspace> {
90         match find_rust_project_json(path) {
91             Some(json_path) => {
92                 let file = File::open(&json_path)
93                     .with_context(|| format!("Failed to open json file {}", json_path.display()))?;
94                 let reader = BufReader::new(file);
95                 Ok(ProjectWorkspace::Json {
96                     project: from_reader(reader).with_context(|| {
97                         format!("Failed to deserialize json file {}", json_path.display())
98                     })?,
99                 })
100             }
101             None => {
102                 let cargo_toml = find_cargo_toml(path).with_context(|| {
103                     format!("Failed to find Cargo.toml for path {}", path.display())
104                 })?;
105                 let cargo = CargoWorkspace::from_cargo_metadata(&cargo_toml, cargo_features)
106                     .with_context(|| {
107                         format!(
108                             "Failed to read Cargo metadata from Cargo.toml file {}",
109                             cargo_toml.display()
110                         )
111                     })?;
112                 let sysroot = if with_sysroot {
113                     Sysroot::discover(&cargo_toml).with_context(|| {
114                         format!(
115                             "Failed to find sysroot for Cargo.toml file {}. Is rust-src installed?",
116                             cargo_toml.display()
117                         )
118                     })?
119                 } else {
120                     Sysroot::default()
121                 };
122                 Ok(ProjectWorkspace::Cargo { cargo, sysroot })
123             }
124         }
125     }
126
127     /// Returns the roots for the current `ProjectWorkspace`
128     /// The return type contains the path and whether or not
129     /// the root is a member of the current workspace
130     pub fn to_roots(&self) -> Vec<PackageRoot> {
131         match self {
132             ProjectWorkspace::Json { project } => {
133                 let mut roots = Vec::with_capacity(project.roots.len());
134                 for root in &project.roots {
135                     roots.push(PackageRoot::new(root.path.clone(), true));
136                 }
137                 roots
138             }
139             ProjectWorkspace::Cargo { cargo, sysroot } => {
140                 let mut roots = Vec::with_capacity(cargo.packages().len() + sysroot.crates().len());
141                 for pkg in cargo.packages() {
142                     let root = cargo[pkg].root().to_path_buf();
143                     let member = cargo[pkg].is_member;
144                     roots.push(PackageRoot::new(root, member));
145                 }
146                 for krate in sysroot.crates() {
147                     roots.push(PackageRoot::new(sysroot[krate].root_dir().to_path_buf(), false))
148                 }
149                 roots
150             }
151         }
152     }
153
154     pub fn out_dirs(&self) -> Vec<PathBuf> {
155         match self {
156             ProjectWorkspace::Json { project } => {
157                 let mut out_dirs = Vec::with_capacity(project.crates.len());
158                 for krate in &project.crates {
159                     if let Some(out_dir) = &krate.out_dir {
160                         out_dirs.push(out_dir.to_path_buf());
161                     }
162                 }
163                 out_dirs
164             }
165             ProjectWorkspace::Cargo { cargo, sysroot: _sysroot } => {
166                 let mut out_dirs = Vec::with_capacity(cargo.packages().len());
167                 for pkg in cargo.packages() {
168                     if let Some(out_dir) = &cargo[pkg].out_dir {
169                         out_dirs.push(out_dir.to_path_buf());
170                     }
171                 }
172                 out_dirs
173             }
174         }
175     }
176
177     pub fn proc_macro_dylib_paths(&self) -> Vec<PathBuf> {
178         match self {
179             ProjectWorkspace::Json { project } => {
180                 let mut proc_macro_dylib_paths = Vec::with_capacity(project.crates.len());
181                 for krate in &project.crates {
182                     if let Some(out_dir) = &krate.proc_macro_dylib_path {
183                         proc_macro_dylib_paths.push(out_dir.to_path_buf());
184                     }
185                 }
186                 proc_macro_dylib_paths
187             }
188             ProjectWorkspace::Cargo { cargo, sysroot: _sysroot } => {
189                 let mut proc_macro_dylib_paths = Vec::with_capacity(cargo.packages().len());
190                 for pkg in cargo.packages() {
191                     if let Some(dylib_path) = &cargo[pkg].proc_macro_dylib_path {
192                         proc_macro_dylib_paths.push(dylib_path.to_path_buf());
193                     }
194                 }
195                 proc_macro_dylib_paths
196             }
197         }
198     }
199
200     pub fn n_packages(&self) -> usize {
201         match self {
202             ProjectWorkspace::Json { project } => project.crates.len(),
203             ProjectWorkspace::Cargo { cargo, sysroot } => {
204                 cargo.packages().len() + sysroot.crates().len()
205             }
206         }
207     }
208
209     pub fn to_crate_graph(
210         &self,
211         default_cfg_options: &CfgOptions,
212         extern_source_roots: &FxHashMap<PathBuf, ExternSourceId>,
213         proc_macro_client: &ProcMacroClient,
214         load: &mut dyn FnMut(&Path) -> Option<FileId>,
215     ) -> CrateGraph {
216         let mut crate_graph = CrateGraph::default();
217         match self {
218             ProjectWorkspace::Json { project } => {
219                 let mut crates = FxHashMap::default();
220                 for (id, krate) in project.crates.iter().enumerate() {
221                     let crate_id = json_project::CrateId(id);
222                     if let Some(file_id) = load(&krate.root_module) {
223                         let edition = match krate.edition {
224                             json_project::Edition::Edition2015 => Edition::Edition2015,
225                             json_project::Edition::Edition2018 => Edition::Edition2018,
226                         };
227                         let cfg_options = {
228                             let mut opts = default_cfg_options.clone();
229                             for name in &krate.atom_cfgs {
230                                 opts.insert_atom(name.into());
231                             }
232                             for (key, value) in &krate.key_value_cfgs {
233                                 opts.insert_key_value(key.into(), value.into());
234                             }
235                             opts
236                         };
237
238                         let mut env = Env::default();
239                         let mut extern_source = ExternSource::default();
240                         if let Some(out_dir) = &krate.out_dir {
241                             // FIXME: We probably mangle non UTF-8 paths here, figure out a better solution
242                             env.set("OUT_DIR", out_dir.to_string_lossy().to_string());
243                             if let Some(&extern_source_id) = extern_source_roots.get(out_dir) {
244                                 extern_source.set_extern_path(&out_dir, extern_source_id);
245                             }
246                         }
247                         let proc_macro = krate
248                             .proc_macro_dylib_path
249                             .clone()
250                             .map(|it| proc_macro_client.by_dylib_path(&it));
251                         // FIXME: No crate name in json definition such that we cannot add OUT_DIR to env
252                         crates.insert(
253                             crate_id,
254                             crate_graph.add_crate_root(
255                                 file_id,
256                                 edition,
257                                 // FIXME json definitions can store the crate name
258                                 None,
259                                 cfg_options,
260                                 env,
261                                 extern_source,
262                                 proc_macro.unwrap_or_default(),
263                             ),
264                         );
265                     }
266                 }
267
268                 for (id, krate) in project.crates.iter().enumerate() {
269                     for dep in &krate.deps {
270                         let from_crate_id = json_project::CrateId(id);
271                         let to_crate_id = dep.krate;
272                         if let (Some(&from), Some(&to)) =
273                             (crates.get(&from_crate_id), crates.get(&to_crate_id))
274                         {
275                             if crate_graph
276                                 .add_dep(from, CrateName::new(&dep.name).unwrap(), to)
277                                 .is_err()
278                             {
279                                 log::error!(
280                                     "cyclic dependency {:?} -> {:?}",
281                                     from_crate_id,
282                                     to_crate_id
283                                 );
284                             }
285                         }
286                     }
287                 }
288             }
289             ProjectWorkspace::Cargo { cargo, sysroot } => {
290                 let mut sysroot_crates = FxHashMap::default();
291                 for krate in sysroot.crates() {
292                     if let Some(file_id) = load(&sysroot[krate].root) {
293                         // Crates from sysroot have `cfg(test)` disabled
294                         let cfg_options = {
295                             let mut opts = default_cfg_options.clone();
296                             opts.remove_atom("test");
297                             opts
298                         };
299
300                         let env = Env::default();
301                         let extern_source = ExternSource::default();
302                         let proc_macro = vec![];
303
304                         let crate_id = crate_graph.add_crate_root(
305                             file_id,
306                             Edition::Edition2018,
307                             Some(
308                                 CrateName::new(&sysroot[krate].name)
309                                     .expect("Sysroot crate names should not contain dashes"),
310                             ),
311                             cfg_options,
312                             env,
313                             extern_source,
314                             proc_macro,
315                         );
316                         sysroot_crates.insert(krate, crate_id);
317                     }
318                 }
319                 for from in sysroot.crates() {
320                     for &to in sysroot[from].deps.iter() {
321                         let name = &sysroot[to].name;
322                         if let (Some(&from), Some(&to)) =
323                             (sysroot_crates.get(&from), sysroot_crates.get(&to))
324                         {
325                             if crate_graph.add_dep(from, CrateName::new(name).unwrap(), to).is_err()
326                             {
327                                 log::error!("cyclic dependency between sysroot crates")
328                             }
329                         }
330                     }
331                 }
332
333                 let libcore = sysroot.core().and_then(|it| sysroot_crates.get(&it).copied());
334                 let liballoc = sysroot.alloc().and_then(|it| sysroot_crates.get(&it).copied());
335                 let libstd = sysroot.std().and_then(|it| sysroot_crates.get(&it).copied());
336                 let libproc_macro =
337                     sysroot.proc_macro().and_then(|it| sysroot_crates.get(&it).copied());
338
339                 let mut pkg_to_lib_crate = FxHashMap::default();
340                 let mut pkg_crates = FxHashMap::default();
341                 // Next, create crates for each package, target pair
342                 for pkg in cargo.packages() {
343                     let mut lib_tgt = None;
344                     for &tgt in cargo[pkg].targets.iter() {
345                         let root = cargo[tgt].root.as_path();
346                         if let Some(file_id) = load(root) {
347                             let edition = cargo[pkg].edition;
348                             let cfg_options = {
349                                 let mut opts = default_cfg_options.clone();
350                                 opts.insert_features(cargo[pkg].features.iter().map(Into::into));
351                                 opts
352                             };
353                             let mut env = Env::default();
354                             let mut extern_source = ExternSource::default();
355                             if let Some(out_dir) = &cargo[pkg].out_dir {
356                                 // FIXME: We probably mangle non UTF-8 paths here, figure out a better solution
357                                 env.set("OUT_DIR", out_dir.to_string_lossy().to_string());
358                                 if let Some(&extern_source_id) = extern_source_roots.get(out_dir) {
359                                     extern_source.set_extern_path(&out_dir, extern_source_id);
360                                 }
361                             }
362                             let proc_macro = cargo[pkg]
363                                 .proc_macro_dylib_path
364                                 .as_ref()
365                                 .map(|it| proc_macro_client.by_dylib_path(&it))
366                                 .unwrap_or_default();
367
368                             let crate_id = crate_graph.add_crate_root(
369                                 file_id,
370                                 edition,
371                                 Some(CrateName::normalize_dashes(&cargo[pkg].name)),
372                                 cfg_options,
373                                 env,
374                                 extern_source,
375                                 proc_macro.clone(),
376                             );
377                             if cargo[tgt].kind == TargetKind::Lib {
378                                 lib_tgt = Some((crate_id, cargo[tgt].name.clone()));
379                                 pkg_to_lib_crate.insert(pkg, crate_id);
380                             }
381                             if cargo[tgt].is_proc_macro {
382                                 if let Some(proc_macro) = libproc_macro {
383                                     if crate_graph
384                                         .add_dep(
385                                             crate_id,
386                                             CrateName::new("proc_macro").unwrap(),
387                                             proc_macro,
388                                         )
389                                         .is_err()
390                                     {
391                                         log::error!(
392                                             "cyclic dependency on proc_macro for {}",
393                                             &cargo[pkg].name
394                                         )
395                                     }
396                                 }
397                             }
398
399                             pkg_crates.entry(pkg).or_insert_with(Vec::new).push(crate_id);
400                         }
401                     }
402
403                     // Set deps to the core, std and to the lib target of the current package
404                     for &from in pkg_crates.get(&pkg).into_iter().flatten() {
405                         if let Some((to, name)) = lib_tgt.clone() {
406                             if to != from
407                                 && crate_graph
408                                     .add_dep(
409                                         from,
410                                         // For root projects with dashes in their name,
411                                         // cargo metadata does not do any normalization,
412                                         // so we do it ourselves currently
413                                         CrateName::normalize_dashes(&name),
414                                         to,
415                                     )
416                                     .is_err()
417                             {
418                                 {
419                                     log::error!(
420                                         "cyclic dependency between targets of {}",
421                                         &cargo[pkg].name
422                                     )
423                                 }
424                             }
425                         }
426                         // core is added as a dependency before std in order to
427                         // mimic rustcs dependency order
428                         if let Some(core) = libcore {
429                             if crate_graph
430                                 .add_dep(from, CrateName::new("core").unwrap(), core)
431                                 .is_err()
432                             {
433                                 log::error!("cyclic dependency on core for {}", &cargo[pkg].name)
434                             }
435                         }
436                         if let Some(alloc) = liballoc {
437                             if crate_graph
438                                 .add_dep(from, CrateName::new("alloc").unwrap(), alloc)
439                                 .is_err()
440                             {
441                                 log::error!("cyclic dependency on alloc for {}", &cargo[pkg].name)
442                             }
443                         }
444                         if let Some(std) = libstd {
445                             if crate_graph
446                                 .add_dep(from, CrateName::new("std").unwrap(), std)
447                                 .is_err()
448                             {
449                                 log::error!("cyclic dependency on std for {}", &cargo[pkg].name)
450                             }
451                         }
452                     }
453                 }
454
455                 // Now add a dep edge from all targets of upstream to the lib
456                 // target of downstream.
457                 for pkg in cargo.packages() {
458                     for dep in cargo[pkg].dependencies.iter() {
459                         if let Some(&to) = pkg_to_lib_crate.get(&dep.pkg) {
460                             for &from in pkg_crates.get(&pkg).into_iter().flatten() {
461                                 if crate_graph
462                                     .add_dep(from, CrateName::new(&dep.name).unwrap(), to)
463                                     .is_err()
464                                 {
465                                     log::error!(
466                                         "cyclic dependency {} -> {}",
467                                         &cargo[pkg].name,
468                                         &cargo[dep.pkg].name
469                                     )
470                                 }
471                             }
472                         }
473                     }
474                 }
475             }
476         }
477         crate_graph
478     }
479
480     pub fn workspace_root_for(&self, path: &Path) -> Option<&Path> {
481         match self {
482             ProjectWorkspace::Cargo { cargo, .. } => {
483                 Some(cargo.workspace_root()).filter(|root| path.starts_with(root))
484             }
485             ProjectWorkspace::Json { project: JsonProject { roots, .. } } => roots
486                 .iter()
487                 .find(|root| path.starts_with(&root.path))
488                 .map(|root| root.path.as_ref()),
489         }
490     }
491 }
492
493 fn find_rust_project_json(path: &Path) -> Option<PathBuf> {
494     if path.ends_with("rust-project.json") {
495         return Some(path.to_path_buf());
496     }
497
498     let mut curr = Some(path);
499     while let Some(path) = curr {
500         let candidate = path.join("rust-project.json");
501         if candidate.exists() {
502             return Some(candidate);
503         }
504         curr = path.parent();
505     }
506
507     None
508 }
509
510 fn find_cargo_toml_in_parent_dir(path: &Path) -> Option<PathBuf> {
511     let mut curr = Some(path);
512     while let Some(path) = curr {
513         let candidate = path.join("Cargo.toml");
514         if candidate.exists() {
515             return Some(candidate);
516         }
517         curr = path.parent();
518     }
519
520     None
521 }
522
523 fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<PathBuf> {
524     // Only one level down to avoid cycles the easy way and stop a runaway scan with large projects
525     let mut valid_canditates = vec![];
526     for entity in entities.filter_map(Result::ok) {
527         let candidate = entity.path().join("Cargo.toml");
528         if candidate.exists() {
529             valid_canditates.push(candidate)
530         }
531     }
532     valid_canditates
533 }
534
535 fn find_cargo_toml(path: &Path) -> Result<PathBuf> {
536     if path.ends_with("Cargo.toml") {
537         return Ok(path.to_path_buf());
538     }
539
540     if let Some(p) = find_cargo_toml_in_parent_dir(path) {
541         return Ok(p);
542     }
543
544     let entities = match read_dir(path) {
545         Ok(entities) => entities,
546         Err(e) => {
547             return Err(CargoTomlNotFoundError {
548                 searched_at: path.to_path_buf(),
549                 reason: format!("file system error: {}", e),
550             }
551             .into());
552         }
553     };
554
555     let mut valid_canditates = find_cargo_toml_in_child_dir(entities);
556     match valid_canditates.len() {
557         1 => Ok(valid_canditates.remove(0)),
558         0 => Err(CargoTomlNotFoundError {
559             searched_at: path.to_path_buf(),
560             reason: "no Cargo.toml file found".to_string(),
561         }
562         .into()),
563         _ => Err(CargoTomlNotFoundError {
564             searched_at: path.to_path_buf(),
565             reason: format!(
566                 "multiple equally valid Cargo.toml files found: {:?}",
567                 valid_canditates
568             ),
569         }
570         .into()),
571     }
572 }
573
574 pub fn get_rustc_cfg_options() -> CfgOptions {
575     let mut cfg_options = CfgOptions::default();
576
577     // Some nightly-only cfgs, which are required for stdlib
578     {
579         cfg_options.insert_atom("target_thread_local".into());
580         for &target_has_atomic in ["8", "16", "32", "64", "cas", "ptr"].iter() {
581             cfg_options.insert_key_value("target_has_atomic".into(), target_has_atomic.into());
582             cfg_options
583                 .insert_key_value("target_has_atomic_load_store".into(), target_has_atomic.into());
584         }
585     }
586
587     match (|| -> Result<String> {
588         // `cfg(test)` and `cfg(debug_assertion)` are handled outside, so we suppress them here.
589         let output = Command::new("rustc")
590             .args(&["--print", "cfg", "-O"])
591             .output()
592             .context("Failed to get output from rustc --print cfg -O")?;
593         if !output.status.success() {
594             bail!(
595                 "rustc --print cfg -O exited with exit code ({})",
596                 output
597                     .status
598                     .code()
599                     .map_or(String::from("no exit code"), |code| format!("{}", code))
600             );
601         }
602         Ok(String::from_utf8(output.stdout)?)
603     })() {
604         Ok(rustc_cfgs) => {
605             for line in rustc_cfgs.lines() {
606                 match line.find('=') {
607                     None => cfg_options.insert_atom(line.into()),
608                     Some(pos) => {
609                         let key = &line[..pos];
610                         let value = line[pos + 1..].trim_matches('"');
611                         cfg_options.insert_key_value(key.into(), value.into());
612                     }
613                 }
614             }
615         }
616         Err(e) => log::error!("failed to get rustc cfgs: {}", e),
617     }
618
619     cfg_options
620 }