# 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=())