From f9be03ebc858a48f75c39c73a4ecf05e2fde9b32 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sat, 3 Nov 2018 22:21:52 +1100 Subject: [PATCH] rename to cache and refactor --- README.md | 1 + Sources/XcodeGen/main.swift | 97 +++++----- Sources/XcodeGenKit/CacheFile.swift | 33 ++++ Sources/XcodeGenKit/MD5.swift | 277 +++++++++++++++++++++++++++ Sources/XcodeGenKit/SpecLoader.swift | 40 ++++ 5 files changed, 395 insertions(+), 53 deletions(-) create mode 100644 Sources/XcodeGenKit/CacheFile.swift create mode 100644 Sources/XcodeGenKit/MD5.swift create mode 100644 Sources/XcodeGenKit/SpecLoader.swift diff --git a/README.md b/README.md index cea216b2..7eda7c59 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ Use `xcodegen --help` to see the list of options: - **--spec**: An optional path to a `.yml` or `.json` project spec. - **--project**: An optional path to a directory where the project will be generated. By default this is the directory the spec lives in. - **--quiet**: Suppress informational and success messages. +- **--use-cache**: Used to prevent unnecessarily generating the project. If this is set, then a cache file will be written to when a project is generated. If `xcodegen` is later run but the spec and all the files it contains are the same, the project won't be generated. ## Editing ```shell diff --git a/Sources/XcodeGen/main.swift b/Sources/XcodeGen/main.swift index 86399942..3b0d0299 100644 --- a/Sources/XcodeGen/main.swift +++ b/Sources/XcodeGen/main.swift @@ -9,7 +9,7 @@ import Yams let version = try Version("2.0.0") -func generate(spec: String, project: String, lockfile: String, isQuiet: Bool, justVersion: Bool) { +func generate(spec: String, project: String, useCache: Bool, isQuiet: Bool, justVersion: Bool) { if justVersion { print(version) exit(EXIT_SUCCESS) @@ -29,87 +29,78 @@ func generate(spec: String, project: String, lockfile: String, isQuiet: Bool, ju fatalError("No project spec found at \(projectSpecPath.absolute())") } - let projectDictionary: JSONDictionary + let specLoader = SpecLoader(version: version) let project: Project + + // load project spec do { - projectDictionary = try Project.loadDictionary(path: projectSpecPath) - project = try Project(basePath: projectSpecPath.parent(), jsonDictionary: projectDictionary) - logger.info("📋 Loaded project:\n \(project.debugDescription.replacingOccurrences(of: "\n", with: "\n "))") + project = try specLoader.loadProject(path: projectSpecPath) } catch let error as CustomStringConvertible { fatalError("Parsing project spec failed: \(error)") } catch { fatalError("Parsing project spec failed: \(error.localizedDescription)") } - // Lock file - var lockFileContent: String = "" - let lockFilePath = lockfile.isEmpty ? nil : Path(lockfile) - if #available(OSX 10.13, *), let lockFilePath = lockFilePath { - // JSONSerialization.WritingOptions.sortedKeys is only available on 10.13 + let cacheFilePath = Path("~/.xcodegen/cache/\(projectSpecPath.absolute().string.md5)").absolute() + var cacheFile: CacheFile? - let files = Array(Set(project.allFiles)) - .map { $0.byRemovingBase(path: project.basePath).string } - .sorted { $0.localizedStandardCompare($1) == .orderedAscending } - .joined(separator: "\n") - - let spec: String + // read cache + if useCache { do { - let data = try JSONSerialization.data(withJSONObject: projectDictionary, options: [.sortedKeys, .prettyPrinted]) - spec = String(data: data, encoding: .utf8)! + cacheFile = try specLoader.generateCacheFile() } catch { - fatalError("Couldn't serialize spec for lockfile\n\(error)") - } - - lockFileContent = """ - # XCODEGEN VERSION - \(version) - - # SPEC - \(spec) - - # FILES - \(files)" - - """ - if lockFilePath.exists { - do { - let lockFile: String = try lockFilePath.read() - let oldFiles = lockFile - - if oldFiles == lockFileContent { - logger.info("✅ Not generating project as lockfile \(lockFilePath) has not changed") - return - } - } catch { - fatalError("Couldn't load \(lockFilePath)") - } + fatalError("Couldn't generate cache file: \(error.localizedDescription)") } } + // check cache + if let cacheFile = cacheFile, cacheFilePath.exists { + do { + let existingCacheFile: String = try cacheFilePath.read() + if cacheFile.string == existingCacheFile { + logger.success("Project has not changed since cache was written") + return + } + } catch { + logger.error("Couldn't load cache at \(cacheFile)") + } + } + + logger.info("Loaded project:\n \(project.debugDescription.replacingOccurrences(of: "\n", with: "\n "))") + do { + // validation try project.validateMinimumXcodeGenVersion(version) try project.validate() + // generation logger.info("⚙️ Generating project...") let projectGenerator = ProjectGenerator(project: project) let xcodeProject = try projectGenerator.generateXcodeProject() + // file writing logger.info("⚙️ Writing project...") let fileWriter = FileWriter(project: project) projectPath = projectPath + "\(project.name).xcodeproj" try fileWriter.writeXcodeProject(xcodeProject, to: projectPath) try fileWriter.writePlists() - if let lockFilePath = lockFilePath { - try lockFilePath.write(lockFileContent) - logger.success("💾 Wrote lockfile to \(lockFilePath)") - } logger.success("💾 Saved project to \(projectPath)") } catch let error as SpecValidationError { fatalError(error.description) } catch { fatalError("Generation failed: \(error.localizedDescription)") } + + // write cache + if let cacheFile = cacheFile { + do { + try cacheFilePath.parent().mkpath() + try cacheFilePath.write(cacheFile.string) + } catch { + logger.error("Failed to write cache: \(error.localizedDescription)") + } + } } command( @@ -125,11 +116,11 @@ command( flag: "p", description: "The path to the folder where the project should be generated" ), - Option( - "lockfile", - default: "", - flag: "l", - description: "The path to a lock file" + Flag( + "use-cache", + default: false, + flag: "c", + description: "Use a cache for the xcodegen spec" ), Flag( "quiet", diff --git a/Sources/XcodeGenKit/CacheFile.swift b/Sources/XcodeGenKit/CacheFile.swift new file mode 100644 index 00000000..3a7e6dd5 --- /dev/null +++ b/Sources/XcodeGenKit/CacheFile.swift @@ -0,0 +1,33 @@ +import Foundation +import ProjectSpec + + +public class CacheFile { + + public let string: String + + init?(version: Version, projectDictionary: [String: Any], project: Project) throws { + + guard #available(OSX 10.13, *) else { return nil } + + let files = Array(Set(project.allFiles)) + .map { $0.byRemovingBase(path: project.basePath).string } + .sorted { $0.localizedStandardCompare($1) == .orderedAscending } + .joined(separator: "\n") + + let data = try JSONSerialization.data(withJSONObject: projectDictionary, options: [.sortedKeys, .prettyPrinted]) + let spec = String(data: data, encoding: .utf8)! + + string = """ + # XCODEGEN VERSION + \(version) + + # SPEC + \(spec) + + # FILES + \(files)" + + """ + } +} diff --git a/Sources/XcodeGenKit/MD5.swift b/Sources/XcodeGenKit/MD5.swift new file mode 100644 index 00000000..963d52cc --- /dev/null +++ b/Sources/XcodeGenKit/MD5.swift @@ -0,0 +1,277 @@ +// To date, adding CommonCrypto to a Swift framework is problematic. See: +// http://stackoverflow.com/questions/25248598/importing-commoncrypto-in-a-swift-framework +// We're using a subset and modified version of CryptoSwift as an alternative. +// The following is an altered source version that only includes MD5. The original software can be found at: +// https://github.com/krzyzanowskim/CryptoSwift +// This is the original copyright notice: + +/* + Copyright (C) 2014 Marcin Krzyżanowski + This software is provided 'as-is', without any express or implied warranty. + In no event will the authors be held liable for any damages arising from the use of this software. + Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. + - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + - This notice may not be removed or altered from any source or binary distribution. + */ + +import Foundation + +extension String { + public var md5: String { + if let data = data(using: .utf8, allowLossyConversion: true) { + let message = data.withUnsafeBytes { bytes -> [UInt8] in + return Array(UnsafeBufferPointer(start: bytes, count: data.count)) + } + + let MD5Calculator = MD5(message) + let MD5Data = MD5Calculator.calculate() + + var MD5String = String() + for c in MD5Data { + MD5String += String(format: "%02x", c) + } + return MD5String + + } else { + return self + } + } +} + +/** array of bytes, little-endian representation */ +func arrayOfBytes(_ value: T, length: Int? = nil) -> [UInt8] { + let totalBytes = length ?? (MemoryLayout.size * 8) + + let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) + valuePointer.pointee = value + + let bytes = valuePointer.withMemoryRebound(to: UInt8.self, capacity: totalBytes) { (bytesPointer) -> [UInt8] in + var bytes = [UInt8](repeating: 0, count: totalBytes) + for j in 0 ..< min(MemoryLayout.size, totalBytes) { + bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee + } + return bytes + } + + #if swift(>=4.1) + valuePointer.deinitialize(count: 1) + valuePointer.deallocate() + #else + valuePointer.deinitialize() + valuePointer.deallocate(capacity: 1) + #endif + + return bytes +} + +extension Int { + /** Array of bytes with optional padding (little-endian) */ + func bytes(_ totalBytes: Int = MemoryLayout.size) -> [UInt8] { + return arrayOfBytes(self, length: totalBytes) + } +} + +extension NSMutableData { + /** Convenient way to append bytes */ + func appendBytes(_ arrayOfBytes: [UInt8]) { + append(arrayOfBytes, length: arrayOfBytes.count) + } +} + +protocol HashProtocol { + var message: Array { get } + + /** Common part for hash calculation. Prepare header data. */ + func prepare(_ len: Int) -> Array +} + +extension HashProtocol { + func prepare(_ len: Int) -> Array { + var tmpMessage = message + + // Step 1. Append Padding Bits + tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message + + // append "0" bit until message length in bits ≡ 448 (mod 512) + var msgLength = tmpMessage.count + var counter = 0 + + while msgLength % len != (len - 8) { + counter += 1 + msgLength += 1 + } + + tmpMessage += Array(repeating: 0, count: counter) + return tmpMessage + } +} + +func toUInt32Array(_ slice: ArraySlice) -> Array { + var result = Array() + result.reserveCapacity(16) + + for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout.size) { + let d0 = UInt32(slice[idx.advanced(by: 3)]) << 24 + let d1 = UInt32(slice[idx.advanced(by: 2)]) << 16 + let d2 = UInt32(slice[idx.advanced(by: 1)]) << 8 + let d3 = UInt32(slice[idx]) + let val: UInt32 = d0 | d1 | d2 | d3 + + result.append(val) + } + return result +} + +struct BytesIterator: IteratorProtocol { + let chunkSize: Int + let data: [UInt8] + + init(chunkSize: Int, data: [UInt8]) { + self.chunkSize = chunkSize + self.data = data + } + + var offset = 0 + + mutating func next() -> ArraySlice? { + let end = min(chunkSize, data.count - offset) + let result = data[offset ..< offset + end] + offset += result.count + return result.count > 0 ? result : nil + } +} + +struct BytesSequence: Sequence { + let chunkSize: Int + let data: [UInt8] + + func makeIterator() -> BytesIterator { + return BytesIterator(chunkSize: chunkSize, data: data) + } +} + +func rotateLeft(_ value: UInt32, bits: UInt32) -> UInt32 { + return ((value << bits) & 0xFFFF_FFFF) | (value >> (32 - bits)) +} + +class MD5: HashProtocol { + static let size = 16 // 128 / 8 + let message: [UInt8] + + init(_ message: [UInt8]) { + self.message = message + } + + /** specifies the per-round shift amounts */ + private let shifts: [UInt32] = [ + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, + ] + + /** binary integer part of the sines of integers (Radians) */ + private let sines: [UInt32] = [ + 0xD76A_A478, 0xE8C7_B756, 0x2420_70DB, 0xC1BD_CEEE, + 0xF57C_0FAF, 0x4787_C62A, 0xA830_4613, 0xFD46_9501, + 0x6980_98D8, 0x8B44_F7AF, 0xFFFF_5BB1, 0x895C_D7BE, + 0x6B90_1122, 0xFD98_7193, 0xA679_438E, 0x49B4_0821, + 0xF61E_2562, 0xC040_B340, 0x265E_5A51, 0xE9B6_C7AA, + 0xD62F_105D, 0x0244_1453, 0xD8A1_E681, 0xE7D3_FBC8, + 0x21E1_CDE6, 0xC337_07D6, 0xF4D5_0D87, 0x455A_14ED, + 0xA9E3_E905, 0xFCEF_A3F8, 0x676F_02D9, 0x8D2A_4C8A, + 0xFFFA_3942, 0x8771_F681, 0x6D9D_6122, 0xFDE5_380C, + 0xA4BE_EA44, 0x4BDE_CFA9, 0xF6BB_4B60, 0xBEBF_BC70, + 0x289B_7EC6, 0xEAA1_27FA, 0xD4EF_3085, 0x4881D05, + 0xD9D4_D039, 0xE6DB_99E5, 0x1FA2_7CF8, 0xC4AC_5665, + 0xF429_2244, 0x432A_FF97, 0xAB94_23A7, 0xFC93_A039, + 0x655B_59C3, 0x8F0C_CC92, 0xFFEF_F47D, 0x8584_5DD1, + 0x6FA8_7E4F, 0xFE2C_E6E0, 0xA301_4314, 0x4E08_11A1, + 0xF753_7E82, 0xBD3A_F235, 0x2AD7_D2BB, 0xEB86_D391, + ] + + private let hashes: [UInt32] = [0x6745_2301, 0xEFCD_AB89, 0x98BA_DCFE, 0x1032_5476] + + func calculate() -> [UInt8] { + var tmpMessage = prepare(64) + tmpMessage.reserveCapacity(tmpMessage.count + 4) + + // hash values + var hh = hashes + + // Step 2. Append Length a 64-bit representation of lengthInBits + let lengthInBits = (message.count * 8) + let lengthBytes = lengthInBits.bytes(64 / 8) + tmpMessage += lengthBytes.reversed() + + // Process the message in successive 512-bit chunks: + let chunkSizeBytes = 512 / 8 // 64 + + for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) { + // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15 + var M = toUInt32Array(chunk) + assert(M.count == 16, "Invalid array") + + // Initialize hash value for this chunk: + var A: UInt32 = hh[0] + var B: UInt32 = hh[1] + var C: UInt32 = hh[2] + var D: UInt32 = hh[3] + + var dTemp: UInt32 = 0 + + // Main loop + for j in 0 ..< sines.count { + var g = 0 + var F: UInt32 = 0 + + switch j { + case 0 ... 15: + F = (B & C) | ((~B) & D) + g = j + break + case 16 ... 31: + F = (D & B) | (~D & C) + g = (5 * j + 1) % 16 + break + case 32 ... 47: + F = B ^ C ^ D + g = (3 * j + 5) % 16 + break + case 48 ... 63: + F = C ^ (B | (~D)) + g = (7 * j) % 16 + break + default: + break + } + dTemp = D + D = C + C = B + B = B &+ rotateLeft((A &+ F &+ sines[j] &+ M[g]), bits: shifts[j]) + A = dTemp + } + + hh[0] = hh[0] &+ A + hh[1] = hh[1] &+ B + hh[2] = hh[2] &+ C + hh[3] = hh[3] &+ D + } + + var result = [UInt8]() + result.reserveCapacity(hh.count / 4) + + hh.forEach { + let itemLE = $0.littleEndian + let r1 = UInt8(itemLE & 0xFF) + let r2 = UInt8((itemLE >> 8) & 0xFF) + let r3 = UInt8((itemLE >> 16) & 0xFF) + let r4 = UInt8((itemLE >> 24) & 0xFF) + result += [r1, r2, r3, r4] + } + return result + } +} + +// swiftlint:enable all diff --git a/Sources/XcodeGenKit/SpecLoader.swift b/Sources/XcodeGenKit/SpecLoader.swift new file mode 100644 index 00000000..0ec58d60 --- /dev/null +++ b/Sources/XcodeGenKit/SpecLoader.swift @@ -0,0 +1,40 @@ +import Foundation +import JSONUtilities +import PathKit +import ProjectSpec +import xcodeproj +import Yams + +public class SpecLoader { + + var project: Project! + private var projectDictionary: [String: Any]? + let version: Version + let cacheFilePath: Path? + + public init(version: Version, cacheFilePath: Path? = nil) { + self.version = version + self.cacheFilePath = cacheFilePath + } + + public func loadProject(path: Path) throws -> Project { + let dictionary = try Project.loadDictionary(path: path) + let project = try Project(basePath: path.parent(), jsonDictionary: dictionary) + + self.project = project + self.projectDictionary = dictionary + + return project + } + + public func generateCacheFile() throws -> CacheFile? { + guard let projectDictionary = projectDictionary, + let project = project else { + return nil + } + return try CacheFile(version: version, + projectDictionary: projectDictionary, + project: project) + } + +}