Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions sd_installer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
python -m sd_installer install --cuda cu128 # Install with specific CUDA
python -m sd_installer verify # Verify existing installation
python -m sd_installer diagnose # Detailed diagnostics
python -m sd_installer report # Write a diagnostic report on demand
python -m sd_installer repair # Auto-fix known issues
python -m sd_installer generate-bat # Generate standalone batch file
python -m sd_installer install-tensorrt # Install TensorRT packages
Expand Down Expand Up @@ -202,6 +203,50 @@ def cmd_diagnose(args):
return 0


def cmd_report(args):
"""Generate a diagnostic report on demand (no failure required)."""
from .report import write_error_report

try:
base = Path(args.base_folder) if args.base_folder else find_base_folder()
except RuntimeError as e:
print(f"ERROR: {e}")
return 1

# Find Python executable in venv
venv_path = base / "venv"
if sys.platform == "win32":
python_exe = venv_path / "Scripts" / "python.exe"
else:
python_exe = venv_path / "bin" / "python"

if not python_exe.exists():
print(f"ERROR: Virtual environment not found at {venv_path}")
return 1

print("Generating StreamDiffusionTD Diagnostic Report")
print("=" * 40)

out_dir = Path(args.output) if args.output else base / "error_reports"
report_path = write_error_report(
out_dir,
{
"stage": "installation",
"phase": "manual",
"python_exe": str(python_exe),
"base_folder": str(base),
"venv_path": str(venv_path),
},
)

if report_path:
print(f"\nReport written to: {report_path}")
return 0

print("ERROR: Failed to write report.")
return 1


def cmd_repair(args):
"""Auto-fix known issues."""
from .verifier import Verifier
Expand Down Expand Up @@ -384,6 +429,14 @@ def main():
diagnose_parser = subparsers.add_parser("diagnose", help="Run detailed diagnostics")
diagnose_parser.set_defaults(func=cmd_diagnose)

# report command
report_parser = subparsers.add_parser("report", help="Generate a diagnostic report on demand")
report_parser.add_argument(
"--output",
help="Directory to write the report into (default: base folder/error_reports)",
)
report_parser.set_defaults(func=cmd_report)

# repair command
repair_parser = subparsers.add_parser("repair", help="Auto-fix known issues")
repair_parser.add_argument(
Expand Down
90 changes: 76 additions & 14 deletions sd_installer/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ def __init__(

self.pytorch_config = PYTORCH_CONFIGS[cuda_version]

# Populated during install() for the on-failure diagnostic report (see report.py).
self.current_phase: Optional[str] = None
self._last_pip_stderr: Optional[str] = None

@property
def python_exe(self) -> Path:
"""Path to Python executable in venv."""
Expand Down Expand Up @@ -154,6 +158,7 @@ def _run_pip(self, args: list, check: bool = True, cwd: Optional[Path] = None) -
)
if check and result.returncode != 0:
print(f" STDERR: {result.stderr}")
self._last_pip_stderr = result.stderr
raise RuntimeError(f"pip failed: {result.stderr}")
return result

Expand Down Expand Up @@ -299,7 +304,8 @@ def phase4b_cuda_link(self):
print(" CUDA-IPC zero-copy export will fall back to the mirror-DAT transport")

def phase4c_cuda_link_env(self):
"""Phase 4c: Persist CUDALINK_LIB_PATH and CUDALINK_DOORBELL (Windows only).
"""Phase 4c: Persist CUDALINK_LIB_PATH, CUDALINK_DOORBELL, and SDTD_BASE_FOLDER_PATH
(Windows only).

CUDALINK_LIB_PATH -> this venv's site-packages:
TouchDesigner's CUDALinkBootstrap.py reads CUDALINK_LIB_PATH at Text DAT import time to
Expand All @@ -318,11 +324,19 @@ def phase4c_cuda_link_env(self):
must be persisted here instead. CUDALINK_WAIT_BACKEND is deliberately left unset -- its
default "auto" already selects the native path.

SDTD_BASE_FOLDER_PATH -> this install's base_folder (StreamDiffusion repo root):
Same cross-process problem as CUDALINK_DOORBELL -- TD's Python process needs a reliable
anchor to the repo root for the inference-side error-report dump
(streamdiffusion.utils.diagnostics.write_error_report), and a runtime os.environ.setdefault
in td_manager.py can't reach that separate process. Persisting it here means every
TD-launched Python inherits it without a manual env-var step.

setx writes to HKCU\\Environment (user scope) and only affects processes started
*after* it runs, so TD must be (re)started after installation to pick it up. This
intentionally overwrites any prior manual value (e.g. an older cuda_link_lib\\ target).
Non-fatal: if setx fails or this isn't Windows, TD simply falls back to the mirror-DAT
classic mode (for CUDALINK_LIB_PATH) or the poll-sleep wait backend (for CUDALINK_DOORBELL).
classic mode (for CUDALINK_LIB_PATH), the poll-sleep wait backend (for CUDALINK_DOORBELL),
or the diagnostics module's own __file__-relative fallback (for SDTD_BASE_FOLDER_PATH).
"""
if sys.platform != "win32":
return # setx is a Windows-only mechanism; non-Windows TD launches are unaffected
Expand Down Expand Up @@ -356,6 +370,17 @@ def phase4c_cuda_link_env(self):
else:
print(" CUDALINK_DOORBELL=1 persisted (enables doorbell/native-wait IPC fast path).")

# SDTD_BASE_FOLDER_PATH -> repo root, so TD's Python process can locate error_reports/
# without a manual env-var step. Independent of the blocks above, so it runs even if
# either warned.
base_result = subprocess.run(
["setx", "SDTD_BASE_FOLDER_PATH", str(self.base_folder)], capture_output=True, text=True
)
if base_result.returncode != 0:
print(f" WARNING: setx failed to persist SDTD_BASE_FOLDER_PATH: {base_result.stderr.strip()}")
else:
print(f" SDTD_BASE_FOLDER_PATH={self.base_folder} persisted (anchors error-report dumps).")

def phase5_missing_pins(self):
"""Phase 5: Install packages not pinned in setup.py and fix diffusers."""
self._report_progress("Installing packages not in setup.py (timm, python-osc, peft)...", 5, 8)
Expand Down Expand Up @@ -411,6 +436,31 @@ def phase8_verify(self) -> bool:
verifier = Verifier(str(self.python_exe))
return verifier.run_all()

def _write_install_error_report(self, exc: BaseException) -> None:
"""Best-effort diagnostic dump on install failure. Never raises -- a bug here must
not mask the real installation error, which the caller re-raises regardless."""
try:
from .report import write_error_report

report_path = write_error_report(
self.base_folder / "error_reports",
{
"stage": "installation",
"exc": exc,
"phase": self.current_phase,
"python_exe": str(self.python_exe),
"base_folder": str(self.base_folder),
"cuda_version": self.cuda_version,
"pytorch_config": self.pytorch_config,
"venv_path": str(self.venv_path),
"pip_stderr": self._last_pip_stderr,
},
)
if report_path:
print(f"\n Error report written to: {report_path}")
except Exception as report_exc:
print(f" WARNING: Failed to generate error report: {report_exc}")

def install(self, python_exe: Optional[str] = None) -> bool:
"""
Run full installation.
Expand All @@ -433,18 +483,30 @@ def install(self, python_exe: Optional[str] = None) -> bool:
# Create venv if needed
self.create_venv(python_exe)

# Run installation phases
self.phase1_foundation()
self.phase2_pytorch()
self.phase3_xformers()
self.phase3b_insightface() # Pre-install insightface from wheel (Windows)
self.phase4_streamdiffusion()
self.phase4b_cuda_link() # Pre-install cuda-link from wheel (CUDA-IPC transport)
self.phase4c_cuda_link_env() # Persist CUDALINK_LIB_PATH -> venv (TD library mode)
self.phase5_missing_pins()
self.phase6_conflict_prone()
self.phase7_numpy_lock()
success = self.phase8_verify()
# Run installation phases. current_phase is tracked so a failure report (see
# _write_install_error_report) can name the phase that was running when it broke.
phases = [
("phase1_foundation", self.phase1_foundation),
("phase2_pytorch", self.phase2_pytorch),
("phase3_xformers", self.phase3_xformers),
("phase3b_insightface", self.phase3b_insightface), # insightface from wheel (Windows)
("phase4_streamdiffusion", self.phase4_streamdiffusion),
("phase4b_cuda_link", self.phase4b_cuda_link), # cuda-link from wheel (CUDA-IPC transport)
("phase4c_cuda_link_env", self.phase4c_cuda_link_env), # CUDALINK_LIB_PATH -> venv (TD library mode)
("phase5_missing_pins", self.phase5_missing_pins),
("phase6_conflict_prone", self.phase6_conflict_prone),
("phase7_numpy_lock", self.phase7_numpy_lock),
]

try:
for name, phase_fn in phases:
self.current_phase = name
phase_fn()
self.current_phase = "phase8_verify"
success = self.phase8_verify()
except Exception as exc:
self._write_install_error_report(exc)
raise

print()
print("=" * 50)
Expand Down
Loading
Loading