bevy_macro_utils/
bevy_manifest.rs

1extern crate proc_macro;
2
3use alloc::collections::BTreeMap;
4use parking_lot::{lock_api::RwLockReadGuard, MappedRwLockReadGuard, RwLock, RwLockWriteGuard};
5use proc_macro::TokenStream;
6use std::{
7    env,
8    path::{Path, PathBuf},
9    time::SystemTime,
10};
11use toml_edit::{ImDocument, Item};
12
13/// The path to the `Cargo.toml` file for the Bevy project.
14#[derive(Debug)]
15pub struct BevyManifest {
16    manifest: ImDocument<Box<str>>,
17    modified_time: SystemTime,
18}
19
20const BEVY: &str = "bevy";
21
22impl BevyManifest {
23    /// Returns a global shared instance of the [`BevyManifest`] struct.
24    pub fn shared() -> MappedRwLockReadGuard<'static, BevyManifest> {
25        static MANIFESTS: RwLock<BTreeMap<PathBuf, BevyManifest>> = RwLock::new(BTreeMap::new());
26        let manifest_path = Self::get_manifest_path();
27        let modified_time = Self::get_manifest_modified_time(&manifest_path)
28            .expect("The Cargo.toml should have a modified time");
29
30        if let Ok(manifest) =
31            RwLockReadGuard::try_map(MANIFESTS.read(), |manifests| manifests.get(&manifest_path))
32        {
33            if manifest.modified_time == modified_time {
34                return manifest;
35            }
36        }
37
38        let manifest = BevyManifest {
39            manifest: Self::read_manifest(&manifest_path),
40            modified_time,
41        };
42
43        let key = manifest_path.clone();
44        let mut manifests = MANIFESTS.write();
45        manifests.insert(key, manifest);
46
47        RwLockReadGuard::map(RwLockWriteGuard::downgrade(manifests), |manifests| {
48            manifests.get(&manifest_path).unwrap()
49        })
50    }
51
52    fn get_manifest_path() -> PathBuf {
53        env::var_os("CARGO_MANIFEST_DIR")
54            .map(|path| {
55                let mut path = PathBuf::from(path);
56                path.push("Cargo.toml");
57                assert!(
58                    path.exists(),
59                    "Cargo manifest does not exist at path {}",
60                    path.display()
61                );
62                path
63            })
64            .expect("CARGO_MANIFEST_DIR is not defined.")
65    }
66
67    fn get_manifest_modified_time(
68        cargo_manifest_path: &Path,
69    ) -> Result<SystemTime, std::io::Error> {
70        std::fs::metadata(cargo_manifest_path).and_then(|metadata| metadata.modified())
71    }
72
73    fn read_manifest(path: &Path) -> ImDocument<Box<str>> {
74        let manifest = std::fs::read_to_string(path)
75            .unwrap_or_else(|_| panic!("Unable to read cargo manifest: {}", path.display()))
76            .into_boxed_str();
77        ImDocument::parse(manifest)
78            .unwrap_or_else(|_| panic!("Failed to parse cargo manifest: {}", path.display()))
79    }
80
81    /// Attempt to retrieve the [path](syn::Path) of a particular package in
82    /// the [manifest](BevyManifest) by [name](str).
83    pub fn maybe_get_path(&self, name: &str) -> Option<syn::Path> {
84        let find_in_deps = |deps: &Item| -> Option<syn::Path> {
85            let package = if deps.get(name).is_some() {
86                return Some(Self::parse_str(name));
87            } else if deps.get(BEVY).is_some() {
88                BEVY
89            } else {
90                // Note: to support bevy crate aliases, we could do scanning here to find a crate with a "package" name that
91                // matches our request, but that would then mean we are scanning every dependency (and dev dependency) for every
92                // macro execution that hits this branch (which includes all built-in bevy crates). Our current stance is that supporting
93                // remapped crate names in derive macros is not worth that "compile time" price of admission. As a workaround, people aliasing
94                // bevy crate names can use "use REMAPPED as bevy_X" or "use REMAPPED::x as bevy_x".
95                return None;
96            };
97
98            let mut path = Self::parse_str::<syn::Path>(package);
99            if let Some(module) = name.strip_prefix("bevy_") {
100                path.segments.push(Self::parse_str(module));
101            }
102            Some(path)
103        };
104
105        let deps = self.manifest.get("dependencies");
106        let deps_dev = self.manifest.get("dev-dependencies");
107
108        deps.and_then(find_in_deps)
109            .or_else(|| deps_dev.and_then(find_in_deps))
110    }
111
112    /// Attempt to parse the provided [path](str) as a [syntax tree node](syn::parse::Parse)
113    pub fn try_parse_str<T: syn::parse::Parse>(path: &str) -> Option<T> {
114        syn::parse(path.parse::<TokenStream>().ok()?).ok()
115    }
116
117    /// Returns the path for the crate with the given name.
118    pub fn get_path(&self, name: &str) -> syn::Path {
119        self.maybe_get_path(name)
120            .unwrap_or_else(|| Self::parse_str(name))
121    }
122
123    /// Attempt to parse provided [path](str) as a [syntax tree node](syn::parse::Parse).
124    ///
125    /// # Panics
126    ///
127    /// Will panic if the path is not able to be parsed. For a non-panicking option, see [`try_parse_str`]
128    ///
129    /// [`try_parse_str`]: Self::try_parse_str
130    pub fn parse_str<T: syn::parse::Parse>(path: &str) -> T {
131        Self::try_parse_str(path).unwrap()
132    }
133}