Skip to content

feat(cmd): support iOS development on Linux via xtool#5

Open
ezynda3 wants to merge 4 commits into
stukennedy:mainfrom
mark3labs:feat/ios-linux-xtool
Open

feat(cmd): support iOS development on Linux via xtool#5
ezynda3 wants to merge 4 commits into
stukennedy:mainfrom
mark3labs:feat/ios-linux-xtool

Conversation

@ezynda3

@ezynda3 ezynda3 commented Jun 10, 2026

Copy link
Copy Markdown

Summary

This PR adds first-class iOS development on Linux. irgo build ios and irgo run ios now work on non-macOS hosts by cross-compiling with the xtool Darwin SDK and deploying to a physical USB/network-connected iOS device — no Mac required.

How it works

  1. cmd/irgo/apple_linux.go — builds build/ios/Irgo.xcframework on Linux. gomobile assumes an Xcode toolchain (xcrun / xcodebuild / lipo), so small generated shim scripts answer the handful of invocations gomobile actually makes, backed by the xtool Darwin SDK (~/.swiftpm/swift-sdks/darwin.artifactbundle, override with IRGO_DARWIN_SDK) plus the Swift toolchain's clang. Only the ios-arm64 device slice is built (there is no Simulator on Linux).
  2. __.SYMDEF fix — Go's c-archive output has no archive table of contents, and ld64.lld silently skips archive members without one (producing "undefined symbol" errors at app link time). Apple libtool -static from the SDK toolset rebuilds the framework archive with a proper TOC.
  3. SwiftPM/xtool app at ios/App — a new scaffold (Package.swift + xtool.yml + UIKit-only entry point) consumes the xcframework as a binary target. xtool dev run builds, signs, installs, and the CLI then best-effort launches it via xtool launch. UIKit-only on purpose: SwiftUI apps built against recent SDKs pull in SwiftUICore, which doesn't exist on devices running iOS < 18 and crashes at launch when cross-linked with ld64.lld. The existing Xcode project at ios/Example is untouched and remains the macOS path.
  4. Dev mode (irgo run ios --dev) — a physical device can't reach localhost, so the dev server URL is derived from the machine's LAN IP (override with IRGO_DEV_SERVER=http://<host>:8080); the WebView navigation policy allows the configured dev host.
  5. Native feel — viewport lock (meta tag + injected WKUserScript), no long-press link previews, no pinch/double-tap zoom, tap-highlight/touch-callout CSS reset in the scaffold.

macOS behavior is unchanged: the new code paths are only taken when runtime.GOOS != "darwin".

Other changes

  • Scaffold templates are now embedded with //go:embed all:templates so dotfiles (e.g. ios/App/.gitignore) reach generated projects.
  • New {{PROJECT_IDENT}} template substitution produces an Apple-provisioning-safe bundle ID fragment (alphanumerics only — the App Store Connect API rejects underscores in App ID names, found the hard way during device testing).
  • Repo .gitignore pattern irgo narrowed to /irgo so it only ignores the root binary instead of also hiding new files under cmd/irgo/.
  • CLI help, install-tools hints, scaffold .gitignore, and README updated.

Testing

  • go test ./cmd/irgo/ — new unit tests for the shim generation, SDK path mapping, xtool.yml parsing, and the identifier sanitizer.
  • End-to-end on Linux (NixOS, Swift 6.1 via swiftly, xtool with Darwin SDK installed):
    • irgo new <app> → scaffold includes ios/App with sanitized bundle ID
    • go build ./... in the scaffolded project ✅
    • irgo build iosbuild/ios/Irgo.xcframework built and synced to ios/App/Irgo.xcframework
    • irgo run ios → app built, signed, installed, and launched on a physical iPhone over USB

This is the same approach we shipped in starboard (the framework forked from irgo), ported back upstream.

ezynda3 added 4 commits June 10, 2026 15:11
- add cmd/irgo/apple_linux.go: build the iOS xcframework on non-macOS
  hosts using the xtool Darwin SDK, with generated xcrun/xcodebuild/lipo
  shims that satisfy gomobile's Xcode expectations
- fix gomobile c-archive output with Apple libtool -static so ld64.lld
  links the framework (Go archives lack a __.SYMDEF table of contents)
- route 'irgo build ios' / 'irgo run ios' to the xtool path on Linux:
  build, sign, install, and launch on a USB/network device
- scaffold a SwiftPM/xtool project at ios/App (UIKit-only entry point to
  avoid SwiftUICore dyld crashes on iOS < 18), mirrored to the framework
  reference tree
- dev mode uses the machine's LAN IP for the device-reachable dev server
  (override with IRGO_DEV_SERVER); allow the dev host in the WebView
  navigation policy
- lock WebView zoom for a native feel: viewport meta + injected
  WKUserScript, no link previews, tap-highlight/touch-callout CSS reset
- embed scaffold templates with 'all:' so dotfiles (ios/App/.gitignore)
  are included; add {{PROJECT_IDENT}} substitution producing an Apple
  provisioning-safe bundle ID fragment (alphanumerics only — the App
  Store Connect API rejects underscores in App ID names)
- narrow the repo .gitignore 'irgo' pattern to '/irgo' so it only
  ignores the root binary instead of hiding new files under cmd/irgo/
- update CLI help, install-tools hints, .gitignore, and README docs

Verified end-to-end on Linux: 'irgo new', 'go build ./...', and
'irgo run ios' building, signing, installing, and launching on a
physical iPhone over USB.
…ut.css

Mobile builds embed static assets into the Go framework at build time
(static/embed.go), so if static/css/output.css was never generated it is
permanently missing from the installed app and every page renders
unstyled. Found by deploying a freshly scaffolded project to a device
without running the documented 'bun install' step first.

- 'irgo new' now runs 'bun install && bun run css' (npm fallback) so
  scaffolded projects are styled and runnable out of the box; the
  "Next steps" hint is only printed when neither tool is available
- ensureMobileBuildSetup() warns when static/css/input.css exists but
  output.css does not, covering 'irgo build ios|android' and
  'irgo run ios|android' for existing projects
Rapidly re-triggering a Datastar action (e.g. mashing a button wired to
@get) makes Datastar abort the previous in-flight fetch. WebKit then
calls stop(_:) on the scheme handler for that task — but the handler
ignored it and, once the Go bridge call finished, still invoked
didReceive/didFinish on the dead task. WebKit raises
NSInternalInconsistencyException ("task has already been stopped") for
that, crashing the app.

Track live tasks in a main-thread-confined set (WebKit delivers both
start and stop on the main thread, and the completion is dispatched
there too, so no locking is needed). stop(_:) removes the task; the
completion silently drops the response if the task is no longer alive.

Also removes the unused mimeType local that produced a compiler warning.

Applied to all four copies of the handler: the SwiftPM app (ios/App),
the framework reference (ios/Irgo), and both scaffold templates.

Reproduced and verified on a physical iPhone: mashing "Trigger Burst"
in the scaffolded demo crashed the app before, survives now.
…ge conflicts

PR stukennedy#4 (android-end-to-end) introduces the same 'irgo new' improvements
this branch needs: //go:embed all:templates, the {{PROJECT_IDENT}}
substitution, sanitizeIdentifier, and the bun/npm + Tailwind CSS build
step. Make every overlapping hunk byte-identical to PR stukennedy#4's version so
git resolves them cleanly whichever PR merges first:

- adopt PR stukennedy#4's sanitizeIdentifier verbatim (Java/Kotlin package
  semantics: non-alphanumerics become underscores)
- adopt PR stukennedy#4's comment wording on the bun/tailwind install block
- keep the {{PROJECT_IDENT}} content substitution line identical
- adopt PR stukennedy#4's root .gitignore verbatim (anchored /irgo plus Android
  and mobile build outputs — all relevant here too)
- move the scaffold .gitignore iOS block below the Dependencies
  section, away from the spot where PR stukennedy#4 inserts its Android block
- replace runtime.GOOS checks in commands.go with an isDarwinHost()
  helper in apple_linux.go, so commands.go needs no new import at the
  line where PR stukennedy#4 adds "regexp"

Apple's provisioning API rejects underscores in App ID names, so the
iOS bundle ID can no longer ride on {{PROJECT_IDENT}}. Introduce a
separate {{BUNDLE_IDENT}} placeholder backed by sanitizeBundleIdent
(letters and digits only), which lives in apple_linux.go — a file PR stukennedy#4
does not touch. The substitution line is added after {{GO_VERSION}},
outside any region PR stukennedy#4 modifies.

Tests updated to match: TestSanitizeIdentifier now asserts PR stukennedy#4
semantics; TestSanitizeBundleIdent covers the Apple-safe variant.

Verified with a trial merge: main + PR stukennedy#4 + this branch now merges
with zero conflicts in either order.
@ezynda3

ezynda3 commented Jun 10, 2026

Copy link
Copy Markdown
Author

Heads-up for reviewers: the latest commit (cc5fdcd) aligns this branch's overlapping scaffold edits with #4 so the two PRs merge cleanly in either order — verified with trial merges (zero conflicts both ways).

Specifics:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant