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
18 changes: 17 additions & 1 deletion Sources/XcodesKit/RuntimeInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,14 @@ public final class RuntimeInstaller: Sendable {

public func downloadAndInstallRuntime(identifier: String, to destinationDirectory: Path, with downloader: Downloader, shouldDelete: Bool, architectures: [ArchitectureFilter] = []) async throws {
let matchedRuntime = try await getMatchingRuntime(identifier: identifier, architectures: architectures)
let runtimeName = downloadName(for: matchedRuntime, architectures: architectures)

if await runtimeIsAlreadyInstalled(matchedRuntime) {
Current.logging.log("Runtime \(runtimeName) is already installed")
return
}

let method = try await installMethod(for: matchedRuntime)
let runtimeName = downloadName(for: matchedRuntime, architectures: architectures)

switch method {
case .archive:
Expand Down Expand Up @@ -342,6 +347,17 @@ public final class RuntimeInstaller: Sendable {
) != nil
}

private func runtimeIsAlreadyInstalled(_ runtime: DownloadableRuntime) async -> Bool {
guard let installedRuntimes = try? await runtimeService.localInstalledRuntimes() else {
return false
}

return RuntimeInstallationLookupService().coreSimulatorImage(
for: runtime,
in: installedRuntimes
) != nil
}

private func isDuplicateRuntimeInstallError(_ error: Swift.Error) -> Bool {
guard let error = error as? ProcessExecutionError else { return false }
let output = error.standardOutput + "\n" + error.standardError
Expand Down
31 changes: 29 additions & 2 deletions Tests/XcodesKitTests/RuntimeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ final class RuntimeTests: XCTestCase {
XCTAssertFalse(log.value.contains("Apple Silicon (arm64)"))
}

func test_downloadAndInstallRuntimeTreatsDuplicateXcodebuildRuntimeAsAlreadyInstalled() async throws {
func test_downloadAndInstallRuntimeSkipsAlreadyInstalledXcodebuildRuntime() async throws {
let log = LockedBox("")
let attempts = LockedBox(0)
XcodesCLIKit.Current.logging.log = { log.append($0 + "\n") }
Expand Down Expand Up @@ -328,10 +328,37 @@ final class RuntimeTests: XCTestCase {
shouldDelete: true
)

XCTAssertEqual(attempts.value, 1)
XCTAssertEqual(attempts.value, 0)
XCTAssertTrue(log.value.contains("Runtime iOS 16.0 - Apple Silicon (arm64) is already installed"))
}

func test_downloadAndInstallRuntimeSkipsAlreadyInstalledDiskImage() async throws {
let log = LockedBox("")
let didInstall = LockedBox(false)
XcodesCLIKit.Current.logging.log = { log.append($0 + "\n") }
Current.shell.isatty = { false }
Current.files.contentsAtPath = { path in
guard path == "/Library/Developer/CoreSimulator/images/images.plist" else { return nil }
return Self.installedRuntimeImagesPlistData()
}
Current.shell.installRuntimeImage = { _ in
didInstall.set(true)
return Shell.processOutputMock
}
mockDownloadables(data: Self.duplicateArchitectureRuntimePlistData())

try await runtimeInstaller.downloadAndInstallRuntime(
identifier: "iOS 16.0",
to: .xcodesCaches,
with: .urlSession,
shouldDelete: true,
architectures: [.variant(.appleSilicon)]
)

XCTAssertFalse(didInstall.value)
XCTAssertTrue(log.value.contains("Runtime iOS 16.0 is already installed"))
}

func test_installStepsForPackage() async throws {
mockDownloadables()
let expectedSteps = [
Expand Down