mirror of
https://github.com/pre-commit/pre-commit.git
synced 2026-02-17 08:14:42 +04:00
Implements Bun as a new language option for pre-commit hooks, enabling hooks to run using the Bun JavaScript runtime and package manager. - Add bun.py language implementation with binary download/install - Support system-installed Bun or automatic version download - Add comprehensive tests including version handling and hook execution - Register bun in all_languages.py - Include test repository fixture for integration tests
195 lines
6.4 KiB
Python
195 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import functools
|
|
import os.path
|
|
import platform
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import urllib.error
|
|
import urllib.request
|
|
import zipfile
|
|
from collections.abc import Generator
|
|
from collections.abc import Sequence
|
|
|
|
import pre_commit.constants as C
|
|
from pre_commit import lang_base
|
|
from pre_commit.envcontext import envcontext
|
|
from pre_commit.envcontext import PatchesT
|
|
from pre_commit.envcontext import Var
|
|
from pre_commit.languages.python import bin_dir
|
|
from pre_commit.prefix import Prefix
|
|
from pre_commit.util import cmd_output_b
|
|
|
|
ENVIRONMENT_DIR = 'bunenv'
|
|
run_hook = lang_base.basic_run_hook
|
|
|
|
# Architecture mapping for Bun binary downloads
|
|
_ARCH_ALIASES = {
|
|
'x86_64': 'x64',
|
|
'amd64': 'x64',
|
|
'aarch64': 'aarch64',
|
|
'arm64': 'aarch64',
|
|
}
|
|
_ARCH = platform.machine().lower()
|
|
_ARCH = _ARCH_ALIASES.get(_ARCH, _ARCH)
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
def get_default_version() -> str:
|
|
"""Detect if Bun is installed system-wide."""
|
|
# Check for system-installed bun
|
|
if lang_base.exe_exists('bun'):
|
|
return 'system'
|
|
else:
|
|
return C.DEFAULT
|
|
|
|
|
|
def _get_platform() -> str:
|
|
"""Get platform string for Bun binary downloads."""
|
|
if sys.platform == 'darwin':
|
|
return 'darwin'
|
|
elif sys.platform == 'win32':
|
|
return 'windows'
|
|
elif sys.platform.startswith('linux'):
|
|
return 'linux'
|
|
else:
|
|
raise AssertionError(f'Unsupported platform: {sys.platform}')
|
|
|
|
|
|
def _normalize_version(version: str) -> str:
|
|
"""Normalize version string for download URL."""
|
|
if version == C.DEFAULT:
|
|
return 'latest'
|
|
# Ensure version has 'bun-v' prefix for download URL
|
|
if not version.startswith('bun-v'):
|
|
if version.startswith('v'):
|
|
return f'bun-{version}'
|
|
else:
|
|
return f'bun-v{version}'
|
|
return version
|
|
|
|
|
|
def _get_download_url(version: str) -> str:
|
|
"""Construct Bun binary download URL from GitHub releases."""
|
|
platform_name = _get_platform()
|
|
normalized_version = _normalize_version(version)
|
|
|
|
# Bun release URL format:
|
|
# https://github.com/oven-sh/bun/releases/download/bun-v1.1.42/bun-darwin-x64.zip
|
|
# https://github.com/oven-sh/bun/releases/download/bun-v1.1.42/bun-linux-x64.zip
|
|
# https://github.com/oven-sh/bun/releases/download/bun-v1.1.42/bun-windows-x64.zip
|
|
base_url = 'https://github.com/oven-sh/bun/releases'
|
|
|
|
if normalized_version == 'latest':
|
|
# Use latest release
|
|
return f'{base_url}/latest/download/bun-{platform_name}-{_ARCH}.zip'
|
|
else:
|
|
# Use specific version
|
|
return (
|
|
f'{base_url}/download/{normalized_version}/'
|
|
f'bun-{platform_name}-{_ARCH}.zip'
|
|
)
|
|
|
|
|
|
def _install_bun(version: str, dest: str) -> None:
|
|
"""Download and extract Bun binary to destination directory."""
|
|
url = _get_download_url(version)
|
|
|
|
try:
|
|
resp = urllib.request.urlopen(url)
|
|
except urllib.error.HTTPError as e:
|
|
if e.code == 404:
|
|
raise ValueError(
|
|
f'Could not find Bun version matching your requirements '
|
|
f'(version={version}; os={_get_platform()}; '
|
|
f'arch={_ARCH}). Check available versions at '
|
|
f'https://github.com/oven-sh/bun/releases',
|
|
) from e
|
|
else:
|
|
raise
|
|
|
|
with tempfile.TemporaryFile() as f:
|
|
shutil.copyfileobj(resp, f)
|
|
f.seek(0)
|
|
|
|
with zipfile.ZipFile(f) as zipf:
|
|
zipf.extractall(dest)
|
|
|
|
# Bun zipfile contains a directory like 'bun-darwin-x64' or 'bun-linux-x64'
|
|
# Move the binary from the extracted directory to dest/bin/
|
|
bin_dir_path = os.path.join(dest, 'bin')
|
|
os.makedirs(bin_dir_path, exist_ok=True)
|
|
|
|
# Find the extracted directory
|
|
for item in os.listdir(dest):
|
|
item_path = os.path.join(dest, item)
|
|
if os.path.isdir(item_path) and item.startswith('bun-'):
|
|
# Move bun executable to bin directory
|
|
bun_exe = 'bun.exe' if sys.platform == 'win32' else 'bun'
|
|
src_exe = os.path.join(item_path, bun_exe)
|
|
if os.path.exists(src_exe):
|
|
shutil.move(src_exe, os.path.join(bin_dir_path, bun_exe))
|
|
# Remove the extracted directory
|
|
shutil.rmtree(item_path)
|
|
break
|
|
|
|
|
|
def get_env_patch(venv: str) -> PatchesT:
|
|
"""Prepare environment variables for Bun execution."""
|
|
# Bun is much simpler than Node - primarily just needs PATH
|
|
return (
|
|
('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))),
|
|
# BUN_INSTALL controls where global packages are installed
|
|
('BUN_INSTALL', venv),
|
|
)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
|
"""Context manager for Bun environment."""
|
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
|
with envcontext(get_env_patch(envdir)):
|
|
yield
|
|
|
|
|
|
def health_check(prefix: Prefix, version: str) -> str | None:
|
|
"""Check if Bun environment is healthy."""
|
|
with in_env(prefix, version):
|
|
retcode, _, _ = cmd_output_b('bun', '--version', check=False)
|
|
if retcode != 0: # pragma: no cover
|
|
return f'`bun --version` returned {retcode}'
|
|
else:
|
|
return None
|
|
|
|
|
|
def install_environment(
|
|
prefix: Prefix,
|
|
version: str,
|
|
additional_dependencies: Sequence[str],
|
|
) -> None:
|
|
"""Install Bun environment and dependencies."""
|
|
assert prefix.exists('package.json')
|
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
|
|
|
# Install Bun binary (unless using system version)
|
|
if version != 'system':
|
|
_install_bun(version, envdir)
|
|
|
|
with in_env(prefix, version):
|
|
# Install local dependencies from package.json
|
|
# Use --no-progress to avoid cluttering output
|
|
install_cmd = ('bun', 'install', '--no-progress')
|
|
lang_base.setup_cmd(prefix, install_cmd)
|
|
|
|
# Install the package globally from the current directory
|
|
# Bun's global install uses `bun add -g` with file: protocol
|
|
# We need to install from an absolute file path, so we use file:.
|
|
# Note: Unlike npm, bun creates symlinks to the local package,
|
|
# so we must NOT delete node_modules or the bin directory.
|
|
abs_prefix = os.path.abspath(prefix.prefix_dir)
|
|
install = ['bun', 'add', '-g', f'file:{abs_prefix}']
|
|
if additional_dependencies:
|
|
install.extend(additional_dependencies)
|
|
lang_base.setup_cmd(prefix, tuple(install))
|