Skip to content

Commit a2a75a0

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

8 files changed

Lines changed: 249 additions & 1 deletion

File tree

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
@@ -55,6 +55,8 @@ fn track_script(
5555

5656
#[test_case("node")]
5757
#[ignore = "requires node"]
58+
#[test_case("bun")]
59+
#[ignore = "requires node"]
5860
#[test_case("deno")]
5961
#[ignore = "requires node"]
6062
fn read_sync(runtime: &str) -> anyhow::Result<()> {
@@ -65,6 +67,8 @@ fn read_sync(runtime: &str) -> anyhow::Result<()> {
6567

6668
#[test_case("node")]
6769
#[ignore = "requires node"]
70+
#[test_case("bun")]
71+
#[ignore = "requires node"]
6872
#[test_case("deno")]
6973
#[ignore = "requires node"]
7074
fn exist_sync(runtime: &str) -> anyhow::Result<()> {
@@ -75,6 +79,8 @@ fn exist_sync(runtime: &str) -> anyhow::Result<()> {
7579

7680
#[test_case("node")]
7781
#[ignore = "requires node"]
82+
#[test_case("bun")]
83+
#[ignore = "requires node"]
7884
#[test_case("deno")]
7985
#[ignore = "requires node"]
8086
fn stat_sync(runtime: &str) -> anyhow::Result<()> {
@@ -85,6 +91,8 @@ fn stat_sync(runtime: &str) -> anyhow::Result<()> {
8591

8692
#[test_case("node")]
8793
#[ignore = "requires node"]
94+
#[test_case("bun")]
95+
#[ignore = "requires node"]
8896
#[test_case("deno")]
8997
#[ignore = "requires node"]
9098
fn create_read_stream(runtime: &str) -> anyhow::Result<()> {
@@ -99,6 +107,8 @@ fn create_read_stream(runtime: &str) -> anyhow::Result<()> {
99107

100108
#[test_case("node")]
101109
#[ignore = "requires node"]
110+
#[test_case("bun")]
111+
#[ignore = "requires node"]
102112
#[test_case("deno")]
103113
#[ignore = "requires node"]
104114
fn create_write_stream(runtime: &str) -> anyhow::Result<()> {
@@ -115,6 +125,8 @@ fn create_write_stream(runtime: &str) -> anyhow::Result<()> {
115125

116126
#[test_case("node")]
117127
#[ignore = "requires node"]
128+
#[test_case("bun")]
129+
#[ignore = "requires node"]
118130
#[test_case("deno")]
119131
#[ignore = "requires node"]
120132
fn write_sync(runtime: &str) -> anyhow::Result<()> {
@@ -131,6 +143,8 @@ fn write_sync(runtime: &str) -> anyhow::Result<()> {
131143

132144
#[test_case("node")]
133145
#[ignore = "requires node"]
146+
#[test_case("bun")]
147+
#[ignore = "requires node"]
134148
#[test_case("deno")]
135149
#[ignore = "requires node"]
136150
fn read_dir_sync(runtime: &str) -> anyhow::Result<()> {
@@ -141,6 +155,8 @@ fn read_dir_sync(runtime: &str) -> anyhow::Result<()> {
141155

142156
#[test_case("node")]
143157
#[ignore = "requires node"]
158+
#[test_case("bun")]
159+
#[ignore = "requires node"]
144160
#[test_case("deno")]
145161
#[ignore = "requires node"]
146162
fn subprocess(runtime: &str) -> anyhow::Result<()> {

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:",

pnpm-lock.yaml

Lines changed: 159 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ packages:
44
- packages/vite-task-client
55

66
allowBuilds:
7+
bun: true
78
deno: true
89

910
catalog:
1011
'@tsconfig/strictest': ^2.0.8
1112
'@types/node': 25.0.3
13+
bun: 1.3.14
1214
cross-env: ^10.1.0
1315
deno: 2.9.2
1416
husky: ^9.1.7

0 commit comments

Comments
 (0)