Симптом: коллега присылает фразу «на моей машине работает», а на второй Windows-машине та же команда падает или собирает другой результат. Цена поспешного ответа — потерянный след: переустановка инструмента, очистка каталогов и правка глобального PATH меняют несколько переменных сразу и уже не дают понять, что было причиной.
Разберём один вопрос: как найти различие между двумя Windows-окружениями, не превращая диагностику в серию случайных установок? Ниже учебный разбор. Пути и версии в нём условные, а не рассказ о чужом компьютере; важен порядок сбора доказательств и одна обратимая проверка за раз.
Сначала делаю два запуска сравнимыми
До снимка фиксирую одинаковые входные данные: один commit проекта, одинаковую команду, наличие lock-файла и каталог, из которого она запущена. Если на одной машине уже изменён package-lock.json или добавлены локальные правки, сравнение среды смешается со сравнением проекта. В этом случае сначала сохраняю статус VCS и называю различие, а затем возвращаюсь к окружению.
На обеих машинах запускаю один и тот же Capture-Environment.ps1 из предыдущей заметки. Две копии JSON называю machine-a.json и machine-b.json. Перед отправкой в общий канал убираю личные части путей. Нам не нужен полный профиль пользователя; для расследования нужны порядок папок, кандидаты команд, версия 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 в учебном примере | Что это доказывает |
|---|---|---|---|
Первый кандидат node | C:\Tools\node-6\node.exe | C:\Program Files\nodejs\node.exe | PowerShell может запускать разные файлы при одинаковом имени команды |
Позиция каталога в PATH | C:\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 не совпадает | Использовать одобренную поставку этой версии в отдельном сеансе | Если после совпадения версии ошибка переходит к другой причине |
Порядок полевого разбора
- Сохранить commit, lock-файл и точный запуск, который расходится на двух машинах.
- Снять очищенные отчёты из одинакового PowerShell и не публиковать в них секреты или персональные пути.
- Сравнить PowerShell, кодовую страницу, порядок
PATH,PATHEXT, первого кандидата и вывод версии. - Выбрать одно отличие, напрямую связанное с ошибкой, и проверить его в новом сеансе только через процессное
$env:Path. - Повторить исходную команду и сохранить новый снимок, если результат изменился.
- Лишь после подтверждения обновить README или согласованный способ установки; ненужные глобальные правки не оставлять.
Что останется вне этого разбора
Два одинаковых снимка не гарантируют одинаковую сборку. Причина может жить в правах на каталог, сертификате прокси, сетевом доступе, разрядности, нативной библиотеке, антивирусе, содержимом кеша или строках окончания файла. Эти варианты проверяются следующими отдельными наблюдениями. Нельзя делать вывод, что защита мешает проекту, и отключать её без подтверждённой причины и разрешённого процесса.
Также не стоит превращать учебный дифф в обязательный корпоративный формат. Для маленького проекта достаточно текстового снимка и одной команды сравнения. Главное — чтобы следующий человек мог повторить путь: увидеть исходную ошибку, сравнить один набор полей, провести обратимый опыт и сказать, что именно изменилось.
Итог: вместо «у меня работает»
Фраза становится полезной, когда к ней приложены три вещи: команда, снимок процесса и сравнение кандидата бинарника. Тогда диагностика не начинается с переустановки. Мы сначала видим отличие, проверяем его в новом сеансе и только затем решаем, нужно ли менять постоянную настройку. В большинстве локальных случаев этого достаточно, чтобы вернуть разговор от мнений к наблюдаемым данным.