File: DotPIFSerializer.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (239 lines) | stat: -rw-r--r-- 7,124 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
//
//  DotPIFSerializer.swift
//  SwiftPM
//
//  Created by Paulo Mattos on 2025-04-18.
//

import Basics
import Foundation
import protocol TSCBasic.OutputByteStream

import SwiftBuild

/// Serializes the specified PIF as a **Graphviz** directed graph.
///
/// * [DOT command line](https://graphviz.org/doc/info/command.html)
/// * [DOT language specs](https://graphviz.org/doc/info/lang.html)
func writePIF(_ workspace: PIF.Workspace, toDOT outputStream: OutputByteStream) {
    var graph = DotPIFSerializer()

    graph.node(
        id: workspace.id,
        label: """
            <workspace>
            \(workspace.id)
            """,
        shape: "box3d",
        color: .black,
        fontsize: 7
    )

    for project in workspace.projects.map(\.underlying) {
        graph.edge(from: workspace.id, to: project.id, color: .lightskyblue)
        graph.node(
            id: project.id,
            label: """
                <project>
                \(project.id)
                """,
            shape: "box3d",
            color: .gray56,
            fontsize: 7
        )

        for target in project.targets {
            graph.edge(from: project.id, to: target.id, color: .lightskyblue)

            switch target {
            case .target(let target):
                graph.node(
                    id: target.id,
                    label: """
                        <target>
                        \(target.id)
                        name: \(target.name)
                        product type: \(target.productType)
                        \(target.buildPhases.summary)
                        """,
                    shape: "box",
                    color: .gray88,
                    fontsize: 5
                )

            case .aggregate:
                graph.node(
                    id: target.id,
                    label: """
                        <aggregate target>
                        \(target.id)
                        """,
                    shape: "folder",
                    color: .gray88,
                    fontsize: 5,
                    style: "bold"
                )
            }

            for targetDependency in target.common.dependencies {
                let linked = target.isLinkedAgainst(dependencyId: targetDependency.targetId)
                graph.edge(from: target.id, to: targetDependency.targetId, color: .gray40, style: linked ? "filled" : "dotted")
            }
        }
    }

    graph.write(to: outputStream)
}

fileprivate struct DotPIFSerializer {
    private var objects: [String] = []

    mutating func write(to outputStream: OutputByteStream) {
        func write(_ object: String) { outputStream.write("\(object)\n") }

        write("digraph PIF {")
        write("  dpi=400;") // i.e., MacBook Pro 16" is 226 pixels per inch (3072 x 1920).
        for object in objects {
            write("  \(object);")
        }
        write("}")
    }

    mutating func node(
        id: PIF.GUID,
        label: String? = nil,
        shape: String? = nil,
        color: Color? = nil,
        fontname: String? = "SF Mono Light",
        fontsize: Int? = nil,
        style: String? = nil,
        margin: Int? = nil
    ) {
        var attributes: [String] = []

        if let label { attributes.append("label=\(label.quote)") }
        if let shape { attributes.append("shape=\(shape)") }
        if let color { attributes.append("color=\(color)") }

        if let fontname { attributes.append("fontname=\(fontname.quote)") }
        if let fontsize { attributes.append("fontsize=\(fontsize)") }

        if let style { attributes.append("style=\(style)") }
        if let margin { attributes.append("margin=\(margin)") }

        var node = "\(id.quote)"
        if !attributes.isEmpty {
            let attributesList = attributes.joined(separator: ", ")
            node += " [\(attributesList)]"
        }
        objects.append(node)
    }

    mutating func edge(
        from left: PIF.GUID,
        to right: PIF.GUID,
        color: Color? = nil,
        style: String? = nil
    ) {
        var attributes: [String] = []

        if let color { attributes.append("color=\(color)") }
        if let style { attributes.append("style=\(style)") }

        var edge = "\(left.quote) -> \(right.quote)"
        if !attributes.isEmpty {
            let attributesList = attributes.joined(separator: ", ")
            edge += " [\(attributesList)]"
        }
        objects.append(edge)
    }

    /// Graphviz  default color scheme is **X11**:
    /// * https://graphviz.org/doc/info/colors.html
    enum Color: String {
        case black
        case gray
        case gray40
        case gray56
        case gray88
        case lightskyblue
    }
}

// MARK: - Helpers

fileprivate extension ProjectModel.BaseTarget {
    func isLinkedAgainst(dependencyId: ProjectModel.GUID) -> Bool {
        for buildPhase in self.common.buildPhases {
            switch buildPhase {
            case .frameworks(let frameworksPhase):
                for buildFile in frameworksPhase.files {
                    switch buildFile.ref {
                    case .reference(let id):
                        if dependencyId == id { return true }
                    case .targetProduct(let id):
                        if dependencyId == id { return true }
                    }
                }

            case .sources, .shellScript, .headers, .copyFiles, .copyBundleResources:
                break
            }
        }
        return false
    }
}

fileprivate extension [ProjectModel.BuildPhase] {
    var summary: String {
        var phases: [String] = []

        for buildPhase in self {
            switch buildPhase {
            case .sources(let sourcesPhase):
                var sources = "sources: "
                if sourcesPhase.files.count == 1 {
                    sources += "1 source file"
                } else {
                    sources += "\(sourcesPhase.files.count) source files"
                }
                phases.append(sources)

            case .frameworks(let frameworksPhase):
                var frameworks = "frameworks: "
                if frameworksPhase.files.count == 1 {
                    frameworks += "1 linked target"
                } else {
                    frameworks += "\(frameworksPhase.files.count) linked targets"
                }
                phases.append(frameworks)

            case .shellScript:
                phases.append("shellScript: 1 shell script")

            case .headers, .copyFiles, .copyBundleResources:
                break
            }
        }

        guard !phases.isEmpty else { return "" }
        return phases.joined(separator: "\n")
    }
}

fileprivate extension PIF.GUID {
    var quote: String {
        self.value.quote
    }
}

fileprivate extension String {
    /// Quote the name and escape the quotes and backslashes.
    var quote: String {
        "\"" + self
            .replacing("\"", with: "\\\"")
            .replacing("\\", with: "\\\\")
            .replacing("\n", with: "\\n") +
        "\""
    }
}