hatch-xclam 0.7.0__py3-none-any.whl
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.
- hatch/__init__.py +21 -0
- hatch/cli_hatch.py +2748 -0
- hatch/environment_manager.py +1375 -0
- hatch/installers/__init__.py +25 -0
- hatch/installers/dependency_installation_orchestrator.py +636 -0
- hatch/installers/docker_installer.py +545 -0
- hatch/installers/hatch_installer.py +198 -0
- hatch/installers/installation_context.py +109 -0
- hatch/installers/installer_base.py +195 -0
- hatch/installers/python_installer.py +342 -0
- hatch/installers/registry.py +179 -0
- hatch/installers/system_installer.py +588 -0
- hatch/mcp_host_config/__init__.py +38 -0
- hatch/mcp_host_config/backup.py +458 -0
- hatch/mcp_host_config/host_management.py +572 -0
- hatch/mcp_host_config/models.py +602 -0
- hatch/mcp_host_config/reporting.py +181 -0
- hatch/mcp_host_config/strategies.py +513 -0
- hatch/package_loader.py +263 -0
- hatch/python_environment_manager.py +734 -0
- hatch/registry_explorer.py +171 -0
- hatch/registry_retriever.py +335 -0
- hatch/template_generator.py +179 -0
- hatch_xclam-0.7.0.dist-info/METADATA +150 -0
- hatch_xclam-0.7.0.dist-info/RECORD +93 -0
- hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
- hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
- hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
- hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/run_environment_tests.py +124 -0
- tests/test_cli_version.py +122 -0
- tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
- tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
- tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
- tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
- tests/test_data_utils.py +472 -0
- tests/test_dependency_orchestrator_consent.py +266 -0
- tests/test_docker_installer.py +524 -0
- tests/test_env_manip.py +991 -0
- tests/test_hatch_installer.py +179 -0
- tests/test_installer_base.py +221 -0
- tests/test_mcp_atomic_operations.py +276 -0
- tests/test_mcp_backup_integration.py +308 -0
- tests/test_mcp_cli_all_host_specific_args.py +303 -0
- tests/test_mcp_cli_backup_management.py +295 -0
- tests/test_mcp_cli_direct_management.py +453 -0
- tests/test_mcp_cli_discovery_listing.py +582 -0
- tests/test_mcp_cli_host_config_integration.py +823 -0
- tests/test_mcp_cli_package_management.py +360 -0
- tests/test_mcp_cli_partial_updates.py +859 -0
- tests/test_mcp_environment_integration.py +520 -0
- tests/test_mcp_host_config_backup.py +257 -0
- tests/test_mcp_host_configuration_manager.py +331 -0
- tests/test_mcp_host_registry_decorator.py +348 -0
- tests/test_mcp_pydantic_architecture_v4.py +603 -0
- tests/test_mcp_server_config_models.py +242 -0
- tests/test_mcp_server_config_type_field.py +221 -0
- tests/test_mcp_sync_functionality.py +316 -0
- tests/test_mcp_user_feedback_reporting.py +359 -0
- tests/test_non_tty_integration.py +281 -0
- tests/test_online_package_loader.py +202 -0
- tests/test_python_environment_manager.py +882 -0
- tests/test_python_installer.py +327 -0
- tests/test_registry.py +51 -0
- tests/test_registry_retriever.py +250 -0
- tests/test_system_installer.py +733 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
"""Installer for system package dependencies using apt.
|
|
2
|
+
|
|
3
|
+
This module implements installation logic for system packages using apt via subprocess,
|
|
4
|
+
with support for Ubuntu/Debian platforms, version constraints, and comprehensive error handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import platform
|
|
8
|
+
import subprocess
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, Any, Optional, Callable, List
|
|
15
|
+
from packaging.specifiers import SpecifierSet
|
|
16
|
+
|
|
17
|
+
from .installer_base import DependencyInstaller, InstallationError
|
|
18
|
+
from .installation_context import InstallationContext, InstallationResult, InstallationStatus
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SystemInstaller(DependencyInstaller):
|
|
22
|
+
"""Installer for system package dependencies using apt.
|
|
23
|
+
|
|
24
|
+
Handles installation of system packages using apt package manager via subprocess.
|
|
25
|
+
Supports Ubuntu/Debian platforms with platform detection and version constraint handling.
|
|
26
|
+
User consent is managed at the orchestrator level - this installer assumes permission
|
|
27
|
+
has been granted.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
"""Initialize the SystemInstaller."""
|
|
32
|
+
self.logger = logging.getLogger("hatch.installers.system_installer")
|
|
33
|
+
self.logger.setLevel(logging.INFO)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def installer_type(self) -> str:
|
|
37
|
+
"""Get the type identifier for this installer.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
str: Unique identifier for the installer type ("system").
|
|
41
|
+
"""
|
|
42
|
+
return "system"
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def supported_schemes(self) -> List[str]:
|
|
46
|
+
"""Get the URI schemes this installer can handle.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List[str]: List of URI schemes (["apt"] for apt package manager).
|
|
50
|
+
"""
|
|
51
|
+
return ["apt"]
|
|
52
|
+
|
|
53
|
+
def can_install(self, dependency: Dict[str, Any]) -> bool:
|
|
54
|
+
"""Check if this installer can handle the given dependency.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
dependency (Dict[str, Any]): Dependency object.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
bool: True if this installer can handle the dependency, False otherwise.
|
|
61
|
+
"""
|
|
62
|
+
if dependency.get("type") != self.installer_type:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
# Check platform compatibility
|
|
66
|
+
if not self._is_platform_supported():
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
# Check if apt is available
|
|
70
|
+
return self._is_apt_available()
|
|
71
|
+
|
|
72
|
+
def validate_dependency(self, dependency: Dict[str, Any]) -> bool:
|
|
73
|
+
"""Validate that a dependency object has required fields for system packages.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
dependency (Dict[str, Any]): Dependency object to validate.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
bool: True if dependency is valid, False otherwise.
|
|
80
|
+
"""
|
|
81
|
+
# Required fields per schema
|
|
82
|
+
required_fields = ["name", "version_constraint"]
|
|
83
|
+
if not all(field in dependency for field in required_fields):
|
|
84
|
+
self.logger.error(f"Missing required fields. Expected: {required_fields}, got: {list(dependency.keys())}")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
# Validate package manager
|
|
88
|
+
package_manager = dependency.get("package_manager", "apt")
|
|
89
|
+
if package_manager != "apt":
|
|
90
|
+
self.logger.error(f"Unsupported package manager: {package_manager}. Only 'apt' is supported.")
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
# Validate version constraint format
|
|
94
|
+
version_constraint = dependency.get("version_constraint", "")
|
|
95
|
+
if not self._validate_version_constraint(version_constraint):
|
|
96
|
+
self.logger.error(f"Invalid version constraint format: {version_constraint}")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
def install(self, dependency: Dict[str, Any], context: InstallationContext,
|
|
102
|
+
progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
|
|
103
|
+
"""Install a system dependency using apt.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
dependency (Dict[str, Any]): Dependency object containing:
|
|
107
|
+
- name (str): Name of the system package
|
|
108
|
+
- version_constraint (str): Version constraint
|
|
109
|
+
- package_manager (str): Must be "apt"
|
|
110
|
+
context (InstallationContext): Installation context with environment info
|
|
111
|
+
progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
InstallationResult: Result of the installation operation.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
InstallationError: If installation fails for any reason.
|
|
118
|
+
"""
|
|
119
|
+
if not self.validate_dependency(dependency):
|
|
120
|
+
raise InstallationError(
|
|
121
|
+
f"Invalid dependency: {dependency}",
|
|
122
|
+
dependency_name=dependency.get("name"),
|
|
123
|
+
error_code="INVALID_DEPENDENCY"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
package_name = dependency["name"]
|
|
127
|
+
version_constraint = dependency["version_constraint"]
|
|
128
|
+
|
|
129
|
+
if progress_callback:
|
|
130
|
+
progress_callback(f"Installing {package_name}", 0.0, "Starting installation")
|
|
131
|
+
|
|
132
|
+
self.logger.info(f"Installing system package: {package_name} with constraint: {version_constraint}")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# Handle dry-run/simulation mode
|
|
136
|
+
if context.simulation_mode:
|
|
137
|
+
return self._simulate_installation(dependency, context, progress_callback)
|
|
138
|
+
|
|
139
|
+
# Run apt-get update first
|
|
140
|
+
update_cmd = ["sudo", "apt-get", "update"]
|
|
141
|
+
update_returncode = self._run_apt_subprocess(update_cmd)
|
|
142
|
+
if update_returncode != 0:
|
|
143
|
+
raise InstallationError(
|
|
144
|
+
f"apt-get update failed (see logs for details).",
|
|
145
|
+
dependency_name=package_name,
|
|
146
|
+
error_code="APT_UPDATE_FAILED",
|
|
147
|
+
cause=None
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Build and execute apt install command
|
|
151
|
+
cmd = self._build_apt_command(dependency, context)
|
|
152
|
+
|
|
153
|
+
if progress_callback:
|
|
154
|
+
progress_callback(f"Installing {package_name}", 25.0, "Executing apt command")
|
|
155
|
+
|
|
156
|
+
returncode = self._run_apt_subprocess(cmd)
|
|
157
|
+
self.logger.debug(f"apt command: {cmd}\nreturn code: {returncode}")
|
|
158
|
+
|
|
159
|
+
if returncode != 0:
|
|
160
|
+
raise InstallationError(
|
|
161
|
+
f"Installation failed for {package_name} (see logs for details).",
|
|
162
|
+
dependency_name=package_name,
|
|
163
|
+
error_code="APT_INSTALL_FAILED",
|
|
164
|
+
cause=None
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if progress_callback:
|
|
168
|
+
progress_callback(f"Installing {package_name}", 75.0, "Verifying installation")
|
|
169
|
+
|
|
170
|
+
# Verify installation
|
|
171
|
+
installed_version = self._verify_installation(package_name)
|
|
172
|
+
|
|
173
|
+
if progress_callback:
|
|
174
|
+
progress_callback(f"Installing {package_name}", 100.0, "Installation complete")
|
|
175
|
+
|
|
176
|
+
return InstallationResult(
|
|
177
|
+
dependency_name=package_name,
|
|
178
|
+
status=InstallationStatus.COMPLETED,
|
|
179
|
+
installed_version=installed_version,
|
|
180
|
+
metadata={
|
|
181
|
+
"package_manager": "apt",
|
|
182
|
+
"command_executed": " ".join(cmd),
|
|
183
|
+
"platform": platform.platform(),
|
|
184
|
+
"automated": context.get_config("automated", False),
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
except InstallationError as e:
|
|
189
|
+
self.logger.error(f"Installation error for {package_name}: {str(e)}")
|
|
190
|
+
raise e
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
self.logger.error(f"Unexpected error installing {package_name}: {str(e)}")
|
|
194
|
+
raise InstallationError(
|
|
195
|
+
f"Unexpected error installing {package_name}: {str(e)}",
|
|
196
|
+
dependency_name=package_name,
|
|
197
|
+
error_code="UNEXPECTED_ERROR",
|
|
198
|
+
cause=e
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def uninstall(self, dependency: Dict[str, Any], context: InstallationContext,
|
|
202
|
+
progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
|
|
203
|
+
"""Uninstall a system dependency using apt.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
dependency (Dict[str, Any]): Dependency object to uninstall.
|
|
207
|
+
context (InstallationContext): Installation context.
|
|
208
|
+
progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
InstallationResult: Result of the uninstall operation.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
InstallationError: If uninstall fails for any reason.
|
|
215
|
+
"""
|
|
216
|
+
package_name = dependency["name"]
|
|
217
|
+
|
|
218
|
+
if progress_callback:
|
|
219
|
+
progress_callback(f"Uninstalling {package_name}", 0.0, "Starting uninstall")
|
|
220
|
+
|
|
221
|
+
self.logger.info(f"Uninstalling system package: {package_name}")
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
# Handle dry-run/simulation mode
|
|
225
|
+
if context.simulation_mode:
|
|
226
|
+
return self._simulate_uninstall(dependency, context, progress_callback)
|
|
227
|
+
|
|
228
|
+
# Build apt remove command
|
|
229
|
+
cmd = ["sudo", "apt", "remove", package_name]
|
|
230
|
+
|
|
231
|
+
# Add automation flag if configured
|
|
232
|
+
if context.get_config("automated", False):
|
|
233
|
+
cmd.append("-y")
|
|
234
|
+
|
|
235
|
+
if progress_callback:
|
|
236
|
+
progress_callback(f"Uninstalling {package_name}", 50.0, "Executing apt remove")
|
|
237
|
+
|
|
238
|
+
# Execute command
|
|
239
|
+
returncode = self._run_apt_subprocess(cmd)
|
|
240
|
+
|
|
241
|
+
if returncode != 0:
|
|
242
|
+
raise InstallationError(
|
|
243
|
+
f"Uninstallation failed for {package_name} (see logs for details).",
|
|
244
|
+
dependency_name=package_name,
|
|
245
|
+
error_code="APT_UNINSTALL_FAILED",
|
|
246
|
+
cause=None
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if progress_callback:
|
|
250
|
+
progress_callback(f"Uninstalling {package_name}", 100.0, "Uninstall complete")
|
|
251
|
+
|
|
252
|
+
return InstallationResult(
|
|
253
|
+
dependency_name=package_name,
|
|
254
|
+
status=InstallationStatus.COMPLETED,
|
|
255
|
+
metadata={
|
|
256
|
+
"operation": "uninstall",
|
|
257
|
+
"package_manager": "apt",
|
|
258
|
+
"command_executed": " ".join(cmd),
|
|
259
|
+
"automated": context.get_config("automated", False),
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
except InstallationError as e:
|
|
263
|
+
self.logger.error(f"Uninstallation error for {package_name}: {str(e)}")
|
|
264
|
+
raise e
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
self.logger.error(f"Unexpected error uninstalling {package_name}: {str(e)}")
|
|
268
|
+
raise InstallationError(
|
|
269
|
+
f"Unexpected error uninstalling {package_name}: {str(e)}",
|
|
270
|
+
dependency_name=package_name,
|
|
271
|
+
error_code="UNEXPECTED_ERROR",
|
|
272
|
+
cause=e
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def _is_platform_supported(self) -> bool:
|
|
276
|
+
"""Check if the current platform supports apt package manager.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
bool: True if platform is Ubuntu/Debian-based, False otherwise.
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
# Check if we're on a Debian-based system
|
|
283
|
+
if Path("/etc/debian_version").exists():
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
# Check platform string
|
|
287
|
+
system = platform.system().lower()
|
|
288
|
+
if system == "linux":
|
|
289
|
+
# Additional check for Ubuntu
|
|
290
|
+
try:
|
|
291
|
+
with open("/etc/os-release", "r") as f:
|
|
292
|
+
content = f.read().lower()
|
|
293
|
+
return "ubuntu" in content or "debian" in content
|
|
294
|
+
|
|
295
|
+
except FileNotFoundError:
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
except Exception:
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
def _is_apt_available(self) -> bool:
|
|
304
|
+
"""Check if apt command is available on the system.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
bool: True if apt is available, False otherwise.
|
|
308
|
+
"""
|
|
309
|
+
return shutil.which("apt") is not None
|
|
310
|
+
|
|
311
|
+
def _validate_version_constraint(self, version_constraint: str) -> bool:
|
|
312
|
+
"""Validate version constraint format.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
version_constraint (str): Version constraint to validate.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
bool: True if format is valid, False otherwise.
|
|
319
|
+
"""
|
|
320
|
+
try:
|
|
321
|
+
if not version_constraint.strip():
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
SpecifierSet(version_constraint)
|
|
325
|
+
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
except Exception:
|
|
329
|
+
self.logger.error(f"Invalid version constraint format: {version_constraint}")
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
def _build_apt_command(self, dependency: Dict[str, Any], context: InstallationContext) -> List[str]:
|
|
333
|
+
"""Build the apt install command for the dependency.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
dependency (Dict[str, Any]): Dependency object.
|
|
337
|
+
context (InstallationContext): Installation context.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
List[str]: Apt command as list of arguments.
|
|
341
|
+
"""
|
|
342
|
+
package_name = dependency["name"]
|
|
343
|
+
version_constraint = dependency["version_constraint"]
|
|
344
|
+
|
|
345
|
+
# Start with base command
|
|
346
|
+
command = ["sudo", "apt", "install"]
|
|
347
|
+
|
|
348
|
+
# Add automation flag if configured
|
|
349
|
+
if context.get_config("automated", False):
|
|
350
|
+
command.append("-y")
|
|
351
|
+
|
|
352
|
+
# Handle version constraints
|
|
353
|
+
# apt doesn't support complex version constraints directly,
|
|
354
|
+
# but we can specify exact versions for == constraints
|
|
355
|
+
if version_constraint.startswith("=="):
|
|
356
|
+
# Extract version from constraint like "== 1.2.3"
|
|
357
|
+
version = version_constraint.replace("==", "").strip()
|
|
358
|
+
package_spec = f"{package_name}={version}"
|
|
359
|
+
else:
|
|
360
|
+
# For other constraints (>=, <=, !=), install latest and let apt handle it
|
|
361
|
+
package_spec = package_name
|
|
362
|
+
self.logger.warning(f"Version constraint {version_constraint} simplified to latest version for {package_name}")
|
|
363
|
+
|
|
364
|
+
command.append(package_spec)
|
|
365
|
+
return command
|
|
366
|
+
|
|
367
|
+
def _run_apt_subprocess(self, cmd: List[str]) -> int:
|
|
368
|
+
"""Run an apt subprocess and return the return code.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
cmd (List[str]): The apt command to execute as a list.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
int: The return code of the process.
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
subprocess.TimeoutExpired: If the process times out.
|
|
378
|
+
InstallationError: For unexpected errors.
|
|
379
|
+
"""
|
|
380
|
+
env = os.environ.copy()
|
|
381
|
+
try:
|
|
382
|
+
|
|
383
|
+
process = subprocess.Popen(
|
|
384
|
+
cmd,
|
|
385
|
+
text=True,
|
|
386
|
+
universal_newlines=True
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
process.communicate() # Set a timeout for the command
|
|
390
|
+
process.wait() # Ensure cleanup
|
|
391
|
+
return process.returncode
|
|
392
|
+
|
|
393
|
+
except subprocess.TimeoutExpired:
|
|
394
|
+
process.kill()
|
|
395
|
+
process.wait() # Ensure cleanup
|
|
396
|
+
raise InstallationError("Apt subprocess timed out", error_code="TIMEOUT", cause=None)
|
|
397
|
+
|
|
398
|
+
except Exception as e:
|
|
399
|
+
raise InstallationError(
|
|
400
|
+
f"Unexpected error running apt command: {e}",
|
|
401
|
+
error_code="APT_SUBPROCESS_ERROR",
|
|
402
|
+
cause=e
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def _verify_installation(self, package_name: str) -> Optional[str]:
|
|
406
|
+
"""Verify that a package was installed and get its version.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
package_name (str): Name of package to verify.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Optional[str]: Installed version if found, None otherwise.
|
|
413
|
+
"""
|
|
414
|
+
try:
|
|
415
|
+
result = subprocess.run(
|
|
416
|
+
["apt-cache", "policy", package_name],
|
|
417
|
+
text=True,
|
|
418
|
+
capture_output=True,
|
|
419
|
+
check=False
|
|
420
|
+
)
|
|
421
|
+
if result.returncode == 0:
|
|
422
|
+
for line in result.stdout.splitlines():
|
|
423
|
+
if "***" in line:
|
|
424
|
+
parts = line.split()
|
|
425
|
+
if len(parts) > 1:
|
|
426
|
+
version = parts[1]
|
|
427
|
+
if version and version != "(none)":
|
|
428
|
+
return version
|
|
429
|
+
return None
|
|
430
|
+
except Exception:
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
def _parse_apt_error(self, error: InstallationError) -> str:
|
|
434
|
+
"""Parse apt error output to provide actionable error messages.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
error (InstallationError): The installation error.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
str: Human-readable error message with suggestions.
|
|
441
|
+
"""
|
|
442
|
+
error_output = error.message
|
|
443
|
+
|
|
444
|
+
# Common apt error patterns and suggestions
|
|
445
|
+
if "permission denied" in error_output.lower():
|
|
446
|
+
return "Permission denied. Try running with sudo or check user permissions."
|
|
447
|
+
elif "could not get lock" in error_output.lower():
|
|
448
|
+
return "Another package manager is running. Wait for it to finish and try again."
|
|
449
|
+
elif "unable to locate package" in error_output.lower():
|
|
450
|
+
return "Package not found. Check package name and update package lists with 'apt update'."
|
|
451
|
+
elif "network" in error_output.lower() or "connection" in error_output.lower():
|
|
452
|
+
return "Network connectivity issue. Check internet connection and repository availability."
|
|
453
|
+
elif "space" in error_output.lower():
|
|
454
|
+
return "Insufficient disk space. Free up space and try again."
|
|
455
|
+
else:
|
|
456
|
+
return f"Apt command failed: {error_output}"
|
|
457
|
+
|
|
458
|
+
def _simulate_installation(self, dependency: Dict[str, Any], context: InstallationContext,
|
|
459
|
+
progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
|
|
460
|
+
"""Simulate installation without making actual changes.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
dependency (Dict[str, Any]): Dependency object.
|
|
464
|
+
context (InstallationContext): Installation context.
|
|
465
|
+
progress_callback (Callable[[str, float, str], None], optional): Progress callback.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
InstallationResult: Simulated result.
|
|
469
|
+
"""
|
|
470
|
+
package_name = dependency["name"]
|
|
471
|
+
|
|
472
|
+
if progress_callback:
|
|
473
|
+
progress_callback(f"Simulating {package_name}", 0.5, "Running dry-run")
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
# Use apt's dry-run functionality - need to use apt-get with --dry-run
|
|
477
|
+
cmd = ["apt-get", "install", "--dry-run", dependency["name"]]
|
|
478
|
+
|
|
479
|
+
# Add automation flag if configured
|
|
480
|
+
if context.get_config("automated", False):
|
|
481
|
+
cmd.append("-y")
|
|
482
|
+
|
|
483
|
+
returncode = self._run_apt_subprocess(cmd)
|
|
484
|
+
|
|
485
|
+
if returncode != 0:
|
|
486
|
+
raise InstallationError(
|
|
487
|
+
f"Simulation failed for {package_name} (see logs for details).",
|
|
488
|
+
dependency_name=package_name,
|
|
489
|
+
error_code="APT_SIMULATION_FAILED",
|
|
490
|
+
cause=None
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
if progress_callback:
|
|
494
|
+
progress_callback(f"Simulating {package_name}", 1.0, "Simulation complete")
|
|
495
|
+
|
|
496
|
+
return InstallationResult(
|
|
497
|
+
dependency_name=package_name,
|
|
498
|
+
status=InstallationStatus.COMPLETED,
|
|
499
|
+
metadata={
|
|
500
|
+
"simulation": True,
|
|
501
|
+
"command_simulated": " ".join(cmd),
|
|
502
|
+
"automated": context.get_config("automated", False),
|
|
503
|
+
"package_manager": "apt",
|
|
504
|
+
}
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
except InstallationError as e:
|
|
508
|
+
self.logger.error(f"Error during installation simulation for {package_name}: {e.message}")
|
|
509
|
+
raise e
|
|
510
|
+
|
|
511
|
+
except Exception as e:
|
|
512
|
+
return InstallationResult(
|
|
513
|
+
dependency_name=package_name,
|
|
514
|
+
status=InstallationStatus.FAILED,
|
|
515
|
+
error_message=f"Simulation failed: {e}",
|
|
516
|
+
metadata={
|
|
517
|
+
"simulation": True,
|
|
518
|
+
"simulation_error": e,
|
|
519
|
+
"command_simulated": " ".join(cmd),
|
|
520
|
+
"automated": context.get_config("automated", False)
|
|
521
|
+
}
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
def _simulate_uninstall(self, dependency: Dict[str, Any], context: InstallationContext,
|
|
525
|
+
progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
|
|
526
|
+
"""Simulate uninstall without making actual changes.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
dependency (Dict[str, Any]): Dependency object.
|
|
530
|
+
context (InstallationContext): Installation context.
|
|
531
|
+
progress_callback (Callable[[str, float, str], None], optional): Progress callback.
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
InstallationResult: Simulated result.
|
|
535
|
+
"""
|
|
536
|
+
package_name = dependency["name"]
|
|
537
|
+
|
|
538
|
+
if progress_callback:
|
|
539
|
+
progress_callback(f"Simulating uninstall {package_name}", 0.5, "Running dry-run")
|
|
540
|
+
|
|
541
|
+
try:
|
|
542
|
+
# Use apt's dry-run functionality for remove - use apt-get with --dry-run
|
|
543
|
+
cmd = ["apt-get", "remove", "--dry-run", dependency["name"]]
|
|
544
|
+
returncode = self._run_apt_subprocess(cmd)
|
|
545
|
+
|
|
546
|
+
if returncode != 0:
|
|
547
|
+
raise InstallationError(
|
|
548
|
+
f"Uninstall simulation failed for {package_name} (see logs for details).",
|
|
549
|
+
dependency_name=package_name,
|
|
550
|
+
error_code="APT_UNINSTALL_SIMULATION_FAILED",
|
|
551
|
+
cause=None
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if progress_callback:
|
|
555
|
+
progress_callback(f"Simulating uninstall {package_name}", 1.0, "Simulation complete")
|
|
556
|
+
|
|
557
|
+
return InstallationResult(
|
|
558
|
+
dependency_name=package_name,
|
|
559
|
+
status=InstallationStatus.COMPLETED,
|
|
560
|
+
metadata={
|
|
561
|
+
"operation": "uninstall",
|
|
562
|
+
"simulation": True,
|
|
563
|
+
"command_simulated": " ".join(cmd),
|
|
564
|
+
"automated": context.get_config("automated", False)
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
except InstallationError as e:
|
|
569
|
+
self.logger.error(f"Uninstall simulation error for {package_name}: {str(e)}")
|
|
570
|
+
raise e
|
|
571
|
+
|
|
572
|
+
except Exception as e:
|
|
573
|
+
return InstallationResult(
|
|
574
|
+
dependency_name=package_name,
|
|
575
|
+
status=InstallationStatus.FAILED,
|
|
576
|
+
error_message=f"Uninstall simulation failed: {str(e)}",
|
|
577
|
+
metadata={
|
|
578
|
+
"operation": "uninstall",
|
|
579
|
+
"simulation": True,
|
|
580
|
+
"simulation_error": str(e),
|
|
581
|
+
"command_simulated": " ".join(cmd),
|
|
582
|
+
"automated": context.get_config("automated", False)
|
|
583
|
+
}
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Register this installer with the global registry
|
|
587
|
+
from .registry import installer_registry
|
|
588
|
+
installer_registry.register_installer("system", SystemInstaller)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) support for Hatch.
|
|
2
|
+
|
|
3
|
+
This module provides MCP host configuration management functionality,
|
|
4
|
+
including backup and restore capabilities for MCP server configurations,
|
|
5
|
+
decorator-based strategy registration, and consolidated Pydantic models.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .backup import MCPHostConfigBackupManager
|
|
9
|
+
from .models import (
|
|
10
|
+
MCPHostType, MCPServerConfig, HostConfiguration, EnvironmentData,
|
|
11
|
+
PackageHostConfiguration, EnvironmentPackageEntry, ConfigurationResult, SyncResult,
|
|
12
|
+
# Host-specific configuration models
|
|
13
|
+
MCPServerConfigBase, MCPServerConfigGemini, MCPServerConfigVSCode,
|
|
14
|
+
MCPServerConfigCursor, MCPServerConfigClaude, MCPServerConfigOmni,
|
|
15
|
+
HOST_MODEL_REGISTRY
|
|
16
|
+
)
|
|
17
|
+
from .host_management import (
|
|
18
|
+
MCPHostRegistry, MCPHostStrategy, MCPHostConfigurationManager, register_host_strategy
|
|
19
|
+
)
|
|
20
|
+
from .reporting import (
|
|
21
|
+
FieldOperation, ConversionReport, generate_conversion_report, display_report
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Import strategies to trigger decorator registration
|
|
25
|
+
from . import strategies
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
'MCPHostConfigBackupManager',
|
|
29
|
+
'MCPHostType', 'MCPServerConfig', 'HostConfiguration', 'EnvironmentData',
|
|
30
|
+
'PackageHostConfiguration', 'EnvironmentPackageEntry', 'ConfigurationResult', 'SyncResult',
|
|
31
|
+
# Host-specific configuration models
|
|
32
|
+
'MCPServerConfigBase', 'MCPServerConfigGemini', 'MCPServerConfigVSCode',
|
|
33
|
+
'MCPServerConfigCursor', 'MCPServerConfigClaude', 'MCPServerConfigOmni',
|
|
34
|
+
'HOST_MODEL_REGISTRY',
|
|
35
|
+
# User feedback reporting
|
|
36
|
+
'FieldOperation', 'ConversionReport', 'generate_conversion_report', 'display_report',
|
|
37
|
+
'MCPHostRegistry', 'MCPHostStrategy', 'MCPHostConfigurationManager', 'register_host_strategy'
|
|
38
|
+
]
|