diff --git a/Sources/Swiftly/Install.swift b/Sources/Swiftly/Install.swift index d7eb33f1..abb85361 100644 --- a/Sources/Swiftly/Install.swift +++ b/Sources/Swiftly/Install.swift @@ -159,6 +159,12 @@ struct Install: SwiftlyCommand { let swiftlyBinDirContents = (try? await fs.ls(atPath: swiftlyBinDir)) ?? [String]() let toolchainBinDir = try await Swiftly.currentPlatform.findToolchainBinDir(ctx, version) + guard try await fs.exists(atPath: toolchainBinDir) else { + throw SwiftlyError( + message: + "Toolchain \(version) could not be located in \(toolchainBinDir). You can try `swiftly uninstall \(version)` to uninstall it and then `swiftly install \(version)` to install it again." + ) + } let toolchainBinDirContents = try await fs.ls(atPath: toolchainBinDir) var existingProxies: [String] = [] @@ -250,18 +256,26 @@ struct Install: SwiftlyCommand { assumeYes: Bool, progressFile: FilePath? = nil ) async throws -> (postInstall: String?, pathChanged: Bool) { - guard !config.installedToolchains.contains(version) else { - let installInfo = InstallInfo( - version: version, alreadyInstalled: true - ) - let pathChanged = if useInstalledToolchain { - try await Use.execute(ctx, version, globalDefault: false, verbose: verbose, &config) - } else { - false + if config.installedToolchains.contains(version) { + let toolchainBinDir = try await Swiftly.currentPlatform.findToolchainBinDir(ctx, version) + + if try await fs.exists(atPath: toolchainBinDir) { + let installInfo = InstallInfo( + version: version, alreadyInstalled: true + ) + let pathChanged = if useInstalledToolchain { + try await Use.execute(ctx, version, globalDefault: false, verbose: verbose, &config) + } else { + false + } + + try await ctx.output(installInfo) + return (nil, pathChanged) } - try await ctx.output(installInfo) - return (nil, pathChanged) + await ctx.message( + "Toolchain \(version) can no longer be located in \(toolchainBinDir). You can try `swiftly uninstall \(version.name); swiftly install \(version.name)` to reinstall it." + ) } // Ensure the system is set up correctly before downloading it. Problems that prevent installation diff --git a/Tests/SwiftlyTests/InstallTests.swift b/Tests/SwiftlyTests/InstallTests.swift index 28b60331..43cf4063 100644 --- a/Tests/SwiftlyTests/InstallTests.swift +++ b/Tests/SwiftlyTests/InstallTests.swift @@ -239,6 +239,26 @@ import Testing #expect(before == after) } + /// Tests that a toolchain that is installed according to the config, but missing from disk, is installed again. + @Test(.testHomeMockedToolchain()) func installToolchainMissingFromDisk() async throws { + try await SwiftlyTests.runCommand(Install.self, ["install", ToolchainVersion.newStable.name, "--post-install-file=\(fs.mktemp())"]) + + try await fs.remove(atPath: Swiftly.currentPlatform.findToolchainLocation(SwiftlyTests.ctx, .newStable)) + + let output = try await SwiftlyTests.runWithMockedIO( + Install.self, ["install", ToolchainVersion.newStable.name, "--post-install-file=\(fs.mktemp())", "--format", "json"], format: .json + ) + + let installInfo = try JSONDecoder().decode( + InstallInfo.self, + from: output[0].data(using: .utf8)! + ) + + #expect(installInfo.alreadyInstalled == false) + + try await SwiftlyTests.validateInstalledToolchains([.newStable], description: "install a toolchain missing from disk") + } + /// Verify that the installed toolchain will be used if no toolchains currently are installed. @Test(.testHomeMockedToolchain()) func installUsesFirstToolchain() async throws { let config = try await Config.load() diff --git a/Tests/SwiftlyTests/UseTests.swift b/Tests/SwiftlyTests/UseTests.swift index 70687a4f..ef497647 100644 --- a/Tests/SwiftlyTests/UseTests.swift +++ b/Tests/SwiftlyTests/UseTests.swift @@ -244,6 +244,16 @@ import Testing ) } + /// Tests that the `use` command reports a toolchain that is installed according to the config, but missing from disk. + @Test(.mockedSwiftlyVersion(), .mockHomeToolchains(toolchains: [.newStable])) func useToolchainMissingFromDisk() async throws { + try "swiftly binary".write(to: Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx) / "swiftly", atomically: true) + try await fs.remove(atPath: Swiftly.currentPlatform.findToolchainLocation(SwiftlyTests.ctx, .newStable)) + + await #expect(throws: SwiftlyError.self) { + try await SwiftlyTests.runCommand(Use.self, ["use", ToolchainVersion.newStable.name]) + } + } + /// Tests that the `use` command works with all the installed toolchains in this test harness. @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useAll() async throws { let config = try await Config.load()