開発者向けのプロジェクトおよびVisual Studioソリューションを構築するためのイベントモデル

この短い記事が役立ちます:


あらすじ


多くの場合、特定のプロセスの自動化に対処する必要があるため、ソリューションの一部が遅かれ早かれVisual Studioに触れたことは驚くことではありません。

実際、この記事、またはメモは、2年前にC ++の1つのプロジェクトで作業したときの副産物でしかなかった、長く使用されているプラ​​グインの結果です。 ただし、Habrahabrでの私のデビューは、おそらくこれからでしょう。

最近、DevDivの男が同様のニーズ(プラグインが元々解決することを意図していたもの)で私に対処しました。 高度にカスタマイズされた自動化という彼の問題を解決する試みにおいて、ソリューションとVSとMSBuildの相互作用のいくつかの側面を強調することが依然として不可欠であることが明らかになりました。 結局のところ、この資料はもともと入手可能ではなく、まだパブリックドメインにないようです。

警告


始める前に、資料はVisual Studioユーザー向けではなく、開発者向け(Visual Studio環境自体の1つまたは別の拡張機能を提案する)ことに注意してください。 そして、そもそも、誰かが自分の開発のために同様のタスクに直面した場合に、これらのコンポーネントのソリューションと相互作用のいくつかの側面を強調するために呼び出されます。 これは段階的な説明ではありませんが、最低限の原則と作業スキームを理解するのに役立つことを目的としています。 たとえば、devenv.exe、msbuild.exeとの相互作用は、優先サブスクリプションで動作し、上記のリストに示されているすべてのものです。

問題について


私たち両方にとっての最初の必要性は、スタジオのイベントモデル内でSolution-Contextを使用したことであり、現在も使用中です。
もちろん、Visual Studioで作業した人は、プロジェクトレベルでのビルド前/ビルド後のイベントについて知っておく必要があります。 ただし、特にこれらのプロジェクトが数十個ある場合など、ソリューション全体のプロジェクト管理のニーズを完全に満たすわけではありません。これは主にC ++プロジェクトで一般的です。

この問題を解決するにはいくつかの方法があります 。 たとえば、前述のものは、ソリューションから他のすべてが依存するプロジェクトがあるときに、ソリューションからの移行を実行します。

では、なぜMSはIDEの同様のコンテキストを強調していないのですか?

実際、すべてが.sln形式と同じです。これは、プロジェクトファイルやより柔軟でエレガントな有効なxmlドキュメントではなく、ハンターのメモを表します。
互換性? ただし、おそらく、VS2010で大きな変更が発生した場合でも、それを壊す必要がありました。

ソリューションコンテキストを使用します。 イベントを上位レベルに上げることはできません 1つのコンテキストでmsbuildデータなどを管理する問題を解決する必要があります。ロードされたタイムラインにこれ​​を追加するのを急いでいる人はいません。

MS Visual Studioのプロジェクトおよびソリューションのイベント処理(VS2010、VS2012、VS2013、VS2015)


まず、EnvDTEオプションを見てみましょう。 このタスクの主な焦点はBuildEventsで 、これはOnBuildBeginOnBuildProjConfigBeginなどの基本的なイベントに手頃な価格のサブスクリプションを提供します。 つまり、公開イベントとして実装されます。

ただし、この場合、すべてのDTEリスナーに通知されたときに動作が遅すぎます。 パッケージでできるだけ早く状況を制御する必要があります。

幸いなことに、VSはそのような処理を優先し、それらはすべてそのような処理で実行されます。

一般的に、これは同じオブザーバークラシックであり、特定のサービスのAdviseメソッドを通じて実装されます。 しかし、まず最初に。

まず、 Microsoft.VisualStudio.Shell.Interopに注意を向ける必要があります 。 VSおよびソリューションレベルのその他の基本的な「ビルドイベント」、つまりIVsUpdateSolutionEventsの操作を支援するのは彼です。

前述のニーズに対応するには、 VS2010まで利用可能な基本的なIVsUpdateSolutionEvents2のみを考慮するだけで十分です。 実際、 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); 

これらの方法はすべて、ソリューションコンテキストレベルの実装に主に適しています。
私たちが提供したプロジェクトについて:

 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から保護するためのクラスの一部である必要があります。

これで最前線で聴く準備ができました。 私たちが一番最初ではないことを理解することも重要ですが、これは必要ありません。

コマンドラインモードをサポート


誰かがプラグインで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 
[?]
しかし...

 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); } ... 

しかし、誰もが長い間知っているように、アドインはVisual Studio 2013で非推奨になりました 。 つまり、このようなトリックはVS2010、VS2012、VS2013で機能します。 計画されているVS2015では、このようなゲームは機能しません。

これについてはすでにMS Connect Issue#1075033で書いていますが、 興味のある人に別れを告げることができます。 VS2015は既にRCにあり、タスクは単純に閉じられています。

どうぞ

MSBuildツールから同様のイベントモデルをエミュレートする


そもそも、MSBuildツールは最も強力なツールであり、上記の問題を誰も知らないはずです。 ソリューションレベルの$(構成)と$(プラットフォーム)を簡単に処理できます。ターゲットなどで作業できます。ただし、ソリューションは同じである必要があり、VS IDEとCI /特別ビルドサーバーでのビルドの作業の違いに気付かないはずです。 したがって、msbuildツールのフレームワークで上記のイベントを使用する機会を検討します。

DTE2コンテキストも、イベントの登録サービスも利用できません。VSと通信するものはありませんが、他に何を待っていましたか 。 はい、もちろん、たとえば、 GetActiveObjectを使用して同じDTE2コンテキストを取得できます。

 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) { ... } } 

ただし、知っておく必要がある機能がいくつかあります。

実際には、ハンドラー-ProjectStartedEventHandlerは引数としてProjectStartedEventArgsも送信します。たとえば、次のように、 .slnファイルを追跡して処理することができます。

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

このオプションは、処理のために.slnを受け取るまで待機している可能性があり、プロジェクトファイルを通過する前でなければなりません。

これに対する苦痛は始まったばかりです、なぜなら 本格的な作業には、たとえば、新しいmsbuildプロパティの評価、ビルドの中断など、msbuildエンジンにアクセスするためのプロジェクトの読み込みなど、.slnデータが必要になります。

残念ながら、.slnファイルを操作するには、通常のものは表示されません。より正確には、好みに合わせて選択します。

選択肢は豊富ではありませんが、.slnは長い間理解できない形で...

注:

上記の「IEventSource.BuildStartedは動作しません」というフレーズは完全に真実ではありません...必要なデータを取得するには実際には早すぎますが、両刃の剣です。 ここでは、同様の実装で発生する可能性のある問題について説明しました。 したがって、マルチスレッドアセンブリ/ m:2+ (使用する2つ以上の同時プロセス)の場合、これは非常に好ましくない結果を引き起こし、最初のイベントの処理前にmsbuildコレクターによってすべてのターゲットが決定されたCSCエラー(CS2001)の特殊なケース、実行時の対応する変更は、新しい変更に影響しなくなりました。 ただし、このカバレッジは、建設プロセスなどを妨げるタスクの一部にすぎません。

したがって、最も柔軟な方法をお勧めします。必要なすべてのデータを事前に準備して、IEventSource.BuildStartedを使用します。 この方法については、 この資料で説明しましたが、MITの簡単なslnパーサーを見つけるか、メインライブラリの一部としてパーサーを見ることができます。

どうぞ
プロジェクトでは、msbuild(MSBuild Property Functions)などのプロパティと機能を評価する必要がありますが、 それらがない場合はどうでしょうか...

これが必要な場合は、これを使用するには、msbuildエンジンを準備し、最初に開始する必要があります。 分離された環境で作業するため、msbuild.exeから開始されたプロパティを転送することも必要です。

.NET 4.0を使用している場合、これを手動で行う必要があります。 Configuration、Platform、SolutionDirなどを定義する必要があります。 ハンドラー用。 ただし、.NET 4.5プラットフォームの場合、 ProjectStartedEventArgs.GlobalPropertiesは使用可能です。

サンドボックスが完全に初期化された後、最終的にプロジェクトイベントを処理できます。 ただし、msbuildを使用し、ターゲットを使用します。 したがって、 TargetStartedにサブスクライブする必要があります。

ブロードキャストの場合、受信したTargetNameのPreBuildEventとPostBuildEventに満足します。次に例を示します。

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

しかし、ここでは、すべてがスムーズではありません。 実際には、現在TargetStartedEventArgsで処理されているプロジェクトを参照することはできませんが、 ProjectInstanceIdを取得できるBuildEventContextを取得できます。 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を使用する方が良いですが、バージョン互換性と優先処理を維持するための唯一の有効なオプションです。 そして、誰も他の決定を喜ばせようとしません。

モジュール間相互作用


さて、ここでは、実際、目立ったものは何もありません。すべては平凡ですが、記事の枠組みの中には小さな説明があります。

既にお気づきのように、コンポーネントは用途が広く、イベント処理モデルも異なります。 プラグインを例として使用すると、次のようになります。

画像
表示されているもののシートはここにあります

仕事のハイライト:

ここには重要なポイントがあります。 私たちのプラグインはもともとそのようなボリュームで使用されることを意図していなかったため、システムのコアはVSPackageにあります。

はい、個別のコンポーネントに分割しない方が便利ですが、なぜ悪いのですか? 実際、上の図の私のVSPackageは、EnvDTEとEnvDTE80(さらにエキゾチックな接続を持っている)に強く依存している必要があります。 これはすべて、継続的インテグレーションサーバーでは可能です。 まず、標準のmsbuildツールなどが提供されます。 つまり、軽量の自己完結型コンポーネントと透過的なインターフェイスを忘れないでください。 私は分離に急いでいません このすべての時間、そして今のところそれは価値がありません、結局のところ、それはもともとVSPackageソリューションでした。

MSBuildプロパティとMSBuildプロパティ関数へのアクセスと評価


最後に考慮したいのは、プロパティの評価です。 一般に、プロパティへのアクセスは、次のようないくつかの方法で発生します。

他の人と同様。

最も柔軟なオプションは、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)); } } } 

ただし、既に理解できたように、ソリューションコンテキストでは、プロジェクトのデータを操作する方法を決定する必要があります。 私は個人的に追加を実装しました 構文拡張

つまり、最初にアナライザーのツールを使用して解析する必要があります。次に、元のmsbuildエンジンが既に対応している準備済みのデータを送信します。

おわりに


実際、これはHabrahabrに関する最初のメモです。 最後ではないことを願っています-_-。

この資料では、実際のソリューションの例を使用して、実行する必要があるもの、存在するソリューションオプション、VSでビルドのイベントモデルを操作するときに発生する問題と機能、および関連するコンポーネント、devenvおよびMSBuildツールを理解するための重要なポイントについて説明します

ソースで実装の詳細を確認したり、追加したりすることができます。 質問がある場合は、コメントの情報を後で。

エラーと不正確さについて書き、それらがあれば修正します... それはマークダウンなしで非常に不便でした。

更新しました。


コメンテーターに次のことを伝えたいと思っています。
この資料では 、これらの要素を使用する方法、および説明されている状況で要素を使用する際の問題を解決する方法について説明しています。 VS開発者が既存のソリューションに精通し、開発で得た知識を適用できるようにするために- なんとか、彼自身のもの... 残念ながら、それらは失敗しました

エンドユーザー向けにこれらの開発を使用して問題を解決する方法、 および一般に、どのような方法でも提示されなかった、または詳細に説明されなかった抽象的な問題を解決しようとする方法を専ら議論 します-私たちは、主題分野に精通せずに解決するために解決します。

形成された聴衆の排他性を考慮して、この記事でコメントを続けるために、私はそれが適切であるとは思いません。
ただし 、このトピックについてまだサポートが必要な場合は、空き時間があるので、おそらくアドバイスできます。 G +の連絡先。

upd2。 プロジェクトのターゲットとマップ。


さまざまな状況のためにIVsUpdateSolutionEvents2 / IVsUpdateSolutionEventsを使用したいが使用できない場合は、この資料をサポートする一方で、わずかなボーナスがあります。

「ソリューションレベル」のイベントの問題(簡単にするために、このような定式化を引き続き参照します)を私に連絡した人と議論し、示されたインターフェイスを実装する代わりにターゲットを操作する可能性も考えました。 彼は明らかに取り除くための余分なリンクでした。 まあ、いくつかの同等を達成してみてください。

さらに、そのようなものが好きな場合-MSBuild:ソリューションビルドの拡張 (ただし、VSIDEではなくmsbuild.exeからビルドする場合にのみ同様のオプションが機能します)

 ... <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" /> 


つまり VS IDEからのビルド操作で一般的なターゲットを操作するには、プロジェクトファイル(VSを変更/拡張せずに)を使用できますが、いくつかの制限があります。

それぞれ、一般的なソリューションの状況、またはエンドユーザーがソリューションで持つプロジェクトについてまったくわからない場合を考慮します。

このような制限として、プロジェクトマップを検討できます(少なくとも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の検出器で形成されます) 、すなわち:

すべてまたはほとんどの場合に安全です(アセンブリの順序を変更したり、ソリューションから一部のプロジェクトを削除したり、すべてが正常に機能するはずです)。 しかし!このオプションには、初期化段階や新しいプロジェクトを追加するときのインポートサービスという形で多くの不都合があります。これは不便ですが、IVsUpdateSolutionEventsのオプションです。

さらに、ヘッダーでプロジェクトのアンロード(IDEによりユーザーが一時的にソリューションからプロジェクトをアンロードできる場合)の潜在的な問題に注意しましたが、ほとんどの場合、これは有効なオプションです。このアンロードは一時的なものであり、エラーは何らかの形で有効であるため、許可できます(さらに、必要に応じて問題を解決できます)。

利便性に関しては、議論の余地があります... VSPackageは製品の追加リンクでもあり、オプションとして、そのようなものを検討するかもしれません...
しかし 信頼性に関しては、VSPackageでIVsUpdateSolutionEventsを実装することを選択します! なぜなら このような最小値は標準化されているため、異なるバージョンで予測可能です。

upd3。





関連リンク


Source: https://habr.com/ru/post/J258057/


All Articles