426 lines
51 KiB
JavaScript
426 lines
51 KiB
JavaScript
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const escapeHtml = (value) => String(value)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
|
||
const paragraph = (content) => '<p>' + content + '</p>';
|
||
const heading = (content) => '<h2>' + content + '</h2>';
|
||
const codeBlock = (source) => '<pre><code>' + escapeHtml(source.trim()) + '</code></pre>';
|
||
const figure = (src, alt, caption) => [
|
||
'<figure>',
|
||
'<img src="' + src + '" alt="' + alt + '" />',
|
||
'<figcaption>' + caption + '</figcaption>',
|
||
'</figure>',
|
||
].join('');
|
||
|
||
function dataTable(headers, rows) {
|
||
const head = headers.map((header) => '<th scope="col">' + header + '</th>').join('');
|
||
const body = rows.map((row) => (
|
||
'<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>'
|
||
)).join('');
|
||
|
||
return '<div class="table-scroll"><table><thead><tr>' + head
|
||
+ '</tr></thead><tbody>' + body + '</tbody></table></div>';
|
||
}
|
||
|
||
function orderedList(items) {
|
||
return '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||
}
|
||
|
||
function sourceList(items) {
|
||
return heading('Проверяемые источники') + '<ul>' + items.map(({ label, url }) => (
|
||
'<li><a href="' + url + '" target="_blank" rel="noopener noreferrer">' + label + '</a></li>'
|
||
)).join('') + '</ul>';
|
||
}
|
||
|
||
const sources = {
|
||
environment: {
|
||
label: 'Microsoft Learn — about_Environment_Variables для Windows PowerShell',
|
||
url: 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1',
|
||
},
|
||
processEnvironment: {
|
||
label: 'Microsoft Learn — Environment Variables (Win32)',
|
||
url: 'https://learn.microsoft.com/en-us/windows/win32/procthread/environment-variables',
|
||
},
|
||
getCommand: {
|
||
label: 'Microsoft Learn — Get-Command и параметр -All',
|
||
url: 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-command?view=powershell-5.1',
|
||
},
|
||
where: {
|
||
label: 'Microsoft Learn — команда where',
|
||
url: 'https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/where',
|
||
},
|
||
chcp: {
|
||
label: 'Microsoft Learn — команда chcp',
|
||
url: 'https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/chcp',
|
||
},
|
||
convertToJson: {
|
||
label: 'Microsoft Learn — ConvertTo-Json',
|
||
url: 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-json?view=powershell-5.1',
|
||
},
|
||
compareObject: {
|
||
label: 'Microsoft Learn — Compare-Object',
|
||
url: 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/compare-object?view=powershell-5.1',
|
||
},
|
||
};
|
||
|
||
const practiceArticle = {
|
||
slug: 'editorial-2018-09-practice-windows-dev-env',
|
||
title: 'Windows. Как сохранить минимальный снимок окружения проекта',
|
||
categories: ['Windows', 'Инструменты'],
|
||
cover: '/assets/editorial/2018/windows-dev-env-snapshot-2018.svg',
|
||
excerpt: 'Фиксируем то, что реально запускает проект на Windows: порядок PATH, найденные бинарники, версии и кодировку консоли — без копирования всего компьютера и без секретов.',
|
||
readingMinutes: 11,
|
||
contentHtml: [
|
||
paragraph('На одном Windows-компьютере проект собирается, а на другом команда <code>npm run build</code> не находит <code>node</code> или запускает другую версию PHP. Цена такой ошибки — не только потерянный вечер: в спешке легко добавить в <code>PATH</code> случайную папку, переслать коллеге пароль из переменной среды или поставить непонятный архив с интерпретатором.'),
|
||
paragraph('Здесь один вопрос: <strong>какой минимальный снимок окружения нужен, чтобы другой разработчик увидел тот же запуск проекта?</strong> Не будем архивировать весь диск и делать вид, что любая разница машины важна. Зафиксируем только то, что влияет на поиск команд, их версию и текст, который видит консоль.'),
|
||
heading('Граница задачи: сохраняю запуск, а не весь компьютер'),
|
||
paragraph('Окружение процесса — это набор строк, с которым стартует конкретная консоль и её дочерние программы. В Windows есть пользовательский, системный и процессный уровни переменных. Уже открытый PowerShell не обязан получить изменения, сделанные в окне настроек: новая консоль наследует новое значение, старая продолжает работать со своим набором. Поэтому запись «у меня установлен Node» ничего не объясняет, пока не известно, какой <code>node.exe</code> нашёл именно этот процесс.'),
|
||
paragraph('Минимальный снимок отвечает на четыре проверяемых вопроса: какая версия Windows PowerShell запустила команду; какие папки стоят в <code>PATH</code> и какие расширения допускает <code>PATHEXT</code>; какие кандидаты вернул <code>Get-Command -All</code>; что напечатала сама программа для <code>--version</code>. В отдельной строке оставляю активную кодовую страницу консоли. Этого достаточно для первого сравнения, но в снимок не попадают токены, cookie, пароли, содержимое домашней папки и полный дамп всех переменных.'),
|
||
figure(
|
||
'/assets/editorial/2018/windows-dev-env-snapshot-2018.svg',
|
||
'Схема минимального снимка Windows: проект запускает PowerShell, он передаёт дочерним процессам PATH и PATHEXT, затем фиксируются найденный бинарник, версия и кодовая страница в JSON без секретных переменных.',
|
||
'Снимок описывает путь запуска команды. Он не является резервной копией компьютера и перед отправкой всё равно требует просмотра.',
|
||
),
|
||
heading('Что кладу в файл, а что оставляю на машине'),
|
||
dataTable(
|
||
['Поле снимка', 'Как получить', 'Зачем оно нужно', 'Чего в нём не должно быть'],
|
||
[
|
||
['Версия PowerShell', '<code>$PSVersionTable.PSVersion</code>', 'От неё зависят доступные команды и формат части вывода', 'Имя пользователя и содержимое профиля'],
|
||
['Порядок <code>PATH</code>', '<code>$env:Path -split ";"</code>', 'Показывает, какая папка может дать первый бинарник', 'Полный список случайных переменных среды'],
|
||
['<code>PATHEXT</code>', '<code>$env:PATHEXT</code>', 'Объясняет, какие расширения Windows считает исполняемыми', 'Изменение системного значения ради эксперимента'],
|
||
['Кандидаты команды', '<code>Get-Command node -All</code>', 'Отделяет alias или функцию от настоящего <code>node.exe</code>', 'Непроверенный вывод из чужого скриншота'],
|
||
['Версия инструмента', '<code>node --version</code>', 'Связывает найденный путь с фактическим запуском', 'Секреты, переданные приложению аргументами'],
|
||
['Кодовая страница', '<code>cmd /c chcp</code>', 'Помогает объяснить нечитаемый вывод старой консоли', 'Глобальное переключение кодировки без проверки'],
|
||
],
|
||
),
|
||
heading('Один скрипт для Windows PowerShell 5.1'),
|
||
paragraph('Ниже не установщик и не «починка» окружения. Он только собирает отчёт рядом с проектом. Список <code>$toolChecks</code> нужно оставить коротким: добавьте туда реальные инструменты проекта, например <code>php</code> и <code>composer</code>, а не все программы из меню Пуск. Команда <code>Get-Command -All</code> намеренно сохраняет все найденные варианты, потому что первый путь без остальных кандидатов часто скрывает причину расхождения.'),
|
||
codeBlock([
|
||
'# 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)",
|
||
].join('\n')),
|
||
paragraph('В отчёт попадает только заранее выбранный набор полей. Это полезнее, чем <code>Get-ChildItem Env:</code> целиком: там могут оказаться адрес прокси, ключ приложения или служебный путь. Сам <code>PATH</code> тоже способен раскрыть имя локального пользователя и внутренние каталоги. Перед публикацией в задаче или чате открываю JSON, заменяю такие части на нейтральные метки и сохраняю исходник только в закрытом месте.'),
|
||
heading('Снимаю отчёт в отдельном сеансе и читаю его как данные'),
|
||
paragraph('Для первой проверки полезен PowerShell без профиля: так случайная функция из <code>$PROFILE</code> не выдаст себя за установленный инструмент. Это не отменяет проверку обычной рабочей консоли. Наоборот, если снимки расходятся, профиль становится одной из гипотез, которую можно подтвердить отдельным запуском.'),
|
||
codeBlock([
|
||
'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',
|
||
].join('\n')),
|
||
paragraph('Готовый запуск не обязан вернуть все четыре инструмента. Для проекта на PHP отсутствие <code>node</code> может быть нормальным, а отсутствие <code>php</code> — нет. Важен не список «зелёных» строк, а заранее оговорённый набор. Если <code>versionOutput</code> содержит <code>NOT FOUND</code>, я не дописываю путь в системные настройки наугад: сначала смотрю кандидатов и документацию самого проекта.'),
|
||
heading('Короткий порядок работы'),
|
||
orderedList([
|
||
'В README назвать команды проекта и инструменты, которые им нужны: например, <code>php</code>, <code>composer</code>, <code>node</code> и <code>git</code>.',
|
||
'Сохранить скрипт в репозитории или рядом с ним, но добавить созданный JSON в ignore, если он содержит локальные пути.',
|
||
'Запустить снимок из обычной консоли и, при спорном случае, повторить запуск с <code>-NoProfile</code>.',
|
||
'Проверить для каждого инструмента первый кандидат, остальные кандидаты и фактический вывод <code>--version</code>.',
|
||
'Перед передачей отчёта убрать имя пользователя, внутренние каталоги и всё, что не требуется для разбора.',
|
||
'После правки среды открыть новую консоль, снять новый отчёт и сравнить именно изменившиеся строки.',
|
||
]),
|
||
heading('Где минимальный снимок не отвечает'),
|
||
paragraph('Такой файл не доказывает, что зависимости проекта совпадают. Он не заменяет lock-файл, исходный код, права доступа к каталогу, настройки прокси и архитектуру 32/64 bit. Кодовая страница консоли тоже не равна кодировке каждого файла: файл может быть сохранён в другой кодировке, а программа может читать его своим правилом. Если симптом связан с файлами, отдельно проверяю байты файла и настройки конкретного компилятора, а не объявляю <code>chcp</code> единственной причиной.'),
|
||
paragraph('Не стоит хранить снимок годами как истину. После обновления PHP, Node или Git он честно меняется. Его ценность в другом: у команды есть маленькая запись «что именно запускалось в этот день» и воспроизводимый способ обновить её без переустановки всего компьютера.'),
|
||
heading('Что считаю готовым'),
|
||
paragraph('Задача закрыта, когда новый разработчик может открыть JSON и ответить на три вопроса: какая консоль работала, откуда она взяла нужный бинарник и какую версию тот напечатал. Если один из ответов отсутствует, следующий шаг тоже ясен — дополнить снимок конкретным инструментом, а не обмениваться фразой «у меня работает».'),
|
||
sourceList([sources.environment, sources.getCommand, sources.chcp, sources.convertToJson]),
|
||
].join('\n'),
|
||
};
|
||
|
||
const mechanismArticle = {
|
||
slug: 'editorial-2018-09-mechanism-windows-dev-env',
|
||
title: 'Windows. Почему PATH, кодировка и версия бинарника меняют запуск проекта',
|
||
categories: ['Windows', 'Инструменты'],
|
||
cover: '/assets/editorial/2018/windows-dev-env-resolution-2018.svg',
|
||
excerpt: 'Разбираем один механизм: как PowerShell выбирает команду, почему where.exe не заменяет Get-Command -All и где кодовая страница действительно влияет на диагностику.',
|
||
readingMinutes: 11,
|
||
contentHtml: [
|
||
paragraph('В одной консоли <code>node --version</code> показывает ожидаемую версию, в другой — старую, а русское сообщение инструмента превращается в набор знаков. Цена ошибки выше одной неудачной сборки: можно исправить не тот <code>node.exe</code>, сохранить повреждённый текстовый файл или навсегда засорить системный <code>PATH</code> ради разового опыта.'),
|
||
paragraph('Разберём один вопрос: <strong>как Windows и PowerShell выбирают бинарник и почему рядом с этим выбором приходится проверять кодировку?</strong> Сначала увидим путь команды в текущем процессе, затем отдельно посмотрим кодовую страницу. Это две связанные проверки, но не одна «настройка окружения».'),
|
||
heading('У процесса свой набор переменных'),
|
||
paragraph('Переменная среды всегда строка, а дочерний процесс наследует её от родителя. На Windows значения могут существовать в системной, пользовательской и процессной области. Когда мы присваиваем <code>$env:Path</code> в текущем PowerShell, меняется только этот сеанс и программы, запущенные из него. Такой короткий опыт безопаснее постоянной правки: можно поставить папку с нужной версией первой, посмотреть результат и закрыть окно, если гипотеза не подтвердилась.'),
|
||
paragraph('Из этого следует простой, но важный вывод. Снимок <code>PATH</code> из нового окна и снимок из уже открытой консоли могут различаться без ошибки Windows. Если правка сделана в настройках пользователя или системы, старое приложение не переписывает свой блок переменных само. Для проверки открываю новую консоль и фиксирую время снимка, а не ожидаю, что одна строка из реестра мгновенно объяснит чужой терминал.'),
|
||
figure(
|
||
'/assets/editorial/2018/windows-dev-env-resolution-2018.svg',
|
||
'Схема выбора команды в Windows PowerShell: процесс получает PATH и PATHEXT, Get-Command -All показывает функции, alias и приложения в порядке выбора, where.exe ищет файлы, а кодовая страница проверяется отдельной веткой.',
|
||
'Путь к бинарнику и способ отображения его сообщения надо проверять отдельно: один отвечает за запуск, второй — за читаемость вывода.',
|
||
),
|
||
heading('PATH и PATHEXT отвечают только за часть выбора'),
|
||
paragraph('<code>PATH</code> — это список каталогов, где система ищет исполняемые файлы. В Windows его элементы разделены точкой с запятой. <code>PATHEXT</code> задаёт расширения, которые считаются исполняемыми, поэтому имя <code>tool</code> может привести к <code>tool.exe</code> или <code>tool.cmd</code>. Но для PowerShell этого мало: перед приложением может оказаться alias, функция, cmdlet или внешний скрипт с тем же именем.'),
|
||
paragraph('Поэтому <code>where.exe node</code> полезен, но не является окончательным ответом. Он ищет файлы в текущем каталоге и папках <code>PATH</code>; он не покажет функцию <code>node</code>, определённую в профиле PowerShell. <code>Get-Command node -All</code> выводит все варианты в порядке, который PowerShell использует при выборе. В диагностике я запускаю обе команды: первая хорошо показывает физические файлы, вторая — фактическую команду оболочки.'),
|
||
codeBlock([
|
||
'$names = @("node", "npm", "php", "git")',
|
||
'',
|
||
'foreach ($name in $names) {',
|
||
' Write-Host ""',
|
||
' Write-Host ("== " + $name + " ==")',
|
||
' Get-Command -Name $name -All -ErrorAction SilentlyContinue |',
|
||
' Select-Object CommandType, Name, Version, Source, Definition |',
|
||
' Format-Table -AutoSize',
|
||
'',
|
||
' Write-Host "-- files found by where.exe --"',
|
||
' where.exe $name 2>$null',
|
||
'',
|
||
' if (Get-Command -Name $name -ErrorAction SilentlyContinue) {',
|
||
' & $name --version',
|
||
' Write-Host ("exitCode=" + $LASTEXITCODE)',
|
||
' }',
|
||
'}',
|
||
].join('\n')),
|
||
paragraph('У этой проверки есть ограничение: аргумент <code>--version</code> подходит не каждой утилите. Для старого проекта список аргументов лучше хранить рядом с проектом, а не использовать универсальный запуск. Если команда является функцией, её вывод ещё не доказывает путь к приложению. В таком случае сначала называю тип кандидата, затем проверяю его определение и решаю, является ли это частью проекта или случайной надстройкой профиля.'),
|
||
heading('Кодовая страница — отдельный слой'),
|
||
paragraph('<code>chcp</code> показывает активную кодовую страницу консоли и умеет её менять. Документация Windows отдельно оговаривает, что программы, запущенные после смены страницы, используют новое значение, а уже запущенные программы обычно сохраняют прежнее. Это объясняет частый ложный вывод «я поменял кодировку, но ошибка осталась»: проверка сделана в процессе, который стартовал раньше.'),
|
||
paragraph('Для разбора не надо немедленно переключать консоль на 65001 или менять системный язык. Сначала записываю три наблюдения: строку <code>chcp</code>, <code>[Console]::OutputEncoding.WebName</code> и байты проблемного файла, если проблема именно в файле. Кодовая страница <code>cmd.exe</code>, настройка вывода .NET и кодировка файла — разные вещи. Совпадение двух из них ничего не гарантирует, а изменение одного не лечит остальные.'),
|
||
codeBlock([
|
||
'# Наблюдение, без изменения глобальных настроек.',
|
||
'& cmd.exe /d /c chcp',
|
||
'[Console]::OutputEncoding.WebName',
|
||
'[Text.Encoding]::Default.WebName',
|
||
'',
|
||
'# Короткий опыт только в текущем PowerShell.',
|
||
'$projectTools = Join-Path $PSScriptRoot "tools"',
|
||
'$env:Path = $projectTools + ";" + $env:Path',
|
||
'',
|
||
'Get-Command node -All |',
|
||
' Select-Object CommandType, Name, Version, Definition |',
|
||
' Format-Table -AutoSize',
|
||
'node --version',
|
||
].join('\n')),
|
||
paragraph('Последние четыре строки намеренно меняют только процессную область. Если нужный <code>node.exe</code> оказался первым и проект прошёл документированную команду, гипотеза подтверждена. Если нет, закрываю окно — постоянного следа не осталось. Лишь после такой проверки имеет смысл обсуждать установщик, путь в пользовательском <code>PATH</code> или явный путь в скрипте проекта.'),
|
||
heading('Как не спутать три похожих симптома'),
|
||
dataTable(
|
||
['Наблюдение', 'Что оно означает', 'Чем проверить', 'Следующее действие'],
|
||
[
|
||
['<code>node --version</code> даёт старую версию', 'Выбран другой кандидат или путь в <code>PATH</code> стоит раньше', '<code>Get-Command node -All</code> и <code>where.exe node</code>', 'Временно поставить нужную папку первой в текущем сеансе и повторить версию'],
|
||
['<code>where.exe</code> показывает два файла, а PowerShell запускает функцию', 'Физические файлы не описывают приоритет PowerShell', '<code>Get-Command node -All</code> с колонкой <code>CommandType</code>', 'Проверить профиль и не менять <code>PATH</code>, пока функция не исключена'],
|
||
['Русский вывод нечитаем только в одной консоли', 'Различается кодовая страница или настройка вывода', '<code>chcp</code> и <code>[Console]::OutputEncoding</code>', 'Сравнить новую консоль и конкретный способ записи файла'],
|
||
['После правки переменной результат прежний', 'Работает старый процесс с унаследованными значениями', 'Снимок из нового PowerShell без профиля', 'Открыть новый сеанс и повторить одну исходную команду'],
|
||
['Сборка читает верный Node, но падает дальше', 'Причина не в поиске бинарника', 'Lock-файл, лог команды, права и конфигурация проекта', 'Не продолжать править <code>PATH</code>; расследовать следующий симптом'],
|
||
],
|
||
),
|
||
heading('Последовательность проверки'),
|
||
orderedList([
|
||
'Зафиксировать одну воспроизводимую команду проекта и её полный текст ошибки, не меняя настройки заранее.',
|
||
'В той же консоли вывести <code>Get-Command -All</code>, <code>where.exe</code> и <code>--version</code> для нужного инструмента.',
|
||
'Открыть новый PowerShell с <code>-NoProfile</code> и повторить ровно те же три наблюдения.',
|
||
'Если расходится путь, добавить нужный каталог только в <code>$env:Path</code> текущего сеанса и проверить исходную команду.',
|
||
'Если расходится текст, записать <code>chcp</code>, настройку вывода и кодировку конкретного файла, не подменяя одну проверку другой.',
|
||
'Постоянное изменение делать лишь после того, как временный опыт дал понятный результат и его можно описать в README.',
|
||
]),
|
||
heading('Границы такого объяснения'),
|
||
paragraph('Механизм выбора команды не объясняет всё. Бинарники могут отличаться разрядностью, зависимыми DLL, правами запуска, содержимым каталога или настройками антивируса и прокси. Эти ограничения не повод отключать защиту и начинать с переустановки Windows. Они означают, что после совпадения пути и версии нужно смотреть следующий наблюдаемый факт — лог приложения, разрешения каталога или сетевой запрос.'),
|
||
paragraph('Так же осторожно отношусь к кодировке. <code>chcp</code> нужен для проверки активной консоли, но не является рецептом «всегда ставить UTF-8». У старых программ и редакторов могут быть свои ожидания. Воспроизводимым считается результат, когда одна и та же программа из новой консоли выдаёт читаемый текст и открывает нужные файлы по документированному правилу.'),
|
||
heading('Итог механизма'),
|
||
paragraph('Имя команды не равно конкретному файлу. В Windows PowerShell между ними стоят процессное окружение, порядок <code>PATH</code>, <code>PATHEXT</code> и приоритет alias, функций, cmdlet и приложений. Рядом живёт отдельный слой отображения текста. Когда эти два пути проверены отдельными командами, «на моей машине другая версия» превращается в строку, которую можно сравнить и исправить без гадания.'),
|
||
sourceList([sources.environment, sources.processEnvironment, sources.getCommand, sources.where, sources.chcp]),
|
||
].join('\n'),
|
||
};
|
||
|
||
const fieldArticle = {
|
||
slug: 'editorial-2018-09-field-windows-dev-env',
|
||
title: 'Windows. Разбор «работает на моей машине» без переустановки среды',
|
||
categories: ['Windows', 'Инструменты'],
|
||
cover: '/assets/editorial/2018/windows-dev-env-diff-2018.svg',
|
||
excerpt: 'Полевой маршрут для двух Windows-машин: сравнить снимки, отделить PATH от кодировки и версии бинарника, проверить одну гипотезу в новом сеансе и не стереть полезные доказательства.',
|
||
readingMinutes: 12,
|
||
contentHtml: [
|
||
paragraph('Симптом: коллега присылает фразу «на моей машине работает», а на второй Windows-машине та же команда падает или собирает другой результат. Цена поспешного ответа — потерянный след: переустановка инструмента, очистка каталогов и правка глобального <code>PATH</code> меняют несколько переменных сразу и уже не дают понять, что было причиной.'),
|
||
paragraph('Разберём один вопрос: <strong>как найти различие между двумя Windows-окружениями, не превращая диагностику в серию случайных установок?</strong> Ниже учебный разбор. Пути и версии в нём условные, а не рассказ о чужом компьютере; важен порядок сбора доказательств и одна обратимая проверка за раз.'),
|
||
heading('Сначала делаю два запуска сравнимыми'),
|
||
paragraph('До снимка фиксирую одинаковые входные данные: один commit проекта, одинаковую команду, наличие lock-файла и каталог, из которого она запущена. Если на одной машине уже изменён <code>package-lock.json</code> или добавлены локальные правки, сравнение среды смешается со сравнением проекта. В этом случае сначала сохраняю статус VCS и называю различие, а затем возвращаюсь к окружению.'),
|
||
paragraph('На обеих машинах запускаю один и тот же <code>Capture-Environment.ps1</code> из предыдущей заметки. Две копии JSON называю <code>machine-a.json</code> и <code>machine-b.json</code>. Перед отправкой в общий канал убираю личные части путей. Нам не нужен полный профиль пользователя; для расследования нужны порядок папок, кандидаты команд, версия PowerShell и признаки консоли.'),
|
||
figure(
|
||
'/assets/editorial/2018/windows-dev-env-diff-2018.svg',
|
||
'Диагностический путь для двух Windows-машин: одинаковая команда и commit, два очищенных снимка окружения, сравнение PATH и кандидатов бинарника, временный опыт в новом PowerShell, затем повтор исходной команды.',
|
||
'Сравнение не выбирает настройку за человека. Оно сужает вопрос до одного наблюдаемого различия и оставляет обратимый шаг проверки.',
|
||
),
|
||
heading('Учебный пример: один репозиторий, два кандидата node'),
|
||
paragraph('Представим, что на машине A <code>Get-Command node -All</code> первым показывает <code>C:\\Tools\\node-6\\node.exe</code>, а на машине B — <code>C:\\Program Files\\nodejs\\node.exe</code>. В JSON это не просто две строки с разными цифрами. На машине A первая папка из <code>PATH</code> раньше; на машине B этой папки нет. Пока не известно, какую версию требует проект, нельзя объявить один компьютер «правильным» и переносить его путь на второй.'),
|
||
dataTable(
|
||
['Поле сравнения', 'Машина A в учебном примере', 'Машина B в учебном примере', 'Что это доказывает'],
|
||
[
|
||
['Первый кандидат <code>node</code>', '<code>C:\\Tools\\node-6\\node.exe</code>', '<code>C:\\Program Files\\nodejs\\node.exe</code>', 'PowerShell может запускать разные файлы при одинаковом имени команды'],
|
||
['Позиция каталога в <code>PATH</code>', '<code>C:\\Tools\\node-6</code> стоит раньше', 'Этого каталога нет', 'Различие объясняет порядок поиска, но ещё не требуемую версию проекта'],
|
||
['<code>node --version</code>', 'Условно <code>v6.x</code>', 'Условно <code>v8.x</code>', 'Версия должна быть зафиксирована рядом с зависимостями проекта'],
|
||
['Кодовая страница', 'Например, один текст <code>chcp</code>', 'Другая строка <code>chcp</code>', 'Нечитаемый вывод может потребовать отдельной проверки, но не меняет путь к бинарнику'],
|
||
['Исходная команда', 'Падает или даёт другой лог', 'Работает в учебном случае', 'Это повод проверить одну гипотезу, а не удалить всё окружение A'],
|
||
],
|
||
),
|
||
heading('Превращаю JSON в сравнимые строки'),
|
||
paragraph('В снимке есть время создания и путь каждого кандидата, поэтому сырой текст файлов неудобно сравнивать целиком. Небольшая функция ниже выбирает поля, которые относятся к запуску. Она не скрывает порядок <code>PATH</code>: каждая папка остаётся отдельной строкой. Если на машине не найден инструмент, функция записывает это явно вместо пустого объекта.'),
|
||
codeBlock([
|
||
'# 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',
|
||
].join('\n')),
|
||
paragraph('<code>Compare-Object</code> показывает строки, существующие только в одной из сторон. Его стрелки не являются диагнозом: они лишь говорят, в каком файле встретилась строка. Сначала смотрю пару «первый кандидат инструмента + соответствующая папка <code>PATH</code>», затем проверяю версию командой. Если отличаются десять строк, не исправляю все десять. Выбираю различие, которое прямо связано с исходной ошибкой.'),
|
||
heading('Проверяю гипотезу в новом сеансе'),
|
||
paragraph('Допустим, README проекта требует конкретную версию Node, а учебный снимок A показывает старый бинарник раньше нужного. Сначала открываю новый PowerShell и временно добавляю каталог с нужной версией в начало <code>$env:Path</code>. Это действие влияет только на текущий процесс и всё, что будет запущено из него. Если исходная команда не изменилась, гипотеза про порядок <code>PATH</code> не подтвердилась — возвращаюсь к логу, а не переношу путь в системные переменные.'),
|
||
codeBlock([
|
||
'# Новый 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',
|
||
].join('\n')),
|
||
paragraph('Этот пример не предлагает брать <code>C:\\Tools\\node-8</code> из статьи. Каталог и версия должны быть определены проектом или его официальной документацией. Если файл не найден, скрипт останавливается с понятной ошибкой. Он не скачивает исполняемый файл, не меняет <code>PATH</code> на уровне пользователя или системы и не требует отключать политику запуска сценариев.'),
|
||
heading('Как отделяю PATH, кодировку и версию'),
|
||
paragraph('Три различия часто приходят вместе, но их нельзя лечить одним действием. Старый бинарник проявляется через путь и <code>--version</code>. Кодовая страница проявляется через нечитаемый вывод и значения <code>chcp</code> или <code>OutputEncoding</code>. Версия зависимостей проекта проявляется через lock-файл и лог пакетного менеджера. Если меняется только текст сообщения, а путь и версия совпали, спор про <code>PATH</code> стоит прекратить.'),
|
||
dataTable(
|
||
['Гипотеза', 'Минимальное доказательство', 'Обратимое действие', 'Когда остановиться'],
|
||
[
|
||
['В <code>PATH</code> раньше старая папка', 'Первый кандидат и первая отличающаяся строка пути', 'Добавить документированный каталог только в <code>$env:Path</code> нового PowerShell', 'Если версия или исходная ошибка не изменились'],
|
||
['Профиль подменяет команду', '<code>Get-Command -All</code> показывает функцию или alias', 'Сравнить запуск с <code>-NoProfile</code>', 'Если тип кандидата остаётся <code>Application</code> и путь тот же'],
|
||
['Консоль портит сообщение', 'Различаются <code>chcp</code> и настройка вывода при одинаковом бинарнике', 'Открыть новую консоль и проверить вывод одной команды', 'Если проблема относится к сохранённому файлу, а не к консоли'],
|
||
['Проект требует другую версию', 'README или конфигурация проекта прямо называет версию, а <code>--version</code> не совпадает', 'Использовать одобренную поставку этой версии в отдельном сеансе', 'Если после совпадения версии ошибка переходит к другой причине'],
|
||
],
|
||
),
|
||
heading('Порядок полевого разбора'),
|
||
orderedList([
|
||
'Сохранить commit, lock-файл и точный запуск, который расходится на двух машинах.',
|
||
'Снять очищенные отчёты из одинакового PowerShell и не публиковать в них секреты или персональные пути.',
|
||
'Сравнить PowerShell, кодовую страницу, порядок <code>PATH</code>, <code>PATHEXT</code>, первого кандидата и вывод версии.',
|
||
'Выбрать одно отличие, напрямую связанное с ошибкой, и проверить его в новом сеансе только через процессное <code>$env:Path</code>.',
|
||
'Повторить исходную команду и сохранить новый снимок, если результат изменился.',
|
||
'Лишь после подтверждения обновить README или согласованный способ установки; ненужные глобальные правки не оставлять.',
|
||
]),
|
||
heading('Что останется вне этого разбора'),
|
||
paragraph('Два одинаковых снимка не гарантируют одинаковую сборку. Причина может жить в правах на каталог, сертификате прокси, сетевом доступе, разрядности, нативной библиотеке, антивирусе, содержимом кеша или строках окончания файла. Эти варианты проверяются следующими отдельными наблюдениями. Нельзя делать вывод, что защита мешает проекту, и отключать её без подтверждённой причины и разрешённого процесса.'),
|
||
paragraph('Также не стоит превращать учебный дифф в обязательный корпоративный формат. Для маленького проекта достаточно текстового снимка и одной команды сравнения. Главное — чтобы следующий человек мог повторить путь: увидеть исходную ошибку, сравнить один набор полей, провести обратимый опыт и сказать, что именно изменилось.'),
|
||
heading('Итог: вместо «у меня работает»'),
|
||
paragraph('Фраза становится полезной, когда к ней приложены три вещи: команда, снимок процесса и сравнение кандидата бинарника. Тогда диагностика не начинается с переустановки. Мы сначала видим отличие, проверяем его в новом сеансе и только затем решаем, нужно ли менять постоянную настройку. В большинстве локальных случаев этого достаточно, чтобы вернуть разговор от мнений к наблюдаемым данным.'),
|
||
sourceList([sources.environment, sources.getCommand, sources.chcp, sources.compareObject, sources.where]),
|
||
].join('\n'),
|
||
};
|
||
|
||
export const revisions = [practiceArticle, mechanismArticle, fieldArticle];
|
||
|
||
const isDirectExecution = Boolean(process.argv[1])
|
||
&& path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||
|
||
if (isDirectExecution) {
|
||
if (process.argv.includes('--print-revisions')) {
|
||
process.stdout.write(JSON.stringify(revisions, null, 2) + '\n');
|
||
} else {
|
||
process.stderr.write('Usage: node web/scripts/upgrade-2018-09.mjs --print-revisions\n');
|
||
}
|
||
}
|