min_requirements.py 5.4 KB

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