Source code for bunenv

#!/usr/bin/env python

"""bunenv
~~~~~~
Bun.js virtual environment

Adapted from nodeenv (https://github.com/ekalinin/nodeenv)

:copyright: (c) 2025 Jacob Coffee
            (c) 2011 Eugene Kalinin (original nodeenv)
:license: BSD-3-Clause, see LICENSE for more details.
"""

import argparse
import contextlib
import http.client
import io
import json
import logging
import os
import platform
import shutil
import ssl
import stat
import subprocess
import sys
import sysconfig
import urllib.request as urllib2
import zipfile
from configparser import ConfigParser
from typing import Any, Callable, Iterator

IncompleteRead = http.client.IncompleteRead

bunenv_version: str = "1.0.0"

join: Callable[..., str] = os.path.join
abspath: Callable[[str], str] = os.path.abspath
src_base_url: str | None = None  # Will be set to GitHub API base

is_WIN: bool = platform.system() == "Windows"
is_CYGWIN: bool = platform.system().startswith(("CYGWIN", "MSYS"))

ignore_ssl_certs: bool = False

# ---------------------------------------------------------
# Utils


[docs] class Config: """Configuration namespace.""" # Class attribute for defaults (set at module level) _default: dict[str, Any] # Defaults bun: str = "latest" variant: str = "" # baseline, profile, musl github_token: str | None = None # For GitHub API rate limits prebuilt: bool = True # Always true for Bun (no source builds) ignore_ssl_certs: bool = False mirror: str | None = None # For GitHub mirrors if needed @classmethod def _load(cls, configfiles: list[str], verbose: bool = False) -> None: """Load configuration from the given files in reverse order, if they exist and have a [bunenv] section. Additionally, load version from .bun-version if file exists. """ for configfile in reversed(configfiles): configfile = os.path.expanduser(configfile) if not os.path.exists(configfile): continue ini_file = ConfigParser() ini_file.read(configfile) section = "bunenv" if not ini_file.has_section(section): continue for attr, val in vars(cls).items(): if attr.startswith("_") or not ini_file.has_option(section, attr): continue if isinstance(val, bool): val = ini_file.getboolean(section, attr) else: val = ini_file.get(section, attr) if verbose: print(f"CONFIG {os.path.basename(configfile)}: {attr} = {val}") setattr(cls, attr, val) # Support .bun-version file (like .node-version) if os.path.exists(".bun-version"): with open(".bun-version") as v_file: cls.bun = v_file.readline().strip().lstrip("v").lstrip("bun-v") @classmethod def _dump(cls) -> None: """Print defaults for the README.""" print(" [bunenv]") print(" " + "\n ".join(f"{k} = {v}" for k, v in sorted(vars(cls).items()) if not k.startswith("_")))
Config._default: dict[str, Any] = {attr: val for attr, val in vars(Config).items() if not attr.startswith("_")}
[docs] def clear_output(out: bytes) -> str: """Remove new-lines and decode""" return out.decode("utf-8").replace("\n", "")
[docs] def remove_env_bin_from_path(env: str, env_bin_dir: str) -> str: """Remove bin directory of the current environment from PATH""" return env.replace(env_bin_dir + ":", "")
[docs] def parse_version(version_str: str) -> tuple[int, ...]: """Parse version string to a tuple of integer parts""" try: # Remove prefixes in correct order (longest first) v = version_str.replace("bun-v", "").replace("v", "").split(".")[:3] # remove all after '+' in the PATCH part of the version if len(v) >= 3: v[2] = v[2].split("+")[0] return tuple(map(int, v)) except ValueError: # Return empty tuple if version string is invalid return ()
[docs] def bun_version_from_args(args: argparse.Namespace) -> tuple[int, ...]: """Parse the bun version from the argparse args""" if args.bun == "system": out, err = subprocess.Popen(["bun", "--version"], stdout=subprocess.PIPE).communicate() return parse_version(clear_output(out)) return parse_version(args.bun)
[docs] def create_logger() -> logging.Logger: """Create logger for diagnostic""" # create logger loggr = logging.getLogger("bunenv") loggr.setLevel(logging.INFO) # monkey patch def emit(self: logging.StreamHandler, record: logging.LogRecord) -> None: msg = self.format(record) fs = "{}" if getattr(record, "continued", False) else "{}\n" self.stream.write(fs.format(msg)) self.flush() logging.StreamHandler.emit = emit # type: ignore[assignment] # create console handler and set level to debug ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) # create formatter formatter = logging.Formatter(fmt="%(message)s") # add formatter to ch ch.setFormatter(formatter) # add ch to logger loggr.addHandler(ch) return loggr
logger: logging.Logger = create_logger()
[docs] def make_parser() -> argparse.ArgumentParser: """Make a command line argument parser.""" parser = argparse.ArgumentParser(usage="%(prog)s [OPTIONS] DEST_DIR") parser.add_argument("--version", action="version", version=bunenv_version) parser.add_argument( "-b", "--bun", dest="bun", metavar="BUN_VER", default=Config.bun, help="The Bun version to use, e.g., " "--bun=1.0.0 will use bun-v1.0.0 " "to create the new environment. " "The default is last stable version (`latest`). " "Use `system` to use system-wide bun.", ) parser.add_argument( "--variant", action="store", dest="variant", default=Config.variant, choices=["", "baseline", "profile", "musl"], help="Bun variant to install (baseline, profile, musl). Default is auto-detected based on platform.", ) parser.add_argument( "--github-token", action="store", dest="github_token", default=Config.github_token, help="GitHub API token to avoid rate limits when fetching versions.", ) parser.add_argument( "--mirror", action="store", dest="mirror", default=Config.mirror, help="Set mirror server for Bun downloads (GitHub mirror).", ) parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False, help="Verbose mode") parser.add_argument("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Quiet mode") parser.add_argument( "-C", "--config-file", dest="config_file", default=None, help="Load a different file than '~/.bunenvrc'. Pass an empty string for no config (use built-in defaults).", ) parser.add_argument( "-r", "--requirements", dest="requirements", default="", metavar="FILENAME", help="Install all the packages listed in the given requirements file.", ) parser.add_argument("--prompt", dest="prompt", help="Provides an alternative prompt prefix for this environment") parser.add_argument( "-l", "--list", dest="list", action="store_true", default=False, help="Lists available Bun versions" ) parser.add_argument( "--update", dest="update", action="store_true", default=False, help="Install packages from file without bun" ) parser.add_argument( "--python-virtualenv", "-p", dest="python_virtualenv", action="store_true", default=False, help="Use current python virtualenv", ) parser.add_argument( "--clean-src", "-c", dest="clean_src", action="store_true", default=False, help='Remove "src" directory after installation', ) parser.add_argument( "--force", dest="force", action="store_true", default=False, help="Force installation in a pre-existing directory", ) parser.add_argument( "--prebuilt", dest="prebuilt", action="store_true", default=Config.prebuilt, help="Install Bun from prebuilt package (default and only option)", ) parser.add_argument( "--ignore_ssl_certs", dest="ignore_ssl_certs", action="store_true", default=Config.ignore_ssl_certs, help="Ignore certificates for package downloads. - UNSAFE -", ) parser.add_argument(metavar="DEST_DIR", dest="env_dir", nargs="?", help="Destination directory") return parser
[docs] def parse_args(check: bool = True) -> argparse.Namespace: """Parses command line arguments. Set `check` to False to skip validation checks. """ parser = make_parser() args = parser.parse_args() if args.config_file is None: args.config_file = ["./tox.ini", "./setup.cfg", "~/.bunenvrc"] elif not args.config_file: args.config_file = [] else: # Make sure that explicitly provided files exist if not os.path.exists(args.config_file): parser.error(f"Config file '{args.config_file}' doesn't exist!") args.config_file = [args.config_file] if not check: return args if not args.list: if not args.python_virtualenv and not args.env_dir: parser.error("You must provide a DEST_DIR or use current python virtualenv") return args
[docs] def mkdir(path: str) -> None: """Create directory""" logger.debug(" * Creating: %s ... ", path, extra={"continued": True}) os.makedirs(path, exist_ok=True) logger.debug("done.")
[docs] def make_executable(filename: str) -> None: mode_0755 = stat.S_IRWXU | stat.S_IXGRP | stat.S_IRGRP | stat.S_IROTH | stat.S_IXOTH os.chmod(filename, mode_0755)
# noinspection PyArgumentList
[docs] def writefile(dest: str, content: str | bytes, overwrite: bool = True, append: bool = False) -> None: """Create file and write content in it""" if not isinstance(content, bytes): content = bytes(content, "utf-8") if not os.path.exists(dest): logger.debug(" * Writing %s ... ", dest, extra={"continued": True}) with open(dest, "wb") as f: f.write(content) make_executable(dest) logger.debug("done.") return with open(dest, "rb") as f: c = f.read() if content in c: logger.debug(" * Content %s already in place", dest) return if not overwrite: logger.info(" * File %s exists with different content; not overwriting", dest) return if append: logger.info(" * Appending data to %s", dest) with open(dest, "ab") as f: f.write(content) return logger.info(" * Overwriting %s with new content", dest) with open(dest, "wb") as f: f.write(content)
[docs] def callit( cmd: list[str] | str, show_stdout: bool = True, in_shell: bool = False, cwd: str | None = None, extra_env: dict[str, str] | None = None, ) -> tuple[int, list[str]]: """Execute cmd line in sub-shell""" all_output: list[str] = [] cmd_parts: list[str] = [] for part in cmd: # type: ignore[union-attr] if len(part) > 45: part = part[:20] + "..." + part[-20:] if " " in part or "\n" in part or '"' in part or "'" in part: escaped_part = part.replace('"', '\\"') part = f'"{escaped_part}"' cmd_parts.append(part) cmd_desc = " ".join(cmd_parts) logger.debug(f" ** Running command {cmd_desc}") if in_shell: cmd = " ".join(cmd) # type: ignore[arg-type] # output stdout = subprocess.PIPE # env env: dict[str, str] | None = None if extra_env: env = os.environ.copy() if extra_env: env.update(extra_env) # execute try: proc = subprocess.Popen( cmd, stderr=subprocess.STDOUT, stdin=None, stdout=stdout, cwd=cwd, env=env, shell=in_shell ) except Exception as e: logger.error(f"Error {e} while executing command {cmd_desc}") raise stdout_stream = proc.stdout while stdout_stream: line_bytes = stdout_stream.readline() if not line_bytes: break line: str if is_WIN: line = line_bytes.decode("mbcs", errors="replace").rstrip() else: line = line_bytes.decode("utf8", errors="replace").rstrip() all_output.append(line) if show_stdout: logger.info(line) proc.wait() # error handler if proc.returncode: if show_stdout: for s in all_output: logger.critical(s) raise OSError(f"Command {cmd_desc} failed with error code {proc.returncode}") return proc.returncode, all_output
[docs] def is_x86_64_musl() -> bool: """Check if running on musl libc""" return sysconfig.get_config_var("HOST_GNU_TYPE") == "x86_64-pc-linux-musl"
[docs] def get_bun_bin_url(version: str, variant: str = "", mirror: str | None = None) -> str: """Construct GitHub releases URL for Bun binary Bun provides prebuilt binaries for multiple platforms in the format: bun-{platform}-{arch}[-{variant}].zip """ archmap: dict[str, str] = { "x86_64": "x64", "amd64": "x64", "AMD64": "x64", "ARM64": "aarch64", # macOS ARM "arm64": "aarch64", "aarch64": "aarch64", } sysmap: dict[str, str] = { "Darwin": "darwin", "Linux": "linux", "Windows": "windows", } arch = archmap.get(platform.machine(), platform.machine().lower()) sys_name = sysmap.get(platform.system(), platform.system().lower()) # Handle musl variant for Linux variant_str = "" if variant: variant_str = f"-{variant}" elif is_x86_64_musl() and sys_name == "linux": variant_str = "-musl" filename = f"bun-{sys_name}-{arch}{variant_str}.zip" tag = f"bun-v{version}" base_url = mirror if mirror else "https://github.com/oven-sh/bun/releases/download" return f"{base_url}/{tag}/{filename}"
[docs] @contextlib.contextmanager def zipfile_open(*args: Any, **kwargs: Any) -> Iterator[zipfile.ZipFile]: """Context manager for zipfile.""" zf = zipfile.ZipFile(*args, **kwargs) try: yield zf finally: zf.close()
def _download_bun_file(bun_url: str, n_attempt: int = 3) -> io.BytesIO: """Do multiple attempts to avoid incomplete data in case of unstable network """ while n_attempt > 0: try: return io.BytesIO(urlopen(bun_url).read()) except IncompleteRead as e: logger.warning(f"Incomplete read while reading from {bun_url} - {e}") n_attempt -= 1 if n_attempt == 0: raise e # This should never be reached, but makes type checker happy raise RuntimeError("Failed to download file")
[docs] def download_bun_bin(bun_url: str, src_dir: str, args: argparse.Namespace) -> None: """Download Bun binary zip file""" logger.info(".", extra=dict(continued=True)) dl_contents = _download_bun_file(bun_url) logger.info(".", extra=dict(continued=True)) with zipfile_open(dl_contents) as archive: archive.extractall(src_dir)
[docs] def urlopen(url: str) -> Any: home_url = "https://github.com/JacobCoffee/bunenv/" headers: dict[str, str] = {"User-Agent": f"bunenv/{bunenv_version} ({home_url})"} # Add GitHub token if provided if Config.github_token: headers["Authorization"] = f"token {Config.github_token}" req = urllib2.Request(url, None, headers) if ignore_ssl_certs: context = ssl.SSLContext(ssl.PROTOCOL_TLS) context.verify_mode = ssl.CERT_NONE return urllib2.urlopen(req, context=context) return urllib2.urlopen(req)
# --------------------------------------------------------- # Virtual environment functions
[docs] def copy_bun_from_prebuilt(env_dir: str, src_dir: str, bun_version: str) -> None: """Copy prebuilt Bun binary into environment Bun zip structure: bun-{platform}-{arch}/bun (or bun.exe on Windows) Extract to: env_dir/bin/bun (or env_dir/Scripts/bun.exe on Windows) """ logger.info(".", extra={"continued": True}) if is_WIN: dest_dir = join(env_dir, "Scripts") bun_binary = "bun.exe" else: dest_dir = join(env_dir, "bin") bun_binary = "bun" mkdir(dest_dir) # Find the extracted bun directory # It should be something like: bun-linux-x64 or bun-darwin-aarch64 import glob as glob_module bun_folders = glob_module.glob(join(src_dir, "bun-*")) if not bun_folders: raise OSError(f"Could not find extracted Bun directory in {src_dir}") bun_folder = bun_folders[0] src_binary = join(bun_folder, bun_binary) dest_binary = join(dest_dir, bun_binary) if not os.path.exists(src_binary): raise OSError(f"Could not find Bun binary at {src_binary}") # Copy the binary shutil.copy2(src_binary, dest_binary) # Make it executable on Unix-like systems if not is_WIN: make_executable(dest_binary) logger.info(".", extra={"continued": True})
[docs] def install_bun(env_dir: str, src_dir: str, args: argparse.Namespace) -> None: """Download Bun binary and install it in virtual environment. Bun only provides prebuilt binaries (no source builds). """ try: install_bun_wrapped(env_dir, src_dir, args) except Exception: # this restores the newline suppressed by continued=True logger.info("") raise
[docs] def install_bun_wrapped(env_dir: str, src_dir: str, args: argparse.Namespace) -> None: env_dir = abspath(env_dir) logger.info(f" * Install prebuilt Bun ({args.bun}) ", extra={"continued": True}) bun_url = get_bun_bin_url(args.bun, args.variant, args.mirror) # Download and extract try: download_bun_bin(bun_url, src_dir, args) except urllib2.HTTPError as e: logger.error(f"Failed to download from {bun_url}: {e}") raise logger.info(".", extra={"continued": True}) # Copy binary to environment copy_bun_from_prebuilt(env_dir, src_dir, args.bun) logger.info(" done.")
[docs] def install_packages(env_dir: str, args: argparse.Namespace) -> None: """Install packages via bun add -g""" if not args.requirements: return logger.info(" * Install packages ... ", extra={"continued": True}) bun_bin = join(env_dir, "bin", "bun") if is_WIN: bun_bin = join(env_dir, "Scripts", "bun.exe") with open(args.requirements) as f: for line in f: line = line.strip() if line and not line.startswith("#"): callit([bun_bin, "add", "-g", line], show_stdout=args.verbose, in_shell=False) logger.info("done.")
[docs] def install_activate(env_dir: str, args: argparse.Namespace) -> None: """Install virtual environment activation script""" if is_WIN: files: dict[str, str] = { "activate.bat": ACTIVATE_BAT, "deactivate.bat": DEACTIVATE_BAT, "Activate.ps1": ACTIVATE_PS1, } bin_dir = join(env_dir, "Scripts") shim_bun = join(bin_dir, "bun.exe") else: files = {"activate": ACTIVATE_SH, "activate.fish": ACTIVATE_FISH, "shim": SHIM} bin_dir = join(env_dir, "bin") shim_bun = join(bin_dir, "bun") if is_CYGWIN: mkdir(bin_dir) if args.bun == "system": files["bun"] = SHIM prompt = args.prompt or f"({os.path.basename(os.path.abspath(env_dir))})" if args.bun == "system": env: dict[str, str] = os.environ.copy() env.update({"PATH": remove_env_bin_from_path(env["PATH"], bin_dir)}) which_bun_output, _ = subprocess.Popen(["which", "bun"], stdout=subprocess.PIPE, env=env).communicate() shim_bun = clear_output(which_bun_output) assert shim_bun, "Did not find bun system executable" for name, content in files.items(): file_path = join(bin_dir, name) content = content.replace("__BUN_VIRTUAL_PROMPT__", prompt) content = content.replace("__BUN_VIRTUAL_ENV__", os.path.abspath(env_dir)) content = content.replace("__SHIM_BUN__", shim_bun) content = content.replace("__BIN_NAME__", os.path.basename(bin_dir)) # Bun-specific environment variables content = content.replace("__BUN_INSTALL__", "$BUN_VIRTUAL_ENV") content = content.replace("__BUN_INSTALL_BIN__", "$BUN_VIRTUAL_ENV/__BIN_NAME__") # Handle append for python virtualenv integration need_append = False if args.python_virtualenv: disable_prompt = DISABLE_PROMPT.get(name, "") enable_prompt = ENABLE_PROMPT.get(name, "") content = disable_prompt + content + enable_prompt need_append = bool(disable_prompt) writefile(file_path, content, append=need_append)
[docs] def set_predeactivate_hook(env_dir: str) -> None: if not is_WIN: with open(join(env_dir, "bin", "predeactivate"), "a") as hook: hook.write(PREDEACTIVATE_SH)
[docs] def create_environment(env_dir: str, args: argparse.Namespace) -> None: """Creates a new Bun environment in ``env_dir``.""" if os.path.exists(env_dir) and not args.python_virtualenv: logger.info(" * Environment already exists: %s", env_dir) if not args.force: sys.exit(2) src_dir = abspath(join(env_dir, "src")) mkdir(src_dir) if args.bun != "system": install_bun(env_dir, src_dir, args) else: # Create basic directory structure for system bun mkdir(join(env_dir, "bin")) # Bun manages its own package cache mkdir(join(env_dir, "install")) mkdir(join(env_dir, "install", "cache")) # activate script install install_activate(env_dir, args) if args.requirements: install_packages(env_dir, args) if args.python_virtualenv: set_predeactivate_hook(env_dir) # Cleanup if args.clean_src: shutil.rmtree(src_dir)
def _get_versions_json() -> list[dict[str, Any]]: """Fetch Bun versions from GitHub Releases API""" headers: dict[str, str] = {} if Config.github_token: headers["Authorization"] = f"token {Config.github_token}" url = "https://api.github.com/repos/oven-sh/bun/releases" req = urllib2.Request(url, None, headers) if ignore_ssl_certs: context = ssl.SSLContext(ssl.PROTOCOL_TLS) context.verify_mode = ssl.CERT_NONE response = urllib2.urlopen(req, context=context) else: response = urllib2.urlopen(req) releases = json.loads(response.read().decode("UTF-8")) # Transform to compatible format return [ { "version": r["tag_name"].replace("bun-v", ""), "lts": False, # Bun doesn't have LTS concept "tag_name": r["tag_name"], "assets": r.get("assets", []), } for r in releases if r["tag_name"].startswith("bun-v") ]
[docs] def get_bun_versions() -> list[str]: """Return all available Bun versions""" return [dct["version"] for dct in _get_versions_json()]
[docs] def get_last_stable_bun_version() -> str | None: """Return last stable Bun version (first in the list from GitHub)""" versions = get_bun_versions() if versions: return versions[0] return None
[docs] def get_env_dir(args: argparse.Namespace) -> Any: if args.python_virtualenv: if sys.base_prefix != sys.prefix or "CONDA_PREFIX" in os.environ: res = sys.prefix else: logger.error("No python virtualenv is available") sys.exit(2) else: res = args.env_dir return res
# noinspection PyProtectedMember
[docs] def main() -> None: """Entry point""" # quick&dirty way to help update the README if "--dump-config-defaults" in sys.argv: Config._dump() return args = parse_args(check=False) # noinspection PyProtectedMember Config._load(args.config_file, args.verbose) args = parse_args() if args.bun.lower() == "system" and is_WIN: logger.error("Installing system bun on Windows is not supported!") exit(1) global ignore_ssl_certs ignore_ssl_certs = args.ignore_ssl_certs # Set GitHub token from args if provided if args.github_token: Config.github_token = args.github_token # Handle version resolution if not args.bun or args.bun.lower() == "latest": args.bun = get_last_stable_bun_version() if not args.bun: logger.error("Could not determine latest Bun version") sys.exit(1) if args.list: print_bun_versions() elif args.update: env_dir = get_env_dir(args) install_packages(env_dir, args) else: env_dir = get_env_dir(args) create_environment(env_dir, args)
# --------------------------------------------------------- # Shell scripts content DISABLE_PROMPT: dict[str, str] = { "activate": """ # disable bunenv's prompt # (prompt already changed by original virtualenv's script) BUN_VIRTUAL_ENV_DISABLE_PROMPT=1 """, "activate.fish": """ # disable bunenv's prompt # (prompt already changed by original virtualenv's script) set BUN_VIRTUAL_ENV_DISABLE_PROMPT 1 """, } ENABLE_PROMPT: dict[str, str] = { "activate": """ unset BUN_VIRTUAL_ENV_DISABLE_PROMPT """, "activate.fish": """ set -e BUN_VIRTUAL_ENV_DISABLE_PROMPT """, } SHIM: str = """#!/usr/bin/env bash export BUN_INSTALL='__BUN_VIRTUAL_ENV__' export BUN_INSTALL_BIN='__BUN_VIRTUAL_ENV__/__BIN_NAME__' exec '__SHIM_BUN__' "$@" """ ACTIVATE_BAT: str = r""" @echo off set "BUN_VIRTUAL_ENV=__BUN_VIRTUAL_ENV__" if not defined PROMPT ( set "PROMPT=$P$G" ) if defined _OLD_VIRTUAL_PROMPT ( set "PROMPT=%_OLD_VIRTUAL_PROMPT%" ) if defined _OLD_VIRTUAL_BUN_INSTALL ( set "BUN_INSTALL=%_OLD_VIRTUAL_BUN_INSTALL%" ) set "_OLD_VIRTUAL_PROMPT=%PROMPT%" set "PROMPT=__BUN_VIRTUAL_PROMPT__ %PROMPT%" if defined BUN_INSTALL ( set "_OLD_VIRTUAL_BUN_INSTALL=%BUN_INSTALL%" ) set "BUN_INSTALL=%BUN_VIRTUAL_ENV%" if defined _OLD_VIRTUAL_PATH ( set "PATH=%_OLD_VIRTUAL_PATH%" ) else ( set "_OLD_VIRTUAL_PATH=%PATH%" ) set "PATH=%BUN_VIRTUAL_ENV%\Scripts;%PATH%" :END """ DEACTIVATE_BAT: str = """\ @echo off if defined _OLD_VIRTUAL_PROMPT ( set "PROMPT=%_OLD_VIRTUAL_PROMPT%" ) set _OLD_VIRTUAL_PROMPT= if defined _OLD_VIRTUAL_BUN_INSTALL ( set "BUN_INSTALL=%_OLD_VIRTUAL_BUN_INSTALL%" set _OLD_VIRTUAL_BUN_INSTALL= ) if defined _OLD_VIRTUAL_PATH ( set "PATH=%_OLD_VIRTUAL_PATH%" ) set _OLD_VIRTUAL_PATH= set BUN_VIRTUAL_ENV= :END """ ACTIVATE_PS1: str = r""" function global:deactivate ([switch]$NonDestructive) { # Revert to original values if (Test-Path function:_OLD_VIRTUAL_PROMPT) { copy-item function:_OLD_VIRTUAL_PROMPT function:prompt remove-item function:_OLD_VIRTUAL_PROMPT } if (Test-Path env:_OLD_VIRTUAL_BUN_INSTALL) { copy-item env:_OLD_VIRTUAL_BUN_INSTALL env:BUN_INSTALL remove-item env:_OLD_VIRTUAL_BUN_INSTALL } if (Test-Path env:_OLD_VIRTUAL_PATH) { copy-item env:_OLD_VIRTUAL_PATH env:PATH remove-item env:_OLD_VIRTUAL_PATH } if (Test-Path env:BUN_VIRTUAL_ENV) { remove-item env:BUN_VIRTUAL_ENV } if (!$NonDestructive) { # Self destruct! remove-item function:deactivate } } deactivate -nondestructive $env:BUN_VIRTUAL_ENV="__BUN_VIRTUAL_ENV__" # Set the prompt to include the env name # Make sure _OLD_VIRTUAL_PROMPT is global function global:_OLD_VIRTUAL_PROMPT {""} copy-item function:prompt function:_OLD_VIRTUAL_PROMPT function global:prompt { Write-Host -NoNewline -ForegroundColor Green '__BUN_VIRTUAL_PROMPT__ ' _OLD_VIRTUAL_PROMPT } # Set BUN_INSTALL if (Test-Path env:BUN_INSTALL) { copy-item env:BUN_INSTALL env:_OLD_VIRTUAL_BUN_INSTALL } $env:BUN_INSTALL = "$env:BUN_VIRTUAL_ENV" # Add the venv to the PATH copy-item env:PATH env:_OLD_VIRTUAL_PATH $env:PATH = "$env:BUN_VIRTUAL_ENV\Scripts;$env:PATH" """ ACTIVATE_SH: str = r""" # This file must be used with "source bin/activate" *from bash* # you cannot run it directly deactivate_bun () { # reset old environment variables if [ -n "${_OLD_BUN_VIRTUAL_PATH:-}" ] ; then PATH="${_OLD_BUN_VIRTUAL_PATH:-}" export PATH unset _OLD_BUN_VIRTUAL_PATH BUN_INSTALL="${_OLD_BUN_INSTALL:-}" export BUN_INSTALL unset _OLD_BUN_INSTALL BUN_INSTALL_BIN="${_OLD_BUN_INSTALL_BIN:-}" export BUN_INSTALL_BIN unset _OLD_BUN_INSTALL_BIN fi # This should detect bash and zsh, which have a hash command that must # be called to get it to forget past commands. Without forgetting # past commands the $PATH changes we made may not be respected if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then hash -r fi if [ -n "${_OLD_BUN_VIRTUAL_PS1:-}" ] ; then PS1="${_OLD_BUN_VIRTUAL_PS1:-}" export PS1 unset _OLD_BUN_VIRTUAL_PS1 fi unset BUN_VIRTUAL_ENV if [ ! "${1:-}" = "nondestructive" ] ; then # Self destruct! unset -f deactivate_bun fi } # unset irrelevant variables deactivate_bun nondestructive # find the directory of this script # http://stackoverflow.com/a/246128 if [ "${BASH_SOURCE:-}" ] ; then SOURCE="${BASH_SOURCE[0]}" while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done DIR="$( command cd -P "$( dirname "$SOURCE" )" > /dev/null && pwd )" BUN_VIRTUAL_ENV="$(dirname "$DIR")" else # dash not movable. fix use case: # dash -c " . bun-env/bin/activate && bun -v" BUN_VIRTUAL_ENV="__BUN_VIRTUAL_ENV__" fi # BUN_VIRTUAL_ENV is the parent of the directory where this script is export BUN_VIRTUAL_ENV _OLD_BUN_VIRTUAL_PATH="$PATH" PATH="$BUN_VIRTUAL_ENV/__BIN_NAME__:$PATH" export PATH _OLD_BUN_INSTALL="${BUN_INSTALL:-}" BUN_INSTALL="$BUN_VIRTUAL_ENV" export BUN_INSTALL _OLD_BUN_INSTALL_BIN="${BUN_INSTALL_BIN:-}" BUN_INSTALL_BIN="$BUN_VIRTUAL_ENV/__BIN_NAME__" export BUN_INSTALL_BIN if [ -z "${BUN_VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then _OLD_BUN_VIRTUAL_PS1="${PS1:-}" if [ "x__BUN_VIRTUAL_PROMPT__" != x ] ; then PS1="__BUN_VIRTUAL_PROMPT__ ${PS1:-}" else if [ "`basename \"$BUN_VIRTUAL_ENV\"`" = "__" ] ; then # special case for Aspen magic directories # see http://www.zetadev.com/software/aspen/ PS1="[`basename \`dirname \"$BUN_VIRTUAL_ENV\"\``] ${PS1:-}" else PS1="(`basename \"$BUN_VIRTUAL_ENV\"`) ${PS1:-}" fi fi export PS1 fi # This should detect bash and zsh, which have a hash command that must # be called to get it to forget past commands. Without forgetting # past commands the $PATH changes we made may not be respected if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then hash -r fi """ ACTIVATE_FISH: str = """ # This file must be used with "source bin/activate.fish" *from fish* # you cannot run it directly function deactivate_bun -d 'Exit bunenv and return to normal environment.' # reset old environment variables if test -n "$_OLD_BUN_VIRTUAL_PATH" set -gx PATH $_OLD_BUN_VIRTUAL_PATH set -e _OLD_BUN_VIRTUAL_PATH end if test -n "$_OLD_BUN_INSTALL" set -gx BUN_INSTALL $_OLD_BUN_INSTALL set -e _OLD_BUN_INSTALL else set -e BUN_INSTALL end if test -n "$_OLD_BUN_INSTALL_BIN" set -gx BUN_INSTALL_BIN $_OLD_BUN_INSTALL_BIN set -e _OLD_BUN_INSTALL_BIN else set -e BUN_INSTALL_BIN end if test -n "$_OLD_BUN_FISH_PROMPT_OVERRIDE" # Set an empty local `$fish_function_path` to allow the removal of # `fish_prompt` using `functions -e`. set -l fish_function_path # Prevents error when using nested fish instances if functions -q _bun_old_fish_prompt # Erase virtualenv's `fish_prompt` and restore the original. functions -e fish_prompt functions -c _bun_old_fish_prompt fish_prompt functions -e _bun_old_fish_prompt end set -e _OLD_BUN_FISH_PROMPT_OVERRIDE end set -e BUN_VIRTUAL_ENV if test (count $argv) = 0 -o "$argv[1]" != "nondestructive" # Self destruct! functions -e deactivate_bun end end # unset irrelevant variables deactivate_bun nondestructive # find the directory of this script begin set -l SOURCE (status filename) while test -L "$SOURCE" set SOURCE (readlink "$SOURCE") end set -l DIR (dirname (realpath "$SOURCE")) # BUN_VIRTUAL_ENV is the parent of the directory where this script is set -gx BUN_VIRTUAL_ENV (dirname "$DIR") end set -gx _OLD_BUN_VIRTUAL_PATH $PATH set -gx PATH "$BUN_VIRTUAL_ENV/__BIN_NAME__" $PATH if set -q BUN_INSTALL set -gx _OLD_BUN_INSTALL $BUN_INSTALL end set -gx BUN_INSTALL "$BUN_VIRTUAL_ENV" if set -q BUN_INSTALL_BIN set -gx _OLD_BUN_INSTALL_BIN $BUN_INSTALL_BIN end set -gx BUN_INSTALL_BIN "$BUN_VIRTUAL_ENV/__BIN_NAME__" if test -z "$BUN_VIRTUAL_ENV_DISABLE_PROMPT" # Copy the current `fish_prompt` function as `_bun_old_fish_prompt`. functions -c fish_prompt _bun_old_fish_prompt function fish_prompt # Save the current $status, for fish_prompts that display it. set -l old_status $status # Prompt override provided? # If not, just prepend the environment name. if test -n "__BUN_VIRTUAL_PROMPT__" printf '%s%s ' "__BUN_VIRTUAL_PROMPT__" (set_color normal) else printf '%s(%s) ' (set_color normal) (basename "$BUN_VIRTUAL_ENV") end # Restore the original $status echo "exit $old_status" | source _bun_old_fish_prompt end set -gx _OLD_BUN_FISH_PROMPT_OVERRIDE "$BUN_VIRTUAL_ENV" end """ PREDEACTIVATE_SH: str = """ if type -p deactivate_bun > /dev/null; then deactivate_bun;fi """ if __name__ == "__main__": main()