From 626a6640f86583025cf936187a613dd8faa16e58 Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 15 Mar 2026 11:04:44 +0100 Subject: [PATCH] feat: proper windows support --- MODULE.bazel.lock | 12 +- README.md | 30 +- bun/extensions.bzl | 2 +- docs/bun_install.md | 20 + docs/index.md | 7 + docs/rules.md | 32 +- internal/BUILD.bazel | 14 + internal/bun_binary.bzl | 174 +-- internal/bun_build_support.bzl | 94 ++ internal/bun_bundle.bzl | 31 +- internal/bun_command.bzl | 29 + internal/bun_compile.bzl | 120 +- internal/bun_dev.bzl | 292 ++-- internal/bun_install.bzl | 12 +- internal/bun_script.bzl | 251 ++-- internal/bun_test.bzl | 336 +++-- internal/js_run_devserver.bzl | 130 +- internal/runtime_launcher.bzl | 173 +++ internal/runtime_launcher.js | 1199 +++++++++++++++++ internal/workspace.bzl | 743 +--------- tests/binary_test/BUILD.bazel | 66 +- tests/binary_test/path_probe.ts | 5 + tests/binary_test/run_binary.sh | 18 +- tests/binary_test/run_env_binary.sh | 18 +- tests/binary_test/run_flag_binary.sh | 18 +- tests/binary_test/run_parent_env_binary.sh | 18 +- tests/binary_test/run_path_binary.sh | 33 + .../verify_configured_launcher_shape.sh | 31 +- .../binary_test/verify_runtime_flags_shape.sh | 20 +- tests/bun_test_test/BUILD.bazel | 70 +- tests/bun_test_test/cache_hit_shape.sh | 18 +- tests/bun_test_test/cache_miss_shape.sh | 20 +- tests/bun_test_test/configured_suite_shape.sh | 54 +- tests/bun_test_test/junit_shape.sh | 16 +- tests/bundle_test/BUILD.bazel | 142 +- tests/bundle_test/collision_case/a/main.ts | 1 + tests/bundle_test/collision_case/b/main.ts | 1 + tests/bundle_test/verify_collision_outputs.sh | 25 + tests/bundle_test/verify_flag_aquery.sh | 2 - tests/bundle_test/verify_hermetic_shape.sh | 6 +- tests/ci_test/BUILD.bazel | 19 + tests/ci_test/verify_native_wrapper_shape.sh | 17 + tests/install_extension_test/BUILD.bazel | 2 + .../extension_shape_test.sh | 1 + tests/install_test/BUILD.bazel | 59 +- tests/install_test/lifecycle_scripts.sh | 118 ++ tests/install_test/repeatability.sh | 128 ++ tests/install_test/workspaces.sh | 3 + tests/install_test/workspaces_catalog.sh | 3 + tests/integration_test/BUILD.bazel | 16 +- .../examples_basic_hot_restart_shape_test.sh | 24 +- .../examples_basic_run_e2e_test.sh | 20 +- .../examples_vite_monorepo_e2e_test.sh | 20 +- tests/js_compat_test/BUILD.bazel | 17 +- tests/js_compat_test/run_binary.sh | 18 +- tests/js_compat_test/run_devserver.sh | 18 +- .../js_compat_test/verify_workspace_shape.sh | 27 +- tests/library_test/BUILD.bazel | 4 +- tests/npm_compat_test/BUILD.bazel | 3 +- tests/script_test/BUILD.bazel | 118 +- tests/script_test/run_env_script.sh | 18 +- .../run_paraglide_monorepo_builds.sh | 17 +- tests/script_test/run_script.sh | 18 +- tests/script_test/run_vite_app.sh | 20 +- tests/script_test/run_vite_monorepo_apps.sh | 20 +- tests/script_test/run_workspace_parallel.sh | 18 +- tests/script_test/run_workspace_script.sh | 18 +- tests/script_test/verify_launcher_flags.sh | 23 +- .../verify_monorepo_launcher_shape.sh | 27 + tests/toolchain_test/BUILD.bazel | 2 + 70 files changed, 3410 insertions(+), 1689 deletions(-) create mode 100644 internal/runtime_launcher.bzl create mode 100644 internal/runtime_launcher.js create mode 100644 tests/binary_test/path_probe.ts create mode 100755 tests/binary_test/run_path_binary.sh create mode 100644 tests/bundle_test/collision_case/a/main.ts create mode 100644 tests/bundle_test/collision_case/b/main.ts create mode 100755 tests/bundle_test/verify_collision_outputs.sh create mode 100755 tests/ci_test/verify_native_wrapper_shape.sh create mode 100755 tests/install_test/lifecycle_scripts.sh create mode 100755 tests/install_test/repeatability.sh create mode 100755 tests/script_test/verify_monorepo_launcher_shape.sh diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index eb33e2b..5897e4c 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -192,7 +192,7 @@ "moduleExtensions": { "//bun:extensions.bzl%bun": { "general": { - "bzlTransitiveDigest": "lzOUyaXDbkH922ruNkkwEF2cnI4m0XpzrOti0qypwtA=", + "bzlTransitiveDigest": "314UOH4dQIGBHGpxCwA7yzI++E2J3bjIc20m5MZhM7U=", "usagesDigest": "iOEuN6Lhr5Bejh9GRDT3yaCI7umBsNZo5CLQSzt+tNQ=", "recordedInputs": [ "REPO_MAPPING:,bazel_tools bazel_tools" @@ -283,7 +283,7 @@ }, "//bun:extensions.bzl%bun_install": { "general": { - "bzlTransitiveDigest": "lzOUyaXDbkH922ruNkkwEF2cnI4m0XpzrOti0qypwtA=", + "bzlTransitiveDigest": "314UOH4dQIGBHGpxCwA7yzI++E2J3bjIc20m5MZhM7U=", "usagesDigest": "eD/dECSVWIFS/qMgEXZ6501CimKmsHt7yTIFPX4XdLo=", "recordedInputs": [ "REPO_MAPPING:,bazel_tools bazel_tools" @@ -300,7 +300,7 @@ "omit": [], "linker": "", "backend": "", - "ignore_scripts": false, + "ignore_scripts": true, "install_flags": [], "visible_repo_name": "script_test_vite_node_modules" } @@ -316,7 +316,7 @@ "omit": [], "linker": "", "backend": "", - "ignore_scripts": false, + "ignore_scripts": true, "install_flags": [], "visible_repo_name": "script_test_vite_monorepo_node_modules" } @@ -332,7 +332,7 @@ "omit": [], "linker": "", "backend": "", - "ignore_scripts": false, + "ignore_scripts": true, "install_flags": [], "visible_repo_name": "script_test_paraglide_monorepo_node_modules" } @@ -348,7 +348,7 @@ "omit": [], "linker": "", "backend": "", - "ignore_scripts": false, + "ignore_scripts": true, "install_flags": [], "visible_repo_name": "examples_vite_monorepo_node_modules" } diff --git a/README.md b/README.md index 4030ef4..df990b7 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ This repository follows the standard Bazel ruleset layout: The public entrypoint for rule authors and users is `@rules_bun//bun:defs.bzl`. +Runtime launcher targets from `bun_binary`, `bun_script`, `bun_test`, +`bun_dev`, and `js_run_devserver` use native platform wrappers. Windows runtime +support is native and does not require Git Bash or MSYS. + ## Public API `rules_bun` exports these primary rules: @@ -48,6 +52,22 @@ Reference documentation: - `bun_install` extension docs: [docs/bun_install.md](docs/bun_install.md) - Docs index: [docs/index.md](docs/index.md) +## Hermeticity + +`rules_bun` now draws a sharp line between hermetic rule surfaces and local +workflow helpers. + +- Hermetic build/test surfaces: `bun_build`, `bun_bundle`, `bun_compile`, `bun_test` +- Runfiles-only executable surface: `bun_binary` +- Reproducible but non-hermetic repository fetch surface: `bun_install` +- Local workflow helpers: `bun_script`, `bun_dev`, `js_run_devserver` + +Strict defaults are enabled by default: + +- `bun_install` skips lifecycle scripts unless `ignore_scripts = False` +- `bun_build`, `bun_bundle`, `bun_compile`, and `bun_test` require `install_mode = "disable"` +- Runtime launchers do not inherit the host `PATH` unless `inherit_host_path = True` + To refresh generated rule docs: ```bash @@ -104,9 +124,10 @@ bun_install_ext.install( name = "bun_deps", package_json = "//:package.json", bun_lockfile = "//:bun.lock", - # Optional: include extra install-time files or allow Bun to reuse the - # host HOME/cache. + # Optional: include extra install-time files. # install_inputs = ["//:.npmrc"], + # Optional non-hermetic opt-in: + # ignore_scripts = False, # isolated_home = False, ) @@ -198,8 +219,9 @@ bun_script( ``` When `node_modules` is provided, executables from `node_modules/.bin` are added -to `PATH`. This label typically comes from `bun_install`, which still produces a -standard `node_modules/` directory. +to the runtime `PATH`. The host `PATH` is not inherited unless +`inherit_host_path = True`. This label typically comes from `bun_install`, +which still produces a standard `node_modules/` directory. ### `bun_build` and `bun_compile` diff --git a/bun/extensions.bzl b/bun/extensions.bzl index 2009234..3ae581b 100644 --- a/bun/extensions.bzl +++ b/bun/extensions.bzl @@ -81,7 +81,7 @@ _install = tag_class( "omit": attr.string_list(), "linker": attr.string(), "backend": attr.string(), - "ignore_scripts": attr.bool(default = False), + "ignore_scripts": attr.bool(default = True), "install_flags": attr.string_list(), }, ) diff --git a/docs/bun_install.md b/docs/bun_install.md index 209d264..3098b89 100644 --- a/docs/bun_install.md +++ b/docs/bun_install.md @@ -20,6 +20,21 @@ Unlike the build rules in [rules.md](rules.md), `bun_install` is not loaded from The generated repository can then be passed to rules such as `bun_script`, `bun_binary`, `bun_bundle`, and `bun_test`. +## Hermeticity + +`bun_install` is a repository convenience rule, not a hermetic build action. +It is intended to be reproducible from a checked-in lockfile and pinned Bun +toolchain, but it still performs dependency fetching as part of repository +materialization. + +Strict defaults now favor reproducibility: + +- `isolated_home = True` +- `ignore_scripts = True` + +Set `ignore_scripts = False` only when you explicitly want lifecycle scripts to +run during repository creation. + ## Usage ```starlark @@ -150,6 +165,9 @@ Examples include `hardlink`, `symlink`, and `copyfile`. Optional boolean controlling whether Bun skips lifecycle scripts in the project manifest. +- `True` (default): skips lifecycle scripts for stricter, more reproducible installs +- `False`: allows lifecycle scripts to run during repository creation + ### `install_flags` Optional list of additional raw flags forwarded to `bun install`. @@ -164,3 +182,5 @@ Optional list of additional raw flags forwarded to `bun install`. `package.json`. - Additional `install_inputs` must be files under the same package root as the selected `package_json`. +- `bun_install` does not make the install step remotely hermetic; it makes the + generated repository stricter and more reproducible by default. diff --git a/docs/index.md b/docs/index.md index db90505..9d46af5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,6 +17,13 @@ Supporting material lives in: - [docs/rules.md](rules.md) for generated build rule reference - [docs/bun_install.md](bun_install.md) for `bun_install` extension docs +## Hermeticity + +- Hermetic rule surfaces: `bun_build`, `bun_bundle`, `bun_compile`, `bun_test` +- Runfiles-only executable surface: `bun_binary` +- Reproducible but non-hermetic repository surface: `bun_install` +- Local workflow helpers: `bun_script`, `bun_dev`, `js_run_devserver` + ## Rule reference - [rules.md](rules.md) diff --git a/docs/rules.md b/docs/rules.md index e22c7ba..dd0f774 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -2,6 +2,18 @@ Public API surface for Bun Bazel rules. +## Hermeticity And Determinism + +- Hermetic rule surfaces: `bun_build`, `bun_bundle`, `bun_compile`, `bun_test` +- Runfiles-only executable surface: `bun_binary` +- Reproducible but non-hermetic repository surface: `bun_install` +- Local workflow helpers: `bun_script`, `bun_dev`, `js_run_devserver` + +Strict defaults: + +- `bun_build`, `bun_bundle`, `bun_compile`, and `bun_test` require `install_mode = "disable"` +- Runtime launchers do not inherit the host `PATH` unless `inherit_host_path = True` + ## bun_binary @@ -28,7 +40,8 @@ Use this rule for non-test scripts and CLIs that should run via `bazel run`. | conditions | Custom package resolve conditions passed to Bun. | List of strings | optional | `[]` | | entry_point | Path to the main JS/TS file to execute. | Label | required | | | env_files | Additional environment files loaded with `--env-file`. | List of labels | optional | `[]` | -| install_mode | Whether Bun may auto-install missing packages at runtime. | String | optional | `"disable"` | +| install_mode | Whether Bun may auto-install missing packages at runtime. Non-`disable` values are runtime opt-ins and are not hermetic. | String | optional | `"disable"` | +| inherit_host_path | If true, appends the host PATH after staged `node_modules/.bin` entries at runtime. | Boolean | optional | `False` | | no_env_file | If true, disables Bun's automatic `.env` loading. | Boolean | optional | `False` | | node_modules | Optional label providing package files from a `node_modules` tree, typically produced by `bun_install`, in runfiles. | Label | optional | `None` | | preload | Modules to preload with `--preload` before running the entry point. | List of labels | optional | `[]` | @@ -82,7 +95,7 @@ may be requested with `metafile` and `metafile_md`. | feature | Repeated `--feature` values for dead-code elimination. | List of strings | optional | `[]` | | footer | Optional bundle footer text. | String | optional | `""` | | format | Output module format. | String | optional | `"esm"` | -| install_mode | Whether Bun may auto-install missing packages while executing the build. | String | optional | `"disable"` | +| install_mode | Whether Bun may auto-install missing packages while executing the build. Hermetic builds require `\"disable\"`, and other values are rejected. | String | optional | `"disable"` | | jsx_factory | Optional JSX factory override. | String | optional | `""` | | jsx_fragment | Optional JSX fragment override. | String | optional | `""` | | jsx_import_source | Optional JSX import source override. | String | optional | `""` | @@ -135,7 +148,7 @@ Each entry point produces one output JavaScript artifact. | entry_points | Entry files to bundle. | List of labels | required | | | external | Package names to treat as externals (not bundled). | List of strings | optional | `[]` | | format | Output module format. | String | optional | `"esm"` | -| install_mode | Whether Bun may auto-install missing packages during bundling. | String | optional | `"disable"` | +| install_mode | Whether Bun may auto-install missing packages during bundling. Hermetic bundles require `\"disable\"`, and other values are rejected. | String | optional | `"disable"` | | minify | If true, minifies bundle output. | Boolean | optional | `False` | | node_modules | Optional label providing package files from a `node_modules` tree, typically produced by `bun_install`, for package resolution. | Label | optional | `None` | | sourcemap | If true, emits source maps. | Boolean | optional | `False` | @@ -194,7 +207,7 @@ Compiles a Bun program into a standalone executable with `bun build --compile`. | feature | Repeated `--feature` values for dead-code elimination. | List of strings | optional | `[]` | | footer | Optional bundle footer text. | String | optional | `""` | | format | Output module format. | String | optional | `"esm"` | -| install_mode | Whether Bun may auto-install missing packages while executing the build. | String | optional | `"disable"` | +| install_mode | Whether Bun may auto-install missing packages while executing the build. Hermetic compile actions require `\"disable\"`, and other values are rejected. | String | optional | `"disable"` | | jsx_factory | Optional JSX factory override. | String | optional | `""` | | jsx_fragment | Optional JSX fragment override. | String | optional | `""` | | jsx_import_source | Optional JSX import source override. | String | optional | `""` | @@ -251,7 +264,8 @@ watch/HMR plus optional full restarts on selected file changes. | conditions | Custom package resolve conditions passed to Bun. | List of strings | optional | `[]` | | entry_point | Path to the main JS/TS file to execute in dev mode. | Label | required | | | env_files | Additional environment files loaded with `--env-file`. | List of labels | optional | `[]` | -| install_mode | Whether Bun may auto-install missing packages in dev mode. | String | optional | `"disable"` | +| install_mode | Whether Bun may auto-install missing packages in dev mode. This is a local workflow helper, not a hermetic execution surface. | String | optional | `"disable"` | +| inherit_host_path | If true, appends the host PATH after staged `node_modules/.bin` entries at runtime. | Boolean | optional | `False` | | no_clear_screen | If true, disables terminal clearing on Bun reloads. | Boolean | optional | `False` | | no_env_file | If true, disables Bun's automatic `.env` loading. | Boolean | optional | `False` | | node_modules | Optional label providing package files from a `node_modules` tree, typically produced by `bun_install`, in runfiles. | Label | optional | `None` | @@ -294,7 +308,8 @@ declared in `package.json` and expect to run from the package directory with | env_files | Additional environment files loaded with `--env-file`. | List of labels | optional | `[]` | | execution_mode | How Bun should execute matching workspace scripts. | String | optional | `"single"` | | filters | Workspace package filters passed via repeated `--filter` flags. | List of strings | optional | `[]` | -| install_mode | Whether Bun may auto-install missing packages while running the script. | String | optional | `"disable"` | +| install_mode | Whether Bun may auto-install missing packages while running the script. This is a local workflow helper, not a hermetic execution surface. | String | optional | `"disable"` | +| inherit_host_path | If true, appends the host PATH after staged `node_modules/.bin` entries at runtime. | Boolean | optional | `False` | | no_env_file | If true, disables Bun's automatic `.env` loading. | Boolean | optional | `False` | | no_exit_on_error | If true, Bun keeps running other workspace scripts when one fails. | Boolean | optional | `False` | | node_modules | Optional label providing package files from a `node_modules` tree, typically produced by `bun_install`, in runfiles. Executables from `node_modules/.bin` are added to `PATH`, which is useful for scripts such as `vite`. | Label | optional | `None` | @@ -340,7 +355,8 @@ Supports Bazel test filtering (`--test_filter`) and coverage integration. | coverage | If true, always enables Bun coverage output. | Boolean | optional | `False` | | coverage_reporters | Repeated Bun coverage reporters such as `text` or `lcov`. | List of strings | optional | `[]` | | env_files | Additional environment files loaded with `--env-file`. | List of labels | optional | `[]` | -| install_mode | Whether Bun may auto-install missing packages while testing. | String | optional | `"disable"` | +| install_mode | Whether Bun may auto-install missing packages while testing. Hermetic tests require `\"disable\"`, and other values are rejected. | String | optional | `"disable"` | +| inherit_host_path | If true, appends the host PATH after staged `node_modules/.bin` entries at runtime. | Boolean | optional | `False` | | max_concurrency | Optional maximum number of concurrent tests. | Integer | optional | `0` | | no_env_file | If true, disables Bun's automatic `.env` loading. | Boolean | optional | `False` | | node_modules | Optional label providing package files from a `node_modules` tree, typically produced by `bun_install`, in runfiles. | Label | optional | `None` | @@ -411,6 +427,7 @@ the provided tool with any default arguments. | package_dir_hint | Optional package-relative directory hint when package_json is not supplied. | String | optional | `"."` | | package_json | Optional package.json used to resolve the package working directory. | Label | optional | `None` | | tool | Executable target to launch as the dev server. | Label | required | | +| inherit_host_path | If true, appends the host PATH after staged `node_modules/.bin` entries at runtime. | Boolean | optional | `False` | | working_dir | Working directory at runtime: Bazel runfiles workspace root or the resolved package directory. | String | optional | `"workspace"` | @@ -480,4 +497,3 @@ js_test(name, entry_p | entry_point |

-

| `None` | | srcs |

-

| `None` | | kwargs |

-

| none | - diff --git a/internal/BUILD.bazel b/internal/BUILD.bazel index 32cdaa3..eefaae0 100644 --- a/internal/BUILD.bazel +++ b/internal/BUILD.bazel @@ -15,6 +15,8 @@ exports_files([ "js_compat.bzl", "js_library.bzl", "js_run_devserver.bzl", + "runtime_launcher.bzl", + "runtime_launcher.js", "workspace.bzl", ]) @@ -34,6 +36,8 @@ filegroup( "js_compat.bzl", "js_library.bzl", "js_run_devserver.bzl", + "runtime_launcher.bzl", + "runtime_launcher.js", "workspace.bzl", ], visibility = ["//visibility:public"], @@ -59,6 +63,7 @@ bzl_library( deps = [ ":bun_command_bzl", ":js_library_bzl", + ":runtime_launcher_bzl", ":workspace_bzl", ], ) @@ -84,6 +89,7 @@ bzl_library( srcs = ["bun_dev.bzl"], deps = [ ":bun_command_bzl", + ":runtime_launcher_bzl", ":workspace_bzl", ], ) @@ -98,6 +104,7 @@ bzl_library( srcs = ["bun_script.bzl"], deps = [ ":bun_command_bzl", + ":runtime_launcher_bzl", ":workspace_bzl", ], ) @@ -108,6 +115,7 @@ bzl_library( deps = [ ":bun_command_bzl", ":js_library_bzl", + ":runtime_launcher_bzl", ":workspace_bzl", ], ) @@ -133,10 +141,16 @@ bzl_library( srcs = ["js_run_devserver.bzl"], deps = [ ":js_library_bzl", + ":runtime_launcher_bzl", ":workspace_bzl", ], ) +bzl_library( + name = "runtime_launcher_bzl", + srcs = ["runtime_launcher.bzl"], +) + bzl_library( name = "workspace_bzl", srcs = ["workspace.bzl"], diff --git a/internal/bun_binary.bzl b/internal/bun_binary.bzl index d160fca..24951dd 100644 --- a/internal/bun_binary.bzl +++ b/internal/bun_binary.bzl @@ -1,8 +1,9 @@ """Rule for running JS/TS scripts with Bun.""" -load("//internal:bun_command.bzl", "append_shell_flag", "append_shell_flag_files", "append_shell_flag_values", "append_shell_install_mode", "append_shell_raw_flags", "render_shell_array", "shell_quote") +load("//internal:bun_command.bzl", "append_flag", "append_flag_values", "append_install_mode", "append_raw_flags") load("//internal:js_library.bzl", "collect_js_runfiles") -load("//internal:workspace.bzl", "create_bun_workspace_info", "render_workspace_setup", "workspace_runfiles") +load("//internal:runtime_launcher.bzl", "declare_runtime_wrapper", "runfiles_path", "runtime_launcher_attrs", "write_launcher_spec") +load("//internal:workspace.bzl", "create_bun_workspace_info", "workspace_runfiles") def _bun_binary_impl(ctx): toolchain = ctx.toolchains["//bun:toolchain_type"] @@ -15,50 +16,107 @@ def _bun_binary_impl(ctx): primary_file = entry_point, ) - launcher_lines = [render_shell_array("bun_args", ["--bun", "run"])] - append_shell_install_mode(launcher_lines, "bun_args", ctx.attr.install_mode) - append_shell_flag_files(launcher_lines, "bun_args", "--preload", ctx.files.preload) - append_shell_flag_files(launcher_lines, "bun_args", "--env-file", ctx.files.env_files) - append_shell_flag(launcher_lines, "bun_args", "--no-env-file", ctx.attr.no_env_file) - append_shell_flag(launcher_lines, "bun_args", "--smol", ctx.attr.smol) - append_shell_flag_values(launcher_lines, "bun_args", "--conditions", ctx.attr.conditions) - append_shell_raw_flags(launcher_lines, "bun_args", ctx.attr.run_flags) - launcher_lines.append('bun_args+=("${primary_source}")') - for arg in ctx.attr.args: - launcher_lines.append("bun_args+=(%s)" % shell_quote(arg)) + argv = ["--bun", "run"] + append_install_mode(argv, ctx.attr.install_mode) + append_flag(argv, "--no-env-file", ctx.attr.no_env_file) + append_flag(argv, "--smol", ctx.attr.smol) + append_flag_values(argv, "--conditions", ctx.attr.conditions) + append_raw_flags(argv, ctx.attr.run_flags) - command = """ -trap cleanup_runtime_workspace EXIT -cd "${runtime_exec_dir}" -__BUN_ARGS__ -exec "${bun_bin}" "${bun_args[@]}" "$@" -""".replace("__BUN_ARGS__", "\n".join(launcher_lines)) - - launcher = ctx.actions.declare_file(ctx.label.name) - ctx.actions.write( - output = launcher, - is_executable = True, - content = render_workspace_setup( - bun_short_path = bun_bin.short_path, - install_metadata_short_path = workspace_info.install_metadata_file.short_path if workspace_info.install_metadata_file else "", - primary_source_short_path = entry_point.short_path, - working_dir_mode = ctx.attr.working_dir, - ) + command, - ) + spec_file = write_launcher_spec(ctx, { + "version": 1, + "kind": "bun_run", + "bun_short_path": runfiles_path(bun_bin), + "primary_source_short_path": runfiles_path(entry_point), + "package_json_short_path": "", + "install_metadata_short_path": runfiles_path(workspace_info.install_metadata_file) if workspace_info.install_metadata_file else "", + "install_repo_runfiles_path": workspace_info.install_repo_runfiles_path, + "node_modules_roots": workspace_info.node_modules_roots, + "package_dir_hint": workspace_info.package_dir_hint, + "working_dir_mode": ctx.attr.working_dir, + "inherit_host_path": ctx.attr.inherit_host_path, + "argv": argv, + "args": ctx.attr.args, + "passthrough_args": True, + "tool_short_path": "", + "restart_on": [], + "watch_mode": "", + "reporter": "", + "coverage": False, + "coverage_reporters": [], + "preload_short_paths": [runfiles_path(file) for file in ctx.files.preload], + "env_file_short_paths": [runfiles_path(file) for file in ctx.files.env_files], + "test_short_paths": [], + }) + launcher = declare_runtime_wrapper(ctx, bun_bin, spec_file) return [ workspace_info, DefaultInfo( - executable = launcher, + executable = launcher.executable, runfiles = workspace_runfiles( ctx, workspace_info, - direct_files = [launcher], + direct_files = [launcher.executable, launcher.runner, spec_file], transitive_files = dep_runfiles, ), ), ] +_BUN_BINARY_ATTRS = runtime_launcher_attrs() +_BUN_BINARY_ATTRS.update({ + "entry_point": attr.label( + mandatory = True, + allow_single_file = [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"], + doc = "Path to the main JS/TS file to execute.", + ), + "node_modules": attr.label( + doc = "Optional label providing package files from a `node_modules` tree, typically produced by `bun_install`, in runfiles.", + ), + "data": attr.label_list( + allow_files = True, + doc = "Additional runtime files required by the program.", + ), + "deps": attr.label_list( + doc = "Library dependencies required by the program.", + ), + "preload": attr.label_list( + allow_files = True, + doc = "Modules to preload with `--preload` before running the entry point.", + ), + "env_files": attr.label_list( + allow_files = True, + doc = "Additional environment files loaded with `--env-file`.", + ), + "no_env_file": attr.bool( + default = False, + doc = "If true, disables Bun's automatic `.env` loading.", + ), + "smol": attr.bool( + default = False, + doc = "If true, enables Bun's lower-memory runtime mode.", + ), + "conditions": attr.string_list( + doc = "Custom package resolve conditions passed to Bun.", + ), + "install_mode": attr.string( + default = "disable", + values = ["disable", "auto", "fallback", "force"], + doc = "Whether Bun may auto-install missing packages at runtime.", + ), + "run_flags": attr.string_list( + doc = "Additional raw flags forwarded to `bun run` before the entry point.", + ), + "working_dir": attr.string( + default = "workspace", + values = ["workspace", "entry_point"], + doc = "Working directory at runtime: `workspace` root or nearest `entry_point` ancestor containing `.env`/`package.json`.", + ), + "inherit_host_path": attr.bool( + default = False, + doc = "If true, appends the host PATH after staged node_modules/.bin entries at runtime.", + ), +}) bun_binary = rule( implementation = _bun_binary_impl, @@ -66,55 +124,7 @@ bun_binary = rule( Use this rule for non-test scripts and CLIs that should run via `bazel run`. """, - attrs = { - "entry_point": attr.label( - mandatory = True, - allow_single_file = [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"], - doc = "Path to the main JS/TS file to execute.", - ), - "node_modules": attr.label( - doc = "Optional label providing package files from a `node_modules` tree, typically produced by `bun_install`, in runfiles.", - ), - "data": attr.label_list( - allow_files = True, - doc = "Additional runtime files required by the program.", - ), - "deps": attr.label_list( - doc = "Library dependencies required by the program.", - ), - "preload": attr.label_list( - allow_files = True, - doc = "Modules to preload with `--preload` before running the entry point.", - ), - "env_files": attr.label_list( - allow_files = True, - doc = "Additional environment files loaded with `--env-file`.", - ), - "no_env_file": attr.bool( - default = False, - doc = "If true, disables Bun's automatic `.env` loading.", - ), - "smol": attr.bool( - default = False, - doc = "If true, enables Bun's lower-memory runtime mode.", - ), - "conditions": attr.string_list( - doc = "Custom package resolve conditions passed to Bun.", - ), - "install_mode": attr.string( - default = "disable", - values = ["disable", "auto", "fallback", "force"], - doc = "Whether Bun may auto-install missing packages at runtime.", - ), - "run_flags": attr.string_list( - doc = "Additional raw flags forwarded to `bun run` before the entry point.", - ), - "working_dir": attr.string( - default = "workspace", - values = ["workspace", "entry_point"], - doc = "Working directory at runtime: `workspace` root or nearest `entry_point` ancestor containing `.env`/`package.json`.", - ), - }, + attrs = _BUN_BINARY_ATTRS, executable = True, toolchains = ["//bun:toolchain_type"], ) diff --git a/internal/bun_build_support.bzl b/internal/bun_build_support.bzl index fb6b2f8..20cd9b3 100644 --- a/internal/bun_build_support.bzl +++ b/internal/bun_build_support.bzl @@ -3,6 +3,74 @@ load("//internal:bun_command.bzl", "add_flag", "add_flag_value", "add_flag_values", "add_install_mode", "add_raw_flags") load("//internal:js_library.bzl", "collect_js_sources") +_STAGED_BUILD_RUNNER = """import { spawnSync } from "node:child_process"; +import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, resolve } from "node:path"; + +const [, , manifestPath, ...buildArgs] = process.argv; +const execroot = process.cwd(); +const stageDir = mkdtempSync(resolve(tmpdir(), "rules_bun_build-")); + +function rewriteArgPath(flag, value) { + return `${flag}=${resolve(execroot, value)}`; +} + +try { + for (const relpath of readFileSync(manifestPath, "utf8").split(/\\r?\\n/)) { + if (!relpath) { + continue; + } + const src = resolve(execroot, relpath); + const dest = resolve(stageDir, relpath); + mkdirSync(dirname(dest), { recursive: true }); + cpSync(src, dest, { dereference: true, force: true, recursive: true }); + } + + const forwardedArgs = []; + for (let index = 0; index < buildArgs.length; index += 1) { + const arg = buildArgs[index]; + if ((arg === "--outdir" || arg === "--outfile") && index + 1 < buildArgs.length) { + forwardedArgs.push(arg, resolve(execroot, buildArgs[index + 1])); + index += 1; + continue; + } + if (arg.startsWith("--metafile=")) { + forwardedArgs.push(rewriteArgPath("--metafile", arg.slice("--metafile=".length))); + continue; + } + if (arg.startsWith("--metafile-md=")) { + forwardedArgs.push(rewriteArgPath("--metafile-md", arg.slice("--metafile-md=".length))); + continue; + } + forwardedArgs.push(arg); + } + + const result = spawnSync(process.execPath, forwardedArgs, { + cwd: stageDir, + stdio: "inherit", + }); + if (result.error) { + throw result.error; + } + process.exit(typeof result.status === "number" ? result.status : 1); +} finally { + rmSync(stageDir, { recursive: true, force: true }); +} +""" + +def sort_files_by_short_path(files): + files_by_path = {} + short_paths = [] + for file in files: + files_by_path[file.short_path] = file + short_paths.append(file.short_path) + return [files_by_path[short_path] for short_path in sorted(short_paths)] + +def validate_hermetic_install_mode(attr, rule_name): + if getattr(attr, "install_mode", "disable") != "disable": + fail("{} requires install_mode = \"disable\" for hermetic execution".format(rule_name)) + def infer_entry_point_root(entries): if not entries: return None @@ -112,3 +180,29 @@ def add_bun_compile_flags(args, attr, compile_executable = None): add_flag_value(args, "--windows-version", getattr(attr, "windows_version", None)) add_flag_value(args, "--windows-description", getattr(attr, "windows_description", None)) add_flag_value(args, "--windows-copyright", getattr(attr, "windows_copyright", None)) + +def declare_staged_bun_build_action(ctx, bun_bin, build_args, build_inputs, outputs, mnemonic, progress_message, name_suffix): + sorted_inputs = sort_files_by_short_path(build_inputs.to_list()) + input_manifest = ctx.actions.declare_file(ctx.label.name + name_suffix + ".inputs") + runner = ctx.actions.declare_file(ctx.label.name + name_suffix + "_runner.js") + + ctx.actions.write( + output = input_manifest, + content = "".join([file.path + "\n" for file in sorted_inputs]), + ) + ctx.actions.write( + output = runner, + content = _STAGED_BUILD_RUNNER, + ) + + ctx.actions.run( + executable = bun_bin, + arguments = ["--bun", runner.path, input_manifest.path, build_args], + inputs = depset( + direct = [input_manifest, runner], + transitive = [build_inputs], + ), + outputs = outputs, + mnemonic = mnemonic, + progress_message = progress_message, + ) diff --git a/internal/bun_bundle.bzl b/internal/bun_bundle.bzl index e417d0c..54bd07d 100644 --- a/internal/bun_bundle.bzl +++ b/internal/bun_bundle.bzl @@ -1,21 +1,30 @@ """Rule for bundling JS/TS sources with Bun.""" -load("//internal:bun_build_support.bzl", "add_bun_build_common_flags", "bun_build_transitive_inputs") +load("//internal:bun_build_support.bzl", "add_bun_build_common_flags", "bun_build_transitive_inputs", "declare_staged_bun_build_action", "sort_files_by_short_path", "validate_hermetic_install_mode") def _output_name(target_name, entry): - stem = entry.basename.rsplit(".", 1)[0] - return "{}__{}.js".format(target_name, stem) + stem = entry.short_path.rsplit(".", 1)[0] + sanitized = stem.replace("\\", "_").replace("/", "_").replace("-", "_").replace(".", "_").replace("@", "at_") + sanitized = sanitized.replace("__", "_").replace("__", "_").replace("__", "_") + sanitized = sanitized.strip("_") + if not sanitized: + sanitized = entry.basename.rsplit(".", 1)[0] + return "{}__{}.js".format(target_name, sanitized) def _bun_bundle_impl(ctx): + validate_hermetic_install_mode(ctx.attr, "bun_bundle") + toolchain = ctx.toolchains["//bun:toolchain_type"] bun_bin = toolchain.bun.bun_bin + entry_points = sort_files_by_short_path(ctx.files.entry_points) + data_files = sort_files_by_short_path(ctx.files.data) transitive_inputs = bun_build_transitive_inputs(ctx) outputs = [] - for entry in ctx.files.entry_points: + for entry in entry_points: output = ctx.actions.declare_file(_output_name(ctx.label.name, entry)) outputs.append(output) @@ -27,16 +36,18 @@ def _bun_bundle_impl(ctx): args.add(output.path) args.add(entry.path) - ctx.actions.run( - executable = bun_bin, - arguments = [args], - inputs = depset( - direct = [entry] + ctx.files.data, + declare_staged_bun_build_action( + ctx, + bun_bin, + args, + depset( + direct = [entry] + data_files, transitive = transitive_inputs, ), outputs = [output], mnemonic = "BunBundle", progress_message = "Bundling {} with Bun".format(entry.short_path), + name_suffix = "_bundle_{}".format(output.basename.rsplit(".", 1)[0]), ) return [DefaultInfo(files = depset(outputs))] @@ -67,7 +78,7 @@ Each entry point produces one output JavaScript artifact. "install_mode": attr.string( default = "disable", values = ["disable", "auto", "fallback", "force"], - doc = "Whether Bun may auto-install missing packages during bundling.", + doc = "Whether Bun may auto-install missing packages during bundling. Hermetic bundle actions require `disable`; other values are rejected.", ), "target": attr.string( default = "browser", diff --git a/internal/bun_command.bzl b/internal/bun_command.bzl index 46aace5..230aae4 100644 --- a/internal/bun_command.bzl +++ b/internal/bun_command.bzl @@ -82,3 +82,32 @@ def add_install_mode(args, install_mode): args.add("--no-install") elif install_mode in ["fallback", "force"]: add_flag_value(args, "--install", install_mode) + +def append_arg(values, value): + values.append(str(value)) + +def append_flag(values, flag, enabled): + if enabled: + append_arg(values, flag) + +def append_flag_value(values, flag, value): + if value == None: + return + if type(value) == type("") and not value: + return + append_arg(values, flag) + append_arg(values, value) + +def append_flag_values(values, flag, items): + for item in items: + append_flag_value(values, flag, item) + +def append_raw_flags(values, items): + for item in items: + append_arg(values, item) + +def append_install_mode(values, install_mode): + if install_mode == "disable": + append_arg(values, "--no-install") + elif install_mode in ["fallback", "force"]: + append_flag_value(values, "--install", install_mode) diff --git a/internal/bun_compile.bzl b/internal/bun_compile.bzl index 8326274..2b903cd 100644 --- a/internal/bun_compile.bzl +++ b/internal/bun_compile.bzl @@ -1,10 +1,14 @@ """Rules for Bun build outputs and standalone executables.""" -load("//internal:bun_build_support.bzl", "add_bun_build_common_flags", "add_bun_compile_flags", "bun_build_transitive_inputs", "infer_entry_point_root") +load("//internal:bun_build_support.bzl", "add_bun_build_common_flags", "add_bun_compile_flags", "bun_build_transitive_inputs", "declare_staged_bun_build_action", "infer_entry_point_root", "sort_files_by_short_path", "validate_hermetic_install_mode") def _bun_build_impl(ctx): + validate_hermetic_install_mode(ctx.attr, "bun_build") + toolchain = ctx.toolchains["//bun:toolchain_type"] bun_bin = toolchain.bun.bun_bin + entry_points = sort_files_by_short_path(ctx.files.entry_points) + data_files = sort_files_by_short_path(ctx.files.data) output_dir = ctx.actions.declare_directory(ctx.label.name) metafile = None if ctx.attr.metafile: @@ -14,24 +18,20 @@ def _bun_build_impl(ctx): metafile_md = ctx.actions.declare_file(ctx.label.name + ".meta.md") build_root = ctx.attr.root if not build_root: - build_root = infer_entry_point_root(ctx.files.entry_points) + build_root = infer_entry_point_root(entry_points) transitive_inputs = bun_build_transitive_inputs(ctx) build_inputs = depset( - direct = ctx.files.entry_points + ctx.files.data, + direct = entry_points + data_files, transitive = transitive_inputs, ) - input_manifest = ctx.actions.declare_file(ctx.label.name + ".inputs") - runner = ctx.actions.declare_file(ctx.label.name + "_runner.sh") - args = ctx.actions.args() - args.add(input_manifest.path) - args.add(bun_bin.path) - args.add("--bun") - args.add("build") - add_bun_build_common_flags(args, ctx.attr, metafile = metafile, metafile_md = metafile_md, root = build_root) - args.add("--outdir") - args.add(output_dir.path) - args.add_all(ctx.files.entry_points) + build_args = ctx.actions.args() + build_args.add("--bun") + build_args.add("build") + add_bun_build_common_flags(build_args, ctx.attr, metafile = metafile, metafile_md = metafile_md, root = build_root) + build_args.add("--outdir") + build_args.add(output_dir.path) + build_args.add_all(entry_points) outputs = [output_dir] if metafile: @@ -39,87 +39,27 @@ def _bun_build_impl(ctx): if metafile_md: outputs.append(metafile_md) - ctx.actions.write( - output = input_manifest, - content = "".join([file.path + "\n" for file in build_inputs.to_list()]), - ) - - ctx.actions.write( - output = runner, - is_executable = True, - content = """#!/usr/bin/env bash -set -euo pipefail - -manifest="$1" -execroot="$(pwd -P)" -bun_bin="$2" -if [[ "${bun_bin}" != /* ]]; then - bun_bin="${execroot}/${bun_bin}" -fi -shift 2 - -stage_dir="$(mktemp -d "${TMPDIR:-/tmp}/rules_bun_build.XXXXXX")" -cleanup() { - rm -rf "${stage_dir}" -} -trap cleanup EXIT - -while IFS= read -r relpath; do - if [[ -z "${relpath}" ]]; then - continue - fi - src="${execroot}/${relpath}" - dest="${stage_dir}/${relpath}" - mkdir -p "$(dirname "${dest}")" - cp -L "${src}" "${dest}" -done < "${manifest}" - -forwarded_args=() -while (($#)); do - case "$1" in - --outdir) - forwarded_args+=("$1" "${execroot}/$2") - shift 2 - ;; - --metafile=*) - forwarded_args+=("--metafile=${execroot}/${1#--metafile=}") - shift - ;; - --metafile-md=*) - forwarded_args+=("--metafile-md=${execroot}/${1#--metafile-md=}") - shift - ;; - *) - forwarded_args+=("$1") - shift - ;; - esac -done - -cd "${stage_dir}" -exec "${bun_bin}" "${forwarded_args[@]}" -""", - ) - - ctx.actions.run( - executable = runner, - arguments = [args], - inputs = depset( - direct = [input_manifest, bun_bin], - transitive = [build_inputs], - ), + declare_staged_bun_build_action( + ctx, + bun_bin, + build_args, + build_inputs, outputs = outputs, mnemonic = "BunBuild", progress_message = "Building {} with Bun".format(ctx.label.name), + name_suffix = "_build", ) return [DefaultInfo(files = depset(outputs))] def _bun_compile_impl(ctx): + validate_hermetic_install_mode(ctx.attr, "bun_compile") + toolchain = ctx.toolchains["//bun:toolchain_type"] bun_bin = toolchain.bun.bun_bin output = ctx.actions.declare_file(ctx.label.name) compile_executable = ctx.file.compile_executable + data_files = sort_files_by_short_path(ctx.files.data) args = ctx.actions.args() args.add("--bun") @@ -130,20 +70,22 @@ def _bun_compile_impl(ctx): args.add(output.path) args.add(ctx.file.entry_point.path) - direct_inputs = [ctx.file.entry_point] + ctx.files.data + direct_inputs = [ctx.file.entry_point] + data_files if compile_executable: direct_inputs.append(compile_executable) - ctx.actions.run( - executable = bun_bin, - arguments = [args], - inputs = depset( + declare_staged_bun_build_action( + ctx, + bun_bin, + args, + depset( direct = direct_inputs, transitive = bun_build_transitive_inputs(ctx), ), outputs = [output], mnemonic = "BunCompile", progress_message = "Compiling {} with Bun".format(ctx.file.entry_point.short_path), + name_suffix = "_compile", ) return [ @@ -167,7 +109,7 @@ _COMMON_BUILD_ATTRS = { "install_mode": attr.string( default = "disable", values = ["disable", "auto", "fallback", "force"], - doc = "Whether Bun may auto-install missing packages while executing the build.", + doc = "Whether Bun may auto-install missing packages while executing the build. Hermetic build actions require `disable`; other values are rejected.", ), "target": attr.string( default = "browser", diff --git a/internal/bun_dev.bzl b/internal/bun_dev.bzl index edddbb0..10ffbd6 100644 --- a/internal/bun_dev.bzl +++ b/internal/bun_dev.bzl @@ -1,7 +1,8 @@ """Rule for running JS/TS scripts with Bun in watch mode for development.""" -load("//internal:bun_command.bzl", "append_shell_flag", "append_shell_flag_files", "append_shell_flag_values", "append_shell_install_mode", "append_shell_raw_flags", "render_shell_array", "shell_quote") -load("//internal:workspace.bzl", "create_bun_workspace_info", "render_workspace_setup", "workspace_runfiles") +load("//internal:bun_command.bzl", "append_flag", "append_flag_values", "append_install_mode", "append_raw_flags") +load("//internal:runtime_launcher.bzl", "declare_runtime_wrapper", "runfiles_path", "runtime_launcher_attrs", "write_launcher_spec") +load("//internal:workspace.bzl", "create_bun_workspace_info", "workspace_runfiles") def _bun_dev_impl(ctx): toolchain = ctx.toolchains["//bun:toolchain_type"] @@ -13,200 +14,127 @@ def _bun_dev_impl(ctx): primary_file = entry_point, ) - restart_watch_paths = "\n".join([path.short_path for path in ctx.files.restart_on]) - launcher_lines = [render_shell_array("bun_args", ["--bun", "run"])] - append_shell_install_mode(launcher_lines, "bun_args", ctx.attr.install_mode) - append_shell_flag_files(launcher_lines, "bun_args", "--preload", ctx.files.preload) - append_shell_flag_files(launcher_lines, "bun_args", "--env-file", ctx.files.env_files) - append_shell_flag(launcher_lines, "bun_args", "--no-env-file", ctx.attr.no_env_file) - append_shell_flag(launcher_lines, "bun_args", "--smol", ctx.attr.smol) - append_shell_flag_values(launcher_lines, "bun_args", "--conditions", ctx.attr.conditions) - append_shell_flag(launcher_lines, "bun_args", "--no-clear-screen", ctx.attr.no_clear_screen) - append_shell_raw_flags(launcher_lines, "bun_args", ctx.attr.run_flags) - launcher_lines.append('bun_args+=("${primary_source}")') - for arg in ctx.attr.args: - launcher_lines.append("bun_args+=(%s)" % shell_quote(arg)) + argv = ["--bun", "run"] + append_install_mode(argv, ctx.attr.install_mode) + append_flag(argv, "--no-env-file", ctx.attr.no_env_file) + append_flag(argv, "--smol", ctx.attr.smol) + append_flag_values(argv, "--conditions", ctx.attr.conditions) + append_flag(argv, "--no-clear-screen", ctx.attr.no_clear_screen) + append_raw_flags(argv, ctx.attr.run_flags) - command = """ -__BUN_ARGS__ -watch_mode="__WATCH_MODE__" -if [[ "${watch_mode}" == "hot" ]]; then - bun_args+=("--hot") -else - bun_args+=("--watch") -fi - -if [[ __RESTART_COUNT__ -eq 0 ]]; then - trap cleanup_runtime_workspace EXIT - cd "${runtime_exec_dir}" - exec "${bun_bin}" "${bun_args[@]}" "$@" -fi - -readarray -t restart_paths <<'EOF_RESTART_PATHS' -__RESTART_PATHS__ -EOF_RESTART_PATHS - -file_mtime() { - local path="$1" - if stat -f '%m' "${path}" >/dev/null 2>&1; then - stat -f '%m' "${path}" - return 0 - fi - stat -c '%Y' "${path}" -} - -declare -A mtimes -for rel in "${restart_paths[@]}"; do - path="${runfiles_dir}/_main/${rel}" - if [[ -e "${path}" ]]; then - mtimes["${rel}"]="$(file_mtime "${path}")" - else - mtimes["${rel}"]="missing" - fi -done - -child_pid="" -restart_child() { - if [[ -n "${child_pid}" ]] && kill -0 "${child_pid}" 2>/dev/null; then - kill "${child_pid}" - wait "${child_pid}" || true - fi - - ( - cd "${runtime_exec_dir}" - exec "${bun_bin}" "${bun_args[@]}" "$@" - ) & - child_pid=$! -} - -cleanup() { - if [[ -n "${child_pid}" ]] && kill -0 "${child_pid}" 2>/dev/null; then - kill "${child_pid}" - wait "${child_pid}" || true - fi - cleanup_runtime_workspace -} - -trap cleanup EXIT INT TERM - -restart_child "$@" - -while true; do - sleep 1 - changed=0 - for rel in "${restart_paths[@]}"; do - path="${runfiles_dir}/_main/${rel}" - if [[ -e "${path}" ]]; then - current="$(file_mtime "${path}")" - else - current="missing" - fi - if [[ "${current}" != "${mtimes[${rel}]}" ]]; then - mtimes["${rel}"]="${current}" - changed=1 - fi - done - if [[ "${changed}" -eq 1 ]]; then - restart_child "$@" - fi -done -""".replace("__WATCH_MODE__", ctx.attr.watch_mode).replace( - "__RESTART_COUNT__", - str(len(ctx.files.restart_on)), - ).replace( - "__RESTART_PATHS__", - restart_watch_paths, - ).replace( - "__BUN_ARGS__", - "\n".join(launcher_lines), - ) - - launcher = ctx.actions.declare_file(ctx.label.name) - ctx.actions.write( - output = launcher, - is_executable = True, - content = render_workspace_setup( - bun_short_path = bun_bin.short_path, - install_metadata_short_path = workspace_info.install_metadata_file.short_path if workspace_info.install_metadata_file else "", - primary_source_short_path = entry_point.short_path, - working_dir_mode = ctx.attr.working_dir, - ) + command, - ) + spec_file = write_launcher_spec(ctx, { + "version": 1, + "kind": "bun_run", + "bun_short_path": runfiles_path(bun_bin), + "primary_source_short_path": runfiles_path(entry_point), + "package_json_short_path": "", + "install_metadata_short_path": runfiles_path(workspace_info.install_metadata_file) if workspace_info.install_metadata_file else "", + "install_repo_runfiles_path": workspace_info.install_repo_runfiles_path, + "node_modules_roots": workspace_info.node_modules_roots, + "package_dir_hint": workspace_info.package_dir_hint, + "working_dir_mode": ctx.attr.working_dir, + "inherit_host_path": ctx.attr.inherit_host_path, + "argv": argv, + "args": ctx.attr.args, + "passthrough_args": True, + "tool_short_path": "", + "restart_on": [runfiles_path(file) for file in ctx.files.restart_on], + "watch_mode": ctx.attr.watch_mode, + "reporter": "", + "coverage": False, + "coverage_reporters": [], + "preload_short_paths": [runfiles_path(file) for file in ctx.files.preload], + "env_file_short_paths": [runfiles_path(file) for file in ctx.files.env_files], + "test_short_paths": [], + }) + launcher = declare_runtime_wrapper(ctx, bun_bin, spec_file) return [ workspace_info, DefaultInfo( - executable = launcher, - runfiles = workspace_runfiles(ctx, workspace_info, direct_files = [launcher]), + executable = launcher.executable, + runfiles = workspace_runfiles( + ctx, + workspace_info, + direct_files = [launcher.executable, launcher.runner, spec_file], + ), ), ] +_BUN_DEV_ATTRS = runtime_launcher_attrs() +_BUN_DEV_ATTRS.update({ + "entry_point": attr.label( + mandatory = True, + allow_single_file = [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"], + doc = "Path to the main JS/TS file to execute in dev mode.", + ), + "watch_mode": attr.string( + default = "watch", + values = ["watch", "hot"], + doc = "Bun live-reload mode: `watch` (default) or `hot`.", + ), + "restart_on": attr.label_list( + allow_files = True, + doc = "Files that trigger a full Bun process restart when they change.", + ), + "node_modules": attr.label( + doc = "Optional label providing package files from a `node_modules` tree, typically produced by `bun_install`, in runfiles.", + ), + "data": attr.label_list( + allow_files = True, + doc = "Additional runtime files required by the dev process.", + ), + "preload": attr.label_list( + allow_files = True, + doc = "Modules to preload with `--preload` before running the entry point.", + ), + "env_files": attr.label_list( + allow_files = True, + doc = "Additional environment files loaded with `--env-file`.", + ), + "no_env_file": attr.bool( + default = False, + doc = "If true, disables Bun's automatic `.env` loading.", + ), + "smol": attr.bool( + default = False, + doc = "If true, enables Bun's lower-memory runtime mode.", + ), + "conditions": attr.string_list( + doc = "Custom package resolve conditions passed to Bun.", + ), + "install_mode": attr.string( + default = "disable", + values = ["disable", "auto", "fallback", "force"], + doc = "Whether Bun may auto-install missing packages in dev mode.", + ), + "no_clear_screen": attr.bool( + default = False, + doc = "If true, disables terminal clearing on Bun reloads.", + ), + "run_flags": attr.string_list( + doc = "Additional raw flags forwarded to `bun run` before the entry point.", + ), + "working_dir": attr.string( + default = "workspace", + values = ["workspace", "entry_point"], + doc = "Working directory at runtime: `workspace` root or nearest `entry_point` ancestor containing `.env`/`package.json`.", + ), + "inherit_host_path": attr.bool( + default = False, + doc = "If true, appends the host PATH after staged node_modules/.bin entries at runtime.", + ), +}) + bun_dev = rule( implementation = _bun_dev_impl, doc = """Runs a JS/TS entry point in Bun development watch mode. This rule is intended for local dev loops (`bazel run`) and supports Bun -watch/HMR plus optional full restarts on selected file changes. +watch/HMR plus optional full restarts on selected file changes. It is a local +workflow helper rather than a hermetic build rule. """, - attrs = { - "entry_point": attr.label( - mandatory = True, - allow_single_file = [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"], - doc = "Path to the main JS/TS file to execute in dev mode.", - ), - "watch_mode": attr.string( - default = "watch", - values = ["watch", "hot"], - doc = "Bun live-reload mode: `watch` (default) or `hot`.", - ), - "restart_on": attr.label_list( - allow_files = True, - doc = "Files that trigger a full Bun process restart when they change.", - ), - "node_modules": attr.label( - doc = "Optional label providing package files from a `node_modules` tree, typically produced by `bun_install`, in runfiles.", - ), - "data": attr.label_list( - allow_files = True, - doc = "Additional runtime files required by the dev process.", - ), - "preload": attr.label_list( - allow_files = True, - doc = "Modules to preload with `--preload` before running the entry point.", - ), - "env_files": attr.label_list( - allow_files = True, - doc = "Additional environment files loaded with `--env-file`.", - ), - "no_env_file": attr.bool( - default = False, - doc = "If true, disables Bun's automatic `.env` loading.", - ), - "smol": attr.bool( - default = False, - doc = "If true, enables Bun's lower-memory runtime mode.", - ), - "conditions": attr.string_list( - doc = "Custom package resolve conditions passed to Bun.", - ), - "install_mode": attr.string( - default = "disable", - values = ["disable", "auto", "fallback", "force"], - doc = "Whether Bun may auto-install missing packages in dev mode.", - ), - "no_clear_screen": attr.bool( - default = False, - doc = "If true, disables terminal clearing on Bun reloads.", - ), - "run_flags": attr.string_list( - doc = "Additional raw flags forwarded to `bun run` before the entry point.", - ), - "working_dir": attr.string( - default = "workspace", - values = ["workspace", "entry_point"], - doc = "Working directory at runtime: `workspace` root or nearest `entry_point` ancestor containing `.env`/`package.json`.", - ), - }, + attrs = _BUN_DEV_ATTRS, executable = True, toolchains = ["//bun:toolchain_type"], ) diff --git a/internal/bun_install.bzl b/internal/bun_install.bzl index c875d43..d262363 100644 --- a/internal/bun_install.bzl +++ b/internal/bun_install.bzl @@ -205,8 +205,14 @@ def _materialize_workspace_packages(repository_ctx, package_json): workspace_packages[relative_dir] = package_name if type(package_name) == type("") else "" package_dirs = sorted(workspace_packages.keys()) + package_names_by_dir = {} + for package_dir in package_dirs: + package_name = workspace_packages[package_dir] + if package_name: + package_names_by_dir[package_dir] = package_name return struct( package_dirs = package_dirs, + package_names_by_dir = package_names_by_dir, package_names = [workspace_packages[package_dir] for package_dir in package_dirs if workspace_packages[package_dir]], ) @@ -381,8 +387,10 @@ stderr: "node_modules/.rules_bun/install.json", json.encode({ "bun_lockfile": lockfile_name, + "install_root_rel_dir": ".", "package_json": "package.json", "workspace_package_dirs": workspace_packages.package_dirs, + "workspace_package_names_by_dir": workspace_packages.package_names_by_dir, }) + "\n", ) @@ -409,7 +417,7 @@ bun_install_repository = repository_rule( "omit": attr.string_list(), "linker": attr.string(), "backend": attr.string(), - "ignore_scripts": attr.bool(default = False), + "ignore_scripts": attr.bool(default = True), "install_flags": attr.string_list(), "visible_repo_name": attr.string(), "bun_linux_x64": attr.label(default = "@bun_linux_x64//:bun-linux-x64/bun", allow_single_file = True), @@ -430,7 +438,7 @@ def bun_install( omit = [], linker = "", backend = "", - ignore_scripts = False, + ignore_scripts = True, install_flags = []): """Create an external repository containing installed node_modules. diff --git a/internal/bun_script.bzl b/internal/bun_script.bzl index 7178c31..fce857f 100644 --- a/internal/bun_script.bzl +++ b/internal/bun_script.bzl @@ -1,8 +1,8 @@ """Rule for running package.json scripts with Bun.""" -load("//internal:bun_command.bzl", "append_shell_flag", "append_shell_flag_files", "append_shell_flag_value", "append_shell_flag_values", "append_shell_install_mode", "append_shell_raw_flags", "render_shell_array", "shell_quote") -load("//internal:workspace.bzl", "create_bun_workspace_info", "render_workspace_setup", "workspace_runfiles") - +load("//internal:bun_command.bzl", "append_flag", "append_flag_value", "append_flag_values", "append_install_mode", "append_raw_flags") +load("//internal:runtime_launcher.bzl", "declare_runtime_wrapper", "runfiles_path", "runtime_launcher_attrs", "write_launcher_spec") +load("//internal:workspace.bzl", "create_bun_workspace_info", "workspace_runfiles") def _bun_script_impl(ctx): toolchain = ctx.toolchains["//bun:toolchain_type"] @@ -16,56 +16,141 @@ def _bun_script_impl(ctx): primary_file = package_json, ) - launcher_lines = [render_shell_array("bun_args", ["--bun", "run"])] - append_shell_install_mode(launcher_lines, "bun_args", ctx.attr.install_mode) - append_shell_flag_files(launcher_lines, "bun_args", "--preload", ctx.files.preload) - append_shell_flag_files(launcher_lines, "bun_args", "--env-file", ctx.files.env_files) - append_shell_flag(launcher_lines, "bun_args", "--no-env-file", ctx.attr.no_env_file) - append_shell_flag(launcher_lines, "bun_args", "--smol", ctx.attr.smol) - append_shell_flag_values(launcher_lines, "bun_args", "--conditions", ctx.attr.conditions) - append_shell_flag(launcher_lines, "bun_args", "--workspaces", ctx.attr.workspaces) - append_shell_flag_values(launcher_lines, "bun_args", "--filter", ctx.attr.filters) + argv = ["--bun", "run"] + append_install_mode(argv, ctx.attr.install_mode) + append_flag(argv, "--no-env-file", ctx.attr.no_env_file) + append_flag(argv, "--smol", ctx.attr.smol) + append_flag_values(argv, "--conditions", ctx.attr.conditions) + append_flag(argv, "--workspaces", ctx.attr.workspaces) + append_flag_values(argv, "--filter", ctx.attr.filters) if ctx.attr.execution_mode == "parallel": - append_shell_flag(launcher_lines, "bun_args", "--parallel", True) + append_flag(argv, "--parallel", True) elif ctx.attr.execution_mode == "sequential": - append_shell_flag(launcher_lines, "bun_args", "--sequential", True) - append_shell_flag(launcher_lines, "bun_args", "--no-exit-on-error", ctx.attr.no_exit_on_error) - append_shell_flag_value(launcher_lines, "bun_args", "--shell", ctx.attr.shell) - append_shell_flag(launcher_lines, "bun_args", "--silent", ctx.attr.silent) - append_shell_raw_flags(launcher_lines, "bun_args", ctx.attr.run_flags) - launcher_lines.append('bun_args+=(%s)' % shell_quote(ctx.attr.script)) - for arg in ctx.attr.args: - launcher_lines.append("bun_args+=(%s)" % shell_quote(arg)) + append_flag(argv, "--sequential", True) + append_flag(argv, "--no-exit-on-error", ctx.attr.no_exit_on_error) + append_flag_value(argv, "--shell", ctx.attr.shell) + append_flag(argv, "--silent", ctx.attr.silent) + append_raw_flags(argv, ctx.attr.run_flags) - command = """ -trap cleanup_runtime_workspace EXIT -cd "${runtime_exec_dir}" -__BUN_ARGS__ -exec "${bun_bin}" "${bun_args[@]}" "$@" -""".replace("__BUN_ARGS__", "\n".join(launcher_lines)) - - launcher = ctx.actions.declare_file(ctx.label.name) - ctx.actions.write( - output = launcher, - is_executable = True, - content = render_workspace_setup( - bun_short_path = bun_bin.short_path, - package_dir_hint = package_json.dirname or ".", - package_json_short_path = package_json.short_path, - primary_source_short_path = package_json.short_path, - install_metadata_short_path = workspace_info.install_metadata_file.short_path if workspace_info.install_metadata_file else "", - working_dir_mode = ctx.attr.working_dir, - ) + command, - ) + spec_file = write_launcher_spec(ctx, { + "version": 1, + "kind": "bun_run", + "bun_short_path": runfiles_path(bun_bin), + "primary_source_short_path": "", + "package_json_short_path": runfiles_path(package_json), + "install_metadata_short_path": runfiles_path(workspace_info.install_metadata_file) if workspace_info.install_metadata_file else "", + "install_repo_runfiles_path": workspace_info.install_repo_runfiles_path, + "node_modules_roots": workspace_info.node_modules_roots, + "package_dir_hint": package_json.dirname or ".", + "working_dir_mode": ctx.attr.working_dir, + "inherit_host_path": ctx.attr.inherit_host_path, + "argv": argv, + "args": [ctx.attr.script] + ctx.attr.args, + "passthrough_args": True, + "tool_short_path": "", + "restart_on": [], + "watch_mode": "", + "reporter": "", + "coverage": False, + "coverage_reporters": [], + "preload_short_paths": [runfiles_path(file) for file in ctx.files.preload], + "env_file_short_paths": [runfiles_path(file) for file in ctx.files.env_files], + "test_short_paths": [], + }) + launcher = declare_runtime_wrapper(ctx, bun_bin, spec_file) return [ workspace_info, DefaultInfo( - executable = launcher, - runfiles = workspace_runfiles(ctx, workspace_info, direct_files = [launcher]), + executable = launcher.executable, + runfiles = workspace_runfiles( + ctx, + workspace_info, + direct_files = [launcher.executable, launcher.runner, spec_file], + ), ), ] +_BUN_SCRIPT_ATTRS = runtime_launcher_attrs() +_BUN_SCRIPT_ATTRS.update({ + "script": attr.string( + mandatory = True, + doc = "Name of the `package.json` script to execute via `bun run