diff --git a/reflex/constants.py b/reflex/constants.py index 0914aa89a..a7686ca66 100644 --- a/reflex/constants.py +++ b/reflex/constants.py @@ -45,42 +45,9 @@ MODULE_NAME = "reflex" # The current version of Reflex. VERSION = metadata.version(MODULE_NAME) -# Project dependencies. -# The directory to store reflex dependencies. -REFLEX_DIR = os.path.expandvars("$HOME/.reflex") - -# Bun config. -# The Bun version. -BUN_VERSION = "0.7.0" -# The directory to store the bun. -BUN_ROOT_PATH = f"{REFLEX_DIR}/.bun" -# The bun path. -BUN_PATH = f"{BUN_ROOT_PATH}/bin/bun" -# Command to install bun. -INSTALL_BUN = f"curl -fsSL https://bun.sh/install | env BUN_INSTALL={BUN_ROOT_PATH} bash -s -- bun-v{BUN_VERSION}" - -# NVM / Node config. -# The Node version. -NODE_VERSION = "18.17.0" -# The minimum required node version. -MIN_NODE_VERSION = "16.8.0" -# The directory to store nvm. -NVM_ROOT_PATH = f"{REFLEX_DIR}/.nvm" -# The nvm path. -NVM_PATH = f"{NVM_ROOT_PATH}/nvm.sh" -# The node bin path. -NODE_BIN_PATH = f"{NVM_ROOT_PATH}/versions/node/v{NODE_VERSION}/bin" -# The default path where node is installed. -NODE_PATH = "node" if platform.system() == "Windows" else f"{NODE_BIN_PATH}/node" -# The default path where npm is installed. -NPM_PATH = "npm" if platform.system() == "Windows" else f"{NODE_BIN_PATH}/npm" -# Command to install nvm. -INSTALL_NVM = f"curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | env NVM_DIR={NVM_ROOT_PATH} bash" -# Command to install node. -INSTALL_NODE = f'bash -c "export NVM_DIR={NVM_ROOT_PATH} && . {NVM_ROOT_PATH}/nvm.sh && nvm install {NODE_VERSION}"' - - # Files and directories used to init a new project. +# The directory to store reflex dependencies. +REFLEX_DIR = os.path.expandvars(os.path.join("$HOME", f".{MODULE_NAME}")) # The root directory of the reflex library. ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # The name of the assets directory. @@ -94,6 +61,42 @@ ASSETS_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, APP_ASSETS_DIR) # The jinja template directory. JINJA_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, "jinja") +# Bun config. +# The Bun version. +BUN_VERSION = "0.7.0" +# The directory to store the bun. +BUN_ROOT_PATH = os.path.join(REFLEX_DIR, ".bun") +# The bun path. +BUN_PATH = os.path.join(BUN_ROOT_PATH, "bin", "bun") +# URL to bun install script. +BUN_INSTALL_URL = "https://bun.sh/install" + +# NVM / Node config. +# The NVM version. +NVM_VERSION = "0.39.1" +# The Node version. +NODE_VERSION = "18.17.0" +# The minimum required node version. +NODE_VERSION_MIN = "16.8.0" +# The directory to store nvm. +NVM_DIR = os.path.join(REFLEX_DIR, ".nvm") +# The nvm path. +NVM_PATH = os.path.join(NVM_DIR, "nvm.sh") +# The node bin path. +NODE_BIN_PATH = os.path.join(NVM_DIR, "versions", "node", f"v{NODE_VERSION}", "bin") +# The default path where node is installed. +NODE_PATH = ( + "node" if platform.system() == "Windows" else os.path.join(NODE_BIN_PATH, "node") +) +# The default path where npm is installed. +NPM_PATH = ( + "npm" if platform.system() == "Windows" else os.path.join(NODE_BIN_PATH, "npm") +) +# The URL to the nvm install script. +NVM_INSTALL_URL = ( + f"https://raw.githubusercontent.com/nvm-sh/nvm/v{NVM_VERSION}/install.sh" +) + # The frontend directories in a project. # The web folder where the NextJS app is compiled to. WEB_DIR = ".web" diff --git a/reflex/utils/build.py b/reflex/utils/build.py index 744b45d63..8a167c246 100644 --- a/reflex/utils/build.py +++ b/reflex/utils/build.py @@ -147,7 +147,7 @@ def export_app( # Check for special strings and update the progress bar. for special_string in checkpoints: if special_string in line: - if special_string == "Export successful": + if special_string == checkpoints[-1]: progress.update(task, completed=len(checkpoints)) break # Exit the loop if the completion message is found else: diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index a69c47449..1a436148d 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -9,12 +9,15 @@ import platform import re import subprocess import sys +import tempfile +import threading from datetime import datetime from fileinput import FileInput from pathlib import Path from types import ModuleType from typing import Optional +import httpx import typer from alembic.util.exc import CommandError from packaging import version @@ -44,7 +47,7 @@ def check_node_version(): current_version = version.parse(result.stdout.decode()) # Compare the version numbers return ( - current_version >= version.parse(constants.MIN_NODE_VERSION) + current_version >= version.parse(constants.NODE_VERSION_MIN) if IS_WINDOWS else current_version == version.parse(constants.NODE_VERSION) ) @@ -273,39 +276,68 @@ def initialize_node(): install_node() +def download_and_run(url: str, *args, **env): + """Download and run a script. + + + Args: + url: The url of the script. + args: The arguments to pass to the script. + env: The environment variables to use. + + + Raises: + Exit: if installation failed + """ + # Download the script + response = httpx.get(url) + if response.status_code != httpx.codes.OK: + response.raise_for_status() + + # Save the script to a temporary file. + script = tempfile.NamedTemporaryFile() + with open(script.name, "w") as f: + f.write(response.text) + + # Run the script. + env = { + **os.environ, + **env, + } + result = subprocess.run(["bash", f.name, *args], env=env) + if result.returncode != 0: + raise typer.Exit(code=result.returncode) + + def install_node(): """Install nvm and nodejs for use by Reflex. Independent of any existing system installations. Raises: - FileNotFoundError: if unzip or curl packages are not found. Exit: if installation failed """ + # NVM is not supported on Windows. if IS_WINDOWS: console.print( f"[red]Node.js version {constants.NODE_VERSION} or higher is required to run Reflex." ) raise typer.Exit() - # Only install if bun is not already installed. - console.log("Installing nvm...") - - # Check if curl is installed - # TODO no need to shell out to curl - curl_path = path_ops.which("curl") - if curl_path is None: - raise FileNotFoundError("Reflex requires curl to be installed.") - # Create the nvm directory and install. - path_ops.mkdir(constants.NVM_ROOT_PATH) - result = subprocess.run(constants.INSTALL_NVM, shell=True) - - if result.returncode != 0: - raise typer.Exit(code=result.returncode) - - console.log("Installing node...") - result = subprocess.run(constants.INSTALL_NODE, shell=True) + path_ops.mkdir(constants.NVM_DIR) + env = {**os.environ, "NVM_DIR": constants.NVM_DIR} + download_and_run(constants.NVM_INSTALL_URL, **env) + # Install node. + # We use bash -c as we need to source nvm.sh to use nvm. + result = subprocess.run( + [ + "bash", + "-c", + f". {constants.NVM_DIR}/nvm.sh && nvm install {constants.NODE_VERSION}", + ], + env=env, + ) if result.returncode != 0: raise typer.Exit(code=result.returncode) @@ -314,32 +346,27 @@ def install_bun(): """Install bun onto the user's system. Raises: - FileNotFoundError: if unzip or curl packages are not found. - Exit: if installation failed + FileNotFoundError: If required packages are not found. """ # Bun is not supported on Windows. if IS_WINDOWS: - console.log("Skipping bun installation on Windows.") return - # Only install if bun is not already installed. - if not os.path.exists(constants.BUN_PATH): - console.log("Installing bun...") + # Skip if bun is already installed. + if os.path.exists(constants.BUN_PATH): + return - # Check if curl is installed - curl_path = path_ops.which("curl") - if curl_path is None: - raise FileNotFoundError("Reflex requires curl to be installed.") + # Check if unzip is installed + unzip_path = path_ops.which("unzip") + if unzip_path is None: + raise FileNotFoundError("Reflex requires unzip to be installed.") - # Check if unzip is installed - unzip_path = path_ops.which("unzip") - if unzip_path is None: - raise FileNotFoundError("Reflex requires unzip to be installed.") - - result = subprocess.run(constants.INSTALL_BUN, shell=True) - - if result.returncode != 0: - raise typer.Exit(code=result.returncode) + # 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 install_frontend_packages(): @@ -417,12 +444,20 @@ def initialize_frontend_dependencies(): path_ops.mkdir(constants.REFLEX_DIR) # Install the frontend dependencies. - initialize_bun() - initialize_node() + threads = [ + threading.Thread(target=initialize_bun), + threading.Thread(target=initialize_node), + ] + for thread in threads: + thread.start() # Set up the web directory. initialize_web_directory() + # Wait for the threads to finish. + for thread in threads: + thread.join() + def check_admin_settings(): """Check if admin settings are set and valid for logging in cli app.""" diff --git a/reflex/utils/processes.py b/reflex/utils/processes.py index d462adc7a..75fe5afa5 100644 --- a/reflex/utils/processes.py +++ b/reflex/utils/processes.py @@ -134,13 +134,15 @@ def new_process(args, **kwargs): Returns: Execute a child program in a new process. """ - env = os.environ.copy() - env["PATH"] = os.pathsep.join([constants.NODE_BIN_PATH, env["PATH"]]) + env = { + **os.environ, + "PATH": os.pathsep.join([constants.NODE_BIN_PATH, os.environ["PATH"]]), + } kwargs = { "env": env, "stderr": subprocess.STDOUT, - "stdout": subprocess.PIPE, # Redirect stdout to a pipe - "universal_newlines": True, # Set universal_newlines to True for text mode + "stdout": subprocess.PIPE, + "universal_newlines": True, "encoding": "UTF-8", **kwargs, } diff --git a/tests/test_utils.py b/tests/test_utils.py index e2cd9ba8e..aa54eaa35 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -9,6 +9,7 @@ import typer from packaging import version from reflex import Env, constants +from reflex.base import Base from reflex.utils import build, format, imports, prerequisites, types from reflex.vars import Var @@ -527,13 +528,20 @@ def test_node_install_windows(mocker): def test_node_install_unix(tmp_path, mocker): nvm_root_path = tmp_path / ".reflex" / ".nvm" - mocker.patch("reflex.utils.prerequisites.constants.NVM_ROOT_PATH", nvm_root_path) + mocker.patch("reflex.utils.prerequisites.constants.NVM_DIR", nvm_root_path) subprocess_run = mocker.patch( "reflex.utils.prerequisites.subprocess.run", return_value=subprocess.CompletedProcess(args="", returncode=0), ) mocker.patch("reflex.utils.prerequisites.IS_WINDOWS", False) + class Resp(Base): + status_code = 200 + text = "test" + + mocker.patch("httpx.get", return_value=Resp()) + mocker.patch("reflex.utils.prerequisites.download_and_run") + prerequisites.install_node() assert nvm_root_path.exists() @@ -541,14 +549,15 @@ def test_node_install_unix(tmp_path, mocker): subprocess_run.call_count = 2 -def test_node_install_without_curl(mocker): - """Test that an error is thrown when installing node with curl not installed. +def test_bun_install_without_unzip(mocker): + """Test that an error is thrown when installing bun with unzip not installed. Args: mocker: Pytest mocker object. """ - mocker.patch("reflex.utils.prerequisites.path_ops.which", return_value=None) + mocker.patch("reflex.utils.path_ops.which", return_value=None) + mocker.patch("os.path.exists", return_value=False) mocker.patch("reflex.utils.prerequisites.IS_WINDOWS", False) with pytest.raises(FileNotFoundError): - prerequisites.install_node() + prerequisites.install_bun()