Skip to content

.NET SDK Setup Check

Verify the .NET SDK is installed and optionally auto-install it

Metadata

  • Author: ropean, Claude Sonnet (Anthropic)
  • Version: 1.0.0

Code

python
#!/usr/bin/env python3
"""
@title .NET SDK Setup Check
@description Verify the .NET SDK is installed and optionally auto-install it
@author ropean, Claude Sonnet (Anthropic)
@version 1.0.0

Verify that the .NET SDK is installed and accessible on PATH.
If `dotnet` is not found or the version check fails, prints actionable
installation instructions for every major platform and exits with code 1.
With --install, automatically downloads and installs the recommended SDK
version and configures the shell PATH.

Exit codes:
    0 - dotnet CLI is available
    1 - dotnet CLI is missing or broken

@example
Usage example:
    python dotnet-setup.py              # check only
    python dotnet-setup.py --install    # auto-install if missing
"""

import argparse
import os
import shutil
import subprocess
import sys

# ── Constants ──────────────────────────────────────────────────────────────────

RECOMMENDED_MAJOR = "8"

DOWNLOAD_URL = f"https://dotnet.microsoft.com/en-us/download/dotnet/{RECOMMENDED_MAJOR}.0"
PS_INSTALL_SCRIPT = "https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.ps1"
BASH_INSTALL_SCRIPT = "https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.sh"

# ── Colored output helpers ─────────────────────────────────────────────────────

_NO_COLOR = not sys.stdout.isatty()


def _colored(text: str, code: str) -> str:
    if _NO_COLOR:
        return text
    return f"\033[{code}m{text}\033[0m"


def green(text: str) -> str:
    return _colored(text, "32")


def yellow(text: str) -> str:
    return _colored(text, "33")


def red(text: str) -> str:
    return _colored(text, "31")


def cyan(text: str) -> str:
    return _colored(text, "36")


def dim(text: str) -> str:
    return _colored(text, "90")


# ── Core logic ─────────────────────────────────────────────────────────────────


def check_dotnet() -> bool:
    """Return True if `dotnet --version` succeeds."""
    if not shutil.which("dotnet"):
        return False
    try:
        result = subprocess.run(
            ["dotnet", "--version"],
            capture_output=True,
            text=True,
            timeout=15,
        )
        return result.returncode == 0 and result.stdout.strip() != ""
    except (OSError, subprocess.TimeoutExpired):
        return False


def get_dotnet_version() -> str:
    result = subprocess.run(
        ["dotnet", "--version"],
        capture_output=True,
        text=True,
        timeout=15,
    )
    return result.stdout.strip()


def print_install_instructions() -> None:
    is_windows = sys.platform == "win32"
    is_mac = sys.platform == "darwin"

    print()
    print(red("ERROR: 'dotnet' CLI is not installed or not on PATH."))
    print()
    print(f"  .NET {RECOMMENDED_MAJOR} (recommended) is required. Install using one of these methods:")
    print()

    idx = 1
    print(f"  {cyan(f'{idx}.')} Download installer:")
    print(f"     {DOWNLOAD_URL}")
    print()
    idx += 1

    if is_windows:
        print(f"  {cyan(f'{idx}.')} PowerShell:")
        print(f"     Invoke-WebRequest '{PS_INSTALL_SCRIPT}' -OutFile dotnet-install.ps1")
        print(f"     ./dotnet-install.ps1 -Channel {RECOMMENDED_MAJOR}.0")
        print()
        idx += 1

    print(f"  {cyan(f'{idx}.')} Bash:")
    print(f"     curl -fsSL {BASH_INSTALL_SCRIPT} | bash /dev/stdin --channel {RECOMMENDED_MAJOR}.0")
    print()
    idx += 1

    if is_mac:
        print(f"  {cyan(f'{idx}.')} Homebrew:")
        print(f"     brew install dotnet@{RECOMMENDED_MAJOR}")
        print()

    print(dim("  After installation, restart your terminal and re-run this script."))
    print()


# ── Auto-install ───────────────────────────────────────────────────────────────

DOTNET_INSTALL_DIR = os.path.join(os.path.expanduser("~"), ".dotnet")


def _shell_profile_path() -> str | None:
    """Return the most appropriate shell profile for the current user."""
    home = os.path.expanduser("~")
    shell = os.environ.get("SHELL", "")

    if "zsh" in shell:
        candidates = [".zshrc", ".zprofile"]
    elif "bash" in shell:
        candidates = [".bashrc", ".bash_profile", ".profile"]
    else:
        candidates = [".profile"]

    for name in candidates:
        path = os.path.join(home, name)
        if os.path.isfile(path):
            return path

    # Fallback: create the first candidate
    return os.path.join(home, candidates[0])


def _path_already_configured(profile_path: str) -> bool:
    """Check if .dotnet is already exported in the shell profile."""
    if not os.path.isfile(profile_path):
        return False
    try:
        with open(profile_path, encoding="utf-8", errors="replace") as f:
            content = f.read()
        return ".dotnet" in content and "PATH" in content
    except OSError:
        return False


def _add_path_to_profile(profile_path: str) -> bool:
    """Append dotnet PATH export to the shell profile. Return True on success."""
    export_block = (
        '\n# .NET SDK\n'
        f'export DOTNET_ROOT="{DOTNET_INSTALL_DIR}"\n'
        f'export PATH="$DOTNET_ROOT:$PATH"\n'
    )
    try:
        with open(profile_path, "a", encoding="utf-8") as f:
            f.write(export_block)
        return True
    except OSError as e:
        print(red(f"  Failed to update {profile_path}: {e}"))
        return False


def install_dotnet() -> int:
    """Download and install .NET SDK, then configure PATH."""
    is_windows = sys.platform == "win32"

    print()
    print(cyan(f"  Installing .NET {RECOMMENDED_MAJOR}.0 SDK..."))
    print()

    if is_windows:
        # Download and run PowerShell install script
        ps_local = os.path.join(os.environ.get("TEMP", "."), "dotnet-install.ps1")
        print(dim(f"  Downloading install script to {ps_local}..."))
        dl = subprocess.run(
            ["powershell", "-Command",
             f"Invoke-WebRequest '{PS_INSTALL_SCRIPT}' -OutFile '{ps_local}' -UseBasicParsing"],
            capture_output=True, text=True, timeout=60,
        )
        if dl.returncode != 0:
            print(red(f"  Failed to download install script: {dl.stderr.strip()}"))
            return 1

        print(dim(f"  Running install script (channel {RECOMMENDED_MAJOR}.0)..."))
        install = subprocess.run(
            ["powershell", "-ExecutionPolicy", "Bypass", "-File", ps_local,
             "-Channel", f"{RECOMMENDED_MAJOR}.0"],
            timeout=300,
        )
    else:
        # Download and run Bash install script
        print(dim(f"  Downloading and running install script (channel {RECOMMENDED_MAJOR}.0)..."))
        curl = subprocess.run(
            ["curl", "-fsSL", BASH_INSTALL_SCRIPT],
            capture_output=True, timeout=60,
        )
        if curl.returncode != 0:
            print(red("  Failed to download install script."))
            return 1

        install = subprocess.run(
            ["bash", "/dev/stdin", "--channel", f"{RECOMMENDED_MAJOR}.0"],
            input=curl.stdout, timeout=300,
        )

    if install.returncode != 0:
        print(red("  Installation failed. See output above."))
        return 1

    # Verify the binary exists
    dotnet_bin = os.path.join(DOTNET_INSTALL_DIR, "dotnet")
    if is_windows:
        dotnet_bin += ".exe"
    if not os.path.isfile(dotnet_bin):
        print(red(f"  Expected binary not found: {dotnet_bin}"))
        return 1

    # Get installed version
    ver_result = subprocess.run(
        [dotnet_bin, "--version"], capture_output=True, text=True, timeout=15,
    )
    version = ver_result.stdout.strip() if ver_result.returncode == 0 else "unknown"
    print(green(f"  Installed .NET SDK v{version} to {DOTNET_INSTALL_DIR}"))

    # Configure PATH
    if is_windows:
        print()
        print(yellow("  To add dotnet to your PATH permanently on Windows, run:"))
        print(f'     [Environment]::SetEnvironmentVariable("PATH", "{DOTNET_INSTALL_DIR};$env:PATH", "User")')
        print()
    else:
        profile = _shell_profile_path()
        if profile and not _path_already_configured(profile):
            if _add_path_to_profile(profile):
                print(green(f"  Added dotnet to PATH in {profile}"))
                print(dim("  Run the following to apply now, or restart your terminal:"))
                print(dim(f"    source {profile}"))
            else:
                print(yellow(f"  Could not update {profile}. Add manually:"))
                print(f'     export PATH="{DOTNET_INSTALL_DIR}:$PATH"')
        elif profile:
            print(dim(f"  PATH already configured in {profile}"))
        else:
            print(yellow("  Add this to your shell profile:"))
            print(f'     export PATH="{DOTNET_INSTALL_DIR}:$PATH"')

    print()
    return 0


# ── Main ───────────────────────────────────────────────────────────────────────


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Check (and optionally install) the .NET SDK.",
    )
    parser.add_argument(
        "--install", action="store_true",
        help=f"Auto-install .NET {RECOMMENDED_MAJOR} SDK and configure PATH",
    )
    return parser.parse_args()


def main() -> int:
    args = parse_args()

    print()
    print(cyan("=== .NET SDK Setup Check ==="))
    print()

    if check_dotnet():
        version = get_dotnet_version()
        print(green(f"  dotnet CLI is available: v{version}"))

        major = version.split(".")[0]
        if major != RECOMMENDED_MAJOR:
            print(yellow(f"  Note: Recommended version is .NET {RECOMMENDED_MAJOR}.x, you have {version}."))
            print(yellow(f"  This may still work, but consider upgrading: {DOWNLOAD_URL}"))

        print()
        return 0

    if args.install:
        return install_dotnet()

    print_install_instructions()
    print(dim(f"  Or run this script with --install to auto-install .NET {RECOMMENDED_MAJOR}:"))
    print(dim(f"    python3 {os.path.basename(__file__)} --install"))
    print()
    return 1


if __name__ == "__main__":
    sys.exit(main())

File Information

  • Filename: dotnet-setup.py
  • Category: python
  • Language: PYTHON

View on GitHub