/
FrameworkBuilder.swift
executable file
·806 lines (749 loc) · 36.9 KB
/
FrameworkBuilder.swift
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
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
import Utils
/// A structure to build a .framework in a given project directory.
struct FrameworkBuilder {
/// Platforms to be included in the built frameworks.
private let targetPlatforms: [TargetPlatform]
/// The directory containing the Xcode project and Pods folder.
private let projectDir: URL
/// Flag for building dynamic frameworks instead of static frameworks.
private let dynamicFrameworks: Bool
/// The Pods directory for building the framework.
private var podsDir: URL {
return projectDir.appendingPathComponent("Pods", isDirectory: true)
}
/// Default initializer.
init(projectDir: URL, targetPlatforms: [TargetPlatform], dynamicFrameworks: Bool) {
self.projectDir = projectDir
self.targetPlatforms = targetPlatforms
self.dynamicFrameworks = dynamicFrameworks
}
// MARK: - Public Functions
/// Compiles the specified framework in a temporary directory and writes the build logs to file.
/// This will compile all architectures for a single platform at a time.
///
/// - Parameter framework: The name of the framework to be built.
/// - Parameter logsOutputDir: The path to the directory to place build logs.
/// - Parameter setCarthage: Set Carthage diagnostics flag in build.
/// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
/// - Returns: A path to the newly compiled frameworks, and Resources.
func compileFrameworkAndResources(withName framework: String,
logsOutputDir: URL? = nil,
setCarthage: Bool,
podInfo: CocoaPodUtils.PodInfo) -> ([URL], URL?) {
let fileManager = FileManager.default
let outputDir = fileManager.temporaryDirectory(withName: "frameworks_being_built")
let logsDir = logsOutputDir ?? fileManager.temporaryDirectory(withName: "build_logs")
do {
// Remove the compiled frameworks directory, this isn't the cache we're using.
if fileManager.directoryExists(at: outputDir) {
try fileManager.removeItem(at: outputDir)
}
try fileManager.createDirectory(at: outputDir, withIntermediateDirectories: true)
// Create our logs directory if it doesn't exist.
if !fileManager.directoryExists(at: logsDir) {
try fileManager.createDirectory(at: logsDir, withIntermediateDirectories: true)
}
} catch {
fatalError("Failure creating temporary directory while building \(framework): \(error)")
}
if dynamicFrameworks {
return (buildDynamicFrameworks(withName: framework, logsDir: logsDir, outputDir: outputDir),
nil)
} else {
return buildStaticFrameworks(
withName: framework,
logsDir: logsDir,
outputDir: outputDir,
setCarthage: setCarthage,
podInfo: podInfo
)
}
}
// MARK: - Private Helpers
/// This runs a command and immediately returns a Shell result.
/// NOTE: This exists in conjunction with the `Shell.execute...` due to issues with different
/// `.bash_profile` environment variables. This should be consolidated in the future.
private static func syncExec(command: String,
args: [String] = [],
captureOutput: Bool = false) -> Shell
.Result {
let task = Process()
task.launchPath = command
task.arguments = args
// If we want to output to the console, create a readabilityHandler and save each line along the
// way. Otherwise, we can just read the pipe at the end. By disabling outputToConsole, some
// commands (such as any xcodebuild) can run much, much faster.
var output = ""
if captureOutput {
let pipe = Pipe()
task.standardOutput = pipe
let outHandle = pipe.fileHandleForReading
outHandle.readabilityHandler = { pipe in
// This will be run any time data is sent to the pipe. We want to print it and store it for
// later. Ignore any non-valid Strings.
guard let line = String(data: pipe.availableData, encoding: .utf8) else {
print("Could not get data from pipe for command \(command): \(pipe.availableData)")
return
}
if !line.isEmpty {
output += line
}
}
// Also set the termination handler on the task in order to stop the readabilityHandler from
// parsing any more data from the task.
task.terminationHandler = { t in
guard let stdOut = t.standardOutput as? Pipe else { return }
stdOut.fileHandleForReading.readabilityHandler = nil
}
} else {
// No capturing output, just mark it as complete.
output = "The task completed"
}
task.launch()
task.waitUntilExit()
// Normally we'd use a pipe to retrieve the output, but for whatever reason it slows things down
// tremendously for xcodebuild.
guard task.terminationStatus == 0 else {
return .error(code: task.terminationStatus, output: output)
}
return .success(output: output)
}
/// Build all thin slices for an open source pod.
/// - Parameter framework: The name of the framework to be built.
/// - Parameter logsDir: The path to the directory to place build logs.
/// - Parameter setCarthage: Set Carthage flag in GoogleUtilities for metrics.
/// - Returns: A dictionary of URLs to the built thin libraries keyed by platform.
private func buildFrameworksForAllPlatforms(withName framework: String,
logsDir: URL,
setCarthage: Bool) -> [TargetPlatform: URL] {
// Build every architecture and save the locations in an array to be assembled.
var slicedFrameworks = [TargetPlatform: URL]()
for targetPlatform in targetPlatforms {
let buildDir = projectDir.appendingPathComponent(targetPlatform.buildName)
let sliced = buildSlicedFramework(withName: framework,
targetPlatform: targetPlatform,
buildDir: buildDir,
logRoot: logsDir,
setCarthage: setCarthage)
slicedFrameworks[targetPlatform] = sliced
}
return slicedFrameworks
}
/// Uses `xcodebuild` to build a framework for a specific target platform.
///
/// - Parameters:
/// - framework: Name of the framework being built.
/// - targetPlatform: The target platform to target for the build.
/// - buildDir: Location where the project should be built.
/// - logRoot: Root directory where all logs should be written.
/// - setCarthage: Set Carthage flag in GoogleUtilities for metrics.
/// - Returns: A URL to the framework that was built.
private func buildSlicedFramework(withName framework: String,
targetPlatform: TargetPlatform,
buildDir: URL,
logRoot: URL,
setCarthage: Bool = false) -> URL {
let isMacCatalyst = targetPlatform == .catalyst
let isMacCatalystString = isMacCatalyst ? "YES" : "NO"
let workspacePath = projectDir.appendingPathComponent("FrameworkMaker.xcworkspace").path
let distributionFlag = setCarthage ? "-DFIREBASE_BUILD_CARTHAGE" :
"-DFIREBASE_BUILD_ZIP_FILE"
let cFlags = "OTHER_CFLAGS=$(value) \(distributionFlag)"
var archs = targetPlatform.archs.map { $0.rawValue }.joined(separator: " ")
// The 32 bit archs do not build for iOS 11.
if framework == "FirebaseAppCheck" || framework.hasSuffix("Swift") {
if targetPlatform == .iOSDevice {
archs = "arm64"
} else if targetPlatform == .iOSSimulator {
archs = "x86_64 arm64"
}
}
var args = ["build",
"-configuration", "release",
"-workspace", workspacePath,
"-scheme", framework,
"GCC_GENERATE_DEBUGGING_SYMBOLS=NO",
"ARCHS=\(archs)",
"VALID_ARCHS=\(archs)",
"ONLY_ACTIVE_ARCH=NO",
// BUILD_LIBRARY_FOR_DISTRIBUTION=YES is necessary for Swift libraries.
// See https://forums.developer.apple.com/thread/125646.
// Unlike the comment there, the option here is sufficient to cause .swiftinterface
// files to be generated in the .swiftmodule directory. The .swiftinterface files
// are required for xcodebuild to successfully generate an xcframework.
"BUILD_LIBRARY_FOR_DISTRIBUTION=YES",
// Remove the -fembed-bitcode-marker compiling flag.
"ENABLE_BITCODE=NO",
"SUPPORTS_MACCATALYST=\(isMacCatalystString)",
"BUILD_DIR=\(buildDir.path)",
"-sdk", targetPlatform.sdkName,
cFlags]
// Code signing isn't needed for libraries. Disabling signing is required for
// Catalyst libs with resources. See
// https://github.com/CocoaPods/CocoaPods/issues/8891#issuecomment-573301570
if isMacCatalyst {
args.append("CODE_SIGN_IDENTITY=-")
}
print("""
Compiling \(framework) for \(targetPlatform.buildName) (\(archs)) with command:
/usr/bin/xcodebuild \(args.joined(separator: " "))
""")
// Regardless if it succeeds or not, we want to write the log to file in case we need to inspect
// things further.
let logFileName = "\(framework)-\(targetPlatform.buildName).txt"
let logFile = logRoot.appendingPathComponent(logFileName)
let result = FrameworkBuilder.syncExec(command: "/usr/bin/xcodebuild",
args: args,
captureOutput: true)
switch result {
case let .error(code, output):
// Write output to disk and print the location of it. Force unwrapping here since it's going
// to crash anyways, and at this point the root log directory exists, we know it's UTF8, so it
// should pass every time. Revisit if that's not the case.
try! output.write(to: logFile, atomically: true, encoding: .utf8)
fatalError("Error building \(framework) for \(targetPlatform.buildName). Code: \(code). See " +
"the build log at \(logFile)")
case let .success(output):
// Try to write the output to the log file but if it fails it's not a huge deal since it was
// a successful build.
try? output.write(to: logFile, atomically: true, encoding: .utf8)
print("""
Successfully built \(framework) for \(targetPlatform.buildName). Build log is at \(logFile).
""")
// Use the Xcode-generated path to return the path to the compiled library.
// The framework name may be different from the pod name if the module is reset in the
// podspec - like Release-iphonesimulator/BoringSSL-GRPC/openssl_grpc.framework.
print("buildDir: \(buildDir)")
let frameworkPath = buildDir.appendingPathComponents([targetPlatform.buildDirName, framework])
var actualFramework: String
do {
let files = try FileManager.default.contentsOfDirectory(at: frameworkPath,
includingPropertiesForKeys: nil)
.compactMap { $0.path }
let frameworkDir = files.filter { $0.contains(".framework") }
actualFramework = URL(fileURLWithPath: frameworkDir[0]).lastPathComponent
} catch {
fatalError("Error while enumerating files \(frameworkPath): \(error.localizedDescription)")
}
let libPath = frameworkPath.appendingPathComponent(actualFramework)
print("buildSliced returns \(libPath)")
return libPath
}
}
// TODO: Automatically get the right name.
/// The module name is different from the pod name when the module_name
/// specifier is used in the podspec.
///
/// - Parameter framework: The name of the pod to be built.
/// - Returns: The corresponding framework/module name.
private static func frameworkBuildName(_ framework: String) -> String {
switch framework {
case "abseil":
return "absl"
case "BoringSSL-GRPC":
return "openssl_grpc"
case "gRPC-Core":
return "grpc"
case "gRPC-C++":
return "grpcpp"
case "leveldb-library":
return "leveldb"
case "PromisesObjC":
return "FBLPromises"
case "PromisesSwift":
return "Promises"
case "Protobuf":
return "protobuf"
default:
return framework
}
}
/// Compiles the specified framework in a temporary directory and writes the build logs to file.
/// This will compile all architectures and use the -create-xcframework command to create a modern
/// "fat" framework.
///
/// - Parameter framework: The name of the framework to be built.
/// - Parameter logsDir: The path to the directory to place build logs.
/// - Returns: A path to the newly compiled frameworks (with any included Resources embedded).
private func buildDynamicFrameworks(withName framework: String,
logsDir: URL,
outputDir: URL) -> [URL] {
// xcframework doesn't lipo things together but accepts fat frameworks for one target.
// We group architectures here to deal with this fact.
var thinFrameworks = [URL]()
for targetPlatform in TargetPlatform.allCases {
let buildDir = projectDir.appendingPathComponent(targetPlatform.buildName)
let slicedFramework = buildSlicedFramework(
withName: FrameworkBuilder.frameworkBuildName(framework),
targetPlatform: targetPlatform,
buildDir: buildDir,
logRoot: logsDir
)
thinFrameworks.append(slicedFramework)
}
return thinFrameworks
}
/// Compiles the specified framework in a temporary directory and writes the build logs to file.
/// This will compile all architectures and use the -create-xcframework command to create a modern
/// "fat" framework.
///
/// - Parameter framework: The name of the framework to be built.
/// - Parameter logsDir: The path to the directory to place build logs.
/// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
/// - Returns: A path to the newly compiled framework, and the Resource URL.
private func buildStaticFrameworks(withName framework: String,
logsDir: URL,
outputDir: URL,
setCarthage: Bool,
podInfo: CocoaPodUtils.PodInfo) -> ([URL], URL) {
// Build every architecture and save the locations in an array to be assembled.
let slicedFrameworks = buildFrameworksForAllPlatforms(withName: framework, logsDir: logsDir,
setCarthage: setCarthage)
// Create the framework directory in the filesystem for the thin archives to go.
let fileManager = FileManager.default
let frameworkName = FrameworkBuilder.frameworkBuildName(framework)
let frameworkDir = outputDir.appendingPathComponent("\(frameworkName).framework")
do {
try fileManager.createDirectory(at: frameworkDir, withIntermediateDirectories: true)
} catch {
fatalError("Could not create framework directory while building framework \(frameworkName). " +
"\(error)")
}
guard let anyPlatform = targetPlatforms.first,
let archivePath = slicedFrameworks[anyPlatform] else {
fatalError("Could not get a path to an archive to fetch headers in \(frameworkName).")
}
// Find CocoaPods generated umbrella header.
var umbrellaHeader = ""
// TODO(ncooke3): Evaluate if `TensorFlowLiteObjC` is needed?
if framework == "gRPC-Core" || framework == "TensorFlowLiteObjC" {
// TODO: Proper handling of podspec-specified module.modulemap files with customized umbrella
// headers. This is good enough for Firebase since it doesn't need these modules.
// TODO(ncooke3): Is this needed for gRPC-Core?
umbrellaHeader = "\(framework)-umbrella.h"
} else {
var umbrellaHeaderURL: URL
// Get the framework Headers directory. On macOS, it's a symbolic link.
let headersDir = archivePath.appendingPathComponent("Headers").resolvingSymlinksInPath()
do {
let files = try fileManager.contentsOfDirectory(at: headersDir,
includingPropertiesForKeys: nil)
.compactMap { $0.path }
let umbrellas = files.filter { $0.hasSuffix("umbrella.h") }
if umbrellas.count != 1 {
fatalError("Did not find exactly one umbrella header in \(headersDir).")
}
guard let firstUmbrella = umbrellas.first else {
fatalError("Failed to get umbrella header in \(headersDir).")
}
umbrellaHeaderURL = URL(fileURLWithPath: firstUmbrella)
} catch {
fatalError("Error while enumerating files \(headersDir): \(error.localizedDescription)")
}
umbrellaHeader = umbrellaHeaderURL.lastPathComponent
}
// TODO: copy PrivateHeaders directory as well if it exists. SDWebImage is an example pod.
// Move all the Resources into .bundle directories in the destination Resources dir. The
// Resources live are contained within the folder structure:
// `projectDir/arch/Release-platform/FrameworkName`.
// The Resources are stored at the top-level of the .framework or .xcframework directory.
// For Firebase distributions, they are propagated one level higher in the final distribution.
let resourceContents = projectDir.appendingPathComponents([anyPlatform.buildName,
anyPlatform.buildDirName,
framework])
guard let moduleMapContentsTemplate = podInfo.moduleMapContents else {
fatalError("Module map contents missing for framework \(frameworkName)")
}
let moduleMapContents = moduleMapContentsTemplate.get(umbrellaHeader: umbrellaHeader)
let frameworks = groupFrameworks(withName: frameworkName,
isCarthage: setCarthage,
fromFolder: frameworkDir,
slicedFrameworks: slicedFrameworks,
moduleMapContents: moduleMapContents)
// Remove the temporary thin archives.
for slicedFramework in slicedFrameworks.values {
do {
try fileManager.removeItem(at: slicedFramework)
} catch {
// Just log a warning instead of failing, since this doesn't actually affect the build
// itself. This should only be shown to help users clean up their disk afterwards.
print("""
WARNING: Failed to remove temporary sliced framework at \(slicedFramework.path). This should
be removed from your system to save disk space. \(error). You should be able to remove the
archive from Terminal with:
rm \(slicedFramework.path)
""")
}
}
return (frameworks, resourceContents)
}
/// Parses CocoaPods config files or uses the passed in `moduleMapContents` to write the
/// appropriate `moduleMap` to the `destination`.
/// Returns true to fail if building for Carthage and there are Swift modules.
@discardableResult
private func packageModuleMaps(inFrameworks frameworks: [URL],
frameworkName: String,
moduleMapContents: String,
destination: URL,
buildingCarthage: Bool = false) -> Bool {
// CocoaPods does not put dependent frameworks and libraries into the module maps it generates.
// Instead it use build options to specify them. For the zip build, we need the module maps to
// include the dependent frameworks and libraries. Therefore we reconstruct them by parsing
// the CocoaPods config files and add them here.
// In the case of a mixed language framework, not only are the Swift module
// files copied, but a `module.modulemap` is created by combining the given
// module map contents and a synthesized submodule that modularizes the
// generated Swift header.
if makeSwiftModuleMap(thinFrameworks: frameworks,
frameworkName: frameworkName,
destination: destination,
moduleMapContents: moduleMapContents,
buildingCarthage: buildingCarthage) {
return buildingCarthage
}
// Copy the module map to the destination.
let moduleDir = destination.appendingPathComponent("Modules")
do {
try FileManager.default.createDirectory(at: moduleDir, withIntermediateDirectories: true)
} catch {
let frameworkName: String = frameworks.first?.lastPathComponent ?? "<UNKNOWN"
fatalError("Could not create Modules directory for framework: \(frameworkName). \(error)")
}
let modulemap = moduleDir.appendingPathComponent("module.modulemap")
do {
try moduleMapContents.write(to: modulemap, atomically: true, encoding: .utf8)
} catch {
let frameworkName: String = frameworks.first?.lastPathComponent ?? "<UNKNOWN"
fatalError("Could not write modulemap to disk for \(frameworkName): \(error)")
}
return false
}
/// URLs pointing to the frameworks containing architecture specific code.
/// Returns true if there are Swift modules.
private func makeSwiftModuleMap(thinFrameworks: [URL],
frameworkName: String,
destination: URL,
moduleMapContents: String,
buildingCarthage: Bool = false) -> Bool {
let fileManager = FileManager.default
for thinFramework in thinFrameworks {
// Get the Modules directory. The Catalyst one is a symbolic link.
let moduleDir = thinFramework.appendingPathComponent("Modules").resolvingSymlinksInPath()
do {
let files = try fileManager.contentsOfDirectory(at: moduleDir,
includingPropertiesForKeys: nil)
.compactMap { $0.path }
let swiftModules = files.filter { $0.hasSuffix(".swiftmodule") }
if swiftModules.isEmpty {
return false
} else if buildingCarthage {
return true
}
guard let first = swiftModules.first,
let swiftModule = URL(string: first) else {
fatalError("Failed to get swiftmodule in \(moduleDir).")
}
let destModuleDir = destination.appendingPathComponent("Modules")
if !fileManager.directoryExists(at: destModuleDir) {
do {
try fileManager.copyItem(at: moduleDir, to: destModuleDir)
} catch {
fatalError("Could not copy Modules from \(moduleDir) to " +
"\(destModuleDir): \(error)")
}
} else {
// If the Modules directory is already there, only copy in the architecture specific files
// from the *.swiftmodule subdirectory.
do {
let files = try fileManager.contentsOfDirectory(at: swiftModule,
includingPropertiesForKeys: nil)
.compactMap { $0.path }
let destSwiftModuleDir = destModuleDir
.appendingPathComponent(swiftModule.lastPathComponent)
for file in files {
let fileURL = URL(fileURLWithPath: file)
let projectDir = swiftModule.appendingPathComponent("Project")
if fileURL.lastPathComponent == "Project",
fileManager.directoryExists(at: projectDir) {
// The Project directory (introduced with Xcode 11.4) already exists, only copy in
// new contents.
let projectFiles = try fileManager.contentsOfDirectory(at: projectDir,
includingPropertiesForKeys: nil)
.compactMap { $0.path }
let destProjectDir = destSwiftModuleDir.appendingPathComponent("Project")
for projectFile in projectFiles {
let projectFileURL = URL(fileURLWithPath: projectFile)
do {
try fileManager.copyItem(at: projectFileURL, to:
destProjectDir.appendingPathComponent(projectFileURL.lastPathComponent))
} catch {
fatalError("Could not copy Project file from \(projectFileURL) to " +
"\(destProjectDir): \(error)")
}
}
} else {
do {
try fileManager.copyItem(at: fileURL, to:
destSwiftModuleDir
.appendingPathComponent(fileURL.lastPathComponent))
} catch {
fatalError("Could not copy Swift module file from \(fileURL) to " +
"\(destSwiftModuleDir): \(error)")
}
}
}
} catch {
fatalError("Failed to get Modules directory contents - \(moduleDir):" +
"\(error.localizedDescription)")
}
}
do {
// If this point is reached, the framework contains a Swift module,
// so it's built from either Swift sources or Swift & C Family
// Language sources. Frameworks built from only Swift sources will
// contain only two headers: the CocoaPods-generated umbrella header
// and the Swift-generated Swift header. If the framework's `Headers`
// directory contains more than two resources, then it is assumed
// that the framework was built from mixed language sources because
// those additional headers are public headers for the C Family
// Language sources.
let headersDir = destination.appendingPathComponent("Headers")
let headers = try fileManager.contentsOfDirectory(
at: headersDir,
includingPropertiesForKeys: nil
)
if headers.count > 2 {
// It is assumed that the framework will always contain a
// `module.modulemap` (either CocoaPods generates it or a custom
// one was set in the podspec corresponding to the framework being
// processed) within the framework's `Modules` directory. The main
// module declaration within this `module.modulemap` should be
// replaced with the given module map contents that was computed to
// include frameworks and libraries that the framework slice
// depends on.
let newModuleMapContents = moduleMapContents + """
module \(frameworkName).Swift {
header "\(frameworkName)-Swift.h"
requires objc
}
"""
let modulemapURL = destination.appendingPathComponents(["Modules", "module.modulemap"])
try newModuleMapContents.write(to: modulemapURL, atomically: true, encoding: .utf8)
}
} catch {
fatalError(
"Error while synthesizing a mixed language framework's module map: \(error.localizedDescription)"
)
}
} catch {
fatalError("Error while enumerating files \(moduleDir): \(error.localizedDescription)")
}
}
return true
}
/// Groups slices for each platform into a minimal set of frameworks.
/// - Parameter withName: The framework name.
/// - Parameter isCarthage: Name the temp directory differently for Carthage.
/// - Parameter fromFolder: The almost complete framework folder. Includes Headers, Info.plist,
/// and Resources.
/// - Parameter slicedFrameworks: All the frameworks sliced by platform.
/// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
private func groupFrameworks(withName framework: String,
isCarthage: Bool,
fromFolder: URL,
slicedFrameworks: [TargetPlatform: URL],
moduleMapContents: String) -> ([URL]) {
let fileManager = FileManager.default
// Create a `.framework` for each of the thinArchives using the `fromFolder` as the base.
let platformFrameworksDir = fileManager.temporaryDirectory(
withName: isCarthage ? "carthage_frameworks" : "platform_frameworks"
)
if !fileManager.directoryExists(at: platformFrameworksDir) {
do {
try fileManager.createDirectory(at: platformFrameworksDir,
withIntermediateDirectories: true)
} catch {
fatalError("Could not create a temp directory to store all thin frameworks: \(error)")
}
}
// Group the thin frameworks into three groups: device, simulator, and Catalyst (all represented
// by the `TargetPlatform` enum. The slices need to be packaged that way with lipo before
// creating a .framework that works for similar grouped architectures. If built separately,
// `-create-xcframework` will return an error and fail:
// `Both ios-arm64 and ios-armv7 represent two equivalent library definitions`
var frameworksBuilt: [URL] = []
for (platform, frameworkPath) in slicedFrameworks {
// Create the following structure in the platform frameworks directory:
// - platform_frameworks
// └── $(PLATFORM)
// └── $(FRAMEWORK).framework
let platformFrameworkDir = platformFrameworksDir
.appendingPathComponent(platform.buildName)
.appendingPathComponent(fromFolder.lastPathComponent)
do {
try fileManager.createDirectory(at: platformFrameworkDir, withIntermediateDirectories: true)
} catch {
fatalError("Could not create directory for architecture slices on \(platform) for " +
"\(framework): \(error)")
}
processPrivacyManifests(fileManager, frameworkPath, platformFrameworkDir)
// Headers from slice
do {
let headersSrc: URL = frameworkPath.appendingPathComponent("Headers")
.resolvingSymlinksInPath()
// The macOS slice's `Headers` directory may have a `Headers` file in
// it that symbolically links to nowhere. For example, in the 8.0.0
// zip distribution, see the `Headers` directory in the macOS slice
// of the `PromisesObjC.xcframework`. Delete it here to avoid putting
// it in the zip or crashing the Carthage hash generation. Because
// this will throw an error for cases where the file does not exist,
// the error is ignored.
try? fileManager.removeItem(at: headersSrc.appendingPathComponent("Headers"))
try fileManager.copyItem(
at: headersSrc,
to: platformFrameworkDir.appendingPathComponent("Headers")
)
} catch {
fatalError("Could not create framework directory needed to build \(framework): \(error)")
}
// Copy the binary and Info.plist to the right location.
let binaryName = frameworkPath.lastPathComponent.replacingOccurrences(of: ".framework",
with: "")
let fatBinary = frameworkPath.appendingPathComponent(binaryName).resolvingSymlinksInPath()
let plistPathComponents = {
if platform == .catalyst || platform == .macOS {
// Frameworks for macOS and macCatalyst have a different directory
// structure so the framework-level `Info.plist` is found in a
// different spot.
return ["Versions", "A", "Resources", "Info.plist"]
} else {
return ["Info.plist"]
}
}()
let infoPlist = frameworkPath.appendingPathComponents(plistPathComponents)
.resolvingSymlinksInPath()
let infoPlistDestination = platformFrameworkDir.appendingPathComponent("Info.plist")
let fatBinaryDestination = platformFrameworkDir.appendingPathComponent(framework)
do {
try fileManager.copyItem(at: fatBinary, to: fatBinaryDestination)
} catch {
fatalError("Could not copy fat binary to framework directory for \(framework): \(error)")
}
do {
// The minimum OS version is set to 100.0 to work around b/327020913.
// TODO(ncooke3): Revert this logic once b/327020913 is fixed.
var plistDictionary = try PropertyListSerialization.propertyList(
from: Data(contentsOf: infoPlist), format: nil
) as! [AnyHashable: Any]
plistDictionary["MinimumOSVersion"] = "100.0"
let updatedPlistData = try PropertyListSerialization.data(
fromPropertyList: plistDictionary,
format: .xml,
options: 0
)
try updatedPlistData.write(to: infoPlistDestination)
} catch {
fatalError(
"Could not copy framework-level plist to framework directory for \(framework): \(error)"
)
}
// Use the appropriate moduleMaps
packageModuleMaps(inFrameworks: [frameworkPath],
frameworkName: framework,
moduleMapContents: moduleMapContents,
destination: platformFrameworkDir)
frameworksBuilt.append(platformFrameworkDir)
}
return frameworksBuilt
}
/// Process privacy manifests.
///
/// Move any privacy manifest-containing resource bundles into the platform framework.
func processPrivacyManifests(_ fileManager: FileManager,
_ frameworkPath: URL,
_ platformFrameworkDir: URL) {
try? fileManager.contentsOfDirectory(
at: frameworkPath.deletingLastPathComponent(),
includingPropertiesForKeys: nil
)
.filter { $0.pathExtension == "bundle" }
// TODO(ncooke3): Once the zip is built with Xcode 15, the following
// `filter` can be removed. The following block exists to preserve
// how resources (e.g. like FIAM's) are packaged for use in Xcode 14.
.filter { bundleURL in
let dirEnum = fileManager.enumerator(atPath: bundleURL.path)
var containsPrivacyManifest = false
while let relativeFilePath = dirEnum?.nextObject() as? String {
if relativeFilePath.hasSuffix("PrivacyInfo.xcprivacy") {
containsPrivacyManifest = true
break
}
}
return containsPrivacyManifest
}
// Bundles are moved rather than copied to prevent them from being
// packaged in a `Resources` directory at the root of the xcframework.
.forEach { try! fileManager.moveItem(
at: $0,
to: platformFrameworkDir.appendingPathComponent($0.lastPathComponent)
) }
}
/// Package the built frameworks into an XCFramework.
/// - Parameter withName: The framework name.
/// - Parameter frameworks: The grouped frameworks.
/// - Parameter xcframeworksDir: Location at which to build the xcframework.
/// - Parameter resourceContents: Location of the resources for this xcframework.
static func makeXCFramework(withName name: String,
frameworks: [URL],
xcframeworksDir: URL,
resourceContents: URL?) -> URL {
let xcframework = xcframeworksDir
.appendingPathComponent(frameworkBuildName(name) + ".xcframework")
// The arguments for the frameworks need to be separated.
var frameworkArgs: [String] = []
for frameworkBuilt in frameworks {
frameworkArgs.append("-framework")
frameworkArgs.append(frameworkBuilt.path)
}
let outputArgs = ["-output", xcframework.path]
let args = ["-create-xcframework"] + frameworkArgs + outputArgs
print("""
Building \(xcframework) with command:
/usr/bin/xcodebuild \(args.joined(separator: " "))
""")
let result = syncExec(command: "/usr/bin/xcodebuild", args: args, captureOutput: true)
switch result {
case let .error(code, output):
fatalError("Could not build xcframework for \(name) exit code \(code): \(output)")
case .success:
print("XCFramework for \(name) built successfully at \(xcframework).")
}
// xcframework resources are packaged at top of xcframework.
if let resourceContents = resourceContents {
let resourceDir = xcframework.appendingPathComponent("Resources")
do {
try ResourcesManager.moveAllBundles(inDirectory: resourceContents, to: resourceDir)
} catch {
fatalError("Could not move bundles into Resources directory while building \(name): " +
"\(error)")
}
}
return xcframework
}
}