DarkRiDDeR12 мин

Windows. Разбор «работает на моей машине» без переустановки среды

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

Симптом: коллега присылает фразу «на моей машине работает», а на второй Windows-машине та же команда падает или собирает другой результат. Цена поспешного ответа — потерянный след: переустановка инструмента, очистка каталогов и правка глобального PATH меняют несколько переменных сразу и уже не дают понять, что было причиной.

Разберём один вопрос: как найти различие между двумя Windows-окружениями, не превращая диагностику в серию случайных установок? Ниже учебный разбор. Пути и версии в нём условные, а не рассказ о чужом компьютере; важен порядок сбора доказательств и одна обратимая проверка за раз.

Сначала делаю два запуска сравнимыми

До снимка фиксирую одинаковые входные данные: один commit проекта, одинаковую команду, наличие lock-файла и каталог, из которого она запущена. Если на одной машине уже изменён package-lock.json или добавлены локальные правки, сравнение среды смешается со сравнением проекта. В этом случае сначала сохраняю статус VCS и называю различие, а затем возвращаюсь к окружению.

На обеих машинах запускаю один и тот же Capture-Environment.ps1 из предыдущей заметки. Две копии JSON называю machine-a.json и machine-b.json. Перед отправкой в общий канал убираю личные части путей. Нам не нужен полный профиль пользователя; для расследования нужны порядок папок, кандидаты команд, версия PowerShell и признаки консоли.

Диагностический путь для двух Windows-машин: одинаковая команда и commit, два очищенных снимка окружения, сравнение PATH и кандидатов бинарника, временный опыт в новом PowerShell, затем повтор исходной команды.
Сравнение не выбирает настройку за человека. Оно сужает вопрос до одного наблюдаемого различия и оставляет обратимый шаг проверки.

Учебный пример: один репозиторий, два кандидата node

Представим, что на машине A Get-Command node -All первым показывает C:\Tools\node-6\node.exe, а на машине B — C:\Program Files\nodejs\node.exe. В JSON это не просто две строки с разными цифрами. На машине A первая папка из PATH раньше; на машине B этой папки нет. Пока не известно, какую версию требует проект, нельзя объявить один компьютер «правильным» и переносить его путь на второй.

Поле сравненияМашина A в учебном примереМашина B в учебном примереЧто это доказывает
Первый кандидат nodeC:\Tools\node-6\node.exeC:\Program Files\nodejs\node.exePowerShell может запускать разные файлы при одинаковом имени команды
Позиция каталога в PATHC:\Tools\node-6 стоит раньшеЭтого каталога нетРазличие объясняет порядок поиска, но ещё не требуемую версию проекта
node --versionУсловно v6.xУсловно v8.xВерсия должна быть зафиксирована рядом с зависимостями проекта
Кодовая страницаНапример, один текст chcpДругая строка chcpНечитаемый вывод может потребовать отдельной проверки, но не меняет путь к бинарнику
Исходная командаПадает или даёт другой логРаботает в учебном случаеЭто повод проверить одну гипотезу, а не удалить всё окружение A

Превращаю JSON в сравнимые строки

В снимке есть время создания и путь каждого кандидата, поэтому сырой текст файлов неудобно сравнивать целиком. Небольшая функция ниже выбирает поля, которые относятся к запуску. Она не скрывает порядок PATH: каждая папка остаётся отдельной строкой. Если на машине не найден инструмент, функция записывает это явно вместо пустого объекта.

# tools/Compare-Environment.ps1
function Get-SnapshotLines {
  param([string]$SnapshotPath)

  $snapshot = Get-Content -LiteralPath $SnapshotPath -Raw | ConvertFrom-Json
  $lines = @(
    "powershell=" + $snapshot.powershell.version
    "consoleCodePage=" + $snapshot.console.activeCodePage
    "outputEncoding=" + $snapshot.console.outputEncoding
    "pathext=" + $snapshot.environment.pathext
  )

  foreach ($entry in @($snapshot.environment.pathEntries)) {
    $lines += "path=" + $entry
  }

  foreach ($tool in @($snapshot.commands)) {
    $first = @($tool.candidates | Select-Object -First 1)
    if ($first.Count -eq 0) {
      $lines += $tool.name + "=NOT FOUND"
      continue
    }
    $candidate = $first[0]
    $lines += ("{0}={1}|{2}|{3}" -f $tool.name, $candidate.commandType, $candidate.definition, $candidate.version)
  }

  return $lines
}

$machineA = Get-SnapshotLines .\machine-a.json
$machineB = Get-SnapshotLines .\machine-b.json
Compare-Object -ReferenceObject $machineA -DifferenceObject $machineB

Compare-Object показывает строки, существующие только в одной из сторон. Его стрелки не являются диагнозом: они лишь говорят, в каком файле встретилась строка. Сначала смотрю пару «первый кандидат инструмента + соответствующая папка PATH», затем проверяю версию командой. Если отличаются десять строк, не исправляю все десять. Выбираю различие, которое прямо связано с исходной ошибкой.

Проверяю гипотезу в новом сеансе

Допустим, README проекта требует конкретную версию Node, а учебный снимок A показывает старый бинарник раньше нужного. Сначала открываю новый PowerShell и временно добавляю каталог с нужной версией в начало $env:Path. Это действие влияет только на текущий процесс и всё, что будет запущено из него. Если исходная команда не изменилась, гипотеза про порядок PATH не подтвердилась — возвращаюсь к логу, а не переношу путь в системные переменные.

# Новый PowerShell. Путь взят из документации проекта, не из случайного каталога.
$requiredNode = "C:\Tools\node-8"
if (-not (Test-Path (Join-Path $requiredNode "node.exe"))) {
  throw "В каталоге нет node.exe: $requiredNode"
}

$env:Path = $requiredNode + ";" + $env:Path
Get-Command node -All |
  Select-Object CommandType, Name, Version, Definition |
  Format-Table -AutoSize
node --version

# Только после этих строк повторяется исходная команда проекта.
npm run build

Этот пример не предлагает брать C:\Tools\node-8 из статьи. Каталог и версия должны быть определены проектом или его официальной документацией. Если файл не найден, скрипт останавливается с понятной ошибкой. Он не скачивает исполняемый файл, не меняет PATH на уровне пользователя или системы и не требует отключать политику запуска сценариев.

Как отделяю PATH, кодировку и версию

Три различия часто приходят вместе, но их нельзя лечить одним действием. Старый бинарник проявляется через путь и --version. Кодовая страница проявляется через нечитаемый вывод и значения chcp или OutputEncoding. Версия зависимостей проекта проявляется через lock-файл и лог пакетного менеджера. Если меняется только текст сообщения, а путь и версия совпали, спор про PATH стоит прекратить.

ГипотезаМинимальное доказательствоОбратимое действиеКогда остановиться
В PATH раньше старая папкаПервый кандидат и первая отличающаяся строка путиДобавить документированный каталог только в $env:Path нового PowerShellЕсли версия или исходная ошибка не изменились
Профиль подменяет командуGet-Command -All показывает функцию или aliasСравнить запуск с -NoProfileЕсли тип кандидата остаётся Application и путь тот же
Консоль портит сообщениеРазличаются chcp и настройка вывода при одинаковом бинарникеОткрыть новую консоль и проверить вывод одной командыЕсли проблема относится к сохранённому файлу, а не к консоли
Проект требует другую версиюREADME или конфигурация проекта прямо называет версию, а --version не совпадаетИспользовать одобренную поставку этой версии в отдельном сеансеЕсли после совпадения версии ошибка переходит к другой причине

Порядок полевого разбора

  1. Сохранить commit, lock-файл и точный запуск, который расходится на двух машинах.
  2. Снять очищенные отчёты из одинакового PowerShell и не публиковать в них секреты или персональные пути.
  3. Сравнить PowerShell, кодовую страницу, порядок PATH, PATHEXT, первого кандидата и вывод версии.
  4. Выбрать одно отличие, напрямую связанное с ошибкой, и проверить его в новом сеансе только через процессное $env:Path.
  5. Повторить исходную команду и сохранить новый снимок, если результат изменился.
  6. Лишь после подтверждения обновить README или согласованный способ установки; ненужные глобальные правки не оставлять.

Что останется вне этого разбора

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

Также не стоит превращать учебный дифф в обязательный корпоративный формат. Для маленького проекта достаточно текстового снимка и одной команды сравнения. Главное — чтобы следующий человек мог повторить путь: увидеть исходную ошибку, сравнить один набор полей, провести обратимый опыт и сказать, что именно изменилось.

Итог: вместо «у меня работает»

Фраза становится полезной, когда к ней приложены три вещи: команда, снимок процесса и сравнение кандидата бинарника. Тогда диагностика не начинается с переустановки. Мы сначала видим отличие, проверяем его в новом сеансе и только затем решаем, нужно ли менять постоянную настройку. В большинстве локальных случаев этого достаточно, чтобы вернуть разговор от мнений к наблюдаемым данным.

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