diff --git a/Sources/XCLogParser/commands/CommandHandler.swift b/Sources/XCLogParser/commands/CommandHandler.swift index dab1c2d..c97b507 100644 --- a/Sources/XCLogParser/commands/CommandHandler.swift +++ b/Sources/XCLogParser/commands/CommandHandler.swift @@ -21,10 +21,16 @@ import Foundation public struct CommandHandler { - let logFinder = LogFinder() - let activityLogParser = ActivityParser() + let logFinder: LogFinder + let activityLogParser: ActivityParser - public init() { } + public init( + logFinder: LogFinder = .init(), + activityLogParser: ActivityParser = .init() + ) { + self.logFinder = logFinder + self.activityLogParser = activityLogParser + } public func handle(command: Command) throws { switch command.action { diff --git a/Sources/XCLogParser/commands/LogOptions.swift b/Sources/XCLogParser/commands/LogOptions.swift index da21f0e..5f2182b 100644 --- a/Sources/XCLogParser/commands/LogOptions.swift +++ b/Sources/XCLogParser/commands/LogOptions.swift @@ -38,9 +38,15 @@ public struct LogOptions { /// The path to a LogManifest.plist file let logManifestPath: String + + /// Type of logs to search for + let logType: LogType /// Use strict Xcode project naming. let strictProjectName: Bool + + /// Timestamp to check + let newerThan: Date? /// Computed property, return the xcworkspacePath if not empty or /// the xcodeprojPath if xcworkspacePath is empty @@ -53,29 +59,37 @@ public struct LogOptions { xcodeprojPath: String, derivedDataPath: String, xcactivitylogPath: String, - strictProjectName: Bool = false) { + logType: LogType, + strictProjectName: Bool = false, + newerThan: Date? = nil) { self.projectName = projectName self.xcworkspacePath = xcworkspacePath self.xcodeprojPath = xcodeprojPath self.derivedDataPath = derivedDataPath self.xcactivitylogPath = xcactivitylogPath + self.logType = logType self.logManifestPath = String() self.strictProjectName = strictProjectName + self.newerThan = newerThan } public init(projectName: String, xcworkspacePath: String, xcodeprojPath: String, derivedDataPath: String, + logType: LogType, logManifestPath: String, - strictProjectName: Bool = false) { + strictProjectName: Bool = false, + newerThan: Date? = nil) { self.projectName = projectName self.xcworkspacePath = xcworkspacePath self.xcodeprojPath = xcodeprojPath self.derivedDataPath = derivedDataPath self.logManifestPath = logManifestPath + self.logType = logType self.xcactivitylogPath = String() self.strictProjectName = strictProjectName + self.newerThan = newerThan } } diff --git a/Sources/XCLogParser/loglocation/LogFinder.swift b/Sources/XCLogParser/loglocation/LogFinder.swift index e317c1c..a6595d2 100644 --- a/Sources/XCLogParser/loglocation/LogFinder.swift +++ b/Sources/XCLogParser/loglocation/LogFinder.swift @@ -26,13 +26,11 @@ public struct LogFinder { let buildDirSettingsPrefix = "BUILD_DIR = " - let xcodebuildPath = "/usr/bin/xcodebuild" - - let logsDir = "/Logs/Build/" + let xcodebuildPath: String let logManifestFile = "LogStoreManifest.plist" - let emmptyDirResponseMessage = """ + let emptyDirResponseMessage = """ Error. Couldn't find the derived data directory. Please use the --filePath option to specify the path to the xcactivitylog file you want to parse. """ @@ -44,7 +42,11 @@ public struct LogFinder { return homeDirURL.appendingPathComponent("Library/Developer/Xcode/DerivedData", isDirectory: true) } - public init() {} + public init( + xcodebuildPath: String = "/usr/bin/xcodebuild" + ) { + self.xcodebuildPath = xcodebuildPath + } public func findLatestLogWithLogOptions(_ logOptions: LogOptions) throws -> URL { guard logOptions.xcactivitylogPath.isEmpty else { @@ -54,10 +56,17 @@ public struct LogFinder { let projectDir = try getProjectDirWithLogOptions(logOptions) // get latestLog - return try URL(fileURLWithPath: getLatestLogInDir(projectDir)) } + + public func findLatestLogsWithLogOptions(_ logOptions: LogOptions) throws -> [URL] { + // get project dir + let projectDir = try getProjectDirWithLogOptions(logOptions) + + // get latestLog + return try getLatestLogsInDir(projectDir, since: logOptions.newerThan) + } public func findLogManifestWithLogOptions(_ logOptions: LogOptions) throws -> URL { let logManifestURL = try findLogManifestURLWithLogOptions(logOptions) @@ -111,17 +120,18 @@ public struct LogFinder { // when xcodebuild is run with -derivedDataPath the logs are at the root level if logOptions.derivedDataPath.isEmpty == false { if FileManager.default.fileExists(atPath: - derivedData.appendingPathComponent(logsDir).path) { - return derivedData.appendingPathComponent(logsDir) + derivedData.appendingPathComponent(logOptions.logType.path).path) { + return derivedData.appendingPathComponent(logOptions.logType.path) } } if logOptions.projectLocation.isEmpty == false { - let folderName = try getProjectFolderWithHash(logOptions.projectLocation) + let folderName = try getProjectFolderWithHash(logOptions.projectLocation, logType: logOptions.logType) return derivedData.appendingPathComponent(folderName) } if logOptions.projectName.isEmpty == false { return try findDerivedDataForProject(logOptions.projectName, inDir: derivedData, + logType: logOptions.logType, strictProjectName: logOptions.strictProjectName) } throw LogError.noLogFound(dir: derivedData.path) @@ -143,9 +153,12 @@ public struct LogFinder { /// - parameter name: Name of the project /// - parameter inDir: URL of the derived data directory /// - returns: The path to the derived data of the project or nil if it is not found. - public func findDerivedDataForProject(_ name: String, - inDir derivedDataDir: URL, - strictProjectName: Bool) throws -> URL { + public func findDerivedDataForProject( + _ name: String, + inDir derivedDataDir: URL, + logType: LogType, + strictProjectName: Bool + ) throws -> URL { let fileManager = FileManager.default @@ -185,7 +198,7 @@ public struct LogFinder { with --file or the right DerivedData folder with --derived_data """) } - return match.appendingPathComponent(logsDir) + return match.appendingPathComponent(logType.path) } /// Gets the full path of the Build/Logs directory for the given project @@ -194,12 +207,12 @@ public struct LogFinder { /// - parameter projectPath: The path to the .xcodeproj folder /// - returns: The full path to the `Build/Logs` directory /// - throws: An error if the derived data directory couldn't be found - public func logsDirectoryForXcodeProject(projectPath: String) throws -> String { + public func logsDirectoryForXcodeProject(projectPath: String, logType: LogType) throws -> String { let arguments = ["-project", projectPath, "-showBuildSettings"] if let result = try executeXcodeBuild(args: arguments) { - return try parseXcodeBuildDir(result) + return try parseXcodeBuildDir(result, logType: logType) } - throw LogError.xcodeBuildError(emmptyDirResponseMessage) + throw LogError.xcodeBuildError(emptyDirResponseMessage) } /// Gets the latest xcactivitylog file path for the given projectFolder @@ -243,12 +256,12 @@ public struct LogFinder { /// - parameter andScheme: The name of the scheme /// - returns: The full path to the `Build/Logs` directory /// - throws: An error if the derived data directory can't be found. - public func logsDirectoryForWorkspace(_ workspace: String, andScheme scheme: String) throws -> String { + public func logsDirectoryForWorkspace(_ workspace: String, andScheme scheme: String, logType: LogType) throws -> String { let arguments = ["-workspace", workspace, "-scheme", scheme, "-showBuildSettings"] if let result = try executeXcodeBuild(args: arguments) { - return try parseXcodeBuildDir(result) + return try parseXcodeBuildDir(result, logType: logType) } - throw LogError.xcodeBuildError(emmptyDirResponseMessage) + throw LogError.xcodeBuildError(emptyDirResponseMessage) } /// Returns the latest xcactivitylog file path in the given directory @@ -273,18 +286,50 @@ public struct LogFinder { } return logPath.path } + + /// Returns the latest xcactivitylog file path in the given directory + /// - parameter dir: The full path for the directory + /// - returns: The paths of the latest xcactivitylog file in it since given date. + /// - throws: An `Error` if the directory doesn't exist or if there are no xcactivitylog files in it. + public func getLatestLogsInDir(_ dir: URL, since date: Date?) throws -> [URL] { + let fileManager = FileManager.default + let files = try fileManager.contentsOfDirectory(at: dir, + includingPropertiesForKeys: [.contentModificationDateKey], + options: .skipsHiddenFiles) + let sorted = try files + .filter { $0.path.hasSuffix(".xcactivitylog") } + .filter { + guard let timestamp = date else { return true } + guard + let lastModified = try? $0.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate + else { return false } + return lastModified > timestamp + } + .sorted { + let lhv = try $0.resourceValues(forKeys: [.contentModificationDateKey]) + let rhv = try $1.resourceValues(forKeys: [.contentModificationDateKey]) + guard let lhDate = lhv.contentModificationDate, let rhDate = rhv.contentModificationDate else { + return false + } + return lhDate.compare(rhDate) == .orderedDescending + } + guard !sorted.isEmpty else { + throw LogError.noLogFound(dir: dir.path) + } + return sorted + } /// Generates the Derived Data Build Logs Folder name for the given project path /// - parameter projectFilePath: A path (relative or absolut) to an .xcworkspace or an .xcodeproj directory /// - returns The name of the folder with the same hash Xcode generates. /// For instance MyApp-dtpdmwoqyxcbrmauwqvycvmftqah/Logs/Build - public func getProjectFolderWithHash(_ projectFilePath: String) throws -> String { + public func getProjectFolderWithHash(_ projectFilePath: String, logType: LogType) throws -> String { let path = Path(projectFilePath).absolute() let projectName = path.lastComponent .replacingOccurrences(of: ".xcworkspace", with: "") .replacingOccurrences(of: ".xcodeproj", with: "") let hash = try XcodeHasher.hashString(for: path.string) - return "\(projectName)-\(hash)".appending(logsDir) + return "\(projectName)-\(hash)".appending(logType.path) } private func executeXcodeBuild(args: [String]) throws -> String? { @@ -305,7 +350,7 @@ public struct LogFinder { return String(data: data, encoding: .utf8) } - private func parseXcodeBuildDir(_ response: String) throws -> String { + private func parseXcodeBuildDir(_ response: String, logType: LogType) throws -> String { guard !response.starts(with: "xcodebuild: error: ") else { throw LogError.xcodeBuildError(response.replacingOccurrences(of: "xcodebuild: ", with: "")) } @@ -315,8 +360,8 @@ public struct LogFinder { if let settings = buildDirSettings.first { return settings.trimmingCharacters(in: .whitespacesAndNewlines) .replacingOccurrences(of: buildDirSettingsPrefix, with: "") - .replacingOccurrences(of: "Build/Products", with: logsDir) + .replacingOccurrences(of: "Build/Products", with: logType.path) } - throw LogError.xcodeBuildError(emmptyDirResponseMessage) + throw LogError.xcodeBuildError(emptyDirResponseMessage) } } diff --git a/Sources/XCLogParser/loglocation/LogType.swift b/Sources/XCLogParser/loglocation/LogType.swift new file mode 100644 index 0000000..6b4e296 --- /dev/null +++ b/Sources/XCLogParser/loglocation/LogType.swift @@ -0,0 +1,27 @@ +// +// LogType.swift +// +// +// Created by Danny Gilbert on 2/2/22. +// + +import Foundation + +public enum LogType: String { + case build = "Build" + case indexBuild = "Index Build" + case install = "Install" + case issues = "Issues" + case package = "Package" + case run = "Run" + case test = "Test" + case updateSigning = "Update Signing" +} + +// MARK: - Log Location +public extension LogType { + + var path: String { + "/Logs/\(self.rawValue)/" + } +} diff --git a/Sources/XCLogParserApp/commands/DumpCommand.swift b/Sources/XCLogParserApp/commands/DumpCommand.swift index 8996503..ae466a6 100644 --- a/Sources/XCLogParserApp/commands/DumpCommand.swift +++ b/Sources/XCLogParserApp/commands/DumpCommand.swift @@ -26,6 +26,9 @@ struct DumpCommand: ParsableCommand { commandName: "dump", abstract: "Dumps the xcactivitylog file into a JSON document" ) + + @Option(name: .long, help: "Type of .xactivitylog file to look for.") + var logs: LogType = .build @Option(name: .long, help: "The path to a .xcactivitylog file.") var file: String? @@ -104,6 +107,7 @@ struct DumpCommand: ParsableCommand { xcodeprojPath: xcodeproj ?? "", derivedDataPath: derivedData ?? "", xcactivitylogPath: file ?? "", + logType: logs, strictProjectName: strictProjectName) let actionOptions = ActionOptions(reporter: .json, outputPath: output ?? "", diff --git a/Sources/XCLogParserApp/commands/ManifestCommand.swift b/Sources/XCLogParserApp/commands/ManifestCommand.swift index 4976134..6e1ad19 100644 --- a/Sources/XCLogParserApp/commands/ManifestCommand.swift +++ b/Sources/XCLogParserApp/commands/ManifestCommand.swift @@ -26,6 +26,9 @@ struct ManifestCommand: ParsableCommand { commandName: "manifest", abstract: "Shows the content of a LogManifest plist file as a JSON document." ) + + @Option(name: .long, help: "Type of .xactivitylog file to look for.") + var logs: LogType = .build @Option(name: .customLong("long_manifest"), help: "The path to an existing LogStoreManifest.plist.") var logManifest: String? @@ -89,6 +92,7 @@ struct ManifestCommand: ParsableCommand { xcworkspacePath: workspace ?? "", xcodeprojPath: xcodeproj ?? "", derivedDataPath: derivedData ?? "", + logType: logs, logManifestPath: logManifest ?? "", strictProjectName: strictProjectName) diff --git a/Sources/XCLogParserApp/commands/ParseCommand.swift b/Sources/XCLogParserApp/commands/ParseCommand.swift index 3b9124f..3a69fbe 100644 --- a/Sources/XCLogParserApp/commands/ParseCommand.swift +++ b/Sources/XCLogParserApp/commands/ParseCommand.swift @@ -26,6 +26,9 @@ struct ParseCommand: ParsableCommand { commandName: "parse", abstract: "Parses the content of an xcactivitylog file" ) + + @Option(name: .long, help: "Type of .xactivitylog file to look for.") + var logs: LogType = .build @Option(name: .long, help: "The path to a .xcactivitylog file.") var file: String? @@ -163,6 +166,7 @@ struct ParseCommand: ParsableCommand { xcodeprojPath: xcodeproj ?? "", derivedDataPath: derivedData ?? "", xcactivitylogPath: file ?? "", + logType: logs, strictProjectName: strictProjectName) let actionOptions = ActionOptions(reporter: xclReporter, outputPath: output ?? "", diff --git a/Sources/XCLogParserApp/extensions/LogType+.swift b/Sources/XCLogParserApp/extensions/LogType+.swift new file mode 100644 index 0000000..423d994 --- /dev/null +++ b/Sources/XCLogParserApp/extensions/LogType+.swift @@ -0,0 +1,11 @@ +// +// LogType+.swift +// +// +// Created by Danny Gilbert on 2/2/22. +// + +import XCLogParser +import ArgumentParser + +extension LogType: ExpressibleByArgument { } diff --git a/Tests/XCLogParserTests/LogFinderTests.swift b/Tests/XCLogParserTests/LogFinderTests.swift index be1d02f..b7dd087 100644 --- a/Tests/XCLogParserTests/LogFinderTests.swift +++ b/Tests/XCLogParserTests/LogFinderTests.swift @@ -74,7 +74,7 @@ class LogFinderTests: XCTestCase { XCTFail("Unable to create test directories.") return } - let projectFolder = try logFinder.getProjectFolderWithHash("/Users/user/projects/MyProject.xcworkspace") + let projectFolder = try logFinder.getProjectFolderWithHash("/Users/user/projects/MyProject.xcworkspace", logType: .build) let logsFolder = projectFolder.appending("/Logs/Build") let projectLogFolder = derivedDataDir.appendingPathComponent(logsFolder, isDirectory: true) _ = try TestUtils.createSubdir(logsFolder, in: derivedDataDir) @@ -97,13 +97,13 @@ class LogFinderTests: XCTestCase { return } // since there is not a valid xcodeproj in the path, it should fail - XCTAssertThrowsError(try logFinder.logsDirectoryForXcodeProject(projectPath: dirWithProject.path)) + XCTAssertThrowsError(try logFinder.logsDirectoryForXcodeProject(projectPath: dirWithProject.path, logType: .build)) } func testGetProjectFolderWithHash() throws { let projectPath = "/tmp/MyWorkspace.xcworkspace" let expectedProjectFolder = "MyWorkspace-fvaxjdltriwevoggjzpmzcohhhxf/Logs/Build/" - let projectFolder = try logFinder.getProjectFolderWithHash(projectPath) + let projectFolder = try logFinder.getProjectFolderWithHash(projectPath, logType: .build) XCTAssertEqual(expectedProjectFolder, projectFolder) } @@ -116,6 +116,7 @@ class LogFinderTests: XCTestCase { xcworkspacePath: "/tmp/MyWorkspace.xcworkspace", xcodeprojPath: "", derivedDataPath: derivedDataDir.path, + logType: .build, logManifestPath: "") let logManifestURL = try logFinder.findLogManifestURLWithLogOptions(logOptions) let expectedPathPattern = "\(derivedDataDir.path)/MyWorkspace-fvaxjdltriwevoggjzpmzcohhhxf" + @@ -128,6 +129,7 @@ class LogFinderTests: XCTestCase { xcworkspacePath: "", xcodeprojPath: "/tmp/MyApp.xcodeproj", derivedDataPath: "/projects/DerivedData", + logType: .build, logManifestPath: "") let logManifestURL = try logFinder.findLogManifestURLWithLogOptions(logOptions) let expectedPathPattern = "/projects/DerivedData/" + @@ -140,6 +142,7 @@ class LogFinderTests: XCTestCase { xcworkspacePath: "", xcodeprojPath: "/tmp/MyApp.xcodeproj", derivedDataPath: "/projects/DerivedData", + logType: .build, logManifestPath: "") XCTAssertThrowsError(try logFinder.findLogManifestWithLogOptions(logOptions)) } @@ -154,6 +157,7 @@ class LogFinderTests: XCTestCase { xcworkspacePath: "", xcodeprojPath: "/tmp/MyApp.xcodeproj", derivedDataPath: customDerivedDataDir.path, + logType: .build, logManifestPath: "") let latestLog = try logFinder.findLatestLogWithLogOptions(logOptions) diff --git a/Tests/XCLogParserTests/LogManifestTests.swift b/Tests/XCLogParserTests/LogManifestTests.swift index bbccf04..9f7378a 100644 --- a/Tests/XCLogParserTests/LogManifestTests.swift +++ b/Tests/XCLogParserTests/LogManifestTests.swift @@ -78,6 +78,7 @@ class LogManifestTests: XCTestCase { xcworkspacePath: "", xcodeprojPath: "", derivedDataPath: "", + logType: .build, logManifestPath: logURL.path) let logEntries = try LogManifest().getWithLogOptions(logOptions) XCTAssertEqual(1, logEntries.count)