project_model/
sysroot.rs

1//! Loads "sysroot" crate.
2//!
3//! One confusing point here is that normally sysroot is a bunch of `.rlib`s,
4//! but we can't process `.rlib` and need source code instead. The source code
5//! is typically installed with `rustup component add rust-src` command.
6
7use core::fmt;
8use std::{env, fs, ops::Not, path::Path, process::Command};
9
10use anyhow::{Result, format_err};
11use itertools::Itertools;
12use paths::{AbsPath, AbsPathBuf, Utf8Path, Utf8PathBuf};
13use rustc_hash::FxHashMap;
14use stdx::format_to;
15use toolchain::{Tool, probe_for_binary};
16
17use crate::{
18    CargoWorkspace, ManifestPath, ProjectJson, RustSourceWorkspaceConfig,
19    cargo_workspace::{CargoMetadataConfig, FetchMetadata},
20    utf8_stdout,
21};
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Sysroot {
25    root: Option<AbsPathBuf>,
26    rust_lib_src_root: Option<AbsPathBuf>,
27    workspace: RustLibSrcWorkspace,
28    error: Option<String>,
29}
30
31#[derive(Debug, Clone, Eq, PartialEq)]
32pub enum RustLibSrcWorkspace {
33    Workspace(CargoWorkspace),
34    Json(ProjectJson),
35    Stitched(stitched::Stitched),
36    Empty,
37}
38
39impl fmt::Display for RustLibSrcWorkspace {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            RustLibSrcWorkspace::Workspace(ws) => write!(f, "workspace {}", ws.workspace_root()),
43            RustLibSrcWorkspace::Json(json) => write!(f, "json {}", json.manifest_or_root()),
44            RustLibSrcWorkspace::Stitched(stitched) => {
45                write!(f, "stitched with {} crates", stitched.crates.len())
46            }
47            RustLibSrcWorkspace::Empty => write!(f, "empty"),
48        }
49    }
50}
51
52impl Sysroot {
53    pub const fn empty() -> Sysroot {
54        Sysroot {
55            root: None,
56            rust_lib_src_root: None,
57            workspace: RustLibSrcWorkspace::Empty,
58            error: None,
59        }
60    }
61
62    /// Returns sysroot "root" directory, where `bin/`, `etc/`, `lib/`, `libexec/`
63    /// subfolder live, like:
64    /// `$HOME/.rustup/toolchains/nightly-2022-07-23-x86_64-unknown-linux-gnu`
65    pub fn root(&self) -> Option<&AbsPath> {
66        self.root.as_deref()
67    }
68
69    /// Returns the sysroot "source" directory, where stdlib sources are located, like:
70    /// `$HOME/.rustup/toolchains/nightly-2022-07-23-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library`
71    pub fn rust_lib_src_root(&self) -> Option<&AbsPath> {
72        self.rust_lib_src_root.as_deref()
73    }
74
75    pub fn is_rust_lib_src_empty(&self) -> bool {
76        match &self.workspace {
77            RustLibSrcWorkspace::Workspace(ws) => ws.packages().next().is_none(),
78            RustLibSrcWorkspace::Json(project_json) => project_json.n_crates() == 0,
79            RustLibSrcWorkspace::Stitched(stitched) => stitched.crates.is_empty(),
80            RustLibSrcWorkspace::Empty => true,
81        }
82    }
83
84    pub fn error(&self) -> Option<&str> {
85        self.error.as_deref()
86    }
87
88    pub fn num_packages(&self) -> usize {
89        match &self.workspace {
90            RustLibSrcWorkspace::Workspace(ws) => ws.packages().count(),
91            RustLibSrcWorkspace::Json(project_json) => project_json.n_crates(),
92            RustLibSrcWorkspace::Stitched(stitched) => stitched.crates.len(),
93            RustLibSrcWorkspace::Empty => 0,
94        }
95    }
96
97    pub(crate) fn workspace(&self) -> &RustLibSrcWorkspace {
98        &self.workspace
99    }
100}
101
102impl Sysroot {
103    /// Attempts to discover the toolchain's sysroot from the given `dir`.
104    pub fn discover(dir: &AbsPath, extra_env: &FxHashMap<String, Option<String>>) -> Sysroot {
105        let sysroot_dir = discover_sysroot_dir(dir, extra_env);
106        let rust_lib_src_dir = sysroot_dir.as_ref().ok().map(|sysroot_dir| {
107            discover_rust_lib_src_dir_or_add_component(sysroot_dir, dir, extra_env)
108        });
109        Sysroot::assemble(Some(sysroot_dir), rust_lib_src_dir)
110    }
111
112    pub fn discover_with_src_override(
113        current_dir: &AbsPath,
114        extra_env: &FxHashMap<String, Option<String>>,
115        rust_lib_src_dir: AbsPathBuf,
116    ) -> Sysroot {
117        let sysroot_dir = discover_sysroot_dir(current_dir, extra_env);
118        Sysroot::assemble(Some(sysroot_dir), Some(Ok(rust_lib_src_dir)))
119    }
120
121    pub fn discover_rust_lib_src_dir(sysroot_dir: AbsPathBuf) -> Sysroot {
122        let rust_lib_src_dir = discover_rust_lib_src_dir(&sysroot_dir)
123            .ok_or_else(|| format_err!("can't find standard library sources in {sysroot_dir}"));
124        Sysroot::assemble(Some(Ok(sysroot_dir)), Some(rust_lib_src_dir))
125    }
126
127    pub fn discover_rustc_src(&self) -> Option<ManifestPath> {
128        get_rustc_src(self.root()?)
129    }
130
131    pub fn new(sysroot_dir: Option<AbsPathBuf>, rust_lib_src_dir: Option<AbsPathBuf>) -> Sysroot {
132        Self::assemble(sysroot_dir.map(Ok), rust_lib_src_dir.map(Ok))
133    }
134
135    /// Returns a command to run a tool preferring the cargo proxies if the sysroot exists.
136    pub fn tool(
137        &self,
138        tool: Tool,
139        current_dir: impl AsRef<Path>,
140        envs: &FxHashMap<String, Option<String>>,
141    ) -> Command {
142        match self.root() {
143            Some(root) => {
144                // special case rustc, we can look that up directly in the sysroot's bin folder
145                // as it should never invoke another cargo binary
146                if let Tool::Rustc = tool
147                    && let Some(path) =
148                        probe_for_binary(root.join("bin").join(Tool::Rustc.name()).into())
149                {
150                    return toolchain::command(path, current_dir, envs);
151                }
152
153                let mut cmd = toolchain::command(tool.prefer_proxy(), current_dir, envs);
154                if !envs.contains_key("RUSTUP_TOOLCHAIN")
155                    && std::env::var_os("RUSTUP_TOOLCHAIN").is_none()
156                {
157                    cmd.env("RUSTUP_TOOLCHAIN", AsRef::<std::path::Path>::as_ref(root));
158                }
159
160                cmd
161            }
162            _ => toolchain::command(tool.path(), current_dir, envs),
163        }
164    }
165
166    pub fn discover_proc_macro_srv(&self) -> Option<anyhow::Result<AbsPathBuf>> {
167        let root = self.root()?;
168        Some(
169            ["libexec", "lib"]
170                .into_iter()
171                .map(|segment| root.join(segment).join("rust-analyzer-proc-macro-srv"))
172                .find_map(|server_path| probe_for_binary(server_path.into()))
173                .map(AbsPathBuf::assert)
174                .ok_or_else(|| {
175                    anyhow::format_err!("cannot find proc-macro server in sysroot `{}`", root)
176                }),
177        )
178    }
179
180    fn assemble(
181        sysroot_dir: Option<Result<AbsPathBuf, anyhow::Error>>,
182        rust_lib_src_dir: Option<Result<AbsPathBuf, anyhow::Error>>,
183    ) -> Sysroot {
184        let mut errors = String::new();
185        let root = match sysroot_dir {
186            Some(Ok(sysroot_dir)) => Some(sysroot_dir),
187            Some(Err(e)) => {
188                format_to!(errors, "{e}\n");
189                None
190            }
191            None => None,
192        };
193        let rust_lib_src_root = match rust_lib_src_dir {
194            Some(Ok(rust_lib_src_dir)) => Some(rust_lib_src_dir),
195            Some(Err(e)) => {
196                format_to!(errors, "{e}\n");
197                None
198            }
199            None => None,
200        };
201        Sysroot {
202            root,
203            rust_lib_src_root,
204            workspace: RustLibSrcWorkspace::Empty,
205            error: errors.is_empty().not().then_some(errors),
206        }
207    }
208
209    pub fn load_workspace(
210        &self,
211        sysroot_source_config: &RustSourceWorkspaceConfig,
212        no_deps: bool,
213        current_dir: &AbsPath,
214        target_dir: &Utf8Path,
215        progress: &dyn Fn(String),
216    ) -> Option<RustLibSrcWorkspace> {
217        assert!(matches!(self.workspace, RustLibSrcWorkspace::Empty), "workspace already loaded");
218        let Self { root: _, rust_lib_src_root: Some(src_root), workspace: _, error: _ } = self
219        else {
220            return None;
221        };
222        if let RustSourceWorkspaceConfig::CargoMetadata(cargo_config) = sysroot_source_config {
223            let library_manifest = ManifestPath::try_from(src_root.join("Cargo.toml")).unwrap();
224            if fs::metadata(&library_manifest).is_ok() {
225                match self.load_library_via_cargo(
226                    &library_manifest,
227                    current_dir,
228                    target_dir,
229                    cargo_config,
230                    no_deps,
231                    progress,
232                ) {
233                    Ok(loaded) => return Some(loaded),
234                    Err(e) => {
235                        tracing::error!("`cargo metadata` failed on `{library_manifest}` : {e}")
236                    }
237                }
238            }
239            tracing::debug!("Stitching sysroot library: {src_root}");
240
241            let mut stitched = stitched::Stitched { crates: Default::default() };
242
243            for path in stitched::SYSROOT_CRATES.trim().lines() {
244                let name = path.split('/').next_back().unwrap();
245                let root = [format!("{path}/src/lib.rs"), format!("lib{path}/lib.rs")]
246                    .into_iter()
247                    .map(|it| src_root.join(it))
248                    .filter_map(|it| ManifestPath::try_from(it).ok())
249                    .find(|it| fs::metadata(it).is_ok());
250
251                if let Some(root) = root {
252                    stitched.crates.alloc(stitched::RustLibSrcCrateData {
253                        name: name.into(),
254                        root,
255                        deps: Vec::new(),
256                    });
257                }
258            }
259
260            if let Some(std) = stitched.by_name("std") {
261                for dep in stitched::STD_DEPS.trim().lines() {
262                    if let Some(dep) = stitched.by_name(dep) {
263                        stitched.crates[std].deps.push(dep)
264                    }
265                }
266            }
267
268            if let Some(alloc) = stitched.by_name("alloc") {
269                for dep in stitched::ALLOC_DEPS.trim().lines() {
270                    if let Some(dep) = stitched.by_name(dep) {
271                        stitched.crates[alloc].deps.push(dep)
272                    }
273                }
274            }
275
276            if let Some(proc_macro) = stitched.by_name("proc_macro") {
277                for dep in stitched::PROC_MACRO_DEPS.trim().lines() {
278                    if let Some(dep) = stitched.by_name(dep) {
279                        stitched.crates[proc_macro].deps.push(dep)
280                    }
281                }
282            }
283            return Some(RustLibSrcWorkspace::Stitched(stitched));
284        } else if let RustSourceWorkspaceConfig::Json(project_json) = sysroot_source_config {
285            return Some(RustLibSrcWorkspace::Json(project_json.clone()));
286        }
287
288        None
289    }
290
291    pub fn set_workspace(&mut self, workspace: RustLibSrcWorkspace) {
292        self.workspace = workspace;
293        if self.error.is_none()
294            && let Some(src_root) = &self.rust_lib_src_root
295        {
296            let has_core = match &self.workspace {
297                RustLibSrcWorkspace::Workspace(ws) => ws.packages().any(|p| ws[p].name == "core"),
298                RustLibSrcWorkspace::Json(project_json) => project_json
299                    .crates()
300                    .filter_map(|(_, krate)| krate.display_name.clone())
301                    .any(|name| name.canonical_name().as_str() == "core"),
302                RustLibSrcWorkspace::Stitched(stitched) => stitched.by_name("core").is_some(),
303                RustLibSrcWorkspace::Empty => true,
304            };
305            if !has_core {
306                let var_note = if env::var_os("RUST_SRC_PATH").is_some() {
307                    " (env var `RUST_SRC_PATH` is set and may be incorrect, try unsetting it)"
308                } else {
309                    ", try running `rustup component add rust-src` to possibly fix this"
310                };
311                self.error =
312                    Some(format!("sysroot at `{src_root}` is missing a `core` library{var_note}",));
313            }
314        }
315    }
316
317    fn load_library_via_cargo(
318        &self,
319        library_manifest: &ManifestPath,
320        current_dir: &AbsPath,
321        target_dir: &Utf8Path,
322        cargo_config: &CargoMetadataConfig,
323        no_deps: bool,
324        progress: &dyn Fn(String),
325    ) -> Result<RustLibSrcWorkspace> {
326        tracing::debug!("Loading library metadata: {library_manifest}");
327        let mut cargo_config = cargo_config.clone();
328        // the sysroot uses `public-dependency`, so we make cargo think it's a nightly
329        cargo_config.extra_env.insert(
330            "__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS".to_owned(),
331            Some("nightly".to_owned()),
332        );
333
334        // Make sure we never attempt to write to the sysroot
335        let locked = true;
336        let (mut res, _) =
337            FetchMetadata::new(library_manifest, current_dir, &cargo_config, self, no_deps)
338                .exec(target_dir, locked, progress)?;
339
340        // Patch out `rustc-std-workspace-*` crates to point to the real crates.
341        // This is done prior to `CrateGraph` construction to prevent de-duplication logic from failing.
342        let patches = {
343            let mut fake_core = None;
344            let mut fake_alloc = None;
345            let mut fake_std = None;
346            let mut real_core = None;
347            let mut real_alloc = None;
348            let mut real_std = None;
349            res.packages.iter().enumerate().for_each(|(idx, package)| {
350                match package.name.strip_prefix("rustc-std-workspace-") {
351                    Some("core") => fake_core = Some((idx, package.id.clone())),
352                    Some("alloc") => fake_alloc = Some((idx, package.id.clone())),
353                    Some("std") => fake_std = Some((idx, package.id.clone())),
354                    Some(_) => {
355                        tracing::warn!("unknown rustc-std-workspace-* crate: {}", package.name)
356                    }
357                    None => match &**package.name {
358                        "core" => real_core = Some(package.id.clone()),
359                        "alloc" => real_alloc = Some(package.id.clone()),
360                        "std" => real_std = Some(package.id.clone()),
361                        _ => (),
362                    },
363                }
364            });
365
366            [fake_core.zip(real_core), fake_alloc.zip(real_alloc), fake_std.zip(real_std)]
367                .into_iter()
368                .flatten()
369        };
370
371        if let Some(resolve) = res.resolve.as_mut() {
372            resolve.nodes.retain_mut(|node| {
373                // Replace `rustc-std-workspace` crate with the actual one in the dependency list
374                node.deps.iter_mut().for_each(|dep| {
375                    let real_pkg = patches.clone().find(|((_, fake_id), _)| *fake_id == dep.pkg);
376                    if let Some((_, real)) = real_pkg {
377                        dep.pkg = real;
378                    }
379                });
380                // Remove this node if it's a fake one
381                !patches.clone().any(|((_, fake), _)| fake == node.id)
382            });
383        }
384        // Remove the fake ones from the package list
385        patches.map(|((idx, _), _)| idx).sorted().rev().for_each(|idx| {
386            res.packages.remove(idx);
387        });
388
389        let cargo_workspace =
390            CargoWorkspace::new(res, library_manifest.clone(), Default::default(), true);
391        Ok(RustLibSrcWorkspace::Workspace(cargo_workspace))
392    }
393}
394
395fn discover_sysroot_dir(
396    current_dir: &AbsPath,
397    extra_env: &FxHashMap<String, Option<String>>,
398) -> Result<AbsPathBuf> {
399    let mut rustc = toolchain::command(Tool::Rustc.path(), current_dir, extra_env);
400    rustc.current_dir(current_dir).args(["--print", "sysroot"]);
401    tracing::debug!("Discovering sysroot by {:?}", rustc);
402    let stdout = utf8_stdout(&mut rustc)?;
403    Ok(AbsPathBuf::assert(Utf8PathBuf::from(stdout)))
404}
405
406fn discover_rust_lib_src_dir(sysroot_path: &AbsPathBuf) -> Option<AbsPathBuf> {
407    if let Ok(path) = env::var("RUST_SRC_PATH") {
408        if let Ok(path) = AbsPathBuf::try_from(path.as_str()) {
409            let core = path.join("core");
410            if fs::metadata(&core).is_ok() {
411                tracing::debug!("Discovered sysroot by RUST_SRC_PATH: {path}");
412                return Some(path);
413            }
414            tracing::debug!("RUST_SRC_PATH is set, but is invalid (no core: {core:?}), ignoring");
415        } else {
416            tracing::debug!("RUST_SRC_PATH is set, but is invalid, ignoring");
417        }
418    }
419
420    get_rust_lib_src(sysroot_path)
421}
422
423fn discover_rust_lib_src_dir_or_add_component(
424    sysroot_path: &AbsPathBuf,
425    current_dir: &AbsPath,
426    extra_env: &FxHashMap<String, Option<String>>,
427) -> Result<AbsPathBuf> {
428    discover_rust_lib_src_dir(sysroot_path)
429        .or_else(|| {
430            let mut rustup = toolchain::command(Tool::Rustup.prefer_proxy(), current_dir, extra_env);
431            rustup.args(["component", "add", "rust-src"]);
432            tracing::info!("adding rust-src component by {:?}", rustup);
433            utf8_stdout(&mut rustup).ok()?;
434            get_rust_lib_src(sysroot_path)
435        })
436        .ok_or_else(|| {
437            tracing::error!(%sysroot_path, "can't load standard library, try installing `rust-src`");
438            format_err!(
439                "\
440can't load standard library from sysroot
441{sysroot_path}
442(discovered via `rustc --print sysroot`)
443try installing `rust-src` the same way you installed `rustc`"
444            )
445        })
446}
447
448fn get_rustc_src(sysroot_path: &AbsPath) -> Option<ManifestPath> {
449    let rustc_src = sysroot_path.join("lib/rustlib/rustc-src/rust/compiler/rustc/Cargo.toml");
450    let rustc_src = ManifestPath::try_from(rustc_src).ok()?;
451    tracing::debug!("checking for rustc source code: {rustc_src}");
452    if fs::metadata(&rustc_src).is_ok() { Some(rustc_src) } else { None }
453}
454
455fn get_rust_lib_src(sysroot_path: &AbsPath) -> Option<AbsPathBuf> {
456    let rust_lib_src = sysroot_path.join("lib/rustlib/src/rust/library");
457    tracing::debug!("checking sysroot library: {rust_lib_src}");
458    if fs::metadata(&rust_lib_src).is_ok() { Some(rust_lib_src) } else { None }
459}
460
461// FIXME: Remove this, that will bump our project MSRV to 1.82
462pub(crate) mod stitched {
463    use std::ops;
464
465    use base_db::CrateName;
466    use la_arena::{Arena, Idx};
467
468    use crate::ManifestPath;
469
470    #[derive(Debug, Clone, Eq, PartialEq)]
471    pub struct Stitched {
472        pub(super) crates: Arena<RustLibSrcCrateData>,
473    }
474
475    impl ops::Index<RustLibSrcCrate> for Stitched {
476        type Output = RustLibSrcCrateData;
477        fn index(&self, index: RustLibSrcCrate) -> &RustLibSrcCrateData {
478            &self.crates[index]
479        }
480    }
481
482    impl Stitched {
483        pub(crate) fn public_deps(
484            &self,
485        ) -> impl Iterator<Item = (CrateName, RustLibSrcCrate, bool)> + '_ {
486            // core is added as a dependency before std in order to
487            // mimic rustcs dependency order
488            [("core", true), ("alloc", false), ("std", true), ("test", false)]
489                .into_iter()
490                .filter_map(move |(name, prelude)| {
491                    Some((CrateName::new(name).unwrap(), self.by_name(name)?, prelude))
492                })
493        }
494
495        pub(crate) fn proc_macro(&self) -> Option<RustLibSrcCrate> {
496            self.by_name("proc_macro")
497        }
498
499        pub(crate) fn crates(&self) -> impl ExactSizeIterator<Item = RustLibSrcCrate> + '_ {
500            self.crates.iter().map(|(id, _data)| id)
501        }
502
503        pub(super) fn by_name(&self, name: &str) -> Option<RustLibSrcCrate> {
504            let (id, _data) = self.crates.iter().find(|(_id, data)| data.name == name)?;
505            Some(id)
506        }
507    }
508
509    pub(crate) type RustLibSrcCrate = Idx<RustLibSrcCrateData>;
510
511    #[derive(Debug, Clone, Eq, PartialEq)]
512    pub(crate) struct RustLibSrcCrateData {
513        pub(crate) name: String,
514        pub(crate) root: ManifestPath,
515        pub(crate) deps: Vec<RustLibSrcCrate>,
516    }
517
518    pub(super) const SYSROOT_CRATES: &str = "
519alloc
520backtrace
521core
522panic_abort
523panic_unwind
524proc_macro
525profiler_builtins
526std
527stdarch/crates/std_detect
528test
529unwind";
530
531    pub(super) const ALLOC_DEPS: &str = "core";
532
533    pub(super) const STD_DEPS: &str = "
534alloc
535panic_unwind
536panic_abort
537core
538profiler_builtins
539unwind
540std_detect
541test";
542
543    // core is required for our builtin derives to work in the proc_macro lib currently
544    pub(super) const PROC_MACRO_DEPS: &str = "
545std
546core";
547}