Skip to content

test: run the bunx symlink test on a clone of the executable on macOS - #43721

Open
robobun wants to merge 2 commits into
mainfrom
robobun/5cdc7386/bunx-symlink-test-private-copy
Open

robobun wants to merge 2 commits into
mainfrom
robobun/5cdc7386/bunx-symlink-test-private-copy

Conversation

@robobun

@robobun robobun commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • test/cli/bun.test.ts > "installs a bunx symlink to the executable, falling back through the install directories" fails its first attempt in 36 of the last 400 CI builds, only on macOS aarch64. expect(received).toBe(expected) at :287: Expected: ".../bun-darwin-aarch64-profile/bun-profile", Received: "/private/tmp/buntmp-.../bin/bun".
  • The test hard-links bunExe() into its temporary directory and expects the symlink target to be fs.realpathSync() of the link. On macOS that call sometimes returns the path of bunExe() itself.
  • The cause is fcntl(F_GETPATH) (src/sys/lib.rs:7529), which fs.realpathSync() uses on macOS. For a file with several hard links it reports the name that was looked up last. Other tests of the batch run bunExe() all the time.

Fix

  • On macOS the test runs a clone of the executable (fs.copyFileSync with COPYFILE_FICLONE). A clone has one name, so its realpath is stable.
  • Linux keeps the hardlink. There, realpath reads the name the file was opened with.
  • Verified on a macOS 15.7.9 CI host with a standalone copy of the test's first step, while a second process runs the original executable in a loop: the hardlink fails 6 of 150 runs with the CI message, the clone 0 of 150. On Linux test/cli/bun.test.ts passes.

Background

  • A hard link is a second name for the same file (inode). A clone is a new file that shares data blocks until one side is written, so on APFS it copies no data.
  • F_GETPATH builds a path from the name and parent that the kernel cached for the open file. A lookup through a different link replaces that cached name.
Notes

CI data. Last 400 finished builds (118369 to 119356, all branches): 36 builds, each time the retry passes, 33 on :mac: any aarch64 and 3 on :mac: 14 aarch64, always "1 retry". The 12 failing jobs I mapped to agents all ran on macOS 15 Tart guests (darwin-arm64-{ciabatta,sourdough,focaccia}-tart-15-*).

The primitive alone, macOS 15.7.9, bun 1.3.14. A second process runs the original executable in a loop. The script calls fs.realpathSync() 2000 times on a hardlink to that executable and 2000 times on a clone of it:

hardlink: { "<D>/link/bun": 1995, "/usr/local/bin/bun": 5 }
clone:    { "<D>/clone/bun": 2000 }   (st_nlink 1; the clone runs and reports "<D>/clone/bun" as its realpath)

Without the second process the hardlink resolves to its own path in 200 of 200 calls. macOS 26.6.1 shows the same race (157 of 3000 calls return the other name for a copy of /bin/echo).

The first step of the test, same host, same second process, 150 runs per variant (create the private executable, run <exe> completions, compare readlink(bin/bunx) with fs.realpathSync(exe)):

hardlink: 6 mismatches, first: expected "/usr/local/bin/bun", received "/private<D>/hardlink-E5ND8J/bin/bun"
clone:    0 mismatches

Not in this PR. fs.realpathSync() of a hard link can name a different link on macOS. Node's fs.realpathSync walks the path and returns the link that was passed. That is a runtime question with a cost trade-off (F_GETPATH is the fast path), so it is left for its own change.

copyFileSync on macOS already tries clonefile() first for a file over 128 KB (src/runtime/node/node_fs.rs:4877) and falls back to copyfile() when the volume cannot clone, so the flag states the intent and the fallback still works across devices.


no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/bun.test.ts

On macOS, fs.realpathSync() resolves a path through fcntl(F_GETPATH). For a
file with several hard links, F_GETPATH reports the name that was looked up
last. The test hard-linked bunExe() into its temporary directory. Other
tests of the same CI batch run bunExe() all the time, so the realpath of the
link was sometimes the path of bunExe() itself, and the assertion on the
symlink target failed.

A clone (COPYFILE_FICLONE) is a file of its own, so its realpath is stable.
Linux keeps the hardlink.
@robobun

robobun commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

  • Reproduced on a macOS 15.7.9 CI host (bun 1.3.14): a standalone copy of the test's first step, with a second process that runs the original executable in a loop. The hardlink variant fails 6 of 150 runs with the CI message (Expected is the original executable, Received is the link in the temporary directory). The clone variant fails 0 of 150.
  • The primitive alone: fs.realpathSync() of a hardlink to the running executable returns the other name in 5 of 2000 calls under the same load, and in 0 of 200 calls without it.
  • On Linux, test/cli/bun.test.ts passes with this change (39 pass, 2 todo).

@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Understand this PR’s impact

Explore downstream dependencies and potential security impact with Blast Radius.

View blast radius →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 6272c413-6dde-4cf0-82e7-43130ab3ea2e

📥 Commits

Reviewing files that changed from the base of the PR and between 6cbf919 and 926e786.

📒 Files selected for processing (1)
  • test/cli/bun.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.


Walkthrough

Changes

The completions test now detects macOS and creates its temporary executable with an APFS clone. Other platforms use a hardlink and fall back to copying when linking fails.

Executable preparation — test/cli/bun.test.ts

Layer / File(s) Summary
Platform-specific executable preparation
test/cli/bun.test.ts
The test imports isMacOS. macOS uses an APFS clone. Other platforms use a hardlink with a copy fallback.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: the macOS bunx symlink test now runs on a cloned executable instead of a hard link.
Description check ✅ Passed The description explains the problem, the platform-specific fix, and the verification performed. It does not use the exact template headings, but it provides the required information in equivalent sec…

Comment @coderabbitai help to get the list of available commands.

@claude claude 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.

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:40 PM PT - Sep 21st, 2026

✅ @robobun, your commit 381103d08effa8b54acf5171f30b716a2837b6f2 passed in Build #119435! 🎉


🧪   To try this PR locally:

bunx bun-pr 43721

That installs a local version of the PR into your bun-43721 executable, so you can run:

bun-43721 --bun

@robobun

robobun commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

The runtime fix is in #43760. With it, fs.realpathSync() of the hard link in this test returns the link: the helper walks the path when fcntl(F_GETPATH) names another link. The three new tests there fail on both macOS lanes without the fix and pass with it (builds 119604 and 119726).

If #43760 lands, this PR is not needed. The hard link in test/cli/bun.test.ts can stay as a natural check of the fix. #43760 also removes the test/cli/bun.test.ts entry from test/flaky-tests.txt. If #43760 does not land, this PR is still the way to stop the flake.

@robobun

robobun commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

#46423 moves the private executable setup of this test into a helper (bunxInstaller) and includes the macOS clone from this PR there, because it adds more bunx tests that run on macOS. If #46423 merges first, this PR is not needed. If this one merges first, #46423 needs a rebase of that helper.

This branch has not been deployed

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant