├── .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 |
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 |
--------------------------------------------------------------------------------