diff --git a/Cargo.toml b/Cargo.toml index 70e9a4ca7cb15..fb9e44f8b093f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1099,6 +1099,17 @@ description = "Implements a custom AssetReader" category = "Assets" wasm = true +[[example]] +name = "embedded_asset" +path = "examples/asset/embedded_asset.rs" +doc-scrape-examples = true + +[package.metadata.example.embedded_asset] +name = "Embedded Asset" +description = "Embed an asset in the application binary and load it" +category = "Assets" +wasm = true + [[example]] name = "hot_asset_reloading" path = "examples/asset/hot_asset_reloading.rs" diff --git a/crates/bevy_asset/src/io/embedded/mod.rs b/crates/bevy_asset/src/io/embedded/mod.rs index e5470cd3d5c3f..13b0ec423b9c8 100644 --- a/crates/bevy_asset/src/io/embedded/mod.rs +++ b/crates/bevy_asset/src/io/embedded/mod.rs @@ -104,20 +104,48 @@ impl EmbeddedAssetRegistry { #[macro_export] macro_rules! embedded_path { ($path_str: expr) => {{ - embedded_path!("/src/", $path_str) + embedded_path!("src", $path_str) }}; ($source_path: expr, $path_str: expr) => {{ let crate_name = module_path!().split(':').next().unwrap(); - let after_src = file!().split($source_path).nth(1).unwrap(); - let file_path = std::path::Path::new(after_src) - .parent() - .unwrap() - .join($path_str); - std::path::Path::new(crate_name).join(file_path) + $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`]. /// @@ -186,7 +214,7 @@ macro_rules! embedded_path { #[macro_export] macro_rules! embedded_asset { ($app: ident, $path: expr) => {{ - embedded_asset!($app, "/src/", $path) + embedded_asset!($app, "src", $path) }}; ($app: ident, $source_path: expr, $path: expr) => {{ @@ -250,3 +278,111 @@ macro_rules! load_internal_binary_asset { ); }}; } + +#[cfg(test)] +mod tests { + use super::_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")); + } +} diff --git a/examples/README.md b/examples/README.md index db73c982f060c..39e13a8647fa4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -185,6 +185,7 @@ Example | Description [Asset Processing](../examples/asset/processing/asset_processing.rs) | Demonstrates how to process and load custom assets [Custom Asset](../examples/asset/custom_asset.rs) | Implements a custom asset loader [Custom Asset IO](../examples/asset/custom_asset_reader.rs) | Implements a custom AssetReader +[Embedded Asset](../examples/asset/embedded_asset.rs) | Embed an asset in the application binary and load it [Hot Reloading of Assets](../examples/asset/hot_asset_reloading.rs) | Demonstrates automatic reloading of assets when modified on disk ## Async Tasks diff --git a/examples/asset/bevy_pixel_light.png b/examples/asset/bevy_pixel_light.png new file mode 100644 index 0000000000000..f6225fe25eabb Binary files /dev/null and b/examples/asset/bevy_pixel_light.png differ diff --git a/examples/asset/embedded_asset.rs b/examples/asset/embedded_asset.rs new file mode 100644 index 0000000000000..af865b6007f5e --- /dev/null +++ b/examples/asset/embedded_asset.rs @@ -0,0 +1,53 @@ +//! Example of loading an embedded asset. + +use bevy::asset::{embedded_asset, io::AssetSourceId, AssetPath}; +use bevy::prelude::*; +use std::path::Path; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, EmbeddedAssetPlugin)) + .add_systems(Startup, setup) + .run(); +} + +struct EmbeddedAssetPlugin; + +impl Plugin for EmbeddedAssetPlugin { + fn build(&self, app: &mut App) { + // We get to choose some prefix relative to the workspace root which + // will be ignored in "embedded://" asset paths. + let omit_prefix = "examples/asset"; + // Path to asset must be relative to this file, because that's how + // include_bytes! works. + embedded_asset!(app, omit_prefix, "bevy_pixel_light.png"); + } +} + +fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2dBundle::default()); + + // Each example is its own crate (with name from [[example]] in Cargo.toml). + let crate_name = "embedded_asset"; + + // The actual file path relative to workspace root is + // "examples/asset/bevy_pixel_light.png". + // + // We omit the "examples/asset" from the embedded_asset! call and replace it + // with the crate name. + let path = Path::new(crate_name).join("bevy_pixel_light.png"); + let source = AssetSourceId::from("embedded"); + let asset_path = AssetPath::from_path(&path).with_source(source); + + // You could also parse this URL-like string representation for the asset + // path. + assert_eq!( + asset_path, + "embedded://embedded_asset/bevy_pixel_light.png".into() + ); + + commands.spawn(SpriteBundle { + texture: asset_server.load(asset_path), + ..default() + }); +}