1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:io' show ProcessResult, exitCode, stderr;
6
7import 'package:args/args.dart';
8import 'package:file/file.dart';
9import 'package:file/local.dart';
10import 'package:path/path.dart' as path;
11import 'package:platform/platform.dart';
12import 'package:process/process.dart';
13import 'package:snippets/snippets.dart';
14
15const String _kElementOption = 'element';
16const String _kHelpOption = 'help';
17const String _kInputOption = 'input';
18const String _kLibraryOption = 'library';
19const String _kOutputDirectoryOption = 'output-directory';
20const String _kOutputOption = 'output';
21const String _kPackageOption = 'package';
22const String _kSerialOption = 'serial';
23const String _kTypeOption = 'type';
24
25class GitStatusFailed implements Exception {
26 GitStatusFailed(this.gitResult);
27
28 final ProcessResult gitResult;
29
30 @override
31 String toString() {
32 return 'git status exited with a non-zero exit code: '
33 '${gitResult.exitCode}:\n${gitResult.stderr}\n${gitResult.stdout}';
34 }
35}
36
37/// A singleton filesystem that can be set by tests to a memory filesystem.
38FileSystem filesystem = const LocalFileSystem();
39
40/// A singleton snippet generator that can be set by tests to a mock, so that
41/// we can test the command line parsing.
42SnippetGenerator snippetGenerator = SnippetGenerator();
43
44/// A singleton platform that can be set by tests for use in testing command line
45/// parsing.
46Platform platform = const LocalPlatform();
47
48/// A singleton process manager that can be set by tests for use in testing.
49ProcessManager processManager = const LocalProcessManager();
50
51/// Get the name of the channel these docs are from.
52///
53/// First check env variable LUCI_BRANCH, then refer to the currently
54/// checked out git branch.
55String getChannelName({
56 Platform platform = const LocalPlatform(),
57 ProcessManager processManager = const LocalProcessManager(),
58}) {
59 switch (platform.environment['LUCI_BRANCH']?.trim()) {
60 // Backward compatibility: Still support running on "master", but pretend it is "main".
61 case 'master' || 'main':
62 return 'main';
63 case 'stable':
64 return 'stable';
65 }
66
67 final RegExp gitBranchRegexp = RegExp(r'^## (?<branch>.*)');
68 final ProcessResult gitResult = processManager.runSync(
69 <String>['git', 'status', '-b', '--porcelain'],
70 // Use the FLUTTER_ROOT, if defined.
71 workingDirectory:
72 platform.environment['FLUTTER_ROOT']?.trim() ?? filesystem.currentDirectory.path,
73 // Adding extra debugging output to help debug why git status inexplicably fails
74 // (random non-zero error code) about 2% of the time.
75 environment: <String, String>{'GIT_TRACE': '2', 'GIT_TRACE_SETUP': '2'},
76 );
77 if (gitResult.exitCode != 0) {
78 throw GitStatusFailed(gitResult);
79 }
80
81 final RegExpMatch? gitBranchMatch = gitBranchRegexp.firstMatch(
82 (gitResult.stdout as String).trim().split('\n').first,
83 );
84 return gitBranchMatch == null
85 ? '<unknown>'
86 : gitBranchMatch.namedGroup('branch')!.split('...').first;
87}
88
89const List<String> sampleTypes = <String>['snippet', 'sample', 'dartpad'];
90
91// This is a hack to workaround the fact that git status inexplicably fails
92// (with random non-zero error code) about 2% of the time.
93String getChannelNameWithRetries({
94 Platform platform = const LocalPlatform(),
95 ProcessManager processManager = const LocalProcessManager(),
96}) {
97 int retryCount = 0;
98
99 while (retryCount < 2) {
100 try {
101 return getChannelName(platform: platform, processManager: processManager);
102 } on GitStatusFailed catch (e) {
103 retryCount += 1;
104 stderr.write('git status failed, retrying ($retryCount)\nError report:\n$e');
105 }
106 }
107
108 return getChannelName(platform: platform, processManager: processManager);
109}
110
111/// Generates snippet dartdoc output for a given input, and creates any sample
112/// applications needed by the snippet.
113void main(List<String> argList) {
114 final Map<String, String> environment = platform.environment;
115 final ArgParser parser = ArgParser();
116
117 parser.addOption(
118 _kTypeOption,
119 defaultsTo: 'dartpad',
120 allowed: sampleTypes,
121 allowedHelp: <String, String>{
122 'dartpad': 'Produce a code sample application for using in Dartpad.',
123 'sample': 'Produce a code sample application.',
124 'snippet': 'Produce a nicely formatted piece of sample code.',
125 },
126 help: 'The type of snippet to produce.',
127 );
128 parser.addOption(
129 _kOutputOption,
130 help:
131 'The output name for the generated sample application. Overrides '
132 'the naming generated by the --$_kPackageOption/--$_kLibraryOption/--$_kElementOption '
133 'arguments. Metadata will be written alongside in a .json file. '
134 'The basename of this argument is used as the ID. If this is a '
135 'relative path, will be placed under the --$_kOutputDirectoryOption location.',
136 );
137 parser.addOption(
138 _kOutputDirectoryOption,
139 defaultsTo: '.',
140 help: 'The output path for the generated sample application.',
141 );
142 parser.addOption(
143 _kInputOption,
144 defaultsTo: environment['INPUT'],
145 help: 'The input file containing the sample code to inject.',
146 );
147 parser.addOption(
148 _kPackageOption,
149 defaultsTo: environment['PACKAGE_NAME'],
150 help: 'The name of the package that this sample belongs to.',
151 );
152 parser.addOption(
153 _kLibraryOption,
154 defaultsTo: environment['LIBRARY_NAME'],
155 help: 'The name of the library that this sample belongs to.',
156 );
157 parser.addOption(
158 _kElementOption,
159 defaultsTo: environment['ELEMENT_NAME'],
160 help: 'The name of the element that this sample belongs to.',
161 );
162 parser.addOption(
163 _kSerialOption,
164 defaultsTo: environment['INVOCATION_INDEX'],
165 help: 'A unique serial number for this snippet tool invocation.',
166 );
167 parser.addFlag(
168 _kHelpOption,
169 negatable: false,
170 help: 'Prints help documentation for this command',
171 );
172
173 final ArgResults args = parser.parse(argList);
174
175 if (args[_kHelpOption]! as bool) {
176 stderr.writeln(parser.usage);
177 exitCode = 0;
178 return;
179 }
180
181 final String sampleType = args[_kTypeOption]! as String;
182
183 if (args[_kInputOption] == null) {
184 stderr.writeln(parser.usage);
185 errorExit(
186 'The --$_kInputOption option must be specified, either on the command '
187 'line, or in the INPUT environment variable.',
188 );
189 return;
190 }
191
192 final File input = filesystem.file(args['input']! as String);
193 if (!input.existsSync()) {
194 errorExit('The input file ${input.path} does not exist.');
195 return;
196 }
197
198 final String packageName = args[_kPackageOption] as String? ?? '';
199 final String libraryName = args[_kLibraryOption] as String? ?? '';
200 final String elementName = args[_kElementOption] as String? ?? '';
201 final String serial = args[_kSerialOption] as String? ?? '';
202 late String id;
203 File? output;
204 final Directory outputDirectory =
205 filesystem.directory(args[_kOutputDirectoryOption]! as String).absolute;
206
207 if (args[_kOutputOption] != null) {
208 id = path.basenameWithoutExtension(args[_kOutputOption]! as String);
209 final File outputPath = filesystem.file(args[_kOutputOption]! as String);
210 if (outputPath.isAbsolute) {
211 output = outputPath;
212 } else {
213 output = filesystem.file(path.join(outputDirectory.path, outputPath.path));
214 }
215 } else {
216 final List<String> idParts = <String>[];
217 if (packageName.isNotEmpty && packageName != 'flutter') {
218 idParts.add(packageName.replaceAll(RegExp(r'\W'), '_').toLowerCase());
219 }
220 if (libraryName.isNotEmpty) {
221 idParts.add(libraryName.replaceAll(RegExp(r'\W'), '_').toLowerCase());
222 }
223 if (elementName.isNotEmpty) {
224 idParts.add(elementName);
225 }
226 if (serial.isNotEmpty) {
227 idParts.add(serial);
228 }
229 if (idParts.isEmpty) {
230 errorExit(
231 'Unable to determine ID. At least one of --$_kPackageOption, '
232 '--$_kLibraryOption, --$_kElementOption, -$_kSerialOption, or the environment variables '
233 'PACKAGE_NAME, LIBRARY_NAME, ELEMENT_NAME, or INVOCATION_INDEX must be non-empty.',
234 );
235 return;
236 }
237 id = idParts.join('.');
238 output = outputDirectory.childFile('$id.dart');
239 }
240 output.parent.createSync(recursive: true);
241
242 final int? sourceLine =
243 environment['SOURCE_LINE'] != null ? int.tryParse(environment['SOURCE_LINE']!) : null;
244 final String sourcePath = environment['SOURCE_PATH'] ?? 'unknown.dart';
245 final SnippetDartdocParser sampleParser = SnippetDartdocParser(filesystem);
246 final SourceElement element = sampleParser.parseFromDartdocToolFile(
247 input,
248 startLine: sourceLine,
249 element: elementName,
250 sourceFile: filesystem.file(sourcePath),
251 type: sampleType,
252 );
253 final Map<String, Object?> metadata = <String, Object?>{
254 'channel': getChannelNameWithRetries(platform: platform, processManager: processManager),
255 'serial': serial,
256 'id': id,
257 'package': packageName,
258 'library': libraryName,
259 'element': elementName,
260 };
261
262 for (final CodeSample sample in element.samples) {
263 sample.metadata.addAll(metadata);
264 snippetGenerator.generateCode(sample, output: output);
265 print(snippetGenerator.generateHtml(sample));
266 }
267
268 exitCode = 0;
269}
270

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com