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