diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5167181e9..6bb73c700 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -38,3 +38,32 @@ jobs: release-builds: name: Release builds uses: apple/swift-nio/.github/workflows/release_builds.yml@main + + construct-linkage-test-matrix: + name: Construct linkage matrix + runs-on: ubuntu-latest + outputs: + linkage-test-matrix: '${{ steps.generate-matrix.outputs.linkage-test-matrix }}' + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: false + - id: generate-matrix + run: echo "linkage-test-matrix=$(curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT" + env: + MATRIX_LINUX_SETUP_COMMAND: apt-get update -y && apt-get install -yq jq && git config --global --add safe.directory /async-http-client + MATRIX_LINUX_COMMAND: ./scripts/run-linkage-test.sh + MATRIX_LINUX_5_10_ENABLED: false + MATRIX_LINUX_6_0_ENABLED: false + MATRIX_LINUX_6_1_ENABLED: false + MATRIX_LINUX_NIGHTLY_NEXT_ENABLED: false + MATRIX_LINUX_NIGHTLY_MAIN_ENABLED: false + + linkage-test: + name: Linkage test + needs: construct-linkage-test-matrix + uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main + with: + name: "Linkage test" + matrix_string: '${{ needs.construct-linkage-test-matrix.outputs.linkage-test-matrix }}' diff --git a/Package.swift b/Package.swift index 9a7ee856e..14f4c412a 100644 --- a/Package.swift +++ b/Package.swift @@ -44,7 +44,8 @@ let package = Package( .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.3.0"), - .package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0"), + // Disable all traits to prevent linking Foundation + .package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0", traits: []), .package(url: "https://github.com/apple/swift-service-context.git", from: "1.1.0"), ], targets: [ @@ -68,7 +69,11 @@ let package = Package( .product(name: "NIOSSL", package: "swift-nio-ssl"), .product(name: "NIOHTTPCompression", package: "swift-nio-extras"), .product(name: "NIOSOCKS", package: "swift-nio-extras"), - .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), + .product( + name: "NIOTransportServices", + package: "swift-nio-transport-services", + condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS, .macCatalyst, .visionOS]) + ), .product(name: "Atomics", package: "swift-atomics"), .product(name: "Algorithms", package: "swift-algorithms"), .product(name: "Configuration", package: "swift-configuration"), diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift index b77cb9527..a9484394c 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift @@ -17,7 +17,11 @@ import NIOCore import NIOHTTP1 import Tracing +#if canImport(FoundationEssentials) +import struct FoundationEssentials.URL +#else import struct Foundation.URL +#endif @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClient { diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift index 03cd8e464..dd39d8315 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift @@ -18,7 +18,11 @@ import NIOHTTP1 import NIOSSL import ServiceContextModule +#if canImport(FoundationEssentials) +import struct FoundationEssentials.URL +#else import struct Foundation.URL +#endif @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientRequest { diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+auth.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+auth.swift index 106a8f76b..ca9aba356 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+auth.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+auth.swift @@ -12,7 +12,11 @@ // //===----------------------------------------------------------------------===// +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientRequest { diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift index 36c1cb36f..13c08f19f 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift @@ -15,7 +15,11 @@ import NIOCore import NIOHTTP1 +#if canImport(FoundationEssentials) +import struct FoundationEssentials.URL +#else import struct Foundation.URL +#endif /// A representation of an HTTP response for the Swift Concurrency HTTPClient API. /// diff --git a/Sources/AsyncHTTPClient/BasicAuth.swift b/Sources/AsyncHTTPClient/BasicAuth.swift index 3e69f8277..2a0260eb7 100644 --- a/Sources/AsyncHTTPClient/BasicAuth.swift +++ b/Sources/AsyncHTTPClient/BasicAuth.swift @@ -12,9 +12,14 @@ // //===----------------------------------------------------------------------===// -import Foundation import NIOHTTP1 +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + /// Generates base64 encoded username + password for http basic auth. /// /// - Parameters: diff --git a/Sources/AsyncHTTPClient/DeconstructedURL.swift b/Sources/AsyncHTTPClient/DeconstructedURL.swift index f7d0b1977..96a16d87d 100644 --- a/Sources/AsyncHTTPClient/DeconstructedURL.swift +++ b/Sources/AsyncHTTPClient/DeconstructedURL.swift @@ -12,7 +12,11 @@ // //===----------------------------------------------------------------------===// +#if canImport(FoundationEssentials) +import struct FoundationEssentials.URL +#else import struct Foundation.URL +#endif struct DeconstructedURL { var scheme: Scheme diff --git a/Sources/AsyncHTTPClient/FileDownloadDelegate.swift b/Sources/AsyncHTTPClient/FileDownloadDelegate.swift index 33a4d3cb2..4076d9559 100644 --- a/Sources/AsyncHTTPClient/FileDownloadDelegate.swift +++ b/Sources/AsyncHTTPClient/FileDownloadDelegate.swift @@ -17,7 +17,11 @@ import NIOCore import NIOHTTP1 import NIOPosix +#if canImport(FoundationEssentials) +import struct FoundationEssentials.URL +#else import struct Foundation.URL +#endif /// Handles a streaming download to a given file path, allowing headers and progress to be reported. public final class FileDownloadDelegate: HTTPClientResponseDelegate { diff --git a/Sources/AsyncHTTPClient/FoundationExtensions.swift b/Sources/AsyncHTTPClient/FoundationExtensions.swift index 452cb7b13..adcd39166 100644 --- a/Sources/AsyncHTTPClient/FoundationExtensions.swift +++ b/Sources/AsyncHTTPClient/FoundationExtensions.swift @@ -15,7 +15,11 @@ // Extensions which provide better ergonomics when using Foundation types, // or by using Foundation APIs. +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif extension HTTPClient.Cookie { /// The cookie's expiration date. @@ -73,3 +77,75 @@ extension HTTPClient.Body { self.bytes(data) } } + +extension StringProtocol { + func addingPercentEncodingAllowingURLHost() -> String { + guard !self.isEmpty else { return String(self) } + + let percent = UInt8(ascii: "%") + let utf8Buffer = self.utf8 + let maxLength = utf8Buffer.count * 3 + return withUnsafeTemporaryAllocation(of: UInt8.self, capacity: maxLength) { outputBuffer in + var i = 0 + for byte in utf8Buffer { + if byte.isURLHostAllowed { + outputBuffer[i] = byte + i += 1 + } else { + outputBuffer[i] = percent + outputBuffer[i + 1] = hexToAscii(byte >> 4) + outputBuffer[i + 2] = hexToAscii(byte & 0xF) + i += 3 + } + } + return String(decoding: outputBuffer[.. UInt8? { + switch ascii { + case UInt8(ascii: "0")...UInt8(ascii: "9"): return ascii - UInt8(ascii: "0") + case UInt8(ascii: "A")...UInt8(ascii: "F"): return ascii - UInt8(ascii: "A") + 10 + case UInt8(ascii: "a")...UInt8(ascii: "f"): return ascii - UInt8(ascii: "a") + 10 + default: return nil + } +} + +private func hexToAscii(_ hex: UInt8) -> UInt8 { + switch hex { + case 0x0: return UInt8(ascii: "0") + case 0x1: return UInt8(ascii: "1") + case 0x2: return UInt8(ascii: "2") + case 0x3: return UInt8(ascii: "3") + case 0x4: return UInt8(ascii: "4") + case 0x5: return UInt8(ascii: "5") + case 0x6: return UInt8(ascii: "6") + case 0x7: return UInt8(ascii: "7") + case 0x8: return UInt8(ascii: "8") + case 0x9: return UInt8(ascii: "9") + case 0xA: return UInt8(ascii: "A") + case 0xB: return UInt8(ascii: "B") + case 0xC: return UInt8(ascii: "C") + case 0xD: return UInt8(ascii: "D") + case 0xE: return UInt8(ascii: "E") + case 0xF: return UInt8(ascii: "F") + default: fatalError("Invalid hex digit: \(hex)") + } +} + +extension UInt8 { + fileprivate var isURLHostAllowed: Bool { + switch self { + case UInt8(ascii: "0")...UInt8(ascii: "9"), + UInt8(ascii: "A")...UInt8(ascii: "Z"), + UInt8(ascii: "a")...UInt8(ascii: "z"), + UInt8(ascii: "!"), UInt8(ascii: "$"), UInt8(ascii: "&"), UInt8(ascii: "'"), + UInt8(ascii: "("), UInt8(ascii: ")"), UInt8(ascii: "*"), UInt8(ascii: "+"), + UInt8(ascii: ","), UInt8(ascii: "-"), UInt8(ascii: "."), UInt8(ascii: ";"), + UInt8(ascii: "="), UInt8(ascii: "_"), UInt8(ascii: "~"): + return true + default: return false + } + } +} diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 1aa37fb7e..56211e631 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// import Atomics -import Foundation +import Dispatch import Logging import NIOConcurrencyHelpers import NIOCore @@ -22,9 +22,18 @@ import NIOHTTPCompression import NIOPosix import NIOSSL import NIOTLS -import NIOTransportServices import Tracing +#if canImport(Network) +import NIOTransportServices +#endif + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + extension Logger { private func requestInfo(_ request: HTTPClient.Request) -> Logger.Metadata.Value { "\(request.method) \(request.url)" diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 9c7becb0b..a34d5de77 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -13,7 +13,6 @@ //===----------------------------------------------------------------------===// import Algorithms -import Foundation import Logging import NIOConcurrencyHelpers import NIOCore @@ -22,6 +21,12 @@ import NIOPosix import NIOSSL import Tracing +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + extension HTTPClient { /// A request body. public struct Body: Sendable { @@ -880,7 +885,7 @@ extension URL { /// - socketPath: The path to the unix domain socket to connect to. /// - uri: The URI path and query that will be sent to the server. public init?(httpURLWithSocketPath socketPath: String, uri: String = "/") { - guard let host = socketPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return nil } + guard let host = Self.percentEncodedHost(from: socketPath) else { return nil } var urlString: String if uri.hasPrefix("/") { urlString = "http+unix://\(host)\(uri)" @@ -895,7 +900,7 @@ extension URL { /// - socketPath: The path to the unix domain socket to connect to. /// - uri: The URI path and query that will be sent to the server. public init?(httpsURLWithSocketPath socketPath: String, uri: String = "/") { - guard let host = socketPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return nil } + guard let host = Self.percentEncodedHost(from: socketPath) else { return nil } var urlString: String if uri.hasPrefix("/") { urlString = "https+unix://\(host)\(uri)" @@ -904,6 +909,14 @@ extension URL { } self.init(string: urlString) } + + private static func percentEncodedHost(from socketPath: String) -> String? { + #if canImport(FoundationEssentials) + socketPath.addingPercentEncodingAllowingURLHost() + #else + socketPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) + #endif + } } protocol HTTPClientTaskDelegate: Sendable { diff --git a/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift b/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift index 148b4a4c4..704329b89 100644 --- a/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift +++ b/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift @@ -14,10 +14,10 @@ import NIOCore import NIOHTTP1 -import NIOTransportServices #if canImport(Network) import Network +import NIOTransportServices #endif extension HTTPClient { diff --git a/Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift b/Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift index e8278e095..ad3e65074 100644 --- a/Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift +++ b/Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift @@ -14,7 +14,12 @@ #if canImport(Network) +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif +import Dispatch import Network import NIOCore import NIOSSL diff --git a/Sources/AsyncHTTPClient/RedirectState.swift b/Sources/AsyncHTTPClient/RedirectState.swift index 4e14712d6..9665c03d4 100644 --- a/Sources/AsyncHTTPClient/RedirectState.swift +++ b/Sources/AsyncHTTPClient/RedirectState.swift @@ -14,7 +14,11 @@ import NIOHTTP1 +#if canImport(FoundationEssentials) +import struct FoundationEssentials.URL +#else import struct Foundation.URL +#endif typealias RedirectMode = HTTPClient.Configuration.RedirectConfiguration.Mode diff --git a/Sources/AsyncHTTPClient/RequestBag+StateMachine.swift b/Sources/AsyncHTTPClient/RequestBag+StateMachine.swift index 7accdc51a..8f5cd37ce 100644 --- a/Sources/AsyncHTTPClient/RequestBag+StateMachine.swift +++ b/Sources/AsyncHTTPClient/RequestBag+StateMachine.swift @@ -15,7 +15,11 @@ import NIOCore import NIOHTTP1 +#if canImport(FoundationEssentials) +import struct FoundationEssentials.URL +#else import struct Foundation.URL +#endif extension HTTPClient { /// The maximum body size allowed, before a redirect response is cancelled. 3KB. diff --git a/Tests/AsyncHTTPClientTests/ConnectionPoolSizeConfigValueIsRespectedTests.swift b/Tests/AsyncHTTPClientTests/ConnectionPoolSizeConfigValueIsRespectedTests.swift index 962791334..1bdaba2e7 100644 --- a/Tests/AsyncHTTPClientTests/ConnectionPoolSizeConfigValueIsRespectedTests.swift +++ b/Tests/AsyncHTTPClientTests/ConnectionPoolSizeConfigValueIsRespectedTests.swift @@ -23,11 +23,11 @@ import NIOHTTPCompression import NIOPosix import NIOSSL import NIOTestUtils -import NIOTransportServices import XCTest #if canImport(Network) import Network +import NIOTransportServices #endif final class ConnectionPoolSizeConfigValueIsRespectedTests: XCTestCaseHTTPClientTestsBaseClass { diff --git a/Tests/AsyncHTTPClientTests/FoundationExtensionTests.swift b/Tests/AsyncHTTPClientTests/FoundationExtensionTests.swift new file mode 100644 index 000000000..1d1b04d8f --- /dev/null +++ b/Tests/AsyncHTTPClientTests/FoundationExtensionTests.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2026 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import XCTest + +@testable import AsyncHTTPClient + +final class FoundationExtensionTests: XCTestCaseHTTPClientTestsBaseClass { + func testAddingPercentEncodingAllowingURLHost() { + let testCases = [ + "localhost", // Alphanumerics (No encoding needed) + "example.com", // Domain with unreserved dot (No encoding needed) + "user@email.com", // '@' is not allowed in host (Should be encoded to %40) + "!$&'()*+,;=[]", // Sub-delimiters and brackets (Allowed in host, NO encoding) + "~_.-", // Unreserved punctuation (Allowed in host, NO encoding) + "café", // Non-ASCII character (Should be encoded) + "👨‍💻 swift", // Emoji and space (Space to %20, Emoji encoded) + "", // Empty string + "100% coverage", // '%' symbol itself (Must be encoded to %25) + "sub.domain_test~1.com", // Mix of allowed characters + "path/to/api?query=1#frag", // Invalid host chars like '/', '?', and '#' (Should be encoded), '=' is a valid sub-delimiter + ] + + for input in testCases { + let foundationResult = input.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) + let customResult = input.addingPercentEncodingAllowingURLHost() + + XCTAssertEqual( + customResult, + foundationResult, + "Encoding mismatch for input: '\(input)'. Expected '\(String(describing: foundationResult))', got '\(String(describing: customResult))'" + ) + } + } +} diff --git a/Tests/AsyncHTTPClientTests/HTTPClientBase.swift b/Tests/AsyncHTTPClientTests/HTTPClientBase.swift index 90ab12fe5..cca04bfd9 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientBase.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientBase.swift @@ -24,11 +24,11 @@ import NIOHTTPCompression import NIOPosix import NIOSSL import NIOTestUtils -import NIOTransportServices import XCTest #if canImport(Network) import Network +import NIOTransportServices #endif class XCTestCaseHTTPClientTestsBaseClass: XCTestCase { diff --git a/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift index 4c2d24dc4..eb43e8cd9 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift @@ -16,13 +16,13 @@ import NIOConcurrencyHelpers import NIOCore import NIOPosix import NIOSSL -import NIOTransportServices import XCTest @testable import AsyncHTTPClient #if canImport(Network) import Network +import NIOTransportServices #endif class HTTPClientNIOTSTests: XCTestCase { diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift index 2dcce3e12..a7cf91c57 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift @@ -27,11 +27,15 @@ import NIOHTTPCompression import NIOPosix import NIOSSL import NIOTLS -import NIOTransportServices import XCTest @testable import AsyncHTTPClient +#if canImport(Network) +import Network +import NIOTransportServices +#endif + #if canImport(xlocale) import xlocale #elseif canImport(locale_h) diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index 959e0f939..24bbafdb6 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -26,11 +26,11 @@ import NIOHTTPCompression import NIOPosix import NIOSSL import NIOTestUtils -import NIOTransportServices import XCTest #if canImport(Network) import Network +import NIOTransportServices #endif final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass { diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTracingInternalTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTracingInternalTests.swift index 53f1138ba..3b9e86c5b 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTracingInternalTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTracingInternalTests.swift @@ -24,7 +24,6 @@ import NIOHTTPCompression import NIOPosix import NIOSSL import NIOTestUtils -import NIOTransportServices import Tracing import XCTest @@ -32,6 +31,7 @@ import XCTest #if canImport(Network) import Network +import NIOTransportServices #endif private func makeTracedHTTPClient(tracer: InMemoryTracer) -> HTTPClient { diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift index 047c66e6d..7e8d09d1e 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift @@ -25,12 +25,12 @@ import NIOHTTPCompression import NIOPosix import NIOSSL import NIOTestUtils -import NIOTransportServices import Tracing import XCTest #if canImport(Network) import Network +import NIOTransportServices #endif private func makeTracedHTTPClient(tracer: InMemoryTracer) -> HTTPClient { diff --git a/Tests/AsyncHTTPClientTests/IdleTimeoutNoReuseTests.swift b/Tests/AsyncHTTPClientTests/IdleTimeoutNoReuseTests.swift index e9a0d46dc..bc44012ed 100644 --- a/Tests/AsyncHTTPClientTests/IdleTimeoutNoReuseTests.swift +++ b/Tests/AsyncHTTPClientTests/IdleTimeoutNoReuseTests.swift @@ -23,11 +23,11 @@ import NIOHTTPCompression import NIOPosix import NIOSSL import NIOTestUtils -import NIOTransportServices import XCTest #if canImport(Network) import Network +import NIOTransportServices #endif final class TestIdleTimeoutNoReuse: XCTestCaseHTTPClientTestsBaseClass { diff --git a/Tests/AsyncHTTPClientTests/NoBytesSentOverBodyLimitTests.swift b/Tests/AsyncHTTPClientTests/NoBytesSentOverBodyLimitTests.swift index 026a45d4c..32e8e5f4a 100644 --- a/Tests/AsyncHTTPClientTests/NoBytesSentOverBodyLimitTests.swift +++ b/Tests/AsyncHTTPClientTests/NoBytesSentOverBodyLimitTests.swift @@ -23,11 +23,11 @@ import NIOHTTPCompression import NIOPosix import NIOSSL import NIOTestUtils -import NIOTransportServices import XCTest #if canImport(Network) import Network +import NIOTransportServices #endif final class NoBytesSentOverBodyLimitTests: XCTestCaseHTTPClientTestsBaseClass { diff --git a/Tests/AsyncHTTPClientTests/RacePoolIdleConnectionsAndGetTests.swift b/Tests/AsyncHTTPClientTests/RacePoolIdleConnectionsAndGetTests.swift index 35a09c421..43c348113 100644 --- a/Tests/AsyncHTTPClientTests/RacePoolIdleConnectionsAndGetTests.swift +++ b/Tests/AsyncHTTPClientTests/RacePoolIdleConnectionsAndGetTests.swift @@ -23,11 +23,11 @@ import NIOHTTPCompression import NIOPosix import NIOSSL import NIOTestUtils -import NIOTransportServices import XCTest #if canImport(Network) import Network +import NIOTransportServices #endif final class RacePoolIdleConnectionsAndGetTests: XCTestCaseHTTPClientTestsBaseClass { diff --git a/Tests/AsyncHTTPClientTests/ResponseDelayGetTests.swift b/Tests/AsyncHTTPClientTests/ResponseDelayGetTests.swift index 5fd1d6720..7874a185c 100644 --- a/Tests/AsyncHTTPClientTests/ResponseDelayGetTests.swift +++ b/Tests/AsyncHTTPClientTests/ResponseDelayGetTests.swift @@ -23,11 +23,11 @@ import NIOHTTPCompression import NIOPosix import NIOSSL import NIOTestUtils -import NIOTransportServices import XCTest #if canImport(Network) import Network +import NIOTransportServices #endif final class ResponseDelayGetTests: XCTestCaseHTTPClientTestsBaseClass { diff --git a/Tests/AsyncHTTPClientTests/StressGetHttpsTests.swift b/Tests/AsyncHTTPClientTests/StressGetHttpsTests.swift index 587e6c64c..d5b77556c 100644 --- a/Tests/AsyncHTTPClientTests/StressGetHttpsTests.swift +++ b/Tests/AsyncHTTPClientTests/StressGetHttpsTests.swift @@ -23,11 +23,11 @@ import NIOHTTPCompression import NIOPosix import NIOSSL import NIOTestUtils -import NIOTransportServices import XCTest #if canImport(Network) import Network +import NIOTransportServices #endif final class StressGetHttpsTests: XCTestCaseHTTPClientTestsBaseClass { diff --git a/Tests/LinkageTest/.gitignore b/Tests/LinkageTest/.gitignore new file mode 100644 index 000000000..55ad514a0 --- /dev/null +++ b/Tests/LinkageTest/.gitignore @@ -0,0 +1,2 @@ +.build +Package.resolved \ No newline at end of file diff --git a/Tests/LinkageTest/Package.swift b/Tests/LinkageTest/Package.swift new file mode 100644 index 000000000..c207514f9 --- /dev/null +++ b/Tests/LinkageTest/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version: 6.2 + +import PackageDescription + +let package = Package( + name: "linkage-test", + dependencies: [ + .package(name: "async-http-client", path: "../..") + ], + targets: [ + .executableTarget( + name: "linkageTest", + dependencies: [ + .product(name: "AsyncHTTPClient", package: "async-http-client") + ] + ) + ] +) diff --git a/Tests/LinkageTest/Sources/linkageTest/main.swift b/Tests/LinkageTest/Sources/linkageTest/main.swift new file mode 100644 index 000000000..af48b95a6 --- /dev/null +++ b/Tests/LinkageTest/Sources/linkageTest/main.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2026 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AsyncHTTPClient + +print("\(HTTPClient.shared)") diff --git a/scripts/run-linkage-test.sh b/scripts/run-linkage-test.sh new file mode 100755 index 000000000..1686adc48 --- /dev/null +++ b/scripts/run-linkage-test.sh @@ -0,0 +1,51 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the AsyncHTTPClient open source project +## +## Copyright (c) 2026 Apple Inc. and the AsyncHTTPClient project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## +set -eu + +# Validate that we're running on Linux +if [[ "$(uname -s)" != "Linux" ]]; then + echo "Error: This script must be run on Linux. Current OS: $(uname -s)" >&2 + exit 1 +fi + +echo "Running on Linux - proceeding with linkage test..." + +# Build the linkage test package +echo "Building linkage test package..." +swift build --package-path Tests/LinkageTest + +# Construct build path +build_path=$(swift build --package-path Tests/LinkageTest --show-bin-path) +binary_path=$build_path/linkageTest + +# Verify the binary exists +if [[ ! -f "$binary_path" ]]; then + echo "Error: Built binary not found at $binary_path" >&2 + exit 1 +fi + +echo "Checking linkage for binary: $binary_path" + +# Run ldd and check if libFoundation.so is linked +ldd_output=$(ldd "$binary_path") +echo "LDD output:" +echo "$ldd_output" + +if echo "$ldd_output" | grep -q "libFoundation.so"; then + echo "Error: Binary is linked against libFoundation.so - this indicates incorrect linkage. Ensure the full Foundation is not linked on Linux when default traits are disabled." >&2 + exit 1 +else + echo "Success: Binary is not linked against libFoundation.so - linkage test passed." +fi \ No newline at end of file