new python build-deploy chain
This commit is contained in:
64
scripts/build-deploy.py
Normal file
64
scripts/build-deploy.py
Normal 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
6
scripts/build-deploy.sh
Normal 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
142
scripts/build.py
Normal 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
97
scripts/deploy.py
Normal 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()
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"user": "pi",
|
||||
"host": "192.168.114.5",
|
||||
"host": "raspberrypi.local",
|
||||
"remote_path": "/opt/smartserow",
|
||||
"service_name": "smartserow-ui"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user