Skip to content

Commit 8caff70

Browse files
committed
Build: initial pass to support static archives on Windows
Introduce a SPM controlled build rule for building static libraries. This is the intended way to use llbuild to drive the generation of static libraries. We would previously rely on the static default rule intended for testing to generate the static libraries. Not only did this tool not properly support Windows, it would actually cause problems on macOS due to the use of `ar` for the creation of the library over the preferred tool - `libtool`. We now locally determine the correct rule and generate the command. This is incomplete support for Windows and in fact regresses functionality. We no longer honour `AR` as an environment variable on Windows and thus cannot switch the implementation of the librarian. We now drive the archiving through `lld-link` unconditionally while we should prefer `link` unless otherwise requested. This is covered as an issue in #5719.
1 parent 630bdea commit 8caff70

File tree

8 files changed

+154
-16
lines changed

8 files changed

+154
-16
lines changed

Sources/Build/BuildPlan.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,19 @@ public final class ProductBuildDescription {
13441344
}
13451345
}
13461346

1347+
/// The arguments to the librarian to create a static library.
1348+
public func archiveArguments() throws -> [String] {
1349+
let librarian = buildParameters.toolchain.librarianPath.pathString
1350+
let triple = buildParameters.triple
1351+
if triple.isWindows(), librarian.hasSuffix("link") || librarian.hasSuffix("link.exe") {
1352+
return [librarian, "/LIB", "/OUT:\(binary.pathString)", "@\(linkFileListPath.pathString)"]
1353+
}
1354+
if triple.isDarwin(), librarian.hasSuffix("libtool") {
1355+
return [librarian, "-o", binary.pathString, "@\(linkFileListPath.pathString)"]
1356+
}
1357+
return [librarian, "crs", binary.pathString, "@\(linkFileListPath.pathString)"]
1358+
}
1359+
13471360
/// The arguments to link and create this product.
13481361
public func linkArguments() throws -> [String] {
13491362
var args = [buildParameters.toolchain.swiftCompilerPath.pathString]

Sources/Build/LLBuildManifestBuilder.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -853,14 +853,17 @@ extension LLBuildManifestBuilder {
853853
private func createProductCommand(_ buildProduct: ProductBuildDescription) throws {
854854
let cmdName = try buildProduct.product.getCommandName(config: buildConfig)
855855

856-
// Create archive tool for static library and shell tool for rest of the products.
857-
if buildProduct.product.type == .library(.static) {
858-
manifest.addArchiveCmd(
856+
switch buildProduct.product.type {
857+
case .library(.static):
858+
manifest.addShellCmd(
859859
name: cmdName,
860+
description: "Archiving \(buildProduct.binary.prettyPath())",
860861
inputs: buildProduct.objects.map(Node.file),
861-
outputs: [.file(buildProduct.binary)]
862+
outputs: [.file(buildProduct.binary)],
863+
arguments: try buildProduct.archiveArguments()
862864
)
863-
} else {
865+
866+
default:
864867
let inputs = buildProduct.objects + buildProduct.dylibs.map({ $0.binary })
865868

866869
manifest.addShellCmd(

Sources/LLBuildManifest/BuildManifest.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,6 @@ public struct BuildManifest {
8888
commands[name] = Command(name: name, tool: tool)
8989
}
9090

91-
public mutating func addArchiveCmd(
92-
name: String,
93-
inputs: [Node],
94-
outputs: [Node]
95-
) {
96-
assert(commands[name] == nil, "already had a command named '\(name)'")
97-
let tool = ArchiveTool(inputs: inputs, outputs: outputs)
98-
commands[name] = Command(name: name, tool: tool)
99-
}
100-
10191
public mutating func addShellCmd(
10292
name: String,
10393
description: String,

Sources/PackageModel/Toolchain.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
import TSCBasic
1414

1515
public protocol Toolchain {
16+
/// Path of the librarian.
17+
var librarianPath: AbsolutePath { get }
18+
1619
/// Path of the `swiftc` compiler.
1720
var swiftCompilerPath: AbsolutePath { get }
1821

Sources/PackageModel/ToolchainConfiguration.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import TSCBasic
1818
/// These requirements are abstracted out to make it easier to add support for
1919
/// using the package manager with alternate toolchains in the future.
2020
public struct ToolchainConfiguration {
21+
/// The path of the librarian.
22+
public var librarianPath: AbsolutePath
23+
2124
/// The path of the swift compiler.
2225
public var swiftCompilerPath: AbsolutePath
2326

@@ -43,13 +46,15 @@ public struct ToolchainConfiguration {
4346
/// Creates the set of manifest resources associated with a `swiftc` executable.
4447
///
4548
/// - Parameters:
46-
/// - swiftCompilerPath: The absolute path of the associated swift compiler executable (`swiftc`).
49+
/// - librarianPath: The absolute path to the librarian
50+
/// - swiftCompilerPath: The absolute path of the associated swift compiler executable (`swiftc`).
4751
/// - swiftCompilerFlags: Extra flags to pass to the Swift compiler.
4852
/// - swiftCompilerEnvironment: Environment variables to pass to the Swift compiler.
4953
/// - swiftPMLibrariesRootPath: Custom path for SwiftPM libraries. Computed based on the compiler path by default.
5054
/// - sdkRootPath: Optional path to SDK root.
5155
/// - xctestPath: Optional path to XCTest.
5256
public init(
57+
librarianPath: AbsolutePath,
5358
swiftCompilerPath: AbsolutePath,
5459
swiftCompilerFlags: [String] = [],
5560
swiftCompilerEnvironment: EnvironmentVariables = .process(),
@@ -61,6 +66,7 @@ public struct ToolchainConfiguration {
6166
return .init(swiftCompilerPath: swiftCompilerPath)
6267
}()
6368

69+
self.librarianPath = librarianPath
6470
self.swiftCompilerPath = swiftCompilerPath
6571
self.swiftCompilerFlags = swiftCompilerFlags
6672
self.swiftCompilerEnvironment = swiftCompilerEnvironment

Sources/PackageModel/UserToolchain.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ public final class UserToolchain: Toolchain {
2828
/// The toolchain configuration.
2929
private let configuration: ToolchainConfiguration
3030

31+
/// Path of the librarian.
32+
public let librarianPath: AbsolutePath
33+
3134
/// Path of the `swiftc` compiler.
3235
public let swiftCompilerPath: AbsolutePath
3336

@@ -113,6 +116,43 @@ public final class UserToolchain: Toolchain {
113116

114117
// MARK: - public API
115118

119+
public static func determineLibrarian(triple: Triple, binDir: AbsolutePath,
120+
useXcrun: Bool,
121+
environment: EnvironmentVariables,
122+
searchPaths: [AbsolutePath]) throws
123+
-> AbsolutePath {
124+
let variable: String = triple.isDarwin() ? "LIBTOOL" : "AR"
125+
let tool: String = {
126+
if triple.isDarwin() { return "libtool" }
127+
if triple.isWindows() {
128+
if let librarian: AbsolutePath =
129+
UserToolchain.lookup(variable: "AR",
130+
searchPaths: searchPaths,
131+
environment: environment) {
132+
return librarian.basename
133+
}
134+
// TODO(5719) use `lld-link` if the build requests lld.
135+
return "link"
136+
}
137+
// TODO(compnerd) consider defaulting to `llvm-ar` universally with
138+
// a fallback to `ar`.
139+
return triple.isAndroid() ? "llvm-ar" : "ar"
140+
}()
141+
142+
if let librarian: AbsolutePath = UserToolchain.lookup(variable: variable,
143+
searchPaths: searchPaths,
144+
environment: environment) {
145+
if localFileSystem.isExecutableFile(librarian) {
146+
return librarian
147+
}
148+
}
149+
150+
if let librarian = try? UserToolchain.getTool(tool, binDir: binDir) {
151+
return librarian
152+
}
153+
return try UserToolchain.findTool(tool, envSearchPaths: searchPaths, useXcrun: useXcrun)
154+
}
155+
116156
/// Determines the Swift compiler paths for compilation and manifest parsing.
117157
public static func determineSwiftCompilers(binDir: AbsolutePath, useXcrun: Bool, environment: EnvironmentVariables, searchPaths: [AbsolutePath]) throws -> SwiftCompilers {
118158
func validateCompiler(at path: AbsolutePath?) throws {
@@ -339,6 +379,8 @@ public final class UserToolchain: Toolchain {
339379
// Use the triple from destination or compute the host triple using swiftc.
340380
var triple = destination.target ?? Triple.getHostTriple(usingSwiftCompiler: swiftCompilers.compile)
341381

382+
self.librarianPath = try UserToolchain.determineLibrarian(triple: triple, binDir: binDir, useXcrun: useXcrun, environment: environment, searchPaths: envSearchPaths)
383+
342384
// Change the triple to the specified arch if there's exactly one of them.
343385
// The Triple property is only looked at by the native build system currently.
344386
if archs.count == 1 {
@@ -400,6 +442,7 @@ public final class UserToolchain: Toolchain {
400442
}
401443

402444
self.configuration = .init(
445+
librarianPath: librarianPath,
403446
swiftCompilerPath: swiftCompilers.manifest,
404447
swiftCompilerFlags: self.extraSwiftCFlags,
405448
swiftCompilerEnvironment: environment,

Tests/BuildTests/BuildPlanTests.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3227,6 +3227,77 @@ final class BuildPlanTests: XCTestCase {
32273227
"""))
32283228
}
32293229

3230+
func testArchiving() throws {
3231+
let fs = InMemoryFileSystem(emptyFiles:
3232+
"/Package/Sources/rary/rary.swift"
3233+
)
3234+
3235+
let observability = ObservabilitySystem.makeForTesting()
3236+
let graph = try loadPackageGraph(
3237+
fileSystem: fs,
3238+
manifests: [
3239+
Manifest.createRootManifest(
3240+
name: "Package",
3241+
path: .init("/Package"),
3242+
products: [
3243+
ProductDescription(name: "rary", type: .library(.static), targets: ["rary"]),
3244+
],
3245+
targets: [
3246+
TargetDescription(name: "rary", dependencies: []),
3247+
]
3248+
),
3249+
],
3250+
observabilityScope: observability.topScope
3251+
)
3252+
XCTAssertNoDiagnostics(observability.diagnostics)
3253+
3254+
let result = try BuildPlanResult(plan: BuildPlan(
3255+
buildParameters: mockBuildParameters(),
3256+
graph: graph,
3257+
fileSystem: fs,
3258+
observabilityScope: observability.topScope
3259+
))
3260+
3261+
let buildPath: AbsolutePath = result.plan.buildParameters.dataPath.appending(components: "debug")
3262+
3263+
let yaml = fs.tempDirectory.appending(components: UUID().uuidString, "debug.yaml")
3264+
try fs.createDirectory(yaml.parentDirectory, recursive: true)
3265+
3266+
let llbuild = LLBuildManifestBuilder(result.plan, fileSystem: fs, observabilityScope: observability.topScope)
3267+
try llbuild.generateManifest(at: yaml)
3268+
3269+
let contents: String = try fs.readFileContents(yaml)
3270+
3271+
if result.plan.buildParameters.triple.isWindows() {
3272+
XCTAssertMatch(contents, .contains("""
3273+
"C.rary-debug.a":
3274+
tool: shell
3275+
inputs: ["\(buildPath.appending(components: "rary.build", "rary.swift.o").escapedPathString())","\(buildPath.appending(components: "rary.build", "rary.swiftmodule.o").escapedPathString())"]
3276+
outputs: ["\(buildPath.appending(components: "library.a").escapedPathString())"]
3277+
description: "Archiving \(buildPath.appending(components: "library.a").escapedPathString())"
3278+
args: ["\(result.plan.buildParameters.toolchain.librarianPath.escapedPathString())","/LIB","/OUT:\(buildPath.appending(components: "library.a").escapedPathString())","@\(buildPath.appending(components: "rary.product", "Objects.LinkFileList").escapedPathString())"]
3279+
"""))
3280+
} else if result.plan.buildParameters.triple.isDarwin() {
3281+
XCTAssertMatch(contents, .contains("""
3282+
"C.rary-debug.a":
3283+
tool: shell
3284+
inputs: ["\(buildPath.appending(components: "rary.build", "rary.swift.o").escapedPathString())"]
3285+
outputs: ["\(buildPath.appending(components: "library.a").escapedPathString())"]
3286+
description: "Archiving \(buildPath.appending(components: "library.a").escapedPathString())"
3287+
args: ["\(result.plan.buildParameters.toolchain.librarianPath.escapedPathString())","-o","\(buildPath.appending(components: "library.a").escapedPathString())","@\(buildPath.appending(components: "rary.product", "Objects.LinkFileList").escapedPathString())"]
3288+
"""))
3289+
} else { // assume Unix `ar` is the librarian
3290+
XCTAssertMatch(contents, .contains("""
3291+
"C.rary-debug.a":
3292+
tool: shell
3293+
inputs: ["\(buildPath.appending(components: "rary.build", "rary.swift.o").escapedPathString())","\(buildPath.appending(components: "rary.build", "rary.swiftmodule.o").escapedPathString())"]
3294+
outputs: ["\(buildPath.appending(components: "library.a").escapedPathString())"]
3295+
description: "Archiving \(buildPath.appending(components: "library.a").escapedPathString())"
3296+
args: ["\(result.plan.buildParameters.toolchain.librarianPath.escapedPathString())","crs","\(buildPath.appending(components: "library.a").escapedPathString())","@\(buildPath.appending(components: "rary.product", "Objects.LinkFileList").escapedPathString())"]
3297+
"""))
3298+
}
3299+
}
3300+
32303301
func testSwiftBundleAccessor() throws {
32313302
// This has a Swift and ObjC target in the same package.
32323303
let fs = InMemoryFileSystem(emptyFiles:

Tests/BuildTests/MockBuildTestHelper.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ import TSCBasic
77
import XCTest
88

99
struct MockToolchain: PackageModel.Toolchain {
10+
#if os(Windows)
11+
let librarianPath = AbsolutePath("/fake/path/to/link.exe")
12+
#elseif os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
13+
let librarianPath = AbsolutePath("/fake/path/to/libtool")
14+
#elseif os(Android)
15+
let librarianPath = AbsolutePath("/fake/path/to/llvm-ar")
16+
#else
17+
let librarianPath = AbsolutePath("/fake/path/to/ar")
18+
#endif
1019
let swiftCompilerPath = AbsolutePath("/fake/path/to/swiftc")
1120
let extraCCFlags: [String] = []
1221
let extraSwiftCFlags: [String] = []

0 commit comments

Comments
 (0)