# Download settings are process-local; never edit global pip/npm/uv settings. # Keep this file ASCII for Windows PowerShell 5.1. function Initialize-ArtemisDownloads { param([string]$ProjectRoot, [string]$Mode) $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." } } } # 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 $script:ArtemisDownloads = [pscustomobject]$values 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=["''](?[^"'']*uv-(?\d+\.\d+\.\d+)-py3-none-' + $platform + '\.whl#sha256=(?[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.' } $hasher = [Security.Cryptography.SHA256]::Create() $stream = [IO.File]::OpenRead($zip) try { $digest = [BitConverter]::ToString($hasher.ComputeHash($stream)).Replace('-', '') } finally { $stream.Dispose(); $hasher.Dispose() } 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 } }