Merge release-4-6 into release-5-0
[gromacs.git] / admin / copyright.py
blob24191e30e9b5cccc158ee68ec7844542a58581bb
1 #!/usr/bin/python
3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2013, by the GROMACS development team, led by
6 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7 # and including many others, as listed in the AUTHORS file in the
8 # top-level source directory and at http://www.gromacs.org.
10 # GROMACS is free software; you can redistribute it and/or
11 # modify it under the terms of the GNU Lesser General Public License
12 # as published by the Free Software Foundation; either version 2.1
13 # of the License, or (at your option) any later version.
15 # GROMACS is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 # Lesser General Public License for more details.
20 # You should have received a copy of the GNU Lesser General Public
21 # License along with GROMACS; if not, see
22 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
23 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 # If you want to redistribute modifications to GROMACS, please
26 # consider that scientific software is very special. Version
27 # control is crucial - bugs must be traceable. We will be happy to
28 # consider code for inclusion in the official distribution, but
29 # derived work must not be called official GROMACS. Details are found
30 # in the README & COPYING files - if they are missing, get the
31 # official version at http://www.gromacs.org.
33 # To help us fund GROMACS development, we humbly ask that you cite
34 # the research papers on the package. Check out http://www.gromacs.org.
36 import datetime
37 import os.path
38 import re
39 import sys
41 from optparse import OptionParser
43 class CopyrightState(object):
45 """Information about an existing (or non-existing) copyright header."""
47 def __init__(self, has_copyright, is_correct, is_newstyle, years, other_copyrights):
48 self.has_copyright = has_copyright
49 self.is_correct = is_correct
50 self.is_newstyle = is_newstyle
51 self.years = years
52 self.other_copyrights = other_copyrights
54 class CopyrightChecker(object):
56 """Logic for analyzing existing copyright headers and generating new ones."""
58 _header = ["", "This file is part of the GROMACS molecular simulation package.", ""]
59 _copyright = "Copyright (c) {0}, by the GROMACS development team, led by"
60 _footer = """
61 Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
62 and including many others, as listed in the AUTHORS file in the
63 top-level source directory and at http://www.gromacs.org.
65 GROMACS is free software; you can redistribute it and/or
66 modify it under the terms of the GNU Lesser General Public License
67 as published by the Free Software Foundation; either version 2.1
68 of the License, or (at your option) any later version.
70 GROMACS is distributed in the hope that it will be useful,
71 but WITHOUT ANY WARRANTY; without even the implied warranty of
72 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
73 Lesser General Public License for more details.
75 You should have received a copy of the GNU Lesser General Public
76 License along with GROMACS; if not, see
77 http://www.gnu.org/licenses, or write to the Free Software Foundation,
78 Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
80 If you want to redistribute modifications to GROMACS, please
81 consider that scientific software is very special. Version
82 control is crucial - bugs must be traceable. We will be happy to
83 consider code for inclusion in the official distribution, but
84 derived work must not be called official GROMACS. Details are found
85 in the README & COPYING files - if they are missing, get the
86 official version at http://www.gromacs.org.
88 To help us fund GROMACS development, we humbly ask that you cite
89 the research papers on the package. Check out http://www.gromacs.org.
90 """.strip().splitlines()
92 def check_copyright(self, comment_block):
93 """Analyze existing copyright header for correctness and extract information."""
94 copyright_re = r'Copyright \(c\) (([0-9]{4}[,-])*[0-9]{4}),? by the GROMACS development team,'
95 has_copyright = False
96 is_newstyle = True
97 is_correct = True
98 next_header_line = 0
99 next_footer_line = 0
100 append_next_line_to_other_copyrights = False
101 existing_years = ''
102 other_copyrights = []
103 for line in comment_block:
104 if append_next_line_to_other_copyrights:
105 other_copyrights[-1] += ' ' + line
106 append_next_line_to_other_copyrights = False
107 continue
108 if 'Copyright' in line:
109 has_copyright = True
110 match = re.match(copyright_re, line)
111 if match:
112 existing_years = match.group(1)
113 new_line = self._copyright.format(existing_years)
114 if line != new_line:
115 is_correct = False
116 else:
117 other_copyrights.append(line[line.find('Copyright'):])
118 if not line.startswith('Copyright'):
119 append_next_line_to_other_copyrights = True
120 if next_header_line != -1 or next_footer_line != 0:
121 is_correct = False
122 continue
123 if line.startswith('Written by the Gromacs development team'):
124 has_copyright = True
125 if next_header_line >= 0:
126 if line == self._header[next_header_line]:
127 next_header_line += 1
128 if next_header_line >= len(self._header):
129 next_header_line = -1
130 else:
131 is_correct = False
132 is_newstyle = False
133 elif next_footer_line >= 0:
134 if line == self._footer[next_footer_line]:
135 next_footer_line += 1
136 if next_footer_line >= len(self._footer):
137 next_footer_line = -1
138 else:
139 is_correct = False
140 else:
141 is_correct = False
142 if next_header_line != -1 or next_footer_line != -1:
143 is_correct = False
145 return CopyrightState(has_copyright, is_correct, is_newstyle, existing_years, other_copyrights)
147 def process_copyright(self, state, options, current_years, reporter):
148 """Determine whether a copyrigth header needs to be updated and report issues."""
149 need_update = False
151 if state.years:
152 if options.replace_years:
153 if state.years != current_years:
154 need_update = True
155 reporter.report('copyright years replaced')
156 new_years = current_years
157 else:
158 new_years = state.years
159 if not new_years.endswith(current_years):
160 if options.update_year:
161 need_update = True
162 new_years += ',' + current_years
163 if options.check or not need_update:
164 reporter.report('copyright year outdated')
165 else:
166 reporter.report('copyright year added')
167 else:
168 new_years = current_years
170 if not state.has_copyright:
171 if options.add_missing:
172 need_update = True
173 if options.check or not need_update:
174 reporter.report('copyright header missing')
175 elif options.add_missing:
176 reporter.report('copyright header added')
177 else:
178 if not state.is_newstyle:
179 if options.replace_header:
180 need_update = True
181 if options.check or not need_update:
182 reporter.report('copyright header incorrect')
183 else:
184 reporter.report('copyright header replaced')
185 elif not state.is_correct:
186 if options.update_header:
187 need_update = True
188 if options.check or not need_update:
189 reporter.report('copyright header outdated')
190 else:
191 reporter.report('copyright header updated')
193 return need_update, new_years
195 def get_copyright_text(self, years, other_copyrights):
196 """Construct a new copyright header."""
197 output = []
198 output.extend(self._header)
199 if other_copyrights:
200 for line in other_copyrights:
201 outline = line.rstrip()
202 if outline.endswith(','):
203 outline = outline[:-1]
204 if not outline.endswith('.'):
205 outline += '.'
206 output.append(outline)
207 output.append(self._copyright.format(years))
208 output.extend(self._footer)
209 return output
211 class Reporter(object):
213 """Wrapper for reporting issues in a file."""
215 def __init__(self, reportfile, filename):
216 self._reportfile = reportfile
217 self._filename = filename
219 def report(self, text):
220 self._reportfile.write(self._filename + ': ' + text + '\n');
222 class CommentHandlerC(object):
224 """Handler for extracting and creating C-style comments."""
226 def extract_first_comment_block(self, content_lines):
227 if not content_lines or not content_lines[0].startswith('/*'):
228 return ([], 0)
229 comment_block = [content_lines[0][2:].strip()]
230 line_index = 1
231 while line_index < len(content_lines):
232 line = content_lines[line_index]
233 if '*/' in content_lines[line_index]:
234 break
235 comment_block.append(line.lstrip('* ').rstrip())
236 line_index += 1
237 return (comment_block, line_index + 1)
239 def create_comment_block(self, lines):
240 output = []
241 output.append(('/* ' + lines[0]).rstrip())
242 output.extend([(' * ' + x).rstrip() for x in lines[1:]])
243 output.append(' */')
244 return output
246 class CommentHandlerSimple(object):
248 """Handler for extracting and creating sh-style comments.
250 Also other comments of the same type, but with a different comment
251 character are supported."""
253 def __init__(self, comment_char):
254 self._comment_char = comment_char
256 def extract_first_comment_block(self, content_lines):
257 if not content_lines or not content_lines[0].startswith(self._comment_char):
258 return ([], 0)
259 comment_block = []
260 line_index = 0
261 while line_index < len(content_lines):
262 line = content_lines[line_index]
263 if not line.startswith(self._comment_char):
264 break
265 comment_block.append(line.lstrip(self._comment_char + ' ').rstrip())
266 line_index += 1
267 if line == self._comment_char + ' the research papers on the package. Check out http://www.gromacs.org.':
268 break
269 while line_index < len(content_lines):
270 line = content_lines[line_index].rstrip()
271 if len(line) > 0 and line != self._comment_char:
272 break
273 line_index += 1
274 return (comment_block, line_index)
276 def create_comment_block(self, lines):
277 output = []
278 output.extend([(self._comment_char + ' ' + x).rstrip() for x in lines])
279 output.append('')
280 return output
282 comment_handlers = {
283 'c': CommentHandlerC(),
284 'tex': CommentHandlerSimple('%'),
285 'sh': CommentHandlerSimple('#')
288 def select_comment_handler(override, filename):
289 """Select comment handler for a file based on file name and input options."""
290 filetype = override
291 if not filetype and filename != '-':
292 basename = os.path.basename(filename)
293 root, ext = os.path.splitext(basename)
294 if ext == '.cmakein':
295 dummy, ext2 = os.path.splitext(root)
296 if ext2:
297 ext = ext2
298 if ext in ('.c', '.cu', '.cpp', '.h', '.cuh', '.y', '.l', '.pre', '.bm'):
299 filetype = 'c'
300 elif ext in ('.tex',):
301 filetype = 'tex'
302 elif basename in ('CMakeLists.txt', 'GMXRC', 'git-pre-commit') or \
303 ext in ('.cmake', '.cmakein', '.py', '.sh', '.bash', '.csh', '.zsh'):
304 filetype = 'sh'
305 if filetype in comment_handlers:
306 return comment_handlers[filetype]
307 if filetype:
308 sys.stderr.write("Unsupported input format: {0}\n".format(filetype))
309 elif filename != '-':
310 sys.stderr.write("Unsupported input format: {0}\n".format(filename))
311 else:
312 sys.stderr.write("No file name or file type provided.\n")
313 sys.exit(1)
315 def create_copyright_header(years, other_copyrights=None, language='c'):
316 if language not in comment_handlers:
317 sys.strerr.write("Unsupported language: {0}\n".format(language))
318 sys.exit(1)
319 copyright_checker = CopyrightChecker()
320 comment_handler = comment_handlers[language]
321 copyright_lines = copyright_checker.get_copyright_text(years, other_copyrights)
322 comment_lines = comment_handler.create_comment_block(copyright_lines)
323 return '\n'.join(comment_lines) + '\n'
325 def process_options():
326 """Process input options."""
327 parser = OptionParser()
328 parser.add_option('-l', '--lang',
329 help='Comment type to use (c or sh)')
330 parser.add_option('-y', '--years',
331 help='Comma-separated list of years')
332 parser.add_option('-F', '--files',
333 help='File to read list of files from')
334 parser.add_option('--check', action='store_true',
335 help='Do not modify the files, only check the copyright (default action). ' +
336 'If specified together with --update, do the modifications ' +
337 'but produce output as if only --check was provided.')
338 parser.add_option('--update-year', action='store_true',
339 help='Update the copyright year if outdated')
340 parser.add_option('--replace-years', action='store_true',
341 help='Replace the copyright years with those given with --years')
342 parser.add_option('--update-header', action='store_true',
343 help='Update the copyright header if outdated')
344 parser.add_option('--replace-header', action='store_true',
345 help='Replace any copyright header with the current one')
346 parser.add_option('--remove-old-copyrights', action='store_true',
347 help='Remove copyright statements not in the new format')
348 parser.add_option('--add-missing', action='store_true',
349 help='Add missing copyright headers')
350 options, args = parser.parse_args()
352 filenames = args
353 if options.files:
354 with open(options.files, 'r') as filelist:
355 filenames = [x.strip() for x in filelist.read().splitlines()]
356 elif not filenames:
357 filenames = ['-']
359 # Default is --check if nothing provided.
360 if not options.check and not options.update_year and \
361 not options.update_header and not options.replace_header and \
362 not options.add_missing:
363 options.check = True
365 return options, filenames
367 def main():
368 """Do processing as a stand-alone script."""
369 options, filenames = process_options()
370 years = options.years
371 if not years:
372 years = str(datetime.date.today().year)
373 if years.endswith(','):
374 years = years[:-1]
376 checker = CopyrightChecker()
378 # Process each input file in turn.
379 for filename in filenames:
380 comment_handler = select_comment_handler(options.lang, filename)
382 # Read the input file. We are doing an in-place operation, so can't
383 # operate in pass-through mode.
384 if filename == '-':
385 contents = sys.stdin.read().splitlines()
386 reporter = Reporter(sys.stderr, '<stdin>')
387 else:
388 with open(filename, 'r') as inputfile:
389 contents = inputfile.read().splitlines()
390 reporter = Reporter(sys.stdout, filename)
392 output = []
393 # Keep lines that must be at the beginning of the file and skip them in
394 # the check.
395 if contents and (contents[0].startswith('#!/') or \
396 contents[0].startswith('%code requires') or \
397 contents[0].startswith('/* #if')):
398 output.append(contents[0])
399 contents = contents[1:]
400 # Remove and skip empty lines at the beginning.
401 while contents and len(contents[0]) == 0:
402 contents = contents[1:]
404 # Analyze the first comment block in the file.
405 comment_block, line_count = comment_handler.extract_first_comment_block(contents)
406 state = checker.check_copyright(comment_block)
407 need_update, file_years = checker.process_copyright(state, options, years, reporter)
408 if state.other_copyrights and options.remove_old_copyrights:
409 need_update = True
410 state.other_copyrights = []
411 reporter.report('old copyrights removed')
413 if need_update:
414 # Remove the original comment if it was a copyright comment.
415 if state.has_copyright:
416 contents = contents[line_count:]
417 new_block = checker.get_copyright_text(file_years, state.other_copyrights)
418 output.extend(comment_handler.create_comment_block(new_block))
420 # Write the output file if required.
421 if need_update or filename == '-':
422 # Append the rest of the input file as it was.
423 output.extend(contents)
424 output = '\n'.join(output) + '\n'
425 if filename == '-':
426 sys.stdout.write(output)
427 else:
428 with open(filename, 'w') as outputfile:
429 outputfile.write(output)
431 if __name__ == "__main__":
432 main()