#!/usr/bin/env python3
"""
# Make program executable
chmod u+x code.py
# Make it known under various names
ln -s code.py get-version
ln -s code.py get-tag
ln -s code.py new-release-major
ln -s code.py new-release-minor
ln -s code.py new-release-build
ln -s code.py new-dev
get-version: Get the newest version at or below the current revision.
get-number: Get the number related to a version.
new-release-major: Invent a new major release version
new-release-minor: Invent a new minor release version
new-release-build: Invent a new build release version
new-dev: Invent a new development version
"""
import sys, os, subprocess, re
def exe_cmd(cmd, env=None):
"""
Execute a shell command.
@param cmd: List of command-line arguments.
@param env: Environment variables to set (dict of key/value pairs)
@return: Lines of output.
@note: If command fails, program is aborted.
"""
if dump_commands:
print("CMD: {}".format(" ".join(escape_name(arg) for arg in cmd)))
if env is not None:
new_env = os.environ.copy()
new_env.update(env)
else:
new_env = None
lines = output.splitlines()
return lines
def get_git_tag(match=None):
"""
Query git for the current-label, return prefix + label if possible.
"""
cmd = ['git', 'describe']
if match is not None:
cmd.append('--match=' + match)
if sys.version_info.minor < 5:
# Pre 3.5 has no subprocess.run
output = subprocess.check_output(cmd, universal_newlines=True)
else:
output = subprocess.run(cmd, check=True, stdout=subprocess.PIPE,
universal_newlines=True).stdout
lines = [line for line in output.splitlines() if line]
if len(lines) != 1:
return None
line = lines[0]
# Either the current commit is a tag, or it is a commit above a tag.
parts = line.split('-')
label = parse_tag(parts[-1])
if label:
return "-".join(parts[:-1]), label
if len(parts) > 1:
label = parse_tag(parts[-2])
if label:
return "-".join(parts[:-2]), label
if len(parts) > 2:
label = parse_tag(parts[-3])
if label:
return "-".join(parts[:-3]), label
return None, None
def get_git_label(match=None):
prefix, label = get_git_tag(match)
return label
NUMBITS_MAJOR = 4
NUMBITS_MINOR = 8
NUMBITS_RELEASE = 1
NUMBITS_BUILD = 31 - NUMBITS_RELEASE - NUMBITS_MINOR - NUMBITS_MAJOR
MAX_MAJOR = 2 ** NUMBITS_MAJOR - 1
MAX_MINOR = 2 ** NUMBITS_MINOR - 1
MAX_RELEASE = 2 ** NUMBITS_RELEASE - 1
MAX_BUILD = 2 ** NUMBITS_BUILD - 1
assert NUMBITS_BUILD > 0
def good_version(major, minor, release, build):
"""
Verify whether the 4 provided numbers are within boundaries of a version number.
"""
if major < 0 or major > MAX_MAJOR: return False
if minor < 0 or minor > MAX_MINOR: return False
if release < 0 or release > MAX_RELEASE: return False
if build < 0 or build > MAX_BUILD: return False
return True
class VersionLabel:
"""
Object holding a version. Constructor crashes if an incorret number
is supplied, check against 'good_version' first.
"""
def __init__(self, major, minor, release, build):
assert good_version(major, minor, release, build)
self.major = major
self.minor = minor
self.release = release
self.build = build
def to_tag(self, all_numbers=False):
"""
Convert version label instance to tag text.
"""
parts = []
if self.release == 0:
parts.append('dev')
else:
parts.append('rel')
parts.append(str(self.major))
if not all_numbers and self.minor == 0 and self.build == 0:
return "".join(parts)
parts.extend(['.', str(self.minor)])
if not all_numbers and self.build == 0:
return "".join(parts)
parts.extend(['.', str(self.build)])
return "".join(parts)
def to_id(self):
"""
Convert version label instance to numeric id.
"""
value = self.build
shift = NUMBITS_BUILD
value = value + (self.release << shift)
shift = shift + NUMBITS_RELEASE
value = value + (self.minor << shift)
shift = shift + NUMBITS_MINOR
value = value + (self.major << shift)
# 'value' is at most 31 bits, so always fits in a DWORD.
return value
def next_major(self):
assert self.release == 1
if self.major >= MAX_MAJOR:
return None
return VersionLabel(self.major + 1, 0, 1, 0)
def next_minor(self):
assert self.release == 1
if self.minor >= MAX_MINOR:
return None
return VersionLabel(self.major, self.minor + 1, 1, 0)
def next_build(self):
if self.build >= MAX_BUILD:
return None
return VersionLabel(self.major, self.minor, self.release, self.build + 1)
def next_dev(self):
if self.release == 0:
return self.next_build()
else:
label = self.next_minor()
label.release = 0
return label
TAG_PATTERN = re.compile("(rel|dev)([0-9]+)(?:\\.([0-9]+)(?:\\.([0-9]+))?)?$")
def parse_tag(text):
"""
Convert a tag text like "rel1.3.25" to a VersionLabel if allowed.
"""
m = TAG_PATTERN.search(text)
if m:
if m.group(1) == 'rel':
release = 1
else:
release = 0
major = int(m.group(2))
if m.group(3) is not None:
minor = int(m.group(3))
else:
minor = 0
if m.group(4) is not None:
build = int(m.group(4))
else:
build = 0
if good_version(major, minor, release, build):
return VersionLabel(major, minor, release, build)
return None
def parse_id(value):
"""
Convert a numeric id to a VersionLabel, if allowed.
"""
build = value & MAX_BUILD
value = value >> NUMBITS_BUILD
release = value & MAX_RELEASE
value = value >> NUMBITS_RELEASE
minor = value & MAX_MINOR
value = value >> NUMBITS_MINOR
major = value & MAX_MAJOR
if good_version(major, minor, release, build):
return VersionLabel(major, minor, release, build)
return None
#
# get-version: Get the newest version at or below the current revision.
# get-tag: Get the current tag (so it can be checked out)
# get-number: Get the number related to a version.
# new-release-major: Invent a new major release version
# new-release-minor: Invent a new minor release version
# new-release-build: Invent a new build release version
# new-dev: Invent a new development version
# Dirty hacky way to get several 'binaries' in the same program
if sys.argv[0].endswith('get-version'):
label = get_git_label()
if label is None:
print("Cannot find a git tag with a proper name")
sys.exit(1)
print(label.to_id())
sys.exit(0)
elif sys.argv[0].endswith('get-tag'):
prefix, label = get_git_tag()
if prefix is None:
print("Cannot find a git tag with a proper name")
sys.exit(1)
print(prefix + "-" + label.to_tag())
sys.exit(0)
elif sys.argv[0].endswith('get-label'):
label = get_git_label()
if label is None:
print("Cannot find a git tag with a proper name")
sys.exit(1)
print(label.to_tag())
sys.exit(0)
elif sys.argv[0].endswith('new-release-major'):
prefix, label = get_git_tag("*-rel[0-9]*")
if label is None:
print("Cannot find a git tag with a proper name")
sys.exit(1)
label = label.next_major()
if label is None:
print("Cannot create a new major release version, maximum major number reached")
sys.exit(1)
print(prefix + "-" + label.to_tag())
sys.exit(0)
elif sys.argv[0].endswith('new-release-minor'):
prefix, label = get_git_tag("*-rel[0-9]*")
if label is None:
print("Cannot find a git tag with a proper name")
sys.exit(1)
label = label.next_minor()
if label is None:
print("Cannot create a new major release version, maximum minor number reached")
sys.exit(1)
print(prefix + "-" + label.to_tag())
sys.exit(0)
elif sys.argv[0].endswith('new-release-build'):
prefix, label = get_git_tag("*-rel[0-9]*")
if label is None:
print("Cannot find a git tag with a proper name")
sys.exit(1)
label = label.next_build()
if label is None:
print("Cannot create a new major release version, maximum build number reached")
sys.exit(1)
print(prefix + "-" + label.to_tag())
sys.exit(0)
elif sys.argv[0].endswith('new-dev'):
prefix, label = get_git_tag()
if label is None:
print("Cannot find a git tag with a proper name")
sys.exit(1)
label = label.next_dev()
if label is None:
print("Cannot create a new development version, maximum minor number reached")
sys.exit(1)
print(prefix + "-" + label.to_tag())
sys.exit(0)
elif sys.argv[0].endswith('get-info'):
print("Version: \"[rel|dev][major].[minor].[build]\"")
print("Min/max value of major: 0..{}".format(MAX_MAJOR))
print("Min/max value of minor: 0..{}".format(MAX_MINOR))
print("Min/max value of build: 0..{}".format(MAX_BUILD))
sys.exit(0)
else:
print("Unknown executable name, bye!")
sys.exit(1)