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