Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 56 additions & 5 deletions Sources/XcodeSupport/Xcodebuild.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,19 @@ public final class Xcodebuild {

public 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) }
guard let path = pathsToTry.first(where: { $0.exists }) else {
throw PeripheryError.indexStoreNotFound(derivedDataPath: derivedDataPath.string)

if let path = findIndexStorePath(in: derivedDataPath) {
return path
}

return path
// Periphery's own DerivedData may not exist when --skip-build is used and
// the project was built by Xcode or xcodebuild. Fall back to searching
// Xcode's default DerivedData location for a matching project directory.
if let path = findIndexStoreInDefaultDerivedData(projectName: project.name) {
return path
}

throw PeripheryError.indexStoreNotFound(derivedDataPath: derivedDataPath.string)
}

func schemes(project: XcodeProjectlike, additionalArguments: [String]) throws -> Set<String> {
Expand Down Expand Up @@ -119,6 +125,51 @@ public final class Xcodebuild {
}
}

func findIndexStorePath(in derivedDataPath: FilePath) -> FilePath? {
["Index.noindex/DataStore", "Index/DataStore"]
.map { derivedDataPath.appending($0) }
.first { $0.exists }
}

func findIndexStoreInDefaultDerivedData(projectName: String) -> FilePath? {
let defaultDerivedData = FilePath(
NSHomeDirectory()
).appending("Library/Developer/Xcode/DerivedData")

return findIndexStoreInDerivedData(projectName: projectName, derivedDataRoot: defaultDerivedData)
}

func findIndexStoreInDerivedData(projectName: String, derivedDataRoot: FilePath) -> FilePath? {
guard derivedDataRoot.exists else { return nil }

// Xcode names DerivedData subdirectories as "<ProjectName>-<hash>".
// Find the most recently modified one matching the project name.
let fm = FileManager.default
guard let entries = try? fm.contentsOfDirectory(atPath: derivedDataRoot.string) else {
return nil
}

let candidates = entries
.filter { $0.hasPrefix("\(projectName)-") }
.compactMap { entry -> (path: FilePath, modified: Date)? in
let entryPath = derivedDataRoot.appending(entry)
guard let attrs = try? fm.attributesOfItem(atPath: entryPath.string),
let modified = attrs[.modificationDate] as? Date else {
return nil
}
return (entryPath, modified)
}
.sorted { $0.modified > $1.modified }

for candidate in candidates {
if let path = findIndexStorePath(in: candidate.path) {
return path
}
}

return nil
}

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
Expand Down
144 changes: 144 additions & 0 deletions Tests/XcodeTests/XcodebuildIndexStoreTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import Foundation
import Logger
import SystemPackage
@testable import XcodeSupport
import XCTest

final class XcodebuildIndexStoreTest: XCTestCase {
private var xcodebuild: Xcodebuild!
private var tmpDir: FilePath!

override func setUp() {
super.setUp()

let logger = Logger(quiet: true, verbose: false, colorMode: .never)
let shell = ShellMock(output: "")
xcodebuild = Xcodebuild(shell: shell, logger: logger)
tmpDir = FilePath(NSTemporaryDirectory()).appending("XcodebuildIndexStoreTest-\(UUID().uuidString)")
try! FileManager.default.createDirectory(atPath: tmpDir.string, withIntermediateDirectories: true)
}

override func tearDown() {
try? FileManager.default.removeItem(atPath: tmpDir.string)
xcodebuild = nil
tmpDir = nil
super.tearDown()
}

// MARK: - findIndexStorePath(in:)

func testFindIndexStorePathFindsIndexNoindex() throws {
let dataStore = tmpDir.appending("Index.noindex/DataStore")
try FileManager.default.createDirectory(atPath: dataStore.string, withIntermediateDirectories: true)

let result = xcodebuild.findIndexStorePath(in: tmpDir)
XCTAssertEqual(result, dataStore)
}

func testFindIndexStorePathFindsLegacyIndex() throws {
let dataStore = tmpDir.appending("Index/DataStore")
try FileManager.default.createDirectory(atPath: dataStore.string, withIntermediateDirectories: true)

let result = xcodebuild.findIndexStorePath(in: tmpDir)
XCTAssertEqual(result, dataStore)
}

func testFindIndexStorePathPrefersIndexNoindex() throws {
let noindex = tmpDir.appending("Index.noindex/DataStore")
let legacy = tmpDir.appending("Index/DataStore")
try FileManager.default.createDirectory(atPath: noindex.string, withIntermediateDirectories: true)
try FileManager.default.createDirectory(atPath: legacy.string, withIntermediateDirectories: true)

let result = xcodebuild.findIndexStorePath(in: tmpDir)
XCTAssertEqual(result, noindex)
}

func testFindIndexStorePathReturnsNilWhenNoneExist() {
let result = xcodebuild.findIndexStorePath(in: tmpDir)
XCTAssertNil(result)
}

// MARK: - findIndexStoreInDerivedData(projectName:derivedDataRoot:)

func testFindIndexStoreInDerivedDataFindsMatchingProject() throws {
let projectDir = tmpDir.appending("MyProject-abc123")
let dataStore = projectDir.appending("Index.noindex/DataStore")
try FileManager.default.createDirectory(atPath: dataStore.string, withIntermediateDirectories: true)

let result = xcodebuild.findIndexStoreInDerivedData(projectName: "MyProject", derivedDataRoot: tmpDir)
XCTAssertEqual(result, dataStore)
}

func testFindIndexStoreInDerivedDataReturnsNilForNonMatchingProject() throws {
let projectDir = tmpDir.appending("OtherProject-abc123")
let dataStore = projectDir.appending("Index.noindex/DataStore")
try FileManager.default.createDirectory(atPath: dataStore.string, withIntermediateDirectories: true)

let result = xcodebuild.findIndexStoreInDerivedData(projectName: "MyProject", derivedDataRoot: tmpDir)
XCTAssertNil(result)
}

func testFindIndexStoreInDerivedDataReturnsNilWhenRootDoesNotExist() {
let nonexistent = tmpDir.appending("nonexistent")

let result = xcodebuild.findIndexStoreInDerivedData(projectName: "MyProject", derivedDataRoot: nonexistent)
XCTAssertNil(result)
}

func testFindIndexStoreInDerivedDataReturnsNilWhenNoIndexStore() throws {
let projectDir = tmpDir.appending("MyProject-abc123")
try FileManager.default.createDirectory(atPath: projectDir.string, withIntermediateDirectories: true)

let result = xcodebuild.findIndexStoreInDerivedData(projectName: "MyProject", derivedDataRoot: tmpDir)
XCTAssertNil(result)
}

func testFindIndexStoreInDerivedDataPrefersMostRecentlyModified() throws {
let fm = FileManager.default

// Create an older project directory with a valid index store
let olderDir = tmpDir.appending("MyProject-older111")
let olderDataStore = olderDir.appending("Index.noindex/DataStore")
try fm.createDirectory(atPath: olderDataStore.string, withIntermediateDirectories: true)

// Set its modification date to the past
try fm.setAttributes(
[.modificationDate: Date.distantPast],
ofItemAtPath: olderDir.string
)

// Create a newer project directory with a valid index store
let newerDir = tmpDir.appending("MyProject-newer222")
let newerDataStore = newerDir.appending("Index.noindex/DataStore")
try fm.createDirectory(atPath: newerDataStore.string, withIntermediateDirectories: true)

// Set its modification date to now
try fm.setAttributes(
[.modificationDate: Date()],
ofItemAtPath: newerDir.string
)

let result = xcodebuild.findIndexStoreInDerivedData(projectName: "MyProject", derivedDataRoot: tmpDir)
XCTAssertEqual(result, newerDataStore)
}

func testFindIndexStoreInDerivedDataDoesNotMatchExactName() throws {
// A directory named exactly "MyProject" (no hash suffix) should not match
let projectDir = tmpDir.appending("MyProject")
let dataStore = projectDir.appending("Index.noindex/DataStore")
try FileManager.default.createDirectory(atPath: dataStore.string, withIntermediateDirectories: true)

let result = xcodebuild.findIndexStoreInDerivedData(projectName: "MyProject", derivedDataRoot: tmpDir)
XCTAssertNil(result)
}

func testFindIndexStoreInDerivedDataDoesNotMatchPrefix() throws {
// "MyProjectExtra-abc123" should not match project name "MyProject"
let projectDir = tmpDir.appending("MyProjectExtra-abc123")
let dataStore = projectDir.appending("Index.noindex/DataStore")
try FileManager.default.createDirectory(atPath: dataStore.string, withIntermediateDirectories: true)

let result = xcodebuild.findIndexStoreInDerivedData(projectName: "MyProject", derivedDataRoot: tmpDir)
XCTAssertNil(result)
}
}