diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c55376b1b..a18e8de553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ ##### Bug Fixes -- None. +- Fix indexing multi-platform projects such as those containing watchOS extensions. +- Subclasses of CLKComplicationPrincipalClass referenced from an Info.plist are now retained. ## 2.14.1 (2023-06-25) diff --git a/Sources/PeripheryKit/Generic/GenericProjectDriver.swift b/Sources/PeripheryKit/Generic/GenericProjectDriver.swift index 6a5e063c95..236ec81f19 100644 --- a/Sources/PeripheryKit/Generic/GenericProjectDriver.swift +++ b/Sources/PeripheryKit/Generic/GenericProjectDriver.swift @@ -9,7 +9,7 @@ public final class GenericProjectDriver { decoder.keyDecodingStrategy = .convertFromSnakeCase let sourceFiles = try configuration.fileTargetsPath - .reduce(into: [FilePath: Set]()) { result, mapPath in + .reduce(into: [FilePath: Set]()) { result, mapPath in guard mapPath.exists else { throw PeripheryError.pathDoesNotExist(path: mapPath.string) } @@ -18,7 +18,7 @@ public final class GenericProjectDriver { let map = try decoder .decode(FileTargetMapContainer.self, from: data) .fileTargets - .reduce(into: [FilePath: Set](), { (result, tuple) in + .reduce(into: [FilePath: Set](), { (result, tuple) in let (key, value) = tuple let path = FilePath.makeAbsolute(key) @@ -26,7 +26,8 @@ public final class GenericProjectDriver { throw PeripheryError.pathDoesNotExist(path: path.string) } - result[path] = value + let indexTargets = value.mapSet { IndexTarget(name: $0) } + result[path] = indexTargets }) result.merge(map) { $0.union($1) } } @@ -34,10 +35,10 @@ public final class GenericProjectDriver { return self.init(sourceFiles: sourceFiles, configuration: configuration) } - private let sourceFiles: [FilePath: Set] + private let sourceFiles: [FilePath: Set] private let configuration: Configuration - init(sourceFiles: [FilePath: Set], configuration: Configuration) { + init(sourceFiles: [FilePath: Set], configuration: Configuration) { self.sourceFiles = sourceFiles self.configuration = configuration } diff --git a/Sources/PeripheryKit/Indexer/IndexTarget.swift b/Sources/PeripheryKit/Indexer/IndexTarget.swift new file mode 100644 index 0000000000..f1d1262365 --- /dev/null +++ b/Sources/PeripheryKit/Indexer/IndexTarget.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct IndexTarget: Hashable { + public let name: String + public var triple: String? + + public init(name: String, triple: String? = nil) { + self.name = name + self.triple = triple + } +} diff --git a/Sources/PeripheryKit/Indexer/InfoPlistParser.swift b/Sources/PeripheryKit/Indexer/InfoPlistParser.swift index 9a3d8804d9..60d46607d4 100644 --- a/Sources/PeripheryKit/Indexer/InfoPlistParser.swift +++ b/Sources/PeripheryKit/Indexer/InfoPlistParser.swift @@ -4,7 +4,7 @@ import AEXML import Shared final class InfoPlistParser { - private static let elements = ["UISceneClassName", "UISceneDelegateClassName", "NSExtensionPrincipalClass"] + private static let elements = ["UISceneClassName", "UISceneDelegateClassName", "NSExtensionPrincipalClass", "CLKComplicationPrincipalClass"] private let path: FilePath required init(path: FilePath) { diff --git a/Sources/PeripheryKit/Indexer/SwiftIndexer.swift b/Sources/PeripheryKit/Indexer/SwiftIndexer.swift index 36533a5009..57c7f97b40 100644 --- a/Sources/PeripheryKit/Indexer/SwiftIndexer.swift +++ b/Sources/PeripheryKit/Indexer/SwiftIndexer.swift @@ -5,14 +5,14 @@ import SystemPackage import Shared public final class SwiftIndexer: Indexer { - private let sourceFiles: [FilePath: Set] + private let sourceFiles: [FilePath: Set] private let graph: SourceGraph private let logger: ContextualLogger private let configuration: Configuration private let indexStorePaths: [FilePath] public required init( - sourceFiles: [FilePath: Set], + sourceFiles: [FilePath: Set], graph: SourceGraph, indexStorePaths: [FilePath], logger: Logger = .init(), @@ -32,7 +32,7 @@ public final class SwiftIndexer: Indexer { excludedFiles.forEach { self.logger.debug("Excluding \($0.string)") } let unitsByFile: [FilePath: [(IndexStore, IndexStoreUnit)]] = try JobPool(jobs: indexStorePaths) - .map { [logger] indexStorePath in + .map { [logger, sourceFiles] indexStorePath in logger.debug("Reading \(indexStorePath)") var unitsByFile: [FilePath: [(IndexStore, IndexStoreUnit)]] = [:] let indexStore = try IndexStore.open(store: URL(fileURLWithPath: indexStorePath.string), lib: .open()) @@ -43,7 +43,20 @@ public final class SwiftIndexer: Indexer { let file = FilePath.makeAbsolute(filePath) if includedFiles.contains(file) { - unitsByFile[file, default: []].append((indexStore, unit)) + // Ignore units built for other architectures/platforms. + let validTargetTriples = sourceFiles[file]?.compactMapSet { $0.triple } ?? [] + + if validTargetTriples.isEmpty { + unitsByFile[file, default: []].append((indexStore, unit)) + } else { + if let unitTargetTriple = try indexStore.target(for: unit) { + if validTargetTriples.contains(unitTargetTriple) { + unitsByFile[file, default: []].append((indexStore, unit)) + } + } else { + logger.warn("No unit target triple for: \(file)") + } + } } return true @@ -62,7 +75,7 @@ public final class SwiftIndexer: Indexer { if !unindexedFiles.isEmpty { unindexedFiles.forEach { logger.debug("Source file not indexed: \($0)") } - let targets: Set = Set(unindexedFiles.flatMap { sourceFiles[$0] ?? [] }) + let targets = unindexedFiles.flatMapSet { sourceFiles[$0] ?? [] }.mapSet { $0.name } throw PeripheryError.unindexedTargetsError(targets: targets, indexStorePaths: indexStorePaths) } diff --git a/Sources/PeripheryKit/SPM/SPMProjectDriver.swift b/Sources/PeripheryKit/SPM/SPMProjectDriver.swift index 98c6b1c94f..ae5796a547 100644 --- a/Sources/PeripheryKit/SPM/SPMProjectDriver.swift +++ b/Sources/PeripheryKit/SPM/SPMProjectDriver.swift @@ -58,9 +58,12 @@ extension SPMProjectDriver: ProjectDriver { } public func index(graph: SourceGraph) throws { - let sourceFiles = targets.reduce(into: [FilePath: Set]()) { result, target in + let sourceFiles = targets.reduce(into: [FilePath: Set]()) { result, target in let targetPath = absolutePath(for: target) - target.sources.forEach { result[targetPath.appending($0), default: []].insert(target.name) } + target.sources.forEach { + let indexTarget = IndexTarget(name: target.name) + result[targetPath.appending($0), default: []].insert(indexTarget) + } } let storePaths: [FilePath] diff --git a/Sources/Shared/Extensions/Sequence+Extension.swift b/Sources/Shared/Extensions/Sequence+Extension.swift index 5715f4b54e..8c849e8e29 100644 --- a/Sources/Shared/Extensions/Sequence+Extension.swift +++ b/Sources/Shared/Extensions/Sequence+Extension.swift @@ -30,4 +30,11 @@ public extension Sequence { } } } + + func mapDict(_ transform: (Element) throws -> (Key, Value)) rethrows -> Dictionary { + try reduce(into: .init()) { result, element in + let pair = try transform(element) + result[pair.0] = pair.1 + } + } } diff --git a/Sources/Shared/Logger.swift b/Sources/Shared/Logger.swift index 290d76186b..77053ad488 100644 --- a/Sources/Shared/Logger.swift +++ b/Sources/Shared/Logger.swift @@ -146,6 +146,10 @@ public struct ContextualLogger { logger.debug("[\(context)] \(text)") } + public func warn(_ text: String) { + logger.warn("[\(context)] \(text)") + } + public func beginInterval(_ name: StaticString) -> SignpostInterval { logger.beginInterval(name) } diff --git a/Sources/Shared/PeripheryError.swift b/Sources/Shared/PeripheryError.swift index 03e9514bac..b67bb1da08 100644 --- a/Sources/Shared/PeripheryError.swift +++ b/Sources/Shared/PeripheryError.swift @@ -8,7 +8,6 @@ public enum PeripheryError: Error, LocalizedError, CustomStringConvertible { case underlyingError(Error) case invalidScheme(name: String, project: String) case invalidTargets(names: [String], project: String) - case testTargetsNotBuildable(names: [String]) case sourceGraphIntegrityError(message: String) case guidedSetupError(message: String) case updateCheckError(message: String) @@ -22,6 +21,7 @@ public enum PeripheryError: Error, LocalizedError, CustomStringConvertible { case jsonDeserializationError(error: Error, json: String) case indexStoreNotFound(derivedDataPath: String) case conflictingIndexUnitsError(file: FilePath, module: String, unitTargets: Set) + case invalidTargetTriple(target: String, arch: String, vendor: String, osVersion: String) public var errorDescription: String? { switch self { @@ -42,9 +42,6 @@ public enum PeripheryError: Error, LocalizedError, CustomStringConvertible { let declinedTarget = names.count == 1 ? "Target" : "Targets" let conjugatedDo = names.count == 1 ? "does" : "do" return "\(declinedTarget) \(formattedNames) \(conjugatedDo) not exist in '\(project)'." - case .testTargetsNotBuildable(let names): - let joinedNames = names.joined(separator: "', '") - return "The following test targets are not built by any of the given schemes: '\(joinedNames)'" case .sourceGraphIntegrityError(let message): return message case .guidedSetupError(let message): @@ -63,7 +60,7 @@ public enum PeripheryError: Error, LocalizedError, CustomStringConvertible { return "Failed to parse Swift version from: \(fullVersion)" case let .unindexedTargetsError(targets, indexStorePath): let joinedTargets = targets.sorted().joined(separator: ", ") - return "The index store at '\(indexStorePath)' does not contain data for the following targets: \(joinedTargets). Either the index store is outdated, or you have requested to scan targets that have not been built." + return "The index store at '\(indexStorePath)' does not contain data for the following targets: \(joinedTargets). Either the index store is outdated, or you have requested to scan targets that have not been built. For Xcode projects, the chosen schemes must build all of the chosen targets." case let .swiftVersionUnsupportedError(version, minimumVersion): return "This version of Periphery only supports Swift >= \(minimumVersion), you're using \(version)." case let .jsonDeserializationError(error, json): @@ -77,6 +74,8 @@ public enum PeripheryError: Error, LocalizedError, CustomStringConvertible { } parts.append("If you passed the '--index-store-path' option, ensure that Xcode is not open with a project that may write to this index store while Periphery is running.") return parts.joined(separator: " ") + case let .invalidTargetTriple(target, arch, vendor, osVersion): + return "Failed to construct triple for target '\(target)': \(arch), \(vendor), \(osVersion)" } } diff --git a/Sources/XcodeSupport/XcodeProject.swift b/Sources/XcodeSupport/XcodeProject.swift index 651b2d8507..062e8dbab0 100644 --- a/Sources/XcodeSupport/XcodeProject.swift +++ b/Sources/XcodeSupport/XcodeProject.swift @@ -105,11 +105,8 @@ final class XcodeProject: XcodeProjectlike { } } - func schemes() throws -> Set { - let schemes = try xcodebuild.schemes(project: self).map { - try XcodeScheme(project: self, name: $0) - } - return Set(schemes) + func schemes() throws -> Set { + try xcodebuild.schemes(project: self) } } diff --git a/Sources/XcodeSupport/XcodeProjectDriver.swift b/Sources/XcodeSupport/XcodeProjectDriver.swift index f9b82a9363..21a8e1f525 100644 --- a/Sources/XcodeSupport/XcodeProjectDriver.swift +++ b/Sources/XcodeSupport/XcodeProjectDriver.swift @@ -47,8 +47,8 @@ public final class XcodeProjectDriver { } // Ensure schemes exist within the project - let schemes = try project.schemes().filter { configuration.schemes.contains($0.name) } - let validSchemeNames = Set(schemes.map { $0.name }) + let schemes = try project.schemes().filter { configuration.schemes.contains($0) } + let validSchemeNames = Set(schemes.map { $0 }) if let scheme = Set(configuration.schemes).subtracting(validSchemeNames).first { throw PeripheryError.invalidScheme(name: scheme, project: project.path.lastComponent?.string ?? "") @@ -66,7 +66,7 @@ public final class XcodeProjectDriver { private let configuration: Configuration private let xcodebuild: Xcodebuild private let project: XcodeProjectlike - private let schemes: Set + private let schemes: Set private let targets: Set private let packageTargets: [SPM.Package: Set] @@ -75,7 +75,7 @@ public final class XcodeProjectDriver { configuration: Configuration = .shared, xcodebuild: Xcodebuild = .init(), project: XcodeProjectlike, - schemes: Set, + schemes: Set, targets: Set, packageTargets: [SPM.Package: Set] ) { @@ -113,17 +113,16 @@ public final class XcodeProjectDriver { extension XcodeProjectDriver: ProjectDriver { public func build() throws { - // Ensure test targets are built by chosen schemes. - // SwiftPM test targets are not considered here because xcodebuild does not include them - // in the output of -showBuildSettings - let testTargetNames = targets.filter { $0.isTestTarget }.map { $0.name } - - if !testTargetNames.isEmpty { - let allTestTargets = try schemes.flatMap { try $0.testTargets() } - let missingTestTargets = Set(testTargetNames).subtracting(allTestTargets).sorted() + // Copy target triples to the targets. The triple is used by the indexer to ignore index store units built for + // other architectures/platforms. + let targetTriples = try xcodebuild.buildSettings(targets: targets) + .mapDict { action in + (action.target, try action.makeTargetTriple()) + } - if !missingTestTargets.isEmpty { - throw PeripheryError.testTargetsNotBuildable(names: missingTestTargets) + for target in targets { + if let triple = targetTriples[target.name] { + target.triple = triple } } @@ -136,14 +135,10 @@ extension XcodeProjectDriver: ProjectDriver { for scheme in schemes { if configuration.outputFormat.supportsAuxiliaryOutput { let asterisk = colorize("*", .boldGreen) - logger.info("\(asterisk) Building \(scheme.name)...") + logger.info("\(asterisk) Building \(scheme)...") } - // Because xcodebuild -showBuildSettings doesn't include package test targets, we can't - // validate that the requested package test targets are actually built by the scheme. - // We'll just assume they are and attempt a test build. If this assumption was false, - // an `unindexedTargetsError` will be thrown. - let containsXcodeTestTargets = !Set(try scheme.testTargets()).isDisjoint(with: testTargetNames) + let containsXcodeTestTargets = targets.contains(where: \.isTestTarget) let containsPackageTestTargets = packageTargets.values.contains { $0.contains(where: \.isTestTarget) } let buildForTesting = containsXcodeTestTargets || containsPackageTestTargets try xcodebuild.build(project: project, @@ -165,10 +160,13 @@ extension XcodeProjectDriver: ProjectDriver { try targets.forEach { try $0.identifyFiles() } - var sourceFiles: [FilePath: Set] = [:] + var sourceFiles: [FilePath: Set] = [:] for target in targets { - target.files(kind: .swift).forEach { sourceFiles[$0, default: []].insert(target.name) } + target.files(kind: .swift).forEach { + let indexTarget = IndexTarget(name: target.name, triple: target.triple) + sourceFiles[$0, default: []].insert(indexTarget) + } } for (package, targets) in packageTargets { @@ -177,7 +175,8 @@ extension XcodeProjectDriver: ProjectDriver { for target in targets { target.sourcePaths.forEach { let absolutePath = packageRoot.pushing($0) - sourceFiles[absolutePath, default: []].insert(target.name) } + let indexTarget = IndexTarget(name: target.name) + sourceFiles[absolutePath, default: []].insert(indexTarget) } } } diff --git a/Sources/XcodeSupport/XcodeProjectSetupGuide.swift b/Sources/XcodeSupport/XcodeProjectSetupGuide.swift index 2420d86507..6b3cc35645 100644 --- a/Sources/XcodeSupport/XcodeProjectSetupGuide.swift +++ b/Sources/XcodeSupport/XcodeProjectSetupGuide.swift @@ -43,7 +43,7 @@ public final class XcodeProjectSetupGuide: SetupGuideHelpers, ProjectSetupGuide print(colorize("Select build targets to analyze:", .bold)) configuration.targets = select(multiple: targets, allowAll: true).selectedValues - let schemes = try filter(project.schemes(), project).map { $0.name }.sorted() + let schemes = try filter(project.schemes(), project).map { $0 }.sorted() print(colorize("\nSelect the schemes necessary to build your chosen targets:", .bold)) configuration.schemes = select(multiple: schemes, allowAll: false).selectedValues @@ -79,17 +79,17 @@ public final class XcodeProjectSetupGuide: SetupGuideHelpers, ProjectSetupGuide // MARK: - Private - private func getPodSchemes(in project: XcodeProjectlike) throws -> [String] { + private func getPodSchemes(in project: XcodeProjectlike) throws -> Set { let path = project.sourceRoot.appending("Pods/Pods.xcodeproj") guard path.exists else { return [] } return try xcodebuild.schemes(type: "project", path: path.lexicallyNormalized().string) } - private func filter(_ schemes: Set, _ project: XcodeProjectlike) throws -> [XcodeScheme] { - let podSchemes = try Set(getPodSchemes(in: project)) + private func filter(_ schemes: Set, _ project: XcodeProjectlike) throws -> [String] { + let podSchemes = try getPodSchemes(in: project) return schemes - .filter { !$0.name.hasPrefix("Pods-") } - .filter { !podSchemes.contains($0.name) } + .filter { !$0.hasPrefix("Pods-") } + .filter { !podSchemes.contains($0) } } private func identifyWorkspace() -> FilePath? { diff --git a/Sources/XcodeSupport/XcodeProjectlike.swift b/Sources/XcodeSupport/XcodeProjectlike.swift index 30a0c54495..a82c4623b7 100644 --- a/Sources/XcodeSupport/XcodeProjectlike.swift +++ b/Sources/XcodeSupport/XcodeProjectlike.swift @@ -10,7 +10,7 @@ protocol XcodeProjectlike: AnyObject { var name: String { get } var sourceRoot: FilePath { get } - func schemes() throws -> Set + func schemes() throws -> Set } extension XcodeProjectlike { diff --git a/Sources/XcodeSupport/XcodeScheme.swift b/Sources/XcodeSupport/XcodeScheme.swift deleted file mode 100644 index bb9e23fbd3..0000000000 --- a/Sources/XcodeSupport/XcodeScheme.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Foundation -import PeripheryKit -import Shared - -final class XcodeScheme { - let project: XcodeProjectlike - let name: String - - private var didPopulate: Bool = false - private let xcodebuild: Xcodebuild - private var testTargetsProperty: [String] = [] - - required init(project: XcodeProjectlike, name: String, xcodebuild: Xcodebuild = .init()) throws { - self.project = project - self.name = name - self.xcodebuild = xcodebuild - } - - func testTargets() throws -> [String] { - try populate() - return testTargetsProperty - } - - // MARK: - Private - - private func populate() throws { - guard !didPopulate else { return } - - didPopulate = true - let settings = try xcodebuild.buildSettings(for: project, scheme: name) - let parser = XcodebuildSettingsParser(settings: settings) - testTargetsProperty = parser.buildTargets(action: "test") - } -} - -extension XcodeScheme: Hashable { - func hash(into hasher: inout Hasher) { - hasher.combine(name) - } -} - -extension XcodeScheme: Equatable { - static func == (lhs: XcodeScheme, rhs: XcodeScheme) -> Bool { - return lhs.name == rhs.name - } -} diff --git a/Sources/XcodeSupport/XcodeSchemeAction.swift b/Sources/XcodeSupport/XcodeSchemeAction.swift new file mode 100644 index 0000000000..893b2e931e --- /dev/null +++ b/Sources/XcodeSupport/XcodeSchemeAction.swift @@ -0,0 +1,24 @@ +import Foundation +import Shared + +struct XcodeBuildAction: Decodable { + let target: String + let buildSettings: [String: String] + + func makeTargetTriple() throws -> String { + let arch = buildSettings["CURRENT_ARCH"] + let vendor = buildSettings["LLVM_TARGET_TRIPLE_VENDOR"] + let osVersion = buildSettings["LLVM_TARGET_TRIPLE_OS_VERSION"] + + if let arch, let vendor, let osVersion { + return "\(arch)-\(vendor)-\(osVersion)" + } else { + throw PeripheryError.invalidTargetTriple( + target: target, + arch: "ARCH = \(String(describing: arch))", + vendor: "LLVM_TARGET_TRIPLE_VENDOR = \(String(describing: vendor))", + osVersion: "LLVM_TARGET_TRIPLE_OS_VERSION = \(String(describing: osVersion))" + ) + } + } +} diff --git a/Sources/XcodeSupport/XcodeTarget.swift b/Sources/XcodeSupport/XcodeTarget.swift index 5dd078a40c..1fec2a3a55 100644 --- a/Sources/XcodeSupport/XcodeTarget.swift +++ b/Sources/XcodeSupport/XcodeTarget.swift @@ -6,6 +6,7 @@ import Shared final class XcodeTarget { let project: XcodeProject + var triple: String? private let target: PBXTarget private var files: [ProjectFileKind: Set] = [:] diff --git a/Sources/XcodeSupport/XcodeWorkspace.swift b/Sources/XcodeSupport/XcodeWorkspace.swift index 3916e84f67..28a07d6173 100644 --- a/Sources/XcodeSupport/XcodeWorkspace.swift +++ b/Sources/XcodeSupport/XcodeWorkspace.swift @@ -42,11 +42,8 @@ final class XcodeWorkspace: XcodeProjectlike { } } - func schemes() throws -> Set { - let schemes = try xcodebuild.schemes(project: self).map { - try XcodeScheme(project: self, name: $0) - } - return Set(schemes) + func schemes() throws -> Set { + try xcodebuild.schemes(project: self) } // MARK: - Private diff --git a/Sources/XcodeSupport/Xcodebuild.swift b/Sources/XcodeSupport/Xcodebuild.swift index 0497952395..dcf715d275 100644 --- a/Sources/XcodeSupport/Xcodebuild.swift +++ b/Sources/XcodeSupport/Xcodebuild.swift @@ -23,11 +23,11 @@ public final class Xcodebuild { } @discardableResult - func build(project: XcodeProjectlike, scheme: XcodeScheme, allSchemes: [XcodeScheme], additionalArguments: [String] = [], buildForTesting: Bool = false) throws -> String { + func build(project: XcodeProjectlike, scheme: String, allSchemes: [String], additionalArguments: [String] = [], buildForTesting: Bool = false) throws -> String { let cmd = buildForTesting ? "build-for-testing" : "build" let args = [ "-\(project.type)", "'\(project.path.lexicallyNormalized().string)'", - "-scheme", "'\(scheme.name)'", + "-scheme", "'\(scheme)'", "-parallelizeTargets", "-derivedDataPath", "'\(try derivedDataPath(for: project, schemes: allSchemes).string)'", "-quiet" @@ -43,11 +43,11 @@ public final class Xcodebuild { return try shell.exec(["/bin/sh", "-c", xcodebuild]) } - func removeDerivedData(for project: XcodeProjectlike, allSchemes: [XcodeScheme]) throws { + func removeDerivedData(for project: XcodeProjectlike, allSchemes: [String]) throws { try shell.exec(["rm", "-rf", try derivedDataPath(for: project, schemes: allSchemes).string]) } - func indexStorePath(project: XcodeProjectlike, schemes: [XcodeScheme]) throws -> FilePath { + func indexStorePath(project: XcodeProjectlike, schemes: [String]) throws -> FilePath { let derivedDataPath = try derivedDataPath(for: project, schemes: schemes) let pathsToTry = ["Index.noindex/DataStore", "Index/DataStore"] .map { derivedDataPath.appending($0) } @@ -57,11 +57,11 @@ public final class Xcodebuild { return path } - func schemes(project: XcodeProjectlike) throws -> [String] { - return try schemes(type: project.type, path: project.path.lexicallyNormalized().string) + func schemes(project: XcodeProjectlike) throws -> Set { + try schemes(type: project.type, path: project.path.lexicallyNormalized().string) } - func schemes(type: String, path: String) throws -> [String] { + func schemes(type: String, path: String) throws -> Set { let args = [ "-\(type)", path, "-list", @@ -85,24 +85,30 @@ public final class Xcodebuild { let details = json[type] as? [String: Any], let schemes = details["schemes"] as? [String] else { return [] } - return schemes + return Set(schemes) } - func buildSettings(for project: XcodeProjectlike, scheme: String) throws -> String { - let args = [ - "-\(project.type)", project.path.lexicallyNormalized().string, - "-showBuildSettings", - "-scheme", scheme - ] - - do { - // Schemes that are not configured for testing will result in an error if the 'test' - // action is supplied. - // Note: we don't use -skipUnavailableActions here as it returns incorrect output. - return try shell.exec(["xcodebuild"] + args + ["build", "test"], stderr: false) - } catch PeripheryError.shellCommandFailed(_, _, _, _) { - return try shell.exec(["xcodebuild"] + args + ["build"], stderr: false) - } + func buildSettings(targets: Set) throws -> [XcodeBuildAction] { + try targets + .reduce(into: [XcodeProject: Set]()) { result, target in + result[target.project, default: []].insert(target.name) + } + .reduce(into: [XcodeBuildAction]()) { result, pair in + let (project, targets) = pair + let args = [ + "-project", project.path.lexicallyNormalized().string, + "-showBuildSettings", + "-json" + ] + targets.flatMap { ["-target", $0] } + + let output = try shell.exec(["xcodebuild"] + args, stderr: false) + + guard let data = output.data(using: .utf8) else { return } + + let decoder = JSONDecoder() + let actions = try decoder.decode([XcodeBuildAction].self, from: data) + result.append(contentsOf: actions) + } } // MARK: - Private @@ -116,7 +122,7 @@ public final class Xcodebuild { } } - private func derivedDataPath(for project: XcodeProjectlike, schemes: [XcodeScheme]) throws -> FilePath { + private func derivedDataPath(for project: XcodeProjectlike, schemes: [String]) throws -> FilePath { // Given a project with two schemes: A and B, a scenario can arise where the index store contains conflicting // data. If scheme A is built, then the source file modified and then scheme B built, the index store will // contain two records for that source file. One reflects the state of the file when scheme A was built, and the @@ -124,7 +130,7 @@ public final class Xcodebuild { let xcodeVersionHash = try version().djb2Hex let projectHash = project.name.djb2Hex - let schemesHash = schemes.map { $0.name }.joined().djb2Hex + let schemesHash = schemes.map { $0 }.joined().djb2Hex return try Constants.cachePath().appending("DerivedData-\(xcodeVersionHash)-\(projectHash)-\(schemesHash)") } diff --git a/Sources/XcodeSupport/XcodebuildSettingsParser.swift b/Sources/XcodeSupport/XcodebuildSettingsParser.swift deleted file mode 100644 index be7a6e2d3f..0000000000 --- a/Sources/XcodeSupport/XcodebuildSettingsParser.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -final class XcodebuildSettingsParser { - private let lines: [String] - - init(settings: String) { - self.lines = settings.split(separator: "\n").map { String($0) } - } - - func buildTargets(action: String) -> [String] { - let sectionTitle = "Build settings for action \(action) and target" - - let sectionIndicies = lines.indices.compactMap { - lines[$0].contains(sectionTitle) ? $0 : nil - } - - return sectionIndicies.compactMap { - if let targetNameLine = lines[$0.. + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/XcodeTests/XcodeBuildActionTests.swift b/Tests/XcodeTests/XcodeBuildActionTests.swift new file mode 100644 index 0000000000..e6e26ca133 --- /dev/null +++ b/Tests/XcodeTests/XcodeBuildActionTests.swift @@ -0,0 +1,23 @@ +// +// File.swift +// +// +// Created by Ian Leitch on 02.07.23. +// + +import Foundation +import XCTest +@testable import XcodeSupport +@testable import PeripheryKit + +final class XcodeBuildActionTests: XCTestCase { + func testTargetTriples() throws { + let action = XcodeBuildAction(target: "Test", buildSettings: [ + "CURRENT_ARCH": "arm64", + "LLVM_TARGET_TRIPLE_VENDOR": "apple", + "LLVM_TARGET_TRIPLE_OS_VERSION": "ios16" + ]) + let triple = try action.makeTargetTriple() + XCTAssertEqual(triple, "arm64-apple-ios16") + } +} diff --git a/Tests/XcodeTests/XcodeSchemeTest.swift b/Tests/XcodeTests/XcodeSchemeTest.swift deleted file mode 100644 index 385962ec26..0000000000 --- a/Tests/XcodeTests/XcodeSchemeTest.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation -import XCTest -@testable import XcodeSupport -@testable import PeripheryKit - -class XcodeSchemeTest: XCTestCase { - private var scheme: XcodeScheme! - - override func setUp() { - let project = try! XcodeProject(path: UIKitProjectPath) - scheme = try! XcodeScheme(project: project, name: "UIKitProject") - } - - func testTargets() throws { - XCTAssertEqual(try scheme.testTargets().sorted(), ["UIKitProject", "UIKitProjectTests"]) - } -} diff --git a/Tests/XcodeTests/XcodebuildSettingsParserTest.swift b/Tests/XcodeTests/XcodebuildSettingsParserTest.swift deleted file mode 100644 index 3dd59bf10b..0000000000 --- a/Tests/XcodeTests/XcodebuildSettingsParserTest.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import XCTest -@testable import XcodeSupport -@testable import PeripheryKit - -class XcodebuildSettingsParserTest: XCTestCase { - private static var project: XcodeProject! - - private var xcodebuild: Xcodebuild! - - private var project: XcodeProject! { - return XcodebuildSettingsParserTest.project - } - - override static func setUp() { - super.setUp() - - project = try! XcodeProject(path: UIKitProjectPath) - } - - override func setUp() { - super.setUp() - - xcodebuild = Xcodebuild() - } - - func testBuildTargets() { - let settings = try! xcodebuild.buildSettings(for: project, scheme: "UIKitProject") - let parser = XcodebuildSettingsParser(settings: settings) - - XCTAssertEqual(parser.buildTargets(action: "build").sorted(), ["UIKitProject"]) - XCTAssertEqual(parser.buildTargets(action: "test").sorted(), ["UIKitProject", "UIKitProjectTests"]) - } -} diff --git a/Tests/XcodeTests/XcodebuildTest.swift b/Tests/XcodeTests/XcodebuildTest.swift index 282af7aa89..68555c2520 100644 --- a/Tests/XcodeTests/XcodebuildTest.swift +++ b/Tests/XcodeTests/XcodebuildTest.swift @@ -5,20 +5,18 @@ import Shared @testable import PeripheryKit class XcodebuildBuildProjectTest: XCTestCase { - var shell: Shell! var xcodebuild: Xcodebuild! var project: XcodeProject! override func setUp() { super.setUp() - shell = Shell.shared - xcodebuild = Xcodebuild(shell: shell) + xcodebuild = Xcodebuild(shell: .shared) project = try! XcodeProject(path: UIKitProjectPath) } func testBuildSchemeWithWhitespace() throws { - let scheme = try XcodeScheme(project: project, name: "Scheme With Spaces") + let scheme = "Scheme With Spaces" try xcodebuild.build(project: project, scheme: scheme, allSchemes: [scheme]) } } @@ -45,6 +43,24 @@ class XcodebuildSchemesTest: XCTestCase { } } +class XcodebuildSettingsTest: XCTestCase { + var xcodebuild: Xcodebuild! + var project: XcodeProject! + + override func setUp() { + super.setUp() + + xcodebuild = Xcodebuild(shell: .shared) + project = try! XcodeProject(path: UIKitProjectPath) + } + + func testBuildSettings() { + let actions = try! xcodebuild.buildSettings(targets: project.targets) + let buildTargets = actions.map(\.target).sorted() + XCTAssertEqual(buildTargets, ["NotificationServiceExtension", "Target With Spaces", "UIKitProject", "UIKitProjectTests"]) + } +} + class ShellMock: Shell { var output: String = ""