Merge pull request #290 from RobotsAndPencils/NoLoginDownload

Add ability to download Xcode without logging in using XcodeReleases
This commit is contained in:
Matt Kiazyk 2022-09-16 22:34:08 -05:00 committed by GitHub
commit c27b28868b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 64 additions and 15 deletions

View file

@ -40,15 +40,10 @@ extension AppState {
}
private func install(_ installationType: InstallationType, downloader: Downloader, attemptNumber: Int) -> AnyPublisher<InstalledXcode, Error> {
// We need to check if the Apple ID that is logged in is an Apple Developer
// Since users can use xcodereleases, we don't check for Apple ID on a xcode list refresh
// If the Apple Id is not a developer, the download action will try and download a xip that is invalid, causing a `xcode13.0.xip is damaged and can't be expanded.`
Logger.appState.info("Using \(downloader) downloader")
return validateSession()
.flatMap { _ in
self.getXcodeArchive(installationType, downloader: downloader)
}
return self.getXcodeArchive(installationType, downloader: downloader)
.flatMap { xcode, url -> AnyPublisher<InstalledXcode, Swift.Error> in
self.installArchivedXcode(xcode, at: url)
}
@ -98,13 +93,16 @@ extension AppState {
}
private func downloadXcode(availableXcode: AvailableXcode, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> {
downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in
DispatchQueue.main.async {
self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress))
return validateADCSession(path: availableXcode.downloadPath)
.flatMap { _ in
return self.downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in
DispatchQueue.main.async {
self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress))
}
})
.map { return (availableXcode, $0) }
}
})
.map { return (availableXcode, $0) }
.eraseToAnyPublisher()
.eraseToAnyPublisher()
}
public func downloadOrUseExistingArchive(for availableXcode: AvailableXcode, downloader: Downloader, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher<URL, Error> {

View file

@ -157,7 +157,16 @@ class AppState: ObservableObject {
}
// MARK: - Authentication
func validateADCSession(path: String) -> AnyPublisher<Void, Error> {
return Current.network.dataTask(with: URLRequest.downloadADCAuth(path: path))
.receive(on: DispatchQueue.main)
.tryMap { _ in
}
.eraseToAnyPublisher()
}
func validateSession() -> AnyPublisher<Void, Error> {
return Current.network.validateSession()
.receive(on: DispatchQueue.main)
.handleEvents(receiveCompletion: { completion in
@ -368,7 +377,12 @@ class AppState: ObservableObject {
}
}
install(id: id)
switch self.dataSource {
case .apple:
install(id: id)
case .xcodeReleases:
installWithoutLogin(id: id)
}
}
func install(id: Xcode.ID) {
@ -439,6 +453,30 @@ class AppState: ObservableObject {
)
}
/// Skips using the username/password to log in to Apple, and simply gets a Auth Cookie used in downloading
func installWithoutLogin(id: Xcode.ID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [unowned self] completion in
self.installationPublishers[id] = nil
if case let .failure(error) = completion {
// Prevent setting the app state error if it is an invalid session, we will present the sign in view instead
if error as? AuthenticationError != .invalidSession {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
}
if let index = self.allXcodes.firstIndex(where: { $0.id == id }) {
self.allXcodes[index].installState = .notInstalled
}
}
},
receiveValue: { _ in }
)
}
func cancelInstall(id: Xcode.ID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }

View file

@ -14,6 +14,9 @@ public struct AvailableXcode: Codable {
public let sdks: SDKs?
public let compilers: Compilers?
public let fileSize: Int64?
public var downloadPath: String {
return url.path
}
public init(
version: Version,

View file

@ -43,7 +43,7 @@ public struct Shell {
"--max-connection-per-server=16",
"--split=16",
"--summary-interval=1",
"--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)",
"--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", // if xcodes quits, stop aria2 process
"--dir=\(destination.parent.string)",
"--out=\(destination.basename())",
"--human-readable=false", // sets the output to use bytes instead of formatting

View file

@ -4,6 +4,7 @@ extension URL {
static let download = URL(string: "https://developer.apple.com/download")!
static let downloads = URL(string: "https://developer.apple.com/services-account/QH65B2/downloadws/listDownloads.action")!
static let downloadXcode = URL(string: "https://developer.apple.com/devcenter/download.action")!
static let downloadADCAuth = URL(string: "https://developerservices2.apple.com/services/download")!
}
extension URLRequest {
@ -25,4 +26,13 @@ extension URLRequest {
request.allHTTPHeaderFields?["Accept"] = "*/*"
return request
}
static func downloadADCAuth(path: String) -> URLRequest {
var components = URLComponents(url: .downloadADCAuth, resolvingAgainstBaseURL: false)!
components.queryItems = [URLQueryItem(name: "path", value: path)]
var request = URLRequest(url: components.url!)
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
request.allHTTPHeaderFields?["Accept"] = "*/*"
return request
}
}