Skip to content

Support separate modules for static and dynamic exporting symbols for Windows #8049

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ public final class SwiftModuleBuildDescription {

var modulesPath: AbsolutePath {
let suffix = self.buildParameters.suffix
return self.buildParameters.buildPath.appending(component: "Modules\(suffix)")
var path = self.buildParameters.buildPath.appending(component: "Modules\(suffix)")
if self.windowsTargetType == .dynamic {
path = path.appending("dynamic")
}
return path
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't like how we're putting all the modules in a single directory and adding it to the include path. Since the exporting modules have the same name, I had to create another directory to put them in and add that include path for the products that consume them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah putting all the modules in the same directory is extra annoying because you can end up importing things you haven't declared a dependency on.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question is, who do I break if I change this?

}

/// The path to the swiftmodule file after compilation.
Expand Down Expand Up @@ -264,6 +268,19 @@ public final class SwiftModuleBuildDescription {
/// Whether to disable sandboxing (e.g. for macros).
private let shouldDisableSandbox: Bool

/// For Windows, we default to static objects and but also create objects
/// that export symbols for DLLs. This allows library targets to be used
/// in both contexts
public enum WindowsTargetType {
case `static`
case dynamic
}
/// The target type. Leave nil for non-Windows behavior.
public let windowsTargetType: WindowsTargetType?

/// The corresponding target for dynamic library export (i.e., not -static)
public private(set) var windowsDynamicTarget: SwiftModuleBuildDescription? = nil

/// Create a new target description with target and build parameters.
init(
package: ResolvedPackage,
Expand Down Expand Up @@ -319,6 +336,14 @@ public final class SwiftModuleBuildDescription {
observabilityScope: observabilityScope
)

if buildParameters.triple.isWindows() {
// Default to static and add another target for DLLs
self.windowsTargetType = .static
self.windowsDynamicTarget = .init(windowsExportFor: self)
} else {
self.windowsTargetType = nil
}

if self.shouldEmitObjCCompatibilityHeader {
self.moduleMap = try self.generateModuleMap()
}
Expand All @@ -340,6 +365,31 @@ public final class SwiftModuleBuildDescription {
try self.generateTestObservation()
}

/// Private init to set up exporting version of this module
private init(windowsExportFor parent: SwiftModuleBuildDescription) {
self.windowsTargetType = .dynamic
self.windowsDynamicTarget = nil
self.tempsPath = parent.tempsPath.appending("dynamic")

// The rest of these are just copied from the parent
self.package = parent.package
self.target = parent.target
self.swiftTarget = parent.swiftTarget
self.toolsVersion = parent.toolsVersion
self.buildParameters = parent.buildParameters
self.macroBuildParameters = parent.macroBuildParameters
self.derivedSources = parent.derivedSources
self.pluginDerivedSources = parent.pluginDerivedSources
self.pluginDerivedResources = parent.pluginDerivedResources
self.testTargetRole = parent.testTargetRole
self.fileSystem = parent.fileSystem
self.buildToolPluginInvocationResults = parent.buildToolPluginInvocationResults
self.prebuildCommandResults = parent.prebuildCommandResults
self.observabilityScope = parent.observabilityScope
self.shouldGenerateTestObservation = parent.shouldGenerateTestObservation
self.shouldDisableSandbox = parent.shouldDisableSandbox
}

private func generateTestObservation() throws {
guard target.type == .test else {
return
Expand Down Expand Up @@ -519,6 +569,18 @@ public final class SwiftModuleBuildDescription {
args += ["-parse-as-library"]
}

switch self.windowsTargetType {
case .static:
// Static on Windows
args += ["-static"]
case .dynamic:
// Add the static versions to the include path
// FIXME: need to be much more deliberate about what we're including
args += ["-I", self.modulesPath.parentDirectory.pathString]
case .none:
break
}

// Only add the build path to the framework search path if there are binary frameworks to link against.
if !self.libraryBinaryPaths.isEmpty {
args += ["-F", self.buildParameters.buildPath.pathString]
Expand Down
19 changes: 17 additions & 2 deletions Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ extension LLBuildManifestBuilder {
)
} else {
try self.addCmdWithBuiltinSwiftTool(target, inputs: inputs, cmdOutputs: cmdOutputs)
if let dynamicTarget = target.windowsDynamicTarget {
// Generate dynamic module for Windows
let inputs = try self.computeSwiftCompileCmdInputs(dynamicTarget)
let objectNodes = dynamicTarget.buildParameters.prepareForIndexing == .off ? try dynamicTarget.objects.map(Node.file) : []
let moduleNode = Node.file(dynamicTarget.moduleOutputPath)
let cmdOutputs = objectNodes + [moduleNode]
try self.addCmdWithBuiltinSwiftTool(dynamicTarget, inputs: inputs, cmdOutputs: cmdOutputs)
self.addTargetCmd(dynamicTarget, cmdOutputs: cmdOutputs)
try self.addModuleWrapCmd(dynamicTarget)
}
}

self.addTargetCmd(target, cmdOutputs: cmdOutputs)
Expand Down Expand Up @@ -532,7 +542,7 @@ extension LLBuildManifestBuilder {
inputs: cmdOutputs,
outputs: [targetOutput]
)
if self.plan.graph.isInRootPackages(target.target, satisfying: target.buildParameters.buildEnvironment) {
if self.plan.graph.isInRootPackages(target.target, satisfying: target.buildParameters.buildEnvironment), target.windowsTargetType != .dynamic {
if !target.isTestTarget {
self.addNode(targetOutput, toTarget: .main)
}
Expand Down Expand Up @@ -636,6 +646,11 @@ extension SwiftModuleBuildDescription {
}

public func getLLBuildTargetName() -> String {
self.target.getLLBuildTargetName(buildParameters: self.buildParameters)
let name = self.target.getLLBuildTargetName(buildParameters: self.buildParameters)
if self.windowsTargetType == .dynamic {
return "dynamic." + name
} else {
return name
}
}
}
13 changes: 12 additions & 1 deletion Sources/Build/BuildPlan/BuildPlan+Product.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,18 @@ extension BuildPlan {

buildProduct.staticTargets = dependencies.staticTargets.map(\.module)
buildProduct.dylibs = dependencies.dylibs
buildProduct.objects += try dependencies.staticTargets.flatMap { try $0.objects }
buildProduct.objects += try dependencies.staticTargets.flatMap {
if buildProduct.product.type == .library(.dynamic),
case let .swift(swiftModule) = $0,
let dynamic = swiftModule.windowsDynamicTarget,
buildProduct.product.modules.contains(id: swiftModule.target.id)
{
// On Windows, export symbols from the direct swift targets of the DLL product
return try dynamic.objects
} else {
return try $0.objects
}
}
buildProduct.libraryBinaryPaths = dependencies.libraryBinaryPaths
buildProduct.availableTools = dependencies.availableTools
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/Build/BuildPlan/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,9 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
switch buildTarget {
case .swift(let target):
try self.plan(swiftTarget: target)
if let dynamicTarget = target.windowsDynamicTarget {
try self.plan(swiftTarget: dynamicTarget)
}
case .clang(let target):
try self.plan(clangTarget: target)
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/_InternalTestSupport/MockBuildTestHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ extension Basics.Triple {
public static let arm64Linux = try! Self("aarch64-unknown-linux-gnu")
public static let arm64Android = try! Self("aarch64-unknown-linux-android")
public static let windows = try! Self("x86_64-unknown-windows-msvc")
public static let x86_64Windows = try! Self("x86_64-unknown-windows-msvc")
public static let arm64Windows = try! Self("aarch64-unknown-windows-msvc")
public static let wasi = try! Self("wasm32-unknown-wasi")
public static let arm64iOS = try! Self("arm64-apple-ios")
}
Expand Down
165 changes: 165 additions & 0 deletions Tests/BuildTests/WindowsBuildPlanTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import XCTest

import Basics
@testable import Build
import LLBuildManifest
import _InternalTestSupport

@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly)
import PackageGraph

final class WindowsBuildPlanTests: XCTestCase {
// Tests that our build plan is build correctly to handle separation
// of object files that export symbols and ones that don't and to ensure
// DLL products pick up the right ones.

func doTest(triple: Triple) async throws {
let fs = InMemoryFileSystem(emptyFiles: [
"/libPkg/Sources/coreLib/coreLib.swift",
"/libPkg/Sources/dllLib/dllLib.swift",
"/libPkg/Sources/staticLib/staticLib.swift",
"/libPkg/Sources/objectLib/objectLib.swift",
"/exePkg/Sources/exe/main.swift",
])

let observability = ObservabilitySystem.makeForTesting()

let graph = try loadModulesGraph(
fileSystem: fs,
manifests: [
.createFileSystemManifest(
displayName: "libPkg",
path: "/libPkg",
products: [
.init(name: "DLLProduct", type: .library(.dynamic), targets: ["dllLib"]),
.init(name: "StaticProduct", type: .library(.static), targets: ["staticLib"]),
.init(name: "ObjectProduct", type: .library(.automatic), targets: ["objectLib"]),
],
targets: [
.init(name: "coreLib", dependencies: []),
.init(name: "dllLib", dependencies: ["coreLib"]),
.init(name: "staticLib", dependencies: ["coreLib"]),
.init(name: "objectLib", dependencies: ["coreLib"]),
]
),
.createRootManifest(
displayName: "exePkg",
path: "/exePkg",
dependencies: [.fileSystem(path: "/libPkg")],
targets: [
.init(name: "exe", dependencies: [
.product(name: "DLLProduct", package: "libPkg"),
.product(name: "StaticProduct", package: "libPkg"),
.product(name: "ObjectProduct", package: "libPkg"),
]),
]
)
],
observabilityScope: observability.topScope
)

let label: String
let dylibPrefix: String
let dylibExtension: String
let dynamic: String
switch triple {
case Triple.x86_64Windows:
label = "x86_64-unknown-windows-msvc"
dylibPrefix = ""
dylibExtension = "dll"
dynamic = "/dynamic"
case Triple.x86_64MacOS:
label = "x86_64-apple-macosx"
dylibPrefix = "lib"
dylibExtension = "dylib"
dynamic = ""
case Triple.x86_64Linux:
label = "x86_64-unknown-linux-gnu"
dylibPrefix = "lib"
dylibExtension = "so"
dynamic = ""
default:
label = "fixme"
dylibPrefix = ""
dylibExtension = ""
dynamic = ""
}

let tools: [String: [String]] = [
"C.exe-\(label)-debug.exe": [
"/path/to/build/\(label)/debug/coreLib.build/coreLib.swift.o",
"/path/to/build/\(label)/debug/exe.build/main.swift.o",
"/path/to/build/\(label)/debug/objectLib.build/objectLib.swift.o",
"/path/to/build/\(label)/debug/staticLib.build/staticLib.swift.o",
"/path/to/build/\(label)/debug/\(dylibPrefix)DLLProduct.\(dylibExtension)",
"/path/to/build/\(label)/debug/exe.product/Objects.LinkFileList",
] + (triple.isMacOSX ? [] : [
// modulewrap
"/path/to/build/\(label)/debug/coreLib.build/coreLib.swiftmodule.o",
"/path/to/build/\(label)/debug/exe.build/exe.swiftmodule.o",
"/path/to/build/\(label)/debug/objectLib.build/objectLib.swiftmodule.o",
"/path/to/build/\(label)/debug/staticLib.build/staticLib.swiftmodule.o",
]),
"C.DLLProduct-\(label)-debug.dylib": [
"/path/to/build/\(label)/debug/coreLib.build/coreLib.swift.o",
"/path/to/build/\(label)/debug/dllLib.build\(dynamic)/dllLib.swift.o",
"/path/to/build/\(label)/debug/DLLProduct.product/Objects.LinkFileList",
] + (triple.isMacOSX ? [] : [
"/path/to/build/\(label)/debug/coreLib.build/coreLib.swiftmodule.o",
"/path/to/build/\(label)/debug/dllLib.build/dllLib.swiftmodule.o",
])
]

let plan = try await BuildPlan(
destinationBuildParameters: mockBuildParameters(
destination: .target,
triple: triple
),
toolsBuildParameters: mockBuildParameters(
destination: .host,
triple: triple
),
graph: graph,
fileSystem: fs,
observabilityScope: observability.topScope
)

let llbuild = LLBuildManifestBuilder(
plan,
fileSystem: fs,
observabilityScope: observability.topScope
)
try llbuild.generateManifest(at: "/manifest")

for (name, inputNames) in tools {
let command = try XCTUnwrap(llbuild.manifest.commands[name])
XCTAssertEqual(Set(command.tool.inputs), Set(inputNames.map({ Node.file(.init($0)) })))
}
}

func testWindows() async throws {
try await doTest(triple: .x86_64Windows)
}

// Make sure we didn't mess up macOS
func testMacOS() async throws {
try await doTest(triple: .x86_64MacOS)
}

// Make sure we didn't mess up linux
func testLinux() async throws {
try await doTest(triple: .x86_64Linux)
}
}