Source code for mafw.scaffold.installer

#  Copyright 2025–2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""
Optional installation of a generated project.

When the user passes ``--install`` to ``mafw init``, the newly generated project
is installed into its own environment. The scaffolding service never implements
virtual-environment creation or dependency resolution itself: it delegates the
whole operation to a supported project manager (Hatch by default), using the
shared :func:`mafw.tools.shell_tools.run` helper.

If the delegated installation fails, the generated sources are left untouched and
the caller receives the manual commands needed to finish the installation by hand.

.. versionadded:: 2.4
"""

from __future__ import annotations

import shutil
from dataclasses import dataclass
from pathlib import Path

from mafw.tools.shell_tools import run

DEFAULT_MANAGER = 'hatch'
"""Project manager used when ``--install`` is given without an explicit choice."""

# Command used to create/install the project environment for each supported
# manager. Only Hatch is supported in the first milestone; the mapping makes the
# supported set explicit and easy to extend.
_MANAGER_COMMANDS: dict[str, list[str]] = {
    'hatch': ['hatch', 'env', 'create'],
}


[docs] @dataclass(frozen=True) class InstallOutcome: """Result of an optional installation attempt. :ivar succeeded: Whether the delegated installation completed successfully. :ivar manager: The project manager that was used. :ivar manual_commands: Commands the user can run to finish installation by hand (populated when the installation failed or was impossible). """ succeeded: bool manager: str manual_commands: tuple[str, ...]
[docs] def _manual_commands(project_dir: Path, manager: str) -> tuple[str, ...]: """Return the manual commands that complete installation for ``manager``. :param project_dir: The generated project directory. :type project_dir: Path :param manager: The selected project manager. :type manager: str :return: A tuple of shell command strings. :rtype: tuple[str, ...] """ command = _MANAGER_COMMANDS.get(manager, _MANAGER_COMMANDS[DEFAULT_MANAGER]) return (f'cd {project_dir}', ' '.join(command))
[docs] def install_project(project_dir: Path, manager: str | None = None) -> InstallOutcome: """Install a generated project by delegating to a supported project manager. :param project_dir: The generated project directory to install. :type project_dir: Path :param manager: The manager to use; defaults to :data:`DEFAULT_MANAGER`. :type manager: str | None :return: The outcome describing success and any manual recovery commands. :rtype: InstallOutcome """ selected = manager or DEFAULT_MANAGER command = _MANAGER_COMMANDS.get(selected) manual = _manual_commands(project_dir, selected) # If the selected manager is unknown or not installed, do not attempt anything # destructive: report failure and the manual steps, leaving sources intact. if command is None or shutil.which(command[0]) is None: return InstallOutcome(succeeded=False, manager=selected, manual_commands=manual) try: # Delegate environment creation and installation entirely to the manager. run(command, cwd=project_dir, check=True) except Exception: # Installation failure must leave the generated sources untouched. return InstallOutcome(succeeded=False, manager=selected, manual_commands=manual) return InstallOutcome(succeeded=True, manager=selected, manual_commands=())