From e30aeee957ef23424f4da1d9c16b48c35c8037d7 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sat, 3 Nov 2018 21:34:27 +1100 Subject: [PATCH 01/11] add lockfile --- Sources/ProjectSpec/Project.swift | 37 ++++++++++++++ Sources/ProjectSpec/SpecLoader.swift | 2 +- Sources/XcodeGen/main.swift | 61 +++++++++++++++++++++++- Sources/XcodeGenKit/PathExtensions.swift | 2 +- 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/Sources/ProjectSpec/Project.swift b/Sources/ProjectSpec/Project.swift index f5d1d351..ddeefd36 100644 --- a/Sources/ProjectSpec/Project.swift +++ b/Sources/ProjectSpec/Project.swift @@ -166,3 +166,40 @@ extension Project { return jsonDictionary } } + +extension Project { + + public var allFiles: [Path] { + var files: [Path] = [] + files.append(contentsOf: configFilePaths) + for fileGroup in fileGroups { + let fileGroupPath = basePath + fileGroup + let fileGroupChildren = (try? fileGroupPath.recursiveChildren()) ?? [] + files.append(contentsOf: fileGroupChildren) + files.append(fileGroupPath) + } + + for target in aggregateTargets { + files.append(contentsOf: target.configFilePaths) + } + + for target in targets { + files.append(contentsOf: target.configFilePaths) + for source in target.sources { + let sourcePath = basePath + source.path + let sourceChildren = (try? sourcePath.recursiveChildren()) ?? [] + files.append(contentsOf: sourceChildren) + files.append(sourcePath) + } + } + return files + } +} + +extension BuildSettingsContainer { + + fileprivate var configFilePaths: [Path] { + return configFiles.values.map{ Path($0) } + } +} + diff --git a/Sources/ProjectSpec/SpecLoader.swift b/Sources/ProjectSpec/SpecLoader.swift index ca49d620..0542081a 100644 --- a/Sources/ProjectSpec/SpecLoader.swift +++ b/Sources/ProjectSpec/SpecLoader.swift @@ -10,7 +10,7 @@ extension Project { try self.init(basePath: path.parent(), jsonDictionary: dictionary) } - private static func loadDictionary(path: Path) throws -> JSONDictionary { + public static func loadDictionary(path: Path) throws -> JSONDictionary { // Depending on the extension we will either load the file as YAML or JSON var json: [String: Any] diff --git a/Sources/XcodeGen/main.swift b/Sources/XcodeGen/main.swift index d914b3fd..f2f40185 100644 --- a/Sources/XcodeGen/main.swift +++ b/Sources/XcodeGen/main.swift @@ -5,10 +5,11 @@ import PathKit import ProjectSpec import XcodeGenKit import xcodeproj +import Yams let version = try Version("2.0.0") -func generate(spec: String, project: String, isQuiet: Bool, justVersion: Bool) { +func generate(spec: String, project: String, lockfile: String, isQuiet: Bool, justVersion: Bool) { if justVersion { print(version) exit(EXIT_SUCCESS) @@ -28,9 +29,11 @@ func generate(spec: String, project: String, isQuiet: Bool, justVersion: Bool) { fatalError("No project spec found at \(projectSpecPath.absolute())") } + let projectDictionary: JSONDictionary let project: Project do { - project = try Project(path: projectSpecPath) + 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 "))") } catch let error as CustomStringConvertible { fatalError("Parsing project spec failed: \(error)") @@ -38,6 +41,50 @@ func generate(spec: String, project: String, isQuiet: Bool, justVersion: Bool) { fatalError("Parsing project spec failed: \(error.localizedDescription)") } + // Lock file + var lockFileContent: String = "" + let lockFilePath = lockfile.isEmpty ? nil : Path(lockfile) + if let lockFilePath = lockFilePath { + + let files = Array(Set(project.allFiles)) + .map { $0.byRemovingBase(path: project.basePath).string } + .sorted { $0.localizedStandardCompare($1) == .orderedAscending } + .joined(separator: "\n") + + let spec: String + do { + let node = try Node(projectDictionary) + spec = try Yams.serialize(node: node) + } catch { + fatalError("Couldn't serialize spec for lockfile") + } + + 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)") + } + } + } + do { try project.validateMinimumXcodeGenVersion(version) try project.validate() @@ -52,6 +99,10 @@ func generate(spec: String, project: String, isQuiet: Bool, justVersion: Bool) { 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) @@ -73,6 +124,12 @@ 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( "quiet", default: false, diff --git a/Sources/XcodeGenKit/PathExtensions.swift b/Sources/XcodeGenKit/PathExtensions.swift index e029844a..35f3675e 100644 --- a/Sources/XcodeGenKit/PathExtensions.swift +++ b/Sources/XcodeGenKit/PathExtensions.swift @@ -3,7 +3,7 @@ import PathKit extension Path { - func byRemovingBase(path: Path) -> Path { + public func byRemovingBase(path: Path) -> Path { return Path(normalize().string.replacingOccurrences(of: "\(path.normalize().string)/", with: "")) } } From ca5ad9bf1438828e536ffa57ee2a14b900489d46 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sat, 3 Nov 2018 19:29:52 +1100 Subject: [PATCH 02/11] use json for serialization --- Sources/XcodeGen/main.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/XcodeGen/main.swift b/Sources/XcodeGen/main.swift index f2f40185..86399942 100644 --- a/Sources/XcodeGen/main.swift +++ b/Sources/XcodeGen/main.swift @@ -44,7 +44,8 @@ func generate(spec: String, project: String, lockfile: String, isQuiet: Bool, ju // Lock file var lockFileContent: String = "" let lockFilePath = lockfile.isEmpty ? nil : Path(lockfile) - if let lockFilePath = lockFilePath { + if #available(OSX 10.13, *), let lockFilePath = lockFilePath { + // JSONSerialization.WritingOptions.sortedKeys is only available on 10.13 let files = Array(Set(project.allFiles)) .map { $0.byRemovingBase(path: project.basePath).string } @@ -53,10 +54,10 @@ func generate(spec: String, project: String, lockfile: String, isQuiet: Bool, ju let spec: String do { - let node = try Node(projectDictionary) - spec = try Yams.serialize(node: node) + let data = try JSONSerialization.data(withJSONObject: projectDictionary, options: [.sortedKeys, .prettyPrinted]) + spec = String(data: data, encoding: .utf8)! } catch { - fatalError("Couldn't serialize spec for lockfile") + fatalError("Couldn't serialize spec for lockfile\n\(error)") } lockFileContent = """ From f9be03ebc858a48f75c39c73a4ecf05e2fde9b32 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sat, 3 Nov 2018 22:21:52 +1100 Subject: [PATCH 03/11] 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) + } + +} From 4560a7d5f220bf83b477c99c1f322ba8240c25b4 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sat, 3 Nov 2018 23:07:14 +1100 Subject: [PATCH 04/11] update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65f8cc6d..af4eee58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Master +#### Added +- Added `--use-cache` argument to prevent unnecessarily generating the project [#412](https://github.com/yonaskolb/XcodeGen/pull/412) @yonaskolb + #### Fixed - Fixed XPC Service package type [#435](https://github.com/yonaskolb/XcodeGen/pull/435) @alvarhansen - Fixed phase ordering for modulemap and static libary header Copy File phases. [402](https://github.com/yonaskolb/XcodeGen/pull/402) @brentleyjones From 0644f18f0be7397794659f0ebfacd3d7ffb88711 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sun, 4 Nov 2018 20:27:37 +1100 Subject: [PATCH 05/11] add cache fixture perf test --- Sources/ProjectSpec/Version.swift | 6 +++++- Sources/XcodeGenKit/SpecLoader.swift | 4 +--- Tests/PerformanceTests/PerformanceTests.swift | 9 +++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Sources/ProjectSpec/Version.swift b/Sources/ProjectSpec/Version.swift index a2c4f713..3d56f2b4 100644 --- a/Sources/ProjectSpec/Version.swift +++ b/Sources/ProjectSpec/Version.swift @@ -1,11 +1,15 @@ import Foundation -public struct Version: CustomStringConvertible, Equatable, Comparable { +public struct Version: CustomStringConvertible, Equatable, Comparable, ExpressibleByStringLiteral { public var major: UInt public var minor: UInt public var patch: UInt + public init(stringLiteral value: String) { + try! self.init(value) + } + public init(_ string: String) throws { let components = try string.split(separator: ".").map { (componentString) -> UInt in guard let uint = UInt(componentString) else { diff --git a/Sources/XcodeGenKit/SpecLoader.swift b/Sources/XcodeGenKit/SpecLoader.swift index 0ec58d60..9c502522 100644 --- a/Sources/XcodeGenKit/SpecLoader.swift +++ b/Sources/XcodeGenKit/SpecLoader.swift @@ -10,11 +10,9 @@ public class SpecLoader { var project: Project! private var projectDictionary: [String: Any]? let version: Version - let cacheFilePath: Path? - public init(version: Version, cacheFilePath: Path? = nil) { + public init(version: Version) { self.version = version - self.cacheFilePath = cacheFilePath } public func loadProject(path: Path) throws -> Project { diff --git a/Tests/PerformanceTests/PerformanceTests.swift b/Tests/PerformanceTests/PerformanceTests.swift index 8fff4947..6779e01a 100644 --- a/Tests/PerformanceTests/PerformanceTests.swift +++ b/Tests/PerformanceTests/PerformanceTests.swift @@ -40,6 +40,15 @@ class FixturePerformanceTests: XCTestCase { } } + func testCacheFileGeneration() throws { + let specLoader = SpecLoader(version: "1.2") + _ = try specLoader.loadProject(path: specPath) + + self.measure { + _ = try! specLoader.generateCacheFile() + } + } + func testFixtureGeneration() throws { let project = try Project(path: specPath) measure { From 8b5ac6f8854bc681366e209506d9d909afb3d274 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sun, 4 Nov 2018 20:30:28 +1100 Subject: [PATCH 06/11] check if project exists before reading from cache --- Sources/XcodeGen/main.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/XcodeGen/main.swift b/Sources/XcodeGen/main.swift index 3b0d0299..c8f8e49f 100644 --- a/Sources/XcodeGen/main.swift +++ b/Sources/XcodeGen/main.swift @@ -35,6 +35,7 @@ func generate(spec: String, project: String, useCache: Bool, isQuiet: Bool, just // load project spec do { project = try specLoader.loadProject(path: projectSpecPath) + projectPath = projectPath + "\(project.name).xcodeproj" } catch let error as CustomStringConvertible { fatalError("Parsing project spec failed: \(error)") } catch { @@ -54,7 +55,9 @@ func generate(spec: String, project: String, useCache: Bool, isQuiet: Bool, just } // check cache - if let cacheFile = cacheFile, cacheFilePath.exists { + if let cacheFile = cacheFile, + projectPath.exists, + cacheFilePath.exists { do { let existingCacheFile: String = try cacheFilePath.read() if cacheFile.string == existingCacheFile { @@ -81,7 +84,6 @@ func generate(spec: String, project: String, useCache: Bool, isQuiet: Bool, just // 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() From 4b9edceb189f5052f163187abda624974b8491a4 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sun, 4 Nov 2018 20:30:43 +1100 Subject: [PATCH 07/11] don't fail if cache file can't be created --- Sources/XcodeGen/main.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XcodeGen/main.swift b/Sources/XcodeGen/main.swift index c8f8e49f..ece4cf91 100644 --- a/Sources/XcodeGen/main.swift +++ b/Sources/XcodeGen/main.swift @@ -50,7 +50,7 @@ func generate(spec: String, project: String, useCache: Bool, isQuiet: Bool, just do { cacheFile = try specLoader.generateCacheFile() } catch { - fatalError("Couldn't generate cache file: \(error.localizedDescription)") + logger.error("Couldn't generate cache file: \(error.localizedDescription)") } } From c4b9b19a967ba91eac108cfc2b108bd655a4395b Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sat, 1 Dec 2018 18:45:06 +1100 Subject: [PATCH 08/11] add cache-path argument --- Sources/XcodeGenCLI/GenerateCommand.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/XcodeGenCLI/GenerateCommand.swift b/Sources/XcodeGenCLI/GenerateCommand.swift index b7569acb..8073196f 100644 --- a/Sources/XcodeGenCLI/GenerateCommand.swift +++ b/Sources/XcodeGenCLI/GenerateCommand.swift @@ -15,9 +15,12 @@ class GenerateCommand: Command { defaultValue: false) let useCache = Flag("-c", "--use-cache", - description: "Use a cache for the xcodegen spe", + description: "Use a cache for the xcodegen spec", defaultValue: false) + let cacheFilePath = Key("--cache-path", + description: "Where the cache file will be loaded from and save to. Defaults to ~/.xcodegen/cache/PATH_HASH") + let spec = Key("-s", "--spec", description: "The path to the project spec file. Defaults to project.yml") @@ -52,7 +55,8 @@ class GenerateCommand: Command { let projectPath = projectDirectory + "\(project.name).xcodeproj" - let cacheFilePath = Path("~/.xcodegen/cache/\(projectSpecPath.absolute().string.md5)").absolute() + let cacheFilePath = self.cacheFilePath.value ?? + Path("~/.xcodegen/cache/\(projectSpecPath.absolute().string.md5)").absolute() var cacheFile: CacheFile? // read cache From ed80164a4db45cff641cd8afb0f806c4c3ae8102 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Wed, 5 Dec 2018 18:38:04 +1100 Subject: [PATCH 09/11] fix project not generating if private settings are checked in --- Sources/XcodeGenCLI/GenerateCommand.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/XcodeGenCLI/GenerateCommand.swift b/Sources/XcodeGenCLI/GenerateCommand.swift index 8073196f..389372d1 100644 --- a/Sources/XcodeGenCLI/GenerateCommand.swift +++ b/Sources/XcodeGenCLI/GenerateCommand.swift @@ -68,9 +68,11 @@ class GenerateCommand: Command { } } + let projectExists = XcodeProj.pbxprojPath(projectPath).exists + // check cache if let cacheFile = cacheFile, - projectPath.exists, + projectExists, cacheFilePath.exists { do { let existingCacheFile: String = try cacheFilePath.read() From cf32c8736a5705fd67cec88faef4072a0aa02e40 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Wed, 5 Dec 2018 18:37:17 +1100 Subject: [PATCH 10/11] update documentation --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f9412ac..4f9f0783 100644 --- a/README.md +++ b/README.md @@ -115,17 +115,18 @@ And then import wherever needed: `import XcodeGenKit` Simply run: ```shell -xcodegen +xcodegen generate ``` This will look for a project spec in the current directory called `project.yml` and generate an Xcode project with the name defined in the spec. -To specify any options use the full `xcodegen generate` command and add the following: +Options: - **--spec**: An optional path to a `.yml` or `.json` project spec. Defaults to `project.yml` - **--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. +- **--cache-path**: A custom path to use for your cache file. This only has an affect is `--use-cache` is passed. This defaults to `~/.xcodegen/cache/{PROJECT_SPEC_PATH_HASH}` Use `xcodegen help` to see more detailed usage information. From c8db3040248c2999b8835718e7febb082f518606 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Tue, 18 Dec 2018 22:04:42 +1100 Subject: [PATCH 11/11] update docs --- CHANGELOG.md | 2 +- README.md | 2 +- Sources/XcodeGenCLI/GenerateCommand.swift | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2961b774..b3477aa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Master #### Added -- Added `--use-cache` argument to prevent unnecessarily generating the project [#412](https://github.com/yonaskolb/XcodeGen/pull/412) @yonaskolb +- Added an experiment new caching feature. Pass `--use-cache` to opt in. This will read and write from a cache file to prevent unnecessarily generating the project. Give it a try as it may become the default in a future release [#412](https://github.com/yonaskolb/XcodeGen/pull/412) @yonaskolb #### Changed - Changed spelling of build phases to **preBuildPhase** and **postBuildPhase**. The older names are deprecated but still work [402](https://github.com/yonaskolb/XcodeGen/pull/402) @brentleyjones diff --git a/README.md b/README.md index a9dae7ba..630602b4 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Options: - **--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. -- **--cache-path**: A custom path to use for your cache file. This only has an affect is `--use-cache` is passed. This defaults to `~/.xcodegen/cache/{PROJECT_SPEC_PATH_HASH}` +- **--cache-path**: A custom path to use for your cache file. This defaults to `~/.xcodegen/cache/{PROJECT_SPEC_PATH_HASH}` Use `xcodegen help` to see more detailed usage information. diff --git a/Sources/XcodeGenCLI/GenerateCommand.swift b/Sources/XcodeGenCLI/GenerateCommand.swift index 389372d1..e80c0e7c 100644 --- a/Sources/XcodeGenCLI/GenerateCommand.swift +++ b/Sources/XcodeGenCLI/GenerateCommand.swift @@ -15,11 +15,11 @@ class GenerateCommand: Command { defaultValue: false) let useCache = Flag("-c", "--use-cache", - description: "Use a cache for the xcodegen spec", + description: "Use a cache for the xcodegen spec. This will prevent unnecessarily generating the project if nothing has changed", defaultValue: false) let cacheFilePath = Key("--cache-path", - description: "Where the cache file will be loaded from and save to. Defaults to ~/.xcodegen/cache/PATH_HASH") + description: "Where the cache file will be loaded from and save to. Defaults to ~/.xcodegen/cache/{SPEC_PATH_HASH}") let spec = Key("-s", "--spec", description: "The path to the project spec file. Defaults to project.yml") @@ -60,7 +60,7 @@ class GenerateCommand: Command { var cacheFile: CacheFile? // read cache - if useCache.value { + if useCache.value || self.cacheFilePath.value != nil { do { cacheFile = try specLoader.generateCacheFile() } catch {