Skip to content
Open
Show file tree
Hide file tree
Changes from 19 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
211 changes: 211 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
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:

permissions:
contents: read

jobs:
build-examples:
name: Build ${{ matrix.os }} ${{ matrix.swift_version }}
strategy:
fail-fast: false
matrix:
#swift_version: ['nightly-main', '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.

commented-out nightly matrix is gonna rot if we forget about it (which... we will). either rip it out or promote it to a workflow_dispatch input so you can actually kick off a nightly run without editing the file. leaving it here means in six months someone uncomments it, it doesn't work, and we all pretend we didn't see it

swift_version: ['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 && \
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tiny style nit: you've got set -euxo pipefail at the top of this step but the macOS branch still chains everything with && \. under -e the chain is redundant and the linux branch right above doesn't do it, so it reads inconsistent. just newline-separate like the linux block?

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

# set environment variables used by swift-android.gradle.kts
echo "SWIFT_VERSION=${swift_install}" >> $GITHUB_ENV
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

worth a one-liner comment here explaining that you're deliberately overwriting the matrix SWIFT_VERSION with the resolved patch version (e.g. 6.3 -> 6.3.1) so gradle picks up the actual bundle dirname. i had to read the jq block twice to convince myself this wasn't a bug. future me will thank present you

echo "SWIFT_ANDROID_SDK_VERSION=${snapshot_tag#swift-}_android" >> $GITHUB_ENV

- name: Configure Swift Android SDK
run: |
set -euo pipefail
cd $HOME/.swiftpm/swift-sdks/*android*.artifactbundle || cd $HOME/.config/swiftpm/swift-sdks/*android*.artifactbundle || cd $HOME/Library/org.swift.swiftpm/swift-sdks/*android*.artifactbundle
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this cd glob || cd glob || cd glob chain is a bit of a timebomb. the globs are unquoted so if *android*.artifactbundle ever matches more than one directory, cd gets multiple args and the step dies with a weird error. also if the first path exists but is empty, the fallback never runs.

safer pattern would be something like:

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
)
[[ ${#candidates[@]} -gt 0 ]] || { echo "no android SDK bundle found"; exit 1; }
cd "${candidates[0]}"

or just parse swift sdk list output since you already call it the step before

# 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 APK
run: |
cd hello-cpp-swift/cpp-lib
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

micro-nit: cd hello-cpp-swift/cpp-lib && ... && cd - would read cleaner as either a (subshell) or a dedicated step with working-directory:. the cd - in particular is the kind of thing that silently breaks if someone adds a command above it that cds somewhere else

./build-android-static.sh
cd -
./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