-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsimcopy
More file actions
executable file
·199 lines (164 loc) · 6.69 KB
/
Copy pathsimcopy
File metadata and controls
executable file
·199 lines (164 loc) · 6.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#!/usr/bin/swift
import Foundation
func main(arguments: [String]) throws {
guard arguments.count >= 2 else {
printHelp()
return
}
switch arguments[1] {
case "copyhome":
guard arguments.count >= 4 else { throw SimulatorError.notEnoughParameters }
guard let sourceUDID = UUID(uuidString: arguments[2]) else { throw SimulatorError.sourceIsNotAnUDID }
guard let targetUDID = UUID(uuidString: arguments[3]) else { throw SimulatorError.targetIsNotanUDID }
try copyHomeData(from: sourceUDID, to: targetUDID)
case "spreadhome":
guard arguments.count >= 3 else { throw SimulatorError.notEnoughParameters }
let deviceName = arguments.suffix(from: 2).joined(separator: " ")
try spread(from: deviceName)
default:
printHelp()
}
}
//MARK: Commands
func printHelp() {
print("""
Use this tool to copy the HomeKit Configurations between iOS simulators.
simcopy help
Prints this help.
simcopy copyhome <sourceUDID> <targetUDID>
Copies the HomeKit configuration and KeyChain from one simulator to the other.
simcopy spreadhome <devicename>
Copies the HomeKit configuration from the simulator with the specified name to all simulators with the same runtime (OS version).
""")
}
func copyHomeData(from sourceUDID: UUID, to targetUDID: UUID) throws {
let manager = FileManager()
for path in relativeHomePaths() {
let source = URL(fileURLWithPath: path, isDirectory: true, relativeTo: simulatorURL(udid: sourceUDID))
let target = URL(fileURLWithPath: path, isDirectory: true, relativeTo: simulatorURL(udid: targetUDID))
print("Copying \(source.absoluteString) to \(target.absoluteString)")
try manager.removeItem(at: target)
try manager.copyItem(at: source, to: target)
}
}
func spread(from deviceName: String) throws {
let simulatorControl = try getSimulatorControl()
for (runtimeIdentifier, devices) in simulatorControl.devices {
let runtime = simulatorControl.runtimes.first(where: {$0.identifier == runtimeIdentifier})
let sources = devices.filter{ $0.name == deviceName }
if sources.count > 1 { throw SimulatorError.multipleDevicesFound }
if let source = sources.first {
//Find out if the source exists. The existence API of Filemanager in Swift is awkward, but writing an extension for a "script" is also not ideal. So If you have an idea how to make it more elegant in-line, feel free to do so.
let manager = FileManager()
for path in relativeHomePaths() {
let url = URL(fileURLWithPath: path, isDirectory: true, relativeTo: simulatorURL(udid: source.udid))
var isDirectory: ObjCBool = false
let exists = manager.fileExists(atPath: url.path, isDirectory: &isDirectory)
if !exists || !isDirectory.boolValue {
print("Directory \(url.absoluteString) does not exist or is not a directory, so skipping spreading home data from \(source.name) to \(runtime?.name ?? runtimeIdentifier) simulators.")
return
}
}
let targets = devices.filter{ $0 != source }
for target in targets {
print("Copying Home Setup from \(source.name) to \(target.name) (\(runtime?.name ?? runtimeIdentifier))")
try copyHomeData(from: source.udid, to: target.udid)
}
}
}
}
//MARK: Helpers
/// Generates a `SimulatorControl` object that represents the output of `xcrun simctl list -j`. To understand the structure, have a look at that command.
func getSimulatorControl() throws -> SimulatorControl {
let jsonData = shell("xcrun simctl list -j")
return try JSONDecoder().decode(SimulatorControl.self, from: jsonData)
}
func shell(_ command: String) -> Data {
let task = Process()
task.launchPath = "/bin/bash"
task.arguments = ["-c", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
return data
}
func simulatorURL(udid: UUID) -> URL {
let userHome = FileManager().homeDirectoryForCurrentUser
return URL(fileURLWithPath: "Library/Developer/CoreSimulator/Devices/" + udid.uuidString, isDirectory: true, relativeTo: userHome)
}
/// The Paths inside the simulator directory that need to be copied for home data
// - Note: if we make that a 'let' the functions cann't access it from a script. That seems to different from a playground.
func relativeHomePaths() -> [String] {
return ["data/Library/homed", "data/Library/Keychains"]
}
//MARK: JSON Data Structure
struct SimulatorControl: Decodable {
let devicetypes: [Devicetype]
let runtimes: [Runtime]
let devices: [String: [Device]]
let pairs: [String: Pair]
}
struct Runtime: Decodable {
let identifier: String
let availabilityError: String
let buildversion: String
let availability: String
let isAvailable: Bool
let version: String
let name: String
}
struct Device: Decodable, Equatable {
let availability: String
let state: String //Should be an enum at some point
let isAvailable: Bool
let name: String
let udid: UUID
let availabilityError: String
}
struct Pair: Decodable {
let watch: PairedDevice
let phone: PairedDevice
let state: String
}
struct PairedDevice: Decodable {
let name: String
let udid: UUID
let state: String
}
struct Devicetype: Decodable {
let name: String
let bundlePath: String
let identifier: String
}
//MARK: Error Handling
enum SimulatorError: LocalizedError {
//This is super lazy: There should be localized descriptions for LocalizedError, attached data, etc. Feel free to add it.
case runtimeNotFound, multipleRuntimesFound, deviceNotFound, multipleDevicesFound, noDevicesForRuntime
case notEnoughParameters, sourceIsNotAnUDID, targetIsNotanUDID
}
func handleError(_ error: Error) {
//This could be WAY more sophisticated, but it parses the JSON as of now, so 🤷♂️
if let error = error as? DecodingError {
switch error {
case .typeMismatch(_, _):
print("Type Mismatch")
case .valueNotFound(_, _):
print("Value Not Found")
case .keyNotFound(let key, _):
print("Key Not Found: \(key)")
case .dataCorrupted(_):
print("Data Corruptedh")
@unknown default:
print("Unknown Decoding Error: \(error.localizedDescription)")
}
} else {
print("An Error occurred: \(error.localizedDescription)")
}
}
//MARK: Actually run something
do {
try main(arguments: CommandLine.arguments)
} catch {
handleError(error)
}