├── .gitignore ├── .gitmodules ├── App ├── App │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ └── Sources │ │ └── App │ │ ├── App.swift │ │ ├── AppReview.swift │ │ ├── Base.lproj │ │ └── Main.storyboard │ │ ├── CommonAppDelegate.swift │ │ ├── MainWindow.swift │ │ ├── Menu │ │ ├── AppMenu.swift │ │ └── Application.xib │ │ ├── SplitController.swift │ │ └── WindowController.swift ├── BuildGraph │ ├── BuildDeps-Info.plist │ ├── BuildGraph │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── MacOS Icon 01@1024-1.png │ │ │ │ ├── MacOS Icon 01@128.png │ │ │ │ ├── MacOS Icon 01@16.png │ │ │ │ ├── MacOS Icon 01@256-1.png │ │ │ │ ├── MacOS Icon 01@256.png │ │ │ │ ├── MacOS Icon 01@32-1.png │ │ │ │ ├── MacOS Icon 01@32.png │ │ │ │ ├── MacOS Icon 01@512-1.png │ │ │ │ ├── MacOS Icon 01@512.png │ │ │ │ └── MacOS Icon 01@64.png │ │ │ └── Contents.json │ │ ├── BuildGraph.entitlements │ │ └── main.swift │ ├── BuildGraphRelease.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── BuildGraph.xcscheme │ └── GoogleService-Info.plist └── BuildGraphDebug │ ├── BuildGraphDebug.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── BuildGraphDebug.xcscheme │ ├── BuildGraphDebug │ ├── AppDelegate.swift │ ├── BuildGraphDebug.entitlements │ ├── Samples │ │ ├── Intel_i5.xcactivitylog │ │ └── M1.xcactivitylog │ └── main.swift │ └── BuildGraphDebugUITests │ ├── BuildGraphDebugUITests.swift │ └── BuildGraphDebugUITestsLaunchTests.swift ├── BulidGraph.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── Domain ├── .gitignore ├── .swiftpm │ └── xcode │ │ └── xcshareddata │ │ └── xcschemes │ │ ├── Domain.xcscheme │ │ └── GraphParser.xcscheme ├── Package.resolved ├── Package.swift ├── README.md ├── Sources │ ├── BuildParser │ │ ├── BuildParser.docc │ │ │ └── BuildParser.md │ │ ├── CGColor+DarkMode.swift │ │ ├── Colors.swift │ │ ├── Domain │ │ │ ├── Analyzer.swift │ │ │ ├── BuildStepCounter.swift │ │ │ ├── DateFormatter+8601.swift │ │ │ ├── Dto.swift │ │ │ ├── DurationFormatter.swift │ │ │ ├── Event+extensions.swift │ │ │ ├── Event.swift │ │ │ ├── FilterSettings.swift │ │ │ ├── FolderMonitor.swift │ │ │ ├── Parsing │ │ │ │ ├── BuildStepConverter.swift │ │ │ │ ├── Collection+ParallelMap.swift │ │ │ │ ├── Collection+Safe.swift │ │ │ │ └── DepsPathExtraction.swift │ │ │ ├── Project.swift │ │ │ ├── ProjectDescriptionService.swift │ │ │ ├── ProjectReference.swift │ │ │ ├── ProjectReferenceFactory.swift │ │ │ ├── Projects │ │ │ │ ├── BookmarkSaver.swift │ │ │ │ ├── FileAccess.swift │ │ │ │ └── ProjectsFinder.swift │ │ │ ├── RealBuildLogParser.swift │ │ │ ├── StateViewController.swift │ │ │ ├── Storage.swift │ │ │ └── UISettings.swift │ │ ├── EventRelativeRect.swift │ │ ├── Layers │ │ │ ├── AppLayer.swift │ │ │ ├── ColorDescriptionLayer.swift │ │ │ ├── ColorsLegendLayer.swift │ │ │ ├── HUDLayer.swift │ │ │ ├── Modules │ │ │ │ ├── DependeciesLayer.swift │ │ │ │ ├── EventLayer.swift │ │ │ │ └── ModulesLayer.swift │ │ │ ├── PeriodsLayer.swift │ │ │ ├── TimelineLayer.swift │ │ │ └── Vertical lines │ │ │ │ ├── BuildStartLayer.swift │ │ │ │ ├── ConcurrencyLayer.swift │ │ │ │ └── VerticalLineLayer.swift │ │ └── Snapshot │ │ │ └── XcodeBuildSnapshot.swift │ ├── GraphParser │ │ ├── Dependency.swift │ │ ├── DependencyParser.swift │ │ ├── DependencyParserXcode15.swift │ │ └── GraphParser.docc │ │ │ └── GraphParser.md │ └── Snapshot │ │ ├── Samples │ │ ├── IncrementalWithBigGap.bgbuildsnapshot │ │ │ └── Logs │ │ │ │ └── Build │ │ │ │ └── 7D6B7C50-46EB-4C8C-B4AE-99590A734A2F.xcactivitylog │ │ ├── PrepareBuildOnly.bgbuildsnapshot │ │ │ └── Logs │ │ │ │ └── Build │ │ │ │ └── D31EC14E-52A6-4CDA-9ABA-1CACA0BC9ACE.xcactivitylog │ │ ├── SimpleClean.bgbuildsnapshot │ │ │ ├── Build │ │ │ │ └── Intermediates.noindex │ │ │ │ │ └── XCBuildData │ │ │ │ │ └── e9f65ec2d9f99e7a6246f6ec22f1e059-targetGraph.txt │ │ │ └── Logs │ │ │ │ └── Build │ │ │ │ └── F5F5EB7C-FD56-4037-9959-7056E5363FCD.xcactivitylog │ │ ├── Xcode14.3.bgbuildsnapshot │ │ │ └── Logs │ │ │ │ └── Build │ │ │ │ └── F8AB69B8-6496-4C72-A4F2-C14465EF248B.xcactivitylog │ │ └── Xcode16.3.bgbuildsnapshot │ │ │ └── Logs │ │ │ └── Build │ │ │ └── B5B813F0-47C9-4857-A605-6CEAE1E726D9.xcactivitylog │ │ ├── Snapshot.docc │ │ └── Snapshot.md │ │ └── TestBundle.swift └── Tests │ ├── BuildParserTests │ ├── DepsPathExtractionTests.swift │ ├── DurationFormatterTest.swift │ ├── EventDepsTests.swift │ ├── EventTest.swift │ ├── GraphTests.swift │ ├── PathFinderTests.swift │ ├── ProjectReferenceTests.swift │ ├── RealBuildLogParserTests.swift │ ├── TimelineLayerTests.swift │ ├── XCLogParserTests.swift │ └── __Snapshots__ │ │ ├── BuildGraphTests │ │ └── test_wholFile.1.txt │ │ ├── BuildParserTests │ │ └── test_drawingTestEvents.1.png │ │ ├── GraphTests │ │ ├── test_drawingIncrementalWithGap_cached.1.png │ │ ├── test_drawingIncrementalWithGap_currentBuild.1.png │ │ ├── test_drawingIncrementalWithGap_everything.1.png │ │ └── test_drawingSimpleClean.1.png │ │ └── TimelineLayerTests │ │ ├── test10Min.1.png │ │ ├── test20Min.1.png │ │ ├── test2Min.1.png │ │ └── test60Min.1.png │ ├── GraphParserTests │ ├── DependencyParserTests.swift │ ├── __Snapshots__ │ │ └── DependencyParserTests │ │ │ ├── test_fullFile_xcode14_2.1.txt │ │ │ ├── test_fullFile_xcode15_16.1.txt │ │ │ └── test_fullFile_xcode15_2.1.txt │ └── targetGraph.txt │ └── SnapshotTests │ └── SnapshotTests.swift ├── README.md ├── UI ├── .gitignore ├── .swiftpm │ └── xcode │ │ └── xcshareddata │ │ └── xcschemes │ │ └── UI.xcscheme ├── Package.resolved ├── Package.swift ├── README.md ├── Sources │ ├── Details │ │ ├── Details.storyboard │ │ ├── Details │ │ │ ├── DetailView.swift │ │ │ ├── DetaliViewController.swift │ │ │ ├── HUDScrollView.swift │ │ │ ├── HUDView.swift │ │ │ ├── MouseContorller.swift │ │ │ └── ZoomController.swift │ │ ├── FileLocationSelector.swift │ │ ├── ImageSaveService.swift │ │ ├── SnapshotSaveService.swift │ │ └── States │ │ │ ├── DetailsStatePresenter.swift │ │ │ ├── DetailsStateViewController.swift │ │ │ ├── LoadingViewController.swift │ │ │ └── RetryViewController.swift │ ├── Filters │ │ ├── DetailStepTypeDescription.swift │ │ ├── Filters.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── Filters.xcscheme │ │ ├── Settings.storyboard │ │ └── SettingsPopoverViewController.swift │ └── Projects │ │ ├── NoAccessViewController.swift │ │ ├── NoProjectsViewController.swift │ │ ├── ProjectSettings.swift │ │ ├── Projects.storyboard │ │ ├── Projects.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Projects.xcscheme │ │ ├── ProjectsOutlineViewController.swift │ │ ├── ProjectsPresenter.swift │ │ └── ProjectsStateViewController.swift └── Tests │ ├── DetailsTests │ └── DetailsTests.swift │ ├── FiltersTests │ └── FiltersTests.swift │ └── ProjectsTests │ ├── Doubles │ ├── DerivedDataStub.swift │ ├── ProjectsFinderMock.swift │ ├── ProjectsSelectionDelegateMock.swift │ └── ProjectsUIMock.swift │ ├── ProjectsIngtegrationTests.swift │ └── XCTest+Extensions.swift └── XCLogParser ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── NOTICE ├── Package.resolved ├── Package.swift ├── Sources ├── XCLogParser │ ├── XCLogParserError.swift │ ├── activityparser │ │ ├── ActivityParser.swift │ │ └── IDEActivityModel.swift │ ├── extensions │ │ ├── ArrayExtension.swift │ │ ├── EncodableExtension.swift │ │ ├── NSRegularExpressionExtension.swift │ │ └── URLExtension.swift │ ├── generated │ │ └── HtmlReporterResources.swift │ ├── lexer │ │ ├── LexRedactor.swift │ │ ├── Lexer.swift │ │ ├── LexerModel.swift │ │ ├── LogRedactor.swift │ │ └── String+BuildSpecificInformationRemoval.swift │ ├── loglocation │ │ ├── DefaultDerivedData.swift │ │ ├── DerivedDataByXcodeProjectLocation.swift │ │ ├── LogError.swift │ │ ├── LogFinder.swift │ │ └── LogLoader.swift │ ├── logmanifest │ │ ├── LogManifest.swift │ │ └── LogManifestModel.swift │ └── parser │ │ ├── BuildStep+Builder.swift │ │ ├── BuildStep+Parser.swift │ │ ├── BuildStep.swift │ │ ├── ClangCompilerParser.swift │ │ ├── Contains.swift │ │ ├── IDEActivityLogSection+Parsing.swift │ │ ├── LinkerStatistics.swift │ │ ├── MachineNameReader.swift │ │ ├── Notice+Parser.swift │ │ ├── Notice.swift │ │ ├── NoticeType.swift │ │ ├── ParserBuildSteps.swift │ │ ├── Prefix.swift │ │ ├── StringExtension.swift │ │ ├── Suffix.swift │ │ ├── SwiftCompilerFunctionTimeOptionParser.swift │ │ ├── SwiftCompilerParser.swift │ │ ├── SwiftCompilerTimeOptionParser.swift │ │ ├── SwiftCompilerTypeCheckOptionParser.swift │ │ ├── SwiftFunctionTime.swift │ │ └── SwiftTypeCheck.swift └── XcodeHasher │ └── XcodeHasher.swift ├── Tests ├── LinuxMain.swift └── XCLogParserTests │ ├── ActivityParserTests.swift │ ├── BuildStep+TestUtils.swift │ ├── ChromeTracerOutputTests.swift │ ├── ClangCompilerParserTests.swift │ ├── IssuesReporterTests.swift │ ├── LexRedactorTests.swift │ ├── LexerTests.swift │ ├── LogFinderTests.swift │ ├── LogManifestTests.swift │ ├── ParserTests.swift │ ├── ReporterTests.swift │ ├── String+BuildSpecificInformationRemovalTests.swift │ ├── SwiftCompilerParserTests.swift │ ├── TestUtils.swift │ └── XCTestManifests.swift └── docs ├── JSON Format.md └── Xcactivitylog Format.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | DerivedData/ 14 | *.moved-aside 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | 27 | ## App packaging 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 41 | # Package.resolved 42 | # *.xcodeproj 43 | # 44 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 45 | # hence it is not needed unless you have added a package configuration file to your project 46 | # .swiftpm 47 | 48 | .build/ 49 | 50 | # CocoaPods 51 | # 52 | # We recommend against adding the Pods directory to your .gitignore. However 53 | # you should judge for yourself, the pros and cons are mentioned at: 54 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 55 | # 56 | # Pods/ 57 | # 58 | # Add this line if you want to avoid checking in source code from the Xcode workspace 59 | # *.xcworkspace 60 | 61 | # Carthage 62 | # 63 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 64 | # Carthage/Checkouts 65 | 66 | Carthage/Build/ 67 | 68 | # Accio dependency management 69 | Dependencies/ 70 | .accio/ 71 | 72 | # fastlane 73 | # 74 | # It is recommended to not store the screenshots in the git repo. 75 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 76 | # For more information about the recommended setup visit: 77 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 78 | 79 | fastlane/report.xml 80 | fastlane/Preview.html 81 | fastlane/screenshots/**/*.png 82 | fastlane/test_output 83 | 84 | # Code Injection 85 | # 86 | # After new code Injection tools there's a generated folder /iOSInjectionProject 87 | # https://github.com/johnno1962/injectionforxcode 88 | 89 | iOSInjectionProject/ 90 | *.xcbkptlist 91 | .DS_Store 92 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/.gitmodules -------------------------------------------------------------------------------- /App/App/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /App/App/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "App", 7 | defaultLocalization: "en", 8 | platforms: [.macOS("11.3")], 9 | products: [ 10 | .library( 11 | name: "App", 12 | targets: ["App"]), 13 | ], 14 | dependencies: [ 15 | .package(path: "./../UI"), 16 | ], 17 | targets: [ 18 | .target( 19 | name: "App", 20 | dependencies: [ 21 | "UI" 22 | ]), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /App/App/README.md: -------------------------------------------------------------------------------- 1 | # App 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /App/App/Sources/App/App.swift: -------------------------------------------------------------------------------- 1 | public struct App { 2 | public private(set) var text = "Hello, World!" 3 | 4 | public init() { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/App/Sources/App/AppReview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppReview.swift 3 | // BuildGraph 4 | // 5 | // Created by Mikhail Rubanov on 16.04.2022. 6 | // 7 | 8 | import Foundation 9 | import StoreKit 10 | 11 | class AppReview { 12 | func requestIfPossible() { 13 | guard canShowReview else { 14 | requestCount += 1 15 | return 16 | } 17 | 18 | guard !wasShown else { return } // Do not show twice in a row 19 | 20 | showReview() 21 | } 22 | 23 | private func showReview() { 24 | SKStoreReviewController.requestReview() 25 | wasShown = true 26 | } 27 | 28 | private var wasShown = false 29 | 30 | private var canShowReview: Bool { 31 | requestCount >= 5 32 | } 33 | 34 | private var requestCount = 0 35 | } 36 | -------------------------------------------------------------------------------- /App/App/Sources/App/CommonAppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonAppDelegate.swift 3 | // App 4 | // 5 | // Created by Mikhail Rubanov on 06.06.2022. 6 | // 7 | 8 | import AppKit 9 | 10 | open class CommonAppDelegate: NSObject, NSApplicationDelegate { 11 | 12 | private var windowController: NSWindowController! 13 | 14 | open func applicationDidFinishLaunching(_ aNotification: Notification) { 15 | windowController = WindowController.fromStoryboard() 16 | windowController.showWindow(self) 17 | 18 | NSApplication.shared.mainMenu = AppMenu.menu() 19 | } 20 | 21 | public func applicationWillTerminate(_ aNotification: Notification) { 22 | // Insert code here to tear down your application 23 | } 24 | 25 | public func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 26 | return true 27 | } 28 | 29 | public func application(_ sender: NSApplication, openFile filename: String) -> Bool { 30 | guard let windowController = NSApplication.shared.windows 31 | .first? 32 | .windowController as? WindowController 33 | else { 34 | return false 35 | } 36 | 37 | let fileURL = URL(fileURLWithPath: filename) 38 | windowController.open(from: fileURL) 39 | 40 | return true 41 | } 42 | 43 | public func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 44 | true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /App/App/Sources/App/MainWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainWindow.swift 3 | // BuildDeps 4 | // 5 | // Created by Mikhail Rubanov on 23.02.2022. 6 | // 7 | 8 | import AppKit 9 | import BuildParser 10 | 11 | public class MainWindow: NSWindow { 12 | 13 | @IBOutlet weak var previousButton: NSToolbarItem! 14 | @IBOutlet weak var nextButton: NSToolbarItem! 15 | 16 | @IBOutlet weak var sendImageToolbarItem: NSToolbarItem! 17 | 18 | func setupToolbar() { 19 | setupToolbar(toolbar!) 20 | } 21 | 22 | func setupToolbar(_ toolbar: NSToolbar) { 23 | toolbar.insertItem(withItemIdentifier: .toggleSidebar, at: 2) 24 | toolbar.insertItem(withItemIdentifier: .sidebarTrackingSeparator, at: 3) 25 | 26 | toolbar.sizeMode = .regular 27 | toolbar.displayMode = .iconOnly 28 | 29 | disableButtons() 30 | } 31 | 32 | func disableButtons() { 33 | sendImageToolbarItem.isEnabled = false 34 | } 35 | 36 | func enableButtons() { 37 | sendImageToolbarItem.isEnabled = true 38 | } 39 | 40 | func updateNavigationButtons(for project: ProjectReference) { 41 | previousButton.isEnabled = project.canIncreaseFile() 42 | nextButton.isEnabled = project.canDecreaseFile() 43 | 44 | subtitle = project.indexDescription 45 | } 46 | 47 | func updateNavigationButtons( 48 | for project: ProjectReference, 49 | buildDuration: TimeInterval? 50 | ) { 51 | if let buildDuration = buildDuration { 52 | subtitle = "\(project.indexDescription), \(durationFormatter.string(from: buildDuration))" 53 | } else { 54 | subtitle = "\(project.indexDescription)" 55 | } 56 | } 57 | 58 | private let durationFormatter = DurationFormatter() 59 | 60 | 61 | func resizeWindowHeight(to newHeight: CGFloat) { 62 | let frame = CGRect(x: frame.minX, 63 | y: max(0, frame.midY - newHeight/2), 64 | width: frame.width, 65 | height: newHeight) 66 | 67 | setFrame(frame, display: true, animate: true) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /App/App/Sources/App/Menu/AppMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppMenu.swift 3 | // App 4 | // 5 | // Created by Mikhail Rubanov on 06.06.2022. 6 | // 7 | 8 | import AppKit 9 | import Filters 10 | 11 | public class AppMenu: NSMenu { 12 | public static func menu() -> NSMenu { 13 | var topLevelObjects: NSArray? = [] 14 | 15 | Bundle.module 16 | .loadNibNamed("Application", owner: self, topLevelObjects: &topLevelObjects) 17 | 18 | let menu = topLevelObjects?.filter { $0 is NSMenu }.first as! AppMenu 19 | 20 | return menu 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /App/BuildGraph/BuildDeps-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDocumentTypes 6 | 7 | 8 | CFBundleTypeExtensions 9 | 10 | xcactivitylog 11 | 12 | CFBundleTypeIconSystemGenerated 13 | 1 14 | CFBundleTypeName 15 | Build log 16 | CFBundleTypeRole 17 | Viewer 18 | LSHandlerRank 19 | Default 20 | 21 | 22 | CFBundleTypeIconSystemGenerated 23 | 1 24 | CFBundleTypeName 25 | Build Snapshot 26 | CFBundleTypeRole 27 | Editor 28 | LSHandlerRank 29 | Owner 30 | LSItemContentTypes 31 | 32 | com.akaDuality.bgbuildsnapshot 33 | 34 | 35 | 36 | CFBundleURLTypes 37 | 38 | 39 | 40 | NSSupportsSuddenTermination 41 | 42 | UTExportedTypeDeclarations 43 | 44 | 45 | UTTypeConformsTo 46 | 47 | com.apple.package 48 | 49 | UTTypeDescription 50 | Bulid Snapshot 51 | UTTypeIcons 52 | 53 | UTTypeIdentifier 54 | com.akaDuality.bgbuildsnapshot 55 | UTTypeTagSpecification 56 | 57 | public.filename-extension 58 | 59 | bgbuildsnapshot 60 | 61 | public.mime-type 62 | 63 | public.text 64 | 65 | 66 | 67 | 68 | UTImportedTypeDeclarations 69 | 70 | 71 | UTTypeConformsTo 72 | 73 | UTTypeDescription 74 | Build log 75 | UTTypeIcons 76 | 77 | UTTypeIconText 78 | 79 | 80 | UTTypeIdentifier 81 | com.akaDuality.xcactivitylog 82 | UTTypeTagSpecification 83 | 84 | public.filename-extension 85 | 86 | xcactivitylog 87 | 88 | public.mime-type 89 | 90 | public.text 91 | 92 | 93 | 94 | 95 | UTTypeConformsTo 96 | 97 | com.apple.package 98 | 99 | UTTypeDescription 100 | Bulid Snapshot 101 | UTTypeIcons 102 | 103 | UTTypeIdentifier 104 | com.akaDuality.bgbuildsnapshot 105 | UTTypeTagSpecification 106 | 107 | public.filename-extension 108 | 109 | bgbuildsnapshot 110 | 111 | public.mime-type 112 | 113 | application/buildGraph 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // BuildGraph 4 | // 5 | // Created by Mikhail Rubanov on 10.10.2021. 6 | // 7 | 8 | import Cocoa 9 | import Firebase 10 | import App 11 | 12 | class AppDelegate: CommonAppDelegate { 13 | 14 | override func applicationDidFinishLaunching(_ aNotification: Notification) { 15 | FirebaseApp.configure() 16 | 17 | super.applicationDidFinishLaunching(aNotification) 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "MacOS Icon 01@16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "MacOS Icon 01@32-1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "MacOS Icon 01@32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "MacOS Icon 01@64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "MacOS Icon 01@128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "MacOS Icon 01@256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "MacOS Icon 01@256-1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "MacOS Icon 01@512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "MacOS Icon 01@512-1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "MacOS Icon 01@1024-1.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@1024-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@1024-1.png -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@128.png -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@16.png -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@256-1.png -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@256.png -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@32-1.png -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@32.png -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@512-1.png -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@512.png -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/App/BuildGraph/BuildGraph/Assets.xcassets/AppIcon.appiconset/MacOS Icon 01@64.png -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/BuildGraph.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /App/BuildGraph/BuildGraph/main.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | let app = NSApplication.shared 4 | let delegate = AppDelegate() 5 | app.delegate = delegate 6 | 7 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) 8 | -------------------------------------------------------------------------------- /App/BuildGraph/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 964101858309-ah1vvm5qor358b0sk5lc52jjejqdild7.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.964101858309-ah1vvm5qor358b0sk5lc52jjejqdild7 9 | API_KEY 10 | AIzaSyCfHY701lV2l00uNA-Xi68yEsnM91_p3lo 11 | GCM_SENDER_ID 12 | 964101858309 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | com.akaDuality.BuildGraph 17 | PROJECT_ID 18 | build-graph 19 | STORAGE_BUCKET 20 | build-graph.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:964101858309:ios:4b80e9180c90eae24ca86c 33 | 34 | 35 | -------------------------------------------------------------------------------- /App/BuildGraphDebug/BuildGraphDebug/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // BuildGraph 4 | // 5 | // Created by Mikhail Rubanov on 10.10.2021. 6 | // 7 | 8 | import Cocoa 9 | import App 10 | 11 | class AppDelegate: CommonAppDelegate { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /App/BuildGraphDebug/BuildGraphDebug/BuildGraphDebug.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /App/BuildGraphDebug/BuildGraphDebug/Samples/Intel_i5.xcactivitylog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/App/BuildGraphDebug/BuildGraphDebug/Samples/Intel_i5.xcactivitylog -------------------------------------------------------------------------------- /App/BuildGraphDebug/BuildGraphDebug/Samples/M1.xcactivitylog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/App/BuildGraphDebug/BuildGraphDebug/Samples/M1.xcactivitylog -------------------------------------------------------------------------------- /App/BuildGraphDebug/BuildGraphDebug/main.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | let app = NSApplication.shared 4 | let delegate = AppDelegate() 5 | app.delegate = delegate 6 | 7 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) 8 | -------------------------------------------------------------------------------- /App/BuildGraphDebug/BuildGraphDebugUITests/BuildGraphDebugUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildGraphDebugUITests.swift 3 | // BuildGraphDebugUITests 4 | // 5 | // Created by Mikhail Rubanov on 05.06.2022. 6 | // 7 | 8 | import XCTest 9 | 10 | class BuildGraphDebugUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /App/BuildGraphDebug/BuildGraphDebugUITests/BuildGraphDebugUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildGraphDebugUITestsLaunchTests.swift 3 | // BuildGraphDebugUITests 4 | // 5 | // Created by Mikhail Rubanov on 05.06.2022. 6 | // 7 | 8 | import XCTest 9 | 10 | class BuildGraphDebugUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /BulidGraph.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /BulidGraph.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Domain/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Domain/.swiftpm/xcode/xcshareddata/xcschemes/Domain.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Domain/.swiftpm/xcode/xcshareddata/xcschemes/GraphParser.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Domain/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "gzipswift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/1024jp/GzipSwift", 7 | "state" : { 8 | "revision" : "7a7f17761c76a932662ab77028a4329f67d645a4", 9 | "version" : "5.2.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-custom-dump", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 16 | "state" : { 17 | "revision" : "505aa98716275fbd045d8f934fee3337c82ffbd3", 18 | "version" : "0.10.3" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-snapshot-testing", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 25 | "state" : { 26 | "revision" : "cef5b3f6f11781dd4591bdd1dd0a3d22bd609334", 27 | "version" : "1.11.0" 28 | } 29 | }, 30 | { 31 | "identity" : "xctest-dynamic-overlay", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 34 | "state" : { 35 | "revision" : "4af50b38daf0037cfbab15514a241224c3f62f98", 36 | "version" : "0.8.5" 37 | } 38 | } 39 | ], 40 | "version" : 2 41 | } 42 | -------------------------------------------------------------------------------- /Domain/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Domain", 7 | platforms: [.macOS("11.3")], 8 | products: [ 9 | .library( 10 | name: "Domain", 11 | targets: [ 12 | "BuildParser", 13 | "GraphParser", 14 | ]), 15 | .library( 16 | name: "Snapshot", 17 | targets: ["Snapshot"]), 18 | ], 19 | dependencies: [ 20 | .package(path: "./../XCLogParser"), 21 | .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "0.10.3"), 22 | .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.11.0"), 23 | ], 24 | targets: [ 25 | .target( 26 | name: "BuildParser", 27 | dependencies: [ 28 | "GraphParser", 29 | "XCLogParser", 30 | ]), 31 | .testTarget( 32 | name: "BuildParserTests", 33 | dependencies: [ 34 | "BuildParser", 35 | "Snapshot", 36 | .product(name: "SnapshotTesting", package: "swift-snapshot-testing", condition: nil), 37 | .product(name: "CustomDump", package: "swift-custom-dump", condition: nil), 38 | ]), 39 | .target( 40 | name: "GraphParser" 41 | ), 42 | .testTarget( 43 | name: "GraphParserTests", 44 | dependencies: [ 45 | "GraphParser", 46 | .product(name: "CustomDump", package: "swift-custom-dump", condition: nil), 47 | .product(name: "SnapshotTesting", package: "swift-snapshot-testing", condition: nil), 48 | ]), 49 | 50 | .target( 51 | name: "Snapshot", // TODO: rename to make test purpose explicit 52 | dependencies: [ 53 | "BuildParser", 54 | ], 55 | resources: [ 56 | .copy("Samples/IncrementalWithBigGap.bgbuildsnapshot"), 57 | .copy("Samples/PrepareBuildOnly.bgbuildsnapshot"), 58 | .copy("Samples/SimpleClean.bgbuildsnapshot"), 59 | .copy("Samples/Xcode14.3.bgbuildsnapshot") 60 | ] 61 | ), 62 | .testTarget( 63 | name: "SnapshotTests", 64 | dependencies: ["Snapshot"]), 65 | ] 66 | ) 67 | -------------------------------------------------------------------------------- /Domain/README.md: -------------------------------------------------------------------------------- 1 | # Domain 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/BuildParser.docc/BuildParser.md: -------------------------------------------------------------------------------- 1 | # ``BuildParser`` 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/CGColor+DarkMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 08.11.2021. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSAppearanceCustomization { 11 | @discardableResult 12 | public func performWithEffectiveAppearanceAsDrawingAppearance( 13 | _ block: () -> T) -> T { 14 | // Similar to `NSAppearance.performAsCurrentDrawingAppearance`, but 15 | // works below macOS 11 and assigns to `result` properly 16 | // (capturing `result` inside a block doesn't work the way we need). 17 | let result: T 18 | let old = NSAppearance.current 19 | NSAppearance.current = self.effectiveAppearance 20 | result = block() 21 | NSAppearance.current = old 22 | return result 23 | } 24 | } 25 | 26 | extension NSColor { 27 | /// Uses the `NSApplication.effectiveAppearance`. 28 | /// If you need per-view accurate appearance, prefer this instead: 29 | /// 30 | /// let cgColor = aView.performWithEffectiveAppearanceAsDrawingAppearance { aColor.cgColor } 31 | public var effectiveCGColor: CGColor { 32 | #if DEBUG 33 | let isTesting = NSClassFromString("XCTest") != nil 34 | if isTesting { 35 | return cgColor 36 | } 37 | #endif 38 | return NSApp.performWithEffectiveAppearanceAsDrawingAppearance { 39 | self.cgColor 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Colors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 10.10.2021. 6 | // 7 | 8 | import CoreGraphics 9 | import AppKit 10 | 11 | struct Colors { 12 | static var textColor: () -> CGColor = { NSColor.labelColor.effectiveCGColor } 13 | static var textOverModuleColor: () -> CGColor = { NSColor.labelColor.effectiveCGColor } 14 | static var textInvertedColor: () -> CGColor = { NSColor.labelColor.effectiveCGColor } 15 | static var backColor: () -> CGColor = { NSColor.clear.effectiveCGColor } 16 | 17 | static var liftColor: () -> CGColor = { NSColor.systemGray.withAlphaComponent(0.05).effectiveCGColor } 18 | static var concurencyColor: () -> CGColor = { NSColor.systemRed.effectiveCGColor } 19 | static var timeColor: () -> CGColor = { NSColor.tertiaryLabelColor.effectiveCGColor } 20 | 21 | static var clear: () -> CGColor = { NSColor.clear.effectiveCGColor } 22 | 23 | static var dimmingAlpha: Float = 0.25 24 | 25 | struct Dependency { 26 | static var critical: () -> CGColor = { NSColor.systemRed.effectiveCGColor } 27 | static var regular: () -> CGColor = { NSColor.systemGreen.effectiveCGColor } 28 | } 29 | 30 | struct Events { 31 | static var step: () -> CGColor = { NSColor.systemBrown.effectiveCGColor } 32 | static var cached: () -> CGColor = { NSColor.systemGray.effectiveCGColor } 33 | static var subtask: () -> CGColor = { NSColor.systemBlue.effectiveCGColor } 34 | static var background: () -> CGColor = { NSColor.systemGray.effectiveCGColor.copy(alpha: 0.25)! } 35 | 36 | static var legendBackground: () -> CGColor = { NSColor.systemGray.effectiveCGColor } 37 | 38 | static var legend: [ColorDescription] { 39 | [ 40 | (NSLocalizedString("Subtasks", comment: ""), subtask()), 41 | (NSLocalizedString("Task", comment: ""), step()), 42 | (NSLocalizedString("Waiting", comment: ""), background()), 43 | (NSLocalizedString("Cached", comment: ""), cached()), 44 | ] 45 | } 46 | } 47 | } 48 | 49 | typealias ColorDescription = (desc: String, color: CGColor) 50 | 51 | extension Event { 52 | var backgroundColor: CGColor { 53 | if steps.count == 0 { 54 | return Colors.Events.step() 55 | } 56 | 57 | // waiting 58 | return Colors.Events.background() 59 | } 60 | 61 | var subtaskColor: CGColor { 62 | if fetchedFromCache { 63 | return Colors.Events.cached() 64 | } 65 | 66 | return Colors.Events.subtask () 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/Analyzer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 09.10.2021. 6 | // 7 | 8 | import Foundation 9 | 10 | class Analyzer { 11 | func analyze(events: [Event]) { 12 | let testHelpers = events.filter("TestHelpers") 13 | printType(testHelpers, name: "TestHelpers") 14 | 15 | let units = events.filter("Unit-Tests") 16 | printType(units, name: "Unit-Tests") 17 | 18 | let other = events.filter { event in 19 | !(units.contains(event) || testHelpers.contains(event)) 20 | } 21 | 22 | printType(other, name: "Other") 23 | } 24 | 25 | func printType(_ events: [Event], name: String) { 26 | // printTime(events) 27 | printSummaryTime(events, name: name) 28 | } 29 | 30 | func printTime(_ events: [Event]) { 31 | for event in events { 32 | print("\(event.duration) \(event.taskName)") 33 | } 34 | } 35 | 36 | func printSummaryTime(_ events: [Event], name: String) { 37 | let sum = events.map { $0.duration }.reduce(0, +) 38 | print("sum \(sum), \(name)") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/BuildStepCounter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildStepCounter.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 23.02.2022. 6 | // 7 | 8 | import Foundation 9 | import XCLogParser 10 | 11 | public class BuildStepCounter { 12 | init(buildStep: BuildStep) { 13 | self.buildStep = buildStep 14 | } 15 | 16 | let buildStep: BuildStep 17 | 18 | public func duration(of stepType: DetailStepType) -> TimeInterval { 19 | buildStep.duration(of: stepType) 20 | } 21 | } 22 | 23 | extension BuildStep { 24 | func duration(of stepType: DetailStepType) -> TimeInterval { 25 | guard !subSteps.isEmpty else { 26 | if self.detailStepType == stepType { 27 | return duration 28 | } else { 29 | return 0 30 | } 31 | } 32 | 33 | return subSteps.reduce(0, { result, step in 34 | result + step.duration(of: stepType) 35 | }) 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/DateFormatter+8601.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter+8601.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 08.04.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension DateFormatter { 11 | public static let iso8601Full_Z: DateFormatter = { 12 | dateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ") 13 | }() 14 | 15 | static func dateFormatter(format: String) -> DateFormatter { 16 | let formatter = DateFormatter() 17 | formatter.dateFormat = format 18 | formatter.calendar = Calendar(identifier: .iso8601) 19 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 20 | formatter.locale = Locale(identifier: "en_US_POSIX") 21 | return formatter 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/Dto.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 11.10.2021. 6 | // 7 | 8 | import Foundation 9 | 10 | struct EventsDTO: Decodable { 11 | let events: [EventDTO] 12 | } 13 | 14 | struct EventDTO: Decodable { 15 | let date: Date 16 | let taskName: String 17 | let event: EventType 18 | } 19 | 20 | enum EventType: String, Decodable { 21 | case start 22 | case end 23 | } 24 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/DurationFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DurationFormatter.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 23.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public class DurationFormatter { 11 | 12 | public init() {} 13 | 14 | lazy var durationFormatter: DateComponentsFormatter = { 15 | let formatter = DateComponentsFormatter() 16 | formatter.unitsStyle = .abbreviated 17 | formatter.allowedUnits = [.minute, .second] 18 | formatter.zeroFormattingBehavior = .dropAll 19 | formatter.allowsFractionalUnits = true 20 | return formatter 21 | }() 22 | 23 | public func string(from ti: TimeInterval) -> String { 24 | guard ti >= 1 else { 25 | return formatMillisecons(from: ti) 26 | } 27 | 28 | return durationFormatter.string(from: ti) ?? "" 29 | } 30 | 31 | private func formatMillisecons(from ti: TimeInterval) -> String { 32 | if ti == 0 { return "" } 33 | if ti <= 0.001 { return "< 1ms" } 34 | if ti < 0.01 { return String(format: "%.2fs", ti) } 35 | if ti < 0.1 { return String(format: "%.2fs", ti) } 36 | if ti < 1 { return String(format: "%.1fs", ti) } 37 | 38 | return "" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/Event.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class Event: Equatable, Hashable { 4 | 5 | public init(taskName: String, 6 | startDate: Date, 7 | duration: TimeInterval, 8 | fetchedFromCache: Bool, 9 | steps: [Event]) { 10 | self.taskName = taskName 11 | self.startDate = startDate 12 | self.duration = duration 13 | self.fetchedFromCache = fetchedFromCache 14 | self.steps = steps 15 | } 16 | 17 | public let taskName: String 18 | public var startDate: Date // Can be moved in case of big gap 19 | public let duration: TimeInterval 20 | public var endDate: Date { 21 | startDate.addingTimeInterval(duration) 22 | } 23 | 24 | public let fetchedFromCache: Bool 25 | public let steps: [Event] 26 | 27 | public var parents: [Event] = [] 28 | 29 | public static func == (lhs: Event, rhs: Event) -> Bool { 30 | lhs.taskName == rhs.taskName 31 | // TODO: Add data 32 | } 33 | 34 | public func hash(into hasher: inout Hasher) { 35 | hasher.combine(taskName) 36 | } 37 | 38 | /// Parent check is heavy operation: a lot of string comparison and array allocations. Cache fix performance problems as a result 39 | var parentCheckResultCache = [String: Bool]() 40 | 41 | var checkedParentsProgress: [String] = [] 42 | 43 | public lazy var durationDescription: String = eventDescriptionFormatter.string(from: duration) 44 | 45 | public var fontSizeCache = [CGFloat: CGSize]() 46 | } 47 | 48 | private let eventDescriptionFormatter = DurationFormatter() 49 | 50 | extension Event: CustomDebugStringConvertible { 51 | public var debugDescription: String { 52 | "\(taskName) with \(steps.count) steps" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/FilterSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterSettings.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 08.04.2022. 6 | // 7 | 8 | import Foundation 9 | import XCLogParser 10 | 11 | public class FilterSettings { 12 | public static var shared = FilterSettings() 13 | 14 | public init() {} 15 | 16 | // @Storage(key: "showCached", defaultValue: true) 17 | // public var showCached: Bool 18 | // public var hideCached: Bool { 19 | // !showCached 20 | // } 21 | 22 | public var cacheVisibility: CacheVisibility { 23 | set { 24 | cacheVisibilityRaw = newValue.rawValue 25 | } 26 | 27 | get { 28 | CacheVisibility(rawValue: cacheVisibilityRaw)! 29 | } 30 | } 31 | 32 | @Storage(key: "cacheVisibility", defaultValue: CacheVisibility.currentBuild.rawValue) 33 | public var cacheVisibilityRaw: CacheVisibility.RawValue 34 | 35 | public var allowedTypes: [DetailStepType] = DetailStepType.compilationSteps 36 | 37 | public func add(stepType: DetailStepType) { 38 | allowedTypes.append(stepType) 39 | } 40 | 41 | public func remove(stepType: DetailStepType) { 42 | guard let indexToRemove = allowedTypes.firstIndex(of: stepType) else { 43 | return 44 | } 45 | allowedTypes.remove(at: indexToRemove) 46 | } 47 | 48 | public func enableAll() { 49 | allowedTypes = DetailStepType.allCases 50 | } 51 | 52 | public enum CacheVisibility: Int { 53 | case all 54 | case cached 55 | case currentBuild 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/FolderMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 13.01.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | class FolderMonitor { 11 | // MARK: Properties 12 | 13 | /// A file descriptor for the monitored directory. 14 | private var monitoredFolderFileDescriptor: CInt = -1 15 | /// A dispatch queue used for sending file changes in the directory. 16 | private let folderMonitorQueue = DispatchQueue(label: "FolderMonitorQueue", attributes: .concurrent) 17 | /// A dispatch source to monitor a file descriptor created from the directory. 18 | private var folderMonitorSource: DispatchSourceFileSystemObject? 19 | /// URL for the directory being monitored. 20 | let url: URL 21 | 22 | var folderDidChange: (() -> Void)? 23 | // MARK: Initializers 24 | init(url: URL) { 25 | self.url = url 26 | } 27 | // MARK: Monitoring 28 | /// Listen for changes to the directory (if we are not already). 29 | func startMonitoring() { 30 | guard folderMonitorSource == nil && monitoredFolderFileDescriptor == -1 else { 31 | return 32 | 33 | } 34 | // Open the directory referenced by URL for monitoring only. 35 | monitoredFolderFileDescriptor = open(url.path, O_EVTONLY) 36 | // Define a dispatch source monitoring the directory for additions, deletions, and renamings. 37 | folderMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredFolderFileDescriptor, eventMask: .write, queue: folderMonitorQueue) 38 | // Define the block to call when a file change is detected. 39 | folderMonitorSource?.setEventHandler { [weak self] in 40 | self?.folderDidChange?() 41 | } 42 | // Define a cancel handler to ensure the directory is closed when the source is cancelled. 43 | folderMonitorSource?.setCancelHandler { [weak self] in 44 | guard let strongSelf = self else { return } 45 | close(strongSelf.monitoredFolderFileDescriptor) 46 | strongSelf.monitoredFolderFileDescriptor = -1 47 | strongSelf.folderMonitorSource = nil 48 | } 49 | // Start monitoring the directory via the source. 50 | folderMonitorSource?.resume() 51 | } 52 | /// Stop listening for changes to the directory, if the source has been created. 53 | func stopMonitoring() { 54 | folderMonitorSource?.cancel() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/Parsing/Collection+ParallelMap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collection+ParallelMap.swift 3 | // BuildParser 4 | // 5 | // Created by Mikhail Rubanov on 20.05.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Collection { 11 | // Atother implementation https://talk.objc.io/episodes/S01E90-concurrent-map 12 | func parallelMap(_ transform: @escaping (Element) -> R) -> [R] { 13 | var res: [R?] = .init(repeating: nil, count: count) 14 | 15 | let lock = NSRecursiveLock() 16 | DispatchQueue.concurrentPerform(iterations: count) { i in 17 | let result = transform(self[index(startIndex, offsetBy: i)]) 18 | lock.lock() 19 | res[i] = result 20 | lock.unlock() 21 | } 22 | 23 | return res.map({ $0! }) 24 | } 25 | 26 | func parallelCompactMap(_ transform: @escaping (Element) -> R?) -> [R] { 27 | var res: [R?] = .init(repeating: nil, count: count) 28 | 29 | let lock = NSRecursiveLock() 30 | DispatchQueue.concurrentPerform(iterations: count) { i in 31 | if let result = transform(self[index(startIndex, offsetBy: i)]) { 32 | lock.lock() 33 | res[i] = result 34 | lock.unlock() 35 | } 36 | } 37 | 38 | return res.compactMap { $0 } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/Parsing/Collection+Safe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collection+Safe.swift 3 | // BuildParser 4 | // 5 | // Created by Mikhail Rubanov on 20.05.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Collection { 11 | /// Returns the element at the specified index if it is within bounds, otherwise nil. 12 | subscript (safe index: Index) -> Element? { 13 | return indices.contains(index) ? self[index] : nil 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/Project.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 08.04.2022. 6 | // 7 | 8 | import Foundation 9 | import GraphParser 10 | 11 | public class Project: Equatable { 12 | public static func == (lhs: Project, rhs: Project) -> Bool { 13 | lhs.events == rhs.events 14 | && lhs.relativeBuildStart == rhs.relativeBuildStart 15 | } 16 | 17 | public init(events: [Event], relativeBuildStart: CGFloat) { 18 | self.events = events 19 | self.relativeBuildStart = relativeBuildStart 20 | } 21 | 22 | public let events: [Event] 23 | public let relativeBuildStart: CGFloat 24 | 25 | public private(set) var cachedDependencies: [Dependency]? 26 | public func connect(dependencies: [Dependency]) { 27 | self.cachedDependencies = dependencies 28 | 29 | events.connect(by: dependencies) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/ProjectDescriptionService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectDescriptionService.swift 3 | // BuildDeps 4 | // 5 | // Created by Mikhail Rubanov on 23.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public class ProjectDescriptionService { 11 | public init() {} 12 | 13 | public func description(for project: ProjectReference) -> String { 14 | "\(project.name), \(dateDescription(for: project.currentActivityLog))" 15 | } 16 | 17 | public func dateDescription(for url: URL) -> String { 18 | guard let creationDate = try? url.resourceValues(forKeys: [.creationDateKey]).creationDate 19 | else { return url.lastPathComponent } 20 | 21 | return dateFormatter.string(from: creationDate) 22 | } 23 | 24 | lazy var dateFormatter: DateFormatter = { 25 | let formatter = DateFormatter() 26 | formatter.dateStyle = .short 27 | formatter.timeStyle = .short 28 | return formatter 29 | }() 30 | } 31 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/ProjectReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectReference.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 09.01.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public class ProjectReference: Equatable { 11 | public static func == (lhs: ProjectReference, rhs: ProjectReference) -> Bool { 12 | lhs.name == rhs.name 13 | && lhs.activityLogURL == rhs.activityLogURL 14 | } 15 | 16 | public init( 17 | name: String, 18 | rootPath: URL, 19 | activityLogURL: [URL] 20 | ) { 21 | precondition(activityLogURL.count > 0) 22 | 23 | self.currentActivityLogIndex = 0 24 | self.name = name 25 | self.rootPath = rootPath 26 | self.activityLogURL = activityLogURL 27 | } 28 | 29 | public let name: String 30 | public let rootPath: URL 31 | public let activityLogURL: [URL] 32 | 33 | // MARK: - Current File 34 | public var currentActivityLogIndex: Int 35 | public var currentActivityLog: URL { 36 | activityLogURL[currentActivityLogIndex] 37 | } 38 | 39 | public var indexDescription: String { 40 | "\(currentActivityLogIndex + 1) of \(activityLogURL.count)" 41 | } 42 | 43 | // MARK: Previous file 44 | public func canDecreaseFile() -> Bool { 45 | currentActivityLogIndex > 0 46 | } 47 | 48 | public func selectPreviousFile() { 49 | precondition(canDecreaseFile()) 50 | 51 | self.currentActivityLogIndex -= 1 52 | } 53 | 54 | // MARK: Next file 55 | public func canIncreaseFile() -> Bool { 56 | currentActivityLogIndex < activityLogURL.count - 1 57 | } 58 | 59 | public func selectNextFile() { 60 | precondition(canIncreaseFile()) 61 | 62 | self.currentActivityLogIndex += 1 63 | } 64 | 65 | static func shortName(from fileName: String) -> String { 66 | fileName.components(separatedBy: "-").dropLast().joined(separator: "-") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/ProjectReferenceFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectReferenceFactory.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 09.01.2022. 6 | // 7 | 8 | import Foundation 9 | import XCLogParser 10 | 11 | public class ProjectReferenceFactory { 12 | public init() {} 13 | 14 | public func projectReference( 15 | activityLogURL: URL 16 | ) -> ProjectReference { 17 | 18 | let folderWithName = activityLogURL.pathComponents[activityLogURL.pathComponents.count - 4] 19 | let name = ProjectReference.shortName(from: folderWithName) 20 | 21 | let rootPathForProject = activityLogURL 22 | .deletingLastPathComponent() // Skip Name of file 23 | .deletingLastPathComponent() // Skip Build folder 24 | .deletingLastPathComponent() // Skip Logs folder 25 | 26 | return ProjectReference(name: name, 27 | rootPath: rootPathForProject, 28 | activityLogURL: [activityLogURL]) 29 | } 30 | 31 | public func projectReference( 32 | folder: URL 33 | ) -> ProjectReference? { 34 | let logFinder = LogFinder(projectDir: folder) 35 | 36 | let fullName = folder.lastPathComponent 37 | let shortName = ProjectReference.shortName(from: fullName) 38 | 39 | do { 40 | let activityLogURL = try logFinder.activityLogs() 41 | let logsWithContent = logsWithContent(urls: activityLogURL) 42 | 43 | guard logsWithContent.count > 0 else { 44 | return nil 45 | } 46 | 47 | return ProjectReference(name: shortName, 48 | rootPath: folder, 49 | activityLogURL: logsWithContent) 50 | } catch { 51 | print("skip \(shortName), can't find .activityLog with build information") 52 | return nil 53 | } 54 | } 55 | 56 | public func projectReference( 57 | accessedDerivedDataURL: URL, 58 | fullName: String 59 | ) -> ProjectReference? { 60 | let rootPath = accessedDerivedDataURL.appendingPathComponent(fullName) 61 | return projectReference(folder: rootPath) 62 | } 63 | 64 | private func logsWithContent(urls: [URL]) -> [URL] { 65 | urls.compactMap { url in 66 | guard let attr = try? FileManager.default.attributesOfItem(atPath: url.path) else { 67 | return nil 68 | } 69 | 70 | guard (attr[.size] as? UInt64 ?? 0) > 0 else { 71 | return nil 72 | } 73 | 74 | return url 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/Projects/BookmarkSaver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkSaver.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 06.03.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | class BookmarkSaver { 11 | let userDefaults = UserDefaults.standard 12 | 13 | func saveDerivedDataBookmark(data: Data) { 14 | userDefaults.set(data, forKey: "derivedDataBookmark") 15 | } 16 | 17 | func derivedDataBookmark() -> Data? { 18 | userDefaults.value(forKey: "derivedDataBookmark") as? Data 19 | } 20 | 21 | // TODO: Union with UISettings 22 | } 23 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/Projects/ProjectsFinder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PathFinder.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 25.10.2021. 6 | // 7 | 8 | import Foundation 9 | import XCLogParser 10 | import Foundation 11 | 12 | public protocol ProjectsFinderProtocol { 13 | func projects(derivedDataPath: URL) throws -> [ProjectReference] 14 | func derivedDataPath() throws -> URL 15 | } 16 | 17 | public class ProjectsFinder: ProjectsFinderProtocol { 18 | 19 | public init() {} 20 | 21 | // MARK: Dependencies 22 | let fileAccess = FileAccess() 23 | let defaultDerivedData = DefaultDerivedData() 24 | 25 | // MARK: Not dependencies 26 | let fileManager = FileManager.default 27 | let projectReferenceFactory = ProjectReferenceFactory() 28 | 29 | public func projects(derivedDataPath: URL) throws -> [ProjectReference] { 30 | let hasAccess = derivedDataPath.startAccessingSecurityScopedResource() 31 | if !hasAccess { 32 | print("This directory might not need it, or this URL might not be a security scoped URL, or maybe something's wrong?") 33 | } 34 | defer { 35 | derivedDataPath.stopAccessingSecurityScopedResource() 36 | } 37 | 38 | let derivedDataContents = try fileManager 39 | .contentsOfDirectory(atPath: derivedDataPath.path) 40 | 41 | let result = filter(derivedDataContents) 42 | .compactMap { 43 | projectReferenceFactory 44 | .projectReference( 45 | accessedDerivedDataURL: derivedDataPath, 46 | fullName: $0) 47 | } 48 | 49 | return result 50 | } 51 | 52 | public func derivedDataPath() throws -> URL { 53 | guard let derivedDataURL = defaultDerivedData.getDerivedDataDir() else { 54 | throw Error.noDerivedData 55 | } 56 | 57 | // TODO: Handle file deprecation 58 | let derivedDataAccessURL = try fileAccess.promptForWorkingDirectoryPermission(directoryURL: derivedDataURL) 59 | return derivedDataAccessURL 60 | } 61 | 62 | func filter(_ names: [String]) -> [String] { 63 | return names 64 | .filter { $0.contains("-") } 65 | .filter { !$0.contains("Manifests") } // TODO: Is it Tuist? Remove from prod 66 | } 67 | 68 | public enum Error: Swift.Error { 69 | case noDerivedData 70 | case cantAccessResourceInScope 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/StateViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 21.08.2021. 6 | // 7 | 8 | import AppKit 9 | 10 | public typealias ViewController = NSViewController 11 | 12 | public protocol StateProtocol: Equatable { 13 | static var `default`: Self { get } 14 | } 15 | 16 | open class StateViewController: ViewController 17 | where State: StateProtocol { 18 | 19 | open var stateFactory: ((State) -> ViewController)! 20 | 21 | open override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | addController(for: state) 25 | } 26 | 27 | // MARK: State 28 | 29 | open var state: State = .default { 30 | didSet { 31 | let isChanged = state != oldValue 32 | if isChanged { 33 | removeCurrenIfExists() 34 | addController(for: state) 35 | } 36 | } 37 | } 38 | 39 | // MARK: Controller managment 40 | 41 | public private(set) weak var currentController: ViewController? 42 | 43 | private func addController(for state: State) { 44 | addNew(stateFactory(state)) 45 | } 46 | 47 | private func addNew(_ newController: ViewController) { 48 | addChild(newController) 49 | view.addSubview(newController.view) 50 | view.pinToBounds(newController.view) 51 | // newController.didMove(toParent: self) 52 | 53 | currentController = newController 54 | } 55 | 56 | private func removeCurrenIfExists() { 57 | if let currentController = currentController { 58 | // currentController.willMove(toParent: nil) 59 | currentController.view.removeFromSuperview() 60 | currentController.removeFromParent() 61 | } 62 | } 63 | } 64 | 65 | public extension NSView { 66 | func pinToBounds( 67 | _ view: NSView, 68 | with insets: NSEdgeInsets = .zero 69 | ) { 70 | view.translatesAutoresizingMaskIntoConstraints = false 71 | 72 | NSLayoutConstraint.activate([ 73 | view.topAnchor.constraint(equalTo: topAnchor, constant: insets.top), 74 | view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: insets.left), 75 | 76 | // To keep contentEdgeInsets positive 77 | trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: insets.right), 78 | bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: insets.bottom) 79 | ]) 80 | } 81 | } 82 | 83 | public extension NSEdgeInsets { 84 | static var zero: Self { 85 | return .init() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/Storage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Storage.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 08.04.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | @propertyWrapper 11 | public struct Storage { 12 | private let key: String 13 | private let defaultValue: T 14 | 15 | public init(key: String, defaultValue: T) { 16 | self.key = key 17 | self.defaultValue = defaultValue 18 | } 19 | 20 | let userDefaults = UserDefaults.standard 21 | 22 | public var wrappedValue: T { 23 | get { 24 | return userDefaults 25 | .object(forKey: key) as? T ?? defaultValue 26 | } 27 | set { 28 | userDefaults 29 | .set(newValue, forKey: key) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Domain/UISettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UISettings.swift 3 | // BuildGraph 4 | // 5 | // Created by Mikhail Rubanov on 26.10.2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public class UISettings { 11 | public init() {} 12 | 13 | // @Storage(key: "showSubtask", defaultValue: false) 14 | // public var showSubtask: Bool 15 | // 16 | // @Storage(key: "showLinks", defaultValue: false) 17 | // public var showLinks: Bool 18 | // 19 | // @Storage(key: "showPerformance", defaultValue: false) 20 | // public var showPerformance: Bool 21 | 22 | @Storage(key: "showLegend", defaultValue: true) 23 | public var showLegend: Bool 24 | 25 | @Storage(key: "textSize", defaultValue: 10) 26 | public var textSize: Int 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/EventRelativeRect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 10.10.2021. 6 | // 7 | 8 | import CoreGraphics 9 | import Foundation 10 | 11 | struct EventRelativeRect { 12 | let event: Event 13 | 14 | let text: String 15 | let start: CGFloat 16 | let duration: CGFloat 17 | 18 | init(event: Event, absoluteStart: Date, totalDuration: TimeInterval) { 19 | precondition(totalDuration != 0) 20 | 21 | self.event = event 22 | self.text = event.description 23 | self.start = relativeStart(absoluteStart: absoluteStart, 24 | start: event.startDate, 25 | duration: totalDuration) 26 | self.duration = relativeDuration(start: event.startDate, 27 | end: event.endDate, 28 | duration: totalDuration) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Layers/ColorDescriptionLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorDescriptionLayer.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 06.03.2022. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class ColorDescriptionLayer: CALayer { 12 | let colorLayer: CALayer 13 | let descriptionLayer: CATextLayer 14 | 15 | init(colorDescription: ColorDescription, scale: CGFloat) { 16 | self.colorLayer = CALayer() 17 | self.descriptionLayer = CATextLayer() 18 | 19 | super.init() 20 | 21 | colorLayer.backgroundColor = colorDescription.color 22 | colorLayer.contentsScale = scale 23 | addSublayer(colorLayer) 24 | 25 | descriptionLayer.contentsScale = scale 26 | descriptionLayer.string = colorDescription.desc 27 | descriptionLayer.foregroundColor = Colors.textColor() 28 | descriptionLayer.fontSize = 10 29 | addSublayer(descriptionLayer) 30 | 31 | colorLayer.cornerRadius = 2 32 | if #available(macOS 10.15, *) { 33 | colorLayer.cornerCurve = .continuous 34 | } 35 | } 36 | 37 | public override init(layer: Any) { 38 | let layer = layer as! ColorDescriptionLayer 39 | 40 | self.colorLayer = layer.colorLayer 41 | self.descriptionLayer = layer.descriptionLayer 42 | 43 | super.init(layer: layer) 44 | } 45 | 46 | required init?(coder: NSCoder) { 47 | fatalError("init(coder:) has not been implemented") 48 | } 49 | 50 | override func layoutSublayers() { 51 | super.layoutSublayers() 52 | 53 | colorLayer.frame = CGRect(x: 0, 54 | y: 0, 55 | width: 50, 56 | height: frame.height) 57 | descriptionLayer.frame = CGRect(x: colorLayer.frame.maxX + 8, 58 | y: 0, 59 | width: 150, 60 | height: frame.height) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Layers/ColorsLegendLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorsLegendLayer.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 19.01.2022. 6 | // 7 | 8 | import QuartzCore 9 | 10 | class ColorsLegendLayer: CALayer { 11 | 12 | var colorDescriptionLayers: [ColorDescriptionLayer] 13 | 14 | init(scale: CGFloat) { 15 | colorDescriptionLayers = [] 16 | 17 | for colorDescription in Colors.Events.legend { 18 | colorDescriptionLayers.append( 19 | ColorDescriptionLayer(colorDescription: colorDescription, 20 | scale: scale) 21 | ) 22 | } 23 | 24 | super.init() 25 | 26 | self.contentsScale = scale 27 | 28 | for subview in colorDescriptionLayers { 29 | subview.contentsScale = scale 30 | addSublayer(subview) 31 | } 32 | } 33 | 34 | var intrinsicContentSize: CGSize { 35 | return CGSize(width: 130, 36 | height: (lineHeight + space) * CGFloat(Colors.Events.legend.count) + inset * 2) 37 | } 38 | 39 | required init?(coder: NSCoder) { 40 | fatalError("init(coder:) has not been implemented") 41 | } 42 | 43 | public override init(layer: Any) { 44 | let layer = layer as! ColorsLegendLayer 45 | 46 | self.colorDescriptionLayers = layer.colorDescriptionLayers 47 | 48 | super.init(layer: layer) 49 | } 50 | 51 | override func layoutSublayers() { 52 | super.layoutSublayers() 53 | 54 | backgroundColor = Colors.Events.legendBackground().copy(alpha: 0.25) 55 | 56 | cornerRadius = 10 57 | if #available(macOS 10.15, *) { 58 | cornerCurve = .continuous 59 | } 60 | 61 | for (index, subview) in colorDescriptionLayers.enumerated() { 62 | 63 | subview.frame = CGRect(x: inset, 64 | y: inset + (lineHeight + 4) * CGFloat(index), 65 | width: frame.width - inset*2, 66 | height: lineHeight) 67 | } 68 | } 69 | 70 | let lineHeight: CGFloat = 14 71 | let inset: CGFloat = 8 72 | let space: CGFloat = 4 73 | } 74 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Layers/HUDLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 02.01.2022. 6 | // 7 | 8 | import QuartzCore 9 | 10 | public class HUDLayer: CALayer { 11 | private var timelineLayer: TimelineLayer 12 | private let legendLayer: ColorsLegendLayer 13 | 14 | private let eventsDuration: TimeInterval 15 | 16 | public init(duration: TimeInterval, legendIsHidden: Bool, scale: CGFloat) { 17 | self.eventsDuration = duration 18 | self.timelineLayer = TimelineLayer(eventsDuration: duration, scale: scale) 19 | 20 | self.legendIsHidden = legendIsHidden 21 | self.legendLayer = ColorsLegendLayer(scale: scale) 22 | 23 | // Time Layer 24 | super.init() 25 | 26 | addSublayer(timelineLayer) 27 | addSublayer(legendLayer) 28 | } 29 | 30 | public func scale(to magnification: CGFloat) { 31 | timelineLayer.removeFromSuperlayer() 32 | 33 | let newDuration = eventsDuration / magnification 34 | 35 | self.timelineLayer = TimelineLayer(eventsDuration: newDuration, scale: contentsScale) 36 | addSublayer(timelineLayer) 37 | self.setNeedsLayout() 38 | } 39 | 40 | public override init(layer: Any) { 41 | let layer = layer as! HUDLayer 42 | 43 | self.timelineLayer = layer.timelineLayer 44 | self.legendLayer = layer.legendLayer 45 | self.legendIsHidden = layer.legendIsHidden 46 | self.eventsDuration = layer.eventsDuration 47 | 48 | super.init(layer: layer) 49 | } 50 | 51 | public required init?(coder: NSCoder) { 52 | fatalError("init(coder:) has not been implemented") 53 | } 54 | 55 | public override func layoutSublayers() { 56 | super.layoutSublayers() 57 | 58 | let frame = superlayer?.bounds ?? bounds // strange zero-bounds when hide legend 59 | 60 | timelineLayer.frame = frame 61 | 62 | let height: CGFloat = legendLayer.intrinsicContentSize.height 63 | legendLayer.frame = CGRect(x: 20, 64 | y: frame.height - height - 60, 65 | width: legendLayer.intrinsicContentSize.width, 66 | height: height) 67 | 68 | 69 | legendLayer.isHidden = legendIsHidden 70 | } 71 | 72 | public var legendIsHidden: Bool 73 | 74 | public func drawTimeline(at coordinate: CGPoint) { 75 | timelineLayer.coordinate = coordinate 76 | } 77 | 78 | public func clearConcurrency() { 79 | timelineLayer.coordinate = nil 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Layers/PeriodsLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PeriodsLayer.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 13.10.2021. 6 | // 7 | 8 | import QuartzCore 9 | import Foundation 10 | 11 | class PeriodsLayer: CALayer { 12 | private let periods: [Period] 13 | private var periodsShapes: [CALayer] 14 | 15 | private let start: Date 16 | private let totalDuration: TimeInterval 17 | 18 | init(periods: [Period], start: Date, totalDuration: TimeInterval) { 19 | self.periods = periods 20 | self.periodsShapes = [CALayer]() 21 | self.start = start 22 | self.totalDuration = totalDuration 23 | super.init() 24 | setup() 25 | } 26 | 27 | public override init(layer: Any) { 28 | let layer = layer as! PeriodsLayer 29 | 30 | self.periods = layer.periods 31 | self.periodsShapes = layer.periodsShapes 32 | self.start = layer.start 33 | self.totalDuration = layer.totalDuration 34 | 35 | super.init(layer: layer) 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | private func setup() { 43 | for period in periods { 44 | let periodLayer = CALayer() 45 | let alpha: CGFloat = 1 / CGFloat(period.concurrency) 46 | periodLayer.backgroundColor = Colors.concurencyColor().copy(alpha: alpha / 4) 47 | periodsShapes.append(periodLayer) 48 | addSublayer(periodLayer) 49 | } 50 | } 51 | 52 | public override func layoutSublayers() { 53 | super.layoutSublayers() 54 | 55 | for (i, period) in periods.enumerated() { 56 | let layer = periodsShapes[i] 57 | 58 | let relativeStart = relativeStart(absoluteStart: start, 59 | start: period.start, 60 | duration: totalDuration) 61 | let relativeDuration = relativeDuration(start: period.start, 62 | end: period.end, 63 | duration: totalDuration) 64 | layer.frame = CGRect( 65 | x: relativeStart * self.frame.width, 66 | y: 0, 67 | width: relativeDuration * self.frame.width, 68 | height: frame.height 69 | ) 70 | } 71 | } 72 | 73 | static public let periodsHeight: CGFloat = 10 74 | } 75 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Layers/Vertical lines/BuildStartLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildStartLayer.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 04.04.2022. 6 | // 7 | 8 | import QuartzCore 9 | 10 | class BuildStartLayer: VerticalLineLayer { 11 | 12 | var relativeBuildStart: CGFloat = 0 { 13 | didSet { 14 | concurrencyTitle.string = NSLocalizedString("Build starts here", comment: "") 15 | color = Colors.Events.cached() 16 | } 17 | } 18 | 19 | public override init(scale: CGFloat) { 20 | super.init(scale: scale) 21 | } 22 | 23 | public override init(layer: Any) { 24 | let layer = layer as! BuildStartLayer 25 | 26 | self.relativeBuildStart = layer.relativeBuildStart 27 | 28 | super.init(layer: layer) 29 | } 30 | 31 | required init?(coder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | override func layoutSublayers() { 36 | super.layoutSublayers() 37 | 38 | self.coordinate = CGPoint(x: relativeBuildStart * bounds.width, y: 30) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Layers/Vertical lines/ConcurrencyLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConcurrencyLayer.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 13.10.2021. 6 | // 7 | 8 | import QuartzCore 9 | 10 | class ConcurrencyLayer: VerticalLineLayer { 11 | 12 | private let events: [Event] 13 | 14 | public init(events: [Event], scale: CGFloat) { 15 | self.events = events 16 | 17 | super.init(scale: scale) 18 | 19 | self.color = Colors.concurencyColor() 20 | } 21 | 22 | public override init(layer: Any) { 23 | let layer = layer as! ConcurrencyLayer 24 | 25 | self.events = layer.events 26 | 27 | super.init(layer: layer) 28 | } 29 | 30 | required init?(coder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | func drawConcurrency(at coordinate: CGPoint) { 35 | self.coordinate = coordinate 36 | updateLayoutWithoutAnimation() 37 | 38 | guard let duration = events.duration() else { 39 | return 40 | } 41 | let relativeX = coordinate.x / frame.width 42 | let time = duration * relativeX 43 | let concurency = events.concurrency(at: time) 44 | concurrencyTitle.string = "\(concurency)" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Domain/Sources/BuildParser/Layers/Vertical lines/VerticalLineLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerticalLineLayer.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 04.04.2022. 6 | // 7 | 8 | import QuartzCore 9 | 10 | class VerticalLineLayer: CALayer { 11 | 12 | var color = Colors.concurencyColor() { 13 | didSet { 14 | concurrencyLine.backgroundColor = color 15 | concurrencyTitle.foregroundColor = color 16 | } 17 | } 18 | 19 | private let concurrencyLine: CALayer 20 | let concurrencyTitle: CATextLayer 21 | 22 | public var coordinate: CGPoint? = nil { 23 | didSet { 24 | concurrencyHidden = coordinate == nil 25 | } 26 | } 27 | 28 | func updateLayoutWithoutAnimation() { 29 | updateWithoutAnimation { 30 | setNeedsLayout() 31 | layoutIfNeeded() 32 | } 33 | } 34 | 35 | private var concurrencyHidden: Bool = false { 36 | didSet { 37 | let opacity: Float = concurrencyHidden ? 0: 1 38 | concurrencyLine.opacity = opacity 39 | concurrencyTitle.opacity = opacity 40 | } 41 | } 42 | 43 | public init(scale: CGFloat) { 44 | self.concurrencyLine = CALayer() 45 | self.concurrencyTitle = CATextLayer() 46 | 47 | super.init() 48 | 49 | setup(scale: scale) 50 | } 51 | 52 | public override init(layer: Any) { 53 | let layer = layer as! VerticalLineLayer 54 | 55 | self.concurrencyLine = layer.concurrencyLine 56 | self.concurrencyTitle = layer.concurrencyTitle 57 | self.coordinate = layer.coordinate 58 | 59 | super.init(layer: layer) 60 | } 61 | 62 | required init?(coder: NSCoder) { 63 | fatalError("init(coder:) has not been implemented") 64 | } 65 | 66 | private func setup(scale: CGFloat) { 67 | concurrencyLine.contentsScale = scale 68 | addSublayer(concurrencyLine) 69 | 70 | concurrencyTitle.contentsScale = scale 71 | concurrencyTitle.fontSize = 20 72 | addSublayer(concurrencyTitle) 73 | } 74 | 75 | public override func layoutSublayers() { 76 | super.layoutSublayers() 77 | 78 | if let coordinate = coordinate { 79 | concurrencyHidden = false 80 | concurrencyLine.frame = CGRect(x: coordinate.x, 81 | y: 0, 82 | width: 1, 83 | height: frame.height) 84 | let titleHeight: CGFloat = 20 85 | let titleWidth: CGFloat = 150 86 | if coordinate.x < bounds.width - titleWidth - 10 { 87 | // Draw right 88 | concurrencyTitle.alignmentMode = .left 89 | concurrencyTitle.frame = CGRect(x: coordinate.x + 10, 90 | y: coordinate.y - titleHeight - 10, 91 | width: titleWidth, 92 | height: titleHeight) 93 | } else { 94 | // Dras left 95 | concurrencyTitle.alignmentMode = .right 96 | concurrencyTitle.frame = CGRect(x: coordinate.x - 10 - titleWidth, 97 | y: coordinate.y - titleHeight - 10, 98 | width: titleWidth, 99 | height: titleHeight) 100 | } 101 | } else { 102 | concurrencyHidden = true 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Domain/Sources/GraphParser/Dependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dependency.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 23.10.2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Dependency: Equatable { 11 | public init( 12 | target: Target, 13 | dependencies: [Target] 14 | ) { 15 | self.target = target 16 | self.dependencies = dependencies 17 | } 18 | 19 | public let target: Target 20 | public let dependencies: [Target] 21 | } 22 | 23 | public struct Target: Equatable { 24 | public init( 25 | target: String, 26 | project: String 27 | ) { 28 | self.target = target 29 | self.project = project 30 | } 31 | 32 | public let target: String 33 | public let project: String 34 | } 35 | 36 | public enum DependencyType: Equatable { 37 | // case explicit 38 | case implicit 39 | } 40 | -------------------------------------------------------------------------------- /Domain/Sources/GraphParser/DependencyParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DependencyParser.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 23.10.2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public class DependencyParser { 11 | 12 | public init() {} 13 | 14 | public func parse(path: URL) -> [Dependency]? { 15 | guard let depsContent = try? String(contentsOf: path) else { 16 | return nil 17 | } 18 | 19 | #if DEBUG 20 | print(depsContent) 21 | #endif 22 | return parseFile(depsContent) 23 | } 24 | 25 | public func parseFile(_ input: String) -> [Dependency] { 26 | let strings = input.components(separatedBy: "\n") 27 | .dropFirst() // No need in "Target dependency graph ..." 28 | 29 | let deps = dependenciesChunks(Array(strings)) 30 | .compactMap(dependency(from:)) 31 | return deps 32 | } 33 | 34 | func dependenciesChunks(_ strings: [String]) -> [[String]] { 35 | var chunkStartIndexes = [Int]() 36 | for (index, string) in strings.enumerated() { 37 | let startOfDependenciesChunk = string.contains(", ") 38 | if startOfDependenciesChunk { 39 | chunkStartIndexes.append(index) 40 | } 41 | } 42 | 43 | var result = [[String]]() 44 | for (i, index) in chunkStartIndexes 45 | .dropLast() 46 | .enumerated() 47 | { 48 | let nextIndex = chunkStartIndexes[i + 1] 49 | let range = index.. 0 else { return [] } 55 | 56 | result.append(Array(strings[chunkStartIndexes.last!...(strings.count - 1)])) 57 | 58 | return result 59 | } 60 | 61 | func dependency(from strings: [String]) -> Dependency? { 62 | var deps: [Target] = [] 63 | if strings[0].hasSuffix(", depends on:") { 64 | deps = strings.dropFirst().map(parseTarget(_:)) 65 | } 66 | 67 | let target = parseTarget(strings[0]) 68 | 69 | guard !deps.hasRecursiveDependencies(to: target) else { 70 | return nil 71 | } 72 | 73 | return Dependency( 74 | target: target, 75 | dependencies: deps) 76 | } 77 | 78 | func parseTarget(_ input: String) -> Target { 79 | let regex = try! NSRegularExpression( 80 | pattern: "(\\w*-?\\w*) in (\\w*-?\\w*)" 81 | ) 82 | 83 | let matche = regex 84 | .matches(in: input, 85 | options: [], 86 | range: input.fullRange) 87 | .first! 88 | 89 | return Target(target: matche.range(at: 1, in: input), 90 | project: matche.range(at: 2, in: input)) 91 | } 92 | } 93 | 94 | extension NSTextCheckingResult { 95 | func range(at index: Int, in content: String) -> String { 96 | let rangeInContent = Range(range(at: index), 97 | in: content)! 98 | 99 | return String(content[rangeInContent]) 100 | } 101 | } 102 | 103 | extension Array where Element == Target { 104 | func hasRecursiveDependencies(to target: Element) -> Bool { 105 | contains(where: { depTarget in 106 | return depTarget.target == target.target 107 | }) 108 | } 109 | } 110 | 111 | extension String { 112 | var fullRange: NSRange { 113 | return NSRange(location: 0, length: count) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Domain/Sources/GraphParser/GraphParser.docc/GraphParser.md: -------------------------------------------------------------------------------- 1 | # ``GraphParser`` 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` -------------------------------------------------------------------------------- /Domain/Sources/Snapshot/Samples/IncrementalWithBigGap.bgbuildsnapshot/Logs/Build/7D6B7C50-46EB-4C8C-B4AE-99590A734A2F.xcactivitylog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Sources/Snapshot/Samples/IncrementalWithBigGap.bgbuildsnapshot/Logs/Build/7D6B7C50-46EB-4C8C-B4AE-99590A734A2F.xcactivitylog -------------------------------------------------------------------------------- /Domain/Sources/Snapshot/Samples/PrepareBuildOnly.bgbuildsnapshot/Logs/Build/D31EC14E-52A6-4CDA-9ABA-1CACA0BC9ACE.xcactivitylog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Sources/Snapshot/Samples/PrepareBuildOnly.bgbuildsnapshot/Logs/Build/D31EC14E-52A6-4CDA-9ABA-1CACA0BC9ACE.xcactivitylog -------------------------------------------------------------------------------- /Domain/Sources/Snapshot/Samples/SimpleClean.bgbuildsnapshot/Build/Intermediates.noindex/XCBuildData/e9f65ec2d9f99e7a6246f6ec22f1e059-targetGraph.txt: -------------------------------------------------------------------------------- 1 | Target dependency graph (14 targets) 2 | CocoaDebug in CocoaDebug, no dependencies 3 | DAcquirers in DAcquirers, no dependencies 4 | NQueue in NQueue, no dependencies 5 | NCallback in NCallback, depends on: 6 | NQueue in NQueue (explicit) 7 | DCommon in DCommon, depends on: 8 | NCallback in NCallback (explicit) 9 | NQueue in NQueue (explicit) 10 | DFoundation in DFoundation, depends on: 11 | DCommon in DCommon (explicit) 12 | NRequest in NRequest, depends on: 13 | NCallback in NCallback (explicit) 14 | NQueue in NQueue (explicit) 15 | DNetwork in DNetwork, depends on: 16 | NQueue in NQueue (explicit) 17 | NRequest in NRequest (explicit) 18 | NInject in NInject, no dependencies 19 | DUIKit in DUIKit, depends on: 20 | DFoundation in DFoundation (explicit) 21 | NInject in NInject (explicit) 22 | NI18n in NI18n, no dependencies 23 | PaymentSDK in PaymentSDK, depends on: 24 | DAcquirers in DAcquirers (explicit) 25 | DCommon in DCommon (explicit) 26 | DFoundation in DFoundation (explicit) 27 | DNetwork in DNetwork (explicit) 28 | DUIKit in DUIKit (explicit) 29 | NInject in NInject (explicit) 30 | NQueue in NQueue (explicit) 31 | Pods-CommonTarget-SDK-ios in Pods, depends on: 32 | CocoaDebug in CocoaDebug (explicit) 33 | DAcquirers in DAcquirers (explicit) 34 | DCommon in DCommon (explicit) 35 | DFoundation in DFoundation (explicit) 36 | DNetwork in DNetwork (explicit) 37 | DUIKit in DUIKit (explicit) 38 | NCallback in NCallback (explicit) 39 | NI18n in NI18n (explicit) 40 | NInject in NInject (explicit) 41 | NQueue in NQueue (explicit) 42 | NRequest in NRequest (explicit) 43 | PaymentSDK in PaymentSDK (explicit) 44 | SDK-ios in PaymentSDK, depends on: 45 | Pods-CommonTarget-SDK-ios in Pods (implicit dependency via file 'Pods_CommonTarget_SDK_ios.framework' in build phase 'Link Binary') 46 | CocoaDebug in CocoaDebug (implicit dependency via options '-framework CocoaDebug' in build setting 'OTHER_LDFLAGS') 47 | DAcquirers in DAcquirers (implicit dependency via options '-framework DAcquirers' in build setting 'OTHER_LDFLAGS') 48 | DCommon in DCommon (implicit dependency via options '-framework DCommon' in build setting 'OTHER_LDFLAGS') 49 | DFoundation in DFoundation (implicit dependency via options '-framework DFoundation' in build setting 'OTHER_LDFLAGS') 50 | DNetwork in DNetwork (implicit dependency via options '-framework DNetwork' in build setting 'OTHER_LDFLAGS') 51 | DUIKit in DUIKit (implicit dependency via options '-framework DUIKit' in build setting 'OTHER_LDFLAGS') 52 | NCallback in NCallback (implicit dependency via options '-framework NCallback' in build setting 'OTHER_LDFLAGS') 53 | NI18n in NI18n (implicit dependency via options '-framework NI18n' in build setting 'OTHER_LDFLAGS') 54 | NInject in NInject (implicit dependency via options '-framework NInject' in build setting 'OTHER_LDFLAGS') 55 | NQueue in NQueue (implicit dependency via options '-framework NQueue' in build setting 'OTHER_LDFLAGS') 56 | NRequest in NRequest (implicit dependency via options '-framework NRequest' in build setting 'OTHER_LDFLAGS') 57 | PaymentSDK in PaymentSDK (implicit dependency via options '-framework PaymentSDK' in build setting 'OTHER_LDFLAGS') -------------------------------------------------------------------------------- /Domain/Sources/Snapshot/Samples/SimpleClean.bgbuildsnapshot/Logs/Build/F5F5EB7C-FD56-4037-9959-7056E5363FCD.xcactivitylog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Sources/Snapshot/Samples/SimpleClean.bgbuildsnapshot/Logs/Build/F5F5EB7C-FD56-4037-9959-7056E5363FCD.xcactivitylog -------------------------------------------------------------------------------- /Domain/Sources/Snapshot/Samples/Xcode14.3.bgbuildsnapshot/Logs/Build/F8AB69B8-6496-4C72-A4F2-C14465EF248B.xcactivitylog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Sources/Snapshot/Samples/Xcode14.3.bgbuildsnapshot/Logs/Build/F8AB69B8-6496-4C72-A4F2-C14465EF248B.xcactivitylog -------------------------------------------------------------------------------- /Domain/Sources/Snapshot/Samples/Xcode16.3.bgbuildsnapshot/Logs/Build/B5B813F0-47C9-4857-A605-6CEAE1E726D9.xcactivitylog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Sources/Snapshot/Samples/Xcode16.3.bgbuildsnapshot/Logs/Build/B5B813F0-47C9-4857-A605-6CEAE1E726D9.xcactivitylog -------------------------------------------------------------------------------- /Domain/Sources/Snapshot/Snapshot.docc/Snapshot.md: -------------------------------------------------------------------------------- 1 | # ``Snapshot`` 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` -------------------------------------------------------------------------------- /Domain/Sources/Snapshot/TestBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestBundle.swift 3 | // BuildParserTests 4 | // 5 | // Created by Mikhail Rubanov on 29.04.2022. 6 | // 7 | 8 | import Foundation 9 | import BuildParser 10 | 11 | public class TestBundle { 12 | 13 | public init() {} 14 | 15 | public var simpleClean: XcodeBuildSnapshot { 16 | try! snapshot(name: "SimpleClean") 17 | } 18 | 19 | public var xcode14_3: XcodeBuildSnapshot { 20 | try! snapshot(name: "Xcode14.3") 21 | } 22 | 23 | public func snapshot(name: String) throws -> XcodeBuildSnapshot { 24 | let url = Bundle.module 25 | .url(forResource: name, 26 | withExtension: "bgbuildsnapshot")! 27 | 28 | return try XcodeBuildSnapshot(url: url) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/DepsPathExtractionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DepsPathExtractionTests.swift 3 | // BuildParserTests 4 | // 5 | // Created by Mikhail Rubanov on 20.05.2023. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | @testable import BuildParser 11 | 12 | // MARK: - New version 13 | class DepsPathExtractionTests: XCTestCase { 14 | 15 | func test_pathFromSectionText() { 16 | let sut = DepsPathExtraction() 17 | let sectionText = "Build description signature: 9bf52d084a567b4e77074ec13b3364ab\rBuild description path: /Users/mikhail/Library/Developer/Xcode/DerivedData/DodoPizza-grkipvsgskordwctxiipkuhrvwfb/Build/Intermediates.noindex/ArchiveIntermediates/DodoPizza/IntermediateBuildFilesPath/XCBuildData/9bf52d084a567b4e77074ec13b3364ab.xcbuilddata\r" 18 | let path = sut.path(sectionText: sectionText) 19 | 20 | XCTAssertNotNil(path) 21 | 22 | let expectedURL = URL(fileURLWithPath: "/Users/mikhail/Library/Developer/Xcode/DerivedData/DodoPizza-grkipvsgskordwctxiipkuhrvwfb/Build/Intermediates.noindex/ArchiveIntermediates/DodoPizza/IntermediateBuildFilesPath/XCBuildData/9bf52d084a567b4e77074ec13b3364ab.xcbuilddata/target-graph.txt") 23 | XCTAssertEqual(path, expectedURL) 24 | } 25 | 26 | // Don't know what is exact version. Probably Xcode 14.3-15 27 | func testNumberExtraction2() { 28 | let sut = DepsPathExtraction() 29 | let sectionText = "Build description signature: 56b40cd4d98fa9450c9d285b429da116\rBuild description path: /Users/mikhail/Library/Developer/Xcode/DerivedData/VoiceOver_Designer-hdaabgdqcsawyycaqzpnlvclcdrb/Build/Intermediates.noindex/XCBuildData/56b40cd4d98fa9450c9d285b429da116.xcbuilddata\r" 30 | let path = sut.path(sectionText: sectionText) 31 | 32 | let expectedURL = URL(fileURLWithPath: "/Users/mikhail/Library/Developer/Xcode/DerivedData/VoiceOver_Designer-hdaabgdqcsawyycaqzpnlvclcdrb/Build/Intermediates.noindex/XCBuildData/56b40cd4d98fa9450c9d285b429da116.xcbuilddata/target-graph.txt") 33 | XCTAssertEqual(path, expectedURL) 34 | } 35 | } 36 | 37 | // MARK: - Old version 38 | class DepsPathExtractionTests_old: XCTestCase { 39 | func testNumberExtraction() { 40 | let result = DepsPathExtraction_old(rootURL: URL(fileURLWithPath: "root")) 41 | .number(from: "Build description signature: b4416238eb7eecbe4969bbd303f28fe5\rBuild description path: /Users/rubanov/Library/Developer/Xcode/DerivedData/CodeMetrics-aegjnninizgadzcfxjaecrwuhtfu/Build/Intermediates.noindex/XCBuildData/b4416238eb7eecbe4969bbd303f28fe5-desc.xcbuild\r") 42 | 43 | XCTAssertEqual(result, "b4416238eb7eecbe4969bbd303f28fe5") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/DurationFormatterTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DurationFormatterTest.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 23.02.2022. 6 | // 7 | 8 | import XCTest 9 | import BuildParser 10 | 11 | class DurationFormatterTest: XCTestCase { 12 | 13 | let formatter = DurationFormatter() 14 | 15 | func test_0sec() { 16 | XCTAssertEqual(formatter.string(from: 0), "") 17 | } 18 | 19 | func test_0_00001sec() { 20 | XCTAssertEqual(formatter.string(from: 0.00001), "< 1ms") 21 | } 22 | 23 | func test_0_001sec() { 24 | XCTAssertEqual(formatter.string(from: 0.001), "< 1ms") 25 | } 26 | 27 | func test_0_010004sec_shouldRoundUpTo0_01() { 28 | XCTAssertEqual(formatter.string(from: 0.01004), "0.01s") 29 | } 30 | 31 | func test_0_01sec() { 32 | XCTAssertEqual(formatter.string(from: 0.01), "0.01s") 33 | } 34 | 35 | func test_0_1sec() { 36 | XCTAssertEqual(formatter.string(from: 0.1), "0.1s") 37 | } 38 | 39 | func test_1sec() { 40 | XCTAssertEqual(formatter.string(from: 1), "1s") 41 | } 42 | 43 | func test_60sec() { 44 | XCTAssertEqual(formatter.string(from: 60), "1m") 45 | } 46 | 47 | func test_64_1sec() { 48 | XCTAssertEqual(formatter.string(from: 64.1), "1m 4s") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/EventDepsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventDepsTests.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 05.11.2021. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | import CustomDump 11 | 12 | @testable import BuildParser 13 | import GraphParser 14 | 15 | class EventDepsTests: XCTestCase { 16 | 17 | let target1 = Target(target: "1", project: "1") 18 | let target2 = Target(target: "2", project: "2") 19 | let target3 = Target(target: "3", project: "3") 20 | 21 | var events: [Event]! 22 | 23 | override func setUp() { 24 | super.setUp() 25 | 26 | events = [ 27 | .testMake(taskName: "1"), 28 | .testMake(taskName: "2"), 29 | .testMake(taskName: "3") 30 | ] 31 | } 32 | 33 | func test_2contains1() { 34 | let deps = [ 35 | Dependency(target: target1, dependencies: []), 36 | Dependency(target: target2, dependencies: [target1]) 37 | ] 38 | 39 | events.connect(by: deps) 40 | 41 | XCTAssertEqual(events[0].parents, []) 42 | XCTAssertEqual(events[1].parents, [events[0]]) 43 | } 44 | 45 | 46 | func test_thirdContainsFirstTwo() { 47 | let deps = [ 48 | Dependency(target: target1, dependencies: []), 49 | Dependency(target: target2, dependencies: []), 50 | Dependency(target: target3, dependencies: [ 51 | target1, 52 | target2 53 | ]) 54 | ] 55 | 56 | events.connect(by: deps) 57 | 58 | XCTAssertEqual(events[0].parents, []) 59 | XCTAssertEqual(events[1].parents, []) 60 | XCTAssertNoDifference(events[2].parents, [events[0], events[1]]) 61 | 62 | XCTAssertFalse(events[0].parentsContains("1")) 63 | 64 | XCTAssertTrue(events[2].parentsContains("1")) 65 | XCTAssertTrue(events[2].parentsContains("2")) 66 | 67 | // TODO: Test longer chain 68 | } 69 | 70 | func test_twoDependenciesInChain() { 71 | let deps = [ 72 | Dependency(target: target1, dependencies: []), 73 | Dependency(target: target2, dependencies: [target1]), 74 | Dependency(target: target3, dependencies: [ 75 | target2 76 | ]) 77 | ] 78 | 79 | events.connect(by: deps) 80 | 81 | XCTAssertEqual(events[0].parents, []) 82 | XCTAssertEqual(events[1].parents, [events[0]]) 83 | XCTAssertNoDifference(events[2].parents, [events[1]]) 84 | 85 | XCTAssertFalse(events[0].parentsContains("1")) 86 | XCTAssertTrue(events[1].parentsContains("1")) 87 | XCTAssertTrue(events[2].parentsContains("1")) 88 | XCTAssertTrue(events[2].parentsContains("2")) 89 | } 90 | 91 | func test_dependenciesLoop() { 92 | let deps = [ 93 | Dependency(target: target1, dependencies: [target2]), 94 | Dependency(target: target2, dependencies: [target1]), 95 | ] 96 | 97 | events.connect(by: deps) 98 | 99 | XCTAssertTrue(events[0].parentsContains("2")) 100 | XCTAssertTrue(events[1].parentsContains("1")) 101 | } 102 | } 103 | 104 | extension Event { 105 | static func testMake( 106 | taskName: String = "", 107 | startDate: Date = Date(), 108 | duration: TimeInterval = 0, 109 | fetchedFromCache: Bool = false, 110 | steps: [Event] = [] 111 | ) -> Event { 112 | Event(taskName: taskName, startDate: startDate, duration: duration, fetchedFromCache: fetchedFromCache, steps: steps) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/EventTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 10.10.2021. 6 | // 7 | 8 | import XCTest 9 | @testable import BuildParser 10 | 11 | final class EventTest: XCTestCase { 12 | func event(name: String) -> Event { 13 | Event(taskName: name, 14 | startDate: Date(), 15 | duration: 0, 16 | fetchedFromCache: false, 17 | steps: []) 18 | } 19 | 20 | func test_ClearDomain() { 21 | let event = event(name: "Crypto") 22 | XCTAssertEqual(event.domain, "Crypto") 23 | XCTAssertEqual(event.type, .framework) 24 | } 25 | 26 | func test_HelpersDomain() { 27 | let event = event(name: "CryptoTestHelpers") 28 | XCTAssertEqual(event.domain, "Crypto") 29 | XCTAssertEqual(event.type, .helpers) 30 | } 31 | 32 | func test_TestDomain() { 33 | let event = event(name: "CryptoTestHelpers-Unit-Tests") 34 | XCTAssertEqual(event.domain, "Crypto") 35 | XCTAssertEqual(event.type, .tests) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/GraphTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 12.10.2021. 6 | // 7 | 8 | import XCTest 9 | @testable import BuildParser 10 | import SnapshotTesting 11 | import Snapshot 12 | 13 | final class GraphTests: XCTestCase { 14 | 15 | let record = false 16 | 17 | let parser = RealBuildLogParser() 18 | 19 | // MARK: DSL 20 | private func layer(snapshotName: String, filter: FilterSettings) throws -> CALayer { 21 | let snapshot = try TestBundle().snapshot(name: snapshotName) 22 | 23 | let project = try parser.parse(projectReference: snapshot.project, 24 | filter: filter) 25 | 26 | let layer = AppLayer(events: project.events, relativeBuildStart: 0, fontSize: 10, scale: 1) 27 | 28 | layer.frame = CGRect(x: 0, 29 | y: 0, 30 | width: 500, 31 | height: layer.intrinsicContentSize.height) 32 | layer.backgroundColor = CGColor.white 33 | 34 | return layer 35 | } 36 | 37 | private func snapshot(name: String, 38 | filter: FilterSettings, 39 | testName: String = #function, 40 | file: StaticString = #file, 41 | line: UInt = #line) throws { 42 | let layer = try layer(snapshotName: name, filter: filter) 43 | 44 | assertSnapshot(matching: layer, 45 | as: .image, 46 | record: record, 47 | file: file, 48 | testName: testName, 49 | line: line) 50 | } 51 | 52 | // MARK: Tests 53 | func test_drawingSimpleClean() throws { 54 | try snapshot(name: "SimpleClean", filter: .shared) 55 | } 56 | 57 | func test_drawingIncrementalWithGap_everything() throws { 58 | try snapshot(name: "IncrementalWithBigGap", filter: .all) 59 | } 60 | 61 | func test_drawingIncrementalWithGap_cached() throws { 62 | try snapshot(name: "IncrementalWithBigGap", filter: .cached) 63 | } 64 | 65 | func test_drawingIncrementalWithGap_currentBuild() throws { 66 | try snapshot(name: "IncrementalWithBigGap", filter: .currentBuld) 67 | } 68 | } 69 | 70 | extension FilterSettings { 71 | static var currentBuld: FilterSettings { 72 | let filter = FilterSettings() 73 | filter.cacheVisibility = .currentBuild 74 | return filter 75 | } 76 | 77 | static var all: FilterSettings { 78 | let filter = FilterSettings() 79 | filter.cacheVisibility = .all 80 | return filter 81 | } 82 | 83 | static var cached: FilterSettings { 84 | let filter = FilterSettings() 85 | filter.cacheVisibility = .cached 86 | return filter 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/PathFinderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PathFinderTests.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 30.10.2021. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | import XCLogParser 11 | import CustomDump 12 | 13 | @testable import BuildParser 14 | 15 | class PathFinderTests: XCTestCase { 16 | 17 | var sut: ProjectsFinder! 18 | 19 | func test_searchProjects() throws { 20 | let files = [ 21 | "Manifests-gngpennuzxpepwgywnlojtwalfno", 22 | "BuildTime-eknojwbtcdfjuxfipboqpabjuzsy", 23 | "DodoPizzaTuist-fmlmdqfbrolxgjanbelteljkwvns", 24 | ".DS_Store", 25 | "ci", 26 | "SymbolCache.noindex", 27 | "doner-mobile-ios-frqradomrxpjpuesrfiztrcjbvhf", 28 | "Unsaved_Xcode_Document-ffgwvkfhkycgbqayujppkkmvzhlp",] 29 | 30 | let projects = sut.filter(files) 31 | 32 | XCTAssertNoDifference(projects, [ 33 | "BuildTime-eknojwbtcdfjuxfipboqpabjuzsy", 34 | "DodoPizzaTuist-fmlmdqfbrolxgjanbelteljkwvns", 35 | "doner-mobile-ios-frqradomrxpjpuesrfiztrcjbvhf", 36 | "Unsaved_Xcode_Document-ffgwvkfhkycgbqayujppkkmvzhlp", 37 | ]) 38 | } 39 | 40 | // TODO: Restore 41 | // func test_searchTargetGraph() throws { 42 | // let projectDir = URL(string: "/Users/rubanov/Library/Developer/Xcode/DerivedData")! 43 | // 44 | // let path = try sut.targetGraph(projectDir: projectDir) 45 | // 46 | // XCTAssertNoDifference( 47 | // URL(string: "/Users/rubanov/Library/Developer/Xcode/DerivedData/Build/Intermediates.noIndex/XCBuildData/hnthnt-targetGraph.txt"), 48 | // path) 49 | // } 50 | 51 | override func setUp() { 52 | super.setUp() 53 | 54 | // let url = URL(string: "/Users/rubanov/Library/Developer/Xcode/DerivedData/Build/Intermediates.noIndex/XCBuildData/hnthnt-targetGraph.txt")! 55 | 56 | sut = ProjectsFinder( 57 | // logOptions: logOptions, 58 | // fileScanner: scannerFake, 59 | // logFinder: LogFinder(fileManager: FileManager.default) 60 | ) 61 | } 62 | } 63 | 64 | //class LatestFileScannerFake: LatestFileScannerProtocol { 65 | // 66 | // var files: [URL] = [] 67 | // 68 | // func findLatestForProject( 69 | // inDir directory: URL, 70 | // filter: (URL) -> Bool 71 | // ) throws -> URL { 72 | // return files.filter(filter).first! 73 | // } 74 | //} 75 | -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/TimelineLayerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 06.01.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import BuildParser 10 | import SnapshotTesting 11 | 12 | final class TimelineLayerTests: XCTestCase { 13 | let record = false 14 | 15 | func test2Min() { 16 | let layer = layer(minutes: 2) 17 | 18 | assertSnapshot(matching: layer, 19 | as: .image, 20 | record: record) 21 | } 22 | 23 | func test10Min() { 24 | let layer = layer(minutes: 10) 25 | 26 | assertSnapshot(matching: layer, 27 | as: .image, 28 | record: record) 29 | } 30 | 31 | func test20Min() { 32 | let layer = layer(minutes: 20) 33 | 34 | assertSnapshot(matching: layer, 35 | as: .image, 36 | record: record) 37 | } 38 | 39 | func test60Min() { 40 | let layer = layer(minutes: 60) 41 | 42 | assertSnapshot(matching: layer, 43 | as: .image, 44 | record: record) 45 | } 46 | 47 | private func layer(minutes: TimeInterval) -> TimelineLayer { 48 | let layer = TimelineLayer(eventsDuration: 60*minutes, scale: 1) 49 | 50 | layer.frame = .init(x: 0, 51 | y: 0, 52 | width: 300, 53 | height: 300) 54 | return layer 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/XCLogParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 25.10.2021. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | import BuildParser 11 | 12 | class XCLogParserTests: XCTestCase { 13 | 14 | func test_realBuildLogParser() throws { 15 | #warning("Fake and test") 16 | // let events = try RealBuildLogParser().parse(projectName: "DodoPizza") 17 | // 18 | // XCTAssertEqual(events.count, 92) 19 | 20 | // for substep in buildSteps.subSteps { 21 | // print("\(substep.title), \(substep.duration)") 22 | // } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/__Snapshots__/BuildParserTests/test_drawingTestEvents.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Tests/BuildParserTests/__Snapshots__/BuildParserTests/test_drawingTestEvents.1.png -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/__Snapshots__/GraphTests/test_drawingIncrementalWithGap_cached.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Tests/BuildParserTests/__Snapshots__/GraphTests/test_drawingIncrementalWithGap_cached.1.png -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/__Snapshots__/GraphTests/test_drawingIncrementalWithGap_currentBuild.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Tests/BuildParserTests/__Snapshots__/GraphTests/test_drawingIncrementalWithGap_currentBuild.1.png -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/__Snapshots__/GraphTests/test_drawingIncrementalWithGap_everything.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Tests/BuildParserTests/__Snapshots__/GraphTests/test_drawingIncrementalWithGap_everything.1.png -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/__Snapshots__/GraphTests/test_drawingSimpleClean.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Tests/BuildParserTests/__Snapshots__/GraphTests/test_drawingSimpleClean.1.png -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/__Snapshots__/TimelineLayerTests/test10Min.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Tests/BuildParserTests/__Snapshots__/TimelineLayerTests/test10Min.1.png -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/__Snapshots__/TimelineLayerTests/test20Min.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Tests/BuildParserTests/__Snapshots__/TimelineLayerTests/test20Min.1.png -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/__Snapshots__/TimelineLayerTests/test2Min.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Tests/BuildParserTests/__Snapshots__/TimelineLayerTests/test2Min.1.png -------------------------------------------------------------------------------- /Domain/Tests/BuildParserTests/__Snapshots__/TimelineLayerTests/test60Min.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akaDuality/build-graph-for-xcode/16422319a14026d88489126d7d1912d31d261487/Domain/Tests/BuildParserTests/__Snapshots__/TimelineLayerTests/test60Min.1.png -------------------------------------------------------------------------------- /Domain/Tests/SnapshotTests/SnapshotTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnapshotTests.swift 3 | // SnapshotTests 4 | // 5 | // Created by Mikhail Rubanov on 29.05.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import Snapshot 10 | 11 | class SnapshotTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build Graph for Xcode 2 | 3 | **Build graph** – the essential tool to analyze the compilation time of a multi-module app. 4 | 5 | The app shows the compilation order for modules and links between them, allowing the filter of any step. 6 | 7 | > Verified for Xcode 16.3 8 | 9 | [More info](https://rubanov.dev/build-graph/) 10 | 11 | HeaderLight 12 | 13 | ## How to use 14 | - Open Xcode with your project, clean, and build from scratch 15 | - Launch Build Graph, give access to read files at Derived Data 16 | - Select your project in the left panel 17 | 18 | ## Modules Structure 19 | - Build Graph Release – target for release, including FireBase. 20 | - Build Graph Debug – target for development purpose 21 | 22 | Modules: 23 | - `App` – joins feature modules in the app 24 | - `UI` – main app features 25 | - `Projects` – left panel with project selection 26 | - `Details` – center panel that draws a graph 27 | - `Filters`– right panel that allows to filter graph 28 | - `Domain` – infrastructure module with parsing domain 29 | - `BuildParser` – reads `.xcactivitylog` files with compilation information 30 | - `GraphParser` – reads `target-graph` file with dependencies between modules 31 | - `Snapshot` – samples of different Xcode's builds 32 | - `XCLogParser` – fork that doesn't parse String and stays on Substring as long as possible to speedup the parsing 33 | 34 | ## Contribution 35 | Each new version of Xcode can break parsing. You can try to fix and suggest PR. 36 | -------------------------------------------------------------------------------- /UI/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /UI/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "gzipswift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/1024jp/GzipSwift", 7 | "state" : { 8 | "revision" : "7a7f17761c76a932662ab77028a4329f67d645a4", 9 | "version" : "5.2.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-custom-dump", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 16 | "state" : { 17 | "revision" : "505aa98716275fbd045d8f934fee3337c82ffbd3", 18 | "version" : "0.10.3" 19 | } 20 | }, 21 | { 22 | "identity" : "xctest-dynamic-overlay", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 25 | "state" : { 26 | "revision" : "4af50b38daf0037cfbab15514a241224c3f62f98", 27 | "version" : "0.8.5" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /UI/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "UI", 7 | platforms: [.macOS("11.3")], 8 | products: [ 9 | .library( 10 | name: "UI", 11 | targets: [ 12 | "Details", 13 | "Filters", 14 | "Projects", 15 | ]), 16 | ], 17 | dependencies: [ 18 | .package(path: "./../Domain"), 19 | .package(path: "./../XCLogParser"), 20 | .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "0.10.3"), 21 | ], 22 | targets: [ 23 | .target( 24 | name: "Details", 25 | dependencies: [ 26 | "Domain", 27 | "XCLogParser", 28 | ]), 29 | .testTarget( 30 | name: "DetailsTests", 31 | dependencies: [ 32 | "Details", 33 | .product(name: "Snapshot", package: "Domain"), 34 | .product(name: "CustomDump", package: "swift-custom-dump", condition: nil), 35 | ]), 36 | 37 | .target( 38 | name: "Filters", 39 | dependencies: [ 40 | "Domain", 41 | ]), 42 | .testTarget( 43 | name: "FiltersTests", 44 | dependencies: [ 45 | "Filters", 46 | ]), 47 | 48 | .target( 49 | name: "Projects", 50 | dependencies: [ 51 | "Domain", 52 | ]), 53 | .testTarget( 54 | name: "ProjectsTests", 55 | dependencies: [ 56 | "Projects", 57 | ]), 58 | ] 59 | ) 60 | -------------------------------------------------------------------------------- /UI/README.md: -------------------------------------------------------------------------------- 1 | # UI 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /UI/Sources/Details/Details/HUDScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HUDScrollView.swift 3 | // BuildGraph 4 | // 5 | // Created by Mikhail Rubanov on 06.03.2022. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import BuildParser 11 | 12 | class HUDScrollView: NSScrollView { 13 | var hudLayer: HUDLayer? 14 | 15 | // func observeScrollChange() { 16 | //// allowsMagnification = false // Here is a problem with smooth zoom by touchpad 17 | // 18 | // contentView.postsBoundsChangedNotifications = true 19 | // NotificationCenter.default.addObserver(self, 20 | // selector: #selector(didScrollContent), 21 | // name: NSView.boundsDidChangeNotification, 22 | // object: nil) 23 | // } 24 | // 25 | // @objc func didScrollContent() { 26 | //// hudLayer?.updateWithoutAnimation { 27 | //// hudLayer?.frame = contentView.bounds.offsetBy(dx: 0, dy: 52) // TODO: Remove hardcode 28 | //// } 29 | // 30 | // hudLayer?.updateScale(to: magnification) 31 | // } 32 | } 33 | -------------------------------------------------------------------------------- /UI/Sources/Details/Details/HUDView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HUDView.swift 3 | // BuildGraph 4 | // 5 | // Created by Mikhail Rubanov on 06.03.2022. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import BuildParser 11 | 12 | class HUDView: FlippedView { 13 | var hudLayer: HUDLayer? 14 | 15 | required init?(coder: NSCoder) { 16 | super.init(coder: coder) 17 | } 18 | 19 | func setup(duration: TimeInterval, legendIsHidden: Bool, scale: CGFloat) { 20 | hudLayer?.removeFromSuperlayer() // remove previous one 21 | 22 | hudLayer = HUDLayer(duration: duration, 23 | legendIsHidden: legendIsHidden, 24 | scale: scale) 25 | wantsLayer = true 26 | layer?.addSublayer(hudLayer!) 27 | } 28 | 29 | override func layout() { 30 | super.layout() 31 | 32 | layer?.updateWithoutAnimation { 33 | self.hudLayer?.frame = bounds 34 | self.hudLayer?.layoutIfNeeded() 35 | } 36 | } 37 | 38 | override func updateLayer() { 39 | super.updateLayer() 40 | 41 | hudLayer?.setNeedsLayout() 42 | } 43 | 44 | override func hitTest(_ point: NSPoint) -> NSView? { 45 | nil 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /UI/Sources/Details/Details/MouseContorller.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MouseContorller.swift 3 | // BuildGraph 4 | // 5 | // Created by Mikhail Rubanov on 06.03.2022. 6 | // 7 | 8 | import AppKit 9 | 10 | class MouseContorller: NSResponder { 11 | 12 | var trackingArea: NSTrackingArea! 13 | var view: DetailView! 14 | 15 | func addMouseTracking(to view: DetailView) { 16 | self.view = view 17 | 18 | trackingArea = NSTrackingArea( 19 | rect: view.bounds, 20 | options: [.activeAlways, 21 | .mouseMoved, 22 | .mouseEnteredAndExited, 23 | .inVisibleRect], 24 | owner: self, 25 | userInfo: nil) 26 | view.addTrackingArea(trackingArea) 27 | } 28 | 29 | override func mouseEntered(with event: NSEvent) { 30 | view.window?.acceptsMouseMovedEvents = true 31 | view.window?.makeFirstResponder(self) 32 | } 33 | 34 | override func mouseMoved(with event: NSEvent) { 35 | let coordinate = view.contentView.convert( 36 | event.locationInWindow, 37 | from: nil) 38 | 39 | highlightEvent(at: coordinate) 40 | } 41 | 42 | override func mouseExited(with event: NSEvent) { 43 | view.window?.acceptsMouseMovedEvents = false 44 | 45 | removeHighlightedEvent() 46 | } 47 | 48 | func highlightEvent(at coordinate: CGPoint) { 49 | view.modulesLayer?.highlightEvent(at: coordinate) 50 | view.modulesLayer?.drawConcurrencyLine(at: coordinate) 51 | view.hudView.hudLayer?.drawTimeline(at: coordinate) 52 | } 53 | 54 | func removeHighlightedEvent() { 55 | view.modulesLayer?.clearHighlightedEvent() 56 | view.modulesLayer?.clearConcurrency() 57 | view.hudView.hudLayer?.clearConcurrency() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /UI/Sources/Details/Details/ZoomController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZoomController.swift 3 | // BuildDeps 4 | // 5 | // Created by Mikhail Rubanov on 13.02.2022. 6 | // 7 | 8 | import AppKit 9 | 10 | protocol ZoomDelegate: AnyObject { 11 | func didZoom(to magnification: CGFloat) 12 | } 13 | 14 | class ZoomController { 15 | init(scrollView: NSScrollView, 16 | delegate: ZoomDelegate, 17 | scaleFactor: CGFloat = NSScreen.main!.backingScaleFactor) { 18 | self.scrollView = scrollView 19 | self.screenScaleFactor = scaleFactor 20 | self.delegate = delegate 21 | } 22 | 23 | func observeScrollChange() { 24 | scrollView.contentView.postsBoundsChangedNotifications = true 25 | NotificationCenter.default.addObserver(self, 26 | selector: #selector(didScrollContent), 27 | name: NSView.boundsDidChangeNotification, 28 | object: nil) 29 | } 30 | 31 | @objc func didScrollContent() { 32 | delegate?.didZoom(to: magnification) 33 | } 34 | 35 | let scrollView: NSScrollView 36 | let screenScaleFactor: CGFloat 37 | weak var delegate: ZoomDelegate? 38 | 39 | func zoomIn() { 40 | zoom(to: magnification + magnificationStep) 41 | } 42 | 43 | func zoomOut() { 44 | zoom(to: magnification - magnificationStep) 45 | } 46 | 47 | var magnificationStep: CGFloat { 48 | if magnification <= 1 { 49 | return 0.25 50 | } else { 51 | return 0.5 52 | } 53 | } 54 | 55 | var magnification: CGFloat { 56 | scrollView.magnification 57 | } 58 | 59 | var center: NSPoint { 60 | NSPoint(x: scrollView.bounds.midX, 61 | y: scrollView.bounds.midY) 62 | } 63 | 64 | private func zoom(to magnification: CGFloat) { 65 | scrollView.setMagnification(magnification, 66 | centeredAt: center) 67 | 68 | updateScale(to: magnification * screenScaleFactor) 69 | 70 | delegate?.didZoom(to: magnification) 71 | } 72 | 73 | private func updateScale(to scale: CGFloat) { 74 | scrollView.contentView.layer?.updateScale(to: scale) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /UI/Sources/Details/FileLocationSelector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileLocationSelector.swift 3 | // Details 4 | // 5 | // Created by Mikhail Rubanov on 26.04.2022. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import BuildParser 11 | 12 | class FileLocationSelector { 13 | func requestLocation( 14 | project: ProjectReference?, 15 | title: String, 16 | fileExtension: String, 17 | then completion: @escaping (URL) -> Void 18 | ) { 19 | let savePanel = NSSavePanel() 20 | savePanel.canCreateDirectories = true 21 | savePanel.showsTagField = false 22 | savePanel.nameFieldStringValue = "\(fileName(for: project, title: title)).\(fileExtension)" 23 | savePanel.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.modalPanelWindow))) 24 | savePanel.begin { (result) in 25 | if result == .OK { 26 | completion(savePanel.url!) 27 | } 28 | } 29 | } 30 | 31 | private func fileName(for project: ProjectReference?, title: String?) -> String { 32 | guard let project = project else { 33 | return (title ?? Date().description) 34 | } 35 | 36 | return ProjectDescriptionService().description(for: project) 37 | } 38 | } 39 | 40 | fileprivate extension String { 41 | var appedingPngFormat: Self { 42 | (self) + ".png" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /UI/Sources/Details/ImageSaveService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageSaveService.swift 3 | // BuildGraph 4 | // 5 | // Created by Mikhail Rubanov on 31.10.2021. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import BuildParser 11 | 12 | class ImageSaveService { 13 | func saveImage(url: URL, view: NSView) { 14 | let previousColor = view.layer?.backgroundColor 15 | defer { 16 | view.layer?.backgroundColor = previousColor 17 | } 18 | 19 | view.layer?.backgroundColor = NSColor.textBackgroundColor.effectiveCGColor 20 | 21 | writeToFile(url: url, view: view) 22 | } 23 | 24 | private func writeToFile(url: URL, view: NSView) { 25 | let rep = view.bitmapImageRepForCachingDisplay(in: view.bounds)! 26 | view.cacheDisplay(in: view.bounds, to: rep) 27 | 28 | let img = NSImage(size: view.bounds.size) 29 | img.addRepresentation(rep) 30 | 31 | let png = UIImagePNGRepresentation(img) 32 | 33 | do { 34 | try png?.write(to: url) 35 | } catch let error { 36 | print(error) 37 | } 38 | } 39 | } 40 | 41 | public func UIImagePNGRepresentation(_ image: NSImage) -> Data? { 42 | guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) 43 | else { return nil } 44 | let imageRep = NSBitmapImageRep(cgImage: cgImage) 45 | imageRep.size = image.size // display size in points 46 | return imageRep.representation(using: .png, properties: [:]) 47 | } 48 | -------------------------------------------------------------------------------- /UI/Sources/Details/SnapshotSaveService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnapshotSaveService.swift 3 | // Details 4 | // 5 | // Created by Mikhail Rubanov on 26.04.2022. 6 | // 7 | 8 | import Foundation 9 | import BuildParser 10 | 11 | class SnapshotSaveService { 12 | func save(project: ProjectReference, to url: URL) { 13 | let document = XcodeBuildSnapshot(project: project) 14 | 15 | do { 16 | document.save(to: url, ofType: XcodeBuildSnapshot.bgbuildsnapshot, for: .saveOperation) { error in 17 | 18 | } 19 | } catch let _ { 20 | // TODO: Handle error 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /UI/Sources/Details/States/LoadingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingViewController.swift 3 | // BuildDeps 4 | // 5 | // Created by Mikhail Rubanov on 03.01.2022. 6 | // 7 | 8 | import AppKit 9 | 10 | class LoadingViewController: NSViewController { 11 | @IBOutlet var progressIndicator: NSProgressIndicator! 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | progressIndicator.startAnimation(self) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /UI/Sources/Details/States/RetryViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RetryViewController.swift 3 | // BuildDeps 4 | // 5 | // Created by Mikhail Rubanov on 03.01.2022. 6 | // 7 | 8 | import AppKit 9 | 10 | class RetryViewController: NSViewController { 11 | 12 | @IBOutlet weak var titleLabel: NSTextField! 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /UI/Sources/Filters/DetailStepTypeDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailStepTypeDescription.swift 3 | // BuildDeps 4 | // 5 | // Created by Mikhail Rubanov on 23.02.2022. 6 | // 7 | 8 | import XCLogParser 9 | 10 | extension DetailStepType { 11 | var title: String { 12 | switch self { 13 | // Compile 14 | case .cCompilation: return "C" 15 | 16 | case .swiftCompilation: return "Swift" 17 | 18 | case .compileAssetsCatalog: return "Asset catalogs" 19 | 20 | case .compileStoryboard: return "Storyboard" 21 | 22 | case .XIBCompilation: return "Xib" 23 | 24 | case .swiftAggregatedCompilation: return "Swift Aggregated Compilation" 25 | 26 | // Non-compile 27 | case .scriptExecution: return "Script execution" 28 | 29 | case .createStaticLibrary: return "Create static library" 30 | 31 | case .linker: return "Linker" 32 | 33 | case .copySwiftLibs: return "Copy Swift Libs" 34 | 35 | case .writeAuxiliaryFile: return "Write auxiliary file" 36 | 37 | case .linkStoryboards: return "Link storyboards" 38 | 39 | case .copyResourceFile: return "Copy resourve files" 40 | 41 | case .mergeSwiftModule: return "Merge Swift module" 42 | 43 | case .precompileBridgingHeader: return "Precompile Bridging Header" 44 | 45 | case .other: return "Other" 46 | 47 | case .none: return "Unknown" 48 | 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /UI/Sources/Filters/Filters.xcodeproj/xcshareddata/xcschemes/Filters.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /UI/Sources/Projects/NoAccessViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoAccessViewController.swift 3 | // BuildGraph 4 | // 5 | // Created by Mikhail Rubanov on 06.03.2022. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class NoAccessViewController: NSViewController { 12 | weak var delegate: NoProjectsDelegate? 13 | 14 | @IBAction func changeDerivedDataDidPress(_ sender: Any) { 15 | delegate?.requestAccessAndReloadProjects() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /UI/Sources/Projects/NoProjectsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoProjectsViewController.swift 3 | // BuildDeps 4 | // 5 | // Created by Mikhail Rubanov on 23.02.2022. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import BuildParser 11 | 12 | protocol NoProjectsDelegate: AnyObject { 13 | func requestAccessAndReloadProjects() 14 | func reloadProjetcs() 15 | } 16 | 17 | class NoProjectsViewController: NSViewController { 18 | 19 | weak var delegate: NoProjectsDelegate? 20 | 21 | @IBAction func refreshDidPress(_ sender: Any) { 22 | delegate?.reloadProjetcs() 23 | } 24 | 25 | @IBAction func changeDerivedDataDidPress(_ sender: Any) { 26 | delegate?.requestAccessAndReloadProjects() 27 | } 28 | 29 | func view() -> NoProjectsView { 30 | view as! NoProjectsView 31 | } 32 | } 33 | 34 | class NoProjectsView: NSView { 35 | var derivedData: URL! { 36 | didSet { 37 | derivedDataPath = derivedData.path 38 | } 39 | } 40 | 41 | fileprivate var derivedDataPath: String! { 42 | didSet { 43 | pathLabel.stringValue = derivedDataPath 44 | } 45 | } 46 | 47 | @IBOutlet weak var pathLabel: NSTextField! 48 | } 49 | -------------------------------------------------------------------------------- /UI/Sources/Projects/ProjectSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectSettings.swift 3 | // Projects 4 | // 5 | // Created by Mikhail Rubanov on 22.05.2022. 6 | // 7 | 8 | import BuildParser 9 | import Foundation 10 | 11 | protocol ProjectSettingsProtocol { 12 | var selectedProject: String? { get set } 13 | } 14 | 15 | public class ProjectSettings: ProjectSettingsProtocol { 16 | public init() {} 17 | 18 | @Storage(key: "selectedProject", defaultValue: nil) 19 | public var selectedProject: String? 20 | 21 | public func removeSelectedProject() { 22 | UserDefaults.standard.removeObject(forKey: "selectedProject") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /UI/Sources/Projects/Projects.xcodeproj/xcshareddata/xcschemes/Projects.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /UI/Sources/Projects/ProjectsStateViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectsStateViewController.swift 3 | // BuildDeps 4 | // 5 | // Created by Mikhail Rubanov on 23.02.2022. 6 | // 7 | 8 | import Foundation 9 | import BuildParser 10 | import AppKit 11 | 12 | public enum ProjectsState: StateProtocol { 13 | case loading 14 | case empty(_ derivedDataURL: URL) 15 | case projects(_ selectedProject: ProjectReference?) 16 | case noAccessToDerivedData 17 | 18 | public static var `default`: Self = .loading 19 | } 20 | 21 | public class ProjectsStateViewController: StateViewController, ProjectsUI { 22 | 23 | public var presenter: ProjectsPresenter! 24 | 25 | public required init?(coder: NSCoder) { 26 | super.init(coder: coder) 27 | 28 | self.stateFactory = { state in 29 | let storyboard = NSStoryboard(name: "Projects", bundle: Bundle.module) 30 | 31 | switch state { 32 | case .loading: 33 | return storyboard.instantiateController(withIdentifier: "loading") as! ViewController 34 | 35 | case .empty(let derivedDataURL): 36 | let controller = storyboard.instantiateController(withIdentifier: "empty") as! NoProjectsViewController 37 | controller.view().derivedData = derivedDataURL 38 | controller.delegate = self.presenter 39 | return controller 40 | 41 | case .projects(let selectedProject): 42 | let controller = storyboard.instantiateController(withIdentifier: "projects") as! ProjectsOutlineViewController 43 | controller.presenter = self.presenter 44 | 45 | if let selectedProject = selectedProject { 46 | controller.select(project: selectedProject) 47 | } 48 | 49 | return controller 50 | case .noAccessToDerivedData: 51 | let controller = storyboard.instantiateController(withIdentifier: "noAccess") as! NoAccessViewController 52 | controller.delegate = self.presenter 53 | return controller 54 | } 55 | } 56 | } 57 | } 58 | 59 | extension ProjectsPresenter: NoProjectsDelegate {} 60 | -------------------------------------------------------------------------------- /UI/Tests/FiltersTests/FiltersTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FiltersTests.swift 3 | // FiltersTests 4 | // 5 | // Created by Mikhail Rubanov on 16.04.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import Filters 10 | 11 | class FiltersTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /UI/Tests/ProjectsTests/Doubles/DerivedDataStub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DerivedDataStub.swift 3 | // ProjectsTests 4 | // 5 | // Created by Mikhail Rubanov on 22.05.2022. 6 | // 7 | 8 | import BuildParser 9 | import Foundation 10 | 11 | class DerivedDataStub { 12 | let derivedData = URL(fileURLWithPath: "/Users/rubanov/Library/Developer/Xcode/DerivedData") 13 | 14 | var buildGraph: ProjectReference { 15 | let rootPath = derivedData.appendingPathComponent("BulidGraph-dwlksaohfylpdedqejrvuuglqzeo") 16 | 17 | return ProjectReference( 18 | name: "Build Graph", 19 | rootPath: rootPath, 20 | activityLogURL: [ 21 | rootPath.appendingPathComponent("Logs/Build/0539119C-C9F6-4C93-9FD4-C9B5E6DCCFC9.xcactivitylog")] 22 | ) 23 | } 24 | 25 | var mobileBank: ProjectReference { 26 | let rootPath = derivedData.appendingPathComponent("MB-dadwdawdwafewfew") 27 | 28 | return ProjectReference( 29 | name: "Mobile Bank", 30 | rootPath: rootPath, 31 | activityLogURL: [ 32 | rootPath.appendingPathComponent("Logs/Build/3452119C-C9F6-3213-9FD4-C9B5E6DCCFC9.xcactivitylog")] 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /UI/Tests/ProjectsTests/Doubles/ProjectsFinderMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectsFinderMock.swift 3 | // ProjectsTests 4 | // 5 | // Created by Mikhail Rubanov on 22.05.2022. 6 | // 7 | 8 | import BuildParser 9 | import Foundation 10 | 11 | class ProjectsFinderMock: ProjectsFinderProtocol { 12 | var projects = [ProjectReference]() 13 | func projects(derivedDataPath: URL) throws -> [ProjectReference] { 14 | return projects 15 | } 16 | 17 | var derivedDataPathResult: Result = .failure(ProjectsFinder.Error.noDerivedData) 18 | func derivedDataPath() throws -> URL { 19 | switch derivedDataPathResult { 20 | case .success(let url): return url 21 | case .failure(let error): throw error 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /UI/Tests/ProjectsTests/Doubles/ProjectsSelectionDelegateMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectsSelectionDelegateMock.swift 3 | // ProjectsTests 4 | // 5 | // Created by Mikhail Rubanov on 22.05.2022. 6 | // 7 | 8 | import Projects 9 | import BuildParser 10 | 11 | class ProjectsSelectionDelegateMock: ProjectsSelectionDelegate { 12 | var selectedProject: ProjectReference? 13 | var selectedProjectCount = 0 14 | func select(project: ProjectReference) { 15 | selectedProjectCount += 1 16 | self.selectedProject = project 17 | } 18 | 19 | var didSelectNothing = false 20 | func selectNothing() { 21 | self.didSelectNothing = true 22 | } 23 | 24 | // TODO: В тестах проще проверять вызов одного метода, можно сделать проект опциональным (обратно, да)) 25 | } 26 | -------------------------------------------------------------------------------- /UI/Tests/ProjectsTests/Doubles/ProjectsUIMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectsUIMock.swift 3 | // ProjectsTests 4 | // 5 | // Created by Mikhail Rubanov on 22.05.2022. 6 | // 7 | 8 | import Projects 9 | 10 | class ProjectsUIMock: ProjectsUI { 11 | var state: ProjectsState = .default { 12 | didSet { 13 | states.append(state) 14 | } 15 | } 16 | 17 | var states: [ProjectsState] = [] 18 | } 19 | -------------------------------------------------------------------------------- /UI/Tests/ProjectsTests/XCTest+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTest+Extensions.swift 3 | // ProjectsTests 4 | // 5 | // Created by Danila Ferentz on 18.06.22. 6 | // 7 | 8 | import XCTest 9 | 10 | public extension XCTest { 11 | 12 | @discardableResult 13 | func name(_ value: String) -> XCTest { 14 | XCTContext.runActivity(named: value, block: { _ in }) 15 | return self 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /XCLogParser/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /XCLogParser/NOTICE: -------------------------------------------------------------------------------- 1 | XcLogParser 2 | Copyright 2019 Spotify AB 3 | 4 | This product includes software developed by the "Marcin Krzyzanowski" (http://krzyzanowskim.com/): 5 | https://github.com/krzyzanowskim/CryptoSwift 6 | -------------------------------------------------------------------------------- /XCLogParser/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Gzip", 6 | "repositoryURL": "https://github.com/1024jp/GzipSwift", 7 | "state": { 8 | "branch": null, 9 | "revision": "7a7f17761c76a932662ab77028a4329f67d645a4", 10 | "version": "5.2.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /XCLogParser/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "XCLogParser", 8 | platforms: [.macOS(.v10_13)], 9 | products: [ 10 | .library(name: "XCLogParser", targets: ["XCLogParser"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/1024jp/GzipSwift", from: "5.1.0"), 14 | ], 15 | targets: [ 16 | .target( 17 | name: "XCLogParser", 18 | dependencies: ["Gzip"] 19 | ), 20 | .testTarget( 21 | name: "XCLogParserTests", 22 | dependencies: ["XCLogParser"] 23 | ), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/XCLogParserError.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | public enum XCLogParserError: LocalizedError { 23 | case invalidLogHeader(String) 24 | case invalidLine(String) 25 | case errorCreatingReport(String) 26 | case wrongLogManifestFile(String, String) 27 | case parseError(String) 28 | 29 | public var errorDescription: String? { return description } 30 | } 31 | 32 | extension XCLogParserError: CustomStringConvertible { 33 | public var description: String { 34 | switch self { 35 | case .invalidLogHeader(let path): 36 | return "The file in \(path) is not a valid SLF log" 37 | case .invalidLine(let line): 38 | return "The line \(line) doesn't seem like a valid SLF line" 39 | case .errorCreatingReport(let error): 40 | return "Can't create the report: \(error)" 41 | case .wrongLogManifestFile(let path, let error): 42 | return "There was an error reading the latest build time " + 43 | " from the file \(path). Error: \(error)" 44 | case .parseError(let message): 45 | return "Error parsing the log: \(message)" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/extensions/ArrayExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | extension Array where Element: Hashable { 23 | 24 | func removingDuplicates() -> [Element] { 25 | var addedDict = [Element: Bool]() 26 | return filter { 27 | addedDict.updateValue(true, forKey: $0) == nil 28 | } 29 | } 30 | 31 | } 32 | 33 | extension Array where Element: Notice { 34 | 35 | func getWarnings() -> [Notice] { 36 | return filter { 37 | $0.type == .swiftWarning || 38 | $0.type == .clangWarning || 39 | $0.type == .projectWarning || 40 | $0.type == .analyzerWarning || 41 | $0.type == .interfaceBuilderWarning || 42 | $0.type == .deprecatedWarning 43 | } 44 | } 45 | 46 | func getErrors() -> [Notice] { 47 | return filter { 48 | $0.type == .swiftError || 49 | $0.type == .error || 50 | $0.type == .clangError || 51 | $0.type == .linkerError || 52 | $0.type == .packageLoadingError || 53 | $0.type == .scriptPhaseError 54 | } 55 | } 56 | 57 | func getNotes() -> [Notice] { 58 | return filter { 59 | $0.type == .note 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/extensions/EncodableExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | extension Encodable { 23 | 24 | func toJSON() throws -> Data { 25 | let encoder = JSONEncoder() 26 | encoder.outputFormatting = .prettyPrinted 27 | return try encoder.encode(self) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/extensions/NSRegularExpressionExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | extension NSRegularExpression { 23 | 24 | static func fromPattern(_ pattern: String) -> NSRegularExpression? { 25 | do { 26 | return try NSRegularExpression(pattern: pattern) 27 | } catch { 28 | return nil 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/extensions/URLExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | extension URL { 23 | 24 | static var homeDir: URL? { 25 | if #available(OSX 10.12, *) { 26 | return FileManager.default.homeDirectoryForCurrentUser 27 | } else { 28 | return FileManager.default.urls(for: .userDirectory, in: .userDomainMask).first 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/lexer/LexRedactor.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | public class LexRedactor: LogRedactor { 23 | private static let redactedTemplate = "/Users//" 24 | private lazy var userDirRegex: NSRegularExpression? = { 25 | do { 26 | return try NSRegularExpression(pattern: "/Users/([^/]+)/?") 27 | } catch { 28 | return nil 29 | } 30 | }() 31 | public var userDirToRedact: String? 32 | 33 | public init() { 34 | } 35 | 36 | public func redactUserDir(string: String) -> String { 37 | guard let regex = userDirRegex else { 38 | return string 39 | } 40 | if let userDirToRedact = userDirToRedact { 41 | return string.replacingOccurrences(of: userDirToRedact, with: Self.redactedTemplate) 42 | } else { 43 | guard let firstMatch = regex.firstMatch(in: string, 44 | options: [], 45 | range: NSRange(location: 0, length: string.count)) else { 46 | return string 47 | } 48 | let userDir = string.substring(firstMatch.range) 49 | userDirToRedact = userDir 50 | return string.replacingOccurrences(of: userDir, with: Self.redactedTemplate) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/lexer/LexerModel.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | public enum TokenType: String, CaseIterable { 23 | case int = "#" 24 | case className = "%" 25 | case classNameRef = "@" 26 | case string = "\"" 27 | case double = "^" 28 | case null = "-" 29 | case list = "(" 30 | case json = "*" 31 | 32 | static func all() -> String { 33 | return TokenType.allCases.reduce(String()) { 34 | return "\($0)\($1.rawValue)" 35 | } 36 | } 37 | } 38 | 39 | public enum Token: CustomDebugStringConvertible, Equatable { 40 | case int(UInt64) 41 | case className(Substring) 42 | case classNameRef(String) 43 | case string(Substring) 44 | case double(Double) 45 | case null 46 | case list(Int) 47 | case json(Substring) 48 | } 49 | 50 | extension Token { 51 | public var debugDescription: String { 52 | switch self { 53 | case .int(let value): 54 | return "[type: int, value: \(value)]" 55 | case .className(let name): 56 | return "[type: className, name: \"\(name)\"]" 57 | case .classNameRef(let name): 58 | return "[type: classNameRef, className: \"\(name)\"]" 59 | case .string(let value): 60 | return "[type: string, value: \"\(value)\"]" 61 | case .double(let value): 62 | return "[type: double, value: \(value)]" 63 | case .null: 64 | return "[type: nil]" 65 | case .list(let count): 66 | return "[type: list, count: \(count)]" 67 | case .json(let json): 68 | return "[type: json, value: \(json)]" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/lexer/LogRedactor.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | public protocol LogRedactor { 21 | /// Predefined (or inferrred during redation process) user home path. 22 | /// Introduced for better performance. 23 | var userDirToRedact: String? {get set} 24 | 25 | /// Redacts a string by replacing sensitive username path with a template 26 | /// - parameter string: The string to redact 27 | /// - returns: Redacted text with 28 | func redactUserDir(string: String) -> String 29 | } 30 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/lexer/String+BuildSpecificInformationRemoval.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | /// Methods on `String` that remove build specific information from the log. 23 | /// 24 | /// This can be useful if we want to group logs by its content in order to indicate 25 | /// how often particular log occurs. 26 | extension String { 27 | /// Removes autogenerated build identifier in built product path. 28 | /// 29 | /// Example: "DerivedData/Product-bolnckhlbzxpxoeyfujluasoupft/Build" becomes "DerivedData/Product/Build". 30 | func removeProductBuildIdentifier() -> String { 31 | do { 32 | var mutableSelf = self 33 | let regularExpression = try NSRegularExpression(pattern: "/DerivedData/(.*)-(.*)/Build/") 34 | regularExpression.enumerateMatches(in: self, 35 | options: [], 36 | range: NSRange(location: 0, length: count)) { match, _, _ in 37 | if let match = match, match.numberOfRanges == 3 { 38 | let buildIdentifier = self.substring(match.range(at: 2)) 39 | mutableSelf = mutableSelf.replacingOccurrences(of: "-" + buildIdentifier, with: "") 40 | } 41 | } 42 | return mutableSelf 43 | } catch { 44 | return self 45 | } 46 | } 47 | 48 | /// Removes hexadecimal numbers from the log and puts `` instead. 49 | /// 50 | /// Example: "NSUnderlyingError=0x7fcdc8712290" becomes "NSUnderlyingError=". 51 | func removeHexadecimalNumbers() -> String { 52 | do { 53 | var mutableSelf = self 54 | let regularExpression = try NSRegularExpression(pattern: "0[xX][0-9a-fA-F]+") 55 | regularExpression.enumerateMatches(in: self, 56 | options: [], 57 | range: NSRange(location: 0, length: count)) { match, _, _ in 58 | if let match = match { 59 | let hexadecimalNumber = self.substring(match.range(at: 0)) 60 | mutableSelf = mutableSelf.replacingOccurrences(of: hexadecimalNumber, with: "") 61 | } 62 | } 63 | return mutableSelf 64 | } catch { 65 | return self 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/loglocation/DefaultDerivedData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultDerivedData.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 09.01.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public class DefaultDerivedData { 11 | 12 | public init() {} 13 | 14 | private func getCustomDerivedDataDir() -> URL? { 15 | guard let xcodeOptions = UserDefaults.standard.persistentDomain(forName: "com.apple.dt.Xcode") else { 16 | return nil 17 | } 18 | guard let customLocation = xcodeOptions["IDECustomDerivedDataLocation"] as? String else { 19 | return nil 20 | } 21 | return URL(fileURLWithPath: customLocation) 22 | } 23 | 24 | public func getDerivedDataDir() -> URL? { 25 | if let customDerivedDataDir = getCustomDerivedDataDir() { 26 | return customDerivedDataDir 27 | } 28 | 29 | return defaultDerivedData 30 | } 31 | 32 | var defaultDerivedData: URL? { 33 | guard let homeDirURL = URL.homeDir else { 34 | return nil 35 | } 36 | return homeDirURL.appendingPathComponent("Library/Developer/Xcode/DerivedData", isDirectory: true) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/loglocation/DerivedDataByXcodeProjectLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DerivedDataByXcodeProjectLocation.swift 3 | // 4 | // 5 | // Created by Mikhail Rubanov on 09.01.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/loglocation/LogError.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | /// Errors thrown by the LogFinder 23 | public enum LogError: LocalizedError { 24 | case noDerivedDataFound 25 | case noLogFound(dir: String) 26 | case xcodeBuildError(String) 27 | case readingFile(String) 28 | case invalidFile(String) 29 | case noLogManifestFound(dir: String) 30 | case invalidLogManifest(String) 31 | 32 | public var errorDescription: String? { return description } 33 | 34 | } 35 | 36 | extension LogError: CustomStringConvertible { 37 | 38 | public var description: String { 39 | switch self { 40 | case .noDerivedDataFound: 41 | return "We couldn't find the derivedData directory. " + 42 | "If you use a custom derivedData directory, use the --derivedData option to pass it. " 43 | case .noLogFound(let dir): 44 | return "We couldn't find a log in the directory \(dir). " + 45 | "If the log is in a custom derivedData dir, use the --derivedData option. " + 46 | "You can also pass the full path to the xcactivity log with the --file option" 47 | case .xcodeBuildError(let error): 48 | return error 49 | case .readingFile(let path): 50 | return "Can't read file \(path)" 51 | case .invalidFile(let path): 52 | return "\(path) is not a valid xcactivitylog file" 53 | case .noLogManifestFound(let path): 54 | return "We couldn't find a logManifest in the path \(path). " + 55 | "If the LogManifest is in a custom derivedData directory, use the --derivedData option." 56 | case .invalidLogManifest(let path): 57 | return "\(path) is not a valid LogManifest file" 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/loglocation/LogLoader.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | import Gzip 22 | 23 | public struct LogLoader { 24 | 25 | func loadFromURL(_ url: URL) throws -> String { 26 | do { 27 | let data = try Data(contentsOf: url) 28 | let unzipped = try data.gunzipped() 29 | let string: String? = unzipped.withUnsafeBytes { pointer in 30 | guard let charPointer = pointer 31 | .assumingMemoryBound(to: CChar.self) 32 | .baseAddress 33 | else { 34 | return nil 35 | } 36 | 37 | return String(cString: charPointer, encoding: .ascii) 38 | } 39 | guard let contents = string else { 40 | throw LogError.readingFile(url.path) 41 | } 42 | return contents 43 | } catch { 44 | throw LogError.invalidFile(url.path) 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/logmanifest/LogManifestModel.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | public enum LogManifestEntryType: String, Encodable { 23 | case xcode 24 | case xcodebuild 25 | 26 | private static let xcodeLogClassName = "IDEActivityLogSection" 27 | private static let xcodebuildLogClassName = "IDECommandLineBuildLog" 28 | 29 | public static func buildFromClassName(_ className: String) -> LogManifestEntryType? { 30 | if className == xcodeLogClassName { 31 | return .xcode 32 | } 33 | if className == xcodebuildLogClassName { 34 | return .xcodebuild 35 | } 36 | return nil 37 | } 38 | } 39 | 40 | public struct LogManifestEntry: Encodable { 41 | public let uniqueIdentifier: String 42 | public let title: String 43 | public let scheme: String 44 | public let fileName: String 45 | public let timestampStart: Int 46 | public let timestampEnd: Int 47 | public let duration: Int 48 | public let type: LogManifestEntryType 49 | 50 | public init(uniqueIdentifier: String, title: String, scheme: String, fileName: String, 51 | timestampStart: Int, timestampEnd: Int, duration: Int, type: LogManifestEntryType) { 52 | self.uniqueIdentifier = uniqueIdentifier 53 | self.title = title 54 | self.scheme = scheme 55 | self.fileName = fileName 56 | self.timestampStart = timestampStart 57 | self.timestampEnd = timestampEnd 58 | self.duration = duration 59 | self.type = type 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/parser/BuildStep+Parser.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | public extension BuildStep { 23 | 24 | /// Flattens a group of swift compilations steps. 25 | /// 26 | /// When a Swift module is compiled with `whole module` option 27 | /// The parsed log looks like: 28 | /// - CompileSwiftTarget 29 | /// - CompileSwift 30 | /// - CompileSwift file1.swift 31 | /// - CompileSwift file2.swift 32 | /// This tasks removes the intermediate CompileSwift step and moves the substeps 33 | /// to the root: 34 | /// - CompileSwiftTarget 35 | /// - CompileSwift file1.swift 36 | /// - CompileSwift file2.swift 37 | /// - Returns: The build step with its swift substeps at the root level, and intermediate CompileSwift step removed. 38 | func moveSwiftStepsToRoot() -> BuildStep { 39 | var updatedSubSteps = subSteps 40 | for (index, subStep) in subSteps.enumerated() { 41 | if subStep.detailStepType == .swiftCompilation && subStep.subSteps.count > 0 { 42 | updatedSubSteps.remove(at: index) 43 | updatedSubSteps.append(contentsOf: subStep.subSteps) 44 | } 45 | } 46 | return with(subSteps: updatedSubSteps) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/parser/Contains.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | public struct Contains { 23 | 24 | let str: String 25 | 26 | public init(_ str: String) { 27 | self.str = str.lowercased() 28 | } 29 | 30 | private func match(_ input: String) -> Bool { 31 | return input.lowercased().contains(str) 32 | } 33 | } 34 | 35 | extension Contains { 36 | static func ~= (contains: Contains, input: String) -> Bool { 37 | return contains.match(input) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/parser/MachineNameReader.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | /// Reads the name of the machine where this command is executed 23 | protocol MachineNameReader { 24 | var machineName: String? { get } 25 | } 26 | 27 | /// Implementation of `MachineReader` that uses the name of the host as the Machine name 28 | class MacOSMachineNameReader: MachineNameReader { 29 | var machineName: String? { 30 | return Host.current().localizedName 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/parser/NoticeType.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | /// The type of a Notice 23 | public enum NoticeType: String, Codable { 24 | 25 | /// Notes 26 | case note 27 | 28 | /// A warning thrown by the Swift compiler 29 | case swiftWarning 30 | 31 | /// A warning thrown by the C compiler 32 | case clangWarning 33 | 34 | /// A warning at a project level. For instance: 35 | /// "Warning Swift 3 mode has been deprecated and will be removed in a later version of Xcode" 36 | case projectWarning 37 | 38 | /// An error in a non-compilation step. For instance creating a directory or running a shell script phase 39 | case error 40 | 41 | /// An error thrown by the Swift compiler 42 | case swiftError 43 | 44 | /// An error thrown by the C compiler 45 | case clangError 46 | 47 | /// A warning returned by Xcode static analyzer 48 | case analyzerWarning 49 | 50 | /// A warning inside an Interface Builder file 51 | case interfaceBuilderWarning 52 | 53 | /// A warning about the usage of a deprecated API 54 | case deprecatedWarning 55 | 56 | /// Error thrown by the Linker 57 | case linkerError 58 | 59 | /// Error loading Swift Packages 60 | case packageLoadingError 61 | 62 | /// Error running a Build Phase's script 63 | case scriptPhaseError 64 | 65 | // swiftlint:disable:next cyclomatic_complexity 66 | public static func fromTitle(_ title: String) -> NoticeType? { 67 | switch title { 68 | case "Swift Compiler Warning": 69 | return .swiftWarning 70 | case "Notice": 71 | return .note 72 | case "Swift Compiler Error": 73 | return .swiftError 74 | case Prefix("Lexical"), Suffix("Semantic Issue"), "Parse Issue", "Uncategorized": 75 | return .clangError 76 | case Suffix("Deprecations"): 77 | return .deprecatedWarning 78 | case "Warning", "Apple Mach-O Linker Warning", "Target Integrity": 79 | return .projectWarning 80 | case Suffix("Error"): 81 | return .error 82 | case Suffix("Notice"): 83 | return .note 84 | case Prefix("/* com.apple.ibtool.document.warnings */"): 85 | return .interfaceBuilderWarning 86 | case "Package Loading": 87 | return .packageLoadingError 88 | case Contains("Command PhaseScriptExecution"): 89 | return .scriptPhaseError 90 | case Prefix("error: Swiftc"): 91 | return .swiftError 92 | default: 93 | return .note 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/parser/Prefix.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | public struct Prefix { 23 | 24 | let prefix: String 25 | 26 | public init(_ prefix: String) { 27 | self.prefix = prefix.lowercased() 28 | } 29 | 30 | private func match(_ input: String) -> Bool { 31 | return input.lowercased().starts(with: prefix) 32 | } 33 | } 34 | 35 | extension Prefix { 36 | static func ~= (prefix: Prefix, input: String) -> Bool { 37 | return prefix.match(input) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/parser/StringExtension.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | extension String { 23 | 24 | func substring(_ range: NSRange) -> String { 25 | guard let stringRange = Range(range, in: self) else { 26 | return "" 27 | } 28 | return String(self[stringRange]) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/parser/Suffix.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | public struct Suffix { 23 | 24 | let suffix: String 25 | 26 | public init(_ suffix: String) { 27 | self.suffix = suffix.lowercased() 28 | } 29 | 30 | private func match(_ input: String) -> Bool { 31 | return input.lowercased().hasSuffix(suffix) 32 | } 33 | } 34 | 35 | extension Suffix { 36 | static func ~= (suffix: Suffix, input: String) -> Bool { 37 | return suffix.match(input) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/parser/SwiftCompilerFunctionTimeOptionParser.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | /// Parses Swift Function times generated by `swiftc` 23 | /// if you pass the flags `-Xfrontend -debug-time-function-bodies` 24 | class SwiftCompilerFunctionTimeOptionParser: SwiftCompilerTimeOptionParser { 25 | 26 | private static let compilerFlag = "-debug-time-function-bodies" 27 | 28 | func hasCompilerFlag(commandDesc: String) -> Bool { 29 | commandDesc.range(of: Self.compilerFlag) != nil 30 | } 31 | 32 | func parse(from commands: [String: Int]) -> [String: [SwiftFunctionTime]] { 33 | let functionsPerFile = commands.compactMap { parse(command: $0.key, occurrences: $0.value) } 34 | .joined().reduce([:]) { (functionsPerFile, functionTime) 35 | -> [String: [SwiftFunctionTime]] in 36 | var functionsPerFile = functionsPerFile 37 | if var functions = functionsPerFile[functionTime.file] { 38 | functions.append(functionTime) 39 | functionsPerFile[functionTime.file] = functions 40 | } else { 41 | functionsPerFile[functionTime.file] = [functionTime] 42 | } 43 | return functionsPerFile 44 | } 45 | return functionsPerFile 46 | } 47 | 48 | private func parse(command: String, occurrences: Int) -> [SwiftFunctionTime]? { 49 | let functions: [SwiftFunctionTime] = command.components(separatedBy: "\r").compactMap { commandLine in 50 | 51 | // 0.14ms /users/mnf/project/SomeFile.swift:10:12 someMethod(param:) 52 | let parts = commandLine.components(separatedBy: "\t") 53 | 54 | guard parts.count == 3 else { 55 | return nil 56 | } 57 | 58 | // 0.14ms 59 | let duration = parseCompileDuration(parts[0]) 60 | 61 | // /users/mnf/project/SomeFile.swift:10:12 62 | let fileAndLocation = parts[1] 63 | guard let (file, line, column) = parseNameAndLocation(from: fileAndLocation) else { 64 | return nil 65 | } 66 | 67 | // someMethod(param:) 68 | let signature = parts[2] 69 | 70 | return SwiftFunctionTime(file: file, 71 | durationMS: duration, 72 | startingLine: line, 73 | startingColumn: column, 74 | signature: signature, 75 | occurrences: occurrences) 76 | } 77 | 78 | return functions 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/parser/SwiftCompilerTimeOptionParser.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | /// Parses `swiftc` commands for time compiler outputs 23 | protocol SwiftCompilerTimeOptionParser { 24 | 25 | associatedtype SwiftcOption 26 | 27 | /// Returns true if the compiler command included the flag to generate 28 | /// this compiler report 29 | /// - Parameter commandDesc: The command description 30 | func hasCompilerFlag(commandDesc: String) -> Bool 31 | 32 | /// Parses the Set of commands to look for swift compiler time outputs of type `SwiftcOption` 33 | /// - Parameter commands: Dictionary of command descriptions and ocurrences 34 | /// - Returns: A dictionary using the key as file and the Compiler time output as value 35 | func parse(from commands: [String: Int]) -> [String: [SwiftcOption]] 36 | 37 | } 38 | 39 | extension SwiftCompilerTimeOptionParser { 40 | 41 | /// Parses /users/mnf/project/SomeFile.swift:10:12 42 | /// - Returns: ("file:///users/mnf/project/SomeFile.swift", 10, 12) 43 | // swiftlint:disable:next large_tuple 44 | func parseNameAndLocation(from fileAndLocation: String) -> (String, Int, Int)? { 45 | // /users/mnf/project/SomeFile.swift:10:12 46 | let fileAndLocationParts = fileAndLocation.components(separatedBy: ":") 47 | let rawFile = fileAndLocationParts[0] 48 | 49 | guard rawFile != "" else { 50 | return nil 51 | } 52 | 53 | guard 54 | fileAndLocationParts.count == 3, 55 | let line = Int(fileAndLocationParts[1]), 56 | let column = Int(fileAndLocationParts[2]) 57 | else { 58 | return nil 59 | } 60 | 61 | let file = prefixWithFileURL(fileName: rawFile) 62 | 63 | return (file, line, column) 64 | } 65 | 66 | /// Parses 67 | func parseCompileDuration(_ durationString: String) -> Double { 68 | if let duration = Double(durationString.replacingOccurrences(of: "ms", with: "")) { 69 | return duration 70 | } 71 | return 0.0 72 | } 73 | 74 | /// Transforms the fileName to a file URL to match the one in IDELogSection.documentURL 75 | /// It doesn't use `URL` class to do it, because it was slow in benchmarks 76 | /// - Parameter fileName: String with a fileName 77 | /// - Returns: A String with the URL to the file like `file:///` 78 | func prefixWithFileURL(fileName: String) -> String { 79 | return "file://\(fileName)" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/parser/SwiftCompilerTypeCheckOptionParser.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | /// Parses Swift Function times generated by `swiftc` 23 | /// if you pass the flags `-Xfrontend -debug-time-expression-type-checking` 24 | class SwiftCompilerTypeCheckOptionParser: SwiftCompilerTimeOptionParser { 25 | 26 | private static let compilerFlag = "-debug-time-expression-type-checking" 27 | 28 | func hasCompilerFlag(commandDesc: String) -> Bool { 29 | commandDesc.range(of: Self.compilerFlag) != nil 30 | } 31 | 32 | func parse(from commands: [String: Int]) -> [String: [SwiftTypeCheck]] { 33 | return commands.compactMap { parse(command: $0.key, occurrences: $0.value) } 34 | .joined().reduce([:]) { (typeChecksPerFile, typeCheckTime) 35 | -> [String: [SwiftTypeCheck]] in 36 | var typeChecksPerFile = typeChecksPerFile 37 | if var typeChecks = typeChecksPerFile[typeCheckTime.file] { 38 | typeChecks.append(typeCheckTime) 39 | typeChecksPerFile[typeCheckTime.file] = typeChecks 40 | } else { 41 | typeChecksPerFile[typeCheckTime.file] = [typeCheckTime] 42 | } 43 | return typeChecksPerFile 44 | } 45 | } 46 | 47 | private func parse(command: String, occurrences: Int) -> [SwiftTypeCheck]? { 48 | return command.components(separatedBy: "\r").compactMap { commandLine in 49 | // 0.14ms /users/mnf/project/SomeFile.swift:10:12 50 | let parts = commandLine.components(separatedBy: "\t") 51 | 52 | guard parts.count == 2 else { 53 | return nil 54 | } 55 | 56 | // 0.14ms 57 | let duration = parseCompileDuration(parts[0]) 58 | 59 | // /users/mnf/project/SomeFile.swift:10:12 60 | let fileAndLocation = parts[1] 61 | guard let (file, line, column) = parseNameAndLocation(from: fileAndLocation) else { 62 | return nil 63 | } 64 | 65 | return SwiftTypeCheck(file: file, 66 | durationMS: duration, 67 | startingLine: line, 68 | startingColumn: column, 69 | occurrences: occurrences) 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/parser/SwiftFunctionTime.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | /// Represents the time it took to the Swift Compiler to compile a function 23 | public struct SwiftFunctionTime: Encodable { 24 | /// URL of the file where the function is 25 | public let file: String 26 | 27 | /// Duration in Miliseconds 28 | public let durationMS: Double 29 | 30 | /// Line number where the function is declared 31 | public let startingLine: Int 32 | 33 | /// Column number where the function is declared 34 | public let startingColumn: Int 35 | 36 | /// function signature 37 | public let signature: String 38 | 39 | /// Number of occurences this function is compiled during the build 40 | public let occurrences: Int 41 | 42 | } 43 | -------------------------------------------------------------------------------- /XCLogParser/Sources/XCLogParser/parser/SwiftTypeCheck.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | /// Represents the time it took to the Swift Compiler to type check an expression 23 | public struct SwiftTypeCheck: Encodable { 24 | 25 | /// URL of the file where the function is 26 | public let file: String 27 | 28 | /// Duration in Miliseconds 29 | public let durationMS: Double 30 | 31 | /// Line number where the function is declared 32 | public let startingLine: Int 33 | 34 | /// Column number where the function is declared 35 | public let startingColumn: Int 36 | 37 | /// Number of occurences this type is checked during the build 38 | public let occurrences: Int 39 | 40 | } 41 | -------------------------------------------------------------------------------- /XCLogParser/Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import XCLogParserTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += XCLogParserTests.__allTests() 7 | 8 | XCTMain(tests) 9 | -------------------------------------------------------------------------------- /XCLogParser/Tests/XCLogParserTests/BuildStep+TestUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import XCLogParser 3 | 4 | func makeFakeBuildStep(title: String, 5 | type: BuildStepType, 6 | detailStepType: DetailStepType, 7 | startTimestamp: Double, 8 | fetchedFromCache: Bool) -> BuildStep { 9 | return BuildStep(type: type, 10 | machineName: "", 11 | buildIdentifier: "", 12 | identifier: "", 13 | parentIdentifier: "", 14 | domain: "", 15 | title: title, 16 | signature: "", 17 | startDate: "", 18 | endDate: "", 19 | startTimestamp: startTimestamp, 20 | endTimestamp: 0, 21 | duration: 0, 22 | detailStepType: detailStepType, 23 | buildStatus: "", 24 | schema: "", 25 | subSteps: [], 26 | warningCount: 0, 27 | errorCount: 0, 28 | architecture: "", 29 | documentURL: "", 30 | warnings: nil, 31 | errors: nil, 32 | notes: nil, 33 | swiftFunctionTimes: nil, 34 | fetchedFromCache: fetchedFromCache, 35 | compilationEndTimestamp: 0, 36 | compilationDuration: 0, 37 | clangTimeTraceFile: nil, 38 | linkerStatistics: nil, 39 | swiftTypeCheckTimes: nil) 40 | } 41 | -------------------------------------------------------------------------------- /XCLogParser/Tests/XCLogParserTests/LexRedactorTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | import XCTest 22 | @testable import XCLogParser 23 | 24 | class LexRedactorTests: XCTestCase { 25 | let redactor = LexRedactor() 26 | 27 | func testRedacting() { 28 | let redactedText = redactor.redactUserDir(string: "Some /Users/private/path") 29 | 30 | XCTAssertEqual(redactedText, "Some /Users//path") 31 | } 32 | 33 | func testRedactingComplexUsername() { 34 | let redactedText = redactor.redactUserDir(string: "Some /Users/private-user/path") 35 | 36 | XCTAssertEqual(redactedText, "Some /Users//path") 37 | } 38 | 39 | func testRedactingHomePath() { 40 | let redactedText = redactor.redactUserDir(string: "/Users/private-user") 41 | 42 | XCTAssertEqual(redactedText, "/Users//") 43 | } 44 | 45 | func testMultiplePathsRedacting() { 46 | let redactedText = redactor.redactUserDir(string: "Some /Users/private/path and other /Users/private/path2") 47 | 48 | XCTAssertEqual(redactedText, "Some /Users//path and other /Users//path2") 49 | } 50 | 51 | func testRedactingFillsUserDir() { 52 | _ = redactor.redactUserDir(string: "Some /Users/private/path") 53 | 54 | XCTAssertEqual(redactor.userDirToRedact, "/Users/private/") 55 | } 56 | 57 | func testPredefinedUserDirIsRedacted() { 58 | redactor.userDirToRedact = "/Users/private/" 59 | 60 | let redactedText = redactor.redactUserDir(string: "Some /Users/private/path") 61 | 62 | XCTAssertEqual(redactedText, "Some /Users//path") 63 | } 64 | 65 | func testNotInPredefinedUserDirIsNotRedacted() { 66 | redactor.userDirToRedact = "/Users/priv/" 67 | 68 | let redactedText = redactor.redactUserDir(string: "Some /Users/private/path") 69 | 70 | XCTAssertEqual(redactedText, "Some /Users/private/path") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /XCLogParser/Tests/XCLogParserTests/ReporterTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import XCTest 21 | 22 | @testable import XCLogParser 23 | 24 | class ReporterTests: XCTestCase { 25 | 26 | func testMakeLogReporter() { 27 | let chromeTracerReporter = Reporter.chromeTracer.makeLogReporter() 28 | XCTAssertTrue(chromeTracerReporter is ChromeTracerReporter) 29 | 30 | let flatJsonReporter = Reporter.flatJson.makeLogReporter() 31 | XCTAssertTrue(flatJsonReporter is FlatJsonReporter) 32 | 33 | let htmlReporter = Reporter.html.makeLogReporter() 34 | XCTAssertTrue(htmlReporter is HtmlReporter) 35 | 36 | let jsonReporter = Reporter.json.makeLogReporter() 37 | XCTAssertTrue(jsonReporter is JsonReporter) 38 | 39 | let issuesReporter = Reporter.issues.makeLogReporter() 40 | XCTAssertTrue(issuesReporter is IssuesReporter) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /XCLogParser/Tests/XCLogParserTests/TestUtils.swift: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Spotify AB. 2 | // 3 | // Licensed to the Apache Software Foundation (ASF) under one 4 | // or more contributor license agreements. See the NOTICE file 5 | // distributed with this work for additional information 6 | // regarding copyright ownership. The ASF licenses this file 7 | // to you under the Apache License, Version 2.0 (the 8 | // "License"); you may not use this file except in compliance 9 | // with the License. You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, 14 | // software distributed under the License is distributed on an 15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | // KIND, either express or implied. See the License for the 17 | // specific language governing permissions and limitations 18 | // under the License. 19 | 20 | import Foundation 21 | 22 | struct TestUtils { 23 | 24 | static func createRandomTestDir() throws -> URL { 25 | let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 26 | try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) 27 | return url 28 | } 29 | 30 | static func createRandomTestDirWithPath(_ path: String) throws -> URL { 31 | let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 32 | let dirUrl = url.appendingPathComponent(path) 33 | try FileManager.default.createDirectory(at: dirUrl, withIntermediateDirectories: true, attributes: nil) 34 | return url 35 | } 36 | 37 | @discardableResult 38 | static func createSubdir(_ name: String, in dir: URL, attributes: [FileAttributeKey: Any]? = nil) throws -> URL { 39 | let url = dir.appendingPathComponent(name, isDirectory: true) 40 | try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: attributes) 41 | return url 42 | } 43 | 44 | } 45 | --------------------------------------------------------------------------------