diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index f3a3d58..139f1a7 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -14,11 +14,31 @@ CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA735108257BF96D00EA9CF8 /* AttributedText.swift */; }; CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */; }; CAA1CB2D255A5262003FD669 /* AppleAPI in Frameworks */ = {isa = PBXBuildFile; productRef = CAA1CB2C255A5262003FD669 /* AppleAPI */; }; - CAA1CB2F255A5262003FD669 /* XcodesKit in Frameworks */ = {isa = PBXBuildFile; productRef = CAA1CB2E255A5262003FD669 /* XcodesKit */; }; CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */; }; CAA1CB45255A5B60003FD669 /* SignIn2FAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */; }; CAA1CB49255A5C97003FD669 /* SignInSMSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */; }; CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB4C255A5CFD003FD669 /* SignInPhoneListView.swift */; }; + CABFA9BB2592EEEA00380FEE /* DateFormatter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9BA2592EEEA00380FEE /* DateFormatter+.swift */; }; + CABFA9BD2592EEEA00380FEE /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9A92592EEE900380FEE /* Environment.swift */; }; + CABFA9BF2592EEEA00380FEE /* URLSession+Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9B32592EEEA00380FEE /* URLSession+Promise.swift */; }; + CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9A82592EEE900380FEE /* Version+.swift */; }; + CABFA9C22592EEEA00380FEE /* Promise+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9B02592EEEA00380FEE /* Promise+.swift */; }; + CABFA9C32592EEEA00380FEE /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9B92592EEEA00380FEE /* Models.swift */; }; + CABFA9C52592EEEA00380FEE /* FileManager+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9B82592EEEA00380FEE /* FileManager+.swift */; }; + CABFA9C72592EEEA00380FEE /* Entry+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9B22592EEEA00380FEE /* Entry+.swift */; }; + CABFA9C92592EEEA00380FEE /* URLRequest+Apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9AB2592EEE900380FEE /* URLRequest+Apple.swift */; }; + CABFA9CA2592EEEA00380FEE /* XcodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9A72592EEE900380FEE /* XcodeList.swift */; }; + CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9AE2592EEE900380FEE /* Path+.swift */; }; + CABFA9CD2592EEEA00380FEE /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9AC2592EEE900380FEE /* Foundation.swift */; }; + CABFA9CE2592EEEA00380FEE /* Version+Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9A62592EEE900380FEE /* Version+Xcode.swift */; }; + CABFA9CF2592EEEA00380FEE /* Process.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFA9B42592EEEA00380FEE /* Process.swift */; }; + CABFA9DF2592F07A00380FEE /* Path in Frameworks */ = {isa = PBXBuildFile; productRef = CABFA9DE2592F07A00380FEE /* Path */; }; + CABFA9E42592F08E00380FEE /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = CABFA9E32592F08E00380FEE /* Version */; }; + CABFA9E92592F0B400380FEE /* PromiseKit in Frameworks */ = {isa = PBXBuildFile; productRef = CABFA9E82592F0B400380FEE /* PromiseKit */; }; + CABFA9EE2592F0CC00380FEE /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = CABFA9ED2592F0CC00380FEE /* SwiftSoup */; }; + CABFA9F32592F0E400380FEE /* PMKFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CABFA9F22592F0E400380FEE /* PMKFoundation */; }; + CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = CABFA9F72592F0F900380FEE /* KeychainAccess */; }; + CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */ = {isa = PBXBuildFile; productRef = CABFA9FC2592F13300380FEE /* LegibleError */; }; CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD2E7A12449574E00113D76 /* XcodesApp.swift */; }; CAD2E7A42449574E00113D76 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD2E7A32449574E00113D76 /* ContentView.swift */; }; CAD2E7A62449575000113D76 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAD2E7A52449575000113D76 /* Assets.xcassets */; }; @@ -42,7 +62,6 @@ CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreButtonStyle.swift; sourceTree = ""; }; CA44901E2463AD34003D8213 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; CA538A0C255A4F1A00E64DD7 /* AppleAPI */ = {isa = PBXFileReference; lastKnownFileType = folder; name = AppleAPI; path = Xcodes/AppleAPI; sourceTree = ""; }; - CA538A0F255A4F3300E64DD7 /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = ""; }; CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCodeTextView.swift; sourceTree = ""; }; CA735108257BF96D00EA9CF8 /* AttributedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedText.swift; sourceTree = ""; }; CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+.swift"; sourceTree = ""; }; @@ -56,6 +75,20 @@ CABFA9A02592EAF500380FEE /* R&PLogo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "R&PLogo.png"; sourceTree = ""; }; CABFA9A12592EAFB00380FEE /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; CABFA9A32592ED5700380FEE /* Apple.paw */ = {isa = PBXFileReference; lastKnownFileType = file; name = Apple.paw; path = ../xcodes/Apple.paw; sourceTree = ""; }; + CABFA9A62592EEE900380FEE /* Version+Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Version+Xcode.swift"; sourceTree = ""; }; + CABFA9A72592EEE900380FEE /* XcodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeList.swift; sourceTree = ""; }; + CABFA9A82592EEE900380FEE /* Version+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Version+.swift"; sourceTree = ""; }; + CABFA9A92592EEE900380FEE /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; + CABFA9AB2592EEE900380FEE /* URLRequest+Apple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Apple.swift"; sourceTree = ""; }; + CABFA9AC2592EEE900380FEE /* Foundation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Foundation.swift; sourceTree = ""; }; + CABFA9AE2592EEE900380FEE /* Path+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Path+.swift"; sourceTree = ""; }; + CABFA9B02592EEEA00380FEE /* Promise+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+.swift"; sourceTree = ""; }; + CABFA9B22592EEEA00380FEE /* Entry+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Entry+.swift"; sourceTree = ""; }; + CABFA9B32592EEEA00380FEE /* URLSession+Promise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Promise.swift"; sourceTree = ""; }; + CABFA9B42592EEEA00380FEE /* Process.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Process.swift; sourceTree = ""; }; + CABFA9B82592EEEA00380FEE /* FileManager+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+.swift"; sourceTree = ""; }; + CABFA9B92592EEEA00380FEE /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + CABFA9BA2592EEEA00380FEE /* DateFormatter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+.swift"; sourceTree = ""; }; CABFA9D42592EF6300380FEE /* DECISIONS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DECISIONS.md; sourceTree = ""; }; CAD2E79E2449574E00113D76 /* Xcodes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Xcodes.app; sourceTree = BUILT_PRODUCTS_DIR; }; CAD2E7A12449574E00113D76 /* XcodesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodesApp.swift; sourceTree = ""; }; @@ -75,8 +108,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CABFA9E42592F08E00380FEE /* Version in Frameworks */, + CABFA9E92592F0B400380FEE /* PromiseKit in Frameworks */, + CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */, + CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */, + CABFA9F32592F0E400380FEE /* PMKFoundation in Frameworks */, CAA1CB2D255A5262003FD669 /* AppleAPI in Frameworks */, - CAA1CB2F255A5262003FD669 /* XcodesKit in Frameworks */, + CABFA9DF2592F07A00380FEE /* Path in Frameworks */, + CABFA9EE2592F0CC00380FEE /* SwiftSoup in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -118,7 +157,6 @@ CABFA9A32592ED5700380FEE /* Apple.paw */, CABFA9A12592EAFB00380FEE /* LICENSE */, CA8FB61C256E115700469DA5 /* .github */, - CA538A0F255A4F3300E64DD7 /* XcodesKit */, CA538A0C255A4F1A00E64DD7 /* AppleAPI */, CAD2E7A02449574E00113D76 /* Xcodes */, CAD2E7B62449575100113D76 /* XcodesTests */, @@ -139,19 +177,33 @@ CAD2E7A02449574E00113D76 /* Xcodes */ = { isa = PBXGroup; children = ( - CA8FB64D256E17B100469DA5 /* XcodesTest.entitlements */, CAD2E7A12449574E00113D76 /* XcodesApp.swift */, + CA378F982466567600A58CE0 /* AppState.swift */, CAD2E7A32449574E00113D76 /* ContentView.swift */, CAA1CB50255A5D16003FD669 /* SignIn */, - CA378F982466567600A58CE0 /* AppState.swift */, CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */, CA735108257BF96D00EA9CF8 /* AttributedText.swift */, CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */, + CABFA9BA2592EEEA00380FEE /* DateFormatter+.swift */, + CABFA9B22592EEEA00380FEE /* Entry+.swift */, + CABFA9A92592EEE900380FEE /* Environment.swift */, + CABFA9B82592EEEA00380FEE /* FileManager+.swift */, + CABFA9AC2592EEE900380FEE /* Foundation.swift */, + CABFA9B92592EEEA00380FEE /* Models.swift */, + CABFA9AE2592EEE900380FEE /* Path+.swift */, + CABFA9B42592EEEA00380FEE /* Process.swift */, + CABFA9B02592EEEA00380FEE /* Promise+.swift */, + CABFA9AB2592EEE900380FEE /* URLRequest+Apple.swift */, + CABFA9B32592EEEA00380FEE /* URLSession+Promise.swift */, + CABFA9A82592EEE900380FEE /* Version+.swift */, + CABFA9A62592EEE900380FEE /* Version+Xcode.swift */, + CABFA9A72592EEE900380FEE /* XcodeList.swift */, CA44901E2463AD34003D8213 /* Tag.swift */, CAD2E7A52449575000113D76 /* Assets.xcassets */, CAD2E7AA2449575000113D76 /* Main.storyboard */, CAD2E7AD2449575000113D76 /* Info.plist */, CAD2E7AE2449575000113D76 /* Xcodes.entitlements */, + CA8FB64D256E17B100469DA5 /* XcodesTest.entitlements */, CAD2E7A72449575000113D76 /* Preview Content */, ); path = Xcodes; @@ -192,7 +244,13 @@ name = Xcodes; packageProductDependencies = ( CAA1CB2C255A5262003FD669 /* AppleAPI */, - CAA1CB2E255A5262003FD669 /* XcodesKit */, + CABFA9DE2592F07A00380FEE /* Path */, + CABFA9E32592F08E00380FEE /* Version */, + CABFA9E82592F0B400380FEE /* PromiseKit */, + CABFA9ED2592F0CC00380FEE /* SwiftSoup */, + CABFA9F22592F0E400380FEE /* PMKFoundation */, + CABFA9F72592F0F900380FEE /* KeychainAccess */, + CABFA9FC2592F13300380FEE /* LegibleError */, ); productName = XcodesMac; productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */; @@ -245,6 +303,13 @@ ); mainGroup = CAD2E7952449574E00113D76; packageReferences = ( + CABFA9DD2592F07A00380FEE /* XCRemoteSwiftPackageReference "Path" */, + CABFA9E22592F08E00380FEE /* XCRemoteSwiftPackageReference "Version" */, + CABFA9E72592F0B400380FEE /* XCRemoteSwiftPackageReference "PromiseKit" */, + CABFA9EC2592F0CC00380FEE /* XCRemoteSwiftPackageReference "SwiftSoup" */, + CABFA9F12592F0E400380FEE /* XCRemoteSwiftPackageReference "Foundation" */, + CABFA9F62592F0F900380FEE /* XCRemoteSwiftPackageReference "KeychainAccess" */, + CABFA9FB2592F13300380FEE /* XCRemoteSwiftPackageReference "LegibleError" */, ); productRefGroup = CAD2E79F2449574E00113D76 /* Products */; projectDirPath = ""; @@ -282,14 +347,28 @@ buildActionMask = 2147483647; files = ( CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */, + CABFA9CA2592EEEA00380FEE /* XcodeList.swift in Sources */, CA44901F2463AD34003D8213 /* Tag.swift in Sources */, + CABFA9BF2592EEEA00380FEE /* URLSession+Promise.swift in Sources */, + CABFA9BB2592EEEA00380FEE /* DateFormatter+.swift in Sources */, + CABFA9BD2592EEEA00380FEE /* Environment.swift in Sources */, + CABFA9C32592EEEA00380FEE /* Models.swift in Sources */, CA378F992466567600A58CE0 /* AppState.swift in Sources */, CAD2E7A42449574E00113D76 /* ContentView.swift in Sources */, CAA1CB45255A5B60003FD669 /* SignIn2FAView.swift in Sources */, + CABFA9C52592EEEA00380FEE /* FileManager+.swift in Sources */, + CABFA9CD2592EEEA00380FEE /* Foundation.swift in Sources */, + CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */, + CABFA9CE2592EEEA00380FEE /* Version+Xcode.swift in Sources */, CAA1CB49255A5C97003FD669 /* SignInSMSView.swift in Sources */, CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */, CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */, + CABFA9C22592EEEA00380FEE /* Promise+.swift in Sources */, CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */, + CABFA9CF2592EEEA00380FEE /* Process.swift in Sources */, + CABFA9C72592EEEA00380FEE /* Entry+.swift in Sources */, + CABFA9C92592EEEA00380FEE /* URLRequest+Apple.swift in Sources */, + CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */, CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */, CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */, CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */, @@ -667,14 +746,104 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + CABFA9DD2592F07A00380FEE /* XCRemoteSwiftPackageReference "Path" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mxcl/Path.swift"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 0.16.0; + }; + }; + CABFA9E22592F08E00380FEE /* XCRemoteSwiftPackageReference "Version" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mxcl/Version"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 1.0.3; + }; + }; + CABFA9E72592F0B400380FEE /* XCRemoteSwiftPackageReference "PromiseKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mxcl/PromiseKit"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 6.8.3; + }; + }; + CABFA9EC2592F0CC00380FEE /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/scinfu/SwiftSoup"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 2.0.0; + }; + }; + CABFA9F12592F0E400380FEE /* XCRemoteSwiftPackageReference "Foundation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/PromiseKit/Foundation"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 3.3.1; + }; + }; + CABFA9F62592F0F900380FEE /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 3.2.0; + }; + }; + CABFA9FB2592F13300380FEE /* XCRemoteSwiftPackageReference "LegibleError" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mxcl/LegibleError"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 1.0.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ CAA1CB2C255A5262003FD669 /* AppleAPI */ = { isa = XCSwiftPackageProductDependency; productName = AppleAPI; }; - CAA1CB2E255A5262003FD669 /* XcodesKit */ = { + CABFA9DE2592F07A00380FEE /* Path */ = { isa = XCSwiftPackageProductDependency; - productName = XcodesKit; + package = CABFA9DD2592F07A00380FEE /* XCRemoteSwiftPackageReference "Path" */; + productName = Path; + }; + CABFA9E32592F08E00380FEE /* Version */ = { + isa = XCSwiftPackageProductDependency; + package = CABFA9E22592F08E00380FEE /* XCRemoteSwiftPackageReference "Version" */; + productName = Version; + }; + CABFA9E82592F0B400380FEE /* PromiseKit */ = { + isa = XCSwiftPackageProductDependency; + package = CABFA9E72592F0B400380FEE /* XCRemoteSwiftPackageReference "PromiseKit" */; + productName = PromiseKit; + }; + CABFA9ED2592F0CC00380FEE /* SwiftSoup */ = { + isa = XCSwiftPackageProductDependency; + package = CABFA9EC2592F0CC00380FEE /* XCRemoteSwiftPackageReference "SwiftSoup" */; + productName = SwiftSoup; + }; + CABFA9F22592F0E400380FEE /* PMKFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CABFA9F12592F0E400380FEE /* XCRemoteSwiftPackageReference "Foundation" */; + productName = PMKFoundation; + }; + CABFA9F72592F0F900380FEE /* KeychainAccess */ = { + isa = XCSwiftPackageProductDependency; + package = CABFA9F62592F0F900380FEE /* XCRemoteSwiftPackageReference "KeychainAccess" */; + productName = KeychainAccess; + }; + CABFA9FC2592F13300380FEE /* LegibleError */ = { + isa = XCSwiftPackageProductDependency; + package = CABFA9FB2592F13300380FEE /* XCRemoteSwiftPackageReference "LegibleError" */; + productName = LegibleError; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 14fd872..0ee46f5 100644 --- a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -3,7 +3,7 @@ "pins": [ { "package": "PMKFoundation", - "repositoryURL": "https://github.com/PromiseKit/Foundation.git", + "repositoryURL": "https://github.com/PromiseKit/Foundation", "state": { "branch": null, "revision": "1a276e598dac59489ed904887e0740fa75e571e0", @@ -12,7 +12,7 @@ }, { "package": "KeychainAccess", - "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", + "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess", "state": { "branch": null, "revision": "8d33ffd6f74b3bcfc99af759d4204c6395a3f918", @@ -21,7 +21,7 @@ }, { "package": "LegibleError", - "repositoryURL": "https://github.com/mxcl/LegibleError.git", + "repositoryURL": "https://github.com/mxcl/LegibleError", "state": { "branch": null, "revision": "909e9bab3ded97350b28a5ab41dd745dd8aa9710", @@ -30,7 +30,7 @@ }, { "package": "Path.swift", - "repositoryURL": "https://github.com/mxcl/Path.swift.git", + "repositoryURL": "https://github.com/mxcl/Path.swift", "state": { "branch": null, "revision": "dac007e907a4f4c565cfdc55a9ce148a761a11d5", @@ -39,29 +39,29 @@ }, { "package": "PromiseKit", - "repositoryURL": "https://github.com/mxcl/PromiseKit.git", + "repositoryURL": "https://github.com/mxcl/PromiseKit", "state": { "branch": null, - "revision": "aea48ea1855f5d82e2dffa6027afce3aab8f3dd7", - "version": "6.13.3" + "revision": "1c296a8637838901d2b01e4c46875ee749506133", + "version": "6.8.5" } }, { "package": "SwiftSoup", - "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", + "repositoryURL": "https://github.com/scinfu/SwiftSoup", "state": { "branch": null, - "revision": "774dc9c7213085db8aa59595e27c1cd22e428904", - "version": "2.3.2" + "revision": "aeb5b4249c273d1783a5299e05be1b26e061ea81", + "version": "2.0.0" } }, { "package": "Version", - "repositoryURL": "https://github.com/mxcl/Version.git", + "repositoryURL": "https://github.com/mxcl/Version", "state": { "branch": null, - "revision": "a94b48f36763c05629fc102837398505032dead9", - "version": "2.0.0" + "revision": "087c91fedc110f9f833b14ef4c32745dabca8913", + "version": "1.0.3" } } ] diff --git a/Xcodes/AppState.swift b/Xcodes/AppState.swift index 2ba4907..ad13945 100644 --- a/Xcodes/AppState.swift +++ b/Xcodes/AppState.swift @@ -3,50 +3,20 @@ import AppleAPI import Combine import Path import PromiseKit -import XcodesKit +import LegibleError class AppState: ObservableObject { private let list = XcodeList() - private lazy var installer = XcodeInstaller(configuration: Configuration(), xcodeList: list) - - struct XcodeVersion: Identifiable { - let title: String - let installState: InstallState - let selected: Bool - let path: String? - var id: String { title } - var installed: Bool { installState == .installed } - } - enum InstallState: Equatable { - case notInstalled - case installing(Progress) - case installed - } + private let client = AppleAPI.Client() + private var cancellables = Set() @Published var authenticationState: AuthenticationState = .unauthenticated @Published var allVersions: [XcodeVersion] = [] - - struct AlertContent: Identifiable { - var title: String - var message: String - var id: String { title + message } - } @Published var error: AlertContent? - @Published var presentingSignInAlert = false @Published var secondFactorData: SecondFactorData? - - struct SecondFactorData { - let option: TwoFactorOption - let authOptions: AuthOptionsResponse - let sessionData: AppleSessionData - } - - private var cancellables = Set() - let client = AppleAPI.Client() func load() { -// if list.shouldUpdate { // Treat this implementation as a placeholder that can be thrown away. // It's only here to make it easy to see that auth works. update() @@ -242,15 +212,7 @@ class AppState: ObservableObject { } func uninstall(id: String) { - guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version.xcodeDescription == id }) else { return } - // TODO: would be nice to have a version of this method that just took the InstalledXcode - installer.uninstallXcode(installedXcode.version.xcodeDescription, destination: Path.root/"Applications") - .done { - - } - .catch { error in - - } + // TODO: } func reveal(id: String) { @@ -262,4 +224,33 @@ class AppState: ObservableObject { func select(id: String) { // TODO: } + + // MARK: - Nested Types + + struct XcodeVersion: Identifiable { + let title: String + let installState: InstallState + let selected: Bool + let path: String? + var id: String { title } + var installed: Bool { installState == .installed } + } + + enum InstallState: Equatable { + case notInstalled + case installing(Progress) + case installed + } + + struct AlertContent: Identifiable { + var title: String + var message: String + var id: String { title + message } + } + + struct SecondFactorData { + let option: TwoFactorOption + let authOptions: AuthOptionsResponse + let sessionData: AppleSessionData + } } diff --git a/Xcodes/ContentView.swift b/Xcodes/ContentView.swift index 8164f16..cc1a2d1 100644 --- a/Xcodes/ContentView.swift +++ b/Xcodes/ContentView.swift @@ -1,5 +1,4 @@ import SwiftUI -import XcodesKit import Version import PromiseKit diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/DateFormatter+.swift b/Xcodes/DateFormatter+.swift similarity index 100% rename from Xcodes/XcodesKit/Sources/XcodesKit/DateFormatter+.swift rename to Xcodes/DateFormatter+.swift diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Entry+.swift b/Xcodes/Entry+.swift similarity index 100% rename from Xcodes/XcodesKit/Sources/XcodesKit/Entry+.swift rename to Xcodes/Entry+.swift diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Environment.swift b/Xcodes/Environment.swift similarity index 59% rename from Xcodes/XcodesKit/Sources/XcodesKit/Environment.swift rename to Xcodes/Environment.swift index 5db9b5e..e28b951 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Environment.swift +++ b/Xcodes/Environment.swift @@ -53,130 +53,6 @@ public struct Shell { public func xcodeSelectSwitch(password: String?, path: String) -> Promise { xcodeSelectSwitch(password, path) } - - public var downloadWithAria2: (Path, URL, Path, [HTTPCookie]) -> (Progress, Promise) = { aria2Path, url, destination, cookies in - let process = Process() - process.executableURL = aria2Path.url - process.arguments = [ - "--header=Cookie: \(cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; "))", - "--max-connection-per-server=16", - "--split=16", - "--summary-interval=1", - "--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", - "--dir=\(destination.parent.string)", - "--out=\(destination.basename())", - url.absoluteString, - ] - let stdOutPipe = Pipe() - process.standardOutput = stdOutPipe - let stdErrPipe = Pipe() - process.standardError = stdErrPipe - - var progress = Progress(totalUnitCount: 100) - - let observer = NotificationCenter.default.addObserver( - forName: .NSFileHandleDataAvailable, - object: nil, - queue: OperationQueue.main - ) { note in - guard - // This should always be the case for Notification.Name.NSFileHandleDataAvailable - let handle = note.object as? FileHandle, - handle === stdOutPipe.fileHandleForReading || handle === stdErrPipe.fileHandleForReading - else { return } - - defer { handle.waitForDataInBackgroundAndNotify() } - - let string = String(decoding: handle.availableData, as: UTF8.self) - let regex = try! NSRegularExpression(pattern: #"((?\d+)%\))"#) - let range = NSRange(location: 0, length: string.utf16.count) - - guard - let match = regex.firstMatch(in: string, options: [], range: range), - let matchRange = Range(match.range(withName: "percent"), in: string), - let percentCompleted = Int64(string[matchRange]) - else { return } - - progress.completedUnitCount = percentCompleted - } - - stdOutPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() - stdErrPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() - - do { - try process.run() - } catch { - return (progress, Promise(error: error)) - } - - let promise = Promise { seal in - DispatchQueue.global(qos: .default).async { - process.waitUntilExit() - - NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil) - - guard process.terminationReason == .exit, process.terminationStatus == 0 else { - if let aria2cError = Aria2CError(exitStatus: process.terminationStatus) { - return seal.reject(aria2cError) - } else { - return seal.reject(Process.PMKError.execution(process: process, standardOutput: "", standardError: "")) - } - } - seal.fulfill(()) - } - } - - return (progress, promise) - } - - public var readLine: (String) -> String? = { prompt in - print(prompt, terminator: "") - return Swift.readLine() - } - public func readLine(prompt: String) -> String? { - readLine(prompt) - } - - public var readSecureLine: (String, Int) -> String? = { prompt, maximumLength in - let buffer = UnsafeMutablePointer.allocate(capacity: maximumLength) - buffer.initialize(repeating: 0, count: maximumLength) - defer { - buffer.deinitialize(count: maximumLength) - buffer.initialize(repeating: 0, count: maximumLength) - buffer.deinitialize(count: maximumLength) - buffer.deallocate() - } - - guard let passwordData = readpassphrase(prompt, buffer, maximumLength, 0) else { - return nil - } - - return String(validatingUTF8: passwordData) - } - /** - Like `readLine()`, but doesn't echo the user's input to the screen. - - - Parameter prompt: Prompt printed on the line preceding user input - - Parameter maximumLength: The maximum length to read, in bytes - - - Returns: The entered password, or nil if an error occurred. - - Buffer is zeroed after use. - - - SeeAlso: [readpassphrase man page](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/readpassphrase.3.html) - */ - public func readSecureLine(prompt: String, maximumLength: Int = 8192) -> String? { - readSecureLine(prompt, maximumLength) - } - - public var env: (String) -> String? = { key in - ProcessInfo.processInfo.environment[key] - } - public func env(_ key: String) -> String? { - env(key) - } - - public var exit: (Int32) -> Void = { Darwin.exit($0) } } public struct Files { @@ -223,9 +99,9 @@ public struct Files { try createDirectory(url, createIntermediates, attributes) } - public var installedXcodes = XcodesKit.installedXcodes + public var installedXcodes = _installedXcodes } -private func installedXcodes(destination: Path) -> [InstalledXcode] { +private func _installedXcodes(destination: Path) -> [InstalledXcode] { ((try? destination.ls()) ?? []) .filter { $0.isAppBundle && $0.infoPlist?.bundleID == "com.apple.dt.Xcode" } .map { $0.path } diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/FileManager+.swift b/Xcodes/FileManager+.swift similarity index 100% rename from Xcodes/XcodesKit/Sources/XcodesKit/FileManager+.swift rename to Xcodes/FileManager+.swift diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Foundation.swift b/Xcodes/Foundation.swift similarity index 100% rename from Xcodes/XcodesKit/Sources/XcodesKit/Foundation.swift rename to Xcodes/Foundation.swift diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models.swift b/Xcodes/Models.swift similarity index 100% rename from Xcodes/XcodesKit/Sources/XcodesKit/Models.swift rename to Xcodes/Models.swift diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Path+.swift b/Xcodes/Path+.swift similarity index 53% rename from Xcodes/XcodesKit/Sources/XcodesKit/Path+.swift rename to Xcodes/Path+.swift index 44315a6..6601bf0 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Path+.swift +++ b/Xcodes/Path+.swift @@ -1,8 +1,6 @@ import Path extension Path { - static let oldXcodesApplicationSupport = Path.applicationSupport/"ca.brandonevans.xcodes" static let xcodesApplicationSupport = Path.applicationSupport/"com.robotsandpencils.xcodes" static let cacheFile = xcodesApplicationSupport/"available-xcodes.json" - static let configurationFile = xcodesApplicationSupport/"configuration.json" } diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Process.swift b/Xcodes/Process.swift similarity index 100% rename from Xcodes/XcodesKit/Sources/XcodesKit/Process.swift rename to Xcodes/Process.swift diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Promise+.swift b/Xcodes/Promise+.swift similarity index 100% rename from Xcodes/XcodesKit/Sources/XcodesKit/Promise+.swift rename to Xcodes/Promise+.swift diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/URLRequest+Apple.swift b/Xcodes/URLRequest+Apple.swift similarity index 100% rename from Xcodes/XcodesKit/Sources/XcodesKit/URLRequest+Apple.swift rename to Xcodes/URLRequest+Apple.swift diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/URLSession+Promise.swift b/Xcodes/URLSession+Promise.swift similarity index 100% rename from Xcodes/XcodesKit/Sources/XcodesKit/URLSession+Promise.swift rename to Xcodes/URLSession+Promise.swift diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Version+.swift b/Xcodes/Version+.swift similarity index 100% rename from Xcodes/XcodesKit/Sources/XcodesKit/Version+.swift rename to Xcodes/Version+.swift diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Version+Xcode.swift b/Xcodes/Version+Xcode.swift similarity index 100% rename from Xcodes/XcodesKit/Sources/XcodesKit/Version+Xcode.swift rename to Xcodes/Version+Xcode.swift diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/XcodeList.swift b/Xcodes/XcodeList.swift similarity index 100% rename from Xcodes/XcodesKit/Sources/XcodesKit/XcodeList.swift rename to Xcodes/XcodeList.swift diff --git a/Xcodes/XcodesKit/.gitignore b/Xcodes/XcodesKit/.gitignore deleted file mode 100644 index 95c4320..0000000 --- a/Xcodes/XcodesKit/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj -xcuserdata/ diff --git a/Xcodes/XcodesKit/Package.swift b/Xcodes/XcodesKit/Package.swift deleted file mode 100644 index 9e200a4..0000000 --- a/Xcodes/XcodesKit/Package.swift +++ /dev/null @@ -1,37 +0,0 @@ -// swift-tools-version:5.3 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "XcodesKit", - platforms: [.macOS(.v10_15)], - products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "XcodesKit", - targets: ["XcodesKit"]), - ], - dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), - .package(path: "../AppleAPI"), - .package(url: "https://github.com/mxcl/Path.swift.git", .upToNextMajor(from: "0.16.0")), - .package(url: "https://github.com/mxcl/Version.git", .upToNextMajor(from: "2.0.0")), - .package(url: "https://github.com/mxcl/PromiseKit.git", .upToNextMajor(from: "6.8.3")), - .package(name: "PMKFoundation", url: "https://github.com/PromiseKit/Foundation.git", .upToNextMajor(from: "3.3.1")), - .package(url: "https://github.com/scinfu/SwiftSoup.git", .upToNextMajor(from: "2.3.2")), - .package(url: "https://github.com/mxcl/LegibleError.git", .upToNextMajor(from: "1.0.1")), - .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMajor(from: "3.2.0")), - ], - targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( - name: "XcodesKit", - dependencies: ["AppleAPI", .product(name: "Path", package: "Path.swift"), "Version", "PromiseKit", "PMKFoundation", "SwiftSoup", "LegibleError", "KeychainAccess"]), - .testTarget( - name: "XcodesKitTests", - dependencies: ["XcodesKit"]), - ] -) diff --git a/Xcodes/XcodesKit/README.md b/Xcodes/XcodesKit/README.md deleted file mode 100644 index 5312c49..0000000 --- a/Xcodes/XcodesKit/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# XcodesKit - -A description of this package. diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Aria2CError.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Aria2CError.swift deleted file mode 100644 index c652626..0000000 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Aria2CError.swift +++ /dev/null @@ -1,125 +0,0 @@ -import Foundation - -/// A LocalizedError that represents a non-zero exit code from running aria2c. -struct Aria2CError: LocalizedError { - var code: Code - - init?(exitStatus: Int32) { - guard let code = Code(rawValue: exitStatus) else { return nil } - self.code = code - } - - var errorDescription: String? { - "aria2c error: \(code.description)" - } - - // https://github.com/aria2/aria2/blob/master/src/error_code.h - enum Code: Int32, CustomStringConvertible { - case undefined = -1 - // Ignoring, not an error - // case finished = 0 - case unknownError = 1 - case timeOut - case resourceNotFound - case maxFileNotFound - case tooSlowDownloadSpeed - case networkProblem - case inProgress - case cannotResume - case notEnoughDiskSpace - case pieceLengthChanged - case duplicateDownload - case duplicateInfoHash - case fileAlreadyExists - case fileRenamingFailed - case fileOpenError - case fileCreateError - case fileIoError - case dirCreateError - case nameResolveError - case metalinkParseError - case ftpProtocolError - case httpProtocolError - case httpTooManyRedirects - case httpAuthFailed - case bencodeParseError - case bittorrentParseError - case magnetParseError - case optionError - case httpServiceUnavailable - case jsonParseError - case removed - case checksumError - - var description: String { - switch self { - case .undefined: - return "Undefined" - case .unknownError: - return "Unknown error" - case .timeOut: - return "Timed out" - case .resourceNotFound: - return "Resource not found" - case .maxFileNotFound: - return "Maximum number of file not found errors reached" - case .tooSlowDownloadSpeed: - return "Download speed too slow" - case .networkProblem: - return "Network problem" - case .inProgress: - return "Unfinished downloads in progress" - case .cannotResume: - return "Remote server did not support resume when resume was required to complete download" - case .notEnoughDiskSpace: - return "Not enough disk space available" - case .pieceLengthChanged: - return "Piece length was different from one in .aria2 control file" - case .duplicateDownload: - return "Duplicate download" - case .duplicateInfoHash: - return "Duplicate info hash torrent" - case .fileAlreadyExists: - return "File already exists" - case .fileRenamingFailed: - return "Renaming file failed" - case .fileOpenError: - return "Could not open existing file" - case .fileCreateError: - return "Could not create new file or truncate existing file" - case .fileIoError: - return "File I/O error" - case .dirCreateError: - return "Could not create directory" - case .nameResolveError: - return "Name resolution failed" - case .metalinkParseError: - return "Could not parse Metalink document" - case .ftpProtocolError: - return "FTP command failed" - case .httpProtocolError: - return "HTTP response header was bad or unexpected" - case .httpTooManyRedirects: - return "Too many redirects occurred" - case .httpAuthFailed: - return "HTTP authorization failed" - case .bencodeParseError: - return "Could not parse bencoded file (usually \".torrent\" file)" - case .bittorrentParseError: - return "\".torrent\" file was corrupted or missing information" - case .magnetParseError: - return "Magnet URI was bad" - case .optionError: - return "Bad/unrecognized option was given or unexpected option argument was given" - case .httpServiceUnavailable: - return "HTTP service unavailable" - case .jsonParseError: - return "Could not parse JSON-RPC request" - case .removed: - return "Reserved. Not used." - case .checksumError: - return "Checksum validation failed" - } - } - } -} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Configuration.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Configuration.swift deleted file mode 100644 index 9446730..0000000 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Configuration.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import Path - -public struct Configuration: Codable { - public var defaultUsername: String? - - public init() { - self.defaultUsername = nil - } - - public mutating func load() throws { - guard let data = Current.files.contents(atPath: Path.configurationFile.string) else { return } - self = try JSONDecoder().decode(Configuration.self, from: data) - } - - public func save() throws { - let data = try JSONEncoder().encode(self) - try Current.files.createDirectory(at: Path.configurationFile.url.deletingLastPathComponent(), withIntermediateDirectories: true) - Current.files.createFile(atPath: Path.configurationFile.string, contents: data) - } -} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Migration.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Migration.swift deleted file mode 100644 index 3c7460a..0000000 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Migration.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Path - -/// Migrates any application support files from Xcodes < v0.4 if application support files from >= v0.4 don't exist -public func migrateApplicationSupportFiles() { - if Current.files.fileExistsAtPath(Path.oldXcodesApplicationSupport.string) { - if Current.files.fileExistsAtPath(Path.xcodesApplicationSupport.string) { - Current.logging.log("Removing old support files...") - try? Current.files.removeItem(Path.oldXcodesApplicationSupport.url) - Current.logging.log("Done") - } - else { - Current.logging.log("Migrating old support files...") - try? Current.files.moveItem(Path.oldXcodesApplicationSupport.url, Path.xcodesApplicationSupport.url) - Current.logging.log("Done") - } - } -} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Version+Gem.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Version+Gem.swift deleted file mode 100644 index 57869ab..0000000 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Version+Gem.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import Version - -public extension Version { - /** - Attempts to parse Gem::Version representations. - - E.g.: - 9.2b3 - 9.1.2 - 9.2 - 9 - - Doesn't handle GM prerelease identifier - */ - init?(gemVersion: String) { - let nsrange = NSRange(gemVersion.startIndex.. String in - switch identifier.lowercased() { - case "a": return "Alpha" - case "b": return "Beta" - default: return identifier - } - } - - self = Version(major: major, minor: minor, patch: patch, prereleaseIdentifiers: prereleaseIdentifiers) - } -} \ No newline at end of file diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Version.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Version.swift deleted file mode 100644 index cafdedb..0000000 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Version.swift +++ /dev/null @@ -1,3 +0,0 @@ -import Version - -public let version = Version("0.12.0")! diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/XcodeInstaller.swift b/Xcodes/XcodesKit/Sources/XcodesKit/XcodeInstaller.swift deleted file mode 100644 index f89d7f8..0000000 --- a/Xcodes/XcodesKit/Sources/XcodesKit/XcodeInstaller.swift +++ /dev/null @@ -1,733 +0,0 @@ -import Foundation -import PromiseKit -import Path -import AppleAPI -import Version -import LegibleError - -/// Downloads and installs Xcodes -public final class XcodeInstaller { - static let XcodeTeamIdentifier = "59GAB85EFG" - static let XcodeCertificateAuthority = ["Software Signing", "Apple Code Signing Certification Authority", "Apple Root CA"] - - public enum Error: LocalizedError, Equatable { - case damagedXIP(url: URL) - case failedToMoveXcodeToDestination(Path) - case failedSecurityAssessment(xcode: InstalledXcode, output: String) - case codesignVerifyFailed(output: String) - case unexpectedCodeSigningIdentity(identifier: String, certificateAuthority: [String]) - case unsupportedFileFormat(extension: String) - case missingSudoerPassword - case unavailableVersion(Version) - case noNonPrereleaseVersionAvailable - case noPrereleaseVersionAvailable - case missingUsernameOrPassword - case versionAlreadyInstalled(InstalledXcode) - case invalidVersion(String) - case versionNotInstalled(Version) - - public var errorDescription: String? { - switch self { - case .damagedXIP(let url): - return "The archive \"\(url.lastPathComponent)\" is damaged and can't be expanded." - case .failedToMoveXcodeToDestination(let destination): - return "Failed to move Xcode to the \(destination.string) directory." - case .failedSecurityAssessment(let xcode, let output): - return """ - Xcode \(xcode.version) failed its security assessment with the following output: - \(output) - It remains installed at \(xcode.path) if you wish to use it anyways. - """ - case .codesignVerifyFailed(let output): - return """ - The downloaded Xcode failed code signing verification with the following output: - \(output) - """ - case .unexpectedCodeSigningIdentity(let identity, let certificateAuthority): - return """ - The downloaded Xcode doesn't have the expected code signing identity. - Got: - \(identity) - \(certificateAuthority) - Expected: - \(XcodeInstaller.XcodeTeamIdentifier) - \(XcodeInstaller.XcodeCertificateAuthority) - """ - case .unsupportedFileFormat(let fileExtension): - return "xcodes doesn't (yet) support installing Xcode from the \(fileExtension) file format." - case .missingSudoerPassword: - return "Missing password. Please try again." - case let .unavailableVersion(version): - return "Could not find version \(version.xcodeDescription)." - case .noNonPrereleaseVersionAvailable: - return "No non-prerelease versions available." - case .noPrereleaseVersionAvailable: - return "No prerelease versions available." - case .missingUsernameOrPassword: - return "Missing username or a password. Please try again." - case let .versionAlreadyInstalled(installedXcode): - return "\(installedXcode.version.xcodeDescription) is already installed at \(installedXcode.path)" - case let .invalidVersion(version): - return "\(version) is not a valid version number." - case let .versionNotInstalled(version): - return "\(version.xcodeDescription) is not installed." - } - } - } - - /// A numbered step - enum InstallationStep: CustomStringConvertible { - case downloading(version: String, progress: String) - case unarchiving - case moving(destination: String) - case trashingArchive(archiveName: String) - case checkingSecurity - case finishing - - var description: String { - "(\(stepNumber)/\(stepCount)) \(message)" - } - - var message: String { - switch self { - case .downloading(let version, let progress): - return "Downloading Xcode \(version): \(progress)" - case .unarchiving: - return "Unarchiving Xcode (This can take a while)" - case .moving(let destination): - return "Moving Xcode to \(destination)" - case .trashingArchive(let archiveName): - return "Moving Xcode archive \(archiveName) to the Trash" - case .checkingSecurity: - return "Checking security assessment and code signing" - case .finishing: - return "Finishing installation" - } - } - - var stepNumber: Int { - switch self { - case .downloading: return 1 - case .unarchiving: return 2 - case .moving: return 3 - case .trashingArchive: return 4 - case .checkingSecurity: return 5 - case .finishing: return 6 - } - } - - var stepCount: Int { 6 } - } - - private var configuration: Configuration - private var xcodeList: XcodeList - - public init(configuration: Configuration, xcodeList: XcodeList) { - self.configuration = configuration - self.xcodeList = xcodeList - } - - public enum InstallationType { - case version(String) - case url(String, Path) - case latest - case latestPrerelease - } - - public enum Downloader { - case urlSession - case aria2(Path) - } - - public func install(_ installationType: InstallationType, downloader: Downloader, destination: Path) -> Promise { - return firstly { () -> Promise in - return self.install(installationType, downloader: downloader, destination: destination, attemptNumber: 0) - } - .done { xcode in - Current.logging.log("\nXcode \(xcode.version.descriptionWithoutBuildMetadata) has been installed to \(xcode.path.string)") - Current.shell.exit(0) - } - } - - private func install(_ installationType: InstallationType, downloader: Downloader, destination: Path, attemptNumber: Int) -> Promise { - return firstly { () -> Promise<(Xcode, URL)> in - return self.getXcodeArchive(installationType, downloader: downloader, destination: destination) - } - .then { xcode, url -> Promise in - return self.installArchivedXcode(xcode, at: url, to: destination) - } - .recover { error -> Promise in - switch error { - case XcodeInstaller.Error.damagedXIP(let damagedXIPURL): - guard attemptNumber < 1 else { throw error } - - switch installationType { - case .url: - // If the user provided the URL, don't try to recover and leave it up to them. - throw error - default: - // If the XIP was just downloaded, remove it and try to recover. - return firstly { () -> Promise in - Current.logging.log(error.legibleLocalizedDescription) - Current.logging.log("Removing damaged XIP and re-attempting installation.\n") - try Current.files.removeItem(at: damagedXIPURL) - return self.install(installationType, downloader: downloader, destination: destination, attemptNumber: attemptNumber + 1) - } - } - default: - throw error - } - } - } - - private func getXcodeArchive(_ installationType: InstallationType, downloader: Downloader, destination: Path) -> Promise<(Xcode, URL)> { - return firstly { () -> Promise<(Xcode, URL)> in - switch installationType { - case .latest: - Current.logging.log("Updating...") - - return update() - .then { availableXcodes -> Promise<(Xcode, URL)> in - guard let latestNonPrereleaseXcode = availableXcodes.filter(\.version.isNotPrerelease).sorted(\.version).last else { - throw Error.noNonPrereleaseVersionAvailable - } - Current.logging.log("Latest non-prerelease version available is \(latestNonPrereleaseXcode.version.xcodeDescription)") - - if let installedXcode = Current.files.installedXcodes(destination).first(where: { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: latestNonPrereleaseXcode.version) }) { - throw Error.versionAlreadyInstalled(installedXcode) - } - - return self.downloadXcode(version: latestNonPrereleaseXcode.version, downloader: downloader) - } - case .latestPrerelease: - Current.logging.log("Updating...") - - return update() - .then { availableXcodes -> Promise<(Xcode, URL)> in - guard let latestPrereleaseXcode = availableXcodes - .filter({ $0.version.isPrerelease }) - .filter({ $0.releaseDate != nil }) - .sorted(by: { $0.releaseDate! < $1.releaseDate! }) - .last - else { - throw Error.noNonPrereleaseVersionAvailable - } - Current.logging.log("Latest prerelease version available is \(latestPrereleaseXcode.version.xcodeDescription)") - - if let installedXcode = Current.files.installedXcodes(destination).first(where: { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: latestPrereleaseXcode.version) }) { - throw Error.versionAlreadyInstalled(installedXcode) - } - - return self.downloadXcode(version: latestPrereleaseXcode.version, downloader: downloader) - } - case .url(let versionString, let path): - guard let version = Version(xcodeVersion: versionString) ?? versionFromXcodeVersionFile() else { - throw Error.invalidVersion(versionString) - } - let xcode = Xcode(version: version, url: path.url, filename: String(path.string.suffix(fromLast: "/")), releaseDate: nil) - return Promise.value((xcode, path.url)) - case .version(let versionString): - guard let version = Version(xcodeVersion: versionString) ?? versionFromXcodeVersionFile() else { - throw Error.invalidVersion(versionString) - } - if let installedXcode = Current.files.installedXcodes(destination).first(where: { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: version) }) { - throw Error.versionAlreadyInstalled(installedXcode) - } - return self.downloadXcode(version: version, downloader: downloader) - } - } - } - - private func versionFromXcodeVersionFile() -> Version? { - let xcodeVersionFilePath = Path.cwd.join(".xcode-version") - let version = (try? Data(contentsOf: xcodeVersionFilePath.url)) - .flatMap { String(data: $0, encoding: .utf8) } - .flatMap(Version.init(gemVersion:)) - return version - } - - private func downloadXcode(version: Version, downloader: Downloader) -> Promise<(Xcode, URL)> { - return firstly { () -> Promise in -// loginIfNeeded().map { version } -// } -// .then { version -> Promise in - if self.xcodeList.shouldUpdate { - return self.xcodeList.update().map { _ in version } - } - else { - return Promise.value(version) - } - } - .then { version -> Promise<(Xcode, URL)> in - guard let xcode = self.xcodeList.availableXcodes.first(where: { version.isEqualWithoutBuildMetadataIdentifiers(to: $0.version) }) else { - throw Error.unavailableVersion(version) - } - - // Move to the next line - Current.logging.log("") - let formatter = NumberFormatter(numberStyle: .percent) - var observation: NSKeyValueObservation? - - let promise = self.downloadOrUseExistingArchive(for: xcode, downloader: downloader, progressChanged: { progress in - observation?.invalidate() - observation = progress.observe(\.fractionCompleted) { progress, _ in - // These escape codes move up a line and then clear to the end - Current.logging.log("\u{1B}[1A\u{1B}[K\(InstallationStep.downloading(version: xcode.version.description, progress: formatter.string(from: progress.fractionCompleted)!))") - } - }) - - return promise - .get { _ in observation?.invalidate() } - .map { return (xcode, $0) } - } - } - -// func loginIfNeeded(withUsername existingUsername: String? = nil) -> Promise { -// return firstly { () -> Promise in -// return Current.network.validateSession() -// } -// .recover { error -> Promise in -// guard -// let username = existingUsername ?? self.findUsername() ?? Current.shell.readLine(prompt: "Apple ID: "), -// let password = self.findPassword(withUsername: username) ?? Current.shell.readSecureLine(prompt: "Apple ID Password: ") -// else { throw Error.missingUsernameOrPassword } -// -// return firstly { () -> Promise in -// self.login(username, password: password) -// } -// .recover { error -> Promise in -// Current.logging.log(error.legibleLocalizedDescription) -// -// if case Client.AuthenticationErrro.invalidUsernameOrPassword = error { -// Current.logging.log("Try entering your password again") -// return self.loginIfNeeded(withUsername: username) -// } -// else { -// return Promise(error: error) -// } -// } -// } -// } -// -// public func login(_ username: String, password: String) -> Promise { -// return firstly { () -> Promise in -// Current.network.login(accountName: username, password: password) -// } -// .recover { error -> Promise in -// -// if let error = error as? Client.AuthenticationErrro { -// switch error { -// case .invalidUsernameOrPassword(_): -// // remove any keychain password if we fail to log with an invalid username or password so it doesn't try again. -// try? Current.keychain.remove(username) -// default: -// break -// } -// } -// -// return Promise(error: error) -// } -// .done { _ in -// try? Current.keychain.set(password, key: username) -// -// if self.configuration.defaultUsername != username { -// self.configuration.defaultUsername = username -// try? self.configuration.save() -// } -// } -// } - - let xcodesUsername = "XCODES_USERNAME" - let xcodesPassword = "XCODES_PASSWORD" - - func findUsername() -> String? { - if let username = Current.shell.env(xcodesUsername) { - return username - } - else if let username = configuration.defaultUsername { - return username - } - return nil - } - - func findPassword(withUsername username: String) -> String? { - if let password = Current.shell.env(xcodesPassword) { - return password - } - else if let password = try? Current.keychain.getString(username){ - return password - } - return nil - } - - public func downloadOrUseExistingArchive(for xcode: Xcode, downloader: Downloader, progressChanged: @escaping (Progress) -> Void) -> Promise { - // Check to see if the archive is in the expected path in case it was downloaded but failed to install - let expectedArchivePath = Path.xcodesApplicationSupport/"Xcode-\(xcode.version).\(xcode.filename.suffix(fromLast: "."))" - // aria2 downloads directly to the destination (instead of into /tmp first) so we need to make sure that the download isn't incomplete - let aria2DownloadMetadataPath = expectedArchivePath.parent/(expectedArchivePath.basename() + ".aria2") - var aria2DownloadIsIncomplete = false - if case .aria2 = downloader, aria2DownloadMetadataPath.exists { - aria2DownloadIsIncomplete = true - } - if Current.files.fileExistsAtPath(expectedArchivePath.string), aria2DownloadIsIncomplete == false { - Current.logging.log("(1/6) Found existing archive that will be used for installation at \(expectedArchivePath).") - return Promise.value(expectedArchivePath.url) - } - else { - let destination = Path.xcodesApplicationSupport/"Xcode-\(xcode.version).\(xcode.filename.suffix(fromLast: "."))" - switch downloader { - case .aria2(let aria2Path): - return downloadXcodeWithAria2( - xcode, - to: destination, - aria2Path: aria2Path, - progressChanged: progressChanged - ) - case .urlSession: - return downloadXcodeWithURLSession( - xcode, - to: destination, - progressChanged: progressChanged - ) - } - } - } - - public func downloadXcodeWithAria2(_ xcode: Xcode, to destination: Path, aria2Path: Path, progressChanged: @escaping (Progress) -> Void) -> Promise { - let cookies = AppleAPI.Current.network.session.configuration.httpCookieStorage?.cookies(for: xcode.url) ?? [] - - return attemptRetryableTask(maximumRetryCount: 3) { - let (progress, promise) = Current.shell.downloadWithAria2( - aria2Path, - xcode.url, - destination, - cookies - ) - progressChanged(progress) - return promise.map { _ in destination.url } - } - } - - public func downloadXcodeWithURLSession(_ xcode: Xcode, to destination: Path, progressChanged: @escaping (Progress) -> Void) -> Promise { - let resumeDataPath = Path.xcodesApplicationSupport/"Xcode-\(xcode.version).resumedata" - let persistedResumeData = Current.files.contents(atPath: resumeDataPath.string) - - return attemptResumableTask(maximumRetryCount: 3) { resumeData in - let (progress, promise) = Current.network.downloadTask(with: xcode.url, - to: destination.url, - resumingWith: resumeData ?? persistedResumeData) - progressChanged(progress) - return promise.map { $0.saveLocation } - } - .tap { result in - self.persistOrCleanUpResumeData(at: resumeDataPath, for: result) - } - } - - public func installArchivedXcode(_ xcode: Xcode, at archiveURL: URL, to destination: Path) -> Promise { - let passwordInput = { - Promise { seal in - Current.logging.log("xcodes requires superuser privileges in order to finish installation.") - guard let password = Current.shell.readSecureLine(prompt: "macOS User Password: ") else { seal.reject(Error.missingSudoerPassword); return } - seal.fulfill(password + "\n") - } - } - - return firstly { () -> Promise in - let destinationURL = destination.join("Xcode-\(xcode.version.descriptionWithoutBuildMetadata).app").url - switch archiveURL.pathExtension { - case "xip": - return unarchiveAndMoveXIP(at: archiveURL, to: destinationURL).map { xcodeURL in - guard - let path = Path(url: xcodeURL), - Current.files.fileExists(atPath: path.string), - let installedXcode = InstalledXcode(path: path) - else { throw Error.failedToMoveXcodeToDestination(destination) } - return installedXcode - } - case "dmg": - throw Error.unsupportedFileFormat(extension: "dmg") - default: - throw Error.unsupportedFileFormat(extension: archiveURL.pathExtension) - } - } - .then { xcode -> Promise in - Current.logging.log(InstallationStep.trashingArchive(archiveName: archiveURL.lastPathComponent).description) - try Current.files.trashItem(at: archiveURL) - Current.logging.log(InstallationStep.checkingSecurity.description) - - return when(fulfilled: self.verifySecurityAssessment(of: xcode), - self.verifySigningCertificate(of: xcode.path.url)) - .map { xcode } - } - .then { xcode -> Promise in - Current.logging.log(InstallationStep.finishing.description) - - return self.enableDeveloperMode(passwordInput: passwordInput).map { xcode } - } - .then { xcode -> Promise in - self.approveLicense(for: xcode, passwordInput: passwordInput).map { xcode } - } - .then { xcode -> Promise in - self.installComponents(for: xcode, passwordInput: passwordInput).map { xcode } - } - } - - public func uninstallXcode(_ versionString: String, destination: Path) -> Promise { - return firstly { () -> Promise<(InstalledXcode, URL)> in - guard let version = Version(xcodeVersion: versionString) else { - throw Error.invalidVersion(versionString) - } - - guard let installedXcode = Current.files.installedXcodes(destination).first(where: { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: version) }) else { - throw Error.versionNotInstalled(version) - } - - return Promise { seal in - seal.fulfill(try Current.files.trashItem(at: installedXcode.path.url)) - }.map { (installedXcode, $0) } - } - .then { (installedXcode, trashURL) -> Promise<(InstalledXcode, URL)> in - // If we just uninstalled the selected Xcode, try to select the latest installed version so things don't accidentally break - Current.shell.xcodeSelectPrintPath() - .then { output -> Promise<(InstalledXcode, URL)> in - if output.out.hasPrefix(installedXcode.path.string), - let latestInstalledXcode = Current.files.installedXcodes(destination).sorted(by: { $0.version < $1.version }).last { - return selectXcodeAtPath(latestInstalledXcode.path.string) - .map { output in - Current.logging.log("Selected \(output.out)") - return (installedXcode, trashURL) - } - } - else { - return Promise.value((installedXcode, trashURL)) - } - } - } - .done { (installedXcode, trashURL) in - Current.logging.log("Xcode \(installedXcode.version.xcodeDescription) moved to Trash: \(trashURL.path)") - Current.shell.exit(0) - } - } - - public func update() -> Promise<[Xcode]> { -// return firstly { () -> Promise in -// loginIfNeeded() -// } -// .then { () -> Promise<[Xcode]> in - self.xcodeList.update() -// } - } - - public func updateAndPrint(destination: Path) -> Promise { - update() - .then { xcodes -> Promise in - self.printAvailableXcodes(xcodes, installed: Current.files.installedXcodes(destination)) - } - .done { - Current.shell.exit(0) - } - } - - public func printAvailableXcodes(_ xcodes: [Xcode], installed installedXcodes: [InstalledXcode]) -> Promise { - struct ReleasedVersion { - let version: Version - let releaseDate: Date? - } - - var allXcodeVersions = xcodes.map { ReleasedVersion(version: $0.version, releaseDate: $0.releaseDate) } - for installedXcode in installedXcodes { - // If an installed version isn't listed online, add the installed version - if !allXcodeVersions.contains(where: { releasedVersion in - releasedVersion.version.isEquivalentForDeterminingIfInstalled(toInstalled: installedXcode.version) - }) { - allXcodeVersions.append(ReleasedVersion(version: installedXcode.version, releaseDate: nil)) - } - // If an installed version is the same as one that's listed online which doesn't have build metadata, replace it with the installed version with build metadata - else if let index = allXcodeVersions.firstIndex(where: { releasedVersion in - releasedVersion.version.isEquivalentForDeterminingIfInstalled(toInstalled: installedXcode.version) && - releasedVersion.version.buildMetadataIdentifiers.isEmpty - }) { - allXcodeVersions[index] = ReleasedVersion(version: installedXcode.version, releaseDate: nil) - } - } - - return Current.shell.xcodeSelectPrintPath() - .done { output in - let selectedInstalledXcodeVersion = installedXcodes.first { output.out.hasPrefix($0.path.string) }.map { $0.version } - - allXcodeVersions - .sorted { first, second -> Bool in - // Sort prereleases by release date, otherwise sort by version - if first.version.isPrerelease, second.version.isPrerelease, let firstDate = first.releaseDate, let secondDate = second.releaseDate { - return firstDate < secondDate - } - return first.version < second.version - } - .forEach { releasedVersion in - var output = releasedVersion.version.xcodeDescription - if installedXcodes.contains(where: { releasedVersion.version.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) }) { - if releasedVersion.version == selectedInstalledXcodeVersion { - output += " (Installed, Selected)" - } - else { - output += " (Installed)" - } - } - Current.logging.log(output) - } - } - } - - public func printInstalledXcodes(destination: Path) -> Promise { - Current.shell.xcodeSelectPrintPath() - .done { pathOutput in - Current.files.installedXcodes(destination) - .sorted { $0.version < $1.version } - .forEach { installedXcode in - var output = installedXcode.version.xcodeDescription - if pathOutput.out.hasPrefix(installedXcode.path.string) { - output += " (Selected)" - } - Current.logging.log(output) - } - } - } - - func unarchiveAndMoveXIP(at source: URL, to destination: URL) -> Promise { - return firstly { () -> Promise in - Current.logging.log(InstallationStep.unarchiving.description) - return Current.shell.unxip(source) - .recover { (error) throws -> Promise in - if case Process.PMKError.execution(_, _, let standardError) = error, - standardError?.contains("damaged and can’t be expanded") == true { - throw Error.damagedXIP(url: source) - } - throw error - } - } - .map { output -> URL in - Current.logging.log(InstallationStep.moving(destination: destination.path).description) - - let xcodeURL = source.deletingLastPathComponent().appendingPathComponent("Xcode.app") - let xcodeBetaURL = source.deletingLastPathComponent().appendingPathComponent("Xcode-beta.app") - if Current.files.fileExists(atPath: xcodeURL.path) { - try Current.files.moveItem(at: xcodeURL, to: destination) - } - else if Current.files.fileExists(atPath: xcodeBetaURL.path) { - try Current.files.moveItem(at: xcodeBetaURL, to: destination) - } - - return destination - } - } - - public func verifySecurityAssessment(of xcode: InstalledXcode) -> Promise { - return Current.shell.spctlAssess(xcode.path.url) - .recover { (error: Swift.Error) throws -> Promise in - var output = "" - if case let Process.PMKError.execution(_, possibleOutput, possibleError) = error { - output = [possibleOutput, possibleError].compactMap { $0 }.joined(separator: "\n") - } - throw Error.failedSecurityAssessment(xcode: xcode, output: output) - } - .asVoid() - } - - func verifySigningCertificate(of url: URL) -> Promise { - return Current.shell.codesignVerify(url) - .recover { error -> Promise in - var output = "" - if case let Process.PMKError.execution(_, possibleOutput, possibleError) = error { - output = [possibleOutput, possibleError].compactMap { $0 }.joined(separator: "\n") - } - throw Error.codesignVerifyFailed(output: output) - } - .map { output -> CertificateInfo in - // codesign prints to stderr - return self.parseCertificateInfo(output.err) - } - .done { cert in - guard - cert.teamIdentifier == XcodeInstaller.XcodeTeamIdentifier, - cert.authority == XcodeInstaller.XcodeCertificateAuthority - else { throw Error.unexpectedCodeSigningIdentity(identifier: cert.teamIdentifier, certificateAuthority: cert.authority) } - } - } - - public struct CertificateInfo { - public var authority: [String] - public var teamIdentifier: String - public var bundleIdentifier: String - } - - public func parseCertificateInfo(_ rawInfo: String) -> CertificateInfo { - var info = CertificateInfo(authority: [], teamIdentifier: "", bundleIdentifier: "") - - for part in rawInfo.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: .newlines) { - if part.hasPrefix("Authority") { - info.authority.append(part.components(separatedBy: "=")[1]) - } - if part.hasPrefix("TeamIdentifier") { - info.teamIdentifier = part.components(separatedBy: "=")[1] - } - if part.hasPrefix("Identifier") { - info.bundleIdentifier = part.components(separatedBy: "=")[1] - } - } - - return info - } - - func enableDeveloperMode(passwordInput: @escaping () -> Promise) -> Promise { - return firstly { () -> Promise in - Current.shell.authenticateSudoerIfNecessary(passwordInput: passwordInput) - } - .then { possiblePassword -> Promise in - return Current.shell.devToolsSecurityEnable(possiblePassword).map { _ in possiblePassword } - } - .then { possiblePassword in - return Current.shell.addStaffToDevelopersGroup(possiblePassword).asVoid() - } - } - - func approveLicense(for xcode: InstalledXcode, passwordInput: @escaping () -> Promise) -> Promise { - return firstly { () -> Promise in - Current.shell.authenticateSudoerIfNecessary(passwordInput: passwordInput) - } - .then { possiblePassword in - return Current.shell.acceptXcodeLicense(xcode, possiblePassword).asVoid() - } - } - - func installComponents(for xcode: InstalledXcode, passwordInput: @escaping () -> Promise) -> Promise { - return firstly { () -> Promise in - Current.shell.authenticateSudoerIfNecessary(passwordInput: passwordInput) - } - .then { possiblePassword -> Promise in - Current.shell.runFirstLaunch(xcode, possiblePassword).asVoid() - } - .then { () -> Promise<(String, String, String)> in - return when(fulfilled: - Current.shell.getUserCacheDir().map { $0.out }, - Current.shell.buildVersion().map { $0.out }, - Current.shell.xcodeBuildVersion(xcode).map { $0.out } - ) - } - .then { cacheDirectory, macOSBuildVersion, toolsVersion -> Promise in - return Current.shell.touchInstallCheck(cacheDirectory, macOSBuildVersion, toolsVersion).asVoid() - } - } -} - -private extension XcodeInstaller { - func persistOrCleanUpResumeData(at path: Path, for result: Result) { - switch result { - case .fulfilled: - try? Current.files.removeItem(at: path.url) - case .rejected(let error): - guard let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data else { return } - Current.files.createFile(atPath: path.string, contents: resumeData) - } - } -} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/XcodeSelect.swift b/Xcodes/XcodesKit/Sources/XcodesKit/XcodeSelect.swift deleted file mode 100644 index 3bc5185..0000000 --- a/Xcodes/XcodesKit/Sources/XcodesKit/XcodeSelect.swift +++ /dev/null @@ -1,134 +0,0 @@ -import Foundation -import PromiseKit -import Path -import Version - -public func selectXcode(shouldPrint: Bool, pathOrVersion: String, destination: Path) -> Promise { - firstly { () -> Promise in - Current.shell.xcodeSelectPrintPath() - } - .then { output -> Promise in - if shouldPrint { - if output.out.isEmpty == false { - Current.logging.log(output.out) - Current.shell.exit(0) - return Promise.value(()) - } - else { - Current.logging.log("No selected Xcode") - Current.shell.exit(0) - return Promise.value(()) - } - } - - if let version = Version(xcodeVersion: pathOrVersion), - let installedXcode = Current.files.installedXcodes(destination).first(where: { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: version) }) { - return selectXcodeAtPath(installedXcode.path.string) - .done { output in - Current.logging.log("Selected \(output.out)") - Current.shell.exit(0) - } - } - else { - return selectXcodeAtPath(pathOrVersion) - .done { output in - Current.logging.log("Selected \(output.out)") - Current.shell.exit(0) - } - .recover { _ in - try selectXcodeInteractively(currentPath: output.out, destination: destination) - .done { output in - Current.logging.log("Selected \(output.out)") - Current.shell.exit(0) - } - } - } - } -} - -public func selectXcodeInteractively(currentPath: String, destination: Path, shouldRetry: Bool) -> Promise { - if shouldRetry { - func selectWithRetry(currentPath: String) -> Promise { - return firstly { - try selectXcodeInteractively(currentPath: currentPath, destination: destination) - } - .recover { error throws -> Promise in - guard case XcodeSelectError.invalidIndex = error else { throw error } - Current.logging.log("\(error.legibleLocalizedDescription)\n") - return selectWithRetry(currentPath: currentPath) - } - } - - return selectWithRetry(currentPath: currentPath) - } - else { - return firstly { - try selectXcodeInteractively(currentPath: currentPath, destination: destination) - } - } -} - -public func selectXcodeInteractively(currentPath: String, destination: Path) throws -> Promise { - let sortedInstalledXcodes = Current.files.installedXcodes(destination).sorted { $0.version < $1.version } - - Current.logging.log("Available Xcode versions:") - - sortedInstalledXcodes - .enumerated() - .forEach { index, installedXcode in - var output = "\(index + 1)) \(installedXcode.version.xcodeDescription)" - if currentPath.hasPrefix(installedXcode.path.string) { - output += " (Selected)" - } - Current.logging.log(output) - } - - let possibleSelectionNumberString = Current.shell.readLine(prompt: "Enter the number of the Xcode to select: ") - guard - let selectionNumberString = possibleSelectionNumberString, - let selectionNumber = Int(selectionNumberString), - sortedInstalledXcodes.indices.contains(selectionNumber - 1) - else { - throw XcodeSelectError.invalidIndex(min: 1, max: sortedInstalledXcodes.count, given: possibleSelectionNumberString) - } - - return selectXcodeAtPath(sortedInstalledXcodes[selectionNumber - 1].path.string) -} - -public func selectXcodeAtPath(_ pathString: String) -> Promise { - firstly { () -> Promise in - guard Current.files.fileExists(atPath: pathString) else { - throw XcodeSelectError.invalidPath(pathString) - } - - let passwordInput = { - Promise { seal in - Current.logging.log("xcodes requires superuser privileges to select an Xcode") - guard let password = Current.shell.readSecureLine(prompt: "macOS User Password: ") else { seal.reject(XcodeInstaller.Error.missingSudoerPassword); return } - seal.fulfill(password + "\n") - } - } - - return Current.shell.authenticateSudoerIfNecessary(passwordInput: passwordInput) - } - .then { possiblePassword in - Current.shell.xcodeSelectSwitch(password: possiblePassword, path: pathString) - } - .then { _ in - Current.shell.xcodeSelectPrintPath() - } -} - -public enum XcodeSelectError: LocalizedError { - case invalidPath(String) - case invalidIndex(min: Int, max: Int, given: String?) - - public var errorDescription: String? { - switch self { - case .invalidPath(let pathString): - return "Not a valid Xcode path: \(pathString)" - case .invalidIndex(let min, let max, let given): - return "Not a valid number. Expecting a whole number between \(min)-\(max), but given \(given ?? "nothing")." - } - } -} diff --git a/Xcodes/XcodesKit/Tests/LinuxMain.swift b/Xcodes/XcodesKit/Tests/LinuxMain.swift deleted file mode 100644 index e779a38..0000000 --- a/Xcodes/XcodesKit/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import XcodesKitTests - -var tests = [XCTestCaseEntry]() -tests += XcodesKitTests.allTests() -XCTMain(tests) diff --git a/Xcodes/XcodesKit/Tests/XcodesKitTests/XCTestManifests.swift b/Xcodes/XcodesKit/Tests/XcodesKitTests/XCTestManifests.swift deleted file mode 100644 index d577375..0000000 --- a/Xcodes/XcodesKit/Tests/XcodesKitTests/XCTestManifests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest - -#if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(XcodesKitTests.allTests), - ] -} -#endif diff --git a/Xcodes/XcodesKit/Tests/XcodesKitTests/XcodesKitTests.swift b/Xcodes/XcodesKit/Tests/XcodesKitTests/XcodesKitTests.swift deleted file mode 100644 index f379c74..0000000 --- a/Xcodes/XcodesKit/Tests/XcodesKitTests/XcodesKitTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import XCTest -@testable import XcodesKit - -final class XcodesKitTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(XcodesKit().text, "Hello, World!") - } - - static var allTests = [ - ("testExample", testExample), - ] -}