Skip to content
Snippets Groups Projects
mpsd_software.py 9.14 KiB
"""mpsd-software: entry point for command line application."""
import argparse
import logging
import os
import sys
from pathlib import Path

from .cmds.available import get_available_package_sets, get_available_releases
from .cmds.init import initialise_environment
from .cmds.install import install_environment
from .cmds.prepare import prepare_environment
from .cmds.remove import remove_environment
from .cmds.status import environment_status
from .utils.filesystem_utils import get_root_dir
from .utils.logging import (
    get_log_file_path,
    record_script_execution_summary,
    set_up_logging,
)
from mpsd_software_manager import __version__, command_name

# command_name = Path(sys.argv[0]).name

about_intro = f"""
Build software as on MPSD HPC.


    This tool builds software package sets (including toolchains for Octopus).
    It follows recipes as used on the MPSD HPC system and the (spack-based)
    Octopus buildbot. Compiled software is organised into MPSD software release
    versions (such as `dev-23a`) and CPU microarchitecture (such as `sandybridge`).

    Compiled packages and toolchains can be activated and used via `module load` as
    on the HPC system.

    Further documentation is available in the README.rst file, online at
    https://gitlab.gwdg.de/mpsd-cs/mpsd-software-manager/-/blob/main/README.rst

Command line usage:

   $> {command_name}

"""


about_epilog = f"""


Examples:

    1. Query what releases are available for installation

       $> {command_name} available

    2. Query what package sets and toolchains are available for installation in
       release dev-23a

       $> {command_name} available dev-23a

    3. Install foss2022a-serial toolchain from the dev-23a release

       $> {command_name} install dev-23a foss2022a-serial

    4. Check what package sets and toolchains are installed from release dev-23a

       $> {command_name} status dev-23a

       The `status` command also displays the `module use` command needed to load
       the created modules.

"""


# TODO @Ashwin
# Martin: Do we still need this function?
# It seems to be similar to 'prepare'; the parser does not offer it.
def start_new_environment(release, from_release, target_dir):
    """Start new MPSD software environment version."""
    msg = f"Starting new release {release} from {from_release} to {target_dir}"
    logging.info(msg)
    raise NotImplementedError(msg)


def main():
    """Execute main entry point."""
    parser = argparse.ArgumentParser(
        description=about_intro,
        epilog=about_epilog,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "-l",
        dest="loglevel",
        choices=["warning", "info", "debug"],
        required=False,
        default="warning",
        help="Set the log level",
    )

    parser.add_argument("--version", action="version", version=__version__)

    parser.add_argument(
        "-d",
        "--develop",
        action="store_true",
        default=False,
        help="Allow the use of unreleased branches",
    )

    subparsers = parser.add_subparsers(
        dest="action",
        title="actions",
        description="valid actions",  # required=True
    )
    subparsers.required = True
    list_of_cmds = [
        ("init", "Initialise the MPSD software instance in the current directory"),
        ("available", "What is available for installation?"),
        ("install", "Install a software environment"),
        # ("reinstall", "Reinstall a package_set"),
        ("remove", "Remove a package set"),
        # ("start-new", "Start a new MPSD software release version"),
        ("status", "Show status: what is installed?"),
        ("prepare", "Prepare installation of MPSD-release (dev only)"),
    ]
    for cmd, help_text in list_of_cmds:
        subp = subparsers.add_parser(cmd, help=help_text)

        if cmd == "start-new":
            subp.add_argument(
                "--from-release",
                dest="from_release",
                type=str,
                required=True,
                help="Release version to start from",
            )
            subp.add_argument(
                "--to-release",
                dest="to_release",
                type=str,
                required=True,
                help="Release version to create",
            )

        else:
            # most commands except need a release version
            if cmd in ["install", "prepare", "reinstall", "remove"]:
                subp.add_argument(
                    "release",
                    type=str,
                    help="Release version to prepare, install, reinstall or remove",
                )
            elif cmd in ["available", "status"]:
                # for some commands the release version is optional
                subp.add_argument(
                    "release",
                    type=str,
                    nargs="?",
                    help="Release version to prepare, install, reinstall or remove",
                )

            if cmd in ["install", "reinstall", "remove"]:
                # "install" command needs additional documentation
                package_set_help = (
                    f"One or more package sets (like toolchains) to be {cmd}ed. "
                    "Use 'ALL' to refer to all available package sets."
                )

                subp.add_argument(
                    "package_set",  # first option defines attribute
                    # name `args.package_set` in `args = parser_args()`
                    type=str,
                    nargs="+",
                    default="NONE",
                    help=package_set_help,
                )
                # TODO Move the enable-build-cache flag to only 'install' cmd
                subp.add_argument(
                    "--enable-build-cache",
                    action="store_true",
                    help=(
                        "Enable Spack build cache. Useful for reinstallation but "
                        "consumes time and disk space."
                    ),
                )
            if cmd in ["status"]:
                subp.add_argument(
                    "package_set",
                    type=str,
                    nargs="?",
                    default="NONE",
                    help="Package set to show status for.",
                )
    # Carry out the action
    args = parser.parse_args()

    # Set up logging without file handle:
    # this is used in the init action and for logging the
    # get_root_dir() function
    set_up_logging(args.loglevel)

    # Check if the action is init
    # if so, call the init function and exit
    if args.action == "init":
        initialise_environment(Path(os.getcwd()))
        sys.exit(0)

    # if a release version is specified:
    if (
        args.release
        # sanity check for common mistakes in command line arguments
        and args.release.endswith("/")  # happens easily with autocompletion
    ):
        removesuffix = lambda s, p: s[: -len(p)] if p and s.endswith(p) else s  # noqa: E731
        args.release = removesuffix(args.release, "/")
        logging.warning(f"Removed trailing slash from release: {args.release}")

    # root_dir is the place where this MPSD software instance has its root
    root_dir = get_root_dir()

    # set up logging filename: we record activities that change the installation
    if args.action in ["init", "install", "prepare", "reinstall", "remove"]:
        apex_log_file = get_log_file_path(
            args.release,
            args.action,
            root_dir,
        )
    # some commands do not write any log_files:
    elif args.action in ["available", "status"]:
        apex_log_file = None
    else:
        # sanity check
        raise NotImplementedError(
            f"Should never happen: unknown args.action={args.action}"
        )

    set_up_logging(
        args.loglevel,
        apex_log_file,
    )
    if args.action not in ["status", "available"]:
        # record the script execution summary only if
        # the action is one that changes files on disk
        record_script_execution_summary(root_dir, apex_log_file)
    # Check the command and run related function
    if args.action == "remove":
        remove_environment(args.release, root_dir, args.package_set)
    elif args.action == "start-new":
        start_new_environment(args.from_release, args.to_release, root_dir)
    elif args.action == "install":
        install_environment(
            args.release, args.package_set, root_dir, args.enable_build_cache
        )
    elif args.action == "status":
        environment_status(args.release, root_dir, args.package_set)
    elif args.action == "prepare":
        prepare_environment(args.release, root_dir, args.develop)
    elif args.action == "available":
        if args.release:
            get_available_package_sets(args.release, args.develop)
        else:
            get_available_releases(print_result=True)
            sys.exit(0)
    else:
        message = f"No known action found (args.action={args.action}). Should probably never happen."  # noqa: E501
        logging.error(message)
        raise NotImplementedError(message)


# TODO Martin: This can be removed (will do that separately)
if __name__ == "__main__":
    main()