Skip to content

Commit 1f58ae7

Browse files
committed
Allow test targets to link against executable targets so they can import and test the code. Since compiler support isn't yet available, this implementation elides the _main symbol from the linked binary (using nmedit on Darwin and objcopy elsewhere). To produce alternate versions of .o files for the unit test to link against.
1 parent 1837e63 commit 1f58ae7

File tree

13 files changed

+226
-32
lines changed

13 files changed

+226
-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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import XCTest
2+
3+
import TestableExeTests
4+
5+
var tests = [XCTestCaseEntry]()
6+
tests += TestableExeTests.allTests()
7+
XCTMain(tests)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
XCTAssertEqual(GetGreeting1(), "Hello, world")
14+
XCTAssertEqual(GetGreeting2(), "Hello, planet")
15+
XCTAssertEqual(String(cString: GetGreeting3()), "Hello, universe")
16+
17+
// Some of the APIs that we use below are available in macOS 10.13 and above.
18+
guard #available(macOS 10.13, *) else {
19+
return
20+
}
21+
22+
let fooBinary = productsDirectory.appendingPathComponent("TestableExe1")
23+
24+
let process = Process()
25+
process.executableURL = fooBinary
26+
27+
let pipe = Pipe()
28+
process.standardOutput = pipe
29+
30+
try process.run()
31+
process.waitUntilExit()
32+
33+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
34+
let output = String(data: data, encoding: .utf8)
35+
36+
XCTAssertEqual(output, "Hello, world!\n")
37+
}
38+
39+
/// Returns path to the built products directory.
40+
var productsDirectory: URL {
41+
#if os(macOS)
42+
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
43+
return bundle.bundleURL.deletingLastPathComponent()
44+
}
45+
fatalError("couldn't find the products directory")
46+
#else
47+
return Bundle.main.bundleURL
48+
#endif
49+
}
50+
51+
static var allTests = [
52+
("testExample", testExample),
53+
]
54+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import XCTest
2+
3+
#if !canImport(ObjectiveC)
4+
public func allTests() -> [XCTestCaseEntry] {
5+
return [
6+
testCase(TestableExeTests.allTests),
7+
]
8+
}
9+
#endif

Sources/Build/BuildPlan.swift

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,8 @@ public final class ClangTargetBuildDescription {
259259
self.derivedSources = Sources(paths: [], root: tempsPath.appending(component: "DerivedSources"))
260260

261261
// Try computing modulemap path for a C library. This also creates the file in the file system, if needed.
262-
if target.type == .library {
262+
// FIXME: Adding .executable here is probably not right, but is needed in order to test Clang executables.
263+
if target.type == .library || target.type == .executable {
263264
// If there's a custom module map, use it as given.
264265
if case .custom(let path) = clangTarget.moduleMapType {
265266
self.moduleMap = path
@@ -503,8 +504,7 @@ public final class SwiftTargetBuildDescription {
503504

504505
/// The path to the swiftmodule file after compilation.
505506
var moduleOutputPath: AbsolutePath {
506-
let dirPath = (target.type == .executable) ? tempsPath : buildParameters.buildPath
507-
return dirPath.appending(component: target.c99name + ".swiftmodule")
507+
return buildParameters.buildPath.appending(component: target.c99name + ".swiftmodule")
508508
}
509509

510510
/// The path to the wrapped swift module which is created using the modulewrap tool. This is required
@@ -1003,7 +1003,7 @@ public final class ProductBuildDescription {
10031003
return buildParameters.binaryPath(for: product)
10041004
}
10051005

1006-
/// The objects in this product.
1006+
/// All object files to link into this product.
10071007
///
10081008
// Computed during build planning.
10091009
public fileprivate(set) var objects = SortedArray<AbsolutePath>()
@@ -1021,6 +1021,10 @@ public final class ProductBuildDescription {
10211021
/// The list of Swift modules that should be passed to the linker. This is required for debugging to work.
10221022
fileprivate var swiftASTs: SortedArray<AbsolutePath> = .init()
10231023

1024+
/// Ordered mapping of any mainless object files used by the product to the originals in the executable
1025+
/// targets that produce them.
1026+
public fileprivate(set) var executableObjects: OrderedDictionary<AbsolutePath, AbsolutePath> = .init()
1027+
10241028
/// Paths to the binary libraries the product depends on.
10251029
fileprivate var libraryBinaryPaths: Set<AbsolutePath> = []
10261030

@@ -1034,6 +1038,11 @@ public final class ProductBuildDescription {
10341038
return tempsPath.appending(component: "Objects.LinkFileList")
10351039
}
10361040

1041+
/// Path to the symbol removal list file (list of symbols to remove from any objects linked into this product).
1042+
var mainSymbolRemovalListFilePath: AbsolutePath {
1043+
return tempsPath.appending(component: "MainSymbol.SymbolList")
1044+
}
1045+
10371046
/// Diagnostics Engine for emitting diagnostics.
10381047
let diagnostics: DiagnosticsEngine
10391048

@@ -1191,6 +1200,16 @@ public final class ProductBuildDescription {
11911200
try fs.writeFileContents(linkFileListPath, bytes: stream.bytes)
11921201
}
11931202

1203+
/// Writes symbol removal list file to the filesystem.
1204+
func writeMainSymbolRemovalListFile(_ fs: FileSystem) throws {
1205+
let stream = BufferedOutputByteStream()
1206+
1207+
stream <<< "_main\n"
1208+
1209+
try fs.createDirectory(mainSymbolRemovalListFilePath.parentDirectory, recursive: true)
1210+
try fs.writeFileContents(mainSymbolRemovalListFilePath, bytes: stream.bytes)
1211+
}
1212+
11941213
/// Returns the build flags from the declared build settings.
11951214
private func buildSettingsFlags() -> [String] {
11961215
var flags: [String] = []
@@ -1482,14 +1501,14 @@ public class BuildPlan {
14821501

14831502
// Link C++ if needed.
14841503
// Note: This will come from build settings in future.
1485-
for target in dependencies.staticTargets {
1504+
for target in dependencies.staticTargets + dependencies.executableTargets {
14861505
if case let target as ClangTarget = target.underlyingTarget, target.isCXX {
14871506
buildProduct.additionalFlags += self.buildParameters.toolchain.extraCPPFlags
14881507
break
14891508
}
14901509
}
14911510

1492-
for target in dependencies.staticTargets {
1511+
for target in dependencies.staticTargets + dependencies.executableTargets {
14931512
switch target.underlyingTarget {
14941513
case is SwiftTarget:
14951514
// Swift targets are guaranteed to have a corresponding Swift description.
@@ -1513,7 +1532,7 @@ public class BuildPlan {
15131532
}
15141533
}
15151534

1516-
buildProduct.staticTargets = dependencies.staticTargets
1535+
buildProduct.staticTargets = dependencies.staticTargets + dependencies.executableTargets
15171536
buildProduct.dylibs = try dependencies.dylibs.map{
15181537
guard let product = productMap[$0] else {
15191538
throw InternalError("unknown product \($0)")
@@ -1527,6 +1546,25 @@ public class BuildPlan {
15271546
return target.objects
15281547
}
15291548
buildProduct.libraryBinaryPaths = dependencies.libraryBinaryPaths
1549+
1550+
// If we're linking against any executable targets, we need to create versions of the .o files from those
1551+
// targets that elide the `_main` symbol. We should look into whether linker options can be added to specify
1552+
// this on the command line.
1553+
if !dependencies.executableTargets.isEmpty {
1554+
// Creating a mapping from each .o file in each executable target to a corresponding modified .o file in
1555+
// our product directory. This duplicates work if an executable is tested by more than one test product
1556+
// but has the advantage of keeping the executable target clean unless it's being used by a test target.
1557+
for target in dependencies.executableTargets.map({ targetMap[$0]! }) {
1558+
for object in target.objects {
1559+
// FIXME: Plenty of opportunity for collisions here — how is this handled for regular object files?
1560+
let mainlessObject = buildProduct.tempsPath.appending(components: "LinkedExecutableObjects", "\(target.target.c99name)_\(object.basename)")
1561+
buildProduct.executableObjects[mainlessObject] = object
1562+
buildProduct.objects.insert(mainlessObject)
1563+
}
1564+
}
1565+
// The symbol removal tool on some platforms requires a separate file list in the file system.
1566+
try buildProduct.writeMainSymbolRemovalListFile(fileSystem)
1567+
}
15301568

15311569
// Write the link filelist file.
15321570
//
@@ -1541,6 +1579,7 @@ public class BuildPlan {
15411579
) throws -> (
15421580
dylibs: [ResolvedProduct],
15431581
staticTargets: [ResolvedTarget],
1582+
executableTargets: [ResolvedTarget],
15441583
systemModules: [ResolvedTarget],
15451584
libraryBinaryPaths: Set<AbsolutePath>
15461585
) {
@@ -1568,6 +1607,7 @@ public class BuildPlan {
15681607
// Create empty arrays to collect our results.
15691608
var linkLibraries = [ResolvedProduct]()
15701609
var staticTargets = [ResolvedTarget]()
1610+
var executableTargets = [ResolvedTarget]()
15711611
var systemModules = [ResolvedTarget]()
15721612
var libraryBinaryPaths: Set<AbsolutePath> = []
15731613

@@ -1577,7 +1617,14 @@ public class BuildPlan {
15771617
switch target.type {
15781618
// Include executable and tests only if they're top level contents
15791619
// of the product. Otherwise they are just build time dependency.
1580-
case .executable, .test:
1620+
case .executable:
1621+
if product.targets.contains(target) {
1622+
staticTargets.append(target)
1623+
}
1624+
else {
1625+
executableTargets.append(target)
1626+
}
1627+
case .test:
15811628
if product.targets.contains(target) {
15821629
staticTargets.append(target)
15831630
}
@@ -1612,7 +1659,7 @@ public class BuildPlan {
16121659
}
16131660
}
16141661

1615-
return (linkLibraries, staticTargets, systemModules, libraryBinaryPaths)
1662+
return (linkLibraries, staticTargets, executableTargets, systemModules, libraryBinaryPaths)
16161663
}
16171664

16181665
/// Plan a Clang target.
@@ -1657,7 +1704,8 @@ public class BuildPlan {
16571704
// depends on.
16581705
for case .target(let dependency, _) in try swiftTarget.target.recursiveDependencies(satisfying: buildEnvironment) {
16591706
switch dependency.underlyingTarget {
1660-
case let underlyingTarget as ClangTarget where underlyingTarget.type == .library:
1707+
// FIXME: Adding .executable here is probably not right, but is needed in order to test Clang executables.
1708+
case let underlyingTarget as ClangTarget where underlyingTarget.type == .library || underlyingTarget.type == .executable:
16611709
guard case let .clang(target)? = targetMap[dependency] else {
16621710
fatalError("unexpected clang target \(underlyingTarget)")
16631711
}

Sources/Build/ManifestBuilder.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,40 @@ extension LLBuildManifestBuilder {
624624
outputs: [.file(target.wrappedModuleOutputPath)],
625625
args: moduleWrapArgs)
626626
}
627+
628+
/// Add a command to produce a new .o file that removes (or hides) the `_main` symbol from a compiled .o file.
629+
/// This is used to modify .o files produced by executables so they can be linked into unit test products. The
630+
/// symbol list file is expected to only contain the symbol `_main` and is only needed because `nmedit` needs a
631+
/// file with the names of the symbols.
632+
private func addMainSymbolRemovalCmd(toolchain: Toolchain,
633+
inputFile: AbsolutePath, outputFile: AbsolutePath,
634+
mainSymbolListFile: AbsolutePath) {
635+
let args: [String]
636+
#if canImport(Darwin)
637+
// On Darwin systems, use `nmedit` to remove the `main` symbol.
638+
args = [
639+
// FIXME: The toolchain should provide the path of the `nmedit` tool.
640+
toolchain.swiftCompiler.parentDirectory.appending(component: "nmedit").pathString,
641+
"-R", mainSymbolListFile.pathString,
642+
inputFile.pathString,
643+
"-o", outputFile.pathString
644+
]
645+
#else
646+
// On non-Darwin systems, use `objcopy` from `binutils` to mark the `main` symbol as local.
647+
args = [
648+
"objcopy",
649+
"-L", "main",
650+
inputFile.pathString,
651+
outputFile.pathString
652+
]
653+
#endif
654+
manifest.addShellCmd(
655+
name: outputFile.pathString,
656+
description: "Eliding symbols from \(outputFile.basename)",
657+
inputs: [.file(inputFile)], // Note: we don't add the symbol file as an input since it's a constant
658+
outputs: [.file(outputFile)],
659+
args: args)
660+
}
627661
}
628662

629663
// MARK:- Compile C-family
@@ -793,6 +827,11 @@ extension LLBuildManifestBuilder {
793827
outputs: [.file(buildProduct.binary)],
794828
args: try buildProduct.linkArguments()
795829
)
830+
831+
// Add a separate command to remove the main symbol.
832+
for (mainlessObject, object) in buildProduct.executableObjects {
833+
addMainSymbolRemovalCmd(toolchain: buildProduct.buildParameters.toolchain, inputFile: object, outputFile: mainlessObject, mainSymbolListFile: buildProduct.mainSymbolRemovalListFilePath)
834+
}
796835
}
797836

798837
// Create a phony node to represent the entire target.

Tests/BuildTests/BuildPlanTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ final class BuildPlanTests: XCTestCase {
143143
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
144144
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
145145
"-target", "x86_64-apple-macosx10.10", "-Xlinker", "-add_ast_path",
146-
"-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",
147147
"-Xlinker", "/path/to/build/debug/lib.swiftmodule",
148148
]
149149
#else
@@ -784,7 +784,7 @@ final class BuildPlanTests: XCTestCase {
784784
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
785785
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
786786
"-target", "x86_64-apple-macosx10.10",
787-
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.build/exe.swiftmodule",
787+
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.swiftmodule",
788788
])
789789
#else
790790
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
@@ -994,7 +994,7 @@ final class BuildPlanTests: XCTestCase {
994994
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
995995
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
996996
"-target", "x86_64-apple-macosx10.10",
997-
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.build/exe.swiftmodule",
997+
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.swiftmodule",
998998
])
999999
#else
10001000
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
@@ -1092,7 +1092,7 @@ final class BuildPlanTests: XCTestCase {
10921092
"@/path/to/build/debug/Foo.product/Objects.LinkFileList",
10931093
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
10941094
"-target", "x86_64-apple-macosx10.10",
1095-
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/Foo.build/Foo.swiftmodule"
1095+
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/Foo.swiftmodule"
10961096
])
10971097

10981098
XCTAssertEqual(barLinkArgs, [
@@ -2339,10 +2339,10 @@ final class BuildPlanTests: XCTestCase {
23392339
XCTAssertMatch(contents, .contains("""
23402340
"/path/to/build/debug/exe.build/exe.swiftmodule.o":
23412341
tool: shell
2342-
inputs: ["/path/to/build/debug/exe.build/exe.swiftmodule"]
2342+
inputs: ["/path/to/build/debug/exe.swiftmodule"]
23432343
outputs: ["/path/to/build/debug/exe.build/exe.swiftmodule.o"]
23442344
description: "Wrapping AST for exe for debugging"
2345-
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"]
2345+
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"]
23462346
"""))
23472347
XCTAssertMatch(contents, .contains("""
23482348
"/path/to/build/debug/lib.build/lib.swiftmodule.o":

0 commit comments

Comments
 (0)