├── .gitignore ├── BuildAnalyzer.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── BuildAnalyzer ├── AppView.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 1024-mac.png │ │ ├── 128-mac.png │ │ ├── 16-mac.png │ │ ├── 256-mac.png │ │ ├── 32-mac.png │ │ ├── 512-mac.png │ │ ├── 64-mac.png │ │ └── Contents.json │ └── Contents.json ├── BuildAnalyzer.entitlements ├── BuildAnalyzerApp.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Resources │ ├── build │ │ ├── d3-graphviz.js │ │ └── d3-graphviz.js.map │ ├── img │ │ ├── cone.fill.svg │ │ ├── cone.fill_in.svg │ │ ├── cone.fill_in_out.svg │ │ ├── cone.fill_out.svg │ │ ├── cone.svg │ │ ├── cone_in.svg │ │ ├── cone_in_out.svg │ │ ├── cone_out.svg │ │ ├── cube.fill.svg │ │ ├── cube.fill_in.svg │ │ ├── cube.fill_in_out.svg │ │ ├── cube.fill_out.svg │ │ ├── cube.svg │ │ ├── cube.transparent.fill.svg │ │ ├── cube.transparent.fill_in.svg │ │ ├── cube.transparent.fill_in_out.svg │ │ ├── cube.transparent.fill_out.svg │ │ ├── cube.transparent.svg │ │ ├── cube.transparent_in.svg │ │ ├── cube.transparent_in_out.svg │ │ ├── cube.transparent_out.svg │ │ ├── cube_in.svg │ │ ├── cube_in_out.svg │ │ ├── cube_out.svg │ │ ├── doc.fill.svg │ │ ├── doc.fill_in.svg │ │ ├── doc.fill_in_out.svg │ │ ├── doc.fill_out.svg │ │ ├── doc.svg │ │ ├── doc_in.svg │ │ ├── doc_in_out.svg │ │ ├── doc_out.svg │ │ ├── drag-and-drop.svg │ │ ├── elypsis.svg │ │ ├── elypsis_in.svg │ │ ├── elypsis_in_out.svg │ │ ├── elypsis_out.svg │ │ ├── gate.svg │ │ ├── gate_in.svg │ │ ├── gate_in_out.svg │ │ ├── gate_out.svg │ │ ├── pyramid.fill.svg │ │ ├── pyramid.fill_in.svg │ │ ├── pyramid.fill_in_out.svg │ │ ├── pyramid.fill_out.svg │ │ ├── pyramid.svg │ │ ├── pyramid_in.svg │ │ ├── pyramid_in_out.svg │ │ ├── pyramid_out.svg │ │ ├── question.svg │ │ ├── question_in.svg │ │ ├── question_in_out.svg │ │ ├── question_out.svg │ │ ├── scope.svg │ │ ├── scope_in.svg │ │ ├── scope_in_out.svg │ │ ├── scope_out.svg │ │ ├── shipping.svg │ │ ├── shipping_in.svg │ │ ├── shipping_in_out.svg │ │ ├── shipping_out.svg │ │ ├── stop.svg │ │ ├── stop_in.svg │ │ ├── stop_in_out.svg │ │ └── stop_out.svg │ ├── index.html │ ├── package-lock.json │ └── package.json ├── Utils │ ├── ManifestFinder.swift │ └── ManifestFinderError.swift └── Views │ ├── Alerts │ └── AlertView.swift │ ├── DetailsView │ └── GraphItemView.swift │ ├── HierarchyView │ ├── GraphHierarchyContentBuilder.swift │ ├── GraphHierarchyElement.swift │ └── GraphHierarchyView.swift │ └── WebView │ ├── D3Projector │ ├── D3BuildGraphProjector.swift │ ├── D3PageRequest.swift │ └── D3PageResponse.swift │ ├── GraphWebView.swift │ └── LayoutStyle.swift ├── BuildAnalyzerTests └── BuildAnalyzerTests.swift ├── BuildAnalyzerUITests ├── BuildAnalyzerUITests.swift └── BuildAnalyzerUITestsLaunchTests.swift ├── LICENSE ├── Package.swift ├── Readme.md ├── Sources ├── BuildAnalyzerKit │ ├── BuildGraph.swift │ ├── BuildGraphGenerator.swift │ ├── BuildGraphNode.swift │ ├── BuildGraphNodeKind.swift │ ├── BuildGraphProtocol.swift │ ├── BuildManifestParser.swift │ ├── DataStructure │ │ └── Queue.swift │ ├── Projection │ │ ├── BuildGraphNodeProjectionNode.swift │ │ ├── BuildGraphProjection.swift │ │ ├── BuildGraphProjectionExpansion.swift │ │ └── BuildGraphProtocol+Expansion.swift │ └── Timing │ │ ├── BuildGraphNodeTimingReader.swift │ │ └── BuildGraphNodeTimingSqlReader.swift ├── GraphKit │ └── D3Projector.swift └── XcodeHasher │ └── XcodeHasher.swift ├── Tests ├── BuildAnalyzerKitTests │ ├── AnalyzerTests.swift │ └── BuildGraphNodeKindTests.swift └── GraphKitTests │ └── StartingFileTests.swift ├── docs └── img │ ├── cycle.png │ ├── cycle_warning.png │ ├── demo.gif │ ├── details.png │ ├── executed.png │ ├── expand.png │ ├── graph.png │ ├── icon.png │ ├── subgraph.png │ └── timing.png └── scripts └── dependencies.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | BuildAnalyzer/Resources/node_modules 10 | 11 | -------------------------------------------------------------------------------- /BuildAnalyzer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BuildAnalyzer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BuildAnalyzer/AppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // BuildAnalyzer 4 | // 5 | // Created by Bartosz Polaczyk on 6/18/23. 6 | // 7 | 8 | import SwiftUI 9 | import GraphKit 10 | import BuildAnalyzerKit 11 | 12 | 13 | func buildGraph(manifestLocation: ManifestLocation) throws -> BuildGraph { 14 | let parser = BuildManifestParser() 15 | let manifest = try parser.process(manifestLocation.manifest) 16 | let timings = try manifestLocation.timingDatabase.flatMap(BuildGraphNodeTimingSqlReader.init(file:))?.read() ?? [:] 17 | 18 | // TODO: Enable reliable timings which provide valuable timestamps 19 | return BuildGraph(manifest: manifest, timings: timings) 20 | } 21 | 22 | class AppAlternatives: ObservableObject { 23 | @Published var command: Bool = false 24 | } 25 | 26 | struct AppView: View { 27 | 28 | @Binding var selection: Set 29 | @Binding var focus: String? 30 | @State private var search: String = "" 31 | @Binding var graph: BuildGraph 32 | @Binding var graphUrl: URL? 33 | @Binding var graphLayout: GraphLayoutStyle 34 | let web: GraphWebView 35 | private let hierarchyBuilder = GraphHierarchyContentBuilder() 36 | @Binding var error: ManifestFinderError? 37 | @Binding var loading: Bool 38 | @Binding var appAlternatives: AppAlternatives 39 | 40 | var body: some View { 41 | ZStack { 42 | VStack { 43 | GeometryReader { geometry in 44 | HSplitView{ 45 | GraphHierarchyView( 46 | selection: $selection, 47 | graph: graph, 48 | search: $search, 49 | focus: $focus, 50 | hierarchyBuilder: hierarchyBuilder 51 | ).frame(minWidth: 300).padding(.horizontal) 52 | 53 | ZStack(alignment: .topLeading) { 54 | web.onChange(of: selection) { newValue in 55 | web.controller.select(nodes: newValue, focus: focus) 56 | }.onChange(of: graph) { newValue in 57 | web.controller.reset() 58 | selection = [] 59 | }.onAppear { 60 | web.controller.coordinator.setBinding($selection) 61 | } 62 | 63 | HStack { 64 | Button("❖") { 65 | if appAlternatives.command { 66 | web.controller.webView.reload() 67 | } else { 68 | web.controller.resetZoom() 69 | } 70 | } 71 | .help("Reset zoom") 72 | 73 | Toggle("◎", isOn: .init(get: { graphLayout == .circo }, set: {graphLayout = $0 ? .circo : .standard; web.controller.setLayout(graphLayout)})) 74 | .help("Circular layout") 75 | .toggleStyle(.button) 76 | 77 | } 78 | .opacity(graph.nodes.isEmpty ? 0 : 1) 79 | .padding(5) 80 | 81 | } 82 | .layoutPriority(1000) 83 | 84 | GraphItemView ( 85 | item: graph.nodes[BuildGraphNodeId(id: ($focus.wrappedValue ?? ""))], focus: $focus 86 | ).frame(minWidth: 300) 87 | } 88 | } 89 | } 90 | .padding() 91 | .onDrop(of: [.fileURL], isTargeted: nil) { providers in 92 | if let provider = providers.first(where: { $0.canLoadObject(ofClass: URL.self) } ) { 93 | let _ = provider.loadObject(ofClass: URL.self) { object, error in 94 | if let url = object { 95 | graphUrl = url 96 | } 97 | } 98 | return true 99 | } 100 | return false 101 | } 102 | if loading { 103 | Rectangle() 104 | .fill(Color.black.opacity(0.4)) 105 | ProgressView() 106 | } 107 | }.errorAlert(error: $error) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /BuildAnalyzer/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 | -------------------------------------------------------------------------------- /BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/1024-mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/1024-mac.png -------------------------------------------------------------------------------- /BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/128-mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/128-mac.png -------------------------------------------------------------------------------- /BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/16-mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/16-mac.png -------------------------------------------------------------------------------- /BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/256-mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/256-mac.png -------------------------------------------------------------------------------- /BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/32-mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/32-mac.png -------------------------------------------------------------------------------- /BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/512-mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/512-mac.png -------------------------------------------------------------------------------- /BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/64-mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/64-mac.png -------------------------------------------------------------------------------- /BuildAnalyzer/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "1024-mac.png", 5 | "idiom" : "ios-marketing", 6 | "scale" : "1x", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "filename" : "16-mac.png", 11 | "idiom" : "mac", 12 | "scale" : "1x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "32-mac.png", 17 | "idiom" : "mac", 18 | "scale" : "2x", 19 | "size" : "16x16" 20 | }, 21 | { 22 | "filename" : "32-mac.png", 23 | "idiom" : "mac", 24 | "scale" : "1x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "64-mac.png", 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "32x32" 32 | }, 33 | { 34 | "filename" : "128-mac.png", 35 | "idiom" : "mac", 36 | "scale" : "1x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "256-mac.png", 41 | "idiom" : "mac", 42 | "scale" : "2x", 43 | "size" : "128x128" 44 | }, 45 | { 46 | "filename" : "256-mac.png", 47 | "idiom" : "mac", 48 | "scale" : "1x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "512-mac.png", 53 | "idiom" : "mac", 54 | "scale" : "2x", 55 | "size" : "256x256" 56 | }, 57 | { 58 | "filename" : "512-mac.png", 59 | "idiom" : "mac", 60 | "scale" : "1x", 61 | "size" : "512x512" 62 | }, 63 | { 64 | "filename" : "1024-mac.png", 65 | "idiom" : "mac", 66 | "scale" : "2x", 67 | "size" : "512x512" 68 | } 69 | ], 70 | "info" : { 71 | "author" : "xcode", 72 | "version" : 1 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /BuildAnalyzer/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BuildAnalyzer/BuildAnalyzer.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /BuildAnalyzer/BuildAnalyzerApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildAnalyzerApp.swift 3 | // BuildAnalyzer 4 | // 5 | // Created by Bartosz Polaczyk on 6/18/23. 6 | // 7 | 8 | import SwiftUI 9 | import BuildAnalyzerKit 10 | 11 | @main 12 | struct BuildAnalyzerApp: App { 13 | @State private var graph: BuildGraph = .empty 14 | @State private var selection: Set = [] 15 | @State private var focus: String? 16 | private static let DefaultTitle = "XCBuildAnalyzer" 17 | @State private var windowTitle = Self.DefaultTitle 18 | private let manifestFinder = ManifestFinder() 19 | @State private var presentationError: ManifestFinderError? 20 | @State private var loading: Bool = false 21 | @State private var graphStyle: GraphLayoutStyle = .standard 22 | @State private var appAlternatives: AppAlternatives = AppAlternatives() 23 | 24 | var body: some Scene { 25 | Window(Self.DefaultTitle, id: "MainWindow") { 26 | var url: URL? 27 | let graphUrl = Binding(get: { url }, set: { newUrl in 28 | guard !loading else { 29 | return 30 | } 31 | loading = true 32 | Task.detached { 33 | do { 34 | guard let inputUrl = newUrl, let newUrlManifest = try manifestFinder.findLatestManifest(options: .build(project: inputUrl)) else { 35 | return 36 | } 37 | 38 | let newGraph = try buildGraph(manifestLocation: newUrlManifest) 39 | DispatchQueue.main.async { 40 | url = newUrlManifest.manifest 41 | windowTitle = newUrlManifest.projectFile?.absoluteString ?? Self.DefaultTitle 42 | graph = newGraph 43 | selection = [] 44 | focus = nil 45 | loading = false 46 | } 47 | } catch let error as ManifestFinderError { 48 | presentationError = error 49 | loading = false 50 | return 51 | } catch { 52 | presentationError = .otherError(error) 53 | loading = false 54 | return 55 | } 56 | } 57 | }) 58 | let webView = GraphWebView(graph: $graph, graphUrl: graphUrl, selection: $selection, focus: $focus, alternatives: $appAlternatives) 59 | AppView( 60 | selection: $selection, focus: $focus, graph: $graph, graphUrl: graphUrl, graphLayout: $graphStyle, web: webView, error: $presentationError, loading: $loading, appAlternatives: $appAlternatives 61 | ) 62 | .onAppear { 63 | NSEvent.addLocalMonitorForEvents(matching: [.flagsChanged]) { event in 64 | appAlternatives.command = event.modifierFlags.contains([.command]) 65 | return event 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /BuildAnalyzer/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cone.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cone.fill_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cone.fill_in_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cone.fill_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cone_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cone_in_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cone_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube.fill_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube.fill_in_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube.fill_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube.transparent.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube.transparent.fill_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube.transparent.fill_in_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube.transparent.fill_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube.transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube.transparent_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube.transparent_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube_in_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/cube_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/doc.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/doc.fill_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/doc.fill_in_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/doc.fill_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/doc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/doc_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/doc_in_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/doc_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/elypsis.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/elypsis_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/elypsis_in_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/elypsis_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/gate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/gate_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/gate_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/pyramid.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/pyramid.fill_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/pyramid.fill_in_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/pyramid.fill_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/pyramid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/pyramid_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/pyramid_in_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/pyramid_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/question.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/question_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/question_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/scope.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/scope_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/scope_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/shipping.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/shipping_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/shipping_in_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/shipping_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/stop_in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/stop_in_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/img/stop_out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BuildAnalyzer/Resources/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "d3-graphviz": "^5.2.0", 4 | "d3": "7.8.5" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BuildAnalyzer/Utils/ManifestFinderError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManifestFinderError.swift 3 | // BuildAnalyzer 4 | // 5 | // Created by Bartosz Polaczyk on 9/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ManifestFinderError: Error, LocalizedError { 11 | case projectDictionaryNotFound(lookupLocations: [URL]) 12 | case manifestNotFound 13 | case unknownPackageFormat(URL, Error?) 14 | case invalidFileFormat(URL) 15 | case otherError(Error) 16 | 17 | var errorDescription: String? { 18 | switch self { 19 | case .projectDictionaryNotFound: "Cannot find DerivedData for a project" 20 | case .invalidFileFormat: "The format is not supported" 21 | case .unknownPackageFormat: "Unexpected Package.swift format" 22 | case .manifestNotFound: "Xcode-generated manifest files has not been found" 23 | default: "Build manifest json file reading error" 24 | } 25 | } 26 | 27 | var recoverySuggestion: String? { 28 | switch self { 29 | case .projectDictionaryNotFound(let dirs): 30 | return "Make sure you DerivedData dir exist at any of these directories: \n\(dirs.map(\.path).joined(separator: ",\n")).\n\n If the project name does not match, you can manually drag&drop *-manifest.xcbuild or manifest.json files from XCBuildData in your DerivedData" 31 | case .invalidFileFormat(let url): return "The file \(url.path) is not supported. Please open .xcodeproj, .xcworkspace, Package.swift, .playground or raw manifest.json/-manifest.xcbuild" 32 | default: 33 | return "Make sure you built at least once from Xcode" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /BuildAnalyzer/Views/Alerts/AlertView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Alert.swift 3 | // BuildAnalyzer 4 | // 5 | // Created by Bartosz Polaczyk on 9/11/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | @ViewBuilder func errorAlert(error: Binding, buttonTitle: LocalizedStringKey = "OK", action: (() -> Void)? = nil) -> some View where E: LocalizedError{ 12 | let errorValue = error.wrappedValue 13 | alert(isPresented: .constant(errorValue != nil), error: errorValue) { _ in 14 | Button(buttonTitle) { 15 | action?() 16 | error.wrappedValue = nil 17 | } 18 | } message: { error in 19 | Text([error.failureReason, error.recoverySuggestion].compactMap({$0}).joined(separator: "\n\n")) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BuildAnalyzer/Views/HierarchyView/GraphHierarchyElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphHierarchyElement.swift 3 | // BuildAnalyzer 4 | // 5 | // Created by Bartosz Polaczyk on 6/19/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct GraphHierarchyElementInfo: OptionSet { 11 | let rawValue: Int 12 | 13 | static let inCycle = GraphHierarchyElementInfo(rawValue: 1 << 0) 14 | static let active = GraphHierarchyElementInfo(rawValue: 1 << 1) 15 | } 16 | 17 | struct GraphHierarchyElement: Identifiable, Equatable { 18 | let id: String 19 | let name: String 20 | let info: GraphHierarchyElementInfo? 21 | var items: [GraphHierarchyElement]? 22 | } 23 | 24 | extension GraphHierarchyElement { 25 | func filter(_ string: String) -> GraphHierarchyElement? { 26 | guard string != "" else { return self } 27 | guard items != nil else { 28 | // do not modify unnecessary 29 | return filter(search: string) ? self : nil 30 | } 31 | let children = (items ?? []).compactMap( { $0.filter(string) }) 32 | if children.count > 0 || filter(search: string) { 33 | return .init(id: name, name: name, info: info, items: children) 34 | } 35 | return nil 36 | } 37 | 38 | private func filter(search: String) -> Bool { 39 | return name.lowercased().contains(search.lowercased()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /BuildAnalyzer/Views/HierarchyView/GraphHierarchyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GraphHierarchyView.swift 3 | // BuildAnalyzer 4 | // 5 | // Created by Bartosz Polaczyk on 6/19/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import Combine 11 | import BuildAnalyzerKit 12 | 13 | 14 | // Represent left pane - a list of nodes in a graph aggregated by types 15 | struct GraphHierarchyView: View { 16 | @Binding var selection: Set 17 | @Binding var focus: String? 18 | // Actual search (throttled) 19 | @Binding var search: String 20 | var graph: BuildGraph 21 | @State var viewSelection: Set 22 | 23 | @State private var searchRaw: String = "" 24 | let searchTextPublisher = PassthroughSubject() 25 | private let items: [GraphHierarchyElement] 26 | 27 | init( 28 | selection: Binding>, 29 | graph: BuildGraph, 30 | search: Binding, 31 | focus: Binding, 32 | hierarchyBuilder: GraphHierarchyContentBuilderProtocol 33 | ) { 34 | viewSelection = selection.wrappedValue 35 | self._selection = selection 36 | self.graph = graph 37 | self._search = search 38 | self._focus = focus 39 | items = hierarchyBuilder.build(from: graph).compactMap {$0.filter(search.wrappedValue)} 40 | } 41 | 42 | var body: some View { 43 | VStack { 44 | List(items, children: \.items, selection: $viewSelection) { row in 45 | HStack { 46 | 47 | Text(row.name) 48 | .help(row.name) 49 | .lineLimit(1) 50 | .truncationMode(.middle) 51 | .onChange(of: viewSelection) { v in 52 | focus = v.first 53 | selection = v 54 | } 55 | .frame(maxWidth: .infinity, alignment: .leading) 56 | if row.info?.contains(.active) == true { Image(systemName: "hammer.circle").help("Executed in a last build") } 57 | if row.info?.contains(.inCycle) == true { Image(systemName: "exclamationmark.arrow.circlepath").foregroundColor(.yellow).help("Exists in a cycle") } 58 | } 59 | } 60 | TextField("Search", text: $searchRaw) 61 | .padding(5) 62 | .onChange(of: searchRaw) { searchText in 63 | searchTextPublisher.send(searchText) 64 | } 65 | .onReceive( 66 | searchTextPublisher 67 | .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) 68 | ) { debouncedSearchText in 69 | self.search = debouncedSearchText 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /BuildAnalyzer/Views/WebView/D3Projector/D3PageRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D3PageRequest.swift 3 | // BuildAnalyzer 4 | // 5 | // Created by Bartosz Polaczyk on 9/13/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct D3PageRequest: Encodable { 11 | struct Option: Encodable { 12 | let reset: Bool 13 | 14 | static let noop = Option(reset: false) 15 | } 16 | 17 | var option: Option 18 | /// D3 graphiz content 19 | var graph: String? 20 | /// extra config of a digraph 21 | var extra: String? 22 | /// the id (e.g. N1) of the node that should be selected 23 | var highlight: String? 24 | } 25 | -------------------------------------------------------------------------------- /BuildAnalyzer/Views/WebView/D3Projector/D3PageResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D3PageResponse.swift 3 | // BuildAnalyzer 4 | // 5 | // Created by Bartosz Polaczyk on 6/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct D3PageResponse: Decodable { 11 | enum Message: String, Decodable { 12 | case selected 13 | case expandIn 14 | case expandOut 15 | } 16 | let msg: Message 17 | let id: String 18 | } 19 | -------------------------------------------------------------------------------- /BuildAnalyzer/Views/WebView/LayoutStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutStyle.swift 3 | // BuildAnalyzer 4 | // 5 | // Created by Bartosz Polaczyk on 9/14/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum GraphLayoutStyle { 11 | case standard 12 | case circo 13 | } 14 | -------------------------------------------------------------------------------- /BuildAnalyzerTests/BuildAnalyzerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildAnalyzerTests.swift 3 | // BuildAnalyzerTests 4 | // 5 | // Created by Bartosz Polaczyk on 6/18/23. 6 | // 7 | 8 | import XCTest 9 | @testable import BuildAnalyzer 10 | 11 | final class BuildAnalyzerTests: 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 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /BuildAnalyzerUITests/BuildAnalyzerUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildAnalyzerUITests.swift 3 | // BuildAnalyzerUITests 4 | // 5 | // Created by Bartosz Polaczyk on 6/18/23. 6 | // 7 | 8 | import XCTest 9 | 10 | final class BuildAnalyzerUITests: 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 | -------------------------------------------------------------------------------- /BuildAnalyzerUITests/BuildAnalyzerUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildAnalyzerUITestsLaunchTests.swift 3 | // BuildAnalyzerUITests 4 | // 5 | // Created by Bartosz Polaczyk on 6/18/23. 6 | // 7 | 8 | import XCTest 9 | 10 | final class BuildAnalyzerUITestsLaunchTests: 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Bartosz Polaczyk. 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let swiftSettings: [SwiftSetting] = [ 7 | // -enable-bare-slash-regex becomes 8 | .enableUpcomingFeature("BareSlashRegexLiterals"), 9 | ] 10 | 11 | let package = Package( 12 | name: "BuildAnalyzer", 13 | platforms: [ 14 | .macOS(.v13), 15 | ], 16 | products: [ 17 | // Products define the executables and libraries a package produces, making them visible to other packages. 18 | .library( 19 | name: "BuildAnalyzerKit", 20 | targets: ["BuildAnalyzerKit"]), 21 | .library( 22 | name: "GraphKit", 23 | targets: ["GraphKit"]), 24 | .library( 25 | name: "XcodeHasher", 26 | targets: ["XcodeHasher"]), 27 | ], dependencies: [ 28 | .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.1"), 29 | .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.3.3"), 30 | ], 31 | targets: [ 32 | .target( 33 | name: "BuildAnalyzerKit", 34 | dependencies: [.product(name: "SQLite", package: "SQLite.swift")], 35 | // -enable-bare-slash-regex becomes 36 | swiftSettings: swiftSettings 37 | ), 38 | .target( 39 | name: "GraphKit"), 40 | .testTarget( 41 | name: "BuildAnalyzerKitTests", 42 | dependencies: ["BuildAnalyzerKit"]), 43 | .testTarget( 44 | name: "GraphKitTests", 45 | dependencies: ["GraphKit"]), 46 | .target( 47 | name:"XcodeHasher", 48 | dependencies: ["CryptoSwift"] 49 | ), 50 | ] 51 | ) 52 | -------------------------------------------------------------------------------- /Sources/BuildAnalyzerKit/BuildGraphGenerator.swift: -------------------------------------------------------------------------------- 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | 4 | import Foundation 5 | 6 | 7 | // MARK: Building a graph 8 | 9 | public protocol BuildGraphGenerator { 10 | func build() throws -> BuildGraphProtocol 11 | } 12 | 13 | public class FileGraphGenerator: BuildGraphGenerator { 14 | 15 | private let path: URL 16 | 17 | public init(_ path: URL){ 18 | self.path = path 19 | } 20 | 21 | public func build() throws -> BuildGraphProtocol { 22 | return BuildGraph(nodes: [:], cycles: [], buildInterval: nil) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/BuildAnalyzerKit/BuildGraphNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bartosz Polaczyk on 6/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /// The node identifier in the the Build Graph context 12 | /// (Now, the id represents the command id string) 13 | public struct BuildGraphNodeId: Hashable, Equatable { 14 | public let id: String 15 | 16 | public init(id: String) { 17 | self.id = id 18 | } 19 | } 20 | 21 | // Not in use for now 22 | public typealias BuildGraphEdgeId = String 23 | public struct BuildGraphEdge: Hashable, Equatable { 24 | let source: BuildGraphNodeId 25 | let destination: BuildGraphNodeId 26 | 27 | public init(source: BuildGraphNodeId, destination: BuildGraphNodeId) { 28 | self.source = source 29 | self.destination = destination 30 | } 31 | } 32 | 33 | public struct BuildGraphNode: Hashable, Equatable { 34 | public typealias Property = String 35 | public typealias PropertyValue = String 36 | 37 | public let id: BuildGraphNodeId 38 | public let tool: String 39 | public let name: String 40 | /// Generic properties that should be presented in the "Details" section 41 | public var properties: [Property: PropertyValue] 42 | public var inputs: Set 43 | public var outputs: Set 44 | public let env: [String: String]? 45 | public let description: String? 46 | public let roots: [String]? 47 | public let expectedOutputs: [String]? 48 | public let args: [String]? 49 | public let signature: String? 50 | public let workingDirectory: String? 51 | public let timing: BuildGraphNodeTiming? 52 | public let kind: BuildGraphNode.Kind 53 | public let type: BuildGraphNode.NodeType 54 | 55 | public init(id: BuildGraphNodeId, tool: String, name: String, properties: [Property : PropertyValue], inputs: Set, outputs: Set, expectedOutputs: [String]?, roots: [String]?, env: [String: String]?, description: String?, args: [String]?, signature: String?, workingDirectory: String?, timing: BuildGraphNodeTiming?, type: NodeType) { 56 | self.id = id 57 | self.tool = tool 58 | self.name = name 59 | self.properties = properties 60 | self.inputs = inputs 61 | self.outputs = outputs 62 | self.env = env 63 | self.timing = timing 64 | self.kind = Kind.generateKind(name: name) 65 | self.args = args 66 | self.signature = signature 67 | self.workingDirectory = workingDirectory 68 | self.description = description 69 | self.roots = roots 70 | self.expectedOutputs = expectedOutputs 71 | self.type = type 72 | } 73 | } 74 | 75 | // MARK: Helper static methods to build Node and NodeId 76 | 77 | extension BuildGraphNode { 78 | static func buildEmpty(id: BuildGraphNodeId, name: String) -> Self { 79 | return .init( 80 | id: id, 81 | tool: "", 82 | name: name, 83 | properties: [:], 84 | inputs: [], 85 | outputs: [], 86 | expectedOutputs: nil, 87 | roots: nil, 88 | env: [:], 89 | description: nil, 90 | args: nil, 91 | signature: nil, 92 | workingDirectory: nil, 93 | timing: nil, 94 | type: .node 95 | ) 96 | } 97 | } 98 | 99 | extension BuildGraphNodeId { 100 | init(nodeName: BuildManifestId) { 101 | self.id = nodeName 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/BuildAnalyzerKit/BuildGraphProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bartosz Polaczyk on 6/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | public protocol BuildGraphProtocol: AnyObject { 12 | var nodes: [BuildGraphNodeId: BuildGraphNode] { get } 13 | var cycles: [[BuildGraphNodeId]] {get} 14 | 15 | func expand(projection: BuildGraphProjection, with: BuildGraphProjectionExpansion) -> BuildGraphProjection 16 | } 17 | 18 | public extension BuildGraphProtocol { 19 | var cycleNodes: [BuildGraphNodeId] { 20 | return cycles.flatMap({$0}) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/BuildAnalyzerKit/BuildManifestParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bartosz Polaczyk on 6/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias BuildManifestTool = String 11 | public typealias BuildManifestId = String 12 | 13 | public struct BuildManifestCommand: Codable, Hashable { 14 | 15 | enum CodingKeys: String, CodingKey { 16 | case tool 17 | case inputs 18 | case outputs 19 | case expectedOutputs 20 | case roots 21 | case env 22 | case description 23 | case args 24 | case signature 25 | case workingDirectory = "working-directory" 26 | case alwaysOutOfDate = "always-out-of-date" 27 | } 28 | 29 | var tool: BuildManifestTool 30 | var inputs: [String]? 31 | var outputs: [String]? 32 | var expectedOutputs: [String]? 33 | var roots: [String]? 34 | var env: [String: String]? 35 | var description: String? 36 | var args: [String]? 37 | var signature: String? 38 | var workingDirectory: String? 39 | var alwaysOutOfDate: Bool? 40 | } 41 | 42 | public struct BuildManifestClient: Codable { 43 | let version: Int 44 | let name: String 45 | } 46 | 47 | public struct BuildManifest: Codable { 48 | let client: BuildManifestClient 49 | /// Actually all nodes 50 | let commands: [String: BuildManifestCommand] 51 | /// All targets that are requested 52 | let targets: [String: [String]] 53 | 54 | public init( 55 | client: BuildManifestClient, 56 | commands: [String : BuildManifestCommand], 57 | targets: [String: [String]] 58 | ) { 59 | self.commands = commands 60 | self.client = client 61 | self.targets = targets 62 | } 63 | } 64 | 65 | public class BuildManifestParser { 66 | private let decoder: JSONDecoder 67 | 68 | public init() { 69 | decoder = JSONDecoder() 70 | } 71 | 72 | public func process(_ path: String) throws -> BuildManifest { 73 | return try process(URL(fileURLWithPath: path)) 74 | } 75 | 76 | public func process(_ url: URL) throws -> BuildManifest { 77 | let data = try Data(contentsOf: url) 78 | return try decoder.decode(BuildManifest.self, from: data) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/BuildAnalyzerKit/DataStructure/Queue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bartosz Polaczyk on 9/9/23. 6 | // 7 | 8 | import Foundation 9 | 10 | class Queue { 11 | class Node { 12 | var next: Node? 13 | var prev: Node? 14 | let value: T 15 | 16 | init(next: Node? = nil, prev: Node? = nil, value: T) { 17 | self.next = next 18 | self.prev = prev 19 | self.value = value 20 | } 21 | } 22 | private(set) var head: Node? 23 | private(set) var tail: Node? 24 | 25 | func enqueue(_ value: T) { 26 | let newNode = Node(prev: tail, value: value) 27 | guard let last = tail else { 28 | head = newNode 29 | tail = newNode 30 | return 31 | } 32 | last.next = newNode 33 | tail = newNode 34 | } 35 | 36 | func dequeue() -> T? { 37 | guard let first = head else { 38 | return nil 39 | } 40 | head = first.next 41 | if head == nil { 42 | tail = nil 43 | } 44 | return first.value 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/BuildAnalyzerKit/Projection/BuildGraphNodeProjectionNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bartosz Polaczyk on 6/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | public struct BuildGraphNodeProjectionNode: Comparable { 12 | // An opinionated sorting order - it is used to have a deterministic projection in the UI 13 | public static func < (lhs: BuildGraphNodeProjectionNode, rhs: BuildGraphNodeProjectionNode) -> Bool { 14 | lhs.node.id < rhs.node.id 15 | } 16 | 17 | public let node: BuildGraphNodeId 18 | public internal(set) var inputNodes: Set 19 | public internal(set) var outputNodes: Set 20 | 21 | public internal(set) var hidesSomeInputs: Bool 22 | public internal(set) var hidesSomeOutputs: Bool 23 | public internal(set) var level: Int 24 | public internal(set) var highlighted: Bool 25 | 26 | public init( 27 | node: BuildGraphNodeId, 28 | inputNodes: Set, 29 | outputNodes: Set, 30 | hidesSomeInputs: Bool, 31 | hidesSomeOutputs: Bool, 32 | level: Int, 33 | highlighted: Bool 34 | ) { 35 | self.node = node 36 | self.inputNodes = inputNodes 37 | self.outputNodes = outputNodes 38 | self.hidesSomeInputs = hidesSomeInputs 39 | self.hidesSomeOutputs = hidesSomeOutputs 40 | self.level = level 41 | self.highlighted = highlighted 42 | } 43 | } 44 | 45 | extension BuildGraphNodeProjectionNode { 46 | /// Builds an isolated projection node that contracts all input and outpus 47 | func buildIsolated(for node: BuildGraphNode) -> Self { 48 | return .init( 49 | node: node.id, 50 | inputNodes: [], 51 | outputNodes: [], 52 | hidesSomeInputs: !node.inputs.isEmpty, 53 | hidesSomeOutputs: !node.outputs.isEmpty, 54 | level: 0, 55 | highlighted: false 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/BuildAnalyzerKit/Projection/BuildGraphProjection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bartosz Polaczyk on 6/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// How should the graph be layout in the UI 11 | public enum BuildGraphProjectionLayoutType { 12 | /// The standard graph layout 13 | case flow 14 | /// Show nodes in a circular manner (e.g. when a cycle has been found) 15 | case circular 16 | } 17 | 18 | /// Describes which part the build graph should be presented 19 | /// The`BuildGraphView` might be a better name, but to not mislead with UI views 20 | /// using a "projection" term 21 | public protocol BuildGraphProjection { 22 | var type: BuildGraphProjectionLayoutType { get set } 23 | var nodes: [BuildGraphNodeId: BuildGraphNodeProjectionNode] { get set } 24 | var highlightedEdges: Set {get set} 25 | } 26 | 27 | public extension BuildGraphProjection { 28 | var highlightedNodes: Set { 29 | set { 30 | nodes = nodes.mapValues { node in 31 | var newNode = node 32 | newNode.highlighted = newValue.contains(node.node) 33 | return newNode 34 | } 35 | } 36 | get { 37 | Set(nodes.values.filter({$0.highlighted}).map(\.node)) 38 | } 39 | } 40 | } 41 | 42 | // Consider skipping the Protocol/Impl 43 | public class BuildGraphProjectionImpl: BuildGraphProjection { 44 | public var type: BuildGraphProjectionLayoutType 45 | public var nodes: [BuildGraphNodeId: BuildGraphNodeProjectionNode] 46 | public var highlightedEdges: Set 47 | 48 | public init(nodes: [BuildGraphNodeProjectionNode], type: BuildGraphProjectionLayoutType, highlightedEdges: Set) { 49 | self.nodes = nodes.reduce(into: [:], { hash, node in 50 | hash[node.node] = node 51 | }) 52 | self.type = type 53 | self.highlightedEdges = highlightedEdges 54 | } 55 | } 56 | 57 | 58 | extension BuildGraphProjectionImpl { 59 | public static var empty: BuildGraphProjectionImpl { 60 | return BuildGraphProjectionImpl(nodes: [], type: .flow, highlightedEdges: []) 61 | } 62 | } 63 | 64 | extension BuildGraphProjectionImpl { 65 | public convenience init(startingNode: BuildGraphNodeId) { 66 | // Assume the node hides something by default. Otherwise, it wouldn't be considered in the appending flow 67 | self.init(nodes: [ 68 | .init(node: startingNode, inputNodes: [], outputNodes: [], hidesSomeInputs: true, hidesSomeOutputs: true, level: 0, highlighted: false) 69 | ], type: .flow, highlightedEdges: []) 70 | } 71 | } 72 | 73 | extension BuildGraphProjectionImpl { 74 | public convenience init(startingNodes: Set, highlightedNodes: [BuildGraphNodeId]) { 75 | let allNodes = startingNodes.map { startingNodeId in 76 | BuildGraphNodeProjectionNode(node: startingNodeId, inputNodes: [], outputNodes: [], hidesSomeInputs: true, hidesSomeOutputs: true, level: 0, highlighted: highlightedNodes.contains(startingNodeId)) 77 | } 78 | self.init(nodes: allNodes, type: .flow, highlightedEdges: []) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/BuildAnalyzerKit/Projection/BuildGraphProjectionExpansion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bartosz Polaczyk on 6/18/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Describes how the projection should be expanded 11 | public enum BuildGraphProjectionExpansion { 12 | /// user wants to see more inputs of a node 13 | case inputs(of: BuildGraphNodeId, count: Int = 100) 14 | /// user wants to see more outputs of a node 15 | case outputs(of: BuildGraphNodeId, count: Int = 100) 16 | /// include a cycle of nodes 17 | case cycle(of: BuildGraphNodeId, cycle: [BuildGraphNodeId]) 18 | /// path between few nodes 19 | case path(nodes: Set) 20 | } 21 | 22 | extension BuildGraphProjectionExpansion { 23 | var nodeId: BuildGraphNodeId { 24 | switch self { 25 | case .inputs(of: let nodeId, _): 26 | return nodeId 27 | case .outputs(of: let nodeId, _): 28 | return nodeId 29 | case .cycle(of: let nodeId, _): 30 | return nodeId 31 | case .path(let nodes): 32 | // take any node (pick any) 33 | return nodes.first! 34 | } 35 | } 36 | } 37 | 38 | extension BuildGraphProjectionExpansion { 39 | var levelDirection: Int { 40 | switch self { 41 | case .inputs: 42 | return -1 43 | case .outputs: 44 | return 1 45 | case .cycle: 46 | return -2 47 | case .path: 48 | return 0 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/BuildAnalyzerKit/Timing/BuildGraphNodeTimingReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bartosz Polaczyk on 8/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct BuildGraphNodeTimingIdExtra: Hashable { 11 | let nodeId: BuildGraphNodeId 12 | let type: String 13 | } 14 | 15 | public typealias BuildGraphNodeTimingId = BuildGraphNodeId 16 | 17 | public struct BuildGraphNodeTiming: Hashable { 18 | public let node: BuildGraphNodeTimingId 19 | public let start: Double 20 | public let end: Double 21 | // 0 - 100% of the start in the entire build 22 | public let percentage: Double 23 | 24 | public init(node: BuildGraphNodeTimingId, start: Double, end: Double, percentage: Double) { 25 | self.node = node 26 | self.start = start 27 | self.end = end 28 | self.percentage = percentage 29 | } 30 | } 31 | 32 | public extension BuildGraphNodeTiming { 33 | var duration: Double { 34 | end - start 35 | } 36 | } 37 | 38 | public protocol BuildGraphNodeTimingReader { 39 | func read() throws -> [BuildGraphNodeTimingId: BuildGraphNodeTiming] 40 | } 41 | -------------------------------------------------------------------------------- /Sources/BuildAnalyzerKit/Timing/BuildGraphNodeTimingSqlReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bartosz Polaczyk on 8/12/23. 6 | // 7 | 8 | import Foundation 9 | import SQLite 10 | 11 | public enum BuildGraphNodeTimingSqlReaderError: Error { 12 | case unexpectedRowFormat(Binding?) 13 | } 14 | 15 | public class BuildGraphNodeTimingSqlReader: BuildGraphNodeTimingReader { 16 | let file: URL 17 | 18 | public init(file: URL) { 19 | self.file = file 20 | } 21 | 22 | 23 | public func read() throws -> [BuildGraphNodeTimingId: BuildGraphNodeTiming] { 24 | 25 | var result: [BuildGraphNodeTimingId: BuildGraphNodeTiming] = [:] 26 | let db = try Connection(file.absoluteString, readonly: true) 27 | 28 | let iteration: Int64 = try read(try db.scalar("SELECT max(iteration) FROM info")) 29 | let buildStart = try readDate(try db.scalar("SELECT min(start) FROM rule_results where built_at = \(iteration)")) 30 | let buildEnd = try readDate(try db.scalar("SELECT max(end) FROM rule_results where built_at = \(iteration)")) 31 | let buildDuration = buildEnd - buildStart 32 | 33 | for row in try db.prepare("SELECT start, end, key_names.key, key_id from rule_results inner join key_names on rule_results.key_id = key_names.id where built_at = \(iteration)") { 34 | // first char in row[2] is a type (C,K etc). Example: "C 35 | let nodeId = try readNodeId(row[2]) 36 | 37 | let start = try readDate(row[0]) 38 | let end = try readDate(row[1]) 39 | 40 | let node = nodeId 41 | let timing = BuildGraphNodeTiming( 42 | node: node, 43 | start: start, 44 | end: end, 45 | percentage: (start - buildStart)/(buildDuration) 46 | ) 47 | result[node] = timing 48 | } 49 | return result 50 | } 51 | 52 | private func readDate(_ rowOptional: Binding?) throws -> Double { 53 | guard let row = rowOptional, let time = row as? Double else { 54 | throw BuildGraphNodeTimingSqlReaderError.unexpectedRowFormat(rowOptional) 55 | } 56 | return time 57 | } 58 | 59 | private func readNodeId(_ rowOptional: Binding?) throws -> BuildGraphNodeTimingId { 60 | guard let row = rowOptional, let nodeId = row as? String else { 61 | throw BuildGraphNodeTimingSqlReaderError.unexpectedRowFormat(rowOptional) 62 | } 63 | return BuildGraphNodeId(id: String(nodeId.dropFirst())) 64 | } 65 | 66 | private func read(_ rowOptional: Binding?) throws -> T { 67 | guard let row = rowOptional, let value = row as? T else { 68 | throw BuildGraphNodeTimingSqlReaderError.unexpectedRowFormat(rowOptional) 69 | } 70 | return value 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/GraphKit/D3Projector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Bartosz Polaczyk on 6/19/23. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /Sources/XcodeHasher/XcodeHasher.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 CryptoSwift 22 | 23 | // Thanks to https://pewpewthespells.com/blog/xcode_deriveddata_hashes.html for 24 | // the initial Objective-C implementation. 25 | public class XcodeHasher { 26 | 27 | enum HashingError: Error { 28 | case invalidPartitioning 29 | } 30 | 31 | public static func hashString(for path: String) throws -> String { 32 | // Initialize a 28 `String` array since we can't initialize empty `Character`s. 33 | var result = Array(repeating: "", count: 28) 34 | 35 | // Compute md5 hash of the path 36 | let digest = path.bytes.md5() 37 | 38 | // Split 16 bytes into two chunks of 8 bytes each. 39 | let partitions = stride(from: 0, to: digest.count, by: 8).map { 40 | Array(digest[$0..=5.0) 47 | var startValue = UInt64(bigEndian: Data(firstHalf).withUnsafeBytes { $0.load(as: UInt64.self) }) 48 | #else 49 | var startValue = UInt64(bigEndian: Data(firstHalf).withUnsafeBytes { $0.pointee }) 50 | #endif 51 | for index in stride(from: 13, through: 0, by: -1) { 52 | // Take the startValue % 26 to restrict to alphabetic characters and add 'a' scalar value (97). 53 | let char = String(UnicodeScalar(Int(startValue % 26) + 97)!) 54 | result[index] = char 55 | startValue /= 26 56 | } 57 | // We would need to reverse the bytes, so we just read them in big endian. 58 | #if swift(>=5.0) 59 | startValue = UInt64(bigEndian: Data(secondHalf).withUnsafeBytes { $0.load(as: UInt64.self) }) 60 | #else 61 | startValue = UInt64(bigEndian: Data(secondHalf).withUnsafeBytes { $0.pointee }) 62 | #endif 63 | for index in stride(from: 27, through: 14, by: -1) { 64 | // Take the startValue % 26 to restrict to alphabetic characters and add 'a' scalar value (97). 65 | let char = String(UnicodeScalar(Int(startValue % 26) + 97)!) 66 | result[index] = char 67 | startValue /= 26 68 | } 69 | 70 | return result.joined() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/BuildAnalyzerKitTests/AnalyzerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class BuildAnalyzerTests: XCTestCase { 4 | func testExample() throws { 5 | // XCTest Documentation 6 | // https://developer.apple.com/documentation/xctest 7 | 8 | // Defining Test Cases and Test Methods 9 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/GraphKitTests/StartingFileTests.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/Tests/GraphKitTests/StartingFileTests.swift -------------------------------------------------------------------------------- /docs/img/cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/docs/img/cycle.png -------------------------------------------------------------------------------- /docs/img/cycle_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/docs/img/cycle_warning.png -------------------------------------------------------------------------------- /docs/img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/docs/img/demo.gif -------------------------------------------------------------------------------- /docs/img/details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/docs/img/details.png -------------------------------------------------------------------------------- /docs/img/executed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/docs/img/executed.png -------------------------------------------------------------------------------- /docs/img/expand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/docs/img/expand.png -------------------------------------------------------------------------------- /docs/img/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/docs/img/graph.png -------------------------------------------------------------------------------- /docs/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/docs/img/icon.png -------------------------------------------------------------------------------- /docs/img/subgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/docs/img/subgraph.png -------------------------------------------------------------------------------- /docs/img/timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polac24/XCBuildAnalyzer/39e1c2346397627f1a5490933485126872089e79/docs/img/timing.png -------------------------------------------------------------------------------- /scripts/dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Downloads all external dependencies 4 | 5 | 6 | #### Node.js deps (for graph visualization) 7 | 8 | repo=$(git rev-parse --show-toplevel) 9 | pushd $repo/BuildAnalyzer/Resources 10 | npm install 11 | popd 12 | --------------------------------------------------------------------------------