Skip to content

Commit 3ad06c0

Browse files
baujlajakepetroules
authored andcommitted
Add build warning when usage description key has empty string for value. rdar://129306042
1 parent dd5f85a commit 3ad06c0

File tree

2 files changed

+114
-51
lines changed

2 files changed

+114
-51
lines changed

Sources/SWBTaskExecution/TaskActions/InfoPlistProcessorTaskAction.swift

Lines changed: 69 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,55 @@ public final class InfoPlistProcessorTaskAction: TaskAction
632632
return (.succeeded, messages)
633633
}
634634

635+
private let usageDescriptionStringKeys: Set<String> = [
636+
"NFCReaderUsageDescription",
637+
"NSAppleEventsUsageDescription",
638+
"NSAppleMusicUsageDescription",
639+
"NSBluetoothAlwaysUsageDescription",
640+
"NSBluetoothPeripheralUsageDescription",
641+
"NSBluetoothWhileInUseUsageDescription",
642+
"NSCalendarsUsageDescription",
643+
"NSCameraUsageDescription",
644+
"NSContactsUsageDescription",
645+
"NSDesktopFolderUsageDescription",
646+
"NSDocumentsFolderUsageDescription",
647+
"NSDownloadsFolderUsageDescription",
648+
"NSFaceIDUsageDescription",
649+
"NSFallDetectionUsageDescription",
650+
"NSFileProviderDomainUsageDescription",
651+
"NSFileProviderPresenceUsageDescription",
652+
"NSFocusStatusUsageDescription",
653+
"NSGKFriendListUsageDescription",
654+
"NSHealthClinicalHealthRecordsShareUsageDescription",
655+
"NSHealthShareUsageDescription",
656+
"NSHealthUpdateUsageDescription",
657+
"NSHomeKitUsageDescription",
658+
"NSIdentityUsageDescription",
659+
"NSLocalNetworkUsageDescription",
660+
"NSLocationAlwaysAndWhenInUseUsageDescription",
661+
"NSLocationAlwaysUsageDescription",
662+
"NSLocationUsageDescription",
663+
"NSLocationWhenInUseUsageDescription",
664+
"NSMicrophoneUsageDescription",
665+
"NSMotionUsageDescription",
666+
"NSNearbyInteractionAllowOnceUsageDescription",
667+
"NSNearbyInteractionUsageDescription",
668+
"NSNetworkVolumesUsageDescription",
669+
"NSPhotoLibraryAddUsageDescription",
670+
"NSPhotoLibraryUsageDescription",
671+
"NSRemindersUsageDescription",
672+
"NSRemovableVolumesUsageDescription",
673+
"NSSensorKitUsageDescription",
674+
"NSSiriUsageDescription",
675+
"NSSpeechRecognitionUsageDescription",
676+
"NSSystemAdministrationUsageDescription",
677+
"NSSystemExtensionUsageDescription",
678+
"NSUserTrackingUsageDescription",
679+
"NSVideoSubscriberAccountUsageDescription",
680+
"NSVoIPUsageDescription",
681+
"OSBundleUsageDescription",
682+
]
683+
635684
/// Produce a collection of default Info.plist keys based on content in the project, taking platform and product type into account.
636685
/// Could be done through InfoPlistAdditions properties in product types, but that would result in more duplication at this point
637686
private func defaultInfoPlistContent(scope: MacroEvaluationScope, platform: Platform?, productType: ProductTypeSpec?) -> [String: PropertyListItem]
@@ -687,59 +736,9 @@ public final class InfoPlistProcessorTaskAction: TaskAction
687736
"LSApplicationCategoryType",
688737
"NSHumanReadableCopyright",
689738
"NSPrincipalClass",
690-
691-
// Usage Descriptions
692-
693739
"ITSAppUsesNonExemptEncryption",
694740
"ITSEncryptionExportComplianceCode",
695-
"NFCReaderUsageDescription",
696-
"NSAppleEventsUsageDescription",
697-
"NSAppleMusicUsageDescription",
698-
"NSBluetoothAlwaysUsageDescription",
699-
"NSBluetoothPeripheralUsageDescription",
700-
"NSBluetoothWhileInUseUsageDescription",
701-
"NSCalendarsUsageDescription",
702-
"NSCameraUsageDescription",
703-
"NSContactsUsageDescription",
704-
"NSDesktopFolderUsageDescription",
705-
"NSDocumentsFolderUsageDescription",
706-
"NSDownloadsFolderUsageDescription",
707-
"NSFaceIDUsageDescription",
708-
"NSFallDetectionUsageDescription",
709-
"NSFileProviderDomainUsageDescription",
710-
"NSFileProviderPresenceUsageDescription",
711-
"NSFocusStatusUsageDescription",
712-
"NSGKFriendListUsageDescription",
713-
"NSHealthClinicalHealthRecordsShareUsageDescription",
714-
"NSHealthShareUsageDescription",
715-
"NSHealthUpdateUsageDescription",
716-
"NSHomeKitUsageDescription",
717-
"NSIdentityUsageDescription",
718-
"NSLocalNetworkUsageDescription",
719-
"NSLocationAlwaysAndWhenInUseUsageDescription",
720-
"NSLocationAlwaysUsageDescription",
721741
"NSLocationTemporaryUsageDescriptionDictionary",
722-
"NSLocationUsageDescription",
723-
"NSLocationWhenInUseUsageDescription",
724-
"NSMicrophoneUsageDescription",
725-
"NSMotionUsageDescription",
726-
"NSNearbyInteractionAllowOnceUsageDescription",
727-
"NSNearbyInteractionUsageDescription",
728-
"NSNetworkVolumesUsageDescription",
729-
"NSPhotoLibraryAddUsageDescription",
730-
"NSPhotoLibraryUsageDescription",
731-
"NSRemindersUsageDescription",
732-
"NSRemovableVolumesUsageDescription",
733-
"NSSensorKitPrivacyPolicyURL",
734-
"NSSensorKitUsageDescription",
735-
"NSSiriUsageDescription",
736-
"NSSpeechRecognitionUsageDescription",
737-
"NSSystemAdministrationUsageDescription",
738-
"NSSystemExtensionUsageDescription",
739-
"NSUserTrackingUsageDescription",
740-
"NSVideoSubscriberAccountUsageDescription",
741-
"NSVoIPUsageDescription",
742-
"OSBundleUsageDescription",
743742

744743
// macOS
745744

@@ -759,6 +758,7 @@ public final class InfoPlistProcessorTaskAction: TaskAction
759758
// iOS
760759

761760
"LSSupportsOpeningDocumentsInPlace",
761+
"NSSensorKitPrivacyPolicyURL",
762762
"NSSupportsLiveActivities",
763763
"NSSupportsLiveActivitiesFrequentUpdates",
764764
"UIApplicationSupportsIndirectInputEvents",
@@ -791,7 +791,7 @@ public final class InfoPlistProcessorTaskAction: TaskAction
791791
// Sticker Packs
792792

793793
"NSStickerSharingLevel",
794-
]
794+
] + usageDescriptionStringKeys
795795

796796
for key in generatedInfoPlistKeys {
797797
updateContentWithInfoPlistKeyMacroValue(&content, key)
@@ -1511,9 +1511,27 @@ public final class InfoPlistProcessorTaskAction: TaskAction
15111511
}
15121512
}
15131513

1514+
validateUsageStringDefinitions(plistDict, outputDelegate: outputDelegate)
15141515
return true
15151516
}
15161517

1518+
private func validateUsageStringDefinitions(_ plistDict: [String: PropertyListItem], outputDelegate: any TaskOutputDelegate) {
1519+
let usageStringPlistEntries = plistDict.filter { key, _ in
1520+
usageDescriptionStringKeys.contains(key)
1521+
}
1522+
1523+
for (key, value) in usageStringPlistEntries {
1524+
switch value {
1525+
case .plString(let stringValue):
1526+
if stringValue.isEmpty {
1527+
outputDelegate.emitWarning("The value for \(key) must be a non-empty string.")
1528+
}
1529+
default:
1530+
outputDelegate.emitWarning("The value for \(key) must be of type string, but is \(value.typeDisplayString.withIndefiniteArticle).")
1531+
}
1532+
}
1533+
}
1534+
15171535
/// Scans the path for an `PrivacyInfo.xcprivacy` file and returns the `Path` to that file, if found. Otherwise, returns `nil`.
15181536
private func scanForPrivacyFile(at path: Path, fs: any FSProxy) -> Path? {
15191537
do {

Tests/SWBTaskExecutionTests/InfoPlistProcessorTaskTests.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2371,4 +2371,49 @@ fileprivate struct InfoPlistProcessorTaskTests: CoreBasedTests {
23712371
XCTAssertEqualPropertyListItems(dict["CFBundleIcons~ipad"], expectedMerge)
23722372
}
23732373
}
2374+
2375+
@Test
2376+
func usageDescriptionKeyWarning() async throws {
2377+
let core = try await getCore()
2378+
let scope = try createMockScope { namespace, table in
2379+
try namespace.declareBooleanMacro("GENERATE_INFOPLIST_FILE")
2380+
table.push(try #require(namespace.lookupMacroDeclaration("GENERATE_INFOPLIST_FILE") as? BooleanMacroDeclaration), literal: true)
2381+
try table.remove(namespace.declareStringMacro("MARKETING_VERSION"))
2382+
}
2383+
let platformName = "iphoneos"
2384+
let platform = try #require(core.platformRegistry.lookup(name: platformName), "invalid platform name '\(platformName)'")
2385+
let executionDelegate = MockExecutionDelegate(core: try await getCore())
2386+
2387+
let action = InfoPlistProcessorTaskAction(try prepareContext(InfoPlistProcessorTaskActionContext(scope: scope, productType: nil, platform: platform, sdk: core.sdkRegistry.lookup(platformName), sdkVariant: nil, cleanupRequiredArchitectures: []), fs: executionDelegate.fs))
2388+
let task = Task(forTarget: nil, ruleInfo: [], commandLine: ["builtin-infoPlistUtility", "-expandbuildsettings", "-platform", platformName, "/tmp/input.plist", "-o", "/tmp/output.plist"], workingDirectory: Path.root.join("tmp"), outputs: [], action: action, execDescription: "Copy Info.plist")
2389+
2390+
// Write the test files.
2391+
try executionDelegate.fs.createDirectory(Path.root.join("tmp"))
2392+
try await executionDelegate.fs.writePlist(Path("/tmp/input.plist"), .plDict([
2393+
"CFBundleIconName": .plString(""),
2394+
"NSHomeKitUsageDescription": .plString(""),
2395+
"NSSiriUsageDescription": .plBool(true),
2396+
"NSFaceIDUsageDescription": .plString("Used for authentication."),
2397+
]))
2398+
2399+
let outputDelegate = MockTaskOutputDelegate()
2400+
let result = await action.performTaskAction(
2401+
task,
2402+
dynamicExecutionDelegate: MockDynamicTaskExecutionDelegate(),
2403+
executionDelegate: executionDelegate,
2404+
clientDelegate: MockTaskExecutionClientDelegate(),
2405+
outputDelegate: outputDelegate
2406+
)
2407+
2408+
guard result == .succeeded else {
2409+
Issue.record("task failed; errors: \(outputDelegate.errors)")
2410+
return
2411+
}
2412+
2413+
let warnings = outputDelegate.warnings
2414+
2415+
#expect(warnings.count == 2)
2416+
#expect(warnings.contains("warning: The value for NSHomeKitUsageDescription must be a non-empty string."))
2417+
#expect(warnings.contains("warning: The value for NSSiriUsageDescription must be of type string, but is a boolean."))
2418+
}
23742419
}

0 commit comments

Comments
 (0)