From dc2fed2287316e987cd92ee19d4f9eb4596ae04f Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Wed, 16 Apr 2025 18:09:55 -0700 Subject: [PATCH] Add support for testable executables on Windows This uses the /ALTERNATENAME flag to link.exe, which is roughly equivalent to -alias/--defsym. This has been verified to work in a sample project. See https://devblogs.microsoft.com/oldnewthing/20200731-00/ for more info. Closes #6367 --- Sources/Build/BuildPlan/BuildPlan.swift | 20 ++++++++++++++++++++ Tests/BuildTests/BuildPlanTests.swift | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Sources/Build/BuildPlan/BuildPlan.swift b/Sources/Build/BuildPlan/BuildPlan.swift index 01ffe8ec665..d2c27d849cf 100644 --- a/Sources/Build/BuildPlan/BuildPlan.swift +++ b/Sources/Build/BuildPlan/BuildPlan.swift @@ -151,12 +151,32 @@ extension BuildParameters { args = ["-alias", "_\(target.c99name)_main", "_main"] case .elf: args = ["--defsym", "main=\(target.c99name)_main"] + case .coff: + // If the user is specifying a custom entry point name that isn't "main", assume they may be setting WinMain or wWinMain + // and don't do any modifications ourselves. In that case the linker will infer the WINDOWS subsystem and call WinMainCRTStartup, + // which will then call the custom entry point. And WinMain/wWinMain != main, so this still won't run into duplicate symbol + // issues when called from a test target, which always uses main. + if let customEntryPointFunctionName = findCustomEntryPointFunctionName(of: target), customEntryPointFunctionName != "main" { + return nil + } + args = ["/ALTERNATENAME:main=\(target.c99name)_main", "/SUBSYSTEM:CONSOLE"] default: return nil } return args.asSwiftcLinkerFlags() } + private func findCustomEntryPointFunctionName(of target: ResolvedModule) -> String? { + let flags = createScope(for: target).evaluate(.OTHER_SWIFT_FLAGS) + var it = flags.makeIterator() + while let value = it.next() { + if value == "-Xfrontend" && it.next() == "-entry-point-function-name" && it.next() == "-Xfrontend" { + return it.next() + } + } + return nil + } + /// Returns the scoped view of build settings for a given target. func createScope(for target: ResolvedModule) -> BuildSettings.Scope { BuildSettings.Scope(target.underlying.buildSettings, environment: buildEnvironment) diff --git a/Tests/BuildTests/BuildPlanTests.swift b/Tests/BuildTests/BuildPlanTests.swift index 6d8741e7ad4..65fc0b5b206 100644 --- a/Tests/BuildTests/BuildPlanTests.swift +++ b/Tests/BuildTests/BuildPlanTests.swift @@ -3875,7 +3875,7 @@ class BuildPlanTestCase: BuildSystemProviderTestCase { observabilityScope: observability.topScope )) } - let supportingTriples: [Basics.Triple] = [.x86_64Linux, .x86_64MacOS] + let supportingTriples: [Basics.Triple] = [.x86_64Linux, .x86_64MacOS, .x86_64Windows] for triple in supportingTriples { let result = try await createResult(for: triple) let exe = try result.moduleBuildDescription(for: "exe").swift().compileArguments() @@ -3884,7 +3884,7 @@ class BuildPlanTestCase: BuildSystemProviderTestCase { XCTAssertMatch(linkExe, [.contains("exe_main")]) } - let unsupportingTriples: [Basics.Triple] = [.wasi, .windows] + let unsupportingTriples: [Basics.Triple] = [.wasi] for triple in unsupportingTriples { let result = try await createResult(for: triple) let exe = try result.moduleBuildDescription(for: "exe").swift().compileArguments()