3 # Copyright (C) 2013-2024 Free Software Foundation, Inc.
5 # This script is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3, or (at your option)
10 # This script adjusts the copyright notices at the top of source files
11 # so that they have the form:
13 # Copyright XXXX-YYYY Free Software Foundation, Inc.
15 # It doesn't change code that is known to be maintained elsewhere or
16 # that carries a non-FSF copyright.
18 # The script also doesn't change testsuite files, except those in
19 # libstdc++-v3. This is because libstdc++-v3 has a conformance testsuite,
20 # while most tests in other directories are just things that failed at some
23 # Pass --this-year to the script if you want it to add the current year
24 # to all applicable notices. Pass --quilt if you are using quilt and
25 # want files to be added to the quilt before being changed.
27 # By default the script will update all directories for which the
28 # output has been vetted. You can instead pass the names of individual
29 # directories, including those that haven't been approved. So:
31 # update-copyright.py --this-year
33 # is the command that would be used at the beginning of a year to update
34 # all copyright notices (and possibly at other times to check whether
35 # new files have been added with old years). On the other hand:
37 # update-copyright.py --this-year libitm
39 # would run the script on just libitm/.
41 # Note that things like --version output strings must be updated before
42 # this script is run. There's already a separate procedure for that.
54 def report (self
, filename
, string
):
56 string
= filename
+ ': ' + string
57 sys
.stderr
.write (string
+ '\n')
61 return self
.num_errors
== 0
65 self
.skip_files
= set()
66 self
.skip_dirs
= set()
67 self
.skip_extensions
= set([
71 self
.fossilised_files
= set()
72 self
.own_files
= set()
74 self
.skip_files |
= set ([
86 'gpl_v3_without_node.texi',
88 # Skip auto- and libtool-related files
109 # Skip FSF mission statement, etc.
114 # Skip imported texinfo files.
119 def get_line_filter (self
, dir, filename
):
120 if filename
.startswith ('ChangeLog'):
121 # Ignore references to copyright in changelog entries.
122 return re
.compile ('\t')
126 def skip_file (self
, dir, filename
):
127 if filename
in self
.skip_files
:
130 (base
, extension
) = os
.path
.splitext (os
.path
.join (dir, filename
))
131 if extension
in self
.skip_extensions
:
134 if extension
== '.in':
135 # Skip .in files produced by automake.
136 if os
.path
.exists (base
+ '.am'):
139 # Skip files produced by autogen
140 if (os
.path
.exists (base
+ '.def')
141 and os
.path
.exists (base
+ '.tpl')):
144 # Skip configure files produced by autoconf
145 if filename
== 'configure':
146 if os
.path
.exists (base
+ '.ac'):
148 if os
.path
.exists (base
+ '.in'):
153 def skip_dir (self
, dir, subdir
):
154 return subdir
in self
.skip_dirs
156 def is_fossilised_file (self
, dir, filename
):
157 if filename
in self
.fossilised_files
:
159 # Only touch current current ChangeLogs.
160 if filename
!= 'ChangeLog' and filename
.find ('ChangeLog') >= 0:
164 def by_package_author (self
, dir, filename
):
165 return filename
in self
.own_files
168 def __init__ (self
, errors
):
171 # Characters in a range of years. Include '.' for typos.
172 ranges
= '[0-9](?:[-0-9.,\s]|\s+and\s+)*[0-9]'
174 # Non-whitespace characters in a copyright holder's name.
178 self
.year_re
= re
.compile ('[0-9]+')
180 # Matches part of a year or copyright holder.
181 self
.continuation_re
= re
.compile (ranges
+ '|' + name
)
183 # Matches a full copyright notice:
184 self
.copyright_re
= re
.compile (
185 # 1: 'Copyright (C)', etc.
187 '|[Cc]opyright\s+\([Cc]\)'
189 '|[Cc]opyright\s+©'
190 '|[Cc]opyright\s+@copyright{}'
192 '|@set\s+copyright[\w-]+)'
194 # 2: the years. Include the whitespace in the year, so that
195 # we can remove any excess.
196 '(\s*(?:' + ranges
+ ',?'
197 '|@value\{[^{}]*\})\s*)'
202 # 4: the copyright holder. Don't allow multiple consecutive
203 # spaces, so that right-margin gloss doesn't get caught
204 # (e.g. gnat_ugn.texi).
205 '(' + name
+ '(?:\s?' + name
+ ')*)?')
207 # A regexp for notices that might have slipped by. Just matching
208 # 'copyright' is too noisy, and 'copyright.*[0-9]' falls foul of
209 # HTML header markers, so check for 'copyright' and two digits.
210 self
.other_copyright_re
= re
.compile ('copyright.*[0-9][0-9]',
212 self
.comment_re
= re
.compile('#+|[*]+|;+|%+|//+|@c |dnl ')
213 self
.holders
= { '@copying': '@copying' }
214 self
.holder_prefixes
= set()
216 # True to 'quilt add' files before changing them.
217 self
.use_quilt
= False
219 # If set, force all notices to include this year.
222 # Goes after the year(s). Could be ', '.
225 def add_package_author (self
, holder
, canon_form
= None):
228 self
.holders
[holder
] = canon_form
229 index
= holder
.find (' ')
231 self
.holder_prefixes
.add (holder
[:index
])
232 index
= holder
.find (' ', index
+ 1)
234 def add_external_author (self
, holder
):
235 self
.holders
[holder
] = None
237 class BadYear (Exception):
238 def __init__ (self
, year
):
242 return 'unrecognised year: ' + self
.year
244 def parse_year (self
, string
):
246 if len (string
) == 2:
249 elif len (string
) == 4:
251 raise self
.BadYear (string
)
253 def year_range (self
, years
):
254 year_list
= [self
.parse_year (year
)
255 for year
in self
.year_re
.findall (years
)]
256 assert len (year_list
) > 0
257 return (min (year_list
), max (year_list
))
259 def set_use_quilt (self
, use_quilt
):
260 self
.use_quilt
= use_quilt
262 def include_year (self
, year
):
263 assert not self
.max_year
266 def canonicalise_years (self
, dir, filename
, filter, years
):
267 # Leave texinfo variables alone.
268 if years
.startswith ('@value'):
271 (min_year
, max_year
) = self
.year_range (years
)
273 # Update the upper bound, if enabled.
274 if self
.max_year
and not filter.is_fossilised_file (dir, filename
):
275 max_year
= max (max_year
, self
.max_year
)
278 if min_year
== max_year
:
279 return '%d' % min_year
281 return '%d-%d' % (min_year
, max_year
)
283 def strip_continuation (self
, line
):
285 match
= self
.comment_re
.match (line
)
287 line
= line
[match
.end():].lstrip()
290 def is_complete (self
, match
):
291 holder
= match
.group (4)
293 and (holder
not in self
.holder_prefixes
294 or holder
in self
.holders
))
296 def update_copyright (self
, dir, filename
, filter, file, line
, match
):
299 pathname
= os
.path
.join (dir, filename
)
301 intro
= match
.group (1)
302 if intro
.startswith ('@set'):
303 # Texinfo year variables should always be on one line
304 after_years
= line
[match
.end (2):].strip()
305 if after_years
!= '':
306 self
.errors
.report (pathname
,
307 'trailing characters in @set: '
309 return (False, orig_line
, next_line
)
311 # If it looks like the copyright is incomplete, add the next line.
312 while not self
.is_complete (match
):
314 next_line
= file.readline()
315 except StopIteration:
318 # If the next line doesn't look like a proper continuation,
319 # assume that what we've got is complete.
320 continuation
= self
.strip_continuation (next_line
)
321 if not self
.continuation_re
.match (continuation
):
324 # Merge the lines for matching purposes.
325 orig_line
+= next_line
326 line
= line
.rstrip() + ' ' + continuation
329 # Rematch with the longer line, at the original position.
330 match
= self
.copyright_re
.match (line
, match
.start())
333 holder
= match
.group (4)
335 # Use the filter to test cases where markup is getting in the way.
336 if filter.by_package_author (dir, filename
):
337 assert holder
not in self
.holders
340 self
.errors
.report (pathname
, 'missing copyright holder')
341 return (False, orig_line
, next_line
)
343 elif holder
not in self
.holders
:
344 self
.errors
.report (pathname
,
345 'unrecognised copyright holder: ' + holder
)
346 return (False, orig_line
, next_line
)
349 # See whether the copyright is associated with the package
351 canon_form
= self
.holders
[holder
]
353 return (False, orig_line
, next_line
)
355 # Make sure the author is given in a consistent way.
356 line
= (line
[:match
.start (4)]
358 + line
[match
.end (4):])
361 line
= line
[:match
.start (3)] + line
[match
.end (3):]
363 # Update the copyright years.
364 years
= match
.group (2).strip()
366 canon_form
= self
.canonicalise_years (dir, filename
, filter, years
)
367 except self
.BadYear
as e
:
368 self
.errors
.report (pathname
, str (e
))
369 return (False, orig_line
, next_line
)
371 line
= (line
[:match
.start (2)]
372 + ('' if intro
.startswith ('copyright = ') else ' ')
373 + canon_form
+ self
.separator
374 + line
[match
.end (2):])
376 # Use the standard (C) form.
377 if intro
.endswith ('right'):
379 elif intro
.endswith ('(c)'):
380 intro
= intro
[:-3] + '(C)'
381 line
= line
[:match
.start (1)] + intro
+ line
[match
.end (1):]
383 # Strip trailing whitespace
384 line
= line
.rstrip() + '\n'
386 return (line
!= orig_line
, line
, next_line
)
388 def guess_encoding (self
, pathname
):
389 for encoding
in ('utf8', 'iso8859'):
391 open(pathname
, 'r', encoding
=encoding
).read()
393 except UnicodeDecodeError:
397 def process_file (self
, dir, filename
, filter):
398 pathname
= os
.path
.join (dir, filename
)
399 if filename
.endswith ('.tmp'):
400 # Looks like something we tried to create before.
409 line_filter
= filter.get_line_filter (dir, filename
)
411 encoding
= self
.guess_encoding(pathname
)
412 with
open (pathname
, 'r', encoding
=encoding
) as file:
414 mode
= os
.fstat (file.fileno()).st_mode
418 # Leave filtered-out lines alone.
419 if not (line_filter
and line_filter
.match (line
)):
420 match
= self
.copyright_re
.search (line
)
422 res
= self
.update_copyright (dir, filename
, filter,
424 (this_changed
, line
, next_line
) = res
425 changed
= changed
or this_changed
427 # Check for copyright lines that might have slipped by.
428 elif self
.other_copyright_re
.search (line
):
429 self
.errors
.report (pathname
,
430 'unrecognised copyright: %s'
435 # If something changed, write the new file out.
436 if changed
and self
.errors
.ok():
437 tmp_pathname
= pathname
+ '.tmp'
438 with
open (tmp_pathname
, 'w', encoding
=encoding
) as file:
441 os
.fchmod (file.fileno(), mode
)
443 subprocess
.call (['quilt', 'add', pathname
])
444 os
.rename (tmp_pathname
, pathname
)
446 def process_tree (self
, tree
, filter):
447 for (dir, subdirs
, filenames
) in os
.walk (tree
):
448 # Don't recurse through directories that should be skipped.
449 for i
in range (len (subdirs
) - 1, -1, -1):
450 if filter.skip_dir (dir, subdirs
[i
]):
453 # Handle the files in this directory.
454 for filename
in filenames
:
455 if filter.skip_file (dir, filename
):
456 sys
.stdout
.write ('Skipping %s\n'
457 % os
.path
.join (dir, filename
))
459 self
.process_file (dir, filename
, filter)
462 def __init__ (self
, copyright
= Copyright
):
463 self
.errors
= Errors()
464 self
.copyright
= copyright (self
.errors
)
466 self
.default_dirs
= []
467 self
.chosen_dirs
= []
468 self
.option_handlers
= dict()
469 self
.option_help
= []
471 self
.add_option ('--help', 'Print this help', self
.o_help
)
472 self
.add_option ('--quilt', '"quilt add" files before changing them',
474 self
.add_option ('--this-year', 'Add the current year to every notice',
477 def add_option (self
, name
, help, handler
):
478 self
.option_help
.append ((name
, help))
479 self
.option_handlers
[name
] = handler
481 def add_dir (self
, dir, filter = GenericFilter()):
482 self
.dirs
.append ((dir, filter))
484 def o_help (self
, option
= None):
485 sys
.stdout
.write ('Usage: %s [options] dir1 dir2...\n\n'
486 'Options:\n' % sys
.argv
[0])
487 format
= '%-15s %s\n'
488 for (what
, help) in self
.option_help
:
489 sys
.stdout
.write (format
% (what
, help))
490 sys
.stdout
.write ('\nDirectories:\n')
494 for (dir, filter) in self
.dirs
:
496 if i
% 3 == 0 or i
== len (self
.dirs
):
497 sys
.stdout
.write (dir + '\n')
499 sys
.stdout
.write (format
% dir)
502 def o_quilt (self
, option
):
503 self
.copyright
.set_use_quilt (True)
505 def o_this_year (self
, option
):
506 self
.copyright
.include_year (time
.localtime().tm_year
)
509 for arg
in sys
.argv
[1:]:
511 self
.chosen_dirs
.append (arg
)
512 elif arg
in self
.option_handlers
:
513 self
.option_handlers
[arg
] (arg
)
515 self
.errors
.report (None, 'unrecognised option: ' + arg
)
517 if len (self
.chosen_dirs
) == 0:
518 self
.chosen_dirs
= self
.default_dirs
519 if len (self
.chosen_dirs
) == 0:
522 for chosen_dir
in self
.chosen_dirs
:
523 canon_dir
= os
.path
.join (chosen_dir
, '')
525 for (dir, filter) in self
.dirs
:
526 if (dir + os
.sep
).startswith (canon_dir
):
528 self
.copyright
.process_tree (dir, filter)
530 self
.errors
.report (None, 'unrecognised directory: '
532 sys
.exit (0 if self
.errors
.ok() else 1)
534 #----------------------------------------------------------------------------
536 class TopLevelFilter (GenericFilter
):
537 def skip_dir (self
, dir, subdir
):
540 class ConfigFilter (GenericFilter
):
542 GenericFilter
.__init
__ (self
)
544 def skip_file (self
, dir, filename
):
545 if filename
.endswith ('.m4'):
546 pathname
= os
.path
.join (dir, filename
)
547 with
open (pathname
) as file:
548 # Skip files imported from gettext.
549 if file.readline().find ('gettext-') >= 0:
551 return GenericFilter
.skip_file (self
, dir, filename
)
553 class GCCFilter (GenericFilter
):
555 GenericFilter
.__init
__ (self
)
557 self
.skip_files |
= set ([
561 # Weird ways to compose copyright year
565 self
.skip_dirs |
= set ([
566 # Better not create a merge nightmare for the GNAT folks.
569 # Handled separately.
573 self
.skip_extensions |
= set ([
574 # Maintained by the translation project.
577 # Automatically-generated.
581 self
.fossilised_files |
= set ([
582 # Old news won't be updated.
586 class TestsuiteFilter (GenericFilter
):
588 GenericFilter
.__init
__ (self
)
590 self
.skip_extensions |
= set ([
591 # Don't change the tests, which could be woend by anyone.
607 def skip_file (self
, dir, filename
):
608 # g++.niklas/README contains historical copyright information
610 if filename
== 'README' and os
.path
.basename (dir) == 'g++.niklas':
612 # Similarly params/README.
613 if filename
== 'README' and os
.path
.basename (dir) == 'params':
615 if filename
== 'pdt_5.f03' and os
.path
.basename (dir) == 'gfortran.dg':
617 return GenericFilter
.skip_file (self
, dir, filename
)
619 class LibCppFilter (GenericFilter
):
621 GenericFilter
.__init
__ (self
)
623 self
.skip_extensions |
= set ([
624 # Maintained by the translation project.
627 # Automatically-generated.
631 class LibGCCFilter (GenericFilter
):
633 GenericFilter
.__init
__ (self
)
635 self
.skip_dirs |
= set ([
636 # Imported from GLIBC.
640 class LibPhobosFilter (GenericFilter
):
642 GenericFilter
.__init
__ (self
)
644 self
.skip_files |
= set ([
645 # Source modules imported from upstream.
650 self
.skip_dirs |
= set ([
651 # Contains sources imported from upstream.
660 class LibStdCxxFilter (GenericFilter
):
662 GenericFilter
.__init
__ (self
)
664 self
.skip_files |
= set ([
665 # Contains no copyright of its own, but quotes the GPL.
669 self
.skip_dirs |
= set ([
670 # Contains automatically-generated sources.
673 # The testsuite data files shouldn't be changed.
676 # Contains imported images
680 self
.own_files |
= set ([
681 # Contains markup around the copyright owner.
685 def get_line_filter (self
, dir, filename
):
686 if filename
== 'boost_concept_check.h':
687 return re
.compile ('// \(C\) Copyright Jeremy Siek')
688 return GenericFilter
.get_line_filter (self
, dir, filename
)
690 class ContribFilter(GenericFilter
):
692 GenericFilter
.__init
__ (self
)
694 self
.skip_files |
= set ([
695 # A different copyrights.
696 'unicode-license.txt',
703 class GCCCopyright (Copyright
):
704 def __init__ (self
, errors
):
705 Copyright
.__init
__ (self
, errors
)
707 canon_fsf
= 'Free Software Foundation, Inc.'
708 self
.add_package_author ('Free Software Foundation', canon_fsf
)
709 self
.add_package_author ('Free Software Foundation.', canon_fsf
)
710 self
.add_package_author ('Free Software Foundation Inc.', canon_fsf
)
711 self
.add_package_author ('Free Software Foundation, Inc', canon_fsf
)
712 self
.add_package_author ('Free Software Foundation, Inc.', canon_fsf
)
713 self
.add_package_author ('The Free Software Foundation', canon_fsf
)
714 self
.add_package_author ('The Free Software Foundation, Inc.', canon_fsf
)
715 self
.add_package_author ('Software Foundation, Inc.', canon_fsf
)
717 self
.add_external_author ('ARM')
718 self
.add_external_author ('AdaCore')
719 self
.add_external_author ('Advanced Micro Devices Inc.')
720 self
.add_external_author ('Ami Tavory and Vladimir Dreizin, IBM-HRL.')
721 self
.add_external_author ('Cavium Networks.')
722 self
.add_external_author ('David Malcolm')
723 self
.add_external_author ('Faraday Technology Corp.')
724 self
.add_external_author ('Florida State University')
725 self
.add_external_author ('Gerard Jungman')
726 self
.add_external_author ('Greg Colvin and Beman Dawes.')
727 self
.add_external_author ('Hewlett-Packard Company')
728 self
.add_external_author ('Intel Corporation')
729 self
.add_external_author ('Information Technology Industry Council.')
730 self
.add_external_author ('James Theiler, Brian Gough')
731 self
.add_external_author ('Makoto Matsumoto and Takuji Nishimura,')
732 self
.add_external_author ('Mentor Graphics Corporation')
733 self
.add_external_author ('National Research Council of Canada.')
734 self
.add_external_author ('NVIDIA Corporation')
735 self
.add_external_author ('Peter Dimov and Multi Media Ltd.')
736 self
.add_external_author ('Peter Dimov')
737 self
.add_external_author ('Pipeline Associates, Inc.')
738 self
.add_external_author ('Regents of the University of California.')
739 self
.add_external_author ('Silicon Graphics Computer Systems, Inc.')
740 self
.add_external_author ('Silicon Graphics')
741 self
.add_external_author ('Stephen L. Moshier')
742 self
.add_external_author ('Sun Microsystems, Inc. All rights reserved.')
743 self
.add_external_author ('The D Language Foundation, All Rights Reserved')
744 self
.add_external_author ('The fast_float authors')
745 self
.add_external_author ('The Go Authors. All rights reserved.')
746 self
.add_external_author ('The Go Authors. All rights reserved.')
747 self
.add_external_author ('The Go Authors.')
748 self
.add_external_author ('The Regents of the University of California.')
749 self
.add_external_author ('Ulf Adams')
750 self
.add_external_author ('Unicode, Inc.')
751 self
.add_external_author ('University of Illinois at Urbana-Champaign.')
752 self
.add_external_author ('University of Toronto.')
753 self
.add_external_author ('Yoshinori Sato')
755 class GCCCmdLine (CmdLine
):
757 CmdLine
.__init
__ (self
, GCCCopyright
)
759 self
.add_dir ('.', TopLevelFilter())
760 # boehm-gc is imported from upstream.
761 self
.add_dir ('c++tools')
762 self
.add_dir ('config', ConfigFilter())
763 self
.add_dir ('contrib', ContribFilter())
764 self
.add_dir ('fixincludes')
765 self
.add_dir ('gcc', GCCFilter())
766 self
.add_dir (os
.path
.join ('gcc', 'testsuite'), TestsuiteFilter())
767 self
.add_dir ('gnattools')
768 self
.add_dir ('gotools')
769 self
.add_dir ('include')
770 # intl is imported from upstream.
771 self
.add_dir ('libada')
772 self
.add_dir ('libatomic')
773 self
.add_dir ('libbacktrace')
774 self
.add_dir ('libcc1')
775 self
.add_dir ('libcpp', LibCppFilter())
776 self
.add_dir ('libdecnumber')
777 # libffi is imported from upstream.
778 self
.add_dir ('libgcc', LibGCCFilter())
779 self
.add_dir ('libgfortran')
780 # libgo is imported from upstream.
781 self
.add_dir ('libgomp')
782 self
.add_dir ('libiberty')
783 self
.add_dir ('libitm')
784 self
.add_dir ('libobjc')
785 self
.add_dir ('libphobos', LibPhobosFilter())
786 self
.add_dir ('libquadmath')
787 # libsanitizer is imported from upstream.
788 self
.add_dir ('libssp')
789 self
.add_dir ('libstdc++-v3', LibStdCxxFilter())
790 self
.add_dir ('libvtv')
791 self
.add_dir ('lto-plugin')
792 # maintainer-scripts maintainer-scripts
793 # zlib is imported from upstream.
795 self
.default_dirs
= [