Skip to content

fix(apple): fall back to the default Xcode past a dangling selection link - #4146

Merged
thymikee merged 4 commits into
mainfrom
fix/xcode-dangling-selection-link-4145
Nov 2, 2026
Merged

thymikee merged 4 commits into
mainfrom
fix/xcode-dangling-selection-link-4145

Conversation

@thymikee

@thymikee thymikee commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Summary

Fixes #4145. In 0.21.34+, iOS input fails with apple_toolchain_unreadable when /var/db/xcode_select_link points at a deleted Xcode, even though xcode-select -p and xcrun resolve /Applications/Xcode.app.

How it regressed: #3995 stopped running xcrun to build the runner cache key and reads Xcode's files instead. To do that it copied Apple's developer-dir resolution: DEVELOPER_DIR, then the selection link, then the default Xcode. One rule did not match Apple: a dangling link was treated as a hard failure. Apple's tools skip it and use the default Xcode. A test (a selection link whose Xcode is gone fails …) locked in that assumption, so nothing caught it.

Fix: if the link's target is missing (ENOENT), the link is treated as absent, just like a missing link, and resolution uses the default Xcode. Any other unreadable link, such as EACCES on the link or its target, still fails. That keeps the #3995 guarantee: an unreadable selection never silently stands in for another Xcode.

Prevention:

  • The resolver doc now says the order must match xcode-select -p, because the cache key must name the Xcode that builds the runner.
  • The incorrect test is replaced with one that pins Apple's behavior: a dangling link falls back to the default Xcode, for both the cache key and the failure-report version.
  • New tests cover the default Xcode with no link, and the closest negative: a link target that cannot be read fails and does not fall back.
  • The test fixture now stubs the host's default Xcode path as well as the selection link. Cases no longer depend on whatever Xcode the machine has installed.

Validation

  • pnpm check:affected --run --head 031c78fc13: all runnable checks passed, exactCommit=true (pre-squash head, same tree as the pushed commit).
  • The new fallback test fails on the old code (COMMAND_FAILED, dangling selection link) and passes with the fix.
  • Independent review: it found the first draft's "no default Xcode" case was host-dependent and that realPath swallowed EACCES. Both are fixed: that case is dropped and an EACCES negative is added.
  • Pre-existing flake, not from this change: packages/apple-runner/src/__tests__/index-recovery-wiring.test.ts fails 39 tests together in about 1 of 6 isolated runs at base 32bf98def5.
  • Not verified on a live device: this host has a valid selection link, and making a dangling /var/db/xcode_select_link needs root. The normal path (link resolves) is unchanged.

View guided diff Turn on auto-fix

…link

xcode-select -p and xcrun skip a /var/db/xcode_select_link whose target was
deleted and use /Applications/Xcode.app. #3995 replaced the xcrun probe with a
file reader that failed with apple_toolchain_unreadable instead, which blocked
iOS input on hosts with a stale link from an uninstalled Xcode. Any other
unreadable link still fails.

Fixes #4145
@vercel

vercel Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agent-device-website Ready Ready Preview Nov 1, 2026 9:19pm UTC

Request Review

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.65 MB 5.65 MB +107 B
Package (unpacked) 5.65 MB 5.65 MB +107 B
Package (download) 1.72 MB 1.72 MB +39 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 36.1 ms 35.0 ms -1.1 ms
CLI --help 76.7 ms 74.8 ms -1.9 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@apex-sentinel-staging

apex-sentinel-staging Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Apex Sentinel

Verdict: approve

Since the last review: 4 new · 0 still open · 4 no longer raised

4 finding(s): 1 minor, 3 nit. Verdict: approve. reached max rounds (1).

🟡 Minor (1)

Where Finding
packages/apple-runner/src/runner-toolchain-identity.ts:46 🆕 DEFAULT_DEVELOPER_DIR is exported only so the test fixture can stub it
🔵 Nit (3)
Where Finding
packages/apple-runner/src/runner-toolchain-identity.ts:49 🆕 Dangling-link fallback is hardcoded in the hint instead of the exported constant
packages/apple-runner/src/runner-toolchain-identity.ts:122 🆕 Dangling link with no default Xcode now fails naming /Applications/Xcode.app, hiding the real cause
packages/apple-runner/src/runner-toolchain-identity.ts:128 🆕 The existence-probe result in selectionLinkTarget is discarded without explanation

Reply under an inline comment with @apex-sentinel-staging note ... or @apex-sentinel-staging pivot ... to re-check that finding.
Comment @apex-sentinel-staging review on this conversation to run a full review.

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 3 new findings on 031c78f. The verdict and the full list are in the summary comment.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

I read the change at 031c78f, and the fix itself looks right. The fallback past a dangling selection link works as described, and permission errors still fail loudly. CI is green with 19 checks and none failing, and the index-recovery-wiring flake you reported does not touch the toolchain identity resolver. There are no conflicts. One open thread, the fixture default dir below, should be fixed before merge; the other notes are optional.

Not blocking, and you can take or leave these: the fixture at apple-toolchain-fixtures.ts#L5 redeclares the default developer dir as a literal, so exporting DEFAULT_DEVELOPER_DIR from runner-toolchain-identity.ts next to XCODE_SELECT_LINK_PATH would keep the stub in step with production. The fallback at runner-toolchain-identity.ts#L142 only covers ENOENT. A link target that fails with ENOTDIR or ELOOP still throws apple_toolchain_unreadable, and I did not check how Apple treats those, so add ENOTDIR to the same check only if Apple is shown to skip it.

These review threads still apply: the fixture default dir thread is a real gap (with defaultDeveloperDir undefined the stub does not hide the host Xcode), though no current test reaches it; the root-safe EACCES test thread is valid because chmodSync(0o000) does not stop a root process, and mocking realpathSync like the readlinkSync test would fix it; and one lower-priority thread on the error hint still applies. The thread on reusing the realPath helper does not apply, because that helper swallows every error and would bring back the EACCES masking this PR removes, so you can resolve it.

I did not run the tests, including the root-user and EACCES cases, and I did not reproduce the flake. I did no live run, since a dangling /var/db/xcode_select_link needs root and the route is a pure file read. I also did not check whether xcode-select -p falls back to CommandLineTools when no default Xcode exists. The resolver does not model that, and the Xcode version file read fails loudly in that case.

…ds behind it

Report danglingSelectionLink and an xcode-select hint when the fallback
default Xcode is missing too. Share one ENOENT/unreadable mapping for both
selection-link reads. Keep the fixture hermetic against the host's default
Xcode, and raise EACCES through the fixture so the case holds as root.
@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Addressed the review in b08ccb2:

  • The fixture's default Xcode no longer reads the host's /Applications/Xcode.app, and DEFAULT_DEVELOPER_DIR is exported from production.
  • The EACCES case is root-safe: the error comes from the stubbed realpathSync, not from chmod.
  • A dangling link with no default Xcode now reports danglingSelectionLink and an xcode-select -s hint.
  • Both selection-link reads share one ENOENT/unreadable mapping. realPath is unchanged, for the reason in its thread.
  • ENOTDIR/ELOOP are unchanged. I found no evidence that Apple skips them, so they still fail.

Validation: pnpm check:affected --run --head b08ccb28ba, exactCommit=true. Format, lint, typecheck, layering, fallow, build, and vitest-related passed. Unit had one failure, scripts/lib/recorded-daemon.test.ts > a timeout rejects without retiring the daemon. This change does not touch that test, and it passed 2 of 2 times in isolation.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 4 new findings on b08ccb2. The verdict and the full list are in the summary comment.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Thanks for the update. At b08ccb2 the earlier findings are fixed: the tests no longer read the host Xcode, and the EACCES case no longer depends on chmod or on running as non-root. One open thread, on the dangling-link hint wording below, still applies and should be settled before merge. I did not run the tests. I traced the mocked fs calls against the resolver at this commit. I also did not run against a real dangling /var/db/xcode_select_link, which needs root.

Not blocking, and fine to take or leave: the dangling-link hint at https://github.com/callstack/agent-device/blob/b08ccb2/packages/apple-runner/src/runner-toolchain-identity.ts#L52 says no Xcode is installed at the default path, but it is chosen from the link state alone. If the default Xcode exists but lacks the requested SDK platform, or its version.plist or SDKSettings.json is unreadable, the message tells the user the wrong thing. Also, danglingSelectionLink is a boolean on SelectedDeveloperDir but the link path in the error details (https://github.com/callstack/agent-device/blob/b08ccb2/packages/apple-runner/src/runner-toolchain-identity.ts#L108), and only the two tests read that detail.

Could we drop the diagnostic layer this change added (SelectedDeveloperDir, the SelectionLink union, the flag, the detail, DANGLING_SELECTION_LINK_HINT and its two tests)? resolveSelectedDeveloperDir could return a string again, with dangling collapsing to undefined, plus one sentence in TOOLCHAIN_FILE_HINT: if /var/db/xcode_select_link names a deleted Xcode, run sudo xcode-select -s <Xcode.app>. That hint is true in every state, and it removes the need for the wording and naming fixes above.

Of the other open threads, the one on the hint wording (#4146 (comment)) still applies, along with the matching wording thread (#4146 (comment)) and the same-name nit (#4146 (comment)). These are fixed at this head, so please resolve them: the fixture reading the host Xcode (#4146 (comment)), the EACCES test under root (#4146 (comment)), the missing dangling-link hint (#4146 (comment)) and the swallowed realPath errors (#4146 (comment)). These do not apply, because the fallback to the Xcode that xcode-select -p resolves is intended and the helper comment states only its contract: #4146 (comment) and #4146 (comment).

Smoke Tests, Repo Guards & Compatibility and Coverage are still running on this commit, and the earlier commit passed all 19 checks. No conflicts. Once those three jobs pass, nothing else stands in the way of merging.

The dangling-link hint asserted a cause the resolver never checked. Return the
developer dir as a string again and add the xcode-select -s step to the hint
that every toolchain-read failure carries.
@thymikee

thymikee commented Nov 1, 2026 •

Copy link
Copy Markdown
Member Author

Addressed in 2d51640: I removed the dangling-link diagnostic layer as suggested. resolveSelectedDeveloperDir returns a string again, and TOOLCHAIN_FILE_HINT has one more sentence about sudo xcode-select -s. I replied to all open threads and resolved them.

Validation: pnpm check:affected --run --head 2d51640bf1, exactCommit=true. Format, lint, typecheck, layering, fallow, build, and vitest-related passed. Unit had one failure, packages/provider-revyl/src/runtime.test.ts > a lease start waits for the worker and the device, and reports the rented device. It is intermittent in isolation (2 of 5 runs fail) at this PR's base 32bf98d. This PR does not touch packages/provider-revyl, so the failure is not from this change.

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 3 new findings on 2d51640. The verdict and the full list are in the summary comment.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

I found no problems in 8b13b82. The findings from the earlier review (#4146 (comment)) are fixed: the hint no longer asserts a cause, and the dangling-link field and name collision are gone.

CI is green: 18 checks, none failing. There are no conflicts. The earlier unrelated provider-revyl flakes in your local check:affected run did not show up on this head.

I did not run the tests locally. I traced the fixture's fs mocks against the resolver at this head and relied on the green checks. I also did not see a live run with a real dangling /var/db/xcode_select_link, which needs root, as the PR body says. The one changed branch, the ENOENT mapping, is exercised by the fixture with a real ENOENT from realpathSync. I did not verify Apple's behavior for ELOOP or ENOTDIR on the link target, so those stay a hard failure by your choice.

The bot threads are settled, so you can resolve them. These are fixed at this head: #4146 (comment), #4146 (comment), #4146 (comment), #4146 (comment) and #4146 (comment). These are accepted as intended behavior: #4146 (comment) (skipping a dangling link matches xcode-select -p), #4146 (comment) (DEFAULT_DEVELOPER_DIR is an exported constant, like XCODE_SELECT_LINK_PATH) and #4146 (comment) (naming the link was the diagnostic layer the maintainer asked to drop).

Nothing else stands in the way. This is ready to merge once the maintainer is satisfied the open conversation is settled.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 1, 2026
@thymikee
thymikee merged commit 3cd64f5 into main Nov 2, 2026
18 checks passed
@thymikee
thymikee deleted the fix/xcode-dangling-selection-link-4145 branch November 2, 2026 06:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS input fails in 0.21.34+ when Apple tools resolve past a stale Xcode selection symlink

1 participant