zoukankan      html  css  js  c++  java
  • [独孤九剑]持续集成实践(二)– MSBuild语法入门

    本系列文章包含:

    [独孤九剑]持续集成实践(一)- 引子

    [独孤九剑]持续集成实践(二)– MSBuild语法入门

    [独孤九剑]持续集成实践(三)- Jenkins安装与配置(Jenkins+MSBuild+GitHub)

    1、开始                                                                                                                       

    在这篇文章中,我们会从头开始,一步步完成一个属于我们自己的MSBuild脚本。在它完成以后,我们只需要一个命令就可以删除之前的构建产物,构建.NET应用,运行单元测试。后面我们还会配一个Jenkins Job,让它从代码库中更新代码,执行MSBuild脚本。最后还会配另一个Jenkins Job,让它监听第一个Job的结果,当第一步成功以后,它会把相关的构建产物复制出来,放到web服务器里启动运行。

    我们用一个ASP.NET MVC 3应用做例子,在VS里面创建ASP.NET MVC 3应用并选择“application”模版就行。我们还要用一个单元测试项目来跑测试。代码可以在这里下载。【由于我的机器环境无法跑通他给的例子,因此我简单的创建了另一个webForm项目用于测试,如果你同样无法跑起来HelloCI这个项目,并且懒癌严重,请点击这里下载我的代码】

    2、你好,MSBuild                                                                                                          

    MSBuild是在.NET 2.0中引入的针对Visual Studio的构建系统。它可以执行构建脚本,完成各种Task──最主要的是把.NET项目编译成可执行文件或者DLL。从技术角度来说,制作EXE或者DLL的重要工作是由编译器(csc,vbc等等)完成的。MSBuild会从内部调用编译器,并完成其他必要的工作(例如拷贝引用──CopyLocal,执行构建前后的准备及清理工作等)。

    这些工作都是MSBuild执行脚本中的Task完成的。MSBuild脚本就是XML文件,根元素是Project,使用MSBuild自己的命名空间。

    MSBuild文件都要有Target。Target由Task组成,MSBuild运行这些Task,完成一个完整的目标。Target中可以不包含Task,但是所有的Target都要有名字。

    下面来一起创建一个“Hello World”的MSBuild脚本,先保证配置正确。我建议用VS来写,因为它可以提供IntelliSense支持,不过用文本编辑器也无所谓,因为只是写个XML文件,IntelliSense的用处也不是很大。先创建一个XML文件,命名为“basics.msbuild”,这个扩展名只是个约定而已,好让我们容易认出这是个MSBuild脚本,你倒不用非写这样的扩展名。给文件添加一个Project元素作为根元素,把 http://schemas.microsoft.com/developer/msbuild/2003设置成命名空间,如下所示

    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    </Project>

    下一步,给Project元素添加一个Target元素,起名叫“EchoGreeting”

    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
        <Target Name="EchoGreeting" />
    </Project>

    这就行了。我们已经有了一个可以运行的MSBuild脚本。它虽然还啥事都没干,但我们可以用它来验证当前环境是不是可以运行MSBuild脚本。

    在运行脚本的时候,我们要用到.NET框架安装路径下的MSBuild可执行文件。打开命令行,执行“MSBuild /nologo /version”命令,看看.NET框架安装路径是不是放到了PATH环境变量里面。如果一切正确,你应该能看到屏幕上打印出MSBuild的当前版本。倘若没有的话,或者把.NET框架安装路径放到PATH里面去,或者直接用Visual Studio Command Prompt,它已经把该配的都配好了。【我的Path里没有,所以要配置PATH环境变量,机器是Win7 x64的,VS2013,MSBuild.exe文件的Bin目录位置在C:Program Files (x86)MSBuild12.0Bin】

    进入存放刚才那个脚本的目录后,以文件名当作参数调用MSBuild,就可以执行脚本了。在我的机器上可以看到下面的执行结果:

    C:>msbuild basics.msbuild
    
    Microsoft (R) Build Engine Version 4.0.30319.1
    [Microsoft .NET Framework, Version 4.0.30319.269]
    Copyright (C) Microsoft Corporation 2007. All rights reserved.
    Build started 8/2/2012 5:59:45 AM.
    
    Build succeeded.
    
    0 Warning(s)
    0 Error(s)
    
    Time Elapsed 00:00:00.03

    执行完脚本以后,MSBuild会首先显示一个启动界面和版权信息(用 /nologo 开关可以隐藏掉它们)。接下来会显示一个启动时间,然后便是真正的构建过程。因为咱们的脚本啥都没干,所以构建就直接成功了。总计用时也会显示在界面上。下面咱们来给EchoGreeting Target添加一个Task,让脚本真的干点事。【以下内容在练习是一定要注意拼写错误,不要问我为什么。。。】

    <Target Name="EchoGreeting">
        <Exec Command="echo Hello from MSBuild" />
    </Target>

    现在EchoGreeting Target有了一个Exec Task,它会执行Command属性中定义的任何命令【Command里的命令应该都是批处理命令】。再运行一次脚本,你应该能看到更多信息了。在大多数时候,MSBuild的输出信息都很长,你可以用 /verbosity 开关来只显示必要信息【使用MSBuild /help可查询所有命令参数】。不过无论怎样,MSBuild都会把我们的文字显示到屏幕上。下面再添加一个Target。

    <Target Name="EchoDate">
        <Exec Command="echo %25date%25" />
    </Target>

    这个Target会输出当前日期。它的命令要做的事情就是“echo %25date%25”,但是“%”字符在MSBuild中有特殊含义,所以这个命令需要被转义。当遇到转义字符的时候,“%”后面的十进制字符会被转成对应的ASCII码。MSBuild只会执行Project元素中的第一个Target。要执行其他Target的时候,需要把/target开关(可简写为 /t)加上Target名称传给MSBuild。你也可以指定MSBuild执行多个Target,只要用分号分割Target名字就可以。

    C:>msbuild basics.msbuild /nologo /verbosity:minimal /t:EchoGreeting;EchoDate
    Hello from MSBuild
    Thu 08/02/2012

    3、更实用的构建脚本                                                                                                         

    演示就先到这里。下面来用MSBuild来构建一个真实项目。首先把示例代码下载下来,或是自己创建一个ASP.NET应用。给它添加一个MSBuild脚本,以solution或project名字给脚本命名,扩展名用“.msbuild”。照先前一样指定MSBuild命名空间。

    开始写脚本之前,先把脚本要干的事情列出来:

    1. 创建BuildArtifacts目录

    2. 构建solution,把构建产物(DLL,EXE,静态内容等等)放到BuildArtifacts目录下。

    3. 运行单元测试。

    因为示例应用叫做HelloCI,于是这个脚本也就命名为HelloCI.msbuild。先添加命名空间,然后就可以添加第一个Target了,我管它叫做Init。

    <Target Name="Init">
        <MakeDir Directories="BuildArtifacts" />
    </Target>

    这个Target会调用MakeDir Task创建一个新的目录,名叫BuildArtifacts,跟脚本在同一目录下。运行脚本,你会发现该目录被成功创建。如果再次运行,MSBuild就会跳过这个Task,因为同名目录已经存在了。

    接下来写一个Clean Target,它负责删除BuildArtifacts目录和里面的文件。

    <Target Name="Clean">
        <RemoveDir Directories="BuildArtifacts" />
    </Target>

    理解了Init之后,这段脚本就应该很好懂了。试着执行一下,BuildArtifacts目录应该就被删掉了。下面再来把代码中的重复干掉。在Init和Clean两个Target里面,我们都把BuildArtifacts的目录名硬编码到代码里面了,如果未来要修改这个名字的话,就得同时改两个地方。这里可以利用Item或Property避免这种问题。

    Item和Property只有些许差别。Property由简单的键值对构成,在脚本执行的时候还可以用 /property 赋值。Item更强大一些,它可以用来存储更复杂的数据。我们这里不用任何复杂数据,但需要用Items获取额外的元信息,例如文件全路径。

    接下来修改一下脚本,用一个Item存放路径名,然后修改Init和Clean,让它们引用这个Item。

    <ItemGroup>
        <BuildArtifactsDir Include="BuildArtifacts" />
    </ItemGroup>
    
    <Target Name="Init">
        <MakeDir Directories="@(BuildArtifactsDir)" />
    </Target>
    <Target Name="Clean">
        <RemoveDir Directories="@(BuildArtifactsDir)" />
    </Target>

    Item是在ItemGroup里面定义的。在一个Project中可以有多个ItemGroup元素,用来把有关系的Item分组。这个功能在Item较多的时候特别有用。我们在ItemGroup里定义了BuildArtifactsDir元素,并用Include属性指定BuildArtifacts目录。记得BuildArtifacts目录后面要有个斜杠。最后,我们用了@(ItemName)语法在Target里面引用这个目录。现在如果要修改目录名的话,只需要改BuildArtifactsDir的Include属性就好了。

    接下来还有个问题要处理。在BuildArtifacts目录已经存在的情况下,Init是什么事都不干的。也是就说,在调用Init的时候磁盘上的已有文件还会被保留下来。这一点着实不妥,如果能每次调用Init的时候,都把目录和目录里面的所有文件都一起删掉再重新创建,就能保证后续环节都在干净的环境下执行了。我们固然可以在每次调用Init的时候先手工调一下Clean,但给Init Target加一个DependsOnTargets属性会更简单,这个属性会告诉MSBuild,每次执行Init的时候都先执行Clean。

    <Target Name="Init" DependsOnTargets="Clean">
        <MakeDir Directories="@(BuildArtifactsDir)" />
    </Target>

    现在MSBuild会帮我们在调Init之前先调Clean了。跟DependsOnTargets这个属性所暗示的一样,一个Target可以依赖于多个Target,之间用分号分割就行

    接下来我们要编译应用程序,把编译后的结果放到BuildArtifacts目录下。先写一个Compile Target,让它依赖于Init。这个Target会调用另一个MSBuild实例来编译应用。我们把BuildArtifacts目录传进去,作为编译结果的输出目录。

    <ItemGroup>
        <BuildArtifactsDir Include="BuildArtifacts" />
        <SolutionFile Include="HelloCI.sln" />
    </ItemGroup>
    
    <PropertyGroup>
        <Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
        <BuildPlatform Condition=" '$(BuildPlatform)' == '' ">Any CPU</BuildPlatform>
    </PropertyGroup>
    
    <Target Name="Compile" DependsOnTargets="Init">
        <MSBuild Projects="@(SolutionFile)" Targets="Rebuild" Properties="OutDir=%(BuildArtifactsDir.FullPath);Configuration=$(Configuration);Platform=$(BuildPlatform)" />
    </Target>

    上面的脚本做了几件事情。

    首先,ItemGroup添加了另一个Item,叫做SolutionFile,它指向solution文件。在构建脚本中用Item或Property代替硬编码,这算的是一个优秀实践吧。

    其次,我们创建了一个PropertyGroup,里面包含两个Property:Configuration和BuildPlatform。它们的值分别是“Release”和“Any CPU”。当然,Property也可以在运行时通过/property(简写为/p)赋值。我们还用了Condition属性,它在这里的含义是,只有当这两个属性没有值的情况下,才用我们定义的数据给它们赋值。这段代码实际上就是给它们一个默认值。

    接下来就是Compile Target了,它依赖于Init,里面内嵌了一个MSBuild Task。它在运行的时候会调用另外一个MSBuild实例。在脚本中定义了这个被内嵌的MSBuild Task要操作的项目。在这里,我们既可以传入另外一个MSBuild脚本,也可以传入.csproj文件(它本身也是个MSBuild脚本)。但我们选择了传入HelloCI应用的solution文件。Solution文件不是MSBuild脚本,但是MSBuild可以解析它。脚本中还指定了内嵌的MSBuild Task要执行的Target名称:“Rebuild”,这个Target已经被导入到solution的.csproj文件中了。最后,我们给内嵌的Task传入了三个Property。

    OutDir 编译结果的输出目录
    Configuration 构建(调试、发布等)时要使用的配置
    Platform 编译所用的平台(x86、x64等)

    给上面这三个Property赋值用的就是先前定义的Item和Property。OutDir Property用的是BuildArtifacts目录的全路径。这里用了%(Item.MetaData) 语法。这个语法应该看起来很眼熟吧?就跟访问C#对象属性的语法一样。MSBuild创建出来的任何Item,都提供了某些元数据以供访问,例如FullPath和ModifiedTime。但这些元数据有时候也没啥大用,因为Item不一定是文件。

    Configuration和Platform用到了先前定义好的Property,语法格式是$(PropertyName)。在这里可以看到系统保留的一些属性名,用户不能更改。定义Property的时候请不要用它们。

    这里还有些东西值得提一下。用了Property以后,我们可以在不更改构建脚本的情况下使用不同的Configuration或者BuildPlatform,只要在运行的时候用 /property 传值进去就行。所以“msbuild HelloCI.msbuild /t:Compile /p:Configuration:Debug”这个命令会用Debug配置构建项目,而“msbuild HelloCI.msbuild /t:Compile /p:Configuration:Test;BuildPlatform:x86”会在x86平台下使用Test配置。

    现在运行Compile,就可以编译solution下的两个项目,把编译结果放到BuildArtifacts目录下。在完成构建脚本之前,只剩下最后一个Target了:

    <ItemGroup>
        <BuildArtifacts Include="BuildArtifacts" />
        <SolutionFile Include="HelloCI.sln" />
        <NUnitConsole Include="C:Program Files (x86)NUnit 2.6in
    unit-console.exe" />
        <UnitTestsDLL Include="BuildArtifactsHelloCI.Web.UnitTests.dll" />
        <TestResultsPath Include="BuildArtifactsTestResults.xml" />
    </ItemGroup>
    <Target Name="RunUnitTests" DependsOnTargets="Compile">
        <Exec Command='"@(NUnitConsole)" @(UnitTestsDLL) /xml=@(TestResultsPath)' />
    </Target>

    ItemGroup里现在又多了三个Item:

    NUnitConsole指向NUnit控制台运行器(console runner);

    UnitTestDLL指向单元测试项目生成的DLL文件;

    TestResultsPath是要传给NUnit的,这样测试结果就会放到BuildArtifacts目录下。

    RunUnitTests Target用到了Exec Task。如果有一个测试运行失败,NUnit控制台运行器会返回一个非0的结果。这个返回值会告诉MSBuild有个地方出错了,于是整个构建的状态就是失败【这里提一下,单元测试之类的第三方类库要保证一直在项目中,否则在上传至版本管理服务器上时,可能会被忽略导致后面的执行测试期间出现问题】

    现在这个脚本比较完善了,用一个命令就可以删除旧的构建产物、编译、运行单元测试:

    C:HelloCI> msbuild HelloCI.msbuild /t:RunUnitTests

    我们还可以给脚本设一个默认Target,就省得某次都要指定了。在Project元素上加一个DefaultTargets属性,让RunUnitTests成为默认Target。

    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="RunUnitTests">

    你还可以创建自己的Task。这里有个例子,AsyncExec【我是没打开,可能被Q了】,它允许人们以异步的方式执行命令。比如有个Target用来启动Web服务器,要是用Exec命令的话,整个构建都会停住,直到服务器关闭。用AsyncExec这个命令可以让构建继续执行,不用等待命令执行结束。

    本文的完整脚本可以在这里下载【或者下载我的】。

    在接下来的文章中,我会讲述如何配置Jenkins。我们不再需要手动运行命令来构建整个项目,Jenkins会检测代码库,一旦有更新就会自动触发构建。

    参考:

    用MSBuild和Jenkins搭建持续集成环境

  • 相关阅读:
    85. Maximal Rectangle
    120. Triangle
    72. Edit Distance
    39. Combination Sum
    44. Wildcard Matching
    138. Copy List with Random Pointer
    91. Decode Ways
    142. Linked List Cycle II
    异或的性质及应用
    64. Minimum Path Sum
  • 原文地址:https://www.cnblogs.com/cloud915/p/4724673.html
Copyright © 2011-2022 走看看