From f594124c6d74c3b23498b32e4b076f2ae03e5bff Mon Sep 17 00:00:00 2001 From: Boris Buegling Date: Wed, 29 Oct 2025 16:39:51 -0700 Subject: [PATCH] Dump dependency information during the build - Extend `BuildDependencyInfo` with module/header dependencies - Extend `ValidateDependenciesTaskAction` to dump per target dependency information in the new format - The new behavior is opt-in behind a `DUMP_DEPENDENCIES` build setting --- .../SWBBuildService/BuildDependencyInfo.swift | 287 +-------------- Sources/SWBCore/BuildDependencyInfo.swift | 347 ++++++++++++++++++ Sources/SWBCore/CMakeLists.txt | 1 + Sources/SWBCore/Settings/BuiltinMacros.swift | 4 + .../Tools/ValidateDependencies.swift | 14 +- Sources/SWBCore/Specs/CoreBuildSystem.xcspec | 12 + .../SourcesTaskProducer.swift | 12 +- .../ValidateDependenciesTaskAction.swift | 58 ++- .../DependencyValidationTests.swift | 25 +- 9 files changed, 471 insertions(+), 289 deletions(-) create mode 100644 Sources/SWBCore/BuildDependencyInfo.swift diff --git a/Sources/SWBBuildService/BuildDependencyInfo.swift b/Sources/SWBBuildService/BuildDependencyInfo.swift index 09386ef3..a3261c23 100644 --- a/Sources/SWBBuildService/BuildDependencyInfo.swift +++ b/Sources/SWBBuildService/BuildDependencyInfo.swift @@ -10,16 +10,6 @@ // //===----------------------------------------------------------------------===// -#if canImport(System) -import struct System.FilePath -#else -import struct SystemPackage.FilePath -#endif - -import struct Foundation.Data -import class Foundation.JSONEncoder -import class Foundation.JSONDecoder - import SWBUtil import enum SWBProtocol.ExternalToolResult import struct SWBProtocol.BuildOperationTaskEnded @@ -27,277 +17,6 @@ package import SWBCore import SWBTaskConstruction import SWBMacro -// MARK: Data structures - - -/// Hierarchy of data structures containing the dependencies for all targets in a build. -/// -/// These structures can be encoded to and decoded from JSON. The JSON is an API used by clients, and the data structures may become such an API eventually if we decide to share them directly with clients. -/// -/// The names of properties in these structures are chosen mainly to be useful in the JSON file, so they may be a bit more verbose for use in Swift than they might be otherwise. -/// -/// Presently the main way to instantiate these structures is to use `init(workspaceContext:buildRequest:buildRequestContext:operation:)`, which is defined below after the data structures. - - -/// The input and output dependencies for all targets in a build. -package struct BuildDependencyInfo: Codable { - - /// Structure describing the dependencies for a single target. This includes a structure describing the identity of the target, and the declared inputs and outputs of the target. - package struct TargetDependencyInfo: Codable { - - /// Structure describing the identity of a target. This structure is `Hashable` so it can be used to determine if we've seen exactly this target before, and for testing purposes. - package struct Target: Hashable { - - /// The name of the target. - package let targetName: String - - /// The name of the project (for builds which use multiple Xcode projects). - package let projectName: String? - - /// The name of the platform the target is building for. - package let platformName: String? - - } - - /// Structure describing an input to a target. - package struct Input: Hashable, Codable, Sendable { - - /// An input can be a framework or a library. - package enum InputType: String, Codable, Sendable { - case framework - case library - } - - /// The name reflects what information we have about the input in the project. Since Xcode often finds libraries and frameworks with search paths, we will have the the name of the input - or even only a stem if it's a `-l` option from `OTHER_LDFLAGS`. We may have an absolute path. - package enum NameType: Hashable, Codable, Sendable { - /// An absolute path, typically either because we found it in a build setting such as `OTHER_LDFLAGS`, or because some internal logic decided to link with an absolute path. - case absolutePath(String) - - /// A file name being linked with a search path. This will be the whole name such as `Foo.framework` or `libFoo.dylib`. - case name(String) - - /// The stem of a file being linked with a search path. For libraries this will be the part of the file name after `lib` and before the suffix. For other files this will be the file's base name without the suffix. - /// - /// Stems are often found after `-l` or `-framework` options in a build setting such as `OTHER_LDFLAGS`. - case stem(String) - - /// Convenience method to return the associated value of the input as a String. This is mainly for sorting purposes during tests to emit consistent results, since the names may be of different types. - package var stringForm: String { - switch self { - case .absolutePath(let str): - return str - case .name(let str): - return str - case .stem(let str): - return str - } - } - - /// Convenience method to return a string to use for sorting different names. - package var sortableName: String { - switch self { - case .absolutePath(let str): - return FilePath(str).lastComponent.flatMap({ $0.string }) ?? str - case .name(let str): - return str - case .stem(let str): - return str - } - } - - } - - /// For inputs which are linkages, we note whether we're linking using a search path or an absolute path. - package enum LinkType: String, Codable, Sendable { - case absolutePath - case searchPath - } - - /// The library type of the input. If we know that it's a dynamic or static library (usually from the file type of the input) then we note that. But for inputs from `-l` options in `OTHER_LDFLAGS`, we don't know the type. - package enum LibraryType: String, Codable, Sendable { - case dynamic - case `static` - case upward - case unknown - } - - package let inputType: InputType - package let name: NameType - package let linkType: LinkType - package let libraryType: LibraryType - - package init(inputType: InputType, name: NameType, linkType: LinkType, libraryType: LibraryType) { - self.inputType = inputType - self.name = name - self.linkType = linkType - self.libraryType = libraryType - } - - } - - /// The identifying information of the target. - package let target: Target - - /// List of input files being used by the target. - /// - remark: Presently this is the list of linked libraries and frameworks, often located using search paths. - package let inputs: [Input] - - /// List of paths of outputs in the `DSTROOT` which we report. - /// - remark: Presently this contains only the product of the target, if any. - package let outputPaths: [String] - - } - - /// Info for all of the targets in the build. - package let targets: [TargetDependencyInfo] - - /// Any errors detected in collecting the dependency info for the build. - package let errors: [String] - -} - - -// MARK: Encoding and decoding - - -extension BuildDependencyInfo.TargetDependencyInfo { - - package func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(target.targetName, forKey: .targetName) - try container.encode(target.projectName, forKey: .projectName) - try container.encode(target.platformName, forKey: .platformName) - if !inputs.isEmpty { - // Sort the inputs by name, stem, or last path component. - let sortedInputs = inputs.sorted(by: { $0.name.sortableName < $1.name.sortableName }) - try container.encode(sortedInputs, forKey: .inputs) - } - if !outputPaths.isEmpty { - try container.encode(outputPaths.sorted(), forKey: .outputPaths) - } - } - - package init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let targetName = try container.decode(String.self, forKey: .targetName) - let projectName = try container.decode(String.self, forKey: .projectName) - let platformName = try container.decode(String.self, forKey: .platformName) - self.target = Target(targetName: targetName, projectName: projectName, platformName: platformName) - self.inputs = try container.decodeIfPresent([Input].self, forKey: .inputs) ?? [] - self.outputPaths = try container.decodeIfPresent([String].self, forKey: .outputPaths) ?? [] - } - - private enum CodingKeys: String, CodingKey { - case targetName - case projectName - case platformName - case inputs - case outputPaths - } - - package init(targetName: String, projectName: String?, platformName: String?, inputs: [Input], outputPaths: [String]) { - self.target = Target(targetName: targetName, projectName: projectName, platformName: platformName) - self.inputs = inputs - self.outputPaths = outputPaths - } - -} - -extension BuildDependencyInfo.TargetDependencyInfo.Input { - - package func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(inputType, forKey: .inputType) - try container.encode(name, forKey: .name) - try container.encode(linkType, forKey: .linkType) - try container.encode(libraryType, forKey: .libraryType) - } - - package init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.inputType = try container.decode(BuildDependencyInfo.TargetDependencyInfo.Input.InputType.self, forKey: .inputType) - self.name = try container.decode(BuildDependencyInfo.TargetDependencyInfo.Input.NameType.self, forKey: .name) - self.linkType = try container.decode(BuildDependencyInfo.TargetDependencyInfo.Input.LinkType.self, forKey: .linkType) - self.libraryType = try container.decode(BuildDependencyInfo.TargetDependencyInfo.Input.LibraryType.self, forKey: .libraryType) - } - - private enum CodingKeys: String, CodingKey { - case inputType - case name - case linkType - case libraryType - } - -} - -extension BuildDependencyInfo.TargetDependencyInfo.Input.NameType { - - package func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - switch self { - case .absolutePath(let path): - try container.encode(path, forKey: .path) - case .name(let name): - try container.encode(name, forKey: .name) - case .stem(let stem): - try container.encode(stem, forKey: .stem) - } - } - - package init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - if let path = try container.decodeIfPresent(String.self, forKey: .path) { - self = .absolutePath(path) - } - else if let name = try container.decodeIfPresent(String.self, forKey: .name) { - self = .name(name) - } - else if let stem = try container.decodeIfPresent(String.self, forKey: .stem) { - self = .stem(stem) - } - else { - throw StubError.error("unknown type for input name") - } - } - - private enum CodingKeys: String, CodingKey { - case path - case name - case stem - } - -} - - -// MARK: Custom string definitions for better debugging - - -extension BuildDependencyInfo.TargetDependencyInfo.Target: CustomStringConvertible { - package var description: String { - return "\(type(of: self))" - } -} - -extension BuildDependencyInfo.TargetDependencyInfo.Input: CustomStringConvertible { - package var description: String { - return "\(type(of: self))<\(inputType):\(name):linkType=\(linkType):libraryType=\(libraryType)>" - } -} - -extension BuildDependencyInfo.TargetDependencyInfo.Input.NameType: CustomStringConvertible { - package var description: String { - switch self { - case .absolutePath(let path): - return "path=\(path).str" - case .name(let name): - return "name=\(name)" - case .stem(let stem): - return "stem=\(stem)" - } - } -} - - // MARK: Creating a BuildDependencyInfo from a BuildRequest @@ -319,7 +38,7 @@ extension BuildDependencyInfo { var errors = OrderedSet() // Walk the target dependency closure to collect the desired info. - self.targets = await buildGraph.allTargets.asyncMap { configuredTarget in + let targets = await buildGraph.allTargets.asyncMap { configuredTarget in let settings = buildRequestContext.getCachedSettings(configuredTarget.parameters, target: configuredTarget.target) let targetName = configuredTarget.target.name let projectName = settings.project?.name @@ -331,7 +50,7 @@ extension BuildDependencyInfo { errors.append(contentsOf: inputsErrors) - return TargetDependencyInfo(targetName: targetName, projectName: projectName, platformName: platformName, inputs: inputs, outputPaths: outputPaths) + return TargetDependencyInfo(targetName: targetName, projectName: projectName, platformName: platformName, inputs: inputs, outputPaths: outputPaths, dependencies: []) } // Validate that we didn't encounter anything surprising. @@ -346,7 +65,7 @@ extension BuildDependencyInfo { } } - self.errors = errors.elements + self.init(targets: targets, errors: errors.elements) } // FIXME: This is incomplete. We likely need to use `TaskProducer.willProduceBinary()` to know this, which means factoring that out somewhere where we can use it. For now we use whether the target is a StandardTarget as a proxy for this. diff --git a/Sources/SWBCore/BuildDependencyInfo.swift b/Sources/SWBCore/BuildDependencyInfo.swift new file mode 100644 index 00000000..f6f915b2 --- /dev/null +++ b/Sources/SWBCore/BuildDependencyInfo.swift @@ -0,0 +1,347 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(System) +import struct System.FilePath +#else +import struct SystemPackage.FilePath +#endif + +import struct Foundation.Data +import class Foundation.JSONEncoder +import class Foundation.JSONDecoder + +import SWBUtil + +// MARK: Data structures + + +/// Hierarchy of data structures containing the dependencies for all targets in a build. +/// +/// These structures can be encoded to and decoded from JSON. The JSON is an API used by clients, and the data structures may become such an API eventually if we decide to share them directly with clients. +/// +/// The names of properties in these structures are chosen mainly to be useful in the JSON file, so they may be a bit more verbose for use in Swift than they might be otherwise. +/// +/// Presently the main way to instantiate these structures is to use `init(workspaceContext:buildRequest:buildRequestContext:operation:)`, which is defined below after the data structures. + + +/// The input and output dependencies for all targets in a build. +package struct BuildDependencyInfo: Codable { + package init(targets: [BuildDependencyInfo.TargetDependencyInfo], errors: [String]) { + self.targets = targets + self.errors = errors + } + + /// Structure describing the dependencies for a single target. This includes a structure describing the identity of the target, and the declared inputs and outputs of the target. + package struct TargetDependencyInfo: Codable { + + /// Structure describing the identity of a target. This structure is `Hashable` so it can be used to determine if we've seen exactly this target before, and for testing purposes. + package struct Target: Hashable { + + /// The name of the target. + package let targetName: String + + /// The name of the project (for builds which use multiple Xcode projects). + package let projectName: String? + + /// The name of the platform the target is building for. + package let platformName: String? + + } + + /// Structure describing an input to a target. + package struct Input: Hashable, Codable, Sendable { + + /// An input can be a framework or a library. + package enum InputType: String, Codable, Sendable { + case framework + case library + } + + /// The name reflects what information we have about the input in the project. Since Xcode often finds libraries and frameworks with search paths, we will have the the name of the input - or even only a stem if it's a `-l` option from `OTHER_LDFLAGS`. We may have an absolute path. + package enum NameType: Hashable, Codable, Sendable { + /// An absolute path, typically either because we found it in a build setting such as `OTHER_LDFLAGS`, or because some internal logic decided to link with an absolute path. + case absolutePath(String) + + /// A file name being linked with a search path. This will be the whole name such as `Foo.framework` or `libFoo.dylib`. + case name(String) + + /// The stem of a file being linked with a search path. For libraries this will be the part of the file name after `lib` and before the suffix. For other files this will be the file's base name without the suffix. + /// + /// Stems are often found after `-l` or `-framework` options in a build setting such as `OTHER_LDFLAGS`. + case stem(String) + + /// Convenience method to return the associated value of the input as a String. This is mainly for sorting purposes during tests to emit consistent results, since the names may be of different types. + package var stringForm: String { + switch self { + case .absolutePath(let str): + return str + case .name(let str): + return str + case .stem(let str): + return str + } + } + + /// Convenience method to return a string to use for sorting different names. + package var sortableName: String { + switch self { + case .absolutePath(let str): + return FilePath(str).lastComponent.flatMap({ $0.string }) ?? str + case .name(let str): + return str + case .stem(let str): + return str + } + } + + } + + /// For inputs which are linkages, we note whether we're linking using a search path or an absolute path. + package enum LinkType: String, Codable, Sendable { + case absolutePath + case searchPath + } + + /// The library type of the input. If we know that it's a dynamic or static library (usually from the file type of the input) then we note that. But for inputs from `-l` options in `OTHER_LDFLAGS`, we don't know the type. + package enum LibraryType: String, Codable, Sendable { + case dynamic + case `static` + case upward + case unknown + } + + package let inputType: InputType + package let name: NameType + package let linkType: LinkType + package let libraryType: LibraryType + + package init(inputType: InputType, name: NameType, linkType: LinkType, libraryType: LibraryType) { + self.inputType = inputType + self.name = name + self.linkType = linkType + self.libraryType = libraryType + } + + } + + package enum Dependency: Hashable, Codable, Sendable, Comparable { + case `import`(name: String, accessLevel: AccessLevel, optional: Bool) + case include(path: String) + + public static func < (lhs: Self, rhs: Self) -> Bool { + switch lhs { + case .import(let lhsName, let lhsAccessLevel, _): + switch rhs { + case .import(let rhsName, let rhsAccessLevel, _): + if lhsName == rhsName { + return lhsAccessLevel < rhsAccessLevel + } else { + return lhsName < rhsName + } + case .include: return false + } + case .include(let lhsPath): + switch rhs { + case .import: return true + case .include(let rhsPath): + return lhsPath < rhsPath + } + } + } + } + + package enum AccessLevel: String, Hashable, Codable, Sendable, Comparable { + case Private = "private" + case Package = "package" + case Public = "public" + + public static func < (lhs: Self, rhs: Self) -> Bool { + switch lhs { + case .Private: + return true + case .Public: + return false + case .Package: + return rhs == .Public + } + } + } + + /// The identifying information of the target. + package let target: Target + + /// List of input files being used by the target. + /// - remark: Presently this is the list of linked libraries and frameworks, often located using search paths. + package let inputs: [Input] + + /// List of paths of outputs in the `DSTROOT` which we report. + /// - remark: Presently this contains only the product of the target, if any. + package let outputPaths: [String] + + package let dependencies: [Dependency] + } + + /// Info for all of the targets in the build. + package let targets: [TargetDependencyInfo] + + /// Any errors detected in collecting the dependency info for the build. + package let errors: [String] + +} + + +// MARK: Encoding and decoding + + +extension BuildDependencyInfo.TargetDependencyInfo { + + package func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(target.targetName, forKey: .targetName) + try container.encode(target.projectName, forKey: .projectName) + try container.encode(target.platformName, forKey: .platformName) + if !inputs.isEmpty { + // Sort the inputs by name, stem, or last path component. + let sortedInputs = inputs.sorted(by: { $0.name.sortableName < $1.name.sortableName }) + try container.encode(sortedInputs, forKey: .inputs) + } + if !outputPaths.isEmpty { + try container.encode(outputPaths.sorted(), forKey: .outputPaths) + } + if !dependencies.isEmpty { + try container.encode(dependencies.sorted(), forKey: .dependencies) + } + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let targetName = try container.decode(String.self, forKey: .targetName) + let projectName = try container.decode(String.self, forKey: .projectName) + let platformName = try container.decode(String.self, forKey: .platformName) + self.target = Target(targetName: targetName, projectName: projectName, platformName: platformName) + self.inputs = try container.decodeIfPresent([Input].self, forKey: .inputs) ?? [] + self.outputPaths = try container.decodeIfPresent([String].self, forKey: .outputPaths) ?? [] + self.dependencies = try container.decodeIfPresent([Dependency].self, forKey: .dependencies) ?? [] + } + + private enum CodingKeys: String, CodingKey { + case targetName + case projectName + case platformName + case inputs + case outputPaths + case dependencies + } + + package init(targetName: String, projectName: String?, platformName: String?, inputs: [Input], outputPaths: [String], dependencies: [Dependency]) { + self.target = Target(targetName: targetName, projectName: projectName, platformName: platformName) + self.inputs = inputs + self.outputPaths = outputPaths + self.dependencies = dependencies + } + +} + +extension BuildDependencyInfo.TargetDependencyInfo.Input { + + package func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(inputType, forKey: .inputType) + try container.encode(name, forKey: .name) + try container.encode(linkType, forKey: .linkType) + try container.encode(libraryType, forKey: .libraryType) + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.inputType = try container.decode(BuildDependencyInfo.TargetDependencyInfo.Input.InputType.self, forKey: .inputType) + self.name = try container.decode(BuildDependencyInfo.TargetDependencyInfo.Input.NameType.self, forKey: .name) + self.linkType = try container.decode(BuildDependencyInfo.TargetDependencyInfo.Input.LinkType.self, forKey: .linkType) + self.libraryType = try container.decode(BuildDependencyInfo.TargetDependencyInfo.Input.LibraryType.self, forKey: .libraryType) + } + + private enum CodingKeys: String, CodingKey { + case inputType + case name + case linkType + case libraryType + } + +} + +extension BuildDependencyInfo.TargetDependencyInfo.Input.NameType { + + package func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .absolutePath(let path): + try container.encode(path, forKey: .path) + case .name(let name): + try container.encode(name, forKey: .name) + case .stem(let stem): + try container.encode(stem, forKey: .stem) + } + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let path = try container.decodeIfPresent(String.self, forKey: .path) { + self = .absolutePath(path) + } + else if let name = try container.decodeIfPresent(String.self, forKey: .name) { + self = .name(name) + } + else if let stem = try container.decodeIfPresent(String.self, forKey: .stem) { + self = .stem(stem) + } + else { + throw StubError.error("unknown type for input name") + } + } + + private enum CodingKeys: String, CodingKey { + case path + case name + case stem + } + +} + + +// MARK: Custom string definitions for better debugging + + +extension BuildDependencyInfo.TargetDependencyInfo.Target: CustomStringConvertible { + package var description: String { + return "\(type(of: self))" + } +} + +extension BuildDependencyInfo.TargetDependencyInfo.Input: CustomStringConvertible { + package var description: String { + return "\(type(of: self))<\(inputType):\(name):linkType=\(linkType):libraryType=\(libraryType)>" + } +} + +extension BuildDependencyInfo.TargetDependencyInfo.Input.NameType: CustomStringConvertible { + package var description: String { + switch self { + case .absolutePath(let path): + return "path=\(path).str" + case .name(let name): + return "name=\(name)" + case .stem(let stem): + return "stem=\(stem)" + } + } +} diff --git a/Sources/SWBCore/CMakeLists.txt b/Sources/SWBCore/CMakeLists.txt index f3cabed7..aba34322 100644 --- a/Sources/SWBCore/CMakeLists.txt +++ b/Sources/SWBCore/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(SWBCore ActivityReporting.swift Apple/DeviceFamily.swift Apple/InterfaceBuilderShared.swift + BuildDependencyInfo.swift BuildFileFilteringContext.swift BuildFileResolution.swift BuildParameters.swift diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index 247f44de..bbda11b6 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -616,6 +616,8 @@ public final class BuiltinMacros { public static let DSYMUTIL_VARIANT_SUFFIX = BuiltinMacros.declareStringMacro("DSYMUTIL_VARIANT_SUFFIX") public static let DSYMUTIL_DSYM_SEARCH_PATHS = BuiltinMacros.declarePathListMacro("DSYMUTIL_DSYM_SEARCH_PATHS") public static let DSYMUTIL_QUIET_OPERATION = BuiltinMacros.declareBooleanMacro("DSYMUTIL_QUIET_OPERATION") + public static let DUMP_DEPENDENCIES = BuiltinMacros.declareBooleanMacro("DUMP_DEPENDENCIES") + public static let DUMP_DEPENDENCIES_OUTPUT_PATH = BuiltinMacros.declarePathMacro("DUMP_DEPENDENCIES_OUTPUT_PATH") public static let DWARF_DSYM_FILE_NAME = BuiltinMacros.declareStringMacro("DWARF_DSYM_FILE_NAME") public static let DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT = BuiltinMacros.declareBooleanMacro("DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT") public static let DWARF_DSYM_FOLDER_PATH = BuiltinMacros.declarePathMacro("DWARF_DSYM_FOLDER_PATH") @@ -1685,6 +1687,8 @@ public final class BuiltinMacros { DSYMUTIL_DSYM_SEARCH_PATHS, DSYMUTIL_QUIET_OPERATION, DT_TOOLCHAIN_DIR, + DUMP_DEPENDENCIES, + DUMP_DEPENDENCIES_OUTPUT_PATH, DWARF_DSYM_FILE_NAME, DWARF_DSYM_FILE_SHOULD_ACCOMPANY_PRODUCT, DWARF_DSYM_FOLDER_PATH, diff --git a/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift index 9407cd11..5f9abb7e 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift @@ -58,8 +58,20 @@ public struct ValidateDependenciesPayload: TaskPayload, Sendable, SerializableCo public let moduleDependenciesContext: ModuleDependenciesContext? public let headerDependenciesContext: HeaderDependenciesContext? - public init(moduleDependenciesContext: ModuleDependenciesContext?, headerDependenciesContext: HeaderDependenciesContext?) { + public let dumpDependencies: Bool + public let dumpDependenciesOutputPath: String + + public let platformName: String? + public let projectName: String? + public let targetName: String + + public init(moduleDependenciesContext: ModuleDependenciesContext?, headerDependenciesContext: HeaderDependenciesContext?, dumpDependencies: Bool, dumpDependenciesOutputPath: String, platformName: String?, projectName: String?, targetName: String) { self.moduleDependenciesContext = moduleDependenciesContext self.headerDependenciesContext = headerDependenciesContext + self.dumpDependencies = dumpDependencies + self.dumpDependenciesOutputPath = dumpDependenciesOutputPath + self.platformName = platformName + self.projectName = projectName + self.targetName = targetName } } diff --git a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec index 59786d5b..d0e4fdb1 100644 --- a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec +++ b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec @@ -4674,6 +4674,18 @@ When this setting is enabled: Type = Boolean; DefaultValue = "NO"; }, + + { + Name = "DUMP_DEPENDENCIES"; + Type = Boolean; + DefaultValue = "NO"; + }, + + { + Name = "DUMP_DEPENDENCIES_OUTPUT_PATH"; + Type = Path; + DefaultValue = "$(TARGET_TEMP_DIR)/$(TARGET_NAME)-BuildDependencyInfo.json"; + }, ); }, ) diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift index c02f73e2..49b51ea6 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift @@ -1576,14 +1576,22 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F // Create a task to validate dependencies if that feature is enabled. let validateModuleDeps = (context.moduleDependenciesContext?.validate ?? .no) != .no let validateHeaderDeps = (context.headerDependenciesContext?.validate ?? .no) != .no - if validateModuleDeps || validateHeaderDeps { + if validateModuleDeps || validateHeaderDeps, let target = targetContext.configuredTarget?.target { var validateDepsTasks = [any PlannedTask]() await appendGeneratedTasks(&validateDepsTasks, usePhasedOrdering: true) { delegate in await context.validateDependenciesSpec.createTasks( CommandBuildContext(producer: context, scope: scope, inputs: []), delegate, dependencyInfos: dependencyDataFiles, - payload: .init(moduleDependenciesContext: context.moduleDependenciesContext, headerDependenciesContext: context.headerDependenciesContext) + payload: .init( + moduleDependenciesContext: context.moduleDependenciesContext, + headerDependenciesContext: context.headerDependenciesContext, + dumpDependencies: scope.evaluate(BuiltinMacros.DUMP_DEPENDENCIES), + dumpDependenciesOutputPath: scope.evaluate(BuiltinMacros.DUMP_DEPENDENCIES_OUTPUT_PATH).str, + platformName: context.settings.platform?.name, + projectName: context.settings.project?.name, + targetName: target.name + ) ) } tasks.append(contentsOf: validateDepsTasks) diff --git a/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift index dc4f94b6..46c87dd2 100644 --- a/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift @@ -11,9 +11,11 @@ //===----------------------------------------------------------------------===// import Foundation + public import SWBCore -import SWBUtil +internal import SWBMacro internal import SWBProtocol +import SWBUtil public final class ValidateDependenciesTaskAction: TaskAction { public override class var toolIdentifier: String { @@ -127,6 +129,12 @@ public final class ValidateDependenciesTaskAction: TaskAction { } } + try dumpDependenciesIfNeeded( + imports: Array(allClangImports.union(allSwiftImports)), + includes: Array(allClangIncludes), + payload: payload + ) + for diagnostic in diagnostics { outputDelegate.emit(diagnostic) } @@ -141,4 +149,52 @@ public final class ValidateDependenciesTaskAction: TaskAction { return .succeeded } + + private func dumpDependenciesIfNeeded(imports: [DependencyValidationInfo.Import], includes: [DependencyValidationInfo.Include], payload: ValidateDependenciesPayload) throws { + guard payload.dumpDependencies else { + return + } + + var dependencies = [BuildDependencyInfo.TargetDependencyInfo.Dependency]() + imports.forEach { + dependencies.append(.import(name: $0.dependency.name, accessLevel: .init($0.dependency.accessLevel), optional: $0.dependency.optional)) + } + includes.forEach { + dependencies.append(.include(path: $0.path.str)) + } + + let dependencyInfo = BuildDependencyInfo( + targets: [ + .init( + targetName: payload.targetName, + projectName: payload.projectName, + platformName: payload.platformName, + inputs: [], + outputPaths: [], + dependencies: dependencies + ) + ], errors: [] + ) + + let outputData = try JSONEncoder().encode(dependencyInfo) + let outputURL = URL(fileURLWithPath: payload.dumpDependenciesOutputPath) + try outputData.write(to: outputURL) + } +} + +extension BuildDependencyInfo.TargetDependencyInfo.AccessLevel { + init(_ accessLevel: ModuleDependency.AccessLevel) { + switch accessLevel { + case .Package: self = .Package + case .Private: self = .Private + case .Public: self = .Public + } + } + + init(_ accessLevel: HeaderDependency.AccessLevel) { + switch accessLevel { + case .Private: self = .Private + case .Public: self = .Public + } + } } diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift index 6218a563..fca50223 100644 --- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift +++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift @@ -345,7 +345,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { } } - func validateModuleDependenciesSwift(explicitModules: Bool) async throws { + func validateModuleDependenciesSwift(explicitModules: Bool, dumpDependencies: Bool = false, body: ((Path, BuildOperationTester.BuildResults) async throws -> ())? = nil) async throws { try await withTemporaryDirectory { tmpDir in let testWorkspace = try await TestWorkspace( "Test", @@ -371,6 +371,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { "SWIFT_VERSION": swiftVersion, "DEFINES_MODULE": "YES", "DSTROOT": tmpDir.join("dstroot").str, + "DUMP_DEPENDENCIES": dumpDependencies ? "YES" : "NO", "VALIDATE_MODULE_DEPENDENCIES": "YES_ERROR", "SDKROOT": "$(HOST_PLATFORM)", "SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)", @@ -476,6 +477,10 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { let buildRequest = BuildRequest(parameters: parameters, buildTargets: [BuildRequest.BuildTargetInfo(parameters: parameters, target: target)], continueBuildingAfterErrors: false, useParallelTargets: true, useImplicitDependencies: true, useDryRun: false) try await tester.checkBuild(runDestination: .host, buildRequest: buildRequest, persistent: true) { results in + if let body { + try await body(tmpDir, results) + } + guard !results.checkWarning(.prefix("The current toolchain does not support VALIDATE_MODULE_DEPENDENCIES"), failIfNotFound: false) else { return } for expectedDiag in expectedDiags { @@ -802,4 +807,22 @@ fileprivate struct DependencyValidationTests: CoreBasedTests { } } } + + @Test(.requireSDKs(.macOS)) + func dumpDependenciesDuringBuild() async throws { + try await validateModuleDependenciesSwift(explicitModules: true, dumpDependencies: true) { tmpDir, _ in + let debugDir = tmpDir.join("Test/Project/build/Project.build/Debug") + for dir in try localFS.listdir(debugDir) { + let buildDir = debugDir.join(dir) + if buildDir.fileExtension == "build" { + let dependencyInfos = try localFS.listdir(buildDir).filter { $0.hasSuffix("BuildDependencyInfo.json") } + #expect(dependencyInfos.count == 1) + let dependencyInfoPath = buildDir.join(dependencyInfos.first) + let dependencyInfo = try JSONDecoder().decode(BuildDependencyInfo.self, from: dependencyInfoPath, fs: localFS) + // One "actual" dependency, plus `Swift`, `_Concurrency`, `_StringProcessing`, `_SwiftConcurrencyShims` which are always present. + #expect(dependencyInfo.targets.first?.dependencies.count == 5) + } + } + } + } }