Skip to main content

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