blob: fe026ddc81fdd4346f7f65bde58e192d1ab7977d [file] [log] [blame]
// 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';
import 'package:args/args.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as path;
const String kDocRoot = 'dev/docs/doc';
/// 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/`.
/// This script also updates the index.html file so that it can be placed
/// at the root of We are keeping the files inside of
/// for now, so we need to manipulate paths
/// a bit. See for more info.
/// 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.
Future<Null> main(List<String> arguments) async {
final ArgParser argParser = _createArgsParser();
final ArgResults args = argParser.parse(arguments);
if (args['help']) {
print ('Usage:');
print (argParser.usage);
// 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;
final ProcessResult flutter = Process.runSync('flutter', <String>[]);
final File versionFile = new File('version');
if (flutter.exitCode != 0 || !versionFile.existsSync())
throw new Exception('Failed to determine Flutter version.');
final String version = versionFile.readAsStringSync();
// Create the pubspec.yaml file.
final StringBuffer buf = new StringBuffer();
buf.writeln('name: Flutter');
buf.writeln('version: $version');
for (String package in findPackageNames()) {
buf.writeln(' $package:');
buf.writeln(' sdk: flutter');
buf.writeln(' platform_integration: 0.0.1');
buf.writeln(' platform_integration:');
buf.writeln(' path: platform_integration');
new File('dev/docs/pubspec.yaml').writeAsStringSync(buf.toString());
// Create the library file.
final Directory libDir = new Directory('dev/docs/lib');
final StringBuffer contents = new StringBuffer('library temp_doc;\n\n');
for (String libraryRef in libraryRefs()) {
contents.writeln('import \'package:$libraryRef\';');
new File('dev/docs/lib/temp_doc.dart').writeAsStringSync(contents.toString());
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';
if (new Directory(pubCachePath).existsSync()) {
pubEnvironment['PUB_CACHE'] = pubCachePath;
final String pubExecutable = '$flutterRoot/bin/cache/dart-sdk/bin/pub';
// Run pub.
Process process = await Process.start(
workingDirectory: 'dev/docs',
environment: pubEnvironment,
printStream(process.stdout, prefix: 'pub:stdout: ');
printStream(process.stderr, prefix: 'pub:stderr: ');
final int code = await process.exitCode;
if (code != 0)
final List<String> dartdocBaseArgs = <String>['global', 'run'];
if (args['checked']) {
// Verify which version of dartdoc we're using.
final ProcessResult result = Process.runSync(
workingDirectory: 'dev/docs',
environment: pubEnvironment,
print('\n${result.stdout}flutter version: $version\n');
if (args['json']) {
if (args['validate-links']) {
} else {
// Generate the documentation.
// 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
final List<String> dartdocArgs = <String>[]..addAll(dartdocBaseArgs)..addAll(<String>[
'--header', 'styles.html',
'--header', 'analytics.html',
'--header', 'survey.html',
'--footer-text', 'lib/footer.html',
'--package-order', 'flutter,Dart,flutter_test,flutter_driver',
// Explicitly list all the packages in //flutter/packages/* that are
// not listed 'nodoc' in their pubspec.yaml.
for (String libraryRef in libraryRefs(diskPath: true)) {
String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg;
print('Executing: (cd dev/docs ; $pubExecutable ${' ')})');
process = await Process.start(
workingDirectory: 'dev/docs',
environment: pubEnvironment,
printStream(process.stdout, prefix: args['json'] ? '' : 'dartdoc:stdout: ',
filter: args['verbose'] ? const <Pattern>[] : <Pattern>[
new RegExp(r'^generating docs for library '), // unnecessary verbosity
new RegExp(r'^pars'), // unnecessary verbosity
printStream(process.stderr, prefix: args['json'] ? '' : 'dartdoc:stderr: ',
filter: args['verbose'] ? const <Pattern>[] : <Pattern>[
new RegExp(r'^[ ]+warning: generic type handled as HTML:'), //
new RegExp(r'^ warning: .+: \(.+/\.pub-cache/hosted/\)'), // packages outside our control
final int exitCode = await process.exitCode;
if (exitCode != 0)
ArgParser _createArgsParser() {
final ArgParser parser = new ArgParser();
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.');
parser.addFlag('validate-links', negatable: true,
help: 'Display warnings for broken links generated by dartdoc (slow)');
return parser;
void createFooter(String footerPath) {
const int kGitRevisionLength = 10;
final ProcessResult gitResult = Process.runSync('git', <String>['rev-parse', 'HEAD']);
if (gitResult.exitCode != 0)
throw 'git exit with non-zero exit code: ${gitResult.exitCode}';
String gitRevision = gitResult.stdout.trim();
gitRevision = gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision;
final String timestamp = new DateFormat('yyyy-MM-dd HH:mm').format(new;
new File(footerPath).writeAsStringSync(
'• </span class="no-break">$timestamp<span> '
'• </span class="no-break">$gitRevision</span>'
void sanityCheckDocs() {
final List<String> canaries = <String>[
for (String canary in canaries) {
if (!new File(canary).existsSync())
throw new Exception('Missing "$canary", which probably means the documentation failed to build correctly.');
/// 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');
print('\nDocs ready to go!');
void removeOldFlutterDocsDir() {
try {
new Directory('$kDocRoot/flutter').deleteSync(recursive: true);
} catch (e) {
// If the directory does not exist, that's OK.
void renameApiDir() {
new Directory('$kDocRoot/api').renameSync('$kDocRoot/flutter');
void copyIndexToRootOfDocs() {
new File('$kDocRoot/flutter/index.html').copySync('$kDocRoot/index.html');
void changePackageToSdkInTitlebar() {
final File indexFile = new File('$kDocRoot/index.html');
String indexContents = indexFile.readAsStringSync();
indexContents = indexContents.replaceFirst(
'<li><a href="">Flutter package</a></li>',
'<li><a href="">Flutter SDK</a></li>',
void addHtmlBaseToIndex() {
final File indexFile = new File('$kDocRoot/index.html');
String indexContents = indexFile.readAsStringSync();
indexContents = indexContents.replaceFirst(
'</title>\n <base href="./flutter/">\n',
indexContents = indexContents.replaceAll(
indexContents = indexContents.replaceAll(
void putRedirectInOldIndexLocation() {
const String metaTag = '<meta http-equiv="refresh" content="0;URL=../index.html">';
new File('$kDocRoot/flutter/index.html').writeAsStringSync(metaTag);
List<String> findPackageNames() {
return findPackages().map((Directory dir) => path.basename(dir.path)).toList();
/// Finds all packages in the Flutter SDK
List<Directory> findPackages() {
return new Directory('packages')
.where((FileSystemEntity entity) {
if (entity is! Directory)
return false;
final File pubspec = new File('${entity.path}/pubspec.yaml');
// TODO(ianh): Use a real YAML parser here
return !pubspec.readAsStringSync().contains('nodoc: true');
/// Returns import or on-disk paths for all libraries in the Flutter SDK.
/// diskPath toggles between import paths vs. disk paths.
Iterable<String> libraryRefs({ bool diskPath: false }) sync* {
for (Directory dir in findPackages()) {
final String dirName = path.basename(dir.path);
for (FileSystemEntity file in new Directory('${dir.path}/lib').listSync()) {
if (file is File && file.path.endsWith('.dart')) {
if (diskPath)
yield '$dirName/lib/${path.basename(file.path)}';
yield '$dirName/${path.basename(file.path)}';
// Add a fake package for platform integration APIs.
if (diskPath) {
yield 'platform_integration/lib/android.dart';
yield 'platform_integration/lib/ios.dart';
} else {
yield 'platform_integration/android.dart';
yield 'platform_integration/ios.dart';
void printStream(Stream<List<int>> stream, { String prefix: '', List<Pattern> filter: const <Pattern>[] }) {
assert(prefix != null);
assert(filter != null);
.transform(const LineSplitter())
.listen((String line) {
if (!filter.any((Pattern pattern) => line.contains(pattern)))