Skip to content

Environment Setup

environment

Python environment and configuration provisioning — Steps 3-4.

Creates the Python virtual environment and copies the minimum configuration files needed for the rest of the install:

  • venv creation (Step 3): tries uv first (auto-downloads Python 3.11-3.13), falls back to system Python.
  • Provisioning (Step 4): copies dependencies.json and the model catalog to the install directory.

setup_environment(install_path, install_type, log)

Create the Python virtual environment.

Strategy (in order):

  1. uv venv with Python >=3.11,<3.14 auto-managed.
  2. System conda (Miniconda/Anaconda) with a local prefix.
  3. System Python 3.11-3.13 (detected via platform abstraction).
  4. Auto-install Python on Windows (if user agrees).

After creation, verifies the expected python executable exists inside the venv.

Parameters:

Name Type Description Default
install_path Path

Root installation directory.

required
install_type InstallType

:attr:InstallType.VENV or :attr:InstallType.CONDA.

required
log InstallerLogger

Installer logger for user-facing messages.

required

Returns:

Type Description
Path

Absolute path to the Python executable inside the environment.

Raises:

Type Description
InstallerFatalError

If no usable Python 3.11+ can be found or created.

Source code in src/installer/environment.py
def setup_environment(
    install_path: Path,
    install_type: InstallType,
    log: InstallerLogger,
) -> Path:
    """Create the Python virtual environment.

    Strategy (in order):

    1. ``uv venv`` with Python >=3.11,<3.14 auto-managed.
    2. System conda (Miniconda/Anaconda) with a local prefix.
    3. System Python 3.11-3.13 (detected via platform abstraction).
    4. Auto-install Python on Windows (if user agrees).

    After creation, verifies the expected ``python`` executable
    exists inside the venv.

    Args:
        install_path: Root installation directory.
        install_type: :attr:`InstallType.VENV` or
            :attr:`InstallType.CONDA`.
        log: Installer logger for user-facing messages.

    Returns:
        Absolute path to the Python executable inside the environment.

    Raises:
        InstallerFatalError: If no usable Python 3.11+ can be found or created.
    """
    scripts_dir = install_path / "scripts"
    scripts_dir.mkdir(parents=True, exist_ok=True)

    log.item(f"Install path: {install_path}")

    if install_type is InstallType.VENV:
        venv_path = scripts_dir / "venv"

        if venv_path.exists():
            log.sub("Virtual environment already exists.", style="success")
        else:
            # Find uv: PATH first, then local scripts/uv/
            uv_cmd: str | Path | None = None
            if check_command_exists("uv"):
                uv_cmd = "uv"
            else:
                local_uv = scripts_dir / "uv" / ("uv.exe" if sys.platform == "win32" else "uv")
                if local_uv.exists():
                    uv_cmd = local_uv

            if uv_cmd is not None:
                log.item("Creating Python environment...")
                _create_venv_with_uv(uv_cmd, venv_path, log)

            # Fallback: use system Python if uv didn't create the venv
            if not venv_path.exists():
                platform = get_platform()
                python_path = None
                for try_version in ("3.13", "3.12", "3.11"):
                    python_path = platform.detect_python(try_version)
                    if python_path:
                        break

                if python_path is None:
                    log.error("Python 3.11+ is required but could not be acquired.")
                    log.item("If 'uv' failed, this may be due to network or Antivirus restrictions.")
                    log.item("Please install Python 3.11-3.13 from https://www.python.org/downloads/")
                    raise InstallerFatalError("Python 3.11+ is required but could not be acquired.")

                log.item(f"Creating venv with {python_path}...")
                run_and_log(str(python_path), ["-m", "venv", str(venv_path)])
                log.sub("Virtual environment created.", style="success")

        # Return the venv python and verify it exists
        if sys.platform == "win32":
            python_exe = venv_path / "Scripts" / "python.exe"
        else:
            python_exe = venv_path / "bin" / "python"

        if not python_exe.exists():
            log.error(f"Venv python not found at expected path: {python_exe}")
            log.item(f"Venv directory: {venv_path}")
            raise InstallerFatalError(f"Venv python not found at expected path: {python_exe}")

        log.sub(f"Venv python: {python_exe}", style="success")
        return python_exe

    elif install_type is InstallType.CONDA:
        conda_env_path = scripts_dir / "conda_env"

        # Determine Python executable path based on OS
        if sys.platform == "win32":
            python_exe = conda_env_path / "python.exe"
        else:
            python_exe = conda_env_path / "bin" / "python"

        if conda_env_path.exists() and python_exe.exists():
            log.sub("Conda environment already exists.", style="success")
            log.sub(f"Conda python: {python_exe}", style="success")
            return python_exe

        # Find or install Conda
        conda_exe = _find_conda(log)
        if not conda_exe:
            if sys.platform == "win32" and confirm("Conda not found. Install Miniconda automatically?"):
                conda_exe = _install_miniconda_windows(log)

            if not conda_exe:
                log.error("Conda is required for this installation type.")
                log.item("Please install Miniconda from https://docs.anaconda.com/free/miniconda/")
                raise InstallerFatalError("Conda is required for this installation type.")

        # Provision the environment.yml first so conda can use it
        provision_scripts(install_path, log)
        env_yml = scripts_dir / "environment.yml"

        if not env_yml.exists():
            log.error(f"environment.yml not found at {env_yml}")
            raise InstallerFatalError(f"environment.yml not found at {env_yml}")

        log.item(f"Creating local Conda environment at {conda_env_path}...")
        try:
            run_and_log(
                str(conda_exe),
                ["env", "create", "-p", str(conda_env_path), "-f", str(env_yml), "-y"]
            )
            log.sub("Conda environment created.", style="success")
        except CommandError:
            log.error("Failed to create Conda environment.")
            raise InstallerFatalError("Failed to create Conda environment.") from None

        if not python_exe.exists():
            log.error(f"Conda python not found at expected path: {python_exe}")
            raise InstallerFatalError(f"Conda python not found at expected path: {python_exe}")

        log.sub(f"Conda python: {python_exe}", style="success")
        return python_exe

    else:
        raise InstallerFatalError(f"Unknown install type: {install_type}")

find_source_scripts()

Locate the source scripts/ directory containing config files.

Searches relative to this package: ../../scripts/ from environment.py. Falls back to Path.cwd() / "scripts" to support installed environments (like CI).

Raises:

Type Description
FileNotFoundError

If the scripts directory or dependencies.json is missing (enforces package integrity).

Source code in src/installer/environment.py
def find_source_scripts() -> Path:
    """Locate the source ``scripts/`` directory containing config files.

    Searches relative to this package: ``../../scripts/`` from ``environment.py``.
    Falls back to ``Path.cwd() / "scripts"`` to support installed environments (like CI).

    Raises:
        FileNotFoundError: If the scripts directory or dependencies.json
            is missing (enforces package integrity).
    """
    package_root = Path(__file__).resolve().parent.parent.parent
    candidate = package_root / "scripts"

    if candidate.exists() and (candidate / "dependencies.json").exists():
        return candidate

    cwd_candidate = Path.cwd() / "scripts"
    if cwd_candidate.exists() and (cwd_candidate / "dependencies.json").exists():
        return cwd_candidate

    raise FileNotFoundError(
        f"Crucial source directory missing: {candidate}. "
        "Ensure the installer is not separated from its 'scripts' directory."
    )

provision_scripts(install_path, log)

Copy bootstrap config files to the install directory.

Only copies files listed in BOOTSTRAP_FILES (currently just dependencies.json). Other configs like custom_nodes.json are resolved on-demand by later steps from the source directory.

Also copies model_manifest.json to install_path/scripts/ so the model downloader can find it.

Parameters:

Name Type Description Default
install_path Path

Root installation directory.

required
log InstallerLogger

Installer logger for user-facing messages.

required
Source code in src/installer/environment.py
def provision_scripts(install_path: Path, log: InstallerLogger) -> None:
    """Copy bootstrap config files to the install directory.

    Only copies files listed in ``BOOTSTRAP_FILES`` (currently just
    ``dependencies.json``). Other configs like ``custom_nodes.json``
    are resolved on-demand by later steps from the source directory.

    Also copies ``model_manifest.json`` to ``install_path/scripts/``
    so the model downloader can find it.

    Args:
        install_path: Root installation directory.
        log: Installer logger for user-facing messages.
    """

    try:
        source_dir = find_source_scripts()
    except FileNotFoundError as e:
        log.error(str(e))
        raise InstallerFatalError(str(e)) from None

    dest_dir = install_path / "scripts"
    dest_dir.mkdir(parents=True, exist_ok=True)

    copied = 0
    for filename in BOOTSTRAP_FILES:
        src = source_dir / filename
        dst = dest_dir / filename

        if not src.exists():
            log.sub(f"  {filename}: [dim]not in source (skipped)[/dim]")
            continue

        if dst.exists():
            # Update if source is newer
            if src.stat().st_mtime > dst.stat().st_mtime:
                shutil.copy2(src, dst)
                log.sub(f"  {filename}: updated", style="cyan")
                copied += 1
            else:
                log.sub(f"  {filename}: already up to date", style="success")
        else:
            shutil.copy2(src, dst)
            log.sub(f"  {filename}: copied", style="success")
            copied += 1

    # Download model_manifest.json from Assets repo (HF primary, ModelScope fallback)
    _provision_bundles_manifest(dest_dir, log)

    log.item(f"{copied} config file(s) provisioned.")