diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5425d62f..e941e6b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,8 +132,16 @@ jobs: with: architecture: ${{ matrix.target == 'x86_64-apple-darwin' && 'x64' || '' }} - - name: Install Chromium - run: pnpm --filter vite-task-tools rebuild @playwright/browser-chromium + - name: Install runtime binaries + run: | + if [[ '${{ matrix.target }}' == 'x86_64-apple-darwin' ]]; then + # Bun normally prefers the native arm64 binary under Rosetta, but + # this job needs x64 Bun so the x64 preload dylib can be injected. + PATH="$(dirname "$(command -v node)"):$PNPM_HOME:/usr/bin:/bin" \ + pnpm --filter vite-task-tools rebuild --pending + else + pnpm --filter vite-task-tools rebuild --pending + fi - name: Install Chromium system dependencies if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} @@ -267,9 +275,9 @@ jobs: - uses: oxc-project/setup-node@4c588e9266bd930b6ddc34307df0659ed511d187 # v1.3.1 if: matrix.mode == 'ignored' - - name: Install Chromium + - name: Install runtime binaries if: matrix.mode == 'ignored' - run: pnpm --filter vite-task-tools rebuild @playwright/browser-chromium + run: pnpm --filter vite-task-tools rebuild --pending - name: Run ignored tests if: matrix.mode == 'ignored' diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e3c6485..05973e2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Fixed** An issue where Bun tasks on macOS did not rerun when files they read, wrote, or listed changed ([#532](https://github.com/voidzero-dev/vite-task/issues/532), [#542](https://github.com/voidzero-dev/vite-task/pull/542)). - **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)). - **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)). - **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)). diff --git a/crates/fspy/tests/node_fs.rs b/crates/fspy/tests/node_fs.rs index e902bdf1..96e95148 100644 --- a/crates/fspy/tests/node_fs.rs +++ b/crates/fspy/tests/node_fs.rs @@ -53,10 +53,13 @@ fn track_script( }) } -// Deno does not distribute a musl runtime, so only its generated cases are -// excluded there. Node remains covered by the same tests on musl. +// Bun's Linux runtime uses direct syscalls, which require universal seccomp +// tracing instead of preload interception. Deno does not distribute a musl +// runtime. Node remains covered on every target. #[test_case("node")] #[ignore = "requires node"] +#[cfg_attr(not(target_os = "linux"), test_case("bun"))] +#[cfg_attr(not(target_os = "linux"), ignore = "requires node")] #[cfg_attr(not(target_env = "musl"), test_case("deno"))] #[cfg_attr(not(target_env = "musl"), ignore = "requires node")] fn read_sync(runtime: &str) -> anyhow::Result<()> { @@ -67,6 +70,8 @@ fn read_sync(runtime: &str) -> anyhow::Result<()> { #[test_case("node")] #[ignore = "requires node"] +#[cfg_attr(not(target_os = "linux"), test_case("bun"))] +#[cfg_attr(not(target_os = "linux"), ignore = "requires node")] #[cfg_attr(not(target_env = "musl"), test_case("deno"))] #[cfg_attr(not(target_env = "musl"), ignore = "requires node")] fn exist_sync(runtime: &str) -> anyhow::Result<()> { @@ -77,6 +82,8 @@ fn exist_sync(runtime: &str) -> anyhow::Result<()> { #[test_case("node")] #[ignore = "requires node"] +#[cfg_attr(not(target_os = "linux"), test_case("bun"))] +#[cfg_attr(not(target_os = "linux"), ignore = "requires node")] #[cfg_attr(not(target_env = "musl"), test_case("deno"))] #[cfg_attr(not(target_env = "musl"), ignore = "requires node")] fn stat_sync(runtime: &str) -> anyhow::Result<()> { @@ -87,6 +94,8 @@ fn stat_sync(runtime: &str) -> anyhow::Result<()> { #[test_case("node")] #[ignore = "requires node"] +#[cfg_attr(not(target_os = "linux"), test_case("bun"))] +#[cfg_attr(not(target_os = "linux"), ignore = "requires node")] #[cfg_attr(not(target_env = "musl"), test_case("deno"))] #[cfg_attr(not(target_env = "musl"), ignore = "requires node")] fn create_read_stream(runtime: &str) -> anyhow::Result<()> { @@ -101,6 +110,8 @@ fn create_read_stream(runtime: &str) -> anyhow::Result<()> { #[test_case("node")] #[ignore = "requires node"] +#[cfg_attr(not(target_os = "linux"), test_case("bun"))] +#[cfg_attr(not(target_os = "linux"), ignore = "requires node")] #[cfg_attr(not(target_env = "musl"), test_case("deno"))] #[cfg_attr(not(target_env = "musl"), ignore = "requires node")] fn create_write_stream(runtime: &str) -> anyhow::Result<()> { @@ -117,6 +128,8 @@ fn create_write_stream(runtime: &str) -> anyhow::Result<()> { #[test_case("node")] #[ignore = "requires node"] +#[cfg_attr(not(target_os = "linux"), test_case("bun"))] +#[cfg_attr(not(target_os = "linux"), ignore = "requires node")] #[cfg_attr(not(target_env = "musl"), test_case("deno"))] #[cfg_attr(not(target_env = "musl"), ignore = "requires node")] fn write_sync(runtime: &str) -> anyhow::Result<()> { @@ -133,6 +146,8 @@ fn write_sync(runtime: &str) -> anyhow::Result<()> { #[test_case("node")] #[ignore = "requires node"] +#[cfg_attr(not(target_os = "linux"), test_case("bun"))] +#[cfg_attr(not(target_os = "linux"), ignore = "requires node")] #[cfg_attr(not(target_env = "musl"), test_case("deno"))] #[cfg_attr(not(target_env = "musl"), ignore = "requires node")] fn read_dir_sync(runtime: &str) -> anyhow::Result<()> { @@ -143,6 +158,8 @@ fn read_dir_sync(runtime: &str) -> anyhow::Result<()> { #[test_case("node")] #[ignore = "requires node"] +#[cfg_attr(not(target_os = "linux"), test_case("bun"))] +#[cfg_attr(not(target_os = "linux"), ignore = "requires node")] #[cfg_attr(not(target_env = "musl"), test_case("deno"))] #[cfg_attr(not(target_env = "musl"), ignore = "requires node")] fn subprocess(runtime: &str) -> anyhow::Result<()> { diff --git a/crates/fspy_preload_unix/src/interceptions/dirent.rs b/crates/fspy_preload_unix/src/interceptions/dirent.rs index c5a9b412..d2b11d39 100644 --- a/crates/fspy_preload_unix/src/interceptions/dirent.rs +++ b/crates/fspy_preload_unix/src/interceptions/dirent.rs @@ -26,7 +26,8 @@ unsafe extern "C" fn scandir( #[cfg(target_os = "macos")] mod macos_only { - use super::{AccessMode, c_char, c_int, c_void, handle_open, intercept}; + use super::{AccessMode, Fd, c_char, c_int, c_void, handle_open, intercept}; + intercept!(scandir_b: unsafe extern "C" fn ( dirname: *const c_char, namelist: *mut c_void, @@ -44,6 +45,19 @@ mod macos_only { // SAFETY: calling the original libc scandir_b() with the same arguments forwarded from the interposed function unsafe { scandir_b::original()(dirname, namelist, select, compar) } } + + intercept!(__getdirentries64: unsafe extern "C" fn(c_int, *mut u8, usize, *mut i64) -> isize); + unsafe extern "C" fn __getdirentries64( + fd: c_int, + buf: *mut u8, + buf_len: usize, + basep: *mut i64, + ) -> isize { + // SAFETY: fd is a valid file descriptor provided by the caller of __getdirentries64 + unsafe { handle_open(Fd(fd), AccessMode::READ_DIR) }; + // SAFETY: calling the original libc __getdirentries64() with the same arguments forwarded from the interposed function + unsafe { __getdirentries64::original()(fd, buf, buf_len, basep) } + } } intercept!(getdirentries(64): unsafe extern "C" fn (fd: c_int, buf: *mut c_char, nbytes: c_int, basep: *mut c_long) -> c_int); diff --git a/crates/fspy_preload_unix/src/interceptions/open.rs b/crates/fspy_preload_unix/src/interceptions/open.rs index 8dca5b48..641593a1 100644 --- a/crates/fspy_preload_unix/src/interceptions/open.rs +++ b/crates/fspy_preload_unix/src/interceptions/open.rs @@ -62,6 +62,45 @@ unsafe extern "C" fn openat( } } +#[cfg(target_os = "macos")] +intercept!(open_nocancel: unsafe extern "C" fn(*const c_char, c_int, ...) -> c_int); +#[cfg(target_os = "macos")] +unsafe extern "C" fn open_nocancel(path: *const c_char, flags: c_int, mut args: ...) -> c_int { + // SAFETY: path is a valid C string pointer provided by the caller of open$NOCANCEL + unsafe { handle_open(path, OpenFlags(flags)) }; + if has_mode_arg(flags) { + // SAFETY: O_CREAT requires a mode argument, matching the open$NOCANCEL contract + let mode: Mode = unsafe { args.next_arg() }; + // SAFETY: calling the original libc open$NOCANCEL() with the same arguments forwarded from the interposed function + unsafe { open_nocancel::original()(path, flags, mode) } + } else { + // SAFETY: calling the original libc open$NOCANCEL() with the same arguments forwarded from the interposed function + unsafe { open_nocancel::original()(path, flags) } + } +} + +#[cfg(target_os = "macos")] +intercept!(openat_nocancel: unsafe extern "C" fn(c_int, *const c_char, c_int, ...) -> c_int); +#[cfg(target_os = "macos")] +unsafe extern "C" fn openat_nocancel( + dirfd: c_int, + path: *const c_char, + flags: c_int, + mut args: ... +) -> c_int { + // SAFETY: dirfd and path are valid arguments provided by the caller of openat$NOCANCEL + unsafe { handle_open(PathAt(dirfd, path), OpenFlags(flags)) }; + if has_mode_arg(flags) { + // SAFETY: O_CREAT requires a mode argument, matching the openat$NOCANCEL contract + let mode: Mode = unsafe { args.next_arg() }; + // SAFETY: calling the original libc openat$NOCANCEL() with the same arguments forwarded from the interposed function + unsafe { openat_nocancel::original()(dirfd, path, flags, mode) } + } else { + // SAFETY: calling the original libc openat$NOCANCEL() with the same arguments forwarded from the interposed function + unsafe { openat_nocancel::original()(dirfd, path, flags) } + } +} + intercept!(fopen(64): unsafe extern "C" fn(path: *const c_char, mode: *const c_char) -> *mut FILE); unsafe extern "C" fn fopen(path: *const c_char, mode: *const c_char) -> *mut libc::FILE { // SAFETY: path and mode are valid C string pointers provided by the caller of the interposed function diff --git a/crates/fspy_preload_unix/src/libc.rs b/crates/fspy_preload_unix/src/libc.rs index 8d5d2d0a..65677981 100644 --- a/crates/fspy_preload_unix/src/libc.rs +++ b/crates/fspy_preload_unix/src/libc.rs @@ -27,4 +27,20 @@ unsafe extern "C" { nbytes: c_int, basep: *mut c_long, ) -> c_int; + + #[cfg(target_os = "macos")] + #[link_name = "open$NOCANCEL"] + pub unsafe fn open_nocancel(path: *const c_char, flags: c_int, ...) -> c_int; + + #[cfg(target_os = "macos")] + #[link_name = "openat$NOCANCEL"] + pub unsafe fn openat_nocancel(dirfd: c_int, path: *const c_char, flags: c_int, ...) -> c_int; + + #[cfg(target_os = "macos")] + pub unsafe fn __getdirentries64( + fd: c_int, + buf: *mut u8, + buf_len: usize, + basep: *mut i64, + ) -> isize; } diff --git a/packages/tools/package.json b/packages/tools/package.json index 0302643d..0fb97185 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -6,6 +6,7 @@ "@playwright/browser-chromium": "catalog:", "@vitest/browser-playwright": "catalog:", "@voidzero-dev/vite-task-client": "workspace:*", + "bun": "catalog:", "cross-env": "catalog:", "deno": "catalog:", "oxfmt": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2430d90..44979759 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ catalogs: '@vitest/browser-playwright': specifier: 4.1.10 version: 4.1.10 + bun: + specifier: 1.3.14 + version: 1.3.14 cross-env: specifier: ^10.1.0 version: 10.1.0 @@ -79,6 +82,9 @@ importers: '@voidzero-dev/vite-task-client': specifier: workspace:* version: link:../vite-task-client + bun: + specifier: 'catalog:' + version: 1.3.14 cross-env: specifier: 'catalog:' version: 10.1.0 @@ -176,6 +182,86 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@oven/bun-darwin-aarch64@1.3.14': + resolution: {integrity: sha512-Omj20SuiHBOUjUBIyqtkNjSUIjOtEOJwmbix/ZyFH4BaQ6OZTaaRWIR4TjHVz0yadHgli6lLTiAh1uarnvD49A==} + cpu: [arm64] + os: [darwin] + + '@oven/bun-darwin-x64-baseline@1.3.14': + resolution: {integrity: sha512-OSfsTZstc898HHElhU4NccaBGOSSDn5VfahiVTnidZ9B/+wb7WTyfZJaBeJcfjwJ9H2W9uTh2TGtl3UfcXgV9g==} + cpu: [x64] + os: [darwin] + + '@oven/bun-darwin-x64@1.3.14': + resolution: {integrity: sha512-FFj3QdU/OhlDyZOJ8CWfN5eWLpRlT4qjZg7lMQi7jA6GuoY5ajlO1zWLP/MuHYRSbXQUvV52RejNi8DVnAp13w==} + cpu: [x64] + os: [darwin] + + '@oven/bun-freebsd-aarch64@1.3.14': + resolution: {integrity: sha512-LIKrXaFxAHybVO5Pf+9XP2FHUj/5APvXTUKk9dqHm5iFz4oH+W24cmhjkJirNujh9hKeTyrpWSe3no9JZKowIw==} + cpu: [arm64] + os: [freebsd] + + '@oven/bun-freebsd-x64@1.3.14': + resolution: {integrity: sha512-uwD+fGUH1ADpIF3B1U2jWzzb20QwRLZfj5QZ28GUCGrAJ/nTmWrD6YYGsblCY1wuhldRez3lU40AyuvSCyLYmw==} + cpu: [x64] + os: [freebsd] + + '@oven/bun-linux-aarch64-android@1.3.14': + resolution: {integrity: sha512-y4kq5b85lsrmFb9Xvi4w9mA5IEFJkLMrSmYn06q24KjL9rUWDWO3VFZEtteZxUN5+ec3Zm5S8OnJw1umaCbVjA==} + cpu: [arm64] + os: [android] + + '@oven/bun-linux-aarch64-musl@1.3.14': + resolution: {integrity: sha512-jmqOA92Cd1NL/1XBd4bFkJLxQ86K0RW7ohxS2qzzAvuitO4JiIxjjTeCspoU44zCozH72HpfZfUE2On31OjnWA==} + cpu: [arm64] + os: [linux] + + '@oven/bun-linux-aarch64@1.3.14': + resolution: {integrity: sha512-X5SsPZHs+iYO8R/efIcRtc7gT2Q2DgPfliCxEkx4cXBumwkw0c/EsHMNwH3EgGpCDaZ7IYVPhpCG/xBOQHEwZw==} + cpu: [arm64] + os: [linux] + + '@oven/bun-linux-x64-android@1.3.14': + resolution: {integrity: sha512-qe9e1d+3VAEU7nAA2ol9Jvmy/o99PVMSgZhHn7Q/9O3YcDrfEqyQ8zm4zoe5qTEo8HZH0dN03Le0Ys2eQPs7eg==} + cpu: [x64] + os: [android] + + '@oven/bun-linux-x64-baseline@1.3.14': + resolution: {integrity: sha512-q/8EdOC0yUE8FPeoOVq8/Pw5I9/tJaYmUfO/uDUAREx8IUnOJH1RJ5A3BjFqre8pvJoiZA9AovPJq5FnNNjSxA==} + cpu: [x64] + os: [linux] + + '@oven/bun-linux-x64-musl-baseline@1.3.14': + resolution: {integrity: sha512-n6iE71G4lQE4XkrZhQQcL5YUlxDbnq6nqV7zeQi33PMsLT/0kYE+RvHOtBWZ3w0wMdXZfINmp63hIb9ijUBGtw==} + cpu: [x64] + os: [linux] + + '@oven/bun-linux-x64-musl@1.3.14': + resolution: {integrity: sha512-GBCB/k/sIqcr06eTNgg7g46qiUv35Jasx4XiccJ/n7RGqrE4RWUD/XJBbWFprVPjvqd59+QtSnS99XGqvftHfg==} + cpu: [x64] + os: [linux] + + '@oven/bun-linux-x64@1.3.14': + resolution: {integrity: sha512-7OVTAKvwfPmSbIV1HpdOoVVx5VRc427GuPPne93N6vk4eQBPId9nXmZDh9/zGaKPdbVjVtQSZafWQoUjx38Utw==} + cpu: [x64] + os: [linux] + + '@oven/bun-windows-aarch64@1.3.14': + resolution: {integrity: sha512-T7s3x/BsVKQObGU6QDkZeI6wKynzqGbBH1yI77jrrj5siElclxr3DQrDIk8CV4G5/SJq2HHq4kpLyYY2DKCSmA==} + cpu: [arm64] + os: [win32] + + '@oven/bun-windows-x64-baseline@1.3.14': + resolution: {integrity: sha512-uIjLUC1S9DWgICzuoMba7vurBJnBruE4S5CxnvmZkdqWVXRzx1Rgu636HoH+k0qeaQCFh3jeG3JQ1y6fRHv0sw==} + cpu: [x64] + os: [win32] + + '@oven/bun-windows-x64@1.3.14': + resolution: {integrity: sha512-mUFWL3BoYkNpjd8e9PqROiFF/1Xeotq20mABJsiQH62jM1g5zqWh4khw1RZ6bX8Q8fWvlPaxG1PjofkmjUi3vg==} + cpu: [x64] + os: [win32] + '@oxc-project/runtime@0.138.0': resolution: {integrity: sha512-yHhoXsN8tYxgdJCdD91PbySNjEEaBX/tH2OQRDXJpsQv5b184oC4/qVbU7qlblvfil/JP15Lh2HW7+HN5DS90Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -829,6 +915,12 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + bun@1.3.14: + resolution: {integrity: sha512-aB6GVd42x1Y5ie1K16SF+oLGtgSkwX9hgoDdIW88pjvfTccU8F1vfpoOt34QLv0dZ1v3XimtaxPlZUG81Gx9Zg==} + cpu: [arm64, x64] + os: [darwin, linux, android, freebsd, win32] + hasBin: true + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -1299,6 +1391,54 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@oven/bun-darwin-aarch64@1.3.14': + optional: true + + '@oven/bun-darwin-x64-baseline@1.3.14': + optional: true + + '@oven/bun-darwin-x64@1.3.14': + optional: true + + '@oven/bun-freebsd-aarch64@1.3.14': + optional: true + + '@oven/bun-freebsd-x64@1.3.14': + optional: true + + '@oven/bun-linux-aarch64-android@1.3.14': + optional: true + + '@oven/bun-linux-aarch64-musl@1.3.14': + optional: true + + '@oven/bun-linux-aarch64@1.3.14': + optional: true + + '@oven/bun-linux-x64-android@1.3.14': + optional: true + + '@oven/bun-linux-x64-baseline@1.3.14': + optional: true + + '@oven/bun-linux-x64-musl-baseline@1.3.14': + optional: true + + '@oven/bun-linux-x64-musl@1.3.14': + optional: true + + '@oven/bun-linux-x64@1.3.14': + optional: true + + '@oven/bun-windows-aarch64@1.3.14': + optional: true + + '@oven/bun-windows-x64-baseline@1.3.14': + optional: true + + '@oven/bun-windows-x64@1.3.14': + optional: true + '@oxc-project/runtime@0.138.0': {} '@oxc-project/types@0.137.0': {} @@ -1726,6 +1866,25 @@ snapshots: assertion-error@2.0.1: {} + bun@1.3.14: + optionalDependencies: + '@oven/bun-darwin-aarch64': 1.3.14 + '@oven/bun-darwin-x64': 1.3.14 + '@oven/bun-darwin-x64-baseline': 1.3.14 + '@oven/bun-freebsd-aarch64': 1.3.14 + '@oven/bun-freebsd-x64': 1.3.14 + '@oven/bun-linux-aarch64': 1.3.14 + '@oven/bun-linux-aarch64-android': 1.3.14 + '@oven/bun-linux-aarch64-musl': 1.3.14 + '@oven/bun-linux-x64': 1.3.14 + '@oven/bun-linux-x64-android': 1.3.14 + '@oven/bun-linux-x64-baseline': 1.3.14 + '@oven/bun-linux-x64-musl': 1.3.14 + '@oven/bun-linux-x64-musl-baseline': 1.3.14 + '@oven/bun-windows-aarch64': 1.3.14 + '@oven/bun-windows-x64': 1.3.14 + '@oven/bun-windows-x64-baseline': 1.3.14 + chai@6.2.2: {} convert-source-map@2.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b30b4fd9..4869bffb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,7 @@ packages: allowBuilds: '@playwright/browser-chromium': true + bun: true deno: true catalog: @@ -12,6 +13,7 @@ catalog: '@types/node': 25.0.3 '@playwright/browser-chromium': 1.52.0 '@vitest/browser-playwright': 4.1.10 + bun: 1.3.14 cross-env: ^10.1.0 deno: 2.9.2 husky: ^9.1.7