[CmdletBinding(PositionalBinding = $false)] # Именованные служебные параметры не должны поглощать позиционный путь запускаемого Python-скрипта. param( # Единая точка входа CCS/Keil работает без зависимости от python.exe в PATH. [string]$ResolveFile, # При настройке IDE записывает точный путь проверенного интерпретатора в UTF-8. [switch]$CheckOnly, # Проверка окружения не скачивает runtime и не запускает проектный скрипт. [Parameter(Position = 0, ValueFromRemainingArguments = $true)][string[]]$PythonArguments # Путь скрипта и все оставшиеся аргументы передаются Python без позиционной привязки к ResolveFile. ) # Завершает описание публичных параметров обвязки. $ErrorActionPreference = 'Stop' # Любая ошибка подготовки возвращается IDE как отказ шага сборки. $env:PYTHONUTF8 = '1' # Python 3.8+ передаёт пути и диагностику в UTF-8 независимо от локальной кодовой страницы Windows. [Console]::OutputEncoding = New-Object Text.UTF8Encoding($false) # Windows PowerShell 5 должен читать вывод Python в той же кодировке. $OutputEncoding = [Console]::OutputEncoding # Согласует кодировку дочерних native-процессов с JSON и сообщениями SETGUI. function Test-FirmwarePython([string]$Candidate) { # Проверяет настоящий Python вместо одноимённого псевдонима Microsoft Store. if (-not $Candidate -or -not (Test-Path -LiteralPath $Candidate -PathType Leaf) -or $Candidate -like '*WindowsApps*') { return $null } # Пропускает отсутствующие файлы и псевдонимы магазина без открытия окна установки. try { # Неподходящий кандидат не мешает проверить остальные установленные интерпретаторы. $result = & $Candidate -c 'import sys,json,hashlib,struct,tempfile,ctypes,xml.etree.ElementTree;sys.exit(1) if sys.version_info<(3,8) else print(sys.executable)' 2>$null # Общим инструментам нужны Python 3.8+ и только стандартная библиотека. if ($LASTEXITCODE -eq 0 -and $result) { return [string]($result | Select-Object -Last 1) } # Возвращает фактический exe, а не имя команды или путь launcher. } catch {} # Ошибка запуска одного кандидата оставляет поиск остальных доступным. return $null # Отсутствие пригодного runtime обрабатывается общей установкой ниже. } # Завершает проверку одного интерпретатора. function Find-FirmwarePython { # Ищет готовый runtime до любых сетевых действий. if ($env:FIRMWARE_PYTHON) { # Явный выбор администратора проекта имеет приоритет над автоматическим поиском. $chosen = Test-FirmwarePython $env:FIRMWARE_PYTHON # Проверяет указанный абсолютный путь тем же набором импортов. if (-not $chosen) { throw 'FIRMWARE_PYTHON должен указывать на работающий Python 3.8+.' } # Неверное явное значение не подменяется другой версией незаметно. return $chosen # Оба шага IDE используют выбранный интерпретатор. } # После явной настройки остальные кандидаты не нужны. $cache = Join-Path $env:LOCALAPPDATA 'SET/FirmwareBuild' # Локальный runtime принадлежит пользователю и не требует администратора. foreach ($relative in @('python-3.13.12-amd64/python.exe', 'python-3.8.10-win32/python.exe')) { # Порядок предпочитает современный runtime, сохраняя совместимость с Windows 7. $chosen = Test-FirmwarePython (Join-Path $cache $relative) # Проверяет ранее подготовленную локальную копию. if ($chosen) { return $chosen } # Повторные сборки обходятся без скачивания и установки. } # Завершает поиск локального управляемого runtime. foreach ($command in @('python', 'python3')) { # Установленный Python можно использовать без изменения пользовательского PATH. $candidate = Get-Command $command -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 # Получает полный путь исполняемого файла команды. if ($candidate) { $chosen = Test-FirmwarePython $candidate.Source; if ($chosen) { return $chosen } } # Не принимает неподходящую версию за готовое окружение. } # После PATH проверяет зарегистрированные установки без требования перезапуска IDE. foreach ($registry in @('HKCU:/Software/Python/PythonCore', 'HKLM:/Software/Python/PythonCore', 'HKLM:/Software/WOW6432Node/Python/PythonCore')) { # PEP 514 учитывает установки текущего пользователя, x64 и x86. foreach ($version in @(Get-ChildItem -LiteralPath $registry -ErrorAction SilentlyContinue | Sort-Object PSChildName -Descending)) { # Перебирает только зарегистрированные версии Python. $key = Get-Item -LiteralPath ($version.PSPath + '/InstallPath') -ErrorAction SilentlyContinue # Реестр хранит корневой каталог независимой установки. if ($key) { $chosen = Test-FirmwarePython (Join-Path ([string]$key.GetValue('')) 'python.exe'); if ($chosen) { return $chosen } } # Возвращает первую реально работающую версию 3.8+. } # Завершает перебор версий одного раздела реестра. } # Ни один существующий runtime не оказался пригодным. return $null # Вызывающий подготовит переносимый runtime, если проверка допускает установку. } # Завершает поиск установленного Python. try { # Все ошибки возвращаются одним ненулевым кодом, понятным CCS/Keil. $pythonExe = Find-FirmwarePython # Использует готовую установку без изменения окружения машины. if (-not $pythonExe -and $CheckOnly) { throw 'Python 3.8+ не найден; примените автоматическую настройку проекта для подготовки runtime.' } # Режим диагностики не устанавливает ПО. if (-not $pythonExe) { # Установка нужна только компьютеру без подходящего Python. $legacy = [Environment]::OSVersion.Version -lt [Version]'6.2' -or -not [Environment]::Is64BitOperatingSystem # Windows 7 и x86 используют последнюю совместимую бинарную ветку 3.8. $version = if ($legacy) { '3.8.10' } else { '3.13.12' } # Зафиксированные версии делают подготовку воспроизводимой. $arch = if ($legacy) { 'win32' } else { 'amd64' } # Разрядность runtime не зависит от разрядности установленной IDE. $expected = if ($legacy) { '760dc79bcb434ee80b1001a30bb6f798287881851bac6d8137867894d40ef1fc' } else { '76f238f606250c87c6beac75dccd35ee99070a13490555936abb6cb64ecce3d0' } # SHA-256 официальных embeddable ZIP с python.org проверен при подготовке обвязки. $cache = Join-Path $env:LOCALAPPDATA 'SET/FirmwareBuild' # Runtime хранится отдельно от исходников и сохраняется после Clean. $destination = Join-Path $cache ('python-' + $version + '-' + $arch) # Каждый вариант получает независимый каталог пользователя. New-Item -ItemType Directory -Force -Path $cache | Out-Null # Создаёт только собственный каталог инструментов сборки. $temporary = Join-Path $cache ('download-' + [Guid]::NewGuid().ToString('N')) # Уникальный staging исключает частично установленный runtime. New-Item -ItemType Directory -Path $temporary | Out-Null # Распаковка выполняется вне постоянного каталога. try { # Ошибка сети или распаковки не должна повредить ранее установленный Python. $archive = Join-Path $temporary 'python.zip' # Собственный архив удаляется вместе со staging. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Загрузка использует TLS 1.2 также на Windows 7 с обновлённой .NET. $client = New-Object Net.WebClient # WebClient доступен штатной Windows PowerShell без внешних модулей. try { $client.DownloadFile(('https://www.python.org/ftp/python/' + $version + '/python-' + $version + '-embed-' + $arch + '.zip'), $archive) } finally { $client.Dispose() } # Скачивает строго официальный зафиксированный пакет Python. $sha = [Security.Cryptography.SHA256]::Create() # Проверяет полные байты архива до запуска любого файла из него. try { $actual = ([BitConverter]::ToString($sha.ComputeHash([IO.File]::ReadAllBytes($archive)))).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() } # Нормализует SHA-256 для точного сравнения с закреплённым значением. if ($actual -ne $expected) { throw 'SHA-256 архива Python не совпал с ожидаемым значением.' } # Неподтверждённый архив никогда не распаковывается. Add-Type -AssemblyName System.IO.Compression.FileSystem # Использует штатную .NET 4.5+ для ZIP без зависимости от Python. $unpacked = Join-Path $temporary 'runtime' # Отделяет исполняемые файлы от скачанного ZIP. [IO.Compression.ZipFile]::ExtractToDirectory($archive, $unpacked) # Распаковывает проверенный официальный ZIP во временный каталог. $checked = Test-FirmwarePython (Join-Path $unpacked 'python.exe') # До публикации проверяет версию и необходимые модули runtime. if (-not $checked) { throw 'Подготовленный Python не запускается; проверьте Universal C Runtime Windows.' } # Отсутствующая системная зависимость не выдаётся за успех установки. if (Test-Path -LiteralPath $destination) { throw ('Каталог runtime занят непригодной установкой: ' + $destination) } # Не удаляет существующие пользовательские файлы автоматически. Move-Item -LiteralPath $unpacked -Destination $destination # Атомарно публикует только полностью проверенную локальную установку. $pythonExe = Join-Path $destination 'python.exe' # Все последующие вызовы используют точный путь установленного runtime. } finally { # Очищает только собственный staging после проверки его абсолютного расположения. $resolved = [IO.Path]::GetFullPath($temporary) # Нормализует путь перед рекурсивным удалением на Windows. if ($resolved.StartsWith([IO.Path]::GetFullPath($cache) + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { Remove-Item -LiteralPath $resolved -Recurse -Force -ErrorAction SilentlyContinue } # Удаляет исключительно созданную текущим вызовом папку внутри собственного кэша. } # Завершает подготовку одного переносимого runtime. } # Готовый Python найден или успешно подготовлен. if ($ResolveFile) { [IO.File]::WriteAllText([IO.Path]::GetFullPath($ResolveFile), $pythonExe, (New-Object Text.UTF8Encoding($false))) } # GUI получает точный путь без разбора локализованного вывода консоли. if ($PythonArguments.Count) { # Непустые аргументы означают запуск шага сборки после подготовки зависимостей. $bootstrap = 'import os,runpy,sys;p=sys.argv[1];sys.argv=sys.argv[1:];sys.path.insert(0,os.path.dirname(os.path.abspath(p)));runpy.run_path(p,run_name=''__main__'')' # Одинарные Python-кавычки сохраняются при передаче через Windows PowerShell 5 и изолированный runtime. & $pythonExe -c $bootstrap @PythonArguments # Передаёт исходные аргументы отдельными элементами без повторного разбора командной оболочкой. exit $LASTEXITCODE # Ошибка Python останавливает prebuild/postbuild с исходным кодом завершения. } # В режиме настройки достаточно успешной подготовки runtime. Write-Output $pythonExe # Терминал показывает фактически выбранный интерпретатор. exit 0 # Успех означает работающий Python с проверенной стандартной библиотекой. } catch { # Единая диагностика доступна как в IDE, так и в окне настройки SETGUI. [Console]::Error.WriteLine('[firmware-build] ' + $_.Exception.Message) # Показывает конкретную причину отказа без скрытого перехода к другой цепочке. exit 2 # Ненулевой код запрещает дальнейшие стадии неподготовленной сборки. } # Завершает общий запуск runtime и инструментов прошивки.