File: InfoPlistProcessorTaskAction.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (1693 lines) | stat: -rw-r--r-- 86,414 bytes parent folder | download
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
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

public import SWBUtil
import SWBLibc
public import SWBCore
import SWBMacro
import struct Foundation.CharacterSet
import struct Foundation.Data
import class Foundation.PropertyListSerialization
import class Foundation.NSError

/// Concrete implementation of the Info.plist processor in-process task.
public final class InfoPlistProcessorTaskAction: TaskAction
{
    public override class var toolIdentifier: String
    {
        return "info-plist-processor"
    }

    let contextPath: Path

    // Cached properties of this task parsed from its configuration.
    private var configParsingResult: CommandResult? = nil
    private var configParsingMessages: TaskActionMessageCollection? = nil
    private var productTypeIdentifier: String? = nil
    private var inputPath: Path? = nil
    private var outputPath: Path? = nil
    private var pkgInfoPath: Path? = nil
    private var platformName: String? = nil
    private var enforceMinimumOS: Bool = false
    private var expandBuildSettings: Bool = false
    private var resourceRulesFileName: Path? = nil
    private var additionalContentFilePaths = [Path]()
    private var privacyFileContentFilePaths = [Path]()
    private var requiredArchitecture: String? = nil
    private var outputFormat: PropertyListSerialization.PropertyListFormat? = nil

    public init(_ contextPath: Path)
    {
        self.contextPath = contextPath
        super.init()
    }

    override public func performTaskAction(
        _ task: any ExecutableTask,
        dynamicExecutionDelegate: any DynamicTaskExecutionDelegate,
        executionDelegate: any TaskExecutionDelegate,
        clientDelegate: any TaskExecutionClientDelegate,
        outputDelegate: any TaskOutputDelegate
    ) async -> CommandResult {
        // Deserialize the context attachment. We discard this at the end of `performTaskAction` because it is particularly heavyweight.
        let context: InfoPlistProcessorTaskActionContext
        do {
            struct ContextDeserializerDelegate: InfoPlistProcessorTaskActionContextDeserializerDelegate {
                var namespace: SWBMacro.MacroNamespace
                let sdkRegistry: SDKRegistry
                let specRegistry: SpecRegistry
                let platformRegistry: PlatformRegistry
            }
            let deserializer = MsgPackDeserializer(try executionDelegate.fs.read(contextPath), delegate: ContextDeserializerDelegate(namespace: executionDelegate.namespace, sdkRegistry: executionDelegate.sdkRegistry, specRegistry: executionDelegate.specRegistry, platformRegistry: executionDelegate.platformRegistry))
            context = try deserializer.deserialize()
        } catch {
            outputDelegate.emitError("failed to deserialize Info.plist task context: \(error.localizedDescription)")
            return .failed
        }
        let scope = context.scope
        let productType = context.productType
        let platform = context.platform
        // Parse the arguments and cache them.  We only have to do this the first time the task is run, since the task won't change from run to run, and it cannot be run multiple times in parallel in the same process.
        if configParsingResult == nil
        {
            // The compiler won't let us just do a tuple assignment of the method return value here.  c.f <rdar://problem/24317727>
            let (result, messages) = parseConfiguration(task, context: context)
            configParsingResult = result
            configParsingMessages = messages
        }
        // We do, however, have to emit the messages and handle a bad exit code on each run.
        configParsingMessages?.emitMessages(outputDelegate)
        if configParsingResult! != .succeeded
        {
            return configParsingResult!
        }

        guard task.workingDirectory.isAbsolute else {
            outputDelegate.emitError("task working directory \(task.workingDirectory) is not an absolute path")
            return .failed
        }

        // Perform some correctness checks and make the paths absolute, if needed.
        guard let inputPath = self.inputPath?.makeAbsolute(relativeTo: task.workingDirectory) else {
            outputDelegate.emitError("no input file specified")
            return .failed
        }

        guard let outputPath = self.outputPath?.makeAbsolute(relativeTo: task.workingDirectory) else {
            outputDelegate.emitError("no output file specified")
            return .failed
        }

        // Read the property list from the file.  We assume this method will emit any appropriate messages during reading.
        // We will modify the plist in place throughout the course of the tool until we finally have the contents we want to write out.
        let contentsData: ByteString
        do
        {
            contentsData = try executionDelegate.fs.read(inputPath)
        }
        catch let e
        {
            outputDelegate.emitError("unable to read input file '\(inputPath.str)': \(e.localizedDescription)")
            return .failed
        }
        var (plist, inputFormat): (PropertyListItem, PropertyListSerialization.PropertyListFormat)
        do
        {
            (plist, inputFormat) = try PropertyList.fromBytesWithFormat(contentsData.bytes)
        }
        catch let error as NSError
        {
            outputDelegate.emitError("unable to read property list from file: \(inputPath.str): \(error.localizedDescription)")
            return .failed
        }
        catch
        {
            outputDelegate.emitError("unable to read property list from file: \(inputPath.str): unknown error")
            return .failed
        }


        // Confirm that the property list is a dictionary.
        guard case let .plDict(plistDict) = plist else { outputDelegate.emitError("contents of file is not a dictionary: \(inputPath.str)"); return .failed }

        // Apply INFOPLIST_FILE_CONTENTS
        if let infoPlistFileContents = scope.evaluate(BuiltinMacros.INFOPLIST_FILE_CONTENTS).nilIfEmpty {
            do {
                let infoPlistFileContentsPropertyListItem = try PropertyList.fromString(infoPlistFileContents)
                guard case let .plDict(infoPlistFileContentsDict) = infoPlistFileContentsPropertyListItem else {
                    outputDelegate.emitError("contents of INFOPLIST_FILE_CONTENTS is not a dictionary")
                    return .failed
                }
                plist = .plDict(plistDict.addingContents(of: infoPlistFileContentsDict))
            }
            catch {
                outputDelegate.emitError("unable to create property list from string '\(infoPlistFileContents)': \(error.localizedDescription)")
                return .failed
            }
        }


        // Load the additional content files, if any, and merge their content into the input plist.
        for path in additionalContentFilePaths
        {
            let additionalContentResult = addAdditionalContent(from: path, &plist, context: context, executionDelegate, outputDelegate)
            guard additionalContentResult == .succeeded else
            {
                // addAdditionalContent() will have emitted any issues as it executed.
                return additionalContentResult
            }
        }

        for path in privacyFileContentFilePaths
        {
            if let privacyFile = scanForPrivacyFile(at: path, fs: executionDelegate.fs) {
                let additionalPrivacyContentResult = addAppPrivacyContent(from: privacyFile, &plist, executionDelegate, outputDelegate)
                guard additionalPrivacyContentResult == .succeeded else
                {
                    // addAppPrivacyContent() will have emitted any issues as it executed.
                    return additionalPrivacyContentResult
                }
            }
        }

        // If we were passed any required architecture, then we apply it to the value of the 'UIRequiredDeviceCapabilities' key, by setting it and removing any others.
        if requiredArchitecture != nil
        {
            let result = setRequiredDeviceCapabilities(&plist, context: context, outputDelegate: outputDelegate)
            if result != .succeeded
            {
                return result
            }
        }

        // Add the data from the additional info property in the platform to the plist.
        let additionalInfoResult = addAdditionalEntriesFromPlatform(&plist, context: context, outputDelegate: outputDelegate)
        if additionalInfoResult != .succeeded
        {
            // addAdditionalEntriesFromPlatform() will have emitted any issues as it executed.
            return additionalInfoResult
        }

        // Expand build settings in the property list if requested.  We only expand the values, not the keys.
        if expandBuildSettings {
            if SWBFeatureFlag.enableDefaultInfoPlistTemplateKeys.value || scope.evaluate(BuiltinMacros.GENERATE_INFOPLIST_FILE) {
                let plistDefaults = defaultInfoPlistContent(scope: scope, platform: platform, productType: productType)
                do {
                    let result = try withTemporaryDirectory(fs: executionDelegate.fs) { plistDefaultsDir -> CommandResult in
                        let plistDefaultsPath = plistDefaultsDir.join("DefaultInfoPlistContent.plist")
                        try executionDelegate.fs.write(plistDefaultsPath, contents: ByteString(PropertyListItem.plDict(plistDefaults).asBytes(.binary)))
                        return addAdditionalContent(from: plistDefaultsPath, &plist, context: context, executionDelegate, outputDelegate)
                    }

                    if result != .succeeded {
                        return result
                    }
                } catch {
                    outputDelegate.emitError("\(error)")
                    return .failed
                }
            }

            // Define overriding build settings for when we evaluate build settings in the plist.
            let lookupClosure: ((MacroDeclaration) -> MacroExpression?)?
            if case .plString(let cfBundleIdentifier)? = plist.dictValue?["CFBundleIdentifier"] {
                let cfBundleIdentifierExpr = scope.namespace.parseString(cfBundleIdentifier)
                lookupClosure = { return $0 == BuiltinMacros.CFBundleIdentifier ? cfBundleIdentifierExpr : nil }
            } else {
                lookupClosure = nil
            }

            plist = plist.byEvaluatingMacros(withScope: scope, lookup: lookupClosure)
        }

        // Elide any empty string values for a specific set of keys (bad things happen in the system if an Info.plist has an empty value for any of these keys).
        plist = plist.byElidingRecursivelyEmptyStringValuesInDictionaries(Set<String>([
            "CFBundleDevelopmentRegion",
            "CFBundleExecutable",
            "CFBundleGetInfoString",
            "CFBundleIconFile",
            "CFBundleIdentifier",
            "CFBundleName",
            "CFBundlePackageType",
            "CFBundleResourceSpecification",
            "CFBundleShortVersionString",
            "CFBundleSignature",
            "CFBundleTypeIconFile",
            "CFBundleTypeRole",
            "CFBundleVersion",
            "NSDocumentClass",
            "NSHelpFile",
            "NSHumanReadableCopyright",
            "NSMainNibFile",
            "NSPrincipalClass",
            "NSStickerSharingLevel",

            // These aren't harmful to the system if they're empty, but will cause App Store submission to fail, so elide them as well.
            "BuildMachineOSBuild",
            "DTSwiftPlaygroundsBuild",
            "DTSwiftPlaygroundsVersion",
            "DTXcode",
            "DTXcodeBuild",
        ]))

        // Convert the PropertyListItem to a Dictionary so we can inject content
        guard case .plDict(var plistDict) = plist else {
            outputDelegate.emitError("Content of file '\(inputPath.str)' is not a dictionary")
            return .failed
        }

        // Determine the build platform we're targeting.  Note that this could resolve to nil.
        let buildPlatform = context.sdk?.targetBuildVersionPlatform(sdkVariant: context.sdkVariant)

        // Compute the deployment target macro name and effective deployment target.
        // Note that this will resolve to MACOSX_DEPLOYMENT_TARGET for Mac Catalyst, because it's the macOS (not iOS) deployment target which is relevant for the purposes of Info.plist processing.  If at some point Info.plist processing needs to consider both deployment targets for Catalyst, then this will need refinement.
        let deploymentTarget: Version?
        if let deploymentTargetMacro = platform?.deploymentTargetMacro {
            deploymentTarget = try? Version(scope.evaluate(deploymentTargetMacro))
        }
        else {
            deploymentTarget = nil
        }

        // Merge content about the targeted device family into the Info.plist.
        plist = mergeUIDeviceFamilyContent(into: &plistDict, context: context, executionDelegate, outputDelegate)

        // Remove or add entries to the plist based on which platform is being targeted.  This is primarily to deal with the advent of macCatalyst, where iOS targets can be built for either iOS or macCatalyst, but it could be expanded in the future.
        let disableInfoPlistPlatformEditing = scope.evaluate(BuiltinMacros.DISABLE_INFOPLIST_PLATFORM_PROCESSING)
        if !disableInfoPlistPlatformEditing {
            plist = editPlatformSpecificEntries(in: &plistDict, outputDelegate, context: context)
        }

        if scope.evaluate(BuiltinMacros.ENABLE_THREAD_SANITIZER) && productType?.requiresStrictInfoPlistKeys(scope) != true {
            plistDict["NSBuiltWithThreadSanitizer"] = .plBool(true)
            plist = .plDict(plistDict)
        }

        // Shallow bundles must have a resource specification file (c.f. <rdar://problem/5799827>, so we add it to the additional info.
        if let resourceRulesFileName {
            plistDict["CFBundleResourceSpecification"] = .plString(resourceRulesFileName.split().1)
            plist = .plDict(plistDict)
        }

        // If we are asked to enforce the minimum OS in the Info.plist, do so. c.f. <rdar://problem/30538173>
        if enforceMinimumOS && plistDict["LSMinimumSystemVersion"] == nil {
            if let packageType = plistDict["CFBundlePackageType"], packageType == .plString("APPL") {
                if let deploymentTarget = deploymentTarget {
                    plistDict["LSMinimumSystemVersion"] = .plString(deploymentTarget.description)
                    plist = .plDict(plistDict)
                }
            }
        }

        // Codeless bundles specify a list of their statically known clients as `DTBundleClientLibraries`.
        if !context.clientLibrariesForCodelessBundle.isEmpty, scope.evaluate(BuiltinMacros.ENABLE_SDK_IMPORTS) {
            plistDict["DTBundleClientLibraries"] = .plArray(context.clientLibrariesForCodelessBundle.map { .plString($0) })
            plist = .plDict(plistDict)
        }

        // LSSupportsOpeningDocumentsInPlace has some special checks:
        // - When building for iOS, warn if it doesn't declare whether it supports either open-in-place or document browsing (see <rdar://problem/41161290>).
        // - When building for macOS, error if it sets 'LSSupportsOpeningDocumentsInPlace = NO', as that mode is not supported on macOS (see <rdar://problem/46390792>.
        //   (We can't just force it to YES because it's a declaration of behavior, so the app could still do something very bad on macOS no matter what the entry is.)
        // LSSupportsOpeningDocumentsInPlace is only relevant when document types are also defined.
        // This key is being deprecated on all platforms so all of this logic might be removed someday.
        let numDocumentTypes = plistDict["CFBundleDocumentTypes"]?.arrayValue?.count ?? 0
        if platform?.familyName == "iOS" {
            if numDocumentTypes > 0, plistDict["LSSupportsOpeningDocumentsInPlace"] == nil, plistDict["UISupportsDocumentBrowser"] == nil {
                outputDelegate.emitWarning("The application supports opening files, but doesn't declare whether it supports opening them in place. You can add an LSSupportsOpeningDocumentsInPlace entry or an UISupportsDocumentBrowser entry to your Info.plist to declare support.")
            }
        }
        else if platform?.familyName == "macOS", !disableInfoPlistPlatformEditing {
            if numDocumentTypes > 0, let sodip = plistDict["LSSupportsOpeningDocumentsInPlace"], sodip.boolValue == false {
                outputDelegate.emitError("'LSSupportsOpeningDocumentsInPlace = NO' is not supported on macOS. Either remove the entry or set it to YES, and also ensure that the application does open documents in place on macOS.")
                return .failed
            }
        }

        // Validate the contents of the plist and emit warnings and errors as necessary.  This may make some simple edits to the plist.
        if !validatePlistContents(&plistDict, buildPlatform, context.targetBuildVersionPlatforms, deploymentTarget, scope, infoLookup: executionDelegate.infoLookup, context: context, outputDelegate: outputDelegate) {
            return .failed
        }

        plist = .plDict(plistDict)

        let bytes: [UInt8]
        do {
            // Generate output data in the requested format (defaulting to the same format as the input file, if no output format has been specified).
            let effectiveOutputFormat = outputFormat ?? inputFormat

            // Foundation has not supported writing OpenStep format since Leopard (c.f. <rdar://5882332>), so in that case we default to XML format.
            bytes = try plist.asBytes(effectiveOutputFormat == .openStep ? .xml : effectiveOutputFormat)
        } catch {
            outputDelegate.emitError("unable to convert property list to output format, error: \(error.localizedDescription)")
            return .failed
        }

        do {
            _ = try executionDelegate.fs.writeIfChanged(outputPath, contents: ByteString(bytes))
        } catch {
            outputDelegate.emitError("unable to write file '\(outputPath.str)': \(error.localizedDescription)")
            return .failed
        }

        // Also generate a PkgInfo file, if we've been asked to do so.
        if let pkgInfoPath = self.pkgInfoPath?.makeAbsolute(relativeTo: task.workingDirectory) {
            do {
                try generatePackageInfo(atPath: pkgInfoPath, usingPlist: plistDict, context: context, executionDelegate, outputDelegate)
            } catch {
                return .failed
            }
        }

        return .succeeded
    }


    // MARK: Parsing the configuration


    private func parseConfiguration(_ task: any ExecutableTask, context: InfoPlistProcessorTaskActionContext) -> (CommandResult, TaskActionMessageCollection)
    {
        let messages = TaskActionMessageCollection()
        let commandLine = [String](task.commandLineAsStrings)

        var i = 1                     // Skip over the tool name
        while i < commandLine.count
        {
            let option = commandLine[i]
            switch option
            {
            case "-format":
                // The '-format' option takes a single argument: 'openstep', 'xml', or 'binary'.
                if commandLine.count > i+1
                {
                    // The argument is the format in which to write the output.
                    i += 1
                    let arg = commandLine[i]
                    if arg == "openstep"
                    {
                        outputFormat = .openStep
                    }
                    else if arg == "xml"
                    {
                        outputFormat = .xml
                    }
                    else if arg == "binary"
                    {
                        outputFormat = .binary
                    }
                    else
                    {
                        // Unrecognized argument to -format.
                        messages.addMessage(.error("unrecognized argument to \(option): '\(arg)' (use 'openstep', 'xml', or 'binary')"))
                        return (.failed, messages)
                    }
                }
                else
                {
                    // No argument to -format.
                    messages.addMessage(.error("missing argument for \(option) (use 'openstep', 'xml', or 'binary')"))
                    return (.failed, messages)
                }

            case "-genpkginfo":
                // The '-pkginfo' option takes a single argument: the path of the pkginfo file to generate.
                if commandLine.count > i+1
                {
                    // The argument is the output path.
                    i += 1
                    let arg = commandLine[i]
                    if pkgInfoPath == nil
                    {
                        // We don't already have a pkginfo path; we do now.
                        pkgInfoPath = Path(arg)
                    }
                    else
                    {
                        // We already have a pkginfo path.
                        messages.addMessage(.error("multiple pkginfo paths specified"))
                        return (.failed, messages)
                    }
                }
                else
                {
                    // No argument to -genpkginfo.
                    messages.addMessage(.error("missing argument for \(option)"))
                    return (.failed, messages)
                }

            case "-producttype":
                if commandLine.count > i + 1 {
                    i += 1
                    let arg = commandLine[i]
                    if productTypeIdentifier == nil {
                        productTypeIdentifier = arg
                        if let productType = context.productType, arg != productType.identifier {
                            messages.addMessage(.error("argument to \(option) '\(arg)' differs from product type being built '\(productType.name)'"))
                            return (.failed, messages)
                        }
                    } else {
                        messages.addMessage(.error("multiple product types specified"))
                        return (.failed, messages)
                    }
                } else {
                    messages.addMessage(.error("missing argument for \(option)"))
                    return (.failed, messages)
                }

            case "-platform":
                // The '-platform' option takes a single argument: the name of the platform for which the Info.plist is being generated.
                if commandLine.count > i+1
                {
                    // The argument is the platform name.
                    i += 1
                    let arg = commandLine[i]
                    if platformName == nil
                    {
                        // We don't already have a platform name; we do now.
                        platformName = arg

                        // If the platform name we were passed differs from the platform we were configured with, then emit an error.
                        if platformName! != context.platform?.name
                        {
                            messages.addMessage(.error("argument to -platform '\(platformName!)' differs from platform being targeted '\(context.platform?.name ?? "")'"))
                            return (.failed, messages)
                        }
                    }
                    else
                    {
                        // We already have a pkginfo path.
                        messages.addMessage(.error("multiple platform names specified"))
                        return (.failed, messages)
                    }
                }
                else
                {
                    // No argument to -platform.
                    messages.addMessage(.error("missing argument for \(option)"))
                    return (.failed, messages)
                }

            case "-enforceminimumos":
                // The '-enforceminimumos' option takes no arguments.
                enforceMinimumOS = true

            case "-expandbuildsettings":
                // The '-expandbuildsettings' option takes no arguments.
                expandBuildSettings = true

            case "-resourcerulesfile":
                // The '-resourcerulesfile' option takes a single argument: the name (or path) of the resource rules file.
                if commandLine.count > i+1
                {
                    // The argument is the resource rules file.
                    i += 1
                    let arg = commandLine[i]
                    if resourceRulesFileName == nil
                    {
                        // We don't already have a resource rules file; we do now.
                        resourceRulesFileName = Path(arg)
                    }
                    else
                    {
                        // We already have a pkginfo path.
                        messages.addMessage(.error("multiple resource rules files specified"))
                        return (.failed, messages)
                    }
                }
                else
                {
                    // No argument to -resourcerulesfile.
                    messages.addMessage(.error("missing argument for \(option)"))
                    return (.failed, messages)
                }

            case "-additionalcontentfile":
                // The '-additionalcontentfile' option takes a single argument: the name of the additional content file.  There may be more than one such file passed.
                if commandLine.count > i+1
                {
                    // The argument is the additional content file.
                    i += 1
                    let arg = commandLine[i]
                    additionalContentFilePaths.append(Path(arg))
                }
                else
                {
                    // No argument to -additionalcontentfile.
                    messages.addMessage(.error("missing argument for \(option)"))
                    return (.failed, messages)
                }

            case "-scanforprivacyfile":
                // The '-scanforprivacyfile' option takes a single argument: the path of the path to scan for an `PrivacyInfo.xcprivacy` file.  There may be more than one such path passed.
                if commandLine.count > i+1
                {
                    i += 1
                    let arg = commandLine[i]
                    privacyFileContentFilePaths.append(Path(arg))
                }
                else
                {
                    // No argument to -scanforprivacyfile.
                    messages.addMessage(.error("missing argument for \(option)"))
                    return (.failed, messages)
                }

            case "-requiredArchitecture":
                // The 'requiredArchitecture' option takes a single argument: An architecture to use to populate the 'UIRequiredDeviceCapabilities' key

                guard requiredArchitecture == nil else {
                    messages.addMessage(.error("multiple -requiredArchitecture specified"))
                    return (.failed, messages)
                }

                guard commandLine.count > i+1 else {
                    messages.addMessage(.error("missing argument for \(option)"))
                    return (.failed, messages)
                }

                // The argument is the additional content file.
                i += 1
                let arg = commandLine[i]
                requiredArchitecture = arg

            case "-o":
                // The '-o' option takes a single argument: the output path.
                if commandLine.count > i+1
                {
                    // The argument is the resource rules file.
                    i += 1
                    let arg = commandLine[i]
                    if outputPath == nil
                    {
                        // We don't already have a resource rules file; we do now.
                        outputPath = Path(arg)
                    }
                    else
                    {
                        // We already have a pkginfo path.
                        messages.addMessage(.error("multiple output files specified"))
                        return (.failed, messages)
                    }
                }
                else
                {
                    // No argument to -o.
                    messages.addMessage(.error("missing argument for \(option)"))
                    return (.failed, messages)
                }

            default:
                if option.hasPrefix("-")
                {
                    // Unknown option.
                    messages.addMessage(.error("unrecognized option: \(option)"))
                    return (.failed, messages)
                }
                else if inputPath == nil
                {
                    // If we don't already have an input path, then we use this as the input path.
                    inputPath = Path(option)
                }
                else
                {
                    // It's an input path and we already have one; that's not allowed.
                    messages.addMessage(.error("multiple input files specified"))
                    return (.failed, messages)
                }
            }

            i += 1
        }

        return (.succeeded, messages)
    }

    private let usageDescriptionStringKeys: Set<String> = [
        "NFCReaderUsageDescription",
        "NSAccessoryTrackingUsageDescription",
        "NSAppDataUsageDescription",
        "NSAppleEventsUsageDescription",
        "NSAppleMusicUsageDescription",
        "NSBluetoothAlwaysUsageDescription",
        "NSBluetoothPeripheralUsageDescription",
        "NSBluetoothWhileInUseUsageDescription",
        "NSCalendarsUsageDescription",
        "NSCalendarsFullAccessUsageDescription",
        "NSCalendarsWriteOnlyAccessUsageDescription",
        "NSCameraUsageDescription",
        "NSContactsUsageDescription",
        "NSCriticalMessagingUsageDescription",
        "NSDesktopFolderUsageDescription",
        "NSDocumentsFolderUsageDescription",
        "NSDownloadsFolderUsageDescription",
        "NSFaceIDUsageDescription",
        "NSFallDetectionUsageDescription",
        "NSFileProviderDomainUsageDescription",
        "NSFileProviderPresenceUsageDescription",
        "NSFinancialDataUsageDescription",
        "NSFocusStatusUsageDescription",
        "NSGKFriendListUsageDescription",
        "NSHandsTrackingUsageDescription",
        "NSHealthClinicalHealthRecordsShareUsageDescription",
        "NSHealthShareUsageDescription",
        "NSHealthUpdateUsageDescription",
        "NSHomeKitUsageDescription",
        "NSIdentityUsageDescription",
        "NSLocalNetworkUsageDescription",
        "NSLocationAlwaysAndWhenInUseUsageDescription",
        "NSLocationAlwaysUsageDescription",
        "NSLocationUsageDescription",
        "NSLocationWhenInUseUsageDescription",
        "NSMicrophoneUsageDescription",
        "NSMotionUsageDescription",
        "NSNearbyInteractionAllowOnceUsageDescription",
        "NSNearbyInteractionUsageDescription",
        "NSNetworkVolumesUsageDescription",
        "NSPhotoLibraryAddUsageDescription",
        "NSPhotoLibraryUsageDescription",
        "NSRemindersUsageDescription",
        "NSRemindersFullAccessUsageDescription",
        "NSRemovableVolumesUsageDescription",
        "NSSensorKitUsageDescription",
        "NSSiriUsageDescription",
        "NSSpeechRecognitionUsageDescription",
        "NSSystemAdministrationUsageDescription",
        "NSSystemExtensionUsageDescription",
        "NSUserTrackingUsageDescription",
        "NSVideoSubscriberAccountUsageDescription",
        "NSVoIPUsageDescription",
        "NSWorldSensingUsageDescription",
        "OSBundleUsageDescription",
    ]

    /// Produce a collection of default Info.plist keys based on content in the project, taking platform and product type into account.
    /// Could be done through InfoPlistAdditions properties in product types, but that would result in more duplication at this point
    private func defaultInfoPlistContent(scope: MacroEvaluationScope, platform: Platform?, productType: ProductTypeSpec?) -> [String: PropertyListItem]
    {
        var content: [String: PropertyListItem] = [:]

        // The general theory of this method is that we now have a bunch of INFOPLIST_KEY-prefixed build settings that are used to define sufficiently simple Info.plist content, and we go to those (which will either have appropriate backstops set as necessary, or be empty) to determine the content to generate.

        /// Get the macro name corresponding to the given Info.plist key name.
        /// This accommodates platform-specific override names like UISomething~ipad, which may use proper capitalization in the build setting.
        func macroNameForInfoPlistKey(key: String, prefix: String) -> String {
            let replacements = [ "~iphone": "_iPhone",
                                 "~ipad": "_iPad" ]
            var adjustedKey = key
            for (infoPlistPart, macroNamePart) in replacements {
                adjustedKey = adjustedKey.replacingOccurrences(of: infoPlistPart, with: macroNamePart)
            }
            return prefix + adjustedKey
        }

        /// Gets the value of the given setting only if it's actually set somewhere or has a non-empty default value.
        /// This is necessary because some Info.plist settings can be valid with an empty value assigned.
        func valueForInfoPlistKeyMacro(_ name: String, prefix: String = "INFOPLIST_KEY_") -> PropertyListItem? {
            let infoPlistMacroName = macroNameForInfoPlistKey(key: name, prefix: prefix)
            guard let macro = BuiltinMacros.namespace.lookupMacroDeclaration(infoPlistMacroName) else {
                return nil
            }

            let isAssigned = scope.table.contains(macro)
            let isEmpty = scope.evaluateAsString(macro).isEmpty

            guard isAssigned || !isEmpty else {
                return nil
            }

            return macro.propertyListValue(in: scope)
        }

        /// Updates the content to have the given Info.plist key macro's value, if such a value is actually set.
        /// This is necessary because some Info.plist settings are valid with an empty value assigned by the developer.
        func updateContentWithInfoPlistKeyMacroValue(_ content: inout [String: PropertyListItem], _ name: String) -> Void {
            if let value = valueForInfoPlistKeyMacro(name) {
                content[name] = value
            }
        }

        // Note that for flexibility, instead of rigidly assigning only some values for some platforms, we (mostly) rely on the templates to specify the right values for the platforms involved, and to leave values they don't care about unspecified.

        let generatedInfoPlistKeys: [String] = [
            // General

            "CFBundleDisplayName",
            "LSApplicationCategoryType",
            "NSHumanReadableCopyright",
            "NSPrincipalClass",
            "ITSAppUsesNonExemptEncryption",
            "ITSEncryptionExportComplianceCode",
            "NSLocationTemporaryUsageDescriptionDictionary",

            // macOS

            "LSBackgroundOnly",
            "LSUIElement",
            "NSMainNibFile",
            "NSMainStoryboardFile",

            // iOS and Derived

            "UILaunchStoryboardName",
            "UIMainStoryboardFile",
            "UIRequiredDeviceCapabilities",
            "UISupportedInterfaceOrientations",
            "UIUserInterfaceStyle",

            // iOS

            "LSSupportsOpeningDocumentsInPlace",
            "NSSensorKitPrivacyPolicyURL",
            "NSSupportsLiveActivities",
            "NSSupportsLiveActivitiesFrequentUpdates",
            "UIApplicationSupportsIndirectInputEvents",
            "UIRequiresFullScreen",
            "UIStatusBarHidden",
            "UIStatusBarStyle",
            "UISupportedInterfaceOrientations~ipad",
            "UISupportedInterfaceOrientations~iphone",
            "UISupportsDocumentBrowser",

            // watchOS

            "CLKComplicationPrincipalClass",
            "WKApplication",
            "WKCompanionAppBundleIdentifier",
            "WKExtensionDelegateClassName",
            "WKRunsIndependentlyOfCompanionApp",
            "WKWatchOnly",
            "WKSupportsLiveActivityLaunchAttributeTypes",

            // Metal

            "MetalCaptureEnabled",

            // Game Controller and Game Mode

            "GCSupportsControllerUserInteraction",
            "GCSupportsGameMode",

            // Sticker Packs

            "NSStickerSharingLevel",
        ] + usageDescriptionStringKeys

        for key in generatedInfoPlistKeys {
            updateContentWithInfoPlistKeyMacroValue(&content, key)
        }

        // General - Special Cases

        func updateContent(_ content: inout [String: PropertyListItem], key: String, buildSetting: String) -> Void {
            if let value = valueForInfoPlistKeyMacro(buildSetting, prefix: "") {
                content[key] = value
            }
        }

        // Only add the default keys if the corresponding build setting is defined, so that we don't overwrite static values for these keys which may be in the input Info.plist
        updateContent(&content, key: "CFBundleName", buildSetting: BuiltinMacros.PRODUCT_NAME.name)
        // <rdar://64456434> This should only be added if we will actually produce an executable
        updateContent(&content, key: "CFBundleExecutable", buildSetting: BuiltinMacros.EXECUTABLE_NAME.name)
        updateContent(&content, key: "CFBundleVersion", buildSetting: BuiltinMacros.CURRENT_PROJECT_VERSION.name)
        updateContent(&content, key: "CFBundleDevelopmentRegion", buildSetting: BuiltinMacros.DEVELOPMENT_LANGUAGE.name)
        updateContent(&content, key: "CFBundleIdentifier", buildSetting: BuiltinMacros.PRODUCT_BUNDLE_IDENTIFIER.name)
        updateContent(&content, key: "CFBundlePackageType", buildSetting: BuiltinMacros.PRODUCT_BUNDLE_PACKAGE_TYPE.name)
        updateContent(&content, key: "CFBundleShortVersionString", buildSetting: BuiltinMacros.MARKETING_VERSION.name)

        content["CFBundleInfoDictionaryVersion"] = .plString("6.0") // TODO: consider removing this since it likely isn't needed

        // iOS and Derived Platforms - Special Cases

        // This is one exception to the rule above about letting the templates specify values.
        if ["iOS", "tvOS"].contains(platform?.familyName ?? "") && productType is ApplicationProductTypeSpec {
            // According to rdar://47870882, watchOS should have this key as well, but for now
            // we'll avoid adding it for consistency with the new project templates in Xcode and to reduce the
            // risk that it breaks watchOS app installation on older versions of watchOS.
            content["LSRequiresIPhoneOS"] = .plBool(true)
        }

        // Allow iOS apps containing extensionless watchOS apps to be installed on iPads running versions of iPadOS affected by rdar://92164048
        if ["watchOS"].contains(platform?.familyName ?? "") && productType is ApplicationProductTypeSpec && productType?.conformsTo(identifier: "com.apple.product-type.application.watchapp2") == false {
            content["MinimumOSVersion~ipad"] = .plString("9.0")
        }

        if let launchScreenGenerationValue = valueForInfoPlistKeyMacro("UILaunchScreen_Generation") {
            if launchScreenGenerationValue == .plBool(true) {
                content["UILaunchScreen"] = .plDict(["UILaunchScreen": .plDict([:])])
            }
        }

        // iOS - Special Cases

        if let applicationSceneManifestGenerationValue = valueForInfoPlistKeyMacro("UIApplicationSceneManifest_Generation") {
            if applicationSceneManifestGenerationValue == .plBool(true) {
                // This used to just generate `{ UIApplicationSupportsMultipleScenes = YES; }`, but SwiftUI still needs a UISceneConfigurations key to be added too, even if its value is just an empty dictionary. See <rdar://101916343> and related.
                content["UIApplicationSceneManifest"] = .plDict([
                    "UIApplicationSupportsMultipleScenes": .plBool(true),
                    "UISceneConfigurations": .plDict([:]),
                ])
            }
        }

        // watchOS - Special Cases

        if platform?.familyName == "watchOS" && productType?.identifier == "com.apple.product-type.application.watchapp2" {
            content["WKWatchKitApp"] = .plBool(true)
        }

        return content
    }

    /// Add the keys from the `AdditionalInfo` dictionary in the platform to the Info.plist.  Only keys which do not exist in the input plist will be added.  This will modify the  input `plist` in place.
    private func addAdditionalEntriesFromPlatform(_ plist: inout PropertyListItem, context: InfoPlistProcessorTaskActionContext, outputDelegate: any TaskOutputDelegate) -> CommandResult
    {
        // If we were passed a platform with -platform, then we use its additional info.
        if platformName != nil
        {
            // Only do anything if the platform's additional info is not empty.
            if context.platform?.additionalInfoPlistEntries.count > 0
            {
                guard case .plDict(let dict) = plist else { outputDelegate.emitError("Info.plist contents are not a dictionary prior to adding additional entries from platform"); return .failed }
                var plistDict = dict

                // Go through the additional entries from the platform and add them to the plist dictionary.  But only if they're not already present in the plist.
                for (key, plValue) in context.platform?.additionalInfoPlistEntries ?? [:] where plistDict[key] == nil
                {
                    if case .plString(let value) = plValue
                    {
                        // In the past, the <some string> convention was used to indicate that the value for this key should be the value in the platform's properties.  It looks like no platform circa Xcode 9.0 uses this convention anymore, so we no longer support it and we warn if we find such a key - we should encourage platforms to use actual build setting evaluation instead.
                        if value.hasPrefix("<")  &&  value.hasSuffix(">")  &&  value.count > 2
                        {
                            outputDelegate.emitWarning("key '\(key)' in 'AdditionalInfo' dictionary for platform \(context.platform?.displayName ?? "") uses unsupported bracket evaluation convention for its value '\(value)'")
                            plistDict[key] = plValue
                        }
                        else
                        {
                            // String values which aren't surrounded by <>.
                            plistDict[key] = plValue
                        }
                    }
                    else
                    {
                        // Non-string values.
                        plistDict[key] = plValue
                    }
                }

                // Create a new PropertyListItem to send back out.
                plist = .plDict(plistDict)
            }
        }
        return .succeeded
    }

    /// Add the properties from the additional content file to the plist.  This will modify the plist in place.
    private func addAdditionalContent(from path: Path, _ plist: inout PropertyListItem, context: InfoPlistProcessorTaskActionContext, _ executionDelegate: any TaskExecutionDelegate, _ outputDelegate: any TaskOutputDelegate) -> CommandResult {
        // Read the additional content file and parse it as a property list.
        let contentsData: ByteString
        do {
            contentsData = try executionDelegate.fs.read(path)
        }
        catch {
            outputDelegate.emitError("unable to read additional content file '\(path.str)': \(error.localizedDescription)")
            return .failed
        }
        let additionalPlist: PropertyListItem
        do {
            (additionalPlist, _) = try PropertyList.fromBytesWithFormat(contentsData.bytes)
        }
        catch let error as NSError {
            outputDelegate.emitError("unable to read property list from additional content file: \(path.str): \(error.localizedDescription)")
            return .failed
        }
        catch {
            outputDelegate.emitError("unable to read property list from additional content file: \(path.str): unknown error")
            return .failed
        }
        guard case .plDict(let additionalDict) = additionalPlist else {
            outputDelegate.emitError("additional content file is not a dictionary: \(path.str)")
            return .failed
        }

        // Extract the top-level dictionary from the main plist in a variable we can modify.
        guard case .plDict(var plistDict) = plist else {
            outputDelegate.emitError("Info.plist contents are not a dictionary prior to adding entries from additional content files")
            return .failed
        }

        // Go through the entries in the additional content dict and add them to the plist dictionary.  Note that we do *not* do a deep merge of dictionaries within the top-level dictionary.
        for (key, valueToMerge) in additionalDict {
            let workingValue = plistDict[key]

            // Handle special-case keys first.
            if key == "UIRequiredDeviceCapabilities" {
                if let mergedCapabilitiesResult = mergeUIRequiredDeviceCapabilities(valueToMerge, from: path, into: workingValue, from: inputPath!, outputDelegate) {
                    plistDict[key] = mergedCapabilitiesResult
                }
                else {
                    return .failed
                }
            } else if (key == "CFBundleIcons" || key.hasPrefix("CFBundleIcons~")) && context.scope.evaluate(BuiltinMacros.INFOPLIST_ENABLE_CFBUNDLEICONS_MERGE) {
                plistDict[key] = mergeCFBundleIcons(valueToMerge: valueToMerge, into: workingValue)
            }
            // Merge other keys in a standard manner.
            else {
                if let workingValue = workingValue {
                    // If we already have a value for this key, then we perform a merge based on its type.
                    switch (workingValue, valueToMerge) {
                    case (let workingValue, let valueToMerge) where workingValue.isScalarType && valueToMerge.isScalarType:
                        // When both values are scalar types, we overwrite with the new value.
                        plistDict[key] = valueToMerge

                    case (.plArray(let workingArray), .plArray(let arrayToMerge)):
                        // Both values are arrays - we append the contents of the new value to the old value, making sure not to add the same value twice.
                        // (This would be easier if PropertyListItem were hashable and we could create an OrderedSet here.)
                        var newArray = workingArray
                        for item in arrayToMerge {
                            if !workingArray.contains(item) {
                                newArray.append(item)
                            }
                        }
                        plistDict[key] = .plArray(newArray)

                    case (.plDict(var workingDict), .plDict(let dictToMerge)):
                        // Both values are dictionaries - we merge the contents of the new value into the old value.  Note that we don't do a recursive merge, so if the same key appears in both, then we will overwrite it regardless of their respective types.
                        workingDict.addContents(of: dictToMerge)
                        plistDict[key] = .plDict(workingDict)

                    default:
                        // The two values are of different types, or types we don't recognize - this is an error.
                        outputDelegate.emitError("tried to merge \(valueToMerge.typeDisplayString) value for key '\(key)' onto \(workingValue.typeDisplayString) value")
                        return .failed
                    }
                }
                else {
                    // If the key isn't defined, then we can just set it.
                    plistDict[key] = valueToMerge
                }
            }
        }

        // Create a new PropertyListItem to send back out.
        plist = .plDict(plistDict)

        return .succeeded
    }

    private func addAppPrivacyContent(from path: Path, _ plist: inout PropertyListItem, _ executionDelegate: any TaskExecutionDelegate, _ outputDelegate: any TaskOutputDelegate) -> CommandResult {
        let trackedDomainsKey = "NSPrivacyTrackingDomains"

        // Read the privacy content file and parse it as a property list.
        let contentsData: ByteString
        do {
            contentsData = try executionDelegate.fs.read(path)
        }
        catch {
            outputDelegate.emitError("unable to read privacy file '\(path.str)': \(error.localizedDescription)")
            return .failed
        }
        let privacyPlist: PropertyListItem
        do {
            (privacyPlist, _) = try PropertyList.fromBytesWithFormat(contentsData.bytes)
        }
        catch let error as NSError {
            outputDelegate.emitError("unable to read property list from privacy file: \(path.str): \(error.localizedDescription)")
            return .failed
        }
        catch {
            outputDelegate.emitError("unable to read property list from privacy file: \(path.str): unknown error")
            return .failed
        }
        guard case .plDict(let privacyDict) = privacyPlist else {
            outputDelegate.emitError("privacy file is not a dictionary: \(path.str)")
            return .failed
        }

        // Extract the top-level dictionary from the main plist in a variable we can modify.
        guard case .plDict(var plistDict) = plist else {
            outputDelegate.emitError("Info.plist contents are not a dictionary prior to adding entries from additional content files")
            return .failed
        }

        var trackedDomains = plistDict[trackedDomainsKey]?.arrayValue ?? []
        if let additionalTrackedDomains = privacyDict[trackedDomainsKey]?.arrayValue {
            // It's import to both remove duplicates and sort the items to ensure stable file contents.
            var set = Set<String>(trackedDomains.compactMap( { $0.stringValue }))
            for domain in additionalTrackedDomains.compactMap( { $0.stringValue }) {
                set.insert(domain)
            }
            trackedDomains = set.sorted().map { PropertyListItem.plString($0) }
        }
        if !trackedDomains.isEmpty {
            plistDict[trackedDomainsKey] = .plArray(trackedDomains)

            // Create a new PropertyListItem to send back out.
            plist = .plDict(plistDict)

            return .succeeded
        }

        return .succeeded
    }

    private func mergeUIDeviceFamilyContent(into plistDict: inout [String: PropertyListItem], context: InfoPlistProcessorTaskActionContext, _ executionDelegate: any TaskExecutionDelegate, _ outputDelegate: any TaskOutputDelegate) -> PropertyListItem {
        // If we're targeting a specific device family, set that into the Info.plist.
        let targetedDeviceFamily = context.scope.evaluate(BuiltinMacros.TARGETED_DEVICE_FAMILY)
        let isMacCatalyst = context.sdkVariant?.isMacCatalyst == true
        if !targetedDeviceFamily.isEmpty || isMacCatalyst, let sdkVariant = context.sdkVariant {
            let (devices, effectiveDevices, _, unexpectedValues) = sdkVariant.evaluateTargetedDeviceFamilyBuildSetting(context.scope, context.productType)

            for string in unexpectedValues {
                outputDelegate.emitWarning("unexpected TARGETED_DEVICE_FAMILY item: `\(string)`")
            }

            if !effectiveDevices.isEmpty {
                // If no devices were specified, but this platform has device families, warn
                if devices.isEmpty {
                    let allowed = sdkVariant.deviceFamilies.targetDeviceIdentifiers.sorted()
                    if !allowed.isEmpty {
                        let platformDisplayName: String
                        if isMacCatalyst {
                            platformDisplayName = BuildVersion.Platform.macCatalyst.displayName(infoLookup: executionDelegate.infoLookup)
                        } else {
                            platformDisplayName = context.platform?.familyDisplayName ?? ""
                        }

                        let targetedDeviceFamilyValueString = !targetedDeviceFamily.isEmpty ? " (\(targetedDeviceFamily))" : ""

                        let title = "TARGETED_DEVICE_FAMILY value\(targetedDeviceFamilyValueString) does not contain any device family values compatible with the \(platformDisplayName) platform"
                        if allowed.count == 1 {
                            let suffix = !isMacCatalyst ? " to indicate that this target supports the '\(sdkVariant.deviceFamilies.deviceDisplayName(for: allowed[0]) ?? "<<unknown>>")' device family" : ""
                            outputDelegate.emitWarning("\(title). Please add the value '\(allowed[0])' to the TARGETED_DEVICE_FAMILY build setting\(suffix).")
                        } else {
                            outputDelegate.emitWarning("\(title). Please add one or more of the following values to the TARGETED_DEVICE_FAMILY build setting to indicate the device families supported by this target: \(allowed.map { "'\($0)' (indicating '\(sdkVariant.deviceFamilies.deviceDisplayName(for: $0) ?? "<<unknown>>")')" }.joined(separator: ", ")).")
                        }
                    }
                }

                // Generate the UIDeviceFamily key.
                if plistDict["UIDeviceFamily"] != nil {
                    outputDelegate.emitWarning("User supplied UIDeviceFamily key in the Info.plist will be overwritten. Please use the build setting TARGETED_DEVICE_FAMILY and remove UIDeviceFamily from your Info.plist.")
                }
                plistDict["UIDeviceFamily"] = .plArray(effectiveDevices.sorted().map{ .plInt($0) })
            }
        }

        return .plDict(plistDict)
    }

    /// Remove or add entries based on the platform being built for.  This is primarily interesting for macCatalyst, since an iOS target contains keys not relevant (and even harmful) on macOS, and may be changed to contain macOS keys which should not be included when building for iOS.
    private func editPlatformSpecificEntries(in plistDict: inout [String: PropertyListItem], _ outputDelegate: any TaskOutputDelegate, context: InfoPlistProcessorTaskActionContext) -> PropertyListItem {
        let isMacCatalyst = context.sdkVariant?.isMacCatalyst == true

        /// Convenience enum used to define whether an Info.plist key is supported or not supported on certain platform families.
        enum PlatformFamilySupportInfo {
            /// Indicates that this key is supported on these platform families and no others.
            case supportedOnlyOn(Set<String>)
            /// Indicates that this key is supported on all platform families except these.
            case notSupportedOn(Set<String>)
        }

        /// Utility function to remove a key for a platform, and emit output about the change if it is defined.
        func remove(unsupportedKey key: String, _ reason: String) {
            if plistDict[key] != nil {
                outputDelegate.emitOutput(ByteString(encodingAsUTF8: "removing entry for \"\(key)\" - \(reason)\n"))
                plistDict.removeValue(forKey: key)
            }
        }

        /// Utility function to remove a value from a key for a platform, and emit output about the change if it is defined.
        func remove(unsupportedValue value: String, from key: String, _ reason: String) {
            if let itemListValue = plistDict[key]?.arrayValue {
                var itemList = itemListValue
                let num = itemList.removeAll(where: { $0.stringValue == value })
                if num > 0 {
                    outputDelegate.emitOutput(ByteString(encodingAsUTF8: "removing value \"\(value)\" for \"\(key)\" - \(reason)\n"))
                }
                plistDict[key] = PropertyListItem.plArray(itemList)
            }
        }

        /// Mapping of all keys known to not be supported on some platforms to the info about which platforms they are or aren't supported on.
        let keyPlatformSupport: [String: PlatformFamilySupportInfo] = [
            // Keys not supported on macOS, watchOS.
            "LSRequiresIPhoneOS":               .notSupportedOn(Set(["macOS", "watchOS"])),

            // Keys only supported on macOS.
            "LSBackgroundOnly":                 .supportedOnlyOn(Set(["macOS"])),
            "LSUIElement":                      .supportedOnlyOn(Set(["macOS"])),
            "NSDocumentClass":                  .supportedOnlyOn(Set(["macOS"])),
            "NSServices":                       .supportedOnlyOn(Set(["macOS"])),
            "NSSupportsAutomaticTermination":   .supportedOnlyOn(Set(["macOS"])),
            "NSSupportsSuddenTermination":      .supportedOnlyOn(Set(["macOS"])),

            // Keys only supported on macOS, iOS, tvOS.
            "LSApplicationCategoryType":        .supportedOnlyOn(Set(["macOS", "iOS", "tvOS"])),
        ]

        /// Mapping of all the values for keys known to not be supported on some platforms to the info about which platforms they are or aren't supported on.
        let keyValuesPlatformSupport: [String:[String:PlatformFamilySupportInfo]] = [
            "UIBackgroundModes": [
                "audio": .supportedOnlyOn(["iOS", "tvOS", "watchOS", "xrOS"]),
                "location": .supportedOnlyOn(["iOS", "watchOS"]),
                "voip": .supportedOnlyOn(["iOS", "watchOS", "xrOS"]),
                "fetch": .supportedOnlyOn(["iOS", "tvOS", "xrOS"]),
                "remote-notification": .supportedOnlyOn(["iOS", "tvOS", "watchOS", "xrOS"]),
                "newsstand-content": .supportedOnlyOn(["iOS"]),
                "external-accessory": .supportedOnlyOn(["iOS"]),
                "bluetooth-central": .supportedOnlyOn(["iOS", "watchOS", "xrOS"]),
                "bluetooth-peripheral": .supportedOnlyOn(["iOS"]),
                "network-authentication": .supportedOnlyOn(["iOS", "xrOS"]),
                "processing": .supportedOnlyOn(["iOS", "tvOS", "xrOS"]),
                "push-to-talk": .supportedOnlyOn(["iOS"]),
                "nearby-interaction": .supportedOnlyOn(["iOS"]),
            ]
        ]

        // Process all the keys we might need to remove.  We process them in a sorted order to ensure any output to the transcript is deterministic.
        for (key, platformInfo) in keyPlatformSupport.sorted(byKey: <) {
            switch platformInfo {
            case .notSupportedOn(let platforms):
                // If the platform we're building for is not supported for this key, then remove it.
                if platforms.contains(context.platform?.familyName ?? "") {
                    remove(unsupportedKey: key, "not supported on \(context.platform?.familyDisplayName ?? "")")
                }
            case .supportedOnlyOn(let platforms):
                // If this key is only supported for some platforms, and the platform we're building for is not among them, then remove it.
                if !platforms.contains(context.platform?.familyName ?? "") {
                    let reason: String
                    if let platform = platforms.only {
                        reason = "only supported on \(platform)"
                    }
                    else {
                        reason = "not supported on \(context.platform?.familyDisplayName ?? "")"
                    }
                    remove(unsupportedKey: key,  reason)
                }
            }
        }

        // Process all the keys we might need to remove.  We process them in a sorted order to ensure any output to the transcript is deterministic.
        for (key, valueDict) in keyValuesPlatformSupport.sorted(byKey: <) {
            for (value, platformInfo) in valueDict.sorted(byKey: <) {
                switch platformInfo {
                case .notSupportedOn(let platforms):
                    if platforms.contains(context.platform?.familyName ?? "") {
                        remove(unsupportedValue: value, from: key, "not supported on \(context.platform?.familyDisplayName ?? "")")
                    }

                case .supportedOnlyOn(let platforms):
                    // Let either iOS key-values through for Mac Catalyst...
                    // rdar://96613340 (Change strings to enum values and separate macCatalyst from iOS/macOS)
                    if isMacCatalyst && platforms.contains("iOS") { continue }

                    if !platforms.contains(context.platform?.familyName ?? "") {
                        let reason: String
                        if let platform = platforms.only {
                            reason = "only supported on \(platform)"
                        }
                        else {
                            reason = "not supported on \(context.platform?.familyDisplayName ?? "")"
                        }
                        remove(unsupportedValue: value, from: key,  reason)
                    }
                }
            }
        }

        /// Utility function to add an entry to the Info.plist, and emit output about the change.
        func set(_ key: String, to value: PropertyListItem, displayString: String? = nil) {
            let displayString = displayString ?? "\(value.unsafePropertyList)"
            outputDelegate.emitOutput(ByteString(encodingAsUTF8: "adding entry for \"\(key)\" => \(displayString)\n"))
            plistDict[key] = value
        }

        // Add macCatalyst-specific entries.
        if isMacCatalyst {
            // Default NSSupportsAutomaticTermination and NSSupportsSuddenTermination to YES for macCatalyst if it is not defined.  But only for app targets.
            if context.productType is ApplicationProductTypeSpec {
                if plistDict["NSSupportsAutomaticTermination"] == nil {
                    set("NSSupportsAutomaticTermination", to: .plBool(true))
                }
                if plistDict["NSSupportsSuddenTermination"] == nil {
                    set("NSSupportsSuddenTermination", to: .plBool(true))
                }
            }
        }

        return .plDict(plistDict)
    }

    /// Recursively merge subdictionaries for `CFBundleIcons` to avoid clobbering values that aren't actually being set in `valueToMerge`.
    private func mergeCFBundleIcons(valueToMerge: PropertyListItem, into workingValue: PropertyListItem?) -> PropertyListItem {
        switch (valueToMerge, workingValue) {
        case (.plDict(let dictToMerge), .plDict(var workingDict)):
            for (key, valueToMerge) in dictToMerge {
                workingDict[key] = mergeCFBundleIcons(valueToMerge: valueToMerge, into: workingDict[key])
            }
            return .plDict(workingDict)
        default:
            return valueToMerge
        }
    }

    /// Special handling of the 'UIRequiredDeviceCapabilities' entry in an additional content file.  This entry can be either an array or a dictionary (or it might not exist at all), so we need to handle the case where the values from two different inputs are in different formats.
    private func mergeUIRequiredDeviceCapabilities(_ newValue: PropertyListItem, from newValuePath: Path, into oldValue: PropertyListItem?, from oldValuePath: Path, _ outputDelegate: any TaskOutputDelegate) -> PropertyListItem? {
        if let oldValue {
            switch (oldValue, newValue) {
            case (.plArray(let oldArray), .plArray(let newArray)):
                        // Both values are arrays - we append the contents of newValue to oldValue, making sure not to add the same value twice.
                // (This would be easier if PropertyListItem were hashable and we could create an OrderedSet here.)
                var resultArray = oldArray
                for item in newArray {
                    if !oldArray.contains(item) {
                        resultArray.append(item)
                    }
                }
                return .plArray(resultArray)

            // If one is an array or one is a dictionary, or both are dictionaries, then we promote any arrays to dictionaries and then merge them.  We do a shallow merge.
            case (.plArray(let oldArray), .plDict(let newDict)):
                var oldDict = [String: PropertyListItem]()
                for item in oldArray {
                    if let key = item.stringValue {
                        oldDict[key] = .plBool(true)
                    }
                    else {
                        outputDelegate.emitError("all values in 'UIRequiredDeviceCapabilities' must be strings (file \(oldValuePath)")
                        return nil
                    }
                }
                oldDict.addContents(of: newDict)
                return .plDict(oldDict)

            case (.plDict(var oldDict), .plArray(let newArray)):
                var newDict = [String: PropertyListItem]()
                for item in newArray {
                    if let key = item.stringValue {
                        newDict[key] = .plBool(true)
                    }
                    else {
                        outputDelegate.emitError("all values in 'UIRequiredDeviceCapabilities' must be strings (file \(newValuePath)")
                        return nil
                    }
                }
                oldDict.addContents(of: newDict)
                return .plDict(oldDict)

            case (.plDict(var oldDict), .plDict(let newDict)):
                oldDict.addContents(of: newDict)
                return .plDict(oldDict)

            default:
                // Emit an error that one or both values are not arrays or dictionaries.
                if oldValue.arrayValue == nil, oldValue.dictValue == nil {
                    outputDelegate.emitError("'UIRequiredDeviceCapabilities' must be an array or dictionary but it is \(oldValue.typeDisplayString.withIndefiniteArticle) (file \(oldValuePath.str)")
                }
                if newValue.arrayValue == nil, newValue.dictValue == nil {
                    outputDelegate.emitError("'UIRequiredDeviceCapabilities' must be an array or dictionary but it is \(newValue.typeDisplayString.withIndefiniteArticle) (file \(newValuePath.str)")
                }
                return nil
            }
        }
        else {
            // If oldValue is empty then we can just set it to newValue, as long as it is an array or a dictionary.
            if newValue.arrayValue != nil || newValue.dictValue != nil {
                return newValue
            }
            else {
                outputDelegate.emitError("'UIRequiredDeviceCapabilities' must be an array or dictionary but it is \(newValue.typeDisplayString.withIndefiniteArticle) (file \(oldValuePath.str))")
                return nil
            }
        }
    }

    private func setRequiredDeviceCapabilities(_ plist: inout PropertyListItem, context: InfoPlistProcessorTaskActionContext, outputDelegate: any TaskOutputDelegate) -> CommandResult
    {
        guard case .plDict(var plistDict) = plist else {
            outputDelegate.emitError("Info.plist contents are not a dictionary prior to setting required device capabilities")
            return .failed
        }

        var requiredDeviceCapabilities: PropertyListItem = requiredArchitecture.map { .plArray([.plString($0)]) } ?? .plArray([])

        let allArchitectures = context.cleanupRequiredArchitectures

        if let capabilities = plistDict["UIRequiredDeviceCapabilities"] {
            switch capabilities {
            case .plArray(let existingArchitectures):
                var workingArray = existingArchitectures

                if let requiredArchitecture {
                    let item: PropertyListItem = .plString(requiredArchitecture)
                    if !existingArchitectures.contains(item) {
                        workingArray.append(item)
                    }
                }

                workingArray = workingArray.filter { plistItem in
                    if case .plString(let arch) = plistItem {
                        return !allArchitectures.contains(arch) || (requiredArchitecture == arch)
                    }
                    return true
                }

                requiredDeviceCapabilities = .plArray(workingArray)
            case .plDict(let existingArchitectures):
                var workingDict = existingArchitectures

                if let requiredArchitecture {
                    workingDict[requiredArchitecture] = .plBool(true)
                }

                for architecture in allArchitectures {
                    if requiredArchitecture != architecture {
                        workingDict.removeValue(forKey: architecture)
                    }
                }

                requiredDeviceCapabilities = .plDict(workingDict)
            default:
                outputDelegate.emitError("'UIRequiredDeviceCapabilities' must be an array or dictionary but it is \(capabilities.typeDisplayString.withIndefiniteArticle)")
                return .failed
            }
        }

        plistDict["UIRequiredDeviceCapabilities"] = requiredDeviceCapabilities
        plist = .plDict(plistDict)

        return .succeeded
    }

    /// Validate the contents of the plist and emit warnings and errors as necessary.
    ///
    /// This method will perform some simple edits as part of validation, but more complicated edits (e.g., of more than a top-level value) should be broken out into a separate method.
    private func validatePlistContents(_ plistDict: inout [String: PropertyListItem], _ buildPlatform: BuildVersion.Platform?, _ buildPlatforms: Set<BuildVersion.Platform>?, _ deploymentTarget: Version?, _ scope: MacroEvaluationScope, infoLookup: any PlatformInfoLookup, context: InfoPlistProcessorTaskActionContext, outputDelegate: any TaskOutputDelegate) -> Bool {
        // Validate the bundle identifier.
        if let identifier = plistDict["CFBundleIdentifier"] {
            if case .plString(let identifierString) = identifier {
                if !identifierString.isEmpty {
                    if identifierString != identifierString.asLegalBundleIdentifier {
                        outputDelegate.emitWarning("invalid character in Bundle Identifier. This string must be a uniform type identifier (UTI) that contains only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.) characters.")
                    }

                    let buildSettingIdentifierString = scope.evaluate(BuiltinMacros.PRODUCT_BUNDLE_IDENTIFIER)
                    if identifierString != buildSettingIdentifierString {
                        outputDelegate.warning("User-supplied CFBundleIdentifier value '\(identifierString)' in the Info.plist must be the same as the PRODUCT_BUNDLE_IDENTIFIER build setting value '\(buildSettingIdentifierString)'.", location: .buildSetting(BuiltinMacros.PRODUCT_BUNDLE_IDENTIFIER))
                    }
                }
            }
            else {
                outputDelegate.emitWarning("the value for Bundle Identifier must be of type string, but is \(identifier.typeDisplayString.withIndefiniteArticle)")
            }
        }

        // Figure out the minimum system version to use based on the SDK.
        let minimumSystemVersionKey: String
        switch buildPlatform {
        case .macOS?, .macCatalyst?:
            minimumSystemVersionKey = "LSMinimumSystemVersion"
        case .driverKit?:
            minimumSystemVersionKey = "OSMinimumDriverKitVersion"
        default:
            minimumSystemVersionKey = "MinimumOSVersion"
        }

        // Validate that the minimum system version key is not less than the deployment target.  If it is, then emit a warning and also set it to the deployment target.
        // Note that for MacCatalyst the relevant deployment target here is the macOS one.
        if let deploymentTarget, let minimumSystemVersionStr = plistDict[minimumSystemVersionKey]?.stringValue {
            let deploymentTargetName = scope.evaluate(BuiltinMacros.DEPLOYMENT_TARGET_SETTING_NAME)
            if minimumSystemVersionStr.isEmpty {
                outputDelegate.emitWarning("\(minimumSystemVersionKey) is explicitly set to empty - setting to value of \(deploymentTargetName) '\(deploymentTarget.description)'.")
                plistDict[minimumSystemVersionKey] = .plString(deploymentTarget.description)
            }
            else if let minimumSystemVersion = try? Version(minimumSystemVersionStr) {
                if minimumSystemVersion < deploymentTarget {
                    outputDelegate.emitWarning("\(minimumSystemVersionKey) of '\(minimumSystemVersionStr)' is less than the value of \(deploymentTargetName) '\(deploymentTarget.description)' - setting to '\(deploymentTarget.description)'.")
                    plistDict[minimumSystemVersionKey] = .plString(deploymentTarget.description)
                }
            }
            else {
                outputDelegate.emitError("\(minimumSystemVersionKey) '\(minimumSystemVersionStr)' is not a valid version.")
                return false
            }
        }

        if let productType = context.productType {
            if productType.conformsTo(identifier: "com.apple.product-type.application.on-demand-install-capable") {
                let appClipProhibitedKeys = ["CFBundleDocumentTypes", "LSApplicationQueriesSchemes"]
                for key in appClipProhibitedKeys {
                    if plistDict[key] != nil {
                        outputDelegate.emitWarning("'\(key)' has no effect for \(productType.name) targets and will be ignored.")
                    }
                }
            }
        }

        struct DeprecationInfo {
            enum DeprecationPlatform: String {
                case macOS
                case iOS
                case tvOS
                case watchOS
                case visionOS = "xrOS" // must be "xrOS" as it's compared against Platform.familyName
            }

            /// The values of the key that are deprecated, if only specific values are deprecated. `nil` indicates the key as a whole is deprecated.
            let values: [PropertyListItem]?

            enum MoreInfo {
                /// An infix to display in the "use (alternative) instead" portion of the deprecation message.
                case alternate(String)

                case ignored(String)
            }

            let moreInfo: MoreInfo

            /// Mapping of platforms and the version of that platform beginning in which the deprecation warning should be shown.
            ///
            /// If the version is empty, the warning will always be shown for that platform.
            /// If there is no version value present for a platform at all, no warning will ever be shown for that platform.
            let deprecationVersions: [DeprecationPlatform: Version]

            init(values: [PropertyListItem]? = nil, moreInfo: MoreInfo, deprecationVersions: [DeprecationPlatform: Version]) {
                self.values = values
                self.moreInfo = moreInfo
                self.deprecationVersions = deprecationVersions
            }
        }

        let plistKeyDeprecationInfo: [PropertyListKeyPath: DeprecationInfo] = [
            "UILaunchImages": .init(moreInfo: .alternate("launch storyboards"), deprecationVersions: [.iOS: Version(), .tvOS: Version(13)]),
            "CLKComplicationSupportedFamilies": .init(moreInfo: .alternate("the ClockKit complications API"), deprecationVersions: [.watchOS: Version(7)]),
            PropertyListKeyPath(.dict(.equal("NSAppTransportSecurity")), .dict(.equal("NSExceptionDomains")), .dict(.any), .any(.equal("NSExceptionMinimumTLSVersion"))): .init(values: [.plString("TLSv1.0"), .plString("TLSv1.1")], moreInfo: .alternate("TLSv1.2 or TLSv1.3"), deprecationVersions: [
                .macOS: Version(12),
                .iOS: Version(15),
                .tvOS: Version(15),
                .watchOS: Version(8)
            ]),
            "UIRequiresFullScreen": .init(moreInfo: .ignored("See the UIRequiresFullScreen documentation for more details."), deprecationVersions: [
                .macOS: Version(26),
                .iOS: Version(26),
                .tvOS: Version(26),
                .watchOS: Version(26),
                .visionOS: Version(26),
            ])
        ]

        for (key, info) in plistKeyDeprecationInfo.sorted(byKey: <) {
            for item in plistDict.propertyListItem[key] {
                if let platformFamily = DeprecationInfo.DeprecationPlatform(rawValue: context.platform?.familyName ?? ""), let deprecationVersion = info.deprecationVersions[platformFamily] {
                    if let specificValues = info.values, !specificValues.contains(item.value) {
                        continue
                    }

                    let prefixPart: String
                    if info.values != nil {
                        prefixPart = "The value \(item.value) for '\(item.actualKeyPath.joined(separator: "' => '"))'"
                    } else {
                        prefixPart = "'\(item.actualKeyPath.joined(separator: "' => '"))'"
                    }

                    let suffixPart: String
                    switch info.moreInfo {
                    case let .alternate(alternate):
                        suffixPart = ", use \(alternate) instead."
                    case let .ignored(ignored):
                        suffixPart = " and will be ignored in a future release. \(ignored)"
                    }

                    let message: String
                    if deprecationVersion > Version() {
                        message = "\(prefixPart) has been deprecated starting in \(context.platform?.familyDisplayName ?? "") \(deprecationVersion.canonicalDeploymentTargetForm.description)\(suffixPart)"
                    } else {
                        message = "\(prefixPart) has been deprecated\(suffixPart)"
                    }

                    if let deploymentTarget = deploymentTarget, deploymentTarget >= deprecationVersion {
                        outputDelegate.emitWarning(message)
                    }
                }
            }
        }

        validateUsageStringDefinitions(plistDict, outputDelegate: outputDelegate)
        return true
    }

    private func validateUsageStringDefinitions(_ plistDict: [String: PropertyListItem], outputDelegate: any TaskOutputDelegate) {
        let usageStringPlistEntries = plistDict.filter { key, _ in
            usageDescriptionStringKeys.contains(key)
        }

        for (key, value) in usageStringPlistEntries {
            switch value {
            case .plString(let stringValue):
                if stringValue.isEmpty {
                    outputDelegate.emitWarning("The value for \(key) must be a non-empty string.")
                }
            default:
                outputDelegate.emitWarning("The value for \(key) must be of type string, but is \(value.typeDisplayString.withIndefiniteArticle).")
            }
        }
    }

    /// Scans the path for an `PrivacyInfo.xcprivacy` file and returns the `Path` to that file, if found. Otherwise, returns `nil`.
    private func scanForPrivacyFile(at path: Path, fs: any FSProxy) -> Path? {
        do {
            return try fs.traverse(path) { $0.basename == "PrivacyInfo.xcprivacy" ? $0 : nil }.first
        }
        catch {}

        return nil
    }

    private func generatePackageInfo(atPath pkgInfoPath: Path, usingPlist plistDict: [String: PropertyListItem], context: InfoPlistProcessorTaskActionContext, _ executionDelegate: any TaskExecutionDelegate, _ outputDelegate: any TaskOutputDelegate) throws {
        // Get the Mac OS Roman four character code for a key.
        func getFourCharCode(forKey key: String) -> Data {
            guard let value = plistDict[key] else {
                return Data("????".utf8)
            }

            guard case let .plString(string) = value else {
                outputDelegate.emitWarning("key '\(key)' in 'Info.plist' is not a valid string value")
                return Data("????".utf8)
            }

            let evaluatedString = context.scope.evaluate(context.scope.table.namespace.parseString(string))
            guard let encoded = evaluatedString.data(using: .macOSRoman), encoded.count == 4 else {
                outputDelegate.emitWarning("value '\(evaluatedString)' for '\(key)' in 'Info.plist' must be a four character string")
                return Data("????".utf8)
            }

            return encoded
        }

        // Get the package type code and signature (a.k.a. creator) code from the Info.plist.  We do various correctness checks.  One of the more interesting restrictions is that both the type code and the signature code have to be convertible to Mac OS Roman encoding.  The reason for this is that both four-character codes are really OSTypes, which were implicitly encoded in Mac OS Roman back in historical times.
        let pkgInfoBytes = (OutputByteStream()
            <<< getFourCharCode(forKey: "CFBundlePackageType")
            <<< getFourCharCode(forKey: "CFBundleSignature")).bytes
        do {
            _ = try executionDelegate.fs.writeIfChanged(pkgInfoPath, contents: pkgInfoBytes)
        } catch {
            outputDelegate.emitError("unable to write file '\(pkgInfoPath.str)': \(error.localizedDescription)")
        }
    }


    // Serialization


    public override func serialize<T: Serializer>(to serializer: T)
    {
        serializer.beginAggregate(2)
        serializer.serialize(contextPath)
        super.serialize(to: serializer)
        serializer.endAggregate()
    }

    public required init(from deserializer: any Deserializer) throws
    {
        try deserializer.beginAggregate(2)
        self.contextPath = try deserializer.deserialize()
        try super.init(from: deserializer)
    }
}


private extension PropertyListItem
{
    /// Specialized private method to transform a property list by removing any keys which are in the set `keys` from any dictionary in the plist where the value for that key is empty.
    func byElidingRecursivelyEmptyStringValuesInDictionaries(_ keysToElide: Set<String>) -> PropertyListItem
    {
        switch self
        {
        case .plArray(let value):
            // Dive into the array to elide keys from any dictionaries in it.
            return .plArray(value.map({ return $0.byElidingRecursivelyEmptyStringValuesInDictionaries(keysToElide) }))

        case .plDict(let value):
            // Handle this dictionary.
            var result = [String: PropertyListItem]()
            for (key, item) in value
            {
                switch item
                {
                case .plString(let value):
                    // For strings, we elide it only if the key is in keysToElide and the value is empty.
                    if !value.isEmpty || !keysToElide.contains(key)
                    {
                        result[key] = item
                    }

                default:
                    // For other values we process them recursively.
                    result[key] = item.byElidingRecursivelyEmptyStringValuesInDictionaries(keysToElide)
                }
            }
            return .plDict(result)

        default:
            // For non-collection types we just return the item.
            return self
        }
    }
}

extension ProductTypeSpec {
    /// watchOS stub apps deployed to versions of watchOS earlier than 6.0 have a fixed set of Info.plist keys which are allowed.
    ///
    /// We need to be careful not to add any keys outside that list when deploying to legacy versions of watchOS.
    func requiresStrictInfoPlistKeys(_ scope: MacroEvaluationScope) -> Bool {
        guard let deploymentTarget = try? Version(scope.evaluate(BuiltinMacros.WATCHOS_DEPLOYMENT_TARGET)) else {
            return false
        }
        return identifier == "com.apple.product-type.application.watchapp2" && deploymentTarget < Version(6)
    }
}

extension PropertyListItem {
    var extensionPointIdentifier: String? {
        let foundationExtensionPointIdentifier = dictValue?["NSExtension"]?.dictValue?["NSExtensionPointIdentifier"]?.stringValue
        let extensionKitExtensionPointIdentifier = dictValue?["EXAppExtensionAttributes"]?.dictValue?["EXExtensionPointIdentifier"]?.stringValue
        switch (foundationExtensionPointIdentifier, extensionKitExtensionPointIdentifier) {
        case (let .some(id1), let .some(id2)):
            return Set([id1, id2]).only
        case (let .some(extensionPointIdentifier), nil), (nil, let .some(extensionPointIdentifier)):
            return extensionPointIdentifier
        case (nil, nil):
            return nil
        }
    }
}