From b3fd07e815909cf526f058bcc2f68a687a7ffc81 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 18 May 2026 21:37:42 +0100 Subject: [PATCH] Exclude Emscripten tests with older toolchains This allows tests to pass with older versions of `swift-frontend` that don't support Emscripten triples. --- .../Helpers/DriverTestHelpers.swift | 23 +++ .../Helpers/TestBuildConfig.swift | 40 +++++ .../Helpers/TestBuildConfigTests.swift | 29 ++++ Tests/SwiftDriverTests/LinkJobTests.swift | 162 ++++++++++-------- Tests/SwiftDriverTests/ToolchainTests.swift | 68 ++++---- 5 files changed, 208 insertions(+), 114 deletions(-) create mode 100644 Tests/SwiftDriverTests/Helpers/TestBuildConfigTests.swift diff --git a/Tests/SwiftDriverTests/Helpers/DriverTestHelpers.swift b/Tests/SwiftDriverTests/Helpers/DriverTestHelpers.swift index a8c77f37f..f942d79eb 100644 --- a/Tests/SwiftDriverTests/Helpers/DriverTestHelpers.swift +++ b/Tests/SwiftDriverTests/Helpers/DriverTestHelpers.swift @@ -150,6 +150,29 @@ func makeLdStub() throws -> AbsolutePath { } } +/// Writes a tiny PE-binary `clang` executable into the given directory. +/// Used by tests that exercise `-tools-directory` lookup and need a discoverable +/// binary on disk; the contents are intentionally minimal, just enough that the +/// host OS treats the file as executable on Windows runners. +func makeClangStub(in tmpDir: AbsolutePath) throws -> AbsolutePath { + let clang = tmpDir.appending(component: executableName("clang")) + // tiny PE binary from: https://archive.is/w01DO + let contents: ByteString = [ + 0x4d, 0x5a, 0x00, 0x00, 0x50, 0x45, 0x00, 0x00, 0x4c, 0x01, 0x01, 0x00, + 0x6a, 0x2a, 0x58, 0xc3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x03, 0x01, 0x0b, 0x01, 0x08, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x68, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, + ] + try localFileSystem.writeFileContents(clang, bytes: contents) + try localFileSystem.chmod(.executable, path: try AbsolutePath(validating: clang.pathString)) + return clang +} + // MARK: - Job.ArgTemplate Extensions extension Array where Element == Job.ArgTemplate { diff --git a/Tests/SwiftDriverTests/Helpers/TestBuildConfig.swift b/Tests/SwiftDriverTests/Helpers/TestBuildConfig.swift index 9009cf600..cceb953ec 100644 --- a/Tests/SwiftDriverTests/Helpers/TestBuildConfig.swift +++ b/Tests/SwiftDriverTests/Helpers/TestBuildConfig.swift @@ -125,6 +125,29 @@ extension Trait where Self == Testing.ConditionTrait { ) } + /// Requires that the Swift frontend recognizes the given target triple. + /// + /// Skips the test when the frontend rejects the triple (e.g. older toolchains + /// that predate a target-OS addition). Falls open and runs the test when the + /// frontend can't even handle a baseline invocation, so a broken toolchain + /// surfaces a real failure instead of silently green-skipping. + package static func requireFrontendSupportsTarget( + _ targetTriple: String, + _ comment: Comment? = nil + ) -> Self { + let supported: Bool + switch targetTriple { + case "wasm32-unknown-emscripten": + supported = frontendSupportsEmscripten + default: + supported = probeFrontendForTarget(targetTriple) + } + return enabled( + if: supported, + comment ?? "Frontend does not support target '\(targetTriple)'" + ) + } + /// Requires that libSwiftScan supports link library reporting. package static func requireScannerSupportsLinkLibraries(_ comment: Comment? = nil) -> Self { let supported = (try? _scannerOracle?.supportsLinkLibraries()) ?? false @@ -231,3 +254,20 @@ let cachingFeatureSupported: Bool = { guard let driver = try? TestDriver(args: ["swiftc"]) else { return false } return driver.isFeatureSupported(.compilation_caching) }() + +/// Probe `swift-frontend -print-target-info` for whether it accepts a target triple. +/// +/// Returns `true` when the frontend handles the triple, `false` when it rejects +/// it specifically. If the frontend can't construct a baseline driver at all, +/// returns `true` ("fail open") so a broken toolchain causes a loud test failure +/// rather than a silent skip. +package func probeFrontendForTarget(_ targetTriple: String) -> Bool { + // Baseline: can the frontend handle a trivial invocation at all? + let baselineWorks = (try? TestDriver(args: ["swiftc", "test.swift"])) != nil + guard baselineWorks else { return true } + return (try? TestDriver(args: ["swiftc", "-target", targetTriple, "test.swift"])) != nil +} + +/// Cached probe result for `wasm32-unknown-emscripten`. Evaluated once per +/// process at module load (matches `cachingFeatureSupported`). +private let frontendSupportsEmscripten: Bool = probeFrontendForTarget("wasm32-unknown-emscripten") diff --git a/Tests/SwiftDriverTests/Helpers/TestBuildConfigTests.swift b/Tests/SwiftDriverTests/Helpers/TestBuildConfigTests.swift new file mode 100644 index 000000000..e54bac00e --- /dev/null +++ b/Tests/SwiftDriverTests/Helpers/TestBuildConfigTests.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@Suite struct RequireFrontendSupportsTargetTests { + /// `wasm32-unknown-wasi` is recognized by every toolchain that ships swift-frontend + /// (it predates emscripten support), so the probe must accept it. + @Test func probeAcceptsKnownTriple() async throws { + #expect(probeFrontendForTarget("wasm32-unknown-wasi") == true) + } + + /// A nonsense OS is never recognized, so the probe must reject it (causing the trait + /// to skip rather than run). + @Test func probeRejectsBogusTriple() async throws { + #expect(probeFrontendForTarget("madeup-unknown-bogusos") == false) + } +} diff --git a/Tests/SwiftDriverTests/LinkJobTests.swift b/Tests/SwiftDriverTests/LinkJobTests.swift index 5a245e699..056af7f6f 100644 --- a/Tests/SwiftDriverTests/LinkJobTests.swift +++ b/Tests/SwiftDriverTests/LinkJobTests.swift @@ -24,11 +24,16 @@ import Testing private var ld: AbsolutePath { get throws { try makeLdStub() } } - @Test func linking() async throws { + private var defaultEnv: ProcessEnvironmentBlock { var env = ProcessEnv.block env["SWIFT_DRIVER_TESTS_ENABLE_EXEC_PATH_FALLBACK"] = "1" env["SWIFT_DRIVER_SWIFT_AUTOLINK_EXTRACT_EXEC"] = "/garbage/swift-autolink-extract" env["SWIFT_DRIVER_DSYMUTIL_EXEC"] = "/garbage/dsymutil" + return env + } + + @Test func linking() async throws { + let env = defaultEnv let commonArgs = ["swiftc", "foo.swift", "bar.swift", "-module-name", "Test"] @@ -757,80 +762,6 @@ import Testing } } - do { - // Emscripten executable linking — uses emcc -s settings instead of -Xlinker - try await withTemporaryDirectory { path in - try localFileSystem.writeFileContents( - path.appending(components: "emscripten", "static-executable-args.lnk") - ) { - $0.send("garbage") - } - var driver = try TestDriver( - args: commonArgs + [ - "-emit-executable", "-Ounchecked", - "-target", "wasm32-unknown-emscripten", - "-Xlinker", "--export=myFunc", - "-Xclang-linker", "-resource-dir", - "-Xclang-linker", "/fake/clang/dir", - "-Xemcc-linker", "-sENVIRONMENT=shell", - "-Xemcc-linker", "-sALLOW_MEMORY_GROWTH", - "-resource-dir", path.pathString, - ], - env: env - ) - let plannedJobs = try await driver.planBuild() - let linkJob = plannedJobs.last! - let cmd = linkJob.commandLine - - // emcc supports -Xlinker for passing flags to wasm-ld - #expect(cmd.contains(subsequence: [.flag("-Xlinker"), .flag("--export=myFunc")])) - // -Xclang-linker is clang-specific — should NOT be forwarded to emcc - #expect(!cmd.contains(.flag("-resource-dir"))) - #expect(!cmd.contains(.flag("/fake/clang/dir"))) - - // -Xemcc-linker flags appear directly in emcc command (not wrapped in -Xlinker) - #expect(cmd.contains(.flag("-sENVIRONMENT=shell"))) - #expect(cmd.contains(.flag("-sALLOW_MEMORY_GROWTH"))) - // They must NOT be preceded by -Xlinker (that would forward them to wasm-ld) - #expect(!cmd.contains(subsequence: [.flag("-Xlinker"), .flag("-sENVIRONMENT=shell")])) - - // Linker flags should use emcc -s settings - #expect(cmd.contains(.flag("-sGLOBAL_BASE=4096"))) - #expect(cmd.contains(.flag("-sTABLE_BASE=4096"))) - #expect(cmd.contains(.flag("-sSTACK_SIZE=\(128 * 1024)"))) - #expect(cmd.contains(.flag("-O3"))) - #expect(try linkJob.outputs[0].file == toPath("Test.js")) - - // emcc manages its own target, sysroot, and system libraries - #expect(!cmd.contains(subsequence: ["-target", "wasm32-unknown-emscripten"])) - #expect(!cmd.contains(.flag("--sysroot"))) - #expect(!cmd.contains(.flag("-ldlmalloc"))) - #expect(!cmd.contains(.flag("-lstandalonewasm"))) - } - } - - do { - // -Xclang-linker should warn for Emscripten targets - try await withTemporaryDirectory { resourceDir in - try localFileSystem.writeFileContents( - resourceDir.appending(components: "emscripten", "static-executable-args.lnk") - ) { $0.send("garbage") } - - try await assertDriverDiagnostics( - args: ["swiftc", "-no-color-diagnostics", - "-target", "wasm32-unknown-emscripten", - "-resource-dir", resourceDir.pathString, - "-Xclang-linker", "-resource-dir", - "-Xclang-linker", "/fake/path", - "foo.swift"], - env: env - ) { driver, verifier in - verifier.expect(.warning("'-Xclang-linker -resource-dir' is not supported for Emscripten targets; use '-Xemcc-linker' to pass flags to emcc")) - verifier.expect(.warning("'-Xclang-linker /fake/path' is not supported for Emscripten targets; use '-Xemcc-linker' to pass flags to emcc")) - } - } - } - do { // -Xemcc-linker should warn for non-Emscripten (WASI) targets try await withTemporaryDirectory { resourceDir in @@ -895,6 +826,87 @@ import Testing } } + @Test(.requireFrontendSupportsTarget("wasm32-unknown-emscripten")) + func emscriptenExecutableLinking() async throws { + let env = defaultEnv + let commonArgs = ["swiftc", "foo.swift", "bar.swift", "-module-name", "Test"] + + // Emscripten executable linking uses emcc -s settings instead of -Xlinker + try await withTemporaryDirectory { path in + try localFileSystem.writeFileContents( + path.appending(components: "emscripten", "static-executable-args.lnk") + ) { + $0.send("garbage") + } + var driver = try TestDriver( + args: commonArgs + [ + "-emit-executable", "-Ounchecked", + "-target", "wasm32-unknown-emscripten", + "-Xlinker", "--export=myFunc", + "-Xclang-linker", "-resource-dir", + "-Xclang-linker", "/fake/clang/dir", + "-Xemcc-linker", "-sENVIRONMENT=shell", + "-Xemcc-linker", "-sALLOW_MEMORY_GROWTH", + "-resource-dir", path.pathString, + ], + env: env + ) + let plannedJobs = try await driver.planBuild() + let linkJob = plannedJobs.last! + let cmd = linkJob.commandLine + + // emcc supports -Xlinker for passing flags to wasm-ld + #expect(cmd.contains(subsequence: [.flag("-Xlinker"), .flag("--export=myFunc")])) + // -Xclang-linker is clang-specific, so it should not be forwarded to emcc + #expect(!cmd.contains(.flag("-resource-dir"))) + #expect(!cmd.contains(.flag("/fake/clang/dir"))) + + // -Xemcc-linker flags appear directly in emcc command (not wrapped in -Xlinker) + #expect(cmd.contains(.flag("-sENVIRONMENT=shell"))) + #expect(cmd.contains(.flag("-sALLOW_MEMORY_GROWTH"))) + // They must NOT be preceded by -Xlinker (that would forward them to wasm-ld) + #expect(!cmd.contains(subsequence: [.flag("-Xlinker"), .flag("-sENVIRONMENT=shell")])) + + // Linker flags should use emcc -s settings + #expect(cmd.contains(.flag("-sGLOBAL_BASE=4096"))) + #expect(cmd.contains(.flag("-sTABLE_BASE=4096"))) + #expect(cmd.contains(.flag("-sSTACK_SIZE=\(128 * 1024)"))) + #expect(cmd.contains(.flag("-O3"))) + #expect(try linkJob.outputs[0].file == toPath("Test.js")) + + // emcc manages its own target, sysroot, and system libraries + #expect(!cmd.contains(subsequence: ["-target", "wasm32-unknown-emscripten"])) + #expect(!cmd.contains(.flag("--sysroot"))) + #expect(!cmd.contains(.flag("-ldlmalloc"))) + #expect(!cmd.contains(.flag("-lstandalonewasm"))) + } + } + + @Test(.requireFrontendSupportsTarget("wasm32-unknown-emscripten")) + func emscriptenXclangLinkerWarning() async throws { + let env = defaultEnv + + // -Xclang-linker should warn for Emscripten targets + try await withTemporaryDirectory { resourceDir in + try localFileSystem.writeFileContents( + resourceDir.appending(components: "emscripten", "static-executable-args.lnk") + ) { $0.send("garbage") } + + try await assertDriverDiagnostics( + args: ["swiftc", "-no-color-diagnostics", + "-target", "wasm32-unknown-emscripten", + "-resource-dir", resourceDir.pathString, + "-Xclang-linker", "-resource-dir", + "-Xclang-linker", "/fake/path", + "foo.swift"], + env: env + ) { driver, verifier in + verifier.expect(.warning("'-Xclang-linker -resource-dir' is not supported for Emscripten targets; use '-Xemcc-linker' to pass flags to emcc")) + verifier.expect(.warning("'-Xclang-linker /fake/path' is not supported for Emscripten targets; use '-Xemcc-linker' to pass flags to emcc")) + } + } + } + @Test func lEqualPassedDownToLinkerInvocation() async throws { let workingDirectory = localFileSystem.currentWorkingDirectory!.appending(components: "Foo", "Bar") diff --git a/Tests/SwiftDriverTests/ToolchainTests.swift b/Tests/SwiftDriverTests/ToolchainTests.swift index a9dff2281..397f1c46d 100644 --- a/Tests/SwiftDriverTests/ToolchainTests.swift +++ b/Tests/SwiftDriverTests/ToolchainTests.swift @@ -123,21 +123,7 @@ import CRT @Test func toolsDirectory() async throws { try await withTemporaryDirectory { tmpDir in - let ld = tmpDir.appending(component: executableName("clang")) - // tiny PE binary from: https://archive.is/w01DO - let contents: ByteString = [ - 0x4d, 0x5a, 0x00, 0x00, 0x50, 0x45, 0x00, 0x00, 0x4c, 0x01, 0x01, 0x00, - 0x6a, 0x2a, 0x58, 0xc3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x03, 0x01, 0x0b, 0x01, 0x08, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x68, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x02, - ] - try localFileSystem.writeFileContents(ld, bytes: contents) - try localFileSystem.chmod(.executable, path: try AbsolutePath(validating: ld.pathString)) + let ld = try makeClangStub(in: tmpDir) // Drop SWIFT_DRIVER_CLANG_EXEC from the environment so it doesn't // interfere with tool lookup. @@ -182,33 +168,37 @@ import CRT expectJobInvocationMatches(frontendJobs[1], .flag("-B"), .path(.absolute(tmpDir))) } } + } + } - // Emscripten toolchain — emcc should NOT get -B - do { - var env = ProcessEnv.block - env["SWIFT_DRIVER_SWIFT_AUTOLINK_EXTRACT_EXEC"] = "//bin/swift-autolink-extract" - env["SWIFT_DRIVER_EMCC_EXEC"] = "//bin/emcc" + @Test(.requireFrontendSupportsTarget("wasm32-unknown-emscripten")) + func emscriptenToolsDirectory() async throws { + try await withTemporaryDirectory { tmpDir in + _ = try makeClangStub(in: tmpDir) - try await withTemporaryDirectory { resourceDir in - try localFileSystem.writeFileContents( - resourceDir.appending(components: "emscripten", "static-executable-args.lnk") - ) { - $0.send("garbage") - } - var driver = try TestDriver( - args: [ - "swiftc", - "-target", "wasm32-unknown-emscripten", - "-resource-dir", resourceDir.pathString, - "-tools-directory", tmpDir.pathString, - "foo.swift", - ], - env: env - ) - let frontendJobs = try await driver.planBuild().removingAutolinkExtractJobs() - let linkJob = frontendJobs.last! - #expect(!linkJob.commandLine.contains(.flag("-B"))) + var env = ProcessEnv.block + env["SWIFT_DRIVER_SWIFT_AUTOLINK_EXTRACT_EXEC"] = "//bin/swift-autolink-extract" + env["SWIFT_DRIVER_EMCC_EXEC"] = "//bin/emcc" + + try await withTemporaryDirectory { resourceDir in + try localFileSystem.writeFileContents( + resourceDir.appending(components: "emscripten", "static-executable-args.lnk") + ) { + $0.send("garbage") } + var driver = try TestDriver( + args: [ + "swiftc", + "-target", "wasm32-unknown-emscripten", + "-resource-dir", resourceDir.pathString, + "-tools-directory", tmpDir.pathString, + "foo.swift", + ], + env: env + ) + let frontendJobs = try await driver.planBuild().removingAutolinkExtractJobs() + let linkJob = frontendJobs.last! + #expect(!linkJob.commandLine.contains(.flag("-B"))) } } }