#!/usr/bin/env python # -*- coding: utf-8 -*- # # This script expects cargo-attribution # to be installed. import argparse import datetime import json import os import re import shutil import subprocess import tomllib def process_licenses(source_dir, destination_dir): # CC0, Unlicense and Zlib don't need licenses to be included. # MPL doesn't need the license included with binary distributions, and # allows distribution under a compatible license. # cargo attribution's GPL-3.0 download doesn't contain the license text. # The ISC license is skipped because its dependency gets filtered out, same # with the exceptions. skip_entries = [ 'exceptions', 'CC0-1.0', 'GPL-3.0', 'ISC', 'MPL-2.0', 'Unlicense', 'Zlib' ] for entry in os.scandir(source_dir): if entry.name not in skip_entries: destination_path = os.path.join(destination_dir, entry.name) if entry.is_dir(): shutil.copytree(entry.path, destination_path, dirs_exist_ok=True) else: shutil.copy2(entry.path, destination_path) def recurse_dependencies(packages, package, dependency_names): for dependency in package['dependencies']: name = dependency['name'] if name in dependency_names: continue dependency_names.add(name) for package in packages: if package['name'] == name: recurse_dependencies(packages, package, dependency_names) def get_target_dependency_names(target_package_name): result = subprocess.run( [ 'cargo', 'metadata', '--manifest-path', cargo_toml_path, '--filter-platform', 'x86_64-pc-windows-msvc', '--format-version', '1' ], capture_output=True, text=True, check=True ) json_output = json.loads(result.stdout[:-1]) packages = json_output['packages'] target_dependency_names = set() for package in packages: if package['name'] == 'libloot-cpp': recurse_dependencies(packages, package, target_dependency_names) return target_dependency_names if __name__ == "__main__": target_package_name = 'libloot-cpp' cargo_toml_path = os.path.join('..', 'cpp', 'Cargo.toml') attribution_dir = os.path.join('build', 'attribution') output_dir = os.path.join('api', 'copyright') subprocess.run( [ 'cargo', 'attribution', '--manifest-path', cargo_toml_path, '--output-dir', attribution_dir, '--filter-platform', 'x86_64-pc-windows-msvc', '--only-normal-dependencies' ], check=True ) process_licenses(os.path.join(attribution_dir, 'licenses'), os.path.join(output_dir, 'licenses')) target_dependency_names = get_target_dependency_names(target_package_name) dependencies_toml_path = os.path.join(attribution_dir, 'dependencies.toml') with open(dependencies_toml_path, 'rb') as f: data = tomllib.load(f) dependencies = data['dependencies'] # esplugin's and libloadorder's notices are nonsense, but they're my # libraries so I give myself permission to skip providing the notices. skip_dependencies = [ 'libloot', 'libloot-ffi-errors', 'libloot-cpp', 'libloot-nodejs', 'libloot-python', 'array-parameterized-test', 'esplugin', 'libloadorder' ] notices_rst = f'.. This file was generated by scripts/licenses.py at {datetime.datetime.now().isoformat()}.\n\n' heading = 'Dependency Copyright Notices' notices_rst += heading notices_rst += '\n' notices_rst += '=' * len(heading) notices_rst += '\n\n' for dependency in dependencies: if dependency['name'] not in target_dependency_names: continue if dependency['name'] in skip_dependencies: continue if 'license' not in dependency: print(f'Found dependency with no license: {dependency['name']}') exit(1) if 'ISC' in dependency['license']: print(f'Found dependency that may require the ISC license: {dependency['name']}') exit(1) if 'exception' in dependency['license']: print(f'Found dependency that may require an exception: {dependency['name']}') exit(1) if 'CC0-1.0' in dependency['license']: print(f'Skipping {dependency['name']} because it uses the CC0-1.0 license: {dependency['license']}') continue if 'Unlicense' in dependency['license']: print(f'Skipping {dependency['name']} because it uses the Unlicense license: {dependency['license']}') continue if 'Zlib' in dependency['license']: print(f'Skipping {dependency['name']} because it uses the Zlib license: {dependency['license']}') continue if 'notices' not in dependency: print(f'Skipping dependency with no notices: {dependency['name']}') continue if 'repository' not in dependency: print(f'Found dependency with no repository: {dependency['name']}') exit(1) if dependency['name'] == 'hashlink': # The notice for hashlink is mangled so correcting it here. dependency['notices'] = ['Copyright (c) 2015 The Rust Project Developers'] link_rst = f'`{dependency['name']} v{dependency['version']} <{dependency['repository']}>`_' underline_rst = '-' * len(link_rst) dep_notices_rst = '\n '.join(dependency['notices']) section_rst = f'{link_rst}\n{underline_rst}\n\n::\n\n {dep_notices_rst}\n\n' notices_rst += section_rst notices_rst_path = os.path.join(output_dir, 'dependency-notices.rst') with open(notices_rst_path, 'w', encoding="utf-8") as outfile: outfile.write(notices_rst)