Skip to content
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
23 changes: 23 additions & 0 deletions Tests/SwiftDriverTests/Helpers/DriverTestHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions Tests/SwiftDriverTests/Helpers/TestBuildConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
29 changes: 29 additions & 0 deletions Tests/SwiftDriverTests/Helpers/TestBuildConfigTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
162 changes: 87 additions & 75 deletions Tests/SwiftDriverTests/LinkJobTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")

Expand Down
68 changes: 29 additions & 39 deletions Tests/SwiftDriverTests/ToolchainTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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")))
}
}
}
Expand Down
Loading