692 lines
27 KiB
Python
692 lines
27 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Windows Package Builder for Pixelle-Video
|
|
|
|
This script automates the creation of a Windows portable package:
|
|
1. Downloads Python embedded distribution
|
|
2. Downloads FFmpeg portable
|
|
3. Prepares Python environment (enable site-packages, install pip)
|
|
4. Installs project dependencies
|
|
5. Copies project files
|
|
6. Generates launcher scripts
|
|
7. Creates final ZIP package
|
|
|
|
Usage:
|
|
python build.py [--config CONFIG] [--output OUTPUT] [--cn-mirror]
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import zipfile
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from urllib.request import urlretrieve
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
print("ERROR: PyYAML is required. Install it with: pip install pyyaml")
|
|
sys.exit(1)
|
|
|
|
|
|
class Color:
|
|
"""ANSI color codes for terminal output"""
|
|
HEADER = '\033[95m'
|
|
BLUE = '\033[94m'
|
|
CYAN = '\033[96m'
|
|
GREEN = '\033[92m'
|
|
YELLOW = '\033[93m'
|
|
RED = '\033[91m'
|
|
RESET = '\033[0m'
|
|
BOLD = '\033[1m'
|
|
|
|
|
|
class WindowsPackageBuilder:
|
|
"""Build Windows portable package for Pixelle-Video"""
|
|
|
|
def __init__(self, config_path: str, output_dir: Optional[str] = None, use_cn_mirror: bool = False):
|
|
self.config_path = Path(config_path)
|
|
self.script_dir = Path(__file__).parent
|
|
self.project_root = self.script_dir.parent.parent
|
|
|
|
# Load configuration
|
|
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
self.config = yaml.safe_load(f)
|
|
|
|
# Override mirror setting if specified
|
|
if use_cn_mirror:
|
|
self.config['mirrors']['use_cn_mirror'] = True
|
|
|
|
# Setup paths
|
|
self.output_dir = Path(output_dir) if output_dir else self.project_root / self.config['build']['output_dir']
|
|
self.cache_dir = self.project_root / self.config['cache']['cache_dir']
|
|
self.templates_dir = self.script_dir / 'templates'
|
|
|
|
# Get version from pyproject.toml
|
|
self.version = self._read_version()
|
|
self.package_name = f"{self.config['package']['name']}-v{self.version}-{self.config['package']['architecture']}"
|
|
self.build_dir = self.output_dir / self.package_name
|
|
|
|
def _read_version(self) -> str:
|
|
"""Read version from pyproject.toml"""
|
|
pyproject_path = self.project_root / 'pyproject.toml'
|
|
try:
|
|
import tomllib
|
|
except ImportError:
|
|
# Python < 3.11 fallback
|
|
try:
|
|
import tomli as tomllib
|
|
except ImportError:
|
|
# Simple regex fallback
|
|
import re
|
|
with open(pyproject_path, 'r') as f:
|
|
content = f.read()
|
|
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
|
if match:
|
|
return match.group(1)
|
|
return "0.1.0"
|
|
|
|
with open(pyproject_path, 'rb') as f:
|
|
pyproject = tomllib.load(f)
|
|
return pyproject.get('project', {}).get('version', '0.1.0')
|
|
|
|
def log(self, message: str, level: str = "INFO"):
|
|
"""Print colored log message"""
|
|
colors = {
|
|
"INFO": Color.BLUE,
|
|
"SUCCESS": Color.GREEN,
|
|
"WARNING": Color.YELLOW,
|
|
"ERROR": Color.RED,
|
|
"HEADER": Color.HEADER,
|
|
}
|
|
color = colors.get(level, Color.RESET)
|
|
print(f"{color}[{level}]{Color.RESET} {message}")
|
|
|
|
def download_file(self, url: str, output_path: Path, description: str = "", max_retries: int = 3) -> bool:
|
|
"""Download file with progress indication and retry support"""
|
|
import ssl
|
|
import urllib.request
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
if attempt > 0:
|
|
self.log(f"Retry {attempt}/{max_retries}...")
|
|
|
|
self.log(f"Downloading {description or url}...")
|
|
|
|
# Create SSL context that's more lenient
|
|
ssl_context = ssl.create_default_context()
|
|
ssl_context.check_hostname = False
|
|
ssl_context.verify_mode = ssl.CERT_NONE
|
|
|
|
def report_progress(block_num, block_size, total_size):
|
|
downloaded = block_num * block_size
|
|
percent = min(downloaded / total_size * 100, 100) if total_size > 0 else 0
|
|
print(f"\r Progress: {percent:.1f}%", end='', flush=True)
|
|
|
|
# Try with urllib first
|
|
opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ssl_context))
|
|
urllib.request.install_opener(opener)
|
|
urlretrieve(url, output_path, reporthook=report_progress)
|
|
print() # New line after progress
|
|
self.log(f"Downloaded to {output_path}", "SUCCESS")
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.log(f"Download attempt {attempt + 1} failed: {e}", "WARNING")
|
|
if attempt < max_retries - 1:
|
|
import time
|
|
time.sleep(2) # Wait before retry
|
|
else:
|
|
self.log(f"All download attempts failed", "ERROR")
|
|
# Try with curl as fallback
|
|
return self._download_with_curl(url, output_path, description)
|
|
|
|
return False
|
|
|
|
def _find_suitable_python(self) -> Optional[str]:
|
|
"""Find a suitable Python 3.11+ for installing dependencies"""
|
|
candidates = [
|
|
# Try common locations for newer Python versions
|
|
'/Users/puke/miniforge3/bin/python3', # User's conda
|
|
'/opt/homebrew/bin/python3', # Homebrew
|
|
'/usr/local/bin/python3', # Manual install
|
|
]
|
|
|
|
# Also check what's in PATH
|
|
for i in range(11, 14): # Python 3.11, 3.12, 3.13
|
|
for py_name in [f'python3.{i}', f'python{i}']:
|
|
found = shutil.which(py_name)
|
|
if found and found not in candidates:
|
|
candidates.append(found)
|
|
|
|
# Check generic python3
|
|
python3_path = shutil.which('python3')
|
|
if python3_path and '.venv' not in python3_path:
|
|
candidates.append(python3_path)
|
|
|
|
# Test each candidate
|
|
for candidate in candidates:
|
|
try:
|
|
if not candidate:
|
|
continue
|
|
|
|
# Skip if in project venv
|
|
if '.venv' in candidate or 'venv' in candidate:
|
|
continue
|
|
|
|
# Check if path exists
|
|
if not os.path.exists(candidate):
|
|
continue
|
|
|
|
# Check Python version
|
|
result = subprocess.run(
|
|
[candidate, '-c', 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")'],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
version = result.stdout.strip()
|
|
major, minor = map(int, version.split('.'))
|
|
|
|
# Need Python 3.11+
|
|
if major == 3 and minor >= 11:
|
|
# Check if pip is available
|
|
pip_check = subprocess.run(
|
|
[candidate, '-m', 'pip', '--version'],
|
|
capture_output=True,
|
|
timeout=5
|
|
)
|
|
if pip_check.returncode == 0:
|
|
self.log(f"Found Python {version} at {candidate}", "SUCCESS")
|
|
return candidate
|
|
except Exception as e:
|
|
continue
|
|
|
|
return None
|
|
|
|
def _download_with_curl(self, url: str, output_path: Path, description: str = "") -> bool:
|
|
"""Fallback download method using curl"""
|
|
try:
|
|
self.log(f"Trying curl fallback for {description}...")
|
|
result = subprocess.run(
|
|
['curl', '-L', '-o', str(output_path), url, '--progress-bar'],
|
|
check=True,
|
|
capture_output=False
|
|
)
|
|
if result.returncode == 0 and output_path.exists():
|
|
self.log(f"Downloaded with curl to {output_path}", "SUCCESS")
|
|
return True
|
|
except Exception as e:
|
|
self.log(f"Curl download also failed: {e}", "ERROR")
|
|
return False
|
|
|
|
def download_python(self) -> Path:
|
|
"""Download Python embedded distribution"""
|
|
python_config = self.config['python']
|
|
cache_file = self.cache_dir / f"python-{python_config['version']}-embed-amd64.zip"
|
|
|
|
if cache_file.exists():
|
|
self.log(f"Using cached Python: {cache_file}")
|
|
return cache_file
|
|
|
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Choose URL based on mirror setting
|
|
url = python_config['mirror_url'] if self.config['mirrors']['use_cn_mirror'] else python_config['download_url']
|
|
|
|
if self.download_file(url, cache_file, f"Python {python_config['version']}"):
|
|
return cache_file
|
|
else:
|
|
raise RuntimeError("Failed to download Python")
|
|
|
|
def download_ffmpeg(self) -> Path:
|
|
"""Download FFmpeg portable"""
|
|
ffmpeg_config = self.config['ffmpeg']
|
|
cache_file = self.cache_dir / f"ffmpeg-{ffmpeg_config['version']}-win64.zip"
|
|
|
|
if cache_file.exists():
|
|
self.log(f"Using cached FFmpeg: {cache_file}")
|
|
return cache_file
|
|
|
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
url = ffmpeg_config['mirror_url'] if self.config['mirrors']['use_cn_mirror'] else ffmpeg_config['download_url']
|
|
|
|
if self.download_file(url, cache_file, f"FFmpeg {ffmpeg_config['version']}"):
|
|
return cache_file
|
|
else:
|
|
raise RuntimeError("Failed to download FFmpeg")
|
|
|
|
def extract_python(self, zip_path: Path, target_dir: Path):
|
|
"""Extract Python embedded distribution"""
|
|
self.log(f"Extracting Python to {target_dir}...")
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
|
zip_ref.extractall(target_dir)
|
|
|
|
# Add execute permissions to .exe files (needed on Unix systems)
|
|
if os.name != 'nt': # Not on Windows
|
|
for exe_file in target_dir.glob('*.exe'):
|
|
os.chmod(exe_file, 0o755)
|
|
for exe_file in target_dir.glob('**/*.exe'):
|
|
os.chmod(exe_file, 0o755)
|
|
|
|
self.log("Python extracted successfully", "SUCCESS")
|
|
|
|
def extract_ffmpeg(self, zip_path: Path, target_dir: Path):
|
|
"""Extract FFmpeg portable"""
|
|
self.log(f"Extracting FFmpeg to {target_dir}...")
|
|
temp_extract = target_dir.parent / "ffmpeg_temp"
|
|
temp_extract.mkdir(parents=True, exist_ok=True)
|
|
|
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
|
zip_ref.extractall(temp_extract)
|
|
|
|
# Find the bin directory (FFmpeg archive has nested structure)
|
|
bin_dir = None
|
|
for root, dirs, files in os.walk(temp_extract):
|
|
if 'bin' in dirs:
|
|
bin_dir = Path(root) / 'bin'
|
|
break
|
|
|
|
if bin_dir and bin_dir.exists():
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
shutil.copytree(bin_dir, target_dir, dirs_exist_ok=True)
|
|
shutil.rmtree(temp_extract)
|
|
self.log("FFmpeg extracted successfully", "SUCCESS")
|
|
else:
|
|
raise RuntimeError("FFmpeg bin directory not found in archive")
|
|
|
|
def prepare_python_environment(self, python_dir: Path):
|
|
"""Prepare Python environment: enable site-packages"""
|
|
self.log("Preparing Python environment...")
|
|
|
|
# Modify python311._pth to enable site-packages
|
|
pth_file = python_dir / "python311._pth"
|
|
if pth_file.exists():
|
|
with open(pth_file, 'r') as f:
|
|
lines = f.readlines()
|
|
|
|
# Uncomment "import site" line or add it
|
|
modified = False
|
|
for i, line in enumerate(lines):
|
|
if line.strip().startswith('#import site'):
|
|
lines[i] = 'import site\n'
|
|
modified = True
|
|
break
|
|
|
|
if not modified and 'import site' not in ''.join(lines):
|
|
lines.append('import site\n')
|
|
|
|
with open(pth_file, 'w') as f:
|
|
f.writelines(lines)
|
|
|
|
self.log("Enabled site-packages in Python", "SUCCESS")
|
|
|
|
# Note: On non-Windows systems, we can't run python.exe directly
|
|
# Pip and dependencies will be installed using system Python
|
|
if os.name == 'nt':
|
|
# On Windows, we can install pip directly
|
|
python_exe = python_dir / "python.exe"
|
|
get_pip_path = self.cache_dir / "get-pip.py"
|
|
|
|
if not get_pip_path.exists():
|
|
self.log("Downloading get-pip.py...")
|
|
pip_url = "https://bootstrap.pypa.io/get-pip.py"
|
|
self.download_file(pip_url, get_pip_path, "get-pip.py")
|
|
|
|
self.log("Installing pip...")
|
|
result = subprocess.run(
|
|
[str(python_exe), str(get_pip_path)],
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
self.log("Pip installed successfully", "SUCCESS")
|
|
else:
|
|
self.log(f"Pip installation warning: {result.stderr}", "WARNING")
|
|
else:
|
|
self.log("Cross-platform build detected (building on non-Windows)", "INFO")
|
|
self.log("Dependencies will be installed using system Python", "INFO")
|
|
|
|
def install_dependencies(self, python_dir: Path):
|
|
"""Install project dependencies"""
|
|
self.log("Installing project dependencies...")
|
|
|
|
# Determine target directory for site-packages
|
|
site_packages = python_dir / "Lib" / "site-packages"
|
|
site_packages.mkdir(parents=True, exist_ok=True)
|
|
|
|
if os.name == 'nt':
|
|
# On Windows, use the embedded Python
|
|
python_exe = python_dir / "python.exe"
|
|
|
|
# Install uv first if configured
|
|
if self.config['build'].get('use_uv', True):
|
|
self.log("Installing uv...")
|
|
subprocess.run(
|
|
[str(python_exe), "-m", "pip", "install", "uv"],
|
|
check=True
|
|
)
|
|
|
|
# Install dependencies
|
|
if self.config['build'].get('use_uv', True):
|
|
cmd = [str(python_exe), "-m", "uv", "pip", "install", "-e", str(self.project_root)]
|
|
if self.config['mirrors']['use_cn_mirror']:
|
|
cmd.extend(["--index-url", self.config['mirrors']['pypi_mirror']])
|
|
else:
|
|
cmd = [str(python_exe), "-m", "pip", "install", "-e", str(self.project_root)]
|
|
if self.config['mirrors']['use_cn_mirror']:
|
|
cmd.extend(["--index-url", self.config['mirrors']['pypi_mirror']])
|
|
|
|
self.log(f"Running: {' '.join(cmd)}")
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
if result.returncode == 0:
|
|
self.log("Dependencies installed successfully", "SUCCESS")
|
|
else:
|
|
self.log(f"Dependency installation failed:\n{result.stderr}", "ERROR")
|
|
raise RuntimeError("Failed to install dependencies")
|
|
else:
|
|
# Cross-platform build: use system Python to install to target directory
|
|
self.log("Cross-platform build: using system Python to install dependencies")
|
|
|
|
# Find a Python 3.11+ executable (not from project venv)
|
|
python_cmd = self._find_suitable_python()
|
|
|
|
if not python_cmd:
|
|
self.log("No suitable Python 3.11+ found. Please install Python 3.11+ or use Windows to build.", "ERROR")
|
|
raise RuntimeError("Python 3.11+ required for cross-platform build")
|
|
|
|
self.log(f"Using Python: {python_cmd}")
|
|
|
|
# Use pip with --target to install to specific directory
|
|
cmd = [
|
|
python_cmd, "-m", "pip", "install",
|
|
"--target", str(site_packages),
|
|
"--no-user",
|
|
"--no-warn-script-location"
|
|
]
|
|
|
|
# Read dependencies from pyproject.toml
|
|
try:
|
|
import tomllib
|
|
except ImportError:
|
|
try:
|
|
import tomli as tomllib
|
|
except ImportError:
|
|
self.log("tomllib/tomli not available, trying simple parsing", "WARNING")
|
|
tomllib = None
|
|
|
|
if tomllib:
|
|
pyproject_path = self.project_root / "pyproject.toml"
|
|
with open(pyproject_path, 'rb') as f:
|
|
pyproject = tomllib.load(f)
|
|
deps = pyproject.get('project', {}).get('dependencies', [])
|
|
else:
|
|
# Simple fallback: read from pyproject.toml manually
|
|
import re
|
|
pyproject_path = self.project_root / "pyproject.toml"
|
|
with open(pyproject_path, 'r') as f:
|
|
content = f.read()
|
|
# Find dependencies section
|
|
deps_match = re.search(r'dependencies\s*=\s*\[(.*?)\]', content, re.DOTALL)
|
|
if deps_match:
|
|
deps_str = deps_match.group(1)
|
|
deps = [dep.strip(' "\',\n') for dep in deps_str.split('\n') if dep.strip() and not dep.strip().startswith('#')]
|
|
else:
|
|
deps = []
|
|
|
|
if deps:
|
|
cmd.extend(deps)
|
|
|
|
if self.config['mirrors']['use_cn_mirror']:
|
|
cmd.extend(["--index-url", self.config['mirrors']['pypi_mirror']])
|
|
|
|
self.log(f"Installing {len(deps)} dependencies...")
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
if result.returncode == 0:
|
|
self.log("Dependencies installed successfully", "SUCCESS")
|
|
else:
|
|
self.log(f"Dependency installation output:\n{result.stdout}", "INFO")
|
|
if result.stderr:
|
|
self.log(f"Warnings: {result.stderr}", "WARNING")
|
|
else:
|
|
self.log("No dependencies found in pyproject.toml", "WARNING")
|
|
|
|
def copy_project_files(self, target_dir: Path):
|
|
"""Copy project files to build directory"""
|
|
self.log(f"Copying project files to {target_dir}...")
|
|
|
|
exclude_patterns = self.config['build']['exclude_patterns']
|
|
|
|
def should_exclude(path: Path) -> bool:
|
|
path_str = str(path.relative_to(self.project_root))
|
|
for pattern in exclude_patterns:
|
|
if pattern.endswith('/*'):
|
|
# Directory content exclusion - must match exact directory name or start with "dirname/"
|
|
dir_name = pattern[:-2]
|
|
if path_str == dir_name or path_str.startswith(f"{dir_name}/"):
|
|
return True
|
|
elif pattern.endswith('*'):
|
|
# Wildcard pattern
|
|
if path_str.startswith(pattern[:-1]):
|
|
return True
|
|
elif '*' in pattern:
|
|
# Glob pattern (simple check)
|
|
import fnmatch
|
|
if fnmatch.fnmatch(path_str, pattern):
|
|
return True
|
|
else:
|
|
# Exact match or directory
|
|
if path_str == pattern or path_str.startswith(f"{pattern}/"):
|
|
return True
|
|
return False
|
|
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Copy files
|
|
copied_count = 0
|
|
for item in self.project_root.iterdir():
|
|
if item.name in ['.git', 'packaging', 'dist', '.venv', 'venv']:
|
|
continue
|
|
|
|
if should_exclude(item):
|
|
continue
|
|
|
|
target_path = target_dir / item.name
|
|
|
|
if item.is_file():
|
|
shutil.copy2(item, target_path)
|
|
copied_count += 1
|
|
elif item.is_dir():
|
|
shutil.copytree(item, target_path, ignore=lambda d, names: [
|
|
n for n in names if should_exclude(Path(d) / n)
|
|
])
|
|
# Count files in copied directory
|
|
copied_count += sum(1 for _ in target_path.rglob('*') if _.is_file())
|
|
|
|
self.log(f"Copied {copied_count} files", "SUCCESS")
|
|
|
|
def generate_launcher_scripts(self):
|
|
"""Generate launcher scripts from templates"""
|
|
self.log("Generating launcher scripts...")
|
|
|
|
replacements = {
|
|
'{VERSION}': self.version,
|
|
'{BUILD_DATE}': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
|
}
|
|
|
|
# Copy and process templates
|
|
for template_file in self.templates_dir.glob('*'):
|
|
if template_file.is_file():
|
|
target_file = self.build_dir / template_file.name
|
|
|
|
with open(template_file, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# Replace placeholders
|
|
for key, value in replacements.items():
|
|
content = content.replace(key, value)
|
|
|
|
with open(target_file, 'w', encoding='utf-8', newline='\r\n') as f:
|
|
f.write(content)
|
|
|
|
self.log(f"Generated: {template_file.name}")
|
|
|
|
self.log("Launcher scripts generated", "SUCCESS")
|
|
|
|
def create_empty_directories(self):
|
|
"""Create empty directories specified in config"""
|
|
self.log("Creating empty directories...")
|
|
|
|
for dir_name in self.config['build'].get('create_empty_dirs', []):
|
|
dir_path = self.build_dir / dir_name
|
|
dir_path.mkdir(parents=True, exist_ok=True)
|
|
# Create .gitkeep to preserve directory in git
|
|
(dir_path / '.gitkeep').touch()
|
|
|
|
self.log("Empty directories created", "SUCCESS")
|
|
|
|
def create_zip_package(self):
|
|
"""Create final ZIP package"""
|
|
if not self.config['build'].get('create_zip', True):
|
|
return
|
|
|
|
zip_path = self.output_dir / f"{self.package_name}.zip"
|
|
self.log(f"Creating ZIP package: {zip_path}...")
|
|
|
|
compression_map = {
|
|
'deflate': zipfile.ZIP_DEFLATED,
|
|
'bzip2': zipfile.ZIP_BZIP2,
|
|
'lzma': zipfile.ZIP_LZMA,
|
|
}
|
|
compression = compression_map.get(
|
|
self.config['build'].get('zip_compression', 'deflate'),
|
|
zipfile.ZIP_DEFLATED
|
|
)
|
|
|
|
with zipfile.ZipFile(zip_path, 'w', compression) as zipf:
|
|
for root, dirs, files in os.walk(self.build_dir):
|
|
for file in files:
|
|
file_path = Path(root) / file
|
|
arcname = file_path.relative_to(self.build_dir.parent)
|
|
zipf.write(file_path, arcname)
|
|
|
|
# Calculate file size and hash
|
|
size_mb = zip_path.stat().st_size / (1024 * 1024)
|
|
|
|
with open(zip_path, 'rb') as f:
|
|
file_hash = hashlib.sha256(f.read()).hexdigest()
|
|
|
|
self.log(f"ZIP package created: {zip_path}", "SUCCESS")
|
|
self.log(f"Size: {size_mb:.2f} MB")
|
|
self.log(f"SHA256: {file_hash}")
|
|
|
|
# Write hash to file
|
|
hash_file = zip_path.with_suffix('.zip.sha256')
|
|
with open(hash_file, 'w') as f:
|
|
f.write(f"{file_hash} {zip_path.name}\n")
|
|
|
|
def build(self):
|
|
"""Main build process"""
|
|
self.log("=" * 60, "HEADER")
|
|
self.log(f"Building {self.package_name}", "HEADER")
|
|
self.log("=" * 60, "HEADER")
|
|
|
|
try:
|
|
# Clean build directory
|
|
if self.build_dir.exists():
|
|
self.log(f"Cleaning existing build directory: {self.build_dir}")
|
|
shutil.rmtree(self.build_dir)
|
|
|
|
self.build_dir.mkdir(parents=True, exist_ok=True)
|
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Download dependencies
|
|
python_zip = self.download_python()
|
|
ffmpeg_zip = self.download_ffmpeg()
|
|
|
|
# Extract Python
|
|
python_dir = self.build_dir / "python" / "python311"
|
|
self.extract_python(python_zip, python_dir)
|
|
|
|
# Extract FFmpeg
|
|
ffmpeg_dir = self.build_dir / "tools" / "ffmpeg" / "bin"
|
|
self.extract_ffmpeg(ffmpeg_zip, ffmpeg_dir)
|
|
|
|
# Prepare Python environment
|
|
self.prepare_python_environment(python_dir)
|
|
|
|
# Install dependencies
|
|
if self.config['build'].get('pre_install_deps', True):
|
|
self.install_dependencies(python_dir)
|
|
|
|
# Copy project files
|
|
project_target = self.build_dir / "Pixelle-Video"
|
|
self.copy_project_files(project_target)
|
|
|
|
# Generate launcher scripts
|
|
self.generate_launcher_scripts()
|
|
|
|
# Create empty directories
|
|
self.create_empty_directories()
|
|
|
|
# Create ZIP package
|
|
self.create_zip_package()
|
|
|
|
self.log("=" * 60, "HEADER")
|
|
self.log("Build completed successfully!", "SUCCESS")
|
|
self.log(f"Package location: {self.build_dir}", "SUCCESS")
|
|
self.log("=" * 60, "HEADER")
|
|
|
|
except Exception as e:
|
|
self.log(f"Build failed: {e}", "ERROR")
|
|
import traceback
|
|
traceback.print_exc()
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Build Windows portable package for Pixelle-Video")
|
|
parser.add_argument(
|
|
'--config',
|
|
default='packaging/windows/config/build_config.yaml',
|
|
help='Path to build configuration file'
|
|
)
|
|
parser.add_argument(
|
|
'--output',
|
|
help='Output directory (default: dist/windows)'
|
|
)
|
|
parser.add_argument(
|
|
'--cn-mirror',
|
|
action='store_true',
|
|
help='Use China mirrors for faster downloads'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
builder = WindowsPackageBuilder(
|
|
config_path=args.config,
|
|
output_dir=args.output,
|
|
use_cn_mirror=args.cn_mirror
|
|
)
|
|
builder.build()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|