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.

173 lines
6.2 KiB

  1. # -*- coding: utf-8 -*-
  2. import logging
  3. import os
  4. from glob import glob
  5. from pprint import pformat
  6. from subprocess import check_output
  7. import yaml
  8. # Constants needed in scripts
  9. CUSTOM_DIR = "/app/code/custom"
  10. AUTO_DIR = "/app/code/auto"
  11. ADDONS_DIR = os.path.join(AUTO_DIR, "addons")
  12. SRC_DIR = os.path.join(CUSTOM_DIR, "src")
  13. ADDONS_YAML = os.path.join(SRC_DIR, "addons")
  14. if os.path.isfile("%s.yaml" % ADDONS_YAML):
  15. ADDONS_YAML = "%s.yaml" % ADDONS_YAML
  16. else:
  17. ADDONS_YAML = "%s.yml" % ADDONS_YAML
  18. REPOS_YAML = os.path.join(SRC_DIR, "repos")
  19. if os.path.isfile("%s.yaml" % REPOS_YAML):
  20. REPOS_YAML = "%s.yaml" % REPOS_YAML
  21. else:
  22. REPOS_YAML = "%s.yml" % REPOS_YAML
  23. AUTO_REPOS_YAML = os.path.join(AUTO_DIR, "repos")
  24. if os.path.isfile("%s.yml" % AUTO_REPOS_YAML):
  25. AUTO_REPOS_YAML = "%s.yml" % AUTO_REPOS_YAML
  26. else:
  27. AUTO_REPOS_YAML = "%s.yaml" % AUTO_REPOS_YAML
  28. CLEAN = os.environ.get("CLEAN") == "true"
  29. LOG_LEVELS = frozenset({"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"})
  30. FILE_APT_BUILD = os.path.join(CUSTOM_DIR, "dependencies", "apt_build.txt")
  31. PRIVATE = "private"
  32. CORE = "odoo/addons"
  33. ENTERPRISE = "enterprise"
  34. PRIVATE_DIR = os.path.join(SRC_DIR, PRIVATE)
  35. CORE_DIR = os.path.join(SRC_DIR, CORE)
  36. ODOO_DIR = os.path.join(SRC_DIR, "odoo")
  37. ODOO_VERSION = os.environ["ODOO_VERSION"]
  38. MANIFESTS = ("__manifest__.py", "__openerp__.py")
  39. # Customize logging for build
  40. logger = logging.getLogger("doodba")
  41. log_handler = logging.StreamHandler()
  42. log_formatter = logging.Formatter("%(name)s %(levelname)s: %(message)s")
  43. log_handler.setFormatter(log_formatter)
  44. logger.addHandler(log_handler)
  45. _log_level = os.environ.get("LOG_LEVEL", "")
  46. if _log_level.isdigit():
  47. _log_level = int(_log_level)
  48. elif _log_level in LOG_LEVELS:
  49. _log_level = getattr(logging, _log_level)
  50. else:
  51. if _log_level:
  52. logger.warning("Wrong value in $LOG_LEVEL, falling back to INFO")
  53. _log_level = logging.INFO
  54. logger.setLevel(_log_level)
  55. class AddonsConfigError(Exception):
  56. def __init__(self, message, *args):
  57. super(AddonsConfigError, self).__init__(message, *args)
  58. self.message = message
  59. def addons_config(filtered=True, strict=False):
  60. """Yield addon name and path from ``ADDONS_YAML``.
  61. :param bool filtered:
  62. Use ``False`` to include all addon definitions. Use ``True`` (default)
  63. to include only those matched by ``ONLY`` clauses, if any.
  64. :param bool strict:
  65. Use ``True`` to raise an exception if any declared addon is not found.
  66. :return Iterator[str, str]:
  67. A generator that yields ``(addon, repo)`` pairs.
  68. """
  69. config = dict()
  70. missing_glob = set()
  71. missing_manifest = set()
  72. all_globs = {}
  73. try:
  74. with open(ADDONS_YAML) as addons_file:
  75. for doc in yaml.safe_load_all(addons_file):
  76. # Skip sections with ONLY and that don't match
  77. only = doc.pop("ONLY", {})
  78. if not filtered:
  79. doc.setdefault(CORE, ["*"])
  80. doc.setdefault(PRIVATE, ["*"])
  81. elif any(
  82. os.environ.get(key) not in values for key, values in only.items()
  83. ):
  84. logger.debug("Skipping section with ONLY %s", only)
  85. continue
  86. # Flatten all sections in a single dict
  87. for repo, partial_globs in doc.items():
  88. if repo == "ENV":
  89. continue
  90. logger.debug("Processing %s repo", repo)
  91. all_globs.setdefault(repo, set())
  92. all_globs[repo].update(partial_globs)
  93. except IOError:
  94. logger.debug("Could not find addons configuration yaml.")
  95. # Add default values for special sections
  96. for repo in (CORE, PRIVATE):
  97. all_globs.setdefault(repo, {"*"})
  98. logger.debug("Merged addons definition before expanding: %r", all_globs)
  99. # Expand all globs and store config
  100. for repo, partial_globs in all_globs.items():
  101. for partial_glob in partial_globs:
  102. logger.debug("Expanding in repo %s glob %s", repo, partial_glob)
  103. full_glob = os.path.join(SRC_DIR, repo, partial_glob)
  104. found = glob(full_glob)
  105. if not found:
  106. # Projects without private addons should never fail
  107. if (repo, partial_glob) != (PRIVATE, "*"):
  108. missing_glob.add(full_glob)
  109. logger.debug("Skipping unexpandable glob '%s'", full_glob)
  110. continue
  111. for addon in found:
  112. if not os.path.isdir(addon):
  113. continue
  114. manifests = (os.path.join(addon, m) for m in MANIFESTS)
  115. if not any(os.path.isfile(m) for m in manifests):
  116. missing_manifest.add(addon)
  117. logger.debug(
  118. "Skipping '%s' as it is not a valid Odoo " "module", addon
  119. )
  120. continue
  121. logger.debug("Registering addon %s", addon)
  122. addon = os.path.basename(addon)
  123. config.setdefault(addon, set())
  124. config[addon].add(repo)
  125. # Fail now if running in strict mode
  126. if strict:
  127. error = []
  128. if missing_glob:
  129. error += ["Addons not found:", pformat(missing_glob)]
  130. if missing_manifest:
  131. error += ["Addons without manifest:", pformat(missing_manifest)]
  132. if error:
  133. raise AddonsConfigError("\n".join(error), missing_glob, missing_manifest)
  134. logger.debug("Resulting configuration after expanding: %r", config)
  135. for addon, repos in config.items():
  136. # Private addons are most important
  137. if PRIVATE in repos:
  138. yield addon, PRIVATE
  139. continue
  140. # Odoo core addons are least important
  141. if repos == {CORE}:
  142. yield addon, CORE
  143. continue
  144. repos.discard(CORE)
  145. # Other addons fall in between
  146. if filtered and len(repos) != 1:
  147. raise AddonsConfigError(
  148. u"Addon {} defined in several repos {}".format(addon, repos)
  149. )
  150. for repo in repos:
  151. yield addon, repo
  152. try:
  153. from shutil import which
  154. except ImportError:
  155. # Custom which implementation for Python 2
  156. def which(binary):
  157. return check_output(["which", binary]).strip()