На одном Windows-компьютере проект собирается, а на другом команда npm run build не находит node или запускает другую версию PHP. Цена такой ошибки — не только потерянный вечер: в спешке легко добавить в PATH случайную папку, переслать коллеге пароль из переменной среды или поставить непонятный архив с интерпретатором.
Здесь один вопрос: какой минимальный снимок окружения нужен, чтобы другой разработчик увидел тот же запуск проекта? Не будем архивировать весь диск и делать вид, что любая разница машины важна. Зафиксируем только то, что влияет на поиск команд, их версию и текст, который видит консоль.
Граница задачи: сохраняю запуск, а не весь компьютер
Окружение процесса — это набор строк, с которым стартует конкретная консоль и её дочерние программы. В Windows есть пользовательский, системный и процессный уровни переменных. Уже открытый PowerShell не обязан получить изменения, сделанные в окне настроек: новая консоль наследует новое значение, старая продолжает работать со своим набором. Поэтому запись «у меня установлен Node» ничего не объясняет, пока не известно, какой node.exe нашёл именно этот процесс.
Минимальный снимок отвечает на четыре проверяемых вопроса: какая версия Windows PowerShell запустила команду; какие папки стоят в PATH и какие расширения допускает PATHEXT; какие кандидаты вернул Get-Command -All; что напечатала сама программа для --version. В отдельной строке оставляю активную кодовую страницу консоли. Этого достаточно для первого сравнения, но в снимок не попадают токены, cookie, пароли, содержимое домашней папки и полный дамп всех переменных.
Что кладу в файл, а что оставляю на машине
| Поле снимка | Как получить | Зачем оно нужно | Чего в нём не должно быть |
|---|---|---|---|
| Версия PowerShell | $PSVersionTable.PSVersion | От неё зависят доступные команды и формат части вывода | Имя пользователя и содержимое профиля |
Порядок PATH | $env:Path -split ";" | Показывает, какая папка может дать первый бинарник | Полный список случайных переменных среды |
PATHEXT | $env:PATHEXT | Объясняет, какие расширения Windows считает исполняемыми | Изменение системного значения ради эксперимента |
| Кандидаты команды | Get-Command node -All | Отделяет alias или функцию от настоящего node.exe | Непроверенный вывод из чужого скриншота |
| Версия инструмента | node --version | Связывает найденный путь с фактическим запуском | Секреты, переданные приложению аргументами |
| Кодовая страница | cmd /c chcp | Помогает объяснить нечитаемый вывод старой консоли | Глобальное переключение кодировки без проверки |
Один скрипт для Windows PowerShell 5.1
Ниже не установщик и не «починка» окружения. Он только собирает отчёт рядом с проектом. Список $toolChecks нужно оставить коротким: добавьте туда реальные инструменты проекта, например php и composer, а не все программы из меню Пуск. Команда Get-Command -All намеренно сохраняет все найденные варианты, потому что первый путь без остальных кандидатов часто скрывает причину расхождения.
# tools/Capture-Environment.ps1
param(
[string]$OutputPath = (Join-Path $PSScriptRoot 'environment-snapshot.json')
)
$ErrorActionPreference = 'Stop'
$toolChecks = @(
[pscustomobject]@{ name = 'node'; arguments = @('--version') },
[pscustomobject]@{ name = 'npm'; arguments = @('--version') },
[pscustomobject]@{ name = 'php'; arguments = @('--version') },
[pscustomobject]@{ name = 'git'; arguments = @('--version') }
)
function Get-CommandCandidates {
param([string]$Name)
@(
Get-Command -Name $Name -All -ErrorAction SilentlyContinue |
ForEach-Object {
[ordered]@{
commandType = $_.CommandType.ToString()
name = $_.Name
definition = $_.Definition
source = $_.Source
version = if ($_.Version) { $_.Version.ToString() } else { $null }
}
}
)
}
function Get-VersionOutput {
param([string]$Name, [string[]]$Arguments)
if (-not (Get-Command -Name $Name -ErrorAction SilentlyContinue)) {
return @('NOT FOUND')
}
try {
$lines = @(& $Name @Arguments 2>&1 | Select-Object -First 3 | ForEach-Object { $_.ToString() })
return @($lines + ('exitCode=' + $LASTEXITCODE))
} catch {
return @('FAILED: ' + $_.Exception.Message)
}
}
$pathEntries = @($env:Path -split ";" | ForEach-Object { $_.Trim() } | Where-Object { $_ })
$report = [ordered]@{
formatVersion = 1
capturedAt = (Get-Date).ToString('o')
powershell = [ordered]@{
version = $PSVersionTable.PSVersion.ToString()
edition = $PSVersionTable.PSEdition
}
console = [ordered]@{
activeCodePage = ((& cmd.exe /d /c chcp) -join " ").Trim()
outputEncoding = [Console]::OutputEncoding.WebName
}
environment = [ordered]@{
pathEntries = $pathEntries
pathext = $env:PATHEXT
}
commands = @(
foreach ($check in $toolChecks) {
[ordered]@{
name = $check.name
candidates = Get-CommandCandidates $check.name
versionOutput = Get-VersionOutput $check.name $check.arguments
}
}
)
}
$report | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $OutputPath -Encoding UTF8
Write-Host ('Written: ' + $OutputPath)
В отчёт попадает только заранее выбранный набор полей. Это полезнее, чем Get-ChildItem Env: целиком: там могут оказаться адрес прокси, ключ приложения или служебный путь. Сам PATH тоже способен раскрыть имя локального пользователя и внутренние каталоги. Перед публикацией в задаче или чате открываю JSON, заменяю такие части на нейтральные метки и сохраняю исходник только в закрытом месте.
Снимаю отчёт в отдельном сеансе и читаю его как данные
Для первой проверки полезен PowerShell без профиля: так случайная функция из $PROFILE не выдаст себя за установленный инструмент. Это не отменяет проверку обычной рабочей консоли. Наоборот, если снимки расходятся, профиль становится одной из гипотез, которую можно подтвердить отдельным запуском.
powershell.exe -NoProfile -File .\tools\Capture-Environment.ps1
Get-Content .\tools\environment-snapshot.json -Raw |
ConvertFrom-Json |
Format-List formatVersion, powershell, console, environment, commands
Get-Command node -All |
Select-Object CommandType, Name, Version, Definition |
Format-Table -AutoSize
Готовый запуск не обязан вернуть все четыре инструмента. Для проекта на PHP отсутствие node может быть нормальным, а отсутствие php — нет. Важен не список «зелёных» строк, а заранее оговорённый набор. Если versionOutput содержит NOT FOUND, я не дописываю путь в системные настройки наугад: сначала смотрю кандидатов и документацию самого проекта.
Короткий порядок работы
- В README назвать команды проекта и инструменты, которые им нужны: например,
php,composer,nodeиgit. - Сохранить скрипт в репозитории или рядом с ним, но добавить созданный JSON в ignore, если он содержит локальные пути.
- Запустить снимок из обычной консоли и, при спорном случае, повторить запуск с
-NoProfile. - Проверить для каждого инструмента первый кандидат, остальные кандидаты и фактический вывод
--version. - Перед передачей отчёта убрать имя пользователя, внутренние каталоги и всё, что не требуется для разбора.
- После правки среды открыть новую консоль, снять новый отчёт и сравнить именно изменившиеся строки.
Где минимальный снимок не отвечает
Такой файл не доказывает, что зависимости проекта совпадают. Он не заменяет lock-файл, исходный код, права доступа к каталогу, настройки прокси и архитектуру 32/64 bit. Кодовая страница консоли тоже не равна кодировке каждого файла: файл может быть сохранён в другой кодировке, а программа может читать его своим правилом. Если симптом связан с файлами, отдельно проверяю байты файла и настройки конкретного компилятора, а не объявляю chcp единственной причиной.
Не стоит хранить снимок годами как истину. После обновления PHP, Node или Git он честно меняется. Его ценность в другом: у команды есть маленькая запись «что именно запускалось в этот день» и воспроизводимый способ обновить её без переустановки всего компьютера.
Что считаю готовым
Задача закрыта, когда новый разработчик может открыть JSON и ответить на три вопроса: какая консоль работала, откуда она взяла нужный бинарник и какую версию тот напечатал. Если один из ответов отсутствует, следующий шаг тоже ясен — дополнить снимок конкретным инструментом, а не обмениваться фразой «у меня работает».