├── .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 |
9 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cone.fill_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cone.fill_in_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cone.fill_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cone.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cone_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cone_in_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cone_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube.fill.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube.fill_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube.fill_in_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube.fill_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube.transparent.fill.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube.transparent.fill_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube.transparent.fill_in_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
20 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube.transparent.fill_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
20 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube.transparent.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube.transparent_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube.transparent_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube_in_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/cube_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/doc.fill.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/doc.fill_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/doc.fill_in_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/doc.fill_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/doc.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/doc_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/doc_in_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/doc_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/elypsis.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/elypsis_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/elypsis_in_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/elypsis_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/gate.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/gate_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/gate_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
16 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/pyramid.fill.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/pyramid.fill_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/pyramid.fill_in_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/pyramid.fill_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/pyramid.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/pyramid_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/pyramid_in_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/pyramid_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/question.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/question_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/question_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/scope.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/scope_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/scope_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/shipping.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/shipping_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/shipping_in_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/shipping_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/stop.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/stop_in.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/stop_in_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
--------------------------------------------------------------------------------
/BuildAnalyzer/Resources/img/stop_out.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------