dartdoc.dart 21.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';
import 'dart:io';

8
import 'package:args/args.dart';
9
import 'package:intl/intl.dart';
10
import 'package:meta/meta.dart';
11
import 'package:path/path.dart' as path;
12
import 'package:platform/platform.dart';
13
import 'package:process/process.dart';
14

15 16
import 'dartdoc_checker.dart';

17 18
const String kDocsRoot = 'dev/docs';
const String kPublishRoot = '$kDocsRoot/doc';
Seth Ladd's avatar
Seth Ladd committed
19

20 21 22
const String kDummyPackageName = 'Flutter';
const String kPlatformIntegrationPackageName = 'platform_integration';

23 24 25
/// This script expects to run with the cwd as the root of the flutter repo. It
/// will generate documentation for the packages in `//packages/` and write the
/// documentation to `//dev/docs/doc/api/`.
26
///
Seth Ladd's avatar
Seth Ladd committed
27
/// This script also updates the index.html file so that it can be placed
28 29
/// at the root of api.flutter.dev. We are keeping the files inside of
/// api.flutter.dev/flutter for now, so we need to manipulate paths
Seth Ladd's avatar
Seth Ladd committed
30
/// a bit. See https://github.com/flutter/flutter/issues/3900 for more info.
31 32 33 34 35
///
/// This will only work on UNIX systems, not Windows. It requires that 'git' be
/// in your path. It requires that 'flutter' has been run previously. It uses
/// the version of Dart downloaded by the 'flutter' tool in this repository and
/// will crash if that is absent.
36
Future<void> main(List<String> arguments) async {
37 38
  final ArgParser argParser = _createArgsParser();
  final ArgResults args = argParser.parse(arguments);
39
  if (args['help'] as bool) {
40 41 42 43
    print ('Usage:');
    print (argParser.usage);
    exit(0);
  }
44
  // If we're run from the `tools` dir, set the cwd to the repo root.
45
  if (path.basename(Directory.current.path) == 'tools') {
46
    Directory.current = Directory.current.parent.parent;
47
  }
48

49
  final ProcessResult flutter = Process.runSync('flutter', <String>[]);
50
  final File versionFile = File('version');
51
  if (flutter.exitCode != 0 || !versionFile.existsSync()) {
52
    throw Exception('Failed to determine Flutter version.');
53
  }
54
  final String version = versionFile.readAsStringSync();
55

56
  // Create the pubspec.yaml file.
57
  final StringBuffer buf = StringBuffer();
58
  buf.writeln('name: $kDummyPackageName');
59
  buf.writeln('homepage: https://flutter.dev');
60
  buf.writeln('version: 0.0.0');
61
  buf.writeln('environment:');
62
  buf.writeln("  sdk: '>=3.0.0-0 <4.0.0'");
63
  buf.writeln('dependencies:');
64
  for (final String package in findPackageNames()) {
65
    buf.writeln('  $package:');
66
    buf.writeln('    sdk: flutter');
67
  }
68
  buf.writeln('  $kPlatformIntegrationPackageName: 0.0.1');
69
  buf.writeln('dependency_overrides:');
70 71
  buf.writeln('  $kPlatformIntegrationPackageName:');
  buf.writeln('    path: $kPlatformIntegrationPackageName');
72
  File('$kDocsRoot/pubspec.yaml').writeAsStringSync(buf.toString());
73 74

  // Create the library file.
75
  final Directory libDir = Directory('$kDocsRoot/lib');
76 77
  libDir.createSync();

78
  final StringBuffer contents = StringBuffer('library temp_doc;\n\n');
79
  for (final String libraryRef in libraryRefs()) {
80
    contents.writeln("import 'package:$libraryRef';");
81
  }
82
  File('$kDocsRoot/lib/temp_doc.dart').writeAsStringSync(contents.toString());
83

84 85 86 87 88 89 90
  final String flutterRoot = Directory.current.path;
  final Map<String, String> pubEnvironment = <String, String>{
    'FLUTTER_ROOT': flutterRoot,
  };

  // If there's a .pub-cache dir in the flutter root, use that.
  final String pubCachePath = '$flutterRoot/.pub-cache';
91
  if (Directory(pubCachePath).existsSync()) {
92 93 94
    pubEnvironment['PUB_CACHE'] = pubCachePath;
  }

95
  final String dartExecutable = '$flutterRoot/bin/cache/dart-sdk/bin/dart';
96

97
  // Run pub.
98 99 100
  ProcessWrapper process = ProcessWrapper(await runPubProcess(
    dartBinaryPath: dartExecutable,
    arguments: <String>['get'],
101
    workingDirectory: kDocsRoot,
102
    environment: pubEnvironment,
103
  ));
104 105
  printStream(process.stdout, prefix: 'pub:stdout: ');
  printStream(process.stderr, prefix: 'pub:stderr: ');
106
  final int code = await process.done;
107
  if (code != 0) {
108
    exit(code);
109
  }
110

111
  createFooter('$kDocsRoot/lib/', version);
112
  copyAssets();
113
  createSearchMetadata('$kDocsRoot/lib/opensearch.xml', '$kDocsRoot/doc/opensearch.xml');
114
  cleanOutSnippets();
115

116 117 118
  final List<String> dartdocBaseArgs = <String>[
    'global',
    'run',
119
    if (args['checked'] as bool) '--enable-asserts',
120 121
    'dartdoc',
  ];
122

123 124
  // Verify which version of snippets and dartdoc we're using.
  final ProcessResult snippetsResult = Process.runSync(
125
    dartExecutable,
126
    <String>[
127
      'pub',
128 129 130
      'global',
      'list',
    ],
131
    workingDirectory: kDocsRoot,
132
    environment: pubEnvironment,
133
    stdoutEncoding: utf8,
134
  );
135 136 137 138 139 140 141 142
  print('');
  final Iterable<RegExpMatch> versionMatches = RegExp(r'^(?<name>snippets|dartdoc) (?<version>[^\s]+)', multiLine: true)
      .allMatches(snippetsResult.stdout as String);
  for (final RegExpMatch match in versionMatches) {
    print('${match.namedGroup('name')} version: ${match.namedGroup('version')}');
  }

  print('flutter version: $version\n');
143

144 145 146 147 148 149
  // Dartdoc warnings and errors in these packages are considered fatal.
  // All packages owned by flutter should be in the list.
  final List<String> flutterPackages = <String>[
    kDummyPackageName,
    kPlatformIntegrationPackageName,
    ...findPackageNames(),
150 151
    // TODO(goderbauer): Figure out how to only include `dart:ui` of `sky_engine` below, https://github.com/dart-lang/dartdoc/issues/2278.
    // 'sky_engine',
152
  ];
153

154
  // Generate the documentation.
155 156
  // We don't need to exclude flutter_tools in this list because it's not in the
  // recursive dependencies of the package defined at dev/docs/pubspec.yaml
157 158
  final List<String> dartdocArgs = <String>[
    ...dartdocBaseArgs,
159
    '--allow-tools',
160 161
    if (args['json'] as bool) '--json',
    if (args['validate-links'] as bool) '--validate-links' else '--no-validate-links',
162 163 164
    '--link-to-source-excludes', '../../bin/cache',
    '--link-to-source-root', '../..',
    '--link-to-source-uri-template', 'https://github.com/flutter/flutter/blob/master/%f%#L%l%',
165
    '--inject-html',
166
    '--use-base-href',
167 168
    '--header', 'styles.html',
    '--header', 'analytics.html',
169
    '--header', 'survey.html',
170
    '--header', 'snippets.html',
171
    '--header', 'opensearch.html',
172
    '--footer-text', 'lib/footer.html',
173
    '--allow-warnings-in-packages', flutterPackages.join(','),
174
    '--exclude-packages',
175 176 177 178 179 180
    <String>[
      'analyzer',
      'args',
      'barback',
      'csslib',
      'flutter_goldens',
181
      'flutter_goldens_client',
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
      'front_end',
      'fuchsia_remote_debug_protocol',
      'glob',
      'html',
      'http_multi_server',
      'io',
      'isolate',
      'js',
      'kernel',
      'logging',
      'mime',
      'mockito',
      'node_preamble',
      'plugin',
      'shelf',
      'shelf_packages_handler',
      'shelf_static',
      'shelf_web_socket',
      'utf',
      'watcher',
      'yaml',
    ].join(','),
204
    '--exclude',
205
    <String>[
206
      'dart:io/network_policy.dart', // dart-lang/dartdoc#2437
207 208 209 210 211 212 213 214 215
      'package:Flutter/temp_doc.dart',
      'package:http/browser_client.dart',
      'package:intl/intl_browser.dart',
      'package:matcher/mirror_matchers.dart',
      'package:quiver/io.dart',
      'package:quiver/mirrors.dart',
      'package:vm_service_client/vm_service_client.dart',
      'package:web_socket_channel/html.dart',
    ].join(','),
216
    '--favicon=favicon.ico',
217
    '--package-order', 'flutter,Dart,$kPlatformIntegrationPackageName,flutter_test,flutter_driver',
218
    '--auto-include-dependencies',
219
  ];
220

221
  String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg;
222
  print('Executing: (cd $kDocsRoot ; $dartExecutable ${dartdocArgs.map<String>(quote).join(' ')})');
223

224 225 226
  process = ProcessWrapper(await runPubProcess(
    dartBinaryPath: dartExecutable,
    arguments: dartdocArgs,
227
    workingDirectory: kDocsRoot,
228
    environment: pubEnvironment,
229
  ));
230 231
  printStream(process.stdout, prefix: args['json'] as bool ? '' : 'dartdoc:stdout: ',
    filter: args['verbose'] as bool ? const <Pattern>[] : <Pattern>[
232
      RegExp(r'^Generating docs for library '), // unnecessary verbosity
233 234
    ],
  );
235 236
  printStream(process.stderr, prefix: args['json'] as bool ? '' : 'dartdoc:stderr: ',
    filter: args['verbose'] as bool ? const <Pattern>[] : <Pattern>[
237
      RegExp(r'^ warning: .+: \(.+/\.pub-cache/hosted/pub.dartlang.org/.+\)'), // packages outside our control
238 239
    ],
  );
240
  final int exitCode = await process.done;
Seth Ladd's avatar
Seth Ladd committed
241

242
  if (exitCode != 0) {
Seth Ladd's avatar
Seth Ladd committed
243
    exit(exitCode);
244
  }
Seth Ladd's avatar
Seth Ladd committed
245

246
  sanityCheckDocs();
247
  checkForUnresolvedDirectives('$kPublishRoot/api');
248

Seth Ladd's avatar
Seth Ladd committed
249 250 251
  createIndexAndCleanup();
}

252
ArgParser _createArgsParser() {
253
  final ArgParser parser = ArgParser();
254 255
  parser.addFlag('help', abbr: 'h', negatable: false,
      help: 'Show command help.');
256
  parser.addFlag('verbose', defaultsTo: true,
257
      help: 'Whether to report all error messages (on) or attempt to '
258
          'filter out some known false positives (off). Shut this off '
259
          'locally if you want to address Flutter-specific issues.');
260
  parser.addFlag('checked', abbr: 'c',
261
      help: 'Run dartdoc with asserts enabled.');
262
  parser.addFlag('json',
263
      help: 'Display json-formatted output from dartdoc and skip stdout/stderr prefixing.');
264
  parser.addFlag('validate-links',
265
      help: 'Display warnings for broken links generated by dartdoc (slow)');
266 267 268
  return parser;
}

269
final RegExp gitBranchRegexp = RegExp(r'^## (.*)');
270

271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
/// Get the name of the release branch.
///
/// On LUCI builds, the git HEAD is detached, so first check for the env
/// variable "LUCI_BRANCH"; if it is not set, fall back to calling git.
String getBranchName({
  @visibleForTesting
  Platform platform = const LocalPlatform(),
  @visibleForTesting
  ProcessManager processManager = const LocalProcessManager(),
}) {
  final String? luciBranch = platform.environment['LUCI_BRANCH'];
  if (luciBranch != null && luciBranch.trim().isNotEmpty) {
    return luciBranch.trim();
  }
  final ProcessResult gitResult = processManager.runSync(<String>['git', 'status', '-b', '--porcelain']);
286
  if (gitResult.exitCode != 0) {
287
    throw 'git status exit with non-zero exit code: ${gitResult.exitCode}';
288
  }
289
  final RegExpMatch? gitBranchMatch = gitBranchRegexp.firstMatch(
290
      (gitResult.stdout as String).trim().split('\n').first);
291
  return gitBranchMatch == null ? '' : gitBranchMatch.group(1)!.split('...').first;
292 293
}

294
String gitRevision() {
295 296
  const int kGitRevisionLength = 10;

297
  final ProcessResult gitResult = Process.runSync('git', <String>['rev-parse', 'HEAD']);
298
  if (gitResult.exitCode != 0) {
299
    throw 'git rev-parse exit with non-zero exit code: ${gitResult.exitCode}';
300
  }
301
  final String gitRevision = (gitResult.stdout as String).trim();
302

303 304
  return gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision;
}
305

306
void createFooter(String footerPath, String version) {
307
  final String timestamp = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now());
308
  final String gitBranch = getBranchName();
309 310 311 312
  final String gitBranchOut = gitBranch.isEmpty ? '' : '• $gitBranch';
  File('${footerPath}footer.html').writeAsStringSync('<script src="footer.js"></script>');
  File('$kPublishRoot/api/footer.js')
    ..createSync(recursive: true)
313 314
    ..writeAsStringSync('''
(function() {
Dan Field's avatar
Dan Field committed
315 316 317 318 319 320 321 322 323
  var span = document.querySelector('footer>span');
  if (span) {
    span.innerText = 'Flutter $version  $timestamp  ${gitRevision()} $gitBranchOut';
  }
  var sourceLink = document.querySelector('a.source-link');
  if (sourceLink) {
    sourceLink.href = sourceLink.href.replace('/master/', '/${gitRevision()}/');
  }
})();
324
''');
325 326
}

327 328 329 330 331 332 333 334 335
/// Generates an OpenSearch XML description that can be used to add a custom
/// search for Flutter API docs to the browser. Unfortunately, it has to know
/// the URL to which site to search, so we customize it here based upon the
/// branch name.
void createSearchMetadata(String templatePath, String metadataPath) {
  final String template = File(templatePath).readAsStringSync();
  final String branch = getBranchName();
  final String metadata = template.replaceAll(
    '{SITE_URL}',
336
    branch == 'stable' ? 'https://api.flutter.dev/' : 'https://master-api.flutter.dev/',
337 338 339 340 341
  );
  Directory(path.dirname(metadataPath)).create(recursive: true);
  File(metadataPath).writeAsStringSync(metadata);
}

342 343 344 345
/// Recursively copies `srcDir` to `destDir`, invoking [onFileCopied], if
/// specified, for each source/destination file pair.
///
/// Creates `destDir` if needed.
346
void copyDirectorySync(Directory srcDir, Directory destDir, [void Function(File srcFile, File destFile)? onFileCopied]) {
347
  if (!srcDir.existsSync()) {
348
    throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy');
349
  }
350

351
  if (!destDir.existsSync()) {
352
    destDir.createSync(recursive: true);
353
  }
354

355
  for (final FileSystemEntity entity in srcDir.listSync()) {
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
    final String newPath = path.join(destDir.path, path.basename(entity.path));
    if (entity is File) {
      final File newFile = File(newPath);
      entity.copySync(newPath);
      onFileCopied?.call(entity, newFile);
    } else if (entity is Directory) {
      copyDirectorySync(entity, Directory(newPath));
    } else {
      throw Exception('${entity.path} is neither File nor Directory');
    }
  }
}

void copyAssets() {
  final Directory assetsDir = Directory(path.join(kPublishRoot, 'assets'));
  if (assetsDir.existsSync()) {
    assetsDir.deleteSync(recursive: true);
  }
  copyDirectorySync(
      Directory(path.join(kDocsRoot, 'assets')),
      Directory(path.join(kPublishRoot, 'assets')),
          (File src, File dest) => print('Copied ${src.path} to ${dest.path}'));
}

380 381
/// Clean out any existing snippets so that we don't publish old files from
/// previous runs accidentally.
382 383 384 385 386 387 388 389 390
void cleanOutSnippets() {
  final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets'));
  if (snippetsDir.existsSync()) {
    snippetsDir
      ..deleteSync(recursive: true)
      ..createSync(recursive: true);
  }
}

391 392
void _sanityCheckExample(String fileString, String regExpString) {
  final File file = File(fileString);
393
  if (file.existsSync()) {
394 395 396
    final RegExp regExp = RegExp(regExpString, dotAll: true);
    final String contents = file.readAsStringSync();
    if (!regExp.hasMatch(contents)) {
397
      throw Exception("Missing example code matching '$regExpString' in ${file.path}.");
398 399
    }
  } else {
400 401
    throw Exception(
        "Missing example code sanity test file ${file.path}. Either it didn't get published, or you might have to update the test to look at a different file.");
402 403 404 405
  }
}

/// Runs a sanity check by running a test.
406
void sanityCheckDocs([Platform platform = const LocalPlatform()]) {
407
  final List<String> canaries = <String>[
408 409 410 411 412 413 414 415 416
    '$kPublishRoot/assets/overrides.css',
    '$kPublishRoot/api/dart-io/File-class.html',
    '$kPublishRoot/api/dart-ui/Canvas-class.html',
    '$kPublishRoot/api/dart-ui/Canvas/drawRect.html',
    '$kPublishRoot/api/flutter_driver/FlutterDriver/FlutterDriver.connectedTo.html',
    '$kPublishRoot/api/flutter_test/WidgetTester/pumpWidget.html',
    '$kPublishRoot/api/material/Material-class.html',
    '$kPublishRoot/api/material/Tooltip-class.html',
    '$kPublishRoot/api/widgets/Widget-class.html',
417
    '$kPublishRoot/api/widgets/Listener-class.html',
418
  ];
419
  for (final String canary in canaries) {
420
    if (!File(canary).existsSync()) {
421
      throw Exception('Missing "$canary", which probably means the documentation failed to build correctly.');
422
    }
423
  }
424 425 426
  // Make sure at least one example of each kind includes source code.

  // Check a "sample" example, any one will do.
427 428 429 430
  _sanityCheckExample(
    '$kPublishRoot/api/widgets/showGeneralDialog.html',
    r'\s*<pre\s+id="longSnippet1".*<code\s+class="language-dart">\s*import &#39;package:flutter&#47;material.dart&#39;;',
  );
431 432

  // Check a "snippet" example, any one will do.
433 434 435 436
  _sanityCheckExample(
    '$kPublishRoot/api/widgets/ModalRoute/barrierColor.html',
    r'\s*<pre.*id="sample-code">.*Color\s+get\s+barrierColor.*</pre>',
  );
437

438 439
  // Check a "dartpad" example, any one will do, and check for the correct URL
  // arguments.
Lioness100's avatar
Lioness100 committed
440
  // Just use "master" for any branch other than the LUCI_BRANCH.
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
  final String? luciBranch = platform.environment['LUCI_BRANCH']?.trim();
  final String expectedBranch = luciBranch != null && luciBranch.isNotEmpty ? luciBranch : 'master';
  final List<String> argumentRegExps = <String>[
    r'split=\d+',
    r'run=true',
    r'sample_id=widgets\.Listener\.\d+',
    'sample_channel=$expectedBranch',
    'channel=$expectedBranch',
  ];
  for (final String argumentRegExp in argumentRegExps) {
    _sanityCheckExample(
      '$kPublishRoot/api/widgets/Listener-class.html',
      r'\s*<iframe\s+class="snippet-dartpad"\s+src="'
      r'https:\/\/dartpad.dev\/embed-flutter.html\?.*?\b'
      '$argumentRegExp'
      r'\b.*">\s*<\/iframe>',
    );
  }
459 460
}

Seth Ladd's avatar
Seth Ladd committed
461 462 463
/// Creates a custom index.html because we try to maintain old
/// paths. Cleanup unused index.html files no longer needed.
void createIndexAndCleanup() {
464
  print('\nCreating a custom index.html in $kPublishRoot/index.html');
465
  removeOldFlutterDocsDir();
Seth Ladd's avatar
Seth Ladd committed
466 467 468
  renameApiDir();
  copyIndexToRootOfDocs();
  addHtmlBaseToIndex();
469
  changePackageToSdkInTitlebar();
Seth Ladd's avatar
Seth Ladd committed
470
  putRedirectInOldIndexLocation();
471
  writeSnippetsIndexFile();
Seth Ladd's avatar
Seth Ladd committed
472 473 474
  print('\nDocs ready to go!');
}

475 476
void removeOldFlutterDocsDir() {
  try {
477
    Directory('$kPublishRoot/flutter').deleteSync(recursive: true);
478
  } on FileSystemException {
479 480 481 482
    // If the directory does not exist, that's OK.
  }
}

Seth Ladd's avatar
Seth Ladd committed
483
void renameApiDir() {
484
  Directory('$kPublishRoot/api').renameSync('$kPublishRoot/flutter');
Seth Ladd's avatar
Seth Ladd committed
485 486
}

487
void copyIndexToRootOfDocs() {
488
  File('$kPublishRoot/flutter/index.html').copySync('$kPublishRoot/index.html');
Seth Ladd's avatar
Seth Ladd committed
489 490
}

491
void changePackageToSdkInTitlebar() {
492
  final File indexFile = File('$kPublishRoot/index.html');
493 494
  String indexContents = indexFile.readAsStringSync();
  indexContents = indexContents.replaceFirst(
495 496
    '<li><a href="https://flutter.dev">Flutter package</a></li>',
    '<li><a href="https://flutter.dev">Flutter SDK</a></li>',
497 498 499 500 501
  );

  indexFile.writeAsStringSync(indexContents);
}

Seth Ladd's avatar
Seth Ladd committed
502
void addHtmlBaseToIndex() {
503
  final File indexFile = File('$kPublishRoot/index.html');
Seth Ladd's avatar
Seth Ladd committed
504
  String indexContents = indexFile.readAsStringSync();
505 506 507 508
  indexContents = indexContents.replaceFirst(
    '</title>\n',
    '</title>\n  <base href="./flutter/">\n',
  );
509 510
  indexContents = indexContents.replaceAll(
    'href="Android/Android-library.html"',
511
    'href="/javadoc/"',
512
  );
513 514
  indexContents = indexContents.replaceAll(
      'href="iOS/iOS-library.html"',
515
      'href="/objcdoc/"',
516 517
  );

Seth Ladd's avatar
Seth Ladd committed
518 519 520 521
  indexFile.writeAsStringSync(indexContents);
}

void putRedirectInOldIndexLocation() {
522
  const String metaTag = '<meta http-equiv="refresh" content="0;URL=../index.html">';
523
  File('$kPublishRoot/flutter/index.html').writeAsStringSync(metaTag);
524 525
}

526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541
void writeSnippetsIndexFile() {
  final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets'));
  if (snippetsDir.existsSync()) {
    const JsonEncoder jsonEncoder = JsonEncoder.withIndent('    ');
    final Iterable<File> files = snippetsDir
        .listSync()
        .whereType<File>()
        .where((File file) => path.extension(file.path) == '.json');
        // Combine all the metadata into a single JSON array.
    final Iterable<String> fileContents = files.map((File file) => file.readAsStringSync());
    final List<dynamic> metadataObjects = fileContents.map<dynamic>(json.decode).toList();
    final String jsonArray = jsonEncoder.convert(metadataObjects);
    File('$kPublishRoot/snippets/index.json').writeAsStringSync(jsonArray);
  }
}

Seth Ladd's avatar
Seth Ladd committed
542
List<String> findPackageNames() {
543
  return findPackages().map<String>((FileSystemEntity file) => path.basename(file.path)).toList();
544 545
}

546
/// Finds all packages in the Flutter SDK
547
List<Directory> findPackages() {
548
  return Directory('packages')
549
    .listSync()
550
    .where((FileSystemEntity entity) {
551
      if (entity is! Directory) {
552
        return false;
553
      }
554
      final File pubspec = File('${entity.path}/pubspec.yaml');
555 556 557 558
      if (!pubspec.existsSync()) {
        print("Unexpected package '${entity.path}' found in packages directory");
        return false;
      }
559 560
      // TODO(ianh): Use a real YAML parser here
      return !pubspec.readAsStringSync().contains('nodoc: true');
561
    })
562
    .cast<Directory>()
563 564 565
    .toList();
}

566
/// Returns import or on-disk paths for all libraries in the Flutter SDK.
567
Iterable<String> libraryRefs() sync* {
568
  for (final Directory dir in findPackages()) {
569
    final String dirName = path.basename(dir.path);
570
    for (final FileSystemEntity file in Directory('${dir.path}/lib').listSync()) {
571
      if (file is File && file.path.endsWith('.dart')) {
572 573
        yield '$dirName/${path.basename(file.path)}';
      }
574 575
    }
  }
576 577

  // Add a fake package for platform integration APIs.
578 579
  yield '$kPlatformIntegrationPackageName/android.dart';
  yield '$kPlatformIntegrationPackageName/ios.dart';
580 581
}

582
void printStream(Stream<List<int>> stream, { String prefix = '', List<Pattern> filter = const <Pattern>[] }) {
583
  stream
584 585
    .transform<String>(utf8.decoder)
    .transform<String>(const LineSplitter())
586
    .listen((String line) {
587
      if (!filter.any((Pattern pattern) => line.contains(pattern))) {
588
        print('$prefix$line'.trim());
589
      }
590
    });
591
}
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606

Future<Process> runPubProcess({
  required String dartBinaryPath,
  required List<String> arguments,
  String? workingDirectory,
  Map<String, String>? environment,
  @visibleForTesting
  ProcessManager processManager = const LocalProcessManager(),
}) {
  return processManager.start(
    <Object>[dartBinaryPath, 'pub', ...arguments],
    workingDirectory: workingDirectory,
    environment: environment,
  );
}