Unverified Commit ced3e766 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Reland: "Reorganize and clarify API doc generator" (#132353) (#132710)

## Description

This re-lands #132353 with some additional options for keeping around the staging directory, so that the recipe for publishing docs can give those options and have the staging directory left around for deploying to the website.

Reverted in #132613

## Related Issues
 - https://flutter-review.googlesource.com/c/recipes/+/49580
parent d19fb632
......@@ -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,115 @@ 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
function usage() {
echo "Usage: $(basename "${BASH_SOURCE[0]}") [--keep-temp] [--output <output.zip>]"
echo ""
echo " --keep-staging Do not delete the staging directory created while generating"
echo " docs. Normally the script deletes the staging directory after"
echo " generating the output ZIP file."
echo " --output <output.zip> specifies where the output ZIP file containing the documentation"
echo " data will be written."
echo " --staging-dir <directory> specifies where the temporary output files will be written while"
echo " generating docs. This directory will be deleted after generation"
echo " unless --keep-staging is also specified."
echo ""
}
function parse_args() {
local arg
local args=()
STAGING_DIR=
KEEP_STAGING=0
DESTINATION="$FLUTTER_ROOT/dev/docs/api_docs.zip"
while (( "$#" )); do
case "$1" in
--help)
usage
exit 0
;;
--staging-dir)
STAGING_DIR="$2"
shift
;;
--keep-staging)
KEEP_STAGING=1
;;
--output)
DESTINATION="$2"
shift
;;
*)
args=("${args[@]}" "$1")
;;
esac
shift
done
if [[ -z $STAGING_DIR ]]; then
STAGING_DIR=$(mktemp -d /tmp/dartdoc.XXXXX)
fi
DOC_DIR="$STAGING_DIR/doc"
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 "$STAGING_DIR"; zip -r -9 -q "$DESTINATION" ./doc)
if [[ $KEEP_STAGING -eq 1 ]]; then
echo "Staging documentation output left in $STAGING_DIR"
else
echo "Removing staging documentation output from $STAGING_DIR"
rm -rf "$STAGING_DIR"
fi
echo "Wrote docs ZIP file to $DESTINATION"
}
# Zip docs
cd "$FLUTTER_ROOT/dev/docs"
zip -r api_docs.zip doc
parse_args "$@"
main
// 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);
}
// 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';
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<void> main(List<String> arguments) async {
const FileSystem filesystem = LocalFileSystem();
const ProcessManager processManager = LocalProcessManager();
const Platform platform = LocalPlatform();
// 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).absolute;
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,
filesystem: filesystem,
processManager: processManager,
platform: platform,
);
configurator.generateConfiguration();
final PlatformDocGenerator platformGenerator = PlatformDocGenerator(outputDir: publishRoot, filesystem: filesystem);
platformGenerator.generatePlatformDocs();
final DartdocGenerator dartdocGenerator = DartdocGenerator(
publishRoot: publishRoot,
packageRoot: packageRoot,
docsRoot: docsRoot,
filesystem: filesystem,
processManager: processManager,
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,
required this.filesystem,
required this.processManager,
required this.platform,
});
/// 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;
/// The [ProcessManager] object used to invoke external processes.
///
/// Can be replaced by tests to have a fake process manager.
final ProcessManager processManager;
/// The [Platform] to use for this run.
///
/// Can be replaced by tests to test behavior on different plaforms.
final Platform platform;
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<void> 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<String> _libraryRefs() sync* {
for (final Directory dir in findPackages(filesystem)) {
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<String> pubspec = <String>[
'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(filesystem)) ' $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('<script src="footer.js"></script>');
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<String> files = <String>[
'README.md',
'analysis_options.yaml',
'dartdoc_options.yaml',
];
for (final String file in files) {
final File source = docsRoot.childFile(file);
final File destination = packageRoot.childFile(file);
// Have to canonicalize because otherwise things like /foo/bar/baz and
// /foo/../foo/bar/baz won't compare as identical.
if (path.canonicalize(source.absolute.path) != path.canonicalize(destination.absolute.path)) {
source.copySync(destination.path);
print('Copied ${path.canonicalize(source.absolute.path)} to ${path.canonicalize(destination.absolute.path)}');
}
}
final Directory assetsDir = filesystem.directory(publishRoot.childDirectory('assets'));
final Directory assetSource = docsRoot.childDirectory('assets');
if (path.canonicalize(assetSource.absolute.path) == path.canonicalize(assetsDir.absolute.path)) {
// Don't try and copy the directory over itself.
return;
}
if (assetsDir.existsSync()) {
assetsDir.deleteSync(recursive: true);
}
copyDirectorySync(
docsRoot.childDirectory('assets'),
assetsDir,
onFileCopied: (File src, File dest) {
print('Copied ${path.canonicalize(src.absolute.path)} to ${path.canonicalize(dest.absolute.path)}');
},
filesystem: filesystem,
);
}
/// 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<void> _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(
<String>[
'dashing',
'build',
'--source',
publishRoot.path,
'--config',
docsRoot.childFile('dashing.json').path,
],
workingDirectory: packageRoot.path,
),
);
final List<int> buffer = <int>[];
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<String> 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*<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);
final Directory offlineDir = publishRoot.childDirectory('offline');
if (!offlineDir.existsSync()) {
offlineDir.createSync(recursive: true);
}
tarDirectory(packageRoot, offlineDir.childFile('flutter.docset.tar.gz'), processManager: processManager);
// Write the Dash/Zeal XML feed file.
final bool isStable = platform.environment['LUCI_BRANCH'] == 'stable';
offlineDir.childFile('flutter.xml').writeAsStringSync('<entry>\n'
' <version>${FlutterInformation.instance.getFlutterVersion()}</version>\n'
' <url>https://${isStable ? '' : 'master-'}api.flutter.dev/offline/flutter.docset.tar.gz</url>\n'
'</entry>\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'), processManager: processManager);
}
// 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,
required this.filesystem,
required this.processManager,
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;
/// The [ProcessManager] object used to invoke external processes.
///
/// Can be replaced by tests to have a fake process manager.
final ProcessManager processManager;
/// 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<void> generateDartdoc() async {
final Directory flutterRoot = FlutterInformation.instance.getFlutterRoot();
final Map<String, String> pubEnvironment = <String, String>{
'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: <String>['get'],
workingDirectory: packageRoot,
environment: pubEnvironment,
filesystem: filesystem,
processManager: processManager,
));
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,
<String>[
'pub',
'global',
'list',
],
workingDirectory: packageRoot.path,
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(filesystem),
// 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<String> dartdocArgs = <String>[
'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',
<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',
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<String>(quote).join(' ')})');
process = ProcessWrapper(await runPubProcess(
arguments: dartdocArgs,
workingDirectory: packageRoot,
environment: pubEnvironment,
));
printStream(
process.stdout,
prefix: useJson ? '' : 'dartdoc:stdout: ',
filter: <Pattern>[
if (!verbose) RegExp(r'^Generating docs for library '), // Unnecessary verbosity
],
);
printStream(
process.stderr,
prefix: useJson ? '' : 'dartdoc:stderr: ',
filter: <Pattern>[
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<File> canaries = <File>[
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*<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(
widgetsDirectory.childDirectory('ModalRoute').childFile('barrierColor.html').path,
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(
widgetsDirectory.childFile('Listener-class.html').path,
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 ${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(
'<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 = publishRoot.childFile('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">';
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<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);
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, required this.filesystem});
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<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 = 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<void> _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<int>);
}
}
/// 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, filesystem: filesystem);
}
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, required FileSystem filesystem}) {
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), filesystem: filesystem);
} else {
throw Exception('${entity.path} is neither File nor Directory');
}
}
}
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());
}
});
}
void zipDirectory(Directory src, File output, {required ProcessManager processManager}) {
// 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(
<String>[
'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, {required ProcessManager processManager}) {
// 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(
<String>[
'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<Process> runPubProcess({
required List<String> arguments,
Directory? workingDirectory,
Map<String, String>? environment,
@visibleForTesting ProcessManager processManager = const LocalProcessManager(),
@visibleForTesting FileSystem filesystem = const LocalFileSystem(),
}) {
return processManager.start(
<Object>[FlutterInformation.instance.getDartBinaryPath().path, 'pub', ...arguments],
workingDirectory: (workingDirectory ?? filesystem.currentDirectory).path,
environment: environment,
);
}
List<String> findPackageNames(FileSystem filesystem) {
return findPackages(filesystem).map<String>((FileSystemEntity file) => path.basename(file.path)).toList();
}
/// Finds all packages in the Flutter SDK
List<Directory> findPackages(FileSystem filesystem) {
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<Directory>()
.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<String, Object>? _cachedFlutterInformation;
/// Gets a Map of various kinds of information about the Flutter repo.
Map<String, Object> 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(<String>[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<String, dynamic> flutterVersion = json.decode(flutterVersionJson) as Map<String, dynamic>;
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<String, Object> info = <String, Object>{};
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'(?<base>[\d.]+)(?:\s+\(build (?<detail>[-.\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(<String>['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', <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;
}
}
// 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,
);
}
// 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)));
}
});
}
......@@ -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"
......
// 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;
import '../examples_smoke_test.dart';
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,
filesystem: filesystem,
);
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"
}
''';
// 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);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment