prepare_gaudi_release.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 '''
3 Script to prepare the release of Gaudi.
4 
5 @author Marco Clemencic
6 '''
7 
8 import os
9 import sys
10 import logging
11 import re
12 import ConfigParser
13 from subprocess import Popen, PIPE
14 
16  '''
17  Ensure we have a usable version of Git (>= 1.7.9.1).
18 
19  See:
20  * https://raw.githubusercontent.com/git/git/master/Documentation/RelNotes/1.7.9.1.txt
21  * https://github.com/git/git/commit/36ed1913e1d5de0930e59db6eeec3ccb2bd58bd9
22  '''
23  proc = Popen(['git', '--version'], stdout=PIPE)
24  version = proc.communicate()[0].split()[-1]
25  if proc.returncode:
26  raise RuntimeError('could not get git version')
27  if versionKey(version) < versionKey('1.7.9.1'):
28  raise RuntimeError('bad version of git found: %s (1.7.9.1 required)' % version)
29 
30 _VK_RE = re.compile(r'(\d+|\D+)')
31 def versionKey(x):
32  '''
33  Key function to be passes to list.sort() to sort strings
34  as version numbers.
35  '''
36  return [int(i) if i[0] in '0123456789' else i
37  for i in _VK_RE.split(x)
38  if i]
39 
41  '''
42  Return the latest Gaudi tag (of the format "GAUDI/GAUDI_*").
43  '''
44  logging.info('looking for latest tag')
45  cmd = ['git', 'tag']
46  logging.debug('using command %r', cmd)
47  proc = Popen(cmd, stdout=PIPE)
48  output = proc.communicate()[0]
49  tags = [tag
50  for tag in output.splitlines()
51  if tag.startswith('GAUDI/GAUDI_')]
52  if tags:
53  tags.sort(key=versionKey)
54  logging.info('found %s', tags[-1])
55  return tags[-1]
56  logging.info('no valid tag found')
57 
58 def releaseNotes(path=os.curdir, from_tag=None, branch=None):
59  '''
60  Return the release notes (in the old LHCb format) extracted from git
61  commits for a given path.
62  '''
63  cmd = ['git', 'log',
64  '--first-parent', '--date=short',
65  '--pretty=format:! %ad - commit %h%n%n%w(80,1,3)- %s%n%n%b%n']
66  if from_tag:
67  cmd.append('{0}..{1}'.format(from_tag, branch or ''))
68  elif branch:
69  cmd.append(branch)
70  cmd.append('--')
71  cmd.append(path)
72  logging.info('preparing release notes for %s%s', path,
73  ' since ' + from_tag if from_tag else '')
74  logging.debug('using command %r', cmd)
75  proc = Popen(cmd, stdout=PIPE)
76  return proc.communicate()[0]
77 
78 def updateReleaseNotes(path, notes):
79  '''
80  Smartly prepend the content of notes to the release.notes file in path.
81  '''
82  notes_filename = os.path.join(path, 'doc', 'release.notes')
83  logging.info('updating %s', notes_filename)
84  from itertools import takewhile, dropwhile
85  def dropuntil(predicate, iterable):
86  return dropwhile(lambda x: not predicate(x), iterable)
87  with open(notes_filename) as notes_file:
88  orig_data = iter(list(notes_file))
89  header = takewhile(str.strip, orig_data)
90  with open(notes_filename, 'w') as notes_file:
91  notes_file.writelines(header)
92  notes_file.write('\n')
93  notes_file.writelines(l.rstrip() + '\n' for l in notes.splitlines())
94  notes_file.writelines(dropuntil(re.compile(r'^!?============').match, orig_data))
95 
96 # guess current version
97 _req_version_pattern = re.compile(r"^\s*version\s*(v[0-9]+r[0-9]+(?:p[0-9]+)?)\s*$")
98 _cml_version_pattern = re.compile(r"^\s*gaudi_subdir\s*\(\s*\S+\s+(v[0-9]+r[0-9]+(?:p[0-9]+)?)\)\s*$")
99 def extract_version(path):
100  """
101  Find the version number of a subdirectory.
102  """
103  global _cml_version_pattern
104  for l in open(os.path.join(path, 'CMakeLists.txt')):
105  m = _cml_version_pattern.match(l)
106  if m:
107  return m.group(1)
108  return None
109 
110 def change_cml_version(cml, newversion):
111  if os.path.exists(cml):
112  out = []
113  changed = False
114  for l in open(cml):
115  m = _cml_version_pattern.match(l)
116  if m and m.group(1) != newversion:
117  logging.debug('%s: %s -> %s', cml, m.group(1), newversion)
118  l = l.replace(m.group(1), newversion)
119  changed = True
120  out.append(l)
121  if changed:
122  open(cml, "w").writelines(out)
123 
124 def change_version(packagedir, newversion):
125  """
126  Compare the version of the package with the new one and update the package if
127  needed.
128 
129  Returns true if the package have been modified.
130  """
131  global _req_version_pattern
132  changed = False
133  out = []
134  req = os.path.join(packagedir, 'cmt', 'requirements')
135  for l in open(req):
136  m = _req_version_pattern.match(l)
137  if m:
138  if m.group(1) != newversion:
139  logging.debug('%s: %s -> %s', packagedir, m.group(1), newversion)
140  l = l.replace(m.group(1),newversion)
141  changed = True
142  out.append(l)
143  if changed:
144  open(req,"w").writelines(out)
145  # verify the version.cmt file
146  ver = os.path.join(packagedir,"version.cmt")
147  if os.path.exists(ver):
148  current = open(ver).read().strip()
149  if current != newversion:
150  open(ver,"w").write(newversion + "\n")
151  # update CMakeLists.txt
152  cml = os.path.normpath(os.path.join(packagedir, 'CMakeLists.txt'))
153  change_cml_version(cml, newversion)
154  if 'GaudiKernel' in packagedir:
155  cml = os.path.normpath(os.path.join(packagedir, 'src', 'Util', 'CMakeLists.txt'))
156  change_cml_version(cml, newversion)
157  return changed
158 
159 def tag_bar(pkg, version=None):
160  title = ' %s %s ' % (pkg, version)
161  letf_chars = (78 - len(title)) / 2
162  right_chars = 78 - letf_chars - len(title)
163  separator = ('=' * letf_chars) + title + ('=' * right_chars)
164  return separator
165 
166 def main():
167  logging.basicConfig(level=logging.DEBUG)
168  os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
169  # Find the version of HEPTools (LCG)
170  for l in open('toolchain.cmake'):
171  m = re.match(r'^\s*set\(\s*heptools_version\s+(\S*)\s*\)', l)
172  if m:
173  HEPToolsVers = m.group(1)
174  print "Using HEPTools", HEPToolsVers
175  break
176  else:
177  logging.error('Cannot find HEPTools version')
178  sys.exit(1)
179 
180  # Collect all the packages in the project with their directory
181  def all_subdirs():
182  for dirpath, dirnames, filenames in os.walk(os.curdir):
183  if 'CMakeLists.txt' in filenames and dirpath != os.curdir:
184  dirnames[:] = []
185  yield dirpath
186  else:
187  dirnames[:] = [dirname for dirname in dirnames
188  if not dirname.startswith('build.') and
189  dirname != 'cmake']
190 
191  # Packages which version must match the version of the project
192  special_subdirs = ["Gaudi", "GaudiExamples", "GaudiSys", "GaudiRelease"]
193 
194  # Ask for the version of the project
195  latest_tag = findLatestTag()
196 
197  old_version = latest_tag.split('_')[-1]
198  new_version = raw_input("The old version of the project is %s, which is the new one? " % old_version)
199 
200  old_versions = {}
201  release_notes = {}
202  new_versions = {}
203  # for each package in the project check if there were changes and ask for the new version number
204  for pkgdir in all_subdirs():
205  cmlfile = os.path.join(pkgdir, 'CMakeLists.txt')
206  reqfile = os.path.join(pkgdir, 'cmt', 'requirements')
207 
208  pkg = os.path.basename(pkgdir)
209  old_vers = extract_version(pkgdir)
210 
211  vers = None
212  if pkg in special_subdirs:
213  vers = new_version
214  else:
215  git_log = Popen(['git', 'log', '-m', '--first-parent',
216  '--stat', latest_tag + '..master', pkgdir],
217  stdout=PIPE).communicate()[0].strip()
218  if git_log:
219  msg = ('\nThe old version of {0} is {1}, these are the changes:\n'
220  '{2}\n'
221  'Which version you want (old is {1})? ').format(pkg,
222  old_vers,
223  git_log)
224  vers = raw_input(msg)
225  while not vers or vers == old_vers:
226  vers = raw_input('give me a version, please. ')
227  if vers:
228  change_version(pkgdir, vers)
229  updateReleaseNotes(pkgdir,
230  tag_bar(pkg, vers) + '\n\n' +
231  releaseNotes(pkgdir, latest_tag, 'master'))
232  new_versions[pkg] = vers or old_vers
233  print "=" * 80
234 
235  # The changes in the GaudiRelease requirements for the other packages can be postponed to now
236  reqfile = os.path.join('GaudiRelease', 'cmt', 'requirements')
237  out = []
238  for l in open(reqfile):
239  sl = l.strip().split()
240  if sl and sl[0] == "use":
241  if sl[1] in new_versions:
242  if sl[2] != new_versions[sl[1]]:
243  l = l.replace(sl[2], new_versions[sl[1]])
244  out.append(l)
245  open(reqfile, "w").writelines(out)
246  # Update project.info
247  config = ConfigParser.ConfigParser()
248  config.optionxform = str # make the options case sensitive
249  if os.path.exists('project.info'):
250  config.read('project.info')
251  if not config.has_section('Packages'):
252  config.add_section('Packages')
253  for pack_vers in sorted(new_versions.items()):
254  config.set('Packages', *pack_vers)
255  config.write(open('project.info', 'wb'))
256 
257  # update the global CMakeLists.txt
258  out = []
259  for l in open('CMakeLists.txt'):
260  if l.strip().startswith('gaudi_project'):
261  l = 'gaudi_project(Gaudi %s)\n' % new_version
262  out.append(l)
263  open('CMakeLists.txt', "w").writelines(out)
264 
265 
266 if __name__ == '__main__':
268  main()
GAUDI_API std::string format(const char *,...)
MsgStream format utility "a la sprintf(...)".
Definition: MsgStream.cpp:119
def updateReleaseNotes(path, notes)
def change_cml_version(cml, newversion)
def change_version(packagedir, newversion)