Files
firebase-ios-sdk/scripts/zip_builder/Sources/ManifestReader/Models/ResolutionContext.swift

179 lines
7.2 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 Common
import Foundation
@preconcurrency import PackageGraph
@preconcurrency import PackageModel
/// Context providing package graph lookups, aggregate mappings, and configuration
/// during target and product resolution.
public struct ResolutionContext: @unchecked Sendable {
/// The resolved SwiftPM package graph.
public let graph: ModulesGraph
/// The top-level product configuration options.
public let config: Config
/// Absolute path to the repository root directory.
public let repoRoot: String
/// Root directory paths for all packages in the graph.
public let packageRoots: [String]
/// Map of module names to their corresponding `ResolvedModule` instances.
public let modulesMap: [String: ResolvedModule]
/// Pre-computed aggregate target definitions (e.g. `GoogleUtilities`).
public let aggregates: [String: TargetDefinition]
/// Mapping from fine-grained subtarget names to parent aggregate target names.
public let subtargetMap: [String: String]
/// Recognized directory names containing public headers.
public let publicHeaderFolders: [String]
/// Initializes a resolution context.
public init(graph: ModulesGraph,
config: Config,
repoRoot: String,
packageRoots: [String],
modulesMap: [String: ResolvedModule]? = nil,
aggregates: [String: TargetDefinition],
subtargetMap: [String: String],
publicHeaderFolders: [String]? = nil) {
self.graph = graph
self.config = config
self.repoRoot = repoRoot
self.packageRoots = packageRoots
self.modulesMap = modulesMap ?? graph.reachableModules.keyedBy(\.name)
self.aggregates = aggregates
self.subtargetMap = subtargetMap
self.publicHeaderFolders = publicHeaderFolders ?? config.publicHeaderFolderNames
}
/// Maps a target name to its parent aggregate target if one exists, or returns the original name.
///
/// ```swift
/// mappedTargetName(for: "GULEnvironment") // -> "GoogleUtilities"
/// mappedTargetName(for: "FirebaseAuth") // -> "FirebaseAuth" (unchanged)
/// ```
public func mappedTargetName(for rawName: String) -> String {
subtargetMap[rawName] ?? rawName
}
/// Looks up a `ResolvedModule` by target name (resolving aggregate mappings first).
///
/// Resolves submodule aliases before querying `modulesMap`:
/// ```swift
/// // Maps "GULEnvironment" -> "GoogleUtilities", then returns the ResolvedModule for
/// GoogleUtilities:
/// context.module(named: "GULEnvironment")
///
/// // Directly returns the ResolvedModule for FirebaseAuth:
/// context.module(named: "FirebaseAuth")
///
/// // Returns nil if the target does not exist in the package graph:
/// context.module(named: "UnknownTarget") // -> nil
/// ```
public func module(named targetName: String) -> ResolvedModule? {
let mapped = mappedTargetName(for: targetName)
return modulesMap[mapped]
}
/// Looks up an aggregate `TargetDefinition` by target name.
///
/// Resolves submodule aliases and returns the combined aggregate target containing all merged
/// source files, headers, and build settings:
/// ```swift
/// // Returns the combined GoogleUtilities TargetDefinition:
/// context.aggregate(named: "GULEnvironment")
/// context.aggregate(named: "GoogleUtilities")
///
/// // Returns nil for regular non-aggregate targets:
/// context.aggregate(named: "FirebaseFirestore") // -> nil
/// ```
public func aggregate(named targetName: String) -> TargetDefinition? {
let mapped = mappedTargetName(for: targetName)
return aggregates[mapped]
}
/// Resolves the platforms for a product entry, taking into account configured overrides, explicit
/// entry platforms, graph-inferred platforms, and an optional global platform filter.
public func platforms(for entry: Config.ProductEntry,
filter: [Common.Platform]? = nil) -> [Common.Platform] {
let inferred = config.productPlatformOverrides[entry.name] ?? entry.platforms ?? ProductResolver
.inferPlatforms(
forTargetNames: entry.targets,
context: self
)
if let filter {
return inferred.filter(filter.contains)
}
return inferred
}
/// Resolves extra frameworks for a product entry, combining global product-level configuration
/// and entry-specific frameworks.
public func extraFrameworks(for entry: Config.ProductEntry) -> [String] {
(config.extraFrameworksPerProduct[entry.name] ?? []) + (entry.extraFrameworks ?? [])
}
/// Evaluates whether a target name represents an internal companion or suppressed framework.
///
/// Checks against `config.companionModuleSuffixes` (default `["Internal"]`) and
/// `config.suppressedInternalFrameworks`:
/// ```swift
/// context.isInternalCompanion(named: "FirebaseCoreInternal") // -> true (matches "Internal"
/// suffix)
/// context.isInternalCompanion(named: "FirebaseAuth") // -> false
/// ```
public func isInternalCompanion(named targetName: String) -> Bool {
let mapped = mappedTargetName(for: targetName)
if config.suppressedInternalFrameworks.containsAny(targetName, mapped) {
return true
}
return config.companionModuleSuffixes.contains { suffix in
targetName.hasSuffix(suffix) || mapped.hasSuffix(suffix)
}
}
/// Evaluates whether a dependency target should expose its full internal header search paths
/// to dependent targets rather than only public exported include directories.
///
/// Standard dependencies only expose public exported headers to maintain modular encapsulation,
/// whereas internal companions (e.g. `FirebaseCoreInternal`) and wrappers must expose
/// private/internal directories so dependent targets can access non-public shared headers.
public func shouldExposeInternalHeaders(forDependency name: String,
module: ResolvedModule? = nil) -> Bool {
if isInternalCompanion(named: name) {
return true
}
// check if it's a wrapper target
let mapped = mappedTargetName(for: name)
guard let module = module ?? self.module(named: mapped) ?? self.module(named: name) else {
// if no module exists, check wrapper target names and patterns
return config.wrapperTargetNames.containsAny(name, mapped)
|| config.wrapperTargetPatterns.contains(where: name.matchesWildcard)
|| config.wrapperTargetPatterns.contains(where: mapped.matchesWildcard)
}
return TargetClassifier.isWrapperTarget(
name: module.name,
isBinary: module.isBinary,
sources: module.sources.paths.map(\.pathString),
config: config
)
}
}