[game-data-packager] 03/06: Turn inter-package relationships into an object

Simon McVittie smcv at debian.org
Mon Jan 25 01:14:28 UTC 2016


This is an automated email from the git hooks/post-receive script.

smcv pushed a commit to branch master
in repository game-data-packager.

commit dd581327e0d14e8ebd9d76cfc50cdd93b9da1471
Author: Simon McVittie <smcv at debian.org>
Date:   Mon Jan 25 00:16:29 2016 +0000

    Turn inter-package relationships into an object
---
 Makefile                                 |   2 +
 data/lgeneral.yaml                       |   2 +-
 data/morrowind.yaml                      |  10 +-
 data/quake2.yaml                         |   8 +-
 game_data_packager/__init__.py           | 166 ++++++++++++++++++++-----------
 game_data_packager/build.py              | 103 +++++++++++--------
 game_data_packager/data.py               |  64 ++++++++++++
 game_data_packager/packaging/__init__.py |  62 +++++++++++-
 game_data_packager/packaging/arch.py     |  19 ++++
 game_data_packager/packaging/deb.py      |  38 +++++++
 game_data_packager/packaging/rpm.py      |  19 ++++
 tests/deb.py                             |  68 +++++++++++++
 tests/rpm.py                             |  54 ++++++++++
 tools/check_syntax.py                    |   6 ++
 14 files changed, 507 insertions(+), 114 deletions(-)

diff --git a/Makefile b/Makefile
index 9effe78..55d1001 100644
--- a/Makefile
+++ b/Makefile
@@ -121,7 +121,9 @@ clean:
 
 check:
 	LC_ALL=C $(PYFLAKES3) game_data_packager/*.py game_data_packager/*/*.py runtime/*.py tools/*.py || :
+	LC_ALL=C GDP_UNINSTALLED=1 PYTHONPATH=. $(PYTHON) tests/deb.py
 	LC_ALL=C GDP_UNINSTALLED=1 PYTHONPATH=. $(PYTHON) tests/hashed_file.py
+	LC_ALL=C GDP_UNINSTALLED=1 PYTHONPATH=. $(PYTHON) tests/rpm.py
 	LC_ALL=C GDP_UNINSTALLED=1 PYTHONPATH=. $(PYTHON) tests/umod.py
 	LC_ALL=C GDP_UNINSTALLED=1 PYTHONPATH=. $(PYTHON) tools/check_syntax.py
 	LC_ALL=C GDP_UNINSTALLED=1 PYTHONPATH=. $(PYTHON) tools/check_equivalence.py
diff --git a/data/lgeneral.yaml b/data/lgeneral.yaml
index 244e8af..97d26d9 100644
--- a/data/lgeneral.yaml
+++ b/data/lgeneral.yaml
@@ -13,7 +13,7 @@ packages:
     longname: Panzer General data for LGeneral
     install_to: usr/share/games/lgeneral
     debian:
-      build-depends: lgc-pg
+      build_depends: lgc-pg
     # pg-data.tar.gz is not actually needed, but it's small, and putting it
     # in the .deb means we can easily repack it if lgc-pg changes
     #
diff --git a/data/morrowind.yaml b/data/morrowind.yaml
index c306a3d..2940047 100644
--- a/data/morrowind.yaml
+++ b/data/morrowind.yaml
@@ -42,7 +42,7 @@ packages:
       - morrowind-complete-en-data
     provides: morrowind-data
     lang: en
-    build-depends: [openmw-launcher]
+    build_depends: [openmw-launcher]
     install:
       - Morrowind assets from unshield
       - Morrowind English GOTY assets from unshield
@@ -58,7 +58,7 @@ packages:
       - morrowind-complete-en-data
     provides: morrowind-data
     lang: en
-    build-depends: [openmw-launcher]
+    build_depends: [openmw-launcher]
     install:
       - Morrowind assets from unshield
       - Morrowind English GOTY assets from unshield
@@ -77,7 +77,7 @@ packages:
       - morrowind-complete-en-data
     provides: morrowind-data
     lang: en
-    build-depends: [openmw-launcher]
+    build_depends: [openmw-launcher]
     install:
       - Morrowind assets from unshield
       - Morrowind English GOTY assets from unshield
@@ -93,7 +93,7 @@ packages:
   morrowind-complete-en-data:
     provides: morrowind-data
     lang: en
-    build-depends: [openmw-launcher]
+    build_depends: [openmw-launcher]
     install:
       - Morrowind assets from unshield
       - Morrowind English GOTY assets from unshield
@@ -117,7 +117,7 @@ packages:
   morrowind-fr-data:
     provides: morrowind-data
     lang: fr
-    build-depends: [openmw-launcher]
+    build_depends: [openmw-launcher]
     install:
       - Morrowind assets from unshield
       - Morrowind French assets from unshield
diff --git a/data/quake2.yaml b/data/quake2.yaml
index 67eb5b3..2b85d7e 100644
--- a/data/quake2.yaml
+++ b/data/quake2.yaml
@@ -111,11 +111,11 @@ packages:
       id: 2330
       path: "common/Quake 2/xatrix"
     debian:
-      build-depends: [gcc, make, libc6-dev]
+      build_depends: [gcc, make, libc6-dev]
       provides: quake2-xatrix
       replaces: quake2-xatrix
     fedora:
-      build-depends: [gcc, make, glibc-devel]
+      build_depends: [gcc, make, glibc-devel]
     expansion_for: quake2-full-data
     # this is what Makefile says has been tested
     architecture: amd64 i386 ia64 sparc64
@@ -167,11 +167,11 @@ packages:
     gog:
       url: quake_ii_quad_damage
     debian:
-      build-depends: [gcc, make, libc6-dev]
+      build_depends: [gcc, make, libc6-dev]
       provides: quake2-rogue
       replaces: quake2-rogue
     fedora:
-      build-depends: [gcc, make, glibc-devel]
+      build_depends: [gcc, make, glibc-devel]
     expansion_for: quake2-full-data
     # this is what Makefile says has been tested
     architecture: amd64 i386 ia64 sparc64
diff --git a/game_data_packager/__init__.py b/game_data_packager/__init__.py
index 321218e..f34618c 100644
--- a/game_data_packager/__init__.py
+++ b/game_data_packager/__init__.py
@@ -31,7 +31,7 @@ import zipfile
 import yaml
 
 from .build import (PackagingTask)
-from .data import (WantedFile)
+from .data import (PackageRelation, WantedFile)
 from .paths import (DATADIR, USE_VFS)
 from .util import ascii_safe
 from .version import (DISTRO, FORMAT, GAME_PACKAGE_VERSION)
@@ -53,18 +53,22 @@ class GameDataPackage(object):
         self.demo_for = set()
         self._better_versions = set()
         self.expansion_for = None
-        # use this to group together dubs
-        self.provides = None
         # use this for games with demo_for/better_version/provides
         self.mutually_exclusive = False
         # expansion for a package outside of this yaml file;
         # may be another GDP package or a package not made by GDP
         self.expansion_for_ext = None
 
-        # distro-agnostic depedencies inside the same .yaml file
-        # that can't be handled with expansion_for heuristics
-        # *) on Fedora this maps to 'Requires:'
-        self._depends = set()
+        self.relations = dict(
+            breaks=[],
+            build_depends=[],
+            conflicts=[],
+            depends=[],
+            provides=[],
+            recommends=[],
+            replaces=[],
+            suggests=[],
+        )
 
         # The optional marketing name of this version
         self.longname = None
@@ -188,16 +192,6 @@ class GameDataPackage(object):
             return None
 
     @property
-    def depends(self):
-        return self._depends
-    @depends.setter
-    def depends(self, value):
-        if type(value) is str:
-            self._depends = set([value])
-        else:
-            self._depends = set(value)
-
-    @property
     def optional(self):
         return self._optional
     @optional.setter
@@ -249,7 +243,6 @@ class GameDataPackage(object):
                 'aliases',
                 'better_versions',
                 'demo_for',
-                'depends',
                 'dotemu',
                 'gog',
                 'origin',
@@ -265,6 +258,14 @@ class GameDataPackage(object):
                 else:
                     ret[k] = v
 
+        for relation, related in self.relations.items():
+            # The .to_data() of a PackageRelation doesn't have a defined
+            # sorting order, so do a Schwartzian transform to get a
+            # stable order
+            tmp = sorted([(str(x), x.to_data()) for x in related])
+            tmp = [x[1] for x in tmp]
+            ret[relation] = tmp
+
         if expand and self.install_files is not None:
             if self.install_files:
                 ret['install'] = sorted(f.name for f in self.install_files)
@@ -660,7 +661,7 @@ class GameData(object):
 
     def _populate_package(self, package, d):
         for k in ('expansion_for', 'expansion_for_ext', 'longname', 'symlinks', 'install_to',
-                'description', 'depends',
+                'description',
                 'rip_cd', 'architecture', 'aliases', 'better_versions', 'langs', 'mutually_exclusive',
                 'copyright', 'engine', 'lang', 'component', 'section', 'disks', 'provides',
                 'steam', 'gog', 'dotemu', 'origin', 'url_misc', 'wiki', 'copyright_notice',
@@ -672,6 +673,34 @@ class GameData(object):
             assert 'better_versions' not in d
             package.better_versions = set([d['better_version']])
 
+        for rel in package.relations:
+            if rel in d:
+                related = d[rel]
+
+                if isinstance(related, (str, dict)):
+                    related = [related]
+                else:
+                    assert isinstance(related, list)
+
+                for x in related:
+                    pr = PackageRelation(x)
+                    # Fedora doesn't handle alternatives, everything must
+                    # be handled with virtual packages. Assume the same is
+                    # true for everything except dpkg.
+                    assert not pr.alternatives, pr
+
+                    if pr.contextual:
+                        for context, specific in pr.contextual.items():
+                            assert (context == 'deb' or
+                                    not specific.alternatives), pr
+
+                    if pr.package == 'libjpeg.so.62':
+                        # we can't really translate versions for libjpeg,
+                        # since it could be either libjpeg6b or libjpeg-turbo
+                        assert pr.version is None
+
+                    package.relations[rel].append(pr)
+
         for port in (
                 # packaging formats (we treat "debian" as "any dpkg-based"
                 # for historical reasons)
@@ -679,24 +708,37 @@ class GameData(object):
                 # specific distributions
                 'arch', 'fedora', 'mageia', 'suse',
                 ):
-            if port in d:
-                package.specifics[port] = d[port]
-
-            # FIXME: this object's contents should be 1:1 mapped from the
-            # YAML, and not format- or distribution-specific.
-            # Distribution-specific stuff should be done in the PackagingTask
-            # or PackagingSystem
-            if port in d and (FORMAT == port or DISTRO == port or
-                    (FORMAT == 'deb' and port == 'debian')):
-                for k in ('engine', 'install_to', 'description', 'provides'):
-                    if k in d[port]:
+
+            for k in d.get(port, {}):
+                if k in ('engine', 'install_to', 'description'):
+                    # FIXME: this object's contents should be 1:1 mapped
+                    # from the YAML, and not format- or distribution-specific.
+                    # Distribution-specific stuff should be done in the
+                    # PackagingTask or PackagingSystem
+                    if port in d and (FORMAT == port or DISTRO == port or
+                            (FORMAT == 'deb' and port == 'debian')):
                         setattr(package, k, d[port][k])
+                elif k in package.relations:
+                    related = d[port][k]
+
+                    if isinstance(related, str):
+                        related = [related]
+
+                    for r in related:
+                        if port == 'debian':
+                            # we treat "debian:" as meaning "any dpkg-based"
+                            pr = PackageRelation({'deb': r})
+                        else:
+                            pr = PackageRelation({port: r})
+                            assert not pr.alternatives, pr
 
-        # Fedora doesn't handle alternatives, everything must be handled with
-        # virtual packages
-        if FORMAT == 'rpm':
-            for dep in package.depends:
-                assert '|' not in dep, (package.name, package.depends)
+                        if pr.package == 'libjpeg.so.62':
+                            assert pr.version is None
+
+                        package.relations[k].append(pr)
+                else:
+                    raise AssertionError('%s: unknown key %r in port %r' %
+                            (package.name, k, port))
 
         assert self.copyright or package.copyright, package.name
         assert package.component in ('main', 'contrib', 'non-free', 'local')
@@ -705,24 +747,19 @@ class GameData(object):
         assert type(package.langs) is list
         assert type(package.mutually_exclusive) is bool
 
-        if 'debian' in d:
-            debian = d['debian']
-            assert type(debian) is dict
-            for k, v in debian.items():
-                assert k in ('breaks', 'conflicts', 'depends', 'provides',
-                             'recommends', 'replaces', 'suggests',
-                             'build-depends'), (package.name, debian)
-                assert type(v) in (str, list), (package.name, debian)
-                if type(v) == str:
-                    assert ',' not in v, (package.name, debian)
-                    package.specifics['debian'][k] = [v]
-                assert package.name not in v, \
-                   "A package shouldn't extraneously %s itself" % k
-
-        if 'provides' in d:
-            assert type(package.provides) is str
-            assert package.name != package.provides, \
-               "A package shouldn't extraneously provide itself"
+        for rel, related in package.relations.items():
+            for pr in related:
+                packages = set()
+                if pr.contextual:
+                    for p in pr.contextual.values():
+                        packages.add(p.package)
+                elif pr.alternatives:
+                    for p in pr.alternatives:
+                        packages.add(p.package)
+                else:
+                    packages.add(pr.package)
+                assert package.name not in packages, \
+                   "%s should not be in its own %s set" % (package.name, rel)
 
         if 'install_to' in d:
             assert '$assets/' + package.name != d['install_to'] + '-data', \
@@ -1051,13 +1088,30 @@ class GameData(object):
             # check internal depedencies
             for demo_for_item in package.demo_for:
                 assert demo_for_item in self.packages, demo_for_item
+
             if package.expansion_for:
                 if package.expansion_for not in self.packages:
-                    for p in self.packages.values():
-                        if package.expansion_for == p.provides:
+                    # It needs to be provided on all distributions,
+                    # so we can ignore contextual package relations,
+                    # which have package = None.
+                    #
+                    # We also already asserted that distro-independent
+                    # relations don't have alternatives (not that they
+                    # would be meaningful for provides).
+                    provider = None
+
+                    for other in self.packages.values():
+                        for provided in other.relations['provides']:
+                            if package.expansion_for == provided.package:
+                                provider = other
+                                break
+
+                        if provider is not None:
                             break
                     else:
-                        raise Exception('virtual pkg %s not found' % package.expansion_for)
+                        raise Exception('%s: %s: virtual package %s not found' %
+                                (self.shortname, package.name,
+                                    package.expansion_for))
 
             if package.better_versions:
                 for v in package.better_versions:
diff --git a/game_data_packager/build.py b/game_data_packager/build.py
index 4c1d547..63ec622 100644
--- a/game_data_packager/build.py
+++ b/game_data_packager/build.py
@@ -1320,21 +1320,8 @@ class PackagingTask(object):
                  '--options=!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link']
                  + sorted(files), env={'LANG':'C'}, cwd=destdir)
 
-    def __merge_relationships(self, package, related, key):
-        # copy it so we don't modify it in-place
-        related = set(related)
-
-        if FORMAT in package.specifics:
-            related |= set(package.specifics[FORMAT].get(key, ()))
-
-        if FORMAT == 'deb' and 'debian' in package.specifics:
-            # we treat "debian" as "any dpkg-based" for historical reasons
-            related |= set(package.specifics['debian'].get(key, ()))
-
-        if DISTRO in package.specifics:
-            related |= set(package.specifics[DISTRO].get(key, ()))
-
-        return related
+    def __merge_relations(self, package, rel):
+        return set(self.packaging.format_relations(package.relations[rel]))
 
     def fill_dest_dir_rpm(self, package, destdir, compress, architecture, release):
         specfile = os.path.join(self.get_workdir(), '%s.spec' % package.name)
@@ -1400,19 +1387,39 @@ class PackagingTask(object):
             else:
                 spec.write('Group: Amusements/Games\n')
             spec.write('BuildArch: %s\n' % architecture)
-            if package.provides:
-                spec.write('Provides: %s\n' % package.provides)
+
+            for p in self.__merge_relations(package, 'provides'):
+                spec.write('Provides: %s\n' % p)
+
                 if package.mutually_exclusive:
-                    spec.write('Conflicts: %s\n' % package.provides)
+                    spec.write('Conflicts: %s\n' % p)
+
             if package.expansion_for:
                 spec.write('Requires: %s\n' % package.expansion_for)
             else:
                 engine = package.engine or self.game.engine
+
                 if engine and len(engine.split()) == 1:
                     spec.write('Requires: %s\n' % engine)
-            for p in self.__merge_relationships(package, package.depends,
-                    'depends'):
+
+            for p in self.__merge_relations(package, 'depends'):
                 spec.write('Requires: %s\n' % p)
+
+            for p in (self.__merge_relations(package, 'conflicts'),
+                    self.__merge_relations(package, 'breaks')):
+                spec.write('Conflicts: %s\n' % p)
+
+            for p in self.__merge_relations(package, 'recommends'):
+                # FIXME: some RPM distributions do have recommends;
+                # which ones?
+                pass
+
+            for p in self.__merge_relations(package, 'suggests'):
+                # FIXME: likewise
+                pass
+
+            # FIXME: replaces?
+
             if not compress or not self.compress_deb or package.rip_cd:
                 spec.write('%define _binary_payload w0.gzdio\n')
             elif self.compress_deb == ['-Zgzip', '-z1']:
@@ -1643,22 +1650,20 @@ class PackagingTask(object):
             control['Architecture'] = self.packaging.get_architecture(package.architecture)
 
         dep = dict()
-        debian = package.specifics.get('debian', {})
-        for field in ('breaks', 'conflicts', 'provides',
-                      'recommends', 'replaces', 'suggests'):
-            dep[field] = set(debian.get(field,[]))
 
-        dep['depends'] = self.__merge_relationships(package, package.depends,
-                'depends')
+        for rel in package.relations:
+            if rel == 'build_depends':
+                continue
+
+            dep[rel] = self.__merge_relations(package, rel)
+            logger.debug('%s %s %s', package.name, rel, ', '.join(dep[rel]))
 
         if package.mutually_exclusive:
             dep['conflicts'] |= package.demo_for
             dep['conflicts'] |= package.better_versions
 
-        if package.provides:
-            dep['provides'].add(package.provides)
-            if package.mutually_exclusive:
-                dep['replaces'].add(package.provides)
+        if package.mutually_exclusive:
+            dep['replaces'] |= dep['provides']
 
         engine = package.engine or self.game.engine
         if engine and '>=' in engine:
@@ -1686,9 +1691,13 @@ class PackagingTask(object):
 
         # dependencies derived from *other* package's data
         for other_package in self.game.packages.values():
-            if (other_package.expansion_for and
-             other_package.expansion_for in (package.name, package.provides)):
+            if other_package.expansion_for:
+                if package.name == other_package.expansion_for:
                     dep['suggests'].add(other_package.name)
+                else:
+                    for p in package.relations['provides']:
+                        if p.package == other_package.expansion_for:
+                            dep['suggests'].add(other_package.name)
 
             if other_package.mutually_exclusive:
                 if package.name in other_package.better_versions:
@@ -2181,15 +2190,15 @@ class PackagingTask(object):
                     possible.discard(package)
 
         for package in set(possible):
-            debian = package.specifics.get('debian', {})
-            if 'build-depends' in debian:
-                for tool in debian['build-depends']:
-                    tool = tool.strip()
-                    if not which(tool) and not self.packaging.is_installed(tool):
-                        logger.error('package "%s" is needed to build "%s"' %
-                                     (tool, package.name))
-                        possible.discard(package)
-                        self.missing_tools.add(tool)
+            build_depends = self.__merge_relations(package, 'build_depends')
+            for tool in build_depends:
+                tool = tool.strip()
+
+                if not which(tool) and not self.packaging.is_installed(tool):
+                    logger.error('package "%s" is needed to build "%s"' %
+                                 (tool, package.name))
+                    possible.discard(package)
+                    self.missing_tools.add(tool)
 
         logger.debug('possible packages: %r', set(p.name for p in possible))
         if not possible:
@@ -2216,12 +2225,18 @@ class PackagingTask(object):
                                 package.name, package.lang)
                     possible.discard(package)
                     continue
+
                 # keep only preferred language for this virtual package
-                if package.provides:
+                provides = self.__merge_relations(package, 'provides')
+
+                if provides:
                     for other_p in possible:
                         if other_p.name == package.name:
                             continue
-                        if other_p.provides != package.provides:
+                        other_provides = self.__merge_relations(other_p,
+                                'provides')
+                        if other_provides - provides:
+                            # it provides something this one doesn't
                             continue
                         if score < lang_score(other_p.lang):
                             logger.info('will not produce "%s" '
@@ -2668,7 +2683,7 @@ class PackagingTask(object):
         packages = set()
 
         for t in self.missing_tools:
-            p = self.packaging.PACKAGE_MAP.get(t, t)
+            p = self.packaging.package_for_tool(t)
             if p is not None:
                 packages.add(p)
 
diff --git a/game_data_packager/data.py b/game_data_packager/data.py
index b1de792..12fba35 100644
--- a/game_data_packager/data.py
+++ b/game_data_packager/data.py
@@ -295,3 +295,67 @@ class WantedFile(HashedFile):
                     ret[k] = v
 
         return ret
+
+class PackageRelation:
+    def __init__(self, rel):
+        assert isinstance(rel, str) or isinstance(rel, dict)
+        assert ',' not in rel
+
+        self.package = None
+        self.version = None
+        self.version_operator = None
+        self.alternatives = []
+        self.contextual = {}
+
+        if isinstance(rel, dict):
+            for context, specific in rel.items():
+                assert isinstance(context, str), context
+                assert isinstance(specific, str), specific
+                self.contextual[context] = PackageRelation(specific)
+        elif '|' in rel:
+            self.alternatives = [PackageRelation(bit.strip())
+                    for bit in rel.split('|')]
+        else:
+            for operator in '>=', '>>', '<=', '<<', '=':
+                if operator in rel:
+                    package, version = rel.split(operator)
+                    package = package.rstrip('(')
+                    self.package = package.strip()
+                    version = version.rstrip(')')
+                    self.version = version.strip()
+                    self.version_operator = operator
+                    break
+            else:
+                self.package = rel
+
+                assert self.package.strip() == self.package, repr(self.package)
+
+    def to_data(self):
+        if self.contextual:
+            data = {}
+
+            for context, specific in self.contextual.items():
+                data[context] = specific.to_data()
+
+            return data
+
+        if self.alternatives:
+            return ' | '.join([alt.to_data() for alt in self.alternatives])
+
+        return str(self)
+
+    def __str__(self):
+        if self.contextual:
+            return repr(self)
+
+        if self.alternatives:
+            return ' | '.join([str(s) for s in self.alternatives])
+
+        if self.version is None:
+            return self.package
+
+        return '%s (%s %s)' % (self.package, self.version_operator,
+                self.version)
+
+    def __repr__(self):
+        return 'PackageRelation(' + repr(self.to_data()) + ')'
diff --git a/game_data_packager/packaging/__init__.py b/game_data_packager/packaging/__init__.py
index c474d74..eb16b49 100644
--- a/game_data_packager/packaging/__init__.py
+++ b/game_data_packager/packaging/__init__.py
@@ -27,10 +27,23 @@ class PackagingSystem(metaclass=ABCMeta):
     LICENSEDIR = 'usr/share/doc'
     CHECK_CMD = None
     INSTALL_CMD = None
-    # by default pgm 'unzip' is provided by package 'unzip' etc...
-    # only exceptions needs to be listed
-    # 'None' means that this pgm is not packaged by $distro
-    PACKAGE_MAP = dict()
+
+    # Exceptions to our normal heuristic for mapping a tool to a package:
+    # the executable tool 'unzip' is in the unzip package, etc.
+    #
+    # Only exceptions need to be listed.
+    #
+    # 'NotImplemented' means that this dependency is not packaged by
+    # the distro.
+    PACKAGE_MAP = {}
+
+    # Exceptions to our normal heuristic for mapping an abstract package name
+    # to a package:
+    #
+    # - the library 'libfoo.so.0' is in a package that Provides libfoo.so.0
+    #   (suitable for RPM)
+    # - anything else is in the obvious package name
+    RENAME_PACKAGES = {}
 
     # we keep Debian codification as reference, as it
     # - has the most architectures supported
@@ -41,6 +54,9 @@ class PackagingSystem(metaclass=ABCMeta):
     def __init__(self):
         self._architecture = None
         self._foreign_architectures = set()
+        # contexts to use when evaluating format- or distro-specific
+        # dependencies, in order by preference
+        self._contexts = ('generic',)
 
     def read_architecture(self):
         arch = os.uname()[4]
@@ -119,6 +135,44 @@ class PackagingSystem(metaclass=ABCMeta):
     def override_lintian(self, destdir, package, tag, args):
         pass
 
+    def format_relations(self, relations):
+        """Yield a native dependency representation for this packaging system
+        for each gdp.data.PackagingRelation in relations.
+        """
+        for pr in relations:
+            if pr.contextual:
+                for c in self._contexts:
+                    if c in pr.contextual:
+                        for x in self.format_relations([pr.contextual[c]]):
+                            yield x
+
+                        break
+            else:
+                yield self.format_relation(pr)
+
+    @abstractmethod
+    def format_relation(self, pr):
+        """Return a native dependency representation for this packaging system
+        and the given gdp.data.PackagingRelation. It is guaranteed
+        that pr.contextual is empty.
+        """
+        raise NotImplementedError
+
+    def rename_package(self, dependency):
+        """Given an abstract package name, return the corresponding
+        package name in this packaging system.
+
+        Abstract package names are mostly the same as for Debian,
+        except that libraries are represented as libfoo.so.0.
+        """
+        return self.RENAME_PACKAGES.get(dependency, dependency)
+
+    def package_for_tool(self, tool):
+        """Given an executable name, return the corresponding
+        package name in this packaging system.
+        """
+        return self.PACKAGE_MAP.get(tool, tool)
+
 def get_native_packaging_system():
     # lazy import when actually needed
     from ..version import (FORMAT)
diff --git a/game_data_packager/packaging/arch.py b/game_data_packager/packaging/arch.py
index 48f6d2b..3b85e74 100644
--- a/game_data_packager/packaging/arch.py
+++ b/game_data_packager/packaging/arch.py
@@ -40,6 +40,10 @@ class ArchPackaging(PackagingSystem):
                   'i386': 'i686',
                   }
 
+    def __init__(self):
+        super(ArchPackaging, self).__init__()
+        self._contexts = ('arch', 'generic')
+
     def read_architecture(self):
         super(ArchPackaging, self).read_architecture()
         # https://wiki.archlinux.org/index.php/Multilib
@@ -80,5 +84,20 @@ class ArchPackaging(PackagingSystem):
     def install_packages(self, pkgs, method=None, gain_root='su'):
         run_as_root(['pacman', '-U'] + list(pkgs), gain_root)
 
+    def format_relation(self, pr):
+        assert not pr.contextual
+        assert not pr.alternatives
+
+        if pr.version is not None:
+            op = pr.version_operator
+
+            if op in ('<<', '>>'):
+                op = op[0]
+
+            # foo>=1.0
+            return '%s%s%s' % (self.rename_package(pr.package), op, pr.version)
+
+        return self.rename_package(pr.package)
+
 def get_distro_packaging():
     return ArchPackaging()
diff --git a/game_data_packager/packaging/deb.py b/game_data_packager/packaging/deb.py
index a9f1cf1..ef4650b 100644
--- a/game_data_packager/packaging/deb.py
+++ b/game_data_packager/packaging/deb.py
@@ -38,11 +38,17 @@ class DebPackaging(PackagingSystem):
                   '7z': 'p7zip-full',
                   'unrar-nonfree': 'unrar',
                   }
+    RENAME_PACKAGES = {
+            'libSDL-1.2.so.0': 'libsdl1.2debian',
+            'libgcc_s.so.1': 'libgcc1',
+            'libjpeg.so.62': 'libjpeg62-turbo | libjpeg62',
+    }
 
     def __init__(self):
         super(DebPackaging, self).__init__()
         self.__installed = None
         self.__available = None
+        self._contexts = ('deb', 'generic')
 
     def read_architecture(self):
         self._architecture = check_output(['dpkg',
@@ -138,6 +144,24 @@ class DebPackaging(PackagingSystem):
             # gdebi-gtk etc.
             subprocess.call([method] + list(debs))
 
+    def rename_package(self, p):
+        mapped = super(DebPackaging, self).rename_package(p)
+
+        if mapped != p:
+            return mapped
+
+        p = p.lower().replace('_', '-')
+
+        if '.so.' in p:
+            lib, version = p.split('.so.', 1)
+
+            if lib[-1] in '012345679':
+                lib += '-'
+
+            return lib + version
+
+        return p
+
     def override_lintian(self, destdir, package, tag, args):
         assert type(package) is str
         lintiandir = os.path.join(destdir, 'usr/share/lintian/overrides')
@@ -145,5 +169,19 @@ class DebPackaging(PackagingSystem):
         with open(os.path.join(lintiandir, package), 'a', encoding='utf-8') as l:
             l.write('%s: %s %s\n' % (package, tag, args))
 
+    def format_relation(self, pr):
+        assert not pr.contextual
+
+        if pr.alternatives:
+            return ' | '.join([self.format_relation(p)
+                for p in pr.alternatives])
+
+        if pr.version is not None:
+            # foo (>= 1.0)
+            return '%s (%s %s)' % (self.rename_package(pr.package),
+                    pr.version_operator, pr.version)
+
+        return self.rename_package(pr.package)
+
 def get_distro_packaging():
     return DebPackaging()
diff --git a/game_data_packager/packaging/rpm.py b/game_data_packager/packaging/rpm.py
index 77dfb4a..1f056f9 100644
--- a/game_data_packager/packaging/rpm.py
+++ b/game_data_packager/packaging/rpm.py
@@ -34,6 +34,10 @@ class RpmPackaging(PackagingSystem):
                   'amd64': 'x86_64',
                   }
 
+    def __init__(self, distro):
+        super(RpmPackaging, self).__init__()
+        self._contexts = (distro, 'rpm', 'generic')
+
     def is_installed(self, package):
         return 0 == subprocess.call(['rpm', '-q', package],
                                     stdout=subprocess.DEVNULL,
@@ -72,6 +76,21 @@ class RpmPackaging(PackagingSystem):
                                 ' using rpm instead') % method)
             run_as_root(['rpm', '-U'] + list(rpms), gain_root)
 
+    def format_relation(self, pr):
+        assert not pr.contextual
+        assert not pr.alternatives
+
+        if pr.version is not None:
+            op = pr.version_operator
+
+            if op in ('<<', '>>'):
+                op = op[0]
+
+            # foo >= 1.0
+            return '%s %s %s' % (self.rename_package(pr.package), op,
+                    pr.version)
+
+        return self.rename_package(pr.package)
 
 # XXX: dnf is written in python3 and has a stable public api,
 #      it is likely faster to use it instead of calling 'dnf' pgm.
diff --git a/tests/deb.py b/tests/deb.py
new file mode 100644
index 0000000..d9fe984
--- /dev/null
+++ b/tests/deb.py
@@ -0,0 +1,68 @@
+#!/usr/bin/python3
+# encoding=utf-8
+#
+# Copyright © 2016 Simon McVittie <smcv at debian.org>
+#
+# 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; either version 2
+# of the License, or (at your option) any later version.
+#
+# 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.
+#
+# You can find the GPL license text on a Debian system under
+# /usr/share/common-licenses/GPL-2.
+
+import unittest
+
+from game_data_packager.data import (PackageRelation)
+from game_data_packager.packaging.deb import (DebPackaging)
+
+class DebTestCase(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def test_rename_package(self):
+        dp = DebPackaging()
+
+        def t(in_, out):
+            self.assertEqual(dp.rename_package(in_), out)
+
+        # generic
+        t('libc.so.6', 'libc6')
+        t('libalut.so.0', 'libalut0')
+        t('libXxf86dga.so.1', 'libxxf86dga1')
+        t('libopenal.so.1', 'libopenal1')
+        t('libstdc++.so.6', 'libstdc++6')
+        t('libdbus-1.so.3', 'libdbus-1-3')
+
+        # special cases
+        t('libSDL-1.2.so.0', 'libsdl1.2debian')
+        t('libgcc_s.so.1', 'libgcc1')
+        t('libjpeg.so.62', 'libjpeg62-turbo | libjpeg62')
+
+    def test_relation(self):
+        dp = DebPackaging()
+
+        def t(in_, out):
+            self.assertEqual(
+                    sorted(dp.format_relations(map(PackageRelation, in_))),
+                    out)
+
+        t(['libc.so.6'], ['libc6'])
+        t(['libc.so.6 (>= 2.19)'], ['libc6 (>= 2.19)'])
+        t(['libjpeg.so.62'], ['libjpeg62-turbo | libjpeg62'])
+        t(['libopenal.so.1 | bundled-openal'], ['libopenal1 | bundled-openal'])
+        t(['libc.so.6', 'libopenal.so.1'], ['libc6', 'libopenal1'])
+        t([dict(deb='foo', rpm='bar')], ['foo'])
+        t([dict(deb='foo', rpm='bar', generic='baz')], ['foo'])
+        t([dict(rpm='bar', generic='baz')], ['baz'])
+        t([dict(rpm='bar')], [])
+
+    def tearDown(self):
+        pass
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/tests/rpm.py b/tests/rpm.py
new file mode 100644
index 0000000..c3db71b
--- /dev/null
+++ b/tests/rpm.py
@@ -0,0 +1,54 @@
+#!/usr/bin/python3
+# encoding=utf-8
+#
+# Copyright © 2016 Simon McVittie <smcv at debian.org>
+#
+# 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; either version 2
+# of the License, or (at your option) any later version.
+#
+# 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.
+#
+# You can find the GPL license text on a Debian system under
+# /usr/share/common-licenses/GPL-2.
+
+import unittest
+
+from game_data_packager.data import (PackageRelation)
+from game_data_packager.packaging.rpm import (RpmPackaging)
+
+class DebTestCase(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def test_relation(self):
+        # a random distribution that we don't actually support
+        rp = RpmPackaging('caldera')
+
+        def t(in_, out):
+            self.assertEqual(
+                    sorted(rp.format_relations(map(PackageRelation, in_))),
+                    out)
+
+        t(['libc.so.6'], ['libc.so.6'])
+        t(['libc.so.6 (>= 2.19)'], ['libc.so.6 >= 2.19'])
+        t(['libjpeg.so.62'], ['libjpeg.so.62'])
+        t(['libc.so.6', 'libopenal.so.1'], ['libc.so.6', 'libopenal.so.1'])
+        t([dict(deb='foo', rpm='bar')], ['bar'])
+        t([dict(deb='foo', rpm='bar', generic='baz')], ['bar'])
+        t([dict(deb='foo', generic='baz')], ['baz'])
+        t([dict(deb='foo')], [])
+        t([dict(rpm='foo', caldera='bar', fedora='baz')], ['bar'])
+
+        with self.assertRaises(AssertionError):
+            rp.format_relation(
+                    PackageRelation('libopenal.so.1 | bundled-openal'))
+
+    def tearDown(self):
+        pass
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/tools/check_syntax.py b/tools/check_syntax.py
index 0148a24..6c8e230 100755
--- a/tools/check_syntax.py
+++ b/tools/check_syntax.py
@@ -15,12 +15,18 @@
 # You can find the GPL license text on a Debian system under
 # /usr/share/common-licenses/GPL-2.
 
+import logging
 import os
 import yaml
 
 from game_data_packager import load_games
 from game_data_packager.util import ascii_safe
 
+if os.environ.get('DEBUG') or os.environ.get('GDP_DEBUG'):
+    logging.getLogger().setLevel(logging.DEBUG)
+else:
+    logging.getLogger().setLevel(logging.INFO)
+
 if __name__ == '__main__':
     for name, game in load_games().items():
         game.load_file_data()

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-games/game-data-packager.git



More information about the Pkg-games-commits mailing list