
The goal of `bevy_platform_support` is to provide a set of platform agnostic APIs, alongside platform-specific functionality. This is a high traffic crate (providing things like HashMap and Instant). Especially in light of https://github.com/bevyengine/bevy/discussions/18799, it deserves a friendlier / shorter name. Given that it hasn't had a full release yet, getting this change in before Bevy 0.16 makes sense. - Rename `bevy_platform_support` to `bevy_platform`.
452 lines
17 KiB
Rust
452 lines
17 KiB
Rust
#[cfg(feature = "embedded_watcher")]
|
|
mod embedded_watcher;
|
|
|
|
#[cfg(feature = "embedded_watcher")]
|
|
pub use embedded_watcher::*;
|
|
|
|
use crate::io::{
|
|
memory::{Dir, MemoryAssetReader, Value},
|
|
AssetSource, AssetSourceBuilders,
|
|
};
|
|
use alloc::boxed::Box;
|
|
use bevy_ecs::resource::Resource;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
#[cfg(feature = "embedded_watcher")]
|
|
use alloc::borrow::ToOwned;
|
|
|
|
/// The name of the `embedded` [`AssetSource`],
|
|
/// as stored in the [`AssetSourceBuilders`] resource.
|
|
pub const EMBEDDED: &str = "embedded";
|
|
|
|
/// A [`Resource`] that manages "rust source files" in a virtual in memory [`Dir`], which is intended
|
|
/// to be shared with a [`MemoryAssetReader`].
|
|
/// Generally this should not be interacted with directly. The [`embedded_asset`] will populate this.
|
|
///
|
|
/// [`embedded_asset`]: crate::embedded_asset
|
|
#[derive(Resource, Default)]
|
|
pub struct EmbeddedAssetRegistry {
|
|
dir: Dir,
|
|
#[cfg(feature = "embedded_watcher")]
|
|
root_paths: alloc::sync::Arc<
|
|
parking_lot::RwLock<bevy_platform::collections::HashMap<Box<Path>, PathBuf>>,
|
|
>,
|
|
}
|
|
|
|
impl EmbeddedAssetRegistry {
|
|
/// Inserts a new asset. `full_path` is the full path (as [`file`] would return for that file, if it was capable of
|
|
/// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded`
|
|
/// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]`
|
|
/// or a [`Vec<u8>`](alloc::vec::Vec).
|
|
#[cfg_attr(
|
|
not(feature = "embedded_watcher"),
|
|
expect(
|
|
unused_variables,
|
|
reason = "The `full_path` argument is not used when `embedded_watcher` is disabled."
|
|
)
|
|
)]
|
|
pub fn insert_asset(&self, full_path: PathBuf, asset_path: &Path, value: impl Into<Value>) {
|
|
#[cfg(feature = "embedded_watcher")]
|
|
self.root_paths
|
|
.write()
|
|
.insert(full_path.into(), asset_path.to_owned());
|
|
self.dir.insert_asset(asset_path, value);
|
|
}
|
|
|
|
/// Inserts new asset metadata. `full_path` is the full path (as [`file`] would return for that file, if it was capable of
|
|
/// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded`
|
|
/// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]`
|
|
/// or a [`Vec<u8>`](alloc::vec::Vec).
|
|
#[cfg_attr(
|
|
not(feature = "embedded_watcher"),
|
|
expect(
|
|
unused_variables,
|
|
reason = "The `full_path` argument is not used when `embedded_watcher` is disabled."
|
|
)
|
|
)]
|
|
pub fn insert_meta(&self, full_path: &Path, asset_path: &Path, value: impl Into<Value>) {
|
|
#[cfg(feature = "embedded_watcher")]
|
|
self.root_paths
|
|
.write()
|
|
.insert(full_path.into(), asset_path.to_owned());
|
|
self.dir.insert_meta(asset_path, value);
|
|
}
|
|
|
|
/// Removes an asset stored using `full_path` (the full path as [`file`] would return for that file, if it was capable of
|
|
/// running in a non-rust file). If no asset is stored with at `full_path` its a no-op.
|
|
/// It returning `Option` contains the originally stored `Data` or `None`.
|
|
pub fn remove_asset(&self, full_path: &Path) -> Option<super::memory::Data> {
|
|
self.dir.remove_asset(full_path)
|
|
}
|
|
|
|
/// Registers the [`EMBEDDED`] [`AssetSource`] with the given [`AssetSourceBuilders`].
|
|
pub fn register_source(&self, sources: &mut AssetSourceBuilders) {
|
|
let dir = self.dir.clone();
|
|
let processed_dir = self.dir.clone();
|
|
|
|
#[cfg_attr(
|
|
not(feature = "embedded_watcher"),
|
|
expect(
|
|
unused_mut,
|
|
reason = "Variable is only mutated when `embedded_watcher` feature is enabled."
|
|
)
|
|
)]
|
|
let mut source = AssetSource::build()
|
|
.with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() }))
|
|
.with_processed_reader(move || {
|
|
Box::new(MemoryAssetReader {
|
|
root: processed_dir.clone(),
|
|
})
|
|
})
|
|
// Note that we only add a processed watch warning because we don't want to warn
|
|
// noisily about embedded watching (which is niche) when users enable file watching.
|
|
.with_processed_watch_warning(
|
|
"Consider enabling the `embedded_watcher` cargo feature.",
|
|
);
|
|
|
|
#[cfg(feature = "embedded_watcher")]
|
|
{
|
|
let root_paths = self.root_paths.clone();
|
|
let dir = self.dir.clone();
|
|
let processed_root_paths = self.root_paths.clone();
|
|
let processed_dir = self.dir.clone();
|
|
source = source
|
|
.with_watcher(move |sender| {
|
|
Some(Box::new(EmbeddedWatcher::new(
|
|
dir.clone(),
|
|
root_paths.clone(),
|
|
sender,
|
|
core::time::Duration::from_millis(300),
|
|
)))
|
|
})
|
|
.with_processed_watcher(move |sender| {
|
|
Some(Box::new(EmbeddedWatcher::new(
|
|
processed_dir.clone(),
|
|
processed_root_paths.clone(),
|
|
sender,
|
|
core::time::Duration::from_millis(300),
|
|
)))
|
|
});
|
|
}
|
|
sources.insert(EMBEDDED, source);
|
|
}
|
|
}
|
|
|
|
/// Returns the [`Path`] for a given `embedded` asset.
|
|
/// This is used internally by [`embedded_asset`] and can be used to get a [`Path`]
|
|
/// that matches the [`AssetPath`](crate::AssetPath) used by that asset.
|
|
///
|
|
/// [`embedded_asset`]: crate::embedded_asset
|
|
#[macro_export]
|
|
macro_rules! embedded_path {
|
|
($path_str: expr) => {{
|
|
embedded_path!("src", $path_str)
|
|
}};
|
|
|
|
($source_path: expr, $path_str: expr) => {{
|
|
let crate_name = module_path!().split(':').next().unwrap();
|
|
$crate::io::embedded::_embedded_asset_path(
|
|
crate_name,
|
|
$source_path.as_ref(),
|
|
file!().as_ref(),
|
|
$path_str.as_ref(),
|
|
)
|
|
}};
|
|
}
|
|
|
|
/// Implementation detail of `embedded_path`, do not use this!
|
|
///
|
|
/// Returns an embedded asset path, given:
|
|
/// - `crate_name`: name of the crate where the asset is embedded
|
|
/// - `src_prefix`: path prefix of the crate's source directory, relative to the workspace root
|
|
/// - `file_path`: `std::file!()` path of the source file where `embedded_path!` is called
|
|
/// - `asset_path`: path of the embedded asset relative to `file_path`
|
|
#[doc(hidden)]
|
|
pub fn _embedded_asset_path(
|
|
crate_name: &str,
|
|
src_prefix: &Path,
|
|
file_path: &Path,
|
|
asset_path: &Path,
|
|
) -> PathBuf {
|
|
let mut maybe_parent = file_path.parent();
|
|
let after_src = loop {
|
|
let Some(parent) = maybe_parent else {
|
|
panic!("Failed to find src_prefix {src_prefix:?} in {file_path:?}")
|
|
};
|
|
if parent.ends_with(src_prefix) {
|
|
break file_path.strip_prefix(parent).unwrap();
|
|
}
|
|
maybe_parent = parent.parent();
|
|
};
|
|
let asset_path = after_src.parent().unwrap().join(asset_path);
|
|
Path::new(crate_name).join(asset_path)
|
|
}
|
|
|
|
/// Creates a new `embedded` asset by embedding the bytes of the given path into the current binary
|
|
/// and registering those bytes with the `embedded` [`AssetSource`].
|
|
///
|
|
/// This accepts the current [`App`](bevy_app::App) as the first parameter and a path `&str` (relative to the current file) as the second.
|
|
///
|
|
/// By default this will generate an [`AssetPath`] using the following rules:
|
|
///
|
|
/// 1. Search for the first `$crate_name/src/` in the path and trim to the path past that point.
|
|
/// 2. Re-add the current `$crate_name` to the front of the path
|
|
///
|
|
/// For example, consider the following file structure in the theoretical `bevy_rock` crate, which provides a Bevy [`Plugin`](bevy_app::Plugin)
|
|
/// that renders fancy rocks for scenes.
|
|
///
|
|
/// ```text
|
|
/// bevy_rock
|
|
/// ├── src
|
|
/// │ ├── render
|
|
/// │ │ ├── rock.wgsl
|
|
/// │ │ └── mod.rs
|
|
/// │ └── lib.rs
|
|
/// └── Cargo.toml
|
|
/// ```
|
|
///
|
|
/// `rock.wgsl` is a WGSL shader asset that the `bevy_rock` plugin author wants to bundle with their crate. They invoke the following
|
|
/// in `bevy_rock/src/render/mod.rs`:
|
|
///
|
|
/// `embedded_asset!(app, "rock.wgsl")`
|
|
///
|
|
/// `rock.wgsl` can now be loaded by the [`AssetServer`](crate::AssetServer) with the following path:
|
|
///
|
|
/// ```no_run
|
|
/// # use bevy_asset::{Asset, AssetServer};
|
|
/// # use bevy_reflect::TypePath;
|
|
/// # let asset_server: AssetServer = panic!();
|
|
/// # #[derive(Asset, TypePath)]
|
|
/// # struct Shader;
|
|
/// let shader = asset_server.load::<Shader>("embedded://bevy_rock/render/rock.wgsl");
|
|
/// ```
|
|
///
|
|
/// Some things to note in the path:
|
|
/// 1. The non-default `embedded://` [`AssetSource`]
|
|
/// 2. `src` is trimmed from the path
|
|
///
|
|
/// The default behavior also works for cargo workspaces. Pretend the `bevy_rock` crate now exists in a larger workspace in
|
|
/// `$SOME_WORKSPACE/crates/bevy_rock`. The asset path would remain the same, because [`embedded_asset`] searches for the
|
|
/// _first instance_ of `bevy_rock/src` in the path.
|
|
///
|
|
/// For most "standard crate structures" the default works just fine. But for some niche cases (such as cargo examples),
|
|
/// the `src` path will not be present. You can override this behavior by adding it as the second argument to [`embedded_asset`]:
|
|
///
|
|
/// `embedded_asset!(app, "/examples/rock_stuff/", "rock.wgsl")`
|
|
///
|
|
/// When there are three arguments, the second argument will replace the default `/src/` value. Note that these two are
|
|
/// equivalent:
|
|
///
|
|
/// `embedded_asset!(app, "rock.wgsl")`
|
|
/// `embedded_asset!(app, "/src/", "rock.wgsl")`
|
|
///
|
|
/// This macro uses the [`include_bytes`] macro internally and _will not_ reallocate the bytes.
|
|
/// Generally the [`AssetPath`] generated will be predictable, but if your asset isn't
|
|
/// available for some reason, you can use the [`embedded_path`] macro to debug.
|
|
///
|
|
/// Hot-reloading `embedded` assets is supported. Just enable the `embedded_watcher` cargo feature.
|
|
///
|
|
/// [`AssetPath`]: crate::AssetPath
|
|
/// [`embedded_asset`]: crate::embedded_asset
|
|
/// [`embedded_path`]: crate::embedded_path
|
|
#[macro_export]
|
|
macro_rules! embedded_asset {
|
|
($app: ident, $path: expr) => {{
|
|
$crate::embedded_asset!($app, "src", $path)
|
|
}};
|
|
|
|
($app: ident, $source_path: expr, $path: expr) => {{
|
|
let mut embedded = $app
|
|
.world_mut()
|
|
.resource_mut::<$crate::io::embedded::EmbeddedAssetRegistry>();
|
|
let path = $crate::embedded_path!($source_path, $path);
|
|
let watched_path = $crate::io::embedded::watched_path(file!(), $path);
|
|
embedded.insert_asset(watched_path, &path, include_bytes!($path));
|
|
}};
|
|
}
|
|
|
|
/// Returns the path used by the watcher.
|
|
#[doc(hidden)]
|
|
#[cfg(feature = "embedded_watcher")]
|
|
pub fn watched_path(source_file_path: &'static str, asset_path: &'static str) -> PathBuf {
|
|
PathBuf::from(source_file_path)
|
|
.parent()
|
|
.unwrap()
|
|
.join(asset_path)
|
|
}
|
|
|
|
/// Returns an empty PathBuf.
|
|
#[doc(hidden)]
|
|
#[cfg(not(feature = "embedded_watcher"))]
|
|
pub fn watched_path(_source_file_path: &'static str, _asset_path: &'static str) -> PathBuf {
|
|
PathBuf::from("")
|
|
}
|
|
|
|
/// Loads an "internal" asset by embedding the string stored in the given `path_str` and associates it with the given handle.
|
|
#[macro_export]
|
|
macro_rules! load_internal_asset {
|
|
($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{
|
|
let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
|
|
assets.insert($handle.id(), ($loader)(
|
|
include_str!($path_str),
|
|
std::path::Path::new(file!())
|
|
.parent()
|
|
.unwrap()
|
|
.join($path_str)
|
|
.to_string_lossy()
|
|
));
|
|
}};
|
|
// we can't support params without variadic arguments, so internal assets with additional params can't be hot-reloaded
|
|
($app: ident, $handle: ident, $path_str: expr, $loader: expr $(, $param:expr)+) => {{
|
|
let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
|
|
assets.insert($handle.id(), ($loader)(
|
|
include_str!($path_str),
|
|
std::path::Path::new(file!())
|
|
.parent()
|
|
.unwrap()
|
|
.join($path_str)
|
|
.to_string_lossy(),
|
|
$($param),+
|
|
));
|
|
}};
|
|
}
|
|
|
|
/// Loads an "internal" binary asset by embedding the bytes stored in the given `path_str` and associates it with the given handle.
|
|
#[macro_export]
|
|
macro_rules! load_internal_binary_asset {
|
|
($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{
|
|
let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
|
|
assets.insert(
|
|
$handle.id(),
|
|
($loader)(
|
|
include_bytes!($path_str).as_ref(),
|
|
std::path::Path::new(file!())
|
|
.parent()
|
|
.unwrap()
|
|
.join($path_str)
|
|
.to_string_lossy()
|
|
.into(),
|
|
),
|
|
);
|
|
}};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{EmbeddedAssetRegistry, _embedded_asset_path};
|
|
use std::path::Path;
|
|
|
|
// Relative paths show up if this macro is being invoked by a local crate.
|
|
// In this case we know the relative path is a sub- path of the workspace
|
|
// root.
|
|
|
|
#[test]
|
|
fn embedded_asset_path_from_local_crate() {
|
|
let asset_path = _embedded_asset_path(
|
|
"my_crate",
|
|
"src".as_ref(),
|
|
"src/foo/plugin.rs".as_ref(),
|
|
"the/asset.png".as_ref(),
|
|
);
|
|
assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
|
|
}
|
|
|
|
// A blank src_path removes the embedded's file path altogether only the
|
|
// asset path remains.
|
|
#[test]
|
|
fn embedded_asset_path_from_local_crate_blank_src_path_questionable() {
|
|
let asset_path = _embedded_asset_path(
|
|
"my_crate",
|
|
"".as_ref(),
|
|
"src/foo/some/deep/path/plugin.rs".as_ref(),
|
|
"the/asset.png".as_ref(),
|
|
);
|
|
assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "Failed to find src_prefix \"NOT-THERE\" in \"src")]
|
|
fn embedded_asset_path_from_local_crate_bad_src() {
|
|
let _asset_path = _embedded_asset_path(
|
|
"my_crate",
|
|
"NOT-THERE".as_ref(),
|
|
"src/foo/plugin.rs".as_ref(),
|
|
"the/asset.png".as_ref(),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn embedded_asset_path_from_local_example_crate() {
|
|
let asset_path = _embedded_asset_path(
|
|
"example_name",
|
|
"examples/foo".as_ref(),
|
|
"examples/foo/example.rs".as_ref(),
|
|
"the/asset.png".as_ref(),
|
|
);
|
|
assert_eq!(asset_path, Path::new("example_name/the/asset.png"));
|
|
}
|
|
|
|
// Absolute paths show up if this macro is being invoked by an external
|
|
// dependency, e.g. one that's being checked out from a crates repo or git.
|
|
#[test]
|
|
fn embedded_asset_path_from_external_crate() {
|
|
let asset_path = _embedded_asset_path(
|
|
"my_crate",
|
|
"src".as_ref(),
|
|
"/path/to/crate/src/foo/plugin.rs".as_ref(),
|
|
"the/asset.png".as_ref(),
|
|
);
|
|
assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
|
|
}
|
|
|
|
#[test]
|
|
fn embedded_asset_path_from_external_crate_root_src_path() {
|
|
let asset_path = _embedded_asset_path(
|
|
"my_crate",
|
|
"/path/to/crate/src".as_ref(),
|
|
"/path/to/crate/src/foo/plugin.rs".as_ref(),
|
|
"the/asset.png".as_ref(),
|
|
);
|
|
assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
|
|
}
|
|
|
|
// Although extraneous slashes are permitted at the end, e.g., "src////",
|
|
// one or more slashes at the beginning are not.
|
|
#[test]
|
|
#[should_panic(expected = "Failed to find src_prefix \"////src\" in")]
|
|
fn embedded_asset_path_from_external_crate_extraneous_beginning_slashes() {
|
|
let asset_path = _embedded_asset_path(
|
|
"my_crate",
|
|
"////src".as_ref(),
|
|
"/path/to/crate/src/foo/plugin.rs".as_ref(),
|
|
"the/asset.png".as_ref(),
|
|
);
|
|
assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
|
|
}
|
|
|
|
// We don't handle this edge case because it is ambiguous with the
|
|
// information currently available to the embedded_path macro.
|
|
#[test]
|
|
fn embedded_asset_path_from_external_crate_is_ambiguous() {
|
|
let asset_path = _embedded_asset_path(
|
|
"my_crate",
|
|
"src".as_ref(),
|
|
"/path/to/.cargo/registry/src/crate/src/src/plugin.rs".as_ref(),
|
|
"the/asset.png".as_ref(),
|
|
);
|
|
// Really, should be "my_crate/src/the/asset.png"
|
|
assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));
|
|
}
|
|
|
|
#[test]
|
|
fn remove_embedded_asset() {
|
|
let reg = EmbeddedAssetRegistry::default();
|
|
let path = std::path::PathBuf::from("a/b/asset.png");
|
|
reg.insert_asset(path.clone(), &path, &[]);
|
|
assert!(reg.dir.get_asset(&path).is_some());
|
|
assert!(reg.remove_asset(&path).is_some());
|
|
assert!(reg.dir.get_asset(&path).is_none());
|
|
assert!(reg.remove_asset(&path).is_none());
|
|
}
|
|
}
|