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
173 lines
6.2 KiB
# -*- coding: utf-8 -*-
|
|
import logging
|
|
import os
|
|
from glob import glob
|
|
from pprint import pformat
|
|
from subprocess import check_output
|
|
|
|
import yaml
|
|
|
|
# Constants needed in scripts
|
|
CUSTOM_DIR = "/app/code/custom"
|
|
AUTO_DIR = "/app/code/auto"
|
|
ADDONS_DIR = os.path.join(AUTO_DIR, "addons")
|
|
SRC_DIR = os.path.join(CUSTOM_DIR, "src")
|
|
|
|
ADDONS_YAML = os.path.join(SRC_DIR, "addons")
|
|
if os.path.isfile("%s.yaml" % ADDONS_YAML):
|
|
ADDONS_YAML = "%s.yaml" % ADDONS_YAML
|
|
else:
|
|
ADDONS_YAML = "%s.yml" % ADDONS_YAML
|
|
|
|
REPOS_YAML = os.path.join(SRC_DIR, "repos")
|
|
if os.path.isfile("%s.yaml" % REPOS_YAML):
|
|
REPOS_YAML = "%s.yaml" % REPOS_YAML
|
|
else:
|
|
REPOS_YAML = "%s.yml" % REPOS_YAML
|
|
|
|
AUTO_REPOS_YAML = os.path.join(AUTO_DIR, "repos")
|
|
if os.path.isfile("%s.yml" % AUTO_REPOS_YAML):
|
|
AUTO_REPOS_YAML = "%s.yml" % AUTO_REPOS_YAML
|
|
else:
|
|
AUTO_REPOS_YAML = "%s.yaml" % AUTO_REPOS_YAML
|
|
|
|
CLEAN = os.environ.get("CLEAN") == "true"
|
|
LOG_LEVELS = frozenset({"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"})
|
|
FILE_APT_BUILD = os.path.join(CUSTOM_DIR, "dependencies", "apt_build.txt")
|
|
PRIVATE = "private"
|
|
CORE = "odoo/addons"
|
|
ENTERPRISE = "enterprise"
|
|
PRIVATE_DIR = os.path.join(SRC_DIR, PRIVATE)
|
|
CORE_DIR = os.path.join(SRC_DIR, CORE)
|
|
ODOO_DIR = os.path.join(SRC_DIR, "odoo")
|
|
ODOO_VERSION = os.environ["ODOO_VERSION"]
|
|
MANIFESTS = ("__manifest__.py", "__openerp__.py")
|
|
|
|
# Customize logging for build
|
|
logger = logging.getLogger("doodba")
|
|
log_handler = logging.StreamHandler()
|
|
log_formatter = logging.Formatter("%(name)s %(levelname)s: %(message)s")
|
|
log_handler.setFormatter(log_formatter)
|
|
logger.addHandler(log_handler)
|
|
_log_level = os.environ.get("LOG_LEVEL", "")
|
|
if _log_level.isdigit():
|
|
_log_level = int(_log_level)
|
|
elif _log_level in LOG_LEVELS:
|
|
_log_level = getattr(logging, _log_level)
|
|
else:
|
|
if _log_level:
|
|
logger.warning("Wrong value in $LOG_LEVEL, falling back to INFO")
|
|
_log_level = logging.INFO
|
|
logger.setLevel(_log_level)
|
|
|
|
|
|
class AddonsConfigError(Exception):
|
|
def __init__(self, message, *args):
|
|
super(AddonsConfigError, self).__init__(message, *args)
|
|
self.message = message
|
|
|
|
|
|
def addons_config(filtered=True, strict=False):
|
|
"""Yield addon name and path from ``ADDONS_YAML``.
|
|
|
|
:param bool filtered:
|
|
Use ``False`` to include all addon definitions. Use ``True`` (default)
|
|
to include only those matched by ``ONLY`` clauses, if any.
|
|
|
|
:param bool strict:
|
|
Use ``True`` to raise an exception if any declared addon is not found.
|
|
|
|
:return Iterator[str, str]:
|
|
A generator that yields ``(addon, repo)`` pairs.
|
|
"""
|
|
config = dict()
|
|
missing_glob = set()
|
|
missing_manifest = set()
|
|
all_globs = {}
|
|
try:
|
|
with open(ADDONS_YAML) as addons_file:
|
|
for doc in yaml.safe_load_all(addons_file):
|
|
# Skip sections with ONLY and that don't match
|
|
only = doc.pop("ONLY", {})
|
|
if not filtered:
|
|
doc.setdefault(CORE, ["*"])
|
|
doc.setdefault(PRIVATE, ["*"])
|
|
elif any(
|
|
os.environ.get(key) not in values for key, values in only.items()
|
|
):
|
|
logger.debug("Skipping section with ONLY %s", only)
|
|
continue
|
|
# Flatten all sections in a single dict
|
|
for repo, partial_globs in doc.items():
|
|
if repo == "ENV":
|
|
continue
|
|
logger.debug("Processing %s repo", repo)
|
|
all_globs.setdefault(repo, set())
|
|
all_globs[repo].update(partial_globs)
|
|
except IOError:
|
|
logger.debug("Could not find addons configuration yaml.")
|
|
# Add default values for special sections
|
|
for repo in (CORE, PRIVATE):
|
|
all_globs.setdefault(repo, {"*"})
|
|
logger.debug("Merged addons definition before expanding: %r", all_globs)
|
|
# Expand all globs and store config
|
|
for repo, partial_globs in all_globs.items():
|
|
for partial_glob in partial_globs:
|
|
logger.debug("Expanding in repo %s glob %s", repo, partial_glob)
|
|
full_glob = os.path.join(SRC_DIR, repo, partial_glob)
|
|
found = glob(full_glob)
|
|
if not found:
|
|
# Projects without private addons should never fail
|
|
if (repo, partial_glob) != (PRIVATE, "*"):
|
|
missing_glob.add(full_glob)
|
|
logger.debug("Skipping unexpandable glob '%s'", full_glob)
|
|
continue
|
|
for addon in found:
|
|
if not os.path.isdir(addon):
|
|
continue
|
|
manifests = (os.path.join(addon, m) for m in MANIFESTS)
|
|
if not any(os.path.isfile(m) for m in manifests):
|
|
missing_manifest.add(addon)
|
|
logger.debug(
|
|
"Skipping '%s' as it is not a valid Odoo " "module", addon
|
|
)
|
|
continue
|
|
logger.debug("Registering addon %s", addon)
|
|
addon = os.path.basename(addon)
|
|
config.setdefault(addon, set())
|
|
config[addon].add(repo)
|
|
# Fail now if running in strict mode
|
|
if strict:
|
|
error = []
|
|
if missing_glob:
|
|
error += ["Addons not found:", pformat(missing_glob)]
|
|
if missing_manifest:
|
|
error += ["Addons without manifest:", pformat(missing_manifest)]
|
|
if error:
|
|
raise AddonsConfigError("\n".join(error), missing_glob, missing_manifest)
|
|
logger.debug("Resulting configuration after expanding: %r", config)
|
|
for addon, repos in config.items():
|
|
# Private addons are most important
|
|
if PRIVATE in repos:
|
|
yield addon, PRIVATE
|
|
continue
|
|
# Odoo core addons are least important
|
|
if repos == {CORE}:
|
|
yield addon, CORE
|
|
continue
|
|
repos.discard(CORE)
|
|
# Other addons fall in between
|
|
if filtered and len(repos) != 1:
|
|
raise AddonsConfigError(
|
|
u"Addon {} defined in several repos {}".format(addon, repos)
|
|
)
|
|
for repo in repos:
|
|
yield addon, repo
|
|
|
|
|
|
try:
|
|
from shutil import which
|
|
except ImportError:
|
|
# Custom which implementation for Python 2
|
|
def which(binary):
|
|
return check_output(["which", binary]).strip()
|