diff --git a/dev/bots/docs.sh b/dev/bots/docs.sh index 64cd279c49..6a0cec83c9 100755 --- a/dev/bots/docs.sh +++ b/dev/bots/docs.sh @@ -16,13 +16,102 @@ function script_location() { cd -P "$(dirname "$script_location")" >/dev/null && pwd } +function generate_docs() { + # Install and activate dartdoc. + # When updating to a new dartdoc version, please also update + # `dartdoc_options.yaml` to include newly introduced error and warning types. + "$DART" pub global activate dartdoc 6.3.0 + + # Install and activate the snippets tool, which resides in the + # assets-for-api-docs repo: + # https://github.com/flutter/assets-for-api-docs/tree/master/packages/snippets + "$DART" pub global activate snippets 0.3.1 + + # This script generates a unified doc set, and creates + # a custom index.html, placing everything into dev/docs/doc. + (cd "$FLUTTER_ROOT/dev/tools" && "$FLUTTER" pub get) + (cd "$FLUTTER_ROOT/dev/tools" && "$DART" pub get) + (cd "$FLUTTER_ROOT" && "$DART" --disable-dart-dev --enable-asserts "$FLUTTER_ROOT/dev/tools/dartdoc.dart") + (cd "$FLUTTER_ROOT" && "$DART" --disable-dart-dev --enable-asserts "$FLUTTER_ROOT/dev/tools/java_and_objc_doc.dart") +} + +# Zip up the docs so people can download them for offline usage. +function create_offline_zip() { + # Must be run from "$FLUTTER_ROOT/dev/docs" + echo "$(date): Zipping Flutter offline docs archive." + rm -rf flutter.docs.zip doc/offline + (cd ./doc; zip -r -9 -q ../flutter.docs.zip .) +} + +# Generate the docset for Flutter docs for use with Dash, Zeal, and Velocity. +function create_docset() { + # Must be run from "$FLUTTER_ROOT/dev/docs" + # Must have dashing installed: go get -u github.com/technosophos/dashing + # Dashing produces a LOT of log output (~30MB), so we redirect it, and just + # show the end of it if there was a problem. + echo "$(date): Building Flutter docset." + rm -rf flutter.docset + # If dashing gets stuck, Cirrus will time out the build after an hour, and we + # never get to see the logs. Thus, we run it in the background and tail the logs + # while we wait for it to complete. + dashing_log=/tmp/dashing.log + dashing build --source ./doc --config ./dashing.json > $dashing_log 2>&1 & + dashing_pid=$! + wait $dashing_pid && \ + cp ./doc/flutter/static-assets/favicon.png ./flutter.docset/icon.png && \ + "$DART" --disable-dart-dev --enable-asserts ./dashing_postprocess.dart && \ + tar cf flutter.docset.tar.gz --use-compress-program="gzip --best" flutter.docset + if [[ $? -ne 0 ]]; then + >&2 echo "Dashing docset generation failed" + tail -200 $dashing_log + exit 1 + fi +} + +function deploy_docs() { + case "$LUCI_BRANCH" in + master) + echo "$(date): Updating $LUCI_BRANCH docs: https://master-api.flutter.dev/" + # Disable search indexing on the master staging site so searches get only + # the stable site. + echo -e "User-agent: *\nDisallow: /" > "$FLUTTER_ROOT/dev/docs/doc/robots.txt" + ;; + stable) + echo "$(date): Updating $LUCI_BRANCH docs: https://api.flutter.dev/" + # Enable search indexing on the master staging site so searches get only + # the stable site. + echo -e "# All robots welcome!" > "$FLUTTER_ROOT/dev/docs/doc/robots.txt" + ;; + *) + >&2 echo "Docs deployment cannot be run on the $LUCI_BRANCH branch." + exit 0 + esac +} + +# Move the offline archives into place, after all the processing of the doc +# directory is done. This avoids the tools recursively processing the archives +# as part of their process. +function move_offline_into_place() { + # Must be run from "$FLUTTER_ROOT/dev/docs" + echo "$(date): Moving offline data into place." + mkdir -p doc/offline + mv flutter.docs.zip doc/offline/flutter.docs.zip + du -sh doc/offline/flutter.docs.zip + if [[ "$LUCI_BRANCH" == "stable" ]]; then + echo -e "\n ${FLUTTER_VERSION_STRING}\n https://api.flutter.dev/offline/flutter.docset.tar.gz\n" > doc/offline/flutter.xml + else + echo -e "\n ${FLUTTER_VERSION_STRING}\n https://master-api.flutter.dev/offline/flutter.docset.tar.gz\n" > doc/offline/flutter.xml + fi + mv flutter.docset.tar.gz doc/offline/flutter.docset.tar.gz + du -sh doc/offline/flutter.docset.tar.gz +} + # So that users can run this script from anywhere and it will work as expected. SCRIPT_LOCATION="$(script_location)" # Sets the Flutter root to be "$(script_location)/../..": This script assumes # that it resides two directory levels down from the root, so if that changes, # then this line will need to as well. FLUTTER_ROOT="$(dirname "$(dirname "$SCRIPT_LOCATION")")" -export FLUTTER_ROOT echo "$(date): Running docs.sh" @@ -35,106 +124,31 @@ FLUTTER_BIN="$FLUTTER_ROOT/bin" DART_BIN="$FLUTTER_ROOT/bin/cache/dart-sdk/bin" FLUTTER="$FLUTTER_BIN/flutter" DART="$DART_BIN/dart" -PATH="$FLUTTER_BIN:$DART_BIN:$PATH" +export PATH="$FLUTTER_BIN:$DART_BIN:$PATH" -# Make sure dart is installed by invoking Flutter to download it if it is missing. -# Also make sure the flutter command is ready to run before capturing output from -# it: if it has to rebuild itself or something, it'll spoil our JSON output. -"$FLUTTER" > /dev/null 2>&1 -FLUTTER_VERSION="$("$FLUTTER" --version --machine)" +# Make sure dart is installed by invoking Flutter to download it. +# This also creates the 'version' file. +FLUTTER_VERSION=$("$FLUTTER" --version --machine) export FLUTTER_VERSION +FLUTTER_VERSION_STRING=$(cat "$FLUTTER_ROOT/version") # If the pub cache directory exists in the root, then use that. FLUTTER_PUB_CACHE="$FLUTTER_ROOT/.pub-cache" if [[ -d "$FLUTTER_PUB_CACHE" ]]; then # This has to be exported, because pub interprets setting it to the empty # string in the same way as setting it to ".". - PUB_CACHE="${PUB_CACHE:-"$FLUTTER_PUB_CACHE"}" - export PUB_CACHE + export PUB_CACHE="${PUB_CACHE:-"$FLUTTER_PUB_CACHE"}" fi -OUTPUT_DIR=$(mktemp -d /tmp/dartdoc.XXXXX) -DOC_DIR="$OUTPUT_DIR/doc" +generate_docs +# Skip publishing docs for PRs and release candidate branches +if [[ -n "$LUCI_CI" && -z "$LUCI_PR" ]]; then + (cd "$FLUTTER_ROOT/dev/docs"; create_offline_zip) + (cd "$FLUTTER_ROOT/dev/docs"; create_docset) + (cd "$FLUTTER_ROOT/dev/docs"; move_offline_into_place) + deploy_docs +fi -function usage() { - echo "Usage: $(basename "${BASH_SOURCE[0]}") [--keep-temp] [--output ]" - echo "" - echo " --keep-temp Do not delete the temporary output directory created while generating docs." - echo " Normally the script deletes the temporary directory after generating the" - echo " output ZIP file." - echo " --output specifies where the output ZIP file containing the documentation data" - echo " will be written." - echo "" -} - -function parse_args() { - local arg - local args=() - KEEP_TEMP=0 - DESTINATION="$FLUTTER_ROOT/dev/docs/api_docs.zip" - while (( "$#" )); do - case "$1" in - --help) - usage - exit 0 - ;; - --keep-temp) - KEEP_TEMP=1 - ;; - --output) - DESTINATION="$2" - shift - ;; - *) - args=("${args[@]}" "$1") - ;; - esac - shift - done - if [[ ${#args[@]} != 0 ]]; then - >&2 echo "ERROR: Unknown arguments: ${args[@]}" - usage - exit 1 - fi -} - -function generate_docs() { - # Install and activate dartdoc. - # When updating to a new dartdoc version, please also update - # `dartdoc_options.yaml` to include newly introduced error and warning types. - "$DART" pub global activate dartdoc 6.3.0 - - # Install and activate the snippets tool, which resides in the - # assets-for-api-docs repo: - # https://github.com/flutter/assets-for-api-docs/tree/master/packages/snippets - "$DART" pub global activate snippets 0.4.0 - - # This script generates a unified doc set, and creates - # a custom index.html, placing everything into DOC_DIR. - - # Make sure that create_api_docs.dart has all the dependencies it needs. - (cd "$FLUTTER_ROOT/dev/tools" && "$FLUTTER" pub get) - (cd "$FLUTTER_ROOT" && "$DART" --disable-dart-dev --enable-asserts "$FLUTTER_ROOT/dev/tools/create_api_docs.dart" --output-dir="$DOC_DIR") -} - -function main() { - echo "Writing docs build temporary output to $DOC_DIR" - mkdir -p "$DOC_DIR" - generate_docs - # If the destination isn't an absolute path, make it into one. - if ! [[ "$DESTINATION" =~ ^/ ]]; then - DESTINATION="$PWD/$DESTINATION" - fi - # Zip up doc directory and write the output to the destination. - (cd "$OUTPUT_DIR"; zip -r -9 -q "$DESTINATION" ./doc) - if [[ $KEEP_TMP == 1 ]]; then - echo "Temporary document generation output left in $OUTPUT_DIR" - else - echo "Removing Temporary document generation output from $OUTPUT_DIR" - rm -rf "$OUTPUT_DIR" - fi - echo "Wrote docs ZIP file to $DESTINATION" -} - -parse_args "$@" -main +# Zip docs +cd "$FLUTTER_ROOT/dev/docs" +zip -r api_docs.zip doc diff --git a/dev/docs/dashing_postprocess.dart b/dev/docs/dashing_postprocess.dart new file mode 100644 index 0000000000..9f186e4f2c --- /dev/null +++ b/dev/docs/dashing_postprocess.dart @@ -0,0 +1,29 @@ +// Copyright 2014 The Flutter 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:io'; + +/// This changes the DocSetPlatformFamily key to be "dartlang" instead of the +/// name of the package (usually "flutter"). +/// +/// This is so that the IntelliJ plugin for Dash will be able to go directly to +/// the docs for a symbol from a keystroke. Without this, flutter isn't part +/// of the list of package names it searches. After this, it finds the flutter +/// docs because they're declared here to be part of the "dartlang" family of +/// docs. +/// +/// Dashing doesn't have a way to configure this, so we modify the Info.plist +/// directly to make the change. +void main(List args) { + final File infoPlist = File('flutter.docset/Contents/Info.plist'); + String contents = infoPlist.readAsStringSync(); + + // Since I didn't want to add the XML package as a dependency just for this, + // I just used a regular expression to make this simple change. + final RegExp findRe = RegExp(r'(\s*DocSetPlatformFamily\s*)[^<]+()', multiLine: true); + contents = contents.replaceAllMapped(findRe, (Match match) { + return '${match.group(1)}dartlang${match.group(2)}'; + }); + infoPlist.writeAsStringSync(contents); +} diff --git a/dev/tools/create_api_docs.dart b/dev/tools/create_api_docs.dart deleted file mode 100644 index 924e34a34e..0000000000 --- a/dev/tools/create_api_docs.dart +++ /dev/null @@ -1,1095 +0,0 @@ -// Copyright 2014 The Flutter 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:convert'; -import 'dart:io'; -import 'dart:math' as math; - -import 'package:archive/archive_io.dart'; -import 'package:args/args.dart'; -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:http/http.dart' as http; -import 'package:intl/intl.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as path; -import 'package:platform/platform.dart'; -import 'package:process/process.dart'; -import 'package:pub_semver/pub_semver.dart'; - -import 'dartdoc_checker.dart'; -import 'examples_smoke_test.dart'; - -FileSystem filesystem = const LocalFileSystem(); - -const String kDummyPackageName = 'Flutter'; -const String kPlatformIntegrationPackageName = 'platform_integration'; - -/// This script will generate documentation for the packages in `packages/` and -/// write the documentation to the output directory specified on the command -/// line. -/// -/// This script also updates the index.html file so that it can be placed at the -/// root of api.flutter.dev. The files are kept inside of -/// api.flutter.dev/flutter, 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', -/// 'zip', and 'tar' be in the 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 fail if that is absent. -Future main(List arguments) async { - // The place to find customization files and configuration files for docs - // generation. - final Directory docsRoot = filesystem - .directory(FlutterInformation.instance.getFlutterRoot().childDirectory('dev').childDirectory('docs')) - .absolute; - final ArgParser argParser = _createArgsParser( - publishDefault: docsRoot.childDirectory('doc').path, - ); - final ArgResults args = argParser.parse(arguments); - if (args['help'] as bool) { - print('Usage:'); - print(argParser.usage); - exit(0); - } - - final Directory publishRoot = filesystem.directory(args['output-dir']! as String); - final Directory packageRoot = publishRoot.parent; - if (!filesystem.directory(packageRoot).existsSync()) { - filesystem.directory(packageRoot).createSync(recursive: true); - } - - if (!filesystem.directory(publishRoot).existsSync()) { - filesystem.directory(publishRoot).createSync(recursive: true); - } - - final Configurator configurator = Configurator( - publishRoot: publishRoot, - packageRoot: packageRoot, - docsRoot: docsRoot, - ); - configurator.generateConfiguration(); - - final PlatformDocGenerator platformGenerator = PlatformDocGenerator(outputDir: publishRoot); - platformGenerator.generatePlatformDocs(); - - final DartdocGenerator dartdocGenerator = DartdocGenerator( - publishRoot: publishRoot, - packageRoot: packageRoot, - docsRoot: docsRoot, - useJson: args['json'] as bool? ?? true, - validateLinks: args['validate-links']! as bool, - verbose: args['verbose'] as bool? ?? false, - ); - - await dartdocGenerator.generateDartdoc(); - await configurator.generateOfflineAssetsIfNeeded(); -} - -ArgParser _createArgsParser({required String publishDefault}) { - final ArgParser parser = ArgParser(); - parser.addFlag('help', abbr: 'h', negatable: false, help: 'Show command help.'); - parser.addFlag('verbose', - 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('json', help: 'Display json-formatted output from dartdoc and skip stdout/stderr prefixing.'); - parser.addFlag('validate-links', help: 'Display warnings for broken links generated by dartdoc (slow)'); - parser.addOption('output-dir', defaultsTo: publishDefault, help: 'Sets the output directory for the documentation.'); - return parser; -} - -/// A class used to configure the staging area for building the docs in. -/// -/// The [generateConfiguration] function generates a dummy package with a -/// pubspec. It copies any assets and customization files from the framework -/// repo. It creates a metadata file for searches. -/// -/// Once the docs have been generated, [generateOfflineAssetsIfNeeded] will -/// create offline assets like Dash/Zeal docsets and an offline ZIP file of the -/// site if the build is a CI build that is not a presubmit build. -class Configurator { - Configurator({ - required this.docsRoot, - required this.publishRoot, - required this.packageRoot, - this.filesystem = const LocalFileSystem(), - }); - - /// The root of the directory in the Flutter repo where configuration data is - /// stored. - final Directory docsRoot; - - /// The root of the output area for the dartdoc docs. - /// - /// Typically this is a "doc" subdirectory under the [packageRoot]. - final Directory publishRoot; - - /// The root of the staging area for creating docs. - final Directory packageRoot; - - /// The [FileSystem] object used to create [File] and [Directory] objects. - final FileSystem filesystem; - - void generateConfiguration() { - final Version version = FlutterInformation.instance.getFlutterVersion(); - _createDummyPubspec(); - _createDummyLibrary(); - _createPageFooter(packageRoot, version); - _copyCustomizations(); - _createSearchMetadata( - docsRoot.childDirectory('lib').childFile('opensearch.xml'), publishRoot.childFile('opensearch.xml')); - } - - Future generateOfflineAssetsIfNeeded() async { - // Only create the offline docs if we're running in a non-presubmit build: - // it takes too long otherwise. - if (platform.environment.containsKey('LUCI_CI') && (platform.environment['LUCI_PR'] ?? '').isEmpty) { - _createOfflineZipFile(); - await _createDocset(); - _moveOfflineIntoPlace(); - _createRobotsTxt(); - } - } - - /// Returns import or on-disk paths for all libraries in the Flutter SDK. - Iterable _libraryRefs() sync* { - for (final Directory dir in findPackages()) { - final String dirName = dir.basename; - for (final FileSystemEntity file in dir.childDirectory('lib').listSync()) { - if (file is File && file.path.endsWith('.dart')) { - yield '$dirName/${file.basename}'; - } - } - } - - // Add a fake package for platform integration APIs. - yield '$kPlatformIntegrationPackageName/android.dart'; - yield '$kPlatformIntegrationPackageName/ios.dart'; - } - - void _createDummyPubspec() { - // Create the pubspec.yaml file. - final List pubspec = [ - 'name: $kDummyPackageName', - 'homepage: https://flutter.dev', - 'version: 0.0.0', - 'environment:', - " sdk: '>=3.0.0-0 <4.0.0'", - 'dependencies:', - for (final String package in findPackageNames()) ' $package:\n sdk: flutter', - ' $kPlatformIntegrationPackageName: 0.0.1', - 'dependency_overrides:', - ' $kPlatformIntegrationPackageName:', - ' path: ${docsRoot.childDirectory(kPlatformIntegrationPackageName).path}', - ]; - - packageRoot.childFile('pubspec.yaml').writeAsStringSync(pubspec.join('\n')); - } - - void _createDummyLibrary() { - final Directory libDir = packageRoot.childDirectory('lib'); - libDir.createSync(); - - final StringBuffer contents = StringBuffer('library temp_doc;\n\n'); - for (final String libraryRef in _libraryRefs()) { - contents.writeln("import 'package:$libraryRef';"); - } - packageRoot.childDirectory('lib') - ..createSync(recursive: true) - ..childFile('temp_doc.dart').writeAsStringSync(contents.toString()); - } - - void _createPageFooter(Directory footerPath, Version version) { - final String timestamp = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()); - final String gitBranch = FlutterInformation.instance.getBranchName(); - final String gitRevision = FlutterInformation.instance.getFlutterRevision(); - final String gitBranchOut = gitBranch.isEmpty ? '' : '• $gitBranch'; - footerPath.childFile('footer.html').writeAsStringSync(''); - publishRoot.childDirectory('flutter').childFile('footer.js') - ..createSync(recursive: true) - ..writeAsStringSync(''' -(function() { - var span = document.querySelector('footer>span'); - if (span) { - span.innerText = 'Flutter $version • $timestamp • $gitRevision $gitBranchOut'; - } - var sourceLink = document.querySelector('a.source-link'); - if (sourceLink) { - sourceLink.href = sourceLink.href.replace('/master/', '/$gitRevision/'); - } -})(); -'''); - } - - void _copyCustomizations() { - final List files = [ - 'README.md', - 'analysis_options.yaml', - 'dartdoc_options.yaml', - ]; - for (final String file in files) { - filesystem.file(docsRoot.childFile(file)).copySync(packageRoot.childFile(file).path); - } - final Directory assetsDir = filesystem.directory(publishRoot.childDirectory('assets')); - if (assetsDir.existsSync()) { - assetsDir.deleteSync(recursive: true); - } - copyDirectorySync(docsRoot.childDirectory('assets'), assetsDir, - (File src, File dest) => print('Copied ${src.path} to ${dest.path}')); - } - - /// Generates an OpenSearch XML description that can be used to add a custom - /// search for Flutter API docs to the browser. Unfortunately, it has to know - /// the URL to which site to search, so we customize it here based upon the - /// branch name. - void _createSearchMetadata(File templatePath, File metadataPath) { - final String template = templatePath.readAsStringSync(); - final String branch = FlutterInformation.instance.getBranchName(); - final String metadata = template.replaceAll( - '{SITE_URL}', - branch == 'stable' ? 'https://api.flutter.dev/' : 'https://master-api.flutter.dev/', - ); - metadataPath.parent.create(recursive: true); - metadataPath.writeAsStringSync(metadata); - } - - Future _createDocset() async { - // Must have dashing installed: go get -u github.com/technosophos/dashing - // Dashing produces a LOT of log output (~30MB), so we collect it, and just - // show the end of it if there was a problem. - print('${DateTime.now().toUtc()}: Building Flutter docset.'); - - // If dashing gets stuck, Cirrus will time out the build after an hour, and we - // never get to see the logs. Thus, we run it in the background and tail the - // logs only if it fails. - final ProcessWrapper result = ProcessWrapper( - await processManager.start( - [ - 'dashing', - 'build', - '--source', - publishRoot.path, - '--config', - docsRoot.childFile('dashing.json').path, - ], - workingDirectory: packageRoot.path, - ), - ); - final List buffer = []; - result.stdout.listen(buffer.addAll); - result.stderr.listen(buffer.addAll); - // If the dashing process exited with an error, print the last 200 lines of stderr and exit. - final int exitCode = await result.done; - if (exitCode != 0) { - print('Dashing docset generation failed with code $exitCode'); - final List output = systemEncoding.decode(buffer).split('\n'); - print(output.sublist(math.max(output.length - 200, 0)).join('\n')); - exit(exitCode); - } - buffer.clear(); - - // Copy the favicon file to the output directory. - final File faviconFile = - publishRoot.childDirectory('flutter').childDirectory('static-assets').childFile('favicon.png'); - final File iconFile = packageRoot.childDirectory('flutter.docset').childFile('icon.png'); - faviconFile..createSync(recursive: true)..copySync(iconFile.path); - - // Post-process the dashing output. - final File infoPlist = - packageRoot.childDirectory('flutter.docset').childDirectory('Contents').childFile('Info.plist'); - String contents = infoPlist.readAsStringSync(); - - // Since I didn't want to add the XML package as a dependency just for this, - // I just used a regular expression to make this simple change. - final RegExp findRe = RegExp(r'(\s*DocSetPlatformFamily\s*)[^<]+()', multiLine: true); - contents = contents.replaceAllMapped(findRe, (Match match) { - return '${match.group(1)}dartlang${match.group(2)}'; - }); - infoPlist.writeAsStringSync(contents); - final Directory offlineDir = publishRoot.childDirectory('offline'); - if (!offlineDir.existsSync()) { - offlineDir.createSync(recursive: true); - } - tarDirectory(packageRoot, offlineDir.childFile('flutter.docset.tar.gz')); - - // Write the Dash/Zeal XML feed file. - final bool isStable = platform.environment['LUCI_BRANCH'] == 'stable'; - offlineDir.childFile('flutter.xml').writeAsStringSync('\n' - ' ${FlutterInformation.instance.getFlutterVersion()}\n' - ' https://${isStable ? '' : 'master-'}api.flutter.dev/offline/flutter.docset.tar.gz\n' - '\n'); - } - - // Creates the offline ZIP file containing all of the website HTML files. - void _createOfflineZipFile() { - print('${DateTime.now().toLocal()}: Creating offline docs archive.'); - zipDirectory(publishRoot, packageRoot.childFile('flutter.docs.zip')); - } - - // Moves the generated offline archives into the publish directory so that - // they can be included in the output ZIP file. - void _moveOfflineIntoPlace() { - print('${DateTime.now().toUtc()}: Moving offline docs into place.'); - final Directory offlineDir = publishRoot.childDirectory('offline')..createSync(recursive: true); - packageRoot.childFile('flutter.docs.zip').renameSync(offlineDir.childFile('flutter.docs.zip').path); - } - - // Creates a robots.txt file that disallows indexing unless the branch is the - // stable branch. - void _createRobotsTxt() { - final File robotsTxt = publishRoot.childFile('robots.txt'); - if (FlutterInformation.instance.getBranchName() == 'stable') { - robotsTxt.writeAsStringSync('# All robots welcome!'); - } else { - robotsTxt.writeAsStringSync('User-agent: *\nDisallow: /'); - } - } -} - -/// Runs Dartdoc inside of the given pre-prepared staging area, prepared by -/// [Configurator.generateConfiguration]. -/// -/// Performs a sanity check of the output once the generation is complete. -class DartdocGenerator { - DartdocGenerator({ - required this.docsRoot, - required this.publishRoot, - required this.packageRoot, - this.filesystem = const LocalFileSystem(), - this.useJson = true, - this.validateLinks = true, - this.verbose = false, - }); - - /// The root of the directory in the Flutter repo where configuration data is - /// stored. - final Directory docsRoot; - - /// The root of the output area for the dartdoc docs. - /// - /// Typically this is a "doc" subdirectory under the [packageRoot]. - final Directory publishRoot; - - /// The root of the staging area for creating docs. - final Directory packageRoot; - - /// The [FileSystem] object used to create [File] and [Directory] objects. - final FileSystem filesystem; - - /// Whether or not dartdoc should output an index.json file of the - /// documentation. - final bool useJson; - - // Whether or not to have dartdoc validate its own links. - final bool validateLinks; - - /// Whether or not to filter overly verbose log output from dartdoc. - final bool verbose; - - Future generateDartdoc() async { - final Directory flutterRoot = FlutterInformation.instance.getFlutterRoot(); - final Map pubEnvironment = { - 'FLUTTER_ROOT': flutterRoot.absolute.path, - }; - - // If there's a .pub-cache dir in the Flutter root, use that. - final File pubCache = flutterRoot.childFile('.pub-cache'); - if (pubCache.existsSync()) { - pubEnvironment['PUB_CACHE'] = pubCache.path; - } - - // Run pub. - ProcessWrapper process = ProcessWrapper(await runPubProcess( - arguments: ['get'], - workingDirectory: packageRoot, - environment: pubEnvironment, - )); - printStream(process.stdout, prefix: 'pub:stdout: '); - printStream(process.stderr, prefix: 'pub:stderr: '); - final int code = await process.done; - if (code != 0) { - exit(code); - } - - final Version version = FlutterInformation.instance.getFlutterVersion(); - - // Verify which version of snippets and dartdoc we're using. - final ProcessResult snippetsResult = Process.runSync( - FlutterInformation.instance.getDartBinaryPath().path, - [ - 'pub', - 'global', - 'list', - ], - workingDirectory: packageRoot.path, - environment: pubEnvironment, - stdoutEncoding: utf8, - ); - print(''); - final Iterable versionMatches = - RegExp(r'^(?snippets|dartdoc) (?[^\s]+)', multiLine: true) - .allMatches(snippetsResult.stdout as String); - for (final RegExpMatch match in versionMatches) { - print('${match.namedGroup('name')} version: ${match.namedGroup('version')}'); - } - - print('flutter version: $version\n'); - - // Dartdoc warnings and errors in these packages are considered fatal. - // All packages owned by flutter should be in the list. - final List flutterPackages = [ - kDummyPackageName, - kPlatformIntegrationPackageName, - ...findPackageNames(), - // TODO(goderbauer): Figure out how to only include `dart:ui` of - // `sky_engine` below, https://github.com/dart-lang/dartdoc/issues/2278. - // 'sky_engine', - ]; - - // 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 packageRoot - final List dartdocArgs = [ - 'global', - 'run', - '--enable-asserts', - 'dartdoc', - '--output', - publishRoot.childDirectory('flutter').path, - '--allow-tools', - if (useJson) '--json', - if (validateLinks) '--validate-links' else '--no-validate-links', - '--link-to-source-excludes', - flutterRoot.childDirectory('bin').childDirectory('cache').path, - '--link-to-source-root', - flutterRoot.path, - '--link-to-source-uri-template', - 'https://github.com/flutter/flutter/blob/master/%f%#L%l%', - '--inject-html', - '--use-base-href', - '--header', - docsRoot.childFile('styles.html').path, - '--header', - docsRoot.childFile('analytics.html').path, - '--header', - docsRoot.childFile('survey.html').path, - '--header', - docsRoot.childFile('snippets.html').path, - '--header', - docsRoot.childFile('opensearch.html').path, - '--footer-text', - packageRoot.childFile('footer.html').path, - '--allow-warnings-in-packages', - flutterPackages.join(','), - '--exclude-packages', - [ - 'analyzer', - 'args', - 'barback', - 'csslib', - 'flutter_goldens', - 'flutter_goldens_client', - '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', - [ - 'dart:io/network_policy.dart', // dart-lang/dartdoc#2437 - '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', - docsRoot.childFile('favicon.ico').absolute.path, - '--package-order', - 'flutter,Dart,$kPlatformIntegrationPackageName,flutter_test,flutter_driver', - '--auto-include-dependencies', - ]; - - String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg; - print('Executing: (cd "${packageRoot.path}" ; ' - '${FlutterInformation.instance.getDartBinaryPath().path} ' - '${dartdocArgs.map(quote).join(' ')})'); - - process = ProcessWrapper(await runPubProcess( - arguments: dartdocArgs, - workingDirectory: packageRoot, - environment: pubEnvironment, - )); - printStream( - process.stdout, - prefix: useJson ? '' : 'dartdoc:stdout: ', - filter: [ - if (!verbose) RegExp(r'^Generating docs for library '), // Unnecessary verbosity - ], - ); - printStream( - process.stderr, - prefix: useJson ? '' : 'dartdoc:stderr: ', - filter: [ - if (!verbose) - RegExp( - // Remove warnings from packages outside our control - r'^ warning: .+: \(.+[\\/]\.pub-cache[\\/]hosted[\\/]pub.dartlang.org[\\/].+\)', - ), - ], - ); - final int exitCode = await process.done; - - if (exitCode != 0) { - exit(exitCode); - } - - _sanityCheckDocs(); - checkForUnresolvedDirectives(publishRoot.childDirectory('flutter').path); - - _createIndexAndCleanup(); - - print('Documentation written to ${publishRoot.path}'); - } - - void _sanityCheckExample(String fileString, String regExpString) { - final File file = filesystem.file(fileString); - if (file.existsSync()) { - final RegExp regExp = RegExp(regExpString, dotAll: true); - final String contents = file.readAsStringSync(); - if (!regExp.hasMatch(contents)) { - throw Exception("Missing example code matching '$regExpString' in ${file.path}."); - } - } else { - throw Exception( - "Missing example code sanity test file ${file.path}. Either it didn't get published, or you might have to update the test to look at a different file."); - } - } - - /// Runs a sanity check by running a test. - void _sanityCheckDocs([Platform platform = const LocalPlatform()]) { - final Directory flutterDirectory = publishRoot.childDirectory('flutter'); - final Directory widgetsDirectory = flutterDirectory.childDirectory('widgets'); - - final List canaries = [ - publishRoot.childDirectory('assets').childFile('overrides.css'), - flutterDirectory.childDirectory('dart-io').childFile('File-class.html'), - flutterDirectory.childDirectory('dart-ui').childFile('Canvas-class.html'), - flutterDirectory.childDirectory('dart-ui').childDirectory('Canvas').childFile('drawRect.html'), - flutterDirectory - .childDirectory('flutter_driver') - .childDirectory('FlutterDriver') - .childFile('FlutterDriver.connectedTo.html'), - flutterDirectory.childDirectory('flutter_test').childDirectory('WidgetTester').childFile('pumpWidget.html'), - flutterDirectory.childDirectory('material').childFile('Material-class.html'), - flutterDirectory.childDirectory('material').childFile('Tooltip-class.html'), - widgetsDirectory.childFile('Widget-class.html'), - widgetsDirectory.childFile('Listener-class.html'), - ]; - for (final File canary in canaries) { - if (!canary.existsSync()) { - throw Exception('Missing "${canary.path}", which probably means the documentation failed to build correctly.'); - } - } - // Make sure at least one example of each kind includes source code. - - // Check a "sample" example, any one will do. - _sanityCheckExample( - widgetsDirectory.childFile('showGeneralDialog.html').path, - r'\s*\s*import 'package:flutter/material.dart';', - ); - - // Check a "snippet" example, any one will do. - _sanityCheckExample( - widgetsDirectory.childDirectory('ModalRoute').childFile('barrierColor.html').path, - r'\s*.*Color\s+get\s+barrierColor.*', - ); - - // Check a "dartpad" example, any one will do, and check for the correct URL - // arguments. - // Just use "master" for any branch other than the LUCI_BRANCH. - final String? luciBranch = platform.environment['LUCI_BRANCH']?.trim(); - final String expectedBranch = luciBranch != null && luciBranch.isNotEmpty ? luciBranch : 'master'; - final List argumentRegExps = [ - r'split=\d+', - r'run=true', - r'sample_id=widgets\.Listener\.\d+', - 'sample_channel=$expectedBranch', - 'channel=$expectedBranch', - ]; - for (final String argumentRegExp in argumentRegExps) { - _sanityCheckExample( - widgetsDirectory.childFile('Listener-class.html').path, - r'\s*\s*<\/iframe>', - ); - } - } - - /// 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 ${publishRoot.childFile('index.html').path}'); - _copyIndexToRootOfDocs(); - _addHtmlBaseToIndex(); - _changePackageToSdkInTitlebar(); - _putRedirectInOldIndexLocation(); - _writeSnippetsIndexFile(); - print('\nDocs ready to go!'); - } - - void _copyIndexToRootOfDocs() { - publishRoot.childDirectory('flutter').childFile('index.html').copySync(publishRoot.childFile('index.html').path); - } - - void _changePackageToSdkInTitlebar() { - final File indexFile = publishRoot.childFile('index.html'); - String indexContents = indexFile.readAsStringSync(); - indexContents = indexContents.replaceFirst( - '
  • Flutter package
  • ', - '
  • Flutter SDK
  • ', - ); - - indexFile.writeAsStringSync(indexContents); - } - - void _addHtmlBaseToIndex() { - final File indexFile = publishRoot.childFile('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 = ''; - publishRoot.childDirectory('flutter').childFile('index.html').writeAsStringSync(metaTag); - } - - void _writeSnippetsIndexFile() { - final Directory snippetsDir = publishRoot.childDirectory('snippets'); - if (snippetsDir.existsSync()) { - const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' '); - final Iterable files = - snippetsDir.listSync().whereType().where((File file) => path.extension(file.path) == '.json'); - // Combine all the metadata into a single JSON array. - final Iterable fileContents = files.map((File file) => file.readAsStringSync()); - final List metadataObjects = fileContents.map(json.decode).toList(); - final String jsonArray = jsonEncoder.convert(metadataObjects); - snippetsDir.childFile('index.json').writeAsStringSync(jsonArray); - } - } -} - -/// Downloads and unpacks the platform specific documentation generated by the -/// engine build. -/// -/// Unpacks and massages the data so that it can be properly included in the -/// output archive. -class PlatformDocGenerator { - PlatformDocGenerator({required this.outputDir, this.filesystem = const LocalFileSystem()}); - - final FileSystem filesystem; - final Directory outputDir; - final String engineRevision = FlutterInformation.instance.getEngineRevision(); - - /// This downloads an archive of platform docs for the engine from the artifact - /// store and extracts them to the location used for Dartdoc. - void generatePlatformDocs() { - final String javadocUrl = - 'https://storage.googleapis.com/flutter_infra_release/flutter/$engineRevision/android-javadoc.zip'; - _extractDocs(javadocUrl, 'javadoc', 'io/flutter/view/FlutterView.html', outputDir); - - final String objcdocUrl = - 'https://storage.googleapis.com/flutter_infra_release/flutter/$engineRevision/ios-objcdoc.zip'; - _extractDocs(objcdocUrl, 'objcdoc', 'Classes/FlutterViewController.html', outputDir); - } - - /// Fetches the zip archive at the specified url. - /// - /// Returns null if the archive fails to download after [maxTries] attempts. - Future _fetchArchive(String url, int maxTries) async { - List? responseBytes; - for (int i = 0; i < maxTries; i++) { - final http.Response response = await http.get(Uri.parse(url)); - if (response.statusCode == 200) { - responseBytes = response.bodyBytes; - break; - } - stderr.writeln('Failed attempt ${i + 1} to fetch $url.'); - - // On failure print a short snipped from the body in case it's helpful. - final int bodyLength = math.min(1024, response.body.length); - stderr.writeln('Response status code ${response.statusCode}. Body: ${response.body.substring(0, bodyLength)}'); - sleep(const Duration(seconds: 1)); - } - return responseBytes == null ? null : ZipDecoder().decodeBytes(responseBytes); - } - - Future _extractDocs(String url, String docName, String checkFile, Directory outputDir) async { - const int maxTries = 5; - final Archive? archive = await _fetchArchive(url, maxTries); - if (archive == null) { - stderr.writeln('Failed to fetch zip archive from: $url after $maxTries attempts. Giving up.'); - exit(1); - } - - final Directory output = outputDir.childDirectory(docName); - print('Extracting $docName to ${output.path}'); - output.createSync(recursive: true); - - for (final ArchiveFile af in archive) { - if (!af.name.endsWith('/')) { - final File file = filesystem.file('${output.path}/${af.name}'); - file.createSync(recursive: true); - file.writeAsBytesSync(af.content as List); - } - } - - /// If object then copy files to old location if the archive is using the new location. - final Directory objcDocsDir = output.childDirectory('objectc_docs'); - if (objcDocsDir.existsSync()) { - copyDirectorySync(objcDocsDir, output); - } - - final File testFile = output.childFile(checkFile); - if (!testFile.existsSync()) { - print('Expected file ${testFile.path} not found'); - exit(1); - } - print('$docName ready to go!'); - } -} - -/// 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 Function(File srcFile, File destFile)? onFileCopied]) { - if (!srcDir.existsSync()) { - throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy'); - } - - if (!destDir.existsSync()) { - destDir.createSync(recursive: true); - } - - for (final FileSystemEntity entity in srcDir.listSync()) { - final String newPath = path.join(destDir.path, path.basename(entity.path)); - if (entity is File) { - final File newFile = filesystem.file(newPath); - entity.copySync(newPath); - onFileCopied?.call(entity, newFile); - } else if (entity is Directory) { - copyDirectorySync(entity, filesystem.directory(newPath)); - } else { - throw Exception('${entity.path} is neither File nor Directory'); - } - } -} - -void printStream(Stream> stream, {String prefix = '', List filter = const []}) { - stream.transform(utf8.decoder).transform(const LineSplitter()).listen((String line) { - if (!filter.any((Pattern pattern) => line.contains(pattern))) { - print('$prefix$line'.trim()); - } - }); -} - -void zipDirectory(Directory src, File output) { - // We would use the archive package to do this in one line, but it - // is a lot slower, and doesn't do compression nearly as well. - final ProcessResult zipProcess = processManager.runSync( - [ - 'zip', - '-r', - '-9', - '-q', - output.path, - '.', - ], - workingDirectory: src.path, - ); - - if (zipProcess.exitCode != 0) { - print('Creating offline ZIP archive ${output.path} failed:'); - print(zipProcess.stderr); - exit(1); - } -} - -void tarDirectory(Directory src, File output) { - // We would use the archive package to do this in one line, but it - // is a lot slower, and doesn't do compression nearly as well. - final ProcessResult tarProcess = processManager.runSync( - [ - 'tar', - 'cf', - output.path, - '--use-compress-program', - 'gzip --best', - 'flutter.docset', - ], - workingDirectory: src.path, - ); - - if (tarProcess.exitCode != 0) { - print('Creating a tarball ${output.path} failed:'); - print(tarProcess.stderr); - exit(1); - } -} - -Future runPubProcess({ - required List arguments, - Directory? workingDirectory, - Map? environment, - @visibleForTesting ProcessManager processManager = const LocalProcessManager(), - @visibleForTesting FileSystem filesystem = const LocalFileSystem(), -}) { - return processManager.start( - [FlutterInformation.instance.getDartBinaryPath().path, 'pub', ...arguments], - workingDirectory: (workingDirectory ?? filesystem.currentDirectory).path, - environment: environment, - ); -} - -List findPackageNames() { - return findPackages().map((FileSystemEntity file) => path.basename(file.path)).toList(); -} - -/// Finds all packages in the Flutter SDK -List findPackages() { - return FlutterInformation.instance - .getFlutterRoot() - .childDirectory('packages') - .listSync() - .where((FileSystemEntity entity) { - if (entity is! Directory) { - return false; - } - final File pubspec = filesystem.file('${entity.path}/pubspec.yaml'); - if (!pubspec.existsSync()) { - print("Unexpected package '${entity.path}' found in packages directory"); - return false; - } - // TODO(ianh): Use a real YAML parser here - return !pubspec.readAsStringSync().contains('nodoc: true'); - }) - .cast() - .toList(); -} - -/// An exception class used to indicate problems when collecting information. -class DartdocException implements Exception { - DartdocException(this.message, {this.file, this.line}); - final String message; - final String? file; - final int? line; - - @override - String toString() { - if (file != null || line != null) { - final String fileStr = file == null ? '' : '$file:'; - final String lineStr = line == null ? '' : '$line:'; - return '$runtimeType: $fileStr$lineStr: $message'; - } else { - return '$runtimeType: $message'; - } - } -} - -/// A singleton used to consolidate the way in which information about the -/// Flutter repo and environment is collected. -/// -/// Collects the information once, and caches it for any later access. -/// -/// The singleton instance can be overridden by tests by setting [instance]. -class FlutterInformation { - FlutterInformation({ - this.platform = const LocalPlatform(), - this.processManager = const LocalProcessManager(), - this.filesystem = const LocalFileSystem(), - }); - - final Platform platform; - final ProcessManager processManager; - final FileSystem filesystem; - - static FlutterInformation? _instance; - - static FlutterInformation get instance => _instance ??= FlutterInformation(); - - @visibleForTesting - static set instance(FlutterInformation? value) => _instance = value; - - /// The path to the Dart binary in the Flutter repo. - /// - /// This is probably a shell script. - File getDartBinaryPath() { - return getFlutterRoot().childDirectory('bin').childFile('dart'); - } - - /// The path to the Flutter repo root directory. - /// - /// If the environment variable `FLUTTER_ROOT` is set, will use that instead - /// of looking for it. - /// - /// Otherwise, uses the output of `flutter --version --machine` to find the - /// Flutter root. - Directory getFlutterRoot() { - if (platform.environment['FLUTTER_ROOT'] != null) { - return filesystem.directory(platform.environment['FLUTTER_ROOT']); - } - return getFlutterInformation()['flutterRoot']! as Directory; - } - - /// Gets the semver version of the Flutter framework in the repo. - Version getFlutterVersion() => getFlutterInformation()['frameworkVersion']! as Version; - - /// Gets the git hash of the engine used by the Flutter framework in the repo. - String getEngineRevision() => getFlutterInformation()['engineRevision']! as String; - - /// Gets the git hash of the Flutter framework in the repo. - String getFlutterRevision() => getFlutterInformation()['flutterGitRevision']! as String; - - /// Gets the name of the current branch in the Flutter framework in the repo. - String getBranchName() => getFlutterInformation()['branchName']! as String; - - Map? _cachedFlutterInformation; - - /// Gets a Map of various kinds of information about the Flutter repo. - Map getFlutterInformation() { - if (_cachedFlutterInformation != null) { - return _cachedFlutterInformation!; - } - - String flutterVersionJson; - if (platform.environment['FLUTTER_VERSION'] != null) { - flutterVersionJson = platform.environment['FLUTTER_VERSION']!; - } else { - String flutterCommand; - if (platform.environment['FLUTTER_ROOT'] != null) { - flutterCommand = filesystem - .directory(platform.environment['FLUTTER_ROOT']) - .childDirectory('bin') - .childFile('flutter') - .absolute - .path; - } else { - flutterCommand = 'flutter'; - } - ProcessResult result; - try { - result = processManager.runSync([flutterCommand, '--version', '--machine'], stdoutEncoding: utf8); - } on ProcessException catch (e) { - throw DartdocException( - 'Unable to determine Flutter information. Either set FLUTTER_ROOT, or place flutter command in your path.\n$e'); - } - if (result.exitCode != 0) { - throw DartdocException('Unable to determine Flutter information, because of abnormal exit to flutter command.'); - } - flutterVersionJson = (result.stdout as String) - .replaceAll('Waiting for another flutter command to release the startup lock...', ''); - } - - final Map flutterVersion = json.decode(flutterVersionJson) as Map; - if (flutterVersion['flutterRoot'] == null || - flutterVersion['frameworkVersion'] == null || - flutterVersion['dartSdkVersion'] == null) { - throw DartdocException( - 'Flutter command output has unexpected format, unable to determine flutter root location.'); - } - - final Map info = {}; - final Directory flutterRoot = filesystem.directory(flutterVersion['flutterRoot']! as String); - info['flutterRoot'] = flutterRoot; - info['frameworkVersion'] = Version.parse(flutterVersion['frameworkVersion'] as String); - info['engineRevision'] = flutterVersion['engineRevision'] as String; - - final RegExpMatch? dartVersionRegex = RegExp(r'(?[\d.]+)(?:\s+\(build (?[-.\w]+)\))?') - .firstMatch(flutterVersion['dartSdkVersion'] as String); - if (dartVersionRegex == null) { - throw DartdocException( - 'Flutter command output has unexpected format, unable to parse dart SDK version ${flutterVersion['dartSdkVersion']}.'); - } - info['dartSdkVersion'] = - Version.parse(dartVersionRegex.namedGroup('detail') ?? dartVersionRegex.namedGroup('base')!); - - info['branchName'] = _getBranchName(); - info['flutterGitRevision'] = _getFlutterGitRevision(); - _cachedFlutterInformation = info; - - return info; - } - - // Get the name of the release branch. - // - // On LUCI builds, the git HEAD is detached, so first check for the env - // variable "LUCI_BRANCH"; if it is not set, fall back to calling git. - String _getBranchName() { - final String? luciBranch = platform.environment['LUCI_BRANCH']; - if (luciBranch != null && luciBranch.trim().isNotEmpty) { - return luciBranch.trim(); - } - final ProcessResult gitResult = processManager.runSync(['git', 'status', '-b', '--porcelain']); - if (gitResult.exitCode != 0) { - throw 'git status exit with non-zero exit code: ${gitResult.exitCode}'; - } - final RegExp gitBranchRegexp = RegExp(r'^## (.*)'); - final RegExpMatch? gitBranchMatch = - gitBranchRegexp.firstMatch((gitResult.stdout as String).trim().split('\n').first); - return gitBranchMatch == null ? '' : gitBranchMatch.group(1)!.split('...').first; - } - - // Get the git revision for the repo. - String _getFlutterGitRevision() { - const int kGitRevisionLength = 10; - - final ProcessResult gitResult = Process.runSync('git', ['rev-parse', 'HEAD']); - if (gitResult.exitCode != 0) { - throw 'git rev-parse exit with non-zero exit code: ${gitResult.exitCode}'; - } - final String gitRevision = (gitResult.stdout as String).trim(); - - return gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision; - } -} diff --git a/dev/tools/dartdoc.dart b/dev/tools/dartdoc.dart new file mode 100644 index 0000000000..8bd8b39d73 --- /dev/null +++ b/dev/tools/dartdoc.dart @@ -0,0 +1,606 @@ +// Copyright 2014 The Flutter 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:convert'; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:intl/intl.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; + +import 'dartdoc_checker.dart'; + +const String kDocsRoot = 'dev/docs'; +const String kPublishRoot = '$kDocsRoot/doc'; + +const String kDummyPackageName = 'Flutter'; +const String kPlatformIntegrationPackageName = 'platform_integration'; + +/// 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 api.flutter.dev. We are keeping the files inside of +/// api.flutter.dev/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'] as bool) { + 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: $kDummyPackageName'); + buf.writeln('homepage: https://flutter.dev'); + buf.writeln('version: 0.0.0'); + buf.writeln('environment:'); + buf.writeln(" sdk: '>=3.0.0-0 <4.0.0'"); + buf.writeln('dependencies:'); + for (final String package in findPackageNames()) { + buf.writeln(' $package:'); + buf.writeln(' sdk: flutter'); + } + buf.writeln(' $kPlatformIntegrationPackageName: 0.0.1'); + buf.writeln('dependency_overrides:'); + buf.writeln(' $kPlatformIntegrationPackageName:'); + buf.writeln(' path: $kPlatformIntegrationPackageName'); + 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 (final 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 dartExecutable = '$flutterRoot/bin/cache/dart-sdk/bin/dart'; + + // Run pub. + ProcessWrapper process = ProcessWrapper(await runPubProcess( + dartBinaryPath: dartExecutable, + arguments: ['get'], + workingDirectory: kDocsRoot, + environment: pubEnvironment, + )); + printStream(process.stdout, prefix: 'pub:stdout: '); + printStream(process.stderr, prefix: 'pub:stderr: '); + final int code = await process.done; + if (code != 0) { + exit(code); + } + + createFooter('$kDocsRoot/lib/', version); + copyAssets(); + createSearchMetadata('$kDocsRoot/lib/opensearch.xml', '$kDocsRoot/doc/opensearch.xml'); + cleanOutSnippets(); + + final List dartdocBaseArgs = [ + 'global', + 'run', + if (args['checked'] as bool) '--enable-asserts', + 'dartdoc', + ]; + + // Verify which version of snippets and dartdoc we're using. + final ProcessResult snippetsResult = Process.runSync( + dartExecutable, + [ + 'pub', + 'global', + 'list', + ], + workingDirectory: kDocsRoot, + environment: pubEnvironment, + stdoutEncoding: utf8, + ); + print(''); + final Iterable versionMatches = RegExp(r'^(?snippets|dartdoc) (?[^\s]+)', multiLine: true) + .allMatches(snippetsResult.stdout as String); + for (final RegExpMatch match in versionMatches) { + print('${match.namedGroup('name')} version: ${match.namedGroup('version')}'); + } + + print('flutter version: $version\n'); + + // Dartdoc warnings and errors in these packages are considered fatal. + // All packages owned by flutter should be in the list. + final List flutterPackages = [ + kDummyPackageName, + kPlatformIntegrationPackageName, + ...findPackageNames(), + // TODO(goderbauer): Figure out how to only include `dart:ui` of `sky_engine` below, https://github.com/dart-lang/dartdoc/issues/2278. + // 'sky_engine', + ]; + + // 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 = [ + ...dartdocBaseArgs, + '--allow-tools', + if (args['json'] as bool) '--json', + if (args['validate-links'] as bool) '--validate-links' else '--no-validate-links', + '--link-to-source-excludes', '../../bin/cache', + '--link-to-source-root', '../..', + '--link-to-source-uri-template', 'https://github.com/flutter/flutter/blob/master/%f%#L%l%', + '--inject-html', + '--use-base-href', + '--header', 'styles.html', + '--header', 'analytics.html', + '--header', 'survey.html', + '--header', 'snippets.html', + '--header', 'opensearch.html', + '--footer-text', 'lib/footer.html', + '--allow-warnings-in-packages', flutterPackages.join(','), + '--exclude-packages', + [ + 'analyzer', + 'args', + 'barback', + 'csslib', + 'flutter_goldens', + 'flutter_goldens_client', + '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', + [ + 'dart:io/network_policy.dart', // dart-lang/dartdoc#2437 + '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,$kPlatformIntegrationPackageName,flutter_test,flutter_driver', + '--auto-include-dependencies', + ]; + + String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg; + print('Executing: (cd $kDocsRoot ; $dartExecutable ${dartdocArgs.map(quote).join(' ')})'); + + process = ProcessWrapper(await runPubProcess( + dartBinaryPath: dartExecutable, + arguments: dartdocArgs, + workingDirectory: kDocsRoot, + environment: pubEnvironment, + )); + printStream(process.stdout, prefix: args['json'] as bool ? '' : 'dartdoc:stdout: ', + filter: args['verbose'] as bool ? const [] : [ + RegExp(r'^Generating docs for library '), // unnecessary verbosity + ], + ); + printStream(process.stderr, prefix: args['json'] as bool ? '' : 'dartdoc:stderr: ', + filter: args['verbose'] as bool ? const [] : [ + RegExp(r'^ warning: .+: \(.+/\.pub-cache/hosted/pub.dartlang.org/.+\)'), // packages outside our control + ], + ); + final int exitCode = await process.done; + + if (exitCode != 0) { + exit(exitCode); + } + + sanityCheckDocs(); + checkForUnresolvedDirectives('$kPublishRoot/api'); + + createIndexAndCleanup(); +} + +ArgParser _createArgsParser() { + final ArgParser parser = ArgParser(); + parser.addFlag('help', abbr: 'h', negatable: false, + help: 'Show command help.'); + parser.addFlag('verbose', 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', + help: 'Run dartdoc with asserts enabled.'); + parser.addFlag('json', + help: 'Display json-formatted output from dartdoc and skip stdout/stderr prefixing.'); + parser.addFlag('validate-links', + help: 'Display warnings for broken links generated by dartdoc (slow)'); + return parser; +} + +final RegExp gitBranchRegexp = RegExp(r'^## (.*)'); + +/// Get the name of the release branch. +/// +/// On LUCI builds, the git HEAD is detached, so first check for the env +/// variable "LUCI_BRANCH"; if it is not set, fall back to calling git. +String getBranchName({ + @visibleForTesting + Platform platform = const LocalPlatform(), + @visibleForTesting + ProcessManager processManager = const LocalProcessManager(), +}) { + final String? luciBranch = platform.environment['LUCI_BRANCH']; + if (luciBranch != null && luciBranch.trim().isNotEmpty) { + return luciBranch.trim(); + } + final ProcessResult gitResult = processManager.runSync(['git', 'status', '-b', '--porcelain']); + if (gitResult.exitCode != 0) { + throw 'git status exit with non-zero exit code: ${gitResult.exitCode}'; + } + final RegExpMatch? gitBranchMatch = gitBranchRegexp.firstMatch( + (gitResult.stdout as String).trim().split('\n').first); + return gitBranchMatch == null ? '' : gitBranchMatch.group(1)!.split('...').first; +} + +String gitRevision() { + const int kGitRevisionLength = 10; + + final ProcessResult gitResult = Process.runSync('git', ['rev-parse', 'HEAD']); + if (gitResult.exitCode != 0) { + throw 'git rev-parse exit with non-zero exit code: ${gitResult.exitCode}'; + } + final String gitRevision = (gitResult.stdout as String).trim(); + + return gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision; +} + +void createFooter(String footerPath, String version) { + final String timestamp = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()); + final String gitBranch = getBranchName(); + final String gitBranchOut = gitBranch.isEmpty ? '' : '• $gitBranch'; + File('${footerPath}footer.html').writeAsStringSync(''); + File('$kPublishRoot/api/footer.js') + ..createSync(recursive: true) + ..writeAsStringSync(''' +(function() { + var span = document.querySelector('footer>span'); + if (span) { + span.innerText = 'Flutter $version • $timestamp • ${gitRevision()} $gitBranchOut'; + } + var sourceLink = document.querySelector('a.source-link'); + if (sourceLink) { + sourceLink.href = sourceLink.href.replace('/master/', '/${gitRevision()}/'); + } +})(); +'''); +} + +/// Generates an OpenSearch XML description that can be used to add a custom +/// search for Flutter API docs to the browser. Unfortunately, it has to know +/// the URL to which site to search, so we customize it here based upon the +/// branch name. +void createSearchMetadata(String templatePath, String metadataPath) { + final String template = File(templatePath).readAsStringSync(); + final String branch = getBranchName(); + final String metadata = template.replaceAll( + '{SITE_URL}', + branch == 'stable' ? 'https://api.flutter.dev/' : 'https://master-api.flutter.dev/', + ); + Directory(path.dirname(metadataPath)).create(recursive: true); + File(metadataPath).writeAsStringSync(metadata); +} + +/// 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 Function(File srcFile, File destFile)? onFileCopied]) { + if (!srcDir.existsSync()) { + throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy'); + } + + if (!destDir.existsSync()) { + destDir.createSync(recursive: true); + } + + for (final 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}')); +} + +/// Clean out any existing snippets so that we don't publish old files from +/// previous runs accidentally. +void cleanOutSnippets() { + final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets')); + if (snippetsDir.existsSync()) { + snippetsDir + ..deleteSync(recursive: true) + ..createSync(recursive: true); + } +} + +void _sanityCheckExample(String fileString, String regExpString) { + final File file = File(fileString); + if (file.existsSync()) { + final RegExp regExp = RegExp(regExpString, dotAll: true); + final String contents = file.readAsStringSync(); + if (!regExp.hasMatch(contents)) { + throw Exception("Missing example code matching '$regExpString' in ${file.path}."); + } + } else { + throw Exception( + "Missing example code sanity test file ${file.path}. Either it didn't get published, or you might have to update the test to look at a different file."); + } +} + +/// Runs a sanity check by running a test. +void sanityCheckDocs([Platform platform = const LocalPlatform()]) { + 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', + '$kPublishRoot/api/widgets/Listener-class.html', + ]; + for (final String canary in canaries) { + if (!File(canary).existsSync()) { + throw Exception('Missing "$canary", which probably means the documentation failed to build correctly.'); + } + } + // Make sure at least one example of each kind includes source code. + + // Check a "sample" example, any one will do. + _sanityCheckExample( + '$kPublishRoot/api/widgets/showGeneralDialog.html', + r'\s*\s*import 'package:flutter/material.dart';', + ); + + // Check a "snippet" example, any one will do. + _sanityCheckExample( + '$kPublishRoot/api/widgets/ModalRoute/barrierColor.html', + r'\s*.*Color\s+get\s+barrierColor.*', + ); + + // Check a "dartpad" example, any one will do, and check for the correct URL + // arguments. + // Just use "master" for any branch other than the LUCI_BRANCH. + final String? luciBranch = platform.environment['LUCI_BRANCH']?.trim(); + final String expectedBranch = luciBranch != null && luciBranch.isNotEmpty ? luciBranch : 'master'; + final List argumentRegExps = [ + r'split=\d+', + r'run=true', + r'sample_id=widgets\.Listener\.\d+', + 'sample_channel=$expectedBranch', + 'channel=$expectedBranch', + ]; + for (final String argumentRegExp in argumentRegExps) { + _sanityCheckExample( + '$kPublishRoot/api/widgets/Listener-class.html', + r'\s*\s*<\/iframe>', + ); + } +} + +/// 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(); + writeSnippetsIndexFile(); + 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); +} + +void writeSnippetsIndexFile() { + final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets')); + if (snippetsDir.existsSync()) { + const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' '); + final Iterable files = snippetsDir + .listSync() + .whereType() + .where((File file) => path.extension(file.path) == '.json'); + // Combine all the metadata into a single JSON array. + final Iterable fileContents = files.map((File file) => file.readAsStringSync()); + final List metadataObjects = fileContents.map(json.decode).toList(); + final String jsonArray = jsonEncoder.convert(metadataObjects); + File('$kPublishRoot/snippets/index.json').writeAsStringSync(jsonArray); + } +} + +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'); + if (!pubspec.existsSync()) { + print("Unexpected package '${entity.path}' found in packages directory"); + return false; + } + // 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 (final Directory dir in findPackages()) { + final String dirName = path.basename(dir.path); + for (final 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 '$kPlatformIntegrationPackageName/android.dart'; + yield '$kPlatformIntegrationPackageName/ios.dart'; +} + +void printStream(Stream> stream, { String prefix = '', List filter = const [] }) { + stream + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + if (!filter.any((Pattern pattern) => line.contains(pattern))) { + print('$prefix$line'.trim()); + } + }); +} + +Future runPubProcess({ + required String dartBinaryPath, + required List arguments, + String? workingDirectory, + Map? environment, + @visibleForTesting + ProcessManager processManager = const LocalProcessManager(), +}) { + return processManager.start( + [dartBinaryPath, 'pub', ...arguments], + workingDirectory: workingDirectory, + environment: environment, + ); +} diff --git a/dev/tools/java_and_objc_doc.dart b/dev/tools/java_and_objc_doc.dart new file mode 100644 index 0000000000..260cdc7b60 --- /dev/null +++ b/dev/tools/java_and_objc_doc.dart @@ -0,0 +1,97 @@ +// Copyright 2014 The Flutter 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:io'; +import 'dart:math'; + +import 'package:archive/archive.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as path; + +const String kDocRoot = 'dev/docs/doc'; + +/// This script downloads an archive of Javadoc and objc doc for the engine from +/// the artifact store and extracts them to the location used for Dartdoc. +Future main(List args) async { + final String engineVersion = File('bin/internal/engine.version').readAsStringSync().trim(); + String engineRealm = File('bin/internal/engine.realm').readAsStringSync().trim(); + if (engineRealm.isNotEmpty) { + engineRealm = '$engineRealm/'; + } + + final String javadocUrl = 'https://storage.googleapis.com/${engineRealm}flutter_infra_release/flutter/$engineVersion/android-javadoc.zip'; + generateDocs(javadocUrl, 'javadoc', 'io/flutter/view/FlutterView.html'); + + final String objcdocUrl = 'https://storage.googleapis.com/${engineRealm}flutter_infra_release/flutter/$engineVersion/ios-objcdoc.zip'; + generateDocs(objcdocUrl, 'objcdoc', 'Classes/FlutterViewController.html'); +} + +/// Fetches the zip archive at the specified url. +/// +/// Returns null if the archive fails to download after [maxTries] attempts. +Future fetchArchive(String url, int maxTries) async { + List? responseBytes; + for (int i = 0; i < maxTries; i++) { + final http.Response response = await http.get(Uri.parse(url)); + if (response.statusCode == 200) { + responseBytes = response.bodyBytes; + break; + } + stderr.writeln('Failed attempt ${i+1} to fetch $url.'); + + // On failure print a short snipped from the body in case it's helpful. + final int bodyLength = min(1024, response.body.length); + stderr.writeln('Response status code ${response.statusCode}. Body: ${response.body.substring(0, bodyLength)}'); + sleep(const Duration(seconds: 1)); + } + return responseBytes == null ? null : ZipDecoder().decodeBytes(responseBytes); +} + +Future generateDocs(String url, String docName, String checkFile) async { + const int maxTries = 5; + final Archive? archive = await fetchArchive(url, maxTries); + if (archive == null) { + stderr.writeln('Failed to fetch zip archive from: $url after $maxTries attempts. Giving up.'); + exit(1); + } + + final Directory output = Directory('$kDocRoot/$docName'); + print('Extracting $docName to ${output.path}'); + output.createSync(recursive: true); + + for (final ArchiveFile af in archive) { + if (!af.name.endsWith('/')) { + final File file = File('${output.path}/${af.name}'); + file.createSync(recursive: true); + file.writeAsBytesSync(af.content as List); + } + } + + /// If object then copy files to old location if the archive is using the new location. + final bool exists = Directory('$kDocRoot/$docName/objectc_docs').existsSync(); + if (exists) { + copyFolder(Directory('$kDocRoot/$docName/objectc_docs'), Directory('$kDocRoot/$docName/')); + } + + final File testFile = File('${output.path}/$checkFile'); + if (!testFile.existsSync()) { + print('Expected file ${testFile.path} not found'); + exit(1); + } + print('$docName ready to go!'); +} + +/// Copies the files in a directory recursively to a new location. +void copyFolder(Directory source, Directory destination) { + source.listSync() + .forEach((FileSystemEntity entity) { + if (entity is Directory) { + final Directory newDirectory = Directory(path.join(destination.absolute.path, path.basename(entity.path))); + newDirectory.createSync(); + copyFolder(entity.absolute, newDirectory); + } else if (entity is File) { + entity.copySync(path.join(destination.path, path.basename(entity.path))); + } + }); +} diff --git a/dev/tools/pubspec.yaml b/dev/tools/pubspec.yaml index fdf94b40dd..82ad7215a6 100644 --- a/dev/tools/pubspec.yaml +++ b/dev/tools/pubspec.yaml @@ -12,7 +12,6 @@ dependencies: meta: 1.9.1 path: 1.8.3 process: 4.2.4 - pub_semver: 2.1.4 async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -46,6 +45,7 @@ dev_dependencies: node_preamble: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" package_config: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pool: 1.5.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + pub_semver: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" shelf: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" shelf_packages_handler: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" shelf_static: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" diff --git a/dev/tools/test/create_api_docs_test.dart b/dev/tools/test/create_api_docs_test.dart deleted file mode 100644 index a12edbbfdf..0000000000 --- a/dev/tools/test/create_api_docs_test.dart +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2014 The Flutter 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 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:test/test.dart'; - -import '../../../packages/flutter_tools/test/src/fake_process_manager.dart'; -import '../create_api_docs.dart' as apidocs; - -void main() { - test('getBranchName does not call git if env LUCI_BRANCH provided', () { - final Platform platform = FakePlatform( - environment: { - 'LUCI_BRANCH': branchName, - }, - ); - - final ProcessManager processManager = FakeProcessManager.list( - [ - const FakeCommand( - command: ['flutter', '--version', '--machine'], - stdout: testVersionInfo, - ), - ], - ); - - expect( - apidocs.FlutterInformation(platform: platform, processManager: processManager).getBranchName(), - branchName, - ); - expect(processManager, hasNoRemainingExpectations); - }); - - test('getBranchName calls git if env LUCI_BRANCH not provided', () { - final Platform platform = FakePlatform( - environment: {}, - ); - - final ProcessManager processManager = FakeProcessManager.list( - [ - const FakeCommand( - command: ['flutter', '--version', '--machine'], - stdout: testVersionInfo, - ), - const FakeCommand( - command: ['git', 'status', '-b', '--porcelain'], - stdout: '## $branchName', - ), - ], - ); - - expect( - apidocs.FlutterInformation(platform: platform, processManager: processManager).getBranchName(), - branchName, - ); - expect(processManager, hasNoRemainingExpectations); - }); - - test('getBranchName calls git if env LUCI_BRANCH is empty', () { - final Platform platform = FakePlatform( - environment: { - 'LUCI_BRANCH': '', - }, - ); - - final ProcessManager processManager = FakeProcessManager.list( - [ - const FakeCommand( - command: ['flutter', '--version', '--machine'], - stdout: testVersionInfo, - ), - const FakeCommand( - command: ['git', 'status', '-b', '--porcelain'], - stdout: '## $branchName', - ), - ], - ); - - expect( - apidocs.FlutterInformation(platform: platform, processManager: processManager).getBranchName(), - branchName, - ); - expect(processManager, hasNoRemainingExpectations); - }); - - test("runPubProcess doesn't use the pub binary", () { - final Platform platform = FakePlatform( - environment: { - 'FLUTTER_ROOT': '/flutter', - }, - ); - final ProcessManager processManager = FakeProcessManager.list( - [ - const FakeCommand( - command: ['/flutter/bin/dart', 'pub', '--one', '--two'], - ), - ], - ); - apidocs.FlutterInformation.instance = - apidocs.FlutterInformation(platform: platform, processManager: processManager); - - apidocs.runPubProcess( - arguments: ['--one', '--two'], - processManager: processManager, - ); - - expect(processManager, hasNoRemainingExpectations); - }); - - group('FlutterInformation', () { - late FakeProcessManager fakeProcessManager; - late FakePlatform fakePlatform; - late MemoryFileSystem memoryFileSystem; - late apidocs.FlutterInformation flutterInformation; - - void setUpWithEnvironment(Map environment) { - fakePlatform = FakePlatform(environment: environment); - flutterInformation = apidocs.FlutterInformation( - filesystem: memoryFileSystem, - processManager: fakeProcessManager, - platform: fakePlatform, - ); - apidocs.FlutterInformation.instance = flutterInformation; - } - - setUp(() { - fakeProcessManager = FakeProcessManager.empty(); - memoryFileSystem = MemoryFileSystem(); - setUpWithEnvironment({}); - }); - - test('calls out to flutter if FLUTTER_VERSION is not set', () async { - fakeProcessManager.addCommand( - const FakeCommand(command: ['flutter', '--version', '--machine'], stdout: testVersionInfo)); - fakeProcessManager.addCommand( - const FakeCommand(command: ['git', 'status', '-b', '--porcelain'], stdout: testVersionInfo)); - final Map info = flutterInformation.getFlutterInformation(); - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(info['frameworkVersion'], equals(Version.parse('2.5.0'))); - }); - test("doesn't call out to flutter if FLUTTER_VERSION is set", () async { - setUpWithEnvironment({ - 'FLUTTER_VERSION': testVersionInfo, - }); - fakeProcessManager.addCommand( - const FakeCommand(command: ['git', 'status', '-b', '--porcelain'], stdout: testVersionInfo)); - final Map info = flutterInformation.getFlutterInformation(); - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(info['frameworkVersion'], equals(Version.parse('2.5.0'))); - }); - test('getFlutterRoot calls out to flutter if FLUTTER_ROOT is not set', () async { - fakeProcessManager.addCommand( - const FakeCommand(command: ['flutter', '--version', '--machine'], stdout: testVersionInfo)); - fakeProcessManager.addCommand( - const FakeCommand(command: ['git', 'status', '-b', '--porcelain'], stdout: testVersionInfo)); - final Directory root = flutterInformation.getFlutterRoot(); - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(root.path, equals('/home/user/flutter')); - }); - test("getFlutterRoot doesn't call out to flutter if FLUTTER_ROOT is set", () async { - setUpWithEnvironment({'FLUTTER_ROOT': '/home/user/flutter'}); - final Directory root = flutterInformation.getFlutterRoot(); - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(root.path, equals('/home/user/flutter')); - }); - test('parses version properly', () async { - fakePlatform.environment['FLUTTER_VERSION'] = testVersionInfo; - fakeProcessManager.addCommand( - const FakeCommand(command: ['git', 'status', '-b', '--porcelain'], stdout: testVersionInfo)); - final Map info = flutterInformation.getFlutterInformation(); - expect(info['frameworkVersion'], isNotNull); - expect(info['frameworkVersion'], equals(Version.parse('2.5.0'))); - expect(info['dartSdkVersion'], isNotNull); - expect(info['dartSdkVersion'], equals(Version.parse('2.14.0-360.0.dev'))); - }); - }); -} - -const String branchName = 'stable'; -const String testVersionInfo = ''' -{ - "frameworkVersion": "2.5.0", - "channel": "$branchName", - "repositoryUrl": "git@github.com:flutter/flutter.git", - "frameworkRevision": "0000000000000000000000000000000000000000", - "frameworkCommitDate": "2021-07-28 13:03:40 -0700", - "engineRevision": "0000000000000000000000000000000000000001", - "dartSdkVersion": "2.14.0 (build 2.14.0-360.0.dev)", - "flutterRoot": "/home/user/flutter" -} -'''; diff --git a/dev/tools/test/dartdoc_test.dart b/dev/tools/test/dartdoc_test.dart new file mode 100644 index 0000000000..5744650ee3 --- /dev/null +++ b/dev/tools/test/dartdoc_test.dart @@ -0,0 +1,98 @@ +// Copyright 2014 The Flutter 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 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import '../../../packages/flutter_tools/test/src/fake_process_manager.dart'; +import '../dartdoc.dart' show getBranchName, runPubProcess; + +void main() { + const String branchName = 'stable'; + test('getBranchName does not call git if env LUCI_BRANCH provided', () { + final Platform platform = FakePlatform( + environment: { + 'LUCI_BRANCH': branchName, + }, + ); + + final ProcessManager processManager = FakeProcessManager.empty(); + + expect( + getBranchName( + platform: platform, + processManager: processManager, + ), + branchName, + ); + }); + + test('getBranchName calls git if env LUCI_BRANCH not provided', () { + final Platform platform = FakePlatform( + environment: {}, + ); + + final ProcessManager processManager = FakeProcessManager.list( + [ + const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + ), + ], + ); + + expect( + getBranchName( + platform: platform, + processManager: processManager, + ), + branchName, + ); + expect(processManager, hasNoRemainingExpectations); + }); + + test('getBranchName calls git if env LUCI_BRANCH is empty', () { + final Platform platform = FakePlatform( + environment: { + 'LUCI_BRANCH': '', + }, + ); + + final ProcessManager processManager = FakeProcessManager.list( + [ + const FakeCommand( + command: ['git', 'status', '-b', '--porcelain'], + stdout: '## $branchName', + ), + ], + ); + + expect( + getBranchName( + platform: platform, + processManager: processManager, + ), + branchName, + ); + expect(processManager, hasNoRemainingExpectations); + }); + + test("runPubProcess doesn't use the pub binary", () { + final ProcessManager processManager = FakeProcessManager.list( + [ + const FakeCommand( + command: ['dart', 'pub', '--one', '--two'], + ), + ], + ); + + runPubProcess( + dartBinaryPath: 'dart', + arguments: ['--one', '--two'], + processManager: processManager, + ); + + expect(processManager, hasNoRemainingExpectations); + }); +}