Commit | Line | Data |
---|---|---|
7c2a23b2 AM |
1 | #!/usr/bin/python |
2 | # | |
3 | # Copyright (C) 2013-2017 Free Software Foundation, Inc. | |
4 | # | |
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) | |
8 | # any later version. | |
9 | ||
10 | # This script adjusts the copyright notices at the top of source files | |
11 | # so that they have the form: | |
12 | # | |
13 | # Copyright XXXX-YYYY Free Software Foundation, Inc. | |
14 | # | |
15 | # It doesn't change code that is known to be maintained elsewhere or | |
16 | # that carries a non-FSF copyright. | |
17 | # | |
18 | # Pass --this-year to the script if you want it to add the current year | |
19 | # to all applicable notices. Pass --quilt if you are using quilt and | |
20 | # want files to be added to the quilt before being changed. | |
21 | # | |
22 | # By default the script will update all directories for which the | |
23 | # output has been vetted. You can instead pass the names of individual | |
24 | # directories, including those that haven't been approved. So: | |
25 | # | |
26 | # update-copyright.pl --this-year | |
27 | # | |
28 | # is the command that would be used at the beginning of a year to update | |
29 | # all copyright notices (and possibly at other times to check whether | |
30 | # new files have been added with old years). On the other hand: | |
31 | # | |
32 | # update-copyright.pl --this-year libjava | |
33 | # | |
34 | # would run the script on just libjava/. | |
35 | # | |
36 | # This script was copied from gcc's contrib/ and modified to suit | |
37 | # binutils. In contrast to the gcc script, this one will update | |
38 | # the testsuite and --version output strings too. | |
39 | ||
40 | import os | |
41 | import re | |
42 | import sys | |
43 | import time | |
44 | import subprocess | |
45 | ||
46 | class Errors: | |
47 | def __init__ (self): | |
48 | self.num_errors = 0 | |
49 | ||
50 | def report (self, filename, string): | |
51 | if filename: | |
52 | string = filename + ': ' + string | |
53 | sys.stderr.write (string + '\n') | |
54 | self.num_errors += 1 | |
55 | ||
56 | def ok (self): | |
57 | return self.num_errors == 0 | |
58 | ||
59 | class GenericFilter: | |
60 | def __init__ (self): | |
61 | self.skip_files = set() | |
62 | self.skip_dirs = set() | |
63 | self.skip_extensions = set() | |
64 | self.fossilised_files = set() | |
65 | self.own_files = set() | |
66 | ||
67 | self.skip_files |= set ([ | |
68 | # Skip licence files. | |
69 | 'COPYING', | |
70 | 'COPYING.LIB', | |
71 | 'COPYING3', | |
72 | 'COPYING3.LIB', | |
73 | 'COPYING.LIBGLOSS', | |
74 | 'COPYING.NEWLIB', | |
75 | 'LICENSE', | |
76 | 'fdl.texi', | |
77 | 'gpl_v3.texi', | |
78 | 'fdl-1.3.xml', | |
79 | 'gpl-3.0.xml', | |
80 | ||
81 | # Skip auto- and libtool-related files | |
82 | 'aclocal.m4', | |
83 | 'compile', | |
84 | 'config.guess', | |
85 | 'config.sub', | |
86 | 'depcomp', | |
87 | 'install-sh', | |
88 | 'libtool.m4', | |
89 | 'ltmain.sh', | |
90 | 'ltoptions.m4', | |
91 | 'ltsugar.m4', | |
92 | 'ltversion.m4', | |
93 | 'lt~obsolete.m4', | |
94 | 'missing', | |
95 | 'mkdep', | |
96 | 'mkinstalldirs', | |
97 | 'move-if-change', | |
98 | 'shlibpath.m4', | |
99 | 'symlink-tree', | |
100 | 'ylwrap', | |
101 | ||
102 | # Skip FSF mission statement, etc. | |
103 | 'gnu.texi', | |
104 | 'funding.texi', | |
105 | 'appendix_free.xml', | |
106 | ||
107 | # Skip imported texinfo files. | |
108 | 'texinfo.tex', | |
109 | ]) | |
110 | ||
111 | self.skip_extensions |= set ([ | |
112 | # Maintained by the translation project. | |
113 | '.po', | |
114 | ||
115 | # Automatically-generated. | |
116 | '.pot', | |
117 | ]) | |
118 | ||
119 | self.skip_dirs |= set ([ | |
120 | 'autom4te.cache', | |
121 | ]) | |
122 | ||
123 | ||
124 | def get_line_filter (self, dir, filename): | |
125 | if filename.startswith ('ChangeLog'): | |
126 | # Ignore references to copyright in changelog entries. | |
127 | return re.compile ('\t') | |
128 | ||
129 | return None | |
130 | ||
131 | def skip_file (self, dir, filename): | |
132 | if filename in self.skip_files: | |
133 | return True | |
134 | ||
135 | (base, extension) = os.path.splitext (os.path.join (dir, filename)) | |
136 | if extension in self.skip_extensions: | |
137 | return True | |
138 | ||
139 | if extension == '.in': | |
140 | # Skip .in files produced by automake. | |
141 | if os.path.exists (base + '.am'): | |
142 | return True | |
143 | ||
144 | # Skip files produced by autogen | |
145 | if (os.path.exists (base + '.def') | |
146 | and os.path.exists (base + '.tpl')): | |
147 | return True | |
148 | ||
149 | # Skip configure files produced by autoconf | |
150 | if filename == 'configure': | |
151 | if os.path.exists (base + '.ac'): | |
152 | return True | |
153 | if os.path.exists (base + '.in'): | |
154 | return True | |
155 | ||
156 | return False | |
157 | ||
158 | def skip_dir (self, dir, subdir): | |
159 | return subdir in self.skip_dirs | |
160 | ||
161 | def is_fossilised_file (self, dir, filename): | |
162 | if filename in self.fossilised_files: | |
163 | return True | |
164 | # Only touch current current ChangeLogs. | |
165 | if filename != 'ChangeLog' and filename.find ('ChangeLog') >= 0: | |
166 | return True | |
167 | return False | |
168 | ||
169 | def by_package_author (self, dir, filename): | |
170 | return filename in self.own_files | |
171 | ||
172 | class Copyright: | |
173 | def __init__ (self, errors): | |
174 | self.errors = errors | |
175 | ||
176 | # Characters in a range of years. Include '.' for typos. | |
177 | ranges = '[0-9](?:[-0-9.,\s]|\s+and\s+)*[0-9]' | |
178 | ||
179 | # Non-whitespace characters in a copyright holder's name. | |
180 | name = '[\w.,-]' | |
181 | ||
182 | # Matches one year. | |
183 | self.year_re = re.compile ('[0-9]+') | |
184 | ||
185 | # Matches part of a year or copyright holder. | |
186 | self.continuation_re = re.compile (ranges + '|' + name) | |
187 | ||
188 | # Matches a full copyright notice: | |
189 | self.copyright_re = re.compile ( | |
190 | # 1: 'Copyright (C)', etc. | |
191 | '([Cc]opyright' | |
192 | '|[Cc]opyright\s+\([Cc]\)' | |
193 | '|[Cc]opyright\s+%s' | |
194 | '|[Cc]opyright\s+©' | |
195 | '|[Cc]opyright\s+@copyright{}' | |
196 | '|@set\s+copyright[\w-]+)' | |
197 | ||
198 | # 2: the years. Include the whitespace in the year, so that | |
199 | # we can remove any excess. | |
200 | '(\s*(?:' + ranges + ',?' | |
201 | '|@value\{[^{}]*\})\s*)' | |
202 | ||
203 | # 3: 'by ', if used | |
204 | '(by\s+)?' | |
205 | ||
206 | # 4: the copyright holder. Don't allow multiple consecutive | |
207 | # spaces, so that right-margin gloss doesn't get caught | |
208 | # (e.g. gnat_ugn.texi). | |
209 | '(' + name + '(?:\s?' + name + ')*)?') | |
210 | ||
211 | # A regexp for notices that might have slipped by. Just matching | |
212 | # 'copyright' is too noisy, and 'copyright.*[0-9]' falls foul of | |
213 | # HTML header markers, so check for 'copyright' and two digits. | |
214 | self.other_copyright_re = re.compile ('(^|[^\._])copyright[^=]*[0-9][0-9]', | |
215 | re.IGNORECASE) | |
216 | self.comment_re = re.compile('#+|[*]+|;+|%+|//+|@c |dnl ') | |
217 | self.holders = { '@copying': '@copying' } | |
218 | self.holder_prefixes = set() | |
219 | ||
220 | # True to 'quilt add' files before changing them. | |
221 | self.use_quilt = False | |
222 | ||
223 | # If set, force all notices to include this year. | |
224 | self.max_year = None | |
225 | ||
226 | # Goes after the year(s). Could be ', '. | |
227 | self.separator = ' ' | |
228 | ||
229 | def add_package_author (self, holder, canon_form = None): | |
230 | if not canon_form: | |
231 | canon_form = holder | |
232 | self.holders[holder] = canon_form | |
233 | index = holder.find (' ') | |
234 | while index >= 0: | |
235 | self.holder_prefixes.add (holder[:index]) | |
236 | index = holder.find (' ', index + 1) | |
237 | ||
238 | def add_external_author (self, holder): | |
239 | self.holders[holder] = None | |
240 | ||
241 | class BadYear(): | |
242 | def __init__ (self, year): | |
243 | self.year = year | |
244 | ||
245 | def __str__ (self): | |
246 | return 'unrecognised year: ' + self.year | |
247 | ||
248 | def parse_year (self, string): | |
249 | year = int (string) | |
250 | if len (string) == 2: | |
251 | if year > 70: | |
252 | return year + 1900 | |
253 | elif len (string) == 4: | |
254 | return year | |
255 | raise self.BadYear (string) | |
256 | ||
257 | def year_range (self, years): | |
258 | year_list = [self.parse_year (year) | |
259 | for year in self.year_re.findall (years)] | |
260 | assert len (year_list) > 0 | |
261 | return (min (year_list), max (year_list)) | |
262 | ||
263 | def set_use_quilt (self, use_quilt): | |
264 | self.use_quilt = use_quilt | |
265 | ||
266 | def include_year (self, year): | |
267 | assert not self.max_year | |
268 | self.max_year = year | |
269 | ||
270 | def canonicalise_years (self, dir, filename, filter, years): | |
271 | # Leave texinfo variables alone. | |
272 | if years.startswith ('@value'): | |
273 | return years | |
274 | ||
275 | (min_year, max_year) = self.year_range (years) | |
276 | ||
277 | # Update the upper bound, if enabled. | |
278 | if self.max_year and not filter.is_fossilised_file (dir, filename): | |
279 | max_year = max (max_year, self.max_year) | |
280 | ||
281 | # Use a range. | |
282 | if min_year == max_year: | |
283 | return '%d' % min_year | |
284 | else: | |
285 | return '%d-%d' % (min_year, max_year) | |
286 | ||
287 | def strip_continuation (self, line): | |
288 | line = line.lstrip() | |
289 | match = self.comment_re.match (line) | |
290 | if match: | |
291 | line = line[match.end():].lstrip() | |
292 | return line | |
293 | ||
294 | def is_complete (self, match): | |
295 | holder = match.group (4) | |
296 | return (holder | |
297 | and (holder not in self.holder_prefixes | |
298 | or holder in self.holders)) | |
299 | ||
300 | def update_copyright (self, dir, filename, filter, file, line, match): | |
301 | orig_line = line | |
302 | next_line = None | |
303 | pathname = os.path.join (dir, filename) | |
304 | ||
305 | intro = match.group (1) | |
306 | if intro.startswith ('@set'): | |
307 | # Texinfo year variables should always be on one line | |
308 | after_years = line[match.end (2):].strip() | |
309 | if after_years != '': | |
310 | self.errors.report (pathname, | |
311 | 'trailing characters in @set: ' | |
312 | + after_years) | |
313 | return (False, orig_line, next_line) | |
314 | else: | |
315 | # If it looks like the copyright is incomplete, add the next line. | |
316 | while not self.is_complete (match): | |
317 | try: | |
318 | next_line = file.next() | |
319 | except StopIteration: | |
320 | break | |
321 | ||
322 | # If the next line doesn't look like a proper continuation, | |
323 | # assume that what we've got is complete. | |
324 | continuation = self.strip_continuation (next_line) | |
325 | if not self.continuation_re.match (continuation): | |
326 | break | |
327 | ||
328 | # Merge the lines for matching purposes. | |
329 | orig_line += next_line | |
330 | line = line.rstrip() + ' ' + continuation | |
331 | next_line = None | |
332 | ||
333 | # Rematch with the longer line, at the original position. | |
334 | match = self.copyright_re.match (line, match.start()) | |
335 | assert match | |
336 | ||
337 | holder = match.group (4) | |
338 | ||
339 | # Use the filter to test cases where markup is getting in the way. | |
340 | if filter.by_package_author (dir, filename): | |
341 | assert holder not in self.holders | |
342 | ||
343 | elif not holder: | |
344 | self.errors.report (pathname, 'missing copyright holder') | |
345 | return (False, orig_line, next_line) | |
346 | ||
347 | elif holder not in self.holders: | |
348 | self.errors.report (pathname, | |
349 | 'unrecognised copyright holder: ' + holder) | |
350 | return (False, orig_line, next_line) | |
351 | ||
352 | else: | |
353 | # See whether the copyright is associated with the package | |
354 | # author. | |
355 | canon_form = self.holders[holder] | |
356 | if not canon_form: | |
357 | return (False, orig_line, next_line) | |
358 | ||
359 | # Make sure the author is given in a consistent way. | |
360 | line = (line[:match.start (4)] | |
361 | + canon_form | |
362 | + line[match.end (4):]) | |
363 | ||
364 | # Remove any 'by' | |
365 | line = line[:match.start (3)] + line[match.end (3):] | |
366 | ||
367 | # Update the copyright years. | |
368 | years = match.group (2).strip() | |
369 | if (self.max_year | |
370 | and match.start(0) > 0 and line[match.start(0)-1] == '"' | |
371 | and not filter.is_fossilised_file (dir, filename)): | |
372 | # A printed copyright date consists of the current year | |
373 | canon_form = '%d' % self.max_year | |
374 | else: | |
375 | try: | |
376 | canon_form = self.canonicalise_years (dir, filename, filter, years) | |
377 | except self.BadYear as e: | |
378 | self.errors.report (pathname, str (e)) | |
379 | return (False, orig_line, next_line) | |
380 | ||
381 | line = (line[:match.start (2)] | |
382 | + ' ' + canon_form + self.separator | |
383 | + line[match.end (2):]) | |
384 | ||
385 | # Use the standard (C) form. | |
386 | if intro.endswith ('right'): | |
387 | intro += ' (C)' | |
388 | elif intro.endswith ('(c)'): | |
389 | intro = intro[:-3] + '(C)' | |
390 | line = line[:match.start (1)] + intro + line[match.end (1):] | |
391 | ||
392 | # Strip trailing whitespace | |
393 | line = line.rstrip() + '\n' | |
394 | ||
395 | return (line != orig_line, line, next_line) | |
396 | ||
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. | |
401 | try: | |
402 | os.remove (pathname) | |
403 | except OSError: | |
404 | pass | |
405 | return | |
406 | ||
407 | lines = [] | |
408 | changed = False | |
409 | line_filter = filter.get_line_filter (dir, filename) | |
410 | with open (pathname, 'r') as file: | |
411 | prev = None | |
412 | for line in file: | |
413 | while line: | |
414 | next_line = None | |
415 | # Leave filtered-out lines alone. | |
416 | if not (line_filter and line_filter.match (line)): | |
417 | match = self.copyright_re.search (line) | |
418 | if match: | |
419 | res = self.update_copyright (dir, filename, filter, | |
420 | file, line, match) | |
421 | (this_changed, line, next_line) = res | |
422 | changed = changed or this_changed | |
423 | ||
424 | # Check for copyright lines that might have slipped by. | |
425 | elif self.other_copyright_re.search (line): | |
426 | self.errors.report (pathname, | |
427 | 'unrecognised copyright: %s' | |
428 | % line.strip()) | |
429 | lines.append (line) | |
430 | line = next_line | |
431 | ||
432 | # If something changed, write the new file out. | |
433 | if changed and self.errors.ok(): | |
434 | tmp_pathname = pathname + '.tmp' | |
435 | with open (tmp_pathname, 'w') as file: | |
436 | for line in lines: | |
437 | file.write (line) | |
438 | if self.use_quilt: | |
439 | subprocess.call (['quilt', 'add', pathname]) | |
440 | os.rename (tmp_pathname, pathname) | |
441 | ||
442 | def process_tree (self, tree, filter): | |
443 | for (dir, subdirs, filenames) in os.walk (tree): | |
444 | # Don't recurse through directories that should be skipped. | |
445 | for i in xrange (len (subdirs) - 1, -1, -1): | |
446 | if filter.skip_dir (dir, subdirs[i]): | |
447 | del subdirs[i] | |
448 | ||
449 | # Handle the files in this directory. | |
450 | for filename in filenames: | |
451 | if filter.skip_file (dir, filename): | |
452 | sys.stdout.write ('Skipping %s\n' | |
453 | % os.path.join (dir, filename)) | |
454 | else: | |
455 | self.process_file (dir, filename, filter) | |
456 | ||
457 | class CmdLine: | |
458 | def __init__ (self, copyright = Copyright): | |
459 | self.errors = Errors() | |
460 | self.copyright = copyright (self.errors) | |
461 | self.dirs = [] | |
462 | self.default_dirs = [] | |
463 | self.chosen_dirs = [] | |
464 | self.option_handlers = dict() | |
465 | self.option_help = [] | |
466 | ||
467 | self.add_option ('--help', 'Print this help', self.o_help) | |
468 | self.add_option ('--quilt', '"quilt add" files before changing them', | |
469 | self.o_quilt) | |
470 | self.add_option ('--this-year', 'Add the current year to every notice', | |
471 | self.o_this_year) | |
472 | ||
473 | def add_option (self, name, help, handler): | |
474 | self.option_help.append ((name, help)) | |
475 | self.option_handlers[name] = handler | |
476 | ||
477 | def add_dir (self, dir, filter = GenericFilter()): | |
478 | self.dirs.append ((dir, filter)) | |
479 | ||
480 | def o_help (self, option = None): | |
481 | sys.stdout.write ('Usage: %s [options] dir1 dir2...\n\n' | |
482 | 'Options:\n' % sys.argv[0]) | |
483 | format = '%-15s %s\n' | |
484 | for (what, help) in self.option_help: | |
485 | sys.stdout.write (format % (what, help)) | |
486 | sys.stdout.write ('\nDirectories:\n') | |
487 | ||
488 | format = '%-25s' | |
489 | i = 0 | |
490 | for (dir, filter) in self.dirs: | |
491 | i += 1 | |
492 | if i % 3 == 0 or i == len (self.dirs): | |
493 | sys.stdout.write (dir + '\n') | |
494 | else: | |
495 | sys.stdout.write (format % dir) | |
496 | sys.exit (0) | |
497 | ||
498 | def o_quilt (self, option): | |
499 | self.copyright.set_use_quilt (True) | |
500 | ||
501 | def o_this_year (self, option): | |
502 | self.copyright.include_year (time.localtime().tm_year) | |
503 | ||
504 | def main (self): | |
505 | for arg in sys.argv[1:]: | |
506 | if arg[:1] != '-': | |
507 | self.chosen_dirs.append (arg) | |
508 | elif arg in self.option_handlers: | |
509 | self.option_handlers[arg] (arg) | |
510 | else: | |
511 | self.errors.report (None, 'unrecognised option: ' + arg) | |
512 | if self.errors.ok(): | |
513 | if len (self.chosen_dirs) == 0: | |
514 | self.chosen_dirs = self.default_dirs | |
515 | if len (self.chosen_dirs) == 0: | |
516 | self.o_help() | |
517 | else: | |
518 | for chosen_dir in self.chosen_dirs: | |
519 | canon_dir = os.path.join (chosen_dir, '') | |
520 | count = 0 | |
521 | for (dir, filter) in self.dirs: | |
522 | if (dir + os.sep).startswith (canon_dir): | |
523 | count += 1 | |
524 | self.copyright.process_tree (dir, filter) | |
525 | if count == 0: | |
526 | self.errors.report (None, 'unrecognised directory: ' | |
527 | + chosen_dir) | |
528 | sys.exit (0 if self.errors.ok() else 1) | |
529 | ||
530 | #---------------------------------------------------------------------------- | |
531 | ||
532 | class TopLevelFilter (GenericFilter): | |
533 | def skip_dir (self, dir, subdir): | |
534 | return True | |
535 | ||
536 | class ConfigFilter (GenericFilter): | |
537 | def __init__ (self): | |
538 | GenericFilter.__init__ (self) | |
539 | ||
540 | def skip_file (self, dir, filename): | |
541 | if filename.endswith ('.m4'): | |
542 | pathname = os.path.join (dir, filename) | |
543 | with open (pathname) as file: | |
544 | # Skip files imported from gettext. | |
545 | if file.readline().find ('gettext-') >= 0: | |
546 | return True | |
547 | return GenericFilter.skip_file (self, dir, filename) | |
548 | ||
549 | class LdFilter (GenericFilter): | |
550 | def __init__ (self): | |
551 | GenericFilter.__init__ (self) | |
552 | ||
553 | self.skip_extensions |= set ([ | |
554 | # ld testsuite output match files. | |
555 | '.ro', | |
556 | ]) | |
557 | ||
558 | class BinutilsCopyright (Copyright): | |
559 | def __init__ (self, errors): | |
560 | Copyright.__init__ (self, errors) | |
561 | ||
562 | canon_fsf = 'Free Software Foundation, Inc.' | |
563 | self.add_package_author ('Free Software Foundation', canon_fsf) | |
564 | self.add_package_author ('Free Software Foundation.', canon_fsf) | |
565 | self.add_package_author ('Free Software Foundation Inc.', canon_fsf) | |
566 | self.add_package_author ('Free Software Foundation, Inc', canon_fsf) | |
567 | self.add_package_author ('Free Software Foundation, Inc.', canon_fsf) | |
568 | self.add_package_author ('The Free Software Foundation', canon_fsf) | |
569 | self.add_package_author ('The Free Software Foundation, Inc.', canon_fsf) | |
570 | self.add_package_author ('Software Foundation, Inc.', canon_fsf) | |
571 | ||
572 | self.add_external_author ('Carnegie Mellon University') | |
573 | self.add_external_author ('John D. Polstra.') | |
574 | self.add_external_author ('Linaro Ltd.') | |
575 | self.add_external_author ('MIPS Computer Systems, Inc.') | |
576 | self.add_external_author ('Red Hat Inc.') | |
577 | self.add_external_author ('Regents of the University of California.') | |
578 | self.add_external_author ('The Regents of the University of California.') | |
579 | self.add_external_author ('Third Eye Software, Inc.') | |
580 | self.add_external_author ('Ulrich Drepper') | |
581 | self.add_external_author ('Synopsys Inc.') | |
582 | ||
583 | class BinutilsCmdLine (CmdLine): | |
584 | def __init__ (self): | |
585 | CmdLine.__init__ (self, BinutilsCopyright) | |
586 | ||
587 | self.add_dir ('.', TopLevelFilter()) | |
588 | self.add_dir ('bfd') | |
589 | self.add_dir ('binutils') | |
590 | self.add_dir ('config', ConfigFilter()) | |
591 | self.add_dir ('cpu') | |
592 | self.add_dir ('elfcpp') | |
593 | self.add_dir ('etc') | |
594 | self.add_dir ('gas') | |
595 | self.add_dir ('gdb') | |
596 | self.add_dir ('gold') | |
597 | self.add_dir ('gprof') | |
598 | self.add_dir ('include') | |
599 | self.add_dir ('ld', LdFilter()) | |
600 | self.add_dir ('libdecnumber') | |
601 | self.add_dir ('libiberty') | |
602 | self.add_dir ('opcodes') | |
603 | self.add_dir ('readline') | |
604 | self.add_dir ('sim') | |
605 | ||
606 | self.default_dirs = [ | |
607 | 'bfd', | |
608 | 'binutils', | |
609 | 'elfcpp', | |
610 | 'etc', | |
611 | 'gas', | |
612 | 'gold', | |
613 | 'gprof', | |
614 | 'include', | |
615 | 'ld', | |
616 | 'libiberty', | |
617 | 'opcodes', | |
618 | ] | |
619 | ||
620 | BinutilsCmdLine().main() |