-
-
Notifications
You must be signed in to change notification settings - Fork 42
Expand file tree
/
Copy pathinstall.py
More file actions
1918 lines (1640 loc) · 87.3 KB
/
install.py
File metadata and controls
1918 lines (1640 loc) · 87.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
FunGen Universal Installer - Stage 2
Version: 1.2.0
Complete installation system that assumes Python is available but nothing else
This installer handles the complete FunGen setup after Python is installed:
- Git installation and repository cloning
- FFmpeg suite installation (ffmpeg, ffprobe, ffplay)
- GPU detection and appropriate PyTorch installation
- Virtual environment setup
- All Python dependencies
- Launcher script creation and validation
Supports: Windows, macOS (Intel/Apple Silicon), Linux (x86_64/ARM64)
"""
import os
import sys
import platform
import subprocess
import urllib.request
import urllib.error
import shutil
import tempfile
import time
import json
import zipfile
import tarfile
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import argparse
# Version information
INSTALLER_VERSION = "1.3.5"
# Configuration
CONFIG = {
"repo_url": "https://github.com/ack00gar/FunGen-AI-Powered-Funscript-Generator.git",
"project_name": "FunGen",
"env_name": "FunGen",
"python_version": "3.11",
"main_script": "main.py",
"min_disk_space_gb": 10,
"requirements_files": {
"core": "requirements/core.requirements.txt",
"cuda": "requirements/cuda.requirements.txt",
"cpu": "requirements/cpu.requirements.txt",
"rocm": "requirements/rocm.requirements.txt"
}
}
# Download URLs for various tools
DOWNLOAD_URLS = {
"git": {
"windows": "https://github.com/git-for-windows/git/releases/download/v2.45.2.windows.1/Git-2.45.2-64-bit.exe",
"portable_windows": "https://github.com/git-for-windows/git/releases/download/v2.45.2.windows.1/PortableGit-2.45.2-64-bit.7z.exe"
},
"ffmpeg": {
"windows": "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip",
"macos": "https://evermeet.cx/ffmpeg/ffmpeg-6.1.zip",
"linux": {
"x86_64": "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz",
"aarch64": "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz"
}
}
}
class Colors:
"""ANSI color codes for terminal output"""
if platform.system() == "Windows":
# Try to enable ANSI colors on Windows
try:
import ctypes
kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
except Exception:
pass
HEADER = '\033[95m'
BLUE = '\033[94m'
CYAN = '\033[96m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
class ProgressBar:
"""Simple progress bar for downloads"""
def __init__(self, total_size: int, description: str = ""):
self.total_size = total_size
self.downloaded = 0
self.description = description
self.last_update = 0
def update(self, chunk_size: int):
self.downloaded += chunk_size
current_time = time.time()
# Update every 0.1 seconds to avoid too much output
if current_time - self.last_update > 0.1:
if self.total_size > 0:
percent = min(100, (self.downloaded * 100) // self.total_size)
bar_length = 40
filled = (percent * bar_length) // 100
bar = '█' * filled + '░' * (bar_length - filled)
size_mb = self.downloaded / (1024 * 1024)
total_mb = self.total_size / (1024 * 1024)
print(f"\r {self.description}: {bar} {percent}% ({size_mb:.1f}/{total_mb:.1f} MB)",
end="", flush=True)
else:
size_mb = self.downloaded / (1024 * 1024)
print(f"\r {self.description}: {size_mb:.1f} MB downloaded", end="", flush=True)
self.last_update = current_time
def finish(self):
print() # New line after completion
class FunGenUniversalInstaller:
"""Universal FunGen installer - assumes Python is available"""
def __init__(self, install_dir: Optional[str] = None, force: bool = False,
bootstrap_version: Optional[str] = None, skip_clone: bool = False):
self.platform = platform.system()
self.arch = platform.machine().lower()
self.force = force
self.bootstrap_version = bootstrap_version
self.skip_clone = skip_clone
if skip_clone:
# Use the directory where this script is located
self.install_dir = Path(__file__).parent.resolve()
self.project_path = self.install_dir
print(f"{Colors.CYAN}Using existing repository at: {self.project_path}{Colors.ENDC}")
else:
self.install_dir = Path(install_dir) if install_dir else Path.cwd()
self.project_path = self.install_dir / CONFIG["project_name"]
# Setup paths
self.setup_paths()
# Progress tracking
self.current_step = 0
self.total_steps = 9
# Installation state
self.conda_available = False
self.venv_path = None
def setup_paths(self):
"""Setup platform-specific paths"""
self.home = Path.home()
if self.platform == "Windows":
self.miniconda_path = self.home / "miniconda3"
# install.bat may use C:\miniconda3 when user profile has spaces
if not (self.miniconda_path / "Scripts" / "conda.exe").exists():
alt = Path("C:/miniconda3")
if (alt / "Scripts" / "conda.exe").exists():
self.miniconda_path = alt
self.tools_dir = self.install_dir / "tools"
self.git_path = self.tools_dir / "git"
self.ffmpeg_path = self.tools_dir / "ffmpeg"
else:
self.miniconda_path = self.home / "miniconda3"
self.tools_dir = self.home / ".local" / "bin"
self.git_path = self.tools_dir
self.ffmpeg_path = self.tools_dir
def print_header(self):
"""Print installer header"""
print(f"\n{Colors.HEADER}{Colors.BOLD}=" * 60)
print(" FunGen Universal Installer")
print(f" v{INSTALLER_VERSION}")
if self.bootstrap_version:
print(f" (Bootstrap v{self.bootstrap_version})")
print("=" * 60 + Colors.ENDC)
print(f"{Colors.CYAN}Platform: {self.platform} ({self.arch})")
print(f"Install Directory: {self.install_dir}")
print(f"Project Path: {self.project_path}{Colors.ENDC}")
# Add interactive warning for macOS/Linux
if self.platform in ["Darwin", "Linux"]:
print(f"\n{Colors.YELLOW}⚠️ INTERACTIVE INSTALLATION NOTICE:")
print(" Some system installations may require your interaction:")
print(" • Password prompts for system package installation")
print(" • License agreement acceptance (Xcode Command Line Tools)")
print(" • Package manager confirmations")
print(f" Please stay near your computer during installation.{Colors.ENDC}")
print()
def print_step(self, step_name: str):
"""Print current installation step"""
self.current_step += 1
print(f"{Colors.BLUE}[{self.current_step}/{self.total_steps}] {step_name}...{Colors.ENDC}")
def print_success(self, message: str):
"""Print success message"""
print(f"{Colors.GREEN}✓ {message}{Colors.ENDC}")
def print_warning(self, message: str):
"""Print warning message"""
print(f"{Colors.YELLOW}⚠ {message}{Colors.ENDC}")
def print_error(self, message: str):
"""Print error message"""
print(f"{Colors.RED}✗ {message}{Colors.ENDC}")
def command_exists(self, command: str) -> bool:
"""Check if a command exists"""
return shutil.which(command) is not None
def run_command(self, cmd: List[str], cwd: Optional[Path] = None,
check: bool = True, capture: bool = False,
env: Optional[Dict] = None) -> Tuple[int, str, str]:
"""Run a command with comprehensive error handling"""
try:
kwargs = {
'cwd': cwd,
'check': check,
'env': env or os.environ.copy()
}
if capture:
kwargs.update({'capture_output': True, 'text': True})
result = subprocess.run(cmd, **kwargs)
if capture:
return result.returncode, result.stdout, result.stderr
else:
return result.returncode, "", ""
except subprocess.CalledProcessError as e:
stdout = getattr(e, 'stdout', '') or ''
stderr = getattr(e, 'stderr', '') or ''
return e.returncode, stdout, stderr
except FileNotFoundError:
return 127, "", f"Command not found: {cmd[0]}"
except Exception as e:
return 1, "", str(e)
def download_with_progress(self, url: str, filepath: Path, description: str = "") -> bool:
"""Download file with progress bar"""
try:
print(f" Downloading {description or url}...")
# Get file size
req = urllib.request.urlopen(url)
total_size = int(req.headers.get('Content-Length', 0))
progress = ProgressBar(total_size, description or "File")
with open(filepath, 'wb') as f:
while True:
chunk = req.read(8192)
if not chunk:
break
f.write(chunk)
progress.update(len(chunk))
progress.finish()
req.close()
return True
except Exception as e:
self.print_error(f"Download failed: {e}")
return False
def extract_archive(self, archive_path: Path, extract_to: Path, description: str = "") -> bool:
"""Extract various archive formats"""
try:
print(f" Extracting {description}...")
if archive_path.suffix.lower() in ['.zip']:
with zipfile.ZipFile(archive_path, 'r') as zip_ref:
zip_ref.extractall(extract_to)
elif archive_path.suffix.lower() in ['.tar', '.gz', '.xz']:
with tarfile.open(archive_path, 'r:*') as tar_ref:
tar_ref.extractall(extract_to)
else:
self.print_error(f"Unsupported archive format: {archive_path.suffix}")
return False
self.print_success(f"{description} extracted successfully")
return True
except Exception as e:
self.print_error(f"Extraction failed: {e}")
return False
def safe_rmtree(self, path: Path) -> bool:
"""Safely remove a directory tree with error handling."""
try:
if path.exists():
shutil.rmtree(path)
self.print_success(f"Removed directory: {path}")
return True
except Exception as e:
self.print_error(f"Failed to remove directory {path}: {e}")
return False
def check_system_requirements(self) -> bool:
"""Check system requirements"""
self.print_step("Checking system requirements")
# Check Python version
if sys.version_info < (3, 9):
self.print_error(f"Python 3.9+ required, found {sys.version}")
return False
self.print_success(f"Python {sys.version.split()[0]} available")
# Check disk space
try:
disk_usage = shutil.disk_usage(self.install_dir)
free_gb = disk_usage.free / (1024**3)
if free_gb < CONFIG["min_disk_space_gb"]:
self.print_error(f"Insufficient disk space: {free_gb:.1f}GB available, {CONFIG['min_disk_space_gb']}GB required")
return False
self.print_success(f"Disk space: {free_gb:.1f}GB available")
except Exception as e:
self.print_warning(f"Could not check disk space: {e}")
# Check for spaces in paths (Windows-specific issue with conda)
if self.platform == "Windows" and ' ' in str(self.home):
self.print_warning("╔" + "="*68 + "╗")
self.print_warning("║ WARNING: Your user profile path contains spaces! ║")
self.print_warning("║ ║")
truncated_path = str(self.home)[:62]
self.print_warning(f"║ Path: {truncated_path:<62}║")
self.print_warning("║ ║")
self.print_warning("║ Conda/Miniconda can have issues with spaces in paths. ║")
self.print_warning("║ This may cause activation or package installation failures. ║")
self.print_warning("╚" + "="*68 + "╝")
if not self.force:
print(f"\n{Colors.CYAN}Options:{Colors.ENDC}")
print(" 1. Continue anyway (may work, but could have issues)")
print(" 2. Specify alternative Miniconda install path (recommended)")
print(" 3. Abort installation")
while True:
response = input("\n Enter choice [1/2/3]: ").strip()
if response == '1':
self.print_warning("Continuing with spaces in path - watch for issues")
break
elif response == '2':
print(f"\n{Colors.CYAN}Enter alternative path for Miniconda (no spaces):{Colors.ENDC}")
print(" Example: C:\\Miniconda3 or D:\\tools\\miniconda3")
alt_path = input(" Path: ").strip().strip('"').strip("'")
if ' ' in alt_path:
self.print_error("Path still contains spaces! Please choose a path without spaces.")
continue
if alt_path:
self.miniconda_path = Path(alt_path)
self.print_success(f"Will use alternative Miniconda path: {self.miniconda_path}")
break
else:
self.print_error("No path entered")
continue
elif response == '3':
print("\n Installation aborted.")
return False
else:
print(" Invalid choice. Please enter 1, 2, or 3.")
# Detect active conda env (install.bat may have already activated it)
conda_prefix = os.environ.get("CONDA_PREFIX")
if conda_prefix and not self.conda_available:
prefix_path = Path(conda_prefix)
# CONDA_PREFIX points to the env dir (e.g. .../envs/FunGen) or base
# Derive miniconda root from it
if prefix_path.name == "miniconda3":
candidate = prefix_path
elif "envs" in prefix_path.parts:
candidate = prefix_path.parent.parent # .../miniconda3/envs/FunGen -> .../miniconda3
else:
candidate = prefix_path
conda_bin = candidate / ("Scripts/conda.exe" if self.platform == "Windows" else "bin/conda")
if conda_bin.exists() and candidate != self.miniconda_path:
self.miniconda_path = candidate
self.print_success(f"Detected active conda at: {self.miniconda_path}")
# Check if conda is available
self.conda_available = (self.miniconda_path / "bin" / "conda").exists() or (self.miniconda_path / "Scripts" / "conda.exe").exists()
if self.conda_available:
# Verify conda is functional, not just present
conda_test = self.miniconda_path / ("Scripts/conda.exe" if self.platform == "Windows" else "bin/conda")
ret, _, stderr = self.run_command([str(conda_test), "--version"], capture=True, check=False)
if ret != 0:
self.print_warning(f"Conda binary found but not functional: {stderr.strip()}")
self.print_warning("Will attempt to use it anyway, but may fall back to venv")
self.print_success("Conda environment manager available")
# Check if conda Python is wrong architecture on macOS
if self.platform == "Darwin" and self.arch == "arm64":
conda_python = self.miniconda_path / "bin" / "python"
if conda_python.exists():
try:
result = subprocess.run(['file', str(conda_python)],
capture_output=True, text=True, timeout=5)
if 'x86_64' in result.stdout:
self.print_warning("╔" + "="*70 + "╗")
self.print_warning("║ WARNING: x86_64 (Intel) Miniconda detected on Apple Silicon! ║")
self.print_warning("║ This will cause: ║")
self.print_warning("║ • Slower performance (running under Rosetta 2) ║")
self.print_warning("║ • CoreML model conversion will NOT work ║")
self.print_warning("║ • No GPU acceleration via Metal Performance Shaders (MPS) ║")
self.print_warning("║ ║")
self.print_warning("║ Recommended: Delete ~/miniconda3 and rerun installer ║")
self.print_warning("║ rm -rf ~/miniconda3 ║")
self.print_warning("║ curl -fsSL https://raw.githubusercontent.com/.../install.sh | bash ║")
self.print_warning("╚" + "="*70 + "╝")
# Give user option to abort
if not self.force:
response = input("\n Continue anyway? [y/N]: ").strip().lower()
if response != 'y':
print("\n Installation aborted. Please reinstall with ARM64 Miniconda.")
return False
except Exception:
pass
else:
self.print_success("Will use Python venv for environment management")
return True
def install_git(self) -> bool:
"""Install Git if not available"""
if self.command_exists("git"):
self.print_success("Git already available")
return True
print(" Installing Git...")
if self.platform == "Windows":
return self._install_git_windows()
elif self.platform == "Darwin":
return self._install_git_macos()
else:
return self._install_git_linux()
def _install_git_windows(self) -> bool:
"""Install Git on Windows"""
git_url = DOWNLOAD_URLS["git"]["windows"]
with tempfile.TemporaryDirectory() as temp_dir:
installer_path = Path(temp_dir) / "git-installer.exe"
if not self.download_with_progress(git_url, installer_path, "Git installer"):
return False
# Install silently with user-only installation (no admin required)
ret, _, stderr = self.run_command([
str(installer_path), "/VERYSILENT", "/NORESTART", "/NOCANCEL",
"/SP-", "/CLOSEAPPLICATIONS", "/RESTARTAPPLICATIONS",
"/CURRENTUSER" # Install for current user only
], check=False)
if ret == 0:
self.print_success("Git installed successfully")
# Refresh PATH
git_paths = [
str(Path.home() / "AppData" / "Local" / "Programs" / "Git" / "bin"),
str(Path("C:") / "Program Files" / "Git" / "bin")
]
for git_path in git_paths:
if Path(git_path).exists() and git_path not in os.environ["PATH"]:
os.environ["PATH"] = git_path + ";" + os.environ["PATH"]
break
return True
else:
self.print_error(f"Git installation failed: {stderr}")
return False
def _install_git_macos(self) -> bool:
"""Install Git on macOS"""
# Check if Homebrew is available
if self.command_exists("brew"):
print(" Installing Git via Homebrew...")
print(" Note: This may prompt for your password or Xcode license acceptance")
ret, _, stderr = self.run_command(["brew", "install", "git"], check=False)
if ret == 0:
self.print_success("Git installed via Homebrew")
return True
else:
print(f" Homebrew install failed: {stderr}")
# Try to install Xcode Command Line Tools
print(" Installing Xcode Command Line Tools (includes Git)...")
print(" This will open a dialog - please accept the license agreement")
ret, _, _ = self.run_command(["xcode-select", "--install"], check=False)
if ret == 0:
print(" ⚠️ INTERACTIVE STEP REQUIRED:")
print(" A dialog has opened for Xcode Command Line Tools installation")
print(" Please accept the license agreement and complete the installation")
print(" This may take several minutes to download and install")
print(" ")
input(" Press Enter when the installation is complete...")
# Verify installation
if self.command_exists("git"):
self.print_success("Git installed via Xcode Command Line Tools")
return True
else:
self.print_error("Git installation verification failed")
return False
else:
self.print_error("Could not install Git automatically")
self.print_error("Please install Git manually: https://git-scm.com/download/mac")
return False
def _install_git_linux(self) -> bool:
"""Install Git on Linux"""
# Try different package managers
package_managers = [
(["apt", "update"], ["apt", "install", "-y", "git"]),
(None, ["yum", "install", "-y", "git"]),
(None, ["dnf", "install", "-y", "git"]),
(None, ["pacman", "-S", "--noconfirm", "git"]),
(None, ["zypper", "install", "-y", "git"]),
(None, ["apk", "add", "git"])
]
for update_cmd, install_cmd in package_managers:
if self.command_exists(install_cmd[0]):
print(f" Using {install_cmd[0]} package manager...")
print(" Note: You may be prompted for your password or to accept terms")
if update_cmd:
print(" Updating package lists...")
self.run_command(update_cmd, check=False)
print(f" Installing Git with {install_cmd[0]}...")
ret, _, stderr = self.run_command(install_cmd, check=False)
if ret == 0:
self.print_success(f"Git installed via {install_cmd[0]}")
return True
else:
self.print_warning(f"Failed to install with {install_cmd[0]}: {stderr}")
# If interactive prompts were missed, suggest manual installation
if "interactive" in stderr.lower() or "prompt" in stderr.lower():
print(" ⚠️ This package manager may require interactive input")
print(f" Try running manually: sudo {' '.join(install_cmd)}")
return False
self.print_error("Could not install Git automatically")
self.print_error("Please install Git manually using your system's package manager")
return False
def clone_repository(self) -> bool:
"""Clone or update the FunGen repository"""
if self.skip_clone:
# Verify we're in a valid git repository
if not (self.project_path / ".git").exists():
self.print_error("--skip-clone specified but current directory is not a git repository")
self.print_error(f"Expected .git directory in: {self.project_path}")
return False
# Verify it's the FunGen repository
ret, stdout, _ = self.run_command(
["git", "config", "--get", "remote.origin.url"],
cwd=self.project_path,
capture=True,
check=False
)
if ret == 0 and "FunGen" in stdout:
ret, stdout, _ = self.run_command(
["git", "rev-parse", "--short", "HEAD"],
cwd=self.project_path,
capture=True,
check=False
)
if ret == 0:
commit = stdout.strip()
self.print_success(f"Using existing repository (commit: {commit})")
else:
self.print_success("Using existing repository")
return True
else:
self.print_warning("Repository URL does not match FunGen - continuing anyway")
self.print_success("Using existing repository")
return True
if self.project_path.exists():
if self.force:
print(" Removing existing project directory...")
if not self.safe_rmtree(self.project_path):
return False
else:
print(" Project directory exists, updating...")
ret, _, stderr = self.run_command(
["git", "pull"],
cwd=self.project_path,
check=False
)
if ret == 0:
self.print_success("Repository updated")
return True
else:
self.print_warning(f"Git pull failed: {stderr}")
# Continue with fresh clone
print(" Cloning repository...")
ret, _, stderr = self.run_command([
"git", "clone", "--branch", "main", CONFIG["repo_url"], str(self.project_path)
], check=False)
if ret == 0:
# Configure git safe.directory to prevent permission issues
self.run_command([
"git", "config", "--add", "safe.directory", str(self.project_path)
], cwd=self.project_path, check=False)
# Verify git repository is properly set up
ret, stdout, _ = self.run_command([
"git", "rev-parse", "--short", "HEAD"
], cwd=self.project_path, check=False)
if ret == 0:
commit = stdout.strip()
self.print_success(f"Repository cloned successfully (main@{commit})")
else:
self.print_success("Repository cloned successfully")
return True
else:
self.print_error(f"Failed to clone repository: {stderr}")
return False
def install_ffmpeg(self) -> bool:
"""Install FFmpeg, FFprobe, and FFplay"""
if self.command_exists("ffmpeg") and self.command_exists("ffprobe") and self.command_exists("ffplay"):
self.print_success("FFmpeg suite already available")
return True
print(" Installing FFmpeg...")
if self.platform == "Windows":
return self._install_ffmpeg_windows()
elif self.platform == "Darwin":
return self._install_ffmpeg_macos()
else:
return self._install_ffmpeg_linux()
def _install_ffmpeg_windows(self) -> bool:
"""Install FFmpeg on Windows"""
ffmpeg_url = DOWNLOAD_URLS["ffmpeg"]["windows"]
with tempfile.TemporaryDirectory() as temp_dir:
archive_path = Path(temp_dir) / "ffmpeg.zip"
if not self.download_with_progress(ffmpeg_url, archive_path, "FFmpeg"):
return False
# Extract to tools directory
self.tools_dir.mkdir(parents=True, exist_ok=True)
extract_dir = Path(temp_dir) / "extracted"
if not self.extract_archive(archive_path, extract_dir, "FFmpeg"):
return False
# Find the ffmpeg directory (varies by build)
ffmpeg_dirs = [d for d in extract_dir.iterdir() if d.is_dir() and "ffmpeg" in d.name.lower()]
if not ffmpeg_dirs:
self.print_error("Could not find FFmpeg directory in archive")
return False
ffmpeg_source = ffmpeg_dirs[0] / "bin"
if not ffmpeg_source.exists():
self.print_error("Could not find FFmpeg binaries")
return False
# Copy to tools directory
ffmpeg_dest = self.tools_dir / "ffmpeg"
if ffmpeg_dest.exists():
shutil.rmtree(ffmpeg_dest)
shutil.copytree(ffmpeg_source, ffmpeg_dest)
# Add to PATH for this session
ffmpeg_bin = str(ffmpeg_dest)
if ffmpeg_bin not in os.environ["PATH"]:
os.environ["PATH"] = ffmpeg_bin + ";" + os.environ["PATH"]
# Verify all FFmpeg tools are available
if self.command_exists("ffmpeg") and self.command_exists("ffprobe") and self.command_exists("ffplay"):
self.print_success("FFmpeg suite installed successfully")
return True
else:
self.print_error("FFmpeg installation incomplete - some tools missing")
return False
def _install_ffmpeg_macos(self) -> bool:
"""Install FFmpeg on macOS"""
if self.command_exists("brew"):
ret, _, stderr = self.run_command(["brew", "install", "ffmpeg"], check=False)
if ret == 0:
# Verify all FFmpeg tools are available
if self.command_exists("ffmpeg") and self.command_exists("ffprobe") and self.command_exists("ffplay"):
self.print_success("FFmpeg suite installed via Homebrew")
return True
else:
self.print_error("FFmpeg installation incomplete - some tools missing")
return False
self.print_warning("Could not install FFmpeg automatically")
self.print_warning("Please install Homebrew and run: brew install ffmpeg")
self.print_warning("FFmpeg suite (including ffplay) is required for fullscreen functionality")
return False # Fail installation if FFmpeg not available
def _install_ffmpeg_linux(self) -> bool:
"""Install FFmpeg on Linux"""
# Try package managers first
package_managers = [
(["apt", "update"], ["apt", "install", "-y", "ffmpeg"]),
(None, ["yum", "install", "-y", "ffmpeg"]),
(None, ["dnf", "install", "-y", "ffmpeg"]),
(None, ["pacman", "-S", "--noconfirm", "ffmpeg"]),
(None, ["zypper", "install", "-y", "ffmpeg"]),
(None, ["apk", "add", "ffmpeg"])
]
for update_cmd, install_cmd in package_managers:
if self.command_exists(install_cmd[0]):
print(f" Using {install_cmd[0]} package manager...")
if update_cmd:
self.run_command(update_cmd, check=False)
ret, _, stderr = self.run_command(install_cmd, check=False)
if ret == 0:
# Verify all FFmpeg tools are available
if self.command_exists("ffmpeg") and self.command_exists("ffprobe") and self.command_exists("ffplay"):
self.print_success(f"FFmpeg suite installed via {install_cmd[0]}")
return True
else:
self.print_error("FFmpeg installation incomplete - some tools missing")
continue # Try next package manager
self.print_warning("Could not install FFmpeg automatically")
self.print_warning("Please install FFmpeg using your system's package manager")
self.print_warning("FFmpeg suite (including ffplay) is required for fullscreen functionality")
return False # Fail installation if FFmpeg not available
def _check_arm64_windows_compatibility(self):
"""Check for ARM64 Windows and provide guidance."""
if platform.system() == "Windows" and platform.machine().lower() in ['arm64', 'aarch64']:
python_arch = platform.architecture()[0]
self.print_warning("ARM64 Windows detected!")
self.print_warning("")
self.print_warning("ARM64 Windows has limited Python package support.")
self.print_warning("Many packages (including imgui) cannot compile on ARM64.")
self.print_warning("")
if "arm" in platform.platform().lower() or "arm64" in python_arch.lower():
self.print_warning("RECOMMENDED SOLUTION:")
self.print_warning("1. Uninstall current Python (if ARM64)")
self.print_warning("2. Download Python 3.11 x64 from python.org")
self.print_warning("3. Install x64 Python (will run via emulation)")
self.print_warning("4. Rerun this installer")
self.print_warning("")
self.print_warning("ALTERNATIVE: Use Windows Subsystem for Linux (WSL)")
self.print_warning("- Run: wsl --install")
self.print_warning("- Install Ubuntu and run FunGen in Linux")
self.print_warning("")
response = input("Continue anyway? (not recommended) [y/N]: ").lower()
if response != 'y':
self.print_error("Installation cancelled. Please install x64 Python first.")
return False
return True
def _handle_imgui_installation_failure(self):
"""Handle imgui installation failure with appropriate guidance."""
self.print_error("All imgui installation strategies failed.")
self.print_error("This means no precompiled wheels are available and compilation failed.")
self.print_error("")
# ARM64-specific guidance
if platform.machine().lower() in ['arm64', 'aarch64']:
self.print_error("ARM64 WINDOWS DETECTED:")
self.print_error("imgui does not compile on ARM64 Windows.")
self.print_error("")
self.print_error("RECOMMENDED SOLUTION:")
self.print_error("1. Uninstall current ARM64 Python")
self.print_error("2. Download Python 3.11 x64 from python.org")
self.print_error("3. Install x64 Python (runs via emulation)")
self.print_error("4. Rerun this installer")
self.print_error("")
self.print_error("ALTERNATIVE: Use WSL2 Ubuntu")
else:
self.print_error("SOLUTION OPTIONS:")
self.print_error("1. EASIEST: Install Microsoft Visual C++ Build Tools:")
self.print_error(" https://visualstudio.microsoft.com/visual-cpp-build-tools/")
self.print_error(" - Download 'Build Tools for Visual Studio 2022'")
self.print_error(" - Select 'C++ build tools' workload")
self.print_error(" - Restart computer after installation")
self.print_error("")
self.print_error("2. Install Visual Studio Community (includes build tools)")
self.print_error("3. Use Windows Subsystem for Linux (WSL2)")
self.print_error("")
self.print_error("⚠️ WITHOUT IMGUI, FUNGEN CANNOT DISPLAY ITS GUI!")
self.print_error("The installation will continue, but FunGen won't work until this is fixed.")
print(" Core requirements installed (GUI unavailable)")
def setup_python_environment(self) -> bool:
"""Setup Python virtual environment"""
print(" Setting up Python environment...")
# Check ARM64 compatibility before proceeding
if not self._check_arm64_windows_compatibility():
return False
if self.conda_available:
if self._setup_conda_environment():
return True
else:
self.print_warning("╔" + "="*68 + "╗")
self.print_warning("║ WARNING: Conda environment setup FAILED ║")
self.print_warning("║ Falling back to Python venv (may cause issues) ║")
self.print_warning("║ ║")
self.print_warning("║ For best results, run install.bat (Windows) or install.sh ║")
self.print_warning("║ instead of install.py directly. ║")
self.print_warning("║ Or install Miniconda: https://docs.conda.io/miniconda ║")
self.print_warning("╚" + "="*68 + "╝")
self.conda_available = False
return self._setup_venv_environment()
else:
return self._setup_venv_environment()
def _setup_conda_environment(self) -> bool:
"""Setup conda environment"""
if self.platform == "Windows":
conda_exe = self.miniconda_path / "Scripts" / "conda.exe"
if not conda_exe.exists():
conda_exe = self.miniconda_path / "condabin" / "conda.bat"
else:
conda_exe = self.miniconda_path / "bin" / "conda"
# Accept conda Terms of Service if not already accepted
print(" Accepting conda Terms of Service...")
channels = [
"https://repo.anaconda.com/pkgs/main",
"https://repo.anaconda.com/pkgs/r"
]
for channel in channels:
ret, stdout, stderr = self.run_command([
str(conda_exe), "tos", "accept", "--override-channels", "--channel", channel
], capture=True, check=False)
if ret != 0 and stderr.strip():
self.print_warning(f" Conda TOS issue ({channel}): {stderr.strip()[:120]}")
# Check if environment exists
ret, stdout, _ = self.run_command([str(conda_exe), "env", "list"], capture=True, check=False)
if ret != 0:
self.print_warning(f" 'conda env list' failed (exit {ret}). Conda may need initialization.")
self.print_warning(f" Try: conda init")
env_exists = CONFIG["env_name"] in stdout if ret == 0 else False
if not env_exists:
print(f" Creating conda environment '{CONFIG['env_name']}'...")
print(f" Using conda at: {conda_exe}")
print(f" Command: conda create -n {CONFIG['env_name']} python={CONFIG['python_version']} -y")
ret, stdout, stderr = self.run_command([
str(conda_exe), "create", "-n", CONFIG["env_name"],
f"python={CONFIG['python_version']}", "-y"
], check=False)
if ret != 0:
self.print_error(f"Failed to create conda environment")
self.print_error(f"Error details: {stderr}")
if stdout:
self.print_error(f"Output: {stdout}")
# Common solutions
print("\nPossible solutions:")
print("1. Try running manually:")
print(f" conda create -n {CONFIG['env_name']} python={CONFIG['python_version']} -y")
print("2. Check if conda is properly initialized:")
print(" conda init")
print("3. Try using system Python instead (rerun installer)")
print("4. Check available conda channels:")
print(" conda info")
return False
else:
self.print_success(f"Using existing conda environment '{CONFIG['env_name']}'")
return True
def _setup_venv_environment(self) -> bool:
"""Setup Python venv environment"""
self.venv_path = self.project_path / "venv"
if self.venv_path.exists() and not self.force:
self.print_success("Using existing virtual environment")
return True
print(f" Creating virtual environment...")
print(f" Using Python: {sys.executable}")
print(f" Target path: {self.venv_path}")
ret, _, stderr = self.run_command([
sys.executable, "-m", "venv", str(self.venv_path)
], check=False)
if ret != 0:
self.print_error(f"Failed to create virtual environment")
self.print_error(f"Error: {stderr}")
print("\nPossible solutions:")
print("1. Check if Python venv module is available:")
print(f" {sys.executable} -m venv --help")
print("2. Try installing python3-venv (Linux):")
print(" sudo apt install python3-venv")
print("3. Use system Python directly (not recommended)")
return False
self.print_success("Virtual environment created")
return True
def install_python_dependencies(self) -> bool:
"""Install Python dependencies"""
print(" Installing Python dependencies...")
original_dir = Path.cwd()
try:
os.chdir(self.project_path)
# Get Python executable for the environment
python_exe = self._get_python_executable()
if not python_exe:
self.print_error("Could not find Python executable for environment")
return False
# Log Python version and path for diagnostics
ret, py_ver, _ = self.run_command([str(python_exe), "--version"], capture=True, check=False)
print(f" Using Python: {python_exe} ({py_ver.strip() if ret == 0 else 'version unknown'})")
# Upgrade pip first
print(" Upgrading pip...")
self.run_command([str(python_exe), "-m", "pip", "install", "--upgrade", "pip"], check=False)
# Install core requirements
core_req = CONFIG["requirements_files"]["core"]
core_req_path = self.project_path / core_req
if core_req_path.exists():
# On Windows, try to avoid imgui compilation issues by installing packages individually first
if platform.system() == "Windows":
print(f" Installing core requirements (Windows optimized approach)...")
print(" Installing packages individually to avoid compilation issues...")
# Install all packages except imgui first
non_imgui_packages = [
"numpy", "ultralytics==8.3.78", "glfw~=2.8.0", "pyopengl~=3.1.7",
"imageio~=2.36.1", "tqdm~=4.67.1", "colorama~=0.4.6",
"opencv-python~=4.10.0.84", "scipy~=1.15.1", "simplification~=0.7.13",
"msgpack~=1.1.0", "pillow~=11.1.0", "orjson~=3.10.15",
"send2trash~=1.8.3", "aiosqlite"
]
ret, stdout, stderr = self.run_command([
str(python_exe), "-m", "pip", "install"
] + non_imgui_packages, check=False)
if ret != 0:
self.print_error(f"Failed to install core packages: {stderr}")
return False
# Install imgui separately — pin to 2.0.0 which has prebuilt wheels
# for Python 3.7-3.11 on Windows/Linux/macOS x86_64.
# --prefer-binary tells pip to use wheels when available, but allows
# compilation as a fallback (e.g., Python 3.12+ or ARM64).
print(" Installing imgui==2.0.0...")
ret_imgui, stdout_imgui, stderr_imgui = self.run_command([
str(python_exe), "-m", "pip", "install", "imgui==2.0.0",
"--prefer-binary"
], check=False)
if ret_imgui == 0:
self.print_success("imgui installed successfully")
else:
self.print_warning(f"imgui install failed: {stderr_imgui.strip()}")
# Retry without version pin in case a newer version has wheels
print(" Retrying with latest imgui version...")
ret_imgui2, _, stderr_imgui2 = self.run_command([