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

Provided by KDAB

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