Path: blob/main/crates/bevy_asset/src/io/embedded/mod.rs
9395 views
#[cfg(feature = "embedded_watcher")]1mod embedded_watcher;23#[cfg(feature = "embedded_watcher")]4pub use embedded_watcher::*;56use crate::io::{7memory::{Dir, MemoryAssetReader, Value},8AssetSourceBuilder, AssetSourceBuilders,9};10use crate::AssetServer;11use alloc::boxed::Box;12use bevy_app::App;13use bevy_ecs::{resource::Resource, world::World};14#[cfg(feature = "embedded_watcher")]15use bevy_platform::sync::{Arc, PoisonError, RwLock};16use std::path::{Path, PathBuf};1718#[cfg(feature = "embedded_watcher")]19use alloc::borrow::ToOwned;2021/// The name of the `embedded` [`AssetSource`](crate::io::AssetSource),22/// as stored in the [`AssetSourceBuilders`] resource.23pub const EMBEDDED: &str = "embedded";2425/// A [`Resource`] that manages "rust source files" in a virtual in memory [`Dir`], which is intended26/// to be shared with a [`MemoryAssetReader`].27/// Generally this should not be interacted with directly. The [`embedded_asset`] will populate this.28///29/// [`embedded_asset`]: crate::embedded_asset30#[derive(Resource, Default)]31pub struct EmbeddedAssetRegistry {32dir: Dir,33#[cfg(feature = "embedded_watcher")]34root_paths: Arc<RwLock<bevy_platform::collections::HashMap<Box<Path>, PathBuf>>>,35}3637impl EmbeddedAssetRegistry {38/// Inserts a new asset. `full_path` is the full path (as [`file`] would return for that file, if it was capable of39/// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded`40/// [`AssetSource`](crate::io::AssetSource). `value` is the bytes that will be returned for the asset. This can be41/// _either_ a `&'static [u8]` or a [`Vec<u8>`](alloc::vec::Vec).42#[cfg_attr(43not(feature = "embedded_watcher"),44expect(45unused_variables,46reason = "The `full_path` argument is not used when `embedded_watcher` is disabled."47)48)]49pub fn insert_asset(&self, full_path: PathBuf, asset_path: &Path, value: impl Into<Value>) {50#[cfg(feature = "embedded_watcher")]51self.root_paths52.write()53.unwrap_or_else(PoisonError::into_inner)54.insert(full_path.into(), asset_path.to_owned());55self.dir.insert_asset(asset_path, value);56}5758/// Inserts new asset metadata. `full_path` is the full path (as [`file`] would return for that file, if it was capable of59/// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded`60/// [`AssetSource`](crate::io::AssetSource). `value` is the bytes that will be returned for the asset. This can be _either_61/// a `&'static [u8]` or a [`Vec<u8>`](alloc::vec::Vec).62#[cfg_attr(63not(feature = "embedded_watcher"),64expect(65unused_variables,66reason = "The `full_path` argument is not used when `embedded_watcher` is disabled."67)68)]69pub fn insert_meta(&self, full_path: &Path, asset_path: &Path, value: impl Into<Value>) {70#[cfg(feature = "embedded_watcher")]71self.root_paths72.write()73.unwrap_or_else(PoisonError::into_inner)74.insert(full_path.into(), asset_path.to_owned());75self.dir.insert_meta(asset_path, value);76}7778/// Removes an asset stored using `full_path` (the full path as [`file`] would return for that file, if it was capable of79/// running in a non-rust file). If no asset is stored with at `full_path` its a no-op.80/// It returning `Option` contains the originally stored `Data` or `None`.81pub fn remove_asset(&self, full_path: &Path) -> Option<super::memory::Data> {82self.dir.remove_asset(full_path)83}8485/// Registers the [`EMBEDDED`] [`AssetSource`](crate::io::AssetSource) with the given [`AssetSourceBuilders`].86pub fn register_source(&self, sources: &mut AssetSourceBuilders) {87let dir = self.dir.clone();88let processed_dir = self.dir.clone();8990#[cfg_attr(91not(feature = "embedded_watcher"),92expect(93unused_mut,94reason = "Variable is only mutated when `embedded_watcher` feature is enabled."95)96)]97let mut source =98AssetSourceBuilder::new(move || Box::new(MemoryAssetReader { root: dir.clone() }))99.with_processed_reader(move || {100Box::new(MemoryAssetReader {101root: processed_dir.clone(),102})103})104// Note that we only add a processed watch warning because we don't want to warn105// noisily about embedded watching (which is niche) when users enable file watching.106.with_processed_watch_warning(107"Consider enabling the `embedded_watcher` cargo feature.",108);109110#[cfg(feature = "embedded_watcher")]111{112let root_paths = self.root_paths.clone();113let dir = self.dir.clone();114let processed_root_paths = self.root_paths.clone();115let processed_dir = self.dir.clone();116source = source117.with_watcher(move |sender| {118Some(Box::new(EmbeddedWatcher::new(119dir.clone(),120root_paths.clone(),121sender,122core::time::Duration::from_millis(300),123)))124})125.with_processed_watcher(move |sender| {126Some(Box::new(EmbeddedWatcher::new(127processed_dir.clone(),128processed_root_paths.clone(),129sender,130core::time::Duration::from_millis(300),131)))132});133}134sources.insert(EMBEDDED, source);135}136}137138/// Trait for the [`load_embedded_asset!`] macro, to access [`AssetServer`]139/// from arbitrary things.140///141/// [`load_embedded_asset!`]: crate::load_embedded_asset142pub trait GetAssetServer {143fn get_asset_server(&self) -> &AssetServer;144}145146impl GetAssetServer for App {147fn get_asset_server(&self) -> &AssetServer {148self.world().get_asset_server()149}150}151152impl GetAssetServer for World {153fn get_asset_server(&self) -> &AssetServer {154self.resource()155}156}157158impl GetAssetServer for AssetServer {159fn get_asset_server(&self) -> &AssetServer {160self161}162}163164/// Load an [embedded asset](crate::embedded_asset).165///166/// This is useful if the embedded asset in question is not publicly exposed, but167/// you need to use it internally.168///169/// # Syntax170///171/// This macro takes two arguments and an optional third one:172/// 1. The asset source. It may be `AssetServer`, `World` or `App`.173/// 2. The path to the asset to embed, as a string literal.174/// 3. Optionally, a closure of the same type as in [`AssetServer::load_with_settings`].175/// Consider explicitly typing the closure argument in case of type error.176///177/// # Usage178///179/// The advantage compared to using directly [`AssetServer::load`] is:180/// - This also accepts [`World`] and [`App`] arguments.181/// - This uses the exact same path as `embedded_asset!`, so you can keep it182/// consistent.183///184/// As a rule of thumb:185/// - If the asset in used in the same module as it is declared using `embedded_asset!`,186/// use this macro.187/// - Otherwise, use `AssetServer::load`.188#[macro_export]189macro_rules! load_embedded_asset {190(@get: $path: literal, $provider: expr) => {{191let path = $crate::embedded_path!($path);192let path = $crate::AssetPath::from_path_buf(path).with_source("embedded");193let asset_server = $crate::io::embedded::GetAssetServer::get_asset_server($provider);194(path, asset_server)195}};196($provider: expr, $path: literal, $settings: expr) => {{197let (path, asset_server) = $crate::load_embedded_asset!(@get: $path, $provider);198asset_server.load_with_settings(path, $settings)199}};200($provider: expr, $path: literal) => {{201let (path, asset_server) = $crate::load_embedded_asset!(@get: $path, $provider);202asset_server.load(path)203}};204}205206/// Returns the [`Path`] for a given `embedded` asset.207/// This is used internally by [`embedded_asset`] and can be used to get a [`Path`]208/// that matches the [`AssetPath`](crate::AssetPath) used by that asset.209///210/// [`embedded_asset`]: crate::embedded_asset211#[macro_export]212macro_rules! embedded_path {213($path_str: expr) => {{214$crate::embedded_path!("src", $path_str)215}};216217($source_path: expr, $path_str: expr) => {{218let crate_name = module_path!().split(':').next().unwrap();219$crate::io::embedded::_embedded_asset_path(220crate_name,221$source_path.as_ref(),222file!().as_ref(),223$path_str.as_ref(),224)225}};226}227228/// Implementation detail of `embedded_path`, do not use this!229///230/// Returns an embedded asset path, given:231/// - `crate_name`: name of the crate where the asset is embedded232/// - `src_prefix`: path prefix of the crate's source directory, relative to the workspace root233/// - `file_path`: `std::file!()` path of the source file where `embedded_path!` is called234/// - `asset_path`: path of the embedded asset relative to `file_path`235#[doc(hidden)]236pub fn _embedded_asset_path(237crate_name: &str,238src_prefix: &Path,239file_path: &Path,240asset_path: &Path,241) -> PathBuf {242let file_path = if cfg!(not(target_family = "windows")) {243// Work around bug: https://github.com/bevyengine/bevy/issues/14246244// Note, this will break any paths on Linux/Mac containing "\"245PathBuf::from(file_path.to_str().unwrap().replace("\\", "/"))246} else {247PathBuf::from(file_path)248};249let mut maybe_parent = file_path.parent();250let after_src = loop {251let Some(parent) = maybe_parent else {252panic!("Failed to find src_prefix {src_prefix:?} in {file_path:?}")253};254if parent.ends_with(src_prefix) {255break file_path.strip_prefix(parent).unwrap();256}257maybe_parent = parent.parent();258};259let asset_path = after_src.parent().unwrap().join(asset_path);260Path::new(crate_name).join(asset_path)261}262263/// Creates a new `embedded` asset by embedding the bytes of the given path into the current binary264/// and registering those bytes with the `embedded` [`AssetSource`](crate::io::AssetSource).265///266/// This accepts the current [`App`] as the first parameter and a path `&str` (relative to the current file) as the second.267///268/// By default this will generate an [`AssetPath`] using the following rules:269///270/// 1. Search for the first `$crate_name/src/` in the path and trim to the path past that point.271/// 2. Re-add the current `$crate_name` to the front of the path272///273/// For example, consider the following file structure in the theoretical `bevy_rock` crate, which provides a Bevy [`Plugin`](bevy_app::Plugin)274/// that renders fancy rocks for scenes.275///276/// ```text277/// bevy_rock278/// ├── src279/// │ ├── render280/// │ │ ├── rock.wgsl281/// │ │ └── mod.rs282/// │ └── lib.rs283/// └── Cargo.toml284/// ```285///286/// `rock.wgsl` is a WGSL shader asset that the `bevy_rock` plugin author wants to bundle with their crate. They invoke the following287/// in `bevy_rock/src/render/mod.rs`:288///289/// `embedded_asset!(app, "rock.wgsl")`290///291/// `rock.wgsl` can now be loaded by the [`AssetServer`] as follows:292///293/// ```no_run294/// # use bevy_asset::{Asset, AssetServer, load_embedded_asset};295/// # use bevy_reflect::TypePath;296/// # let asset_server: AssetServer = panic!();297/// # #[derive(Asset, TypePath)]298/// # struct Shader;299/// // If we are loading the shader in the same module we used `embedded_asset!`:300/// let shader = load_embedded_asset!(&asset_server, "rock.wgsl");301/// # let _: bevy_asset::Handle<Shader> = shader;302///303/// // If the goal is to expose the asset **to the end user**:304/// let shader = asset_server.load::<Shader>("embedded://bevy_rock/render/rock.wgsl");305/// ```306///307/// Some things to note in the path:308/// 1. The non-default `embedded://` [`AssetSource`](crate::io::AssetSource)309/// 2. `src` is trimmed from the path310///311/// The default behavior also works for cargo workspaces. Pretend the `bevy_rock` crate now exists in a larger workspace in312/// `$SOME_WORKSPACE/crates/bevy_rock`. The asset path would remain the same, because [`embedded_asset`] searches for the313/// _first instance_ of `bevy_rock/src` in the path.314///315/// For most "standard crate structures" the default works just fine. But for some niche cases (such as cargo examples),316/// the `src` path will not be present. You can override this behavior by adding it as the second argument to [`embedded_asset`]:317///318/// `embedded_asset!(app, "/examples/rock_stuff/", "rock.wgsl")`319///320/// When there are three arguments, the second argument will replace the default `/src/` value. Note that these two are321/// equivalent:322///323/// `embedded_asset!(app, "rock.wgsl")`324/// `embedded_asset!(app, "/src/", "rock.wgsl")`325///326/// This macro uses the [`include_bytes`] macro internally and _will not_ reallocate the bytes.327/// Generally the [`AssetPath`] generated will be predictable, but if your asset isn't328/// available for some reason, you can use the [`embedded_path`] macro to debug.329///330/// Hot-reloading `embedded` assets is supported. Just enable the `embedded_watcher` cargo feature.331///332/// [`AssetPath`]: crate::AssetPath333/// [`embedded_asset`]: crate::embedded_asset334/// [`embedded_path`]: crate::embedded_path335#[macro_export]336macro_rules! embedded_asset {337($app: expr, $path: expr) => {{338$crate::embedded_asset!($app, "src", $path)339}};340341($app: expr, $source_path: expr, $path: expr) => {{342let mut embedded = $app343.world_mut()344.resource_mut::<$crate::io::embedded::EmbeddedAssetRegistry>();345let path = $crate::embedded_path!($source_path, $path);346let watched_path = $crate::io::embedded::watched_path(file!(), $path);347embedded.insert_asset(watched_path, &path, include_bytes!($path));348}};349}350351/// Returns the path used by the watcher.352#[doc(hidden)]353#[cfg(feature = "embedded_watcher")]354pub fn watched_path(source_file_path: &'static str, asset_path: &'static str) -> PathBuf {355PathBuf::from(source_file_path)356.parent()357.unwrap()358.join(asset_path)359}360361/// Returns an empty PathBuf.362#[doc(hidden)]363#[cfg(not(feature = "embedded_watcher"))]364pub fn watched_path(_source_file_path: &'static str, _asset_path: &'static str) -> PathBuf {365PathBuf::from("")366}367368/// Loads an "internal" asset by embedding the string stored in the given `path_str` and associates it with the given handle.369#[macro_export]370macro_rules! load_internal_asset {371($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{372let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();373assets.insert($handle.id(), ($loader)(374include_str!($path_str),375std::path::Path::new(file!())376.parent()377.unwrap()378.join($path_str)379.to_string_lossy()380)).unwrap();381}};382// we can't support params without variadic arguments, so internal assets with additional params can't be hot-reloaded383($app: ident, $handle: ident, $path_str: expr, $loader: expr $(, $param:expr)+) => {{384let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();385assets.insert($handle.id(), ($loader)(386include_str!($path_str),387std::path::Path::new(file!())388.parent()389.unwrap()390.join($path_str)391.to_string_lossy(),392$($param),+393)).unwrap();394}};395}396397/// Loads an "internal" binary asset by embedding the bytes stored in the given `path_str` and associates it with the given handle.398#[macro_export]399macro_rules! load_internal_binary_asset {400($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{401let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();402assets403.insert(404$handle.id(),405($loader)(406include_bytes!($path_str).as_ref(),407std::path::Path::new(file!())408.parent()409.unwrap()410.join($path_str)411.to_string_lossy()412.into(),413),414)415.unwrap();416}};417}418419#[cfg(test)]420mod tests {421use super::{EmbeddedAssetRegistry, _embedded_asset_path};422use std::path::Path;423424// Relative paths show up if this macro is being invoked by a local crate.425// In this case we know the relative path is a sub- path of the workspace426// root.427428#[test]429fn embedded_asset_path_from_local_crate() {430let asset_path = _embedded_asset_path(431"my_crate",432"src".as_ref(),433"src/foo/plugin.rs".as_ref(),434"the/asset.png".as_ref(),435);436assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));437}438439// A blank src_path removes the embedded's file path altogether only the440// asset path remains.441#[test]442fn embedded_asset_path_from_local_crate_blank_src_path_questionable() {443let asset_path = _embedded_asset_path(444"my_crate",445"".as_ref(),446"src/foo/some/deep/path/plugin.rs".as_ref(),447"the/asset.png".as_ref(),448);449assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));450}451452#[test]453#[should_panic(expected = "Failed to find src_prefix \"NOT-THERE\" in \"src")]454fn embedded_asset_path_from_local_crate_bad_src() {455let _asset_path = _embedded_asset_path(456"my_crate",457"NOT-THERE".as_ref(),458"src/foo/plugin.rs".as_ref(),459"the/asset.png".as_ref(),460);461}462463#[test]464fn embedded_asset_path_from_local_example_crate() {465let asset_path = _embedded_asset_path(466"example_name",467"examples/foo".as_ref(),468"examples/foo/example.rs".as_ref(),469"the/asset.png".as_ref(),470);471assert_eq!(asset_path, Path::new("example_name/the/asset.png"));472}473474// Absolute paths show up if this macro is being invoked by an external475// dependency, e.g. one that's being checked out from a crates repo or git.476#[test]477fn embedded_asset_path_from_external_crate() {478let asset_path = _embedded_asset_path(479"my_crate",480"src".as_ref(),481"/path/to/crate/src/foo/plugin.rs".as_ref(),482"the/asset.png".as_ref(),483);484assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));485}486487#[test]488fn embedded_asset_path_from_external_crate_root_src_path() {489let asset_path = _embedded_asset_path(490"my_crate",491"/path/to/crate/src".as_ref(),492"/path/to/crate/src/foo/plugin.rs".as_ref(),493"the/asset.png".as_ref(),494);495assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));496}497498// Although extraneous slashes are permitted at the end, e.g., "src////",499// one or more slashes at the beginning are not.500#[test]501#[should_panic(expected = "Failed to find src_prefix \"////src\" in")]502fn embedded_asset_path_from_external_crate_extraneous_beginning_slashes() {503let asset_path = _embedded_asset_path(504"my_crate",505"////src".as_ref(),506"/path/to/crate/src/foo/plugin.rs".as_ref(),507"the/asset.png".as_ref(),508);509assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));510}511512// We don't handle this edge case because it is ambiguous with the513// information currently available to the embedded_path macro.514#[test]515fn embedded_asset_path_from_external_crate_is_ambiguous() {516let asset_path = _embedded_asset_path(517"my_crate",518"src".as_ref(),519"/path/to/.cargo/registry/src/crate/src/src/plugin.rs".as_ref(),520"the/asset.png".as_ref(),521);522// Really, should be "my_crate/src/the/asset.png"523assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));524}525526#[test]527fn remove_embedded_asset() {528let reg = EmbeddedAssetRegistry::default();529let path = std::path::PathBuf::from("a/b/asset.png");530reg.insert_asset(path.clone(), &path, &[]);531assert!(reg.dir.get_asset(&path).is_some());532assert!(reg.remove_asset(&path).is_some());533assert!(reg.dir.get_asset(&path).is_none());534assert!(reg.remove_asset(&path).is_none());535}536}537538539