mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-11 22:55:59 +05:45
156 lines
7.1 KiB
Swift
156 lines
7.1 KiB
Swift
/*
|
|
* Copyright 2026 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 ArgumentParser
|
|
import Foundation
|
|
|
|
/// Turns a finished, unsigned distribution into Carthage archives and binary specs, as configured
|
|
/// by the config's `carthage` group (see docs/how-it-works.md#carthage).
|
|
struct CarthageCommand: AsyncParsableCommand {
|
|
static let configuration = CommandConfiguration(
|
|
commandName: "carthage",
|
|
abstract: "Package a finished, unsigned distribution for Carthage."
|
|
)
|
|
|
|
@OptionGroup var options: SharedOptions
|
|
@OptionGroup var logging: LoggingOptions
|
|
|
|
@Argument(help: ArgumentHelp(
|
|
"The distribution: a directory or a zip.",
|
|
discussion: "Default: <work-dir>/dist/<release.name>."
|
|
))
|
|
var distribution: String?
|
|
|
|
@Option(help: ArgumentHelp(
|
|
"Where the archives and binary specs go, in <output>/<version>/.",
|
|
discussion: "Default: <work-dir>/carthage."
|
|
))
|
|
var output: String?
|
|
|
|
@Option(help: "The framework cache, for the extra build.")
|
|
var cacheDir = FrameworkCache.defaultDirectory
|
|
|
|
func validate() throws {
|
|
// The extra build is limited by `carthage.rebuild`, and the distribution's folders are given.
|
|
if options.product != nil {
|
|
throw ValidationError("--product doesn't apply to carthage; set carthage.rebuild.")
|
|
}
|
|
if options.withoutDependencies {
|
|
throw ValidationError("--without-dependencies doesn't apply to carthage.")
|
|
}
|
|
}
|
|
|
|
func run() async throws {
|
|
logging.apply()
|
|
let config = try options.loadConfig()
|
|
guard let carthage = config.carthage else {
|
|
throw ZipBuilderError("No carthage group in \(options.configPath): add one to use "
|
|
+ "carthage (see docs/configuration.md#carthage).")
|
|
}
|
|
let rebuild = try extraBuild(of: carthage)
|
|
let version = try options.releaseVersion(config)
|
|
let workDir = URL(fileURLWithPath: options.workDir)
|
|
let input = distribution ?? distributionRoot(workDir: options.workDir, config: config).path
|
|
let found = try await readDistribution(at: input)
|
|
defer { found.removeExtracted() }
|
|
|
|
// The extra build's own work directory; its plan and logs land there too.
|
|
var buildOptions = options
|
|
buildOptions.workDir = workDir.appendingPathComponent("carthage-build").path
|
|
var buildConfig = options.applyingFlags(to: config)
|
|
buildConfig.build.defines += rebuild?.defines ?? []
|
|
// Planned up front: the umbrella framework needs the iOS deployment target, and the extra
|
|
// build's frameworks are checked for signatures before anything is compiled.
|
|
let plans = try await carthagePlans(buildConfig, rebuild: rebuild?.product, version: version,
|
|
needed: carthage.umbrellaFramework != nil)
|
|
let rebuilt = plans?.rebuild?.frameworks.map(\.name) ?? []
|
|
try checkUnsigned(xcframeworksChangedByCarthage(found, rebuilt: rebuilt,
|
|
files: carthage.files))
|
|
|
|
let staged = try stageDistribution(found.root, at: distributionRoot(
|
|
workDir: buildOptions.workDir, config: config
|
|
))
|
|
if let rebuildPlan = plans?.rebuild {
|
|
let defines = (rebuild?.defines ?? []).joined(separator: " ")
|
|
logger.info("Extra build: \(rebuildPlan.frameworks.count) frameworks with \(defines)")
|
|
let planFile = try buildOptions.writePlan(rebuildPlan)
|
|
logger.info("Planned: \(planFile.path)")
|
|
let xcframeworks = try await buildOptions.build(rebuildPlan, cacheDir: cacheDir)
|
|
let replaced = try replacingFrameworks(rebuilt, in: staged, from: xcframeworks)
|
|
let list = replaced.isEmpty ? "none" : replaced.joined(separator: ", ")
|
|
logger.info("Replaced with the extra build: \(list)")
|
|
}
|
|
try addCarthageFiles(carthage.files, in: staged)
|
|
if let umbrella = carthage.umbrellaFramework {
|
|
try makeUmbrellaFramework(umbrella, in: staged, version: version,
|
|
iOSDeploymentTarget: plans?.iOSDeploymentTarget,
|
|
packageRoot: options.packagePath)
|
|
}
|
|
|
|
let outputDirectory = URL(fileURLWithPath: output ?? workDir.appendingPathComponent("carthage")
|
|
.path).appendingPathComponent(version)
|
|
// Archives are named by their contents, so an earlier run's would linger beside them.
|
|
try? FileManager.default.removeItem(at: outputDirectory)
|
|
let archives = try await makeCarthageArchives(of: staged, carthage, version: version,
|
|
packageRoot: options.packagePath,
|
|
output: outputDirectory)
|
|
logger.info("Carthage: \(archives.count) archives and binary specs in \(outputDirectory.path)")
|
|
}
|
|
|
|
/// `carthage.rebuild` and `carthage.defines`, which go together; nil without an extra build.
|
|
private func extraBuild(of carthage: Config.Carthage) throws
|
|
-> (product: String, defines: [String])? {
|
|
switch (carthage.rebuild, carthage.defines) {
|
|
case (nil, nil):
|
|
return nil
|
|
case let (product?, defines?) where !defines.isEmpty:
|
|
return (product, defines)
|
|
default:
|
|
throw ZipBuilderError("carthage.rebuild and carthage.defines go together: set both "
|
|
+ "(with at least one define), or neither.")
|
|
}
|
|
}
|
|
|
|
/// The whole release's plan with `config`, for its iOS deployment target, and the extra build's
|
|
/// plan (`restricting`) when there's a `rebuild` product. Nil when neither is `needed`.
|
|
private func carthagePlans(_ config: Config, rebuild: String?, version: String,
|
|
needed: Bool) async throws
|
|
-> (iOSDeploymentTarget: String?, rebuild: Plan?)? {
|
|
guard needed || rebuild != nil else { return nil }
|
|
let graph = try await loadPackage(at: options.packagePath)
|
|
let root = options.packagePath
|
|
let whole = try customize(makePlan(graph, config, version: version), config,
|
|
packageRoot: root, version: version)
|
|
var target: String?
|
|
if case let .string(value) = whole.settings["IPHONEOS_DEPLOYMENT_TARGET"] {
|
|
target = value
|
|
}
|
|
guard let rebuild else { return (target, nil) }
|
|
// Only its names are used. Its warnings repeat the whole plan's, or only reflect planning
|
|
// one product (shared frameworks outside any folder), so they're dropped.
|
|
let product = try Warnings.$collected.withValue(Warnings()) {
|
|
do {
|
|
return try customize(makePlan(graph, config, version: version, product: rebuild), config,
|
|
packageRoot: root, version: version)
|
|
} catch {
|
|
throw ZipBuilderError("carthage.rebuild: \(error)")
|
|
}
|
|
}
|
|
let names = product.frameworkNames.union(product.binaries.map(\.name))
|
|
return (target, restricting(whole, toWhatIsReachedFrom: names))
|
|
}
|
|
}
|