Skip to content

Commit c3e3a3f

Browse files
committed
Set output path for bc files
- Updates spm to pass the object file path as the output path for bitcode files for use when building with lto. A more ideal solution may be to use a different path for bitcode files, however using the `.o` extension is also a common pattern and requires a significantly less invasive diff.
1 parent 3f4af6a commit c3e3a3f

File tree

7 files changed

+177
-21
lines changed

7 files changed

+177
-21
lines changed

Sources/Build/BuildDescription/ClangTargetBuildDescription.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,17 @@ public final class ClangTargetBuildDescription {
293293
// User arguments (from -Xcxx) should follow generated arguments to allow user overrides
294294
args += self.buildParameters.flags.cxxCompilerFlags
295295
}
296+
297+
// Enable the correct lto mode if requested.
298+
switch self.buildParameters.linkTimeOptimizationMode {
299+
case nil:
300+
break
301+
case .full:
302+
args += ["-flto=full"]
303+
case .thin:
304+
args += ["-flto=thin"]
305+
}
306+
296307
return args
297308
}
298309

Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,19 @@ public final class SwiftTargetBuildDescription {
7979
self.target.underlyingTarget.resources + self.pluginDerivedResources
8080
}
8181

82-
/// The objects in this target.
82+
/// The objects in this target, containing either machine code or bitcode
83+
/// depending on the build parameters used.
8384
public var objects: [AbsolutePath] {
8485
get throws {
85-
let relativePaths = self.target.sources.relativePaths + self.derivedSources.relativePaths + self
86-
.pluginDerivedSources.relativePaths
87-
return try relativePaths.map {
88-
try AbsolutePath(validating: "\($0.pathString).o", relativeTo: tempsPath)
86+
let relativeSources = self.target.sources.relativePaths
87+
+ self.derivedSources.relativePaths
88+
+ self.pluginDerivedSources.relativePaths
89+
let ltoEnabled = self.buildParameters.linkTimeOptimizationMode != nil
90+
let objectFileExtension = ltoEnabled ? "bc" : "o"
91+
return try relativeSources.map {
92+
try AbsolutePath(
93+
validating: "\($0.pathString).\(objectFileExtension)",
94+
relativeTo: self.tempsPath)
8995
}
9096
}
9197
}
@@ -512,6 +518,16 @@ public final class SwiftTargetBuildDescription {
512518
// // User arguments (from -Xcxx) should follow generated arguments to allow user overrides
513519
// args += self.buildParameters.flags.cxxCompilerFlags.asSwiftcCXXCompilerFlags()
514520

521+
// Enable the correct lto mode if requested.
522+
switch self.buildParameters.linkTimeOptimizationMode {
523+
case nil:
524+
break
525+
case .full:
526+
args += ["-lto=llvm-full"]
527+
case .thin:
528+
args += ["-lto=llvm-thin"]
529+
}
530+
515531
// suppress warnings if the package is remote
516532
if self.package.isRemote {
517533
args += ["-suppress-warnings"]
@@ -680,6 +696,16 @@ public final class SwiftTargetBuildDescription {
680696
// // User arguments (from -Xcxx) should follow generated arguments to allow user overrides
681697
// result += self.buildParameters.flags.cxxCompilerFlags.asSwiftcCXXCompilerFlags()
682698

699+
// Enable the correct lto mode if requested.
700+
switch self.buildParameters.linkTimeOptimizationMode {
701+
case nil:
702+
break
703+
case .full:
704+
result += ["-lto=llvm-full"]
705+
case .thin:
706+
result += ["-lto=llvm-thin"]
707+
}
708+
683709
result += try self.macroArguments()
684710
return result
685711
}
@@ -729,14 +755,18 @@ public final class SwiftTargetBuildDescription {
729755

730756

731757
// Write out the entries for each source file.
732-
let sources = self.target.sources.paths + self.derivedSources.paths + self.pluginDerivedSources.paths
733-
for (idx, source) in sources.enumerated() {
734-
let object = try objects[idx]
735-
let objectDir = object.parentDirectory
758+
let sources = self.sources
759+
let objects = try self.objects
760+
let ltoEnabled = self.buildParameters.linkTimeOptimizationMode != nil
761+
let objectKey = ltoEnabled ? "llvm-bc" : "object"
736762

737-
let sourceFileName = source.basenameWithoutExt
763+
for idx in 0..<sources.count {
764+
let source = sources[idx]
765+
let object = objects[idx]
738766

739-
let swiftDepsPath = objectDir.appending(component: sourceFileName + ".swiftdeps")
767+
let sourceFileName = source.basenameWithoutExt
768+
let partialModulePath = self.tempsPath.appending(component: sourceFileName + "~partial.swiftmodule")
769+
let swiftDepsPath = self.tempsPath.appending(component: sourceFileName + ".swiftdeps")
740770

741771
content +=
742772
#"""
@@ -745,7 +775,7 @@ public final class SwiftTargetBuildDescription {
745775
"""#
746776

747777
if !self.buildParameters.useWholeModuleOptimization {
748-
let depsPath = objectDir.appending(component: sourceFileName + ".d")
778+
let depsPath = self.tempsPath.appending(component: sourceFileName + ".d")
749779
content +=
750780
#"""
751781
"dependencies": "\#(depsPath._nativePathString(escaped: true))",
@@ -754,12 +784,9 @@ public final class SwiftTargetBuildDescription {
754784
// FIXME: Need to record this deps file for processing it later.
755785
}
756786

757-
758-
let partialModulePath = objectDir.appending(component: sourceFileName + "~partial.swiftmodule")
759-
760787
content +=
761788
#"""
762-
"object": "\#(object._nativePathString(escaped: true))",
789+
"\#(objectKey)": "\#(object._nativePathString(escaped: true))",
763790
"swiftmodule": "\#(partialModulePath._nativePathString(escaped: true))",
764791
"swift-dependencies": "\#(swiftDepsPath._nativePathString(escaped: true))"
765792
}\#((idx + 1) < sources.count ? "," : "")

Sources/CoreCommands/Options.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,13 @@ public struct BuildOptions: ParsableArguments {
470470
)
471471
public var testEntryPointPath: AbsolutePath?
472472

473+
/// The lto mode to use if any.
474+
@Option(
475+
name: .customLong("experimental-lto-mode"),
476+
help: .hidden
477+
)
478+
public var linkTimeOptimizationMode: LinkTimeOptimizationMode?
479+
473480
// @Flag works best when there is a default value present
474481
// if true, false aren't enough and a third state is needed
475482
// nil should not be the goto. Instead create an enum
@@ -484,6 +491,14 @@ public struct BuildOptions: ParsableArguments {
484491
case warn
485492
case error
486493
}
494+
495+
/// See `BuildParameters.LinkTimeOptimizationMode` for details.
496+
public enum LinkTimeOptimizationMode: String, Codable, ExpressibleByArgument {
497+
/// See `BuildParameters.LinkTimeOptimizationMode.full` for details.
498+
case full
499+
/// See `BuildParameters.LinkTimeOptimizationMode.thin` for details.
500+
case thin
501+
}
487502
}
488503

489504
public struct LinkerOptions: ParsableArguments {

Sources/CoreCommands/SwiftTool.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,8 @@ public final class SwiftTool {
703703
testEntryPointPath: options.build.testEntryPointPath,
704704
explicitTargetDependencyImportCheckingMode: options.build.explicitTargetDependencyImportCheck.modeParameter,
705705
linkerDeadStrip: options.linker.linkerDeadStrip,
706-
verboseOutput: self.logLevel <= .info
706+
verboseOutput: self.logLevel <= .info,
707+
linkTimeOptimizationMode: options.build.linkTimeOptimizationMode?.buildParameter
707708
)
708709
})
709710
}()
@@ -966,6 +967,17 @@ extension BuildOptions.TargetDependencyImportCheckingMode {
966967
}
967968
}
968969

970+
extension BuildOptions.LinkTimeOptimizationMode {
971+
fileprivate var buildParameter: BuildParameters.LinkTimeOptimizationMode? {
972+
switch self {
973+
case .full:
974+
return .full
975+
case .thin:
976+
return .thin
977+
}
978+
}
979+
}
980+
969981
extension Basics.Diagnostic {
970982
public static func mutuallyExclusiveArgumentsError(arguments: [String]) -> Self {
971983
.error(arguments.map { "'\($0)'" }.spm_localizedJoin(type: .conjunction) + " are mutually exclusive")

Sources/SPMBuildCore/BuildParameters.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,32 @@ public struct BuildParameters: Encodable {
2626
case auto
2727
}
2828

29+
/// An optional intermodule optimization to run at link time.
30+
///
31+
/// When using Link Time Optimization (LTO for short) the swift and clang
32+
/// compilers produce objects containing containing a higher level
33+
/// representation of the program bitcode instead of machine code. The
34+
/// linker combines these objects together performing additional
35+
/// optimizations with visibility into each module/object, resulting in a
36+
/// further optimized version of the executable.
37+
///
38+
/// Using LTO can have significant impact on compile times, however can be
39+
/// used to dramatically reduce code-size in some cases.
40+
///
41+
/// Note: Bitcode objects and machine code objects can be linked together.
42+
public enum LinkTimeOptimizationMode: String, Encodable {
43+
/// The "standard" LTO mode designed to produce minimal code sign.
44+
///
45+
/// Full LTO can lead to large link times. Consider using thin LTO if
46+
/// build time is more important than minimizing binary size.
47+
case full
48+
/// An LTO mode designed to scale better with input size.
49+
///
50+
/// Thin LTO typically results in faster link times than traditional LTO.
51+
/// However, thin LTO may not result in binary as small as full LTO.
52+
case thin
53+
}
54+
2955
/// Represents the debugging strategy.
3056
///
3157
/// Swift binaries requires the swiftmodule files in order for lldb to work.
@@ -219,6 +245,8 @@ public struct BuildParameters: Encodable {
219245

220246
public var verboseOutput: Bool
221247

248+
public var linkTimeOptimizationMode: LinkTimeOptimizationMode?
249+
222250
public init(
223251
dataPath: AbsolutePath,
224252
configuration: BuildConfiguration,
@@ -247,7 +275,8 @@ public struct BuildParameters: Encodable {
247275
explicitTargetDependencyImportCheckingMode: TargetDependencyImportCheckingMode = .none,
248276
linkerDeadStrip: Bool = true,
249277
colorizedOutput: Bool = false,
250-
verboseOutput: Bool = false
278+
verboseOutput: Bool = false,
279+
linkTimeOptimizationMode: LinkTimeOptimizationMode? = nil
251280
) throws {
252281
let triple = try destinationTriple ?? .getHostTriple(usingSwiftCompiler: toolchain.swiftCompilerPath)
253282

@@ -288,6 +317,7 @@ public struct BuildParameters: Encodable {
288317
self.linkerDeadStrip = linkerDeadStrip
289318
self.colorizedOutput = colorizedOutput
290319
self.verboseOutput = verboseOutput
320+
self.linkTimeOptimizationMode = linkTimeOptimizationMode
291321
}
292322

293323
public func withDestination(_ destinationTriple: Triple) throws -> BuildParameters {
@@ -330,7 +360,8 @@ public struct BuildParameters: Encodable {
330360
explicitTargetDependencyImportCheckingMode: self.explicitTargetDependencyImportCheckingMode,
331361
linkerDeadStrip: self.linkerDeadStrip,
332362
colorizedOutput: self.colorizedOutput,
333-
verboseOutput: self.verboseOutput
363+
verboseOutput: self.verboseOutput,
364+
linkTimeOptimizationMode: self.linkTimeOptimizationMode
334365
)
335366
}
336367

Tests/BuildTests/BuildPlanTests.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4690,4 +4690,62 @@ final class BuildPlanTests: XCTestCase {
46904690

46914691
XCTAssertMatch(try result.buildProduct(for: "exe").linkArguments(), ["-sanitize=\(expectedName)"])
46924692
}
4693+
4694+
func testBuildParameterLTOMode() throws {
4695+
let fileSystem = InMemoryFileSystem(emptyFiles:
4696+
"/Pkg/Sources/exe/main.swift",
4697+
"/Pkg/Sources/cLib/cLib.c",
4698+
"/Pkg/Sources/cLib/include/cLib.h"
4699+
)
4700+
4701+
let observability = ObservabilitySystem.makeForTesting()
4702+
let graph = try loadPackageGraph(
4703+
fileSystem: fileSystem,
4704+
manifests: [
4705+
Manifest.createRootManifest(
4706+
displayName: "Pkg",
4707+
path: "/Pkg",
4708+
targets: [
4709+
TargetDescription(name: "exe", dependencies: ["cLib"]),
4710+
TargetDescription(name: "cLib", dependencies: []),
4711+
]),
4712+
],
4713+
observabilityScope: observability.topScope
4714+
)
4715+
XCTAssertNoDiagnostics(observability.diagnostics)
4716+
4717+
let toolchain = try UserToolchain.default
4718+
let buildParameters = mockBuildParameters(
4719+
toolchain: toolchain,
4720+
linkTimeOptimizationMode: .full)
4721+
let result = try BuildPlanResult(plan: BuildPlan(
4722+
buildParameters: buildParameters,
4723+
graph: graph,
4724+
fileSystem: fileSystem,
4725+
observabilityScope: observability.topScope))
4726+
result.checkProductsCount(1)
4727+
result.checkTargetsCount(2)
4728+
4729+
// Compile C Target
4730+
let cLibCompileArguments = try result.target(for: "cLib").clangTarget().basicArguments(isCXX: false)
4731+
let cLibCompileArgumentsPattern: [StringPattern] = ["-flto=full"]
4732+
XCTAssertMatch(cLibCompileArguments, cLibCompileArgumentsPattern)
4733+
4734+
// Compile Swift Target
4735+
let exeCompileArguments = try result.target(for: "exe").swiftTarget().compileArguments()
4736+
let exeCompileArgumentsPattern: [StringPattern] = ["-lto=llvm-full"]
4737+
XCTAssertMatch(exeCompileArguments, exeCompileArgumentsPattern)
4738+
4739+
// Assert the objects built by the Swift Target are actually bitcode
4740+
// files, indicated by the "bc" extension.
4741+
let exeCompileObjects = try result.target(for: "exe").swiftTarget().objects
4742+
XCTAssert(exeCompileObjects.allSatisfy { $0.extension == "bc"})
4743+
4744+
// Assert the objects getting linked contain all the bitcode objects
4745+
// built by the Swift Target
4746+
let exeProduct = try result.buildProduct(for: "exe")
4747+
for exeCompileObject in exeCompileObjects {
4748+
XCTAssertTrue(exeProduct.objects.contains(exeCompileObject))
4749+
}
4750+
}
46934751
}

Tests/BuildTests/MockBuildTestHelper.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ func mockBuildParameters(
7272
destinationTriple: Basics.Triple = hostTriple,
7373
indexStoreMode: BuildParameters.IndexStoreMode = .off,
7474
useExplicitModuleBuild: Bool = false,
75-
linkerDeadStrip: Bool = true
75+
linkerDeadStrip: Bool = true,
76+
linkTimeOptimizationMode: BuildParameters.LinkTimeOptimizationMode? = nil
7677
) -> BuildParameters {
7778
return try! BuildParameters(
7879
dataPath: buildPath,
@@ -87,7 +88,8 @@ func mockBuildParameters(
8788
canRenameEntrypointFunctionName: canRenameEntrypointFunctionName,
8889
indexStoreMode: indexStoreMode,
8990
useExplicitModuleBuild: useExplicitModuleBuild,
90-
linkerDeadStrip: linkerDeadStrip
91+
linkerDeadStrip: linkerDeadStrip,
92+
linkTimeOptimizationMode: linkTimeOptimizationMode
9193
)
9294
}
9395

0 commit comments

Comments
 (0)