mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Move XcodesKit source into Xcodes.app
This commit is contained in:
parent
17145ec16d
commit
18a7ea3af2
31 changed files with 226 additions and 1347 deletions
|
|
@ -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 = "<group>"; };
|
||||
CA44901E2463AD34003D8213 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||
CA538A0C255A4F1A00E64DD7 /* AppleAPI */ = {isa = PBXFileReference; lastKnownFileType = folder; name = AppleAPI; path = Xcodes/AppleAPI; sourceTree = "<group>"; };
|
||||
CA538A0F255A4F3300E64DD7 /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = "<group>"; };
|
||||
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCodeTextView.swift; sourceTree = "<group>"; };
|
||||
CA735108257BF96D00EA9CF8 /* AttributedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedText.swift; sourceTree = "<group>"; };
|
||||
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -56,6 +75,20 @@
|
|||
CABFA9A02592EAF500380FEE /* R&PLogo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "R&PLogo.png"; sourceTree = "<group>"; };
|
||||
CABFA9A12592EAFB00380FEE /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||
CABFA9A32592ED5700380FEE /* Apple.paw */ = {isa = PBXFileReference; lastKnownFileType = file; name = Apple.paw; path = ../xcodes/Apple.paw; sourceTree = "<group>"; };
|
||||
CABFA9A62592EEE900380FEE /* Version+Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Version+Xcode.swift"; sourceTree = "<group>"; };
|
||||
CABFA9A72592EEE900380FEE /* XcodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeList.swift; sourceTree = "<group>"; };
|
||||
CABFA9A82592EEE900380FEE /* Version+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Version+.swift"; sourceTree = "<group>"; };
|
||||
CABFA9A92592EEE900380FEE /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = "<group>"; };
|
||||
CABFA9AB2592EEE900380FEE /* URLRequest+Apple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Apple.swift"; sourceTree = "<group>"; };
|
||||
CABFA9AC2592EEE900380FEE /* Foundation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Foundation.swift; sourceTree = "<group>"; };
|
||||
CABFA9AE2592EEE900380FEE /* Path+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Path+.swift"; sourceTree = "<group>"; };
|
||||
CABFA9B02592EEEA00380FEE /* Promise+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+.swift"; sourceTree = "<group>"; };
|
||||
CABFA9B22592EEEA00380FEE /* Entry+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Entry+.swift"; sourceTree = "<group>"; };
|
||||
CABFA9B32592EEEA00380FEE /* URLSession+Promise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Promise.swift"; sourceTree = "<group>"; };
|
||||
CABFA9B42592EEEA00380FEE /* Process.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Process.swift; sourceTree = "<group>"; };
|
||||
CABFA9B82592EEEA00380FEE /* FileManager+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+.swift"; sourceTree = "<group>"; };
|
||||
CABFA9B92592EEEA00380FEE /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = "<group>"; };
|
||||
CABFA9BA2592EEEA00380FEE /* DateFormatter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+.swift"; sourceTree = "<group>"; };
|
||||
CABFA9D42592EF6300380FEE /* DECISIONS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DECISIONS.md; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
|
|
@ -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 */
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
@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<AnyCancellable>()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import SwiftUI
|
||||
import XcodesKit
|
||||
import Version
|
||||
import PromiseKit
|
||||
|
||||
|
|
|
|||
|
|
@ -53,130 +53,6 @@ public struct Shell {
|
|||
public func xcodeSelectSwitch(password: String?, path: String) -> Promise<ProcessOutput> {
|
||||
xcodeSelectSwitch(password, path)
|
||||
}
|
||||
|
||||
public var downloadWithAria2: (Path, URL, Path, [HTTPCookie]) -> (Progress, Promise<Void>) = { 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: #"((?<percent>\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<Void> { 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<Int8>.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 }
|
||||
|
|
@ -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"
|
||||
}
|
||||
5
Xcodes/XcodesKit/.gitignore
vendored
5
Xcodes/XcodesKit/.gitignore
vendored
|
|
@ -1,5 +0,0 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
|
|
@ -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"]),
|
||||
]
|
||||
)
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# XcodesKit
|
||||
|
||||
A description of this package.
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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..<gemVersion.endIndex, in: gemVersion)
|
||||
let pattern = "^(?<major>\\d+)\\.?(?<minor>\\d?)?\\.?(?<patch>\\d?)?\\.?(?<prereleaseType>\\w?)?\\.?(?<prereleaseVersion>\\d?)"
|
||||
|
||||
guard
|
||||
let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive),
|
||||
let match = regex.firstMatch(in: gemVersion, options: [], range: nsrange),
|
||||
let majorString = match.groupNamed("major", in: gemVersion),
|
||||
let major = Int(majorString),
|
||||
let minorString = match.groupNamed("minor", in: gemVersion),
|
||||
let patchString = match.groupNamed("patch", in: gemVersion)
|
||||
else { return nil }
|
||||
|
||||
let minor = Int(minorString) ?? 0
|
||||
let patch = Int(patchString) ?? 0
|
||||
let prereleaseIdentifiers = [match.groupNamed("prereleaseType", in: gemVersion),
|
||||
match.groupNamed("prereleaseVersion", in: gemVersion)]
|
||||
.compactMap { $0 }
|
||||
.filter { !$0.isEmpty }
|
||||
.map { identifier -> 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import Version
|
||||
|
||||
public let version = Version("0.12.0")!
|
||||
|
|
@ -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<Void> {
|
||||
return firstly { () -> Promise<InstalledXcode> 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<InstalledXcode> {
|
||||
return firstly { () -> Promise<(Xcode, URL)> in
|
||||
return self.getXcodeArchive(installationType, downloader: downloader, destination: destination)
|
||||
}
|
||||
.then { xcode, url -> Promise<InstalledXcode> in
|
||||
return self.installArchivedXcode(xcode, at: url, to: destination)
|
||||
}
|
||||
.recover { error -> Promise<InstalledXcode> 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<InstalledXcode> 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<Version> in
|
||||
// loginIfNeeded().map { version }
|
||||
// }
|
||||
// .then { version -> Promise<Version> 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<Void> {
|
||||
// return firstly { () -> Promise<Void> in
|
||||
// return Current.network.validateSession()
|
||||
// }
|
||||
// .recover { error -> Promise<Void> 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<Void> in
|
||||
// self.login(username, password: password)
|
||||
// }
|
||||
// .recover { error -> Promise<Void> 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<Void> {
|
||||
// return firstly { () -> Promise<Void> in
|
||||
// Current.network.login(accountName: username, password: password)
|
||||
// }
|
||||
// .recover { error -> Promise<Void> 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<URL> {
|
||||
// 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<URL> {
|
||||
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<URL> {
|
||||
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<InstalledXcode> {
|
||||
let passwordInput = {
|
||||
Promise<String> { 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<InstalledXcode> 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<InstalledXcode> 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<InstalledXcode> in
|
||||
Current.logging.log(InstallationStep.finishing.description)
|
||||
|
||||
return self.enableDeveloperMode(passwordInput: passwordInput).map { xcode }
|
||||
}
|
||||
.then { xcode -> Promise<InstalledXcode> in
|
||||
self.approveLicense(for: xcode, passwordInput: passwordInput).map { xcode }
|
||||
}
|
||||
.then { xcode -> Promise<InstalledXcode> in
|
||||
self.installComponents(for: xcode, passwordInput: passwordInput).map { xcode }
|
||||
}
|
||||
}
|
||||
|
||||
public func uninstallXcode(_ versionString: String, destination: Path) -> Promise<Void> {
|
||||
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<URL> { 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<Void> in
|
||||
// loginIfNeeded()
|
||||
// }
|
||||
// .then { () -> Promise<[Xcode]> in
|
||||
self.xcodeList.update()
|
||||
// }
|
||||
}
|
||||
|
||||
public func updateAndPrint(destination: Path) -> Promise<Void> {
|
||||
update()
|
||||
.then { xcodes -> Promise<Void> in
|
||||
self.printAvailableXcodes(xcodes, installed: Current.files.installedXcodes(destination))
|
||||
}
|
||||
.done {
|
||||
Current.shell.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
public func printAvailableXcodes(_ xcodes: [Xcode], installed installedXcodes: [InstalledXcode]) -> Promise<Void> {
|
||||
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<Void> {
|
||||
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<URL> {
|
||||
return firstly { () -> Promise<ProcessOutput> in
|
||||
Current.logging.log(InstallationStep.unarchiving.description)
|
||||
return Current.shell.unxip(source)
|
||||
.recover { (error) throws -> Promise<ProcessOutput> 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<Void> {
|
||||
return Current.shell.spctlAssess(xcode.path.url)
|
||||
.recover { (error: Swift.Error) throws -> Promise<ProcessOutput> 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<Void> {
|
||||
return Current.shell.codesignVerify(url)
|
||||
.recover { error -> Promise<ProcessOutput> 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<String>) -> Promise<Void> {
|
||||
return firstly { () -> Promise<String?> in
|
||||
Current.shell.authenticateSudoerIfNecessary(passwordInput: passwordInput)
|
||||
}
|
||||
.then { possiblePassword -> Promise<String?> 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<String>) -> Promise<Void> {
|
||||
return firstly { () -> Promise<String?> in
|
||||
Current.shell.authenticateSudoerIfNecessary(passwordInput: passwordInput)
|
||||
}
|
||||
.then { possiblePassword in
|
||||
return Current.shell.acceptXcodeLicense(xcode, possiblePassword).asVoid()
|
||||
}
|
||||
}
|
||||
|
||||
func installComponents(for xcode: InstalledXcode, passwordInput: @escaping () -> Promise<String>) -> Promise<Void> {
|
||||
return firstly { () -> Promise<String?> in
|
||||
Current.shell.authenticateSudoerIfNecessary(passwordInput: passwordInput)
|
||||
}
|
||||
.then { possiblePassword -> Promise<Void> 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<Void> in
|
||||
return Current.shell.touchInstallCheck(cacheDirectory, macOSBuildVersion, toolsVersion).asVoid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension XcodeInstaller {
|
||||
func persistOrCleanUpResumeData<T>(at path: Path, for result: Result<T>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
import Foundation
|
||||
import PromiseKit
|
||||
import Path
|
||||
import Version
|
||||
|
||||
public func selectXcode(shouldPrint: Bool, pathOrVersion: String, destination: Path) -> Promise<Void> {
|
||||
firstly { () -> Promise<ProcessOutput> in
|
||||
Current.shell.xcodeSelectPrintPath()
|
||||
}
|
||||
.then { output -> Promise<Void> 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<ProcessOutput> {
|
||||
if shouldRetry {
|
||||
func selectWithRetry(currentPath: String) -> Promise<ProcessOutput> {
|
||||
return firstly {
|
||||
try selectXcodeInteractively(currentPath: currentPath, destination: destination)
|
||||
}
|
||||
.recover { error throws -> Promise<ProcessOutput> 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<ProcessOutput> {
|
||||
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<ProcessOutput> {
|
||||
firstly { () -> Promise<String?> in
|
||||
guard Current.files.fileExists(atPath: pathString) else {
|
||||
throw XcodeSelectError.invalidPath(pathString)
|
||||
}
|
||||
|
||||
let passwordInput = {
|
||||
Promise<String> { 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")."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import XCTest
|
||||
|
||||
import XcodesKitTests
|
||||
|
||||
var tests = [XCTestCaseEntry]()
|
||||
tests += XcodesKitTests.allTests()
|
||||
XCTMain(tests)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import XCTest
|
||||
|
||||
#if !canImport(ObjectiveC)
|
||||
public func allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(XcodesKitTests.allTests),
|
||||
]
|
||||
}
|
||||
#endif
|
||||
|
|
@ -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),
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue