Add AltKit back in as checked-in code

This commit is contained in:
Eric Warmenhoven 2023-05-07 03:06:24 -04:00 committed by LibretroAdmin
parent cd9fd1ec9b
commit 1b3f9b84d9
19 changed files with 1642 additions and 14 deletions

View File

@ -0,0 +1,7 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

View File

@ -0,0 +1,38 @@
cmake_minimum_required(VERSION 3.15.1)
project(AltKit LANGUAGES C Swift)
add_library(CAltKit
Sources/CAltKit/NSError+ALTServerError.h
Sources/CAltKit/NSError+ALTServerError.m
)
add_library(AltKit
Sources/AltKit/Extensions/ALTServerError+Conveniences.swift
Sources/AltKit/Extensions/Result+Conveniences.swift
Sources/AltKit/Server/Connection.swift
Sources/AltKit/Server/NetworkConnection.swift
Sources/AltKit/Server/Server.swift
Sources/AltKit/Server/ServerConnection.swift
Sources/AltKit/Server/ServerManager.swift
Sources/AltKit/Server/ServerProtocol.swift
Sources/AltKit/Types/CodableServerError.swift
)
target_link_libraries(AltKit PRIVATE CAltKit)
set_property(TARGET AltKit PROPERTY XCODE_ATTRIBUTE_SWIFT_VERSION "5.0")
# Make CAltKit's modulemap available to AltKit
set_property(TARGET AltKit PROPERTY XCODE_ATTRIBUTE_SWIFT_INCLUDE_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/Sources/CAltKit")
# Add binary dir to interface include path to make Swift header accessible to targets using AltKit
target_include_directories(AltKit INTERFACE ${CMAKE_CURRENT_BINARY_DIR})
# Copy generated Swift header to binary dir
add_custom_command(TARGET AltKit
POST_BUILD
COMMAND cp $DERIVED_SOURCES_DIR/AltKit-Swift.h ${CMAKE_CURRENT_BINARY_DIR}
)

View File

@ -0,0 +1,32 @@
// 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: "AltKit",
platforms: [
.iOS(.v11),
.tvOS(.v11)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "AltKit",
targets: ["AltKit"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.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: "CAltKit",
dependencies: []),
.target(
name: "AltKit",
dependencies: ["CAltKit"]),
]
)

View File

@ -0,0 +1,103 @@
# AltKit
AltKit allows apps to communicate with AltServers on the same WiFi network and enable features such as JIT compilation.
## Installation
To use AltKit in your app, add the following to your `Package.swift` file's dependencies:
```swift
.package(url: "https://github.com/rileytestut/AltKit.git", .upToNextMajor(from: "0.0.1")),
```
Next, add the AltKit package as a dependency for your target:
```swift
.product(name: "AltKit", package: "AltKit"),
```
Finally, right-click on your app's `Info.plist`, select "Open As > Source Code", then add the following entries:
```xml
<key>NSBonjourServices</key>
<array>
<string>_altserver._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>[Your app] uses the local network to find and communicate with AltServer.</string>
<key>ALTDeviceID</key>
<string></string>
```
⚠️ The `ALTDeviceID` key must be present in your app's `Info.plist` to let AltStore know it should replace that entry with the user's device's UDID when sideloading your app, which is required for AltKit to work. For local development with AltKit, we recommend setting `ALTDeviceID` to your development device's UDID to ensure everything works as expected. Otherwise, the exact value doesn't matter as long as the entry exists, since it will be replaced by AltStore during installation.
### CMake Integration
Note: CMake 3.15 is required for the integration. The integration only works with the Xcode generator.
Steps:
- Add the AltKit CMake project to your CMake project using `add_subdirectory`.
- Add Swift to your project's supported languages. (ex.: `project(projName LANGUAGES C Swift)`)
If you're using `add_compile_options` or `target_compile_options` and the Swift compiler complains about the option not being supported, it's possible to use CMake's generator expressions to limit the options to non-Swift source files.
Example:
```
add_compile_options($<$<NOT:$<COMPILE_LANGUAGE:Swift>>:-fPIC>)
```
## Usage
### Swift
```swift
import AltKit
ServerManager.shared.startDiscovering()
ServerManager.shared.autoconnect { result in
switch result
{
case .failure(let error): print("Could not auto-connect to server.", error)
case .success(let connection):
connection.enableUnsignedCodeExecution { result in
switch result
{
case .failure(let error): print("Could not enable JIT compilation.", error)
case .success:
print("Successfully enabled JIT compilation!")
ServerManager.shared.stopDiscovering()
}
connection.disconnect()
}
}
}
```
### Objective-C
```objc
@import AltKit;
[[ALTServerManager sharedManager] startDiscovering];
[[ALTServerManager sharedManager] autoconnectWithCompletionHandler:^(ALTServerConnection *connection, NSError *error) {
if (error)
{
return NSLog(@"Could not auto-connect to server. %@", error);
}
[connection enableUnsignedCodeExecutionWithCompletionHandler:^(BOOL success, NSError *error) {
if (success)
{
NSLog(@"Successfully enabled JIT compilation!");
[[ALTServerManager sharedManager] stopDiscovering];
}
else
{
NSLog(@"Could not enable JIT compilation. %@", error);
}
[connection disconnect];
}];
}];
```

View File

@ -0,0 +1,38 @@
//
// ALTServerError+Conveniences.swift
// AltKit
//
// Created by Riley Testut on 6/4/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
public extension ALTServerError
{
init<E: Error>(_ error: E)
{
switch error
{
case let error as ALTServerError: self = error
case let error as ALTServerConnectionError:
self = ALTServerError(.connectionFailed, underlyingError: error)
case is DecodingError: self = ALTServerError(.invalidRequest, underlyingError: error)
case is EncodingError: self = ALTServerError(.invalidResponse, underlyingError: error)
case let error as NSError:
var userInfo = error.userInfo
if !userInfo.keys.contains(NSUnderlyingErrorKey)
{
// Assign underlying error (if there isn't already one).
userInfo[NSUnderlyingErrorKey] = error
}
self = ALTServerError(.underlyingError, userInfo: userInfo)
}
}
init<E: Error>(_ code: ALTServerError.Code, underlyingError: E)
{
self = ALTServerError(code, userInfo: [NSUnderlyingErrorKey: underlyingError])
}
}

View File

@ -0,0 +1,76 @@
//
// Result+Conveniences.swift
// AltStore
//
// Created by Riley Testut on 5/22/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
extension Result
{
var value: Success? {
switch self
{
case .success(let value): return value
case .failure: return nil
}
}
var error: Failure? {
switch self
{
case .success: return nil
case .failure(let error): return error
}
}
init(_ value: Success?, _ error: Failure?)
{
switch (value, error)
{
case (let value?, _): self = .success(value)
case (_, let error?): self = .failure(error)
case (nil, nil): preconditionFailure("Either value or error must be non-nil")
}
}
}
extension Result where Success == Void
{
init(_ success: Bool, _ error: Failure?)
{
if success
{
self = .success(())
}
else if let error = error
{
self = .failure(error)
}
else
{
preconditionFailure("Error must be non-nil if success is false")
}
}
}
extension Result
{
init<T, U>(_ values: (T?, U?), _ error: Failure?) where Success == (T, U)
{
if let value1 = values.0, let value2 = values.1
{
self = .success((value1, value2))
}
else if let error = error
{
self = .failure(error)
}
else
{
preconditionFailure("Error must be non-nil if either provided values are nil")
}
}
}

View File

@ -0,0 +1,18 @@
//
// Connection.swift
// AltKit
//
// Created by Riley Testut on 6/1/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import Network
public protocol Connection
{
func send(_ data: Data, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
func receiveData(expectedSize: Int, completionHandler: @escaping (Result<Data, ALTServerError>) -> Void)
func disconnect()
}

View File

@ -0,0 +1,60 @@
//
// NetworkConnection.swift
// AltKit
//
// Created by Riley Testut on 6/1/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import Network
@available(iOS 12, tvOS 12, watchOS 5, macOS 10.14, *)
public class NetworkConnection: NSObject, Connection
{
public let nwConnection: NWConnection
public init(_ nwConnection: NWConnection)
{
self.nwConnection = nwConnection
}
public func send(_ data: Data, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
self.nwConnection.send(content: data, completion: .contentProcessed { (error) in
if let error = error
{
completionHandler(.failure(.init(.lostConnection, underlyingError: error)))
}
else
{
completionHandler(.success(()))
}
})
}
public func receiveData(expectedSize: Int, completionHandler: @escaping (Result<Data, ALTServerError>) -> Void)
{
self.nwConnection.receive(minimumIncompleteLength: expectedSize, maximumLength: expectedSize) { (data, context, isComplete, error) in
switch (data, error)
{
case (let data?, _): completionHandler(.success(data))
case (_, let error?): completionHandler(.failure(.init(.lostConnection, underlyingError: error)))
case (nil, nil): completionHandler(.failure(ALTServerError(.lostConnection)))
}
}
}
public func disconnect()
{
switch self.nwConnection.state
{
case .cancelled, .failed: break
default: self.nwConnection.cancel()
}
}
override public var description: String {
return "\(self.nwConnection.endpoint) (Network)"
}
}

View File

@ -0,0 +1,44 @@
//
// Server.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
@objc(ALTServer)
public class Server: NSObject, Identifiable
{
public let id: String
public let service: NetService
public var name: String? {
return self.service.hostName
}
public internal(set) var isPreferred = false
public override var hash: Int {
return self.id.hashValue ^ self.service.name.hashValue
}
init?(service: NetService, txtData: Data)
{
let txtDictionary = NetService.dictionary(fromTXTRecord: txtData)
guard let identifierData = txtDictionary["serverID"], let identifier = String(data: identifierData, encoding: .utf8) else { return nil }
self.id = identifier
self.service = service
super.init()
}
public override func isEqual(_ object: Any?) -> Bool
{
guard let server = object as? Server else { return false }
return self.id == server.id && self.service.name == server.service.name // service.name is consistent, and is not the human readable name (hostName).
}
}

View File

@ -0,0 +1,180 @@
//
// ServerConnection.swift
// AltStore
//
// Created by Riley Testut on 1/7/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
@objc(ALTServerConnection) @objcMembers
public class ServerConnection: NSObject
{
public let server: Server
public let connection: Connection
init(server: Server, connection: Connection)
{
self.server = server
self.connection = connection
}
deinit
{
self.connection.disconnect()
}
@objc
public func disconnect()
{
self.connection.disconnect()
}
}
public extension ServerConnection
{
func enableUnsignedCodeExecution(completion: @escaping (Result<Void, Error>) -> Void)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: "ALTDeviceID") as? String else {
return ServerManager.shared.callbackQueue.async {
completion(.failure(ConnectionError.unknownUDID))
}
}
self.enableUnsignedCodeExecution(udid: udid, completion: completion)
}
func enableUnsignedCodeExecution(udid: String, completion: @escaping (Result<Void, Error>) -> Void)
{
func finish(_ result: Result<Void, Error>)
{
ServerManager.shared.callbackQueue.async {
completion(result)
}
}
let request = EnableUnsignedCodeExecutionRequest(udid: udid, processID: ProcessInfo.processInfo.processIdentifier)
self.send(request) { (result) in
switch result
{
case .failure(let error): finish(.failure(error))
case .success:
self.receiveResponse() { (result) in
switch result
{
case .failure(let error): finish(.failure(error))
case .success(.error(let response)): finish(.failure(response.error))
case .success(.enableUnsignedCodeExecution): finish(.success(()))
case .success: finish(.failure(ALTServerError(.unknownResponse)))
}
}
}
}
}
}
public extension ServerConnection
{
@objc(enableUnsignedCodeExecutionWithCompletionHandler:)
func __enableUnsignedCodeExecution(completion: @escaping (Bool, Error?) -> Void)
{
self.enableUnsignedCodeExecution { result in
switch result {
case .failure(let error): completion(false, error)
case .success: completion(true, nil)
}
}
}
@objc(enableUnsignedCodeExecutionWithUDID:completionHandler:)
func __enableUnsignedCodeExecution(udid: String, completion: @escaping (Bool, Error?) -> Void)
{
self.enableUnsignedCodeExecution(udid: udid) { result in
switch result {
case .failure(let error): completion(false, error)
case .success: completion(true, nil)
}
}
}
}
private extension ServerConnection
{
func send<T: Encodable>(_ payload: T, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
let data: Data
if let payload = payload as? Data
{
data = payload
}
else
{
data = try JSONEncoder().encode(payload)
}
func process<T>(_ result: Result<T, ALTServerError>) -> Bool
{
switch result
{
case .success: return true
case .failure(let error):
completionHandler(.failure(error))
return false
}
}
let requestSize = Int32(data.count)
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
self.connection.send(requestSizeData) { (result) in
guard process(result) else { return }
self.connection.send(data) { (result) in
guard process(result) else { return }
completionHandler(.success(()))
}
}
}
catch
{
print("Invalid request.", error)
completionHandler(.failure(ALTServerError(.invalidRequest)))
}
}
func receiveResponse(completionHandler: @escaping (Result<ServerResponse, Error>) -> Void)
{
let size = MemoryLayout<Int32>.size
self.connection.receiveData(expectedSize: size) { (result) in
do
{
let data = try result.get()
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
self.connection.receiveData(expectedSize: expectedBytes) { (result) in
do
{
let data = try result.get()
let response = try JSONDecoder().decode(ServerResponse.self, from: data)
completionHandler(.success(response))
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
}

View File

@ -0,0 +1,354 @@
//
// ServerManager.swift
// AltStore
//
// Created by Riley Testut on 5/30/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Network
import UIKit
@_exported import CAltKit
public enum ConnectionError: LocalizedError
{
case serverNotFound
case connectionFailed(Server)
case connectionDropped(Server)
case unknownUDID
case unsupportedOS
public var errorDescription: String? {
switch self
{
case .serverNotFound: return NSLocalizedString("Could not find AltServer.", comment: "")
case .connectionFailed: return NSLocalizedString("Could not connect to AltServer.", comment: "")
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
case .unknownUDID: return NSLocalizedString("This device's UDID could not be determined.", comment: "")
case .unsupportedOS: return NSLocalizedString("This device's OS version is too old to run AltKit.", comment: "")
}
}
}
@objc(ALTServerManager) @objcMembers
public class ServerManager: NSObject
{
public static let shared = ServerManager()
private(set) var isDiscovering = false
private(set) var discoveredServers = [Server]()
public var discoveredServerHandler: ((Server) -> Void)?
public var lostServerHandler: ((Server) -> Void)?
public var callbackQueue: DispatchQueue = .main
// Allow other AltKit queues to target this one.
internal let dispatchQueue = DispatchQueue(label: "io.altstore.altkit.ServerManager", qos: .utility, autoreleaseFrequency: .workItem)
private var serviceBrowser: NetServiceBrowser?
private var resolvingServices = Set<NetService>()
private var autoconnectGroup: DispatchGroup?
private var ignoredServers = Set<Server>()
private override init()
{
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(ServerManager.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ServerManager.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
}
}
public extension ServerManager
{
@objc
func startDiscovering()
{
guard !self.isDiscovering else { return }
self.isDiscovering = true
DispatchQueue.main.async {
// NetServiceBrowser must be initialized on main thread.
// https://stackoverflow.com/questions/3526661/nsnetservicebrowser-delegate-not-called-when-searching
let serviceBrowser = NetServiceBrowser()
serviceBrowser.delegate = self
serviceBrowser.includesPeerToPeer = false
serviceBrowser.searchForServices(ofType: ALTServerServiceType, inDomain: "")
self.serviceBrowser = serviceBrowser
}
}
@objc
func stopDiscovering()
{
guard self.isDiscovering else { return }
self.isDiscovering = false
self.discoveredServers.removeAll()
self.ignoredServers.removeAll()
self.resolvingServices.removeAll()
self.serviceBrowser?.stop()
self.serviceBrowser = nil
}
func connect(to server: Server, completion: @escaping (Result<ServerConnection, Error>) -> Void)
{
var didFinish = false
func finish(_ result: Result<ServerConnection, Error>)
{
guard !didFinish else { return }
didFinish = true
self.ignoredServers.insert(server)
self.callbackQueue.async {
completion(result)
}
}
self.dispatchQueue.async {
guard #available(iOS 12, tvOS 12, watchOS 5, macOS 10.14, *) else {
finish(.failure(ConnectionError.unsupportedOS))
return
}
print("Connecting to service:", server.service)
let connection = NWConnection(to: .service(name: server.service.name, type: server.service.type, domain: server.service.domain, interface: nil), using: .tcp)
connection.stateUpdateHandler = { [unowned connection] (state) in
switch state
{
case .failed(let error):
print("Failed to connect to service \(server.service.name).", error)
finish(.failure(ConnectionError.connectionFailed(server)))
case .cancelled: finish(.failure(CocoaError(.userCancelled)))
case .ready:
let networkConnection = NetworkConnection(connection)
let serverConnection = ServerConnection(server: server, connection: networkConnection)
finish(.success(serverConnection))
case .waiting: break
case .setup: break
case .preparing: break
@unknown default: break
}
}
connection.start(queue: self.dispatchQueue)
}
}
func autoconnect(completion: @escaping (Result<ServerConnection, Error>) -> Void)
{
self.dispatchQueue.async {
if case let availableServers = self.discoveredServers.filter({ !self.ignoredServers.contains($0) }),
let server = availableServers.first(where: { $0.isPreferred }) ?? availableServers.first
{
return self.connect(to: server, completion: completion)
}
self.autoconnectGroup = DispatchGroup()
self.autoconnectGroup?.enter()
self.autoconnectGroup?.notify(queue: self.dispatchQueue) {
self.autoconnectGroup = nil
guard
case let availableServers = self.discoveredServers.filter({ !self.ignoredServers.contains($0) }),
let server = availableServers.first(where: { $0.isPreferred }) ?? availableServers.first
else { return self.autoconnect(completion: completion) }
self.connect(to: server, completion: completion)
}
}
}
}
public extension ServerManager
{
@objc(sharedManager)
class var __shared: ServerManager {
return ServerManager.shared
}
@objc(connectToServer:completionHandler:)
func __connect(to server: Server, completion: @escaping (ServerConnection?, Error?) -> Void)
{
self.connect(to: server) { result in
completion(result.value, result.error)
}
}
@objc(autoconnectWithCompletionHandler:)
func __autoconnect(completion: @escaping (ServerConnection?, Error?) -> Void)
{
self.autoconnect { result in
completion(result.value, result.error)
}
}
}
private extension ServerManager
{
func addDiscoveredServer(_ server: Server)
{
self.dispatchQueue.async {
let serverID = Bundle.main.object(forInfoDictionaryKey: "ALTServerID") as? String
server.isPreferred = (server.id == serverID)
guard !self.discoveredServers.contains(server) else { return }
self.discoveredServers.append(server)
if let callback = self.discoveredServerHandler
{
self.callbackQueue.async {
callback(server)
}
}
}
}
func removeDiscoveredServer(_ server: Server)
{
self.dispatchQueue.async {
guard let index = self.discoveredServers.firstIndex(of: server) else { return }
self.discoveredServers.remove(at: index)
if let callback = self.lostServerHandler
{
self.callbackQueue.async {
callback(server)
}
}
}
}
}
@objc
private extension ServerManager
{
@objc
func didEnterBackground(_ notification: Notification)
{
guard self.isDiscovering else { return }
self.resolvingServices.removeAll()
self.discoveredServers.removeAll()
self.serviceBrowser?.stop()
}
@objc
func willEnterForeground(_ notification: Notification)
{
guard self.isDiscovering else { return }
self.serviceBrowser?.searchForServices(ofType: ALTServerServiceType, inDomain: "")
}
}
extension ServerManager: NetServiceBrowserDelegate
{
public func netServiceBrowserWillSearch(_ browser: NetServiceBrowser)
{
print("Discovering servers...")
}
public func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser)
{
print("Stopped discovering servers.")
}
public func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber])
{
print("Failed to discover servers.", errorDict)
}
public func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool)
{
self.dispatchQueue.async {
service.delegate = self
if let txtData = service.txtRecordData(), let server = Server(service: service, txtData: txtData)
{
self.addDiscoveredServer(server)
}
else
{
service.resolve(withTimeout: 3.0)
self.resolvingServices.insert(service)
}
self.autoconnectGroup?.enter()
if !moreComing
{
self.autoconnectGroup?.leave()
}
}
}
public func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool)
{
if let server = self.discoveredServers.first(where: { $0.service == service })
{
self.removeDiscoveredServer(server)
}
}
}
extension ServerManager: NetServiceDelegate
{
public func netServiceDidResolveAddress(_ service: NetService)
{
defer {
self.dispatchQueue.async {
guard self.resolvingServices.contains(service) else { return }
self.resolvingServices.remove(service)
self.autoconnectGroup?.leave()
}
}
guard let data = service.txtRecordData(), let server = Server(service: service, txtData: data) else { return }
self.addDiscoveredServer(server)
}
public func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber])
{
print("Error resolving net service \(sender).", errorDict)
self.dispatchQueue.async {
guard self.resolvingServices.contains(sender) else { return }
self.resolvingServices.remove(sender)
self.autoconnectGroup?.leave()
}
}
public func netService(_ sender: NetService, didUpdateTXTRecord data: Data)
{
let txtDict = NetService.dictionary(fromTXTRecord: data)
print("Service \(sender) updated TXT Record:", txtDict)
}
public func netServiceDidStop(_ sender: NetService)
{
self.dispatchQueue.async {
guard self.resolvingServices.contains(sender) else { return }
self.resolvingServices.remove(sender)
self.autoconnectGroup?.leave()
}
}
}

View File

@ -0,0 +1,165 @@
//
// ServerProtocol.swift
// AltServer
//
// Created by Riley Testut on 5/24/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
public let ALTServerServiceType = "_altserver._tcp"
protocol ServerMessageProtocol: Codable
{
var version: Int { get }
var identifier: String { get }
}
public enum ServerRequest: Decodable
{
case enableUnsignedCodeExecution(EnableUnsignedCodeExecutionRequest)
case unknown(identifier: String, version: Int)
var identifier: String {
switch self
{
case .enableUnsignedCodeExecution(let request): return request.identifier
case .unknown(let identifier, _): return identifier
}
}
var version: Int {
switch self
{
case .enableUnsignedCodeExecution(let request): return request.version
case .unknown(_, let version): return version
}
}
private enum CodingKeys: String, CodingKey
{
case identifier
case version
}
public init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
let version = try container.decode(Int.self, forKey: .version)
let identifier = try container.decode(String.self, forKey: .identifier)
switch identifier
{
case "EnableUnsignedCodeExecutionRequest":
let request = try EnableUnsignedCodeExecutionRequest(from: decoder)
self = .enableUnsignedCodeExecution(request)
default:
self = .unknown(identifier: identifier, version: version)
}
}
}
public enum ServerResponse: Decodable
{
case enableUnsignedCodeExecution(EnableUnsignedCodeExecutionResponse)
case error(ErrorResponse)
case unknown(identifier: String, version: Int)
var identifier: String {
switch self
{
case .enableUnsignedCodeExecution(let response): return response.identifier
case .error(let response): return response.identifier
case .unknown(let identifier, _): return identifier
}
}
var version: Int {
switch self
{
case .enableUnsignedCodeExecution(let response): return response.version
case .error(let response): return response.version
case .unknown(_, let version): return version
}
}
private enum CodingKeys: String, CodingKey
{
case identifier
case version
}
public init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
let version = try container.decode(Int.self, forKey: .version)
let identifier = try container.decode(String.self, forKey: .identifier)
switch identifier
{
case "EnableUnsignedCodeExecutionResponse":
let response = try EnableUnsignedCodeExecutionResponse(from: decoder)
self = .enableUnsignedCodeExecution(response)
case "ErrorResponse":
let response = try ErrorResponse(from: decoder)
self = .error(response)
default:
self = .unknown(identifier: identifier, version: version)
}
}
}
// _Don't_ provide generic SuccessResponse, as that would prevent us
// from easily changing response format for a request in the future.
public struct ErrorResponse: ServerMessageProtocol
{
public var version = 2
public var identifier = "ErrorResponse"
public var error: ALTServerError {
return self.serverError?.error ?? ALTServerError(self.errorCode)
}
private var serverError: CodableServerError?
// Legacy (v1)
private var errorCode: ALTServerError.Code
public init(error: ALTServerError)
{
self.serverError = CodableServerError(error: error)
self.errorCode = error.code
}
}
public struct EnableUnsignedCodeExecutionRequest: ServerMessageProtocol
{
public var version = 1
public var identifier = "EnableUnsignedCodeExecutionRequest"
public var udid: String
public var processID: Int32?
public var processName: String?
public init(udid: String, processID: Int32? = nil, processName: String? = nil)
{
self.udid = udid
self.processID = processID
self.processName = processName
}
}
public struct EnableUnsignedCodeExecutionResponse: ServerMessageProtocol
{
public var version = 1
public var identifier = "EnableUnsignedCodeExecutionResponse"
public init()
{
}
}

View File

@ -0,0 +1,126 @@
//
// CodableServerError.swift
// AltKit
//
// Created by Riley Testut on 3/5/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {}
extension CodableServerError
{
enum UserInfoValue: Codable
{
case string(String)
case error(NSError)
public init(from decoder: Decoder) throws
{
let container = try decoder.singleValueContainer()
if
let data = try? container.decode(Data.self),
let error = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: data)
{
self = .error(error)
}
else if let string = try? container.decode(String.self)
{
self = .string(string)
}
else
{
throw DecodingError.dataCorruptedError(in: container, debugDescription: "UserInfoValue value cannot be decoded")
}
}
func encode(to encoder: Encoder) throws
{
var container = encoder.singleValueContainer()
switch self
{
case .string(let string): try container.encode(string)
case .error(let error):
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: error, requiringSecureCoding: true) else {
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "UserInfoValue value \(self) cannot be encoded")
throw EncodingError.invalidValue(self, context)
}
try container.encode(data)
}
}
}
}
struct CodableServerError: Codable
{
var error: ALTServerError {
return ALTServerError(self.errorCode, userInfo: self.userInfo ?? [:])
}
private var errorCode: ALTServerError.Code
private var userInfo: [String: Any]?
private enum CodingKeys: String, CodingKey
{
case errorCode
case userInfo
}
init(error: ALTServerError)
{
self.errorCode = error.code
var userInfo = error.userInfo
if let localizedRecoverySuggestion = (error as NSError).localizedRecoverySuggestion
{
userInfo[NSLocalizedRecoverySuggestionErrorKey] = localizedRecoverySuggestion
}
if !userInfo.isEmpty
{
self.userInfo = userInfo
}
}
init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
let errorCode = try container.decode(Int.self, forKey: .errorCode)
self.errorCode = ALTServerError.Code(rawValue: errorCode) ?? .unknown
let rawUserInfo = try container.decodeIfPresent([String: UserInfoValue].self, forKey: .userInfo)
let userInfo = rawUserInfo?.mapValues { (value) -> Any in
switch value
{
case .string(let string): return string
case .error(let error): return error
}
}
self.userInfo = userInfo
}
func encode(to encoder: Encoder) throws
{
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.error.code.rawValue, forKey: .errorCode)
let rawUserInfo = self.userInfo?.compactMapValues { (value) -> UserInfoValue? in
switch value
{
case let string as String: return .string(string)
case let error as NSError: return .error(error)
default: return nil
}
}
try container.encodeIfPresent(rawUserInfo, forKey: .userInfo)
}
}

View File

@ -0,0 +1,69 @@
//
// NSError+ALTServerError.h
// AltStore
//
// Created by Riley Testut on 5/30/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
#import <Foundation/Foundation.h>
extern NSErrorDomain const AltServerErrorDomain;
extern NSErrorDomain const AltServerInstallationErrorDomain;
extern NSErrorDomain const AltServerConnectionErrorDomain;
extern NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey;
extern NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey;
extern NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey;
extern NSErrorUserInfoKey const ALTAppNameErrorKey;
extern NSErrorUserInfoKey const ALTDeviceNameErrorKey;
typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError)
{
ALTServerErrorUnderlyingError = -1,
ALTServerErrorUnknown = 0,
ALTServerErrorConnectionFailed = 1,
ALTServerErrorLostConnection = 2,
ALTServerErrorDeviceNotFound = 3,
ALTServerErrorDeviceWriteFailed = 4,
ALTServerErrorInvalidRequest = 5,
ALTServerErrorInvalidResponse = 6,
ALTServerErrorInvalidApp = 7,
ALTServerErrorInstallationFailed = 8,
ALTServerErrorMaximumFreeAppLimitReached = 9,
ALTServerErrorUnsupportediOSVersion = 10,
ALTServerErrorUnknownRequest = 11,
ALTServerErrorUnknownResponse = 12,
ALTServerErrorInvalidAnisetteData = 13,
ALTServerErrorPluginNotFound = 14,
ALTServerErrorProfileNotFound = 15,
ALTServerErrorAppDeletionFailed = 16,
ALTServerErrorRequestedAppNotRunning = 100,
};
typedef NS_ERROR_ENUM(AltServerConnectionErrorDomain, ALTServerConnectionError)
{
ALTServerConnectionErrorUnknown,
ALTServerConnectionErrorDeviceLocked,
ALTServerConnectionErrorInvalidRequest,
ALTServerConnectionErrorInvalidResponse,
ALTServerConnectionErrorUSBMUXD,
ALTServerConnectionErrorSSL,
ALTServerConnectionErrorTimedOut,
};
NS_ASSUME_NONNULL_BEGIN
@interface NSError (ALTServerError)
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,305 @@
//
// NSError+ALTServerError.m
// AltStore
//
// Created by Riley Testut on 5/30/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
#import "NSError+ALTServerError.h"
NSErrorDomain const AltServerErrorDomain = @"com.rileytestut.AltServer";
NSErrorDomain const AltServerInstallationErrorDomain = @"com.rileytestut.AltServer.Installation";
NSErrorDomain const AltServerConnectionErrorDomain = @"com.rileytestut.AltServer.Connection";
NSErrorUserInfoKey const ALTUnderlyingErrorDomainErrorKey = @"underlyingErrorDomain";
NSErrorUserInfoKey const ALTUnderlyingErrorCodeErrorKey = @"underlyingErrorCode";
NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdentifier";
NSErrorUserInfoKey const ALTAppNameErrorKey = @"appName";
NSErrorUserInfoKey const ALTDeviceNameErrorKey = @"deviceName";
@implementation NSError (ALTServerError)
+ (void)load
{
[NSError setUserInfoValueProviderForDomain:AltServerErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
if ([userInfoKey isEqualToString:NSLocalizedFailureReasonErrorKey])
{
return [error altserver_localizedFailureReason];
}
else if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey])
{
return [error altserver_localizedRecoverySuggestion];
}
return nil;
}];
[NSError setUserInfoValueProviderForDomain:AltServerConnectionErrorDomain provider:^id _Nullable(NSError * _Nonnull error, NSErrorUserInfoKey _Nonnull userInfoKey) {
if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey])
{
return [error altserver_connection_localizedDescription];
}
else if ([userInfoKey isEqualToString:NSLocalizedRecoverySuggestionErrorKey])
{
return [error altserver_connection_localizedRecoverySuggestion];
}
// else if ([userInfoKey isEqualToString:NSLocalizedFailureErrorKey])
// {
// return @"";
// }
return nil;
}];
}
//- (nullable NSString *)altserver_localizedDescription
//{
// switch ((ALTServerError)self.code)
// {
// case ALTServerErrorUnderlyingError:
// {
// NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
// return [underlyingError localizedDescription];
// }
//
// default:
// {
// return [self altserver_localizedFailureReason];
// }
// }
//}
- (nullable NSString *)altserver_localizedFailureReason
{
switch ((ALTServerError)self.code)
{
case ALTServerErrorUnderlyingError:
{
NSError *underlyingError = self.userInfo[NSUnderlyingErrorKey];
if (underlyingError.localizedFailureReason != nil)
{
return underlyingError.localizedFailureReason;
}
NSString *underlyingErrorCode = self.userInfo[ALTUnderlyingErrorCodeErrorKey];
if (underlyingErrorCode != nil)
{
return [NSString stringWithFormat:NSLocalizedString(@"Error code: %@", @""), underlyingErrorCode];
}
return nil;
}
case ALTServerErrorUnknown:
return NSLocalizedString(@"An unknown error occured.", @"");
case ALTServerErrorConnectionFailed:
#if TARGET_OS_OSX
return NSLocalizedString(@"There was an error connecting to the device.", @"");
#else
return NSLocalizedString(@"Could not connect to AltServer.", @"");
#endif
case ALTServerErrorLostConnection:
return NSLocalizedString(@"Lost connection to AltServer.", @"");
case ALTServerErrorDeviceNotFound:
return NSLocalizedString(@"AltServer could not find this device.", @"");
case ALTServerErrorDeviceWriteFailed:
return NSLocalizedString(@"Failed to write app data to device.", @"");
case ALTServerErrorInvalidRequest:
return NSLocalizedString(@"AltServer received an invalid request.", @"");
case ALTServerErrorInvalidResponse:
return NSLocalizedString(@"AltServer sent an invalid response.", @"");
case ALTServerErrorInvalidApp:
return NSLocalizedString(@"The app is invalid.", @"");
case ALTServerErrorInstallationFailed:
return NSLocalizedString(@"An error occured while installing the app.", @"");
case ALTServerErrorMaximumFreeAppLimitReached:
return NSLocalizedString(@"Cannot activate more than 3 apps and app extensions.", @"");
case ALTServerErrorUnsupportediOSVersion:
return NSLocalizedString(@"Your device must be running iOS 12.2 or later to install AltStore.", @"");
case ALTServerErrorUnknownRequest:
return NSLocalizedString(@"AltServer does not support this request.", @"");
case ALTServerErrorUnknownResponse:
return NSLocalizedString(@"Received an unknown response from AltServer.", @"");
case ALTServerErrorInvalidAnisetteData:
return NSLocalizedString(@"The provided anisette data is invalid.", @"");
case ALTServerErrorPluginNotFound:
return NSLocalizedString(@"AltServer could not connect to Mail plug-in.", @"");
case ALTServerErrorProfileNotFound:
return [self profileErrorLocalizedDescriptionWithBaseDescription:NSLocalizedString(@"Could not find profile", "")];
case ALTServerErrorAppDeletionFailed:
return NSLocalizedString(@"An error occured while removing the app.", @"");
case ALTServerErrorRequestedAppNotRunning:
{
NSString *appName = self.userInfo[ALTAppNameErrorKey] ?: NSLocalizedString(@"The requested app", @"");
NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"the device", @"");
return [NSString stringWithFormat:NSLocalizedString(@"%@ is not currently running on %@.", ""), appName, deviceName];
}
}
}
- (nullable NSString *)altserver_localizedRecoverySuggestion
{
switch ((ALTServerError)self.code)
{
case ALTServerErrorConnectionFailed:
case ALTServerErrorDeviceNotFound:
return NSLocalizedString(@"Make sure you have trusted this device with your computer and WiFi sync is enabled.", @"");
case ALTServerErrorPluginNotFound:
return NSLocalizedString(@"Make sure Mail is running and the plug-in is enabled in Mail's preferences.", @"");
case ALTServerErrorMaximumFreeAppLimitReached:
return NSLocalizedString(@"Make sure “Offload Unused Apps” is disabled in Settings > iTunes & App Stores, then install or delete all offloaded apps.", @"");
case ALTServerErrorRequestedAppNotRunning:
{
NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"your device", @"");
return [NSString stringWithFormat:NSLocalizedString(@"Make sure the app is running in the foreground on %@ then try again.", @""), deviceName];
}
default:
return nil;
}
}
- (NSString *)profileErrorLocalizedDescriptionWithBaseDescription:(NSString *)baseDescription
{
NSString *localizedDescription = nil;
NSString *bundleID = self.userInfo[ALTProvisioningProfileBundleIDErrorKey];
if (bundleID)
{
localizedDescription = [NSString stringWithFormat:@"%@ “%@”", baseDescription, bundleID];
}
else
{
localizedDescription = [NSString stringWithFormat:@"%@.", baseDescription];
}
return localizedDescription;
}
#pragma mark - AltServerConnectionErrorDomain -
- (nullable NSString *)altserver_connection_localizedDescription
{
switch ((ALTServerConnectionError)self.code)
{
case ALTServerConnectionErrorUnknown:
{
NSString *underlyingErrorDomain = self.userInfo[ALTUnderlyingErrorDomainErrorKey];
NSString *underlyingErrorCode = self.userInfo[ALTUnderlyingErrorCodeErrorKey];
if (underlyingErrorDomain != nil && underlyingErrorCode != nil)
{
return [NSString stringWithFormat:NSLocalizedString(@"%@ error %@.", @""), underlyingErrorDomain, underlyingErrorCode];
}
else if (underlyingErrorCode != nil)
{
return [NSString stringWithFormat:NSLocalizedString(@"Connection error code: %@", @""), underlyingErrorCode];
}
return nil;
}
case ALTServerConnectionErrorDeviceLocked:
{
NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"The device", @"");
return [NSString stringWithFormat:NSLocalizedString(@"%@ is currently locked.", @""), deviceName];
}
case ALTServerConnectionErrorInvalidRequest:
{
NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"The device", @"");
return [NSString stringWithFormat:NSLocalizedString(@"%@ received an invalid request from AltServer.", @""), deviceName];
}
case ALTServerConnectionErrorInvalidResponse:
{
NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"the device", @"");
return [NSString stringWithFormat:NSLocalizedString(@"AltServer received an invalid response from %@.", @""), deviceName];
}
case ALTServerConnectionErrorUSBMUXD:
{
return NSLocalizedString(@"A connection to usbmuxd could not be established.", @"");
}
case ALTServerConnectionErrorSSL:
{
NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"the device", @"");
return [NSString stringWithFormat:NSLocalizedString(@"A secure connection between AltServer and %@ could not be established.", @""), deviceName];
}
case ALTServerConnectionErrorTimedOut:
{
NSString *deviceName = self.userInfo[ALTDeviceNameErrorKey] ?: NSLocalizedString(@"the device", @"");
return [NSString stringWithFormat:NSLocalizedString(@"The connection to %@ timed out.", @""), deviceName];
}
}
return nil;
}
- (nullable NSString *)altserver_connection_localizedRecoverySuggestion
{
switch ((ALTServerConnectionError)self.code)
{
case ALTServerConnectionErrorUnknown:
{
return nil;
}
case ALTServerConnectionErrorDeviceLocked:
{
return NSLocalizedString(@"Please unlock the device with your passcode and try again.", @"");
}
case ALTServerConnectionErrorInvalidRequest:
{
break;
}
case ALTServerConnectionErrorInvalidResponse:
{
break;
}
case ALTServerConnectionErrorUSBMUXD:
{
break;
}
case ALTServerConnectionErrorSSL:
{
break;
}
case ALTServerConnectionErrorTimedOut:
{
break;
}
}
return nil;
}
@end

View File

@ -0,0 +1 @@
../NSError+ALTServerError.h

View File

@ -0,0 +1,4 @@
module CAltKit {
umbrella "./include"
export *
}

View File

@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
0714E7142983A5AC00E6B45B /* libMoltenVK.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 0714E7132983A5AC00E6B45B /* libMoltenVK.dylib */; };
0714E7152983A5E500E6B45B /* libMoltenVK.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 0714E7132983A5AC00E6B45B /* libMoltenVK.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
0789FC302A07847E00D042B7 /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = 0789FC2F2A07847E00D042B7 /* AltKit */; };
07B7872D29E8FE8F0088B74F /* filters in Resources */ = {isa = PBXBuildFile; fileRef = 07B7872C29E8FE8F0088B74F /* filters */; };
9204BE0D1D319EF300BD49DB /* griffin_objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 50521A431AA23BF500185CC9 /* griffin_objc.m */; };
9204BE101D319EF300BD49DB /* griffin.c in Sources */ = {isa = PBXBuildFile; fileRef = 501232C9192E5FC40063A359 /* griffin.c */; };
@ -157,6 +158,7 @@
/* Begin PBXFileReference section */
0714E7132983A5AC00E6B45B /* libMoltenVK.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libMoltenVK.dylib; path = tvOS/modules/libMoltenVK.dylib; sourceTree = "<group>"; };
0789FC2E2A07845300D042B7 /* AltKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AltKit; path = Frameworks/AltKit; sourceTree = "<group>"; };
07B7872429E8D13F0088B74F /* .empty */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .empty; sourceTree = "<group>"; };
07B7872C29E8FE8F0088B74F /* filters */ = {isa = PBXFileReference; lastKnownFileType = folder; path = filters; sourceTree = "<group>"; };
501232C9192E5FC40063A359 /* griffin.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = griffin.c; path = ../../griffin/griffin.c; sourceTree = SOURCE_ROOT; };
@ -466,6 +468,7 @@
9204BE1B1D319EF300BD49DB /* CoreAudio.framework in Frameworks */,
9204BE1C1D319EF300BD49DB /* UIKit.framework in Frameworks */,
9204BE1D1D319EF300BD49DB /* Foundation.framework in Frameworks */,
0789FC302A07847E00D042B7 /* AltKit in Frameworks */,
9204BE1E1D319EF300BD49DB /* CoreGraphics.framework in Frameworks */,
9204BE1F1D319EF300BD49DB /* GLKit.framework in Frameworks */,
9204BE201D319EF300BD49DB /* OpenGLES.framework in Frameworks */,
@ -494,6 +497,14 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0789FC2D2A07845300D042B7 /* Packages */ = {
isa = PBXGroup;
children = (
0789FC2E2A07845300D042B7 /* AltKit */,
);
name = Packages;
sourceTree = "<group>";
};
07B7872329E8D13F0088B74F /* filters */ = {
isa = PBXGroup;
children = (
@ -1142,6 +1153,7 @@
96AFAE1A16C1D4EA009DE44C = {
isa = PBXGroup;
children = (
0789FC2D2A07845300D042B7 /* Packages */,
96AFAE9C16C1D976009DE44C /* Main Entry Core */,
92B9EAE024E04F8800E6CFB2 /* Sources */,
9222F2082315DAD50097C0FD /* Launch Screen.storyboard */,
@ -1252,6 +1264,7 @@
);
name = RetroArchiOS;
packageProductDependencies = (
0789FC2F2A07847E00D042B7 /* AltKit */,
);
productName = RetroArch;
productReference = 9204BE2B1D319EF300BD49DB /* RetroArch.app */;
@ -1569,6 +1582,7 @@
"-DGLES_SILENCE_DEPRECATION",
"-DGLSLANG_OSINCLUDE_UNIX",
"-DHAVE_7ZIP",
"-DHAVE_ALTKIT",
"-DHAVE_AUDIOMIXER",
"-DHAVE_BTSTACK",
"-DHAVE_BUILTINGLSLANG",
@ -1710,6 +1724,7 @@
"-DGLES_SILENCE_DEPRECATION",
"-DGLSLANG_OSINCLUDE_UNIX",
"-DHAVE_7ZIP",
"-DHAVE_ALTKIT",
"-DHAVE_AUDIOMIXER",
"-DHAVE_BTSTACK",
"-DHAVE_BUILTINGLSLANG",
@ -2475,6 +2490,13 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
0789FC2F2A07847E00D042B7 /* AltKit */ = {
isa = XCSwiftPackageProductDependency;
productName = AltKit;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 96AFAE1C16C1D4EA009DE44C /* Project object */;
}

View File

@ -1,14 +0,0 @@
{
"pins" : [
{
"identity" : "altkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/rileytestut/AltKit.git",
"state" : {
"revision" : "f799f60ef6fa8b9676b4102b7dfa169fb40b6c92",
"version" : "0.0.2"
}
}
],
"version" : 2
}