mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-10 22:45:59 +05:45
195 lines
7.5 KiB
Swift
195 lines
7.5 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
|
|
|
|
/// Options shared by the pipeline stages, and the stage steps that use them (plan, build,
|
|
/// package).
|
|
struct SharedOptions: ParsableArguments {
|
|
@Option(help: "The Swift package to build.")
|
|
var packagePath = "."
|
|
|
|
@Option(help: ArgumentHelp(
|
|
"The config file.",
|
|
discussion: "Default: <package-path>/zip_builder.jsonc, else <package-path>/zip_builder.json."
|
|
))
|
|
var config: String?
|
|
|
|
@Option(help: "Where each stage writes its output.")
|
|
var workDir = "build"
|
|
|
|
@Option(help: "Only this product and what it needs.")
|
|
var product: String?
|
|
|
|
@Option(help: "The release version (x.y.z). Takes precedence over release.version.")
|
|
var version: String?
|
|
|
|
@Option(help: ArgumentHelp(
|
|
"How the frameworks built from source link. Takes precedence over build.linkage.",
|
|
valueName: "static|dynamic"
|
|
))
|
|
var linkage: Linkage?
|
|
|
|
@Option(help: ArgumentHelp(
|
|
"The platforms to build, comma-separated. Takes precedence over build.platforms.",
|
|
valueName: "ios,catalyst,…"
|
|
), transform: parsePlatforms)
|
|
var platforms: [Platform]?
|
|
|
|
@Flag(help: ArgumentHelp(
|
|
"Ship --product without its dependencies: they're built, but not zipped.",
|
|
discussion: "Sets products.withoutDependencies. Needs --product."
|
|
))
|
|
var withoutDependencies = false
|
|
|
|
func validate() throws {
|
|
if withoutDependencies, product == nil {
|
|
throw ValidationError("""
|
|
--without-dependencies needs --product; to ship every product without its dependencies, \
|
|
set products.withoutDependencies in the config.
|
|
""")
|
|
}
|
|
}
|
|
|
|
/// `--config`, else the package's config file; names the preferred file when there's none.
|
|
var configPath: Path {
|
|
config ?? Config.file(in: packagePath) ?? Config.paths(in: packagePath)[0]
|
|
}
|
|
|
|
/// The config: `--config`, else the package's config file, which must exist.
|
|
func loadConfig() throws -> Config {
|
|
guard let found = try Config.load(from: config, orDefaultIn: packagePath) else {
|
|
throw ZipBuilderError("No config: \(configPath) doesn't exist. Add it, or pass --config.")
|
|
}
|
|
return found
|
|
}
|
|
|
|
/// `--version`, else `release.version` from the config.
|
|
func releaseVersion(_ config: Config) throws -> String {
|
|
guard let version = version ?? config.release.version else {
|
|
throw ZipBuilderError(
|
|
"No release version: pass --version or set release.version in \(configPath)."
|
|
)
|
|
}
|
|
return version
|
|
}
|
|
|
|
/// `--linkage`, else `build.linkage` from the config.
|
|
func releaseLinkage(_ config: Config) -> Linkage {
|
|
linkage ?? config.build.linkage
|
|
}
|
|
|
|
/// The config planning and `customize` get: `config` with the flags that override it
|
|
/// (`--linkage`, `--platforms`, `--without-dependencies`) applied, since both stages read those
|
|
/// keys.
|
|
func applyingFlags(to config: Config) -> Config {
|
|
var config = config
|
|
config.build.linkage = releaseLinkage(config)
|
|
if let platforms {
|
|
config.build.platforms = platforms
|
|
}
|
|
if withoutDependencies {
|
|
config.products.withoutDependencies = true
|
|
}
|
|
return config
|
|
}
|
|
|
|
/// Plans the release from the package and config (`makePlan`, then `customize`).
|
|
func makeCustomizedPlan(_ config: Config) async throws -> Plan {
|
|
let version = try releaseVersion(config)
|
|
let config = applyingFlags(to: config)
|
|
let graph = try await loadPackage(at: packagePath)
|
|
let plan = try makePlan(graph, config, version: version, product: product)
|
|
return customize(plan, config, packageRoot: packagePath, version: version)
|
|
}
|
|
|
|
/// The plan at `path` (`--plan`), else a new one. `--product`, `--linkage`, `--platforms` and
|
|
/// `--without-dependencies` don't apply to a given plan.
|
|
func loadOrMakePlan(_ path: Path?, _ config: Config) async throws -> Plan {
|
|
guard let path else { return try await makeCustomizedPlan(config) }
|
|
if product != nil {
|
|
warn("--product is ignored with --plan: the plan already holds its frameworks.")
|
|
}
|
|
if linkage != nil {
|
|
warn("--linkage is ignored with --plan: the plan already holds each framework's linkage.")
|
|
}
|
|
if platforms != nil {
|
|
warn("--platforms is ignored with --plan: the plan already holds each framework's platforms.")
|
|
}
|
|
if withoutDependencies {
|
|
warn("--without-dependencies is ignored with --plan: the plan already holds its folders.")
|
|
}
|
|
do {
|
|
return try Plan.read(from: path)
|
|
} catch {
|
|
throw ZipBuilderError("Couldn't read the plan at \(path): \(error)")
|
|
}
|
|
}
|
|
|
|
/// Writes `<work-dir>/plan.json`.
|
|
func writePlan(_ plan: Plan) throws -> URL {
|
|
let workDir = URL(fileURLWithPath: workDir)
|
|
try FileManager.default.createDirectory(at: workDir, withIntermediateDirectories: true)
|
|
let planURL = workDir.appendingPathComponent("plan.json")
|
|
try plan.json().write(to: planURL)
|
|
return planURL
|
|
}
|
|
|
|
/// Builds the `.xcframeworks` (`buildXCFrameworks`) and returns `<work-dir>/xcframeworks`.
|
|
/// With `pruneCache`, the cache keeps only this build's entries.
|
|
func build(_ plan: Plan, cacheDir: Path, pruneCache: Bool = false,
|
|
project: Path? = nil) async throws -> Path {
|
|
let start = ContinuousClock.now
|
|
let paths = try await buildXCFrameworks(plan, packageRoot: packagePath, workDir: workDir,
|
|
cache: FrameworkCache(directory: cacheDir),
|
|
pruneCache: pruneCache, project: project)
|
|
let output = xcframeworksDirectory(workDir: workDir).path
|
|
logger.info("""
|
|
Built \(paths.count) .xcframeworks (\(plan.frameworks.count) frameworks, \
|
|
\(plan.binaries.count) binaries) in \(formatted(.now - start)): \(output)
|
|
""")
|
|
return output
|
|
}
|
|
|
|
/// Lays out, signs and zips the distribution (`makeZip`); returns the distribution root.
|
|
func package(_ plan: Plan, _ config: Config, xcframeworks: Path, sign: String?,
|
|
version: String) async throws -> URL {
|
|
let zip = try await makeZip(xcframeworks: xcframeworks, plan: plan, config: config,
|
|
packageRoot: packagePath, workDir: workDir,
|
|
signingIdentity: sign, version: version)
|
|
let root = distributionRoot(workDir: workDir, config: config)
|
|
logger.info("Distribution: \(root.path)")
|
|
logger.info("Zip: \(zip) (\(formattedSize(ofFile: zip)))")
|
|
return root
|
|
}
|
|
}
|
|
|
|
/// `--platforms ios,catalyst`: platform names as in `build.platforms`, comma-separated.
|
|
func parsePlatforms(_ text: String) throws -> [Platform] {
|
|
let names = text.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
|
|
return try names.map { name in
|
|
guard let platform = Platform(rawValue: name) else {
|
|
let valid = Platform.allCases.map(\.rawValue).joined(separator: ", ")
|
|
throw ValidationError("--platforms: \(name) isn't a platform; use \(valid).")
|
|
}
|
|
return platform
|
|
}
|
|
}
|
|
|
|
/// `--linkage static|dynamic`, spelled like `build.linkage`.
|
|
extension Linkage: ExpressibleByArgument {}
|