HomeiOS Development4 Inexperienced Checkmarks: GitHub CI for macOS, iOS, Linux, and Home windows

4 Inexperienced Checkmarks: GitHub CI for macOS, iOS, Linux, and Home windows


A number of hours once more, 4 little checkmarks lit up subsequent to a commit in SwiftScript‘s GitHub Actions:

That’s a Swift package deal — written in Swift, relying on swift-syntax, exposing a Swift API — constructing and operating its full check suite on all 4 platforms Swift formally helps at the moment.

I can’t keep in mind what prompted me to such insanity: I simply needed to attempt it and see if Opus might get it to construct. I used to be able to abandon this try on the first signal of hassle. However then it succeeded earlier than even the Linux construct went inexperienced!

It’s the primary undertaking in my catalogue that does that. SwiftBash and plenty of others construct on three.
DTCoreText is Apple-only by definition. SwiftScript is the primary one the place making an attempt to construct for Home windows didn’t find yourself blowing up in my face.

Many of the work to get there had nothing to do with Home windows particularly. It was about taming the auto-generated Basis bridge the interpreter makes use of — which I’ve written about individually — so the identical supply tree compiles cleanly towards Apple’s Basis overlay, Linux’s swift-corelibs-foundation, and Home windows’ identical-to-Linux Basis construct. As soon as that landed, the CI itself was nearly an afterthought.
Virtually.

This put up is the CI facet of the story: what the workflow appears to be like like, why every platform wants the setup it has, and one bizarre env-var that quietly stops your runs from failing each different Tuesday.

The form of it

The entire workflow is one file: .github/workflows/swift.yml. 4 jobs, one per platform, every with a Construct step and a Check step:

identify: Swift
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

concurrency:
  group: swift-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build-macos:   ...
  build-ios:     ...
  build-linux:   ...
  build-windows: ...

Two issues on the prime earn their preserve earlier than any job even begins.

The concurrency block. With out cancel-in-progress: true, each
push spawns a recent run whereas the earlier one retains grinding away. Home windows particularly takes a couple of minutes from chilly cache, and stacking runs on prime of one another wastes each wall-clock time and (in case you’re on a paid plan) minutes. The group key contains the ref, so pushes to totally different branches don’t clobber one another — solely newer commits on the identical department do.

The Node.js env var. This one took me an embarrassing period of time to determine. As of the GitHub Actions runner picture rotation in spring 2026, Node 20 is being deprecated and Node 16 is gone. Some older actions nonetheless declare runs.utilizing: node16 of their motion.yml, and beginning round April the runner started erroring out on these actions as a substitute of warning. The escape hatch is one atmosphere
variable:

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

Set it on the workflow stage and each JavaScript-based motion runs below Node 24, no matter what the motion’s manifest claims. Should you inherited a workflow from earlier than April 2026 and it abruptly began failing on actions/checkout or comparable with a Node model error, that is what you need. (The right repair is for the motion authors to bump their runs.utilizing, however till everybody catches up, the env var is the seatbelt.)

macOS: the straightforward one

build-macos:
  runs-on: macos-26
  timeout-minutes: 20
  steps:
    - makes use of: actions/checkout@v6
    - identify: Choose Xcode 26.0
      makes use of: maxim-lobanov/setup-xcode@v1
      with:
        xcode-version: "26.0"
    - identify: Confirm Swift model
      run: swift --version
    - identify: Construct (macOS)
      run: swift construct --build-tests -v
    - identify: Check (macOS)
      run: swift check -v --skip-build

macos-26 is the brand new GitHub-hosted picture (launched in early 2026) that ships with macOS Tahoe 26 and Xcode 26. Till that runner confirmed up I used to be caught on macos-latest — which remains to be macOS 14 or 15 — and couldn’t truly run the checks, as a result of SwiftScript’s package deal declares .macOS("26.0") and the auto-generated Basis bridges name macOS-26-only APIs unconditionally. dyld would refuse to load thetest bundle on the older runner.

Now? swift construct --build-tests then swift check --skip-build. Splitting construct and check into two steps is only beauty — the Actions UI then exhibits you precisely the place the time goes, which is
useful once you’re tuning. On macOS the entire job takes about 90 seconds.

iOS: wants an precise simulator

iOS is the platform the place you’ll be able to’t get away with swift construct. Right here’s the job:

build-ios:
  runs-on: macos-26
  timeout-minutes: 20
  steps:
    - makes use of: actions/checkout@v6
    - identify: Choose Xcode 26.0
      makes use of: maxim-lobanov/setup-xcode@v1
      with:
        xcode-version: "26.0"
    - identify: Construct (iOS Simulator)
      run: |
        xcodebuild build-for-testing 
          -scheme SwiftScript-Package deal 
          -destination 'platform=iOS Simulator,OS=newest,identify=iPhone 17' 
          -skipPackagePluginValidation
    - identify: Check (iOS Simulator)
      run: |
        xcodebuild test-without-building 
          -scheme SwiftScript-Package deal 
          -destination 'platform=iOS Simulator,OS=newest,identify=iPhone 17' 
          -skipPackagePluginValidation

A number of traps to say.

Why xcodebuild and never swift construct? SwiftPM’s swift construct is host-only. There’s no --triple arm64-apple-ios flag in upstream SwiftPM. Cross-compiling to iOS requires the Xcode toolchain — that’s the place the SDK choice, simulator runtimes, and code signing reside.

Even when swift construct might produce an iOS binary, you couldn’t run it on macOS with out an iOS Simulator runtime, and solely Xcode is aware of the way to handle these. So xcodebuild it’s.

Which scheme? SwiftPM auto-generates an umbrella scheme referred to as PackageName-Package deal that accommodates each goal plus the check goal. The library scheme by itself (SwiftScriptInterpreter in our case) doesn’t have a check motion outlined. Should you level xcodebuild check on the library scheme you’ll get:

xcodebuild: error: Scheme SwiftScriptInterpreter isn't at the moment configured
for the check motion.

Swap to -scheme SwiftScript-Package deal and it simply works.

build-for-testing + test-without-building is the iOS analogue of swift construct --build-tests + swift check --skip-build. Similar two-step construction, separate timings within the UI, similar logical behaviour.

iOS provides about 60 seconds of simulator warm-up over the macOS time. So ~2.5 minutes whole. Not free, however not painful.

Linux: simply give me a container

build-linux:
  runs-on: ubuntu-latest
  timeout-minutes: 30
  container:
    picture: swift:6.3-jammy
  steps:
    - makes use of: actions/checkout@v6
    - identify: Confirm Swift model
      run: swift --version
    - identify: Construct (Linux)
      run: swift construct --build-tests -v
    - identify: Check (Linux)
      run: swift check -v --skip-build

The official swift:6.3-jammy Docker picture provides you Swift 6.3 on Ubuntu 22.04 with every little thing pre-installed. No setup steps, no apt faff, no toolchain set up. Run swift --version to substantiate and also you’re
already finished.

The model pin issues greater than it appears to be like. SwiftScript’s bridge generator extracts a “what’s accessible on the cross-platform facet” oracle from a checkout of swift-corelibs-foundation, which itself pulls in swift-foundation as a dependency. No matter revision of swift-foundation ships in your Linux toolchain needs to be no less than as new as what the oracle was generated from — in any other case you’ll get kind 'X' has no member 'Y' errors on perfectly-fresh-looking code. swift:6.0-jammy was too previous. swift:6.3-jammy traces up.

Linux finishes in about 3.5 minutes — slower than macOS due to container pull, however the entire swift construct --build-tests cycle is a clear chilly compile each time.

Home windows: the one everyone seems to be afraid of

That is the one I anticipated to be the rabbit gap. It wasn’t, in the long run, however there have been two false begins.

build-windows:
  runs-on: windows-latest
  timeout-minutes: 45
  steps:
    - makes use of: actions/checkout@v6
    - identify: Setup Swift
      makes use of: SwiftyLab/setup-swift@newest
      with:
        swift-version: "6.3.1"
    - identify: Confirm Swift model
      run: swift --version
    - identify: Construct (Home windows)
      run: swift construct --build-tests -v
    - identify: Check (Home windows)
      run: swift check -v --skip-build

The toolchain installer. I began with the long-time go-to, compnerd/gha-setup-swift.
It really works, however pinning to Swift 6.0.3 hit a now-known challenge: swift-syntax did not compile on the Home windows runner with cyclic dependency in module 'ucrt'. That’s a conflict between Swift’s ucrt module shim and the bundled MSVC headers, fastened in 6.3. The event snapshots that had the repair had been unreliable on the hosted runner — generally they’d set up, generally they’d 404.

Then I switched to SwiftyLab/setup-swift.

That is the unified macOS / Linux / Home windows installer that will get much less consideration than it deserves. Pinning to swift-version: "6.3.1" gave me a dependable set up in about 90 seconds, each time. No visual-studio-component dance, no cache configuration. (The motion’s README says toolchain caching is not supported on Home windows for Swift 5.10+, so I attempted including an actions/cache for .construct/. It didn’t assist sufficient to justify the additional step — set up + first compile is already sooner than the cache thrash.)

Patch-level pin issues. The primary time I had swift-version: "6.3" and the motion resolved that to a barely totally different snapshot between runs. Pinning the patch ("6.3.1") makes the toolchain
similar run-to-run, which retains the cache key secure on the motion’s inner cache and makes the set up genuinely deterministic.

The total Home windows job — toolchain set up, swift-syntax compile, each bridge file, plus swift check — runs in about 8 minutes from a chilly runner. The primary time it ran, it took fourteen. The cancel-in-progress block on the prime of the workflow actually earns its preserve right here.

Really helpful setup, condensed

Should you’re beginning a recent Swift package deal at the moment and wish all 4 platforms inexperienced, right here’s the shortest model of the recipe that really works in late April 2026:

Platform Runner Toolchain step Construct/check
macOS macos-26 maxim-lobanov/setup-xcode@v1 (Xcode 26) swift construct --build-tests + swift check --skip-build
iOS macos-26 similar xcodebuild build-for-testing + xcodebuild test-without-building, scheme SwiftScript-Package deal, simulator vacation spot
Linux ubuntu-latest + container: swift:6.3-jammy none (image-provided) swift construct --build-tests + swift check --skip-build
Home windows windows-latest SwiftyLab/setup-swift@newest, swift-version: "6.3.1" swift construct --build-tests + swift check --skip-build

Plus the 2 workflow-level helpers:

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

concurrency:
  group: swift-${{ github.ref }}
  cancel-in-progress: true

A number of guidelines of thumb that fall out of the desk:

  • Pin Swift variations to a patch quantity on Home windows. Floating tags there value you cache hits and reproducibility.
  • Don’t overthink Home windows caching. SwiftyLab’s installer is quick sufficient that actions/cache for .construct/ has a poor value/profit ratio. The primary commit’s run is your sincere cold-start time.
  • Cut up construct and check. The 2-step sample matches throughout all 4 platforms and provides you exact timings within the UI with out altering semantics.
  • Use the SwiftPM umbrella scheme on iOS. Don’t waste time configuring a customized check goal in Xcode — SwiftPM already generates -Package deal for you.

The one bizarre factor about Apple platforms

Discover that macOS and iOS each run on macos-26, however solely macOS makes use of swift construct. iOS goes via xcodebuild. That’s not a workflow alternative — it’s a SwiftPM limitation. SwiftPM compiles for the host platform and solely the host platform. On a Mac runner the host is macOS. There’s no swift construct --triple arm64-apple-ios as a result of there’s no host that is iOS.

Xcode papers over this by figuring out the way to drive SwiftPM with the proper SDK and the way to spin up a simulator to run the consequence. Should you’ve ever puzzled why xcodebuild exists alongside swift construct,
that is the second that solutions it. On Linux and Home windows the host is the deployment goal, so swift construct is sufficient. On non-Mac Apple platforms (iOS, watchOS, tvOS, visionOS), you cross-
compile via Xcode, full cease.

The Home windows-on-other-projects downside

Getting Home windows inexperienced on SwiftScript emboldened me to take a look at my different OSS packages and ask: how a lot additional would I get if I simply dropped the identical workflow into SwiftMCP and SwiftMail?

Not very far, because it seems. Each of these rely on swift-nio — straight in SwiftMCP’s case, transitively via swift-nio-imap in SwiftMail’s — and swift-nio doesn’t but construct on Home windows.

There’s been an open PR for that since November 2025: It will get TCP servers “largely working” on Home windows, has been iterated on for half a yr, and has been ready on assessment.

I posted on the PR this morning so as to add my voice — not as somebody who can assessment the networking internals (I can’t), however as somebody with a number of shipped packages whose Home windows story is gated solely on this single piece touchdown. The pitch is simply: extra downstream OSS would achieve Home windows assist in a single day if this merges.

What’s subsequent

The rumor I’ve heard — and I need to flag it as precisely that, a hearsay — is that the maintainers are reluctant to tackle the long-term burden of Home windows assist: the bug studies, safety work, platform-specific edge circumstances. That’s a totally truthful concern; each new platform a maintainer accepts is a everlasting dedication. However the flip facet is that somebody within the Swift ecosystem has to soak up that work for cross-platform Swift to be actual, and swift-nio is the inspiration for a lot community code that Home windows assist upstream unblocks an infinite fraction of the ecosystem without delay.

Should you keep a swift-nio-using package deal and also you’d like Home windows assist, please go say so on that PR. Maintainers reply to demand indicators like everybody else. The technical work has already been
finished by Joannis; what’s lacking is the institutional urge for food to merge and personal it.

After which there’s Android. I assume now that the Home windows spell has been damaged, I might look into that subsequent, simply to see if it builds too.

And about SwiftScript, I’ll let you know much more about this within the subsequent weblog put up…


Classes: Instruments

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments