Skip to content

Commit c27b288

Browse files
authored
Merge pull request #290 from RobotsAndPencils/NoLoginDownload
Add ability to download Xcode without logging in using XcodeReleases
2 parents e987104 + 8b43903 commit c27b288

5 files changed

Lines changed: 64 additions & 15 deletions

File tree

Xcodes/Backend/AppState+Install.swift

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,10 @@ extension AppState {
4040
}
4141

4242
private func install(_ installationType: InstallationType, downloader: Downloader, attemptNumber: Int) -> AnyPublisher<InstalledXcode, Error> {
43-
// We need to check if the Apple ID that is logged in is an Apple Developer
44-
// Since users can use xcodereleases, we don't check for Apple ID on a xcode list refresh
45-
// 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.`
43+
4644
Logger.appState.info("Using \(downloader) downloader")
4745

48-
return validateSession()
49-
.flatMap { _ in
50-
self.getXcodeArchive(installationType, downloader: downloader)
51-
}
46+
return self.getXcodeArchive(installationType, downloader: downloader)
5247
.flatMap { xcode, url -> AnyPublisher<InstalledXcode, Swift.Error> in
5348
self.installArchivedXcode(xcode, at: url)
5449
}
@@ -98,13 +93,16 @@ extension AppState {
9893
}
9994

10095
private func downloadXcode(availableXcode: AvailableXcode, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> {
101-
downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in
102-
DispatchQueue.main.async {
103-
self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress))
96+
return validateADCSession(path: availableXcode.downloadPath)
97+
.flatMap { _ in
98+
return self.downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in
99+
DispatchQueue.main.async {
100+
self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress))
101+
}
102+
})
103+
.map { return (availableXcode, $0) }
104104
}
105-
})
106-
.map { return (availableXcode, $0) }
107-
.eraseToAnyPublisher()
105+
.eraseToAnyPublisher()
108106
}
109107

110108
public func downloadOrUseExistingArchive(for availableXcode: AvailableXcode, downloader: Downloader, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher<URL, Error> {

Xcodes/Backend/AppState.swift

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,16 @@ class AppState: ObservableObject {
157157
}
158158
// MARK: - Authentication
159159

160+
func validateADCSession(path: String) -> AnyPublisher<Void, Error> {
161+
return Current.network.dataTask(with: URLRequest.downloadADCAuth(path: path))
162+
.receive(on: DispatchQueue.main)
163+
.tryMap { _ in
164+
}
165+
.eraseToAnyPublisher()
166+
}
167+
160168
func validateSession() -> AnyPublisher<Void, Error> {
169+
161170
return Current.network.validateSession()
162171
.receive(on: DispatchQueue.main)
163172
.handleEvents(receiveCompletion: { completion in
@@ -368,7 +377,12 @@ class AppState: ObservableObject {
368377
}
369378
}
370379

371-
install(id: id)
380+
switch self.dataSource {
381+
case .apple:
382+
install(id: id)
383+
case .xcodeReleases:
384+
installWithoutLogin(id: id)
385+
}
372386
}
373387

374388
func install(id: Xcode.ID) {
@@ -439,6 +453,30 @@ class AppState: ObservableObject {
439453
)
440454
}
441455

456+
/// Skips using the username/password to log in to Apple, and simply gets a Auth Cookie used in downloading
457+
func installWithoutLogin(id: Xcode.ID) {
458+
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
459+
460+
installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2)
461+
.receive(on: DispatchQueue.main)
462+
.sink(
463+
receiveCompletion: { [unowned self] completion in
464+
self.installationPublishers[id] = nil
465+
if case let .failure(error) = completion {
466+
// Prevent setting the app state error if it is an invalid session, we will present the sign in view instead
467+
if error as? AuthenticationError != .invalidSession {
468+
self.error = error
469+
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
470+
}
471+
if let index = self.allXcodes.firstIndex(where: { $0.id == id }) {
472+
self.allXcodes[index].installState = .notInstalled
473+
}
474+
}
475+
},
476+
receiveValue: { _ in }
477+
)
478+
}
479+
442480
func cancelInstall(id: Xcode.ID) {
443481
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
444482

Xcodes/Backend/AvailableXcode.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ public struct AvailableXcode: Codable {
1414
public let sdks: SDKs?
1515
public let compilers: Compilers?
1616
public let fileSize: Int64?
17+
public var downloadPath: String {
18+
return url.path
19+
}
1720

1821
public init(
1922
version: Version,

Xcodes/Backend/Environment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public struct Shell {
4343
"--max-connection-per-server=16",
4444
"--split=16",
4545
"--summary-interval=1",
46-
"--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)",
46+
"--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", // if xcodes quits, stop aria2 process
4747
"--dir=\(destination.parent.string)",
4848
"--out=\(destination.basename())",
4949
"--human-readable=false", // sets the output to use bytes instead of formatting

Xcodes/Backend/URLRequest+Apple.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ extension URL {
44
static let download = URL(string: "https://developer.apple.com/download")!
55
static let downloads = URL(string: "https://developer.apple.com/services-account/QH65B2/downloadws/listDownloads.action")!
66
static let downloadXcode = URL(string: "https://developer.apple.com/devcenter/download.action")!
7+
static let downloadADCAuth = URL(string: "https://developerservices2.apple.com/services/download")!
78
}
89

910
extension URLRequest {
@@ -25,4 +26,13 @@ extension URLRequest {
2526
request.allHTTPHeaderFields?["Accept"] = "*/*"
2627
return request
2728
}
29+
30+
static func downloadADCAuth(path: String) -> URLRequest {
31+
var components = URLComponents(url: .downloadADCAuth, resolvingAgainstBaseURL: false)!
32+
components.queryItems = [URLQueryItem(name: "path", value: path)]
33+
var request = URLRequest(url: components.url!)
34+
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
35+
request.allHTTPHeaderFields?["Accept"] = "*/*"
36+
return request
37+
}
2838
}

0 commit comments

Comments
 (0)