diff --git a/.github/workflows/package-ffi-engine.yml b/.github/workflows/package-ffi-engine.yml
index 9735d164..18aabfaf 100644
--- a/.github/workflows/package-ffi-engine.yml
+++ b/.github/workflows/package-ffi-engine.yml
@@ -55,10 +55,15 @@ jobs:
target: x86_64-pc-windows-msvc
use_cross: false
- # - name: iOS-arm64
- # os: macos-latest
- # target: aarch64-apple-ios
- # use_cross: false
+ - name: iOS-arm64
+ os: macos-latest
+ target: aarch64-apple-ios
+ use_cross: false
+
+ - name: iOS-arm64-sim
+ os: macos-latest
+ target: aarch64-apple-ios-sim
+ use_cross: false
# - name: Android-arm64
# os: ubuntu-latest
@@ -125,7 +130,6 @@ jobs:
7z a -tzip flipt-engine-ffi-${{ matrix.platform.name }}.zip `
target\${{ matrix.platform.target }}\release\fliptengine.* `
|| true
-
- name: Upload To Pull Request (Pull Request)
uses: actions/upload-artifact@v4
if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'package/')
diff --git a/.github/workflows/package-swift-sdk.yml b/.github/workflows/package-swift-sdk.yml
new file mode 100644
index 00000000..5e709e34
--- /dev/null
+++ b/.github/workflows/package-swift-sdk.yml
@@ -0,0 +1,17 @@
+name: Package Swift SDK
+on:
+ push:
+ tags:
+ - "flipt-client-swift-**"
+
+permissions:
+ contents: write
+ id-token: write
+
+jobs:
+ build:
+ uses: ./.github/workflows/package-ffi-sdks.yml
+ with:
+ sdks: "swift"
+ tag: ${{ github.ref }}
+ secrets: inherit
diff --git a/.github/workflows/test-swift-sdk.yml b/.github/workflows/test-swift-sdk.yml
new file mode 100644
index 00000000..1119e17c
--- /dev/null
+++ b/.github/workflows/test-swift-sdk.yml
@@ -0,0 +1,59 @@
+name: Test Swift SDK
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ id-token: write
+
+jobs:
+ test:
+ name: Integration Tests
+ runs-on: macos-latest
+ steps:
+ - name: Checkout Sources
+ uses: actions/checkout@v4
+
+ - name: Install Flipt
+ uses: flipt-io/setup-action@v0.2.0
+ with:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Install Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: 1.75.0
+ override: true
+
+ - name: Install Swift
+ uses: swift-actions/setup-swift@v1
+ with:
+ swift-version: "5.9"
+
+ - name: run flipt
+ env:
+ FLIPT_STORAGE_TYPE: "local"
+ FLIPT_STORAGE_LOCAL_PATH: "./test/fixtures/testdata"
+ run: flipt&
+
+ - name: Install Rust Targets
+ run: |
+ rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim
+
+ - name: Build Engine
+ run: |
+ cd ./flipt-engine-ffi
+ ./build.sh
+ cd ..
+
+ - name: Run Integration Tests
+ env:
+ FLIPT_URL: "http://0.0.0.0:8080"
+ FLIPT_AUTH_TOKEN: "secret"
+ run: |
+ cd ./flipt-client-swift
+ swift test
diff --git a/flipt-client-swift/.gitignore b/flipt-client-swift/.gitignore
new file mode 100644
index 00000000..0023a534
--- /dev/null
+++ b/flipt-client-swift/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/flipt-client-swift/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flipt-client-swift/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/flipt-client-swift/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/flipt-client-swift/Package.swift b/flipt-client-swift/Package.swift
new file mode 100644
index 00000000..d542462f
--- /dev/null
+++ b/flipt-client-swift/Package.swift
@@ -0,0 +1,29 @@
+// swift-tools-version: 5.9.2
+
+import PackageDescription
+
+let package = Package(
+ name: "FliptClient",
+ platforms: [.iOS(.v13), .macOS(.v10_15)],
+ products: [
+ .library(
+ name: "FliptClient",
+ targets: ["FliptClient"]),
+ ],
+ targets: [
+ .target(
+ name: "FliptClient",
+ dependencies: ["FliptEngineFFI"],
+ linkerSettings: [
+ .linkedFramework("SystemConfiguration"),
+ .linkedLibrary("fliptengine")
+ ]
+ ),
+ .binaryTarget(
+ name: "FliptEngineFFI",
+ path: "./Sources/FliptEngineFFI.xcframework"),
+ .testTarget(
+ name: "FliptClientTests",
+ dependencies: ["FliptClient"]),
+ ]
+)
diff --git a/flipt-client-swift/Sources/FliptClient/FliptClient.swift b/flipt-client-swift/Sources/FliptClient/FliptClient.swift
new file mode 100644
index 00000000..535cd93c
--- /dev/null
+++ b/flipt-client-swift/Sources/FliptClient/FliptClient.swift
@@ -0,0 +1,282 @@
+import Foundation
+import FliptEngineFFI
+
+public class FliptClient {
+ private var engine: UnsafeMutableRawPointer?
+ private var namespace: String = "default"
+ private var url: String = ""
+ private var authentication: Authentication?
+ private var ref: String = ""
+ private var updateInterval: Int = 0
+ private var fetchMode: FetchMode = .polling
+
+ init(namespace: String = "default",
+ url: String = "",
+ authentication: Authentication? = nil,
+ ref: String = "",
+ updateInterval: Int = 120,
+ fetchMode: FetchMode = .polling) throws {
+ self.namespace = namespace
+ self.url = url
+ self.authentication = authentication
+ self.ref = ref
+ self.updateInterval = updateInterval
+ self.fetchMode = fetchMode
+
+ let clientOptions = ClientOptions(
+ url: url,
+ authentication: authentication,
+ updateInterval: updateInterval,
+ reference: ref,
+ fetchMode: fetchMode
+ )
+
+ guard let jsonData = try? JSONEncoder().encode(clientOptions) else {
+ throw ClientError.invalidOptions
+ }
+
+ let jsonStr = String(data: jsonData, encoding: .utf8)
+ let namespaceCString = strdup(namespace)
+ let clientOptionsCString = strdup(jsonStr)
+
+ engine = initialize_engine(namespaceCString, clientOptionsCString)
+
+ free(namespaceCString)
+ free(clientOptionsCString)
+ }
+
+ deinit {
+ close()
+ }
+
+ func close() {
+ if let engine = engine {
+ destroy_engine(engine)
+ self.engine = nil
+ }
+ }
+
+ func evaluateVariant(flagKey: String, entityID: String, evalContext: [String: String]) throws -> VariantEvaluationResponse {
+ let evaluationRequest = EvaluationRequest(
+ flag_key: flagKey,
+ entity_id: entityID,
+ context: evalContext
+ )
+
+ guard let requestData = try? JSONEncoder().encode(evaluationRequest) else {
+ throw ClientError.invalidRequest
+ }
+
+ let requestCString = strdup(String(data: requestData, encoding: .utf8))
+
+ let variantResponse = evaluate_variant(engine, requestCString)
+ free(requestCString)
+
+ let responseString = String(cString: variantResponse!)
+ destroy_string(UnsafeMutablePointer(mutating: variantResponse))
+
+ do {
+ let variantResult = try JSONDecoder().decode(VariantResult.self, from: Data(responseString.utf8))
+ // Use variantResult here
+ if variantResult.status != "success" {
+ throw ClientError.evaluationFailed(message: variantResult.error_message ?? "Unknown error")
+ }
+ guard let result = variantResult.result else {
+ throw ClientError.evaluationFailed(message: "missing result")
+ }
+
+ return result
+ } catch {
+ throw ClientError.parsingError
+ }
+
+ }
+
+ func evaluateBoolean(flagKey: String, entityID: String, evalContext: [String: String]) throws -> BooleanEvaluationResponse {
+ let evaluationRequest = EvaluationRequest(
+ flag_key: flagKey,
+ entity_id: entityID,
+ context: evalContext
+ )
+
+ guard let requestData = try? JSONEncoder().encode(evaluationRequest) else {
+ throw ClientError.invalidRequest
+ }
+
+ let requestCString = strdup(String(data: requestData, encoding: .utf8))
+
+ let booleanResponse = evaluate_boolean(engine, requestCString)
+ free(requestCString)
+
+ let responseString = String(cString: booleanResponse!)
+ destroy_string(UnsafeMutablePointer(mutating: booleanResponse))
+
+ guard let booleanResult = try? JSONDecoder().decode(BooleanResult.self, from: Data(responseString.utf8)) else {
+ throw ClientError.parsingError
+ }
+
+ if booleanResult.status != "success" {
+ throw ClientError.evaluationFailed(message: booleanResult.error_message ?? "Unknown error")
+ }
+
+ return booleanResult.result!
+ }
+
+ func listFlags() throws -> [Flag] {
+ let flagsResponse = list_flags(engine)
+
+ let responseString = String(cString: flagsResponse!)
+ destroy_string(UnsafeMutablePointer(mutating: flagsResponse))
+
+ guard let listFlagsResult = try? JSONDecoder().decode(ListFlagsResult.self, from: Data(responseString.utf8)) else {
+ throw ClientError.parsingError
+ }
+
+ if listFlagsResult.status != "success" {
+ throw ClientError.evaluationFailed(message: listFlagsResult.error_message ?? "Unknown error")
+ }
+
+ return listFlagsResult.result!
+ }
+
+ func evaluateBatch(requests: [EvaluationRequest]) throws -> BatchEvaluationResponse {
+ guard let requestsData = try? JSONEncoder().encode(requests) else {
+ throw ClientError.invalidRequest
+ }
+
+ let requestCString = strdup(String(data: requestsData, encoding: .utf8))
+
+ let batchResponse = evaluate_batch(engine, requestCString)
+ free(requestCString)
+
+ let responseString = String(cString: batchResponse!)
+ destroy_string(UnsafeMutablePointer(mutating: batchResponse))
+
+ guard let batchResult = try? JSONDecoder().decode(BatchResult.self, from: Data(responseString.utf8)) else {
+ throw ClientError.parsingError
+ }
+
+ if batchResult.status != "success" {
+ throw ClientError.evaluationFailed(message: batchResult.error_message ?? "Unknown error")
+ }
+
+ return batchResult.result!
+ }
+
+
+
+
+ enum ClientError: Error {
+ case invalidOptions
+ case invalidRequest
+ case evaluationFailed(message: String)
+ case parsingError
+ }
+}
+
+enum Authentication: Encodable {
+ case clientToken(String)
+ case jwtToken(String)
+
+ // Custom Codable logic to encode/decode based on the case
+ private enum CodingKeys: String, CodingKey {
+ case client_token
+ case jwt_token
+ }
+
+ enum AuthenticationType: String, Codable {
+ case clientToken, jwtToken
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ switch self {
+ case .clientToken(let token):
+ try container.encode(token, forKey: .client_token)
+ case .jwtToken(let token):
+ try container.encode(token, forKey: .jwt_token)
+ }
+ }
+}
+
+struct EvaluationRequest: Codable {
+ let flag_key: String
+ let entity_id: String
+ let context: [String: String]
+}
+
+struct ClientTokenAuthentication: Codable {
+ let token: String
+}
+
+struct JWTAuthentication: Codable {
+ let token: String
+}
+
+enum FetchMode: String, Codable {
+ case streaming
+ case polling
+}
+
+struct ClientOptions: Encodable {
+ let url: String
+ let authentication: T?
+ let updateInterval: Int
+ let reference: String
+ let fetchMode: FetchMode
+}
+
+struct Flag: Codable {
+ let key: String
+ let enabled: Bool
+ let type: String
+}
+
+struct VariantEvaluationResponse: Codable {
+ let match: Bool
+ let segment_keys: [String]
+ let reason: String
+ let flag_key: String
+ let variant_key: String
+ let variant_attachment: String?
+ let request_duration_millis: Double
+ let timestamp: String
+}
+
+struct BooleanEvaluationResponse: Codable {
+ let enabled: Bool
+ let flag_key: String
+ let reason: String
+ let request_duration_millis: Double
+ let timestamp: String
+}
+
+struct ErrorEvaluationResponse: Codable {
+ let flag_key: String
+ let namespace_key: String
+ let reason: String
+}
+
+struct BatchEvaluationResponse: Codable {
+ let responses: [Response]
+ let request_duration_millis: Double
+}
+
+struct Response: Codable {
+ let type: String
+ let variant_evaluation_response: VariantEvaluationResponse?
+ let boolean_evaluation_response: BooleanEvaluationResponse?
+ let error_evaluation_response: ErrorEvaluationResponse?
+}
+
+struct Result: Codable {
+ let status: String
+ let result: R?
+ let error_message: String?
+}
+
+// Specific result types
+typealias VariantResult = Result
+typealias BooleanResult = Result
+typealias BatchResult = Result
+typealias ListFlagsResult = Result<[Flag]>
diff --git a/flipt-client-swift/Sources/FliptEngineFFI.xcframework/Info.plist b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/Info.plist
new file mode 100644
index 00000000..3e9609bd
--- /dev/null
+++ b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/Info.plist
@@ -0,0 +1,63 @@
+
+
+
+
+ AvailableLibraries
+
+
+ BinaryPath
+ libfliptengine.a
+ HeadersPath
+ Headers
+ LibraryIdentifier
+ ios-arm64-simulator
+ LibraryPath
+ libfliptengine.a
+ SupportedArchitectures
+
+ arm64
+
+ SupportedPlatform
+ ios
+ SupportedPlatformVariant
+ simulator
+
+
+ BinaryPath
+ libfliptengine.a
+ HeadersPath
+ Headers
+ LibraryIdentifier
+ ios-arm64
+ LibraryPath
+ libfliptengine.a
+ SupportedArchitectures
+
+ arm64
+
+ SupportedPlatform
+ ios
+
+
+ BinaryPath
+ libfliptengine.a
+ HeadersPath
+ Headers
+ LibraryIdentifier
+ macos-arm64
+ LibraryPath
+ libfliptengine.a
+ SupportedArchitectures
+
+ arm64
+
+ SupportedPlatform
+ macos
+
+
+ CFBundlePackageType
+ XFWK
+ XCFrameworkFormatVersion
+ 1.0
+
+
diff --git a/flipt-client-swift/Sources/FliptEngineFFI.xcframework/ios-arm64-simulator/Headers/module.modulemap b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/ios-arm64-simulator/Headers/module.modulemap
new file mode 100644
index 00000000..b7484109
--- /dev/null
+++ b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/ios-arm64-simulator/Headers/module.modulemap
@@ -0,0 +1,4 @@
+module FliptEngineFFI {
+ header "flipt_engine.h"
+ export *
+}
diff --git a/flipt-client-swift/Sources/FliptEngineFFI.xcframework/ios-arm64/Headers/module.modulemap b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/ios-arm64/Headers/module.modulemap
new file mode 100644
index 00000000..b7484109
--- /dev/null
+++ b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/ios-arm64/Headers/module.modulemap
@@ -0,0 +1,4 @@
+module FliptEngineFFI {
+ header "flipt_engine.h"
+ export *
+}
diff --git a/flipt-client-swift/Sources/FliptEngineFFI.xcframework/macos-arm64/Headers/module.modulemap b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/macos-arm64/Headers/module.modulemap
new file mode 100644
index 00000000..b7484109
--- /dev/null
+++ b/flipt-client-swift/Sources/FliptEngineFFI.xcframework/macos-arm64/Headers/module.modulemap
@@ -0,0 +1,4 @@
+module FliptEngineFFI {
+ header "flipt_engine.h"
+ export *
+}
diff --git a/flipt-client-swift/Tests/FliptClientTests/FliptClientTests.swift b/flipt-client-swift/Tests/FliptClientTests/FliptClientTests.swift
new file mode 100644
index 00000000..b5a82b66
--- /dev/null
+++ b/flipt-client-swift/Tests/FliptClientTests/FliptClientTests.swift
@@ -0,0 +1,156 @@
+import XCTest
+@testable import FliptClient
+
+class FliptClientTests: XCTestCase {
+
+ var evaluationClient: FliptClient!
+ var fliptUrl: String = ""
+ var authToken: String = ""
+
+ override func setUp() {
+ super.setUp()
+
+ guard let fliptUrl = ProcessInfo.processInfo.environment["FLIPT_URL"],
+ let authToken = ProcessInfo.processInfo.environment["FLIPT_AUTH_TOKEN"] else {
+ XCTFail("FLIPT_URL and FLIPT_AUTH_TOKEN must be set")
+ return
+ }
+
+ do {
+ evaluationClient = try FliptClient(
+ namespace: "default",
+ url: fliptUrl,
+ authentication: .clientToken(authToken)
+ )
+ } catch {
+ XCTFail("Failed to initialize EvaluationClient: \(error)")
+ }
+ }
+
+ override func tearDown() {
+ evaluationClient = nil
+ super.tearDown()
+ }
+
+ func testInvalidAuthentication() {
+ do {
+ let client = try FliptClient(
+ namespace: "default",
+ url: fliptUrl,
+ authentication: .clientToken("invalid")
+ )
+
+ let _ = try client.evaluateVariant(flagKey: "flag1", entityID: "someentity", evalContext: ["fizz": "buzz"])
+ XCTFail("Expected an error, but got none")
+ } catch let error as FliptClient.ClientError {
+ XCTAssertTrue(error.localizedDescription != "") // this could be better
+ } catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+
+ func testVariant() {
+ do {
+ let variant = try evaluationClient.evaluateVariant(
+ flagKey: "flag1",
+ entityID: "someentity",
+ evalContext: ["fizz": "buzz"]
+ )
+ XCTAssertTrue(variant.match)
+ XCTAssertEqual(variant.flag_key, "flag1")
+ XCTAssertEqual(variant.reason, "MATCH_EVALUATION_REASON")
+ XCTAssertTrue(variant.segment_keys.contains("segment1"))
+ } catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+ func testBoolean() {
+ do {
+ let boolean = try evaluationClient.evaluateBoolean(
+ flagKey: "flag_boolean",
+ entityID: "someentity",
+ evalContext: ["fizz": "buzz"]
+ )
+ XCTAssertEqual(boolean.flag_key, "flag_boolean")
+ XCTAssertTrue(boolean.enabled)
+ XCTAssertEqual(boolean.reason, "MATCH_EVALUATION_REASON")
+ } catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+ func testBatch() {
+ do {
+ let requests = [
+ EvaluationRequest(flag_key: "flag1", entity_id: "someentity", context: ["fizz": "buzz"]),
+ EvaluationRequest(flag_key: "flag_boolean", entity_id: "someentity", context: ["fizz": "buzz"]),
+ EvaluationRequest(flag_key: "notfound", entity_id: "someentity", context: ["fizz": "buzz"])
+ ]
+
+ let batch = try evaluationClient.evaluateBatch(requests: requests)
+ XCTAssertEqual(batch.responses.count, 3)
+
+ let variant = batch.responses[0]
+ XCTAssertEqual(variant.type, "VARIANT_EVALUATION_RESPONSE_TYPE")
+ guard let variantResp = variant.variant_evaluation_response else {
+ XCTFail("unexpected response type")
+ return
+ }
+ XCTAssertTrue(variantResp.match)
+ XCTAssertEqual(variantResp.flag_key, "flag1")
+ XCTAssertEqual(variantResp.reason, "MATCH_EVALUATION_REASON")
+ XCTAssertTrue(variantResp.segment_keys.contains("segment1"))
+
+ let boolean = batch.responses[1]
+ XCTAssertEqual(boolean.type, "BOOLEAN_EVALUATION_RESPONSE_TYPE")
+ guard let boolResp = boolean.boolean_evaluation_response else {
+ XCTFail("unexpected response type")
+ return
+ }
+ XCTAssertEqual(boolResp.flag_key, "flag_boolean")
+ XCTAssertTrue(boolResp.enabled)
+ XCTAssertEqual(boolResp.reason, "MATCH_EVALUATION_REASON")
+
+ let errorResponse = batch.responses[2]
+ XCTAssertEqual(errorResponse.type, "ERROR_EVALUATION_RESPONSE_TYPE")
+ guard let errResp = errorResponse.error_evaluation_response else {
+ XCTFail("unexpected response type")
+ return
+ }
+ XCTAssertEqual(errResp.flag_key, "notfound")
+ XCTAssertEqual(errResp.namespace_key, "default")
+ XCTAssertEqual(errResp.reason, "NOT_FOUND_ERROR_EVALUATION_REASON")
+ } catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+ func testListFlags() {
+ do {
+ let flags = try evaluationClient.listFlags()
+ XCTAssertFalse(flags.isEmpty)
+ XCTAssertEqual(flags.count, 2)
+ } catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+ func testVariantFailure() {
+ do {
+ let _ = try evaluationClient.evaluateVariant(
+ flagKey: "nonexistent",
+ entityID: "someentity",
+ evalContext: ["fizz": "buzz"]
+ )
+ XCTFail("Expected an error, but got none")
+ } catch let error as FliptClient.ClientError {
+ XCTAssertTrue(error.localizedDescription != "") // this could be better
+ } catch {
+ XCTFail("Unexpected error: \(error)")
+ }
+ }
+
+
+}
diff --git a/flipt-engine-ffi/build.sh b/flipt-engine-ffi/build.sh
new file mode 100755
index 00000000..086e1b60
--- /dev/null
+++ b/flipt-engine-ffi/build.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+cargo build -p flipt-engine-ffi --release --target=aarch64-apple-ios-sim
+cargo build -p flipt-engine-ffi --release --target=aarch64-apple-ios
+cargo build -p flipt-engine-ffi --release --target=aarch64-apple-darwin
+
+rm -rf ../flipt-client-swift/Sources/FliptEngineFFI.xcframework
+
+xcodebuild -create-xcframework \
+ -library ../target/aarch64-apple-ios-sim/release/libfliptengine.a -headers ./include/ \
+ -library ../target/aarch64-apple-ios/release/libfliptengine.a -headers ./include/ \
+ -library ../target/aarch64-apple-darwin/release/libfliptengine.a -headers ./include/ \
+ -output "../flipt-client-swift/Sources/FliptEngineFFI.xcframework"
diff --git a/flipt-engine-ffi/include/module.modulemap b/flipt-engine-ffi/include/module.modulemap
new file mode 100644
index 00000000..b7484109
--- /dev/null
+++ b/flipt-engine-ffi/include/module.modulemap
@@ -0,0 +1,4 @@
+module FliptEngineFFI {
+ header "flipt_engine.h"
+ export *
+}
diff --git a/package/ffi/main.go b/package/ffi/main.go
index 91aa28d3..4b42f122 100644
--- a/package/ffi/main.go
+++ b/package/ffi/main.go
@@ -31,6 +31,7 @@ var (
"java-musl": javaMuslBuild,
"dart": dartBuild,
"csharp": csharpBuild,
+ "swift": swiftBuild,
}
sema = make(chan struct{}, 5)
// defaultInclude is the default include for all builds to copy over the
@@ -150,6 +151,8 @@ var (
{id: "Linux-arm64-musl", target: "aarch64-unknown-linux-musl", ext: "tar.gz", libc: musl},
{id: "Linux-x86_64-musl", target: "x86_64-unknown-linux-musl", ext: "tar.gz", libc: musl},
{id: "Darwin-arm64", target: "aarch64-apple-darwin", ext: "tar.gz", libc: both},
+ {id: "iOS-arm64", target: "aarch64-apple-ios", ext: "tar.gz", libc: both},
+ {id: "iOS-arm64-sim", target: "aarch64-apple-ios-sim", ext: "tar.gz", libc: both},
{id: "Darwin-x86_64", target: "x86_64-apple-darwin", ext: "tar.gz", libc: both},
{id: "Windows-x86_64", target: "x86_64-pc-windows-msvc", ext: "zip", libc: both},
}
@@ -172,10 +175,10 @@ func getLatestEngineTag() (string, error) {
// use github api to get the latest release of the ffi engine
cmd := exec.Command("sh", "-c", `
- curl -s "https://api.github.com/repos/flipt-io/flipt-client-sdks/releases" |
- jq -r '.[] | select(.tag_name | startswith("flipt-engine-ffi-")) | .tag_name' |
- sort -Vr |
- head -n 1 |
+ curl -s "https://api.github.com/repos/flipt-io/flipt-client-sdks/releases" |
+ jq -r '.[] | select(.tag_name | startswith("flipt-engine-ffi-")) | .tag_name' |
+ sort -Vr |
+ head -n 1 |
sed "s/^flipt-engine-ffi-//"
`)
@@ -588,6 +591,89 @@ func csharpBuild(ctx context.Context, client *dagger.Client, hostDirectory *dagg
return err
}
+func swiftBuild(ctx context.Context, client *dagger.Client, hostDirectory *dagger.Directory, opts ...buildOptionsFn) error {
+ buildOpts := buildOptions{
+ libc: glibc,
+ }
+
+ for _, opt := range opts {
+ opt(&buildOpts)
+ }
+
+ pat := os.Getenv("GITHUB_TOKEN")
+ if pat == "" {
+ return errors.New("GITHUB_TOKEN environment variable must be set")
+ }
+
+ var (
+ encodedPAT = base64.URLEncoding.EncodeToString([]byte("pat:" + pat))
+ ghToken = client.SetSecret("gh-token", encodedPAT)
+ )
+
+ var gitUserName = os.Getenv("GIT_USER_NAME")
+ if gitUserName == "" {
+ gitUserName = "flipt-bot"
+ }
+
+ var gitUserEmail = os.Getenv("GIT_USER_EMAIL")
+ if gitUserEmail == "" {
+ gitUserEmail = "dev@flipt.io"
+ }
+
+ git := client.Container().From("golang:1.21.3-bookworm"). // probably don't need golang here but i assume it has good deps
+ WithSecretVariable("GITHUB_TOKEN", ghToken).
+ WithExec([]string{"git", "config", "--global", "user.email", gitUserEmail}).
+ WithExec([]string{"git", "config", "--global", "user.name", gitUserName}).
+ WithExec([]string{"sh", "-c", `git config --global http.https://github.com/.extraheader "AUTHORIZATION: Basic ${GITHUB_TOKEN}"`})
+
+ repository := git.
+ WithExec([]string{"git", "clone", "https://github.com/flipt-io/flipt-client-sdks.git", "/src"}).
+ WithWorkdir("/src").
+ WithFile("Sources/FliptEngineFFI.xcframework/ios-arm64/Headers/flipt_engine.h", hostDirectory.File("flipt-engine-ffi/include/flipt_engine.h")).
+ WithFile("Sources/FliptEngineFFI.xcframework/ios-arm64-simulator/Headers/flipt_engine.h", hostDirectory.File("flipt-engine-ffi/include/flipt_engine.h")).
+ WithFile("Sources/FliptEngineFFI.xcframework/ios-arm64/libfliptengine.a", hostDirectory.File("target/aarch64-apple-ios/release/libfliptengine.a")).
+ WithFile("Sources/FliptEngineFFI.xcframework/ios-arm64-simulator/libfliptengine.a", hostDirectory.File("target/aarch64-apple-ios-sim/release/libfliptengine.a"))
+
+ filtered := repository.
+ WithEnvVariable("FILTER_BRANCH_SQUELCH_WARNING", "1").
+ WithExec([]string{"git", "filter-branch", "-f", "--prune-empty",
+ "--subdirectory-filter", "flipt-client-swift",
+ "--tree-filter", "cp -r /tmp/ext .",
+ "--", tag})
+
+ _, err := filtered.Sync(ctx)
+ if !push {
+ return err
+ }
+
+ if tag == "" {
+ return fmt.Errorf("tag is not set")
+ }
+
+ const tagPrefix = "refs/tags/flipt-client-ios-"
+ if !strings.HasPrefix(tag, tagPrefix) {
+ return fmt.Errorf("tag %q must start with %q", tag, tagPrefix)
+ }
+
+ targetRepo := os.Getenv("TARGET_REPO")
+ if targetRepo == "" {
+ targetRepo = "https://github.com/flipt-io/flipt-client-swift.git"
+ }
+
+ targetTag := strings.TrimPrefix(tag, tagPrefix)
+
+ // push to target repo/tag
+ _, err = filtered.WithExec([]string{
+ "git",
+ "push",
+ "-f",
+ targetRepo,
+ fmt.Sprintf("%s:%s", tag, targetTag)}).
+ Sync(ctx)
+
+ return err
+}
+
func isDirEmptyOrNotExist(path string) (bool, error) {
f, err := os.Open(path)
if err != nil {