Reorganize and clarify API doc generator (#132353)

## Description

This cleans up a lot of issues with the API doc generation.

Here are the main changes:
 - Rename `dartdoc.dart` to `create_api_docs.dart`
 - Move the bulk of the operations out of `dev/bots/docs.sh` into `create_api_docs.dart`.
 - Delete `dashing_postprocess.dart` and `java_and_objc.dart` and incorporate those operations into `create_api_docs.dart`.
 - Refactor the doc generation into more understandable classes
 - Bump the snippets tool version to 0.4.0 (the latest one)
 - Centralize the information gathering about the Flutter repo into the new `FlutterInformation` class.
 - Clean up the directory handling, and convert to using the `file` package for all file and directory paths.
 - Add an `--output` option to docs.sh that specifies the location of the output ZIP file containing the docs.
   - Defaults to placing the output in `dev/docs/api_docs.zip` (i.e. where the previous code generates the file).
 - Moved all document generation into a temporary folder that is removed once the documents are generated, to avoid VSCode and other IDEs trying to index the thousands of HTML and JS files in the docs output.
 - Updated pubspec dependencies.

## Tests
 - Added tests for doc generation.
This commit is contained in:
Greg Spencer 2023-08-15 15:12:16 -07:00 committed by GitHub
parent 301577a34f
commit 899a29f830
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1384 additions and 938 deletions

View File

@ -16,102 +16,13 @@ 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 "<entry>\n <version>${FLUTTER_VERSION_STRING}</version>\n <url>https://api.flutter.dev/offline/flutter.docset.tar.gz</url>\n</entry>" > doc/offline/flutter.xml
else
echo -e "<entry>\n <version>${FLUTTER_VERSION_STRING}</version>\n <url>https://master-api.flutter.dev/offline/flutter.docset.tar.gz</url>\n</entry>" > 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"
@ -124,31 +35,106 @@ FLUTTER_BIN="$FLUTTER_ROOT/bin"
DART_BIN="$FLUTTER_ROOT/bin/cache/dart-sdk/bin"
FLUTTER="$FLUTTER_BIN/flutter"
DART="$DART_BIN/dart"
export PATH="$FLUTTER_BIN:$DART_BIN:$PATH"
PATH="$FLUTTER_BIN:$DART_BIN:$PATH"
# Make sure dart is installed by invoking Flutter to download it.
# This also creates the 'version' file.
FLUTTER_VERSION=$("$FLUTTER" --version --machine)
# 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)"
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 ".".
export PUB_CACHE="${PUB_CACHE:-"$FLUTTER_PUB_CACHE"}"
PUB_CACHE="${PUB_CACHE:-"$FLUTTER_PUB_CACHE"}"
export PUB_CACHE
fi
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
OUTPUT_DIR=$(mktemp -d /tmp/dartdoc.XXXXX)
DOC_DIR="$OUTPUT_DIR/doc"
# Zip docs
cd "$FLUTTER_ROOT/dev/docs"
zip -r api_docs.zip doc
function usage() {
echo "Usage: $(basename "${BASH_SOURCE[0]}") [--keep-temp] [--output <output.zip>]"
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 <output.zip> 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

View File

@ -1,29 +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: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<String> 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*<key>DocSetPlatformFamily</key>\s*<string>)[^<]+(</string>)', multiLine: true);
contents = contents.replaceAllMapped(findRe, (Match match) {
return '${match.group(1)}dartlang${match.group(2)}';
});
infoPlist.writeAsStringSync(contents);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,606 +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 '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<void> main(List<String> 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', <String>[]);
final File versionFile = File('version');
if (flutter.exitCode != 0 || !versionFile.existsSync()) {
throw Exception('Failed to determine Flutter version.');
}
final String version = versionFile.readAsStringSync();
// Create the pubspec.yaml file.
final StringBuffer buf = StringBuffer();
buf.writeln('name: $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<String, String> pubEnvironment = <String, String>{
'FLUTTER_ROOT': flutterRoot,
};
// If there's a .pub-cache dir in the flutter root, use that.
final String pubCachePath = '$flutterRoot/.pub-cache';
if (Directory(pubCachePath).existsSync()) {
pubEnvironment['PUB_CACHE'] = pubCachePath;
}
final String dartExecutable = '$flutterRoot/bin/cache/dart-sdk/bin/dart';
// Run pub.
ProcessWrapper process = ProcessWrapper(await runPubProcess(
dartBinaryPath: dartExecutable,
arguments: <String>['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<String> dartdocBaseArgs = <String>[
'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,
<String>[
'pub',
'global',
'list',
],
workingDirectory: kDocsRoot,
environment: pubEnvironment,
stdoutEncoding: utf8,
);
print('');
final Iterable<RegExpMatch> versionMatches = RegExp(r'^(?<name>snippets|dartdoc) (?<version>[^\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<String> flutterPackages = <String>[
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<String> dartdocArgs = <String>[
...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',
<String>[
'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',
<String>[
'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<String>(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 <Pattern>[] : <Pattern>[
RegExp(r'^Generating docs for library '), // unnecessary verbosity
],
);
printStream(process.stderr, prefix: args['json'] as bool ? '' : 'dartdoc:stderr: ',
filter: args['verbose'] as bool ? const <Pattern>[] : <Pattern>[
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(<String>['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', <String>['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('<script src="footer.js"></script>');
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<String> canaries = <String>[
'$kPublishRoot/assets/overrides.css',
'$kPublishRoot/api/dart-io/File-class.html',
'$kPublishRoot/api/dart-ui/Canvas-class.html',
'$kPublishRoot/api/dart-ui/Canvas/drawRect.html',
'$kPublishRoot/api/flutter_driver/FlutterDriver/FlutterDriver.connectedTo.html',
'$kPublishRoot/api/flutter_test/WidgetTester/pumpWidget.html',
'$kPublishRoot/api/material/Material-class.html',
'$kPublishRoot/api/material/Tooltip-class.html',
'$kPublishRoot/api/widgets/Widget-class.html',
'$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*<pre\s+id="longSnippet1".*<code\s+class="language-dart">\s*import &#39;package:flutter&#47;material.dart&#39;;',
);
// Check a "snippet" example, any one will do.
_sanityCheckExample(
'$kPublishRoot/api/widgets/ModalRoute/barrierColor.html',
r'\s*<pre.*id="sample-code">.*Color\s+get\s+barrierColor.*</pre>',
);
// 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<String> argumentRegExps = <String>[
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*<iframe\s+class="snippet-dartpad"\s+src="'
r'https:\/\/dartpad.dev\/embed-flutter.html\?.*?\b'
'$argumentRegExp'
r'\b.*">\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(
'<li><a href="https://flutter.dev">Flutter package</a></li>',
'<li><a href="https://flutter.dev">Flutter SDK</a></li>',
);
indexFile.writeAsStringSync(indexContents);
}
void addHtmlBaseToIndex() {
final File indexFile = File('$kPublishRoot/index.html');
String indexContents = indexFile.readAsStringSync();
indexContents = indexContents.replaceFirst(
'</title>\n',
'</title>\n <base href="./flutter/">\n',
);
indexContents = indexContents.replaceAll(
'href="Android/Android-library.html"',
'href="/javadoc/"',
);
indexContents = indexContents.replaceAll(
'href="iOS/iOS-library.html"',
'href="/objcdoc/"',
);
indexFile.writeAsStringSync(indexContents);
}
void putRedirectInOldIndexLocation() {
const String metaTag = '<meta http-equiv="refresh" content="0;URL=../index.html">';
File('$kPublishRoot/flutter/index.html').writeAsStringSync(metaTag);
}
void writeSnippetsIndexFile() {
final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets'));
if (snippetsDir.existsSync()) {
const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' ');
final Iterable<File> files = snippetsDir
.listSync()
.whereType<File>()
.where((File file) => path.extension(file.path) == '.json');
// Combine all the metadata into a single JSON array.
final Iterable<String> fileContents = files.map((File file) => file.readAsStringSync());
final List<dynamic> metadataObjects = fileContents.map<dynamic>(json.decode).toList();
final String jsonArray = jsonEncoder.convert(metadataObjects);
File('$kPublishRoot/snippets/index.json').writeAsStringSync(jsonArray);
}
}
List<String> findPackageNames() {
return findPackages().map<String>((FileSystemEntity file) => path.basename(file.path)).toList();
}
/// Finds all packages in the Flutter SDK
List<Directory> 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<Directory>()
.toList();
}
/// Returns import or on-disk paths for all libraries in the Flutter SDK.
Iterable<String> 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<List<int>> stream, { String prefix = '', List<Pattern> filter = const <Pattern>[] }) {
stream
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
if (!filter.any((Pattern pattern) => line.contains(pattern))) {
print('$prefix$line'.trim());
}
});
}
Future<Process> runPubProcess({
required String dartBinaryPath,
required List<String> arguments,
String? workingDirectory,
Map<String, String>? environment,
@visibleForTesting
ProcessManager processManager = const LocalProcessManager(),
}) {
return processManager.start(
<Object>[dartBinaryPath, 'pub', ...arguments],
workingDirectory: workingDirectory,
environment: environment,
);
}

View File

@ -1,97 +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: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<void> main(List<String> 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<Archive?> fetchArchive(String url, int maxTries) async {
List<int>? 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<void> 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<int>);
}
}
/// 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)));
}
});
}

View File

@ -12,6 +12,7 @@ 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"
@ -45,7 +46,6 @@ 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"

View File

@ -0,0 +1,195 @@
// 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: <String, String>{
'LUCI_BRANCH': branchName,
},
);
final ProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <String>['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: <String, String>{},
);
final ProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <String>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
const FakeCommand(
command: <String>['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: <String, String>{
'LUCI_BRANCH': '',
},
);
final ProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <String>['flutter', '--version', '--machine'],
stdout: testVersionInfo,
),
const FakeCommand(
command: <String>['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: <String, String>{
'FLUTTER_ROOT': '/flutter',
},
);
final ProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <String>['/flutter/bin/dart', 'pub', '--one', '--two'],
),
],
);
apidocs.FlutterInformation.instance =
apidocs.FlutterInformation(platform: platform, processManager: processManager);
apidocs.runPubProcess(
arguments: <String>['--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<String, String> 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(<String, String>{});
});
test('calls out to flutter if FLUTTER_VERSION is not set', () async {
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['flutter', '--version', '--machine'], stdout: testVersionInfo));
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'status', '-b', '--porcelain'], stdout: testVersionInfo));
final Map<String, dynamic> 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(<String, String>{
'FLUTTER_VERSION': testVersionInfo,
});
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['git', 'status', '-b', '--porcelain'], stdout: testVersionInfo));
final Map<String, dynamic> 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: <Pattern>['flutter', '--version', '--machine'], stdout: testVersionInfo));
fakeProcessManager.addCommand(
const FakeCommand(command: <Pattern>['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(<String, String>{'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: <Pattern>['git', 'status', '-b', '--porcelain'], stdout: testVersionInfo));
final Map<String, dynamic> 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"
}
''';

View File

@ -1,98 +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: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: <String, String>{
'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: <String, String>{},
);
final ProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <String>['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: <String, String>{
'LUCI_BRANCH': '',
},
);
final ProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <String>['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(
<FakeCommand>[
const FakeCommand(
command: <String>['dart', 'pub', '--one', '--two'],
),
],
);
runPubProcess(
dartBinaryPath: 'dart',
arguments: <String>['--one', '--two'],
processManager: processManager,
);
expect(processManager, hasNoRemainingExpectations);
});
}