Skip to main content

bevy_asset/io/embedded/
mod.rs

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