991 lines
31 KiB
Python
991 lines
31 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
CapacityReport 统一构建脚本
|
||
支持完整部署包和更新包的构建
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
import platform
|
||
import shutil
|
||
import subprocess
|
||
import re
|
||
import tarfile
|
||
import traceback
|
||
from pathlib import Path
|
||
|
||
# ============================================================================
|
||
# 配置常量 - 可根据需要修改
|
||
# ============================================================================
|
||
|
||
# 默认镜像版本
|
||
DEFAULT_MYSQL_VERSION = "8.0.44"
|
||
DEFAULT_PYTHON_VERSION = "3.13.11-slim"
|
||
|
||
# 版本要求
|
||
MYSQL_MIN_VERSION = (8, 0, 0) # MySQL >= 8.0
|
||
MYSQL_MAX_VERSION = (9, 0, 0) # MySQL < 9.0
|
||
PYTHON_MIN_VERSION = (3, 10, 0) # Python >= 3.10
|
||
PYTHON_MAX_VERSION = (3, 14, 0) # Python < 3.14
|
||
|
||
# 数据库配置
|
||
MYSQL_ROOT_USER = "root"
|
||
MYSQL_ROOT_PASSWORD = "gmcc123"
|
||
MYSQL_DATABASE = "CapacityReport"
|
||
MYSQL_HOST = "capacity-mysql"
|
||
MYSQL_PORT = 3306
|
||
|
||
# 应用端口映射
|
||
APP_PORT_HOST = 19081
|
||
MYSQL_PORT_HOST = 13306
|
||
|
||
# ============================================================================
|
||
# 脚本目录配置
|
||
# ============================================================================
|
||
|
||
SCRIPT_DIR = Path(__file__).parent.absolute()
|
||
PROJECT_ROOT = SCRIPT_DIR.parent
|
||
DIST_DIR = PROJECT_ROOT / "dist"
|
||
BUILD_DIR = PROJECT_ROOT / "build"
|
||
TEMP_BUILD_DIR = BUILD_DIR / "temp"
|
||
|
||
if platform.system() == 'Windows':
|
||
try:
|
||
import ctypes
|
||
kernel32 = ctypes.windll.kernel32
|
||
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
||
except OSError:
|
||
pass
|
||
|
||
class Colors:
|
||
GREEN = '\033[92m'
|
||
YELLOW = '\033[93m'
|
||
RED = '\033[91m'
|
||
BLUE = '\033[94m'
|
||
RESET = '\033[0m'
|
||
|
||
def print_step(msg):
|
||
print(f"\n{Colors.GREEN}{msg}{Colors.RESET}")
|
||
|
||
def print_info(msg):
|
||
print(f" {msg}")
|
||
|
||
def print_warning(msg):
|
||
print(f"{Colors.YELLOW}警告: {msg}{Colors.RESET}")
|
||
|
||
def print_error(msg):
|
||
print(f"{Colors.RED}错误: {msg}{Colors.RESET}")
|
||
|
||
def print_choice(msg):
|
||
print(f"{Colors.BLUE}{msg}{Colors.RESET}")
|
||
|
||
def run_cmd(cmd, check=True, capture_output=False):
|
||
"""执行命令"""
|
||
if isinstance(cmd, str):
|
||
cmd = cmd.split()
|
||
try:
|
||
result = subprocess.run(
|
||
cmd,
|
||
check=check,
|
||
capture_output=capture_output,
|
||
text=True
|
||
)
|
||
return result
|
||
except subprocess.CalledProcessError:
|
||
if check:
|
||
print_error(f"命令执行失败: {' '.join(cmd)}")
|
||
sys.exit(1)
|
||
return None
|
||
except FileNotFoundError:
|
||
print_error(f"命令未找到: {cmd[0]}")
|
||
sys.exit(1)
|
||
|
||
def check_command(cmd):
|
||
"""检查命令是否存在"""
|
||
try:
|
||
if cmd == "docker":
|
||
subprocess.run([cmd, "--version"], capture_output=True, check=True)
|
||
elif cmd == "docker-compose":
|
||
subprocess.run([cmd, "version"], capture_output=True, check=True)
|
||
return True
|
||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||
return False
|
||
|
||
def parse_version(version_str):
|
||
"""解析版本字符串为元组,例如 '8.0.44' -> (8, 0, 44)"""
|
||
parts = version_str.split('.')
|
||
try:
|
||
# 处理带后缀的版本,如 '3.13.11-slim' -> (3, 13, 11)
|
||
base_version = parts[0] if len(parts) > 0 else '0'
|
||
major = int(base_version)
|
||
minor = int(parts[1]) if len(parts) > 1 else 0
|
||
patch_str = parts[2] if len(parts) > 2 else '0'
|
||
# 移除后缀,如 '11-slim' -> '11'
|
||
patch = int(patch_str.split('-')[0])
|
||
return (major, minor, patch)
|
||
except (ValueError, IndexError):
|
||
return None
|
||
|
||
def check_version_in_range(version_tuple, min_version, max_version):
|
||
"""检查版本是否在指定范围内 [min, max)"""
|
||
if version_tuple is None:
|
||
return False
|
||
return min_version <= version_tuple < max_version
|
||
|
||
def get_python_tag_from_dockerfile():
|
||
"""从 Dockerfile 中提取 Python 镜像标签"""
|
||
# 从 build 目录读取 Dockerfile
|
||
dockerfile_path = BUILD_DIR / "Dockerfile"
|
||
if not dockerfile_path.exists():
|
||
print_error(f"Dockerfile 不存在: {dockerfile_path}")
|
||
print_error("请确保 build/Dockerfile 文件存在")
|
||
sys.exit(1)
|
||
|
||
with open(dockerfile_path, 'r', encoding='utf-8') as f:
|
||
for line in f:
|
||
match = re.match(r'^FROM\s+python:(.+?)(?:\s|$)', line, re.IGNORECASE)
|
||
if match:
|
||
tag = match.group(1).strip()
|
||
return tag
|
||
|
||
print_error("无法从 Dockerfile 中找到 Python 镜像标签")
|
||
print_error("请确保 Dockerfile 中包含 'FROM python:xxx' 行")
|
||
sys.exit(1)
|
||
|
||
def check_and_get_python_image():
|
||
"""检查并获取符合要求的 Python 镜像"""
|
||
dockerfile_tag = get_python_tag_from_dockerfile()
|
||
dockerfile_version = parse_version(dockerfile_tag)
|
||
|
||
# 检查 Dockerfile 中的版本是否符合要求
|
||
if dockerfile_version and check_version_in_range(
|
||
dockerfile_version, PYTHON_MIN_VERSION, PYTHON_MAX_VERSION
|
||
):
|
||
print_info(f"Dockerfile 中的 Python 版本符合要求: {dockerfile_tag}")
|
||
return f"python:{dockerfile_tag}"
|
||
else:
|
||
print_warning(f"Dockerfile 中的 Python 版本 {dockerfile_tag} 不符合要求")
|
||
print_warning(f"要求: Python >= {'.'.join(map(str, PYTHON_MIN_VERSION))} < {'.'.join(map(str, PYTHON_MAX_VERSION))}")
|
||
print_info(f"将使用默认版本: {DEFAULT_PYTHON_VERSION}")
|
||
print_info(f"如果镜像不存在,将自动拉取: python:{DEFAULT_PYTHON_VERSION}")
|
||
return f"python:{DEFAULT_PYTHON_VERSION}"
|
||
|
||
def check_and_get_mysql_image():
|
||
"""检查并获取符合要求的 MySQL 镜像"""
|
||
# 检查默认版本是否符合要求
|
||
default_version = parse_version(DEFAULT_MYSQL_VERSION)
|
||
if default_version and check_version_in_range(
|
||
default_version, MYSQL_MIN_VERSION, MYSQL_MAX_VERSION
|
||
):
|
||
print_info(f"默认 MySQL 版本符合要求: {DEFAULT_MYSQL_VERSION}")
|
||
return f"mysql:{DEFAULT_MYSQL_VERSION}"
|
||
else:
|
||
print_error(f"默认 MySQL 版本 {DEFAULT_MYSQL_VERSION} 不符合要求")
|
||
print_error(f"要求: MySQL >= {'.'.join(map(str, MYSQL_MIN_VERSION))} < {'.'.join(map(str, MYSQL_MAX_VERSION))}")
|
||
sys.exit(1)
|
||
|
||
def image_exists(image_name):
|
||
"""检查镜像是否已存在"""
|
||
try:
|
||
result = subprocess.run(
|
||
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"],
|
||
capture_output=True,
|
||
text=True,
|
||
check=True
|
||
)
|
||
return image_name in result.stdout
|
||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||
return False
|
||
|
||
def format_size(size_bytes):
|
||
"""格式化文件大小"""
|
||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||
if size_bytes < 1024.0:
|
||
return f"{size_bytes:.1f} {unit}"
|
||
size_bytes /= 1024.0
|
||
return f"{size_bytes:.1f} TB"
|
||
|
||
def write_sh_script(file_path, content):
|
||
"""写入 shell 脚本,确保使用 LF 换行和 UTF-8 编码"""
|
||
# 确保内容使用 LF 换行
|
||
content = content.replace('\r\n', '\n').replace('\r', '\n')
|
||
# 写入文件(二进制模式确保换行符不被转换)
|
||
with open(file_path, 'wb') as f:
|
||
f.write(content.encode('utf-8'))
|
||
# 设置执行权限(在 Linux 上)
|
||
if platform.system() != 'Windows':
|
||
os.chmod(file_path, 0o755)
|
||
|
||
def get_deploy_sh():
|
||
"""生成 deploy.sh 脚本内容"""
|
||
mysql_version = DEFAULT_MYSQL_VERSION
|
||
return f'''#!/bin/bash
|
||
# CapacityReport Deployment Script
|
||
# Run this script on offline machine to import images and start services
|
||
|
||
set -e
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
|
||
cd "$SCRIPT_DIR"
|
||
|
||
# Set permissions automatically
|
||
chmod -R 0777 "$SCRIPT_DIR" 2>/dev/null || true
|
||
find "$SCRIPT_DIR" -type f \\( -name "*.sh" -o -name "*.py" \\) -exec chmod +x {{}} \\; 2>/dev/null || true
|
||
|
||
echo "=========================================="
|
||
echo "CapacityReport Deployment"
|
||
echo "=========================================="
|
||
echo ""
|
||
|
||
# Check Docker
|
||
if ! command -v docker &> /dev/null; then
|
||
echo "Error: Docker not found, please install Docker"
|
||
exit 1
|
||
fi
|
||
|
||
# Check Docker Compose
|
||
DOCKER_COMPOSE_CMD=""
|
||
if docker compose version &> /dev/null; then
|
||
DOCKER_COMPOSE_CMD="docker compose"
|
||
elif command -v docker-compose &> /dev/null; then
|
||
DOCKER_COMPOSE_CMD="docker-compose"
|
||
else
|
||
echo "Error: Docker Compose not found, please install Docker Compose"
|
||
exit 1
|
||
fi
|
||
|
||
# Check image file
|
||
if [ ! -f "images/capacity-images.tar" ]; then
|
||
echo "Error: Image file not found: images/capacity-images.tar"
|
||
exit 1
|
||
fi
|
||
|
||
echo "Step 1: Checking ports..."
|
||
check_port() {{
|
||
local port=${{1}}
|
||
local name=${{2}}
|
||
local in_use=false
|
||
|
||
if command -v lsof &> /dev/null; then
|
||
if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1; then
|
||
in_use=true
|
||
fi
|
||
elif command -v netstat &> /dev/null; then
|
||
if netstat -tuln 2>/dev/null | grep -q ":$port "; then
|
||
in_use=true
|
||
fi
|
||
elif command -v ss &> /dev/null; then
|
||
if ss -tuln 2>/dev/null | grep -q ":$port "; then
|
||
in_use=true
|
||
fi
|
||
fi
|
||
|
||
if [ "$in_use" = true ]; then
|
||
echo "Warning: Port $port ($name) is in use"
|
||
read -p "Continue? (y/n) " -n 1 -r
|
||
echo
|
||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||
exit 1
|
||
fi
|
||
fi
|
||
{{}}
|
||
|
||
check_port {APP_PORT_HOST} "Frontend"
|
||
check_port {MYSQL_PORT_HOST} "MySQL"
|
||
|
||
echo ""
|
||
echo "Step 2: Loading images..."
|
||
docker load -i images/capacity-images.tar >/dev/null 2>&1
|
||
|
||
echo ""
|
||
echo "Step 3: Verifying images..."
|
||
if docker images | grep -q "capacity-report-app.*latest"; then
|
||
echo " App image: OK"
|
||
else
|
||
echo " App image: FAILED"
|
||
exit 1
|
||
fi
|
||
|
||
if docker images | grep -q "capacity-mysql.*{mysql_version}"; then
|
||
echo " MySQL image: OK"
|
||
else
|
||
echo " MySQL image: FAILED"
|
||
exit 1
|
||
fi
|
||
|
||
echo ""
|
||
echo "Step 4: Creating directories..."
|
||
mkdir -p cache logs
|
||
chmod 777 cache logs 2>/dev/null || true
|
||
|
||
echo ""
|
||
echo "Step 5: Checking config..."
|
||
if [ ! -f "Configure.json" ]; then
|
||
echo " Creating default Configure.json..."
|
||
cat > Configure.json << 'EOF'
|
||
{{
|
||
"MySQL_DBInfo": {{
|
||
"host": "{MYSQL_HOST}",
|
||
"port": {MYSQL_PORT},
|
||
"user": "{MYSQL_ROOT_USER}",
|
||
"passwd": "{MYSQL_ROOT_PASSWORD}",
|
||
"dbname": "{MYSQL_DATABASE}"
|
||
}},
|
||
"ExtractField": []
|
||
}}
|
||
EOF
|
||
else
|
||
if command -v python3 &> /dev/null || command -v python &> /dev/null; then
|
||
(command -v python3 >/dev/null && python3 || python) << 'PYEOF'
|
||
import json
|
||
import sys
|
||
|
||
try:
|
||
with open('Configure.json', 'r', encoding='utf-8') as f:
|
||
config = json.load(f)
|
||
|
||
db_info = config.get('MySQL_DBInfo', {{}})
|
||
needs_update = False
|
||
|
||
if db_info.get('host') != '{MYSQL_HOST}':
|
||
db_info['host'] = '{MYSQL_HOST}'
|
||
needs_update = True
|
||
|
||
if db_info.get('port') != {MYSQL_PORT}:
|
||
db_info['port'] = {MYSQL_PORT}
|
||
needs_update = True
|
||
|
||
if db_info.get('user') != '{MYSQL_ROOT_USER}':
|
||
db_info['user'] = '{MYSQL_ROOT_USER}'
|
||
needs_update = True
|
||
|
||
if db_info.get('passwd') != '{MYSQL_ROOT_PASSWORD}':
|
||
db_info['passwd'] = '{MYSQL_ROOT_PASSWORD}'
|
||
needs_update = True
|
||
|
||
if db_info.get('dbname') != '{MYSQL_DATABASE}':
|
||
db_info['dbname'] = '{MYSQL_DATABASE}'
|
||
needs_update = True
|
||
|
||
if needs_update:
|
||
with open('Configure.json', 'w', encoding='utf-8') as f:
|
||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||
print(" Updated Configure.json")
|
||
except:
|
||
pass
|
||
PYEOF
|
||
fi
|
||
fi
|
||
|
||
if [ ! -f "ReportScript.sql" ]; then
|
||
touch ReportScript.sql
|
||
fi
|
||
|
||
echo ""
|
||
echo "Step 6: Starting services..."
|
||
$DOCKER_COMPOSE_CMD down 2>/dev/null || true
|
||
$DOCKER_COMPOSE_CMD up -d >/dev/null 2>&1
|
||
|
||
echo ""
|
||
echo "Step 7: Waiting for services..."
|
||
for i in {{1..30}}; do
|
||
if docker exec capacity-mysql mysqladmin ping -h localhost -u {MYSQL_ROOT_USER} -p{MYSQL_ROOT_PASSWORD} --silent 2>/dev/null; then
|
||
echo " MySQL: Ready"
|
||
break
|
||
fi
|
||
if [ $i -eq 30 ]; then
|
||
echo " MySQL: Timeout"
|
||
exit 1
|
||
fi
|
||
sleep 2
|
||
done
|
||
|
||
if command -v curl &> /dev/null; then
|
||
for i in {{1..30}}; do
|
||
if curl -f http://localhost:{APP_PORT_HOST}/health >/dev/null 2>&1; then
|
||
echo " App: Ready"
|
||
break
|
||
fi
|
||
if [ $i -eq 30 ]; then
|
||
echo " App: Timeout"
|
||
echo " Check logs: $DOCKER_COMPOSE_CMD logs capacity-app"
|
||
exit 1
|
||
fi
|
||
sleep 2
|
||
done
|
||
else
|
||
for i in {{1..30}}; do
|
||
if docker ps --filter "name=capacity-report-app" --filter "status=running" --format "{{.Names}}" | grep -q "capacity-report-app"; then
|
||
if docker inspect capacity-report-app --format='{{.State.Health.Status}}' 2>/dev/null | grep -q "healthy"; then
|
||
echo " App: Ready"
|
||
break
|
||
elif [ $i -gt 10 ]; then
|
||
echo " App: Running"
|
||
break
|
||
fi
|
||
fi
|
||
if [ $i -eq 30 ]; then
|
||
echo " App: Timeout"
|
||
exit 1
|
||
fi
|
||
sleep 2
|
||
done
|
||
fi
|
||
|
||
echo ""
|
||
echo "=========================================="
|
||
echo "Deployment completed!"
|
||
echo "=========================================="
|
||
echo ""
|
||
echo "Frontend: http://localhost:{APP_PORT_HOST}"
|
||
echo ""
|
||
echo "Commands:"
|
||
echo " Logs: $DOCKER_COMPOSE_CMD logs -f"
|
||
echo " Stop: $DOCKER_COMPOSE_CMD down"
|
||
echo " Restart: $DOCKER_COMPOSE_CMD restart"
|
||
echo " Status: $DOCKER_COMPOSE_CMD ps"
|
||
echo ""
|
||
echo "Database:"
|
||
echo " Host: localhost"
|
||
echo " Port: {MYSQL_PORT_HOST}"
|
||
echo " User: {MYSQL_ROOT_USER}"
|
||
echo " Password: {MYSQL_ROOT_PASSWORD}"
|
||
echo " Database: {MYSQL_DATABASE}"
|
||
echo ""
|
||
'''
|
||
|
||
def get_update_sh():
|
||
"""生成 update.sh 脚本内容"""
|
||
return f'''#!/bin/bash
|
||
# CapacityReport Update Script
|
||
# Update application container without affecting database
|
||
# Run this script when you need to update the application
|
||
|
||
set -e
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
|
||
cd "$SCRIPT_DIR"
|
||
|
||
# Set permissions automatically
|
||
chmod -R 0777 "$SCRIPT_DIR" 2>/dev/null || true
|
||
find "$SCRIPT_DIR" -type f \\( -name "*.sh" -o -name "*.py" \\) -exec chmod +x {{}} \\; 2>/dev/null || true
|
||
|
||
echo "=========================================="
|
||
echo "CapacityReport Application Update"
|
||
echo "=========================================="
|
||
echo ""
|
||
|
||
# Check Docker
|
||
if ! command -v docker &> /dev/null; then
|
||
echo "Error: Docker not found, please install Docker"
|
||
exit 1
|
||
fi
|
||
|
||
# Check Docker Compose
|
||
DOCKER_COMPOSE_CMD=""
|
||
if docker compose version &> /dev/null; then
|
||
DOCKER_COMPOSE_CMD="docker compose"
|
||
elif command -v docker-compose &> /dev/null; then
|
||
DOCKER_COMPOSE_CMD="docker-compose"
|
||
else
|
||
echo "Error: Docker Compose not found, please install Docker Compose"
|
||
exit 1
|
||
fi
|
||
|
||
# Check if services are running
|
||
if ! docker ps --format "{{.Names}}" | grep -q "capacity-report-app"; then
|
||
echo "Error: Application container is not running"
|
||
echo "Please use deploy.sh for initial deployment"
|
||
exit 1
|
||
fi
|
||
|
||
echo "Step 1: Checking current status..."
|
||
if docker ps --format "{{.Names}}" | grep -q "capacity-mysql"; then
|
||
echo " MySQL: Running"
|
||
else
|
||
echo " Warning: MySQL container is not running"
|
||
read -p "Continue? (y/n) " -n 1 -r
|
||
echo
|
||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
echo "Step 2: Loading new application image..."
|
||
if [ -f "images/capacity-app-update.tar" ]; then
|
||
echo " Loading images from capacity-app-update.tar..."
|
||
docker load -i images/capacity-app-update.tar >/dev/null 2>&1
|
||
|
||
if docker images | grep -q "capacity-report-app.*latest"; then
|
||
echo " App image: Loaded"
|
||
else
|
||
echo " App image: Not found in tar file"
|
||
exit 1
|
||
fi
|
||
else
|
||
echo " Error: Image file not found: images/capacity-app-update.tar"
|
||
exit 1
|
||
fi
|
||
|
||
echo ""
|
||
echo "Step 3: Stopping application container..."
|
||
$DOCKER_COMPOSE_CMD stop capacity-app 2>/dev/null || true
|
||
$DOCKER_COMPOSE_CMD rm -f capacity-app 2>/dev/null || true
|
||
echo " Application stopped"
|
||
|
||
echo ""
|
||
echo "Step 4: Updating configuration files..."
|
||
if [ -f "Configure.json" ]; then
|
||
if command -v python3 &> /dev/null || command -v python &> /dev/null; then
|
||
(command -v python3 >/dev/null && python3 || python) << 'PYEOF'
|
||
import json
|
||
import sys
|
||
|
||
try:
|
||
with open('Configure.json', 'r', encoding='utf-8') as f:
|
||
config = json.load(f)
|
||
|
||
db_info = config.get('MySQL_DBInfo', {{}})
|
||
needs_update = False
|
||
|
||
if db_info.get('host') != '{MYSQL_HOST}':
|
||
db_info['host'] = '{MYSQL_HOST}'
|
||
needs_update = True
|
||
|
||
if db_info.get('port') != {MYSQL_PORT}:
|
||
db_info['port'] = {MYSQL_PORT}
|
||
needs_update = True
|
||
|
||
if needs_update:
|
||
with open('Configure.json', 'w', encoding='utf-8') as f:
|
||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||
print(" Updated Configure.json")
|
||
except:
|
||
pass
|
||
PYEOF
|
||
fi
|
||
else
|
||
echo " Warning: Configure.json not found, creating default..."
|
||
cat > Configure.json << 'EOF'
|
||
{{
|
||
"MySQL_DBInfo": {{
|
||
"host": "{MYSQL_HOST}",
|
||
"port": {MYSQL_PORT},
|
||
"user": "{MYSQL_ROOT_USER}",
|
||
"passwd": "{MYSQL_ROOT_PASSWORD}",
|
||
"dbname": "{MYSQL_DATABASE}"
|
||
}},
|
||
"ExtractField": []
|
||
}}
|
||
EOF
|
||
fi
|
||
|
||
if [ ! -f "ReportScript.sql" ]; then
|
||
touch ReportScript.sql
|
||
fi
|
||
|
||
mkdir -p cache logs
|
||
chmod 777 cache logs 2>/dev/null || true
|
||
|
||
echo ""
|
||
echo "Step 5: Starting application container..."
|
||
$DOCKER_COMPOSE_CMD up -d capacity-app >/dev/null 2>&1
|
||
echo " Application started"
|
||
|
||
echo ""
|
||
echo "Step 6: Waiting for application to be ready..."
|
||
if command -v curl &> /dev/null; then
|
||
for i in {{1..30}}; do
|
||
if curl -f http://localhost:{APP_PORT_HOST}/health >/dev/null 2>&1; then
|
||
echo " App: Ready"
|
||
break
|
||
fi
|
||
if [ $i -eq 30 ]; then
|
||
echo " App: Timeout (may still be starting)"
|
||
echo " Check logs: $DOCKER_COMPOSE_CMD logs capacity-app"
|
||
exit 1
|
||
fi
|
||
sleep 2
|
||
done
|
||
else
|
||
for i in {{1..30}}; do
|
||
if docker ps --filter "name=capacity-report-app" --filter "status=running" --format "{{.Names}}" | grep -q "capacity-report-app"; then
|
||
if docker inspect capacity-report-app --format='{{.State.Health.Status}}' 2>/dev/null | grep -q "healthy"; then
|
||
echo " App: Ready"
|
||
break
|
||
elif [ $i -gt 10 ]; then
|
||
echo " App: Running"
|
||
break
|
||
fi
|
||
fi
|
||
if [ $i -eq 30 ]; then
|
||
echo " App: Timeout"
|
||
echo " Check logs: $DOCKER_COMPOSE_CMD logs capacity-app"
|
||
exit 1
|
||
fi
|
||
sleep 2
|
||
done
|
||
fi
|
||
|
||
echo ""
|
||
echo "=========================================="
|
||
echo "Update completed!"
|
||
echo "=========================================="
|
||
echo ""
|
||
echo "Frontend: http://localhost:{APP_PORT_HOST}"
|
||
echo ""
|
||
echo "Database status:"
|
||
if docker ps --format "{{.Names}}" | grep -q "capacity-mysql"; then
|
||
echo " MySQL: Running (unchanged)"
|
||
else
|
||
echo " MySQL: Not running"
|
||
fi
|
||
echo ""
|
||
echo "Useful commands:"
|
||
echo " View logs: $DOCKER_COMPOSE_CMD logs -f capacity-app"
|
||
echo " Restart app: $DOCKER_COMPOSE_CMD restart capacity-app"
|
||
echo " Status: $DOCKER_COMPOSE_CMD ps"
|
||
echo ""
|
||
'''
|
||
|
||
def build_full_package():
|
||
"""构建完整部署包"""
|
||
print_step("构建完整部署包...")
|
||
|
||
# 清理临时目录
|
||
if TEMP_BUILD_DIR.exists():
|
||
shutil.rmtree(TEMP_BUILD_DIR)
|
||
TEMP_BUILD_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 检查 Docker
|
||
if not check_command("docker"):
|
||
print_error("未检测到 Docker,请先安装 Docker Desktop")
|
||
sys.exit(1)
|
||
|
||
# 检查 Docker Compose
|
||
try:
|
||
subprocess.run(["docker", "compose", "version"], capture_output=True, check=True)
|
||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||
if not check_command("docker-compose"):
|
||
print_error("未检测到 Docker Compose")
|
||
sys.exit(1)
|
||
|
||
# 检查并获取符合要求的镜像
|
||
print_step("检查镜像版本...")
|
||
python_image = check_and_get_python_image()
|
||
mysql_image = check_and_get_mysql_image()
|
||
|
||
# 拉取基础镜像(如果不存在则自动拉取)
|
||
print_step("拉取基础镜像...")
|
||
if not image_exists(python_image):
|
||
print_info(f"Python 镜像不存在,正在拉取: {python_image}")
|
||
run_cmd(["docker", "pull", python_image])
|
||
print_info("Python 镜像拉取完成")
|
||
else:
|
||
print_info(f"Python 镜像已存在: {python_image}")
|
||
|
||
if not image_exists(mysql_image):
|
||
print_info(f"MySQL 镜像不存在,正在拉取: {mysql_image}")
|
||
run_cmd(["docker", "pull", mysql_image])
|
||
print_info("MySQL 镜像拉取完成")
|
||
else:
|
||
print_info(f"MySQL 镜像已存在: {mysql_image}")
|
||
|
||
# 构建应用镜像
|
||
print_step("构建应用镜像...")
|
||
# 使用 build 目录的 Dockerfile
|
||
dockerfile_path = BUILD_DIR / "Dockerfile"
|
||
if not dockerfile_path.exists():
|
||
print_error(f"Dockerfile 不存在: {dockerfile_path}")
|
||
sys.exit(1)
|
||
print_info(f"使用 build 目录的 Dockerfile: {dockerfile_path}")
|
||
|
||
# 构建时使用项目根目录作为上下文(因为需要复制应用代码)
|
||
os.chdir(PROJECT_ROOT)
|
||
# 但使用 build 目录的 Dockerfile
|
||
dockerfile_arg = str(dockerfile_path)
|
||
# .dockerignore 会自动使用构建上下文根目录的(即 PROJECT_ROOT/.dockerignore)
|
||
# 从 build 目录复制 .dockerignore 到根目录(临时,构建时使用)
|
||
dockerignore_build = BUILD_DIR / ".dockerignore"
|
||
dockerignore_root = PROJECT_ROOT / ".dockerignore"
|
||
if dockerignore_build.exists():
|
||
shutil.copy2(dockerignore_build, dockerignore_root)
|
||
print_info("已使用 build 目录的 .dockerignore")
|
||
run_cmd(["docker", "build", "-t", "capacity-report-app:latest", "-f", dockerfile_arg, "."])
|
||
# 清理临时复制的 .dockerignore(如果根目录原本没有)
|
||
if dockerignore_build.exists() and dockerignore_root.exists():
|
||
try:
|
||
# 如果内容相同,说明是从 build 目录复制的,可以删除
|
||
if dockerignore_build.read_text() == dockerignore_root.read_text():
|
||
dockerignore_root.unlink()
|
||
print_info("已清理临时 .dockerignore")
|
||
except OSError:
|
||
pass
|
||
|
||
# 标记 MySQL 镜像(提取版本号)
|
||
mysql_tag = mysql_image.split(':')[1]
|
||
mysql_tagged = f"capacity-mysql:{mysql_tag}"
|
||
run_cmd(["docker", "tag", mysql_image, mysql_tagged])
|
||
|
||
# 导出镜像
|
||
print_step("导出镜像...")
|
||
images_dir = TEMP_BUILD_DIR / "images"
|
||
images_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
tar_file = images_dir / "capacity-images.tar"
|
||
# 提取 MySQL 标签
|
||
mysql_tag = mysql_image.split(':')[1]
|
||
mysql_tagged = f"capacity-mysql:{mysql_tag}"
|
||
|
||
run_cmd([
|
||
"docker", "save",
|
||
"capacity-report-app:latest",
|
||
mysql_tagged,
|
||
"-o", str(tar_file)
|
||
])
|
||
print_info(f"镜像导出完成: {format_size(tar_file.stat().st_size)}")
|
||
|
||
# 复制配置文件
|
||
print_step("复制配置文件...")
|
||
# 优先从 build 目录读取,如果不存在则从根目录读取
|
||
def get_source_path(filename, subdir=None):
|
||
"""获取源文件路径,优先 build 目录,其次根目录"""
|
||
if subdir:
|
||
build_path = BUILD_DIR / subdir / filename
|
||
root_path = PROJECT_ROOT / subdir / filename
|
||
else:
|
||
build_path = BUILD_DIR / filename
|
||
root_path = PROJECT_ROOT / filename
|
||
return build_path if build_path.exists() else root_path
|
||
|
||
# Configure.json 和 ReportScript.sql 从项目根目录读取(应用配置文件)
|
||
files_to_copy = [
|
||
(PROJECT_ROOT / "Configure.json", TEMP_BUILD_DIR / "Configure.json"),
|
||
(PROJECT_ROOT / "ReportScript.sql", TEMP_BUILD_DIR / "ReportScript.sql"),
|
||
(get_source_path("docker-compose.yml"), TEMP_BUILD_DIR / "docker-compose.yml"),
|
||
(get_source_path("custom.cnf", "mysql/conf.d"), TEMP_BUILD_DIR / "mysql" / "conf.d" / "custom.cnf"),
|
||
(get_source_path("01-init-db.sql", "mysql/init"), TEMP_BUILD_DIR / "mysql" / "init" / "01-init-db.sql"),
|
||
]
|
||
|
||
for src, dst in files_to_copy:
|
||
if src.exists():
|
||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||
shutil.copy2(src, dst)
|
||
else:
|
||
print_warning(f"{src.name} 不存在: {src}")
|
||
|
||
# 更新 Configure.json
|
||
config_file = TEMP_BUILD_DIR / "Configure.json"
|
||
if config_file.exists():
|
||
with open(config_file, 'r', encoding='utf-8') as f:
|
||
config = json.load(f)
|
||
config['MySQL_DBInfo'] = {
|
||
'host': MYSQL_HOST,
|
||
'port': MYSQL_PORT,
|
||
'user': MYSQL_ROOT_USER,
|
||
'passwd': MYSQL_ROOT_PASSWORD,
|
||
'dbname': MYSQL_DATABASE
|
||
}
|
||
with open(config_file, 'w', encoding='utf-8') as f:
|
||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||
|
||
# 生成 deploy.sh
|
||
print_step("生成部署脚本...")
|
||
write_sh_script(TEMP_BUILD_DIR / "deploy.sh", get_deploy_sh())
|
||
|
||
# 打包
|
||
print_step("打包部署包...")
|
||
tar_output = DIST_DIR / "capacity-report-full.tar.gz"
|
||
DIST_DIR.mkdir(exist_ok=True)
|
||
|
||
with tarfile.open(tar_output, 'w:gz') as tar:
|
||
tar.add(TEMP_BUILD_DIR, arcname='.')
|
||
|
||
print_info(f"部署包已生成: {tar_output}")
|
||
print_info(f"文件大小: {format_size(tar_output.stat().st_size)}")
|
||
|
||
# 清理临时文件和缓存
|
||
print_step("清理临时文件...")
|
||
if TEMP_BUILD_DIR.exists():
|
||
shutil.rmtree(TEMP_BUILD_DIR, ignore_errors=True)
|
||
print_info("已清理临时构建目录")
|
||
|
||
return tar_output
|
||
|
||
def build_update_package():
|
||
"""构建更新包"""
|
||
print_step("构建更新包...")
|
||
|
||
# 清理临时目录
|
||
if TEMP_BUILD_DIR.exists():
|
||
shutil.rmtree(TEMP_BUILD_DIR)
|
||
TEMP_BUILD_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 检查 Docker
|
||
if not check_command("docker"):
|
||
print_error("未检测到 Docker")
|
||
sys.exit(1)
|
||
|
||
# 检查并获取符合要求的镜像
|
||
print_step("检查镜像版本...")
|
||
python_image = check_and_get_python_image()
|
||
|
||
# 拉取基础镜像(如果不存在则自动拉取)
|
||
print_step("拉取基础镜像...")
|
||
if not image_exists(python_image):
|
||
print_info(f"Python 镜像不存在,正在拉取: {python_image}")
|
||
run_cmd(["docker", "pull", python_image])
|
||
print_info("Python 镜像拉取完成")
|
||
else:
|
||
print_info(f"Python 镜像已存在: {python_image}")
|
||
|
||
# 构建应用镜像
|
||
print_step("构建应用镜像...")
|
||
# 使用 build 目录的 Dockerfile
|
||
dockerfile_path = BUILD_DIR / "Dockerfile"
|
||
if not dockerfile_path.exists():
|
||
print_error(f"Dockerfile 不存在: {dockerfile_path}")
|
||
sys.exit(1)
|
||
print_info(f"使用 build 目录的 Dockerfile: {dockerfile_path}")
|
||
|
||
# 构建时使用项目根目录作为上下文(因为需要复制应用代码)
|
||
os.chdir(PROJECT_ROOT)
|
||
# 但使用 build 目录的 Dockerfile
|
||
dockerfile_arg = str(dockerfile_path)
|
||
# .dockerignore 会自动使用构建上下文根目录的(即 PROJECT_ROOT/.dockerignore)
|
||
# 从 build 目录复制 .dockerignore 到根目录(临时,构建时使用)
|
||
dockerignore_build = BUILD_DIR / ".dockerignore"
|
||
dockerignore_root = PROJECT_ROOT / ".dockerignore"
|
||
if dockerignore_build.exists():
|
||
shutil.copy2(dockerignore_build, dockerignore_root)
|
||
print_info("已使用 build 目录的 .dockerignore")
|
||
run_cmd(["docker", "build", "-t", "capacity-report-app:latest", "-f", dockerfile_arg, "."])
|
||
# 清理临时复制的 .dockerignore(如果根目录原本没有)
|
||
if dockerignore_build.exists() and dockerignore_root.exists():
|
||
try:
|
||
# 如果内容相同,说明是从 build 目录复制的,可以删除
|
||
if dockerignore_build.read_text() == dockerignore_root.read_text():
|
||
dockerignore_root.unlink()
|
||
print_info("已清理临时 .dockerignore")
|
||
except OSError:
|
||
pass
|
||
|
||
# 导出镜像
|
||
print_step("导出镜像...")
|
||
images_dir = TEMP_BUILD_DIR / "images"
|
||
images_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
tar_file = images_dir / "capacity-app-update.tar"
|
||
run_cmd([
|
||
"docker", "save",
|
||
"capacity-report-app:latest",
|
||
"-o", str(tar_file)
|
||
])
|
||
print_info(f"镜像导出完成: {format_size(tar_file.stat().st_size)}")
|
||
|
||
# 复制配置文件
|
||
print_step("复制配置文件...")
|
||
# 从 build 目录读取配置文件
|
||
def get_source_path(filename, subdir=None):
|
||
"""获取源文件路径,从 build 目录读取"""
|
||
if subdir:
|
||
return BUILD_DIR / subdir / filename
|
||
else:
|
||
return BUILD_DIR / filename
|
||
|
||
files_to_copy = [
|
||
(get_source_path("docker-compose.yml"), TEMP_BUILD_DIR / "docker-compose.yml"),
|
||
]
|
||
|
||
for src, dst in files_to_copy:
|
||
if src.exists():
|
||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||
shutil.copy2(src, dst)
|
||
|
||
# 生成 update.sh
|
||
print_step("生成更新脚本...")
|
||
write_sh_script(TEMP_BUILD_DIR / "update.sh", get_update_sh())
|
||
|
||
# 打包
|
||
print_step("打包更新包...")
|
||
tar_output = DIST_DIR / "capacity-report-update.tar.gz"
|
||
DIST_DIR.mkdir(exist_ok=True)
|
||
|
||
with tarfile.open(tar_output, 'w:gz') as tar:
|
||
tar.add(TEMP_BUILD_DIR, arcname='.')
|
||
|
||
print_info(f"更新包已生成: {tar_output}")
|
||
print_info(f"文件大小: {format_size(tar_output.stat().st_size)}")
|
||
|
||
# 清理临时文件和缓存
|
||
print_step("清理临时文件...")
|
||
if TEMP_BUILD_DIR.exists():
|
||
shutil.rmtree(TEMP_BUILD_DIR, ignore_errors=True)
|
||
print_info("已清理临时构建目录")
|
||
|
||
return tar_output
|
||
|
||
def main():
|
||
print("=" * 60)
|
||
print("CapacityReport 统一构建脚本")
|
||
print("=" * 60)
|
||
print()
|
||
|
||
print_choice("请选择构建类型:")
|
||
print(" 1. 完整部署包 (包含 MySQL + 应用)")
|
||
print(" 2. 更新包 (仅应用)")
|
||
print()
|
||
|
||
while True:
|
||
choice = input("请输入选项 (1/2): ").strip()
|
||
if choice in ['1', '2']:
|
||
break
|
||
print_error("无效选项,请输入 1 或 2")
|
||
|
||
print()
|
||
|
||
if choice == '1':
|
||
tar_file = build_full_package()
|
||
print()
|
||
print("=" * 60)
|
||
print("构建完成!")
|
||
print("=" * 60)
|
||
print()
|
||
print(f"部署包位置: {tar_file}")
|
||
print()
|
||
print("使用方法:")
|
||
print("1. 将 tar.gz 文件传输到目标 Linux 机器")
|
||
print("2. 解压: tar -xzf capacity-report-full.tar.gz")
|
||
print("3. 进入解压目录,执行: sh deploy.sh")
|
||
print()
|
||
else:
|
||
tar_file = build_update_package()
|
||
print()
|
||
print("=" * 60)
|
||
print("构建完成!")
|
||
print("=" * 60)
|
||
print()
|
||
print(f"更新包位置: {tar_file}")
|
||
print()
|
||
print("使用方法:")
|
||
print("1. 将 tar.gz 文件传输到目标 Linux 机器")
|
||
print("2. 解压: tar -xzf capacity-report-update.tar.gz")
|
||
print("3. 进入解压目录,执行: sh update.sh")
|
||
print()
|
||
print("注意: 更新包仅用于更新已部署的应用,不包含 MySQL")
|
||
print()
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
main()
|
||
except KeyboardInterrupt:
|
||
print("\n\n构建已取消")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print_error(f"构建失败: {e}")
|
||
traceback.print_exc()
|
||
sys.exit(1)
|