Files
Artemis-Eapil/scripts/download_config.ps1
T

209 lines
13 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Download settings are process-local; never edit global pip/npm/uv settings.
# Save as UTF-8 with BOM so Windows PowerShell 5.1 reads Chinese messages correctly.
function Get-DownloadText {
param([string]$English, [string]$Chinese)
if ($env:ARTEMIS_LAUNCH_LANG -eq 'zh-CN') { return $Chinese }
return $English
}
function Invoke-DownloadFile {
param(
[Parameter(Mandatory=$true)][string]$Uri,
[Parameter(Mandatory=$true)][string]$OutFile,
[int]$TimeoutSec = $ArtemisDownloads.timeout_seconds
)
$script:ArtemisDownloadError = ''
Remove-Item -LiteralPath $OutFile -Force -ErrorAction SilentlyContinue
if (Get-Command curl.exe -ErrorAction SilentlyContinue) {
$previousErrors = $ErrorActionPreference
try {
# Native stderr is an ErrorRecord in PowerShell 5.1. Inspect the code,
# and log only HTTP status/exit code, never a URL containing credentials.
$ErrorActionPreference = 'Continue'
$status = & curl.exe -f -sSL --connect-timeout 5 --max-time $TimeoutSec --proto '=https' --proto-redir '=https' --write-out '%{http_code}' -o $OutFile $Uri 2>$null
$curlCode = $LASTEXITCODE
if ($curlCode -eq 0 -and (Test-Path -LiteralPath $OutFile) -and (Get-Item -LiteralPath $OutFile).Length -gt 0) { return $true }
$script:ArtemisDownloadError = if ($curlCode -eq 22) { "HTTP $status" }
elseif ($curlCode -eq 28) { Get-DownloadText "Download timed out after $TimeoutSec seconds" "下载超时($TimeoutSec 秒)" }
else { "curl exit $curlCode" }
# Do not repeat an HTTP rejection or a full timeout with a second client.
if ($curlCode -in @(22, 28)) { return $false }
} catch {
$script:ArtemisDownloadError = 'curl failed'
} finally {
$ErrorActionPreference = $previousErrors
if ($script:ArtemisDownloadError) { Remove-Item -LiteralPath $OutFile -Force -ErrorAction SilentlyContinue }
}
}
$previousProgress = $ProgressPreference
try {
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $Uri -OutFile $OutFile -TimeoutSec $TimeoutSec -UseBasicParsing -ErrorAction Stop
if ((Test-Path -LiteralPath $OutFile) -and (Get-Item -LiteralPath $OutFile).Length -gt 0) {
$script:ArtemisDownloadError = ''
return $true
}
$script:ArtemisDownloadError = Get-DownloadText 'Empty download' '下载内容为空'
} catch {
$response = $_.Exception.Response
$script:ArtemisDownloadError = if ($response) { "HTTP $([int]$response.StatusCode)" }
else { Get-DownloadText 'Network/TLS error or download timeout' '网络、TLS 连接失败或下载超时' }
} finally {
$ProgressPreference = $previousProgress
if ($script:ArtemisDownloadError) { Remove-Item -LiteralPath $OutFile -Force -ErrorAction SilentlyContinue }
}
return $false
}
function Get-ArtemisFileSha256 {
param([Parameter(Mandatory=$true)][string]$Path)
# Do not depend on Get-FileHash module autoloading: PowerShell 7 can pass its
# own module search path to the Windows PowerShell 5.1 launcher.
$hasher = [Security.Cryptography.SHA256]::Create()
$stream = [IO.File]::OpenRead($Path)
try { return [BitConverter]::ToString($hasher.ComputeHash($stream)).Replace('-', '').ToLowerInvariant() }
finally { $stream.Dispose(); $hasher.Dispose() }
}
function Invoke-ArtemisVerifiedDownload {
param(
[Parameter(Mandatory=$true)][string[]]$Urls,
[Parameter(Mandatory=$true)][string]$OutFile,
[Parameter(Mandatory=$true)][string]$Sha256
)
$sources = @($Urls | Select-Object -Unique)
for ($index = 0; $index -lt $sources.Count; $index++) {
$sourceHost = ([Uri]$sources[$index]).Host
Write-Host (Get-DownloadText " [DOWNLOAD $($index + 1)/$($sources.Count)] scrcpy: $sourceHost" " [下载 $($index + 1)/$($sources.Count)] scrcpy:$sourceHost") -ForegroundColor Cyan
if (Invoke-DownloadFile -Uri $sources[$index] -OutFile $OutFile) {
$actualHash = Get-ArtemisFileSha256 -Path $OutFile
if ($actualHash -eq $Sha256) { return $true }
$script:ArtemisDownloadError = Get-DownloadText 'Official SHA256 verification failed; archive rejected' '官方 SHA256 校验失败,已拒绝此安装包'
Remove-Item -LiteralPath $OutFile -Force -ErrorAction SilentlyContinue
}
Write-Host (Get-DownloadText " [WARN] $sourceHost`: $script:ArtemisDownloadError" " [提示] $sourceHost 下载失败:$script:ArtemisDownloadError") -ForegroundColor Yellow
}
return $false
}
function Initialize-ArtemisDownloads {
param([string]$ProjectRoot, [string]$Mode)
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$path = Join-Path $ProjectRoot 'config\downloads.json'
$config = Get-Content -LiteralPath $path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
if (-not $Mode) { $Mode = $config.mode }
if ($Mode -notin @('Official', 'China')) { throw 'Download mode must be Official or China.' }
$timeout = 0
if (-not [int]::TryParse([string]$config.timeout_seconds, [ref]$timeout) -or $timeout -lt 10 -or $timeout -gt 3600) {
throw 'config/downloads.json: timeout_seconds must be between 10 and 3600.'
}
$values = @{
python_index = ''; python_download_mirror = ''; npm_registry = ''
node_mirror = 'https://nodejs.org/dist'
adb_url = 'https://dl.google.com/android/repository/platform-tools-latest-windows.zip'
scrcpy_url = 'https://github.com/Genymobile/scrcpy/releases/download/v4.1/scrcpy-win64-v4.1.zip'
uv_installer_url = 'https://astral.sh/uv/install.ps1'
}
foreach ($name in @($values.Keys)) {
if ($Mode -eq 'China' -and $config.china.$name) { $values[$name] = [string]$config.china.$name }
if ($config.overrides.$name) { $values[$name] = [string]$config.overrides.$name }
if ($values[$name]) {
$uri = $null
if (-not [Uri]::TryCreate($values[$name], [UriKind]::Absolute, [ref]$uri) -or $uri.Scheme -ne 'https') {
throw "config/downloads.json: $name must be an HTTPS URL."
}
}
}
$fallbacks = @('https://downloads.sourceforge.net/project/scrcpy.mirror/v4.1/scrcpy-win64-v4.1.zip')
if ($Mode -eq 'China' -and $null -ne $config.china.scrcpy_fallback_urls) { $fallbacks = $config.china.scrcpy_fallback_urls }
if ($null -ne $config.overrides.scrcpy_fallback_urls) { $fallbacks = $config.overrides.scrcpy_fallback_urls }
if ($fallbacks -isnot [Array]) { throw 'config/downloads.json: scrcpy_fallback_urls must be an array of HTTPS URLs.' }
foreach ($url in $fallbacks) {
$uri = $null
if ($url -isnot [string] -or -not [Uri]::TryCreate($url, [UriKind]::Absolute, [ref]$uri) -or $uri.Scheme -ne 'https') {
throw 'config/downloads.json: scrcpy_fallback_urls must contain only HTTPS URLs.'
}
}
$values.scrcpy_urls = @(@($values.scrcpy_url) + @($fallbacks) | Select-Object -Unique)
if ($Mode -eq 'China') {
# China mode must never silently fall back to a direct GitHub download.
$values.scrcpy_urls = @($values.scrcpy_urls | Where-Object { ([Uri]$_).Host -notin @('github.com', 'www.github.com') })
}
# Explicit caller environment settings take priority over the selected preset.
if ($env:UV_DEFAULT_INDEX) { $values.python_index = $env:UV_DEFAULT_INDEX }
elseif ($env:UV_INDEX_URL) { $values.python_index = $env:UV_INDEX_URL }
if ($env:UV_PYTHON_INSTALL_MIRROR) { $values.python_download_mirror = $env:UV_PYTHON_INSTALL_MIRROR }
if ($env:npm_config_registry) { $values.npm_registry = $env:npm_config_registry }
if ($values.python_download_mirror) { $env:UV_PYTHON_INSTALL_MIRROR = $values.python_download_mirror }
if ($values.npm_registry) { $env:npm_config_registry = $values.npm_registry }
$env:UV_HTTP_TIMEOUT = [string]$timeout
$env:npm_config_fetch_timeout = [string]($timeout * 1000)
$values.mode = $Mode
$values.timeout_seconds = $timeout
# Official scrcpy v4.1 win64 release digest. Verify every mirror/override equally.
$values.scrcpy_sha256 = '5b12172b3264b2889f4583ee64752ce832e29bc8b1089dca81093459697165db'
$script:ArtemisDownloads = [pscustomobject]$values
if ($env:ARTEMIS_LAUNCH_LANG -eq 'zh-CN' -and $Mode -eq 'China') {
Write-Host " [镜像] 使用国内下载源(config/downloads.json)。" -ForegroundColor Cyan
} else {
Write-Host " [INFO] Download mode: $Mode (config/downloads.json)." -ForegroundColor Cyan
}
}
function Install-ArtemisUvFromIndex {
param([string]$IndexUrl, [string]$InstallDir = (Join-Path $env:USERPROFILE '.local\bin'))
# A uv wheel contains the standalone executable; no preinstalled Python is needed.
$work = Join-Path ([IO.Path]::GetTempPath()) ('artemis-uv-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $work -ErrorAction Stop | Out-Null
try {
$index = $IndexUrl.TrimEnd('/') + '/uv/'
$page = Join-Path $work 'index.html'
if (-not (Invoke-DownloadFile -Uri $index -OutFile $page)) { throw 'Could not download the uv package index.' }
$platform = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64') { 'win_arm64' } else { 'win_amd64' }
$pattern = 'href=["''](?<link>[^"'']*uv-(?<version>\d+\.\d+\.\d+)-py3-none-' + $platform + '\.whl#sha256=(?<hash>[a-fA-F0-9]{64}))["'']'
$candidates = [regex]::Matches((Get-Content -LiteralPath $page -Raw), $pattern)
$candidate = $candidates | Sort-Object { [version]$_.Groups['version'].Value } -Descending | Select-Object -First 1
if (-not $candidate) { throw "No stable uv wheel for $platform was found on the selected index." }
$url = [Uri]::new([Uri]$index, [System.Net.WebUtility]::HtmlDecode($candidate.Groups['link'].Value))
if ($url.Scheme -ne 'https') { throw 'The uv download must use HTTPS.' }
$zip = Join-Path $work 'uv.zip'
if (-not (Invoke-DownloadFile -Uri $url.AbsoluteUri.Split('#')[0] -OutFile $zip)) { throw 'Could not download uv from the selected index.' }
$digest = Get-ArtemisFileSha256 -Path $zip
if ($digest -ne $candidate.Groups['hash'].Value) {
throw 'The downloaded uv package failed SHA256 verification.'
}
$extract = Join-Path $work 'wheel'
Add-Type -AssemblyName System.IO.Compression.FileSystem
[IO.Compression.ZipFile]::ExtractToDirectory($zip, $extract)
$executable = Get-ChildItem -LiteralPath $extract -Filter uv.exe -Recurse -File | Select-Object -First 1
if (-not $executable) { throw 'The uv package did not contain uv.exe.' }
New-Item -ItemType Directory -Path $InstallDir -Force -ErrorAction Stop | Out-Null
Copy-Item -LiteralPath $executable.FullName -Destination (Join-Path $InstallDir 'uv.exe') -Force -ErrorAction Stop
$uvx = Join-Path $executable.DirectoryName 'uvx.exe'
if (Test-Path -LiteralPath $uvx) { Copy-Item -LiteralPath $uvx -Destination $InstallDir -Force -ErrorAction Stop }
$env:PATH = "$InstallDir;$env:PATH"
} finally {
$resolved = (Resolve-Path -LiteralPath $work).Path
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\'
if ($resolved.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase) -and (Split-Path $resolved -Leaf) -like 'artemis-uv-*') {
Remove-Item -LiteralPath $resolved -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
function Install-ArtemisLockedDependencies {
param([string]$PythonExe, [string]$IndexUrl)
# uv sync uses the original artifact URLs in uv.lock. Export exact versions and
# hashes, then let uv pip fetch those same artifacts through the chosen index.
$requirements = Join-Path ([IO.Path]::GetTempPath()) ('artemis-requirements-' + [guid]::NewGuid().ToString('N') + '.txt')
try {
& uv export --frozen --no-dev --format requirements-txt --output-file $requirements 1>$null
if ($LASTEXITCODE -ne 0) { throw 'Could not export the locked dependencies.' }
& uv pip install --python $PythonExe --index-url $IndexUrl --requirements $requirements | Out-Host
if ($LASTEXITCODE -ne 0) { throw 'Dependency installation failed. Check mirror availability or select another source in config/downloads.json.' }
} finally {
Remove-Item -LiteralPath $requirements -Force -ErrorAction SilentlyContinue
}
}