modularity: depsolving

The commit implements modularity for the dnf4 dependency solver.

The dependency solver now takes two new keys per transaction:
`module-install-specs` and `module-enable-specs`, each of which is a
list of module specifications.

When modules are used and packages for them are installed a new
dictionary is returned in the response where each key is the name of an
enabled module and the value are the contents of the module
configuration and the module failsafe file that DNF needs in the
resulting system.

These values can be used by consumers of the dependency solver, such as
`images`, to create the appropriate inline sources and copy stages.

Signed-off-by: Simon de Vlieger <supakeen@redhat.com>
This commit is contained in:
Simon de Vlieger 2024-11-22 09:41:44 +01:00 committed by Ondřej Budai
parent 1a529d78a9
commit 6e9fb7dd6a

View file

@ -1,6 +1,8 @@
import itertools
import os
import os.path
import tempfile
import textwrap
from datetime import datetime
from typing import Dict, List
@ -54,6 +56,7 @@ class DNF(SolverBase):
self.base.conf.substitutions['arch'] = arch
self.base.conf.substitutions['basearch'] = dnf.rpm.basearch(arch)
self.base.conf.substitutions['releasever'] = releasever
if hasattr(self.base.conf, "optional_metadata_types"):
# the attribute doesn't exist on older versions of dnf; ignore the option when not available
self.base.conf.optional_metadata_types.extend(arguments.get("optional-metadata", []))
@ -85,6 +88,10 @@ class DNF(SolverBase):
except dnf.exceptions.Error as e:
raise RepoError(e) from e
# enable module resolving
self.base_module = dnf.module.module_base.ModuleBase(self.base)
# pylint: disable=too-many-branches
@staticmethod
def _dnfrepo(desc, parent_conf=None):
@ -261,7 +268,14 @@ class DNF(SolverBase):
for installed_pkg in last_transaction:
self.base.package_install(installed_pkg, strict=True)
# depsolve the current transaction
# enabling a module means that packages can be installed from that
# module
self.base_module.enable(transaction.get("module-enable-specs", []))
# installing a module takes the specification of the module and then
# installs all packages belonging to it
self.base_module.install(transaction.get("module-install-specs", []))
self.base.install_specs(
transaction.get("package-specs"),
transaction.get("exclude-specs"),
@ -288,6 +302,7 @@ class DNF(SolverBase):
pkg_repos = {}
for package in last_transaction:
packages.append({
"nevra": f"{package.name}-{package.evr}.{package.arch}",
"name": package.name,
"epoch": package.epoch,
"version": package.version,
@ -322,9 +337,73 @@ class DNF(SolverBase):
"solver": "dnf",
"packages": packages,
"repos": repositories,
"modules": {},
}
if "sbom" in arguments:
response["sbom"] = self._sbom_for_pkgset(last_transaction)
# if any modules have been requested we add sources for these so they can
# be used by stages to enable the modules in the eventual artifact
modules = []
for transaction in transactions:
if transaction.get("module-install-specs") or transaction.get("module-enable-specs"):
# we'll be checking later if any packages-from-modules are in the
# packages-to-install set so let's do this only once here
package_nevras = list(p["nevra"] for p in packages)
for module_spec in itertools.chain(
transaction.get("module-install-specs", []),
transaction.get("module-enable-specs", []),
):
# we don't particularly care about the NSVCAP here, just the module
# packages that were previously selected
module_packages, _ = self.base_module.get_modules(module_spec)
# we now need to do an annoying dance as multiple modules could be
# returned by `.get_modules`, we need to select the *same* one as
# previously selected. we do this by checking if any of the module
# packages are in the packages set marked for installation.
# this is a result of not being able to get the enabled modules
# from the transaction, if that turns out to be possible then
# we can get rid of these shenanigans
for module_package in module_packages:
module_nevras = module_package.getArtifacts()
if any(module_nevra in package_nevras for module_nevra in module_nevras):
# a package from this module is being installed so we must
# use this module
modules.append(module_package)
# we are probably able to skip the rest of the `module_packages`
# here if we want since no two modules can be enabled by the same
# name
break
# now we have the information we need about modules so we need to return *some*
# information to who is using the depsolver so they can use that information to
# enable these modules in the artifact
# there are two files that matter for each module that is used, the caller needs
# to write a file to `/etc/dnf/modules.d/{module_name}.module` to enable the
# module for dnf
# the caller also needs to set up `/var/lib/dnf/modulefailsafe/` with the contents
# of the modulemd for the selected modules, this is to ensure that even when a
# repository is disabled or disappears that non-modular content can't be installed
# see: https://dnf.readthedocs.io/en/latest/modularity.html#fail-safe-mechanisms
for module in modules:
response["modules"][module.getName()] = {
"module-file": textwrap.dedent(f"""\
[{module.getName()}]
name={module.getName()}
stream={module.getStream()}
profiles=common
state=enabled
"""),
"failsafe-file": module.getYaml(),
}
return response