blob: 31c549953af7b82ce07a1310635a377a36d0674c [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import os
import shutil
import subprocess
import time
import webbrowser
from contextlib import contextmanager
import click
import requests
from loguru import logger
def _command(command: str, capture_output: bool) -> str:
"""Runs a simple command"""
logger.info(f"Running command: {command}")
if isinstance(command, str):
command = command.split(" ")
if capture_output:
try:
return (
subprocess.check_output(command, stderr=subprocess.PIPE, shell=False)
.decode()
.strip()
)
except subprocess.CalledProcessError as e:
print(e.stdout.decode())
print(e.stderr.decode())
raise e
subprocess.run(command, shell=False, check=True)
def _get_git_root() -> str:
return _command("git rev-parse --show-toplevel", capture_output=True)
def open_when_ready(check_url: str, open_url: str):
while True:
try:
response = requests.get(check_url)
if response.status_code == 200:
webbrowser.open(open_url)
return
else:
pass
except requests.exceptions.RequestException:
pass
time.sleep(1)
@contextmanager
def cd(path):
old_dir = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old_dir)
@click.group()
def cli():
pass
def _build_ui(skip_install: bool = False):
"""
Build the UI from source following Burr's pattern.
Steps (matching burr/cli/__main__.py:135-156):
1. npm install (unless --skip-install)
2. npm run build
3. rm -rf backend/server/build
4. mkdir -p backend/server/build
5. cp -a frontend/build/. backend/server/build/
6. Verify critical files exist
"""
# Step 1: Install dependencies (like Burr does)
if not skip_install:
logger.info("Installing npm dependencies...")
cmd = "npm install --prefix ui/frontend"
_command(cmd, capture_output=False)
# Step 2: Build frontend
logger.info("Building frontend...")
cmd = "npm run build --prefix ui/frontend"
_command(cmd, capture_output=False)
# Step 3: Clear old build
logger.info("Clearing old build directory...")
cmd = "rm -rf ui/backend/server/build"
_command(cmd, capture_output=False)
# Step 4: Ensure directory exists
cmd = "mkdir -p ui/backend/server/build"
_command(cmd, capture_output=False)
# Step 5: Copy with archive mode (like Burr: cp -a)
logger.info("Copying built assets to backend...")
cmd = "cp -a ui/frontend/build/. ui/backend/server/build/"
_command(cmd, capture_output=False)
# Step 6: Verify build succeeded
git_root = _get_git_root()
build_dir = os.path.join(git_root, "ui/backend/server/build")
if not os.path.exists(os.path.join(build_dir, "index.html")):
raise RuntimeError("Build failed: index.html not found in build directory")
# Vite outputs to assets/, CRA outputs to static/
if not (
os.path.exists(os.path.join(build_dir, "assets"))
or os.path.exists(os.path.join(build_dir, "static"))
):
raise RuntimeError(
"Build failed: assets/ or static/ directory not found in build directory"
)
logger.info(f"✓ Build verified: {build_dir}")
@cli.command()
@click.option("--skip-install", is_flag=True, help="Skip npm install for faster builds")
def build_ui(skip_install: bool):
logger.info("Building UI -- this may take a bit...")
git_root = _get_git_root()
with cd(git_root):
_build_ui(skip_install=skip_install)
logger.info("Built UI!")
@cli.command(help="Publishes the package to a repository")
@click.option("--prod", is_flag=True, help="Publish to pypi (rather than test pypi)")
@click.option("--no-wipe-dist", is_flag=True, help="Wipe the dist/ directory before building")
def build_and_publish(prod: bool, no_wipe_dist: bool):
git_root = _get_git_root()
install_path = os.path.join(git_root, "ui/backend")
logger.info("Building UI -- this may take a bit...")
build_ui.callback() # use the underlying function, not click's object
with cd(install_path):
logger.info("Built UI!")
if not no_wipe_dist:
logger.info("Wiping dist/ directory for a clean publish.")
shutil.rmtree("dist", ignore_errors=True)
_command("uv run python -m build", capture_output=False)
repository = "pypi" if prod else "testpypi"
_command(
f"uv run python -m twine upload --repository {repository} dist/*", capture_output=False
)
logger.info(f"Published to {repository}! 🎉")
if __name__ == "__main__":
cli()