// 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 main(List 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', []); 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 pubEnvironment = { '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, ['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(); final List dartdocBaseArgs = ['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, []..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 dartdocArgs = []..addAll(dartdocBaseArgs)..addAll([ '--inject-html', '--header', 'styles.html', '--header', 'analytics.html', '--header', 'survey.html', '--header', 'snippets.html', '--footer-text', 'lib/footer.html', '--exclude-packages', [ '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', [ '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(quote).join(' ')})'); process = await Process.start( pubExecutable, dartdocArgs, workingDirectory: kDocsRoot, environment: pubEnvironment, ); printStream(process.stdout, prefix: args['json'] ? '' : 'dartdoc:stdout: ', filter: args['verbose'] ? const [] : [ 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 [] : [ 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', ['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', ['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 ? '' : '• ${gitBranchMatch.group(1).split('...').first}'; gitRevision = gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision; final String timestamp = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()); File(footerPath).writeAsStringSync([ '• $timestamp', '• $gitRevision', 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); } } void sanityCheckDocs() { final List canaries = [ '$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( '
  • Flutter package
  • ', '
  • Flutter SDK
  • ', ); indexFile.writeAsStringSync(indexContents); } void addHtmlBaseToIndex() { final File indexFile = File('$kPublishRoot/index.html'); String indexContents = indexFile.readAsStringSync(); indexContents = indexContents.replaceFirst( '\n', '\n \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 = ''; File('$kPublishRoot/flutter/index.html').writeAsStringSync(metaTag); } List findPackageNames() { return findPackages().map((FileSystemEntity file) => path.basename(file.path)).toList(); } /// Finds all packages in the Flutter SDK List 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() .toList(); } /// Returns import or on-disk paths for all libraries in the Flutter SDK. Iterable 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> stream, { String prefix = '', List filter = const [] }) { assert(prefix != null); assert(filter != null); stream .transform(utf8.decoder) .transform(const LineSplitter()) .listen((String line) { if (!filter.any((Pattern pattern) => line.contains(pattern))) print('$prefix$line'.trim()); }); }