Move XcodesKit source into Xcodes.app

This commit is contained in:
Brandon Evans 2020-12-22 20:41:36 -07:00
parent 17145ec16d
commit 18a7ea3af2
No known key found for this signature in database
GPG key ID: D58A4B8DB64F8E93
31 changed files with 226 additions and 1347 deletions

View file

@ -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 */
};

View file

@ -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"
}
}
]

View file

@ -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
}
}

View file

@ -1,5 +1,4 @@
import SwiftUI
import XcodesKit
import Version
import PromiseKit

View file

@ -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 }

View file

@ -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"
}

View file

@ -1,5 +0,0 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/

View file

@ -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"]),
]
)

View file

@ -1,3 +0,0 @@
# XcodesKit
A description of this package.

View file

@ -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"
}
}
}
}

View file

@ -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)
}
}

View file

@ -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")
}
}
}

View file

@ -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)
}
}

View file

@ -1,3 +0,0 @@
import Version
public let version = Version("0.12.0")!

View file

@ -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 cant 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)
}
}
}

View file

@ -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")."
}
}
}

View file

@ -1,7 +0,0 @@
import XCTest
import XcodesKitTests
var tests = [XCTestCaseEntry]()
tests += XcodesKitTests.allTests()
XCTMain(tests)

View file

@ -1,9 +0,0 @@
import XCTest
#if !canImport(ObjectiveC)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(XcodesKitTests.allTests),
]
}
#endif

View file

@ -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),
]
}