Skip to content

Xcode 26.3 + xcodebuild CLI: builtin-SwiftDriver bypasses swift-frontend wrapper via swiftc symlink redirect #147

@Condo97

Description

@Condo97

Summary

On Xcode 26.3 + InjectionNext 2.0.0RC5, hot-reload from a pure xcodebuild-CLI workflow is broken at an architectural level. Even with Patch Compiler correctly applied AND all three documented build flags set, the feedcommands call inside the swift-frontend wrapper is never reached, so the compile-command cache (/tmp/InjectionNext_iPhoneSimulator_builds.json.gz) never updates for the current build. Saves never produce /tmp/injectionNext_*.o and the RC5 console reports Could not locate command for <file>. InjectionNext is not compatible with "Whole Module" Compilation Mode. (the warning text is misleading — SWIFT_COMPILATION_MODE is singlefile).

Environment

  • macOS 26.x (Darwin 25.3.0)
  • Xcode 26.3 (post-Feb-20 toolchain ship)
  • InjectionNext 2.0.0RC5 (build 14492)
  • Inject SPM 1.6.0 (@ObserveInjection + .enableInjection())
  • Workflow: xcodeless — builds via xcodebuild CLI, app launched on simulator with SIMCTL_CHILD_DYLD_INSERT_LIBRARIES pointing at iOSInjection.bundle.
  • Project: large iOS workspace with CocoaPods + SPM. SWIFT_ENABLE_EXPLICIT_MODULES=YES (required by targets).

Reproduction

  1. Apply "Patch Compiler" via RC5 menu → Settings → Compiler. Confirm "Compiler State: Intercepted".
  2. Verify on disk:
    ls -la "/Applications/Xcode 26.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend"*
    Yields:
    -rwxr-xr-x  swift-frontend       (374-byte bash wrapper)
    -rwxr-xr-x  swift-frontend.save  (169 MB original binary)
    
  3. Truncate /tmp/feedcommands.log and note the mtime/size of /tmp/InjectionNext_iPhoneSimulator_builds.json.gz.
  4. Run a clean xcodebuild with all three flags:
    xcodebuild -workspace … -scheme … -configuration Debug \
      -destination "platform=iOS Simulator,id=…" \
      EMIT_FRONTEND_COMMAND_LINES=YES \
      COMPILATION_CACHE_ENABLE_CACHING=NO \
      CLANG_CACHE_FINE_GRAINED_OUTPUTS=NO \
      build
    
  5. Build succeeds. 513 swift-frontend command lines emitted to log, ~358 of them -frontend -c -primary-file …. swift-frontend.save subprocesses are observable in ps during build.
  6. Result: json.gz mtime DID NOT advance. New .swift files (e.g. a freshly-created MyView.swift) are NOT in the cache. Saving any file produces no /tmp/injectionNext_*.o. RC5 console shows the "Could not locate command" warning.

Root cause (verified empirically)

RC5's "Patch Compiler" performs three actions:

  1. Renames swift-frontend (binary) → swift-frontend.save.
  2. Installs swift-frontend.sh as new swift-frontend.
  3. Re-points the swift- symlinks (swift, swiftc, swift-api-digester, swift-cache-tool, swift-symbolgraph-extract) to → swift-frontend.save* (the original), deliberately bypassing the wrapper.

Confirmed by ls -la:

swiftc -> swift-frontend.save
swift  -> swift-frontend.save
swift-api-digester -> swift-frontend.save
swift-cache-tool   -> swift-frontend.save
swift-symbolgraph-extract -> swift-frontend.save
swift-synthesize-interface -> swift-frontend

(That last one is the only symlink still routing through the wrapper.)

This was sensible pre-Xcode-26 when Xcode invoked swift-frontend directly. But Xcode 26.3 uses builtin-SwiftDriver, which invokes swiftc directly. Through the symlink, that resolves straight to swift-frontend.savethe wrapper is never executed and feedcommands is never called. The compile cache never learns about the current build.

The xcactivitylog fallback path in InjectionLite/Sources/InjectionLite/LogParser.swift also fails on this configuration: xcodebuild from CLI on Xcode 26.3 emits a ~479-byte effectively-empty xcactivitylog, with no -primary-file lines and no builtin-Swift-Compilation lines that the v26.3 fallback grep can match.

What I tried (didn't work)

  • SWIFT_USE_INTEGRATED_DRIVER=NO — Xcode rejects it on this project: "Enabling Swift explicit modules also requires: SWIFT_USE_INTEGRATED_DRIVER". Targets require explicit modules; circular dependency.
  • Setting all three documented flags (COMPILATION_CACHE_ENABLE_CACHING=NO, CLANG_CACHE_FINE_GRAINED_OUTPUTS=NO, EMIT_FRONTEND_COMMAND_LINES=YES) — flags applied successfully but the wrapper still isn't reached because the bypass is at symlink-resolution time, not at cache time.
  • Re-applying Patch Compiler (multiple times) — the wrapper is correctly installed each time; the symlink-bypass is by design.
  • Downgrading to InjectionNext 1.6.0 — same compile-discovery code (PR Xcode26.3 #125 shipped in both), so same gap.

feedcommands.log is not a useful diagnostic for this issue, by the way — feedcommands is silent on both success and most failure paths. The reliable diagnostic is /tmp/InjectionNext_iPhoneSimulator_builds.json.gz mtime: if it doesn't advance after a build, the wrapper isn't reaching feedcommands.

Question for maintainers

What's the intended xcodeless flow on Xcode 26.3? Specifically:

  1. Is the swiftc → swift-frontend.save symlink redirect intentional on Xcode 26+? If so, how is feedcommands supposed to capture commands when builtin-SwiftDriver invokes swiftc instead of swift-frontend?
  2. Would re-pointing swiftc → swift-frontend (the wrapper) and creating a swiftc.save → swift-frontend.save symlink be a viable patch shape, or does it break driver-mode invocations? (Specifically for cases where swiftc is invoked without -frontend.)
  3. Is the LogParser.swift xCode26_3 fallback expected to handle CLI xcodebuild builds? Empirically the xcactivitylog from CLI builds is ~479 bytes with no usable per-file lines — happy to attach a sample if useful.

I'm happy to test patch candidates on this project — it's a real-world large workspace (CocoaPods + SPM, ~500 swift-frontend invocations per clean build) and reliably reproduces the gap. cc @maatheusgois-dd @johnno1962

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions