root / devflow / versioning.py @ 9b0839dd
History | View | Annotate | Download (14.5 kB)
1 |
#!/usr/bin/env python
|
---|---|
2 |
#
|
3 |
# Copyright (C) 2012, 2013 GRNET S.A. All rights reserved.
|
4 |
#
|
5 |
# Redistribution and use in source and binary forms, with or
|
6 |
# without modification, are permitted provided that the following
|
7 |
# conditions are met:
|
8 |
#
|
9 |
# 1. Redistributions of source code must retain the above
|
10 |
# copyright notice, this list of conditions and the following
|
11 |
# disclaimer.
|
12 |
#
|
13 |
# 2. Redistributions in binary form must reproduce the above
|
14 |
# copyright notice, this list of conditions and the following
|
15 |
# disclaimer in the documentation and/or other materials
|
16 |
# provided with the distribution.
|
17 |
#
|
18 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
|
19 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
20 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
21 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
|
22 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
23 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
24 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
25 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
26 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
27 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
28 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
29 |
# POSSIBILITY OF SUCH DAMAGE.
|
30 |
#
|
31 |
# The views and conclusions contained in the software and
|
32 |
# documentation are those of the authors and should not be
|
33 |
# interpreted as representing official policies, either expressed
|
34 |
# or implied, of GRNET S.A.
|
35 |
|
36 |
"""Helper functions for automatic version computation.
|
37 |
|
38 |
This module contains helper functions for extracting information
|
39 |
from a Git repository, and computing the python and debian version
|
40 |
of the repository code.
|
41 |
|
42 |
"""
|
43 |
|
44 |
import os |
45 |
import re |
46 |
import sys |
47 |
|
48 |
from distutils import log # pylint: disable=E0611 |
49 |
|
50 |
from devflow import BRANCH_TYPES, BASE_VERSION_FILE, VERSION_RE |
51 |
from devflow import utils |
52 |
|
53 |
|
54 |
DEFAULT_VERSION_FILE = """
|
55 |
__version__ = "%(DEVFLOW_VERSION)s"
|
56 |
__version_vcs_info__ = {
|
57 |
'branch': '%(DEVFLOW_BRANCH)s',
|
58 |
'revid': '%(DEVFLOW_REVISION_ID)s',
|
59 |
'revno': %(DEVFLOW_REVISION_NUMBER)s}
|
60 |
__version_user_email__ = "%(DEVFLOW_USER_EMAIL)s"
|
61 |
__version_user_name__ = "%(DEVFLOW_USER_NAME)s"
|
62 |
"""
|
63 |
|
64 |
|
65 |
def get_base_version(vcs_info): |
66 |
"""Determine the base version from a file in the repository"""
|
67 |
|
68 |
f = open(os.path.join(vcs_info.toplevel, BASE_VERSION_FILE))
|
69 |
lines = [l.strip() for l in f.readlines()] |
70 |
lines = [l for l in lines if not l.startswith("#")] |
71 |
if len(lines) != 1: |
72 |
raise ValueError("File '%s' should contain a single non-comment line.") |
73 |
f.close() |
74 |
return lines[0] |
75 |
|
76 |
def validate_version(base_version, vcs_info): |
77 |
branch = vcs_info.branch |
78 |
|
79 |
brnorm = utils.normalize_branch_name(branch) |
80 |
btypestr = utils.get_branch_type(branch) |
81 |
|
82 |
try:
|
83 |
btype = BRANCH_TYPES[btypestr] |
84 |
except KeyError: |
85 |
allowed_branches = ", ".join(x for x in BRANCH_TYPES.keys()) |
86 |
raise ValueError("Malformed branch name '%s', cannot classify as one " |
87 |
"of %s" % (btypestr, allowed_branches))
|
88 |
|
89 |
if btype.versioned:
|
90 |
try:
|
91 |
bverstr = brnorm.split("-")[1] |
92 |
except IndexError: |
93 |
# No version
|
94 |
raise ValueError("Branch name '%s' should contain version" % |
95 |
branch) |
96 |
|
97 |
# Check that version is well-formed
|
98 |
if not re.match(VERSION_RE, bverstr): |
99 |
raise ValueError("Malformed version '%s' in branch name '%s'" % |
100 |
(bverstr, branch)) |
101 |
|
102 |
m = re.match(btype.allowed_version_re, base_version) |
103 |
if not m or (btype.versioned and m.groupdict()["bverstr"] != bverstr): |
104 |
raise ValueError("Base version '%s' unsuitable for branch name '%s'" % |
105 |
(base_version, branch)) |
106 |
|
107 |
def python_version(base_version, vcs_info, mode): |
108 |
"""Generate a Python distribution version following devtools conventions.
|
109 |
|
110 |
This helper generates a Python distribution version from a repository
|
111 |
commit, following devtools conventions. The input data are:
|
112 |
* base_version: a base version number, presumably stored in text file
|
113 |
inside the repository, e.g., /version
|
114 |
* vcs_info: vcs information: current branch name and revision no
|
115 |
* mode: "snapshot", or "release"
|
116 |
|
117 |
This helper assumes a git branching model following:
|
118 |
http://nvie.com/posts/a-successful-git-branching-model/
|
119 |
|
120 |
with 'master', 'develop', 'release-X', 'hotfix-X' and 'feature-X' branches.
|
121 |
|
122 |
General rules:
|
123 |
a) any repository commit can get as a Python version
|
124 |
b) a version is generated either in 'release' or in 'snapshot' mode
|
125 |
c) the choice of mode depends on the branch, see following table.
|
126 |
|
127 |
A python version is of the form A_NNN,
|
128 |
where A: X.Y.Z{,next,rcW} and NNN: a revision number for the commit,
|
129 |
as returned by vcs_info().
|
130 |
|
131 |
For every combination of branch and mode, releases are numbered as follows:
|
132 |
|
133 |
BRANCH: / MODE: snapshot release
|
134 |
-------- ------------------------------
|
135 |
feature 0.14next_150 N/A
|
136 |
develop 0.14next_151 N/A
|
137 |
release 0.14rc2_249 0.14rc2
|
138 |
master N/A 0.14
|
139 |
hotfix 0.14.1rc6_121 0.14.1rc6
|
140 |
N/A 0.14.1
|
141 |
|
142 |
The suffix 'next' in a version name is used to denote the upcoming version,
|
143 |
the one being under development in the develop and release branches.
|
144 |
Version '0.14next' is the version following 0.14, and only lives on the
|
145 |
develop and feature branches.
|
146 |
|
147 |
The suffix 'rc' is used to denote release candidates. 'rc' versions live
|
148 |
only in release and hotfix branches.
|
149 |
|
150 |
Suffixes 'next' and 'rc' have been chosen to ensure proper ordering
|
151 |
according to setuptools rules:
|
152 |
|
153 |
http://www.python.org/dev/peps/pep-0386/#setuptools
|
154 |
|
155 |
Every branch uses a value for A so that all releases are ordered based
|
156 |
on the branch they came from, so:
|
157 |
|
158 |
So
|
159 |
0.13next < 0.14rcW < 0.14 < 0.14next < 0.14.1
|
160 |
|
161 |
and
|
162 |
|
163 |
>>> V("0.14next") > V("0.14")
|
164 |
True
|
165 |
>>> V("0.14next") > V("0.14rc7")
|
166 |
True
|
167 |
>>> V("0.14next") > V("0.14.1")
|
168 |
False
|
169 |
>>> V("0.14rc6") > V("0.14")
|
170 |
False
|
171 |
>>> V("0.14.2rc6") > V("0.14.1")
|
172 |
True
|
173 |
|
174 |
The value for _NNN is chosen based of the revision number of the specific
|
175 |
commit. It is used to ensure ascending ordering of consecutive releases
|
176 |
from the same branch. Every version of the form A_NNN comes *before*
|
177 |
than A: All snapshots are ordered so they come before the corresponding
|
178 |
release.
|
179 |
|
180 |
So
|
181 |
0.14next_* < 0.14
|
182 |
0.14.1_* < 0.14.1
|
183 |
etc
|
184 |
|
185 |
and
|
186 |
|
187 |
>>> V("0.14next_150") < V("0.14next")
|
188 |
True
|
189 |
>>> V("0.14.1next_150") < V("0.14.1next")
|
190 |
True
|
191 |
>>> V("0.14.1_149") < V("0.14.1")
|
192 |
True
|
193 |
>>> V("0.14.1_149") < V("0.14.1_150")
|
194 |
True
|
195 |
|
196 |
Combining both of the above, we get
|
197 |
0.13next_* < 0.13next < 0.14rcW_* < 0.14rcW < 0.14_* < 0.14
|
198 |
< 0.14next_* < 0.14next < 0.14.1_* < 0.14.1
|
199 |
|
200 |
and
|
201 |
|
202 |
>>> V("0.13next_102") < V("0.13next")
|
203 |
True
|
204 |
>>> V("0.13next") < V("0.14rc5_120")
|
205 |
True
|
206 |
>>> V("0.14rc3_120") < V("0.14rc3")
|
207 |
True
|
208 |
>>> V("0.14rc3") < V("0.14_1")
|
209 |
True
|
210 |
>>> V("0.14_120") < V("0.14")
|
211 |
True
|
212 |
>>> V("0.14") < V("0.14next_20")
|
213 |
True
|
214 |
>>> V("0.14next_20") < V("0.14next")
|
215 |
True
|
216 |
|
217 |
Note: one of the tests above fails because of constraints in the way
|
218 |
setuptools parses version numbers. It does not affect us because the
|
219 |
specific version format that triggers the problem is not contained in the
|
220 |
table showing allowed branch / mode combinations, above.
|
221 |
|
222 |
|
223 |
"""
|
224 |
validate_version(base_version, vcs_info) |
225 |
branch = vcs_info.branch |
226 |
btypestr = utils.get_branch_type(branch) |
227 |
#this cannot fail
|
228 |
btype = BRANCH_TYPES[btypestr] |
229 |
|
230 |
if mode not in ["snapshot", "release"]: |
231 |
raise ValueError("Specified mode '%s' should be one of 'snapshot' or " |
232 |
"'release'" % mode)
|
233 |
snap = (mode == "snapshot")
|
234 |
|
235 |
if (snap and not btype.builds_snapshot) or\ |
236 |
(not snap and not btype.builds_release): # nopep8 |
237 |
raise ValueError("Invalid mode '%s' in branch type '%s'" % |
238 |
(mode, btypestr)) |
239 |
|
240 |
if snap:
|
241 |
v = "%s_%d_%s" % (base_version, vcs_info.revno, vcs_info.revid)
|
242 |
else:
|
243 |
v = base_version |
244 |
return v
|
245 |
|
246 |
|
247 |
def debian_version_from_python_version(pyver): |
248 |
"""Generate a debian package version from a Python version.
|
249 |
|
250 |
This helper generates a Debian package version from a Python version,
|
251 |
following devtools conventions.
|
252 |
|
253 |
Debian sorts version strings differently compared to setuptools:
|
254 |
http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
|
255 |
|
256 |
Initial tests:
|
257 |
|
258 |
>>> debian_version("3") < debian_version("6")
|
259 |
True
|
260 |
>>> debian_version("3") < debian_version("2")
|
261 |
False
|
262 |
>>> debian_version("1") == debian_version("1")
|
263 |
True
|
264 |
>>> debian_version("1") != debian_version("1")
|
265 |
False
|
266 |
>>> debian_version("1") >= debian_version("1")
|
267 |
True
|
268 |
>>> debian_version("1") <= debian_version("1")
|
269 |
True
|
270 |
|
271 |
This helper defines a 1-1 mapping between Python and Debian versions,
|
272 |
with the same ordering.
|
273 |
|
274 |
Debian versions are ordered in the same way as Python versions:
|
275 |
|
276 |
>>> D("0.14next") > D("0.14")
|
277 |
True
|
278 |
>>> D("0.14next") > D("0.14rc7")
|
279 |
True
|
280 |
>>> D("0.14next") > D("0.14.1")
|
281 |
False
|
282 |
>>> D("0.14rc6") > D("0.14")
|
283 |
False
|
284 |
>>> D("0.14.2rc6") > D("0.14.1")
|
285 |
True
|
286 |
|
287 |
and
|
288 |
|
289 |
>>> D("0.14next_150") < D("0.14next")
|
290 |
True
|
291 |
>>> D("0.14.1next_150") < D("0.14.1next")
|
292 |
True
|
293 |
>>> D("0.14.1_149") < D("0.14.1")
|
294 |
True
|
295 |
>>> D("0.14.1_149") < D("0.14.1_150")
|
296 |
True
|
297 |
|
298 |
and
|
299 |
|
300 |
>>> D("0.13next_102") < D("0.13next")
|
301 |
True
|
302 |
>>> D("0.13next") < D("0.14rc5_120")
|
303 |
True
|
304 |
>>> D("0.14rc3_120") < D("0.14rc3")
|
305 |
True
|
306 |
>>> D("0.14rc3") < D("0.14_1")
|
307 |
True
|
308 |
>>> D("0.14_120") < D("0.14")
|
309 |
True
|
310 |
>>> D("0.14") < D("0.14next_20")
|
311 |
True
|
312 |
>>> D("0.14next_20") < D("0.14next")
|
313 |
True
|
314 |
|
315 |
"""
|
316 |
version = pyver.replace("_", "~").replace("rc", "~rc") |
317 |
codename = utils.get_distribution_codename() |
318 |
minor = str(get_revision(version, codename))
|
319 |
return version + "-" + minor + "~" + codename |
320 |
|
321 |
|
322 |
def get_revision(version, codename): |
323 |
"""Find revision for a debian version"""
|
324 |
version_tag = utils.version_to_tag(version) |
325 |
repo = utils.get_repository() |
326 |
minor = 1
|
327 |
while True: |
328 |
tag = "debian/" + version_tag + "-" + str(minor) + codename |
329 |
if tag in repo.tags: |
330 |
minor += 1
|
331 |
else:
|
332 |
return minor
|
333 |
|
334 |
|
335 |
def get_python_version(): |
336 |
v = utils.get_vcs_info() |
337 |
b = get_base_version(v) |
338 |
mode = utils.get_build_mode() |
339 |
return python_version(b, v, mode)
|
340 |
|
341 |
|
342 |
def debian_version(base_version, vcs_info, mode): |
343 |
p = python_version(base_version, vcs_info, mode) |
344 |
return debian_version_from_python_version(p)
|
345 |
|
346 |
|
347 |
def get_debian_version(): |
348 |
v = utils.get_vcs_info() |
349 |
b = get_base_version(v) |
350 |
mode = utils.get_build_mode() |
351 |
return debian_version(b, v, mode)
|
352 |
|
353 |
|
354 |
def update_version(): |
355 |
"""Generate or replace version files
|
356 |
|
357 |
Helper function for generating/replacing version files containing version
|
358 |
information.
|
359 |
|
360 |
"""
|
361 |
|
362 |
v = utils.get_vcs_info() |
363 |
toplevel = v.toplevel |
364 |
|
365 |
config = utils.get_config() |
366 |
if not v: |
367 |
# Return early if not in development environment
|
368 |
raise RuntimeError("Can not compute version outside of a git" |
369 |
" repository.")
|
370 |
b = get_base_version(v) |
371 |
mode = utils.get_build_mode() |
372 |
version = python_version(b, v, mode) |
373 |
debian_version_ = debian_version_from_python_version(version) |
374 |
env = {"DEVFLOW_VERSION": version,
|
375 |
"DEVFLOW_DEBIAN_VERSION": debian_version_,
|
376 |
"DEVFLOW_BRANCH": v.branch,
|
377 |
"DEVFLOW_REVISION_ID": v.revid,
|
378 |
"DEVFLOW_REVISION_NUMBER": v.revno,
|
379 |
"DEVFLOW_USER_EMAIL": v.email,
|
380 |
"DEVFLOW_USER_NAME": v.name}
|
381 |
|
382 |
for _pkg_name, pkg_info in config['packages'].items(): |
383 |
version_filename = pkg_info.get('version_file')
|
384 |
if not version_filename: |
385 |
continue
|
386 |
version_template = pkg_info.get('version_template')
|
387 |
if version_template:
|
388 |
vtemplate_file = os.path.join(toplevel, version_template) |
389 |
try:
|
390 |
with file(vtemplate_file) as f: |
391 |
content = f.read(-1) % env
|
392 |
except IOError as e: |
393 |
if e.errno == 2: |
394 |
raise RuntimeError("devflow.conf contains '%s' as a" |
395 |
" version template file, but file does"
|
396 |
" not exists!" % vtemplate_file)
|
397 |
else:
|
398 |
raise
|
399 |
else:
|
400 |
content = DEFAULT_VERSION_FILE % env |
401 |
with file(os.path.join(toplevel, version_filename), 'w+') as f: |
402 |
log.info("Updating version file '%s'" % version_filename)
|
403 |
f.write(content) |
404 |
|
405 |
|
406 |
def bump_version_main(): |
407 |
try:
|
408 |
version = sys.argv[1]
|
409 |
bump_version(version) |
410 |
except IndexError: |
411 |
sys.stdout.write("Give me a version %s!\n")
|
412 |
sys.stdout.write("usage: %s version\n" % sys.argv[0]) |
413 |
|
414 |
|
415 |
def bump_version(new_version): |
416 |
"""Set new base version to base version file and commit"""
|
417 |
v = utils.get_vcs_info() |
418 |
mode = utils.get_build_mode() |
419 |
|
420 |
# Check that new base version is valid
|
421 |
python_version(new_version, v, mode) |
422 |
|
423 |
repo = utils.get_repository() |
424 |
toplevel = repo.working_dir |
425 |
|
426 |
old_version = get_base_version(v) |
427 |
sys.stdout.write("Current base version is '%s'\n" % old_version)
|
428 |
|
429 |
version_file = os.path.join(toplevel, "version")
|
430 |
sys.stdout.write("Updating version file %s from version '%s' to '%s'\n"
|
431 |
% (version_file, old_version, new_version)) |
432 |
|
433 |
f = open(version_file, 'rw+') |
434 |
lines = f.readlines() |
435 |
for i in range(0, len(lines)): |
436 |
if not lines[i].startswith("#"): |
437 |
lines[i] = lines[i].replace(old_version, new_version) |
438 |
f.seek(0)
|
439 |
f.truncate(0)
|
440 |
f.writelines(lines) |
441 |
f.close() |
442 |
|
443 |
repo.git.add(version_file) |
444 |
repo.git.commit(m="Bump version to %s" % new_version)
|
445 |
sys.stdout.write("Update version file and commited\n")
|
446 |
|
447 |
|
448 |
def main(): |
449 |
v = utils.get_vcs_info() |
450 |
b = get_base_version(v) |
451 |
mode = utils.get_build_mode() |
452 |
|
453 |
try:
|
454 |
arg = sys.argv[1]
|
455 |
assert arg == "python" or arg == "debian" |
456 |
except IndexError: |
457 |
raise ValueError("A single argument, 'python' or 'debian is required") |
458 |
|
459 |
if arg == "python": |
460 |
print python_version(b, v, mode)
|
461 |
elif arg == "debian": |
462 |
print debian_version(b, v, mode)
|
463 |
|
464 |
if __name__ == "__main__": |
465 |
sys.exit(main()) |