diff --git a/.github/workflows/bun-windows.yml b/.github/workflows/bun-windows.yml index 8b2a201dba..3595a5aea8 100644 --- a/.github/workflows/bun-windows.yml +++ b/.github/workflows/bun-windows.yml @@ -434,6 +434,22 @@ jobs: $null = node packages/bun-internal-test/src/runner.node.mjs ${{runner.temp}}/release/${{env.tag}}-${{ matrix.arch == 'x86_64' && 'x64' || 'aarch64' }}${{ matrix.cpu == 'nehalem' && '-baseline' || '' }}-profile/bun.exe || $true } catch {} $ErrorActionPreference = "Stop" + - uses: sarisia/actions-status-discord@v1 + if: always() && steps.test.outputs.failing_tests != '' && github.event_name == 'pull_request' + with: + title: "" + webhook: ${{ secrets.DISCORD_WEBHOOK_WINTEST }} + status: "failure" + noprefix: true + nocontext: true + description: | + ### ❌🪟 [${{github.event.pull_request.title}}](https://github.com/oven-sh/bun/pull/${{github.event.number}}) + + @${{ github.actor }}, there are **${{ steps.test.outputs.failing_test_count }} failing tests** on Windows ${{ matrix.arch }}${{ matrix.cpu == 'nehalem' && ' Baseline' || '' }} + + ${{ steps.test.outputs.failing_tests }} + + [Full Test Output](https://github.com/oven-sh/bun/actions/runs/${{github.run_id}}) - uses: sarisia/actions-status-discord@v1 if: always() && steps.test.outputs.regressing_tests != '' && github.event_name == 'pull_request' with: diff --git a/.gitignore b/.gitignore index 8721435d6b..8373c489d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,166 +1,167 @@ -.DS_Store -zig-cache -packages/*/*.wasm -*.o -*.a -profile.json - -node_modules -.envrc -.swcrc -yarn.lock -dist -*.tmp -*.log -*.out.js -*.out.refresh.js -**/package-lock.json -build -*.wat -zig-out -pnpm-lock.yaml -README.md.template -src/deps/zig-clap/example -src/deps/zig-clap/README.md -src/deps/zig-clap/.github -src/deps/zig-clap/.gitattributes -out -outdir - -.trace -cover -coverage -coverv -*.trace -github -out.* -out -.parcel-cache -esbuilddir -*.bun -parceldist -esbuilddir -outdir/ -outcss -.next -txt.js -.idea -.vscode/cpp* -.vscode/clang* - -node_modules_* -*.jsb -*.zip -bun-zigld -bun-singlehtreaded -bun-nomimalloc -bun-mimalloc -examples/lotta-modules/bun-yday -examples/lotta-modules/bun-old -examples/lotta-modules/bun-nofscache - -src/node-fallbacks/out/* -src/node-fallbacks/node_modules -sign.json -release/ -*.dmg -sign.*.json -packages/debug-* -packages/bun-cli/postinstall.js -packages/bun-*/bun -packages/bun-*/bun-profile -packages/bun-*/debug-bun -packages/bun-*/*.o -packages/bun-cli/postinstall.js - -packages/bun-cli/bin/* -bun-test-scratch -misctools/fetch - -src/deps/libiconv -src/deps/openssl -src/tests.zig -*.blob -src/deps/s2n-tls -.npm -.npm.gz - -bun-binary - -src/deps/PLCrashReporter/ - -*.dSYM -*.crash -misctools/sha -packages/bun-wasm/*.mjs -packages/bun-wasm/*.cjs -packages/bun-wasm/*.map -packages/bun-wasm/*.js -packages/bun-wasm/*.d.ts -packages/bun-wasm/*.d.cts -packages/bun-wasm/*.d.mts -*.bc - -src/fallback.version -src/runtime.version -*.sqlite -*.database -*.db -misctools/machbench -*.big -.eslintcache - -/bun-webkit - -src/deps/c-ares/build -src/bun.js/bindings-obj -src/bun.js/debug-bindings-obj - -failing-tests.txt -test.txt -myscript.sh - -cold-jsc-start -cold-jsc-start.d - -/test.ts -/test.js - -src/js/out/modules* -src/js/out/functions* -src/js/out/tmp -src/js/out/DebugPath.h - -make-dev-stats.csv - -.uuid -tsconfig.tsbuildinfo - -test/js/bun/glob/fixtures -*.lib -*.pdb -CMakeFiles -build.ninja -.ninja_deps -.ninja_log -CMakeCache.txt -cmake_install.cmake -compile_commands.json - -*.lib -x64 -**/*.vcxproj* -**/*.sln* -**/*.dir -**/*.pdb - -/.webkit-cache -/.cache -/src/deps/libuv -/build-*/ - -.vs - -**/.verdaccio-db.json -/test-report.md +.DS_Store +zig-cache +packages/*/*.wasm +*.o +*.a +profile.json + +node_modules +.envrc +.swcrc +yarn.lock +dist +*.tmp +*.log +*.out.js +*.out.refresh.js +**/package-lock.json +build +*.wat +zig-out +pnpm-lock.yaml +README.md.template +src/deps/zig-clap/example +src/deps/zig-clap/README.md +src/deps/zig-clap/.github +src/deps/zig-clap/.gitattributes +out +outdir + +.trace +cover +coverage +coverv +*.trace +github +out.* +out +.parcel-cache +esbuilddir +*.bun +parceldist +esbuilddir +outdir/ +outcss +.next +txt.js +.idea +.vscode/cpp* +.vscode/clang* + +node_modules_* +*.jsb +*.zip +bun-zigld +bun-singlehtreaded +bun-nomimalloc +bun-mimalloc +examples/lotta-modules/bun-yday +examples/lotta-modules/bun-old +examples/lotta-modules/bun-nofscache + +src/node-fallbacks/out/* +src/node-fallbacks/node_modules +sign.json +release/ +*.dmg +sign.*.json +packages/debug-* +packages/bun-cli/postinstall.js +packages/bun-*/bun +packages/bun-*/bun-profile +packages/bun-*/debug-bun +packages/bun-*/*.o +packages/bun-cli/postinstall.js + +packages/bun-cli/bin/* +bun-test-scratch +misctools/fetch + +src/deps/libiconv +src/deps/openssl +src/tests.zig +*.blob +src/deps/s2n-tls +.npm +.npm.gz + +bun-binary + +src/deps/PLCrashReporter/ + +*.dSYM +*.crash +misctools/sha +packages/bun-wasm/*.mjs +packages/bun-wasm/*.cjs +packages/bun-wasm/*.map +packages/bun-wasm/*.js +packages/bun-wasm/*.d.ts +packages/bun-wasm/*.d.cts +packages/bun-wasm/*.d.mts +*.bc + +src/fallback.version +src/runtime.version +*.sqlite +*.database +*.db +misctools/machbench +*.big +.eslintcache + +/bun-webkit + +src/deps/c-ares/build +src/bun.js/bindings-obj +src/bun.js/debug-bindings-obj + +failing-tests.txt +test.txt +myscript.sh + +cold-jsc-start +cold-jsc-start.d + +/testdir +/test.ts +/test.js + +src/js/out/modules* +src/js/out/functions* +src/js/out/tmp +src/js/out/DebugPath.h + +make-dev-stats.csv + +.uuid +tsconfig.tsbuildinfo + +test/js/bun/glob/fixtures +*.lib +*.pdb +CMakeFiles +build.ninja +.ninja_deps +.ninja_log +CMakeCache.txt +cmake_install.cmake +compile_commands.json + +*.lib +x64 +**/*.vcxproj* +**/*.sln* +**/*.dir +**/*.pdb + +/.webkit-cache +/.cache +/src/deps/libuv +/build-*/ + +.vs + +**/.verdaccio-db.json +/test-report.md /test-report.json \ No newline at end of file diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 03366aa692..0000000000 --- a/.prettierignore +++ /dev/null @@ -1,14 +0,0 @@ -src/fallback.html -src/bun.js/WebKit -src/js/out -src/*.out.js -src/*out.*.js -src/deps -src/test/fixtures -src/react-refresh.js -test/snapshots -test/snapshots-no-hmr -test/js/deno/*.test.ts -test/js/deno/**/*.test.ts -bench/react-hello-world/react-hello-world.node.js -test/cli/run/encoding-utf16-le-bom.ts diff --git a/.prettierrc.cjs b/.prettierrc.cjs deleted file mode 100644 index 44f2bd9331..0000000000 --- a/.prettierrc.cjs +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - arrowParens: "avoid", - printWidth: 120, - trailingComma: "all", - useTabs: false, - quoteProps: "preserve", - overrides: [ - { - files: ["*.md"], - options: { - printWidth: 80, - }, - }, - ], -}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json index be3d5ef659..4b5985593d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,10 +1,33 @@ { "recommendations": [ + // Zig "ziglang.vscode-zig", - "biomejs.biome", + + // C/C++ + "clang.clangd", + "ms-vscode.cmake-tools", "xaver.clang-format", "vadimcn.vscode-lldb", + + // JavaScript + "oven.bun-vscode", + "biomejs.biome", + + // TypeScript + "better-ts-errors.better-ts-errors", + "MylesMurphy.prettify-ts", + + // Markdown + "bierner.markdown-preview-github-styles", + "bierner.markdown-emoji", + "bierner.emojisense", + "bierner.markdown-checkbox", + "bierner.jsdoc-markdown-highlighting", + + // TOML + "tamasfe.even-better-toml", + + // Other "bierner.comment-tagged-templates", - "clangd.clangd" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index de9075c40c..24279f7d2d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,36 +1,24 @@ { - // The usage of BUN_GARBAGE_COLLECTOR_LEVEL=2 is important for debugging - // It will force the garbage collector to run after every test and every call to expect() - // it makes our tests very slow - // But it helps catch memory bugs + // Notes: + // - BUN_GARBAGE_COLLECTOR_LEVEL=2 forces GC to run after every `expect()`, but is slower + // - BUN_DEBUG_QUIET_LOGS=1 disables the debug logs + // - FORCE_COLOR=1 forces colors in the terminal + // - "${workspaceFolder}/test" is the cwd for `bun test` so it matches CI, we should fix this later + // - "cppvsdbg" is used instead of "lldb" on Windows, because "lldb" is too slow "version": "0.2.0", "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "sharp", - "program": "bun-debug", - "args": ["install", "sharp"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. - "cwd": "/tmp/scratchpad_20230911T213851", - "env": { - "FORCE_COLOR": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2" - }, - "console": "internalConsole" - }, + // bun test [file] { "type": "lldb", "request": "launch", "name": "bun test [file]", - "program": "bun-debug", + "program": "${workspaceFolder}/build/bun-debug", "args": ["test", "${file}"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. "cwd": "${workspaceFolder}/test", "env": { - "BUN_GARBAGE_COLLECTOR_LEVEL": "2", "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole" }, @@ -38,13 +26,13 @@ "type": "lldb", "request": "launch", "name": "bun test [file] (fast)", - "program": "bun-debug", + "program": "${workspaceFolder}/build/bun-debug", "args": ["test", "${file}"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. "cwd": "${workspaceFolder}/test", "env": { "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "0", }, "console": "internalConsole" }, @@ -52,12 +40,13 @@ "type": "lldb", "request": "launch", "name": "bun test [file] (verbose)", - "program": "bun-debug", + "program": "${workspaceFolder}/build/bun-debug", "args": ["test", "${file}"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. "cwd": "${workspaceFolder}/test", "env": { - "FORCE_COLOR": "1" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "0", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole" }, @@ -65,36 +54,299 @@ "type": "lldb", "request": "launch", "name": "bun test [file] --watch", - "program": "bun-debug", + "program": "${workspaceFolder}/build/bun-debug", "args": ["test", "--watch", "${file}"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. "cwd": "${workspaceFolder}/test", "env": { "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole" }, { "type": "lldb", "request": "launch", - "name": "bun test [file] --only", - "program": "bun-debug", - "args": ["test", "--only", "${file}"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. + "name": "bun test [file] --hot", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "--hot", "${file}"], "cwd": "${workspaceFolder}/test", "env": { "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", }, "console": "internalConsole" }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [file] --inspect", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${file}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/?wait=1" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + } + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [file] --inspect-brk", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${file}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/?break=1" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + } + }, + // bun run [file] + { + "type": "lldb", + "request": "launch", + "name": "bun run [file]", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "0", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole", + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] (fast)", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "0", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] (verbose)", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "0", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] --watch", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "--watch", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] --hot", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "--hot", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] --inspect", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "0", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/?wait=1" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + { + "type": "lldb", + "request": "launch", + "name": "bun run [file] --inspect-brk", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "0", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/?break=1" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + // bun test [...] + { + "type": "lldb", + "request": "launch", + "name": "bun test [...]", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [...] (fast)", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "0", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [...] (verbose)", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "0", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [...] --watch", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "--watch", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [...] --hot", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "--hot", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + }, + "console": "internalConsole" + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [...] --inspect", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/?wait=1" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + } + }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [...] --inspect-brk", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/?break=1" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + } + }, + // bun test [*] { "type": "lldb", "request": "launch", "name": "bun test [*]", - "program": "bun-debug", - "args": ["test", "js/node"], + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test"], "cwd": "${workspaceFolder}/test", "env": { "FORCE_COLOR": "1", @@ -107,297 +359,572 @@ "type": "lldb", "request": "launch", "name": "bun test [*] (fast)", - "program": "bun-debug", - "args": ["test", "js"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. - "cwd": "${workspaceFolder}/test", - "env": { - "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun test [*] --only", - "program": "bun-debug", - "args": ["test", "--only"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. - "cwd": "${workspaceFolder}/test", - "env": { - "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "ZACK", - "program": "bun-debug", - "args": ["run", "./wtf"], - // "args": ["test", "bunshell.test.ts", "-t", "cd"], - // "args": ["test", "lex.test.ts"], - // "args": ["test", "bunshell.test.ts", "-t", "invalid surrogates"], - // "args": ["test", "bunshell.test.ts"], - // "args": ["test", "bunshell.test.ts", "-t", "recursive"], - // The cwd here must be the same as in CI. Or you will cause test failures that only happen in CI. - "cwd": "${workspaceFolder}", - "env": { - "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file]", - "program": "bun-debug", - "args": ["run", "${file}"], - "cwd": "${fileDirname}", - "env": { - "FORCE_COLOR": "1", - "NODE_ENV": "development" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] (gc)", - "program": "bun-debug", - "args": ["run", "${file}"], - "cwd": "${fileDirname}", - "env": { - "FORCE_COLOR": "1", - "BUN_DEBUG_QUIET_LOGS": "1", - "BUN_GARBAGE_COLLECTOR_LEVEL": "2" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] (verbose)", - "program": "bun-debug", - "args": ["run", "${file}"], - "cwd": "${fileDirname}", - "env": { - "FORCE_COLOR": "1" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] --watch", - "program": "bun-debug", - "args": ["run", "--watch", "${file}"], - "cwd": "${fileDirname}", - "env": { - "FORCE_COLOR": "1" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun run [file] --hot", - "program": "bun-debug", - "args": ["run", "--hot", "${file}"], - "cwd": "${fileDirname}", - "env": { - "FORCE_COLOR": "1" - }, - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "HTTP bench", - "program": "${workspaceFolder}/misctools/http_bench", - "args": ["https://twitter.com", "--count=100"], - "cwd": "${workspaceFolder}", - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "bun build debug", - "program": "bun-debug", - "args": ["bun", "${file}"], - "cwd": "${workspaceFolder}", - "console": "internalConsole", - "env": { - "BUN_CONFIG_MINIFY_WHITESPACE": "1" - } - }, - { - "type": "lldb", - "request": "launch", - "name": "bun build debug out.js", - "program": "bun-debug", - "args": ["--outfile=out.js", "bun", "${file}"], - "cwd": "${file}/../", - "console": "internalConsole", - "env": { - "BUN_CONFIG_MINIFY_WHITESPACE": "1" - } - }, - { - "type": "lldb", - "request": "launch", - "name": "bun build debug STDOUT", - "program": "bun-debug", - "args": ["bun", "${file}"], - "cwd": "${file}/../", - "console": "internalConsole", - "env": {} - }, - { - "type": "lldb", - "request": "launch", - "name": "bun build debug (no splitting, browser entry)", - "program": "bun-debug", - "args": [ - "--entry-names=./[name].[ext]", - "--outdir=/Users/jarred/Code/bun-rsc/.rsc-no-split", - "--platform=browser", - "bun", - "./quick.tsx" - ], - "cwd": "/Users/jarred/Code/bun-rsc", - "console": "internalConsole", - "env": { - "NODE_ENV": "production" - // "BUN_DEBUG_QUIET_LOGS": "1" - // "BUN_DUMP_SYMBOLS": "1" - } - }, - { - "type": "lldb", - "request": "launch", - "name": "bun build debug (splitting, rsc)", - "program": "bun-debug", - "args": [ - "--entry-names=./[name].[ext]", - "--outdir=/Users/jarred/Code/bun-rsc/.rsc-split", - "--server-components", - "--platform=bun", - "--splitting", - "bun", - "/Users/jarred/Code/bun-rsc/components/Message.tsx", - "/Users/jarred/Code/bun-rsc/components/Button.tsx" - ], - "cwd": "/Users/jarred/Code/bun-rsc", - "console": "internalConsole", - "env": { - "NODE_ENV": "production" - // "BUN_DEBUG_QUIET_LOGS": "1" - // "BUN_DUMP_SYMBOLS": "1" - } - }, - { - "type": "lldb", - "request": "launch", - "name": "bun build debug (NO splitting, rsc)", - "program": "bun-debug", - "args": [ - "--entry-names=./[name].[ext]", - "--outdir=/Users/jarred/Code/bun-rsc/.rsccheck", - "--server-components", - "--platform=bun", - "bun", - "/Users/jarred/Code/bun-rsc/pages/index.js" - ], - "cwd": "/Users/jarred/Code/bun-rsc", - "console": "internalConsole", - "env": { - "NODE_ENV": "production" - // "BUN_DEBUG_QUIET_LOGS": "1" - // "BUN_DUMP_SYMBOLS": "1" - } - }, - { - "type": "lldb", - "request": "launch", - "name": "bunx debug", - "program": "bun-debug", - "args": ["--bun", "x", "tsc", "--help"], - "cwd": "${workspaceFolder}", - "console": "internalConsole", - "env": { - "BUN_DEBUG_QUIET_LOGS": "1" - } - }, - { - "type": "lldb", - "request": "launch", - "name": "bun install", - "program": "bun-debug", - "args": ["install"], - "cwd": "${fileDirname}", - "console": "internalConsole", - "env": {} - }, - { - "type": "lldb", - "request": "launch", - "name": "fetch debug", - "program": "${workspaceFolder}/misctools/fetch", - "args": ["https://example.com", "--verbose"], - "cwd": "${workspaceFolder}", - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "Build zig unit test", - "program": "make", - "args": ["build-unit", "${file}"], - "cwd": "${workspaceFolder}", - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "Run zig unit test", - "program": "${workspaceFolder}/zig-out/bin/test", - "args": ["abc"], - "cwd": "${workspaceFolder}", - "console": "internalConsole" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug REPL", "program": "${workspaceFolder}/build/bun-debug", - "args": ["/Users/dave/.bun/bin/bun-repl"], - "cwd": "${workspaceFolder}", - "console": "internalConsole", + "args": ["test"], + "cwd": "${workspaceFolder}/test", "env": { - "BUN_DEBUG_QUIET_LOGS": "1" + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "0", }, - "terminal": "integrated" + "console": "internalConsole" }, { - "type": "cppvsdbg", + "type": "lldb", "request": "launch", - "name": "Windows: bun run [file]", - "program": "${workspaceFolder}/build/bun-debug.exe", - "args": ["run", "${file}"], - "cwd": "${fileDirname}" + "name": "bun test [*] --inspect", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2", + "BUN_INSPECT": "ws://localhost:0/" + }, + "console": "internalConsole", + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + } }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [*] (ci)", + "program": "node", + "args": ["src/runner.node.mjs"], + "cwd": "${workspaceFolder}/packages/bun-internal-test", + "console": "internalConsole" + }, + // Windows: bun test [file] { "type": "cppvsdbg", "request": "launch", "name": "Windows: bun test [file]", "program": "${workspaceFolder}/build/bun-debug.exe", "args": ["test", "${file}"], - "cwd": "${fileDirname}" + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [file] (fast)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${file}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "0" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [file] (verbose)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${file}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "0" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [file] --inspect", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${file}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/?wait=1" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [file] --inspect-brk", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${file}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/?break=1" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + // Windows: bun run [file] + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun run [file]", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun run [file] (fast)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "0" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun run [file] (verbose)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "0" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun run [file] --inspect", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/?wait=1" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun run [file] --inspect-brk", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["run", "${fileBasename}"], + "cwd": "${fileDirname}", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/?break=1" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + // Windows: bun test [...] + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...]", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...] (fast)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "0" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...] (verbose)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "0" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...] --watch", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "--watch", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...] --hot", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "--hot", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...] --inspect", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/?wait=1" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [...] --inspect-brk", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test", "${input:testName}"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/?break=1" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + // Windows: bun test [*] + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [*]", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [*] (fast)", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "1" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "0" + } + ], + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [*] --inspect", + "program": "${workspaceFolder}/build/bun-debug.exe", + "args": ["test"], + "cwd": "${workspaceFolder}/test", + "environment": [ + { + "name": "FORCE_COLOR", + "value": "1" + }, + { + "name": "BUN_DEBUG_QUIET_LOGS", + "value": "0" + }, + { + "name": "BUN_GARBAGE_COLLECTOR_LEVEL", + "value": "2" + }, + { + "name": "BUN_INSPECT", + "value": "ws://localhost:0/" + } + ], + "serverReadyAction": { + "pattern": "https:\/\/debug.bun.sh\/#localhost:([0-9]+)/", + "uriFormat": "https://debug.bun.sh/#ws://localhost:%s/", + "action": "openExternally" + }, + }, + { + "type": "cppvsdbg", + "request": "launch", + "name": "Windows: bun test [*] (ci)", + "program": "node", + "args": ["src/runner.node.mjs"], + "cwd": "${workspaceFolder}/packages/bun-internal-test", + "console": "internalConsole" + }, + ], + "inputs": [ + { + "id": "commandLine", + "type": "promptString", + "description": "Usage: bun [...]" + }, + { + "id": "testName", + "type": "promptString", + "description": "Usage: bun test [...]" }, ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index ff8461dcf0..c0001c02d6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,58 +1,89 @@ { - "git.autoRepositoryDetection": "openEditors", + // Editor + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "modificationsIfAvailable", + + // Search "search.quickOpen.includeSymbols": false, "search.seedWithNearestWord": true, "search.smartCase": true, "search.exclude": { "node_modules": true, - "src/bun.js/WebKit": true, ".git": true, + "src/bun.js/WebKit": true, "src/deps/*/**": true }, "search.followSymlinks": false, "search.useIgnoreFiles": true, + + // Git + "git.autoRepositoryDetection": "openEditors", + "git.ignoreSubmodules": true, + "git.ignoreLimitWarning": true, + + // Zig + "zig.initialSetupDone": true, "zig.buildOnSave": false, - "zig.formattingProvider": "zls", "zig.buildOption": "build", "zig.buildFilePath": "${workspaceFolder}/build.zig", - "zig.initialSetupDone": true, - "editor.formatOnSave": true, - - // We are using biome instead of prettier. - "prettier.enable": false, - - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome" - }, + "zig.path": "${workspaceFolder}/.cache/zig/zig.exe", + "zig.formattingProvider": "zls", + "zig.zls.enableInlayHints": false, "[zig]": { "editor.tabSize": 4, "editor.useTabStops": false, "editor.defaultFormatter": "ziglang.vscode-zig" }, - "[ts]": { + + // C++ + "lldb.verboseLogging": false, + "cmake.configureOnOpen": false, + "C_Cpp.errorSquiggles": "enabled", + "[cpp]": { + "editor.defaultFormatter": "xaver.clang-format" + }, + "[c]": { + "editor.defaultFormatter": "xaver.clang-format" + }, + "[h]": { + "editor.defaultFormatter": "xaver.clang-format" + }, + + // JavaScript + "prettier.enable": false, + "eslint.workingDirectories": ["${workspaceFolder}/packages/bun-types"], + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome", + }, + "[javascriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, - "[js]": { + + // TypeScript + "typescript.tsdk": "${workspaceFolder}/node_modules/typescript/lib", + "[typescript]": { "editor.defaultFormatter": "biomejs.biome" }, - "zig.zls.enableInlayHints": false, - "zig.path": "${workspaceFolder}/.cache/zig/zig.exe", - "git.ignoreSubmodules": true, - "[jsx]": { + "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, - "[tsx]": { - "editor.defaultFormatter": "biomejs.biome" + + // JSON + "[json]": { + "editor.defaultFormatter": "biomejs.biome", }, - "[yaml]": {}, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome", + }, + + // Markdown "[markdown]": { + "editor.defaultFormatter": "biomejs.biome", "editor.unicodeHighlight.ambiguousCharacters": false, "editor.unicodeHighlight.invisibleCharacters": false, "diffEditor.ignoreTrimWhitespace": false, - "editor.defaultFormatter": "biomejs.biome", "editor.wordWrap": "on", "editor.quickSuggestions": { "comments": "off", @@ -60,7 +91,18 @@ "other": "off" } }, - "lldb.verboseLogging": false, + + // TOML + "[toml]": { + "editor.defaultFormatter": "biomejs.biome", + }, + + // YAML + "[yaml]": { + "editor.defaultFormatter": "biomejs.biome", + }, + + // Files "files.exclude": { "**/.git": true, "**/.svn": true, @@ -87,33 +129,6 @@ "**/*.i": true, "packages/bun-uws/fuzzing/seed-corpus/**/*": true }, - "C_Cpp.files.exclude": { - "**/.vscode": true, - "WebKit/JSTests": true, - "WebKit/Tools": true, - "WebKit/WebDriverTests": true, - "WebKit/WebKit.xcworkspace": true, - "WebKit/WebKitLibraries": true, - "WebKit/Websites": true, - "WebKit/resources": true, - "WebKit/LayoutTests": true, - "WebKit/ManualTests": true, - "WebKit/PerformanceTests": true, - "WebKit/WebKitLegacy": true, - "WebKit/WebCore": true, - "WebKit/WebDriver": true, - "WebKit/WebKitBuild": true, - "WebKit/WebInspectorUI": true - }, - "[cpp]": { - "editor.defaultFormatter": "xaver.clang-format" - }, - "[h]": { - "editor.defaultFormatter": "xaver.clang-format" - }, - "[c]": { - "editor.defaultFormatter": "xaver.clang-format" - }, "files.associations": { "*.lock": "yarnlock", "*.idl": "cpp", @@ -233,9 +248,22 @@ "xtree": "cpp", "xutility": "cpp" }, - "C_Cpp.errorSquiggles": "enabled", - "eslint.workingDirectories": ["packages/bun-types"], - "typescript.tsdk": "node_modules/typescript/lib", - "cmake.configureOnOpen": false, - "git.ignoreLimitWarning": true + "C_Cpp.files.exclude": { + "**/.vscode": true, + "WebKit/JSTests": true, + "WebKit/Tools": true, + "WebKit/WebDriverTests": true, + "WebKit/WebKit.xcworkspace": true, + "WebKit/WebKitLibraries": true, + "WebKit/Websites": true, + "WebKit/resources": true, + "WebKit/LayoutTests": true, + "WebKit/ManualTests": true, + "WebKit/PerformanceTests": true, + "WebKit/WebKitLegacy": true, + "WebKit/WebCore": true, + "WebKit/WebDriver": true, + "WebKit/WebKitBuild": true, + "WebKit/WebInspectorUI": true + }, } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ae67585340..cec77199e5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,10 +2,51 @@ "version": "2.0.0", "tasks": [ { - "label": "Rebuild Debug", - "command": "ninja", - "args": ["-Cbuild"], "type": "process", - } + "label": "Install Dependencies", + "command": "scripts/all-dependencies.sh", + "windows": { + "command": "scripts/all-dependencies.ps1" + }, + "icon": { + "id": "arrow-down" + }, + "options": { + "cwd": "${workspaceFolder}" + }, + }, + { + "type": "process", + "label": "Setup Environment", + "dependsOn": ["Install Dependencies"], + "command": "scripts/setup.sh", + "windows": { + "command": "scripts/setup.ps1" + }, + "icon": { + "id": "check" + }, + "options": { + "cwd": "${workspaceFolder}" + }, + }, + { + "type": "process", + "label": "Build Bun", + "dependsOn": ["Setup Environment"], + "command": "bun", + "args": ["run", "build"], + "icon": { + "id": "gear" + }, + "options": { + "cwd": "${workspaceFolder}" + }, + "isBuildCommand": true, + "runOptions": { + "instanceLimit": 1, + "reevaluateOnRerun": true, + }, + }, ] } diff --git a/CMakeLists.txt b/CMakeLists.txt index 208ef75dde..c23c4b2001 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,8 +2,8 @@ cmake_minimum_required(VERSION 3.22) cmake_policy(SET CMP0091 NEW) cmake_policy(SET CMP0067 NEW) -set(Bun_VERSION "1.0.26") -set(WEBKIT_TAG 9c501b9aa712b7959f80dc99491e8758c151c20e) +set(Bun_VERSION "1.1.0") +set(WEBKIT_TAG c3712c13dcdc091cfe4c7cb8f2c1fd16472e6f92) set(BUN_WORKDIR "${CMAKE_CURRENT_BINARY_DIR}") message(STATUS "Configuring Bun ${Bun_VERSION} in ${BUN_WORKDIR}") @@ -233,7 +233,7 @@ endif() if(UNIX AND NOT APPLE) execute_process(COMMAND cat /etc/os-release COMMAND head -n1 OUTPUT_VARIABLE LINUX_DISTRO) - if(${LINUX_DISTRO} MATCHES "NAME=\"(Arch|Manjaro) Linux\"\n") + if(${LINUX_DISTRO} MATCHES "NAME=\"(Arch|Manjaro|Artix) Linux\"\n") set(DEFAULT_USE_STATIC_LIBATOMIC OFF) endif() endif() diff --git a/Dockerfile b/Dockerfile index f0f9871de8..392db49f34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -290,7 +290,6 @@ ENV CCACHE_DIR=/ccache COPY Makefile ${BUN_DIR}/Makefile COPY src/deps/zstd ${BUN_DIR}/src/deps/zstd -COPY .prettierrc.cjs ${BUN_DIR}/.prettierrc.cjs WORKDIR $BUN_DIR @@ -378,7 +377,7 @@ RUN --mount=type=cache,target=/ccache mkdir ${BUN_DIR}/build \ FROM bun-base-with-zig as bun-codegen-for-zig -COPY package.json bun.lockb Makefile .gitmodules .prettierrc.cjs ${BUN_DIR}/ +COPY package.json bun.lockb Makefile .gitmodules ${BUN_DIR}/ COPY src/runtime ${BUN_DIR}/src/runtime COPY src/runtime.js src/runtime.bun.js ${BUN_DIR}/src/ COPY packages/bun-error ${BUN_DIR}/packages/bun-error diff --git a/Makefile b/Makefile index 580f1c37ff..8a6142c4ff 100644 --- a/Makefile +++ b/Makefile @@ -823,7 +823,6 @@ fmt: fmt-cpp fmt-zig api: ./node_modules/.bin/peechy --schema src/api/schema.peechy --esm src/api/schema.js --ts src/api/schema.d.ts --zig src/api/schema.zig $(ZIG) fmt src/api/schema.zig - $(PRETTIER) --config=.prettierrc.cjs --write src/api/schema.js src/api/schema.d.ts .PHONY: node-fallbacks node-fallbacks: diff --git a/bench/package.json b/bench/package.json index 1e9d5bc9bb..b91097050b 100644 --- a/bench/package.json +++ b/bench/package.json @@ -25,6 +25,5 @@ }, "devDependencies": { "fast-deep-equal": "^3.1.3" - }, - "prettier": "../.prettierrc.cjs" + } } diff --git a/bun.lockb b/bun.lockb index e7d7e711d7..2811f62f92 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/api/binary-data.md b/docs/api/binary-data.md index c6f9df35bb..98c9a21119 100644 --- a/docs/api/binary-data.md +++ b/docs/api/binary-data.md @@ -70,14 +70,14 @@ const buf = new ArrayBuffer(4); const dv = new DataView(buf); dv.setUint8(0, 3); // write value 3 at byte offset 0 dv.getUint8(0); // => 3 -// [0x11, 0x0, 0x0, 0x0] +// [0b00000011, 0b00000000, 0b00000000, 0b00000000] ``` Now let's write a `Uint16` at byte offset `1`. This requires two bytes. We're using the value `513`, which is `2 * 256 + 1`; in bytes, that's `00000010 00000001`. ```ts dv.setUint16(1, 513); -// [0x11, 0x10, 0x1, 0x0] +// [0b00000011, 0b00000010, 0b00000001, 0b00000000] console.log(dv.getUint16(1)); // => 513 ``` diff --git a/docs/api/transpiler.md b/docs/api/transpiler.md index ede4ee7cce..308a4e152d 100644 --- a/docs/api/transpiler.md +++ b/docs/api/transpiler.md @@ -50,7 +50,7 @@ export default jsx( To override the default loader specified in the `new Bun.Transpiler()` constructor, pass a second argument to `.transformSync()`. ```ts -await transpiler.transform("
hi!
", "tsx"); +transpiler.transformSync("
hi!
", "tsx"); ``` {% details summary="Nitty gritty" %} diff --git a/docs/cli/init.md b/docs/cli/init.md index f17c8e1b79..fefbe99830 100644 --- a/docs/cli/init.md +++ b/docs/cli/init.md @@ -35,6 +35,6 @@ It creates: If you pass `-y` or `--yes`, it will assume you want to continue without asking questions. -At the end, it runs `bun install` to install `bun-types`. +At the end, it runs `bun install` to install `@types/bun`. {% /details %} diff --git a/docs/cli/test.md b/docs/cli/test.md index 3dc58d0b5c..205d367f11 100644 --- a/docs/cli/test.md +++ b/docs/cli/test.md @@ -113,10 +113,6 @@ See [Test > Lifecycle](/docs/test/lifecycle) for complete documentation. ## Mocks -{% callout %} -Module mocking (`jest.mock()`) is not yet supported. Track support for it [here](https://github.com/oven-sh/bun/issues/5394). -{% /callout %} - Create mock functions with the `mock` function. Mocks are automatically reset between tests. ```ts diff --git a/docs/project/building-windows.md b/docs/project/building-windows.md index 9de2eae8eb..9fe9208bb0 100644 --- a/docs/project/building-windows.md +++ b/docs/project/building-windows.md @@ -114,6 +114,7 @@ bun install # or npm install .\scripts\env.ps1 .\scripts\update-submodules.ps1 # this syncs git submodule state +.\scripts\make-old-js.ps1 # runs some old code generators .\scripts\all-dependencies.ps1 # this builds all dependencies cd build # this was created by the codegen.ps1 script earlier diff --git a/docs/quickstart.md b/docs/quickstart.md index 2c818f15dc..9865c22f2b 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -45,18 +45,25 @@ console.log(`Listening on http://localhost:${server.port} ...`); {% details summary="Seeing TypeScript errors on `Bun`?" %} If you used `bun init`, Bun will have automatically installed Bun's TypeScript declarations and configured your `tsconfig.json`. If you're trying out Bun in an existing project, you may see a type error on the `Bun` global. -To fix this, first install `bun-types` as a dev dependency. +To fix this, first install `@types/bun` as a dev dependency. ```sh -$ bun add -d bun-types +$ bun add -d @types/bun ``` -Then add the following line to your `compilerOptions` in `tsconfig.json`. +Then add the following to your `compilerOptions` in `tsconfig.json`: -```json-diff#tsconfig.json +```json#tsconfig.json { "compilerOptions": { -+ "types": ["bun-types"] + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, } } ``` @@ -117,12 +124,12 @@ Update `index.ts` to use `figlet` in the `fetch` handler. + import figlet from "figlet"; const server = Bun.serve({ - fetch() { + port: 3000, + fetch(req) { + const body = figlet.textSync("Bun!"); + return new Response(body); - return new Response("Bun!"); }, - port: 3000, }); ``` diff --git a/docs/runtime/nodejs-apis.md b/docs/runtime/nodejs-apis.md index f11a9d3c19..ef3b8271a0 100644 --- a/docs/runtime/nodejs-apis.md +++ b/docs/runtime/nodejs-apis.md @@ -312,23 +312,23 @@ The table below lists all globals implemented by Node.js and Bun's current compa ### [`PerformanceEntry`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry) -🔴 Not implemented. +🟢 Fully implemented. ### [`PerformanceMark`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMark) -🔴 Not implemented. +🟢 Fully implemented. ### [`PerformanceMeasure`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMeasure) -🔴 Not implemented. +🟢 Fully implemented. ### [`PerformanceObserver`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver) -🔴 Not implemented. +🟢 Fully implemented. ### [`PerformanceObserverEntryList`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserverEntryList) -🔴 Not implemented. +🟢 Fully implemented. ### [`PerformanceResourceTiming`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming) @@ -356,11 +356,11 @@ The table below lists all globals implemented by Node.js and Bun's current compa ### [`ReadableStreamBYOBReader`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader) -🔴 Not implemented. +🟢 Fully implemented. ### [`ReadableStreamBYOBRequest`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBRequest) -🔴 Not implemented. +🟢 Fully implemented. ### [`ReadableStreamDefaultController`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultController) diff --git a/docs/test/writing.md b/docs/test/writing.md index dd9f19179d..e073ced734 100644 --- a/docs/test/writing.md +++ b/docs/test/writing.md @@ -109,7 +109,7 @@ $ bun test --todo ## `test.only` -To run a particular test or suite of tests use `test.only()` or `describe.only()`. Once declared, running `bun test --only` will only execute tests/suites that have been marked with `.only()`. +To run a particular test or suite of tests use `test.only()` or `describe.only()`. Once declared, running `bun test --only` will only execute tests/suites that have been marked with `.only()`. Running `bun test` without the `--only` option with `test.only()` declared will result in all tests in the given suite being executed _up to_ the test with `.only()`. `describe.only()` functions the same in both execution scenarios. ```ts import { test, describe } from "bun:test"; @@ -135,6 +135,12 @@ The following command will only execute tests #2 and #3. $ bun test --only ``` +The following command will only execute tests #1, #2 and #3. + +```sh +$ bun test +``` + ## `test.if` To run a test conditionally, use `test.if()`. The test will run if the condition is truthy. This is particularly useful for tests that should only run on specific architectures or operating systems. diff --git a/package.json b/package.json index defee9719e..4dc8e1453e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { + "private": true, "name": "bun", "dependencies": { - "@biomejs/biome": "^1.5.3", + "@biomejs/biome": "1.5.3", "@vscode/debugadapter": "^1.61.0", "esbuild": "^0.17.15", "eslint": "^8.20.0", @@ -14,7 +15,11 @@ "source-map-js": "^1.0.2", "typescript": "^5.0.2" }, - "private": true, + "devDependencies": { + "@types/react": "^18.0.25", + "@typescript-eslint/eslint-plugin": "^5.31.0", + "@typescript-eslint/parser": "^5.31.0" + }, "scripts": { "setup": "./scripts/setup.sh", "build": "if [ ! -e build ]; then bun setup; fi && ninja -C build", @@ -22,19 +27,12 @@ "build:release": "cmake . -DCMAKE_BUILD_TYPE=Release -GNinja -Bbuild-release && ninja -Cbuild-release", "build:safe": "cmake . -DZIG_OPTIMIZE=ReleaseSafe -DUSE_DEBUG_JSC=ON -DCMAKE_BUILD_TYPE=Release -GNinja -Bbuild-safe && ninja -Cbuild-safe", "typecheck": "tsc --noEmit && cd test && bun run typecheck", - "fmt": "biome format --write {src,test,bench,packages/{bun-types,bun-inspector-*,bun-vscode,bun-debug-adapter-protocol}}", + "fmt": "biome format --write {.vscode,src,test,bench,packages/{bun-types,bun-inspector-*,bun-vscode,bun-debug-adapter-protocol}}", "fmt:zig": "zig fmt src/*.zig src/**/*.zig", "lint": "eslint './**/*.d.ts' --cache", "lint:fix": "eslint './**/*.d.ts' --cache --fix", "test": "node packages/bun-internal-test/src/runner.node.mjs ./build/bun-debug", "test:release": "node packages/bun-internal-test/src/runner.node.mjs ./build-release/bun", "update-known-failures": "node packages/bun-internal-test/src/update-known-windows-failures.mjs" - }, - "devDependencies": { - "@types/react": "^18.0.25", - "@typescript-eslint/eslint-plugin": "^5.31.0", - "@typescript-eslint/parser": "^5.31.0" - }, - "version": "0.0.0", - "prettier": "./.prettierrc.cjs" + } } diff --git a/packages/bun-internal-test/src/runner.node.mjs b/packages/bun-internal-test/src/runner.node.mjs index b1b4e3bc6e..cec84753bf 100644 --- a/packages/bun-internal-test/src/runner.node.mjs +++ b/packages/bun-internal-test/src/runner.node.mjs @@ -107,9 +107,9 @@ async function runTest(path) { const expected_crash_reason = windows ? await readFile(resolve(path), "utf-8").then(data => { - const match = data.match(/@known-failing-on-windows:(.*)\n/); - return match ? match[1].trim() : null; - }) + const match = data.match(/@known-failing-on-windows:(.*)\n/); + return match ? match[1].trim() : null; + }) : null; const start = Date.now(); @@ -195,8 +195,7 @@ async function runTest(path) { } console.log( - `\x1b[2m${formatTime(duration).padStart(6, " ")}\x1b[0m ${ - passed ? "\x1b[32m✔" : expected_crash_reason ? "\x1b[33m⚠" : "\x1b[31m✖" + `\x1b[2m${formatTime(duration).padStart(6, " ")}\x1b[0m ${passed ? "\x1b[32m✔" : expected_crash_reason ? "\x1b[33m⚠" : "\x1b[31m✖" } ${name}\x1b[0m${reason ? ` (${reason})` : ""}`, ); @@ -320,10 +319,9 @@ console.log("\n" + "-".repeat(Math.min(process.stdout.columns || 40, 80)) + "\n" console.log(header); console.log("\n" + "-".repeat(Math.min(process.stdout.columns || 40, 80)) + "\n"); -let report = `# bun test on ${ - process.env["GITHUB_REF"] ?? +let report = `# bun test on ${process.env["GITHUB_REF"] ?? spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf-8" }).stdout.trim() -} + } \`\`\` ${header} @@ -347,8 +345,7 @@ if (regressions.length > 0) { report += regressions .map( ({ path, reason, expected_crash_reason }) => - `- [\`${path}\`](${sectionLink(path)}) ${reason}${ - expected_crash_reason ? ` (expected: ${expected_crash_reason})` : "" + `- [\`${path}\`](${sectionLink(path)}) ${reason}${expected_crash_reason ? ` (expected: ${expected_crash_reason})` : "" }`, ) .join("\n"); @@ -403,18 +400,14 @@ console.log("-> test-report.md, test-report.json"); if (ci) { if (windows) { - if (regressions.length > 0) { - action.setFailed(`${regressions.length} regressing tests`); - } action.setOutput("regressing_tests", regressions.map(({ path }) => `- \`${path}\``).join("\n")); action.setOutput("regressing_test_count", regressions.length); - } else { - if (failing_tests.length > 0) { - action.setFailed(`${failing_tests.length} files with failing tests`); - } - action.setOutput("failing_tests", failingTestDisplay); - action.setOutput("failing_tests_count", failing_tests.length); } + if (failing_tests.length > 0) { + action.setFailed(`${failing_tests.length} files with failing tests`); + } + action.setOutput("failing_tests", failingTestDisplay); + action.setOutput("failing_tests_count", failing_tests.length); let truncated_report = report; if (truncated_report.length > 512 * 1000) { truncated_report = truncated_report.slice(0, 512 * 1000) + "\n\n...truncated..."; diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 9c3fc0631a..ca6d4c4206 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3158,26 +3158,26 @@ declare module "bun" { * @param options Compression options to use * @returns The output buffer with the compressed data */ - function deflateSync(data: Uint8Array, options?: ZlibCompressionOptions): Uint8Array; + function deflateSync(data: Uint8Array | string | ArrayBuffer, options?: ZlibCompressionOptions): Uint8Array; /** * Compresses a chunk of data with `zlib` GZIP algorithm. * @param data The buffer of data to compress * @param options Compression options to use * @returns The output buffer with the compressed data */ - function gzipSync(data: Uint8Array, options?: ZlibCompressionOptions): Uint8Array; + function gzipSync(data: Uint8Array | string | ArrayBuffer, options?: ZlibCompressionOptions): Uint8Array; /** * Decompresses a chunk of data with `zlib` INFLATE algorithm. * @param data The buffer of data to decompress * @returns The output buffer with the decompressed data */ - function inflateSync(data: Uint8Array): Uint8Array; + function inflateSync(data: Uint8Array | string | ArrayBuffer): Uint8Array; /** * Decompresses a chunk of data with `zlib` GUNZIP algorithm. * @param data The buffer of data to decompress * @returns The output buffer with the decompressed data */ - function gunzipSync(data: Uint8Array): Uint8Array; + function gunzipSync(data: Uint8Array | string | ArrayBuffer): Uint8Array; type Target = /** diff --git a/packages/bun-types/test.d.ts b/packages/bun-types/test.d.ts index 50b3e03b39..43c84e50ee 100644 --- a/packages/bun-types/test.d.ts +++ b/packages/bun-types/test.d.ts @@ -346,7 +346,7 @@ declare module "bun:test" { options?: number | TestOptions, ): void; /** - * Skips all other tests, except this test. + * Skips all other tests, except this test when run with the `--only` option. * * @param label the label for the test * @param fn the test function diff --git a/packages/bun-usockets/src/bsd.c b/packages/bun-usockets/src/bsd.c index e520fa8721..4a58b984eb 100644 --- a/packages/bun-usockets/src/bsd.c +++ b/packages/bun-usockets/src/bsd.c @@ -269,6 +269,8 @@ LIBUS_SOCKET_DESCRIPTOR apple_no_sigpipe(LIBUS_SOCKET_DESCRIPTOR fd) { LIBUS_SOCKET_DESCRIPTOR bsd_set_nonblocking(LIBUS_SOCKET_DESCRIPTOR fd) { #ifdef _WIN32 /* Libuv will set windows sockets as non-blocking */ +#elif defined(__APPLE__) + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK | O_CLOEXEC); #else fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); #endif diff --git a/packages/bun-usockets/src/eventing/libuv.c b/packages/bun-usockets/src/eventing/libuv.c index c1db86c431..c7625114b6 100644 --- a/packages/bun-usockets/src/eventing/libuv.c +++ b/packages/bun-usockets/src/eventing/libuv.c @@ -198,7 +198,7 @@ void us_loop_free(struct us_loop_t *loop) { void us_loop_run(struct us_loop_t *loop) { us_loop_integrate(loop); - uv_run(loop->uv_loop, UV_RUN_NOWAIT); + uv_run(loop->uv_loop, UV_RUN_ONCE); } struct us_poll_t *us_create_poll(struct us_loop_t *loop, int fallthrough, @@ -327,11 +327,11 @@ void us_internal_async_wakeup(struct us_internal_async *a) { uv_async_send(uv_async); } -int us_socket_get_error(int ssl, struct us_socket_t* s) -{ +int us_socket_get_error(int ssl, struct us_socket_t *s) { int error = 0; socklen_t len = sizeof(error); - if (getsockopt(us_poll_fd((struct us_poll_t*)s), SOL_SOCKET, SO_ERROR, (char*)&error, &len) == -1) { + if (getsockopt(us_poll_fd((struct us_poll_t *)s), SOL_SOCKET, SO_ERROR, + (char *)&error, &len) == -1) { return errno; } return error; diff --git a/packages/bun-vscode/assets/package.json b/packages/bun-vscode/assets/package.json index 021c8125e7..b5faabfeb1 100644 --- a/packages/bun-vscode/assets/package.json +++ b/packages/bun-vscode/assets/package.json @@ -796,9 +796,6 @@ "eslintConfig": { "$ref": "https://json.schemastore.org/eslintrc.json" }, - "prettier": { - "$ref": "https://json.schemastore.org/prettierrc.json" - }, "stylelint": { "$ref": "https://json.schemastore.org/stylelintrc.json" }, diff --git a/scripts/build.ps1 b/scripts/build.ps1 index facf34749c..f70328c71a 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -1,2 +1,2 @@ -.\scripts\env.sh +.\scripts\env.ps1 ninja -Cbuild diff --git a/scripts/download-zig.sh b/scripts/download-zig.sh index cec996d47d..19860243a4 100755 --- a/scripts/download-zig.sh +++ b/scripts/download-zig.sh @@ -59,13 +59,11 @@ update_repo_if_needed() { done printf "Zig was updated to ${zig_version}. Please commit new files." - - # symlink extracted zig to extracted zig.exe - # TODO: Workaround for https://github.com/ziglang/vscode-zig/issues/164 - ln -sf "${extract_at}/zig" "${extract_at}/zig.exe" - chmod +x "${extract_at}/zig.exe" - fi + # symlink extracted zig to extracted zig.exe + # TODO: Workaround for https://github.com/ziglang/vscode-zig/issues/164 + ln -sf "${extract_at}/zig" "${extract_at}/zig.exe" + chmod +x "${extract_at}/zig.exe" } if [ -e "${extract_at}/.version" ]; then diff --git a/scripts/make-old-js.ps1 b/scripts/make-old-js.ps1 index f51122aa41..42cdf20c85 100644 --- a/scripts/make-old-js.ps1 +++ b/scripts/make-old-js.ps1 @@ -1,32 +1,32 @@ -$npm_client = "npm" - -# & ${npm_client} i - -$root = Join-Path (Split-Path -Path $MyInvocation.MyCommand.Definition -Parent) "..\" -$esbuild = Join-Path $root "node_modules\.bin\esbuild.cmd" - -$env:NODE_ENV = "production" - -# runtime.js -echo $esbuild -& ${esbuild} ` - "--target=esnext" "--bundle" ` - "src/runtime.bun.js" ` - "--format=esm" "--platform=node" "--minify" "--external:/bun:*" ` - "--outfile=src/runtime.out.js" -if ($LASTEXITCODE -ne 0) { throw "esbuild failed with exit code $LASTEXITCODE" } - -# fallback_decoder -& ${esbuild} --target=esnext --bundle src/fallback.ts --format=iife --platform=browser --minify > src/fallback.out.js - -# bun-error -Push-Location packages\bun-error -& ${npm_client} install -& ${npm_client} run build -Pop-Location - -# node-fallbacks -Push-Location src\node-fallbacks -& ${npm_client} install -& ${esbuild} --bundle @(Get-Item .\*.js) --outdir=out --format=esm --minify --platform=browser -Pop-Location +$npm_client = "npm" + +# & ${npm_client} i + +$root = Join-Path (Split-Path -Path $MyInvocation.MyCommand.Definition -Parent) "..\" +$esbuild = Join-Path $root "node_modules\.bin\esbuild.cmd" + +$env:NODE_ENV = "production" + +# runtime.js +echo $esbuild +& ${esbuild} ` + "--target=esnext" "--bundle" ` + "src/runtime.bun.js" ` + "--format=esm" "--platform=node" "--minify" "--external:/bun:*" ` + "--outfile=src/runtime.out.js" +if ($LASTEXITCODE -ne 0) { throw "esbuild failed with exit code $LASTEXITCODE" } + +# fallback_decoder +& ${esbuild} --target=esnext --bundle src/fallback.ts --format=iife --platform=browser --minify > src/fallback.out.js + +# bun-error +Push-Location packages\bun-error +& ${npm_client} install +& ${npm_client} run build +Pop-Location + +# node-fallbacks +Push-Location src\node-fallbacks +& ${npm_client} install +& ${esbuild} --bundle @(Get-Item .\*.js) --outdir=out --format=esm --minify --platform=browser +Pop-Location diff --git a/src/__global.zig b/src/__global.zig index fcb03d76d7..4c20917964 100644 --- a/src/__global.zig +++ b/src/__global.zig @@ -69,6 +69,8 @@ pub fn setThreadName(name: StringTypes.stringZ) void { _ = std.os.prctl(.SET_NAME, .{@intFromPtr(name.ptr)}) catch 0; } else if (Environment.isMac) { _ = std.c.pthread_setname_np(name); + } else if (Environment.isWindows) { + // _ = std.os.SetThreadDescription(std.os.GetCurrentThread(), name); } } diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index a332cd3bc5..840b05d350 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -1107,22 +1107,24 @@ pub const Formatter = struct { return .{ .tag = switch (js_type) { - JSValue.JSType.ErrorInstance => .Error, - JSValue.JSType.NumberObject => .Double, - JSValue.JSType.DerivedArray, JSValue.JSType.Array => .Array, - JSValue.JSType.DerivedStringObject, JSValue.JSType.String, JSValue.JSType.StringObject => .String, - JSValue.JSType.RegExpObject => .String, - JSValue.JSType.Symbol => .Symbol, - JSValue.JSType.BooleanObject => .Boolean, - JSValue.JSType.JSFunction => .Function, - JSValue.JSType.JSWeakMap, JSValue.JSType.JSMap => .Map, - JSValue.JSType.JSMapIterator => .MapIterator, - JSValue.JSType.JSSetIterator => .SetIterator, - JSValue.JSType.JSWeakSet, JSValue.JSType.JSSet => .Set, - JSValue.JSType.JSDate => .JSON, - JSValue.JSType.JSPromise => .Promise, - JSValue.JSType.Object, - JSValue.JSType.FinalObject, + .ErrorInstance => .Error, + .NumberObject => .Double, + .DerivedArray, JSValue.JSType.Array => .Array, + .DerivedStringObject, JSValue.JSType.String, JSValue.JSType.StringObject => .String, + .RegExpObject => .String, + .Symbol => .Symbol, + .BooleanObject => .Boolean, + .JSFunction => .Function, + .JSWeakMap, JSValue.JSType.JSMap => .Map, + .JSMapIterator => .MapIterator, + .JSSetIterator => .SetIterator, + .JSWeakSet, JSValue.JSType.JSSet => .Set, + .JSDate => .JSON, + .JSPromise => .Promise, + + .Object, + .FinalObject, + .ProxyObject, .ModuleNamespaceObject, => .Object, @@ -1132,17 +1134,17 @@ pub const Formatter = struct { .GlobalObject, .ArrayBuffer, - JSValue.JSType.Int8Array, - JSValue.JSType.Uint8Array, - JSValue.JSType.Uint8ClampedArray, - JSValue.JSType.Int16Array, - JSValue.JSType.Uint16Array, - JSValue.JSType.Int32Array, - JSValue.JSType.Uint32Array, - JSValue.JSType.Float32Array, - JSValue.JSType.Float64Array, - JSValue.JSType.BigInt64Array, - JSValue.JSType.BigUint64Array, + .Int8Array, + .Uint8Array, + .Uint8ClampedArray, + .Int16Array, + .Uint16Array, + .Int32Array, + .Uint32Array, + .Float32Array, + .Float64Array, + .BigInt64Array, + .BigUint64Array, .DataView, => .TypedArray, diff --git a/src/bun.js/RuntimeTranspilerCache.zig b/src/bun.js/RuntimeTranspilerCache.zig index 891e3e4866..0bc5536c6a 100644 --- a/src/bun.js/RuntimeTranspilerCache.zig +++ b/src/bun.js/RuntimeTranspilerCache.zig @@ -210,16 +210,16 @@ pub const RuntimeTranspilerCache = struct { break :brk metadata_buf[0..metadata_stream.pos]; }; - const vecs: []const std.os.iovec_const = if (output_bytes.len > 0) + const vecs: []const bun.PlatformIOVecConst = if (output_bytes.len > 0) &.{ - .{ .iov_base = metadata_bytes.ptr, .iov_len = metadata_bytes.len }, - .{ .iov_base = output_bytes.ptr, .iov_len = output_bytes.len }, - .{ .iov_base = sourcemap.ptr, .iov_len = sourcemap.len }, + bun.platformIOVecConstCreate(metadata_bytes), + bun.platformIOVecConstCreate(output_bytes), + bun.platformIOVecConstCreate(sourcemap), } else &.{ - .{ .iov_base = metadata_bytes.ptr, .iov_len = metadata_bytes.len }, - .{ .iov_base = sourcemap.ptr, .iov_len = sourcemap.len }, + bun.platformIOVecConstCreate(metadata_bytes), + bun.platformIOVecConstCreate(sourcemap), }; var position: isize = 0; @@ -228,8 +228,13 @@ pub const RuntimeTranspilerCache = struct { if (bun.Environment.allow_assert) { var total: usize = 0; for (vecs) |v| { - std.debug.assert(v.iov_len > 0); - total += v.iov_len; + if (comptime bun.Environment.isWindows) { + std.debug.assert(v.len > 0); + total += v.len; + } else { + std.debug.assert(v.iov_len > 0); + total += v.iov_len; + } } std.debug.assert(end_position == total); } @@ -246,7 +251,7 @@ pub const RuntimeTranspilerCache = struct { } } - try tmpfile.finish(destination_path.sliceAssumeZ()); + try tmpfile.finish(@ptrCast(std.fs.path.basename(destination_path.slice()))); } pub fn load( @@ -481,6 +486,7 @@ pub const RuntimeTranspilerCache = struct { const file = cache_fd.asFile(); const metadata_bytes = try file.preadAll(&metadata_bytes_buf, 0); + if (comptime bun.Environment.isWindows) try file.seekTo(0); var metadata_stream = std.io.fixedBufferStream(metadata_bytes_buf[0..metadata_bytes]); var entry = Entry{ @@ -539,7 +545,7 @@ pub const RuntimeTranspilerCache = struct { const cache_dir_fd = brk: { if (std.fs.path.dirname(cache_file_path)) |dirname| { const dir = try std.fs.cwd().makeOpenPath(dirname, .{ .access_sub_paths = true }); - break :brk bun.toFD(dir.fd); + break :brk bun.toLibUVOwnedFD(dir.fd); } break :brk bun.toFD(std.fs.cwd().fd); diff --git a/src/bun.js/WebKit b/src/bun.js/WebKit index 347037014a..c3712c13dc 160000 --- a/src/bun.js/WebKit +++ b/src/bun.js/WebKit @@ -1 +1 @@ -Subproject commit 347037014ae069eed1c4f4687001a256949b124e +Subproject commit c3712c13dcdc091cfe4c7cb8f2c1fd16472e6f92 diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index a1c36bd840..1781799e79 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -3076,7 +3076,7 @@ pub export fn Bun__escapeHTML16(globalObject: *JSC.JSGlobalObject, input_value: std.debug.assert( std.mem.eql( u16, - (strings.toUTF16Alloc(bun.default_allocator, strings.toUTF8Alloc(bun.default_allocator, escaped_html) catch unreachable, false) catch unreachable).?, + (strings.toUTF16Alloc(bun.default_allocator, strings.toUTF8Alloc(bun.default_allocator, escaped_html) catch unreachable, false, false) catch unreachable).?, escaped_html, ), ); diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig index 7985273b4f..b57b61c48a 100644 --- a/src/bun.js/api/glob.zig +++ b/src/bun.js/api/glob.zig @@ -51,31 +51,23 @@ const ScanOpts = struct { return null; }; - break :cwd_str_raw ZigString.Slice.from(duped, allocator); + break :cwd_str_raw ZigString.Slice.init(allocator, duped); } - // Conver to utf-16 - const utf16 = (bun.strings.toUTF16Alloc( + // Convert to utf-16 + const utf16 = bun.strings.toUTF16AllocForReal( allocator, cwd_zig_str.slice(), // Let windows APIs handle errors with invalid surrogate pairs, etc. false, + false, ) catch { globalThis.throwOutOfMemory(); return null; - }) orelse brk: { - // All ascii - const output = allocator.alloc(u16, cwd_zig_str.len) catch { - globalThis.throwOutOfMemory(); - return null; - }; - - bun.strings.copyU8IntoU16(output, cwd_zig_str.slice()); - break :brk output; }; const ptr: [*]u8 = @ptrCast(utf16.ptr); - break :cwd_str_raw ZigString.Slice.from(ptr[0 .. utf16.len * 2], allocator); + break :cwd_str_raw ZigString.Slice.init(allocator, ptr[0 .. utf16.len * 2]); } // `.toSlice()` internally converts to WTF-8 diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 2d6aef1963..a383be2ed8 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -32,6 +32,10 @@ #include #include #include +// Using the same typedef and define for `mode_t` and `umask` as node on windows. +// https://github.com/nodejs/node/blob/ad5e2dab4c8306183685973387829c2f69e793da/src/node_process_methods.cc#L29 +#define umask _umask +typedef int mode_t; #endif #include "JSNextTickQueue.h" #include "ProcessBindingUV.h" @@ -342,8 +346,6 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { -#if !OS(WINDOWS) - if (callFrame->argumentCount() == 0 || callFrame->argument(0).isUndefined()) { mode_t currentMask = umask(0); umask(currentMask); @@ -376,9 +378,6 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionUmask, } return JSC::JSValue::encode(JSC::jsNumber(umask(newUmask))); -#else - return JSC::JSValue::encode(JSC::jsNumber(0)); -#endif } extern "C" uint64_t Bun__readOriginTimer(void*); diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/CommonJSModuleRecord.cpp index cdcb36451d..48c03939b6 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/CommonJSModuleRecord.cpp @@ -66,6 +66,7 @@ #include #include #include +#include "PathInlines.h" extern "C" bool Bun__isBunMain(JSC::JSGlobalObject* global, const BunString*); @@ -305,7 +306,7 @@ JSC_DEFINE_CUSTOM_GETTER(getterPath, (JSC::JSGlobalObject * globalObject, JSC::E if (UNLIKELY(!thisObject)) { return JSValue::encode(jsUndefined()); } - return JSValue::encode(thisObject->m_id.get()); + return JSValue::encode(thisObject->m_dirname.get()); } JSC_DEFINE_CUSTOM_GETTER(getterParent, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) @@ -342,7 +343,7 @@ JSC_DEFINE_CUSTOM_SETTER(setterPath, if (!thisObject) return false; - thisObject->m_id.set(globalObject->vm(), thisObject, JSValue::decode(value).toString(globalObject)); + thisObject->m_dirname.set(globalObject->vm(), thisObject, JSValue::decode(value).toString(globalObject)); return true; } @@ -469,7 +470,7 @@ JSC_DEFINE_HOST_FUNCTION(functionCommonJSModuleRecord_compile, (JSGlobalObject * JSSourceCode* jsSourceCode = JSSourceCode::create(vm, WTFMove(sourceCode)); moduleObject->sourceCode.set(vm, moduleObject, jsSourceCode); - auto index = filenameString.reverseFind('/', filenameString.length()); + auto index = filenameString.reverseFind(PLATFORM_SEP, filenameString.length()); String dirnameString; if (index != WTF::notFound) { dirnameString = filenameString.substring(0, index); @@ -621,10 +622,14 @@ JSCommonJSModule* JSCommonJSModule::create( { auto& vm = globalObject->vm(); JSString* requireMapKey = JSC::jsStringWithCache(vm, key); - auto index = key.reverseFind('/', key.length()); - JSString* dirname = jsEmptyString(vm); + + auto index = key.reverseFind(PLATFORM_SEP, key.length()); + + JSString* dirname; if (index != WTF::notFound) { dirname = JSC::jsSubstring(globalObject, requireMapKey, 0, index); + } else { + dirname = jsEmptyString(vm); } auto* out = JSCommonJSModule::create( @@ -918,7 +923,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionRequireCommonJS, (JSGlobalObject * lexicalGlo BunString typeAttributeStr = { BunStringTag::Dead }; String typeAttribute = String(); - // We need to be able to wire in the "type" import attribute from bundled code.. // so we do it via CommonJS require(). int32_t previousArgumentCount = callframe->argument(2).asInt32(); @@ -1019,11 +1023,13 @@ std::optional createCommonJSModule( if (!moduleObject) { auto& vm = globalObject->vm(); auto* requireMapKey = jsStringWithCache(vm, sourceURL); - auto index = sourceURL.reverseFind('/', sourceURL.length()); - JSString* dirname = jsEmptyString(vm); + auto index = sourceURL.reverseFind(PLATFORM_SEP, sourceURL.length()); + JSString* dirname; JSString* filename = requireMapKey; if (index != WTF::notFound) { dirname = JSC::jsSubstring(globalObject, requireMapKey, 0, index); + } else { + dirname = jsEmptyString(vm); } moduleObject = JSCommonJSModule::create( @@ -1089,10 +1095,12 @@ JSObject* JSCommonJSModule::createBoundRequireFunction(VM& vm, JSGlobalObject* l auto* globalObject = jsCast(lexicalGlobalObject); JSString* filename = JSC::jsStringWithCache(vm, pathString); - auto index = pathString.reverseFind('/', pathString.length()); - JSString* dirname = jsEmptyString(vm); + auto index = pathString.reverseFind(PLATFORM_SEP, pathString.length()); + JSString* dirname; if (index != WTF::notFound) { dirname = JSC::jsSubstring(globalObject, filename, 0, index); + } else { + dirname = jsEmptyString(vm); } auto moduleObject = Bun::JSCommonJSModule::create( diff --git a/src/bun.js/bindings/JSEnvironmentVariableMap.cpp b/src/bun.js/bindings/JSEnvironmentVariableMap.cpp index b23eac87d0..d0514ae781 100644 --- a/src/bun.js/bindings/JSEnvironmentVariableMap.cpp +++ b/src/bun.js/bindings/JSEnvironmentVariableMap.cpp @@ -5,7 +5,13 @@ #include #include +#include +#include +#include +#include + #include "BunClientData.h" + using namespace JSC; extern "C" size_t Bun__getEnvCount(JSGlobalObject* globalObject, void** list_ptr); @@ -48,7 +54,11 @@ JSC_DEFINE_CUSTOM_SETTER(jsSetterEnvironmentVariable, (JSGlobalObject * globalOb if (!object) return false; - object->putDirect(vm, propertyName, JSValue::decode(value), 0); + auto string = JSValue::decode(value).toString(globalObject); + if (UNLIKELY(!string)) + return false; + + object->putDirect(vm, propertyName, string, 0); return true; } @@ -125,19 +135,30 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject) object = constructEmptyObject(globalObject, globalObject->objectPrototype()); } +#if OS(WINDOWS) + JSArray* keyArray = constructEmptyArray(globalObject, nullptr, count); +#endif + static NeverDestroyed TZ = MAKE_STATIC_STRING_IMPL("TZ"); bool hasTZ = false; for (size_t i = 0; i < count; i++) { unsigned char* chars; size_t len = Bun__getEnvKey(list, i, &chars); auto name = String::fromUTF8(chars, len); +#if OS(WINDOWS) + keyArray->putByIndexInline(globalObject, (unsigned)i, jsString(vm, name), false); +#endif if (name == TZ) { hasTZ = true; continue; } ASSERT(len > 0); - - Identifier identifier = Identifier::fromString(vm, name); +#if OS(WINDOWS) + String idName = name.convertToASCIIUppercase(); +#else + String idName = name; +#endif + Identifier identifier = Identifier::fromString(vm, idName); // CustomGetterSetter doesn't support indexed properties yet. // This causes strange issues when the environment variable name is an integer. @@ -165,6 +186,26 @@ JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject) vm, Identifier::fromString(vm, TZ), JSC::CustomGetterSetter::create(vm, jsTimeZoneEnvironmentVariableGetter, jsTimeZoneEnvironmentVariableSetter), TZAttrs); +#if OS(WINDOWS) + JSC::JSFunction* getSourceEvent = JSC::JSFunction::create(vm, processObjectInternalsWindowsEnvCodeGenerator(vm), globalObject); + RETURN_IF_EXCEPTION(scope, {}); + JSC::MarkedArgumentBuffer args; + args.append(object); + args.append(keyArray); + auto clientData = WebCore::clientData(vm); + JSC::CallData callData = JSC::getCallData(getSourceEvent); + NakedPtr returnedException = nullptr; + auto result = JSC::call(globalObject, getSourceEvent, callData, globalObject->globalThis(), args, returnedException); + RETURN_IF_EXCEPTION(scope, {}); + + if (returnedException) { + throwException(globalObject, scope, returnedException.get()); + return jsUndefined(); + } + + RELEASE_AND_RETURN(scope, result); +#else return object; +#endif } } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index a6ab105e58..c1fd071cc7 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -121,7 +121,7 @@ pub const ZigString = extern struct { } pub fn dupeForJS(utf8: []const u8, allocator: std.mem.Allocator) !ZigString { - if (try strings.toUTF16Alloc(allocator, utf8, false)) |utf16| { + if (try strings.toUTF16Alloc(allocator, utf8, false, false)) |utf16| { var out = ZigString.init16(utf16); out.mark(); out.markUTF16(); @@ -432,13 +432,6 @@ pub const ZigString = extern struct { pub const byteSlice = Slice.slice; - pub fn from(input: []u8, allocator: std.mem.Allocator) Slice { - return .{ - .ptr = input.ptr, - .len = @as(u32, @truncate(input.len)), - .allocator = NullableAllocator.init(allocator), - }; - } pub fn fromUTF8NeverFree(input: []const u8) Slice { return .{ diff --git a/src/bun.js/bindings/c-bindings.cpp b/src/bun.js/bindings/c-bindings.cpp index 1576fa4e20..91f646e7cc 100644 --- a/src/bun.js/bindings/c-bindings.cpp +++ b/src/bun.js/bindings/c-bindings.cpp @@ -160,3 +160,27 @@ extern "C" int clock_gettime_monotonic(int64_t* tv_sec, int64_t* tv_nsec) return 0; } #endif + +#if OS(LINUX) + +#include + +// close_range is glibc > 2.33, which is very new +static ssize_t bun_close_range(unsigned int start, unsigned int end, unsigned int flags) +{ + return syscall(__NR_close_range, start, end, flags); +} + +extern "C" void on_before_reload_process_linux() +{ + // close all file descriptors except stdin, stdout, stderr and possibly IPC. + // if you're passing additional file descriptors to Bun, you're probably not passing more than 8. + bun_close_range(8, ~0U, 0U); + + // reset all signals to default + sigset_t signal_set; + sigfillset(&signal_set); + sigprocmask(SIG_SETMASK, &signal_set, nullptr); +} + +#endif \ No newline at end of file diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index 6b4460a4f9..9ed16ea24c 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -2089,6 +2089,25 @@ extern "C" napi_status napi_get_element(napi_env env, napi_value objectValue, return napi_ok; } +extern "C" napi_status napi_delete_element(napi_env env, napi_value objectValue, + uint32_t index, bool* result) +{ + NAPI_PREMABLE + + JSValue jsValue = toJS(objectValue); + if (UNLIKELY(!env || !jsValue || !jsValue.isObject())) { + return napi_invalid_arg; + } + + JSObject* object = jsValue.getObject(); + + auto scope = DECLARE_THROW_SCOPE(object->vm()); + *result = JSObject::deletePropertyByIndex(object, toJS(env), index); + RETURN_IF_EXCEPTION(scope, napi_generic_failure); + + return napi_ok; +} + extern "C" napi_status napi_create_object(napi_env env, napi_value* result) { NAPI_PREMABLE diff --git a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp index b0d0f0cdfc..b381eaca39 100644 --- a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp +++ b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp @@ -96,6 +96,16 @@ static void enableFastMallocForSQLite() #endif } +class AutoDestructingSQLiteStatement { +public: + sqlite3_stmt* stmt { nullptr }; + + ~AutoDestructingSQLiteStatement() + { + sqlite3_finalize(stmt); + } +}; + static void initializeSQLite() { static std::once_flag onceFlag; @@ -899,6 +909,11 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction, (JSC::JSGlobalObje RELEASE_AND_RETURN(scope, JSValue::encode(JSC::jsUndefined())); } +static bool isSkippedInSQLiteQuery(const char c) +{ + return c == ' ' || c == ';' || (c >= '\t' && c <= '\r'); +} + // This runs a query one-off // without the overhead of a long-lived statement object // does not return anything @@ -945,65 +960,92 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteFunction, (JSC::JSGlobalObject * l return JSValue::encode(JSC::jsUndefined()); } - // TODO: trim whitespace & newlines before sending - // we don't because webkit doesn't expose a function that makes this super - // easy without using unicode whitespace definition the - // StringPrototype.trim() implementation GC allocates a new JSString* and - // potentially re-allocates the string (not 100% sure if reallocates) so we - // can't use that here - sqlite3_stmt* statement = nullptr; + CString utf8; + + const char* sqlStringHead; + const char* end; + bool didSetBindings = false; - int rc = SQLITE_OK; if ( // fast path: ascii latin1 string is utf8 sqlString.is8Bit() && simdutf::validate_ascii(reinterpret_cast(sqlString.characters8()), sqlString.length())) { - rc = sqlite3_prepare_v3(db, reinterpret_cast(sqlString.characters8()), sqlString.length(), 0, &statement, nullptr); + + sqlStringHead = reinterpret_cast(sqlString.characters8()); + end = sqlStringHead + sqlString.length(); } else { // slow path: utf16 or latin1 string with supplemental characters - CString utf8 = sqlString.utf8(); - rc = sqlite3_prepare_v3(db, utf8.data(), utf8.length(), 0, &statement, nullptr); + utf8 = sqlString.utf8(); + sqlStringHead = utf8.data(); + end = sqlStringHead + utf8.length(); } - if (UNLIKELY(rc != SQLITE_OK || statement == nullptr)) { - throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, rc == SQLITE_OK ? "Query contained no valid SQL statement; likely empty query."_s : WTF::String::fromUTF8(sqlite3_errmsg(db)))); - // sqlite3 handles when the pointer is null - sqlite3_finalize(statement); - return JSValue::encode(JSC::jsUndefined()); - } + bool didExecuteAny = false; - if (!bindingsAliveScope.value().isUndefinedOrNull()) { - if (bindingsAliveScope.value().isObject()) { - JSC::JSValue reb = rebindStatement(lexicalGlobalObject, bindingsAliveScope.value(), scope, db, statement, false); - if (UNLIKELY(!reb.isNumber())) { - sqlite3_finalize(statement); - return JSValue::encode(reb); /* this means an error */ - } - } else { - throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Expected bindings to be an object or array"_s)); - sqlite3_finalize(statement); - return JSValue::encode(jsUndefined()); + int rc = SQLITE_OK; + +#if ASSERT_ENABLED + int maxSqlStringBytes = end - sqlStringHead; +#endif + + while (sqlStringHead && sqlStringHead < end) { + if (UNLIKELY(isSkippedInSQLiteQuery(*sqlStringHead))) { + sqlStringHead++; + + while (sqlStringHead < end && isSkippedInSQLiteQuery(*sqlStringHead)) + sqlStringHead++; } + + AutoDestructingSQLiteStatement sql; + const char* tail = nullptr; + + // Bounds checks + ASSERT(end >= sqlStringHead); + ASSERT(end - sqlStringHead >= 0); + ASSERT(end - sqlStringHead <= maxSqlStringBytes); + + rc = sqlite3_prepare_v3(db, sqlStringHead, end - sqlStringHead, 0, &sql.stmt, &tail); + + if (rc != SQLITE_OK) + break; + + if (!sql.stmt) { + // this happens for an empty statement + sqlStringHead = tail; + continue; + } + + // First statement gets the bindings. + if (!didSetBindings && !bindingsAliveScope.value().isUndefinedOrNull()) { + if (bindingsAliveScope.value().isObject()) { + JSC::JSValue reb = rebindStatement(lexicalGlobalObject, bindingsAliveScope.value(), scope, db, sql.stmt, false); + if (UNLIKELY(!reb.isNumber())) { + return JSValue::encode(reb); /* this means an error */ + } + } else { + throwException(lexicalGlobalObject, scope, createTypeError(lexicalGlobalObject, "Expected bindings to be an object or array"_s)); + return JSValue::encode(jsUndefined()); + } + didSetBindings = true; + } + + do { + rc = sqlite3_step(sql.stmt); + } while (rc == SQLITE_ROW); + + didExecuteAny = true; + sqlStringHead = tail; } - rc = sqlite3_step(statement); - if (!sqlite3_stmt_readonly(statement)) { - databases()[handle]->version++; - } - - while (rc == SQLITE_ROW) { - rc = sqlite3_step(statement); - } - - if (UNLIKELY(rc != SQLITE_DONE && rc != SQLITE_OK)) { + if (UNLIKELY(rc != SQLITE_OK && rc != SQLITE_DONE)) { throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, db)); - // we finalize after just incase something about error messages in - // sqlite depends on the existence of the most recent statement i don't - // think that's actually how this works - just being cautious - sqlite3_finalize(statement); return JSValue::encode(JSC::jsUndefined()); } - sqlite3_finalize(statement); + if (!didExecuteAny) { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Query contained no valid SQL statement; likely empty query."_s)); + return JSValue::encode(JSC::jsUndefined()); + } + return JSValue::encode(jsUndefined()); } diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index d91fbee5ad..fd842fc6f9 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -83,6 +83,7 @@ const PendingResolution = @import("../resolver/resolver.zig").PendingResolution; const ThreadSafeFunction = JSC.napi.ThreadSafeFunction; const PackageManager = @import("../install/install.zig").PackageManager; const IPC = @import("ipc.zig"); +pub const GenericWatcher = @import("../watcher.zig"); const ModuleLoader = JSC.ModuleLoader; const FetchFlags = JSC.FetchFlags; @@ -430,22 +431,22 @@ pub const ImportWatcher = union(enum) { pub fn start(this: ImportWatcher) !void { switch (this) { - inline .hot => |watcher| try watcher.start(), - inline .watch => |watcher| try watcher.start(), + inline .hot => |w| try w.start(), + inline .watch => |w| try w.start(), else => {}, } } - pub inline fn watchlist(this: ImportWatcher) Watcher.WatchListArray { + pub inline fn watchlist(this: ImportWatcher) GenericWatcher.WatchList { return switch (this) { - inline .hot, .watch => |wacher| wacher.watchlist, + inline .hot, .watch => |w| w.watchlist, else => .{}, }; } - pub inline fn indexOf(this: ImportWatcher, hash: Watcher.HashType) ?u32 { + pub inline fn indexOf(this: ImportWatcher, hash: GenericWatcher.HashType) ?u32 { return switch (this) { - inline .hot, .watch => |wacher| wacher.indexOf(hash), + inline .hot, .watch => |w| w.indexOf(hash), else => null, }; } @@ -454,7 +455,7 @@ pub const ImportWatcher = union(enum) { this: ImportWatcher, fd: StoredFileDescriptorType, file_path: string, - hash: Watcher.HashType, + hash: GenericWatcher.HashType, loader: options.Loader, dir_fd: StoredFileDescriptorType, package_json: ?*PackageJSON, @@ -2146,7 +2147,7 @@ pub const VirtualMachine = struct { pub fn reloadEntryPoint(this: *VirtualMachine, entry_path: []const u8) !*JSInternalPromise { this.has_loaded = false; this.main = entry_path; - this.main_hash = bun.JSC.Watcher.getHash(entry_path); + this.main_hash = GenericWatcher.getHash(entry_path); try this.entry_point.generate( this.allocator, @@ -2184,7 +2185,7 @@ pub const VirtualMachine = struct { pub fn reloadEntryPointForTestRunner(this: *VirtualMachine, entry_path: []const u8) !*JSInternalPromise { this.has_loaded = false; this.main = entry_path; - this.main_hash = bun.JSC.Watcher.getHash(entry_path); + this.main_hash = GenericWatcher.getHash(entry_path); this.eventLoop().ensureWaker(); @@ -3078,11 +3079,10 @@ extern fn BunDebugger__willHotReload() void; pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime reload_immediately: bool) type { return struct { - const watcher = @import("../watcher.zig"); - pub const Watcher = watcher.NewWatcher(*@This()); + pub const Watcher = GenericWatcher.NewWatcher(*@This()); const Reloader = @This(); - onAccept: std.ArrayHashMapUnmanaged(@This().Watcher.HashType, bun.BabyList(OnAcceptCallback), bun.ArrayIdentityContext, false) = .{}, + onAccept: std.ArrayHashMapUnmanaged(GenericWatcher.HashType, bun.BabyList(OnAcceptCallback), bun.ArrayIdentityContext, false) = .{}, ctx: *Ctx, verbose: bool = false, @@ -3221,7 +3221,7 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime // - Directories outside the root directory // - Directories inside node_modules if (std.mem.indexOf(u8, file_path, "node_modules") == null and std.mem.indexOf(u8, file_path, watch.fs.top_level_dir) != null) { - watch.addDirectory(dir_fd, file_path, @This().Watcher.getHash(file_path), false) catch {}; + watch.addDirectory(dir_fd, file_path, GenericWatcher.getHash(file_path), false) catch {}; } } @@ -3254,9 +3254,9 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime pub fn onFileUpdate( this: *@This(), - events: []watcher.WatchEvent, + events: []GenericWatcher.WatchEvent, changed_files: []?[:0]u8, - watchlist: watcher.Watchlist, + watchlist: GenericWatcher.WatchList, ) void { var slice = watchlist.slice(); const file_paths = slice.items(.file_path); @@ -3318,6 +3318,13 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime } }, .directory => { + if (comptime Environment.isWindows) { + // on windows we receive file events for all items affected by a directory change + // so we only need to clear the directory cache. all other effects will be handled + // by the file events + resolver.bustDirCache(file_path); + continue; + } var affected_buf: [128][]const u8 = undefined; var entries_option: ?*Fs.FileSystem.RealFS.EntriesOption = null; @@ -3368,7 +3375,7 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime resolver.bustDirCache(file_path); if (entries_option) |dir_ent| { - var last_file_hash: @This().Watcher.HashType = std.math.maxInt(@This().Watcher.HashType); + var last_file_hash: GenericWatcher.HashType = std.math.maxInt(GenericWatcher.HashType); for (affected) |changed_name_| { const changed_name: []const u8 = if (comptime Environment.isMac) @@ -3381,14 +3388,14 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime var prev_entry_id: usize = std.math.maxInt(usize); if (loader != .file) { var path_string: bun.PathString = undefined; - var file_hash: @This().Watcher.HashType = last_file_hash; + var file_hash: GenericWatcher.HashType = last_file_hash; const abs_path: string = brk: { if (dir_ent.entries.get(@as([]const u8, @ptrCast(changed_name)))) |file_ent| { // reset the file descriptor file_ent.entry.cache.fd = .zero; file_ent.entry.need_stat = true; path_string = file_ent.entry.abs_path; - file_hash = @This().Watcher.getHash(path_string.slice()); + file_hash = GenericWatcher.getHash(path_string.slice()); for (hashes, 0..) |hash, entry_id| { if (hash == file_hash) { if (file_descriptors[entry_id] != .zero) { @@ -3416,7 +3423,7 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime @memcpy(_on_file_update_path_buf[file_path_without_trailing_slash.len..][0..changed_name.len], changed_name); const path_slice = _on_file_update_path_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1]; - file_hash = @This().Watcher.getHash(path_slice); + file_hash = GenericWatcher.getHash(path_slice); break :brk path_slice; } }; diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 9ffc358cfe..e1f7f21d56 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -401,7 +401,7 @@ pub const RuntimeTranspilerStore = struct { var fd: ?StoredFileDescriptorType = null; var package_json: ?*PackageJSON = null; - const hash = JSC.Watcher.getHash(path.text); + const hash = JSC.GenericWatcher.getHash(path.text); switch (vm.bun_watcher) { .hot, .watch => { @@ -1447,7 +1447,7 @@ pub const ModuleLoader = struct { .js, .jsx, .ts, .tsx, .json, .toml, .text => { jsc_vm.transpiled_count += 1; jsc_vm.bundler.resetStore(); - const hash = JSC.Watcher.getHash(path.text); + const hash = JSC.GenericWatcher.getHash(path.text); const is_main = jsc_vm.main.len == path.text.len and jsc_vm.main_hash == hash and strings.eqlLong(jsc_vm.main, path.text, false); diff --git a/src/bun.js/node/dir_iterator.zig b/src/bun.js/node/dir_iterator.zig index 761f426365..a5912b148a 100644 --- a/src/bun.js/node/dir_iterator.zig +++ b/src/bun.js/node/dir_iterator.zig @@ -225,16 +225,14 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { self.first = false; if (io.Information == 0) { - bun.sys.syslog("NtQueryDirectoryFile({d}) = 0", .{ - @intFromPtr(self.dir.fd), - }); + bun.sys.syslog("NtQueryDirectoryFile({}) = 0", .{bun.toFD(self.dir.fd)}); return .{ .result = null }; } self.index = 0; self.end_index = io.Information; // If the handle is not a directory, we'll get STATUS_INVALID_PARAMETER. if (rc == .INVALID_PARAMETER) { - bun.sys.syslog("NtQueryDirectoryFile({d}) = {s}", .{ @intFromPtr(self.dir.fd), @tagName(rc) }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ bun.toFD(self.dir.fd), @tagName(rc) }); return .{ .err = .{ .errno = @intFromEnum(bun.C.SystemErrno.ENOTDIR), @@ -244,13 +242,13 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { } if (rc == .NO_MORE_FILES) { - bun.sys.syslog("NtQueryDirectoryFile({d}) = {s}", .{ @intFromPtr(self.dir.fd), @tagName(rc) }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ bun.toFD(self.dir.fd), @tagName(rc) }); self.end_index = self.index; return .{ .result = null }; } if (rc != .SUCCESS) { - bun.sys.syslog("NtQueryDirectoryFile({d}) = {s}", .{ @intFromPtr(self.dir.fd), @tagName(rc) }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ bun.toFD(self.dir.fd), @tagName(rc) }); if ((bun.windows.Win32Error.fromNTStatus(rc).toSystemErrno())) |errno| { return .{ @@ -269,7 +267,7 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { }; } - bun.sys.syslog("NtQueryDirectoryFile({d}) = {d}", .{ @intFromPtr(self.dir.fd), self.end_index }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {d}", .{ bun.toFD(self.dir.fd), self.end_index }); } const dir_info: *w.FILE_DIRECTORY_INFORMATION = @ptrCast(@alignCast(&self.buf[self.index])); diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 2e505b8de4..ac9b00aef1 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -4486,7 +4486,11 @@ pub const NodeFS = struct { } pub fn open(this: *NodeFS, args: Arguments.Open, comptime _: Flavor) Maybe(Return.Open) { - const path = args.path.sliceZ(&this.sync_error_buf); + const path = if (Environment.isWindows and bun.strings.eqlComptime(args.path.slice(), "/dev/null")) + "\\\\.\\NUL" + else + args.path.sliceZ(&this.sync_error_buf); + return switch (Syscall.open(path, @intFromEnum(args.flags), args.mode)) { .err => |err| .{ .err = err.withPath(args.path.slice()), @@ -5269,13 +5273,52 @@ pub const NodeFS = struct { pub fn writeFileWithPathBuffer(pathbuf: *[bun.MAX_PATH_BYTES]u8, args: Arguments.WriteFile) Maybe(Return.WriteFile) { var path: [:0]const u8 = undefined; + var pathbuf2: [bun.MAX_PATH_BYTES]u8 = undefined; const fd = switch (args.file) { .path => brk: { - path = args.file.path.sliceZ(pathbuf); + // On Windows, we potentially mutate the path in posixToPlatformInPlace + // We cannot mutate JavaScript strings in-place. That will break many things. + // So we must always copy the path string on Windows. + path = args.file.path.sliceZWithForceCopy(pathbuf, Environment.isWindows); + bun.path.posixToPlatformInPlace(u8, @constCast(path)); + + var is_dirfd_different = false; + var dirfd = args.dirfd; + if (Environment.isWindows) { + while (std.mem.startsWith(u8, path, "..\\")) { + is_dirfd_different = true; + var buffer: bun.WPathBuffer = undefined; + const dirfd_path_len = std.os.windows.kernel32.GetFinalPathNameByHandleW(args.dirfd.cast(), &buffer, buffer.len, 0); + const dirfd_path = buffer[0..dirfd_path_len]; + const parent_path = bun.Dirname.dirname(u16, dirfd_path).?; + if (std.mem.startsWith(u16, parent_path, &bun.windows.nt_maxpath_prefix)) @constCast(parent_path)[1] = '?'; + const newdirfd = switch (bun.sys.openDirAtWindows(bun.invalid_fd, parent_path, false, true)) { + .result => |fd| fd, + .err => |err| { + return .{ .err = err.withPath(path) }; + }, + }; + path = path[3..]; + dirfd = newdirfd; + } + } + defer if (is_dirfd_different) { + var d = dirfd.asDir(); + d.close(); + }; + if (Environment.isWindows) { + // windows openat does not support path traversal, fix it here. + // use pathbuf2 here since without it 'panic: @memcpy arguments alias' triggers + if (std.mem.indexOf(u8, path, "\\.\\") != null or std.mem.indexOf(u8, path, "\\..\\") != null) { + const fixed_path = bun.path.normalizeStringWindows(path, &pathbuf2, false, false); + pathbuf2[fixed_path.len] = 0; + path = pathbuf2[0..fixed_path.len :0]; + } + } const open_result = Syscall.openat( - args.dirfd, + dirfd, path, @intFromEnum(args.flag) | os.O.NOCTTY, args.mode, @@ -5986,7 +6029,16 @@ pub const NodeFS = struct { } const flags = os.O.DIRECTORY | os.O.RDONLY; - const fd = switch (Syscall.openatOSPath(bun.toFD((std.fs.cwd().fd)), src, flags, 0)) { + var wbuf: if (Environment.isWindows) bun.WPathBuffer else void = undefined; + const fd = switch (Syscall.openatOSPath( + bun.toFD((std.fs.cwd().fd)), + if (Environment.isWindows and std.fs.path.isAbsoluteWindowsWTF16(src)) + bun.strings.addNTPathPrefixIfNeeded(&wbuf, src) + else + src, + flags, + 0, + )) { .err => |err| { return .{ .err = err.withPath(this.osPathIntoSyncErrorBuf(src)) }; }, @@ -6323,9 +6375,13 @@ pub const NodeFS = struct { if (Environment.isWindows) { const result = windows.CopyFileW(src, dest, @intFromBool(mode.shouldntOverwrite())); - if (Maybe(Return.CopyFile).errnoSysP(result, .copyfile, this.osPathIntoSyncErrorBuf(src))) |e| { - return e; + if (result == bun.windows.FALSE) { + if (Maybe(Return.CopyFile).errnoSysP(result, .copyfile, this.osPathIntoSyncErrorBuf(src))) |e| { + return e; + } } + + return ret.success; } return ret.todo(); diff --git a/src/bun.js/node/path_watcher.zig b/src/bun.js/node/path_watcher.zig index c2740a753d..63cd1d5f27 100644 --- a/src/bun.js/node/path_watcher.zig +++ b/src/bun.js/node/path_watcher.zig @@ -13,6 +13,7 @@ const StoredFileDescriptorType = bun.StoredFileDescriptorType; const string = bun.string; const JSC = bun.JSC; const VirtualMachine = JSC.VirtualMachine; +const GenericWatcher = @import("../../watcher.zig"); const sync = @import("../../sync.zig"); const Semaphore = sync.Semaphore; @@ -21,7 +22,6 @@ var default_manager_mutex: Mutex = Mutex.init(); var default_manager: ?*PathWatcherManager = null; pub const PathWatcherManager = struct { - const GenericWatcher = @import("../../watcher.zig"); const options = @import("../../options.zig"); pub const Watcher = GenericWatcher.NewWatcher(*PathWatcherManager); const log = Output.scoped(.PathWatcherManager, false); @@ -43,7 +43,7 @@ pub const PathWatcherManager = struct { path: [:0]const u8, dirname: string, refs: u32 = 0, - hash: Watcher.HashType, + hash: GenericWatcher.HashType, }; fn refPendingTask(this: *PathWatcherManager) bool { @@ -96,7 +96,7 @@ pub const PathWatcherManager = struct { .is_file = false, .path = cloned_path, .dirname = cloned_path, - .hash = Watcher.getHash(cloned_path), + .hash = GenericWatcher.getHash(cloned_path), .refs = 1, }; _ = try this.file_paths.put(cloned_path, result); @@ -110,7 +110,7 @@ pub const PathWatcherManager = struct { .path = cloned_path, // if is really a file we need to get the dirname .dirname = std.fs.path.dirname(cloned_path) orelse cloned_path, - .hash = Watcher.getHash(cloned_path), + .hash = GenericWatcher.getHash(cloned_path), .refs = 1, }; _ = try this.file_paths.put(cloned_path, result); @@ -154,7 +154,7 @@ pub const PathWatcherManager = struct { this: *PathWatcherManager, events: []GenericWatcher.WatchEvent, changed_files: []?[:0]u8, - watchlist: GenericWatcher.Watchlist, + watchlist: GenericWatcher.WatchList, ) void { var slice = watchlist.slice(); const file_paths = slice.items(.file_path); @@ -197,7 +197,7 @@ pub const PathWatcherManager = struct { if (event.op.write or event.op.delete or event.op.rename) { const event_type: PathWatcher.EventType = if (event.op.delete or event.op.rename or event.op.move_to) .rename else .change; - const hash = Watcher.getHash(file_path); + const hash = GenericWatcher.getHash(file_path); for (watchers) |w| { if (w) |watcher| { @@ -268,7 +268,7 @@ pub const PathWatcherManager = struct { const len = file_path_without_trailing_slash.len + changed_name.len; const path_slice = _on_file_update_path_buf[0 .. len + 1]; - const hash = Watcher.getHash(path_slice); + const hash = GenericWatcher.getHash(path_slice); // skip consecutive duplicates const event_type: PathWatcher.EventType = .rename; // renaming folders, creating folder or files will be always be rename @@ -688,7 +688,7 @@ pub const PathWatcher = struct { has_pending_directories: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), closed: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), pub const ChangeEvent = struct { - hash: PathWatcherManager.Watcher.HashType = 0, + hash: GenericWatcher.HashType = 0, event_type: EventType = .change, time_stamp: i64 = 0, }; @@ -805,7 +805,7 @@ pub const PathWatcher = struct { } } - pub fn emit(this: *PathWatcher, path: string, hash: PathWatcherManager.Watcher.HashType, time_stamp: i64, is_file: bool, event_type: EventType) void { + pub fn emit(this: *PathWatcher, path: string, hash: GenericWatcher.HashType, time_stamp: i64, is_file: bool, event_type: EventType) void { const time_diff = time_stamp - this.last_change_event.time_stamp; // skip consecutive duplicates if ((this.last_change_event.time_stamp == 0 or time_diff > 1) or this.last_change_event.event_type != event_type and this.last_change_event.hash != hash) { diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index cb220276a1..9e3481b048 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -150,6 +150,9 @@ pub fn Maybe(comptime ResultType: type) type { } pub inline fn errnoSys(rc: anytype, syscall: Syscall.Tag) ?@This() { + if (comptime Environment.isWindows) { + if (rc != 0) return null; + } return switch (Syscall.getErrno(rc)) { .SUCCESS => null, else => |err| @This(){ @@ -173,6 +176,9 @@ pub fn Maybe(comptime ResultType: type) type { } pub inline fn errnoSysFd(rc: anytype, syscall: Syscall.Tag, fd: bun.FileDescriptor) ?@This() { + if (comptime Environment.isWindows) { + if (rc != 0) return null; + } return switch (Syscall.getErrno(rc)) { .SUCCESS => null, else => |err| @This(){ @@ -190,6 +196,9 @@ pub fn Maybe(comptime ResultType: type) type { if (std.meta.Child(@TypeOf(path)) == u16) { @compileError("Do not pass WString path to errnoSysP, it needs the path encoded as utf8"); } + if (comptime Environment.isWindows) { + if (rc != 0) return null; + } return switch (Syscall.getErrno(rc)) { .SUCCESS => null, else => |err| @This(){ @@ -207,7 +216,6 @@ pub fn Maybe(comptime ResultType: type) type { fn translateToErrInt(err: anytype) bun.sys.Error.Int { return switch (@TypeOf(err)) { - bun.windows.Win32Error => @intFromEnum(bun.windows.translateWinErrorToErrno(err)), bun.windows.NTSTATUS => @intFromEnum(bun.windows.translateNTStatusToErrno(err)), else => @truncate(@intFromEnum(err)), }; @@ -432,7 +440,7 @@ pub const StringOrBuffer = union(enum) { defer global.vm().reportExtraMemory(out.len); return .{ - .encoded_slice = JSC.ZigString.Slice.from(out, bun.default_allocator), + .encoded_slice = JSC.ZigString.Slice.init(bun.default_allocator, out), }; } @@ -658,12 +666,17 @@ pub const PathLike = union(enum) { pub fn sliceZWithForceCopy(this: PathLike, buf: *[bun.MAX_PATH_BYTES]u8, comptime force: bool) [:0]const u8 { const sliced = this.slice(); + if (Environment.isWindows) { + if (std.fs.path.isAbsolute(sliced)) { + return resolve_path.PosixToWinNormalizer.resolveCWDWithExternalBufZ(buf, sliced) catch @panic("Error while resolving path."); + } + } + if (sliced.len == 0) return ""; if (comptime !force) { if (sliced[sliced.len - 1] == 0) { - var sliced_ptr = sliced.ptr; - return sliced_ptr[0 .. sliced.len - 1 :0]; + return sliced[0 .. sliced.len - 1 :0]; } } @@ -673,13 +686,6 @@ pub const PathLike = union(enum) { } pub inline fn sliceZ(this: PathLike, buf: *[bun.MAX_PATH_BYTES]u8) [:0]const u8 { - if (Environment.isWindows) { - const data = this.slice(); - if (!std.fs.path.isAbsolute(data)) { - return sliceZWithForceCopy(this, buf, false); - } - return resolve_path.PosixToWinNormalizer.resolveCWDWithExternalBufZ(buf, data) catch @panic("Error while resolving path."); - } return sliceZWithForceCopy(this, buf, false); } diff --git a/src/bun.js/node/win_watcher.zig b/src/bun.js/node/win_watcher.zig index c94608d3ef..bcff289446 100644 --- a/src/bun.js/node/win_watcher.zig +++ b/src/bun.js/node/win_watcher.zig @@ -9,17 +9,15 @@ const JSC = bun.JSC; const VirtualMachine = JSC.VirtualMachine; const StoredFileDescriptorType = bun.StoredFileDescriptorType; const Output = bun.Output; +const Watcher = @import("../../watcher.zig"); var default_manager: ?*PathWatcherManager = null; // TODO: make this a generic so we can reuse code with path_watcher // TODO: we probably should use native instead of libuv abstraction here for better performance pub const PathWatcherManager = struct { - const GenericWatcher = @import("../../watcher.zig"); const options = @import("../../options.zig"); - pub const Watcher = GenericWatcher.NewWatcher(*PathWatcherManager); const log = Output.scoped(.PathWatcherManager, false); - main_watcher: *Watcher, watchers: bun.BabyList(?*PathWatcher) = .{}, watcher_count: u32 = 0, @@ -85,55 +83,31 @@ pub const PathWatcherManager = struct { var this = PathWatcherManager.new(.{ .file_paths = bun.StringHashMap(PathInfo).init(bun.default_allocator), .watchers = watchers, - .main_watcher = undefined, .vm = vm, .watcher_count = 0, }); errdefer this.destroy(); - this.main_watcher = try Watcher.init( - this, - vm.bundler.fs, - bun.default_allocator, - ); - - errdefer this.main_watcher.deinit(false); - - try this.main_watcher.start(); return this; } - fn _addDirectory(this: *PathWatcherManager, _: *PathWatcher, path: PathInfo) !void { - const fd = path.fd; - try this.main_watcher.addDirectory(fd, path.path, path.hash, false); - } - fn registerWatcher(this: *PathWatcherManager, watcher: *PathWatcher) !void { - { - if (this.watcher_count == this.watchers.len) { - this.watcher_count += 1; - this.watchers.push(bun.default_allocator, watcher) catch |err| { - this.watcher_count -= 1; - return err; - }; - } else { - var watchers = this.watchers.slice(); - for (watchers, 0..) |w, i| { - if (w == null) { - watchers[i] = watcher; - this.watcher_count += 1; - break; - } + if (this.watcher_count == this.watchers.len) { + this.watcher_count += 1; + this.watchers.push(bun.default_allocator, watcher) catch |err| { + this.watcher_count -= 1; + return err; + }; + } else { + var watchers = this.watchers.slice(); + for (watchers, 0..) |w, i| { + if (w == null) { + watchers[i] = watcher; + this.watcher_count += 1; + break; } } } - - const path = watcher.path; - if (path.is_file) { - try this.main_watcher.addFile(path.fd, path.path, path.hash, options.Loader.file, .zero, null, false); - } else { - try this._addDirectory(watcher, path); - } } fn _incrementPathRef(this: *PathWatcherManager, file_path: [:0]const u8) void { @@ -152,7 +126,6 @@ pub const PathWatcherManager = struct { path.refs -= 1; if (path.refs == 0) { const path_ = path.path; - this.main_watcher.remove(path.hash); _ = this.file_paths.remove(path_); bun.default_allocator.free(path_); } @@ -198,8 +171,6 @@ pub const PathWatcherManager = struct { return; } - this.main_watcher.deinit(false); - if (this.watcher_count > 0) { while (this.watchers.popOrNull()) |watcher| { if (watcher) |w| { @@ -242,7 +213,7 @@ pub const PathWatcher = struct { const log = Output.scoped(.PathWatcher, false); pub const ChangeEvent = struct { - hash: PathWatcherManager.Watcher.HashType = 0, + hash: Watcher.HashType = 0, event_type: EventType = .change, time_stamp: i64 = 0, }; @@ -330,7 +301,7 @@ pub const PathWatcher = struct { return this; } - pub fn emit(this: *PathWatcher, path: string, hash: PathWatcherManager.Watcher.HashType, time_stamp: i64, is_file: bool, event_type: EventType) void { + pub fn emit(this: *PathWatcher, path: string, hash: Watcher.HashType, time_stamp: i64, is_file: bool, event_type: EventType) void { const time_diff = time_stamp - this.last_change_event.time_stamp; // skip consecutive duplicates if ((this.last_change_event.time_stamp == 0 or time_diff > 1) or this.last_change_event.event_type != event_type and this.last_change_event.hash != hash) { diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index e343654878..30983c99f5 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -1519,21 +1519,27 @@ pub const Blob = struct { return ptr.toJS(globalObject); } - pub fn findOrCreateFileFromPath(path_: *JSC.Node.PathOrFileDescriptor, globalThis: *JSGlobalObject) Blob { + pub fn findOrCreateFileFromPath(path_or_fd: *JSC.Node.PathOrFileDescriptor, globalThis: *JSGlobalObject) Blob { var vm = globalThis.bunVM(); const allocator = bun.default_allocator; const path: JSC.Node.PathOrFileDescriptor = brk: { - switch (path_.*) { + switch (path_or_fd.*) { .path => { - const slice = path_.path.slice(); + const slice = path_or_fd.path.slice(); + + if (Environment.isWindows and bun.strings.eqlComptime(slice, "/dev/null")) { + // it is okay to use rodata here, because the '.string' case + // in PathLike.deinit does not free anything. + path_or_fd.* = .{ .path = .{ .string = bun.PathString.init("\\\\.\\NUL") } }; + } if (vm.standalone_module_graph) |graph| { if (graph.find(slice)) |file| { defer { - if (path_.path != .string) { - path_.deinit(); - path_.* = .{ .path = .{ .string = bun.PathString.empty } }; + if (path_or_fd.path != .string) { + path_or_fd.deinit(); + path_or_fd.* = .{ .path = .{ .string = bun.PathString.empty } }; } } @@ -1541,13 +1547,13 @@ pub const Blob = struct { } } - path_.toThreadSafe(); - const copy = path_.*; - path_.* = .{ .path = .{ .string = bun.PathString.empty } }; + path_or_fd.toThreadSafe(); + const copy = path_or_fd.*; + path_or_fd.* = .{ .path = .{ .string = bun.PathString.empty } }; break :brk copy; }, .fd => { - switch (bun.FDTag.get(path_.fd)) { + switch (bun.FDTag.get(path_or_fd.fd)) { .stdin => return Blob.initWithStore( vm.rareData().stdin(), globalThis, @@ -1562,7 +1568,7 @@ pub const Blob = struct { ), else => {}, } - break :brk path_.*; + break :brk path_or_fd.*; }, } }; @@ -2892,14 +2898,9 @@ pub const Blob = struct { return JSValue.jsBoolean(true); } - if (comptime Environment.isWindows) { - this.globalThis.throwTODO("exists is not implemented on Windows"); - return JSValue.jsUndefined(); - } - // We say regular files and pipes exist. // This is mostly meant for "Can we use this in new Response(file)?" - return JSValue.jsBoolean(bun.isRegularFile(store.data.file.mode) or std.os.S.ISFIFO(store.data.file.mode)); + return JSValue.jsBoolean(bun.isRegularFile(store.data.file.mode) or bun.C.S.ISFIFO(store.data.file.mode)); } // This mostly means 'can it be read?' @@ -3631,7 +3632,10 @@ pub const Blob = struct { if (could_be_all_ascii == null or !could_be_all_ascii.?) { // if toUTF16Alloc returns null, it means there are no non-ASCII characters // instead of erroring, invalid characters will become a U+FFFD replacement character - if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch unreachable) |external| { + if (strings.toUTF16Alloc(bun.default_allocator, buf, false, false) catch { + global.throwOutOfMemory(); + return .zero; + }) |external| { if (lifetime != .temporary) this.setIsASCIIFlag(false); @@ -3731,7 +3735,7 @@ pub const Blob = struct { var stack_fallback = std.heap.stackFallback(4096, bun.default_allocator); const allocator = stack_fallback.get(); // if toUTF16Alloc returns null, it means there are no non-ASCII characters - if (strings.toUTF16Alloc(allocator, buf, false) catch null) |external| { + if (strings.toUTF16Alloc(allocator, buf, false, false) catch null) |external| { if (comptime lifetime != .temporary) this.setIsASCIIFlag(false); const result = ZigString.init16(external).toJSONObject(global); allocator.free(external); @@ -4389,7 +4393,7 @@ pub const InternalBlob = struct { pub fn toStringOwned(this: *@This(), globalThis: *JSC.JSGlobalObject) JSValue { const bytes_without_bom = strings.withoutUTF8BOM(this.bytes.items); - if (strings.toUTF16Alloc(globalThis.allocator(), bytes_without_bom, false) catch &[_]u16{}) |out| { + if (strings.toUTF16Alloc(globalThis.allocator(), bytes_without_bom, false, false) catch &[_]u16{}) |out| { const return_value = ZigString.toExternalU16(out.ptr, out.len, globalThis); return_value.ensureStillAlive(); this.deinit(); diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig index 7c87810c06..50b1737953 100644 --- a/src/bun.js/webcore/encoding.zig +++ b/src/bun.js/webcore/encoding.zig @@ -641,7 +641,7 @@ pub const TextDecoder = struct { buffer_slice; if (this.fatal) { - if (toUTF16(default_allocator, moved_buffer_slice_8, true)) |result_| { + if (toUTF16(default_allocator, moved_buffer_slice_8, true, false)) |result_| { if (result_) |result| { return ZigString.toExternalU16(result.ptr, result.len, globalThis); } @@ -660,7 +660,7 @@ pub const TextDecoder = struct { } } } else { - if (toUTF16(default_allocator, moved_buffer_slice_8, false)) |result_| { + if (toUTF16(default_allocator, moved_buffer_slice_8, false, false)) |result_| { if (result_) |result| { return ZigString.toExternalU16(result.ptr, result.len, globalThis); } @@ -899,7 +899,7 @@ pub const Encoder = struct { return bun.String.createExternalGloballyAllocated(.latin1, input); }, .buffer, .utf8 => { - const converted = strings.toUTF16Alloc(bun.default_allocator, input, false) catch return bun.String.dead; + const converted = strings.toUTF16Alloc(bun.default_allocator, input, false, false) catch return bun.String.dead; if (converted) |utf16| { defer bun.default_allocator.free(input); return bun.String.createExternalGloballyAllocated(.utf16, utf16); @@ -978,7 +978,7 @@ pub const Encoder = struct { return str.toJS(global); }, .buffer, .utf8 => { - const converted = strings.toUTF16Alloc(allocator, input, false) catch return ZigString.init("Out of memory").toErrorInstance(global); + const converted = strings.toUTF16Alloc(allocator, input, false, false) catch return ZigString.init("Out of memory").toErrorInstance(global); if (converted) |utf16| { return ZigString.toExternalU16(utf16.ptr, utf16.len, global); } diff --git a/src/bun.zig b/src/bun.zig index 2b1a606cf0..7050e0b7e6 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -42,6 +42,7 @@ pub const resolver = @import("./resolver//resolver.zig"); pub const DirIterator = @import("./bun.js/node/dir_iterator.zig"); pub const PackageJSON = @import("./resolver/package_json.zig").PackageJSON; pub const fmt = @import("./fmt.zig"); +pub const allocators = @import("./allocators.zig"); pub const shell = struct { pub usingnamespace @import("./shell/shell.zig"); @@ -103,6 +104,14 @@ pub const FileDescriptor = enum(FileDescriptorInt) { try FDImpl.format(FDImpl.decode(fd), fmt_, options_, writer); } + pub fn assertValid(fd: FileDescriptor) void { + FDImpl.decode(fd).assertValid(); + } + + pub fn isValid(fd: FileDescriptor) bool { + return FDImpl.decode(fd).isValid(); + } + pub fn assertKind(fd: FileDescriptor, kind: FDImpl.Kind) void { std.debug.assert(FDImpl.decode(fd).kind == kind); } @@ -122,6 +131,11 @@ pub const PlatformIOVec = if (Environment.isWindows) else std.os.iovec; +pub const PlatformIOVecConst = if (Environment.isWindows) + windows.libuv.uv_buf_t +else + std.os.iovec_const; + pub fn platformIOVecCreate(input: []const u8) PlatformIOVec { if (Environment.isWindows) return windows.libuv.uv_buf_t.init(input); if (Environment.allow_assert) { @@ -132,6 +146,16 @@ pub fn platformIOVecCreate(input: []const u8) PlatformIOVec { return .{ .iov_len = @intCast(input.len), .iov_base = @constCast(input.ptr) }; } +pub fn platformIOVecConstCreate(input: []const u8) PlatformIOVecConst { + if (Environment.isWindows) return windows.libuv.uv_buf_t.init(input); + if (Environment.allow_assert) { + if (input.len > @as(usize, std.math.maxInt(u32))) { + Output.debugWarn("call to bun.PlatformIOVecConst.init with length larger than u32, this will overflow on windows", .{}); + } + } + return .{ .iov_len = @intCast(input.len), .iov_base = input.ptr }; +} + pub fn platformIOVecToSlice(iovec: PlatformIOVec) []u8 { if (Environment.isWindows) return windows.libuv.uv_buf_t.slice(iovec); return iovec.base[0..iovec.len]; @@ -529,6 +553,16 @@ pub inline fn isSliceInBuffer(slice: []const u8, buffer: []const u8) bool { return slice.len > 0 and @intFromPtr(buffer.ptr) <= @intFromPtr(slice.ptr) and ((@intFromPtr(slice.ptr) + slice.len) <= (@intFromPtr(buffer.ptr) + buffer.len)); } +pub inline fn sliceInBuffer(stable: string, value: string) string { + if (allocators.sliceRange(stable, value)) |_| { + return value; + } + if (strings.indexOf(stable, value)) |index| { + return stable[index..][0..value.len]; + } + return value; +} + pub fn rangeOfSliceInBuffer(slice: []const u8, buffer: []const u8) ?[2]u32 { if (!isSliceInBuffer(slice, buffer)) return null; const r = [_]u32{ @@ -1383,9 +1417,31 @@ pub const failing_allocator = std.mem.Allocator{ .ptr = undefined, .vtable = &.{ pub fn reloadProcess( allocator: std.mem.Allocator, clear_terminal: bool, -) void { - const PosixSpawn = posix.spawn; +) noreturn { + if (clear_terminal) { + Output.flush(); + Output.disableBuffering(); + Output.resetTerminalAll(); + } const bun = @This(); + + if (comptime Environment.isWindows) { + // this assumes that our parent process assigned us to a job object (see runWatcherManager) + var procinfo: std.os.windows.PROCESS_INFORMATION = undefined; + win32.spawnProcessCopy(allocator, &procinfo, false, false) catch |err| { + Output.panic("Error while reloading process: {s}", .{@errorName(err)}); + }; + + // terminate the current process + const rc = bun.windows.TerminateProcess(@ptrFromInt(std.math.maxInt(usize)), 0); + if (rc == 0) { + const err = bun.windows.GetLastError(); + Output.panic("Error while reloading process: {s}", .{@tagName(err)}); + } else { + Output.panic("Unexpected error while reloading process\n", .{}); + } + } + const PosixSpawn = posix.spawn; const dupe_argv = allocator.allocSentinel(?[*:0]const u8, bun.argv().len, null) catch unreachable; for (bun.argv(), dupe_argv) |src, *dest| { dest.* = (allocator.dupeZ(u8, src) catch unreachable).ptr; @@ -1410,20 +1466,16 @@ pub fn reloadProcess( // we clone envp so that the memory address of environment variables isn't the same as the libc one const envp = @as([*:null]?[*:0]const u8, @ptrCast(environ.ptr)); - // Clear the terminal - if (clear_terminal) { - Output.flush(); - Output.disableBuffering(); - Output.resetTerminalAll(); - } - // macOS doesn't have CLOEXEC, so we must go through posix_spawn if (comptime Environment.isMac) { var actions = PosixSpawn.Actions.init() catch unreachable; actions.inherit(posix.STDIN_FD) catch unreachable; actions.inherit(posix.STDOUT_FD) catch unreachable; actions.inherit(posix.STDERR_FD) catch unreachable; + var attrs = PosixSpawn.Attr.init() catch unreachable; + attrs.resetSignals() catch {}; + attrs.set( C.POSIX_SPAWN_CLOEXEC_DEFAULT | // Apple Extension: If this bit is set, rather @@ -1437,15 +1489,24 @@ pub fn reloadProcess( .err => |err| { Output.panic("Unexpected error while reloading: {d} {s}", .{ err.errno, @tagName(err.getErrno()) }); }, - .result => |_| {}, + .result => |_| { + Output.panic("Unexpected error while reloading: posix_spawn returned a result", .{}); + }, } - } else { + } else if (comptime Environment.isPosix) { + const on_before_reload_process_linux = struct { + pub extern "C" fn on_before_reload_process_linux() void; + }.on_before_reload_process_linux; + + on_before_reload_process_linux(); const err = std.os.execveZ( exec_path, newargv, envp, ); Output.panic("Unexpected error while reloading: {s}", .{@errorName(err)}); + } else { + @compileError("unsupported platform for reloadProcess"); } } pub var auto_reload_on_crash = false; @@ -1853,10 +1914,13 @@ pub const posix = struct { }; pub const win32 = struct { + const w = std.os.windows; pub var STDOUT_FD: FileDescriptor = undefined; pub var STDERR_FD: FileDescriptor = undefined; pub var STDIN_FD: FileDescriptor = undefined; + const watcherChildEnv: [:0]const u16 = strings.toUTF16LiteralZ("_BUN_WATCHER_CHILD"); + pub fn stdio(i: anytype) FileDescriptor { return switch (i) { 0 => STDIN_FD, @@ -1867,6 +1931,158 @@ pub const win32 = struct { } pub const spawn = @import("./bun.js/api/bun/spawn.zig").PosixSpawn; + + pub fn isWatcherChild() bool { + var buf: [1]u16 = undefined; + return windows.GetEnvironmentVariableW(@constCast(watcherChildEnv.ptr), &buf, 1) > 0; + } + + pub fn becomeWatcherManager(allocator: std.mem.Allocator) noreturn { + // this process will be the parent of the child process that actually runs the script + // based on https://devblogs.microsoft.com/oldnewthing/20130405-00/?p=4743 + const job = windows.CreateJobObjectA(null, null); + const iocp = windows.CreateIoCompletionPort(windows.INVALID_HANDLE_VALUE, null, 0, 1) orelse { + Output.panic("Failed to create IOCP\n", .{}); + }; + var assoc = windows.JOBOBJECT_ASSOCIATE_COMPLETION_PORT{ + .CompletionKey = job, + .CompletionPort = iocp, + }; + if (windows.SetInformationJobObject(job, windows.JobObjectAssociateCompletionPortInformation, &assoc, @sizeOf(windows.JOBOBJECT_ASSOCIATE_COMPLETION_PORT)) == 0) { + const err = windows.GetLastError(); + Output.panic("Failed to associate completion port: {s}\n", .{@tagName(err)}); + } + + var procinfo: std.os.windows.PROCESS_INFORMATION = undefined; + spawnProcessCopy(allocator, &procinfo, true, true) catch |err| { + Output.panic("Failed to spawn process: {s}\n", .{@errorName(err)}); + }; + if (windows.AssignProcessToJobObject(job, procinfo.hProcess) == 0) { + const err = windows.GetLastError(); + Output.panic("Failed to assign process to job object: {s}\n", .{@tagName(err)}); + } + if (windows.ResumeThread(procinfo.hThread) == 0) { + const err = windows.GetLastError(); + Output.panic("Failed to resume child process: {s}\n", .{@tagName(err)}); + } + + var completion_code: w.DWORD = 0; + var completion_key: w.ULONG_PTR = 0; + var overlapped: ?*w.OVERLAPPED = null; + var last_pid: w.DWORD = 0; + while (true) { + if (w.kernel32.GetQueuedCompletionStatus(iocp, &completion_code, &completion_key, &overlapped, w.INFINITE) == 0) { + const err = windows.GetLastError(); + Output.panic("Failed to query completion status: {s}\n", .{@tagName(err)}); + } + // only care about events concerning our job object (theoretically unnecessary) + if (completion_key != @intFromPtr(job)) { + continue; + } + if (completion_code == windows.JOB_OBJECT_MSG_EXIT_PROCESS) { + last_pid = @truncate(@intFromPtr(overlapped)); + } else if (completion_code == windows.JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO) { + break; + } + } + // NOTE: for now we always exit with a zero exit code. + // This is because there's no straightforward way to communicate the exit code + // of subsequently spawned child processes to the original parent process. + Global.exit(0); + } + + pub fn spawnProcessCopy( + allocator: std.mem.Allocator, + procinfo: *std.os.windows.PROCESS_INFORMATION, + suspended: bool, + setChild: bool, + ) !void { + var flags: std.os.windows.DWORD = w.CREATE_UNICODE_ENVIRONMENT; + if (suspended) { + // see CREATE_SUSPENDED at + // https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags + flags |= 0x00000004; + } + + const image_path = &w.peb().ProcessParameters.ImagePathName; + var wbuf: WPathBuffer = undefined; + @memcpy(wbuf[0..image_path.Length], image_path.Buffer); + wbuf[image_path.Length] = 0; + + const image_pathZ = wbuf[0..image_path.Length :0]; + + const kernelenv = w.kernel32.GetEnvironmentStringsW(); + var newenv: ?[]u16 = null; + defer { + if (kernelenv) |envptr| { + _ = w.kernel32.FreeEnvironmentStringsW(envptr); + } + if (newenv) |ptr| { + allocator.free(ptr); + } + } + + if (setChild) { + var size: usize = 0; + if (kernelenv) |ptr| { + // check that env is non-empty + if (ptr[0] != 0 or ptr[1] != 0) { + // array is terminated by two nulls + while (ptr[size] != 0 or ptr[size + 1] != 0) size += 1; + size += 1; + } + } + // now ptr[size] is the first null + const buf = try allocator.alloc(u16, size + watcherChildEnv.len + 4); + if (kernelenv) |ptr| { + @memcpy(buf[0..size], ptr); + } + @memcpy(buf[size .. size + watcherChildEnv.len], watcherChildEnv); + buf[size + watcherChildEnv.len] = '='; + buf[size + watcherChildEnv.len + 1] = '1'; + buf[size + watcherChildEnv.len + 2] = 0; + buf[size + watcherChildEnv.len + 3] = 0; + newenv = buf; + } + + const env: ?[*]u16 = if (newenv) |e| e.ptr else kernelenv; + + var startupinfo = w.STARTUPINFOW{ + .cb = @sizeOf(w.STARTUPINFOW), + .lpReserved = null, + .lpDesktop = null, + .lpTitle = null, + .dwX = 0, + .dwY = 0, + .dwXSize = 0, + .dwYSize = 0, + .dwXCountChars = 0, + .dwYCountChars = 0, + .dwFillAttribute = 0, + .dwFlags = w.STARTF_USESTDHANDLES, + .wShowWindow = 0, + .cbReserved2 = 0, + .lpReserved2 = null, + .hStdInput = std.io.getStdIn().handle, + .hStdOutput = std.io.getStdOut().handle, + .hStdError = std.io.getStdErr().handle, + }; + const rc = w.kernel32.CreateProcessW( + image_pathZ, + w.kernel32.GetCommandLineW(), + null, + null, + 1, + flags, + env, + null, + &startupinfo, + procinfo, + ); + if (rc == 0) { + Output.panic("Unexpected error while reloading process\n", .{}); + } + } }; pub usingnamespace if (@import("builtin").target.os.tag != .windows) posix else win32; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 1385c83d5f..02438f0d8a 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -665,6 +665,7 @@ pub const BundleV2 = struct { } _ = @atomicRmw(usize, &this.graph.parse_pending, .Add, 1, .Monotonic); const source_index = Index.source(this.graph.input_files.len); + if (path.pretty.ptr == path.text.ptr) { // TODO: outbase const rel = bun.path.relative(this.bundler.fs.top_level_dir, path.text); @@ -673,6 +674,8 @@ pub const BundleV2 = struct { } } path.* = try path.dupeAlloc(this.graph.allocator); + // TODO: this shouldn't be necessary + path.pretty = bun.sliceInBuffer(path.text, path.pretty); entry.value_ptr.* = source_index.get(); this.graph.ast.append(bun.default_allocator, JSAst.empty) catch unreachable; @@ -1114,9 +1117,7 @@ pub const BundleV2 = struct { }, }, .size = source.contents.len, - .output_path = std.fmt.allocPrint(bun.default_allocator, "{}", .{ - template, - }) catch unreachable, + .output_path = std.fmt.allocPrint(bun.default_allocator, "{}", .{template}) catch unreachable, .input_path = bun.default_allocator.dupe(u8, source.path.text) catch unreachable, .input_loader = .file, .output_kind = .asset, @@ -2028,6 +2029,8 @@ pub const BundleV2 = struct { } path.* = path.dupeAlloc(this.graph.allocator) catch @panic("Ran out of memory"); + // TODO: this shouldn't be necessary + path.pretty = bun.sliceInBuffer(path.text, path.pretty); import_record.path = path.*; debug("created ParseTask: {s}", .{path.text}); @@ -9026,11 +9029,20 @@ const LinkerContext = struct { chunk.template.placeholder.hash = chunk.isolated_hash; const rel_path = std.fmt.allocPrint(c.allocator, "{any}", .{chunk.template}) catch unreachable; + bun.path.platformToPosixInPlace(u8, rel_path); + if ((try path_names_map.getOrPut(rel_path)).found_existing) { try c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "Multiple files share the same output path: {s}", .{rel_path}); return error.DuplicateOutputPath; } - + // resolve any /./ and /../ occurrences + // use resolvePosix since we asserted above all seps are '/' + if (Environment.isWindows and std.mem.indexOf(u8, rel_path, "/./") != null) { + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const rel_path_fixed = c.allocator.dupe(u8, bun.path.normalizeBuf(rel_path, &buf, .posix)) catch unreachable; + chunk.final_rel_path = rel_path_fixed; + continue; + } chunk.final_rel_path = rel_path; } } @@ -9376,7 +9388,7 @@ const LinkerContext = struct { defer max_heap_allocator.reset(); const rel_path = chunk.final_rel_path; - if (std.fs.path.dirname(rel_path)) |rel_parent| { + if (std.fs.path.dirnamePosix(rel_path)) |rel_parent| { if (rel_parent.len > 0) { root_dir.makePath(rel_parent) catch |err| { c.log.addErrorFmt(null, Logger.Loc.Empty, bun.default_allocator, "{s} creating outdir {} while saving chunk {}", .{ @@ -11081,7 +11093,7 @@ pub const Chunk = struct { shifts.appendAssumeCapacity(shift); var count: usize = 0; - var from_chunk_dir = std.fs.path.dirname(chunk.final_rel_path) orelse ""; + var from_chunk_dir = std.fs.path.dirnamePosix(chunk.final_rel_path) orelse ""; if (strings.eqlComptime(from_chunk_dir, ".")) from_chunk_dir = ""; @@ -11102,7 +11114,7 @@ pub const Chunk = struct { if (from_chunk_dir.len == 0) file_path else - bun.path.relative(from_chunk_dir, file_path), + bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), ); count += cheap_normalizer[0].len + cheap_normalizer[1].len; }, @@ -11159,7 +11171,7 @@ pub const Chunk = struct { if (from_chunk_dir.len == 0) file_path else - bun.path.relative(from_chunk_dir, file_path), + bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), ); if (cheap_normalizer[0].len > 0) { @@ -11249,7 +11261,7 @@ pub const Chunk = struct { var count: usize = 0; const file_path_buf: [4096]u8 = undefined; _ = file_path_buf; - var from_chunk_dir = std.fs.path.dirname(chunk.final_rel_path) orelse ""; + var from_chunk_dir = std.fs.path.dirnamePosix(chunk.final_rel_path) orelse ""; if (strings.eqlComptime(from_chunk_dir, ".")) from_chunk_dir = ""; @@ -11273,7 +11285,8 @@ pub const Chunk = struct { .chunk => chunks[index].final_rel_path, else => unreachable, }; - + // normalize windows paths to '/' + bun.path.platformToPosixInPlace(u8, @constCast(file_path)); const cheap_normalizer = cheapPrefixNormalizer( import_prefix, if (from_chunk_dir.len == 0) @@ -11314,12 +11327,14 @@ pub const Chunk = struct { .chunk => chunks[index].final_rel_path, else => unreachable, }; + // normalize windows paths to '/' + bun.path.platformToPosixInPlace(u8, @constCast(file_path)); const cheap_normalizer = cheapPrefixNormalizer( import_prefix, if (from_chunk_dir.len == 0) file_path else - bun.path.relative(from_chunk_dir, file_path), + bun.path.relativePlatform(from_chunk_dir, file_path, .posix, false), ); if (cheap_normalizer[0].len > 0) { diff --git a/src/child_process_windows.zig b/src/child_process_windows.zig new file mode 100644 index 0000000000..452bd1ee37 --- /dev/null +++ b/src/child_process_windows.zig @@ -0,0 +1,743 @@ +//! TODO: Delete this entire file once https://github.com/ziglang/zig/issues/18694 is resolved. +const bun = @import("root").bun; +const std = @import("std"); + +const os = std.os; +const windows = os.windows; +const mem = std.mem; +const unicode = std.unicode; +const fs = std.fs; +const math = std.math; + +const File = fs.File; + +const ChildProcess = std.ChildProcess; +const SpawnError = ChildProcess.SpawnError; +const StdIo = ChildProcess.StdIo; +const EnvMap = std.process.EnvMap; + +pub fn toUTF16Alloc(alloc: mem.Allocator, bytes: []const u8) ![:0]u16 { + return bun.strings.toUTF16AllocForReal(alloc, bytes, false, true); +} +const utf8ToUtf16Le = bun.strings.convertUTF8toUTF16InBuffer; + +pub fn spawnWindows(self: *ChildProcess) SpawnError!void { + const saAttr = windows.SECURITY_ATTRIBUTES{ + .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES), + .bInheritHandle = windows.TRUE, + .lpSecurityDescriptor = null, + }; + + const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore); + + const nul_handle = if (any_ignore) + // "\Device\Null" or "\??\NUL" + windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{ + .access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE, + .share_access = windows.FILE_SHARE_READ, + .creation = windows.OPEN_EXISTING, + .io_mode = .blocking, + }) catch |err| switch (err) { + error.PathAlreadyExists => unreachable, // not possible for "NUL" + error.PipeBusy => unreachable, // not possible for "NUL" + error.FileNotFound => unreachable, // not possible for "NUL" + error.AccessDenied => unreachable, // not possible for "NUL" + error.NameTooLong => unreachable, // not possible for "NUL" + error.WouldBlock => unreachable, // not possible for "NUL" + error.NetworkNotFound => unreachable, // not possible for "NUL" + else => |e| return e, + } + else + undefined; + defer { + if (any_ignore) os.close(nul_handle); + } + if (any_ignore) { + try windows.SetHandleInformation(nul_handle, windows.HANDLE_FLAG_INHERIT, 0); + } + + var g_hChildStd_IN_Rd: ?windows.HANDLE = null; + var g_hChildStd_IN_Wr: ?windows.HANDLE = null; + switch (self.stdin_behavior) { + StdIo.Pipe => { + try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr); + }, + StdIo.Ignore => { + g_hChildStd_IN_Rd = nul_handle; + }, + StdIo.Inherit => { + g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null; + }, + StdIo.Close => { + g_hChildStd_IN_Rd = null; + }, + } + errdefer if (self.stdin_behavior == StdIo.Pipe) { + windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr); + }; + + var g_hChildStd_OUT_Rd: ?windows.HANDLE = null; + var g_hChildStd_OUT_Wr: ?windows.HANDLE = null; + switch (self.stdout_behavior) { + StdIo.Pipe => { + try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr); + }, + StdIo.Ignore => { + g_hChildStd_OUT_Wr = nul_handle; + }, + StdIo.Inherit => { + g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null; + }, + StdIo.Close => { + g_hChildStd_OUT_Wr = null; + }, + } + errdefer if (self.stdin_behavior == StdIo.Pipe) { + windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr); + }; + + var g_hChildStd_ERR_Rd: ?windows.HANDLE = null; + var g_hChildStd_ERR_Wr: ?windows.HANDLE = null; + switch (self.stderr_behavior) { + StdIo.Pipe => { + try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr); + }, + StdIo.Ignore => { + g_hChildStd_ERR_Wr = nul_handle; + }, + StdIo.Inherit => { + g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null; + }, + StdIo.Close => { + g_hChildStd_ERR_Wr = null; + }, + } + errdefer if (self.stdin_behavior == StdIo.Pipe) { + windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); + }; + + const cmd_line = try windowsCreateCommandLine(self.allocator, self.argv); + defer self.allocator.free(cmd_line); + + var siStartInfo = windows.STARTUPINFOW{ + .cb = @sizeOf(windows.STARTUPINFOW), + .hStdError = g_hChildStd_ERR_Wr, + .hStdOutput = g_hChildStd_OUT_Wr, + .hStdInput = g_hChildStd_IN_Rd, + .dwFlags = windows.STARTF_USESTDHANDLES, + + .lpReserved = null, + .lpDesktop = null, + .lpTitle = null, + .dwX = 0, + .dwY = 0, + .dwXSize = 0, + .dwYSize = 0, + .dwXCountChars = 0, + .dwYCountChars = 0, + .dwFillAttribute = 0, + .wShowWindow = 0, + .cbReserved2 = 0, + .lpReserved2 = null, + }; + var piProcInfo: windows.PROCESS_INFORMATION = undefined; + + const cwd_w = if (self.cwd) |cwd| try toUTF16Alloc(self.allocator, cwd) else null; + defer if (cwd_w) |cwd| self.allocator.free(cwd); + const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null; + + const maybe_envp_buf = if (self.env_map) |env_map| try createWindowsEnvBlock(self.allocator, env_map) else null; + defer if (maybe_envp_buf) |envp_buf| self.allocator.free(envp_buf); + const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null; + + const app_name_utf8 = self.argv[0]; + const app_name_is_absolute = fs.path.isAbsolute(app_name_utf8); + + // the cwd set in ChildProcess is in effect when choosing the executable path + // to match posix semantics + var cwd_path_w_needs_free = false; + const cwd_path_w = x: { + // If the app name is absolute, then we need to use its dirname as the cwd + if (app_name_is_absolute) { + cwd_path_w_needs_free = true; + const dir = fs.path.dirname(app_name_utf8).?; + break :x try toUTF16Alloc(self.allocator, dir); + } else if (self.cwd) |cwd| { + cwd_path_w_needs_free = true; + break :x try toUTF16Alloc(self.allocator, cwd); + } else { + break :x &[_:0]u16{}; // empty for cwd + } + }; + defer if (cwd_path_w_needs_free) self.allocator.free(cwd_path_w); + + // If the app name has more than just a filename, then we need to separate that + // into the basename and dirname and use the dirname as an addition to the cwd + // path. This is because NtQueryDirectoryFile cannot accept FileName params with + // path separators. + const app_basename_utf8 = fs.path.basename(app_name_utf8); + // If the app name is absolute, then the cwd will already have the app's dirname in it, + // so only populate app_dirname if app name is a relative path with > 0 path separators. + const maybe_app_dirname_utf8 = if (!app_name_is_absolute) fs.path.dirname(app_name_utf8) else null; + const app_dirname_w: ?[:0]u16 = x: { + if (maybe_app_dirname_utf8) |app_dirname_utf8| { + break :x try toUTF16Alloc(self.allocator, app_dirname_utf8); + } + break :x null; + }; + defer if (app_dirname_w != null) self.allocator.free(app_dirname_w.?); + + const app_name_w = try toUTF16Alloc(self.allocator, app_basename_utf8); + defer self.allocator.free(app_name_w); + + const cmd_line_w = try toUTF16Alloc(self.allocator, cmd_line); + defer self.allocator.free(cmd_line_w); + + run: { + const PATH: [:0]const u16 = std.os.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{}; + const PATHEXT: [:0]const u16 = std.os.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{}; + + var app_buf = std.ArrayListUnmanaged(u16){}; + defer app_buf.deinit(self.allocator); + + try app_buf.appendSlice(self.allocator, app_name_w); + + var dir_buf = std.ArrayListUnmanaged(u16){}; + defer dir_buf.deinit(self.allocator); + + if (cwd_path_w.len > 0) { + try dir_buf.appendSlice(self.allocator, cwd_path_w); + } + if (app_dirname_w) |app_dir| { + if (dir_buf.items.len > 0) try dir_buf.append(self.allocator, fs.path.sep); + try dir_buf.appendSlice(self.allocator, app_dir); + } + if (dir_buf.items.len > 0) { + // Need to normalize the path, openDirW can't handle things like double backslashes + const normalized_len = windows.normalizePath(u16, dir_buf.items) catch return error.BadPathName; + dir_buf.shrinkRetainingCapacity(normalized_len); + } + + windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, cmd_line_w.ptr, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo) catch |no_path_err| { + const original_err = switch (no_path_err) { + error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e, + error.UnrecoverableInvalidExe => return error.InvalidExe, + else => |e| return e, + }; + + // If the app name had path separators, that disallows PATH searching, + // and there's no need to search the PATH if the app name is absolute. + // We still search the path if the cwd is absolute because of the + // "cwd set in ChildProcess is in effect when choosing the executable path + // to match posix semantics" behavior--we don't want to skip searching + // the PATH just because we were trying to set the cwd of the child process. + if (app_dirname_w != null or app_name_is_absolute) { + return original_err; + } + + var it = mem.tokenizeScalar(u16, PATH, ';'); + while (it.next()) |search_path| { + dir_buf.clearRetainingCapacity(); + try dir_buf.appendSlice(self.allocator, search_path); + // Need to normalize the path, some PATH values can contain things like double + // backslashes which openDirW can't handle + const normalized_len = windows.normalizePath(u16, dir_buf.items) catch continue; + dir_buf.shrinkRetainingCapacity(normalized_len); + + if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, cmd_line_w.ptr, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo)) { + break :run; + } else |err| switch (err) { + error.FileNotFound, error.AccessDenied, error.InvalidExe => continue, + error.UnrecoverableInvalidExe => return error.InvalidExe, + else => |e| return e, + } + } else { + return original_err; + } + }; + } + + if (g_hChildStd_IN_Wr) |h| { + self.stdin = File{ .handle = h }; + } else { + self.stdin = null; + } + if (g_hChildStd_OUT_Rd) |h| { + self.stdout = File{ .handle = h }; + } else { + self.stdout = null; + } + if (g_hChildStd_ERR_Rd) |h| { + self.stderr = File{ .handle = h }; + } else { + self.stderr = null; + } + + self.id = piProcInfo.hProcess; + self.thread_handle = piProcInfo.hThread; + self.term = null; + + if (self.stdin_behavior == StdIo.Pipe) { + os.close(g_hChildStd_IN_Rd.?); + } + if (self.stderr_behavior == StdIo.Pipe) { + os.close(g_hChildStd_ERR_Wr.?); + } + if (self.stdout_behavior == StdIo.Pipe) { + os.close(g_hChildStd_OUT_Wr.?); + } +} + +/// Caller must dealloc. +fn windowsCreateCommandLine(allocator: mem.Allocator, argv: []const []const u8) ![:0]u8 { + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + + for (argv, 0..) |arg, arg_i| { + if (arg_i != 0) try buf.append(' '); + if (mem.indexOfAny(u8, arg, " \t\n\"") == null) { + try buf.appendSlice(arg); + continue; + } + try buf.append('"'); + var backslash_count: usize = 0; + for (arg) |byte| { + switch (byte) { + '\\' => backslash_count += 1, + '"' => { + try buf.appendNTimes('\\', backslash_count * 2 + 1); + try buf.append('"'); + backslash_count = 0; + }, + else => { + try buf.appendNTimes('\\', backslash_count); + try buf.append(byte); + backslash_count = 0; + }, + } + } + try buf.appendNTimes('\\', backslash_count * 2); + try buf.append('"'); + } + + return buf.toOwnedSliceSentinel(0); +} + +fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void { + if (rd) |h| os.close(h); + if (wr) |h| os.close(h); +} + +fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { + var rd_h: windows.HANDLE = undefined; + var wr_h: windows.HANDLE = undefined; + try windows.CreatePipe(&rd_h, &wr_h, sattr); + errdefer windowsDestroyPipe(rd_h, wr_h); + try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0); + rd.* = rd_h; + wr.* = wr_h; +} + +var pipe_name_counter = std.atomic.Value(u32).init(1); + +fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { + var tmp_bufw: [128]u16 = undefined; + + // Anonymous pipes are built upon Named pipes. + // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe + // Asynchronous (overlapped) read and write operations are not supported by anonymous pipes. + // https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations + const pipe_path = blk: { + var tmp_buf: [128]u8 = undefined; + // Forge a random path for the pipe. + const pipe_path = std.fmt.bufPrintZ( + &tmp_buf, + "\\\\.\\pipe\\zig-childprocess-{d}-{d}", + .{ windows.kernel32.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .Monotonic) }, + ) catch unreachable; + const buf_2 = utf8ToUtf16Le(&tmp_bufw, pipe_path); + tmp_bufw[buf_2.len] = 0; + break :blk tmp_bufw[0..buf_2.len :0]; + }; + + // Create the read handle that can be used with overlapped IO ops. + const read_handle = windows.kernel32.CreateNamedPipeW( + pipe_path.ptr, + windows.PIPE_ACCESS_INBOUND | windows.FILE_FLAG_OVERLAPPED, + windows.PIPE_TYPE_BYTE, + 1, + 4096, + 4096, + 0, + sattr, + ); + if (read_handle == windows.INVALID_HANDLE_VALUE) { + switch (windows.kernel32.GetLastError()) { + else => |err| return windows.unexpectedError(err), + } + } + errdefer os.close(read_handle); + + var sattr_copy = sattr.*; + const write_handle = windows.kernel32.CreateFileW( + pipe_path.ptr, + windows.GENERIC_WRITE, + 0, + &sattr_copy, + windows.OPEN_EXISTING, + windows.FILE_ATTRIBUTE_NORMAL, + null, + ); + if (write_handle == windows.INVALID_HANDLE_VALUE) { + switch (windows.kernel32.GetLastError()) { + else => |err| return windows.unexpectedError(err), + } + } + errdefer os.close(write_handle); + + try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0); + + rd.* = read_handle; + wr.* = write_handle; +} + +pub fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const EnvMap) ![]u16 { + // count bytes needed + const max_chars_needed = x: { + var max_chars_needed: usize = 4; // 4 for the final 4 null bytes + var it = env_map.iterator(); + while (it.next()) |pair| { + // +1 for '=' + // +1 for null byte + max_chars_needed += pair.key_ptr.len + pair.value_ptr.len + 2; + } + break :x max_chars_needed; + }; + const result = try allocator.alloc(u16, max_chars_needed); + errdefer allocator.free(result); + + var it = env_map.iterator(); + var i: usize = 0; + while (it.next()) |pair| { + i += utf8ToUtf16Le(result[i..], pair.key_ptr.*).len; + result[i] = '='; + i += 1; + i += utf8ToUtf16Le(result[i..], pair.value_ptr.*).len; + result[i] = 0; + i += 1; + } + result[i] = 0; + i += 1; + result[i] = 0; + i += 1; + result[i] = 0; + i += 1; + result[i] = 0; + i += 1; + return try allocator.realloc(result, i); +} + +/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path. +/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path. +/// Note: `app_buf` should not contain any leading path separators. +/// Note: If the dir is the cwd, dir_buf should be empty (len = 0). +fn windowsCreateProcessPathExt( + allocator: mem.Allocator, + dir_buf: *std.ArrayListUnmanaged(u16), + app_buf: *std.ArrayListUnmanaged(u16), + pathext: [:0]const u16, + cmd_line: [*:0]u16, + envp_ptr: ?[*]u16, + cwd_ptr: ?[*:0]u16, + lpStartupInfo: *windows.STARTUPINFOW, + lpProcessInformation: *windows.PROCESS_INFORMATION, +) !void { + const app_name_len = app_buf.items.len; + const dir_path_len = dir_buf.items.len; + + if (app_name_len == 0) return error.FileNotFound; + + defer app_buf.shrinkRetainingCapacity(app_name_len); + defer dir_buf.shrinkRetainingCapacity(dir_path_len); + + // The name of the game here is to avoid CreateProcessW calls at all costs, + // and only ever try calling it when we have a real candidate for execution. + // Secondarily, we want to minimize the number of syscalls used when checking + // for each PATHEXT-appended version of the app name. + // + // An overview of the technique used: + // - Open the search directory for iteration (either cwd or a path from PATH) + // - Use NtQueryDirectoryFile with a wildcard filename of `*` to + // check if anything that could possibly match either the unappended version + // of the app name or any of the versions with a PATHEXT value appended exists. + // - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early + // without needing to use PATHEXT at all. + // + // This allows us to use a sequence + // for any directory that doesn't contain any possible matches, instead of having + // to use a separate look up for each individual filename combination (unappended + + // each PATHEXT appended). For directories where the wildcard *does* match something, + // we iterate the matches and take note of any that are either the unappended version, + // or a version with a supported PATHEXT appended. We then try calling CreateProcessW + // with the found versions in the appropriate order. + + var dir = dir: { + // needs to be null-terminated + try dir_buf.append(allocator, 0); + defer dir_buf.shrinkRetainingCapacity(dir_path_len); + const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; + const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); + break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{ .iterate = true }) catch + return error.FileNotFound; + }; + defer dir.close(); + + // Add wildcard and null-terminator + try app_buf.append(allocator, '*'); + try app_buf.append(allocator, 0); + const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0]; + + // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries + // returned per NtQueryDirectoryFile call. + var file_information_buf: [2048]u8 align(@alignOf(os.windows.FILE_DIRECTORY_INFORMATION)) = undefined; + const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2); + if (file_information_buf.len < file_info_maximum_single_entry_size) { + @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry"); + } + var io_status: windows.IO_STATUS_BLOCK = undefined; + + const num_supported_pathext = @typeInfo(CreateProcessSupportedExtension).Enum.fields.len; + var pathext_seen = [_]bool{false} ** num_supported_pathext; + var any_pathext_seen = false; + var unappended_exists = false; + + // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions + // of the app_name we should try to spawn. + // Note: This is necessary because the order of the files returned is filesystem-dependent: + // On NTFS, `blah.exe*` will always return `blah.exe` first if it exists. + // On FAT32, it's possible for something like `blah.exe.obj` to be returned first. + while (true) { + const app_name_len_bytes = math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong; + var app_name_unicode_string = windows.UNICODE_STRING{ + .Length = app_name_len_bytes, + .MaximumLength = app_name_len_bytes, + .Buffer = @constCast(app_name_wildcard.ptr), + }; + const rc = windows.ntdll.NtQueryDirectoryFile( + dir.fd, + null, + null, + null, + &io_status, + &file_information_buf, + file_information_buf.len, + .FileDirectoryInformation, + windows.FALSE, // single result + &app_name_unicode_string, + windows.FALSE, // restart iteration + ); + + // If we get nothing with the wildcard, then we can just bail out + // as we know appending PATHEXT will not yield anything. + switch (rc) { + .SUCCESS => {}, + .NO_SUCH_FILE => return error.FileNotFound, + .NO_MORE_FILES => break, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + + // According to the docs, this can only happen if there is not enough room in the + // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry. + // Therefore, this condition should not be possible to hit with the buffer size we use. + std.debug.assert(io_status.Information != 0); + + var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf }; + while (it.next()) |info| { + // Skip directories + if (info.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) continue; + const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2]; + // Because all results start with the app_name since we're using the wildcard `app_name*`, + // if the length is equal to app_name then this is an exact match + if (filename.len == app_name_len) { + // Note: We can't break early here because it's possible that the unappended version + // fails to spawn, in which case we still want to try the PATHEXT appended versions. + unappended_exists = true; + } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| { + pathext_seen[@intFromEnum(pathext_ext)] = true; + any_pathext_seen = true; + } + } + } + + const unappended_err = unappended: { + if (unappended_exists) { + if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { + '/', '\\' => {}, + else => try dir_buf.append(allocator, fs.path.sep), + }; + try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); + try dir_buf.append(allocator, 0); + const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; + + if (windowsCreateProcess(full_app_name.ptr, cmd_line, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { + return; + } else |err| switch (err) { + error.FileNotFound, + error.AccessDenied, + => break :unappended err, + error.InvalidExe => { + // On InvalidExe, if the extension of the app name is .exe then + // it's treated as an unrecoverable error. Otherwise, it'll be + // skipped as normal. + const app_name = app_buf.items[0..app_name_len]; + const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err; + const ext = app_name[ext_start..]; + if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { + return error.UnrecoverableInvalidExe; + } + break :unappended err; + }, + else => return err, + } + } + break :unappended error.FileNotFound; + }; + + if (!any_pathext_seen) return unappended_err; + + // Now try any PATHEXT appended versions that we've seen + var ext_it = mem.tokenizeScalar(u16, pathext, ';'); + while (ext_it.next()) |ext| { + const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue; + if (!pathext_seen[@intFromEnum(ext_enum)]) continue; + + dir_buf.shrinkRetainingCapacity(dir_path_len); + if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { + '/', '\\' => {}, + else => try dir_buf.append(allocator, fs.path.sep), + }; + try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); + try dir_buf.appendSlice(allocator, ext); + try dir_buf.append(allocator, 0); + const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; + + if (windowsCreateProcess(full_app_name.ptr, cmd_line, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { + return; + } else |err| switch (err) { + error.FileNotFound => continue, + error.AccessDenied => continue, + error.InvalidExe => { + // On InvalidExe, if the extension of the app name is .exe then + // it's treated as an unrecoverable error. Otherwise, it'll be + // skipped as normal. + if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { + return error.UnrecoverableInvalidExe; + } + continue; + }, + else => return err, + } + } + + return unappended_err; +} + +// Should be kept in sync with `windowsCreateProcessSupportsExtension` +const CreateProcessSupportedExtension = enum { + bat, + cmd, + com, + exe, +}; + +/// Case-insensitive UTF-16 lookup +fn windowsCreateProcessSupportsExtension(ext: []const u16) ?CreateProcessSupportedExtension { + if (ext.len != 4) return null; + const State = enum { + start, + dot, + b, + ba, + c, + cm, + co, + e, + ex, + }; + var state: State = .start; + for (ext) |c| switch (state) { + .start => switch (c) { + '.' => state = .dot, + else => return null, + }, + .dot => switch (c) { + 'b', 'B' => state = .b, + 'c', 'C' => state = .c, + 'e', 'E' => state = .e, + else => return null, + }, + .b => switch (c) { + 'a', 'A' => state = .ba, + else => return null, + }, + .c => switch (c) { + 'm', 'M' => state = .cm, + 'o', 'O' => state = .co, + else => return null, + }, + .e => switch (c) { + 'x', 'X' => state = .ex, + else => return null, + }, + .ba => switch (c) { + 't', 'T' => return .bat, + else => return null, + }, + .cm => switch (c) { + 'd', 'D' => return .cmd, + else => return null, + }, + .co => switch (c) { + 'm', 'M' => return .com, + else => return null, + }, + .ex => switch (c) { + 'e', 'E' => return .exe, + else => return null, + }, + }; + return null; +} + +fn windowsCreateProcess(app_name: [*:0]u16, cmd_line: [*:0]u16, envp_ptr: ?[*]u16, cwd_ptr: ?[*:0]u16, lpStartupInfo: *windows.STARTUPINFOW, lpProcessInformation: *windows.PROCESS_INFORMATION) !void { + // TODO the docs for environment pointer say: + // > A pointer to the environment block for the new process. If this parameter + // > is NULL, the new process uses the environment of the calling process. + // > ... + // > An environment block can contain either Unicode or ANSI characters. If + // > the environment block pointed to by lpEnvironment contains Unicode + // > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT. + // > If this parameter is NULL and the environment block of the parent process + // > contains Unicode characters, you must also ensure that dwCreationFlags + // > includes CREATE_UNICODE_ENVIRONMENT. + // This seems to imply that we have to somehow know whether our process parent passed + // CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter. + // Since we do not know this information that would imply that we must not pass NULL + // for the parameter. + // However this would imply that programs compiled with -DUNICODE could not pass + // environment variables to programs that were not, which seems unlikely. + // More investigation is needed. + return windows.CreateProcessW( + app_name, + cmd_line, + null, + null, + windows.TRUE, + windows.CREATE_UNICODE_ENVIRONMENT, + @as(?*anyopaque, @ptrCast(envp_ptr)), + cwd_ptr, + lpStartupInfo, + lpProcessInformation, + ); +} diff --git a/src/cli.zig b/src/cli.zig index 3061942c3f..a6e1791b93 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -196,7 +196,10 @@ pub const Arguments = struct { const run_only_params = [_]ParamType{ clap.parseParam("--silent Don't print the script command") catch unreachable, clap.parseParam("-b, --bun Force a script or package to use Bun's runtime instead of Node.js (via symlinking node)") catch unreachable, - }; + } ++ if (Environment.isWindows) [_]ParamType{ + // clap.parseParam("--native-shell Use cmd.exe to interpret package.json scripts") catch unreachable, + clap.parseParam("--no-native-shell Use Bun shell (TODO: flip this switch)") catch unreachable, + } else .{}; pub const run_params = run_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_; const bunx_commands = [_]ParamType{ @@ -835,6 +838,13 @@ pub const Arguments = struct { if (output_file != null) ctx.debug.output_file = output_file.?; + if (cmd == .RunCommand) { + ctx.debug.use_native_shell = if (Environment.isWindows) + !args.flag("--no-native-shell") + else + true; + } + return opts; } }; @@ -1028,6 +1038,8 @@ pub const Command = struct { offline_mode_setting: ?Bunfig.OfflineMode = null, run_in_bun: bool = false, loaded_bunfig: bool = false, + /// Disables using bun.shell.Interpreter for `bun run`, instead spawning cmd.exe + use_native_shell: bool = false, // technical debt macros: MacroOptions = MacroOptions.unspecified, @@ -1125,6 +1137,14 @@ pub const Command = struct { if (comptime Command.Tag.uses_global_options.get(command)) { ctx.args = try Arguments.parse(allocator, &ctx, command); } + + if (comptime Environment.isWindows) { + if (ctx.debug.hot_reload == .watch and !bun.isWatcherChild()) { + // this is noreturn + bun.becomeWatcherManager(allocator); + } + } + return ctx; } }; @@ -1148,15 +1168,12 @@ pub const Command = struct { } }; - const exe_suffix = if (Environment.isWindows) ".exe" else ""; - pub fn isBunX(argv0: []const u8) bool { - return strings.endsWithComptime(argv0, "bunx" ++ exe_suffix) or - (Environment.isDebug and strings.endsWithComptime(argv0, "bunx-debug" ++ exe_suffix)); + return strings.endsWithComptime(argv0, "bunx") or (Environment.isDebug and strings.endsWithComptime(argv0, "bunx-debug")); } pub fn isNode(argv0: []const u8) bool { - return strings.endsWithComptime(argv0, "node" ++ exe_suffix); + return strings.endsWithComptime(argv0, "node"); } pub fn which() Tag { @@ -1164,10 +1181,15 @@ pub const Command = struct { const argv0 = args_iter.next() orelse return .HelpCommand; - // symlink is argv[0] - if (isBunX(argv0)) return .BunxCommand; + const without_exe = if (Environment.isWindows) + strings.withoutSuffixComptime(argv0, ".exe") + else + argv0; - if (isNode(argv0)) { + // symlink is argv[0] + if (isBunX(without_exe)) return .BunxCommand; + + if (isNode(without_exe)) { @import("./deps/zig-clap/clap/streaming.zig").warn_on_unrecognized_flag = false; pretend_to_be_node = true; return .RunAsNodeCommand; @@ -1606,7 +1628,7 @@ pub const Command = struct { const ctx = try Command.Context.create(allocator, log, .RunCommand); if (ctx.positionals.len > 0) { - if (try RunCommand.exec(ctx, false, true)) { + if (try RunCommand.exec(ctx, false, true, false)) { return; } @@ -1723,7 +1745,7 @@ pub const Command = struct { } if (ctx.positionals.len > 0 and extension.len == 0) { - if (try RunCommand.exec(ctx, true, false)) { + if (try RunCommand.exec(ctx, true, false, true)) { return; } @@ -1767,50 +1789,73 @@ pub const Command = struct { const script_name_to_search = ctx.args.entry_points[0]; + var absolute_script_path: ?string = null; + + // TODO: optimize this pass for Windows. we can make better use of system apis available var file_path = script_name_to_search; - const file_: anyerror!std.fs.File = brk: { - if (std.fs.path.isAbsoluteWindows(script_name_to_search)) { - var win_resolver = resolve_path.PosixToWinNormalizer{}; - var resolved = win_resolver.resolveCWD(script_name_to_search) catch @panic("Could not resolve path"); - if (comptime Environment.isWindows) { - resolved = resolve_path.normalizeString(resolved, true, .windows); - } - break :brk bun.openFile( - resolved, - .{ .mode = .read_only }, - ); - } else if (!strings.hasPrefix(script_name_to_search, "..") and script_name_to_search[0] != '~') { - const file_pathZ = brk2: { - @memcpy(script_name_buf[0..file_path.len], file_path); + { + const file = bun.toLibUVOwnedFD(((brk: { + if (std.fs.path.isAbsolute(script_name_to_search)) { + var win_resolver = resolve_path.PosixToWinNormalizer{}; + var resolved = win_resolver.resolveCWD(script_name_to_search) catch @panic("Could not resolve path"); + if (comptime Environment.isWindows) { + resolved = resolve_path.normalizeString(resolved, true, .windows); + } + absolute_script_path = resolved; + break :brk bun.openFile( + resolved, + .{ .mode = .read_only }, + ); + } else if (!strings.hasPrefix(script_name_to_search, "..") and script_name_to_search[0] != '~') { + const file_pathZ = brk2: { + @memcpy(script_name_buf[0..file_path.len], file_path); + script_name_buf[file_path.len] = 0; + break :brk2 script_name_buf[0..file_path.len :0]; + }; + + break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); + } else { + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const cwd = bun.getcwd(&path_buf) catch return false; + path_buf[cwd.len] = std.fs.path.sep; + var parts = [_]string{script_name_to_search}; + file_path = resolve_path.joinAbsStringBuf( + path_buf[0 .. cwd.len + 1], + &script_name_buf, + &parts, + .auto, + ); + if (file_path.len == 0) return false; script_name_buf[file_path.len] = 0; - break :brk2 script_name_buf[0..file_path.len :0]; - }; + const file_pathZ = script_name_buf[0..file_path.len :0]; + break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); + } + }) catch return false).handle); + defer _ = bun.sys.close(file); - break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); - } else { - var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const cwd = bun.getcwd(&path_buf) catch return false; - path_buf[cwd.len] = std.fs.path.sep; - var parts = [_]string{script_name_to_search}; - file_path = resolve_path.joinAbsStringBuf( - path_buf[0 .. cwd.len + 1], - &script_name_buf, - &parts, - .auto, - ); - if (file_path.len == 0) return false; - script_name_buf[file_path.len] = 0; - const file_pathZ = script_name_buf[0..file_path.len :0]; - break :brk bun.openFileZ(file_pathZ, .{ .mode = .read_only }); + switch (bun.sys.fstat(file)) { + .result => |stat| { + // directories cannot be run. if only there was a faster way to check this + if (bun.S.ISDIR(@intCast(stat.mode))) return false; + }, + .err => return false, } - }; - const file = file_ catch return false; + Global.configureAllocator(.{ .long_running = true }); - Global.configureAllocator(.{ .long_running = true }); + // the case where this doesn't work is if the script name on disk doesn't end with a known JS-like file extension + absolute_script_path = absolute_script_path orelse brk: { + if (comptime !Environment.isWindows) break :brk bun.getFdPath(file, &script_name_buf) catch return false; - // the case where this doesn't work is if the script name on disk doesn't end with a known JS-like file extension - const absolute_script_path = bun.getFdPath(file.handle, &script_name_buf) catch return false; + var fd_path_buf: bun.PathBuffer = undefined; + const path = bun.getFdPath(file, &fd_path_buf) catch return false; + break :brk resolve_path.normalizeString( + resolve_path.PosixToWinNormalizer.resolveCWDWithExternalBufZ(&script_name_buf, path) catch @panic("Could not resolve path"), + true, + .windows, + ); + }; + } if (!ctx.debug.loaded_bunfig) { bun.CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", ctx, .RunCommand) catch {}; @@ -1818,7 +1863,7 @@ pub const Command = struct { BunJS.Run.boot( ctx.*, - absolute_script_path, + absolute_script_path.?, ) catch |err| { if (Output.enable_ansi_colors) { ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {}; diff --git a/src/cli/bunx_command.zig b/src/cli/bunx_command.zig index 4bf74f71df..711277a8b2 100644 --- a/src/cli/bunx_command.zig +++ b/src/cli/bunx_command.zig @@ -142,6 +142,7 @@ pub const BunxCommand = struct { /// Check the enclosing package.json for a matching "bin" /// If not found, check bunx cache dir fn getBinName(bundler: *bun.Bundler, toplevel_fd: bun.FileDescriptor, tempdir_name: []const u8, package_name: []const u8) error{ NoBinFound, NeedToInstall }![]const u8 { + toplevel_fd.assertValid(); return getBinNameFromProjectDirectory(bundler, toplevel_fd, package_name) catch |err| { if (err == error.NoBinFound) { return error.NoBinFound; @@ -323,13 +324,13 @@ pub const BunxCommand = struct { if (PATH.len > 0) { PATH = try std.fmt.allocPrint( ctx.allocator, - "{s}/{s}--bunx/node_modules/.bin:{s}", + bun.pathLiteral("{s}/{s}--bunx/node_modules/.bin:{s}"), .{ temp_dir, package_fmt, PATH }, ); } else { PATH = try std.fmt.allocPrint( ctx.allocator, - "{s}/{s}--bunx/node_modules/.bin", + bun.pathLiteral("{s}/{s}--bunx/node_modules/.bin"), .{ temp_dir, package_fmt }, ); } @@ -337,7 +338,7 @@ pub const BunxCommand = struct { const bunx_cache_dir = PATH[0 .. temp_dir.len + "/--bunx".len + package_fmt.len]; var absolute_in_cache_dir_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}", .{ bunx_cache_dir, initial_bin_name }) catch unreachable; + var absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, bun.pathLiteral("{s}/node_modules/.bin/{s}"), .{ bunx_cache_dir, initial_bin_name }) catch unreachable; const passthrough = passthrough_list.items; diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index df48d36b09..f51ea93f73 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -555,6 +555,7 @@ pub const CreateCommand = struct { package_json_contents = try MutableString.init(ctx.allocator, size); package_json_contents.list.expandToCapacity(); + const prev_file_pos = if (comptime Environment.isWindows) try pkg.getPos() else 0; _ = pkg.preadAll(package_json_contents.list.items, 0) catch |err| { package_json_file = null; @@ -565,6 +566,7 @@ pub const CreateCommand = struct { Output.prettyErrorln("Error reading package.json: {s}", .{@errorName(err)}); break :read_package_json; }; + if (comptime Environment.isWindows) try pkg.seekTo(prev_file_pos); // The printer doesn't truncate, so we must do so manually std.os.ftruncate(pkg.handle, 0) catch {}; @@ -2253,18 +2255,30 @@ const GitHandler = struct { ) !bool { const git_start = std.time.nanoTimestamp(); - // This feature flag is disabled. - // using libgit2 is slower than the CLI. - // [481.00ms] git - // [89.00ms] git - // if (comptime FeatureFlags.use_libgit2) { - // } + // Not sure why... + // But using libgit for this operation is slower than the CLI! + // Used to have a feature flag to try it but was removed: + // https://github.com/oven-sh/bun/commit/deafd3d0d42fb8d7ddf2b06cde2d7c7ee8bc7144 + // + // ~/Build/throw + // ❯ hyperfine "bun create react3 app --force --no-install" --prepare="rm -rf app" + // Benchmark #1: bun create react3 app --force --no-install + // Time (mean ± σ): 974.6 ms ± 6.8 ms [User: 170.5 ms, System: 798.3 ms] + // Range (min … max): 960.8 ms … 984.6 ms 10 runs + // + // ❯ mv /usr/local/opt/libgit2/lib/libgit2.dylib /usr/local/opt/libgit2/lib/libgit2.dylib.1 + // + // ~/Build/throw + // ❯ hyperfine "bun create react3 app --force --no-install" --prepare="rm -rf app" + // Benchmark #1: bun create react3 app --force --no-install + // Time (mean ± σ): 306.7 ms ± 6.1 ms [User: 31.7 ms, System: 269.8 ms] + // Range (min … max): 299.5 ms … 318.8 ms 10 runs if (which(&bun_path_buf, PATH, destination, "git")) |git| { const git_commands = .{ - &[_]string{ bun.asByteSlice(git), "init", "--quiet" }, - &[_]string{ bun.asByteSlice(git), "add", destination, "--ignore-errors" }, - &[_]string{ bun.asByteSlice(git), "commit", "-am", "Initial commit (via bun create)", "--quiet" }, + &[_]string{ git, "init", "--quiet" }, + &[_]string{ git, "add", destination, "--ignore-errors" }, + &[_]string{ git, "commit", "-am", "Initial commit (via bun create)", "--quiet" }, }; if (comptime verbose) { diff --git a/src/cli/init_command.zig b/src/cli/init_command.zig index 5bae97dda4..2af41fc381 100644 --- a/src/cli/init_command.zig +++ b/src/cli/init_command.zig @@ -136,10 +136,12 @@ pub const InitCommand = struct { package_json_contents = try MutableString.init(alloc, size); package_json_contents.list.expandToCapacity(); + const prev_file_pos = if (comptime Environment.isWindows) try pkg.getPos() else 0; _ = pkg.preadAll(package_json_contents.list.items, 0) catch { package_json_file = null; break :read_package_json; }; + if (comptime Environment.isWindows) try pkg.seekTo(prev_file_pos); } } diff --git a/src/cli/install_completions_command.zig b/src/cli/install_completions_command.zig index b2cf708249..b0953d9b99 100644 --- a/src/cli/install_completions_command.zig +++ b/src/cli/install_completions_command.zig @@ -446,6 +446,10 @@ pub const InstallCompletionsCommand = struct { 0, ) catch break :brk true; + if (comptime Environment.isWindows) { + try dot_zshrc.seekTo(0); + } + const contents = buf[0..read]; // Do they possibly have it in the file already? diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index d4d58d0c20..963500fcb1 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -271,6 +271,7 @@ pub const RunCommand = struct { env: *DotEnv.Loader, passthrough: []const string, silent: bool, + use_native_shell: bool, ) !bool { const shell_bin = findShell(env.map.get("PATH") orelse "", cwd) orelse return error.MissingShell; @@ -303,6 +304,28 @@ pub const RunCommand = struct { combined_script = combined_script_buf; } + if (Environment.isWindows and !use_native_shell) { + if (!silent) { + if (Environment.isDebug) { + Output.prettyError("[bun shell] ", .{}); + } + Output.prettyErrorln("$ {s}", .{combined_script}); + Output.flush(); + } + + const mini = bun.JSC.MiniEventLoop.initGlobal(env); + bun.shell.InterpreterMini.initAndRunFromSource(mini, name, combined_script) catch |err| { + if (!silent) { + Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ name, @errorName(err) }); + } + + Output.flush(); + Global.exit(1); + }; + + return true; + } + var argv = [_]string{ shell_bin, if (Environment.isWindows) "/c" else "-c", @@ -324,7 +347,13 @@ pub const RunCommand = struct { child_process.stdin_behavior = .Inherit; child_process.stdout_behavior = .Inherit; - const result = child_process.spawnAndWait() catch |err| { + if (Environment.isWindows) { + try @import("../child_process_windows.zig").spawnWindows(&child_process); + } else { + try child_process.spawn(); + } + + const result = child_process.wait() catch |err| { if (!silent) { Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ name, @errorName(err) }); } @@ -1069,7 +1098,12 @@ pub const RunCommand = struct { } } - pub fn exec(ctx_: Command.Context, comptime bin_dirs_only: bool, comptime log_errors: bool) !bool { + pub fn exec( + ctx_: Command.Context, + comptime bin_dirs_only: bool, + comptime log_errors: bool, + comptime did_try_open_with_bun_js: bool, + ) !bool { var ctx = ctx_; // Step 1. Figure out what we're trying to run var positionals = ctx.positionals; @@ -1109,7 +1143,7 @@ pub const RunCommand = struct { return true; } - if (log_errors or force_using_bun) { + if (!did_try_open_with_bun_js and (log_errors or force_using_bun)) { if (script_name_to_search.len > 0) { possibly_open_with_bun_js: { const ext = std.fs.path.extension(script_name_to_search); @@ -1249,6 +1283,7 @@ pub const RunCommand = struct { this_bundler.env, &.{}, ctx.debug.silent, + ctx.debug.use_native_shell, )) { return false; } @@ -1262,6 +1297,7 @@ pub const RunCommand = struct { this_bundler.env, passthrough, ctx.debug.silent, + ctx.debug.use_native_shell, )) return false; temp_script_buffer[0.."post".len].* = "post".*; @@ -1275,6 +1311,7 @@ pub const RunCommand = struct { this_bundler.env, &.{}, ctx.debug.silent, + ctx.debug.use_native_shell, )) { return false; } @@ -1307,7 +1344,7 @@ pub const RunCommand = struct { if (Environment.isWindows) try_bunx_file: { const WinBunShimImpl = @import("../install/windows-shim/bun_shim_impl.zig"); const w = std.os.windows; - const debug = Output.scoped(.BunRunXFastPath, true); + const debug = Output.scoped(.BunRunXFastPath, false); // Attempt to find a ".bunx" file on disk, and run it, skipping the wrapper exe. // we build the full exe path even though we could do a relative lookup, because in the case we do find it, we have to generate this full path anyways diff --git a/src/codegen/bundle-functions.ts b/src/codegen/bundle-functions.ts index fa3907ceb1..417b910fbc 100644 --- a/src/codegen/bundle-functions.ts +++ b/src/codegen/bundle-functions.ts @@ -206,7 +206,8 @@ $$capture_start$$(${fn.async ? "async " : ""}${ ) ) .replace(/^\((async )?function\(/, "($1function (") - .replace(/__intrinsic__/g, "@") + "\n"; + .replace(/__intrinsic__/g, "@") + .replace(/__no_intrinsic__/g, "") + "\n"; bundledFunctions.push({ name: fn.name, diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index be479ae081..73d1b4c13e 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -232,7 +232,8 @@ for (const entrypoint of bundledEntryPoints) { .replace(/import.meta.require\((.*?)\)/g, (expr, specifier) => { throw new Error(`Builtin Bundler: do not use import.meta.require() (in ${file_path}))`); }) - .replace(/__intrinsic__/g, "@") + "\n"; + .replace(/__intrinsic__/g, "@") + .replace(/__no_intrinsic__/g, "") + "\n"; captured = captured.replace( /function\s*\(.*?\)\s*{/, '$&"use strict";' + diff --git a/src/codegen/replacements.ts b/src/codegen/replacements.ts index 2d16667949..8f8581f591 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -54,6 +54,11 @@ export const globalsToPrefix = [ "undefined", ]; +replacements.push({ + from: new RegExp(`\\bextends\\s+(${globalsToPrefix.join("|")})`, "g"), + to: "extends __no_intrinsic__%1", +}); + // These enums map to $IdToLabel and $LabelToId // Make sure to define in ./builtins.d.ts export const enums = { @@ -131,7 +136,7 @@ export function applyReplacements(src: string, length: number) { let rest = src.slice(length); slice = slice.replace(/([^a-zA-Z0-9_\$])\$([a-zA-Z0-9_]+\b)/gm, `$1__intrinsic__$2`); for (const replacement of replacements) { - slice = slice.replace(replacement.from, replacement.to.replaceAll("$", "__intrinsic__")); + slice = slice.replace(replacement.from, replacement.to.replaceAll("$", "__intrinsic__").replaceAll("%", "$")); } let match; if ((match = slice.match(/__intrinsic__(debug|assert)$/)) && rest.startsWith("(")) { diff --git a/src/darwin_c.zig b/src/darwin_c.zig index d3f715fc45..b3ec5b1015 100644 --- a/src/darwin_c.zig +++ b/src/darwin_c.zig @@ -775,8 +775,8 @@ pub const preallocate_length = std.math.maxInt(u51); pub const Mode = std.os.mode_t; pub const E = std.os.E; +pub const S = std.os.S; pub fn getErrno(rc: anytype) E { return std.c.getErrno(rc); } - pub extern "c" fn umask(Mode) Mode; diff --git a/src/deps/uws.zig b/src/deps/uws.zig index d75a94ef1e..1e3acfa314 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -2540,11 +2540,10 @@ pub const UVLoop = extern struct { pub fn run(this: *UVLoop) void { us_loop_run(this); } - pub const tick = run; - pub fn wait(this: *UVLoop) void { - us_loop_run(this); - } + // TODO: remove these two aliases + pub const tick = run; + pub const wait = run; pub fn inc(this: *UVLoop) void { this.uv_loop.inc(); diff --git a/src/fd.zig b/src/fd.zig index aa18bcf160..a1ade3c6ef 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -20,8 +20,12 @@ fn handleToNumber(handle: FDImpl.System) FDImpl.SystemAsInt { return handle; } } + fn numberToHandle(handle: FDImpl.SystemAsInt) FDImpl.System { if (env.os == .windows) { + if (!@inComptime()) { + std.debug.assert(handle != FDImpl.invalid_value); + } return @ptrFromInt(handle); } else { return handle; @@ -122,7 +126,17 @@ pub const FDImpl = packed struct { } pub fn isValid(this: FDImpl) bool { - return this.value.as_system != invalid_value; + return switch (env.os) { + // the 'zero' value on posix is debatable. it can be standard in. + // TODO(@paperdave): steamroll away every use of bun.FileDescriptor.zero + else => this.value.as_system != invalid_value, + .windows => switch (this.kind) { + // zero is not allowed in addition to the invalid value (zero would be a null ptr) + .system => this.value.as_system != invalid_value and this.value.as_system != 0, + // the libuv tag is always fine + .uv => true, + }, + }; } /// When calling this function, you may not be able to close the returned fd. @@ -171,9 +185,9 @@ pub const FDImpl = packed struct { if (env.os != .windows or this.kind == .uv) { // This branch executes always on linux (uv() is no-op), // or on Windows when given a UV file descriptor. - const fd = bun.toFD(this.uv()); - if (fd == bun.STDOUT_FD or fd == bun.STDERR_FD) { - log("close({}) SKIPPED", .{this}); + const fd = this.uv(); + if (fd == 1 or fd == 2) { + log("close({}) SKIPPED", .{fd}); return null; } } @@ -197,6 +211,11 @@ pub const FDImpl = packed struct { std.debug.assert(this.value.as_system != invalid_value); // probably a UAF } + // Format the file descriptor for logging BEFORE closing it. + // Otherwise the file descriptor is always invalid after closing it. + var buf: [1050]u8 = undefined; + const this_fmt = if (env.isDebug) std.fmt.bufPrint(&buf, "{}", .{this}) catch unreachable; + const result: ?bun.sys.Error = switch (env.os) { .linux => result: { const fd = this.encode(); @@ -248,12 +267,12 @@ pub const FDImpl = packed struct { if (result) |err| { if (err.errno == @intFromEnum(os.E.BADF)) { // TODO(@paperdave): Zig Compiler Bug, if you remove `this` from the log. An error is correctly printed, but with the wrong reference trace - bun.Output.debugWarn("close({}) = EBADF. This is an indication of a file descriptor UAF", .{this}); + bun.Output.debugWarn("close({s}) = EBADF. This is an indication of a file descriptor UAF", .{this_fmt}); } else { - log("close({}) = {}", .{ this, err }); + log("close({s}) = {}", .{ this_fmt, err }); } } else { - log("close({})", .{this}); + log("close({s})", .{this_fmt}); } } @@ -290,7 +309,13 @@ pub const FDImpl = packed struct { } switch (env.os) { else => { - try writer.print("{d}", .{this.system()}); + const fd = this.system(); + try writer.print("{d}", .{fd}); + if (env.isDebug and fd >= 3) print_with_path: { + var path_buf: bun.PathBuffer = undefined; + const path = std.os.getFdPath(fd, &path_buf) catch break :print_with_path; + try writer.print("[{s}]", .{path}); + } }, .windows => { switch (this.kind) { @@ -323,4 +348,8 @@ pub const FDImpl = packed struct { }, } } + + pub fn assertValid(this: FDImpl) void { + std.debug.assert(this.isValid()); + } }; diff --git a/src/feature_flags.zig b/src/feature_flags.zig index 4c3874bc40..c458909b52 100644 --- a/src/feature_flags.zig +++ b/src/feature_flags.zig @@ -69,22 +69,6 @@ pub const verbose_analytics = false; pub const disable_compression_in_http_client = false; pub const enable_keepalive = true; -// Not sure why... -// But this is slower! -// ~/Build/throw -// ❯ hyperfine "bun create react3 app --force --no-install" --prepare="rm -rf app" -// Benchmark #1: bun create react3 app --force --no-install -// Time (mean ± σ): 974.6 ms ± 6.8 ms [User: 170.5 ms, System: 798.3 ms] -// Range (min … max): 960.8 ms … 984.6 ms 10 runs - -// ❯ mv /usr/local/opt/libgit2/lib/libgit2.dylib /usr/local/opt/libgit2/lib/libgit2.dylib.1 - -// ~/Build/throw -// ❯ hyperfine "bun create react3 app --force --no-install" --prepare="rm -rf app" -// Benchmark #1: bun create react3 app --force --no-install -// Time (mean ± σ): 306.7 ms ± 6.1 ms [User: 31.7 ms, System: 269.8 ms] -// Range (min … max): 299.5 ms … 318.8 ms 10 runs -pub const use_libgit2 = true; pub const atomic_file_watcher = env.isLinux; @@ -177,5 +161,4 @@ pub const concurrent_transpiler = !env.isWindows; // https://github.com/oven-sh/bun/issues/5426#issuecomment-1813865316 pub const disable_auto_js_to_ts_in_node_modules = true; -// TODO: implement the IO for rtc for windows pub const runtime_transpiler_cache = !env.isWindows; diff --git a/src/fs.zig b/src/fs.zig index dab45f0df6..f0ecdb18eb 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -18,7 +18,7 @@ const Semaphore = sync.Semaphore; const Fs = @This(); const path_handler = @import("./resolver/resolve_path.zig"); const PathString = bun.PathString; -const allocators = @import("./allocators.zig"); +const allocators = bun.allocators; const MAX_PATH_BYTES = bun.MAX_PATH_BYTES; const PathBuffer = bun.PathBuffer; @@ -1133,10 +1133,12 @@ pub const FileSystem = struct { while (true) { // We use pread to ensure if the file handle was open, it doesn't seek from the last position + const prev_file_pos = if (comptime Environment.isWindows) try file.getPos() else 0; const read_count = file.preadAll(shared_buffer.list.items[offset..], offset) catch |err| { fs.readFileError(path, err); return err; }; + if (comptime Environment.isWindows) try file.seekTo(prev_file_pos); shared_buffer.list.items = shared_buffer.list.items[0 .. read_count + offset]; file_contents = shared_buffer.list.items; debug("pread({d}, {d}) = {d}", .{ file.handle, size, read_count }); @@ -1179,10 +1181,12 @@ pub const FileSystem = struct { // stick a zero at the end buf[size] = 0; + const prev_file_pos = if (comptime Environment.isWindows) try file.getPos() else 0; const read_count = file.preadAll(buf, 0) catch |err| { fs.readFileError(path, err); return err; }; + if (comptime Environment.isWindows) try file.seekTo(prev_file_pos); file_contents = buf[0..read_count]; debug("pread({d}, {d}) = {d}", .{ file.handle, size, read_count }); @@ -1639,7 +1643,7 @@ pub const Path = struct { // This duplicates but only when strictly necessary // This will skip allocating if it's already in FilenameStore or DirnameStore pub fn dupeAlloc(this: *const Path, allocator: std.mem.Allocator) !Fs.Path { - if (this.text.ptr == this.pretty.ptr and this.text.len == this.text.len) { + if (this.text.ptr == this.pretty.ptr and this.text.len == this.pretty.len) { if (FileSystem.FilenameStore.instance.exists(this.text) or FileSystem.DirnameStore.instance.exists(this.text)) { return this.*; } @@ -1659,12 +1663,12 @@ pub const Path = struct { new_path.namespace = this.namespace; new_path.is_symlink = this.is_symlink; return new_path; - } else if (allocators.sliceRange(this.pretty, this.text)) |start_end| { + } else if (allocators.sliceRange(this.pretty, this.text)) |start_len| { if (FileSystem.FilenameStore.instance.exists(this.text) or FileSystem.DirnameStore.instance.exists(this.text)) { return this.*; } var new_path = Fs.Path.init(try FileSystem.FilenameStore.instance.append([]const u8, this.text)); - new_path.pretty = this.text[start_end[0]..start_end[1]]; + new_path.pretty = this.text[start_len[0]..][0..start_len[1]]; new_path.namespace = this.namespace; new_path.is_symlink = this.is_symlink; return new_path; diff --git a/src/http.zig b/src/http.zig index 42529d43b6..82be549922 100644 --- a/src/http.zig +++ b/src/http.zig @@ -799,8 +799,13 @@ pub const HTTPThread = struct { } fn processEvents(this: *@This()) noreturn { - if (comptime Environment.isPosix) + if (comptime Environment.isPosix) { this.loop.num_polls = @max(2, this.loop.num_polls); + } else if (comptime Environment.isWindows) { + this.loop.inc(); + } else { + @compileError("TODO:"); + } while (true) { this.drainEvents(); @@ -810,7 +815,6 @@ pub const HTTPThread = struct { start_time = std.time.nanoTimestamp(); } Output.flush(); - // TODO(@paperdave): this does not wait any time on windows this.loop.run(); if (comptime Environment.isDebug) { const end = std.time.nanoTimestamp(); diff --git a/src/http/url_path.zig b/src/http/url_path.zig index e511acf3ba..d1e50072b3 100644 --- a/src/http/url_path.zig +++ b/src/http/url_path.zig @@ -71,12 +71,7 @@ pub fn parse(possibly_encoded_pathname_: string) !URLPath { bun.copy(u8, possibly_encoded_pathname, possibly_encoded_pathname_[0..possibly_encoded_pathname.len]); const clone = possibly_encoded_pathname[0..possibly_encoded_pathname.len]; - var fbs = std.io.fixedBufferStream( - // This is safe because: - // - this comes from a non-const buffer - // - percent *decoding* will always be <= length of the original string (no buffer overflow) - @constCast(possibly_encoded_pathname), - ); + var fbs = std.io.fixedBufferStream(possibly_encoded_pathname); const writer = fbs.writer(); decoded_pathname = possibly_encoded_pathname[0..try PercentEncoding.decodeFaultTolerant(@TypeOf(writer), writer, clone, &needs_redirect, true)]; diff --git a/src/http/websocket_http_client.zig b/src/http/websocket_http_client.zig index 06763e3388..03913d50d3 100644 --- a/src/http/websocket_http_client.zig +++ b/src/http/websocket_http_client.zig @@ -1062,7 +1062,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { // this function encodes to UTF-16 if > 127 // so we don't need to worry about latin1 non-ascii code points // we avoid trim since we wanna keep the utf8 validation intact - const utf16_bytes_ = strings.toUTF16AllocNoTrim(bun.default_allocator, data_, true) catch { + const utf16_bytes_ = strings.toUTF16AllocNoTrim(bun.default_allocator, data_, true, false) catch { this.terminate(ErrorCode.invalid_utf8); return; }; diff --git a/src/install/bin.zig b/src/install/bin.zig index 31278c69aa..1d752d9be2 100644 --- a/src/install/bin.zig +++ b/src/install/bin.zig @@ -392,40 +392,41 @@ pub const Bin = extern struct { }; defer file.close(); - const first_content_chunk = contents: { - const fd = bun.sys.openatWindows( - this.package_installed_node_modules, - if (link_global) - bun.strings.toWPathNormalized( - &filename3_buf, - target_path[this.relative_path_to_bin_for_windows_global_link_offset..], - ) - else - target_wpath, - std.os.O.RDONLY, - ).unwrap() catch |err| { - this.err = err; - return; + const shebang = shebang: { + const first_content_chunk = contents: { + const fd = bun.sys.openatWindows( + this.package_installed_node_modules, + if (link_global) + bun.strings.toWPathNormalized( + &filename3_buf, + target_path[this.relative_path_to_bin_for_windows_global_link_offset..], + ) + else + target_wpath, + std.os.O.RDONLY, + ).unwrap() catch break :contents null; + defer _ = bun.sys.close(fd); + const reader = fd.asFile().reader(); + const read = reader.read(&read_in_buf) catch break :contents null; + if (read == 0) { + break :contents null; + } + break :contents read_in_buf[0..read]; }; - defer _ = bun.sys.close(fd); - const reader = fd.asFile().reader(); - const read = reader.read(&read_in_buf) catch |err| { - this.err = err; - return; - }; - if (read == 0) { - this.err = error.FileNotFound; - return; + + if (first_content_chunk) |chunk| { + break :shebang WinBinLinkingShim.Shebang.parse(chunk, target_wpath) catch { + this.err = error.InvalidBinContent; + return; + }; + } else { + break :shebang WinBinLinkingShim.Shebang.parseFromBinPath(target_wpath); } - break :contents read_in_buf[0..read]; }; const shim = WinBinLinkingShim{ .bin_path = target_wpath, - .shebang = WinBinLinkingShim.Shebang.parse(first_content_chunk, target_wpath) catch { - this.err = error.InvalidBinContent; - return; - }, + .shebang = shebang, }; const len = shim.encodedLength(); @@ -448,19 +449,17 @@ pub const Bin = extern struct { destination_wpath.len -= 1; @memcpy(destination_wpath[destination_wpath.len - 3 ..], &[_]u16{ 'e', 'x', 'e' }); - if (node_modules.createFileW(destination_wpath, .{ - .exclusive = true, - })) |exe_file| { + // truncate=false is intentional so that the exe is always rewritten. this helps + // - you upgrade to a new version of bin_shim_impl (unlikely but possible) + // - if otherwise corrupt it yourself + if (node_modules.createFileW(destination_wpath, .{})) |exe_file| { defer exe_file.close(); exe_file.writer().writeAll(WinBinLinkingShim.embedded_executable_data) catch |err| { this.err = err; return; }; } else |err| { - if (err != error.PathAlreadyExists) { - this.err = err; - return; - } + this.err = err; } } } diff --git a/src/install/extract_tarball.zig b/src/install/extract_tarball.zig index 7bb2212d0d..4c1bd8969e 100644 --- a/src/install/extract_tarball.zig +++ b/src/install/extract_tarball.zig @@ -17,6 +17,7 @@ const string = @import("../string_types.zig").string; const strings = @import("../string_immutable.zig"); const Path = @import("../resolver/resolve_path.zig"); const Environment = bun.Environment; +const w = std.os.windows; const ExtractTarball = @This(); @@ -157,7 +158,7 @@ threadlocal var folder_name_buf: bun.PathBuffer = undefined; threadlocal var json_path_buf: bun.PathBuffer = undefined; fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractData { - var tmpdir = this.temp_dir; + const tmpdir = this.temp_dir; var tmpname_buf: [bun.MAX_PATH_BYTES]u8 = undefined; const name = this.name.slice(); const basename = brk: { @@ -183,8 +184,21 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD var resolved: string = ""; const tmpname = try FileSystem.instance.tmpname(basename[0..@min(basename.len, 32)], &tmpname_buf, tgz_bytes.len); - { - var extract_destination = tmpdir.makeOpenPath(std.mem.span(tmpname), .{}) catch |err| { + const extract_fd_on_windows = brk: { + var extract_destination = switch (Environment.os) { + .windows => makeOpenPathAccessMaskW( + tmpdir, + std.mem.span(tmpname), + w.STANDARD_RIGHTS_READ | + w.FILE_READ_ATTRIBUTES | + w.FILE_READ_EA | + w.SYNCHRONIZE | + w.FILE_TRAVERSE | + w.DELETE, + false, + ), + else => tmpdir.makeOpenPath(std.mem.span(tmpname), .{}), + } catch |err| { this.package_manager.log.addErrorFmt( null, logger.Loc.Empty, @@ -195,7 +209,8 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD return error.InstallFailed; }; - defer extract_destination.close(); + errdefer if (Environment.isWindows) extract_destination.close(); + defer if (!Environment.isWindows) extract_destination.close(); if (PackageManager.verbose_install) { Output.prettyErrorln("[{s}] Start extracting {s}", .{ name, tmpname }); @@ -276,7 +291,11 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD Output.prettyErrorln("[{s}] Extracted", .{name}); Output.flush(); } - } + + if (Environment.isWindows) { + break :brk bun.toFD(extract_destination.fd); + } + }; const folder_name = switch (this.resolution.tag) { .npm => this.package_manager.cachedNPMPackageFolderNamePrint(&folder_name_buf, name, this.resolution.value.npm.version), .github => PackageManager.cachedGitHubFolderNamePrint(&folder_name_buf, resolved), @@ -295,33 +314,23 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD // Now that we've extracted the archive, we rename. if (comptime Environment.isWindows) { - // TODO(dylan-conway) make this less painful - var from_buf: bun.PathBuffer = undefined; - const tmpdir_path = try bun.getFdPath(tmpdir.fd, &from_buf); - const from_path = Path.joinAbsStringZ(tmpdir_path, &.{bun.sliceTo(tmpname, 0)}, .auto); + defer _ = bun.sys.close(extract_fd_on_windows); - var to_buf: bun.PathBuffer = undefined; - const cache_dir_path = try bun.getFdPath(cache_dir.fd, &to_buf); - const to_path = Path.joinAbsStringBufZ(cache_dir_path, &to_buf, &.{folder_name}, .auto); + var folder_name_wbuf: bun.WPathBuffer = undefined; + const folder_name_w = bun.strings.toWPathNormalized(&folder_name_wbuf, folder_name); - var from_path_buf_w: bun.WPathBuffer = undefined; - const from_path_w = bun.strings.toWPath(&from_path_buf_w, from_path); - var to_path_buf_w: bun.WPathBuffer = undefined; - const to_path_w = bun.strings.toWPath(&to_path_buf_w, to_path); - - if (bun.windows.MoveFileExW( - from_path_w, - to_path_w, - bun.windows.MOVEFILE_COPY_ALLOWED | bun.windows.MOVEFILE_REPLACE_EXISTING | bun.windows.MOVEFILE_WRITE_THROUGH, - ) == bun.windows.FALSE) { - this.package_manager.log.addErrorFmt( - null, - logger.Loc.Empty, - this.package_manager.allocator, - "moving \"{s}\" to cache dir failed: From: {s}\n To: {s}", - .{ name, tmpname, folder_name }, - ) catch unreachable; - return error.InstallFailed; + switch (bun.C.moveOpenedFileAtLoose(extract_fd_on_windows, bun.toFD(cache_dir.fd), folder_name_w, false)) { + .err => |err| { + this.package_manager.log.addErrorFmt( + null, + logger.Loc.Empty, + this.package_manager.allocator, + "moving \"{s}\" to cache dir failed: {}\n From: {s}\n To: {}", + .{ name, err, tmpname, std.unicode.fmtUtf16le(folder_name_w) }, + ) catch unreachable; + return error.InstallFailed; + }, + .result => {}, } } else { switch (bun.sys.renameat(bun.toFD(tmpdir.fd), bun.sliceTo(tmpname, 0), bun.toFD(cache_dir.fd), folder_name)) { @@ -351,7 +360,6 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD ) catch unreachable; return error.InstallFailed; }; - defer final_dir.close(); // and get the fd path const final_path = bun.getFdPath( @@ -433,3 +441,92 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD .json_len = json_len, }; } + +// TODO(@paperdave): upstream making this public into zig std +// there is zero reason this must be copied +// +/// Calls makeOpenDirAccessMaskW iteratively to make an entire path +/// (i.e. creating any parent directories that do not exist). +/// Opens the dir if the path already exists and is a directory. +/// This function is not atomic, and if it returns an error, the file system may +/// have been modified regardless. +fn makeOpenPathAccessMaskW(self: std.fs.Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) std.os.OpenError!std.fs.Dir { + var it = try std.fs.path.componentIterator(sub_path); + // If there are no components in the path, then create a dummy component with the full path. + var component = it.last() orelse std.fs.path.NativeUtf8ComponentIterator.Component{ + .name = "", + .path = sub_path, + }; + + while (true) { + const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path); + const is_last = it.peekNext() == null; + var result = makeOpenDirAccessMaskW(self, sub_path_w.span().ptr, access_mask, .{ + .no_follow = no_follow, + .create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE, + }) catch |err| switch (err) { + error.FileNotFound => |e| { + component = it.previous() orelse return e; + continue; + }, + else => |e| return e, + }; + + component = it.next() orelse return result; + // Don't leak the intermediate file handles + result.close(); + } +} +const MakeOpenDirAccessMaskWOptions = struct { + no_follow: bool, + create_disposition: u32, +}; + +fn makeOpenDirAccessMaskW(self: std.fs.Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) std.os.OpenError!std.fs.Dir { + var result = std.fs.Dir{ + .fd = undefined, + }; + + const path_len_bytes = @as(u16, @intCast(std.mem.sliceTo(sub_path_w, 0).len * 2)); + var nt_name = w.UNICODE_STRING{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w), + }; + var attr = w.OBJECT_ATTRIBUTES{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; + var io: w.IO_STATUS_BLOCK = undefined; + const rc = w.ntdll.NtCreateFile( + &result.fd, + access_mask, + &attr, + &io, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_READ | w.FILE_SHARE_WRITE, + flags.create_disposition, + w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, + null, + 0, + ); + + switch (rc) { + .SUCCESS => return result, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .NOT_A_DIRECTORY => return error.NotDir, + // This can happen if the directory has 'List folder contents' permission set to 'Deny' + // and the directory is trying to be opened for iteration. + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_PARAMETER => return error.BadPathName, + else => return w.unexpectedStatus(rc), + } +} diff --git a/src/install/install.zig b/src/install/install.zig index 333e7a8b7a..1f405c7224 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -692,7 +692,9 @@ const Task = struct { ) catch |err| { if (comptime Environment.isDebug) { if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); + _ = trace; // autofix + + // std.debug.dumpStackTrace(trace.*); } } @@ -1061,10 +1063,40 @@ pub const PackageInstall = struct { var package_json_checker = json_parser.PackageJSONVersionChecker.init(allocator, &source, &log) catch return false; _ = package_json_checker.parseExpr() catch return false; if (!package_json_checker.has_found_name or !package_json_checker.has_found_version or log.errors > 0) return false; + const found_version = package_json_checker.found_version; + // Check if the version matches + if (!strings.eql(found_version, this.package_version)) { + const offset = brk: { + // ASCII only. + for (0..found_version.len) |c| { + switch (found_version[c]) { + // newlines & whitespace + ' ', + '\t', + '\n', + '\r', + std.ascii.control_code.vt, + std.ascii.control_code.ff, - // Version is more likely to not match than name, so we check it first. - return strings.eql(package_json_checker.found_version, this.package_version) and - strings.eql(package_json_checker.found_name, this.package_name); + // version separators + 'v', + '=', + => {}, + else => { + break :brk c; + }, + } + } + // If we didn't find any of these characters, there's no point in checking the version again. + // it will never match. + return false; + }; + + if (!strings.eql(found_version[offset..], this.package_version)) return false; + } + + // lastly, check the name. + return strings.eql(package_json_checker.found_name, this.package_name); } pub const Result = union(Tag) { @@ -1416,11 +1448,12 @@ pub const PackageInstall = struct { // Windows limits hardlinks to 1023 per file if (bun.windows.CreateHardLinkW(dest, src, null) == 0) { if (bun.windows.CopyFileW(src, dest, 0) == 0) { - if (bun.windows.Win32Error.get().toSystemErrno()) |_| { - // TODO: make this better - return error.FailedToCopyFile; + const e = bun.windows.Win32Error.get(); + if (e.toSystemErrno()) |err| { + return bun.errnoToZigErr(err); } - + // If this code path is reached, it should have a toSystemErrno mapping + Output.warn("Failed to copy file during installation: {s}", .{@tagName(e)}); return error.FailedToCopyFile; } } @@ -5016,7 +5049,7 @@ pub const PackageManager = struct { }, ); } else if (comptime log_level != .silent) { - const fmt = "error: {s} extracting tarball for {s}"; + const fmt = "error: {s} extracting tarball for {s}\n"; const args = .{ @errorName(err), alias, @@ -6544,6 +6577,7 @@ pub const PackageManager = struct { current_package_json_buf, 0, ); + if (comptime Environment.isWindows) try manager.root_package_json_file.seekTo(0); const package_json_source = logger.Source.initPathString( package_json_cwd, @@ -6719,6 +6753,7 @@ pub const PackageManager = struct { current_package_json_buf, 0, ); + if (comptime Environment.isWindows) try manager.root_package_json_file.seekTo(0); const package_json_source = logger.Source.initPathString( package_json_cwd, @@ -7424,6 +7459,7 @@ pub const PackageManager = struct { current_package_json_buf, 0, ); + if (comptime Environment.isWindows) try manager.root_package_json_file.seekTo(0); const package_json_source = logger.Source.initPathString( package_json_cwd, @@ -9279,14 +9315,11 @@ pub const PackageManager = struct { } } - switch (Output.enable_ansi_colors) { - inline else => |enable_ansi_colors| { - try manager.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), enable_ansi_colors); - }, - } - + try manager.log.printForLogLevel(Output.errorWriter()); if (manager.log.hasErrors()) Global.crash(); + manager.log.reset(); + // This operation doesn't perform any I/O, so it should be relatively cheap. manager.lockfile = try manager.lockfile.cleanWithLogger( manager.package_json_updates, @@ -9423,6 +9456,9 @@ pub const PackageManager = struct { ); } + try manager.log.printForLogLevel(Output.errorWriter()); + if (manager.log.hasErrors()) Global.crash(); + if (needs_new_lockfile) { manager.summary.add = @as(u32, @truncate(manager.lockfile.packages.len)); } diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index cbf5d5e006..602efc4aa0 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -1646,7 +1646,7 @@ pub fn saveToDisk(this: *Lockfile, filename: stringZ) void { .fd = bun.toFD(file.handle), }, .dirfd = bun.invalid_fd, - .data = .{ .string = .{ .utf8 = bun.JSC.ZigString.Slice.from(bytes.items, bun.default_allocator) } }, + .data = .{ .string = .{ .utf8 = bun.JSC.ZigString.Slice.init(bun.default_allocator, bytes.items) } }, }, .sync, )) { diff --git a/src/install/migration.zig b/src/install/migration.zig index 5a058963f3..8c404bce59 100644 --- a/src/install/migration.zig +++ b/src/install/migration.zig @@ -387,9 +387,20 @@ pub fn migrateNPMLockfile(this: *Lockfile, allocator: Allocator, log: *logger.Lo try this.workspace_paths.ensureTotalCapacity(allocator, wksp.map.unmanaged.entries.len); try this.workspace_versions.ensureTotalCapacity(allocator, wksp.map.unmanaged.entries.len); + var path_buf: if (Environment.isWindows) bun.PathBuffer else void = undefined; for (wksp.map.keys(), wksp.map.values()) |k, v| { const name_hash = stringHash(v.name); - this.workspace_paths.putAssumeCapacity(name_hash, builder.append(String, k)); + + this.workspace_paths.putAssumeCapacity( + name_hash, + builder.append( + String, + if (comptime Environment.isWindows) + bun.path.normalizeBuf(k, &path_buf, .windows) + else + k, + ), + ); if (v.version) |version_string| { const sliced_version = Semver.SlicedString.init(version_string, version_string); diff --git a/src/install/resolution.zig b/src/install/resolution.zig index 040d2c0fda..bc8f14eed4 100644 --- a/src/install/resolution.zig +++ b/src/install/resolution.zig @@ -9,6 +9,7 @@ const ExtractTarball = @import("./extract_tarball.zig"); const strings = @import("../string_immutable.zig"); const VersionedURL = @import("./versioned_url.zig").VersionedURL; const bun = @import("root").bun; +const Path = bun.path; pub const Resolution = extern struct { tag: Tag = .uninitialized, diff --git a/src/install/semver.zig b/src/install/semver.zig index b1fafd5f47..2942c384ca 100644 --- a/src/install/semver.zig +++ b/src/install/semver.zig @@ -1043,17 +1043,27 @@ pub const Version = extern struct { var i: usize = 0; - i += strings.lengthOfLeadingWhitespaceASCII(input[i..]); - if (i == input.len) { - result.valid = false; - return result; + for (0..input.len) |c| { + switch (input[c]) { + // newlines & whitespace + ' ', + '\t', + '\n', + '\r', + std.ascii.control_code.vt, + std.ascii.control_code.ff, + + // version separators + 'v', + '=', + => {}, + else => { + i = c; + break; + }, + } } - if (input[i] == 'v' or input[i] == '=') { - i += 1; - } - - i += strings.lengthOfLeadingWhitespaceASCII(input[i..]); if (i == input.len) { result.valid = false; return result; diff --git a/src/install/windows-shim/BinLinkingShim.zig b/src/install/windows-shim/BinLinkingShim.zig index a1a8465e98..ae8438d7e3 100644 --- a/src/install/windows-shim/BinLinkingShim.zig +++ b/src/install/windows-shim/BinLinkingShim.zig @@ -26,23 +26,36 @@ bin_path: []const u16, /// Information found within the target file's shebang shebang: ?Shebang, +/// Random numbers are chosen for validation purposes +/// These arbitrary numbers will probably not show up in the other fields. +/// This will reveal off-by-one mistakes. +pub const VersionFlag = enum(u13) { + pub const current = .v3; + + v1 = 5474, + // Fix bug where paths were not joined correctly + v2 = 5475, + // Added an error message for when the process is not found + v3 = 5476, + _, +}; + pub const Flags = packed struct(u16) { - // the shim doesnt use this right now + // this is set if the shebang content is "node" or "bun" is_node_or_bun: bool, // this is for validation that the shim is not corrupt and to detect offset memory reads - // if this format is ever modified, we will set this flag to false to indicate version 2+ - is_version_1: bool = true, + is_valid: bool = true, // indicates if a shebang is present has_shebang: bool, - // this is for validation that the shim is not corrupt and to detect offset memory reads - must_be_5474: u13 = 5474, + + version_tag: VersionFlag = VersionFlag.current, pub fn isValid(flags: Flags) bool { const mask: u16 = @bitCast(Flags{ .is_node_or_bun = false, - .is_version_1 = true, + .is_valid = true, .has_shebang = false, - .must_be_5474 = std.math.maxInt(u13), + .version_tag = @enumFromInt(std.math.maxInt(u13)), }); const compare_to: u16 = @bitCast(Flags{ diff --git a/src/install/windows-shim/build.zig b/src/install/windows-shim/build.zig index 48e93da2e9..0f369f33bc 100644 --- a/src/install/windows-shim/build.zig +++ b/src/install/windows-shim/build.zig @@ -34,11 +34,7 @@ pub fn build(b: *std.Build) void { .optimize = .Debug, .use_llvm = true, .use_lld = true, - .unwind_tables = false, - .omit_frame_pointer = true, - .strip = true, .linkage = .static, - .sanitize_thread = false, .single_threaded = true, .link_libc = false, }); diff --git a/src/install/windows-shim/bun_shim_impl.exe b/src/install/windows-shim/bun_shim_impl.exe index ded01c18a8..5a81cbcafb 100755 Binary files a/src/install/windows-shim/bun_shim_impl.exe and b/src/install/windows-shim/bun_shim_impl.exe differ diff --git a/src/install/windows-shim/bun_shim_impl.zig b/src/install/windows-shim/bun_shim_impl.zig index 5f84407afd..278beeef27 100644 --- a/src/install/windows-shim/bun_shim_impl.zig +++ b/src/install/windows-shim/bun_shim_impl.zig @@ -34,7 +34,8 @@ //! Prior Art: //! - https://github.com/ScoopInstaller/Shim/blob/master/src/shim.cs //! -//! The compiled binary is 10240 bytes and is `@embedFile`d into Bun itself +//! The compiled binary is 10752 bytes and is `@embedFile`d into Bun itself. +//! When this file is updated, the new binary should be compiled and BinLinkingShim.VersionFlag.current should be updated. const std = @import("std"); const builtin = @import("builtin"); @@ -147,6 +148,9 @@ const FailReason = enum { InvalidShimValidation, InvalidShimBounds, CouldNotDirectLaunch, + BinNotFound, + InterpreterNotFound, + ElevationRequired, pub fn render(reason: FailReason) []const u8 { return switch (reason) { @@ -158,6 +162,12 @@ const FailReason = enum { .InvalidShimDataSize => "bin metadata is corrupt (size)", .InvalidShimValidation => "bin metadata is corrupt (validate)", .InvalidShimBounds => "bin metadata is corrupt (bounds)", + // The difference between these two is that one is with a shebang (#!/usr/bin/env node) and + // the other is without. This is a helpful distinction because it can detect if something + // like node or bun is not in %path%, vs the actual executable was not installed in node_modules. + .InterpreterNotFound => "interpreter executable could not be found", + .BinNotFound => "bin executable does not exist on disk", + .ElevationRequired => "process requires elevation", .CreateProcessFailed => "could not create process", .CouldNotDirectLaunch => if (!is_standalone) @@ -243,7 +253,7 @@ noinline fn failWithReason(reason: FailReason) noreturn { const nt_object_prefix = [4]u16{ '\\', '?', '?', '\\' }; -inline fn launcher(bun_ctx: anytype) noreturn { +fn launcher(bun_ctx: anytype) noreturn { // peb! w.teb is a couple instructions of inline asm const teb: *w.TEB = @call(.always_inline, w.teb, .{}); const peb = teb.ProcessEnvironmentBlock; @@ -394,32 +404,54 @@ inline fn launcher(bun_ctx: anytype) noreturn { // // we do this by reusing the memory in the first buffer // BUF1: '\??\C:\Users\dave\project\node_modules\.bin\hello.bunx!!!!!!!!!!!!!!!!!!!!!!' - // ^^ ^ ^ - // S| | image_path_b_len + // ^^ ^ ^ + // S| | image_path_b_len + nt_object_prefix.len // | 'ptr' initial value // the read ptr - var read_ptr = brk: { - var left = image_path_b_len / 2 - (if (is_standalone) 2 * ".exe".len else ".bunx".len); - var ptr: [*]u16 = buf1_u16[left..]; - inline for (0..1) |_| { - while (true) { - if (ptr[0] == '\\') { - break; - } + var read_ptr: [*]u16 = brk: { + var left = image_path_b_len / 2 - (if (is_standalone) ".exe".len else ".bunx".len) - 1; + var ptr: [*]u16 = buf1_u16[nt_object_prefix.len + left ..]; + if (dbg) debug("left = {d}, at {}, after {}\n", .{ left, ptr[0], ptr[1] }); + + // if this is false, potential out of bounds memory access + std.debug.assert(@intFromPtr(ptr) - left * @sizeOf(std.meta.Child(@TypeOf(ptr))) >= @intFromPtr(buf1_u16)); + // we start our search right before the . as we know the extension is '.bunx' + std.debug.assert(ptr[1] == '.'); + + while (true) { + if (dbg) debug("1 - {}\n", .{std.unicode.fmtUtf16le(ptr[0..1])}); + if (ptr[0] == '\\') { left -= 1; - if (left == 0) { - fail(.NoDirname); - } - ptr -= 2; - std.debug.assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); + // ptr is of type [*]u16, which means -= operates on number of ITEMS, not BYTES + ptr -= 1; + break; } + left -= 1; + if (left == 0) { + fail(.NoDirname); + } + ptr -= 1; + std.debug.assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); } - // in this state, ptr is pointing to what is marked 'S' above - // adding one to get to the read ptr - ptr = @ptrFromInt(@intFromPtr(ptr) + @sizeOf(u16)); - break :brk ptr; + // inlined loop to do this again, because the completion case is different + // using `inline for` caused comptime issues that made the code much harder to read + while (true) { + if (dbg) debug("2 - {}\n", .{std.unicode.fmtUtf16le(ptr[0..1])}); + if (ptr[0] == '\\') { + // ptr is at the position marked s, so move forward one *character* + break :brk ptr + 1; + } + left -= 1; + if (left == 0) { + fail(.NoDirname); + } + ptr -= 1; + std.debug.assert(@intFromPtr(ptr) >= @intFromPtr(buf1_u16)); + } + comptime unreachable; }; std.debug.assert(read_ptr[0] != '\\'); + std.debug.assert((read_ptr - 1)[0] == '\\'); const read_max_len = buf1.len * 2 - (@intFromPtr(read_ptr) - @intFromPtr(buf1_u16)); @@ -577,9 +609,9 @@ inline fn launcher(bun_ctx: anytype) noreturn { // Copy the filename in. There is no leading " but there is a trailing " // BUF1: '\??\C:\Users\dave\project\node_modules\my-cli\src\app.js"#node #####!!!!!!!!!!' - // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ^ read_ptr + // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ^ read_ptr // BUF2: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js"!!!!!!!!!!!!!!!!!!!!' - const length_of_filename_u8 = (@intFromPtr(read_ptr) - (2 * "\x00".len)) - @intFromPtr(buf1_u8); + const length_of_filename_u8 = @intFromPtr(read_ptr) - @intFromPtr(buf1_u8) - nt_object_prefix.len - 6; @memcpy( buf2_u8[shebang_arg_len_u8 + 2 * "\"".len ..][0..length_of_filename_u8], buf1_u8[2 * nt_object_prefix.len ..][0..length_of_filename_u8], @@ -591,10 +623,10 @@ inline fn launcher(bun_ctx: anytype) noreturn { // | |filename_len where the user args go // | the quote // shebang_arg_len - read_ptr = @ptrFromInt(@intFromPtr(buf2_u8) + shebang_arg_len_u8 + length_of_filename_u8 + 2 * "\"".len); + read_ptr = @ptrFromInt(@intFromPtr(buf2_u8) + length_of_filename_u8 + 2 * "\"\"".len + 2 * nt_object_prefix.len); if (user_arguments_u8.len > 0) { @memcpy(@as([*]u8, @ptrCast(read_ptr)), user_arguments_u8); - read_ptr += user_arguments_u8.len; + read_ptr = @ptrFromInt(@intFromPtr(read_ptr) + user_arguments_u8.len); } // BUF2: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js" --flags#!!!!!!!!!!' @@ -655,17 +687,27 @@ inline fn launcher(bun_ctx: anytype) noreturn { &process, ); if (did_process_spawn == 0) { + const spawn_err = k32.GetLastError(); if (dbg) { - const spawn_err = k32.GetLastError(); printError("CreateProcessW failed: {s}\n", .{@tagName(spawn_err)}); } - // TODO: ERROR_ELEVATION_REQUIRED must take a fallback path, this path is potentially slower: - // This likely will not be an issue anyone runs into for a while, because it implies - // the shebang depends on something that requires UAC, which .... why? - // - // https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/how-it-works#user - // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew - fail(.CreateProcessFailed); + switch (spawn_err) { + .FILE_NOT_FOUND => if (flags.has_shebang) + fail(.InterpreterNotFound) + else + fail(.BinNotFound), + + // TODO: ERROR_ELEVATION_REQUIRED must take a fallback path, this path is potentially slower: + // This likely will not be an issue anyone runs into for a while, because it implies + // the shebang depends on something that requires UAC, which .... why? + // + // https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/how-it-works#user + // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew + .ELEVATION_REQUIRED => fail(.ElevationRequired), + + else => fail(.CreateProcessFailed), + } + comptime unreachable; } _ = k32.WaitForSingleObject(process.hProcess, w.INFINITE); diff --git a/src/js/README.md b/src/js/README.md index 52c9ba6f5e..3723492955 100644 --- a/src/js/README.md +++ b/src/js/README.md @@ -38,13 +38,13 @@ On top of this, we have some special functions that are handled by the builtin p - `require` works, but it must be passed a **string literal** that resolves to a module within `src/js`. This call gets replaced with `$getInternalField($internalModuleRegistery, )`, which directly loads the module by its generated numerical ID, skipping the resolver for inter-internal modules. -- `$debug` is exactly like console.log, but is stripped in release builds. It is disabled by default, requiring you to pass one of: `BUN_DEBUG_MODULE_NAME=1`, `BUN_DEBUG_JS=1`, or `BUN_DEBUG_ALL=1`. You can also do `if($debug) {}` to check if debug env var is set. +- `$debug()` is exactly like console.log, but is stripped in release builds. It is disabled by default, requiring you to pass one of: `BUN_DEBUG_MODULE_NAME=1`, `BUN_DEBUG_JS=1`, or `BUN_DEBUG_ALL=1`. You can also do `if($debug) {}` to check if debug env var is set. + +- `$assert()` in debug builds will assert the condition, but it is stripped in release builds. If an assertion fails, the program continues to run, but an error is logged in the console containing the original source condition and any extra messages specified. - `IS_BUN_DEVELOPMENT` is inlined to be `true` in all development builds. -- `process.platform` is properly inlined and DCE'd. Do use this to run different code on different platforms. - -- `$bundleError()` is like Zig's `@compileError`. It will stop a compile from succeeding. +- `process.platform` and `process.arch` is properly inlined and DCE'd. Do use this to run different code on different platforms. ## Builtin Modules diff --git a/src/js/builtins/Module.ts b/src/js/builtins/Module.ts index 01ca8db21c..ad098e93b6 100644 --- a/src/js/builtins/Module.ts +++ b/src/js/builtins/Module.ts @@ -12,7 +12,7 @@ export function require(this: CommonJSModuleRecord, id: string) { $overriddenName = "require"; $visibility = "Private"; export function overridableRequire(this: CommonJSModuleRecord, id: string) { - const existing = $requireMap.$get(id) || $requireMap.$get((id = $resolveSync(id, this.path, false))); + const existing = $requireMap.$get(id) || $requireMap.$get((id = $resolveSync(id, this.id, false))); if (existing) { // Scenario where this is necessary: // @@ -86,8 +86,8 @@ export function overridableRequire(this: CommonJSModuleRecord, id: string) { } $visibility = "Private"; -export function requireResolve(this: string | { path: string }, id: string) { - return $resolveSync(id, typeof this === "string" ? this : this?.path, false); +export function requireResolve(this: string | { id: string }, id: string) { + return $resolveSync(id, typeof this === "string" ? this : this?.id, false); } $visibility = "Private"; diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts index 48495efdeb..2bff5b66e5 100644 --- a/src/js/builtins/ProcessObjectInternals.ts +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -344,3 +344,47 @@ export function setMainModule(value) { $putByIdDirectPrivate(this, "main", value); return true; } + +type InternalEnvMap = Record; + +export function windowsEnv(internalEnv: InternalEnvMap, envMapList: Array) { + // The use of String(key) here is intentional because Node.js as of v21.5.0 will throw + // on symbol keys as it seems they assume the user uses string keys: + // + // it throws "Cannot convert a Symbol value to a string" + + return new Proxy(internalEnv, { + get(_, p) { + return typeof p === "string" ? Reflect.get(internalEnv, p.toUpperCase()) : undefined; + }, + set(_, p, value) { + var k = String(p).toUpperCase(); + $assert(typeof p === "string"); // proxy is only string and symbol. the symbol would have thrown by now + if (!Reflect.has(internalEnv, k)) { + envMapList.push(p); + } + return Reflect.set(internalEnv, k, String(value)); + }, + has(_, p) { + return typeof p === "string" ? Reflect.has(internalEnv, p.toUpperCase()) : false; + }, + deleteProperty(_, p) { + return typeof p === "string" ? Reflect.deleteProperty(internalEnv, p.toUpperCase()) : true; + }, + defineProperty(_, p, attributes) { + var k = String(p).toUpperCase(); + $assert(typeof p === "string"); // proxy is only string and symbol. the symbol would have thrown by now + if (!Reflect.has(internalEnv, k)) { + envMapList.push(p); + } + return Reflect.defineProperty(internalEnv, k, attributes); + }, + getOwnPropertyDescriptor(target, p) { + return typeof p === "string" ? Reflect.getOwnPropertyDescriptor(target, p.toUpperCase()) : undefined; + }, + ownKeys() { + // .slice() because paranoia that there is a way to call this without the engine cloning it for us + return envMapList.slice(); + }, + }); +} diff --git a/src/js/private.d.ts b/src/js/private.d.ts index e7564e6b72..f79ded7f45 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -1,11 +1,6 @@ // The types in this file are not publicly defined, but do exist. // Stuff like `Bun.fs()` and so on. -/** - * Works like the zig `@compileError` built-in, but only supports plain strings. - */ -declare function $bundleError(error: string); - type BunFSWatchOptions = { encoding?: BufferEncoding; persistent?: boolean; recursive?: boolean; signal?: AbortSignal }; type BunWatchEventType = "rename" | "change" | "error" | "close"; type BunWatchListener = (event: WatchEventType, filename: T | undefined) => void; diff --git a/src/js_ast.zig b/src/js_ast.zig index 05b7aeb9ae..8672c28dc3 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -2266,7 +2266,7 @@ pub const E = struct { if (s.isUTF8()) { if (comptime !Environment.isNative) { - const allocated = (strings.toUTF16Alloc(bun.default_allocator, s.data, false) catch return 0) orelse return s.data.len; + const allocated = (strings.toUTF16Alloc(bun.default_allocator, s.data, false, false) catch return 0) orelse return s.data.len; defer bun.default_allocator.free(allocated); return @as(u32, @truncate(allocated.len)); } diff --git a/src/libarchive/libarchive-bindings.zig b/src/libarchive/libarchive-bindings.zig index 73a6cc8cb7..33b0766b29 100644 --- a/src/libarchive/libarchive-bindings.zig +++ b/src/libarchive/libarchive-bindings.zig @@ -1,5 +1,5 @@ const bun = @import("root").bun; -pub const wchar_t = c_int; +pub const wchar_t = u16; pub const la_int64_t = i64; pub const la_ssize_t = isize; pub const struct_archive = opaque {}; diff --git a/src/libarchive/libarchive.zig b/src/libarchive/libarchive.zig index a5e2f10db5..d5d00754e0 100644 --- a/src/libarchive/libarchive.zig +++ b/src/libarchive/libarchive.zig @@ -471,7 +471,7 @@ pub const Archive = struct { pub fn extractToDir( file_buffer: []const u8, - dir_: std.fs.Dir, + dir: std.fs.Dir, ctx: ?*Archive.Context, comptime ContextType: type, appender: ContextType, @@ -487,9 +487,10 @@ pub const Archive = struct { _ = stream.openRead(); const archive = stream.archive; var count: u32 = 0; - const dir = dir_; const dir_fd = dir.fd; + var w_path_buf: if (Environment.isWindows) bun.WPathBuffer else void = undefined; + loop: while (true) { const r = @as(Status, @enumFromInt(lib.archive_read_next_header(archive, &entry))); @@ -498,19 +499,40 @@ pub const Archive = struct { Status.retry => continue :loop, Status.failed, Status.fatal => return error.Fail, else => { - var pathname: [:0]const u8 = bun.sliceTo(lib.archive_entry_pathname(entry).?, 0); + // TODO: + // Due to path separator replacement and other copies that happen internally, libarchive changes the + // storage type of paths on windows to wide character strings. Using `archive_entry_pathname` or `archive_entry_pathname_utf8` + // on an wide character string will return null if there are non-ascii characters. + // (this can be seen by installing @fastify/send, which has a path "@fastify\send\test\fixtures\snow ☃") + // + // Ideally, we find a way to tell libarchive to not convert the strings to wide characters and also to not + // replace path separators. We can do both of these with our own normalization and utf8/utf16 string conversion code. + var pathname: bun.OSPathSliceZ = if (comptime Environment.isWindows) brk: { + const normalized = bun.path.normalizeBufT( + u16, + std.mem.span(lib.archive_entry_pathname_w(entry)), + &w_path_buf, + .windows, + ); + w_path_buf[normalized.len] = 0; + break :brk w_path_buf[0..normalized.len :0]; + } else std.mem.sliceTo(lib.archive_entry_pathname(entry), 0); if (comptime ContextType != void and @hasDecl(std.meta.Child(ContextType), "onFirstDirectoryName")) { if (appender.needs_first_dirname) { - appender.onFirstDirectoryName(strings.withoutTrailingSlash(bun.asByteSlice(pathname))); + if (comptime Environment.isWindows) { + const list = std.ArrayList(u8).init(default_allocator); + var result = try strings.toUTF8ListWithType(list, []const u16, pathname[0..pathname.len]); + // onFirstDirectoryName copies the contents of pathname to another buffer, safe to free + defer result.deinit(); + appender.onFirstDirectoryName(strings.withoutTrailingSlash(result.items)); + } else { + appender.onFirstDirectoryName(strings.withoutTrailingSlash(bun.asByteSlice(pathname))); + } } } - var tokenizer = if (comptime Environment.isWindows) - // TODO(dylan-conway): I think this should only be '/' - std.mem.tokenizeAny(u8, bun.asByteSlice(pathname), "/\\") - else - std.mem.tokenizeScalar(u8, bun.asByteSlice(pathname), '/'); + var tokenizer = std.mem.tokenizeScalar(bun.OSPathChar, pathname, std.fs.path.sep); comptime var depth_i: usize = 0; inline while (depth_i < depth_to_skip) : (depth_i += 1) { @@ -518,15 +540,15 @@ pub const Archive = struct { } const pathname_ = tokenizer.rest(); - pathname = @as([*]const u8, @ptrFromInt(@intFromPtr(pathname_.ptr)))[0..pathname_.len :0]; + pathname = @as([*]const bun.OSPathChar, @ptrFromInt(@intFromPtr(pathname_.ptr)))[0..pathname_.len :0]; if (pathname.len == 0) continue; const kind = C.kindFromMode(lib.archive_entry_filetype(entry)); - const slice = bun.asByteSlice(pathname); + const path_slice: bun.OSPathSlice = pathname.ptr[0..pathname.len]; if (comptime log) { - Output.prettyln(" {s}", .{pathname}); + Output.prettyln(" {}", .{bun.fmt.fmtOSPath(path_slice)}); } count += 1; @@ -545,15 +567,15 @@ pub const Archive = struct { mode |= 0o1; if (comptime Environment.isWindows) { - std.os.mkdirat(dir_fd, pathname, @as(u32, @intCast(mode))) catch |err| { + std.os.mkdiratW(dir_fd, pathname, @as(u32, @intCast(mode))) catch |err| { if (err == error.PathAlreadyExists or err == error.NotDir) break; - try bun.makePath(dir, std.fs.path.dirname(slice) orelse return err); - try std.os.mkdirat(dir_fd, pathname, 0o777); + try bun.MakePath.makePath(u16, dir, bun.Dirname.dirname(u16, path_slice) orelse return err); + try std.os.mkdiratW(dir_fd, pathname, 0o777); }; } else { std.os.mkdiratZ(dir_fd, pathname, @as(u32, @intCast(mode))) catch |err| { if (err == error.PathAlreadyExists or err == error.NotDir) break; - try bun.makePath(dir, std.fs.path.dirname(slice) orelse return err); + try bun.makePath(dir, std.fs.path.dirname(path_slice) orelse return err); try std.os.mkdiratZ(dir_fd, pathname, 0o777); }; } @@ -561,12 +583,12 @@ pub const Archive = struct { Kind.sym_link => { const link_target = lib.archive_entry_symlink(entry).?; if (comptime Environment.isWindows) { - @panic("TODO on Windows"); + @panic("TODO on Windows: Extracting archives containing symbolic links."); } std.os.symlinkatZ(link_target, dir_fd, pathname) catch |err| brk: { switch (err) { error.AccessDenied, error.FileNotFound => { - dir.makePath(std.fs.path.dirname(slice) orelse return err) catch {}; + dir.makePath(std.fs.path.dirname(path_slice) orelse return err) catch {}; break :brk try std.os.symlinkatZ(link_target, dir_fd, pathname); }, else => { @@ -577,39 +599,71 @@ pub const Archive = struct { }, Kind.file => { const mode: bun.Mode = if (comptime Environment.isWindows) 0 else @intCast(lib.archive_entry_perm(entry)); - const file = dir.createFileZ(pathname, .{ .truncate = true, .mode = mode }) catch |err| brk: { - switch (err) { - error.AccessDenied, error.FileNotFound => { - dir.makePath(std.fs.path.dirname(slice) orelse return err) catch {}; - break :brk try dir.createFileZ(pathname, .{ - .truncate = true, - .mode = mode, - }); - }, - else => { - return err; - }, + + const file_handle_native = brk: { + if (Environment.isWindows) { + const flags = std.os.O.WRONLY | std.os.O.CREAT | std.os.O.TRUNC; + switch (bun.sys.openatWindows(bun.toFD(dir_fd), pathname, flags)) { + .result => |fd| break :brk fd, + .err => |e| switch (e.errno) { + @intFromEnum(bun.C.E.PERM), @intFromEnum(bun.C.E.NOENT) => { + bun.MakePath.makePath(u16, dir, bun.Dirname.dirname(u16, path_slice) orelse return bun.errnoToZigErr(e.errno)) catch {}; + break :brk try bun.sys.openatWindows(bun.toFD(dir_fd), pathname, flags).unwrap(); + }, + else => { + return bun.errnoToZigErr(e.errno); + }, + }, + } + } else { + break :brk (dir.createFileZ(pathname, .{ .truncate = true, .mode = mode }) catch |err| { + switch (err) { + error.AccessDenied, error.FileNotFound => { + dir.makePath(std.fs.path.dirname(path_slice) orelse return err) catch {}; + break :brk (try dir.createFileZ(pathname, .{ + .truncate = true, + .mode = mode, + })).handle; + }, + else => { + return err; + }, + } + }).handle; } }; - const file_handle = bun.toLibUVOwnedFD(file.handle); + const file_handle = bun.toLibUVOwnedFD(file_handle_native); - defer { - if (comptime close_handles) _ = bun.sys.close(file_handle); - } + defer if (comptime close_handles) { + // On windows, AV hangs these closes really badly. + // 'bun i @mui/icons-material' takes like 20 seconds to extract + // mostly spend on waiting for things to close closing + // + // Using Async.Closer defers closing the file to a different thread, + // which can make the NtSetInformationFile call fail. + // + // Using async closing doesnt actually improve end user performance + // probably because our process is still waiting on AV to do it's thing. + // + // But this approach does not actually solve the problem, it just + // defers the close to a different thread. And since we are already + // on a worker thread, that doesn't help us. + _ = bun.sys.close(file_handle); + }; const entry_size = @max(lib.archive_entry_size(entry), 0); const size = @as(usize, @intCast(entry_size)); if (size > 0) { if (ctx) |ctx_| { const hash: u64 = if (ctx_.pluckers.len > 0) - bun.hash(slice) + bun.hash(std.mem.sliceAsBytes(path_slice)) else @as(u64, 0); if (comptime ContextType != void and @hasDecl(std.meta.Child(ContextType), "appendMutable")) { const result = ctx.?.all_files.getOrPutAdapted(hash, Context.U64Context{}) catch unreachable; if (!result.found_existing) { - result.value_ptr.* = (try appender.appendMutable(@TypeOf(slice), slice)).ptr; + result.value_ptr.* = (try appender.appendMutable(@TypeOf(path_slice), path_slice)).ptr; } } @@ -644,13 +698,20 @@ pub const Archive = struct { lib.ARCHIVE_OK => break :possibly_retry, lib.ARCHIVE_RETRY => { if (comptime log) { - Output.prettyErrorln("[libarchive] Error extracting {s}, retry {d} / {d}", .{ pathname_, retries_remaining, 5 }); + Output.err("libarchive error", "extracting {}, retry {d} / {d}", .{ + bun.fmt.fmtOSPath(path_slice), + retries_remaining, + 5, + }); } }, else => { if (comptime log) { const archive_error = std.mem.span(lib.archive_error_string(archive)); - Output.prettyErrorln("[libarchive] Error extracting {s}: {s}", .{ pathname_, archive_error }); + Output.err("libarchive error", "extracting {}: {s}", .{ + bun.fmt.fmtOSPath(path_slice), + archive_error, + }); } return error.Fail; }, diff --git a/src/linux_c.zig b/src/linux_c.zig index 26d392128c..f42b06f441 100644 --- a/src/linux_c.zig +++ b/src/linux_c.zig @@ -576,6 +576,7 @@ pub const IFF_LOOPBACK = net_c.IFF_LOOPBACK; pub const Mode = u32; pub const E = std.os.E; +pub const S = std.os.S; pub extern "c" fn umask(Mode) Mode; diff --git a/src/napi/napi.zig b/src/napi/napi.zig index 135cb9314f..a3eb87860e 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -560,6 +560,7 @@ pub export fn napi_has_element(env: napi_env, object: napi_value, index: c_uint, return .ok; } pub extern fn napi_get_element(env: napi_env, object: napi_value, index: u32, result: *napi_value) napi_status; +pub extern fn napi_delete_element(env: napi_env, object: napi_value, index: u32, result: *napi_value) napi_status; pub extern fn napi_define_properties(env: napi_env, object: napi_value, property_count: usize, properties: [*c]const napi_property_descriptor) napi_status; pub export fn napi_is_array(_: napi_env, value: napi_value, result: *bool) napi_status { log("napi_is_array", .{}); @@ -1203,8 +1204,12 @@ pub export fn napi_get_node_version(_: napi_env, version: **const napi_node_vers } pub export fn napi_get_uv_event_loop(env: napi_env, loop: **JSC.EventLoop) napi_status { log("napi_get_uv_event_loop", .{}); - // lol - loop.* = env.bunVM().eventLoop(); + if (bun.Environment.isWindows) { + loop.* = @ptrCast(@alignCast(env.bunVM().uvLoop())); + } else { + // there is no uv event loop on posix, we use our event loop handle. + loop.* = env.bunVM().eventLoop(); + } return .ok; } pub extern fn napi_fatal_exception(env: napi_env, err: napi_value) napi_status; diff --git a/src/options.zig b/src/options.zig index dc3a9b802c..99d727c5fc 100644 --- a/src/options.zig +++ b/src/options.zig @@ -2589,6 +2589,7 @@ pub const PathTemplate = struct { pub fn format(self: PathTemplate, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { var remain = self.data; + bun.path.posixToPlatformInPlace(u8, @constCast(remain)); while (strings.indexOfChar(remain, '[')) |j| { try writer.writeAll(remain[0..j]); remain = remain[j + 1 ..]; diff --git a/src/report.zig b/src/report.zig index d5bf3ed7d2..2dfb5b0513 100644 --- a/src/report.zig +++ b/src/report.zig @@ -119,10 +119,12 @@ pub fn printMetadata() void { const analytics_platform = Platform.forOS(); + const maybe_baseline = if (Environment.baseline) " (baseline)" else ""; + crash_report_writer.print( \\ \\----- bun meta ----- - ++ "\nBun v" ++ Global.package_json_version_with_sha ++ " " ++ platform ++ " " ++ arch ++ " {s}\n" ++ + ++ "\nBun v" ++ Global.package_json_version_with_sha ++ " " ++ platform ++ " " ++ arch ++ maybe_baseline ++ " {s}\n" ++ \\{s}: {} \\ , .{ diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index e9f9e443f9..5c7411c11b 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -46,12 +46,19 @@ inline fn nqlAtIndexCaseInsensitive(comptime string_count: comptime_int, index: } const IsSeparatorFunc = fn (char: u8) bool; +const IsSeparatorFuncT = fn (comptime T: type, char: anytype) bool; const LastSeparatorFunction = fn (slice: []const u8) ?usize; +const LastSeparatorFunctionT = fn (comptime T: type, slice: anytype) ?usize; inline fn @"is .."(slice: []const u8) bool { return slice.len >= 2 and @as(u16, @bitCast(slice[0..2].*)) == comptime std.mem.readInt(u16, "..", .little); } +inline fn @"is .. with type"(comptime T: type, slice: []const T) bool { + if (comptime T == u8) return @"is .."(slice); + return slice.len >= 2 and slice[0] == '.' and slice[1] == '.'; +} + inline fn isDotSlash(slice: []const u8) bool { return @as(u16, @bitCast(slice[0..2].*)) == comptime std.mem.readInt(u16, "./", .little); } @@ -60,6 +67,23 @@ inline fn @"is ../"(slice: []const u8) bool { return strings.hasPrefixComptime(slice, "../"); } +const ParentEqual = enum { + parent, + equal, + unrelated, +}; + +pub fn isParentOrEqual(parent_: []const u8, child: []const u8) ParentEqual { + var parent = parent_; + while (parent.len > 0 and isSepAny(parent[parent.len - 1])) { + parent = parent[0 .. parent.len - 1]; + } + if (std.mem.indexOf(u8, child, parent) != 0) return .unrelated; + if (child.len == parent.len) return .equal; + if (isSepAny(child[parent.len])) return .parent; + return .unrelated; +} + pub fn getIfExistsLongestCommonPathGeneric(input: []const []const u8, comptime platform: Platform) ?[]const u8 { const separator = comptime platform.separator(); const isPathSeparator = comptime platform.getSeparatorFunc(); @@ -512,6 +536,9 @@ pub fn relativeAlloc(allocator: std.mem.Allocator, from: []const u8, to: []const // https://cs.opensource.google/go/go/+/refs/tags/go1.17.6:src/path/filepath/path_windows.go;l=57 // volumeNameLen returns length of the leading volume name on Windows. fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { + return windowsVolumeNameLenT(u8, path); +} +fn windowsVolumeNameLenT(comptime T: type, path: []const T) struct { usize, usize } { if (path.len < 2) return .{ 0, 0 }; // with drive letter const c = path[0]; @@ -522,18 +549,32 @@ fn windowsVolumeNameLen(path: []const u8) struct { usize, usize } { } // UNC if (path.len >= 5 and - Platform.windows.isSeparator(path[0]) and - Platform.windows.isSeparator(path[1]) and - !Platform.windows.isSeparator(path[2]) and + Platform.windows.isSeparatorT(T, path[0]) and + Platform.windows.isSeparatorT(T, path[1]) and + !Platform.windows.isSeparatorT(T, path[2]) and path[2] != '.') { - if (strings.indexOfAny(path[3..], "/\\")) |idx| { - // TODO: handle input "//abc//def" should be picked up as a unc path - if (path.len > idx + 4 and !Platform.windows.isSeparator(path[idx + 4])) { - if (strings.indexOfAny(path[idx + 4 ..], "/\\")) |idx2| { - return .{ idx + idx2 + 4, idx + 3 }; - } else { - return .{ path.len, idx + 3 }; + if (T == u8) { + if (strings.indexOfAny(path[3..], "/\\")) |idx| { + // TODO: handle input "//abc//def" should be picked up as a unc path + if (path.len > idx + 4 and !Platform.windows.isSeparatorT(T, path[idx + 4])) { + if (strings.indexOfAny(path[idx + 4 ..], "/\\")) |idx2| { + return .{ idx + idx2 + 4, idx + 3 }; + } else { + return .{ path.len, idx + 3 }; + } + } + } + } else { + // TODO(dylan-conway): use strings.indexOfAny instead of std + if (std.mem.indexOfAny(T, path[3..], comptime strings.literal(T, "/\\"))) |idx| { + // TODO: handle input "//abc//def" should be picked up as a unc path + if (path.len > idx + 4 and !Platform.windows.isSeparatorT(T, path[idx + 4])) { + if (std.mem.indexOfAny(T, path[idx + 4 ..], comptime strings.literal(T, "/\\"))) |idx2| { + return .{ idx + idx2 + 4, idx + 3 }; + } else { + return .{ path.len, idx + 3 }; + } } } } @@ -545,30 +586,40 @@ pub fn windowsVolumeName(path: []const u8) []const u8 { return path[0..@call(.always_inline, windowsVolumeNameLen, .{path})[0]]; } -// path.relative lets you do relative across different share drives pub fn windowsFilesystemRoot(path: []const u8) []const u8 { + return windowsFilesystemRootT(u8, path); +} + +// path.relative lets you do relative across different share drives +pub fn windowsFilesystemRootT(comptime T: type, path: []const T) []const T { if (path.len < 3) - return if (isSepAny(path[0])) path[0..1] else path[0..0]; + return if (isSepAnyT(T, path[0])) path[0..1] else path[0..0]; // with drive letter const c = path[0]; - if (path[1] == ':' and isSepAny(path[2])) { + if (path[1] == ':' and isSepAnyT(T, path[2])) { if ('a' <= c and c <= 'z' or 'A' <= c and c <= 'Z') { return path[0..3]; } } // UNC if (path.len >= 5 and - Platform.windows.isSeparator(path[0]) and - Platform.windows.isSeparator(path[1]) and - !Platform.windows.isSeparator(path[2]) and + Platform.windows.isSeparatorT(T, path[0]) and + Platform.windows.isSeparatorT(T, path[1]) and + !Platform.windows.isSeparatorT(T, path[2]) and path[2] != '.') { - if (strings.indexOfAny(path[3..], "/\\")) |idx| { - // TODO: handle input "//abc//def" should be picked up as a unc path - return path[0 .. idx + 4]; + if (comptime T == u8) { + if (strings.indexOfAny(path[3..], "/\\")) |idx| { + // TODO: handle input "//abc//def" should be picked up as a unc path + return path[0 .. idx + 4]; + } + } else { + if (std.mem.indexOfAny(T, path[3..], "/\\")) |idx| { + return path[0 .. idx + 4]; + } } } - if (isSepAny(path[0])) return path[0..1]; + if (isSepAnyT(T, path[0])) return path[0..1]; return path[0..0]; } @@ -580,24 +631,34 @@ pub fn normalizeStringGeneric( comptime allow_above_root: bool, comptime separator: u8, comptime isSeparator: anytype, - _: anytype, comptime preserve_trailing_slash: bool, ) []u8 { - const isWindows = comptime separator == std.fs.path.sep_windows; + return normalizeStringGenericT(u8, path_, buf, allow_above_root, separator, isSeparator, preserve_trailing_slash); +} +pub fn normalizeStringGenericT( + comptime T: type, + path_: []const T, + buf: []T, + comptime allow_above_root: bool, + comptime separator: T, + comptime isSeparatorT: anytype, + comptime preserve_trailing_slash: bool, +) []T { + const isWindows, const sep_str = comptime .{ separator == std.fs.path.sep_windows, &[_]u8{separator} }; if (isWindows and bun.Environment.isDebug) { // this is here to catch a potential mistake by the caller // // since it is theoretically possible to get here in release // we will not do this check in release. - std.debug.assert(!strings.startsWith(path_, ":\\")); + std.debug.assert(!strings.hasPrefixComptimeType(T, path_, comptime strings.literal(T, ":\\"))); } var buf_i: usize = 0; var dotdot: usize = 0; const volLen, const indexOfThirdUNCSlash = if (isWindows and !allow_above_root) - windowsVolumeNameLen(path_) + windowsVolumeNameLenT(T, path_) else .{ 0, 0 }; @@ -605,7 +666,7 @@ pub fn normalizeStringGeneric( if (volLen > 0) { if (path_[1] != ':') { // UNC paths - buf[0..2].* = [_]u8{ separator, separator }; + buf[0..2].* = comptime strings.literalBuf(T, sep_str ++ sep_str); @memcpy(buf[2 .. indexOfThirdUNCSlash + 1], path_[2 .. indexOfThirdUNCSlash + 1]); buf[indexOfThirdUNCSlash] = separator; @memcpy( @@ -625,7 +686,7 @@ pub fn normalizeStringGeneric( buf_i = 2; dotdot = buf_i; } - } else if (path_.len > 0 and isSeparator(path_[0])) { + } else if (path_.len > 0 and isSeparatorT(T, path_[0])) { buf[buf_i] = separator; buf_i += 1; dotdot = 1; @@ -650,7 +711,7 @@ pub fn normalizeStringGeneric( if (isWindows and (allow_above_root or volLen > 0)) { // consume leading slashes on windows - if (r < n and isSeparator(path[r])) { + if (r < n and isSeparatorT(T, path[r])) { r += 1; buf[buf_i] = separator; buf_i += 1; @@ -661,31 +722,31 @@ pub fn normalizeStringGeneric( // empty path element // or // . element - if (isSeparator(path[r])) { + if (isSeparatorT(T, path[r])) { r += 1; continue; } - if (path[r] == '.' and (r + 1 == n or isSeparator(path[r + 1]))) { + if (path[r] == '.' and (r + 1 == n or isSeparatorT(T, path[r + 1]))) { // skipping two is a windows-specific bugfix r += 1; continue; } - if (@"is .."(path[r..]) and (r + 2 == n or isSeparator(path[r + 2]))) { + if (@"is .. with type"(T, path[r..]) and (r + 2 == n or isSeparatorT(T, path[r + 2]))) { r += 2; // .. element: remove to last separator if (buf_i > dotdot) { buf_i -= 1; - while (buf_i > dotdot and !isSeparator(buf[buf_i])) { + while (buf_i > dotdot and !isSeparatorT(T, buf[buf_i])) { buf_i -= 1; } } else if (allow_above_root) { if (buf_i > buf_start) { - buf[buf_i..][0..3].* = [_]u8{ separator, '.', '.' }; + buf[buf_i..][0..3].* = comptime strings.literalBuf(T, sep_str ++ ".."); buf_i += 3; } else { - buf[buf_i..][0..2].* = [_]u8{ '.', '.' }; + buf[buf_i..][0..2].* = comptime strings.literalBuf(T, ".."); buf_i += 2; } dotdot = buf_i; @@ -696,13 +757,13 @@ pub fn normalizeStringGeneric( // real path element. // add slash if needed - if (buf_i != buf_start and !isSeparator(buf[buf_i - 1])) { + if (buf_i != buf_start and !isSeparatorT(T, buf[buf_i - 1])) { buf[buf_i] = separator; buf_i += 1; } const from = r; - while (r < n and !isSeparator(path[r])) : (r += 1) {} + while (r < n and !isSeparatorT(T, path[r])) : (r += 1) {} const count = r - from; @memcpy(buf[buf_i..][0..count], path[from..][0..count]); buf_i += count; @@ -726,7 +787,7 @@ pub fn normalizeStringGeneric( const result = buf[0..buf_i]; if (bun.Environment.allow_assert and isWindows) { - std.debug.assert(!strings.startsWith(result, "\\:\\")); + std.debug.assert(!strings.hasPrefixComptimeType(T, result, comptime strings.literal(T, "\\:\\"))); } return result; @@ -739,12 +800,20 @@ pub const Platform = enum { posix, pub fn isAbsolute(comptime platform: Platform, path: []const u8) bool { + return isAbsoluteT(platform, u8, path); + } + + pub fn isAbsoluteT(comptime platform: Platform, comptime T: type, path: []const T) bool { + if (comptime T != u8 and T != u16) @compileError("Unsupported type given to isAbsoluteT"); return switch (comptime platform) { - .auto => (comptime platform.resolve()).isAbsolute(path), + .auto => (comptime platform.resolve()).isAbsoluteT(T, path), .posix => path.len > 0 and path[0] == '/', .windows, .loose, - => std.fs.path.isAbsoluteWindows(path), + => if (T == u8) + std.fs.path.isAbsoluteWindows(path) + else + std.fs.path.isAbsoluteWindowsWTF16(path), }; } @@ -784,6 +853,21 @@ pub const Platform = enum { } } + pub fn getSeparatorFuncT(comptime _platform: Platform) IsSeparatorFuncT { + switch (comptime _platform.resolve()) { + .auto => comptime unreachable, + .loose => { + return isSepAnyT; + }, + .windows => { + return isSepAnyT; + }, + .posix => { + return isSepPosixT; + }, + } + } + pub fn getLastSeparatorFunc(comptime _platform: Platform) LastSeparatorFunction { switch (comptime _platform.resolve()) { .auto => comptime unreachable, @@ -799,17 +883,36 @@ pub const Platform = enum { } } - pub inline fn isSeparator(comptime _platform: Platform, char: u8) bool { + pub fn getLastSeparatorFuncT(comptime _platform: Platform) LastSeparatorFunctionT { switch (comptime _platform.resolve()) { .auto => comptime unreachable, .loose => { - return isSepAny(char); + return lastIndexOfSeparatorLooseT; }, .windows => { - return isSepAny(char); + return lastIndexOfSeparatorWindowsT; }, .posix => { - return isSepPosix(char); + return lastIndexOfSeparatorPosixT; + }, + } + } + + pub inline fn isSeparator(comptime _platform: Platform, char: u8) bool { + return isSeparatorT(_platform, u8, char); + } + + pub inline fn isSeparatorT(comptime _platform: Platform, comptime T: type, char: T) bool { + switch (comptime _platform.resolve()) { + .auto => comptime unreachable, + .loose => { + return isSepAnyT(T, char); + }, + .windows => { + return isSepAnyT(T, char); + }, + .posix => { + return isSepPosixT(T, char); }, } } @@ -883,35 +986,57 @@ pub fn normalizeString(str: []const u8, comptime allow_above_root: bool, comptim } pub fn normalizeBuf(str: []const u8, buf: []u8, comptime _platform: Platform) []u8 { + return normalizeBufT(u8, str, buf, _platform); +} + +pub fn normalizeBufT(comptime T: type, str: []const T, buf: []T, comptime _platform: Platform) []T { if (str.len == 0) { buf[0] = '.'; return buf[0..1]; } - const is_absolute = _platform.isAbsolute(str); + const is_absolute = _platform.isAbsoluteT(T, str); - const trailing_separator = _platform.getLastSeparatorFunc()(str) == str.len - 1; + const trailing_separator = _platform.getLastSeparatorFuncT()(T, str) == str.len - 1; if (is_absolute and trailing_separator) - return normalizeStringBuf(str, buf, true, _platform, true); + return normalizeStringBufT(T, str, buf, true, _platform, true); if (is_absolute and !trailing_separator) - return normalizeStringBuf(str, buf, true, _platform, false); + return normalizeStringBufT(T, str, buf, true, _platform, false); if (!is_absolute and !trailing_separator) - return normalizeStringBuf(str, buf, false, _platform, false); + return normalizeStringBufT(T, str, buf, false, _platform, false); - return normalizeStringBuf(str, buf, false, _platform, true); + return normalizeStringBufT(T, str, buf, false, _platform, true); } -pub fn normalizeStringBuf(str: []const u8, buf: []u8, comptime allow_above_root: bool, comptime _platform: Platform, comptime preserve_trailing_slash: anytype) []u8 { +pub fn normalizeStringBuf( + str: []const u8, + buf: []u8, + comptime allow_above_root: bool, + comptime _platform: Platform, + comptime preserve_trailing_slash: anytype, +) []u8 { + return normalizeStringBufT(u8, str, buf, allow_above_root, _platform, preserve_trailing_slash); +} + +pub fn normalizeStringBufT( + comptime T: type, + str: []const T, + buf: []T, + comptime allow_above_root: bool, + comptime _platform: Platform, + comptime preserve_trailing_slash: anytype, +) []T { const platform = comptime _platform.resolve(); switch (comptime platform) { .auto => @compileError("unreachable"), .windows => { - return normalizeStringWindows( + return normalizeStringWindowsT( + T, str, buf, allow_above_root, @@ -919,7 +1044,8 @@ pub fn normalizeStringBuf(str: []const u8, buf: []u8, comptime allow_above_root: ); }, .posix => { - return normalizeStringLooseBuf( + return normalizeStringLooseBufT( + T, str, buf, allow_above_root, @@ -928,7 +1054,8 @@ pub fn normalizeStringBuf(str: []const u8, buf: []u8, comptime allow_above_root: }, .loose => { - return normalizeStringLooseBuf( + return normalizeStringLooseBufT( + T, str, buf, allow_above_root, @@ -1243,7 +1370,7 @@ fn _joinAbsStringBufWindows( // skip over volume name const volume = part[0..windowsVolumeNameLen(part)[0]]; - if (volume.len > 0 and !strings.eql(volume, root)) + if (volume.len > 0 and !strings.eqlLong(volume, root, true)) continue; const part_without_vol = part[volume.len..]; @@ -1273,23 +1400,48 @@ fn _joinAbsStringBufWindows( } pub fn isSepPosix(char: u8) bool { + return isSepPosixT(u8, char); +} + +pub fn isSepPosixT(comptime T: type, char: anytype) bool { + if (comptime @TypeOf(char) != T) @compileError("Incorrect type passed to isSepPosixT"); return char == std.fs.path.sep_posix; } pub fn isSepWin32(char: u8) bool { + return isSepWin32T(u8, char); +} + +pub fn isSepWin32T(comptime T: type, char: anytype) bool { + if (comptime @TypeOf(char) != T) @compileError("Incorrect type passed to isSepWin32T"); return char == std.fs.path.sep_windows; } pub fn isSepAny(char: u8) bool { - return @call(.always_inline, isSepPosix, .{char}) or @call(.always_inline, isSepWin32, .{char}); + return isSepAnyT(u8, char); +} + +pub fn isSepAnyT(comptime T: type, char: anytype) bool { + if (comptime @TypeOf(char) != T) @compileError("Incorrect type passed to isSepAnyT"); + return @call(.always_inline, isSepPosixT, .{ T, char }) or @call(.always_inline, isSepWin32T, .{ T, char }); } pub fn lastIndexOfSeparatorWindows(slice: []const u8) ?usize { - return std.mem.lastIndexOfAny(u8, slice, "\\/"); + return lastIndexOfSeparatorWindowsT(u8, slice); +} + +pub fn lastIndexOfSeparatorWindowsT(comptime T: type, slice: anytype) ?usize { + if (comptime std.meta.Child(@TypeOf(slice)) != T) @compileError("Invalid type passed to lastIndexOfSeparatorWindowsT"); + return std.mem.lastIndexOfAny(T, slice, comptime strings.literal(T, "\\/")); } pub fn lastIndexOfSeparatorPosix(slice: []const u8) ?usize { - return std.mem.lastIndexOfScalar(u8, slice, std.fs.path.sep_posix); + return lastIndexOfSeparatorPosixT(u8, slice); +} + +pub fn lastIndexOfSeparatorPosixT(comptime T: type, slice: anytype) ?usize { + if (comptime std.meta.Child(@TypeOf(slice)) != T) @compileError("Invalid type passed to lastIndexOfSeparatorPosixT"); + return std.mem.lastIndexOfScalar(T, slice, std.fs.path.sep_posix); } pub fn lastIndexOfNonSeparatorPosix(slice: []const u8) ?u32 { @@ -1304,7 +1456,12 @@ pub fn lastIndexOfNonSeparatorPosix(slice: []const u8) ?u32 { } pub fn lastIndexOfSeparatorLoose(slice: []const u8) ?usize { - return lastIndexOfSep(slice); + return lastIndexOfSeparatorLooseT(u8, slice); +} + +pub fn lastIndexOfSeparatorLooseT(comptime T: type, slice: anytype) ?usize { + if (comptime std.meta.Child(@TypeOf(slice)) != T) @compileError("Invalid type passed to lastIndexOfSeparatorLooseT"); + return lastIndexOfSepT(T, slice); } pub fn normalizeStringLooseBuf( @@ -1313,13 +1470,23 @@ pub fn normalizeStringLooseBuf( comptime allow_above_root: bool, comptime preserve_trailing_slash: bool, ) []u8 { - return normalizeStringGeneric( + return normalizeStringLooseBufT(u8, str, buf, allow_above_root, preserve_trailing_slash); +} + +pub fn normalizeStringLooseBufT( + comptime T: type, + str: []const T, + buf: []T, + comptime allow_above_root: bool, + comptime preserve_trailing_slash: bool, +) []T { + return normalizeStringGenericT( + T, str, buf, allow_above_root, std.fs.path.sep_posix, - isSepAny, - lastIndexOfSeparatorLoose, + isSepAnyT, preserve_trailing_slash, ); } @@ -1330,13 +1497,23 @@ pub fn normalizeStringWindows( comptime allow_above_root: bool, comptime preserve_trailing_slash: bool, ) []u8 { - return normalizeStringGeneric( + return normalizeStringWindowsT(u8, str, buf, allow_above_root, preserve_trailing_slash); +} + +pub fn normalizeStringWindowsT( + comptime T: type, + str: []const T, + buf: []T, + comptime allow_above_root: bool, + comptime preserve_trailing_slash: bool, +) []T { + return normalizeStringGenericT( + T, str, buf, allow_above_root, std.fs.path.sep_windows, - isSepAny, - lastIndexOfSeparatorWindows, + isSepAnyT, preserve_trailing_slash, ); } @@ -1363,16 +1540,14 @@ pub fn normalizeStringNode( buf_, true, comptime platform.resolve().separator(), - comptime platform.getSeparatorFunc(), - comptime platform.getLastSeparatorFunc(), + comptime platform.getSeparatorFuncT(), false, ) else normalizeStringGeneric( str, buf_, false, comptime platform.resolve().separator(), - comptime platform.getSeparatorFunc(), - comptime platform.getLastSeparatorFunc(), + comptime platform.getSeparatorFuncT(), false, ); @@ -1760,12 +1935,17 @@ pub fn basename(path: []const u8) []const u8 { return path[start_index + 1 .. end_index]; } + pub fn lastIndexOfSep(path: []const u8) ?usize { + return lastIndexOfSepT(u8, path); +} + +pub fn lastIndexOfSepT(comptime T: type, path: []const T) ?usize { if (comptime !bun.Environment.isWindows) { - return strings.lastIndexOfChar(path, '/'); + return strings.lastIndexOfCharT(T, path, '/'); } - return std.mem.lastIndexOfAny(u8, path, "/\\"); + return std.mem.lastIndexOfAny(T, path, "/\\"); } pub fn nextDirname(path_: []const u8) ?[]const u8 { @@ -1943,3 +2123,19 @@ export fn ResolvePath__joinAbsStringBufCurrentPlatformBunString( return bun.String.createUTF8(out_slice); } + +pub fn platformToPosixInPlace(comptime T: type, path_buffer: []T) void { + if (std.fs.path.sep == '/') return; + var idx: usize = 0; + while (std.mem.indexOfScalarPos(T, path_buffer, idx, std.fs.path.sep)) |index| : (idx = index) { + path_buffer[index] = '/'; + } +} + +pub fn posixToPlatformInPlace(comptime T: type, path_buffer: []T) void { + if (std.fs.path.sep == '/') return; + var idx: usize = 0; + while (std.mem.indexOfScalarPos(T, path_buffer, idx, '/')) |index| : (idx = index) { + path_buffer[index] = std.fs.path.sep; + } +} diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 9fce946cc1..b71a098bdb 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -1023,6 +1023,7 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { defer file.close(); break :src try file.reader().readAllAlloc(arena.allocator(), std.math.maxInt(u32)); }; + defer arena.deinit(); const jsobjs: []JSValue = &[_]JSValue{}; var out_parser: ?bun.shell.Parser = null; @@ -1066,6 +1067,52 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { mini.tick(&is_done, @as(fn (*anyopaque) bool, IsDone.isDone)); } + pub fn initAndRunFromSource(mini: *JSC.MiniEventLoop, path_for_errors: []const u8, src: []const u8) !void { + var arena = bun.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + + const jsobjs: []JSValue = &[_]JSValue{}; + var out_parser: ?bun.shell.Parser = null; + var out_lex_result: ?bun.shell.LexResult = null; + const script = ThisInterpreter.parse(&arena, src, jsobjs, &out_parser, &out_lex_result) catch |err| { + if (err == bun.shell.ParseError.Lex) { + std.debug.assert(out_lex_result != null); + const str = out_lex_result.?.combineErrors(arena.allocator()); + bun.Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ path_for_errors, str }); + bun.Global.exit(1); + } + + if (out_parser) |*p| { + const errstr = p.combineErrors(); + bun.Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ path_for_errors, errstr }); + bun.Global.exit(1); + } + + return err; + }; + const script_heap = try arena.allocator().create(ast.Script); + script_heap.* = script; + var interp = switch (ThisInterpreter.init(mini, bun.default_allocator, &arena, script_heap, jsobjs)) { + .err => |e| { + GlobalHandle.init(mini).actuallyThrow(e); + return; + }, + .result => |i| i, + }; + const IsDone = struct { + done: bool = false, + + fn isDone(this: *anyopaque) bool { + const asdlfk = bun.cast(*const @This(), this); + return asdlfk.done; + } + }; + var is_done: IsDone = .{}; + interp.done = &is_done.done; + try interp.run(); + mini.tick(&is_done, @as(fn (*anyopaque) bool, IsDone.isDone)); + } + pub fn run(this: *ThisInterpreter) !void { var root = Script.init(this, &this.root_shell, this.script, Script.ParentPtr.init(this), this.root_shell.io); this.started.store(true, .SeqCst); diff --git a/src/string.zig b/src/string.zig index c70920715e..2a1bbf5539 100644 --- a/src/string.zig +++ b/src/string.zig @@ -1270,7 +1270,7 @@ pub const SliceWithUnderlyingString = struct { } if (this.utf8.allocator.get()) |_| { - if (bun.strings.toUTF16Alloc(bun.default_allocator, this.utf8.slice(), false) catch null) |utf16| { + if (bun.strings.toUTF16Alloc(bun.default_allocator, this.utf8.slice(), false, false) catch null) |utf16| { this.utf8.deinit(); this.utf8 = .{}; return JSC.ZigString.toExternalU16(utf16.ptr, utf16.len, globalObject); diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 7b7e8bf4cf..47ad783768 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -40,18 +40,51 @@ pub inline fn w(comptime str: []const u8) [:0]const u16 { } pub fn toUTF16Literal(comptime str: []const u8) []const u16 { - return comptime brk: { - comptime var output: [str.len]u16 = undefined; + return comptime literal(u16, str); +} - for (str, 0..) |c, i| { - output[i] = c; - } +pub inline fn literal(comptime T: type, comptime str: string) []const T { + if (!@inComptime()) @compileError("strings.literal() should be called in a comptime context"); + comptime var output: [str.len]T = undefined; - const Static = struct { - pub const literal: []const u16 = output[0..]; - }; - break :brk Static.literal; + for (str, 0..) |c, i| { + // TODO(dylan-conway): should we check for non-ascii characters like JSC does with operator""_s + output[i] = c; + } + + const Static = struct { + pub const literal: []const T = output[0..]; }; + return Static.literal; +} + +pub inline fn literalBuf(comptime T: type, comptime str: string) [str.len]T { + if (!@inComptime()) @compileError("strings.literalBuf() should be called in a comptime context"); + comptime var output: [str.len]T = undefined; + + for (str, 0..) |c, i| { + // TODO(dylan-conway): should we check for non-ascii characters like JSC does with operator""_s + output[i] = c; + } + + const Static = struct { + pub const literal: [str.len]T = output; + }; + return Static.literal; +} + +pub inline fn toUTF16LiteralZ(comptime str: []const u8) [:0]const u16 { + comptime var output: [str.len + 1]u16 = undefined; + + for (str, 0..) |c, i| { + output[i] = c; + } + output[str.len] = 0; + + const Static = struct { + pub const literal: [:0]const u16 = output[0..str.len :0]; + }; + return Static.literal; } pub const OptionalUsize = std.meta.Int(.unsigned, @bitSizeOf(usize) - 1); @@ -211,8 +244,12 @@ pub fn indexOfSigned(self: string, str: string) i32 { return @as(i32, @intCast(i)); } -pub inline fn lastIndexOfChar(self: string, char: u8) ?usize { - return std.mem.lastIndexOfScalar(u8, self, char); +pub inline fn lastIndexOfChar(self: []const u8, char: u8) ?usize { + return lastIndexOfCharT(u8, self, char); +} + +pub inline fn lastIndexOfCharT(comptime T: type, self: []const T, char: T) ?usize { + return std.mem.lastIndexOfScalar(T, self, char); } pub inline fn lastIndexOf(self: string, str: string) ?usize { @@ -641,6 +678,14 @@ pub fn startsWith(self: string, str: string) bool { return eqlLong(self[0..str.len], str, false); } +pub fn startsWithGeneric(comptime T: type, self: []const T, str: []const T) bool { + if (str.len > self.len) { + return false; + } + + return eqlLong(bun.reinterpretSlice(u8, self[0..str.len]), str, false); +} + pub inline fn endsWith(self: string, str: string) bool { return str.len == 0 or @call(.always_inline, std.mem.endsWith, .{ u8, self, str }); } @@ -832,8 +877,16 @@ pub fn hasPrefixComptimeUTF16(self: []const u16, comptime alt: []const u8) bool return self.len >= alt.len and eqlComptimeCheckLenWithType(u16, self[0..alt.len], comptime toUTF16Literal(alt), false); } -pub fn hasPrefixComptimeType(comptime T: type, self: []const T, comptime alt: []const T) bool { - return self.len >= alt.len and eqlComptimeCheckLenWithType(u16, self[0..alt.len], alt, false); +pub fn hasPrefixComptimeType(comptime T: type, self: []const T, comptime alt: anytype) bool { + const rhs = comptime switch (T) { + u8 => alt, + u16 => switch (std.meta.Child(@TypeOf(alt))) { + u16 => alt, + else => w(alt), + }, + else => @compileError("Unsupported type given to hasPrefixComptimeType"), + }; + return self.len >= alt.len and eqlComptimeCheckLenWithType(T, self[0..rhs.len], rhs, false); } pub fn hasSuffixComptime(self: string, comptime alt: anytype) bool { @@ -1316,7 +1369,7 @@ pub fn withoutUTF8BOM(bytes: []const u8) []const u8 { /// Convert a UTF-8 string to a UTF-16 string IF there are any non-ascii characters /// If there are no non-ascii characters, this returns null /// This is intended to be used for strings that go to JavaScript -pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool) !?[]u16 { +pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime sentinel: bool) !if (sentinel) ?[:0]u16 else ?[]u16 { if (strings.firstNonASCII(bytes)) |i| { const output_: ?std.ArrayList(u16) = if (comptime bun.FeatureFlags.use_simdutf) simd: { const trimmed = bun.simdutf.trim.utf8(bytes); @@ -1329,11 +1382,15 @@ pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fa if (out_length == 0) break :simd null; - var out = try allocator.alloc(u16, out_length); + var out = try allocator.alloc(u16, out_length + if (sentinel) 1 else 0); log("toUTF16 {d} UTF8 -> {d} UTF16", .{ bytes.len, out_length }); const res = bun.simdutf.convert.utf8.to.utf16.with_errors.le(trimmed, out); if (res.status == .success) { + if (comptime sentinel) { + out[out_length] = 0; + return out[0 .. out_length + 1 :0]; + } return out; } @@ -1429,13 +1486,33 @@ pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fa strings.copyU8IntoU16(output.items[output.items.len - remaining.len ..], remaining); } + if (comptime sentinel) { + output.items[output.items.len] = 0; + return output.items[0 .. output.items.len + 1 :0]; + } + return output.items; } return null; } -pub fn toUTF16AllocNoTrim(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool) !?[]u16 { +// this one does the thing it's named after +pub fn toUTF16AllocForReal(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime sentinel: bool) !if (sentinel) [:0]u16 else []u16 { + return (try toUTF16Alloc(allocator, bytes, fail_if_invalid, sentinel)) orelse { + const output = try allocator.alloc(u16, bytes.len + if (sentinel) 1 else 0); + bun.strings.copyU8IntoU16(output, bytes); + + if (comptime sentinel) { + output[bytes.len] = 0; + return output[0..bytes.len :0]; + } + + return output; + }; +} + +pub fn toUTF16AllocNoTrim(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime _: bool) !?[]u16 { if (strings.firstNonASCII(bytes)) |i| { const output_: ?std.ArrayList(u16) = if (comptime bun.FeatureFlags.use_simdutf) simd: { const out_length = bun.simdutf.length.utf16.from.utf8(bytes); @@ -1610,10 +1687,22 @@ pub fn utf16Codepoint(comptime Type: type, input: Type) UTF16Replacement { } } -fn windowsPathIsPosixAbsolute(utf8: []const u8) bool { - if (utf8.len == 0) return false; - if (!charIsAnySlash(utf8[0])) return false; - if (utf8.len > 1 and charIsAnySlash(utf8[1])) return false; +/// '/hello' -> true +/// '\hello' -> true +/// 'C:/hello' -> false +/// '\??\C:\hello' -> false +fn windowsPathIsPosixAbsolute(comptime T: type, chars: []const T) bool { + if (chars.len == 0) return false; + if (!(chars[0] == '/' or chars[0] == '\\')) return false; + if (chars.len > 1 and + (chars[1] == '/' or chars[1] == '\\')) return false; + if (chars.len > 2 and + chars[2] == ':') return false; + if (chars.len > 4 and + chars[1] == '?' and + chars[2] == '?' and + (chars[3] == '/' or chars[3] == '\\')) + return windowsPathIsPosixAbsolute(T, chars[4..]); return true; } @@ -1641,12 +1730,21 @@ pub fn addNTPathPrefix(wbuf: []u16, utf16: []const u16) [:0]const u16 { return wbuf[0 .. utf16.len + bun.windows.nt_object_prefix.len :0]; } +pub fn addNTPathPrefixIfNeeded(wbuf: []u16, utf16: []const u16) [:0]const u16 { + if (hasPrefixComptimeType(u16, utf16, bun.windows.nt_object_prefix)) { + @memcpy(wbuf[0..utf16.len], utf16); + wbuf[utf16.len] = 0; + return wbuf[0..utf16.len :0]; + } + return addNTPathPrefix(wbuf, utf16); +} + // These are the same because they don't have rules like needing a trailing slash pub const toNTDir = toNTPath; pub fn toExtendedPathNormalized(wbuf: []u16, utf8: []const u8) [:0]const u16 { std.debug.assert(wbuf.len > 4); - wbuf[0..4].* = [_]u16{ '\\', '\\', '?', '\\' }; + wbuf[0..4].* = bun.windows.nt_maxpath_prefix; return wbuf[0 .. toWPathNormalized(wbuf[4..], utf8).len + 4 :0]; } @@ -1705,11 +1803,16 @@ pub fn toWDirPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { return toWPathMaybeDir(wbuf, utf8, true); } -pub fn assertIsValidWindowsPath(utf8: []const u8) void { +pub fn assertIsValidWindowsPath(comptime T: type, path: []const T) void { if (Environment.allow_assert and Environment.isWindows) { - if (startsWith(utf8, ":/")) { + if (windowsPathIsPosixAbsolute(T, path)) { + std.debug.panic("Do not pass posix paths to windows APIs, was given '{s}' (missing a root like 'C:\\', see PosixToWinNormalizer for why this is an assertion)", .{ + if (T == u8) path else std.unicode.fmtUtf16le(path), + }); + } + if (hasPrefixComptimeType(T, path, ":/")) { std.debug.panic("Path passed to windows API '{s}' is almost certainly invalid. Where did the drive letter go?", .{ - utf8, + if (T == u8) path else std.unicode.fmtUtf16le(path), }); } } @@ -1718,8 +1821,6 @@ pub fn assertIsValidWindowsPath(utf8: []const u8) void { pub fn toWPathMaybeDir(wbuf: []u16, utf8: []const u8, comptime add_trailing_lash: bool) [:0]const u16 { std.debug.assert(wbuf.len > 0); - assertIsValidWindowsPath(utf8); - var result = bun.simdutf.convert.utf8.to.utf16.with_errors.le( utf8, wbuf[0..wbuf.len -| (1 + @as(usize, @intFromBool(add_trailing_lash)))], @@ -5256,34 +5357,38 @@ pub fn concatIfNeeded( std.debug.assert(remain.len == 0); } +/// This will simply ignore invalid UTF-8 and just do it pub fn convertUTF8toUTF16InBuffer( buf: []u16, input: []const u8, ) []u16 { - if (!Environment.isWindows) @compileError("please dont't use this function on posix until fixing the todos."); - - const result = bun.simdutf.convert.utf8.to.utf16.with_errors.le(input, buf); - switch (result.status) { - .success => return buf[0..result.count], - // TODO(@paperdave): handle surrogate - .surrogate => @panic("TODO: handle surrogate in convertUTF8toUTF16"), - else => @panic("TODO: handle error in convertUTF8toUTF16"), - } + // TODO(@paperdave): implement error handling here. + // for now this will cause invalid utf-8 to be ignored and become empty. + // this is lame because of https://github.com/oven-sh/bun/issues/8197 + // it will cause process.env.whatever to be len=0 instead of the data + // but it's better than failing the run entirely + // + // the reason i didn't implement the fallback is purely because our + // code in this file is too chaotic. it is left as a TODO + if (input.len == 0) return &[_]u16{}; + const result = bun.simdutf.convert.utf8.to.utf16.le(input, buf); + return buf[0..result]; } pub fn convertUTF16toUTF8InBuffer( buf: []u8, input: []const u16, ) ![]const u8 { - if (!Environment.isWindows) @compileError("please dont't use this function on posix until fixing the todos."); - - const result = bun.simdutf.convert.utf16.to.utf8.with_errors.le(input, buf); - switch (result.status) { - .success => return buf[0..result.count], - // TODO(@paperdave): handle surrogate - .surrogate => @panic("TODO: handle surrogate in convertUTF8toUTF16"), - else => @panic("TODO: handle error in convertUTF16toUTF8InBuffer"), - } + // See above + if (input.len == 0) return &[_]u8{}; + const result = bun.simdutf.convert.utf16.to.utf8.le(input, buf); + // switch (result.status) { + // .success => return buf[0..result.count], + // // TODO(@paperdave): handle surrogate + // .surrogate => @panic("TODO: handle surrogate in convertUTF8toUTF16"), + // else => @panic("TODO: handle error in convertUTF16toUTF8InBuffer"), + // } + return buf[0..result]; } pub inline fn charIsAnySlash(char: u8) bool { @@ -5861,3 +5966,10 @@ pub inline fn indexOfScalar(input: anytype, scalar: std.meta.Child(@TypeOf(input pub fn containsScalar(input: anytype, item: std.meta.Child(@TypeOf(input))) bool { return indexOfScalar(input, item) != null; } + +pub fn withoutSuffixComptime(input: []const u8, comptime suffix: []const u8) []const u8 { + if (hasSuffixComptime(input, suffix)) { + return input[0 .. input.len - suffix.len]; + } + return input; +} diff --git a/src/symbols.dyn b/src/symbols.dyn index 63b17aa619..0be0db03ca 100644 --- a/src/symbols.dyn +++ b/src/symbols.dyn @@ -67,6 +67,7 @@ _napi_get_dataview_info; _napi_get_date_value; _napi_get_element; + _napi_delete_element; _napi_get_global; _napi_get_instance_data; _napi_get_last_error_info; diff --git a/src/symbols.txt b/src/symbols.txt index 4d39e499be..1d047fe7e2 100644 --- a/src/symbols.txt +++ b/src/symbols.txt @@ -66,6 +66,7 @@ _napi_get_cb_info _napi_get_dataview_info _napi_get_date_value _napi_get_element +_napi_delete_element _napi_get_global _napi_get_instance_data _napi_get_last_error_info diff --git a/src/sys.zig b/src/sys.zig index a7117c35b9..20f331555f 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -15,6 +15,7 @@ const C = @import("root").bun.C; const linux = os.linux; const Maybe = JSC.Maybe; const kernel32 = bun.windows; +const assertIsValidWindowsPath = bun.strings.assertIsValidWindowsPath; pub const sys_uv = if (Environment.isWindows) @import("./sys_uv.zig") else Syscall; @@ -169,6 +170,8 @@ pub fn fchmod(fd: bun.FileDescriptor, mode: bun.Mode) Maybe(void) { } pub fn chdirOSPath(destination: bun.OSPathSliceZ) Maybe(void) { + assertIsValidWindowsPath(bun.OSPathChar, destination); + if (comptime Environment.isPosix) { const rc = sys.chdir(destination); return Maybe(void).errnoSys(rc, .chdir) orelse Maybe(void).success; @@ -271,11 +274,11 @@ pub fn mkdir(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { .windows => { var wbuf: bun.WPathBuffer = undefined; - const rc = kernel32.CreateDirectoryW(bun.strings.toWPath(&wbuf, file_path).ptr, null); - return if (rc != 0) - Maybe(void).success - else - Maybe(void).errnoSys(rc, .mkdir) orelse Maybe(void).success; + return Maybe(void).errnoSysP( + kernel32.CreateDirectoryW(bun.strings.toWPath(&wbuf, file_path).ptr, null), + .mkdir, + file_path, + ) orelse Maybe(void).success; }, else => @compileError("mkdir is not implemented on this platform"), @@ -303,11 +306,13 @@ pub fn mkdirA(file_path: []const u8, flags: bun.Mode) Maybe(void) { if (comptime Environment.isWindows) { var wbuf: bun.WPathBuffer = undefined; - const rc = kernel32.CreateDirectoryW(bun.strings.toWPath(&wbuf, file_path).ptr, null); - return if (rc != 0) - Maybe(void).success - else - Maybe(void).errnoSys(rc, .mkdir) orelse Maybe(void).success; + const wpath = bun.strings.toWPath(&wbuf, file_path); + assertIsValidWindowsPath(u16, wpath); + return Maybe(void).errnoSysP( + kernel32.CreateDirectoryW(wpath.ptr, null), + .mkdir, + file_path, + ) orelse Maybe(void).success; } } @@ -315,11 +320,11 @@ pub fn mkdirOSPath(file_path: bun.OSPathSliceZ, flags: bun.Mode) Maybe(void) { return switch (Environment.os) { else => mkdir(file_path, flags), .windows => { - const rc = kernel32.CreateDirectoryW(file_path, null); - return if (rc != 0) - Maybe(void).success - else - Maybe(void).errnoSys(rc, .mkdir) orelse Maybe(void).success; + assertIsValidWindowsPath(bun.OSPathChar, file_path); + return Maybe(void).errnoSys( + kernel32.CreateDirectoryW(file_path, null), + .mkdir, + ) orelse Maybe(void).success; }, }; } @@ -417,6 +422,7 @@ pub fn openDirAtWindows( iterable: bool, no_follow: bool, ) Maybe(bun.FileDescriptor) { + assertIsValidWindowsPath(u16, path); const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | w.SYNCHRONIZE | w.FILE_TRAVERSE; const flags: u32 = if (iterable) base_flags | w.FILE_LIST_DIRECTORY else base_flags; @@ -569,6 +575,7 @@ pub fn ntCreateFile( // this path is probably already backslash normalized so we're only going to check for '.\' const path = if (bun.strings.hasPrefixComptimeUTF16(path_maybe_leading_dot, ".\\")) path_maybe_leading_dot[2..] else path_maybe_leading_dot; std.debug.assert(!bun.strings.hasPrefixComptimeUTF16(path_maybe_leading_dot, "./")); + assertIsValidWindowsPath(u16, path); const path_len_bytes = std.math.cast(u16, path.len * 2) orelse return .{ .err = .{ @@ -591,7 +598,7 @@ pub fn ntCreateFile( // file specification, provided that the floppy driver and overlying file system are already // loaded. For more information, see File Names, Paths, and Namespaces. .ObjectName = &nt_name, - .RootDirectory = if (bun.strings.hasPrefixComptimeType(u16, path, &windows.nt_object_prefix)) + .RootDirectory = if (bun.strings.hasPrefixComptimeType(u16, path, windows.nt_object_prefix)) null else if (dir == bun.invalid_fd) std.fs.cwd().fd @@ -872,7 +879,10 @@ pub fn writev(fd: bun.FileDescriptor, buffers: []std.os.iovec) Maybe(usize) { } } -pub fn pwritev(fd: bun.FileDescriptor, buffers: []const std.os.iovec_const, position: isize) Maybe(usize) { +pub fn pwritev(fd: bun.FileDescriptor, buffers: []const bun.PlatformIOVecConst, position: isize) Maybe(usize) { + if (comptime Environment.isWindows) { + return sys_uv.pwritev(fd, buffers, position); + } if (comptime Environment.isMac) { const rc = pwritev_sym(fd.cast(), buffers.ptr, @as(i32, @intCast(buffers.len)), position); if (comptime Environment.allow_assert) @@ -1401,6 +1411,7 @@ pub fn mmap( } pub fn mmapFile(path: [:0]const u8, flags: u32, wanted_size: ?usize, offset: usize) Maybe([]align(mem.page_size) u8) { + assertIsValidWindowsPath(u8, path); const fd = switch (open(path, os.O.RDWR, 0)) { .result => |fd| fd, .err => |err| return .{ .err = err }, @@ -1656,6 +1667,7 @@ pub fn existsOSPath(path: bun.OSPathSliceZ) bool { } if (comptime Environment.isWindows) { + assertIsValidWindowsPath(bun.OSPathChar, path); const result = kernel32.GetFileAttributesW(path.ptr); if (Environment.isDebug) { log("GetFileAttributesW({}) = {d}", .{ bun.fmt.fmtUTF16(path), result }); @@ -1674,6 +1686,7 @@ pub fn exists(path: []const u8) bool { if (comptime Environment.isWindows) { var wbuf: bun.WPathBuffer = undefined; const path_to_use = bun.strings.toWPath(&wbuf, path); + assertIsValidWindowsPath(u16, path_to_use); return kernel32.GetFileAttributesW(path_to_use.ptr) != windows.INVALID_FILE_ATTRIBUTES; } diff --git a/src/sys_uv.zig b/src/sys_uv.zig index e12139b46b..49ee7471c0 100644 --- a/src/sys_uv.zig +++ b/src/sys_uv.zig @@ -15,6 +15,7 @@ const E = C.E; const linux = os.linux; const Maybe = JSC.Maybe; const kernel32 = bun.windows; +const assertIsValidWindowsPath = bun.strings.assertIsValidWindowsPath; const uv = bun.windows.libuv; @@ -38,6 +39,8 @@ pub const mkdirOSPath = bun.sys.mkdirOSPath; // Note: `req = undefined; req.deinit()` has a saftey-check in a debug build pub fn open(file_path: [:0]const u8, c_flags: bun.Mode, _perm: bun.Mode) Maybe(bun.FileDescriptor) { + assertIsValidWindowsPath(u8, file_path); + var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); @@ -58,6 +61,7 @@ pub fn open(file_path: [:0]const u8, c_flags: bun.Mode, _perm: bun.Mode) Maybe(b } pub fn mkdir(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_mkdir(uv.Loop.get(), &req, file_path.ptr, flags, null); @@ -70,6 +74,7 @@ pub fn mkdir(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { } pub fn chmod(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_chmod(uv.Loop.get(), &req, file_path.ptr, flags, null); @@ -95,6 +100,7 @@ pub fn fchmod(fd: FileDescriptor, flags: bun.Mode) Maybe(void) { } pub fn chown(file_path: [:0]const u8, uid: uv.uv_uid_t, gid: uv.uv_uid_t) Maybe(void) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_chown(uv.Loop.get(), &req, file_path.ptr, uid, gid, null); @@ -121,6 +127,7 @@ pub fn fchown(fd: FileDescriptor, uid: uv.uv_uid_t, gid: uv.uv_uid_t) Maybe(void } pub fn access(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_access(uv.Loop.get(), &req, file_path.ptr, flags, null); @@ -133,6 +140,7 @@ pub fn access(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { } pub fn rmdir(file_path: [:0]const u8) Maybe(void) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_rmdir(uv.Loop.get(), &req, file_path.ptr, null); @@ -145,6 +153,7 @@ pub fn rmdir(file_path: [:0]const u8) Maybe(void) { } pub fn unlink(file_path: [:0]const u8) Maybe(void) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_unlink(uv.Loop.get(), &req, file_path.ptr, null); @@ -157,6 +166,7 @@ pub fn unlink(file_path: [:0]const u8) Maybe(void) { } pub fn readlink(file_path: [:0]const u8, buf: []u8) Maybe(usize) { + assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); // Edge cases: http://docs.libuv.org/en/v1.x/fs.html#c.uv_fs_realpath @@ -180,6 +190,8 @@ pub fn readlink(file_path: [:0]const u8, buf: []u8) Maybe(usize) { } pub fn rename(from: [:0]const u8, to: [:0]const u8) Maybe(void) { + assertIsValidWindowsPath(u8, from); + assertIsValidWindowsPath(u8, to); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_rename(uv.Loop.get(), &req, from.ptr, to.ptr, null); @@ -192,6 +204,8 @@ pub fn rename(from: [:0]const u8, to: [:0]const u8) Maybe(void) { } pub fn link(from: [:0]const u8, to: [:0]const u8) Maybe(void) { + assertIsValidWindowsPath(u8, from); + assertIsValidWindowsPath(u8, to); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_link(uv.Loop.get(), &req, from.ptr, to.ptr, null); @@ -204,6 +218,8 @@ pub fn link(from: [:0]const u8, to: [:0]const u8) Maybe(void) { } pub fn symlinkUV(from: [:0]const u8, to: [:0]const u8, flags: c_int) Maybe(void) { + assertIsValidWindowsPath(u8, from); + assertIsValidWindowsPath(u8, to); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_symlink(uv.Loop.get(), &req, from.ptr, to.ptr, flags, null); @@ -268,6 +284,7 @@ pub fn fsync(fd: FileDescriptor) Maybe(void) { } pub fn stat(path: [:0]const u8) Maybe(bun.Stat) { + assertIsValidWindowsPath(u8, path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_stat(uv.Loop.get(), &req, path.ptr, null); @@ -280,6 +297,7 @@ pub fn stat(path: [:0]const u8) Maybe(bun.Stat) { } pub fn lstat(path: [:0]const u8) Maybe(bun.Stat) { + assertIsValidWindowsPath(u8, path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); const rc = uv.uv_fs_lstat(uv.Loop.get(), &req, path.ptr, null); @@ -333,7 +351,7 @@ pub fn preadv(fd: FileDescriptor, bufs: []const bun.PlatformIOVec, position: i64 } } -pub fn pwritev(fd: FileDescriptor, bufs: []const bun.PlatformIOVec, position: i64) Maybe(usize) { +pub fn pwritev(fd: FileDescriptor, bufs: []const bun.PlatformIOVecConst, position: i64) Maybe(usize) { const uv_fd = bun.uvfdcast(fd); comptime std.debug.assert(bun.PlatformIOVec == uv.uv_buf_t); diff --git a/src/tmp.zig b/src/tmp.zig index ce30a8b058..2efd8c82ec 100644 --- a/src/tmp.zig +++ b/src/tmp.zig @@ -28,7 +28,7 @@ pub const Tmpfile = struct { if (comptime allow_tmpfile) { switch (bun.sys.openat(destination_dir, ".", O.WRONLY | O.TMPFILE | O.CLOEXEC, perm)) { .result => |fd| { - tmpfile.fd = fd; + tmpfile.fd = bun.toLibUVOwnedFD(fd); break :open; }, .err => |err| { @@ -43,7 +43,7 @@ pub const Tmpfile = struct { } tmpfile.fd = switch (bun.sys.openat(destination_dir, tmpfilename, O.CREAT | O.CLOEXEC | O.WRONLY, perm)) { - .result => |fd| fd, + .result => |fd| bun.toLibUVOwnedFD(fd), .err => |err| return .{ .err = err }, }; break :open; diff --git a/src/watcher.zig b/src/watcher.zig index 55d160ae9e..360a102895 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -1,4 +1,3 @@ -const Fs = @import("./fs.zig"); const std = @import("std"); const bun = @import("root").bun; const string = bun.string; @@ -6,59 +5,31 @@ const Output = bun.Output; const Global = bun.Global; const Environment = bun.Environment; const strings = bun.strings; -const MutableString = bun.MutableString; const stringZ = bun.stringZ; -const StoredFileDescriptorType = bun.StoredFileDescriptorType; const FeatureFlags = bun.FeatureFlags; -const default_allocator = bun.default_allocator; -const C = bun.C; -const c = std.c; const options = @import("./options.zig"); -const IndexType = @import("./allocators.zig").IndexType; - -const os = std.os; const Mutex = @import("./lock.zig").Lock; const Futex = @import("./futex.zig"); pub const WatchItemIndex = u16; -const NoWatchItem: WatchItemIndex = std.math.maxInt(WatchItemIndex); const PackageJSON = @import("./resolver/package_json.zig").PackageJSON; +const log = bun.Output.scoped(.watcher, false); + const WATCHER_MAX_LIST = 8096; -pub const INotify = struct { - pub const IN_CLOEXEC = std.os.O.CLOEXEC; - pub const IN_NONBLOCK = std.os.O.NONBLOCK; +const INotify = struct { + loaded_inotify: bool = false, + inotify_fd: EventListIndex = 0, - pub const IN_ACCESS = 0x00000001; - pub const IN_MODIFY = 0x00000002; - pub const IN_ATTRIB = 0x00000004; - pub const IN_CLOSE_WRITE = 0x00000008; - pub const IN_CLOSE_NOWRITE = 0x00000010; - pub const IN_CLOSE = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE; - pub const IN_OPEN = 0x00000020; - pub const IN_MOVED_FROM = 0x00000040; - pub const IN_MOVED_TO = 0x00000080; - pub const IN_MOVE = IN_MOVED_FROM | IN_MOVED_TO; - pub const IN_CREATE = 0x00000100; - pub const IN_DELETE = 0x00000200; - pub const IN_DELETE_SELF = 0x00000400; - pub const IN_MOVE_SELF = 0x00000800; - pub const IN_ALL_EVENTS = 0x00000fff; + eventlist: EventListBuffer = undefined, + eventlist_ptrs: [128]*const INotifyEvent = undefined, - pub const IN_UNMOUNT = 0x00002000; - pub const IN_Q_OVERFLOW = 0x00004000; - pub const IN_IGNORED = 0x00008000; - - pub const IN_ONLYDIR = 0x01000000; - pub const IN_DONT_FOLLOW = 0x02000000; - pub const IN_EXCL_UNLINK = 0x04000000; - pub const IN_MASK_ADD = 0x20000000; - - pub const IN_ISDIR = 0x40000000; - pub const IN_ONESHOT = 0x80000000; + watch_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), + coalesce_interval: isize = 100_000, pub const EventListIndex = c_int; + const EventListBuffer = [@sizeOf([128]INotifyEvent) + (128 * bun.MAX_PATH_BYTES + (128 * @alignOf(INotifyEvent)))]u8; pub const INotifyEvent = extern struct { watch_descriptor: c_int, @@ -76,62 +47,48 @@ pub const INotify = struct { return bun.sliceTo(@as([*:0]u8, @ptrFromInt(@intFromPtr(&this.name_len) + @sizeOf(u32))), 0)[0.. :0]; } }; - pub var inotify_fd: EventListIndex = 0; - pub var loaded_inotify = false; - const EventListBuffer = [@sizeOf([128]INotifyEvent) + (128 * bun.MAX_PATH_BYTES + (128 * @alignOf(INotifyEvent)))]u8; - var eventlist: EventListBuffer = undefined; - var eventlist_ptrs: [128]*const INotifyEvent = undefined; - - var watch_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0); - - const watch_file_mask = std.os.linux.IN.EXCL_UNLINK | std.os.linux.IN.MOVE_SELF | std.os.linux.IN.DELETE_SELF | std.os.linux.IN.MOVED_TO | std.os.linux.IN.MODIFY; - const watch_dir_mask = std.os.linux.IN.EXCL_UNLINK | std.os.linux.IN.DELETE | std.os.linux.IN.DELETE_SELF | std.os.linux.IN.CREATE | std.os.linux.IN.MOVE_SELF | std.os.linux.IN.ONLYDIR | std.os.linux.IN.MOVED_TO; - - pub fn watchPath(pathname: [:0]const u8) !EventListIndex { - std.debug.assert(loaded_inotify); - const old_count = watch_count.fetchAdd(1, .Release); - defer if (old_count == 0) Futex.wake(&watch_count, 10); - return std.os.inotify_add_watchZ(inotify_fd, pathname, watch_file_mask); + pub fn watchPath(this: *INotify, pathname: [:0]const u8) !EventListIndex { + std.debug.assert(this.loaded_inotify); + const old_count = this.watch_count.fetchAdd(1, .Release); + defer if (old_count == 0) Futex.wake(&this.watch_count, 10); + const watch_file_mask = std.os.linux.IN.EXCL_UNLINK | std.os.linux.IN.MOVE_SELF | std.os.linux.IN.DELETE_SELF | std.os.linux.IN.MOVED_TO | std.os.linux.IN.MODIFY; + return std.os.inotify_add_watchZ(this.inotify_fd, pathname, watch_file_mask); } - pub fn watchDir(pathname: [:0]const u8) !EventListIndex { - std.debug.assert(loaded_inotify); - const old_count = watch_count.fetchAdd(1, .Release); - defer if (old_count == 0) Futex.wake(&watch_count, 10); - return std.os.inotify_add_watchZ(inotify_fd, pathname, watch_dir_mask); + pub fn watchDir(this: *INotify, pathname: [:0]const u8) !EventListIndex { + std.debug.assert(this.loaded_inotify); + const old_count = this.watch_count.fetchAdd(1, .Release); + defer if (old_count == 0) Futex.wake(&this.watch_count, 10); + const watch_dir_mask = std.os.linux.IN.EXCL_UNLINK | std.os.linux.IN.DELETE | std.os.linux.IN.DELETE_SELF | std.os.linux.IN.CREATE | std.os.linux.IN.MOVE_SELF | std.os.linux.IN.ONLYDIR | std.os.linux.IN.MOVED_TO; + return std.os.inotify_add_watchZ(this.inotify_fd, pathname, watch_dir_mask); } - pub fn unwatch(wd: EventListIndex) void { - std.debug.assert(loaded_inotify); - _ = watch_count.fetchSub(1, .Release); - std.os.inotify_rm_watch(inotify_fd, wd); + pub fn unwatch(this: *INotify, wd: EventListIndex) void { + std.debug.assert(this.loaded_inotify); + _ = this.watch_count.fetchSub(1, .Release); + std.os.inotify_rm_watch(this.inotify_fd, wd); } - pub fn isRunning() bool { - return loaded_inotify; - } - - var coalesce_interval: isize = 100_000; - pub fn init() !void { - std.debug.assert(!loaded_inotify); - loaded_inotify = true; + pub fn init(this: *INotify, _: []const u8) !void { + std.debug.assert(!this.loaded_inotify); + this.loaded_inotify = true; if (bun.getenvZ("BUN_INOTIFY_COALESCE_INTERVAL")) |env| { - coalesce_interval = std.fmt.parseInt(isize, env, 10) catch 100_000; + this.coalesce_interval = std.fmt.parseInt(isize, env, 10) catch 100_000; } - inotify_fd = try std.os.inotify_init1(IN_CLOEXEC); + this.inotify_fd = try std.os.inotify_init1(std.os.linux.IN.CLOEXEC); } - pub fn read() ![]*const INotifyEvent { - std.debug.assert(loaded_inotify); + pub fn read(this: *INotify) ![]*const INotifyEvent { + std.debug.assert(this.loaded_inotify); restart: while (true) { - Futex.wait(&watch_count, 0, null) catch unreachable; + Futex.wait(&this.watch_count, 0, null) catch unreachable; const rc = std.os.system.read( - inotify_fd, - @as([*]u8, @ptrCast(@alignCast(&eventlist))), + this.inotify_fd, + @as([*]u8, @ptrCast(@alignCast(&this.eventlist))), @sizeOf(EventListBuffer), ); @@ -145,16 +102,16 @@ pub const INotify = struct { // we do a 0.1ms sleep to try to coalesce events better if (len < (@sizeOf(EventListBuffer) / 2)) { var fds = [_]std.os.pollfd{.{ - .fd = inotify_fd, + .fd = this.inotify_fd, .events = std.os.POLL.IN | std.os.POLL.ERR, .revents = 0, }}; - var timespec = std.os.timespec{ .tv_sec = 0, .tv_nsec = coalesce_interval }; + var timespec = std.os.timespec{ .tv_sec = 0, .tv_nsec = this.coalesce_interval }; if ((std.os.ppoll(&fds, ×pec, null) catch 0) > 0) { while (true) { const new_rc = std.os.system.read( - inotify_fd, - @as([*]u8, @ptrCast(@alignCast(&eventlist))) + len, + this.inotify_fd, + @as([*]u8, @ptrCast(@alignCast(&this.eventlist))) + len, @sizeOf(EventListBuffer) - len, ); switch (std.os.errno(new_rc)) { @@ -186,14 +143,14 @@ pub const INotify = struct { var i: u32 = 0; while (i < len) : (i += @sizeOf(INotifyEvent)) { @setRuntimeSafety(false); - const event = @as(*INotifyEvent, @ptrCast(@alignCast(eventlist[i..][0..@sizeOf(INotifyEvent)]))); + const event = @as(*INotifyEvent, @ptrCast(@alignCast(this.eventlist[i..][0..@sizeOf(INotifyEvent)]))); i += event.name_len; - eventlist_ptrs[count] = event; + this.eventlist_ptrs[count] = event; count += 1; } - return eventlist_ptrs[0..count]; + return this.eventlist_ptrs[0..count]; }, .AGAIN => continue :restart, .INVAL => return error.ShortRead, @@ -205,10 +162,10 @@ pub const INotify = struct { unreachable; } - pub fn stop() void { - if (inotify_fd != 0) { - _ = bun.sys.close(bun.toFD(inotify_fd)); - inotify_fd = 0; + pub fn stop(this: *INotify) void { + if (this.inotify_fd != 0) { + _ = bun.sys.close(bun.toFD(this.inotify_fd)); + this.inotify_fd = 0; } } }; @@ -217,69 +174,205 @@ const DarwinWatcher = struct { pub const EventListIndex = u32; const KEvent = std.c.Kevent; + // Internal - pub var changelist: [128]KEvent = undefined; + changelist: [128]KEvent = undefined, // Everything being watched - pub var eventlist: [WATCHER_MAX_LIST]KEvent = undefined; - pub var eventlist_index: EventListIndex = 0; + eventlist: [WATCHER_MAX_LIST]KEvent = undefined, + eventlist_index: EventListIndex = 0, - pub var fd: i32 = 0; + fd: i32 = 0, - pub fn init() !void { - std.debug.assert(fd == 0); - - fd = try std.os.kqueue(); - if (fd == 0) return error.KQueueError; + pub fn init(this: *DarwinWatcher, _: []const u8) !void { + this.fd = try std.os.kqueue(); + if (this.fd == 0) return error.KQueueError; } - pub fn isRunning() bool { - return fd != 0; - } - - pub fn stop() void { - if (fd != 0) { - _ = bun.sys.close(fd); + pub fn stop(this: *DarwinWatcher) void { + if (this.fd != 0) { + _ = bun.sys.close(this.fd); } - - fd = 0; + this.fd = 0; } }; -pub const Placeholder = struct { - pub const EventListIndex = u32; +const WindowsWatcher = struct { + mutex: Mutex = Mutex.init(), + iocp: w.HANDLE = undefined, + watcher: DirWatcher = undefined, - pub var eventlist: [WATCHER_MAX_LIST]EventListIndex = undefined; - pub var eventlist_index: EventListIndex = 0; + const w = std.os.windows; + pub const EventListIndex = c_int; - pub fn isRunning() bool { - return true; + const Error = error{ + IocpFailed, + ReadDirectoryChangesFailed, + CreateFileFailed, + InvalidPath, + }; + + const Action = enum(w.DWORD) { + Added = w.FILE_ACTION_ADDED, + Removed = w.FILE_ACTION_REMOVED, + Modified = w.FILE_ACTION_MODIFIED, + RenamedOld = w.FILE_ACTION_RENAMED_OLD_NAME, + RenamedNew = w.FILE_ACTION_RENAMED_NEW_NAME, + }; + + const FileEvent = struct { + action: Action, + filename: []u16 = undefined, + }; + + const DirWatcher = struct { + // must be initialized to zero (even though it's never read or written in our code), + // otherwise ReadDirectoryChangesW will fail with INVALID_HANDLE + overlapped: w.OVERLAPPED = std.mem.zeroes(w.OVERLAPPED), + buf: [64 * 1024]u8 align(@alignOf(w.FILE_NOTIFY_INFORMATION)) = undefined, + dirHandle: w.HANDLE, + + // invalidates any EventIterators + fn prepare(this: *DirWatcher) Error!void { + const filter = w.FILE_NOTIFY_CHANGE_FILE_NAME | w.FILE_NOTIFY_CHANGE_DIR_NAME | w.FILE_NOTIFY_CHANGE_LAST_WRITE | w.FILE_NOTIFY_CHANGE_CREATION; + if (w.kernel32.ReadDirectoryChangesW(this.dirHandle, &this.buf, this.buf.len, 1, filter, null, &this.overlapped, null) == 0) { + const err = w.kernel32.GetLastError(); + log("failed to start watching directory: {s}", .{@tagName(err)}); + return Error.ReadDirectoryChangesFailed; + } + } + }; + + const EventIterator = struct { + watcher: *DirWatcher, + offset: usize = 0, + hasNext: bool = true, + + pub fn next(this: *EventIterator) ?FileEvent { + if (!this.hasNext) return null; + const info_size = @sizeOf(w.FILE_NOTIFY_INFORMATION); + const info: *w.FILE_NOTIFY_INFORMATION = @alignCast(@ptrCast(this.watcher.buf[this.offset..].ptr)); + const name_ptr: [*]u16 = @alignCast(@ptrCast(this.watcher.buf[this.offset + info_size ..])); + const filename: []u16 = name_ptr[0 .. info.FileNameLength / @sizeOf(u16)]; + + const action: Action = @enumFromInt(info.Action); + + if (info.NextEntryOffset == 0) { + this.hasNext = false; + } else { + this.offset += @as(usize, info.NextEntryOffset); + } + + return FileEvent{ + .action = action, + .filename = filename, + }; + } + }; + + pub fn init(this: *WindowsWatcher, root: []const u8) !void { + var pathbuf: bun.WPathBuffer = undefined; + const wpath = bun.strings.toNTPath(&pathbuf, root); + const path_len_bytes: u16 = @truncate(wpath.len * 2); + var nt_name = w.UNICODE_STRING{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(wpath.ptr), + }; + var attr = w.OBJECT_ATTRIBUTES{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = null, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + var handle: w.HANDLE = w.INVALID_HANDLE_VALUE; + var io: w.IO_STATUS_BLOCK = undefined; + const rc = w.ntdll.NtCreateFile( + &handle, + w.FILE_LIST_DIRECTORY, + &attr, + &io, + null, + 0, + w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, + w.FILE_OPEN, + w.FILE_DIRECTORY_FILE | w.FILE_OPEN_FOR_BACKUP_INTENT, + null, + 0, + ); + + if (rc != .SUCCESS) { + const err = bun.windows.Win32Error.fromNTStatus(rc); + log("failed to open directory for watching: {s}", .{@tagName(err)}); + return Error.CreateFileFailed; + } + errdefer _ = w.kernel32.CloseHandle(handle); + + this.iocp = try w.CreateIoCompletionPort(handle, null, 0, 1); + errdefer _ = w.kernel32.CloseHandle(this.iocp); + + this.watcher = .{ .dirHandle = handle }; } - pub fn init() !void {} + const Timeout = enum(w.DWORD) { + infinite = w.INFINITE, + minimal = 1, + none = 0, + }; + + // wait until new events are available + pub fn next(this: *WindowsWatcher, timeout: Timeout) !?EventIterator { + try this.watcher.prepare(); + + var nbytes: w.DWORD = 0; + var key: w.ULONG_PTR = 0; + var overlapped: ?*w.OVERLAPPED = null; + while (true) { + const rc = w.kernel32.GetQueuedCompletionStatus(this.iocp, &nbytes, &key, &overlapped, @intFromEnum(timeout)); + if (rc == 0) { + const err = w.kernel32.GetLastError(); + if (err == w.Win32Error.IMEOUT) { + return null; + } else { + log("GetQueuedCompletionStatus failed: {s}", .{@tagName(err)}); + return Error.IocpFailed; + } + } + + if (overlapped) |ptr| { + // ignore possible spurious events + if (ptr != &this.watcher.overlapped) { + continue; + } + if (nbytes == 0) { + // shutdown notification + // TODO close handles? + return Error.IocpFailed; + } + return EventIterator{ .watcher = &this.watcher }; + } else { + log("GetQueuedCompletionStatus returned no overlapped event", .{}); + return Error.IocpFailed; + } + } + } + + pub fn stop(this: *WindowsWatcher) void { + w.CloseHandle(this.watcher.dirHandle); + w.CloseHandle(this.iocp); + } }; const PlatformWatcher = if (Environment.isMac) DarwinWatcher else if (Environment.isLinux) INotify +else if (Environment.isWindows) + WindowsWatcher else - Placeholder; - -pub const WatchItem = struct { - file_path: string, - // filepath hash for quick comparison - hash: u32, - eventlist_index: PlatformWatcher.EventListIndex, - loader: options.Loader, - fd: StoredFileDescriptorType, - count: u32, - parent_hash: u32, - kind: Kind, - package_json: ?*PackageJSON, - - pub const Kind = enum { file, directory }; -}; + @compileError("Unsupported platform"); pub const WatchEvent = struct { index: WatchItemIndex, @@ -332,11 +425,21 @@ pub const WatchEvent = struct { pub fn fromINotify(this: *WatchEvent, event: INotify.INotifyEvent, index: WatchItemIndex) void { this.* = WatchEvent{ .op = Op{ - .delete = (event.mask & INotify.IN_DELETE_SELF) > 0 or (event.mask & INotify.IN_DELETE) > 0, - .metadata = false, - .rename = (event.mask & INotify.IN_MOVE_SELF) > 0, - .move_to = (event.mask & INotify.IN_MOVED_TO) > 0, - .write = (event.mask & INotify.IN_MODIFY) > 0, + .delete = (event.mask & std.os.linux.IN.DELETE_SELF) > 0 or (event.mask & std.os.linux.IN.DELETE) > 0, + .rename = (event.mask & std.os.linux.IN.MOVE_SELF) > 0, + .move_to = (event.mask & std.os.linux.IN.MOVED_TO) > 0, + .write = (event.mask & std.os.linux.IN.MODIFY) > 0, + }, + .index = index, + }; + } + + pub fn fromFileNotify(this: *WatchEvent, event: WindowsWatcher.FileEvent, index: WatchItemIndex) void { + this.* = WatchEvent{ + .op = Op{ + .delete = event.action == .Removed, + .rename = event.action == .RenamedOld, + .write = event.action == .Modified, }, .index = index, }; @@ -351,13 +454,33 @@ pub const WatchEvent = struct { }; }; -pub const Watchlist = std.MultiArrayList(WatchItem); +pub const WatchItem = struct { + file_path: string, + // filepath hash for quick comparison + hash: u32, + loader: options.Loader, + fd: bun.FileDescriptor, + count: u32, + parent_hash: u32, + kind: Kind, + package_json: ?*PackageJSON, + eventlist_index: if (Environment.isLinux) PlatformWatcher.EventListIndex else u0 = 0, + + pub const Kind = enum { file, directory }; +}; + +pub const WatchList = std.MultiArrayList(WatchItem); +pub const HashType = u32; + +pub fn getHash(filepath: string) HashType { + return @as(HashType, @truncate(bun.hash(filepath))); +} pub fn NewWatcher(comptime ContextType: type) type { return struct { const Watcher = @This(); - watchlist: Watchlist, + watchlist: WatchList, watched_count: usize = 0, mutex: Mutex, @@ -365,12 +488,10 @@ pub fn NewWatcher(comptime ContextType: type) type { // User-facing watch_events: [128]WatchEvent = undefined, - changed_filepaths: [128]?[:0]u8 = std.mem.zeroes([128]?[:0]u8), + changed_filepaths: [128]?[:0]u8 = [_]?[:0]u8{null} ** 128, - fs: *Fs.FileSystem, - // this is what kqueue knows about - fd: StoredFileDescriptorType, ctx: ContextType, + fs: *bun.fs.FileSystem, allocator: std.mem.Allocator, watchloop_handle: ?std.Thread.Id = null, cwd: string, @@ -378,42 +499,33 @@ pub fn NewWatcher(comptime ContextType: type) type { running: bool = true, close_descriptors: bool = false, - pub const HashType = u32; - pub const WatchListArray = Watchlist; + evict_list: [WATCHER_MAX_LIST]WatchItemIndex = undefined, + evict_list_i: WatchItemIndex = 0, - var evict_list: [WATCHER_MAX_LIST]WatchItemIndex = undefined; + const no_watch_item: WatchItemIndex = std.math.maxInt(WatchItemIndex); - pub fn getHash(filepath: string) HashType { - return @as(HashType, @truncate(bun.hash(filepath))); - } - - pub fn init(ctx: ContextType, fs: *Fs.FileSystem, allocator: std.mem.Allocator) !*Watcher { + pub fn init(ctx: ContextType, fs: *bun.fs.FileSystem, allocator: std.mem.Allocator) !*Watcher { const watcher = try allocator.create(Watcher); errdefer allocator.destroy(watcher); - if (!PlatformWatcher.isRunning()) { - try PlatformWatcher.init(); - } - watcher.* = Watcher{ .fs = fs, - .fd = .zero, .allocator = allocator, .watched_count = 0, .ctx = ctx, - .watchlist = Watchlist{}, + .watchlist = WatchList{}, .mutex = Mutex.init(), .cwd = fs.top_level_dir, }; + try PlatformWatcher.init(&watcher.platform, fs.top_level_dir); + return watcher; } pub fn start(this: *Watcher) !void { - if (!Environment.isWindows) { - std.debug.assert(this.watchloop_handle == null); - this.thread = try std.Thread.spawn(.{}, Watcher.watchLoop, .{this}); - } + std.debug.assert(this.watchloop_handle == null); + this.thread = try std.Thread.spawn(.{}, Watcher.watchLoop, .{this}); } pub fn deinit(this: *Watcher, close_descriptors: bool) void { @@ -440,10 +552,6 @@ pub fn NewWatcher(comptime ContextType: type) type { // This must only be called from the watcher thread pub fn watchLoop(this: *Watcher) !void { - if (Environment.isWindows) { - @compileError("watchLoop should not be used on Windows"); - } - this.watchloop_handle = std.Thread.getCurrentId(); Output.Source.configureNamedThread("File Watcher"); @@ -452,7 +560,7 @@ pub fn NewWatcher(comptime ContextType: type) type { this._watchLoop() catch |err| { this.watchloop_handle = null; - PlatformWatcher.stop(); + this.platform.stop(); if (this.running) { this.ctx.onError(err); } @@ -471,70 +579,39 @@ pub fn NewWatcher(comptime ContextType: type) type { allocator.destroy(this); } - pub fn remove(this: *Watcher, hash: HashType) void { - this.mutex.lock(); - defer this.mutex.unlock(); - if (this.indexOf(hash)) |index| { - const fds = this.watchlist.items(.fd); - const fd = fds[index]; - _ = bun.sys.close(fd); - this.watchlist.swapRemove(index); - } - } - - var evict_list_i: WatchItemIndex = 0; - - pub fn removeAtIndex(_: *Watcher, index: WatchItemIndex, hash: HashType, parents: []HashType, comptime kind: WatchItem.Kind) void { - std.debug.assert(index != NoWatchItem); - - evict_list[evict_list_i] = index; - evict_list_i += 1; - - if (comptime kind == .directory) { - for (parents) |parent| { - if (parent == hash) { - evict_list[evict_list_i] = @as(WatchItemIndex, @truncate(parent)); - evict_list_i += 1; - } - } - } - } - pub fn flushEvictions(this: *Watcher) void { - if (evict_list_i == 0) return; - defer evict_list_i = 0; + if (this.evict_list_i == 0) return; + defer this.evict_list_i = 0; // swapRemove messes up the order // But, it only messes up the order if any elements in the list appear after the item being removed // So if we just sort the list by the biggest index first, that should be fine std.sort.pdq( WatchItemIndex, - evict_list[0..evict_list_i], + this.evict_list[0..this.evict_list_i], {}, comptime std.sort.desc(WatchItemIndex), ); var slice = this.watchlist.slice(); const fds = slice.items(.fd); - var last_item = NoWatchItem; + var last_item = no_watch_item; - for (evict_list[0..evict_list_i]) |item| { + for (this.evict_list[0..this.evict_list_i]) |item| { // catch duplicates, since the list is sorted, duplicates will appear right after each other if (item == last_item) continue; - // close the file descriptors here. this should automatically remove it from being watched too. - _ = bun.sys.close(fds[item]); - - // if (Environment.isLinux) { - // INotify.unwatch(event_list_ids[item]); - // } - + if (!Environment.isWindows) { + // on mac and linux we can just close the file descriptor + // TODO do we need to call inotify_rm_watch on linux? + _ = bun.sys.close(fds[item]); + } last_item = item; } - last_item = NoWatchItem; + last_item = no_watch_item; // This is split into two passes because reading the slice while modified is potentially unsafe. - for (evict_list[0..evict_list_i]) |item| { + for (this.evict_list[0..this.evict_list_i]) |item| { if (item == last_item) continue; this.watchlist.swapRemove(item); last_item = item; @@ -543,7 +620,7 @@ pub fn NewWatcher(comptime ContextType: type) type { fn _watchLoop(this: *Watcher) !void { if (Environment.isMac) { - std.debug.assert(DarwinWatcher.fd > 0); + std.debug.assert(this.platform.fd > 0); const KEvent = std.c.Kevent; var changelist_array: [128]KEvent = std.mem.zeroes([128]KEvent); @@ -552,7 +629,7 @@ pub fn NewWatcher(comptime ContextType: type) type { defer Output.flush(); var count_ = std.os.system.kevent( - DarwinWatcher.fd, + this.platform.fd, @as([*]KEvent, changelist), 0, @as([*]KEvent, changelist), @@ -566,7 +643,7 @@ pub fn NewWatcher(comptime ContextType: type) type { const remain = 128 - count_; var timespec = std.os.timespec{ .tv_sec = 0, .tv_nsec = 100_000 }; const extra = std.os.system.kevent( - DarwinWatcher.fd, + this.platform.fd, @as([*]KEvent, changelist[@as(usize, @intCast(count_))..].ptr), 0, @as([*]KEvent, changelist[@as(usize, @intCast(count_))..].ptr), @@ -613,7 +690,7 @@ pub fn NewWatcher(comptime ContextType: type) type { restart: while (true) { defer Output.flush(); - var events = try INotify.read(); + var events = try this.platform.read(); if (events.len == 0) continue :restart; // TODO: is this thread safe? @@ -685,50 +762,96 @@ pub fn NewWatcher(comptime ContextType: type) type { } } } else if (Environment.isWindows) { - @compileError("watchLoop should not be used on Windows"); - } - } - - pub fn indexOf(this: *Watcher, hash: HashType) ?u32 { - for (this.watchlist.items(.hash), 0..) |other, i| { - if (hash == other) { - return @as(u32, @truncate(i)); + var buf: bun.PathBuffer = undefined; + const root = this.fs.top_level_dir; + @memcpy(buf[0..root.len], root); + const needs_slash = root.len == 0 or !bun.strings.charIsAnySlash(root[root.len - 1]); + if (needs_slash) { + buf[root.len] = '\\'; } - } - return null; - } + const baseidx = if (needs_slash) root.len + 1 else root.len; + restart: while (true) { + var event_id: usize = 0; - pub fn addFile( - this: *Watcher, - fd: StoredFileDescriptorType, - file_path: string, - hash: HashType, - loader: options.Loader, - dir_fd: StoredFileDescriptorType, - package_json: ?*PackageJSON, - comptime copy_file_path: bool, - ) !void { - // This must lock due to concurrent transpiler - this.mutex.lock(); - defer this.mutex.unlock(); + // first wait has infinite timeout - we're waiting for the next event and don't want to spin + var timeout = WindowsWatcher.Timeout.infinite; + while (true) { + var iter = try this.platform.next(timeout) orelse break; + // after the first wait, we want to start coalescing events, so we wait for a minimal amount of time + timeout = WindowsWatcher.Timeout.minimal; + const item_paths = this.watchlist.items(.file_path); + log("number of watched items: {d}", .{item_paths.len}); + while (iter.next()) |event| { + const convert_res = bun.strings.copyUTF16IntoUTF8(buf[baseidx..], []const u16, event.filename, false); + const eventpath = buf[0 .. baseidx + convert_res.written]; - if (this.indexOf(hash)) |index| { - if (comptime FeatureFlags.atomic_file_watcher) { - // On Linux, the file descriptor might be out of date. - if (fd.int() > 0) { - var fds = this.watchlist.items(.fd); - fds[index] = fd; + log("watcher update event: (filename: {s}, action: {s}", .{ eventpath, @tagName(event.action) }); + + // TODO this probably needs a more sophisticated search algorithm in the future + // Possible approaches: + // - Keep a sorted list of the watched paths and perform a binary search. We could use a bool to keep + // track of whether the list is sorted and only sort it when we detect a change. + // - Use a prefix tree. Potentially more efficient for large numbers of watched paths, but complicated + // to implement and maintain. + // - others that i'm not thinking of + + for (item_paths, 0..) |path_, item_idx| { + var path = path_; + if (path.len > 0 and bun.strings.charIsAnySlash(path[path.len - 1])) { + path = path[0 .. path.len - 1]; + } + // log("checking path: {s}\n", .{path}); + // check if the current change applies to this item + // if so, add it to the eventlist + const rel = bun.path.isParentOrEqual(eventpath, path); + // skip unrelated items + if (rel == .unrelated) continue; + // if the event is for a parent dir of the item, only emit it if it's a delete or rename + if (rel == .parent and (event.action != .Removed or event.action != .RenamedOld)) continue; + this.watch_events[event_id].fromFileNotify(event, @truncate(item_idx)); + event_id += 1; + } + } + } + if (event_id == 0) { + continue :restart; } - } - return; - } - try this.appendFileMaybeLock(fd, file_path, hash, loader, dir_fd, package_json, copy_file_path, false); + // log("event_id: {d}\n", .{event_id}); + + var all_events = this.watch_events[0..event_id]; + std.sort.pdq(WatchEvent, all_events, {}, WatchEvent.sortByIndex); + + var last_event_index: usize = 0; + var last_event_id: INotify.EventListIndex = std.math.maxInt(INotify.EventListIndex); + + for (all_events, 0..) |_, i| { + // if (all_events[i].name_len > 0) { + // this.changed_filepaths[name_off] = temp_name_list[all_events[i].name_off]; + // all_events[i].name_off = name_off; + // name_off += 1; + // } + + if (all_events[i].index == last_event_id) { + all_events[last_event_index].merge(all_events[i]); + continue; + } + last_event_index = i; + last_event_id = all_events[i].index; + } + if (all_events.len == 0) continue :restart; + all_events = all_events[0 .. last_event_index + 1]; + + log("calling onFileUpdate (all_events.len = {d})", .{all_events.len}); + + this.ctx.onFileUpdate(all_events, this.changed_filepaths[0 .. last_event_index + 1], this.watchlist); + } + } } fn appendFileAssumeCapacity( this: *Watcher, - fd: StoredFileDescriptorType, + fd: bun.FileDescriptor, file_path: string, hash: HashType, loader: options.Loader, @@ -736,7 +859,15 @@ pub fn NewWatcher(comptime ContextType: type) type { package_json: ?*PackageJSON, comptime copy_file_path: bool, ) !void { - var index: PlatformWatcher.EventListIndex = std.math.maxInt(PlatformWatcher.EventListIndex); + if (comptime Environment.isWindows) { + // on windows we can only watch items that are in the directory tree of the top level dir + const rel = bun.path.isParentOrEqual(this.fs.top_level_dir, file_path); + if (rel == .unrelated) { + Output.warn("File {s} is not in the project directory and will not be watched\n", .{file_path}); + return; + } + } + const watchlist_id = this.watchlist.len; const file_path_: string = if (comptime copy_file_path) @@ -744,13 +875,24 @@ pub fn NewWatcher(comptime ContextType: type) type { else file_path; + var item = WatchItem{ + .file_path = file_path_, + .fd = fd, + .hash = hash, + .count = 0, + .loader = loader, + .parent_hash = parent_hash, + .package_json = package_json, + .kind = .file, + }; + if (comptime Environment.isMac) { const KEvent = std.c.Kevent; // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html var event = std.mem.zeroes(KEvent); - event.flags = c.EV_ADD | c.EV_CLEAR | c.EV_ENABLE; + event.flags = std.c.EV_ADD | std.c.EV_CLEAR | std.c.EV_ENABLE; // we want to know about the vnode event.filter = std.c.EVFILT_VNODE; @@ -768,7 +910,7 @@ pub fn NewWatcher(comptime ContextType: type) type { // - We register the event here. // our while(true) loop above receives notification of changes to any of the events created here. _ = std.os.system.kevent( - DarwinWatcher.fd, + this.platform.fd, @as([]KEvent, events[0..1]).ptr, 1, @as([]KEvent, events[0..1]).ptr, @@ -782,37 +924,35 @@ pub fn NewWatcher(comptime ContextType: type) type { // buf[file_path_to_use_.len] = 0; var buf = file_path_.ptr; const slice: [:0]const u8 = buf[0..file_path_.len :0]; - index = try INotify.watchPath(slice); + item.eventlist_index = try this.platform.watchPath(slice); } - this.watchlist.appendAssumeCapacity(.{ - .file_path = file_path_, - .fd = fd, - .hash = hash, - .count = 0, - .eventlist_index = index, - .loader = loader, - .parent_hash = parent_hash, - .package_json = package_json, - .kind = .file, - }); + this.watchlist.appendAssumeCapacity(item); } fn appendDirectoryAssumeCapacity( this: *Watcher, - stored_fd: StoredFileDescriptorType, + stored_fd: bun.FileDescriptor, file_path: string, hash: HashType, comptime copy_file_path: bool, ) !WatchItemIndex { + if (comptime Environment.isWindows) { + // on windows we can only watch items that are in the directory tree of the top level dir + const rel = bun.path.isParentOrEqual(this.fs.top_level_dir, file_path); + if (rel == .unrelated) { + Output.warn("Directory {s} is not in the project directory and will not be watched\n", .{file_path}); + return no_watch_item; + } + } + const fd = brk: { if (stored_fd.int() > 0) break :brk stored_fd; const dir = try std.fs.cwd().openDir(file_path, .{}); break :brk bun.toFD(dir.fd); }; - const parent_hash = Watcher.getHash(Fs.PathName.init(file_path).dirWithTrailingSlash()); - var index: PlatformWatcher.EventListIndex = std.math.maxInt(PlatformWatcher.EventListIndex); + const parent_hash = getHash(bun.fs.PathName.init(file_path).dirWithTrailingSlash()); const file_path_: string = if (comptime copy_file_path) bun.asByteSlice(try this.allocator.dupeZ(u8, file_path)) @@ -821,13 +961,24 @@ pub fn NewWatcher(comptime ContextType: type) type { const watchlist_id = this.watchlist.len; + var item = WatchItem{ + .file_path = file_path_, + .fd = fd, + .hash = hash, + .count = 0, + .loader = options.Loader.file, + .parent_hash = parent_hash, + .kind = .directory, + .package_json = null, + }; + if (Environment.isMac) { const KEvent = std.c.Kevent; // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html var event = std.mem.zeroes(KEvent); - event.flags = c.EV_ADD | c.EV_CLEAR | c.EV_ENABLE; + event.flags = std.c.EV_ADD | std.c.EV_CLEAR | std.c.EV_ENABLE; // we want to know about the vnode event.filter = std.c.EVFILT_VNODE; @@ -849,7 +1000,7 @@ pub fn NewWatcher(comptime ContextType: type) type { // - We register the event here. // our while(true) loop above receives notification of changes to any of the events created here. _ = std.os.system.kevent( - DarwinWatcher.fd, + this.platform.fd, @as([]KEvent, events[0..1]).ptr, 1, @as([]KEvent, events[0..1]).ptr, @@ -862,53 +1013,22 @@ pub fn NewWatcher(comptime ContextType: type) type { bun.copy(u8, &buf, file_path_to_use_); buf[file_path_to_use_.len] = 0; const slice: [:0]u8 = buf[0..file_path_to_use_.len :0]; - index = try INotify.watchDir(slice); + item.eventlist_index = try this.platform.watchDir(slice); } - this.watchlist.appendAssumeCapacity(.{ - .file_path = file_path_, - .fd = fd, - .hash = hash, - .count = 0, - .eventlist_index = index, - .loader = options.Loader.file, - .parent_hash = parent_hash, - .kind = .directory, - .package_json = null, - }); + this.watchlist.appendAssumeCapacity(item); return @as(WatchItemIndex, @truncate(this.watchlist.len - 1)); } - pub inline fn isEligibleDirectory(this: *Watcher, dir: string) bool { - return strings.indexOf(dir, this.fs.top_level_dir) != null and strings.indexOf(dir, "node_modules") == null; - } - - pub fn addDirectory( - this: *Watcher, - fd: StoredFileDescriptorType, - file_path: string, - hash: HashType, - comptime copy_file_path: bool, - ) !void { - this.mutex.lock(); - defer this.mutex.unlock(); - - if (this.indexOf(hash) != null) { - return; - } - - try this.watchlist.ensureUnusedCapacity(this.allocator, 1); - - _ = try this.appendDirectoryAssumeCapacity(fd, file_path, hash, copy_file_path); - } + // Below is platform-independent pub fn appendFileMaybeLock( this: *Watcher, - fd: StoredFileDescriptorType, + fd: bun.FileDescriptor, file_path: string, hash: HashType, loader: options.Loader, - dir_fd: StoredFileDescriptorType, + dir_fd: bun.FileDescriptor, package_json: ?*PackageJSON, comptime copy_file_path: bool, comptime lock: bool, @@ -916,10 +1036,10 @@ pub fn NewWatcher(comptime ContextType: type) type { if (comptime lock) this.mutex.lock(); defer if (comptime lock) this.mutex.unlock(); std.debug.assert(file_path.len > 1); - const pathname = Fs.PathName.init(file_path); + const pathname = bun.fs.PathName.init(file_path); const parent_dir = pathname.dirWithTrailingSlash(); - const parent_dir_hash: HashType = Watcher.getHash(parent_dir); + const parent_dir_hash: HashType = getHash(parent_dir); var parent_watch_item: ?WatchItemIndex = null; const autowatch_parent_dir = (comptime FeatureFlags.watch_directories) and this.isEligibleDirectory(parent_dir); @@ -928,7 +1048,7 @@ pub fn NewWatcher(comptime ContextType: type) type { if (dir_fd.int() > 0) { const fds = watchlist_slice.items(.fd); - if (std.mem.indexOfScalar(StoredFileDescriptorType, fds, dir_fd)) |i| { + if (std.mem.indexOfScalar(bun.FileDescriptor, fds, dir_fd)) |i| { parent_watch_item = @as(WatchItemIndex, @truncate(i)); } } @@ -965,17 +1085,101 @@ pub fn NewWatcher(comptime ContextType: type) type { } } + inline fn isEligibleDirectory(this: *Watcher, dir: string) bool { + return strings.contains(dir, this.fs.top_level_dir) and !strings.contains(dir, "node_modules"); + } + pub fn appendFile( this: *Watcher, - fd: StoredFileDescriptorType, + fd: bun.FileDescriptor, file_path: string, hash: HashType, loader: options.Loader, - dir_fd: StoredFileDescriptorType, + dir_fd: bun.FileDescriptor, package_json: ?*PackageJSON, comptime copy_file_path: bool, ) !void { return appendFileMaybeLock(this, fd, file_path, hash, loader, dir_fd, package_json, copy_file_path, true); } + + pub fn addDirectory( + this: *Watcher, + fd: bun.FileDescriptor, + file_path: string, + hash: HashType, + comptime copy_file_path: bool, + ) !void { + this.mutex.lock(); + defer this.mutex.unlock(); + + if (this.indexOf(hash) != null) { + return; + } + + try this.watchlist.ensureUnusedCapacity(this.allocator, 1); + + _ = try this.appendDirectoryAssumeCapacity(fd, file_path, hash, copy_file_path); + } + + pub fn addFile( + this: *Watcher, + fd: bun.FileDescriptor, + file_path: string, + hash: HashType, + loader: options.Loader, + dir_fd: bun.FileDescriptor, + package_json: ?*PackageJSON, + comptime copy_file_path: bool, + ) !void { + // This must lock due to concurrent transpiler + this.mutex.lock(); + defer this.mutex.unlock(); + + if (this.indexOf(hash)) |index| { + if (comptime FeatureFlags.atomic_file_watcher) { + // On Linux, the file descriptor might be out of date. + if (fd.int() > 0) { + var fds = this.watchlist.items(.fd); + fds[index] = fd; + } + } + return; + } + + try this.appendFileMaybeLock(fd, file_path, hash, loader, dir_fd, package_json, copy_file_path, false); + } + + pub fn indexOf(this: *Watcher, hash: HashType) ?u32 { + for (this.watchlist.items(.hash), 0..) |other, i| { + if (hash == other) { + return @as(u32, @truncate(i)); + } + } + return null; + } + + pub fn remove(this: *Watcher, hash: HashType) void { + this.mutex.lock(); + defer this.mutex.unlock(); + if (this.indexOf(hash)) |index| { + this.removeAtIndex(@truncate(index), hash, &[_]HashType{}, .file); + } + } + + pub fn removeAtIndex(this: *Watcher, index: WatchItemIndex, hash: HashType, parents: []HashType, comptime kind: WatchItem.Kind) void { + std.debug.assert(index != no_watch_item); + + this.evict_list[this.evict_list_i] = index; + this.evict_list_i += 1; + + if (comptime kind == .directory) { + for (parents) |parent| { + if (parent == hash) { + this.evict_list[this.evict_list_i] = @as(WatchItemIndex, @truncate(parent)); + this.evict_list_i += 1; + } + } + } + } }; } diff --git a/src/windows.zig b/src/windows.zig index c74afad7c0..be35bc0eb6 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -64,6 +64,7 @@ pub const advapi32 = windows.advapi32; pub const INVALID_FILE_ATTRIBUTES: u32 = std.math.maxInt(u32); pub const nt_object_prefix = [4]u16{ '\\', '?', '?', '\\' }; +pub const nt_maxpath_prefix = [4]u16{ '\\', '\\', '?', '\\' }; const std = @import("std"); pub const HANDLE = win32.HANDLE; @@ -2985,28 +2986,7 @@ pub extern "kernel32" fn SetFileInformationByHandle( ) BOOL; pub fn getLastErrno() bun.C.E { - return translateWinErrorToErrno(bun.windows.kernel32.GetLastError()); -} - -pub fn translateWinErrorToErrno(err: win32.Win32Error) bun.C.E { - return switch (err) { - .SUCCESS => .SUCCESS, - .FILE_NOT_FOUND => .NOENT, - .PATH_NOT_FOUND => .NOENT, - .TOO_MANY_OPEN_FILES => .NOMEM, - .ACCESS_DENIED => .PERM, - .INVALID_HANDLE => .BADF, - .NOT_ENOUGH_MEMORY => .NOMEM, - .OUTOFMEMORY => .NOMEM, - .INVALID_PARAMETER => .INVAL, - - else => |t| { - // if (bun.Environment.isDebug) { - bun.Output.warn("Called translateWinErrorToErrno with {s} which does not have a mapping to errno.", .{@tagName(t)}); - // } - return .UNKNOWN; - }, - }; + return (bun.C.SystemErrno.init(bun.windows.kernel32.GetLastError()) orelse SystemErrno.EUNKNOWN).toE(); } pub fn translateNTStatusToErrno(err: win32.NTSTATUS) bun.C.E { @@ -3032,10 +3012,62 @@ pub fn translateNTStatusToErrno(err: win32.NTSTATUS) bun.C.E { pub extern "kernel32" fn GetHostNameW( lpBuffer: PWSTR, nSize: c_int, -) BOOL; +) callconv(windows.WINAPI) BOOL; /// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw pub extern "kernel32" fn GetTempPathW( nBufferLength: DWORD, // [in] lpBuffer: LPCWSTR, // [out] ) DWORD; + +pub extern "kernel32" fn CreateJobObjectA( + lpJobAttributes: ?*anyopaque, // [in, optional] + lpName: ?LPCSTR, // [in, optional] +) callconv(windows.WINAPI) HANDLE; + +pub extern "kernel32" fn AssignProcessToJobObject( + hJob: HANDLE, // [in] + hProcess: HANDLE, // [in] +) callconv(windows.WINAPI) BOOL; + +pub extern "kernel32" fn ResumeThread( + hJob: HANDLE, // [in] +) callconv(windows.WINAPI) DWORD; + +pub const JOBOBJECT_ASSOCIATE_COMPLETION_PORT = extern struct { + CompletionKey: windows.PVOID, + CompletionPort: HANDLE, +}; + +pub const JobObjectAssociateCompletionPortInformation: DWORD = 7; + +pub extern "kernel32" fn SetInformationJobObject( + hJob: HANDLE, + JobObjectInformationClass: DWORD, + lpJobObjectInformation: LPVOID, + cbJobObjectInformationLength: DWORD, +) callconv(windows.WINAPI) BOOL; + +// Found experimentally: +// #include +// #include +// +// int main() { +// printf("%ld\n", JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO); +// printf("%ld\n", JOB_OBJECT_MSG_EXIT_PROCESS); +// } +// +// Output: +// 4 +// 7 +pub const JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO = 4; +pub const JOB_OBJECT_MSG_EXIT_PROCESS = 7; + +pub extern "kernel32" fn OpenProcess( + dwDesiredAccess: DWORD, + bInheritHandle: BOOL, + dwProcessId: DWORD, +) callconv(windows.WINAPI) ?HANDLE; + +// https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights +pub const PROCESS_QUERY_LIMITED_INFORMATION: DWORD = 0x1000; diff --git a/src/windows_c.zig b/src/windows_c.zig index 37c6fb5d83..7221835e30 100644 --- a/src/windows_c.zig +++ b/src/windows_c.zig @@ -695,16 +695,16 @@ pub const SystemErrno = enum(u16) { return init(@as(Win32Error, @enumFromInt(code))); } else { if (comptime bun.Environment.allow_assert) - bun.Output.debug("Unknown error code: {any}\n", .{code}); + bun.Output.debugWarn("Unknown error code: {any}\n", .{code}); return null; } } - if (comptime @TypeOf(code) == Win32Error) { - return switch (code) { + if (comptime @TypeOf(code) == Win32Error or @TypeOf(code) == std.os.windows.Win32Error) { + return switch (@as(Win32Error, @enumFromInt(@intFromEnum(code)))) { Win32Error.NOACCESS => SystemErrno.EACCES, - @as(Win32Error, @enumFromInt(10013)) => SystemErrno.EACCES, + Win32Error.WSAEACCES => SystemErrno.EACCES, Win32Error.ELEVATION_REQUIRED => SystemErrno.EACCES, Win32Error.CANT_ACCESS_FILE => SystemErrno.EACCES, Win32Error.ADDRESS_ALREADY_ASSOCIATED => SystemErrno.EADDRINUSE, @@ -803,7 +803,7 @@ pub const SystemErrno = enum(u16) { Win32Error.META_EXPANSION_TOO_LONG => SystemErrno.E2BIG, Win32Error.WSAESOCKTNOSUPPORT => SystemErrno.ESOCKTNOSUPPORT, Win32Error.DELETE_PENDING => SystemErrno.EBUSY, - else => return null, + else => null, }; } @@ -1269,6 +1269,12 @@ pub fn renameAtW( new_path_w: []const u16, replace_if_exists: bool, ) Maybe(void) { + if (comptime bun.Environment.allow_assert) { + // if the directories are the same and the destination path is absolute, the old path name is kept + if (old_dir_fd == new_dir_fd) { + std.debug.assert(!std.fs.path.isAbsoluteWindowsWTF16(new_path_w)); + } + } const src_fd = switch (bun.sys.ntCreateFile( old_dir_fd, old_path_w, @@ -1309,6 +1315,11 @@ pub fn moveOpenedFileAt( // and therefore having different behavior when the Windows version is >= rs1 but < rs5. comptime std.debug.assert(builtin.target.os.version_range.windows.min.isAtLeast(.win10_rs5)); + if (bun.Environment.allow_assert) { + std.debug.assert(std.mem.indexOfScalar(u16, new_file_name, '\\') == null); // Call moveOpenedFileAtLoose + std.debug.assert(std.mem.indexOfScalar(u16, new_file_name, '/') == null); // Call moveOpenedFileAtLoose + } + const struct_buf_len = @sizeOf(w.FILE_RENAME_INFORMATION_EX) + (bun.MAX_PATH_BYTES - 1); var rename_info_buf: [struct_buf_len]u8 align(@alignOf(w.FILE_RENAME_INFORMATION_EX)) = undefined; diff --git a/test/bun.lockb b/test/bun.lockb index 9484b06eca..858b706a69 100755 Binary files a/test/bun.lockb and b/test/bun.lockb differ diff --git a/test/bundler/bundler_cjs2esm.test.ts b/test/bundler/bundler_cjs2esm.test.ts index 5b34a27f8f..01a17356b6 100644 --- a/test/bundler/bundler_cjs2esm.test.ts +++ b/test/bundler/bundler_cjs2esm.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import assert from "assert"; import dedent from "dedent"; import { itBundled, testForFile } from "./expectBundled"; diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index f9a427ab0f..ea9cd810e2 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -1,5 +1,6 @@ import assert from "assert"; import dedent from "dedent"; +import { sep } from "path"; import { itBundled, testForFile } from "./expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); @@ -36,7 +37,7 @@ describe("bundler", () => { }, target: "bun", run: { - stdout: "a/b", + stdout: `a${sep}b`, }, }); itBundled("edgecase/ImportStarFunction", { diff --git a/test/bundler/bundler_naming.test.ts b/test/bundler/bundler_naming.test.ts index 5bdc1fbce4..2004056d87 100644 --- a/test/bundler/bundler_naming.test.ts +++ b/test/bundler/bundler_naming.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import assert from "assert"; import dedent from "dedent"; import { ESBUILD, itBundled, testForFile } from "./expectBundled"; @@ -270,4 +269,37 @@ describe("bundler", () => { file: "/out/_.._/hello/file.js", }, }); + itBundled("naming/WithPathTraversal", { + files: { + "/a/hello/entry.js": /* js */ ` + import data from '../dependency' + console.log(data); + `, + "/a/dependency.js": /* js */ ` + export default 1; + `, + "/a/hello/world/entry.js": /* js */ ` + console.log(2); + `, + "/a/hello/world/a/a/a/a/a/a/a/entry.js": /* js */ ` + console.log(3); + `, + }, + entryNaming: "foo/../bar/[dir]/file.[ext]", + entryPointsRaw: ["./a/hello/entry.js", "./a/hello/world/entry.js", "./a/hello/world/a/a/a/a/a/a/a/entry.js"], + run: [ + { + file: "/out/bar/file.js", + stdout: "1", + }, + { + file: "/out/bar/world/file.js", + stdout: "2", + }, + { + file: "/out/bar/world/a/a/a/a/a/a/a/file.js", + stdout: "3", + }, + ], + }); }); diff --git a/test/bundler/esbuild/importstar.test.ts b/test/bundler/esbuild/importstar.test.ts index 4b49a68da0..cabe81a4b1 100644 --- a/test/bundler/esbuild/importstar.test.ts +++ b/test/bundler/esbuild/importstar.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import assert from "assert"; import { itBundled, testForFile } from "../expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); diff --git a/test/bundler/esbuild/loader.test.ts b/test/bundler/esbuild/loader.test.ts index 380f74eda3..c76fdd18ac 100644 --- a/test/bundler/esbuild/loader.test.ts +++ b/test/bundler/esbuild/loader.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import fs from "fs"; import { itBundled, testForFile } from "../expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); diff --git a/test/bundler/esbuild/packagejson.test.ts b/test/bundler/esbuild/packagejson.test.ts index 8fed430d38..5d459b968a 100644 --- a/test/bundler/esbuild/packagejson.test.ts +++ b/test/bundler/esbuild/packagejson.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import { itBundled, testForFile } from "../expectBundled"; var { describe, test, expect } = testForFile(import.meta.path); diff --git a/test/bundler/esbuild/splitting.test.ts b/test/bundler/esbuild/splitting.test.ts index 0a7907ae5d..fbf5634fe7 100644 --- a/test/bundler/esbuild/splitting.test.ts +++ b/test/bundler/esbuild/splitting.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import assert from "assert"; import { readdirSync } from "fs"; import { itBundled, testForFile } from "../expectBundled"; diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 32b60bedb9..2cf4dff3ce 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -445,7 +445,7 @@ function expectBundled( backend = plugins !== undefined ? "api" : "cli"; } - const root = path.join(outBase, id.replaceAll("/", path.sep)); + const root = path.join(outBase, id); if (DEBUG) console.log("root:", root); const entryPaths = entryPoints.map(file => path.join(root, file)); @@ -789,7 +789,7 @@ function expectBundled( const warningText = stderr!.toUnixString(); const allWarnings = warnParser(warningText).map(([error, source]) => { const [_str2, fullFilename, line, col] = source.match(/bun-build-tests[\/\\](.*):(\d+):(\d+)/)!; - const file = fullFilename.slice(id.length + path.basename(outBase).length + 1); + const file = fullFilename.slice(id.length + path.basename(outBase).length + 1).replaceAll("\\", "/"); return { error, file, line, col }; }); const expectedWarnings = bundleWarnings @@ -1176,7 +1176,7 @@ for (const [key, blob] of build.outputs) { // check reference if (matchesReference) { const { ref } = matchesReference; - const theirRoot = path.join(outBase, ref.id.replaceAll("/", path.sep)); + const theirRoot = path.join(outBase, ref.id); if (!existsSync(theirRoot)) { expectBundled(ref.id, ref.options, false, true); if (!existsSync(theirRoot)) { @@ -1287,7 +1287,7 @@ for (const [key, blob] of build.outputs) { if (typeof run.stdout === "string") { const expected = dedent(run.stdout).trim(); if (expected !== result) { - console.log(`runtime failed file=${file}`); + console.log(`runtime failed file: ${file}`); console.log(`reference stdout:`); console.log(result); console.log(`---`); @@ -1295,7 +1295,7 @@ for (const [key, blob] of build.outputs) { expect(result).toBe(expected); } else { if (!run.stdout.test(result)) { - console.log(`runtime failed file=${file}`); + console.log(`runtime failed file: ${file}`); console.log(`reference stdout:`); console.log(result); console.log(`---`); @@ -1330,7 +1330,7 @@ export function itBundled( ): BundlerTestRef { if (typeof opts === "function") { const fn = opts; - opts = opts({ root: path.join(outBase, id.replaceAll("/", path.sep)), getConfigRef }); + opts = opts({ root: path.join(outBase, id), getConfigRef }); // @ts-expect-error opts._referenceFn = fn; } diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index facffa042b..422f87eb67 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -1,11 +1,10 @@ -// @known-failing-on-windows: 1 failing import { spawn } from "bun"; import { expect, it } from "bun:test"; import { bunExe, bunEnv, tempDirWithFiles, bunRun, bunRunAsScript } from "harness"; -import { readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs"; +import { readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, copyFileSync } from "fs"; import { join } from "path"; -const hotRunnerRoot = join(import.meta.dir, "/hot-runner-root.js"); +const hotRunnerRoot = join(import.meta.dir, "hot-runner-root.js"); it("should hot reload when file is overwritten", async () => { const root = hotRunnerRoot; @@ -169,7 +168,8 @@ it("should not hot reload when a random file is written", async () => { }); it("should hot reload when a file is deleted and rewritten", async () => { - const root = hotRunnerRoot; + const root = hotRunnerRoot + ".tmp.js"; + copyFileSync(hotRunnerRoot, root); const runner = spawn({ cmd: [bunExe(), "--hot", "run", root], env: bunEnv, @@ -182,7 +182,7 @@ it("should hot reload when a file is deleted and rewritten", async () => { async function onReload() { const contents = readFileSync(root, "utf-8"); - unlinkSync(root); + rmSync(root); writeFileSync(root, contents); } @@ -205,12 +205,13 @@ it("should hot reload when a file is deleted and rewritten", async () => { if (any) await onReload(); } - + rmSync(root); expect(reloadCounter).toBe(3); }); it("should hot reload when a file is renamed() into place", async () => { - const root = hotRunnerRoot; + const root = hotRunnerRoot + ".tmp.js"; + copyFileSync(hotRunnerRoot, root); const runner = spawn({ cmd: [bunExe(), "--hot", "run", root], env: bunEnv, @@ -227,6 +228,8 @@ it("should hot reload when a file is renamed() into place", async () => { await 1; writeFileSync(root + ".tmpfile", contents); await 1; + rmSync(root); + await 1; renameSync(root + ".tmpfile", root); await 1; } @@ -250,6 +253,6 @@ it("should hot reload when a file is renamed() into place", async () => { if (any) await onReload(); } - + rmSync(root); expect(reloadCounter).toBe(3); }); diff --git a/test/cli/install/bun-create.test.ts b/test/cli/install/bun-create.test.ts index 50783d8c18..97ecb89714 100644 --- a/test/cli/install/bun-create.test.ts +++ b/test/cli/install/bun-create.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { spawn, spawnSync } from "bun"; import { afterEach, beforeEach, expect, it, describe } from "bun:test"; import { bunExe, bunEnv as env } from "harness"; diff --git a/test/cli/install/bun-remove.test.ts b/test/cli/install/bun-remove.test.ts index 7659215af7..2a302821af 100644 --- a/test/cli/install/bun-remove.test.ts +++ b/test/cli/install/bun-remove.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { bunExe, bunEnv as env } from "harness"; import { mkdir, mkdtemp, realpath, rm, writeFile } from "fs/promises"; import { join, relative } from "path"; diff --git a/test/cli/install/bun-update.test.ts b/test/cli/install/bun-update.test.ts index 4a5f53b61c..29dcec610d 100644 --- a/test/cli/install/bun-update.test.ts +++ b/test/cli/install/bun-update.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { file, listen, Socket, spawn } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { bunExe, bunEnv as env, toBeValidBin, toHaveBins } from "harness"; diff --git a/test/cli/install/migration/migrate.test.ts b/test/cli/install/migration/migrate.test.ts index bd5c9f2795..5af930d5dc 100644 --- a/test/cli/install/migration/migrate.test.ts +++ b/test/cli/install/migration/migrate.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import fs from "fs"; import { test, expect, beforeAll, afterAll } from "bun:test"; import { bunEnv, bunExe } from "harness"; diff --git a/test/cli/run/env.test.ts b/test/cli/run/env.test.ts index 645b41cf67..a381338535 100644 --- a/test/cli/run/env.test.ts +++ b/test/cli/run/env.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { describe, expect, test, beforeAll, afterAll } from "bun:test"; import { bunRun, bunRunAsScript, bunTest, tempDirWithFiles, bunExe, bunEnv } from "harness"; import path from "path"; @@ -701,8 +700,27 @@ console.log(dynamic().NODE_ENV); }); test("NODE_ENV default is not propogated in bun run", () => { + const getenv = + process.platform !== "win32" ? "env | grep NODE_ENV && exit 1 || true" : "node -e if(process.env.NODE_ENV)throw(1)"; const tmp = tempDirWithFiles("default-node-env", { - "package.json": '{"scripts":{"show-env":"env | grep NODE_ENV && exit 1 || true"}}', + "package.json": '{"scripts":{"show-env":' + JSON.stringify(getenv) + "}}", }); expect(bunRunAsScript(tmp, "show-env", {}).stdout).toBe(""); }); + +const todoOnPosix = process.platform !== "win32" ? test.todo : test; +todoOnPosix("setting process.env coerces the value to a string", () => { + // @ts-expect-error + process.env.SET_TO_TRUE = true; + let did_call = 0; + // @ts-expect-error + process.env.SET_TO_BUN = { + toString() { + did_call++; + return "bun!"; + }, + }; + expect(process.env.SET_TO_TRUE).toBe("true"); + expect(process.env.SET_TO_BUN).toBe("bun!"); + expect(did_call).toBe(1); +}); diff --git a/test/cli/run/run-extensionless.test.ts b/test/cli/run/run-extensionless.test.ts index 077b2b5f56..f2c168847c 100644 --- a/test/cli/run/run-extensionless.test.ts +++ b/test/cli/run/run-extensionless.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { expect, test } from "bun:test"; import { mkdirSync, realpathSync } from "fs"; import { bunEnv, bunExe } from "harness"; diff --git a/test/cli/run/run-process-env.test.ts b/test/cli/run/run-process-env.test.ts index af4f86b541..44bc169824 100644 --- a/test/cli/run/run-process-env.test.ts +++ b/test/cli/run/run-process-env.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { describe, expect, test } from "bun:test"; import { bunExe, bunRunAsScript, tempDirWithFiles } from "harness"; @@ -7,7 +6,7 @@ describe("process.env", () => { const scriptName = "start:dev"; const dir = tempDirWithFiles("processenv", { - "package.json": `{'scripts': {'${scriptName}': '${bunExe()} run index.ts'}}`, + "package.json": JSON.stringify({ "scripts": { [`${scriptName}`]: `${bunExe()} run index.ts` } }), "index.ts": "console.log(process.env.npm_lifecycle_event);", }); @@ -18,9 +17,9 @@ describe("process.env", () => { // https://github.com/oven-sh/bun/issues/3589 test("npm_lifecycle_event should have the value of the last call", () => { const dir = tempDirWithFiles("processenv_ls_call", { - "package.json": `{"scripts": { "first": "${bunExe()} run --cwd lsc second" } }`, + "package.json": JSON.stringify({ scripts: { first: `${bunExe()} run --cwd lsc second` } }), "lsc": { - "package.json": `{"scripts": { "second": "${bunExe()} run index.ts" } }`, + "package.json": JSON.stringify({ scripts: { second: `${bunExe()} run index.ts` } }), "index.ts": "console.log(process.env.npm_lifecycle_event);", }, }); diff --git a/test/cli/run/transpiler-cache.test.ts b/test/cli/run/transpiler-cache.test.ts index cc4a915912..2ce052cdfb 100644 --- a/test/cli/run/transpiler-cache.test.ts +++ b/test/cli/run/transpiler-cache.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import assert from "assert"; import { Subprocess } from "bun"; import { beforeEach, describe, expect, test } from "bun:test"; diff --git a/test/cli/test/bun-test.test.ts b/test/cli/test/bun-test.test.ts index 0c28800280..1ae8d3557c 100644 --- a/test/cli/test/bun-test.test.ts +++ b/test/cli/test/bun-test.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { join, resolve, dirname } from "node:path"; import { tmpdir } from "node:os"; import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from "node:fs"; diff --git a/test/cli/watch/watch.test.ts b/test/cli/watch/watch.test.ts index fc4d65a1da..a4cf98aac5 100644 --- a/test/cli/watch/watch.test.ts +++ b/test/cli/watch/watch.test.ts @@ -1,14 +1,14 @@ -import { describe, test, expect, afterEach } from "bun:test"; +import { it, expect, afterEach } from "bun:test"; import type { Subprocess } from "bun"; import { spawn } from "bun"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { mkdtempSync, writeFileSync } from "node:fs"; +import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; import { bunExe, bunEnv } from "harness"; let watchee: Subprocess; -describe("bun --watch", () => { +it("should watch files", async () => { const cwd = mkdtempSync(join(tmpdir(), "bun-test-")); const path = join(cwd, "watchee.js"); @@ -16,16 +16,25 @@ describe("bun --watch", () => { writeFileSync(path, `console.log(${i});`); }; - test("should watch files", async () => { - watchee = spawn({ - cwd, - cmd: [bunExe(), "--watch", "watchee.js"], - env: bunEnv, - stdout: "inherit", - stderr: "inherit", - }); - await Bun.sleep(2000); + let i = 0; + updateFile(i); + watchee = spawn({ + cwd, + cmd: [bunExe(), "--watch", "watchee.js"], + env: bunEnv, + stdout: "pipe", + stderr: "inherit", + stdin: "ignore", }); + + for await (const line of watchee.stdout) { + if (i == 10) break; + var str = new TextDecoder().decode(line); + expect(str).toContain(`${i}`); + i++; + updateFile(i); + } + rmSync(path); }); afterEach(() => { diff --git a/test/harness.ts b/test/harness.ts index a0952b6f4d..416df595c7 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1,5 +1,5 @@ import { gc as bunGC, unsafe, which } from "bun"; -import { expect } from "bun:test"; +import { describe, test, expect, afterAll, beforeAll } from "bun:test"; import { readlink, readFile } from "fs/promises"; import { isAbsolute } from "path"; import { openSync, closeSync } from "node:fs"; @@ -323,3 +323,109 @@ declare global { Buffer.prototype.toUnixString = function () { return this.toString("utf-8").replaceAll("\r\n", "\n"); }; + +export function dockerExe(): string | null { + return which("docker") || which("podman") || null; +} + +export async function waitForPort(port: number, timeout: number = 60_000): Promise { + let deadline = Date.now() + Math.max(1, timeout); + let error: unknown; + while (Date.now() < deadline) { + error = await new Promise(resolve => { + Bun.connect({ + hostname: "localhost", + port, + socket: { + data: socket => { + resolve(undefined); + socket.end(); + }, + end: () => resolve(new Error("Socket closed")), + error: (_, cause) => resolve(new Error("Socket error", { cause })), + connectError: (_, cause) => resolve(new Error("Socket connect error", { cause })), + }, + }); + }); + if (error) { + await Bun.sleep(1000); + } else { + return; + } + } + throw error; +} + +export async function describeWithContainer( + label: string, + { + image, + env = {}, + args = [], + archs, + }: { + image: string; + env?: Record; + args?: string[]; + archs?: NodeJS.Architecture[]; + }, + fn: (port: number) => void, +) { + describe(label, () => { + const docker = dockerExe(); + if (!docker) { + test.skip(`docker is not installed, skipped: ${image}`, () => {}); + return; + } + const { arch, platform } = process; + if ((archs && !archs?.includes(arch)) || platform === "win32") { + test.skip(`docker image is not supported on ${platform}/${arch}, skipped: ${image}`, () => {}); + return false; + } + let containerId: string; + { + const envs = Object.entries(env).map(([k, v]) => `-e${k}=${v}`); + const { exitCode, stdout, stderr } = Bun.spawnSync({ + cmd: [docker, "run", "--rm", "-dPit", ...envs, image, ...args], + stdout: "pipe", + stderr: "pipe", + }); + if (exitCode !== 0) { + process.stderr.write(stderr); + test.skip(`docker container for ${image} failed to start`, () => {}); + return false; + } + containerId = stdout.toString("utf-8").trim(); + } + let port: number; + { + const { exitCode, stdout, stderr } = Bun.spawnSync({ + cmd: [docker, "port", containerId], + stdout: "pipe", + stderr: "pipe", + }); + if (exitCode !== 0) { + process.stderr.write(stderr); + test.skip(`docker container for ${image} failed to find a port`, () => {}); + return false; + } + const [firstPort] = stdout + .toString("utf-8") + .trim() + .split("\n") + .map(line => parseInt(line.split(":").pop()!)); + port = firstPort; + } + beforeAll(async () => { + await waitForPort(port); + }); + afterAll(() => { + Bun.spawnSync({ + cmd: [docker, "rm", "-f", containerId], + stdout: "ignore", + stderr: "ignore", + }); + }); + fn(port); + }); +} diff --git a/test/integration/mysql2/mysql2.test.ts b/test/integration/mysql2/mysql2.test.ts new file mode 100644 index 0000000000..cb0ef919f6 --- /dev/null +++ b/test/integration/mysql2/mysql2.test.ts @@ -0,0 +1,63 @@ +import { test, expect } from "bun:test"; +import { describeWithContainer } from "harness"; +import type { Connection, ConnectionOptions } from "mysql2/promise"; +import { createConnection } from "mysql2/promise"; + +const tests: { + label: string; + database: { + image: string; + env?: Record; + }; + client: ConnectionOptions; +}[] = [ + { + label: "mysql:8 with root user and password", + database: { + image: "mysql:8", + env: { + MYSQL_ROOT_PASSWORD: "bun", + }, + }, + client: { + user: "root", + password: "bun", + }, + }, + { + label: "mysql:8 with root user and empty password", + database: { + image: "mysql:8", + env: { + MYSQL_ALLOW_EMPTY_PASSWORD: "yes", + }, + }, + client: { + user: "root", + password: "", + }, + }, +]; + +for (const { label, client, database } of tests) { + describeWithContainer(label, database, (port: number) => { + let sql: Connection; + test("can connect to database", async () => { + sql = await createConnection({ + ...client, + port, + }); + }); + test("can query database", async () => { + const result = await sql?.query("SELECT 1"); + expect(result).toBeArrayOfSize(2); + const [rows, fields] = result; + expect(rows).toBeArrayOfSize(1); + const [row] = rows as any[]; + expect(row).toMatchObject({ "1": 1 }); + }); + test("can close database", async () => { + await sql?.end(); + }); + }); +} diff --git a/test/js/bun/console/console-iterator.test.ts b/test/js/bun/console/console-iterator.test.ts index ba20d74bda..6c10625440 100644 --- a/test/js/bun/console/console-iterator.test.ts +++ b/test/js/bun/console/console-iterator.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { spawnSync, spawn } from "bun"; import { describe, expect, it } from "bun:test"; import { bunExe } from "harness"; diff --git a/test/js/bun/plugin/muh.ts b/test/js/bun/plugin/muh.ts deleted file mode 100644 index d5fbbcd864..0000000000 --- a/test/js/bun/plugin/muh.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { plugin } from "bun"; - -await plugin({ - name: "svelte loader", - async setup(builder) { - var { compile } = await import("svelte/compiler"); - var { readFileSync } = await import("fs"); - await 2; - console.log(1); - builder.onLoad({ filter: /\.svelte$/ }, ({ path }) => { - console.log(2); - return { - contents: compile(readFileSync(path, "utf8"), { - filename: path, - generate: "ssr", - }).js.code, - loader: "js", - }; - }); - await 1; - }, -}); - -console.log(require("./hello.svelte")); diff --git a/test/js/bun/shell/leak.test.ts b/test/js/bun/shell/leak.test.ts index dd6979089d..2d38686cbe 100644 --- a/test/js/bun/shell/leak.test.ts +++ b/test/js/bun/shell/leak.test.ts @@ -4,7 +4,7 @@ import { $ } from "bun"; import { describe, expect, test } from "bun:test"; import { bunEnv } from "harness"; import { appendFileSync, closeSync, openSync, writeFileSync } from "node:fs"; -import { tmpdir } from "os"; +import { tmpdir, devNull } from "os"; import { join } from "path"; import { TestBuilder } from "./util"; @@ -55,13 +55,13 @@ describe("fd leak", () => { await builder().quiet().run(); } - const baseline = openSync("/dev/null", "r"); + const baseline = openSync(devNull, "r"); closeSync(baseline); for (let i = 0; i < runs; i++) { await builder().quiet().run(); } - const fd = openSync("/dev/null", "r"); + const fd = openSync(devNull, "r"); closeSync(fd); expect(fd).toBe(baseline); }, 100_000); diff --git a/test/js/bun/spawn/spawn-streaming-stdin.test.ts b/test/js/bun/spawn/spawn-streaming-stdin.test.ts index 1f9d804733..10f8c0d65f 100644 --- a/test/js/bun/spawn/spawn-streaming-stdin.test.ts +++ b/test/js/bun/spawn/spawn-streaming-stdin.test.ts @@ -3,13 +3,13 @@ import { it, test, expect } from "bun:test"; import { spawn } from "bun"; import { bunExe, bunEnv, gcTick } from "harness"; import { closeSync, openSync } from "fs"; -import { tmpdir } from "node:os"; +import { tmpdir, devNull } from "node:os"; import { join } from "path"; import { unlinkSync } from "node:fs"; const N = 100; test("spawn can write to stdin multiple chunks", async () => { - const maxFD = openSync("/dev/null", "w"); + const maxFD = openSync(devNull, "w"); for (let i = 0; i < N; i++) { var exited; await (async function () { @@ -59,7 +59,7 @@ test("spawn can write to stdin multiple chunks", async () => { } closeSync(maxFD); - const newMaxFD = openSync("/dev/null", "w"); + const newMaxFD = openSync(devNull, "w"); closeSync(newMaxFD); // assert we didn't leak any file descriptors diff --git a/test/js/bun/spawn/spawn-streaming-stdout.test.ts b/test/js/bun/spawn/spawn-streaming-stdout.test.ts index 7fc7161661..7947ff9170 100644 --- a/test/js/bun/spawn/spawn-streaming-stdout.test.ts +++ b/test/js/bun/spawn/spawn-streaming-stdout.test.ts @@ -3,6 +3,7 @@ import { it, test, expect } from "bun:test"; import { spawn } from "bun"; import { bunExe, bunEnv, gcTick } from "harness"; import { closeSync, openSync } from "fs"; +import { devNull } from "os"; test("spawn can read from stdout multiple chunks", async () => { gcTick(true); @@ -35,11 +36,11 @@ test("spawn can read from stdout multiple chunks", async () => { await proc.exited; })(); if (maxFD === -1) { - maxFD = openSync("/dev/null", "w"); + maxFD = openSync(devNull, "w"); closeSync(maxFD); } } - const newMaxFD = openSync("/dev/null", "w"); + const newMaxFD = openSync(devNull, "w"); closeSync(newMaxFD); expect(newMaxFD).toBe(maxFD); }, 60_000); diff --git a/test/js/bun/sqlite/sqlite.test.js b/test/js/bun/sqlite/sqlite.test.js index 2d2c643d04..79a8514f71 100644 --- a/test/js/bun/sqlite/sqlite.test.js +++ b/test/js/bun/sqlite/sqlite.test.js @@ -669,3 +669,77 @@ it("empty blob", () => { }, ]); }); + +it("multiple statements with a schema change", () => { + const db = new Database(":memory:"); + db.run( + ` + CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); + CREATE TABLE bar (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); + + INSERT INTO foo (name) VALUES ('foo'); + INSERT INTO foo (name) VALUES ('bar'); + + INSERT INTO bar (name) VALUES ('foo'); + INSERT INTO bar (name) VALUES ('bar'); + `, + ); + + expect(db.query("SELECT * FROM foo").all()).toEqual([ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]); + + expect(db.query("SELECT * FROM bar").all()).toEqual([ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]); +}); + +it("multiple statements", () => { + const fixtures = [ + "INSERT INTO foo (name) VALUES ('foo')", + "INSERT INTO foo (name) VALUES ('barabc')", + "INSERT INTO foo (name) VALUES ('!bazaspdok')", + ]; + for (let separator of [";", ";\n", "\n;", "\r\n;", ";\r\n", ";\t", "\t;", "\r\n;"]) { + for (let spaceOffset of [1, 0, -1]) { + for (let spacesCount = 0; spacesCount < 8; spacesCount++) { + const db = new Database(":memory:"); + db.run("CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); + + const prefix = spaceOffset < 0 ? " ".repeat(spacesCount) : ""; + const suffix = spaceOffset > 0 ? " ".repeat(spacesCount) : ""; + const query = fixtures.join(prefix + separator + suffix); + db.run(query); + + expect(db.query("SELECT * FROM foo").all()).toEqual([ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "barabc", + }, + { + id: 3, + name: "!bazaspdok", + }, + ]); + } + } + } +}); diff --git a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap index 82555bf5c5..5bf59a64d0 100644 --- a/test/js/bun/util/__snapshots__/inspect-error.test.js.snap +++ b/test/js/bun/util/__snapshots__/inspect-error.test.js.snap @@ -23,15 +23,15 @@ error: error 1 `; exports[`Error 1`] = ` -" 6 | const err2 = new Error("error 2", { cause: err }); - 7 | expect(Bun.inspect(err2).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); - 8 | }); - 9 | -10 | test("Error", () => { -11 | const err = new Error("my message"); +"10 | .replaceAll("//", "/"), +11 | ).toMatchSnapshot(); +12 | }); +13 | +14 | test("Error", () => { +15 | const err = new Error("my message"); ^ error: my message - at [dir]/inspect-error.test.js:11:15 + at [dir]/inspect-error.test.js:15:15 " `; diff --git a/test/js/bun/util/bun-file-exists.test.js b/test/js/bun/util/bun-file-exists.test.js index 603c7adf34..cca28e3599 100644 --- a/test/js/bun/util/bun-file-exists.test.js +++ b/test/js/bun/util/bun-file-exists.test.js @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { test, expect } from "bun:test"; import { join } from "path"; import { tmpdir } from "os"; diff --git a/test/js/bun/util/bun-file-windows.test.ts b/test/js/bun/util/bun-file-windows.test.ts new file mode 100644 index 0000000000..cdbf5ab71a --- /dev/null +++ b/test/js/bun/util/bun-file-windows.test.ts @@ -0,0 +1,16 @@ +import { openSync, closeSync } from "fs"; +import { open } from "fs/promises"; + +test('Bun.file("/dev/null") works on windows', async () => { + expect(await Bun.file("/dev/null").arrayBuffer()).toHaveLength(0); +}); + +test('openSync("/dev/null") works on windows', async () => { + const handle = openSync("/dev/null", "r"); + closeSync(handle); +}); + +test('open("/dev/null") works on windows', async () => { + const handle = await open("/dev/null", "r"); + await handle.close(); +}); diff --git a/test/js/bun/util/bun-isMainThread.test.js b/test/js/bun/util/bun-isMainThread.test.js index 46fecbc743..87f74d136c 100644 --- a/test/js/bun/util/bun-isMainThread.test.js +++ b/test/js/bun/util/bun-isMainThread.test.js @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { test, expect } from "bun:test"; import { bunEnv, bunExe } from "harness"; diff --git a/test/js/bun/util/inspect-error.test.js b/test/js/bun/util/inspect-error.test.js index f9f8feb701..31f8d79f74 100644 --- a/test/js/bun/util/inspect-error.test.js +++ b/test/js/bun/util/inspect-error.test.js @@ -1,15 +1,22 @@ -// @known-failing-on-windows: 1 failing import { test, expect } from "bun:test"; test("error.cause", () => { const err = new Error("error 1"); const err2 = new Error("error 2", { cause: err }); - expect(Bun.inspect(err2).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); + expect( + Bun.inspect(err2) + .replaceAll(import.meta.dir, "[dir]") + .replaceAll("\\", "/"), + ).toMatchSnapshot(); }); test("Error", () => { const err = new Error("my message"); - expect(Bun.inspect(err).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); + expect( + Bun.inspect(err) + .replaceAll(import.meta.dir, "[dir]") + .replaceAll("\\", "/"), + ).toMatchSnapshot(); }); test("BuildMessage", async () => { @@ -17,6 +24,10 @@ test("BuildMessage", async () => { await import("./inspect-error-fixture-bad.js"); expect.unreachable(); } catch (e) { - expect(Bun.inspect(e).replaceAll(import.meta.dir, "[dir]")).toMatchSnapshot(); + expect( + Bun.inspect(e) + .replaceAll(import.meta.dir, "[dir]") + .replaceAll("\\", "/"), + ).toMatchSnapshot(); } }); diff --git a/test/js/bun/util/reportError.test.ts b/test/js/bun/util/reportError.test.ts index 223e0f626a..f6520b90b7 100644 --- a/test/js/bun/util/reportError.test.ts +++ b/test/js/bun/util/reportError.test.ts @@ -1,12 +1,12 @@ -// @known-failing-on-windows: 1 failing import { test, expect } from "bun:test"; import { spawnSync } from "bun"; +import { join } from "path"; import { bunEnv, bunExe } from "harness"; test("reportError", () => { const cwd = import.meta.dir; const { stderr } = spawnSync({ - cmd: [bunExe(), new URL("./reportError.ts", import.meta.url).pathname], + cmd: [bunExe(), join(import.meta.dir, "reportError.ts")], cwd, env: { ...bunEnv, @@ -14,6 +14,6 @@ test("reportError", () => { BUN_JSC_showPrivateScriptsInStackTraces: "0", }, }); - const output = stderr.toString().replaceAll(cwd, ""); + const output = stderr.toString().replaceAll(cwd, "").replaceAll("\\", "/"); expect(output).toMatchSnapshot(); }); diff --git a/test/js/bun/util/text-loader.test.ts b/test/js/bun/util/text-loader.test.ts index a081b6daa9..f7d3b2a284 100644 --- a/test/js/bun/util/text-loader.test.ts +++ b/test/js/bun/util/text-loader.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { spawnSync } from "bun"; import { describe, expect, it } from "bun:test"; import { bunEnv, bunExe } from "harness"; diff --git a/test/js/first_party/ws/ws.test.ts b/test/js/first_party/ws/ws.test.ts index 778c18f9e3..6c8334f115 100644 --- a/test/js/first_party/ws/ws.test.ts +++ b/test/js/first_party/ws/ws.test.ts @@ -1,9 +1,9 @@ -// @known-failing-on-windows: 1 failing import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import type { Subprocess } from "bun"; import { spawn } from "bun"; import { bunEnv, bunExe, nodeExe } from "harness"; import { Server, WebSocket, WebSocketServer } from "ws"; +import path from "node:path"; const strings = [ { @@ -362,7 +362,7 @@ function test(label: string, fn: (ws: WebSocket, done: (err?: unknown) => void) } async function listen(): Promise { - const { pathname } = new URL("../../web/websocket/websocket-server-echo.mjs", import.meta.url); + const pathname = path.resolve(import.meta.dir, "../../web/websocket/websocket-server-echo.mjs"); const server = spawn({ cmd: [nodeExe() ?? bunExe(), pathname], cwd: import.meta.dir, diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index a6d067acb7..ee32d73218 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { Buffer, SlowBuffer, isAscii, isUtf8 } from "buffer"; import { describe, it, expect, beforeEach, afterEach } from "bun:test"; import { gc } from "harness"; diff --git a/test/js/node/env-windows.test.ts b/test/js/node/env-windows.test.ts new file mode 100644 index 0000000000..36ddd41d98 --- /dev/null +++ b/test/js/node/env-windows.test.ts @@ -0,0 +1,33 @@ +import { test, expect } from "bun:test"; + +test.if(process.platform === "win32")("process.env is case insensitive on windows", () => { + const keys = Object.keys(process.env); + // this should have at least one character that is lowercase + // it is likely that PATH will be 'Path', and also stuff like 'WindowsLibPath' and so on. + // but not guaranteed, so we just check that there is at least one of each case + expect( + keys + .join("") + .split("") + .some(c => c.toUpperCase() !== c), + ).toBe(true); + expect( + keys + .join("") + .split("") + .some(c => c.toLowerCase() !== c), + ).toBe(true); + expect(process.env.path).toBe(process.env.PATH!); + expect(process.env.pAtH).toBe(process.env.PATH!); + + expect(process.env.doesntexistahahahahaha).toBeUndefined(); + // @ts-expect-error + process.env.doesntExistAHaHaHaHaHa = true; + expect(process.env.doesntexistahahahahaha).toBe("true"); + expect(process.env.doesntexistahahahahaha).toBe("true"); + expect(process.env.doesnteXISTahahahahaha).toBe("true"); + expect(Object.keys(process.env).pop()).toBe("doesntExistAHaHaHaHaHa"); + delete process.env.DOESNTEXISTAHAHAHAHAHA; + expect(process.env.doesntexistahahahahaha).toBeUndefined(); + expect(Object.keys(process.env)).not.toInclude("doesntExistAHaHaHaHaHa"); +}); diff --git a/test/js/node/events/event-emitter.test.ts b/test/js/node/events/event-emitter.test.ts index c18fea561b..687e90910e 100644 --- a/test/js/node/events/event-emitter.test.ts +++ b/test/js/node/events/event-emitter.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { test, describe, expect } from "bun:test"; import { sleep } from "bun"; import { createRequire } from "module"; diff --git a/test/js/node/fs/fs-stream.link.js b/test/js/node/fs/fs-stream.link.js deleted file mode 120000 index 0cadae0e54..0000000000 --- a/test/js/node/fs/fs-stream.link.js +++ /dev/null @@ -1 +0,0 @@ -./test/bun.js/fs-stream.js \ No newline at end of file diff --git a/test/js/node/http/node-http.test.ts b/test/js/node/http/node-http.test.ts index 4e84b1f6de..a8e2372211 100644 --- a/test/js/node/http/node-http.test.ts +++ b/test/js/node/http/node-http.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing // @ts-nocheck import { createServer, diff --git a/test/js/node/process/process-args.test.js b/test/js/node/process/process-args.test.js index cb9bda00d0..c95d2e22b6 100644 --- a/test/js/node/process/process-args.test.js +++ b/test/js/node/process/process-args.test.js @@ -1,11 +1,11 @@ -// @known-failing-on-windows: 1 failing import { spawn } from "bun"; import { test, expect } from "bun:test"; +import { join } from "path"; import { bunExe } from "harness"; test("args exclude run", async () => { const arg0 = process.argv[0]; - const arg1 = import.meta.dir + "/print-process-args.js"; + const arg1 = join(import.meta.dir, "/print-process-args.js"); const exe = bunExe(); const { stdout: s1 } = spawn([exe, "print-process-args.js"], { cwd: import.meta.dir, diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index d2d18b4dbb..ffdd7ab269 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -9,13 +9,14 @@ it("process", () => { // this property isn't implemented yet but it should at least return a string const isNode = !process.isBun; - if (!isNode && process.title !== "bun") throw new Error("process.title is not 'bun'"); + if (!isNode && process.platform !== "win32" && process.title !== "bun") throw new Error("process.title is not 'bun'"); if (typeof process.env.USER !== "string") throw new Error("process.env is not an object"); if (process.env.USER.length === 0) throw new Error("process.env is missing a USER property"); - if (process.platform !== "darwin" && process.platform !== "linux") throw new Error("process.platform is invalid"); + if (process.platform !== "darwin" && process.platform !== "linux" && process.platform !== "win32") + throw new Error("process.platform is invalid"); if (isNode) throw new Error("process.isBun is invalid"); @@ -68,8 +69,9 @@ it("process.hrtime.bigint()", () => { it("process.release", () => { expect(process.release.name).toBe("node"); + const platform = process.platform == "win32" ? "windows" : process.platform; expect(process.release.sourceUrl).toContain( - `https://github.com/oven-sh/bun/release/bun-v${process.versions.bun}/bun-${process.platform}-${ + `https://github.com/oven-sh/bun/release/bun-v${process.versions.bun}/bun-${platform}-${ { arm64: "aarch64", x64: "x64" }[process.arch] || process.arch }`, ); @@ -155,11 +157,16 @@ it("process.umask()", () => { }).toThrow(RangeError); } - const orig = process.umask(0o777); - expect(orig).toBeGreaterThan(0); - expect(process.umask()).toBe(0o777); - expect(process.umask(undefined)).toBe(0o777); - expect(process.umask(Number(orig))).toBe(0o777); + const mask = process.platform == "win32" ? 0o600 : 0o777; + const orig = process.umask(mask); + if (process.platform == "win32") { + expect(orig).toBe(0); + } else { + expect(orig).toBeGreaterThan(0); + } + expect(process.umask()).toBe(mask); + expect(process.umask(undefined)).toBe(mask); + expect(process.umask(Number(orig))).toBe(mask); expect(process.umask()).toBe(orig); }); diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index f00b401c5b..3fd164a889 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import wt, { getEnvironmentData, isMainThread, diff --git a/test/js/third_party/comlink/comlink.test.ts b/test/js/third_party/comlink/comlink.test.ts index f3b87ea01f..77d8109504 100644 --- a/test/js/third_party/comlink/comlink.test.ts +++ b/test/js/third_party/comlink/comlink.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { test, expect, describe } from "bun:test"; import { join } from "path"; import * as Comlink from "comlink"; diff --git a/test/js/third_party/resvg/bbox.test.js b/test/js/third_party/resvg/bbox.test.js index 2932edcc7e..0c8a195781 100644 --- a/test/js/third_party/resvg/bbox.test.js +++ b/test/js/third_party/resvg/bbox.test.js @@ -1,4 +1,3 @@ -// @known-failing-on-windows: code 3765269347 import { test, expect } from "bun:test"; import { Resvg } from "@resvg/resvg-js"; diff --git a/test/js/third_party/rollup-v4/rollup-v4.test.ts b/test/js/third_party/rollup-v4/rollup-v4.test.ts index 3c8ff062f8..9aef2c0275 100644 --- a/test/js/third_party/rollup-v4/rollup-v4.test.ts +++ b/test/js/third_party/rollup-v4/rollup-v4.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { parseAst } from "rollup/parseAst"; test("it works", () => { diff --git a/test/js/third_party/socket.io/socket.io-close.test.ts b/test/js/third_party/socket.io/socket.io-close.test.ts index bef50f0243..feac0fbad1 100644 --- a/test/js/third_party/socket.io/socket.io-close.test.ts +++ b/test/js/third_party/socket.io/socket.io-close.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { describe, it, expect } from "bun:test"; import { io as ioc } from "socket.io-client"; import { join } from "path"; diff --git a/test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts b/test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts index 6677d29633..02a5271962 100644 --- a/test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts +++ b/test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { describe, it, expect } from "bun:test"; import { Server, Socket } from "socket.io"; diff --git a/test/js/web/workers/message-channel.test.ts b/test/js/web/workers/message-channel.test.ts index 092d8f31b0..033343b34f 100644 --- a/test/js/web/workers/message-channel.test.ts +++ b/test/js/web/workers/message-channel.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing test("simple usage", done => { const channel = new MessageChannel(); const port1 = channel.port1; diff --git a/test/package.json b/test/package.json index 7f2c75454a..dcd4d721c8 100644 --- a/test/package.json +++ b/test/package.json @@ -28,6 +28,7 @@ "lodash": "4.17.21", "mongodb": "6.0.0", "msgpackr-extract": "3.0.2", + "mysql2": "3.7.0", "node-gyp": "10.0.1", "nodemailer": "6.9.3", "pg": "8.11.1", diff --git a/test/regression/issue/07500.test.ts b/test/regression/issue/07500.test.ts index d7434d40d7..20ac53f761 100644 --- a/test/regression/issue/07500.test.ts +++ b/test/regression/issue/07500.test.ts @@ -4,24 +4,30 @@ import { bunEnv, bunExe } from "harness"; import { tmpdir } from "os"; import { join } from "path"; test("7500 - Bun.stdin.text() doesn't read all data", async () => { - const filename = join(tmpdir(), "/bun.test.offset.txt"); + const filename = join(tmpdir(), "bun.test.offset." + Date.now() + ".txt"); const text = "contents of file to be read with several lines of text and lots and lots and lots and lots of bytes! " .repeat(1000) .repeat(9) .split(" ") .join("\n"); await Bun.write(filename, text); - const bunCommand = `${bunExe()} ${join(import.meta.dir, "7500-repro-fixture.js")}`; const shellCommand = `cat ${filename} | ${bunCommand}`; + + const cmd = process.platform === "win32" ? ["pwsh.exe", `-Command='${shellCommand}'`] : ["bash", "-c", shellCommand]; const proc = Bun.spawnSync({ - cmd: ["bash", "-c", shellCommand], + cmd, stdin: "inherit", stdout: "pipe", stderr: "inherit", env: bunEnv, }); - const output = proc.stdout.toString(); - expect(output).toBe(text); + + const output = proc.stdout.toString().replaceAll("\r\n", "\n"); + if (output !== text) { + expect(output).toHaveLength(text.length); + throw new Error("Output didn't match!\n"); + } + expect(proc.exitCode).toBe(0); }, 100000); diff --git a/test/snippets/package.json b/test/snippets/package.json index 478234d5c0..0c05b97bea 100644 --- a/test/snippets/package.json +++ b/test/snippets/package.json @@ -10,6 +10,5 @@ "react-dom": "^17.0.2", "redux": "^4.1.1", "styled-components": "^5.3.1" - }, - "prettier": "../../.prettierrc.cjs" + } }