Skip to content

Commit 71dc485

Browse files
committed
WIP: Allow unit tests to link against executable targets, postprocessing their object files to elide _main. Currently works only on Darwin (it requires nmedit) but can be made to work on other platforms using strip. It would be better to have a linker flag to allow _main to be ignored from certain .o files. It's also not clear that we will want to allow any product to link against an executable target.
1 parent dafe0e6 commit 71dc485

File tree

2 files changed

+82
-6
lines changed

2 files changed

+82
-6
lines changed

Sources/Build/BuildPlan.swift

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,7 +1001,7 @@ public final class ProductBuildDescription {
10011001
return buildParameters.binaryPath(for: product)
10021002
}
10031003

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

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

@@ -1032,6 +1036,11 @@ public final class ProductBuildDescription {
10321036
return tempsPath.appending(component: "Objects.LinkFileList")
10331037
}
10341038

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

@@ -1189,6 +1198,16 @@ public final class ProductBuildDescription {
11891198
try fs.writeFileContents(linkFileListPath, bytes: stream.bytes)
11901199
}
11911200

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

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

1490-
for target in dependencies.staticTargets {
1509+
for target in dependencies.staticTargets + dependencies.executableTargets {
14911510
switch target.underlyingTarget {
14921511
case is SwiftTarget:
14931512
// Swift targets are guaranteed to have a corresponding Swift description.
@@ -1509,10 +1528,29 @@ public class BuildPlan {
15091528
}
15101529
}
15111530

1512-
buildProduct.staticTargets = dependencies.staticTargets
1531+
buildProduct.staticTargets = dependencies.staticTargets + dependencies.executableTargets
15131532
buildProduct.dylibs = dependencies.dylibs.map({ productMap[$0]! })
15141533
buildProduct.objects += dependencies.staticTargets.flatMap({ targetMap[$0]!.objects })
15151534
buildProduct.libraryBinaryPaths = dependencies.libraryBinaryPaths
1535+
1536+
// If we're linking against any executable targets, we need to create versions of the .o files from those
1537+
// targets that elide the `_main` symbol. We should look into whether linker options can be added to specify
1538+
// this on the command line.
1539+
if !dependencies.executableTargets.isEmpty {
1540+
// Creating a mapping from each .o file in each executable target to a corresponding modified .o file in
1541+
// our product directory. This duplicates work if an executable is tested by more than one test product
1542+
// but has the advantage of keeping the executable target clean unless it's being used by a test target.
1543+
for target in dependencies.executableTargets.map({ targetMap[$0]! }) {
1544+
for object in target.objects {
1545+
// FIXME: Plenty of opportunity for collisions here — how is this handled for regular object files?
1546+
let mainlessObject = buildProduct.tempsPath.appending(components: "LinkedExecutableObjects", "\(target.target.c99name)_\(object.basename)")
1547+
buildProduct.executableObjects[mainlessObject] = object
1548+
buildProduct.objects.insert(mainlessObject)
1549+
}
1550+
}
1551+
// The symbol removal tool on some platforms requires a separate file list in the file system.
1552+
try buildProduct.writeMainSymbolRemovalListFile(fileSystem)
1553+
}
15161554

15171555
// Write the link filelist file.
15181556
//
@@ -1527,6 +1565,7 @@ public class BuildPlan {
15271565
) -> (
15281566
dylibs: [ResolvedProduct],
15291567
staticTargets: [ResolvedTarget],
1568+
executableTargets: [ResolvedTarget],
15301569
systemModules: [ResolvedTarget],
15311570
libraryBinaryPaths: Set<AbsolutePath>
15321571
) {
@@ -1554,6 +1593,7 @@ public class BuildPlan {
15541593
// Create empty arrays to collect our results.
15551594
var linkLibraries = [ResolvedProduct]()
15561595
var staticTargets = [ResolvedTarget]()
1596+
var executableTargets = [ResolvedTarget]()
15571597
var systemModules = [ResolvedTarget]()
15581598
var libraryBinaryPaths: Set<AbsolutePath> = []
15591599

@@ -1563,7 +1603,14 @@ public class BuildPlan {
15631603
switch target.type {
15641604
// Include executable and tests only if they're top level contents
15651605
// of the product. Otherwise they are just build time dependency.
1566-
case .executable, .test:
1606+
case .executable:
1607+
if product.targets.contains(target) {
1608+
staticTargets.append(target)
1609+
}
1610+
else {
1611+
executableTargets.append(target)
1612+
}
1613+
case .test:
15671614
if product.targets.contains(target) {
15681615
staticTargets.append(target)
15691616
}
@@ -1598,7 +1645,7 @@ public class BuildPlan {
15981645
}
15991646
}
16001647

1601-
return (linkLibraries, staticTargets, systemModules, libraryBinaryPaths)
1648+
return (linkLibraries, staticTargets, executableTargets, systemModules, libraryBinaryPaths)
16021649
}
16031650

16041651
/// Plan a Clang target.

Sources/Build/ManifestBuilder.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,30 @@ extension LLBuildManifestBuilder {
610610
outputs: [.file(target.wrappedModuleOutputPath)],
611611
args: moduleWrapArgs)
612612
}
613+
614+
/// Add a command to produce a new .o file that removes (or hides) the `_main` symbol from a compiled .o file.
615+
/// This is used to modify .o files produced by executables so they can be linked into unit test products. The
616+
/// symbol list file is expected to only contain the symbol `_main` and is only needed because `nmedit` needs a
617+
/// file with the names of the symbols.
618+
private func addMainSymbolRemovalCmd(toolchain: Toolchain,
619+
inputFile: AbsolutePath, outputFile: AbsolutePath,
620+
mainSymbolListFile: AbsolutePath) {
621+
let args = [
622+
// FIXME: Using `nmedit` only works on Darwin. For Linux (and Windows?) we need to use `strip` from the
623+
// `binutils`, and for other platforms, who knows. This needs to be made generic.
624+
// FIXME: Even on Darwin, we need the toolchain to provide the path of the `nmedit` tool.
625+
toolchain.swiftCompiler.parentDirectory.appending(component: "nmedit").pathString,
626+
"-R", mainSymbolListFile.pathString,
627+
inputFile.pathString,
628+
"-o", outputFile.pathString
629+
]
630+
manifest.addShellCmd(
631+
name: outputFile.pathString,
632+
description: "Eliding symbols from \(outputFile.basename)",
633+
inputs: [.file(inputFile)], // Note: we don't add the symbol file as an input since it's a constant
634+
outputs: [.file(outputFile)],
635+
args: args)
636+
}
613637
}
614638

615639
// MARK:- Compile C-family
@@ -773,6 +797,11 @@ extension LLBuildManifestBuilder {
773797
outputs: [.file(buildProduct.binary)],
774798
args: buildProduct.linkArguments()
775799
)
800+
801+
// Add a separate command to remove the main symbol.
802+
for (mainlessObject, object) in buildProduct.executableObjects {
803+
addMainSymbolRemovalCmd(toolchain: buildProduct.buildParameters.toolchain, inputFile: object, outputFile: mainlessObject, mainSymbolListFile: buildProduct.mainSymbolRemovalListFilePath)
804+
}
776805
}
777806

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

0 commit comments

Comments
 (0)