Skip to content

Commit 9358e9d

Browse files
wan9chicodex
andcommitted
fix(fspy): track Bun file accesses on macOS
Co-authored-by: GPT-5 Codex <codex@openai.com>
1 parent 14d3c1e commit 9358e9d

9 files changed

Lines changed: 259 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ jobs:
128128
with:
129129
architecture: ${{ matrix.target == 'x86_64-apple-darwin' && 'x64' || '' }}
130130

131+
- name: Install Bun binary
132+
run: pnpm --filter vite-task-tools rebuild --pending
133+
131134
- name: Run ignored tests
132135
run: ${{ matrix.cargo_cmd }} test --target ${{ matrix.build_target }} -- --ignored
133136

@@ -256,6 +259,10 @@ jobs:
256259
- uses: oxc-project/setup-node@4c588e9266bd930b6ddc34307df0659ed511d187 # v1.3.1
257260
if: matrix.mode == 'ignored'
258261

262+
- name: Install Bun binary
263+
if: matrix.mode == 'ignored'
264+
run: pnpm --filter vite-task-tools rebuild --pending
265+
259266
- name: Run ignored tests
260267
if: matrix.mode == 'ignored'
261268
run: cargo-nextest nextest run --archive-file windows-tests.tar.zst --workspace-remap . --run-ignored ignored-only
@@ -308,6 +315,9 @@ jobs:
308315

309316
- uses: oxc-project/setup-node@4c588e9266bd930b6ddc34307df0659ed511d187 # v1.3.1
310317

318+
- name: Install Bun binary
319+
run: pnpm --filter vite-task-tools rebuild --pending
320+
311321
- name: Run ignored tests
312322
run: cargo test -- --ignored
313323

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Changelog
22

3+
- **Fixed** Automatic file-access tracking on macOS now observes files read, written, and listed by Bun, so changing those files causes affected tasks to rerun ([#532](https://github.com/voidzero-dev/vite-task/issues/532), [#542](https://github.com/voidzero-dev/vite-task/pull/542)).
34
- **Improved** Windows file-access tracking now uses sparse temporary backing files where supported, avoiding upfront allocation of the full backing file on disk ([#524](https://github.com/voidzero-dev/vite-task/pull/524)).
45
- **Fixed** Automatic file-access tracking on Linux now works in containers and Kubernetes runners with limited `/dev/shm` space ([#353](https://github.com/voidzero-dev/vite-task/issues/353), [#523](https://github.com/voidzero-dev/vite-task/pull/523)).
56
- **Fixed** Windows automatic input tracking now records executable image reads when process creation performs the lookup inside `NtCreateUserProcess` ([#518](https://github.com/voidzero-dev/vite-task/pull/518)).

crates/fspy/tests/node_fs.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ fn track_script(
5757
// excluded there. Node remains covered by the same tests on musl.
5858
#[test_case("node")]
5959
#[ignore = "requires node"]
60+
#[test_case("bun")]
61+
#[ignore = "requires node"]
6062
#[test_case("deno")]
6163
#[cfg(not(target_env = "musl"))]
6264
#[ignore = "requires node"]
@@ -68,6 +70,8 @@ fn read_sync(runtime: &str) -> anyhow::Result<()> {
6870

6971
#[test_case("node")]
7072
#[ignore = "requires node"]
73+
#[test_case("bun")]
74+
#[ignore = "requires node"]
7175
#[test_case("deno")]
7276
#[cfg(not(target_env = "musl"))]
7377
#[ignore = "requires node"]
@@ -79,6 +83,8 @@ fn exist_sync(runtime: &str) -> anyhow::Result<()> {
7983

8084
#[test_case("node")]
8185
#[ignore = "requires node"]
86+
#[test_case("bun")]
87+
#[ignore = "requires node"]
8288
#[test_case("deno")]
8389
#[cfg(not(target_env = "musl"))]
8490
#[ignore = "requires node"]
@@ -90,6 +96,8 @@ fn stat_sync(runtime: &str) -> anyhow::Result<()> {
9096

9197
#[test_case("node")]
9298
#[ignore = "requires node"]
99+
#[test_case("bun")]
100+
#[ignore = "requires node"]
93101
#[test_case("deno")]
94102
#[cfg(not(target_env = "musl"))]
95103
#[ignore = "requires node"]
@@ -105,6 +113,8 @@ fn create_read_stream(runtime: &str) -> anyhow::Result<()> {
105113

106114
#[test_case("node")]
107115
#[ignore = "requires node"]
116+
#[test_case("bun")]
117+
#[ignore = "requires node"]
108118
#[test_case("deno")]
109119
#[cfg(not(target_env = "musl"))]
110120
#[ignore = "requires node"]
@@ -122,6 +132,8 @@ fn create_write_stream(runtime: &str) -> anyhow::Result<()> {
122132

123133
#[test_case("node")]
124134
#[ignore = "requires node"]
135+
#[test_case("bun")]
136+
#[ignore = "requires node"]
125137
#[test_case("deno")]
126138
#[cfg(not(target_env = "musl"))]
127139
#[ignore = "requires node"]
@@ -139,6 +151,8 @@ fn write_sync(runtime: &str) -> anyhow::Result<()> {
139151

140152
#[test_case("node")]
141153
#[ignore = "requires node"]
154+
#[test_case("bun")]
155+
#[ignore = "requires node"]
142156
#[test_case("deno")]
143157
#[cfg(not(target_env = "musl"))]
144158
#[ignore = "requires node"]
@@ -150,6 +164,8 @@ fn read_dir_sync(runtime: &str) -> anyhow::Result<()> {
150164

151165
#[test_case("node")]
152166
#[ignore = "requires node"]
167+
#[test_case("bun")]
168+
#[ignore = "requires node"]
153169
#[test_case("deno")]
154170
#[cfg(not(target_env = "musl"))]
155171
#[ignore = "requires node"]

crates/fspy_preload_unix/src/interceptions/dirent.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ unsafe extern "C" fn scandir(
2626

2727
#[cfg(target_os = "macos")]
2828
mod macos_only {
29-
use super::{AccessMode, c_char, c_int, c_void, handle_open, intercept};
29+
use super::{AccessMode, Fd, c_char, c_int, c_void, handle_open, intercept};
30+
3031
intercept!(scandir_b: unsafe extern "C" fn (
3132
dirname: *const c_char,
3233
namelist: *mut c_void,
@@ -44,6 +45,19 @@ mod macos_only {
4445
// SAFETY: calling the original libc scandir_b() with the same arguments forwarded from the interposed function
4546
unsafe { scandir_b::original()(dirname, namelist, select, compar) }
4647
}
48+
49+
intercept!(__getdirentries64: unsafe extern "C" fn(c_int, *mut u8, usize, *mut i64) -> isize);
50+
unsafe extern "C" fn __getdirentries64(
51+
fd: c_int,
52+
buf: *mut u8,
53+
buf_len: usize,
54+
basep: *mut i64,
55+
) -> isize {
56+
// SAFETY: fd is a valid file descriptor provided by the caller of __getdirentries64
57+
unsafe { handle_open(Fd(fd), AccessMode::READ_DIR) };
58+
// SAFETY: calling the original libc __getdirentries64() with the same arguments forwarded from the interposed function
59+
unsafe { __getdirentries64::original()(fd, buf, buf_len, basep) }
60+
}
4761
}
4862

4963
intercept!(getdirentries(64): unsafe extern "C" fn (fd: c_int, buf: *mut c_char, nbytes: c_int, basep: *mut c_long) -> c_int);

crates/fspy_preload_unix/src/interceptions/open.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,45 @@ unsafe extern "C" fn openat(
6262
}
6363
}
6464

65+
#[cfg(target_os = "macos")]
66+
intercept!(open_nocancel: unsafe extern "C" fn(*const c_char, c_int, ...) -> c_int);
67+
#[cfg(target_os = "macos")]
68+
unsafe extern "C" fn open_nocancel(path: *const c_char, flags: c_int, mut args: ...) -> c_int {
69+
// SAFETY: path is a valid C string pointer provided by the caller of open$NOCANCEL
70+
unsafe { handle_open(path, OpenFlags(flags)) };
71+
if has_mode_arg(flags) {
72+
// SAFETY: O_CREAT requires a mode argument, matching the open$NOCANCEL contract
73+
let mode: Mode = unsafe { args.next_arg() };
74+
// SAFETY: calling the original libc open$NOCANCEL() with the same arguments forwarded from the interposed function
75+
unsafe { open_nocancel::original()(path, flags, mode) }
76+
} else {
77+
// SAFETY: calling the original libc open$NOCANCEL() with the same arguments forwarded from the interposed function
78+
unsafe { open_nocancel::original()(path, flags) }
79+
}
80+
}
81+
82+
#[cfg(target_os = "macos")]
83+
intercept!(openat_nocancel: unsafe extern "C" fn(c_int, *const c_char, c_int, ...) -> c_int);
84+
#[cfg(target_os = "macos")]
85+
unsafe extern "C" fn openat_nocancel(
86+
dirfd: c_int,
87+
path: *const c_char,
88+
flags: c_int,
89+
mut args: ...
90+
) -> c_int {
91+
// SAFETY: dirfd and path are valid arguments provided by the caller of openat$NOCANCEL
92+
unsafe { handle_open(PathAt(dirfd, path), OpenFlags(flags)) };
93+
if has_mode_arg(flags) {
94+
// SAFETY: O_CREAT requires a mode argument, matching the openat$NOCANCEL contract
95+
let mode: Mode = unsafe { args.next_arg() };
96+
// SAFETY: calling the original libc openat$NOCANCEL() with the same arguments forwarded from the interposed function
97+
unsafe { openat_nocancel::original()(dirfd, path, flags, mode) }
98+
} else {
99+
// SAFETY: calling the original libc openat$NOCANCEL() with the same arguments forwarded from the interposed function
100+
unsafe { openat_nocancel::original()(dirfd, path, flags) }
101+
}
102+
}
103+
65104
intercept!(fopen(64): unsafe extern "C" fn(path: *const c_char, mode: *const c_char) -> *mut FILE);
66105
unsafe extern "C" fn fopen(path: *const c_char, mode: *const c_char) -> *mut libc::FILE {
67106
// SAFETY: path and mode are valid C string pointers provided by the caller of the interposed function

crates/fspy_preload_unix/src/libc.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,20 @@ unsafe extern "C" {
2727
nbytes: c_int,
2828
basep: *mut c_long,
2929
) -> c_int;
30+
31+
#[cfg(target_os = "macos")]
32+
#[link_name = "open$NOCANCEL"]
33+
pub unsafe fn open_nocancel(path: *const c_char, flags: c_int, ...) -> c_int;
34+
35+
#[cfg(target_os = "macos")]
36+
#[link_name = "openat$NOCANCEL"]
37+
pub unsafe fn openat_nocancel(dirfd: c_int, path: *const c_char, flags: c_int, ...) -> c_int;
38+
39+
#[cfg(target_os = "macos")]
40+
pub unsafe fn __getdirentries64(
41+
fd: c_int,
42+
buf: *mut u8,
43+
buf_len: usize,
44+
basep: *mut i64,
45+
) -> isize;
3046
}

packages/tools/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"type": "module",
55
"dependencies": {
66
"@voidzero-dev/vite-task-client": "workspace:*",
7+
"bun": "catalog:",
78
"cross-env": "catalog:",
89
"deno": "catalog:",
910
"oxfmt": "catalog:",

0 commit comments

Comments
 (0)