#!/usr/bin/env python3 # # boilerplate.py utility to rewrite the boilerplate with new dates. # # Copyright (C) 2018-2024 Free Software Foundation, Inc. # Contributed by Gaius Mulley . # # This file is part of GNU Modula-2. # # GNU Modula-2 is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3, or (at your option) # any later version. # # GNU Modula-2 is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with GNU Modula-2; see the file COPYING3. If not see # . # import argparse import datetime import os import sys error_count = 0 seen_files = [] output_name = None ISO_COPYRIGHT = 'Copyright ISO/IEC' COPYRIGHT = 'Copyright (C)' GNU_PUBLIC_LICENSE = 'GNU General Public License' GNU_LESSER_GENERAL = 'GNU Lesser General' GCC_RUNTIME_LIB_EXC = 'GCC Runtime Library Exception' VERSION_2_1 = 'version 2.1' VERSION_2 = 'version 2' VERSION_3 = 'version 3' Licenses = {VERSION_2_1: 'v2.1', VERSION_2: 'v2', VERSION_3: 'v3'} CONTRIBUTED_BY = 'ontributed by' def printf(fmt, *args): # printf - keeps C programmers happy :-) print(str(fmt) % args, end=' ') def error(fmt, *args): # error - issue an error message. global error_count print(str(fmt) % args, end=' ') error_count += 1 def halt_on_error(): if error_count > 0: os.sys.exit(1) def basename(f): b = f.split('/') return b[-1] def analyse_comment(text, f): # analyse_comment determine the license from the top comment. start_date, end_date = None, None contribution, summary, lic = None, None, None if text.find(ISO_COPYRIGHT) > 0: lic = 'BSISO' now = datetime.datetime.now() for d in range(1984, now.year+1): if text.find(str(d)) > 0: if start_date is None: start_date = str(d) end_date = str(d) return start_date, end_date, '', '', lic elif text.find(COPYRIGHT) > 0: if text.find(GNU_PUBLIC_LICENSE) > 0: lic = 'GPL' elif text.find(GNU_LESSER_GENERAL) > 0: lic = 'LGPL' for license_ in Licenses.keys(): if text.find(license_) > 0: lic += Licenses[license_] if text.find(GCC_RUNTIME_LIB_EXC) > 0: lic += 'x' now = datetime.datetime.now() for d in range(1984, now.year+1): if text.find(str(d)) > 0: if start_date is None: start_date = str(d) end_date = str(d) if text.find(CONTRIBUTED_BY) > 0: i = text.find(CONTRIBUTED_BY) i += len(CONTRIBUTED_BY) j = text.index('. ', i) contribution = text[i:j] if text.find(basename(f)) > 0: i = text.find(basename(f)) j = text.find('. ', i) if j < 0: error("summary of the file does not finish with a '.'") summary = text[i:] else: summary = text[i:j] return start_date, end_date, contribution, summary, lic def analyse_header_without_terminator(f, start): text = '' for count, l in enumerate(open(f).readlines()): parts = l.split(start) if len(parts) > 1: line = start.join(parts[1:]) line = line.strip() text += ' ' text += line elif (l.rstrip() != '') and (len(parts[0]) > 0): return analyse_comment(text, f), count return [None, None, None, None, None], 0 def analyse_header_with_terminator(f, start, end): inComment = False text = '' for count, line in enumerate(open(f).readlines()): while line != '': line = line.strip() if inComment: text += ' ' pos = line.find(end) if pos >= 0: text += line[:pos] line = line[pos:] inComment = False else: text += line line = '' else: pos = line.find(start) if (pos >= 0) and (len(line) > len(start)): before = line[:pos].strip() if before != '': return analyse_comment(text, f), count line = line[pos + len(start):] inComment = True elif (line != '') and (line == end): line = '' else: return analyse_comment(text, f), count return [None, None, None, None, None], 0 def analyse_header(f, start, end): # analyse_header - if end is None: return analyse_header_without_terminator(f, start) else: return analyse_header_with_terminator(f, start, end) def add_stop(sentence): # add_stop - add a full stop to a sentance. if sentence is None: return None sentence = sentence.rstrip() if (len(sentence) > 0) and (sentence[-1] != '.'): return sentence + '.' return sentence GPLv3 = """ %s Copyright (C) %s Free Software Foundation, Inc. Contributed by %s This file is part of GNU Modula-2. GNU Modula-2 is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Modula-2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Modula-2; see the file COPYING3. If not see . """ GPLv3x = """ %s Copyright (C) %s Free Software Foundation, Inc. Contributed by %s This file is part of GNU Modula-2. GNU Modula-2 is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Modula-2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. Under Section 7 of GPL version 3, you are granted additional permissions described in the GCC Runtime Library Exception, version 3.1, as published by the Free Software Foundation. You should have received a copy of the GNU General Public License and a copy of the GCC Runtime Library Exception along with this program; see the files COPYING3 and COPYING.RUNTIME respectively. If not, see . """ LGPLv3 = """ %s Copyright (C) %s Free Software Foundation, Inc. Contributed by %s This file is part of GNU Modula-2. GNU Modula-2 is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. GNU Modula-2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with GNU Modula-2. If not, see . """ BSISO = """ Library module defined by the International Standard Information technology - programming languages BS ISO/IEC 10514-1:1996E Part 1: Modula-2, Base Language. Copyright ISO/IEC (International Organization for Standardization and International Electrotechnical Commission) %s. It may be freely copied for the purpose of implementation (see page 707 of the Information technology - Programming languages Part 1: Modula-2, Base Language. BS ISO/IEC 10514-1:1996). """ templates = {} templates['GPLv3'] = GPLv3 templates['GPLv3x'] = GPLv3x templates['LGPLv3'] = LGPLv3 templates['LGPLv2.1'] = LGPLv3 templates['BSISO'] = BSISO def write_template(fo, magic, start, end, dates, contribution, summary, lic): if lic in templates: if lic == 'BSISO': # non gpl but freely distributed for the implementation of a # compiler text = templates[lic] % (dates) text = text.rstrip() else: summary = summary.lstrip() contribution = contribution.lstrip() summary = add_stop(summary) contribution = add_stop(contribution) if magic is not None: fo.write(magic) fo.write('\n') text = templates[lic] % (summary, dates, contribution) text = text.rstrip() if end is None: text = text.split('\n') for line in text: fo.write(start) fo.write(' ') fo.write(line) fo.write('\n') else: text = text.lstrip() fo.write(start) fo.write(' ') fo.write(text) fo.write(' ') fo.write(end) fo.write('\n') # add a blank comment line for a script for eye candy. if start == '#' and end is None: fo.write(start) fo.write('\n') else: error('no template found for: %s\n', lic) os.sys.exit(1) return fo def write_boiler_plate(fo, magic, start, end, start_date, end_date, contribution, summary, gpl): if start_date == end_date: dates = start_date else: dates = '%s-%s' % (start_date, end_date) return write_template(fo, magic, start, end, dates, contribution, summary, gpl) def rewrite_file(f, magic, start, end, start_date, end_date, contribution, summary, gpl, lines): text = ''.join(open(f).readlines()[lines:]) if output_name == '-': fo = sys.stdout else: fo = open(f, 'w') fo = write_boiler_plate(fo, magic, start, end, start_date, end_date, contribution, summary, gpl) fo.write(text) fo.flush() if output_name != '-': fo.close() def handle_header(f, magic, start, end): # handle_header keep reading lines of file, f, looking for start, end # sequences and comments inside. The comments are checked for: # date, contribution, summary global error_count error_count = 0 [start_date, end_date, contribution, summary, lic], lines = analyse_header(f, start, end) if lic is None: error('%s:1:no GPL found at the top of the file\n', f) else: if args.verbose: printf('copyright: %s\n', lic) if (start_date is not None) and (end_date is not None): if start_date == end_date: printf('dates = %s\n', start_date) else: printf('dates = %s-%s\n', start_date, end_date) if summary is not None: printf('summary: %s\n', summary) if contribution is not None: printf('contribution: %s\n', contribution) if start_date is None: error('%s:1:no date found in the GPL at the top of the file\n', f) if args.contribution is None: if contribution == '': error('%s:1:no contribution found in the ' + 'GPL at the top of the file\n', f) else: contribution = args.contribution if summary is None: if args.summary == '': error('%s:1:no single line summary found in the ' + 'GPL at the top of the file\n', f) else: summary = args.summary if error_count == 0: now = datetime.datetime.now() if args.no: print(f, 'suppressing change as requested: %s-%s %s' % (start_date, end_date, lic)) else: if lic == 'BSISO': # don't change the BS ISO license! pass elif args.extensions: lic = 'GPLv3x' elif args.gpl3: lic = 'GPLv3' rewrite_file(f, magic, start, end, start_date, str(now.year), contribution, summary, lic, lines) else: printf('too many errors, no modifications will occur\n') def bash_tidy(f): # bash_tidy tidy up dates using '#' comment handle_header(f, '#!/bin/bash', '#', None) def python_tidy(f): # python_tidy tidy up dates using '#' comment handle_header(f, '#!/usr/bin/env python3', '#', None) def bnf_tidy(f): # bnf_tidy tidy up dates using '--' comment handle_header(f, None, '--', None) def c_tidy(f): # c_tidy tidy up dates using '/* */' comments handle_header(f, None, '/*', '*/') def m2_tidy(f): # m2_tidy tidy up dates using '(* *)' comments handle_header(f, None, '(*', '*)') def in_tidy(f): # in_tidy tidy up dates using '#' as a comment and check # the first line for magic number. first = open(f).readlines()[0] if (len(first) > 0) and (first[:2] == '#!'): # magic number found, use this handle_header(f, first, '#', None) else: handle_header(f, None, '#', None) def do_visit(args, dirname, names): # do_visit helper function to call func on every extension file. global output_name func, extension = args for f in names: if len(f) > len(extension) and f[-len(extension):] == extension: output_name = f func(os.path.join(dirname, f)) def visit_dir(startDir, ext, func): # visit_dir call func for each file in startDir which has ext. global output_name, seen_files for dirName, subdirList, fileList in os.walk(startDir): for fname in fileList: if (len(fname) > len(ext)) and (fname[-len(ext):] == ext): fullpath = os.path.join(dirName, fname) output_name = fullpath if not (fullpath in seen_files): seen_files += [fullpath] func(fullpath) # Remove the first entry in the list of sub-directories # if there are any sub-directories present if len(subdirList) > 0: del subdirList[0] def find_files(): # find_files for each file extension call the appropriate tidy routine. visit_dir(args.recursive, '.h.in', c_tidy) visit_dir(args.recursive, '.in', in_tidy) visit_dir(args.recursive, '.sh', in_tidy) visit_dir(args.recursive, '.py', python_tidy) visit_dir(args.recursive, '.c', c_tidy) visit_dir(args.recursive, '.h', c_tidy) visit_dir(args.recursive, '.cc', c_tidy) visit_dir(args.recursive, '.def', m2_tidy) visit_dir(args.recursive, '.mod', m2_tidy) visit_dir(args.recursive, '.bnf', bnf_tidy) def handle_arguments(): # handle_arguments create and return the args object. parser = argparse.ArgumentParser() parser.add_argument('-c', '--contribution', help='set the contribution string ' + 'at the top of the file.', default='', action='store') parser.add_argument('-d', '--debug', help='turn on internal debugging.', default=False, action='store_true') parser.add_argument('-f', '--force', help='force a check to insist that the ' + 'contribution, summary and GPL exist.', default=False, action='store_true') parser.add_argument('-g', '--gplv3', help='change to GPLv3', default=False, action='store_true') parser.add_argument('-o', '--outputfile', help='set the output file', default='-', action='store') parser.add_argument('-r', '--recursive', help='recusively scan directory for known file ' + 'extensions (.def, .mod, .c, .h, .py, .in, .sh).', default='.', action='store') parser.add_argument('-s', '--summary', help='set the summary line for the file.', default=None, action='store') parser.add_argument('-u', '--update', help='update all dates.', default=False, action='store_true') parser.add_argument('-v', '--verbose', help='display copyright, ' + 'date and contribution messages', action='store_true') parser.add_argument('-x', '--extensions', help='change to GPLv3 with GCC runtime extensions.', default=False, action='store_true') parser.add_argument('-N', '--no', help='do not modify any file.', action='store_true') args = parser.parse_args() return args def has_ext(name, ext): # has_ext return True if, name, ends with, ext. if len(name) > len(ext): return name[-len(ext):] == ext return False def single_file(name): # single_file scan the single file for a GPL boilerplate which # has a GPL, contribution field and a summary heading. if has_ext(name, '.def') or has_ext(name, '.mod'): m2_tidy(name) elif has_ext(name, '.h') or has_ext(name, '.c') or has_ext(name, '.cc'): c_tidy(name) elif has_ext(name, '.in'): in_tidy(name) elif has_ext(name, '.sh'): in_tidy(name) # uses magic number for actual sh/bash elif has_ext(name, '.py'): python_tidy(name) def main(): # main - handle_arguments and then find source files. global args, output_name args = handle_arguments() output_name = args.outputfile if args.recursive: find_files() elif args.inputfile is None: print('an input file must be specified on the command line') else: single_file(args.inputfile) halt_on_error() main()