diff --git a/.github/workflows/install-ci.yml b/.github/workflows/install-ci.yml index 9a720575118c..69b509edba80 100644 --- a/.github/workflows/install-ci.yml +++ b/.github/workflows/install-ci.yml @@ -132,7 +132,7 @@ jobs: fi install-tests: - name: Installation Tests + name: Installation Tests (uv) needs: [changes] if: needs.changes.outputs.run_install_tests == 'true' runs-on: [self-hosted, gpu] @@ -140,7 +140,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 # v6 - - name: Run installation tests + - name: Run installation tests (uv) env: BASE_IMAGE: ${{ github.event_name == 'workflow_dispatch' && inputs.base_image || 'ubuntu:24.04' }} TEST_FILTER: ${{ github.event_name == 'workflow_dispatch' && inputs.test_filter || '' }} @@ -148,8 +148,36 @@ jobs: RUNNER_ARGS="--base-image $BASE_IMAGE" RUNNER_ARGS="$RUNNER_ARGS --results-dir ${{ github.workspace }}/results" RUNNER_ARGS="$RUNNER_ARGS --gpu" + RUNNER_ARGS="$RUNNER_ARGS --no-cache" - PYTEST_EXTRA_ARGS=(-sv) + PYTEST_EXTRA_ARGS=(-sv -m uv) + if [ -n "$TEST_FILTER" ]; then + PYTEST_EXTRA_ARGS+=(-k "$TEST_FILTER") + fi + + tools/run_install_ci.py docker $RUNNER_ARGS -- --tb=short "${PYTEST_EXTRA_ARGS[@]}" + + install-tests-conda: + name: Installation Tests (conda) + needs: [changes] + if: needs.changes.outputs.run_install_tests == 'true' + runs-on: [self-hosted, gpu] + timeout-minutes: 150 + steps: + - name: Checkout + uses: actions/checkout@v6 # v6 + - name: Run installation tests (conda) + env: + BASE_IMAGE: ${{ github.event_name == 'workflow_dispatch' && inputs.base_image || 'ubuntu:24.04' }} + TEST_FILTER: ${{ github.event_name == 'workflow_dispatch' && inputs.test_filter || '' }} + run: | + RUNNER_ARGS="--base-image $BASE_IMAGE" + RUNNER_ARGS="$RUNNER_ARGS --results-dir ${{ github.workspace }}/results-conda" + RUNNER_ARGS="$RUNNER_ARGS --gpu" + RUNNER_ARGS="$RUNNER_ARGS --conda" + RUNNER_ARGS="$RUNNER_ARGS --no-cache" + + PYTEST_EXTRA_ARGS=(-sv -m conda) if [ -n "$TEST_FILTER" ]; then PYTEST_EXTRA_ARGS+=(-k "$TEST_FILTER") fi diff --git a/docker/test/test_dockerfile_nonroot.py b/docker/test/test_dockerfile_nonroot.py index 2ba800d91365..aefbd566bba0 100644 --- a/docker/test/test_dockerfile_nonroot.py +++ b/docker/test/test_dockerfile_nonroot.py @@ -8,19 +8,31 @@ import pytest -DOCKER_DIR = Path(__file__).resolve().parent.parent -DOCKERFILES = sorted(DOCKER_DIR.glob("Dockerfile.*")) +REPO_ROOT = Path(__file__).resolve().parents[2] +DOCKER_DIR = REPO_ROOT / "docker" + +# Collect every Dockerfile.* from the entire repository tree. +DOCKERFILES = sorted(REPO_ROOT.glob("**/Dockerfile.*")) + ROOT_USERS = {"root", "0"} # Keep every Dockerfile in this map so new containers must make an explicit # runtime-user decision instead of silently escaping this regression test. +# Keys are Dockerfile *names* (unique across the repo); values are the +# expected final USER directive (None = not yet migrated, test skipped). DOCKERFILE_RUNTIME_USERS = { "Dockerfile.base": "isaaclab", "Dockerfile.curobo": "isaaclab", - "Dockerfile.installci": None, + "Dockerfile.installci": "isaaclab", + "Dockerfile.installci-conda": "isaaclab", "Dockerfile.ros2": "isaaclab", } -DOCKERFILES_CREATING_RUNTIME_USER = {"Dockerfile.base", "Dockerfile.curobo"} + +# Dockerfiles that are expected to *create* the non-root runtime user +# (i.e. contain groupadd/useradd/USER isaaclab). Inherited-user images +# (like Dockerfile.installci-conda which builds on top of Dockerfile.installci) +# are excluded here. +DOCKERFILES_CREATING_RUNTIME_USER = {"Dockerfile.base", "Dockerfile.curobo", "Dockerfile.installci"} USER_DIRECTIVE_RE = re.compile(r"^USER\s+(\S+)\s*$") @@ -42,6 +54,13 @@ def _final_user(dockerfile_path: Path) -> str | None: return users[-1] if users else None +def _find_dockerfile(name: str) -> Path: + """Return the path of the unique Dockerfile with the given name.""" + matches = [p for p in DOCKERFILES if p.name == name] + assert len(matches) == 1, f"Expected exactly one {name}, found: {matches}" + return matches[0] + + def test_all_dockerfiles_have_runtime_user_expectations(): expected_dockerfiles = set(DOCKERFILE_RUNTIME_USERS) actual_dockerfiles = {dockerfile.name for dockerfile in DOCKERFILES} @@ -63,7 +82,7 @@ def test_non_root_runtime_dockerfiles(dockerfile: Path): @pytest.mark.parametrize("dockerfile_name", sorted(DOCKERFILES_CREATING_RUNTIME_USER)) def test_dockerfile_creates_non_root_runtime_user(dockerfile_name: str): - dockerfile_text = (DOCKER_DIR / dockerfile_name).read_text(encoding="utf-8") + dockerfile_text = _find_dockerfile(dockerfile_name).read_text(encoding="utf-8") assert re.search(r"\bgroupadd\b.*--gid\s+1000\b.*\bisaaclab\b", dockerfile_text, re.DOTALL) assert re.search(r"\buseradd\b.*--uid\s+1000\b.*--gid\s+1000\b.*\bisaaclab\b", dockerfile_text, re.DOTALL) diff --git a/docs/source/setup/installation/include/src_build_isaaclab.rst b/docs/source/setup/installation/include/src_build_isaaclab.rst index 3d93e8f3452d..2335a968c7d9 100644 --- a/docs/source/setup/installation/include/src_build_isaaclab.rst +++ b/docs/source/setup/installation/include/src_build_isaaclab.rst @@ -16,8 +16,8 @@ Installation sudo apt install python3.12-dev libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev -- Run the install command that iterates over all the extensions in ``source`` directory and installs them - using pip (with ``--editable`` flag): +- Run the install command, which installs all core Isaac Lab packages and, by default, + the standard optional submodules and auto-selected extras: .. tab-set:: :sync-group: os @@ -37,33 +37,55 @@ Installation isaaclab.bat --install :: or "isaaclab.bat -i" - By default, the above will install **all** Isaac Lab submodules (under ``source/isaaclab``). - To install only specific Isaac Lab submodules, pass a comma-separated list of submodule names. The available - Isaac Lab submodules are: ``assets``, ``contrib``, ``mimic``, ``newton``, ``ov``, ``physx``, ``rl``, ``tasks``, - ``teleop``, ``visualizers``. Available RL frameworks are: ``rl_games``, ``rsl_rl``, ``sb3``, ``skrl``, ``robomimic``. + All core submodules are **always** installed regardless of what is passed to ``-i``. + The argument controls which optional submodules and extra feature dependencies to add on top. + The contrib and OV source packages (``isaaclab_contrib``, ``isaaclab_ov``, and + ``isaaclab_ovphysx``) are part of the core set so core modules and task configs can + import their config and preset classes without installing their heavy runtime dependencies. - For example, to install a small subset of submodules: + **Optional submodules**: - .. tab-set:: - :sync-group: os + .. list-table:: + :header-rows: 1 - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux + * - Token + - What it installs + * - ``mimic`` + - ``isaaclab_mimic`` (ipywidgets, h5py, imitation-learning tools) + * - ``teleop`` + - ``isaaclab_teleop`` (isaacteleop SDK, dex-retargeting — Linux x86 only) - .. code:: bash + **Optional extra feature sets** (heavy optional deps on top of core packages): - ./isaaclab.sh --install physx,newton,assets,rl[rsl_rl],tasks,ov # or "./isaaclab.sh -i physx,newton,assets,rl[rsl_rl],tasks,ov" + .. list-table:: + :header-rows: 1 - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows + * - Token + - What it installs + * - ``contrib[]`` + - Contrib runtime extras. Selector: ``rlinf``. + * - ``newton`` + - Newton physics library (``newton[sim]`` git dep) across ``isaaclab_newton``, ``isaaclab_physx``, ``isaaclab_visualizers`` + * - ``ov[]`` + - OV runtime wheels. Selectors: ``ovrtx``, ``ovphysx``. Use ``ov[all]`` for both. + * - ``rl[]`` + - RL framework extras on ``isaaclab_rl``. Selectors: ``rsl-rl``, ``skrl``, ``sb3``, ``rl-games``. Omit selector for all. + * - ``visualizer[]`` + - Visualizer backend extras. Selectors: ``rerun``, ``viser``, ``newton``, ``kit``. Omit selector for all. - .. code:: batch + **Special values**: + + - ``all`` — core + optional submodules (mimic, teleop) + auto extra features (newton, rl, visualizer) — default when ``-i`` is used with no argument + - ``none`` — core submodules only; no optional submodules, no extra feature dependencies - isaaclab.bat --install physx,newton,assets,rl[rsl_rl],tasks,ov :: or "isaaclab.bat -i physx,newton,assets,rl[rsl_rl],tasks,ov" + .. note:: - To install specific visualizer, pass a comma-separated list of supported visualizers, - or ``all`` to install all available options: ``newton``, ``rerun``, ``viser``, ``kit``. Note when following the - default installation, all visualizers are installed. + ``all`` installs the contrib and OV source packages, but not their heavy + dependency extras. Use ``contrib[rlinf]`` for rlinf + dependencies and ``ov[ovrtx]``, ``ov[ovphysx]``, or ``ov[all]`` for OV runtime + wheels. + + Examples: .. tab-set:: :sync-group: os @@ -73,14 +95,43 @@ Installation .. code:: bash - ./isaaclab.sh --install visualizers[rerun] # or "./isaaclab.sh -i visualizers[rerun]" + # Default: core + optional submodules + auto extras + ./isaaclab.sh -i + + # Newton physics + RSL-RL framework + ./isaaclab.sh -i 'newton,rl[rsl-rl]' + + # Newton + rerun visualizer + mimic + ./isaaclab.sh -i 'newton,visualizer[rerun],mimic' + + # OV source packages + OVRTX wheel + ./isaaclab.sh -i 'ov[ovrtx]' + + # Contrib rlinf dependencies + ./isaaclab.sh -i 'contrib[rlinf]' + + # Core only — no optional submodules, no extras + ./isaaclab.sh -i none .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows .. code:: batch - isaaclab.bat --install visualizers[rerun] :: or "isaaclab.bat -i visualizers[rerun]" + :: Default: core + optional submodules + auto extras + isaaclab.bat -i + + :: Newton physics + RSL-RL framework + isaaclab.bat -i "newton,rl[rsl-rl]" + + :: Newton + rerun visualizer + mimic + isaaclab.bat -i "newton,visualizer[rerun],mimic" + + :: OV source packages + OVRTX wheel + isaaclab.bat -i "ov[ovrtx]" + :: Contrib rlinf dependencies + isaaclab.bat -i "contrib[rlinf]" - Pass ``none`` to install only the core ``isaaclab`` package without any Isaac Lab submodules or RL frameworks. + :: Core only - no optional submodules, no extras + isaaclab.bat -i none diff --git a/docs/source/setup/installation/kitless_installation.rst b/docs/source/setup/installation/kitless_installation.rst index 19303b842b9d..644acbb07c1b 100644 --- a/docs/source/setup/installation/kitless_installation.rst +++ b/docs/source/setup/installation/kitless_installation.rst @@ -80,40 +80,45 @@ To install Isaac Sim, use the pip method described in :doc:`pip_installation`. Selective Install ----------------- -If you want a minimal environment, ``./isaaclab.sh -i`` accepts comma-separated -sub-package names: +``./isaaclab.sh -i`` always installs the full core package set (assets, tasks, physx, rl, +visualizers, …). The argument controls which **optional** submodules and extra feature +dependencies are added on top. + +**Optional submodules** (heavy — must be explicitly requested): .. list-table:: :header-rows: 1 - * - Option - - What it does - * - ``isaacsim`` - - Install Isaac Sim pip package + * - Token + - What it installs + * - ``mimic`` + - ``isaaclab_mimic`` — imitation-learning tools (ipywidgets, h5py) + * - ``teleop`` + - ``isaaclab_teleop`` — teleoperation SDK (Linux x86 only) + +**Optional extra feature sets** (heavy deps on top of always-installed core): + +.. list-table:: + :header-rows: 1 + + * - Token + - What it installs * - ``newton`` - - Install Newton physics + Newton visualizer - * - ``physx`` - - Install PhysX physics runtime + - Newton physics library (``newton[sim]``) + newton extras across ``isaaclab_newton``, ``isaaclab_physx``, ``isaaclab_visualizers`` + * - ``rl[]`` + - RL framework. Selectors: ``rsl-rl``, ``skrl``, ``sb3``, ``rl-games``. Omit selector for all. + * - ``visualizer[]`` + - Visualizer backend. Selectors: ``rerun``, ``viser``, ``newton``, ``kit``. Omit selector for all. + * - ``contrib[rlinf]`` + - rlinf extras (ray, diffusers, etc.) * - ``ov`` - - Install Omniverse renderer runtime - * - ``tasks`` - - Install built-in task environments - * - ``assets`` - - Install robot/object configurations - * - ``visualizers`` - - Install all visualizer backends - * - ``rsl_rl`` - - Install RSL-RL framework - * - ``skrl`` - - Install skrl framework - * - ``sb3`` - - Install Stable Baselines3 framework - * - ``rl_games`` - - Install rl_games framework - * - ``robomimic`` - - Install robomimic framework + - OVRTX + OVPhysX extras for Omniverse rendering + * - ``isaacsim`` + - Isaac Sim pip package + * - ``all`` + - Core + optional submodules (mimic, teleop) + auto extras (newton, rl, visualizer, ov). Default. Does not include ``contrib``. * - ``none`` - - Install only core ``isaaclab`` package + - Core packages only — no optional submodules, no extra feature deps Examples: @@ -125,22 +130,28 @@ Examples: .. code-block:: bash - # Minimal Newton setup - ./isaaclab.sh -i newton,tasks,assets,ov,rl[rsl_rl] + # Core only (physx, tasks, assets always included — no optional extras) + ./isaaclab.sh -i none - # Newton with OVRTX, RSL-RL, and Newton visualizer - ./isaaclab.sh -i newton,tasks,assets,ov[ovrtx],rl[rsl_rl],visualizers[newton] + # Newton physics + RSL-RL (most common kitless setup) + ./isaaclab.sh -i newton,'rl[rsl-rl]' + + # Newton + OVRTX renderer + RSL-RL + Newton visualizer + ./isaaclab.sh -i newton,ov,'rl[rsl-rl]','visualizer[newton]' .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows .. code-block:: batch - :: Minimal Newton setup - isaaclab.bat -i newton,tasks,assets,ov,rl[rsl_rl] + :: Core only + isaaclab.bat -i none + + :: Newton physics + RSL-RL + isaaclab.bat -i newton,rl[rsl-rl] - :: Newton with OVRTX, RSL-RL, and Newton visualizer - isaaclab.bat -i newton,tasks,assets,ov[ovrtx],rl[rsl_rl],visualizers[newton] + :: Newton + OVRTX + RSL-RL + Newton visualizer + isaaclab.bat -i newton,ov,rl[rsl-rl],visualizer[newton] .. _installation-ovrtx: diff --git a/pyproject.toml b/pyproject.toml index 572395018482..974ab0caaf19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,57 +12,31 @@ dependencies = [ "isaaclab", "isaaclab-assets", "isaaclab-contrib", - "isaaclab-newton[all]", + "isaaclab-experimental", + "isaaclab-newton", "isaaclab-ov", "isaaclab-ovphysx", - "isaaclab-physx[newton]", - "isaaclab-rl[rsl-rl]", + "isaaclab-physx", + "isaaclab-rl", "isaaclab-tasks", + "isaaclab-tasks-experimental", + "isaaclab-visualizers", "torch==2.10.0", "torchaudio==2.10.0", "torchvision==0.25.0", ] [project.optional-dependencies] -assets = [ - "isaaclab-assets", +isaacsim = [ + "isaacsim[all,extscache]==5.1.0", ] -contrib = [ - "isaaclab-contrib", -] -mimic = [ +all = [ + "isaacsim[all,extscache]==5.1.0", "isaaclab-mimic", -] -newton = [ "isaaclab-newton[all]", "isaaclab-physx[newton]", -] -ov = [ - "isaaclab-ovphysx[ovphysx]", -] -physx = [ - "isaaclab-physx", -] -rl = [ - "isaaclab-rl[rsl-rl]", -] -rl-all = [ "isaaclab-rl[all]", -] -rtx = [ - "isaaclab-ov[ovrtx]", -] -tasks = [ - "isaaclab-assets", - "isaaclab-contrib", - "isaaclab-ov", - "isaaclab-ovphysx", - "isaaclab-tasks", -] -tasks-experimental = [ - "isaaclab-tasks-experimental", -] -visualizers = [ + "isaaclab-teleop", "isaaclab-visualizers[all]", ] diff --git a/source/isaaclab/changelog.d/kellyguo11-fix-modular-install.minor.rst b/source/isaaclab/changelog.d/kellyguo11-fix-modular-install.minor.rst new file mode 100644 index 000000000000..649b8f65c363 --- /dev/null +++ b/source/isaaclab/changelog.d/kellyguo11-fix-modular-install.minor.rst @@ -0,0 +1,35 @@ +Changed +^^^^^^^ + +* Changed the installation model of :meth:`~isaaclab.cli.commands.install.command_install` + from per-submodule selection to a three-tier system. All core submodules + (``isaaclab``, ``isaaclab_assets``, ``isaaclab_contrib``, ``isaaclab_experimental``, + ``isaaclab_newton``, ``isaaclab_ov``, ``isaaclab_ovphysx``, ``isaaclab_physx``, + ``isaaclab_rl``, ``isaaclab_tasks``, ``isaaclab_tasks_experimental``, + ``isaaclab_visualizers``) + are now always installed by ``./isaaclab.sh -i``. Optional submodules + (``mimic``, ``teleop``) and automatic extra feature sets + (``newton``, ``rl[...]``, ``visualizer[...]``) are installed by ``./isaaclab.sh -i`` + / ``./isaaclab.sh -i all``. + Optional dependency extras require selectors, so rlinf dependencies are + installed with ``contrib[rlinf]`` and the ``ovrtx`` / ``ovphysx`` wheels are installed + with ``ov[ovrtx]``, ``ov[ovphysx]``, or ``ov[all]``. Old per-submodule tokens (e.g. + ``assets``, ``tasks``, ``physx``) now emit a warning and are skipped gracefully. + Migrate using the table below: + + +----------------------------------------------+-------------------------------------------+ + | Old command | New command | + +==============================================+===========================================+ + | ``./isaaclab.sh -i assets,tasks,physx`` | ``./isaaclab.sh -i none`` | + +----------------------------------------------+-------------------------------------------+ + | ``./isaaclab.sh -i assets,tasks,ov,rl[...]`` | ``./isaaclab.sh -i ov[all],rl[...]`` | + +----------------------------------------------+-------------------------------------------+ + | ``./isaaclab.sh -i newton,rl[all]`` | unchanged | + +----------------------------------------------+-------------------------------------------+ + | ``./isaaclab.sh -i mimic,teleop`` | unchanged | + +----------------------------------------------+-------------------------------------------+ + | ``uv pip install isaaclab[tasks,rl,assets]`` | ``uv pip install isaaclab[all]`` | + +----------------------------------------------+-------------------------------------------+ + +* Simplified :mod:`isaaclab` package extras to ``isaacsim`` and ``all``; removed the old + per-submodule extras (``tasks``, ``rl``, ``assets``, etc.) from ``pip install isaaclab[...]``. diff --git a/source/isaaclab/isaaclab/cli/__init__.py b/source/isaaclab/isaaclab/cli/__init__.py index ee04e558705b..0d71d4e48bc0 100644 --- a/source/isaaclab/isaaclab/cli/__init__.py +++ b/source/isaaclab/isaaclab/cli/__init__.py @@ -9,7 +9,12 @@ from .commands.envs import command_setup_conda, command_setup_uv from .commands.format import command_format -from .commands.install import VALID_ISAACLAB_SUBMODULES, VALID_RL_FRAMEWORKS, command_install +from .commands.install import ( + CORE_ISAACLAB_SUBMODULES, + OPTIONAL_ISAACLAB_SUBMODULES, + VALID_EXTRA_FEATURES, + command_install, +) from .commands.misc import ( command_build_docs, command_new, @@ -61,28 +66,53 @@ def cli() -> None: ), ) - _submodules_str = ", ".join(sorted(VALID_ISAACLAB_SUBMODULES)) - _frameworks_str = ", ".join(sorted(VALID_RL_FRAMEWORKS)) + _optional_str = ", ".join(sorted(OPTIONAL_ISAACLAB_SUBMODULES)) + _extras_str = ", ".join(sorted(VALID_EXTRA_FEATURES)) + _core_str = ", ".join(CORE_ISAACLAB_SUBMODULES) parser.add_argument( "-i", "--install", nargs="?", const="all", help=( - "Install Isaac Lab submodules and RL frameworks.\n" - "Accepts a comma-separated list of submodule names, one of the RL frameworks, or a special value.\n" + "Install Isaac Lab submodules and optional extra dependencies.\n" + "\n" + "All core submodules are always installed:\n" + f" {_core_str}\n" + "\n" + "Accepts a comma-separated list of optional submodule names and/or\n" + "extra feature selectors, or one of the special values below.\n" "\n" - f"* Isaac Lab submodules: {_submodules_str}\n" - " Any submodule accepts an editable selector, e.g. visualizers[all|kit|newton|rerun|viser], rl[rsl_rl|skrl].\n" + f"* Optional submodules: {_optional_str}\n" + " Installed by 'all' or by explicit token.\n" "\n" - f"* RL frameworks: {_frameworks_str}\n" - " Passing an RL framework name installs all Isaac Lab submodules + that framework.\n" - " On Linux/macOS, quote selectors containing brackets: --install 'visualizers[rerun]'.\n" + f"* Extra feature sets: {_extras_str}\n" + " Install optional heavy dependencies for a feature on top of the core.\n" + " Supports an optional selector in brackets:\n" + " contrib[rlinf]\n" + " ov[ovrtx|ovphysx|all]\n" + " rl[rsl-rl|skrl|sb3|rl-games] (default: all)\n" + " visualizer[kit|newton|rerun|viser] (default: all)\n" + " On Linux/macOS, quote selectors containing brackets:\n" + " --install 'rl[rsl-rl]'\n" "\n" "* Special values:\n" - "- all - Install all Isaac Lab submodules + all RL frameworks (default).\n" - "- none - Install only the core 'isaaclab' package.\n" - "- (-i or --install without value) - Install all Isaac Lab submodules + all RL frameworks.\n" + " all - Core + optional submodules (mimic, teleop) + auto extra\n" + " features (newton, rl, visualizer). Does not install contrib/ov\n" + " dependency extras (default).\n" + " none - Core submodules only; no optional submodules, no extra features.\n" + " (-i with no value) - Same as 'all'.\n" + "\n" + "Note: Contrib and OV source packages are core; runtime dependencies require selectors:\n" + " ./isaaclab.sh -i 'contrib[rlinf]'\n" + " ./isaaclab.sh -i 'ov[ovrtx]'\n" + "\n" + "Examples:\n" + " ./isaaclab.sh -i\n" + " ./isaaclab.sh -i none\n" + " ./isaaclab.sh -i newton,'rl[rsl-rl]'\n" + " ./isaaclab.sh -i mimic,teleop,'visualizer[rerun]'\n" + " ./isaaclab.sh -i 'ov[ovrtx]'\n" "\n" ), ) diff --git a/source/isaaclab/isaaclab/cli/commands/install.py b/source/isaaclab/isaaclab/cli/commands/install.py index f83c4dfdbf87..90fa929b69e5 100644 --- a/source/isaaclab/isaaclab/cli/commands/install.py +++ b/source/isaaclab/isaaclab/cli/commands/install.py @@ -431,24 +431,44 @@ def _install_isaacsim() -> None: ) -# Valid Isaac Lab submodule names that can be passed to --install. -# Each Isaac Lab submodule maps to a source directory named "isaaclab_" under source/. -VALID_ISAACLAB_SUBMODULES: set[str] = { - "assets", +# Source directories installed on every ./isaaclab.sh -i invocation (even "none"). +# Order matters: isaaclab must be first so dependents resolve against the local copy. +CORE_ISAACLAB_SUBMODULES: list[str] = [ + "isaaclab", + "isaaclab_assets", + "isaaclab_contrib", + "isaaclab_experimental", + "isaaclab_newton", + "isaaclab_ov", + "isaaclab_ovphysx", + "isaaclab_physx", + "isaaclab_rl", + "isaaclab_tasks", + "isaaclab_tasks_experimental", + "isaaclab_visualizers", +] + +# Optional submodules — only installed when explicitly requested or with 'all'. +# Maps the short CLI name to one or more source directory names under source/. +OPTIONAL_ISAACLAB_SUBMODULES: dict[str, tuple[str, ...]] = { + "mimic": ("isaaclab_mimic",), + "teleop": ("isaaclab_teleop",), +} + +# Extra feature sets that install optional heavy dependencies on top of the +# always-installed core submodules. Each name corresponds to one or more +# 'pip install --editable path[extra]' calls against packages already in the +# core set. +VALID_EXTRA_FEATURES: set[str] = { "contrib", - "mimic", "newton", "ov", - "physx", "rl", - "tasks", - "teleop", - "visualizers", + "visualizer", } -# RL framework names accepted. -# Passing one of these installs all extensions + that framework. -VALID_RL_FRAMEWORKS: set[str] = {"rl_games", "rsl_rl", "sb3", "skrl", "robomimic"} +# Extra features excluded from the automatic ``-i all`` / ``-i`` install. +MANUAL_EXTRA_FEATURES: set[str] = {"contrib", "ov"} def _split_install_items(install_type: str) -> list[str]: @@ -474,25 +494,13 @@ def _split_install_items(install_type: str) -> list[str]: return parts -def _install_isaaclab_submodules( - isaaclab_submodules: list[str] | None = None, - submodule_extras: dict[str, str] | None = None, - exclude: set[str] | None = None, -) -> None: - """Install Isaac Lab submodules from the source directory. - - Scans ``source/`` for sub-directories that contain a ``setup.py`` and - installs each one as an editable pip package. +def _install_isaaclab_submodules(isaaclab_submodules: list[str]) -> None: + """Install Isaac Lab submodules from the source directory as editable packages. Args: - isaaclab_submodules: Optional, list of source directory names to install. - If ``None`` is provided, every submodule found under ``source/`` - is installed (subject to *exclude*). - submodule_extras: Optional mapping from submodule source directory - name to pip editable selector (e.g. - ``{"isaaclab_visualizers": "[rerun]"}``). - exclude: Optional set of source directory names to skip even when - *isaaclab_submodules* is ``None``. + isaaclab_submodules: Ordered list of source directory names to install + (e.g. ``["isaaclab", "isaaclab_assets", ...]``). ``isaaclab`` must + appear first so downstream packages resolve against the local copy. """ python_exe = extract_python_exe() source_dir = ISAACLAB_ROOT / "source" @@ -501,58 +509,132 @@ def _install_isaaclab_submodules( print_warning(f"Source directory not found: {source_dir}") return - # Collect installable submodules from source/. - install_items = [] - for item in source_dir.iterdir(): - if not (item.is_dir() and (item / "setup.py").exists()): - continue - if isaaclab_submodules is not None and item.name not in isaaclab_submodules: - continue - if exclude and item.name in exclude: - continue - install_items.append(item) - - # Install order matters for local editable deps: - # packages like isaaclab_visualizers depend on the local isaaclab package. - install_items.sort(key=lambda item: (item.name != "isaaclab", item.name)) - pip_cmd = get_pip_command(python_exe) - for item in install_items: - print_info(f"Installing submodule: {item.name}") - editable = (submodule_extras or {}).get(item.name, "") - install_target = f"{item}{editable}" - run_command(pip_cmd + ["install", "--editable", install_target]) + for pkg_name in isaaclab_submodules: + item = source_dir / pkg_name + if not item.is_dir() or not (item / "setup.py").exists(): + print_warning(f"Submodule directory not found or missing setup.py: {item}") + continue + print_info(f"Installing submodule: {pkg_name}") + run_command(pip_cmd + ["install", "--editable", str(item)]) _upgrade_extension_pip_dependencies( python_exe, pip_cmd, - item.name, + pkg_name, _get_extension_pip_upgrade_dependencies(item), ) -def _install_extra_frameworks(framework_name: str = "all") -> None: - """install the python packages for supported reinforcement learning frameworks +def _install_optional_submodule_extra_dependencies(submodule_name: str, selector: str) -> None: + """Install optional dependency extras for an optional submodule. + + Args: + submodule_name: One of :data:`OPTIONAL_ISAACLAB_SUBMODULES`. + selector: Extra selector from a token such as ``mimic[foo]``. + """ + if not selector: + return + + print_warning(f"Optional submodule '{submodule_name}' does not support selectors (got '{selector}').") + + +def _install_contrib_extra_dependencies(selector: str) -> None: + """Install optional contrib runtime dependencies. Args: - framework_name: Framework extra to install (for example ``all`` or ``none``). + selector: Contrib extra selector, currently ``rlinf``. """ + if not selector: + print_info( + "Contrib source package is installed with the core submodules. " + "Use 'contrib[rlinf]' to install contrib runtime dependencies." + ) + return + python_exe = extract_python_exe() pip_cmd = get_pip_command(python_exe) + source_dir = ISAACLAB_ROOT / "source" + + print_info(f"Installing contrib optional dependencies: {selector}...") + run_command(pip_cmd + ["install", "--editable", f"{source_dir}/isaaclab_contrib[{selector}]"]) - extras = "" - if framework_name != "none": - extras = f"[{framework_name}]" - # Check if specified which rl-framework to install. - if framework_name == "none": - print_info("No rl-framework will be installed.") +def _install_ov_extra_dependencies(selector: str) -> None: + """Install optional OV runtime dependencies. + + Args: + selector: One or more OV selectors from ``ov[ovrtx]``, + ``ov[ovphysx]``, or ``ov[all]``. + """ + if not selector: + print_info( + "OV source packages are installed with the core submodules. " + "Use 'ov[ovrtx]', 'ov[ovphysx]', or 'ov[all]' to install OV runtime dependencies." + ) return - print_info(f"Installing rl-framework: {framework_name}") + python_exe = extract_python_exe() + pip_cmd = get_pip_command(python_exe) + source_dir = ISAACLAB_ROOT / "source" + + selectors = {item.strip().lower() for item in selector.split(",") if item.strip()} + valid_selectors = {"all", "ovrtx", "ovphysx"} + unknown_selectors = selectors - valid_selectors + if unknown_selectors: + print_warning( + f"Unknown ov selector(s): {', '.join(sorted(unknown_selectors))}. " + f"Valid selectors: {', '.join(sorted(valid_selectors))}." + ) + if "all" in selectors: + selectors.update({"ovrtx", "ovphysx"}) + if "ovrtx" in selectors: + print_info("Installing OVRTX optional dependency...") + run_command(pip_cmd + ["install", "--editable", f"{source_dir}/isaaclab_ov[ovrtx]"]) + if "ovphysx" in selectors: + print_info("Installing OVPhysX optional dependency...") + run_command(pip_cmd + ["install", "--editable", f"{source_dir}/isaaclab_ovphysx[ovphysx]"]) + - # Install the learning frameworks specified. - run_command(pip_cmd + ["install", "-e", f"{ISAACLAB_ROOT}/source/isaaclab_rl{extras}"]) - run_command(pip_cmd + ["install", "-e", f"{ISAACLAB_ROOT}/source/isaaclab_mimic{extras}"]) +def _install_extra_feature(feature_name: str, selector: str = "") -> None: + """Install optional extra dependencies for a feature set. + + Each feature maps to one or more editable installs with extras applied to + packages that are already part of the core set. + + Args: + feature_name: One of :data:`VALID_EXTRA_FEATURES`. + selector: Optional extra selector (e.g. ``"rsl-rl"`` for + ``rl[rsl-rl]``). When empty a sensible default is chosen per + feature (``"all"`` for ``rl`` and ``visualizer``). + """ + python_exe = extract_python_exe() + pip_cmd = get_pip_command(python_exe) + source_dir = ISAACLAB_ROOT / "source" + + if feature_name == "contrib": + _install_contrib_extra_dependencies(selector) + elif feature_name == "newton": + if selector: + print_warning(f"'newton' does not support selectors (got '{selector}'). Installing all newton extras.") + print_info("Installing newton extras (newton[sim], PyOpenGL-accelerate, imgui-bundle)...") + run_command(pip_cmd + ["install", "--editable", f"{source_dir}/isaaclab_newton[all]"]) + run_command(pip_cmd + ["install", "--editable", f"{source_dir}/isaaclab_physx[newton]"]) + run_command(pip_cmd + ["install", "--editable", f"{source_dir}/isaaclab_visualizers[newton]"]) + elif feature_name == "rl": + extra = selector if selector else "all" + print_info(f"Installing RL framework extras: {extra}...") + run_command(pip_cmd + ["install", "--editable", f"{source_dir}/isaaclab_rl[{extra}]"]) + elif feature_name == "visualizer": + extra = selector if selector else "all" + print_info(f"Installing visualizer extras: {extra}...") + run_command(pip_cmd + ["install", "--editable", f"{source_dir}/isaaclab_visualizers[{extra}]"]) + elif feature_name == "ov": + _install_ov_extra_dependencies(selector) + else: + print_warning( + f"Unknown extra feature '{feature_name}'. " + f"Valid features: {', '.join(sorted(VALID_EXTRA_FEATURES))}. Skipping." + ) _PREBUNDLE_REPOINT_PACKAGES: list[str] = [ @@ -688,94 +770,92 @@ def _repoint_prebundle_packages() -> None: def command_install(install_type: str = "all") -> None: - """Install Isaac Lab extensions and optional submodules. + """Install Isaac Lab extensions and optional extras. + + All core submodules are always installed. Optional submodules, optional + submodule extras, and extra feature dependencies are installed based on + *install_type*. Args: - install_type: Comma-separated list of extras to install, or one of the - special values ``"all"`` / ``"none"``. Extra names match the keys - in ``source/isaaclab/setup.py``'s ``extras_require``: - * ``"all"`` (default) — install every extension found under - ``source/``, plus all RL frameworks. - * ``"none"`` — install only the "core" ``isaaclab`` package and skip - RL frameworks. - * Comma-separated extras, e.g. ``"mimic,assets"`` — install - only the "core" ``isaaclab`` package plus the listed submodules. + install_type: Controls which optional submodules and extra feature + dependencies to install on top of the always-installed core set. + + * ``"all"`` (default) — install core submodules + optional + submodules (``mimic``, ``teleop``) + all automatic + extra features. + * ``"none"`` — install core submodules only; no optional + submodules, no extra feature dependencies. + * Comma-separated tokens — install core submodules plus the listed + optional submodules and extra features. Valid tokens: + + - Optional submodules: ``mimic``, ``teleop`` + - Extra features: ``contrib[rlinf]``, ``newton``, ``rl[]``, + ``visualizer[]``, ``ov[ovrtx|ovphysx|all]`` + - Special: ``isaacsim`` + + Examples:: + + ./isaaclab.sh -i newton,rl[rsl-rl] + ./isaaclab.sh -i mimic,visualizer[rerun] + ./isaaclab.sh -i teleop,rl[skrl],newton """ # Install system dependencies first. _install_system_deps() - # Install the python packages in IsaacLab/source directory. print_info("Installing extensions inside the Isaac Lab repository...") python_exe = extract_python_exe() - # Show which environment is being used. if os.environ.get("VIRTUAL_ENV"): print_info(f"Using uv/venv environment: {os.environ['VIRTUAL_ENV']}") elif os.environ.get("CONDA_PREFIX"): print_info(f"Using conda environment: {os.environ['CONDA_PREFIX']}") - print_info(f"Python executable: {python_exe}") - # Decide which source directories (source/isaaclab/*) to install. - # "all" : install everything + all RL frameworks - # "none" : core isaaclab only, no RL frameworks - # RL framework : install everything + only that RL framework (e.g. "skrl") - # "a,b" : core + selected submodule directories, no RL frameworks install_isaacsim = False + # Always start with the full core set (isaaclab must be first). + submodules_to_install: list[str] = list(CORE_ISAACLAB_SUBMODULES) + # List of (feature_name, selector) tuples to apply after the base install. + extra_features: list[tuple[str, str]] = [] + # List of (submodule_name, selector) tuples for optional submodule extras. + optional_submodule_extra_dependencies: list[tuple[str, str]] = [] + + def append_submodules_once(package_dirs: tuple[str, ...]) -> None: + for pkg_dir in package_dirs: + if pkg_dir not in submodules_to_install: + submodules_to_install.append(pkg_dir) if install_type == "all": - isaaclab_submodules = None - exclude = None - submodule_extras = {"isaaclab_visualizers": "[all]"} - framework_type = "all" + for package_dirs in OPTIONAL_ISAACLAB_SUBMODULES.values(): + append_submodules_once(package_dirs) + extra_features = [(name, "") for name in sorted(VALID_EXTRA_FEATURES - MANUAL_EXTRA_FEATURES)] elif install_type == "none": - isaaclab_submodules = ["isaaclab"] - exclude = None - submodule_extras = {} - framework_type = "none" - elif install_type in VALID_RL_FRAMEWORKS: - isaaclab_submodules = None - exclude = None - submodule_extras = {"isaaclab_visualizers": "[all]"} - framework_type = install_type + # Core only — no optional submodules, no extra features. + pass else: - # Parse comma-separated submodule names and RL framework names. - isaaclab_submodules = ["isaaclab"] # core is always required - exclude = None # explicit selection — no exclusions - submodule_extras = {} - framework_type = "none" for token in _split_install_items(install_type): - # Parse optional editable selector: "name[extra1,extra2]" if "[" in token: bracket_pos = token.index("[") name = token[:bracket_pos].strip() - editable = token[bracket_pos:].strip() + if "]" not in token: + print_warning(f"Malformed install token '{token}': missing closing ']'. Skipping.") + continue + selector = token[bracket_pos + 1 : token.index("]")].strip() else: name = token.strip() - editable = "" + selector = "" + if name == "isaacsim": install_isaacsim = True - continue - if name in VALID_RL_FRAMEWORKS: - framework_type = name - # Ensure isaaclab_rl is installed so the framework extra works. - if "isaaclab_rl" not in isaaclab_submodules: - isaaclab_submodules.append("isaaclab_rl") - continue - if name in VALID_ISAACLAB_SUBMODULES: - pkg_dir = f"isaaclab_{name}" - if pkg_dir not in isaaclab_submodules: - isaaclab_submodules.append(pkg_dir) - if editable: - submodule_extras[pkg_dir] = editable - # Auto-include the matching visualizer when installing a physics backend. - if name == "newton" and "isaaclab_visualizers" not in isaaclab_submodules: - isaaclab_submodules.append("isaaclab_visualizers") - submodule_extras["isaaclab_visualizers"] = "[newton]" + elif name in OPTIONAL_ISAACLAB_SUBMODULES: + append_submodules_once(OPTIONAL_ISAACLAB_SUBMODULES[name]) + if selector: + optional_submodule_extra_dependencies.append((name, selector)) + elif name in VALID_EXTRA_FEATURES: + extra_features.append((name, selector)) else: - valid = sorted(VALID_ISAACLAB_SUBMODULES) + sorted(VALID_RL_FRAMEWORKS) + ["isaacsim"] - print_warning(f"Unknown Isaac Lab submodule '{name}'. Valid values: {', '.join(valid)}. Skipping.") + valid = sorted(OPTIONAL_ISAACLAB_SUBMODULES) + sorted(VALID_EXTRA_FEATURES) + ["isaacsim"] + print_warning(f"Unknown install token '{name}'. Valid values: {', '.join(valid)}. Skipping.") # Configure extra package indexes for NVIDIA and MuJoCo wheels. os.environ.setdefault("UV_EXTRA_INDEX_URL", "https://pypi.nvidia.com") @@ -841,12 +921,20 @@ def command_install(install_type: str = "all") -> None: # Install pytorch (version based on arch). _ensure_cuda_torch() - # Install the python modules for the extensions in Isaac Lab. - _install_isaaclab_submodules(isaaclab_submodules, submodule_extras, exclude) + # Install all submodules (core set + any explicitly requested optional ones). + _install_isaaclab_submodules(submodules_to_install) + + # Install requested optional submodule dependency extras. + if optional_submodule_extra_dependencies: + print_info("Installing optional submodule dependencies...") + for submodule_name, selector in optional_submodule_extra_dependencies: + _install_optional_submodule_extra_dependencies(submodule_name, selector) - # Install the python packages for supported reinforcement learning frameworks. - print_info("Installing extra requirements such as learning frameworks...") - _install_extra_frameworks(framework_type) + # Install requested extra feature dependencies. + if extra_features: + print_info("Installing extra feature dependencies...") + for feature_name, selector in extra_features: + _install_extra_feature(feature_name, selector) # In some rare cases, torch might not be installed properly by setup.py, add one more check here. # Can prevent that from happening. diff --git a/source/isaaclab/setup.py b/source/isaaclab/setup.py index 48dc5eff70aa..bc64609de3fd 100644 --- a/source/isaaclab/setup.py +++ b/source/isaaclab/setup.py @@ -87,32 +87,24 @@ PYTORCH_INDEX_URL = ["https://download.pytorch.org/whl/cu128"] -# Isaac Lab subpackages + Isaac Sim +# Optional extras for pip/uv installs. +# Use ``pip install isaaclab[isaacsim]`` to add Isaac Sim, or +# ``pip install isaaclab[all]`` to pull in all sub-packages and extras. EXTRAS_REQUIRE = { "isaacsim": ["isaacsim[all,extscache]==5.1.0"], - # Individual Isaac Lab sub-packages - "assets": ["isaaclab_assets"], - "physx": ["isaaclab_physx"], - "contrib": ["isaaclab_contrib"], - "mimic": ["isaaclab_mimic"], - "newton": ["isaaclab_newton"], - "rl": ["isaaclab_rl"], - "tasks": ["isaaclab_tasks"], - "teleop": ["isaaclab_teleop"], - "visualizers": ["isaaclab_visualizers[all]"], - "visualizers-kit": ["isaaclab_visualizers[kit]"], - "visualizers-newton": ["isaaclab_visualizers[newton]"], - "visualizers-rerun": ["isaaclab_visualizers[rerun]"], - "visualizers-viser": ["isaaclab_visualizers[viser]"], - # Convenience: all sub-packages (does not include isaacsim) "all": [ + "isaacsim[all,extscache]==5.1.0", "isaaclab_assets", - "isaaclab_physx", "isaaclab_contrib", + "isaaclab_experimental", "isaaclab_mimic", - "isaaclab_newton", - "isaaclab_rl", + "isaaclab_newton[all]", + "isaaclab_ov", + "isaaclab_ovphysx", + "isaaclab_physx[newton]", + "isaaclab_rl[all]", "isaaclab_tasks", + "isaaclab_tasks_experimental", "isaaclab_teleop", "isaaclab_visualizers[all]", ], diff --git a/docker/Dockerfile.installci b/source/isaaclab/test/install_ci/Dockerfile.installci similarity index 72% rename from docker/Dockerfile.installci rename to source/isaaclab/test/install_ci/Dockerfile.installci index 30da2b55b82a..0e149e61b84d 100644 --- a/docker/Dockerfile.installci +++ b/source/isaaclab/test/install_ci/Dockerfile.installci @@ -38,14 +38,13 @@ RUN apt-get update && \ # Make python3 the default python RUN ln -sf /usr/bin/python3 /usr/bin/python -# Install test runner dependencies +# Install test runner dependencies into the system Python RUN pip install --break-system-packages \ pytest>=8.0 \ pytest-timeout>=2.0 -# Install uv -RUN curl -LsSf https://astral.sh/uv/install.sh | sh -ENV PATH="/root/.local/bin:${PATH}" +# Install uv system-wide so non-root users can invoke it without PATH trickery +RUN curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh # Copy the repo ARG ISAACLAB_PATH=/workspace/isaaclab @@ -53,12 +52,20 @@ ENV ISAACLAB_PATH=${ISAACLAB_PATH} COPY . ${ISAACLAB_PATH} # Fix line endings in .sh files (Win git may add \r) -# This hack need to be in the main Dockerfiles too RUN find ${ISAACLAB_PATH} -type f -name "*.sh" -exec sed -i 's/\r$//' {} + RUN chmod +x ${ISAACLAB_PATH}/isaaclab.sh +# Create non-root runtime user and transfer workspace ownership. +# uid/gid 1000 match the GitHub runner bind-mounts used by Docker-based tests. +# --non-unique allows reuse of existing uid/gid slots in some base images. +RUN groupadd --non-unique --gid 1000 isaaclab \ + && useradd --non-unique --uid 1000 --gid 1000 -m -s /bin/bash isaaclab \ + && chown -R isaaclab:isaaclab ${ISAACLAB_PATH} + WORKDIR ${ISAACLAB_PATH} +USER isaaclab + # isaaclab.x debug mode ENV DEBUG=1 diff --git a/source/isaaclab/test/install_ci/Dockerfile.installci-conda b/source/isaaclab/test/install_ci/Dockerfile.installci-conda new file mode 100644 index 000000000000..3a90d5c070b5 --- /dev/null +++ b/source/isaaclab/test/install_ci/Dockerfile.installci-conda @@ -0,0 +1,50 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# Conda-enabled layer for installation CI tests. +# Builds on top of the uv-based installci image. +# +# Usage: +# # First build the base uv image +# docker build --build-arg BASE_IMAGE=ubuntu:24.04 \ +# -f docker/Dockerfile.installci -t isaaclab-installci:uv . +# # Then build this conda layer on top +# docker build --build-arg UV_IMAGE=isaaclab-installci:uv \ +# -f docker/Dockerfile.installci-conda -t isaaclab-installci:conda . +# docker run --rm --gpus all isaaclab-installci:conda -v -m conda + +ARG UV_IMAGE=isaaclab-installci:uv +FROM ${UV_IMAGE} + +SHELL ["/bin/bash", "-c"] + +# Switch to root for system-level conda installation +USER root + +# Install Miniconda into the isaaclab user's home directory so the runtime user +# has full write access for creating environments and caching packages. +ARG MINICONDA_VERSION=latest +RUN curl -fsSL "https://repo.anaconda.com/miniconda/Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh" \ + -o /tmp/miniconda.sh \ + && bash /tmp/miniconda.sh -b -p /home/isaaclab/miniconda3 \ + && rm /tmp/miniconda.sh \ + && chown -R isaaclab:isaaclab /home/isaaclab/miniconda3 + +ENV PATH="/home/isaaclab/miniconda3/bin:${PATH}" + +# Drop back to the non-root runtime user for all subsequent steps +USER isaaclab + +# Accept Miniconda default-channel Terms of Service non-interactively so that +# `conda create` works without user prompts inside the container. +# Runs as isaaclab so the acceptance is written to /home/isaaclab/.condarc. +RUN conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main && \ + conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r + +# Install test runner dependencies into the conda base Python +RUN pip install "pytest>=8.0" "pytest-timeout>=2.0" + +# Smoke-test: conda should be functional and pytest importable +RUN conda --version && python --version && python -m pytest --version diff --git a/source/isaaclab/test/install_ci/conftest.py b/source/isaaclab/test/install_ci/conftest.py index c4bbc94ab200..4965ac9dd778 100644 --- a/source/isaaclab/test/install_ci/conftest.py +++ b/source/isaaclab/test/install_ci/conftest.py @@ -85,6 +85,8 @@ def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line("markers", "native: tests that only run natively (not in Docker)") config.addinivalue_line("markers", "slow: tests that take a long time") config.addinivalue_line("markers", "uv: tests that require the uv package manager") + config.addinivalue_line("markers", "conda: tests that require the conda package manager") + config.addinivalue_line("markers", "timeout: per-test timeout in seconds") try: config.stash[_EXECUTION_ENVIRONMENT_KEY] = _utils.detect_execution_environment() diff --git a/source/isaaclab/test/install_ci/test_install_command_parsing.py b/source/isaaclab/test/install_ci/test_install_command_parsing.py new file mode 100644 index 000000000000..33cb55dba094 --- /dev/null +++ b/source/isaaclab/test/install_ci/test_install_command_parsing.py @@ -0,0 +1,402 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Unit tests for install.py token parsing and command_install dispatch logic. + +These tests exercise the pure parsing logic and the install dispatch logic by +mocking all external I/O (pip, subprocess, filesystem), so they can run +without a GPU or Isaac Sim installation. +""" + +from __future__ import annotations + +import os + +# --------------------------------------------------------------------------- +# Helpers to import install.py symbols with the isaaclab source root on PATH +# --------------------------------------------------------------------------- +import sys +from pathlib import Path +from unittest.mock import patch + +_ISAACLAB_SRC = Path(__file__).resolve().parents[2] +if str(_ISAACLAB_SRC) not in sys.path: + sys.path.insert(0, str(_ISAACLAB_SRC)) + +from isaaclab.cli.commands.install import ( + CORE_ISAACLAB_SUBMODULES, + MANUAL_EXTRA_FEATURES, + OPTIONAL_ISAACLAB_SUBMODULES, + VALID_EXTRA_FEATURES, + _split_install_items, + command_install, +) + + +def _optional_submodule_packages() -> list[str]: + """Return flattened optional submodule source package names.""" + return [pkg for packages in OPTIONAL_ISAACLAB_SUBMODULES.values() for pkg in packages] + + +# --------------------------------------------------------------------------- +# _split_install_items +# --------------------------------------------------------------------------- + + +class TestSplitInstallItems: + """Tests for _split_install_items().""" + + def test_single_token(self): + assert _split_install_items("newton") == ["newton"] + + def test_two_plain_tokens(self): + assert _split_install_items("newton,mimic") == ["newton", "mimic"] + + def test_token_with_selector(self): + assert _split_install_items("rl[rsl-rl]") == ["rl[rsl-rl]"] + + def test_comma_inside_brackets_not_split(self): + assert _split_install_items("rl[rsl-rl,skrl]") == ["rl[rsl-rl,skrl]"] + + def test_mixed_tokens(self): + result = _split_install_items("newton,rl[rsl-rl],mimic") + assert result == ["newton", "rl[rsl-rl]", "mimic"] + + def test_whitespace_stripped(self): + assert _split_install_items("newton , mimic") == ["newton", "mimic"] + + def test_empty_string(self): + assert _split_install_items("") == [] + + def test_all_special_value(self): + assert _split_install_items("all") == ["all"] + + def test_none_special_value(self): + assert _split_install_items("none") == ["none"] + + def test_visualizer_with_selector(self): + assert _split_install_items("visualizer[rerun]") == ["visualizer[rerun]"] + + def test_multiple_selectors_mixed(self): + result = _split_install_items("mimic,visualizer[rerun],rl[rsl-rl]") + assert result == ["mimic", "visualizer[rerun]", "rl[rsl-rl]"] + + def test_nested_brackets_depth(self): + # Depth > 1 should not split on commas. + result = _split_install_items("contrib[a[b,c]]") + assert result == ["contrib[a[b,c]]"] + + def test_missing_closing_bracket_not_split(self): + # A malformed token with no closing ']' should come through as one item; + # the install dispatcher is responsible for emitting the warning. + result = _split_install_items("rl[rsl-rl") + assert result == ["rl[rsl-rl"] + + +# --------------------------------------------------------------------------- +# Constants sanity checks +# --------------------------------------------------------------------------- + + +class TestInstallConstants: + """Sanity checks for module-level install constants.""" + + def test_core_submodules_starts_with_isaaclab(self): + assert CORE_ISAACLAB_SUBMODULES[0] == "isaaclab", ( + "isaaclab must be first so dependents resolve against the local copy" + ) + + def test_core_submodules_contains_expected_packages(self): + expected = { + "isaaclab", + "isaaclab_assets", + "isaaclab_contrib", + "isaaclab_experimental", + "isaaclab_newton", + "isaaclab_ov", + "isaaclab_ovphysx", + "isaaclab_physx", + "isaaclab_rl", + "isaaclab_tasks", + "isaaclab_tasks_experimental", + "isaaclab_visualizers", + } + assert set(CORE_ISAACLAB_SUBMODULES) == expected + + def test_optional_submodules_contains_expected_packages(self): + assert set(OPTIONAL_ISAACLAB_SUBMODULES.keys()) == {"mimic", "teleop"} + assert OPTIONAL_ISAACLAB_SUBMODULES["mimic"] == ("isaaclab_mimic",) + assert OPTIONAL_ISAACLAB_SUBMODULES["teleop"] == ("isaaclab_teleop",) + + def test_valid_extra_features(self): + expected = {"contrib", "newton", "ov", "rl", "visualizer"} + assert expected == VALID_EXTRA_FEATURES + + def test_manual_extra_features_subset_of_valid(self): + assert MANUAL_EXTRA_FEATURES <= VALID_EXTRA_FEATURES + + def test_manual_extra_features(self): + assert {"contrib", "ov"} == MANUAL_EXTRA_FEATURES + + def test_no_overlap_between_optional_submodules_and_extra_features(self): + assert not (set(OPTIONAL_ISAACLAB_SUBMODULES.keys()) & VALID_EXTRA_FEATURES) + + def test_optional_submodules_not_in_core(self): + core_names = set(CORE_ISAACLAB_SUBMODULES) + for pkg in _optional_submodule_packages(): + assert pkg not in core_names + + +# --------------------------------------------------------------------------- +# command_install dispatch tests (all external I/O mocked) +# --------------------------------------------------------------------------- + +_INSTALL_MODULE = "isaaclab.cli.commands.install" + +# Functions that must be mocked to prevent actual system calls. +_PATCHES = [ + f"{_INSTALL_MODULE}._install_system_deps", + f"{_INSTALL_MODULE}._install_isaaclab_submodules", + f"{_INSTALL_MODULE}._install_extra_feature", + f"{_INSTALL_MODULE}._install_optional_submodule_extra_dependencies", + f"{_INSTALL_MODULE}._install_isaacsim", + f"{_INSTALL_MODULE}._ensure_cuda_torch", + f"{_INSTALL_MODULE}._maybe_preinstall_arm_nlopt", + f"{_INSTALL_MODULE}._maybe_uninstall_prebundled_torch", + f"{_INSTALL_MODULE}._ensure_pink_ik_dependencies_installed", + f"{_INSTALL_MODULE}._repoint_prebundle_packages", + f"{_INSTALL_MODULE}.command_vscode_settings", + f"{_INSTALL_MODULE}.get_pip_command", + f"{_INSTALL_MODULE}.extract_python_exe", + # run_command is called directly inside command_install for pip/setuptools upgrades. + f"{_INSTALL_MODULE}.run_command", +] + + +def _make_mock_env(**extra_env): + """Return an os.environ copy suitable for mocking docker-detection.""" + env = {k: v for k, v in os.environ.items() if k not in ("VIRTUAL_ENV", "CONDA_PREFIX")} + env.update(extra_env) + return env + + +class TestCommandInstallDispatch: + """Test that command_install() calls the right functions with the right args.""" + + def _run(self, install_type: str): + """Invoke command_install() with all I/O mocked; return captured mock calls.""" + mocks = {} + patchers = [] + for target in _PATCHES: + p = patch(target) + m = p.start() + mocks[target.split(".")[-1]] = m + patchers.append(p) + + # Prevent docker-detection from reading /proc or .dockerenv. + env_patcher = patch.dict(os.environ, {}, clear=False) + exists_patcher = patch("os.path.exists", return_value=False) + env_patcher.start() + exists_patcher.start() + patchers.extend([env_patcher, exists_patcher]) + + try: + command_install(install_type) + finally: + for p in patchers: + p.stop() + + return mocks + + # --- "all" --- + + def test_all_installs_core_plus_optional_submodules(self): + mocks = self._run("all") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + # Core set must be present. + for pkg in CORE_ISAACLAB_SUBMODULES: + assert pkg in installed, f"Expected {pkg} in submodules for 'all'" + # Optional submodules must be present. + for pkg in _optional_submodule_packages(): + assert pkg in installed, f"Expected {pkg} (optional) in submodules for 'all'" + + def test_all_installs_auto_extra_features_not_manual(self): + mocks = self._run("all") + called_features = {c.args[0] for c in mocks["_install_extra_feature"].call_args_list} + expected = VALID_EXTRA_FEATURES - MANUAL_EXTRA_FEATURES + assert called_features == expected, f"'all' should install {expected}, got {called_features}" + + def test_all_does_not_install_optional_submodule_extras(self): + mocks = self._run("all") + mocks["_install_optional_submodule_extra_dependencies"].assert_not_called() + + def test_all_does_not_install_manual_extra_dependencies(self): + mocks = self._run("all") + called_features = {c.args[0] for c in mocks["_install_extra_feature"].call_args_list} + assert "contrib" not in called_features + assert "ov" not in called_features + + def test_all_does_not_call_install_isaacsim(self): + mocks = self._run("all") + mocks["_install_isaacsim"].assert_not_called() + + # --- "none" --- + + def test_none_installs_only_core_submodules(self): + mocks = self._run("none") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert set(installed) == set(CORE_ISAACLAB_SUBMODULES) + + def test_none_installs_no_extra_features(self): + mocks = self._run("none") + mocks["_install_extra_feature"].assert_not_called() + + def test_none_does_not_install_optional_submodules(self): + mocks = self._run("none") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + for pkg in _optional_submodule_packages(): + assert pkg not in installed + + # --- extra features --- + + def test_newton_installs_core_plus_newton_extra(self): + mocks = self._run("newton") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert set(installed) == set(CORE_ISAACLAB_SUBMODULES) + mocks["_install_extra_feature"].assert_called_once_with("newton", "") + + def test_rl_with_selector(self): + mocks = self._run("rl[rsl-rl]") + mocks["_install_extra_feature"].assert_called_once_with("rl", "rsl-rl") + + def test_rl_without_selector(self): + mocks = self._run("rl") + mocks["_install_extra_feature"].assert_called_once_with("rl", "") + + def test_visualizer_with_selector(self): + mocks = self._run("visualizer[rerun]") + mocks["_install_extra_feature"].assert_called_once_with("visualizer", "rerun") + + # --- manual extra features and optional submodules --- + + def test_contrib_without_selector_dispatches_manual_extra_feature(self): + mocks = self._run("contrib") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert set(installed) == set(CORE_ISAACLAB_SUBMODULES) + mocks["_install_extra_feature"].assert_called_once_with("contrib", "") + mocks["_install_optional_submodule_extra_dependencies"].assert_not_called() + + def test_contrib_with_selector_dispatches_manual_extra_feature(self): + mocks = self._run("contrib[rlinf]") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert set(installed) == set(CORE_ISAACLAB_SUBMODULES) + mocks["_install_extra_feature"].assert_called_once_with("contrib", "rlinf") + mocks["_install_optional_submodule_extra_dependencies"].assert_not_called() + + def test_mimic_adds_mimic_to_submodules(self): + mocks = self._run("mimic") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert "isaaclab_mimic" in installed + mocks["_install_extra_feature"].assert_not_called() + mocks["_install_optional_submodule_extra_dependencies"].assert_not_called() + + def test_ov_without_selector_dispatches_manual_extra_feature(self): + mocks = self._run("ov") + mocks["_install_extra_feature"].assert_called_once_with("ov", "") + mocks["_install_optional_submodule_extra_dependencies"].assert_not_called() + + def test_ov_with_selector_dispatches_manual_extra_feature(self): + mocks = self._run("ov[ovrtx]") + mocks["_install_extra_feature"].assert_called_once_with("ov", "ovrtx") + mocks["_install_optional_submodule_extra_dependencies"].assert_not_called() + + def test_teleop_adds_teleop_to_submodules(self): + mocks = self._run("teleop") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert "isaaclab_teleop" in installed + mocks["_install_extra_feature"].assert_not_called() + mocks["_install_optional_submodule_extra_dependencies"].assert_not_called() + + # --- combined tokens --- + + def test_newton_and_rl_rsl_rl(self): + mocks = self._run("newton,rl[rsl-rl]") + calls = mocks["_install_extra_feature"].call_args_list + features = {(c.args[0], c.args[1]) for c in calls} + assert ("newton", "") in features + assert ("rl", "rsl-rl") in features + + def test_mimic_and_newton(self): + mocks = self._run("mimic,newton") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert "isaaclab_mimic" in installed + mocks["_install_extra_feature"].assert_called_once_with("newton", "") + + def test_mimic_and_teleop(self): + mocks = self._run("mimic,teleop") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert "isaaclab_mimic" in installed + assert "isaaclab_teleop" in installed + mocks["_install_extra_feature"].assert_not_called() + + # --- isaacsim token --- + + def test_isaacsim_token_triggers_isaacsim_install(self): + mocks = self._run("isaacsim") + mocks["_install_isaacsim"].assert_called_once() + + def test_isaacsim_still_installs_core_submodules(self): + mocks = self._run("isaacsim") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert set(installed) == set(CORE_ISAACLAB_SUBMODULES) + + # --- malformed tokens --- + + def test_malformed_bracket_token_emits_warning_and_installs_core(self): + with patch(f"{_INSTALL_MODULE}.print_warning") as mock_warn: + mocks = self._run("rl[rsl-rl") # missing closing bracket + mock_warn.assert_called_once() + warn_msg = mock_warn.call_args[0][0] + assert "rl[rsl-rl" in warn_msg + # Core submodules still installed. + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert set(installed) == set(CORE_ISAACLAB_SUBMODULES) + # No extra feature should be installed. + mocks["_install_extra_feature"].assert_not_called() + + def test_newton_with_selector_still_dispatches(self): + # The selector is forwarded to _install_extra_feature which emits the warning + # internally (that function is mocked here; the warning itself is tested separately). + mocks = self._run("newton[sim]") + mocks["_install_extra_feature"].assert_called_once_with("newton", "sim") + + # --- unknown token --- + + def test_unknown_token_emits_warning_and_installs_core(self): + with patch(f"{_INSTALL_MODULE}.print_warning") as mock_warn: + mocks = self._run("totally_unknown_package") + mock_warn.assert_called_once() + warn_msg = mock_warn.call_args[0][0] + assert "totally_unknown_package" in warn_msg + # Core submodules still installed. + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert set(installed) == set(CORE_ISAACLAB_SUBMODULES) + + # --- isaaclab is always first --- + + def test_isaaclab_is_first_in_submodules_for_all(self): + mocks = self._run("all") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert installed[0] == "isaaclab" + + def test_isaaclab_is_first_in_submodules_for_none(self): + mocks = self._run("none") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert installed[0] == "isaaclab" + + def test_isaaclab_is_first_when_mimic_added(self): + mocks = self._run("mimic") + installed = mocks["_install_isaaclab_submodules"].call_args[0][0] + assert installed[0] == "isaaclab" diff --git a/source/isaaclab/test/install_ci/test_install_workflow_training.py b/source/isaaclab/test/install_ci/test_install_workflow_training.py new file mode 100644 index 000000000000..b10c958f5ab5 --- /dev/null +++ b/source/isaaclab/test/install_ci/test_install_workflow_training.py @@ -0,0 +1,231 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""End-to-end installation and training workflow tests. + +Covers every documented installation path: + - uv × kitless (core-only, ``-i none``) + - uv × newton training (``-i newton,rl[rsl-rl]``) + - uv × ov + newton training (``-i newton,ov,rl[rsl-rl]``) + - conda × kitless (core-only, ``-i none``) + - conda × newton training (``-i newton,rl[rsl-rl]``) + +Tests in this file are intentionally slow and GPU-dependent. They are +gated behind pytest markers so they only run in the appropriate CI +environment: + + ``@pytest.mark.uv`` – routed to the uv-based Docker image + ``@pytest.mark.conda`` – routed to the conda-enabled Docker image + ``@pytest.mark.gpu`` – requires a GPU + ``@pytest.mark.slow`` – skipped in fast/smoke runs +""" + +from __future__ import annotations + +import shutil + +import pytest +from utils import Conda_Mixin, UV_Mixin + +# --------------------------------------------------------------------------- +# Shared training helper +# --------------------------------------------------------------------------- + +_TRAIN_CMD = [ + "train", + "--rl_library", + "rsl_rl", + "--task", + "Isaac-Cartpole-Direct-v0", + "--num_envs", + "16", + "presets=newton_mjwarp", + "--max_iterations", + "5", + "--headless", +] + + +def _assert_training_passed(result) -> None: + output = result.stdout + (result.stderr or "") + assert result.returncode == 0, f"Training failed (rc={result.returncode}):\n{output}" + assert "Traceback (most recent call last):" not in output, f"Training produced a traceback:\n{output}" + assert "Training time:" in output, f"Training did not report completion:\n{output}" + + +# --------------------------------------------------------------------------- +# uv-based tests +# --------------------------------------------------------------------------- + + +class TestUVWorkflow(UV_Mixin): + """Installation and training smoke tests using uv environments.""" + + @classmethod + def setup_class(cls): + if not shutil.which("uv"): + pytest.skip("uv is not available") + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.gpu + @pytest.mark.timeout(900) + def test_uv_none_installs_core_submodules(self, isaaclab_root): + """``./isaaclab.sh -i none`` installs all core submodules without extras.""" + try: + self.create_uv_env(isaaclab_root) + result = self.run_in_uv_env( + [str(self.cli_script), "-i", "none"], + cwd=isaaclab_root, + timeout=600, + ) + assert result.returncode == 0, f"isaaclab -i none failed:\n{result.stdout}\n{result.stderr}" + output = result.stdout + result.stderr + # All core submodules should be installed; no optional tokens should warn + assert "WARNING" not in output or "Unknown install token" not in output, ( + f"Unexpected warnings from -i none:\n{output}" + ) + # Verify core packages importable + for pkg in ("isaaclab", "isaaclab_assets", "isaaclab_tasks", "isaaclab_physx"): + r = self.run_in_uv_env( + [str(self.python), "-c", f"import {pkg}; print({pkg!r}, 'ok')"], + cwd=isaaclab_root, + timeout=60, + ) + assert r.returncode == 0, f"{pkg} not importable after -i none:\n{r.stdout}\n{r.stderr}" + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.gpu + @pytest.mark.timeout(1200) + def test_uv_newton_rsl_rl_trains_cartpole(self, isaaclab_root): + """``./isaaclab.sh -i newton,rl[rsl-rl]`` + training completes successfully.""" + try: + self.create_uv_env(isaaclab_root) + result = self.run_in_uv_env( + [str(self.cli_script), "-i", "newton,rl[rsl-rl]"], + cwd=isaaclab_root, + timeout=900, + ) + assert result.returncode == 0, f"isaaclab -i newton,rl[rsl-rl] failed:\n{result.stdout}\n{result.stderr}" + result = self.run_in_uv_env( + [str(self.cli_script)] + _TRAIN_CMD, + cwd=isaaclab_root, + timeout=600, + ) + _assert_training_passed(result) + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.gpu + @pytest.mark.timeout(1800) + def test_uv_newton_ov_rsl_rl_trains_cartpole(self, isaaclab_root): + """``./isaaclab.sh -i newton,ov,rl[rsl-rl]`` + training completes successfully.""" + try: + self.create_uv_env(isaaclab_root) + result = self.run_in_uv_env( + [str(self.cli_script), "-i", "newton,ov,rl[rsl-rl]"], + cwd=isaaclab_root, + timeout=1200, + ) + assert result.returncode == 0, f"isaaclab -i newton,ov,rl[rsl-rl] failed:\n{result.stdout}\n{result.stderr}" + result = self.run_in_uv_env( + [str(self.cli_script)] + _TRAIN_CMD, + cwd=isaaclab_root, + timeout=600, + ) + _assert_training_passed(result) + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.gpu + @pytest.mark.timeout(1800) + def test_uv_all_trains_cartpole(self, isaaclab_root): + """``./isaaclab.sh -i all`` (full install) + training completes successfully.""" + try: + self.create_uv_env(isaaclab_root) + result = self.run_in_uv_env( + [str(self.cli_script), "-i", "all"], + cwd=isaaclab_root, + timeout=1200, + ) + assert result.returncode == 0, f"isaaclab -i all failed:\n{result.stdout}\n{result.stderr}" + result = self.run_in_uv_env( + [str(self.cli_script)] + _TRAIN_CMD, + cwd=isaaclab_root, + timeout=600, + ) + _assert_training_passed(result) + finally: + self.destroy_uv_env() + + +# --------------------------------------------------------------------------- +# conda-based tests +# --------------------------------------------------------------------------- + + +class TestCondaWorkflow(Conda_Mixin): + """Installation and training smoke tests using conda environments.""" + + @classmethod + def setup_class(cls): + if not shutil.which("conda"): + pytest.skip("conda is not available") + + @pytest.mark.conda + @pytest.mark.slow + @pytest.mark.gpu + @pytest.mark.timeout(1200) + def test_conda_none_installs_core_submodules(self, isaaclab_root): + """conda + ``./isaaclab.sh -i none`` installs all core submodules without extras.""" + try: + self.create_conda_env(isaaclab_root) + result = self.run_in_conda_env( + [str(self.cli_script), "-i", "none"], + cwd=isaaclab_root, + timeout=900, + ) + assert result.returncode == 0, f"conda isaaclab -i none failed:\n{result.stdout}\n{result.stderr}" + for pkg in ("isaaclab", "isaaclab_assets", "isaaclab_tasks", "isaaclab_physx"): + r = self.run_in_conda_env( + [str(self.python), "-c", f"import {pkg}; print({pkg!r}, 'ok')"], + cwd=isaaclab_root, + timeout=60, + ) + assert r.returncode == 0, f"{pkg} not importable after conda -i none:\n{r.stdout}\n{r.stderr}" + finally: + self.destroy_conda_env() + + @pytest.mark.conda + @pytest.mark.slow + @pytest.mark.gpu + @pytest.mark.timeout(1800) + def test_conda_newton_rsl_rl_trains_cartpole(self, isaaclab_root): + """conda + ``./isaaclab.sh -i newton,rl[rsl-rl]`` + training completes successfully.""" + try: + self.create_conda_env(isaaclab_root) + result = self.run_in_conda_env( + [str(self.cli_script), "-i", "newton,rl[rsl-rl]"], + cwd=isaaclab_root, + timeout=1200, + ) + assert result.returncode == 0, ( + f"conda isaaclab -i newton,rl[rsl-rl] failed:\n{result.stdout}\n{result.stderr}" + ) + result = self.run_in_conda_env( + [str(self.cli_script)] + _TRAIN_CMD, + cwd=isaaclab_root, + timeout=600, + ) + _assert_training_passed(result) + finally: + self.destroy_conda_env() diff --git a/source/isaaclab/test/install_ci/test_isaaclabx_i_mimic.py b/source/isaaclab/test/install_ci/test_isaaclabx_i_mimic.py new file mode 100644 index 000000000000..0fc5fbc99bcf --- /dev/null +++ b/source/isaaclab/test/install_ci/test_isaaclabx_i_mimic.py @@ -0,0 +1,94 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Test the optional mimic submodule install (./isaaclab.sh -i mimic). + +``mimic`` is an optional submodule — it is not part of the always-installed +core set because its base dependencies (ipywidgets, h5py) are heavier than +the rest. Users who need imitation-learning workflows explicitly opt in with +``./isaaclab.sh -i mimic``. +""" + +from __future__ import annotations + +import shutil + +import pytest +from utils import UV_Mixin, find_isaaclab_root + + +class Test_Install_Mimic(UV_Mixin): + """./isaaclab.sh -i mimic: installs core + isaaclab_mimic.""" + + @classmethod + def setup_class(cls): + if not shutil.which("uv"): + pytest.skip("uv is not available") + + try: + import isaacsim # noqa: F401 + except ImportError: + if not (find_isaaclab_root() / "_isaac_sim").exists(): + pytest.skip("isaacsim is not importable and _isaac_sim link not found, skipping") + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(1800) + def test_mimic_importable_after_install(self, isaaclab_root): + """isaaclab_mimic is importable after ./isaaclab.sh -i mimic.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "mimic"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i mimic failed:\n{result.stdout}\n{result.stderr}" + + result = self.run_in_uv_env(["python", "-c", "import isaaclab_mimic; print('isaaclab_mimic ok')"]) + assert result.returncode == 0, f"import isaaclab_mimic failed:\n{result.stdout}\n{result.stderr}" + + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(1800) + def test_mimic_not_installed_by_none(self, isaaclab_root): + """isaaclab_mimic is absent after ./isaaclab.sh -i none (core only).""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "none"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i none failed:\n{result.stdout}\n{result.stderr}" + + result = self.run_in_uv_env(["python", "-c", "import isaaclab_mimic"]) + assert result.returncode != 0, "isaaclab_mimic should not be installed after -i none" + + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(1800) + def test_core_still_present_after_mimic_install(self, isaaclab_root): + """Core packages remain importable after ./isaaclab.sh -i mimic.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "mimic"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i mimic failed:\n{result.stdout}\n{result.stderr}" + + for pkg in ("isaaclab", "isaaclab_assets", "isaaclab_tasks", "isaaclab_rl"): + result = self.run_in_uv_env(["python", "-c", f"import {pkg}; print('{pkg} ok')"]) + assert result.returncode == 0, ( + f"import {pkg} failed after mimic install:\n{result.stdout}\n{result.stderr}" + ) + + finally: + self.destroy_uv_env() diff --git a/source/isaaclab/test/install_ci/test_isaaclabx_i_none.py b/source/isaaclab/test/install_ci/test_isaaclabx_i_none.py new file mode 100644 index 000000000000..471da4c8cfef --- /dev/null +++ b/source/isaaclab/test/install_ci/test_isaaclabx_i_none.py @@ -0,0 +1,120 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Test the core install (./isaaclab.sh -i none). + +``./isaaclab.sh -i none`` installs the always-on core set of submodules without +any optional submodules (mimic, teleop) or optional extra dependencies +(newton physics library, RL frameworks, visualizer backends, OV wheels). All core +packages must be importable after this install, and training with the default physics +preset must succeed. +""" + +from __future__ import annotations + +import shutil + +import pytest +from utils import UV_Mixin, find_isaaclab_root + +# Core packages that must be importable after ``-i none``. +_CORE_PACKAGES = [ + "isaaclab", + "isaaclab_assets", + "isaaclab_contrib", + "isaaclab_experimental", + "isaaclab_newton", + "isaaclab_ov", + "isaaclab_ovphysx", + "isaaclab_physx", + "isaaclab_rl", + "isaaclab_tasks", + "isaaclab_tasks_experimental", + "isaaclab_visualizers", +] + + +class Test_Install_None(UV_Mixin): + """./isaaclab.sh -i none: core set installed, no optional extras.""" + + @classmethod + def setup_class(cls): + if not shutil.which("uv"): + pytest.skip("uv is not available") + + try: + import isaacsim # noqa: F401 + except ImportError: + if not (find_isaaclab_root() / "_isaac_sim").exists(): + pytest.skip("isaacsim is not importable and _isaac_sim link not found, skipping") + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(1800) + def test_core_install_all_packages_importable(self, isaaclab_root): + """All core packages are importable after ./isaaclab.sh -i none.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "none"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i none failed:\n{result.stdout}\n{result.stderr}" + + for pkg in _CORE_PACKAGES: + result = self.run_in_uv_env(["python", "-c", f"import {pkg}; print('{pkg} ok')"]) + assert result.returncode == 0, f"import {pkg} failed:\n{result.stdout}\n{result.stderr}" + + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(1800) + def test_core_install_optional_submodules_not_installed(self, isaaclab_root): + """Optional submodules (mimic, teleop) are absent after -i none.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "none"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i none failed:\n{result.stdout}\n{result.stderr}" + + for pkg in ("isaaclab_mimic", "isaaclab_teleop"): + result = self.run_in_uv_env(["python", "-c", f"import {pkg}"]) + assert result.returncode != 0, f"{pkg} should not be installed after -i none" + + for pkg in ("ovrtx", "ovphysx"): + result = self.run_in_uv_env(["python", "-c", f"import {pkg}"]) + assert result.returncode != 0, f"{pkg} should not be installed after -i none" + + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.gpu + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(3600) + def test_core_install_physx_tests_pass(self, isaaclab_root): + """isaaclab_physx tests pass after core install (physx is always in the core set).""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "none"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i none failed:\n{result.stdout}\n{result.stderr}" + + test_dir = str(isaaclab_root / "source" / "isaaclab_physx" / "test") + result = self.run_in_uv_env( + ["python", "-m", "pytest", test_dir, "-sv", "--tb=short"], + cwd=isaaclab_root, + ) + output = result.stdout + result.stderr + assert result.returncode == 0, f"isaaclab_physx tests failed (rc={result.returncode}):\n{output}" + + finally: + self.destroy_uv_env() diff --git a/source/isaaclab/test/install_ci/test_isaaclabx_i_physx.py b/source/isaaclab/test/install_ci/test_isaaclabx_i_physx.py index 04bf2b346b23..b9699e9d902d 100644 --- a/source/isaaclab/test/install_ci/test_isaaclabx_i_physx.py +++ b/source/isaaclab/test/install_ci/test_isaaclabx_i_physx.py @@ -3,7 +3,12 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Test installing isaaclab_physx and running its test suite.""" +"""Test that isaaclab_physx is importable after a core install (./isaaclab.sh -i none). + +Under the new installation model, ``isaaclab_physx`` is part of the always-installed +core set. A plain ``./isaaclab.sh -i none`` (core only, no optional extras) is +therefore sufficient to make ``isaaclab_physx`` importable and to run its test suite. +""" from __future__ import annotations @@ -14,23 +19,18 @@ class Test_Install_Physx(UV_Mixin): - """Install ./isaaclab.sh -i physx and run the isaaclab_physx test suite.""" + """Core install (./isaaclab.sh -i none) makes isaaclab_physx importable.""" @classmethod def setup_class(cls): - # check if uv is available if not shutil.which("uv"): pytest.skip("uv is not available") - # check if isaacsim is importable - # or "_isaac_sim" link is present try: import isaacsim # noqa: F401 except ImportError: - print("[DEBUG] Module isaacsim is not importable") isaac_sim_link = find_isaaclab_root() / "_isaac_sim" if not isaac_sim_link.exists(): - print(f'[DEBUG] Link "{isaac_sim_link}" does not exist') pytest.skip("isaacsim is not importable and _isaac_sim link not found, skipping") @pytest.mark.uv @@ -38,17 +38,23 @@ def setup_class(cls): @pytest.mark.slow @pytest.mark.native @pytest.mark.timeout(3600) - def test_install_physx_and_run_tests(self, isaaclab_root): - """Install physx extension and run the isaaclab_physx test suite.""" + def test_core_install_includes_physx_and_runs_tests(self, isaaclab_root): + """./isaaclab.sh -i none installs the core set (including physx) and tests pass.""" try: self.create_uv_env(isaaclab_root) - # ./isaaclab.sh -i physx - result = self.run_in_uv_env([str(self.cli_script), "-i", "physx"], cwd=isaaclab_root) - assert result.returncode == 0, f"isaaclab -i physx failed:\n{result.stdout}\n{result.stderr}" + # Core install — physx is part of the always-installed set. + result = self.run_in_uv_env([str(self.cli_script), "-i", "none"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i none failed:\n{result.stdout}\n{result.stderr}" + + # Verify isaaclab_physx is importable. + result = self.run_in_uv_env( + ["python", "-c", "import isaaclab_physx; print('isaaclab_physx ok')"], + ) + assert result.returncode == 0, f"import isaaclab_physx failed:\n{result.stdout}\n{result.stderr}" - # Run isaaclab_physx test suite + # Run the isaaclab_physx test suite. test_dir = str(isaaclab_root / "source" / "isaaclab_physx" / "test") result = self.run_in_uv_env( ["python", "-m", "pytest", test_dir, "-sv", "--tb=short"], diff --git a/source/isaaclab/test/install_ci/test_isaaclabx_i_rl.py b/source/isaaclab/test/install_ci/test_isaaclabx_i_rl.py new file mode 100644 index 000000000000..076b4134c78e --- /dev/null +++ b/source/isaaclab/test/install_ci/test_isaaclabx_i_rl.py @@ -0,0 +1,202 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Integration tests for RL framework extra-feature installs. + +Each test installs the core set + a specific RL framework via +``./isaaclab.sh -i 'rl[]'`` and then verifies that +(a) the framework is importable and (b) a short training run succeeds. + +Valid selectors for the ``rl`` feature: + - ``rsl-rl`` → rsl-rl-lib + - ``skrl`` → skrl + - ``sb3`` → stable-baselines3 + - ``rl-games`` → rl-games (git dep) + - (no selector / ``all``) → all frameworks +""" + +from __future__ import annotations + +import shutil + +import pytest +from utils import UV_Mixin, find_isaaclab_root + +_TRAIN_SCRIPT = "scripts/reinforcement_learning/{framework}/train.py" + +# (selector, importable_package, train_script_dir, train_extra_args) +_RL_CONFIGS = [ + ("rsl-rl", "rsl_rl", "rsl_rl", ["presets=newton_mjwarp"]), + ("skrl", "skrl", "skrl", []), + ("sb3", "stable_baselines3", "sb3", []), +] + + +class Test_Install_RL_Frameworks(UV_Mixin): + """./isaaclab.sh -i 'rl[]' installs the RL framework extras.""" + + @classmethod + def setup_class(cls): + if not shutil.which("uv"): + pytest.skip("uv is not available") + + try: + import isaacsim # noqa: F401 + except ImportError: + if not (find_isaaclab_root() / "_isaac_sim").exists(): + pytest.skip("isaacsim is not importable and _isaac_sim link not found, skipping") + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(1800) + @pytest.mark.parametrize("selector,import_pkg,_train_dir,_train_args", _RL_CONFIGS) + def test_rl_framework_importable_after_install(self, isaaclab_root, selector, import_pkg, _train_dir, _train_args): + """./isaaclab.sh -i 'rl[]' makes the framework importable.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", f"rl[{selector}]"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i rl[{selector}] failed:\n{result.stdout}\n{result.stderr}" + + result = self.run_in_uv_env(["python", "-c", f"import {import_pkg}; print('{import_pkg} ok')"]) + assert result.returncode == 0, ( + f"import {import_pkg} failed after rl[{selector}]:\n{result.stdout}\n{result.stderr}" + ) + + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.gpu + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(3600) + def test_train_cartpole_rsl_rl(self, isaaclab_root): + """./isaaclab.sh -i 'newton,rl[rsl-rl]' then train Isaac-Cartpole-Direct-v0 with rsl_rl.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "newton,rl[rsl-rl]"], cwd=isaaclab_root) + assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}" + + result = self.run_in_uv_env( + [ + str(self.cli_script), + "-p", + "scripts/reinforcement_learning/rsl_rl/train.py", + "--task", + "Isaac-Cartpole-Direct-v0", + "--num_envs", + "64", + "presets=newton_mjwarp", + "--max_iterations", + "5", + "--headless", + ], + cwd=isaaclab_root, + ) + output = result.stdout + result.stderr + assert result.returncode == 0, f"rsl_rl training failed (rc={result.returncode}):\n{output}" + assert "Traceback (most recent call last):" not in output, f"rsl_rl training raised an exception:\n{output}" + + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.gpu + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(3600) + def test_train_cartpole_skrl(self, isaaclab_root): + """./isaaclab.sh -i 'newton,rl[skrl]' then train Isaac-Cartpole-Direct-v0 with skrl.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "newton,rl[skrl]"], cwd=isaaclab_root) + assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}" + + result = self.run_in_uv_env( + [ + str(self.cli_script), + "-p", + "scripts/reinforcement_learning/skrl/train.py", + "--task", + "Isaac-Cartpole-Direct-v0", + "--num_envs", + "64", + "presets=newton_mjwarp", + "--max_iterations", + "5", + "--headless", + ], + cwd=isaaclab_root, + ) + output = result.stdout + result.stderr + assert result.returncode == 0, f"skrl training failed (rc={result.returncode}):\n{output}" + assert "Traceback (most recent call last):" not in output, f"skrl training raised an exception:\n{output}" + + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.gpu + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(3600) + def test_train_cartpole_sb3(self, isaaclab_root): + """./isaaclab.sh -i 'newton,rl[sb3]' then train Isaac-Cartpole-Direct-v0 with sb3.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "newton,rl[sb3]"], cwd=isaaclab_root) + assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}" + + result = self.run_in_uv_env( + [ + str(self.cli_script), + "-p", + "scripts/reinforcement_learning/sb3/train.py", + "--task", + "Isaac-Cartpole-Direct-v0", + "--num_envs", + "64", + "presets=newton_mjwarp", + "--max_iterations", + "5", + "--headless", + ], + cwd=isaaclab_root, + ) + output = result.stdout + result.stderr + assert result.returncode == 0, f"sb3 training failed (rc={result.returncode}):\n{output}" + assert "Traceback (most recent call last):" not in output, f"sb3 training raised an exception:\n{output}" + + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(1800) + def test_rl_all_installs_all_frameworks(self, isaaclab_root): + """./isaaclab.sh -i 'rl' (no selector) installs all RL frameworks.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "rl"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i rl failed:\n{result.stdout}\n{result.stderr}" + + for pkg in ("rsl_rl", "skrl", "stable_baselines3"): + result = self.run_in_uv_env(["python", "-c", f"import {pkg}; print('{pkg} ok')"]) + assert result.returncode == 0, f"import {pkg} failed after rl[all]:\n{result.stdout}\n{result.stderr}" + + finally: + self.destroy_uv_env() diff --git a/source/isaaclab/test/install_ci/test_isaaclabx_i_visualizer.py b/source/isaaclab/test/install_ci/test_isaaclabx_i_visualizer.py new file mode 100644 index 000000000000..d22e8cf9ef4e --- /dev/null +++ b/source/isaaclab/test/install_ci/test_isaaclabx_i_visualizer.py @@ -0,0 +1,161 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Tests for visualizer backend extra-feature installs. + +``visualizer`` is an extra feature selector that reinstalls the already-present +``isaaclab_visualizers`` core package with specific backend extras: + + - ``./isaaclab.sh -i 'visualizer[rerun]'`` → rerun-sdk + newton[sim] + - ``./isaaclab.sh -i 'visualizer[viser]'`` → viser + newton[sim] + - ``./isaaclab.sh -i 'visualizer[newton]'`` → imgui-bundle + newton[sim] + - ``./isaaclab.sh -i visualizer`` → all backends (default) + +All backends also pull in the ``newton[sim]`` git dependency because the +Newton renderer underpins every visualizer implementation. +""" + +from __future__ import annotations + +import shutil + +import pytest +from utils import UV_Mixin, find_isaaclab_root + + +class Test_Install_Visualizer(UV_Mixin): + """./isaaclab.sh -i 'visualizer[]' installs the chosen visualizer extras.""" + + @classmethod + def setup_class(cls): + if not shutil.which("uv"): + pytest.skip("uv is not available") + + try: + import isaacsim # noqa: F401 + except ImportError: + if not (find_isaaclab_root() / "_isaac_sim").exists(): + pytest.skip("isaacsim is not importable and _isaac_sim link not found, skipping") + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(1800) + def test_visualizer_rerun_backend_importable(self, isaaclab_root): + """rerun-sdk is importable after ./isaaclab.sh -i 'visualizer[rerun]'.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "visualizer[rerun]"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i visualizer[rerun] failed:\n{result.stdout}\n{result.stderr}" + + result = self.run_in_uv_env(["python", "-c", "import rerun; print('rerun ok')"]) + assert result.returncode == 0, f"import rerun failed:\n{result.stdout}\n{result.stderr}" + + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(1800) + def test_visualizer_viser_backend_importable(self, isaaclab_root): + """viser is importable after ./isaaclab.sh -i 'visualizer[viser]'.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "visualizer[viser]"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i visualizer[viser] failed:\n{result.stdout}\n{result.stderr}" + + result = self.run_in_uv_env(["python", "-c", "import viser; print('viser ok')"]) + assert result.returncode == 0, f"import viser failed:\n{result.stdout}\n{result.stderr}" + + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(1800) + def test_visualizer_default_installs_all_backends(self, isaaclab_root): + """./isaaclab.sh -i visualizer (no selector) installs all visualizer backends.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "visualizer"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i visualizer failed:\n{result.stdout}\n{result.stderr}" + + for pkg in ("rerun", "viser"): + result = self.run_in_uv_env(["python", "-c", f"import {pkg}; print('{pkg} ok')"]) + assert result.returncode == 0, ( + f"import {pkg} failed after visualizer[all]:\n{result.stdout}\n{result.stderr}" + ) + + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(1800) + def test_visualizer_all_backends_pull_newton_sim(self, isaaclab_root): + """Every visualizer backend install also provides the newton package.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env([str(self.cli_script), "-i", "visualizer"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i visualizer failed:\n{result.stdout}\n{result.stderr}" + + result = self.run_in_uv_env(["python", "-c", "import newton; print('newton ok')"]) + assert result.returncode == 0, ( + f"import newton failed after visualizer install:\n{result.stdout}\n{result.stderr}" + ) + + finally: + self.destroy_uv_env() + + @pytest.mark.uv + @pytest.mark.gpu + @pytest.mark.slow + @pytest.mark.native + @pytest.mark.timeout(3600) + def test_train_with_rerun_visualizer(self, isaaclab_root): + """Training with --visualizer rerun works after ./isaaclab.sh -i 'newton,rl[rsl-rl],visualizer[rerun]'.""" + + try: + self.create_uv_env(isaaclab_root) + + result = self.run_in_uv_env( + [str(self.cli_script), "-i", "newton,rl[rsl-rl],visualizer[rerun]"], + cwd=isaaclab_root, + ) + assert result.returncode == 0, f"install failed:\n{result.stdout}\n{result.stderr}" + + result = self.run_in_uv_env( + [ + str(self.cli_script), + "-p", + "scripts/reinforcement_learning/rsl_rl/train.py", + "--task", + "Isaac-Cartpole-Direct-v0", + "--num_envs", + "64", + "presets=newton_mjwarp", + "--max_iterations", + "5", + "--headless", + ], + cwd=isaaclab_root, + ) + output = result.stdout + result.stderr + assert result.returncode == 0, f"training failed (rc={result.returncode}):\n{output}" + assert "Traceback (most recent call last):" not in output, f"training raised an exception:\n{output}" + + finally: + self.destroy_uv_env() diff --git a/source/isaaclab/test/install_ci/test_isaaclabx_uv_smoke.py b/source/isaaclab/test/install_ci/test_isaaclabx_uv_smoke.py index 8bff8426e1fe..9d8fefd370ce 100644 --- a/source/isaaclab/test/install_ci/test_isaaclabx_uv_smoke.py +++ b/source/isaaclab/test/install_ci/test_isaaclabx_uv_smoke.py @@ -36,38 +36,49 @@ def test_isaaclab_sh_uv_creates_env_with_python_312(self, isaaclab_root): @pytest.mark.uv @pytest.mark.timeout(200) - def test_isaaclab_install_assets(self, isaaclab_root): - """Run ./isaaclab.x -i 'assets' and verify isaaclab_assets is importable.""" + def test_isaaclab_none_installs_core_including_assets(self, isaaclab_root): + """Run ./isaaclab.x -i none and verify the core set (incl. assets) is importable. + + Under the new install model, ``isaaclab_assets`` is always installed as + part of the core set. Passing ``none`` installs the full core set without + any optional submodules or extra feature dependencies. + """ try: self.create_uv_env(isaaclab_root) - # ./isaaclab.x -i assets - result = self.run_in_uv_env([str(self.cli_script), "-i", "assets"], cwd=isaaclab_root) - assert result.returncode == 0, f"isaaclab -i assets failed:\n{result.stdout}\n{result.stderr}" + # ./isaaclab.x -i none — core set only, no optional extras + result = self.run_in_uv_env([str(self.cli_script), "-i", "none"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i none failed:\n{result.stdout}\n{result.stderr}" - # import isaaclab_assets - result = self.run_in_uv_env(["python", "-c", "import isaaclab_assets; print(isaaclab_assets.__version__)"]) - assert result.returncode == 0, f"import isaaclab_assets failed:\n{result.stdout}\n{result.stderr}" + # All core packages should be importable. + for pkg in ("isaaclab_assets", "isaaclab_tasks", "isaaclab_rl", "isaaclab_physx"): + result = self.run_in_uv_env(["python", "-c", f"import {pkg}; print('{pkg} ok')"]) + assert result.returncode == 0, f"import {pkg} failed:\n{result.stdout}\n{result.stderr}" finally: self.destroy_uv_env() @pytest.mark.uv @pytest.mark.timeout(300) - def test_isaaclab_newton_installs_isaaclab_newton(self, isaaclab_root): - """Run ./isaaclab.x -i 'newton' and verify isaaclab_newton is importable.""" + def test_isaaclab_newton_extra_installs_newton_sim(self, isaaclab_root): + """Run ./isaaclab.x -i newton and verify the newton[sim] extra is installed. + + ``newton`` is an extra feature selector: it reinstalls the already-present + core packages (``isaaclab_newton``, ``isaaclab_physx``, ``isaaclab_visualizers``) + with their newton extras, pulling in the ``newton[sim]`` git dependency. + """ try: self.create_uv_env(isaaclab_root) - # ./isaaclab.x -i newton + # ./isaaclab.x -i newton — installs core + newton extras result = self.run_in_uv_env([str(self.cli_script), "-i", "newton"], cwd=isaaclab_root) assert result.returncode == 0, f"isaaclab -i newton failed:\n{result.stdout}\n{result.stderr}" - # import isaaclab_newton - result = self.run_in_uv_env(["python", "-c", "import isaaclab_newton; print(isaaclab_newton.__version__)"]) - assert result.returncode == 0, f"import isaaclab_newton failed:\n{result.stdout}\n{result.stderr}" + # The newton[sim] extra should make the newton package importable. + result = self.run_in_uv_env(["python", "-c", "import newton; print('newton ok')"]) + assert result.returncode == 0, f"import newton failed:\n{result.stdout}\n{result.stderr}" finally: self.destroy_uv_env() diff --git a/source/isaaclab/test/install_ci/test_isaaclabx_uv_training.py b/source/isaaclab/test/install_ci/test_isaaclabx_uv_training.py index 9bd2bfa40f32..eae856c6a725 100644 --- a/source/isaaclab/test/install_ci/test_isaaclabx_uv_training.py +++ b/source/isaaclab/test/install_ci/test_isaaclabx_uv_training.py @@ -28,16 +28,19 @@ def setup_class(cls): @pytest.mark.skip(reason="Cartpole training fails in MuJoCo stiffness conversion.") @pytest.mark.timeout(1200) def test_install_and_train_cartpole(self, isaaclab_root): - """`isaaclab.x -i assets,tasks,rl[all],physx,newton,contrib` then train Isaac-Cartpole-Direct-v0""" + """``./isaaclab.sh -i newton,'rl[all]'`` then train Isaac-Cartpole-Direct-v0. + + Under the new install model, the core set (assets, tasks, physx, contrib, …) + is always installed. Only the optional extras (newton physics library and + RL frameworks) need to be explicitly requested. + """ try: self.create_uv_env(isaaclab_root) - # Install assets, tasks, rl[all], physx, newton, contrib - result = self.run_in_uv_env( - [str(self.cli_script), "-i", "assets,tasks,rl[all],physx,newton,contrib"], cwd=isaaclab_root - ) - assert result.returncode == 0, f"isaaclab -i failed:\n{result.stdout}\n{result.stderr}" + # Core set is always installed; only request optional extras. + result = self.run_in_uv_env([str(self.cli_script), "-i", "newton,rl[all]"], cwd=isaaclab_root) + assert result.returncode == 0, f"isaaclab -i newton,rl[all] failed:\n{result.stdout}\n{result.stderr}" # Run a short training result = self.run_in_uv_env( diff --git a/source/isaaclab/test/install_ci/utils.py b/source/isaaclab/test/install_ci/utils.py index 85055f26c0bc..4a24ce21bd97 100644 --- a/source/isaaclab/test/install_ci/utils.py +++ b/source/isaaclab/test/install_ci/utils.py @@ -242,3 +242,87 @@ def run_in_uv_env(self, cmd: list[str], **kwargs) -> subprocess.CompletedProcess activate = shlex.quote(str(self.env_path / "bin" / "activate")) shell_cmd = f"source {activate} && {escaped}" return run_cmd(["bash", "-c", shell_cmd], **kwargs) + + +def drop_keys(env: dict[str, str], keys: tuple[str, ...]) -> dict[str, str]: + """Return a copy of *env* with the given keys removed. + + Useful for stripping venv/conda activation markers from the environment + before creating a fresh isolated environment inside a test. + + Args: + env: Source environment dictionary (e.g. ``os.environ``). + keys: Variable names to exclude from the returned copy. + """ + return {k: v for k, v in env.items() if k not in keys} + + +class Conda_Mixin: + """Mixin providing conda virtual-environment helpers for test classes. + + Pair with :class:`UVMixin` or use standalone for conda-only tests. + Requires ``conda`` (or ``mamba``) to be on ``PATH``. + + Usage:: + + class TestFoo(Conda_Mixin, unittest.TestCase): + def setUp(self): + self.create_conda_env(find_isaaclab_root()) + + def tearDown(self): + self.destroy_conda_env() + """ + + @property + def cli_script(self) -> Path: + """Path to ``isaaclab.sh`` (or ``.bat``) inside the repository root.""" + root: Path = self._isaaclab_root # type: ignore[attr-defined] + return root / ("isaaclab.bat" if _IS_WINDOWS else "isaaclab.sh") + + def create_conda_env(self, isaaclab_root: Path, env_name: str = "", python_version: str = "3.12") -> None: + """Create an isolated conda environment. + + Args: + isaaclab_root: Path to the IsaacLab repository root. + env_name: Name for the conda environment. A unique name based on the + test id is generated automatically when left empty. + python_version: Python version to install in the environment. + """ + if not env_name: + import uuid + + env_name = f"isaaclab_ci_{uuid.uuid4().hex[:8]}" + self._conda_env_name = env_name + self._isaaclab_root = isaaclab_root + + result = run_cmd( + ["conda", "create", "-n", env_name, f"python={python_version}", "-y", "--quiet"], + env=drop_keys(dict(os.environ), ("VIRTUAL_ENV", "CONDA_DEFAULT_ENV", "CONDA_PREFIX")), + ) + assert result.returncode == 0, f"conda env creation failed:\n{result.stdout}\n{result.stderr}" + + # Resolve the python executable inside the new conda env. + conda_prefix_result = run_cmd( + ["conda", "run", "-n", env_name, "python", "-c", "import sys; print(sys.executable)"], + env=drop_keys(dict(os.environ), ("VIRTUAL_ENV", "CONDA_DEFAULT_ENV", "CONDA_PREFIX")), + ) + assert conda_prefix_result.returncode == 0, ( + f"Could not resolve conda python:\n{conda_prefix_result.stdout}\n{conda_prefix_result.stderr}" + ) + self.python = Path(conda_prefix_result.stdout.strip()) + + def destroy_conda_env(self) -> None: + """Remove the conda environment created by :meth:`create_conda_env`.""" + if hasattr(self, "_conda_env_name"): + run_cmd(["conda", "env", "remove", "-n", self._conda_env_name, "-y", "--quiet"]) + + def run_in_conda_env(self, cmd: list[str], **kwargs) -> subprocess.CompletedProcess: + """Run *cmd* inside the conda environment. + + Args: + cmd: Command and arguments to execute. + **kwargs: Extra keyword arguments forwarded to :func:`run_cmd`. + """ + escaped = " ".join(shlex.quote(str(a)) for a in cmd) + shell_cmd = f"conda run -n {shlex.quote(self._conda_env_name)} --no-capture-output {escaped}" + return run_cmd(["bash", "-c", shell_cmd], **kwargs) diff --git a/source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py b/source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py index 0319bf80ef1a..aba7a6348b61 100644 --- a/source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py +++ b/source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py @@ -712,6 +712,7 @@ def test_output_equal_to_usd_camera_intrinsics(setup_simulation, height, width): del camera_usd, camera_warp +@pytest.mark.flaky(max_runs=3, min_passes=1) @pytest.mark.isaacsim_ci def test_output_equal_to_usd_camera_when_intrinsics_set(setup_simulation): """Test that the output of the ray caster camera is equal to the output of the usd camera when both are placed diff --git a/source/isaaclab_mimic/changelog.d/kellyguo11-fix-modular-install.minor.rst b/source/isaaclab_mimic/changelog.d/kellyguo11-fix-modular-install.minor.rst new file mode 100644 index 000000000000..4cc6d862d135 --- /dev/null +++ b/source/isaaclab_mimic/changelog.d/kellyguo11-fix-modular-install.minor.rst @@ -0,0 +1,7 @@ +Changed +^^^^^^^ + +* Moved ``robomimic`` from an opt-in extra (``isaaclab_mimic[robomimic]``) to a + required dependency of :mod:`isaaclab_mimic` on Linux (via a ``sys_platform`` + marker). ``robomimic`` is now installed automatically whenever + ``isaaclab_mimic`` is installed on Linux; no extra selector is needed. diff --git a/source/isaaclab_mimic/setup.py b/source/isaaclab_mimic/setup.py index 279a7a0a248d..7b1aa19f6b79 100644 --- a/source/isaaclab_mimic/setup.py +++ b/source/isaaclab_mimic/setup.py @@ -5,7 +5,6 @@ """Installation script for the 'isaaclab_mimic' python package.""" -import itertools import os import platform @@ -30,17 +29,10 @@ if platform.machine() != "aarch64": INSTALL_REQUIRES.append("nvidia-srl-usd-to-urdf") -# Extra dependencies for IL agents -EXTRAS_REQUIRE = {"robomimic": []} - -# Check if the platform is Linux and add the dependency +# robomimic has no Windows/macOS wheels; only add it on Linux if platform.system() == "Linux": - EXTRAS_REQUIRE["robomimic"].append("robomimic@git+https://github.com/ARISE-Initiative/robomimic.git@v0.4.0") + INSTALL_REQUIRES.append("robomimic @ git+https://github.com/ARISE-Initiative/robomimic.git@v0.4.0") -# Cumulation of all extra-requires -EXTRAS_REQUIRE["all"] = list(itertools.chain.from_iterable(EXTRAS_REQUIRE.values())) -# Remove duplicates in the all list to avoid double installations -EXTRAS_REQUIRE["all"] = list(set(EXTRAS_REQUIRE["all"])) # Installation operation setup( @@ -53,7 +45,6 @@ description=EXTENSION_TOML_DATA["package"]["description"], keywords=EXTENSION_TOML_DATA["package"]["keywords"], install_requires=INSTALL_REQUIRES, - extras_require=EXTRAS_REQUIRE, license="Apache-2.0", include_package_data=True, python_requires=">=3.12", diff --git a/tools/run_install_ci.py b/tools/run_install_ci.py index ba2dedd1fe60..e233326ffe45 100755 --- a/tools/run_install_ci.py +++ b/tools/run_install_ci.py @@ -51,6 +51,7 @@ import subprocess import sys import time +import uuid from pathlib import Path _DIM = "\033[2m" @@ -151,63 +152,142 @@ def _find_repo_root() -> Path: # Docker mode +def _build_image( + repo_root: Path, + dockerfile: Path, + image_tag: str, + build_args: dict[str, str], + no_cache: bool, +) -> int: + """Build a Docker image, returning the exit code.""" + build_cmd = ["docker", "build", "--progress=plain"] + for key, value in build_args.items(): + build_cmd.extend(["--build-arg", f"{key}={value}"]) + build_cmd.extend(["-f", str(dockerfile), "-t", image_tag]) + if no_cache: + build_cmd.append("--no-cache") + build_cmd.append(str(repo_root)) + result = run_cmd(build_cmd, check=False, stream=True) + return result.returncode + + +def _prepare_results_dir(results_dir: str) -> Path: + """Prepare a host directory for Docker-copied test results. + + Args: + results_dir: Host directory where the JUnit XML file should be copied. + + Returns: + Path to the expected host JUnit XML file. + """ + results_abs = Path(results_dir).resolve() + results_abs.mkdir(parents=True, exist_ok=True) + + # Keep the directory writable across repeated self-hosted runner jobs. The + # actual XML is copied out with ``docker cp`` after the container exits, so + # pytest never has to open a bind-mounted host file from inside Docker. + try: + results_abs.chmod(0o777) + except OSError as exc: + print(f"Warning: could not chmod results directory {results_abs}: {exc}", file=sys.stderr) + + results_xml = results_abs / "results.xml" + if results_xml.exists() or results_xml.is_symlink(): + try: + if results_xml.is_dir(): + shutil.rmtree(results_xml) + else: + results_xml.unlink() + except OSError as exc: + print(f"Warning: could not remove stale results file {results_xml}: {exc}", file=sys.stderr) + + return results_xml + + +def _copy_junit_xml(container_name: str, container_results_xml: str, host_results_xml: Path) -> None: + """Copy JUnit XML from a completed Docker container to the host. + + Args: + container_name: Name of the Docker container to copy from. + container_results_xml: Path to the JUnit XML file inside the container. + host_results_xml: Host path where the JUnit XML file should be stored. + """ + copy_cmd = ["docker", "cp", f"{container_name}:{container_results_xml}", str(host_results_xml)] + copy_result = run_cmd(copy_cmd, check=False, stream=True) + if copy_result.returncode != 0: + print( + f"Warning: could not copy JUnit XML from {container_name}:{container_results_xml} to {host_results_xml}.", + file=sys.stderr, + ) + return + + try: + host_results_xml.chmod(0o666) + except OSError as exc: + print(f"Warning: could not chmod results file {host_results_xml}: {exc}", file=sys.stderr) + + def _cmd_docker(args: argparse.Namespace) -> int: """Build the Docker image and run tests inside the container based on *args*.""" repo_root = _find_repo_root() - dockerfile = repo_root / "docker" / "Dockerfile.installci" - image_tag = f"isaaclab-installci:{args.base_image.replace(':', '-').replace('/', '-')}" - - # Build the Docker image - build_cmd = [ - "docker", - "build", - "--build-arg", - f"BASE_IMAGE={args.base_image}", - "-f", - str(dockerfile), - "-t", - image_tag, - "--progress=plain", - ] - - if args.no_cache: - build_cmd.append("--no-cache") - build_cmd.append(str(repo_root)) - - result = run_cmd(build_cmd, check=False, stream=True) - if result.returncode != 0: - print(f"Docker build failed (exit {result.returncode})") - return result.returncode + # Build the uv base image first. + _install_ci_dir = repo_root / "source" / "isaaclab" / "test" / "install_ci" + uv_dockerfile = _install_ci_dir / "Dockerfile.installci" + uv_tag = f"isaaclab-installci:{args.base_image.replace(':', '-').replace('/', '-')}" + + rc = _build_image(repo_root, uv_dockerfile, uv_tag, {"BASE_IMAGE": args.base_image}, args.no_cache) + if rc != 0: + print(f"Docker build (uv base) failed (exit {rc})") + return rc + print(f"Docker uv base image built: {uv_tag}") + + # If conda mode, build the conda layer on top. + if getattr(args, "conda", False): + conda_dockerfile = _install_ci_dir / "Dockerfile.installci-conda" + image_tag = f"{uv_tag}-conda" + rc = _build_image(repo_root, conda_dockerfile, image_tag, {"UV_IMAGE": uv_tag}, args.no_cache) + if rc != 0: + print(f"Docker build (conda layer) failed (exit {rc})") + return rc + print(f"Docker conda image built: {image_tag}") + else: + image_tag = uv_tag - print(f"Docker image built successfully: {image_tag}") + host_results_xml: Path | None = None + container_name: str | None = None + container_results_xml = "/tmp/isaaclab-installci-results.xml" + if args.results_dir: + host_results_xml = _prepare_results_dir(args.results_dir) + if not args.shell: + container_name = f"isaaclab-installci-{os.getpid()}-{uuid.uuid4().hex[:8]}" # Run - docker_run_cmd: list[str] = [ - "docker", - "run", - "--rm", - "--network=host", - ] + docker_run_cmd: list[str] = ["docker", "run"] + if container_name: + docker_run_cmd.extend(["--name", container_name]) + else: + docker_run_cmd.append("--rm") + docker_run_cmd.append("--network=host") if args.gpu: docker_run_cmd.extend(["--gpus", "all"]) - # Persist pip and uv caches across runs via named Docker volumes + # Persist pip and uv caches across runs via named Docker volumes. + # The container runs as the non-root 'isaaclab' user (uid 1000), so caches + # must live under /home/isaaclab rather than /root. if not args.no_pip_cache: - docker_run_cmd.extend(["-v", "isaaclab-installci-pip-cache:/root/.cache/pip"]) + docker_run_cmd.extend(["-v", "isaaclab-installci-pip-cache:/home/isaaclab/.cache/pip"]) if not args.no_uv_cache: - docker_run_cmd.extend(["-v", "isaaclab-installci-uv-cache:/root/.cache/uv"]) + docker_run_cmd.extend(["-v", "isaaclab-installci-uv-cache:/home/isaaclab/.cache/uv"]) # Pass environment variables docker_run_cmd.extend(["-e", "OMNI_KIT_ACCEPT_EULA=Y"]) docker_run_cmd.extend(["-e", "ACCEPT_EULA=Y"]) - if args.results_dir: - results_abs = Path(args.results_dir).resolve() - results_abs.mkdir(parents=True, exist_ok=True) - docker_run_cmd.extend(["-v", f"{results_abs}:/tmp/results"]) + if args.results_dir and args.shell and host_results_xml is not None: + docker_run_cmd.extend(["-v", f"{host_results_xml.parent}:/tmp/results"]) if args.wheel: wheel_abs = Path(args.wheel).resolve() @@ -222,7 +302,7 @@ def _cmd_docker(args: argparse.Namespace) -> int: # Test execution mode pytest_args = args.pytest_args or ["--tb=short"] if args.results_dir: - pytest_args = ["--junitxml=/tmp/results/results.xml"] + pytest_args + pytest_args = [f"--junitxml={container_results_xml}"] + pytest_args docker_run_cmd.extend([image_tag] + pytest_args) print(f"{_MAGENTA}[COMMAND] {' '.join(docker_run_cmd)}{_RESET}") @@ -232,7 +312,14 @@ def _cmd_docker(args: argparse.Namespace) -> int: ret = subprocess.call(docker_run_cmd, timeout=5400) except subprocess.TimeoutExpired: print("Docker run timed out after 90 minutes", file=sys.stderr) + if container_name: + run_cmd(["docker", "kill", container_name], check=False, stream=True) ret = 124 + finally: + if container_name: + if host_results_xml is not None: + _copy_junit_xml(container_name, container_results_xml, host_results_xml) + run_cmd(["docker", "rm", "-f", container_name], check=False, stream=True) elapsed = time.monotonic() - t0 print(f"{_MAGENTA}[{elapsed:.1f}s]{_RESET}") return ret @@ -279,6 +366,7 @@ def main() -> int: docker options: --base-image IMAGE Docker base image (default: ubuntu:24.04) --gpu Pass --gpus all to docker run + --conda Build and use a conda-enabled image (layered on the uv base) --shell Drop into interactive bash instead of running tests --no-cache Build Docker image without layer cache --no-pip-cache Disable persistent pip cache volume @@ -296,7 +384,8 @@ def main() -> int: %(prog)s docker # run all tests in Docker %(prog)s docker --base-image ubuntu:22.04 -- -vs -k "testname" # custom base image %(prog)s docker --gpu # GPU support (--gpus all) - %(prog)s docker -- -m uv # filter by marker + %(prog)s docker --gpu -- -m uv # uv tests only + %(prog)s docker --gpu --conda -- -m conda # conda tests (conda image) %(prog)s docker --gpu -- -m "slow and gpu" # combine markers with GPU %(prog)s docker --gpu -- -m nvbugs_5968136 # filter by bug ID %(prog)s docker --shell # drop into shell for debugging @@ -315,6 +404,11 @@ def main() -> int: help="Docker base image (default: ubuntu:24.04)", ) docker_p.add_argument("--gpu", action="store_true", help="Pass --gpus all to docker run") + docker_p.add_argument( + "--conda", + action="store_true", + help="Build and use a conda-enabled Docker image (layered on top of the uv base image)", + ) docker_p.add_argument( "--shell", action="store_true", help="Drop into an interactive bash shell instead of running tests" )