Skip to content

System Installer

hatch.installers.system_installer

Installer for system package dependencies using apt.

This module implements installation logic for system packages using apt via subprocess, with support for Ubuntu/Debian platforms, version constraints, and comprehensive error handling.

Classes

SystemInstaller

Bases: DependencyInstaller

Installer for system package dependencies using apt.

Handles installation of system packages using apt package manager via subprocess. Supports Ubuntu/Debian platforms with platform detection and version constraint handling. User consent is managed at the orchestrator level - this installer assumes permission has been granted.

Source code in hatch/installers/system_installer.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
class SystemInstaller(DependencyInstaller):
    """Installer for system package dependencies using apt.

    Handles installation of system packages using apt package manager via subprocess.
    Supports Ubuntu/Debian platforms with platform detection and version constraint handling.
    User consent is managed at the orchestrator level - this installer assumes permission
    has been granted.
    """

    def __init__(self):
        """Initialize the SystemInstaller."""
        self.logger = logging.getLogger("hatch.installers.system_installer")

    @property
    def installer_type(self) -> str:
        """Get the type identifier for this installer.

        Returns:
            str: Unique identifier for the installer type ("system").
        """
        return "system"

    @property
    def supported_schemes(self) -> List[str]:
        """Get the URI schemes this installer can handle.

        Returns:
            List[str]: List of URI schemes (["apt"] for apt package manager).
        """
        return ["apt"]

    def can_install(self, dependency: Dict[str, Any]) -> bool:
        """Check if this installer can handle the given dependency.

        Args:
            dependency (Dict[str, Any]): Dependency object.

        Returns:
            bool: True if this installer can handle the dependency, False otherwise.
        """
        if dependency.get("type") != self.installer_type:
            return False

        # Check platform compatibility
        if not self._is_platform_supported():
            return False

        # Check if apt is available
        return self._is_apt_available()

    def validate_dependency(self, dependency: Dict[str, Any]) -> bool:
        """Validate that a dependency object has required fields for system packages.

        Args:
            dependency (Dict[str, Any]): Dependency object to validate.

        Returns:
            bool: True if dependency is valid, False otherwise.
        """
        # Required fields per schema
        required_fields = ["name", "version_constraint"]
        if not all(field in dependency for field in required_fields):
            self.logger.error(
                f"Missing required fields. Expected: {required_fields}, got: {list(dependency.keys())}"
            )
            return False

        # Validate package manager
        package_manager = dependency.get("package_manager", "apt")
        if package_manager != "apt":
            self.logger.error(
                f"Unsupported package manager: {package_manager}. Only 'apt' is supported."
            )
            return False

        # Validate version constraint format
        version_constraint = dependency.get("version_constraint", "")
        if not self._validate_version_constraint(version_constraint):
            self.logger.error(
                f"Invalid version constraint format: {version_constraint}"
            )
            return False

        return True

    def install(
        self,
        dependency: Dict[str, Any],
        context: InstallationContext,
        progress_callback: Optional[Callable[[str, float, str], None]] = None,
    ) -> InstallationResult:
        """Install a system dependency using apt.

        Args:
            dependency (Dict[str, Any]): Dependency object containing:
                - name (str): Name of the system package
                - version_constraint (str): Version constraint
                - package_manager (str): Must be "apt"
            context (InstallationContext): Installation context with environment info
            progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.

        Returns:
            InstallationResult: Result of the installation operation.

        Raises:
            InstallationError: If installation fails for any reason.
        """
        if not self.validate_dependency(dependency):
            raise InstallationError(
                f"Invalid dependency: {dependency}",
                dependency_name=dependency.get("name"),
                error_code="INVALID_DEPENDENCY",
            )

        package_name = dependency["name"]
        version_constraint = dependency["version_constraint"]

        if progress_callback:
            progress_callback(
                f"Installing {package_name}", 0.0, "Starting installation"
            )

        self.logger.info(
            f"Installing system package: {package_name} with constraint: {version_constraint}"
        )

        try:
            # Handle dry-run/simulation mode
            if context.simulation_mode:
                return self._simulate_installation(
                    dependency, context, progress_callback
                )

            # Run apt-get update first
            update_cmd = ["sudo", "apt-get", "update"]
            update_returncode = self._run_apt_subprocess(update_cmd)
            if update_returncode != 0:
                raise InstallationError(
                    "apt-get update failed (see logs for details).",
                    dependency_name=package_name,
                    error_code="APT_UPDATE_FAILED",
                    cause=None,
                )

            # Build and execute apt install command
            cmd = self._build_apt_command(dependency, context)

            if progress_callback:
                progress_callback(
                    f"Installing {package_name}", 25.0, "Executing apt command"
                )

            returncode = self._run_apt_subprocess(cmd)
            self.logger.debug(f"apt command: {cmd}\nreturn code: {returncode}")

            if returncode != 0:
                raise InstallationError(
                    f"Installation failed for {package_name} (see logs for details).",
                    dependency_name=package_name,
                    error_code="APT_INSTALL_FAILED",
                    cause=None,
                )

            if progress_callback:
                progress_callback(
                    f"Installing {package_name}", 75.0, "Verifying installation"
                )

            # Verify installation
            installed_version = self._verify_installation(package_name)

            if progress_callback:
                progress_callback(
                    f"Installing {package_name}", 100.0, "Installation complete"
                )

            return InstallationResult(
                dependency_name=package_name,
                status=InstallationStatus.COMPLETED,
                installed_version=installed_version,
                metadata={
                    "package_manager": "apt",
                    "command_executed": " ".join(cmd),
                    "platform": platform.platform(),
                    "automated": context.get_config("automated", False),
                },
            )

        except InstallationError as e:
            self.logger.error(f"Installation error for {package_name}: {str(e)}")
            raise e

        except Exception as e:
            self.logger.error(f"Unexpected error installing {package_name}: {str(e)}")
            raise InstallationError(
                f"Unexpected error installing {package_name}: {str(e)}",
                dependency_name=package_name,
                error_code="UNEXPECTED_ERROR",
                cause=e,
            )

    def uninstall(
        self,
        dependency: Dict[str, Any],
        context: InstallationContext,
        progress_callback: Optional[Callable[[str, float, str], None]] = None,
    ) -> InstallationResult:
        """Uninstall a system dependency using apt.

        Args:
            dependency (Dict[str, Any]): Dependency object to uninstall.
            context (InstallationContext): Installation context.
            progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.

        Returns:
            InstallationResult: Result of the uninstall operation.

        Raises:
            InstallationError: If uninstall fails for any reason.
        """
        package_name = dependency["name"]

        if progress_callback:
            progress_callback(f"Uninstalling {package_name}", 0.0, "Starting uninstall")

        self.logger.info(f"Uninstalling system package: {package_name}")

        try:
            # Handle dry-run/simulation mode
            if context.simulation_mode:
                return self._simulate_uninstall(dependency, context, progress_callback)

            # Build apt remove command
            cmd = ["sudo", "apt", "remove", package_name]

            # Add automation flag if configured
            if context.get_config("automated", False):
                cmd.append("-y")

            if progress_callback:
                progress_callback(
                    f"Uninstalling {package_name}", 50.0, "Executing apt remove"
                )

            # Execute command
            returncode = self._run_apt_subprocess(cmd)

            if returncode != 0:
                raise InstallationError(
                    f"Uninstallation failed for {package_name} (see logs for details).",
                    dependency_name=package_name,
                    error_code="APT_UNINSTALL_FAILED",
                    cause=None,
                )

            if progress_callback:
                progress_callback(
                    f"Uninstalling {package_name}", 100.0, "Uninstall complete"
                )

            return InstallationResult(
                dependency_name=package_name,
                status=InstallationStatus.COMPLETED,
                metadata={
                    "operation": "uninstall",
                    "package_manager": "apt",
                    "command_executed": " ".join(cmd),
                    "automated": context.get_config("automated", False),
                },
            )
        except InstallationError as e:
            self.logger.error(f"Uninstallation error for {package_name}: {str(e)}")
            raise e

        except Exception as e:
            self.logger.error(f"Unexpected error uninstalling {package_name}: {str(e)}")
            raise InstallationError(
                f"Unexpected error uninstalling {package_name}: {str(e)}",
                dependency_name=package_name,
                error_code="UNEXPECTED_ERROR",
                cause=e,
            )

    def _is_platform_supported(self) -> bool:
        """Check if the current platform supports apt package manager.

        Returns:
            bool: True if platform is Ubuntu/Debian-based, False otherwise.
        """
        try:
            # Check if we're on a Debian-based system
            if Path("/etc/debian_version").exists():
                return True

            # Check platform string
            system = platform.system().lower()
            if system == "linux":
                # Additional check for Ubuntu
                try:
                    with open("/etc/os-release", "r") as f:
                        content = f.read().lower()
                        return "ubuntu" in content or "debian" in content

                except FileNotFoundError:
                    pass

            return False

        except Exception:
            return False

    def _is_apt_available(self) -> bool:
        """Check if apt command is available on the system.

        Returns:
            bool: True if apt is available, False otherwise.
        """
        return shutil.which("apt") is not None

    def _validate_version_constraint(self, version_constraint: str) -> bool:
        """Validate version constraint format.

        Args:
            version_constraint (str): Version constraint to validate.

        Returns:
            bool: True if format is valid, False otherwise.
        """
        try:
            if not version_constraint.strip():
                return True

            SpecifierSet(version_constraint)

            return True

        except Exception:
            self.logger.error(
                f"Invalid version constraint format: {version_constraint}"
            )
            return False

    def _build_apt_command(
        self, dependency: Dict[str, Any], context: InstallationContext
    ) -> List[str]:
        """Build the apt install command for the dependency.

        Args:
            dependency (Dict[str, Any]): Dependency object.
            context (InstallationContext): Installation context.

        Returns:
            List[str]: Apt command as list of arguments.
        """
        package_name = dependency["name"]
        version_constraint = dependency["version_constraint"]

        # Start with base command
        command = ["sudo", "apt", "install"]

        # Add automation flag if configured
        if context.get_config("automated", False):
            command.append("-y")

        # Handle version constraints
        # apt doesn't support complex version constraints directly,
        # but we can specify exact versions for == constraints
        if version_constraint.startswith("=="):
            # Extract version from constraint like "== 1.2.3"
            version = version_constraint.replace("==", "").strip()
            package_spec = f"{package_name}={version}"
        else:
            # For other constraints (>=, <=, !=), install latest and let apt handle it
            package_spec = package_name
            self.logger.warning(
                f"Version constraint {version_constraint} simplified to latest version for {package_name}"
            )

        command.append(package_spec)
        return command

    def _run_apt_subprocess(self, cmd: List[str]) -> int:
        """Run an apt subprocess and return the return code.

        Args:
            cmd (List[str]): The apt command to execute as a list.

        Returns:
            int: The return code of the process.

        Raises:
            subprocess.TimeoutExpired: If the process times out.
            InstallationError: For unexpected errors.
        """
        # env = os.environ.copy()  # Reserved for future environment customization
        try:
            process = subprocess.Popen(cmd, text=True, universal_newlines=True)

            process.communicate()  # Set a timeout for the command
            process.wait()  # Ensure cleanup
            return process.returncode

        except subprocess.TimeoutExpired:
            process.kill()
            process.wait()  # Ensure cleanup
            raise InstallationError(
                "Apt subprocess timed out", error_code="TIMEOUT", cause=None
            )

        except Exception as e:
            raise InstallationError(
                f"Unexpected error running apt command: {e}",
                error_code="APT_SUBPROCESS_ERROR",
                cause=e,
            )

    def _verify_installation(self, package_name: str) -> Optional[str]:
        """Verify that a package was installed and get its version.

        Args:
            package_name (str): Name of package to verify.

        Returns:
            Optional[str]: Installed version if found, None otherwise.
        """
        try:
            result = subprocess.run(
                ["apt-cache", "policy", package_name],
                text=True,
                capture_output=True,
                check=False,
            )
            if result.returncode == 0:
                for line in result.stdout.splitlines():
                    if "***" in line:
                        parts = line.split()
                        if len(parts) > 1:
                            version = parts[1]
                            if version and version != "(none)":
                                return version
            return None
        except Exception:
            return None

    def _parse_apt_error(self, error: InstallationError) -> str:
        """Parse apt error output to provide actionable error messages.

        Args:
            error (InstallationError): The installation error.

        Returns:
            str: Human-readable error message with suggestions.
        """
        error_output = error.message

        # Common apt error patterns and suggestions
        if "permission denied" in error_output.lower():
            return "Permission denied. Try running with sudo or check user permissions."
        elif "could not get lock" in error_output.lower():
            return "Another package manager is running. Wait for it to finish and try again."
        elif "unable to locate package" in error_output.lower():
            return "Package not found. Check package name and update package lists with 'apt update'."
        elif "network" in error_output.lower() or "connection" in error_output.lower():
            return "Network connectivity issue. Check internet connection and repository availability."
        elif "space" in error_output.lower():
            return "Insufficient disk space. Free up space and try again."
        else:
            return f"Apt command failed: {error_output}"

    def _simulate_installation(
        self,
        dependency: Dict[str, Any],
        context: InstallationContext,
        progress_callback: Optional[Callable[[str, float, str], None]] = None,
    ) -> InstallationResult:
        """Simulate installation without making actual changes.

        Args:
            dependency (Dict[str, Any]): Dependency object.
            context (InstallationContext): Installation context.
            progress_callback (Callable[[str, float, str], None], optional): Progress callback.

        Returns:
            InstallationResult: Simulated result.
        """
        package_name = dependency["name"]

        if progress_callback:
            progress_callback(f"Simulating {package_name}", 0.5, "Running dry-run")

        try:
            # Use apt's dry-run functionality - need to use apt-get with --dry-run
            cmd = ["apt-get", "install", "--dry-run", dependency["name"]]

            # Add automation flag if configured
            if context.get_config("automated", False):
                cmd.append("-y")

            returncode = self._run_apt_subprocess(cmd)

            if returncode != 0:
                raise InstallationError(
                    f"Simulation failed for {package_name} (see logs for details).",
                    dependency_name=package_name,
                    error_code="APT_SIMULATION_FAILED",
                    cause=None,
                )

            if progress_callback:
                progress_callback(
                    f"Simulating {package_name}", 1.0, "Simulation complete"
                )

            return InstallationResult(
                dependency_name=package_name,
                status=InstallationStatus.COMPLETED,
                metadata={
                    "simulation": True,
                    "command_simulated": " ".join(cmd),
                    "automated": context.get_config("automated", False),
                    "package_manager": "apt",
                },
            )

        except InstallationError as e:
            self.logger.error(
                f"Error during installation simulation for {package_name}: {e.message}"
            )
            raise e

        except Exception as e:
            return InstallationResult(
                dependency_name=package_name,
                status=InstallationStatus.FAILED,
                error_message=f"Simulation failed: {e}",
                metadata={
                    "simulation": True,
                    "simulation_error": e,
                    "command_simulated": " ".join(cmd),
                    "automated": context.get_config("automated", False),
                },
            )

    def _simulate_uninstall(
        self,
        dependency: Dict[str, Any],
        context: InstallationContext,
        progress_callback: Optional[Callable[[str, float, str], None]] = None,
    ) -> InstallationResult:
        """Simulate uninstall without making actual changes.

        Args:
            dependency (Dict[str, Any]): Dependency object.
            context (InstallationContext): Installation context.
            progress_callback (Callable[[str, float, str], None], optional): Progress callback.

        Returns:
            InstallationResult: Simulated result.
        """
        package_name = dependency["name"]

        if progress_callback:
            progress_callback(
                f"Simulating uninstall {package_name}", 0.5, "Running dry-run"
            )

        try:
            # Use apt's dry-run functionality for remove - use apt-get with --dry-run
            cmd = ["apt-get", "remove", "--dry-run", dependency["name"]]
            returncode = self._run_apt_subprocess(cmd)

            if returncode != 0:
                raise InstallationError(
                    f"Uninstall simulation failed for {package_name} (see logs for details).",
                    dependency_name=package_name,
                    error_code="APT_UNINSTALL_SIMULATION_FAILED",
                    cause=None,
                )

            if progress_callback:
                progress_callback(
                    f"Simulating uninstall {package_name}", 1.0, "Simulation complete"
                )

            return InstallationResult(
                dependency_name=package_name,
                status=InstallationStatus.COMPLETED,
                metadata={
                    "operation": "uninstall",
                    "simulation": True,
                    "command_simulated": " ".join(cmd),
                    "automated": context.get_config("automated", False),
                },
            )

        except InstallationError as e:
            self.logger.error(
                f"Uninstall simulation error for {package_name}: {str(e)}"
            )
            raise e

        except Exception as e:
            return InstallationResult(
                dependency_name=package_name,
                status=InstallationStatus.FAILED,
                error_message=f"Uninstall simulation failed: {str(e)}",
                metadata={
                    "operation": "uninstall",
                    "simulation": True,
                    "simulation_error": str(e),
                    "command_simulated": " ".join(cmd),
                    "automated": context.get_config("automated", False),
                },
            )
Attributes
installer_type property

Get the type identifier for this installer.

Returns:

Name Type Description
str str

Unique identifier for the installer type ("system").

supported_schemes property

Get the URI schemes this installer can handle.

Returns:

Type Description
List[str]

List[str]: List of URI schemes (["apt"] for apt package manager).

Functions
__init__()

Initialize the SystemInstaller.

Source code in hatch/installers/system_installer.py
def __init__(self):
    """Initialize the SystemInstaller."""
    self.logger = logging.getLogger("hatch.installers.system_installer")
can_install(dependency)

Check if this installer can handle the given dependency.

Parameters:

Name Type Description Default
dependency Dict[str, Any]

Dependency object.

required

Returns:

Name Type Description
bool bool

True if this installer can handle the dependency, False otherwise.

Source code in hatch/installers/system_installer.py
def can_install(self, dependency: Dict[str, Any]) -> bool:
    """Check if this installer can handle the given dependency.

    Args:
        dependency (Dict[str, Any]): Dependency object.

    Returns:
        bool: True if this installer can handle the dependency, False otherwise.
    """
    if dependency.get("type") != self.installer_type:
        return False

    # Check platform compatibility
    if not self._is_platform_supported():
        return False

    # Check if apt is available
    return self._is_apt_available()
install(dependency, context, progress_callback=None)

Install a system dependency using apt.

Parameters:

Name Type Description Default
dependency Dict[str, Any]

Dependency object containing: - name (str): Name of the system package - version_constraint (str): Version constraint - package_manager (str): Must be "apt"

required
context InstallationContext

Installation context with environment info

required
progress_callback Callable[[str, float, str], None]

Progress reporting callback.

None

Returns:

Name Type Description
InstallationResult InstallationResult

Result of the installation operation.

Raises:

Type Description
InstallationError

If installation fails for any reason.

Source code in hatch/installers/system_installer.py
def install(
    self,
    dependency: Dict[str, Any],
    context: InstallationContext,
    progress_callback: Optional[Callable[[str, float, str], None]] = None,
) -> InstallationResult:
    """Install a system dependency using apt.

    Args:
        dependency (Dict[str, Any]): Dependency object containing:
            - name (str): Name of the system package
            - version_constraint (str): Version constraint
            - package_manager (str): Must be "apt"
        context (InstallationContext): Installation context with environment info
        progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.

    Returns:
        InstallationResult: Result of the installation operation.

    Raises:
        InstallationError: If installation fails for any reason.
    """
    if not self.validate_dependency(dependency):
        raise InstallationError(
            f"Invalid dependency: {dependency}",
            dependency_name=dependency.get("name"),
            error_code="INVALID_DEPENDENCY",
        )

    package_name = dependency["name"]
    version_constraint = dependency["version_constraint"]

    if progress_callback:
        progress_callback(
            f"Installing {package_name}", 0.0, "Starting installation"
        )

    self.logger.info(
        f"Installing system package: {package_name} with constraint: {version_constraint}"
    )

    try:
        # Handle dry-run/simulation mode
        if context.simulation_mode:
            return self._simulate_installation(
                dependency, context, progress_callback
            )

        # Run apt-get update first
        update_cmd = ["sudo", "apt-get", "update"]
        update_returncode = self._run_apt_subprocess(update_cmd)
        if update_returncode != 0:
            raise InstallationError(
                "apt-get update failed (see logs for details).",
                dependency_name=package_name,
                error_code="APT_UPDATE_FAILED",
                cause=None,
            )

        # Build and execute apt install command
        cmd = self._build_apt_command(dependency, context)

        if progress_callback:
            progress_callback(
                f"Installing {package_name}", 25.0, "Executing apt command"
            )

        returncode = self._run_apt_subprocess(cmd)
        self.logger.debug(f"apt command: {cmd}\nreturn code: {returncode}")

        if returncode != 0:
            raise InstallationError(
                f"Installation failed for {package_name} (see logs for details).",
                dependency_name=package_name,
                error_code="APT_INSTALL_FAILED",
                cause=None,
            )

        if progress_callback:
            progress_callback(
                f"Installing {package_name}", 75.0, "Verifying installation"
            )

        # Verify installation
        installed_version = self._verify_installation(package_name)

        if progress_callback:
            progress_callback(
                f"Installing {package_name}", 100.0, "Installation complete"
            )

        return InstallationResult(
            dependency_name=package_name,
            status=InstallationStatus.COMPLETED,
            installed_version=installed_version,
            metadata={
                "package_manager": "apt",
                "command_executed": " ".join(cmd),
                "platform": platform.platform(),
                "automated": context.get_config("automated", False),
            },
        )

    except InstallationError as e:
        self.logger.error(f"Installation error for {package_name}: {str(e)}")
        raise e

    except Exception as e:
        self.logger.error(f"Unexpected error installing {package_name}: {str(e)}")
        raise InstallationError(
            f"Unexpected error installing {package_name}: {str(e)}",
            dependency_name=package_name,
            error_code="UNEXPECTED_ERROR",
            cause=e,
        )
uninstall(dependency, context, progress_callback=None)

Uninstall a system dependency using apt.

Parameters:

Name Type Description Default
dependency Dict[str, Any]

Dependency object to uninstall.

required
context InstallationContext

Installation context.

required
progress_callback Callable[[str, float, str], None]

Progress reporting callback.

None

Returns:

Name Type Description
InstallationResult InstallationResult

Result of the uninstall operation.

Raises:

Type Description
InstallationError

If uninstall fails for any reason.

Source code in hatch/installers/system_installer.py
def uninstall(
    self,
    dependency: Dict[str, Any],
    context: InstallationContext,
    progress_callback: Optional[Callable[[str, float, str], None]] = None,
) -> InstallationResult:
    """Uninstall a system dependency using apt.

    Args:
        dependency (Dict[str, Any]): Dependency object to uninstall.
        context (InstallationContext): Installation context.
        progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.

    Returns:
        InstallationResult: Result of the uninstall operation.

    Raises:
        InstallationError: If uninstall fails for any reason.
    """
    package_name = dependency["name"]

    if progress_callback:
        progress_callback(f"Uninstalling {package_name}", 0.0, "Starting uninstall")

    self.logger.info(f"Uninstalling system package: {package_name}")

    try:
        # Handle dry-run/simulation mode
        if context.simulation_mode:
            return self._simulate_uninstall(dependency, context, progress_callback)

        # Build apt remove command
        cmd = ["sudo", "apt", "remove", package_name]

        # Add automation flag if configured
        if context.get_config("automated", False):
            cmd.append("-y")

        if progress_callback:
            progress_callback(
                f"Uninstalling {package_name}", 50.0, "Executing apt remove"
            )

        # Execute command
        returncode = self._run_apt_subprocess(cmd)

        if returncode != 0:
            raise InstallationError(
                f"Uninstallation failed for {package_name} (see logs for details).",
                dependency_name=package_name,
                error_code="APT_UNINSTALL_FAILED",
                cause=None,
            )

        if progress_callback:
            progress_callback(
                f"Uninstalling {package_name}", 100.0, "Uninstall complete"
            )

        return InstallationResult(
            dependency_name=package_name,
            status=InstallationStatus.COMPLETED,
            metadata={
                "operation": "uninstall",
                "package_manager": "apt",
                "command_executed": " ".join(cmd),
                "automated": context.get_config("automated", False),
            },
        )
    except InstallationError as e:
        self.logger.error(f"Uninstallation error for {package_name}: {str(e)}")
        raise e

    except Exception as e:
        self.logger.error(f"Unexpected error uninstalling {package_name}: {str(e)}")
        raise InstallationError(
            f"Unexpected error uninstalling {package_name}: {str(e)}",
            dependency_name=package_name,
            error_code="UNEXPECTED_ERROR",
            cause=e,
        )
validate_dependency(dependency)

Validate that a dependency object has required fields for system packages.

Parameters:

Name Type Description Default
dependency Dict[str, Any]

Dependency object to validate.

required

Returns:

Name Type Description
bool bool

True if dependency is valid, False otherwise.

Source code in hatch/installers/system_installer.py
def validate_dependency(self, dependency: Dict[str, Any]) -> bool:
    """Validate that a dependency object has required fields for system packages.

    Args:
        dependency (Dict[str, Any]): Dependency object to validate.

    Returns:
        bool: True if dependency is valid, False otherwise.
    """
    # Required fields per schema
    required_fields = ["name", "version_constraint"]
    if not all(field in dependency for field in required_fields):
        self.logger.error(
            f"Missing required fields. Expected: {required_fields}, got: {list(dependency.keys())}"
        )
        return False

    # Validate package manager
    package_manager = dependency.get("package_manager", "apt")
    if package_manager != "apt":
        self.logger.error(
            f"Unsupported package manager: {package_manager}. Only 'apt' is supported."
        )
        return False

    # Validate version constraint format
    version_constraint = dependency.get("version_constraint", "")
    if not self._validate_version_constraint(version_constraint):
        self.logger.error(
            f"Invalid version constraint format: {version_constraint}"
        )
        return False

    return True