При разработке плагинов для САПР приложений (в моем случае это AutoCAD, Revit и Renga) со временем появляется одна проблема – выходят новые версии программ, меняется их API и нужно делать новые версии плагинов.
Когда у вас всего один плагин или Вы еще новичок-самоучка в этом деле, то можно просто сделать копию проекта, поменять в нем нужные места и собрать новую версию плагина. Соответственно, последующее внесение изменений в код повлечет за собой многократное увеличение трудозатрат.
По мере накопления опыта и знаний Вы найдете несколько способов автоматизации этого процесса. Я прошел этот путь и хочу рассказать Вам к чему я пришел в итоге и насколько это удобно.
Для начала рассмотрим способ, который является очевидным и которым я долгое время пользовался
Ссылки на файлы проекта
И чтобы все было просто, наглядно и понятно, я буду все описывать на абстрактном примере разработки плагина.
Откроем Visual Studio (у меня версия Community 2019. И да – на русском языке) и создадим новое решение. Назовем его MySuperPluginForRevit
Мы будем делать плагин под Revit для версий 2015-2020. Поэтому создам в решении новый проект (Библиотека классов Net Framework) и называем его MySuperPluginForRevit_2015
Нам нужно добавить ссылки на API Revit. Конечно, мы можем добавить ссылки на локальные файлы (нужно будет установить себе все нужные SDK или все версии Revit), но мы пойдем сразу по правильному пути и подключим NuGet-пакет. Вы можете найти не малое количество пакетов, но я буду использовать свои собственные.
После подключения пакета жмем правой кнопкой мышки на пункт «Ссылки» и выбираем в меню пункт «Перенести packages.config в PackageReference…»
Если вдруг на этом месте у вас начнется паника, так как в окне свойств пакета не будет важного пункта «Копировать локально», которое нам обязательно нужно установить в значение false, то не стоит паниковать – идем в папку с проектом, открываем файл с расширением .csproj в удобном вам редакторе (я использую Notepad++) и находим там запись о нашем пакете. Выглядит она сейчас так:
<PackageReference Include="ModPlus.Revit.API.2015">
<Version>1.0.0</Version>
</PackageReference>
Добавляем ему свойство <ExcludeAssets>runtime</ExcludeAssets>. Получится вот так:
<PackageReference Include="ModPlus.Revit.API.2015">
<Version>1.0.0</Version>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
Теперь при построении проекта файлы из пакета не будут копироваться в выходную папку.
Идем дальше – сразу представим, что наш плагин будет использовать что-то из Revit API, что менялось со временем выхода новых версий. Ну или просто нам нужно что-то свое менять в коде в зависимости от версии Revit, под которую мы делаем плагин. Для решения таких различий в коде мы будем использовать символы условной компиляции. Откроем свойства проекта, перейдем на вкладку «Сборка» и в поле «Обозначения условной компиляции» напишем R2015.
Обратите внимание, что символ надо добавить и для конфигурации Debug и для конфигурации Release.
Ну и пока мы находимся в окне свойств, то сразу переходим на вкладку «Приложение» и в поле «Пространство имен по умолчанию» удаляем суффикс _2015, чтобы у нас пространство имен было универсальным и независимым от имени сборки:
В моем случае в конечном продукте плагины всех версий складываются в одну папку, поэтому у меня имена сборок остаются с суффиксом вида _20хх. Но вы можете удалить суффикс и из имени сборки, если предполагается расположение файлов в разных папках.
Переходим к коду файла Class1.cs и имитируем там некий код с учетом разных версий Revit:
namespace MySuperPluginForRevit
{
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
[Regeneration(RegenerationOption.Manual)]
[Transaction(TransactionMode.Manual)]
public class Class1 : IExternalCommand
{
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
#if R2015
TaskDialog.Show("ModPlus", "Hello Revit 2015");
#elif R2016
TaskDialog.Show("ModPlus", "Hello Revit 2016");
#elif R2017
TaskDialog.Show("ModPlus", "Hello Revit 2017");
#elif R2018
TaskDialog.Show("ModPlus", "Hello Revit 2018");
#elif R2019
TaskDialog.Show("ModPlus", "Hello Revit 2019");
#elif R2020
TaskDialog.Show("ModPlus", "Hello Revit 2020");
#endif
return Result.Succeeded;
}
}
}
Я сразу учел все версии Revit выше 2015 версии (которые были на момент написания статьи) и сразу учел наличие символов условной компиляции, которые у меня создаются по одинаковому шаблону.
Переходим к главной изюминке. Создаем новый проект в нашем решении, только уже для версии плагина под Revit 2016. Повторяем все описанные выше действия, соответственно, заменяя число 2015 на число 2016. Но файл Class1.cs из нового проекта удаляем.
Файл с нужным кодом – Class1.cs – у нас уже имеется и нам нужно просто вставить на него ссылку в новом проекте. Есть два пути вставки ссылок:
- Долгий – жмем на проекте правой кнопкой мышки, выбираем пункт «Добавить» -> «Существующий элемент», в открывшемся окне находим нужный файл и вместо варианта «Добавить» выбираем вариант «Добавить как связь»
- Короткий – прям в обозревателе решений выбираем нужный файл (или даже файлы. А можно даже целые папки) и перетаскиваем в новый проект с зажатой клавишей Alt. При перетаскивании вы увидите, что при нажатии клавиши Alt курсор на мышке будет меняться с плюсика на стрелочку.
UPD: Я немного внес смуты в этом параграфе — чтобы переносить несколько файлов следует зажимать Shift+Alt!
После проведения процедуры у нас появится во втором проекте файл Class1.cs с соответствующей иконкой (синяя стрелочка):
При редактировании кода в окне редактора вы также можете выбирать в контексте какого проекта отображать код, что позволит вам видеть редактировать код при разных символах условной компиляции:
По этой схеме создаем все остальные проекты (2017-2020). Лайфхак – если перетаскивать файлы в обозревателе решений не из базового проекта, а из проекта, где они уже вставлены как связь, то можно не зажимать клавишу Alt!
Описанный вариант вполне хорош до момента добавления новой версии плагина или до момента добавления в проект новых файлов – все это становится очень муторным. А недавно я вдруг внезапно осознал как все это разрулить одним проектом и мы переходим ко второму способу
Магия конфигураций
Дочитав сюда, вы можете воскликнуть «А нафиг ты описывал первый способ, если статья сразу про второй?!». А описал я все, чтобы было яснее для чего нам нужны символы условной компиляции и в каких местах у нас отличаются проекты. И теперь нам становится яснее, какие именно различия проектов нам надо реализовать, оставив всего один проект.
И чтобы все было более очевидным, мы не будем создавать нового проекта, а внесем изменения в наш текущий проект, созданный первым способом.
Итак, в первую очередь удаляем из решения все проекты, кроме основного (содержащего непосредственно файлы). Т.е. проекты для версий 2016-2020. Открываем папку с решением и удаляем там папки этих проектов.
У нас в решении остался один проект — MySuperPluginForRevit_2015. Открываем его свойства и:
- На вкладке «Приложение» из имени сборки удаляем суффикс _2015 (далее станет ясно зачем)
- На вкладке «Сборка» удаляем символ условной компиляции R2015 из соответствующего поля
Примечание: в последней версии Visual Studio имеется глюк – символы условной компиляции не выводятся в окне свойств проекта, хотя они имеются. Если у вас этот глюк наблюдается, то вам нужно удалять их вручную из файла .csproj. Однако, нам все-равно работать в нем, так что читаем дальше.
Переименовываем проект в окне обозревателя решений, удалив суффикс _2015 и затем удаляем проект из решения. Это нужно для поддержания порядка и чувств перфекционистов! Открываем папку нашего решения, переименовываем там таким-же образом папку проекта и загружаем проект обратно в решение.
Открываем диспетчер конфигураций. Нам конфигурация Release в принципе не нужна будет, поэтому удаляем её. Создаем новые конфигурации с уже привычными нам именами R2015, R2016, …, R2020. Обратите внимание, что не нужно копировать параметры из других конфигураций и не нужно создавать конфигурации проекта:
Идем в папку с проектом и открываем файл с расширением .csproj в удобном вам редакторе. Кстати, его можно открыть и в Visual Studio – надо выгрузить проект и затем в контекстном меню будет нужный пункт:
Редактировать в Visual Studio даже предпочтительней, так как редактор и выравнивает и подсказывает.
В файле мы увидим элементы PropertyGroup – в самом верху идет общий, а следом идут с условиями. Эти элементы задают свойства проекта при его сборке. Первый элемент, который без условий, задает общие свойства, а элементы с условиями, соответственно, меняют некоторые свойства в зависимости от конфигураций.
Переходим в общий (первый) элемент PropertyGroup и смотрим свойство AssemblyName – это имя сборки и оно у нас должно быть без суффикса _2015. Если суффикс имеется, то удаляем его.
Находим элемент с условием
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
Он нам не нужен – удаляем его.
Элемент с условием
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
нужен будет для работы на этапе разработки и отладки кода. Вы можете менять его свойства под ваши нужды – задавать разные пути вывода, менять символы условной компиляции и т.п.
Теперь создаем новые элементы PropertyGroup для наших конфигураций. В этих элементам нам достаточно задать четыре свойства:
- OutputPath – выходная папка. Я задаю стандартное значение bin\R20xx
- DefineConstants – символы условной компиляции. Следует задавать значение TRACE;R20хх
- TargetFrameworkVersion – версия платформы. Для разных версий Revit API нужно задавать разные платформы.
- AssemblyName – имя сборки (т.е. имя файла). Вы можете писать прям нужное имя сборки, но для универсальности я советую писать значение $(AssemblyName)_20хх. Для этого мы ранее и удаляли суффикс из имени сборки
Самая главная фишка всех этих элементов – их можно будет банально копировать в другие проекты вообще не изменяя. Далее в статье я приложу все содержимое файла .csproj.
Хорошо, со свойствами проекта разобрались – это не сложно. Но что делать с подключаемыми библиотеками (NuGet-пакетами). Если посмотреть далее, мы увидим, что подключаемые библиотеки задаются элементах ItemGroup. Но вот незадача – этот элемент неверно обрабатывает условия, как элемент PropertyGroup. Возможно это даже глюк Visual Studio, но если задать несколько элементов ItemGroup с условиями конфигураций, а внутри вставить разные ссылки на NuGet-пакеты, то при смене конфигурации к проекту подключаются все указанные пакеты.
На помощь нам приходит элемент Choose, который работает по привычной нам логике if-then-else.
Используя элемент Choose, задаем разные NuGet-пакеты для разных конфигураций:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{5AD738D6-4122-4E76-B865-BE7CE0F6B3EB}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MySuperPluginForRevit</RootNamespace>
<AssemblyName>MySuperPluginForRevit</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;R2015</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2015|AnyCPU' ">
<OutputPath>bin\R2015\</OutputPath>
<DefineConstants>TRACE;R2015</DefineConstants>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<AssemblyName>$(AssemblyName)_2015</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2016|AnyCPU' ">
<OutputPath>bin\R2016\</OutputPath>
<DefineConstants>TRACE;R2016</DefineConstants>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<AssemblyName>$(AssemblyName)_2016</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2017|AnyCPU' ">
<OutputPath>bin\R2017\</OutputPath>
<DefineConstants>TRACE;R2017</DefineConstants>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<AssemblyName>$(AssemblyName)_2017</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2018|AnyCPU' ">
<OutputPath>bin\R2018\</OutputPath>
<DefineConstants>TRACE;R2018</DefineConstants>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<AssemblyName>$(AssemblyName)_2018</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2019|AnyCPU' ">
<OutputPath>bin\R2019\</OutputPath>
<DefineConstants>TRACE;R2019</DefineConstants>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
<AssemblyName>$(AssemblyName)_2019</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2020|AnyCPU' ">
<OutputPath>bin\R2020\</OutputPath>
<DefineConstants>TRACE;R2020</DefineConstants>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
<AssemblyName>$(AssemblyName)_2020</AssemblyName>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Class1.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Choose>
<When Condition=" '$(Configuration)'=='R2015' ">
<ItemGroup>
<PackageReference Include="ModPlus.Revit.API.2015">
<Version>1.0.0</Version>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
</When>
<When Condition=" '$(Configuration)'=='R2016' ">
<ItemGroup>
<PackageReference Include="ModPlus.Revit.API.2016">
<Version>1.0.0</Version>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
</When>
<When Condition=" '$(Configuration)'=='R2017' ">
<ItemGroup>
<PackageReference Include="ModPlus.Revit.API.2017">
<Version>1.0.0</Version>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
</When>
<When Condition=" '$(Configuration)'=='R2018' ">
<ItemGroup>
<PackageReference Include="ModPlus.Revit.API.2018">
<Version>1.0.0</Version>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
</When>
<When Condition=" '$(Configuration)'=='R2019' ">
<ItemGroup>
<PackageReference Include="ModPlus.Revit.API.2019">
<Version>1.0.0</Version>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
</When>
<When Condition=" '$(Configuration)'=='R2020' or '$(Configuration)'=='Debug'">
<ItemGroup>
<PackageReference Include="ModPlus.Revit.API.2020">
<Version>1.0.0</Version>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
</When>
</Choose>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
Обратите внимание, что в одном из условий я указал две конфигурации через ИЛИ (Or). Таким образом будет подключаться нужный пакет при конфигурации Debug.
И вот у нас почти все идеально. Загружаем обратно проект, включаем нужную нам конфигурацию, вызываем в контекстном меню решения (не проекта) пункт «Восстановить все пакеты NuGet» и видим как у нас меняются пакеты.
И вот на этом этапе я пришел в тупик – чтобы собрать сразу все конфигурации мы могли бы воспользоваться пакетной сборкой (меню «Сборка» -> «Пакетная сборка»), но при переключении конфигураций не происходит автоматического восстановления пакетов. И при сборке проекта тоже не происходит, хотя, по идее, должно. Решения этой проблемы стандартными средствами я так и не нашел. И скорее всего это тоже баг Visual Studio.
Поэтому для пакетной сборки решено было использовать специальную систему автоматизированной сборки Nuke. На самом деле я этого не хотел, так как считаю это излишним в рамках разработки плагинов, но на данный момент другого решения я не вижу. А на вопрос «Почему именно Nuke?» ответ прост – используем на работе.
Итак, переходим в папку нашего решения (не проекта), зажимаем клавишу Shift и кликаем правой кнопкой мышки по пустому месту в папке – в контекстном меню выбираем пункт «Открыть окно PowerShell здесь».
Если у вас не установлен nuke, то сначала пишите команду
dotnet tool install Nuke.GlobalTool –global
Теперь пишите команду nuke и вам будет предложено настроить nuke для текущего проекта. Не знаю как это правильнее написать на русском языке – на английском будет написано Could not find .nuke file. Do you want to setup a build? [y/n]
Нажимаем клавишу Y и дальше будут непосредственные пункты настройки. Нам нужен самый простой вариант с использованием MSBuild, поэтому отвечаем как на скриншоте:
Перейдем в Visual Studio, которая нам предложит перезагрузить решение, так как в него добавлен новый проект. Перезагружаем решение и видим, что у нас появился проект build в котором нас интересует только один файл – Build.cs
Открываем этот файл и пишем скрипт по сборке проекта под все конфигурации. Ну или используем мой скрипт, который вы можете отредактировать под себя:
using System.IO;
using Nuke.Common;
using Nuke.Common.Execution;
using Nuke.Common.ProjectModel;
using Nuke.Common.Tools.MSBuild;
using static Nuke.Common.Tools.MSBuild.MSBuildTasks;
[CheckBuildProjectConfigurations]
[UnsetVisualStudioEnvironmentVariables]
class Build : NukeBuild
{
public static int Main () => Execute<Build>(x => x.Compile);
[Solution] readonly Solution Solution;
// If the solution name and the project (plugin) name are different, then indicate the project (plugin) name here
string PluginName => Solution.Name;
Target Compile => _ => _
.Executes(() =>
{
var project = Solution.GetProject(PluginName);
if (project == null)
throw new FileNotFoundException("Not found!");
var build = new List<string>();
foreach (var (_, c) in project.Configurations)
{
var configuration = c.Split("|")[0];
if (configuration == "Debug" || build.Contains(configuration))
continue;
Logger.Normal($"Configuration: {configuration}");
build.Add(configuration);
MSBuild(_ => _
.SetProjectFile(project.Path)
.SetConfiguration(configuration)
.SetTargets("Restore"));
MSBuild(_ => _
.SetProjectFile(project.Path)
.SetConfiguration(configuration)
.SetTargets("Rebuild"));
}
});
}
Возвращаемся в окно PowerShell и снова пишем команду nuke (можно писать команду nuke с указанием нужного Target. Но у нас один Target, который запускается по умолчанию). После нажатия клавиши Enter мы почувствуем себя настоящими хакерами, ибо как в кино будет происходить автоматическая сборка нашего проекта под разные конфигурации.
Кстати, можно использовать PowerShell прям из Visual Studio (меню «Вид» -> «Другие окна» -> «Консоль диспетчера пакетов»), но там все будет черно-белым, что не очень удобно.
На этом моя статья закончена. Уверен, что с вариантом для AutoCAD вы сможете разобраться сами. Надеюсь, что изложенный тут материал найдет своих «клиентов».
Спасибо за внимание!
BoxaShu
Спасибо за развернутое описание.
Что то подобное у Андрея Бушмана было сделано, но без разбора и описания.