mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
103 lines
4.9 KiB
Swift
103 lines
4.9 KiB
Swift
import Foundation
|
|
import Path
|
|
import Version
|
|
import PromiseKit
|
|
import SwiftSoup
|
|
|
|
/// Provides lists of available and installed Xcodes
|
|
public final class XcodeList {
|
|
public init() {
|
|
try? loadCachedAvailableXcodes()
|
|
}
|
|
|
|
public private(set) var availableXcodes: [Xcode] = []
|
|
|
|
public var shouldUpdate: Bool {
|
|
return availableXcodes.isEmpty
|
|
}
|
|
|
|
public func update() -> Promise<[Xcode]> {
|
|
return when(fulfilled: releasedXcodes(), prereleaseXcodes())
|
|
.map { releasedXcodes, prereleaseXcodes in
|
|
// Starting with Xcode 11 beta 6, developer.apple.com/download and developer.apple.com/download/more both list some pre-release versions of Xcode.
|
|
// Previously pre-release versions only appeared on developer.apple.com/download.
|
|
// /download/more doesn't include build numbers, so we trust that if the version number and prerelease identifiers are the same that they're the same build.
|
|
// If an Xcode version is listed on both sites then prefer the one on /download because the build metadata is used to compare against installed Xcodes.
|
|
let xcodes = releasedXcodes.filter { releasedXcode in
|
|
prereleaseXcodes.contains { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: releasedXcode.version) } == false
|
|
} + prereleaseXcodes
|
|
self.availableXcodes = xcodes
|
|
try? self.cacheAvailableXcodes(xcodes)
|
|
return xcodes
|
|
}
|
|
}
|
|
}
|
|
|
|
extension XcodeList {
|
|
private func loadCachedAvailableXcodes() throws {
|
|
guard let data = Current.files.contents(atPath: Path.cacheFile.string) else { return }
|
|
let xcodes = try JSONDecoder().decode([Xcode].self, from: data)
|
|
self.availableXcodes = xcodes
|
|
}
|
|
|
|
private func cacheAvailableXcodes(_ xcodes: [Xcode]) throws {
|
|
let data = try JSONEncoder().encode(xcodes)
|
|
try FileManager.default.createDirectory(at: Path.cacheFile.url.deletingLastPathComponent(),
|
|
withIntermediateDirectories: true)
|
|
try data.write(to: Path.cacheFile.url)
|
|
}
|
|
}
|
|
|
|
extension XcodeList {
|
|
private func releasedXcodes() -> Promise<[Xcode]> {
|
|
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
|
|
Current.network.dataTask(with: URLRequest.downloads)
|
|
}
|
|
.map { (data, response) -> [Xcode] in
|
|
let decoder = JSONDecoder()
|
|
decoder.dateDecodingStrategy = .formatted(.downloadsDateModified)
|
|
let downloads = try decoder.decode(Downloads.self, from: data)
|
|
let xcodes = downloads
|
|
.downloads
|
|
.filter { $0.name.range(of: "^Xcode [0-9]", options: .regularExpression) != nil }
|
|
.compactMap { download -> Xcode? in
|
|
let urlPrefix = URL(string: "https://download.developer.apple.com/")!
|
|
guard
|
|
let xcodeFile = download.files.first(where: { $0.remotePath.hasSuffix("dmg") || $0.remotePath.hasSuffix("xip") }),
|
|
let version = Version(xcodeVersion: download.name)
|
|
else { return nil }
|
|
|
|
let url = urlPrefix.appendingPathComponent(xcodeFile.remotePath)
|
|
return Xcode(version: version, url: url, filename: String(xcodeFile.remotePath.suffix(fromLast: "/")), releaseDate: download.dateModified)
|
|
}
|
|
return xcodes
|
|
}
|
|
}
|
|
|
|
private func prereleaseXcodes() -> Promise<[Xcode]> {
|
|
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
|
|
Current.network.dataTask(with: URLRequest.download)
|
|
}
|
|
.map { (data, _) -> [Xcode] in
|
|
try self.parsePrereleaseXcodes(from: data)
|
|
}
|
|
}
|
|
|
|
func parsePrereleaseXcodes(from data: Data) throws -> [Xcode] {
|
|
let body = String(data: data, encoding: .utf8)!
|
|
let document = try SwiftSoup.parse(body)
|
|
|
|
guard
|
|
let xcodeHeader = try document.select("h2:containsOwn(Xcode)").first(),
|
|
let productBuildVersion = try xcodeHeader.parent()?.select("li:contains(Build)").text().replacingOccurrences(of: "Build", with: ""),
|
|
let releaseDateString = try xcodeHeader.parent()?.select("li:contains(Released)").text().replacingOccurrences(of: "Released", with: ""),
|
|
let version = Version(xcodeVersion: try xcodeHeader.text(), buildMetadataIdentifier: productBuildVersion),
|
|
let path = try document.select(".direct-download[href*=xip]").first()?.attr("href"),
|
|
let url = URL(string: "https://developer.apple.com" + path)
|
|
else { return [] }
|
|
|
|
let filename = String(path.suffix(fromLast: "/"))
|
|
|
|
return [Xcode(version: version, url: url, filename: filename, releaseDate: DateFormatter.downloadsReleaseDate.date(from: releaseDateString))]
|
|
}
|
|
}
|