mixbox

Форк
0
/
PackageGenerator.swift 
334 строки · 9.8 Кб
1
// swiftlint:disable all
2

3
import Foundation
4

5
let knownImportsToIgnore = [
6
    "Foundation",
7
    "XCTest",
8
    "CryptoKit",
9
    "Dispatch",
10
]
11

12
// .product(name: "ArgumentParser", package: "swift-argument-parser")
13
let explicitlyIdentifiedPackages = [
14
    "SourceKittenFramework": "SourceKitten",
15
    "SourceryFramework": "Sourcery",
16
    "SourceryRuntime": "Sourcery"
17
]
18

19
let importStatementExpression = try NSRegularExpression(
20
    pattern: "^(@testable )?import ([a-zA-Z0-9_]+)$",
21
    options: [.anchorsMatchLines]
22
)
23

24
let moduleDescriptions: [ModuleDescription] = [
25
    try generate(
26
        moduleName: "MixboxMocksGeneration",
27
        path: "Frameworks/MocksGeneration/Sources",
28
        isTestTarget: false
29
    ),
30
    try generate(
31
        moduleName: "MixboxMocksGenerator",
32
        path: "MocksGenerator/Sources",
33
        isTestTarget: false
34
    ),
35
    try generate(
36
        moduleName: "MixboxDi",
37
        path: "Frameworks/Di/Sources",
38
        isTestTarget: false,
39
        hasConditionalCompilation: true
40
    ),
41
    try generate(
42
        moduleName: "MixboxBuiltinDi",
43
        path: "Frameworks/BuiltinDi/Sources",
44
        isTestTarget: false,
45
        hasConditionalCompilation: true
46
    ),
47
]
48

49
func main() throws {
50
    var generatedTargetStatements = [String]()
51
    let sortedModuleDescriptions: [ModuleDescription] = moduleDescriptions.sorted { $0.name < $1.name }
52
    
53
    for moduleDescription in sortedModuleDescriptions {
54
        let targetType = moduleDescription.isTest ? "testTarget" : "target"
55
        
56
        var args = [String]()
57
        
58
        args.append(
59
            """
60
            name: "\(moduleDescription.name)"
61
            """
62
        )
63
        
64
        var dependencies = [String]()
65
        
66
        for dependency in moduleDescription.deps {
67
            if explicitlyIdentifiedPackages.keys.contains(dependency) {
68
                let package = explicitlyIdentifiedPackages[dependency]!
69
                dependencies.append(
70
                    """
71
                    .product(name: "\(dependency)", package: "\(package)")
72
                    """
73
                )
74
            } else {
75
                dependencies.append(
76
                    """
77
                    "\(dependency)"
78
                    """
79
                )
80
            }
81
        }
82
        
83
        args.append(
84
            """
85
            dependencies: [
86
                \(dependencies.joined(separator: ",\n").indent())
87
            ]
88
            """
89
        )
90
        
91
        args.append(
92
            """
93
            path: "\(moduleDescription.path)"
94
            """
95
        )
96
        
97
        if !moduleDescription.defines.isEmpty {
98
            args.append(
99
                """
100
                swiftSettings: [
101
                    \(moduleDescription.defines.map { ".define(\"\($0)\")" }.joined(separator: ",\n").indent())
102
                ]
103
                """
104
            )
105
        }
106
        
107
        generatedTargetStatements.append(
108
            """
109
            .\(targetType)(
110
                // MARK: \(moduleDescription.name)
111
                \(args.joined(separator: ",\n").indent())
112
            )
113
            """
114
        )
115
    }
116
    
117
    try generatePackageSwift(replacementForTargets: generatedTargetStatements)
118
}
119

120
func generate(moduleName: String, path: String, isTestTarget: Bool, hasConditionalCompilation: Bool = false) throws -> ModuleDescription {
121
    let moduleFolderUrl = repoRoot().appendingPathComponent(path)
122
    
123
    guard directoryExists(url: moduleFolderUrl) else {
124
        throw ErrorString("Directory doesn't exist at \(moduleFolderUrl)")
125
    }
126
    
127
    let moduleEnumerator = FileManager().enumerator(
128
        at: moduleFolderUrl,
129
        includingPropertiesForKeys: [.isRegularFileKey],
130
        options: [.skipsHiddenFiles]
131
    )
132
    
133
    log("Analyzing \(moduleName) at \(moduleFolderUrl)")
134
    
135
    var importedModuleNames = Set<String>()
136
    
137
    while let moduleFile = moduleEnumerator?.nextObject() as? URL {
138
        if moduleFile.pathExtension != "swift" {
139
            log("    Skipping \(moduleFile.lastPathComponent): is not Swift file")
140
            continue
141
        }
142
        
143
        log("    Analyzing \(moduleFile.lastPathComponent)")
144
        
145
        guard try moduleFile.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile == true else {
146
            log("    Skipping \(moduleFile.lastPathComponent): is not regular file")
147
            continue
148
        }
149
        
150
        let fileContents = try String(contentsOf: moduleFile)
151
            .split(separator: "\n")
152
            .filter { !$0.starts(with: "//") }
153

154
        for line in fileContents {
155
            let matches = importStatementExpression.matches(in: String(line), options: [], range: NSMakeRange(0, line.count))
156
            
157
            guard matches.count == 1 else {
158
                continue
159
            }
160
            
161
            let importedModuleName = (line as NSString).substring(with: matches[0].range(at: 2))
162
            importedModuleNames.insert(importedModuleName)
163
        }
164
    }
165

166
    importedModuleNames.remove(moduleName)
167
    
168
    let dependencies = importedModuleNames.filter { !knownImportsToIgnore.contains($0) }.sorted()
169
    
170
    return ModuleDescription(
171
        name: moduleName,
172
        deps: dependencies, 
173
        path: String(path),
174
        isTest: isTestTarget,
175
        defines: hasConditionalCompilation ? ["MIXBOX_ENABLE_ALL_FRAMEWORKS"] : []
176
    )
177
}
178

179
func generatePackageSwift(replacementForTargets: [String]) throws {
180
    log("Loading template")
181
    var templateContents = try String(contentsOf: URL(fileURLWithPath: "Package.template.swift"))
182
    
183
    templateContents = templateContents.replacingOccurrences(
184
        of: "<__TARGETS__>",
185
        with: replacementForTargets.map { $0.indent(level: 2, includingFirstLine: true) }.joined(separator: ",\n")
186
    )
187
    
188
    templateContents = templateContents.replacingOccurrences(
189
        of: "<__SOURCERY_PACKAGE__>",
190
        with: sourceryPackage()
191
    )
192
    
193
    if ProcessInfo.processInfo.environment["MIXBOX_CI_IS_CI_BUILD"] != nil {
194
        log("Checking for Package.swift consistency")
195
        let existingContents = try String(contentsOf: URL(fileURLWithPath: "Package.swift"))
196
        if existingContents != templateContents {
197
            print("\(#file):\(#line): MIXBOX_CI_IS_CI_BUILD is set, and Package.swift differs. Please update and commit Package.swift!")
198
            exit(1)
199
        }
200
    }
201
    
202
    log("Saving Package.swift")
203
    try templateContents.write(to: URL(fileURLWithPath: "Package.swift"), atomically: true, encoding: .utf8)
204
}
205

206
// MARK: - Development dependencies support: Sourcery
207

208
func sourceryPackage() -> String {
209
    do {
210
        if ProcessInfo.processInfo.environment["SYNC_WITH_DEV_PODS"] != "true" {
211
            throw ErrorString("Development pods are disabled")
212
        }
213
        
214
        return """
215
        .package(name: "Sourcery", path: "\(try sourceryDevelopmentPath())")
216
        """
217
    } catch {
218
        return """
219
        .package(url: "https://github.com/avito-tech/Sourcery.git", .revision("0564feccdc8fade6c68376bdf7f8dab9b79863fe")),
220
        """
221
    }
222
}
223

224
func sourceryDevelopmentPath() throws -> String {
225
    return try developmentPodPath(
226
        podName: "Sourcery",
227
        podfileLockContents: mixboxPodfileLockContents()
228
    )
229
}
230

231
func repoRoot() -> URL {
232
    return URL(fileURLWithPath: #file, isDirectory: false)
233
        .deletingLastPathComponent()
234
}
235

236
func mixboxPodfileLockContents() throws -> String {
237
    
238
    let podfileLockPath = repoRoot()
239
        .appendingPathComponent("Tests")
240
        .appendingPathComponent("Podfile.lock")
241

242
    return try String(contentsOf: podfileLockPath)
243
}
244

245
// MARK: - Development dependencies support: Shared
246

247
func developmentPodPath(podName: String, podfileLockContents: String) throws -> String {
248
    return fixPathForSpm(
249
        path: try singleMatch(
250
            regex: "\(podName) \\(from `(.*?)`\\)",
251
            string: podfileLockContents,
252
            groupIndex: 1
253
        )
254
    )
255
}
256

257
// Without slash SPM fails with this error:
258
// error: the Package.resolved file is most likely severely out-of-date and is preventing correct resolution; delete the resolved file and try again
259
func fixPathForSpm(path: String) -> String {
260
    if path.hasSuffix("/") {
261
        return path
262
    } else {
263
        return "\(path)/"
264
    }
265
}
266

267
func singleMatch(regex: String, string: String, groupIndex: Int) throws -> String {
268
    let regex = try NSRegularExpression(pattern: regex)
269
    let results = regex.matches(
270
        in: string,
271
        range: NSRange(string.startIndex..., in: string)
272
    )
273
    
274
    guard let first = results.first, results.count == 1 else {
275
        throw ErrorString("Expected exactly one match") 
276
    }
277
    
278
    guard let range = Range(first.range(at: groupIndex), in: string) else {
279
        throw ErrorString("Failed to get range from match")
280
    }
281
    
282
    return String(string[range])
283
}
284

285
// MARK: - Models
286

287
struct ErrorString: Error, CustomStringConvertible {
288
    let description: String
289
    
290
    init(_ description: String) {
291
        self.description = description
292
    }
293
}
294

295
struct ModuleDescription {
296
    let name: String
297
    let deps: [String]
298
    let path: String
299
    let isTest: Bool
300
    let defines: [String]
301
}
302

303
// MARK: - Utility
304

305
func log(_ text: String) {
306
    if ProcessInfo.processInfo.environment["DEBUG"] != nil {
307
        print(text)
308
    }
309
}
310

311
func directoryExists(url: URL) -> Bool {
312
    var isDirectory = ObjCBool(false)
313
    return FileManager().fileExists(atPath: url.path, isDirectory: &isDirectory) && isDirectory.boolValue
314
}
315

316
extension String {
317
    private static let newLine = "\n"
318
    
319
    func indent(level: Int = 1, includingFirstLine: Bool = false) -> String {
320
        let indentation = String(repeating: " ", count: level * 4)
321
        
322
        return self
323
            .components(separatedBy: String.newLine)
324
            .enumerated()
325
            .map { index, line in (index == 0 && !includingFirstLine) ? line : indentation + line }
326
            .joined(separator: String.newLine)
327
    }
328
}
329

330
// MARK: - Main
331

332
try main()
333

334
// swiftlint:enable all
335

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.