carton/Sources/SwiftToolchain/ToolchainManagement.swift

283 lines
8.8 KiB
Swift

// Copyright 2020 Carton contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import AsyncHTTPClient
import CartonHelpers
import Foundation
import TSCBasic
import TSCUtility
public func processStringOutput(_ arguments: [String]) throws -> String? {
try ByteString(processDataOutput(arguments)).validDescription
}
// swiftlint:disable:next force_try
private let versionRegEx = try! RegEx(pattern: "(?:swift-)?(.+-.)-.+\\.tar.gz")
private struct Release: Decodable {
struct Asset: Decodable {
enum CodingKeys: String, CodingKey {
case name
case url = "browser_download_url"
}
let name: String
let url: Foundation.URL
}
let assets: [Asset]
}
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 libraryPaths: [AbsolutePath] {
NSSearchPathForDirectoriesInDomains(
.libraryDirectory, [.localDomainMask], true
).map { AbsolutePath($0) }
}
public var swiftVersionPath: AbsolutePath {
guard let cwd = fileSystem.currentWorkingDirectory else {
fatalError()
}
return cwd.appending(component: ".swift-version")
}
private func getDirectoryPaths(_ directoryPath: AbsolutePath) throws -> [AbsolutePath] {
if fileSystem.isDirectory(directoryPath) {
return try fileSystem.getDirectoryContents(directoryPath)
.map { directoryPath.appending(component: $0) }
} else {
return []
}
}
func inferSwiftVersion(
from versionSpec: String? = nil,
_ terminal: InteractiveWriter
) throws -> String {
if let versionSpec = versionSpec {
if let url = URL(string: versionSpec),
let filename = url.pathComponents.last,
let match = versionRegEx.matchGroups(in: filename).first?.first
{
terminal.logLookup("Inferred swift version: ", match)
return match
} else {
return versionSpec
}
}
guard let version = try fetchLocalSwiftVersion(), version.contains("wasm") else {
return defaultToolchainVersion
}
return version
}
private func checkAndLog(
installationPath: AbsolutePath,
_ terminal: InteractiveWriter
) throws -> AbsolutePath? {
let swiftPath = installationPath.appending(components: "usr", "bin", "swift")
terminal.logLookup("- checking Swift compiler path: ", swiftPath)
guard fileSystem.isFile(swiftPath) else { return nil }
terminal.write("Inferring basic settings...\n", inColor: .yellow)
terminal.logLookup("- swift executable: ", swiftPath)
if let output = try processStringOutput([swiftPath.pathString, "--version"]) {
terminal.write(output)
}
return swiftPath
}
private func inferDownloadURL(
from version: String,
_ client: HTTPClient,
_ terminal: InteractiveWriter
) throws -> Foundation.URL? {
let releaseURL = """
https://api.github.com/repos/swiftwasm/swift/releases/tags/\
swift-\(version)
"""
terminal.logLookup("Fetching release assets from ", releaseURL)
let decoder = JSONDecoder()
let request = try HTTPClient.Request.get(url: releaseURL)
let release = try await {
client.execute(request: request).flatMapResult { response -> Result<Release, Error> in
guard (200..<300).contains(response.status.code), let body = response.body else {
return .failure(ToolchainError.invalidResponse(
url: releaseURL,
status: response.status.code
))
}
terminal.write("Response contained body, parsing it now...\n", inColor: .green)
return Result { try decoder.decode(Release.self, from: body) }
}.whenComplete($0)
}
#if os(macOS)
let platformSuffixes = ["osx", "catalina", "macos"]
#elseif os(Linux)
let releaseFile = AbsolutePath("/etc").appending(component: "lsb-release")
guard fileSystem.isFile(releaseFile) else {
throw ToolchainError.unsupportedOperatingSystem
}
let releaseData = try fileSystem.readFileContents(releaseFile).description
let ubuntuSuffix: String
if releaseData.contains("DISTRIB_RELEASE=18.04") {
ubuntuSuffix = "ubuntu18.04"
} else if releaseData.contains("DISTRIB_RELEASE=20.04") {
ubuntuSuffix = "ubuntu20.04"
} else {
throw ToolchainError.unsupportedOperatingSystem
}
let platformSuffixes = ["linux", ubuntuSuffix]
#endif
terminal.logLookup(
"Response succesfully parsed, choosing from this number of assets: ",
release.assets.count
)
return release.assets.map(\.url).filter { url in
platformSuffixes.contains { url.absoluteString.contains($0) }
}.first
}
/** Infer `swift` binary path matching a given version if any is present, or infer the
version from the `.swift-version` file. If neither version is installed, download it.
*/
func inferSwiftPath(
from versionSpec: String? = nil,
_ terminal: InteractiveWriter
) throws -> (AbsolutePath, String) {
let specURL = versionSpec.flatMap { (string: String) -> Foundation.URL? in
guard
let url = Foundation.URL(string: string),
let scheme = url.scheme,
["http", "https"].contains(scheme)
else { return nil }
return url
}
let swiftVersion = try inferSwiftVersion(from: versionSpec, terminal)
for resolver in resolvers {
if let path = try checkAndLog(
installationPath: resolver.toolchain(for: swiftVersion),
terminal
) {
return (path, swiftVersion)
}
}
let client = HTTPClient(eventLoopGroupProvider: .createNew)
// swiftlint:disable:next force_try
defer { try! client.syncShutdown() }
let downloadURL: Foundation.URL
if let specURL = specURL {
downloadURL = specURL
} else if let inferredURL = try inferDownloadURL(from: swiftVersion, client, terminal) {
downloadURL = inferredURL
} else {
terminal.write("The Swift version \(swiftVersion) was not found\n", inColor: .red)
throw ToolchainError.invalidVersion(version: swiftVersion)
}
terminal.write(
"Local installation of Swift version \(swiftVersion) not found\n",
inColor: .yellow
)
terminal.logLookup("Swift toolchain/SDK download URL: ", downloadURL)
let installationPath = try installSDK(
version: swiftVersion,
from: downloadURL,
to: cartonToolchainResolver.cartonSDKPath,
client,
terminal
)
guard let path = try checkAndLog(installationPath: installationPath, terminal) else {
throw ToolchainError.invalidInstallationArchive(installationPath)
}
return (path, swiftVersion)
}
public func fetchAllSwiftVersions() throws -> [String] {
try resolvers.flatMap { try $0.fetchVersions() }
.filter { fileSystem.isDirectory($0.path) }
.map(\.version)
.sorted()
}
public func fetchLocalSwiftVersion() throws -> String? {
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 fileSystem.writeFileContents(swiftVersionPath, bytes: ByteString([UInt8](version.utf8)))
}
}