| // 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' show ProcessResult, exitCode, stderr; | |
| import 'package:args/args.dart'; | |
| import 'package:file/file.dart'; | |
| import 'package:file/local.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'package:platform/platform.dart'; | |
| import 'package:process/process.dart'; | |
| import 'package:snippets/snippets.dart'; | |
| const String _kElementOption = 'element'; | |
| const String _kHelpOption = 'help'; | |
| const String _kInputOption = 'input'; | |
| const String _kLibraryOption = 'library'; | |
| const String _kOutputDirectoryOption = 'output-directory'; | |
| const String _kOutputOption = 'output'; | |
| const String _kPackageOption = 'package'; | |
| const String _kSerialOption = 'serial'; | |
| const String _kTypeOption = 'type'; | |
| class GitStatusFailed implements Exception { | |
| GitStatusFailed(this.gitResult); | |
| final ProcessResult gitResult; | |
| @override | |
| String toString() { | |
| return 'git status exited with a non-zero exit code: ' | |
| '${gitResult.exitCode}:\n${gitResult.stderr}\n${gitResult.stdout}'; | |
| } | |
| } | |
| /// A singleton filesystem that can be set by tests to a memory filesystem. | |
| FileSystem filesystem = const LocalFileSystem(); | |
| /// A singleton snippet generator that can be set by tests to a mock, so that | |
| /// we can test the command line parsing. | |
| SnippetGenerator snippetGenerator = SnippetGenerator(); | |
| /// A singleton platform that can be set by tests for use in testing command line | |
| /// parsing. | |
| Platform platform = const LocalPlatform(); | |
| /// A singleton process manager that can be set by tests for use in testing. | |
| ProcessManager processManager = const LocalProcessManager(); | |
| /// Get the name of the channel these docs are from. | |
| /// | |
| /// First check env variable LUCI_BRANCH, then refer to the currently | |
| /// checked out git branch. | |
| String getChannelName({ | |
| Platform platform = const LocalPlatform(), | |
| ProcessManager processManager = const LocalProcessManager(), | |
| }) { | |
| switch (platform.environment['LUCI_BRANCH']?.trim()) { | |
| // Backward compatibility: Still support running on "master", but pretend it is "main". | |
| case 'master' || 'main': | |
| return 'main'; | |
| case 'stable': | |
| return 'stable'; | |
| } | |
| final gitBranchRegexp = RegExp(r'^## (?<branch>.*)'); | |
| final ProcessResult gitResult = processManager.runSync( | |
| <String>['git', 'status', '-b', '--porcelain'], | |
| // Use the FLUTTER_ROOT, if defined. | |
| workingDirectory: | |
| platform.environment['FLUTTER_ROOT']?.trim() ?? filesystem.currentDirectory.path, | |
| // Adding extra debugging output to help debug why git status inexplicably fails | |
| // (random non-zero error code) about 2% of the time. | |
| environment: <String, String>{'GIT_TRACE': '2', 'GIT_TRACE_SETUP': '2'}, | |
| ); | |
| if (gitResult.exitCode != 0) { | |
| throw GitStatusFailed(gitResult); | |
| } | |
| final RegExpMatch? gitBranchMatch = gitBranchRegexp.firstMatch( | |
| (gitResult.stdout as String).trim().split('\n').first, | |
| ); | |
| return gitBranchMatch == null | |
| ? '<unknown>' | |
| : gitBranchMatch.namedGroup('branch')!.split('...').first; | |
| } | |
| const List<String> sampleTypes = <String>['snippet', 'sample', 'dartpad']; | |
| // This is a hack to workaround the fact that git status inexplicably fails | |
| // (with random non-zero error code) about 2% of the time. | |
| String getChannelNameWithRetries({ | |
| Platform platform = const LocalPlatform(), | |
| ProcessManager processManager = const LocalProcessManager(), | |
| }) { | |
| var retryCount = 0; | |
| while (retryCount < 2) { | |
| try { | |
| return getChannelName(platform: platform, processManager: processManager); | |
| } on GitStatusFailed catch (e) { | |
| retryCount += 1; | |
| stderr.write('git status failed, retrying ($retryCount)\nError report:\n$e'); | |
| } | |
| } | |
| return getChannelName(platform: platform, processManager: processManager); | |
| } | |
| /// Generates snippet dartdoc output for a given input, and creates any sample | |
| /// applications needed by the snippet. | |
| void main(List<String> argList) { | |
| final Map<String, String> environment = platform.environment; | |
| final parser = ArgParser(); | |
| parser.addOption( | |
| _kTypeOption, | |
| defaultsTo: 'dartpad', | |
| allowed: sampleTypes, | |
| allowedHelp: <String, String>{ | |
| 'dartpad': 'Produce a code sample application for using in Dartpad.', | |
| 'sample': 'Produce a code sample application.', | |
| 'snippet': 'Produce a nicely formatted piece of sample code.', | |
| }, | |
| help: 'The type of snippet to produce.', | |
| ); | |
| parser.addOption( | |
| _kOutputOption, | |
| help: | |
| 'The output name for the generated sample application. Overrides ' | |
| 'the naming generated by the --$_kPackageOption/--$_kLibraryOption/--$_kElementOption ' | |
| 'arguments. Metadata will be written alongside in a .json file. ' | |
| 'The basename of this argument is used as the ID. If this is a ' | |
| 'relative path, will be placed under the --$_kOutputDirectoryOption location.', | |
| ); | |
| parser.addOption( | |
| _kOutputDirectoryOption, | |
| defaultsTo: '.', | |
| help: 'The output path for the generated sample application.', | |
| ); | |
| parser.addOption( | |
| _kInputOption, | |
| defaultsTo: environment['INPUT'], | |
| help: 'The input file containing the sample code to inject.', | |
| ); | |
| parser.addOption( | |
| _kPackageOption, | |
| defaultsTo: environment['PACKAGE_NAME'], | |
| help: 'The name of the package that this sample belongs to.', | |
| ); | |
| parser.addOption( | |
| _kLibraryOption, | |
| defaultsTo: environment['LIBRARY_NAME'], | |
| help: 'The name of the library that this sample belongs to.', | |
| ); | |
| parser.addOption( | |
| _kElementOption, | |
| defaultsTo: environment['ELEMENT_NAME'], | |
| help: 'The name of the element that this sample belongs to.', | |
| ); | |
| parser.addOption( | |
| _kSerialOption, | |
| defaultsTo: environment['INVOCATION_INDEX'], | |
| help: 'A unique serial number for this snippet tool invocation.', | |
| ); | |
| parser.addFlag( | |
| _kHelpOption, | |
| negatable: false, | |
| help: 'Prints help documentation for this command', | |
| ); | |
| final ArgResults args = parser.parse(argList); | |
| if (args[_kHelpOption]! as bool) { | |
| stderr.writeln(parser.usage); | |
| exitCode = 0; | |
| return; | |
| } | |
| final sampleType = args[_kTypeOption]! as String; | |
| if (args[_kInputOption] == null) { | |
| stderr.writeln(parser.usage); | |
| errorExit( | |
| 'The --$_kInputOption option must be specified, either on the command ' | |
| 'line, or in the INPUT environment variable.', | |
| ); | |
| return; | |
| } | |
| final File input = filesystem.file(args['input']! as String); | |
| if (!input.existsSync()) { | |
| errorExit('The input file ${input.path} does not exist.'); | |
| return; | |
| } | |
| final String packageName = args[_kPackageOption] as String? ?? ''; | |
| final String libraryName = args[_kLibraryOption] as String? ?? ''; | |
| final String elementName = args[_kElementOption] as String? ?? ''; | |
| final String serial = args[_kSerialOption] as String? ?? ''; | |
| late String id; | |
| File? output; | |
| final Directory outputDirectory = filesystem | |
| .directory(args[_kOutputDirectoryOption]! as String) | |
| .absolute; | |
| if (args[_kOutputOption] != null) { | |
| id = path.basenameWithoutExtension(args[_kOutputOption]! as String); | |
| final File outputPath = filesystem.file(args[_kOutputOption]! as String); | |
| if (outputPath.isAbsolute) { | |
| output = outputPath; | |
| } else { | |
| output = filesystem.file(path.join(outputDirectory.path, outputPath.path)); | |
| } | |
| } else { | |
| final idParts = <String>[]; | |
| if (packageName.isNotEmpty && packageName != 'flutter') { | |
| idParts.add(packageName.replaceAll(RegExp(r'\W'), '_').toLowerCase()); | |
| } | |
| if (libraryName.isNotEmpty) { | |
| idParts.add(libraryName.replaceAll(RegExp(r'\W'), '_').toLowerCase()); | |
| } | |
| if (elementName.isNotEmpty) { | |
| idParts.add(elementName); | |
| } | |
| if (serial.isNotEmpty) { | |
| idParts.add(serial); | |
| } | |
| if (idParts.isEmpty) { | |
| errorExit( | |
| 'Unable to determine ID. At least one of --$_kPackageOption, ' | |
| '--$_kLibraryOption, --$_kElementOption, -$_kSerialOption, or the environment variables ' | |
| 'PACKAGE_NAME, LIBRARY_NAME, ELEMENT_NAME, or INVOCATION_INDEX must be non-empty.', | |
| ); | |
| return; | |
| } | |
| id = idParts.join('.'); | |
| output = outputDirectory.childFile('$id.dart'); | |
| } | |
| output.parent.createSync(recursive: true); | |
| final int? sourceLine = environment['SOURCE_LINE'] != null | |
| ? int.tryParse(environment['SOURCE_LINE']!) | |
| : null; | |
| final String sourcePath = environment['SOURCE_PATH'] ?? 'unknown.dart'; | |
| final sampleParser = SnippetDartdocParser(filesystem); | |
| final SourceElement element = sampleParser.parseFromDartdocToolFile( | |
| input, | |
| startLine: sourceLine, | |
| element: elementName, | |
| sourceFile: filesystem.file(sourcePath), | |
| type: sampleType, | |
| ); | |
| final metadata = <String, Object?>{ | |
| 'channel': getChannelNameWithRetries(platform: platform, processManager: processManager), | |
| 'serial': serial, | |
| 'id': id, | |
| 'package': packageName, | |
| 'library': libraryName, | |
| 'element': elementName, | |
| }; | |
| for (final CodeSample sample in element.samples) { | |
| sample.metadata.addAll(metadata); | |
| snippetGenerator.generateCode(sample, output: output); | |
| print(snippetGenerator.generateHtml(sample)); | |
| } | |
| exitCode = 0; | |
| } |