diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 4ff5741..8201351 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -76,6 +76,57 @@ package name. Using multiple files looks like this:: .. _PEP0508: https://www.python.org/dev/peps/pep-0508/ .. _global-requirements.txt: https://git.openstack.org/cgit/openstack/requirements/tree/global-requirements.txt +Handling the package version +**************************** + +Distributions handle versions, especially pre-release versions differently. +SUSE for example allows using RPM's tilde ('~) while Fedora doesn't allow that +and uses a combination of RPM `Version` and `Release` tag to express pre-releases. +To support both styles with renderspec, the upstream version and a release +must be available in the context:: + + {% set upstream_version = '1.2.3.0rc1' %} + {% set rpm_release = '1' %} + +This should be done on the first lines in the spec.j2 template. The `rpm_release` is +only used in the fedora style. +Then for the RPM version and release, use:: + + Version: {{ py2rpmversion() }} + Release: {{ py2rpmrelease() }} + +For suse-style, this renders to:: + + Version: 1.2.3.0~rc1 + Release: 0 + +For fedora-style, this renders to:: + + Version: 1.2.3 + Release: 0.1.0rc1%{?dist} + +Note that in case of pre-releases you may need to adjust the version that is used +in the `Source` tag and the `%prep` sections `%setup`. So use e.g. :: + + {% set upstream_version = '1.2.3.0rc1' %} + {% set rpm_release = '1' %} + %name oslo.config + Version: {{ py2rpmversion() }} + Release: {{ py2rpmrelease() }} + Source0: https://pypi.io/packages/source/o/%{sname}/%{sname}-{{ upstream_version }}.tar.gz + %prep + %setup -q -n %{sname}-{{upstream_version}} + +which would render (with suse-style) to:: + + %name oslo.config + Version: 1.2.3.0~rc1 + Release: 0 + Source0: https://pypi.io/packages/source/o/%{sname}/%{sname}-1.2.3rc1.tar.gz + %prep + %setup -q -n %{sname}-1.2.3.0rc1 + + Template features ================= @@ -164,6 +215,47 @@ With the `suse` spec-style:: License: Apache-2.0 +context function `py2rpmversion` +******************************** +Python has a semantic version schema (see `PEP0440`_) and converting Python versions +to RPM compatible versions is needed in some cases. For example, in the Python world +the version "1.1.0a3" is lower than "1.1.0" but for RPM the version is higher. +To transform a Python version to a RPM compatible version, use:: + + {% set upstream_version = '1.1.0a3' %} + {% set rpm_release = '1' %} + + Version: {{ py2rpmversion() }} + +With the `suse` spec-style it will be translated to:: + + Version: 1.1.0~xalpha3 + +Note that you need to set 2 context variables (`upstream_version` and `rpm_release`) +to be able to use the `py2rpmversion()` function. + + +context function `py2rpmrelease` +******************************** +Fedora doesn't allow the usage of `~` (tilde) in the `Version` tag. So for pre-releases +the `Release` tag is used (see `Fedora Packaging Versioning`_) +For the fedora-style:: + + {% set upstream_version = '1.1.0a3' %} + {% set rpm_release = '1' %} + + Version: {{ py2rpmversion() }} + Release: {{ py2rpmrelease() }} + +this would render to:: + + Version: 1.1.0 + Release: 0.1a3%{?dist} + +Note that you need to set 2 context variables (`upstream_version` and `rpm_release`) +to be able to use the `py2rpmrelease()` function. + + distribution specific blocks & child templates ********************************************** @@ -199,3 +291,5 @@ For more information, see current `renderspec/dist-templates` and usage in .. _pymod2pkg: https://git.openstack.org/cgit/openstack/pymod2pkg .. _pypi.python.org: https://pypi.python.org/pypi .. _SPDX: https://spdx.org/licenses/ +.. _PEP0440: https://www.python.org/dev/peps/pep-0440/ +.. _Fedora Packaging Versioning: https://fedoraproject.org/wiki/Packaging:Versioning#Pre-Release_packages diff --git a/renderspec/__init__.py b/renderspec/__init__.py index cb1e138..31254e8 100644 --- a/renderspec/__init__.py +++ b/renderspec/__init__.py @@ -24,6 +24,8 @@ import sys from jinja2 import contextfilter from jinja2 import contextfunction from jinja2 import Environment +from jinja2.exceptions import TemplateRuntimeError +from packaging.version import parse import pymod2pkg import yaml @@ -32,6 +34,66 @@ from renderspec.distloader import RenderspecLoader from renderspec import versions +# a variable that needs to be set for some functions in the context +CONTEXT_VAR_UPSTREAM_VERSION = "upstream_version" +CONTEXT_VAR_RPM_RELEASE = "rpm_release" + + +def _context_check_variable(context, var_name, needed_by): + """check that the context has a given variable""" + if var_name not in context.vars: + raise TemplateRuntimeError("Variable '%s' not available in context but" + " needed for '%s'" % (var_name, needed_by)) + + +def _context_py2rpmversion(context): + """get a python PEP0440 compatible version and translate it to an RPM + version""" + # the context needs a variable set via {% set upstream_version = 'ver' %} + _context_check_variable(context, CONTEXT_VAR_UPSTREAM_VERSION, + 'py2rpmversion') + version = context.vars[CONTEXT_VAR_UPSTREAM_VERSION] + v_python = parse(version) + # fedora does not allow '~' in versions but uses a combination of Version + # and Release + # https://fedoraproject.org/wiki/Packaging:Versioning\#Pre-Release_packages + if context['spec_style'] == 'fedora': + return v_python.base_version + else: + v_rpm = v_python.public + if v_python.is_prerelease: + # we need to add the 'x' in front of alpha/beta releases 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', '~rc') + v_rpm = v_rpm.replace('.dev', '~dev') + return v_rpm + + +def _context_py2rpmrelease(context): + if context['spec_style'] == 'fedora': + # the context needs a var set via {% set upstream_version = 'ver' %} + _context_check_variable(context, CONTEXT_VAR_UPSTREAM_VERSION, + 'py2rpmrelease') + # the context needs a var set via {% set rpm_release = 'ver' %} + _context_check_variable(context, CONTEXT_VAR_RPM_RELEASE, + 'py2rpmrelease') + upstream_version = context.vars[CONTEXT_VAR_UPSTREAM_VERSION] + rpm_release = context.vars[CONTEXT_VAR_RPM_RELEASE] + v_python = parse(upstream_version) + if v_python.is_prerelease: + _, alphatag = v_python.public.split(v_python.base_version) + return '0.{}.{}%{{?dist}}'.format(rpm_release, + alphatag.lstrip('.')) + else: + return '{}%{{?dist}}'.format(rpm_release) + else: + # SUSE uses just '0'. The OpenBuildService handles the Release tag + return '0' + + def _context_epoch(context, pkg_name): """get the epoch (or 0 if unknown) for the given pkg name""" return context['epochs'].get(pkg_name, 0) @@ -115,6 +177,16 @@ def _globals_py2pkg(context, pkg_name, pkg_version=None): return _context_py2pkg(context, pkg_name, pkg_version) +@contextfunction +def _globals_py2rpmversion(context): + return _context_py2rpmversion(context) + + +@contextfunction +def _globals_py2rpmrelease(context): + return _context_py2rpmrelease(context) + + @contextfunction def _globals_epoch(context, value): return _context_epoch(context, value) @@ -133,6 +205,8 @@ def _globals_py2name(context, value): def _env_register_filters_and_globals(env): """register all the jinja2 filters we want in the environment""" env.filters['epoch'] = _filter_epoch + env.globals['py2rpmversion'] = _globals_py2rpmversion + env.globals['py2rpmrelease'] = _globals_py2rpmrelease env.globals['py2pkg'] = _globals_py2pkg env.globals['py2name'] = _globals_py2name env.globals['epoch'] = _globals_epoch diff --git a/tests.py b/tests.py index cc84bd5..9538b22 100644 --- a/tests.py +++ b/tests.py @@ -185,6 +185,56 @@ class RenderspecTemplateFunctionTests(unittest.TestCase): template.render(**context), expected_result) + @data( + ('suse', '1.1.0', '1.1.0'), + ('suse', '1.1.0.post2', '1.1.0.post2'), + ('suse', '1.1.0dev10', '1.1.0~dev10'), + ('suse', '1.1.0a10', '1.1.0~xalpha10'), + ('suse', '1.1.0a10dev5', '1.1.0~xalpha10~dev5'), + ('suse', '1.1.0b10', '1.1.0~xbeta10'), + ('suse', '1.1.0rc2', '1.1.0~rc2'), + ('suse', '1.1.0rc2dev2', '1.1.0~rc2~dev2'), + ('fedora', '1.1.0', '1.1.0'), + ('fedora', '1.1.0b10', '1.1.0'), + ('fedora', '1.1.0rc2dev2', '1.1.0'), + ) + @unpack + def test_render_func_py2rpmversion(self, style, py_ver, rpm_ver): + context = {'spec_style': style, 'epochs': {}, 'requirements': {}} + # need to escape '{' and '}' here + s = "{{% set upstream_version = '{}' %}}{{{{ py2rpmversion() }}}}"\ + .format(py_ver) + template = self.env.from_string(s) + self.assertEqual( + template.render(**context), + rpm_ver) + + @data( + ('suse', '1.1.0', '1', '0'), + ('suse', '1.1.0.post2', '1', '0'), + ('suse', '1.1.0dev10', '2', '0'), + ('fedora', '1.1.0', '1', '1%{?dist}'), + # ('fedora', '1.1.0.post2', '1', 'FIXME'), + ('fedora', '1.1.0dev10', '1', '0.1.dev10%{?dist}'), + ('fedora', '1.1.0a10', '1', '0.1.a10%{?dist}'), + ('fedora', '1.1.0a10dev5', '1', '0.1.a10.dev5%{?dist}'), + ('fedora', '1.1.0b10', '1', '0.1.b10%{?dist}'), + ('fedora', '1.1.0rc2', '5', '0.5.rc2%{?dist}'), + ('fedora', '1.1.0rc2dev2', '1', '0.1.rc2.dev2%{?dist}'), + ) + @unpack + def test_render_func_py2rpmrelease(self, style, upstream_ver, rpm_release, + rpm_release_expected): + context = {'spec_style': style, 'epochs': {}, 'requirements': {}} + # need to escape '{' and '}' here + s = "{{% set upstream_version = '{}' %}}" \ + "{{% set rpm_release = '{}' %}}" \ + "{{{{ py2rpmrelease() }}}}".format(upstream_ver, rpm_release) + template = self.env.from_string(s) + self.assertEqual( + template.render(**context), + rpm_release_expected) + class RenderspecVersionsTests(unittest.TestCase): def test_without_version(self):