blob: 3b156a36ab40d67a48281386840d6c0d8cab7c7c [file] [log] [blame]
Gilles Peskinee4d142f2021-11-17 19:25:43 +01001#!/usr/bin/env python3
2"""Install all the required Python packages, with the minimum Python version.
3"""
4
5# Copyright The Mbed TLS Contributors
6# SPDX-License-Identifier: Apache-2.0
7#
8# Licensed under the Apache License, Version 2.0 (the "License"); you may
9# not use this file except in compliance with the License.
10# You may obtain a copy of the License at
11#
12# http://www.apache.org/licenses/LICENSE-2.0
13#
14# Unless required by applicable law or agreed to in writing, software
15# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17# See the License for the specific language governing permissions and
18# limitations under the License.
19
20import argparse
21import os
22import re
23import sys
24import typing
25
26from typing import List
27from mbedtls_dev import typing_util
28
29def pylint_doesn_t_notice_that_certain_types_are_used_in_annotations(
30 _list: List[typing.Any],
31) -> None:
32 pass
33
34
35class Requirements:
36 """Collect and massage Python requirements."""
37
38 def __init__(self) -> None:
39 self.requirements = [] #type: List[str]
40
41 def adjust_requirement(self, req: str) -> str:
42 """Adjust a requirement to the minimum specified version."""
43 # allow inheritance #pylint: disable=no-self-use
44 # If a requirement specifies a minimum version, impose that version.
45 req = re.sub(r'>=|~=', r'==', req)
46 return req
47
48 def add_file(self, filename: str) -> None:
49 """Add requirements from the specified file.
50
51 This method supports a subset of pip's requirement file syntax:
52 * One requirement specifier per line, which is passed to
53 `adjust_requirement`.
54 * Comments (``#`` at the beginning of the line or after whitespace).
55 * ``-r FILENAME`` to include another file.
56 """
57 for line in open(filename):
58 line = line.strip()
59 line = re.sub(r'(\A|\s+)#.*', r'', line)
60 if not line:
61 continue
62 m = re.match(r'-r\s+', line)
63 if m:
64 nested_file = os.path.join(os.path.dirname(filename),
65 line[m.end(0):])
66 self.add_file(nested_file)
67 continue
68 self.requirements.append(self.adjust_requirement(line))
69
70 def write(self, out: typing_util.Writable) -> None:
71 """List the gathered requirements."""
72 for req in self.requirements:
73 out.write(req + '\n')
74
75 def install(self) -> None:
76 """Call pip to install the requirements."""
77 if not self.requirements:
78 return
79 ret = os.spawnl(os.P_WAIT, sys.executable, 'python', '-m', 'pip',
80 'install', *self.requirements)
81 if ret != 0:
82 sys.exit(ret)
83
84
85def main() -> None:
86 """Command line entry point."""
87 parser = argparse.ArgumentParser(description=__doc__)
88 parser.add_argument('--no-act', '-n',
89 action='store_true',
90 help="Don't act, just print what will be done")
91 parser.add_argument('files', nargs='*', metavar='FILE',
92 help="Requirement files"
93 "(default: requirements.txt in the script's directory)")
94 options = parser.parse_args()
95 if not options.files:
96 options.files = [os.path.join(os.path.dirname(__file__),
97 'ci.requirements.txt')]
98 reqs = Requirements()
99 for filename in options.files:
100 reqs.add_file(filename)
101 reqs.write(sys.stdout)
102 if not options.no_act:
103 reqs.install()
104
105if __name__ == '__main__':
106 main()