| // Copyright 2013 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. |
| |
| // See README in this directory for information on how this code is organized. |
| |
| import 'dart:core' hide RegExp; |
| import 'dart:io' as system; |
| import 'dart:math' as math; |
| |
| import 'package:args/args.dart'; |
| import 'package:collection/collection.dart' show IterableExtension; |
| import 'package:crypto/crypto.dart' as crypto; |
| import 'package:path/path.dart' as path; |
| |
| import 'filesystem.dart' as fs; |
| import 'licenses.dart'; |
| import 'paths.dart'; |
| import 'patterns.dart'; |
| import 'regexp_debug.dart'; |
| |
| abstract class _RepositoryEntry implements Comparable<_RepositoryEntry> { |
| _RepositoryEntry(this.parent, this.io); |
| final _RepositoryDirectory? parent; |
| final fs.IoNode io; |
| String get name => io.name; |
| |
| @override |
| int compareTo(_RepositoryEntry other) => toString().compareTo(other.toString()); |
| |
| @override |
| String toString() => io.fullName; |
| } |
| |
| abstract class _RepositoryFile extends _RepositoryEntry { |
| _RepositoryFile(_RepositoryDirectory super.parent, fs.File super.io); |
| |
| fs.File get ioFile => super.io as fs.File; |
| } |
| |
| abstract class _RepositoryLicensedFile extends _RepositoryFile { |
| _RepositoryLicensedFile(super.parent, super.io); |
| |
| // Returns the License that is found inside this file. |
| // |
| // Used when one file says it is covered by the license of another file. |
| License extractInternalLicense() { |
| throw 'tried to extract a license from $this but it does not contain text'; |
| } |
| |
| // Creates the mapping from the file to its licenses. |
| // |
| // Subclasses implement things like applying licenses found within the file |
| // itself, applying licenses that point to other files, etc. |
| // |
| // The assignments are later grouped when generating the output, see |
| // groupLicenses in licenses.dart. |
| Iterable<Assignment> assignLicenses() { |
| final List<License> licenses = parent!.nearestLicensesFor(name); |
| if (licenses.isEmpty) { |
| throw 'file has no detectable license and no in-scope default license file'; |
| } |
| return licenses.map((License license) => license.assignLicenses(io.fullName, parent!)); |
| } |
| } |
| |
| class _RepositorySourceFile extends _RepositoryLicensedFile { |
| _RepositorySourceFile(super.parent, fs.TextFile super.io) : _contents = _readFile(io); |
| |
| fs.TextFile get ioTextFile => super.io as fs.TextFile; |
| |
| final String _contents; |
| |
| static String _readFile(fs.TextFile ioTextFile) { |
| late String contents; |
| try { |
| contents = ioTextFile.readString(); |
| } on FormatException { |
| throw 'non-UTF8 data in $ioTextFile'; |
| } |
| return contents; |
| } |
| |
| List<License>? _internalLicenses; |
| |
| @override |
| License extractInternalLicense() { |
| _internalLicenses ??= determineLicensesFor(_contents, name, parent, origin: io.fullName); |
| if (_internalLicenses == null || _internalLicenses!.isEmpty) { |
| throw 'tried to extract a license from $this but there is nothing here'; |
| } |
| if (_internalLicenses!.length > 1) { |
| throw 'tried to extract a license from $this but there are multiple licenses in this file'; |
| } |
| return _internalLicenses!.single; |
| } |
| |
| @override |
| Iterable<Assignment> assignLicenses() { |
| _internalLicenses ??= determineLicensesFor(_contents, name, parent, origin: io.fullName); |
| final List<License>? licenses = _internalLicenses; |
| if (licenses != null && licenses.isNotEmpty) { |
| return licenses.map((License license) => license.assignLicenses(io.fullName, parent!)); |
| } |
| return super.assignLicenses(); |
| } |
| } |
| |
| class _RepositoryBinaryFile extends _RepositoryLicensedFile { |
| _RepositoryBinaryFile(super.parent, super.io); |
| |
| @override |
| Iterable<Assignment> assignLicenses() { |
| final List<License> licenses = parent!.nearestLicensesFor(name); |
| if (licenses.isEmpty) { |
| throw 'no license file found in scope for ${io.fullName}'; |
| } |
| return licenses.map((License license) => license.assignLicenses(io.fullName, parent!)); |
| } |
| } |
| |
| // LICENSES |
| |
| abstract class _RepositoryLicenseFile extends _RepositoryFile { |
| _RepositoryLicenseFile(super.parent, fs.TextFile super.io); |
| |
| // returns any licenses that apply specifically to the file named "name" |
| List<License> licensesFor(String name) => licenses; |
| |
| // returns the license that applies to files looking for a license of the given type |
| License? licenseOfType(LicenseType type); |
| |
| // returns the license that applies to files looking for a license of the given name |
| License? licenseWithName(String name); |
| |
| License? get defaultLicense; |
| List<License> get licenses; |
| } |
| |
| abstract class _RepositorySingleLicenseFile extends _RepositoryLicenseFile { |
| _RepositorySingleLicenseFile(super.parent, super.io, this.license); |
| |
| final License license; |
| |
| @override |
| License? licenseWithName(String name) { |
| if (this.name == name) { |
| return license; |
| } |
| return null; |
| } |
| |
| @override |
| License? licenseOfType(LicenseType type) { |
| assert(type != LicenseType.unknown); |
| if (type == license.type) { |
| return license; |
| } |
| return null; |
| } |
| |
| @override |
| License get defaultLicense => license; |
| |
| @override |
| List<License> get licenses => <License>[license]; |
| } |
| |
| class _RepositoryGeneralSingleLicenseFile extends _RepositorySingleLicenseFile { |
| _RepositoryGeneralSingleLicenseFile(_RepositoryDirectory parent, fs.TextFile io) |
| : super(parent, io, _parseLicense(io)); |
| |
| static License _parseLicense(fs.TextFile io) { |
| final String body = io.readString(); |
| int start = 0; |
| Match? match; |
| while ((match = licenseHeaders.matchAsPrefix(body, start)) != null) { |
| assert(match!.end > start); |
| start = match!.end; // https://github.com/dart-lang/sdk/issues/50264 |
| } |
| return License.fromBodyAndName(body.substring(start), io.name, origin: io.fullName); |
| } |
| } |
| |
| // Avoid using this unless the license file has multiple licenses in a weird mishmash. |
| // For example, the RapidJSON license which mixes BSD and MIT in somewhat confusing ways |
| // (e.g. having the copyright for one above the terms for the other and so on). |
| class _RepositoryOpaqueLicenseFile extends _RepositorySingleLicenseFile { |
| _RepositoryOpaqueLicenseFile(_RepositoryDirectory parent, fs.TextFile io) |
| : super(parent, io, License.unique(io.readString(), LicenseType.unknown, origin: io.fullName, yesWeKnowWhatItLooksLikeButItIsNot: true)); |
| } |
| |
| class _RepositoryReadmeIjgFile extends _RepositorySingleLicenseFile { |
| _RepositoryReadmeIjgFile(_RepositoryDirectory parent, fs.TextFile io) |
| : super(parent, io, _parseLicense(io)); |
| |
| // The message we are required to include in our output. |
| // |
| // We include it by just including the whole license. |
| static const String _message = 'this software is based in part on the work of the Independent JPEG Group'; |
| |
| // The license text that says we should output _message. |
| // |
| // We inject _message into it, munged such that each space character can also |
| // match a newline, so that if the license wraps the text, it still matches. |
| static final RegExp _pattern = RegExp( |
| r'Permission is hereby granted to use, copy, modify, and distribute this\n' |
| r'software \(or portions thereof\) for any purpose, without fee, subject to these\n' |
| r'conditions:\n' |
| r'\(1\) If any part of the source code for this software is distributed, then this\n' |
| r'README file must be included, with this copyright and no-warranty notice\n' |
| r'unaltered; and any additions, deletions, or changes to the original files\n' |
| r'must be clearly indicated in accompanying documentation\.\n' |
| r'\(2\) If only executable code is distributed, then the accompanying\n' |
| r'documentation must state that "' '${_message.replaceAll(" ", "[ \n]+")}' r'"\.\n' |
| r'\(3\) Permission for use of this software is granted only if the user accepts\n' |
| r'full responsibility for any undesirable consequences; the authors accept\n' |
| r'NO LIABILITY for damages of any kind\.\n', |
| ); |
| |
| static License _parseLicense(fs.TextFile io) { |
| final String body = io.readString(); |
| if (!body.contains(_pattern)) { |
| throw 'unexpected contents in IJG README'; |
| } |
| return License.message(body, LicenseType.ijg, origin: io.fullName); |
| } |
| |
| @override |
| License? licenseWithName(String name) { |
| if (this.name == name) { |
| return license; |
| } |
| return null; |
| } |
| } |
| |
| class _RepositoryDartLicenseFile extends _RepositorySingleLicenseFile { |
| _RepositoryDartLicenseFile(_RepositoryDirectory parent, fs.TextFile io) |
| : super(parent, io, _parseLicense(io)); |
| |
| static final RegExp _pattern = RegExp( |
| r'(Copyright (?:.|\n)+)$', |
| ); |
| |
| static License _parseLicense(fs.TextFile io) { |
| final Match? match = _pattern.firstMatch(io.readString()); |
| if (match == null || match.groupCount != 1) { |
| throw 'unexpected Dart license file contents'; |
| } |
| return License.template(match.group(1)!, LicenseType.bsd, origin: io.fullName); |
| } |
| } |
| |
| class _RepositoryLibPngLicenseFile extends _RepositorySingleLicenseFile { |
| _RepositoryLibPngLicenseFile(_RepositoryDirectory parent, fs.TextFile io) |
| : super(parent, io, License.message(io.readString(), LicenseType.libpng, origin: io.fullName)) { |
| _verifyLicense(io); |
| } |
| |
| static void _verifyLicense(fs.TextFile io) { |
| final String contents = io.readString(); |
| if (!contents.contains(RegExp('COPYRIGHT NOTICE, DISCLAIMER, and LICENSE:?')) || |
| !contents.contains('png')) { |
| throw 'unexpected libpng license file contents:\n----8<----\n$contents\n----<8----'; |
| } |
| } |
| } |
| |
| class _RepositoryLibJpegTurboLicenseFile extends _RepositoryLicenseFile { |
| _RepositoryLibJpegTurboLicenseFile(_RepositoryDirectory parent, fs.TextFile io) |
| : super(parent, io) { |
| _parseLicense(io); |
| } |
| |
| static final RegExp _pattern = RegExp( |
| r'libjpeg-turbo is covered by three compatible BSD-style open source licenses:\n' |
| r'\n' |
| r'- The IJG \(Independent JPEG Group\) License, which is listed in\n' |
| r' \[README\.ijg\]\(README\.ijg\)\n' |
| r'\n' |
| r' This license applies to the libjpeg API library and associated programs\n' |
| r' \(any code inherited from libjpeg, and any modifications to that code\.\)\n' |
| r'\n' |
| r'- The Modified \(3-clause\) BSD License, which is listed in\n' |
| r' \[turbojpeg\.c\]\(turbojpeg\.c\)\n' |
| r'\n' |
| r' This license covers the TurboJPEG API library and associated programs\.\n' |
| r'\n' |
| r'- The zlib License, which is listed in \[simd/jsimdext\.inc\]\(simd/jsimdext\.inc\)\n' |
| r'\n' |
| r' This license is a subset of the other two, and it covers the libjpeg-turbo\n' |
| r' SIMD extensions\.\n' |
| ); |
| |
| static void _parseLicense(fs.TextFile io) { |
| final String body = io.readString(); |
| if (!body.contains(_pattern)) { |
| throw 'unexpected contents in libjpeg-turbo LICENSE'; |
| } |
| } |
| |
| @override |
| License? licenseWithName(String name) { |
| return null; |
| } |
| |
| @override |
| License? licenseOfType(LicenseType type) { |
| return null; |
| } |
| |
| @override |
| License? get defaultLicense => null; |
| |
| List<License>? _licenses; |
| |
| @override |
| List<License> get licenses { |
| if (_licenses == null) { |
| final _RepositoryLicenseFile readme = parent!.getChildByName('README.ijg') as _RepositoryReadmeIjgFile; |
| final _RepositorySourceFile main = parent!.getChildByName('turbojpeg.c') as _RepositorySourceFile; |
| final _RepositoryDirectory simd = parent!.getChildByName('simd') as _RepositoryDirectory; |
| final _RepositorySourceFile zlib = simd.getChildByName('jsimdext.inc') as _RepositorySourceFile; |
| _licenses = <License>[]; |
| _licenses!.addAll(readme.licenses); |
| _licenses!.add(main.extractInternalLicense()); |
| _licenses!.add(zlib.extractInternalLicense()); |
| } |
| return _licenses!; |
| } |
| } |
| |
| class _RepositoryFreetypeLicenseFile extends _RepositoryLicenseFile { |
| _RepositoryFreetypeLicenseFile(super.parent, super.io) |
| : _target = _parseLicense(io); |
| |
| static final RegExp _pattern = RegExp( |
| r'FREETYPE LICENSES\n' |
| r'-----------------\n' |
| r'\n' |
| r'The FreeType 2 font engine is copyrighted work and cannot be used\n' |
| r'legally without a software license\. In order to make this project\n' |
| r'usable to a vast majority of developers, we distribute it under two\n' |
| r'mutually exclusive open-source licenses\.\n' |
| r'\n' |
| r'This means that \*you\* must choose \*one\* of the two licenses described\n' |
| r'below, then obey all its terms and conditions when using FreeType 2 in\n' |
| r'any of your projects or products\.\n' |
| r'\n' |
| r' - The FreeType License, found in the file `docs/(FTL\.TXT)`, which is\n' |
| r' similar to the original BSD license \*with\* an advertising clause\n' |
| r' that forces you to explicitly cite the FreeType project in your\n' |
| r" product's documentation\. All details are in the license file\.\n" |
| r" This license is suited to products which don't use the GNU General\n" |
| r' Public License\.\n' |
| r'\n' |
| r' Note that this license is compatible to the GNU General Public\n' |
| r' License version 3, but not version 2\.\n' |
| r'\n' |
| r' - The GNU General Public License version 2, found in\n' |
| r' `docs/GPLv2\.TXT` \(any later version can be used also\), for\n' |
| r' programs which already use the GPL\. Note that the FTL is\n' |
| r' incompatible with GPLv2 due to its advertisement clause\.\n' |
| r'\n' |
| r'The contributed BDF and PCF drivers come with a license similar to\n' |
| r'that of the X Window System\. It is compatible to the above two\n' |
| r'licenses \(see files `src/bdf/README` and `src/pcf/README`\)\. The same\n' |
| r'holds for the source code files `src/base/fthash\.c` and\n' |
| r'`include/freetype/internal/fthash\.h`; they wer part of the BDF driver\n' |
| r'in earlier FreeType versions\.\n' |
| r'\n' |
| r'The gzip module uses the zlib license \(see `src/gzip/zlib\.h`\) which\n' |
| r'too is compatible to the above two licenses\.\n' |
| r'\n' |
| r'The MD5 checksum support \(only used for debugging in development\n' |
| r'builds\) is in the public domain\.\n' |
| r'\n*' |
| r'--- end of LICENSE\.TXT ---\n*$' |
| ); |
| |
| static String _parseLicense(fs.TextFile io) { |
| final Match? match = _pattern.firstMatch(io.readString()); |
| if (match == null || match.groupCount != 1) { |
| throw 'unexpected Freetype license file contents'; |
| } |
| return match.group(1)!; |
| } |
| |
| final String _target; |
| List<License>? _targetLicense; |
| |
| void _warmCache() { |
| if (parent != null && _targetLicense == null) { |
| final License? license = parent!.nearestLicenseWithName(_target); |
| if (license == null) { |
| throw 'Could not find license in Freetype directory. Make sure $_target is not excluded from consideration.'; |
| } |
| _targetLicense = <License>[license]; |
| } |
| } |
| |
| @override |
| License? licenseOfType(LicenseType type) => null; |
| |
| @override |
| License? licenseWithName(String name) => null; |
| |
| @override |
| License get defaultLicense { |
| _warmCache(); |
| return _targetLicense!.single; |
| } |
| |
| @override |
| List<License> get licenses { |
| _warmCache(); |
| return _targetLicense ?? const <License>[]; |
| } |
| } |
| |
| class _RepositoryIcuLicenseFile extends _RepositorySingleLicenseFile { |
| factory _RepositoryIcuLicenseFile(_RepositoryDirectory parent, fs.TextFile io) { |
| final Match? match = _pattern.firstMatch(_fixup(io.readString())); |
| if (match == null) { |
| throw 'could not parse ICU license file'; |
| } |
| const int groupCount = 22; |
| assert(match.groupCount == groupCount, 'ICU: expected $groupCount groups, but got ${match.groupCount}'); |
| const int timeZoneGroup = 18; |
| if (match.group(timeZoneGroup)!.contains(copyrightMentionPattern)) { |
| throw 'ICU: unexpected copyright in time zone database group\n:${match.group(timeZoneGroup)}'; |
| } |
| if (!match.group(timeZoneGroup)!.contains('7. Database Ownership')) { |
| throw 'ICU: unexpected text in time zone database group\n:${match.group(timeZoneGroup)}'; |
| } |
| const int gplGroup1 = 20; |
| const int gplGroup2 = 21; |
| if (!match.group(gplGroup1)!.contains(gplExceptionExplanation1) || !match.group(gplGroup2)!.contains(gplExceptionExplanation2)) { |
| throw 'ICU: did not find GPL exception in GPL-licensed files'; |
| } |
| const Set<int> skippedGroups = <int>{ timeZoneGroup, gplGroup1, gplGroup2 }; |
| return _RepositoryIcuLicenseFile._( |
| parent, |
| io, |
| License.template(match.group(2)!, LicenseType.bsd, origin: io.fullName), |
| License.fromMultipleBlocks( |
| match.groups( |
| Iterable<int> |
| .generate(groupCount, (int index) => index + 1) |
| .where((int index) => !skippedGroups.contains(index)) |
| .toList() |
| ).cast<String>(), |
| LicenseType.icu, |
| origin: io.fullName, |
| yesWeKnowWhatItLooksLikeButItIsNot: true, |
| ), |
| ); |
| } |
| |
| _RepositoryIcuLicenseFile._(_RepositoryDirectory parent, fs.TextFile io, this.template, License license) |
| : super(parent, io, license); |
| |
| // Every paragraph of the license is mentioned. All newlines are disregarded [\n+]. |
| static final RegExp _pattern = RegExp( |
| // Starting from ICU 74.1 this has been changed to Unicode License V3. |
| r'^(UNICODE LICENSE V3\n+' // group |
| r'COPYRIGHT AND PERMISSION NOTICE(?:.|\n)+?' |
| r')(Permission is hereby granted(?:.|\n)+?)' // template group |
| r'\n+-+\n+' |
| // From here onwards should be the same as before 74.1 |
| r'(Third-Party Software Licenses\n+' // group |
| r'This section contains third-party software notices and/or additional\n' |
| r'terms for licensed third-party software components included within ICU\n' |
| r'libraries\.\n+)' |
| r'-+\n+' |
| r'(ICU License - ICU 1\.8\.1 to ICU 57.1[ \n]+?' // group |
| r'COPYRIGHT AND PERMISSION NOTICE\n+' |
| r'Copyright (?:.|\n)+?)\n+' |
| r'-+\n+' |
| r'(Chinese/Japanese Word Break Dictionary Data \(cjdict\.txt\)\n+)' // group |
| r'( # The Google Chrome software developed by Google is licensed under\n?' // group |
| r' # the BSD license\. Other software included in this distribution is\n?' |
| r' # provided under other licenses, as set forth below\.\n' |
| r' #\n' |
| r' # The BSD License\n' |
| r' # http://opensource\.org/licenses/bsd-license\.php\n' |
| r' # +Copyright(?:.|\n)+?)\n' |
| r' #\n' |
| r' #\n' |
| r'( # The word list in cjdict.txt are generated by combining three word lists\n?' // group |
| r' # listed below with further processing for compound word breaking\. The\n?' |
| r' # frequency is generated with an iterative training against Google web\n?' |
| r' # corpora\.\n' |
| r' #\n' // if this section is taken out, make sure to remove the cjdict.txt exclusion in paths.dart |
| r' # \* Libtabe \(Chinese\)\n' |
| r' # - https://sourceforge\.net/project/\?group_id=1519\n' |
| r' # - Its license terms and conditions are shown below\.\n' |
| r' #\n' |
| r' # \* IPADIC \(Japanese\)\n' |
| r' # - http://chasen\.aist-nara\.ac\.jp/chasen/distribution\.html\n' |
| r' # - Its license terms and conditions are shown below\.\n' |
| r' #\n)' |
| r' # ---------COPYING\.libtabe ---- BEGIN--------------------\n' |
| r' #\n' |
| r' # +/\*\n' |
| r'( # +\* Copyright (?:.|\n)+?)\n' // group |
| r' # +\*/\n' |
| r' #\n' |
| r' # +/\*\n' |
| r'( # +\* Copyright (?:.|\n)+?)\n' // group |
| r' # +\*/\n' |
| r' #\n' |
| r'( # +Copyright (?:.|\n)+?)\n' // group |
| r' #\n' |
| r' # +---------------COPYING\.libtabe-----END--------------------------------\n' |
| r' #\n' |
| r' #\n' |
| r' # +---------------COPYING\.ipadic-----BEGIN-------------------------------\n' |
| r' #\n' |
| r'( # +Copyright (?:.|\n)+?)\n' // group |
| r' #\n' |
| r' # +---------------COPYING\.ipadic-----END----------------------------------\n+' |
| r'-+\n+' |
| r'(Lao Word Break Dictionary Data \(laodict\.txt\)\n)' // group |
| r'\n' // if this section is taken out, make sure to remove the laodict.txt exclusion in paths.dart |
| r'( # +Copyright(?:.|\n)+?)' // group |
| r'( # +This file is derived(?:.|\n)+?)\n' |
| r'\n' |
| r'-+\n' |
| r'\n' |
| r'(Burmese Word Break Dictionary Data \(burmesedict\.txt\)\n)' // group |
| r'\n' // if this section is taken out, make sure to remove the burmesedict.txt exclusion in paths.dart |
| r'( # +Copyright(?:.|\n)+?)\n' // group |
| r' # +-+\n' |
| r'( # +Copyright(?:.|\n)+?)\n' // group |
| r' # +-+\n+' |
| r'-+\n+' |
| r'( *Time Zone Database\n' |
| r'(?:.|\n)+)\n' // group (not really a license -- excluded) |
| r'\n' |
| r'-+\n' |
| r'\n' |
| r'(Google double-conversion\n' // group |
| r'\n' |
| r'Copyright(?:.|\n)+)\n' |
| r'\n' |
| r'-+\n' |
| r'\n' |
| r'(File: aclocal\.m4 \(only for ICU4C\)\n' // group (excluded) |
| r'Section: pkg\.m4 - Macros to locate and utilise pkg-config\.\n+' |
| r'Copyright (?:.|\n)+?)\n' |
| r'\n' |
| r'-+\n' |
| r'\n' |
| r'(File: config\.guess \(only for ICU4C\)\n+' // group (excluded) |
| r'This file is free software(?:.|\n)+?)\n' |
| r'\n' |
| r'-+\n' |
| r'\n' |
| r'(File: install-sh \(only for ICU4C\)\n+' // group |
| r'Copyright(?:.|\n)+)', |
| ); |
| |
| static const String gplExceptionExplanation1 = |
| 'As a special exception to the GNU General Public License, if you\n' |
| 'distribute this file as part of a program that contains a\n' |
| 'configuration script generated by Autoconf, you may include it under\n' |
| 'the same distribution terms that you use for the rest of that\n' |
| 'program.\n' |
| '\n' |
| '\n' |
| '(The condition for the exception is fulfilled because\n' |
| 'ICU4C includes a configuration script generated by Autoconf,\n' |
| 'namely the `configure` script.)'; |
| |
| static const String gplExceptionExplanation2 = |
| 'As a special exception to the GNU General Public License, if you\n' |
| 'distribute this file as part of a program that contains a\n' |
| 'configuration script generated by Autoconf, you may include it under\n' |
| 'the same distribution terms that you use for the rest of that\n' |
| 'program. This Exception is an additional permission under section 7\n' |
| 'of the GNU General Public License, version 3 ("GPLv3").\n' |
| '\n' |
| '\n' |
| '(The condition for the exception is fulfilled because\n' |
| 'ICU4C includes a configuration script generated by Autoconf,\n' |
| 'namely the `configure` script.)'; |
| |
| // Fixes an error in the license's formatting that our reformatter wouldn't be |
| // able to figure out on its own and which would otherwise completely mess up |
| // the reformatting of this license file. |
| static String _fixup(String license) { |
| return license.replaceAll( |
| ' # * Sinica. All rights reserved.', |
| ' # * Sinica. All rights reserved.', |
| ); |
| } |
| |
| final License template; |
| |
| @override |
| License? licenseOfType(LicenseType type) { |
| if (type == LicenseType.defaultTemplate) { |
| return template; |
| } |
| return super.licenseOfType(type); |
| } |
| } |
| |
| class _RepositoryCxxStlDualLicenseFile extends _RepositoryLicenseFile { |
| _RepositoryCxxStlDualLicenseFile(super.parent, super.io) |
| : _licenses = _parseLicenses(io); |
| |
| static final RegExp _pattern = RegExp( |
| r'^' |
| r'==============================================================================\n' |
| r'The LLVM Project is under the Apache License v2\.0 with LLVM Exceptions:\n' |
| r'==============================================================================\n' |
| r'\n(' |
| r' *Apache License\n' |
| r' *Version 2.0, January 2004\n' |
| r' *http://www.apache.org/licenses/\n' |
| r'\n' |
| r'.+?)\n+' |
| r'---- LLVM Exceptions to the Apache 2.0 License ----' |
| r'.+?' |
| r'==============================================================================\n' |
| r'Software from third parties included in the LLVM Project:\n' |
| r'==============================================================================\n' |
| r'The LLVM Project contains third party software which is under different license\n' |
| r'terms\. All such code will be identified clearly using at least one of two\n' |
| r'mechanisms:\n' |
| r'1\) It will be in a separate directory tree with its own `LICENSE\.txt` or\n' |
| r' *`LICENSE` file at the top containing the specific license and restrictions\n' |
| r' *which apply to that software, or\n' |
| r'2\) It will contain specific license and restriction terms at the top of every\n' |
| r' *file\.\n' |
| r'\n' |
| r'==============================================================================\n' |
| r'Legacy LLVM License \(https://llvm\.org/docs/DeveloperPolicy\.html#legacy\):\n' |
| r'==============================================================================\n' |
| r'\n' |
| r'The libc\+\+(?:abi)? library is dual licensed under both the University of Illinois\n' |
| r'"BSD-Like" license and the MIT license\. *As a user of this code you may choose\n' |
| r'to use it under either license\. *As a contributor, you agree to allow your code\n' |
| r'to be used under both\.\n' |
| r'\n' |
| r'Full text of the relevant licenses is included below\.\n' |
| r'\n' |
| r'==============================================================================\n' |
| r'\n' |
| r'University of Illinois/NCSA\n' |
| r'Open Source License\n' |
| r'\n' |
| r'(Copyright \(c\) 2009-2019 by the contributors listed in CREDITS\.TXT\n' |
| r'\n' |
| r'All rights reserved\.\n' |
| r'\n' |
| r'Developed by:\n' |
| r'\n' |
| r' *LLVM Team\n' |
| r'\n' |
| r' *University of Illinois at Urbana-Champaign\n' |
| r'\n' |
| r' *http://llvm\.org\n' |
| r'\n' |
| r'Permission is hereby granted, free of charge, to any person obtaining a copy of\n' |
| r'this software and associated documentation files \(the "Software"\), to deal with\n' |
| r'the Software without restriction, including without limitation the rights to\n' |
| r'use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\n' |
| r'of the Software, and to permit persons to whom the Software is furnished to do\n' |
| r'so, subject to the following conditions:\n' |
| r'\n' |
| r' *\* Redistributions of source code must retain the above copyright notice,\n' |
| r' *this list of conditions and the following disclaimers\.\n' |
| r'\n' |
| r' *\* Redistributions in binary form must reproduce the above copyright notice,\n' |
| r' *this list of conditions and the following disclaimers in the\n' |
| r' *documentation and/or other materials provided with the distribution\.\n' |
| r'\n' |
| r' *\* Neither the names of the LLVM Team, University of Illinois at\n' |
| r' *Urbana-Champaign, nor the names of its contributors may be used to\n' |
| r' *endorse or promote products derived from this Software without specific\n' |
| r' *prior written permission\.\n' |
| r'\n' |
| r'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n' |
| r'IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n' |
| r'FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE\n' |
| r'CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n' |
| r'LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n' |
| r'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE\n' |
| r'SOFTWARE\.)\n*' |
| r'==============================================================================\n*' |
| r'(Copyright \(c\) 2009-2014 by the contributors listed in CREDITS\.TXT\n' |
| r'\n' |
| r'Permission is hereby granted, free of charge, to any person obtaining a copy\n' |
| r'of this software and associated documentation files \(the "Software"\), to deal\n' |
| r'in the Software without restriction, including without limitation the rights\n' |
| r'to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n' |
| r'copies of the Software, and to permit persons to whom the Software is\n' |
| r'furnished to do so, subject to the following conditions:\n' |
| r'\n' |
| r'The above copyright notice and this permission notice shall be included in\n' |
| r'all copies or substantial portions of the Software\.\n' |
| r'\n' |
| r'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n' |
| r'IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n' |
| r'FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE\n' |
| r'AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n' |
| r'LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n' |
| r'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n' |
| r'THE SOFTWARE\.)\n*' |
| r'$', |
| dotAll: true, |
| ); |
| |
| static List<License> _parseLicenses(fs.TextFile io) { |
| final Match? match = _pattern.firstMatch(io.readString()); |
| if (match == null) { |
| throw 'unexpected license file contents'; |
| } |
| if (match.groupCount != 3) { |
| throw 'internal error; match count inconsistency\nRemainder:[[${match.input.substring(match.end)}]]'; |
| } |
| return <License>[ |
| // License.fromBodyAndType(match.group(1), LicenseType.apache), // the exception says we can ignore this |
| License.fromBodyAndType(match.group(2)!, LicenseType.bsd, origin: io.fullName), |
| License.fromBodyAndType(match.group(3)!, LicenseType.mit, origin: io.fullName), |
| ]; |
| } |
| |
| final List<License> _licenses; |
| |
| @override |
| License licenseOfType(LicenseType type) { |
| throw 'tried to look up a dual-license license by type ("$type")'; |
| } |
| |
| @override |
| License licenseWithName(String name) { |
| throw 'tried to look up a dual-license license by name ("$name")'; |
| } |
| |
| @override |
| License get defaultLicense => _licenses[0]; |
| |
| @override |
| List<License> get licenses => _licenses; |
| } |
| |
| class _RepositoryKhronosLicenseFile extends _RepositoryLicenseFile { |
| _RepositoryKhronosLicenseFile(super.parent, super.io) |
| : _licenses = _parseLicenses(io); |
| |
| static final RegExp _pattern = RegExp( |
| r'^(Copyright .+?)\n' |
| r'SGI FREE SOFTWARE LICENSE B[^\n]+\n\n' |
| r'(Copyright .+?)$', |
| dotAll: true, |
| ); |
| |
| static List<License> _parseLicenses(fs.TextFile io) { |
| final Match? match = _pattern.firstMatch(io.readString()); |
| if (match == null || match.groupCount != 2) { |
| throw 'unexpected Khronos license file contents'; |
| } |
| return <License>[ |
| License.fromBodyAndType(match.group(1)!, LicenseType.mit, origin: io.fullName), |
| License.fromBodyAndType(match.group(2)!, LicenseType.mit, origin: io.fullName), |
| ]; |
| } |
| |
| final List<License> _licenses; |
| |
| @override |
| License licenseOfType(LicenseType type) { |
| throw 'tried to look up a combination license by type ("$type")'; |
| } |
| |
| @override |
| License licenseWithName(String name) { |
| throw 'tried to look up a combination license by name ("$name")'; |
| } |
| |
| @override |
| License get defaultLicense { |
| throw 'there is no default Khronos license'; |
| } |
| |
| @override |
| List<License> get licenses => _licenses; |
| } |
| |
| /// The BoringSSL license file. |
| /// |
| /// This file contains a bunch of different licenses, but other files |
| /// refer to it as if it was a monolithic license so we sort of have |
| /// to treat the whole thing as a MultiLicense. |
| class _RepositoryOpenSSLLicenseFile extends _RepositorySingleLicenseFile { |
| _RepositoryOpenSSLLicenseFile(_RepositoryDirectory parent, fs.TextFile io) |
| : super(parent, io, _parseLicense(io)); |
| |
| static final RegExp _pattern = RegExp( |
| // advice is to skip the first 27 lines of this file in the LICENSE file |
| r'^BoringSSL is a fork of OpenSSL. As such, .+?' |
| r'The following are Google-internal bug numbers where explicit permission from\n' |
| r'some authors is recorded for use of their work\. \(This is purely for our own\n' |
| r'record keeping\.\)\n+' |
| r'[0-9+ \n]+\n+' |
| r'(' // 1 |
| r' *OpenSSL License\n' |
| r' *---------------\n+)' |
| r'(.+?)\n+' // 2 |
| r'(' // 3 |
| r' *Original SSLeay License\n' |
| r' *-----------------------\n+)' |
| r'(.+?)\n+' // 4 |
| r'( *ISC license used for completely new code in BoringSSL:\n+)' // 5 |
| r'(.+?)\n+' // 6 |
| r'( *The code in third_party/fiat carries the MIT license:\n+)' // 7 |
| r'(.+?)\n+' // 8 |
| r'(' // 9 |
| r' *Licenses for support code\n' |
| r' *-------------------------\n+)' |
| r'(.+?)\n+' // 10 |
| r'(BoringSSL uses the Chromium test infrastructure to run a continuous build,\n' // 11 |
| r'trybots etc\. The scripts which manage this, and the script for generating build\n' |
| r'metadata, are under the Chromium license\. Distributing code linked against\n' |
| r'BoringSSL does not trigger this license\.)\n+' |
| r'(.+?)\n+$', // 12 |
| dotAll: true, |
| ); |
| |
| static License _parseLicense(fs.TextFile io) { |
| final Match? match = _pattern.firstMatch(io.readString()); |
| if (match == null) { |
| throw 'Failed to match OpenSSL license pattern.'; |
| } |
| assert(match.groupCount == 12); |
| return License.fromMultipleBlocks( |
| List<String>.generate(match.groupCount, (int index) => match.group(index + 1)!).toList(), |
| LicenseType.openssl, |
| origin: io.fullName, |
| authors: 'The OpenSSL Project Authors', |
| yesWeKnowWhatItLooksLikeButItIsNot: true, // looks like BSD, but... |
| ); |
| } |
| } |
| |
| class _RepositoryFuchsiaSdkLinuxLicenseFile extends _RepositorySingleLicenseFile { |
| _RepositoryFuchsiaSdkLinuxLicenseFile(_RepositoryDirectory parent, fs.TextFile io) |
| : super(parent, io, _parseLicense(io)); |
| |
| static const String _pattern = 'The majority of files in this project use the Apache 2.0 License.\n' |
| 'There are a few exceptions and their license can be found in the source.\n' |
| 'Any license deviations from Apache 2.0 are "more permissive" licenses.\n'; |
| |
| static License _parseLicense(fs.TextFile io) { |
| final String body = io.readString(); |
| if (!body.startsWith(_pattern)) { |
| throw 'unexpected Fuchsia license file contents'; |
| } |
| return License.fromBodyAndType(body.substring(_pattern.length), LicenseType.apache, origin: io.fullName); |
| } |
| } |
| |
| /// The BoringSSL license file. |
| /// |
| /// This file contains a bunch of different licenses, but other files |
| /// refer to it as if it was a monolithic license so we sort of have |
| /// to treat the whole thing as a MultiLicense. |
| class _RepositoryVulkanApacheLicenseFile extends _RepositorySingleLicenseFile { |
| _RepositoryVulkanApacheLicenseFile(_RepositoryDirectory parent, fs.TextFile io) |
| : super(parent, io, _parseLicense(io)); |
| |
| static const String _prefix = |
| 'The majority of files in this project use the Apache 2.0 License.\n' |
| 'There are a few exceptions and their license can be found in the source.\n' |
| 'Any license deviations from Apache 2.0 are "more permissive" licenses.\n' |
| "Any file without a license in it's source defaults to the repository Apache 2.0 License.\n" |
| '\n' |
| '===========================================================================================\n' |
| '\n'; |
| |
| static License _parseLicense(fs.TextFile io) { |
| final String body = io.readString(); |
| if (!body.startsWith(_prefix)) { |
| throw 'Failed to match Vulkan Apache license prefix.'; |
| } |
| return License.fromBody(body.substring(_prefix.length), origin: io.fullName); |
| } |
| } |
| |
| |
| // DIRECTORIES |
| |
| typedef _Constructor = _RepositoryFile Function(_RepositoryDirectory parent, fs.TextFile); |
| |
| class _RepositoryDirectory extends _RepositoryEntry implements LicenseSource { |
| _RepositoryDirectory(super.parent, fs.Directory super.io) { |
| crawl(); |
| } |
| |
| fs.Directory get ioDirectory => super.io as fs.Directory; |
| fs.Directory get rootDirectory => parent != null ? parent!.rootDirectory : ioDirectory; |
| |
| final List<_RepositoryDirectory> _subdirectories = <_RepositoryDirectory>[]; |
| final List<_RepositoryLicensedFile> _files = <_RepositoryLicensedFile>[]; |
| final List<_RepositoryLicenseFile> _licenses = <_RepositoryLicenseFile>[]; |
| static final List<fs.IoNode> _excluded = <fs.IoNode>[]; |
| |
| List<_RepositoryDirectory> get subdirectories => _subdirectories; |
| |
| final Map<String,_RepositoryEntry> _childrenByName = <String,_RepositoryEntry>{}; |
| |
| void crawl() { |
| for (final fs.IoNode entry in ioDirectory.walk) { |
| if (shouldRecurse(entry)) { |
| assert(!_childrenByName.containsKey(entry.name)); |
| if (entry is fs.Directory) { |
| final _RepositoryDirectory child = createSubdirectory(entry); |
| _subdirectories.add(child); |
| _childrenByName[child.name] = child; |
| } else if (entry is fs.File) { |
| try { |
| final _RepositoryFile child = createFile(entry); |
| if (child is _RepositoryLicensedFile) { |
| _files.add(child); |
| } else { |
| assert(child is _RepositoryLicenseFile); |
| _licenses.add(child as _RepositoryLicenseFile); |
| } |
| _childrenByName[child.name] = child; |
| } catch (error, stack) { |
| throw 'failed to handle $entry\n$error\nOriginal stack:\n$stack'; |
| } |
| } else { |
| assert(entry is fs.Link); |
| } |
| } else { |
| _excluded.add(entry); |
| } |
| } |
| for (final _RepositoryDirectory child in virtualSubdirectories) { |
| _subdirectories.add(child); |
| _childrenByName[child.name] = child; |
| } |
| } |
| |
| // Override this to add additional child directories that do not represent a |
| // direct child of this directory's filesystem node. |
| List<_RepositoryDirectory> get virtualSubdirectories => <_RepositoryDirectory>[]; |
| |
| bool shouldRecurse(fs.IoNode entry) { |
| if (entry is fs.File) { |
| if (skippedCommonFiles.contains(entry.name)) { |
| return false; |
| } |
| if (skippedCommonExtensions.contains(path.extension(entry.name))) { |
| return false; |
| } |
| } else if (entry is fs.Directory) { |
| if (skippedCommonDirectories.contains(entry.name)) { |
| return false; |
| } |
| } else { |
| throw 'unexpected entry type ${entry.runtimeType}: ${entry.fullName}'; |
| } |
| final String target = path.relative(entry.fullName, from: rootDirectory.fullName); |
| if (skippedPaths.contains(target)) { |
| return false; |
| } |
| for (final Pattern pattern in skippedFilePatterns) { |
| if (target.contains(pattern)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| _RepositoryDirectory createSubdirectory(fs.Directory entry) { |
| if (entry.name == 'third_party') { |
| return _RepositoryGenericThirdPartyDirectory(this, entry); |
| } |
| return _RepositoryDirectory(this, entry); |
| } |
| |
| // the bit at the beginning excludes files like "license.py". |
| static final RegExp _licenseNamePattern = RegExp(r'^(?!.*\.py$)(?!.*(?:no|update)-copyright)(?!.*mh-bsd-gcc).*\b_*(?:license(?!\.html)|copying|copyright|notice|l?gpl|GPLv2|bsd|mit|mpl?|ftl|Apache)_*\b', caseSensitive: false); |
| |
| static const Map<String, _Constructor> _specialCaseFiles = <String, _Constructor>{ |
| '/flutter/third_party/boringssl/src/LICENSE': _RepositoryOpenSSLLicenseFile.new, |
| '/flutter/third_party/dart/LICENSE': _RepositoryDartLicenseFile.new, |
| '/flutter/third_party/freetype2/LICENSE.TXT': _RepositoryFreetypeLicenseFile.new, |
| '/flutter/third_party/icu/LICENSE': _RepositoryIcuLicenseFile.new, |
| '/flutter/third_party/inja/third_party/include/nlohmann/json.hpp': _RepositoryInjaJsonFile.new, |
| '/flutter/third_party/libcxx/LICENSE.TXT': _RepositoryCxxStlDualLicenseFile.new, |
| '/flutter/third_party/libcxxabi/LICENSE.TXT': _RepositoryCxxStlDualLicenseFile.new, |
| '/flutter/third_party/libjpeg-turbo/src/LICENSE': _RepositoryLibJpegTurboLicenseFile.new, |
| '/flutter/third_party/libjpeg-turbo/src/README.ijg': _RepositoryReadmeIjgFile.new, |
| '/flutter/third_party/libpng/LICENSE': _RepositoryLibPngLicenseFile.new, |
| '/flutter/third_party/rapidjson/LICENSE': _RepositoryOpaqueLicenseFile.new, |
| '/flutter/third_party/rapidjson/license.txt': _RepositoryOpaqueLicenseFile.new, |
| '/flutter/third_party/vulkan-deps/vulkan-validation-layers/src/LICENSE.txt': _RepositoryVulkanApacheLicenseFile.new, |
| '/fuchsia/sdk/linux/LICENSE.vulkan': _RepositoryFuchsiaSdkLinuxLicenseFile.new, |
| '/fuchsia/sdk/mac/LICENSE.vulkan': _RepositoryFuchsiaSdkLinuxLicenseFile.new, |
| '/third_party/khronos/LICENSE': _RepositoryKhronosLicenseFile.new, |
| }; |
| |
| _RepositoryFile createFile(fs.IoNode entry) { |
| final String key = '/${path.relative(entry.fullName, from: rootDirectory.fullName)}'; |
| final _Constructor? constructor = _specialCaseFiles[key]; |
| if (entry is fs.TextFile) { |
| if (constructor != null) { |
| return constructor(this, entry); |
| } |
| if (entry.name.contains(_licenseNamePattern)) { |
| return _RepositoryGeneralSingleLicenseFile(this, entry); |
| } |
| return _RepositorySourceFile(this, entry); |
| } |
| return _RepositoryBinaryFile(this, entry as fs.File); |
| } |
| |
| int get count => _files.length + _subdirectories.fold<int>(0, (int count, _RepositoryDirectory child) => count + child.count); |
| |
| bool _canGoUp() { |
| assert(parent != null || isLicenseRoot); |
| return isLicenseRootException || (!isLicenseRoot && !parent!.subdirectoriesAreLicenseRoots); |
| } |
| |
| @override |
| List<License> nearestLicensesFor(String name) { |
| if (_licenses.isEmpty) { |
| if (_canGoUp()) { |
| return parent!.nearestLicensesFor('${io.name}/$name'); |
| } |
| return const <License>[]; |
| } |
| return _licenses.expand((_RepositoryLicenseFile license) { |
| return license.licensesFor(name); |
| }).toList(); |
| } |
| |
| final Map<LicenseType, License?> _nearestLicenseOfTypeCache = <LicenseType, License?>{}; |
| |
| /// Searches the current directory, all parent directories up to the license |
| /// root, and all their descendants, for a license of the specified type. |
| @override |
| License? nearestLicenseOfType(LicenseType type) { |
| return _nearestLicenseOfTypeCache.putIfAbsent(type, () { |
| final License? result = _localLicenseWithType(type); |
| if (result != null) { |
| return result; |
| } |
| if (_canGoUp()) { |
| return parent!.nearestLicenseOfType(type); |
| } |
| return _fullWalkDownForLicenseWithType(type); |
| }); |
| } |
| |
| /// Searches the current directory for licenses of the specified type. |
| License? _localLicenseWithType(LicenseType type) { |
| final List<License> licenses = _licenses.expand((_RepositoryLicenseFile license) { |
| final License? result = license.licenseOfType(type); |
| if (result != null) { |
| return <License>[result]; |
| } |
| return const <License>[]; |
| }).toList(); |
| if (licenses.length > 1) { |
| print('unexpectedly found multiple matching licenses in $name of type $type'); |
| return null; |
| } |
| if (licenses.isNotEmpty) { |
| return licenses.single; |
| } |
| return null; |
| } |
| |
| /// Searches all subdirectories (depth-first) for a license of the specified type. |
| License? _fullWalkDownForLicenseWithType(LicenseType type) { |
| for (final _RepositoryDirectory directory in _subdirectories) { |
| if (directory._canGoUp()) { // avoid crawling into other license scopes |
| final License? result = directory._localLicenseWithType(type) ?? directory._fullWalkDownForLicenseWithType(type); |
| if (result != null) { |
| return result; |
| } |
| } |
| } |
| return null; |
| } |
| |
| final Map<String, License?> _nearestLicenseWithNameCache = <String, License?>{}; |
| |
| @override |
| License? nearestLicenseWithName(String name, {String? authors}) { |
| assert(!name.contains('\x00')); |
| assert(authors == null || !authors.contains('\x00')); |
| assert(authors == null || authors != '${null}'); |
| final License? result = _nearestLicenseWithNameCache.putIfAbsent('$name\x00$authors', () { |
| final License? result = _localLicenseWithName(name, authors: authors); |
| if (result != null) { |
| return result; |
| } |
| if (_canGoUp()) { |
| return parent!.nearestLicenseWithName(name, authors: authors); |
| } |
| return _fullWalkDownForLicenseWithName(name, authors: authors) |
| ?? (authors != null ? parent?._fullWalkUpForLicenseWithName(name, authors: authors) : null); |
| }); |
| return result; |
| } |
| |
| License? _localLicenseWithName(String name, { String? authors }) { |
| final _RepositoryEntry? entry = _childrenByName[name]; |
| License? license; |
| if (entry is _RepositoryLicensedFile) { |
| license = entry.extractInternalLicense(); |
| } else if (entry is _RepositoryLicenseFile) { |
| license = entry.defaultLicense; |
| } else if (entry != null) { |
| if (authors == null) { |
| throw 'found "$name" in $this but it was a ${entry.runtimeType}'; |
| } |
| } |
| if (license != null && authors != null && authors != license.authors) { |
| license = null; |
| } |
| return license; |
| } |
| |
| License? _fullWalkUpForLicenseWithName(String name, { required String authors }) { |
| // When looking for a license specific to certain authors, we want to walk |
| // to the top of the local license root, then from there check all the |
| // ancestors and all the descendants. |
| // |
| // We check even the ancestors (on the other side of license root |
| // boundaries) for this because when we know which authors we're looking |
| // for, it's reasonable to look all the way up the tree (e.g. the Flutter |
| // Authors license is at the root, and is sometimes mentioned in various |
| // files deep inside third party directories). |
| return _localLicenseWithName(name, authors: authors) ?? |
| parent?._fullWalkUpForLicenseWithName(name, authors: authors); |
| } |
| |
| License? _fullWalkDownForLicenseWithName(String name, { String? authors }) { |
| for (final _RepositoryDirectory directory in _subdirectories) { |
| if (directory._canGoUp()) { // avoid crawling into other license scopes |
| final License? result = directory._localLicenseWithName(name, authors: authors) |
| ?? directory._fullWalkDownForLicenseWithName(name, authors: authors); |
| if (result != null) { |
| return result; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /// Unless isLicenseRootException is true, we should not walk up the tree from |
| /// here looking for licenses. |
| bool get isLicenseRoot => parent == null; |
| |
| /// Unless isLicenseRootException is true on a child, the child should not |
| /// walk up the tree to here looking for licenses. |
| bool get subdirectoriesAreLicenseRoots => false; |
| |
| @override |
| String get libraryName { |
| if (isLicenseRoot || parent!.subdirectoriesAreLicenseRoots) { |
| return name; |
| } |
| if (parent!.parent == null) { |
| throw '$this is not a license root'; |
| } |
| return parent!.libraryName; |
| } |
| |
| /// Overrides isLicenseRoot and parent.subdirectoriesAreLicenseRoots for cases |
| /// where a directory contains license roots instead of being one. This |
| /// allows, for example, the expat third_party directory to contain a |
| /// subdirectory with expat while itself containing a BUILD file that points |
| /// to the LICENSE in the root of the repo. |
| bool get isLicenseRootException => false; |
| |
| _RepositoryEntry getChildByName(String name) { |
| assert(_childrenByName.containsKey(name), 'missing $name in ${io.fullName}'); |
| return _childrenByName[name]!; |
| } |
| |
| Iterable<Assignment> assignLicenses(_Progress progress) { |
| final List<Assignment> result = <Assignment>[]; |
| // Report licenses for files in this directory |
| for (final _RepositoryLicensedFile file in _files) { |
| try { |
| progress.label = '$file'; |
| final Iterable<Assignment> licenses = file.assignLicenses(); |
| assert(licenses.isNotEmpty); |
| result.addAll(licenses); |
| progress.advance(success: true); |
| } catch (e, stack) { |
| system.stderr.writeln('\nerror searching for copyright in: ${file.io}\n$e'); |
| if (e is! String) { |
| system.stderr.writeln(stack); |
| } |
| system.stderr.writeln('\n'); |
| progress.advance(success: false); |
| } |
| } |
| // Recurse to subdirectories |
| for (final _RepositoryDirectory directory in _subdirectories) { |
| result.addAll(directory.assignLicenses(progress)); |
| } |
| return result; |
| } |
| |
| int get fileCount { |
| int result = _files.length; |
| for (final _RepositoryDirectory directory in _subdirectories) { |
| result += directory.fileCount; |
| } |
| return result; |
| } |
| |
| Iterable<_RepositoryLicensedFile> get _signatureFiles sync* { |
| for (final _RepositoryLicensedFile file in _files) { |
| yield file; |
| } |
| for (final _RepositoryDirectory directory in _subdirectories) { |
| if (directory.includeInSignature) { |
| yield* directory._signatureFiles; |
| } |
| } |
| } |
| |
| Stream<List<int>> _signatureStream(List<_RepositoryLicensedFile> files) async* { |
| for (final _RepositoryLicensedFile file in files) { |
| yield file.io.fullName.codeUnits; |
| yield file.ioFile.readBytes()!; |
| } |
| } |
| |
| /// Compute a signature representing a hash of all the licensed files within |
| /// this directory tree. |
| Future<String> get signature async { |
| final List<_RepositoryLicensedFile> allFiles = _signatureFiles.toList(); |
| allFiles.sort((_RepositoryLicensedFile a, _RepositoryLicensedFile b) => |
| a.io.fullName.compareTo(b.io.fullName)); |
| final crypto.Digest digest = await crypto.md5.bind(_signatureStream(allFiles)).single; |
| return digest.bytes.map((int e) => e.toRadixString(16).padLeft(2, '0')).join(); |
| } |
| |
| /// True if this directory's contents should be included when computing the signature. |
| bool get includeInSignature => true; |
| |
| @override |
| String get officialSourceLocation { |
| throw 'license requested source location for directory that should not need it'; |
| } |
| } |
| |
| class _RepositoryReachOutFile extends _RepositoryLicensedFile { |
| _RepositoryReachOutFile(super.parent, super.io, this.offset); |
| |
| final int offset; |
| |
| @override |
| Iterable<Assignment> assignLicenses() { |
| _RepositoryDirectory? directory = parent; |
| int index = offset; |
| while (index > 1) { |
| if (directory == null) { |
| break; |
| } |
| directory = directory.parent; |
| index -= 1; |
| } |
| return directory!.nearestLicensesFor(name).map((License license) => license.assignLicenses(io.fullName, parent!)); |
| } |
| } |
| |
| class _RepositoryReachOutDirectory extends _RepositoryDirectory { |
| _RepositoryReachOutDirectory(_RepositoryDirectory super.parent, super.io, this.reachOutFilenames, this.offset); |
| |
| final Set<String> reachOutFilenames; |
| final int offset; |
| |
| @override |
| _RepositoryFile createFile(fs.IoNode entry) { |
| if (reachOutFilenames.contains(entry.name)) { |
| return _RepositoryReachOutFile(this, entry as fs.File, offset); |
| } |
| return super.createFile(entry); |
| } |
| } |
| |
| class _RepositoryInjaJsonFile extends _RepositorySourceFile { |
| _RepositoryInjaJsonFile(super.parent, super.io); |
| @override |
| License extractInternalLicense() { |
| throw '$this does not have a clearly extractable license'; |
| } |
| |
| static final RegExp _pattern = RegExp( |
| r'^(.*?)' // 1 |
| r'Licensed under the MIT License <http://opensource\.org/licenses/MIT>\.\n' |
| r'SPDX-License-Identifier: MIT\n' |
| r'(Copyright \(c\) 2013-2019 Niels Lohmann <http://nlohmann\.me>\.)\n' // 2 |
| r'\n' |
| r'(Permission is hereby granted, free of charge, to any person obtaining a copy\n' // 3 |
| r'of this software and associated documentation files \(the "Software"\), to deal\n' |
| r'in the Software without restriction, including without limitation the rights\n' |
| r'to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n' |
| r'copies of the Software, and to permit persons to whom the Software is\n' |
| r'furnished to do so, subject to the following conditions:\n' |
| r'\n' |
| r'The above copyright notice and this permission notice shall be included in all\n' |
| r'copies or substantial portions of the Software\.\n' |
| r'\n' |
| r'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n' |
| r'IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n' |
| r'FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE\n' |
| r'AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n' |
| r'LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n' |
| r'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n' |
| r'SOFTWARE\.)\n' |
| r'(.*?)' // 4 |
| r' \* Created by Evan Nemerson <evan@nemerson\.com>\n' |
| r' \*\n' |
| r' \* To the extent possible under law, the author\(s\) have dedicated all\n' |
| r' \* copyright and related and neighboring rights to this software to\n' |
| r' \* the public domain worldwide\. This software is distributed without\n' |
| r' \* any warranty\.\n' |
| r' \*\n' |
| r' \* For details, see <http://creativecommons\.org/publicdomain/zero/1\.0/>\.\n' |
| r' \* SPDX-License-Identifier: CC0-1\.0\n' |
| r'(.*?)' // 5 |
| r'The code is distributed under the MIT license, (Copyright \(c\) 2009 Florian Loitsch\.)\n' // 6 |
| r'(.*?)' // 7 |
| r' @copyright (Copyright \(c\) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann\.de>)\n' // 8 |
| r'(.*?)' // 9 |
| r' `copyright` \| The copyright line for the library as string\.\n' |
| r'(.*?)' // 10 |
| r' result\["copyright"\] = "\(C\) 2013-2020 Niels Lohmann";\n' |
| r'(.*)$', // 11 |
| dotAll: true, // isMultiLine is false, so ^ and $ only match start and end. |
| ); |
| |
| @override |
| Iterable<Assignment> assignLicenses() { |
| if (_internalLicenses == null) { |
| final Match? match = _pattern.matchAsPrefix(_contents); |
| if (match == null) { |
| throw '${io.fullName} has changed contents.'; |
| } |
| final String license = match.group(3)!; |
| _internalLicenses = match.groups(const <int>[ 2, 6, 8 ]).map<License>((String? copyright) { |
| assert(copyright!.contains('Copyright')); |
| return License.fromCopyrightAndLicense(copyright!, license, LicenseType.mit, origin: io.fullName); |
| }).toList(); |
| assert(!match.groups(const <int>[ 1, 4, 5, 7, 9, 10, 11]).any((String? text) => text!.contains(copyrightMentionPattern))); |
| } |
| return _internalLicenses!.map((License license) => license.assignLicenses(io.fullName, parent!)); |
| } |
| } |
| |
| /// The `src/` directory created by gclient sync (a.k.a. buildroot). |
| /// |
| /// This directory is the parent of the flutter/engine repository. The license |
| /// crawler begins its search starting from this directory and recurses down |
| /// from it. |
| /// |
| /// This is not the root of the flutter/engine repository. |
| /// [_RepositoryFlutterDirectory] represents that. |
| class _EngineSrcDirectory extends _RepositoryDirectory { |
| _EngineSrcDirectory(fs.Directory io) : super(null, io); |
| |
| @override |
| String get libraryName { |
| throw 'Package failed to determine library name.'; |
| } |
| |
| @override |
| bool get isLicenseRoot => true; |
| |
| @override |
| bool get subdirectoriesAreLicenseRoots => false; |
| |
| @override |
| bool shouldRecurse(fs.IoNode entry) { |
| return entry.name != 'third_party' // all third_party components have been moved to flutter/third_party |
| && super.shouldRecurse(entry); |
| } |
| |
| @override |
| _RepositoryDirectory createSubdirectory(fs.Directory entry) { |
| if (entry.name == 'flutter') { |
| return _RepositoryFlutterDirectory(this, entry); |
| } |
| if (entry.name == 'gpu') { |
| return _RepositoryGpuShimDirectory(this, entry); |
| } |
| if (entry.name == 'fuchsia') { |
| return _RepositoryFuchsiaDirectory(this, entry); |
| } |
| return super.createSubdirectory(entry); |
| } |
| |
| @override |
| List<_RepositoryDirectory> get virtualSubdirectories { |
| // Dart and Skia are updated more frequently than other third party |
| // libraries and is therefore represented as a separate top-level component. |
| final fs.Directory flutterNode = findChildDirectory(ioDirectory, 'flutter')!; |
| final fs.Directory flutterThirdPartyNode = findChildDirectory(flutterNode, 'third_party')!; |
| final fs.Directory skiaNode = findChildDirectory(flutterThirdPartyNode, 'skia')!; |
| final fs.Directory dartNode = findChildDirectory(flutterThirdPartyNode, 'dart')!; |
| |
| return <_RepositoryDirectory>[ |
| _RepositoryDartDirectory(this, dartNode), |
| _RepositorySkiaDirectory(this, skiaNode), |
| ]; |
| } |
| } |
| |
| class _RepositoryGenericThirdPartyDirectory extends _RepositoryDirectory { |
| _RepositoryGenericThirdPartyDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| @override |
| bool get subdirectoriesAreLicenseRoots => true; |
| } |
| |
| class _RepositoryExpatDirectory extends _RepositoryDirectory { |
| _RepositoryExpatDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| @override |
| bool get isLicenseRootException => true; |
| |
| @override |
| bool get subdirectoriesAreLicenseRoots => true; |
| } |
| |
| class _RepositoryFreetypeDirectory extends _RepositoryDirectory { |
| _RepositoryFreetypeDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| @override |
| List<License> nearestLicensesFor(String name) { |
| final List<License> result = super.nearestLicensesFor(name); |
| if (result.isEmpty) { |
| final License? license = nearestLicenseWithName('LICENSE.TXT'); |
| assert(license != null); |
| if (license != null) { |
| return <License>[license]; |
| } |
| } |
| return result; |
| } |
| |
| @override |
| License? nearestLicenseOfType(LicenseType type) { |
| if (type == LicenseType.freetype) { |
| final License? result = nearestLicenseWithName('FTL.TXT'); |
| assert(result != null); |
| return result; |
| } |
| return super.nearestLicenseOfType(type); |
| } |
| |
| @override |
| _RepositoryDirectory createSubdirectory(fs.Directory entry) { |
| if (entry.name == 'src') { |
| return _RepositoryFreetypeSrcDirectory(this, entry); |
| } |
| return super.createSubdirectory(entry); |
| } |
| } |
| |
| class _RepositoryFreetypeSrcDirectory extends _RepositoryDirectory { |
| _RepositoryFreetypeSrcDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| @override |
| _RepositoryDirectory createSubdirectory(fs.Directory entry) { |
| if (entry.name == 'gzip') { |
| return _RepositoryFreetypeSrcGZipDirectory(this, entry); |
| } |
| return super.createSubdirectory(entry); |
| } |
| } |
| |
| class _RepositoryFreetypeSrcGZipDirectory extends _RepositoryDirectory { |
| _RepositoryFreetypeSrcGZipDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| // advice was to make this directory's inffixed.h file (which has no license) |
| // use the license in zlib.h. |
| |
| @override |
| List<License> nearestLicensesFor(String name) { |
| final License? zlib = nearestLicenseWithName('zlib.h'); |
| assert(zlib != null); |
| if (zlib != null) { |
| return <License>[zlib]; |
| } |
| return super.nearestLicensesFor(name); |
| } |
| |
| @override |
| License? nearestLicenseOfType(LicenseType type) { |
| if (type == LicenseType.zlib) { |
| final License? result = nearestLicenseWithName('zlib.h'); |
| assert(result != null); |
| return result; |
| } |
| return super.nearestLicenseOfType(type); |
| } |
| } |
| |
| class _RepositoryFallbackRootCertificatesDirectory extends _RepositoryDirectory { |
| _RepositoryFallbackRootCertificatesDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| @override |
| String get officialSourceLocation { |
| final system.ProcessResult result = system.Process.runSync('git', <String>['rev-parse', 'HEAD'], workingDirectory: '$this'); |
| if (result.exitCode != 0) { |
| throw 'Failed to run "git rev-parse HEAD"; got non-zero exit code ${result.exitCode}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}'; |
| } |
| |
| final String rev = result.stdout as String; |
| return 'https://dart.googlesource.com/sdk/+/$rev/third_party/fallback_root_certificates/'; |
| } |
| } |
| |
| class _RepositoryZLibDirectory extends _RepositoryDirectory { |
| _RepositoryZLibDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| // Some files in this directory refer to "MiniZip_info.txt". |
| // As best we can tell, that refers to a file that itself includes the |
| // exact same license text as in LICENSE. |
| |
| @override |
| License? nearestLicenseWithName(String name, { String? authors }) { |
| if (name == 'MiniZip_info.txt') { |
| return super.nearestLicenseWithName('LICENSE', authors: authors)!; |
| } |
| return super.nearestLicenseWithName(name, authors: authors); |
| } |
| |
| @override |
| License? nearestLicenseOfType(LicenseType type) { |
| if (type == LicenseType.zlib) { |
| return nearestLicenseWithName('LICENSE')!; |
| } |
| return super.nearestLicenseOfType(type); |
| } |
| } |
| |
| class _RepositoryDartDirectory extends _RepositoryDirectory { |
| _RepositoryDartDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| @override |
| bool get isLicenseRoot => true; |
| |
| @override |
| _RepositoryDirectory createSubdirectory(fs.Directory entry) { |
| if (entry.name == 'third_party') { |
| return _RepositoryDartThirdPartyDirectory(this, entry); |
| } |
| return super.createSubdirectory(entry); |
| } |
| } |
| |
| class _RepositoryDartThirdPartyDirectory extends _RepositoryGenericThirdPartyDirectory { |
| _RepositoryDartThirdPartyDirectory(super.parent, super.io); |
| |
| @override |
| _RepositoryDirectory createSubdirectory(fs.Directory entry) { |
| if (entry.name == 'fallback_root_certificates') { |
| return _RepositoryFallbackRootCertificatesDirectory(this, entry); |
| } |
| return super.createSubdirectory(entry); |
| } |
| } |
| |
| class _RepositorySkiaDirectory extends _RepositoryDirectory { |
| _RepositorySkiaDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| @override |
| bool get isLicenseRoot => true; |
| |
| @override |
| _RepositoryDirectory createSubdirectory(fs.Directory entry) { |
| if (entry.name == 'third_party') { |
| return _RepositorySkiaThirdPartyDirectory(this, entry); |
| } |
| return super.createSubdirectory(entry); |
| } |
| } |
| |
| class _RepositorySkiaThirdPartyDirectory extends _RepositoryGenericThirdPartyDirectory { |
| _RepositorySkiaThirdPartyDirectory(super.parent, super.io); |
| |
| @override |
| _RepositoryDirectory createSubdirectory(fs.Directory entry) { |
| if (entry.name == 'ktx') { |
| return _RepositoryReachOutDirectory(this, entry, const <String>{'ktx.h', 'ktx.cpp'}, 2); |
| } |
| if (entry.name == 'libmicrohttpd') { |
| return _RepositoryReachOutDirectory(this, entry, const <String>{'MHD_config.h'}, 2); |
| } |
| if (entry.name == 'libwebp') { |
| return _RepositorySkiaLibWebPDirectory(this, entry); |
| } |
| if (entry.name == 'libsdl') { |
| return _RepositorySkiaLibSdlDirectory(this, entry); |
| } |
| return super.createSubdirectory(entry); |
| } |
| } |
| |
| class _RepositorySkiaLibWebPDirectory extends _RepositoryDirectory { |
| _RepositorySkiaLibWebPDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| @override |
| _RepositoryDirectory createSubdirectory(fs.Directory entry) { |
| if (entry.name == 'webp') { |
| return _RepositoryReachOutDirectory(this, entry, const <String>{'config.h'}, 3); |
| } |
| return super.createSubdirectory(entry); |
| } |
| } |
| |
| class _RepositorySkiaLibSdlDirectory extends _RepositoryDirectory { |
| _RepositorySkiaLibSdlDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| @override |
| bool get isLicenseRootException => true; |
| } |
| |
| class _RepositoryBoringSSLDirectory extends _RepositoryDirectory { |
| _RepositoryBoringSSLDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| // This directory contains boringssl itself in the 'src' subdirectory, |
| // and the rest of files are code generated from tools in that directory. |
| // We redirect any license queries to the src/LICENSE file. |
| |
| _RepositoryDirectory get src => getChildByName('src') as _RepositoryDirectory; |
| |
| @override |
| List<License> nearestLicensesFor(String name) { |
| final List<License> result = super.nearestLicensesFor(name); |
| if (result.isEmpty) { |
| return (src.getChildByName('LICENSE') as _RepositoryLicenseFile).licenses; |
| } |
| return result; |
| } |
| |
| @override |
| License? nearestLicenseOfType(LicenseType type) { |
| assert(!src._canGoUp()); |
| return super.nearestLicenseOfType(type) ?? src.nearestLicenseOfType(type); |
| } |
| |
| @override |
| License? nearestLicenseWithName(String name, {String? authors}) { |
| assert(!src._canGoUp()); |
| final License? result = super.nearestLicenseWithName(name, authors: authors) ?? src.nearestLicenseWithName(name, authors: authors); |
| return result; |
| } |
| |
| @override |
| _RepositoryDirectory createSubdirectory(fs.Directory entry) { |
| if (entry.name == 'src') { |
| // This is the actual BoringSSL library. |
| return _RepositoryBoringSSLSourceDirectory(this, entry); |
| } |
| return super.createSubdirectory(entry); |
| } |
| } |
| |
| class _RepositoryBoringSSLSourceDirectory extends _RepositoryDirectory { |
| _RepositoryBoringSSLSourceDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| // This directory is called "src" because of the way we import boringssl. The |
| // parent is called "boringssl". Since we are a licenseRoot, the default |
| // "libraryName" implementation would use "src", so we force it to go up here. |
| @override |
| String get libraryName => parent!.libraryName; |
| |
| @override |
| bool get isLicenseRoot => true; |
| } |
| |
| class _RepositoryFlutterDirectory extends _RepositoryDirectory { |
| _RepositoryFlutterDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| @override |
| String get libraryName => 'engine'; |
| |
| @override |
| bool get isLicenseRoot => true; |
| |
| @override |
| _RepositoryDirectory createSubdirectory(fs.Directory entry) { |
| if (entry.name == 'third_party') { |
| return _RepositoryFlutterThirdPartyDirectory(this, entry); |
| } |
| return _RepositoryDirectory(this, entry); |
| } |
| } |
| |
| class _RepositoryFlutterThirdPartyDirectory extends _RepositoryGenericThirdPartyDirectory { |
| _RepositoryFlutterThirdPartyDirectory(super.parent, super.io); |
| |
| @override |
| bool shouldRecurse(fs.IoNode entry) { |
| return entry.name != 'skia' // handled as a virtual directory of the root |
| && entry.name != 'dart' // handled as a virtual directory of the root |
| && super.shouldRecurse(entry); |
| } |
| |
| @override |
| _RepositoryDirectory createSubdirectory(fs.Directory entry) { |
| if (entry.name == 'boringssl') { |
| return _RepositoryBoringSSLDirectory(this, entry); |
| } |
| if (entry.name == 'expat') { |
| return _RepositoryExpatDirectory(this, entry); |
| } |
| if (entry.name == 'freetype2') { |
| return _RepositoryFreetypeDirectory(this, entry); |
| } |
| if (entry.name == 'vulkan-deps') { |
| return _RepositoryGenericThirdPartyDirectory(this, entry); |
| } |
| if (entry.name == 'zlib') { |
| return _RepositoryZLibDirectory(this, entry); |
| } |
| return super.createSubdirectory(entry); |
| } |
| } |
| |
| class _RepositoryFuchsiaDirectory extends _RepositoryDirectory { |
| _RepositoryFuchsiaDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| @override |
| String get libraryName => 'fuchsia_sdk'; |
| |
| @override |
| bool get isLicenseRoot => true; |
| } |
| |
| class _RepositoryGpuShimDirectory extends _RepositoryDirectory { |
| _RepositoryGpuShimDirectory(_RepositoryDirectory super.parent, super.io); |
| |
| @override |
| String get libraryName => 'engine'; |
| } |
| |
| |
| /// The license tool directory. |
| /// |
| /// This is a special-case root node that is not used for license aggregation, |
| /// but simply to compute a signature for the license tool itself. When this |
| /// signature changes, we force re-run license collection for all components in |
| /// order to verify the tool itself still produces the same output. |
| class _RepositoryFlutterLicenseToolDirectory extends _RepositoryDirectory { |
| _RepositoryFlutterLicenseToolDirectory(fs.Directory io) : super(null, io); |
| |
| @override |
| bool shouldRecurse(fs.IoNode entry) { |
| return entry.name != 'data' && super.shouldRecurse(entry); |
| } |
| } |
| |
| |
| // BOOTSTRAPPING LOGIC |
| |
| fs.Directory? findChildDirectory(fs.Directory parent, String name) { |
| return parent.walk.firstWhereOrNull( // from IterableExtension in package:collection |
| (fs.IoNode child) => child.name == name, |
| ) as fs.Directory?; |
| } |
| |
| class _Progress { |
| _Progress(this.max, {this.quiet = false}) : millisecondsBetweenUpdates = quiet ? 10000 : 0 { |
| // This may happen when a git client contains left-over empty component |
| // directories after DEPS file changes. |
| if (max <= 0) { |
| throw ArgumentError('Progress.max must be > 0 but was: $max'); |
| } |
| } |
| |
| final int max; |
| final bool quiet; |
| final int millisecondsBetweenUpdates; |
| int get withLicense => _withLicense; |
| int _withLicense = 0; |
| int get withoutLicense => _withoutLicense; |
| int _withoutLicense = 0; |
| String get label => _label; |
| String _label = ''; |
| int _lastLength = 0; |
| set label(String value) { |
| if (value.length > 60) { |
| value = '.../${value.substring(math.max(0, value.lastIndexOf('/', value.length - 45) + 1))}'; |
| } |
| if (_label != value) { |
| _label = value; |
| update(); |
| } |
| } |
| |
| void advance({required bool success}) { |
| if (success) { |
| _withLicense += 1; |
| } else { |
| _withoutLicense += 1; |
| } |
| update(); |
| } |
| |
| Stopwatch? _lastUpdate; |
| void update({bool flush = false}) { |
| if (_lastUpdate == null || _lastUpdate!.elapsedMilliseconds >= millisecondsBetweenUpdates || flush) { |
| _lastUpdate ??= Stopwatch(); |
| if (!quiet) { |
| final String line = toString(); |
| system.stderr.write('\r$line'); |
| if (_lastLength > line.length) { |
| system.stderr.write(' ' * (_lastLength - line.length)); |
| } |
| _lastLength = line.length; |
| } |
| _lastUpdate!.reset(); |
| _lastUpdate!.start(); |
| } |
| } |
| |
| void flush() => update(flush: true); |
| bool get hadErrors => _withoutLicense > 0; |
| @override |
| String toString() { |
| final int percent = (100.0 * (_withLicense + _withoutLicense) / max).round(); |
| return '${(_withLicense + _withoutLicense).toString().padLeft(10)} of ${max.toString().padRight(6)} ' |
| '${'â–ˆ' * (percent ~/ 10)}${'â–‘' * (10 - (percent ~/ 10))} $percent% ' |
| '${ _withoutLicense > 0 ? "($_withoutLicense missing licenses) " : ""}' |
| '$label'; |
| } |
| } |
| |
| final RegExp _signaturePattern = RegExp(r'^Signature: (\w+)$', multiLine: true, expectNoMatch: true); |
| |
| /// Reads the signature from a golden file. |
| String? _readSignature(String goldenPath) { |
| try { |
| final system.File goldenFile = system.File(goldenPath); |
| if (!goldenFile.existsSync()) { |
| system.stderr.writeln(' Could not find signature file ($goldenPath).'); |
| return null; |
| } |
| final String goldenSignature = goldenFile.readAsStringSync(); |
| final Match? goldenMatch = _signaturePattern.matchAsPrefix(goldenSignature); |
| if (goldenMatch != null) { |
| return goldenMatch.group(1); |
| } |
| system.stderr.writeln(' Signature file ($goldenPath) did not match expected pattern.'); |
| } on system.FileSystemException { |
| system.stderr.writeln(' Failed to read signature file ($goldenPath).'); |
| return null; |
| } |
| return null; |
| } |
| |
| /// Writes a signature to an [system.IOSink] in the expected format. |
| void _writeSignature(String signature, system.IOSink sink) { |
| sink.writeln('Signature: $signature\n'); |
| } |
| |
| // Checks for changes to the license tool itself. |
| // |
| // Returns true if changes are detected. |
| Future<bool> _computeLicenseToolChanges(_RepositoryDirectory root, { required String goldenSignaturePath, required String outputSignaturePath }) async { |
| final fs.Directory flutterNode = findChildDirectory(root.ioDirectory, 'flutter')!; |
| final fs.Directory toolsNode = findChildDirectory(flutterNode, 'tools')!; |
| final fs.Directory licenseNode = findChildDirectory(toolsNode, 'licenses')!; |
| final _RepositoryDirectory licenseToolDirectory = _RepositoryFlutterLicenseToolDirectory(licenseNode); |
| final String toolSignature = await licenseToolDirectory.signature; |
| final system.IOSink sink = system.File(outputSignaturePath).openWrite(); |
| _writeSignature(toolSignature, sink); |
| await sink.close(); |
| final String? goldenSignature = _readSignature(goldenSignaturePath); |
| return toolSignature != goldenSignature; |
| } |
| |
| /// Collects licenses for the specified component. |
| /// |
| /// If [writeSignature] is set, the signature is written to the output file. |
| /// If [force] is set, collection is run regardless of whether or not the signature matches. |
| Future<void> _collectLicensesForComponent(_RepositoryDirectory componentRoot, { |
| required String inputGoldenPath, |
| String? outputGoldenPath, |
| required bool writeSignature, |
| required bool force, |
| required bool quiet, |
| }) async { |
| final String signature = await componentRoot.signature; |
| if (writeSignature) { |
| // Check whether the golden file matches the signature of the current contents of this directory. |
| // (We only do this for components where we write the signature, since if there's no signature, |
| // there's no point trying to read it...) |
| final String? goldenSignature = _readSignature(inputGoldenPath); |
| if (!force && goldenSignature == signature) { |
| system.stderr.writeln(' Skipping this component - no change in signature'); |
| return; |
| } |
| } |
| |
| final _Progress progress = _Progress(componentRoot.fileCount, quiet: quiet); |
| |
| final system.File outFile = system.File(outputGoldenPath!); |
| final system.IOSink sink = outFile.openWrite(); |
| if (writeSignature) { |
| _writeSignature(signature, sink); |
| } |
| |
| final List<GroupedLicense> licenses = groupLicenses(componentRoot.assignLicenses(progress)); |
| if (progress.hadErrors) { |
| throw 'Had failures while collecting licenses.'; |
| } |
| progress.label = 'Dumping results...'; |
| progress.flush(); |
| final List<String> output = licenses.map((GroupedLicense license) => license.toStringDebug()).toList(); |
| for (int index = 0; index < output.length; index += 1) { |
| // The strings we look for here are strings which we do not expect to see in |
| // any of the licenses we use. They either represent examples of misparsing |
| // licenses (issues we've previously run into and fixed), or licenses we |
| // know we are trying to avoid (e.g. the GPL, or licenses that only apply to |
| // test content which shouldn't get built at all). |
| // If you find that one of these tests is getting hit, and it's not obvious |
| // to you why the relevant license is a problem, please ask around (e.g. try |
| // asking Hixie). Do not merely remove one of these checks, sometimes the |
| // issues involved are relatively subtle. |
| if (output[index].contains('Version: MPL 1.1/GPL 2.0/LGPL 2.1')) { |
| throw 'Unexpected trilicense block found in:\n${output[index]}'; |
| } |
| if (output[index].contains('The contents of this file are subject to the Mozilla Public License Version')) { |
| throw 'Unexpected MPL block found in:\n${output[index]}'; |
| } |
| if (output[index].contains('You should have received a copy of the GNU')) { |
| throw 'Unexpected GPL block found in:\n${output[index]}'; |
| } |
| if (output[index].contains('Contents of this folder are ported from')) { |
| throw 'Unexpected block found in:\n${output[index]}'; |
| } |
| if (output[index].contains('https://github.com/w3c/web-platform-tests/tree/master/selectors-api')) { |
| throw 'Unexpected W3C content found in:\n${output[index]}'; |
| } |
| if (output[index].contains('http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html')) { |
| throw 'Unexpected W3C copyright found in:\n${output[index]}'; |
| } |
| if (output[index].contains('It is based on commit')) { |
| throw 'Unexpected content found in:\n${output[index]}'; |
| } |
| if (output[index].contains('The original code is covered by the dual-licensing approach described in:')) { |
| throw 'Unexpected old license reference found in:\n${output[index]}'; |
| } |
| if (output[index].contains('must choose')) { |
| throw 'Unexpected indecisiveness found in:\n${output[index]}'; |
| } |
| } |
| sink.writeln(output.join('\n')); |
| sink.writeln('Total license count: ${licenses.length}'); |
| |
| await sink.close(); |
| progress.label = 'Done.'; |
| progress.flush(); |
| system.stderr.writeln(); |
| } |
| |
| // MAIN |
| |
| Future<void> main(List<String> arguments) async { |
| final ArgParser parser = ArgParser() |
| ..addOption('src', help: 'The root of the engine source.') |
| ..addOption('out', help: 'The directory where output is written. (Ignored if used with --release.)') |
| ..addOption('golden', help: 'The directory containing golden results.') |
| ..addFlag('quiet', help: 'If set, the diagnostic output is much less verbose.') |
| ..addFlag('verbose', help: 'If set, print additional information to help with development.') |
| ..addFlag('release', help: 'Print output in the format used for product releases.'); |
| |
| final ArgResults argResults = parser.parse(arguments); |
| final bool quiet = argResults['quiet'] as bool; |
| final bool verbose = argResults['verbose'] as bool; |
| final bool releaseMode = argResults['release'] as bool; |
| if (argResults['src'] == null) { |
| print('Flutter license script: Must provide --src directory'); |
| print(parser.usage); |
| system.exit(1); |
| } |
| if (!releaseMode) { |
| if (argResults['out'] == null || argResults['golden'] == null) { |
| print('Flutter license script: Must provide --out and --golden directories in non-release mode'); |
| print(parser.usage); |
| system.exit(1); |
| } |
| if (!system.FileSystemEntity.isDirectorySync(argResults['golden'] as String)) { |
| print('Flutter license script: Golden directory does not exist'); |
| print(parser.usage); |
| system.exit(1); |
| } |
| final system.Directory out = system.Directory(argResults['out'] as String); |
| if (!out.existsSync()) { |
| out.createSync(recursive: true); |
| } |
| } |
| |
| try { |
| system.stderr.writeln('Finding files...'); |
| final fs.FileSystemDirectory rootDirectory = fs.FileSystemDirectory.fromPath(argResults['src'] as String); |
| final _RepositoryDirectory root = _EngineSrcDirectory(rootDirectory); |
| |
| if (releaseMode) { |
| system.stderr.writeln('Collecting licenses...'); |
| final _Progress progress = _Progress(root.fileCount, quiet: quiet); |
| final List<GroupedLicense> licenses = groupLicenses(root.assignLicenses(progress)); |
| if (progress.hadErrors) { |
| throw 'Had failures while collecting licenses.'; |
| } |
| progress.label = 'Dumping results...'; |
| progress.flush(); |
| final String output = licenses |
| .where((GroupedLicense license) => license.body.isNotEmpty) |
| .map((GroupedLicense license) => license.toStringFormal()) |
| .join('\n${"-" * 80}\n'); |
| print(output); |
| progress.label = 'Done.'; |
| progress.flush(); |
| system.stderr.writeln(); |
| } else { |
| // If changes are detected to the license tool itself, force collection |
| // for all components in order to check we're still generating correct |
| // output. |
| const String toolSignatureFilename = 'tool_signature'; |
| final bool forceRunAll = await _computeLicenseToolChanges( |
| root, |
| goldenSignaturePath: path.join(argResults['golden'] as String, toolSignatureFilename), |
| outputSignaturePath: path.join(argResults['out'] as String, toolSignatureFilename), |
| ); |
| if (forceRunAll) { |
| system.stderr.writeln('Detected changes to license tool. Forcing license collection for all components.'); |
| } |
| final List<String> usedGoldens = <String>[]; |
| bool isFirstComponent = true; |
| for (final _RepositoryDirectory component in root.subdirectories) { |
| system.stderr.writeln('Collecting licenses for ${component.io.name}'); |
| _RepositoryDirectory componentRoot; |
| if (isFirstComponent) { |
| // For the first component, we can use the results of the initial repository crawl. |
| isFirstComponent = false; |
| componentRoot = component; |
| } else { |
| // For other components, we need a clean repository that does not |
| // contain any state left over from previous components. |
| componentRoot = _EngineSrcDirectory(rootDirectory) |
| .subdirectories |
| .firstWhere((_RepositoryDirectory dir) => dir.name == component.name); |
| } |
| final String goldenFileName = 'licenses_${component.io.name}'; |
| await _collectLicensesForComponent( |
| componentRoot, |
| inputGoldenPath: path.join(argResults['golden'] as String, goldenFileName), |
| outputGoldenPath: path.join(argResults['out'] as String, goldenFileName), |
| writeSignature: component.io.name != 'flutter', |
| // Always run the full license check on the flutter tree. The flutter |
| // tree is relatively small and changes frequently in ways that do not |
| // affect the license output, and we don't want to require updates to |
| // the golden signature for those changes. |
| force: forceRunAll || component.io.name == 'flutter', |
| quiet: quiet, |
| ); |
| usedGoldens.add(goldenFileName); |
| } |
| final Set<String> unusedGoldens = system.Directory(argResults['golden'] as String).listSync() |
| .map<String>((system.FileSystemEntity file) => path.basename(file.path)) |
| .where((String name) => name.startsWith('licenses_')) |
| .toSet() |
| ..removeAll(usedGoldens); |
| if (unusedGoldens.isNotEmpty) { |
| system.stderr.writeln('The following golden files in ${argResults['golden']} are unused and need to be deleted:'); |
| unusedGoldens.map((String s) => ' * $s').forEach(system.stderr.writeln); |
| system.exit(1); |
| } |
| // write to disk the list of files we did _not_ cover, so it's easier to catch in diffs |
| final String excluded = (_RepositoryDirectory._excluded.map( |
| (fs.IoNode node) => node.fullName, |
| ).toSet().toList()..sort()).join('\n'); |
| system.File(path.join(argResults['out'] as String, 'excluded_files')).writeAsStringSync( |
| '$excluded\n', |
| ); |
| } |
| } catch (e, stack) { |
| system.stderr.writeln(); |
| system.stderr.writeln('failure: $e\n$stack'); |
| system.stderr.writeln('aborted.'); |
| system.exit(1); |
| } finally { |
| if (verbose) { |
| RegExp.printDiagnostics(); |
| } |
| } |
| } |