#!/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)