forked from danger/swift
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathScript.swift
286 lines (231 loc) · 9.55 KB
/
Script.swift
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
import DangerShellExecutor
import Foundation
import Logger
public struct ScriptManager {
public struct Config {
let dependencyPrefix: String
let dependencyFile: String
let majorVersionPrefix: String
public init(prefix: String = "package: ",
file: String = "Dangerplugins",
major: String = "~> ") {
dependencyPrefix = prefix
dependencyFile = file
majorVersionPrefix = major
}
}
enum Errors: Error {
case failedToCreatePackageFile(String)
case invalidInlineDependencyURL(String)
case failedToAddDependencyScript(String)
case scriptNotFound(String)
}
private let config = Config()
private let packageManager: PackageManager
private let folder: String
private let cacheFolder: String
private let temporaryFolder: String
private let logger: Logger
private let inlineDependenciesFinder: InlineDependenciesFinder
public init(folder: String,
packageManager: PackageManager,
logger: Logger) throws {
self.folder = folder
self.logger = logger
cacheFolder = try folder.createSubfolderIfNeeded(withName: "Cache")
temporaryFolder = try folder.createSubfolderIfNeeded(withName: "Temp")
inlineDependenciesFinder = InlineDependenciesFinder(config: config)
self.packageManager = packageManager
}
public func script(atPath path: String) throws -> Script {
let path = path.asScriptPath()
if FileManager.default.fileExists(atPath: path) {
return try script(fromPath: path)
} else {
throw Errors.scriptNotFound(path)
}
}
private func script(fromPath path: String) throws -> Script {
let identifier = scriptIdentifier(fromPath: path)
let folder = try createFolderIfNeededForScript(withIdentifier: identifier, filePath: path)
let script = Script(name: path.nameExcludingExtension, folder: folder, logger: logger)
let packages = try inlineDependenciesFinder.resolveInlineDependencies(fromPath: path)
try packageManager.addPackagesIfNeeded(from: packages)
do {
try FileManager.default.createFile(atPath: folder.appendingPath("Package.swift"),
contents: packageManager.makePackageDescription(for: script),
attributes: [:])
} catch {
throw Errors.failedToCreatePackageFile(folder)
}
return script
}
private func scriptIdentifier(fromPath path: String) -> String {
let pathExcludingExtension = path.components(separatedBy: ".swift").first
return pathExcludingExtension?.replacingOccurrences(of: ":", with: "-")
.replacingOccurrences(of: "/", with: "-")
.replacingOccurrences(of: " ", with: "-") ?? "Dangerfile.swift"
}
private func createFolderIfNeededForScript(withIdentifier identifier: String, filePath: String) throws -> String {
let scriptFolder = try cacheFolder.createSubfolderIfNeeded(withName: identifier)
try packageManager.symlinkPackages(to: scriptFolder)
if !FileManager.default.fileExists(atPath: scriptFolder.appendingPath("OriginalFile")) {
try scriptFolder.createSymlink(to: filePath, at: "OriginalFile")
}
let sourcesFolder = try scriptFolder.createSubfolderIfNeeded(withName: "Sources")
try FileManager.default.removeItem(atPath: sourcesFolder)
let moduleFolder = try sourcesFolder.createSubfolder(withName: filePath.nameExcludingExtension)
FileManager.default.createFile(atPath: moduleFolder.appendingPath("main.swift"),
contents: Data(try String(contentsOfFile: filePath).utf8),
attributes: [:])
return scriptFolder
}
}
public final class Script {
enum Errors: Error {
case watchingFailed(String)
}
public let name: String
public let folder: String
private var copyLoopDispatchQueue: DispatchQueue?
private var localPath: String { "Sources/\(name)/main.swift" }
private var logger: Logger
init(name: String, folder: String, logger: Logger) {
self.name = name
self.folder = folder
self.logger = logger
}
public func build(withArguments arguments: [String] = []) throws {
let executor = ShellExecutor()
try executeSwiftCommand("build --package-path \(folder)", arguments: arguments, executor: executor)
}
@discardableResult
public func setupForEdit(importedFiles: [String], configPath: String) throws -> String {
try importedFiles.forEach {
if !FileManager.default.fileExists(atPath: $0) {
_ = FileManager.default.createFile(atPath: $0, contents: nil, attributes: nil)
}
try FileManager.default.copyItem(atPath: $0, toPath: sourcesImportPath(forImportPath: $0))
}
try generateXCodeProjWithConfig(configPath: configPath)
return editingPath()
}
private func editingPath() -> String {
folder.appendingPath(name + ".xcodeproj")
}
private func generateXCodeProjWithConfig(configPath: String) throws {
try executeSwiftCommand("package generate-xcodeproj --xcconfig-overrides \(configPath)",
onFolder: folder,
executor: ShellExecutor())
}
private func sourcesImportPath(forImportPath importPath: String) -> String {
folder
.appendingPath("Sources")
.appendingPath(name)
.appendingPath(importPath.fileName)
}
public func watch(importedFiles: [String]) throws {
let fullPathImports = importedFiles.map(\.fullPath)
try watch(imports: fullPathImports)
try? copyImports(fullPathImports)
}
public func watch(imports: [String]) throws {
do {
let path = editingPath()
try ShellExecutor().spawn("open \"\(path)\"", arguments: [])
logger.logInfo("\nℹ️ Danger will keep running, " +
"in order to commit any changes you make in Xcode back to the original script file")
logger.logInfo(" Press the return key once you're done")
startCopyLoop(imports: imports)
_ = FileHandle.standardInput.availableData
try copyChangesToSymlinkedFile()
} catch {
throw Errors.watchingFailed(name)
}
}
private func startCopyLoop(imports: [String]) {
let dispatchQueue: DispatchQueue
if let existingQueue = copyLoopDispatchQueue {
dispatchQueue = existingQueue
} else {
let newQueue = DispatchQueue(label: "com.danger.fileCopyLoop")
copyLoopDispatchQueue = newQueue
dispatchQueue = newQueue
}
dispatchQueue.asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in
try? self?.copyChangesToSymlinkedFile()
try? self?.copyImports(imports)
self?.startCopyLoop(imports: imports)
}
}
private func copyChangesToSymlinkedFile() throws {
let script = try expandSymlink()
let data = try Data(contentsOf: URL(fileURLWithPath: folder.appendingPath(localPath)))
try data.write(to: URL(fileURLWithPath: script))
}
private func expandSymlink() throws -> String {
try ShellExecutor().spawn("readlink \(folder.appendingPath("OriginalFile"))", arguments: [])
}
private func copyImports(_ imports: [String]) throws {
try imports.forEach { importPath in
try Data(contentsOf:
URL(fileURLWithPath: sourcesImportPath(forImportPath: importPath)))
.write(to: URL(fileURLWithPath: importPath))
}
}
}
@discardableResult
func executeSwiftCommand(_ command: String,
onFolder folder: String? = nil,
arguments: [String] = [],
executor: ShellExecutor) throws -> String {
func resolveSwiftPath() -> String {
#if os(Linux)
return "swift"
#else
return "/usr/bin/env xcrun --sdk macosx swift"
#endif
}
let swiftPath = resolveSwiftPath()
let command = folder.map { "cd \($0) && \(swiftPath) \(command)" } ?? "\(swiftPath) \(command)"
return try executor.spawn(command, arguments: arguments)
}
private extension String {
func asScriptPath() -> String {
var value = self
if !hasSuffix(".swift") {
value += ".swift"
}
if !hasPrefix("/") {
value = value.fullPath
}
return value
}
var fullPath: String {
if hasPrefix("/") {
return self
} else {
return FileManager.default.currentDirectoryPath.appendingPath(self)
}
}
var nameExcludingExtension: String {
guard let `extension` = `extension` else {
return fileName
}
let endIndex = fileName.index(fileName.endIndex, offsetBy: -`extension`.count - 1)
return String(fileName[..<endIndex])
}
var `extension`: String? {
let components = fileName.components(separatedBy: ".")
guard components.count > 1 else {
return nil
}
return components.last
}
var fileName: String {
components(separatedBy: "/").last ?? "Dangerfile.swift"
}
var folderPath: String {
components(separatedBy: "/").dropLast().joined(separator: "/")
}
}