fix: bun install symlinks

This commit is contained in:
eric
2026-03-06 23:52:27 +01:00
parent e567dad413
commit 40d621d1cf
24 changed files with 1464 additions and 56 deletions

View File

@@ -1,5 +1,30 @@
"""Repository-rule based bun_install implementation."""
_DEFAULT_INSTALL_INPUTS = [
".npmrc",
"bunfig.json",
"bunfig.toml",
]
def _normalize_path(path):
normalized = path.replace("\\", "/")
if normalized.endswith("/") and normalized != "/":
normalized = normalized[:-1]
return normalized
def _relative_to_root(root, child):
normalized_root = _normalize_path(root)
normalized_child = _normalize_path(child)
if normalized_child == normalized_root:
return ""
prefix = normalized_root + "/"
if not normalized_child.startswith(prefix):
fail("bun_install: expected install input {} to be under {}".format(child, root))
return normalized_child[len(prefix):]
def _segment_matches(name, pattern):
if pattern == "*":
return True
@@ -87,7 +112,7 @@ def _materialize_workspace_packages(repository_ctx, package_json):
if workspace_dir_str == package_root_str:
continue
relative_dir = workspace_dir_str[len(package_root_str) + 1:]
relative_dir = _relative_to_root(package_root_str, workspace_dir_str)
if relative_dir in written:
continue
@@ -97,6 +122,36 @@ def _materialize_workspace_packages(repository_ctx, package_json):
)
written[relative_dir] = True
def _materialize_install_inputs(repository_ctx, package_json):
package_root = package_json.dirname
package_root_str = str(package_root)
written = {}
for relative_path in _DEFAULT_INSTALL_INPUTS:
source_path = repository_ctx.path(str(package_root) + "/" + relative_path)
if source_path.exists and not source_path.is_dir:
repository_ctx.file(relative_path, repository_ctx.read(source_path))
written[relative_path] = True
for install_input in repository_ctx.attr.install_inputs:
source_path = repository_ctx.path(install_input)
if not source_path.exists:
fail("bun_install: install input not found: {}".format(install_input))
if source_path.is_dir:
fail("bun_install: install_inputs must be files under the package root: {}".format(install_input))
relative_path = _relative_to_root(package_root_str, str(source_path))
if not relative_path:
fail("bun_install: install input must be a file under the package root: {}".format(install_input))
if relative_path in written:
continue
repository_ctx.file(relative_path, repository_ctx.read(source_path))
written[relative_path] = True
def _select_bun_binary(repository_ctx):
os_name = repository_ctx.os.name.lower()
arch = repository_ctx.os.arch.lower()
@@ -134,14 +189,23 @@ def _bun_install_repository_impl(repository_ctx):
repository_ctx.file("package.json", repository_ctx.read(package_json))
repository_ctx.symlink(bun_lockfile, lockfile_name)
_materialize_install_inputs(repository_ctx, package_json)
_materialize_workspace_packages(repository_ctx, package_json)
result = repository_ctx.execute(
[str(bun_bin), "--bun", "install", "--frozen-lockfile", "--no-progress"],
timeout = 600,
quiet = False,
environment = {"HOME": str(repository_ctx.path("."))},
)
install_args = [str(bun_bin), "--bun", "install", "--frozen-lockfile", "--no-progress"]
if repository_ctx.attr.isolated_home:
result = repository_ctx.execute(
install_args,
timeout = 600,
quiet = False,
environment = {"HOME": str(repository_ctx.path("."))},
)
else:
result = repository_ctx.execute(
install_args,
timeout = 600,
quiet = False,
)
if result.return_code:
fail("""bun_install failed running `bun --bun install --frozen-lockfile`.
@@ -155,7 +219,7 @@ stderr:
"BUILD.bazel",
"""filegroup(
name = "node_modules",
srcs = glob(["node_modules/**"], allow_empty = False),
srcs = glob(["**/node_modules/**"], allow_empty = False),
visibility = ["//visibility:public"],
)
""",
@@ -166,6 +230,8 @@ bun_install_repository = repository_rule(
attrs = {
"package_json": attr.label(mandatory = True, allow_single_file = True),
"bun_lockfile": attr.label(mandatory = True, allow_single_file = True),
"install_inputs": attr.label_list(allow_files = True),
"isolated_home": attr.bool(default = True),
"bun_linux_x64": attr.label(default = "@bun_linux_x64//:bun-linux-x64/bun", allow_single_file = True),
"bun_linux_aarch64": attr.label(default = "@bun_linux_aarch64//:bun-linux-aarch64/bun", allow_single_file = True),
"bun_darwin_x64": attr.label(default = "@bun_darwin_x64//:bun-darwin-x64/bun", allow_single_file = True),
@@ -174,13 +240,17 @@ bun_install_repository = repository_rule(
},
)
def bun_install(name, package_json, bun_lockfile):
def bun_install(name, package_json, bun_lockfile, install_inputs = [], isolated_home = True):
"""Create an external repository containing installed node_modules.
Args:
name: Repository name to create.
package_json: Label to a package.json file.
bun_lockfile: Label to a bun.lockb file.
install_inputs: Optional additional files under the package root to copy
into the install context, such as patch files or auth/config files.
isolated_home: Whether to run Bun with HOME set to the generated
repository root for a more isolated install context.
Usage (WORKSPACE):
bun_install(
@@ -194,4 +264,6 @@ def bun_install(name, package_json, bun_lockfile):
name = name,
package_json = package_json,
bun_lockfile = bun_lockfile,
install_inputs = install_inputs,
isolated_home = isolated_home,
)

View File

@@ -19,30 +19,115 @@ set -euo pipefail
runfiles_dir="${{RUNFILES_DIR:-$0.runfiles}}"
workspace_root="${{runfiles_dir}}/_main"
workspace_root="$(cd "${{workspace_root}}" && pwd -P)"
bun_bin="${{runfiles_dir}}/_main/{bun_short_path}"
package_json="${{runfiles_dir}}/_main/{package_json_short_path}"
package_dir="$(dirname "${{package_json}}")"
package_dir="$(cd "$(dirname "${{package_json}}")" && pwd -P)"
package_rel_dir="{package_rel_dir}"
node_modules_bin_dirs=()
while IFS= read -r node_modules_bin; do
node_modules_bin_dirs+=("${{node_modules_bin}}")
done < <(find "${{runfiles_dir}}" -type d -path '*/node_modules/.bin' 2>/dev/null | sort)
select_primary_node_modules() {{
local selected=""
local fallback=""
while IFS= read -r node_modules_dir; do
if [[ -z "${{fallback}}" ]]; then
fallback="${{node_modules_dir}}"
fi
if [[ ${{#node_modules_bin_dirs[@]}} -gt 0 ]]; then
export PATH="$(IFS=:; echo "${{node_modules_bin_dirs[*]}}"):${{PATH}}"
if [[ ! -d "${{node_modules_dir}}/.bun" ]]; then
continue
fi
if [[ "${{node_modules_dir}}" != *"/runfiles/_main/"* ]]; then
selected="${{node_modules_dir}}"
break
fi
if [[ -z "${{selected}}" ]]; then
selected="${{node_modules_dir}}"
fi
done < <(find -L "${{runfiles_dir}}" -type d -name node_modules 2>/dev/null | sort)
if [[ -n "${{selected}}" ]]; then
echo "${{selected}}"
else
echo "${{fallback}}"
fi
}}
primary_node_modules="$(select_primary_node_modules)"
runtime_workspace="$(mktemp -d)"
cleanup_runtime_workspace() {{
rm -rf "${{runtime_workspace}}"
}}
trap cleanup_runtime_workspace EXIT
runtime_package_dir="${{runtime_workspace}}/${{package_rel_dir}}"
mkdir -p "${{runtime_package_dir}}"
cp -RL "${{package_dir}}/." "${{runtime_package_dir}}/"
install_repo_root=""
if [[ -n "${{primary_node_modules}}" ]]; then
install_repo_root="$(dirname "${{primary_node_modules}}")"
ln -s "${{primary_node_modules}}" "${{runtime_workspace}}/node_modules"
fi
find_node_modules() {{
local dir="$1"
local root="$2"
while [[ "$dir" == "$root"* ]]; do
if [[ -d "$dir/node_modules" ]]; then
echo "$dir/node_modules"
return 0
fi
if [[ "$dir" == "$root" ]]; then
break
fi
dir="$(dirname "$dir")"
done
return 1
}}
if [[ -n "${{install_repo_root}}" && -d "${{install_repo_root}}/${{package_rel_dir}}/node_modules" ]]; then
rm -rf "${{runtime_package_dir}}/node_modules"
ln -s "${{install_repo_root}}/${{package_rel_dir}}/node_modules" "${{runtime_package_dir}}/node_modules"
else
resolved_node_modules="$(find_node_modules "${{runtime_package_dir}}" "${{runtime_workspace}}" || true)"
if [[ -n "${{resolved_node_modules}}" && "${{resolved_node_modules}}" != "${{runtime_package_dir}}/node_modules" ]]; then
rm -rf "${{runtime_package_dir}}/node_modules"
ln -s "${{resolved_node_modules}}" "${{runtime_package_dir}}/node_modules"
fi
fi
path_entries=()
if [[ -d "${{runtime_package_dir}}/node_modules/.bin" ]]; then
path_entries+=("${{runtime_package_dir}}/node_modules/.bin")
fi
if [[ -d "${{runtime_workspace}}/node_modules/.bin" && "${{runtime_workspace}}/node_modules/.bin" != "${{runtime_package_dir}}/node_modules/.bin" ]]; then
path_entries+=("${{runtime_workspace}}/node_modules/.bin")
fi
if [[ ${{#path_entries[@]}} -gt 0 ]]; then
export PATH="$(IFS=:; echo "${{path_entries[*]}}"):${{PATH}}"
fi
working_dir="{working_dir}"
if [[ "${{working_dir}}" == "package" ]]; then
cd "${{package_dir}}"
cd "${{runtime_package_dir}}"
else
cd "${{workspace_root}}"
cd "${{runtime_workspace}}"
fi
exec "${{bun_bin}}" --bun run {script} "$@"
""".format(
bun_short_path = bun_bin.short_path,
package_json_short_path = package_json.short_path,
package_rel_dir = package_json.dirname,
working_dir = ctx.attr.working_dir,
script = _shell_quote(ctx.attr.script),
),