Migrate LIBLITERALPREFIX test to file with same name
[scons.git] / bin / update-release-info.py
blob5e2e3265eb2a4cf80ce6153c9c35388f8b813d0b
1 #!/usr/bin/env python
2 """
3 This program takes information about a release in the ReleaseConfig file
4 and inserts the information in various files. It helps to keep the files
5 in sync because it never forgets which files need to be updated, nor what
6 information should be inserted in each file.
8 It takes one parameter that says what the mode of update should be, which
9 may be one of 'develop', 'release', or 'post'.
11 In 'develop' mode, which is what someone would use as part of their own
12 development practices, the release type is forced to be 'dev' and the
13 patch level is the string 'yyyymmdd'. Otherwise, it's the same as the
14 'release' mode.
16 In 'release' mode, the release type is taken from ReleaseConfig and the
17 patch level is calculated from the release date for non-final runs and
18 taken from the version tuple for final runs. It then inserts information
19 in various files:
20 - The RELEASE header line in src/CHANGES.txt and src/Announce.txt.
21 - The version string at the top of src/RELEASE.txt.
22 - The version string in the 'default_version' variable in testing/framework/TestSCons.py.
23 - The copyright years in testing/framework/TestSCons.py.
24 - The unsupported and deprecated Python floors in testing/framework/TestSCons.py
25 and src/engine/SCons/Script/Main.py
26 - The version string in the filenames in README.
28 In 'post' mode, files are prepared for the next release cycle:
29 - In ReleaseConfig, the version tuple is updated for the next release
30 by incrementing the release number (either minor or micro, depending
31 on the branch) and resetting release type to 'dev'.
32 - A blank template replaces src/RELEASE.txt.
33 - A new section to accumulate changes is added to src/CHANGES.txt and
34 src/Announce.txt.
35 """
36 # MIT License
38 # Copyright The SCons Foundation
40 # Permission is hereby granted, free of charge, to any person obtaining
41 # a copy of this software and associated documentation files (the
42 # "Software"), to deal in the Software without restriction, including
43 # without limitation the rights to use, copy, modify, merge, publish,
44 # distribute, sublicense, and/or sell copies of the Software, and to
45 # permit persons to whom the Software is furnished to do so, subject to
46 # the following conditions:
48 # The above copyright notice and this permission notice shall be included
49 # in all copies or substantial portions of the Software.
51 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
52 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
53 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
54 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
55 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
56 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
57 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
60 import argparse
61 import os
62 import re
63 import sys
64 import time
66 DEBUG = os.environ.get('DEBUG', 0)
69 class ReleaseInfo:
70 def __init__(self, args):
71 self.config = {}
72 self.args = args
73 self.release_date = time.localtime()[:6]
75 self.unsupported_version = None
76 self.deprecated_version = None
78 # version_tuple = (3, 1, 3, 'alpha', 0)
79 self.version_tuple = (-1, -1, -1, 'not_set', -1)
81 self.version_string = "UNSET"
82 self.version_type = 'UNSET'
83 self.month_year = 'UNSET'
84 self.copyright_years = 'UNSET'
85 self.new_date = 'NEW DATE WILL BE INSERTED HERE'
87 self.read_config()
88 self.process_config()
89 if not self.args.timestamp:
90 self.set_new_date()
92 def read_config(self):
93 # Get configuration information
95 config = dict()
96 with open('ReleaseConfig') as f:
97 release_config = f.read()
98 exec(release_config, globals(), config)
100 self.config = config
102 def process_config(self):
104 Process and validate the config info loaded from ReleaseConfig file
107 try:
108 self.version_tuple = self.config['version_tuple']
109 self.unsupported_version = self.config['unsupported_python_version']
110 self.deprecated_version = self.config['deprecated_python_version']
111 except KeyError:
112 print('''ERROR: Config file must contain at least version_tuple,
113 \tunsupported_python_version, and deprecated_python_version.''')
114 sys.exit(1)
116 if 'release_date' in self.config:
117 self.release_date = self.config['release_date']
118 if len(self.release_date) == 3:
119 self.release_date = self.release_date + time.localtime()[3:6]
120 if len(self.release_date) != 6:
121 print('''ERROR: Invalid release date''', self.release_date)
122 sys.exit(1)
124 yyyy, mm, dd, h, m, s = self.release_date
125 date_string = "".join(["%.2d" % d for d in self.release_date])
127 if self.args.timestamp:
128 date_string = self.args.timestamp
130 if self.args.mode == 'develop' and self.version_tuple[3] != 'a':
131 self.version_tuple == self.version_tuple[:3] + ('a', 0)
133 if len(self.version_tuple) > 3 and self.version_tuple[3] != 'final':
134 self.version_tuple = self.version_tuple[:4] + (date_string,)
136 self.version_string = '.'.join(map(str, self.version_tuple[:4])) + date_string
138 if len(self.version_tuple) > 3:
139 self.version_type = self.version_tuple[3]
140 else:
141 self.version_type = 'final'
143 if self.version_type not in ['a', 'b', 'rc', 'final']:
144 print(("""ERROR: `%s' is not a valid release type in version tuple;
145 \tit must be one of a, b, rc, or final""" % self.version_type))
146 sys.exit(1)
148 try:
149 self.month_year = self.config['month_year']
150 except KeyError:
151 if self.args.timestamp:
152 self.month_year = "MONTH YEAR"
153 else:
154 self.month_year = time.strftime('%B %Y', self.release_date + (0, 0, 0))
156 try:
157 self.copyright_years = self.config['copyright_years']
158 except KeyError:
159 self.copyright_years = '2001 - %d' % (self.release_date[0] + 1)
161 if DEBUG:
162 print('version tuple', self.version_tuple)
163 print('unsupported Python version', self.unsupported_version)
164 print('deprecated Python version', self.deprecated_version)
165 print('release date', self.release_date)
166 print('version string', self.version_string)
167 print('month year', self.month_year)
168 print('copyright years', self.copyright_years)
170 def set_new_date(self):
172 Determine the release date and the pattern to match a date
173 Mon, 05 Jun 2010 21:17:15 -0700
174 NEW DATE WILL BE INSERTED HERE
176 min = (time.daylight and time.altzone or time.timezone) // 60
177 hr = min // 60
178 min = -(min % 60 + hr * 100)
179 self.new_date = (time.strftime('%a, %d %b %Y %X', self.release_date + (0, 0, 0))
180 + ' %+.4d' % min)
183 class UpdateFile:
184 """ XXX """
186 rel_info = None
187 mode = 'develop'
188 _days = r'(Sun|Mon|Tue|Wed|Thu|Fri|Sat)'
189 _months = r'(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oce|Nov|Dec)'
190 match_date = r''.join([_days, r', \d\d ', _months, r' \d\d\d\d \d\d:\d\d:\d\d [+-]\d\d\d\d'])
191 match_date = re.compile(match_date)
193 # Determine the pattern to match a version
195 _rel_types = r'[(a|b|rc)]'
196 match_pat = r'\d+\.\d+\.\d+\.' + _rel_types + r'\.?(\d+|yyyymmdd)'
197 match_rel = re.compile(match_pat)
199 def __init__(self, file, orig=None):
200 if orig is None:
201 orig = file
203 try:
204 with open(orig, 'r') as f:
205 self.content = f.read()
206 except IOError:
207 # Couldn't open file; don't try to write anything in __del__
208 self.file = None
209 raise
210 else:
211 self.file = file
212 if file == orig:
213 # so we can see if it changed
214 self.orig = self.content
215 else:
216 # pretend file changed
217 self.orig = ''
219 def sub(self, pattern, replacement, count=1):
220 """ XXX """
221 self.content = re.sub(pattern, replacement, self.content, count)
223 def replace_assign(self, name, replacement, count=1):
224 """ XXX """
225 self.sub('\n' + name + ' = .*', '\n' + name + ' = ' + replacement)
227 def replace_version(self, count=1):
228 """ XXX """
229 self.content = self.match_rel.sub(rel_info.version_string, self.content, count)
231 def replace_date(self, count=1):
232 """ XXX """
233 self.content = self.match_date.sub(rel_info.new_date, self.content, count)
235 def __del__(self):
236 """ XXX """
237 if self.file is not None and self.content != self.orig:
238 print('Updating ' + self.file + '...')
239 with open(self.file, 'w') as f:
240 f.write(self.content)
243 def main(args, rel_info):
244 if args.mode == 'post':
245 # Set up for the next release series.
247 if rel_info.version_tuple[2]:
248 # micro release, increment micro value
249 minor = rel_info.version_tuple[1]
250 micro = rel_info.version_tuple[2] + 1
251 else:
252 # minor release, increment minor value
253 minor = rel_info.version_tuple[1] + 1
254 micro = 0
255 new_tuple = (rel_info.version_tuple[0], minor, micro, 'a', 0)
256 new_version = '.'.join(map(str, new_tuple[:3])) + new_tuple[3] + 'yyyymmdd'
258 rel_info.version_string = new_version
260 # Update ReleaseConfig
261 t = UpdateFile('ReleaseConfig')
262 if DEBUG:
263 t.file = '/tmp/ReleaseConfig'
264 t.replace_assign('version_tuple', str(new_tuple))
266 # Update src/CHANGES.txt
267 t = UpdateFile('CHANGES.txt')
268 if DEBUG:
269 t.file = '/tmp/CHANGES.txt'
270 t.sub('(\nRELEASE .*)', r"""\nRELEASE VERSION/DATE TO BE FILLED IN LATER\n
271 From John Doe:\n
272 - Whatever John Doe did.\n\n\1""")
274 # Update src/RELEASE.txt
275 t = UpdateFile('RELEASE.txt',
276 os.path.join('template', 'RELEASE.txt'))
277 if DEBUG:
278 t.file = '/tmp/RELEASE.txt'
279 t.replace_version()
281 # Update README
282 for readme_file in ['README.rst', 'README-SF.rst']:
283 t = UpdateFile(readme_file)
284 if DEBUG: t.file = os.path.join('/tmp/', readme_file)
285 t.sub('-' + t.match_pat + r'\.', '-' + rel_info.version_string + '.', count=0)
286 for suf in ['tar', 'win32', 'zip']:
287 t.sub(r'-(\d+\.\d+\.\d+)\.%s' % suf,
288 '-%s.%s' % (rel_info.version_string, suf),
289 count=0)
291 # Update testing/framework/TestSCons.py
292 t = UpdateFile(os.path.join('testing', 'framework', 'TestSCons.py'))
293 if DEBUG:
294 t.file = '/tmp/TestSCons.py'
295 t.replace_assign('copyright_years', repr(rel_info.copyright_years))
296 t.replace_assign('default_version', repr(rel_info.version_string))
297 # ??? t.replace_assign('SConsVersion', repr(version_string))
298 t.replace_assign('python_version_unsupported', str(rel_info.unsupported_version))
299 t.replace_assign('python_version_deprecated', str(rel_info.deprecated_version))
301 # Update Script/Main.py
302 t = UpdateFile(os.path.join('SCons', 'Script', 'Main.py'))
303 if DEBUG:
304 t.file = '/tmp/Main.py'
305 t.replace_assign('unsupported_python_version', str(rel_info.unsupported_version))
306 t.replace_assign('deprecated_python_version', str(rel_info.deprecated_version))
308 # Update doc/user/main.xml
309 docyears = '2004 - %d' % rel_info.release_date[0]
310 t = UpdateFile(os.path.join('doc', 'user', 'main.xml'))
311 if DEBUG:
312 t.file = '/tmp/main.xml'
313 t.sub('<pubdate>[^<]*</pubdate>', '<pubdate>Released: ' + rel_info.new_date + '</pubdate>')
314 t.sub('<year>[^<]*</year>', '<year>' + docyears + '</year>')
316 # Write out the last update
318 t = None
321 def parse_arguments():
323 Create ArgumentParser object and process arguments
326 parser = argparse.ArgumentParser(prog='update-release-info.py')
327 parser.add_argument('mode', nargs='?', choices=['develop', 'release', 'post'], default='post')
328 parser.add_argument('--verbose', dest='verbose', action='store_true', help='Enable verbose logging')
330 parser.add_argument('--timestamp', dest='timestamp', help='Override the default current timestamp')
332 args = parser.parse_args()
333 return args
336 if __name__ == "__main__":
337 options = parse_arguments()
338 rel_info = ReleaseInfo(options)
339 UpdateFile.rel_info = rel_info
340 main(options, rel_info)
342 # Local Variables:
343 # tab-width:4
344 # indent-tabs-mode:nil
345 # End:
346 # vim: set expandtab tabstop=4 shiftwidth=4: