Source code for pspman.installations

#!/usr/bin/env python3
# -*- coding:utf-8; mode:python -*-
#
# Copyright 2020 Pradyumna Paranjape
# This file is part of pspman.
#
# pspman is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pspman is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pspman.  If not, see <https://www.gnu.org/licenses/>.
#
'''
Parse install-config files located at standard locations
to define installation methods.

Variables:
    code_path: str: path to source-code
    prefix: str: install-prefix
    build_dir: str: directory to build source for installation
    library: str: include libraries -L
    include: str: include libraries -I
    argv: list: argv to be passed during installation

Sequence of commands called:
    Installation: prefare, build, compile, install, whichever is not ``null``
    Uninstallation: build, uninstall, whichever is not ``null``

'''


import os
import shutil
from pathlib import Path
import yaml
import typing
from pspman.errors import (InstructError, InstructFmtError,
                           InstructTypeError, MissingInstructError)
from pspman.shell import process_comm
from pspman import CONFIG


[docs]class Instruct(): ''' Format handle of yaml file Arguments: indicate: list of file/dirnames that indicate the method exdicate: list of file/dirnames that prohibit the method requires: list of required dependencies to run the method env: dict of environment variables during (un)installation install: `ordered` list of ``steps`` for installation uninstall: `ordered` list of ``steps`` for uninstallation * steps: strings that may start with a caret sign (^) ^ indicates that this step is allowed to fail Args: instruct_file ''' def __init__(self, instruct_file: Path = None): self.indicate: typing.List[str] = [] self.exdicate: typing.List[str] = [] self.requires: typing.List[str] = [] self.env: typing.Dict[str, str] = {} self.install: typing.List[str] = [] self.uninstall: typing.List[str] = [] if instruct_file: self.read(instruct_file)
[docs] def read(self, instruct_file: Path): ''' Read instruction file and pass data for parsing Args: instruct_file: file path to <method>.yml ''' try: with open(instruct_file, 'r') as instruct_h: instruct = yaml.safe_load(instruct_h) self.parse(instruct) except InstructError as err: raise InstructFmtError(instruct_file) from err
[docs] def parse(self, instruct: dict): ''' parse data from ``instruct`` yaml file Args: instruct: data out from yaml load Raises: InstructFmtError: bad format for install instructions MissingInstructError: essential instruction is missing ''' self.indicate = instruct.get('indicate') or [] if not isinstance(self.indicate, list): raise InstructTypeError('indicate', type(self.indicate), 'list') if self.indicate == []: raise MissingInstructError('indicate') self.exdicate = instruct.get('exdicate') or [] if not isinstance(self.exdicate, list): raise InstructTypeError('exdicate', type(self.exdicate), 'list') self.requires = instruct.get('requires') or [] if not isinstance(self.requires, list): raise InstructTypeError('requires', type(self.requires), 'list') self.env = instruct.get('env') or {} if not isinstance(self.env, dict): raise InstructTypeError('env', type(self.env), 'dict') self.install = instruct.get('install') or [] if not isinstance(self.install, list): raise InstructTypeError('install', type(self.install), 'list') if self.install == []: raise MissingInstructError('install') self.uninstall = instruct.get('uninstall') or [] if not isinstance(self.uninstall, list): raise InstructTypeError('uninstall', type(self.uninstall), 'list') # parse install for ids, step in enumerate(self.install): if not isinstance(step, str): raise InstructTypeError(f'step {ids}', type(step), 'str')
[docs]class DefInstruct(): ''' Instructions definition class Attributes: indicate: List[str]: files that help identfy the type of install commands: List[str]: commands essential for installation type: str: type of installation instruct: Dict[str, str]: * prepare: preparations instruction * build: build instruction * compile: compilation instruction * install: install instruction * uninstall: uninstall instruction env: dict: modified environment variables definition: Dict[str, Callable]: * prepare: preparations function * build: build function * compile: compilation function * install: install function * uninstall: uninstall function Args: instruct_file: yaml file describing instructions Raises: MissingInstructError: missing essential instruction InstructFmtError: Bad format of ``instruct_file`` ''' def __init__(self, instruct_file: Path): self.type = Path(instruct_file).name self.instruct = Instruct(instruct_file) self.i_steps: typing.List[typing.Callable] = [] self.u_steps: typing.List[typing.Callable] = [] for action in self.instruct.install: self.i_steps.append(self.instruct_def(action=action)) for action in self.instruct.uninstall: self.u_steps.append(self.instruct_def(action=action)) @staticmethod def _parse_i(instr: str = None, argv: typing.Tuple[str, ...] = None, libs: typing.Tuple[str, ...] = None, incl: typing.Tuple[str, ...] = None) -> typing.List[str]: ''' parse instruction string instr Args: instr: instruction string Returns: List of string separated by " " ''' tuple_vars = {"__argv__": argv or (), "__library__": libs or (), "__include__": incl or ()} if instr is None: return [] while " " in instr: instr = instr.replace(" ", " ") cmd = instr.split(" ") for var, args in tuple_vars.items(): if var not in cmd: continue var_idx = cmd.index(var) cmd.pop(var_idx) for element in args[::-1]: cmd.insert(var_idx, element) return cmd
[docs] def instruct_def(self, action: str) -> typing.Callable: ''' Generate a ``def`` (function) given an ``instructions`` Object Args: instructions Returns: Installation function constructed from action ''' def act_f(code_path: Path, prefix: Path, build_dir: Path, env: typing.Dict[str, str] = None, argv: typing.Tuple[str, ...] = None) -> bool: ''' function to prepare based on instructions Args: code_path: path to source code prefix: installtion prefix build_dir: build_directory env: custom env during installation argv: arguments to be supplied during installation Returns: success of compilation ''' i_var_vals = {'__code_path__': str(code_path), '__prefix__': str(prefix), '__build_dir__': str(build_dir)} env = env or {} argv = argv or () inc = prefix.joinpath('include') lib = prefix.joinpath('lib') incl = ("-I", str(inc)) if inc.is_dir() else () libs = ("-L", str(lib)) if lib.is_dir() else () env = {**self.instruct.env, **env} act_i = action ret_code = True if act_i[0] == '^' else False act_i = act_i.strip('^') for ivar, ival in i_var_vals.items(): act_i = act_i.replace(ivar, ival) for key, val in env.items(): if val == ivar: env[key] = ival comm = self._parse_i(instr=act_i, argv=argv, libs=libs, incl=incl) step_success = process_comm(*comm, env=env, fail_handle='report') return ret_code or bool(step_success) return act_f
[docs]def get_instruct(config: Path = None) -> typing.Dict[str, DefInstruct]: ''' Scan standard locations for instruction files Args: custom_path: Scan this too ''' standard_paths: typing.List[Path] = [ Path(__file__).parent.joinpath('inst_config').resolve() ] if config is not None: if config.is_dir(): standard_paths.append(config.resolve()) known_types: typing.Dict[str, DefInstruct] = {} for inst_path in standard_paths: for node in inst_path.iterdir(): if all((node.is_file(), node.stem != 'template', node.suffix == '.yml')): known_types[node.stem] = DefInstruct(node) return known_types
INST_METHODS = get_instruct(config=CONFIG.config_dir.joinpath('inst_config')) ''' Methods of installation and uninstallation defined from instructions Default: make, cmake, pip, meson/ninja Extended for <instruct> by creating <config_dir>/inst_config/<instruct>.yml '''
[docs]def run_install(i_type: str, code_path: Path, prefix=Path, argv: typing.List[str] = None, env: typing.Dict[str, str] = None) -> bool: ''' (un)Install repository Args: code_path: path to source-code prefix: ``--prefix`` flag value to be supplied argv: Arguments to be supplied during (un)installation env: Modifications in shell env variables during (un)installation Returns: ``False`` if error/failure during (un)installation, else, ``True`` ''' uninstall = False if i_type not in INST_METHODS: i_type = i_type.replace("u_", '') uninstall = True if i_type not in INST_METHODS: return False argv = argv or [] env = env or {} mod_env = os.environ.copy() for var, val in env.items(): mod_env[var] = val build_dir = prefix.joinpath('temp_build', i_type) build_dir.mkdir(parents=True, exist_ok=True) steps = INST_METHODS[i_type].u_steps if uninstall \ else INST_METHODS[i_type].i_steps for action in steps: if not action( code_path=code_path, prefix=prefix, env=mod_env, argv=argv, build_dir=build_dir ): shutil.rmtree(build_dir) return False shutil.rmtree(build_dir) return True