-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add initial dependency loader (Hawkey based) #13
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,308 @@ | ||
#! /usr/bin/env python | ||
""" Interface with rpmreq to determine build dependencies """ | ||
from __future__ import print_function | ||
|
||
import copy | ||
import os | ||
import stat | ||
import logging | ||
|
||
from toolchest.rpm.utils import split_filename, componentize | ||
|
||
try: | ||
from rpmreq.actions import build_requires | ||
from rpmreq.query import Repo | ||
except ImportError: | ||
def build_requires(filename, repos, **kwargs): # NOQA | ||
# pylint: disable=unused-argument | ||
""" | ||
Stub build-requires | ||
rpmreq and, by extension, hawkey is required for deployment, | ||
but not for testing. | ||
Hawkey is part of libdnf, written in C, and not published | ||
to PyPI. | ||
""" | ||
if 'logger' in kwargs: | ||
kwargs['logger'].warn('build_requires: hawkey missing') | ||
return [], [], [] | ||
|
||
from collections import namedtuple | ||
Repo = namedtuple('Repo', ['id', 'url']) | ||
|
||
from atkinson.config.manager import ConfigManager | ||
|
||
|
||
class DependencySet(): | ||
""" | ||
Dependency Set generated from a source spec file and a given build tag. | ||
Includes: Met dependencies, dependencies with the wrong version, | ||
and missing dependencies. | ||
|
||
Met dependencies: | ||
[ {'name', 'version', 'release', 'epoch', 'component'}, ... ] | ||
^ From source rpm name | ||
('comparison' is included, too, but is always '==') | ||
|
||
Unmet dependencies: | ||
[ {'name', 'version', 'release', 'epoch', 'comparison'}, ... ] | ||
^ >=, ==, >, if available | ||
Missing: | ||
[ {'name', 'version', 'release', 'epoch', 'comparison'}, ... ] | ||
|
||
Config file format: | ||
--- | ||
|
||
build_sources: | ||
build-version-1: | ||
- http://whatever-location-1/x86_64 | ||
- http://whatever-location-2/x86_64 | ||
build-version-2: | ||
- http://whatever-location-3/arch/ | ||
""" | ||
|
||
def __init__(self, **kwargs): | ||
if 'config' not in kwargs: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if we just collapse this down to a config param, and then add a helper method to parse this into whatever ConfigManager needs below to initialize it? Then we can have an explicit signature for init, which I think makes usage clearer (if would have 4 params: config, logger, filename, version, which could then be documented, so one know what to pass in). |
||
# Don't need to keep this around | ||
args = {} | ||
if 'config_file' in kwargs: | ||
args['filenames'] = [kwargs['config_file']] | ||
del kwargs['config_file'] | ||
else: | ||
args['filenames'] = ['build_sources.yml'] | ||
|
||
if 'config_path' in kwargs: | ||
args['paths'] = kwargs['config_path'] | ||
|
||
mgr = ConfigManager(**args) | ||
self.config = copy.copy(mgr.config) | ||
else: | ||
self.config = copy.copy(kwargs['config']) | ||
del kwargs['config'] | ||
|
||
self.met = [] | ||
self.wrong_version = [] | ||
self.unmet = [] | ||
|
||
# Allow caller to inject logging infrastructure, if desired. | ||
# Eventually, pass to rpmreq, although, we don't log anything | ||
# in this code directly. | ||
if 'logger' in kwargs: | ||
self.log = kwargs['logger'] | ||
self.provided_log = True | ||
else: | ||
self.log = logging.getLogger(__name__) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should use our logging helper of the same name, so the actual backend we log to can be specified. Hmm, actually, it should probably go one stepfuther. Shouldn't this just be in the config file,and then the Configmanager loads the proper logger defined there? In that casewedon't even need to take the logger here,just set it up in the manager There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, we can have a logging configuration snippet that then just wraps our existing logging infra - good idea. So, we then don't need it as part of the init function at all, which is good. |
||
self.provided_log = False | ||
|
||
if 'filename' in kwargs and 'version' in kwargs: | ||
filename = kwargs['filename'] | ||
del kwargs['filename'] | ||
version = kwargs['version'] | ||
del kwargs['version'] | ||
self.get_spec_build_deps(filename, version, **kwargs) | ||
|
||
def get_spec_build_deps(self, filename, version, **kwargs): | ||
"""Call in to rpmreq and retrieve dependencies.""" | ||
st_info = os.stat(filename) | ||
if not stat.S_ISREG(st_info.st_mode): | ||
raise ValueError('{0} is not a regular file'.format(filename)) | ||
|
||
if not filename.endswith('.spec'): | ||
raise ValueError('{0} is not a spec file'.format(filename)) | ||
|
||
self.filename = filename | ||
|
||
if not self.config or self.config == {} or \ | ||
'build_sources' not in self.config: | ||
return | ||
|
||
srcs = self.config['build_sources'] | ||
if version not in srcs: | ||
raise ValueError('Invalid version: {0}, expected one of {1}'.format( | ||
version, | ||
[ver for ver in srcs])) | ||
|
||
repos = [] | ||
for idx in range(len(srcs[version])): | ||
repos.append(Repo(str(idx), srcs[version][idx])) | ||
lhh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# TODO: rpmreq supporting passdown of logger | ||
# e.g. if self.provided_log: | ||
# (add logger=self.log to kwargs) | ||
met_deps, wrong_version, unmet_deps = \ | ||
build_requires(filename, repos) | ||
|
||
self.met = transmogrify_met(met_deps) | ||
self.wrong_version = transmogrify_unmet(wrong_version) | ||
self.unmet = transmogrify_unmet(unmet_deps) | ||
|
||
if 'logger' in kwargs: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rather not see us interacting with args passed to the object on instantiation here, and simply always use self.logger. |
||
kwargs['logger'].info(str(self)) | ||
|
||
def __str__(self): | ||
"""Report useful information about ourself.""" | ||
fname = os.path.basename(self.filename) | ||
str_format = '{0}: {1} met, {2} incorrect, {3} missing' | ||
return str_format.format(fname, len(self.met), len(self.wrong_version), len(self.unmet)) | ||
|
||
|
||
def transmogrify_met(met_deps): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure where to put this, so I am going to drop it here. I am proposing a slight change in the design of this, suggesting that DependencySet do more what it sounds like (to me, at least), and return a set of Dependency objects. I notice that met, wrong_version and un_met all return hashes with the same fields, so why not simply add a type field or similar to denote met, un_met, or wrong_version. Then your transmogrify methods change to simply populate the current object based on type, and the Set object calls in the init something like:
|
||
""" | ||
Convert a Hawkey dependency into a simple dict | ||
|
||
This is generally only used by DependencySet, but could be used | ||
elsewhere | ||
""" | ||
if not isinstance(met_deps, list): | ||
raise ValueError('met_deps is not a list') | ||
ret = [] | ||
for dep in met_deps: | ||
info = {'component': componentize(dep.sourcerpm), | ||
'epoch': dep.epoch, | ||
'name': dep.name, | ||
'comparison': '==', # This is the version provided | ||
'version': dep.version, | ||
'release': dep.release} | ||
ret.append(info) | ||
return ret | ||
|
||
|
||
def transmogrify_unmet(unmet_deps): | ||
""" | ||
Convert list of string or repo.Depends to our simple dict format | ||
|
||
This is generally only used by DependencySet, but could be used | ||
elsewhere | ||
""" | ||
if not isinstance(unmet_deps, list): | ||
raise ValueError('unmet_deps is not a list') | ||
ret = [] | ||
for dep in unmet_deps: | ||
info = rpmdep_to_dep(str(dep)) | ||
ret.append(info) | ||
return ret | ||
|
||
|
||
def dep_to_tuple(dep): | ||
"""Convert one of our dependencies into an RPM-style tuple""" | ||
if not isinstance(dep, dict): | ||
raise ValueError('dep is not a dict') | ||
if 'version' not in dep: | ||
raise ValueError('Cannot convert {0}: version missing'.format(str(dep))) | ||
epoch = 0 | ||
version = None | ||
release = None | ||
if 'epoch' in dep: | ||
epoch = int(dep['epoch']) | ||
version = dep['version'] | ||
if 'release' in dep: | ||
release = dep['release'] | ||
return (epoch, version, release) | ||
|
||
|
||
def dep_to_nevr(dep): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These could probably remain outside the object, making them more reusable (and possibly moving them to toolchest?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 to making a toolchest rpm module. |
||
"""Convert one of our dependencies into an RPM NEVR string""" | ||
if not isinstance(dep, dict): | ||
raise ValueError('dep is not a dict') | ||
for field in ('name', 'version', 'release'): | ||
if field not in dep: | ||
raise ValueError('Cannot convert {0}: {1} missing'.format(str(dep), field)) | ||
ret = dep['name'] | ||
ret = ret + '-' | ||
if 'epoch' in dep and dep['epoch'] not in ('0', 0, ''): | ||
ret = ret + str(dep['epoch']) + ':' | ||
ret = ret + str(dep['version']) | ||
ret = ret + '-' + dep['release'] | ||
return ret | ||
|
||
|
||
def dep_to_rpmdep(dep): | ||
"""Convert a dependency into an RPM dependency string""" | ||
if 'comparison' not in dep: | ||
return dep['name'] | ||
ret = '{0} {1} '.format(dep['name'], dep['comparison']) | ||
if 'epoch' in dep and dep['epoch'] not in ('', 0, '0'): | ||
ret = ret + '{0}:'.format(dep['epoch']) | ||
ret = ret + '{0}'.format(dep['version']) | ||
if 'release' in dep and dep['release'] != '': | ||
ret = ret + '-{0}'.format(dep['release']) | ||
return ret | ||
|
||
|
||
def rpmdep_to_dep(dep): | ||
"""Convert a RPM style dependency into a simple dict""" | ||
vals = dep.split(' ') | ||
if len(vals) == 1: | ||
return {'name': vals[0]} | ||
if len(vals) != 3: | ||
raise ValueError('Could not parse: \"' + dep + '\"') | ||
ret = {'name': vals[0], | ||
'comparison': vals[1]} | ||
evr = vals[2].split('-') | ||
if len(evr) > 2: | ||
raise ValueError('Could not parse: \"' + dep + '\"') | ||
|
||
if len(evr) == 2: | ||
ret['release'] = evr[1] | ||
if ret['release'] == 'None': | ||
del ret['release'] | ||
|
||
if ':' in evr[0]: | ||
e_v = evr[0].split(':') | ||
ret['epoch'] = int(e_v[0]) | ||
if ret['epoch'] == '0': | ||
del ret['epoch'] | ||
ret['version'] = e_v[1] | ||
else: | ||
ret['version'] = evr[0] | ||
|
||
return ret | ||
|
||
|
||
def nevr_to_dep(dep): | ||
"""Convert a RPM NEVR string to a simple dict""" | ||
(name, version, release, epoch, _) = split_filename(dep) | ||
ret = {} | ||
|
||
if name == '': | ||
raise ValueError('Could not parse: \"' + dep + '\"') | ||
|
||
ret['name'] = name | ||
ret['version'] = version | ||
ret['comparison'] = '==' | ||
if epoch not in ('', 0, '0'): | ||
ret['epoch'] = int(epoch) | ||
if release != 'None' and release is not None: | ||
ret['release'] = release | ||
|
||
return ret | ||
|
||
|
||
def _main(argv): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At some point we should remove this. But that can be at a later time when we have more of the runner infrastructure setup. We could however pull this out to a contrib or manual test area that just calls this module. |
||
"""Simple test program""" | ||
if len(argv) < 3: | ||
print('Usage: {0} <filename> <version>'.format(argv[0])) | ||
return 1 | ||
depends = DependencySet(filename=argv[1], version=argv[2]) | ||
if not depends: | ||
return 1 | ||
|
||
if depends.met: | ||
print('Met:') | ||
for dep in depends.met: | ||
print(' ' + dep_to_nevr(dep)) | ||
if depends.wrong_version: | ||
print() | ||
print('Wrong version:') | ||
for dep in depends.wrong_version: | ||
print(' ' + dep_to_rpmdep(dep)) | ||
if depends.unmet: | ||
print() | ||
print('Unmet:') | ||
for dep in depends.unmet: | ||
print(' ' + dep_to_rpmdep(dep)) | ||
return 0 | ||
|
||
|
||
if __name__ == '__main__': | ||
import sys | ||
exit(_main(sys.argv)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the nice doc string here!