pow-cli 0.2.0rc1__tar.gz → 0.2.0rc2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/CHANGELOG.md +25 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/PKG-INFO +1 -1
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/cli/asset.py +0 -1
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/cli/init.py +19 -28
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/cli/lint.py +0 -2
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/cli/main.py +4 -2
- pow_cli-0.2.0rc2/pow_cli/cli/ros.py +155 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/cli/run.py +0 -2
- pow_cli-0.2.0rc2/pow_cli/cli/sim.py +67 -0
- pow_cli-0.2.0rc2/pow_cli/common/prompt.py +136 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/core/models/pow_config.py +74 -11
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/core/ros_manager.py +77 -129
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/core/runner.py +113 -13
- pow_cli-0.2.0rc2/pow_cli/data/pow.template.toml +19 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/docker/Dockerfile.simros_jazzy +99 -13
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pyproject.toml +6 -1
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/tests/test_config.py +69 -13
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/tests/test_init_cmd.py +30 -7
- pow_cli-0.2.0rc2/tests/test_prompt.py +198 -0
- pow_cli-0.2.0rc2/tests/test_ros_cmd.py +154 -0
- pow_cli-0.2.0rc2/tests/test_ros_manager.py +268 -0
- pow_cli-0.2.0rc2/tests/test_runner.py +299 -0
- pow_cli-0.2.0rc2/tests/test_sim_cmd.py +71 -0
- pow_cli-0.2.0rc1/.vscode/launch.json +0 -49
- pow_cli-0.2.0rc1/.vscode/settings.json +0 -673
- pow_cli-0.2.0rc1/.vscode/tasks.json +0 -14
- pow_cli-0.2.0rc1/pow_cli/cli/ros.py +0 -41
- pow_cli-0.2.0rc1/pow_cli/data/pow.template.toml +0 -28
- pow_cli-0.2.0rc1/pow_cli/docker/Dockerfile.simros_humble +0 -200
- pow_cli-0.2.0rc1/tests/test_ros_manager.py +0 -113
- pow_cli-0.2.0rc1/tests/test_runner.py +0 -88
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/.gitignore +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/LICENSE +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/NOTICE +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/README.md +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/__init__.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/cli/__init__.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/cli/check.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/cli/python.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/common/__init__.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/common/utils.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/core/__init__.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/core/asset_manager.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/core/initializer.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/core/linter.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/core/models/asset_profile.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/core/models/system_config.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/data/asset-registry/isaacsim_assets_5_1_0.toml +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/data/asset-registry/omniverse_assets.toml +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/data/example/asset-profile.example.toml +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/data/gitignore.template +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/data/system.template.toml +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/pow_cli/main.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/scripts/check_assets_urls.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/tests/test_check_cmd.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/tests/test_initializer.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/tests/test_verify_isaacsim.py +0 -0
- {pow_cli-0.2.0rc1 → pow_cli-0.2.0rc2}/tests/test_version.py +0 -0
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `pow-cli` package will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.0-rc.2] - 2026-07-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`pow sim`** — run Isaac Sim from any directory, with no project required: no `pyproject.toml` check and `pow.toml` is never read. Launches the default version (`5.1.0`, override with `-v/--version`) and forwards raw arguments straight to `isaac-sim.sh` (`pow sim -- --no-window`). The ROS 2 bridge environment is loaded by default (`--ros jazzy`); pass `--no-ros` to launch with the inherited environment instead
|
|
10
|
+
- **`pow ros build`** — build the custom image from `ros_dockerfile` without re-running `pow init`; builds the `pow_simros_jazzy` base image first if it is missing, and supports `--no-cache`. `pow ros` is now a command group (bare `pow ros [args...]` usage is unchanged; `pow ros launch` is the explicit form)
|
|
11
|
+
- **`ros_bridge` config in `pow.toml`** — choose which Isaac Sim internal ROS 2 bridge libs to load (`jazzy` or `humble`, default `jazzy`), i.e. which `exts/isaacsim.ros2.bridge/<distro>/lib` is added to `LD_LIBRARY_PATH` when `enable_ros = true`. Overridable per profile. Previously the bridge was inferred from the host Ubuntu version with no way to override it; an unsupported value is now rejected with a clear error
|
|
12
|
+
- **Tab completion for the `pow init` ROS workspace path** — step 6's "Path to clone IsaacSim-ros_workspaces" prompt now completes filesystem paths on <kbd>Tab</kbd> like a shell: unique directories complete inline (no stray trailing space, so the next segment can be typed straight away), ambiguous prefixes list candidates, and `~` is kept in the stored path. Trailing separators and whitespace are trimmed before the value is written to `pow.toml`
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **Breaking (vs 0.2.0-rc.1):** the `ros_container_name` config key was renamed to `ros_docker_image`. The container name is no longer configured directly — it is derived from the image name (`/` and `:` replaced with `_`), so default setups now get a container named `pow_simros_jazzy` instead of `pow_simros`
|
|
17
|
+
- **Breaking (vs 0.2.0-rc.1):** ROS 2 Humble support was removed — the ROS workspace and docker integration now support Jazzy only (`Dockerfile.simros_humble` deleted). Isaac Sim itself still runs on Ubuntu 22.04 and 24.04; Jazzy workspaces build on both
|
|
18
|
+
- **`pow init` no longer builds the Isaac Sim ROS workspace.** Step 6 only clones `IsaacSim-ros_workspaces`; the ROS 2 bridge comes from Isaac Sim's own prebuilt libs (selected by `ros_bridge`), so init is much faster and no longer runs a Docker build for the workspace. It reports the bridge distro in use instead of a build result
|
|
19
|
+
- `pow init` no longer creates the `scripts/`, `.assets/`, and `standalone/` project folders (only `exts/`, `.modules/`, `usda/`). A manually created `scripts/` folder is still mounted into the ROS container when present
|
|
20
|
+
- The `pow_simros_jazzy` image now resolves workspace dependencies with `rosdep install` at build time instead of building the workspace, and source-only packages (`topic_based_ros2_control`) are staged in `/opt/pow/extra_src` and copied into `/jazzy_ws/src` at container start, so the runtime volume mount no longer shadows them
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`rosdep install` failed inside the Jazzy container** — three causes, all fixed (requires rebuilding the image): the Ubuntu 24.04 base image's stock `ubuntu` user took UID 1000 and collided with the mapped host user, so `HOME` pointed at `/home/ubuntu` while the host `~/.ros` was mounted at `/home/hostuser/.ros`; the per-user rosdep cache was empty because `rosdep update` had run as root at build time, giving "your rosdep installation has not been initialized"; and `ros-jazzy-ros-testing` was missing from the image
|
|
25
|
+
- **`cpu_performance_mode` asked for the sudo password on every launch** — `pow run` / `pow python` now read the current CPU governor from `/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor` first and run no `sudo` at all when it is already `performance`, so the password is normally needed once instead of every launch. When sudo credentials are still cached the governor is set without the "requires sudo" notice, and hosts without `cpupower` or without cpufreq support now report and skip rather than prompting for a password to no purpose. The interactive prompt itself is unchanged when the governor really has to be switched
|
|
26
|
+
- **`ros2` tab completion inside the `pow_simros` container** — the entrypoint sourced ROS before exec'ing bash, which carried environment variables but not bash completion functions. Interactive shells now re-source `/opt/ros/jazzy/setup.bash` and the workspace overlay via `/etc/bash.bashrc`, so `ros2` / `colcon` autocomplete works without manually sourcing `setup.bash` (requires rebuilding the image)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
5
30
|
## [0.2.0-rc.1] - 2026-06-14
|
|
6
31
|
|
|
7
32
|
### Added
|
|
@@ -6,11 +6,10 @@ from pathlib import Path
|
|
|
6
6
|
import click
|
|
7
7
|
from rich.panel import Panel
|
|
8
8
|
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
|
|
9
|
-
from rich.prompt import Confirm
|
|
10
|
-
from rich.text import Text
|
|
9
|
+
from rich.prompt import Confirm
|
|
11
10
|
|
|
11
|
+
from ..common.prompt import ask_path
|
|
12
12
|
from ..common.utils import console
|
|
13
|
-
from ..core.models.pow_config import PowConfig
|
|
14
13
|
from ..core.initializer import Initializer
|
|
15
14
|
from ..core.ros_manager import RosManager
|
|
16
15
|
|
|
@@ -63,7 +62,7 @@ def _step1_check_config(global_dir_name: str) -> bool:
|
|
|
63
62
|
"""Print config header and verify pyproject.toml exists. Return False to abort."""
|
|
64
63
|
if not Path("pyproject.toml").exists():
|
|
65
64
|
console.print(
|
|
66
|
-
"\n[bold red][1/
|
|
65
|
+
"\n[bold red][1/10] ❌ Error:[/bold red] pyproject.toml not found. "
|
|
67
66
|
"Please run this command in a valid project directory."
|
|
68
67
|
)
|
|
69
68
|
return False
|
|
@@ -260,7 +259,7 @@ def _step6_ros_integration(
|
|
|
260
259
|
ws_path = forced_ws
|
|
261
260
|
console.print(f" Using existing workspace path from pow.toml: [bold green]{ws_path}[/bold green]")
|
|
262
261
|
else:
|
|
263
|
-
ws_path =
|
|
262
|
+
ws_path = ask_path(
|
|
264
263
|
" Path to clone IsaacSim-ros_workspaces",
|
|
265
264
|
default=default_ws,
|
|
266
265
|
)
|
|
@@ -277,28 +276,19 @@ def _step6_ros_integration(
|
|
|
277
276
|
console.print(f" [dim]Workspace path:[/dim] {display_path}")
|
|
278
277
|
|
|
279
278
|
ros_cloned = False
|
|
280
|
-
ros_already_built = False
|
|
281
279
|
|
|
282
280
|
def ros_status_callback(state):
|
|
283
|
-
nonlocal ros_cloned
|
|
281
|
+
nonlocal ros_cloned
|
|
284
282
|
if state == "cloning":
|
|
285
283
|
ros_cloned = True
|
|
286
284
|
status.update("[bold green]Cloning Isaac Sim ROS workspace...")
|
|
287
285
|
elif state == "existed":
|
|
288
|
-
status.update("[bold yellow]Isaac Sim ROS workspace already exists.
|
|
289
|
-
elif state == "built":
|
|
290
|
-
ros_already_built = True
|
|
291
|
-
status.update("[bold yellow]Docker build already complete.")
|
|
292
|
-
elif state == "building":
|
|
293
|
-
status.update("[bold green]Docker build: Isaac Sim ROS workspace...")
|
|
294
|
-
elif state.startswith("building:"):
|
|
295
|
-
line = state[len("building:"):]
|
|
296
|
-
status.update(f"[bold green]Docker build:[/bold green] [dim]{line[:80]}[/dim]")
|
|
286
|
+
status.update("[bold yellow]Isaac Sim ROS workspace already exists.")
|
|
297
287
|
|
|
298
288
|
# Resolve the tilde path for actual filesystem operations
|
|
299
289
|
resolved_ws = Path(ws_path).expanduser()
|
|
300
290
|
|
|
301
|
-
|
|
291
|
+
ros_setup_failed = False
|
|
302
292
|
|
|
303
293
|
with console.status("Preparing ROS workspace...") as status:
|
|
304
294
|
try:
|
|
@@ -307,11 +297,11 @@ def _step6_ros_integration(
|
|
|
307
297
|
ws_path=resolved_ws,
|
|
308
298
|
)
|
|
309
299
|
except Exception as e:
|
|
310
|
-
|
|
300
|
+
ros_setup_failed = True
|
|
311
301
|
console.print(f" [bold red]❌ ROS Setup Error:[/bold red] {e}")
|
|
312
302
|
|
|
313
|
-
if
|
|
314
|
-
console.print(" [yellow]⊖[/yellow] Skipping remaining steps due to ROS
|
|
303
|
+
if ros_setup_failed:
|
|
304
|
+
console.print(" [yellow]⊖[/yellow] Skipping remaining steps due to ROS setup error.")
|
|
315
305
|
raise SystemExit(1)
|
|
316
306
|
|
|
317
307
|
if ros_cloned:
|
|
@@ -319,11 +309,12 @@ def _step6_ros_integration(
|
|
|
319
309
|
else:
|
|
320
310
|
console.print(f" [yellow]✔[/yellow] IsaacSim-ros_workspaces already available in [dim]{display_path}[/dim]")
|
|
321
311
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
312
|
+
bridge_distro = ros_mgr.config.ros_bridge
|
|
313
|
+
console.print(
|
|
314
|
+
f" [green]✔[/green] ROS bridge: [bold]{bridge_distro}[/bold] "
|
|
315
|
+
f"(ros_bridge in pow.toml, host Ubuntu {ros_res['ubuntu_version']}) "
|
|
316
|
+
f"via Isaac Sim internal libs."
|
|
317
|
+
)
|
|
327
318
|
|
|
328
319
|
# Build pow_simros Docker image
|
|
329
320
|
simros_already_built = False
|
|
@@ -341,7 +332,7 @@ def _step6_ros_integration(
|
|
|
341
332
|
|
|
342
333
|
with console.status("Building pow_simros image...") as simros_status:
|
|
343
334
|
try:
|
|
344
|
-
|
|
335
|
+
ros_mgr.build_simros_image(
|
|
345
336
|
status_callback=simros_status_callback,
|
|
346
337
|
ws_path=resolved_ws,
|
|
347
338
|
)
|
|
@@ -359,7 +350,7 @@ def _step6_ros_integration(
|
|
|
359
350
|
# Build custom ROS image layered on top of pow_simros_<distro>
|
|
360
351
|
custom_dockerfile = ros_mgr.config.ros_dockerfile
|
|
361
352
|
if custom_dockerfile:
|
|
362
|
-
custom_image = ros_mgr.config.
|
|
353
|
+
custom_image = ros_mgr.config.ros_docker_image
|
|
363
354
|
|
|
364
355
|
def custom_status_callback(state):
|
|
365
356
|
if state == "custom_building":
|
|
@@ -387,7 +378,7 @@ def _step6_ros_integration(
|
|
|
387
378
|
def _step7_project_structure(initializer: Initializer):
|
|
388
379
|
"""Create local project folders and .gitignore."""
|
|
389
380
|
console.print("[bold blue][7/10] 🏗️ Project Structure:[/bold blue] Creating local folders...")
|
|
390
|
-
local_folders = ["exts", "
|
|
381
|
+
local_folders = ["exts", ".modules", "usda"]
|
|
391
382
|
|
|
392
383
|
with Progress(
|
|
393
384
|
SpinnerColumn(),
|
|
@@ -5,8 +5,6 @@ from pathlib import Path
|
|
|
5
5
|
import click
|
|
6
6
|
from rich.markup import escape
|
|
7
7
|
from rich.panel import Panel
|
|
8
|
-
from rich.table import Table
|
|
9
|
-
from rich.text import Text
|
|
10
8
|
|
|
11
9
|
from ..common.utils import console
|
|
12
10
|
from ..core.linter import AliasConfig, lint_file, fix_file, scan_directory
|
|
@@ -7,7 +7,8 @@ from .init import init_cmd
|
|
|
7
7
|
from .check import check_cmd
|
|
8
8
|
from .lint import lint_group
|
|
9
9
|
from .run import run_cmd
|
|
10
|
-
from .
|
|
10
|
+
from .sim import sim_cmd
|
|
11
|
+
from .ros import ros_group
|
|
11
12
|
from .python import python_cmd
|
|
12
13
|
from .asset import asset_group
|
|
13
14
|
|
|
@@ -32,6 +33,7 @@ pow_group.add_command(init_cmd)
|
|
|
32
33
|
pow_group.add_command(check_cmd)
|
|
33
34
|
pow_group.add_command(lint_group)
|
|
34
35
|
pow_group.add_command(run_cmd)
|
|
35
|
-
pow_group.add_command(
|
|
36
|
+
pow_group.add_command(sim_cmd)
|
|
37
|
+
pow_group.add_command(ros_group)
|
|
36
38
|
pow_group.add_command(python_cmd)
|
|
37
39
|
pow_group.add_command(asset_group)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Ros command implementation."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from ..common.utils import console
|
|
6
|
+
from ..core.ros_manager import RosManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _launch_container(extra_args, verbose: bool):
|
|
10
|
+
"""Launch or attach to the ROS container, rendering errors as panels."""
|
|
11
|
+
try:
|
|
12
|
+
RosManager.run_simros_container(extra_args=extra_args, verbose=verbose)
|
|
13
|
+
except click.ClickException:
|
|
14
|
+
raise
|
|
15
|
+
except Exception as e:
|
|
16
|
+
console.print(
|
|
17
|
+
Panel(
|
|
18
|
+
f"[bold red]✘[/bold red] {e}",
|
|
19
|
+
title="[bold red]ROS Error[/bold red]",
|
|
20
|
+
border_style="red",
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
raise SystemExit(1)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RosGroup(click.Group):
|
|
27
|
+
"""Group that falls through to `launch` for unrecognized arguments."""
|
|
28
|
+
|
|
29
|
+
def resolve_command(self, ctx, args):
|
|
30
|
+
if args and self.get_command(ctx, args[0]) is not None:
|
|
31
|
+
return super().resolve_command(ctx, args)
|
|
32
|
+
launch = self.get_command(ctx, "launch")
|
|
33
|
+
return launch.name, launch, args
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@click.group(
|
|
37
|
+
name="ros",
|
|
38
|
+
cls=RosGroup,
|
|
39
|
+
invoke_without_command=True,
|
|
40
|
+
context_settings={"ignore_unknown_options": True},
|
|
41
|
+
)
|
|
42
|
+
@click.pass_context
|
|
43
|
+
def ros_group(ctx: click.Context):
|
|
44
|
+
"""Launch or manage the ROS docker container.
|
|
45
|
+
|
|
46
|
+
Without a subcommand, launches an interactive session in the container
|
|
47
|
+
running the image from `ros_docker_image` in pow.toml (or attaches to it
|
|
48
|
+
if it is already running). Unrecognized arguments are forwarded as the
|
|
49
|
+
container command (default: /bin/bash), e.g. `pow ros ros2 topic list`.
|
|
50
|
+
|
|
51
|
+
\b
|
|
52
|
+
Subcommands:
|
|
53
|
+
launch Explicitly launch the container (same as bare `pow ros`).
|
|
54
|
+
build Build the custom image from `ros_dockerfile`.
|
|
55
|
+
|
|
56
|
+
\b
|
|
57
|
+
Requires:
|
|
58
|
+
- ROS integration enabled in pow.toml (enable_ros = true).
|
|
59
|
+
- The pow_simros Docker image built via `pow init`.
|
|
60
|
+
"""
|
|
61
|
+
if ctx.invoked_subcommand is None:
|
|
62
|
+
_launch_container(None, False)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@ros_group.command(
|
|
66
|
+
name="launch",
|
|
67
|
+
context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
|
|
68
|
+
)
|
|
69
|
+
@click.option("--verbose", "-v", is_flag=True, default=False, help="Show detailed feedback during container launch.")
|
|
70
|
+
@click.pass_context
|
|
71
|
+
def launch_cmd(ctx: click.Context, verbose: bool):
|
|
72
|
+
"""Launch the ROS container, forwarding extra args as the container command."""
|
|
73
|
+
_launch_container(ctx.args or None, verbose)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@ros_group.command(name="build")
|
|
77
|
+
@click.option("--no-cache", is_flag=True, default=False, help="Build without using the Docker layer cache.")
|
|
78
|
+
def build_cmd(no_cache: bool):
|
|
79
|
+
"""Build the custom ROS image from `ros_dockerfile` in pow.toml."""
|
|
80
|
+
ros_mgr = RosManager()
|
|
81
|
+
config = ros_mgr.config
|
|
82
|
+
|
|
83
|
+
if config.project_root is None:
|
|
84
|
+
raise click.ClickException("Not initialized. Run `pow init` first.")
|
|
85
|
+
|
|
86
|
+
if not config.ros_dockerfile:
|
|
87
|
+
console.print(
|
|
88
|
+
"[yellow]No `ros_dockerfile` set in pow.toml; nothing to build.[/yellow]\n"
|
|
89
|
+
f"The bundled [bold]pow_simros_{config.ros_distro}[/bold] image is built by `pow init`."
|
|
90
|
+
)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
base_image = f"pow_simros_{config.ros_distro}"
|
|
94
|
+
if not RosManager.image_exists(base_image):
|
|
95
|
+
distro_ws = config.ros_ws_path / f"{config.ros_distro}_ws"
|
|
96
|
+
if not distro_ws.exists():
|
|
97
|
+
raise click.ClickException(
|
|
98
|
+
f"Base image '{base_image}' not found and the ROS workspace "
|
|
99
|
+
f"'{distro_ws}' does not exist.\n"
|
|
100
|
+
"Run `pow init` first to set up the ROS workspace."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
console.print(f"[yellow]Base image '{base_image}' not found — building it first.[/yellow]")
|
|
104
|
+
|
|
105
|
+
def simros_status_callback(state):
|
|
106
|
+
if state == "simros_building":
|
|
107
|
+
base_status.update(f"[bold green]Building base image '{base_image}'...")
|
|
108
|
+
elif state.startswith("simros_building:"):
|
|
109
|
+
line = state[len("simros_building:"):]
|
|
110
|
+
base_status.update(f"[bold green]{base_image} build:[/bold green] [dim]{line[:80]}[/dim]")
|
|
111
|
+
|
|
112
|
+
with console.status(f"Building base image '{base_image}'...") as base_status:
|
|
113
|
+
try:
|
|
114
|
+
ros_mgr.build_simros_image(status_callback=simros_status_callback)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
console.print(
|
|
117
|
+
Panel(
|
|
118
|
+
f"[bold red]✘[/bold red] {e}",
|
|
119
|
+
title="[bold red]ROS Build Error[/bold red]",
|
|
120
|
+
border_style="red",
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
raise SystemExit(1)
|
|
124
|
+
|
|
125
|
+
console.print(f"[green]✔[/green] Base Docker image [bold]{base_image}[/bold] built successfully.")
|
|
126
|
+
|
|
127
|
+
custom_image = config.ros_docker_image
|
|
128
|
+
|
|
129
|
+
def custom_status_callback(state):
|
|
130
|
+
if state == "custom_building":
|
|
131
|
+
custom_status.update(f"[bold green]Building custom image '{custom_image}'...")
|
|
132
|
+
elif state.startswith("custom_building:"):
|
|
133
|
+
line = state[len("custom_building:"):]
|
|
134
|
+
custom_status.update(f"[bold green]custom build:[/bold green] [dim]{line[:80]}[/dim]")
|
|
135
|
+
|
|
136
|
+
with console.status(f"Building custom image '{custom_image}'...") as custom_status:
|
|
137
|
+
try:
|
|
138
|
+
ros_mgr.build_custom_ros_image(
|
|
139
|
+
status_callback=custom_status_callback,
|
|
140
|
+
no_cache=no_cache,
|
|
141
|
+
)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
console.print(
|
|
144
|
+
Panel(
|
|
145
|
+
f"[bold red]✘[/bold red] {e}",
|
|
146
|
+
title="[bold red]ROS Build Error[/bold red]",
|
|
147
|
+
border_style="red",
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
raise SystemExit(1)
|
|
151
|
+
|
|
152
|
+
console.print(
|
|
153
|
+
f"[green]✔[/green] Custom Docker image [bold]{custom_image}[/bold] "
|
|
154
|
+
f"built from [dim]{config.ros_dockerfile}[/dim]."
|
|
155
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Sim command implementation."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from ..common.utils import console
|
|
6
|
+
from ..core.models.pow_config import PowConfig
|
|
7
|
+
from ..core.runner import Runner
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command(
|
|
11
|
+
name="sim",
|
|
12
|
+
context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
|
|
13
|
+
)
|
|
14
|
+
@click.option(
|
|
15
|
+
"-v",
|
|
16
|
+
"--version",
|
|
17
|
+
"sim_version",
|
|
18
|
+
default=PowConfig.ISAACSIM_VERSION,
|
|
19
|
+
show_default=True,
|
|
20
|
+
help="Isaac Sim version to run.",
|
|
21
|
+
)
|
|
22
|
+
@click.option(
|
|
23
|
+
"--ros",
|
|
24
|
+
"ros_bridge",
|
|
25
|
+
type=click.Choice(PowConfig.SUPPORTED_ROS_BRIDGES),
|
|
26
|
+
default=PowConfig.ROS_BRIDGE,
|
|
27
|
+
show_default=True,
|
|
28
|
+
help="ROS 2 bridge distro to load into the environment.",
|
|
29
|
+
)
|
|
30
|
+
@click.option(
|
|
31
|
+
"--no-ros",
|
|
32
|
+
is_flag=True,
|
|
33
|
+
default=False,
|
|
34
|
+
help="Launch without the ROS 2 bridge environment.",
|
|
35
|
+
)
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def sim_cmd(ctx: click.Context, sim_version: str, ros_bridge: str, no_ros: bool):
|
|
38
|
+
"""Run Isaac Sim from any directory, ignoring pow.toml.
|
|
39
|
+
|
|
40
|
+
\b
|
|
41
|
+
Unlike `pow run` this needs no project: no pyproject.toml is required and
|
|
42
|
+
pow.toml is never read. Every unrecognized argument is forwarded verbatim
|
|
43
|
+
to .pow/isaacsim/<version>/isaac-sim.sh.
|
|
44
|
+
|
|
45
|
+
\b
|
|
46
|
+
Examples:
|
|
47
|
+
pow sim
|
|
48
|
+
pow sim --no-ros
|
|
49
|
+
pow sim -- --no-window --/renderer/enabled=gpu
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
Runner.run_sim(
|
|
53
|
+
version=sim_version,
|
|
54
|
+
ros_bridge=None if no_ros else ros_bridge,
|
|
55
|
+
extra_args=ctx.args,
|
|
56
|
+
)
|
|
57
|
+
except click.ClickException:
|
|
58
|
+
raise
|
|
59
|
+
except Exception as e:
|
|
60
|
+
console.print(
|
|
61
|
+
Panel(
|
|
62
|
+
f"[bold red]✘[/bold red] {e}",
|
|
63
|
+
title="[bold red]Sim Error[/bold red]",
|
|
64
|
+
border_style="red",
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Interactive prompts with shell-style TAB path completion."""
|
|
2
|
+
|
|
3
|
+
import glob
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rich.prompt import Prompt
|
|
10
|
+
|
|
11
|
+
from .utils import console
|
|
12
|
+
|
|
13
|
+
# ANSI SGR sequences must be hidden from readline's column arithmetic, otherwise
|
|
14
|
+
# the cursor lands in the wrong place after a completion redisplay.
|
|
15
|
+
_ANSI_RE = re.compile(r"(\x1b\[[0-9;]*m)")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def complete_path(text: str, dirs_only: bool = False) -> list[str]:
|
|
19
|
+
"""Return the filesystem completions for *text*, shell style.
|
|
20
|
+
|
|
21
|
+
Directories come back with a trailing separator, and a leading ``~`` is
|
|
22
|
+
preserved so the completed value stays tilde-relative.
|
|
23
|
+
"""
|
|
24
|
+
expanded = os.path.expanduser(text)
|
|
25
|
+
matches = glob.glob(expanded + "*")
|
|
26
|
+
|
|
27
|
+
if dirs_only:
|
|
28
|
+
matches = [m for m in matches if os.path.isdir(m)]
|
|
29
|
+
|
|
30
|
+
matches = [m + os.sep if os.path.isdir(m) else m for m in matches]
|
|
31
|
+
|
|
32
|
+
if text.startswith("~"):
|
|
33
|
+
home = str(Path.home())
|
|
34
|
+
matches = [
|
|
35
|
+
"~" + m[len(home):] if m.startswith(home) else m
|
|
36
|
+
for m in matches
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
return sorted(matches)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@contextmanager
|
|
43
|
+
def _path_completion(dirs_only: bool = False):
|
|
44
|
+
"""Install a path completer on readline for the duration of one prompt.
|
|
45
|
+
|
|
46
|
+
``readline`` is imported lazily: importing it changes ``input()`` behaviour
|
|
47
|
+
process-wide, which should not happen just because some other command was
|
|
48
|
+
run. Yields ``True`` when completion is active, ``False`` when readline is
|
|
49
|
+
unavailable (no completion, prompt still works).
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
import readline
|
|
53
|
+
except ImportError:
|
|
54
|
+
yield False
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
matches: list[str] = []
|
|
58
|
+
|
|
59
|
+
def completer(text: str, state: int):
|
|
60
|
+
nonlocal matches
|
|
61
|
+
if state == 0:
|
|
62
|
+
# Delims are "\n" (see below), so `text` is the whole line and each
|
|
63
|
+
# match replaces it entirely. rstrip() absorbs any stray trailing
|
|
64
|
+
# space.
|
|
65
|
+
matches = complete_path(text.rstrip(), dirs_only=dirs_only)
|
|
66
|
+
if len(matches) == 1 and matches[0].endswith(os.sep):
|
|
67
|
+
# readline appends a space after a *unique* match and Python
|
|
68
|
+
# exposes no way to turn that off. For a directory that would
|
|
69
|
+
# strand the cursor behind a space; duplicating the match makes
|
|
70
|
+
# it non-unique, so the inserted text is exactly the path and
|
|
71
|
+
# the next segment can be typed straight away, shell style.
|
|
72
|
+
# Files keep the space, which is what a shell does too.
|
|
73
|
+
matches = matches * 2
|
|
74
|
+
return matches[state] if state < len(matches) else None
|
|
75
|
+
|
|
76
|
+
prev_completer = readline.get_completer()
|
|
77
|
+
prev_delims = readline.get_completer_delims()
|
|
78
|
+
|
|
79
|
+
# Breaking words on "\n" only (never present in a line) makes the whole line
|
|
80
|
+
# the completion word. The default delims include "/", "~" and "-", which
|
|
81
|
+
# would hand the completer useless fragments; this also lets paths with
|
|
82
|
+
# spaces complete correctly.
|
|
83
|
+
readline.set_completer_delims("\n")
|
|
84
|
+
readline.set_completer(completer)
|
|
85
|
+
|
|
86
|
+
# uv-managed CPython links libedit, which needs its own binding syntax.
|
|
87
|
+
if "libedit" in (readline.__doc__ or ""):
|
|
88
|
+
readline.parse_and_bind("bind ^I rl_complete")
|
|
89
|
+
else:
|
|
90
|
+
readline.parse_and_bind("tab: complete")
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
yield True
|
|
94
|
+
finally:
|
|
95
|
+
readline.set_completer(prev_completer)
|
|
96
|
+
readline.set_completer_delims(prev_delims)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class PathPrompt(Prompt):
|
|
100
|
+
"""A :class:`rich.prompt.Prompt` whose input line completes paths on TAB."""
|
|
101
|
+
|
|
102
|
+
dirs_only: bool = False
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def get_input(cls, console, prompt, password: bool, stream=None) -> str:
|
|
106
|
+
if password or stream is not None:
|
|
107
|
+
return super().get_input(console, prompt, password, stream=stream)
|
|
108
|
+
|
|
109
|
+
# Hand the rendered prompt to input() rather than printing it first:
|
|
110
|
+
# readline needs to know the prompt to redraw the line after listing
|
|
111
|
+
# matches, otherwise the prompt vanishes on the first ambiguous TAB.
|
|
112
|
+
with console.capture() as capture:
|
|
113
|
+
console.print(prompt, end="")
|
|
114
|
+
rendered = _ANSI_RE.sub("\x01\\1\x02", capture.get())
|
|
115
|
+
|
|
116
|
+
with _path_completion(dirs_only=cls.dirs_only):
|
|
117
|
+
return input(rendered)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class _DirPrompt(PathPrompt):
|
|
121
|
+
dirs_only = True
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def ask_path(message: str, *, default: str, dirs_only: bool = True) -> str:
|
|
125
|
+
"""Ask for a filesystem path, completing it with TAB like a shell does.
|
|
126
|
+
|
|
127
|
+
Trailing whitespace and a trailing separator are stripped, since TAB
|
|
128
|
+
completion makes both common and the value is stored as-is in pow.toml.
|
|
129
|
+
"""
|
|
130
|
+
prompt_cls = _DirPrompt if dirs_only else PathPrompt
|
|
131
|
+
value = prompt_cls.ask(message, default=default, console=console).strip()
|
|
132
|
+
|
|
133
|
+
if len(value) > 1:
|
|
134
|
+
value = value.rstrip(os.sep)
|
|
135
|
+
|
|
136
|
+
return value
|