build.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. #!/usr/bin/env python3
  2. """
  3. Windows Package Builder for Pixelle-Video
  4. This script automates the creation of a Windows portable package:
  5. 1. Downloads Python embedded distribution
  6. 2. Downloads FFmpeg portable
  7. 3. Prepares Python environment (enable site-packages, install pip)
  8. 4. Installs project dependencies
  9. 5. Copies project files
  10. 6. Generates launcher scripts
  11. 7. Creates final ZIP package
  12. Usage:
  13. python build.py [--config CONFIG] [--output OUTPUT] [--cn-mirror]
  14. """
  15. import argparse
  16. import hashlib
  17. import os
  18. import shutil
  19. import subprocess
  20. import sys
  21. import tempfile
  22. import zipfile
  23. from datetime import datetime
  24. from pathlib import Path
  25. from typing import Optional
  26. from urllib.request import urlretrieve
  27. try:
  28. import yaml
  29. except ImportError:
  30. print("ERROR: PyYAML is required. Install it with: pip install pyyaml")
  31. sys.exit(1)
  32. class Color:
  33. """ANSI color codes for terminal output"""
  34. HEADER = '\033[95m'
  35. BLUE = '\033[94m'
  36. CYAN = '\033[96m'
  37. GREEN = '\033[92m'
  38. YELLOW = '\033[93m'
  39. RED = '\033[91m'
  40. RESET = '\033[0m'
  41. BOLD = '\033[1m'
  42. class WindowsPackageBuilder:
  43. """Build Windows portable package for Pixelle-Video"""
  44. def __init__(self, config_path: str, output_dir: Optional[str] = None, use_cn_mirror: bool = False):
  45. self.config_path = Path(config_path)
  46. self.script_dir = Path(__file__).parent
  47. self.project_root = self.script_dir.parent.parent
  48. # Load configuration
  49. with open(self.config_path, 'r', encoding='utf-8') as f:
  50. self.config = yaml.safe_load(f)
  51. # Override mirror setting if specified
  52. if use_cn_mirror:
  53. self.config['mirrors']['use_cn_mirror'] = True
  54. # Setup paths
  55. self.output_dir = Path(output_dir) if output_dir else self.project_root / self.config['build']['output_dir']
  56. self.cache_dir = self.project_root / self.config['cache']['cache_dir']
  57. self.templates_dir = self.script_dir / 'templates'
  58. # Get version from pyproject.toml
  59. self.version = self._read_version()
  60. self.package_name = f"{self.config['package']['name']}-v{self.version}-{self.config['package']['architecture']}"
  61. self.build_dir = self.output_dir / self.package_name
  62. def _read_version(self) -> str:
  63. """Read version from pyproject.toml"""
  64. pyproject_path = self.project_root / 'pyproject.toml'
  65. try:
  66. import tomllib
  67. except ImportError:
  68. # Python < 3.11 fallback
  69. try:
  70. import tomli as tomllib
  71. except ImportError:
  72. # Simple regex fallback
  73. import re
  74. with open(pyproject_path, 'r') as f:
  75. content = f.read()
  76. match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
  77. if match:
  78. return match.group(1)
  79. return "0.1.0"
  80. with open(pyproject_path, 'rb') as f:
  81. pyproject = tomllib.load(f)
  82. return pyproject.get('project', {}).get('version', '0.1.0')
  83. def log(self, message: str, level: str = "INFO"):
  84. """Print colored log message"""
  85. colors = {
  86. "INFO": Color.BLUE,
  87. "SUCCESS": Color.GREEN,
  88. "WARNING": Color.YELLOW,
  89. "ERROR": Color.RED,
  90. "HEADER": Color.HEADER,
  91. }
  92. color = colors.get(level, Color.RESET)
  93. print(f"{color}[{level}]{Color.RESET} {message}")
  94. def download_file(self, url: str, output_path: Path, description: str = "", max_retries: int = 3) -> bool:
  95. """Download file with progress indication and retry support"""
  96. import ssl
  97. import urllib.request
  98. for attempt in range(max_retries):
  99. try:
  100. if attempt > 0:
  101. self.log(f"Retry {attempt}/{max_retries}...")
  102. self.log(f"Downloading {description or url}...")
  103. # Create SSL context that's more lenient
  104. ssl_context = ssl.create_default_context()
  105. ssl_context.check_hostname = False
  106. ssl_context.verify_mode = ssl.CERT_NONE
  107. def report_progress(block_num, block_size, total_size):
  108. downloaded = block_num * block_size
  109. percent = min(downloaded / total_size * 100, 100) if total_size > 0 else 0
  110. print(f"\r Progress: {percent:.1f}%", end='', flush=True)
  111. # Try with urllib first
  112. opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ssl_context))
  113. urllib.request.install_opener(opener)
  114. urlretrieve(url, output_path, reporthook=report_progress)
  115. print() # New line after progress
  116. self.log(f"Downloaded to {output_path}", "SUCCESS")
  117. return True
  118. except Exception as e:
  119. self.log(f"Download attempt {attempt + 1} failed: {e}", "WARNING")
  120. if attempt < max_retries - 1:
  121. import time
  122. time.sleep(2) # Wait before retry
  123. else:
  124. self.log(f"All download attempts failed", "ERROR")
  125. # Try with curl as fallback
  126. return self._download_with_curl(url, output_path, description)
  127. return False
  128. def _find_suitable_python(self) -> Optional[str]:
  129. """Find a suitable Python 3.11+ for installing dependencies"""
  130. candidates = [
  131. # Try common locations for newer Python versions
  132. '/Users/puke/miniforge3/bin/python3', # User's conda
  133. '/opt/homebrew/bin/python3', # Homebrew
  134. '/usr/local/bin/python3', # Manual install
  135. ]
  136. # Also check what's in PATH
  137. for i in range(11, 14): # Python 3.11, 3.12, 3.13
  138. for py_name in [f'python3.{i}', f'python{i}']:
  139. found = shutil.which(py_name)
  140. if found and found not in candidates:
  141. candidates.append(found)
  142. # Check generic python3
  143. python3_path = shutil.which('python3')
  144. if python3_path and '.venv' not in python3_path:
  145. candidates.append(python3_path)
  146. # Test each candidate
  147. for candidate in candidates:
  148. try:
  149. if not candidate:
  150. continue
  151. # Skip if in project venv
  152. if '.venv' in candidate or 'venv' in candidate:
  153. continue
  154. # Check if path exists
  155. if not os.path.exists(candidate):
  156. continue
  157. # Check Python version
  158. result = subprocess.run(
  159. [candidate, '-c', 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")'],
  160. capture_output=True,
  161. text=True,
  162. timeout=5
  163. )
  164. if result.returncode == 0:
  165. version = result.stdout.strip()
  166. major, minor = map(int, version.split('.'))
  167. # Need Python 3.11+
  168. if major == 3 and minor >= 11:
  169. # Check if pip is available
  170. pip_check = subprocess.run(
  171. [candidate, '-m', 'pip', '--version'],
  172. capture_output=True,
  173. timeout=5
  174. )
  175. if pip_check.returncode == 0:
  176. self.log(f"Found Python {version} at {candidate}", "SUCCESS")
  177. return candidate
  178. except Exception as e:
  179. continue
  180. return None
  181. def _download_with_curl(self, url: str, output_path: Path, description: str = "") -> bool:
  182. """Fallback download method using curl"""
  183. try:
  184. self.log(f"Trying curl fallback for {description}...")
  185. result = subprocess.run(
  186. ['curl', '-L', '-o', str(output_path), url, '--progress-bar'],
  187. check=True,
  188. capture_output=False
  189. )
  190. if result.returncode == 0 and output_path.exists():
  191. self.log(f"Downloaded with curl to {output_path}", "SUCCESS")
  192. return True
  193. except Exception as e:
  194. self.log(f"Curl download also failed: {e}", "ERROR")
  195. return False
  196. def download_python(self) -> Path:
  197. """Download Python embedded distribution"""
  198. python_config = self.config['python']
  199. cache_file = self.cache_dir / f"python-{python_config['version']}-embed-amd64.zip"
  200. if cache_file.exists():
  201. self.log(f"Using cached Python: {cache_file}")
  202. return cache_file
  203. self.cache_dir.mkdir(parents=True, exist_ok=True)
  204. # Choose URL based on mirror setting
  205. url = python_config['mirror_url'] if self.config['mirrors']['use_cn_mirror'] else python_config['download_url']
  206. if self.download_file(url, cache_file, f"Python {python_config['version']}"):
  207. return cache_file
  208. else:
  209. raise RuntimeError("Failed to download Python")
  210. def download_ffmpeg(self) -> Path:
  211. """Download FFmpeg portable"""
  212. ffmpeg_config = self.config['ffmpeg']
  213. cache_file = self.cache_dir / f"ffmpeg-{ffmpeg_config['version']}-win64.zip"
  214. if cache_file.exists():
  215. self.log(f"Using cached FFmpeg: {cache_file}")
  216. return cache_file
  217. self.cache_dir.mkdir(parents=True, exist_ok=True)
  218. url = ffmpeg_config['mirror_url'] if self.config['mirrors']['use_cn_mirror'] else ffmpeg_config['download_url']
  219. if self.download_file(url, cache_file, f"FFmpeg {ffmpeg_config['version']}"):
  220. return cache_file
  221. else:
  222. raise RuntimeError("Failed to download FFmpeg")
  223. def extract_python(self, zip_path: Path, target_dir: Path):
  224. """Extract Python embedded distribution"""
  225. self.log(f"Extracting Python to {target_dir}...")
  226. target_dir.mkdir(parents=True, exist_ok=True)
  227. with zipfile.ZipFile(zip_path, 'r') as zip_ref:
  228. zip_ref.extractall(target_dir)
  229. # Add execute permissions to .exe files (needed on Unix systems)
  230. if os.name != 'nt': # Not on Windows
  231. for exe_file in target_dir.glob('*.exe'):
  232. os.chmod(exe_file, 0o755)
  233. for exe_file in target_dir.glob('**/*.exe'):
  234. os.chmod(exe_file, 0o755)
  235. self.log("Python extracted successfully", "SUCCESS")
  236. def extract_ffmpeg(self, zip_path: Path, target_dir: Path):
  237. """Extract FFmpeg portable"""
  238. self.log(f"Extracting FFmpeg to {target_dir}...")
  239. temp_extract = target_dir.parent / "ffmpeg_temp"
  240. temp_extract.mkdir(parents=True, exist_ok=True)
  241. with zipfile.ZipFile(zip_path, 'r') as zip_ref:
  242. zip_ref.extractall(temp_extract)
  243. # Find the bin directory (FFmpeg archive has nested structure)
  244. bin_dir = None
  245. for root, dirs, files in os.walk(temp_extract):
  246. if 'bin' in dirs:
  247. bin_dir = Path(root) / 'bin'
  248. break
  249. if bin_dir and bin_dir.exists():
  250. target_dir.mkdir(parents=True, exist_ok=True)
  251. shutil.copytree(bin_dir, target_dir, dirs_exist_ok=True)
  252. shutil.rmtree(temp_extract)
  253. self.log("FFmpeg extracted successfully", "SUCCESS")
  254. else:
  255. raise RuntimeError("FFmpeg bin directory not found in archive")
  256. def prepare_python_environment(self, python_dir: Path):
  257. """Prepare Python environment: enable site-packages"""
  258. self.log("Preparing Python environment...")
  259. # Modify python311._pth to enable site-packages
  260. pth_file = python_dir / "python311._pth"
  261. if pth_file.exists():
  262. with open(pth_file, 'r') as f:
  263. lines = f.readlines()
  264. # Uncomment "import site" line or add it
  265. modified = False
  266. for i, line in enumerate(lines):
  267. if line.strip().startswith('#import site'):
  268. lines[i] = 'import site\n'
  269. modified = True
  270. break
  271. if not modified and 'import site' not in ''.join(lines):
  272. lines.append('import site\n')
  273. with open(pth_file, 'w') as f:
  274. f.writelines(lines)
  275. self.log("Enabled site-packages in Python", "SUCCESS")
  276. # Note: On non-Windows systems, we can't run python.exe directly
  277. # Pip and dependencies will be installed using system Python
  278. if os.name == 'nt':
  279. # On Windows, we can install pip directly
  280. python_exe = python_dir / "python.exe"
  281. get_pip_path = self.cache_dir / "get-pip.py"
  282. if not get_pip_path.exists():
  283. self.log("Downloading get-pip.py...")
  284. pip_url = "https://bootstrap.pypa.io/get-pip.py"
  285. self.download_file(pip_url, get_pip_path, "get-pip.py")
  286. self.log("Installing pip...")
  287. result = subprocess.run(
  288. [str(python_exe), str(get_pip_path)],
  289. capture_output=True,
  290. text=True
  291. )
  292. if result.returncode == 0:
  293. self.log("Pip installed successfully", "SUCCESS")
  294. else:
  295. self.log(f"Pip installation warning: {result.stderr}", "WARNING")
  296. else:
  297. self.log("Cross-platform build detected (building on non-Windows)", "INFO")
  298. self.log("Dependencies will be installed using system Python", "INFO")
  299. def install_dependencies(self, python_dir: Path):
  300. """Install project dependencies"""
  301. self.log("Installing project dependencies...")
  302. # Determine target directory for site-packages
  303. site_packages = python_dir / "Lib" / "site-packages"
  304. site_packages.mkdir(parents=True, exist_ok=True)
  305. if os.name == 'nt':
  306. # On Windows, use the embedded Python
  307. python_exe = python_dir / "python.exe"
  308. # Install uv first if configured
  309. if self.config['build'].get('use_uv', True):
  310. self.log("Installing uv...")
  311. subprocess.run(
  312. [str(python_exe), "-m", "pip", "install", "uv"],
  313. check=True
  314. )
  315. # Install dependencies
  316. if self.config['build'].get('use_uv', True):
  317. cmd = [str(python_exe), "-m", "uv", "pip", "install", "-e", str(self.project_root)]
  318. if self.config['mirrors']['use_cn_mirror']:
  319. cmd.extend(["--index-url", self.config['mirrors']['pypi_mirror']])
  320. else:
  321. cmd = [str(python_exe), "-m", "pip", "install", "-e", str(self.project_root)]
  322. if self.config['mirrors']['use_cn_mirror']:
  323. cmd.extend(["--index-url", self.config['mirrors']['pypi_mirror']])
  324. self.log(f"Running: {' '.join(cmd)}")
  325. result = subprocess.run(cmd, capture_output=True, text=True)
  326. if result.returncode == 0:
  327. self.log("Dependencies installed successfully", "SUCCESS")
  328. else:
  329. self.log(f"Dependency installation failed:\n{result.stderr}", "ERROR")
  330. raise RuntimeError("Failed to install dependencies")
  331. else:
  332. # Cross-platform build: use system Python to install to target directory
  333. self.log("Cross-platform build: using system Python to install dependencies")
  334. # Find a Python 3.11+ executable (not from project venv)
  335. python_cmd = self._find_suitable_python()
  336. if not python_cmd:
  337. self.log("No suitable Python 3.11+ found. Please install Python 3.11+ or use Windows to build.", "ERROR")
  338. raise RuntimeError("Python 3.11+ required for cross-platform build")
  339. self.log(f"Using Python: {python_cmd}")
  340. # Use pip with --target to install to specific directory
  341. cmd = [
  342. python_cmd, "-m", "pip", "install",
  343. "--target", str(site_packages),
  344. "--no-user",
  345. "--no-warn-script-location"
  346. ]
  347. # Read dependencies from pyproject.toml
  348. try:
  349. import tomllib
  350. except ImportError:
  351. try:
  352. import tomli as tomllib
  353. except ImportError:
  354. self.log("tomllib/tomli not available, trying simple parsing", "WARNING")
  355. tomllib = None
  356. if tomllib:
  357. pyproject_path = self.project_root / "pyproject.toml"
  358. with open(pyproject_path, 'rb') as f:
  359. pyproject = tomllib.load(f)
  360. deps = pyproject.get('project', {}).get('dependencies', [])
  361. else:
  362. # Simple fallback: read from pyproject.toml manually
  363. import re
  364. pyproject_path = self.project_root / "pyproject.toml"
  365. with open(pyproject_path, 'r') as f:
  366. content = f.read()
  367. # Find dependencies section
  368. deps_match = re.search(r'dependencies\s*=\s*\[(.*?)\]', content, re.DOTALL)
  369. if deps_match:
  370. deps_str = deps_match.group(1)
  371. deps = [dep.strip(' "\',\n') for dep in deps_str.split('\n') if dep.strip() and not dep.strip().startswith('#')]
  372. else:
  373. deps = []
  374. if deps:
  375. cmd.extend(deps)
  376. if self.config['mirrors']['use_cn_mirror']:
  377. cmd.extend(["--index-url", self.config['mirrors']['pypi_mirror']])
  378. self.log(f"Installing {len(deps)} dependencies...")
  379. result = subprocess.run(cmd, capture_output=True, text=True)
  380. if result.returncode == 0:
  381. self.log("Dependencies installed successfully", "SUCCESS")
  382. else:
  383. self.log(f"Dependency installation output:\n{result.stdout}", "INFO")
  384. if result.stderr:
  385. self.log(f"Warnings: {result.stderr}", "WARNING")
  386. else:
  387. self.log("No dependencies found in pyproject.toml", "WARNING")
  388. def copy_project_files(self, target_dir: Path):
  389. """Copy project files to build directory"""
  390. self.log(f"Copying project files to {target_dir}...")
  391. exclude_patterns = self.config['build']['exclude_patterns']
  392. def should_exclude(path: Path) -> bool:
  393. path_str = str(path.relative_to(self.project_root))
  394. for pattern in exclude_patterns:
  395. if pattern.endswith('/*'):
  396. # Directory content exclusion - must match exact directory name or start with "dirname/"
  397. dir_name = pattern[:-2]
  398. if path_str == dir_name or path_str.startswith(f"{dir_name}/"):
  399. return True
  400. elif pattern.endswith('*'):
  401. # Wildcard pattern
  402. if path_str.startswith(pattern[:-1]):
  403. return True
  404. elif '*' in pattern:
  405. # Glob pattern (simple check)
  406. import fnmatch
  407. if fnmatch.fnmatch(path_str, pattern):
  408. return True
  409. else:
  410. # Exact match or directory
  411. if path_str == pattern or path_str.startswith(f"{pattern}/"):
  412. return True
  413. return False
  414. target_dir.mkdir(parents=True, exist_ok=True)
  415. # Copy files
  416. copied_count = 0
  417. for item in self.project_root.iterdir():
  418. if item.name in ['.git', 'packaging', 'dist', '.venv', 'venv']:
  419. continue
  420. if should_exclude(item):
  421. continue
  422. target_path = target_dir / item.name
  423. if item.is_file():
  424. shutil.copy2(item, target_path)
  425. copied_count += 1
  426. elif item.is_dir():
  427. shutil.copytree(item, target_path, ignore=lambda d, names: [
  428. n for n in names if should_exclude(Path(d) / n)
  429. ])
  430. # Count files in copied directory
  431. copied_count += sum(1 for _ in target_path.rglob('*') if _.is_file())
  432. self.log(f"Copied {copied_count} files", "SUCCESS")
  433. def generate_launcher_scripts(self):
  434. """Generate launcher scripts from templates"""
  435. self.log("Generating launcher scripts...")
  436. replacements = {
  437. '{VERSION}': self.version,
  438. '{BUILD_DATE}': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  439. }
  440. # Copy and process templates
  441. for template_file in self.templates_dir.glob('*'):
  442. if template_file.is_file():
  443. target_file = self.build_dir / template_file.name
  444. with open(template_file, 'r', encoding='utf-8') as f:
  445. content = f.read()
  446. # Replace placeholders
  447. for key, value in replacements.items():
  448. content = content.replace(key, value)
  449. with open(target_file, 'w', encoding='utf-8', newline='\r\n') as f:
  450. f.write(content)
  451. self.log(f"Generated: {template_file.name}")
  452. self.log("Launcher scripts generated", "SUCCESS")
  453. def create_empty_directories(self):
  454. """Create empty directories specified in config"""
  455. self.log("Creating empty directories...")
  456. for dir_name in self.config['build'].get('create_empty_dirs', []):
  457. dir_path = self.build_dir / dir_name
  458. dir_path.mkdir(parents=True, exist_ok=True)
  459. # Create .gitkeep to preserve directory in git
  460. (dir_path / '.gitkeep').touch()
  461. self.log("Empty directories created", "SUCCESS")
  462. def create_zip_package(self):
  463. """Create final ZIP package"""
  464. if not self.config['build'].get('create_zip', True):
  465. return
  466. zip_path = self.output_dir / f"{self.package_name}.zip"
  467. self.log(f"Creating ZIP package: {zip_path}...")
  468. compression_map = {
  469. 'deflate': zipfile.ZIP_DEFLATED,
  470. 'bzip2': zipfile.ZIP_BZIP2,
  471. 'lzma': zipfile.ZIP_LZMA,
  472. }
  473. compression = compression_map.get(
  474. self.config['build'].get('zip_compression', 'deflate'),
  475. zipfile.ZIP_DEFLATED
  476. )
  477. with zipfile.ZipFile(zip_path, 'w', compression) as zipf:
  478. for root, dirs, files in os.walk(self.build_dir):
  479. for file in files:
  480. file_path = Path(root) / file
  481. arcname = file_path.relative_to(self.build_dir.parent)
  482. zipf.write(file_path, arcname)
  483. # Calculate file size and hash
  484. size_mb = zip_path.stat().st_size / (1024 * 1024)
  485. with open(zip_path, 'rb') as f:
  486. file_hash = hashlib.sha256(f.read()).hexdigest()
  487. self.log(f"ZIP package created: {zip_path}", "SUCCESS")
  488. self.log(f"Size: {size_mb:.2f} MB")
  489. self.log(f"SHA256: {file_hash}")
  490. # Write hash to file
  491. hash_file = zip_path.with_suffix('.zip.sha256')
  492. with open(hash_file, 'w') as f:
  493. f.write(f"{file_hash} {zip_path.name}\n")
  494. def build(self):
  495. """Main build process"""
  496. self.log("=" * 60, "HEADER")
  497. self.log(f"Building {self.package_name}", "HEADER")
  498. self.log("=" * 60, "HEADER")
  499. try:
  500. # Clean build directory
  501. if self.build_dir.exists():
  502. self.log(f"Cleaning existing build directory: {self.build_dir}")
  503. shutil.rmtree(self.build_dir)
  504. self.build_dir.mkdir(parents=True, exist_ok=True)
  505. self.output_dir.mkdir(parents=True, exist_ok=True)
  506. # Download dependencies
  507. python_zip = self.download_python()
  508. ffmpeg_zip = self.download_ffmpeg()
  509. # Extract Python
  510. python_dir = self.build_dir / "python" / "python311"
  511. self.extract_python(python_zip, python_dir)
  512. # Extract FFmpeg
  513. ffmpeg_dir = self.build_dir / "tools" / "ffmpeg" / "bin"
  514. self.extract_ffmpeg(ffmpeg_zip, ffmpeg_dir)
  515. # Prepare Python environment
  516. self.prepare_python_environment(python_dir)
  517. # Install dependencies
  518. if self.config['build'].get('pre_install_deps', True):
  519. self.install_dependencies(python_dir)
  520. # Copy project files
  521. project_target = self.build_dir / "Pixelle-Video"
  522. self.copy_project_files(project_target)
  523. # Generate launcher scripts
  524. self.generate_launcher_scripts()
  525. # Create empty directories
  526. self.create_empty_directories()
  527. # Create ZIP package
  528. self.create_zip_package()
  529. self.log("=" * 60, "HEADER")
  530. self.log("Build completed successfully!", "SUCCESS")
  531. self.log(f"Package location: {self.build_dir}", "SUCCESS")
  532. self.log("=" * 60, "HEADER")
  533. except Exception as e:
  534. self.log(f"Build failed: {e}", "ERROR")
  535. import traceback
  536. traceback.print_exc()
  537. sys.exit(1)
  538. def main():
  539. parser = argparse.ArgumentParser(description="Build Windows portable package for Pixelle-Video")
  540. parser.add_argument(
  541. '--config',
  542. default='packaging/windows/config/build_config.yaml',
  543. help='Path to build configuration file'
  544. )
  545. parser.add_argument(
  546. '--output',
  547. help='Output directory (default: dist/windows)'
  548. )
  549. parser.add_argument(
  550. '--cn-mirror',
  551. action='store_true',
  552. help='Use China mirrors for faster downloads'
  553. )
  554. args = parser.parse_args()
  555. builder = WindowsPackageBuilder(
  556. config_path=args.config,
  557. output_dir=args.output,
  558. use_cn_mirror=args.cn_mirror
  559. )
  560. builder.build()
  561. if __name__ == '__main__':
  562. main()