Skip to content

Commit 11e0f93

Browse files
committed
Verify that only targets that are manifest dependencies can be imported.
This change introduces a build verification step that attempts to detect scenarios where a target contains an `import` of another target in the package without declaring the imported target as a dependency in the manifest. This is done via SwiftDriver's import-prescan capability which relies on libSwiftScan to quickly parse a target's sources and identify all `import`ed modules. Related to rdar://79423257
1 parent 22bfd6b commit 11e0f93

File tree

7 files changed

+105
-2
lines changed

7 files changed

+105
-2
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// swift-tools-version:5.1
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "VerificationTestPackage",
6+
products: [
7+
.executable(name: "BExec", targets: ["B"]),
8+
],
9+
dependencies: [
10+
11+
],
12+
targets: [
13+
.target(
14+
name: "A",
15+
dependencies: []),
16+
.target(
17+
name: "B",
18+
dependencies: []),
19+
]
20+
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import B
2+
public func bar(x: Int) -> Int {
3+
return 11 + foo(x: x)
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public func foo(x: Int) -> Int {
2+
return 11 + x
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print(baz(x: 11))

Sources/Build/BuildOperation.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import SPMLLBuild
1717
import TSCBasic
1818
import TSCUtility
1919

20+
@_implementationOnly import SwiftDriver
21+
2022
public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildSystem, BuildErrorAdviceProvider {
2123

2224
/// The delegate used by the build system.
@@ -118,10 +120,49 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
118120
buildSystem?.cancel()
119121
}
120122

123+
// Emit a warning if a target imports another target in this build
124+
// without specifying it as a dependency in the manifest
125+
private func verifyTargetImports(in description: BuildDescription) throws {
126+
for (target, commandLine) in description.swiftTargetCommandLineArgs {
127+
do {
128+
guard let dependencies = description.targetDependencyMap[target] else {
129+
// Skip target if no dependency information is present
130+
continue
131+
}
132+
let resolver = try ArgsResolver(fileSystem: localFileSystem)
133+
let executor = SPMSwiftDriverExecutor(resolver: resolver,
134+
fileSystem: localFileSystem,
135+
env: ProcessEnv.vars)
136+
var driver = try Driver(args: commandLine,
137+
diagnosticsEngine: diagnostics,
138+
fileSystem: localFileSystem,
139+
executor: executor)
140+
let imports = try driver.performImportPrescan().imports
141+
let targetDependenciesSet = Set(dependencies)
142+
let nonDependencyTargetsSet =
143+
Set(description.targetDependencyMap.keys.filter { !targetDependenciesSet.contains($0) })
144+
let importedTargetsMissingDependency = Set(imports).intersection(nonDependencyTargetsSet)
145+
if let missedDependency = importedTargetsMissingDependency.first {
146+
diagnostics.emit(warning: "Target \(target) imports another target (\(missedDependency)) in the package without declaring it a dependency.", location: nil)
147+
}
148+
} catch {
149+
// The above verification is a best-effort attempt to warn the user about a potential manifest
150+
// error. If something went wrong during the import-prescan, proceed silently.
151+
return
152+
}
153+
}
154+
}
155+
121156
/// Perform a build using the given build description and subset.
122157
public func build(subset: BuildSubset) throws {
158+
// Get the build description
159+
let buildDescription = try getBuildDescription()
160+
161+
// Verify dependency imports on the described targers
162+
try verifyTargetImports(in: buildDescription)
163+
123164
// Create the build system.
124-
let buildSystem = try createBuildSystem(with: getBuildDescription())
165+
let buildSystem = try createBuildSystem(with: buildDescription)
125166
self.buildSystem = buildSystem
126167

127168
// Perform the build.

Sources/Build/BuildOperationBuildSystemDelegateHandler.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ private final class InProcessTool: Tool {
192192
public struct BuildDescription: Codable {
193193
public typealias CommandName = String
194194
public typealias TargetName = String
195+
public typealias CommandLineFlag = String
195196

196197
/// The Swift compiler invocation targets.
197198
let swiftCommands: [BuildManifest.CmdName : SwiftCompilerTool]
@@ -205,8 +206,14 @@ public struct BuildDescription: Codable {
205206
/// The map of copy commands.
206207
let copyCommands: [BuildManifest.CmdName: LLBuildManifest.CopyTool]
207208

209+
/// Every target's set of dependencies.
210+
let targetDependencyMap: [TargetName: [TargetName]]
211+
212+
/// A full swift driver command-line invocation used to build a given Swift target
213+
let swiftTargetCommandLineArgs: [TargetName: [CommandLineFlag]]
214+
208215
/// The built test products.
209-
public let builtTestProducts: [BuiltTestProduct]
216+
let builtTestProducts: [BuiltTestProduct]
210217

211218
public init(
212219
plan: BuildPlan,
@@ -219,6 +226,20 @@ public struct BuildDescription: Codable {
219226
self.swiftFrontendCommands = swiftFrontendCommands
220227
self.testDiscoveryCommands = testDiscoveryCommands
221228
self.copyCommands = copyCommands
229+
self.targetDependencyMap = try plan.targets.reduce(into: [TargetName: [TargetName]]()) {
230+
let deps = try $1.target.recursiveTargetDependencies().map { $0.c99name }
231+
$0[$1.target.c99name] = deps
232+
}
233+
var targetCommandLines: [TargetName: [CommandLineFlag]] = [:]
234+
for (target, description) in plan.targetMap {
235+
guard case .swift(let desc) = description else {
236+
continue
237+
}
238+
targetCommandLines[target.c99name] =
239+
try desc.emitCommandLine() + ["-driver-use-frontend-path",
240+
plan.buildParameters.toolchain.swiftCompiler.pathString]
241+
}
242+
self.swiftTargetCommandLineArgs = targetCommandLines
222243

223244
self.builtTestProducts = plan.buildProducts.filter{ $0.product.type == .test }.map { desc in
224245
return BuiltTestProduct(

Tests/CommandsTests/BuildToolTests.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ final class BuildToolTests: XCTestCase {
7171
}
7272
}
7373

74+
func testImportOfMissedDepWarning() throws {
75+
fixture(name: "Miscellaneous/ImportOfMissingDependency") { path in
76+
let fullPath = resolveSymlinks(path)
77+
XCTAssertThrowsError(try build([], packagePath: fullPath)) { error in
78+
guard case SwiftPMProductError.executionFailure(_, _, let stderr) = error else {
79+
XCTFail()
80+
return
81+
}
82+
XCTAssertTrue(stderr.contains("warning: Target A imports another target (B) in the package without declaring it a dependency."))
83+
}
84+
}
85+
}
86+
7487
func testBinPathAndSymlink() throws {
7588
fixture(name: "ValidLayouts/SingleModule/ExecutableNew") { path in
7689
let fullPath = resolveSymlinks(path)

0 commit comments

Comments
 (0)