Skip to content

Commit 8fe92a5

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 should possibly be guarded by a tools version check, since packages written this way won't be testable on older toolchains.
1 parent 426295a commit 8fe92a5

File tree

10 files changed

+182
-32
lines changed

10 files changed

+182
-32
lines changed
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: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,7 @@ public final class SwiftTargetBuildDescription {
499499

500500
/// The path to the swiftmodule file after compilation.
501501
var moduleOutputPath: AbsolutePath {
502-
let dirPath = (target.type == .executable) ? tempsPath : buildParameters.buildPath
503-
return dirPath.appending(component: target.c99name + ".swiftmodule")
502+
return buildParameters.buildPath.appending(component: target.c99name + ".swiftmodule")
504503
}
505504

506505
/// The path to the wrapped swift module which is created using the modulewrap tool. This is required
@@ -662,6 +661,14 @@ public final class SwiftTargetBuildDescription {
662661
args += buildParameters.sanitizers.compileSwiftFlags()
663662
args += ["-parseable-output"]
664663

664+
// If we're compiling the main module of an executable, we rename the `_main`
665+
// entry point to `_<modulename>_main`. This will allow tests to link against
666+
// them without conflicts. When we link the executable we will ask the linker
667+
// to rename the entry point symbol to just `_main` again.
668+
if target.type == .executable && !isTestTarget {
669+
args += ["-Xfrontend", "-entry-point-function-name", "-Xfrontend", "\(target.c99name)_main"]
670+
}
671+
665672
// Only add the build path to the framework search path if there are binary frameworks to link against.
666673
if !libraryBinaryPaths.isEmpty {
667674
args += ["-F", buildParameters.buildPath.pathString]
@@ -1014,7 +1021,7 @@ public final class ProductBuildDescription {
10141021
return buildParameters.binaryPath(for: product)
10151022
}
10161023

1017-
/// The objects in this product.
1024+
/// All object files to link into this product.
10181025
///
10191026
// Computed during build planning.
10201027
public fileprivate(set) var objects = SortedArray<AbsolutePath>()
@@ -1133,6 +1140,23 @@ public final class ProductBuildDescription {
11331140
}
11341141
}
11351142
args += ["-emit-executable"]
1143+
1144+
// If we're linking an executable whose main module is implemented in Swift,
1145+
// we rename the `_<modulename>_main` entry point symbol to `_main` again.
1146+
// This is because executable modules implemented in Swift are compiled with
1147+
// a main symbol named that way to allow tests to link against it without
1148+
// conflicts. If we're using a linker that doesn't support symbol renaming,
1149+
// an alternate implementation could use a generated source file with a stub
1150+
// implementation of `_main` to call the renamed main symbol.
1151+
let execModule = product.executableModule
1152+
if execModule.underlyingTarget is SwiftTarget {
1153+
if buildParameters.triple.isDarwin() {
1154+
args += ["-Xlinker", "-alias", "-Xlinker", "_\(execModule.c99name)_main", "-Xlinker", "_main"]
1155+
}
1156+
else {
1157+
args += ["-Xlinker", "--defsym", "-Xlinker", "main=\(execModule.c99name)_main"]
1158+
}
1159+
}
11361160
case .plugin:
11371161
throw InternalError("unexpectedly asked to generate linker arguments for a plugin product")
11381162
}
@@ -1615,7 +1639,11 @@ public class BuildPlan {
16151639
switch target.type {
16161640
// Include executable and tests only if they're top level contents
16171641
// of the product. Otherwise they are just build time dependency.
1618-
case .executable, .test:
1642+
case .executable:
1643+
if product.targets.contains(target) || (product.type == .test && target.underlyingTarget is SwiftTarget) {
1644+
staticTargets.append(target)
1645+
}
1646+
case .test:
16191647
if product.targets.contains(target) {
16201648
staticTargets.append(target)
16211649
}

Tests/BuildTests/BuildPlanTests.swift

Lines changed: 22 additions & 9 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
@@ -138,18 +138,20 @@ final class BuildPlanTests: XCTestCase {
138138
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
139139
"-o", "/path/to/build/debug/exe", "-module-name", "exe",
140140
"-emit-executable",
141+
"-Xlinker", "-alias", "-Xlinker", "_exe_main", "-Xlinker", "_main",
141142
"-Xlinker", "-rpath", "-Xlinker", "@loader_path",
142143
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
143144
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
144145
"-target", "x86_64-apple-macosx10.10", "-Xlinker", "-add_ast_path",
145-
"-Xlinker", "/path/to/build/debug/exe.build/exe.swiftmodule", "-Xlinker", "-add_ast_path",
146+
"-Xlinker", "/path/to/build/debug/exe.swiftmodule", "-Xlinker", "-add_ast_path",
146147
"-Xlinker", "/path/to/build/debug/lib.swiftmodule",
147148
]
148149
#else
149150
let linkArguments = [
150151
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
151152
"-o", "/path/to/build/debug/exe", "-module-name", "exe",
152153
"-static-stdlib", "-emit-executable",
154+
"-Xlinker", "--defsym", "-Xlinker", "main=exe_main",
153155
"-Xlinker", "-rpath=$ORIGIN",
154156
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
155157
"-target", defaultTargetTriple,
@@ -450,6 +452,7 @@ final class BuildPlanTests: XCTestCase {
450452
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
451453
"/fake/path/to/swiftc", "-g", "-L", "/path/to/build/release",
452454
"-o", "/path/to/build/release/exe", "-module-name", "exe", "-emit-executable",
455+
"-Xlinker", "-alias", "-Xlinker", "_exe_main", "-Xlinker", "_main",
453456
"-Xlinker", "-rpath", "-Xlinker", "@loader_path",
454457
"@/path/to/build/release/exe.product/Objects.LinkFileList",
455458
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
@@ -459,6 +462,7 @@ final class BuildPlanTests: XCTestCase {
459462
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
460463
"/fake/path/to/swiftc", "-g", "-L", "/path/to/build/release",
461464
"-o", "/path/to/build/release/exe", "-module-name", "exe", "-emit-executable",
465+
"-Xlinker", "--defsym", "-Xlinker", "main=exe_main",
462466
"-Xlinker", "-rpath=$ORIGIN",
463467
"@/path/to/build/release/exe.product/Objects.LinkFileList",
464468
"-target", defaultTargetTriple,
@@ -712,6 +716,7 @@ final class BuildPlanTests: XCTestCase {
712716
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
713717
"/fake/path/to/swiftc", "-lstdc++", "-L", "/path/to/build/debug", "-o",
714718
"/path/to/build/debug/exe", "-module-name", "exe", "-emit-executable",
719+
"-Xlinker", "--defsym", "-Xlinker", "main=exe_main",
715720
"-Xlinker", "-rpath=$ORIGIN",
716721
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
717722
"-runtime-compatibility-version", "none",
@@ -779,16 +784,18 @@ final class BuildPlanTests: XCTestCase {
779784
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
780785
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
781786
"-o", "/path/to/build/debug/exe", "-module-name", "exe", "-emit-executable",
787+
"-Xlinker", "-alias", "-Xlinker", "_exe_main", "-Xlinker", "_main",
782788
"-Xlinker", "-rpath", "-Xlinker", "@loader_path",
783789
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
784790
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
785791
"-target", "x86_64-apple-macosx10.10",
786-
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.build/exe.swiftmodule",
792+
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.swiftmodule",
787793
])
788794
#else
789795
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
790796
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
791797
"-o", "/path/to/build/debug/exe", "-module-name", "exe", "-emit-executable",
798+
"-Xlinker", "--defsym", "-Xlinker", "main=exe_main",
792799
"-Xlinker", "-rpath=$ORIGIN",
793800
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
794801
"-target", defaultTargetTriple,
@@ -989,16 +996,18 @@ final class BuildPlanTests: XCTestCase {
989996
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
990997
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
991998
"-o", "/path/to/build/debug/exe", "-module-name", "exe", "-emit-executable",
999+
"-Xlinker", "-alias", "-Xlinker", "_exe_main", "-Xlinker", "_main",
9921000
"-Xlinker", "-rpath", "-Xlinker", "@loader_path",
9931001
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
9941002
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
9951003
"-target", "x86_64-apple-macosx10.10",
996-
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.build/exe.swiftmodule",
1004+
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.swiftmodule",
9971005
])
9981006
#else
9991007
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
10001008
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
10011009
"-o", "/path/to/build/debug/exe", "-module-name", "exe", "-emit-executable",
1010+
"-Xlinker", "--defsym", "-Xlinker", "main=exe_main",
10021011
"-Xlinker", "-rpath=$ORIGIN",
10031012
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
10041013
"-target", defaultTargetTriple,
@@ -1086,12 +1095,13 @@ final class BuildPlanTests: XCTestCase {
10861095
#if os(macOS)
10871096
XCTAssertEqual(fooLinkArgs, [
10881097
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
1089-
"-o", "/path/to/build/debug/Foo", "-module-name", "Foo", "-lBar-Baz", "-emit-executable",
1090-
"-Xlinker", "-rpath", "-Xlinker", "@loader_path",
1098+
"-o", "/path/to/build/debug/Foo", "-module-name", "Foo", "-lBar-Baz", "-emit-executable",
1099+
"-Xlinker", "-alias", "-Xlinker", "_Foo_main", "-Xlinker", "_main",
1100+
"-Xlinker", "-rpath", "-Xlinker", "@loader_path",
10911101
"@/path/to/build/debug/Foo.product/Objects.LinkFileList",
10921102
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
10931103
"-target", "x86_64-apple-macosx10.10",
1094-
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/Foo.build/Foo.swiftmodule"
1104+
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/Foo.swiftmodule"
10951105
])
10961106

10971107
XCTAssertEqual(barLinkArgs, [
@@ -1108,6 +1118,7 @@ final class BuildPlanTests: XCTestCase {
11081118
XCTAssertEqual(fooLinkArgs, [
11091119
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
11101120
"-o", "/path/to/build/debug/Foo", "-module-name", "Foo", "-lBar-Baz", "-emit-executable",
1121+
"-Xlinker", "-alias", "-Xlinker", "_Foo_main", "-Xlinker", "_main",
11111122
"-Xlinker", "-rpath=$ORIGIN",
11121123
"@/path/to/build/debug/Foo.product/Objects.LinkFileList",
11131124
"-target", defaultTargetTriple,
@@ -1615,6 +1626,7 @@ final class BuildPlanTests: XCTestCase {
16151626
"/fake/path/to/swiftc",
16161627
"-L", "/path/to/build/debug", "-o", "/path/to/build/debug/exe.exe",
16171628
"-module-name", "exe", "-emit-executable",
1629+
"-Xlinker", "--defsym", "-Xlinker", "main=exe_main",
16181630
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
16191631
"-target", "x86_64-unknown-windows-msvc",
16201632
])
@@ -1692,6 +1704,7 @@ final class BuildPlanTests: XCTestCase {
16921704
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
16931705
"-o", "/path/to/build/debug/app.wasm",
16941706
"-module-name", "app", "-static-stdlib", "-emit-executable",
1707+
"-Xlinker", "--defsym", "-Xlinker", "main=app_main",
16951708
"@/path/to/build/debug/app.product/Objects.LinkFileList",
16961709
"-target", "wasm32-unknown-wasi"
16971710
]
@@ -2340,10 +2353,10 @@ final class BuildPlanTests: XCTestCase {
23402353
XCTAssertMatch(contents, .contains("""
23412354
"/path/to/build/debug/exe.build/exe.swiftmodule.o":
23422355
tool: shell
2343-
inputs: ["/path/to/build/debug/exe.build/exe.swiftmodule"]
2356+
inputs: ["/path/to/build/debug/exe.swiftmodule"]
23442357
outputs: ["/path/to/build/debug/exe.build/exe.swiftmodule.o"]
23452358
description: "Wrapping AST for exe for debugging"
2346-
args: ["/fake/path/to/swiftc","-modulewrap","/path/to/build/debug/exe.build/exe.swiftmodule","-o","/path/to/build/debug/exe.build/exe.swiftmodule.o","-target","x86_64-unknown-linux-gnu"]
2359+
args: ["/fake/path/to/swiftc","-modulewrap","/path/to/build/debug/exe.swiftmodule","-o","/path/to/build/debug/exe.build/exe.swiftmodule.o","-target","x86_64-unknown-linux-gnu"]
23472360
"""))
23482361
XCTAssertMatch(contents, .contains("""
23492362
"/path/to/build/debug/lib.build/lib.swiftmodule.o":

Tests/FunctionalTests/MiscellaneousTests.swift

Lines changed: 7 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,15 @@ class MiscellaneousTestCase: XCTestCase {
578578
#endif
579579
}
580580

581-
func testErrorMessageWhenTestLinksExecutable() {
581+
func testTestsCanLinkAgainstExecutable() {
582582
fixture(name: "Miscellaneous/ExeTest") { prefix in
583583
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-
}
584+
let (stdout, _) = try executeSwiftTest(prefix)
585+
XCTAssertMatch(stdout, .contains("Compiling Exe main.swift"))
586+
XCTAssertMatch(stdout, .contains("Compiling ExeTests ExeTests.swift"))
587+
XCTAssertMatch(stdout, .contains("Linking ExeTestPackageTests"))
598588
} catch {
599-
XCTFail()
589+
XCTFail("\(error)")
600590
}
601591
}
602592
}

0 commit comments

Comments
 (0)