new python build-deploy chain

This commit is contained in:
Mikkeli Matlock
2026-01-25 00:34:01 +09:00
parent 42e2094635
commit 754cecffd4
8 changed files with 532 additions and 24 deletions

64
scripts/build-deploy.py Normal file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""One-click build and deploy for Smart Serow.
Combines build.py and deploy.py with sensible defaults.
Defaults to --restart since that's usually what you want.
"""
import argparse
import sys
from pathlib import Path
# Import sibling modules
sys.path.insert(0, str(Path(__file__).parent))
from build import build
from deploy import deploy
def main():
parser = argparse.ArgumentParser(
description="Build and deploy Smart Serow in one step",
)
parser.add_argument(
"--clean", "-c",
action="store_true",
help="Clean CMake cache before building",
)
parser.add_argument(
"--no-restart",
action="store_true",
help="Don't restart service after deploy (default: restart)",
)
parser.add_argument(
"--build-only",
action="store_true",
help="Only build, don't deploy",
)
parser.add_argument(
"--deploy-only",
action="store_true",
help="Only deploy, don't build",
)
args = parser.parse_args()
# Build
if not args.deploy_only:
print()
if not build(clean=args.clean):
print("Build failed!")
sys.exit(1)
# Deploy
if not args.build_only:
print()
restart = not args.no_restart
if not deploy(restart=restart):
print("Deploy failed!")
sys.exit(1)
print()
print("=== All done! ===")
if __name__ == "__main__":
main()

6
scripts/build-deploy.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
# Wrapper for build-deploy.py
# Usage: ./build-deploy.sh [options]
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec python3 "$SCRIPT_DIR/build-deploy.py" "$@"

142
scripts/build.py Normal file
View File

@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Build script for Smart Serow Flutter UI.
Run this in WSL2 with flutter-elinux installed.
"""
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent.resolve()
PROJECT_ROOT = SCRIPT_DIR.parent
UI_DIR = PROJECT_ROOT / "pi" / "ui"
BUILD_OUTPUT = UI_DIR / "build" / "elinux" / "arm64" / "release" / "bundle"
def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
"""Run a command, exit on failure."""
print(f"{' '.join(cmd)}")
result = subprocess.run(cmd, **kwargs)
if result.returncode != 0:
sys.exit(result.returncode)
return result
def check_flutter_elinux() -> str:
"""Check if flutter-elinux is available, return path."""
result = subprocess.run(
["which", "flutter-elinux"],
capture_output=True,
text=True,
)
if result.returncode != 0:
print("ERROR: flutter-elinux not found in PATH")
print("Install it or check your PATH")
print(f"\nCurrent PATH: {os.environ.get('PATH', '')}")
sys.exit(1)
return result.stdout.strip()
def set_cross_compile_env():
"""Set environment variables for ARM64 cross-compilation."""
env_vars = {
"CC": "aarch64-linux-gnu-gcc",
"CXX": "aarch64-linux-gnu-g++",
"AR": "aarch64-linux-gnu-ar",
"LD": "aarch64-linux-gnu-ld",
"CMAKE_C_COMPILER": "aarch64-linux-gnu-gcc",
"CMAKE_CXX_COMPILER": "aarch64-linux-gnu-g++",
}
os.environ.update(env_vars)
return env_vars
def build(clean: bool = False) -> bool:
"""Run the build process. Returns True on success."""
print("=== Smart Serow Build ===")
print(f"Project: {UI_DIR}")
# Check flutter-elinux
flutter_path = check_flutter_elinux()
print(f"Using: {flutter_path}")
# Set cross-compilation env
env_vars = set_cross_compile_env()
print(f"Cross-compiler: {env_vars['CXX']}")
os.chdir(UI_DIR)
# Initialize elinux project if needed
elinux_dir = UI_DIR / "elinux"
if not elinux_dir.exists():
print("Initializing elinux project structure...")
run([
"flutter-elinux", "create", ".",
"--project-name", "smartserow_ui",
"--org", "com.smartserow",
])
# Clean if requested
if clean:
cache_dir = UI_DIR / "build" / "elinux" / "arm64"
if cache_dir.exists():
print("Cleaning CMake cache...")
shutil.rmtree(cache_dir)
# Fetch dependencies
print("Fetching dependencies...")
run(["flutter-elinux", "pub", "get"])
# Build command
print("Building for ARM64 (elinux) with DRM-GBM backend...")
build_cmd = [
"flutter-elinux", "build", "elinux",
"--target-arch=arm64",
"--target-backend-type=gbm",
"--target-compiler-triple=aarch64-linux-gnu",
"--release",
]
# Add sysroot if available
sysroot = PROJECT_ROOT / "pi_sysroot"
if sysroot.exists():
print(f"Using Pi sysroot: {sysroot}")
build_cmd.append(f"--target-sysroot={sysroot}")
run(build_cmd)
# Verify output
if BUILD_OUTPUT.exists():
print()
print("=== Build Complete ===")
print(f"Output: {BUILD_OUTPUT}")
for f in BUILD_OUTPUT.iterdir():
size = f.stat().st_size
print(f" {f.name}: {size:,} bytes")
return True
else:
print(f"ERROR: Build output not found at {BUILD_OUTPUT}")
return False
def main():
parser = argparse.ArgumentParser(description="Build Smart Serow Flutter UI")
parser.add_argument(
"--clean", "-c",
action="store_true",
help="Clean CMake cache before building",
)
args = parser.parse_args()
success = build(clean=args.clean)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

97
scripts/deploy.py Normal file
View File

@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Deploy script for Smart Serow Flutter UI.
Pushes build bundle to Pi and optionally restarts service.
"""
import argparse
import json
import subprocess
import sys
import time
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent.resolve()
PROJECT_ROOT = SCRIPT_DIR.parent
CONFIG_FILE = SCRIPT_DIR / "deploy_target.json"
BUILD_DIR = PROJECT_ROOT / "pi" / "ui" / "build" / "elinux" / "arm64" / "release" / "bundle"
def run(cmd: list[str], check: bool = True, **kwargs) -> subprocess.CompletedProcess:
"""Run a command."""
print(f"{' '.join(cmd)}")
return subprocess.run(cmd, check=check, **kwargs)
def load_config() -> dict:
"""Load deploy target configuration."""
if not CONFIG_FILE.exists():
print(f"ERROR: Config file not found: {CONFIG_FILE}")
print("Create it based on deploy_target.sample.json")
sys.exit(1)
with open(CONFIG_FILE) as f:
return json.load(f)
def deploy(restart: bool = False) -> bool:
"""Deploy to Pi. Returns True on success."""
config = load_config()
pi_user = config["user"]
pi_host = config["host"]
remote_path = config["remote_path"]
service_name = config["service_name"]
ssh_target = f"{pi_user}@{pi_host}"
print("=== Smart Serow Deploy ===")
print(f"Target: {ssh_target}:{remote_path}")
print(f"Source: {BUILD_DIR}")
if not BUILD_DIR.exists():
print("ERROR: Build directory not found. Run build.py first.")
return False
# Sync build to Pi
print()
print("Syncing files...")
run([
"rsync", "-avz", "--delete",
f"{BUILD_DIR}/",
f"{ssh_target}:{remote_path}/bundle/",
])
# Restart service if requested
if restart:
print()
print(f"Restarting service: {service_name}")
run(["ssh", ssh_target, f"sudo systemctl restart {service_name}"])
time.sleep(2)
run(["ssh", ssh_target, f"systemctl status {service_name} --no-pager"], check=False)
else:
print()
print("Deploy complete. To restart service, run:")
print(f" ssh {ssh_target} 'sudo systemctl restart {service_name}'")
print()
print("Or run this script with --restart flag")
return True
def main():
parser = argparse.ArgumentParser(description="Deploy Smart Serow to Pi")
parser.add_argument(
"--restart", "-r",
action="store_true",
help="Restart the systemd service after deploy",
)
args = parser.parse_args()
success = deploy(restart=args.restart)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

View File

@@ -1,6 +1,6 @@
{
"user": "pi",
"host": "192.168.114.5",
"host": "raspberrypi.local",
"remote_path": "/opt/smartserow",
"service_name": "smartserow-ui"
}