diff --git a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift index de958a7d370..3e6dcd88ca4 100644 --- a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift +++ b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift @@ -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 } /// The path to the swiftmodule file after compilation. @@ -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, @@ -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() } @@ -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 @@ -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] diff --git a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift index 11668bbe1b3..73a96bb4140 100644 --- a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift +++ b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift @@ -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) @@ -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) } @@ -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 + } } } diff --git a/Sources/Build/BuildPlan/BuildPlan+Product.swift b/Sources/Build/BuildPlan/BuildPlan+Product.swift index 10fa8c6bc3b..c78da860236 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Product.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Product.swift @@ -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 } diff --git a/Sources/Build/BuildPlan/BuildPlan.swift b/Sources/Build/BuildPlan/BuildPlan.swift index 23b83f89465..17f382ec366 100644 --- a/Sources/Build/BuildPlan/BuildPlan.swift +++ b/Sources/Build/BuildPlan/BuildPlan.swift @@ -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) } diff --git a/Sources/_InternalTestSupport/MockBuildTestHelper.swift b/Sources/_InternalTestSupport/MockBuildTestHelper.swift index ef9bdf7ce55..339d56a502d 100644 --- a/Sources/_InternalTestSupport/MockBuildTestHelper.swift +++ b/Sources/_InternalTestSupport/MockBuildTestHelper.swift @@ -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") } diff --git a/Tests/BuildTests/WindowsBuildPlanTests.swift b/Tests/BuildTests/WindowsBuildPlanTests.swift new file mode 100644 index 00000000000..4672091d051 --- /dev/null +++ b/Tests/BuildTests/WindowsBuildPlanTests.swift @@ -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) + } +}