bevy_asset/io/embedded/
mod.rs

1#[cfg(feature = "embedded_watcher")]
2mod embedded_watcher;
3
4#[cfg(feature = "embedded_watcher")]
5pub use embedded_watcher::*;
6
7use crate::io::{
8    memory::{Dir, MemoryAssetReader, Value},
9    AssetSource, AssetSourceBuilders,
10};
11use bevy_ecs::system::Resource;
12use std::path::{Path, PathBuf};
13
14pub const EMBEDDED: &str = "embedded";
15
16/// A [`Resource`] that manages "rust source files" in a virtual in memory [`Dir`], which is intended
17/// to be shared with a [`MemoryAssetReader`].
18/// Generally this should not be interacted with directly. The [`embedded_asset`] will populate this.
19///
20/// [`embedded_asset`]: crate::embedded_asset
21#[derive(Resource, Default)]
22pub struct EmbeddedAssetRegistry {
23    dir: Dir,
24    #[cfg(feature = "embedded_watcher")]
25    root_paths: alloc::sync::Arc<parking_lot::RwLock<bevy_utils::HashMap<Box<Path>, PathBuf>>>,
26}
27
28impl EmbeddedAssetRegistry {
29    /// Inserts a new asset. `full_path` is the full path (as [`file`] would return for that file, if it was capable of
30    /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded`
31    /// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]`
32    /// or a [`Vec<u8>`].
33    #[cfg_attr(
34        not(feature = "embedded_watcher"),
35        expect(
36            unused_variables,
37            reason = "The `full_path` argument is not used when `embedded_watcher` is disabled."
38        )
39    )]
40    pub fn insert_asset(&self, full_path: PathBuf, asset_path: &Path, value: impl Into<Value>) {
41        #[cfg(feature = "embedded_watcher")]
42        self.root_paths
43            .write()
44            .insert(full_path.into(), asset_path.to_owned());
45        self.dir.insert_asset(asset_path, value);
46    }
47
48    /// Inserts new asset metadata. `full_path` is the full path (as [`file`] would return for that file, if it was capable of
49    /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded`
50    /// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]`
51    /// or a [`Vec<u8>`].
52    #[cfg_attr(
53        not(feature = "embedded_watcher"),
54        expect(
55            unused_variables,
56            reason = "The `full_path` argument is not used when `embedded_watcher` is disabled."
57        )
58    )]
59    pub fn insert_meta(&self, full_path: &Path, asset_path: &Path, value: impl Into<Value>) {
60        #[cfg(feature = "embedded_watcher")]
61        self.root_paths
62            .write()
63            .insert(full_path.into(), asset_path.to_owned());
64        self.dir.insert_meta(asset_path, value);
65    }
66
67    /// Removes an asset stored using `full_path` (the full path as [`file`] would return for that file, if it was capable of
68    /// running in a non-rust file). If no asset is stored with at `full_path` its a no-op.
69    /// It returning `Option` contains the originally stored `Data` or `None`.
70    pub fn remove_asset(&self, full_path: &Path) -> Option<super::memory::Data> {
71        self.dir.remove_asset(full_path)
72    }
73
74    pub fn register_source(&self, sources: &mut AssetSourceBuilders) {
75        let dir = self.dir.clone();
76        let processed_dir = self.dir.clone();
77
78        #[cfg_attr(
79            not(feature = "embedded_watcher"),
80            expect(
81                unused_mut,
82                reason = "Variable is only mutated when `embedded_watcher` feature is enabled."
83            )
84        )]
85        let mut source = AssetSource::build()
86            .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() }))
87            .with_processed_reader(move || {
88                Box::new(MemoryAssetReader {
89                    root: processed_dir.clone(),
90                })
91            })
92            // Note that we only add a processed watch warning because we don't want to warn
93            // noisily about embedded watching (which is niche) when users enable file watching.
94            .with_processed_watch_warning(
95                "Consider enabling the `embedded_watcher` cargo feature.",
96            );
97
98        #[cfg(feature = "embedded_watcher")]
99        {
100            let root_paths = self.root_paths.clone();
101            let dir = self.dir.clone();
102            let processed_root_paths = self.root_paths.clone();
103            let processed_dir = self.dir.clone();
104            source = source
105                .with_watcher(move |sender| {
106                    Some(Box::new(EmbeddedWatcher::new(
107                        dir.clone(),
108                        root_paths.clone(),
109                        sender,
110                        core::time::Duration::from_millis(300),
111                    )))
112                })
113                .with_processed_watcher(move |sender| {
114                    Some(Box::new(EmbeddedWatcher::new(
115                        processed_dir.clone(),
116                        processed_root_paths.clone(),
117                        sender,
118                        core::time::Duration::from_millis(300),
119                    )))
120                });
121        }
122        sources.insert(EMBEDDED, source);
123    }
124}
125
126/// Returns the [`Path`] for a given `embedded` asset.
127/// This is used internally by [`embedded_asset`] and can be used to get a [`Path`]
128/// that matches the [`AssetPath`](crate::AssetPath) used by that asset.
129///
130/// [`embedded_asset`]: crate::embedded_asset
131#[macro_export]
132macro_rules! embedded_path {
133    ($path_str: expr) => {{
134        embedded_path!("src", $path_str)
135    }};
136
137    ($source_path: expr, $path_str: expr) => {{
138        let crate_name = module_path!().split(':').next().unwrap();
139        $crate::io::embedded::_embedded_asset_path(
140            crate_name,
141            $source_path.as_ref(),
142            file!().as_ref(),
143            $path_str.as_ref(),
144        )
145    }};
146}
147
148/// Implementation detail of `embedded_path`, do not use this!
149///
150/// Returns an embedded asset path, given:
151///   - `crate_name`: name of the crate where the asset is embedded
152///   - `src_prefix`: path prefix of the crate's source directory, relative to the workspace root
153///   - `file_path`: `std::file!()` path of the source file where `embedded_path!` is called
154///   - `asset_path`: path of the embedded asset relative to `file_path`
155#[doc(hidden)]
156pub fn _embedded_asset_path(
157    crate_name: &str,
158    src_prefix: &Path,
159    file_path: &Path,
160    asset_path: &Path,
161) -> PathBuf {
162    let mut maybe_parent = file_path.parent();
163    let after_src = loop {
164        let Some(parent) = maybe_parent else {
165            panic!("Failed to find src_prefix {src_prefix:?} in {file_path:?}")
166        };
167        if parent.ends_with(src_prefix) {
168            break file_path.strip_prefix(parent).unwrap();
169        }
170        maybe_parent = parent.parent();
171    };
172    let asset_path = after_src.parent().unwrap().join(asset_path);
173    Path::new(crate_name).join(asset_path)
174}
175
176/// Creates a new `embedded` asset by embedding the bytes of the given path into the current binary
177/// and registering those bytes with the `embedded` [`AssetSource`].
178///
179/// This accepts the current [`App`](bevy_app::App) as the first parameter and a path `&str` (relative to the current file) as the second.
180///
181/// By default this will generate an [`AssetPath`] using the following rules:
182///
183/// 1. Search for the first `$crate_name/src/` in the path and trim to the path past that point.
184/// 2. Re-add the current `$crate_name` to the front of the path
185///
186/// For example, consider the following file structure in the theoretical `bevy_rock` crate, which provides a Bevy [`Plugin`](bevy_app::Plugin)
187/// that renders fancy rocks for scenes.
188///
189/// ```text
190/// bevy_rock
191/// ├── src
192/// │   ├── render
193/// │   │   ├── rock.wgsl
194/// │   │   └── mod.rs
195/// │   └── lib.rs
196/// └── Cargo.toml
197/// ```
198///
199/// `rock.wgsl` is a WGSL shader asset that the `bevy_rock` plugin author wants to bundle with their crate. They invoke the following
200/// in `bevy_rock/src/render/mod.rs`:
201///
202/// `embedded_asset!(app, "rock.wgsl")`
203///
204/// `rock.wgsl` can now be loaded by the [`AssetServer`](crate::AssetServer) with the following path:
205///
206/// ```no_run
207/// # use bevy_asset::{Asset, AssetServer};
208/// # use bevy_reflect::TypePath;
209/// # let asset_server: AssetServer = panic!();
210/// # #[derive(Asset, TypePath)]
211/// # struct Shader;
212/// let shader = asset_server.load::<Shader>("embedded://bevy_rock/render/rock.wgsl");
213/// ```
214///
215/// Some things to note in the path:
216/// 1. The non-default `embedded://` [`AssetSource`]
217/// 2. `src` is trimmed from the path
218///
219/// The default behavior also works for cargo workspaces. Pretend the `bevy_rock` crate now exists in a larger workspace in
220/// `$SOME_WORKSPACE/crates/bevy_rock`. The asset path would remain the same, because [`embedded_asset`] searches for the
221/// _first instance_ of `bevy_rock/src` in the path.
222///
223/// For most "standard crate structures" the default works just fine. But for some niche cases (such as cargo examples),
224/// the `src` path will not be present. You can override this behavior by adding it as the second argument to [`embedded_asset`]:
225///
226/// `embedded_asset!(app, "/examples/rock_stuff/", "rock.wgsl")`
227///
228/// When there are three arguments, the second argument will replace the default `/src/` value. Note that these two are
229/// equivalent:
230///
231/// `embedded_asset!(app, "rock.wgsl")`
232/// `embedded_asset!(app, "/src/", "rock.wgsl")`
233///
234/// This macro uses the [`include_bytes`] macro internally and _will not_ reallocate the bytes.
235/// Generally the [`AssetPath`] generated will be predictable, but if your asset isn't
236/// available for some reason, you can use the [`embedded_path`] macro to debug.
237///
238/// Hot-reloading `embedded` assets is supported. Just enable the `embedded_watcher` cargo feature.
239///
240/// [`AssetPath`]: crate::AssetPath
241/// [`embedded_asset`]: crate::embedded_asset
242/// [`embedded_path`]: crate::embedded_path
243#[macro_export]
244macro_rules! embedded_asset {
245    ($app: ident, $path: expr) => {{
246        $crate::embedded_asset!($app, "src", $path)
247    }};
248
249    ($app: ident, $source_path: expr, $path: expr) => {{
250        let mut embedded = $app
251            .world_mut()
252            .resource_mut::<$crate::io::embedded::EmbeddedAssetRegistry>();
253        let path = $crate::embedded_path!($source_path, $path);
254        let watched_path = $crate::io::embedded::watched_path(file!(), $path);
255        embedded.insert_asset(watched_path, &path, include_bytes!($path));
256    }};
257}
258
259/// Returns the path used by the watcher.
260#[doc(hidden)]
261#[cfg(feature = "embedded_watcher")]
262pub fn watched_path(source_file_path: &'static str, asset_path: &'static str) -> PathBuf {
263    PathBuf::from(source_file_path)
264        .parent()
265        .unwrap()
266        .join(asset_path)
267}
268
269/// Returns an empty PathBuf.
270#[doc(hidden)]
271#[cfg(not(feature = "embedded_watcher"))]
272pub fn watched_path(_source_file_path: &'static str, _asset_path: &'static str) -> PathBuf {
273    PathBuf::from("")
274}
275
276/// Loads an "internal" asset by embedding the string stored in the given `path_str` and associates it with the given handle.
277#[macro_export]
278macro_rules! load_internal_asset {
279    ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{
280        let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
281        assets.insert($handle.id(), ($loader)(
282            include_str!($path_str),
283            std::path::Path::new(file!())
284                .parent()
285                .unwrap()
286                .join($path_str)
287                .to_string_lossy()
288        ));
289    }};
290    // we can't support params without variadic arguments, so internal assets with additional params can't be hot-reloaded
291    ($app: ident, $handle: ident, $path_str: expr, $loader: expr $(, $param:expr)+) => {{
292        let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
293        assets.insert($handle.id(), ($loader)(
294            include_str!($path_str),
295            std::path::Path::new(file!())
296                .parent()
297                .unwrap()
298                .join($path_str)
299                .to_string_lossy(),
300            $($param),+
301        ));
302    }};
303}
304
305/// Loads an "internal" binary asset by embedding the bytes stored in the given `path_str` and associates it with the given handle.
306#[macro_export]
307macro_rules! load_internal_binary_asset {
308    ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{
309        let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
310        assets.insert(
311            $handle.id(),
312            ($loader)(
313                include_bytes!($path_str).as_ref(),
314                std::path::Path::new(file!())
315                    .parent()
316                    .unwrap()
317                    .join($path_str)
318                    .to_string_lossy()
319                    .into(),
320            ),
321        );
322    }};
323}
324
325#[cfg(test)]
326mod tests {
327    use super::{EmbeddedAssetRegistry, _embedded_asset_path};
328    use std::path::Path;
329
330    // Relative paths show up if this macro is being invoked by a local crate.
331    // In this case we know the relative path is a sub- path of the workspace
332    // root.
333
334    #[test]
335    fn embedded_asset_path_from_local_crate() {
336        let asset_path = _embedded_asset_path(
337            "my_crate",
338            "src".as_ref(),
339            "src/foo/plugin.rs".as_ref(),
340            "the/asset.png".as_ref(),
341        );
342        assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
343    }
344
345    // A blank src_path removes the embedded's file path altogether only the
346    // asset path remains.
347    #[test]
348    fn embedded_asset_path_from_local_crate_blank_src_path_questionable() {
349        let asset_path = _embedded_asset_path(
350            "my_crate",
351            "".as_ref(),
352            "src/foo/some/deep/path/plugin.rs".as_ref(),
353            "the/asset.png".as_ref(),
354        );
355        assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));
356    }
357
358    #[test]
359    #[should_panic(expected = "Failed to find src_prefix \"NOT-THERE\" in \"src")]
360    fn embedded_asset_path_from_local_crate_bad_src() {
361        let _asset_path = _embedded_asset_path(
362            "my_crate",
363            "NOT-THERE".as_ref(),
364            "src/foo/plugin.rs".as_ref(),
365            "the/asset.png".as_ref(),
366        );
367    }
368
369    #[test]
370    fn embedded_asset_path_from_local_example_crate() {
371        let asset_path = _embedded_asset_path(
372            "example_name",
373            "examples/foo".as_ref(),
374            "examples/foo/example.rs".as_ref(),
375            "the/asset.png".as_ref(),
376        );
377        assert_eq!(asset_path, Path::new("example_name/the/asset.png"));
378    }
379
380    // Absolute paths show up if this macro is being invoked by an external
381    // dependency, e.g. one that's being checked out from a crates repo or git.
382    #[test]
383    fn embedded_asset_path_from_external_crate() {
384        let asset_path = _embedded_asset_path(
385            "my_crate",
386            "src".as_ref(),
387            "/path/to/crate/src/foo/plugin.rs".as_ref(),
388            "the/asset.png".as_ref(),
389        );
390        assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
391    }
392
393    #[test]
394    fn embedded_asset_path_from_external_crate_root_src_path() {
395        let asset_path = _embedded_asset_path(
396            "my_crate",
397            "/path/to/crate/src".as_ref(),
398            "/path/to/crate/src/foo/plugin.rs".as_ref(),
399            "the/asset.png".as_ref(),
400        );
401        assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
402    }
403
404    // Although extraneous slashes are permitted at the end, e.g., "src////",
405    // one or more slashes at the beginning are not.
406    #[test]
407    #[should_panic(expected = "Failed to find src_prefix \"////src\" in")]
408    fn embedded_asset_path_from_external_crate_extraneous_beginning_slashes() {
409        let asset_path = _embedded_asset_path(
410            "my_crate",
411            "////src".as_ref(),
412            "/path/to/crate/src/foo/plugin.rs".as_ref(),
413            "the/asset.png".as_ref(),
414        );
415        assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
416    }
417
418    // We don't handle this edge case because it is ambiguous with the
419    // information currently available to the embedded_path macro.
420    #[test]
421    fn embedded_asset_path_from_external_crate_is_ambiguous() {
422        let asset_path = _embedded_asset_path(
423            "my_crate",
424            "src".as_ref(),
425            "/path/to/.cargo/registry/src/crate/src/src/plugin.rs".as_ref(),
426            "the/asset.png".as_ref(),
427        );
428        // Really, should be "my_crate/src/the/asset.png"
429        assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));
430    }
431
432    #[test]
433    fn remove_embedded_asset() {
434        let reg = EmbeddedAssetRegistry::default();
435        let path = std::path::PathBuf::from("a/b/asset.png");
436        reg.insert_asset(path.clone(), &path, &[]);
437        assert!(reg.dir.get_asset(&path).is_some());
438        assert!(reg.remove_asset(&path).is_some());
439        assert!(reg.dir.get_asset(&path).is_none());
440        assert!(reg.remove_asset(&path).is_none());
441    }
442}