В предыдущей заметке я начал рассказ о том, как я переходил к использованию NuGet package manager и как использовал в связи с этим скрипт PowerShell, чтобы копировать сторонние библиотеки из одного места в другое.
Удаление недействительных ссылок
Проектов в моем решении очень много. Многие из них ссылаются на одну и ту же стороннюю библиотеку. Сторонние библиотеки у меня живут в папке $(SolutionDir)Lib. С переходом на NuGet значительная часть библиотек перебралась в папку $(SolutionDir)packages. При установке пакета в NuGet вы указываете проект, для которого вы его устанавливаете, в результате чего в проект добавляются ссылки на библиотеки, которые содержатся в пакете и еще в проекте появляется файл packages.config, который содержит информацию об установленном пакете (благодаря этому файлу можно установить все необходимые пакеты, если они не установлены, поэтому этот файл следует добавить в систему управления версиями). Ну и суть в том, что поскольку проектов в решении много, я не для всех проектов установил нужные им пакеты, в результате чего в некоторых проектах остались старые и теперь уже недействительные ссылки на сборки, которые раньше были в папке Lib (а теперь они в папке packages). И вот чтобы обнаружить такие проекты с недействительными ссылками, я решил написать скрипт для PowerShell.
Как выглядит ссылка на стороннюю сборку в проекте Visual Studio? Она существует в файле проекта *.csproj в виде xml-элемента Reference:
<HintPath>..\..\..\..\Lib\SuperPuperLibrary.dll</HintPath>
</Reference>
Я себе поставил задачу просто удалить все недействительные ссылки. Это приведет к ошибкам при построении решения, и я увижу в каких проектах я забыл установить нужные пакеты. Итак, надо во всех проектах удалить ссылки, скажем, на библиотеку SuperPuperLibrary, такие у которых в HintPath есть слово «Lib». Надо найти все файлы *.csproj и сделать в них замену (find-replace). Заменять будем xml-элемент Reference на пустую строку. И для поиска нужного xml-элемента Reference можно использовать регулярное выражение. Нужный нам кусок текста начинается со слова <Reference Include="SuperPuperLibrary
, далее он содержит какие-то неважно какие символы, затем слово Lib\
, еще какие-то символы и наконец завершается словом Reference>
. Регулярное выражение выглядит так:
Здесь \s — это пробельный символ, .*? — это произвольное количество (*) любых (.) символов (? означает ленивое поведение).
Ну а теперь сам скрипт PowerShell:
[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 с регулярным выражением:
[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>
, вот так:
— Было:
<HintPath>..\..\..\..\packages\SuperPuperLibrary.1.2.3\lib\net40\SuperPuperLibrary.dll</HintPath>
</Reference>
— Стало:
<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>
. Я написал такой скрипт:
[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>
.