Pull to refresh

Событийная модель построения проектов и решений Visual Studio для разработчиков

Reading time 17 min
Views 15K
Эта небольшая статья поможет:

  • Ознакомиться с событийной моделью построения проектов и решений MS Visual Studio;
  • Понять, как получить поддержку Command-Line режима devenv.exe для VSPackage (где он изначально не предусмотрен);
  • Понять, как эмулировать схожую модель событий от MSBuild Tools и транслировать на главный плагин;
  • Узнать, как работать по приоритетной подписке;
  • Узнать варианты получения контекста построения при обработке событий Visual Studio / MSBuild Tools;
  • Узнать об оценке MSBuild Property & MSBuild Property Functions;
  • Получить общие сведения межмодульного взаимодействия на слое абстракции для разнородных компонентов системы.

Синопсис


Мне довольно часто приходиться заниматься автоматизацией тех или иных процессов, поэтому не мудрено, что часть решений рано или поздно коснулись и Visual Studio.

На самом деле, эта статья, или даже заметка — результат рабочего и уже давно написанного плагина, который еще года 2 назад являлся лишь побочным продуктом при работе над одним проектом на C++. Однако мой дебют на Хабрахабре будет, пожалуй, с этого.

Недавно ко мне обратился со схожими потребностями (те, которые изначально был призван решать плагин) парень из DevDiv. В попытках решить его проблему весьма кастомной автоматизации стало понятным, что все-таки крайне необходимо осветить некоторые аспекты решения и взаимодействия между VS & MSBuild. Ведь этого материала изначально не было и вроде бы до сих пор нет в публичном доступе.

Предупреждение


Перед тем как начать, пожалуйста, обратите внимание, что материал не для пользователей Visual Studio, а для разработчиков (предполагающее то или иное расширение самой среды Visual Studio). И призван в первую очередь — осветить некоторые аспекты решений и взаимодействия для указанных компонентов, если кто-то столкнулся со схожими задачами для собственных разработок. Это не пошаговая инструкция, однако призвана помочь понять минимальные принципы и схемы работы. К примеру, взаимодействие с devenv.exe, msbuild.exe, работа с приоритетной подпиской, и все что указано в списке выше.

О проблемах


Изначальная потребность, для нас обоих, была и есть работа с Solution-Context в рамках событийной модели студии.
Те, кто работал с Visual Studio, конечно же, должны знать о Pre/Post-build событиях уровня проектов. Однако, они не в полной мере удовлетворяют потребности управления проектами для всего решения, в особенности, когда этих проектов несколько десятков и т.п., что типично прежде всего для C++ проектов.

Есть несколько способов как решить подобную задачу. К примеру, выше упомянутый, сейчас осуществляет переход от решения, когда в Solution есть проект, от которого зависят все остальные.

Так почему MS до сих пор не выделит подобный контекст для своей IDE?

На самом деле, все тоже самое, что и с форматом .sln, который до сих пор представляет собой записки охотника, нежели валидный xml документ с файлами проектов, или что-то более гибкое и изящное.
Совместимость? Вероятно, однако ломать это надо было еще с приходом мажорных изменений в VS2010.

Так и с solution-context. Просто так поднять события на верхний уровень не выйдет, т.к. понадобится решить задачу по управлению msbuild данными и многое другое для одного единого контекста, а дописывать это в свой загруженный таймлайн особо никто и не спешит.

Обработка событий проектов и решений MS Visual Studio (VS2010, VS2012, VS2013, VS2015)


Для начала давайте рассмотрим вариант с EnvDTE. Для нашей задачи взор прежде всего устремляется на BuildEvents, который предоставляет доступную подписку на базовые события, такие как OnBuildBegin, OnBuildProjConfigBegin и тому подобное. То есть они и реализованы в виде публичных events.

Однако нет, в нашем случае они сработают слишком поздно, когда пойдет оповещение всех своих слушателей DTE. Нам же необходимо получить управление над ситуацией так скоро, насколько это возможно в нашем пакете.

К счастью, в VS выделяют приоритетной слой на обработку подобных вещей, они то и будут выполняются в первую очередь со всеми подобными вещами.

В общем виде — это такая же классика observer'а, реализованная посредством Advise методов для конкретных сервисов. Но обо всем по порядку.

Прежде всего нам необходимо обратить свое внимание на Microsoft.VisualStudio.Shell.Interop. Именно он поможет работать с базовыми «build-событиями» VS и прочими уровня solution, а именно — IVsUpdateSolutionEvents:

Для работы с вышеупомянутыми потребностями достаточно рассмотреть только базовый IVsUpdateSolutionEvents2, который доступен вплоть до VS2010. На самом деле желателен IVsUpdateSolutionEvents4 для работы с контекстом построения, однако он доступен только для VS2012 и старше.

Примечание: Для полноценной работы также скорее всего потребуется IVsSolutionEvents, но этот момент не рассматривается этой статьей. Если все-таки потребуется кому-либо, могу показать базовые приемы для обслуживания этого слоя.

Итак, нам необходимо имплементировать следующие базовые вещи интерфейса IVsUpdateSolutionEvents2:

// должны получить управление до того как начнется любой build-action
// и этой последний шанс отменить построение
int UpdateSolution_Begin(ref int pfCancelUpdate);

// вызывается для любого отмененного построения т.е. Cancel / Abort - пользователем или самой студией
int UpdateSolution_Cancel();

// вызывается при любой окончательной остановке построения.
// Обратите внимание, что вызов будет как завершение для нормального построения, так и после Cancel/Abort, т.е.:
//  * Begin -> Done
//  * Begin -> Cancel -> Done
int UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand);

Все эти методы прежде всего и подходят для реализации уровня solution-context.
По проектам у нас предусмотрено:

int UpdateProjectCfg_Begin(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, ref int pfCancel);
int UpdateProjectCfg_Done(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, int fSuccess, int fCancel);

Которые будут вызываться для каждого, конфигурация которого должна пойти на построение. Обратите внимание на pHierProj аргумент, это гибкий способ ссылаться в любое место откуда пришло управление.

Базовая имплементация вашим пакетом не означает, что VS будет готова работать с IVsUpdateSolutionEvents. Как и было сказано выше, нам необходимо зарегистрировать нашего слушателя (кто имплементирует этот интерфейс):

public sealed class vsSolutionBuildEventPackage: Package, IVsSolutionEvents, IVsUpdateSolutionEvents2
{
...
    public int UpdateSolution_Begin(ref int pfCancelUpdate)
    {
        return VSConstants.S_OK;
    }
...
}

Сделать это необходимо с Advise методами, так для IVsUpdateSolutionEvents2 предусмотрен — AdviseUpdateSolutionEvents.

Пример регистрации:

/// http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.shell.interop.ivssolutionbuildmanager2.aspx
private IVsSolutionBuildManager2 sbm;

/// IVsSolutionBuildManager2 / IVsSolutionBuildManager
/// http://msdn.microsoft.com/en-us/library/bb141335.aspx
private uint _sbmCookie;
...
sbm = (IVsSolutionBuildManager2)ServiceProvider.GlobalProvider.GetService(typeof(SVsSolutionBuildManager));
sbm.AdviseUpdateSolutionEvents(this, out _sbmCookie);

Следует заметить, что получить доступ к сервису SVsSolutionBuildManager можно и другими путями. Используйте то, что удобно.

sbm, в примере выше, должен быть частью класса для протекции от GC.

Вот теперь мы готовы слушать в первых рядах. Важно также понимать, что мы не самые первые, но нам это и не нужно.

Поддержка Command-Line режима


Немудрено, что у кого-то возникла потребность работать с devenv.exe (точнее devenv.com, т.к. именно он занимается обработкой в консольном режиме) с нашим плагином. Однако VSPackages не имеет возможностей работать хоть как-то в таком режиме! Соответственно плагин просто останется неактивным, если мы попробуем построить решение в консоли как
devenv "D:\App1\App1.sln" /Rebuild Debug

Мне этот вопрос в первые был задан в Q/A галерее и, честно говоря, у меня не возникало ранее таких потребностей или даже желаний (т.к. можно работать с msbuild.exe и, кроме того, сервера автоматизации все равно будут предоставлять у себя ограниченную среду без всего такого).

Однако, как мы видим, потребности у каждого из нас разные, тем более если это часть VS, то непорядок, надо поддержать.

Позднее в рамках поддержки CI серверов я занялся подобным вопросом с успешным его решением. То есть всю событийную модель проектов и решения мы все-таки можем обрабатывать в VSPackage! Кто бы что не утверждал:


"C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\devenv" "D:\App1.sln" /Rebuild Debug
"C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\devenv" "D:\App1.sln" verbosity:diagnostic /Build Release
[?]
Однако…
  • Решение предполагает использование Add-Ins обертки для трансляции событий;
  • Именно Add-Ins предоставляют command-line режим, точнее VS готова работать с ним в этом режиме;
  • Они же имеют точно такую же модель, поэтому нам нет ничего проще просто имплементировать старый добрый IVsUpdateSolutionEvents2 и произвести регистрацию как и для VSPackage в точке:

void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom);

И произвести перенаправление на главную библиотеку, т.е., например:

...
public int UpdateSolution_Begin(ref int pfCancelUpdate)
{
    return library.Event.onPre(ref pfCancelUpdate);
}

public int UpdateProjectCfg_Begin(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, ref int pfCancel)
{
    return library.Event.onProjectPre(pHierProj, pCfgProj, pCfgSln, dwAction, ref pfCancel);
}        
...

Но, как всем давно известно — the Add-ins are deprecated in Visual Studio 2013. То есть такой трюк будет работать для VS2010, VS2012 и VS2013. Для планируемой VS2015 такие игры не пройдут.

Об этом я уже писал на MS Connect Issue #1075033, однако можете попрощаться для тех, кому это было важно. VS2015 уже в RC, а задачка simply closed.

Идем дальше.

Эмуляция схожей модели событий от MSBuild tools


Начнем с того, что MSBuild tools — безусловно более мощный инструмент и выше указанные проблемы никто и знать не должен. Мы можем спокойно обращаться с $(Configuration) & $(Platform) уровня solution, можем работать с targets и т.п Однако решение должно быть единым и мы не должны замечать разницу работы между VS IDE и построениями на CI/Special Build Servers. Поэтому рассмотрим возможность работать с вышеуказанными событиями в рамках msbuild tools.

Нам недоступен ни DTE2-context, ни сервисы для регистрации событий, ничего такого, чтобы можно было общаться с VS, ну а чего вы еще ждали. Да, конечно, мы можем получить, например, тот же DTE2-context c GetActiveObject, который в свою очередь использует:

HRESULT GetActiveObject(
  _In_       REFCLSID rclsid,
  _Reserved_ void     *pvReserved,
  _Out_      IUnknown **ppunk
);

[?]
Т.е. например так:

(EnvDTE80.DTE2)System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.10.0");

Но это все будет работать только при наличии запущенного экземпляра IDE студии, что не представляет возможным для ограниченных сред, например, для CI.

Поэтому предлагаю получить управление msbuild.exe посредством регистрации разрабатываемого транслятора как логгера. Для этого нам необходимо работать с Microsoft.Build.Framework.

Действительно, базовый IEventSource способен обеспечить все наши основные потребности:

public class EventManager: Logger
{
    ...
    public override void Initialize(IEventSource evt)
    {
        ...
    }
}

Однако и тут есть ряд особенностей, которые необходимо знать.
  • IEventSource.BuildStarted не подойдет (см. примечание ниже), т.к. он срабатывает слишком рано, точнее, для того чтобы получить данные проекта, который должен будет обрабатываться, нам придется подождать. В VS — контекст входа обрабатывается с IVsSolutionEvents;
  • Поэтому для PRE событий представляется возможным использовать IEventSource.ProjectStarted, а он же в свою очередь должен использоваться для событий уровня проектов.

Дело в том, что обработчик — ProjectStartedEventHandler рассылает также ProjectStartedEventArgs в качестве аргумента и .sln файл для работы с ним мы можем отследить, например, так:

evt.ProjectStarted  += (object sender, ProjectStartedEventArgs e) {
    e.ProjectFile; // should be .sln ! 
    ...
};

Вариант может быть с ожиданием пока не получим .sln на обработку, а он в свою очередь должен быть до прохождения файлов проектов.

Мучения на этом только начинаются, т.к. для полноценной работы вам, скорее всего, понадобятся данные .sln, включая загрузку проектов для получения доступа к msbuild движку где-либо еще, например evaluation новых св-в msbuild, прерывание сборки и т.п.

Для работы с .sln файлами, увы, ничего нормального вы не увидите, точнее выбирайте на свой вкус:
  • Либо использовать устаревший т.е. он помечен как obsolete Microsoft.Build.BuildEngine.Project. При этому учитываете, что если вы не используете Microsoft.Build.BuildEngine, то это доп. ненужный reference для вашего проекта.
  • Либо использовать рефлексию на internal методы — Microsoft.Build.BuildEngine.Shared, например:
    -> void ParseProject(string firstLine)
    -> void ParseFirstProjectLine(string firstLine, ProjectInSolution proj)
    -> crackProjectLine -> PROJECTNAME + RELATIVEPATH
  • Либо написать собственный небольшой парсер по примеру того же ParseProject.

Выбор небогатый, однако .sln в непонятном виде уже достаточно давно…

Примечание:

Фраза выше «IEventSource.BuildStarted не подойдет», не совсем правда… Он действительно срабатывает слишком рано, чтобы получить необходимые данные, но это палка о двух концах. Здесь, я описал проблему, с которой можно столкнуться при подобной реализации. Так для мультипоточной сборки /m:2+ (2 or more concurrent processes to use) это может вызвать довольно-таки неприятные последствия, и частный случай CSC error (CS2001), когда все цели определились сборщиком msbuild до момента обработки наших первых событий, а соответствующие модификации at runtime уже никак не могли повлиять на новые. Хотя это покрытие только части задач вмешивающихся в процесс строительства и т.п.

Поэтому рекомендую наиболее гибкий способ, использовать IEventSource.BuildStarted с предварительной подготовкой всех необходимых данных. Как это сделать, я описал в этом материале, там же можно найти простой sln парсер под MIT либо см. разборщик в составе основной библиотеки.

Идем дальше.
В проекте может понадобиться оценка свойств и функций msbuild (MSBuild Property Functions) и т.п., ну а как же без них...

Если вам потребуется подобное, то для того, чтобы с этим работать, необходимо подготовить msbuild движок, а его необходимо сначала инициировать. Так как мы работаем с изолированной средой, необходимо также от msbuild.exe передать свойства, с которым он был иницирован.

Если вы используете .NET 4.0, вам придется это сделать вручную, т.е. вам необходимо определить Configuration, Platform, SolutionDir и т.п. для обработчика. А вот для платформы .NET 4.5 доступен ProjectStartedEventArgs.GlobalProperties.

После того, как изолированная среда полностью проиниализирована, мы, наконец, можем обрабатывать события проектов. Однако мы работаем с msbuild и мы работаем с targets. Поэтому вам необходимо подписаться на TargetStarted.

Для трансляции нам устроят PreBuildEvent и PostBuildEvent от полученного TargetName, например:

protected void onTargetStarted(object sender, TargetStartedEventArgs e)
{
    switch(e.TargetName) { 
        case "PreBuildEvent": {
            ...
            return;
        }
    }
}

Но и тут не все гладко. Дело в том, что мы не можем сослаться на проект, который сейчас обрабатывается с TargetStartedEventArgs, однако мы можем получить BuildEventContext, от которого можно получить ProjectInstanceId, а он в свою очередь существует и для ProjectStarted, т.е. мы можем просто запомнить ProjectId и далее ссылаться на него везде, где доступен BuildEventContext, например, просто:

projects[e.ProjectId] = new Project() {
    Name            = properties["ProjectName"],
    File            = e.ProjectFile,
    Properties      = properties
};

Собственно, теперь вы можете полностью транслировать событийную модель в VSPackage с изолированной средой и работать как есть:


"C:\Program Files (x86)\MSBuild\12.0\bin\msbuild.exe" "app.sln" /l:"<логгер>.dll" /m:12 /t:Rebuild /p:Configuration=<cfg> /p:Platform=<platform>

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


На самом деле здесь есть ряд проблем или особенностей, которые хотелось тоже осветить. Но в целом это слишком специфично именно для нашего проекта, а для вашего случая может вообще ничего подобного и не понадобиться, поэтому можно пропустить.

Так как наш плагин работает с событиями построения, не трудно догадаться, что может потребоваться и получение типа построения, с которым все это началось. Но для VS с этим не все так просто.

Вы уже знаете и понимаете, что мы работаем по приоритетной подписке, и у нас не получится использовать что-то типа:

_buildEvents.OnBuildBegin += (vsBuildScope Scope, vsBuildAction Action) => {
    buildType = (BuildType)Action;
};

Потому что это слишком поздно.

Вы можете попробовать работу с IVsUpdateSolutionEvents4, однако как я и написал в вопросе и ответе, только если вы не планируете совместимсоть со старыми версиями.

На тот момент выбор остановился на перехвате комманд от VS IDE, например:

_cmdEvents.BeforeExecute += (string guid, int id, object customIn, object customOut, ref bool cancelDefault) => {

        if(GuidList.VSStd97CmdID == guid || GuidList.VSStd2KCmdID == guid) {
            _c.updateContext((BuildType)id);
        }

};

Не особо нравится, лучше использовать IVsUpdateSolutionEvents4, но единственный рабочий вариант для совместимости версий и сохранения приоритетной обработки, до чего смог додуматься на тот момент. Да и обрадовать другими решениями никто пока не спешит.

Межмодульное взаимодействие


Ну здесь на самом деле ничего примечательного не будет, все обыденно, однако в рамках статьи небольшой description.

Как вы уже успели заметить, компоненты разносторонние и даже различные по своей модели обработки событий. На примере моего плагина это выглядит следующим образом:

image
Лист того, что изображено, можно найти тут.

Основные моменты для ваших работ:
  • Выделяем общий публичный интерфейс на уровне проектов, для того, чтобы все необходимые компоненты могли предоставлять по нему реализацию. Так делает Shell.Interop, так делает EnvDTE и т.п.;
  • Определяем, где будет располагаться главное ядро программы. Тот, кто и будет ответственнен за единую обработку данных;
  • Определяем где-нибудь загрузчик библиотеки. Это может быть также как у нас внешний Provider и т.п.;
  • Каждый из компонентов, соответсвенно, самостоятельно определяет, как ему подключаться к тому, где он будет использоваться, и в общем виде должен заниматься лишь пересылкой и трансляцией, если нужно, обрабатываемых данных. (полагаю, пример как работать с внешними lib приводить не нужно; вы можете найти либо в моих исходниках, либо вообще где угодно, это за рамками статьи).

Тут есть важное замечание. Наш плагин изначально не предполагалось использовать в таком объеме, поэтому ядро системы располагается в VSPackage.

Да, это удобно не разделять на отдельные компоненты, но чем это плохо? Дело в том, что моему VSPackage на схеме выше, приходиться тянуть за собой тяжелые зависимости на EnvDTE & EnvDTE80 (которые, в свою очередь, имеют еще более экзотические связи). Все это может, и скорее всего будет отсутствовать на серверах непрерывной интеграции, т.к. прежде всего поставляется стандартный msbuild tools, и другое подобное. То есть, не забывайте о легких самодостаточных компонентах и прозрачных интерфейсах. Я же пока не спешу с разделением, т.к. все это время, и оно пока того не стоит, он ведь и был изначально VSPackage решением, как-то так.

Доступ и оценка MSBuild Properties & MSBuild Property Functions


Последний момент, который я хочу рассмотреть — это evaluation свойств. Вообще доступ к properties может происходить несколькими вариантами, например:

А также другие.

Наиболее гибкий вариант — использовать Microsoft.Build.Evaluation, т.к. здесь наиболее простой способ получить оценку средствами msbuild движка и работать с проектами, например:

public virtual string evaluate(string unevaluated, string projectName = null)
{
    ...
    lock(_lock)
    {
        try {
            ...
            project.SetProperty(container, Scripts.Tokens.characters(_wrapProperty(ref unevaluated)));
            return project.GetProperty(container).EvaluatedValue;
        }
        finally {
            project.RemoveProperty(project.GetProperty(container));
        }
    }
}

Однако, как уже можно было понять, для solution-context вам придется решать, как работать с данными от проектов. Я лично реализовал доп. расширение синтаксиса.

То есть предварительно все-таки приходится производить разбор средствами своего анализатора, а далее направлять подготовленные данные, с которыми уже справится оригинальный движок msbuild.

Заключение


Собственно, это первая заметка на Хабрахабр. Надеюсь, не последняя -_-.

В материале рассмотрены ключевые моменты, чтобы понять, что нужно делать, какие варианты решений существуют, и с какими проблемами и особенностями придется столкнуться по работе с событийной моделью построений в VS, а также его связанных компонентах devenv & MSBuild tools, на примере реального рабочего решения.

Детали реализации вы сможете найти в исходниках, а также получить доп. информацию в комментариях позднее, если будут вопросы.

Пишите об ошибках и неточностях, подправлю, если будут… было весьма неудобно без markdown.

upd.


В моих попытках донести до комментаторов, что:
Материал описывает — способы работы с указанными элементами, а также решение проблем при их использовании в описанных обстоятельствах. Для того, чтобы разработчик VS смог ознакомиться с существующими решениями, и применить полученные знанния при разработке — чего угодно и для чего угодно своего… увы не увенчались успехом.

Обсуждаем исключительно — способы решения проблем с помощью этих разработок для конечного пользователя, а также вообще пытаемся решить абстрактные проблемы, которые никак не были представлены или детально описаны — решаем для того чтобы решить, не ознакомившись с предметной областью.

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

upd2. Targets & Map of projects.


Небольшой бонус, пока я поддерживаю материал, для тех кто хочет, но не может использовать IVsUpdateSolutionEvents2 / IVsUpdateSolutionEvents в силу различных обстоятельств.

Обсуждая проблему событий 'уровня solution' (для простоты, продолжаем ссылаться на такую формулировку) с обратившемся ко мне человеком, я также рассмотрел возможность работы с targets взамен реализации указанных интерфейсов, т.к. у него это было явно лишним звеном, от которого следует избавиться. Ну, пробуем добиться некоторого эквивалента.

К тому же, если вам просто нравиться что-то подобное — MSBuild: Extending the solution build, (но подобный вариант может работать только при построении от msbuild.exe и не от VS IDE...)
  • VS IDE вообще не рассматривает .sln файл при построении. Она формирует конечные цели из своего загруженного окружения с EnvDTE и т.п.
  • Файл .sln рассматривается только msbuild.exe — т.е. перед построением он автоматом формирует .metaproj (в памяти, по умолчанию), который содержит что и когда будет строиться, включая общий targets под все проекты если существует. Например:

...
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\SolutionFile\ImportAfter\*" Condition="'$(ImportByWildcardBeforeSolution)' != 'false' and exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\SolutionFile\ImportAfter')" />
<Import Project="D:\tmp\p\after.Sample.sln.targets" Condition="exists('D:\tmp\p\after.Sample.sln.targets')" />
<Target Name="Build" />
<Target Name="Rebuild" />
<Target Name="Clean" />
<Target Name="Publish" />

  • При этом .metaproj не рассматривается VS IDE, т.к. у него уже свои сформированные цели.
  • А также мы не знаем, что и когда вообще происходит, т.к. проекты строяться все по отдельности с заранее определенными целями VS.

т.е. для работы с общим targets под build-операции от VS IDE, мы можем использовать только файлы проектов (имеется ввиду без модификации/расширения VS) с некоторым ограничением.

Рассматриваем, соответственно, ситуацию общего решения, или когда мы можем не знать вообще о проектах которые будут в solution у конечного пользователя.

В качестве такого ограничения можно рассматривать карту проектов (есть еще как минимум 2 варианта, однако только этот показал себя более или менее стабильным, поэтому бонус только про него):


...
<Target Name="_Build" BeforeTargets="Build" DependsOnTargets="ProjectsMap">
    <CallTarget Targets="_BuildPRE" Condition="$(ScopeDetectFirst)" />
    <CallTarget Targets="_BuildPOST" Condition="$(ScopeDetectLast)" />
</Target>
<Target Name="_BuildPRE">
    <!-- ... -->
</Target>
<Target Name="_BuildPOST">
    <!-- ... -->
</Target>
...

В общем виде, формируя подобную карту, мы теперь знаем что и когда должно быть (формируется с ProjectsMap, детект с ScopeDetectFirst и ScopeDetectLast соответсвенно), т.е.:
  • Просто автоматически добавьте (например с NuGet events и т.п.) ваш общий .targets файл во все проекты, например:
    <Import Project="..\<SolutionFile>.targets" />
  • Далее, с ограничением в виде карты проектов, теперь мы сможем добиться следующего выполнения (для Clean, Build, Rebuild и т.д. целей):
    • «до начала первого проекта»
    • «после окончания последнего проекта»

Это безопасно для всех или большинства случаев (изменение порядка сборки или удаление некоторых проектов из решения, все должно отработать нормально). Однако! этот вариант имеет ряд неудобств в виде обслуживания импорта на стадии инициализации, а также при добавлении новых проектов. Это неудобно, но вариант для IVsUpdateSolutionEvents.

К тому же я отметил потенциальную проблему с Unload projects (когда IDE позволяет пользователю временно выгружать проект из решения) в заголовке — однако, в большинстве случаев это будет допустимый вариант, т.к. этот unload временный, и ошибка справедлива в некотором виде, поэтому ее можно допустить (к тому же, проблему все-таки можно решить при желании).

Касательно удобства, спорный момент… в ваших продуктах VSPackage также может оказаться лишним звеном, и как вариант вы можете рассмотреть что-то подобное…
Однако, что касается надежности, я все-таки выбрал бы реализацию IVsUpdateSolutionEvents с VSPackage! т.к. подобное минимум стандартизировано, и соответственно предсказуемо на различных версиях.

upd3.



  • Небольшое примечание к msbuild событиям, с поправкой на мультипоточность и возможность вмешиваться в процесс сборки, пример ошибок CSC error (CS2001). Смотреть выше
  • Продолжение банкета. Расширенная часть по разработке полноценного MSBuild Plugin, в деталях. Однако по понятным причинам, публиковаться здесь не стал. Читайте на CodeProject здесь. Вы узнаете:
    • Возможность обслуживания любых проектов и решений, о которых может быть вообще ничего не известно заранее + гибкое взаимодействие.
    • А также реализация этого обслуживания для MSBuild Tool в качестве плагина подключаемого во время сборки, без каких либо вмешательств в файлы проектов и т.п.
  • Также, вскользь описал работу с EnvDTE.CommandEvents как альтернативу обработчика событий, см. новые варианты в моем ответе на SO здесь.


Ссылки по теме


Tags:
Hubs:
+6
Comments 31
Comments Comments 31

Articles