mumble/scripts/updateLicenseHeaders.py
Robert Adam 86710dab62 MAINT: Added script for updating license headers
The script iterates through (almost) all files within the Mumble
repository and updates the copyright notice in them. If source files
don't contain such a notice yet, it can optionally add one in.

The script asks git when the file was first added to the git index and
uses that year as the starting year for the copyright notice. The end
year is always set to be the current year at the time the script is run.
2021-03-02 10:14:56 +01:00

164 lines
5.7 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import os
import platform
import subprocess
import datetime
from string import Template
import re
def cmd(args):
shell = platform.system() == 'Windows'
p = subprocess.Popen(args, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
raise Exception('cmd(): {0} failed with status {1}: {2}'.format(args, p.returncode, stderr))
return stdout.decode('utf-8')
licenseHeaderTemplate = Template(
"""${commentChar} Copyright ${copyright} The Mumble Developers. All rights reserved.
${commentChar} Use of this source code is governed by a BSD-style license
${commentChar} that can be found in the LICENSE file at the root of the
${commentChar} Mumble source tree or at <https://www.mumble.info/LICENSE>.""")
blackListFiles = []
class InvalidFileError(Exception):
pass
class LicenseNotFoundError(Exception):
pass
def generateHeaderForFile(filePath):
if filePath in blackListFiles:
raise InvalidFileError("File is blacklisted: \"" + filePath + "\"")
fileName = os.path.basename(filePath)
extension = os.path.splitext(fileName)[-1]
commentChar = None
if extension in [".cpp", ".c", ".h", ".hpp", ".cs", ".ice", ".proto", ".mm", ".vs", ".m", ".ps", ".hlsl", ".fx"]:
commentChar = "//"
elif extension in [".bash", ".sh", ".py", ".yaml", ".yml", ".ps1", ".pl", ".cmake", ".rb", ".init"] or fileName == "CMakeLists.txt":
commentChar = "#"
elif extension in [".bat", ".cmd"]:
commentChar = "::"
if commentChar is None:
raise InvalidFileError("Can't handle extension: \"" + extension + "\"")
# Get file's creation date
creationDateStr = cmd(["git", "log", "--diff-filter=A", "--follow", "--format=%ci", "-1", "--", filePath]).strip()
if not creationDateStr:
raise InvalidFileError("File not in git index: \"" + filePath + "\"")
# We only need the day, month and year
creationDate = datetime.datetime.fromisoformat(creationDateStr.split()[0])
copyright = None
if creationDate.year == datetime.datetime.today().year:
copyright = str(creationDate.year)
else:
copyright = str(creationDate.year) + "-" + str(datetime.datetime.today().year)
return licenseHeaderTemplate.substitute(commentChar=commentChar, copyright=copyright)
def findCopyright(content, pattern):
match = re.search(pattern, content)
if match:
return (match.start(), match.end())
else:
raise LicenseNotFoundError()
def exclude(path, excludes):
for current in excludes:
if path.startswith(current):
return True
return False
def main():
parser = argparse.ArgumentParser(description="Updates (and adds) license headers of the Mumble source files")
parser.add_argument("-a", "--add-new", action="store_true", default=False, help="Also add license headers to files that did not contain one before")
args = parser.parse_args()
scriptDir = os.path.dirname(os.path.realpath(__file__))
rootDir = os.path.realpath(os.path.join(scriptDir, ".."))
excludePaths = ["build", "docs", "3rdparty", "3rdPartylLicenses", "themes", ".git", "man"]
for i in range(len(excludePaths)):
excludePaths[i] = os.path.join(rootDir, excludePaths[i])
for i in range(len(blackListFiles)):
blackListFiles[i] = os.path.join(rootDir, blackListFiles[i])
# Convert the license template into a pattern that matches the license declaration inside the source files
licensePattern = re.compile(licenseHeaderTemplate.substitute(copyright="\d+(-\d+)?", commentChar=".+?").strip().replace("\n", "\\r?\\n").replace(" ",
"\\s*"), re.IGNORECASE)
modifiedFiles = 0
for root, dirs, files in os.walk(rootDir, topdown = True):
for currentFile in files:
fullPath = os.path.join(root, currentFile)
if exclude(fullPath, excludePaths):
continue
try:
generatedCopyright = generateHeaderForFile(fullPath)
fileHandle = open(fullPath, "r")
fileContent = fileHandle.read()
try:
start,end = findCopyright(fileContent, licensePattern)
except LicenseNotFoundError:
# no license yet
if not args.add_new:
print("No license header found for", fullPath)
continue
else:
print("Adding license header to", fullPath)
if not fileContent.startswith("#!"):
# File does not start with shebang -> write license to the top
start = 0
end = 0
generatedCopyright += "\n\n"
else:
# File starts with shebang -> append license after the first line (and add
# an extra blank line)
start = fileContent.find("\n") + 1
assert start > 0
end = start
generatedCopyright = "\n" + generatedCopyright + "\n\n"
modifiedContent = fileContent[ : start] + generatedCopyright + fileContent[end : ]
if modifiedContent != fileContent:
fileHandle.close()
# Open for writing
fileHandle = open(fullPath, "w")
modifiedFiles += 1
fileHandle.write(modifiedContent)
except InvalidFileError:
continue
print("Modified {} files".format(modifiedFiles))
if __name__ == "__main__":
main()