diff --git a/Sources/ScipioKit/DescriptionPackage.swift b/Sources/ScipioKit/DescriptionPackage.swift index 305e6b92..8603aff6 100644 --- a/Sources/ScipioKit/DescriptionPackage.swift +++ b/Sources/ScipioKit/DescriptionPackage.swift @@ -149,6 +149,56 @@ struct DescriptionPackage { } extension DescriptionPackage { + func resolveBuildProducts() throws -> OrderedSet { + let resolver = BuildProductsResolver(descriptionPackage: self) + return try resolver.resolveBuildProducts() + } +} + +struct BuildProduct: Hashable, Sendable { + var package: ResolvedPackage + var target: ScipioResolvedModule + + var frameworkName: String { + "\(target.name.packageNamed()).xcframework" + } + + var binaryTarget: ScipioBinaryModule? { + target.underlying as? ScipioBinaryModule + } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.target.name == rhs.target.name && + lhs.package.identity == rhs.package.identity + } + + func hash(into hasher: inout Hasher) { + // Important: Relevant for swift-6.0+ toolchain versions. For the versions below + // this change has no effect as SwiftPM provides its own proper `Hashable` + // implementations for both `ResolvedPackage` and `ResolvedTarget`. + // + // We cannot directly use `ResolvedModule.id` here as `id` also includes `BuildTriple`. + // The reason for this is that `ResolvedModule.buildTriple` is parent-dependent; more + // specifically, the same `ResolvedModule` will have a different build triple depending + // on whether it is in a root or dependency position. + // For more context, see `ResolvedModule.updateBuildTriplesOfDependencies`. + // + // At the same time, build triples remain irrelevant for the `Scipio` use case where the + // build product must be the same regardless of the triple. Meanwhile, the target name and + // package identity remain relevant and unambiguously identify the build product. + hasher.combine(target.name) + hasher.combine(package.identity) + } +} + +private final class BuildProductsResolver { + private var buildProductsCache: [BuildProduct: Set] = [:] + let descriptionPackage: DescriptionPackage + + init(descriptionPackage: DescriptionPackage) { + self.descriptionPackage = descriptionPackage + } + func resolveBuildProducts() throws -> OrderedSet { let targetsToBuild = try targetsToBuild() var products = try targetsToBuild.flatMap(resolveBuildProduct(from:)) @@ -181,7 +231,7 @@ extension DescriptionPackage { } } catch { switch error { - case GraphError.unexpectedCycle: throw Error.cycleDetected + case GraphError.unexpectedCycle: throw DescriptionPackage.Error.cycleDetected default: throw error } } @@ -190,7 +240,7 @@ extension DescriptionPackage { } private func targetsToBuild() throws -> [ScipioResolvedModule] { - switch mode { + switch descriptionPackage.mode { case .createPackage: // In create mode, all products should be built // In future update, users will be enable to specify products want to build @@ -214,8 +264,8 @@ extension DescriptionPackage { } private func fetchRootPackage() throws -> ResolvedPackage { - guard let rootPackage = graph.rootPackages.first else { - throw Error.packageNotDefined + guard let rootPackage = descriptionPackage.graph.rootPackages.first else { + throw DescriptionPackage.Error.packageNotDefined } return rootPackage } @@ -229,7 +279,7 @@ extension DescriptionPackage { .flatMap(buildProducts(from:))) #endif - switch mode { + switch descriptionPackage.mode { case .createPackage: // In create mode, rootTarget should be built let rootTargetProducts = try buildProducts(from: rootTarget) @@ -241,52 +291,25 @@ extension DescriptionPackage { } private func buildProducts(from target: ScipioResolvedModule) throws -> Set { - guard let package = graph.package(for: target) else { + guard let package = descriptionPackage.graph.package(for: target) else { return [] } let rootTargetProduct = BuildProduct(package: package, target: target) + + if let buildProducts = buildProductsCache[rootTargetProduct] { + return buildProducts + } + #if compiler(>=6.0) let dependencyProducts = try target.recursiveDependencies().compactMap(\.module).flatMap(buildProducts(from:)) #else let dependencyProducts = try target.recursiveDependencies().compactMap(\.target).flatMap(buildProducts(from:)) #endif - return Set([rootTargetProduct] + dependencyProducts) - } -} - -struct BuildProduct: Hashable, Sendable { - var package: ResolvedPackage - var target: ScipioResolvedModule - - var frameworkName: String { - "\(target.name.packageNamed()).xcframework" - } - var binaryTarget: ScipioBinaryModule? { - target.underlying as? ScipioBinaryModule - } + let buildProducts = Set([rootTargetProduct] + dependencyProducts) + buildProductsCache.updateValue(buildProducts, forKey: rootTargetProduct) - static func == (lhs: Self, rhs: Self) -> Bool { - lhs.target.name == rhs.target.name && - lhs.package.identity == rhs.package.identity - } - - func hash(into hasher: inout Hasher) { - // Important: Relevant for swift-6.0+ toolchain versions. For the versions below - // this change has no effect as SwiftPM provides its own proper `Hashable` - // implementations for both `ResolvedPackage` and `ResolvedTarget`. - // - // We cannot directly use `ResolvedModule.id` here as `id` also includes `BuildTriple`. - // The reason for this is that `ResolvedModule.buildTriple` is parent-dependent; more - // specifically, the same `ResolvedModule` will have a different build triple depending - // on whether it is in a root or dependency position. - // For more context, see `ResolvedModule.updateBuildTriplesOfDependencies`. - // - // At the same time, build triples remain irrelevant for the `Scipio` use case where the - // build product must be the same regardless of the triple. Meanwhile, the target name and - // package identity remain relevant and unambiguously identify the build product. - hasher.combine(target.name) - hasher.combine(package.identity) + return buildProducts } }