tools/serve.py: build, serve and rebuild on change (t9)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build, serve and rebuild when sources change: python3 tools/serve.py [--port 8000]."""
|
||||
import argparse, errno, http.server, pathlib, subprocess, sys, threading, time
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
DIST = ROOT / 'dist'
|
||||
|
||||
|
||||
class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=str(DIST), **kwargs)
|
||||
|
||||
def end_headers(self):
|
||||
self.send_header('Cache-Control', 'no-store')
|
||||
super().end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
|
||||
def build(first=False):
|
||||
result = subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
if result.returncode:
|
||||
print('build failed:\n' + result.stdout, flush=True)
|
||||
elif first:
|
||||
print(result.stdout, end='', flush=True)
|
||||
else:
|
||||
print(time.strftime('rebuilt %H:%M:%S'), flush=True)
|
||||
|
||||
|
||||
def mtimes():
|
||||
files = [ROOT / 'build.py', *(ROOT / 'src').rglob('*')]
|
||||
stamps = {}
|
||||
for path in files:
|
||||
try:
|
||||
if path.is_file(): stamps[path] = path.stat().st_mtime_ns
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return stamps
|
||||
|
||||
|
||||
def watch(stopped, previous):
|
||||
while not stopped.wait(0.5):
|
||||
current = mtimes()
|
||||
if current != previous:
|
||||
previous = current
|
||||
build()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--port', type=int, default=8000)
|
||||
args = parser.parse_args()
|
||||
if not 1 <= args.port <= 65525:
|
||||
parser.error('--port must be between 1 and 65525 (leaves room for ten retries)')
|
||||
previous = mtimes()
|
||||
build(first=True)
|
||||
if not (DIST / 'index.html').is_file():
|
||||
sys.exit('Cannot serve: dist/index.html does not exist. Fix the build and try again.')
|
||||
for port in range(args.port, args.port + 11):
|
||||
try:
|
||||
server = http.server.ThreadingHTTPServer(('127.0.0.1', port), Handler)
|
||||
break
|
||||
except OSError as error:
|
||||
if error.errno != errno.EADDRINUSE:
|
||||
sys.exit(f'Cannot serve on port {port}: {error}')
|
||||
else:
|
||||
sys.exit(f'Cannot serve: ports {args.port}-{args.port + 10} are all busy.')
|
||||
if port != args.port:
|
||||
print(f'Port {args.port} was busy; using {port}.', flush=True)
|
||||
print(f'Serving dist/ at http://localhost:{port} (Ctrl+C to stop)', flush=True)
|
||||
stopped = threading.Event()
|
||||
threading.Thread(target=watch, args=(stopped, previous), daemon=True).start()
|
||||
try:
|
||||
with server:
|
||||
server.serve_forever()
|
||||
finally:
|
||||
stopped.set()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print('\nServer stopped.', flush=True)
|
||||
Reference in New Issue
Block a user