blob: 2e8c0b108a50d2889953c85485cd061c2bbc4358 [file] [log] [blame]
#!/usr/bin/env python3
#
# boilerplate.py utility to rewrite the boilerplate with new dates.
#
# Copyright (C) 2018-2025 Free Software Foundation, Inc.
# Contributed by Gaius Mulley <gaius@glam.ac.uk>.
#
# 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
# <http://www.gnu.org/licenses/>.
#
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
<http://www.gnu.org/licenses/>.
"""
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
<http://www.gnu.org/licenses/>.
"""
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 <https://www.gnu.org/licenses/>.
"""
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()