[calm - Cygwin server-side packaging maintenance script] branch master, updated. 20171113-15-g353e677

jturney@sourceware.org jturney@sourceware.org
Fri Nov 24 13:52:00 GMT 2017




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

commit 353e677e736a9ee8094c0c1eaa5da6a89eb6b59c
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Tue Nov 21 22:12:18 2017 +0000

    Repository paths generated by mkgitolite should start with git/

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

commit 7792e5f886ac7143e5f59f345023e822df483523
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Mon Nov 20 15:52:40 2017 +0000

    Consider external-source: in 'empty but not obsolete' check

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

commit 3268914882eed98fc8849f83e01163728692e379
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Mon Nov 20 15:50:21 2017 +0000

    Don't warn about directories which just contain .sum files

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

commit 25ab26cdc4c8b75c370cdd2b2685a222ba72b65a
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Mon Nov 20 15:49:38 2017 +0000

    Fix logging of bad sha512.sum line not to include newline

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

commit 2157c661f9e465c82fe557c46b130c44ec0edac3
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Mon Nov 20 12:46:30 2017 +0000

    Suppress empty depends:, obsoletes:, build-depends:

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

commit 982ee146a7d396699455fab6b966aec5ae0f0ffe
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Wed Nov 15 18:37:06 2017 +0000

    Various fixes and improvements to dedup tool

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

commit af4b37a92aa0b6a96f4770fc6b697c2dc5e6f1ba
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Tue Nov 14 15:37:12 2017 +0000

    Add a tool for finding duplicates

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

commit 5917b6dcfb55ea65c05e07f8ebe8f3f688bfdbb2
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Wed Nov 15 10:48:19 2017 +0000

    Add a tool for migrating setup.hint to pvr.hint

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

commit 9793aa7f8dad0a99841e40166a31dfa9ad9d9768
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Fri Nov 17 11:47:01 2017 +0000

    Rationalize the way we run ad-hoc tools


Diff:
---
 calm-tool.sh            |    3 +
 calm/dedupsrc.py        |   76 ++++++++++++++++++---
 calm/find-duplicates.py |  174 +++++++++++++++++++++++++++++++++++++++++++++++
 calm/hint-migrate.py    |  104 ++++++++++++++++++++++++++++
 calm/hint.py            |    1 +
 calm/mkgitoliteconf.py  |    2 +-
 calm/package.py         |   32 ++++++---
 calm/tool.py            |   35 ++++++++++
 setup.py                |    3 +-
 9 files changed, 409 insertions(+), 21 deletions(-)

diff --git a/calm-tool.sh b/calm-tool.sh
new file mode 100755
index 0000000..a9d0bc5
--- /dev/null
+++ b/calm-tool.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+export PYTHONPATH=$(dirname "$0")
+exec python3 -m calm.$1 "${@:2}"
diff --git a/calm/dedupsrc.py b/calm/dedupsrc.py
old mode 100755
new mode 100644
index 8462fa7..d8a8e84
--- a/calm/dedupsrc.py
+++ b/calm/dedupsrc.py
@@ -24,6 +24,7 @@
 #
 # Move a given source archive to src/ (assuming it is indentical in x86/ and
 # x86_64/) and adjust hints appropriately.
+# (XXX: could probably be extended to move to noarch/ if not source, as well)
 #
 
 import argparse
@@ -35,6 +36,8 @@ import sys
 from . import common_constants
 from . import hint
 
+binary_only_hints = ['requires', 'depends', 'obsoletes', 'external-source']
+
 #
 #
 #
@@ -45,6 +48,38 @@ def hint_file_write(fn, hints):
         for k, v in hints.items():
             print("%s: %s" % (k, v), file=f)
 
+
+#
+#
+#
+
+def invent_sdesc(path, vr):
+    for (dirpath, subdirs, files) in os.walk(path):
+        # debuginfo packages never have a good sdesc
+        if 'debuginfo' in dirpath:
+            continue
+
+        # but just pick the sdesc from first sub-package which has one ...
+        for f in files:
+            if re.match('^.*-' + re.escape(vr) + '.hint$', f):
+                hints = hint.hint_file_parse(os.path.join(dirpath, f), hint.pvr)
+                if 'sdesc' in hints:
+                    sdesc = hints['sdesc']
+
+                    # ... which doesn't contain 'Obsoleted'
+                    if 'Obsoleted' in sdesc:
+                        continue
+
+                    # remove anything inside parentheses at the end of quoted
+                    # sdesc
+                    sdesc = re.sub(r'"(.*)"', r'\1', sdesc)
+                    sdesc = re.sub(r'(\(.*?\))$', '', sdesc)
+                    sdesc = sdesc.strip()
+                    sdesc = '"' + sdesc + '"'
+
+                    return sdesc
+
+    return None
 #
 #
 #
@@ -81,21 +116,37 @@ def dedup(archive, relarea):
 
         hints[arch] = hint.hint_file_parse(hint_pathname, hint.pvr)
 
+        # remove hints which only have meaning for binary packages
+        #
+        # (requires: tends to have libgcc1 more often on x86, so otherwise this
+        # would cause spurious differences between hints to be reported)
+        for h in binary_only_hints:
+            if h in hints[arch]:
+                del hints[arch][h]
+
     if hints['x86'] != hints['x86_64']:
         print('hints for %s-%s differ between arches' % (p, vr))
         return 1
 
+    if ('skip' in hints['x86']) and (len(hints['x86']) == 1):
+        print('hints for %s-%s is skip: only' % (p, vr))
+        hints['x86']['category'] = ''
+        # if hint only contains skip:, try to come up with a plausible sdesc
+        sdesc = invent_sdesc(os.path.join(relarea, 'x86', path), vr)
+        if sdesc:
+            print('suggested sdesc is %s' % (sdesc))
+            hints['x86']['sdesc'] = sdesc
+
+    if 'sdesc' not in hints['x86']:
+        print('hints for %s-%s has no sdesc:' % (p, vr))
+        return 1
+
     # ensure target directory exists
     try:
         os.makedirs(os.path.join(relarea, 'src', path, p + '-src'))
     except FileExistsError:
         pass
 
-    # move the src files to src/
-    for arch in ['x86', 'x86_64']:
-        print('%s -> %s' % (os.path.join(relarea, arch, path, filename), os.path.join(relarea, 'src', path, p + '-src', to_filename)))
-        os.rename(os.path.join(relarea, arch, path, filename), os.path.join(relarea, 'src', path, p + '-src', to_filename))
-
     # write .hint file for new -src package
     src_hints = copy.copy(hints['x86'])
 
@@ -104,16 +155,21 @@ def dedup(archive, relarea):
         sdesc += ' (source code)'
         src_hints['sdesc'] = '"' + sdesc + '"'
 
-    if 'requires' in src_hints:
-        del src_hints['requires']
+    if 'Source' not in src_hints['category']:
+        src_hints['category'] = src_hints['category'] + ' Source'
 
-    if 'external-source' in src_hints:
-        del src_hints['external-source']
+    if 'parse-warnings' in src_hints:
+        del src_hints['parse-warnings']
 
     to_hint_pathname = os.path.join(relarea, 'src', path, p + '-src', to_hint_filename)
     print('writing %s' % (to_hint_pathname))
     hint_file_write(to_hint_pathname, src_hints)
 
+    # move the src files to src/
+    for arch in ['x86', 'x86_64']:
+        print('%s -> %s' % (os.path.join(relarea, arch, path, filename), os.path.join(relarea, 'src', path, p + '-src', to_filename)))
+        os.rename(os.path.join(relarea, arch, path, filename), os.path.join(relarea, 'src', path, p + '-src', to_filename))
+
     # adjust external-source in .hint for all subpackages
     for arch in ['x86', 'x86_64']:
         for (dirpath, subdirs, files) in os.walk(os.path.join(relarea, arch, path)):
@@ -122,6 +178,8 @@ def dedup(archive, relarea):
             if filename in files:
                 hint_pathname = os.path.join(dirpath, filename)
                 hints = hint.hint_file_parse(hint_pathname, hint.pvr)
+                if 'parse-warnings' in hints:
+                    del hints['parse-warnings']
                 if ('skip' in hints):
                     # p was source only, so no package remains
                     print('removing %s' % (hint_pathname))
diff --git a/calm/find-duplicates.py b/calm/find-duplicates.py
new file mode 100644
index 0000000..ec850a4
--- /dev/null
+++ b/calm/find-duplicates.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2017 Jon Turney
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+import argparse
+import hashlib
+import re
+import os
+import sys
+import tarfile
+
+from . import common_constants
+
+#
+# look for archives which are duplicated between x86 and x86_64
+# (these should probably be moved to noarch or src)
+#
+
+#
+# helper function to compute sha512 for a particular file
+# (block_size should be some multiple of sha512 block size which can be
+# efficiently read)
+#
+
+
+def sha512_file(f, block_size=256 * 128):
+    sha512 = hashlib.sha512()
+
+    for chunk in iter(lambda: f.read(block_size), b''):
+        sha512.update(chunk)
+
+    return sha512.hexdigest()
+
+#
+#
+#
+
+
+class TarMemberInfo:
+    def __init__(self, info, sha512):
+        self.info = info
+        self.sha512 = sha512
+
+
+def read_tar(f):
+    result = {}
+
+    try:
+        with tarfile.open(f) as t:
+            for m in t:
+                if m.isfile():
+                    f = t.extractfile(m)
+                    sha512 = sha512_file(f)
+                else:
+                    sha512 = None
+                result[m.name] = TarMemberInfo(m, sha512)
+    except tarfile.ReadError:
+        # if we can't read the tar archive, we should never consider it to have
+        # the same contents as another tar archive...
+        result[f] = None
+
+    return result
+
+#
+#
+#
+
+
+def compare_archives(f1, f2):
+    # for speed, first check that archives are of the same size
+    if os.path.getsize(f1) != os.path.getsize(f2):
+        return 'different archive size'
+
+    # if they are both compressed empty files (rather than compressed empty tar
+    # archives), they are the same
+    if os.path.getsize(f1) <= 32:
+        return None
+
+    t1 = read_tar(f1)
+    t2 = read_tar(f2)
+
+    if t1.keys() != t2.keys():
+        return 'different member lists'
+
+    for m in t1:
+        # compare size of member
+        if t1[m].info.size != t2[m].info.size:
+            return 'different size for member %s' % m
+
+        # compare type of member
+        if t1[m].info.type != t2[m].info.type:
+            return 'different type for member %s' % m
+
+        # for files, compare hash of file content
+        if t1[m].info.isfile():
+            if t1[m].sha512 != t2[m].sha512:
+                return 'different hash for member %s' % m
+        # for links, compare target
+        elif t1[m].info.islnk() or t1[m].info.issym():
+            if t1[m].info.linkname != t2[m].info.linkname:
+                return 'different linkname for member %s' % m
+
+        # permitted differences: mtime, mode, owner uid/gid
+
+    return None
+
+#
+#
+#
+
+
+def find_duplicates(args):
+    basedir = os.path.join(args.rel_area, common_constants.ARCHES[0], 'release')
+
+    for (dirpath, subdirs, files) in os.walk(basedir):
+        relpath = os.path.relpath(dirpath, basedir)
+        otherdir = os.path.join(args.rel_area, common_constants.ARCHES[1], 'release', relpath)
+
+        for f in files:
+            # not an archive
+            if not re.match(r'^.*\.tar\.(bz2|gz|lzma|xz)$', f):
+                continue
+
+            f1 = os.path.join(dirpath, f)
+            f2 = os.path.join(otherdir, f)
+
+            if os.path.exists(f2):
+                difference = compare_archives(f1, f2)
+                if difference is None:
+                    print(os.path.join('release', relpath, f))
+                elif args.verbose:
+                    print('%s: %s' % (os.path.join('release', relpath, f), difference))
+
+#
+#
+#
+
+
+def main():
+    relarea_default = common_constants.FTP
+
+    parser = argparse.ArgumentParser(description='Source package deduplicator')
+    parser.add_argument('--releasearea', action='store', metavar='DIR', help="release directory (default: " + relarea_default + ")", default=relarea_default, dest='rel_area')
+    parser.add_argument('-v', '--verbose', action='count', dest='verbose', help='verbose output')
+    (args) = parser.parse_args()
+
+    return find_duplicates(args)
+
+
+#
+#
+#
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/calm/hint-migrate.py b/calm/hint-migrate.py
new file mode 100644
index 0000000..4d8156e
--- /dev/null
+++ b/calm/hint-migrate.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2017 Jon Turney
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+import argparse
+import re
+import os
+import shutil
+import sys
+
+from . import common_constants
+from . import hint
+
+#
+# migrate setup.hint to pvr.hint
+#
+# (just copy setup.hint to any missing pvr.hint. we don't need to bother
+# cleaning up setup.hint which are no longer needed, as calm can do that)
+#
+
+
+def hint_migrate(args):
+    for arch in common_constants.ARCHES + ['noarch']:
+        basedir = os.path.join(args.rel_area, arch, 'release')
+
+        for (dirpath, subdirs, files) in os.walk(basedir):
+            relpath = os.path.relpath(dirpath, basedir)
+
+            if 'setup.hint' not in files:
+                continue
+            setup_hint_fn = os.path.join(dirpath, 'setup.hint')
+
+            migrate = set()
+            for f in files:
+                match = re.match(r'^(.*?)(-src|)\.tar\.(bz2|gz|lzma|xz)$', f)
+
+                # not an archive?
+                if not match:
+                    continue
+
+                pvr = match.group(1)
+
+                # pvr.hint already exists?
+                if os.path.exists(os.path.join(dirpath, pvr + '.hint')):
+                    continue
+
+                migrate.add(pvr)
+
+            # nothing to migrate
+            if not migrate:
+                continue
+
+            # does the setup.hint parse as a pvr.hint
+            # (i.e. does it not contain version keys)
+            hints = hint.hint_file_parse(setup_hint_fn, hint.pvr)
+            if 'parse-errors' in hints:
+                print("can't migrate %s as it contains version keys" % (setup_hint_fn))
+                continue
+
+            for pvr in migrate:
+                pvr_hint_fn = os.path.join(dirpath, pvr + '.hint')
+                print('copy %s -> %s' % (setup_hint_fn, pvr_hint_fn))
+                shutil.copy2(setup_hint_fn, pvr_hint_fn)
+
+
+#
+#
+#
+
+def main():
+    relarea_default = common_constants.FTP
+
+    parser = argparse.ArgumentParser(description='setup.hint migrator')
+    parser.add_argument('--releasearea', action='store', metavar='DIR', help="release directory (default: " + relarea_default + ")", default=relarea_default, dest='rel_area')
+    (args) = parser.parse_args()
+
+    return hint_migrate(args)
+
+
+#
+#
+#
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/calm/hint.py b/calm/hint.py
index 0c9fd50..f454fa8 100755
--- a/calm/hint.py
+++ b/calm/hint.py
@@ -124,6 +124,7 @@ categories = ['accessibility',
               'science',
               'security',
               'shells',
+              'source',  # added to all source packages created by deduplicator to ensure they have a category
               'sugar',
               'system',
               'tcl',
diff --git a/calm/mkgitoliteconf.py b/calm/mkgitoliteconf.py
index 374965e..bf243e6 100755
--- a/calm/mkgitoliteconf.py
+++ b/calm/mkgitoliteconf.py
@@ -77,7 +77,7 @@ def do_main(args):
         if p.startswith('_'):
             p = p[1:]
 
-        print("repo cygwin-packages/%s" % (p))
+        print("repo git/cygwin-packages/%s" % (p))
         print("C  = %s" % (users))
         print("RW = %s" % (users))
         print("owner = %s" % (owner))
diff --git a/calm/package.py b/calm/package.py
index 2d00299..73ba0ed 100755
--- a/calm/package.py
+++ b/calm/package.py
@@ -227,7 +227,7 @@ def read_package(packages, basedir, dirpath, files, strict=False, remove=[], upl
                     if match:
                         sha512[match.group(2)] = match.group(1)
                     else:
-                        logging.warning("bad line '%s' in sha512.sum for package '%s'" % (l, p))
+                        logging.warning("bad line '%s' in sha512.sum for package '%s'" % (l.strip(), p))
 
         # discard obsolete md5.sum
         if 'md5.sum' in files:
@@ -350,9 +350,14 @@ def read_package(packages, basedir, dirpath, files, strict=False, remove=[], upl
         packages[p].path = relpath
         packages[p].skip = any(['skip' in version_hints[vr] for vr in version_hints])
 
-    elif (len(files) > 0) and (relpath.count(os.path.sep) > 1):
-        logging.log(strict_lvl, "no .hint files in %s but has files: %s" % (dirpath, ', '.join(files)))
-        warnings = True
+    elif (relpath.count(os.path.sep) > 1):
+        for s in ['md5.sum', 'sha512.sum']:
+            if s in files:
+                files.remove(s)
+
+        if len(files) > 0:
+            logging.log(strict_lvl, "no .hint files in %s but has files: %s" % (dirpath, ', '.join(files)))
+            warnings = True
 
     if strict:
         return warnings
@@ -623,11 +628,10 @@ def validate_packages(args, packages):
 
         # If the install tarball is empty and there is no source tarball, we
         # should probably be marked obsolete
-        # (XXX: should consider external-source: ?)
         if not packages[p].skip:
             for vr in packages[p].version_hints:
                 if '_obsolete' not in packages[p].version_hints[vr].get('category', ''):
-                    if 'source' not in packages[p].vermap[vr]:
+                    if ('source' not in packages[p].vermap[vr]) and ('external-source' not in packages[p].version_hints[vr]):
                         if 'install' in packages[p].vermap[vr]:
                             if packages[p].tar(vr, 'install').is_empty:
                                 if p in past_mistakes.empty_but_not_obsolete:
@@ -671,6 +675,16 @@ def validate_packages(args, packages):
                         packages[es_p].is_used_by.add(p)
                         continue
 
+                # this is a bodge to follow external-source: which hasn't been
+                # updated following a source package de-duplication
+                es_p = es_p + '-src'
+                if es_p in packages:
+                    if 'source' in packages[es_p].vermap[v]:
+                        logging.warning("package '%s' version '%s' external-source: should be %s" % (p, v, es_p))
+                        packages[es_p].tar(v, 'source').is_used = True
+                        packages[es_p].is_used_by.add(p)
+                        continue
+
             # unless this package is marked as 'self-source'
             if p in past_mistakes.self_source:
                 continue
@@ -899,13 +913,13 @@ def write_setup_ini(args, packages, arch):
                         else:
                             logging.warning("package '%s' version '%s' has no source in external-source '%s'" % (p, version, s))
 
-                if 'depends' in packages[p].version_hints[version]:
+                if packages[p].version_hints[version].get('depends', ''):
                     print("depends: %s" % packages[p].version_hints[version]['depends'], file=f)
 
-                if 'obsoletes' in packages[p].version_hints[version]:
+                if packages[p].version_hints[version].get('obsoletes', ''):
                     print("obsoletes: %s" % packages[p].version_hints[version]['obsoletes'], file=f)
 
-                if 'build-depends' in packages[p].version_hints[version]:
+                if packages[p].version_hints[version].get('build-depends', ''):
                     bd = packages[p].version_hints[version]['build-depends']
 
                     # Ideally, we'd transform dependency atoms which aren't
diff --git a/calm/tool.py b/calm/tool.py
new file mode 100644
index 0000000..e7668ff
--- /dev/null
+++ b/calm/tool.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) Jon Turney
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+import importlib
+import sys
+
+
+def main():
+    # extract module name from argv
+    name = sys.argv[1]
+    sys.argv[1:] = sys.argv[2:]
+
+    # dispatch to main() of tool module
+    module = importlib.import_module('calm.' + name)
+    sys.exit(module.main())
diff --git a/setup.py b/setup.py
index 303277c..94c78ca 100644
--- a/setup.py
+++ b/setup.py
@@ -13,8 +13,7 @@ setup(
         'console_scripts': [
             'calm = calm.calm:main',
             'mksetupini = calm.mksetupini:main',
-            'calm-mkgitoliteconf = calm.mkgitoliteconf:main',
-            'dedup-source = calm.dedupsrc:main',
+            'calm-tool = calm.tool:main',
         ],
     },
     url='https://cygwin.com/git/?p=cygwin-apps/calm.git',



More information about the Cygwin-apps-cvs mailing list