Skip to content

Commit

Permalink
[REF-1586] Use bun as a package manager on windows (#2359)
Browse files Browse the repository at this point in the history
  • Loading branch information
masenf authored Apr 11, 2024
1 parent e377ce7 commit 3c8c7c3
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 36 deletions.
2 changes: 2 additions & 0 deletions reflex/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
COOKIES,
ENV_MODE_ENV_VAR,
IS_WINDOWS,
IS_WINDOWS_BUN_SUPPORTED_MACHINE, # type: ignore
LOCAL_STORAGE,
POLLING_MAX_HTTP_BUFFER_SIZE,
PYTEST_CURRENT_TEST,
Expand Down Expand Up @@ -86,6 +87,7 @@
Hooks,
Imports,
IS_WINDOWS,
IS_WINDOWS_BUN_SUPPORTED_MACHINE,
LOCAL_STORAGE,
LogLevel,
MemoizationDisposition,
Expand Down
5 changes: 5 additions & 0 deletions reflex/constants/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
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):
Expand Down
2 changes: 2 additions & 0 deletions reflex/constants/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
6 changes: 4 additions & 2 deletions reflex/constants/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
11 changes: 10 additions & 1 deletion reflex/utils/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
73 changes: 40 additions & 33 deletions reflex/utils/prerequisites.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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(
Expand All @@ -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):
Expand Down Expand Up @@ -813,40 +819,44 @@ 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",
"-d",
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:
Expand Down Expand Up @@ -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()
Expand Down
37 changes: 37 additions & 0 deletions reflex/utils/processes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 3c8c7c3

Please sign in to comment.