Skip to content
Open
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
236 changes: 236 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
name: ci
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no concurrency: group means rapid pushes to a PR branch queue up N redundant matrix runs. usual pattern for this repo would be something like:

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

at the top of the workflow


on:
push:
branches: [main]
pull_request:
workflow_dispatch:
inputs:
swift_versions:
description: 'JSON array of Swift versions to test (e.g. ["6.3"], ["6.3", "nightly-main"])'
required: false
default: '["6.3"]'

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
build-examples:
name: Build ${{ matrix.os }} ${{ matrix.swift_version }}
strategy:
fail-fast: false
matrix:
swift_version: ${{ fromJSON(inputs.swift_versions || '["6.3"]') }}
sdk_triple: ['aarch64-unknown-linux-android28']
ndk_version: ['r27d']
os: ['ubuntu-latest', 'macos-latest']
runs-on: ${{ matrix.os }}
env:
SDK_TRIPLE: ${{ matrix.sdk_triple }}
NDK_VERSION: ${{ matrix.ndk_version }}
SWIFT_VERSION: ${{ matrix.swift_version }}
steps:
- uses: actions/checkout@v6

- name: Set up JDK 25
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '25'

- name: Install swiftly
run: |
set -euxo pipefail
if [[ "${RUNNER_OS}" == "Linux" ]]; then
sudo apt-get -yq install curl jq gpg unzip libcurl4-openssl-dev
ARCH="$(uname -m)"
curl -L -O --retry 3 "https://download.swift.org/swiftly/linux/swiftly-${ARCH}.tar.gz"
tar -xzf "swiftly-${ARCH}.tar.gz"
./swiftly init \
--assume-yes \
--skip-install \
--no-modify-profile \
--quiet-shell-followup
rm -f "swiftly-${ARCH}.tar.gz" swiftly
# The example projects' Gradle scripts look for swiftly under
# $HOME/.local/share/swiftly/bin, which is also where the official
# installer puts it. Add it to PATH for subsequent steps.
echo "$HOME/.local/share/swiftly/bin" >> "$GITHUB_PATH"
"$HOME/.local/share/swiftly/bin/swiftly" --version
elif [[ "${RUNNER_OS}" == "macOS" ]]; then
curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg
installer -pkg swiftly.pkg -target CurrentUserHomeDirectory
~/.swiftly/bin/swiftly init --quiet-shell-followup
. "${SWIFTLY_HOME_DIR:-$HOME/.swiftly}/env.sh"
hash -r
echo "$HOME/.swiftly/bin" >> "$GITHUB_PATH"
"$HOME/.swiftly/bin/swiftly" --version
else
echo "Unknown OS: ${RUNNER_OS}"
exit 1
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: quoting on $GITHUB_ENV / $GITHUB_PATH is inconsistent across the file. some lines quote it, this one (and a couple below) don't. harmless but my eye keeps snagging on it, would just quote everywhere

- name: Install Android NDK
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no caching = every run re-downloads the NDK (~1GB) and the android swift SDK. that's a lot of bytes off download.swift.org and a couple of minutes off every CI run across both OSes. actions/cache keyed on ${{ matrix.ndk_version }} and ${{ matrix.swift_version }} would be an easy win, and friendlier to the mirror. fine to land as-is and do it in a follow-up though

run: |
set -euxo pipefail
OS="$(uname -s | tr '[A-Z]' '[a-z]')"
curl -L -o ndk.zip --retry 3 "https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-${OS}.zip"
unzip -q ndk.zip -d "$HOME"
rm ndk.zip
echo "ANDROID_NDK_HOME=$HOME/android-ndk-${NDK_VERSION}" >> "$GITHUB_ENV"

- name: Install Android Swift SDK and matching host toolchain
# Looks up the Android Swift SDK URL and checksum from the swift.org
# install API and installs it via `swift sdk install`.
run: |
set -euxo pipefail
case "$SWIFT_VERSION" in
nightly-*)
nightly_version="${SWIFT_VERSION#nightly-}"
sdk_json=$(curl -fsSL "https://www.swift.org/api/v1/install/dev/${nightly_version}/android-sdk.json")
snapshot_tag=$(echo "$sdk_json" | jq -r '.[0].dir')
sdk_checksum=$(echo "$sdk_json" | jq -r '.[0].checksum')
if [ "$nightly_version" = "main" ]; then
branch="development"
else
branch="swift-${nightly_version}-branch"
fi
sdk_url="https://download.swift.org/${branch}/android-sdk/${snapshot_tag}/${snapshot_tag}_android.artifactbundle.tar.gz"
;;
*)
releases_json=$(curl -fsSL "https://www.swift.org/api/v1/install/releases.json")
# Pick the highest patch release whose name starts with the
# requested version (e.g. "6.3" -> "6.3.1" if it exists).
latest_version=$(echo "$releases_json" | jq -r --arg v "$SWIFT_VERSION" \
'[.[] | select(.name | startswith($v))]
| sort_by(.name | split(".") | map(tonumber? // 0))
| last
| .name')
if [ -z "$latest_version" ] || [ "$latest_version" = "null" ]; then
echo "Error: no Swift release matching '$SWIFT_VERSION' found in releases.json" >&2
exit 1
fi
sdk_checksum=$(echo "$releases_json" | jq -r --arg v "$latest_version" \
'.[] | select(.name == $v) | .platforms[] | select(.platform == "android-sdk") | .checksum')
snapshot_tag="swift-${latest_version}-RELEASE"
sdk_url="https://download.swift.org/swift-${latest_version}-release/android-sdk/${snapshot_tag}/${snapshot_tag}_android.artifactbundle.tar.gz"
;;
esac

swift_install=${snapshot_tag}
# trim leading "swift-" and trailing "-RELEASE"
swift_install=${swift_install#swift-}
swift_install=${swift_install/-RELEASE/}

echo "Installing Android Swift SDK and host toolchain"
echo " tag: $swift_install"
echo " url: $sdk_url"
echo " checksum: $sdk_checksum"
swiftly install "${swift_install}"
swift sdk install "$sdk_url" --checksum "$sdk_checksum"
swift sdk list

# Override the matrix-supplied SWIFT_VERSION (e.g. "6.3") with the
# resolved patch version (e.g. "6.3.1") so the gradle scripts pick up
# the actual artifactbundle directory name produced by `swift sdk
# install`. SWIFT_ANDROID_SDK_VERSION pins the bundle suffix for the
# same reason.
echo "SWIFT_VERSION=${swift_install}" >> "$GITHUB_ENV"
echo "SWIFT_ANDROID_SDK_VERSION=${snapshot_tag#swift-}_android" >> "$GITHUB_ENV"

- name: Configure Swift Android SDK
run: |
set -euo pipefail
# Locate the installed Android SDK artifactbundle. Its parent
# directory varies by OS / swiftpm version, so try the known
# candidates and pick the first one that actually matches.
shopt -s nullglob
candidates=(
"$HOME"/.swiftpm/swift-sdks/*android*.artifactbundle
"$HOME"/.config/swiftpm/swift-sdks/*android*.artifactbundle
"$HOME"/Library/org.swift.swiftpm/swift-sdks/*android*.artifactbundle
)
if [[ ${#candidates[@]} -eq 0 ]]; then
echo "No android SDK artifactbundle found in any known location" >&2
exit 1
fi
cd "${candidates[0]}"
# Link the SDK against the NDK we installed in the previous step.
# Someday we might not need this script, so gracefully skip it
# if it does not exist.
if [[ -x "./swift-android/scripts/setup-android-sdk.sh" ]]; then
"./swift-android/scripts/setup-android-sdk.sh"
fi

- name: Publish swift-java packages to local Maven
# The hashing-lib, weather-lib, and hello-cpp-swift/swift-lib modules
# depend on org.swift.swiftkit:swiftkit-core:1.0-SNAPSHOT, which is not
# published to a public Maven repo. The hashing-lib README documents
# publishing it to mavenLocal from the swift-java checkout that
# SwiftPM resolves into .build/checkouts/swift-java.
working-directory: hello-swift-java/hashing-lib
run: |
set -euxo pipefail
swift package resolve
./.build/checkouts/swift-java/gradlew \
--project-dir .build/checkouts/swift-java \
:SwiftKitCore:publishToMavenLocal

- name: Build hello-swift-raw-jni APK
run: ./gradlew :hello-swift-raw-jni:assembleDebug --stacktrace

- name: Build hello-swift-raw-jni-callback APK
run: ./gradlew :hello-swift-raw-jni-callback:assembleDebug --stacktrace

- name: Build hello-swift-raw-jni-library
run: ./gradlew :hello-swift-raw-jni-library:assembleDebug --stacktrace

- name: Build native-activity APK
run: ./gradlew :native-activity:assembleDebug --stacktrace

- name: Build hello-swift-java APK
run: ./gradlew :hello-swift-java-hashing-app:assembleDebug --stacktrace

- name: Build swift-java-weather-app APK
run: ./gradlew :swift-java-weather-app-weather-app:assembleDebug --stacktrace

- name: Build hello-cpp-swift cpp-lib
working-directory: hello-cpp-swift/cpp-lib
run: ./build-android-static.sh

- name: Build hello-cpp-swift APK
run: ./gradlew :hello-cpp-swift:app:assembleDebug --stacktrace

- name: Summarize APK artifacts
if: always()
run: |
echo "## APK Artifacts (${{ matrix.os }} / swift:${{ matrix.swift_version }} / ${{ matrix.sdk_triple }})" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| Project | APK | Size |" >> "$GITHUB_STEP_SUMMARY"
echo "|---------|-----|------|" >> "$GITHUB_STEP_SUMMARY"
found=0
while IFS= read -r apk; do
found=1
# Derive a human-readable project name from the path
project=$(echo "$apk" | sed -E 's#^\./##; s#/build/outputs/.*##')
name=$(basename "$apk")
# Human-readable size (du -h works on both Linux and macOS)
size=$(du -h "$apk" | cut -f1 | tr -d '[:space:]')
echo "| \`$project\` | \`$name\` | $size |" >> "$GITHUB_STEP_SUMMARY"
done < <(find . -path '*/build/outputs/apk/*.apk' -type f | sort)
if [ "$found" -eq 0 ]; then
echo "| _(none)_ | — | — |" >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload APK artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: apks-${{ matrix.os }}-${{ matrix.swift_version }}-${{ matrix.sdk_triple }}-${{ matrix.ndk_version }}
path: '**/build/outputs/apk/**/*.apk'
if-no-files-found: warn

11 changes: 9 additions & 2 deletions hello-cpp-swift/swift-lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,15 @@ def swiftRuntimeLibs = [
"swiftSynchronization"
]

def sdkName = "swift-6.3-RELEASE_android.artifactbundle"
def swiftVersion = "6.3"
// Swift toolchain version passed to swiftly (e.g. "6.3", "main-snapshot").
// Can be overridden via the SWIFT_VERSION environment variable, which is
// useful for CI matrices that test multiple toolchains.
def swiftVersion = System.getenv("SWIFT_VERSION") ?: "6.3"
// Android Swift SDK artifactbundle suffix. Substituted into the bundle
// directory name as "swift-${androidSdkVersion}.artifactbundle". Can be
// overridden via the SWIFT_ANDROID_SDK_VERSION environment variable.
def androidSdkVersion = System.getenv("SWIFT_ANDROID_SDK_VERSION") ?: "${swiftVersion}-RELEASE_android"
def sdkName = "swift-${androidSdkVersion}.artifactbundle"
def minSdk = android.defaultConfig.minSdkVersion.apiLevel

def abis = [
Expand Down
11 changes: 9 additions & 2 deletions hello-swift-java/hashing-lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,15 @@ def swiftRuntimeLibs = [
"swiftSynchronization"
]

def sdkName = "swift-6.3-RELEASE_android.artifactbundle"
def swiftVersion = "6.3"
// Swift toolchain version passed to swiftly (e.g. "6.3", "main-snapshot").
// Can be overridden via the SWIFT_VERSION environment variable, which is
// useful for CI matrices that test multiple toolchains.
def swiftVersion = System.getenv("SWIFT_VERSION") ?: "6.3"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not your fault, but worth calling out: this exact 6-line block is now duplicated across three build.gradle files and will drift the next time someone touches it. prime candidate for hoisting into swift-android.gradle.kts (or a root ext { }) so version bumps land in one place. happy to leave as pre-existing debt for a follow-up PR, totally fine to not touch it here

// Android Swift SDK artifactbundle suffix. Substituted into the bundle
// directory name as "swift-${androidSdkVersion}.artifactbundle". Can be
// overridden via the SWIFT_ANDROID_SDK_VERSION environment variable.
def androidSdkVersion = System.getenv("SWIFT_ANDROID_SDK_VERSION") ?: "${swiftVersion}-RELEASE_android"
def sdkName = "swift-${androidSdkVersion}.artifactbundle"
def minSdk = android.defaultConfig.minSdkVersion.apiLevel
/**
* Android ABIs and their Swift triple mappings
Expand Down
12 changes: 9 additions & 3 deletions swift-android.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ data class SwiftConfig(
var releaseExtraBuildFlags: List<String> = emptyList(),
var swiftlyPath: String? = null, // Optional custom swiftly path
var swiftSDKPath: String? = null, // Optional custom Swift SDK path
var swiftVersion: String = "6.3", // Swift version
var androidSdkVersion: String = "6.3-RELEASE_android" // SDK version
// Swift toolchain version passed to swiftly (e.g. "6.3", "main-snapshot").
// Can be overridden via the SWIFT_VERSION environment variable, which is
// useful for CI matrices that test multiple toolchains.
var swiftVersion: String = System.getenv("SWIFT_VERSION")?.takeIf { it.isNotEmpty() } ?: "6.3",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subtle behavior mismatch with the three groovy files: here you do ?.takeIf { it.isNotEmpty() } ?: "6.3", but the .gradle files do System.getenv("SWIFT_VERSION") ?: "6.3". if someone exports SWIFT_VERSION="" (which CI envs do more often than you'd think), kotlin falls back to 6.3 but groovy keeps the empty string and blows up later in the bundle path. align both sides, prefer the kotlin behavior

// Android Swift SDK artifactbundle suffix. Substituted into the bundle
// directory name as "swift-${androidSdkVersion}.artifactbundle". Can be
// overridden via the SWIFT_ANDROID_SDK_VERSION environment variable.
var androidSdkVersion: String = System.getenv("SWIFT_ANDROID_SDK_VERSION")?.takeIf { it.isNotEmpty() } ?: "${swiftVersion}-RELEASE_android"
)

// Architecture definitions
Expand Down Expand Up @@ -296,4 +302,4 @@ project.afterEvaluate {
} else {
throw GradleException("Android extension not found. Make sure to apply this script after the Android plugin.")
}
}
}
11 changes: 9 additions & 2 deletions swift-java-weather-app/weather-lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,15 @@ def swiftRuntimeLibs = [
"_FoundationICU",
"swiftSynchronization"
]
def sdkName = "swift-6.3-RELEASE_android.artifactbundle"
def swiftVersion = "6.3"
// Swift toolchain version passed to swiftly (e.g. "6.3", "main-snapshot").
// Can be overridden via the SWIFT_VERSION environment variable, which is
// useful for CI matrices that test multiple toolchains.
def swiftVersion = System.getenv("SWIFT_VERSION") ?: "6.3"
// Android Swift SDK artifactbundle suffix. Substituted into the bundle
// directory name as "swift-${androidSdkVersion}.artifactbundle". Can be
// overridden via the SWIFT_ANDROID_SDK_VERSION environment variable.
def androidSdkVersion = System.getenv("SWIFT_ANDROID_SDK_VERSION") ?: "${swiftVersion}-RELEASE_android"
def sdkName = "swift-${androidSdkVersion}.artifactbundle"
def minSdk = android.defaultConfig.minSdkVersion.apiLevel
/**
* Android ABIs and their Swift triple mappings
Expand Down