Skip to content

Commit de01002

Browse files
authored
Dump dependency information during the build (#874)
- 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
1 parent 4f60486 commit de01002

File tree

9 files changed

+471
-289
lines changed

9 files changed

+471
-289
lines changed

Sources/SWBBuildService/BuildDependencyInfo.swift

Lines changed: 3 additions & 284 deletions
Original file line numberDiff line numberDiff line change
@@ -10,294 +10,13 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
#if canImport(System)
14-
import struct System.FilePath
15-
#else
16-
import struct SystemPackage.FilePath
17-
#endif
18-
19-
import struct Foundation.Data
20-
import class Foundation.JSONEncoder
21-
import class Foundation.JSONDecoder
22-
2313
import SWBUtil
2414
import enum SWBProtocol.ExternalToolResult
2515
import struct SWBProtocol.BuildOperationTaskEnded
2616
package import SWBCore
2717
import SWBTaskConstruction
2818
import SWBMacro
2919

30-
// MARK: Data structures
31-
32-
33-
/// Hierarchy of data structures containing the dependencies for all targets in a build.
34-
///
35-
/// 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.
36-
///
37-
/// 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.
38-
///
39-
/// Presently the main way to instantiate these structures is to use `init(workspaceContext:buildRequest:buildRequestContext:operation:)`, which is defined below after the data structures.
40-
41-
42-
/// The input and output dependencies for all targets in a build.
43-
package struct BuildDependencyInfo: Codable {
44-
45-
/// 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.
46-
package struct TargetDependencyInfo: Codable {
47-
48-
/// 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.
49-
package struct Target: Hashable {
50-
51-
/// The name of the target.
52-
package let targetName: String
53-
54-
/// The name of the project (for builds which use multiple Xcode projects).
55-
package let projectName: String?
56-
57-
/// The name of the platform the target is building for.
58-
package let platformName: String?
59-
60-
}
61-
62-
/// Structure describing an input to a target.
63-
package struct Input: Hashable, Codable, Sendable {
64-
65-
/// An input can be a framework or a library.
66-
package enum InputType: String, Codable, Sendable {
67-
case framework
68-
case library
69-
}
70-
71-
/// 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.
72-
package enum NameType: Hashable, Codable, Sendable {
73-
/// 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.
74-
case absolutePath(String)
75-
76-
/// A file name being linked with a search path. This will be the whole name such as `Foo.framework` or `libFoo.dylib`.
77-
case name(String)
78-
79-
/// 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.
80-
///
81-
/// Stems are often found after `-l` or `-framework` options in a build setting such as `OTHER_LDFLAGS`.
82-
case stem(String)
83-
84-
/// 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.
85-
package var stringForm: String {
86-
switch self {
87-
case .absolutePath(let str):
88-
return str
89-
case .name(let str):
90-
return str
91-
case .stem(let str):
92-
return str
93-
}
94-
}
95-
96-
/// Convenience method to return a string to use for sorting different names.
97-
package var sortableName: String {
98-
switch self {
99-
case .absolutePath(let str):
100-
return FilePath(str).lastComponent.flatMap({ $0.string }) ?? str
101-
case .name(let str):
102-
return str
103-
case .stem(let str):
104-
return str
105-
}
106-
}
107-
108-
}
109-
110-
/// For inputs which are linkages, we note whether we're linking using a search path or an absolute path.
111-
package enum LinkType: String, Codable, Sendable {
112-
case absolutePath
113-
case searchPath
114-
}
115-
116-
/// 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.
117-
package enum LibraryType: String, Codable, Sendable {
118-
case dynamic
119-
case `static`
120-
case upward
121-
case unknown
122-
}
123-
124-
package let inputType: InputType
125-
package let name: NameType
126-
package let linkType: LinkType
127-
package let libraryType: LibraryType
128-
129-
package init(inputType: InputType, name: NameType, linkType: LinkType, libraryType: LibraryType) {
130-
self.inputType = inputType
131-
self.name = name
132-
self.linkType = linkType
133-
self.libraryType = libraryType
134-
}
135-
136-
}
137-
138-
/// The identifying information of the target.
139-
package let target: Target
140-
141-
/// List of input files being used by the target.
142-
/// - remark: Presently this is the list of linked libraries and frameworks, often located using search paths.
143-
package let inputs: [Input]
144-
145-
/// List of paths of outputs in the `DSTROOT` which we report.
146-
/// - remark: Presently this contains only the product of the target, if any.
147-
package let outputPaths: [String]
148-
149-
}
150-
151-
/// Info for all of the targets in the build.
152-
package let targets: [TargetDependencyInfo]
153-
154-
/// Any errors detected in collecting the dependency info for the build.
155-
package let errors: [String]
156-
157-
}
158-
159-
160-
// MARK: Encoding and decoding
161-
162-
163-
extension BuildDependencyInfo.TargetDependencyInfo {
164-
165-
package func encode(to encoder: any Encoder) throws {
166-
var container = encoder.container(keyedBy: CodingKeys.self)
167-
try container.encode(target.targetName, forKey: .targetName)
168-
try container.encode(target.projectName, forKey: .projectName)
169-
try container.encode(target.platformName, forKey: .platformName)
170-
if !inputs.isEmpty {
171-
// Sort the inputs by name, stem, or last path component.
172-
let sortedInputs = inputs.sorted(by: { $0.name.sortableName < $1.name.sortableName })
173-
try container.encode(sortedInputs, forKey: .inputs)
174-
}
175-
if !outputPaths.isEmpty {
176-
try container.encode(outputPaths.sorted(), forKey: .outputPaths)
177-
}
178-
}
179-
180-
package init(from decoder: any Decoder) throws {
181-
let container = try decoder.container(keyedBy: CodingKeys.self)
182-
let targetName = try container.decode(String.self, forKey: .targetName)
183-
let projectName = try container.decode(String.self, forKey: .projectName)
184-
let platformName = try container.decode(String.self, forKey: .platformName)
185-
self.target = Target(targetName: targetName, projectName: projectName, platformName: platformName)
186-
self.inputs = try container.decodeIfPresent([Input].self, forKey: .inputs) ?? []
187-
self.outputPaths = try container.decodeIfPresent([String].self, forKey: .outputPaths) ?? []
188-
}
189-
190-
private enum CodingKeys: String, CodingKey {
191-
case targetName
192-
case projectName
193-
case platformName
194-
case inputs
195-
case outputPaths
196-
}
197-
198-
package init(targetName: String, projectName: String?, platformName: String?, inputs: [Input], outputPaths: [String]) {
199-
self.target = Target(targetName: targetName, projectName: projectName, platformName: platformName)
200-
self.inputs = inputs
201-
self.outputPaths = outputPaths
202-
}
203-
204-
}
205-
206-
extension BuildDependencyInfo.TargetDependencyInfo.Input {
207-
208-
package func encode(to encoder: any Encoder) throws {
209-
var container = encoder.container(keyedBy: CodingKeys.self)
210-
try container.encode(inputType, forKey: .inputType)
211-
try container.encode(name, forKey: .name)
212-
try container.encode(linkType, forKey: .linkType)
213-
try container.encode(libraryType, forKey: .libraryType)
214-
}
215-
216-
package init(from decoder: any Decoder) throws {
217-
let container = try decoder.container(keyedBy: CodingKeys.self)
218-
self.inputType = try container.decode(BuildDependencyInfo.TargetDependencyInfo.Input.InputType.self, forKey: .inputType)
219-
self.name = try container.decode(BuildDependencyInfo.TargetDependencyInfo.Input.NameType.self, forKey: .name)
220-
self.linkType = try container.decode(BuildDependencyInfo.TargetDependencyInfo.Input.LinkType.self, forKey: .linkType)
221-
self.libraryType = try container.decode(BuildDependencyInfo.TargetDependencyInfo.Input.LibraryType.self, forKey: .libraryType)
222-
}
223-
224-
private enum CodingKeys: String, CodingKey {
225-
case inputType
226-
case name
227-
case linkType
228-
case libraryType
229-
}
230-
231-
}
232-
233-
extension BuildDependencyInfo.TargetDependencyInfo.Input.NameType {
234-
235-
package func encode(to encoder: any Encoder) throws {
236-
var container = encoder.container(keyedBy: CodingKeys.self)
237-
switch self {
238-
case .absolutePath(let path):
239-
try container.encode(path, forKey: .path)
240-
case .name(let name):
241-
try container.encode(name, forKey: .name)
242-
case .stem(let stem):
243-
try container.encode(stem, forKey: .stem)
244-
}
245-
}
246-
247-
package init(from decoder: any Decoder) throws {
248-
let container = try decoder.container(keyedBy: CodingKeys.self)
249-
if let path = try container.decodeIfPresent(String.self, forKey: .path) {
250-
self = .absolutePath(path)
251-
}
252-
else if let name = try container.decodeIfPresent(String.self, forKey: .name) {
253-
self = .name(name)
254-
}
255-
else if let stem = try container.decodeIfPresent(String.self, forKey: .stem) {
256-
self = .stem(stem)
257-
}
258-
else {
259-
throw StubError.error("unknown type for input name")
260-
}
261-
}
262-
263-
private enum CodingKeys: String, CodingKey {
264-
case path
265-
case name
266-
case stem
267-
}
268-
269-
}
270-
271-
272-
// MARK: Custom string definitions for better debugging
273-
274-
275-
extension BuildDependencyInfo.TargetDependencyInfo.Target: CustomStringConvertible {
276-
package var description: String {
277-
return "\(type(of: self))<target=\(targetName):project=\(projectName == nil ? "nil" : projectName!):platform=\(platformName == nil ? "nil" : platformName!)>"
278-
}
279-
}
280-
281-
extension BuildDependencyInfo.TargetDependencyInfo.Input: CustomStringConvertible {
282-
package var description: String {
283-
return "\(type(of: self))<\(inputType):\(name):linkType=\(linkType):libraryType=\(libraryType)>"
284-
}
285-
}
286-
287-
extension BuildDependencyInfo.TargetDependencyInfo.Input.NameType: CustomStringConvertible {
288-
package var description: String {
289-
switch self {
290-
case .absolutePath(let path):
291-
return "path=\(path).str"
292-
case .name(let name):
293-
return "name=\(name)"
294-
case .stem(let stem):
295-
return "stem=\(stem)"
296-
}
297-
}
298-
}
299-
300-
30120
// MARK: Creating a BuildDependencyInfo from a BuildRequest
30221

30322

@@ -319,7 +38,7 @@ extension BuildDependencyInfo {
31938
var errors = OrderedSet<String>()
32039

32140
// Walk the target dependency closure to collect the desired info.
322-
self.targets = await buildGraph.allTargets.asyncMap { configuredTarget in
41+
let targets = await buildGraph.allTargets.asyncMap { configuredTarget in
32342
let settings = buildRequestContext.getCachedSettings(configuredTarget.parameters, target: configuredTarget.target)
32443
let targetName = configuredTarget.target.name
32544
let projectName = settings.project?.name
@@ -331,7 +50,7 @@ extension BuildDependencyInfo {
33150

33251
errors.append(contentsOf: inputsErrors)
33352

334-
return TargetDependencyInfo(targetName: targetName, projectName: projectName, platformName: platformName, inputs: inputs, outputPaths: outputPaths)
53+
return TargetDependencyInfo(targetName: targetName, projectName: projectName, platformName: platformName, inputs: inputs, outputPaths: outputPaths, dependencies: [])
33554
}
33655

33756
// Validate that we didn't encounter anything surprising.
@@ -346,7 +65,7 @@ extension BuildDependencyInfo {
34665
}
34766
}
34867

349-
self.errors = errors.elements
68+
self.init(targets: targets, errors: errors.elements)
35069
}
35170

35271
// 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.

0 commit comments

Comments
 (0)