{ "index": 336, "slug": "editorial-2018-09-practice-windows-dev-env", "title": "Windows-окружение без гадания: как найти настоящий бинарник и сравнить запуск", "excerpt": "Если одна Windows-машина запускает проект, а другая не находит команду или выбирает другую версию, сначала снимите процессное окружение. Разбираем PATH, профиль PowerShell, кодовую страницу и обратимую проверку без глобальной переустановки.", "contentHtml": "
Симптом знаком: на одном компьютере npm run build проходит, а на другом команда не находится, запускает старый Node или печатает нечитаемый лог. Код и commit совпадают. Разработчик видит только имя node, но Windows выбирает конкретный файл из конкретного процесса. Цена ошибки — потерянные часы и испорченные доказательства. Если сразу переустановить Node или переписать системный PATH, изменятся несколько условий сразу. После этого трудно понять, что действительно помогло.
Тезис простой: сравнивать нужно не список установленных программ, а путь запуска команды. В него входят процессные переменные, порядок каталогов в PATH, тип найденной команды, фактический путь к файлу, вывод версии и состояние консоли. Такой снимок не делает машины одинаковыми. Он сужает причину до наблюдаемого различия, которое можно проверить одним обратимым действием.
PowerShell ищет команды в текущей сессии. Результатом может быть alias, функция, cmdlet, скрипт или приложение. Для приложения поиск использует переменную PATH и расширения из PATHEXT. Первый найденный вариант становится тем, что запустит команда. Поэтому строка «Node установлен» не отвечает на вопрос «какой node.exe запустился сейчас?».
Переменные среды имеют область процесса, пользователя и компьютера. Новая PowerShell-сессия получает значения от родительского процесса. Уже открытое окно не обязано увидеть изменение, сделанное в настройках Windows. Если добавить каталог в системный PATH, а затем повторить команду в старом окне, проверка может измерить старое состояние. Для диагностики сначала используйте новый процесс, а постоянную настройку не меняйте.
Кодовая страница и кодировка обмена с внешней программой — отдельные слои. Команда chcp показывает активную кодовую страницу консоли, а $OutputEncoding задаёт кодировку, с которой PowerShell обменивается данными с native-программами. Ни одна из них не выбирает бинарник. Если путь и версия совпадают, нечитаемый вывод не доказывает проблему PATH. Кодировка файла — ещё один вопрос.
Снимок должен отвечать на четыре вопроса: какая PowerShell выполняет команду; какие каталоги участвуют в поиске; какие кандидаты возвращает Get-Command -All; что сообщает сам инструмент. Отдельно запишите кодовую страницу, кодировку обмена и способ записи файла. Не собирайте весь Env:: в нём могут оказаться токены, прокси, внутренние адреса и персональные пути.
| Поле | Проверка | Что объясняет | Ограничение |
|---|---|---|---|
| Версия и редакция PowerShell | $PSVersionTable.PSVersion, $PSVersionTable.PSEdition | Доступные команды и различия поведения оболочки | Не доказывает версию проекта |
Порядок PATH | $env:Path -split ';' | Какая папка может дать первый бинарник | Сам по себе путь не говорит, что файл исправен |
| Кандидаты команды | Get-Command node -All | Alias, функцию и приложения по одному имени | Не проверяет зависимости приложения |
| Фактическая версия | node --version | Что ответил запущенный инструмент | Не заменяет lock-файл и требования проекта |
| Состояние консоли и обмена | cmd /c chcp, $OutputEncoding.WebName, [Console]::OutputEncoding.WebName | Причину различий в консоли и при обмене с native-программой | Не меняет кодировку файлов и редактора |
Ниже — ограниченный учебный скрипт для PowerShell 5.1. Он собирает выбранные поля и версии четырёх команд. Пример не устанавливает инструменты, не исправляет PATH и не создаёт рабочие данные. Замените список команд на требования конкретного проекта. Если проект использует только PHP, отсутствие Node в отчёте нормально.
# Capture-Environment.ps1\n$ErrorActionPreference = 'Stop'\n$toolChecks = @(\n @{ name = 'node'; arguments = @('--version') },\n @{ name = 'npm'; arguments = @('--version') },\n @{ name = 'php'; arguments = @('--version') },\n @{ name = 'git'; arguments = @('--version') }\n)\n\nfunction Get-Candidates($name) {\n @(Get-Command -Name $name -All -ErrorAction SilentlyContinue |\n ForEach-Object {\n [ordered]@{\n type = $_.CommandType.ToString()\n name = $_.Name\n definition = $_.Definition\n source = $_.Source\n version = if ($_.Version) { $_.Version.ToString() } else { $null }\n }\n })\n}\n\nfunction Get-VersionOutput($name, $arguments) {\n $command = Get-Command -Name $name -ErrorAction SilentlyContinue\n if (-not $command) { return @('NOT FOUND') }\n if ($command.CommandType -ne 'Application') {\n return @('SKIPPED: ' + $command.CommandType.ToString() + ' ' + $command.Definition)\n }\n try {\n $lines = @(& $command.Source @arguments 2>&1 | Select-Object -First 3 |\n ForEach-Object { $_.ToString() })\n return @($lines + ('exitCode=' + $LASTEXITCODE))\n } catch {\n return @('FAILED: ' + $_.Exception.Message)\n }\n}\n\n$report = [ordered]@{\n formatVersion = 1\n capturedAt = (Get-Date).ToString('o')\n powershell = [ordered]@{\n version = $PSVersionTable.PSVersion.ToString()\n edition = $PSVersionTable.PSEdition\n }\n console = [ordered]@{\n codePage = ((cmd.exe /d /c chcp) -join ' ').Trim()\n nativeOutputEncoding = $OutputEncoding.WebName\n consoleOutputEncoding = [Console]::OutputEncoding.WebName\n }\n environment = [ordered]@{\n pathEntries = @($env:Path -split ';' | Where-Object { $_ })\n pathext = $env:PATHEXT\n }\n commands = @($toolChecks | ForEach-Object {\n [ordered]@{\n name = $_.name\n candidates = Get-Candidates $_.name\n versionOutput = Get-VersionOutput $_.name $_.arguments\n }\n })\n}\n\n$report | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath '.\\environment-snapshot.json' -Encoding UTF8\nЗапускайте его из каталога проекта без профиля, чтобы пользовательская функция или alias не скрыли реальную картину. Для версии 5.1 это также сохраняет UTF-8 с BOM, поэтому файл безопаснее читать теми инструментами, которые умеют распознавать BOM:
\npowershell.exe -NoProfile -File .\\tools\\Capture-Environment.ps1\nGet-Content .\\environment-snapshot.json -Raw | ConvertFrom-Json | Format-List\nGet-Command node -All | Select-Object CommandType, Name, Version, Definition\nПеред отправкой JSON удалите имя пользователя, внутренние каталоги, URL прокси и любые значения, которые относятся к доступу. Сохраните исходный файл только там, где это разрешено. Если отчёт нужен для сравнения, полезнее заменить часть пути на <USER>, чем публиковать личные данные. Не добавляйте снимок в репозиторий, пока не проверили его содержимое.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Команда не найдена | Каталог отсутствует в процессном PATH или программа не установлена | $env:Path -split ';', Get-Command name -All | Сверить требование проекта и проверить одобренную установку; не дописывать путь наугад |
| Запускается не та версия | Старая папка стоит раньше в PATH | Сравнить первый Definition, весь список кандидатов и name --version | В новом окне временно проверить документированный каталог в начале процессного $env:Path |
| Вместо приложения найдена функция или alias | Профиль PowerShell подменяет имя | Сравнить обычный запуск с powershell.exe -NoProfile | Использовать явный путь или исправить профиль после подтверждения причины |
| После изменения ничего не поменялось | Команда выполняется в старом процессе | Снять отчёт из новой сессии без профиля | Повторить одну исходную команду; не делать вывод о неработающем исправлении по старому окну |
| Лог нечитаем | Различаются кодовая страница или кодировка вывода | chcp, OutputEncoding и способ записи файла | Проверить консоль, обмен с native-программой и файл раздельно; не менять PATH |
| Путь и версия совпадают, сборка всё ещё падает | Причина находится в зависимостях, правах, сети или конфигурации | Lock-файл, полный лог, права каталога и следующий симптом | Прекратить правку окружения и продолжить расследование по новому факту |
Сначала сделайте входы сравнимыми: один commit, одна команда, один рабочий каталог и одинаковый lock-файл. На обеих машинах сохраните очищенные снимки. Затем сравните только поля, относящиеся к запуску. В учебном примере машина A первой находит C:\\Tools\\node-6\\node.exe, а машина B — C:\\Program Files\\nodejs\\node.exe. Это доказывает различие поиска, но не доказывает, какая версия нужна проекту. Требование должно прийти из README, lock-файла или официальной документации проекта.
function Get-SnapshotLines($path) {\n $s = Get-Content $path -Raw | ConvertFrom-Json\n $lines = @(\n 'powershell=' + $s.powershell.version\n 'codePage=' + $s.console.codePage\n 'nativeOutputEncoding=' + $s.console.nativeOutputEncoding\n 'consoleOutputEncoding=' + $s.console.consoleOutputEncoding\n 'pathext=' + $s.environment.pathext\n )\n foreach ($entry in $s.environment.pathEntries) { $lines += 'path=' + $entry }\n foreach ($tool in $s.commands) {\n $first = @($tool.candidates | Select-Object -First 1)\n if ($first.Count -eq 0) { $lines += $tool.name + '=NOT FOUND'; continue }\n $lines += ('{0}={1}|{2}|{3}' -f $tool.name, $first[0].type, $first[0].definition, $first[0].version)\n }\n return $lines\n}\nCompare-Object (Get-SnapshotLines '.\\machine-a.json') (Get-SnapshotLines '.\\machine-b.json')\nCompare-Object показывает различающиеся строки. Он не ставит диагноз. Если отличаются десять каталогов, выберите тот, который связан с исходной ошибкой, и проверьте его отдельно. Сначала сравните первый кандидат и версию. Только затем смотрите кодовую страницу или второстепенные различия.
Допустим, документация проекта требует Node из конкретного каталога, а снимок показывает старый бинарник. В новом PowerShell добавьте проверенный каталог только в процессную переменную. Учебный путь ниже условный. Его нельзя копировать в рабочую машину без подтверждения версии и расположения файла.
\n$requiredNode = 'C:\\Tools\\node-8'\nif (-not (Test-Path (Join-Path $requiredNode 'node.exe'))) {\n throw 'node.exe not found: ' + $requiredNode\n}\n$env:Path = $requiredNode + ';' + $env:Path\nGet-Command node -All | Select-Object CommandType, Definition\nnode --version\nnpm run build\nЕсли версия изменилась и исходная ошибка исчезла, гипотеза о порядке PATH получила подтверждение в этой сессии. Это ещё не разрешение менять системные настройки. Сначала зафиксируйте требование проекта и способ установки. Если результат не изменился, верните внимание к логу. Не оставляйте случайный каталог в постоянном PATH и не скачивайте исполняемый файл из непроверенного источника.
-NoProfile, если есть подозрение на alias, функцию или профиль.PATH, PATHEXT, кандидатов команды, путь первого приложения и результат --version.$env:Path или другой обратимый шаг.Одинаковый снимок не гарантирует одинаковый запуск. На результат влияют разрядность, DLL, права каталога, антивирус, прокси, сертификаты, кеши, редактор, локальные файлы и сетевой доступ. Снимок не видит все эти причины. Он только помогает исключить подмену команды и ошибку поиска.
\nЕсли команда не найдена, не добавляйте в PATH первую папку из загрузок. Сначала проверьте официальную инструкцию проекта, наличие нужного файла и архитектуру. Если путь и версия совпали, не продолжайте менять PATH ради другого симптома. Если отчёт содержит секрет, удалите его из копии до публикации и сообщите владельцу доступа по принятому каналу.
Если новый сеанс с нужным каталогом не меняет исходную ошибку, отрицательный результат важен. Он исключает одну гипотезу. Верните постоянные настройки в исходное состояние, не скрывайте неудачную проверку и исследуйте следующий наблюдаемый факт: лог пакетного менеджера, права, сертификат или конфигурацию проекта.
\nРазбор готов, когда другой разработчик без устного объяснения может назвать исходную команду и ошибку, показать очищенный снимок, указать первый найденный бинарник, сопоставить его с требуемой версией и повторить обратимую проверку в новом сеансе. Должно быть видно, что изменилось и что не изменилось. Если остаётся только фраза «у меня работает», диагностика не закончена.
\nPATH и PATHEXT.PATH и порядок выполнения при параметре -All.$OutputEncoding, кодировкой консоли и кодировкой файлов.-NoProfile.