param( [Parameter(Mandatory = $true)] [ValidateSet('flutter', 'java', 'android-cli')] [string]$Tool ) $ErrorActionPreference = 'Stop' $taskProjectRoot = Split-Path -Parent $PSScriptRoot $taskToolRoot = Join-Path $taskProjectRoot '.tools' $taskDownloads = Join-Path $taskToolRoot 'downloads' New-Item -ItemType Directory -Force -Path $taskDownloads | Out-Null $taskPackages = @{ 'flutter' = @{ Url = 'https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.47.7-stable.zip' Sha256 = 'c2c875da25a1a1448d18ee4e8f398b2f6fce888b4775d8f9130538e16d826cb4' Zip = 'flutter-3.47.7.zip' Marker = 'flutter/bin/flutter.bat' } 'java' = @{ Url = 'https://aka.ms/download-jdk/microsoft-jdk-17.0.20.1-windows-x64.zip' Sha256 = '3d9006956fc8af5601cd24ffc4f468bef48279c7ebd8171b9bdf90d0aabfbf1f' Zip = 'jdk-17.zip' Marker = 'jdk-17/bin/java.exe' } 'android-cli' = @{ Url = 'https://dl.google.com/android/repository/commandlinetools-win-15859902_latest.zip' Sha256 = '90ae805d20434428bffcb699c290860f19bb5f66a67e6b330067e3de801fb04a' Zip = 'android-commandline.zip' Marker = 'android-sdk/cmdline-tools/latest/bin/sdkmanager.bat' } } $taskPackage = $taskPackages[$Tool] if (Test-Path -LiteralPath (Join-Path $taskToolRoot $taskPackage.Marker)) { Write-Output "$Tool is already installed." exit 0 } $taskArchive = Join-Path $taskDownloads $taskPackage.Zip Write-Output "Downloading $Tool from the official distribution." & curl.exe --fail --location --silent --show-error --retry 3 --retry-all-errors --connect-timeout 30 --speed-limit 1024 --speed-time 60 --continue-at - --output $taskArchive $taskPackage.Url if ($LASTEXITCODE -ne 0) { throw "Download failed: $Tool" } $taskActualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $taskArchive).Hash.ToLowerInvariant() if ($taskActualHash -ne $taskPackage.Sha256) { throw "SHA-256 verification failed: $Tool" } Write-Output "Verified SHA-256 for $Tool; extracting." if ($Tool -eq 'flutter') { & tar.exe -xf $taskArchive -C $taskToolRoot if ($LASTEXITCODE -ne 0) { throw 'Flutter extraction failed.' } } else { $taskStage = Join-Path $taskToolRoot "staging/$Tool" New-Item -ItemType Directory -Force -Path $taskStage | Out-Null & tar.exe -xf $taskArchive -C $taskStage if ($LASTEXITCODE -ne 0) { throw "$Tool extraction failed." } if ($Tool -eq 'java') { $taskSource = (Get-ChildItem -LiteralPath $taskStage -Directory | Where-Object Name -Like 'jdk-*' | Select-Object -First 1).FullName $taskDestination = Join-Path $taskToolRoot 'jdk-17' } else { $taskSource = Join-Path $taskStage 'cmdline-tools' $taskDestination = Join-Path $taskToolRoot 'android-sdk/cmdline-tools/latest' New-Item -ItemType Directory -Force -Path (Split-Path -Parent $taskDestination) | Out-Null } # Validate the resolved paths before moving the unpacked tool directory. $taskResolvedRoot = [IO.Path]::GetFullPath($taskToolRoot).TrimEnd('\') + '\' foreach ($taskMovePath in @($taskSource, $taskDestination)) { if (-not $taskMovePath -or -not [IO.Path]::GetFullPath($taskMovePath).StartsWith($taskResolvedRoot, [StringComparison]::OrdinalIgnoreCase)) { throw 'Tool path is outside the project tool directory.' } } Move-Item -LiteralPath $taskSource -Destination $taskDestination } if (-not (Test-Path -LiteralPath (Join-Path $taskToolRoot $taskPackage.Marker))) { throw "$Tool installation is incomplete." } Write-Output "Installed $Tool."