Revert "Bun as runtime on Mac and Linux (#2138)" (#2153)

This commit is contained in:
Elijah Ahianyo 2023-11-09 21:01:48 +00:00 committed by GitHub
parent 323f7ce5ba
commit 7a04652a6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 194 additions and 170 deletions

View File

@ -2,8 +2,6 @@
from .base import ( from .base import (
COOKIES, COOKIES,
IS_LINUX,
IS_LINUX_OR_MAC,
IS_WINDOWS, IS_WINDOWS,
LOCAL_STORAGE, LOCAL_STORAGE,
POLLING_MAX_HTTP_BUFFER_SIZE, POLLING_MAX_HTTP_BUFFER_SIZE,
@ -71,8 +69,6 @@ __ALL__ = [
Fnm, Fnm,
GitIgnore, GitIgnore,
RequirementsTxt, RequirementsTxt,
IS_LINUX_OR_MAC,
IS_LINUX,
IS_WINDOWS, IS_WINDOWS,
LOCAL_STORAGE, LOCAL_STORAGE,
LogLevel, LogLevel,

View File

@ -11,8 +11,6 @@ from types import SimpleNamespace
from platformdirs import PlatformDirs from platformdirs import PlatformDirs
IS_WINDOWS = platform.system() == "Windows" IS_WINDOWS = platform.system() == "Windows"
IS_LINUX_OR_MAC = platform.system() in ["Linux", "Darwin"]
IS_LINUX = platform.system() == "Linux"
class Dirs(SimpleNamespace): class Dirs(SimpleNamespace):

View File

@ -116,7 +116,7 @@ def run_frontend(root: Path, port: str):
# Start watching asset folder. # Start watching asset folder.
start_watching_assets_folder(root) start_watching_assets_folder(root)
# validate dependencies before run # validate dependencies before run
prerequisites.validate_frontend_dependencies() prerequisites.validate_frontend_dependencies(init=False)
# Run the frontend in development mode. # Run the frontend in development mode.
console.rule("[bold green]App Running") console.rule("[bold green]App Running")
@ -134,7 +134,7 @@ def run_frontend_prod(root: Path, port: str):
# Set the port. # Set the port.
os.environ["PORT"] = str(get_config().frontend_port if port is None else port) os.environ["PORT"] = str(get_config().frontend_port if port is None else port)
# validate dependencies before run # validate dependencies before run
prerequisites.validate_frontend_dependencies() prerequisites.validate_frontend_dependencies(init=False)
# Run the frontend in production mode. # Run the frontend in production mode.
console.rule("[bold green]App Running") console.rule("[bold green]App Running")
run_process_and_launch_url([prerequisites.get_package_manager(), "run", "prod"]) # type: ignore run_process_and_launch_url([prerequisites.get_package_manager(), "run", "prod"]) # type: ignore
@ -239,20 +239,21 @@ def output_system_info():
dependencies = [ dependencies = [
f"[Reflex {constants.Reflex.VERSION} with Python {platform.python_version()} (PATH: {sys.executable})]", f"[Reflex {constants.Reflex.VERSION} with Python {platform.python_version()} (PATH: {sys.executable})]",
f"[Node {prerequisites.get_node_version()} (Expected: {constants.Node.VERSION}) (PATH:{path_ops.get_node_path()})]",
] ]
system = platform.system() system = platform.system()
if system == "Windows" or constants.IS_LINUX and not prerequisites.is_valid_linux():
if system != "Windows":
dependencies.extend( dependencies.extend(
[ [
f"[Node {prerequisites.get_node_version()} (Expected: {constants.Node.VERSION}) (PATH:{path_ops.get_node_path()})]",
f"[FNM {prerequisites.get_fnm_version()} (Expected: {constants.Fnm.VERSION}) (PATH: {constants.Fnm.EXE})]", f"[FNM {prerequisites.get_fnm_version()} (Expected: {constants.Fnm.VERSION}) (PATH: {constants.Fnm.EXE})]",
] f"[Bun {prerequisites.get_bun_version()} (Expected: {constants.Bun.VERSION}) (PATH: {config.bun_path})]",
],
) )
else:
if system != "Windows" or constants.IS_LINUX and not prerequisites.is_valid_linux():
dependencies.append( dependencies.append(
f"[Bun {prerequisites.get_bun_version()} (Expected: {constants.Bun.VERSION}) (PATH: {config.bun_path})]", f"[FNM {prerequisites.get_fnm_version()} (Expected: {constants.Fnm.VERSION}) (PATH: {constants.Fnm.EXE})]",
) )
if system == "Linux": if system == "Linux":

View File

@ -29,6 +29,23 @@ from reflex.config import Config, get_config
from reflex.utils import console, path_ops, processes from reflex.utils import console, path_ops, processes
def check_node_version() -> bool:
"""Check the version of Node.js.
Returns:
Whether the version of Node.js is valid.
"""
current_version = get_node_version()
if current_version:
# Compare the version numbers
return (
current_version >= version.parse(constants.Node.MIN_VERSION)
if constants.IS_WINDOWS
else current_version == version.parse(constants.Node.VERSION)
)
return False
def get_node_version() -> version.Version | None: def get_node_version() -> version.Version | None:
"""Get the version of node. """Get the version of node.
@ -70,35 +87,24 @@ def get_bun_version() -> version.Version | None:
return None return None
def get_package_manager() -> str | None: def get_install_package_manager() -> str | None:
"""Get the package manager executable for installation. """Get the package manager executable for installation.
Currently on unix systems, bun is used for installation only. Currently on unix systems, bun is used for installation only.
Returns: Returns:
The path to the package manager. The path to the package manager.
""" """
# On Windows or lower linux kernels(WSL1), we use npm instead of bun. # On Windows, we use npm instead of bun.
if constants.IS_WINDOWS or constants.IS_LINUX and not is_valid_linux(): if constants.IS_WINDOWS:
return get_npm_package_manager() return get_package_manager()
# On other platforms, we use bun. # On other platforms, we use bun.
return get_config().bun_path return get_config().bun_path
def get_install_package_manager() -> str | None: def get_package_manager() -> str | None:
"""Get package manager to install dependencies. """Get the package manager executable for running app.
Currently on unix systems, npm is used for running the app only.
Returns:
Path to install package manager.
"""
if constants.IS_WINDOWS:
return get_npm_package_manager()
return get_config().bun_path
def get_npm_package_manager() -> str | None:
"""Get the npm package manager executable for installing and running app
on windows.
Returns: Returns:
The path to the package manager. The path to the package manager.
@ -354,6 +360,13 @@ def update_next_config(next_config: str, config: Config) -> str:
return next_config return next_config
def remove_existing_bun_installation():
"""Remove existing bun installation."""
console.debug("Removing existing bun installation.")
if os.path.exists(get_config().bun_path):
path_ops.rm(constants.Bun.ROOT_PATH)
def download_and_run(url: str, *args, show_status: bool = False, **env): def download_and_run(url: str, *args, show_status: bool = False, **env):
"""Download and run a script. """Download and run a script.
@ -415,38 +428,52 @@ def download_and_extract_fnm_zip():
def install_node(): def install_node():
"""Install fnm and nodejs for use by Reflex.""" """Install fnm and nodejs for use by Reflex.
if constants.IS_WINDOWS or constants.IS_LINUX and not is_valid_linux(): Independent of any existing system installations.
"""
if not constants.Fnm.FILENAME:
# fnm only support Linux, macOS and Windows distros.
console.debug("")
return
path_ops.mkdir(constants.Fnm.DIR) path_ops.mkdir(constants.Fnm.DIR)
if not os.path.exists(constants.Fnm.EXE): if not os.path.exists(constants.Fnm.EXE):
download_and_extract_fnm_zip() download_and_extract_fnm_zip()
if constants.IS_WINDOWS: if constants.IS_WINDOWS:
# Install node # Install node
fnm_exe = Path(constants.Fnm.EXE).resolve() fnm_exe = Path(constants.Fnm.EXE).resolve()
fnm_dir = Path(constants.Fnm.DIR).resolve() fnm_dir = Path(constants.Fnm.DIR).resolve()
process = processes.new_process( process = processes.new_process(
[ [
"powershell", "powershell",
"-Command", "-Command",
f'& "{fnm_exe}" install {constants.Node.VERSION} --fnm-dir "{fnm_dir}"', f'& "{fnm_exe}" install {constants.Node.VERSION} --fnm-dir "{fnm_dir}"',
], ],
) )
else: # All other platforms (Linux, WSL1). else: # All other platforms (Linux, MacOS).
# Add execute permissions to fnm executable. # TODO we can skip installation if check_node_version() checks out
os.chmod(constants.Fnm.EXE, stat.S_IXUSR) # Add execute permissions to fnm executable.
# Install node. os.chmod(constants.Fnm.EXE, stat.S_IXUSR)
process = processes.new_process( # Install node.
[ # Specify arm64 arch explicitly for M1s and M2s.
constants.Fnm.EXE, architecture_arg = (
"install", ["--arch=arm64"]
constants.Node.VERSION, if platform.system() == "Darwin" and platform.machine() == "arm64"
"--fnm-dir", else []
constants.Fnm.DIR, )
],
) process = processes.new_process(
processes.show_status("Installing node", process) [
constants.Fnm.EXE,
"install",
*architecture_arg,
constants.Node.VERSION,
"--fnm-dir",
constants.Fnm.DIR,
],
)
processes.show_status("Installing node", process)
def install_bun(): def install_bun():
@ -597,88 +624,50 @@ def validate_bun():
raise typer.Exit(1) raise typer.Exit(1)
def validate_node(): def validate_frontend_dependencies(init=True):
"""Check the version of Node.js is correct. """Validate frontend dependencies to ensure they meet requirements.
Raises:
Exit: If the version of Node.js is incorrect.
"""
current_version = get_node_version()
# Check if Node is installed.
if not current_version:
console.error(
"Failed to obtain node version. Make sure node is installed and in your PATH."
)
raise typer.Exit(1)
# Check if the version of Node is correct.
if current_version < version.parse(constants.Node.MIN_VERSION):
console.error(
f"Reflex requires node version {constants.Node.MIN_VERSION} or higher to run, but the detected version is {current_version}."
)
raise typer.Exit(1)
def remove_existing_fnm_dir():
"""Remove existing fnm directory on linux and mac."""
if os.path.exists(constants.Fnm.DIR):
console.debug("Removing existing fnm installation.")
path_ops.rm(constants.Fnm.DIR)
def validate_frontend_dependencies():
"""Validate frontend dependencies to ensure they meet requirements."""
# Bun only supports linux and Mac. For Non-linux-or-mac, we use node.
validate_bun() if constants.IS_LINUX_OR_MAC else validate_node()
def parse_non_semver_version(version_string: str) -> version.Version | None:
"""Parse unsemantic version string and produce
a clean version that confirms to packaging.version.
Args: Args:
version_string: The version string init: whether running `reflex init`
Returns:
A cleaned semantic packaging.version object.
Raises:
Exit: If the package manager is invalid.
""" """
# Remove non-numeric characters from the version string if not init:
cleaned_version_string = re.sub(r"[^\d.]+", "", version_string) # we only need to validate the package manager when running app.
try: # `reflex init` will install the deps anyway(if applied).
parsed_version = version.parse(cleaned_version_string) package_manager = get_package_manager()
return parsed_version if not package_manager:
except version.InvalidVersion: console.error(
console.debug(f"could not parse version: {version_string}") "Could not find NPM package manager. Make sure you have node installed."
return None )
raise typer.Exit(1)
if not check_node_version():
node_version = get_node_version()
console.error(
f"Reflex requires node version {constants.Node.MIN_VERSION} or higher to run, but the detected version is {node_version}",
)
raise typer.Exit(1)
def is_valid_linux() -> bool: if constants.IS_WINDOWS:
"""Check if the linux kernel version is valid enough to use bun. return
This is typically used run npm at runtime for WSL 1 or lower linux versions.
Returns: if init:
If linux kernel version is valid enough. # we only need bun for package install on `reflex init`.
""" validate_bun()
if not constants.IS_LINUX:
return False
kernel_string = platform.release()
kv = parse_non_semver_version(kernel_string)
return kv.major > 5 or (kv.major == 5 and kv.minor >= 10) if kv else False
def initialize_frontend_dependencies(): def initialize_frontend_dependencies():
"""Initialize all the frontend dependencies.""" """Initialize all the frontend dependencies."""
# Create the reflex directory. # Create the reflex directory.
path_ops.mkdir(constants.Reflex.DIR) path_ops.mkdir(constants.Reflex.DIR)
# validate dependencies before install
validate_frontend_dependencies()
# Install the frontend dependencies. # Install the frontend dependencies.
processes.run_concurrently(install_node, install_bun) processes.run_concurrently(install_node, install_bun)
# Set up the web directory. # Set up the web directory.
initialize_web_directory() initialize_web_directory()
# remove existing fnm dir on linux and mac
if constants.IS_LINUX_OR_MAC and is_valid_linux():
remove_existing_fnm_dir()
def check_db_initialized() -> bool: def check_db_initialized() -> bool:

View File

@ -13,7 +13,6 @@ from typing import Callable, Generator, List, Optional, Tuple, Union
import psutil import psutil
import typer import typer
from reflex import constants
from reflex.utils import console, path_ops, prerequisites from reflex.utils import console, path_ops, prerequisites
@ -126,18 +125,16 @@ def new_process(args, run: bool = False, show_logs: bool = False, **kwargs):
Returns: Returns:
Execute a child program in a new process. Execute a child program in a new process.
""" """
node_bin_path = "" node_bin_path = path_ops.get_node_bin_path()
if not constants.IS_LINUX_OR_MAC or not prerequisites.is_valid_linux(): if not node_bin_path:
node_bin_path = path_ops.get_node_bin_path() or "" console.warn(
if not node_bin_path: "The path to the Node binary could not be found. Please ensure that Node is properly "
console.warn( "installed and added to your system's PATH environment variable."
"The path to the Node binary could not be found. Please ensure that Node is properly " )
"installed and added to your system's PATH environment variable."
)
# Add the node bin path to the PATH environment variable. # Add the node bin path to the PATH environment variable.
env = { env = {
**os.environ, **os.environ,
"PATH": os.pathsep.join([node_bin_path, os.environ["PATH"]]), # type: ignore "PATH": os.pathsep.join([node_bin_path if node_bin_path else "", os.environ["PATH"]]), # type: ignore
**kwargs.pop("env", {}), **kwargs.pop("env", {}),
} }
kwargs = { kwargs = {

View File

@ -4,11 +4,7 @@ import pytest
from reflex import constants from reflex import constants
from reflex.config import Config from reflex.config import Config
from reflex.utils.prerequisites import ( from reflex.utils.prerequisites import initialize_requirements_txt, update_next_config
initialize_requirements_txt,
install_node,
update_next_config,
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -146,29 +142,3 @@ def test_initialize_requirements_txt_not_exist(mocker):
open_mock().write.call_args[0][0] open_mock().write.call_args[0][0]
== f"\n{constants.RequirementsTxt.DEFAULTS_STUB}{constants.Reflex.VERSION}\n" == f"\n{constants.RequirementsTxt.DEFAULTS_STUB}{constants.Reflex.VERSION}\n"
) )
@pytest.mark.parametrize(
"is_windows, is_linux, release, expected",
[
(True, False, "10.0.20348", True),
(False, True, "6.2.0-1015-azure", False),
(False, True, "4.4.0-17763-Microsoft", True),
(False, False, "21.6.0", False),
],
)
def test_install_node(is_windows, is_linux, release, expected, mocker):
mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", is_windows)
mocker.patch("reflex.utils.prerequisites.constants.IS_LINUX", is_linux)
mocker.patch("reflex.utils.prerequisites.platform.release", return_value=release)
mocker.patch("reflex.utils.prerequisites.download_and_extract_fnm_zip")
mocker.patch("reflex.utils.prerequisites.processes.new_process")
mocker.patch("reflex.utils.prerequisites.processes.show_status")
mocker.patch("reflex.utils.prerequisites.os.chmod")
path_ops = mocker.patch("reflex.utils.prerequisites.path_ops.mkdir")
install_node()
if expected:
path_ops.assert_called_once()
else:
path_ops.assert_not_called()

View File

@ -121,6 +121,19 @@ def test_validate_bun_path_incompatible_version(mocker):
prerequisites.validate_bun() prerequisites.validate_bun()
def test_remove_existing_bun_installation(mocker):
"""Test that existing bun installation is removed.
Args:
mocker: Pytest mocker.
"""
mocker.patch("reflex.utils.prerequisites.os.path.exists", return_value=True)
rm = mocker.patch("reflex.utils.prerequisites.path_ops.rm", mocker.Mock())
prerequisites.remove_existing_bun_installation()
rm.assert_called_once()
def test_setup_frontend(tmp_path, mocker): def test_setup_frontend(tmp_path, mocker):
"""Test checking if assets content have been """Test checking if assets content have been
copied into the .web/public folder. copied into the .web/public folder.
@ -351,6 +364,65 @@ def test_node_install_windows(tmp_path, mocker):
download.assert_called_once() download.assert_called_once()
@pytest.mark.parametrize(
"machine, system",
[
("x64", "Darwin"),
("arm64", "Darwin"),
("x64", "Windows"),
("arm64", "Windows"),
("armv7", "Linux"),
("armv8-a", "Linux"),
("armv8.1-a", "Linux"),
("armv8.2-a", "Linux"),
("armv8.3-a", "Linux"),
("armv8.4-a", "Linux"),
("aarch64", "Linux"),
("aarch32", "Linux"),
],
)
def test_node_install_unix(tmp_path, mocker, machine, system):
fnm_root_path = tmp_path / "reflex" / "fnm"
fnm_exe = fnm_root_path / "fnm"
mocker.patch("reflex.utils.prerequisites.constants.Fnm.DIR", fnm_root_path)
mocker.patch("reflex.utils.prerequisites.constants.Fnm.EXE", fnm_exe)
mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", False)
mocker.patch("reflex.utils.prerequisites.platform.machine", return_value=machine)
mocker.patch("reflex.utils.prerequisites.platform.system", return_value=system)
class Resp(Base):
status_code = 200
text = "test"
mocker.patch("httpx.stream", return_value=Resp())
download = mocker.patch("reflex.utils.prerequisites.download_and_extract_fnm_zip")
process = mocker.patch("reflex.utils.processes.new_process")
chmod = mocker.patch("reflex.utils.prerequisites.os.chmod")
mocker.patch("reflex.utils.processes.stream_logs")
prerequisites.install_node()
assert fnm_root_path.exists()
download.assert_called_once()
if system == "Darwin" and machine == "arm64":
process.assert_called_with(
[
fnm_exe,
"install",
"--arch=arm64",
constants.Node.VERSION,
"--fnm-dir",
fnm_root_path,
]
)
else:
process.assert_called_with(
[fnm_exe, "install", constants.Node.VERSION, "--fnm-dir", fnm_root_path]
)
chmod.assert_called_once()
def test_bun_install_without_unzip(mocker): def test_bun_install_without_unzip(mocker):
"""Test that an error is thrown when installing bun with unzip not installed. """Test that an error is thrown when installing bun with unzip not installed.
@ -375,9 +447,10 @@ def test_create_reflex_dir(mocker, is_windows):
is_windows: Whether platform is windows. is_windows: Whether platform is windows.
""" """
mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", is_windows) mocker.patch("reflex.utils.prerequisites.constants.IS_WINDOWS", is_windows)
mocker.patch("reflex.utils.prerequisites.install_bun", mocker.Mock()) mocker.patch("reflex.utils.prerequisites.processes.run_concurrently", mocker.Mock())
mocker.patch("reflex.utils.prerequisites.install_node", mocker.Mock())
mocker.patch("reflex.utils.prerequisites.initialize_web_directory", mocker.Mock()) mocker.patch("reflex.utils.prerequisites.initialize_web_directory", mocker.Mock())
mocker.patch("reflex.utils.processes.run_concurrently")
mocker.patch("reflex.utils.prerequisites.validate_bun")
create_cmd = mocker.patch( create_cmd = mocker.patch(
"reflex.utils.prerequisites.path_ops.mkdir", mocker.Mock() "reflex.utils.prerequisites.path_ops.mkdir", mocker.Mock()
) )