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
82 changes: 41 additions & 41 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,60 +9,60 @@ on:
schedule:
- cron: '45 2,13 * * *'
jobs:
linux-android:
strategy:
fail-fast: false
matrix:
swift: ['6.1', '6.2', 'nightly-6.3', 'nightly-main']
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@v1.3.1
with:
tool-cache: false
android: false
- uses: actions/checkout@v6
- name: "Test Swift Package on Linux"
run: swift test
- name: "Test Swift Package on Android"
#uses: skiptools/swift-android-action@v2
uses: skiptools/swift-android-action@main
with:
swift-version: ${{ matrix.swift }}

macos-ios:
ci:
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
#- os: 'macos-26'
# swift: '6.2'
# arch: 'arm64-v8a'
- os: 'macos-15-intel'
swift: '6.1'
arch: 'x86_64'
- os: 'macos-15-intel'
swift: '6.2'
arch: 'x86_64'
os: ['macos-15-intel', 'ubuntu-latest']
swift: ['6.2.3', 'nightly-6.3', 'nightly-main']
android-api: ['28']
exclude:
# Swift Android SDK 6.2.3 unavailable for Linux
- os: 'ubuntu-latest'
swift: '6.2.3'

# FIXME: test build failure in 6.2.3:
# TestRunner.swift:88:5: error: sending value of non-Sendable type '() async -> ()' risks causing data races [#SendingRisksDataRace]
#- os: 'macos-15-intel'
# swift: '6.2.3'

runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: "Setup Homebrew"
uses: Homebrew/actions/setup-homebrew@main

- name: "Build Swift Package for Android (Skip)"
run: |
brew install skiptools/skip/skip || (brew update && brew install skiptools/skip/skip)

- name: "Install Host Toolchain prerequisites"
if: runner.os == 'Linux'
run: |
sudo apt-get -y install libcurl4-openssl-dev || (sudo apt-get update && sudo apt-get -y install libcurl4-openssl-dev)

- name: "Install Swift SDK for Android"
run: |
# need swiftly init on Linux
swiftly init --assume-yes --no-modify-profile --skip-install
skip android sdk install --verbose --version ${{ matrix.swift }}
# https://github.com/swiftlang/swift-driver/pull/1879
ANDROID_NDK_ROOT="" skip android build --build-tests
- name: "Test Swift Package on Android (Skip)"
echo "ANDROID_NDK_ROOT=" >> $GITHUB_ENV

- uses: actions/checkout@v6

- name: "Build package for Android"
run: skip android build --build-tests

- name: "Test Swift Package on Android"
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 28
arch: ${{ matrix.arch }}
script: ANDROID_NDK_ROOT="" skip android test --verbose
- name: "Test Swift Package on macOS"
api-level: ${{ matrix.android-api }}
arch: x86_64
script: skip android test --apk --verbose
- name: "Test Swift Package on host OS"
run: swift test
- name: "Test Swift Package on iOS"
run: xcodebuild test -sdk "iphonesimulator" -destination "platform=iOS Simulator,name=iPhone 17" -scheme "$(xcodebuild -list -json | jq -r '.workspace.schemes[-1]')"
if: runner.os == 'macOS'
run: xcodebuild test -sdk "iphonesimulator" -destination "platform=iOS Simulator,name=iPhone 17" -scheme swift-android-native-Package

12 changes: 11 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// swift-tools-version: 5.9
import PackageDescription

let android = Context.environment["TARGET_OS_ANDROID"] ?? "0" != "0"

let package = Package(
name: "swift-android-native",
platforms: [.iOS(.v17), .macOS(.v14), .tvOS(.v17), .watchOS(.v10), .macCatalyst(.v17)],
products: [
.library(name: "AndroidNative", targets: ["AndroidNative"]),
.library(name: "AndroidContext", targets: ["AndroidContext"]),
Expand Down Expand Up @@ -73,5 +76,12 @@ let package = Package(
.testTarget(name: "AndroidNativeTests", dependencies: [
"AndroidNative",
], resources: [.embedInCode("Resources/sample_resource.txt")]),
]
],
swiftLanguageModes: [.v5]
)

if android {
// add compatibility import from OSLog to AndroidLogging
package.targets += [.target(name: "OSLog", dependencies: ["AndroidLogging"])]
}

5 changes: 5 additions & 0 deletions Sources/OSLog/OSLog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright 2025 Skip
// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception

@_exported import AndroidLogging

7 changes: 3 additions & 4 deletions Tests/AndroidAssetManagerTests/AndroidAssetManagerTests.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import XCTest
import Testing

@available(iOS 14.0, *)
class AndroidAssetManagerTests : XCTestCase {
public func testAssetManager() async throws {
struct AndroidAssetManagerTests {
@Test func testAssetManager() async throws {
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import XCTest
import Testing
import AndroidSystem

@available(iOS 14.0, *)
class AndroidChoreographerTests : XCTestCase {
public func testChoreographer() async throws {
struct AndroidChoreographerTests {
@Test func testChoreographer() async throws {
}
}
14 changes: 7 additions & 7 deletions Tests/AndroidContextTests/AndroidContextTests.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import XCTest
import Testing
import AndroidContext
import SwiftJNI
#if os(Android)
import AndroidNDK
#endif

@available(iOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
class AndroidContextTests : XCTestCase {
public func testAndroidContext() throws {
throw XCTSkip("this test is only for demo purposes")
#if !os(iOS)
struct AndroidContextTests {
// TODO: activate these tests now that we have `skip android test --apk` and can access the JNI context
@Test(.disabled("this test is only for demo purposes"))
func testAndroidContext() throws {
#if os(Android)
let nativeActivity: ANativeActivity! = nil
AndroidContext.contextPointer = nativeActivity.clazz
Expand All @@ -22,3 +21,4 @@ class AndroidContextTests : XCTestCase {
}
}
}
#endif
7 changes: 3 additions & 4 deletions Tests/AndroidLoggingTests/AndroidLoggingTests.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import XCTest
import Testing
import AndroidLogging // note: on non-android platforms, this will just export the system OSLog

@available(iOS 14.0, *)
class AndroidLoggingTests : XCTestCase {
public func testOSLogAPI() {
struct AndroidLoggingTests {
@Test func testOSLogAPI() {
let emptyLogger = Logger()
emptyLogger.info("Android logger test: empty message")

Expand Down
9 changes: 4 additions & 5 deletions Tests/AndroidLooperTests/AndroidLooperTests.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import XCTest
import Testing
import AndroidLooper

@available(iOS 14.0, *)
class AndroidLooperTests : XCTestCase {
override func setUp() {
struct AndroidLooperTests {
init() {
#if os(Android)
//AndroidLooper_initialize(nil)
#endif
}

public func testLooper() async throws {
@Test func testLooper() async throws {
}
}
34 changes: 19 additions & 15 deletions Tests/AndroidNativeTests/AndroidNativeTests.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import XCTest
import Testing
import AndroidNative
import Foundation
#if canImport(FoundationNetworking)
import FoundationEssentials
import FoundationNetworking
#else
import Foundation
#endif

@available(iOS 14.0, *)
class AndroidNativeTests : XCTestCase {
public func testNetwork() async throws {
private struct RetryableError: Error {
let message: String
}

struct AndroidNativeTests {
@Test(.disabled("temporarily disabled on Android due to hang"))
func testNetwork() async throws {
#if os(Android)
try AndroidBootstrap.setupCACerts() // needed in order to use https
#endif
Expand All @@ -28,14 +31,14 @@ class AndroidNativeTests : XCTestCase {
let statusCode = (response as? HTTPURLResponse)?.statusCode
if statusCode != 200 {
// throw with bad error so we retry
throw XCTSkip("bad status code: \(statusCode ?? 0) for url: \(url.absoluteString)")
throw RetryableError(message: "bad status code: \(statusCode ?? 0) for url: \(url.absoluteString)")
}
XCTAssertEqual(200, statusCode)
#expect(statusCode == 200)
let get = try JSONDecoder().decode([SwiftReleasesResponse].self, from: data)
XCTAssertGreaterThan(get.count, 0)
#expect(get.count > 0)
}
}

/// Retries the given block with an exponential backoff in between attempts.
func retry(count retryCount: Int, block: () async throws -> ()) async throws {
for retry in 1...retryCount {
Expand All @@ -52,14 +55,15 @@ class AndroidNativeTests : XCTestCase {
}
}

public func testEmbedInCodeResource() async throws {
XCTAssertEqual("Hello Android!\n", String(data: Data(PackageResources.sample_resource_txt), encoding: .utf8) ?? "")
@Test func testEmbedInCodeResource() async throws {
#expect(String(data: Data(PackageResources.sample_resource_txt), encoding: .utf8) == "Hello Android!\n")
}

public func testMainActor() async {
@Test(.disabled("temporarily disabled on Android due to hang"))
func testMainActor() async {
let actorDemo = await MainActorDemo()
let result = await actorDemo.add(n1: 1, n2: 2)
XCTAssertEqual(result, 3)
#expect(result == 3)
var tasks: [Task<Int, Never>] = []

for i in 0..<100 {
Expand All @@ -75,7 +79,7 @@ class AndroidNativeTests : XCTestCase {
totalResult += taskResult
}

XCTAssertEqual(9900, totalResult)
#expect(totalResult == 9900)
}
}

Expand Down
7 changes: 3 additions & 4 deletions Tests/AndroidSystemTests/AndroidSystemTests.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import XCTest
import Testing
import AndroidSystem

@available(iOS 14.0, *)
class AndroidSystemTests : XCTestCase {
public func testSystem() async throws {
struct AndroidSystemTests {
@Test func testSystem() async throws {
}
}
Loading