DarkRiDDeR11 мин

Windows. Как сохранить минимальный снимок окружения проекта

WindowsИнструменты

На одном Windows-компьютере проект собирается, а на другом команда npm run build не находит node или запускает другую версию PHP. Цена такой ошибки — не только потерянный вечер: в спешке легко добавить в PATH случайную папку, переслать коллеге пароль из переменной среды или поставить непонятный архив с интерпретатором.

Здесь один вопрос: какой минимальный снимок окружения нужен, чтобы другой разработчик увидел тот же запуск проекта? Не будем архивировать весь диск и делать вид, что любая разница машины важна. Зафиксируем только то, что влияет на поиск команд, их версию и текст, который видит консоль.

Граница задачи: сохраняю запуск, а не весь компьютер

Окружение процесса — это набор строк, с которым стартует конкретная консоль и её дочерние программы. В Windows есть пользовательский, системный и процессный уровни переменных. Уже открытый PowerShell не обязан получить изменения, сделанные в окне настроек: новая консоль наследует новое значение, старая продолжает работать со своим набором. Поэтому запись «у меня установлен Node» ничего не объясняет, пока не известно, какой node.exe нашёл именно этот процесс.

Минимальный снимок отвечает на четыре проверяемых вопроса: какая версия Windows PowerShell запустила команду; какие папки стоят в PATH и какие расширения допускает PATHEXT; какие кандидаты вернул Get-Command -All; что напечатала сама программа для --version. В отдельной строке оставляю активную кодовую страницу консоли. Этого достаточно для первого сравнения, но в снимок не попадают токены, cookie, пароли, содержимое домашней папки и полный дамп всех переменных.

Схема минимального снимка Windows: проект запускает PowerShell, он передаёт дочерним процессам PATH и PATHEXT, затем фиксируются найденный бинарник, версия и кодовая страница в JSON без секретных переменных.
Снимок описывает путь запуска команды. Он не является резервной копией компьютера и перед отправкой всё равно требует просмотра.

Что кладу в файл, а что оставляю на машине

Поле снимкаКак получитьЗачем оно нужноЧего в нём не должно быть
Версия 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, я не дописываю путь в системные настройки наугад: сначала смотрю кандидатов и документацию самого проекта.

Короткий порядок работы

  1. В README назвать команды проекта и инструменты, которые им нужны: например, php, composer, node и git.
  2. Сохранить скрипт в репозитории или рядом с ним, но добавить созданный JSON в ignore, если он содержит локальные пути.
  3. Запустить снимок из обычной консоли и, при спорном случае, повторить запуск с -NoProfile.
  4. Проверить для каждого инструмента первый кандидат, остальные кандидаты и фактический вывод --version.
  5. Перед передачей отчёта убрать имя пользователя, внутренние каталоги и всё, что не требуется для разбора.
  6. После правки среды открыть новую консоль, снять новый отчёт и сравнить именно изменившиеся строки.

Где минимальный снимок не отвечает

Такой файл не доказывает, что зависимости проекта совпадают. Он не заменяет lock-файл, исходный код, права доступа к каталогу, настройки прокси и архитектуру 32/64 bit. Кодовая страница консоли тоже не равна кодировке каждого файла: файл может быть сохранён в другой кодировке, а программа может читать его своим правилом. Если симптом связан с файлами, отдельно проверяю байты файла и настройки конкретного компилятора, а не объявляю chcp единственной причиной.

Не стоит хранить снимок годами как истину. После обновления PHP, Node или Git он честно меняется. Его ценность в другом: у команды есть маленькая запись «что именно запускалось в этот день» и воспроизводимый способ обновить её без переустановки всего компьютера.

Что считаю готовым

Задача закрыта, когда новый разработчик может открыть JSON и ответить на три вопроса: какая консоль работала, откуда она взяла нужный бинарник и какую версию тот напечатал. Если один из ответов отсутствует, следующий шаг тоже ясен — дополнить снимок конкретным инструментом, а не обмениваться фразой «у меня работает».

Проверяемые источники