@@ -102,6 +102,12 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
102
102
/// Alternative path to search for pkg-config `.pc` files.
103
103
private let pkgConfigDirectories : [ AbsolutePath ]
104
104
105
+ /// Map of dependency package identities by root packages that depend on them.
106
+ private let dependenciesByRootPackageIdentity : [ PackageIdentity : [ PackageIdentity ] ]
107
+
108
+ /// Map of root package identities by target names which are declared in them.
109
+ private let rootPackageIdentityByTargetName : [ String : PackageIdentity ]
110
+
105
111
public init (
106
112
productsBuildParameters: BuildParameters ,
107
113
toolsBuildParameters: BuildParameters ,
@@ -110,6 +116,8 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
110
116
pluginConfiguration: PluginConfiguration ? = . none,
111
117
additionalFileRules: [ FileRuleDescription ] ,
112
118
pkgConfigDirectories: [ AbsolutePath ] ,
119
+ dependenciesByRootPackageIdentity: [ PackageIdentity : [ PackageIdentity ] ] ,
120
+ targetsByRootPackageIdentity: [ PackageIdentity : [ String ] ] ,
113
121
outputStream: OutputByteStream ,
114
122
logLevel: Basics . Diagnostic . Severity ,
115
123
fileSystem: Basics . FileSystem ,
@@ -129,6 +137,8 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
129
137
self . additionalFileRules = additionalFileRules
130
138
self . pluginConfiguration = pluginConfiguration
131
139
self . pkgConfigDirectories = pkgConfigDirectories
140
+ self . dependenciesByRootPackageIdentity = dependenciesByRootPackageIdentity
141
+ self . rootPackageIdentityByTargetName = ( try ? Dictionary < String , PackageIdentity > ( throwingUniqueKeysWithValues: targetsByRootPackageIdentity. lazy. flatMap { e in e. value. map { ( $0, e. key) } } ) ) ?? [ : ]
132
142
self . outputStream = outputStream
133
143
self . logLevel = logLevel
134
144
self . fileSystem = fileSystem
@@ -248,6 +258,79 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
248
258
}
249
259
}
250
260
261
+ private static var didEmitUnexpressedDependencies = false
262
+
263
+ private func detectUnexpressedDependencies( ) {
264
+ return self . detectUnexpressedDependencies (
265
+ // Note: once we switch from the toolchain global metadata, we will have to ensure we can match the right metadata used during the build.
266
+ availableLibraries: self . productsBuildParameters. toolchain. providedLibraries,
267
+ targetDependencyMap: self . buildDescription. targetDependencyMap
268
+ )
269
+ }
270
+
271
+ // TODO: Currently this function will only match frameworks.
272
+ internal func detectUnexpressedDependencies(
273
+ availableLibraries: [ LibraryMetadata ] ,
274
+ targetDependencyMap: [ String : [ String ] ] ?
275
+ ) {
276
+ // Ensure we only emit these once, regardless of how many builds are being done.
277
+ guard !Self. didEmitUnexpressedDependencies else {
278
+ return
279
+ }
280
+ Self . didEmitUnexpressedDependencies = true
281
+
282
+ let availableFrameworks = Dictionary < String , PackageIdentity > ( uniqueKeysWithValues: availableLibraries. compactMap {
283
+ if let identity = Set ( $0. identities. map ( \. identity) ) . spm_only {
284
+ return ( " \( $0. productName!) .framework " , identity)
285
+ } else {
286
+ return nil
287
+ }
288
+ } )
289
+
290
+ targetDependencyMap? . keys. forEach { targetName in
291
+ let c99name = targetName. spm_mangledToC99ExtendedIdentifier ( )
292
+ // Since we're analysing post-facto, we don't know which parameters are the correct ones.
293
+ let possibleTempsPaths = [ productsBuildParameters, toolsBuildParameters] . map {
294
+ $0. buildPath. appending ( component: " \( c99name) .build " )
295
+ }
296
+
297
+ let usedSDKDependencies : [ String ] = Set ( possibleTempsPaths) . flatMap { possibleTempsPath in
298
+ guard let contents = try ? self . fileSystem. readFileContents ( possibleTempsPath. appending ( component: " \( c99name) .d " ) ) else {
299
+ return [ String] ( )
300
+ }
301
+
302
+ // FIXME: We need a real makefile deps parser here...
303
+ let deps = contents. description. split ( whereSeparator: { $0. isWhitespace } )
304
+ return deps. filter {
305
+ !$0. hasPrefix ( possibleTempsPath. parentDirectory. pathString)
306
+ } . compactMap {
307
+ try ? AbsolutePath ( validating: String ( $0) )
308
+ } . compactMap {
309
+ return $0. components. first ( where: { $0. hasSuffix ( " .framework " ) } )
310
+ }
311
+ }
312
+
313
+ let dependencies : [ PackageIdentity ]
314
+ if let rootPackageIdentity = self . rootPackageIdentityByTargetName [ targetName] {
315
+ dependencies = self . dependenciesByRootPackageIdentity [ rootPackageIdentity] ?? [ ]
316
+ } else {
317
+ dependencies = [ ]
318
+ }
319
+
320
+ Set ( usedSDKDependencies) . forEach {
321
+ if availableFrameworks. keys. contains ( $0) {
322
+ if let availableFrameworkPackageIdentity = availableFrameworks [ $0] , !dependencies. contains (
323
+ availableFrameworkPackageIdentity
324
+ ) {
325
+ observabilityScope. emit (
326
+ warning: " target ' \( targetName) ' has an unexpressed depedency on ' \( availableFrameworkPackageIdentity) ' "
327
+ )
328
+ }
329
+ }
330
+ }
331
+ }
332
+ }
333
+
251
334
/// Perform a build using the given build description and subset.
252
335
public func build( subset: BuildSubset ) throws {
253
336
guard !self . productsBuildParameters. shouldSkipBuilding else {
@@ -284,6 +367,8 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
284
367
285
368
let duration = buildStartTime. distance ( to: . now( ) )
286
369
370
+ self . detectUnexpressedDependencies ( )
371
+
287
372
let subsetDescriptor : String ?
288
373
switch subset {
289
374
case . product( let productName) :
@@ -466,6 +551,8 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
466
551
packageGraphLoader: { return graph } ,
467
552
additionalFileRules: self . additionalFileRules,
468
553
pkgConfigDirectories: self . pkgConfigDirectories,
554
+ dependenciesByRootPackageIdentity: [ : ] ,
555
+ targetsByRootPackageIdentity: [ : ] ,
469
556
outputStream: self . outputStream,
470
557
logLevel: self . logLevel,
471
558
fileSystem: self . fileSystem,
0 commit comments