From 3a93bf21ff455330316950c13a1e65e20ef0e90c Mon Sep 17 00:00:00 2001 From: Alexander Ignition Date: Thu, 17 Feb 2022 12:01:05 +0300 Subject: [PATCH] Linux build (#53) * Update test.yml * Remove import FoundationNetworking * if !os(Linux) * add linux-build.sh * fix Linux tests * Add HTTPRequest * Add Linux build to release workflow --- .github/scripts/linux-build.sh | 17 ++++++++ .github/workflows/release.yml | 16 ++++++++ .github/workflows/test.yml | 17 ++++++++ .../Sources/CatbirdAPI/Catbird.swift | 40 ++++++++++++++++--- .../Sources/CatbirdAPI/CatbirdAction.swift | 36 +++++++++-------- .../Sources/CatbirdAPI/CatbirdError.swift | 16 ++++---- .../CatbirdAPI/URLSessionTask+Wait.swift | 18 --------- .../Tests/CatbirdAPITests/CatbirdTests.swift | 4 ++ .../Tests/CatbirdAPITests/Network.swift | 4 ++ .../Helpers/Application+CatbirdAction.swift | 8 ++-- 10 files changed, 125 insertions(+), 51 deletions(-) create mode 100755 .github/scripts/linux-build.sh delete mode 100644 Packages/CatbirdAPI/Sources/CatbirdAPI/URLSessionTask+Wait.swift diff --git a/.github/scripts/linux-build.sh b/.github/scripts/linux-build.sh new file mode 100755 index 0000000..46d95c8 --- /dev/null +++ b/.github/scripts/linux-build.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# Usage: .github/scripts/linux-build.sh + +set -eo pipefail + +BUILD_OPTIONS=( + --configuration release + --package-path Packages/CatbirdApp + --disable-sandbox + --static-swift-stdlib +) + +SWIFT_BUILD="swift build ${BUILD_OPTIONS[*]}" +$SWIFT_BUILD +BIN_PATH=$($SWIFT_BUILD --show-bin-path) +cp "$BIN_PATH"/catbird ./catbird-linux diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 19fd793..b31c2d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,3 +28,19 @@ jobs: run: gh workflow run bump-formula.yml --repo RedMadRobot/homebrew-formulae --field formula=catbird --field version=${{ github.event.release.tag_name }} env: GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }} + linux-build: + name: Build on Linux + runs-on: ubuntu-20.04 + container: + image: swift:5.5.1-focal + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Build catbird app + id: build + run: .github/scripts/linux-build.sh + shell: bash + - name: Upload GitHub Release Assets + run: gh release upload ${{ github.event.release.tag_name }} catbird-linux + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af80925..45799fe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,3 +29,20 @@ jobs: uses: actions/checkout@v2 - name: Test catbird app run: swift test --package-path Packages/CatbirdApp --disable-automatic-resolution + test-linux-build: + name: Build on Linux + runs-on: ubuntu-20.04 + container: + image: swift:5.5.1-focal + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Build catbird app + id: build + run: .github/scripts/linux-build.sh + shell: bash + - name: Upload binary + uses: actions/upload-artifact@v2 + with: + name: catbird-linux + path: catbird-linux diff --git a/Packages/CatbirdAPI/Sources/CatbirdAPI/Catbird.swift b/Packages/CatbirdAPI/Sources/CatbirdAPI/Catbird.swift index 98d56dd..c9282a7 100644 --- a/Packages/CatbirdAPI/Sources/CatbirdAPI/Catbird.swift +++ b/Packages/CatbirdAPI/Sources/CatbirdAPI/Catbird.swift @@ -1,9 +1,11 @@ +#if !os(Linux) +/* + On Linux, URLSession and URLRequest are not in Foundation, but in FoundationNetworking. + FoundationNetworking has transitive dependencies that prevent compiling a static binary. + Catbird uses only models from CatbirdAPI, so URLSession was removed from the Linux build. + */ import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - /// API Client to mock server. public final class Catbird { @@ -76,7 +78,7 @@ public final class Catbird { case (_, let error?): completion(error) case (let http as HTTPURLResponse, _): - completion(CatbirdError(response: http, data: data)) + completion(CatbirdError(statusCode: http.statusCode, data: data)) default: completion(nil) } @@ -86,3 +88,31 @@ public final class Catbird { } } + +extension URLSessionTask { + /// Wait until task completed. + fileprivate func wait() { + guard let timeout = currentRequest?.timeoutInterval else { return } + let limitDate = Date(timeInterval: timeout, since: Date()) + while state == .running && RunLoop.current.run(mode: .default, before: limitDate) { + // wait + } + } +} + +extension CatbirdAction { + func makeRequest(to url: URL, parallelId: String? = nil) throws -> URLRequest { + let request = try makeHTTPRequest(to: url, parallelId: parallelId) + + var urlRequest = URLRequest(url: request.url) + urlRequest.httpMethod = request.httpMethod + for (key, value) in request.headers { + urlRequest.addValue(value, forHTTPHeaderField: key) + } + urlRequest.httpBody = request.httpBody + return urlRequest + } +} + +#endif + diff --git a/Packages/CatbirdAPI/Sources/CatbirdAPI/CatbirdAction.swift b/Packages/CatbirdAPI/Sources/CatbirdAPI/CatbirdAction.swift index 70bd47e..f8165f7 100644 --- a/Packages/CatbirdAPI/Sources/CatbirdAPI/CatbirdAction.swift +++ b/Packages/CatbirdAPI/Sources/CatbirdAPI/CatbirdAction.swift @@ -1,9 +1,5 @@ import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - /// Catbird API action. public enum CatbirdAction: Equatable { /// Add, or insert `ResponseMock` for `RequestPattern`. @@ -46,7 +42,7 @@ extension CatbirdAction { } } -// MARK: - CatbirdAction + URLRequest +// MARK: - CatbirdAction + Request extension CatbirdAction { /// Header name for parallel ID. @@ -54,21 +50,29 @@ extension CatbirdAction { private static let encoder = JSONEncoder() - /// Create a new `URLRequest`. - /// - /// - Parameter url: Catbird server base url. - /// - Returns: Request to mock server. - func makeRequest(to url: URL, parallelId: String? = nil) throws -> URLRequest { - var request = URLRequest(url: url.appendingPathComponent("catbird/api/mocks")) - request.httpMethod = "POST" - request.addValue("application/json", forHTTPHeaderField: "Content-Type") + struct HTTPRequest { + var httpMethod: String + var url: URL + var headers: [String: String] + var httpBody: Data? + + func value(forHTTPHeaderField name: String) -> String? { + headers[name] + } + } + + func makeHTTPRequest(to url: URL, parallelId: String? = nil) throws -> HTTPRequest { + var request = HTTPRequest( + httpMethod: "POST", + url: url.appendingPathComponent("catbird/api/mocks"), + headers: ["Content-Type": "application/json"], + httpBody: try CatbirdAction.encoder.encode(self)) + if let parallelId = parallelId { - request.addValue(parallelId, forHTTPHeaderField: CatbirdAction.parallelIdHeaderField) + request.headers[CatbirdAction.parallelIdHeaderField] = parallelId } - request.httpBody = try CatbirdAction.encoder.encode(self) return request } - } // MARK: - Codable diff --git a/Packages/CatbirdAPI/Sources/CatbirdAPI/CatbirdError.swift b/Packages/CatbirdAPI/Sources/CatbirdAPI/CatbirdError.swift index 6708054..93ead30 100644 --- a/Packages/CatbirdAPI/Sources/CatbirdAPI/CatbirdError.swift +++ b/Packages/CatbirdAPI/Sources/CatbirdAPI/CatbirdError.swift @@ -1,9 +1,5 @@ import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - /// From `Vapor.ErrorMiddleware`. private struct ErrorResponse: Codable { @@ -19,15 +15,15 @@ public struct CatbirdError: LocalizedError, CustomNSError { /// The domain of the error. public static var errorDomain = "com.redmadrobot.catbird.APIErrorDomain" - /// HTTP ststus code. + /// HTTP status code. public let errorCode: Int /// A localized message describing the reason for the failure. public let failureReason: String? - init?(response: HTTPURLResponse, data: Data?) { - guard !(200..<300).contains(response.statusCode) else { return nil } - self.errorCode = response.statusCode + init?(statusCode: Int, data: Data?) { + guard !(200..<300).contains(statusCode) else { return nil } + self.errorCode = statusCode self.failureReason = data.flatMap { (body: Data) in try? JSONDecoder().decode(ErrorResponse.self, from: body).reason } @@ -35,7 +31,11 @@ public struct CatbirdError: LocalizedError, CustomNSError { /// A localized message describing what error occurred. public var errorDescription: String? { +#if !os(Linux) return HTTPURLResponse.localizedString(forStatusCode: errorCode) +#else + return "Status code: \(errorCode)" +#endif } /// The user-info dictionary. diff --git a/Packages/CatbirdAPI/Sources/CatbirdAPI/URLSessionTask+Wait.swift b/Packages/CatbirdAPI/Sources/CatbirdAPI/URLSessionTask+Wait.swift deleted file mode 100644 index 0003cdf..0000000 --- a/Packages/CatbirdAPI/Sources/CatbirdAPI/URLSessionTask+Wait.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation - -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -extension URLSessionTask { - - /// Wait until task completed. - func wait() { - guard let timeout = currentRequest?.timeoutInterval else { return } - let limitDate = Date(timeInterval: timeout, since: Date()) - while state == .running && RunLoop.current.run(mode: .default, before: limitDate) { - // wait - } - } - -} diff --git a/Packages/CatbirdAPI/Tests/CatbirdAPITests/CatbirdTests.swift b/Packages/CatbirdAPI/Tests/CatbirdAPITests/CatbirdTests.swift index b721fd4..6cc51e5 100644 --- a/Packages/CatbirdAPI/Tests/CatbirdAPITests/CatbirdTests.swift +++ b/Packages/CatbirdAPI/Tests/CatbirdAPITests/CatbirdTests.swift @@ -1,3 +1,5 @@ +#if !os(Linux) + @testable import CatbirdAPI import XCTest @@ -120,3 +122,5 @@ final class CatbirdTests: XCTestCase { return HTTPURLResponse(url: url, statusCode: status, httpVersion: nil, headerFields: nil)! } } + +#endif diff --git a/Packages/CatbirdAPI/Tests/CatbirdAPITests/Network.swift b/Packages/CatbirdAPI/Tests/CatbirdAPITests/Network.swift index 1daa2c6..a26d55a 100644 --- a/Packages/CatbirdAPI/Tests/CatbirdAPITests/Network.swift +++ b/Packages/CatbirdAPI/Tests/CatbirdAPITests/Network.swift @@ -1,3 +1,5 @@ +#if !os(Linux) + import Foundation final class Network: URLProtocol { @@ -46,3 +48,5 @@ final class Network: URLProtocol { override func stopLoading() {} } + +#endif diff --git a/Packages/CatbirdApp/Tests/CatbirdAppTests/Helpers/Application+CatbirdAction.swift b/Packages/CatbirdApp/Tests/CatbirdAppTests/Helpers/Application+CatbirdAction.swift index 55a62a9..5264619 100644 --- a/Packages/CatbirdApp/Tests/CatbirdAppTests/Helpers/Application+CatbirdAction.swift +++ b/Packages/CatbirdApp/Tests/CatbirdAppTests/Helpers/Application+CatbirdAction.swift @@ -4,11 +4,11 @@ import XCTVapor extension Application { func perform(_ action: CatbirdAction, parallelId: String? = nil, file: StaticString = #file, line: UInt = #line) throws { - let request = try action.makeRequest(to: URL(string: "/")!, parallelId: parallelId) - let method = try XCTUnwrap(request.httpMethod.map { HTTPMethod(rawValue: $0) }, file: file, line: line) - let path = try XCTUnwrap(request.url?.path, file: file, line: line) + let request = try action.makeHTTPRequest(to: URL(string: "/")!, parallelId: parallelId) + let method = HTTPMethod(rawValue: request.httpMethod) + let path = request.url.path var headers = HTTPHeaders() - request.allHTTPHeaderFields?.forEach { key, value in + request.headers.forEach { key, value in headers.add(name: key, value: value) } let body = request.httpBody.map { (data: Data) -> ByteBuffer in