mixbox
/
PackageGenerator.swift
334 строки · 9.8 Кб
1// swiftlint:disable all
2
3import Foundation
4
5let knownImportsToIgnore = [
6"Foundation",
7"XCTest",
8"CryptoKit",
9"Dispatch",
10]
11
12// .product(name: "ArgumentParser", package: "swift-argument-parser")
13let explicitlyIdentifiedPackages = [
14"SourceKittenFramework": "SourceKitten",
15"SourceryFramework": "Sourcery",
16"SourceryRuntime": "Sourcery"
17]
18
19let importStatementExpression = try NSRegularExpression(
20pattern: "^(@testable )?import ([a-zA-Z0-9_]+)$",
21options: [.anchorsMatchLines]
22)
23
24let moduleDescriptions: [ModuleDescription] = [
25try generate(
26moduleName: "MixboxMocksGeneration",
27path: "Frameworks/MocksGeneration/Sources",
28isTestTarget: false
29),
30try generate(
31moduleName: "MixboxMocksGenerator",
32path: "MocksGenerator/Sources",
33isTestTarget: false
34),
35try generate(
36moduleName: "MixboxDi",
37path: "Frameworks/Di/Sources",
38isTestTarget: false,
39hasConditionalCompilation: true
40),
41try generate(
42moduleName: "MixboxBuiltinDi",
43path: "Frameworks/BuiltinDi/Sources",
44isTestTarget: false,
45hasConditionalCompilation: true
46),
47]
48
49func main() throws {
50var generatedTargetStatements = [String]()
51let sortedModuleDescriptions: [ModuleDescription] = moduleDescriptions.sorted { $0.name < $1.name }
52
53for moduleDescription in sortedModuleDescriptions {
54let targetType = moduleDescription.isTest ? "testTarget" : "target"
55
56var args = [String]()
57
58args.append(
59"""
60name: "\(moduleDescription.name)"
61"""
62)
63
64var dependencies = [String]()
65
66for dependency in moduleDescription.deps {
67if explicitlyIdentifiedPackages.keys.contains(dependency) {
68let package = explicitlyIdentifiedPackages[dependency]!
69dependencies.append(
70"""
71.product(name: "\(dependency)", package: "\(package)")
72"""
73)
74} else {
75dependencies.append(
76"""
77"\(dependency)"
78"""
79)
80}
81}
82
83args.append(
84"""
85dependencies: [
86\(dependencies.joined(separator: ",\n").indent())
87]
88"""
89)
90
91args.append(
92"""
93path: "\(moduleDescription.path)"
94"""
95)
96
97if !moduleDescription.defines.isEmpty {
98args.append(
99"""
100swiftSettings: [
101\(moduleDescription.defines.map { ".define(\"\($0)\")" }.joined(separator: ",\n").indent())
102]
103"""
104)
105}
106
107generatedTargetStatements.append(
108"""
109.\(targetType)(
110// MARK: \(moduleDescription.name)
111\(args.joined(separator: ",\n").indent())
112)
113"""
114)
115}
116
117try generatePackageSwift(replacementForTargets: generatedTargetStatements)
118}
119
120func generate(moduleName: String, path: String, isTestTarget: Bool, hasConditionalCompilation: Bool = false) throws -> ModuleDescription {
121let moduleFolderUrl = repoRoot().appendingPathComponent(path)
122
123guard directoryExists(url: moduleFolderUrl) else {
124throw ErrorString("Directory doesn't exist at \(moduleFolderUrl)")
125}
126
127let moduleEnumerator = FileManager().enumerator(
128at: moduleFolderUrl,
129includingPropertiesForKeys: [.isRegularFileKey],
130options: [.skipsHiddenFiles]
131)
132
133log("Analyzing \(moduleName) at \(moduleFolderUrl)")
134
135var importedModuleNames = Set<String>()
136
137while let moduleFile = moduleEnumerator?.nextObject() as? URL {
138if moduleFile.pathExtension != "swift" {
139log(" Skipping \(moduleFile.lastPathComponent): is not Swift file")
140continue
141}
142
143log(" Analyzing \(moduleFile.lastPathComponent)")
144
145guard try moduleFile.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile == true else {
146log(" Skipping \(moduleFile.lastPathComponent): is not regular file")
147continue
148}
149
150let fileContents = try String(contentsOf: moduleFile)
151.split(separator: "\n")
152.filter { !$0.starts(with: "//") }
153
154for line in fileContents {
155let matches = importStatementExpression.matches(in: String(line), options: [], range: NSMakeRange(0, line.count))
156
157guard matches.count == 1 else {
158continue
159}
160
161let importedModuleName = (line as NSString).substring(with: matches[0].range(at: 2))
162importedModuleNames.insert(importedModuleName)
163}
164}
165
166importedModuleNames.remove(moduleName)
167
168let dependencies = importedModuleNames.filter { !knownImportsToIgnore.contains($0) }.sorted()
169
170return ModuleDescription(
171name: moduleName,
172deps: dependencies,
173path: String(path),
174isTest: isTestTarget,
175defines: hasConditionalCompilation ? ["MIXBOX_ENABLE_ALL_FRAMEWORKS"] : []
176)
177}
178
179func generatePackageSwift(replacementForTargets: [String]) throws {
180log("Loading template")
181var templateContents = try String(contentsOf: URL(fileURLWithPath: "Package.template.swift"))
182
183templateContents = templateContents.replacingOccurrences(
184of: "<__TARGETS__>",
185with: replacementForTargets.map { $0.indent(level: 2, includingFirstLine: true) }.joined(separator: ",\n")
186)
187
188templateContents = templateContents.replacingOccurrences(
189of: "<__SOURCERY_PACKAGE__>",
190with: sourceryPackage()
191)
192
193if ProcessInfo.processInfo.environment["MIXBOX_CI_IS_CI_BUILD"] != nil {
194log("Checking for Package.swift consistency")
195let existingContents = try String(contentsOf: URL(fileURLWithPath: "Package.swift"))
196if existingContents != templateContents {
197print("\(#file):\(#line): MIXBOX_CI_IS_CI_BUILD is set, and Package.swift differs. Please update and commit Package.swift!")
198exit(1)
199}
200}
201
202log("Saving Package.swift")
203try templateContents.write(to: URL(fileURLWithPath: "Package.swift"), atomically: true, encoding: .utf8)
204}
205
206// MARK: - Development dependencies support: Sourcery
207
208func sourceryPackage() -> String {
209do {
210if ProcessInfo.processInfo.environment["SYNC_WITH_DEV_PODS"] != "true" {
211throw ErrorString("Development pods are disabled")
212}
213
214return """
215.package(name: "Sourcery", path: "\(try sourceryDevelopmentPath())")
216"""
217} catch {
218return """
219.package(url: "https://github.com/avito-tech/Sourcery.git", .revision("0564feccdc8fade6c68376bdf7f8dab9b79863fe")),
220"""
221}
222}
223
224func sourceryDevelopmentPath() throws -> String {
225return try developmentPodPath(
226podName: "Sourcery",
227podfileLockContents: mixboxPodfileLockContents()
228)
229}
230
231func repoRoot() -> URL {
232return URL(fileURLWithPath: #file, isDirectory: false)
233.deletingLastPathComponent()
234}
235
236func mixboxPodfileLockContents() throws -> String {
237
238let podfileLockPath = repoRoot()
239.appendingPathComponent("Tests")
240.appendingPathComponent("Podfile.lock")
241
242return try String(contentsOf: podfileLockPath)
243}
244
245// MARK: - Development dependencies support: Shared
246
247func developmentPodPath(podName: String, podfileLockContents: String) throws -> String {
248return fixPathForSpm(
249path: try singleMatch(
250regex: "\(podName) \\(from `(.*?)`\\)",
251string: podfileLockContents,
252groupIndex: 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
259func fixPathForSpm(path: String) -> String {
260if path.hasSuffix("/") {
261return path
262} else {
263return "\(path)/"
264}
265}
266
267func singleMatch(regex: String, string: String, groupIndex: Int) throws -> String {
268let regex = try NSRegularExpression(pattern: regex)
269let results = regex.matches(
270in: string,
271range: NSRange(string.startIndex..., in: string)
272)
273
274guard let first = results.first, results.count == 1 else {
275throw ErrorString("Expected exactly one match")
276}
277
278guard let range = Range(first.range(at: groupIndex), in: string) else {
279throw ErrorString("Failed to get range from match")
280}
281
282return String(string[range])
283}
284
285// MARK: - Models
286
287struct ErrorString: Error, CustomStringConvertible {
288let description: String
289
290init(_ description: String) {
291self.description = description
292}
293}
294
295struct ModuleDescription {
296let name: String
297let deps: [String]
298let path: String
299let isTest: Bool
300let defines: [String]
301}
302
303// MARK: - Utility
304
305func log(_ text: String) {
306if ProcessInfo.processInfo.environment["DEBUG"] != nil {
307print(text)
308}
309}
310
311func directoryExists(url: URL) -> Bool {
312var isDirectory = ObjCBool(false)
313return FileManager().fileExists(atPath: url.path, isDirectory: &isDirectory) && isDirectory.boolValue
314}
315
316extension String {
317private static let newLine = "\n"
318
319func indent(level: Int = 1, includingFirstLine: Bool = false) -> String {
320let indentation = String(repeating: " ", count: level * 4)
321
322return 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
332try main()
333
334// swiftlint:enable all
335