Files
nucleic/scripts/build-tailscalekit.sh
T
abkslmandClaude Fable 5 61e11572b8 Add Tailscale (Tailnet) as a sync transport between Mac and iPhone
Settings ▸ Remote gains a "Connect via" picker — LAN (default), Tailscale
(tailnet), or Relay (disabled, coming soon). On Tailnet, both devices run an
embedded tsnet node via TailscaleKit (tailscale/libtailscale) and sync frames
flow over the user's tailnet, so the phone can connect from anywhere the
tailnet reaches; Noise E2EE runs above the transport unchanged.

- NucleicTailnet (new target, macOS + iOS): TailnetNode wraps TailscaleKit's
  node lifecycle (auth-key login, generation-fenced start/stop since up() is
  un-cancellable) and drops to the framework's public C API for the data path
  — tailscale_dial/listen/accept hand back full-duplex socketpair fds, wrapped
  by FDFrameChannel (DispatchIO) into the shared FrameChannel seam. The Swift
  wrapper's one-way connection actors can't carry a bidirectional stream.
- Host: TailnetListener adopts SyncListener; startSyncServer is single-flight
  and honors toggle-off/picker changes at the commit point; pairing QRs carry
  transport + tailnet IP/port hints (PairingPayload additive optional fields,
  forward/backward compatible over CBOR).
- iPhone: pair/reconnect dial over whichever transport the pairing recorded;
  Settings gains a Tailscale auth-key field (Keychain, committed on editing
  end); connectivity chip shows "Connected · Tailnet".
- TailscaleKit has no SwiftPM distribution: scripts/build-tailscalekit.sh
  builds a pinned libtailscale commit into an untracked local xcframework;
  Package.swift links it only when present (everything builds without it, the
  picker then reports Tailscale support as not built in), and the script
  clears SwiftPM's content-keyed manifest cache so the toggle is picked up.
- iOS floor 17.0 → 18.1 (TailscaleKit requires the iOS 18 Swift runtime);
  package-app.sh embeds the framework in the .app like Sparkle.

703-test suite: no new failures (the 7 fake-claude/fake-grok staging issues
reproduce identically on an untouched checkout — pre-existing, tracked
separately). New coverage: FDFrameChannel over socketpairs, pairing-payload
version-skew both directions, transport-setting resolution.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 03:50:45 -07:00

71 lines
3.4 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Build the vendored TailscaleKit.xcframework the Tailnet sync transport links against.
#
# scripts/build-tailscalekit.sh
#
# TailscaleKit (github.com/tailscale/libtailscale, swift/) has no SwiftPM package and no
# tagged releases — it's an Xcode framework project wrapping a Go c-archive (tsnet), so we
# build it from a pinned commit and stage the result as a local binary target:
#
# third_party/TailscaleKit/TailscaleKit.xcframework (macOS + iOS + iOS-simulator slices)
#
# Package.swift picks the xcframework up automatically when it exists (see the
# `tailscaleKitAvailable` conditional there); without it every target still builds — the
# Tailnet transport just reports "not built in" at runtime. Like Resources/vmlinux-arm64,
# the artifact is a large binary and is NOT tracked; re-run this script after a clean clone.
#
# Requirements: Xcode (16.1+) and a Go toolchain (go.mod wants 1.25; any Go >= 1.21 will
# auto-download the right toolchain via GOTOOLCHAIN=auto).
set -euo pipefail
# Pinned libtailscale commit (main @ 2026-02-28, tailscale.com v1.94.1). Bump deliberately:
# the Swift wrapper API is pre-1.0 and the umbrella header mirrors the C API by hand.
LIBTAILSCALE_COMMIT="5e89501def80a6579ca5d0f9a02f336be62b8f2e"
LIBTAILSCALE_URL="https://github.com/tailscale/libtailscale.git"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
STAGE="$ROOT/third_party/TailscaleKit"
CHECKOUT="$STAGE/.build/libtailscale"
OUT="$STAGE/TailscaleKit.xcframework"
command -v go >/dev/null || { echo "error: Go toolchain required (brew install go)" >&2; exit 1; }
mkdir -p "$STAGE/.build"
if [[ ! -d "$CHECKOUT/.git" ]]; then
git clone "$LIBTAILSCALE_URL" "$CHECKOUT"
fi
git -C "$CHECKOUT" fetch --quiet origin "$LIBTAILSCALE_COMMIT" 2>/dev/null || git -C "$CHECKOUT" fetch --quiet origin
git -C "$CHECKOUT" checkout --quiet "$LIBTAILSCALE_COMMIT"
# Their swift/Makefile drives everything: the Go c-archive per platform (via the root
# Makefile), then xcodebuild for each framework slice. Unsigned by design; the app build
# signs on embed. Their Makefile pipes xcodebuild through xcpretty/cat (masking failures),
# so start from a clean slate and verify every slice actually exists afterwards — never
# package stale products from an earlier run.
rm -rf "$CHECKOUT/swift/build"
make -C "$CHECKOUT/swift" macos ios ios-sim
PRODUCTS="$CHECKOUT/swift/build/Build/Products"
for slice in Release Release-iphoneos Release-iphonesimulator; do
[[ -e "$PRODUCTS/$slice/TailscaleKit.framework/TailscaleKit" ]] || {
echo "error: missing $slice slice — xcodebuild failed inside libtailscale's Makefile (see output above)" >&2
exit 1
}
done
rm -rf "$OUT"
xcodebuild -create-xcframework \
-framework "$PRODUCTS/Release/TailscaleKit.framework" \
-framework "$PRODUCTS/Release-iphoneos/TailscaleKit.framework" \
-framework "$PRODUCTS/Release-iphonesimulator/TailscaleKit.framework" \
-output "$OUT"
# SwiftPM caches evaluated manifests by CONTENT, so an unchanged Package.swift keeps the
# pre-artifact "TailscaleKit unavailable" evaluation forever — the next build would silently
# skip the framework. Drop the manifest cache so the availability check re-runs.
rm -rf ~/Library/Caches/org.swift.swiftpm/manifests
echo "✓ $OUT ($(du -sh "$OUT" | cut -f1), libtailscale @ ${LIBTAILSCALE_COMMIT:0:12})"
echo " (if Xcode has the iOS project open, File ▸ Packages ▸ Reset Package Caches once)"