| """ |
| Package MT5 terminal files from a local Windows installation into mt5_terminal/ |
| for Docker/Wine deployment on Hugging Face Spaces. |
| |
| Usage — merge from multiple sources (Program Files + AppData): |
| uv run python scripts/package_mt5.py \\ |
| --source "C:\Program Files\MetaTrader 5" \\ |
| --source "$env:APPDATA\MetaQuotes\Terminal\{GUID}" |
| |
| Later sources override earlier ones for the same relative path. |
| """ |
|
|
| import argparse |
| import shutil |
| import sys |
| from pathlib import Path |
|
|
| ESSENTIAL_FILES = [ |
| "terminal64.exe", |
| "metaeditor64.exe", |
| ] |
|
|
| ESSENTIAL_DIRS = [ |
| "MQL5", |
| "history", |
| "profiles", |
| ] |
|
|
| EXCLUDE_PATTERNS = [ |
| "*.log", |
| "MQL5/Logs", |
| "MQL5/Experts/Examples", |
| "MQL5/Experts/Samples", |
| "MQL5/Indicators/Examples", |
| "MQL5/Indicators/Samples", |
| "MQL5/Scripts/Examples", |
| "MQL5/Scripts/Samples", |
| "MQL5/Include/Examples", |
| "history/*.hst", |
| "profiles/*.chr", |
| ] |
|
|
|
|
| def should_exclude(rel_path: Path, excludes: list[str]) -> bool: |
| rel_str = str(rel_path) |
| for pattern in excludes: |
| if pattern in rel_str: |
| return True |
| for parent in rel_path.parents: |
| if parent.name in ["Logs", "Examples", "Samples"]: |
| return True |
| return False |
|
|
|
|
| def package_source(source: Path, dest: Path) -> tuple[int, int]: |
| """Copy files from one source into dest (merge, don't clear dest).""" |
| if not source.exists(): |
| print(f" [!] Source not found: {source}") |
| return 0, 0 |
|
|
| files_copied = 0 |
| bytes_copied = 0 |
|
|
| for fname in ESSENTIAL_FILES: |
| src_file = source / fname |
| if src_file.exists(): |
| dst_file = dest / fname |
| size = src_file.stat().st_size |
| shutil.copy2(src_file, dst_file) |
| files_copied += 1 |
| bytes_copied += size |
| print(f" + {fname} ({size / 1024 / 1024:.1f} MB)") |
|
|
| for dirname in ESSENTIAL_DIRS: |
| src_dir = source / dirname |
| if not src_dir.exists(): |
| continue |
|
|
| print(f" Copying {dirname}/...") |
| for src_path in src_dir.rglob("*"): |
| if not src_path.is_file(): |
| continue |
| rel_path = src_path.relative_to(source) |
| if should_exclude(rel_path, EXCLUDE_PATTERNS): |
| continue |
|
|
| dst_path = dest / rel_path |
| dst_path.parent.mkdir(parents=True, exist_ok=True) |
| existing_size = dst_path.stat().st_size if dst_path.exists() else 0 |
| shutil.copy2(src_path, dst_path) |
| files_copied += 1 |
| bytes_copied += src_path.stat().st_size |
|
|
| return files_copied, bytes_copied |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Package MT5 terminal for Docker deployment") |
| parser.add_argument("--source", type=str, action="append", required=True, |
| help="Path to MT5 source (use multiple times to merge: " |
| "--source 'C:\\Program Files\\MetaTrader 5' " |
| "--source '$APPDATA\\MetaQuotes\\Terminal\\...')") |
| parser.add_argument("--dest", type=str, default="mt5_terminal", |
| help="Destination directory (default: mt5_terminal/)") |
| parser.add_argument("--dry-run", action="store_true", |
| help="Show what would be copied without actually copying") |
| args = parser.parse_args() |
|
|
| dest = Path(args.dest) |
| sources = [Path(s) for s in args.source] |
|
|
| print(f"Packaging MT5 from {len(sources)} source(s):") |
| for s in sources: |
| print(f" => {s}") |
| print(f"Destination: {dest}") |
| print() |
|
|
| if args.dry_run: |
| print("DRY RUN - no files will be copied") |
| total_files = 0 |
| total_bytes = 0 |
| for source in sources: |
| if not source.exists(): |
| print(f" [!] Not found: {source}") |
| continue |
| for fname in ESSENTIAL_FILES: |
| f = source / fname |
| if f.exists(): |
| size = f.stat().st_size |
| print(f" + {fname} ({size / 1024 / 1024:.1f} MB)") |
| total_files += 1 |
| total_bytes += size |
| for dirname in ESSENTIAL_DIRS: |
| d = source / dirname |
| if d.exists(): |
| count = sum( |
| 1 for p in d.rglob("*") |
| if p.is_file() and not should_exclude(p.relative_to(source), EXCLUDE_PATTERNS) |
| ) |
| print(f" + {dirname}/ (~{count} files)") |
| total_files += count |
| total_bytes += sum( |
| p.stat().st_size for p in d.rglob("*") |
| if p.is_file() and not should_exclude(p.relative_to(source), EXCLUDE_PATTERNS) |
| ) |
| print(f"\nWould copy: ~{total_files} files, {total_bytes / 1024 / 1024:.1f} MB total") |
| return |
|
|
| dest.mkdir(parents=True, exist_ok=True) |
| total_files = 0 |
| total_bytes = 0 |
|
|
| print("Copying files...") |
| for i, source in enumerate(sources, 1): |
| print(f"\n[{i}/{len(sources)}] {source}") |
| fc, bc = package_source(source, dest) |
| total_files += fc |
| total_bytes += bc |
|
|
| print(f"\n[OK] Packaged {total_files} files ({total_bytes / 1024 / 1024:.1f} MB)") |
| print(f"\nNext steps:") |
| print(f" 1. Review {dest}/ to ensure it looks correct") |
| print(f" 2. Check size: du -sh {dest}/") |
| print(f" 3. Deploy to HF: uv run python scripts/upload_to_hf.py --mt5") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|