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