From 3c8c7c3c46ffc02877acc7e27636a34d80fbb686 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 11 Apr 2024 16:43:01 -0700 Subject: [PATCH] [REF-1586] Use bun as a package manager on windows (#2359) --- reflex/constants/__init__.py | 2 + reflex/constants/base.py | 5 +++ reflex/constants/compiler.py | 2 + reflex/constants/installer.py | 6 ++- reflex/utils/console.py | 11 +++++- reflex/utils/prerequisites.py | 73 +++++++++++++++++++---------------- reflex/utils/processes.py | 37 ++++++++++++++++++ 7 files changed, 100 insertions(+), 36 deletions(-) diff --git a/reflex/constants/__init__.py b/reflex/constants/__init__.py index 1f3325a8a39..c5d3586cea8 100644 --- a/reflex/constants/__init__.py +++ b/reflex/constants/__init__.py @@ -4,6 +4,7 @@ COOKIES, ENV_MODE_ENV_VAR, IS_WINDOWS, + IS_WINDOWS_BUN_SUPPORTED_MACHINE, # type: ignore LOCAL_STORAGE, POLLING_MAX_HTTP_BUFFER_SIZE, PYTEST_CURRENT_TEST, @@ -86,6 +87,7 @@ Hooks, Imports, IS_WINDOWS, + IS_WINDOWS_BUN_SUPPORTED_MACHINE, LOCAL_STORAGE, LogLevel, MemoizationDisposition, diff --git a/reflex/constants/base.py b/reflex/constants/base.py index 733859ad4b1..820147ed2fc 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -11,6 +11,11 @@ from platformdirs import PlatformDirs 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): diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index b37c0d1ce96..b99e31e8c70 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -26,6 +26,8 @@ class Ext(SimpleNamespace): CSS = ".css" # The extension for zip files. ZIP = ".zip" + # The extension for executable files on Windows. + EXE = ".exe" class CompileVars(SimpleNamespace): diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 0bac7f5ada5..2560c3451b3 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -35,13 +35,15 @@ class Bun(SimpleNamespace): """Bun constants.""" # The Bun version. - VERSION = "1.0.13" + VERSION = "1.1.3" # Min Bun Version MIN_VERSION = "0.7.0" # The directory to store the bun. ROOT_PATH = os.path.join(Reflex.DIR, "bun") # 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. INSTALL_URL = "https://bun.sh/install" diff --git a/reflex/utils/console.py b/reflex/utils/console.py index 3dfff2828c0..82339f95e35 100644 --- a/reflex/utils/console.py +++ b/reflex/utils/console.py @@ -28,6 +28,15 @@ def set_log_level(log_level: LogLevel): _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): """Print a message. @@ -45,7 +54,7 @@ def debug(msg: str, **kwargs): msg: The debug message. kwargs: Keyword arguments to pass to the print function. """ - if _LOG_LEVEL <= LogLevel.DEBUG: + if is_debug(): msg_ = f"[blue]Debug: {msg}[/blue]" if progress := kwargs.pop("progress", None): progress.console.print(msg_, **kwargs) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 26d4d78d79d..98154566660 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -167,16 +167,13 @@ def get_bun_version() -> version.Version | None: def get_install_package_manager() -> str | None: """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: The path to the package manager. """ - # On Windows, we use npm instead of bun. - if constants.IS_WINDOWS: + if constants.IS_WINDOWS and not constants.IS_WINDOWS_BUN_SUPPORTED_MACHINE: return get_package_manager() - - # On other platforms, we use bun. return get_config().bun_path @@ -729,10 +726,10 @@ def install_bun(): Raises: FileNotFoundError: If required packages are not found. """ - # Bun is not supported on Windows. - if constants.IS_WINDOWS: - console.debug("Skipping bun installation on Windows.") - return + if constants.IS_WINDOWS and not constants.IS_WINDOWS_BUN_SUPPORTED_MACHINE: + console.warn( + "Bun for Windows is currently only available for x86 64-bit Windows. Installation will fall back on npm." + ) # Skip if bun is already installed. if os.path.exists(get_config().bun_path) and get_bun_version() == version.parse( @@ -742,16 +739,25 @@ def install_bun(): return # if unzip is installed - unzip_path = path_ops.which("unzip") - if unzip_path is None: - raise FileNotFoundError("Reflex requires unzip to be installed.") - - # Run the bun install script. - download_and_run( - constants.Bun.INSTALL_URL, - f"bun-v{constants.Bun.VERSION}", - BUN_INSTALL=constants.Bun.ROOT_PATH, - ) + if constants.IS_WINDOWS: + processes.new_process( + ["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. + download_and_run( + constants.Bun.INSTALL_URL, + f"bun-v{constants.Bun.VERSION}", + BUN_INSTALL=constants.Bun.ROOT_PATH, + ) def _write_cached_procedure_file(payload: str, cache_file: str): @@ -813,18 +819,22 @@ def install_frontend_packages(packages: set[str], config: Config): Example: >>> install_frontend_packages(["react", "react-dom"], get_config()) """ - # Install the base packages. - process = processes.new_process( + # unsupported archs will use npm anyway. so we dont have to run npm twice + 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"], + fallback=fallback_command, + show_status_message="Installing base frontend packages", cwd=constants.Dirs.WEB, shell=constants.IS_WINDOWS, ) - processes.show_status("Installing base frontend packages", process) - if config.tailwind is not None: - # install tailwind and tailwind plugins as dev dependencies. - process = processes.new_process( + processes.run_process_with_fallback( [ get_install_package_manager(), "add", @@ -832,21 +842,21 @@ def install_frontend_packages(packages: set[str], config: Config): constants.Tailwind.VERSION, *((config.tailwind or {}).get("plugins", [])), ], + fallback=fallback_command, + show_status_message="Installing tailwind", cwd=constants.Dirs.WEB, shell=constants.IS_WINDOWS, ) - processes.show_status("Installing tailwind", process) # Install custom packages defined in frontend_packages if len(packages) > 0: - process = processes.new_process( + processes.run_process_with_fallback( [get_install_package_manager(), "add", *packages], + fallback=fallback_command, + show_status_message="Installing frontend packages from config and components", cwd=constants.Dirs.WEB, shell=constants.IS_WINDOWS, ) - processes.show_status( - "Installing frontend packages from config and components", process - ) def needs_reinit(frontend: bool = True) -> bool: @@ -953,9 +963,6 @@ def validate_frontend_dependencies(init=True): ) raise typer.Exit(1) - if constants.IS_WINDOWS: - return - if init: # we only need bun for package install on `reflex init`. validate_bun() diff --git a/reflex/utils/processes.py b/reflex/utils/processes.py index 12bedb67c0d..06fa755f677 100644 --- a/reflex/utils/processes.py +++ b/reflex/utils/processes.py @@ -287,3 +287,40 @@ def show_progress(message: str, process: subprocess.Popen, checkpoints: List[str def atexit_handler(): """Display a custom message with the current time when exiting an app.""" 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)