simfix 0.1.2__tar.gz → 0.1.4__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.
Files changed (43) hide show
  1. {simfix-0.1.2/simfix.egg-info → simfix-0.1.4}/PKG-INFO +1 -1
  2. {simfix-0.1.2 → simfix-0.1.4}/pyproject.toml +1 -1
  3. {simfix-0.1.2 → simfix-0.1.4}/simfix/__init__.py +1 -1
  4. {simfix-0.1.2 → simfix-0.1.4}/simfix/cli.py +9 -0
  5. simfix-0.1.4/simfix/cuda.py +133 -0
  6. simfix-0.1.4/simfix/recommendation_types.py +16 -0
  7. simfix-0.1.4/simfix/recommendations.py +320 -0
  8. simfix-0.1.4/simfix/ros_environment.py +106 -0
  9. simfix-0.1.4/simfix/system_capabilities.py +46 -0
  10. simfix-0.1.4/simfix/vendor_dependencies.py +134 -0
  11. {simfix-0.1.2 → simfix-0.1.4/simfix.egg-info}/PKG-INFO +1 -1
  12. {simfix-0.1.2 → simfix-0.1.4}/simfix.egg-info/SOURCES.txt +5 -0
  13. {simfix-0.1.2 → simfix-0.1.4}/tests/test_basic.py +232 -1
  14. simfix-0.1.2/simfix/recommendations.py +0 -131
  15. {simfix-0.1.2 → simfix-0.1.4}/LICENSE +0 -0
  16. {simfix-0.1.2 → simfix-0.1.4}/README.md +0 -0
  17. {simfix-0.1.2 → simfix-0.1.4}/setup.cfg +0 -0
  18. {simfix-0.1.2 → simfix-0.1.4}/simfix/analyzer.py +0 -0
  19. {simfix-0.1.2 → simfix-0.1.4}/simfix/cmake.py +0 -0
  20. {simfix-0.1.2 → simfix-0.1.4}/simfix/commands.py +0 -0
  21. {simfix-0.1.2 → simfix-0.1.4}/simfix/compatibility.py +0 -0
  22. {simfix-0.1.2 → simfix-0.1.4}/simfix/conda_environment.py +0 -0
  23. {simfix-0.1.2 → simfix-0.1.4}/simfix/conda_fixer.py +0 -0
  24. {simfix-0.1.2 → simfix-0.1.4}/simfix/cuda_docker.py +0 -0
  25. {simfix-0.1.2 → simfix-0.1.4}/simfix/docker_runner.py +0 -0
  26. {simfix-0.1.2 → simfix-0.1.4}/simfix/dockerfile.py +0 -0
  27. {simfix-0.1.2 → simfix-0.1.4}/simfix/fixer.py +0 -0
  28. {simfix-0.1.2 → simfix-0.1.4}/simfix/git_assets.py +0 -0
  29. {simfix-0.1.2 → simfix-0.1.4}/simfix/planner.py +0 -0
  30. {simfix-0.1.2 → simfix-0.1.4}/simfix/pypi.py +0 -0
  31. {simfix-0.1.2 → simfix-0.1.4}/simfix/pyproject.py +0 -0
  32. {simfix-0.1.2 → simfix-0.1.4}/simfix/python_requirements.py +0 -0
  33. {simfix-0.1.2 → simfix-0.1.4}/simfix/repo.py +0 -0
  34. {simfix-0.1.2 → simfix-0.1.4}/simfix/report.py +0 -0
  35. {simfix-0.1.2 → simfix-0.1.4}/simfix/ros_docker.py +0 -0
  36. {simfix-0.1.2 → simfix-0.1.4}/simfix/ros_package.py +0 -0
  37. {simfix-0.1.2 → simfix-0.1.4}/simfix/setup_py.py +0 -0
  38. {simfix-0.1.2 → simfix-0.1.4}/simfix/system.py +0 -0
  39. {simfix-0.1.2 → simfix-0.1.4}/simfix/system_docker.py +0 -0
  40. {simfix-0.1.2 → simfix-0.1.4}/simfix.egg-info/dependency_links.txt +0 -0
  41. {simfix-0.1.2 → simfix-0.1.4}/simfix.egg-info/entry_points.txt +0 -0
  42. {simfix-0.1.2 → simfix-0.1.4}/simfix.egg-info/requires.txt +0 -0
  43. {simfix-0.1.2 → simfix-0.1.4}/simfix.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simfix
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A dependency checker and installation assistant for simulator repositories.
5
5
  Author-email: Habib ur Rehmaan <h.rehmaan96@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simfix"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "A dependency checker and installation assistant for simulator repositories."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """SimFix package."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.4"
@@ -15,8 +15,11 @@ from simfix.planner import create_install_plan
15
15
  from simfix.pypi import check_pypi_packages
16
16
  from simfix.repo import clone_repo, is_git_url
17
17
  from simfix.report import generate_markdown_report, write_markdown_report
18
+ from simfix.system_capabilities import detect_system_capabilities
18
19
  from simfix.system import get_system_info
19
20
  from simfix.recommendations import generate_recommendations
21
+ from simfix.cuda import detect_cuda_version_info
22
+ from simfix.ros_environment import detect_ros_environment_info
20
23
 
21
24
  console = Console()
22
25
 
@@ -120,6 +123,9 @@ def recommendations(repo: str) -> None:
120
123
  repo_recommendations = generate_recommendations(
121
124
  dependencies=analysis.all_python_dependencies,
122
125
  detected_ecosystems=analysis.detected_ecosystems,
126
+ system_capabilities=detect_system_capabilities(),
127
+ cuda_version_info=detect_cuda_version_info(repo_path),
128
+ ros_environment_info=detect_ros_environment_info(repo_path),
123
129
  )
124
130
 
125
131
  console.print("[bold]SimFix Recommendations[/bold]")
@@ -405,6 +411,9 @@ def doctor(
405
411
  repo_recommendations = generate_recommendations(
406
412
  dependencies=analysis.all_python_dependencies,
407
413
  detected_ecosystems=analysis.detected_ecosystems,
414
+ system_capabilities=detect_system_capabilities(),
415
+ cuda_version_info=detect_cuda_version_info(repo_path),
416
+ ros_environment_info=detect_ros_environment_info(repo_path),
408
417
  )
409
418
 
410
419
  if repo_recommendations:
@@ -0,0 +1,133 @@
1
+ """CUDA version detection helpers for SimFix recommendations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import subprocess
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class CudaVersionInfo:
13
+ """Detected CUDA version information."""
14
+
15
+ repo_cuda_version: tuple[int, int] | None
16
+ system_cuda_version: tuple[int, int] | None
17
+ repo_cuda_source: str | None
18
+
19
+
20
+ def detect_cuda_version_info(repo_path: Path) -> CudaVersionInfo:
21
+ """Detect repository CUDA requirement and system CUDA capability."""
22
+ repo_cuda_version, repo_cuda_source = detect_repo_cuda_version(repo_path)
23
+ system_cuda_version = detect_system_cuda_version()
24
+
25
+ return CudaVersionInfo(
26
+ repo_cuda_version=repo_cuda_version,
27
+ system_cuda_version=system_cuda_version,
28
+ repo_cuda_source=repo_cuda_source,
29
+ )
30
+
31
+
32
+ def detect_repo_cuda_version(
33
+ repo_path: Path,
34
+ ) -> tuple[tuple[int, int] | None, str | None]:
35
+ """Detect CUDA version requested by repository files.
36
+
37
+ This is intentionally generic. It checks common simulator/deep-learning
38
+ signals such as Docker base images and CUDA-specific dependency names.
39
+ """
40
+ dockerfile = repo_path / "Dockerfile"
41
+ if dockerfile.exists():
42
+ version = _detect_cuda_version_from_text(dockerfile.read_text(encoding="utf-8"))
43
+ if version is not None:
44
+ return version, "Dockerfile"
45
+
46
+ dependency_files = [
47
+ repo_path / "requirements.txt",
48
+ repo_path / "pyproject.toml",
49
+ repo_path / "setup.py",
50
+ repo_path / "environment.yml",
51
+ repo_path / "environment.yaml",
52
+ ]
53
+
54
+ for dependency_file in dependency_files:
55
+ if dependency_file.exists():
56
+ version = _detect_cuda_version_from_text(
57
+ dependency_file.read_text(encoding="utf-8")
58
+ )
59
+ if version is not None:
60
+ return version, dependency_file.name
61
+
62
+ return None, None
63
+
64
+
65
+ def detect_system_cuda_version() -> tuple[int, int] | None:
66
+ """Detect CUDA version supported by the NVIDIA driver using nvidia-smi."""
67
+ try:
68
+ result = subprocess.run(
69
+ ["nvidia-smi"],
70
+ check=False,
71
+ capture_output=True,
72
+ text=True,
73
+ timeout=10,
74
+ )
75
+ except (OSError, subprocess.SubprocessError):
76
+ return None
77
+
78
+ return _parse_nvidia_smi_cuda_version(result.stdout + result.stderr)
79
+
80
+
81
+ def is_cuda_version_mismatch(
82
+ repo_cuda_version: tuple[int, int] | None,
83
+ system_cuda_version: tuple[int, int] | None,
84
+ ) -> bool:
85
+ """Return True if the repo appears to require newer CUDA than the system."""
86
+ if repo_cuda_version is None or system_cuda_version is None:
87
+ return False
88
+
89
+ return repo_cuda_version > system_cuda_version
90
+
91
+
92
+ def _detect_cuda_version_from_text(text: str) -> tuple[int, int] | None:
93
+ """Detect CUDA version from generic text."""
94
+ normalized = text.lower()
95
+
96
+ detected_versions: list[tuple[int, int]] = []
97
+
98
+ explicit_patterns = [
99
+ r"nvidia/cuda:(\d+)\.(\d+)",
100
+ r"cuda[-_]?(\d+)[\._-](\d+)",
101
+ ]
102
+
103
+ for pattern in explicit_patterns:
104
+ for match in re.finditer(pattern, normalized):
105
+ detected_versions.append((int(match.group(1)), int(match.group(2))))
106
+
107
+ for match in re.finditer(r"cuda[-_]?(\d+)x", normalized):
108
+ detected_versions.append((int(match.group(1)), 0))
109
+
110
+ for match in re.finditer(r"cu(\d{2,3})", normalized):
111
+ detected_versions.append(_parse_compact_cuda_version(match.group(1)))
112
+
113
+ if not detected_versions:
114
+ return None
115
+
116
+ return max(detected_versions)
117
+
118
+
119
+ def _parse_nvidia_smi_cuda_version(text: str) -> tuple[int, int] | None:
120
+ """Parse CUDA version from nvidia-smi output."""
121
+ match = re.search(r"cuda version:\s*(\d+)\.(\d+)", text.lower())
122
+ if match is None:
123
+ return None
124
+
125
+ return int(match.group(1)), int(match.group(2))
126
+
127
+
128
+ def _parse_compact_cuda_version(version: str) -> tuple[int, int]:
129
+ """Parse compact CUDA version strings such as cu118 or cu121."""
130
+ if len(version) == 2:
131
+ return int(version[0]), int(version[1])
132
+
133
+ return int(version[:-1]), int(version[-1])
@@ -0,0 +1,16 @@
1
+ """Shared recommendation types used by SimFix."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Recommendation:
10
+ """A safe recommendation for system or vendor dependency guidance."""
11
+
12
+ category: str
13
+ title: str
14
+ status: str
15
+ reason: str
16
+ suggestion: str
@@ -0,0 +1,320 @@
1
+ from __future__ import annotations
2
+
3
+ # from dataclasses import dataclass
4
+ from simfix.system_capabilities import SystemCapabilities
5
+ from simfix.cuda import CudaVersionInfo, is_cuda_version_mismatch
6
+ from simfix.ros_environment import RosEnvironmentInfo
7
+ from simfix.vendor_dependencies import detect_vendor_dependency_recommendations
8
+ from simfix.recommendation_types import Recommendation
9
+ import sys
10
+
11
+
12
+ def generate_recommendations(
13
+ dependencies: list[str],
14
+ detected_ecosystems: list[str],
15
+ python_version: tuple[int, int] | None = None,
16
+ system_capabilities: SystemCapabilities | None = None,
17
+ cuda_version_info: CudaVersionInfo | None = None,
18
+ ros_environment_info: RosEnvironmentInfo | None = None,
19
+ ) -> list[Recommendation]:
20
+ """Generate safe system and vendor dependency recommendations.
21
+
22
+ This function does not install drivers, ROS, CUDA, or vendor tools.
23
+ It only detects likely requirements and returns guidance.
24
+ """
25
+ recommendations: list[Recommendation] = []
26
+
27
+ normalized_dependencies = [dependency.lower() for dependency in dependencies]
28
+ normalized_ecosystems = [ecosystem.lower() for ecosystem in detected_ecosystems]
29
+
30
+ if python_version is None:
31
+ python_version = sys.version_info[:2]
32
+
33
+ has_isaacgym = any(
34
+ "isaacgym" in dependency for dependency in normalized_dependencies
35
+ )
36
+ has_isaacsim = any(
37
+ "isaacsim" in dependency or "omni.isaac" in dependency
38
+ for dependency in normalized_dependencies
39
+ )
40
+ has_cuda_dependency = any(
41
+ _has_cuda_keyword(dependency) for dependency in normalized_dependencies
42
+ )
43
+ has_ros = "ros" in normalized_ecosystems
44
+
45
+ has_old_pinned_dependency = any(
46
+ _has_old_pinned_dependency(dependency) for dependency in normalized_dependencies
47
+ )
48
+
49
+ recommendations.extend(
50
+ detect_vendor_dependency_recommendations(normalized_dependencies)
51
+ )
52
+
53
+ if has_isaacgym:
54
+ recommendations.append(
55
+ Recommendation(
56
+ category="Vendor-managed dependency",
57
+ title="NVIDIA Isaac Gym required",
58
+ status="Manual installation required",
59
+ reason=(
60
+ "The dependency 'isaacgym' was detected, but it is not "
61
+ "available as a normal PyPI package."
62
+ ),
63
+ suggestion=(
64
+ "Install NVIDIA Isaac Gym manually in a compatible Linux "
65
+ "environment with NVIDIA GPU support. If the local machine "
66
+ "is not suitable, use an HPC GPU node or cloud GPU instance."
67
+ ),
68
+ )
69
+ )
70
+
71
+ if has_isaacsim:
72
+ recommendations.append(
73
+ Recommendation(
74
+ category="Vendor-managed dependency",
75
+ title="NVIDIA Isaac Sim required",
76
+ status="Manual installation required",
77
+ reason=(
78
+ "Isaac Sim or omni.isaac dependencies were detected. "
79
+ "These are NVIDIA-managed dependencies."
80
+ ),
81
+ suggestion=(
82
+ "Install Isaac Sim using NVIDIA's official installation "
83
+ "method. Use a supported system with compatible NVIDIA GPU "
84
+ "drivers."
85
+ ),
86
+ )
87
+ )
88
+
89
+ if has_isaacgym or has_isaacsim or has_cuda_dependency:
90
+ recommendations.append(
91
+ Recommendation(
92
+ category="GPU/CUDA",
93
+ title="CUDA-compatible environment recommended",
94
+ status="Check required",
95
+ reason=(
96
+ "GPU/CUDA-related dependencies were detected in the " "repository."
97
+ ),
98
+ suggestion=(
99
+ "Use a compatible Linux/NVIDIA GPU environment. If the "
100
+ "local GPU is not CUDA-compatible, consider a Docker setup "
101
+ "on a compatible machine, an HPC GPU node, a cloud GPU "
102
+ "instance, or CPU-only mode if the simulator supports it."
103
+ ),
104
+ )
105
+ )
106
+
107
+ if ros_environment_info is not None:
108
+ if ros_environment_info.project_type in {"ROS 1 / catkin", "ROS 2 / ament"}:
109
+ recommendations.append(
110
+ Recommendation(
111
+ category="ROS",
112
+ title=f"{ros_environment_info.project_type} environment detected",
113
+ status="Environment recommendation available",
114
+ reason=(
115
+ f"{ros_environment_info.project_type} signals were detected "
116
+ f"from {ros_environment_info.source}."
117
+ ),
118
+ suggestion=(
119
+ f"Use ROS {ros_environment_info.recommended_distribution} "
120
+ f"with {ros_environment_info.recommended_ubuntu}, or use the "
121
+ f"Docker image {ros_environment_info.recommended_docker_image}."
122
+ ),
123
+ )
124
+ )
125
+ else:
126
+ recommendations.append(
127
+ Recommendation(
128
+ category="ROS",
129
+ title="ROS project detected",
130
+ status="Manual environment check recommended",
131
+ reason=(
132
+ f"ROS package files were detected from "
133
+ f"{ros_environment_info.source}, but the ROS build style "
134
+ "could not be confidently classified."
135
+ ),
136
+ suggestion=(
137
+ "Check the package.xml and build files to determine whether "
138
+ "the project requires ROS 1/catkin or ROS 2/ament. Use a "
139
+ "matching ROS Docker image or native ROS installation."
140
+ ),
141
+ )
142
+ )
143
+ elif has_ros:
144
+ recommendations.append(
145
+ Recommendation(
146
+ category="ROS",
147
+ title="ROS environment required",
148
+ status="Manual installation required",
149
+ reason="ROS package files were detected in the repository.",
150
+ suggestion=(
151
+ "Use a matching ROS Docker image or install the correct ROS "
152
+ "distribution manually."
153
+ ),
154
+ )
155
+ )
156
+
157
+ if has_old_pinned_dependency and python_version >= (3, 13):
158
+ recommendations.append(
159
+ Recommendation(
160
+ category="Python environment",
161
+ title="Older pinned dependencies detected",
162
+ status="Python version compatibility check recommended",
163
+ reason=(
164
+ "This repository contains older pinned dependencies that may "
165
+ f"not install cleanly on Python {python_version[0]}.{python_version[1]}."
166
+ ),
167
+ suggestion=(
168
+ "Use Python 3.10 or 3.11 in a virtual environment for better "
169
+ "compatibility before installing the project dependencies."
170
+ ),
171
+ )
172
+ )
173
+
174
+ if has_cuda_dependency and system_capabilities is not None:
175
+ if (
176
+ system_capabilities.has_docker
177
+ and not system_capabilities.has_nvidia_container_runtime
178
+ ):
179
+ recommendations.append(
180
+ Recommendation(
181
+ category="GPU containers",
182
+ title="NVIDIA Docker support not detected",
183
+ status="Configuration check recommended",
184
+ reason=(
185
+ "This repository appears to need GPU/CUDA support, and Docker "
186
+ "is available, but NVIDIA container runtime support was not detected."
187
+ ),
188
+ suggestion=(
189
+ "GPU containers may fail with '--gpus all'. Use a compatible "
190
+ "Linux/NVIDIA environment with NVIDIA Container Toolkit configured, "
191
+ "or use an HPC/cloud GPU environment."
192
+ ),
193
+ )
194
+ )
195
+
196
+ if cuda_version_info is not None:
197
+ repo_cuda_version = cuda_version_info.repo_cuda_version
198
+ system_cuda_version = cuda_version_info.system_cuda_version
199
+
200
+ if repo_cuda_version is not None and system_cuda_version is None:
201
+ recommendations.append(
202
+ Recommendation(
203
+ category="CUDA compatibility",
204
+ title="System CUDA support not detected",
205
+ status="Compatibility unknown",
206
+ reason=(
207
+ "The repository appears to require CUDA "
208
+ f"{repo_cuda_version[0]}.{repo_cuda_version[1]} from "
209
+ f"{cuda_version_info.repo_cuda_source}, but system CUDA support "
210
+ "could not be detected with nvidia-smi."
211
+ ),
212
+ suggestion=(
213
+ "Use a compatible Linux/NVIDIA GPU environment, a configured "
214
+ "GPU Docker setup, an HPC GPU node, or a cloud GPU instance. "
215
+ "If the simulator supports CPU-only mode, that may also be used "
216
+ "for limited testing."
217
+ ),
218
+ )
219
+ )
220
+
221
+ if is_cuda_version_mismatch(repo_cuda_version, system_cuda_version):
222
+ recommendations.append(
223
+ Recommendation(
224
+ category="CUDA compatibility",
225
+ title="CUDA version mismatch detected",
226
+ status="Version mismatch",
227
+ reason=(
228
+ "The repository appears to require CUDA "
229
+ f"{repo_cuda_version[0]}.{repo_cuda_version[1]} from "
230
+ f"{cuda_version_info.repo_cuda_source}, but the NVIDIA driver "
231
+ f"reports CUDA {system_cuda_version[0]}.{system_cuda_version[1]} support."
232
+ ),
233
+ suggestion=(
234
+ "Use a CUDA version compatible with the installed NVIDIA driver, "
235
+ "update the NVIDIA driver if appropriate, or run the simulator on "
236
+ "an HPC/cloud GPU environment with compatible CUDA support."
237
+ ),
238
+ )
239
+ )
240
+ return recommendations
241
+
242
+
243
+ def _has_cuda_keyword(dependency: str) -> bool:
244
+ """Return True if a dependency suggests GPU/CUDA requirements."""
245
+ cuda_keywords = [
246
+ "cuda",
247
+ "cudnn",
248
+ "cupy",
249
+ "pytorch3d",
250
+ "onnxruntime-gpu",
251
+ "tensorflow-gpu",
252
+ "jax[cuda",
253
+ "numba.cuda",
254
+ ]
255
+
256
+ return any(keyword in dependency for keyword in cuda_keywords)
257
+
258
+
259
+ def _has_old_pinned_dependency(dependency: str) -> bool:
260
+ """Return True when a dependency is pinned to an old version.
261
+
262
+ This is a conservative heuristic used for Python-version compatibility
263
+ recommendations. It does not modify dependencies.
264
+ """
265
+ normalized = dependency.lower().strip()
266
+
267
+ if "==" not in normalized:
268
+ return False
269
+
270
+ package_name, version = normalized.split("==", maxsplit=1)
271
+ package_name = package_name.strip()
272
+ version = version.strip()
273
+
274
+ old_version_thresholds = {
275
+ "numpy": (1, 24),
276
+ "scipy": (1, 10),
277
+ "networkx": (3, 0),
278
+ "pandas": (2, 0),
279
+ "matplotlib": (3, 7),
280
+ "gym": (1, 0),
281
+ "urdfpy": (0, 1),
282
+ }
283
+
284
+ if package_name not in old_version_thresholds:
285
+ return False
286
+
287
+ parsed_version = _parse_version_prefix(version)
288
+ if parsed_version is None:
289
+ return False
290
+
291
+ return parsed_version < old_version_thresholds[package_name]
292
+
293
+
294
+ def _parse_version_prefix(version: str) -> tuple[int, ...] | None:
295
+ """Parse the numeric prefix of a dependency version.
296
+
297
+ Examples:
298
+ 1.23.0 -> (1, 23, 0)
299
+ 2.2 -> (2, 2)
300
+ 0.0.22 -> (0, 0, 22)
301
+ """
302
+ parts: list[int] = []
303
+
304
+ for part in version.split("."):
305
+ number = ""
306
+ for character in part:
307
+ if character.isdigit():
308
+ number += character
309
+ else:
310
+ break
311
+
312
+ if not number:
313
+ break
314
+
315
+ parts.append(int(number))
316
+
317
+ if not parts:
318
+ return None
319
+
320
+ return tuple(parts)
@@ -0,0 +1,106 @@
1
+ """ROS environment detection helpers for SimFix recommendations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class RosEnvironmentInfo:
11
+ """Detected ROS environment information."""
12
+
13
+ project_type: str | None
14
+ recommended_distribution: str | None
15
+ recommended_ubuntu: str | None
16
+ recommended_docker_image: str | None
17
+ source: str | None
18
+
19
+
20
+ def detect_ros_environment_info(repo_path: Path) -> RosEnvironmentInfo | None:
21
+ """Detect ROS project style and recommend a common compatible environment.
22
+
23
+ This detection is generic and based on build-system signals, not repository
24
+ names. It does not install ROS or modify files.
25
+ """
26
+ package_xml = repo_path / "package.xml"
27
+ cmake_lists = repo_path / "CMakeLists.txt"
28
+
29
+ package_text = _read_file(package_xml)
30
+ cmake_text = _read_file(cmake_lists)
31
+ combined_text = f"{package_text}\n{cmake_text}".lower()
32
+
33
+ if not combined_text.strip():
34
+ return None
35
+
36
+ if _looks_like_ros2(combined_text):
37
+ return RosEnvironmentInfo(
38
+ project_type="ROS 2 / ament",
39
+ recommended_distribution="Humble",
40
+ recommended_ubuntu="Ubuntu 22.04",
41
+ recommended_docker_image="osrf/ros:humble-desktop",
42
+ source=_source_label(package_xml, cmake_lists),
43
+ )
44
+
45
+ if _looks_like_ros1(combined_text):
46
+ return RosEnvironmentInfo(
47
+ project_type="ROS 1 / catkin",
48
+ recommended_distribution="Noetic",
49
+ recommended_ubuntu="Ubuntu 20.04",
50
+ recommended_docker_image="osrf/ros:noetic-desktop-full",
51
+ source=_source_label(package_xml, cmake_lists),
52
+ )
53
+
54
+ if package_xml.exists():
55
+ return RosEnvironmentInfo(
56
+ project_type="ROS project",
57
+ recommended_distribution=None,
58
+ recommended_ubuntu=None,
59
+ recommended_docker_image=None,
60
+ source="package.xml",
61
+ )
62
+
63
+ return None
64
+
65
+
66
+ def _looks_like_ros2(text: str) -> bool:
67
+ ros2_markers = [
68
+ "ament_cmake",
69
+ "ament_python",
70
+ "ament_package",
71
+ "find_package(ament_cmake",
72
+ "<build_type>ament_cmake</build_type>",
73
+ "<build_type>ament_python</build_type>",
74
+ "rclpy",
75
+ "rclcpp",
76
+ ]
77
+ return any(marker in text for marker in ros2_markers)
78
+
79
+
80
+ def _looks_like_ros1(text: str) -> bool:
81
+ ros1_markers = [
82
+ "catkin_package",
83
+ "find_package(catkin",
84
+ "<buildtool_depend>catkin</buildtool_depend>",
85
+ "<build_depend>catkin</build_depend>",
86
+ "<depend>roscpp</depend>",
87
+ "<depend>rospy</depend>",
88
+ "roslaunch",
89
+ ]
90
+ return any(marker in text for marker in ros1_markers)
91
+
92
+
93
+ def _read_file(path: Path) -> str:
94
+ if not path.exists():
95
+ return ""
96
+
97
+ return path.read_text(encoding="utf-8")
98
+
99
+
100
+ def _source_label(*paths: Path) -> str:
101
+ existing_paths = [path.name for path in paths if path.exists()]
102
+
103
+ if not existing_paths:
104
+ return "ROS project files"
105
+
106
+ return ", ".join(existing_paths)
@@ -0,0 +1,46 @@
1
+ """System capability detection used by SimFix recommendations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class SystemCapabilities:
12
+ """Detected system capabilities relevant to simulator installation."""
13
+
14
+ has_docker: bool
15
+ has_nvidia_smi: bool
16
+ has_nvidia_container_runtime: bool
17
+
18
+
19
+ def detect_system_capabilities() -> SystemCapabilities:
20
+ """Detect system capabilities without modifying the system."""
21
+ return SystemCapabilities(
22
+ has_docker=shutil.which("docker") is not None,
23
+ has_nvidia_smi=shutil.which("nvidia-smi") is not None,
24
+ has_nvidia_container_runtime=_has_nvidia_container_runtime(),
25
+ )
26
+
27
+
28
+ def _has_nvidia_container_runtime() -> bool:
29
+ """Return True if Docker appears to support NVIDIA GPU containers."""
30
+ if shutil.which("docker") is None:
31
+ return False
32
+
33
+ try:
34
+ result = subprocess.run(
35
+ ["docker", "info", "--format", "{{json .Runtimes}}"],
36
+ check=False,
37
+ capture_output=True,
38
+ text=True,
39
+ timeout=10,
40
+ )
41
+ except (OSError, subprocess.SubprocessError):
42
+ return False
43
+
44
+ output = result.stdout.lower() + result.stderr.lower()
45
+
46
+ return "nvidia" in output