Skip to content

Commit

Permalink
Fix indexing multi-platform projects. Closes #597 (#628)
Browse files Browse the repository at this point in the history
  • Loading branch information
ileitch authored Jul 2, 2023
1 parent ff390c7 commit e5d6f5a
Show file tree
Hide file tree
Showing 24 changed files with 267 additions and 212 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
11 changes: 6 additions & 5 deletions Sources/PeripheryKit/Generic/GenericProjectDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public final class GenericProjectDriver {
decoder.keyDecodingStrategy = .convertFromSnakeCase

let sourceFiles = try configuration.fileTargetsPath
.reduce(into: [FilePath: Set<String>]()) { result, mapPath in
.reduce(into: [FilePath: Set<IndexTarget>]()) { result, mapPath in
guard mapPath.exists else {
throw PeripheryError.pathDoesNotExist(path: mapPath.string)
}
Expand All @@ -18,26 +18,27 @@ public final class GenericProjectDriver {
let map = try decoder
.decode(FileTargetMapContainer.self, from: data)
.fileTargets
.reduce(into: [FilePath: Set<String>](), { (result, tuple) in
.reduce(into: [FilePath: Set<IndexTarget>](), { (result, tuple) in
let (key, value) = tuple
let path = FilePath.makeAbsolute(key)

if !path.exists {
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) }
}

return self.init(sourceFiles: sourceFiles, configuration: configuration)
}

private let sourceFiles: [FilePath: Set<String>]
private let sourceFiles: [FilePath: Set<IndexTarget>]
private let configuration: Configuration

init(sourceFiles: [FilePath: Set<String>], configuration: Configuration) {
init(sourceFiles: [FilePath: Set<IndexTarget>], configuration: Configuration) {
self.sourceFiles = sourceFiles
self.configuration = configuration
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/PeripheryKit/Indexer/IndexTarget.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 1 addition & 1 deletion Sources/PeripheryKit/Indexer/InfoPlistParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
23 changes: 18 additions & 5 deletions Sources/PeripheryKit/Indexer/SwiftIndexer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import SystemPackage
import Shared

public final class SwiftIndexer: Indexer {
private let sourceFiles: [FilePath: Set<String>]
private let sourceFiles: [FilePath: Set<IndexTarget>]
private let graph: SourceGraph
private let logger: ContextualLogger
private let configuration: Configuration
private let indexStorePaths: [FilePath]

public required init(
sourceFiles: [FilePath: Set<String>],
sourceFiles: [FilePath: Set<IndexTarget>],
graph: SourceGraph,
indexStorePaths: [FilePath],
logger: Logger = .init(),
Expand All @@ -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())
Expand All @@ -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
Expand All @@ -62,7 +75,7 @@ public final class SwiftIndexer: Indexer {

if !unindexedFiles.isEmpty {
unindexedFiles.forEach { logger.debug("Source file not indexed: \($0)") }
let targets: Set<String> = Set(unindexedFiles.flatMap { sourceFiles[$0] ?? [] })
let targets = unindexedFiles.flatMapSet { sourceFiles[$0] ?? [] }.mapSet { $0.name }
throw PeripheryError.unindexedTargetsError(targets: targets, indexStorePaths: indexStorePaths)
}

Expand Down
7 changes: 5 additions & 2 deletions Sources/PeripheryKit/SPM/SPMProjectDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,12 @@ extension SPMProjectDriver: ProjectDriver {
}

public func index(graph: SourceGraph) throws {
let sourceFiles = targets.reduce(into: [FilePath: Set<String>]()) { result, target in
let sourceFiles = targets.reduce(into: [FilePath: Set<IndexTarget>]()) { 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]
Expand Down
7 changes: 7 additions & 0 deletions Sources/Shared/Extensions/Sequence+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,11 @@ public extension Sequence {
}
}
}

func mapDict<Key, Value>(_ transform: (Element) throws -> (Key, Value)) rethrows -> Dictionary<Key, Value> {
try reduce(into: .init()) { result, element in
let pair = try transform(element)
result[pair.0] = pair.1
}
}
}
4 changes: 4 additions & 0 deletions Sources/Shared/Logger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
9 changes: 4 additions & 5 deletions Sources/Shared/PeripheryError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<String>)
case invalidTargetTriple(target: String, arch: String, vendor: String, osVersion: String)

public var errorDescription: String? {
switch self {
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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)"
}
}

Expand Down
7 changes: 2 additions & 5 deletions Sources/XcodeSupport/XcodeProject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,8 @@ final class XcodeProject: XcodeProjectlike {
}
}

func schemes() throws -> Set<XcodeScheme> {
let schemes = try xcodebuild.schemes(project: self).map {
try XcodeScheme(project: self, name: $0)
}
return Set(schemes)
func schemes() throws -> Set<String> {
try xcodebuild.schemes(project: self)
}
}

Expand Down
45 changes: 22 additions & 23 deletions Sources/XcodeSupport/XcodeProjectDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? "")
Expand All @@ -66,7 +66,7 @@ public final class XcodeProjectDriver {
private let configuration: Configuration
private let xcodebuild: Xcodebuild
private let project: XcodeProjectlike
private let schemes: Set<XcodeScheme>
private let schemes: Set<String>
private let targets: Set<XcodeTarget>
private let packageTargets: [SPM.Package: Set<SPM.Target>]

Expand All @@ -75,7 +75,7 @@ public final class XcodeProjectDriver {
configuration: Configuration = .shared,
xcodebuild: Xcodebuild = .init(),
project: XcodeProjectlike,
schemes: Set<XcodeScheme>,
schemes: Set<String>,
targets: Set<XcodeTarget>,
packageTargets: [SPM.Package: Set<SPM.Target>]
) {
Expand Down Expand Up @@ -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
}
}

Expand All @@ -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,
Expand All @@ -165,10 +160,13 @@ extension XcodeProjectDriver: ProjectDriver {

try targets.forEach { try $0.identifyFiles() }

var sourceFiles: [FilePath: Set<String>] = [:]
var sourceFiles: [FilePath: Set<IndexTarget>] = [:]

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 {
Expand All @@ -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) }
}
}

Expand Down
12 changes: 6 additions & 6 deletions Sources/XcodeSupport/XcodeProjectSetupGuide.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String> {
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<XcodeScheme>, _ project: XcodeProjectlike) throws -> [XcodeScheme] {
let podSchemes = try Set(getPodSchemes(in: project))
private func filter(_ schemes: Set<String>, _ 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? {
Expand Down
2 changes: 1 addition & 1 deletion Sources/XcodeSupport/XcodeProjectlike.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ protocol XcodeProjectlike: AnyObject {
var name: String { get }
var sourceRoot: FilePath { get }

func schemes() throws -> Set<XcodeScheme>
func schemes() throws -> Set<String>
}

extension XcodeProjectlike {
Expand Down
Loading

0 comments on commit e5d6f5a

Please sign in to comment.