65.9K
CodeProject 正在变化。 阅读更多。
Home

MSBuild 插件。在运行时管理大多数进程以及更多。无国界项目

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (3投票s)

2016 年 4 月 13 日

Apache

11分钟阅读

viewsIcon

17402

对强大的 MSBuild 插件系统进行解释和架构设计,以灵活地服务于任何项目和库、构建进程以及运行时进程。

# 简介

我建议从另一个角度来看待 MSBuild -_* plınqsɯ (在你转回头之际,我将在下面告诉你关于本文的内容)。

关于本文

  • 本文描述了服务于任何项目和解决方案的可能性,这些项目和解决方案可能完全未知,以及任何其他灵活的交互方式。
  • 本文还描述了将此服务实现为 MSBuild 工具的通用插件,在特定构建过程中连接(即,无需对项目文件等进行任何额外操作)。

这不是一个“一步一步”的说明,但它应该有助于理解 - 如何通过示例(不是Task库!)开发你自己的 MSBuild 插件,并注重细节。

# 背景

示例将足够简单,但来自我 3-4 年前必须与之斗争的真实项目和真实问题。

目前,它已经以通用工作的形式制作,并被推荐为免费工具,主要用于 Visual Studio 和 MSBuild 工具。而且,根据我所掌握的关于该工具及类似工具开发的资料,继续描述这部分技巧将是非常好的……

当时主要的问题之一是需要对已加载解决方案中的所有项目进行灵活的服务,而这些项目事先是未知的。大多数情况下,这些情况对于一些完整的或开箱即用的解决方案来说很典型,但也适用于日常任务,如自动版本控制,以及一般的任何解决方案级别的事件。作为这部分内容,你们中的几位可能已经在 SO 这里 看到过我的回答。

因此,我们只考虑 MSBuild 组件,但是如果您需要一个完整的画面来了解所有这些解决方案作为 Visual Studio、Devenv、MSBuild 和其他可能的应用程序(如 CI 等)的统一环境,那么我之前写了关于此解决方案的另一部分 这里(仅俄语)。

请记住,可以准备一个统一的环境交互和处理,以根据需要维护所有项目,并使用当前描述的方法。

因此,作为项目构建交互和维护的示例,为了简单起见,我们采用最常见的任务 - 自动或自定义版本控制。至于交互元素,我们将尝试获取 MSBuild 开始构建项目时的一些主要事件层:即解决方案级别、项目级别等的构建前/后事件。

但是,在开始之前,请注意可能的替代方案,例如 ITask,但该解决方案的主要缺点是修改项目文件(+ <UsingTask ...),这可能非常不受欢迎或因某些原因而不方便……等等。

# 插件

让我们开始实现我们的 MSBuild 工具插件。

为了获得上述对 msbuild.exe 的控制,我们应该将开发的逻辑(或最小的翻译到其他库 - 包装器等)注册为一个简单的日志记录器。

实际上,最容易访问的方式(简单易懂)来与 MSBuild 交互并管理其所有内部进程(从不同环境中,它可以在其中工作)是日志记录。为此,我们将使用 Microsoft.Build.Framework,并部分使用 Microsoft.Build.Evaluation

这将使我们能够在修改任何项目文件的情况下解决所有基本任务,并且还可以部分管理构建线程。

需要指出的是,MSBuild 日志记录器当然不是为此目的或类似目的而创建的:)它只是日志记录,仅此而已。
然而,我们将尝试以一种略有不同且灵活的方式使用它来扩展可能任务的范围。

首先,您可以在此处找到一个工作插件的示例

我们的交互将通过日志记录器实现,因此我们在任何操作上都受到严格限制。但实际上并非太糟……

# 入口点

让我们看看基本的入口点和 IEventSource

为了让我们的未来库与 MSBuild 工具协同工作,我们应该实现 ILogger。正如您已经知道的,我们将仅将其用于基本访问(之后,它将是一个强大的工具)。最简单的方法是扩展 Logger 类。

public class EventManager: Logger
{
    public override void Initialize(IEventSource evt)
    {
        ... // our entry point
    }
    
    public override void Shutdown()
    {
        ...
    }
}

在入口点内部,我们可以加载自定义主逻辑,包括从其他库加载,例如。

ILoader loader = new Loader(
                        new Provider.Settings()
                        {
                            DebugMode = log.IsDiagnostic,
                        }
                    );

然后,我们必须了解如何处理项目数据及其解决方案。

# 访问 MSBuild 属性

为了处理项目中的 MSBuild 属性,我们需要将我们的日志记录器扩展为一个正常的插件状态。
如果您使用的是 .NET 4.0,您需要手动完成,即,您需要为下面的处理程序定义 Configuration、Platform、SolutionDir...。但是对于 .NET 4.5 平台,可以使用 ProjectStartedEventArgs.GlobalProperties

如果您仍然使用 .NET 4.0 或需要一个通用工具,那么从上面的项目中还可以找到一个名为 SolutionProperties(b02d72c)的小类,它将帮助您快速获取下一步所需的所有属性,例如。

(new SolutionProperties()).parse("path_to_sln");

结果将是。

public sealed class Result
{
    // Configurations with platforms in solution file
    public List<SolutionCfg> configs;
    
    // Default Configuration for current solution
    public string defaultConfiguration;
    
    // Default Platform for current solution
    public string defaultPlatform;
    
    // All available global properties like for ProjectStartedEventArgs.GlobalProperties (net 4.5+)
    public Dictionary<string, string> properties;
}

# 评估新的 MSBuild 属性和属性函数

拥有对 GlobalProperties(见上文)的访问权限,我们可以尝试全面使用 MSBuild 引擎,例如,评估新的 MSBuild 属性并使用复杂的 属性函数,准备 BuildRequest 等等……

我们只需要为我们自己的插件初始化 MSBuild 引擎。CIM 示例是一个仅用于主插件的包装器,因此类似的逻辑位于客户端之外,并通过通用接口(如 Provider.ILibrary)进行控制。要查看与 MSBuild 引擎交互的完整实现,您需要查看此处:

这是为隔离环境(是的,这是我们的情况)实现的 IEnvironment。总的来说,为了完全开始使用 MSBuild 的所有功能,您可以使用方便的 Microsoft.Build.Evaluation

new Microsoft.Build.Evaluation.Project("path_to_project_file", 
properties, null, ProjectCollection.GlobalProjectCollection);

其中属性是我们上面的全局属性。您还可以使用 Microsoft.Build.BuildEngine 等。

然后,您可以随意使用 MSBuild,例如,任何令人难以置信的属性评估。

简单绕过可用项

foreach(ProjectProperty property in project.Properties) {
  ...
}

或控制其他构建

    BuildRequestData request = new BuildRequestData(
                                    new ProjectInstance(root, propertiesByDefault(evt), 
				    root.ToolsVersion, ProjectCollection.GlobalProjectCollection),
                                    new string[] { ENTRY_POINT },
                                    new HostServices()
                               );

#if !NET_40

    // Using of BuildManager from Microsoft.Build.dll, v4.0.0.0 - .NETFramework\v4.5\Microsoft.Build.dll
    // you should see IDisposable, and of course you can see CA1001 for block as in #else section below.
    using(BuildManager manager = new BuildManager(Settings.APP_NAME_SHORT)) {
        return build(manager, request, evt.Process.Hidden);
    }

#else

    // Using of BuildManager from Microsoft.Build.dll, 
    // v4.0.30319 - .NETFramework\v4.0\Microsoft.Build.dll
    // It doesn't implement IDisposable, and voila:
    // https://ci.appveyor.com/project/3Fs/vssolutionbuildevent/build/build-103
    return build(new BuildManager(Settings.APP_NAME_SHORT), request, evt.Process.Hidden);

#endif

我们已经过半了……

# MSBuild 工具的事件层

好了,现在我们可以做任何您需要的事情,甚至更多。我们只需要解决我们的插件何时工作或遵循某种行为,即事件。

首先,基本入口点已经提供了 IEventSource

void Initialize(IEventSource evt)

因此,首先订阅所需事件就足够了,而对于您未来插件的完整工作,最需要的是:

evt.TargetStarted   += onTargetStarted;  // Pre/Post-Build events of project-level + custom actions for specific targets
evt.ProjectStarted  += onProjectStarted; // Pre-Build events of solution-level (deprecated, see below)
evt.AnyEventRaised  += onAnyEventRaised; // All Build information
evt.ErrorRaised     += onErrorRaised;    // All errors
evt.WarningRaised   += onWarningRaised;  // All warnings
evt.BuildStarted    += onBuildStarted;   // Pre-Build events of solution-level (recommended, see below)
evt.BuildFinished   += onBuildFinished;  // Post-Build & Cancel-Build events of solution-level

但是,请注意以下几点。

# 异常

向 MSBuild 工具抛出异常,您只能在特定上下文中进行,即当它已准备好时,否则存在完全失去控制的风险。进程可能会保持暂停状态,这对于 CI 服务器等至关重要。

这是 ILogger 在 MSBuild 中的内部实现特性,因此第一个抛出安全异常的机会将是:

Initialize() 开始。

  1. 第一个事件 StatusEventRaised 以消息 ~“Build Started”触发 - 我们无法在此处抛出任何异常!
  2. 然后立即应该是 AnyEventRaised,它以相同消息 ~“Build Started”触发 - 我们也无法在此处抛出任何异常!
  3. 然后是 ProjectStarted - 这里已经可以了,应该是 [安全的]。
  4. 对于 StatusEventRaised & AnyEventRaisedProjectStarted 之后也是可能的,但首先检查 BuildContext 是否不为 null

抛出的异常将终止所有 MSBuild 任务的执行,例如:

_________________________________________________
Build started 12.04.2016 21:22:05.

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:00.03
MSBUILD : Logger error MSB1029: muahahahaaa

另外请注意,以下事件将不会触发(在上述异常之后绝不会触发):

    evt.ErrorRaised     += onErrorRaised;
    evt.WarningRaised   += onWarningRaised;

如果您需要控制构建,您应该了解这些特性(我们是日志记录器,我们不能直接控制任何行为 - 这当然是合乎逻辑的……)
总结上述内容,对于 IEventSource,您应该从 ProjectStarted 及之后抛出异常,作为第一个安全的地方,例如:

EventManager:137 (7896c05) - 它支持转发控制命令的简单逻辑 - EventManager:268 (7896c05)

protected void command(object sender, CoreCommandArgs c)
{
    switch(c.Type)
    {
        case CoreCommandType.AbortCommand: {
            abortCommand(c);
            break;
        }
        case CoreCommandType.BuildCancel: {
            abort = true;
            break;
        }
        case CoreCommandType.Nop: {
            break;
        }
        case CoreCommandType.RawCommand: {
            rawCommand(c);
            break;
        }
    }
    receivedCommands.Push(c);
}

这使我们能够继续灵活地管理强制停止等。但这是特定程序的实现细节……

# 当前处理的项目

为了充分处理所有这些事件(TargetStartedWarningRaised,…)中的任何项目,我们需要知道 - 当前正在处理哪个项目。

来自 IEventSource.ProjectStartedProjectStartedEventArgs 将在此方面帮助我们。

实际上,这是在整个处理过程中我们可以获取项目最少信息的唯一地方,具体来说是:ProjectId 及其属性。

因此,我们只需要将其与 ProjectId 相关联,并且由于它在其他事件中也可用,您可以尝试这样做:

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

然后可以将其引用为,例如在 IEventSource.TargetStarted 中:

int pid = e.BuildEventContext.ProjectInstanceId;
library.Event.onProjectPre(projects[pid].Name);

等等。

# 基本事件及其问题

我们即将完成。让我们教我们的插件辨别敌友。:)
我们开始实现基本事件。

# 解决方案级别的构建前事件

在这里,我做了一个小的笔记,说 PRE 事件只能与 IEventSource.ProjectStarted 一起使用,这在一定程度上是正确的,但这个地方可能会给您的逻辑带来一些问题。例如 - **CSC 错误 (CS2001)** 如果对任何构建使用多线程,即 /m:2+(使用 2 个或更多并发进程)。后来,这个问题已经为上面的插件修复了,如下:

    [CI.MSBuild] Fixes for Multi-Processor Environment

    In general, the logic of calling of the PRE event has been moved from onProjectStarted.
    However it also has required a lot of changes with manually detecting properties for our core.

因此,带上所有准备好的 MSBuild 属性(如何做 - 已在上文展示)的 IEventSource.BuildStarted 是处理构建前事件的更正确的位置。

protected void onBuildStarted(object sender, BuildStartedEventArgs e)
{
    try {
        ptargets.Clear();

        // yes, we're ready
        onPre(initializer.Properties.Targets);
    }
    catch(Exception) {
        abort = true;
    }
}

# 项目级别的构建前/后事件

这要容易得多,因为我们只需要定义对所有传入目标的控制,具体来说:

protected void onTargetStarted(object sender, TargetStartedEventArgs e)
{
    int pid = e.BuildEventContext.ProjectInstanceId;

    // the PreBuildEvent & PostBuildEvent should be only for condition '$(PreBuildEvent)'!='' ...
    switch(e.TargetName)
    {
        case "BeforeBuild":
        case "BeforeRebuild":
        case "BeforeClean":
        {
            if(!ptargets.ContainsKey(pid)) {
                ptargets[pid] = false; //pre
                library.Event.onProjectPre(projects[pid].Name);
            }
            break;
        }
        case "AfterBuild":
        case "AfterRebuild":
        case "AfterClean":
        {
            if(!ptargets.ContainsKey(pid) || !ptargets[pid]) {
                ptargets[pid] = true; //post
                library.Event.onProjectPost(projects[pid].Name, projects[pid].HasErrors ? 0 : 1);
            }                    
            break;
        }
    }
}

等等。

# 错误/警告 + 构建信息

这直接是所有日志记录器的职责。:)因此,处理起来没有困难,因为我们已经可以引用特定项目(见上文),并且一切皆有可能。

protected void onErrorRaised(object sender, BuildErrorEventArgs e)
{
    if(projects.ContainsKey(e.BuildEventContext.ProjectInstanceId)) {
        projects[e.BuildEventContext.ProjectInstanceId].HasErrors = true;
    }
    
    library.Build.onBuildRaw(formatEW("error", e.Code, e.Message, e.File, e.LineNumber));
}

# 解决方案级别的构建后和取消构建事件

这也很简单,就像错误/警告一样(最糟糕的已经过去)IEventSource.BuildFinished 及其 BuildFinishedEventArgs 提供了 Succeeded 标志,因此我们可以定义我们的行为,既用于取消构建,也用于最终的构建后操作。

protected void onBuildFinished(object sender, BuildFinishedEventArgs e)
{
    if(!e.Succeeded) {
        library.Event.onCancel(); // Cancel-Build if it is
    }
    library.Event.onPost((e.Succeeded)? 1 : 0, 0, 0); // Post-Build, even if was the Cancel-Build before (like in VS)
}

# Sln-Opened/Closed (解决方案打开/关闭)

最后,对于基本事件,我们还有 Sln-Opened/Closed 事件类型,作为 Visual Studio 中以下事件的等价物(这是另一个故事,例如 这里 及相关内容)。

int OnAfterOpenSolution(object pUnkReserved, int fNewSolution)
int OnAfterCloseSolution(object pUnkReserved)

这与上面提到的 Post/Cancel-Build 事件类型一样简单。主要适用:void Initialize(IEventSource evt) & void Shutdown()

public override void Shutdown()
{
    library.Event.solutionClosed(pUnkReserved);
    detachCoreCommandListener(library);
}

总的来说,现在我们可以访问所有传入的目标以及所有必需的属性,因此我们可以扩展所需事件的范围。

就是这样,现在您可以开始创建插件的主逻辑,见下文;

# 我们已准备好创建插件。结果和可能的场景

现在您拥有了创建简单或复杂 MSBuild 插件所需的一切。

现在,我们不再仅仅是日志记录器,我们是强大的插件,因为我们现在可以做很多事情:

  • 处理所有可用项目的所有 MSBuild 属性。
  • 复杂处理 MSBuild 引擎。
    • 允许在运行时进行新的属性评估。
    • 支持复杂的属性函数和处理可用属性。
  • 针对特定项目或整个解决方案的处理器,支持所有基本事件及更多。
  • 部分管理构建线程并重新定义它们。
    • 从终止到准备额外的 BuildRequestData 等。

作为基本示例,我们考虑了包装器 https://github.com/3F/vsSolutionBuildEvent/tree/master/CI.MSBuild

它只是事件/操作到主插件的翻译器,并且通过外部逻辑,我们还可以编写额外的用户脚本在运行时,根据我们的需要,特别是影响构建过程。

$(buildNumber = $([MSBuild]::Add($(buildNumber), 1)))
...
#[var tStart    = $([System.DateTime]::Parse("2015/12/02").ToBinary())]
#[var tNow      = $([System.DateTime]::UtcNow.Ticks)]
#[var revBuild  = $([System.TimeSpan]::FromTicks($([MSBuild]::Subtract($(tNow), $(tStart)))).TotalMinutes.ToString("0"))]

等等。

现在我们的版本控制(例如)仅仅是技术和偏好的问题。从最简单的构建编号控制到使用复杂的脚本引擎,例如上面插件的示例,该插件为 Visual Studio、MSBuild 工具和其他工具提供了统一的事件处理器。

[最重要的是],所有这些都可以无需任何额外的操作或更改任何项目文件即可工作,而这些项目文件可能完全未知。一切都非常简单和自动化。

> msbuild.exe "SolutionFile.sln" /l:"YourPlugin.dll"

您已准备好征服世界 -_*

# 参考资料

# 源代码

# 注意事项

本文在没有特殊编辑的情况下撰写,即“按原样”发布,因此如果您发现错误或有任何疑问(相关:)),请在此处留言,我会尽力修复并根据我的时间详细解释,如果需要的话……

感谢所有读到最后的人,我希望它至少有帮助或有趣 -_*

历史

  • 2016 年 4 月 13 日:初始版本
© . All rights reserved.