PowerShell, NuGet & Visual Studio postbuild event (продолжение)

В предыдущей заметке я начал рассказ о том, как я переходил к использованию NuGet package manager и как использовал в связи с этим скрипт PowerShell, чтобы копировать сторонние библиотеки из одного места в другое.

Удаление недействительных ссылок

Проектов в моем решении очень много. Многие из них ссылаются на одну и ту же стороннюю библиотеку. Сторонние библиотеки у меня живут в папке $(SolutionDir)Lib. С переходом на NuGet значительная часть библиотек перебралась в папку $(SolutionDir)packages. При установке пакета в NuGet вы указываете проект, для которого вы его устанавливаете, в результате чего в проект добавляются ссылки на библиотеки, которые содержатся в пакете и еще в проекте появляется файл packages.config, который содержит информацию об установленном пакете (благодаря этому файлу можно установить все необходимые пакеты, если они не установлены, поэтому этот файл следует добавить в систему управления версиями). Ну и суть в том, что поскольку проектов в решении много, я не для всех проектов установил нужные им пакеты, в результате чего в некоторых проектах остались старые и теперь уже недействительные ссылки на сборки, которые раньше были в папке Lib (а теперь они в папке packages). И вот чтобы обнаружить такие проекты с недействительными ссылками, я решил написать скрипт для PowerShell.

Как выглядит ссылка на стороннюю сборку в проекте Visual Studio? Она существует в файле проекта *.csproj в виде xml-элемента Reference:

<Reference Include="SuperPuperLibrary, Version=1.2.3.4, Culture=neutral, PublicKeyToken=123456789abcdef, processorArchitecture=MSIL">
    <HintPath>..\..\..\..\Lib\SuperPuperLibrary.dll</HintPath>
</Reference>

Я себе поставил задачу просто удалить все недействительные ссылки. Это приведет к ошибкам при построении решения, и я увижу в каких проектах я забыл установить нужные пакеты. Итак, надо во всех проектах удалить ссылки, скажем, на библиотеку SuperPuperLibrary, такие у которых в HintPath есть слово «Lib». Надо найти все файлы *.csproj и сделать в них замену (find-replace). Заменять будем xml-элемент Reference на пустую строку. И для поиска нужного xml-элемента Reference можно использовать регулярное выражение. Нужный нам кусок текста начинается со слова <Reference Include="SuperPuperLibrary, далее он содержит какие-то неважно какие символы, затем слово Lib\, еще какие-то символы и наконец завершается словом Reference>. Регулярное выражение выглядит так:

<Reference\sInclude="SuperPuperLibrary.*?Lib\\.*?Reference>

Здесь \s — это пробельный символ, .*? — это произвольное количество (*) любых (.) символов (? означает ленивое поведение).

Ну а теперь сам скрипт PowerShell:

param(
    [Parameter(Mandatory=$true)]
    [String]$folder, # the folder containing *.csproj files
    [Parameter(Mandatory=$true)]
    [String]$library # the name of the library references to which are to be removed
)

$files = (Get-ChildItem "$folder" -Recurse -Filter *.csproj)
foreach ($file in $files) {
    $regex = '(?smi)(<Reference\sInclude="' + $library + '.*?Lib\\.*?Reference>)'
    $str = (Get-Content $file.FullName -Raw)
    $str = $str -replace $regex, ''
    $str | Out-File $file.FullName -Encoding utf8
}

Тут есть на что обратить внимание. Команда Get-ChildItem имеет ключ -Recurse — это чтобы поиск осуществлялся и по подпапкам тоже. Регулярное выражение начинается с (?smi) — это значит использовать многострочный режим. Команда Get-Content имеет ключ -Raw — считать весь файл в одну большую строку (а без этого ключа был бы массив строк). Команда Out-File имеет ключ -Encoding utf8 — это важно, так как по-умолчанию кодировка какая-то другая, а правильная кодировка — utf8.

Удаление лишних символов новой строки

Запуск показанного выше скрипта вызвал небольшой побочный эффект: вместо xml-элемента Reference осталась пустая строка плюс символы перевода строки и возврата каретки (\r\n). Да еще оказывается команда Out-File добавляет в конец файла те же перевод строки и возврат каретки. Поэтому после многократного запуска скрипта этих символов накопилось много, и я хотел их удалить. Метод тот же: find-replace с регулярным выражением:

param(
    [Parameter(Mandatory=$true)]
    [String]$folder # the folder containing *.csproj files
)

$files = (Get-ChildItem "$folder" -Recurse -Filter *.csproj)
foreach ($file in $files) {
    $str = (Get-Content $file.FullName -Raw)
    $str = [regex]::Replace($str, "(?smi)(\r\n)\s*?(\r\n)", "`r`n")
    $str = $str.TrimEnd("`r`n")
    $str | Out-File $file.FullName -Encoding utf8
}

На этот раз я нагуглил другой сопособ сделать find-replace, а именно вызов метода [regex]::Replace. Интересно еще, что в powershell символы перевода строки и возврата каретки обозначаются как `r`n.

Copy local = False

И последняя проблема. При установке пакета через NuGet всегда в проект добавляются ссылки на сборки из этого пакета, причем ссылки имеют свойство Copy local = True, т. е сборки, на которые ссылаются, при построении проекта автоматически копируются в его выходную папку. Для меня такое поведение было нежелательно, поскольку у меня все эти сборки не должны лежать в папках проектов, а должны все лежать в папке ThirdPartyNETAssemblies (см. предыдущую заметку). Чтобы изменить свойство Copy local, надо в файл проекта (*.csproj) в соответствующий xml-элемент Reference добавить элемент <Private>False</Private>, вот так:
— Было:

<Reference Include="SuperPuperLibrary, Version=1.2.3.4, Culture=neutral, PublicKeyToken=123456789abcdef, processorArchitecture=MSIL">
    <HintPath>..\..\..\..\packages\SuperPuperLibrary.1.2.3\lib\net40\SuperPuperLibrary.dll</HintPath>
</Reference>

— Стало:

<Reference Include="SuperPuperLibrary, Version=1.2.3.4, Culture=neutral, PublicKeyToken=123456789abcdef, processorArchitecture=MSIL">
    <HintPath>..\..\..\..\packages\SuperPuperLibrary.1.2.3\lib\net40\SuperPuperLibrary.dll</HintPath>
    <Private>False</Private>
</Reference>

Таким образом задача формулируется так: для всех строк вида <HintPath.*?packages\\.*?HintPath>\s*?</Reference> добавить после xml-элемента HintPath строку <Private>False</Private>. Я написал такой скрипт:

param(
    [Parameter(Mandatory=$true)]
    [String]$folder # the folder containing *.csproj files
)

$files = (Get-ChildItem "$folder" -Recurse -Filter *.csproj)

$regex = new-object System.Text.RegularExpressions.Regex ('(?<1><HintPath.*?packages\\SuperPuperLibrary.*?HintPath>)\s*?</Reference>',
     [System.Text.RegularExpressions.RegexOptions]::MultiLine)

foreach ($file in $files) {
    $str = (Get-Content $file.FullName -Raw)

    foreach ($match in $regex.Matches($str)) {
        $hintpath = $match.Groups[1].Value
        $str = $str.Replace($hintpath, $hintpath + "`r`n      <Private>False</Private>")
    }

    $str | Out-File $file.FullName -Encoding utf8
}

В этом скрипте в регулярном выражении используется группа(?<1>...), которой соответствует элемент HintPath, который мы и заменяем на него самого плюс строку <Private>False</Private>.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *