Ver Fonte

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

Masen Furer há 1 ano atrás
pai
commit
3c8c7c3c46

+ 2 - 0
reflex/constants/__init__.py

@@ -4,6 +4,7 @@ from .base import (
     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 @@ __ALL__ = [
     Hooks,
     Imports,
     IS_WINDOWS,
+    IS_WINDOWS_BUN_SUPPORTED_MACHINE,
     LOCAL_STORAGE,
     LogLevel,
     MemoizationDisposition,

+ 5 - 0
reflex/constants/base.py

@@ -11,6 +11,11 @@ from types import SimpleNamespace
 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):

+ 2 - 0
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):

+ 4 - 2
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"
 

+ 10 - 1
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)

+ 40 - 33
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()

+ 37 - 0
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)