41a629969c
https://black.readthedocs.io/en/stable/ JIRA: COMPOSE-4086 Signed-off-by: Haibo Lin <hlin@redhat.com>
562 lines
20 KiB
Python
562 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; version 2 of the License.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Library General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, see <https://gnu.org/licenses/>.
|
|
|
|
import gzip
|
|
import os
|
|
from collections import defaultdict
|
|
from fnmatch import fnmatch
|
|
|
|
import createrepo_c as cr
|
|
import kobo.rpmlib
|
|
from kobo.shortcuts import run
|
|
|
|
import pungi.phases.gather.method
|
|
from pungi import multilib_dnf
|
|
from pungi.module_util import Modulemd
|
|
from pungi.arch import get_valid_arches, tree_arch_to_yum_arch
|
|
from pungi.phases.gather import _mk_pkg_map
|
|
from pungi.util import get_arch_variant_data, pkg_is_debug, temp_dir, as_local_file
|
|
from pungi.wrappers import fus
|
|
from pungi.wrappers.comps import CompsWrapper
|
|
|
|
from .method_nodeps import expand_groups
|
|
|
|
|
|
class FakePackage(object):
|
|
"""This imitates a DNF package object and can be passed to python-multilib
|
|
library.
|
|
"""
|
|
|
|
def __init__(self, pkg):
|
|
self.pkg = pkg
|
|
|
|
def __getattr__(self, attr):
|
|
return getattr(self.pkg, attr)
|
|
|
|
@property
|
|
def files(self):
|
|
return [
|
|
os.path.join(dirname, basename) for (_, dirname, basename) in self.pkg.files
|
|
]
|
|
|
|
@property
|
|
def provides(self):
|
|
# This is supposed to match what yum package object returns. It's a
|
|
# nested tuple (name, flag, (epoch, version, release)). This code only
|
|
# fills in the name, because that's all that python-multilib is using..
|
|
return [(p[0].split()[0], None, (None, None, None)) for p in self.pkg.provides]
|
|
|
|
|
|
class GatherMethodHybrid(pungi.phases.gather.method.GatherMethodBase):
|
|
enabled = True
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(GatherMethodHybrid, self).__init__(*args, **kwargs)
|
|
self.package_maps = {}
|
|
self.packages = {}
|
|
# Mapping from package name to set of langpack packages (stored as
|
|
# names).
|
|
self.langpacks = {}
|
|
# Set of packages for which we already added langpacks.
|
|
self.added_langpacks = set()
|
|
# Set of NEVRAs of modular packages
|
|
self.modular_packages = set()
|
|
# Arch -> pkg name -> set of pkg object
|
|
self.debuginfo = defaultdict(lambda: defaultdict(set))
|
|
|
|
# caches for processed packages
|
|
self.processed_multilib = set()
|
|
self.processed_debuginfo = set()
|
|
|
|
def _get_pkg_map(self, arch):
|
|
"""Create a mapping from NEVRA to actual package object. This will be
|
|
done once for each architecture, since the package set is the same for
|
|
all variants.
|
|
|
|
The keys are in NEVRA format and only include the epoch if it's not
|
|
zero. This makes it easier to query by results for the depsolver.
|
|
"""
|
|
if arch not in self.package_maps:
|
|
pkg_map = {}
|
|
for pkgset in self.package_sets:
|
|
for pkg_arch in pkgset.package_sets[arch].rpms_by_arch:
|
|
for pkg in pkgset.package_sets[arch].rpms_by_arch[pkg_arch]:
|
|
pkg_map[_fmt_nevra(pkg, pkg_arch)] = pkg
|
|
self.package_maps[arch] = pkg_map
|
|
|
|
return self.package_maps[arch]
|
|
|
|
def _prepare_packages(self):
|
|
for repo_path in self.get_repos():
|
|
md = cr.Metadata()
|
|
md.locate_and_load_xml(repo_path)
|
|
for key in md.keys():
|
|
pkg = md.get(key)
|
|
if pkg.arch in self.valid_arches:
|
|
self.packages[_fmt_nevra(pkg, arch=pkg.arch)] = FakePackage(pkg)
|
|
|
|
def _get_package(self, nevra):
|
|
if not self.packages:
|
|
self._prepare_packages()
|
|
return self.packages[nevra]
|
|
|
|
def _prepare_debuginfo(self):
|
|
"""Prepare cache of debuginfo packages for easy access. The cache is
|
|
indexed by package architecture and then by package name. There can be
|
|
more than one debuginfo package with the same name.
|
|
"""
|
|
for pkgset in self.package_sets:
|
|
for pkg_arch in pkgset.package_sets[self.arch].rpms_by_arch:
|
|
for pkg in pkgset.package_sets[self.arch].rpms_by_arch[pkg_arch]:
|
|
self.debuginfo[pkg.arch][pkg.name].add(pkg)
|
|
|
|
def _get_debuginfo(self, name, arch):
|
|
if not self.debuginfo:
|
|
self._prepare_debuginfo()
|
|
return self.debuginfo.get(arch, {}).get(name, set())
|
|
|
|
def expand_list(self, patterns):
|
|
"""Given a list of globs, create a list of package names matching any
|
|
of the pattern.
|
|
"""
|
|
expanded = set()
|
|
for pkgset in self.package_sets:
|
|
for pkg_arch in pkgset.package_sets[self.arch].rpms_by_arch:
|
|
for pkg in pkgset.package_sets[self.arch].rpms_by_arch[pkg_arch]:
|
|
for pattern in patterns:
|
|
if fnmatch(pkg.name, pattern):
|
|
expanded.add(pkg)
|
|
break
|
|
return expanded
|
|
|
|
def prepare_modular_packages(self):
|
|
for var in self.compose.all_variants.values():
|
|
for mmd in var.arch_mmds.get(self.arch, {}).values():
|
|
self.modular_packages.update(mmd.get_rpm_artifacts())
|
|
|
|
def prepare_langpacks(self, arch, variant):
|
|
if not self.compose.has_comps:
|
|
return
|
|
comps_file = self.compose.paths.work.comps(arch, variant, create_dir=False)
|
|
comps = CompsWrapper(comps_file)
|
|
|
|
for name, install in comps.get_langpacks().items():
|
|
# Replace %s with * for fnmatch.
|
|
install_match = install % "*"
|
|
self.langpacks[name] = set()
|
|
for pkgset in self.package_sets:
|
|
for pkg_arch in pkgset.package_sets[arch].rpms_by_arch:
|
|
for pkg in pkgset.package_sets[arch].rpms_by_arch[pkg_arch]:
|
|
if not fnmatch(pkg.name, install_match):
|
|
# Does not match the pattern, ignore...
|
|
continue
|
|
if pkg.name.endswith("-devel") or pkg.name.endswith("-static"):
|
|
continue
|
|
if pkg_is_debug(pkg):
|
|
continue
|
|
self.langpacks[name].add(pkg.name)
|
|
|
|
def __call__(
|
|
self,
|
|
arch,
|
|
variant,
|
|
package_sets,
|
|
packages=[],
|
|
groups=[],
|
|
multilib_whitelist=[],
|
|
multilib_blacklist=[],
|
|
filter_packages=[],
|
|
prepopulate=[],
|
|
**kwargs
|
|
):
|
|
self.arch = arch
|
|
self.variant = variant
|
|
self.valid_arches = get_valid_arches(arch, multilib=True)
|
|
self.package_sets = package_sets
|
|
|
|
self.prepare_langpacks(arch, variant)
|
|
self.prepare_modular_packages()
|
|
|
|
self.multilib_methods = get_arch_variant_data(
|
|
self.compose.conf, "multilib", arch, variant
|
|
)
|
|
self.multilib = multilib_dnf.Multilib(
|
|
self.multilib_methods,
|
|
set(p.name for p in self.expand_list(multilib_blacklist)),
|
|
set(p.name for p in self.expand_list(multilib_whitelist)),
|
|
)
|
|
|
|
platform = get_platform(self.compose, variant, arch)
|
|
|
|
packages.update(
|
|
expand_groups(self.compose, arch, variant, groups, set_pkg_arch=False)
|
|
)
|
|
|
|
packages.update(tuple(pkg.rsplit(".", 1)) for pkg in prepopulate)
|
|
|
|
# Filters are received as tuples (name, arch), we should convert it to
|
|
# strings.
|
|
filters = [_fmt_pkg(*p) for p in filter_packages]
|
|
|
|
cache_prefix = "fus-cache-%s-%s-%s-" % (self.compose.compose_id, variant, arch)
|
|
with temp_dir(prefix=cache_prefix) as cache_dir:
|
|
nvrs, out_modules = self.run_solver(
|
|
variant, arch, packages, platform, filters, cache_dir=cache_dir
|
|
)
|
|
filter_modules(variant, arch, out_modules)
|
|
return expand_packages(
|
|
self._get_pkg_map(arch),
|
|
pungi.phases.gather.get_lookaside_repos(self.compose, arch, variant),
|
|
nvrs,
|
|
filter_packages=filter_packages,
|
|
)
|
|
# maybe check invalid sigkeys
|
|
|
|
def get_repos(self):
|
|
repos = []
|
|
for pkgset in self.package_sets:
|
|
if self.variant.pkgsets and pkgset.name not in self.variant.pkgsets:
|
|
continue
|
|
repos.append(pkgset.paths[self.arch])
|
|
return repos
|
|
|
|
def run_solver(self, variant, arch, packages, platform, filter_packages, cache_dir):
|
|
repos = self.get_repos()
|
|
results = set()
|
|
result_modules = set()
|
|
|
|
modules = []
|
|
for mmd in variant.arch_mmds.get(arch, {}).values():
|
|
modules.append("%s:%s" % (mmd.get_module_name(), mmd.get_stream_name()))
|
|
|
|
input_packages = []
|
|
for pkg_name, pkg_arch in packages:
|
|
input_packages.extend(self._expand_wildcard(pkg_name, pkg_arch))
|
|
|
|
step = 0
|
|
|
|
while True:
|
|
step += 1
|
|
conf_file = self.compose.paths.work.fus_conf(arch, variant, step)
|
|
fus.write_config(conf_file, sorted(modules), sorted(input_packages))
|
|
cmd = fus.get_cmd(
|
|
conf_file,
|
|
tree_arch_to_yum_arch(arch),
|
|
repos,
|
|
pungi.phases.gather.get_lookaside_repos(self.compose, arch, variant),
|
|
platform=platform,
|
|
filter_packages=filter_packages,
|
|
)
|
|
logfile = self.compose.paths.log.log_file(
|
|
arch, "hybrid-depsolver-%s-iter-%d" % (variant, step)
|
|
)
|
|
# Adding this environement variable will tell GLib not to prefix
|
|
# any log messages with the PID of the fus process (which is quite
|
|
# useless for us anyway).
|
|
env = os.environ.copy()
|
|
env["G_MESSAGES_PREFIXED"] = ""
|
|
env["XDG_CACHE_HOME"] = cache_dir
|
|
self.compose.log_debug(
|
|
"[BEGIN] Running fus (arch: %s, variant: %s)" % (arch, variant)
|
|
)
|
|
run(cmd, logfile=logfile, show_cmd=True, env=env)
|
|
output, out_modules = fus.parse_output(logfile)
|
|
self.compose.log_debug(
|
|
"[DONE ] Running fus (arch: %s, variant: %s)" % (arch, variant)
|
|
)
|
|
# No need to resolve modules again. They are not going to change.
|
|
modules = []
|
|
# Reset input packages as well to only solve newly added things.
|
|
input_packages = []
|
|
# Preserve the results from this iteration.
|
|
results.update(output)
|
|
result_modules.update(out_modules)
|
|
|
|
new_multilib = self.add_multilib(variant, arch, output)
|
|
input_packages.extend(
|
|
_fmt_pkg(pkg_name, pkg_arch)
|
|
for pkg_name, pkg_arch in sorted(new_multilib)
|
|
)
|
|
|
|
new_debuginfo = self.add_debuginfo(arch, output)
|
|
input_packages.extend(
|
|
_fmt_pkg(pkg_name, pkg_arch)
|
|
for pkg_name, pkg_arch in sorted(new_debuginfo)
|
|
)
|
|
|
|
new_langpacks = self.add_langpacks(output)
|
|
input_packages.extend(new_langpacks)
|
|
|
|
if not input_packages:
|
|
# Nothing new was added, we can stop now.
|
|
break
|
|
|
|
return results, result_modules
|
|
|
|
def add_multilib(self, variant, arch, nvrs):
|
|
added = set()
|
|
if not self.multilib_methods:
|
|
return []
|
|
|
|
for nvr, pkg_arch, flags in nvrs:
|
|
if (nvr, pkg_arch) in self.processed_multilib:
|
|
continue
|
|
self.processed_multilib.add((nvr, pkg_arch))
|
|
|
|
if "modular" in flags:
|
|
continue
|
|
|
|
if pkg_arch != arch:
|
|
# Not a native package, not checking to add multilib
|
|
continue
|
|
|
|
nevr = kobo.rpmlib.parse_nvr(nvr)
|
|
|
|
for add_arch in self.valid_arches:
|
|
if add_arch == arch:
|
|
continue
|
|
try:
|
|
multilib_candidate = self._get_package("%s.%s" % (nvr, add_arch))
|
|
except KeyError:
|
|
continue
|
|
if self.multilib.is_multilib(multilib_candidate):
|
|
added.add((nevr["name"], add_arch))
|
|
|
|
return added
|
|
|
|
def add_debuginfo(self, arch, nvrs):
|
|
added = set()
|
|
|
|
for nvr, pkg_arch, flags in nvrs:
|
|
if (nvr, pkg_arch) in self.processed_debuginfo:
|
|
continue
|
|
self.processed_debuginfo.add((nvr, pkg_arch))
|
|
|
|
if "modular" in flags:
|
|
continue
|
|
|
|
pkg = self._get_package("%s.%s" % (nvr, pkg_arch))
|
|
|
|
# There are two ways how the debuginfo package can be named. We
|
|
# want to get them all.
|
|
for pattern in ["%s-debuginfo", "%s-debugsource"]:
|
|
debuginfo_name = pattern % pkg.name
|
|
debuginfo = self._get_debuginfo(debuginfo_name, pkg_arch)
|
|
for dbg in debuginfo:
|
|
# For each debuginfo package that matches on name and
|
|
# architecture, we also need to check if it comes from the
|
|
# same build.
|
|
if dbg.sourcerpm == pkg.rpm_sourcerpm:
|
|
added.add((dbg.name, dbg.arch))
|
|
|
|
return added
|
|
|
|
def add_langpacks(self, nvrs):
|
|
if not self.langpacks:
|
|
return set()
|
|
|
|
added = set()
|
|
for nvr, pkg_arch, flags in nvrs:
|
|
if "modular" in flags:
|
|
continue
|
|
name = nvr.rsplit("-", 2)[0]
|
|
if name in self.added_langpacks:
|
|
# This package is already processed.
|
|
continue
|
|
added.update(self.langpacks.get(name, []))
|
|
self.added_langpacks.add(name)
|
|
|
|
return sorted(added)
|
|
|
|
def _expand_wildcard(self, pkg_name, pkg_arch):
|
|
if "*" not in pkg_name:
|
|
return [_fmt_pkg(pkg_name, pkg_arch)]
|
|
|
|
packages = []
|
|
|
|
for pkg in self.expand_list([pkg_name]):
|
|
if pkg_is_debug(pkg):
|
|
# No debuginfo
|
|
continue
|
|
|
|
if pkg_arch:
|
|
if pkg_arch != pkg.arch:
|
|
# Arch is specified and does not match, skip the package.
|
|
continue
|
|
else:
|
|
if pkg.arch not in ("noarch", self.arch):
|
|
# No arch specified and package does not match
|
|
continue
|
|
|
|
strict_nevra = "%s-%s:%s-%s.%s" % (
|
|
pkg.name,
|
|
pkg.epoch or "0",
|
|
pkg.version,
|
|
pkg.release,
|
|
pkg.arch,
|
|
)
|
|
if strict_nevra in self.modular_packages:
|
|
# Wildcards should not match modular packages.
|
|
continue
|
|
|
|
packages.append(_fmt_nevra(pkg, pkg.arch))
|
|
|
|
return packages
|
|
|
|
|
|
def iter_platforms_in_repo(url):
|
|
"""Find all platform streams that any module in give repo requires at runtime.
|
|
Yields lists of stream names (possible empty).
|
|
"""
|
|
repomd = os.path.join(url, "repodata/repomd.xml")
|
|
with as_local_file(repomd) as url_:
|
|
repomd = cr.Repomd(url_)
|
|
for rec in repomd.records:
|
|
if rec.type != "modules":
|
|
continue
|
|
# No with statement on Python 2.6 for GzipFile...
|
|
record_url = os.path.join(url, rec.location_href)
|
|
with as_local_file(record_url) as url_:
|
|
gzipped_file = gzip.GzipFile(url_, "rb")
|
|
mod_index = Modulemd.ModuleIndex.new()
|
|
mod_index.update_from_string(gzipped_file.read().decode("utf-8"), False)
|
|
gzipped_file.close()
|
|
for module_name in mod_index.get_module_names():
|
|
module = mod_index.get_module(module_name)
|
|
for module_stream in module.get_all_streams():
|
|
module_stream = module_stream.upgrade(2)
|
|
for dep in module_stream.get_dependencies():
|
|
yield dep.get_runtime_streams("platform")
|
|
|
|
|
|
def get_platform_from_lookasides(compose, variant, arch):
|
|
"""Find a set of all platform dependencies in all lookaside repos."""
|
|
platforms = set()
|
|
for repo in pungi.phases.gather.get_lookaside_repos(compose, arch, variant):
|
|
for ps in iter_platforms_in_repo(fus._prep_path(repo)):
|
|
platforms.update(ps)
|
|
return platforms
|
|
|
|
|
|
def get_platform(compose, variant, arch):
|
|
"""Find platform stream for modules. Raises RuntimeError if there are
|
|
conflicting requests.
|
|
"""
|
|
platforms = get_platform_from_lookasides(compose, variant, arch)
|
|
|
|
for var in compose.all_variants.values():
|
|
for mmd in var.arch_mmds.get(arch, {}).values():
|
|
for dep in mmd.get_dependencies():
|
|
streams = dep.get_runtime_streams("platform")
|
|
if streams:
|
|
platforms.update(streams)
|
|
|
|
if len(platforms) > 1:
|
|
raise RuntimeError("There are conflicting requests for platform.")
|
|
|
|
return list(platforms)[0] if platforms else None
|
|
|
|
|
|
def _fmt_pkg(pkg_name, arch):
|
|
if arch:
|
|
pkg_name += ".%s" % arch
|
|
return pkg_name
|
|
|
|
|
|
def _nevra(**kwargs):
|
|
if kwargs.get("epoch") not in (None, "", 0, "0"):
|
|
return "%(name)s-%(epoch)s:%(version)s-%(release)s.%(arch)s" % kwargs
|
|
return "%(name)s-%(version)s-%(release)s.%(arch)s" % kwargs
|
|
|
|
|
|
def _fmt_nevra(pkg, arch):
|
|
return _nevra(
|
|
name=pkg.name,
|
|
epoch=pkg.epoch,
|
|
version=pkg.version,
|
|
release=pkg.release,
|
|
arch=arch,
|
|
)
|
|
|
|
|
|
def _get_srpm_nevra(pkg):
|
|
nevra = kobo.rpmlib.parse_nvra(pkg.sourcerpm)
|
|
nevra["epoch"] = nevra["epoch"] or pkg.epoch
|
|
return _nevra(**nevra)
|
|
|
|
|
|
def _make_result(paths):
|
|
return [{"path": path, "flags": []} for path in sorted(paths)]
|
|
|
|
|
|
def expand_packages(nevra_to_pkg, lookasides, nvrs, filter_packages):
|
|
"""For each package add source RPM."""
|
|
# This will serve as the final result. We collect sets of paths to the
|
|
# packages.
|
|
rpms = set()
|
|
srpms = set()
|
|
debuginfo = set()
|
|
|
|
filters = set(filter_packages)
|
|
|
|
# Collect list of all packages in lookaside. These will not be added to the
|
|
# result. Fus handles this in part: if a package is explicitly mentioned as
|
|
# input (which can happen with comps group expansion), it will be in the
|
|
# output even if it's in lookaside.
|
|
lookaside_packages = set()
|
|
for repo in lookasides:
|
|
md = cr.Metadata()
|
|
md.locate_and_load_xml(repo)
|
|
for key in md.keys():
|
|
pkg = md.get(key)
|
|
url = os.path.join(pkg.location_base or repo, pkg.location_href)
|
|
# Strip file:// prefix
|
|
lookaside_packages.add(url[7:])
|
|
|
|
for nvr, pkg_arch, flags in nvrs:
|
|
pkg = nevra_to_pkg["%s.%s" % (nvr, pkg_arch)]
|
|
if pkg.file_path in lookaside_packages:
|
|
# Package is in lookaside, don't add it and ignore sources and
|
|
# debuginfo too.
|
|
continue
|
|
if pkg_is_debug(pkg):
|
|
debuginfo.add(pkg.file_path)
|
|
else:
|
|
rpms.add(pkg.file_path)
|
|
|
|
try:
|
|
srpm_nevra = _get_srpm_nevra(pkg)
|
|
srpm = nevra_to_pkg[srpm_nevra]
|
|
if (srpm.name, "src") in filters:
|
|
# Filtered package, skipping
|
|
continue
|
|
if srpm.file_path not in lookaside_packages:
|
|
srpms.add(srpm.file_path)
|
|
except KeyError:
|
|
# Didn't find source RPM.. this should be logged
|
|
pass
|
|
|
|
return _mk_pkg_map(_make_result(rpms), _make_result(srpms), _make_result(debuginfo))
|
|
|
|
|
|
def filter_modules(variant, arch, nsvcs_to_keep):
|
|
"""Remove any arch-specific module metadata from the module if it's not
|
|
listed in the list to keep. This will ultimately cause the module to not be
|
|
included in the final repodata and module metadata.
|
|
"""
|
|
for nsvc in list(variant.arch_mmds.get(arch, {}).keys()):
|
|
if nsvc not in nsvcs_to_keep:
|
|
del variant.arch_mmds[arch][nsvc]
|