Skip to content

Commit 1518a8f

Browse files
authored
Merge pull request #172 from giginet/move-privacyinfo
Move PrivacyInfo.xcprivacy from the resource bundle to the expected location inside the framework
2 parents e55c6cb + 6cf2c33 commit 1518a8f

File tree

7 files changed

+177
-38
lines changed

7 files changed

+177
-38
lines changed

.swiftlint.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ disabled_rules:
1010
- todo
1111
- cyclomatic_complexity
1212
- optional_data_string_conversion
13-
line_length: 150
13+
line_length:
14+
warning: 150
15+
ignores_comments: true
1416
file_length: 600
1517
nesting:
1618
type_level: 3

Sources/ScipioKit/Producer/PIF/FrameworkBundleAssembler.swift

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,26 @@ struct FrameworkBundleAssembler {
2929
func assemble() throws -> TSCAbsolutePath {
3030
try fileSystem.createDirectory(frameworkBundlePath, recursive: true)
3131

32-
try copyInfoPlist()
33-
3432
try copyBinary()
3533

3634
try copyHeaders()
3735

3836
try copyModules()
3937

40-
try copyResources()
38+
let resourcesProcessor = ResourcesProcessor(fileSystem: fileSystem)
39+
try resourcesProcessor.copyResources(
40+
sourceContext: .init(
41+
isFrameworkVersionedBundle: frameworkComponents.isVersionedBundle,
42+
frameworkBundlePath: frameworkComponents.frameworkPath,
43+
frameworkInfoPlistPath: frameworkComponents.infoPlistPath,
44+
resourceBundlePath: frameworkComponents.resourceBundlePath
45+
),
46+
destinationFrameworkBundlePath: frameworkBundlePath
47+
)
4148

4249
return frameworkBundlePath
4350
}
4451

45-
private func copyInfoPlist() throws {
46-
let sourcePath = frameworkComponents.infoPlistPath
47-
let destinationPath = frameworkBundlePath.appending(component: "Info.plist")
48-
try fileSystem.copy(from: sourcePath, to: destinationPath)
49-
}
50-
5152
private func copyBinary() throws {
5253
let sourcePath = frameworkComponents.binaryPath
5354
let destinationPath = frameworkBundlePath.appending(component: frameworkComponents.frameworkName)
@@ -153,11 +154,101 @@ struct FrameworkBundleAssembler {
153154
)
154155
}
155156
}
157+
}
158+
159+
extension FrameworkBundleAssembler {
160+
struct ResourcesProcessor {
161+
struct SourceContext {
162+
let isFrameworkVersionedBundle: Bool
163+
let frameworkBundlePath: TSCAbsolutePath
164+
let frameworkInfoPlistPath: TSCAbsolutePath
165+
let resourceBundlePath: TSCAbsolutePath?
166+
}
167+
168+
private let fileSystem: any FileSystem
169+
170+
init(fileSystem: some FileSystem) {
171+
self.fileSystem = fileSystem
172+
}
173+
174+
func copyResources(
175+
sourceContext: SourceContext,
176+
destinationFrameworkBundlePath: TSCAbsolutePath
177+
) throws {
178+
if sourceContext.isFrameworkVersionedBundle {
179+
// The framework is a versioned bundle, so copy entire Resources directory
180+
// instead of copying its Info.plist and resource bundle separately.
181+
let sourceResourcesPath = sourceContext.frameworkBundlePath.appending(component: "Resources")
182+
let destinationResourcesPath = destinationFrameworkBundlePath.appending(component: "Resources")
183+
try fileSystem.copy(
184+
from: sourceResourcesPath.asURL.resolvingSymlinksInPath().absolutePath,
185+
to: destinationResourcesPath
186+
)
187+
188+
if let resourceBundleName = sourceContext.resourceBundlePath?.basename {
189+
let resourceBundlePath = destinationResourcesPath.appending(component: resourceBundleName)
190+
// A resource bundle of versioned bundle framework has "Contents/Resources" directory.
191+
try extractPrivacyInfoIfExists(
192+
from: TSCRelativePath(validating: "Contents/Resources/PrivacyInfo.xcprivacy"),
193+
in: resourceBundlePath
194+
)
195+
}
196+
} else {
197+
try copyInfoPlist(
198+
sourceContext: sourceContext,
199+
destinationFrameworkBundlePath: destinationFrameworkBundlePath
200+
)
201+
let copiedResourceBundlePath = try copyResourceBundle(
202+
sourceContext: sourceContext,
203+
destinationFrameworkBundlePath: destinationFrameworkBundlePath
204+
)
205+
206+
if let copiedResourceBundlePath {
207+
try extractPrivacyInfoIfExists(
208+
from: TSCRelativePath(validating: "PrivacyInfo.xcprivacy"),
209+
in: copiedResourceBundlePath
210+
)
211+
}
212+
}
213+
}
214+
215+
private func copyInfoPlist(
216+
sourceContext: SourceContext,
217+
destinationFrameworkBundlePath: TSCAbsolutePath
218+
) throws {
219+
let sourcePath = sourceContext.frameworkInfoPlistPath
220+
let destinationPath = destinationFrameworkBundlePath.appending(component: "Info.plist")
221+
try fileSystem.copy(from: sourcePath, to: destinationPath)
222+
}
156223

157-
private func copyResources() throws {
158-
if let resourceBundlePath = frameworkComponents.resourceBundlePath {
159-
let destinationPath = frameworkBundlePath.appending(component: resourceBundlePath.basename)
160-
try fileSystem.copy(from: resourceBundlePath, to: destinationPath)
224+
/// Returns the resulting, copied resource bundle path.
225+
private func copyResourceBundle(
226+
sourceContext: SourceContext,
227+
destinationFrameworkBundlePath: TSCAbsolutePath
228+
) throws -> TSCAbsolutePath? {
229+
if let sourcePath = sourceContext.resourceBundlePath {
230+
let destinationPath = destinationFrameworkBundlePath.appending(component: sourcePath.basename)
231+
try fileSystem.copy(from: sourcePath, to: destinationPath)
232+
return destinationPath
233+
} else {
234+
return nil
235+
}
236+
}
237+
238+
/// Extracts PrivacyInfo.xcprivacy to expected location (if exists in the resource bundle).
239+
///
240+
/// - seealso: https://developer.apple.com/documentation/bundleresources/adding-a-privacy-manifest-to-your-app-or-third-party-sdk#Add-a-privacy-manifest-to-your-framework
241+
private func extractPrivacyInfoIfExists(
242+
from relativePrivacyInfoPath: TSCRelativePath,
243+
in resourceBundlePath: TSCAbsolutePath
244+
) throws {
245+
let privacyInfoPath = resourceBundlePath.appending(relativePrivacyInfoPath)
246+
if fileSystem.exists(privacyInfoPath) {
247+
try fileSystem.move(
248+
from: privacyInfoPath,
249+
to: resourceBundlePath.parentDirectory.appending(component: relativePrivacyInfoPath.basename)
250+
)
251+
}
161252
}
162253
}
163254
}

Sources/ScipioKit/Producer/PIF/FrameworkComponentsCollector.swift

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import PackageModel
44

55
/// FileLists to assemble a framework bundle
66
struct FrameworkComponents {
7+
/// Whether the built framework is a versioned bundle or not.
8+
///
9+
/// In general, frameworks for macOS would be this format.
10+
///
11+
/// - seealso: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html
12+
var isVersionedBundle: Bool
713
var frameworkName: String
14+
var frameworkPath: TSCAbsolutePath
815
var binaryPath: TSCAbsolutePath
916
var infoPlistPath: TSCAbsolutePath
1017
var swiftModulesPath: TSCAbsolutePath?
@@ -34,6 +41,8 @@ struct FrameworkComponentsCollector {
3441
private let packageLocator: any PackageLocator
3542
private let fileSystem: any FileSystem
3643

44+
private let productsDirectory: TSCAbsolutePath
45+
3746
init(
3847
buildProduct: BuildProduct,
3948
sdk: SDK,
@@ -46,6 +55,11 @@ struct FrameworkComponentsCollector {
4655
self.buildOptions = buildOptions
4756
self.packageLocator = packageLocator
4857
self.fileSystem = fileSystem
58+
59+
productsDirectory = packageLocator.productsDirectory(
60+
buildConfiguration: buildOptions.buildConfiguration,
61+
sdk: sdk
62+
)
4963
}
5064

5165
func collectComponents(sdk: SDK) throws -> FrameworkComponents {
@@ -60,6 +74,11 @@ struct FrameworkComponentsCollector {
6074
let targetName = buildProduct.target.c99name
6175
let generatedFrameworkPath = generatedFrameworkPath()
6276

77+
let isVersionedBundle = fileSystem.exists(
78+
generatedFrameworkPath.appending(component: "Resources"),
79+
followSymlink: true
80+
)
81+
6382
let binaryPath = generatedFrameworkPath.appending(component: targetName)
6483

6584
let swiftModulesPath = try collectSwiftModules(
@@ -74,15 +93,17 @@ struct FrameworkComponentsCollector {
7493

7594
let publicHeaders = try collectPublicHeaders()
7695

77-
let resourceBundlePath = try collectResourceBundle(
78-
of: targetName,
79-
in: generatedFrameworkPath
80-
)
96+
let resourceBundlePath = generatedResourceBundlePath()
8197

82-
let infoPlistPath = try collectInfoPlist(in: generatedFrameworkPath)
98+
let infoPlistPath = try collectInfoPlist(
99+
in: generatedFrameworkPath,
100+
isVersionedBundle: isVersionedBundle
101+
)
83102

84103
let components = FrameworkComponents(
104+
isVersionedBundle: isVersionedBundle,
85105
frameworkName: buildProduct.target.name.packageNamed(),
106+
frameworkPath: generatedFrameworkPath,
86107
binaryPath: binaryPath,
87108
infoPlistPath: infoPlistPath,
88109
swiftModulesPath: swiftModulesPath,
@@ -121,24 +142,33 @@ struct FrameworkComponentsCollector {
121142
}
122143

123144
private func generatedFrameworkPath() -> TSCAbsolutePath {
124-
packageLocator.productsDirectory(
125-
buildConfiguration: buildOptions.buildConfiguration,
126-
sdk: sdk
127-
)
128-
.appending(component: "\(buildProduct.target.c99name).framework")
145+
productsDirectory.appending(component: "\(buildProduct.target.c99name).framework")
129146
}
130147

131-
private func collectInfoPlist(in frameworkBundlePath: TSCAbsolutePath) throws -> TSCAbsolutePath {
132-
let infoPlistLocationCandidates = [
133-
// In a regular framework bundle, Info.plist should be on its root
134-
frameworkBundlePath.appending(component: "Info.plist"),
148+
private func generatedResourceBundlePath() -> TSCAbsolutePath? {
149+
guard let bundleName = buildProduct.target.underlying.bundleName else { return nil }
150+
151+
let path = productsDirectory.appending(component: "\(bundleName).bundle")
152+
return fileSystem.exists(path) ? path : nil
153+
}
154+
155+
private func collectInfoPlist(
156+
in frameworkBundlePath: TSCAbsolutePath,
157+
isVersionedBundle: Bool
158+
) throws -> TSCAbsolutePath {
159+
let infoPlistLocation = if isVersionedBundle {
135160
// In a versioned framework bundle (for macOS), Info.plist should be in Resources
136-
frameworkBundlePath.appending(components: "Resources", "Info.plist"),
137-
]
138-
guard let infoPlistPath = infoPlistLocationCandidates.first(where: fileSystem.exists(_:)) else {
161+
frameworkBundlePath.appending(components: "Resources", "Info.plist")
162+
} else {
163+
// In a regular framework bundle, Info.plist should be on its root
164+
frameworkBundlePath.appending(component: "Info.plist")
165+
}
166+
167+
if fileSystem.exists(infoPlistLocation) {
168+
return infoPlistLocation
169+
} else {
139170
throw Error.infoPlistNotFound(frameworkBundlePath: frameworkBundlePath)
140171
}
141-
return infoPlistPath
142172
}
143173

144174
/// Collects *.swiftmodules* in a generated framework bundle
@@ -193,9 +223,4 @@ struct FrameworkComponentsCollector {
193223

194224
return Set(notSymlinks + notDuplicatedSymlinks)
195225
}
196-
197-
private func collectResourceBundle(of targetName: String, in frameworkPath: TSCAbsolutePath) throws -> TSCAbsolutePath? {
198-
let bundleFileName = try fileSystem.getDirectoryContents(frameworkPath).first { $0.hasSuffix(".bundle") }
199-
return bundleFileName.flatMap { frameworkPath.appending(component: $0) }
200-
}
201226
}

Tests/ScipioKitTests/FrameworkBundleAssemblerTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ struct FrameworkBundleAssemblerTests {
4545
private func assembleFramework(keepPublicHeadersStructure: Bool, outputDirectory: TSCAbsolutePath) throws {
4646
let fixture = fixturesPath.appendingPathComponent("FrameworkBundleAssemblerTests").absolutePath
4747
let frameworkComponents = FrameworkComponents(
48+
isVersionedBundle: false,
4849
frameworkName: "Foo",
50+
frameworkPath: fixture.appending(component: "Foo.framework"),
4951
binaryPath: fixture.appending(components: "Foo.framework", "Foo"),
5052
infoPlistPath: fixture.appending(components: "Foo.framework", "Info.plist"),
5153
includeDir: fixture.appending(components: "include"),

Tests/ScipioKitTests/Resources/Fixtures/ResourcePackage/Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ let package = Package(
2121
dependencies: [
2222
],
2323
resources: [
24+
.copy("Resources/PrivacyInfo.xcprivacy"),
2425
.process("Resources/giginet.png"),
2526
]
2627
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NSPrivacyAccessedAPITypes</key>
6+
<array>
7+
</array>
8+
<key>NSPrivacyCollectedDataTypes</key>
9+
<array>
10+
</array>
11+
</dict>
12+
</plist>

Tests/ScipioKitTests/RunnerTests.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,9 +621,15 @@ final class RunnerTests: XCTestCase {
621621

622622
let xcFramework = frameworkOutputDir.appendingPathComponent("ResourcePackage.xcframework")
623623
for arch in ["ios-arm64", "ios-arm64_x86_64-simulator"] {
624-
let bundlePath = xcFramework
624+
let frameworkPath = xcFramework
625625
.appendingPathComponent(arch)
626626
.appendingPathComponent("ResourcePackage.framework")
627+
XCTAssertTrue(
628+
fileManager.fileExists(atPath: frameworkPath.appendingPathComponent("PrivacyInfo.xcprivacy").path),
629+
"PrivacyInfo.xcprivacy should be located at the expected location"
630+
)
631+
632+
let bundlePath = frameworkPath
627633
.appendingPathComponent("ResourcePackage_ResourcePackage.bundle")
628634
XCTAssertTrue(
629635
fileManager.fileExists(atPath: bundlePath.path),

0 commit comments

Comments
 (0)