| // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file |
| // for details. 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:analyzer/file_system/physical_file_system.dart'; |
| import 'package:args/args.dart'; |
| import 'package:collection/collection.dart'; |
| import 'package:crypto/crypto.dart' as crypto; |
| import 'package:dartdoc/src/package_meta.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:sass/sass.dart' as sass; |
| import 'package:yaml/yaml.dart' as yaml; |
| import 'package:yaml/yaml.dart'; |
| |
| import 'src/flutter_repo.dart'; |
| import 'src/io_utils.dart' as io_utils; |
| import 'src/subprocess_launcher.dart'; |
| import 'src/warnings_collection.dart'; |
| |
| void main(List<String> args) async { |
| var parser = ArgParser() |
| ..addCommand('analyze') |
| ..addCommand('buildbot') |
| ..addCommand('clean') |
| ..addCommand('compare') |
| ..addCommand('help') |
| ..addCommand('test') |
| ..addCommand('try-publish') |
| ..addCommand('validate'); |
| var buildCommand = parser.addCommand('build') |
| ..addFlag('debug', help: 'build unoptimized JavaScript'); |
| var docCommand = parser.addCommand('doc') |
| ..addOption('name', help: 'package name') |
| ..addOption('version', help: 'package version') |
| ..addFlag('stats', help: 'print runtime stats'); |
| var serveCommand = parser.addCommand('serve') |
| ..addOption('name') |
| ..addOption('version'); |
| |
| var results = parser.parse(args); |
| var commandResults = results.command; |
| if (commandResults == null) { |
| return; |
| } |
| |
| buildUsage = buildCommand.usage; |
| docUsage = docCommand.usage; |
| serveUsage = serveCommand.usage; |
| |
| return await switch (commandResults.name) { |
| 'analyze' => runAnalyze(commandResults), |
| 'build' => runBuild(commandResults), |
| 'buildbot' => runBuildbot(), |
| 'clean' => runClean(), |
| 'compare' => runCompare(commandResults), |
| 'doc' => runDoc(commandResults), |
| 'help' => runHelp(commandResults), |
| 'serve' => runServe(commandResults), |
| 'test' => runTest(), |
| 'try-publish' => runTryPublish(), |
| 'validate' => runValidate(commandResults), |
| _ => throw ArgumentError(), |
| }; |
| } |
| |
| late String buildUsage; |
| late String docUsage; |
| late String serveUsage; |
| |
| String _getPackageVersion() { |
| var pubspec = File('pubspec.yaml'); |
| if (!pubspec.existsSync()) { |
| throw StateError('Cannot find pubspec.yaml in ${Directory.current}'); |
| } |
| var yamlDoc = yaml.loadYaml(pubspec.readAsStringSync()) as yaml.YamlMap; |
| return yamlDoc['version'] as String; |
| } |
| |
| Directory get testPackage => |
| Directory(path.joinAll(['testing', 'test_package'])); |
| |
| Directory get testPackageExperiments => |
| Directory(path.joinAll(['testing', 'test_package_experiments'])); |
| |
| Future<void> runAnalyze(ArgResults commandResults) async { |
| for (var target in commandResults.rest) { |
| await switch (target) { |
| 'package' => analyzePackage(), |
| 'test-packages' => analyzeTestPackages(), |
| _ => throw UnimplementedError('Unknown analyze target: "$target"'), |
| }; |
| } |
| } |
| |
| Future<void> analyzePackage() async => |
| await SubprocessLauncher('analyze').runStreamedDartCommand( |
| ['analyze', '--fatal-infos', '.'], |
| ); |
| |
| Future<void> analyzeTestPackages() async { |
| var testPackagePaths = [ |
| testPackage.path, |
| if (Platform.version.contains('dev')) testPackageExperiments.path, |
| ]; |
| for (var testPackagePath in testPackagePaths) { |
| await SubprocessLauncher('pub-get').runStreamedDartCommand( |
| ['pub', 'get'], |
| workingDirectory: testPackagePath, |
| ); |
| await SubprocessLauncher('analyze-test-package').runStreamedDartCommand( |
| // TODO(srawlins): Analyze the whole directory by ignoring the pubspec |
| // reports. |
| ['analyze', 'lib'], |
| workingDirectory: testPackagePath, |
| ); |
| } |
| } |
| |
| Future<void> _buildHelp() async { |
| print(''' |
| Usage: |
| dart tool/task.dart build [renderers|dartdoc-options|web] |
| $buildUsage |
| '''); |
| } |
| |
| Future<void> runBuild(ArgResults commandResults) async { |
| if (commandResults.rest.isEmpty) { |
| await buildAll(); |
| } |
| var debug = (commandResults['debug'] ?? false) as bool; |
| for (var target in commandResults.rest) { |
| await switch (target) { |
| 'renderers' => buildRenderers(), |
| 'dartdoc-options' => buildDartdocOptions(), |
| 'web' => buildWeb(debug: debug), |
| _ => throw UnimplementedError('Unknown build target: "$target"'), |
| }; |
| } |
| } |
| |
| Future<void> buildAll() async { |
| if (Platform.isWindows) { |
| // Built files only need to be built on Linux and MacOS, as there are path |
| // issues with Windows. |
| return; |
| } |
| await buildWeb(); |
| await buildRenderers(); |
| await buildDartdocOptions(); |
| } |
| |
| Future<void> buildRenderers() async => await SubprocessLauncher('build') |
| .runStreamedDartCommand([path.join('tool', 'mustachio', 'builder.dart')]); |
| |
| Future<void> buildDartdocOptions() async { |
| var version = _getPackageVersion(); |
| var dartdocOptions = File('dartdoc_options.yaml'); |
| await dartdocOptions.writeAsString('''dartdoc: |
| linkToSource: |
| root: '.' |
| uriTemplate: 'https://github.com/dart-lang/dartdoc/blob/v$version/%f%#L%l%' |
| '''); |
| } |
| |
| Future<void> buildWeb({bool debug = false}) async { |
| await SubprocessLauncher('build').runStreamedDartCommand([ |
| 'compile', |
| 'js', |
| '--output=lib/resources/docs.dart.js', |
| 'web/docs.dart', |
| if (debug) '--enable-asserts', |
| debug ? '-O0' : '-O4', |
| ]); |
| _delete(File('lib/resources/docs.dart.js.deps')); |
| |
| final compileResult = sass.compileToResult('web/styles/styles.scss', |
| style: debug ? sass.OutputStyle.expanded : sass.OutputStyle.compressed); |
| if (compileResult.css.isNotEmpty) { |
| File('lib/resources/styles.css') |
| .writeAsStringSync('${compileResult.css}\n'); |
| } else { |
| throw StateError('Compiled CSS was empty.'); |
| } |
| |
| var compileSig = |
| await _calcFilesSig(Directory('web'), extensions: {'.dart', '.scss'}); |
| File(path.join('web', 'sig.txt')).writeAsStringSync('$compileSig\n'); |
| } |
| |
| /// Delete the given file entity reference. |
| void _delete(FileSystemEntity entity) { |
| if (entity.existsSync()) { |
| print('deleting ${entity.path}'); |
| entity.deleteSync(recursive: true); |
| } |
| } |
| |
| /// Yields all of the trimmed lines of all of the files in [dir] with |
| /// one of the specified [extensions]. |
| Stream<String> _fileLines(Directory dir, {required Set<String> extensions}) { |
| var files = dir |
| .listSync(recursive: true) |
| .whereType<File>() |
| .where((file) => extensions.contains(path.extension(file.path))) |
| .sorted((a, b) => compareAsciiLowerCase(a.path, b.path)); |
| |
| return Stream.fromIterable([ |
| for (var file in files) |
| for (var line in file.readAsLinesSync()) line.trim(), |
| ]); |
| } |
| |
| Future<void> runBuildbot() async { |
| await analyzeTestPackages(); |
| await analyzePackage(); |
| await validateFormat(); |
| await validateBuild(); |
| await runTryPublish(); |
| await runTest(); |
| await validateDartdocDocs(); |
| } |
| |
| Future<void> runClean() async { |
| // This involves deleting things, so be careful. |
| if (!File(path.join('tool', 'task.dart')).existsSync()) { |
| throw FileSystemException('Wrong CWD, run from root of dartdoc package'); |
| } |
| const pubDataFileNames = {'.dart_tool', '.packages', 'pubspec.lock'}; |
| var nonRootPubData = Directory('.') |
| .listSync(recursive: true) |
| .where((e) => path.dirname(e.path) != '.') |
| .where((e) => pubDataFileNames.contains(path.basename(e.path))); |
| for (var e in nonRootPubData) { |
| e.deleteSync(recursive: true); |
| } |
| } |
| |
| Future<void> runCompare(ArgResults commandResults) async { |
| if (commandResults.rest.length != 1) { |
| throw ArgumentError('"compare" command requires a single target.'); |
| } |
| var target = commandResults.rest.single; |
| await switch (target) { |
| 'flutter-warnings' => compareFlutterWarnings(), |
| 'sdk-warnings' => compareSdkWarnings(), |
| _ => throw UnimplementedError('Unknown compare target: "$target"'), |
| }; |
| } |
| |
| Future<void> compareFlutterWarnings() async { |
| var originalDartdocFlutter = |
| Directory.systemTemp.createTempSync('dartdoc-comparison-flutter'); |
| var originalDartdoc = await _createComparisonDartdoc(); |
| var envCurrent = createThrowawayPubCache(); |
| var envOriginal = createThrowawayPubCache(); |
| var currentDartdocFlutterBuild = _docFlutter( |
| flutterPath: flutterDir.path, |
| cwd: Directory.current.path, |
| env: envCurrent, |
| label: 'docs-current', |
| ); |
| var originalDartdocFlutterBuild = _docFlutter( |
| flutterPath: originalDartdocFlutter.path, |
| cwd: originalDartdoc, |
| env: envOriginal, |
| label: 'docs-original', |
| ); |
| var currentDartdocWarnings = _collectWarnings( |
| messages: await currentDartdocFlutterBuild, |
| tempPath: flutterDir.absolute.path, |
| branch: 'HEAD', |
| pubDir: envCurrent['PUB_CACHE'], |
| ); |
| var originalDartdocWarnings = _collectWarnings( |
| messages: await originalDartdocFlutterBuild, |
| tempPath: originalDartdocFlutter.absolute.path, |
| branch: _dartdocOriginalBranch, |
| pubDir: envOriginal['PUB_CACHE'], |
| ); |
| |
| print(originalDartdocWarnings.warningDeltaText( |
| 'Flutter repo', currentDartdocWarnings)); |
| |
| if (Platform.environment['SERVE_FLUTTER'] == '1') { |
| var launcher = SubprocessLauncher('serve-flutter-docs'); |
| await launcher.runStreamed(Platform.resolvedExecutable, ['pub', 'get']); |
| var original = launcher.runStreamed(Platform.resolvedExecutable, [ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '9000', |
| '--path', |
| path.join(originalDartdocFlutter.absolute.path, 'dev', 'docs', 'doc'), |
| ]); |
| var current = launcher.runStreamed(Platform.resolvedExecutable, [ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '9001', |
| '--path', |
| path.join(flutterDir.absolute.path, 'dev', 'docs', 'doc'), |
| ]); |
| await [original, current].wait; |
| } |
| } |
| |
| Future<void> runDoc(ArgResults commandResults) async { |
| if (commandResults.rest.length != 1) { |
| throw ArgumentError('"doc" command requires a single target.'); |
| } |
| var target = commandResults.rest.single; |
| var stats = commandResults['stats'] as bool; |
| await switch (target) { |
| 'flutter' => docFlutter(withStats: stats), |
| 'help' => _docHelp(), |
| 'package' => _docPackage(commandResults, withStats: stats), |
| 'sdk' => docSdk(withStats: stats), |
| 'testing-package' => docTestingPackage(withStats: stats), |
| _ => throw UnimplementedError('Unknown doc target: "$target"'), |
| }; |
| } |
| |
| Future<void> docFlutter({bool withStats = false}) async { |
| print('building flutter docs into: ${flutterDir.path}'); |
| var env = createThrowawayPubCache(); |
| await _docFlutter( |
| flutterPath: flutterDir.path, |
| cwd: Directory.current.path, |
| env: env, |
| label: 'docs', |
| withStats: withStats, |
| ); |
| } |
| |
| Future<Iterable<Map<String, Object?>>> _docFlutter({ |
| required String flutterPath, |
| required String cwd, |
| required Map<String, String> env, |
| String? label, |
| bool withStats = false, |
| }) async { |
| var flutterRepo = await FlutterRepo.copyFromExistingFlutterRepo( |
| await cleanFlutterRepo, flutterPath, env, label); |
| var snippetsPath = path.join(flutterPath, 'dev', 'snippets'); |
| var snippetsOutPath = |
| path.join(flutterPath, 'bin', 'cache', 'artifacts', 'snippets'); |
| print('building snippets tool executable...'); |
| Directory(snippetsOutPath).createSync(recursive: true); |
| await flutterRepo.launcher.runStreamed( |
| flutterRepo.flutterCmd, |
| ['pub', 'get'], |
| workingDirectory: path.join(snippetsPath), |
| ); |
| await flutterRepo.launcher.runStreamed( |
| flutterRepo.dartCmd, |
| ['compile', 'exe', '-o', '$snippetsOutPath/snippets', 'bin/snippets.dart'], |
| workingDirectory: path.join(snippetsPath), |
| ); |
| // TODO(jcollins-g): flutter's dart SDK pub tries to precompile the universe |
| // when using -spath. Why? |
| await flutterRepo.launcher.runStreamed( |
| flutterRepo.flutterCmd, |
| ['pub', 'global', 'activate', '-spath', '.', '-x', 'dartdoc'], |
| workingDirectory: cwd, |
| ); |
| await flutterRepo.launcher.runStreamed( |
| flutterRepo.flutterCmd, |
| ['pub', 'get'], |
| workingDirectory: path.join(flutterPath, 'dev', 'tools'), |
| ); |
| return await flutterRepo.launcher.runStreamed( |
| flutterRepo.dartCmd, |
| [ |
| '--disable-dart-dev', |
| '--enable-asserts', |
| path.join( |
| 'dev', |
| 'tools', |
| 'create_api_docs.dart', |
| ), |
| '--json', |
| ], |
| workingDirectory: flutterPath, |
| withStats: withStats, |
| ); |
| } |
| |
| final Directory flutterDir = |
| Directory.systemTemp.createTempSync('flutter').absolute; |
| |
| Future<void> _docHelp() async { |
| print(''' |
| Usage: |
| dart tool/task.dart doc [flutter|package|sdk|testing-package] |
| $docUsage |
| '''); |
| } |
| |
| Future<void> _docPackage( |
| ArgResults commandResults, { |
| bool withStats = false, |
| }) async { |
| var name = commandResults['name'] as String; |
| var version = commandResults['version'] as String?; |
| await docPackage(name: name, version: version, withStats: withStats); |
| } |
| |
| Future<String> docPackage({ |
| required String name, |
| required String? version, |
| bool withStats = false, |
| }) async { |
| var env = createThrowawayPubCache(); |
| var versionContext = version == null ? '' : '-$version'; |
| var launcher = SubprocessLauncher('build-$name$versionContext', env); |
| await launcher.runStreamedDartCommand([ |
| 'pub', |
| 'cache', |
| 'add', |
| if (version != null) ...['-v', version], |
| name, |
| ]); |
| var cache = Directory(path.join(env['PUB_CACHE']!, 'hosted', 'pub.dev')); |
| // The pub package should be predictably in a location like this. |
| var pubPackageDirOrig = |
| cache.listSync().firstWhere((e) => e.path.contains('/$name-')); |
| var pubPackageDir = Directory.systemTemp.createTempSync(name); |
| io_utils.copy(pubPackageDirOrig, pubPackageDir); |
| |
| var executable = Platform.executable; |
| var arguments = [ |
| '--enable-asserts', |
| path.join(Directory.current.absolute.path, 'bin', 'dartdoc.dart'), |
| '--link-to-remote', |
| '--show-progress', |
| '--show-stats', |
| '--no-validate-links', |
| ]; |
| Map<String, String>? environment; |
| |
| if (pubPackageMetaProvider |
| .fromDir(PhysicalResourceProvider.INSTANCE.getFolder(pubPackageDir.path))! |
| .requiresFlutter) { |
| var flutterRepo = |
| await FlutterRepo.fromExistingFlutterRepo(await cleanFlutterRepo); |
| executable = flutterRepo.cacheDart; |
| environment = flutterRepo.env; |
| } |
| await launcher.runStreamed( |
| executable, |
| ['pub', 'get'], |
| environment: environment, |
| workingDirectory: pubPackageDir.absolute.path, |
| ); |
| await launcher.runStreamed( |
| executable, |
| // Add the `--json` flag for the support in `runStreamed` to tease out |
| // data and messages. This flag allows the [runCompare] tasks to interpret |
| // data from stdout as structured data, in order to compare the dartdoc |
| // outputs of two commands. We add it here, but exclude it from the |
| // "Quickly re-run" text below, as that command is for human consumption. |
| [...arguments, '--json'], |
| workingDirectory: pubPackageDir.absolute.path, |
| environment: environment, |
| withStats: withStats, |
| ); |
| if (withStats) { |
| (executable, arguments) = |
| SubprocessLauncher.wrapWithTime(executable, arguments); |
| } |
| print('Quickly re-run doc generation with:'); |
| print( |
| ' pushd ${pubPackageDir.absolute.path} ;' |
| ' "$executable" ${arguments.map((a) => '"$a"').join(' ')} ;' |
| ' popd', |
| ); |
| return path.join(pubPackageDir.absolute.path, 'doc', 'api'); |
| } |
| |
| Future<void> docSdk({bool withStats = false}) async => _docSdk( |
| sdkDocsPath: _sdkDocsDir.path, |
| dartdocPath: Directory.current.path, |
| withStats: withStats, |
| ); |
| |
| /// Creates a throwaway pub cache and returns the environment variables |
| /// necessary to use it. |
| Map<String, String> createThrowawayPubCache() { |
| var pubCache = Directory.systemTemp.createTempSync('pubcache'); |
| var pubCacheBin = Directory(path.join(pubCache.path, 'bin')); |
| pubCacheBin.createSync(); |
| return Map.fromIterables([ |
| 'PUB_CACHE', |
| 'PATH', |
| ], [ |
| pubCache.path, |
| [pubCacheBin.path, Platform.environment['PATH']].join(':'), |
| ]); |
| } |
| |
| Future<String> docTestingPackage({ |
| bool withStats = false, |
| }) async { |
| var testPackagePath = testPackage.absolute.path; |
| var launcher = SubprocessLauncher('doc-test-package'); |
| await launcher.runStreamedDartCommand( |
| ['pub', 'get'], |
| workingDirectory: testPackagePath, |
| ); |
| var outputPath = _testingPackageDocsDir.absolute.path; |
| await launcher.runStreamedDartCommand( |
| [ |
| '--enable-asserts', |
| path.join(Directory.current.absolute.path, 'bin', 'dartdoc.dart'), |
| '--output', |
| outputPath, |
| '--include-source', |
| '--json', |
| '--link-to-remote', |
| '--pretty-index-json', |
| ], |
| workingDirectory: testPackagePath, |
| withStats: withStats, |
| ); |
| |
| if (withStats && (Platform.isLinux || Platform.isMacOS)) { |
| // Prints all files in `outputPath`, newline-separated. |
| var findResult = Process.runSync('find', [outputPath, '-type', 'f']); |
| var fileCount = (findResult.stdout as String).split('\n').length; |
| var diskUsageResult = Process.runSync('du', ['-sh', outputPath]); |
| // Output looks like |
| // 146M /var/folders/72/ltck4q353hsg3bn8kpkg7f84005w15/T/sdkdocsHcquiB |
| var diskUsageNumber = |
| (diskUsageResult.stdout as String).trim().split(RegExp('\\s+')).first; |
| print('Generated $fileCount files amounting to $diskUsageNumber.'); |
| } |
| |
| return outputPath; |
| } |
| |
| final Directory _testingPackageDocsDir = |
| Directory.systemTemp.createTempSync('testing_package'); |
| |
| Future<void> compareSdkWarnings() async { |
| var originalDartdocSdkDocs = |
| Directory.systemTemp.createTempSync('dartdoc-comparison-sdkdocs'); |
| var originalDartdoc = await _createComparisonDartdoc(); |
| var sdkDocsPath = |
| Directory.systemTemp.createTempSync('sdkdocs').absolute.path; |
| var currentDartdocSdkBuild = _docSdk( |
| sdkDocsPath: sdkDocsPath, |
| dartdocPath: Directory.current.path, |
| ); |
| var originalDartdocSdkBuild = _docSdk( |
| sdkDocsPath: originalDartdocSdkDocs.path, |
| dartdocPath: originalDartdoc, |
| ); |
| var currentDartdocWarnings = _collectWarnings( |
| messages: await currentDartdocSdkBuild, |
| tempPath: sdkDocsPath, |
| branch: 'HEAD', |
| ); |
| var originalDartdocWarnings = _collectWarnings( |
| messages: await originalDartdocSdkBuild, |
| tempPath: originalDartdocSdkDocs.absolute.path, |
| branch: _dartdocOriginalBranch, |
| ); |
| |
| print(originalDartdocWarnings.warningDeltaText( |
| 'SDK docs', currentDartdocWarnings)); |
| } |
| |
| Future<Iterable<Map<String, Object?>>> _docSdk({ |
| required String sdkDocsPath, |
| required String dartdocPath, |
| bool withStats = false, |
| }) async { |
| var launcher = SubprocessLauncher('build-sdk-docs'); |
| await launcher |
| .runStreamedDartCommand(['pub', 'get'], workingDirectory: dartdocPath); |
| var output = await launcher.runStreamedDartCommand( |
| [ |
| '--enable-asserts', |
| path.join('bin', 'dartdoc.dart'), |
| '--output', |
| sdkDocsPath, |
| '--sdk-docs', |
| '--json', |
| '--show-progress', |
| // Use some local assets for the header and footers, to override the SDK |
| // values, from an options file, which includes not-shipped files. |
| '--header', |
| 'lib/resources/blank.txt', |
| '--footer', |
| 'lib/resources/blank.txt', |
| '--footer-text', |
| 'lib/resources/blank.txt', |
| ], |
| workingDirectory: dartdocPath, |
| withStats: withStats, |
| ); |
| if (withStats && (Platform.isLinux || Platform.isMacOS)) { |
| var diskUsageResult = Process.runSync('du', ['-sh', sdkDocsPath]); |
| // Output looks like |
| // 146M /var/folders/72/ltck4q353hsg3bn8kpkg7f84005w15/T/sdkdocsHcquiB |
| var diskUsageNumber = |
| (diskUsageResult.stdout as String).trim().split(RegExp('\\s+')).first; |
| |
| // Prints all files in `sdkDocsPath`, newline-separated. |
| var findResult = Process.runSync('find', [sdkDocsPath, '-type', 'f']); |
| var fileCount = (findResult.stdout as String).split('\n').length; |
| print('Generated $fileCount files amounting to $diskUsageNumber.'); |
| } |
| return output; |
| } |
| |
| /// Creates a clean version of dartdoc (based on the current directory, assumed |
| /// to be a git repository). |
| /// |
| /// Uses [_dartdocOriginalBranch] to checkout a branch or tag. |
| Future<String> _createComparisonDartdoc() async { |
| var launcher = SubprocessLauncher('create-comparison-dartdoc'); |
| var dartdocClean = Directory.systemTemp.createTempSync('dartdoc-comparison'); |
| await launcher |
| .runStreamed('git', ['clone', Directory.current.path, dartdocClean.path]); |
| await launcher.runStreamed('git', ['checkout', _dartdocOriginalBranch], |
| workingDirectory: dartdocClean.path); |
| await launcher.runStreamed(Platform.resolvedExecutable, ['pub', 'get'], |
| workingDirectory: dartdocClean.path); |
| return dartdocClean.path; |
| } |
| |
| /// Version of dartdoc we should use when making comparisons. |
| String get _dartdocOriginalBranch { |
| var branch = Platform.environment['DARTDOC_ORIGINAL'] ?? 'main'; |
| print('using branch/tag: $branch for comparison from \$DARTDOC_ORIGINAL'); |
| return branch; |
| } |
| |
| Future<void> runHelp(ArgResults commandResults) async { |
| if (commandResults.rest.isEmpty) { |
| // TODO(srawlins): Add more help for more individual commands. |
| print(''' |
| Usage: |
| dart tool/task.dart [analyze|build|buildbot|clean|compare|doc|help|serve|test|tryp-publish|validate] options... |
| |
| Help usage: |
| dart tool/task.dart help [doc|serve] |
| '''); |
| return; |
| } |
| if (commandResults.rest.length != 1) { |
| throw ArgumentError('"help" command requires a single command name.'); |
| } |
| var command = commandResults.rest.single; |
| return switch (command) { |
| 'build' => _buildHelp(), |
| 'doc' => _docHelp(), |
| 'serve' => _serveHelp(), |
| _ => throw UnimplementedError( |
| 'Unknown command: "$command", or no specific help text'), |
| }; |
| } |
| |
| Future<void> runServe(ArgResults commandResults) async { |
| if (commandResults.rest.length != 1) { |
| throw ArgumentError('"serve" command requires a single target.'); |
| } |
| var target = commandResults.rest.single; |
| await switch (target) { |
| 'flutter' => serveFlutterDocs(), |
| 'help' => _serveHelp(), |
| 'package' => _servePackageDocs(commandResults), |
| 'sdk' => serveSdkDocs(), |
| 'testing-package' => serveTestingPackageDocs(), |
| _ => throw UnimplementedError('Unknown serve target: "$target"'), |
| }; |
| } |
| |
| Future<void> serveFlutterDocs() async { |
| await docFlutter(); |
| print('launching dhttpd on port 8001 for Flutter'); |
| var launcher = SubprocessLauncher('serve-flutter-docs'); |
| await launcher.runStreamedDartCommand(['pub', 'get']); |
| await launcher.runStreamedDartCommand([ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '8001', |
| '--path', |
| path.join(flutterDir.path, 'dev', 'docs', 'doc'), |
| ]); |
| } |
| |
| Future<void> _serveHelp() async { |
| print(''' |
| Usage: |
| dart tool/task.dart serve [flutter|package|sdk|testing-package] |
| $docUsage |
| '''); |
| } |
| |
| Future<void> _servePackageDocs(ArgResults commandResults) async { |
| var name = commandResults['name'] as String; |
| var version = commandResults['version'] as String?; |
| await servePackageDocs(name: name, version: version); |
| } |
| |
| Future<void> servePackageDocs( |
| {required String name, required String? version}) async { |
| var packageDocsPath = await docPackage(name: name, version: version); |
| await _serveDocsFrom(packageDocsPath, 9000, 'serve-pub-package'); |
| } |
| |
| Future<void> serveSdkDocs() async { |
| await docSdk(); |
| print('launching dhttpd on port 8000 for SDK'); |
| await SubprocessLauncher('serve-sdk-docs').runStreamedDartCommand([ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '8000', |
| '--path', |
| _sdkDocsDir.path, |
| ]); |
| } |
| |
| bool _serveReady = false; |
| |
| Future<void> _serveDocsFrom(String servePath, int port, String context) async { |
| print('launching dhttpd on port $port for $context'); |
| var launcher = SubprocessLauncher(context); |
| if (!_serveReady) { |
| await launcher.runStreamedDartCommand(['pub', 'get']); |
| await launcher |
| .runStreamedDartCommand(['pub', 'global', 'activate', 'dhttpd']); |
| _serveReady = true; |
| } |
| await launcher.runStreamedDartCommand([ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '$port', |
| '--path', |
| servePath |
| ]); |
| } |
| |
| Future<void> serveTestingPackageDocs() async { |
| var outputPath = await docTestingPackage(); |
| print('launching dhttpd on port 8002 for SDK'); |
| var launcher = SubprocessLauncher('serve-test-package-docs'); |
| await launcher.runStreamed(Platform.resolvedExecutable, [ |
| 'pub', |
| 'global', |
| 'run', |
| 'dhttpd', |
| '--port', |
| '8002', |
| '--path', |
| outputPath, |
| ]); |
| } |
| |
| Future<void> runTest() async { |
| await analyzeTestPackages(); |
| await SubprocessLauncher('dart run test') |
| .runStreamedDartCommand(['--enable-asserts', 'run', 'test']); |
| } |
| |
| Future<void> runTryPublish() async { |
| await SubprocessLauncher('try-publish') |
| .runStreamed(Platform.resolvedExecutable, ['pub', 'publish', '-n']); |
| } |
| |
| Future<void> runValidate(ArgResults commandResults) async { |
| for (var target in commandResults.rest) { |
| await switch (target) { |
| 'build' => validateBuild(), |
| 'dartdoc-docs' => validateDartdocDocs(), |
| 'format' => validateFormat(), |
| 'sdk-docs' => validateSdkDocs(), |
| _ => throw UnimplementedError('Unknown validation target: "$target"'), |
| }; |
| } |
| } |
| |
| Future<void> validateBuild() async { |
| var originalFileContents = <String, String>{}; |
| var differentFiles = <String>[]; |
| |
| // Load original file contents into memory before running the builder; it |
| // modifies them in place. |
| for (var relPath in _generatedFilesList) { |
| var origPath = path.joinAll(['lib', relPath]); |
| var oldVersion = File(origPath); |
| if (oldVersion.existsSync()) { |
| originalFileContents[relPath] = oldVersion.readAsStringSync(); |
| } |
| } |
| |
| await buildAll(); |
| |
| for (var relPath in _generatedFilesList) { |
| if (relPath.contains('runtime_renderers') && !_analyzerInUseIsTarget) { |
| // The content of these files is very specific to the version of the |
| // analyzer package in use. So we only validate if we are working on that |
| // exact version. |
| continue; |
| } |
| var newVersion = File(path.join('lib', relPath)); |
| if (!newVersion.existsSync()) { |
| print('${newVersion.path} does not exist\n'); |
| differentFiles.add(relPath); |
| } else { |
| var newVersionText = await newVersion.readAsString(); |
| if (originalFileContents[relPath] != newVersionText) { |
| print('${newVersion.path} has changed to: \n$newVersionText)'); |
| differentFiles.add(relPath); |
| } |
| } |
| } |
| |
| if (differentFiles.isNotEmpty) { |
| throw StateError(''' |
| The following generated files needed to be rebuilt: |
| ${differentFiles.map((f) => path.join('lib', f)).join("\n ")} |
| Rebuild them with "dart tool/task.dart build" and check the results in. |
| '''); |
| } |
| |
| // Verify that the web frontend has been compiled. |
| final currentSig = |
| await _calcFilesSig(Directory('web'), extensions: {'.dart', '.scss'}); |
| final lastCompileSig = |
| File(path.join('web', 'sig.txt')).readAsStringSync().trim(); |
| if (currentSig != lastCompileSig) { |
| print('current files: $currentSig'); |
| print('cached sig : $lastCompileSig'); |
| throw StateError( |
| 'The web frontend (web/docs.dart) needs to be recompiled; rebuild it ' |
| 'with "dart tool/task.dart build web".'); |
| } |
| |
| // Reset some files for `try-publish` step. This check looks for changes in |
| // the current git checkout: https://github.com/dart-lang/pub/pull/4373. |
| Process.runSync('git', [ |
| 'checkout', |
| '--', |
| 'lib/resources/docs.dart.js', |
| 'lib/resources/docs.dart.js.map', |
| ]); |
| } |
| |
| /// Whether the analyzer in use (as found in `pubspec.lock`) is the target |
| /// version of analyzer, against which we verify the runtime renderer files. |
| bool get _analyzerInUseIsTarget { |
| // TODO(srawlins): Add validation that this number falls within the |
| // constraints of the analyzer package which are set in `pubspec.yaml`. |
| const analyzerTarget = '7.2.0'; |
| |
| var lockfilePath = path.join(Directory.current.path, 'pubspec.lock'); |
| var lockfile = loadYaml(File(lockfilePath).readAsStringSync()) as YamlMap; |
| var packages = lockfile['packages'] as YamlMap; |
| var analyzer = packages['analyzer'] as YamlMap; |
| var analyzerInUse = analyzer['version'] as String; |
| return analyzerInUse == analyzerTarget; |
| } |
| |
| /// Paths in this list are relative to lib/. |
| final _generatedFilesList = [ |
| '../dartdoc_options.yaml', |
| 'src/generator/html_resources.g.dart', |
| 'src/generator/templates.aot_renderers_for_html.dart', |
| 'src/generator/templates.runtime_renderers.dart', |
| 'src/version.dart', |
| '../test/mustachio/foo.dart', |
| ].map((s) => path.joinAll(path.posix.split(s))); |
| |
| Future<void> validateDartdocDocs() async { |
| var launcher = SubprocessLauncher('test-dartdoc'); |
| await launcher.runStreamedDartCommand([ |
| '--enable-asserts', |
| path.join('bin', 'dartdoc.dart'), |
| '--output', |
| _dartdocDocsPath, |
| '--no-link-to-remote', |
| ]); |
| _expectFileContains(path.join(_dartdocDocsPath, 'index.html'), |
| '<title>dartdoc - Dart API docs</title>'); |
| var objectText = RegExp('<li>Object</li>', multiLine: true); |
| _expectFileContains( |
| path.join(_dartdocDocsPath, 'dartdoc', 'PubPackageMeta-class.html'), |
| objectText, |
| ); |
| } |
| |
| /// Kind of an inefficient grepper for now. |
| void _expectFileContains(String filePath, Pattern text) { |
| var source = File(filePath); |
| if (!source.existsSync()) { |
| throw StateError('file not found: $filePath'); |
| } |
| if (!File(filePath).readAsStringSync().contains(text)) { |
| throw StateError('"$text" not found in $filePath'); |
| } |
| } |
| |
| final String _dartdocDocsPath = |
| Directory.systemTemp.createTempSync('dartdoc').path; |
| |
| Future<void> validateFormat() async { |
| var processResult = Process.runSync(Platform.resolvedExecutable, [ |
| 'format', |
| '-o', |
| 'none', |
| 'bin', |
| 'lib', |
| 'test', |
| 'tool', |
| 'web', |
| ]); |
| if (processResult.exitCode != 0) { |
| throw StateError(''' |
| Not all files are formatted: |
| |
| ${processResult.stderr} |
| '''); |
| } |
| } |
| |
| Future<void> validateSdkDocs() async { |
| await docSdk(); |
| const expectedLibCount = 0; |
| const expectedSubLibCount = 20; |
| const expectedTotalCount = 20; |
| var indexHtml = File(path.join(_sdkDocsDir.path, 'index.html')); |
| if (!indexHtml.existsSync()) { |
| throw StateError("No 'index.html' found for the SDK docs"); |
| } |
| print("Found 'index.html'"); |
| var indexContents = indexHtml.readAsStringSync(); |
| var foundLibCount = _findCount(indexContents, ' <li><a href="dart-'); |
| if (expectedLibCount != foundLibCount) { |
| throw StateError( |
| "Expected $expectedLibCount 'dart:' entries in 'index.html', but " |
| 'found $foundLibCount'); |
| } |
| print("Found $foundLibCount 'dart:' entries in 'index.html'"); |
| |
| var libLinkPattern = |
| RegExp('<li class="section-subitem"><a [^>]*href="dart-'); |
| var foundSubLibCount = _findCount(indexContents, libLinkPattern); |
| if (expectedSubLibCount != foundSubLibCount) { |
| throw StateError("Expected $expectedSubLibCount 'dart:' entries in " |
| "'index.html' to be in categories, but found $foundSubLibCount"); |
| } |
| print('$foundSubLibCount index.html dart: entries in categories found'); |
| |
| // Check for the existence of certain files and directories. |
| var libraries = |
| _sdkDocsDir.listSync().where((fs) => fs.path.contains('dart-')); |
| var libraryCount = libraries.length; |
| if (expectedTotalCount != libraryCount) { |
| var libraryNames = |
| libraries.map((l) => "'${path.basename(l.path)}'").join(', '); |
| throw StateError('Unexpected docs generated for SDK libraries; expected ' |
| '$expectedTotalCount directories, but $libraryCount directories were ' |
| 'generated: $libraryNames'); |
| } |
| print("Found $libraryCount 'dart:' libraries"); |
| |
| var futureConstructorFile = |
| File(path.join(_sdkDocsDir.path, 'dart-async', 'Future', 'Future.html')); |
| if (!futureConstructorFile.existsSync()) { |
| throw StateError('No Future.html found for dart:async Future constructor'); |
| } |
| print('Found Future.async constructor'); |
| } |
| |
| /// A temporary directory into which the SDK can be documented. |
| final Directory _sdkDocsDir = |
| Directory.systemTemp.createTempSync('sdkdocs').absolute; |
| |
| /// Returns the number of (perhaps overlapping) occurrences of [str] in [match]. |
| int _findCount(String str, Pattern match) { |
| var count = 0; |
| var index = str.indexOf(match); |
| while (index != -1) { |
| count++; |
| index = str.indexOf(match, index + 1); |
| } |
| return count; |
| } |
| |
| Future<String> _calcFilesSig(Directory dir, |
| {required Set<String> extensions}) async { |
| final digest = await _fileLines(dir, extensions: extensions) |
| .transform(utf8.encoder) |
| .transform(crypto.md5) |
| .single; |
| |
| return digest.bytes |
| .map((byte) => byte.toRadixString(16).padLeft(2, '0').toUpperCase()) |
| .join(); |
| } |
| |
| /// Returns a map of warning texts to the number of times each has been seen. |
| WarningsCollection _collectWarnings({ |
| required Iterable<Map<Object, Object?>> messages, |
| required String tempPath, |
| required String branch, |
| String? pubDir, |
| }) { |
| var warningTexts = WarningsCollection(tempPath, pubDir, branch); |
| for (final message in messages) { |
| if (message.containsKey('level') && |
| message['level'] == 'WARNING' && |
| message.containsKey('data')) { |
| var data = message['data'] as Map; |
| warningTexts.add(data['text'] as String); |
| } |
| } |
| return warningTexts; |
| } |