[REF-1586] Use bun as a package manager on windows (#2359)

This commit is contained in:
Masen Furer 2024-04-11 16:43:01 -07:00 committed by GitHub
parent e377ce70b6
commit 3c8c7c3c46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 99 additions and 35 deletions

View File

@ -4,6 +4,7 @@ from .base import (
COOKIES, COOKIES,
ENV_MODE_ENV_VAR, ENV_MODE_ENV_VAR,
IS_WINDOWS, IS_WINDOWS,
IS_WINDOWS_BUN_SUPPORTED_MACHINE, # type: ignore
LOCAL_STORAGE, LOCAL_STORAGE,
POLLING_MAX_HTTP_BUFFER_SIZE, POLLING_MAX_HTTP_BUFFER_SIZE,
PYTEST_CURRENT_TEST, PYTEST_CURRENT_TEST,
@ -86,6 +87,7 @@ __ALL__ = [
Hooks, Hooks,
Imports, Imports,
IS_WINDOWS, IS_WINDOWS,
IS_WINDOWS_BUN_SUPPORTED_MACHINE,
LOCAL_STORAGE, LOCAL_STORAGE,
LogLevel, LogLevel,
MemoizationDisposition, MemoizationDisposition,

View File

@ -11,6 +11,11 @@ from types import SimpleNamespace
from platformdirs import PlatformDirs from platformdirs import PlatformDirs
IS_WINDOWS = platform.system() == "Windows" IS_WINDOWS = platform.system() == "Windows"
# https://github.com/oven-sh/bun/blob/main/src/cli/install.ps1
IS_WINDOWS_BUN_SUPPORTED_MACHINE = IS_WINDOWS and platform.machine() in [
"AMD64",
"x86_64",
] # filter out 32 bit + ARM
class Dirs(SimpleNamespace): class Dirs(SimpleNamespace):

View File

@ -26,6 +26,8 @@ class Ext(SimpleNamespace):
CSS = ".css" CSS = ".css"
# The extension for zip files. # The extension for zip files.
ZIP = ".zip" ZIP = ".zip"
# The extension for executable files on Windows.
EXE = ".exe"
class CompileVars(SimpleNamespace): class CompileVars(SimpleNamespace):

View File

@ -35,13 +35,15 @@ class Bun(SimpleNamespace):
"""Bun constants.""" """Bun constants."""
# The Bun version. # The Bun version.
VERSION = "1.0.13" VERSION = "1.1.3"
# Min Bun Version # Min Bun Version
MIN_VERSION = "0.7.0" MIN_VERSION = "0.7.0"
# The directory to store the bun. # The directory to store the bun.
ROOT_PATH = os.path.join(Reflex.DIR, "bun") ROOT_PATH = os.path.join(Reflex.DIR, "bun")
# Default bun path. # Default bun path.
DEFAULT_PATH = os.path.join(ROOT_PATH, "bin", "bun") DEFAULT_PATH = os.path.join(
ROOT_PATH, "bin", "bun" if not IS_WINDOWS else "bun.exe"
)
# URL to bun install script. # URL to bun install script.
INSTALL_URL = "https://bun.sh/install" INSTALL_URL = "https://bun.sh/install"

View File

@ -28,6 +28,15 @@ def set_log_level(log_level: LogLevel):
_LOG_LEVEL = log_level _LOG_LEVEL = log_level
def is_debug() -> bool:
"""Check if the log level is debug.
Returns:
True if the log level is debug.
"""
return _LOG_LEVEL <= LogLevel.DEBUG
def print(msg: str, **kwargs): def print(msg: str, **kwargs):
"""Print a message. """Print a message.
@ -45,7 +54,7 @@ def debug(msg: str, **kwargs):
msg: The debug message. msg: The debug message.
kwargs: Keyword arguments to pass to the print function. kwargs: Keyword arguments to pass to the print function.
""" """
if _LOG_LEVEL <= LogLevel.DEBUG: if is_debug():
msg_ = f"[blue]Debug: {msg}[/blue]" msg_ = f"[blue]Debug: {msg}[/blue]"
if progress := kwargs.pop("progress", None): if progress := kwargs.pop("progress", None):
progress.console.print(msg_, **kwargs) progress.console.print(msg_, **kwargs)

View File

@ -167,16 +167,13 @@ def get_bun_version() -> version.Version | None:
def get_install_package_manager() -> str | None: def get_install_package_manager() -> str | None:
"""Get the package manager executable for installation. """Get the package manager executable for installation.
Currently on unix systems, bun is used for installation only. Currently, bun is used for installation only.
Returns: Returns:
The path to the package manager. The path to the package manager.
""" """
# On Windows, we use npm instead of bun. if constants.IS_WINDOWS and not constants.IS_WINDOWS_BUN_SUPPORTED_MACHINE:
if constants.IS_WINDOWS:
return get_package_manager() return get_package_manager()
# On other platforms, we use bun.
return get_config().bun_path return get_config().bun_path
@ -729,10 +726,10 @@ def install_bun():
Raises: Raises:
FileNotFoundError: If required packages are not found. FileNotFoundError: If required packages are not found.
""" """
# Bun is not supported on Windows. if constants.IS_WINDOWS and not constants.IS_WINDOWS_BUN_SUPPORTED_MACHINE:
if constants.IS_WINDOWS: console.warn(
console.debug("Skipping bun installation on Windows.") "Bun for Windows is currently only available for x86 64-bit Windows. Installation will fall back on npm."
return )
# Skip if bun is already installed. # Skip if bun is already installed.
if os.path.exists(get_config().bun_path) and get_bun_version() == version.parse( if os.path.exists(get_config().bun_path) and get_bun_version() == version.parse(
@ -742,16 +739,25 @@ def install_bun():
return return
# if unzip is installed # if unzip is installed
unzip_path = path_ops.which("unzip") if constants.IS_WINDOWS:
if unzip_path is None: processes.new_process(
raise FileNotFoundError("Reflex requires unzip to be installed.") ["powershell", "-c", f"irm {constants.Bun.INSTALL_URL}.ps1|iex"],
env={"BUN_INSTALL": constants.Bun.ROOT_PATH},
shell=True,
run=True,
show_logs=console.is_debug(),
)
else:
unzip_path = path_ops.which("unzip")
if unzip_path is None:
raise FileNotFoundError("Reflex requires unzip to be installed.")
# Run the bun install script. # Run the bun install script.
download_and_run( download_and_run(
constants.Bun.INSTALL_URL, constants.Bun.INSTALL_URL,
f"bun-v{constants.Bun.VERSION}", f"bun-v{constants.Bun.VERSION}",
BUN_INSTALL=constants.Bun.ROOT_PATH, BUN_INSTALL=constants.Bun.ROOT_PATH,
) )
def _write_cached_procedure_file(payload: str, cache_file: str): def _write_cached_procedure_file(payload: str, cache_file: str):
@ -813,18 +819,22 @@ def install_frontend_packages(packages: set[str], config: Config):
Example: Example:
>>> install_frontend_packages(["react", "react-dom"], get_config()) >>> install_frontend_packages(["react", "react-dom"], get_config())
""" """
# Install the base packages. # unsupported archs will use npm anyway. so we dont have to run npm twice
process = processes.new_process( fallback_command = (
get_package_manager()
if constants.IS_WINDOWS and constants.IS_WINDOWS_BUN_SUPPORTED_MACHINE
else None
)
processes.run_process_with_fallback(
[get_install_package_manager(), "install", "--loglevel", "silly"], [get_install_package_manager(), "install", "--loglevel", "silly"],
fallback=fallback_command,
show_status_message="Installing base frontend packages",
cwd=constants.Dirs.WEB, cwd=constants.Dirs.WEB,
shell=constants.IS_WINDOWS, shell=constants.IS_WINDOWS,
) )
processes.show_status("Installing base frontend packages", process)
if config.tailwind is not None: if config.tailwind is not None:
# install tailwind and tailwind plugins as dev dependencies. processes.run_process_with_fallback(
process = processes.new_process(
[ [
get_install_package_manager(), get_install_package_manager(),
"add", "add",
@ -832,21 +842,21 @@ def install_frontend_packages(packages: set[str], config: Config):
constants.Tailwind.VERSION, constants.Tailwind.VERSION,
*((config.tailwind or {}).get("plugins", [])), *((config.tailwind or {}).get("plugins", [])),
], ],
fallback=fallback_command,
show_status_message="Installing tailwind",
cwd=constants.Dirs.WEB, cwd=constants.Dirs.WEB,
shell=constants.IS_WINDOWS, shell=constants.IS_WINDOWS,
) )
processes.show_status("Installing tailwind", process)
# Install custom packages defined in frontend_packages # Install custom packages defined in frontend_packages
if len(packages) > 0: if len(packages) > 0:
process = processes.new_process( processes.run_process_with_fallback(
[get_install_package_manager(), "add", *packages], [get_install_package_manager(), "add", *packages],
fallback=fallback_command,
show_status_message="Installing frontend packages from config and components",
cwd=constants.Dirs.WEB, cwd=constants.Dirs.WEB,
shell=constants.IS_WINDOWS, shell=constants.IS_WINDOWS,
) )
processes.show_status(
"Installing frontend packages from config and components", process
)
def needs_reinit(frontend: bool = True) -> bool: def needs_reinit(frontend: bool = True) -> bool:
@ -953,9 +963,6 @@ def validate_frontend_dependencies(init=True):
) )
raise typer.Exit(1) raise typer.Exit(1)
if constants.IS_WINDOWS:
return
if init: if init:
# we only need bun for package install on `reflex init`. # we only need bun for package install on `reflex init`.
validate_bun() validate_bun()

View File

@ -287,3 +287,40 @@ def show_progress(message: str, process: subprocess.Popen, checkpoints: List[str
def atexit_handler(): def atexit_handler():
"""Display a custom message with the current time when exiting an app.""" """Display a custom message with the current time when exiting an app."""
console.log("Reflex app stopped.") console.log("Reflex app stopped.")
def run_process_with_fallback(args, *, show_status_message, fallback=None, **kwargs):
"""Run subprocess and retry using fallback command if initial command fails.
Args:
args: A string, or a sequence of program arguments.
show_status_message: The status message to be displayed in the console.
fallback: The fallback command to run.
kwargs: Kwargs to pass to new_process function.
"""
def execute_process(process):
if not constants.IS_WINDOWS:
show_status(show_status_message, process)
else:
process.wait()
if process.returncode != 0:
error_output = process.stderr if process.stderr else process.stdout
error_message = f"Error occurred during subprocess execution: {' '.join(args)}\n{error_output.read() if error_output else ''}"
# Only show error in debug mode.
if console.is_debug():
console.error(error_message)
# retry with fallback command.
fallback_args = [fallback, *args[1:]] if fallback else None
console.warn(
f"There was an error running command: {args}. Falling back to: {fallback_args}."
)
if fallback_args:
process = new_process(fallback_args, **kwargs)
execute_process(process)
else:
show_status(show_status_message, process)
process = new_process(args, **kwargs)
execute_process(process)