diff --git a/reflex/config.py b/reflex/config.py index a4aea6111..d4002e30b 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -168,6 +168,9 @@ class Config(Base): # Additional frontend packages to install. frontend_packages: List[str] = [] + # The bun path + bun_path: str = constants.BUN_PATH + # The Admin Dash. admin_dash: Optional[AdminDash] = None diff --git a/reflex/constants.py b/reflex/constants.py index 1080c7798..83eff1baf 100644 --- a/reflex/constants.py +++ b/reflex/constants.py @@ -65,10 +65,14 @@ JINJA_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, "jinja") # Bun config. # The Bun version. BUN_VERSION = "0.7.0" +# Min Bun Version +MIN_BUN_VERSION = "0.7.0" # The directory to store the bun. BUN_ROOT_PATH = os.path.join(REFLEX_DIR, ".bun") +# Default bun path. +DEFAULT_BUN_PATH = os.path.join(BUN_ROOT_PATH, "bin", "bun") # The bun path. -BUN_PATH = os.path.join(BUN_ROOT_PATH, "bin", "bun") +BUN_PATH = get_value("BUN_PATH", DEFAULT_BUN_PATH) # URL to bun install script. BUN_INSTALL_URL = "https://bun.sh/install" diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 0386e8f2f..7c704fe16 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -57,7 +57,7 @@ def get_bun_version() -> Optional[version.Version]: """ try: # Run the bun -v command and capture the output - result = processes.new_process([constants.BUN_PATH, "-v"], run=True) + result = processes.new_process([get_config().bun_path, "-v"], run=True) return version.parse(result.stdout) # type: ignore except FileNotFoundError: return None @@ -92,7 +92,7 @@ def get_install_package_manager() -> str: return get_windows_package_manager() # On other platforms, we use bun. - return constants.BUN_PATH + return get_config().bun_path def get_package_manager() -> str: @@ -245,36 +245,13 @@ def initialize_web_directory(): json.dump(reflex_json, f, ensure_ascii=False) -def initialize_bun(): - """Check that bun requirements are met, and install if not.""" - if IS_WINDOWS: - # Bun is not supported on Windows. - console.debug("Skipping bun installation on Windows.") - return - - # Check the bun version. - bun_version = get_bun_version() - if bun_version != version.parse(constants.BUN_VERSION): - console.debug( - f"Current bun version ({bun_version}) does not match ({constants.BUN_VERSION})." - ) - remove_existing_bun_installation() - install_bun() - - def remove_existing_bun_installation(): """Remove existing bun installation.""" console.debug("Removing existing bun installation.") - if os.path.exists(constants.BUN_PATH): + if os.path.exists(get_config().bun_path): path_ops.rm(constants.BUN_ROOT_PATH) -def initialize_node(): - """Validate nodejs have install or not.""" - if not check_node_version(): - install_node() - - def download_and_run(url: str, *args, show_status: bool = False, **env): """Download and run a script. @@ -352,7 +329,7 @@ def install_bun(): return # Skip if bun is already installed. - if os.path.exists(constants.BUN_PATH): + if os.path.exists(get_config().bun_path): console.debug("Skipping bun installation as it is already installed.") return @@ -435,12 +412,46 @@ def is_latest_template() -> bool: return app_version == constants.VERSION +def validate_bun(): + """Validate bun if a custom bun path is specified to ensure the bun version meets requirements. + + Raises: + Exit: If custom specified bun does not exist or does not meet requirements. + """ + # if a custom bun path is provided, make sure its valid + # This is specific to non-FHS OS + bun_path = get_config().bun_path + if bun_path != constants.DEFAULT_BUN_PATH: + bun_version = get_bun_version() + if not bun_version: + console.error( + "Failed to obtain bun version. Make sure the specified bun path in your config is correct." + ) + raise typer.Exit(1) + elif bun_version < version.parse(constants.MIN_BUN_VERSION): + console.error( + f"Reflex requires bun version {constants.BUN_VERSION} or higher to run, but the detected version is " + f"{bun_version}. If you have specified a custom bun path in your config, make sure to provide one " + f"that satisfies the minimum version requirement." + ) + + raise typer.Exit(1) + + +def validate_frontend_dependencies(): + """Validate frontend dependencies to ensure they meet requirements.""" + if IS_WINDOWS: + return + return validate_bun() + + def initialize_frontend_dependencies(): """Initialize all the frontend dependencies.""" # Create the reflex directory. if not IS_WINDOWS: path_ops.mkdir(constants.REFLEX_DIR) - + # validate dependencies before install + validate_frontend_dependencies() # Install the frontend dependencies. processes.run_concurrently(install_node, install_bun) diff --git a/tests/test_utils.py b/tests/test_utils.py index 765b3d239..e3e496a74 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -255,36 +255,38 @@ def test_format_route(route: str, expected: bool): assert format.format_route(route) == expected -@pytest.mark.parametrize( - "bun_version,is_valid, prompt_input", - [ - (V055, False, "yes"), - (V059, True, None), - (VMAXPLUS1, False, "yes"), - ], -) -def test_initialize_bun(mocker, bun_version, is_valid, prompt_input): - """Test that the bun version on host system is validated properly. Also test that - the required bun version is installed should the user opt for it. +def test_validate_invalid_bun_path(mocker): + """Test that an error is thrown when a custom specified bun path is not valid + or does not exist. Args: mocker: Pytest mocker object. - bun_version: The bun version. - is_valid: Whether bun version is valid for running reflex. - prompt_input: The input from user on whether to install bun. """ - mocker.patch("reflex.utils.prerequisites.get_bun_version", return_value=bun_version) - mocker.patch("reflex.utils.prerequisites.IS_WINDOWS", False) + mock = mocker.Mock() + mocker.patch.object(mock, "bun_path", return_value="/mock/path") + mocker.patch("reflex.utils.prerequisites.get_config", mock) + mocker.patch("reflex.utils.prerequisites.get_bun_version", return_value=None) - bun_install = mocker.patch("reflex.utils.prerequisites.install_bun") - remove_existing_bun_installation = mocker.patch( - "reflex.utils.prerequisites.remove_existing_bun_installation" + with pytest.raises(typer.Exit): + prerequisites.validate_bun() + + +def test_validate_bun_path_incompatible_version(mocker): + """Test that an error is thrown when the bun version does not meet minimum requirements. + + Args: + mocker: Pytest mocker object. + """ + mock = mocker.Mock() + mocker.patch.object(mock, "bun_path", return_value="/mock/path") + mocker.patch("reflex.utils.prerequisites.get_config", mock) + mocker.patch( + "reflex.utils.prerequisites.get_bun_version", + return_value=version.parse("0.6.5"), ) - prerequisites.initialize_bun() - if not is_valid: - remove_existing_bun_installation.assert_called_once() - bun_install.assert_called_once() + with pytest.raises(typer.Exit): + prerequisites.validate_bun() def test_remove_existing_bun_installation(mocker): @@ -521,7 +523,7 @@ def test_node_install_windows(mocker): mocker.patch("reflex.utils.prerequisites.check_node_version", return_value=False) with pytest.raises(typer.Exit): - prerequisites.initialize_node() + prerequisites.install_node() def test_node_install_unix(tmp_path, mocker):