Source code for unisim.backend.isaacsim.dependencies
"""Runtime discovery for the external IsaacSim 5.1 / IsaacLab worker."""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from unisim.backend.subprocess_ipc.runtime import WorkerRuntime, build_worker_environment
from unisim.optional import OptionalDependencyError
ENV_HOME = "UNISIM_ISAACSIM_HOME"
ENV_PYTHON = "UNISIM_ISAACSIM_PYTHON"
_LEGACY_ENV_HOME = "UNILAB_ISAACSIM_HOME"
_LEGACY_ENV_PYTHON = "UNILAB_ISAACSIM_PYTHON"
_SETUP_HINT = (
"Install the dedicated IsaacSim/IsaacLab worker environment with "
"scripts/tools/setup_isaacsim_env.sh (see the IsaacSim backend page in "
"docs/sphinx/source/*/2-user_guide/3-backends/5-isaacsim.md)."
)
[docs]
class IsaacSimDependencyError(OptionalDependencyError):
"""Raised with an actionable setup hint when the worker runtime is absent."""
[docs]
@dataclass(frozen=True)
class IsaacSimRuntime(WorkerRuntime):
"""Resolved on-disk locations for the Python 3.11 IsaacSim worker."""
isaaclab_source: Path | None = None
@property
def isaacsim_python(self) -> Path:
"""Compatibility alias used by worker payloads and diagnostics."""
return self.python
[docs]
def default_isaacsim_home() -> Path:
value = os.environ.get(ENV_HOME) or os.environ.get(_LEGACY_ENV_HOME)
return Path(value or "~/.cache/unisim/isaacsim").expanduser()
def _candidate_paths(home: Path) -> IsaacSimRuntime:
venv = home / "venv"
return IsaacSimRuntime(
python=venv / "bin" / "python",
package_path=venv / "lib" / "python3.11" / "site-packages",
lib_path=venv / "lib",
bin_path=venv / "bin",
isaaclab_source=home / "IsaacLab" / "source",
)
def _runtime_from_override(python: Path, home: Path) -> IsaacSimRuntime:
"""Build a runtime from an explicitly supplied interpreter.
``UNISIM_ISAACSIM_PYTHON`` is intentionally a complete escape hatch for
installations that do not use the helper script's directory layout. If
the interpreter looks like ``<venv>/bin/python``, we opportunistically
discover its sibling library/site-packages directories; otherwise those
paths remain unset and the interpreter's own environment is trusted.
"""
bin_path = python.parent if python.parent.is_dir() else None
venv_root = python.parent.parent if python.parent.name in {"bin", "Scripts"} else None
package_path: Path | None = None
lib_path: Path | None = None
if venv_root is not None:
candidate_lib = venv_root / "lib"
if candidate_lib.is_dir():
lib_path = candidate_lib
site_candidates = sorted(candidate_lib.glob("python*/site-packages"))
if site_candidates:
package_path = site_candidates[0]
source_candidates = [home / "IsaacLab" / "source"]
if venv_root is not None:
source_candidates.append(venv_root.parent / "IsaacLab" / "source")
isaaclab_source = next((path for path in source_candidates if path.is_dir()), None)
return IsaacSimRuntime(
python=python,
package_path=package_path,
lib_path=lib_path,
bin_path=bin_path,
isaaclab_source=isaaclab_source,
)
[docs]
def resolve_isaacsim_runtime(python_override: str | None = None) -> IsaacSimRuntime:
"""Resolve the interpreter and package/source layout without importing Kit."""
home = default_isaacsim_home()
candidate = _candidate_paths(home)
override = python_override or os.environ.get(ENV_PYTHON) or os.environ.get(_LEGACY_ENV_PYTHON)
if override:
python = Path(override).expanduser()
if not python.is_file():
raise IsaacSimDependencyError(
f"isaacsim backend could not find the Python 3.11 worker interpreter at "
f"{python}. {_SETUP_HINT}"
)
# An explicit interpreter may belong to a completely custom layout;
# do not reject it merely because the default helper-tree paths are
# absent. The worker will report import errors with its full
# traceback if that environment is not actually IsaacSim-capable.
return _runtime_from_override(python, home)
python = candidate.python
if not python.is_file():
raise IsaacSimDependencyError(
f"isaacsim backend could not find the Python 3.11 worker interpreter at "
f"{python}. {_SETUP_HINT}"
)
# The venv may use an editable IsaacLab install or a regular wheel. A
# package directory is still required as a cheap, import-free sanity check.
if candidate.package_path is not None and not candidate.package_path.is_dir():
raise IsaacSimDependencyError(
f"isaacsim backend could not find the worker site-packages directory at "
f"{candidate.package_path}. {_SETUP_HINT}"
)
if candidate.isaaclab_source is not None and not candidate.isaaclab_source.is_dir():
raise IsaacSimDependencyError(
f"isaacsim backend could not find the IsaacLab source directory at "
f"{candidate.isaaclab_source}. {_SETUP_HINT}"
)
if candidate.lib_path is not None and not candidate.lib_path.is_dir():
raise IsaacSimDependencyError(
f"isaacsim backend could not find the worker library directory at "
f"{candidate.lib_path}. {_SETUP_HINT}"
)
return IsaacSimRuntime(
python=python,
package_path=candidate.package_path,
lib_path=candidate.lib_path,
bin_path=candidate.bin_path,
isaaclab_source=candidate.isaaclab_source,
)
[docs]
def isaacsim_runtime_available() -> bool:
try:
resolve_isaacsim_runtime()
except IsaacSimDependencyError:
return False
return True
[docs]
def build_worker_env(runtime: IsaacSimRuntime) -> dict[str, str]:
"""Build an environment that prefers the pinned Kit/venv libraries."""
env = build_worker_environment(runtime)
env.setdefault("OMNI_KIT_ACCEPT_EULA", "1")
if runtime.isaaclab_source is not None:
old = env.get("PYTHONPATH", "")
source = str(runtime.isaaclab_source)
env["PYTHONPATH"] = f"{source}:{old}" if old else source
return env
__all__ = [
"ENV_HOME",
"ENV_PYTHON",
"IsaacSimDependencyError",
"IsaacSimRuntime",
"build_worker_env",
"default_isaacsim_home",
"isaacsim_runtime_available",
"resolve_isaacsim_runtime",
]