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 'package:meta/meta.dart';
6
7import '../../src/base/process.dart';
8import '../../src/convert.dart' show json;
9import '../../src/macos/xcode.dart';
10import '../base/version.dart';
11import '../convert.dart';
12
13/// The generator of xcresults.
14///
15/// Call [generate] after an iOS/MacOS build will generate a [XCResult].
16/// This only works when the `-resultBundleVersion` is set to 3.
17/// * See also: [XCResult].
18class XCResultGenerator {
19 /// Construct the [XCResultGenerator].
20 XCResultGenerator({
21 required this.resultPath,
22 required this.xcode,
23 required this.processUtils,
24 });
25
26 /// The file path that used to store the xcrun result.
27 ///
28 /// There's usually a `resultPath.xcresult` file in the same folder.
29 final String resultPath;
30
31 /// The [ProcessUtils] to run commands.
32 final ProcessUtils processUtils;
33
34 /// [Xcode] object used to run xcode command.
35 final Xcode xcode;
36
37 /// Generates the XCResult.
38 ///
39 /// Calls `xcrun xcresulttool get --legacy --path <resultPath> --format json`,
40 /// then stores the useful information the json into an [XCResult] object.
41 ///
42 /// A`issueDiscarders` can be passed to discard any issues that matches the description of any [XCResultIssueDiscarder] in the list.
43 Future<XCResult> generate(
44 {List<XCResultIssueDiscarder> issueDiscarders =
45 const <XCResultIssueDiscarder>[]}) async {
46 final Version? xcodeVersion = xcode.currentVersion;
47 final RunResult result = await processUtils.run(
48 <String>[
49 ...xcode.xcrunCommand(),
50 'xcresulttool',
51 'get',
52 // See https://github.com/flutter/flutter/issues/151502
53 if (xcodeVersion != null && xcodeVersion >= Version(16, 0, 0))
54 '--legacy',
55 '--path',
56 resultPath,
57 '--format',
58 'json',
59 ],
60 );
61 if (result.exitCode != 0) {
62 return XCResult.failed(errorMessage: result.stderr);
63 }
64 if (result.stdout.isEmpty) {
65 return XCResult.failed(
66 errorMessage: 'xcresult parser: Unrecognized top level json format.');
67 }
68 final Object? resultJson = json.decode(result.stdout);
69 if (resultJson == null || resultJson is! Map<String, Object?>) {
70 // If json parsing failed, indicate such error.
71 // This also includes the top level json object is an array, which indicates
72 // the structure of the json is changed and this parser class possibly needs to update for this change.
73 return XCResult.failed(
74 errorMessage: 'xcresult parser: Unrecognized top level json format.');
75 }
76 return XCResult(resultJson: resultJson, issueDiscarders: issueDiscarders);
77 }
78}
79
80/// The xcresult of an `xcodebuild` command.
81///
82/// This is the result from an `xcrun xcresulttool get --legacy --path <resultPath> --format json` run.
83/// The result contains useful information such as build errors and warnings.
84class XCResult {
85 /// Parse the `resultJson` and stores useful information in the returned `XCResult`.
86 factory XCResult({required Map<String, Object?> resultJson, List<XCResultIssueDiscarder> issueDiscarders = const <XCResultIssueDiscarder>[]}) {
87 final List<XCResultIssue> issues = <XCResultIssue>[];
88
89 final Object? issuesMap = resultJson['issues'];
90 if (issuesMap == null || issuesMap is! Map<String, Object?>) {
91 return XCResult.failed(
92 errorMessage: 'xcresult parser: Failed to parse the issues map.');
93 }
94
95 final Object? errorSummaries = issuesMap['errorSummaries'];
96 if (errorSummaries is Map<String, Object?>) {
97 issues.addAll(_parseIssuesFromIssueSummariesJson(
98 type: XCResultIssueType.error,
99 issueSummariesJson: errorSummaries,
100 issueDiscarder: issueDiscarders,
101 ));
102 }
103
104 final Object? warningSummaries = issuesMap['warningSummaries'];
105 if (warningSummaries is Map<String, Object?>) {
106 issues.addAll(_parseIssuesFromIssueSummariesJson(
107 type: XCResultIssueType.warning,
108 issueSummariesJson: warningSummaries,
109 issueDiscarder: issueDiscarders,
110 ));
111 }
112
113 final Object? actionsMap = resultJson['actions'];
114 if (actionsMap is Map<String, Object?>) {
115 final List<XCResultIssue> actionIssues = _parseActionIssues(actionsMap, issueDiscarders: issueDiscarders);
116 issues.addAll(actionIssues);
117 }
118
119 return XCResult._(issues: issues);
120 }
121
122 factory XCResult.failed({required String errorMessage}) {
123 return XCResult._(
124 parseSuccess: false,
125 parsingErrorMessage: errorMessage,
126 );
127 }
128
129 /// Create a [XCResult] with constructed [XCResultIssue]s for testing.
130 @visibleForTesting
131 factory XCResult.test({
132 List<XCResultIssue>? issues,
133 bool? parseSuccess,
134 String? parsingErrorMessage,
135 }) {
136 return XCResult._(
137 issues: issues ?? const <XCResultIssue>[],
138 parseSuccess: parseSuccess ?? true,
139 parsingErrorMessage: parsingErrorMessage,
140 );
141 }
142
143 XCResult._({
144 this.issues = const <XCResultIssue>[],
145 this.parseSuccess = true,
146 this.parsingErrorMessage,
147 });
148
149 /// The issues in the xcresult file.
150 final List<XCResultIssue> issues;
151
152 /// Indicate if the xcresult was successfully parsed.
153 ///
154 /// See also: [parsingErrorMessage] for the error message if the parsing was unsuccessful.
155 final bool parseSuccess;
156
157 /// The error message describes why the parse if unsuccessful.
158 ///
159 /// This is `null` if [parseSuccess] is `true`.
160 final String? parsingErrorMessage;
161}
162
163/// An issue object in the XCResult
164class XCResultIssue {
165 /// Construct an `XCResultIssue` object from `issueJson`.
166 ///
167 /// `issueJson` is the object at xcresultJson[['actions']['_values'][0]['buildResult']['issues']['errorSummaries'/'warningSummaries']['_values'].
168 factory XCResultIssue({
169 required XCResultIssueType type,
170 required Map<String, Object?> issueJson,
171 }) {
172 // Parse type.
173 final Object? issueSubTypeMap = issueJson['issueType'];
174 String? subType;
175 if (issueSubTypeMap is Map<String, Object?>) {
176 final Object? subTypeValue = issueSubTypeMap['_value'];
177 if (subTypeValue is String) {
178 subType = subTypeValue;
179 }
180 }
181
182 // Parse message.
183 String? message;
184 final Object? messageMap = issueJson['message'];
185 if (messageMap is Map<String, Object?>) {
186 final Object? messageValue = messageMap['_value'];
187 if (messageValue is String) {
188 message = messageValue;
189 }
190 }
191
192 final List<String> warnings = <String>[];
193 // Parse url and convert it to a location String.
194 String? location;
195 final Object? documentLocationInCreatingWorkspaceMap =
196 issueJson['documentLocationInCreatingWorkspace'];
197 if (documentLocationInCreatingWorkspaceMap is Map<String, Object?>) {
198 final Object? urlMap = documentLocationInCreatingWorkspaceMap['url'];
199 if (urlMap is Map<String, Object?>) {
200 final Object? urlValue = urlMap['_value'];
201 if (urlValue is String) {
202 location = _convertUrlToLocationString(urlValue);
203 if (location == null) {
204 warnings.add(
205 '(XCResult) The `url` exists but it was failed to be parsed. url: $urlValue');
206 }
207 }
208 }
209 }
210
211 return XCResultIssue._(
212 type: type,
213 subType: subType,
214 message: message,
215 location: location,
216 warnings: warnings,
217 );
218 }
219
220 /// Create a [XCResultIssue] without JSON parsing for testing.
221 @visibleForTesting
222 factory XCResultIssue.test({
223 XCResultIssueType type = XCResultIssueType.error,
224 String? subType,
225 String? message,
226 String? location,
227 List<String> warnings = const <String>[],
228 }) {
229 return XCResultIssue._(
230 type: type,
231 subType: subType,
232 message: message,
233 location: location,
234 warnings: warnings,
235 );
236 }
237
238 XCResultIssue._({
239 required this.type,
240 required this.subType,
241 required this.message,
242 required this.location,
243 required this.warnings,
244 });
245
246 /// The type of the issue.
247 final XCResultIssueType type;
248
249 /// The sub type of the issue.
250 ///
251 /// This is a more detailed category about the issue.
252 /// The possible values are `Warning`, `Semantic Issue'` etc.
253 final String? subType;
254
255 /// Human readable message for the issue.
256 ///
257 /// This can be displayed to user for their information.
258 final String? message;
259
260 /// The location where the issue occurs.
261 ///
262 /// This is a re-formatted version of the "url" value in the json.
263 /// The format looks like <FileLocation>:<StartingLineNumber>:<StartingColumnNumber>.
264 final String? location;
265
266 /// Warnings when constructing the issue object.
267 final List<String> warnings;
268}
269
270/// The type of an `XCResultIssue`.
271enum XCResultIssueType {
272 /// The issue is an warning.
273 ///
274 /// This is for all the issues under the `warningSummaries` key in the xcresult.
275 warning,
276
277 /// The issue is an warning.
278 ///
279 /// This is for all the issues under the `errorSummaries` key in the xcresult.
280 error,
281}
282
283/// Discards the [XCResultIssue] that matches any of the matchers.
284class XCResultIssueDiscarder {
285 XCResultIssueDiscarder(
286 {this.typeMatcher,
287 this.subTypeMatcher,
288 this.messageMatcher,
289 this.locationMatcher})
290 : assert(typeMatcher != null ||
291 subTypeMatcher != null ||
292 messageMatcher != null ||
293 locationMatcher != null);
294
295 /// The type of the discarder.
296 ///
297 /// A [XCResultIssue] should be discarded if its `type` equals to this.
298 final XCResultIssueType? typeMatcher;
299
300 /// The subType of the discarder.
301 ///
302 /// A [XCResultIssue] should be discarded if its `subType` matches the RegExp.
303 final RegExp? subTypeMatcher;
304
305 /// The message of the discarder.
306 ///
307 /// A [XCResultIssue] should be discarded if its `message` matches the RegExp.
308 final RegExp? messageMatcher;
309
310 /// The location of the discarder.
311 ///
312 /// A [XCResultIssue] should be discarded if its `location` matches the RegExp.
313 final RegExp? locationMatcher;
314}
315
316// A typical location url string looks like file:///foo.swift#CharacterRangeLen=0&EndingColumnNumber=82&EndingLineNumber=7&StartingColumnNumber=82&StartingLineNumber=7.
317//
318// This function converts it to something like: /foo.swift::.
319String? _convertUrlToLocationString(String url) {
320 final Uri? fragmentLocation = Uri.tryParse(url);
321 if (fragmentLocation == null) {
322 return null;
323 }
324 // Parse the fragment as a query of key-values:
325 final Uri fileLocation = Uri(
326 path: fragmentLocation.path,
327 query: fragmentLocation.fragment,
328 );
329 String startingLineNumber =
330 fileLocation.queryParameters['StartingLineNumber'] ?? '';
331 if (startingLineNumber.isNotEmpty) {
332 startingLineNumber = ':$startingLineNumber';
333 }
334 String startingColumnNumber =
335 fileLocation.queryParameters['StartingColumnNumber'] ?? '';
336 if (startingColumnNumber.isNotEmpty) {
337 startingColumnNumber = ':$startingColumnNumber';
338 }
339 return '${fileLocation.path}$startingLineNumber$startingColumnNumber';
340}
341
342// Determine if an `issue` should be discarded based on the `discarder`.
343bool _shouldDiscardIssue(
344 {required XCResultIssue issue, required XCResultIssueDiscarder discarder}) {
345 if (issue.type == discarder.typeMatcher) {
346 return true;
347 }
348 if (issue.subType != null &&
349 discarder.subTypeMatcher != null &&
350 discarder.subTypeMatcher!.hasMatch(issue.subType!)) {
351 return true;
352 }
353 if (issue.message != null &&
354 discarder.messageMatcher != null &&
355 discarder.messageMatcher!.hasMatch(issue.message!)) {
356 return true;
357 }
358 if (issue.location != null &&
359 discarder.locationMatcher != null &&
360 discarder.locationMatcher!.hasMatch(issue.location!)) {
361 return true;
362 }
363
364 return false;
365}
366
367List<XCResultIssue> _parseIssuesFromIssueSummariesJson({
368 required XCResultIssueType type,
369 required Map<String, Object?> issueSummariesJson,
370 required List<XCResultIssueDiscarder> issueDiscarder,
371}) {
372 final List<XCResultIssue> issues = <XCResultIssue>[];
373 final Object? errorsList = issueSummariesJson['_values'];
374 if (errorsList is List<Object?>) {
375 for (final Object? issueJson in errorsList) {
376 if (issueJson == null || issueJson is! Map<String, Object?>) {
377 continue;
378 }
379 final XCResultIssue resultIssue = XCResultIssue(
380 type: type,
381 issueJson: issueJson,
382 );
383 bool discard = false;
384 for (final XCResultIssueDiscarder discarder in issueDiscarder) {
385 if (_shouldDiscardIssue(issue: resultIssue, discarder: discarder)) {
386 discard = true;
387 break;
388 }
389 }
390 if (discard) {
391 continue;
392 }
393 issues.add(resultIssue);
394 }
395 }
396 return issues;
397}
398
399List<XCResultIssue> _parseActionIssues(
400 Map<String, Object?> actionsMap, {
401 required List<XCResultIssueDiscarder> issueDiscarders,
402}) {
403 // Example of json:
404 // {
405 // "actions" : {
406 // "_values" : [
407 // {
408 // "actionResult" : {
409 // "_type" : {
410 // "_name" : "ActionResult"
411 // },
412 // "issues" : {
413 // "_type" : {
414 // "_name" : "ResultIssueSummaries"
415 // },
416 // "testFailureSummaries" : {
417 // "_type" : {
418 // "_name" : "Array"
419 // },
420 // "_values" : [
421 // {
422 // "_type" : {
423 // "_name" : "TestFailureIssueSummary",
424 // "_supertype" : {
425 // "_name" : "IssueSummary"
426 // }
427 // },
428 // "issueType" : {
429 // "_type" : {
430 // "_name" : "String"
431 // },
432 // "_value" : "Uncategorized"
433 // },
434 // "message" : {
435 // "_type" : {
436 // "_name" : "String"
437 // },
438 // "_value" : "Unable to find a destination matching the provided destination specifier:\n\t\t{ id:1234D567-890C-1DA2-34E5-F6789A0123C4 }\n\n\tIneligible destinations for the \"Runner\" scheme:\n\t\t{ platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device, error:iOS 17.0 is not installed. To use with Xcode, first download and install the platform }"
439 // }
440 // }
441 // ]
442 // }
443 // }
444 // }
445 // }
446 // ]
447 // }
448 // }
449 final List<XCResultIssue> issues = <XCResultIssue>[];
450 final Object? actionsValues = actionsMap['_values'];
451 if (actionsValues is! List<Object?>) {
452 return issues;
453 }
454
455 for (final Object? actionValue in actionsValues) {
456 if (actionValue is!Map<String, Object?>) {
457 continue;
458 }
459 final Object? actionResult = actionValue['actionResult'];
460 if (actionResult is! Map<String, Object?>) {
461 continue;
462 }
463 final Object? actionResultIssues = actionResult['issues'];
464 if (actionResultIssues is! Map<String, Object?>) {
465 continue;
466 }
467 final Object? testFailureSummaries = actionResultIssues['testFailureSummaries'];
468 if (testFailureSummaries is Map<String, Object?>) {
469 issues.addAll(_parseIssuesFromIssueSummariesJson(
470 type: XCResultIssueType.error,
471 issueSummariesJson: testFailureSummaries,
472 issueDiscarder: issueDiscarders,
473 ));
474 }
475 }
476
477 return issues;
478 }
479

Provided by KDAB

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