2025-07-09 03:47:49 +02:00
|
|
|
|
#!/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'
|
2025-11-13 17:07:23 +01:00
|
|
|
|
,help='append (or replace) a suffix (attached with ~); False -> remove suffix')
|
2025-07-09 03:47:49 +02:00
|
|
|
|
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')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-11-13 17:07:23 +01:00
|
|
|
|
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)
|
2025-07-09 03:47:49 +02:00
|
|
|
|
if not mat:
|
2025-11-13 17:07:23 +01:00
|
|
|
|
__FAIL ('invalid version syntax in "'+verStr+'"')
|
|
|
|
|
|
else:
|
|
|
|
|
|
return mat
|
2025-07-09 03:47:49 +02:00
|
|
|
|
|
2025-11-13 17:07:23 +01:00
|
|
|
|
|
|
|
|
|
|
def rebuild (version, bump=None, suffix=None, snapshot=False):
|
|
|
|
|
|
mat = parseVerNr (version)
|
2025-07-09 03:47:49 +02:00
|
|
|
|
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:
|
2025-11-13 17:07:23 +01:00
|
|
|
|
if not evalBool(suffix):
|
|
|
|
|
|
suf = None
|
|
|
|
|
|
else:
|
|
|
|
|
|
suf = suffix
|
2025-07-09 03:47:49 +02:00
|
|
|
|
|
|
|
|
|
|
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:
|
2025-11-13 17:07:23 +01:00
|
|
|
|
__FAIL ('invoking git '+argStr)
|
2025-07-09 03:47:49 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-11-13 17:07:23 +01:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-07-09 03:47:49 +02:00
|
|
|
|
|
|
|
|
|
|
def __ERR (*args, **kwargs):
|
|
|
|
|
|
print (*args, file=sys.stderr, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
def __FAIL (msg):
|
|
|
|
|
|
__ERR ("FAILURE: "+msg)
|
|
|
|
|
|
exit (-1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__=='__main__':
|
|
|
|
|
|
parseAndBuild()
|