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:
Yuta Saito 2020-11-07 21:29:00 +09:00 committed by GitHub
parent b9702ea285
commit d13e7bad2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 167 additions and 70 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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)))
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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 ?? "") {