commit c9b113ac988150122f500e18a7c0467cabdabb64
Author: Vangelis Koukis <vkoukis@grnet.gr>
Date:   Tue Jan 8 16:12:28 2013 +0200

    WIP commit for the devtools build system

diff --git a/COPYRIGHT b/COPYRIGHT
new file mode 100644
index 0000000..ae73191
--- /dev/null
+++ b/COPYRIGHT
@@ -0,0 +1,32 @@
+Copyright (C) 2010, 2011, 2012 GRNET S.A. All rights reserved.
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the following
+conditions are met:
+
+  1. Redistributions of source code must retain the above
+     copyright notice, this list of conditions and the following
+     disclaimer.
+
+  2. Redistributions in binary form must reproduce the above
+     copyright notice, this list of conditions and the following
+     disclaimer in the documentation and/or other materials
+     provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and
+documentation are those of the authors and should not be
+interpreted as representing official policies, either expressed
+or implied, of GRNET S.A.
diff --git a/devtools/__init__.py b/devtools/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/devtools/autopkg.sh b/devtools/autopkg.sh
index 3107bad..f643195 100755
--- a/devtools/autopkg.sh
+++ b/devtools/autopkg.sh
@@ -1,5 +1,38 @@
 #!/bin/bash
 
+# Copyright 2012 GRNET S.A. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+# 
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+# 
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+# 
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# 
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
 parse_git_branch()
 {
     git branch 2> /dev/null | grep '^*' | sed 's/^*\ //g'
@@ -98,7 +131,7 @@ snap=false
 mrgextra=-m
 dchextra=-R
 mrgmsg="Merge branch '$REMOTEUPSTREAM' into $REMOTEDEBIAN"
-dialog --yesno "Create Snapshot?" 5 20 && snap=true && dchextra=-S && mrgextra= && mrgmsg=
+dialog --yesno "Create Snapshot?" 5 20 && snap=true && GITFLOW_BUILD_MODE=snapshot && dchextra=-S && mrgextra= && mrgmsg=
 
 # merge local branch into tmp branch with a nice commit message,
 # so it can be pushed as is to upstream debian
@@ -107,7 +140,11 @@ git merge $mrgextra ${mrgextra:+"$mrgmsg"} $LOCALBRANCH
 
 # auto edit Debian changelog depending on Snapshot or Release mode
 export EDITOR=/usr/bin/vim
-git-dch --debian-branch=$TMPDEBIAN --git-author --ignore-regex=".*" --multimaint-merge --since=HEAD $dchextra
+
+# use the devtools to determine Debian version
+export GITFLOW_BUILD_MODE
+version=$(devtools/version.py debian)
+git-dch --debian-branch=$TMPDEBIAN --git-author --ignore-regex=".*" --multimaint-merge --since=HEAD -N $version
 git add debian/changelog
 
 # get version from the changelog
diff --git a/devtools/devtools.version_unittest.py b/devtools/devtools.version_unittest.py
new file mode 100755
index 0000000..f592830
--- /dev/null
+++ b/devtools/devtools.version_unittest.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python
+#
+# Copyright 2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+#
+#
+
+"""Unit Tests for devtools.version
+
+Provides unit tests for module devtools.version,
+for automatic generation of version strings.
+
+"""
+
+import os
+import unittest
+from pkg_resources import parse_version
+from version import debian_version_from_python_version
+
+
+class DebianVersionObject(object):
+    """Object representing a Debian Version."""
+    def __init__(self, pyver):
+        self.version = debian_version_from_python_version(pyver)
+
+    def __str__(self):
+        return self.version
+
+
+def debian_compare_versions(a, op, b):
+    i = os.system("dpkg --compare-versions %s %s %s" % (a, op, b))
+    return i == 0
+
+# Set ordering between DebianVersionObject objects, by adding
+# debian_compare_versions
+for op in ["lt", "le", "eq", "ne", "gt", "ge"]:
+    def gen(op):
+        def operator_func(self, other):
+            return debian_compare_versions(self.version, op, other.version)
+        return operator_func
+    setattr(DebianVersionObject, "__%s__" % op, gen(op))
+
+
+def _random_commit():
+    import random
+    import string
+    return "".join(random.choice(string.hexdigits) for n in xrange(8)).lower()
+
+
+# Add a random commit number at the end of snapshot versions
+def version_with_commit(parse_func, v):
+    if "_" in v:
+        return parse_func(v + "_" + _random_commit())
+    else:
+        return parse_func(v)
+
+V = lambda v: version_with_commit(parse_version, v)
+D = lambda v: version_with_commit(DebianVersionObject, v)
+
+
+class TestVersionFunctions(unittest.TestCase):
+    def setUp(self):
+        self.version_orderings = (
+            ("0.14next", ">", "0.14"),
+            ("0.14next", ">", "0.14rc7"),
+            ("0.14next", "<", "0.14.1"),
+            ("0.14rc6", "<", "0.14"),
+            ("0.14.2rc6", ">", "0.14.1"),
+            ("0.14next_150", "<", "0.14next"),
+            ("0.14.1next_150", "<", "0.14.1next"),
+            ("0.14.1_149", "<", "0.14.1"),
+            ("0.14.1_149", "<", "0.14.1_150"),
+            ("0.13next_102", "<", "0.13next"),
+            ("0.13next", "<", "0.14rc5_120"),
+            ("0.14rc3_120", "<", "0.14rc3"),
+            # The following test fails, but version.python_version
+            # will never try to produce such a version:
+            # ("0.14rc3", "<", "0.14_1"),
+            ("0.14_120", "<", "0.14"),
+            ("0.14", "<", "0.14next_20"),
+            ("0.14next_20", "<", "0.14next"),
+        )
+
+    def test_python_versions(self):
+        for a, op, b in self.version_orderings:
+            res = compare(V, a, op, b)
+            self.assertTrue(res, "Python version: %s %s %s"
+                                 " is not True" % (a, op, b))
+
+    def test_debian_versions(self):
+        for a, op, b in self.version_orderings:
+            res = compare(D, a, op, b)
+            self.assertTrue(res, "Debian version %s %s %s"
+                                 " is not True" % (a, op, b))
+
+
+def compare(function, a, op, b):
+    import operator
+    str_to_op = {"<": operator.lt,
+            "<=": operator.le,
+            "==": operator.eq,
+            ">": operator.gt,
+            ">=": operator.ge}
+    try:
+        return str_to_op[op](function(a), function(b))
+    except KeyError:
+        raise ValueError("Unknown operator '%s'" % op)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/devtools/version.py b/devtools/version.py
new file mode 100755
index 0000000..cd196c7
--- /dev/null
+++ b/devtools/version.py
@@ -0,0 +1,421 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2010, 2011, 2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+
+import os
+import re
+import sys
+import pprint
+import subprocess
+from distutils import log
+from collections import namedtuple
+
+
+# Branch types:
+# builds_snapshot: Whether the branch can produce snapshot builds
+# builds_release: Whether the branch can produce release builds
+# versioned: Whether the name of the branch defines a specific version
+# allowed_version_re: A regular expression describing allowed values for
+#                     base_version in this branch
+branch_type = namedtuple("branch_type", ["builds_snapshot", "builds_release",
+                                         "versioned", "allowed_version_re"])
+VERSION_RE = "[0-9]+\.[0-9]+"
+BRANCH_TYPES = {
+    "feature": branch_type(True, False, False, "^%snext$" % VERSION_RE),
+    "develop": branch_type(True, False, False, "^%snext$" % VERSION_RE),
+    "release": branch_type(True, True, True,
+                           "^(?P<bverstr>%s)rc[1-9][0-9]*$" % VERSION_RE),
+    "master": branch_type(False, True, False,
+                          "^%s$" % VERSION_RE),
+    "hotfix": branch_type(True, True, True,
+                          "^(?P<bverstr>^%s\.[1-9][0-9]*)$" % VERSION_RE)}
+BASE_VERSION_FILE = "version"
+
+
+def callgit(cmd):
+    p = subprocess.Popen(["/bin/sh", "-c", cmd],
+                          stdout=subprocess.PIPE,
+                          stderr=subprocess.PIPE)
+    output = p.communicate()[0].strip()
+    if p.returncode != 0:
+        log.error("Command '%s' failed with output:\n%s" % (cmd, output))
+        raise subprocess.CalledProcessError(p.returncode, cmd, output)
+    return output
+
+
+def vcs_info():
+    """
+    Return current git HEAD commit information.
+
+    Returns a tuple containing
+        - branch name
+        - commit id
+        - commit count
+        - git describe output
+        - path of git toplevel directory
+
+    """
+    try:
+        branch = callgit("git rev-parse --abbrev-ref HEAD")
+        revid = callgit("git rev-parse --short HEAD")
+        revno = int(callgit("git rev-list HEAD|wc -l"))
+        desc = callgit("git describe --tags")
+        toplevel = callgit("git rev-parse --show-toplevel")
+    except subprocess.CalledProcessError:
+        log.error("Could not retrieve git information. " +
+                  "Current directory not a git repository?")
+        raise
+
+    info = namedtuple("vcs_info", ["branch", "revid", "revno",
+                                   "desc", "toplevel"])
+
+    return info(branch=branch, revid=revid, revno=revno, desc=desc,
+                toplevel=toplevel)
+
+
+def base_version(vcs_info):
+    """Determine the base version from a file in the repository"""
+
+    f = open(os.path.join(vcs_info.toplevel, BASE_VERSION_FILE))
+    lines = [l.strip() for l in f.readlines()]
+    l = [l for l in lines if not l.startswith("#")]
+    if len(l) != 1:
+        raise ValueError("File '%s' should contain a single non-comment line.")
+    return l[0]
+
+
+def build_mode():
+    """Determine the build mode from the value of $GITFLOW_BUILD_MODE"""
+    try:
+        mode = os.environ["GITFLOW_BUILD_MODE"]
+        assert mode == "release" or mode == "snapshot"
+    except (KeyError, AssertionError):
+        raise ValueError("GITFLOW_BUILD_MODE environment variable must be "
+                         "'release' or 'snapshot'")
+    return mode
+
+
+def python_version(base_version, vcs_info, mode):
+    """Generate a Python distribution version following devtools conventions.
+
+    This helper generates a Python distribution version from a repository
+    commit, following devtools conventions. The input data are:
+        * base_version: a base version number, presumably stored in text file
+          inside the repository, e.g., /version
+        * vcs_info: vcs information: current branch name and revision no
+        * mode: "snapshot", or "release"
+
+    This helper assumes a git branching model following:
+    http://nvie.com/posts/a-successful-git-branching-model/
+
+    with 'master', 'develop', 'release-X', 'hotfix-X' and 'feature-X' branches.
+
+    General rules:
+    a) any repository commit can get as a Python version
+    b) a version is generated either in 'release' or in 'snapshot' mode
+    c) the choice of mode depends on the branch, see following table.
+
+    A python version is of the form A_NNN,
+    where A: X.Y.Z{,next,rcW} and NNN: a revision number for the commit,
+    as returned by vcs_info().
+
+    For every combination of branch and mode, releases are numbered as follows:
+
+    BRANCH:  /  MODE: snapshot        release
+    --------          ------------------------------
+    feature           0.14next_150    N/A
+    develop           0.14next_151    N/A
+    release           0.14rc2_249     0.14rc2
+    master            N/A             0.14
+    hotfix            0.14.1rc6_121   0.14.1rc6
+                      N/A             0.14.1
+
+    The suffix 'next' in a version name is used to denote the upcoming version,
+    the one being under development in the develop and release branches.
+    Version '0.14next' is the version following 0.14, and only lives on the
+    develop and feature branches.
+
+    The suffix 'rc' is used to denote release candidates. 'rc' versions live
+    only release and hotfix branches.
+
+    Suffixes 'next' and 'rc' have been chosen to ensure proper ordering
+    according to setuptools rules:
+
+        http://www.python.org/dev/peps/pep-0386/#setuptools
+
+    Every branch uses a value for A so that all releases are ordered based
+    on the branch they came from, so:
+
+    So
+        0.13next < 0.14rcW < 0.14 < 0.14next < 0.14.1
+
+    and
+
+    >>> V("0.14next") > V("0.14")
+    True
+    >>> V("0.14next") > V("0.14rc7")
+    True
+    >>> V("0.14next") > V("0.14.1")
+    False
+    >>> V("0.14rc6") > V("0.14")
+    False
+    >>> V("0.14.2rc6") > V("0.14.1")
+    True
+
+    The value for _NNN is chosen based of the revision number of the specific
+    commit. It is used to ensure ascending ordering of consecutive releases
+    from the same branch. Every version of the form A_NNN comes *before*
+    than A: All snapshots are ordered so they come before the corresponding
+    release.
+
+    So
+        0.14next_* < 0.14
+        0.14.1_* < 0.14.1
+        etc
+
+    and
+
+    >>> V("0.14next_150") < V("0.14next")
+    True
+    >>> V("0.14.1next_150") < V("0.14.1next")
+    True
+    >>> V("0.14.1_149") < V("0.14.1")
+    True
+    >>> V("0.14.1_149") < V("0.14.1_150")
+    True
+
+    Combining both of the above, we get
+       0.13next_* < 0.13next < 0.14rcW_* < 0.14rcW < 0.14_* < 0.14
+       < 0.14next_* < 0.14next < 0.14.1_* < 0.14.1
+
+    and
+
+    >>> V("0.13next_102") < V("0.13next")
+    True
+    >>> V("0.13next") < V("0.14rc5_120")
+    True
+    >>> V("0.14rc3_120") < V("0.14rc3")
+    True
+    >>> V("0.14rc3") < V("0.14_1")
+    True
+    >>> V("0.14_120") < V("0.14")
+    True
+    >>> V("0.14") < V("0.14next_20")
+    True
+    >>> V("0.14next_20") < V("0.14next")
+    True
+
+    Note: one of the tests above fails because of constraints in the way
+    setuptools parses version numbers. It does not affect us because the
+    specific version format that triggers the problem is not contained in the
+    table showing allowed branch / mode combinations, above.
+
+
+    """
+
+    branch = vcs_info.branch
+
+    # If it's a debian branch, ignore starting "debian-"
+    brnorm = branch
+    if brnorm == "debian":
+        brnorm = "debian-master"
+    if brnorm.startswith("debian-"):
+        brnorm = brnorm.split("debian-")[1]
+
+    # Sanity checks
+    if "-" in brnorm:
+        btypestr = brnorm.split("-")[0]
+        bverstr = brnorm.split("-")[1]
+        if bverstr == "":
+            raise ValueError("Malformed branch name '%s'" % branch)
+        versioned = True
+    else:
+        btypestr = branch
+        versioned = False
+    try:
+        btype = BRANCH_TYPES[btypestr]
+    except KeyError:
+        allowed_branches = ", ".join(x for x in BRANCH_TYPES.keys())
+        raise ValueError("Malformed branch name '%s', cannot classify as one "
+                         "of %s" % (btypestr, allowed_branches))
+
+    if versioned != btype.versioned:
+        raise ValueError(("Branch name '%s' should %s contain version" %
+                          (branch, "not" if versioned else "")))
+    if btype.versioned and not re.match(VERSION_RE, bverstr):
+        raise ValueError(("Malformed version '%s' in branch name '%s'" %
+                          (bverstr, branch)))
+
+    m = re.match(btype.allowed_version_re, base_version)
+    if not m or (btype.versioned and m.groupdict()["bverstr"] != bverstr):
+        raise ValueError(("Base version '%s' unsuitable for branch name '%s'" %
+                         (base_version, branch)))
+
+    if mode not in ["snapshot", "release"]:
+        raise ValueError(("Specified mode '%s' should be one of 'snapshot' or "
+                          "'release'" % mode))
+    snap = (mode == "snapshot")
+
+    if ((snap and not btype.builds_snapshot) or
+        (not snap and not btype.builds_release)):
+        raise ValueError(("Invalid mode '%s' in branch type '%s'" %
+                          (mode, btypestr)))
+
+    if snap:
+        v = "%s_%d_%s" % (base_version, vcs_info.revno, vcs_info.revid)
+    else:
+        v = base_version
+    return v
+
+
+def debian_version_from_python_version(pyver):
+    """Generate a debian package version from a Python version.
+
+    This helper generates a Debian package version from a Python version,
+    following devtools conventions.
+
+    Debian sorts version strings differently compared to setuptools:
+    http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
+
+    Initial tests:
+
+    >>> debian_version("3") < debian_version("6")
+    True
+    >>> debian_version("3") < debian_version("2")
+    False
+    >>> debian_version("1") == debian_version("1")
+    True
+    >>> debian_version("1") != debian_version("1")
+    False
+    >>> debian_version("1") >= debian_version("1")
+    True
+    >>> debian_version("1") <= debian_version("1")
+    True
+
+    This helper defines a 1-1 mapping between Python and Debian versions,
+    with the same ordering.
+
+    Debian versions are ordered in the same way as Python versions:
+
+    >>> D("0.14next") > D("0.14")
+    True
+    >>> D("0.14next") > D("0.14rc7")
+    True
+    >>> D("0.14next") > D("0.14.1")
+    False
+    >>> D("0.14rc6") > D("0.14")
+    False
+    >>> D("0.14.2rc6") > D("0.14.1")
+    True
+
+    and
+
+    >>> D("0.14next_150") < D("0.14next")
+    True
+    >>> D("0.14.1next_150") < D("0.14.1next")
+    True
+    >>> D("0.14.1_149") < D("0.14.1")
+    True
+    >>> D("0.14.1_149") < D("0.14.1_150")
+    True
+
+    and
+
+    >>> D("0.13next_102") < D("0.13next")
+    True
+    >>> D("0.13next") < D("0.14rc5_120")
+    True
+    >>> D("0.14rc3_120") < D("0.14rc3")
+    True
+    >>> D("0.14rc3") < D("0.14_1")
+    True
+    >>> D("0.14_120") < D("0.14")
+    True
+    >>> D("0.14") < D("0.14next_20")
+    True
+    >>> D("0.14next_20") < D("0.14next")
+    True
+
+    """
+    return pyver.replace("_", "~").replace("rc", "~rc")
+
+
+def debian_version(base_version, vcs_info, mode):
+    p = python_version(base_version, vcs_info, mode)
+    return debian_version_from_python_version(p)
+
+
+def update_version(module, name="version", root="."):
+    """
+    Generate or replace version.py as a submodule of `module`.
+
+    This is a helper to generate/replace a version.py file containing version
+    information as a submodule of passed `module`.
+
+    """
+
+    # FIXME: exit or fail if not in development environment?
+    v = vcs_info()
+    b = base_version(v)
+    mode = build_mode()
+    paths = [root] + module.split(".") + ["%s.py" % name]
+    module_filename = os.path.join(*paths)
+    content = """
+__version__ = "%(version)s"
+__version_info__ = __version__.split(".")
+__version_vcs_info__ = %(vcs_info)s
+    """ % dict(version=python_version(b, v, mode),
+            vcs_info=pprint.PrettyPrinter().pformat(dict(v._asdict())))
+
+    module_file = file(module_filename, "w+")
+    module_file.write(content)
+    module_file.close()
+
+
+if __name__ == "__main__":
+    v = vcs_info()
+    b = base_version(v)
+    mode = build_mode()
+
+    try:
+        arg = sys.argv[1]
+        assert arg == "python" or arg == "debian"
+    except IndexError:
+        raise ValueError("A single argument, 'python' or 'debian is required")
+
+    if arg == "python":
+        print python_version(b, v, mode)
+    elif arg == "debian":
+        print debian_version(b, v, mode)
diff --git a/snf-astakos-app/setup.py b/snf-astakos-app/setup.py
index acaaae2..9e3e66a 100644
--- a/snf-astakos-app/setup.py
+++ b/snf-astakos-app/setup.py
@@ -47,11 +47,11 @@ from astakos import get_version
 
 HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
 try:
-    # try to update the version file
-    from synnefo.util import version
-    version.update_version('astakos', 'version', HERE)
+    # use devtools to update the version file
+    from devtools.version import update_version
+    update_version('astakos', 'common', HERE)
 except ImportError:
-    pass
+    raise RuntimeError("devtools is a build dependency")
 
 from astakos.version import __version__
 
diff --git a/snf-common/setup.py b/snf-common/setup.py
index 837ebb4..2a38cfe 100644
--- a/snf-common/setup.py
+++ b/snf-common/setup.py
@@ -44,11 +44,11 @@ from setuptools import setup, find_packages
 HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
 
 try:
-    # try to update the version file
-    from synnefo.util.version import update_version
+    # use devtools to update the version file
+    from devtools.version import update_version
     update_version('synnefo.versions', 'common', HERE)
 except ImportError:
-    pass
+    raise RuntimeError("devtools is a build dependency")
 
 from synnefo.versions.common import __version__
 
diff --git a/snf-cyclades-app/setup.py b/snf-cyclades-app/setup.py
index aed6479..806460f 100644
--- a/snf-cyclades-app/setup.py
+++ b/snf-cyclades-app/setup.py
@@ -44,11 +44,11 @@ from setuptools import setup, find_packages
 HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
 
 try:
-    # try to update the version file
-    from synnefo.util.version import update_version
+    # use devtools to update the version file
+    from devtools.version import update_version
     update_version('synnefo.versions', 'app', HERE)
 except ImportError:
-    pass
+    raise RuntimeError("devtools is a build dependency")
 
 from synnefo.versions.app import __version__
 
diff --git a/snf-cyclades-gtools/setup.py b/snf-cyclades-gtools/setup.py
index 6a2f9bd..f901801 100644
--- a/snf-cyclades-gtools/setup.py
+++ b/snf-cyclades-gtools/setup.py
@@ -40,11 +40,11 @@ from setuptools import setup
 HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
 
 try:
-    # try to update the version file
-    from synnefo.util.version import update_version
+    # use devtools to update the version file
+    from devtools.version import update_version
     update_version('synnefo.versions', 'ganeti', HERE)
 except ImportError:
-    pass
+    raise RuntimeError("devtools is a build dependency")
 
 from synnefo.versions.ganeti import __version__
 
diff --git a/snf-pithos-app/setup.py b/snf-pithos-app/setup.py
index c02ca92..96e16d4 100644
--- a/snf-pithos-app/setup.py
+++ b/snf-pithos-app/setup.py
@@ -42,12 +42,13 @@ from fnmatch import fnmatchcase
 from setuptools import setup, find_packages
 
 HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
+
 try:
-    # try to update the version file
-    from synnefo.util import version
-    version.update_version('pithos.api', 'version', HERE)
+    # use devtools to update the version file
+    from devtools.version import update_version
+    update_version('pithos.api', 'version', HERE)
 except ImportError:
-    pass
+    raise RuntimeError("devtools is a build dependency")
 
 from pithos.api.version import __version__
 
diff --git a/snf-pithos-backend/setup.py b/snf-pithos-backend/setup.py
index 968f137..757e6cf 100644
--- a/snf-pithos-backend/setup.py
+++ b/snf-pithos-backend/setup.py
@@ -42,12 +42,13 @@ from fnmatch import fnmatchcase
 from setuptools import setup, find_packages
 
 HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
+
 try:
-    # try to update the version file
-    from synnefo.util import version
-    version.update_version('pithos.backends', 'version', HERE)
+    # use devtools to update the version file
+    from devtools.version import update_version
+    update_version('pithos.backends', 'version', HERE)
 except ImportError:
-    pass
+    raise RuntimeError("devtools is a build dependency")
 
 from pithos.backends.version import __version__
 
diff --git a/snf-pithos-tools/setup.py b/snf-pithos-tools/setup.py
index 4e7c7cf..dd52407 100644
--- a/snf-pithos-tools/setup.py
+++ b/snf-pithos-tools/setup.py
@@ -44,12 +44,13 @@ from fnmatch import fnmatchcase
 from setuptools import setup, find_packages
 
 HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
+
 try:
-    # try to update the version file
-    from synnefo.util import version
-    version.update_version('pithos.tools', 'version', HERE)
+    # use devtools to update the version file
+    from devtools.version import update_version
+    update_version('pithos.tools', 'version', HERE)
 except ImportError:
-    pass
+    raise RuntimeError("devtools is a build dependency")
 
 from pithos.tools.version import __version__
 
diff --git a/snf-tools/setup.py b/snf-tools/setup.py
index 6408bff..fbf5613 100644
--- a/snf-tools/setup.py
+++ b/snf-tools/setup.py
@@ -44,11 +44,11 @@ from setuptools import setup, find_packages
 HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
 
 try:
-    # try to update the version file
-    from synnefo.util.version import update_version
+    # use devtools to update the version file
+    from devtools.version import update_version
     update_version('synnefo_tools', 'version', HERE)
 except ImportError:
-    pass
+    raise RuntimeError("devtools is a build dependency")
 
 from synnefo_tools.version import __version__
 
diff --git a/snf-webproject/setup.py b/snf-webproject/setup.py
index 10ee2bf..dd745a8 100644
--- a/snf-webproject/setup.py
+++ b/snf-webproject/setup.py
@@ -44,11 +44,11 @@ from setuptools import setup, find_packages
 HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
 
 try:
-    # try to update the version file
-    from synnefo.util.version import update_version
+    # use devtools to update the version file
+    from devtools.version import update_version
     update_version('synnefo.versions', 'webproject', HERE)
 except ImportError:
-    pass
+    raise RuntimeError("devtools is a build dependency")
 
 from synnefo.versions.webproject import __version__
 
diff --git a/version b/version
new file mode 100644
index 0000000..078b8e0
--- /dev/null
+++ b/version
@@ -0,0 +1,2 @@
+# This is a comment!
+0.14next
