Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

При розробці плагінів для САПР додатків (в моєму випадку це AutoCAD, Revit і Renga) з часом з'являється одна проблема - виходять нові версії програм, змінюється їх API і потрібно робити нові версії плагінів.

Коли у вас всього один плагін або Ви ще новачок-самоучка у цій справі, то можна просто зробити копію проекту, поміняти в ньому потрібні місця та зібрати нову версію плагіна. Відповідно, подальше внесення змін до коду спричинить багаторазове збільшення трудовитрат.

У міру накопичення досвіду та знань Ви знайдете кілька способів автоматизації цього процесу. Я пройшов цей шлях і хочу розповісти Вам до чого я прийшов у підсумку та наскільки це зручно.

Спочатку розглянемо спосіб, який є очевидним і яким я довгий час користувався

Посилання на файли проекту

І щоб усе було просто, наочно і зрозуміло, я все описуватиму на абстрактному прикладі розробки плагіна.

Відкриємо Visual Studio (у мене версія Community 2019. І так – російською мовою) та створимо нове рішення. Назвемо його MySuperPluginForRevit

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

Ми будемо робити плагін під Revit для версій 2015-2020. Тому створимо у рішенні новий проект (Бібліотека класів Net Framework) і називаємо його MySuperPluginForRevit_2015

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

Нам потрібно додати посилання на API Revit. Звичайно, ми можемо додати посилання на локальні файли (потрібно буде встановити собі всі потрібні SDK або всі версії Revit), але ми підемо відразу правильним шляхом і підключимо NuGet-пакет. Ви можете знайти багато пакетів, але я буду використовувати свої власні.

Після підключення пакета тиснемо правою кнопкою мишки на пункт «Посилання» та вибираємо в меню пункт «Перенести packages.config у PackageReference…»

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

Якщо раптом на цьому місці у вас почнеться паніка, тому що у вікні властивостей пакета не буде важливого пункту.Копіювати локально», яке нам обов'язково потрібно встановити на значення false, то не варто панікувати - йдемо в папку з проектом, відкриваємо файл з розширенням .csproj в зручному редакторі (я використовую Notepad++) і знаходимо там запис про наш пакет. Виглядає вона зараз так:

<PackageReference Include="ModPlus.Revit.API.2015">
  <Version>1.0.0</Version>
</PackageReference>

Додаємо йому властивість runtime. Вийде ось так:

<PackageReference Include="ModPlus.Revit.API.2015">
  <Version>1.0.0</Version>
  <ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>

Тепер при побудові проекту файли з пакета не копіюватимуться у вихідну папку.
Йдемо далі - відразу уявимо, що наш плагін буде використовувати щось із Revit API, що змінювалося з часом виходу нових версій. Ну чи просто нам потрібно щось своє змінювати в коді залежно від версії Revit, під яку ми робимо плагін. Для вирішення таких відмінностей у коді ми використовуватимемо символи умовної компіляції. Відкриємо властивості проекту, перейдемо на вкладку «збірка» і в полі «Позначення умовної компіляції» напишемо R2015.

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

Зверніть увагу, що символ потрібно додати і конфігурації Debug і конфігурації Release.

Ну і поки ми знаходимося у вікні властивостей, то відразу переходимо на вкладкудодаток» і в полі «Простір імен за замовчуванням» видаляємо суфікс _2015щоб у нас простір імен був універсальним і незалежним від імені збірки:

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

У моєму випадку в кінцевому продукті плагіни всіх версій складаються в одну папку, тому у мене імена збірок залишаються суфіксом виду _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 із нового проекту видаляємо.

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

Файл із потрібним кодом – Class1.cs – ми вже маємо і нам потрібно просто вставити на нього посилання в новому проекті. Є два шляхи вставки посилань:

  1. Довгий - Тиснемо на проекті правою кнопкою мишки, вибираємо пункт «Добавить»->«Існуючий елемент», у вікні знаходимо потрібний файл і замість варіанта «Добавить» вибираємо варіант «Додати як зв'язок»

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

  1. короткий - Прям в браузері рішень вибираємо потрібний файл (або навіть файли. А можна навіть цілі папки) і перетягуємо в новий проект із затиснутою клавішею Alt. При перетягуванні ви побачите, що при натисканні клавіші Alt курсор на мишці змінюватиметься з плюсика на стрілочку.
    UPD: Я трохи вніс смути в цьому параграфі — щоб переносити кілька файлів, слід затискати Shift + Alt!

Після проведення процедури у нас з'явиться у другому проекті файл Class1.cs з відповідною іконкою (синя стрілочка):

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

При редагуванні коду у вікні редактора ви також можете вибирати в контексті якого проекту відображати код, що дозволить вам бачити редагувати код за різних символів умовної компіляції:

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

За цією схемою створюємо решту проектів (2017-2020). Лайфхак – якщо перетягувати файли в браузері рішень не з базового проекту, а з проекту, де вони вже вставлені як зв'язок, то можна не затискати клавішу Alt!

Описаний варіант цілком добрий до моменту додавання нової версії плагіна або до моменту додавання в проект нових файлів - все це стає дуже моторошним. А нещодавно раптом раптом усвідомив як усе це розрулити одним проектом і ми переходимо до другого способу.

Магія конфігурацій

Дочитавши сюди, ви можете вигукнути: «А нафіг ти описував перший спосіб, якщо стаття одразу про другий?!». А описав я все, щоб було ясніше, для чого нам потрібні символи умовної компіляції і в яких місцях у нас відрізняються проекти. І тепер нам стає ясніше, які саме відмінності проектів нам треба реалізувати, залишивши лише один проект.

І щоб усе було очевиднішим, ми не створюватимемо нового проекту, а внесемо зміни до нашого поточного проекту, створеного першим способом.

Отже, в першу чергу видаляємо з рішення всі проекти, крім основного, що містить безпосередньо файли. Тобто. проекти для версій 2016–2020. Відкриваємо папку з рішенням та видаляємо там папки цих проектів.

У нас у рішенні залишився один проект. MySuperPluginForRevit_2015. Відкриваємо його властивості та:

  1. На вкладці «додаток» з імені складання видаляємо суфікс _2015 (Далі стане ясно навіщо)
  2. На вкладці «збірка» видаляємо символ умовної компіляції R2015 з відповідного поля

Примітка: в останній версії Visual Studio є глюк – символи умовної компіляції не виводяться у вікні властивостей проекту, хоча вони є. Якщо у вас цей глюк спостерігається, вам потрібно видаляти їх вручну з файлу .csproj. Однак, нам все одно працювати в ньому, так що читаємо далі.

Перейменовуємо проект у вікні браузера рішень, видаливши суфікс _2015 і потім видаляємо проект із рішення. Це потрібно для підтримання порядку та почуттів перфекціоністів! Відкриваємо папку нашого рішення, перейменовуємо там так само папку проекту і завантажуємо проект назад у рішення.

Відкриваємо диспетчер конфігурацій. Нам конфігурація Відпустіть в принципі не потрібно буде, тому видаляємо її. Створюємо нові зміни з звичними нам іменами R2015, R2016, ..., R2020. Зверніть увагу, що не потрібно копіювати параметри з інших конфігурацій та не потрібно створювати конфігурації проекту:

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

Йдемо в папку з проектом і відкриваємо файл з розширенням .csproj у зручному для вас редакторі. До речі, його можна відкрити і в Visual Studio – треба вивантажити проект, а потім у контекстному меню буде потрібний пункт:

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

Редагувати в Visual Studio навіть краще, оскільки редактор і вирівнює і підказує.

У файлі ми побачимо елементи PropertyGroup – у самому верху йде загальний, а потім йдуть з умовами. Ці елементи задають властивості проекту під час його складання. Перший елемент, який без умов, визначає загальні властивості, а елементи з умовами, відповідно, змінюють деякі властивості в залежності від конфігурацій.

Переходимо у загальний (перший) елемент PropertyGroup і дивимося властивість AssemblyName – це ім'я збірки і воно має бути без суфікса _2015. Якщо суфікс є, видаляємо його.

Знаходимо елемент із умовою

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">

Він нам не потрібний – видаляємо його.

Елемент із умовою

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

потрібен буде для роботи на етапі розробки та налагодження коду. Ви можете змінювати його властивості під ваші потреби – ставити різні шляхи виведення, змінювати символи умовної компіляції тощо.

Тепер створюємо нові елементи PropertyGroup для конфігурацій. У цих елементах нам достатньо задати чотири властивості:

  • OutputPath - Вихідна папка. Я ставлю стандартне значення binR20xx
  • DefineConstants - Символи умовної компіляції. Слід задавати значення TRACE; R20хх
  • TargetFrameworkVersion - Версія платформи. Для різних версій Revit API необхідно задавати різні платформи.
  • AssemblyName – ім'я збирання (тобто ім'я файлу). Ви можете писати прямо потрібне ім'я складання, але для універсальності я раджу писати значення $(AssemblyName)_20хх. Для цього ми раніше видаляли суфікс з імені збірки

Найголовніша фішка всіх цих елементів їх можна буде банально копіювати в інші проекти взагалі не змінюючи. Далі у статті я додаю весь вміст файлу .csproj.

Добре, що з властивостями проекту розібралися – це не складно. Але що робити з бібліотеками, що підключаються (NuGet-пакетами). Якщо подивитися далі, ми побачимо, що бібліотеки, що підключаються, задаються елементах ItemGroup. Але незадача - цей елемент неправильно обробляє умови, як елемент PropertyGroup. Можливо, це навіть глюк Visual Studio, але якщо задати кілька елементів ItemGroup з умовами конфігурацій, а всередині вставити різні посилання на NuGet-пакети, при зміні конфігурації до проекту підключаються всі зазначені пакети.

На допомогу нам приходить елемент Вибирати, який працює за звичною нам логікою якщо-то-інше.

Використовуючи елемент Вибирати, задаємо різні NuGet-пакети для різних конфігурацій:

Весь вміст csproj

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0"  ">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>binDebug</OutputPath>
    <DefineConstants>DEBUG;R2015</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2015|AnyCPU' ">
    <OutputPath>binR2015</OutputPath>
    <DefineConstants>TRACE;R2015</DefineConstants>
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2015</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2016|AnyCPU' ">
    <OutputPath>binR2016</OutputPath>
    <DefineConstants>TRACE;R2016</DefineConstants>
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2016</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2017|AnyCPU' ">
    <OutputPath>binR2017</OutputPath>
    <DefineConstants>TRACE;R2017</DefineConstants>
    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2017</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2018|AnyCPU' ">
    <OutputPath>binR2018</OutputPath>
    <DefineConstants>TRACE;R2018</DefineConstants>
    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2018</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2019|AnyCPU' ">
    <OutputPath>binR2019</OutputPath>
    <DefineConstants>TRACE;R2019</DefineConstants>
    <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2019</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2020|AnyCPU' ">
    <OutputPath>binR2020</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="PropertiesAssemblyInfo.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і бачимо як у нас змінюються пакети.

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

І ось на цьому етапі я прийшов у глухий кут – щоб зібрати відразу всі конфігурації ми могли б скористатися пакетною збіркою (меню «збірка»->«Пакетне складання»), але при перемиканні конфігурацій не відбувається автоматичного відновлення пакетів. І при складанні проекту теж не відбувається, хоча, за ідеєю, має бути. Вирішення цієї проблеми стандартними засобами я так і не знайшов. І найімовірніше це теж баг Visual Studio.

Тому для пакетного складання вирішено було використовувати спеціальну систему автоматизованого складання Nuke. Насправді я цього не хотів, тому що вважаю це зайвим у рамках розробки плагінів, але на даний момент іншого рішення я не бачу. А на запитання «Чому саме Nuke?» відповідь проста – використовуємо на роботі.

Отже, переходимо до папки нашого рішення (не проекту), затискаємо клавішу Shift і клацаємо правою кнопкою мишки по порожньому місцю в папці - в контекстному меню вибираємо пункт «Відкрити вікно PowerShell тут».

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

Якщо у вас не встановлено 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тому відповідаємо як на скріншоті:

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

Перейдемо до Visual Studio, яка запропонує нам перезавантажити рішення, оскільки до нього доданий новий проект. Перезавантажуємо рішення та бачимо, що у нас з'явився проект будувати в якому нас цікавить лише один файл – Build.cs

Робимо один проект плагіна з компіляцією під різні версії Revit/AutoCAD

Відкриваємо цей файл і пишемо скрипт зі збирання проекту під усі конфігурації. Ну або використовуємо мій скрипт, який ви можете відредагувати під себе:

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 із зазначенням потрібного Мета. Але в нас один Мета, який запускається за замовчуванням). Після натискання клавіші Enter ми відчуємо себе справжніми хакерами, бо як у кіно відбуватиметься автоматичне складання нашого проекту під різні конфігурації.

До речі, можна використовувати PowerShell прямий з Visual Studio (меню «Вид»->«Інші вікна»->«Консоль диспетчера пакетів»), але там все буде чорно-білим, що не дуже зручно.

На цьому моя стаття закінчена. Впевнений, що з варіантом AutoCAD ви зможете розібратися самі. Сподіваюся, що викладений тут матеріал знайде своїх клієнтів.

Дякуємо за увагу!

Джерело: habr.com

Додати коментар або відгук