From 8e28a73b3fda1eda0a3c6d73e5a145e819ba4d69 Mon Sep 17 00:00:00 2001 From: Ryu0118 Date: Mon, 8 Jul 2024 00:08:04 +0900 Subject: [PATCH 1/2] Improve resolveBuildProducts performance --- Sources/ScipioKit/DescriptionPackage.swift | 127 ++++++++++++--------- 1 file changed, 74 insertions(+), 53 deletions(-) diff --git a/Sources/ScipioKit/DescriptionPackage.swift b/Sources/ScipioKit/DescriptionPackage.swift index 44daf01d..55a20615 100644 --- a/Sources/ScipioKit/DescriptionPackage.swift +++ b/Sources/ScipioKit/DescriptionPackage.swift @@ -149,10 +149,58 @@ struct DescriptionPackage { } extension DescriptionPackage { + func resolveBuildProducts() throws -> [BuildProduct] { + let resolver = BuildProductsResolver(descriptionPackage: self) + return try resolver.resolveBuildProducts() + } +} + +struct BuildProduct: Hashable, Sendable { + var package: ResolvedPackage + var target: ResolvedTarget + + var frameworkName: String { + "\(target.name.packageNamed()).xcframework" + } + + var binaryTarget: BinaryTarget? { + target.underlyingTarget as? BinaryTarget + } + + 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 visitedTargets: Set = [] + let descriptionPackage: DescriptionPackage + + init(descriptionPackage: DescriptionPackage) { + self.descriptionPackage = descriptionPackage + } + func resolveBuildProducts() throws -> [BuildProduct] { let targetsToBuild = try targetsToBuild() var products = try targetsToBuild.flatMap(resolveBuildProduct(from:)) + visitedTargets.removeAll() + let productMap: [String: BuildProduct] = Dictionary(products.map { ($0.target.name, $0) }) { $1 } func resolvedTargetToBuildProduct(_ target: ResolvedTarget) -> BuildProduct { guard let product = productMap[target.name] else { @@ -174,7 +222,7 @@ extension DescriptionPackage { } } catch { switch error { - case GraphError.unexpectedCycle: throw Error.cycleDetected + case GraphError.unexpectedCycle: throw DescriptionPackage.Error.cycleDetected default: throw error } } @@ -182,33 +230,10 @@ extension DescriptionPackage { return products.reversed() } - private func targetsToBuild() throws -> Set { - switch mode { - case .createPackage: - // In create mode, all products should be built - // In future update, users will be enable to specify products want to build - let rootPackage = try fetchRootPackage() - let productNamesToBuild = rootPackage.manifest.products.map { $0.name } - let productsToBuild = rootPackage.products.filter { productNamesToBuild.contains($0.name) } - return Set(productsToBuild.flatMap(\.targets)) - case .prepareDependencies: - // In prepare mode, all targets should be built - // In future update, users will be enable to specify targets want to build - return Set(try fetchRootPackage().targets) - } - } - - private func fetchRootPackage() throws -> ResolvedPackage { - guard let rootPackage = graph.rootPackages.first else { - throw Error.packageNotDefined - } - return rootPackage - } - private func resolveBuildProduct(from rootTarget: ResolvedTarget) throws -> Set { let dependencyProducts = Set(try rootTarget.recursiveTargetDependencies().flatMap(buildProducts(from:))) - switch mode { + switch descriptionPackage.mode { case .createPackage: // In create mode, rootTarget should be built let rootTargetProducts = try buildProducts(from: rootTarget) @@ -220,43 +245,39 @@ extension DescriptionPackage { } private func buildProducts(from target: ResolvedTarget) throws -> Set { - guard let package = graph.package(for: target) else { + guard let package = descriptionPackage.graph.package(for: target), + !visitedTargets.contains(target) + else { return [] } + visitedTargets.insert(target) + let rootTargetProduct = BuildProduct(package: package, target: target) let dependencyProducts = try target.recursiveDependencies().compactMap(\.target).flatMap(buildProducts(from:)) return Set([rootTargetProduct] + dependencyProducts) } -} - -struct BuildProduct: Hashable, Sendable { - var package: ResolvedPackage - var target: ResolvedTarget - - var frameworkName: String { - "\(target.name.packageNamed()).xcframework" - } - var binaryTarget: BinaryTarget? { - target.underlyingTarget as? BinaryTarget + private func targetsToBuild() throws -> Set { + 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 + let rootPackage = try fetchRootPackage() + let productNamesToBuild = rootPackage.manifest.products.map { $0.name } + let productsToBuild = rootPackage.products.filter { productNamesToBuild.contains($0.name) } + return Set(productsToBuild.flatMap(\.targets)) + case .prepareDependencies: + // In prepare mode, all targets should be built + // In future update, users will be enable to specify targets want to build + return Set(try fetchRootPackage().targets) + } } - 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 func fetchRootPackage() throws -> ResolvedPackage { + guard let rootPackage = descriptionPackage.graph.rootPackages.first else { + throw DescriptionPackage.Error.packageNotDefined + } + return rootPackage } } From 1bb3d133e113c6ee3d0480f188f0d921f9afb678 Mon Sep 17 00:00:00 2001 From: Ryu0118 Date: Wed, 10 Jul 2024 13:37:07 +0900 Subject: [PATCH 2/2] Support Swift6 --- Sources/ScipioKit/DescriptionPackage.swift | 96 ++++++++++++++-------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/Sources/ScipioKit/DescriptionPackage.swift b/Sources/ScipioKit/DescriptionPackage.swift index 4db2f416..8b4a3281 100644 --- a/Sources/ScipioKit/DescriptionPackage.swift +++ b/Sources/ScipioKit/DescriptionPackage.swift @@ -149,6 +149,52 @@ struct DescriptionPackage { } extension DescriptionPackage { + func resolveBuildProducts() throws -> [BuildProduct] { + 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 + } + + 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 -> [BuildProduct] { let targetsToBuild = try targetsToBuild() var products = try targetsToBuild.flatMap(resolveBuildProduct(from:)) @@ -181,7 +227,7 @@ extension DescriptionPackage { } } catch { switch error { - case GraphError.unexpectedCycle: throw Error.cycleDetected + case GraphError.unexpectedCycle: throw DescriptionPackage.Error.cycleDetected default: throw error } } @@ -190,7 +236,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 +260,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 +275,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,47 +287,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" - } + let buildProducts = Set([rootTargetProduct] + dependencyProducts) + buildProductsCache.updateValue(buildProducts, forKey: rootTargetProduct) - var binaryTarget: ScipioBinaryModule? { - target.underlying as? ScipioBinaryModule - } - - 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 } }