blob: 368aab7701f55e5e57f4360d2dde822d638fe43c [file] [log] [blame]
// 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;
}