<# .SYNOPSIS Detects and installs the tools needed to build and develop YMhut Box on Windows. The script is intentionally standalone: it only changes machine/tool caches and dependency directories (node_modules, NuGet, Go modules, and Cargo), not source code. #> [CmdletBinding()] param( [switch] $SkipMachineTools, [switch] $SkipRestore, [switch] $SkipWebView2, [switch] $SkipVisualStudio, [switch] $NoPrompt ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $Root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path $NuGetConfig = Join-Path $Root 'NuGet.Config' function Write-Step([string] $Message) { Write-Host ([Environment]::NewLine + '==> ' + $Message) -ForegroundColor Cyan } function Write-Ok([string] $Message) { Write-Host (' [OK] ' + $Message) -ForegroundColor Green } function Write-Info([string] $Message) { Write-Host (' [INFO] ' + $Message) -ForegroundColor DarkGray } function Get-CommandPath([string] $Name) { $command = Get-Command $Name -ErrorAction SilentlyContinue if ($command) { return $command.Source } return $null } function Refresh-ProcessPath { $machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine') $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if ($machinePath -or $userPath) { $env:Path = (($machinePath, $userPath) -join ';') } } function Invoke-External([string] $FilePath, [string[]] $Arguments, [string] $FailureMessage) { & $FilePath @Arguments if ($LASTEXITCODE -ne 0) { throw ($FailureMessage + ' (exit code ' + $LASTEXITCODE + ').') } } function Confirm-Install([string] $Label) { if ($NoPrompt) { return $true } $answer = Read-Host ('Install ' + $Label + ' now? [Y/n]') return [string]::IsNullOrWhiteSpace($answer) -or $answer -match '^(?i)y(es)?$' } function Install-WingetPackage([string] $Id, [string] $Label, [string[]] $ExtraArguments = @()) { if (-not (Get-CommandPath 'winget.exe')) { throw ('winget.exe is required to download ' + $Label + '. Install App Installer from Microsoft Store, then rerun this script.') } if (-not (Confirm-Install $Label)) { throw ('Installation of ' + $Label + ' was declined.') } $arguments = @('install', '--id', $Id, '--exact', '--source', 'winget', '--accept-source-agreements', '--accept-package-agreements', '--silent') + $ExtraArguments Write-Info ('Downloading/installing ' + $Label + ' (' + $Id + ') via winget...') Invoke-External 'winget.exe' $arguments ('Unable to install ' + $Label) } function Ensure-WingetPackage([string] $CommandName, [string] $Id, [string] $Label, [string[]] $ExtraArguments = @()) { $path = Get-CommandPath $CommandName if ($path) { Write-Ok ($Label + ' found at ' + $path); return $path } if ($SkipMachineTools) { throw ($Label + ' is missing and -SkipMachineTools was specified.') } Install-WingetPackage $Id $Label $ExtraArguments Refresh-ProcessPath $path = Get-CommandPath $CommandName if (-not $path) { throw ($Label + ' was installed, but ' + $CommandName + ' is still not available in PATH. Open a new terminal and rerun.') } Write-Ok ($Label + ' installed at ' + $path) return $path } function Ensure-DotNet10 { $dotnet = Get-CommandPath 'dotnet.exe' $hasNet10 = $false if ($dotnet) { $sdkLines = & $dotnet '--list-sdks' 2>$null $hasNet10 = @($sdkLines | Where-Object { $_ -match '^10\.\d+\.\d+' }).Count -gt 0 } if (-not $hasNet10) { if ($SkipMachineTools) { throw '.NET 10 SDK is missing and -SkipMachineTools was specified.' } Install-WingetPackage 'Microsoft.DotNet.SDK.10' '.NET 10 SDK' $dotnet = Get-CommandPath 'dotnet.exe' if (-not $dotnet) { throw '.NET 10 SDK was installed, but dotnet.exe is not available in PATH. Open a new terminal and rerun.' } } Write-Ok ('.NET 10 SDK found at ' + $dotnet) return $dotnet } function Ensure-DotNetWorkload([string] $DotNet, [string] $Workload) { $installed = & $DotNet 'workload' 'list' 2>$null if (@($installed | Where-Object { $_ -match ('^\s*' + [regex]::Escape($Workload) + '\s') }).Count -gt 0) { Write-Ok ('.NET workload ' + $Workload + ' already installed') return } if ($SkipMachineTools) { throw ('.NET workload ' + $Workload + ' is missing and -SkipMachineTools was specified.') } if (-not (Confirm-Install ('.NET workload ' + $Workload))) { throw ('Installation of .NET workload ' + $Workload + ' was declined.') } Invoke-External $DotNet @('workload', 'install', $Workload, '--skip-manifest-update') ('Unable to install .NET workload ' + $Workload) } function Ensure-Go125 { $go = Get-CommandPath 'go.exe' $hasGo125 = $false if ($go) { $version = (& $go 'version' 2>$null) -join '' $hasGo125 = $version -match '\bgo(1\.(2[5-9]|[3-9][0-9])|[2-9][0-9]+)\.' } if (-not $hasGo125) { if ($SkipMachineTools) { throw 'Go 1.25 or newer is missing and -SkipMachineTools was specified.' } Install-WingetPackage 'GoLang.Go' 'Go 1.25 or newer' Refresh-ProcessPath $go = Get-CommandPath 'go.exe' if (-not $go) { throw 'Go was installed, but go.exe is not available in PATH. Open a new terminal and rerun.' } } Write-Ok ('Go 1.25 or newer found at ' + $go) return $go } function Ensure-Node20 { $node = Get-CommandPath 'node.exe' $hasNode20 = $false if ($node) { $version = (& $node '--version' 2>$null) -join '' $hasNode20 = $version -match '^v(2[0-9]|[3-9][0-9]|[1-9][0-9]{2,})\.' } if (-not $hasNode20) { if ($SkipMachineTools) { throw 'Node.js 20 or newer is missing and -SkipMachineTools was specified.' } Install-WingetPackage 'OpenJS.NodeJS.LTS' 'Node.js 20 or newer' Refresh-ProcessPath $node = Get-CommandPath 'node.exe' if (-not $node) { throw 'Node.js was installed, but node.exe is not available in PATH. Open a new terminal and rerun.' } } Write-Ok ('Node.js 20 or newer found at ' + $node) return $node } function Ensure-Python310 { $python = Get-CommandPath 'python.exe' $hasPython310 = $false if ($python) { $version = (& $python '--version' 2>&1) -join '' $hasPython310 = $version -match '^Python (3\.(1[0-9]|[2-9][0-9])|[4-9]\.[0-9]+)\.' } if (-not $hasPython310) { if ($SkipMachineTools) { throw 'Python 3.10 or newer is missing and -SkipMachineTools was specified.' } Install-WingetPackage 'Python.Python.3.13' 'Python 3.10 or newer' Refresh-ProcessPath $python = Get-CommandPath 'python.exe' if (-not $python) { throw 'Python was installed, but python.exe is not available in PATH. Open a new terminal and rerun.' } } Write-Ok ('Python 3.10 or newer found at ' + $python) return $python } function Test-WebView2Runtime { $roots = @( 'HKLM:\SOFTWARE\Microsoft\EdgeUpdate\Clients', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients' ) foreach ($root in $roots) { if (-not (Test-Path $root)) { continue } foreach ($key in (Get-ChildItem $root -ErrorAction SilentlyContinue)) { $properties = Get-ItemProperty $key.PSPath -ErrorAction SilentlyContinue if ($properties.name -match 'WebView2' -and $properties.pv) { return [string]$properties.pv } } } return $null } function Ensure-WebView2 { if ($SkipWebView2) { Write-Info 'Skipping WebView2 runtime check.'; return } $version = Test-WebView2Runtime if ($version) { Write-Ok ('WebView2 Runtime ' + $version); return } if ($SkipMachineTools) { throw 'WebView2 Runtime is missing and -SkipMachineTools was specified.' } Install-WingetPackage 'Microsoft.EdgeWebView2Runtime' 'Microsoft Edge WebView2 Runtime' $version = Test-WebView2Runtime if (-not $version) { Write-Info 'WebView2 installer completed; restart Windows or rerun if the runtime is not detected.' } } function Ensure-VisualStudioBuildTools { if ($SkipVisualStudio) { Write-Info 'Skipping Visual Studio Build Tools check.'; return } $programFilesX86 = [Environment]::GetEnvironmentVariable('ProgramFiles(x86)') $vswhere = Join-Path $programFilesX86 'Microsoft Visual Studio\Installer\vswhere.exe' if (-not (Test-Path $vswhere)) { if ($SkipMachineTools) { throw 'Visual Studio Build Tools are missing and -SkipMachineTools was specified.' } Install-WingetPackage 'Microsoft.VisualStudio.2022.BuildTools' 'Visual Studio 2022 Build Tools' @( '--override', '--wait --passive --norestart --add Microsoft.VisualStudio.Workload.ManagedDesktopBuildTools --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.Windows10SDK.19041' ) if (-not (Test-Path $vswhere)) { Write-Info 'Build Tools installation finished; rerun this script to verify workloads.'; return } } $installation = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.Windows10SDK.19041 -property installationPath 2>$null if ($installation) { Write-Ok ('Visual Studio Build Tools with Windows SDK found at ' + $installation) } else { Write-Info 'Visual Studio is present, but workload verification was inconclusive; dotnet restore will provide the final check.' } } function Ensure-PythonModule([string] $Python, [string] $Module, [string] $Package) { & $Python -c ('import ' + $Module) 2>$null if ($LASTEXITCODE -eq 0) { Write-Ok ('Python module ' + $Module + ' found'); return } if ($SkipMachineTools) { throw ('Python module ' + $Module + ' is missing and -SkipMachineTools was specified.') } if (-not (Confirm-Install ('Python package ' + $Package))) { throw ('Installation of Python package ' + $Package + ' was declined.') } Invoke-External $Python @('-m', 'pip', 'install', '--user', $Package) ('Unable to install Python package ' + $Package) } Write-Step 'Checking Windows prerequisites' if ($env:OS -ne 'Windows_NT') { throw 'This bootstrap script supports Windows only.' } $dotnet = Ensure-DotNet10 $node = Ensure-Node20 $npm = Ensure-WingetPackage 'npm.cmd' 'OpenJS.NodeJS.LTS' 'npm (from Node.js LTS)' $go = Ensure-Go125 $python = Ensure-Python310 $rustup = Ensure-WingetPackage 'rustup.exe' 'Rustlang.Rustup' 'Rustup' Ensure-WebView2 Ensure-VisualStudioBuildTools Write-Step 'Checking language toolchains' Ensure-DotNetWorkload $dotnet 'maui' if ($rustup) { Invoke-External $rustup @('toolchain', 'install', 'stable-msvc') 'Rust toolchain setup failed' Invoke-External $rustup @('default', 'stable-msvc') 'Unable to select the MSVC Rust toolchain' Refresh-ProcessPath $cargo = Get-CommandPath 'cargo.exe' if ($cargo) { if (-not (Get-CommandPath 'wasm-pack.exe')) { Invoke-External $cargo @('install', 'wasm-pack', '--locked') 'Unable to install wasm-pack' } else { Write-Ok 'wasm-pack already installed' } $targets = & $rustup 'target' 'list' '--installed' 2>$null if (@($targets | Where-Object { $_ -eq 'wasm32-unknown-unknown' }).Count -eq 0) { Invoke-External $rustup @('target', 'add', 'wasm32-unknown-unknown') 'Unable to install the wasm32 target' } else { Write-Ok 'wasm32-unknown-unknown target already installed' } Write-Ok 'Rust MSVC toolchain, wasm32 target, and wasm-pack ready' } } Ensure-PythonModule $python 'PIL' 'Pillow' Write-Step 'Installing JavaScript dependencies' $npmProjects = @( 'server/unified-management/web/admin', 'server/unified-management/web/portal', 'server/unified-management/web/setup', 'src/YMhut.Box.PluginTauriHost' ) | ForEach-Object { Join-Path $Root $_ } foreach ($project in $npmProjects) { if (-not (Test-Path (Join-Path $project 'package.json'))) { continue } Push-Location $project try { if (Test-Path (Join-Path $project 'package-lock.json')) { Invoke-External $npm @('ci', '--no-audit', '--fund=false') ('npm ci failed in ' + $project) } else { Invoke-External $npm @('install', '--no-audit', '--fund=false', '--package-lock=false') ('npm install failed in ' + $project) } Write-Ok ('JavaScript dependencies installed in ' + $project) } finally { Pop-Location } } $musicApi = Join-Path $Root 'third_party/MoeKoeMusic/api' if (Test-Path (Join-Path $musicApi 'pnpm-lock.yaml')) { Write-Info 'Installing the locked music API dependencies through Corepack.' $corepack = Get-CommandPath 'corepack.cmd' if (-not $corepack) { Refresh-ProcessPath; $corepack = Get-CommandPath 'corepack.cmd' } if (-not $corepack) { throw 'corepack.cmd was not found after Node.js installation.' } Push-Location $musicApi try { Invoke-External $corepack @('pnpm', 'install', '--frozen-lockfile') 'pnpm install failed for the music API' } finally { Pop-Location } } if (-not $SkipRestore) { Write-Step 'Restoring .NET, Go, and Rust dependencies' Invoke-External $dotnet @('restore', (Join-Path $Root 'YMhut.Box.Native.sln'), '--configfile', $NuGetConfig) 'dotnet restore failed' $goModule = Join-Path $Root 'server/unified-management' if (Test-Path (Join-Path $goModule 'go.mod')) { Push-Location $goModule try { Invoke-External $go @('mod', 'download') 'Go module download failed' } finally { Pop-Location } } $cargoToml = Join-Path $Root 'server/unified-management/web/wasm/Cargo.toml' if (Test-Path $cargoToml) { $cargoCache = Join-Path $Root '.cache\cargo-wasm' New-Item -ItemType Directory -Force -Path $cargoCache | Out-Null $cargoManifest = Join-Path $cargoCache 'Cargo.toml' $cargoSource = Join-Path $cargoCache 'src' New-Item -ItemType Directory -Force -Path $cargoSource | Out-Null Copy-Item -LiteralPath $cargoToml -Destination $cargoManifest -Force Copy-Item -Path (Join-Path (Split-Path $cargoToml) 'src\*') -Destination $cargoSource -Recurse -Force Push-Location $cargoCache try { $env:CARGO_TARGET_DIR = Join-Path $Root '.cache\cargo-target' Invoke-External 'cargo.exe' @('fetch') 'WASM Cargo dependency download failed' } finally { Remove-Item -LiteralPath $cargoManifest -Force -ErrorAction SilentlyContinue Pop-Location } } } Write-Host ([Environment]::NewLine + 'Dependency setup completed.') -ForegroundColor Green