Skip to content

Commit 96c5014

Browse files
committed
Allow unit tests to import and link any main modules of executables that are implemented in Swift. This uses a new Swift compiler flag to set the name of the entry point when emitting object code, and then uses linker flags to rename the main executable module's entry point back to _main again when actually linking the executable.
This is guarded by a tools version check, since packages written this way won't be testable on older toolchains.
1 parent 5b0597d commit 96c5014

File tree

10 files changed

+193
-27
lines changed

10 files changed

+193
-27
lines changed

Fixtures/Miscellaneous/ExeTest/Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
// swift-tools-version:5.3
1+
// swift-tools-version: 999.0
22
import PackageDescription
33

44
let package = Package(
55
name: "ExeTest",
66
targets: [
7-
.target(
7+
.executableTarget(
88
name: "Exe",
99
dependencies: []
1010
),
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// swift-tools-version:5.3
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "TestableExe",
6+
targets: [
7+
.target(
8+
name: "TestableExe1"
9+
),
10+
.target(
11+
name: "TestableExe2"
12+
),
13+
.target(
14+
name: "TestableExe3"
15+
),
16+
.testTarget(
17+
name: "TestableExeTests",
18+
dependencies: [
19+
"TestableExe1",
20+
"TestableExe2",
21+
"TestableExe3",
22+
]
23+
),
24+
]
25+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
public func GetGreeting1() -> String {
2+
return "Hello, world"
3+
}
4+
5+
print("\(GetGreeting1())!")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
public func GetGreeting2() -> String {
2+
return "Hello, planet"
3+
}
4+
5+
print("\(GetGreeting2())!")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
const char * GetGreeting3();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#include <stdio.h>
2+
#include "include/TestableExe3.h"
3+
4+
const char * GetGreeting3() {
5+
return "Hello, universe";
6+
}
7+
8+
int main() {
9+
printf("%s!\n", GetGreeting3());
10+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import XCTest
2+
import TestableExe1
3+
import TestableExe2
4+
// import TestableExe3
5+
import class Foundation.Bundle
6+
7+
final class TestableExeTests: XCTestCase {
8+
func testExample() throws {
9+
// This is an example of a functional test case.
10+
// Use XCTAssert and related functions to verify your tests produce the correct
11+
// results.
12+
13+
print(GetGreeting1())
14+
XCTAssertEqual(GetGreeting1(), "Hello, world")
15+
print(GetGreeting2())
16+
XCTAssertEqual(GetGreeting2(), "Hello, planet")
17+
// XCTAssertEqual(String(cString: GetGreeting3()), "Hello, universe")
18+
19+
// Some of the APIs that we use below are available in macOS 10.13 and above.
20+
guard #available(macOS 10.13, *) else {
21+
return
22+
}
23+
24+
var execPath = productsDirectory.appendingPathComponent("TestableExe1")
25+
var process = Process()
26+
process.executableURL = execPath
27+
var pipe = Pipe()
28+
process.standardOutput = pipe
29+
try process.run()
30+
process.waitUntilExit()
31+
var data = pipe.fileHandleForReading.readDataToEndOfFile()
32+
var output = String(data: data, encoding: .utf8)
33+
XCTAssertEqual(output, "Hello, world!\n")
34+
35+
execPath = productsDirectory.appendingPathComponent("TestableExe2")
36+
process = Process()
37+
process.executableURL = execPath
38+
pipe = Pipe()
39+
process.standardOutput = pipe
40+
try process.run()
41+
process.waitUntilExit()
42+
data = pipe.fileHandleForReading.readDataToEndOfFile()
43+
output = String(data: data, encoding: .utf8)
44+
XCTAssertEqual(output, "Hello, planet!\n")
45+
46+
execPath = productsDirectory.appendingPathComponent("TestableExe3")
47+
process = Process()
48+
process.executableURL = execPath
49+
pipe = Pipe()
50+
process.standardOutput = pipe
51+
try process.run()
52+
process.waitUntilExit()
53+
data = pipe.fileHandleForReading.readDataToEndOfFile()
54+
output = String(data: data, encoding: .utf8)
55+
XCTAssertEqual(output, "Hello, universe!\n")
56+
}
57+
58+
/// Returns path to the built products directory.
59+
var productsDirectory: URL {
60+
#if os(macOS)
61+
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
62+
return bundle.bundleURL.deletingLastPathComponent()
63+
}
64+
fatalError("couldn't find the products directory")
65+
#else
66+
return Bundle.main.bundleURL
67+
#endif
68+
}
69+
70+
static var allTests = [
71+
("testExample", testExample),
72+
]
73+
}

Sources/Build/BuildPlan.swift

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ public final class SwiftTargetBuildDescription {
515515

516516
/// The path to the swiftmodule file after compilation.
517517
var moduleOutputPath: AbsolutePath {
518-
let dirPath = (target.type == .executable) ? tempsPath : buildParameters.buildPath
518+
let dirPath = (target.type == .executable && toolsVersion < .vNext) ? tempsPath : buildParameters.buildPath
519519
return dirPath.appending(component: target.c99name + ".swiftmodule")
520520
}
521521

@@ -690,6 +690,14 @@ public final class SwiftTargetBuildDescription {
690690
args += buildParameters.sanitizers.compileSwiftFlags()
691691
args += ["-parseable-output"]
692692

693+
// If we're compiling the main module of an executable, we rename the `_main`
694+
// entry point to `_<modulename>_main`. This will allow tests to link against
695+
// them without conflicts. When we link the executable we will ask the linker
696+
// to rename the entry point symbol to just `_main` again.
697+
if target.type == .executable && !isTestTarget && toolsVersion >= .vNext {
698+
args += ["-Xfrontend", "-entry-point-function-name", "-Xfrontend", "\(target.c99name)_main"]
699+
}
700+
693701
// Only add the build path to the framework search path if there are binary frameworks to link against.
694702
if !libraryBinaryPaths.isEmpty {
695703
args += ["-F", buildParameters.buildPath.pathString]
@@ -1047,7 +1055,7 @@ public final class ProductBuildDescription {
10471055
return buildParameters.binaryPath(for: product)
10481056
}
10491057

1050-
/// The objects in this product.
1058+
/// All object files to link into this product.
10511059
///
10521060
// Computed during build planning.
10531061
public fileprivate(set) var objects = SortedArray<AbsolutePath>()
@@ -1167,6 +1175,23 @@ public final class ProductBuildDescription {
11671175
}
11681176
}
11691177
args += ["-emit-executable"]
1178+
1179+
// If we're linking an executable whose main module is implemented in Swift,
1180+
// we rename the `_<modulename>_main` entry point symbol to `_main` again.
1181+
// This is because executable modules implemented in Swift are compiled with
1182+
// a main symbol named that way to allow tests to link against it without
1183+
// conflicts. If we're using a linker that doesn't support symbol renaming,
1184+
// an alternate implementation could use a generated source file with a stub
1185+
// implementation of `_main` to call the renamed main symbol.
1186+
let execModule = product.executableModule
1187+
if execModule.underlyingTarget is SwiftTarget && toolsVersion >= .vNext {
1188+
if buildParameters.triple.isDarwin() {
1189+
args += ["-Xlinker", "-alias", "-Xlinker", "_\(execModule.c99name)_main", "-Xlinker", "_main"]
1190+
}
1191+
else {
1192+
args += ["-Xlinker", "--defsym", "-Xlinker", "main=\(execModule.c99name)_main"]
1193+
}
1194+
}
11701195
case .plugin:
11711196
throw InternalError("unexpectedly asked to generate linker arguments for a plugin product")
11721197
}
@@ -1669,9 +1694,21 @@ public class BuildPlan {
16691694
switch dependency {
16701695
case .target(let target, _):
16711696
switch target.type {
1672-
// Include executable and tests only if they're top level contents
1673-
// of the product. Otherwise they are just build time dependency.
1674-
case .executable, .test:
1697+
// Executable target have historically only been included if they are directly in the product's
1698+
// target list. Otherwise they have always been just build-time dependencies.
1699+
// In tool version .vNext or greater, we also include executable modules implemented in Swift in
1700+
// any test products... this is to allow testing of executables. Note that they are also still
1701+
// built as separate products that the test can invoke as subprocesses.
1702+
case .executable:
1703+
if product.targets.contains(target) {
1704+
staticTargets.append(target)
1705+
} else if product.type == .test && target.underlyingTarget is SwiftTarget {
1706+
if let toolsVersion = graph.package(for: product)?.manifest.toolsVersion, toolsVersion >= .vNext {
1707+
staticTargets.append(target)
1708+
}
1709+
}
1710+
// Test targets should be included only if they are directly in the product's target list.
1711+
case .test:
16751712
if product.targets.contains(target) {
16761713
staticTargets.append(target)
16771714
}

Tests/FunctionalTests/MiscellaneousTests.swift

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -578,25 +578,18 @@ class MiscellaneousTestCase: XCTestCase {
578578
#endif
579579
}
580580

581-
func testErrorMessageWhenTestLinksExecutable() {
581+
func testTestsCanLinkAgainstExecutable() throws {
582+
// Check if the host compiler supports the '-entry-point-function-name' flag.
583+
try XCTSkipUnless(doesHostSwiftCompilerSupportRenamingMainSymbol(), "skipping because host compiler doesn't support '-entry-point-function-name'")
584+
582585
fixture(name: "Miscellaneous/ExeTest") { prefix in
583586
do {
584-
try executeSwiftTest(prefix)
585-
XCTFail()
586-
} catch SwiftPMProductError.executionFailure(let error, let output, let stderr) {
587-
XCTAssertMatch(stderr + output, .contains("Compiling Exe main.swift"))
588-
XCTAssertMatch(stderr + output, .contains("Compiling ExeTests ExeTests.swift"))
589-
XCTAssertMatch(stderr + output, .regex("error: no such module 'Exe'"))
590-
XCTAssertMatch(stderr + output, .regex("note: module 'Exe' is the main module of an executable, and cannot be imported by tests and other targets"))
591-
592-
if case ProcessResult.Error.nonZeroExit(let result) = error {
593-
// if our code crashes we'll get an exit code of 256
594-
XCTAssertEqual(result.exitStatus, .terminated(code: 1))
595-
} else {
596-
XCTFail("\(stderr + output)")
597-
}
587+
let (stdout, _) = try executeSwiftTest(prefix)
588+
XCTAssertMatch(stdout, .contains("Compiling Exe main.swift"))
589+
XCTAssertMatch(stdout, .contains("Compiling ExeTests ExeTests.swift"))
590+
XCTAssertMatch(stdout, .contains("Linking ExeTestPackageTests"))
598591
} catch {
599-
XCTFail()
592+
XCTFail("\(error)")
600593
}
601594
}
602595
}
@@ -613,3 +606,12 @@ class MiscellaneousTestCase: XCTestCase {
613606
}
614607
}
615608
}
609+
610+
func doesHostSwiftCompilerSupportRenamingMainSymbol() throws -> Bool {
611+
try withTemporaryDirectory { tmpDir in
612+
let hostToolchain = try UserToolchain(destination: .hostDestination())
613+
FileManager.default.createFile(atPath: "\(tmpDir)/foo.swift", contents: Data())
614+
let result = try Process.popen(args: hostToolchain.swiftCompiler.pathString, "-c", "-Xfrontend", "-entry-point-function-name", "-Xfrontend", "foo", "\(tmpDir)/foo.swift", "-o", "\(tmpDir)/foo.o")
615+
return try !result.utf8stderrOutput().contains("unknown argument: '-entry-point-function-name'")
616+
}
617+
}

Tests/FunctionalTests/PluginTests.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import TSCBasic
1414

1515
class PluginTests: XCTestCase {
1616

17-
func testUseOfBuildToolPluginTargetByExecutableInSamePackage() {
17+
func testUseOfBuildToolPluginTargetByExecutableInSamePackage() throws {
18+
// Check if the host compiler supports the '-entry-point-function-name' flag. It's not needed for this test but is needed to build any executable from a package that uses tools version 999.0.
19+
try XCTSkipUnless(doesHostSwiftCompilerSupportRenamingMainSymbol(), "skipping because host compiler doesn't support '-entry-point-function-name'")
1820

1921
fixture(name: "Miscellaneous/Plugins") { path in
2022
do {
@@ -31,7 +33,10 @@ class PluginTests: XCTestCase {
3133
}
3234
}
3335

34-
func testUseOfBuildToolPluginProductByExecutableAcrossPackages() {
36+
func testUseOfBuildToolPluginProductByExecutableAcrossPackages() throws {
37+
// Check if the host compiler supports the '-entry-point-function-name' flag. It's not needed for this test but is needed to build any executable from a package that uses tools version 999.0.
38+
try XCTSkipUnless(doesHostSwiftCompilerSupportRenamingMainSymbol(), "skipping because host compiler doesn't support '-entry-point-function-name'")
39+
3540
fixture(name: "Miscellaneous/Plugins") { path in
3641
do {
3742
let (stdout, _) = try executeSwiftBuild(path.appending(component: "MySourceGenClient"), configuration: .Debug, extraArgs: ["--product", "MyTool"], env: ["SWIFTPM_ENABLE_PLUGINS": "1"])
@@ -47,7 +52,10 @@ class PluginTests: XCTestCase {
4752
}
4853
}
4954

50-
func testUseOfPrebuildPluginTargetByExecutableAcrossPackages() {
55+
func testUseOfPrebuildPluginTargetByExecutableAcrossPackages() throws {
56+
// Check if the host compiler supports the '-entry-point-function-name' flag. It's not needed for this test but is needed to build any executable from a package that uses tools version 999.0.
57+
try XCTSkipUnless(doesHostSwiftCompilerSupportRenamingMainSymbol(), "skipping because host compiler doesn't support '-entry-point-function-name'")
58+
5159
fixture(name: "Miscellaneous/Plugins") { path in
5260
do {
5361
let (stdout, _) = try executeSwiftBuild(path.appending(component: "MySourceGenPlugin"), configuration: .Debug, extraArgs: ["--product", "MyOtherLocalTool"], env: ["SWIFTPM_ENABLE_PLUGINS": "1"])

0 commit comments

Comments
 (0)