Turns out that in practice there will be (at least temporarily) some version-tags including a suffix: the RC-versions! Now there is the special twist, that Git does not allow '~' in Tag names, and thus `git-buildpackage` introduced an additional layer of translation. So we are forced to revert that translation, which is possible, since the basic Debian version syntax disallows '_' in version numbers (because '_' is used to separate the package name from the version number). It seems prudent to implement that as an preprocessing step and thus keep it out of the regular version number syntax. Furthermore we need the ability to handle existing suffixes, which (as we know now) can be picked up from the Git history. * my decision is to allow both pass-through and suppressing them * use `--suffix=False` to suppress / remove any existing suffix * the latter now allows us also to automate the setting of the final Release version
183 lines
5.3 KiB
Python
Executable file
183 lines
5.3 KiB
Python
Executable file
#!/usr/bin/python3
|
||
# coding: utf-8
|
||
##
|
||
## buildVersion.py - extract and possibly bump current version from Git
|
||
##
|
||
|
||
# Copyright (C)
|
||
# 2025, Hermann Vosseler <Ichthyostega@web.de>
|
||
#
|
||
# **Lumiera** is free software; you can redistribute it and/or modify it
|
||
# under the terms of the GNU General Public License as published by the
|
||
# Free Software Foundation; either version 2 of the License, or (at your
|
||
# option) any later version. See the file COPYING for further details.
|
||
#####################################################################
|
||
'''
|
||
Build and possibly bump a current project version spec,
|
||
based on the nearest Git tag.
|
||
'''
|
||
|
||
import re
|
||
import os
|
||
import sys
|
||
import datetime
|
||
import argparse
|
||
import subprocess
|
||
|
||
#------------CONFIGURATION----------------------------
|
||
CMDNAME = os.path.basename(__file__)
|
||
TAG_PAT = 'v*.*'
|
||
VER_SEP = r'(?:^v?|\.)'
|
||
VER_NUM = r'(\w[\w\+]*)'
|
||
VER_SUB = r'(?:'+VER_SEP+VER_NUM+')'
|
||
VER_SUF = r'(?:~('+VER_NUM+VER_SUB+'?'+'))'
|
||
VER_SYNTAX = VER_SUB +VER_SUB+'?' +VER_SUB+'?' +VER_SUF+'?'
|
||
GIT = 'git'
|
||
#------------CONFIGURATION----------------------------
|
||
|
||
|
||
|
||
def parseAndBuild():
|
||
''' main: parse cmdline and generate version string '''
|
||
parser = argparse.ArgumentParser (prog=CMDNAME, description='%s: %s' % (CMDNAME, __doc__)
|
||
,formatter_class=argparse.RawDescriptionHelpFormatter)
|
||
|
||
parser.add_argument ('--bump','-b'
|
||
,nargs='?'
|
||
,choices=['maj','min','rev'], const='rev'
|
||
,help='bump the version detected from Git (optionally bump a specific component)')
|
||
parser.add_argument ('--suffix','-s'
|
||
,help='append (or replace) a suffix (attached with ~); False -> remove suffix')
|
||
parser.add_argument ('--snapshot'
|
||
,action='store_true'
|
||
,help='mark as development snapshot by appending ~dev.YYYYMMDDhhmm, using UTC date from HEAD commit')
|
||
|
||
opts = parser.parse_args()
|
||
|
||
version = getVersionFromGit()
|
||
version = rebuild (version, **vars(opts))
|
||
print (version)
|
||
|
||
|
||
def getVersionFromGit():
|
||
get_nearest_matching_tag = 'describe --tags --abbrev=0 --match=' + TAG_PAT
|
||
return runGit (get_nearest_matching_tag)
|
||
|
||
def getTimestampFromGit():
|
||
get_head_author_date = 'show -s --format=%ai'
|
||
timespec = runGit (get_head_author_date)
|
||
timespec = datetime.datetime.fromisoformat (timespec)
|
||
timespec = timespec.astimezone (datetime.timezone.utc) # note: convert into UTC
|
||
return timespec.strftime ('%Y%m%d%H%M')
|
||
|
||
|
||
|
||
def parseVerNr (verStr):
|
||
""" parse a version spec from a git tag,
|
||
possibly preprocess to translate _ -> ~
|
||
"""
|
||
NOT_SFX = r'(?:[^_\W]|[\.\+])+'
|
||
DECODE = r'('+NOT_SFX+')(?:_('+NOT_SFX+'))?'
|
||
#
|
||
mat = re.fullmatch (DECODE, verStr)
|
||
if not mat:
|
||
__FAIL ('version string contains invalid characters: "'+verStr+'"')
|
||
verStr = mat.group(1)
|
||
if mat.group(2):
|
||
verStr += '~'+mat.group(2)
|
||
#
|
||
# check syntax of translated version spec
|
||
mat = re.fullmatch (VER_SYNTAX, verStr)
|
||
if not mat:
|
||
__FAIL ('invalid version syntax in "'+verStr+'"')
|
||
else:
|
||
return mat
|
||
|
||
|
||
def rebuild (version, bump=None, suffix=None, snapshot=False):
|
||
mat = parseVerNr (version)
|
||
maj = mat.group(1)
|
||
min = mat.group(2)
|
||
rev = mat.group(3)
|
||
suf = mat.group(4)
|
||
suf_idi = mat.group(5) # detail structure not used (as of 2025)
|
||
suf_num = mat.group(6)
|
||
|
||
if bump=='maj':
|
||
maj = bumpedNum(maj)
|
||
min = None
|
||
rev = None
|
||
elif bump=='min':
|
||
min = bumpedNum(min)
|
||
rev = None
|
||
elif bump=='rev':
|
||
rev = bumpedNum(rev)
|
||
|
||
if snapshot:
|
||
suf = 'dev.'+getTimestampFromGit()
|
||
elif suffix:
|
||
if not evalBool(suffix):
|
||
suf = None
|
||
else:
|
||
suf = suffix
|
||
|
||
version = maj
|
||
if min:
|
||
version += '.'+min
|
||
elif not min and rev:
|
||
version += '.0'
|
||
if rev:
|
||
version += '.'+rev
|
||
if suf:
|
||
version += '~'+suf
|
||
return version
|
||
|
||
|
||
|
||
def bumpedNum (verStr):
|
||
mat = re.match (r'\d+', str(verStr))
|
||
if not mat:
|
||
return '1'
|
||
numStr = mat.group(0)
|
||
num = int(numStr) + 1
|
||
return str(num).zfill(len(numStr))
|
||
|
||
|
||
def runGit (argStr):
|
||
''' run Git as system command without shell and retrieve the output '''
|
||
argList = [GIT] + argStr.split()
|
||
try:
|
||
proc = subprocess.run (argList, check=True, capture_output=True, encoding='utf-8', env={'LC_ALL':'C'})
|
||
return proc.stdout.rstrip() # Note: sanitised env
|
||
except:
|
||
__FAIL ('invoking git '+argStr)
|
||
|
||
|
||
|
||
def evalBool (val) ->bool:
|
||
""" evaluate as bool value
|
||
@author: Tim Poulsen
|
||
@note: Adapted from the original, published 2023, CC-By-SA-4
|
||
https://www.timpoulsen.com/2023/python-bool-from-any.html
|
||
"""
|
||
try:
|
||
return float(val) > 0
|
||
except:
|
||
if type(val) is str:
|
||
return val.lower() not in ['false', 'no', 'n', 'none', 'null']
|
||
else:
|
||
# rely on Python's type coercion rules
|
||
return bool(val)
|
||
|
||
|
||
|
||
def __ERR (*args, **kwargs):
|
||
print (*args, file=sys.stderr, **kwargs)
|
||
|
||
def __FAIL (msg):
|
||
__ERR ("FAILURE: "+msg)
|
||
exit (-1)
|
||
|
||
|
||
if __name__=='__main__':
|
||
parseAndBuild()
|