1  #!/usr/bin/env python3
       2  #
       3  # boilerplate.py utility to rewrite the boilerplate with new dates.
       4  #
       5  # Copyright (C) 2018-2023 Free Software Foundation, Inc.
       6  # Contributed by Gaius Mulley <gaius@glam.ac.uk>.
       7  #
       8  # This file is part of GNU Modula-2.
       9  #
      10  # GNU Modula-2 is free software; you can redistribute it and/or modify
      11  # it under the terms of the GNU General Public License as published by
      12  # the Free Software Foundation; either version 3, or (at your option)
      13  # any later version.
      14  #
      15  # GNU Modula-2 is distributed in the hope that it will be useful, but
      16  # WITHOUT ANY WARRANTY; without even the implied warranty of
      17  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
      18  # General Public License for more details.
      19  #
      20  # You should have received a copy of the GNU General Public License
      21  # along with GNU Modula-2; see the file COPYING3.  If not see
      22  # <http://www.gnu.org/licenses/>.
      23  #
      24  
      25  import argparse
      26  import datetime
      27  import os
      28  import sys
      29  
      30  
      31  error_count = 0
      32  seen_files = []
      33  output_name = None
      34  
      35  ISO_COPYRIGHT = 'Copyright ISO/IEC'
      36  COPYRIGHT = 'Copyright (C)'
      37  GNU_PUBLIC_LICENSE = 'GNU General Public License'
      38  GNU_LESSER_GENERAL = 'GNU Lesser General'
      39  GCC_RUNTIME_LIB_EXC = 'GCC Runtime Library Exception'
      40  VERSION_2_1 = 'version 2.1'
      41  VERSION_2 = 'version 2'
      42  VERSION_3 = 'version 3'
      43  Licenses = {VERSION_2_1: 'v2.1', VERSION_2: 'v2', VERSION_3: 'v3'}
      44  CONTRIBUTED_BY = 'ontributed by'
      45  
      46  
      47  def printf(fmt, *args):
      48      # printf - keeps C programmers happy :-)
      49      print(str(fmt) % args, end=' ')
      50  
      51  
      52  def error(fmt, *args):
      53      # error - issue an error message.
      54      global error_count
      55  
      56      print(str(fmt) % args, end=' ')
      57      error_count += 1
      58  
      59  
      60  def halt_on_error():
      61      if error_count > 0:
      62          os.sys.exit(1)
      63  
      64  
      65  def basename(f):
      66      b = f.split('/')
      67      return b[-1]
      68  
      69  
      70  def analyse_comment(text, f):
      71      # analyse_comment determine the license from the top comment.
      72      start_date, end_date = None, None
      73      contribution, summary, lic = None, None, None
      74      if text.find(ISO_COPYRIGHT) > 0:
      75          lic = 'BSISO'
      76          now = datetime.datetime.now()
      77          for d in range(1984, now.year+1):
      78              if text.find(str(d)) > 0:
      79                  if start_date is None:
      80                      start_date = str(d)
      81                  end_date = str(d)
      82          return start_date, end_date, '', '', lic
      83      elif text.find(COPYRIGHT) > 0:
      84          if text.find(GNU_PUBLIC_LICENSE) > 0:
      85              lic = 'GPL'
      86          elif text.find(GNU_LESSER_GENERAL) > 0:
      87              lic = 'LGPL'
      88          for license_ in Licenses.keys():
      89              if text.find(license_) > 0:
      90                  lic += Licenses[license_]
      91          if text.find(GCC_RUNTIME_LIB_EXC) > 0:
      92              lic += 'x'
      93          now = datetime.datetime.now()
      94          for d in range(1984, now.year+1):
      95              if text.find(str(d)) > 0:
      96                  if start_date is None:
      97                      start_date = str(d)
      98                  end_date = str(d)
      99          if text.find(CONTRIBUTED_BY) > 0:
     100              i = text.find(CONTRIBUTED_BY)
     101              i += len(CONTRIBUTED_BY)
     102              j = text.index('. ', i)
     103              contribution = text[i:j]
     104      if text.find(basename(f)) > 0:
     105          i = text.find(basename(f))
     106          j = text.find('. ', i)
     107          if j < 0:
     108              error("summary of the file does not finish with a '.'")
     109              summary = text[i:]
     110          else:
     111              summary = text[i:j]
     112      return start_date, end_date, contribution, summary, lic
     113  
     114  
     115  def analyse_header_without_terminator(f, start):
     116      text = ''
     117      for count, l in enumerate(open(f).readlines()):
     118          parts = l.split(start)
     119          if len(parts) > 1:
     120              line = start.join(parts[1:])
     121              line = line.strip()
     122              text += ' '
     123              text += line
     124          elif (l.rstrip() != '') and (len(parts[0]) > 0):
     125              return analyse_comment(text, f), count
     126      return [None, None, None, None, None], 0
     127  
     128  
     129  def analyse_header_with_terminator(f, start, end):
     130      inComment = False
     131      text = ''
     132      for count, line in enumerate(open(f).readlines()):
     133          while line != '':
     134              line = line.strip()
     135              if inComment:
     136                  text += ' '
     137                  pos = line.find(end)
     138                  if pos >= 0:
     139                      text += line[:pos]
     140                      line = line[pos:]
     141                      inComment = False
     142                  else:
     143                      text += line
     144                      line = ''
     145              else:
     146                  pos = line.find(start)
     147                  if (pos >= 0) and (len(line) > len(start)):
     148                      before = line[:pos].strip()
     149                      if before != '':
     150                          return analyse_comment(text, f), count
     151                      line = line[pos + len(start):]
     152                      inComment = True
     153                  elif (line != '') and (line == end):
     154                      line = ''
     155                  else:
     156                      return analyse_comment(text, f), count
     157      return [None, None, None, None, None], 0
     158  
     159  
     160  def analyse_header(f, start, end):
     161      # analyse_header -
     162      if end is None:
     163          return analyse_header_without_terminator(f, start)
     164      else:
     165          return analyse_header_with_terminator(f, start, end)
     166  
     167  
     168  def add_stop(sentence):
     169      # add_stop - add a full stop to a sentance.
     170      if sentence is None:
     171          return None
     172      sentence = sentence.rstrip()
     173      if (len(sentence) > 0) and (sentence[-1] != '.'):
     174          return sentence + '.'
     175      return sentence
     176  
     177  
     178  GPLv3 = """
     179  %s
     180  
     181  Copyright (C) %s Free Software Foundation, Inc.
     182  Contributed by %s
     183  
     184  This file is part of GNU Modula-2.
     185  
     186  GNU Modula-2 is free software; you can redistribute it and/or modify
     187  it under the terms of the GNU General Public License as published by
     188  the Free Software Foundation; either version 3, or (at your option)
     189  any later version.
     190  
     191  GNU Modula-2 is distributed in the hope that it will be useful, but
     192  WITHOUT ANY WARRANTY; without even the implied warranty of
     193  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     194  General Public License for more details.
     195  
     196  You should have received a copy of the GNU General Public License
     197  along with GNU Modula-2; see the file COPYING3.  If not see
     198  <http://www.gnu.org/licenses/>.
     199  """
     200  
     201  GPLv3x = """
     202  %s
     203  
     204  Copyright (C) %s Free Software Foundation, Inc.
     205  Contributed by %s
     206  
     207  This file is part of GNU Modula-2.
     208  
     209  GNU Modula-2 is free software; you can redistribute it and/or modify
     210  it under the terms of the GNU General Public License as published by
     211  the Free Software Foundation; either version 3, or (at your option)
     212  any later version.
     213  
     214  GNU Modula-2 is distributed in the hope that it will be useful, but
     215  WITHOUT ANY WARRANTY; without even the implied warranty of
     216  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     217  General Public License for more details.
     218  
     219  Under Section 7 of GPL version 3, you are granted additional
     220  permissions described in the GCC Runtime Library Exception, version
     221  3.1, as published by the Free Software Foundation.
     222  
     223  You should have received a copy of the GNU General Public License and
     224  a copy of the GCC Runtime Library Exception along with this program;
     225  see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see
     226  <http://www.gnu.org/licenses/>.
     227  """
     228  
     229  LGPLv3 = """
     230  %s
     231  
     232  Copyright (C) %s Free Software Foundation, Inc.
     233  Contributed by %s
     234  
     235  This file is part of GNU Modula-2.
     236  
     237  GNU Modula-2 is free software: you can redistribute it and/or modify
     238  it under the terms of the GNU Lesser General Public License as
     239  published by the Free Software Foundation, either version 3 of the
     240  License, or (at your option) any later version.
     241  
     242  GNU Modula-2 is distributed in the hope that it will be useful, but
     243  WITHOUT ANY WARRANTY; without even the implied warranty of
     244  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     245  Lesser General Public License for more details.
     246  
     247  You should have received a copy of the GNU Lesser General Public License
     248  along with GNU Modula-2.  If not, see <https://www.gnu.org/licenses/>.
     249  """
     250  
     251  BSISO = """
     252  Library module defined by the International Standard
     253     Information technology - programming languages
     254     BS ISO/IEC 10514-1:1996E Part 1: Modula-2, Base Language.
     255  
     256     Copyright ISO/IEC (International Organization for Standardization
     257     and International Electrotechnical Commission) %s.
     258  
     259     It may be freely copied for the purpose of implementation (see page
     260     707 of the Information technology - Programming languages Part 1:
     261     Modula-2, Base Language.  BS ISO/IEC 10514-1:1996).
     262  """
     263  
     264  templates = {}
     265  templates['GPLv3'] = GPLv3
     266  templates['GPLv3x'] = GPLv3x
     267  templates['LGPLv3'] = LGPLv3
     268  templates['LGPLv2.1'] = LGPLv3
     269  templates['BSISO'] = BSISO
     270  
     271  
     272  def write_template(fo, magic, start, end, dates, contribution, summary, lic):
     273      if lic in templates:
     274          if lic == 'BSISO':
     275              # non gpl but freely distributed for the implementation of a
     276              # compiler
     277              text = templates[lic] % (dates)
     278              text = text.rstrip()
     279          else:
     280              summary = summary.lstrip()
     281              contribution = contribution.lstrip()
     282              summary = add_stop(summary)
     283              contribution = add_stop(contribution)
     284              if magic is not None:
     285                  fo.write(magic)
     286                  fo.write('\n')
     287              text = templates[lic] % (summary, dates, contribution)
     288              text = text.rstrip()
     289          if end is None:
     290              text = text.split('\n')
     291              for line in text:
     292                  fo.write(start)
     293                  fo.write(' ')
     294                  fo.write(line)
     295                  fo.write('\n')
     296          else:
     297              text = text.lstrip()
     298              fo.write(start)
     299              fo.write(' ')
     300              fo.write(text)
     301              fo.write('  ')
     302              fo.write(end)
     303              fo.write('\n')
     304          # add a blank comment line for a script for eye candy.
     305          if start == '#' and end is None:
     306              fo.write(start)
     307              fo.write('\n')
     308      else:
     309          error('no template found for: %s\n', lic)
     310          os.sys.exit(1)
     311      return fo
     312  
     313  
     314  def write_boiler_plate(fo, magic, start, end,
     315                         start_date, end_date, contribution, summary, gpl):
     316      if start_date == end_date:
     317          dates = start_date
     318      else:
     319          dates = '%s-%s' % (start_date, end_date)
     320      return write_template(fo, magic, start, end,
     321                            dates, contribution, summary, gpl)
     322  
     323  
     324  def rewrite_file(f, magic, start, end, start_date, end_date,
     325                   contribution, summary, gpl, lines):
     326      text = ''.join(open(f).readlines()[lines:])
     327      if output_name == '-':
     328          fo = sys.stdout
     329      else:
     330          fo = open(f, 'w')
     331      fo = write_boiler_plate(fo, magic, start, end,
     332                              start_date, end_date, contribution, summary, gpl)
     333      fo.write(text)
     334      fo.flush()
     335      if output_name != '-':
     336          fo.close()
     337  
     338  
     339  def handle_header(f, magic, start, end):
     340      # handle_header keep reading lines of file, f, looking for start, end
     341      # sequences and comments inside.  The comments are checked for:
     342      # date, contribution, summary
     343      global error_count
     344  
     345      error_count = 0
     346      [start_date, end_date,
     347       contribution, summary, lic], lines = analyse_header(f, start, end)
     348      if lic is None:
     349          error('%s:1:no GPL found at the top of the file\n', f)
     350      else:
     351          if args.verbose:
     352              printf('copyright: %s\n', lic)
     353              if (start_date is not None) and (end_date is not None):
     354                  if start_date == end_date:
     355                      printf('dates = %s\n', start_date)
     356                  else:
     357                      printf('dates = %s-%s\n', start_date, end_date)
     358              if summary is not None:
     359                  printf('summary: %s\n', summary)
     360              if contribution is not None:
     361                  printf('contribution: %s\n', contribution)
     362          if start_date is None:
     363              error('%s:1:no date found in the GPL at the top of the file\n', f)
     364          if args.contribution is None:
     365              if contribution == '':
     366                  error('%s:1:no contribution found in the ' +
     367                        'GPL at the top of the file\n', f)
     368              else:
     369                  contribution = args.contribution
     370          if summary is None:
     371              if args.summary == '':
     372                  error('%s:1:no single line summary found in the ' +
     373                        'GPL at the top of the file\n', f)
     374              else:
     375                  summary = args.summary
     376      if error_count == 0:
     377          now = datetime.datetime.now()
     378          if args.no:
     379              print(f, 'suppressing change as requested: %s-%s %s'
     380                    % (start_date, end_date, lic))
     381          else:
     382              if lic == 'BSISO':
     383                  # don't change the BS ISO license!
     384                  pass
     385              elif args.extensions:
     386                  lic = 'GPLv3x'
     387              elif args.gpl3:
     388                  lic = 'GPLv3'
     389              rewrite_file(f, magic, start, end, start_date,
     390                           str(now.year), contribution, summary, lic, lines)
     391      else:
     392          printf('too many errors, no modifications will occur\n')
     393  
     394  
     395  def bash_tidy(f):
     396      # bash_tidy tidy up dates using '#' comment
     397      handle_header(f, '#!/bin/bash', '#', None)
     398  
     399  
     400  def python_tidy(f):
     401      # python_tidy tidy up dates using '#' comment
     402      handle_header(f, '#!/usr/bin/env python3', '#', None)
     403  
     404  
     405  def bnf_tidy(f):
     406      # bnf_tidy tidy up dates using '--' comment
     407      handle_header(f, None, '--', None)
     408  
     409  
     410  def c_tidy(f):
     411      # c_tidy tidy up dates using '/* */' comments
     412      handle_header(f, None, '/*', '*/')
     413  
     414  
     415  def m2_tidy(f):
     416      # m2_tidy tidy up dates using '(* *)' comments
     417      handle_header(f, None, '(*', '*)')
     418  
     419  
     420  def in_tidy(f):
     421      # in_tidy tidy up dates using '#' as a comment and check
     422      # the first line for magic number.
     423      first = open(f).readlines()[0]
     424      if (len(first) > 0) and (first[:2] == '#!'):
     425          # magic number found, use this
     426          handle_header(f, first, '#', None)
     427      else:
     428          handle_header(f, None, '#', None)
     429  
     430  
     431  def do_visit(args, dirname, names):
     432      # do_visit helper function to call func on every extension file.
     433      global output_name
     434      func, extension = args
     435      for f in names:
     436          if len(f) > len(extension) and f[-len(extension):] == extension:
     437              output_name = f
     438              func(os.path.join(dirname, f))
     439  
     440  
     441  def visit_dir(startDir, ext, func):
     442      # visit_dir call func for each file in startDir which has ext.
     443      global output_name, seen_files
     444      for dirName, subdirList, fileList in os.walk(startDir):
     445          for fname in fileList:
     446              if (len(fname) > len(ext)) and (fname[-len(ext):] == ext):
     447                  fullpath = os.path.join(dirName, fname)
     448                  output_name = fullpath
     449                  if not (fullpath in seen_files):
     450                      seen_files += [fullpath]
     451                      func(fullpath)
     452              # Remove the first entry in the list of sub-directories
     453              # if there are any sub-directories present
     454          if len(subdirList) > 0:
     455              del subdirList[0]
     456  
     457  
     458  def find_files():
     459      # find_files for each file extension call the appropriate tidy routine.
     460      visit_dir(args.recursive, '.h.in', c_tidy)
     461      visit_dir(args.recursive, '.in', in_tidy)
     462      visit_dir(args.recursive, '.sh', in_tidy)
     463      visit_dir(args.recursive, '.py', python_tidy)
     464      visit_dir(args.recursive, '.c', c_tidy)
     465      visit_dir(args.recursive, '.h', c_tidy)
     466      visit_dir(args.recursive, '.cc', c_tidy)
     467      visit_dir(args.recursive, '.def', m2_tidy)
     468      visit_dir(args.recursive, '.mod', m2_tidy)
     469      visit_dir(args.recursive, '.bnf', bnf_tidy)
     470  
     471  
     472  def handle_arguments():
     473      # handle_arguments create and return the args object.
     474      parser = argparse.ArgumentParser()
     475      parser.add_argument('-c', '--contribution',
     476                          help='set the contribution string ' +
     477                          'at the top of the file.',
     478                          default='', action='store')
     479      parser.add_argument('-d', '--debug', help='turn on internal debugging.',
     480                          default=False, action='store_true')
     481      parser.add_argument('-f', '--force',
     482                          help='force a check to insist that the ' +
     483                          'contribution, summary and GPL exist.',
     484                          default=False, action='store_true')
     485      parser.add_argument('-g', '--gplv3', help='change to GPLv3',
     486                          default=False, action='store_true')
     487      parser.add_argument('-o', '--outputfile', help='set the output file',
     488                          default='-', action='store')
     489      parser.add_argument('-r', '--recursive',
     490                          help='recusively scan directory for known file ' +
     491                          'extensions (.def, .mod, .c, .h, .py, .in, .sh).',
     492                          default='.', action='store')
     493      parser.add_argument('-s', '--summary',
     494                          help='set the summary line for the file.',
     495                          default=None, action='store')
     496      parser.add_argument('-u', '--update', help='update all dates.',
     497                          default=False, action='store_true')
     498      parser.add_argument('-v', '--verbose',
     499                          help='display copyright, ' +
     500                          'date and contribution messages',
     501                          action='store_true')
     502      parser.add_argument('-x', '--extensions',
     503                          help='change to GPLv3 with GCC runtime extensions.',
     504                          default=False, action='store_true')
     505      parser.add_argument('-N', '--no',
     506                          help='do not modify any file.',
     507                          action='store_true')
     508      args = parser.parse_args()
     509      return args
     510  
     511  
     512  def has_ext(name, ext):
     513      # has_ext return True if, name, ends with, ext.
     514      if len(name) > len(ext):
     515          return name[-len(ext):] == ext
     516      return False
     517  
     518  
     519  def single_file(name):
     520      # single_file scan the single file for a GPL boilerplate which
     521      # has a GPL, contribution field and a summary heading.
     522      if has_ext(name, '.def') or has_ext(name, '.mod'):
     523          m2_tidy(name)
     524      elif has_ext(name, '.h') or has_ext(name, '.c') or has_ext(name, '.cc'):
     525          c_tidy(name)
     526      elif has_ext(name, '.in'):
     527          in_tidy(name)
     528      elif has_ext(name, '.sh'):
     529          in_tidy(name)  # uses magic number for actual sh/bash
     530      elif has_ext(name, '.py'):
     531          python_tidy(name)
     532  
     533  
     534  def main():
     535      # main - handle_arguments and then find source files.
     536      global args, output_name
     537      args = handle_arguments()
     538      output_name = args.outputfile
     539      if args.recursive:
     540          find_files()
     541      elif args.inputfile is None:
     542          print('an input file must be specified on the command line')
     543      else:
     544          single_file(args.inputfile)
     545      halt_on_error()
     546  
     547  
     548  main()