3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2013,2014,2015,2016,2018 by the GROMACS development team.
6 # Copyright (c) 2019,2020, by the GROMACS development team, led by
7 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
8 # and including many others, as listed in the AUTHORS file in the
9 # top-level source directory and at http://www.gromacs.org.
11 # GROMACS is free software; you can redistribute it and/or
12 # modify it under the terms of the GNU Lesser General Public License
13 # as published by the Free Software Foundation; either version 2.1
14 # of the License, or (at your option) any later version.
16 # GROMACS is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 # Lesser General Public License for more details.
21 # You should have received a copy of the GNU Lesser General Public
22 # License along with GROMACS; if not, see
23 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
24 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 # If you want to redistribute modifications to GROMACS, please
27 # consider that scientific software is very special. Version
28 # control is crucial - bugs must be traceable. We will be happy to
29 # consider code for inclusion in the official distribution, but
30 # derived work must not be called official GROMACS. Details are found
31 # in the README & COPYING files - if they are missing, get the
32 # official version at http://www.gromacs.org.
34 # To help us fund GROMACS development, we humbly ask that you cite
35 # the research papers on the package. Check out http://www.gromacs.org.
37 """Checks and/or updates copyright headers in GROMACS source files.
39 It is used internally by several bash scripts to do copyright-relates tasks,
40 but can also be invoked directly for some rare use cases.
42 See docs/dev-manual/code-formatting.rst for more details.
50 from optparse
import OptionParser
52 class CopyrightState(object):
54 """Information about an existing (or non-existing) copyright header."""
56 def __init__(self
, has_copyright
, is_correct
, is_newstyle
, years
, other_copyrights
):
57 self
.has_copyright
= has_copyright
58 self
.is_correct
= is_correct
59 self
.is_newstyle
= is_newstyle
61 self
.other_copyrights
= other_copyrights
63 class CopyrightChecker(object):
65 """Logic for analyzing existing copyright headers and generating new ones."""
67 _header
= ["", "This file is part of the GROMACS molecular simulation package.", ""]
68 _copyright
= "Copyright (c) {0}, by the GROMACS development team, led by"
70 Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
71 and including many others, as listed in the AUTHORS file in the
72 top-level source directory and at http://www.gromacs.org.
74 GROMACS is free software; you can redistribute it and/or
75 modify it under the terms of the GNU Lesser General Public License
76 as published by the Free Software Foundation; either version 2.1
77 of the License, or (at your option) any later version.
79 GROMACS is distributed in the hope that it will be useful,
80 but WITHOUT ANY WARRANTY; without even the implied warranty of
81 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
82 Lesser General Public License for more details.
84 You should have received a copy of the GNU Lesser General Public
85 License along with GROMACS; if not, see
86 http://www.gnu.org/licenses, or write to the Free Software Foundation,
87 Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
89 If you want to redistribute modifications to GROMACS, please
90 consider that scientific software is very special. Version
91 control is crucial - bugs must be traceable. We will be happy to
92 consider code for inclusion in the official distribution, but
93 derived work must not be called official GROMACS. Details are found
94 in the README & COPYING files - if they are missing, get the
95 official version at http://www.gromacs.org.
97 To help us fund GROMACS development, we humbly ask that you cite
98 the research papers on the package. Check out http://www.gromacs.org.
99 """.strip().splitlines()
101 def check_copyright(self
, comment_block
):
102 """Analyze existing copyright header for correctness and extract information."""
103 copyright_re
= r
'Copyright \(c\) (([0-9]{4}[,-])*[0-9]{4}),? by the GROMACS development team,'
104 has_copyright
= False
109 append_next_line_to_other_copyrights
= False
111 other_copyrights
= []
112 for line
in comment_block
:
113 if append_next_line_to_other_copyrights
:
114 other_copyrights
[-1] += ' ' + line
115 append_next_line_to_other_copyrights
= False
117 if 'Copyright' in line
:
119 match
= re
.match(copyright_re
, line
)
121 existing_years
= match
.group(1)
122 new_line
= self
._copyright
.format(existing_years
)
126 other_copyrights
.append(line
[line
.find('Copyright'):])
127 if not line
.startswith('Copyright'):
128 append_next_line_to_other_copyrights
= True
129 if next_header_line
!= -1 or next_footer_line
!= 0:
132 if line
.startswith('Written by the Gromacs development team'):
134 if next_header_line
>= 0:
135 if line
== self
._header
[next_header_line
]:
136 next_header_line
+= 1
137 if next_header_line
>= len(self
._header
):
138 next_header_line
= -1
142 elif next_footer_line
>= 0:
143 if line
== self
._footer
[next_footer_line
]:
144 next_footer_line
+= 1
145 if next_footer_line
>= len(self
._footer
):
146 next_footer_line
= -1
151 if next_header_line
!= -1 or next_footer_line
!= -1:
154 return CopyrightState(has_copyright
, is_correct
, is_newstyle
, existing_years
, other_copyrights
)
156 def process_copyright(self
, state
, options
, current_years
, reporter
):
157 """Determine whether a copyrigth header needs to be updated and report issues."""
161 if options
.replace_years
:
162 if state
.years
!= current_years
:
164 reporter
.report('copyright years replaced')
165 new_years
= current_years
167 new_years
= state
.years
168 if not new_years
.endswith(current_years
):
169 if options
.update_year
:
171 new_years
+= ',' + current_years
172 if options
.check
or not need_update
:
173 reporter
.report('copyright year outdated')
175 reporter
.report('copyright year added')
177 new_years
= current_years
179 if not state
.has_copyright
:
180 if options
.add_missing
:
182 if options
.check
or not need_update
:
183 reporter
.report('copyright header missing')
184 elif options
.add_missing
:
185 reporter
.report('copyright header added')
187 if not state
.is_newstyle
:
188 if options
.replace_header
:
190 if options
.check
or not need_update
:
191 reporter
.report('copyright header incorrect')
193 reporter
.report('copyright header replaced')
194 elif not state
.is_correct
:
195 if options
.update_header
:
197 if options
.check
or not need_update
:
198 reporter
.report('copyright header outdated')
200 reporter
.report('copyright header updated')
202 return need_update
, new_years
204 def get_copyright_text(self
, years
, other_copyrights
):
205 """Construct a new copyright header."""
207 output
.extend(self
._header
)
209 for line
in other_copyrights
:
210 outline
= line
.rstrip()
211 if outline
.endswith(','):
212 outline
= outline
[:-1]
213 if not outline
.endswith('.'):
215 output
.append(outline
)
216 output
.append(self
._copyright
.format(years
))
217 output
.extend(self
._footer
)
220 class Reporter(object):
222 """Wrapper for reporting issues in a file."""
224 def __init__(self
, reportfile
, filename
):
225 self
._reportfile
= reportfile
226 self
._filename
= filename
228 def report(self
, text
):
229 self
._reportfile
.write(self
._filename
+ ': ' + text
+ '\n');
231 class CommentHandlerC(object):
233 """Handler for extracting and creating C-style comments."""
235 def extract_first_comment_block(self
, content_lines
):
236 if not content_lines
or not content_lines
[0].startswith('/*'):
238 comment_block
= [content_lines
[0][2:].strip()]
240 while line_index
< len(content_lines
):
241 line
= content_lines
[line_index
]
242 if '*/' in content_lines
[line_index
]:
244 comment_block
.append(line
.lstrip('* ').rstrip())
246 return (comment_block
, line_index
+ 1)
248 def create_comment_block(self
, lines
):
250 output
.append(('/* ' + lines
[0]).rstrip())
251 output
.extend([(' * ' + x
).rstrip() for x
in lines
[1:]])
255 class CommentHandlerSimple(object):
257 """Handler for extracting and creating sh-style comments.
259 Also other comments of the same type, but with a different comment
260 character are supported."""
262 def __init__(self
, comment_char
):
263 self
._comment
_char
= comment_char
265 def extract_first_comment_block(self
, content_lines
):
266 if not content_lines
or not content_lines
[0].startswith(self
._comment
_char
):
270 while line_index
< len(content_lines
):
271 line
= content_lines
[line_index
]
272 if not line
.startswith(self
._comment
_char
):
274 comment_block
.append(line
.lstrip(self
._comment
_char
+ ' ').rstrip())
276 if line
== self
._comment
_char
+ ' the research papers on the package. Check out http://www.gromacs.org.':
278 while line_index
< len(content_lines
):
279 line
= content_lines
[line_index
].rstrip()
280 if len(line
) > 0 and line
!= self
._comment
_char
:
283 return (comment_block
, line_index
)
285 def create_comment_block(self
, lines
):
287 output
.extend([(self
._comment
_char
+ ' ' + x
).rstrip() for x
in lines
])
292 'c': CommentHandlerC(),
293 'tex': CommentHandlerSimple('%'),
294 'sh': CommentHandlerSimple('#')
297 def select_comment_handler(override
, filename
):
298 """Select comment handler for a file based on file name and input options."""
300 if not filetype
and filename
!= '-':
301 basename
= os
.path
.basename(filename
)
302 root
, ext
= os
.path
.splitext(basename
)
303 if ext
== '.cmakein':
304 dummy
, ext2
= os
.path
.splitext(root
)
307 if ext
in ('.c', '.cu', '.cpp', '.cl', '.h', '.cuh', '.clh', '.y', '.l', '.pre', '.bm'):
309 elif ext
in ('.tex',):
311 elif basename
in ('CMakeLists.txt', 'GMXRC', 'git-pre-commit') or \
312 ext
in ('.cmake', '.cmakein', '.py', '.sh', '.bash', '.csh', '.zsh'):
314 if filetype
in comment_handlers
:
315 return comment_handlers
[filetype
]
317 sys
.stderr
.write("Unsupported input format: {0}\n".format(filetype
))
318 elif filename
!= '-':
319 sys
.stderr
.write("Unsupported input format: {0}\n".format(filename
))
321 sys
.stderr
.write("No file name or file type provided.\n")
324 def create_copyright_header(years
, other_copyrights
=None, language
='c'):
325 if language
not in comment_handlers
:
326 sys
.strerr
.write("Unsupported language: {0}\n".format(language
))
328 copyright_checker
= CopyrightChecker()
329 comment_handler
= comment_handlers
[language
]
330 copyright_lines
= copyright_checker
.get_copyright_text(years
, other_copyrights
)
331 comment_lines
= comment_handler
.create_comment_block(copyright_lines
)
332 return '\n'.join(comment_lines
) + '\n'
334 def process_options():
335 """Process input options."""
336 parser
= OptionParser()
337 parser
.add_option('-l', '--lang',
338 help='Comment type to use (c or sh)')
339 parser
.add_option('-y', '--years',
340 help='Comma-separated list of years')
341 parser
.add_option('-F', '--files',
342 help='File to read list of files from')
343 parser
.add_option('--check', action
='store_true',
344 help='Do not modify the files, only check the copyright (default action). ' +
345 'If specified together with --update, do the modifications ' +
346 'but produce output as if only --check was provided.')
347 parser
.add_option('--update-year', action
='store_true',
348 help='Update the copyright year if outdated')
349 parser
.add_option('--replace-years', action
='store_true',
350 help='Replace the copyright years with those given with --years')
351 parser
.add_option('--update-header', action
='store_true',
352 help='Update the copyright header if outdated')
353 parser
.add_option('--replace-header', action
='store_true',
354 help='Replace any copyright header with the current one')
355 parser
.add_option('--remove-old-copyrights', action
='store_true',
356 help='Remove copyright statements not in the new format')
357 parser
.add_option('--add-missing', action
='store_true',
358 help='Add missing copyright headers')
359 options
, args
= parser
.parse_args()
363 with
open(options
.files
, 'r') as filelist
:
364 filenames
= [x
.strip() for x
in filelist
.read().splitlines()]
368 # Default is --check if nothing provided.
369 if not options
.check
and not options
.update_year
and \
370 not options
.update_header
and not options
.replace_header
and \
371 not options
.add_missing
:
374 return options
, filenames
377 """Do processing as a stand-alone script."""
378 options
, filenames
= process_options()
379 years
= options
.years
381 years
= str(datetime
.date
.today().year
)
382 if years
.endswith(','):
385 checker
= CopyrightChecker()
387 # Process each input file in turn.
388 for filename
in filenames
:
389 comment_handler
= select_comment_handler(options
.lang
, filename
)
391 # Read the input file. We are doing an in-place operation, so can't
392 # operate in pass-through mode.
394 contents
= sys
.stdin
.read().splitlines()
395 reporter
= Reporter(sys
.stderr
, '<stdin>')
397 with
open(filename
, 'r', encoding
='utf-8') as inputfile
:
398 contents
= inputfile
.read().splitlines()
399 reporter
= Reporter(sys
.stdout
, filename
)
402 # Keep lines that must be at the beginning of the file and skip them in
404 if contents
and (contents
[0].startswith('#!/') or \
405 contents
[0].startswith('%code requires') or \
406 contents
[0].startswith('/* #if')):
407 output
.append(contents
[0])
408 contents
= contents
[1:]
409 # Remove and skip empty lines at the beginning.
410 while contents
and len(contents
[0]) == 0:
411 contents
= contents
[1:]
413 # Analyze the first comment block in the file.
414 comment_block
, line_count
= comment_handler
.extract_first_comment_block(contents
)
415 state
= checker
.check_copyright(comment_block
)
416 need_update
, file_years
= checker
.process_copyright(state
, options
, years
, reporter
)
417 if state
.other_copyrights
and options
.remove_old_copyrights
:
419 state
.other_copyrights
= []
420 reporter
.report('old copyrights removed')
423 # Remove the original comment if it was a copyright comment.
424 if state
.has_copyright
:
425 contents
= contents
[line_count
:]
426 new_block
= checker
.get_copyright_text(file_years
, state
.other_copyrights
)
427 output
.extend(comment_handler
.create_comment_block(new_block
))
429 # Write the output file if required.
430 if need_update
or filename
== '-':
431 # Append the rest of the input file as it was.
432 output
.extend(contents
)
433 output
= '\n'.join(output
) + '\n'
435 sys
.stdout
.write(output
)
437 with
open(filename
, 'w') as outputfile
:
438 outputfile
.write(output
)
440 if __name__
== "__main__":