From 3ee071e0981df15c36adee4e2915605fbb41fb29 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 24 Jul 2026 10:57:45 +0200 Subject: [PATCH] Add CI: host tests, consumer smoke test, Embedded Swift check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three jobs in GitHub Actions: - Build & test on macOS against SDK headers fetched from Panic (cached), with the pkg-config module generated into PKG_CONFIG_PATH. - A consumer smoke test (Scripts/consumer-test.sh) generating a scratch package that depends on play-date, guarding setting propagation. - An Embedded Swift cross-compile for armv7em-none-none-eabi (Scripts/build-embedded.sh) on a swift.org snapshot toolchain — the check that enforces the Embedded Swift subset promise. Verified locally: the full module compiles for the device triple with no violations. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 99 +++++++++++++++++++++++++++++++++++++++ README.md | 20 +++++++- Scripts/build-embedded.sh | 56 ++++++++++++++++++++++ Scripts/consumer-test.sh | 48 +++++++++++++++++++ 4 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100755 Scripts/build-embedded.sh create mode 100755 Scripts/consumer-test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dbb989e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,99 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +env: + # The SDK is downloaded from Panic (it is not redistributable). Only the + # C_API headers are used, so the Linux tarball works on every runner. + PLAYDATE_SDK_VERSION: "3.1.1" + +jobs: + test: + name: Build & test (macOS) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + # The manifest requires Swift 6.4 tools; pick the newest Xcode on the + # runner. Adjust if the image's default already suffices. + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest + + - name: Cache Playdate SDK + id: sdk-cache + uses: actions/cache@v4 + with: + path: ~/PlaydateSDK + key: playdate-sdk-${{ env.PLAYDATE_SDK_VERSION }} + + - name: Download Playdate SDK headers + if: steps.sdk-cache.outputs.cache-hit != 'true' + run: | + curl -sL "https://download.panic.com/playdate_sdk/Linux/PlaydateSDK-${PLAYDATE_SDK_VERSION}.tar.gz" | tar xz -C "$HOME" + mv "$HOME/PlaydateSDK-${PLAYDATE_SDK_VERSION}" "$HOME/PlaydateSDK" + test -f "$HOME/PlaydateSDK/C_API/pd_api.h" + + - name: Install pkg-config module + run: | + PLAYDATE_SDK_PATH="$HOME/PlaydateSDK" Scripts/install-pkgconfig.sh "$HOME/pkgconfig" + echo "PKG_CONFIG_PATH=$HOME/pkgconfig" >> "$GITHUB_ENV" + + - name: Build + run: swift build + + - name: Test + run: swift test + + - name: Consumer smoke test + run: Scripts/consumer-test.sh + + embedded: + name: Embedded Swift cross-compile + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Embedded Swift for bare-metal ARM needs a swift.org development + # snapshot; release/Xcode toolchains do not ship that stdlib. + - name: Install Swift main snapshot (swiftly) + run: | + curl -sL "https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz" | tar xz + ./swiftly init -y --quiet-shell-followup + . "$HOME/.local/share/swiftly/env.sh" + swiftly install --use main-snapshot + echo "$HOME/.local/share/swiftly/bin" >> "$GITHUB_PATH" + + # newlib provides the libc headers pd_api.h includes on bare metal. + - name: Install arm-none-eabi headers + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libnewlib-arm-none-eabi + + - name: Cache Playdate SDK + id: sdk-cache + uses: actions/cache@v4 + with: + path: ~/PlaydateSDK + key: playdate-sdk-${{ env.PLAYDATE_SDK_VERSION }} + + - name: Download Playdate SDK headers + if: steps.sdk-cache.outputs.cache-hit != 'true' + run: | + curl -sL "https://download.panic.com/playdate_sdk/Linux/PlaydateSDK-${PLAYDATE_SDK_VERSION}.tar.gz" | tar xz -C "$HOME" + mv "$HOME/PlaydateSDK-${PLAYDATE_SDK_VERSION}" "$HOME/PlaydateSDK" + test -f "$HOME/PlaydateSDK/C_API/pd_api.h" + + - name: Install pkg-config module + run: | + PLAYDATE_SDK_PATH="$HOME/PlaydateSDK" Scripts/install-pkgconfig.sh "$HOME/pkgconfig" + echo "PKG_CONFIG_PATH=$HOME/pkgconfig" >> "$GITHUB_ENV" + + - name: Cross-compile for armv7em-none-none-eabi + run: Scripts/build-embedded.sh diff --git a/README.md b/README.md index b94a1ab..0636f9f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # PlayDate +[![CI](https://github.com//play-date/actions/workflows/ci.yml/badge.svg)](https://github.com//play-date/actions/workflows/ci.yml) + Swift bindings to the [Playdate](https://play.date) C API. The Playdate C API is delivered as a `PlaydateAPI*` struct of function @@ -326,7 +328,19 @@ Shipping a `.pdx` needs the Playdate toolchain on top: (`-enable-experimental-feature Embedded`, triple `armv7em-none-none-eabi`). The wrappers are written within the Embedded Swift subset for exactly this -reason: no Foundation, no reflection, no untyped throws. See Apple's +reason: no Foundation, no reflection, no untyped throws. That claim is +enforced, not aspirational: `Scripts/build-embedded.sh` cross-compiles the +whole module for `armv7em-none-none-eabi` with Embedded Swift enabled, and +CI runs it on every push. Running it locally needs a swift.org development +snapshot toolchain (Xcode's toolchain doesn't ship the bare-metal embedded +stdlib) and the Arm GNU toolchain headers: + +```sh +SWIFT_BIN=~/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-.xctoolchain/usr/bin/swift \ + Scripts/build-embedded.sh +``` + +See Apple's [swift-playdate-examples](https://github.com/apple/swift-playdate-examples) for a working Makefile/toolchain setup that this library slots into. @@ -336,6 +350,10 @@ for a working Makefile/toolchain setup that this library slots into. Scripts/ install-pkgconfig.sh One-time setup: points the "playdate" pkg-config module at your SDK installation + build-embedded.sh Compile-only device check: Embedded Swift for + armv7em-none-none-eabi (run by CI) + consumer-test.sh Builds a scratch package depending on play-date + to prove settings propagate to consumers (CI) Sources/ CPlaydate/ System library target: module map + umbrella header importing pd_api.h from the SDK, plus inline shims for diff --git a/Scripts/build-embedded.sh b/Scripts/build-embedded.sh new file mode 100755 index 0000000..0a8e2e3 --- /dev/null +++ b/Scripts/build-embedded.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# +# Cross-compiles the PlayDate target for Playdate hardware (Embedded Swift, +# ARM Cortex-M7) as a compile-only check that the bindings stay within the +# Embedded Swift subset. Linking is left to game projects. +# +# Requirements: +# - A Swift toolchain with the embedded stdlib for armv7em-none-none-eabi +# (a swift.org development snapshot; Xcode's toolchain does not ship it). +# Override the binary with SWIFT_BIN, otherwise `swift` from PATH is used. +# - Arm GNU toolchain (arm-none-eabi) C headers, because pd_api.h includes +# libc headers that bare-metal builds resolve against newlib. Override the +# directory with ARM_NONE_EABI_INCLUDE, otherwise common install locations +# are searched. +# - The "playdate" pkg-config module (Scripts/install-pkgconfig.sh). + +set -eu + +swift_bin="${SWIFT_BIN:-swift}" + +include_dir="${ARM_NONE_EABI_INCLUDE:-}" +if [ -z "$include_dir" ]; then + for candidate in \ + /usr/local/playdate/gcc-arm-none-eabi-*/arm-none-eabi/include \ + /Applications/ArmGNUToolchain/*/arm-none-eabi/arm-none-eabi/include \ + /usr/lib/arm-none-eabi/include \ + /usr/include/newlib; do + if [ -f "$candidate/stdlib.h" ]; then + include_dir="$candidate" + break + fi + done +fi + +if [ -z "$include_dir" ] || [ ! -f "$include_dir/stdlib.h" ]; then + echo "error: arm-none-eabi C headers not found." >&2 + echo "Install the Arm GNU toolchain or set ARM_NONE_EABI_INCLUDE." >&2 + exit 1 +fi + +echo "Using swift: $swift_bin ($($swift_bin --version 2>/dev/null | head -1))" +echo "Using arm-none-eabi headers: $include_dir" + +# --build-system native stops after compilation; the default build system +# also tries to merge objects with the host linker, which cannot process +# bare-metal ELF objects. +exec "$swift_bin" build \ + --build-system native \ + --target PlayDate \ + --triple armv7em-none-none-eabi \ + -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ + -Xswiftc -wmo \ + -Xcc -I"$include_dir" \ + -Xcc -mcpu=cortex-m7 \ + -Xcc -mfloat-abi=hard \ + -Xcc -mfpu=fpv5-sp-d16 diff --git a/Scripts/consumer-test.sh b/Scripts/consumer-test.sh new file mode 100755 index 0000000..0d69c80 --- /dev/null +++ b/Scripts/consumer-test.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# +# Verifies the package works as a SwiftPM dependency: generates a scratch +# executable package that depends on play-date, imports both modules, and +# runs it. This guards the pkg-config setup — target settings that don't +# propagate to consumers (or manifest validation failures) surface here, +# not on this package's own build. + +set -eu + +package_dir="$(cd "$(dirname "$0")/.." && pwd)" +scratch_dir="$(mktemp -d)" +trap 'rm -rf "$scratch_dir"' EXIT + +mkdir -p "$scratch_dir/Sources/consumer" + +cat > "$scratch_dir/Package.swift" < "$scratch_dir/Sources/consumer/main.swift" <<'EOF' +import CPlaydate +import PlayDate + +// Touch a type from each module to prove both import and link. +let event = Playdate.SystemEvent(event: kEventInit, argument: 0) +let buttons: Playdate.System.Buttons = [.a, .up] +print(event != nil && buttons.contains(.a) ? "ok" : "broken") +EOF + +cd "$scratch_dir" +output="$(swift run 2>&1 | tail -1)" +echo "consumer output: $output" +[ "$output" = "ok" ]