Support system installed toolchain (#157)
* Support system installed toolchain * Fix linux build * Apply latest swiftformat * Apply suggestions from code review Co-authored-by: Max Desiatov <max@desiatov.com> * Update Sources/SwiftToolchain/ToolchainResolver.swift Co-authored-by: Max Desiatov <max@desiatov.com> Co-authored-by: Max Desiatov <max@desiatov.com>
This commit is contained in:
parent
b9702ea285
commit
d13e7bad2e
|
@ -93,7 +93,8 @@ public final class Toolchain {
|
|||
_ fileSystem: FileSystem,
|
||||
_ terminal: InteractiveWriter
|
||||
) throws {
|
||||
let (swiftPath, version) = try fileSystem.inferSwiftPath(from: versionSpec, terminal)
|
||||
let toolchainSystem = ToolchainSystem(fileSystem: fileSystem)
|
||||
let (swiftPath, version) = try toolchainSystem.inferSwiftPath(from: versionSpec, terminal)
|
||||
self.swiftPath = swiftPath
|
||||
self.version = version
|
||||
self.fileSystem = fileSystem
|
||||
|
|
|
@ -25,7 +25,7 @@ import TSCUtility
|
|||
|
||||
private let expectedArchiveSize = 891_856_371
|
||||
|
||||
extension FileSystem {
|
||||
extension ToolchainSystem {
|
||||
func installSDK(
|
||||
version: String,
|
||||
from url: Foundation.URL,
|
||||
|
@ -33,11 +33,11 @@ extension FileSystem {
|
|||
_ client: HTTPClient,
|
||||
_ terminal: InteractiveWriter
|
||||
) throws -> AbsolutePath {
|
||||
if !exists(sdkPath, followSymlink: true) {
|
||||
try createDirectory(sdkPath, recursive: true)
|
||||
if !fileSystem.exists(sdkPath, followSymlink: true) {
|
||||
try fileSystem.createDirectory(sdkPath, recursive: true)
|
||||
}
|
||||
|
||||
guard isDirectory(sdkPath) else {
|
||||
guard fileSystem.isDirectory(sdkPath) else {
|
||||
throw ToolchainError.directoryDoesNotExist(sdkPath)
|
||||
}
|
||||
|
||||
|
@ -82,16 +82,16 @@ extension FileSystem {
|
|||
|
||||
let arguments: [String]
|
||||
if ext == "pkg" {
|
||||
guard let path = xcodeToolchainPath(for: version) else {
|
||||
guard let resolver = userXCToolchainResolver else {
|
||||
throw ToolchainError.noInstallationDirectory(path: "~/Library")
|
||||
}
|
||||
installationPath = path
|
||||
installationPath = resolver.toolchain(for: version)
|
||||
arguments = [
|
||||
"installer", "-target", "CurrentUserHomeDirectory", "-pkg", archivePath.pathString,
|
||||
]
|
||||
} else {
|
||||
installationPath = sdkPath.appending(component: version)
|
||||
try createDirectory(installationPath, recursive: true)
|
||||
try fileSystem.createDirectory(installationPath, recursive: true)
|
||||
|
||||
arguments = [
|
||||
"tar", "xzf", archivePath.pathString, "--strip-components=1",
|
||||
|
@ -101,7 +101,7 @@ extension FileSystem {
|
|||
terminal.logLookup("Unpacking the archive: ", arguments.joined(separator: " "))
|
||||
_ = try processDataOutput(arguments)
|
||||
|
||||
try removeFileTree(archivePath)
|
||||
try fileSystem.removeFileTree(archivePath)
|
||||
|
||||
return installationPath
|
||||
}
|
||||
|
|
|
@ -39,28 +39,50 @@ private struct Release: Decodable {
|
|||
let assets: [Asset]
|
||||
}
|
||||
|
||||
extension FileSystem {
|
||||
private var swiftenvVersionsPath: AbsolutePath {
|
||||
homeDirectory.appending(components: ".swiftenv", "versions")
|
||||
public class ToolchainSystem {
|
||||
let fileSystem: FileSystem
|
||||
let userXCToolchainResolver: XCToolchainResolver?
|
||||
let cartonToolchainResolver: CartonToolchainResolver
|
||||
let resolvers: [ToolchainResolver]
|
||||
|
||||
public init(fileSystem: FileSystem) {
|
||||
self.fileSystem = fileSystem
|
||||
|
||||
let userLibraryPath = NSSearchPathForDirectoriesInDomains(
|
||||
.libraryDirectory,
|
||||
.userDomainMask,
|
||||
true
|
||||
).first
|
||||
let rootLibraryPath = NSSearchPathForDirectoriesInDomains(
|
||||
.libraryDirectory,
|
||||
.localDomainMask,
|
||||
true
|
||||
).first
|
||||
userXCToolchainResolver = userLibraryPath.flatMap {
|
||||
XCToolchainResolver(libraryPath: AbsolutePath($0), fileSystem: fileSystem)
|
||||
}
|
||||
let rootXCToolchainResolver = rootLibraryPath.flatMap {
|
||||
XCToolchainResolver(libraryPath: AbsolutePath($0), fileSystem: fileSystem)
|
||||
}
|
||||
let xctoolchainResolvers: [ToolchainResolver] = [
|
||||
userXCToolchainResolver, rootXCToolchainResolver,
|
||||
].compactMap { $0 }
|
||||
|
||||
cartonToolchainResolver = CartonToolchainResolver(fileSystem: fileSystem)
|
||||
resolvers = [
|
||||
cartonToolchainResolver,
|
||||
SwiftEnvToolchainResolver(fileSystem: fileSystem),
|
||||
] + xctoolchainResolvers
|
||||
}
|
||||
|
||||
private var cartonSDKPath: AbsolutePath {
|
||||
homeDirectory.appending(components: ".carton", "sdk")
|
||||
}
|
||||
|
||||
private var xcodeToolchainsPath: AbsolutePath? {
|
||||
NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first
|
||||
.flatMap { .init($0) }
|
||||
}
|
||||
|
||||
func xcodeToolchainPath(for version: String) -> AbsolutePath? {
|
||||
xcodeToolchainsPath?.appending(
|
||||
components: "Developer", "Toolchains", "swift-\(version).xctoolchain"
|
||||
)
|
||||
private var libraryPaths: [AbsolutePath] {
|
||||
NSSearchPathForDirectoriesInDomains(
|
||||
.libraryDirectory, [.localDomainMask], true
|
||||
).map { AbsolutePath($0) }
|
||||
}
|
||||
|
||||
public var swiftVersionPath: AbsolutePath {
|
||||
guard let cwd = currentWorkingDirectory else {
|
||||
guard let cwd = fileSystem.currentWorkingDirectory else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
|
@ -68,8 +90,9 @@ extension FileSystem {
|
|||
}
|
||||
|
||||
private func getDirectoryPaths(_ directoryPath: AbsolutePath) throws -> [AbsolutePath] {
|
||||
if isDirectory(directoryPath) {
|
||||
return try getDirectoryContents(directoryPath).map { directoryPath.appending(component: $0) }
|
||||
if fileSystem.isDirectory(directoryPath) {
|
||||
return try fileSystem.getDirectoryContents(directoryPath)
|
||||
.map { directoryPath.appending(component: $0) }
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
@ -98,14 +121,6 @@ extension FileSystem {
|
|||
return version
|
||||
}
|
||||
|
||||
private func checkAndLog(
|
||||
swiftVersion: String,
|
||||
_ prefix: AbsolutePath,
|
||||
_ terminal: InteractiveWriter
|
||||
) throws -> AbsolutePath? {
|
||||
try checkAndLog(installationPath: prefix.appending(component: swiftVersion), terminal)
|
||||
}
|
||||
|
||||
private func checkAndLog(
|
||||
installationPath: AbsolutePath,
|
||||
_ terminal: InteractiveWriter
|
||||
|
@ -113,7 +128,7 @@ extension FileSystem {
|
|||
let swiftPath = installationPath.appending(components: "usr", "bin", "swift")
|
||||
|
||||
terminal.logLookup("- checking Swift compiler path: ", swiftPath)
|
||||
guard isFile(swiftPath) else { return nil }
|
||||
guard fileSystem.isFile(swiftPath) else { return nil }
|
||||
|
||||
terminal.write("Inferring basic settings...\n", inColor: .yellow)
|
||||
terminal.logLookup("- swift executable: ", swiftPath)
|
||||
|
@ -155,11 +170,11 @@ extension FileSystem {
|
|||
let platformSuffixes = ["osx", "catalina", "macos"]
|
||||
#elseif os(Linux)
|
||||
let releaseFile = AbsolutePath("/etc/lsb-release")
|
||||
guard isFile(releaseFile) else {
|
||||
guard fileSystem.isFile(releaseFile) else {
|
||||
throw ToolchainError.unsupportedOperatingSystem
|
||||
}
|
||||
|
||||
let releaseData = try readFileContents(releaseFile).description
|
||||
let releaseData = try fileSystem.readFileContents(releaseFile).description
|
||||
let ubuntuSuffix: String
|
||||
if releaseData.contains("DISTRIB_RELEASE=18.04") {
|
||||
ubuntuSuffix = "ubuntu18.04"
|
||||
|
@ -199,20 +214,13 @@ extension FileSystem {
|
|||
|
||||
let swiftVersion = try inferSwiftVersion(from: versionSpec, terminal)
|
||||
|
||||
if let path = try checkAndLog(swiftVersion: swiftVersion, swiftenvVersionsPath, terminal) {
|
||||
return (path, swiftVersion)
|
||||
}
|
||||
|
||||
let sdkPath = cartonSDKPath
|
||||
if let path = try checkAndLog(swiftVersion: swiftVersion, sdkPath, terminal) {
|
||||
return (path, swiftVersion)
|
||||
}
|
||||
|
||||
if
|
||||
let candidatePath = xcodeToolchainPath(for: swiftVersion),
|
||||
let path = try checkAndLog(installationPath: candidatePath, terminal)
|
||||
{
|
||||
return (path, swiftVersion)
|
||||
for resolver in resolvers {
|
||||
if let path = try checkAndLog(
|
||||
installationPath: resolver.toolchain(for: swiftVersion),
|
||||
terminal
|
||||
) {
|
||||
return (path, swiftVersion)
|
||||
}
|
||||
}
|
||||
|
||||
let client = HTTPClient(eventLoopGroupProvider: .createNew)
|
||||
|
@ -238,7 +246,7 @@ extension FileSystem {
|
|||
let installationPath = try installSDK(
|
||||
version: swiftVersion,
|
||||
from: downloadURL,
|
||||
to: sdkPath,
|
||||
to: cartonToolchainResolver.cartonSDKPath,
|
||||
client,
|
||||
terminal
|
||||
)
|
||||
|
@ -251,25 +259,24 @@ extension FileSystem {
|
|||
}
|
||||
|
||||
public func fetchAllSwiftVersions() throws -> [String] {
|
||||
try [cartonSDKPath, swiftenvVersionsPath].filter { isDirectory($0) }
|
||||
.map {
|
||||
try getDirectoryPaths($0).filter { isDirectory($0) }.map(\.basename)
|
||||
}
|
||||
.joined()
|
||||
try resolvers.flatMap { try $0.fetchVersions() }
|
||||
.filter { fileSystem.isDirectory($0.path) }
|
||||
.map(\.version)
|
||||
.sorted()
|
||||
}
|
||||
|
||||
public func fetchLocalSwiftVersion() throws -> String? {
|
||||
guard isFile(swiftVersionPath), let version = try readFileContents(swiftVersionPath)
|
||||
.validDescription?
|
||||
// get the first line of the file
|
||||
.components(separatedBy: CharacterSet.newlines).first
|
||||
guard fileSystem.isFile(swiftVersionPath),
|
||||
let version = try fileSystem.readFileContents(swiftVersionPath)
|
||||
.validDescription?
|
||||
// get the first line of the file
|
||||
.components(separatedBy: CharacterSet.newlines).first
|
||||
else { return nil }
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
public func setLocalSwiftVersion(_ version: String) throws {
|
||||
try writeFileContents(swiftVersionPath, bytes: ByteString([UInt8](version.utf8)))
|
||||
try fileSystem.writeFileContents(swiftVersionPath, bytes: ByteString([UInt8](version.utf8)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import TSCBasic
|
||||
|
||||
protocol ToolchainResolver {
|
||||
func fetchVersions() throws -> [(version: String, path: AbsolutePath)]
|
||||
func toolchain(for version: String) -> AbsolutePath
|
||||
}
|
||||
|
||||
final class XCToolchainResolver: ToolchainResolver {
|
||||
let toolchainsPath: AbsolutePath
|
||||
let fileSystem: FileSystem
|
||||
|
||||
init?(libraryPath: AbsolutePath, fileSystem: FileSystem) {
|
||||
toolchainsPath = libraryPath.appending(components: "Developer", "Toolchains")
|
||||
self.fileSystem = fileSystem
|
||||
guard fileSystem.isDirectory(libraryPath) else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func fetchVersions() throws -> [(version: String, path: AbsolutePath)] {
|
||||
let xctoolchains = try fileSystem.getDirectoryContents(toolchainsPath)
|
||||
return xctoolchains.compactMap {
|
||||
guard let name = Self.toolchainName(fromXCToolchain: $0) else { return nil }
|
||||
return (version: name, path: toolchainsPath.appending(component: $0))
|
||||
}
|
||||
}
|
||||
|
||||
func toolchain(for version: String) -> AbsolutePath {
|
||||
toolchainsPath.appending(component: Self.xctoolchainName(fromVersion: version))
|
||||
}
|
||||
|
||||
private static func toolchainName(fromXCToolchain xctoolchain: String) -> String? {
|
||||
let prefix = "swift-"
|
||||
let suffix = ".xctoolchain"
|
||||
guard xctoolchain.hasPrefix(prefix), xctoolchain.hasSuffix(suffix) else {
|
||||
return nil
|
||||
}
|
||||
return String(xctoolchain.dropFirst(prefix.count).dropLast(suffix.count))
|
||||
}
|
||||
|
||||
private static func xctoolchainName(fromVersion version: String) -> String {
|
||||
"swift-\(version).xctoolchain"
|
||||
}
|
||||
}
|
||||
|
||||
final class SwiftEnvToolchainResolver: ToolchainResolver {
|
||||
let versionsPath: AbsolutePath
|
||||
let fileSystem: FileSystem
|
||||
|
||||
init(fileSystem: FileSystem) {
|
||||
versionsPath = fileSystem.homeDirectory.appending(components: ".swiftenv", "versions")
|
||||
self.fileSystem = fileSystem
|
||||
}
|
||||
|
||||
func fetchVersions() throws -> [(version: String, path: AbsolutePath)] {
|
||||
let versions = try fileSystem.getDirectoryContents(versionsPath)
|
||||
return versions.map {
|
||||
(version: $0, path: versionsPath.appending(component: $0))
|
||||
}
|
||||
}
|
||||
|
||||
func toolchain(for version: String) -> AbsolutePath {
|
||||
versionsPath.appending(component: version)
|
||||
}
|
||||
}
|
||||
|
||||
final class CartonToolchainResolver: ToolchainResolver {
|
||||
let cartonSDKPath: AbsolutePath
|
||||
let fileSystem: FileSystem
|
||||
|
||||
init(fileSystem: FileSystem) {
|
||||
cartonSDKPath = fileSystem.homeDirectory.appending(components: ".carton", "sdk")
|
||||
self.fileSystem = fileSystem
|
||||
}
|
||||
|
||||
func fetchVersions() throws -> [(version: String, path: AbsolutePath)] {
|
||||
let versions = try fileSystem.getDirectoryContents(cartonSDKPath)
|
||||
return versions.map {
|
||||
(version: $0, path: cartonSDKPath.appending(component: $0))
|
||||
}
|
||||
}
|
||||
|
||||
func toolchain(for version: String) -> AbsolutePath {
|
||||
cartonSDKPath.appending(component: version)
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import ArgumentParser
|
||||
import CartonHelpers
|
||||
import SwiftToolchain
|
||||
import TSCBasic
|
||||
|
||||
struct Local: ParsableCommand {
|
||||
|
@ -27,8 +28,9 @@ struct Local: ParsableCommand {
|
|||
func run() throws {
|
||||
let terminal = InteractiveWriter.stdout
|
||||
|
||||
guard let localVersion = try localFileSystem.fetchLocalSwiftVersion() else {
|
||||
terminal.logLookup("Version file is not present: ", localFileSystem.swiftVersionPath)
|
||||
let toolchainSystem = ToolchainSystem(fileSystem: localFileSystem)
|
||||
guard let localVersion = try toolchainSystem.fetchLocalSwiftVersion() else {
|
||||
terminal.logLookup("Version file is not present: ", toolchainSystem.swiftVersionPath)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -37,9 +39,9 @@ struct Local: ParsableCommand {
|
|||
return
|
||||
}
|
||||
|
||||
let versions = try localFileSystem.fetchAllSwiftVersions()
|
||||
let versions = try toolchainSystem.fetchAllSwiftVersions()
|
||||
if versions.contains(version) {
|
||||
_ = try localFileSystem.setLocalSwiftVersion(version)
|
||||
_ = try toolchainSystem.setLocalSwiftVersion(version)
|
||||
} else {
|
||||
terminal.write("The version \(version) hasn't been installed!", inColor: .red)
|
||||
}
|
||||
|
|
|
@ -25,8 +25,9 @@ struct Versions: ParsableCommand {
|
|||
func run() throws {
|
||||
let terminal = InteractiveWriter.stdout
|
||||
|
||||
let versions = try localFileSystem.fetchAllSwiftVersions()
|
||||
let localVersion = try localFileSystem.fetchLocalSwiftVersion()
|
||||
let toolchainSystem = ToolchainSystem(fileSystem: localFileSystem)
|
||||
let versions = try toolchainSystem.fetchAllSwiftVersions()
|
||||
let localVersion = try toolchainSystem.fetchLocalSwiftVersion()
|
||||
if versions.count > 0 {
|
||||
versions.forEach { version in
|
||||
if version == (localVersion ?? "") {
|
||||
|
|
Loading…
Reference in New Issue