Just a few hours once more, 4 little checkmarks lit up subsequent to a commit in SwiftScript‘s GitHub Actions:
✓ build-macos
✓ build-ios
✓ build-linux
✓ build-windows
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 immediately.
I can not keep in mind what prompted me to such insanity: I simply wished to strive it and see if Opus may 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 mission in my catalogue that does that. SwiftBash and lots 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 in opposition to 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 submit is the CI facet of the story: what the workflow seems to be 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 high 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 specifically takes a couple of minutes from chilly cache, and stacking runs on high of one another wastes each wall-clock time and (if you happen to’re on a paid plan) minutes. The group key consists of 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 surroundings
variable:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
Set it on the workflow stage and each JavaScript-based motion runs underneath Node 24, no matter what the motion’s manifest claims. For those who 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 correct repair is for the motion authors to bump their runs.utilizing, however till everybody catches up, the env var is the seatbelt.)
macOS: the simple 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 continues to be macOS 14 or 15 — and couldn’t really run the assessments, 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 while you’re tuning. On macOS the entire job takes about 90 seconds.
iOS: wants an precise simulator
iOS is the platform the place you possibly can’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-Bundle
-destination 'platform=iOS Simulator,OS=newest,identify=iPhone 17'
-skipPackagePluginValidation
- identify: Check (iOS Simulator)
run: |
xcodebuild test-without-building
-scheme SwiftScript-Bundle
-destination 'platform=iOS Simulator,OS=newest,identify=iPhone 17'
-skipPackagePluginValidation
Just a few 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 stay.
Even when swift construct may produce an iOS binary, you couldn’t run it on macOS with out an iOS Simulator runtime, and solely Xcode is aware of how you can handle these. So xcodebuild it’s.
Which scheme? SwiftPM auto-generates an umbrella scheme referred to as PackageName-Bundle that incorporates each goal plus the check goal. The library scheme by itself (SwiftScriptInterpreter in our case) doesn’t have a check motion outlined. For those who level xcodebuild check on the library scheme you’ll get:
xcodebuild: error: Scheme SwiftScriptInterpreter just isn't at the moment configured
for the check motion.
Change to -scheme SwiftScript-Bundle and it simply works.
build-for-testing + test-without-building is the iOS analogue of swift construct --build-tests + swift check --skip-build. Identical two-step construction, separate timings within the UI, identical logical behaviour.
iOS provides about 60 seconds of simulator warm-up over the macOS time. So ~2.5 minutes complete. 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 offers you Swift 6.3 on Ubuntu 22.04 with all the pieces pre-installed. No setup steps, no apt faff, no toolchain set up. Run swift --version to substantiate and also you’re
already achieved.
The model pin issues greater than it seems to be. SwiftScript’s bridge generator extracts a “what’s out there 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 must be not 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 outdated. 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 subject: swift-syntax didn’t 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 quicker 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 steady on the motion’s inner cache and makes the set up genuinely deterministic.
The complete 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 high of the workflow actually earns its preserve right here.
Beneficial setup, condensed
For those who’re beginning a recent Swift package deal immediately and wish all 4 platforms inexperienced, right here’s the shortest model of the recipe that truly 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 |
identical | xcodebuild build-for-testing + xcodebuild test-without-building, scheme SwiftScript-Bundle, 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
Just a few guidelines of thumb that fall out of the desk:
- Pin Swift variations to a patch quantity on Home windows. Floating tags there price you cache hits and reproducibility.
- Don’t overthink Home windows caching. SwiftyLab’s installer is quick sufficient that
actions/cachefor.construct/has a poor price/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
for you.-Bundle
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 by way of 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 understanding how you can drive SwiftPM with the right SDK and how you can spin up a simulator to run the outcome. For those who’ve ever questioned 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 by way of 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 depend upon swift-nio — straight in SwiftMCP’s case, transitively by way of 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 “principally working” on Home windows, has been iterated on for half a 12 months, 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 completely on this single piece touchdown. The pitch is simply: extra downstream OSS would acquire Home windows help 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 help: the bug experiences, safety work, platform-specific edge circumstances. That’s a very honest 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 help upstream unblocks an unlimited fraction of the ecosystem directly.
For those who keep a swift-nio-using package deal and also you’d like Home windows help, please go say so on that PR. Maintainers reply to demand indicators like everybody else. The technical work has already been
achieved 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 may 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 submit…
Associated
Classes: Instruments
