blob: 752ccd28f4c24d9d88b4afc4240661c5ccbd72aa [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 kDocsRoot = 'dev/docs';
const String kPublishRoot = '$kDocsRoot/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 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.
///
/// 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<void> main(List<String> arguments) async {
final ArgParser argParser = _createArgsParser();
final ArgResults args = argParser.parse(arguments);
if (args['help']) {
print ('Usage:');
print (argParser.usage);
exit(0);
}
// 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 = File('version');
if (flutter.exitCode != 0 || !versionFile.existsSync())
throw Exception('Failed to determine Flutter version.');
final String version = versionFile.readAsStringSync();
// Create the pubspec.yaml file.
final StringBuffer buf = StringBuffer();
buf.writeln('name: Flutter');
buf.writeln('homepage: https://flutter.io');
buf.writeln('version: $version');
buf.writeln('dependencies:');
for (String package in findPackageNames()) {
buf.writeln(' $package:');
buf.writeln(' sdk: flutter');
}
buf.writeln(' platform_integration: 0.0.1');
buf.writeln('dependency_overrides:');
buf.writeln(' platform_integration:');
buf.writeln(' path: platform_integration');
File('$kDocsRoot/pubspec.yaml').writeAsStringSync(buf.toString());
// Create the library file.
final Directory libDir = Directory('$kDocsRoot/lib');
libDir.createSync();
final StringBuffer contents = StringBuffer('library temp_doc;\n\n');
for (String libraryRef in libraryRefs()) {
contents.writeln('import \'package:$libraryRef\';');
}
File('$kDocsRoot/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 (Directory(pubCachePath).existsSync()) {
pubEnvironment['PUB_CACHE'] = pubCachePath;
}
final String pubExecutable = '$flutterRoot/bin/cache/dart-sdk/bin/pub';
// Run pub.
Process process = await Process.start(
pubExecutable,
<String>['get'],
workingDirectory: kDocsRoot,
environment: pubEnvironment,
);
printStream(process.stdout, prefix: 'pub:stdout: ');
printStream(process.stderr, prefix: 'pub:stderr: ');
final int code = await process.exitCode;
if (code != 0)
exit(code);
createFooter('$kDocsRoot/lib/footer.html');
copyAssets();
cleanOutSnippets();
precompileSnippetsTool();
final List<String> dartdocBaseArgs = <String>['global', 'run'];
if (args['checked']) {
dartdocBaseArgs.add('-c');
}
dartdocBaseArgs.add('dartdoc');
// Verify which version of dartdoc we're using.
final ProcessResult result = Process.runSync(
pubExecutable,
<String>[]..addAll(dartdocBaseArgs)..add('--version'),
workingDirectory: kDocsRoot,
environment: pubEnvironment,
);
print('\n${result.stdout}flutter version: $version\n');
if (args['json']) {
dartdocBaseArgs.add('--json');
}
if (args['validate-links']) {
dartdocBaseArgs.add('--validate-links');
} else {
dartdocBaseArgs.add('--no-validate-links');
}
// 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>[
'--inject-html',
'--header', 'styles.html',
'--header', 'analytics.html',
'--header', 'survey.html',
'--header', 'snippets.html',
'--footer-text', 'lib/footer.html',
'--exclude-packages',
<String>[
'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',
].join(','),
'--exclude',
<String>[
'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(','),
'--favicon=favicon.ico',
'--package-order', 'flutter,Dart,flutter_test,flutter_driver',
'--auto-include-dependencies',
]);
String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg;
print('Executing: (cd $kDocsRoot ; $pubExecutable ${dartdocArgs.map<String>(quote).join(' ')})');
process = await Process.start(
pubExecutable,
dartdocArgs,
workingDirectory: kDocsRoot,
environment: pubEnvironment,
);
printStream(process.stdout, prefix: args['json'] ? '' : 'dartdoc:stdout: ',
filter: args['verbose'] ? const <Pattern>[] : <Pattern>[
RegExp(r'^generating docs for library '), // unnecessary verbosity
RegExp(r'^pars'), // unnecessary verbosity
],
);
printStream(process.stderr, prefix: args['json'] ? '' : 'dartdoc:stderr: ',
filter: args['verbose'] ? const <Pattern>[] : <Pattern>[
RegExp(r'^ warning: .+: \(.+/\.pub-cache/hosted/pub.dartlang.org/.+\)'), // packages outside our control
],
);
final int exitCode = await process.exitCode;
if (exitCode != 0)
exit(exitCode);
sanityCheckDocs();
createIndexAndCleanup();
}
ArgParser _createArgsParser() {
final ArgParser parser = 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;
}
final RegExp gitBranchRegexp = RegExp(r'^## (.*)');
void createFooter(String footerPath) {
const int kGitRevisionLength = 10;
ProcessResult gitResult = Process.runSync('git', <String>['rev-parse', 'HEAD']);
if (gitResult.exitCode != 0)
throw 'git rev-parse exit with non-zero exit code: ${gitResult.exitCode}';
String gitRevision = gitResult.stdout.trim();
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);
final String gitBranchOut = gitBranchMatch == null ? '' : '• </span class="no-break">${gitBranchMatch.group(1).split('...').first}</span>';
gitRevision = gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision;
final String timestamp = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now());
File(footerPath).writeAsStringSync(<String>[
'• </span class="no-break">$timestamp<span>',
'• </span class="no-break">$gitRevision</span>',
gitBranchOut].join(' '));
}
/// Recursively copies `srcDir` to `destDir`, invoking [onFileCopied], if
/// specified, for each source/destination file pair.
///
/// Creates `destDir` if needed.
void copyDirectorySync(Directory srcDir, Directory destDir, [void onFileCopied(File srcFile, File destFile)]) {
if (!srcDir.existsSync())
throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy');
if (!destDir.existsSync())
destDir.createSync(recursive: true);
for (FileSystemEntity entity in srcDir.listSync()) {
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}'));
}
void cleanOutSnippets() {
final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets'));
if (snippetsDir.existsSync()) {
snippetsDir
..deleteSync(recursive: true)
..createSync(recursive: true);
}
}
File precompileSnippetsTool() {
final File snapshotPath = File(path.join('bin', 'cache', 'snippets.snapshot'));
print('Precompiling snippets tool into ${snapshotPath.absolute.path}');
if (snapshotPath.existsSync()) {
snapshotPath.deleteSync();
}
// In order to be able to optimize properly, we need to provide a training set
// of arguments, and an input file to process.
final Directory tempDir = Directory.systemTemp.createTempSync('dartdoc_snippet_');
final File trainingFile = File(path.join(tempDir.path, 'snippet_training'));
trainingFile.writeAsStringSync('```dart\nvoid foo(){}\n```');
Process.runSync(Platform.resolvedExecutable, <String>[
'--snapshot=${snapshotPath.absolute.path}',
'--snapshot_kind=app-jit',
path.join(
'dev',
'snippets',
'lib',
'main.dart',
),
'--type=sample',
'--input=${trainingFile.absolute.path}',
'--output=${path.join(tempDir.absolute.path, 'training_output.txt')}',
]);
tempDir.deleteSync(recursive: true);
return snapshotPath;
}
void sanityCheckDocs() {
final List<String> canaries = <String>[
'$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',
];
for (String canary in canaries) {
if (!File(canary).existsSync())
throw 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 $kPublishRoot/index.html');
removeOldFlutterDocsDir();
renameApiDir();
copyIndexToRootOfDocs();
addHtmlBaseToIndex();
changePackageToSdkInTitlebar();
putRedirectInOldIndexLocation();
print('\nDocs ready to go!');
}
void removeOldFlutterDocsDir() {
try {
Directory('$kPublishRoot/flutter').deleteSync(recursive: true);
} on FileSystemException {
// If the directory does not exist, that's OK.
}
}
void renameApiDir() {
Directory('$kPublishRoot/api').renameSync('$kPublishRoot/flutter');
}
void copyIndexToRootOfDocs() {
File('$kPublishRoot/flutter/index.html').copySync('$kPublishRoot/index.html');
}
void changePackageToSdkInTitlebar() {
final File indexFile = File('$kPublishRoot/index.html');
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);
}
void addHtmlBaseToIndex() {
final File indexFile = File('$kPublishRoot/index.html');
String indexContents = indexFile.readAsStringSync();
indexContents = indexContents.replaceFirst(
'</title>\n',
'</title>\n <base href="./flutter/">\n',
);
indexContents = indexContents.replaceAll(
'href="Android/Android-library.html"',
'href="/javadoc/"',
);
indexContents = indexContents.replaceAll(
'href="iOS/iOS-library.html"',
'href="/objcdoc/"',
);
indexFile.writeAsStringSync(indexContents);
}
void putRedirectInOldIndexLocation() {
const String metaTag = '<meta http-equiv="refresh" content="0;URL=../index.html">';
File('$kPublishRoot/flutter/index.html').writeAsStringSync(metaTag);
}
List<String> findPackageNames() {
return findPackages().map<String>((FileSystemEntity file) => path.basename(file.path)).toList();
}
/// Finds all packages in the Flutter SDK
List<FileSystemEntity> findPackages() {
return Directory('packages')
.listSync()
.where((FileSystemEntity entity) {
if (entity is! Directory)
return false;
final File pubspec = File('${entity.path}/pubspec.yaml');
// TODO(ianh): Use a real YAML parser here
return !pubspec.readAsStringSync().contains('nodoc: true');
})
.cast<Directory>()
.toList();
}
/// Returns import or on-disk paths for all libraries in the Flutter SDK.
Iterable<String> libraryRefs() sync* {
for (Directory dir in findPackages()) {
final String dirName = path.basename(dir.path);
for (FileSystemEntity file in Directory('${dir.path}/lib').listSync()) {
if (file is File && file.path.endsWith('.dart')) {
yield '$dirName/${path.basename(file.path)}';
}
}
}
// Add a fake package for platform integration APIs.
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);
stream
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
if (!filter.any((Pattern pattern) => line.contains(pattern)))
print('$prefix$line'.trim());
});
}