From b35358fdb0fa86248b6337823ede6d44f872d9a1 Mon Sep 17 00:00:00 2001 From: Artem Chikin Date: Thu, 8 Sep 2022 16:31:59 -0700 Subject: [PATCH] Add a Dot serializer for the inter-module dependency graph It can be nice to be able to visualize all of a module's dependencies. --- Sources/SwiftDriver/CMakeLists.txt | 1 + .../DOTModuleDependencyGraphSerializer.swift | 76 +++++++++++++++++++ .../ExplicitModuleBuildTests.swift | 63 +++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 Sources/SwiftDriver/Utilities/DOTModuleDependencyGraphSerializer.swift diff --git a/Sources/SwiftDriver/CMakeLists.txt b/Sources/SwiftDriver/CMakeLists.txt index 2b919ae66..b0873fcd3 100644 --- a/Sources/SwiftDriver/CMakeLists.txt +++ b/Sources/SwiftDriver/CMakeLists.txt @@ -98,6 +98,7 @@ add_library(SwiftDriver Toolchains/WindowsToolchain.swift Utilities/DOTJobGraphSerializer.swift + Utilities/DOTModuleDependencyGraphSerializer.swift Utilities/DateAdditions.swift Utilities/Diagnostics.swift Utilities/FileList.swift diff --git a/Sources/SwiftDriver/Utilities/DOTModuleDependencyGraphSerializer.swift b/Sources/SwiftDriver/Utilities/DOTModuleDependencyGraphSerializer.swift new file mode 100644 index 000000000..7c82fb293 --- /dev/null +++ b/Sources/SwiftDriver/Utilities/DOTModuleDependencyGraphSerializer.swift @@ -0,0 +1,76 @@ +//===----------- DOTModuleDependencyGraphSerializer.swift - Swift ---------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import TSCBasic + +/// Serializes a module dependency graph to a .dot graph +@_spi(Testing) public struct DOTModuleDependencyGraphSerializer { + let graph: InterModuleDependencyGraph + + public init(_ interModuleDependencyGraph: InterModuleDependencyGraph) { + self.graph = interModuleDependencyGraph + } + + func label(for moduleId: ModuleDependencyId) -> String { + let label: String + switch moduleId { + case .swift(let string): + label = "\(string)" + case .swiftPlaceholder(let string): + label = "\(string) (Placeholder)" + case .swiftPrebuiltExternal(let string): + label = "\(string) (Prebuilt)" + case .clang(let string): + label = "\(string) (C)" + } + return label + } + + func quoteName(_ name: String) -> String { + return "\"" + name.replacingOccurrences(of: "\"", with: "\\\"") + "\"" + } + + func outputNode(for moduleId: ModuleDependencyId) -> String { + let nodeName = quoteName(label(for: moduleId)) + let output: String + let font = "fontname=\"Helvetica Bold\"" + + if moduleId == .swift(graph.mainModuleName) { + output = " \(nodeName) [shape=box, style=bold, color=navy, \(font)];\n" + } else { + switch moduleId { + case .swift(_): + output = " \(nodeName) [style=bold, color=orange, style=filled, \(font)];\n" + case .swiftPlaceholder(_): + output = " \(nodeName) [style=bold, color=gold, style=filled, \(font)];\n" + case .swiftPrebuiltExternal(_): + output = " \(nodeName) [style=bold, color=darkorange3, style=filled, \(font)];\n" + case .clang(_): + output = " \(nodeName) [style=bold, color=lightskyblue, style=filled, \(font)];\n" + } + } + return output + } + + public func writeDOT(to stream: inout Stream) { + stream.write("digraph Modules {\n") + for (moduleId, moduleInfo) in graph.modules { + stream.write(outputNode(for: moduleId)) + guard let dependencies = moduleInfo.directDependencies else { + continue + } + for dependencyId in dependencies { + stream.write(" \(quoteName(label(for: moduleId))) -> \(quoteName(label(for: dependencyId))) [color=black];\n") + } + } + stream.write("}\n") + } +} diff --git a/Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift b/Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift index 2a02cdcaf..bb22dca8a 100644 --- a/Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift +++ b/Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift @@ -1167,6 +1167,69 @@ final class ExplicitModuleBuildTests: XCTestCase { } } + func testDependencyGraphDotSerialization() throws { + let (stdlibPath, shimsPath, toolchain, hostTriple) = try getDriverArtifactsForScanning() + let dependencyOracle = InterModuleDependencyOracle() + let scanLibPath = try Driver.getScanLibPath(of: toolchain, + hostTriple: hostTriple, + env: ProcessEnv.vars) + guard try dependencyOracle + .verifyOrCreateScannerInstance(fileSystem: localFileSystem, + swiftScanLibPath: scanLibPath) else { + XCTFail("Dependency scanner library not found") + return + } + // Create a simple test case. + try withTemporaryDirectory { path in + let main = path.appending(component: "testDependencyScanning.swift") + try localFileSystem.writeFileContents(main) { + $0 <<< "import C;" + $0 <<< "import E;" + $0 <<< "import G;" + } + + let cHeadersPath: AbsolutePath = + testInputsPath.appending(component: "ExplicitModuleBuilds") + .appending(component: "CHeaders") + let swiftModuleInterfacesPath: AbsolutePath = + testInputsPath.appending(component: "ExplicitModuleBuilds") + .appending(component: "Swift") + let sdkArgumentsForTesting = (try? Driver.sdkArgumentsForTesting()) ?? [] + var driver = try Driver(args: ["swiftc", + "-I", cHeadersPath.nativePathString(escaped: true), + "-I", swiftModuleInterfacesPath.nativePathString(escaped: true), + "-I", stdlibPath.nativePathString(escaped: true), + "-I", shimsPath.nativePathString(escaped: true), + "-import-objc-header", + "-explicit-module-build", + "-working-directory", path.nativePathString(escaped: true), + "-disable-clang-target", + main.nativePathString(escaped: true)] + sdkArgumentsForTesting, + env: ProcessEnv.vars) + let resolver = try ArgsResolver(fileSystem: localFileSystem) + var scannerCommand = try driver.dependencyScannerInvocationCommand().1.map { try resolver.resolve($0) } + if scannerCommand.first == "-frontend" { + scannerCommand.removeFirst() + } + let dependencyGraph = + try dependencyOracle.getDependencies(workingDirectory: path, + commandLine: scannerCommand) + let serializer = DOTModuleDependencyGraphSerializer(dependencyGraph) + + let outputFile = path.appending(component: "dependency_graph.dot") + var outputStream = try ThreadSafeOutputByteStream(LocalFileOutputByteStream(outputFile)) + serializer.writeDOT(to: &outputStream) + outputStream.flush() + let contents = try localFileSystem.readFileContents(outputFile).description + XCTAssertTrue(contents.contains("\"testDependencyScanning\" [shape=box, style=bold, color=navy")) + XCTAssertTrue(contents.contains("\"G\" [style=bold, color=orange")) + XCTAssertTrue(contents.contains("\"E\" [style=bold, color=orange, style=filled")) + XCTAssertTrue(contents.contains("\"C (C)\" [style=bold, color=lightskyblue, style=filled")) + XCTAssertTrue(contents.contains("\"Swift\" [style=bold, color=orange, style=filled")) + XCTAssertTrue(contents.contains("\"SwiftShims (C)\" [style=bold, color=lightskyblue, style=filled")) + XCTAssertTrue(contents.contains("\"Swift\" -> \"SwiftShims (C)\" [color=black];")) + } + } /// Test the libSwiftScan dependency scanning. func testDependencyScanReuseCache() throws {