• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// See README in this directory for information on how this code is organized.
6
7import 'dart:async';
8import 'dart:collection';
9import 'dart:convert';
10import 'dart:io' as system;
11import 'dart:math' as math;
12
13import 'package:args/args.dart';
14import 'package:crypto/crypto.dart' as crypto;
15import 'package:licenses/patterns.dart';
16import 'package:meta/meta.dart';
17import 'package:path/path.dart' as path;
18
19import 'filesystem.dart' as fs;
20import 'licenses.dart';
21
22
23// REPOSITORY OBJECTS
24
25abstract class _RepositoryEntry implements Comparable<_RepositoryEntry> {
26  _RepositoryEntry(this.parent, this.io);
27  final _RepositoryDirectory parent;
28  final fs.IoNode io;
29  String get name => io.name;
30  String get libraryName;
31
32  @override
33  int compareTo(_RepositoryEntry other) => toString().compareTo(other.toString());
34
35  @override
36  String toString() => io.fullName;
37}
38
39abstract class _RepositoryFile extends _RepositoryEntry {
40  _RepositoryFile(_RepositoryDirectory parent, fs.File io) : super(parent, io);
41
42  Iterable<License> get licenses;
43
44  @override
45  String get libraryName => parent.libraryName;
46
47  @override
48  fs.File get io => super.io;
49}
50
51abstract class _RepositoryLicensedFile extends _RepositoryFile {
52  _RepositoryLicensedFile(_RepositoryDirectory parent, fs.File io) : super(parent, io);
53
54  // file names that we are confident won't be included in the final build product
55  static final RegExp _readmeNamePattern = RegExp(r'\b_*(?:readme|contributing|patents)_*\b', caseSensitive: false);
56  static final RegExp _buildTimePattern = RegExp(r'^(?!.*gen$)(?:CMakeLists\.txt|(?:pkgdata)?Makefile(?:\.inc)?(?:\.am|\.in|)|configure(?:\.ac|\.in)?|config\.(?:sub|guess)|.+\.m4|install-sh|.+\.sh|.+\.bat|.+\.pyc?|.+\.pl|icu-configure|.+\.gypi?|.*\.gni?|.+\.mk|.+\.cmake|.+\.gradle|.+\.yaml|pubspec\.lock|\.packages|vms_make\.com|pom\.xml|\.project|source\.properties)$', caseSensitive: false);
57  static final RegExp _docsPattern = RegExp(r'^(?:INSTALL|NEWS|OWNERS|AUTHORS|ChangeLog(?:\.rst|\.[0-9]+)?|.+\.txt|.+\.md|.+\.log|.+\.css|.+\.1|doxygen\.config|Doxyfile|.+\.spec(?:\.in)?)$', caseSensitive: false);
58  static final RegExp _devPattern = RegExp(r'^(?:codereview\.settings|.+\.~|.+\.~[0-9]+~|\.clang-format|\.gitattributes|\.landmines|\.DS_Store|\.travis\.yml|\.cirrus\.yml)$', caseSensitive: false);
59  static final RegExp _testsPattern = RegExp(r'^(?:tj(?:bench|example)test\.(?:java\.)?in|example\.c)$', caseSensitive: false);
60
61  bool get isIncludedInBuildProducts {
62    return !io.name.contains(_readmeNamePattern)
63        && !io.name.contains(_buildTimePattern)
64        && !io.name.contains(_docsPattern)
65        && !io.name.contains(_devPattern)
66        && !io.name.contains(_testsPattern)
67        && !isShellScript;
68  }
69
70  bool get isShellScript => false;
71}
72
73class _RepositorySourceFile extends _RepositoryLicensedFile {
74  _RepositorySourceFile(_RepositoryDirectory parent, fs.TextFile io) : super(parent, io);
75
76  @override
77  fs.TextFile get io => super.io;
78
79  static final RegExp _hashBangPattern = RegExp(r'^#! *(?:/bin/sh|/bin/bash|/usr/bin/env +(?:python|bash))\b');
80
81  @override
82  bool get isShellScript {
83    return io.readString().startsWith(_hashBangPattern);
84  }
85
86  List<License> _licenses;
87
88  @override
89  Iterable<License> get licenses {
90    if (_licenses != null)
91      return _licenses;
92    String contents;
93    try {
94      contents = io.readString();
95    } on FormatException {
96      print('non-UTF8 data in $io');
97      system.exit(2);
98    }
99    _licenses = determineLicensesFor(contents, name, parent, origin: '$this');
100    if (_licenses == null || _licenses.isEmpty) {
101      _licenses = parent.nearestLicensesFor(name);
102      if (_licenses == null || _licenses.isEmpty)
103        throw 'file has no detectable license and no in-scope default license file';
104    }
105    _licenses.sort();
106    for (License license in licenses)
107      license.markUsed(io.fullName, libraryName);
108    assert(_licenses != null && _licenses.isNotEmpty);
109    return _licenses;
110  }
111}
112
113class _RepositoryBinaryFile extends _RepositoryLicensedFile {
114  _RepositoryBinaryFile(_RepositoryDirectory parent, fs.File io) : super(parent, io);
115
116  List<License> _licenses;
117
118  @override
119  List<License> get licenses {
120    if (_licenses == null) {
121      _licenses = parent.nearestLicensesFor(name);
122      if (_licenses == null || _licenses.isEmpty)
123        throw 'no license file found in scope for ${io.fullName}';
124      for (License license in licenses)
125        license.markUsed(io.fullName, libraryName);
126    }
127    return _licenses;
128  }
129}
130
131
132// LICENSES
133
134abstract class _RepositoryLicenseFile extends _RepositoryFile {
135  _RepositoryLicenseFile(_RepositoryDirectory parent, fs.File io) : super(parent, io);
136
137  List<License> licensesFor(String name);
138  License licenseOfType(LicenseType type);
139  License licenseWithName(String name);
140
141  License get defaultLicense;
142}
143
144abstract class _RepositorySingleLicenseFile extends _RepositoryLicenseFile {
145  _RepositorySingleLicenseFile(_RepositoryDirectory parent, fs.TextFile io, this.license)
146    : super(parent, io);
147
148  final License license;
149
150  @override
151  List<License> licensesFor(String name) {
152    if (license != null)
153      return <License>[license];
154    return null;
155  }
156
157  @override
158  License licenseWithName(String name) {
159    if (this.name == name)
160      return license;
161    return null;
162  }
163
164  @override
165  License get defaultLicense => license;
166
167  @override
168  Iterable<License> get licenses sync* { yield license; }
169}
170
171class _RepositoryGeneralSingleLicenseFile extends _RepositorySingleLicenseFile {
172  _RepositoryGeneralSingleLicenseFile(_RepositoryDirectory parent, fs.TextFile io)
173    : super(parent, io, License.fromBodyAndName(io.readString(), io.name, origin: io.fullName));
174
175  _RepositoryGeneralSingleLicenseFile.fromLicense(_RepositoryDirectory parent, fs.TextFile io, License license)
176    : super(parent, io, license);
177
178  @override
179  License licenseOfType(LicenseType type) {
180    if (type == license.type)
181      return license;
182    return null;
183  }
184}
185
186class _RepositoryApache4DNoticeFile extends _RepositorySingleLicenseFile {
187  _RepositoryApache4DNoticeFile(_RepositoryDirectory parent, fs.TextFile io)
188    : super(parent, io, _parseLicense(io));
189
190  @override
191  License licenseOfType(LicenseType type) => null;
192
193  static final RegExp _pattern = RegExp(
194    r'^(// ------------------------------------------------------------------\n'
195    r'// NOTICE file corresponding to the section 4d of The Apache License,\n'
196    r'// Version 2\.0, in this case for (?:.+)\n'
197    r'// ------------------------------------------------------------------\n)'
198    r'((?:.|\n)+)$',
199    multiLine: false,
200    caseSensitive: false
201  );
202
203  static bool consider(fs.TextFile io) {
204    return io.readString().contains(_pattern);
205  }
206
207  static License _parseLicense(fs.TextFile io) {
208    final Match match = _pattern.allMatches(io.readString()).single;
209    assert(match.groupCount == 2);
210    return License.unique(match.group(2), LicenseType.apacheNotice, origin: io.fullName);
211  }
212}
213
214class _RepositoryLicenseRedirectFile extends _RepositorySingleLicenseFile {
215  _RepositoryLicenseRedirectFile(_RepositoryDirectory parent, fs.TextFile io, License license)
216    : super(parent, io, license);
217
218  @override
219  License licenseOfType(LicenseType type) {
220    if (type == license.type)
221      return license;
222    return null;
223  }
224
225  static _RepositoryLicenseRedirectFile maybeCreateFrom(_RepositoryDirectory parent, fs.TextFile io) {
226    final String contents = io.readString();
227    final License license = interpretAsRedirectLicense(contents, parent, origin: io.fullName);
228    if (license != null)
229      return _RepositoryLicenseRedirectFile(parent, io, license);
230    return null;
231  }
232}
233
234class _RepositoryLicenseFileWithLeader extends _RepositorySingleLicenseFile {
235  _RepositoryLicenseFileWithLeader(_RepositoryDirectory parent, fs.TextFile io, RegExp leader)
236    : super(parent, io, _parseLicense(io, leader));
237
238  @override
239  License licenseOfType(LicenseType type) => null;
240
241  static License _parseLicense(fs.TextFile io, RegExp leader) {
242    final String body = io.readString();
243    final Match match = leader.firstMatch(body);
244    if (match == null)
245      throw 'failed to strip leader from $io\nleader: /$leader/\nbody:\n---\n$body\n---';
246    return License.fromBodyAndName(body.substring(match.end), io.name, origin: io.fullName);
247  }
248}
249
250class _RepositoryReadmeIjgFile extends _RepositorySingleLicenseFile {
251  _RepositoryReadmeIjgFile(_RepositoryDirectory parent, fs.TextFile io)
252    : super(parent, io, _parseLicense(io));
253
254  static final RegExp _pattern = RegExp(
255    r'Permission is hereby granted to use, copy, modify, and distribute this\n'
256    r'software \(or portions thereof\) for any purpose, without fee, subject to these\n'
257    r'conditions:\n'
258    r'\(1\) If any part of the source code for this software is distributed, then this\n'
259    r'README file must be included, with this copyright and no-warranty notice\n'
260    r'unaltered; and any additions, deletions, or changes to the original files\n'
261    r'must be clearly indicated in accompanying documentation\.\n'
262    r'\(2\) If only executable code is distributed, then the accompanying\n'
263    r'documentation must state that "this software is based in part on the work of\n'
264    r'the Independent JPEG Group"\.\n'
265    r'\(3\) Permission for use of this software is granted only if the user accepts\n'
266    r'full responsibility for any undesirable consequences; the authors accept\n'
267    r'NO LIABILITY for damages of any kind\.\n',
268    caseSensitive: false
269  );
270
271  static License _parseLicense(fs.TextFile io) {
272    final String body = io.readString();
273    if (!body.contains(_pattern))
274      throw 'unexpected contents in IJG README';
275    return License.message(body, LicenseType.ijg, origin: io.fullName);
276  }
277
278  @override
279  License licenseWithName(String name) {
280    if (this.name == name)
281      return license;
282    return null;
283  }
284
285  @override
286  License licenseOfType(LicenseType type) {
287    return null;
288  }
289}
290
291class _RepositoryDartLicenseFile extends _RepositorySingleLicenseFile {
292  _RepositoryDartLicenseFile(_RepositoryDirectory parent, fs.TextFile io)
293    : super(parent, io, _parseLicense(io));
294
295  static final RegExp _pattern = RegExp(
296    r'^This license applies to all parts of Dart that are not externally\n'
297    r'maintained libraries\. The external maintained libraries used by\n'
298    r'Dart are:\n'
299    r'\n'
300    r'(?:.+\n)+'
301    r'\n'
302    r'The libraries may have their own licenses; we recommend you read them,\n'
303    r'as their terms may differ from the terms below\.\n'
304    r'\n'
305    r'(Copyright (?:.|\n)+)$',
306    caseSensitive: false
307  );
308
309  static License _parseLicense(fs.TextFile io) {
310    final Match match = _pattern.firstMatch(io.readString());
311    if (match == null || match.groupCount != 1)
312      throw 'unexpected Dart license file contents';
313    return License.template(match.group(1), LicenseType.bsd, origin: io.fullName);
314  }
315
316  @override
317  License licenseOfType(LicenseType type) {
318    return null;
319  }
320}
321
322class _RepositoryLibPngLicenseFile extends _RepositorySingleLicenseFile {
323  _RepositoryLibPngLicenseFile(_RepositoryDirectory parent, fs.TextFile io)
324    : super(parent, io, License.blank(io.readString(), LicenseType.libpng, origin: io.fullName)) {
325    _verifyLicense(io);
326  }
327
328  static void _verifyLicense(fs.TextFile io) {
329    final String contents = io.readString();
330    if (!contents.contains('COPYRIGHT NOTICE, DISCLAIMER, and LICENSE:') ||
331        !contents.contains('png') ||
332        !contents.contains('END OF COPYRIGHT NOTICE, DISCLAIMER, and LICENSE.'))
333      throw 'unexpected libpng license file contents:\n----8<----\n$contents\n----<8----';
334  }
335
336  @override
337  License licenseOfType(LicenseType type) {
338    if (type == LicenseType.libpng)
339      return license;
340    return null;
341  }
342}
343
344class _RepositoryBlankLicenseFile extends _RepositorySingleLicenseFile {
345  _RepositoryBlankLicenseFile(_RepositoryDirectory parent, fs.TextFile io, String sanityCheck)
346    : super(parent, io, License.blank(io.readString(), LicenseType.unknown)) {
347    _verifyLicense(io, sanityCheck);
348  }
349
350  static void _verifyLicense(fs.TextFile io, String sanityCheck) {
351    final String contents = io.readString();
352    if (!contents.contains(sanityCheck))
353      throw 'unexpected file contents; wanted "$sanityCheck", but got:\n----8<----\n$contents\n----<8----';
354  }
355
356  @override
357  License licenseOfType(LicenseType type) => null;
358}
359
360class _RepositoryCatapultApiClientLicenseFile extends _RepositorySingleLicenseFile {
361  _RepositoryCatapultApiClientLicenseFile(_RepositoryDirectory parent, fs.TextFile io)
362    : super(parent, io, _parseLicense(io));
363
364  static final RegExp _pattern = RegExp(
365    r' *Licensed under the Apache License, Version 2\.0 \(the "License"\);\n'
366    r' *you may not use this file except in compliance with the License\.\n'
367    r' *You may obtain a copy of the License at\n'
368    r' *\n'
369    r' *(http://www\.apache\.org/licenses/LICENSE-2\.0)\n'
370    r' *\n'
371    r' *Unless required by applicable law or agreed to in writing, software\n'
372    r' *distributed under the License is distributed on an "AS IS" BASIS,\n'
373    r' *WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.\n'
374    r' *See the License for the specific language governing permissions and\n'
375    r' *limitations under the License\.\n',
376    multiLine: true,
377    caseSensitive: false,
378  );
379
380  static License _parseLicense(fs.TextFile io) {
381    final Match match = _pattern.firstMatch(io.readString());
382    if (match == null || match.groupCount != 1)
383      throw 'unexpected apiclient license file contents';
384    return License.fromUrl(match.group(1), origin: io.fullName);
385  }
386
387  @override
388  License licenseOfType(LicenseType type) {
389    return null;
390  }
391}
392
393class _RepositoryCatapultCoverageLicenseFile extends _RepositorySingleLicenseFile {
394  _RepositoryCatapultCoverageLicenseFile(_RepositoryDirectory parent, fs.TextFile io)
395    : super(parent, io, _parseLicense(io));
396
397  static final RegExp _pattern = RegExp(
398    r' *Except where noted otherwise, this software is licensed under the Apache\n'
399    r' *License, Version 2.0 \(the "License"\); you may not use this work except in\n'
400    r' *compliance with the License\.  You may obtain a copy of the License at\n'
401    r' *\n'
402    r' *(http://www\.apache\.org/licenses/LICENSE-2\.0)\n'
403    r' *\n'
404    r' *Unless required by applicable law or agreed to in writing, software\n'
405    r' *distributed under the License is distributed on an "AS IS" BASIS,\n'
406    r' *WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.\n'
407    r' *See the License for the specific language governing permissions and\n'
408    r' *limitations under the License\.\n',
409    multiLine: true,
410    caseSensitive: false,
411  );
412
413  static License _parseLicense(fs.TextFile io) {
414    final Match match = _pattern.firstMatch(io.readString());
415    if (match == null || match.groupCount != 1)
416      throw 'unexpected coverage license file contents';
417    return License.fromUrl(match.group(1), origin: io.fullName);
418  }
419
420  @override
421  License licenseOfType(LicenseType type) {
422    return null;
423  }
424}
425
426class _RepositoryLibJpegTurboLicense extends _RepositoryLicenseFile {
427  _RepositoryLibJpegTurboLicense(_RepositoryDirectory parent, fs.TextFile io)
428    : super(parent, io) {
429    _parseLicense(io);
430  }
431
432  static final RegExp _pattern = RegExp(
433    r'libjpeg-turbo is covered by three compatible BSD-style open source licenses:\n'
434    r'\n'
435    r'- The IJG \(Independent JPEG Group\) License, which is listed in\n'
436    r'  \[README\.ijg\]\(README\.ijg\)\n'
437    r'\n'
438    r'  This license applies to the libjpeg API library and associated programs\n'
439    r'  \(any code inherited from libjpeg, and any modifications to that code\.\)\n'
440    r'\n'
441    r'- The Modified \(3-clause\) BSD License, which is listed in\n'
442    r'  \[turbojpeg\.c\]\(turbojpeg\.c\)\n'
443    r'\n'
444    r'  This license covers the TurboJPEG API library and associated programs\.\n'
445    r'\n'
446    r'- The zlib License, which is listed in \[simd/jsimdext\.inc\]\(simd/jsimdext\.inc\)\n'
447    r'\n'
448    r'  This license is a subset of the other two, and it covers the libjpeg-turbo\n'
449    r'  SIMD extensions\.\n'
450  );
451
452  static void _parseLicense(fs.TextFile io) {
453    final String body = io.readString();
454    if (!body.contains(_pattern))
455      throw 'unexpected contents in libjpeg-turbo LICENSE';
456  }
457
458  List<License> _licenses;
459
460  @override
461  List<License> get licenses {
462    if (_licenses == null) {
463      final _RepositoryReadmeIjgFile readme = parent.getChildByName('README.ijg');
464      final _RepositorySourceFile main = parent.getChildByName('turbojpeg.c');
465      final _RepositoryDirectory simd = parent.getChildByName('simd');
466      final _RepositorySourceFile zlib = simd.getChildByName('jsimdext.inc');
467      _licenses = <License>[];
468      _licenses.add(readme.license);
469      _licenses.add(main.licenses.single);
470      _licenses.add(zlib.licenses.single);
471    }
472    return _licenses;
473  }
474
475  @override
476  License licenseWithName(String name) {
477    return null;
478  }
479
480  @override
481  List<License> licensesFor(String name) {
482    return licenses;
483  }
484
485  @override
486  License licenseOfType(LicenseType type) {
487    return null;
488  }
489
490  @override
491  License get defaultLicense => null;
492}
493
494class _RepositoryFreetypeLicenseFile extends _RepositoryLicenseFile {
495  _RepositoryFreetypeLicenseFile(_RepositoryDirectory parent, fs.TextFile io)
496    : _target = _parseLicense(io), super(parent, io);
497
498  static final RegExp _pattern = RegExp(
499    r'The  FreeType 2  font  engine is  copyrighted  work and  cannot be  used\n'
500    r'legally  without a  software license\.   In  order to  make this  project\n'
501    r'usable  to a vast  majority of  developers, we  distribute it  under two\n'
502    r'mutually exclusive open-source licenses\.\n'
503    r'\n'
504    r'This means  that \*you\* must choose  \*one\* of the  two licenses described\n'
505    r'below, then obey  all its terms and conditions when  using FreeType 2 in\n'
506    r'any of your projects or products.\n'
507    r'\n'
508    r"  - The FreeType License, found in  the file `(FTL\.TXT)', which is similar\n"
509    r'    to the original BSD license \*with\* an advertising clause that forces\n'
510    r"    you  to  explicitly cite  the  FreeType  project  in your  product's\n"
511    r'    documentation\.  All  details are in the license  file\.  This license\n'
512    r"    is  suited  to products  which  don't  use  the GNU  General  Public\n"
513    r'    License\.\n'
514    r'\n'
515    r'    Note that  this license  is  compatible  to the  GNU General  Public\n'
516    r'    License version 3, but not version 2\.\n'
517    r'\n'
518    r"  - The GNU General Public License version 2, found in  `GPLv2\.TXT' \(any\n"
519    r'    later version can be used  also\), for programs which already use the\n'
520    r'    GPL\.  Note  that the  FTL is  incompatible  with  GPLv2 due  to  its\n'
521    r'    advertisement clause\.\n'
522    r'\n'
523    r'The contributed BDF and PCF drivers  come with a license similar to that\n'
524    r'of the X Window System\.  It is compatible to the above two licenses \(see\n'
525    r'file src/bdf/README and  src/pcf/README\)\.  The same holds  for the files\n'
526    r"`fthash\.c' and  `fthash\.h'; their  code was  part of  the BDF  driver in\n"
527    r'earlier FreeType versions\.\n'
528    r'\n'
529    r'The gzip module uses the zlib license \(see src/gzip/zlib\.h\) which too is\n'
530    r'compatible to the above two licenses\.\n'
531    r'\n'
532    r'The MD5 checksum support \(only used for debugging in development builds\)\n'
533    r'is in the public domain\.\n'
534    r'\n*'
535    r'--- end of LICENSE\.TXT ---\n*$'
536  );
537
538  static String _parseLicense(fs.TextFile io) {
539    final Match match = _pattern.firstMatch(io.readString());
540    if (match == null || match.groupCount != 1)
541      throw 'unexpected Freetype license file contents';
542    return match.group(1);
543  }
544
545  final String _target;
546  List<License> _targetLicense;
547
548  void _warmCache() {
549    _targetLicense ??= <License>[parent.nearestLicenseWithName(_target)];
550  }
551
552  @override
553  List<License> licensesFor(String name) {
554    _warmCache();
555    return _targetLicense;
556  }
557
558  @override
559  License licenseOfType(LicenseType type) => null;
560
561  @override
562  License licenseWithName(String name) => null;
563
564  @override
565  License get defaultLicense {
566    _warmCache();
567    return _targetLicense.single;
568  }
569
570  @override
571  Iterable<License> get licenses sync* { }
572}
573
574class _RepositoryIcuLicenseFile extends _RepositoryLicenseFile {
575  _RepositoryIcuLicenseFile(_RepositoryDirectory parent, fs.TextFile io)
576    : _licenses = _parseLicense(io),
577      super(parent, io);
578
579  @override
580  fs.TextFile get io => super.io;
581
582  final List<License> _licenses;
583
584  static final RegExp _pattern = RegExp(
585    r'^COPYRIGHT AND PERMISSION NOTICE \(ICU 58 and later\)\n+'
586    r'( *Copyright (?:.|\n)+?)\n+' // 1
587    r'Third-Party Software Licenses\n+'
588    r' *This section contains third-party software notices and/or additional\n'
589    r' *terms for licensed third-party software components included within ICU\n'
590    r' *libraries\.\n+'
591    r' *1\. ICU License - ICU 1.8.1 to ICU 57.1[ \n]+?'
592    r' *COPYRIGHT AND PERMISSION NOTICE\n+'
593    r'(Copyright (?:.|\n)+?)\n+' //2
594    r' *2\. Chinese/Japanese Word Break Dictionary Data \(cjdict\.txt\)\n+'
595    r' #     The Google Chrome software developed by Google is licensed under\n?'
596    r' # the BSD license\. Other software included in this distribution is\n?'
597    r' # provided under other licenses, as set forth below\.\n'
598    r' #\n'
599    r'( #  The BSD License\n'
600    r' #  http://opensource\.org/licenses/bsd-license\.php\n'
601    r' # +Copyright(?:.|\n)+?)\n' // 3
602    r' #\n'
603    r' #\n'
604    r' #  The word list in cjdict.txt are generated by combining three word lists\n?'
605    r' # listed below with further processing for compound word breaking\. The\n?'
606    r' # frequency is generated with an iterative training against Google web\n?'
607    r' # corpora\.\n'
608    r' #\n'
609    r' #  \* Libtabe \(Chinese\)\n'
610    r' #    - https://sourceforge\.net/project/\?group_id=1519\n'
611    r' #    - Its license terms and conditions are shown below\.\n'
612    r' #\n'
613    r' #  \* IPADIC \(Japanese\)\n'
614    r' #    - http://chasen\.aist-nara\.ac\.jp/chasen/distribution\.html\n'
615    r' #    - Its license terms and conditions are shown below\.\n'
616    r' #\n'
617    r' #  ---------COPYING\.libtabe ---- BEGIN--------------------\n'
618    r' #\n'
619    r' # +/\*\n'
620    r'( # +\* Copyright (?:.|\n)+?)\n' // 4
621    r' # +\*/\n'
622    r' #\n'
623    r' # +/\*\n'
624    r'( # +\* Copyright (?:.|\n)+?)\n' // 5
625    r' # +\*/\n'
626    r' #\n'
627    r'( # +Copyright (?:.|\n)+?)\n' // 6
628    r' #\n'
629    r' # +---------------COPYING\.libtabe-----END--------------------------------\n'
630    r' #\n'
631    r' #\n'
632    r' # +---------------COPYING\.ipadic-----BEGIN-------------------------------\n'
633    r' #\n'
634    r'( # +Copyright (?:.|\n)+?)\n' // 7
635    r' #\n'
636    r' # +---------------COPYING\.ipadic-----END----------------------------------\n'
637    r'\n'
638    r' *3\. Lao Word Break Dictionary Data \(laodict\.txt\)\n'
639    r'\n'
640    r'( # +Copyright(?:.|\n)+?)\n' // 8
641    r'\n'
642    r' *4\. Burmese Word Break Dictionary Data \(burmesedict\.txt\)\n'
643    r'\n'
644    r'( # +Copyright(?:.|\n)+?)\n' // 9
645    r'\n'
646    r' *5\. Time Zone Database\n'
647    r'((?:.|\n)+)\n' // 10
648    r'\n'
649    r' *6\. Google double-conversion\n'
650    r'\n'
651    r'(Copyright(?:.|\n)+)\n$', // 11
652    multiLine: true,
653    caseSensitive: false
654  );
655
656  static final RegExp _unexpectedHash = RegExp(r'^.+ #', multiLine: true);
657  static final RegExp _newlineHash = RegExp(r' # ?');
658
659  static String _dewrap(String s) {
660    if (!s.startsWith(' # '))
661      return s;
662    if (s.contains(_unexpectedHash))
663      throw 'ICU license file contained unexpected hash sequence';
664    if (s.contains('\x2028'))
665      throw 'ICU license file contained unexpected line separator';
666    return s.replaceAll(_newlineHash, '\x2028').replaceAll('\n', '').replaceAll('\x2028', '\n');
667  }
668
669  static List<License> _parseLicense(fs.TextFile io) {
670    final Match match = _pattern.firstMatch(io.readString());
671    if (match == null)
672      throw 'could not parse ICU license file';
673    assert(match.groupCount == 11);
674    if (match.group(10).contains(copyrightMentionPattern) || match.group(11).contains('7.'))
675      throw 'unexpected copyright in ICU license file';
676    final List<License> result = <License>[
677      License.fromBodyAndType(_dewrap(match.group(1)), LicenseType.unknown, origin: io.fullName),
678      License.fromBodyAndType(_dewrap(match.group(2)), LicenseType.icu, origin: io.fullName),
679      License.fromBodyAndType(_dewrap(match.group(3)), LicenseType.bsd, origin: io.fullName),
680      License.fromBodyAndType(_dewrap(match.group(4)), LicenseType.bsd, origin: io.fullName),
681      License.fromBodyAndType(_dewrap(match.group(5)), LicenseType.bsd, origin: io.fullName),
682      License.fromBodyAndType(_dewrap(match.group(6)), LicenseType.unknown, origin: io.fullName),
683      License.fromBodyAndType(_dewrap(match.group(7)), LicenseType.unknown, origin: io.fullName),
684      License.fromBodyAndType(_dewrap(match.group(8)), LicenseType.bsd, origin: io.fullName),
685      License.fromBodyAndType(_dewrap(match.group(9)), LicenseType.bsd, origin: io.fullName),
686      License.fromBodyAndType(_dewrap(match.group(11)), LicenseType.bsd, origin: io.fullName),
687    ];
688    return result;
689  }
690
691  @override
692  List<License> licensesFor(String name) {
693    return _licenses;
694  }
695
696  @override
697  License licenseOfType(LicenseType type) {
698    if (type == LicenseType.icu)
699      return _licenses[0];
700    throw 'tried to use ICU license file to find a license by type but type wasn\'t ICU';
701  }
702
703  @override
704  License licenseWithName(String name) {
705    throw 'tried to use ICU license file to find a license by name';
706  }
707
708  @override
709  License get defaultLicense => _licenses[0];
710
711  @override
712  Iterable<License> get licenses => _licenses;
713}
714
715Iterable<List<int>> splitIntList(List<int> data, int boundary) sync* {
716  int index = 0;
717  List<int> getOne() {
718    final int start = index;
719    int end = index;
720    while ((end < data.length) && (data[end] != boundary))
721      end += 1;
722    end += 1;
723    index = end;
724    return data.sublist(start, end).toList();
725  }
726  while (index < data.length)
727    yield getOne();
728}
729
730class _RepositoryMultiLicenseNoticesForFilesFile extends _RepositoryLicenseFile {
731  _RepositoryMultiLicenseNoticesForFilesFile(_RepositoryDirectory parent, fs.File io)
732    : _licenses = _parseLicense(io),
733      super(parent, io);
734
735  final Map<String, License> _licenses;
736
737  static Map<String, License> _parseLicense(fs.File io) {
738    final Map<String, License> result = <String, License>{};
739    // Files of this type should begin with:
740    // "Notices for files contained in the"
741    // ...then have a second line which is 60 "=" characters
742    final List<List<int>> contents = splitIntList(io.readBytes(), 0x0A).toList();
743    if (!ascii.decode(contents[0]).startsWith('Notices for files contained in') ||
744        ascii.decode(contents[1]) != '============================================================\n')
745      throw 'unrecognised syntax: ${io.fullName}';
746    int index = 2;
747    while (index < contents.length) {
748      if (ascii.decode(contents[index]) != 'Notices for file(s):\n')
749        throw 'unrecognised syntax on line ${index + 1}: ${io.fullName}';
750      index += 1;
751      final List<String> names = <String>[];
752      do {
753        names.add(ascii.decode(contents[index]));
754        index += 1;
755      } while (ascii.decode(contents[index]) != '------------------------------------------------------------\n');
756      index += 1;
757      final List<List<int>> body = <List<int>>[];
758      do {
759        body.add(contents[index]);
760        index += 1;
761      } while (index < contents.length &&
762          ascii.decode(contents[index], allowInvalid: true) != '============================================================\n');
763      index += 1;
764      final List<int> bodyBytes = body.expand((List<int> line) => line).toList();
765      String bodyText;
766      try {
767        bodyText = utf8.decode(bodyBytes);
768      } on FormatException {
769        bodyText = latin1.decode(bodyBytes);
770      }
771      final License license = License.unique(bodyText, LicenseType.unknown, origin: io.fullName);
772      for (String name in names) {
773        if (result[name] != null)
774          throw 'conflicting license information for $name in ${io.fullName}';
775        result[name] = license;
776      }
777    }
778    return result;
779  }
780
781  @override
782  List<License> licensesFor(String name) {
783    final License license = _licenses[name];
784    if (license != null)
785      return <License>[license];
786    return null;
787  }
788
789  @override
790  License licenseOfType(LicenseType type) {
791    throw 'tried to use multi-license license file to find a license by type';
792  }
793
794  @override
795  License licenseWithName(String name) {
796    throw 'tried to use multi-license license file to find a license by name';
797  }
798
799  @override
800  License get defaultLicense {
801    assert(false);
802    throw '$this ($runtimeType) does not have a concept of a "default" license';
803  }
804
805  @override
806  Iterable<License> get licenses => _licenses.values;
807}
808
809class _RepositoryCxxStlDualLicenseFile extends _RepositoryLicenseFile {
810  _RepositoryCxxStlDualLicenseFile(_RepositoryDirectory parent, fs.TextFile io)
811    : _licenses = _parseLicenses(io), super(parent, io);
812
813  static final RegExp _pattern = RegExp(
814    r'==============================================================================\n'
815    r'.+ License.*\n'
816    r'==============================================================================\n'
817    r'\n'
818    r'The .+ library is dual licensed under both the University of Illinois\n'
819    r'"BSD-Like" license and the MIT license\. +As a user of this code you may choose\n'
820    r'to use it under either license\. +As a contributor, you agree to allow your code\n'
821    r'to be used under both\.\n'
822    r'\n'
823    r'Full text of the relevant licenses is included below\.\n'
824    r'\n'
825    r'==============================================================================\n'
826    r'((?:.|\n)+)\n'
827    r'==============================================================================\n'
828    r'((?:.|\n)+)'
829    r'$'
830  );
831
832  static List<License> _parseLicenses(fs.TextFile io) {
833    final Match match = _pattern.firstMatch(io.readString());
834    if (match == null || match.groupCount != 2)
835      throw 'unexpected dual license file contents';
836    return <License>[
837      License.fromBodyAndType(match.group(1), LicenseType.bsd),
838      License.fromBodyAndType(match.group(2), LicenseType.mit),
839    ];
840  }
841
842  List<License> _licenses;
843
844  @override
845  List<License> licensesFor(String name) {
846    return _licenses;
847  }
848
849  @override
850  License licenseOfType(LicenseType type) {
851    throw 'tried to look up a dual-license license by type ("$type")';
852  }
853
854  @override
855  License licenseWithName(String name) {
856    throw 'tried to look up a dual-license license by name ("$name")';
857  }
858
859  @override
860  License get defaultLicense => _licenses[0];
861
862  @override
863  Iterable<License> get licenses => _licenses;
864}
865
866
867// DIRECTORIES
868
869class _RepositoryDirectory extends _RepositoryEntry implements LicenseSource {
870  _RepositoryDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io) {
871    crawl();
872  }
873
874  @override
875  fs.Directory get io => super.io;
876
877  final List<_RepositoryDirectory> _subdirectories = <_RepositoryDirectory>[];
878  final List<_RepositoryLicensedFile> _files = <_RepositoryLicensedFile>[];
879  final List<_RepositoryLicenseFile> _licenses = <_RepositoryLicenseFile>[];
880
881  List<_RepositoryDirectory> get subdirectories => _subdirectories;
882
883  final Map<String, _RepositoryEntry> _childrenByName = <String, _RepositoryEntry>{};
884
885  // the bit at the beginning excludes files like "license.py".
886  static final RegExp _licenseNamePattern = RegExp(r'^(?!.*\.py$)(?!.*(?:no|update)-copyright)(?!.*mh-bsd-gcc).*\b_*(?:license(?!\.html)|copying|copyright|notice|l?gpl|bsd|mpl?|ftl\.txt)_*\b', caseSensitive: false);
887
888  void crawl() {
889    for (fs.IoNode entry in io.walk) {
890      if (shouldRecurse(entry)) {
891        assert(!_childrenByName.containsKey(entry.name));
892        if (entry is fs.Directory) {
893          final _RepositoryDirectory child = createSubdirectory(entry);
894          _subdirectories.add(child);
895          _childrenByName[child.name] = child;
896        } else if (entry is fs.File) {
897          try {
898            final _RepositoryFile child = createFile(entry);
899            assert(child != null);
900            if (child is _RepositoryLicensedFile) {
901              _files.add(child);
902            } else {
903              assert(child is _RepositoryLicenseFile);
904              _licenses.add(child);
905            }
906            _childrenByName[child.name] = child;
907          } catch (e) {
908            system.stderr.writeln('failed to handle $entry: $e');
909            rethrow;
910          }
911        } else {
912          assert(entry is fs.Link);
913        }
914      }
915    }
916
917    for (_RepositoryDirectory child in virtualSubdirectories) {
918      _subdirectories.add(child);
919      _childrenByName[child.name] = child;
920    }
921  }
922
923  // Override this to add additional child directories that do not represent a
924  // direct child of this directory's filesystem node.
925  List<_RepositoryDirectory> get virtualSubdirectories => <_RepositoryDirectory>[];
926
927  bool shouldRecurse(fs.IoNode entry) {
928    return !entry.fullName.endsWith('third_party/gn') &&
929            entry.name != '.cipd' &&
930            entry.name != '.git' &&
931            entry.name != '.github' &&
932            entry.name != '.gitignore' &&
933            entry.name != '.vscode' &&
934            entry.name != 'test' &&
935            entry.name != 'test.disabled' &&
936            entry.name != 'test_support' &&
937            entry.name != 'tests' &&
938            entry.name != 'javatests' &&
939            entry.name != 'testing';
940  }
941
942  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
943    if (entry.name == 'third_party')
944      return _RepositoryGenericThirdPartyDirectory(this, entry);
945    return _RepositoryDirectory(this, entry);
946  }
947
948  _RepositoryFile createFile(fs.IoNode entry) {
949    if (entry is fs.TextFile) {
950      if (_RepositoryApache4DNoticeFile.consider(entry)) {
951        return _RepositoryApache4DNoticeFile(this, entry);
952      } else {
953        _RepositoryFile result;
954        if (entry.name == 'NOTICE')
955          result = _RepositoryLicenseRedirectFile.maybeCreateFrom(this, entry);
956        if (result != null) {
957          return result;
958        } else if (entry.name.contains(_licenseNamePattern)) {
959          return _RepositoryGeneralSingleLicenseFile(this, entry);
960        } else if (entry.name == 'README.ijg') {
961          return _RepositoryReadmeIjgFile(this, entry);
962        } else {
963          return _RepositorySourceFile(this, entry);
964        }
965      }
966    } else if (entry.name == 'NOTICE.txt') {
967      return _RepositoryMultiLicenseNoticesForFilesFile(this, entry);
968    } else {
969      return _RepositoryBinaryFile(this, entry);
970    }
971  }
972
973  int get count => _files.length + _subdirectories.fold<int>(0, (int count, _RepositoryDirectory child) => count + child.count);
974
975  @override
976  List<License> nearestLicensesFor(String name) {
977    if (_licenses.isEmpty) {
978      if (_canGoUp(null))
979        return parent.nearestLicensesFor('${io.name}/$name');
980      return null;
981    }
982    if (_licenses.length == 1)
983      return _licenses.single.licensesFor(name);
984    final List<License> licenses = _licenses.expand((_RepositoryLicenseFile license) sync* {
985      final List<License> licenses = license.licensesFor(name);
986      if (licenses != null)
987        yield* licenses;
988    }).toList();
989    if (licenses.isEmpty)
990      return null;
991    if (licenses.length > 1) {
992      //print('unexpectedly found multiple matching licenses for: $name');
993      return licenses; // TODO(ianh): disambiguate them, in case we have e.g. a dual GPL/BSD situation
994    }
995    return licenses;
996  }
997
998  @override
999  License nearestLicenseOfType(LicenseType type) {
1000    License result = _nearestAncestorLicenseWithType(type);
1001    if (result == null) {
1002      for (_RepositoryDirectory directory in _subdirectories) {
1003        result = directory._localLicenseWithType(type);
1004        if (result != null)
1005          break;
1006      }
1007    }
1008    result ??= _fullWalkUpForLicenseWithType(type);
1009    return result;
1010  }
1011
1012  /// Searches the current and all parent directories (up to the license root)
1013  /// for a license of the specified type.
1014  License _nearestAncestorLicenseWithType(LicenseType type) {
1015    final License result = _localLicenseWithType(type);
1016    if (result != null)
1017      return result;
1018    if (_canGoUp(null))
1019      return parent._nearestAncestorLicenseWithType(type);
1020    return null;
1021  }
1022
1023  /// Searches all subdirectories below the current license root for a license
1024  /// of the specified type.
1025  License _fullWalkUpForLicenseWithType(LicenseType type) {
1026    return _canGoUp(null)
1027            ? parent._fullWalkUpForLicenseWithType(type)
1028            : _fullWalkDownForLicenseWithType(type);
1029  }
1030
1031  /// Searches the current directory and all subdirectories for a license of
1032  /// the specified type.
1033  License _fullWalkDownForLicenseWithType(LicenseType type) {
1034    License result = _localLicenseWithType(type);
1035    if (result == null) {
1036      for (_RepositoryDirectory directory in _subdirectories) {
1037        result = directory._fullWalkDownForLicenseWithType(type);
1038        if (result != null)
1039          break;
1040      }
1041    }
1042    return result;
1043  }
1044
1045  /// Searches the current directory for licenses of the specified type.
1046  License _localLicenseWithType(LicenseType type) {
1047    final List<License> licenses = _licenses.expand((_RepositoryLicenseFile license) sync* {
1048      final License result = license.licenseOfType(type);
1049      if (result != null)
1050        yield result;
1051    }).toList();
1052    if (licenses.length > 1) {
1053      print('unexpectedly found multiple matching licenses in $name of type $type');
1054      return null;
1055    }
1056    if (licenses.isNotEmpty)
1057      return licenses.single;
1058    return null;
1059  }
1060
1061  @override
1062  License nearestLicenseWithName(String name, { String authors }) {
1063    License result = _nearestAncestorLicenseWithName(name, authors: authors);
1064    if (result == null) {
1065      for (_RepositoryDirectory directory in _subdirectories) {
1066        result = directory._localLicenseWithName(name, authors: authors);
1067        if (result != null)
1068          break;
1069      }
1070    }
1071    result ??= _fullWalkUpForLicenseWithName(name, authors: authors);
1072    result ??= _fullWalkUpForLicenseWithName(name, authors: authors, ignoreCase: true);
1073    if (authors != null && result == null) {
1074      // if (result == null)
1075      //   print('could not find $name for authors "$authors", now looking for any $name in $this');
1076      result = nearestLicenseWithName(name);
1077      // if (result == null)
1078      //   print('completely failed to find $name for authors "$authors"');
1079      // else
1080      //   print('ended up finding a $name for "${result.authors}" instead');
1081    }
1082    return result;
1083  }
1084
1085  bool _canGoUp(String authors) {
1086    return parent != null && (authors != null || isLicenseRootException || (!isLicenseRoot && !parent.subdirectoriesAreLicenseRoots));
1087  }
1088
1089  License _nearestAncestorLicenseWithName(String name, { String authors }) {
1090    final License result = _localLicenseWithName(name, authors: authors);
1091    if (result != null)
1092      return result;
1093    if (_canGoUp(authors))
1094      return parent._nearestAncestorLicenseWithName(name, authors: authors);
1095    return null;
1096  }
1097
1098  License _fullWalkUpForLicenseWithName(String name, { String authors, bool ignoreCase = false }) {
1099    return _canGoUp(authors)
1100            ? parent._fullWalkUpForLicenseWithName(name, authors: authors, ignoreCase: ignoreCase)
1101            : _fullWalkDownForLicenseWithName(name, authors: authors, ignoreCase: ignoreCase);
1102  }
1103
1104  License _fullWalkDownForLicenseWithName(String name, { String authors, bool ignoreCase = false }) {
1105    License result = _localLicenseWithName(name, authors: authors, ignoreCase: ignoreCase);
1106    if (result == null) {
1107      for (_RepositoryDirectory directory in _subdirectories) {
1108        result = directory._fullWalkDownForLicenseWithName(name, authors: authors, ignoreCase: ignoreCase);
1109        if (result != null)
1110          break;
1111      }
1112    }
1113    return result;
1114  }
1115
1116  /// Unless isLicenseRootException is true, we should not walk up the tree from
1117  /// here looking for licenses.
1118  bool get isLicenseRoot => parent == null;
1119
1120  /// Unless isLicenseRootException is true on a child, the child should not
1121  /// walk up the tree to here looking for licenses.
1122  bool get subdirectoriesAreLicenseRoots => false;
1123
1124  @override
1125  String get libraryName {
1126    if (isLicenseRoot)
1127      return name;
1128    assert(parent != null);
1129    if (parent.subdirectoriesAreLicenseRoots)
1130      return name;
1131    return parent.libraryName;
1132  }
1133
1134  /// Overrides isLicenseRoot and parent.subdirectoriesAreLicenseRoots for cases
1135  /// where a directory contains license roots instead of being one. This
1136  /// allows, for example, the expat third_party directory to contain a
1137  /// subdirectory with expat while itself containing a BUILD file that points
1138  /// to the LICENSE in the root of the repo.
1139  bool get isLicenseRootException => false;
1140
1141  License _localLicenseWithName(String name, { String authors, bool ignoreCase = false }) {
1142    Map<String, _RepositoryEntry> map;
1143    if (ignoreCase) {
1144      // we get here if we're trying a last-ditch effort at finding a file.
1145      // so this should happen only rarely.
1146      map = HashMap<String, _RepositoryEntry>(
1147        equals: (String n1, String n2) => n1.toLowerCase() == n2.toLowerCase(),
1148        hashCode: (String n) => n.toLowerCase().hashCode
1149      )
1150        ..addAll(_childrenByName);
1151    } else {
1152      map = _childrenByName;
1153    }
1154    final _RepositoryEntry entry = map[name];
1155    License license;
1156    if (entry is _RepositoryLicensedFile) {
1157      license = entry.licenses.single;
1158    } else if (entry is _RepositoryLicenseFile) {
1159      license = entry.defaultLicense;
1160    } else if (entry != null) {
1161      if (authors == null)
1162        throw 'found "$name" in $this but it was a ${entry.runtimeType}';
1163    }
1164    if (license != null && authors != null) {
1165      if (license.authors?.toLowerCase() != authors.toLowerCase())
1166        license = null;
1167    }
1168    return license;
1169  }
1170
1171  _RepositoryEntry getChildByName(String name) {
1172    return _childrenByName[name];
1173  }
1174
1175  Set<License> getLicenses(_Progress progress) {
1176    final Set<License> result = <License>{};
1177    for (_RepositoryDirectory directory in _subdirectories)
1178      result.addAll(directory.getLicenses(progress));
1179    for (_RepositoryLicensedFile file in _files) {
1180      if (file.isIncludedInBuildProducts) {
1181        try {
1182          progress.label = '$file';
1183          final List<License> licenses = file.licenses;
1184          assert(licenses != null && licenses.isNotEmpty);
1185          result.addAll(licenses);
1186          progress.advance(success: true);
1187        } catch (e, stack) {
1188          system.stderr.writeln('\nerror searching for copyright in: ${file.io}\n$e');
1189          if (e is! String)
1190            system.stderr.writeln(stack);
1191          system.stderr.writeln('\n');
1192          progress.advance(success: false);
1193        }
1194      }
1195    }
1196    for (_RepositoryLicenseFile file in _licenses)
1197      result.addAll(file.licenses);
1198    return result;
1199  }
1200
1201  int get fileCount {
1202    int result = 0;
1203    for (_RepositoryLicensedFile file in _files) {
1204      if (file.isIncludedInBuildProducts)
1205        result += 1;
1206    }
1207    for (_RepositoryDirectory directory in _subdirectories)
1208      result += directory.fileCount;
1209    return result;
1210  }
1211
1212  Iterable<_RepositoryLicensedFile> get _signatureFiles sync* {
1213    for (_RepositoryLicensedFile file in _files) {
1214      if (file.isIncludedInBuildProducts)
1215        yield file;
1216    }
1217    for (_RepositoryDirectory directory in _subdirectories) {
1218      if (directory.includeInSignature)
1219        yield* directory._signatureFiles;
1220    }
1221  }
1222
1223  Stream<List<int>> _signatureStream(List<_RepositoryLicensedFile> files) async* {
1224    for (_RepositoryLicensedFile file in files) {
1225      yield file.io.fullName.codeUnits;
1226      yield file.io.readBytes();
1227    }
1228  }
1229
1230  /// Compute a signature representing a hash of all the licensed files within
1231  /// this directory tree.
1232  Future<String> get signature async {
1233    final List<_RepositoryLicensedFile> allFiles = _signatureFiles.toList();
1234    allFiles.sort((_RepositoryLicensedFile a, _RepositoryLicensedFile b) =>
1235        a.io.fullName.compareTo(b.io.fullName));
1236    final crypto.Digest digest = await crypto.md5.bind(_signatureStream(allFiles)).single;
1237    return digest.bytes.map((int e) => e.toRadixString(16).padLeft(2, '0')).join();
1238  }
1239
1240  /// True if this directory's contents should be included when computing the signature.
1241  bool get includeInSignature => true;
1242}
1243
1244class _RepositoryGenericThirdPartyDirectory extends _RepositoryDirectory {
1245  _RepositoryGenericThirdPartyDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1246
1247  @override
1248  bool get subdirectoriesAreLicenseRoots => true;
1249}
1250
1251class _RepositoryReachOutFile extends _RepositoryLicensedFile {
1252  _RepositoryReachOutFile(_RepositoryDirectory parent, fs.File io, this.offset) : super(parent, io);
1253
1254  final int offset;
1255
1256  @override
1257  List<License> get licenses {
1258    _RepositoryDirectory directory = parent;
1259    int index = offset;
1260    while (index > 1) {
1261      if (directory == null)
1262        break;
1263      directory = directory.parent;
1264      index -= 1;
1265    }
1266    return directory?.nearestLicensesFor(name);
1267  }
1268}
1269
1270class _RepositoryReachOutDirectory extends _RepositoryDirectory {
1271  _RepositoryReachOutDirectory(_RepositoryDirectory parent, fs.Directory io, this.reachOutFilenames, this.offset) : super(parent, io);
1272
1273  final Set<String> reachOutFilenames;
1274  final int offset;
1275
1276  @override
1277  _RepositoryFile createFile(fs.IoNode entry) {
1278    if (reachOutFilenames.contains(entry.name))
1279      return _RepositoryReachOutFile(this, entry, offset);
1280    return super.createFile(entry);
1281  }
1282}
1283
1284class _RepositoryExcludeSubpathDirectory extends _RepositoryDirectory {
1285  _RepositoryExcludeSubpathDirectory(_RepositoryDirectory parent, fs.Directory io, this.paths, [ this.index = 0 ]) : super(parent, io);
1286
1287  final List<String> paths;
1288  final int index;
1289
1290  @override
1291  bool shouldRecurse(fs.IoNode entry) {
1292    if (index == paths.length - 1 && entry.name == paths.last)
1293      return false;
1294    return super.shouldRecurse(entry);
1295  }
1296
1297  @override
1298  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1299    if (entry.name == paths[index] && (index < paths.length - 1))
1300      return _RepositoryExcludeSubpathDirectory(this, entry, paths, index + 1);
1301    return super.createSubdirectory(entry);
1302  }
1303}
1304
1305
1306// WHAT TO CRAWL AND WHAT NOT TO CRAWL
1307
1308class _RepositoryAndroidPlatformDirectory extends _RepositoryDirectory {
1309  _RepositoryAndroidPlatformDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1310
1311  @override
1312  bool shouldRecurse(fs.IoNode entry) {
1313    // we don't link with or use any of the Android NDK samples
1314    return entry.name != 'webview' // not used at all
1315        && entry.name != 'development' // not linked in
1316        && super.shouldRecurse(entry);
1317  }
1318}
1319
1320class _RepositoryExpatDirectory extends _RepositoryDirectory {
1321  _RepositoryExpatDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1322
1323  @override
1324  bool get isLicenseRootException => true;
1325
1326  @override
1327  bool get subdirectoriesAreLicenseRoots => true;
1328}
1329
1330class _RepositoryFreetypeDocsDirectory extends _RepositoryDirectory {
1331  _RepositoryFreetypeDocsDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1332
1333  @override
1334  _RepositoryFile createFile(fs.IoNode entry) {
1335    if (entry.name == 'LICENSE.TXT')
1336      return _RepositoryFreetypeLicenseFile(this, entry);
1337    return super.createFile(entry);
1338  }
1339
1340  @override
1341  int get fileCount => 0;
1342
1343  @override
1344  Set<License> getLicenses(_Progress progress) {
1345    // We don't ship anything in this directory so don't bother looking for licenses there.
1346    // However, there are licenses in this directory referenced from elsewhere, so we do
1347    // want to crawl it and expose them.
1348    return <License>{};
1349  }
1350}
1351
1352class _RepositoryFreetypeSrcGZipDirectory extends _RepositoryDirectory {
1353  _RepositoryFreetypeSrcGZipDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1354
1355  // advice was to make this directory's inffixed.h file (which has no license)
1356  // use the license in zlib.h.
1357
1358  @override
1359  List<License> nearestLicensesFor(String name) {
1360    final License zlib = nearestLicenseWithName('zlib.h');
1361    assert(zlib != null);
1362    if (zlib != null)
1363      return <License>[zlib];
1364    return super.nearestLicensesFor(name);
1365  }
1366
1367  @override
1368  License nearestLicenseOfType(LicenseType type) {
1369    if (type == LicenseType.zlib) {
1370      final License result = nearestLicenseWithName('zlib.h');
1371      assert(result != null);
1372      return result;
1373    }
1374    return null;
1375  }
1376}
1377
1378class _RepositoryFreetypeSrcDirectory extends _RepositoryDirectory {
1379  _RepositoryFreetypeSrcDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1380
1381  @override
1382  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1383    if (entry.name == 'gzip')
1384      return _RepositoryFreetypeSrcGZipDirectory(this, entry);
1385    return super.createSubdirectory(entry);
1386  }
1387
1388  @override
1389  bool shouldRecurse(fs.IoNode entry) {
1390    return entry.name != 'tools'
1391        && super.shouldRecurse(entry);
1392  }
1393}
1394
1395class _RepositoryFreetypeDirectory extends _RepositoryDirectory {
1396  _RepositoryFreetypeDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1397
1398  @override
1399  List<License> nearestLicensesFor(String name) {
1400    final List<License> result = super.nearestLicensesFor(name);
1401    if (result == null) {
1402      final License license = nearestLicenseWithName('LICENSE.TXT');
1403      assert(license != null);
1404      if (license != null)
1405        return <License>[license];
1406    }
1407    return result;
1408  }
1409
1410  @override
1411  License nearestLicenseOfType(LicenseType type) {
1412    if (type == LicenseType.freetype) {
1413      final License result = nearestLicenseWithName('FTL.TXT');
1414      assert(result != null);
1415      return result;
1416    }
1417    return null;
1418  }
1419
1420  @override
1421  bool shouldRecurse(fs.IoNode entry) {
1422    return entry.name != 'builds' // build files
1423        && super.shouldRecurse(entry);
1424  }
1425
1426  @override
1427  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1428    if (entry.name == 'src')
1429      return _RepositoryFreetypeSrcDirectory(this, entry);
1430    if (entry.name == 'docs')
1431      return _RepositoryFreetypeDocsDirectory(this, entry);
1432    return super.createSubdirectory(entry);
1433  }
1434}
1435
1436class _RepositoryGlfwDirectory extends _RepositoryDirectory {
1437  _RepositoryGlfwDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1438
1439  @override
1440  bool shouldRecurse(fs.IoNode entry) {
1441    return entry.name != 'examples' // Not linked in build.
1442        && entry.name != 'tests' // Not linked in build.
1443        && entry.name != 'deps' // Only used by examples and tests; not linked in build.
1444        && super.shouldRecurse(entry);
1445  }
1446}
1447
1448class _RepositoryIcuDirectory extends _RepositoryDirectory {
1449  _RepositoryIcuDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1450
1451  @override
1452  bool shouldRecurse(fs.IoNode entry) {
1453    return entry.name != 'license.html' // redundant with LICENSE file
1454        && super.shouldRecurse(entry);
1455  }
1456
1457  @override
1458  _RepositoryFile createFile(fs.IoNode entry) {
1459    if (entry.name == 'LICENSE')
1460      return _RepositoryIcuLicenseFile(this, entry);
1461    return super.createFile(entry);
1462  }
1463}
1464
1465class _RepositoryHarfbuzzDirectory extends _RepositoryDirectory {
1466  _RepositoryHarfbuzzDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1467
1468  @override
1469  bool shouldRecurse(fs.IoNode entry) {
1470    return entry.name != 'util' // utils are command line tools that do not end up in the binary
1471        && super.shouldRecurse(entry);
1472  }
1473}
1474
1475class _RepositoryJSR305Directory extends _RepositoryDirectory {
1476  _RepositoryJSR305Directory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1477
1478  @override
1479  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1480    if (entry.name == 'src')
1481      return _RepositoryJSR305SrcDirectory(this, entry);
1482    return super.createSubdirectory(entry);
1483  }
1484}
1485
1486class _RepositoryJSR305SrcDirectory extends _RepositoryDirectory {
1487  _RepositoryJSR305SrcDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1488
1489  @override
1490  bool shouldRecurse(fs.IoNode entry) {
1491    return entry.name != 'javadoc'
1492        && entry.name != 'sampleUses'
1493        && super.shouldRecurse(entry);
1494  }
1495}
1496
1497class _RepositoryLibcxxDirectory extends _RepositoryDirectory {
1498  _RepositoryLibcxxDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1499
1500  @override
1501  bool shouldRecurse(fs.IoNode entry) {
1502    return entry.name != 'utils'
1503        && super.shouldRecurse(entry);
1504  }
1505
1506  @override
1507  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1508    if (entry.name == 'src')
1509      return _RepositoryLibcxxSrcDirectory(this, entry);
1510    return super.createSubdirectory(entry);
1511  }
1512
1513  @override
1514  _RepositoryFile createFile(fs.IoNode entry) {
1515    if (entry.name == 'LICENSE.TXT')
1516      return _RepositoryCxxStlDualLicenseFile(this, entry);
1517    return super.createFile(entry);
1518  }
1519}
1520
1521class _RepositoryLibcxxSrcDirectory extends _RepositoryDirectory {
1522  _RepositoryLibcxxSrcDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1523
1524  @override
1525  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1526    if (entry.name == 'support')
1527      return _RepositoryLibcxxSrcSupportDirectory(this, entry);
1528    return super.createSubdirectory(entry);
1529  }
1530}
1531
1532class _RepositoryLibcxxSrcSupportDirectory extends _RepositoryDirectory {
1533  _RepositoryLibcxxSrcSupportDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1534
1535  @override
1536  bool shouldRecurse(fs.IoNode entry) {
1537    return entry.name != 'solaris'
1538        && super.shouldRecurse(entry);
1539  }
1540}
1541
1542class _RepositoryLibcxxabiDirectory extends _RepositoryDirectory {
1543  _RepositoryLibcxxabiDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1544
1545  @override
1546  _RepositoryFile createFile(fs.IoNode entry) {
1547    if (entry.name == 'LICENSE.TXT')
1548      return _RepositoryCxxStlDualLicenseFile(this, entry);
1549    return super.createFile(entry);
1550  }
1551}
1552
1553class _RepositoryLibJpegDirectory extends _RepositoryDirectory {
1554  _RepositoryLibJpegDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1555
1556  @override
1557  _RepositoryFile createFile(fs.IoNode entry) {
1558    if (entry.name == 'README')
1559      return _RepositoryReadmeIjgFile(this, entry);
1560    if (entry.name == 'LICENSE')
1561      return _RepositoryLicenseFileWithLeader(this, entry, RegExp(r'^\(Copied from the README\.\)\n+-+\n+'));
1562    return super.createFile(entry);
1563  }
1564}
1565
1566class _RepositoryLibJpegTurboDirectory extends _RepositoryDirectory {
1567  _RepositoryLibJpegTurboDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1568
1569  @override
1570  _RepositoryFile createFile(fs.IoNode entry) {
1571    if (entry.name == 'LICENSE.md')
1572      return _RepositoryLibJpegTurboLicense(this, entry);
1573    return super.createFile(entry);
1574  }
1575
1576  @override
1577  bool shouldRecurse(fs.IoNode entry) {
1578    return entry.name != 'release' // contains nothing that ends up in the binary executable
1579        && entry.name != 'doc' // contains nothing that ends up in the binary executable
1580        && entry.name != 'testimages' // test assets
1581        && super.shouldRecurse(entry);
1582  }
1583}
1584
1585class _RepositoryLibPngDirectory extends _RepositoryDirectory {
1586  _RepositoryLibPngDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1587
1588  @override
1589  _RepositoryFile createFile(fs.IoNode entry) {
1590    if (entry.name == 'LICENSE' || entry.name == 'png.h')
1591      return _RepositoryLibPngLicenseFile(this, entry);
1592    return super.createFile(entry);
1593  }
1594}
1595
1596class _RepositoryLibWebpDirectory extends _RepositoryDirectory {
1597  _RepositoryLibWebpDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1598
1599  @override
1600  bool shouldRecurse(fs.IoNode entry) {
1601    return entry.name != 'examples' // contains nothing that ends up in the binary executable
1602      && entry.name != 'swig' // not included in our build
1603      && entry.name != 'gradle' // not included in our build
1604      && super.shouldRecurse(entry);
1605  }
1606}
1607
1608class _RepositoryPkgDirectory extends _RepositoryDirectory {
1609  _RepositoryPkgDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1610
1611  @override
1612  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1613    if (entry.name == 'when')
1614      return _RepositoryPkgWhenDirectory(this, entry);
1615    return super.createSubdirectory(entry);
1616  }
1617}
1618
1619class _RepositoryPkgWhenDirectory extends _RepositoryDirectory {
1620  _RepositoryPkgWhenDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1621
1622  @override
1623  bool shouldRecurse(fs.IoNode entry) {
1624    return entry.name != 'example' // contains nothing that ends up in the binary executable
1625        && super.shouldRecurse(entry);
1626  }
1627}
1628
1629class _RepositorySkiaLibWebPDirectory extends _RepositoryDirectory {
1630  _RepositorySkiaLibWebPDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1631
1632  @override
1633  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1634    if (entry.name == 'webp')
1635      return _RepositoryReachOutDirectory(this, entry, const <String>{'config.h'}, 3);
1636    return super.createSubdirectory(entry);
1637  }
1638}
1639
1640class _RepositorySkiaLibSdlDirectory extends _RepositoryDirectory {
1641  _RepositorySkiaLibSdlDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1642
1643  @override
1644  bool get isLicenseRootException => true;
1645}
1646
1647class _RepositorySkiaThirdPartyDirectory extends _RepositoryGenericThirdPartyDirectory {
1648  _RepositorySkiaThirdPartyDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1649
1650  @override
1651  bool shouldRecurse(fs.IoNode entry) {
1652    return entry.name != 'giflib' // contains nothing that ends up in the binary executable
1653        && entry.name != 'freetype' // we use our own version
1654        && entry.name != 'freetype2' // we use our own version
1655        && entry.name != 'gif' // not linked in
1656        && entry.name != 'icu' // we use our own version
1657        && entry.name != 'libjpeg-turbo' // we use our own version
1658        && entry.name != 'libpng' // we use our own version
1659        && entry.name != 'lua' // not linked in
1660        && entry.name != 'yasm' // build tool (assembler)
1661        && super.shouldRecurse(entry);
1662  }
1663
1664  @override
1665  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1666    if (entry.name == 'ktx')
1667      return _RepositoryReachOutDirectory(this, entry, const <String>{'ktx.h', 'ktx.cpp'}, 2);
1668    if (entry.name == 'libmicrohttpd')
1669      return _RepositoryReachOutDirectory(this, entry, const <String>{'MHD_config.h'}, 2);
1670    if (entry.name == 'libwebp')
1671      return _RepositorySkiaLibWebPDirectory(this, entry);
1672    if (entry.name == 'libsdl')
1673      return _RepositorySkiaLibSdlDirectory(this, entry);
1674    return super.createSubdirectory(entry);
1675  }
1676}
1677
1678class _RepositorySkiaDirectory extends _RepositoryDirectory {
1679  _RepositorySkiaDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1680
1681  @override
1682  bool shouldRecurse(fs.IoNode entry) {
1683    return entry.name != 'platform_tools' // contains nothing that ends up in the binary executable
1684        && entry.name != 'tools' // contains nothing that ends up in the binary executable
1685        && entry.name != 'resources' // contains nothing that ends up in the binary executable
1686        && super.shouldRecurse(entry);
1687  }
1688
1689  @override
1690  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1691    if (entry.name == 'third_party')
1692      return _RepositorySkiaThirdPartyDirectory(this, entry);
1693    return super.createSubdirectory(entry);
1694  }
1695}
1696
1697class _RepositoryVulkanDirectory extends _RepositoryDirectory {
1698  _RepositoryVulkanDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1699
1700  @override
1701  bool shouldRecurse(fs.IoNode entry) {
1702    // Flutter only uses the headers in the include directory.
1703    return entry.name == 'include'
1704        && super.shouldRecurse(entry);
1705  }
1706
1707  @override
1708  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1709    if (entry.name == 'src')
1710      return _RepositoryExcludeSubpathDirectory(this, entry, const <String>['spec']);
1711    return super.createSubdirectory(entry);
1712  }
1713}
1714
1715class _RepositoryWuffsDirectory extends _RepositoryDirectory {
1716  _RepositoryWuffsDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1717
1718  @override
1719  bool shouldRecurse(fs.IoNode entry) {
1720    return entry.name != 'CONTRIBUTORS' // not linked in
1721        && super.shouldRecurse(entry);
1722  }
1723
1724  @override
1725  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1726    if (entry.name == 'src')
1727      return _RepositoryExcludeSubpathDirectory(this, entry, const <String>['spec']);
1728    return super.createSubdirectory(entry);
1729  }
1730}
1731
1732class _RepositoryRootThirdPartyDirectory extends _RepositoryGenericThirdPartyDirectory {
1733  _RepositoryRootThirdPartyDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1734
1735  @override
1736  bool shouldRecurse(fs.IoNode entry) {
1737    return entry.name != 'appurify-python' // only used by tests
1738        && entry.name != 'benchmark' // only used by tests
1739        && entry.name != 'dart-sdk' // redundant with //engine/dart; https://github.com/flutter/flutter/issues/2618
1740        && entry.name != 'firebase' // only used by bots; https://github.com/flutter/flutter/issues/3722
1741        && entry.name != 'gyp' // build-time only
1742        && entry.name != 'jinja2' // build-time code generation
1743        && entry.name != 'junit' // only mentioned in build files, not used
1744        && entry.name != 'libxml' // dependency of the testing system that we don't actually use
1745        && entry.name != 'llvm-build' // only used by build
1746        && entry.name != 'markupsafe' // build-time only
1747        && entry.name != 'mockito' // only used by tests
1748        && entry.name != 'pymock' // presumably only used by tests
1749        && entry.name != 'robolectric' // testing framework for android
1750        && entry.name != 'yasm' // build-time dependency only
1751        && entry.name != 'binutils' // build-time dependency only
1752        && entry.name != 'instrumented_libraries' // unused according to chinmay
1753        && entry.name != 'android_tools' // excluded on advice
1754        && entry.name != 'android_support' // build-time only
1755        && entry.name != 'googletest' // only used by tests
1756        && entry.name != 'skia' // treated as a separate component
1757        && entry.name != 'fontconfig' // not used in standard configurations
1758        && entry.name != 'swiftshader' // only used on hosts for tests
1759        && entry.name != 'ocmock' // only used for tests
1760        && super.shouldRecurse(entry);
1761  }
1762
1763  @override
1764  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1765    if (entry.name == 'android_platform')
1766      return _RepositoryAndroidPlatformDirectory(this, entry);
1767    if (entry.name == 'boringssl')
1768      return _RepositoryBoringSSLDirectory(this, entry);
1769    if (entry.name == 'catapult')
1770      return _RepositoryCatapultDirectory(this, entry);
1771    if (entry.name == 'dart')
1772      return _RepositoryDartDirectory(this, entry);
1773    if (entry.name == 'expat')
1774      return _RepositoryExpatDirectory(this, entry);
1775    if (entry.name == 'freetype-android')
1776      throw '//third_party/freetype-android is no longer part of this client: remove it';
1777    if (entry.name == 'freetype2')
1778      return _RepositoryFreetypeDirectory(this, entry);
1779    if (entry.name == 'glfw')
1780      return _RepositoryGlfwDirectory(this, entry);
1781    if (entry.name == 'harfbuzz')
1782      return _RepositoryHarfbuzzDirectory(this, entry);
1783    if (entry.name == 'icu')
1784      return _RepositoryIcuDirectory(this, entry);
1785    if (entry.name == 'jsr-305')
1786      return _RepositoryJSR305Directory(this, entry);
1787    if (entry.name == 'libcxx')
1788      return _RepositoryLibcxxDirectory(this, entry);
1789    if (entry.name == 'libcxxabi')
1790      return _RepositoryLibcxxabiDirectory(this, entry);
1791    if (entry.name == 'libjpeg')
1792      return _RepositoryLibJpegDirectory(this, entry);
1793    if (entry.name == 'libjpeg_turbo' || entry.name == 'libjpeg-turbo')
1794      return _RepositoryLibJpegTurboDirectory(this, entry);
1795    if (entry.name == 'libpng')
1796      return _RepositoryLibPngDirectory(this, entry);
1797    if (entry.name == 'libwebp')
1798      return _RepositoryLibWebpDirectory(this, entry);
1799    if (entry.name == 'pkg')
1800      return _RepositoryPkgDirectory(this, entry);
1801    if (entry.name == 'vulkan')
1802      return _RepositoryVulkanDirectory(this, entry);
1803    if (entry.name == 'wuffs')
1804      return _RepositoryWuffsDirectory(this, entry);
1805    return super.createSubdirectory(entry);
1806  }
1807}
1808
1809class _RepositoryBoringSSLThirdPartyDirectory extends _RepositoryDirectory {
1810  _RepositoryBoringSSLThirdPartyDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1811
1812  @override
1813  bool shouldRecurse(fs.IoNode entry) {
1814    return entry.name != 'android-cmake' // build-time only
1815        && super.shouldRecurse(entry);
1816  }
1817}
1818
1819class _RepositoryBoringSSLSourceDirectory extends _RepositoryDirectory {
1820  _RepositoryBoringSSLSourceDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1821
1822  @override
1823  String get libraryName => 'boringssl';
1824
1825  @override
1826  bool get isLicenseRoot => true;
1827
1828  @override
1829  bool shouldRecurse(fs.IoNode entry) {
1830    return entry.name != 'fuzz' // testing tools, not shipped
1831        && super.shouldRecurse(entry);
1832  }
1833
1834  @override
1835  _RepositoryFile createFile(fs.IoNode entry) {
1836    if (entry.name == 'LICENSE')
1837      return _RepositoryOpenSSLLicenseFile(this, entry);
1838    return super.createFile(entry);
1839  }
1840
1841  @override
1842  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1843    if (entry.name == 'third_party')
1844      return _RepositoryBoringSSLThirdPartyDirectory(this, entry);
1845    return super.createSubdirectory(entry);
1846  }
1847}
1848
1849/// The BoringSSL license file.
1850///
1851/// This license includes 23 lines of informational header text that are not
1852/// part of the copyright notices and can be skipped.
1853/// It also has a trailer that mentions licenses that are used during build
1854/// time of BoringSSL - those can be ignored as well since they don't apply
1855/// to code that is distributed.
1856class _RepositoryOpenSSLLicenseFile extends _RepositorySingleLicenseFile {
1857  _RepositoryOpenSSLLicenseFile(_RepositoryDirectory parent, fs.TextFile io)
1858    : super(parent, io,
1859        License.fromBodyAndType(
1860            LineSplitter.split(io.readString())
1861                .skip(23)
1862                .takeWhile((String s) => !s.startsWith('BoringSSL uses the Chromium test infrastructure to run a continuous build,'))
1863                .join('\n'),
1864            LicenseType.openssl,
1865            origin: io.fullName)) {
1866    _verifyLicense(io);
1867  }
1868
1869  static void _verifyLicense(fs.TextFile io) {
1870    final String contents = io.readString();
1871    if (!contents.contains('BoringSSL is a fork of OpenSSL. As such, large parts of it fall under OpenSSL'))
1872      throw 'unexpected OpenSSL license file contents:\n----8<----\n$contents\n----<8----';
1873  }
1874
1875  @override
1876  License licenseOfType(LicenseType type) {
1877    if (type == LicenseType.openssl)
1878      return license;
1879    return null;
1880  }
1881}
1882
1883class _RepositoryBoringSSLDirectory extends _RepositoryDirectory {
1884  _RepositoryBoringSSLDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1885
1886  @override
1887  _RepositoryFile createFile(fs.IoNode entry) {
1888    if (entry.name == 'README')
1889      return _RepositoryBlankLicenseFile(this, entry, 'This repository contains the files generated by boringssl for its build.');
1890    return super.createFile(entry);
1891  }
1892
1893  @override
1894  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1895    if (entry.name == 'src')
1896      return _RepositoryBoringSSLSourceDirectory(this, entry);
1897    return super.createSubdirectory(entry);
1898  }
1899}
1900
1901class _RepositoryCatapultThirdPartyApiClientDirectory extends _RepositoryDirectory {
1902  _RepositoryCatapultThirdPartyApiClientDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1903
1904  @override
1905  _RepositoryFile createFile(fs.IoNode entry) {
1906    if (entry.name == 'LICENSE')
1907      return _RepositoryCatapultApiClientLicenseFile(this, entry);
1908    return super.createFile(entry);
1909  }
1910}
1911
1912class _RepositoryCatapultThirdPartyCoverageDirectory extends _RepositoryDirectory {
1913  _RepositoryCatapultThirdPartyCoverageDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1914
1915  @override
1916  _RepositoryFile createFile(fs.IoNode entry) {
1917    if (entry.name == 'NOTICE.txt')
1918      return _RepositoryCatapultCoverageLicenseFile(this, entry);
1919    return super.createFile(entry);
1920  }
1921}
1922
1923class _RepositoryCatapultThirdPartyDirectory extends _RepositoryDirectory {
1924  _RepositoryCatapultThirdPartyDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1925
1926  @override
1927  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1928    if (entry.name == 'apiclient')
1929      return _RepositoryCatapultThirdPartyApiClientDirectory(this, entry);
1930    if (entry.name == 'coverage')
1931      return _RepositoryCatapultThirdPartyCoverageDirectory(this, entry);
1932    return super.createSubdirectory(entry);
1933  }
1934}
1935
1936class _RepositoryCatapultDirectory extends _RepositoryDirectory {
1937  _RepositoryCatapultDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1938
1939  @override
1940  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1941    if (entry.name == 'third_party')
1942      return _RepositoryCatapultThirdPartyDirectory(this, entry);
1943    return super.createSubdirectory(entry);
1944  }
1945}
1946
1947class _RepositoryDartRuntimeThirdPartyDirectory extends _RepositoryGenericThirdPartyDirectory {
1948  _RepositoryDartRuntimeThirdPartyDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1949
1950  @override
1951  bool shouldRecurse(fs.IoNode entry) {
1952    return entry.name != 'd3' // Siva says "that is the charting library used by the binary size tool"
1953        && entry.name != 'binary_size' // not linked in either
1954        && super.shouldRecurse(entry);
1955  }
1956}
1957
1958class _RepositoryDartThirdPartyDirectory extends _RepositoryGenericThirdPartyDirectory {
1959  _RepositoryDartThirdPartyDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1960
1961  @override
1962  bool shouldRecurse(fs.IoNode entry) {
1963    return entry.name != 'drt_resources' // test materials
1964        && entry.name != 'firefox_jsshell' // testing tool for dart2js
1965        && entry.name != 'd8' // testing tool for dart2js
1966        && entry.name != 'pkg'
1967        && entry.name != 'pkg_tested'
1968        && entry.name != 'requirejs' // only used by DDC
1969        && super.shouldRecurse(entry);
1970  }
1971
1972  @override
1973  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1974    if (entry.name == 'boringssl')
1975      return _RepositoryBoringSSLDirectory(this, entry);
1976    return super.createSubdirectory(entry);
1977  }
1978}
1979
1980class _RepositoryDartRuntimeDirectory extends _RepositoryDirectory {
1981  _RepositoryDartRuntimeDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1982
1983  @override
1984  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
1985    if (entry.name == 'third_party')
1986      return _RepositoryDartRuntimeThirdPartyDirectory(this, entry);
1987    return super.createSubdirectory(entry);
1988  }
1989}
1990
1991class _RepositoryDartDirectory extends _RepositoryDirectory {
1992  _RepositoryDartDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
1993
1994  @override
1995  bool get isLicenseRoot => true;
1996
1997  @override
1998  _RepositoryFile createFile(fs.IoNode entry) {
1999    if (entry.name == 'LICENSE')
2000      return _RepositoryDartLicenseFile(this, entry);
2001    return super.createFile(entry);
2002  }
2003
2004  @override
2005  bool shouldRecurse(fs.IoNode entry) {
2006    return entry.name != 'pkg' // packages that don't become part of the binary (e.g. the analyzer)
2007        && entry.name != 'tests' // only used by tests, obviously
2008        && entry.name != 'docs' // not shipped in binary
2009        && entry.name != 'build' // not shipped in binary
2010        && entry.name != 'tools' // not shipped in binary
2011        && entry.name != 'samples-dev' // not shipped in binary
2012        && super.shouldRecurse(entry);
2013  }
2014
2015  @override
2016  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
2017    if (entry.name == 'third_party')
2018      return _RepositoryDartThirdPartyDirectory(this, entry);
2019    if (entry.name == 'runtime')
2020      return _RepositoryDartRuntimeDirectory(this, entry);
2021    return super.createSubdirectory(entry);
2022  }
2023}
2024
2025class _RepositoryFlutterDirectory extends _RepositoryDirectory {
2026  _RepositoryFlutterDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
2027
2028  @override
2029  String get libraryName => 'engine';
2030
2031  @override
2032  bool get isLicenseRoot => true;
2033
2034  @override
2035  bool shouldRecurse(fs.IoNode entry) {
2036    return entry.name != 'testing'
2037        && entry.name != 'tools'
2038        && entry.name != 'docs'
2039        && entry.name != 'examples'
2040        && entry.name != 'build'
2041        && entry.name != 'ci'
2042        && entry.name != 'frontend_server'
2043        && super.shouldRecurse(entry);
2044  }
2045
2046  @override
2047  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
2048    if (entry.name == 'sky')
2049      return _RepositoryExcludeSubpathDirectory(this, entry, const <String>['packages', 'sky_engine', 'LICENSE']); // that's the output of this script!
2050    if (entry.name == 'third_party')
2051      return _RepositoryFlutterThirdPartyDirectory(this, entry);
2052    return super.createSubdirectory(entry);
2053  }
2054}
2055
2056class _RepositoryFuchsiaDirectory extends _RepositoryDirectory {
2057  _RepositoryFuchsiaDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
2058
2059  @override
2060  String get libraryName => 'fuchsia_sdk';
2061
2062  @override
2063  bool get isLicenseRoot => true;
2064
2065  @override
2066  bool shouldRecurse(fs.IoNode entry) {
2067    return entry.name != 'toolchain'
2068        && super.shouldRecurse(entry);
2069  }
2070
2071  @override
2072  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
2073    if (entry.name == 'sdk')
2074      return _RepositoryFuchsiaSdkDirectory(this, entry);
2075    return super.createSubdirectory(entry);
2076  }
2077}
2078
2079class _RepositoryFuchsiaSdkDirectory extends _RepositoryDirectory {
2080  _RepositoryFuchsiaSdkDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
2081
2082  @override
2083  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
2084    if (entry.name == 'linux' || entry.name == 'mac')
2085      return _RepositoryFuchsiaSdkLinuxDirectory(this, entry);
2086    return super.createSubdirectory(entry);
2087  }
2088}
2089
2090class _RepositoryFuchsiaSdkLinuxDirectory extends _RepositoryDirectory {
2091  _RepositoryFuchsiaSdkLinuxDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
2092
2093  @override
2094  bool shouldRecurse(fs.IoNode entry) {
2095    return entry.name != '.build-id'
2096        && entry.name != 'docs'
2097        && entry.name != 'images'
2098        && entry.name != 'meta'
2099        && entry.name != 'tools';
2100  }
2101}
2102
2103class _RepositoryFlutterThirdPartyDirectory extends _RepositoryDirectory {
2104  _RepositoryFlutterThirdPartyDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
2105
2106  @override
2107  bool get subdirectoriesAreLicenseRoots => true;
2108
2109  @override
2110  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
2111    if (entry.name == 'txt')
2112      return _RepositoryFlutterTxtDirectory(this, entry);
2113    return super.createSubdirectory(entry);
2114  }
2115}
2116
2117class _RepositoryFlutterTxtDirectory extends _RepositoryDirectory {
2118  _RepositoryFlutterTxtDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
2119
2120  @override
2121  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
2122    if (entry.name == 'third_party')
2123      return _RepositoryFlutterTxtThirdPartyDirectory(this, entry);
2124    return super.createSubdirectory(entry);
2125  }
2126}
2127
2128class _RepositoryFlutterTxtThirdPartyDirectory extends _RepositoryDirectory {
2129  _RepositoryFlutterTxtThirdPartyDirectory(_RepositoryDirectory parent, fs.Directory io) : super(parent, io);
2130
2131  @override
2132  bool shouldRecurse(fs.IoNode entry) {
2133    return entry.name != 'fonts';
2134  }
2135}
2136
2137/// The license tool directory.
2138///
2139/// This is a special-case root node that is not used for license aggregation,
2140/// but simply to compute a signature for the license tool itself. When this
2141/// signature changes, we force re-run license collection for all components in
2142/// order to verify the tool itself still produces the same output.
2143class _RepositoryFlutterLicenseToolDirectory extends _RepositoryDirectory {
2144  _RepositoryFlutterLicenseToolDirectory(fs.Directory io) : super(null, io);
2145
2146  @override
2147  bool shouldRecurse(fs.IoNode entry) {
2148    return entry.name != '.dart_tool'
2149        && entry.name != 'data'
2150        && super.shouldRecurse(entry);
2151  }
2152}
2153
2154class _RepositoryRoot extends _RepositoryDirectory {
2155  _RepositoryRoot(fs.Directory io) : super(null, io);
2156
2157  @override
2158  String get libraryName {
2159    assert(false);
2160    return 'engine';
2161  }
2162
2163  @override
2164  bool get isLicenseRoot => true;
2165
2166  @override
2167  bool get subdirectoriesAreLicenseRoots => true;
2168
2169  @override
2170  bool shouldRecurse(fs.IoNode entry) {
2171    return entry.name != 'testing' // only used by tests
2172        && entry.name != 'build' // only used by build
2173        && entry.name != 'buildtools' // only used by build
2174        && entry.name != 'build_overrides' // only used by build
2175        && entry.name != 'ios_tools' // only used by build
2176        && entry.name != 'tools' // not distributed in binary
2177        && entry.name != 'out' // output of build
2178        && super.shouldRecurse(entry);
2179  }
2180
2181  @override
2182  _RepositoryDirectory createSubdirectory(fs.Directory entry) {
2183    if (entry.name == 'base')
2184      throw '//base is no longer part of this client: remove it';
2185    if (entry.name == 'third_party')
2186      return _RepositoryRootThirdPartyDirectory(this, entry);
2187    if (entry.name == 'flutter')
2188      return _RepositoryFlutterDirectory(this, entry);
2189    if (entry.name == 'fuchsia')
2190      return _RepositoryFuchsiaDirectory(this, entry);
2191    return super.createSubdirectory(entry);
2192  }
2193
2194  @override
2195  List<_RepositoryDirectory> get virtualSubdirectories {
2196    // Skia is updated more frequently than other third party libraries and
2197    // is therefore represented as a separate top-level component.
2198    final fs.Directory thirdPartyNode = io.walk.firstWhere((fs.IoNode node) => node.name == 'third_party');
2199    final fs.IoNode skiaNode = thirdPartyNode.walk.firstWhere((fs.IoNode node) => node.name == 'skia');
2200    return <_RepositoryDirectory>[_RepositorySkiaDirectory(this, skiaNode)];
2201  }
2202}
2203
2204
2205class _Progress {
2206  _Progress(this.max) {
2207    // This may happen when a git client contains left-over empty component
2208    // directories after DEPS file changes.
2209    if (max <= 0)
2210      throw ArgumentError('Progress.max must be > 0 but was: $max');
2211  }
2212
2213  final int max;
2214  int get withLicense => _withLicense;
2215  int _withLicense = 0;
2216  int get withoutLicense => _withoutLicense;
2217  int _withoutLicense = 0;
2218  String get label => _label;
2219  String _label = '';
2220  int _lastLength = 0;
2221  set label(String value) {
2222    if (value.length > 50)
2223      value = '.../' + value.substring(math.max(0, value.lastIndexOf('/', value.length - 45) + 1));
2224    if (_label != value) {
2225      _label = value;
2226      update();
2227    }
2228  }
2229  void advance({@required bool success}) {
2230    assert(success != null);
2231    if (success)
2232      _withLicense += 1;
2233    else
2234      _withoutLicense += 1;
2235    update();
2236  }
2237  Stopwatch _lastUpdate;
2238  void update({bool flush = false}) {
2239    if (_lastUpdate == null || _lastUpdate.elapsedMilliseconds > 90 || flush) {
2240      _lastUpdate ??= Stopwatch();
2241      final String line = toString();
2242      system.stderr.write('\r$line');
2243      if (_lastLength > line.length)
2244        system.stderr.write(' ' * (_lastLength - line.length));
2245      _lastLength = line.length;
2246      _lastUpdate.reset();
2247      _lastUpdate.start();
2248    }
2249  }
2250  void flush() => update(flush: true);
2251  bool get hadErrors => _withoutLicense > 0;
2252  @override
2253  String toString() {
2254    final int percent = (100.0 * (_withLicense + _withoutLicense) / max).round();
2255    return '${(_withLicense + _withoutLicense).toString().padLeft(10)} of $max ${'█' * (percent ~/ 10)}${'░' * (10 - (percent ~/ 10))} $percent% ($_withoutLicense missing licenses)  $label';
2256  }
2257}
2258
2259/// Reads the signature from a golden file.
2260Future<String> _readSignature(String goldenPath) async {
2261  try {
2262    final system.File goldenFile = system.File(goldenPath);
2263    final String goldenSignature = await utf8.decoder.bind(goldenFile.openRead())
2264        .transform(const LineSplitter()).first;
2265    final RegExp signaturePattern = RegExp(r'Signature: (\w+)');
2266    final Match goldenMatch = signaturePattern.matchAsPrefix(goldenSignature);
2267    if (goldenMatch != null)
2268      return goldenMatch.group(1);
2269  } on system.FileSystemException {
2270    system.stderr.writeln('    Failed to read signature file.');
2271    return null;
2272  }
2273  return null;
2274}
2275
2276/// Writes a signature to an [system.IOSink] in the expected format.
2277void _writeSignature(String signature, system.IOSink sink) {
2278  if (signature != null)
2279    sink.writeln('Signature: $signature\n');
2280}
2281
2282// Checks for changes to the license tool itself.
2283//
2284// Returns true if changes are detected.
2285Future<bool> _computeLicenseToolChanges(_RepositoryDirectory root, {String goldenSignaturePath, String outputSignaturePath}) async {
2286  system.stderr.writeln('Computing signature for license tool');
2287  final fs.Directory flutterNode = root.io.walk.firstWhere((fs.IoNode node) => node.name == 'flutter');
2288  final fs.Directory toolsNode = flutterNode.walk.firstWhere((fs.IoNode node) => node.name == 'tools');
2289  final fs.Directory licenseNode = toolsNode.walk.firstWhere((fs.IoNode node) => node.name == 'licenses');
2290  final _RepositoryFlutterLicenseToolDirectory licenseToolDirectory = _RepositoryFlutterLicenseToolDirectory(licenseNode);
2291
2292  final String toolSignature = await licenseToolDirectory.signature;
2293  final system.IOSink sink = system.File(outputSignaturePath).openWrite();
2294  _writeSignature(toolSignature, sink);
2295  await sink.close();
2296
2297  final String goldenSignature = await _readSignature(goldenSignaturePath);
2298  return toolSignature != goldenSignature;
2299}
2300
2301/// Collects licenses for the specified component.
2302///
2303/// If [writeSignature] is set, the signature is written to the output file.
2304/// If [force] is set, collection is run regardless of whether or not the signature matches.
2305Future<void> _collectLicensesForComponent(_RepositoryDirectory componentRoot, {
2306  String inputGoldenPath,
2307  String outputGoldenPath,
2308  bool writeSignature,
2309  bool force,
2310}) async {
2311  // Check whether the golden file matches the signature of the current contents of this directory.
2312  final String goldenSignature = await _readSignature(inputGoldenPath);
2313  final String signature = await componentRoot.signature;
2314  if (!force && goldenSignature == signature) {
2315    system.stderr.writeln('    Skipping this component - no change in signature');
2316    return;
2317  }
2318
2319  final _Progress progress = _Progress(componentRoot.fileCount);
2320
2321  final system.File outFile = system.File(outputGoldenPath);
2322  final system.IOSink sink = outFile.openWrite();
2323  if (writeSignature)
2324    _writeSignature(signature, sink);
2325
2326  final List<License> licenses = Set<License>.from(componentRoot.getLicenses(progress).toList()).toList();
2327
2328  if (progress.hadErrors)
2329    throw 'Had failures while collecting licenses.';
2330
2331  sink.writeln('UNUSED LICENSES:\n');
2332  final List<String> unusedLicenses = licenses
2333    .where((License license) => !license.isUsed)
2334    .map((License license) => license.toString())
2335    .toList();
2336  unusedLicenses.sort();
2337  sink.writeln(unusedLicenses.join('\n\n'));
2338  sink.writeln('~' * 80);
2339
2340  sink.writeln('USED LICENSES:\n');
2341  final List<License> usedLicenses = licenses.where((License license) => license.isUsed).toList();
2342  final List<String> output = usedLicenses.map((License license) => license.toString()).toList();
2343  for (int index = 0; index < output.length; index += 1) {
2344    // The strings we look for here are strings which we do not expect to see in
2345    // any of the licenses we use. They either represent examples of misparsing
2346    // licenses (issues we've previously run into and fixed), or licenses we
2347    // know we are trying to avoid (e.g. the GPL, or licenses that only apply to
2348    // test content which shouldn't get built at all).
2349    // If you find that one of these tests is getting hit, and it's not obvious
2350    // to you why the relevant license is a problem, please ask around (e.g. try
2351    // asking Hixie). Do not merely remove one of these checks, sometimes the
2352    // issues involved are relatively subtle.
2353    if (output[index].contains('Version: MPL 1.1/GPL 2.0/LGPL 2.1'))
2354      throw 'Unexpected trilicense block found in: ${usedLicenses[index].origin}';
2355    if (output[index].contains('The contents of this file are subject to the Mozilla Public License Version'))
2356      throw 'Unexpected MPL block found in: ${usedLicenses[index].origin}';
2357    if (output[index].contains('You should have received a copy of the GNU'))
2358      throw 'Unexpected GPL block found in: ${usedLicenses[index].origin}';
2359    if (output[index].contains('BoringSSL is a fork of OpenSSL'))
2360      throw 'Unexpected legacy BoringSSL block found in: ${usedLicenses[index].origin}';
2361    if (output[index].contains('Contents of this folder are ported from'))
2362      throw 'Unexpected block found in: ${usedLicenses[index].origin}';
2363    if (output[index].contains('https://github.com/w3c/web-platform-tests/tree/master/selectors-api'))
2364      throw 'Unexpected W3C content found in: ${usedLicenses[index].origin}';
2365    if (output[index].contains('http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html'))
2366      throw 'Unexpected W3C copyright found in: ${usedLicenses[index].origin}';
2367    if (output[index].contains('It is based on commit'))
2368      throw 'Unexpected content found in: ${usedLicenses[index].origin}';
2369    if (output[index].contains('The original code is covered by the dual-licensing approach described in:'))
2370      throw 'Unexpected old license reference found in: ${usedLicenses[index].origin}';
2371    if (output[index].contains('must choose'))
2372      throw 'Unexpected indecisiveness found in: ${usedLicenses[index].origin}';
2373  }
2374
2375  output.sort();
2376  sink.writeln(output.join('\n\n'));
2377  sink.writeln('Total license count: ${licenses.length}');
2378
2379  await sink.close();
2380  progress.label = 'Done.';
2381  progress.flush();
2382  system.stderr.writeln('');
2383}
2384
2385
2386// MAIN
2387
2388Future<void> main(List<String> arguments) async {
2389  final ArgParser parser = ArgParser()
2390    ..addOption('src', help: 'The root of the engine source')
2391    ..addOption('out', help: 'The directory where output is written')
2392    ..addOption('golden', help: 'The directory containing golden results')
2393    ..addFlag('release', help: 'Print output in the format used for product releases');
2394
2395  final ArgResults argResults = parser.parse(arguments);
2396  final bool releaseMode = argResults['release'];
2397  if (argResults['src'] == null) {
2398    print('Flutter license script: Must provide --src directory');
2399    print(parser.usage);
2400    system.exit(1);
2401  }
2402  if (!releaseMode) {
2403    if (argResults['out'] == null || argResults['golden'] == null) {
2404      print('Flutter license script: Must provide --out and --golden directories in non-release mode');
2405      print(parser.usage);
2406      system.exit(1);
2407    }
2408    if (!system.FileSystemEntity.isDirectorySync(argResults['golden'])) {
2409      print('Flutter license script: Golden directory does not exist');
2410      print(parser.usage);
2411      system.exit(1);
2412    }
2413    final system.Directory out = system.Directory(argResults['out']);
2414    if (!out.existsSync())
2415      out.createSync(recursive: true);
2416  }
2417
2418  try {
2419    system.stderr.writeln('Finding files...');
2420    final fs.FileSystemDirectory rootDirectory = fs.FileSystemDirectory.fromPath(argResults['src']);
2421    final _RepositoryDirectory root = _RepositoryRoot(rootDirectory);
2422
2423    if (releaseMode) {
2424      system.stderr.writeln('Collecting licenses...');
2425      final _Progress progress = _Progress(root.fileCount);
2426      final List<License> licenses = Set<License>.from(root.getLicenses(progress).toList()).toList();
2427      if (progress.hadErrors)
2428        throw 'Had failures while collecting licenses.';
2429      progress.label = 'Dumping results...';
2430      progress.flush();
2431      final List<String> output = licenses
2432        .where((License license) => license.isUsed)
2433        .map((License license) => license.toStringFormal())
2434        .where((String text) => text != null)
2435        .toList();
2436      output.sort();
2437      print(output.join('\n${"-" * 80}\n'));
2438      progress.label = 'Done.';
2439      progress.flush();
2440      system.stderr.writeln('');
2441    } else {
2442      // If changes are detected to the license tool itself, force collection
2443      // for all components in order to check we're still generating correct
2444      // output.
2445      const String toolSignatureFilename = 'tool_signature';
2446      final bool forceRunAll = await _computeLicenseToolChanges(
2447          root,
2448          goldenSignaturePath: path.join(argResults['golden'], toolSignatureFilename),
2449          outputSignaturePath: path.join(argResults['out'], toolSignatureFilename),
2450      );
2451      if (forceRunAll)
2452        system.stderr.writeln('    Detected changes to license tool. Forcing license collection for all components.');
2453
2454      final List<String> usedGoldens = <String>[];
2455      bool isFirstComponent = true;
2456      for (_RepositoryDirectory component in root.subdirectories) {
2457        system.stderr.writeln('Collecting licenses for ${component.io.name}');
2458
2459        _RepositoryDirectory componentRoot;
2460        if (isFirstComponent) {
2461          // For the first component, we can use the results of the initial repository crawl.
2462          isFirstComponent = false;
2463          componentRoot = component;
2464        } else {
2465          // For other components, we need a clean repository that does not
2466          // contain any state left over from previous components.
2467          clearLicenseRegistry();
2468          componentRoot = _RepositoryRoot(rootDirectory).subdirectories
2469              .firstWhere((_RepositoryDirectory dir) => dir.name == component.name);
2470        }
2471
2472        // Always run the full license check on the flutter tree. The flutter
2473        // tree is relatively small and changes frequently in ways that do not
2474        // affect the license output, and we don't want to require updates to
2475        // the golden signature for those changes.
2476        final String goldenFileName = 'licenses_${component.io.name}';
2477        await _collectLicensesForComponent(
2478            componentRoot,
2479            inputGoldenPath: path.join(argResults['golden'], goldenFileName),
2480            outputGoldenPath: path.join(argResults['out'], goldenFileName),
2481            writeSignature: component.io.name != 'flutter',
2482            force: forceRunAll || component.io.name == 'flutter',
2483        );
2484        usedGoldens.add(goldenFileName);
2485      }
2486
2487      final Set<String> unusedGoldens = system.Directory(argResults['golden']).listSync()
2488        .map((system.FileSystemEntity file) => path.basename(file.path)).toSet()
2489        ..removeAll(usedGoldens)
2490        ..remove(toolSignatureFilename);
2491      if (unusedGoldens.isNotEmpty) {
2492        system.stderr.writeln('The following golden files in ${argResults['golden']} are unused and need to be deleted:');
2493        unusedGoldens.map((String s) => ' * $s').forEach(system.stderr.writeln);
2494        system.exit(1);
2495      }
2496    }
2497  } catch (e, stack) {
2498    system.stderr.writeln('failure: $e\n$stack');
2499    system.stderr.writeln('aborted.');
2500    system.exit(1);
2501  }
2502}
2503