Paul Beusterien преди 5 години
родител
ревизия
47c185b765

+ 97 - 0
ReleaseTooling/DEVELOP.md

@@ -0,0 +1,97 @@
+# Firebase Release Tools
+
+This project includes Firebase release tooling including a zip builder and a
+Firebase release candidate creation tool.
+
+The tools are designed to fail fast with an explanation of what went wrong, so
+you can fix issues or dig in without having to dig too deep into the code.
+
+## Zip Builder
+
+For general usage, see [README.md](README.md).
+
+### Firebase Release zip building
+
+If the `--zip-pods` option is not specified, the tool will build a Firebase zip distribution.
+
+For release engineers (Googlers packaging an upcoming Firebase release) these commands should also
+be used:
+-  `--custom-spec-repos sso://cpdc-internal/firebase`
+  - This pulls the latest podspecs from the CocoaPods staging area.
+- `--enable-carthage-build` Turns on generation of Carthage zips and json file updates.
+- `--keep-build-artifacts` Useful for debugging and verifying the zip build contents.
+
+Putting them all together, here's a common command to build a releaseable Zip file:
+
+```
+swift run zip-builder --update-pod-repo \
+--custom-spec-repos sso://cpdc-internal/firebase \
+--enable-carthage-build \
+--keep-build-artifacts
+```
+
+#### Carthage
+
+Carthage binaries can also be built at the same time as the zip file by passing in `--enable-carthage-build`
+as a command line argument. This directory should contain JSON files describing versions and download
+locations for each product. This will result in a folder called "carthage" at the root where the zip directory exists
+containing all the zip files and JSON files necessary for distribution.
+
+## Firebase Releaser
+
+Provides several functions for staging a Firebase release candidate. See the internal go/firi link
+for the process documentation.
+
+### Launch Arguments
+
+See `main.swift` for information on specific launch arguments.
+
+You can pass in launch arguments with Xcode by selecting the  "firebase-releaser" scheme
+beside the Run/Stop buttons, clicking "Edit Scheme" and adding them in the "Arguments Passed On Launch"
+section.
+
+## Development Philosophy
+
+The following section describes the priorities taken while building this tool and should be followed
+for any modifications.
+
+### Readable and Maintainable
+This code will rarely be modified outside of bug fixes, but read very frequently. There should be no
+"magic lines" that do multiple things. Verbosity is preferred over making the code shorter and
+performing multiple actions at once. All functions should be well documented.
+
+### Avoid Calling bash Commands Where Possible
+Instead of using `cat`, `find`, `grep`, or `perl` use `String` APIs to read the contents of a file,
+`FileManager` to properly list contents of a directory, `RegularExpression` for pattern matching,
+etc. If there's a `Foundation` API available, it should be used.
+
+### Understandable Output
+The output of the script should make it immediately obvious if there were any issues and exactly
+what those issues were, without looking at the code. It should also be very clear if the Zip file
+was properly built and output the file location.
+
+### Show Xcode and API Output on Failures
+In the event that there's an Xcode build failure, the logs should be surfaced immediately to aid
+debugging. Release engineers should not have to find the Xcode project manually. That being said, a
+link to the Xcode project file should be logged as well in case it's necessary. Same goes for errors
+logged by exceptions (ex: `FileManager`).
+
+### Testable and Debuggable
+Components and functions should be split up in a way that make them easy to test and easy to debug.
+Prefer small functions that have proper failure conditions and input validated with `guard`
+statements, throwing `fatalError` with a well written error message if it's a critical issue that
+prevents the Zip file from being built properly.
+
+### Works from the Command Line or Xcode (Environment Agnostic)
+The script should be able to run from the command line to allow for easier automation and Xcode for
+simpler debugging and maintenance.
+
+### Any Failure Exits Immediately
+The script should not continue if anything necessary for a successful Zip file fails. This includes
+things like compiling storyboards, moving resources, missing files, etc. This is to ensure the
+integrity of the zip file and that any issues during testing are SDK bugs and not related to the
+files and folders.
+
+### Prefer File `URL`s over Strings
+Instead of relying on `String`s to represent file paths, use `URL`s as soon as possible to avoid any
+missed or double slashes along with other issues.

+ 20 - 98
ReleaseTooling/README.md

@@ -3,13 +3,14 @@
 This project includes Firebase release tooling including a zip builder and a
 Firebase release candidate creation tool.
 
-The tools are designed to fail fast with an explanation of what went wrong, so
-you can fix issues or dig in without having to dig too deep into the code.
+The rest of this file documents using the `zip-builder` tool. Information about the rest of the
+tools for managing Firebase releases and information about developing these tools is at
+[DEVELOP.md](DEVELOP.md)
 
 ## Zip Builder
 
-This is a Swift Package Manager project that allows users to package an iOS Zip file of binary
-packages.
+The `zip-builder` tool generates a zip distribution of binary `.xcframeworks` from an input set of
+CocoaPods.
 
 ### Requirements
 
@@ -21,10 +22,10 @@ In order to build the Zip file, you will need:
 
 ### Running the Tool
 
-You can run the tool with `swift run zip-builder [ARGS]` or generate an Xcode project with
-`swift package generate-xcodeproj` and run within Xcode.
+You can run the tool with `swift run zip-builder [ARGS]` or `open Package.swift` to debug or run
+within Xcode.
 
-Since Apple does not support linking libraries built by future Xcode versions, make sure to builid with the
+Since Apple does not support linking libraries built by future Xcode versions, make sure to build with the
 earliest Xcode needed by any of the library clients. The Xcode command line tools must also be configured
 for that version. Check with `xcodebuild -version`.
 
@@ -37,14 +38,12 @@ You can pass in launch arguments with Xcode by clicking "zip-builder" beside the
 
 #### Common Arguments
 
-These arguments assume you're running the command from the `ReleaseTooling` directory.
+Use `pods <pods>` to specify the CocoaPods to build.
 
-**Required** arguments:
+The `pods` option will choose whatever pods get installed from an unversioned CocoaPods install,
+typically the latest versions.
 
-- `--repo-dir <PATH_TO_firebase_ios_sdk_REPO>`
-  - The root of the `firebase-ios-sdk` repo.
-
-Typical argument (all use cases except Firebase release build):
+To explicitly specify the CocoaPods versions, use a JSON specification :
 - `--zip-pods <PATH_TO.json>`
   - This is a JSON list of the pods to consolidate into a zip of binary frameworks. For example,
 
@@ -64,90 +63,13 @@ Indicates to install the version 3.2.0 of "GoogleDataTransport" and the latest
 version of "FirebaseMessaging". The version string is optional and can be any
 valid [CocoaPods Podfile version specifier](https://guides.cocoapods.org/syntax/podfile.html#pod).
 
-
-Optional common arguments:
+Other optional arguments:
 - `--no-update-pod-repo`
   - This is for speedups when `pod repo update` has already been run recently.
-
-For release engineers (Googlers packaging an upcoming Firebase release) these commands should also be used:
--  `--custom-spec-repos sso://cpdc-internal/firebase`
-  - This pulls the latest podspecs from the CocoaPods staging area.
-- `--repo-dir path` GitHub repo containing Template and Carthage json file inputs.
-- `--enable-carthage-build` Turns on generation of Carthage zips and json file updates.
-- `--keep-build-artifacts` Useful for debugging and verifying the zip build contents.
-
-Putting them all together, here's a common command to build a releaseable Zip file:
-
-```
-swift run zip-builder --update-pod-repo \
---repo-dir <PATH_TO_current.firebase_ios_sdk.repo> \
---custom-spec-repos sso://cpdc-internal/firebase \
---enable-carthage-build \
---keep-build-artifacts
-```
-
-### Carthage
-
-Carthage binaries can also be built at the same time as the zip file by passing in `--enable-carthage-build`
-as a command line argument. This directory should contain JSON files describing versions and download
-locations for each product. This will result in a folder called "carthage" at the root where the zip directory exists
-containing all the zip files and JSON files necessary for distribution.
-
-## Firebase Releaser
-
-Provides several functions for staging a Firebase release candidate. See the internal go/firi link
-for the process documentation.
-
-### Launch Arguments
-
-See `main.swift`  for information on specific launch arguments.
-
-You can pass in launch arguments with Xcode by selecting the  "firebase-releaser" scheme
-beside the Run/Stop buttons, clicking "Edit Scheme" and adding them in the "Arguments Passed On Launch"
-section.
-
-## Development Philosophy
-
-The following section describes the priorities taken while building this tool and should be followed
-for any modifications.
-
-### Readable and Maintainable
-This code will rarely be modified outside of bug fixes, but read very frequently. There should be no
-"magic lines" that do multiple things. Verbosity is preferred over making the code shorter and
-performing multiple actions at once. All functions should be well documented.
-
-### Avoid Calling bash Commands Where Possible
-Instead of using `cat`, `find`, `grep`, or `perl` use `String` APIs to read the contents of a file,
-`FileManager` to properly list contents of a directory, `RegularExpression` for pattern matching,
-etc. If there's a `Foundation` API available, it should be used.
-
-### Understandable Output
-The output of the script should make it immediately obvious if there were any issues and exactly
-what those issues were, without looking at the code. It should also be very clear if the Zip file
-was properly built and output the file location.
-
-### Show Xcode and API Output on Failures
-In the event that there's an Xcode build failure, the logs should be surfaced immediately to aid
-debugging. Release engineers should not have to find the Xcode project manually. That being said, a
-link to the Xcode project file should be logged as well in case it's necessary. Same goes for errors
-logged by exceptions (ex: `FileManager`).
-
-### Testable and Debuggable
-Components and functions should be split up in a way that make them easy to test and easy to debug.
-Prefer small functions that have proper failure conditions and input validated with `guard`
-statements, throwing `fatalError` with a well written error message if it's a critical issue that
-prevents the Zip file from being built properly.
-
-### Works from the Command Line or Xcode (Environment Agnostic)
-The script should be able to run from the command line to allow for easier automation and Xcode for
-simpler debugging and maintenance.
-
-### Any Failure Exits Immediately
-The script should not continue if anything necessary for a successful Zip file fails. This includes
-things like compiling storyboards, moving resources, missing files, etc. This is to ensure the
-integrity of the zip file and that any issues during testing are SDK bugs and not related to the
-files and folders.
-
-### Prefer File `URL`s over Strings
-Instead of relying on `String`s to represent file paths, use `URL`s as soon as possible to avoid any
-missed or double slashes along with other issues.
+- `--minimum-ios-version <minimum-ios-version>`
+  - Change the minimimum iOS version from the default of 10.
+- `--output-dir <output-dir>`
+  - The directory to copy the built Zip file. If this is not set, the path to the Zip file will
+  be logged to the console.
+- `--keep-build-artifacts`
+  - Keep the build artifacts.

+ 10 - 9
ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift

@@ -30,26 +30,27 @@ public let shared = Manifest(
     Pod("FirebaseCore"),
     Pod("FirebaseInstallations"),
     Pod("FirebaseInstanceID"),
-    Pod("GoogleAppMeasurement", isClosedSource: true),
-    Pod("FirebaseAnalytics", isClosedSource: true, zip: true),
+    Pod("GoogleAppMeasurement", isClosedSource: true, platforms: ["ios"]),
+    Pod("FirebaseAnalytics", isClosedSource: true, platforms: ["ios"], zip: true),
     Pod("FirebaseABTesting", zip: true),
     Pod("FirebaseRemoteConfig", zip: true),
-    Pod("FirebaseAppDistribution", isBeta: true, zip: true),
+    Pod("FirebaseAppDistribution", isBeta: true, platforms: ["ios"], zip: true),
     Pod("FirebaseAuth", zip: true),
     Pod("FirebaseCrashlytics", zip: true),
     Pod("FirebaseDatabase", zip: true),
-    Pod("FirebaseDynamicLinks", zip: true),
+    Pod("FirebaseDynamicLinks", platforms: ["ios"], zip: true),
     Pod("FirebaseFirestore", allowWarnings: true, zip: true),
     Pod("FirebaseFirestoreSwift", isBeta: true),
     Pod("FirebaseFunctions", zip: true),
-    Pod("FirebaseInAppMessaging", isBeta: true, zip: true),
+    Pod("FirebaseInAppMessaging", isBeta: true, platforms: ["ios"], zip: true),
     Pod("FirebaseMessaging", zip: true),
-    Pod("FirebasePerformance", zip: true),
+    Pod("FirebasePerformance", platforms: ["ios"], zip: true),
     Pod("FirebaseStorage", zip: true),
     Pod("FirebaseStorageSwift", isBeta: true),
-    Pod("FirebaseMLCommon", isClosedSource: true, isBeta: true),
-    Pod("FirebaseMLModelInterpreter", isClosedSource: true, isBeta: true, zip: true),
-    Pod("FirebaseMLVision", isClosedSource: true, isBeta: true, zip: true),
+    Pod("FirebaseMLCommon", isClosedSource: true, isBeta: true, platforms: ["ios"]),
+    Pod("FirebaseMLModelInterpreter", isClosedSource: true, isBeta: true, platforms: ["ios"],
+        zip: true),
+    Pod("FirebaseMLVision", isClosedSource: true, isBeta: true, platforms: ["ios"], zip: true),
     Pod("Firebase", allowWarnings: true, zip: true),
   ]
 )

+ 3 - 0
ReleaseTooling/Sources/FirebaseManifest/Pod.swift

@@ -23,6 +23,7 @@ public struct Pod {
   public let isBeta: Bool
   public let isFirebase: Bool
   public let allowWarnings: Bool // Allow validation warnings. Ideally these should all be false
+  public let platforms: Set<String> // Set of platforms to build this pod for
   public let podVersion: String? // Non-Firebase pods have their own version
   public let releasing: Bool // Non-Firebase pods may not release
   public let zip: Bool // Top level pod in Zip Distribution
@@ -32,6 +33,7 @@ public struct Pod {
        isBeta: Bool = false,
        isFirebase: Bool = true,
        allowWarnings: Bool = false,
+       platforms: Set<String> = ["ios", "macos", "tvos"],
        podVersion: String? = nil,
        releasing: Bool = true,
        zip: Bool = false) {
@@ -40,6 +42,7 @@ public struct Pod {
     self.isBeta = isBeta
     self.isFirebase = isFirebase
     self.allowWarnings = allowWarnings
+    self.platforms = platforms
     self.podVersion = podVersion
     self.releasing = releasing
     self.zip = zip

+ 38 - 11
ReleaseTooling/Sources/ZipBuilder/CocoaPodUtils.swift

@@ -36,13 +36,40 @@ enum CocoaPodUtils {
 
   // MARK: - Public API
 
-  struct VersionedPod: Decodable, CustomDebugStringConvertible {
+  // Codable is required because Decodable does not make CodingKeys available.
+  struct VersionedPod: Codable, CustomDebugStringConvertible {
     /// Public name of the pod.
     let name: String
 
     /// The version of the requested pod.
     let version: String?
 
+    /// Platforms supported
+    let platforms: Set<String>
+
+    init(name: String,
+         version: String?,
+         platforms: Set<String> = ["ios", "macos", "tvos"]) {
+      self.name = name
+      self.version = version
+      self.platforms = platforms
+    }
+
+    init(from decoder: Decoder) throws {
+      let container = try decoder.container(keyedBy: CodingKeys.self)
+      name = try container.decode(String.self, forKey: .name)
+      if let platforms = try container.decodeIfPresent(Set<String>.self, forKey: .platforms) {
+        self.platforms = platforms
+      } else {
+        platforms = ["ios", "macos", "tvos"]
+      }
+      if let version = try container.decodeIfPresent(String.self, forKey: .version) {
+        self.version = version
+      } else {
+        version = nil
+      }
+    }
+
     /// The debug description as required by `CustomDebugStringConvertible`.
     var debugDescription: String {
       var desc = name
@@ -141,7 +168,7 @@ enum CocoaPodUtils {
   /// - Parameters:
   ///   - pods: List of VersionedPods to install
   ///   - directory: Destination directory for the pods.
-  ///   - minimumIOSVersion: The minimum iOS version as a string. Ex. `10.0`.
+  ///   - platform: Install for one platform at a time.
   ///   - customSpecRepos: Additional spec repos to check for installation.
   ///   - linkage: Specifies the linkage type. When `forcedStatic` is used, for the module map
   ///        construction, we want pod names not module names in the generated OTHER_LD_FLAGS
@@ -150,7 +177,7 @@ enum CocoaPodUtils {
   @discardableResult
   static func installPods(_ pods: [VersionedPod],
                           inDir directory: URL,
-                          minimumIOSVersion: String,
+                          platform: Platform,
                           customSpecRepos: [URL]?,
                           localPodspecPath: URL?,
                           linkage: LinkageType) -> [String: PodInfo] {
@@ -171,7 +198,7 @@ enum CocoaPodUtils {
       try writePodfile(for: pods,
                        toDirectory: directory,
                        customSpecRepos: customSpecRepos,
-                       minimumIOSVersion: minimumIOSVersion,
+                       platform: platform,
                        localPodspecPath: localPodspecPath,
                        linkage: linkage)
     } catch let FileManager.FileError.directoryNotFound(path) {
@@ -300,7 +327,7 @@ enum CocoaPodUtils {
     }
   }
 
-  static func podInstallPrepare(inProjectDir projectDir: URL, paths: ZipBuilder.FilesystemPaths) {
+  static func podInstallPrepare(inProjectDir projectDir: URL, templateDir: URL) {
     do {
       // Create the directory and all intermediate directories.
       try FileManager.default.createDirectory(at: projectDir, withIntermediateDirectories: true)
@@ -310,7 +337,7 @@ enum CocoaPodUtils {
     }
     // Copy the Xcode project needed in order to be able to install Pods there.
     let templateFiles = Constants.ProjectPath.requiredFilesForBuilding.map {
-      paths.templateDir.appendingPathComponent($0)
+      templateDir.appendingPathComponent($0)
     }
     for file in templateFiles {
       // Each file should be copied to the temporary project directory with the same name.
@@ -414,7 +441,7 @@ enum CocoaPodUtils {
   /// is not empty.
   private static func generatePodfile(for pods: [VersionedPod],
                                       customSpecsRepos: [URL]?,
-                                      minimumIOSVersion: String,
+                                      platform: Platform,
                                       localPodspecPath: URL?,
                                       linkage: LinkageType) -> String {
     // Start assembling the Podfile.
@@ -440,9 +467,9 @@ enum CocoaPodUtils {
       podfile += "  use_frameworks! :linkage => :static\n"
     }
 
-    // Include the minimum iOS version.
+    // Include the platform and its minimum version.
     podfile += """
-    platform :ios, '\(minimumIOSVersion)'
+    platform :\(platform.name), '\(platform.minimumVersion)'
     target 'FrameworkMaker' do\n
     """
 
@@ -504,7 +531,7 @@ enum CocoaPodUtils {
   private static func writePodfile(for pods: [VersionedPod],
                                    toDirectory directory: URL,
                                    customSpecRepos: [URL]?,
-                                   minimumIOSVersion: String,
+                                   platform: Platform,
                                    localPodspecPath: URL?,
                                    linkage: LinkageType) throws {
     guard FileManager.default.directoryExists(at: directory) else {
@@ -516,7 +543,7 @@ enum CocoaPodUtils {
     let path = directory.appendingPathComponent("Podfile")
     let podfile = generatePodfile(for: pods,
                                   customSpecsRepos: customSpecRepos,
-                                  minimumIOSVersion: minimumIOSVersion,
+                                  platform: platform,
                                   localPodspecPath: localPodspecPath,
                                   linkage: linkage)
     do {

+ 2 - 4
ReleaseTooling/Sources/ZipBuilder/FirebaseBuilder.swift

@@ -30,13 +30,11 @@ struct FirebaseBuilder {
 
   /// Wrapper around a generic zip builder that adds in Firebase specific steps including a
   /// multi-level zip file, a README, and optionally Carthage artifacts.
-  func build(in projectDir: URL,
-             minimumIOSVersion: String,
+  func build(templateDir: URL,
              carthageBuildOptions: CarthageBuildOptions?) {
     // Build the zip file and get the path.
     do {
-      let artifacts = try zipBuilder.buildAndAssembleFirebaseRelease(inProjectDir: projectDir,
-                                                                     minimumIOSVersion: minimumIOSVersion,
+      let artifacts = try zipBuilder.buildAndAssembleFirebaseRelease(templateDir: templateDir,
                                                                      includeCarthage: carthageBuildOptions !=
                                                                        nil)
       let firebaseVersion = artifacts.firebaseVersion

+ 124 - 260
ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift

@@ -17,67 +17,10 @@
 import Foundation
 import Utils
 
-/// The target platform that the framework is built for.
-enum TargetPlatform: CaseIterable {
-  /// Binaries to target iOS devices.
-  case iOSDevice
-  /// Binaries to target iOS simulators.
-  case iOSSimulator
-  /// Binaries to target Catalyst.
-  case catalyst
-
-  /// Valid architectures to be built for the platform.
-  var archs: [Architecture] {
-    switch self {
-    case .iOSDevice: return [.armv7, .arm64]
-    // Include arm64 slices in the simulator for Apple silicon Macs.
-    case .iOSSimulator: return [.i386, .x86_64, .arm64]
-    // TODO: Evaluate x86_64h slice. Previous builds were limited to x86_64.
-    case .catalyst: return [.x86_64, .arm64]
-    }
-  }
-
-  /// Flag to determine if bitcode should be used for the platform.
-  var shouldEnableBitcode: Bool {
-    switch self {
-    // TODO: Do we need to include bitcode for Catalyst? We weren't before the latest arm64 changes.
-    case .iOSDevice: return true
-    default: return false
-    }
-  }
-
-  /// Name of the SDK as used by `xcodebuild` for the platform.
-  var sdkName: String {
-    switch self {
-    case .iOSDevice: return "iphoneos"
-    case .iOSSimulator: return "iphonesimulator"
-    case .catalyst: return "macosx"
-    }
-  }
-
-  /// Name of the directory that builds go into, autogenerated from Xcode.
-  var buildDirName: String {
-    switch self {
-    case .iOSDevice: return "Release-iphoneos"
-    case .iOSSimulator: return "Release-iphonesimulator"
-    case .catalyst: return "Release-maccatalyst"
-    }
-  }
-}
-
-/// Different architectures to build frameworks for.
-enum Architecture: String, CaseIterable {
-  case arm64
-  case armv7
-  case i386
-  case x86_64
-  case x86_64h // x86_64h, Haswell, used for Mac Catalyst
-}
-
 /// A structure to build a .framework in a given project directory.
 struct FrameworkBuilder {
   /// Platforms to be included in the built frameworks.
-  private let platforms: [TargetPlatform]
+  private let targetPlatforms: [TargetPlatform]
 
   /// The directory containing the Xcode project and Pods folder.
   private let projectDir: URL
@@ -86,7 +29,7 @@ struct FrameworkBuilder {
   private let dynamicFrameworks: Bool
 
   /// Flag for whether or not Carthage artifacts should be built as well.
-  private let includeCarthage: Bool
+  private let buildCarthage: Bool
 
   /// The Pods directory for building the framework.
   private var podsDir: URL {
@@ -94,75 +37,51 @@ struct FrameworkBuilder {
   }
 
   /// Default initializer.
-  init(projectDir: URL, platforms: [TargetPlatform], includeCarthage: Bool,
+  init(projectDir: URL, platform: Platform, includeCarthage: Bool,
        dynamicFrameworks: Bool) {
     self.projectDir = projectDir
-    self.platforms = platforms
-    self.includeCarthage = includeCarthage
+    targetPlatforms = platform.platformTargets
+    buildCarthage = includeCarthage && platform == .iOS
     self.dynamicFrameworks = dynamicFrameworks
   }
 
   // MARK: - Public Functions
 
-  /// Build a fat library framework file for a given framework name.
+  /// 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.
   ///
-  /// - Parameters:
-  ///   - framework: The name of the Framework being built.
-  ///   - version: String representation of the version.
+  /// - Parameter framework: The name of the framework to be built.
   /// - Parameter logsOutputDir: The path to the directory to place build logs.
   /// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
-  /// - Returns: A URL to the framework that was built (or pulled from the cache) and the Carthage version.
-  func buildFramework(withName podName: String,
-                      podInfo: CocoaPodUtils.PodInfo,
-                      logsOutputDir: URL? = nil) -> (URL, URL?) {
-    print("Building \(podName)")
-
-    // Get (or create) the cache directory for storing built frameworks.
+  /// - Returns: A path to the newly compiled frameworks, the Carthage frameworks, and Resources.
+  func compileFrameworkAndResources(withName framework: String,
+                                    logsOutputDir: URL? = nil,
+                                    podInfo: CocoaPodUtils.PodInfo) -> ([URL], URL?, URL?) {
     let fileManager = FileManager.default
-    var cachedFrameworkRoot: URL
-    var cachedCarthageRoot: URL
-    do {
-      let cacheDir = try fileManager.sourcePodCacheDirectory(withSubdir: "zip")
-      let carthageCacheDir = try fileManager.sourcePodCacheDirectory(withSubdir: "carthage")
-      cachedFrameworkRoot = cacheDir.appendingPathComponents([podName, podInfo.version])
-      cachedCarthageRoot = carthageCacheDir.appendingPathComponents([podName, podInfo.version])
-    } catch {
-      fatalError("Could not create caches directory for building frameworks: \(error)")
-    }
-
-    // Build the full cached framework path.
-    let realFramework = frameworkBuildName(podName)
-    let cachedFrameworkDir = cachedFrameworkRoot
-      .appendingPathComponent("\(realFramework).xcframework")
-    let cachedCarthageDir = cachedCarthageRoot.appendingPathComponent("\(realFramework).framework")
-    let (frameworkDir, carthageDir) = compileFrameworkAndResources(withName: podName,
-                                                                   podInfo: podInfo)
+    let outputDir = fileManager.temporaryDirectory(withName: "frameworks_being_built")
+    let logsDir = logsOutputDir ?? fileManager.temporaryDirectory(withName: "build_logs")
     do {
-      // Remove the previously cached framework if it exists, otherwise the `moveItem` call will
-      // fail.
-      fileManager.removeIfExists(at: cachedFrameworkDir)
-      fileManager.removeIfExists(at: cachedCarthageDir)
-
-      // Create the root cache directories if they don't exist.
-      if !fileManager.directoryExists(at: cachedFrameworkRoot) {
-        // If the root directory doesn't exist, create it so the `moveItem` will succeed.
-        try fileManager.createDirectory(at: cachedFrameworkRoot,
-                                        withIntermediateDirectories: true)
-      }
-      if !fileManager.directoryExists(at: cachedCarthageRoot) {
-        // If the root directory doesn't exist, create it so the `moveItem` will succeed.
-        try fileManager.createDirectory(at: cachedCarthageRoot,
-                                        withIntermediateDirectories: true)
+      // Remove the compiled frameworks directory, this isn't the cache we're using.
+      if fileManager.directoryExists(at: outputDir) {
+        try fileManager.removeItem(at: outputDir)
       }
 
-      // Move the newly built framework to the cache directory.
-      try fileManager.moveItem(at: frameworkDir, to: cachedFrameworkDir)
-      if let carthageDir = carthageDir {
-        try fileManager.moveItem(at: carthageDir, to: cachedCarthageDir)
+      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)
       }
-      return (cachedFrameworkDir, cachedCarthageDir)
     } catch {
-      fatalError("Could not move built frameworks into the cached frameworks directory: \(error)")
+      fatalError("Failure creating temporary directory while building \(framework): \(error)")
+    }
+
+    if dynamicFrameworks {
+      return (buildDynamicFrameworks(withName: framework, logsDir: logsDir, outputDir: outputDir),
+              nil, nil)
+    } else {
+      return buildStaticFrameworks(withName: framework, logsDir: logsDir, outputDir: outputDir,
+                                   podInfo: podInfo)
     }
   }
 
@@ -171,7 +90,9 @@ struct FrameworkBuilder {
   /// 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 func syncExec(command: String, args: [String] = [], captureOutput: Bool = false) -> Shell
+  private static func syncExec(command: String,
+                               args: [String] = [],
+                               captureOutput: Bool = false) -> Shell
     .Result {
     let task = Process()
     task.launchPath = command
@@ -231,39 +152,39 @@ struct FrameworkBuilder {
                                               setCarthage: Bool = false) -> [TargetPlatform: URL] {
     // Build every architecture and save the locations in an array to be assembled.
     var slicedFrameworks = [TargetPlatform: URL]()
-    for platform in platforms {
-      let buildDir = projectDir.appendingPathComponent(platform.sdkName)
+    for targetPlatform in targetPlatforms {
+      let buildDir = projectDir.appendingPathComponent(targetPlatform.buildName)
       let sliced = buildSlicedFramework(withName: framework,
-                                        platform: platform,
+                                        targetPlatform: targetPlatform,
                                         buildDir: buildDir,
                                         logRoot: logsDir,
                                         setCarthage: setCarthage)
-      slicedFrameworks[platform] = sliced
+      slicedFrameworks[targetPlatform] = sliced
     }
     return slicedFrameworks
   }
 
-  /// Uses `xcodebuild` to build a framework for a specific platform.
+  /// Uses `xcodebuild` to build a framework for a specific target platform.
   ///
   /// - Parameters:
   ///   - framework: Name of the framework being built.
-  ///   - platform: The platform to target for the build.
+  ///   - 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 CoreDiagnostics for metrics.
   /// - Returns: A URL to the framework that was built.
   private func buildSlicedFramework(withName framework: String,
-                                    platform: TargetPlatform,
+                                    targetPlatform: TargetPlatform,
                                     buildDir: URL,
                                     logRoot: URL,
                                     setCarthage: Bool = false) -> URL {
-    let isMacCatalyst = platform == .catalyst
+    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)"
-    let archs = platform.archs.map { $0.rawValue }.joined(separator: " ")
+    let archs = targetPlatform.archs.map { $0.rawValue }.joined(separator: " ")
 
     var args = ["build",
                 "-configuration", "release",
@@ -281,10 +202,10 @@ struct FrameworkBuilder {
                 "BUILD_LIBRARY_FOR_DISTRIBUTION=YES",
                 "SUPPORTS_MACCATALYST=\(isMacCatalystString)",
                 "BUILD_DIR=\(buildDir.path)",
-                "-sdk", platform.sdkName,
+                "-sdk", targetPlatform.sdkName,
                 cFlags]
     // Add bitcode option for platforms that need it.
-    if platform.shouldEnableBitcode {
+    if targetPlatform.shouldEnableBitcode {
       args.append("BITCODE_GENERATION_MODE=bitcode")
     }
     // Code signing isn't needed for libraries. Disabling signing is required for
@@ -294,38 +215,40 @@ struct FrameworkBuilder {
       args.append("CODE_SIGN_IDENTITY=-")
     }
     print("""
-    Compiling \(framework) for \(platform.sdkName) (\(archs)) with command:
+    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)-\(platform.sdkName).txt"
+    let logFileName = "\(framework)-\(targetPlatform.buildName).txt"
     let logFile = logRoot.appendingPathComponent(logFileName)
 
-    let result = syncExec(command: "/usr/bin/xcodebuild", args: args, captureOutput: true)
+    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 \(platform.sdkName). Code: \(code). See the " +
-        "build log at \(logFile)")
+      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 \(platform.sdkName). Build log can be found at \(logFile)
+      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([platform.buildDirName, framework])
+      let frameworkPath = buildDir.appendingPathComponents([targetPlatform.buildDirName, framework])
       var actualFramework: String
       do {
         let files = try FileManager.default.contentsOfDirectory(at: frameworkPath,
@@ -363,98 +286,41 @@ struct FrameworkBuilder {
   }
 
   /// 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 logsOutputDir: 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 Carthage version if needed).
-  private func compileFrameworkAndResources(withName framework: String,
-                                            logsOutputDir: URL? = nil,
-                                            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 (buildDynamicXCFramework(withName: framework, logsDir: logsDir, outputDir: outputDir),
-              nil)
-    } else {
-      return buildStaticXCFramework(withName: framework, logsDir: logsDir, outputDir: outputDir,
-                                    podInfo: podInfo)
-    }
-  }
-
-  /// 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.
+  /// 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 framework (with any included Resources embedded).
-  private func buildDynamicXCFramework(withName framework: String,
-                                       logsDir: URL,
-                                       outputDir: URL) -> URL {
+  /// - 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 platform in TargetPlatform.allCases {
-      let buildDir = projectDir.appendingPathComponent(platform.sdkName)
+    for targetPlatform in TargetPlatform.allCases {
+      let buildDir = projectDir.appendingPathComponent(targetPlatform.buildName)
       let slicedFramework = buildSlicedFramework(withName: framework,
-                                                 platform: platform,
+                                                 targetPlatform: targetPlatform,
                                                  buildDir: buildDir,
                                                  logRoot: logsDir)
       thinFrameworks.append(slicedFramework)
     }
-
-    let frameworkDir = outputDir.appendingPathComponent("\(framework).xcframework")
-
-    let inputArgs = thinFrameworks.flatMap { url -> [String] in
-      ["-framework", url.path]
-    }
-
-    print("About to create xcframework for \(frameworkDir.path) with \(inputArgs)")
-
-    let result = syncExec(command: "/usr/bin/xcodebuild",
-                          args: ["-create-xcframework", "-output", frameworkDir.path] + inputArgs)
-    switch result {
-    case let .error(code, output):
-      fatalError("""
-      xcodebuild -create-xcframework command exited with \(code) when trying to build \(framework). Output:
-      \(output)
-      """)
-    case .success:
-      print("xcodebuild -create-xcframework command for \(framework) succeeded.")
-    }
-
-    return frameworkDir
+    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.
+  /// 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 (with any included Resources embedded).
-  private func buildStaticXCFramework(withName framework: String,
-                                      logsDir: URL,
-                                      outputDir: URL,
-                                      podInfo: CocoaPodUtils.PodInfo) -> (URL, URL?) {
+  /// - Returns: A path to the newly compiled framework, the Carthage version, and the Resource URL.
+  private func buildStaticFrameworks(withName framework: String,
+                                     logsDir: URL,
+                                     outputDir: URL,
+                                     podInfo: CocoaPodUtils.PodInfo) -> ([URL], URL?, URL) {
     // Build every architecture and save the locations in an array to be assembled.
     let slicedFrameworks = buildFrameworksForAllPlatforms(withName: framework, logsDir: logsDir)
 
@@ -469,11 +335,13 @@ struct FrameworkBuilder {
     }
 
     // Find the location of the public headers, any platform will do.
-    guard let anyPlatform = platforms.first, let archivePath = slicedFrameworks[anyPlatform] else {
+    guard let anyPlatform = targetPlatforms.first,
+      let archivePath = slicedFrameworks[anyPlatform] else {
       fatalError("Could not get a path to an archive to fetch headers in \(framework).")
     }
 
-    let headersDir = archivePath.appendingPathComponent("Headers")
+    // Get the framework Headers directory. On macOS, it's a symbolic link.
+    let headersDir = archivePath.appendingPathComponent("Headers").resolvingSymlinksInPath()
 
     // Find CocoaPods generated umbrella header.
     var umbrellaHeader = ""
@@ -538,7 +406,7 @@ struct FrameworkBuilder {
     // `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.sdkName,
+    let resourceContents = projectDir.appendingPathComponents([anyPlatform.buildName,
                                                                anyPlatform.buildDirName,
                                                                framework])
 
@@ -546,14 +414,13 @@ struct FrameworkBuilder {
       fatalError("Module map contents missing for framework \(framework)")
     }
     let moduleMapContents = moduleMapContentsTemplate.get(umbrellaHeader: umbrellaHeader)
-    let xcframework = packageXCFramework(withName: framework,
-                                         fromFolder: frameworkDir,
-                                         slicedFrameworks: slicedFrameworks,
-                                         resourceContents: resourceContents,
-                                         moduleMapContents: moduleMapContents)
+    let frameworks = groupFrameworks(withName: framework,
+                                     fromFolder: frameworkDir,
+                                     slicedFrameworks: slicedFrameworks,
+                                     moduleMapContents: moduleMapContents)
 
     var carthageFramework: URL?
-    if includeCarthage {
+    if buildCarthage {
       var carthageThinArchives: [TargetPlatform: URL]
       if framework == "FirebaseCoreDiagnostics" {
         // FirebaseCoreDiagnostics needs to be built with a different ifdef for the Carthage distro.
@@ -584,7 +451,7 @@ struct FrameworkBuilder {
         """)
       }
     }
-    return (xcframework, carthageFramework)
+    return (frameworks, carthageFramework, resourceContents)
   }
 
   /// Parses CocoaPods config files or uses the passed in `moduleMapContents` to write the
@@ -702,19 +569,16 @@ struct FrameworkBuilder {
     return true
   }
 
-  /// Packages an XCFramework based on an almost complete framework folder (missing the binary but
-  /// includes everything else needed) and thin archives for each architecture slice.
+  /// Groups slices for each platform into a minimal set of frameworks.
   /// - Parameter withName: The framework name.
   /// - Parameter fromFolder: The almost complete framework folder. Includes Headers, Info.plist,
   /// and Resources.
   /// - Parameter slicedFrameworks: All the frameworks sliced by platform.
-  /// - Parameter resourceContents: Location of the resources for this xcframework.
   /// - Parameter moduleMapContents: Module map contents for all frameworks in this pod.
-  private func packageXCFramework(withName framework: String,
-                                  fromFolder: URL,
-                                  slicedFrameworks: [TargetPlatform: URL],
-                                  resourceContents: URL,
-                                  moduleMapContents: String) -> URL {
+  private func groupFrameworks(withName framework: String,
+                               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.
@@ -736,7 +600,7 @@ struct FrameworkBuilder {
     // `Both ios-arm64 and ios-armv7 represent two equivalent library definitions`
     var frameworksBuilt: [URL] = []
     for (platform, frameworkPath) in slicedFrameworks {
-      let platformDir = platformFrameworksDir.appendingPathComponent(platform.sdkName)
+      let platformDir = platformFrameworksDir.appendingPathComponent(platform.buildName)
       do {
         try fileManager.createDirectory(at: platformDir, withIntermediateDirectories: true)
       } catch {
@@ -770,26 +634,23 @@ struct FrameworkBuilder {
 
       frameworksBuilt.append(destination)
     }
+    return frameworksBuilt
+  }
 
-    // We now need to package those built frameworks into an XCFramework.
-    let xcframeworksDir = projectDir.appendingPathComponent("xcframeworks")
-    if !fileManager.directoryExists(at: xcframeworksDir) {
-      do {
-        try fileManager.createDirectory(at: xcframeworksDir,
-                                        withIntermediateDirectories: true)
-      } catch {
-        fatalError("Could not create XCFrameworks directory: \(error)")
-      }
-    }
-
-    let xcframework = xcframeworksDir.appendingPathComponent(framework + ".xcframework")
-    if fileManager.fileExists(atPath: xcframework.path) {
-      try! fileManager.removeItem(at: xcframework)
-    }
+  /// 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(name + ".xcframework")
 
     // The arguments for the frameworks need to be separated.
     var frameworkArgs: [String] = []
-    for frameworkBuilt in frameworksBuilt {
+    for frameworkBuilt in frameworks {
       frameworkArgs.append("-framework")
       frameworkArgs.append(frameworkBuilt.path)
     }
@@ -803,23 +664,21 @@ struct FrameworkBuilder {
     let result = syncExec(command: "/usr/bin/xcodebuild", args: args, captureOutput: true)
     switch result {
     case let .error(code, output):
-      fatalError("Could not build xcframework for \(framework) exit code \(code): \(output)")
+      fatalError("Could not build xcframework for \(name) exit code \(code): \(output)")
 
     case .success:
-      print("XCFramework for \(framework) built successfully at \(xcframework).")
+      print("XCFramework for \(name) built successfully at \(xcframework).")
     }
-    // xcframework resources are packaged at top of xcframework. We do a copy instead of a move
-    // because the resources may also be copied for the Carthage distribution.
-    let resourceDir = xcframework.appendingPathComponent("Resources")
-    do {
-      try ResourcesManager.moveAllBundles(inDirectory: resourceContents,
-                                          to: resourceDir,
-                                          keepOriginal: true)
-    } catch {
-      fatalError("Could not move bundles into Resources directory while building \(framework): " +
-        "\(error)")
+    // 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
   }
 
@@ -858,8 +717,10 @@ struct FrameworkBuilder {
     // Build the fat archive using the `lipo` command to make one fat binary that Carthage can use
     // in the framework. We need the full archive path.
     let fatArchive = platformFrameworksDir.appendingPathComponent(framework)
-    let result = syncExec(command: "/usr/bin/lipo", args: ["-create", "-output", fatArchive.path] +
-      thinSlices.map { $0.value.path })
+    let result = FrameworkBuilder.syncExec(
+      command: "/usr/bin/lipo",
+      args: ["-create", "-output", fatArchive.path] + thinSlices.map { $0.value.path }
+    )
     switch result {
     case let .error(code, output):
       fatalError("""
@@ -886,9 +747,12 @@ struct FrameworkBuilder {
                       destination: frameworkDir)
 
     // Carthage Resources are packaged in the framework.
+    // Copy them instead of moving them, since they'll still need to be copied into the xcframework.
     let resourceDir = frameworkDir.appendingPathComponent("Resources")
     do {
-      try ResourcesManager.moveAllBundles(inDirectory: resourceContents, to: resourceDir)
+      try ResourcesManager.moveAllBundles(inDirectory: resourceContents,
+                                          to: resourceDir,
+                                          keepOriginal: true)
     } catch {
       fatalError("Could not move bundles into Resources directory while building \(framework): " +
         "\(error)")
@@ -949,10 +813,10 @@ struct FrameworkBuilder {
         fileManager.removeIfExists(at: destination)
 
         // Use lipo to extract the architecture we're looking for.
-        let result = syncExec(command: "/usr/bin/lipo",
-                              args: [binary.path,
-                                     "-thin",
-                                     arch.rawValue, "-output", destination.path])
+        let result = FrameworkBuilder.syncExec(command: "/usr/bin/lipo",
+                                               args: [binary.path,
+                                                      "-thin", arch.rawValue,
+                                                      "-output", destination.path])
         switch result {
         case let .error(code, output):
           fatalError("""

+ 9 - 7
ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift

@@ -76,8 +76,8 @@ struct ModuleMapBuilder {
   /// Dictionary of installed pods required for this module.
   private var installedPods: [String: FrameworkInfo]
 
-  /// The minimum iOS version targeted for this framework. Ex. "10.0".
-  private let minimumIOSVersion: String
+  /// The platform for this build.
+  private let platform: Platform
 
   /// The path containing local podspec URLs, if specified.
   private let localPodspecPath: URL?
@@ -85,10 +85,10 @@ struct ModuleMapBuilder {
   /// Default initializer.
   init(customSpecRepos: [URL]?,
        selectedPods: [String: CocoaPodUtils.PodInfo],
-       minimumIOSVersion: String,
+       platform: Platform,
        paths: ZipBuilder.FilesystemPaths) {
     projectDir = FileManager.default.temporaryDirectory(withName: "module")
-    CocoaPodUtils.podInstallPrepare(inProjectDir: projectDir, paths: paths)
+    CocoaPodUtils.podInstallPrepare(inProjectDir: projectDir, templateDir: paths.templateDir)
 
     self.customSpecRepos = customSpecRepos
     allPods = selectedPods
@@ -102,7 +102,7 @@ struct ModuleMapBuilder {
     }
     self.installedPods = installedPods
 
-    self.minimumIOSVersion = minimumIOSVersion
+    self.platform = platform
     localPodspecPath = paths.localPodspecPath
   }
 
@@ -112,7 +112,9 @@ struct ModuleMapBuilder {
   ///
   func build() {
     for (_, info) in installedPods {
-      if info.isSourcePod == false || info.transitiveFrameworks != nil {
+      if info.isSourcePod == false ||
+        info.transitiveFrameworks != nil ||
+        info.versionedPod.name == "Firebase" {
         continue
       }
       generate(framework: info)
@@ -129,7 +131,7 @@ struct ModuleMapBuilder {
     let deps = CocoaPodUtils.transitiveVersionedPodDependencies(for: podName, in: allPods)
     _ = CocoaPodUtils.installPods(allSubspecList(framework: framework) + deps,
                                   inDir: projectDir,
-                                  minimumIOSVersion: minimumIOSVersion,
+                                  platform: platform,
                                   customSpecRepos: customSpecRepos,
                                   localPodspecPath: localPodspecPath,
                                   linkage: .forcedStatic)

+ 68 - 0
ReleaseTooling/Sources/ZipBuilder/Platform.swift

@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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
+
+// The supported platforms.
+enum Platform: CaseIterable {
+  case iOS
+  case macOS
+  case tvOS
+
+  var platformTargets: [TargetPlatform] {
+    switch self {
+    case .iOS: return [.iOSDevice, .iOSSimulator] + (SkipCatalyst.skip ? [] : [.catalyst])
+    case .macOS: return [.macOS]
+    case .tvOS: return [.tvOSDevice, .tvOSSimulator]
+    }
+  }
+
+  /// Name of the platform as used in Podfiles.
+  var name: String {
+    switch self {
+    case .iOS: return "ios"
+    case .macOS: return "macos"
+    case .tvOS: return "tvos"
+    }
+  }
+
+  /// Minimum supported version
+  var minimumVersion: String {
+    switch self {
+    case .iOS: return PlatformMinimum.minimumIOSVersion
+    case .macOS: return PlatformMinimum.minimumMacOSVersion
+    case .tvOS: return PlatformMinimum.minimumTVOSVersion
+    }
+  }
+}
+
+class PlatformMinimum {
+  fileprivate static var minimumIOSVersion = ""
+  fileprivate static var minimumMacOSVersion = ""
+  fileprivate static var minimumTVOSVersion = ""
+  static func initialize(ios: String, macos: String, tvos: String) {
+    minimumIOSVersion = ios
+    minimumMacOSVersion = macos
+    minimumTVOSVersion = tvos
+  }
+}
+
+class SkipCatalyst {
+  fileprivate static var skip = false
+  static func set() {
+    skip = true
+  }
+}

+ 99 - 0
ReleaseTooling/Sources/ZipBuilder/TargetPlatform.swift

@@ -0,0 +1,99 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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
+
+/// The target platform that the framework is built for.
+enum TargetPlatform: CaseIterable {
+  /// Binaries to target iOS devices.
+  case iOSDevice
+  /// Binaries to target iOS simulators.
+  case iOSSimulator
+  /// Binaries to target Catalyst.
+  case catalyst
+  /// Binaries to target macOS.
+  case macOS
+  /// Binaries to target tvOS.
+  case tvOSDevice
+  /// Binaries to target tvOS simulators.
+  case tvOSSimulator
+
+  /// Valid architectures to be built for the platform.
+  var archs: [Architecture] {
+    switch self {
+    case .iOSDevice: return [.armv7, .arm64]
+    // Include arm64 slices in the simulator for Apple silicon Macs.
+    case .iOSSimulator: return [.i386, .x86_64, .arm64]
+    // TODO: Evaluate x86_64h slice. Previous builds were limited to x86_64.
+    case .catalyst: return [.x86_64, .arm64]
+    case .macOS: return [.x86_64, .arm64]
+    case .tvOSDevice: return [.arm64]
+    case .tvOSSimulator: return [.x86_64, .arm64]
+    }
+  }
+
+  /// Flag to determine if bitcode should be used for the target platform.
+  var shouldEnableBitcode: Bool {
+    switch self {
+    // TODO: Do we need to include bitcode for Catalyst? We weren't before the latest arm64 changes.
+    case .iOSDevice: return true
+    case .macOS: return true
+    case .tvOSDevice: return true
+    default: return false
+    }
+  }
+
+  /// Name of the SDK as used by `xcodebuild` for the target platforms.
+  var sdkName: String {
+    switch self {
+    case .iOSDevice: return "iphoneos"
+    case .iOSSimulator: return "iphonesimulator"
+    case .catalyst: return "macosx"
+    case .macOS: return "macosx"
+    case .tvOSDevice: return "appletvos"
+    case .tvOSSimulator: return "appletvsimulator"
+    }
+  }
+
+  /// The build name. Distinguished from sdkName to disambiguate catalyst and macOS.
+  var buildName: String {
+    switch self {
+    case .catalyst: return "catalyst"
+    default: return sdkName
+    }
+  }
+
+  /// Name of the directory that builds go into, autogenerated from Xcode.
+  var buildDirName: String {
+    switch self {
+    case .iOSDevice: return "Release-iphoneos"
+    case .iOSSimulator: return "Release-iphonesimulator"
+    case .catalyst: return "Release-maccatalyst"
+    case .macOS: return "Release"
+    case .tvOSDevice: return "Release-appletvos"
+    case .tvOSSimulator: return "Release-appletvsimulator"
+    }
+  }
+}
+
+/// Different architectures to build frameworks for.
+enum Architecture: String, CaseIterable {
+  case arm64
+  case armv7
+  case i386
+  case x86_64
+  case x86_64h // x86_64h, Haswell, used for Mac Catalyst
+}

+ 145 - 131
ReleaseTooling/Sources/ZipBuilder/ZipBuilder.swift

@@ -129,8 +129,8 @@ struct ZipBuilder {
   /// Paths needed throughout the process of packaging the Zip file.
   public let paths: FilesystemPaths
 
-  /// The platforms to target for the builds.
-  public let platforms: [TargetPlatform]
+  /// The targetPlatforms to target for the builds.
+  public let platforms: [Platform]
 
   /// Specifies if the builder is building dynamic frameworks instead of static frameworks.
   private let dynamicFrameworks: Bool
@@ -148,7 +148,7 @@ struct ZipBuilder {
   ///         frameworks are built.
   ///   - customSpecRepo: A custom spec repo to be used for fetching CocoaPods from.
   init(paths: FilesystemPaths,
-       platforms: [TargetPlatform],
+       platforms: [Platform],
        dynamicFrameworks: Bool,
        customSpecRepos: [URL]? = nil) {
     self.paths = paths
@@ -162,8 +162,6 @@ struct ZipBuilder {
   /// - Parameter podsToInstall: All pods to install.
   /// - Returns: Arrays of pod install info and the frameworks installed.
   func buildAndAssembleZip(podsToInstall: [CocoaPodUtils.VersionedPod],
-                           inProjectDir projectDir: URL,
-                           minimumIOSVersion: String,
                            includeDependencies: Bool,
                            includeCarthage: Bool = false) ->
     ([String: CocoaPodUtils.PodInfo], [String: [URL]], [String: [URL]]?) {
@@ -178,71 +176,130 @@ struct ZipBuilder {
     // required frameworks, then use that as the source of frameworks to pull from when including
     // the folders in each product directory.
     let linkage: CocoaPodUtils.LinkageType = dynamicFrameworks ? .dynamic : .standardStatic
-    CocoaPodUtils.installPods(podsToInstall,
-                              inDir: projectDir,
-                              minimumIOSVersion: minimumIOSVersion,
-                              customSpecRepos: customSpecRepos,
-                              localPodspecPath: paths.localPodspecPath,
-                              linkage: linkage)
-
-    // Find out what pods were installed with the above commands.
-    let installedPods = CocoaPodUtils.installedPodsInfo(inProjectDir: projectDir,
-                                                        localPodspecPath: paths.localPodspecPath)
-
-    // If module maps are needed for static frameworks, build them here to be available to copy
-    // into the generated frameworks.
-    if !dynamicFrameworks {
-      ModuleMapBuilder(customSpecRepos: customSpecRepos,
-                       selectedPods: installedPods,
-                       minimumIOSVersion: minimumIOSVersion,
-                       paths: paths).build()
-    }
+    var groupedFrameworks: [String: [URL]] = [:]
+    var carthageToInstall: [String: [URL]] = [:]
+    var podsBuilt: [String: CocoaPodUtils.PodInfo] = [:]
+    var xcframeworks: [String: [URL]] = [:]
+    var resources: [String: URL] = [:]
+
+    for platform in platforms {
+      let includeCarthage = includeCarthage && platform == .iOS
+      let projectDir = FileManager.default.temporaryDirectory(withName: "project-" + platform.name)
+      CocoaPodUtils.podInstallPrepare(inProjectDir: projectDir, templateDir: paths.templateDir)
+
+      let platformPods = podsToInstall.filter { $0.platforms.contains(platform.name) }
+
+      CocoaPodUtils.installPods(platformPods,
+                                inDir: projectDir,
+                                platform: platform,
+                                customSpecRepos: customSpecRepos,
+                                localPodspecPath: paths.localPodspecPath,
+                                linkage: linkage)
+      // Find out what pods were installed with the above commands.
+      let installedPods = CocoaPodUtils.installedPodsInfo(inProjectDir: projectDir,
+                                                          localPodspecPath: paths.localPodspecPath)
+
+      // If module maps are needed for static frameworks, build them here to be available to copy
+      // into the generated frameworks.
+      if !dynamicFrameworks {
+        ModuleMapBuilder(customSpecRepos: customSpecRepos,
+                         selectedPods: installedPods,
+                         platform: platform,
+                         paths: paths).build()
+      }
+
+      let podsToBuild = includeDependencies ? installedPods : installedPods.filter {
+        platformPods.map { $0.name.components(separatedBy: "/").first }.contains($0.key)
+      }
 
-    let podsToBuild = includeDependencies ? installedPods : installedPods.filter {
-      podsToInstall.map { $0.name.components(separatedBy: "/").first }.contains($0.key)
+      for (podName, podInfo) in podsToBuild {
+        if podName == "Firebase" {
+          // Don't build the Firebase pod.
+        } else if podInfo.isSourcePod {
+          let builder = FrameworkBuilder(projectDir: projectDir,
+                                         platform: platform,
+                                         includeCarthage: includeCarthage,
+                                         dynamicFrameworks: dynamicFrameworks)
+          let (frameworks, carthageFramework, resourceContents) =
+            builder.compileFrameworkAndResources(withName: podName,
+                                                 logsOutputDir: paths.logsOutputDir,
+                                                 podInfo: podInfo)
+          groupedFrameworks[podName] = (groupedFrameworks[podName] ?? []) + frameworks
+          if platform == .iOS {
+            if let carthageFramework = carthageFramework {
+              carthageToInstall[podName] = [carthageFramework]
+            }
+          }
+          if resourceContents != nil {
+            resources[podName] = resourceContents
+          }
+        } else if podsBuilt[podName] == nil {
+          // Binary pods need to be collected once, since the platforms should already be merged.
+          let (binaryFrameworks, binaryCarthage) =
+            collectBinaryFrameworks(fromPod: podName,
+                                    podInfo: podInfo)
+          xcframeworks[podName] = binaryFrameworks
+          if includeCarthage {
+            carthageToInstall[podName] = binaryCarthage
+          }
+        }
+        // Union all pods built across platforms.
+        podsBuilt[podName] = podInfo
+      }
     }
 
-    // Generate the frameworks. Each key is the pod name and the URLs are all frameworks to be
-    // copied in each product's directory.
-    let (frameworks, carthageFrameworks) = generateFrameworks(fromPods: podsToBuild,
-                                                              inProjectDir: projectDir,
-                                                              includeCarthage: includeCarthage)
+    // Now consolidate the built frameworks for all platforms into a single xcframework.
+    let xcframeworksDir = FileManager.default.temporaryDirectory(withName: "xcframeworks")
+    do {
+      try FileManager.default.createDirectory(at: xcframeworksDir,
+                                              withIntermediateDirectories: false)
+    } catch {
+      fatalError("Could not create XCFrameworks directory: \(error)")
+    }
 
-    for (framework, paths) in frameworks {
+    for groupedFramework in groupedFrameworks {
+      let name = groupedFramework.key
+      let xcframework = FrameworkBuilder.makeXCFramework(withName: name,
+                                                         frameworks: groupedFramework.value,
+                                                         xcframeworksDir: xcframeworksDir,
+                                                         resourceContents: resources[name])
+      xcframeworks[name] = [xcframework]
+    }
+    for (framework, paths) in xcframeworks {
       print("Frameworks for pod: \(framework) were compiled at \(paths)")
     }
-    return (podsToBuild, frameworks, carthageFrameworks)
+    return (podsBuilt, xcframeworks, carthageToInstall)
   }
 
-  // TODO: This function contains a lot of "copy these paths to this directory, fail if there are
-  //   errors" code. It could probably be broken out into a cleaner interface or broken out into
-  //   separate functions.
   /// Try to build and package the contents of the Zip file. This will throw an error as soon as it
   /// encounters an error, or will quit due to a fatal error with the appropriate log.
   ///
-  /// - Returns: Information related to the built artifacts.
+  /// - Parameter templateDir: The template project for pod install.
+  /// - Parameter includeCarthage: Whether to build and package Carthage.
   /// - Throws: One of many errors that could have happened during the build phase.
-  func buildAndAssembleFirebaseRelease(inProjectDir projectDir: URL,
-                                       minimumIOSVersion: String,
+  func buildAndAssembleFirebaseRelease(templateDir: URL,
                                        includeCarthage: Bool) throws -> ReleaseArtifacts {
     let manifest = FirebaseManifest.shared
     var podsToInstall = manifest.pods.filter { $0.zip }.map {
-      CocoaPodUtils.VersionedPod(name: $0.name, version: manifest.versionString($0))
+      CocoaPodUtils.VersionedPod(name: $0.name,
+                                 version: manifest.versionString($0),
+                                 platforms: $0.platforms)
     }
     guard !podsToInstall.isEmpty else {
       fatalError("Failed to find versions for Firebase release")
     }
     // We don't release Google-Mobile-Ads-SDK and GoogleSignIn, but we include their latest
     // version for convenience in the Zip and Carthage builds.
-    podsToInstall.append(CocoaPodUtils.VersionedPod(name: "Google-Mobile-Ads-SDK", version: nil))
-    podsToInstall.append(CocoaPodUtils.VersionedPod(name: "GoogleSignIn", version: nil))
+    podsToInstall.append(CocoaPodUtils.VersionedPod(name: "Google-Mobile-Ads-SDK",
+                                                    version: nil,
+                                                    platforms: ["ios"]))
+    podsToInstall.append(CocoaPodUtils.VersionedPod(name: "GoogleSignIn",
+                                                    version: nil,
+                                                    platforms: ["ios"]))
 
     print("Final expected versions for the Zip file: \(podsToInstall)")
-
     let (installedPods, frameworks,
          carthageFrameworks) = buildAndAssembleZip(podsToInstall: podsToInstall,
-                                                   inProjectDir: projectDir,
-                                                   minimumIOSVersion: minimumIOSVersion,
                                                    // Always include dependencies for Firebase zips.
                                                    includeDependencies: true,
                                                    includeCarthage: includeCarthage)
@@ -254,16 +311,14 @@ struct ZipBuilder {
         "installed: \(installedPods)")
     }
 
-    let zipDir = try assembleDistributions(inProjectDir: projectDir,
-                                           withPackageKind: "Firebase",
+    let zipDir = try assembleDistributions(withPackageKind: "Firebase",
                                            podsToInstall: podsToInstall,
                                            installedPods: installedPods,
                                            frameworksToAssemble: frameworks,
                                            firebasePod: firebasePod)
     var carthageDir: URL?
     if let carthageFrameworks = carthageFrameworks {
-      carthageDir = try assembleDistributions(inProjectDir: projectDir,
-                                              withPackageKind: "CarthageFirebase",
+      carthageDir = try assembleDistributions(withPackageKind: "CarthageFirebase",
                                               podsToInstall: podsToInstall,
                                               installedPods: installedPods,
                                               frameworksToAssemble: carthageFrameworks,
@@ -290,8 +345,7 @@ struct ZipBuilder {
   ///
   /// - Returns: Return the URL of the folder containing the contents of the Zip or Carthage distribution.
   /// - Throws: One of many errors that could have happened during the build phase.
-  private func assembleDistributions(inProjectDir projectDir: URL,
-                                     withPackageKind packageKind: String,
+  private func assembleDistributions(withPackageKind packageKind: String,
                                      podsToInstall: [CocoaPodUtils.VersionedPod],
                                      installedPods: [String: CocoaPodUtils.PodInfo],
                                      frameworksToAssemble: [String: [URL]],
@@ -321,7 +375,6 @@ struct ZipBuilder {
       /// Example: ["FirebaseInstanceID", "GoogleAppMeasurement", "nanopb", <...>]
       let (dir, frameworks) = try installAndCopyFrameworks(forPod: "FirebaseAnalytics",
                                                            withInstalledPods: installedPods,
-                                                           projectDir: projectDir,
                                                            rootZipDir: zipDir,
                                                            builtFrameworks: frameworksToAssemble)
       analyticsFrameworks = frameworks
@@ -351,7 +404,6 @@ struct ZipBuilder {
         let (productDir, podFrameworks) =
           try installAndCopyFrameworks(forPod: pod.key,
                                        withInstalledPods: installedPods,
-                                       projectDir: projectDir,
                                        rootZipDir: zipDir,
                                        builtFrameworks: frameworksToAssemble,
                                        podsToIgnore: analyticsPods)
@@ -559,7 +611,6 @@ struct ZipBuilder {
   @discardableResult
   func installAndCopyFrameworks(forPod podName: String,
                                 withInstalledPods installedPods: [String: CocoaPodUtils.PodInfo],
-                                projectDir: URL,
                                 rootZipDir: URL,
                                 builtFrameworks: [String: [URL]],
                                 podsToIgnore: [String] = []) throws -> (productDir: URL,
@@ -640,15 +691,13 @@ struct ZipBuilder {
 
   // MARK: - Framework Generation
 
-  /// Generates all the .framework files from a Pods directory. This will go through the contents of
-  /// the directory, copy the .frameworks to a temporary directory and compile any source based
-  /// CocoaPods. Returns a dictionary with the framework name for the key and all information for
-  /// frameworks to install EXCLUDING resources, as they are handled later (if not included in the
-  /// .framework file already).
-  private func generateFrameworks(fromPods pods: [String: CocoaPodUtils.PodInfo],
-                                  inProjectDir projectDir: URL,
-                                  includeCarthage: Bool) -> ([String: [URL]],
-                                                             [String: [URL]]?) {
+  /// Collects the .framework and .xcframeworks files from the binary pods. This will go through
+  /// the contents of the directory and copy the .frameworks to a temporary directory. Returns a
+  /// dictionary with the framework name for the key and all information for frameworks to install
+  /// EXCLUDING resources, as they are handled later (if not included in the .framework file
+  /// already).
+  private func collectBinaryFrameworks(fromPod podName: String,
+                                       podInfo: CocoaPodUtils.PodInfo) -> ([URL], [URL]) {
     // Verify the Pods folder exists and we can get the contents of it.
     let fileManager = FileManager.default
 
@@ -666,78 +715,43 @@ struct ZipBuilder {
     } catch {
       fatalError("Cannot create temporary directory to store binary frameworks: \(error)")
     }
+    var frameworks: [URL] = []
+    var carthageFrameworks: [URL] = []
 
-    // Loop through each pod folder and check if the frameworks already exist, or they need to be
-    // compiled. If they exist, add them to the frameworks dictionary.
-    var toInstall: [String: [URL]] = [:]
-    var carthageToInstall: [String: [URL]] = [:]
-    for (podName, podInfo) in pods {
-      var frameworks: [URL] = []
-      var carthageFrameworks: [URL] = []
-      // Ignore the Firebase umbrella pod.
-      guard podName != "Firebase" else {
-        continue
-      }
+    // Package all resources into the frameworks since that's how Carthage needs it packaged.
+    do {
+      // TODO: Figure out if we need to exclude bundles here or not.
+      try ResourcesManager.packageAllResources(containedIn: podInfo.installedLocation)
+    } catch {
+      fatalError("Tried to package resources for \(podName) but it failed: \(error)")
+    }
 
-      // If it's an open source pod and we need to compile the source to get a framework.
-      if podInfo.isSourcePod {
-        let builder = FrameworkBuilder(projectDir: projectDir,
-                                       platforms: platforms,
-                                       includeCarthage: includeCarthage,
-                                       dynamicFrameworks: dynamicFrameworks)
-        let (framework, carthageFramework) = builder.buildFramework(withName: podName,
-                                                                    podInfo: podInfo,
-                                                                    logsOutputDir: paths
-                                                                      .logsOutputDir)
-
-        frameworks = [framework]
-        if let carthageFramework = carthageFramework {
-          carthageFrameworks = [carthageFramework]
-        }
-      } else {
-        // Package all resources into the frameworks since that's how Carthage needs it packaged.
-        do {
-          // TODO: Figure out if we need to exclude bundles here or not.
-          try ResourcesManager.packageAllResources(containedIn: podInfo.installedLocation)
-        } catch {
-          fatalError("Tried to package resources for \(podName) but it failed: \(error)")
-        }
+    // Copy each of the frameworks to a known temporary directory and store the location.
+    for framework in podInfo.binaryFrameworks {
+      // Copy it to the temporary directory and save it to our list of frameworks.
+      let zipLocation = binaryZipDir.appendingPathComponent(framework.lastPathComponent)
+      let carthageLocation =
+        binaryCarthageDir.appendingPathComponent(framework.lastPathComponent)
 
-        // Copy each of the frameworks to a known temporary directory and store the location.
-        for framework in podInfo.binaryFrameworks {
-          // Copy it to the temporary directory and save it to our list of frameworks.
-          let zipLocation = binaryZipDir.appendingPathComponent(framework.lastPathComponent)
-          let carthageLocation =
-            binaryCarthageDir.appendingPathComponent(framework.lastPathComponent)
-
-          // Remove the framework if it exists since it could be out of date.
-          fileManager.removeIfExists(at: zipLocation)
-          fileManager.removeIfExists(at: carthageLocation)
-          do {
-            try fileManager.copyItem(at: framework, to: zipLocation)
-            try fileManager.copyItem(at: framework, to: carthageLocation)
-          } catch {
-            fatalError("Cannot copy framework at \(framework) while " +
-              "attempting to generate frameworks. \(error)")
-          }
-          frameworks.append(zipLocation)
-
-          CarthageUtils.generatePlistContents(
-            forName: framework.lastPathComponent.components(separatedBy: ".").first!,
-            withVersion: podInfo.version,
-            to: carthageLocation
-          )
-          carthageFrameworks.append(carthageLocation)
-        }
+      // Remove the framework if it exists since it could be out of date.
+      fileManager.removeIfExists(at: zipLocation)
+      fileManager.removeIfExists(at: carthageLocation)
+      do {
+        try fileManager.copyItem(at: framework, to: zipLocation)
+        try fileManager.copyItem(at: framework, to: carthageLocation)
+      } catch {
+        fatalError("Cannot copy framework at \(framework) while " +
+          "attempting to generate frameworks. \(error)")
       }
-      toInstall[podName] = frameworks
-      carthageToInstall[podName] = carthageFrameworks
-    }
-
-    if includeCarthage {
-      return (toInstall, carthageToInstall)
-    } else {
-      return (toInstall, nil)
+      frameworks.append(zipLocation)
+
+      CarthageUtils.generatePlistContents(
+        forName: framework.lastPathComponent.components(separatedBy: ".").first!,
+        withVersion: podInfo.version,
+        to: carthageLocation
+      )
+      carthageFrameworks.append(carthageLocation)
     }
+    return (frameworks, carthageFrameworks)
   }
 }

+ 70 - 47
ReleaseTooling/Sources/ZipBuilder/main.swift

@@ -25,16 +25,15 @@ extension URL: ExpressibleByArgument {
 }
 
 // Enables parsing of platforms as a command line argument.
-extension TargetPlatform: ExpressibleByArgument {
+extension Platform: ExpressibleByArgument {
   public init?(argument: String) {
     // Look for a match in SDK name.
-    for platform in TargetPlatform.allCases {
-      if argument == platform.sdkName {
+    for platform in Platform.allCases {
+      if argument == platform.name {
         self = platform
         return
       }
     }
-
     return nil
   }
 }
@@ -80,6 +79,12 @@ struct ZipBuilderTool: ParsableCommand {
         help: ArgumentHelp("A flag to indicate keeping (not deleting) the build artifacts."))
   var keepBuildArtifacts: Bool
 
+  /// Flag to skip building the Catalyst slices.
+  @Flag(default: true,
+        inversion: .prefixedNo,
+        help: ArgumentHelp("A flag to indicate skip building the Catalyst slice."))
+  var includeCatalyst: Bool
+
   /// Flag to run `pod repo update` and `pod cache clean --all`.
   @Flag(default: true,
         inversion: .prefixedNo,
@@ -93,7 +98,7 @@ struct ZipBuilderTool: ParsableCommand {
   /// Custom CocoaPods spec repos to be used.
   @Option(parsing: .upToNextOption,
           help: ArgumentHelp("""
-          A list of custom CocoaPod Spec repos.  If not provided, the tool will only use the \
+          A list of private CocoaPod Spec repos. If not provided, the tool will only use the \
           CocoaPods master repo.
           """))
   var customSpecRepos: [URL]
@@ -101,19 +106,30 @@ struct ZipBuilderTool: ParsableCommand {
   // MARK: - Platform Arguments
 
   /// The minimum iOS Version to build for.
-  @Option(default: "10.0",
-          help: ArgumentHelp("The minimum supported iOS version. The default is 10.0."))
+  @Option(default: "10.0", help: ArgumentHelp("The minimum supported iOS version."))
   var minimumIOSVersion: String
 
-  /// The list of architectures to build for.
+  /// The minimum macOS Version to build for.
+  @Option(default: "10.12", help: ArgumentHelp("The minimum supported macOS version."))
+  var minimumMacOSVersion: String
+
+  /// The minimum tvOS Version to build for.
+  @Option(default: "10.0", help: ArgumentHelp("The minimum supported tvOS version."))
+  var minimumTVOSVersion: String
+
+  /// The list of platforms to build for.
   @Option(parsing: .upToNextOption,
           help: ArgumentHelp("""
           The list of platforms to build for. The default list is \
-          \(TargetPlatform.allCases.map { $0.sdkName }).
+          \(Platform.allCases.map { $0.name }).
           """))
-  var platforms: [TargetPlatform]
+  var platforms: [Platform]
+
+  // MARK: - Specify Pods
 
-  // MARK: - Zip Pods
+  @Option(parsing: .upToNextOption,
+          help: ArgumentHelp("List of pods to build."))
+  var pods: [String]
 
   @Option(help: ArgumentHelp("""
   The path to a JSON file of the pods (with optional version) to package into a zip.
@@ -128,13 +144,6 @@ struct ZipBuilderTool: ParsableCommand {
 
   // MARK: - Filesystem Paths
 
-  /// The path to the root of the firebase-ios-sdk repo.
-  @Option(help: ArgumentHelp("""
-  The path to the repo from which the Firebase distribution is being built.
-  """),
-  transform: URL.init(fileURLWithPath:))
-  var repoDir: URL
-
   /// Path to override podspec search with local podspec.
   @Option(help: ArgumentHelp("Path to override podspec search with local podspec."),
           transform: URL.init(fileURLWithPath:))
@@ -160,17 +169,6 @@ struct ZipBuilderTool: ParsableCommand {
   // MARK: - Validation
 
   mutating func validate() throws {
-    // Validate the repoDir exists, as well as the templateDir.
-    guard FileManager.default.directoryExists(at: repoDir) else {
-      throw ValidationError("Included a repo-dir that doesn't exist.")
-    }
-
-    // Validate the templateDir exists.
-    let templateDir = ZipBuilder.FilesystemPaths.templateDir(fromRepoDir: repoDir)
-    guard FileManager.default.directoryExists(at: templateDir) else {
-      throw ValidationError("Missing template inside of the repo. \(templateDir) does not exist.")
-    }
-
     // Validate the output directory if provided.
     if let outputDir = outputDir, !FileManager.default.directoryExists(at: outputDir) {
       throw ValidationError("`output-dir` passed in does not exist. Value: \(outputDir)")
@@ -216,6 +214,32 @@ struct ZipBuilderTool: ParsableCommand {
       FileManager.registerBuildRoot(buildRoot: buildRoot.standardizedFileURL)
     }
 
+    // Get the repoDir by deleting four path components from this file to the repo root.
+    let repoDir = URL(fileURLWithPath: #file)
+      .deletingLastPathComponent().deletingLastPathComponent()
+      .deletingLastPathComponent().deletingLastPathComponent()
+
+    // Validate the repoDir exists, as well as the templateDir.
+    guard FileManager.default.directoryExists(at: repoDir) else {
+      fatalError("Failed to find the repo root at \(repoDir).")
+    }
+
+    // Validate the templateDir exists.
+    let templateDir = ZipBuilder.FilesystemPaths.templateDir(fromRepoDir: repoDir)
+    guard FileManager.default.directoryExists(at: templateDir) else {
+      fatalError("Missing template inside of the repo. \(templateDir) does not exist.")
+    }
+
+    // Set the platform minimum versions.
+    PlatformMinimum.initialize(ios: minimumIOSVersion,
+                               macos: minimumMacOSVersion,
+                               tvos: minimumTVOSVersion)
+
+    // Update iOS target platforms if `--include-catalyst` was specified.
+    if !includeCatalyst {
+      SkipCatalyst.set()
+    }
+
     let paths = ZipBuilder.FilesystemPaths(repoDir: repoDir,
                                            buildRoot: buildRoot,
                                            outputDir: outputDir,
@@ -225,19 +249,11 @@ struct ZipBuilderTool: ParsableCommand {
 
     // Populate the platforms list if it's empty. This isn't a great spot, but the argument parser
     // can't specify a default for arrays.
-    let platformsToBuild = !platforms.isEmpty ? platforms : TargetPlatform.allCases
+    let platformsToBuild = !platforms.isEmpty ? platforms : Platform.allCases
     let builder = ZipBuilder(paths: paths,
                              platforms: platformsToBuild,
                              dynamicFrameworks: dynamic,
                              customSpecRepos: customSpecRepos)
-    let projectDir = FileManager.default.temporaryDirectory(withName: "project")
-
-    // If it exists, remove it before we re-create it. This is simpler than removing all objects.
-    if FileManager.default.directoryExists(at: projectDir) {
-      try FileManager.default.removeItem(at: projectDir)
-    }
-
-    CocoaPodUtils.podInstallPrepare(inProjectDir: projectDir, paths: paths)
 
     if let outputDir = outputDir {
       do {
@@ -247,12 +263,19 @@ struct ZipBuilderTool: ParsableCommand {
       }
     }
 
-    if let zipPods = zipPods {
-      let (installedPods, frameworks, _) = builder.buildAndAssembleZip(podsToInstall: zipPods,
-                                                                       inProjectDir: projectDir,
-                                                                       minimumIOSVersion: minimumIOSVersion,
-                                                                       includeDependencies: buildDependencies)
-      let staging = FileManager.default.temporaryDirectory(withName: "staging")
+    var podsToBuild = zipPods
+    if pods.count > 0 {
+      guard podsToBuild == nil else {
+        fatalError("Only one of `--zipPods` or `--pods` can be specified.")
+      }
+      podsToBuild = pods.map { CocoaPodUtils.VersionedPod(name: $0, version: nil) }
+    }
+
+    if let podsToBuild = podsToBuild {
+      let (installedPods, frameworks, _) =
+        builder.buildAndAssembleZip(podsToInstall: podsToBuild,
+                                    includeDependencies: buildDependencies)
+      let staging = FileManager.default.temporaryDirectory(withName: "Binaries")
       try builder.copyFrameworks(fromPods: Array(installedPods.keys), toDirectory: staging,
                                  frameworkLocations: frameworks)
       let zipped = Zip.zipContents(ofDir: staging, name: "Frameworks.zip")
@@ -284,13 +307,13 @@ struct ZipBuilderTool: ParsableCommand {
                                                isVersionCheckEnabled: carthageVersionCheck)
       }
 
-      FirebaseBuilder(zipBuilder: builder).build(in: projectDir,
-                                                 minimumIOSVersion: minimumIOSVersion,
+      FirebaseBuilder(zipBuilder: builder).build(templateDir: paths.templateDir,
                                                  carthageBuildOptions: carthageOptions)
     }
 
     if !keepBuildArtifacts {
-      FileManager.default.removeIfExists(at: projectDir.deletingLastPathComponent())
+      let tempDir = FileManager.default.temporaryDirectory(withName: "placeholder")
+      FileManager.default.removeIfExists(at: tempDir.deletingLastPathComponent())
     }
 
     // Get the time since the start of the build to get the full time.

+ 2 - 2
scripts/build_non_firebase_sdks.sh

@@ -35,12 +35,12 @@ do
 done
 echo "]" >>  "${ZIP_POD_JSON}"
 mkdir -p "${REPO}"/sdk_zip
-swift run zip-builder --keep-build-artifacts --update-pod-repo --repo-dir "${REPO}" \
+swift run zip-builder --keep-build-artifacts --update-pod-repo --platforms ios \
     --zip-pods "${ZIP_POD_JSON}" --output-dir "${REPO}"/sdk_zip --disable-build-dependencies
 
 unzip -o "${REPO}"/sdk_zip/Frameworks.zip -d "${HOME}"/ios_frameworks/Firebase/
 
 # Move Frameworks to Firebase dir, so be align with Firebase SDKs.
-mv -n "${HOME}"/ios_frameworks/Firebase/staging "${HOME}"/ios_frameworks/Firebase/NonFirebaseSDKs/
+mv -n "${HOME}"/ios_frameworks/Firebase/Binaries "${HOME}"/ios_frameworks/Firebase/NonFirebaseSDKs/
 
 

+ 1 - 1
scripts/build_zip.sh

@@ -27,6 +27,6 @@ OUTPUT_DIR="$REPO/$1"
 
 cd ReleaseTooling
 swift run zip-builder --keep-build-artifacts --update-pod-repo \
-    --repo-dir "${REPO}" --local-podspec-path "${REPO}" \
+    --local-podspec-path "${REPO}" \
     --enable-carthage-build --output-dir "${OUTPUT_DIR}" \
     --custom-spec-repos https://github.com/firebase/SpecsStaging.git