Remove curl and parallelize node/bun install (#1458)

This commit is contained in:
Nikhil Rao 2023-07-28 17:53:24 -07:00 committed by GitHub
parent b67bba590d
commit e1cb09e9d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 134 additions and 85 deletions

View File

@ -45,42 +45,9 @@ MODULE_NAME = "reflex"
# The current version of Reflex. # The current version of Reflex.
VERSION = metadata.version(MODULE_NAME) 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. # 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. # The root directory of the reflex library.
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# The name of the assets directory. # 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. # The jinja template directory.
JINJA_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, "jinja") 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 frontend directories in a project.
# The web folder where the NextJS app is compiled to. # The web folder where the NextJS app is compiled to.
WEB_DIR = ".web" WEB_DIR = ".web"

View File

@ -147,7 +147,7 @@ def export_app(
# Check for special strings and update the progress bar. # Check for special strings and update the progress bar.
for special_string in checkpoints: for special_string in checkpoints:
if special_string in line: if special_string in line:
if special_string == "Export successful": if special_string == checkpoints[-1]:
progress.update(task, completed=len(checkpoints)) progress.update(task, completed=len(checkpoints))
break # Exit the loop if the completion message is found break # Exit the loop if the completion message is found
else: else:

View File

@ -9,12 +9,15 @@ import platform
import re import re
import subprocess import subprocess
import sys import sys
import tempfile
import threading
from datetime import datetime from datetime import datetime
from fileinput import FileInput from fileinput import FileInput
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Optional from typing import Optional
import httpx
import typer import typer
from alembic.util.exc import CommandError from alembic.util.exc import CommandError
from packaging import version from packaging import version
@ -44,7 +47,7 @@ def check_node_version():
current_version = version.parse(result.stdout.decode()) current_version = version.parse(result.stdout.decode())
# Compare the version numbers # Compare the version numbers
return ( return (
current_version >= version.parse(constants.MIN_NODE_VERSION) current_version >= version.parse(constants.NODE_VERSION_MIN)
if IS_WINDOWS if IS_WINDOWS
else current_version == version.parse(constants.NODE_VERSION) else current_version == version.parse(constants.NODE_VERSION)
) )
@ -273,39 +276,68 @@ def initialize_node():
install_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(): def install_node():
"""Install nvm and nodejs for use by Reflex. """Install nvm and nodejs for use by Reflex.
Independent of any existing system installations. Independent of any existing system installations.
Raises: Raises:
FileNotFoundError: if unzip or curl packages are not found.
Exit: if installation failed Exit: if installation failed
""" """
# NVM is not supported on Windows.
if IS_WINDOWS: if IS_WINDOWS:
console.print( console.print(
f"[red]Node.js version {constants.NODE_VERSION} or higher is required to run Reflex." f"[red]Node.js version {constants.NODE_VERSION} or higher is required to run Reflex."
) )
raise typer.Exit() 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. # Create the nvm directory and install.
path_ops.mkdir(constants.NVM_ROOT_PATH) path_ops.mkdir(constants.NVM_DIR)
result = subprocess.run(constants.INSTALL_NVM, shell=True) env = {**os.environ, "NVM_DIR": constants.NVM_DIR}
download_and_run(constants.NVM_INSTALL_URL, **env)
if result.returncode != 0:
raise typer.Exit(code=result.returncode)
console.log("Installing node...")
result = subprocess.run(constants.INSTALL_NODE, shell=True)
# 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: if result.returncode != 0:
raise typer.Exit(code=result.returncode) raise typer.Exit(code=result.returncode)
@ -314,32 +346,27 @@ def install_bun():
"""Install bun onto the user's system. """Install bun onto the user's system.
Raises: Raises:
FileNotFoundError: if unzip or curl packages are not found. FileNotFoundError: If required packages are not found.
Exit: if installation failed
""" """
# Bun is not supported on Windows. # Bun is not supported on Windows.
if IS_WINDOWS: if IS_WINDOWS:
console.log("Skipping bun installation on Windows.")
return return
# Only install if bun is not already installed. # Skip if bun is already installed.
if not os.path.exists(constants.BUN_PATH): if os.path.exists(constants.BUN_PATH):
console.log("Installing bun...") return
# Check if curl is installed # Check if unzip is installed
curl_path = path_ops.which("curl") unzip_path = path_ops.which("unzip")
if curl_path is None: if unzip_path is None:
raise FileNotFoundError("Reflex requires curl to be installed.") raise FileNotFoundError("Reflex requires unzip to be installed.")
# Check if unzip is installed # Run the bun install script.
unzip_path = path_ops.which("unzip") download_and_run(
if unzip_path is None: constants.BUN_INSTALL_URL,
raise FileNotFoundError("Reflex requires unzip to be installed.") f"bun-v{constants.BUN_VERSION}",
BUN_INSTALL=constants.BUN_ROOT_PATH,
result = subprocess.run(constants.INSTALL_BUN, shell=True) )
if result.returncode != 0:
raise typer.Exit(code=result.returncode)
def install_frontend_packages(): def install_frontend_packages():
@ -417,12 +444,20 @@ def initialize_frontend_dependencies():
path_ops.mkdir(constants.REFLEX_DIR) path_ops.mkdir(constants.REFLEX_DIR)
# Install the frontend dependencies. # Install the frontend dependencies.
initialize_bun() threads = [
initialize_node() threading.Thread(target=initialize_bun),
threading.Thread(target=initialize_node),
]
for thread in threads:
thread.start()
# Set up the web directory. # Set up the web directory.
initialize_web_directory() initialize_web_directory()
# Wait for the threads to finish.
for thread in threads:
thread.join()
def check_admin_settings(): def check_admin_settings():
"""Check if admin settings are set and valid for logging in cli app.""" """Check if admin settings are set and valid for logging in cli app."""

View File

@ -134,13 +134,15 @@ def new_process(args, **kwargs):
Returns: Returns:
Execute a child program in a new process. Execute a child program in a new process.
""" """
env = os.environ.copy() env = {
env["PATH"] = os.pathsep.join([constants.NODE_BIN_PATH, env["PATH"]]) **os.environ,
"PATH": os.pathsep.join([constants.NODE_BIN_PATH, os.environ["PATH"]]),
}
kwargs = { kwargs = {
"env": env, "env": env,
"stderr": subprocess.STDOUT, "stderr": subprocess.STDOUT,
"stdout": subprocess.PIPE, # Redirect stdout to a pipe "stdout": subprocess.PIPE,
"universal_newlines": True, # Set universal_newlines to True for text mode "universal_newlines": True,
"encoding": "UTF-8", "encoding": "UTF-8",
**kwargs, **kwargs,
} }

View File

@ -9,6 +9,7 @@ import typer
from packaging import version from packaging import version
from reflex import Env, constants from reflex import Env, constants
from reflex.base import Base
from reflex.utils import build, format, imports, prerequisites, types from reflex.utils import build, format, imports, prerequisites, types
from reflex.vars import Var from reflex.vars import Var
@ -527,13 +528,20 @@ def test_node_install_windows(mocker):
def test_node_install_unix(tmp_path, mocker): def test_node_install_unix(tmp_path, mocker):
nvm_root_path = tmp_path / ".reflex" / ".nvm" 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( subprocess_run = mocker.patch(
"reflex.utils.prerequisites.subprocess.run", "reflex.utils.prerequisites.subprocess.run",
return_value=subprocess.CompletedProcess(args="", returncode=0), return_value=subprocess.CompletedProcess(args="", returncode=0),
) )
mocker.patch("reflex.utils.prerequisites.IS_WINDOWS", False) 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() prerequisites.install_node()
assert nvm_root_path.exists() assert nvm_root_path.exists()
@ -541,14 +549,15 @@ def test_node_install_unix(tmp_path, mocker):
subprocess_run.call_count = 2 subprocess_run.call_count = 2
def test_node_install_without_curl(mocker): def test_bun_install_without_unzip(mocker):
"""Test that an error is thrown when installing node with curl not installed. """Test that an error is thrown when installing bun with unzip not installed.
Args: Args:
mocker: Pytest mocker object. 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) mocker.patch("reflex.utils.prerequisites.IS_WINDOWS", False)
with pytest.raises(FileNotFoundError): with pytest.raises(FileNotFoundError):
prerequisites.install_node() prerequisites.install_bun()