• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium 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
5import 'dart:async';
6import 'dart:convert';
7import 'dart:io';
8
9import 'package:args/args.dart';
10import 'package:intl/intl.dart';
11import 'package:path/path.dart' as path;
12import 'package:process/process.dart';
13
14const String kDocsRoot = 'dev/docs';
15const String kPublishRoot = '$kDocsRoot/doc';
16const String kSnippetsRoot = 'dev/snippets';
17
18/// This script expects to run with the cwd as the root of the flutter repo. It
19/// will generate documentation for the packages in `//packages/` and write the
20/// documentation to `//dev/docs/doc/api/`.
21///
22/// This script also updates the index.html file so that it can be placed
23/// at the root of docs.flutter.io. We are keeping the files inside of
24/// docs.flutter.io/flutter for now, so we need to manipulate paths
25/// a bit. See https://github.com/flutter/flutter/issues/3900 for more info.
26///
27/// This will only work on UNIX systems, not Windows. It requires that 'git' be
28/// in your path. It requires that 'flutter' has been run previously. It uses
29/// the version of Dart downloaded by the 'flutter' tool in this repository and
30/// will crash if that is absent.
31Future<void> main(List<String> arguments) async {
32  final ArgParser argParser = _createArgsParser();
33  final ArgResults args = argParser.parse(arguments);
34  if (args['help']) {
35    print ('Usage:');
36    print (argParser.usage);
37    exit(0);
38  }
39  // If we're run from the `tools` dir, set the cwd to the repo root.
40  if (path.basename(Directory.current.path) == 'tools')
41    Directory.current = Directory.current.parent.parent;
42
43  final ProcessResult flutter = Process.runSync('flutter', <String>[]);
44  final File versionFile = File('version');
45  if (flutter.exitCode != 0 || !versionFile.existsSync())
46    throw Exception('Failed to determine Flutter version.');
47  final String version = versionFile.readAsStringSync();
48
49  // Create the pubspec.yaml file.
50  final StringBuffer buf = StringBuffer();
51  buf.writeln('name: Flutter');
52  buf.writeln('homepage: https://flutter.dev');
53  // TODO(dnfield): We should make DartDoc able to avoid emitting this. If we
54  // use the real value here, every file will get marked as new instead of only
55  // files that have otherwise changed. Instead, we replace it dynamically using
56  // JavaScript so that fewer files get marked as changed.
57  // https://github.com/dart-lang/dartdoc/issues/1982
58  buf.writeln('version: 0.0.0');
59  buf.writeln('dependencies:');
60  for (String package in findPackageNames()) {
61    buf.writeln('  $package:');
62    buf.writeln('    sdk: flutter');
63  }
64  buf.writeln('  platform_integration: 0.0.1');
65  buf.writeln('dependency_overrides:');
66  buf.writeln('  platform_integration:');
67  buf.writeln('    path: platform_integration');
68  File('$kDocsRoot/pubspec.yaml').writeAsStringSync(buf.toString());
69
70  // Create the library file.
71  final Directory libDir = Directory('$kDocsRoot/lib');
72  libDir.createSync();
73
74  final StringBuffer contents = StringBuffer('library temp_doc;\n\n');
75  for (String libraryRef in libraryRefs()) {
76    contents.writeln('import \'package:$libraryRef\';');
77  }
78  File('$kDocsRoot/lib/temp_doc.dart').writeAsStringSync(contents.toString());
79
80  final String flutterRoot = Directory.current.path;
81  final Map<String, String> pubEnvironment = <String, String>{
82    'FLUTTER_ROOT': flutterRoot,
83  };
84
85  // If there's a .pub-cache dir in the flutter root, use that.
86  final String pubCachePath = '$flutterRoot/.pub-cache';
87  if (Directory(pubCachePath).existsSync()) {
88    pubEnvironment['PUB_CACHE'] = pubCachePath;
89  }
90
91  final String pubExecutable = '$flutterRoot/bin/cache/dart-sdk/bin/pub';
92
93  // Run pub.
94  ProcessWrapper process = ProcessWrapper(await Process.start(
95    pubExecutable,
96    <String>['get'],
97    workingDirectory: kDocsRoot,
98    environment: pubEnvironment,
99  ));
100  printStream(process.stdout, prefix: 'pub:stdout: ');
101  printStream(process.stderr, prefix: 'pub:stderr: ');
102  final int code = await process.done;
103  if (code != 0)
104    exit(code);
105
106  createFooter('$kDocsRoot/lib/', version);
107  copyAssets();
108  createSearchMetadata('$kDocsRoot/lib/opensearch.xml', '$kDocsRoot/doc/opensearch.xml');
109  cleanOutSnippets();
110
111  final List<String> dartdocBaseArgs = <String>['global', 'run'];
112  if (args['checked']) {
113    dartdocBaseArgs.add('-c');
114  }
115  dartdocBaseArgs.add('dartdoc');
116
117  // Verify which version of dartdoc we're using.
118  final ProcessResult result = Process.runSync(
119    pubExecutable,
120    <String>[...dartdocBaseArgs, '--version'],
121    workingDirectory: kDocsRoot,
122    environment: pubEnvironment,
123  );
124  print('\n${result.stdout}flutter version: $version\n');
125
126  dartdocBaseArgs.add('--allow-tools');
127
128  if (args['json']) {
129    dartdocBaseArgs.add('--json');
130  }
131  if (args['validate-links']) {
132    dartdocBaseArgs.add('--validate-links');
133  } else {
134    dartdocBaseArgs.add('--no-validate-links');
135  }
136  dartdocBaseArgs.addAll(<String>['--link-to-source-excludes', '../../bin/cache',
137                                  '--link-to-source-root', '../..',
138                                  '--link-to-source-uri-template', 'https://github.com/flutter/flutter/blob/master/%f%#L%l%']);
139  // Generate the documentation.
140  // We don't need to exclude flutter_tools in this list because it's not in the
141  // recursive dependencies of the package defined at dev/docs/pubspec.yaml
142  final List<String> dartdocArgs = <String>[
143    ...dartdocBaseArgs,
144    '--inject-html',
145    '--header', 'styles.html',
146    '--header', 'analytics.html',
147    '--header', 'survey.html',
148    '--header', 'snippets.html',
149    '--header', 'opensearch.html',
150    '--footer-text', 'lib/footer.html',
151    '--allow-warnings-in-packages',
152    <String>[
153      'Flutter',
154      'flutter',
155      'platform_integration',
156      'flutter_test',
157      'flutter_driver',
158      'flutter_localizations',
159    ].join(','),
160    '--exclude-packages',
161    <String>[
162      'analyzer',
163      'args',
164      'barback',
165      'cli_util',
166      'csslib',
167      'flutter_goldens',
168      'flutter_goldens_client',
169      'front_end',
170      'fuchsia_remote_debug_protocol',
171      'glob',
172      'html',
173      'http_multi_server',
174      'io',
175      'isolate',
176      'js',
177      'kernel',
178      'logging',
179      'mime',
180      'mockito',
181      'node_preamble',
182      'plugin',
183      'shelf',
184      'shelf_packages_handler',
185      'shelf_static',
186      'shelf_web_socket',
187      'utf',
188      'watcher',
189      'yaml',
190    ].join(','),
191    '--exclude',
192    <String>[
193      'package:Flutter/temp_doc.dart',
194      'package:http/browser_client.dart',
195      'package:intl/intl_browser.dart',
196      'package:matcher/mirror_matchers.dart',
197      'package:quiver/io.dart',
198      'package:quiver/mirrors.dart',
199      'package:vm_service_client/vm_service_client.dart',
200      'package:web_socket_channel/html.dart',
201    ].join(','),
202    '--favicon=favicon.ico',
203    '--package-order', 'flutter,Dart,platform_integration,flutter_test,flutter_driver',
204    '--auto-include-dependencies',
205  ];
206
207  String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg;
208  print('Executing: (cd $kDocsRoot ; $pubExecutable ${dartdocArgs.map<String>(quote).join(' ')})');
209
210  process = ProcessWrapper(await Process.start(
211    pubExecutable,
212    dartdocArgs,
213    workingDirectory: kDocsRoot,
214    environment: pubEnvironment,
215  ));
216  printStream(process.stdout, prefix: args['json'] ? '' : 'dartdoc:stdout: ',
217    filter: args['verbose'] ? const <Pattern>[] : <Pattern>[
218      RegExp(r'^generating docs for library '), // unnecessary verbosity
219      RegExp(r'^pars'), // unnecessary verbosity
220    ],
221  );
222  printStream(process.stderr, prefix: args['json'] ? '' : 'dartdoc:stderr: ',
223    filter: args['verbose'] ? const <Pattern>[] : <Pattern>[
224      RegExp(r'^ warning: .+: \(.+/\.pub-cache/hosted/pub.dartlang.org/.+\)'), // packages outside our control
225    ],
226  );
227  final int exitCode = await process.done;
228
229  if (exitCode != 0)
230    exit(exitCode);
231
232  sanityCheckDocs();
233
234  createIndexAndCleanup();
235}
236
237ArgParser _createArgsParser() {
238  final ArgParser parser = ArgParser();
239  parser.addFlag('help', abbr: 'h', negatable: false,
240      help: 'Show command help.');
241  parser.addFlag('verbose', negatable: true, defaultsTo: true,
242      help: 'Whether to report all error messages (on) or attempt to '
243          'filter out some known false positives (off).  Shut this off '
244          'locally if you want to address Flutter-specific issues.');
245  parser.addFlag('checked', abbr: 'c', negatable: true,
246      help: 'Run dartdoc in checked mode.');
247  parser.addFlag('json', negatable: true,
248      help: 'Display json-formatted output from dartdoc and skip stdout/stderr prefixing.');
249  parser.addFlag('validate-links', negatable: true,
250      help: 'Display warnings for broken links generated by dartdoc (slow)');
251  return parser;
252}
253
254final RegExp gitBranchRegexp = RegExp(r'^## (.*)');
255
256String getBranchName() {
257  final ProcessResult gitResult = Process.runSync('git', <String>['status', '-b', '--porcelain']);
258  if (gitResult.exitCode != 0)
259    throw 'git status exit with non-zero exit code: ${gitResult.exitCode}';
260  final Match gitBranchMatch = gitBranchRegexp.firstMatch(
261      gitResult.stdout.trim().split('\n').first);
262  return gitBranchMatch == null ? '' : gitBranchMatch.group(1).split('...').first;
263}
264
265String gitRevision() {
266  const int kGitRevisionLength = 10;
267
268  final ProcessResult gitResult = Process.runSync('git', <String>['rev-parse', 'HEAD']);
269  if (gitResult.exitCode != 0)
270    throw 'git rev-parse exit with non-zero exit code: ${gitResult.exitCode}';
271  final String gitRevision = gitResult.stdout.trim();
272
273  return gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision;
274}
275
276void createFooter(String footerPath, String version) {
277  final String timestamp = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now());
278  final String gitBranch = getBranchName();
279  final String gitBranchOut = gitBranch.isEmpty ? '' : '• $gitBranch';
280  File('${footerPath}footer.html').writeAsStringSync('<script src="footer.js"></script>');
281  File('$kPublishRoot/api/footer.js')
282    ..createSync(recursive: true)
283    ..writeAsStringSync('''(function() {
284  var span = document.querySelector('footer>span');
285  if (span) {
286    span.innerText = 'Flutter $version • $timestamp • ${gitRevision()} $gitBranchOut';
287  }
288  var sourceLink = document.querySelector('a.source-link');
289  if (sourceLink) {
290    sourceLink.href = sourceLink.href.replace('/master/', '/${gitRevision()}/');
291  }
292})();
293''');
294}
295
296/// Generates an OpenSearch XML description that can be used to add a custom
297/// search for Flutter API docs to the browser. Unfortunately, it has to know
298/// the URL to which site to search, so we customize it here based upon the
299/// branch name.
300void createSearchMetadata(String templatePath, String metadataPath) {
301  final String template = File(templatePath).readAsStringSync();
302  final String branch = getBranchName();
303  final String metadata = template.replaceAll(
304    '{SITE_URL}',
305    branch == 'stable' ? 'https://docs.flutter.io/' : 'https://master-docs.flutter.io/',
306  );
307  Directory(path.dirname(metadataPath)).create(recursive: true);
308  File(metadataPath).writeAsStringSync(metadata);
309}
310
311/// Recursively copies `srcDir` to `destDir`, invoking [onFileCopied], if
312/// specified, for each source/destination file pair.
313///
314/// Creates `destDir` if needed.
315void copyDirectorySync(Directory srcDir, Directory destDir, [void onFileCopied(File srcFile, File destFile)]) {
316  if (!srcDir.existsSync())
317    throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy');
318
319  if (!destDir.existsSync())
320    destDir.createSync(recursive: true);
321
322  for (FileSystemEntity entity in srcDir.listSync()) {
323    final String newPath = path.join(destDir.path, path.basename(entity.path));
324    if (entity is File) {
325      final File newFile = File(newPath);
326      entity.copySync(newPath);
327      onFileCopied?.call(entity, newFile);
328    } else if (entity is Directory) {
329      copyDirectorySync(entity, Directory(newPath));
330    } else {
331      throw Exception('${entity.path} is neither File nor Directory');
332    }
333  }
334}
335
336void copyAssets() {
337  final Directory assetsDir = Directory(path.join(kPublishRoot, 'assets'));
338  if (assetsDir.existsSync()) {
339    assetsDir.deleteSync(recursive: true);
340  }
341  copyDirectorySync(
342      Directory(path.join(kDocsRoot, 'assets')),
343      Directory(path.join(kPublishRoot, 'assets')),
344          (File src, File dest) => print('Copied ${src.path} to ${dest.path}'));
345}
346
347/// Clean out any existing snippets so that we don't publish old files from
348/// previous runs accidentally.
349void cleanOutSnippets() {
350  final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets'));
351  if (snippetsDir.existsSync()) {
352    snippetsDir
353      ..deleteSync(recursive: true)
354      ..createSync(recursive: true);
355  }
356}
357
358void sanityCheckDocs() {
359  final List<String> canaries = <String>[
360    '$kPublishRoot/assets/overrides.css',
361    '$kPublishRoot/api/dart-io/File-class.html',
362    '$kPublishRoot/api/dart-ui/Canvas-class.html',
363    '$kPublishRoot/api/dart-ui/Canvas/drawRect.html',
364    '$kPublishRoot/api/flutter_driver/FlutterDriver/FlutterDriver.connectedTo.html',
365    '$kPublishRoot/api/flutter_test/WidgetTester/pumpWidget.html',
366    '$kPublishRoot/api/material/Material-class.html',
367    '$kPublishRoot/api/material/Tooltip-class.html',
368    '$kPublishRoot/api/widgets/Widget-class.html',
369  ];
370  for (String canary in canaries) {
371    if (!File(canary).existsSync())
372      throw Exception('Missing "$canary", which probably means the documentation failed to build correctly.');
373  }
374}
375
376/// Creates a custom index.html because we try to maintain old
377/// paths. Cleanup unused index.html files no longer needed.
378void createIndexAndCleanup() {
379  print('\nCreating a custom index.html in $kPublishRoot/index.html');
380  removeOldFlutterDocsDir();
381  renameApiDir();
382  copyIndexToRootOfDocs();
383  addHtmlBaseToIndex();
384  changePackageToSdkInTitlebar();
385  putRedirectInOldIndexLocation();
386  writeSnippetsIndexFile();
387  print('\nDocs ready to go!');
388}
389
390void removeOldFlutterDocsDir() {
391  try {
392    Directory('$kPublishRoot/flutter').deleteSync(recursive: true);
393  } on FileSystemException {
394    // If the directory does not exist, that's OK.
395  }
396}
397
398void renameApiDir() {
399  Directory('$kPublishRoot/api').renameSync('$kPublishRoot/flutter');
400}
401
402void copyIndexToRootOfDocs() {
403  File('$kPublishRoot/flutter/index.html').copySync('$kPublishRoot/index.html');
404}
405
406void changePackageToSdkInTitlebar() {
407  final File indexFile = File('$kPublishRoot/index.html');
408  String indexContents = indexFile.readAsStringSync();
409  indexContents = indexContents.replaceFirst(
410    '<li><a href="https://flutter.dev">Flutter package</a></li>',
411    '<li><a href="https://flutter.dev">Flutter SDK</a></li>',
412  );
413
414  indexFile.writeAsStringSync(indexContents);
415}
416
417void addHtmlBaseToIndex() {
418  final File indexFile = File('$kPublishRoot/index.html');
419  String indexContents = indexFile.readAsStringSync();
420  indexContents = indexContents.replaceFirst(
421    '</title>\n',
422    '</title>\n  <base href="./flutter/">\n',
423  );
424  indexContents = indexContents.replaceAll(
425    'href="Android/Android-library.html"',
426    'href="/javadoc/"',
427  );
428  indexContents = indexContents.replaceAll(
429      'href="iOS/iOS-library.html"',
430      'href="/objcdoc/"',
431  );
432
433  indexFile.writeAsStringSync(indexContents);
434}
435
436void putRedirectInOldIndexLocation() {
437  const String metaTag = '<meta http-equiv="refresh" content="0;URL=../index.html">';
438  File('$kPublishRoot/flutter/index.html').writeAsStringSync(metaTag);
439}
440
441
442void writeSnippetsIndexFile() {
443  final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets'));
444  if (snippetsDir.existsSync()) {
445    const JsonEncoder jsonEncoder = JsonEncoder.withIndent('    ');
446    final Iterable<File> files = snippetsDir
447        .listSync()
448        .whereType<File>()
449        .where((File file) => path.extension(file.path) == '.json');
450        // Combine all the metadata into a single JSON array.
451    final Iterable<String> fileContents = files.map((File file) => file.readAsStringSync());
452    final List<dynamic> metadataObjects = fileContents.map<dynamic>(json.decode).toList();
453    final String jsonArray = jsonEncoder.convert(metadataObjects);
454    File('$kPublishRoot/snippets/index.json').writeAsStringSync(jsonArray);
455  }
456}
457
458List<String> findPackageNames() {
459  return findPackages().map<String>((FileSystemEntity file) => path.basename(file.path)).toList();
460}
461
462/// Finds all packages in the Flutter SDK
463List<FileSystemEntity> findPackages() {
464  return Directory('packages')
465    .listSync()
466    .where((FileSystemEntity entity) {
467      if (entity is! Directory)
468        return false;
469      final File pubspec = File('${entity.path}/pubspec.yaml');
470      // TODO(ianh): Use a real YAML parser here
471      return !pubspec.readAsStringSync().contains('nodoc: true');
472    })
473    .cast<Directory>()
474    .toList();
475}
476
477/// Returns import or on-disk paths for all libraries in the Flutter SDK.
478Iterable<String> libraryRefs() sync* {
479  for (Directory dir in findPackages()) {
480    final String dirName = path.basename(dir.path);
481    for (FileSystemEntity file in Directory('${dir.path}/lib').listSync()) {
482      if (file is File && file.path.endsWith('.dart')) {
483        yield '$dirName/${path.basename(file.path)}';
484      }
485    }
486  }
487
488  // Add a fake package for platform integration APIs.
489  yield 'platform_integration/android.dart';
490  yield 'platform_integration/ios.dart';
491}
492
493void printStream(Stream<List<int>> stream, { String prefix = '', List<Pattern> filter = const <Pattern>[] }) {
494  assert(prefix != null);
495  assert(filter != null);
496  stream
497    .transform<String>(utf8.decoder)
498    .transform<String>(const LineSplitter())
499    .listen((String line) {
500      if (!filter.any((Pattern pattern) => line.contains(pattern)))
501        print('$prefix$line'.trim());
502    });
503}
504