| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:package_config/package_config.dart'; |
| |
| import 'base/file_system.dart'; |
| |
| /// Processes dependencies into a string representing the NOTICES file. |
| /// |
| /// Reads the NOTICES or LICENSE file from each package in the .packages file, |
| /// splitting each one into each component license so that it can be de-duped |
| /// if possible. If the NOTICES file exists, it is preferred over the LICENSE |
| /// file. |
| /// |
| /// Individual licenses inside each LICENSE file should be separated by 80 |
| /// hyphens on their own on a line. |
| /// |
| /// If a LICENSE or NOTICES file contains more than one component license, |
| /// then each component license must start with the names of the packages to |
| /// which the component license applies, with each package name on its own line |
| /// and the list of package names separated from the actual license text by a |
| /// blank line. The packages need not match the names of the pub package. For |
| /// example, a package might itself contain code from multiple third-party |
| /// sources, and might need to include a license for each one. |
| class LicenseCollector { |
| LicenseCollector({ |
| required FileSystem fileSystem |
| }) : _fileSystem = fileSystem; |
| |
| final FileSystem _fileSystem; |
| |
| /// The expected separator for multiple licenses. |
| static final String licenseSeparator = '\n${'-' * 80}\n'; |
| |
| /// Obtain licenses from the `packageMap` into a single result. |
| /// |
| /// [additionalLicenses] should contain aggregated license files from all |
| /// of the current applications dependencies. |
| LicenseResult obtainLicenses( |
| PackageConfig packageConfig, |
| Map<String, List<File>> additionalLicenses, |
| ) { |
| final Map<String, Set<String>> packageLicenses = <String, Set<String>>{}; |
| final Set<String> allPackages = <String>{}; |
| final List<File> dependencies = <File>[]; |
| |
| for (final Package package in packageConfig.packages) { |
| final Uri packageUri = package.packageUriRoot; |
| if (packageUri == null || packageUri.scheme != 'file') { |
| continue; |
| } |
| // First check for NOTICES, then fallback to LICENSE |
| File file = _fileSystem.file(packageUri.resolve('../NOTICES')); |
| if (!file.existsSync()) { |
| file = _fileSystem.file(packageUri.resolve('../LICENSE')); |
| } |
| if (!file.existsSync()) { |
| continue; |
| } |
| |
| dependencies.add(file); |
| final List<String> rawLicenses = file |
| .readAsStringSync() |
| .split(licenseSeparator); |
| for (final String rawLicense in rawLicenses) { |
| List<String> packageNames = <String>[]; |
| String? licenseText; |
| if (rawLicenses.length > 1) { |
| final int split = rawLicense.indexOf('\n\n'); |
| if (split >= 0) { |
| packageNames = rawLicense.substring(0, split).split('\n'); |
| licenseText = rawLicense.substring(split + 2); |
| } |
| } |
| if (licenseText == null) { |
| packageNames = <String>[package.name]; |
| licenseText = rawLicense; |
| } |
| packageLicenses.putIfAbsent(licenseText, () => <String>{}).addAll(packageNames); |
| allPackages.addAll(packageNames); |
| } |
| } |
| |
| final List<String> combinedLicensesList = packageLicenses.keys |
| .map<String>((String license) { |
| final List<String> packageNames = packageLicenses[license]!.toList() |
| ..sort(); |
| return '${packageNames.join('\n')}\n\n$license'; |
| }).toList(); |
| combinedLicensesList.sort(); |
| |
| /// Append additional LICENSE files as specified in the pubspec.yaml. |
| final List<String> additionalLicenseText = <String>[]; |
| final List<String> errorMessages = <String>[]; |
| for (final String package in additionalLicenses.keys) { |
| for (final File license in additionalLicenses[package]!) { |
| if (!license.existsSync()) { |
| errorMessages.add( |
| 'package $package specified an additional license at ${license.path}, but this file ' |
| 'does not exist.' |
| ); |
| continue; |
| } |
| dependencies.add(license); |
| try { |
| additionalLicenseText.add(license.readAsStringSync()); |
| } on FormatException catch (err) { |
| // File has an invalid encoding. |
| errorMessages.add( |
| 'package $package specified an additional license at ${license.path}, but this file ' |
| 'could not be read:\n$err' |
| ); |
| } on FileSystemException catch (err) { |
| // File cannot be parsed. |
| errorMessages.add( |
| 'package $package specified an additional license at ${license.path}, but this file ' |
| 'could not be read:\n$err' |
| ); |
| } |
| } |
| } |
| if (errorMessages.isNotEmpty) { |
| return LicenseResult( |
| combinedLicenses: '', |
| dependencies: <File>[], |
| errorMessages: errorMessages, |
| ); |
| } |
| |
| final String combinedLicenses = combinedLicensesList |
| .followedBy(additionalLicenseText) |
| .join(licenseSeparator); |
| |
| return LicenseResult( |
| combinedLicenses: combinedLicenses, |
| dependencies: dependencies, |
| errorMessages: errorMessages, |
| ); |
| } |
| } |
| |
| /// The result of processing licenses with a [LicenseCollector]. |
| class LicenseResult { |
| const LicenseResult({ |
| required this.combinedLicenses, |
| required this.dependencies, |
| required this.errorMessages, |
| }); |
| |
| /// The raw text of the consumed licenses. |
| final String combinedLicenses; |
| |
| /// Each license file that was consumed as input. |
| final List<File> dependencies; |
| |
| /// If non-empty, license collection failed and this messages should |
| /// be displayed by the asset parser. |
| final List<String> errorMessages; |
| } |