python_dependencies.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. # Copyright 2015, 2016 OpenMarket Ltd
  2. # Copyright 2017 Vector Creations Ltd
  3. # Copyright 2018 New Vector Ltd
  4. # Copyright 2020 The Matrix.org Foundation C.I.C.
  5. #
  6. # Licensed under the Apache License, Version 2.0 (the "License");
  7. # you may not use this file except in compliance with the License.
  8. # You may obtain a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. import itertools
  18. import logging
  19. from typing import List, Set
  20. from pkg_resources import (
  21. DistributionNotFound,
  22. Requirement,
  23. VersionConflict,
  24. get_provider,
  25. )
  26. logger = logging.getLogger(__name__)
  27. # REQUIREMENTS is a simple list of requirement specifiers[1], and must be
  28. # installed. It is passed to setup() as install_requires in setup.py.
  29. #
  30. # CONDITIONAL_REQUIREMENTS is the optional dependencies, represented as a dict
  31. # of lists. The dict key is the optional dependency name and can be passed to
  32. # pip when installing. The list is a series of requirement specifiers[1] to be
  33. # installed when that optional dependency requirement is specified. It is passed
  34. # to setup() as extras_require in setup.py
  35. #
  36. # Note that these both represent runtime dependencies (and the versions
  37. # installed are checked at runtime).
  38. #
  39. # Also note that we replicate these constraints in the Synapse Dockerfile while
  40. # pre-installing dependencies. If these constraints are updated here, the same
  41. # change should be made in the Dockerfile.
  42. #
  43. # [1] https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers.
  44. REQUIREMENTS = [
  45. # we use the TYPE_CHECKER.redefine method added in jsonschema 3.0.0
  46. "jsonschema>=3.0.0",
  47. "frozendict>=1",
  48. "unpaddedbase64>=1.1.0",
  49. "canonicaljson>=1.4.0",
  50. # we use the type definitions added in signedjson 1.1.
  51. "signedjson>=1.1.0",
  52. "pynacl>=1.2.1",
  53. "idna>=2.5",
  54. # validating SSL certs for IP addresses requires service_identity 18.1.
  55. "service_identity>=18.1.0",
  56. # Twisted 18.9 introduces some logger improvements that the structured
  57. # logger utilises
  58. "Twisted>=18.9.0",
  59. "treq>=15.1",
  60. # Twisted has required pyopenssl 16.0 since about Twisted 16.6.
  61. "pyopenssl>=16.0.0",
  62. "pyyaml>=3.11",
  63. "pyasn1>=0.1.9",
  64. "pyasn1-modules>=0.0.7",
  65. "bcrypt>=3.1.0",
  66. "pillow>=4.3.0",
  67. "sortedcontainers>=1.4.4",
  68. "pymacaroons>=0.13.0",
  69. "msgpack>=0.5.2",
  70. "phonenumbers>=8.2.0",
  71. # we use GaugeHistogramMetric, which was added in prom-client 0.4.0.
  72. "prometheus_client>=0.4.0",
  73. # we use `order`, which arrived in attrs 19.2.0.
  74. # Note: 21.1.0 broke `/sync`, see #9936
  75. "attrs>=19.2.0,!=21.1.0",
  76. "netaddr>=0.7.18",
  77. "Jinja2>=2.9",
  78. "bleach>=1.4.3",
  79. "typing-extensions>=3.7.4",
  80. # We enforce that we have a `cryptography` version that bundles an `openssl`
  81. # with the latest security patches.
  82. "cryptography>=3.4.7",
  83. "ijson>=3.0",
  84. ]
  85. CONDITIONAL_REQUIREMENTS = {
  86. "matrix-synapse-ldap3": ["matrix-synapse-ldap3>=0.1"],
  87. "postgres": [
  88. # we use execute_values with the fetch param, which arrived in psycopg 2.8.
  89. "psycopg2>=2.8 ; platform_python_implementation != 'PyPy'",
  90. "psycopg2cffi>=2.8 ; platform_python_implementation == 'PyPy'",
  91. "psycopg2cffi-compat==1.1 ; platform_python_implementation == 'PyPy'",
  92. ],
  93. "saml2": [
  94. "pysaml2>=4.5.0",
  95. ],
  96. "oidc": ["authlib>=0.14.0"],
  97. # systemd-python is necessary for logging to the systemd journal via
  98. # `systemd.journal.JournalHandler`, as is documented in
  99. # `contrib/systemd/log_config.yaml`.
  100. "systemd": ["systemd-python>=231"],
  101. "url_preview": ["lxml>=3.5.0"],
  102. "sentry": ["sentry-sdk>=0.7.2"],
  103. "opentracing": ["jaeger-client>=4.0.0", "opentracing>=2.2.0"],
  104. "jwt": ["pyjwt>=1.6.4"],
  105. # hiredis is not a *strict* dependency, but it makes things much faster.
  106. # (if it is not installed, we fall back to slow code.)
  107. "redis": ["txredisapi>=1.4.7", "hiredis"],
  108. # Required to use experimental `caches.track_memory_usage` config option.
  109. "cache_memory": ["pympler"],
  110. }
  111. ALL_OPTIONAL_REQUIREMENTS: Set[str] = set()
  112. for name, optional_deps in CONDITIONAL_REQUIREMENTS.items():
  113. # Exclude systemd as it's a system-based requirement.
  114. # Exclude lint as it's a dev-based requirement.
  115. if name not in ["systemd"]:
  116. ALL_OPTIONAL_REQUIREMENTS = set(optional_deps) | ALL_OPTIONAL_REQUIREMENTS
  117. # ensure there are no double-quote characters in any of the deps (otherwise the
  118. # 'pip install' incantation in DependencyException will break)
  119. for dep in itertools.chain(
  120. REQUIREMENTS,
  121. *CONDITIONAL_REQUIREMENTS.values(),
  122. ):
  123. if '"' in dep:
  124. raise Exception(
  125. "Dependency `%s` contains double-quote; use single-quotes instead" % (dep,)
  126. )
  127. def list_requirements():
  128. return list(set(REQUIREMENTS) | ALL_OPTIONAL_REQUIREMENTS)
  129. class DependencyException(Exception):
  130. @property
  131. def message(self):
  132. return "\n".join(
  133. [
  134. "Missing Requirements: %s" % (", ".join(self.dependencies),),
  135. "To install run:",
  136. " pip install --upgrade --force %s" % (" ".join(self.dependencies),),
  137. "",
  138. ]
  139. )
  140. @property
  141. def dependencies(self):
  142. for i in self.args[0]:
  143. yield '"' + i + '"'
  144. def check_requirements(for_feature=None):
  145. deps_needed = []
  146. errors = []
  147. if for_feature:
  148. reqs = CONDITIONAL_REQUIREMENTS[for_feature]
  149. else:
  150. reqs = REQUIREMENTS
  151. for dependency in reqs:
  152. try:
  153. _check_requirement(dependency)
  154. except VersionConflict as e:
  155. deps_needed.append(dependency)
  156. errors.append(
  157. "Needed %s, got %s==%s"
  158. % (
  159. dependency,
  160. e.dist.project_name, # type: ignore[attr-defined] # noqa
  161. e.dist.version, # type: ignore[attr-defined] # noqa
  162. )
  163. )
  164. except DistributionNotFound:
  165. deps_needed.append(dependency)
  166. if for_feature:
  167. errors.append(
  168. "Needed %s for the '%s' feature but it was not installed"
  169. % (dependency, for_feature)
  170. )
  171. else:
  172. errors.append("Needed %s but it was not installed" % (dependency,))
  173. if not for_feature:
  174. # Check the optional dependencies are up to date. We allow them to not be
  175. # installed.
  176. OPTS: List[str] = sum(CONDITIONAL_REQUIREMENTS.values(), [])
  177. for dependency in OPTS:
  178. try:
  179. _check_requirement(dependency)
  180. except VersionConflict as e:
  181. deps_needed.append(dependency)
  182. errors.append(
  183. "Needed optional %s, got %s==%s"
  184. % (
  185. dependency,
  186. e.dist.project_name, # type: ignore[attr-defined] # noqa
  187. e.dist.version, # type: ignore[attr-defined] # noqa
  188. )
  189. )
  190. except DistributionNotFound:
  191. # If it's not found, we don't care
  192. pass
  193. if deps_needed:
  194. for err in errors:
  195. logging.error(err)
  196. raise DependencyException(deps_needed)
  197. def _check_requirement(dependency_string):
  198. """Parses a dependency string, and checks if the specified requirement is installed
  199. Raises:
  200. VersionConflict if the requirement is installed, but with the the wrong version
  201. DistributionNotFound if nothing is found to provide the requirement
  202. """
  203. req = Requirement.parse(dependency_string)
  204. # first check if the markers specify that this requirement needs installing
  205. if req.marker is not None and not req.marker.evaluate():
  206. # not required for this environment
  207. return
  208. get_provider(req)
  209. if __name__ == "__main__":
  210. import sys
  211. sys.stdout.writelines(req + "\n" for req in list_requirements())