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