085542e16539b8f3f1902b2c6101cf926538e9b3
[alexxy/gromacs.git] / admin / copyright.py
1 #!/usr/bin/python
2 #
3 # This file is part of the GROMACS molecular simulation package.
4 #
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.
9 #
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.
14 #
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.
19 #
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.
24 #
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.
32 #
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.
35
36 import datetime
37 import os.path
38 import re
39 import sys
40
41 from optparse import OptionParser
42
43 class CopyrightState(object):
44
45     """Information about an existing (or non-existing) copyright header."""
46
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
53
54 class CopyrightChecker(object):
55
56     """Logic for analyzing existing copyright headers and generating new ones."""
57
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.
64
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.
69
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.
74
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.
79
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.
87
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()
91
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         existing_years = ''
101         other_copyrights = []
102         for line in comment_block:
103             if 'Copyright' in line:
104                 has_copyright = True
105                 match = re.match(copyright_re, line)
106                 if match:
107                     existing_years = match.group(1)
108                     new_line = self._copyright.format(existing_years)
109                     if line != new_line:
110                         is_correct = False
111                 else:
112                     other_copyrights.append(line)
113                 if next_header_line != -1 or next_footer_line != 0:
114                     is_correct = False
115                 continue
116             if line.startswith('Written by the Gromacs development team'):
117                 has_copyright = True
118             if next_header_line >= 0:
119                 if line == self._header[next_header_line]:
120                     next_header_line += 1
121                     if next_header_line >= len(self._header):
122                         next_header_line = -1
123                 else:
124                     is_correct = False
125                     is_newstyle = False
126             elif next_footer_line >= 0:
127                 if line == self._footer[next_footer_line]:
128                     next_footer_line += 1
129                     if next_footer_line >= len(self._footer):
130                         next_footer_line = -1
131                 else:
132                     is_correct = False
133             else:
134                 is_correct = False
135         if next_header_line != -1 or next_footer_line != -1:
136             is_correct = False
137
138         return CopyrightState(has_copyright, is_correct, is_newstyle, existing_years, other_copyrights)
139
140     def process_copyright(self, state, options, current_years, reporter):
141         """Determine whether a copyrigth header needs to be updated and report issues."""
142         need_update = False
143
144         if state.years:
145             if options.replace_years:
146                 if state.years != current_years:
147                     need_update = True
148                     reporter.report('copyright years replaced')
149                 new_years = current_years
150             else:
151                 new_years = state.years
152                 if not new_years.endswith(current_years):
153                     if options.update_year:
154                         need_update = True
155                         new_years += ',' + current_years
156                     if options.check or not need_update:
157                         reporter.report('copyright year outdated')
158                     else:
159                         reporter.report('copyright year added')
160         else:
161             new_years = current_years
162
163         if not state.has_copyright:
164             if options.add_missing:
165                 need_update = True
166             if options.check or not need_update:
167                 reporter.report('copyright header missing')
168             elif options.add_missing:
169                 reporter.report('copyright header added')
170         else:
171             if not state.is_newstyle:
172                 if options.replace_header:
173                     need_update = True
174                 if options.check or not need_update:
175                     reporter.report('copyright header incorrect')
176                 else:
177                     reporter.report('copyright header replaced')
178             elif not state.is_correct:
179                 if options.update_header:
180                     need_update = True
181                 if options.check or not need_update:
182                     reporter.report('copyright header outdated')
183                 else:
184                     reporter.report('copyright header updated')
185
186         return need_update, new_years
187
188     def get_copyright_text(self, years, other_copyrights):
189         """Construct a new copyright header."""
190         output = []
191         output.extend(self._header)
192         if other_copyrights:
193             for line in other_copyrights:
194                 outline = line.rstrip()
195                 if outline.endswith(','):
196                     outline = outline[:-1]
197                 if not outline.endswith('.'):
198                     outline += '.'
199                 output.append(outline)
200         output.append(self._copyright.format(years))
201         output.extend(self._footer)
202         return output
203
204 class Reporter(object):
205
206     """Wrapper for reporting issues in a file."""
207
208     def __init__(self, reportfile, filename):
209         self._reportfile = reportfile
210         self._filename = filename
211
212     def report(self, text):
213         self._reportfile.write(self._filename + ': ' + text + '\n');
214
215 class CommentHandlerC(object):
216
217     """Handler for extracting and creating C-style comments."""
218
219     def extract_first_comment_block(self, content_lines):
220         if not content_lines or not content_lines[0].startswith('/*'):
221             return ([], 0)
222         comment_block = [content_lines[0][2:].strip()]
223         line_index = 1
224         while line_index < len(content_lines):
225             line = content_lines[line_index]
226             if '*/' in content_lines[line_index]:
227                 break
228             comment_block.append(line.lstrip('* ').rstrip())
229             line_index += 1
230         return (comment_block, line_index + 1)
231
232     def create_comment_block(self, lines):
233         output = []
234         output.append(('/* ' + lines[0]).rstrip())
235         output.extend([(' * ' + x).rstrip() for x in lines[1:]])
236         output.append(' */')
237         return output
238
239 class CommentHandlerSh(object):
240
241     """Handler for extracting and creating sh-style comments."""
242
243     def extract_first_comment_block(self, content_lines):
244         if not content_lines or not content_lines[0].startswith('#'):
245             return ([], 0)
246         comment_block = []
247         line_index = 0
248         while line_index < len(content_lines):
249             line = content_lines[line_index]
250             if not line.startswith('#'):
251                 break
252             comment_block.append(line.lstrip('# ').rstrip())
253             line_index += 1
254             if line == '# the research papers on the package. Check out http://www.gromacs.org.':
255                 break
256         while line_index < len(content_lines):
257             line = content_lines[line_index].rstrip()
258             if len(line) > 0 and line != '#':
259                 break
260             line_index += 1
261         return (comment_block, line_index)
262
263     def create_comment_block(self, lines):
264         output = []
265         output.extend([('# ' + x).rstrip() for x in lines])
266         output.append('')
267         return output
268
269 comment_handlers = {'c': CommentHandlerC(), 'sh': CommentHandlerSh()}
270
271 def select_comment_handler(override, filename):
272     """Select comment handler for a file based on file name and input options."""
273     filetype = override
274     if not filetype and filename != '-':
275         basename = os.path.basename(filename)
276         root, ext = os.path.splitext(basename)
277         if ext == '.cmakein':
278             dummy, ext2 = os.path.splitext(root)
279             if ext2:
280                 ext = ext2
281         if ext in ('.c', '.cpp', '.h', '.y', '.l', '.pre'):
282             filetype = 'c'
283         elif basename in ('CMakeLists.txt', 'GMXRC', 'git-pre-commit') or \
284                 ext in ('.cmake', '.cmakein', '.py', '.sh', '.bash', '.csh', '.zsh'):
285             filetype = 'sh'
286     if filetype in comment_handlers:
287         return comment_handlers[filetype]
288     if filetype:
289         sys.stderr.write("Unsupported input format: {0}\n".format(filetype))
290     elif filename != '-':
291         sys.stderr.write("Unsupported input format: {0}\n".format(filename))
292     else:
293         sys.stderr.write("No file name or file type provided.\n")
294     sys.exit(1)
295
296 def create_copyright_header(years, other_copyrights=None, language='c'):
297     if language not in comment_handlers:
298         sys.strerr.write("Unsupported language: {0}\n".format(language))
299         sys.exit(1)
300     copyright_checker = CopyrightChecker()
301     comment_handler = comment_handlers[language]
302     copyright_lines = copyright_checker.get_copyright_text(years, other_copyrights)
303     comment_lines = comment_handler.create_comment_block(copyright_lines)
304     return '\n'.join(comment_lines) + '\n'
305
306 def process_options():
307     """Process input options."""
308     parser = OptionParser()
309     parser.add_option('-l', '--lang',
310                       help='Comment type to use (c or sh)')
311     parser.add_option('-y', '--years',
312                       help='Comma-separated list of years')
313     parser.add_option('-F', '--files',
314                       help='File to read list of files from')
315     parser.add_option('--check', action='store_true',
316                       help='Do not modify the files, only check the copyright (default action). ' +
317                            'If specified together with --update, do the modifications ' +
318                            'but produce output as if only --check was provided.')
319     parser.add_option('--update-year', action='store_true',
320                       help='Update the copyright year if outdated')
321     parser.add_option('--replace-years', action='store_true',
322                       help='Replace the copyright years with those given with --years')
323     parser.add_option('--update-header', action='store_true',
324                       help='Update the copyright header if outdated')
325     parser.add_option('--replace-header', action='store_true',
326                       help='Replace any copyright header with the current one')
327     parser.add_option('--add-missing', action='store_true',
328                       help='Add missing copyright headers')
329     options, args = parser.parse_args()
330
331     filenames = args
332     if options.files:
333         with open(options.files, 'r') as filelist:
334             filenames = [x.strip() for x in filelist.read().splitlines()]
335     elif not filenames:
336         filenames = ['-']
337
338     # Default is --check if nothing provided.
339     if not options.check and not options.update_year and \
340             not options.update_header and not options.replace_header and \
341             not options.add_missing:
342         options.check = True
343
344     return options, filenames
345
346 def main():
347     """Do processing as a stand-alone script."""
348     options, filenames = process_options()
349     years = options.years
350     if not years:
351         years = str(datetime.date.today().year)
352     if years.endswith(','):
353         years = years[:-1]
354
355     checker = CopyrightChecker()
356
357     # Process each input file in turn.
358     for filename in filenames:
359         comment_handler = select_comment_handler(options.lang, filename)
360
361         # Read the input file.  We are doing an in-place operation, so can't
362         # operate in pass-through mode.
363         if filename == '-':
364             contents = sys.stdin.read().splitlines()
365             reporter = Reporter(sys.stderr, '<stdin>')
366         else:
367             with open(filename, 'r') as inputfile:
368                 contents = inputfile.read().splitlines()
369             reporter = Reporter(sys.stdout, filename)
370
371         output = []
372         # Keep lines that must be at the beginning of the file and skip them in
373         # the check.
374         if contents and (contents[0].startswith('#!/') or \
375                 contents[0].startswith('%code requires') or \
376                 contents[0].startswith('/* #if')):
377             output.append(contents[0])
378             contents = contents[1:]
379         # Remove and skip empty lines at the beginning.
380         while contents and len(contents[0]) == 0:
381             contents = contents[1:]
382
383         # Analyze the first comment block in the file.
384         comment_block, line_count = comment_handler.extract_first_comment_block(contents)
385         state = checker.check_copyright(comment_block)
386         need_update, file_years = checker.process_copyright(state, options, years, reporter)
387
388         if need_update:
389             # Remove the original comment if it was a copyright comment.
390             if state.has_copyright:
391                 contents = contents[line_count:]
392             new_block = checker.get_copyright_text(file_years, state.other_copyrights)
393             output.extend(comment_handler.create_comment_block(new_block))
394
395         # Write the output file if required.
396         if need_update or filename == '-':
397             # Append the rest of the input file as it was.
398             output.extend(contents)
399             output = '\n'.join(output) + '\n'
400             if filename == '-':
401                 sys.stdout.write(output)
402             else:
403                 with open(filename, 'w') as outputfile:
404                     outputfile.write(output)
405
406 if __name__ == "__main__":
407     main()