Skip to content

Commit 50a660f

Browse files
committed
Restore remote asset URLs in stage dumps
1 parent a6060a9 commit 50a660f

5 files changed

Lines changed: 79 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Added
2+
^^^^^
3+
4+
* Added :func:`~isaaclab.utils.assets.unmirror_file_path`, which maps a locally cached asset copy
5+
written by :func:`~isaaclab.utils.assets.retrieve_file_path` back to the URL it was downloaded
6+
from. Exports of a stage that references cached copies can use it to name the source assets
7+
instead of machine-specific cache paths.

source/isaaclab/isaaclab/utils/assets.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ def _resolve_asset_root() -> str:
118118

119119
_GIT_SSH_RE = re.compile(r"^[^@/:]+@[^:]+:.+")
120120

121+
_MIRROR_URL_SCHEMES = frozenset({"http", "https", "omniverse"})
122+
"""URL schemes whose assets are cached locally, and which therefore start a cache layout."""
123+
124+
_MIRROR_NETLOC_PORT_RE = re.compile(r"^(.+)_(\d+)$")
125+
121126

122127
def retrieve_git_asset_path(
123128
git_path: str, local_path: str, cache_dir: str | None = None, force_update: bool = False
@@ -305,6 +310,33 @@ def _mirror_path(url: str, download_dir: str) -> str:
305310
return os.path.join(download_dir, parsed.scheme, netloc, *parsed.path.lstrip("/").split("/"))
306311

307312

313+
def unmirror_file_path(path: str) -> str:
314+
"""Reverses :func:`retrieve_file_path` caching, mapping a cached copy back to its source URL.
315+
316+
A remote asset is cached under ``<download_dir>/<scheme>/<host>/<path>``, and stages reference
317+
that cached copy rather than the URL it came from. Exports of such a stage therefore carry
318+
absolute paths that only resolve on the machine holding the cache. This recovers the URL so an
319+
export can name the source asset instead.
320+
321+
Args:
322+
path: Local filesystem path, typically an asset path read from a USD layer.
323+
324+
Returns:
325+
The URL the path was cached from, or ``""`` when it does not lie inside a cache layout.
326+
"""
327+
parts = path.replace(os.sep, "/").split("/")
328+
# the last two components are the host and at least one path component, so a scheme found
329+
# there cannot be the start of a cache layout
330+
for index, part in enumerate(parts[:-2]):
331+
if part.lower() not in _MIRROR_URL_SCHEMES:
332+
continue
333+
netloc, *remainder = parts[index + 1 :]
334+
# ``_mirror_path`` writes a port separator as '_', which is not valid in a host name
335+
netloc = _MIRROR_NETLOC_PORT_RE.sub(r"\1:\2", netloc)
336+
return f"{part.lower()}://{netloc}/{'/'.join(remainder)}"
337+
return ""
338+
339+
308340
def _remote_fingerprint(url: str) -> dict | None:
309341
"""Provider metadata identifying the revision of ``url`` the server currently holds.
310342

source/isaaclab/test/utils/test_assets.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,28 @@ def test_using_local_copies_is_announced_once_per_cache_directory(asset_cache, m
419419
assert {_REMOTE_URL, other_url} == {record.args[0] for record in per_asset}
420420

421421

422+
@pytest.mark.parametrize(
423+
"url",
424+
[
425+
"https://example.com/Assets/Isaac/6.0/Isaac/Props/Blocks/DexCube/Materials/dex_cube_mod.png",
426+
"http://example.com/Assets/example.usd",
427+
"omniverse://nucleus.example-lab.com:3009/Assets/example.usd",
428+
],
429+
)
430+
def test_unmirror_file_path_recovers_the_url_a_copy_was_cached_from(tmp_path, url):
431+
"""Test a cached copy names the asset it came from, so exports do not carry local paths."""
432+
assert assets_utils.unmirror_file_path(assets_utils._mirror_path(url, str(tmp_path))) == url
433+
434+
435+
@pytest.mark.parametrize(
436+
"path",
437+
["/home/user/assets/example.usd", "Materials/dex_cube_mod.png", "OmniPBR.mdl", ""],
438+
)
439+
def test_unmirror_file_path_leaves_paths_outside_the_cache_unclaimed(path):
440+
"""Test a locally authored asset path is not mistaken for a cached remote copy."""
441+
assert assets_utils.unmirror_file_path(path) == ""
442+
443+
422444
def test_newton_asset_dir_uses_environment_override(tmp_path, monkeypatch):
423445
"""Test that the Newton asset directory is defined from the environment."""
424446
repo_dir = tmp_path / "newton-assets"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Rendering test stage dumps rewrite cached asset paths back to their source URLs, so a stage saved
2+
through ``ISAAC_LAB_SAVE_STAGES`` no longer carries texture paths that only resolve on the machine
3+
that ran the test.

source/isaaclab_tasks/test/rendering_test_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,20 @@ def _sanitize_golden_stage_text(text: str) -> str:
510510
return text.rstrip("\n") + "\n"
511511

512512

513+
def _restore_remote_asset_paths(layer) -> None:
514+
"""Point cached asset paths in ``layer`` back at the URLs they were downloaded from.
515+
516+
Remote USD assets are referenced through a local cache copy, so flattening resolves the
517+
textures and materials they carry into absolute cache paths that exist only on the machine
518+
that ran the test. Locally authored paths are left untouched.
519+
"""
520+
from pxr import UsdUtils # noqa: PLC0415
521+
522+
from isaaclab.utils.assets import unmirror_file_path # noqa: PLC0415
523+
524+
UsdUtils.ModifyAssetPaths(layer, lambda asset_path: unmirror_file_path(asset_path) or asset_path)
525+
526+
513527
def maybe_save_stage(
514528
test_name: str,
515529
physics_backend: str,
@@ -551,6 +565,7 @@ def maybe_save_stage(
551565
flat_layer = opened_stage.Flatten()
552566
if flat_layer is None:
553567
pytest.fail(f"Could not flatten the saved stage at {stage_path}.")
568+
_restore_remote_asset_paths(flat_layer)
554569

555570
if out_dir:
556571
os.makedirs(out_dir, exist_ok=True)

0 commit comments

Comments
 (0)