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