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