// 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. // @dart = 2.8 import 'package:meta/meta.dart'; 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> additionalLicenses, ) { final Map> packageLicenses = >{}; final Set allPackages = {}; final List dependencies = []; 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 rawLicenses = file .readAsStringSync() .split(licenseSeparator); for (final String rawLicense in rawLicenses) { List packageNames; 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 = [package.name]; licenseText = rawLicense; } packageLicenses.putIfAbsent(licenseText, () => {}).addAll(packageNames); allPackages.addAll(packageNames); } } final List combinedLicensesList = packageLicenses.keys .map((String license) { final List 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 additionalLicenseText = []; final List errorMessages = []; 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: [], 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 dependencies; /// If non-empty, license collection failed and this messages should /// be displayed by the asset parser. final List errorMessages; }