This is the mail archive of the cygwin-apps-cvs mailing list for the cygwin-apps project.


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]
Other format: [Raw text]

[calm - Cygwin server-side packaging maintenance script] branch master, updated. 20160705-11-g45ccc3a




https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=45ccc3afdc8cf4bb5fdd441f8663777fce1acc81

commit 45ccc3afdc8cf4bb5fdd441f8663777fce1acc81
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Fri Jul 29 17:46:21 2016 +0100

    Handle the case where there is no current version
    
    Use the information from the 'best' version
    
    (Previously we would have been using the same information from setup.hint
    for all versions, so the precise version chosen perhaps doesn't make a big
    difference)

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=d82a4132bdb25acb86b4417e68891f693f402593

commit d82a4132bdb25acb86b4417e68891f693f402593
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Tue Jun 21 20:15:04 2016 +0100

    Test for upload with per-version hints

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=96c28886048f46040c39254b9fea1497c7e9192e

commit 96c28886048f46040c39254b9fea1497c7e9192e
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Thu Jun 16 19:09:50 2016 +0000

    Tests for packages with per-version hints
    
    Tests for packages with per-version and override hints

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=39136ce032bc74a3ea7ee477d292e468226a3081

commit 39136ce032bc74a3ea7ee477d292e468226a3081
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Fri Jul 29 16:41:23 2016 +0100

    Update processing for per-version hints
    
    Update processing for the hints being stored for each package version,
    rather than each package.
    
    'skip' becomes a package flag, set if any version has it.  (It doesn't seem
    terribly useful to have this in the hint file, since it's implied by the
    absence of any install tarfiles.)
    
    When writing setup.ini
    - sdesc:, ldesc:, category: and message: are taken from the curr version
    - requires: is the union of all versions
    - source: is controlled by external-source: per-version

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=eda55aeb78ea06d47a472b1ec08677e366e11f63

commit eda55aeb78ea06d47a472b1ec08677e366e11f63
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Thu Jun 23 15:40:48 2016 +0100

    Read either setup.hint or pvr.hint
    
    For every package version, either setup.hint or pvr.hint must exist
    Read version overrides from override.hint or setup.hint
    Factor out reading hints and checking for errors in read_package

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=a62d4aa6ea4fe34492e16683535c7d844b213847

commit a62d4aa6ea4fe34492e16683535c7d844b213847
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Thu Jun 23 15:26:12 2016 +0100

    Generalize setup.hint reading to .hint file reading


Diff:
---
 calm/hint.py                                       |   83 +++++--
 calm/mksetupini.py                                 |    2 +-
 calm/package.py                                    |  292 +++++++++++++-------
 calm/pkg2html.py                                   |   10 +-
 calm/uploads.py                                    |    5 +-
 test/test_calm.py                                  |    5 +-
 .../per-version/per-version-5.0-1-src.tar.xz       |  Bin 0 -> 228 bytes
 .../x86/release/per-version/per-version-5.0-1.hint |    4 +
 .../release/per-version/per-version-5.0-1.tar.xz   |  Bin 0 -> 228 bytes
 test/testdata/htdocs.expected/x86/packages.inc     |    1 +
 .../htdocs.expected/x86/per-version/.htaccess      |    3 +
 .../x86/per-version/per-version-4.0-1              |    7 +
 .../x86/per-version/per-version-4.0-1-src          |    7 +
 .../x86/per-version/per-version-4.8-1              |    7 +
 .../x86/per-version/per-version-4.8-1-src          |    7 +
 test/testdata/inifile/setup.ini.expected           |   17 ++
 test/testdata/pkglist/cygwin-pkg-maint             |    2 +
 test/testdata/pkglist/expected                     |    2 +-
 test/testdata/process_arch/homedir.expected        |    1 +
 test/testdata/process_arch/htdocs.expected         |    7 +
 test/testdata/process_arch/rel_area.expected       |   18 ++
 .../per-version-incomplete-36-1-src.tar.xz         |  Bin 0 -> 228 bytes
 .../per-version-incomplete-36-1.hint               |    3 +
 .../per-version-incomplete-36-1.tar.xz             |  Bin 0 -> 228 bytes
 .../per-version-incomplete-39-1-src.tar.xz         |  Bin 0 -> 228 bytes
 .../per-version-incomplete-39-1.tar.xz             |  Bin 0 -> 228 bytes
 .../relarea/x86/release/per-version/override.hint  |    2 +
 .../per-version/per-version-4.0-1-src.tar.xz       |  Bin 0 -> 228 bytes
 .../x86/release/per-version/per-version-4.0-1.hint |    4 +
 .../release/per-version/per-version-4.0-1.tar.xz   |  Bin 0 -> 228 bytes
 .../per-version/per-version-4.8-1-src.tar.xz       |  Bin 0 -> 228 bytes
 .../x86/release/per-version/per-version-4.8-1.hint |    4 +
 .../release/per-version/per-version-4.8-1.tar.xz   |  Bin 0 -> 228 bytes
 test/testdata/uploads/pkglist.expected             |   28 +-
 34 files changed, 379 insertions(+), 142 deletions(-)

diff --git a/calm/hint.py b/calm/hint.py
index 2579fad..68abbd7 100755
--- a/calm/hint.py
+++ b/calm/hint.py
@@ -22,23 +22,61 @@
 #
 
 #
-# parser for setup.hint files
+# parser for .hint files
 #
 
 from collections import OrderedDict
 import re
 import argparse
 
-# keys which always have a value which may be multiline
-multilinevalkeys = ['ldesc', 'message']
-# keys which always have a value
-valkeys = ['curr', 'prev', 'test', 'category', 'external-source', 'sdesc']
-# keys which may have an empty value
-optvalkeys = ['requires']
-# keys which must have an empty value
-novalkeys = ['skip']
 
-hintkeys = multilinevalkeys + valkeys + optvalkeys + novalkeys
+# helper function to merge dicts
+def merge_dicts(x, *y):
+    z = x.copy()
+    for i in y:
+        z.update(i)
+    return z
+
+# types of key:
+# 'multilineval' - always have a value, which may be multiline
+# 'val'          - always have a value
+# 'optval'       - may have an empty value
+# 'noval'        - always have an empty value
+keytypes = ['multilineval', 'val', 'optval', 'noval']
+
+# kinds of hint file, and their allowed keys
+setup, pvr, override = range(3)
+
+commonkeys = {
+    'ldesc': 'multilineval',
+    'message': 'multilineval',
+    'category': 'val',
+    'external-source': 'val',
+    'sdesc': 'val',
+    'skip': 'noval',
+}
+
+versionkeys = {
+    'curr': 'val',
+    'prev': 'val',
+    'test': 'val',
+}
+
+hintkeys = {}
+
+hintkeys[setup] = merge_dicts(commonkeys, versionkeys, {
+    'requires': 'optval',
+})
+
+hintkeys[pvr] = merge_dicts(commonkeys, {
+    'requires': 'optval',
+    # putative syntax for not yet implemented per-version dependencies
+    # (depends could be an alias for requires in this kind of hint file)
+    'depends': 'optval',
+    'build-depends': 'optval',
+})
+
+hintkeys[override] = versionkeys
 
 # valid categories
 categories = ['accessibility',
@@ -145,15 +183,18 @@ def item_lexer(c):
         yield (i, o, None)
 
 
-def setup_hint_parse(fn):
+# parse the file |fn| as a .hint file of kind |kind|
+def hint_file_parse(fn, kind):
     hints = OrderedDict()
     errors = []
     warnings = []
 
+    assert(kind in hintkeys)
+
     with open(fn, 'rb') as f:
         c = f.read()
 
-        # validate that setup.hint is UTF-8 encoded
+        # validate that .hint file is UTF-8 encoded
         try:
             c = c.decode('utf-8')
 
@@ -172,19 +213,20 @@ def setup_hint_parse(fn):
                     key = match.group(1)
                     value = match.group(2)
 
-                    if key not in hintkeys:
+                    if key not in hintkeys[kind]:
                         errors.append('unknown setup key %s at line %d' % (key, i))
                         continue
+                    type = hintkeys[kind][key]
 
                     # check if the key occurs more than once
                     if key in hints:
                         errors.append('duplicate key %s' % (key))
 
                     # check the value meets any key-specific constraints
-                    if (key in valkeys) and (len(value) == 0):
+                    if (type == 'val') and (len(value) == 0):
                         errors.append('%s has empty value' % (key))
 
-                    if (key in novalkeys) and (len(value) != 0):
+                    if (type == 'noval') and (len(value) != 0):
                         errors.append("%s has non-empty value '%s'" % (key, value))
 
                     # validate all categories are in the category list (case-insensitively)
@@ -211,7 +253,7 @@ def setup_hint_parse(fn):
                             warnings.append("sdesc contains '  '")
 
                     # only 'ldesc' and 'message' are allowed a multi-line value
-                    if (key not in multilinevalkeys) and (len(value.splitlines()) > 1):
+                    if (type != 'multilineval') and (len(value.splitlines()) > 1):
                         errors.append("key %s has multi-line value" % (key))
 
                     # message must have an id and some text
@@ -228,9 +270,10 @@ def setup_hint_parse(fn):
                 else:
                     errors.append("unknown setup construct '%s' at line %d" % (item, i))
 
-            # if 'skip' isn't present, 'category' and 'sdesc' must be
+            # for setup and pvr kinds, if 'skip' isn't present, 'category' and
+            # 'sdesc' must be
             # XXX: genini also requires 'requires' but that seems wrong
-            if 'skip' not in hints:
+            if 'skip' not in hints and kind != override:
                 mandatory = ['category', 'sdesc']
                 for k in mandatory:
                     if k not in hints:
@@ -269,7 +312,7 @@ def main(args):
     status = 0
 
     for fn in args.files:
-        hints = setup_hint_parse(fn)
+        hints = hint_file_parse(fn, setup)
 
         if args.verbose > 1:
             print(hints)
@@ -292,7 +335,7 @@ def main(args):
 #
 
 if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description='setup.hint validator')
+    parser = argparse.ArgumentParser(description='.hint file validator')
     parser.add_argument('files', nargs='*', metavar='filename', help='list of files')
     parser.add_argument('-v', '--verbose', action='count', dest='verbose', help='verbose output', default=0)
 
diff --git a/calm/mksetupini.py b/calm/mksetupini.py
index 476b56d..863a14b 100755
--- a/calm/mksetupini.py
+++ b/calm/mksetupini.py
@@ -24,7 +24,7 @@
 #
 # mksetupini
 #
-# Make a setup.ini file from a collection of tarfiles and setup.hints
+# Make a setup.ini file from a collection of tarfiles and .hint files
 # (this is intended to be a replacement for genini)
 #
 
diff --git a/calm/package.py b/calm/package.py
index 4cd7065..6dbceb4 100755
--- a/calm/package.py
+++ b/calm/package.py
@@ -49,12 +49,18 @@ class Package(object):
     def __init__(self):
         self.path = ''  # path to package, relative to release area
         self.tars = {}
-        self.hints = {}
         self.is_used_by = set()
+        self.version_hints = {}
+        self.override_hints = {}
+        self.skip = False
 
     def __repr__(self):
-        return "Package('%s', %s, %s)" % (self.path, pprint.pformat(self.tars),
-                                          pprint.pformat(self.hints))
+        return "Package('%s', %s, %s, %s, %s)" % (
+            self.path,
+            pprint.pformat(self.tars),
+            pprint.pformat(self.version_hints),
+            pprint.pformat(self.override_hints),
+            self.skip)
 
 
 # information we keep about a tar file
@@ -100,6 +106,46 @@ def sha512_file(fn, block_size=256*128):
     return sha512.hexdigest()
 
 
+# helper function to read hints
+def read_hints(p, fn, kind):
+    hints = hint.hint_file_parse(fn, kind)
+
+    if 'parse-errors' in hints:
+        for l in hints['parse-errors']:
+            logging.error("package '%s': %s" % (p, l))
+        logging.error("errors while parsing hints for package '%s'" % p)
+        return None
+
+    if 'parse-warnings' in hints:
+        for l in hints['parse-warnings']:
+            logging.info("package '%s': %s" % (p, l))
+
+    return hints
+
+
+# helper function to clean up hints
+def clean_hints(p, hints, strict_lvl, warnings):
+    #
+    # fix some common defects in the hints
+    #
+
+    # don't allow a redundant 'package:' or 'package - ' at start of sdesc
+    #
+    # match case-insensitively, and use a base package name (trim off any
+    # leading 'lib' from package name, remove any soversion or 'devel'
+    # suffix)
+    #
+    if 'sdesc' in hints:
+        colon = re.match(r'^"(.*?)(\s*:|\s+-)', hints['sdesc'])
+        if colon:
+            package_basename = re.sub(r'^lib(.*?)(|-devel|\d*)$', r'\1', p)
+            if package_basename.upper().startswith(colon.group(1).upper()):
+                logging.log(strict_lvl, "package '%s' sdesc starts with '%s'; this is redundant as the UI will show both the package name and sdesc" % (p, ''.join(colon.group(1, 2))))
+                warnings = True
+
+    return warnings
+
+
 #
 # read a single package
 #
@@ -108,8 +154,7 @@ def read_package(packages, basedir, dirpath, files, strict=False):
     relpath = os.path.relpath(dirpath, basedir)
     warnings = False
 
-    if 'setup.hint' in files:
-        files.remove('setup.hint')
+    if any([f.endswith('.hint') for f in files]):
         # the package name is always the directory name
         p = os.path.basename(dirpath)
 
@@ -123,16 +168,34 @@ def read_package(packages, basedir, dirpath, files, strict=False):
                           (dirpath, packages[p].path))
             return True
 
-        # read setup.hints
-        hints = hint.setup_hint_parse(os.path.join(dirpath, 'setup.hint'))
-        if 'parse-errors' in hints:
-            for l in hints['parse-errors']:
-                logging.error("package '%s': %s" % (p, l))
-            logging.error("errors while parsing hints for package '%s'" % p)
-            return True
-        if 'parse-warnings' in hints:
-            for l in hints['parse-warnings']:
-                logging.info("package '%s': %s" % (p, l))
+        # read setup.hint
+        legacy = 'setup.hint' in files
+        if legacy:
+            hints = read_hints(p, os.path.join(dirpath, 'setup.hint'), hint.setup)
+            if not hints:
+                return True
+            warnings = clean_hints(p, hints, strict_lvl, warnings)
+            files.remove('setup.hint')
+        else:
+            hints = {}
+
+        # determine version overrides
+        if 'override.hint' in files:
+            # read override.hint
+            override_hints = read_hints(p, os.path.join(dirpath, 'override.hint'), hint.override)
+            files.remove('override.hint')
+        else:
+            override_hints = {}
+
+            # if we didn't have a version override hint, extract any version
+            # override from legacy hints
+            for level in ['test', 'curr', 'prev']:
+                if level in hints:
+                    override_hints[level] = hints[level]
+
+        for level in ['test', 'curr', 'prev']:
+            if level in hints:
+                del hints[level]
 
         # read sha512.sum
         sha512 = {}
@@ -155,6 +218,7 @@ def read_package(packages, basedir, dirpath, files, strict=False):
 
         # collect the attributes for each tar file
         tars = {}
+        vr_list = set()
 
         for f in list(filter(lambda f: re.match(r'^' + re.escape(p) + r'.*\.tar.*$', f), files)):
             files.remove(f)
@@ -168,16 +232,22 @@ def read_package(packages, basedir, dirpath, files, strict=False):
                 logging.log(strict_lvl, "tar file '%s' in package '%s' doesn't follow naming convention" % (f, p))
                 warnings = True
             else:
+                v = match.group(1)
+                r = match.group(2)
+
                 # historically, V can contain a '-' (since we can use the fact
                 # we already know P to split unambiguously), but this is a bad
                 # idea.
-                if '-' in match.group(1):
+                if '-' in v:
                     lvl = logging.WARNING if p not in past_mistakes.hyphen_in_version else logging.INFO
                     logging.log(lvl, "tar file '%s' in package '%s' contains '-' in version" % (f, p))
 
-                if not match.group(1)[0].isdigit():
+                if not v[0].isdigit():
                     logging.warning("tar file '%s' in package '%s' has a version which doesn't start with a digit" % (f, p))
 
+                # if not there already, add to version-release list
+                vr_list.add('%s-%s' % (v, r))
+
             tars[f] = Tar()
             tars[f].size = os.path.getsize(os.path.join(dirpath, f))
             tars[f].is_empty = tarfile_is_empty(os.path.join(dirpath, f))
@@ -188,6 +258,27 @@ def read_package(packages, basedir, dirpath, files, strict=False):
                 tars[f].sha512 = sha512_file(os.path.join(dirpath, f))
                 logging.debug("no sha512.sum line for file %s in package '%s', computed sha512 hash is %s" % (f, p, tars[f].sha512))
 
+        # determine hints for each version we've encountered
+        version_hints = {}
+        for vr in vr_list:
+            hint_fn = '%s-%s.hint' % (p, vr)
+            if hint_fn in files:
+                # is there a PVR.hint file?
+                pvr_hint = read_hints(p, os.path.join(dirpath,  hint_fn), hint.pvr)
+                if not pvr_hint:
+                    return True
+                warnings = clean_hints(p, pvr_hint, strict_lvl, warnings)
+                files.remove(hint_fn)
+            elif legacy:
+                # otherwise, use setup.hint
+                pvr_hint = hints
+            else:
+                # it's an error to not have either a setup.hint or a pvr.hint
+                logging.error("package %s has packages for version %s, but no %s or setup.hint" % (p, vr, hint_fn))
+                return True
+
+            version_hints[vr] = pvr_hint
+
         # ignore dotfiles
         for f in files:
             if f.startswith('.'):
@@ -199,30 +290,14 @@ def read_package(packages, basedir, dirpath, files, strict=False):
             logging.log(strict_lvl, "unexpected files in %s: %s" % (p, ', '.join(files)))
             warnings = True
 
-        packages[p].hints = hints
+        packages[p].version_hints = version_hints
+        packages[p].override_hints = override_hints
         packages[p].tars = tars
         packages[p].path = relpath
-
-        #
-        # now we have read the package, fix some common defects in the hints
-        #
-
-        # don't allow a redundant 'package:' or 'package - ' at start of sdesc
-        #
-        # match case-insensitively, and use a base package name (trim off any
-        # leading 'lib' from package name, remove any soversion or 'devel'
-        # suffix)
-        #
-        if 'sdesc' in hints:
-            colon = re.match(r'^"(.*?)(\s*:|\s+-)', hints['sdesc'])
-            if colon:
-                package_basename = re.sub(r'^lib(.*?)(|-devel|\d*)$', r'\1', p)
-                if package_basename.upper().startswith(colon.group(1).upper()):
-                    logging.log(strict_lvl, "package '%s' sdesc starts with '%s'; this is redundant as the UI will show both the package name and sdesc" % (p, ''.join(colon.group(1, 2))))
-                    warnings = True
+        packages[p].skip = any(['skip' in version_hints[vr] for vr in vr_list])
 
     elif (len(files) > 0) and (relpath.count(os.path.sep) > 1):
-        logging.warning("no setup.hint in %s but has files: %s" % (dirpath, ', '.join(files)))
+        logging.warning("no .hint files in %s but has files: %s" % (dirpath, ', '.join(files)))
 
     if strict:
         return warnings
@@ -278,34 +353,35 @@ def validate_packages(args, packages):
     for p in sorted(packages.keys()):
         logging.debug("validating package '%s'" % (p))
 
-        if 'requires' in packages[p].hints:
-            for r in packages[p].hints['requires'].split():
-
-                # a package should not appear in it's own requires
-                if r == p:
-                    lvl = logging.WARNING if p not in past_mistakes.self_requires else logging.DEBUG
-                    logging.log(lvl, "package '%s' requires itself" % (p))
+        for (v, hints) in packages[p].version_hints.items():
+            if 'requires' in hints:
+                for r in hints['requires'].split():
+
+                    # a package should not appear in it's own requires
+                    if r == p:
+                        lvl = logging.WARNING if p not in past_mistakes.self_requires else logging.DEBUG
+                        logging.log(lvl, "package '%s' version '%s' requires itself" % (p, v))
+
+                    # all packages listed in requires must exist (unless
+                    # okmissing says that's ok)
+                    if r not in packages:
+                        if 'required-package' not in getattr(args, 'okmissing', []):
+                            logging.error("package '%s' version '%s' requires nonexistent package '%s'" % (p, v, r))
+                            error = True
+                        continue
 
-                # all packages listed in requires must exist (unless okmissing
-                # says that's ok)
-                if r not in packages:
-                    if 'required-package' not in getattr(args, 'okmissing', []):
-                        logging.error("package '%s' requires nonexistent package '%s'" % (p, r))
+                    # requiring a source-only package makes no sense
+                    if packages[r].skip:
+                        logging.error("package '%s' version '%s' requires source-only package '%s'" % (p, v, r))
                         error = True
-                    continue
 
-                # requiring a source-only package makes no sense
-                if 'skip' in packages[r].hints:
-                    logging.error("package '%s' requires source-only package '%s'" % (p, r))
+            # if external-source is used, the package must exist
+            if 'external-source' in hints:
+                e = hints['external-source']
+                if e not in packages:
+                    logging.error("package '%s' version '%s' refers to nonexistent external-source '%s'" % (p, v, e))
                     error = True
 
-        # if external-source is used, the package must exist
-        if 'external-source' in packages[p].hints:
-            e = packages[p].hints['external-source']
-            if e not in packages:
-                logging.error("package '%s' refers to nonexistent external-source '%s'" % (p, e))
-                error = True
-
         packages[p].vermap = defaultdict(defaultdict)
         is_empty = {}
         has_install = False
@@ -341,18 +417,19 @@ def validate_packages(args, packages):
         # package set, so that an upload containing just a replacement
         # setup.hint is not considered a source-only package)
         #
-        # XXX: the check should probably be for any non-empty install files, but
-        # that differs from what upset does
-        if not has_install and 'skip' not in packages[p].hints:
-            packages[p].hints['skip'] = ''
-            logging.info("package '%s' appears to be source-only as it has no install tarfiles, adding 'skip:' hint" % (p))
+        # XXX: the check should probably be for any non-empty install files, or
+        # (any install files or any dependencies), but that differs from what
+        # upset does
+        if not has_install and not packages[p].skip:
+            packages[p].skip = True
+            logging.info("package '%s' appears to be source-only as it has no install tarfiles, marking as 'skip'" % (p))
 
         # verify the versions specified for stability level exist
         levels = ['test', 'curr', 'prev']
         for l in levels:
-            if l in packages[p].hints:
+            if l in packages[p].override_hints:
                 # check that version exists
-                v = packages[p].hints[l]
+                v = packages[p].override_hints[l]
                 if v not in packages[p].vermap:
                     logging.error("package '%s' stability '%s' selects non-existent version '%s'" % (p, l, v))
                     error = True
@@ -375,9 +452,9 @@ def validate_packages(args, packages):
                 l = levels[0]
 
                 # if current stability level has an override
-                if l in packages[p].hints:
+                if l in packages[p].override_hints:
                     # if we haven't reached that version yet
-                    if v != packages[p].hints[l]:
+                    if v != packages[p].override_hints[l]:
                         break
                     else:
                         logging.debug("package '%s' stability '%s' overridden to version '%s'" % (p, l, v))
@@ -403,8 +480,15 @@ def validate_packages(args, packages):
         # lastly, fill in any levels which we skipped over because a higher
         # stability level was overriden to a lower version
         for l in levels:
-            if l in packages[p].hints:
-                packages[p].stability[l] = packages[p].hints[l]
+            if l in packages[p].override_hints:
+                packages[p].stability[l] = packages[p].override_hints[l]
+
+        # identify a 'best' version to take certain information from: this is
+        # the curr version, if we have one, otherwise, the highest version.
+        if 'curr' in packages[p].stability:
+            packages[p].best_version = packages[p].stability['curr']
+        else:
+            packages[p].best_version = sorted(packages[p].vermap.keys(), key=lambda v: SetupVersion(v), reverse=True)[0]
 
         # the package must have some versions
         if not packages[p].stability:
@@ -416,8 +500,8 @@ def validate_packages(args, packages):
 
         # If, for every stability level, the install tarball is empty and there
         # is no source tarball, we should probably be marked obsolete
-        if 'skip' not in packages[p].hints:
-            if '_obsolete' not in packages[p].hints['category']:
+        if not packages[p].skip:
+            if not any(['_obsolete' in packages[p].version_hints[vr]['category'] for vr in packages[p].version_hints]):
                 has_something = False
 
                 for l in ['test', 'curr', 'prev']:
@@ -452,8 +536,8 @@ def validate_packages(args, packages):
                 packages[p].is_used_by.add(p)
                 continue
 
-            if 'external-source' in packages[p].hints:
-                es_p = packages[p].hints['external-source']
+            if 'external-source' in packages[p].version_hints[v]:
+                es_p = packages[p].version_hints[v]['external-source']
                 if es_p in packages:
                     if 'source' in packages[es_p].vermap[v]:
                         packages[es_p].tars[packages[es_p].vermap[v]['source']].is_used = True
@@ -488,15 +572,14 @@ def validate_packages(args, packages):
 
         for install_p in packages[source_p].is_used_by:
             # ignore obsolete packages
-            if '_obsolete' in packages[install_p].hints['category']:
+            if not any(['_obsolete' in packages[p].version_hints[vr]['category'] for vr in packages[p].version_hints]):
                 continue
             # ignore runtime library packages, as we may keep old versions of
             # those
             if re.match(r'^lib.*\d', install_p):
                 continue
 
-            if 'curr' in packages[install_p].stability:
-                versions[packages[install_p].stability['curr']].append(install_p)
+            versions[packages[install_p].best_version].append(install_p)
 
         if len(versions) > 1:
             out = []
@@ -538,10 +621,10 @@ def validate_package_maintainers(args, packages):
     # validate that all packages are in the package list
     for p in sorted(packages):
         # ignore skip packages
-        if 'skip' in packages[p].hints:
+        if packages[p].skip:
             continue
         # ignore obsolete packages
-        if '_obsolete' in packages[p].hints['category']:
+        if any(['_obsolete' in packages[p].version_hints[vr]['category'] for vr in packages[p].version_hints]):
             continue
         if not is_in_package_list(packages[p].path, all_packages):
             logging.error("package '%s' is not in the package list" % (p))
@@ -574,26 +657,28 @@ def write_setup_ini(args, packages, arch):
         # for each package
         for p in sorted(packages.keys(), key=sort_key):
             # do nothing if 'skip'
-            if 'skip' in packages[p].hints:
+            if packages[p].skip:
                 continue
 
             # write package data
             print("\n@ %s" % p, file=f)
 
-            print("sdesc: %s" % packages[p].hints['sdesc'], file=f)
+            bv = packages[p].best_version
+            print("sdesc: %s" % packages[p].version_hints[bv]['sdesc'], file=f)
 
-            if 'ldesc' in packages[p].hints:
-                print("ldesc: %s" % packages[p].hints['ldesc'], file=f)
+            if 'ldesc' in packages[p].version_hints[bv]:
+                print("ldesc: %s" % packages[p].version_hints[bv]['ldesc'], file=f)
 
             # for historical reasons, category names must start with a capital
             # letter
-            category = ' '.join(map(upper_first_character, packages[p].hints['category'].split()))
+            category = ' '.join(map(upper_first_character, packages[p].version_hints[bv]['category'].split()))
             print("category: %s" % category, file=f)
 
-            # uniquify and sort requires
+            # compute the union of requires for all versions
             requires = set()
-            if 'requires' in packages[p].hints:
-                requires = set(packages[p].hints['requires'].split())
+            for hints in packages[p].version_hints.values():
+                if 'requires' in hints:
+                    requires = set.union(requires, hints['requires'].split())
             # for historical reasons, empty requires are suppressed
             if requires:
                 print("requires: %s" % ' '.join(sorted(requires)), file=f)
@@ -615,16 +700,16 @@ def write_setup_ini(args, packages, arch):
                         t = packages[p].vermap[version]['source']
                         tar_line('source', packages[p], t, f)
                     # if that doesn't exist, follow external-source
-                    elif 'external-source' in packages[p].hints:
-                        s = packages[p].hints['external-source']
+                    elif 'external-source' in packages[p].version_hints[version]:
+                        s = packages[p].version_hints[version]['external-source']
                         if 'source' in packages[s].vermap[version]:
                             t = packages[s].vermap[version]['source']
                             tar_line('source', packages[s], t, f)
                         else:
                             logging.warning("package '%s' version '%s' has no source in external-source '%s'" % (p, version, s))
 
-            if 'message' in packages[p].hints:
-                print("message: %s" % packages[p].hints['message'], file=f)
+            if 'message' in packages[p].version_hints[bv]:
+                print("message: %s" % packages[p].version_hints[bv]['message'], file=f)
 
 
 # helper function to output details for a particular tar file
@@ -672,15 +757,24 @@ def merge(a, *l):
                         else:
                             c[p].tars[t] = b[p].tars[t]
 
-                    # use hints from b, but warn if they have changed
-                    if a[p].hints != b[p].hints:
-                        c[p].hints = b[p].hints
+                    # hints from b override hints from a, but warn if they have
+                    # changed
+                    c[p].version_hints = a[p].version_hints
+                    for vr in b[p].version_hints:
+                        c[p].version_hints[vr] = b[p].version_hints[vr]
+                        if vr in a[p].version_hints:
+                            if a[p].version_hints[vr] != b[p].version_hints[vr]:
+                                diff = '\n'.join(difflib.ndiff(
+                                    pprint.pformat(a[p].version_hints[vr]).splitlines(),
+                                    pprint.pformat(b[p].version_hints[vr]).splitlines()))
+
+                                logging.warning("package '%s' hints changed\n%s" % (p, diff))
 
-                        diff = '\n'.join(difflib.ndiff(
-                            pprint.pformat(a[p].hints).splitlines(),
-                            pprint.pformat(b[p].hints).splitlines()))
+                    # take overrides from b
+                    c[p].override_hints = b[p].override_hints
 
-                        logging.warning("package '%s' hints changed\n%s" % (p, diff))
+                    # skip if both a and b are skip
+                    c[p].skip = a[p].skip and b[p].skip
 
     return c
 
diff --git a/calm/pkg2html.py b/calm/pkg2html.py
index f961ff7..f2e2cc6 100755
--- a/calm/pkg2html.py
+++ b/calm/pkg2html.py
@@ -88,7 +88,7 @@ def update_package_listings(args, packages, arch):
     for p in packages:
 
         # do nothing for packages marked 'skip'
-        if 'skip' in packages[p].hints:
+        if packages[p].skip:
             continue
 
         dir = os.path.join(base, p)
@@ -134,7 +134,8 @@ def update_package_listings(args, packages, arch):
 
                 if not args.dryrun:
                     with open(html, 'w') as f:
-                        header = p + ": " + packages[p].hints['sdesc'].replace('"', '')
+                        bv = packages[p].best_version
+                        header = p + ": " + packages[p].version_hints[bv]['sdesc'].replace('"', '')
                         if fver.endswith('-src'):
                             header = header + " (source code)"
                         else:
@@ -202,10 +203,11 @@ def update_package_listings(args, packages, arch):
 
             for p in sorted(packages.keys(), key=package.sort_key):
                 # don't write anything if 'skip'
-                if 'skip' in packages[p].hints:
+                if packages[p].skip:
                     continue
 
-                header = packages[p].hints['sdesc'].replace('"', '')
+                bv = packages[p].best_version
+                header = packages[p].version_hints[bv]['sdesc'].replace('"', '')
 
                 print('<tr><td><a href="' + arch + '/' + p + '">' + p + '</a></td><td>' + header + '</td></tr>', file=index)
 
diff --git a/calm/uploads.py b/calm/uploads.py
index dcf16a2..8364ff0 100644
--- a/calm/uploads.py
+++ b/calm/uploads.py
@@ -191,7 +191,7 @@ def scan(m, all_packages, arch, args):
             # does file already exist in release area?
             dest = os.path.join(args.rel_area, relpath, f)
             if os.path.isfile(dest):
-                if f != 'setup.hint':
+                if not f.endswith('.hint'):
                     if filecmp.cmp(dest, fn, shallow=False):
                         logging.info("ignoring, identical %s is already in release area" % fn)
                     else:
@@ -203,7 +203,8 @@ def scan(m, all_packages, arch, args):
                         logging.debug("identical %s is already in release area" % fn)
                     else:
                         logging.debug("different %s is already in release area" % fn)
-                    # we always consider setup.hint, as we can't have a valid package without it
+                    # we always consider .hint files as needing to be moved, as
+                    # we currently can't have a valid package without one
                     move[relpath].append(f)
             else:
                 move[relpath].append(f)
diff --git a/test/test_calm.py b/test/test_calm.py
index c872be6..2d60f82 100755
--- a/test/test_calm.py
+++ b/test/test_calm.py
@@ -100,7 +100,7 @@ class CalmTest(unittest.TestCase):
             if 'setup.hint' in files:
                 with self.subTest(package=os.path.basename(dirpath)):
                     logging.info('Reading %s' % os.path.join(dirpath, 'setup.hint'))
-                    results = hint.setup_hint_parse(os.path.join(dirpath, 'setup.hint'))
+                    results = hint.hint_file_parse(os.path.join(dirpath, 'setup.hint'), hint.setup)
                     compare_with_expected_file(self, os.path.join('testdata/hints', relpath), results)
 
 #
@@ -261,7 +261,8 @@ class CalmTest(unittest.TestCase):
                      (os.path.join(m_homedir, 'x86', 'release', 'testpackage2', 'testpackage2-subpackage', '!ready'), ''),
                      (os.path.join(m_homedir, 'x86', 'release', 'after-ready', '!ready'), '-t 198709011700'),
                      (os.path.join(m_homedir, 'noarch', 'release', 'perl-Net-SMTP-SSL', '!ready'), ''),
-                     (os.path.join(m_homedir, 'x86', 'release', 'corrupt', '!ready'), '')]
+                     (os.path.join(m_homedir, 'x86', 'release', 'corrupt', '!ready'), ''),
+                     (os.path.join(m_homedir, 'x86', 'release', 'per-version', '!ready'), '')]
         for (f, t) in ready_fns:
             os.system('touch %s "%s"' % (t, f))
 
diff --git a/test/testdata/homes/Blooey McFooey/x86/release/per-version/per-version-5.0-1-src.tar.xz b/test/testdata/homes/Blooey McFooey/x86/release/per-version/per-version-5.0-1-src.tar.xz
new file mode 100644
index 0000000..0e6f1e8
Binary files /dev/null and b/test/testdata/homes/Blooey McFooey/x86/release/per-version/per-version-5.0-1-src.tar.xz differ
diff --git a/test/testdata/homes/Blooey McFooey/x86/release/per-version/per-version-5.0-1.hint b/test/testdata/homes/Blooey McFooey/x86/release/per-version/per-version-5.0-1.hint
new file mode 100644
index 0000000..1f86c0b
--- /dev/null
+++ b/test/testdata/homes/Blooey McFooey/x86/release/per-version/per-version-5.0-1.hint	
@@ -0,0 +1,4 @@
+sdesc: "Per-version hint test package"
+ldesc: "Per-version hint test package"
+category: Base
+requires: base-cygwin
diff --git a/test/testdata/homes/Blooey McFooey/x86/release/per-version/per-version-5.0-1.tar.xz b/test/testdata/homes/Blooey McFooey/x86/release/per-version/per-version-5.0-1.tar.xz
new file mode 100644
index 0000000..0e6f1e8
Binary files /dev/null and b/test/testdata/homes/Blooey McFooey/x86/release/per-version/per-version-5.0-1.tar.xz differ
diff --git a/test/testdata/htdocs.expected/x86/packages.inc b/test/testdata/htdocs.expected/x86/packages.inc
index 367e3fd..cfb3c5f 100755
--- a/test/testdata/htdocs.expected/x86/packages.inc
+++ b/test/testdata/htdocs.expected/x86/packages.inc
@@ -17,6 +17,7 @@
 <tr><td><a href="x86/libdns_sd1">libdns_sd1</a></td><td>Bonjour Zeroconf implementation</td></tr>
 <tr><td><a href="x86/mDNSResponder">mDNSResponder</a></td><td>Bonjour Zeroconf implementation</td></tr>
 <tr><td><a href="x86/openssh">openssh</a></td><td>The OpenSSH server and client programs</td></tr>
+<tr><td><a href="x86/per-version">per-version</a></td><td>Per-version hint test package</td></tr>
 <tr><td><a href="x86/perl-Net-SMTP-SSL">perl-Net-SMTP-SSL</a></td><td>Perl distribution Net-SMTP-SSL</td></tr>
 <tr><td><a href="x86/rpm-doc">rpm-doc</a></td><td>Obsolete package for RPM package management system manual pages</td></tr>
 <tr><td><a href="x86/testpackage">testpackage</a></td><td>A test package</td></tr>
diff --git a/test/testdata/htdocs.expected/x86/per-version/.htaccess b/test/testdata/htdocs.expected/x86/per-version/.htaccess
new file mode 100644
index 0000000..3196d64
--- /dev/null
+++ b/test/testdata/htdocs.expected/x86/per-version/.htaccess
@@ -0,0 +1,3 @@
+Options Indexes
+IndexOptions -FancyIndexing
+AddType text/html 1 2 3 4 5 6 7 8 9
diff --git a/test/testdata/htdocs.expected/x86/per-version/per-version-4.0-1 b/test/testdata/htdocs.expected/x86/per-version/per-version-4.0-1
new file mode 100644
index 0000000..08c3fcf
--- /dev/null
+++ b/test/testdata/htdocs.expected/x86/per-version/per-version-4.0-1
@@ -0,0 +1,7 @@
+<html>
+<h1>per-version: Per-version hint test package (installed binaries and support files)</h1>
+<tt><pre>
+    2015-10-11 14:45          26 test/test.1
+    2015-10-11 14:45          31 test/test.2
+</pre></tt>
+</html>
diff --git a/test/testdata/htdocs.expected/x86/per-version/per-version-4.0-1-src b/test/testdata/htdocs.expected/x86/per-version/per-version-4.0-1-src
new file mode 100644
index 0000000..9c9bf12
--- /dev/null
+++ b/test/testdata/htdocs.expected/x86/per-version/per-version-4.0-1-src
@@ -0,0 +1,7 @@
+<html>
+<h1>per-version: Per-version hint test package (source code)</h1>
+<tt><pre>
+    2015-10-11 14:45          26 test/test.1
+    2015-10-11 14:45          31 test/test.2
+</pre></tt>
+</html>
diff --git a/test/testdata/htdocs.expected/x86/per-version/per-version-4.8-1 b/test/testdata/htdocs.expected/x86/per-version/per-version-4.8-1
new file mode 100644
index 0000000..08c3fcf
--- /dev/null
+++ b/test/testdata/htdocs.expected/x86/per-version/per-version-4.8-1
@@ -0,0 +1,7 @@
+<html>
+<h1>per-version: Per-version hint test package (installed binaries and support files)</h1>
+<tt><pre>
+    2015-10-11 14:45          26 test/test.1
+    2015-10-11 14:45          31 test/test.2
+</pre></tt>
+</html>
diff --git a/test/testdata/htdocs.expected/x86/per-version/per-version-4.8-1-src b/test/testdata/htdocs.expected/x86/per-version/per-version-4.8-1-src
new file mode 100644
index 0000000..9c9bf12
--- /dev/null
+++ b/test/testdata/htdocs.expected/x86/per-version/per-version-4.8-1-src
@@ -0,0 +1,7 @@
+<html>
+<h1>per-version: Per-version hint test package (source code)</h1>
+<tt><pre>
+    2015-10-11 14:45          26 test/test.1
+    2015-10-11 14:45          31 test/test.2
+</pre></tt>
+</html>
diff --git a/test/testdata/inifile/setup.ini.expected b/test/testdata/inifile/setup.ini.expected
index 3c60a3c..2ef3c93 100644
--- a/test/testdata/inifile/setup.ini.expected
+++ b/test/testdata/inifile/setup.ini.expected
@@ -192,6 +192,23 @@
  'source: x86/release/openssh/openssh-7.2p2-1-src.tar.xz 228 '
  'e675b0ac4bc2c3e1c4971bc56d77b0cd53a9bdf5632873a235d7582e29dfd3e8a7bb04b28f6cdee3e6b3d14c25ed39392538e3f628a9bfda6c905646ebc3c225\n'
  '\n'
+ '@ per-version\n'
+ 'sdesc: "Per-version hint test package"\n'
+ 'ldesc: "Per-version hint test package"\n'
+ 'category: Base\n'
+ 'requires: base-cygwin cygwin\n'
+ 'version: 4.8-1\n'
+ 'install: x86/release/per-version/per-version-4.8-1.tar.xz 228 '
+ 'e675b0ac4bc2c3e1c4971bc56d77b0cd53a9bdf5632873a235d7582e29dfd3e8a7bb04b28f6cdee3e6b3d14c25ed39392538e3f628a9bfda6c905646ebc3c225\n'
+ 'source: x86/release/per-version/per-version-4.8-1-src.tar.xz 228 '
+ 'e675b0ac4bc2c3e1c4971bc56d77b0cd53a9bdf5632873a235d7582e29dfd3e8a7bb04b28f6cdee3e6b3d14c25ed39392538e3f628a9bfda6c905646ebc3c225\n'
+ '[prev]\n'
+ 'version: 4.0-1\n'
+ 'install: x86/release/per-version/per-version-4.0-1.tar.xz 228 '
+ 'e675b0ac4bc2c3e1c4971bc56d77b0cd53a9bdf5632873a235d7582e29dfd3e8a7bb04b28f6cdee3e6b3d14c25ed39392538e3f628a9bfda6c905646ebc3c225\n'
+ 'source: x86/release/per-version/per-version-4.0-1-src.tar.xz 228 '
+ 'e675b0ac4bc2c3e1c4971bc56d77b0cd53a9bdf5632873a235d7582e29dfd3e8a7bb04b28f6cdee3e6b3d14c25ed39392538e3f628a9bfda6c905646ebc3c225\n'
+ '\n'
  '@ perl-Net-SMTP-SSL\n'
  'sdesc: "Perl distribution Net-SMTP-SSL"\n'
  'ldesc: "Implements the same API as Net::SMTP, but uses IO::Socket::SSL for\n'
diff --git a/test/testdata/pkglist/cygwin-pkg-maint b/test/testdata/pkglist/cygwin-pkg-maint
index 0dc42e7..9557e0a 100644
--- a/test/testdata/pkglist/cygwin-pkg-maint
+++ b/test/testdata/pkglist/cygwin-pkg-maint
@@ -1701,6 +1701,8 @@ perl-XML-Simple                              Achim Gratz/Yaakov Selkowitz
 perl-XML-Writer                              Achim Gratz/Ken Brown
 perl-YAML                                    Achim Gratz/Yaakov Selkowitz
 perl-YAML-LibYAML                            Achim Gratz
+per-version                                  Blooey McFooey
+per-version-incomplete                       Blooey McFooey
 phodav                                       Yaakov Selkowitz
 phonon                                       Yaakov Selkowitz
 phonon-backend-gstreamer                     Yaakov Selkowitz
diff --git a/test/testdata/pkglist/expected b/test/testdata/pkglist/expected
index 0f5bb1c..3a5ea77 100644
--- a/test/testdata/pkglist/expected
+++ b/test/testdata/pkglist/expected
@@ -3,7 +3,7 @@
  'Adam Dinwoodie': maintainers.Maintainer('Adam Dinwoodie', [], ['git']),
  'Alexey Sokolov': maintainers.Maintainer('Alexey Sokolov', [], ['znc']),
  'Andrew Schulman': maintainers.Maintainer('Andrew Schulman', [], ['atool', 'autossh', 'bc', 'discus', 'fish', 'lftp', 'libargp', 'nosleep', 'orpie', 'pinfo', 'ploticus', 'ploticus-doc', 'screen', 'sitecopy', 'sng', 'socat', 'stow', 'stunnel', 'time', 'unison2.27', 'unison2.32', 'unison2.40', 'unison2.45', 'unison2.48']),
- 'Blooey McFooey': maintainers.Maintainer('Blooey McFooey', [], ['corrupt', 'perl-Net-SMTP-SSL', 'testpackage']),
+ 'Blooey McFooey': maintainers.Maintainer('Blooey McFooey', [], ['corrupt', 'perl-Net-SMTP-SSL', 'per-version', 'per-version-incomplete', 'testpackage']),
  'Bob Heckel': maintainers.Maintainer('Bob Heckel', [], ['libgc', 'w3m']),
  'Charles Wilson': maintainers.Maintainer('Charles Wilson', [], ['alternatives', 'autobuild', 'cygutils', 'gcc-tools-epoch1-autoconf', 'gcc-tools-epoch1-automake', 'gcc-tools-epoch2-autoconf', 'gcc-tools-epoch2-automake', 'inetutils', 'libassuan', 'libksba', 'libustr', 'libXpm-noX', 'mingw-binutils', 'mingw-bzip2', 'mingw-gcc', 'mingw-libgcrypt', 'mingw-libgpg-error', 'mingw-pthreads', 'mingw-xz', 'mingw-zlib', 'nfrotz', 'pinentry', 'pth', 'rsh', 'run2', 'rxvt', 'sunrpc', 'tcp_wrappers', 'xsri']),
  'Chris J. Breisch': maintainers.Maintainer('Chris J. Breisch', [], ['man-db']),
diff --git a/test/testdata/process_arch/homedir.expected b/test/testdata/process_arch/homedir.expected
index 9110c87..e680cec 100644
--- a/test/testdata/process_arch/homedir.expected
+++ b/test/testdata/process_arch/homedir.expected
@@ -10,6 +10,7 @@
  'Blooey McFooey/x86/release/not-on-maintainer-list': ['not-on-maintainer-list-1.0-1.tar.bz2', 'setup.hint'],
  'Blooey McFooey/x86/release/not-on-package-list': ['not-on-package-list-1.0-1.tar.bz2', 'setup.hint'],
  'Blooey McFooey/x86/release/not-ready': ['-not-ready-0.9-1.tar.bz2', 'not-ready-1.0-1.tar.bz2', 'setup.hint'],
+ 'Blooey McFooey/x86/release/per-version': [],
  'Blooey McFooey/x86/release/testpackage': [],
  'Blooey McFooey/x86/release/testpackage/testpackage-subpackage': [],
  'Blooey McFooey/x86/release/testpackage2': ['setup.hint', 'testpackage2-1.0-1.tar.bz2'],
diff --git a/test/testdata/process_arch/htdocs.expected b/test/testdata/process_arch/htdocs.expected
index 448a696..4e5e9e2 100644
--- a/test/testdata/process_arch/htdocs.expected
+++ b/test/testdata/process_arch/htdocs.expected
@@ -20,6 +20,13 @@
  'x86/libdns_sd1': ['.htaccess', 'libdns_sd1-379.32.1-1'],
  'x86/mDNSResponder': ['.htaccess', 'mDNSResponder-379.32.1-1', 'mDNSResponder-379.32.1-1-src'],
  'x86/openssh': ['.htaccess', 'openssh-7.2p2-1', 'openssh-7.2p2-1-src'],
+ 'x86/per-version': ['.htaccess',
+                     'per-version-4.0-1',
+                     'per-version-4.0-1-src',
+                     'per-version-4.8-1',
+                     'per-version-4.8-1-src',
+                     'per-version-5.0-1',
+                     'per-version-5.0-1-src'],
  'x86/perl-Net-SMTP-SSL': ['.htaccess',
                            'perl-Net-SMTP-SSL-1.03-1',
                            'perl-Net-SMTP-SSL-1.03-1-src',
diff --git a/test/testdata/process_arch/rel_area.expected b/test/testdata/process_arch/rel_area.expected
index d697a10..ae4e2d7 100644
--- a/test/testdata/process_arch/rel_area.expected
+++ b/test/testdata/process_arch/rel_area.expected
@@ -53,6 +53,24 @@
  'x86/release/mingw64-i686-binutils': ['setup.hint', 'sha512.sum'],
  'x86/release/mingw64-i686-binutils/mingw64-i686-binutils-debuginfo': ['setup.hint', 'sha512.sum'],
  'x86/release/openssh': ['openssh-7.2p2-1-src.tar.xz', 'openssh-7.2p2-1.tar.xz', 'setup.hint', 'sha512.sum'],
+ 'x86/release/per-version': ['override.hint',
+                             'per-version-4.0-1-src.tar.xz',
+                             'per-version-4.0-1.hint',
+                             'per-version-4.0-1.tar.xz',
+                             'per-version-4.8-1-src.tar.xz',
+                             'per-version-4.8-1.hint',
+                             'per-version-4.8-1.tar.xz',
+                             'per-version-5.0-1-src.tar.xz',
+                             'per-version-5.0-1.hint',
+                             'per-version-5.0-1.tar.xz',
+                             'sha512.sum'],
+ 'x86/release/per-version-incomplete': ['override.hint',
+                                        'per-version-incomplete-36-1-src.tar.xz',
+                                        'per-version-incomplete-36-1.hint',
+                                        'per-version-incomplete-36-1.tar.xz',
+                                        'per-version-incomplete-39-1-src.tar.xz',
+                                        'per-version-incomplete-39-1.tar.xz',
+                                        'sha512.sum'],
  'x86/release/proj': ['setup.hint', 'sha512.sum'],
  'x86/release/proj/libproj-devel': ['setup.hint', 'sha512.sum'],
  'x86/release/proj/libproj1': ['setup.hint', 'sha512.sum'],
diff --git a/test/testdata/relarea/x86/release/per-version-incomplete/override.hint b/test/testdata/relarea/x86/release/per-version-incomplete/override.hint
new file mode 100644
index 0000000..e69de29
diff --git a/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-36-1-src.tar.xz b/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-36-1-src.tar.xz
new file mode 100644
index 0000000..0e6f1e8
Binary files /dev/null and b/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-36-1-src.tar.xz differ
diff --git a/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-36-1.hint b/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-36-1.hint
new file mode 100644
index 0000000..db723c3
--- /dev/null
+++ b/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-36-1.hint
@@ -0,0 +1,3 @@
+sdesc: "Per-version hint test package"
+ldesc: "Per-version hint test package with missing hint data for some versions"
+category: Base
diff --git a/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-36-1.tar.xz b/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-36-1.tar.xz
new file mode 100644
index 0000000..0e6f1e8
Binary files /dev/null and b/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-36-1.tar.xz differ
diff --git a/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-39-1-src.tar.xz b/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-39-1-src.tar.xz
new file mode 100644
index 0000000..0e6f1e8
Binary files /dev/null and b/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-39-1-src.tar.xz differ
diff --git a/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-39-1.tar.xz b/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-39-1.tar.xz
new file mode 100644
index 0000000..0e6f1e8
Binary files /dev/null and b/test/testdata/relarea/x86/release/per-version-incomplete/per-version-incomplete-39-1.tar.xz differ
diff --git a/test/testdata/relarea/x86/release/per-version/override.hint b/test/testdata/relarea/x86/release/per-version/override.hint
new file mode 100644
index 0000000..05c784e
--- /dev/null
+++ b/test/testdata/relarea/x86/release/per-version/override.hint
@@ -0,0 +1,2 @@
+curr: 4.8-1
+
diff --git a/test/testdata/relarea/x86/release/per-version/per-version-4.0-1-src.tar.xz b/test/testdata/relarea/x86/release/per-version/per-version-4.0-1-src.tar.xz
new file mode 100644
index 0000000..0e6f1e8
Binary files /dev/null and b/test/testdata/relarea/x86/release/per-version/per-version-4.0-1-src.tar.xz differ
diff --git a/test/testdata/relarea/x86/release/per-version/per-version-4.0-1.hint b/test/testdata/relarea/x86/release/per-version/per-version-4.0-1.hint
new file mode 100644
index 0000000..726738d
--- /dev/null
+++ b/test/testdata/relarea/x86/release/per-version/per-version-4.0-1.hint
@@ -0,0 +1,4 @@
+sdesc: "Per-version hint test package"
+ldesc: "Per-version hint test package"
+category: Base
+requires: cygwin
diff --git a/test/testdata/relarea/x86/release/per-version/per-version-4.0-1.tar.xz b/test/testdata/relarea/x86/release/per-version/per-version-4.0-1.tar.xz
new file mode 100644
index 0000000..0e6f1e8
Binary files /dev/null and b/test/testdata/relarea/x86/release/per-version/per-version-4.0-1.tar.xz differ
diff --git a/test/testdata/relarea/x86/release/per-version/per-version-4.8-1-src.tar.xz b/test/testdata/relarea/x86/release/per-version/per-version-4.8-1-src.tar.xz
new file mode 100644
index 0000000..0e6f1e8
Binary files /dev/null and b/test/testdata/relarea/x86/release/per-version/per-version-4.8-1-src.tar.xz differ
diff --git a/test/testdata/relarea/x86/release/per-version/per-version-4.8-1.hint b/test/testdata/relarea/x86/release/per-version/per-version-4.8-1.hint
new file mode 100644
index 0000000..1f86c0b
--- /dev/null
+++ b/test/testdata/relarea/x86/release/per-version/per-version-4.8-1.hint
@@ -0,0 +1,4 @@
+sdesc: "Per-version hint test package"
+ldesc: "Per-version hint test package"
+category: Base
+requires: base-cygwin
diff --git a/test/testdata/relarea/x86/release/per-version/per-version-4.8-1.tar.xz b/test/testdata/relarea/x86/release/per-version/per-version-4.8-1.tar.xz
new file mode 100644
index 0000000..0e6f1e8
Binary files /dev/null and b/test/testdata/relarea/x86/release/per-version/per-version-4.8-1.tar.xz differ
diff --git a/test/testdata/uploads/pkglist.expected b/test/testdata/uploads/pkglist.expected
index 695aa09..f36b2a4 100644
--- a/test/testdata/uploads/pkglist.expected
+++ b/test/testdata/uploads/pkglist.expected
@@ -1,14 +1,16 @@
 {'testpackage': Package('x86/release/testpackage', {'testpackage-1.0-1-src.tar.bz2': Tar('aff488008bee3486e25b539fe6ccd1397bd3c5c0ba2ee2cf34af279554baa195af7493ee51d6f8510735c9a2ea54436d776a71e768165716762aec286abbbf83', 195, False),
- 'testpackage-1.0-1.tar.bz2': Tar('aff488008bee3486e25b539fe6ccd1397bd3c5c0ba2ee2cf34af279554baa195af7493ee51d6f8510735c9a2ea54436d776a71e768165716762aec286abbbf83', 195, False)}, {'sdesc': '"A test package"',
- 'ldesc': '"A test package\n'
-          "It's description might contains some unicode gibberish\n"
-          'Like itâ??s youâ??re Markup Languageâ?¢ Nokogiriâ??s toolâ??that Bézier."',
- 'category': 'Devel',
- 'requires': 'cygwin'}),
- 'testpackage-subpackage': Package('x86/release/testpackage/testpackage-subpackage', {'testpackage-subpackage-1.0-1.tar.bz2': Tar('aff488008bee3486e25b539fe6ccd1397bd3c5c0ba2ee2cf34af279554baa195af7493ee51d6f8510735c9a2ea54436d776a71e768165716762aec286abbbf83', 195, False)}, {'sdesc': '"A test subpackage"',
- 'ldesc': '"A test subpackage"',
- 'category': 'Devel',
- 'external-source': 'testpackage'}),
- 'testpackage2-subpackage': Package('x86/release/testpackage2/testpackage2-subpackage', {'testpackage2-subpackage-1.0-1.tar.bz2': Tar('6de201dfed1d45412509c65deb34690dc2d09c6aafccfe491fd2f440f92842b9c755b61dc7bcdd4cc0c9f18cf46c2b3a1241e99c4c2a33fff5555e7b2f0b6348', 14, True)}, {'sdesc': '"A test subpackage 2"',
- 'ldesc': '"A test subpackage 2"',
- 'category': 'Devel'})}
+ 'testpackage-1.0-1.tar.bz2': Tar('aff488008bee3486e25b539fe6ccd1397bd3c5c0ba2ee2cf34af279554baa195af7493ee51d6f8510735c9a2ea54436d776a71e768165716762aec286abbbf83', 195, False)}, {'1.0-1': {'sdesc': '"A test package"',
+           'ldesc': '"A test package\n'
+                    "It's description might contains some unicode "
+                    'gibberish\n'
+                    'Like itâ??s youâ??re Markup Languageâ?¢ Nokogiriâ??s toolâ??that '
+                    'Bézier."',
+           'category': 'Devel',
+           'requires': 'cygwin'}}, {}, False),
+ 'testpackage-subpackage': Package('x86/release/testpackage/testpackage-subpackage', {'testpackage-subpackage-1.0-1.tar.bz2': Tar('aff488008bee3486e25b539fe6ccd1397bd3c5c0ba2ee2cf34af279554baa195af7493ee51d6f8510735c9a2ea54436d776a71e768165716762aec286abbbf83', 195, False)}, {'1.0-1': {'sdesc': '"A test subpackage"',
+           'ldesc': '"A test subpackage"',
+           'category': 'Devel',
+           'external-source': 'testpackage'}}, {}, False),
+ 'testpackage2-subpackage': Package('x86/release/testpackage2/testpackage2-subpackage', {'testpackage2-subpackage-1.0-1.tar.bz2': Tar('6de201dfed1d45412509c65deb34690dc2d09c6aafccfe491fd2f440f92842b9c755b61dc7bcdd4cc0c9f18cf46c2b3a1241e99c4c2a33fff5555e7b2f0b6348', 14, True)}, {'1.0-1': {'sdesc': '"A test subpackage 2"',
+           'ldesc': '"A test subpackage 2"',
+           'category': 'Devel'}}, {}, False)}


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]