import glob import os from django.conf import settings try: from pip._internal.req import parse_requirements except ImportError: from pip.req import parse_requirements def test_python_and_js_licenses(): def index_licenses(path): # Check for GPL (forbidden) and LGPL (need to ship source) # This is not meant to be an exhaustive check. def check_license(license_file): with open(license_file) as f: data = f.read() is_lgpl = 'GNU LESSER GENERAL PUBLIC LICENSE' in data.upper() # The LGPL refers to the GPL in-text # Case-sensitive for GPL to match license text and not PSF license reference is_gpl = 'GNU GENERAL PUBLIC LICENSE' in data and not is_lgpl return (is_gpl, is_lgpl) def find_embedded_source_version(path, name): for entry in os.listdir(path): # Check variations of '-' and '_' in filenames due to python for fname in [name, name.replace('-','_')]: if entry.startswith(fname) and entry.endswith('.tar.gz'): v = entry.split(name + '-')[1].split('.tar.gz')[0] return v return None list = {} for txt_file in glob.glob('%s/*.txt' % path): filename = txt_file.split('/')[-1] name = filename[:-4].lower() (is_gpl, is_lgpl) = check_license(txt_file) list[name] = { 'name': name, 'filename': filename, 'gpl': is_gpl, 'source_required': (is_gpl or is_lgpl), 'source_version': find_embedded_source_version(path, name) } return list def read_api_requirements(path): ret = {} for req_file in ['requirements.txt', 'requirements_ansible.txt', 'requirements_git.txt', 'requirements_ansible_git.txt']: fname = '%s/%s' % (path, req_file) for reqt in parse_requirements(fname, session=''): name = reqt.name version = str(reqt.specifier) if version.startswith('=='): version=version[2:] if reqt.link: (name, version) = reqt.link.filename.split('@',1) if name.endswith('.git'): name = name[:-4] ret[name] = { 'name': name, 'version': version} return ret def remediate_licenses_and_requirements(licenses, requirements): errors = [] items = list(licenses.keys()) items.sort() for item in items: if item not in [r.lower() for r in requirements.keys()] and item != 'awx': errors.append(" license file %s does not correspond to an existing requirement; it should be removed." % (licenses[item]['filename'],)) continue # uWSGI has a linking exception if licenses[item]['gpl'] and item != 'uwsgi': errors.append(" license for %s is GPL. This software cannot be used." % (item,)) if licenses[item]['source_required']: version = requirements[item]['version'] if version != licenses[item]['source_version']: errors.append(" embedded source for %s is %s instead of the required version %s" % (item, licenses[item]['source_version'], version)) elif licenses[item]['source_version']: errors.append(" embedded source version %s for %s is included despite not being needed" % (licenses[item]['source_version'],item)) items = list(requirements.keys()) items.sort() for item in items: if item.lower() not in licenses.keys(): errors.append(" license for requirement %s is missing" %(item,)) return errors base_dir = settings.BASE_DIR api_licenses = index_licenses('%s/../docs/licenses' % base_dir) api_requirements = read_api_requirements('%s/../requirements' % base_dir) errors = [] errors += remediate_licenses_and_requirements(api_licenses, api_requirements) if errors: raise Exception('Included licenses not consistent with requirements:\n%s' % '\n'.join(errors))