diff --git a/pyproject.toml b/pyproject.toml index 65a237b2..c936b042 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,11 +44,13 @@ dependencies = [ "requests", "resolvelib", "requests-mock", + "starlette", "stevedore", "tomlkit", "tqdm", "wheel", "uv>=0.8.19", + "uvicorn", ] [project.optional-dependencies] diff --git a/src/fromager/commands/server.py b/src/fromager/commands/server.py index 37d17543..d55cdf40 100644 --- a/src/fromager/commands/server.py +++ b/src/fromager/commands/server.py @@ -27,10 +27,10 @@ def wheel_server( ) -> None: "Start a web server to serve the local wheels-repo" server.update_wheel_mirror(wkctx) - t = server.run_wheel_server( + _, _, thread = server.run_wheel_server( wkctx, address=address, port=port, ) print(f"Listening on {wkctx.wheel_server_url}") - t.join() + thread.join() diff --git a/src/fromager/server.py b/src/fromager/server.py index d0e86c47..a529e601 100644 --- a/src/fromager/server.py +++ b/src/fromager/server.py @@ -1,15 +1,24 @@ from __future__ import annotations -import functools -import http.server +import asyncio import logging import os import pathlib import shutil +import socket +import stat +import textwrap import threading import typing +from urllib.parse import quote +import uvicorn from packaging.utils import parse_wheel_filename +from starlette.applications import Starlette +from starlette.exceptions import HTTPException +from starlette.requests import Request +from starlette.responses import FileResponse, HTMLResponse, RedirectResponse, Response +from starlette.routing import Route from .threading_utils import with_thread_lock @@ -19,11 +28,6 @@ logger = logging.getLogger(__name__) -class LoggingHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): - def log_message(self, format: str, *args: typing.Any) -> None: - logger.debug(format, *args) - - def start_wheel_server(ctx: context.WorkContext) -> None: update_wheel_mirror(ctx) if ctx.wheel_server_url: @@ -34,33 +38,24 @@ def start_wheel_server(ctx: context.WorkContext) -> None: def run_wheel_server( ctx: context.WorkContext, - address: str = "localhost", + address: str = "127.0.0.1", port: int = 0, -) -> threading.Thread: - server = http.server.ThreadingHTTPServer( - (address, port), - functools.partial(LoggingHTTPRequestHandler, directory=str(ctx.wheels_repo)), - bind_and_activate=False, +) -> tuple[uvicorn.Server, socket.socket, threading.Thread]: + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + + app = make_app(ctx.wheel_server_dir) + server, sock, thread = _run_background_thread( + loop=loop, app=app, host=address, port=port ) - server.timeout = 0.5 - server.allow_reuse_address = True - - logger.debug(f"address {server.server_address}") - server.server_bind() - ctx.wheel_server_url = f"http://{address}:{server.server_port}/simple/" - logger.debug("starting wheel server at %s", ctx.wheel_server_url) - server.server_activate() + realport = sock.getsockname()[1] + ctx.wheel_server_url = f"http://{address}:{realport}/simple/" - def serve_forever(server: http.server.ThreadingHTTPServer) -> None: - # ensure server.server_close() is called - with server: - server.serve_forever() - - t = threading.Thread(target=serve_forever, args=(server,)) - t.setDaemon(True) - t.start() - return t + logger.info("started wheel server at %s", ctx.wheel_server_url) + return server, sock, thread @with_thread_lock() @@ -92,3 +87,149 @@ def update_wheel_mirror(ctx: context.WorkContext) -> None: logger.debug("linking %s -> %s into local index", wheel.name, relpath) simple_dest_filename.parent.mkdir(parents=True, exist_ok=True) simple_dest_filename.symlink_to(relpath) + + +class SimpleHTMLIndex: + """Simple HTML Repository API (1.0) + + https://packaging.python.org/en/latest/specifications/simple-repository-api/ + """ + + html_index = textwrap.dedent( + """\ + + +
+ +