You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

199 lines
6.9 KiB

5 years ago
5 years ago
  1. import subprocess
  2. import re
  3. import cgi
  4. from odoo.modules.module import load_information_from_description_file
  5. from odoo.tools import which
  6. from .core import Check, CheckSuccess, CheckWarning, CheckFail
  7. class DependencyCheck(Check):
  8. dependency_type = None
  9. def __init__(self, module, dependency):
  10. super(DependencyCheck, self).__init__(module)
  11. self.dependency = dependency
  12. def _dependency_installed(self, env, name):
  13. raise NotImplementedError("Should be overriden by the subclass")
  14. def _installed_version(self, env, name):
  15. raise NotImplementedError("Should be overriden by the subclass")
  16. def _details(self):
  17. if "install" in self.dependency:
  18. return "Install command: <pre>{}</pre>".format(self.dependency["install"])
  19. return None
  20. def __has_required_version(self, installed_version, version_expression):
  21. version_operator = "="
  22. version = self.dependency["version"]
  23. if version[:1] in ["=", "~", "^"]:
  24. version_operator = version[:1]
  25. version = version[1:]
  26. elif version[:2] in [">="]:
  27. version_operator = version[:2]
  28. version = version[2:]
  29. # Py3 : map -> list(map
  30. # https://stackoverflow.com/questions/33717314/attributeerror-map-obejct-has-no-attribute-index-python-3
  31. try:
  32. parsed_version = list(map(int, version.split(".")))
  33. except ValueError:
  34. raise CheckFail(
  35. "Invalid version expression",
  36. details="""
  37. Allowed expressions are <pre>=x.y.z</pre>, <pre>&gt;=x.y.z</pre>, <pre>^x.z.y</pre>,
  38. <pre>~x.y.z. Got <pre>{}</pre>""".format(
  39. cgi.escape(self.dependency["version"])
  40. ),
  41. )
  42. parsed_installed_version = list(map(int, installed_version.split(".")))
  43. parsed_version.extend(
  44. 0 for _ in range(len(parsed_installed_version) - len(parsed_version))
  45. )
  46. parsed_installed_version.extend(
  47. 0 for _ in range(len(parsed_version) - len(parsed_installed_version))
  48. )
  49. if version_operator == "^":
  50. if parsed_installed_version[:1] != parsed_version[:1]:
  51. return False
  52. version_operator = ">="
  53. elif version_operator == "~":
  54. if parsed_installed_version[:2] != parsed_version[:2]:
  55. return False
  56. version_operator = ">="
  57. if version_operator == ">=":
  58. return tuple(parsed_installed_version) >= tuple(parsed_version)
  59. elif version_operator == "=":
  60. return tuple(parsed_installed_version) == tuple(parsed_version)
  61. assert False
  62. def _run(self, env):
  63. name = self.dependency["name"]
  64. if not self._dependency_installed(env, name):
  65. raise CheckFail(
  66. "Required {} - {} - is not installed.".format(
  67. self.dependency_type, name
  68. ),
  69. details=self._details(),
  70. )
  71. if "version" in self.dependency:
  72. version_expression = self.dependency["version"]
  73. installed_version = self._installed_version(env, name)
  74. if not self.__has_required_version(installed_version, version_expression):
  75. raise CheckWarning(
  76. "Required {} - {} - has version {}, but {} is needed.".format(
  77. self.dependency_type,
  78. name,
  79. installed_version,
  80. version_expression,
  81. ),
  82. details=self._details(),
  83. )
  84. return CheckSuccess(
  85. "Required {} - {} - is installed.".format(self.dependency_type, name),
  86. details=self._details(),
  87. )
  88. class InternalDependencyCheck(DependencyCheck):
  89. dependency_type = "Odoo module"
  90. def _dependency_installed(self, env, name):
  91. return name in env.registry._init_modules
  92. def _installed_version(self, env, name):
  93. return (
  94. env["ir.module.module"].sudo().search([("name", "=", name)]).latest_version
  95. )
  96. class PythonDependencyCheck(DependencyCheck):
  97. dependency_type = "Python module"
  98. def _dependency_installed(self, env, name):
  99. try:
  100. __import__(name)
  101. return True
  102. except ImportError:
  103. return False
  104. def _installed_version(self, env, name):
  105. try:
  106. return __import__(name).__version__
  107. except AttributeError:
  108. raise CheckWarning(
  109. "Could not detect version of the Python module: {}.".format(name),
  110. details=self._details(),
  111. )
  112. class ExternalDependencyCheck(DependencyCheck):
  113. dependency_type = "system executable"
  114. def _dependency_installed(self, env, name):
  115. try:
  116. which(name)
  117. return True
  118. except IOError:
  119. return False
  120. def _installed_version(self, env, name):
  121. try:
  122. exe = which(name)
  123. out = str(subprocess.check_output([exe, "--version"])) # Py3 str()
  124. match = re.search("[\d.]+", out)
  125. if not match:
  126. raise CheckWarning(
  127. "Unable to detect version for executable {}".format(name),
  128. details="Command {} --version returned <pre>{}</pre>".format(
  129. exe, out
  130. ),
  131. )
  132. return match.group(0)
  133. except subprocess.CalledProcessError as e:
  134. raise CheckWarning(
  135. "Unable to detect version for executable {}: {}".format(name, e),
  136. details=self._details(),
  137. )
  138. def get_checks_for_module(module_name):
  139. result = []
  140. manifest = load_information_from_description_file(module_name)
  141. manifest_checks = manifest.get("environment_checkup") or {}
  142. dependencies = manifest_checks.get("dependencies") or {}
  143. for dependency in dependencies.get("python") or []:
  144. result.append(PythonDependencyCheck(module_name, dependency))
  145. for dependency in dependencies.get("external") or []:
  146. result.append(ExternalDependencyCheck(module_name, dependency))
  147. for dependency in dependencies.get("internal") or []:
  148. result.append(InternalDependencyCheck(module_name, dependency))
  149. return result
  150. def get_checks_for_module_recursive(module):
  151. class ModuleDFS(object):
  152. def __init__(self):
  153. self.visited_modules = set()
  154. self.checks = []
  155. def visit(self, module):
  156. if module.name in self.visited_modules:
  157. return
  158. self.visited_modules.add(module.name)
  159. self.checks += get_checks_for_module(module.name)
  160. for module_dependency in module.dependencies_id:
  161. if module_dependency.depend_id:
  162. self.visit(module_dependency.depend_id)
  163. return self
  164. return ModuleDFS().visit(module).checks