1a8e1175bSopenharmony_ci#!/usr/bin/env python3 2a8e1175bSopenharmony_ci"""Install all the required Python packages, with the minimum Python version. 3a8e1175bSopenharmony_ci""" 4a8e1175bSopenharmony_ci 5a8e1175bSopenharmony_ci# Copyright The Mbed TLS Contributors 6a8e1175bSopenharmony_ci# SPDX-License-Identifier: Apache-2.0 7a8e1175bSopenharmony_ci# 8a8e1175bSopenharmony_ci# Licensed under the Apache License, Version 2.0 (the "License"); you may 9a8e1175bSopenharmony_ci# not use this file except in compliance with the License. 10a8e1175bSopenharmony_ci# You may obtain a copy of the License at 11a8e1175bSopenharmony_ci# 12a8e1175bSopenharmony_ci# http://www.apache.org/licenses/LICENSE-2.0 13a8e1175bSopenharmony_ci# 14a8e1175bSopenharmony_ci# Unless required by applicable law or agreed to in writing, software 15a8e1175bSopenharmony_ci# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 16a8e1175bSopenharmony_ci# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17a8e1175bSopenharmony_ci# See the License for the specific language governing permissions and 18a8e1175bSopenharmony_ci# limitations under the License. 19a8e1175bSopenharmony_ci 20a8e1175bSopenharmony_ciimport argparse 21a8e1175bSopenharmony_ciimport os 22a8e1175bSopenharmony_ciimport re 23a8e1175bSopenharmony_ciimport subprocess 24a8e1175bSopenharmony_ciimport sys 25a8e1175bSopenharmony_ciimport tempfile 26a8e1175bSopenharmony_ciimport typing 27a8e1175bSopenharmony_ci 28a8e1175bSopenharmony_cifrom typing import List, Optional 29a8e1175bSopenharmony_cifrom mbedtls_dev import typing_util 30a8e1175bSopenharmony_ci 31a8e1175bSopenharmony_cidef pylint_doesn_t_notice_that_certain_types_are_used_in_annotations( 32a8e1175bSopenharmony_ci _list: List[typing.Any], 33a8e1175bSopenharmony_ci) -> None: 34a8e1175bSopenharmony_ci pass 35a8e1175bSopenharmony_ci 36a8e1175bSopenharmony_ci 37a8e1175bSopenharmony_ciclass Requirements: 38a8e1175bSopenharmony_ci """Collect and massage Python requirements.""" 39a8e1175bSopenharmony_ci 40a8e1175bSopenharmony_ci def __init__(self) -> None: 41a8e1175bSopenharmony_ci self.requirements = [] #type: List[str] 42a8e1175bSopenharmony_ci 43a8e1175bSopenharmony_ci def adjust_requirement(self, req: str) -> str: 44a8e1175bSopenharmony_ci """Adjust a requirement to the minimum specified version.""" 45a8e1175bSopenharmony_ci # allow inheritance #pylint: disable=no-self-use 46a8e1175bSopenharmony_ci # If a requirement specifies a minimum version, impose that version. 47a8e1175bSopenharmony_ci split_req = req.split(';', 1) 48a8e1175bSopenharmony_ci split_req[0] = re.sub(r'>=|~=', r'==', split_req[0]) 49a8e1175bSopenharmony_ci return ';'.join(split_req) 50a8e1175bSopenharmony_ci 51a8e1175bSopenharmony_ci def add_file(self, filename: str) -> None: 52a8e1175bSopenharmony_ci """Add requirements from the specified file. 53a8e1175bSopenharmony_ci 54a8e1175bSopenharmony_ci This method supports a subset of pip's requirement file syntax: 55a8e1175bSopenharmony_ci * One requirement specifier per line, which is passed to 56a8e1175bSopenharmony_ci `adjust_requirement`. 57a8e1175bSopenharmony_ci * Comments (``#`` at the beginning of the line or after whitespace). 58a8e1175bSopenharmony_ci * ``-r FILENAME`` to include another file. 59a8e1175bSopenharmony_ci """ 60a8e1175bSopenharmony_ci for line in open(filename): 61a8e1175bSopenharmony_ci line = line.strip() 62a8e1175bSopenharmony_ci line = re.sub(r'(\A|\s+)#.*', r'', line) 63a8e1175bSopenharmony_ci if not line: 64a8e1175bSopenharmony_ci continue 65a8e1175bSopenharmony_ci m = re.match(r'-r\s+', line) 66a8e1175bSopenharmony_ci if m: 67a8e1175bSopenharmony_ci nested_file = os.path.join(os.path.dirname(filename), 68a8e1175bSopenharmony_ci line[m.end(0):]) 69a8e1175bSopenharmony_ci self.add_file(nested_file) 70a8e1175bSopenharmony_ci continue 71a8e1175bSopenharmony_ci self.requirements.append(self.adjust_requirement(line)) 72a8e1175bSopenharmony_ci 73a8e1175bSopenharmony_ci def write(self, out: typing_util.Writable) -> None: 74a8e1175bSopenharmony_ci """List the gathered requirements.""" 75a8e1175bSopenharmony_ci for req in self.requirements: 76a8e1175bSopenharmony_ci out.write(req + '\n') 77a8e1175bSopenharmony_ci 78a8e1175bSopenharmony_ci def install( 79a8e1175bSopenharmony_ci self, 80a8e1175bSopenharmony_ci pip_general_options: Optional[List[str]] = None, 81a8e1175bSopenharmony_ci pip_install_options: Optional[List[str]] = None, 82a8e1175bSopenharmony_ci ) -> None: 83a8e1175bSopenharmony_ci """Call pip to install the requirements.""" 84a8e1175bSopenharmony_ci if pip_general_options is None: 85a8e1175bSopenharmony_ci pip_general_options = [] 86a8e1175bSopenharmony_ci if pip_install_options is None: 87a8e1175bSopenharmony_ci pip_install_options = [] 88a8e1175bSopenharmony_ci with tempfile.TemporaryDirectory() as temp_dir: 89a8e1175bSopenharmony_ci # This is more complicated than it needs to be for the sake 90a8e1175bSopenharmony_ci # of Windows. Use a temporary file rather than the command line 91a8e1175bSopenharmony_ci # to avoid quoting issues. Use a temporary directory rather 92a8e1175bSopenharmony_ci # than NamedTemporaryFile because with a NamedTemporaryFile on 93a8e1175bSopenharmony_ci # Windows, the subprocess can't open the file because this process 94a8e1175bSopenharmony_ci # has an exclusive lock on it. 95a8e1175bSopenharmony_ci req_file_name = os.path.join(temp_dir, 'requirements.txt') 96a8e1175bSopenharmony_ci with open(req_file_name, 'w') as req_file: 97a8e1175bSopenharmony_ci self.write(req_file) 98a8e1175bSopenharmony_ci subprocess.check_call([sys.executable, '-m', 'pip'] + 99a8e1175bSopenharmony_ci pip_general_options + 100a8e1175bSopenharmony_ci ['install'] + pip_install_options + 101a8e1175bSopenharmony_ci ['-r', req_file_name]) 102a8e1175bSopenharmony_ci 103a8e1175bSopenharmony_ciDEFAULT_REQUIREMENTS_FILE = 'ci.requirements.txt' 104a8e1175bSopenharmony_ci 105a8e1175bSopenharmony_cidef main() -> None: 106a8e1175bSopenharmony_ci """Command line entry point.""" 107a8e1175bSopenharmony_ci parser = argparse.ArgumentParser(description=__doc__) 108a8e1175bSopenharmony_ci parser.add_argument('--no-act', '-n', 109a8e1175bSopenharmony_ci action='store_true', 110a8e1175bSopenharmony_ci help="Don't act, just print what will be done") 111a8e1175bSopenharmony_ci parser.add_argument('--pip-install-option', 112a8e1175bSopenharmony_ci action='append', dest='pip_install_options', 113a8e1175bSopenharmony_ci help="Pass this option to pip install") 114a8e1175bSopenharmony_ci parser.add_argument('--pip-option', 115a8e1175bSopenharmony_ci action='append', dest='pip_general_options', 116a8e1175bSopenharmony_ci help="Pass this general option to pip") 117a8e1175bSopenharmony_ci parser.add_argument('--user', 118a8e1175bSopenharmony_ci action='append_const', dest='pip_install_options', 119a8e1175bSopenharmony_ci const='--user', 120a8e1175bSopenharmony_ci help="Install to the Python user install directory" 121a8e1175bSopenharmony_ci " (short for --pip-install-option --user)") 122a8e1175bSopenharmony_ci parser.add_argument('files', nargs='*', metavar='FILE', 123a8e1175bSopenharmony_ci help="Requirement files" 124a8e1175bSopenharmony_ci " (default: {} in the script's directory)" \ 125a8e1175bSopenharmony_ci .format(DEFAULT_REQUIREMENTS_FILE)) 126a8e1175bSopenharmony_ci options = parser.parse_args() 127a8e1175bSopenharmony_ci if not options.files: 128a8e1175bSopenharmony_ci options.files = [os.path.join(os.path.dirname(__file__), 129a8e1175bSopenharmony_ci DEFAULT_REQUIREMENTS_FILE)] 130a8e1175bSopenharmony_ci reqs = Requirements() 131a8e1175bSopenharmony_ci for filename in options.files: 132a8e1175bSopenharmony_ci reqs.add_file(filename) 133a8e1175bSopenharmony_ci reqs.write(sys.stdout) 134a8e1175bSopenharmony_ci if not options.no_act: 135a8e1175bSopenharmony_ci reqs.install(pip_general_options=options.pip_general_options, 136a8e1175bSopenharmony_ci pip_install_options=options.pip_install_options) 137a8e1175bSopenharmony_ci 138a8e1175bSopenharmony_ciif __name__ == '__main__': 139a8e1175bSopenharmony_ci main() 140