blob: 890950c65bcee580397aa49e29305d507e7711c6 [file] [log] [blame]
# -*- coding:utf-8 -*-
import logging
import portage
from itertools import chain
from portage import normalize_path
from portage import os
from portage._sets.base import InternalPackageSet
from portage.output import green
from portage.util.futures.extendedfutures import ExtendedFuture
from repoman.metadata import get_metadata_xsd
from repoman.modules.commit import repochecks
from repoman.modules.commit import manifest
from repoman.profile import check_profiles, dev_profile_keywords, setup_profile
from repoman.repos import repo_metadata
from repoman.modules.scan.module import ModuleConfig
from repoman.modules.scan.scan import scan
from repoman.modules.vcs.vcs import vcs_files_to_cps
DATA_TYPES = {"dict": dict, "Future": ExtendedFuture, "list": list, "set": set}
class Scanner:
"""Primary scan class. Operates all the small Q/A tests and checks"""
def __init__(
self, repo_settings, myreporoot, config_root, options, vcs_settings, mydir, env
):
"""Class __init__"""
self.repo_settings = repo_settings
self.config_root = config_root
self.options = options
self.vcs_settings = vcs_settings
self.env = env
# Repoman sets it's own ACCEPT_KEYWORDS and we don't want it to
# behave incrementally.
self.repoman_incrementals = tuple(
x for x in portage.const.INCREMENTALS if x != "ACCEPT_KEYWORDS"
)
self.categories = []
for path in self.repo_settings.repo_config.eclass_db.porttrees:
self.categories.extend(
portage.util.grabfile(os.path.join(path, "profiles", "categories"))
)
self.repo_settings.repoman_settings.categories = frozenset(
portage.util.stack_lists([self.categories], incremental=1)
)
self.categories = self.repo_settings.repoman_settings.categories
self.portdb = repo_settings.portdb
self.portdb.settings = self.repo_settings.repoman_settings
digest_only = (
self.options.mode != "manifest-check" and self.options.digest == "y"
)
self.generate_manifest = digest_only or self.options.mode in (
"manifest",
"commit",
"fix",
)
# We really only need to cache the metadata that's necessary for visibility
# filtering. Anything else can be discarded to reduce memory consumption.
if not self.generate_manifest:
# Don't do this when generating manifests, since that uses
# additional keys if spawn_nofetch is called (RESTRICT and
# DEFINED_PHASES).
self.portdb._aux_cache_keys.clear()
self.portdb._aux_cache_keys.update(
["EAPI", "IUSE", "KEYWORDS", "repository", "SLOT"]
)
self.reposplit = myreporoot.split(os.path.sep)
self.repolevel = len(self.reposplit)
if self.options.mode == "commit":
repochecks.commit_check(self.repolevel, self.reposplit)
repochecks.conflict_check(self.vcs_settings, self.options)
# Make startdir relative to the canonical repodir, so that we can pass
# it to digestgen and it won't have to be canonicalized again.
if self.repolevel == 1:
startdir = self.repo_settings.repodir
else:
startdir = normalize_path(mydir)
startdir = os.path.join(
self.repo_settings.repodir,
*startdir.split(os.sep)[-2 - self.repolevel + 3 :]
)
# get lists of valid keywords, licenses, and use
new_data = repo_metadata(self.portdb, self.repo_settings.repoman_settings)
(
kwlist,
liclist,
uselist,
profile_list,
global_pmaskdict,
liclist_deprecated,
) = new_data
self.repo_metadata = {
"kwlist": kwlist,
"liclist": liclist,
"uselist": uselist,
"profile_list": profile_list,
"pmaskdict": global_pmaskdict,
"lic_deprecated": liclist_deprecated,
"package.deprecated": InternalPackageSet(
initial_atoms=portage.util.stack_lists(
[
portage.util.grabfile_package(
os.path.join(path, "profiles", "package.deprecated"),
recursive=True,
)
for path in self.portdb.porttrees
],
incremental=True,
)
),
}
self.repo_settings.repoman_settings["PORTAGE_ARCHLIST"] = " ".join(
sorted(kwlist)
)
self.repo_settings.repoman_settings.backup_changes("PORTAGE_ARCHLIST")
profiles = setup_profile(profile_list)
check_profiles(profiles, self.repo_settings.repoman_settings.archlist())
scanlist = scan(
self.repolevel,
self.reposplit,
startdir,
self.categories,
self.repo_settings,
)
self.dev_keywords = dev_profile_keywords(profiles)
self.qatracker = self.vcs_settings.qatracker
if (
self.options.echangelog is None
and self.repo_settings.repo_config.update_changelog
):
self.options.echangelog = "y"
if self.vcs_settings.vcs is None:
self.options.echangelog = "n"
# Initialize the ModuleConfig class here
# TODO Add layout.conf masters repository.yml config to the list to load/stack
self.moduleconfig = ModuleConfig(
self.repo_settings.masters_list,
self.repo_settings.repoman_settings.valid_versions,
repository_modules=self.options.experimental_repository_modules == "y",
)
checks = {}
# The --echangelog option causes automatic ChangeLog generation,
# which invalidates changelog.ebuildadded and changelog.missing
# checks.
# Note: Some don't use ChangeLogs in distributed SCMs.
# It will be generated on server side from scm log,
# before package moves to the rsync server.
# This is needed because they try to avoid merge collisions.
# Gentoo's Council decided to always use the ChangeLog file.
# TODO: shouldn't this just be switched on the repo, iso the VCS?
is_echangelog_enabled = self.options.echangelog in ("y", "force")
self.vcs_settings.vcs_is_cvs_or_svn = self.vcs_settings.vcs in ("cvs", "svn")
checks["changelog"] = (
not is_echangelog_enabled and self.vcs_settings.vcs_is_cvs_or_svn
)
if self.options.mode == "manifest" or self.options.quiet:
pass
elif self.options.pretend:
print(green("\nRepoMan does a once-over of the neighborhood..."))
else:
print(green("\nRepoMan scours the neighborhood..."))
self.changed = self.vcs_settings.changes
# bypass unneeded VCS operations if not needed
if self.options.if_modified == "y" or self.options.mode not in (
"manifest",
"manifest-check",
):
self.changed.scan()
self.have = {
"pmasked": False,
"dev_keywords": False,
}
# NOTE: match-all caches are not shared due to potential
# differences between profiles in _get_implicit_iuse.
self.caches = {
"arch": {},
"arch_xmatch": {},
"shared_xmatch": {"cp-list": {}},
}
self.include_arches = None
if self.options.include_arches:
self.include_arches = set()
self.include_arches.update(
*[x.split() for x in self.options.include_arches]
)
self.include_profiles = None
if self.options.include_profiles:
self.include_profiles = set()
self.include_profiles.update(
*[x.split() for x in self.options.include_profiles]
)
# Disable the "self.modules['Ebuild'].notadded" check when not in commit mode and
# running `svn status` in every package dir will be too expensive.
checks["ebuild_notadded"] = not (
self.vcs_settings.vcs == "svn"
and self.repolevel < 3
and self.options.mode != "commit"
)
self.effective_scanlist = scanlist
if self.options.if_modified == "y":
self.effective_scanlist = sorted(
vcs_files_to_cps(
chain(self.changed.changed, self.changed.new, self.changed.removed),
self.repo_settings.repodir,
self.repolevel,
self.reposplit,
self.categories,
)
)
# Create our kwargs dict here to initialize the plugins with
self.kwargs = {
"repo_settings": self.repo_settings,
"portdb": self.portdb,
"qatracker": self.qatracker,
"vcs_settings": self.vcs_settings,
"options": self.options,
"metadata_xsd": get_metadata_xsd(self.repo_settings),
"uselist": uselist,
"checks": checks,
"repo_metadata": self.repo_metadata,
"profiles": profiles,
"include_arches": self.include_arches,
"include_profiles": self.include_profiles,
"caches": self.caches,
"repoman_incrementals": self.repoman_incrementals,
"env": self.env,
"have": self.have,
"dev_keywords": self.dev_keywords,
"linechecks": self.moduleconfig.linechecks,
}
# initialize the plugin checks here
self.modules = {}
self._ext_futures = {}
self.pkg_level_futures = None
def set_kwargs(self, mod):
"""Creates a limited set of kwargs to pass to the module's __init__()
@param mod: module name string
@returns: dictionary
"""
kwargs = {}
for key in self.moduleconfig.controller.modules[mod]["mod_kwargs"]:
kwargs[key] = self.kwargs[key]
return kwargs
def set_func_kwargs(self, mod, dynamic_data=None):
"""Updates the dynamic_data dictionary with any new key, value pairs.
Creates a limited set of kwargs to pass to the modulefunctions to run
@param mod: module name string
@param dynamic_data: dictionary structure
@returns: dictionary
"""
func_kwargs = self.moduleconfig.controller.modules[mod]["func_kwargs"]
# determine new keys
required = set(list(func_kwargs))
exist = set(list(dynamic_data))
new = required.difference(exist)
# update dynamic_data with initialized entries
for key in new:
logging.debug("set_func_kwargs(); adding: %s, %s", key, func_kwargs[key])
if func_kwargs[key][0] in ["Future", "ExtendedFuture"]:
if key not in self._ext_futures:
logging.debug(
"Adding a new key: %s to the ExtendedFuture dict", key
)
self._ext_futures[key] = func_kwargs[key]
self._set_future(dynamic_data, key, func_kwargs[key])
else: # builtin python data type
dynamic_data[key] = DATA_TYPES[func_kwargs[key][0]]()
kwargs = {}
for key in required:
kwargs[key] = dynamic_data[key]
return kwargs
def reset_futures(self, dynamic_data):
"""Reset any Future data types
@param dynamic_data: dictionary
"""
for key in list(self._ext_futures):
if key not in self.pkg_level_futures:
self._set_future(dynamic_data, key, self._ext_futures[key])
@staticmethod
def _set_future(dynamic_data, key, data):
"""Set a dynamic_data key to a new ExtendedFuture instance
@param dynamic_data: dictionary
@param key: tuple of (dictionary-key, default-value)
"""
if data[0] in ["Future", "ExtendedFuture"]:
if data[1] in ["UNSET"]:
dynamic_data[key] = ExtendedFuture()
else:
if data[1] in DATA_TYPES:
default = DATA_TYPES[data[1]]()
else:
default = data[1]
dynamic_data[key] = ExtendedFuture(default)
def scan_pkgs(self, can_force):
for xpkg in self.effective_scanlist:
xpkg_continue = False
# ebuilds and digests added to cvs respectively.
logging.info("checking package %s", xpkg)
# save memory by discarding xmatch caches from previous package(s)
self.caches["arch_xmatch"].clear()
catdir, pkgdir = xpkg.split("/")
checkdir = self.repo_settings.repodir + "/" + xpkg
checkdir_relative = ""
if self.repolevel < 3:
checkdir_relative = os.path.join(pkgdir, checkdir_relative)
if self.repolevel < 2:
checkdir_relative = os.path.join(catdir, checkdir_relative)
checkdir_relative = os.path.join(".", checkdir_relative)
# Run the status check
if self.kwargs["checks"]["ebuild_notadded"]:
self.vcs_settings.status.check(checkdir, checkdir_relative, xpkg)
if self.generate_manifest:
if not manifest.Manifest(**self.kwargs).update_manifest(checkdir):
self.qatracker.add_error(
"manifest.bad", os.path.join(xpkg, "Manifest")
)
if self.options.mode == "manifest":
continue
checkdirlist = os.listdir(checkdir)
dynamic_data = {
"changelog_modified": False,
"checkdirlist": ExtendedFuture(checkdirlist),
"checkdir": checkdir,
"xpkg": xpkg,
"changed": self.changed,
"checkdir_relative": checkdir_relative,
"can_force": can_force,
"repolevel": self.repolevel,
"catdir": catdir,
"pkgdir": pkgdir,
"validity_future": ExtendedFuture(True),
"y_ebuild": None,
# this needs to be reset at the pkg level only,
# easiest is to just initialize it here
"muselist": ExtendedFuture(set()),
"src_uri_error": ExtendedFuture(),
}
self.pkg_level_futures = [
"checkdirlist",
"muselist",
"pkgs",
"src_uri_error",
"validity_future",
]
# need to set it up for ==> self.modules or some other ordered list
logging.debug("***** starting pkgs_loop: %s", self.moduleconfig.pkgs_loop)
for mod in self.moduleconfig.pkgs_loop:
mod_class = self.moduleconfig.controller.get_class(mod)
logging.debug("Initializing class name: %s", mod_class.__name__)
self.modules[mod_class.__name__] = mod_class(**self.set_kwargs(mod))
logging.debug("scan_pkgs; module: %s", mod_class.__name__)
do_it, functions = self.modules[mod_class.__name__].runInPkgs
if do_it:
for func in functions:
_continue = func(**self.set_func_kwargs(mod, dynamic_data))
if _continue:
# If we can't access all the metadata then it's totally unsafe to
# commit since there's no way to generate a correct Manifest.
# Do not try to do any more QA checks on this package since missing
# metadata leads to false positives for several checks, and false
# positives confuse users.
xpkg_continue = True
break
if xpkg_continue:
continue
# Sort ebuilds in ascending order for the KEYWORDS.dropped check.
pkgs = dynamic_data["pkgs"].get()
ebuildlist = sorted(pkgs.values())
ebuildlist = [pkg.pf for pkg in ebuildlist]
if self.kwargs["checks"]["changelog"] and "ChangeLog" not in checkdirlist:
self.qatracker.add_error("changelog.missing", xpkg + "/ChangeLog")
changelog_path = os.path.join(checkdir_relative, "ChangeLog")
dynamic_data["changelog_modified"] = (
changelog_path in self.changed.changelogs
)
self._scan_ebuilds(ebuildlist, dynamic_data)
def _scan_ebuilds(self, ebuildlist, dynamic_data):
for y_ebuild in ebuildlist:
self.reset_futures(dynamic_data)
dynamic_data["y_ebuild"] = y_ebuild
# initialize per ebuild plugin checks here
# need to set it up for ==> self.modules_list or some other ordered list
for mod in self.moduleconfig.ebuilds_loop:
if mod:
mod_class = self.moduleconfig.controller.get_class(mod)
if mod_class.__name__ not in self.modules:
logging.debug("Initializing class name: %s", mod_class.__name__)
self.modules[mod_class.__name__] = mod_class(
**self.set_kwargs(mod)
)
logging.debug("scan_ebuilds: module: %s", mod_class.__name__)
do_it, functions = self.modules[mod_class.__name__].runInEbuilds
logging.debug(
"do_it: %s, functions: %s", do_it, [x.__name__ for x in functions]
)
if do_it:
for func in functions:
logging.debug("\tRunning function: %s", func)
_continue = func(**self.set_func_kwargs(mod, dynamic_data))
if _continue:
# If we can't access all the metadata then it's totally unsafe to
# commit since there's no way to generate a correct Manifest.
# Do not try to do any more QA checks on this package since missing
# metadata leads to false positives for several checks, and false
# positives confuse users.
# logging.debug("\t>>> Continuing")
break
logging.debug("Finished ebuild plugin loop, continuing...")
# Final checks
# initialize per pkg plugin final checks here
# need to set it up for ==> self.modules_list or some other ordered list
for mod in self.moduleconfig.final_loop:
if mod:
mod_class = self.moduleconfig.controller.get_class(mod)
if mod_class.__name__ not in self.modules:
logging.debug("Initializing class name: %s", mod_class.__name__)
self.modules[mod_class.__name__] = mod_class(**self.set_kwargs(mod))
logging.debug("scan_ebuilds final checks: module: %s", mod_class.__name__)
do_it, functions = self.modules[mod_class.__name__].runInFinal
logging.debug(
"do_it: %s, functions: %s", do_it, [x.__name__ for x in functions]
)
if do_it:
for func in functions:
logging.debug("\tRunning function: %s", func)
_continue = func(**self.set_func_kwargs(mod, dynamic_data))
if _continue:
# logging.debug("\t>>> Continuing")
break