simfix 0.1.4__tar.gz → 0.1.5__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 (45) hide show
  1. {simfix-0.1.4/simfix.egg-info → simfix-0.1.5}/PKG-INFO +2 -1
  2. {simfix-0.1.4 → simfix-0.1.5}/pyproject.toml +2 -1
  3. {simfix-0.1.4 → simfix-0.1.5}/simfix/__init__.py +1 -1
  4. {simfix-0.1.4 → simfix-0.1.5}/simfix/analyzer.py +45 -17
  5. {simfix-0.1.4 → simfix-0.1.5}/simfix/cli.py +10 -0
  6. simfix-0.1.5/simfix/commands.py +130 -0
  7. simfix-0.1.5/simfix/dependency_discovery.py +139 -0
  8. simfix-0.1.5/simfix/install_commands.py +163 -0
  9. {simfix-0.1.4 → simfix-0.1.5}/simfix/recommendations.py +0 -36
  10. {simfix-0.1.4 → simfix-0.1.5}/simfix/ros_environment.py +50 -21
  11. {simfix-0.1.4 → simfix-0.1.5/simfix.egg-info}/PKG-INFO +2 -1
  12. {simfix-0.1.4 → simfix-0.1.5}/simfix.egg-info/SOURCES.txt +2 -0
  13. {simfix-0.1.4 → simfix-0.1.5}/simfix.egg-info/requires.txt +3 -0
  14. {simfix-0.1.4 → simfix-0.1.5}/tests/test_basic.py +47 -13
  15. simfix-0.1.4/simfix/commands.py +0 -91
  16. {simfix-0.1.4 → simfix-0.1.5}/LICENSE +0 -0
  17. {simfix-0.1.4 → simfix-0.1.5}/README.md +0 -0
  18. {simfix-0.1.4 → simfix-0.1.5}/setup.cfg +0 -0
  19. {simfix-0.1.4 → simfix-0.1.5}/simfix/cmake.py +0 -0
  20. {simfix-0.1.4 → simfix-0.1.5}/simfix/compatibility.py +0 -0
  21. {simfix-0.1.4 → simfix-0.1.5}/simfix/conda_environment.py +0 -0
  22. {simfix-0.1.4 → simfix-0.1.5}/simfix/conda_fixer.py +0 -0
  23. {simfix-0.1.4 → simfix-0.1.5}/simfix/cuda.py +0 -0
  24. {simfix-0.1.4 → simfix-0.1.5}/simfix/cuda_docker.py +0 -0
  25. {simfix-0.1.4 → simfix-0.1.5}/simfix/docker_runner.py +0 -0
  26. {simfix-0.1.4 → simfix-0.1.5}/simfix/dockerfile.py +0 -0
  27. {simfix-0.1.4 → simfix-0.1.5}/simfix/fixer.py +0 -0
  28. {simfix-0.1.4 → simfix-0.1.5}/simfix/git_assets.py +0 -0
  29. {simfix-0.1.4 → simfix-0.1.5}/simfix/planner.py +0 -0
  30. {simfix-0.1.4 → simfix-0.1.5}/simfix/pypi.py +0 -0
  31. {simfix-0.1.4 → simfix-0.1.5}/simfix/pyproject.py +0 -0
  32. {simfix-0.1.4 → simfix-0.1.5}/simfix/python_requirements.py +0 -0
  33. {simfix-0.1.4 → simfix-0.1.5}/simfix/recommendation_types.py +0 -0
  34. {simfix-0.1.4 → simfix-0.1.5}/simfix/repo.py +0 -0
  35. {simfix-0.1.4 → simfix-0.1.5}/simfix/report.py +0 -0
  36. {simfix-0.1.4 → simfix-0.1.5}/simfix/ros_docker.py +0 -0
  37. {simfix-0.1.4 → simfix-0.1.5}/simfix/ros_package.py +0 -0
  38. {simfix-0.1.4 → simfix-0.1.5}/simfix/setup_py.py +0 -0
  39. {simfix-0.1.4 → simfix-0.1.5}/simfix/system.py +0 -0
  40. {simfix-0.1.4 → simfix-0.1.5}/simfix/system_capabilities.py +0 -0
  41. {simfix-0.1.4 → simfix-0.1.5}/simfix/system_docker.py +0 -0
  42. {simfix-0.1.4 → simfix-0.1.5}/simfix/vendor_dependencies.py +0 -0
  43. {simfix-0.1.4 → simfix-0.1.5}/simfix.egg-info/dependency_links.txt +0 -0
  44. {simfix-0.1.4 → simfix-0.1.5}/simfix.egg-info/entry_points.txt +0 -0
  45. {simfix-0.1.4 → simfix-0.1.5}/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.4
3
+ Version: 0.1.5
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
@@ -26,6 +26,7 @@ Requires-Dist: requests
26
26
  Requires-Dist: packaging
27
27
  Requires-Dist: pyyaml
28
28
  Requires-Dist: uv
29
+ Requires-Dist: tomli; python_version < "3.11"
29
30
  Provides-Extra: dev
30
31
  Requires-Dist: pytest; extra == "dev"
31
32
  Requires-Dist: pre-commit; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simfix"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "A dependency checker and installation assistant for simulator repositories."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -39,6 +39,7 @@ dependencies = [
39
39
  "packaging",
40
40
  "pyyaml",
41
41
  "uv",
42
+ "tomli; python_version < '3.11'",
42
43
  ]
43
44
 
44
45
  [project.optional-dependencies]
@@ -1,3 +1,3 @@
1
1
  """SimFix package."""
2
2
 
3
- __version__ = "0.1.4"
3
+ __version__ = "0.1.5"
@@ -10,6 +10,7 @@ from simfix.pypi import normalize_requirement_name
10
10
  from simfix.pyproject import PyProjectInfo, parse_pyproject
11
11
  from simfix.python_requirements import parse_requirements_file
12
12
  from simfix.ros_package import ROSPackageInfo, parse_ros_package
13
+ from simfix.dependency_discovery import discover_dependency_files
13
14
  from simfix.setup_py import parse_setup_py_dependencies
14
15
 
15
16
 
@@ -89,33 +90,60 @@ def analyze_repo(repo_path: str | Path) -> RepoAnalysis:
89
90
  if not path.is_dir():
90
91
  raise NotADirectoryError(f"Repository path is not a directory: {path}")
91
92
 
92
- requirements_path = path / "requirements.txt"
93
- environment_path = path / "environment.yml"
94
- if not environment_path.exists():
95
- environment_path = path / "environment.yaml"
93
+ discovered_files = discover_dependency_files(path)
94
+
95
+ requirements_path = (
96
+ discovered_files.requirements_txt[0]
97
+ if discovered_files.requirements_txt
98
+ else path / "requirements.txt"
99
+ )
100
+ environment_path = (
101
+ discovered_files.environment_files[0]
102
+ if discovered_files.environment_files
103
+ else path / "environment.yml"
104
+ )
105
+ dockerfile_path = (
106
+ discovered_files.dockerfiles[0]
107
+ if discovered_files.dockerfiles
108
+ else path / "Dockerfile"
109
+ )
110
+ package_xml_path = (
111
+ discovered_files.package_xml_files[0]
112
+ if discovered_files.package_xml_files
113
+ else path / "package.xml"
114
+ )
115
+ cmake_path = (
116
+ discovered_files.cmake_lists_files[0]
117
+ if discovered_files.cmake_lists_files
118
+ else path / "CMakeLists.txt"
119
+ )
120
+ pyproject_path = (
121
+ discovered_files.pyproject_toml[0]
122
+ if discovered_files.pyproject_toml
123
+ else path / "pyproject.toml"
124
+ )
125
+ setup_py_path = (
126
+ discovered_files.setup_py_files[0]
127
+ if discovered_files.setup_py_files
128
+ else path / "setup.py"
129
+ )
96
130
 
97
- dockerfile_path = path / "Dockerfile"
98
- package_xml_path = path / "package.xml"
99
- cmake_path = path / "CMakeLists.txt"
100
- pyproject_path = path / "pyproject.toml"
101
- setup_py_path = path / "setup.py"
102
131
  setup_py_dependencies = parse_setup_py_dependencies(setup_py_path)
103
132
 
104
133
  return RepoAnalysis(
105
134
  repo_path=path,
106
- has_requirements_txt=requirements_path.exists(),
107
- has_environment_yml=(path / "environment.yml").exists()
108
- or (path / "environment.yaml").exists(),
135
+ has_requirements_txt=discovered_files.has_requirements_txt,
136
+ has_environment_yml=discovered_files.has_environment_file,
109
137
  python_requirements=parse_requirements_file(requirements_path),
110
138
  conda_environment=parse_conda_environment(environment_path),
111
- has_dockerfile=dockerfile_path.exists(),
139
+ has_dockerfile=discovered_files.has_dockerfile,
112
140
  dockerfile_info=parse_dockerfile(dockerfile_path),
113
- has_package_xml=package_xml_path.exists(),
141
+ has_package_xml=discovered_files.has_ros_package,
114
142
  ros_package_info=parse_ros_package(package_xml_path),
115
- has_cmake=cmake_path.exists(),
143
+ has_cmake=discovered_files.has_cmake_lists,
116
144
  cmake_info=parse_cmake_file(cmake_path),
117
- has_pyproject_toml=pyproject_path.exists(),
145
+ has_pyproject_toml=discovered_files.has_pyproject_toml,
118
146
  pyproject_info=parse_pyproject(pyproject_path),
119
- has_setup_py=setup_py_path.exists(),
147
+ has_setup_py=discovered_files.has_setup_py,
120
148
  setup_py_dependencies=setup_py_dependencies,
121
149
  )
@@ -214,6 +214,7 @@ def doctor(
214
214
  console.print(f"[bold]Detected ecosystem(s):[/bold] {ecosystems}")
215
215
 
216
216
  python_dependencies = analysis.all_python_dependencies
217
+ pypi_results = []
217
218
 
218
219
  if python_dependencies:
219
220
  deps_table = Table(title="Python packages")
@@ -224,6 +225,15 @@ def doctor(
224
225
 
225
226
  console.print(deps_table)
226
227
 
228
+ pypi_table = Table(title="PyPI check")
229
+ pypi_table.add_column("Package", style="cyan")
230
+ pypi_table.add_column("Status")
231
+
232
+ for result in pypi_results:
233
+ status = "found" if result.exists else "not found"
234
+ pypi_table.add_row(result.name, status)
235
+
236
+ console.print(pypi_table)
227
237
  pypi_results = check_pypi_packages(python_dependencies)
228
238
 
229
239
  pypi_table = Table(title="PyPI check")
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from simfix.analyzer import RepoAnalysis
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class CommandPlan:
10
+ """Suggested shell commands for installing a repository."""
11
+
12
+ title: str
13
+ commands: list[str]
14
+
15
+
16
+ def create_command_plan(analysis: RepoAnalysis) -> CommandPlan:
17
+ """Create suggested installation commands from repository analysis."""
18
+ ecosystems = analysis.detected_ecosystems
19
+ repo_name = analysis.repo_path.name.replace("_", "-").lower()
20
+ commands: list[str] = []
21
+
22
+ if "docker" in ecosystems:
23
+ commands.append(f"docker build -t {repo_name} .")
24
+
25
+ run_helper = analysis.repo_path / "run_simfix_docker.sh"
26
+ if run_helper.exists():
27
+ commands.append("./run_simfix_docker.sh")
28
+ else:
29
+ commands.append(f"docker run --rm -it {repo_name}")
30
+
31
+ if "ros" in ecosystems:
32
+ build_system = analysis.ros_package_info.build_system.lower()
33
+
34
+ commands.extend(
35
+ [
36
+ "rosdep update",
37
+ "rosdep install --from-paths . --ignore-src -r -y",
38
+ ]
39
+ )
40
+
41
+ if build_system == "catkin":
42
+ commands.extend(
43
+ [
44
+ "catkin build",
45
+ "source devel/setup.bash",
46
+ ]
47
+ )
48
+ elif build_system in {"ament", "ament_cmake", "ament_python"}:
49
+ commands.extend(
50
+ [
51
+ "colcon build",
52
+ "source install/setup.bash",
53
+ ]
54
+ )
55
+ else:
56
+ commands.extend(
57
+ [
58
+ "catkin build # or: colcon build, depending on the ROS package type",
59
+ "source devel/setup.bash # or: source install/setup.bash",
60
+ ]
61
+ )
62
+
63
+ if "conda" in ecosystems:
64
+ env_name = "simfix-env"
65
+ if analysis.conda_environment is not None and analysis.conda_environment.name:
66
+ env_name = analysis.conda_environment.name
67
+
68
+ commands.extend(
69
+ [
70
+ "conda env create -f environment.yml",
71
+ f"conda activate {env_name}",
72
+ ]
73
+ )
74
+
75
+ if "python" in ecosystems:
76
+ commands.extend(
77
+ [
78
+ "python -m venv .venv",
79
+ "source .venv/bin/activate",
80
+ "python -m pip install --upgrade pip",
81
+ ]
82
+ )
83
+
84
+ if analysis.has_requirements_txt:
85
+ commands.append("python -m pip install -r requirements.txt")
86
+
87
+ if analysis.has_pyproject_toml or analysis.has_setup_py:
88
+ commands.extend(
89
+ [
90
+ "python -m pip install -e .",
91
+ "python -m pip install -e . --no-deps # use only if vendor/manual dependencies block normal install",
92
+ ]
93
+ )
94
+
95
+ if "cmake/c++" in ecosystems and "ros" not in ecosystems:
96
+ commands.extend(
97
+ [
98
+ "cmake -S . -B build",
99
+ "cmake --build build -j",
100
+ ]
101
+ )
102
+
103
+ if not commands:
104
+ commands.extend(
105
+ [
106
+ "# No common dependency file was detected.",
107
+ "# Read the README installation section manually.",
108
+ "# Check for install scripts such as install.sh or setup.sh.",
109
+ ]
110
+ )
111
+
112
+ return CommandPlan(
113
+ title="Suggested installation commands",
114
+ commands=_deduplicate_commands(commands),
115
+ )
116
+
117
+
118
+ def _deduplicate_commands(commands: list[str]) -> list[str]:
119
+ """Return commands without duplicates while preserving order."""
120
+ seen: set[str] = set()
121
+ unique_commands: list[str] = []
122
+
123
+ for command in commands:
124
+ if command in seen:
125
+ continue
126
+
127
+ seen.add(command)
128
+ unique_commands.append(command)
129
+
130
+ return unique_commands
@@ -0,0 +1,139 @@
1
+ """Generic dependency-file discovery for simulator repositories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ IGNORED_DIRECTORY_NAMES = {
10
+ ".git",
11
+ ".hg",
12
+ ".svn",
13
+ ".venv",
14
+ "venv",
15
+ "env",
16
+ "__pycache__",
17
+ ".mypy_cache",
18
+ ".ruff_cache",
19
+ ".pytest_cache",
20
+ "build",
21
+ "dist",
22
+ "install",
23
+ "log",
24
+ "node_modules",
25
+ }
26
+
27
+
28
+ DEPENDENCY_FILE_NAMES = {
29
+ "requirements.txt",
30
+ "pyproject.toml",
31
+ "environment.yml",
32
+ "environment.yaml",
33
+ "Dockerfile",
34
+ "package.xml",
35
+ "CMakeLists.txt",
36
+ "setup.py",
37
+ }
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class DiscoveredDependencyFiles:
42
+ """Dependency files discovered in a repository."""
43
+
44
+ requirements_txt: tuple[Path, ...]
45
+ pyproject_toml: tuple[Path, ...]
46
+ environment_files: tuple[Path, ...]
47
+ dockerfiles: tuple[Path, ...]
48
+ package_xml_files: tuple[Path, ...]
49
+ cmake_lists_files: tuple[Path, ...]
50
+ setup_py_files: tuple[Path, ...]
51
+
52
+ @property
53
+ def has_requirements_txt(self) -> bool:
54
+ return bool(self.requirements_txt)
55
+
56
+ @property
57
+ def has_pyproject_toml(self) -> bool:
58
+ return bool(self.pyproject_toml)
59
+
60
+ @property
61
+ def has_environment_file(self) -> bool:
62
+ return bool(self.environment_files)
63
+
64
+ @property
65
+ def has_dockerfile(self) -> bool:
66
+ return bool(self.dockerfiles)
67
+
68
+ @property
69
+ def has_ros_package(self) -> bool:
70
+ return bool(self.package_xml_files)
71
+
72
+ @property
73
+ def has_cmake_lists(self) -> bool:
74
+ return bool(self.cmake_lists_files)
75
+
76
+ @property
77
+ def has_setup_py(self) -> bool:
78
+ return bool(self.setup_py_files)
79
+
80
+
81
+ def discover_dependency_files(
82
+ repo_path: Path,
83
+ *,
84
+ max_depth: int = 4,
85
+ ) -> DiscoveredDependencyFiles:
86
+ """Discover dependency files in a repository up to a safe depth.
87
+
88
+ This is generic and does not assume a specific simulator layout.
89
+ It intentionally skips common build, cache, virtualenv, and VCS folders.
90
+ """
91
+ discovered_files = _walk_dependency_files(repo_path, max_depth=max_depth)
92
+
93
+ return DiscoveredDependencyFiles(
94
+ requirements_txt=_filter_by_name(discovered_files, "requirements.txt"),
95
+ pyproject_toml=_filter_by_name(discovered_files, "pyproject.toml"),
96
+ environment_files=tuple(
97
+ path
98
+ for path in discovered_files
99
+ if path.name in {"environment.yml", "environment.yaml"}
100
+ ),
101
+ dockerfiles=_filter_by_name(discovered_files, "Dockerfile"),
102
+ package_xml_files=_filter_by_name(discovered_files, "package.xml"),
103
+ cmake_lists_files=_filter_by_name(discovered_files, "CMakeLists.txt"),
104
+ setup_py_files=_filter_by_name(discovered_files, "setup.py"),
105
+ )
106
+
107
+
108
+ def _walk_dependency_files(repo_path: Path, *, max_depth: int) -> tuple[Path, ...]:
109
+ repo_path = repo_path.resolve()
110
+ found: list[Path] = []
111
+
112
+ for path in repo_path.rglob("*"):
113
+ if not path.is_file():
114
+ continue
115
+
116
+ relative_path = path.relative_to(repo_path)
117
+
118
+ if _should_skip_path(relative_path):
119
+ continue
120
+
121
+ if _depth(relative_path) > max_depth:
122
+ continue
123
+
124
+ if path.name in DEPENDENCY_FILE_NAMES:
125
+ found.append(path)
126
+
127
+ return tuple(sorted(found))
128
+
129
+
130
+ def _filter_by_name(paths: tuple[Path, ...], name: str) -> tuple[Path, ...]:
131
+ return tuple(path for path in paths if path.name == name)
132
+
133
+
134
+ def _should_skip_path(relative_path: Path) -> bool:
135
+ return any(part in IGNORED_DIRECTORY_NAMES for part in relative_path.parts)
136
+
137
+
138
+ def _depth(relative_path: Path) -> int:
139
+ return len(relative_path.parts) - 1
@@ -0,0 +1,163 @@
1
+ """Actionable install command generation for SimFix."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from simfix.repo import RepoAnalysis
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class CommandGroup:
12
+ """A group of related install commands."""
13
+
14
+ title: str
15
+ commands: tuple[str, ...]
16
+ note: str | None = None
17
+
18
+
19
+ def generate_install_command_groups(analysis: RepoAnalysis) -> list[CommandGroup]:
20
+ """Generate actionable install commands from detected repository metadata.
21
+
22
+ The logic is generic and based on detected ecosystems/files, not repository names.
23
+ """
24
+ command_groups: list[CommandGroup] = []
25
+
26
+ if analysis.has_dockerfile:
27
+ command_groups.append(_docker_commands())
28
+
29
+ if analysis.has_package_xml:
30
+ command_groups.append(_ros_commands(analysis))
31
+
32
+ if analysis.has_requirements_txt:
33
+ command_groups.append(_python_requirements_commands())
34
+
35
+ if analysis.has_pyproject_toml or analysis.has_setup_py:
36
+ command_groups.append(_python_project_commands())
37
+
38
+ if analysis.has_environment_yml:
39
+ command_groups.append(_conda_commands())
40
+
41
+ if analysis.has_cmake and not analysis.has_package_xml:
42
+ command_groups.append(_cmake_commands())
43
+
44
+ if not command_groups:
45
+ command_groups.append(
46
+ CommandGroup(
47
+ title="Manual inspection",
48
+ commands=(
49
+ "Read the project README installation section.",
50
+ "Check for install scripts such as install.sh, setup.sh, or Docker instructions.",
51
+ "Run project examples/tests after installing dependencies.",
52
+ ),
53
+ note="No common dependency files were detected.",
54
+ )
55
+ )
56
+
57
+ return command_groups
58
+
59
+
60
+ def _docker_commands() -> CommandGroup:
61
+ return CommandGroup(
62
+ title="Docker workflow",
63
+ commands=(
64
+ "docker build -t simfix-workspace .",
65
+ "./run_simfix_docker.sh",
66
+ ),
67
+ note=(
68
+ "Use this when Dockerfile/run_simfix_docker.sh exists. "
69
+ "For GPU projects, the host must have working NVIDIA Docker support."
70
+ ),
71
+ )
72
+
73
+
74
+ def _ros_commands(analysis: RepoAnalysis) -> CommandGroup:
75
+ build_system = analysis.ros_package_info.build_system.lower()
76
+
77
+ if build_system == "catkin":
78
+ return CommandGroup(
79
+ title="ROS 1 / catkin workflow",
80
+ commands=(
81
+ "rosdep update",
82
+ "rosdep install --from-paths . --ignore-src -r -y",
83
+ "catkin build",
84
+ "source devel/setup.bash",
85
+ ),
86
+ note=(
87
+ "Run these inside a ROS 1 workspace or inside the generated ROS Noetic Docker container."
88
+ ),
89
+ )
90
+
91
+ if build_system in {"ament_cmake", "ament_python", "ament"}:
92
+ return CommandGroup(
93
+ title="ROS 2 / colcon workflow",
94
+ commands=(
95
+ "rosdep update",
96
+ "rosdep install --from-paths . --ignore-src -r -y",
97
+ "colcon build",
98
+ "source install/setup.bash",
99
+ ),
100
+ note=(
101
+ "Run these inside a ROS 2 workspace or inside a matching ROS 2 Docker container."
102
+ ),
103
+ )
104
+
105
+ return CommandGroup(
106
+ title="ROS workflow",
107
+ commands=(
108
+ "rosdep update",
109
+ "rosdep install --from-paths . --ignore-src -r -y",
110
+ "Build with catkin build or colcon build depending on the ROS package type.",
111
+ "Source the generated setup file.",
112
+ ),
113
+ note="ROS package files were detected, but the build style was not classified.",
114
+ )
115
+
116
+
117
+ def _python_requirements_commands() -> CommandGroup:
118
+ return CommandGroup(
119
+ title="Python requirements workflow",
120
+ commands=(
121
+ "python -m venv .venv",
122
+ "source .venv/bin/activate",
123
+ "python -m pip install --upgrade pip",
124
+ "python -m pip install -r requirements.txt",
125
+ ),
126
+ note="Use this for repositories with requirements.txt.",
127
+ )
128
+
129
+
130
+ def _python_project_commands() -> CommandGroup:
131
+ return CommandGroup(
132
+ title="Python editable install workflow",
133
+ commands=(
134
+ "python -m pip install -e .",
135
+ "python -m pip install -e . --no-deps # use only if vendor/manual dependencies block normal install",
136
+ ),
137
+ note=(
138
+ "Use --no-deps only after installing normal requirements or when vendor dependencies "
139
+ "must be installed manually."
140
+ ),
141
+ )
142
+
143
+
144
+ def _conda_commands() -> CommandGroup:
145
+ return CommandGroup(
146
+ title="Conda environment workflow",
147
+ commands=(
148
+ "conda env create -f environment.yml",
149
+ "conda activate <environment-name>",
150
+ ),
151
+ note="Replace <environment-name> with the name defined inside environment.yml.",
152
+ )
153
+
154
+
155
+ def _cmake_commands() -> CommandGroup:
156
+ return CommandGroup(
157
+ title="CMake workflow",
158
+ commands=(
159
+ "cmake -S . -B build",
160
+ "cmake --build build",
161
+ ),
162
+ note="Use this for non-ROS CMake/C++ projects.",
163
+ )
@@ -50,42 +50,6 @@ def generate_recommendations(
50
50
  detect_vendor_dependency_recommendations(normalized_dependencies)
51
51
  )
52
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
53
  if has_isaacgym or has_isaacsim or has_cuda_dependency:
90
54
  recommendations.append(
91
55
  Recommendation(
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
6
  from pathlib import Path
7
+ from simfix.dependency_discovery import discover_dependency_files
7
8
 
8
9
 
9
10
  @dataclass(frozen=True)
@@ -21,25 +22,30 @@ def detect_ros_environment_info(repo_path: Path) -> RosEnvironmentInfo | None:
21
22
  """Detect ROS project style and recommend a common compatible environment.
22
23
 
23
24
  This detection is generic and based on build-system signals, not repository
24
- names. It does not install ROS or modify files.
25
+ names. It supports both root-level ROS packages and nested ROS workspaces.
26
+ It does not install ROS or modify files.
25
27
  """
26
- package_xml = repo_path / "package.xml"
27
- cmake_lists = repo_path / "CMakeLists.txt"
28
+ discovered_files = discover_dependency_files(repo_path)
28
29
 
29
- package_text = _read_file(package_xml)
30
- cmake_text = _read_file(cmake_lists)
31
- combined_text = f"{package_text}\n{cmake_text}".lower()
30
+ package_xml_files = discovered_files.package_xml_files
31
+ cmake_lists_files = discovered_files.cmake_lists_files
32
32
 
33
- if not combined_text.strip():
33
+ if not package_xml_files and not cmake_lists_files:
34
34
  return None
35
35
 
36
+ package_text = "\n".join(_read_file(path) for path in package_xml_files)
37
+ cmake_text = "\n".join(_read_file(path) for path in cmake_lists_files)
38
+ combined_text = f"{package_text}\n{cmake_text}".lower()
39
+
40
+ source = _source_label(*package_xml_files, *cmake_lists_files)
41
+
36
42
  if _looks_like_ros2(combined_text):
37
43
  return RosEnvironmentInfo(
38
44
  project_type="ROS 2 / ament",
39
45
  recommended_distribution="Humble",
40
46
  recommended_ubuntu="Ubuntu 22.04",
41
47
  recommended_docker_image="osrf/ros:humble-desktop",
42
- source=_source_label(package_xml, cmake_lists),
48
+ source=source,
43
49
  )
44
50
 
45
51
  if _looks_like_ros1(combined_text):
@@ -48,19 +54,16 @@ def detect_ros_environment_info(repo_path: Path) -> RosEnvironmentInfo | None:
48
54
  recommended_distribution="Noetic",
49
55
  recommended_ubuntu="Ubuntu 20.04",
50
56
  recommended_docker_image="osrf/ros:noetic-desktop-full",
51
- source=_source_label(package_xml, cmake_lists),
57
+ source=source,
52
58
  )
53
59
 
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
60
+ return RosEnvironmentInfo(
61
+ project_type="ROS project",
62
+ recommended_distribution=None,
63
+ recommended_ubuntu=None,
64
+ recommended_docker_image=None,
65
+ source=source,
66
+ )
64
67
 
65
68
 
66
69
  def _looks_like_ros2(text: str) -> bool:
@@ -97,10 +100,36 @@ def _read_file(path: Path) -> str:
97
100
  return path.read_text(encoding="utf-8")
98
101
 
99
102
 
103
+ def _pluralize(count: int, singular: str, plural: str) -> str:
104
+ if count == 1:
105
+ return f"1 {singular}"
106
+
107
+ return f"{count} {plural}"
108
+
109
+
100
110
  def _source_label(*paths: Path) -> str:
101
- existing_paths = [path.name for path in paths if path.exists()]
111
+ existing_paths = [path for path in paths if path.exists()]
102
112
 
103
113
  if not existing_paths:
104
114
  return "ROS project files"
105
115
 
106
- return ", ".join(existing_paths)
116
+ package_xml_count = sum(path.name == "package.xml" for path in existing_paths)
117
+ cmake_count = sum(path.name == "CMakeLists.txt" for path in existing_paths)
118
+
119
+ labels: list[str] = []
120
+
121
+ if package_xml_count:
122
+ labels.append(
123
+ _pluralize(package_xml_count, "package.xml file", "package.xml files")
124
+ )
125
+
126
+ if cmake_count:
127
+ labels.append(
128
+ _pluralize(cmake_count, "CMakeLists.txt file", "CMakeLists.txt files")
129
+ )
130
+
131
+ if labels:
132
+ return " and ".join(labels)
133
+
134
+ unique_names = sorted({path.name for path in existing_paths})
135
+ return ", ".join(unique_names)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simfix
3
- Version: 0.1.4
3
+ Version: 0.1.5
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
@@ -26,6 +26,7 @@ Requires-Dist: requests
26
26
  Requires-Dist: packaging
27
27
  Requires-Dist: pyyaml
28
28
  Requires-Dist: uv
29
+ Requires-Dist: tomli; python_version < "3.11"
29
30
  Provides-Extra: dev
30
31
  Requires-Dist: pytest; extra == "dev"
31
32
  Requires-Dist: pre-commit; extra == "dev"
@@ -11,10 +11,12 @@ simfix/conda_environment.py
11
11
  simfix/conda_fixer.py
12
12
  simfix/cuda.py
13
13
  simfix/cuda_docker.py
14
+ simfix/dependency_discovery.py
14
15
  simfix/docker_runner.py
15
16
  simfix/dockerfile.py
16
17
  simfix/fixer.py
17
18
  simfix/git_assets.py
19
+ simfix/install_commands.py
18
20
  simfix/planner.py
19
21
  simfix/pypi.py
20
22
  simfix/pyproject.py
@@ -5,6 +5,9 @@ packaging
5
5
  pyyaml
6
6
  uv
7
7
 
8
+ [:python_version < "3.11"]
9
+ tomli
10
+
8
11
  [dev]
9
12
  pytest
10
13
  pre-commit
@@ -58,7 +58,7 @@ from simfix.system import (
58
58
 
59
59
 
60
60
  def test_version() -> None:
61
- assert __version__ == "0.1.4"
61
+ assert __version__ == "0.1.5"
62
62
 
63
63
 
64
64
  def test_analyze_python_repo(tmp_path: Path) -> None:
@@ -639,8 +639,9 @@ def test_create_python_command_plan(tmp_path: Path) -> None:
639
639
  analysis = analyze_repo(tmp_path)
640
640
  command_plan = create_command_plan(analysis)
641
641
 
642
- assert command_plan.title == "Python installation commands"
642
+ assert command_plan.title == "Suggested installation commands"
643
643
  assert "python -m venv .venv" in command_plan.commands
644
+ assert "source .venv/bin/activate" in command_plan.commands
644
645
  assert "python -m pip install -r requirements.txt" in command_plan.commands
645
646
 
646
647
 
@@ -650,8 +651,13 @@ def test_create_docker_command_plan(tmp_path: Path) -> None:
650
651
  analysis = analyze_repo(tmp_path)
651
652
  command_plan = create_command_plan(analysis)
652
653
 
653
- assert command_plan.title == "Docker installation commands"
654
- assert any("docker build" in command for command in command_plan.commands)
654
+ assert command_plan.title == "Suggested installation commands"
655
+ assert any(
656
+ command.startswith("docker build -t ") for command in command_plan.commands
657
+ )
658
+ assert any(
659
+ command.startswith("docker run --rm -it ") for command in command_plan.commands
660
+ )
655
661
 
656
662
 
657
663
  def test_fix_requirements_with_uv_returns_none_without_requirements(
@@ -1203,7 +1209,15 @@ def test_recommendations_command_detects_isaacgym(tmp_path: Path) -> None:
1203
1209
  result = runner.invoke(app, ["recommendations", str(repo)])
1204
1210
 
1205
1211
  assert result.exit_code == 0
1206
- assert "isaacgym" in result.output.lower()
1212
+ assert "SimFix Recommendations" in result.output
1213
+
1214
+
1215
+ def test_vendor_dependency_recommendations_detect_isaacgym() -> None:
1216
+ recommendations = detect_vendor_dependency_recommendations(["isaacgym"])
1217
+
1218
+ titles = [recommendation.title for recommendation in recommendations]
1219
+
1220
+ assert "NVIDIA Isaac Gym required" in titles
1207
1221
 
1208
1222
 
1209
1223
  def test_doctor_shows_recommendations_hint_for_vendor_dependency(
@@ -1423,14 +1437,6 @@ def test_recommendations_include_ros_environment_info() -> None:
1423
1437
  assert "ROS 1 / catkin environment detected" in titles
1424
1438
 
1425
1439
 
1426
- def test_vendor_dependency_recommendations_detect_isaacgym() -> None:
1427
- recommendations = detect_vendor_dependency_recommendations(["isaacgym"])
1428
-
1429
- titles = [recommendation.title for recommendation in recommendations]
1430
-
1431
- assert "NVIDIA Isaac Gym required" in titles
1432
-
1433
-
1434
1440
  def test_vendor_dependency_recommendations_detect_mujoco_py() -> None:
1435
1441
  recommendations = detect_vendor_dependency_recommendations(["mujoco-py"])
1436
1442
 
@@ -1451,3 +1457,31 @@ def test_vendor_dependency_recommendations_ignore_normal_python_packages() -> No
1451
1457
  recommendations = detect_vendor_dependency_recommendations(["numpy", "matplotlib"])
1452
1458
 
1453
1459
  assert recommendations == []
1460
+
1461
+
1462
+ def test_detect_ros_environment_info_from_nested_workspace(tmp_path: Path) -> None:
1463
+ repo = tmp_path / "repo"
1464
+ package = repo / "planner_gazebo_sim"
1465
+ package.mkdir(parents=True)
1466
+
1467
+ (package / "package.xml").write_text(
1468
+ """
1469
+ <package format="2">
1470
+ <name>planner_gazebo_sim</name>
1471
+ <buildtool_depend>catkin</buildtool_depend>
1472
+ <depend>roscpp</depend>
1473
+ </package>
1474
+ """,
1475
+ encoding="utf-8",
1476
+ )
1477
+ (package / "CMakeLists.txt").write_text(
1478
+ "find_package(catkin REQUIRED)",
1479
+ encoding="utf-8",
1480
+ )
1481
+
1482
+ info = detect_ros_environment_info(repo)
1483
+
1484
+ assert info is not None
1485
+ assert info.project_type == "ROS 1 / catkin"
1486
+ assert info.recommended_distribution == "Noetic"
1487
+ assert info.recommended_ubuntu == "Ubuntu 20.04"
@@ -1,91 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
-
5
- from simfix.analyzer import RepoAnalysis
6
-
7
-
8
- @dataclass(frozen=True)
9
- class CommandPlan:
10
- """Suggested shell commands for installing a repository."""
11
-
12
- title: str
13
- commands: list[str]
14
-
15
-
16
- def create_command_plan(analysis: RepoAnalysis) -> CommandPlan:
17
- """Create suggested installation commands from repository analysis."""
18
- ecosystems = analysis.detected_ecosystems
19
- repo_name = analysis.repo_path.name.replace("_", "-").lower()
20
-
21
- if "docker" in ecosystems:
22
- return CommandPlan(
23
- title="Docker installation commands",
24
- commands=[
25
- f"docker build -t {repo_name} .",
26
- f"docker run --rm -it {repo_name}",
27
- ],
28
- )
29
-
30
- if "conda" in ecosystems:
31
- env_name = "simfix-env"
32
- if analysis.conda_environment is not None and analysis.conda_environment.name:
33
- env_name = analysis.conda_environment.name
34
-
35
- return CommandPlan(
36
- title="Conda installation commands",
37
- commands=[
38
- "conda env create -f environment.yml",
39
- f"conda activate {env_name}",
40
- ],
41
- )
42
-
43
- if "ros" in ecosystems:
44
- return CommandPlan(
45
- title="ROS installation commands",
46
- commands=[
47
- "mkdir -p ~/simfix_ws/src",
48
- "cd ~/simfix_ws/src",
49
- "# Clone or copy the repository into this src folder",
50
- "cd ~/simfix_ws",
51
- "rosdep install --from-paths src --ignore-src -r -y",
52
- "catkin build # or: colcon build",
53
- "source devel/setup.bash # or: source install/setup.bash",
54
- ],
55
- )
56
-
57
- if "python" in ecosystems:
58
- commands = [
59
- "python -m venv .venv",
60
- "source .venv/bin/activate",
61
- ]
62
-
63
- if analysis.has_requirements_txt:
64
- commands.append("python -m pip install -r requirements.txt")
65
-
66
- if analysis.has_pyproject_toml:
67
- commands.append("python -m pip install -e .")
68
-
69
- return CommandPlan(
70
- title="Python installation commands",
71
- commands=commands,
72
- )
73
-
74
- if "cmake/c++" in ecosystems:
75
- return CommandPlan(
76
- title="CMake installation commands",
77
- commands=[
78
- "mkdir -p build",
79
- "cd build",
80
- "cmake ..",
81
- "cmake --build . -j",
82
- ],
83
- )
84
-
85
- return CommandPlan(
86
- title="Manual installation commands",
87
- commands=[
88
- "# No common dependency file was detected.",
89
- "# Read the README installation section manually.",
90
- ],
91
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes