Skip to content
Snippets Groups Projects
mpsd_software.py 47.5 KiB
Newer Older
  • Learn to ignore specific revisions
  •     enable_build_cache : bool, optional
    
            A boolean indicating whether to build the build cache
    
            when installing package_sets. Defaults to False.
    
            If a requested package_set is not available in the specified release.
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
        """
    
            f"Installing release {mpsd_release} with package_sets {package_sets} "
    
            f"to {root_dir}"
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
        )
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
    
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
        # Set required variables
    
        release_base_dir = root_dir / mpsd_release
    
        microarch = get_native_microarchitecture()
    
        package_set_dir = release_base_dir / microarch
        package_set_dir.mkdir(parents=True, exist_ok=True)
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
        spack_setup_script = release_base_dir / "spack-environments" / "spack_setup.sh"
        install_flags = []
    
        if not enable_build_cache:
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
            install_flags.append("-b")
    
        # run the prepare_environment function
    
        available_package_sets = prepare_environment(mpsd_release, root_dir)
        # Ensure that the requested package_sets are available in the release
        if package_sets == "ALL":
            package_sets = available_package_sets
        elif package_sets == "NONE":
            # No package_sets requested, so we only create the env and print the
            # list of available package_sets
    
                "No package_sets requested. Available package_sets for release "
                f"{mpsd_release} are: \n {available_package_sets}"
    
    Hans Fangohr's avatar
    Hans Fangohr committed
            print_log = logging.getLogger("print")
    
            print_log.info(f"{available_package_sets=}")
    
        for package_set in package_sets:
            if package_set not in available_package_sets:
    
    Hans Fangohr's avatar
    Hans Fangohr committed
                msg = f"Package_Set '{package_set}' is not available"
                msg += f" in release {mpsd_release}. "
    
                msg += "Use 'available' command to see list of available package_sets."
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
    
    
        # Install the package_sets
        with os_chdir(package_set_dir):
            # run spack_setup_script with the package_sets as arguments
            for package_set in package_sets:
    
                # Set the install log file name from create_log_file_names
    
                build_log_path = get_log_file_path(
                    mpsd_release, "install", root_dir, package_set
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
    
    
                logging.info(f"Installing package_set {package_set} to {package_set_dir}")
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
                # log the command
    
                record_script_execution_summary(
    
                    msg=f"installing {package_set} and logging at {build_log_path}",
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
                )
    
                record_script_execution_summary(
    
    Hans Fangohr's avatar
    Hans Fangohr committed
                        f"CMD: bash {spack_setup_script} {' '.join(install_flags)} "
    
                        f"{package_set}"
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
                )
    
    Hans Fangohr's avatar
    Hans Fangohr committed
                    f"bash {spack_setup_script} "
                    f"{' '.join(install_flags)} {package_set} 2>&1 "
    
                    f"| tee -a {build_log_path} ",
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
    
    
    def remove_environment(mpsd_release, root_dir, package_sets="NONE", force_remove=False):
    
        """Remove release from installation."""
    
    Hans Fangohr's avatar
    Hans Fangohr committed
        msg = (
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
            f"Removing release {mpsd_release}"
            f" with package_sets {package_sets} from {root_dir}"
    
    Hans Fangohr's avatar
    Hans Fangohr committed
        )
    
        logging.warning(msg)
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
        if package_sets == "NONE":
    
            logging.warning(
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
                "Please specify package_sets to remove, or 'ALL' to remove all toolchains"
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
        if "ALL" in package_sets:
    
            # we need to remove the entire release folder
    
            logging.warning(
    
                f"Removing release {mpsd_release} from {root_dir}"
                "do you want to continue? [y/n]"
            )
    
            if force_remove or input().lower() == "y":
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
                folders_to_remove = os.listdir(root_dir / mpsd_release)
                # skip logs folder
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
                if "logs" in folders_to_remove:
                    folders_to_remove.remove("logs")
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
                for folder in folders_to_remove:
                    shutil.rmtree(root_dir / mpsd_release / folder)
    
                    sys.exit(0)
        for package_set in package_sets:
            # we load the spack environment and remove the package_set
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
            spack_env = ""
    
            commands_to_execute = [
                f"source {spack_env}",
                f"spack env remove -y {package_set}",
            ]
            run(" && ".join(commands_to_execute), shell=True, check=True)
    
    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}"
    
    def environment_status(mpsd_release: str, root_dir: Path) -> Union[dict, None]:
    
        """Show status of release in installation.
    
        Parameters
        ----------
        mpsd_release : str
            A string representing the MPSD release version.
        root_dir : pathlib.Path
    
            A Path object pointing to the root directory of the installation.
            Expect a subfolder root/mpsd_release in which we search for the
            toolchains.
    
    
        Returns
        -------
        toolchain_map : dict
    
            A dictionary containing available microarchitectures as keys and
    
            a list of available package_sets as values for each microarchitecture.
    
            If the release is not installed/found, None is returned.
    
    
            Note: only toolchains can be reported at the moment (i.e. package_sets
            such as global and global_generic are missing, even if installed).
    
    
        """
        msg = f"Showing status of release {mpsd_release} in {root_dir}"
    
        logging.info(msg)
    
        plog = logging.getLogger("print")
    
        release_base_dir = root_dir / mpsd_release
    
        microarch = get_native_microarchitecture()
        toolchain_dir = release_base_dir / microarch
    
        spack_dir = toolchain_dir / "spack"
    
        # if the mpsd_release does not exist:
        if not release_base_dir.exists():
            logging.debug(f"Directory {str(release_base_dir)} does not exist.")
            logging.error(f"MPSD release '{mpsd_release}' is not installed.")
            return None
    
        # if the mpds_release directory exists but the spack repository is not fully
        # cloned - indicates some kind of incomplete installation:
    
        if not spack_dir.exists():
    
            logging.info(f"Could not find directory {spack_dir}.")
    
            logging.error(
    
                f"MPSD release '{mpsd_release}' has not been completely installed."
    
        # find all folders for all microarch in the release directory
        # except for the blacklisted files
        black_listed_files = [
            config_vars["cmd_log_file"],
            "spack-environments",
            "logs",
            "mpsd-spack-cache",
        ]
    
        list_of_microarchs_candidates = os.listdir(release_base_dir)
    
        list_of_microarchs = [
            x for x in list_of_microarchs_candidates if x not in black_listed_files
        ]
    
        logging.debug(f"{list_of_microarchs=}")
    
    
        toolchain_map = {}
        for microarch in list_of_microarchs:
            # get a list of all the toolchains in the microarch
    
            possible_toolchains = (release_base_dir / microarch).glob(
                "lmod/Core/toolchains/*.lua"
            )
    
            # append toolchain which is the name of the file without the .lua extension
            toolchain_map[microarch] = [toolchain.stem for toolchain in possible_toolchains]
    
    
        logging.debug(f"{toolchain_map=}")
    
    
        # pretty print the toolchain map key as the heading
        # and the value as the list of toolchains
    
    Hans Fangohr's avatar
    Hans Fangohr committed
        plog.info(f"Installed toolchains ({mpsd_release}):\n")
    
        for microarch, toolchains in toolchain_map.items():
    
            plog.info(f"- {microarch}")
            for toolchain in toolchains:
    
    Hans Fangohr's avatar
    Hans Fangohr committed
                plog.info(f"    {toolchain}")
    
            plog.info(f"    [module use {str(release_base_dir / microarch / 'lmod/Core')}]")
            plog.info("")
    
        return toolchain_map
    
    def initialise_environment(root_dir: Path) -> None:
    
        """Initialize the software environment.
    
        This creates a hidden file ``.mpsd-software-root`` to tag the location for
        as the root of the installation. All compiled files, logs etc are written in
        or below this subdirectory.
    
        Parameters
        ----------
        root_dir : pathlib.Path
            A Path object pointing to the current directory where the script was called.
    
        """
        # check if the root_dir is not already initialized
        init_file = root_dir / config_vars["init_file"]
        if init_file.exists():
    
            logging.error(f"Directory {str(root_dir)} is already initialised.")
    
        else:
            # create the init file
            init_file.touch()
    
            # note the execution in the execution summary log
    
            # create the log file and fill it with the headers
            record_script_execution_summary(root_dir=root_dir)
            # record the msg in the log file
            record_script_execution_summary(
                root_dir=root_dir,
    
                msg=f"Initialising MPSD software instance at {root_dir}.",
    
    def get_root_dir() -> Path:
        """Get the root directory of the installation.
    
        Look for the hidden file ``.mpsd-software-root``
        (defined in config_vars["init_file"])
        in the current directory, or any parent directory.
        If found, return the path to the root directory
        of the MPSD software instance.
        If not found, exit with an error message.
    
        Returns
        -------
        root_dir : pathlib.Path
            A Path object pointing to the root directory of the installation.
            This folder contains the hidden file ``.mpsd-software-root``,
            ``mpsd_releases`` ( for eg ``dev-23a``) and ``mpsd-spack-cache``.
    
    
        """
        # check if the root_dir is not already initialized
        script_call_dir = Path.cwd()
        init_file = script_call_dir / config_vars["init_file"]
    
        if init_file.exists():
    
            return script_call_dir
    
    
        # if not, look for the init file in the parent directories
        for parent_folder in script_call_dir.parents:
            init_file = parent_folder / config_vars["init_file"]
            if init_file.exists():
                script_call_dir = parent_folder
                return script_call_dir
    
        # if not found in any parent directory, exit with an error message
    
        logging.debug(f"Directory {str(script_call_dir)} is not a MPSD software instance.")
    
        logging.error(
    
    Hans Fangohr's avatar
    Hans Fangohr committed
            "Could not find MPSD software instance "
            "in the current directory or any parent directory.\n\n"
            f"The current directory is {script_call_dir}.\n\n"
            "To initialise a MPSD software instance here, "
            "run 'mpsd-software init'.\n\n"
            f"To find the root directory of an existing MPSD software instance, look "
            f"for the directory containing '{config_vars['cmd_log_file']}' "
            + f"and the hidden file '{config_vars['init_file']}'."
    
    def main():
    
        """Execute main entry point."""
    
    Hans Fangohr's avatar
    Hans Fangohr committed
        parser = argparse.ArgumentParser(
            description=about_intro,
            epilog=about_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter,
        )
    
    Hans Fangohr's avatar
    Hans Fangohr committed
            "-l",
            dest="loglevel",
            choices=["warning", "info", "debug"],
            required=False,
            default="warning",
            help="Set the log level",
        )
    
    Hans Fangohr's avatar
    Hans Fangohr committed
        parser.add_argument("--version", action="version", version=__version__)
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
        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",
                )
    
                # most commands except need a release version
                if cmd in ["install", "prepare", "reinstall", "remove", "status"]:
    
                    subp.add_argument(
                        "release",
                        type=str,
                        help="Release version to prepare, install, reinstall or remove",
                    )
    
                elif cmd in ["available"]:
                    # 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"]:
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
                    # "install" command needs additional documentation
    
                        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()`
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
                        nargs="+",
    
                    )
                    subp.add_argument(
                        "--enable-build-cache",
                        action="store_true",
    
                            "Enable Spack build cache. Useful for reinstallation but "
                            "consumes time and disk space."
    
        # 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()))
    
        # if a release version is specified:
        if args.release:
            # sanity check for common mistakes in command line arguments
            if args.release.endswith("/"):  # happens easily with autocompletion
                args.release = args.release.removesuffix("/")
                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"]:
    
            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"]:
            log_file = None
        else:
            # sanity check
            raise NotImplementedError(f"Should never happen: unknown {args.action=}")
    
    
        set_up_logging(
            args.loglevel,
            log_file,
        )
    
        # Check the command and run related function
    
        if args.action == "remove":
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
            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":
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
            install_environment(
    
                args.release, args.package_set, root_dir, args.enable_build_cache
    
    Ashwin Kumar Karnad's avatar
    Ashwin Kumar Karnad committed
            )
    
        elif args.action == "status":
    
    Hans Fangohr's avatar
    Hans Fangohr committed
            _ = environment_status(args.release, root_dir)
    
        elif args.action == "prepare":
    
            prepare_environment(args.release, root_dir)
    
    Hans Fangohr's avatar
    Hans Fangohr committed
        elif args.action == "available":
    
            if args.release:
                get_available_package_sets(args.release)
            else:
                get_available_releases(print_result=True)
                sys.exit(0)
    
    Hans Fangohr's avatar
    Hans Fangohr committed
        else:
    
            message = (
                f"No known action found ({args.action=}). Should probably never happen."
            )
    
    Hans Fangohr's avatar
    Hans Fangohr committed
            logging.error(message)
            raise NotImplementedError(message)
    
    if __name__ == "__main__":