Skip to content

Move away from std::Path in bevy_assets #19133

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

lielfr
Copy link

@lielfr lielfr commented May 9, 2025

Objective

Solution

  • changes AssetPath to use &str internally instead of keeping a Path
  • some external-facing methods still use either Path or PathBuf, please let me know if these should be changed as well

Testing

  • all existing tests seem to pass
  • added one test for PartialEq implementation

Copy link
Contributor

github-actions bot commented May 9, 2025

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

@alice-i-cecile alice-i-cecile requested review from cart and andriyDev May 9, 2025 00:41
@alice-i-cecile alice-i-cecile added A-Assets Load files from disk to use for things like images, models, and sounds C-Usability A targeted quality-of-life change that makes Bevy easier to use S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 9, 2025
@alice-i-cecile
Copy link
Member

some external-facing methods still use either Path or PathBuf, please let me know if these should be changed as well

My intuition is that these should generally be something like impl Into<AssetPath>, but I don't know this code very well at all.

@lielfr
Copy link
Author

lielfr commented May 9, 2025

some external-facing methods still use either Path or PathBuf, please let me know if these should be changed as well

My intuition is that these should generally be something like impl Into<AssetPath>, but I don't know this code very well at all.

let's wait for other reviewers then :)

Copy link
Contributor

@bushrat011899 bushrat011899 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love that you've taken this on! Just a couple of minor points mostly, but the biggest concern I have is we should probably add more tests that cover the operations we previously relied on Path for. e.g., parent(), path_components(), etc.

label: Option<CowArc<'a, str>>,
}

/// `PartialEq` needs to be derived manually for backwards compatibility.
/// As `path` used to be `std::path::Path`, equality was tricky with a trailing slash.
/// For example, "martin/stephan#dave" should be equal to "martin/stephan/#dave".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very interesting point to raise.
Future PR: Should we keep this kind of equality? martin/stephan#dave reads to me as the asset stephan with the label dave, while martin/stephan/#dave reads like an asset with no name but labelled as dave. I think we should consider changing semantics to make these degenerate states unrepresentable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My opinion is we should not support this behaviour. I am fine to leave this for another PR, but I would appreciate if you (@lielfr) opened the PR after this one lands. At least it would be a good place to discuss thoughts on this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove it, it will just require changing some of the tests to account for it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for not supporting the behaviour. However if we do make this change, two asset paths martin/stephan#dave and martin/stephan/#dave which compare as different would map to Paths which are equal and return the same file.

Given we're inventing our own platform independent version of paths, maybe we could always require trailing slashes for directories.

@bushrat011899
Copy link
Contributor

some external-facing methods still use either Path or PathBuf, please let me know if these should be changed as well

I was thinking about this myself. For now, I agree the path forward is impl Into<AssetPath>, since that is our canonical Path type. But, I also think there's room to consider a new type similar to AssetSourceId that is just for the path part of an AssetPath (I'm choosing the awful name AssetPathId). The reason is two-fold:

  1. Dyn-compatible traits can't use impl T in arguments, so they need a concrete type to refer to.
  2. Traits like AssetLoader don't need to know about the AssetSourceId part of an AssetPath. It's information that does not matter to them.

So in the longer term, I would propose something like:

pub struct AssetPathId<'a>(CowArc<'a, str>);

Then AssetPath would be updated to store AssetPathId for path, and all methods currently working with Path would be swapped to AssetPathId, impl Into<AssetPathId>, AssetPath, or impl Into<AssetPath> based on their dyn-compatibility and information requirements.

label: Option<CowArc<'a, str>>,
}

/// `PartialEq` needs to be derived manually for backwards compatibility.
/// As `path` used to be `std::path::Path`, equality was tricky with a trailing slash.
/// For example, "martin/stephan#dave" should be equal to "martin/stephan/#dave".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My opinion is we should not support this behaviour. I am fine to leave this for another PR, but I would appreciate if you (@lielfr) opened the PR after this one lands. At least it would be a good place to discuss thoughts on this.

sender: Sender<AssetSourceEvent>,
debounce_wait_time: Duration,
) -> Result<Self, notify::Error> {
let root = normalize_path(&path).canonicalize()?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this canonicalize is important here. It makes paths with dots work correctly "../../my_assets_dir". Relevant PR: #18345 (hey look its me! I forgot all about this lol)

Should FileWatcher even be dealing with asset paths? This seems like a case where a file system path actually does make sense? I'm not sure though.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar enough (yet?) with the codebase to decide, so I'd love some guidance here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would have to roll out own version of canonicalize to maintain being able to use ".." for relative paths

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into this a bit more, and I'm doubling down: I think FileWatcher should continue to deal with Path directly. I think the main thing we want to replace is the fact that our asset paths (as in, the ones that we pass into asset_server.load()) include some platform specific nonsense. That does not mean that our entire stack needs to deal only with strings - at some point, we need to convert that asset path into a concrete file path, so a Path instance.

So coming at it from the other angle, FileWatcher should be dealing with concrete paths because it is listening for real file system events for a particular directory. So I think we should revert what's happening in this file_watcher mod.

Copy link
Contributor

@viridia viridia Jun 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plan that cart and I discussed a couple of years ago:

  • Asset server will certainly use FilePaths internally, for environments that have filesystems. For web environments, FilePaths may not be needed at all.
  • However, the semantics of FilePaths should not leak to the external asset server API. There should be a clear distinction between file paths and asset paths. In general, the upper layer uses asset paths, and the lower layer uses file paths.
  • Path manipulations on asset paths (concatenation, parent, etc) should use custom methods written for Bevy (these shouldn't be difficult, it's just string manipulation). So for example AssetPath::resolve shouldn't use FilePath methods.
  • Path manipulations on file paths should use FilePath methods - so for example adding the asset directory base path to the asset path would be a FilePath method.

@@ -255,7 +288,7 @@ impl<'a> AssetPath<'a> {
/// Gets the path to the asset in the "virtual filesystem".
#[inline]
pub fn path(&self) -> &Path {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be returning a str here instead? The comment says "path to the asset in the virtual filesystem", which implies we shouldn't be returning a native OS path.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's possible but will require more extensive changes throughout bevy_asset. Is it ok to do this in the scope of this PR, or is it too big?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove this in this PR. It will be more changes, but it will ensure we aren't accidentally converting to paths somewhere.

Copy link
Contributor

@mcobzarenco mcobzarenco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking on this. As far as I can tell, this PR still leaves Paths in the public API, I imagine the final version to use String/str everywhere. But splitting into multiple PRs may be worthwhile.

In addition to the other reviews above, my main comment is to avoid the lossy conversions -- changing the public APIs to take Strings in the same PR would avoid having to do the conversions. I would also love to see tests for std functions for path manipulation we replaced.

@lielfr
Copy link
Author

lielfr commented May 9, 2025

I would also love to see tests for std functions for path manipulation we replaced.

which tests? I saw there are already some that cover parent and some other manipulations

@lielfr lielfr force-pushed the asset-path-nostd branch from 8c5f81c to 97ba16a Compare May 9, 2025 22:18
@lielfr
Copy link
Author

lielfr commented May 9, 2025

the force-push was a result of a rebase from upstream

Copy link
Contributor

@mcobzarenco mcobzarenco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the changes! I think I meant testing the new equality behaviour, but I now see there is one test for it.

I had a few other questions/thoughts, given we're essentially rolling our own platform independent version of paths, does it make sense to invest in some niceties for manipulating different parts? Not in this PR necessarily, although I think we probably have to roll our own canonicalize in this PR as @andriyDev pointed out.

sender: Sender<AssetSourceEvent>,
debounce_wait_time: Duration,
) -> Result<Self, notify::Error> {
let root = normalize_path(&path).canonicalize()?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would have to roll out own version of canonicalize to maintain being able to use ".." for relative paths

label: Option<CowArc<'a, str>>,
}

/// `PartialEq` needs to be derived manually for backwards compatibility.
/// As `path` used to be `std::path::Path`, equality was tricky with a trailing slash.
/// For example, "martin/stephan#dave" should be equal to "martin/stephan/#dave".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for not supporting the behaviour. However if we do make this change, two asset paths martin/stephan#dave and martin/stephan/#dave which compare as different would map to Paths which are equal and return the same file.

Given we're inventing our own platform independent version of paths, maybe we could always require trailing slashes for directories.

return true;
}
let mut simplified = Vec::new();
for component in self.path_components() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be more readable and potentially more reusable path parsing if we roll our own version of path components with an enum?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's possible, but I'm not sure about this. Do the other maintainers also agree?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion here, leaning to no for simplicity?

@lielfr lielfr force-pushed the asset-path-nostd branch from 006f751 to 2a4df37 Compare May 11, 2025 00:11
@lielfr
Copy link
Author

lielfr commented May 11, 2025

regarding implementing our own canonicalize, does it also need to consider symlinks? are these even possible in AssetPath?

I assume it does need to support . and .., and convert relative paths to absolute paths (or, are there absolute paths here at all?)

Regarding PartialEq and the possibility of requiring trailing slashes, does that need to be an unrecoverable error (panic)?

@viridia
Copy link
Contributor

viridia commented May 16, 2025

See also this comment by cart from a couple years ago: #9528 (comment)

Basically he and I agreed that we should get rid of Path; it has more features than we want, and some of those features produce unexpected and inappropriate behavior when used as an asset path. (Example, asset paths with drive letters should not be supported). Unfortunately, this work never got done.

sender: Sender<AssetSourceEvent>,
debounce_wait_time: Duration,
) -> Result<Self, notify::Error> {
let root = normalize_path(&path).canonicalize()?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into this a bit more, and I'm doubling down: I think FileWatcher should continue to deal with Path directly. I think the main thing we want to replace is the fact that our asset paths (as in, the ones that we pass into asset_server.load()) include some platform specific nonsense. That does not mean that our entire stack needs to deal only with strings - at some point, we need to convert that asset path into a concrete file path, so a Path instance.

So coming at it from the other angle, FileWatcher should be dealing with concrete paths because it is listening for real file system events for a particular directory. So I think we should revert what's happening in this file_watcher mod.

@@ -255,7 +288,7 @@ impl<'a> AssetPath<'a> {
/// Gets the path to the asset in the "virtual filesystem".
#[inline]
pub fn path(&self) -> &Path {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove this in this PR. It will be more changes, but it will ensure we aren't accidentally converting to paths somewhere.

return true;
}
let mut simplified = Vec::new();
for component in self.path_components() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion here, leaning to no for simplicity?

pub(crate) fn normalize_path(path: &Path) -> PathBuf {
let mut result_path = PathBuf::new();
for elt in path.iter() {
pub(crate) fn normalize_path<'b>(path: &'b [&'b str]) -> Vec<&'b str> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something... It seems like we don't normalize our asset paths today. I think that means if you load "asset.txt" and "blah/../asset.txt", they are treated as different assets.

Doesn't need to be addressed in this PR, just realized this grossness.

@lielfr
Copy link
Author

lielfr commented Jun 8, 2025

sorry for the delay.
Regarding file_watcher, I tried to change normalize_path to be generic, like this:

pub(crate) fn normalize_path<'b, P>(path: &'b [P]) -> Vec<P>
where
    P: Into<&'b str> + Copy,
{
    let mut result_path = Vec::new();
    for &elt in path {
        if elt.into() == "." {
            // Skip
        } else if elt.into() == ".." {
            if result_path.is_empty() {
                // Preserve ".." if insufficient matches (per RFC 1808).
                result_path.push(elt);
            } else {
                result_path.pop();
            }
        } else {
            result_path.push(elt);
        }
    }
    result_path
}

but it doesn't seem to work. Will it be ok to create a separate function for PathBuf? or am I missing something?

@lielfr lielfr force-pushed the asset-path-nostd branch from 2a4df37 to 643a1aa Compare June 8, 2025 20:42
@lielfr
Copy link
Author

lielfr commented Jun 8, 2025

I tried to change the readers to use AssetPath instead of Path, but so far have not found a way out of lifetime hell

@lielfr lielfr force-pushed the asset-path-nostd branch from 0ee520c to 27e9bde Compare June 11, 2025 23:08
@lielfr
Copy link
Author

lielfr commented Jun 11, 2025

rebased from main again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Assets Load files from disk to use for things like images, models, and sounds C-Usability A targeted quality-of-life change that makes Bevy easier to use S-Needs-Review Needs reviewer attention (from anyone!) to move forward
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Move away from std::Path in bevy_assets
6 participants