304 lines
9.5 KiB
Python
304 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
# Copyright (C) 2025 ComfyUI-GeometryPack Contributors
|
|
|
|
"""
|
|
Blender Installer for ComfyUI-GeometryPack
|
|
|
|
Downloads and installs Blender for UV unwrapping and remeshing nodes.
|
|
Run this script manually if you need Blender functionality.
|
|
|
|
Usage:
|
|
python blender_install.py
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import platform
|
|
import urllib.request
|
|
import tarfile
|
|
import zipfile
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
# Try to import optimized libraries, fallback to basic if not available
|
|
try:
|
|
import requests
|
|
HAS_REQUESTS = True
|
|
except ImportError:
|
|
HAS_REQUESTS = False
|
|
|
|
try:
|
|
from tqdm import tqdm
|
|
HAS_TQDM = True
|
|
except ImportError:
|
|
HAS_TQDM = False
|
|
|
|
|
|
def get_platform_info():
|
|
"""Detect current platform and architecture."""
|
|
system = platform.system().lower()
|
|
machine = platform.machine().lower()
|
|
|
|
if system == "darwin":
|
|
plat = "macos"
|
|
arch = "arm64" if machine == "arm64" else "x64"
|
|
elif system == "linux":
|
|
plat = "linux"
|
|
arch = "x64"
|
|
elif system == "windows":
|
|
plat = "windows"
|
|
arch = "x64"
|
|
else:
|
|
plat = None
|
|
arch = None
|
|
|
|
return plat, arch
|
|
|
|
|
|
def get_blender_download_url(platform_name, architecture):
|
|
"""
|
|
Get Blender 4.2 LTS download URL for the platform.
|
|
|
|
Returns:
|
|
tuple: (download_url, version, filename) or (None, None, None) if not found
|
|
"""
|
|
version = "4.2.3"
|
|
base_url = "https://download.blender.org/release/Blender4.2"
|
|
|
|
urls = {
|
|
("linux", "x64"): (
|
|
f"{base_url}/blender-{version}-linux-x64.tar.xz",
|
|
version,
|
|
f"blender-{version}-linux-x64.tar.xz"
|
|
),
|
|
("macos", "x64"): (
|
|
f"{base_url}/blender-{version}-macos-x64.dmg",
|
|
version,
|
|
f"blender-{version}-macos-x64.dmg"
|
|
),
|
|
("macos", "arm64"): (
|
|
f"{base_url}/blender-{version}-macos-arm64.dmg",
|
|
version,
|
|
f"blender-{version}-macos-arm64.dmg"
|
|
),
|
|
("windows", "x64"): (
|
|
f"{base_url}/blender-{version}-windows-x64.zip",
|
|
version,
|
|
f"blender-{version}-windows-x64.zip"
|
|
),
|
|
}
|
|
|
|
key = (platform_name, architecture)
|
|
if key in urls:
|
|
url, ver, filename = urls[key]
|
|
print(f"[Blender] Using Blender {ver} for {platform_name}-{architecture}")
|
|
return url, ver, filename
|
|
|
|
return None, None, None
|
|
|
|
|
|
def download_file_optimized(url, dest_path):
|
|
"""Download file with requests and tqdm for better performance and progress."""
|
|
print(f"[Blender] Downloading: {url}")
|
|
print(f"[Blender] Destination: {dest_path}")
|
|
|
|
try:
|
|
response = requests.get(url, stream=True, timeout=30)
|
|
response.raise_for_status()
|
|
|
|
total_size = int(response.headers.get('content-length', 0))
|
|
block_size = 8192
|
|
|
|
if HAS_TQDM:
|
|
with tqdm(total=total_size, unit='B', unit_scale=True, unit_divisor=1024) as pbar:
|
|
with open(dest_path, 'wb') as f:
|
|
for chunk in response.iter_content(chunk_size=block_size):
|
|
if chunk:
|
|
f.write(chunk)
|
|
pbar.update(len(chunk))
|
|
else:
|
|
downloaded = 0
|
|
with open(dest_path, 'wb') as f:
|
|
for chunk in response.iter_content(chunk_size=block_size):
|
|
if chunk:
|
|
f.write(chunk)
|
|
downloaded += len(chunk)
|
|
if total_size > 0:
|
|
percent = int(downloaded * 100 / total_size)
|
|
sys.stdout.write(f"\r[Blender] Progress: {percent}%")
|
|
sys.stdout.flush()
|
|
sys.stdout.write("\n")
|
|
|
|
print("[Blender] Download complete!")
|
|
return True
|
|
except Exception as e:
|
|
print(f"\n[Blender] Error downloading: {e}")
|
|
return False
|
|
|
|
|
|
def download_file(url, dest_path):
|
|
"""Download file with progress."""
|
|
if HAS_REQUESTS:
|
|
return download_file_optimized(url, dest_path)
|
|
|
|
print(f"[Blender] Downloading: {url}")
|
|
print(f"[Blender] Destination: {dest_path}")
|
|
|
|
def progress_hook(count, block_size, total_size):
|
|
percent = int(count * block_size * 100 / total_size)
|
|
sys.stdout.write(f"\r[Blender] Progress: {percent}%")
|
|
sys.stdout.flush()
|
|
|
|
try:
|
|
urllib.request.urlretrieve(url, dest_path, progress_hook)
|
|
sys.stdout.write("\n")
|
|
print("[Blender] Download complete!")
|
|
return True
|
|
except Exception as e:
|
|
print(f"\n[Blender] Error downloading: {e}")
|
|
return False
|
|
|
|
|
|
def extract_archive(archive_path, extract_to):
|
|
"""Extract tar.gz, tar.xz, zip, or handle DMG (macOS) - sequential only."""
|
|
print(f"[Blender] Extracting: {archive_path}")
|
|
|
|
try:
|
|
if archive_path.endswith('.tar.xz'):
|
|
print("[Blender] Extracting .tar.xz archive...")
|
|
with tarfile.open(archive_path, 'r:*') as tar:
|
|
if hasattr(tarfile, 'data_filter'):
|
|
tar.extractall(extract_to, filter='data')
|
|
else:
|
|
tar.extractall(extract_to)
|
|
|
|
elif archive_path.endswith(('.tar.gz', '.tar.bz2')):
|
|
print("[Blender] Extracting tar archive...")
|
|
with tarfile.open(archive_path, 'r:*') as tar:
|
|
if hasattr(tarfile, 'data_filter'):
|
|
tar.extractall(extract_to, filter='data')
|
|
else:
|
|
tar.extractall(extract_to)
|
|
|
|
elif archive_path.endswith('.zip'):
|
|
print("[Blender] Extracting .zip archive (this may take a moment)...")
|
|
with zipfile.ZipFile(archive_path, 'r') as zip_ref:
|
|
zip_ref.extractall(extract_to)
|
|
|
|
elif archive_path.endswith('.dmg'):
|
|
print("[Blender] DMG detected - mounting disk image...")
|
|
|
|
mount_result = subprocess.run(
|
|
['hdiutil', 'attach', '-nobrowse', archive_path],
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
|
|
if mount_result.returncode != 0:
|
|
print(f"[Blender] Error mounting DMG: {mount_result.stderr}")
|
|
return False
|
|
|
|
mount_point = None
|
|
for line in mount_result.stdout.split('\n'):
|
|
if '/Volumes/' in line:
|
|
mount_point = line.split('\t')[-1].strip()
|
|
break
|
|
|
|
if not mount_point:
|
|
print("[Blender] Error: Could not find mount point")
|
|
return False
|
|
|
|
try:
|
|
blender_app = Path(mount_point) / "Blender.app"
|
|
if blender_app.exists():
|
|
dest_app = Path(extract_to) / "Blender.app"
|
|
shutil.copytree(blender_app, dest_app)
|
|
print(f"[Blender] Copied Blender.app to: {dest_app}")
|
|
else:
|
|
print(f"[Blender] Error: Blender.app not found in {mount_point}")
|
|
return False
|
|
finally:
|
|
subprocess.run(['hdiutil', 'detach', mount_point], check=False)
|
|
|
|
else:
|
|
print(f"[Blender] Error: Unknown archive format: {archive_path}")
|
|
return False
|
|
|
|
print(f"[Blender] Extraction complete!")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"[Blender] Error extracting: {e}")
|
|
return False
|
|
|
|
|
|
def main():
|
|
"""Main installation function."""
|
|
print("\n" + "="*60)
|
|
print("ComfyUI-GeometryPack: Blender Installation")
|
|
print("="*60 + "\n")
|
|
|
|
script_dir = Path(__file__).parent.absolute()
|
|
blender_dir = script_dir / "_blender"
|
|
|
|
if blender_dir.exists():
|
|
print("[Blender] Blender directory already exists at:")
|
|
print(f"[Blender] {blender_dir}")
|
|
print("[Blender] Skipping download. Delete '_blender/' folder to reinstall.")
|
|
return True
|
|
|
|
plat, arch = get_platform_info()
|
|
if not plat or not arch:
|
|
print("[Blender] Error: Could not detect platform")
|
|
print("[Blender] Please install Blender manually from: https://www.blender.org/download/")
|
|
return False
|
|
|
|
print(f"[Blender] Detected platform: {plat}-{arch}")
|
|
|
|
url, version, filename = get_blender_download_url(plat, arch)
|
|
if not url:
|
|
print("[Blender] Error: Could not find Blender download for your platform")
|
|
print("[Blender] Please install Blender manually from: https://www.blender.org/download/")
|
|
return False
|
|
|
|
temp_dir = script_dir / "_temp_download"
|
|
temp_dir.mkdir(exist_ok=True)
|
|
|
|
try:
|
|
download_path = temp_dir / filename
|
|
if not download_file(url, str(download_path)):
|
|
return False
|
|
|
|
blender_dir.mkdir(exist_ok=True)
|
|
if not extract_archive(str(download_path), str(blender_dir)):
|
|
return False
|
|
|
|
print("\n[Blender] Installation complete!")
|
|
print(f"[Blender] Location: {blender_dir}")
|
|
|
|
if plat == "windows":
|
|
blender_exe = list(blender_dir.rglob("blender.exe"))
|
|
else:
|
|
blender_exe = list(blender_dir.rglob("blender"))
|
|
|
|
if blender_exe:
|
|
print(f"[Blender] Blender executable: {blender_exe[0]}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"\n[Blender] Error during installation: {e}")
|
|
return False
|
|
|
|
finally:
|
|
if temp_dir.exists():
|
|
print("[Blender] Cleaning up temporary files...")
|
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
success = main()
|
|
sys.exit(0 if success else 1)
|