obs-service-set_version/set_version
2017-11-06 09:52:36 +01:00

378 lines
13 KiB
Python
Executable file

#!/usr/bin/python
# -*- coding: utf-8 -*-
# A simple script to update version number in spec, dsc or arch linux files
#
# (C) 2010 by Adrian Schröter <adrian@suse.de>
# (C) 2015 by Thomas Bechtold <tbechtold@suse.com>
#
# This program 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 http://www.gnu.org/licenses/gpl-2.0.html for full license text.
from __future__ import print_function
import argparse
import glob
import os
import re
import shutil
import sys
import tarfile
import zipfile
try:
from packaging.version import LegacyVersion, Version, parse
except ImportError as exc:
HAS_PACKAGING = False
import warnings
warnings.warn("install 'packaging' to improve python package versions",
RuntimeWarning)
else:
HAS_PACKAGING = True
outdir = None
suffixes = ('obscpio', 'tar', 'tar.gz', 'tgz', 'tar.bz2', 'tbz2', 'tar.xz',
'zip')
suffixes_re = "|".join(map(lambda x: re.escape(x), suffixes))
def _get_local_files():
""" sorted local file list by modification time (newest first)"""
files = glob.glob('*')
files.sort(key=lambda x: os.stat(os.path.join(os.getcwd(), x)).st_mtime,
reverse=True)
return files
class VersionDetector(object):
@staticmethod
def _autodetect(files, basename):
version = VersionDetector._get_version_via_obsinfo(
files, basename)
if not version:
version = VersionDetector._get_version_via_archive_dirname(
files, basename)
if not version:
version = VersionDetector._get_version_via_filename(
files, basename)
if not version:
version = VersionDetector._get_version_via_debian_changelog(
"debian.changelog")
return version
@staticmethod
def _get_version_via_filename(files, basename):
""" detect version based on file names"""
for f in files:
regex = r"^%s.*[-_]([\d].*)\.(?:%s)$" % (re.escape(basename),
suffixes_re)
m = re.match(regex, f)
if m:
return m.group(1)
# Nothing found
return None
@staticmethod
def __get_version(str_list, basename):
regex = "%s.*[-_]([\d][^\/]*).*" % basename
for s in str_list:
m = re.match(regex, s)
if m:
return m.group(1)
# Nothing found
return None
@staticmethod
def _get_version_via_archive_dirname(files, basename):
""" detect version based tar'd directory name"""
for f in filter(lambda x: x.endswith(suffixes), files):
# handle tarfiles
if tarfile.is_tarfile(f):
with tarfile.open(f) as tf:
v = VersionDetector.__get_version(tf.getnames(), basename)
if v:
return v
# handle zipfiles
if zipfile.is_zipfile(f):
with zipfile.ZipFile(f, 'r') as zf:
v = VersionDetector.__get_version(zf.namelist(), basename)
if v:
return v
# Nothing found
return None
@staticmethod
def _get_version_via_obsinfo(files, basename):
join_suffix = basename + ".obsinfo"
for filename in filter(lambda x: x.endswith(join_suffix), files):
if os.path.exists(filename):
with open(filename, "r") as fp:
for line in fp:
if line.startswith("version: "):
string = line[9:]
string = string.rstrip()
return string
# Nothing found
return None
@staticmethod
def _get_version_via_debian_changelog(filename):
# from http://anonscm.debian.org/cgit/pkg-python-debian/\
# python-debian.git/tree/lib/debian/changelog.py
topline = re.compile(r'^(\w%(name_chars)s*) \(([^\(\) \t]+)\)'
'((\s+%(name_chars)s+)+)\;'
% {'name_chars': '[-+0-9a-z.]'},
re.IGNORECASE)
if os.path.exists(filename):
with open(filename, "r") as f:
firstline = f.readline()
topmatch = topline.match(firstline)
if topmatch:
return topmatch.group(2)
# Nothing found
return None
@staticmethod
def _get_version_via_debian_dsc(filename):
version = re.compile(r'^Version:([ \t\f\v]*)[^%\n\r]*', re.IGNORECASE)
if os.path.exists(filename):
with open(filename, "r") as f:
for line in f:
versionmatch = version.match(line)
if versionmatch:
return versionmatch.group(0)
# Nothing found
return None
class PackageTypeDetector(object):
@staticmethod
def _get_package_type(files):
pt_found = False
for f in filter(lambda x: x.endswith(suffixes), files):
pt_found = PackageTypeDetector._is_python(f)
if pt_found:
return "python"
# no package type found
return None
@staticmethod
def _is_python(f):
names = []
if tarfile.is_tarfile(f):
with tarfile.open(f) as tf:
names = tf.getnames()
if zipfile.is_zipfile(f):
with zipfile.ZipFile(f, 'r') as zf:
names = zf.namelist()
for n in map(lambda x: os.path.normpath(x), names):
if n.endswith("egg-info/PKG-INFO"):
return True
return False
def _replace_define(filename, def_name, def_value, add_if_missing=True):
# first, modify a copy of filename and then move it
with open(filename, 'r+') as f:
contents = f.read()
f.seek(0)
contents_new, subs = re.subn(
r'^%define {def_name}(\s*)[^%].*'.format(
def_name=def_name),
r'%define {def_name}\g<1>{def_value}'.format(
def_name=def_name, def_value=def_value),
contents, flags=re.MULTILINE)
if subs == 0 and add_if_missing:
# seems there was no define. add new one before 'Name:'
contents_new, subs = re.subn(
r'^(Name:.*)$',
r'%define {def_name} {def_value}\n\n\g<1>'.format(
def_name=def_name, def_value=def_value),
contents, flags=re.MULTILINE)
f.truncate()
f.write(contents_new)
def _replace_spec_setup(filename, version_define):
# first, modify a copy of filename and then move it
with open(filename, 'r+') as f:
contents = f.read()
f.seek(0)
# %setup without "-n" uses implicit "-n" as "%{name}-%{version}"
contents_new, subs = re.subn(
r'^%setup\s*((?:-q)?)?\s*$',
r'%setup \1 -n %{{name}}-%{{{version_define}}}'.format(
version_define=version_define),
contents, flags=re.MULTILINE)
if subs == 0:
# keep inline macros for rpm
contents_new, subs = re.subn(
r'^%setup(.*)%{version}(.*)$',
r'%setup\g<1>%{{{version_define}}}\g<2>'.format(
version_define=version_define),
contents, flags=re.MULTILINE)
if subs > 0:
f.truncate()
f.write(contents_new)
def _replace_tag(filename, tag, string):
# first, modify a copy of filename and then move it
with open(filename, 'r+') as f:
contents = f.read()
f.seek(0)
if filename.endswith("PKGBUILD") or filename.endswith("build.collax"):
contents_new, subs = re.subn(
r"^{tag}=.*".format(tag=tag),
r"{tag}={string}".format(tag=tag, string=string), contents,
flags=re.MULTILINE)
else:
# keep inline macros for rpm
contents_new, subs = re.subn(
r'^{tag}:([ \t\f\v]*)[^%\n\r]*'.format(tag=tag),
r'{tag}:\g<1>{string}'.format(
tag=tag, string=string),
contents, flags=re.MULTILINE)
if subs > 0:
f.truncate()
f.write(contents_new)
def _replace_debian_changelog_version(filename, version_new):
# first, modify a copy of filename and then move it
# get current version
version_current = VersionDetector._get_version_via_debian_changelog(
filename)
with open(filename, 'r+') as f:
content_lines = f.readlines()
f.seek(0)
content_lines[0] = content_lines[0].replace(
version_current, version_new, 1)
f.truncate()
f.writelines(content_lines)
def _version_python_pip2rpm(version_pip):
"""generate a rpm compatible version from a python pip version"""
version_rpm = version_pip
if not HAS_PACKAGING:
return version_rpm
v = parse(version_pip)
if isinstance(v, Version):
if v.is_prerelease:
v_rpm = v.public
# we need to add the 'x' in front of alpha/beta release because
# in the python world, "1.1a10" > "1.1.dev10"
# but in the rpm world, "1.1~a10" < "1.1~dev10"
v_rpm = v_rpm.replace('a', '~xalpha')
v_rpm = v_rpm.replace('b', '~xbeta')
v_rpm = v_rpm.replace('rc', '~xrc')
v_rpm = v_rpm.replace('.dev', '~dev')
version_rpm = v_rpm
elif isinstance(v, LegacyVersion):
# TODO(toabctl): handle setuptools style legacy version
pass
return version_rpm
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Open Build Service source service "set_version".'
'Used to update build description files with a '
'detected or given version number.')
parser.add_argument('--outdir', required=True,
help='output directory for modified sources')
parser.add_argument('--version',
help='use given version string, do not detect it '
'from source files')
parser.add_argument('--basename', default="",
help='detect version based on the file name with '
'a given prefix')
parser.add_argument('--file', action='append',
help='modify only this build description. '
'maybe used multiple times.')
args = vars(parser.parse_args())
version = args['version']
files_local = _get_local_files()
outdir = args['outdir']
if not outdir:
print("no outdir specified")
sys.exit(-1)
if not version:
version = VersionDetector._autodetect(files_local, args["basename"])
if not version:
print("unable to detect the version")
sys.exit(-1)
# if no files explicitly specified process whole directory
files = args['file'] or files_local
# do version convertion if needed
pack_type = PackageTypeDetector._get_package_type(files)
version_converted = None
if pack_type == "python":
version_converted = _version_python_pip2rpm(version)
# handle rpm specs
for f in filter(lambda x: x.endswith(".spec"), files):
filename = outdir + "/" + f
shutil.copyfile(f, filename)
_replace_define(filename, "version_unconverted", version,
add_if_missing=False)
if version_converted and version_converted != version:
_replace_define(filename, "version_unconverted", version)
_replace_tag(filename, 'Version', version_converted)
_replace_spec_setup(filename, "version_unconverted")
else:
_replace_tag(filename, 'Version', version)
_replace_tag(filename, 'Release', "0")
# handle debian packages
# append -0 only for non-native packages, otherwise native packages
# will be half-converted to non-native and break dpkg-buildpackage
for f in filter(lambda x: x.endswith(".dsc"), files):
filename = outdir + "/" + f
shutil.copyfile(f, filename)
if "-" in VersionDetector._get_version_via_debian_dsc(filename):
_replace_tag(filename, 'Version', version + "-0")
else:
_replace_tag(filename, 'Version', version)
for f in filter(lambda x: x.endswith(("debian.changelog")), files):
filename = outdir + "/" + f
shutil.copyfile(f, filename)
if "-" in VersionDetector._get_version_via_debian_changelog(filename):
_replace_debian_changelog_version(filename, version + "-0")
else:
_replace_debian_changelog_version(filename, version)
# handle build.collax recipes
for f in filter(lambda x: x.endswith(("build.collax")), files):
filename = outdir + "/" + f
shutil.copyfile(f, filename)
_replace_tag(filename, "version", version)
_replace_tag(filename, "build", "0")
# handle arch linux PKGBUILD files
# TODO: Handle the md5sums generation!
for f in filter(lambda x: x.endswith(("PKGBUILD")), files):
filename = outdir + "/" + f
shutil.copyfile(f, filename)
_replace_tag(filename, "md5sums", "('SKIP')")
_replace_tag(filename, "sha256sums", "('SKIP')")
_replace_tag(filename, "pkgver", version)
_replace_tag(filename, "pkgrel", "0")