Skip to content

Commit

Permalink
Add support for privacy manifest file generation (tuist#6117)
Browse files Browse the repository at this point in the history
* Implement xcprivacy as part of ResourceFileElements

* Add acceptance tests

* Fix tests

* Fix acceptance test

* Lint

* Implement PR Feedback

* Fix acceptance test

* Add fixture for privacyManifest

* Add acceptance test

* Run lint:fix

* Fix naming

* Add documentation and more details in example

* Run mise run lint:fix
  • Loading branch information
Lilfaen authored Apr 11, 2024
1 parent 1aa97a8 commit 0d31f0d
Show file tree
Hide file tree
Showing 41 changed files with 655 additions and 116 deletions.
48 changes: 48 additions & 0 deletions Sources/ProjectDescription/PrivacyManifest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation

/// Describe the data your app or third-party SDK collects and the reasons required APIs it uses.
public struct PrivacyManifest: Codable, Equatable {
/// A Boolean that indicates whether your app or third-party SDK uses data for tracking as defined under the App
/// Tracking Transparency framework. For more information, see [User Privacy and Data
/// Use](https://developer.apple.com/app-store/user-privacy-and-data-use/).
public var tracking: Bool

/// An array of strings that lists the internet domains your app or third-party SDK connects to that
/// engage in tracking. If the user has not granted tracking permission through the App Tracking Transparency framework,
/// network requests to these domains fail and your app receives an error. If you set `tracking` to true then you need to
/// provide at least one internet domain in NSPrivacyTrackingDomains; otherwise, you can provide zero or more domains.
public var trackingDomains: [String]

/// An array of dictionaries that describes the data types your app or third-party SDK collects. For
/// information on the keys and values to use in the dictionaries, see [Describing data use in privacy manifests](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests).
public var collectedDataTypes: [[String: Plist.Value]]

/// An array of dictionaries that describe the API types your app or third-party SDK accesses that have
/// been designated as APIs that require reasons to access. For information on the keys and values to use in the dictionaries,
/// see [Describing use of required reason API](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api).
public var accessedApiTypes: [[String: Plist.Value]]

/// Returns a PrivacyManifest.
/// - Parameter tracking: A Boolean that indicates whether your app or third-party SDK uses data for tracking.
/// - Parameter trackingDomains: An array of strings that lists the internet domains your app or third-party SDK connects to
/// that engage in tracking.
/// - Parameter collectedDataTypes: An array of dictionaries that describes the data types your app or third-party SDK
/// collects.
/// - Parameter accessedApiTypes: An array of dictionaries that describe the API types your app or third-party SDK accesses
/// that have
/// been designated as APIs that require reasons to access.
/// - Returns: PrivacyManifest.
public static func privacyManifest(
tracking: Bool,
trackingDomains: [String],
collectedDataTypes: [[String: Plist.Value]],
accessedApiTypes: [[String: Plist.Value]]
) -> Self {
PrivacyManifest(
tracking: tracking,
trackingDomains: trackingDomains,
collectedDataTypes: collectedDataTypes,
accessedApiTypes: accessedApiTypes
)
}
}
7 changes: 5 additions & 2 deletions Sources/ProjectDescription/ResourceFileElements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ public struct ResourceFileElements: Codable, Equatable {
/// List of resource file elements
public var resources: [ResourceFileElement]

public static func resources(_ resources: [ResourceFileElement]) -> Self {
self.init(resources: resources)
/// Define your apps privacy manifest
public var privacyManifest: PrivacyManifest?

public static func resources(_ resources: [ResourceFileElement], privacyManifest: PrivacyManifest? = nil) -> Self {
self.init(resources: resources, privacyManifest: privacyManifest)
}
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public enum TuistAcceptanceFixtures {
case iosAppWithLocalSwiftPackage
case iosAppWithMultiConfigs
case iosAppWithPluginsAndTemplates
case iosAppWithPrivacyManifest
case iosAppWithRemoteBinarySwiftPackage
case iosAppWithRemoteSwiftPackage
case iosAppWithStaticFrameworks
Expand Down Expand Up @@ -140,6 +141,8 @@ public enum TuistAcceptanceFixtures {
return "ios_app_with_multi_configs"
case .iosAppWithPluginsAndTemplates:
return "ios_app_with_plugins_and_templates"
case .iosAppWithPrivacyManifest:
return "ios_app_with_privacy_manifest"
case .iosAppWithRemoteBinarySwiftPackage:
return "ios_app_with_remote_binary_swift_package"
case .iosAppWithRemoteSwiftPackage:
Expand Down
2 changes: 1 addition & 1 deletion Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ final class BuildPhaseGenerator: BuildPhaseGenerating {

pbxBuildFiles.append(contentsOf: try generateResourcesBuildFile(
target: target,
files: target.resources,
files: target.resources.resources,
fileElements: fileElements
))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ final class ProjectDescriptorGenerator: ProjectDescriptorGenerating {
var attributes: [String: Any] = [:]

/// ODR tags
let tags = project.targets.map { $0.resources.map(\.tags).flatMap { $0 } }.flatMap { $0 }
let tags = project.targets.map { $0.resources.resources.map(\.tags).flatMap { $0 } }.flatMap { $0 }
let uniqueTags = Set(tags).sorted()

if !uniqueTags.isEmpty {
Expand Down
2 changes: 1 addition & 1 deletion Sources/TuistGenerator/Generator/ProjectFileElements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class ProjectFileElements {
// Elements
var elements = Set<GroupFileElement>()
elements.formUnion(files.map { GroupFileElement(path: $0, group: target.filesGroup) })
elements.formUnion(target.resources.map {
elements.formUnion(target.resources.resources.map {
GroupFileElement(
path: $0.path,
group: target.filesGroup,
Expand Down
4 changes: 2 additions & 2 deletions Sources/TuistGenerator/Linter/TargetLinter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class TargetLinter: TargetLinting {
private func lintCopiedFiles(target: Target) -> [LintingIssue] {
var issues: [LintingIssue] = []

let files = target.resources.map(\.path)
let files = target.resources.resources.map(\.path)
let entitlements = files.filter { $0.pathString.contains(".entitlements") }

if let targetInfoPlistPath = target.infoPlist?.path, files.contains(targetInfoPlistPath) {
Expand Down Expand Up @@ -193,7 +193,7 @@ class TargetLinter: TargetLinting {
return []
}

if target.resources.isEmpty == false {
if target.resources.resources.isEmpty == false {
return [
LintingIssue(
reason: "Target \(target.name) cannot contain resources. \(target.product) targets do not support resources",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Foundation
import TSCBasic
import TuistCore
import TuistGraph
import TuistSupport
import XcodeProj

/// A project mapper that generates derived privacyManifest files for targets that define it as a dictonary.
public final class GeneratePrivacyManifestProjectMapper: ProjectMapping {
public init() {}

// MARK: - ProjectMapping

public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) {
logger.debug("Transforming project \(project.name): Synthesizing privacy manifest files'")

let results = try project.targets
.reduce(into: (targets: [Target](), sideEffects: [SideEffectDescriptor]())) { results, target in
let (updatedTarget, sideEffects) = try map(target: target, project: project)
results.targets.append(updatedTarget)
results.sideEffects.append(contentsOf: sideEffects)
}

return (project.with(targets: results.targets), results.sideEffects)
}

// MARK: - Private

private func map(target: Target, project: Project) throws -> (Target, [SideEffectDescriptor]) {
guard let privacyManifest = target.resources.privacyManifest else {
return (target, [])
}

let dictionary: [String: Any] = [
"NSPrivacyTracking": privacyManifest.tracking,
"NSPrivacyTrackingDomains": privacyManifest.trackingDomains,
"NSPrivacyCollectedDataTypes": privacyManifest.collectedDataTypes.map { $0.mapValues { $0.value } },
"NSPrivacyAccessedAPITypes": privacyManifest.accessedApiTypes.map { $0.mapValues { $0.value } },
]

let data = try PropertyListSerialization.data(
fromPropertyList: dictionary,
format: .xml,
options: 0
)

let privacyManifestPath = project.path
.appending(component: Constants.DerivedDirectory.name)
.appending(component: Constants.DerivedDirectory.privacyManifest)
.appending(component: target.name)
.appending(component: "PrivacyInfo.xcprivacy")
let sideEffect = SideEffectDescriptor.file(FileDescriptor(path: privacyManifestPath, contents: data))

var resources = target.resources
resources.resources.append(.init(path: privacyManifestPath))

var newTarget = target
newTarget.resources = resources

return (newTarget, [sideEffect])
}
}
6 changes: 3 additions & 3 deletions Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class ResourcesProjectMapper: ProjectMapping { // swiftlint:disable:this

// swiftlint:disable:next function_body_length
public func mapTarget(_ target: Target, project: Project) throws -> ([Target], [SideEffectDescriptor]) {
if target.resources.isEmpty, target.coreDataModels.isEmpty { return ([target], []) }
if target.resources.resources.isEmpty, target.coreDataModels.isEmpty { return ([target], []) }

var additionalTargets: [Target] = []
var sideEffects: [SideEffectDescriptor] = []
Expand Down Expand Up @@ -59,7 +59,7 @@ public class ResourcesProjectMapper: ProjectMapping { // swiftlint:disable:this
coreDataModels: target.coreDataModels,
filesGroup: target.filesGroup
)
modifiedTarget.resources = []
modifiedTarget.resources.resources = []
modifiedTarget.copyFiles = []
modifiedTarget.dependencies.append(.target(name: bundleName, condition: .when(target.dependencyPlatformFilters)))
additionalTargets.append(resourcesTarget)
Expand All @@ -81,7 +81,7 @@ public class ResourcesProjectMapper: ProjectMapping { // swiftlint:disable:this
if project.isExternal,
target.supportsSources,
target.sources.contains(where: { $0.path.extension == "m" || $0.path.extension == "mm" }),
!target.resources.filter({ $0.path.extension != "xcprivacy" }).isEmpty
!target.resources.resources.filter({ $0.path.extension != "xcprivacy" }).isEmpty
{
let (headerFilePath, headerData) = synthesizedObjcHeaderFile(bundleName: bundleName, target: target, project: project)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public final class SynthesizedResourceInterfaceProjectMapper: ProjectMapping { /

/// Map and generate resource interfaces for a given `Target` and `Project`
private func mapTarget(_ target: Target, project: Project) throws -> (Target, [SideEffectDescriptor]) {
guard !target.resources.isEmpty, target.supportsSources else { return (target, []) }
guard !target.resources.resources.isEmpty, target.supportsSources else { return (target, []) }

var target = target

Expand Down Expand Up @@ -152,7 +152,7 @@ public final class SynthesizedResourceInterfaceProjectMapper: ProjectMapping { /
target: Target,
developmentRegion: String?
) -> [AbsolutePath] {
let resourcesPaths = target.resources
let resourcesPaths = target.resources.resources
.map(\.path)

var paths = resourcesPaths
Expand Down
23 changes: 23 additions & 0 deletions Sources/TuistGraph/Models/PrivacyManifest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation

public struct PrivacyManifest: Codable, Equatable {
public var tracking: Bool

public var trackingDomains: [String]

public var collectedDataTypes: [[String: Plist.Value]]

public var accessedApiTypes: [[String: Plist.Value]]

public init(
tracking: Bool,
trackingDomains: [String],
collectedDataTypes: [[String: Plist.Value]],
accessedApiTypes: [[String: Plist.Value]]
) {
self.tracking = tracking
self.trackingDomains = trackingDomains
self.collectedDataTypes = collectedDataTypes
self.accessedApiTypes = accessedApiTypes
}
}
15 changes: 15 additions & 0 deletions Sources/TuistGraph/Models/ResourceFileElements.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

public struct ResourceFileElements: Codable, Equatable {
public var resources: [ResourceFileElement]

public var privacyManifest: PrivacyManifest?

public init(
_ resources: [ResourceFileElement],
privacyManifest: PrivacyManifest? = nil
) {
self.resources = resources
self.privacyManifest = privacyManifest
}
}
4 changes: 2 additions & 2 deletions Sources/TuistGraph/Models/Target.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public struct Target: Equatable, Hashable, Comparable, Codable {
public var settings: Settings?
public var dependencies: [TargetDependency]
public var sources: [SourceFile]
public var resources: [ResourceFileElement]
public var resources: ResourceFileElements
public var copyFiles: [CopyFilesAction]
public var headers: Headers?
public var coreDataModels: [CoreDataModel]
Expand Down Expand Up @@ -60,7 +60,7 @@ public struct Target: Equatable, Hashable, Comparable, Codable {
entitlements: Entitlements? = nil,
settings: Settings? = nil,
sources: [SourceFile] = [],
resources: [ResourceFileElement] = [],
resources: ResourceFileElements = .init([]),
copyFiles: [CopyFilesAction] = [],
headers: Headers? = nil,
coreDataModels: [CoreDataModel] = [],
Expand Down
6 changes: 3 additions & 3 deletions Sources/TuistGraphTesting/Models/Target+TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension Target {
entitlements: Entitlements? = nil,
settings: Settings? = Settings.test(),
sources: [SourceFile] = [],
resources: [ResourceFileElement] = [],
resources: ResourceFileElements = .init([]),
copyFiles: [CopyFilesAction] = [],
coreDataModels: [CoreDataModel] = [],
headers: Headers? = nil,
Expand Down Expand Up @@ -74,7 +74,7 @@ extension Target {
entitlements: Entitlements? = nil,
settings: Settings? = Settings.test(),
sources: [SourceFile] = [],
resources: [ResourceFileElement] = [],
resources: ResourceFileElements = .init([]),
copyFiles: [CopyFilesAction] = [],
coreDataModels: [CoreDataModel] = [],
headers: Headers? = nil,
Expand Down Expand Up @@ -131,7 +131,7 @@ extension Target {
entitlements: Entitlements? = nil,
settings: Settings? = nil,
sources: [SourceFile] = [],
resources: [ResourceFileElement] = [],
resources: ResourceFileElements = .init([]),
copyFiles: [CopyFilesAction] = [],
coreDataModels: [CoreDataModel] = [],
headers: Headers? = nil,
Expand Down
3 changes: 3 additions & 0 deletions Sources/TuistKit/Mappers/Factories/ProjectMapperFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ public final class ProjectMapperFactory: ProjectMapperFactorying {
// Entitlements
mappers.append(GenerateEntitlementsProjectMapper())

// Privacy Manifest
mappers.append(GeneratePrivacyManifestProjectMapper())

// Template macros
mappers.append(IDETemplateMacrosMapper())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ extension ProjectAutomation.Target {
product: target.product.rawValue,
bundleId: target.bundleId,
sources: target.sources.map(\.path.pathString),
resources: target.resources.map(\.path.pathString),
resources: target.resources.resources.map(\.path.pathString),
settings: ProjectAutomation.Settings.from(target.settings),
dependencies: dependencies
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ extension TuistGraph.Target {
generatorPaths: GeneratorPaths
// swiftlint:disable:next large_tuple
) throws -> (
resources: [TuistGraph.ResourceFileElement],
resources: TuistGraph.ResourceFileElements,
playgrounds: [AbsolutePath],
coreDataModels: [AbsolutePath],
invalidResourceGlobs: [InvalidGlob]
Expand All @@ -143,8 +143,17 @@ extension TuistGraph.Target {
TuistGraph.Target.isResource(path: path)
}

let privacyManifest: TuistGraph.PrivacyManifest? = manifest.resources?.privacyManifest.map {
return TuistGraph.PrivacyManifest(
tracking: $0.tracking,
trackingDomains: $0.trackingDomains,
collectedDataTypes: $0.collectedDataTypes.map { $0.mapValues { TuistGraph.Plist.Value.from(manifest: $0) }},
accessedApiTypes: $0.accessedApiTypes.map { $0.mapValues { TuistGraph.Plist.Value.from(manifest: $0) }}
)
}

var invalidResourceGlobs: [InvalidGlob] = []
var filteredResources: [TuistGraph.ResourceFileElement] = []
var filteredResources: TuistGraph.ResourceFileElements = .init([], privacyManifest: privacyManifest)
var playgrounds: Set<AbsolutePath> = []
var coreDataModels: Set<AbsolutePath> = []

Expand All @@ -163,14 +172,14 @@ extension TuistGraph.Target {

for fileElement in allResources {
switch fileElement {
case .folderReference: filteredResources.append(fileElement)
case .folderReference: filteredResources.resources.append(fileElement)
case let .file(path, _, _):
if path.extension == "playground" {
playgrounds.insert(path)
} else if path.extension == "xcdatamodeld" {
coreDataModels.insert(path)
} else {
filteredResources.append(fileElement)
filteredResources.resources.append(fileElement)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ public final class PackageInfoMapper: PackageInfoMapping {

var headers: ProjectDescription.Headers?
var sources: SourceFilesList?
var resources: ResourceFileElements?
var resources: ProjectDescription.ResourceFileElements?

if target.type.supportsPublicHeaderPath {
headers = try Headers.from(moduleMap: moduleMap)
Expand Down Expand Up @@ -762,7 +762,7 @@ extension SourceFilesList {
}
}

extension ResourceFileElements {
extension ProjectDescription.ResourceFileElements {
fileprivate static func from(
sources: [String]?,
resources: [PackageInfo.Target.Resource],
Expand Down
Loading

0 comments on commit 0d31f0d

Please sign in to comment.