dartdoc.dart 13 KB
Newer Older
1 2 3 4 5 6 7 8
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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

9
import 'package:args/args.dart';
10
import 'package:intl/intl.dart';
11 12
import 'package:path/path.dart' as path;

Seth Ladd's avatar
Seth Ladd committed
13 14
const String kDocRoot = 'dev/docs/doc';

15 16 17
/// 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/`.
18
///
Seth Ladd's avatar
Seth Ladd committed
19 20 21 22
/// This script also updates the index.html file so that it can be placed
/// at the root of docs.flutter.io. We are keeping the files inside of
/// docs.flutter.io/flutter for now, so we need to manipulate paths
/// a bit. See https://github.com/flutter/flutter/issues/3900 for more info.
23 24 25 26 27
///
/// 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.
28
Future<void> main(List<String> arguments) async {
29 30 31 32 33 34 35
  final ArgParser argParser = _createArgsParser();
  final ArgResults args = argParser.parse(arguments);
  if (args['help']) {
    print ('Usage:');
    print (argParser.usage);
    exit(0);
  }
36 37 38 39
  // If we're run from the `tools` dir, set the cwd to the repo root.
  if (path.basename(Directory.current.path) == 'tools')
    Directory.current = Directory.current.parent.parent;

40
  final ProcessResult flutter = Process.runSync('flutter', <String>[]);
41
  final File versionFile = File('version');
42
  if (flutter.exitCode != 0 || !versionFile.existsSync())
43
    throw Exception('Failed to determine Flutter version.');
44
  final String version = versionFile.readAsStringSync();
45

46
  // Create the pubspec.yaml file.
47
  final StringBuffer buf = StringBuffer();
48 49 50 51
  buf.writeln('name: Flutter');
  buf.writeln('homepage: https://flutter.io');
  buf.writeln('version: $version');
  buf.writeln('dependencies:');
Seth Ladd's avatar
Seth Ladd committed
52
  for (String package in findPackageNames()) {
53
    buf.writeln('  $package:');
54
    buf.writeln('    sdk: flutter');
55
  }
56 57 58 59
  buf.writeln('  platform_integration: 0.0.1');
  buf.writeln('dependency_overrides:');
  buf.writeln('  platform_integration:');
  buf.writeln('    path: platform_integration');
60
  File('dev/docs/pubspec.yaml').writeAsStringSync(buf.toString());
61 62

  // Create the library file.
63
  final Directory libDir = Directory('dev/docs/lib');
64 65
  libDir.createSync();

66
  final StringBuffer contents = StringBuffer('library temp_doc;\n\n');
Seth Ladd's avatar
Seth Ladd committed
67
  for (String libraryRef in libraryRefs()) {
68 69
    contents.writeln('import \'package:$libraryRef\';');
  }
70
  File('dev/docs/lib/temp_doc.dart').writeAsStringSync(contents.toString());
71

72 73 74 75 76 77 78
  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';
79
  if (Directory(pubCachePath).existsSync()) {
80 81 82 83 84
    pubEnvironment['PUB_CACHE'] = pubCachePath;
  }

  final String pubExecutable = '$flutterRoot/bin/cache/dart-sdk/bin/pub';

85
  // Run pub.
86
  Process process = await Process.start(
87
    pubExecutable,
88
    <String>['get'],
89
    workingDirectory: 'dev/docs',
90
    environment: pubEnvironment,
91
  );
92 93
  printStream(process.stdout, prefix: 'pub:stdout: ');
  printStream(process.stderr, prefix: 'pub:stderr: ');
94
  final int code = await process.exitCode;
95 96 97
  if (code != 0)
    exit(code);

98 99
  createFooter('dev/docs/lib/footer.html');

100 101 102 103 104 105
  final List<String> dartdocBaseArgs = <String>['global', 'run'];
  if (args['checked']) {
    dartdocBaseArgs.add('-c');
  }
  dartdocBaseArgs.add('dartdoc');

106
  // Verify which version of dartdoc we're using.
107
  final ProcessResult result = Process.runSync(
108
    pubExecutable,
109
    <String>[]..addAll(dartdocBaseArgs)..add('--version'),
110
    workingDirectory: 'dev/docs',
111
    environment: pubEnvironment,
112
  );
113
  print('\n${result.stdout}flutter version: $version\n');
114

115 116 117
  if (args['json']) {
    dartdocBaseArgs.add('--json');
  }
118 119 120 121 122
  if (args['validate-links']) {
    dartdocBaseArgs.add('--validate-links');
  } else {
    dartdocBaseArgs.add('--no-validate-links');
  }
123
  // Generate the documentation.
124 125
  // 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
126
  final List<String> dartdocArgs = <String>[]..addAll(dartdocBaseArgs)..addAll(<String>[
127 128
    '--header', 'styles.html',
    '--header', 'analytics.html',
129
    '--header', 'survey.html',
130
    '--footer-text', 'lib/footer.html',
131
    '--exclude-packages',
132
'analyzer,args,barback,cli_util,csslib,flutter_goldens,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',
133
    '--exclude',
Josh Soref's avatar
Josh Soref committed
134
  'package:Flutter/temp_doc.dart,package:http/browser_client.dart,package:intl/intl_browser.dart,package:matcher/mirror_matchers.dart,package:quiver/mirrors.dart,package:quiver/io.dart,package:vm_service_client/vm_service_client.dart,package:web_socket_channel/html.dart',
135
    '--favicon=favicon.ico',
136
    '--package-order', 'flutter,Dart,flutter_test,flutter_driver',
137 138
    '--show-warnings',
    '--auto-include-dependencies',
139
  ]);
140

141 142
  // Explicitly list all the packages in //flutter/packages/* that are
  // not listed 'nodoc' in their pubspec.yaml.
143
  for (String libraryRef in libraryRefs(diskPath: true)) {
144 145
    dartdocArgs.add('--include-external');
    dartdocArgs.add(libraryRef);
146 147
  }

148
  String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg;
149
  print('Executing: (cd dev/docs ; $pubExecutable ${dartdocArgs.map<String>(quote).join(' ')})');
150

151
  process = await Process.start(
152
    pubExecutable,
153
    dartdocArgs,
154
    workingDirectory: 'dev/docs',
155
    environment: pubEnvironment,
156
  );
157 158
  printStream(process.stdout, prefix: args['json'] ? '' : 'dartdoc:stdout: ',
    filter: args['verbose'] ? const <Pattern>[] : <Pattern>[
159 160
      RegExp(r'^generating docs for library '), // unnecessary verbosity
      RegExp(r'^pars'), // unnecessary verbosity
161 162
    ],
  );
163 164
  printStream(process.stderr, prefix: args['json'] ? '' : 'dartdoc:stderr: ',
    filter: args['verbose'] ? const <Pattern>[] : <Pattern>[
165 166
      RegExp(r'^[ ]+warning: generic type handled as HTML:'), // https://github.com/dart-lang/dartdoc/issues/1475
      RegExp(r'^ warning: .+: \(.+/\.pub-cache/hosted/pub.dartlang.org/.+\)'), // packages outside our control
167 168
    ],
  );
169
  final int exitCode = await process.exitCode;
Seth Ladd's avatar
Seth Ladd committed
170 171 172 173

  if (exitCode != 0)
    exit(exitCode);

174 175
  sanityCheckDocs();

Seth Ladd's avatar
Seth Ladd committed
176 177 178
  createIndexAndCleanup();
}

179
ArgParser _createArgsParser() {
180
  final ArgParser parser = ArgParser();
181 182 183 184 185 186 187 188 189 190
  parser.addFlag('help', abbr: 'h', negatable: false,
      help: 'Show command help.');
  parser.addFlag('verbose', negatable: true, defaultsTo: true,
      help: 'Whether to report all error messages (on) or attempt to '
          'filter out some known false positives (off).  Shut this off '
          'locally if you want to address Flutter-specific issues.');
  parser.addFlag('checked', abbr: 'c', negatable: true,
      help: 'Run dartdoc in checked mode.');
  parser.addFlag('json', negatable: true,
      help: 'Display json-formatted output from dartdoc and skip stdout/stderr prefixing.');
191 192
  parser.addFlag('validate-links', negatable: true,
      help: 'Display warnings for broken links generated by dartdoc (slow)');
193 194 195
  return parser;
}

196
final RegExp gitBranchRegexp = RegExp(r'^## (.*)');
197

198
void createFooter(String footerPath) {
199 200
  const int kGitRevisionLength = 10;

201
  ProcessResult gitResult = Process.runSync('git', <String>['rev-parse', 'HEAD']);
202
  if (gitResult.exitCode != 0)
203
    throw 'git rev-parse exit with non-zero exit code: ${gitResult.exitCode}';
204 205
  String gitRevision = gitResult.stdout.trim();

206 207 208 209 210
  gitResult = Process.runSync('git', <String>['status', '-b', '--porcelain']);
   if (gitResult.exitCode != 0)
    throw 'git status exit with non-zero exit code: ${gitResult.exitCode}';
  final Match gitBranchMatch = gitBranchRegexp.firstMatch(
      gitResult.stdout.trim().split('\n').first);
211
  final String gitBranchOut = gitBranchMatch == null ? '' : '• </span class="no-break">${gitBranchMatch.group(1).split('...').first}</span>';
212

213
  gitRevision = gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision;
214

215
  final String timestamp = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now());
216

217
  File(footerPath).writeAsStringSync(<String>[
218 219 220
    '• </span class="no-break">$timestamp<span>',
    '• </span class="no-break">$gitRevision</span>',
    gitBranchOut].join(' '));
221 222
}

223
void sanityCheckDocs() {
224
  final List<String> canaries = <String>[
225 226 227
    '$kDocRoot/api/dart-io/File-class.html',
    '$kDocRoot/api/dart-ui/Canvas-class.html',
    '$kDocRoot/api/dart-ui/Canvas/drawRect.html',
228
    '$kDocRoot/api/flutter_driver/FlutterDriver/FlutterDriver.connectedTo.html',
229
    '$kDocRoot/api/flutter_test/WidgetTester/pumpWidget.html',
230 231 232 233 234
    '$kDocRoot/api/material/Material-class.html',
    '$kDocRoot/api/material/Tooltip-class.html',
    '$kDocRoot/api/widgets/Widget-class.html',
  ];
  for (String canary in canaries) {
235 236
    if (!File(canary).existsSync())
      throw Exception('Missing "$canary", which probably means the documentation failed to build correctly.');
237 238 239
  }
}

Seth Ladd's avatar
Seth Ladd committed
240 241 242 243
/// Creates a custom index.html because we try to maintain old
/// paths. Cleanup unused index.html files no longer needed.
void createIndexAndCleanup() {
  print('\nCreating a custom index.html in $kDocRoot/index.html');
244
  removeOldFlutterDocsDir();
Seth Ladd's avatar
Seth Ladd committed
245 246 247
  renameApiDir();
  copyIndexToRootOfDocs();
  addHtmlBaseToIndex();
248
  changePackageToSdkInTitlebar();
Seth Ladd's avatar
Seth Ladd committed
249 250 251 252
  putRedirectInOldIndexLocation();
  print('\nDocs ready to go!');
}

253 254
void removeOldFlutterDocsDir() {
  try {
255
    Directory('$kDocRoot/flutter').deleteSync(recursive: true);
256
  } on FileSystemException {
257 258 259 260
    // If the directory does not exist, that's OK.
  }
}

Seth Ladd's avatar
Seth Ladd committed
261
void renameApiDir() {
262
  Directory('$kDocRoot/api').renameSync('$kDocRoot/flutter');
Seth Ladd's avatar
Seth Ladd committed
263 264
}

265
void copyIndexToRootOfDocs() {
266
  File('$kDocRoot/flutter/index.html').copySync('$kDocRoot/index.html');
Seth Ladd's avatar
Seth Ladd committed
267 268
}

269
void changePackageToSdkInTitlebar() {
270
  final File indexFile = File('$kDocRoot/index.html');
271 272 273 274 275 276 277 278 279
  String indexContents = indexFile.readAsStringSync();
  indexContents = indexContents.replaceFirst(
    '<li><a href="https://flutter.io">Flutter package</a></li>',
    '<li><a href="https://flutter.io">Flutter SDK</a></li>',
  );

  indexFile.writeAsStringSync(indexContents);
}

Seth Ladd's avatar
Seth Ladd committed
280
void addHtmlBaseToIndex() {
281
  final File indexFile = File('$kDocRoot/index.html');
Seth Ladd's avatar
Seth Ladd committed
282
  String indexContents = indexFile.readAsStringSync();
283 284 285 286
  indexContents = indexContents.replaceFirst(
    '</title>\n',
    '</title>\n  <base href="./flutter/">\n',
  );
287 288
  indexContents = indexContents.replaceAll(
    'href="Android/Android-library.html"',
289
    'href="/javadoc/"',
290
  );
291 292
  indexContents = indexContents.replaceAll(
      'href="iOS/iOS-library.html"',
293
      'href="/objcdoc/"',
294 295
  );

Seth Ladd's avatar
Seth Ladd committed
296 297 298 299
  indexFile.writeAsStringSync(indexContents);
}

void putRedirectInOldIndexLocation() {
300
  const String metaTag = '<meta http-equiv="refresh" content="0;URL=../index.html">';
301
  File('$kDocRoot/flutter/index.html').writeAsStringSync(metaTag);
302 303
}

Seth Ladd's avatar
Seth Ladd committed
304
List<String> findPackageNames() {
305
  return findPackages().map<String>((FileSystemEntity file) => path.basename(file.path)).toList();
306 307
}

308
/// Finds all packages in the Flutter SDK
309
List<FileSystemEntity> findPackages() {
310
  return Directory('packages')
311
    .listSync()
312 313 314
    .where((FileSystemEntity entity) {
      if (entity is! Directory)
        return false;
315
      final File pubspec = File('${entity.path}/pubspec.yaml');
316 317
      // TODO(ianh): Use a real YAML parser here
      return !pubspec.readAsStringSync().contains('nodoc: true');
318
    })
319
    .cast<Directory>()
320 321 322
    .toList();
}

323 324 325
/// Returns import or on-disk paths for all libraries in the Flutter SDK.
///
/// diskPath toggles between import paths vs. disk paths.
326
Iterable<String> libraryRefs({ bool diskPath = false }) sync* {
Seth Ladd's avatar
Seth Ladd committed
327
  for (Directory dir in findPackages()) {
328
    final String dirName = path.basename(dir.path);
329
    for (FileSystemEntity file in Directory('${dir.path}/lib').listSync()) {
330 331 332 333 334 335
      if (file is File && file.path.endsWith('.dart')) {
        if (diskPath)
          yield '$dirName/lib/${path.basename(file.path)}';
        else
          yield '$dirName/${path.basename(file.path)}';
       }
336 337
    }
  }
338 339

  // Add a fake package for platform integration APIs.
340
  if (diskPath) {
341
    yield 'platform_integration/lib/android.dart';
342 343
    yield 'platform_integration/lib/ios.dart';
  } else {
344
    yield 'platform_integration/android.dart';
345 346
    yield 'platform_integration/ios.dart';
  }
347 348
}

349
void printStream(Stream<List<int>> stream, { String prefix = '', List<Pattern> filter = const <Pattern>[] }) {
350 351
  assert(prefix != null);
  assert(filter != null);
352
  stream
353 354
    .transform<String>(utf8.decoder)
    .transform<String>(const LineSplitter())
355 356 357 358
    .listen((String line) {
      if (!filter.any((Pattern pattern) => line.contains(pattern)))
        print('$prefix$line'.trim());
    });
359
}