代码之家  ›  专栏  ›  技术社区  ›  Mark Booth

在Visual Studio C项目中自动嵌入Mercurial修订信息

  •  54
  • Mark Booth  · 技术社区  · 14 年前

    原始问题

    在构建项目时,我希望将每个存储库的mercurial id嵌入到该存储库(库、应用程序或测试应用程序)的产品中。

    我发现,如果您准确知道构建客户使用的应用程序的特定版本的过程,那么在8个时区之外调试客户运行的应用程序就容易得多。因此,系统中的每个项目(应用程序或库)都实现了获取相关修订信息的方法。

    我还发现,能够查看应用程序是否是用存储库中干净(未修改)的变更集编译的,这非常有用。”hg id'在存储库中有未提交的更改时,有效地将a+附加到变更集id,这样我们可以很容易地看到人们是否在运行一个干净的或修改过的代码版本。

    我当前的解决方案在下面详细介绍,并满足基本的需求,但是它有很多问题。

    当前解决方案

    目前,对于每个Visual Studio解决方案,我都添加了以下“预生成事件命令行”命令:

    cd $(ProjectDir)
    HgID
    

    我还将hgid.bat文件添加到项目目录:

    @echo off
    type HgId.pre > HgId.cs
    
    For /F "delims=" %%a in ('hg id') Do <nul >>HgID.cs set /p =            @"%%a"
    
    echo ;                  >> HgId.cs
    echo     }              >> HgId.cs
    echo }                  >> HgId.cs
    

    以及一个hgid.pre文件,定义如下:

    namespace My.Namespace {
    /// <summary> Auto generated Mercurial ID class. </summary>
    internal class HgID {
        /// <summary> Mercurial version ID [+ is modified] [Named branch]</summary>
        public const string Version =
    

    当我构建应用程序时,所有库上都会触发预构建事件,创建一个新的hg id.cs文件(不受修订控制),并使用“version”中的新“hg id”字符串重新编译库。

    当前解决方案的问题

    主要的问题是,由于hgid.cs在每次预构建时都会被重新创建,所以每次我们需要编译任何东西时,当前解决方案中的所有项目都会被重新编译。由于我们希望能够轻松地调试到库中,所以在我们的主要应用程序解决方案中通常会引用许多库。这会导致构建时间明显长于我想要的时间。

    理想情况下,我希望库仅在hgid.cs文件的内容实际发生更改时进行编译,而不是使用完全相同的内容重新创建。

    该方法的第二个问题是它依赖于Windows外壳的特定行为。我已经多次修改了批处理文件,因为原来的版本在XP下工作,而不是在Vista下工作,下一个版本在Vista下工作,但不是在XP下工作,最后我设法使它与两个版本都工作。然而,不管怎样,它是否能与Windows7一起工作都是人们的猜测,随着时间的推移,我认为承包商更有可能期望能够在他们的Windows7盒子上构建我们的应用程序。

    最后,我对这个解决方案有一个美学问题:批处理文件和bodgethetogettemplate文件 感觉 喜欢错误的方式。

    我的实际问题

    你将如何解决/你将如何解决我要解决的问题?

    还有什么比我现在做的更好的选择呢?

    拒绝解决这些问题

    在实现当前的解决方案之前,我研究了Mercurials关键字扩展,因为它似乎是显而易见的解决方案。然而,我看得越多,阅读人们的意见,我就越得出结论,认为这不是正确的事情。

    我还记得在以前公司的项目中,关键词替换给我带来的问题(只是一想到必须再次使用源代码安全,我就有一种恐惧感*8”)。

    另外,我并不特别想启用Mercurial扩展来完成构建。我希望解决方案是自包含的,这样就不容易在没有嵌入版本信息的情况下意外编译应用程序,因为没有启用扩展或没有安装正确的助手软件。

    我还想用一种更好的脚本语言来编写它,在这种语言中我只写hgid.cs文件。 如果 内容实际上已经改变了,但我所能想到的所有选择都要求我的同事、承包商和可能的客户必须安装他们不想安装的软件(例如Cygwin)。

    人们能想到的任何其他选择都会受到赞赏。


    更新

    部分解

    玩了一段时间后,我设法让hgid.bat文件仅在hgid.cs文件更改时覆盖它:

    @echo off
    
    type HgId.pre > HgId.cst
    
    For /F "delims=" %%a in ('hg id') Do <nul >>HgId.cst set /p =            @"%%a"
    
    echo ;                  >> HgId.cst
    echo     }              >> HgId.cst
    echo }                  >> HgId.cst
    
    fc HgId.cs HgId.cst >NUL
    if %errorlevel%==0 goto :ok
    copy HgId.cst HgId.cs
    :ok
    del HgId.cst
    

    此解决方案的问题

    尽管hgid.cs不再每次都被重新创建,但Visual Studio仍然坚持每次都编译所有内容。我试过寻找解决方案,并试着在工具选项项目和解决方案构建和运行中检查“仅构建启动项目和运行依赖项”,但没有什么区别。

    第二个问题仍然存在,现在我没有办法测试它是否能与Vista一起工作,因为那个承包商不再与我们合作。

    • 如果有人能在Windows7和/或Vista设备上测试这个批处理文件,我会很高兴听到它是如何运行的。

    最后,我对这个解决方案的美学问题比以前更为严重,因为批处理文件更复杂,而且现在出错的地方更多。

    如果你能想出更好的解决办法,我很想听听。

    7 回复  |  直到 12 年前
        1
  •  16
  •   Jon Adams    12 年前

    我想我有个答案。这会有点复杂,但它可以让您不必做任何批处理文件。您可以依赖msbuild和自定义任务来完成此操作。我使用了msbuild的扩展包(可在 CodePlex )-但是你需要的第二个任务是你可以很容易地写自己的东西。

    使用此解决方案,您可以右键单击该dll,并在文件属性中查看该dll(或exe)来自哪个Mercurial版本。

    步骤如下:

    1. 获取 MBBuildExtension Pack 写入自定义任务以覆盖 汇编信息.cs
    2. 创建自定义 在自己的项目中生成任务以获取 Mercurial ID(代码如下)。
    3. 编辑需要 使用自定义任务的mercurial id (以下代码)。

    获取Mercurial ID的自定义任务: (这需要很好地进行测试,也许更好地概括…)

    using System;
    using System.Diagnostics;
    using Microsoft.Build.Utilities;
    using Microsoft.Build.Framework;
    
    
    namespace BuildTasks
    {
        public class GetMercurialVersionNumber : Task
        {
            public override bool Execute()
            {
                bool bSuccess = true;
                try
                {
                    GetMercurialVersion();
                    Log.LogMessage(MessageImportance.High, "Build's Mercurial Id is {0}", MercurialId);
                }
                catch (Exception ex)
                {
                    Log.LogMessage(MessageImportance.High, "Could not retrieve or convert Mercurial Id. {0}\n{1}", ex.Message, ex.StackTrace);
                    Log.LogErrorFromException(ex);
                    bSuccess = false;
                }
                return bSuccess;
            }
    
            [Output]
            public string MercurialId { get; set; }
    
            [Required]
            public string DirectoryPath { get; set; }
    
            private void GetMercurialVersion()
            {
                Process p = new Process();
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.RedirectStandardOutput = true;
                p.StartInfo.RedirectStandardError = true;
                p.StartInfo.CreateNoWindow = true;
                p.StartInfo.WorkingDirectory = DirectoryPath;
                p.StartInfo.FileName = "hg";
                p.StartInfo.Arguments = "id";
                p.Start();
    
                string output = p.StandardOutput.ReadToEnd().Trim();
                Log.LogMessage(MessageImportance.Normal, "Standard Output: " + output);
    
                string error = p.StandardError.ReadToEnd().Trim();
                Log.LogMessage(MessageImportance.Normal, "Standard Error: " + error);
    
                p.WaitForExit();
    
                Log.LogMessage(MessageImportance.Normal, "Retrieving Mercurial Version Number");
                Log.LogMessage(MessageImportance.Normal, output);
    
                Log.LogMessage(MessageImportance.Normal, "DirectoryPath is {0}", DirectoryPath);
                MercurialId = output;
    
            }
        }
    

    修改后的项目文件: (评论可能有帮助)

    <?xml version="1.0" encoding="utf-8"?>
    <Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <!--this is the import tag for the MSBuild Extension pack. See their documentation for installation instructions.-->
      <Import Project="C:\Program Files (x86)\MSBuild\ExtensionPack\MSBuild.ExtensionPack.tasks" />
      <!--Below is the required UsingTask tag that brings in our custom task.-->
      <UsingTask TaskName="BuildTasks.GetMercurialVersionNumber" 
                 AssemblyFile="C:\Users\mpld81\Documents\Visual Studio 2008\Projects\LambaCrashCourseProject\BuildTasks\bin\Debug\BuildTasks.dll" />
      <PropertyGroup>
        <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
        <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
        <ProductVersion>9.0.30729</ProductVersion>
        <SchemaVersion>2.0</SchemaVersion>
        <ProjectGuid>{D4BA6C24-EA27-474A-8444-4869D33C22A9}</ProjectGuid>
        <OutputType>Library</OutputType>
        <AppDesignerFolder>Properties</AppDesignerFolder>
        <RootNamespace>LibraryUnderHg</RootNamespace>
        <AssemblyName>LibraryUnderHg</AssemblyName>
        <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
        <FileAlignment>512</FileAlignment>
      </PropertyGroup>
      <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
        <DebugSymbols>true</DebugSymbols>
        <DebugType>full</DebugType>
        <Optimize>false</Optimize>
        <OutputPath>bin\Debug\</OutputPath>
        <DefineConstants>DEBUG;TRACE</DefineConstants>
        <ErrorReport>prompt</ErrorReport>
        <WarningLevel>4</WarningLevel>
      </PropertyGroup>
      <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
        <DebugType>pdbonly</DebugType>
        <Optimize>true</Optimize>
        <OutputPath>bin\Release\</OutputPath>
        <DefineConstants>TRACE</DefineConstants>
        <ErrorReport>prompt</ErrorReport>
        <WarningLevel>4</WarningLevel>
      </PropertyGroup>
      <ItemGroup>
        <Reference Include="System" />
        <Reference Include="System.Core">
          <RequiredTargetFramework>3.5</RequiredTargetFramework>
        </Reference>
        <Reference Include="System.Xml.Linq">
          <RequiredTargetFramework>3.5</RequiredTargetFramework>
        </Reference>
        <Reference Include="System.Data.DataSetExtensions">
          <RequiredTargetFramework>3.5</RequiredTargetFramework>
        </Reference>
        <Reference Include="System.Data" />
        <Reference Include="System.Xml" />
      </ItemGroup>
      <ItemGroup>
        <Compile Include="Class1.cs" />
        <Compile Include="Properties\AssemblyInfo.cs" />
      </ItemGroup>
      <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
    
      <Target Name="Build" DependsOnTargets="BeforeBuild">
        <!--This Item group is a list of configuration files to affect with the change. In this case, just this project's.-->
        <ItemGroup>
          <AssemblyInfoFiles Include="$(MSBuildProjectDirectory)\Properties\AssemblyInfo.cs" />
        </ItemGroup>
        <!--Need the extension pack to do this. I've put the Mercurial Id in the Product Name Attribute on the Assembly.-->
        <MSBuild.ExtensionPack.Framework.AssemblyInfo AssemblyInfoFiles="@(AssemblyInfoFiles)"
                                                      AssemblyProduct="Hg: $(MercurialId)"
                                                      />
        <!--This is here as an example of messaging you can use to debug while you are setting yours up.-->
        <Message Text="In Default Target, File Path is: @(AssemblyInfoFiles)" Importance="normal" />
      </Target>  
    
      <Target Name="BeforeBuild">
        <!--This is the custom build task. The Required Property in the task is set using the property name (DirectoryPath)-->
        <BuildTasks.GetMercurialVersionNumber DirectoryPath="$(MSBuildProjectDirectory)">
          <!--This captures the output by reading the task's MercurialId Property and assigning it to a local
              MSBuild Property called MercurialId - this is reference in the Build Target above.-->
          <Output TaskParameter="MercurialId" PropertyName="MercurialId" />
        </BuildTasks.GetMercurialVersionNumber>
      </Target>
      <!--<Target Name="AfterBuild">
      </Target>-->
    
    </Project>
    

    最后一个注意事项:构建任务项目只需要构建一次。不要每次执行其余的解决方案时都尝试构建它。如果这样做,您将发现VS2008已锁定了DLL。还没弄清楚,但我认为最好的办法是按你想要的方式构建DLL,然后只将它与代码一起分发,确保DLL的位置相对于你需要在其中使用它的每个项目都是固定的。这样,就不用安装任何东西了。

    祝你好运,希望这对你有所帮助!

    奥迪

        2
  •  25
  •   Joe Daley    14 年前

    我刚刚发布了一个小型的开源msbuild任务,以完全满足您的需要:

    • 它将您的Mercurial版本号放入.NET程序集版本中。
    • 您可以从版本中判断程序集是否已使用未提交的更改进行编译
    • 如果修订未更改,则不会导致不必要的生成
    • 不依赖于Windows脚本
    • 无需安装-只需向解决方案中添加一个小dll,然后编辑项目中的一些文件。

    http://versioning.codeplex.com

        3
  •  5
  •   dthorpe    14 年前

    是否考虑使用字符串资源而不是C语言字符串常量?字符串资源可以在生成后的输出二进制文件中使用用于本地化的工具进行编辑/替换。

    您将把您的mercurial版本号发送到C build未使用的文本文件,然后使用后期生成操作将版本资源替换为所发送文本文件的实际值。如果对程序集进行强名称签名,则需要在签名之前进行资源字符串替换。

    这就是我们多年前在Borland为Windows产品处理这个问题的方式。从那时起,世界变得更加复杂,但这一原则仍然适用。

        4
  •  2
  •   kovica    14 年前

    我是我的。我有这个:

    [extensions]                                                                                                                                                               
    hgext.keyword =                                                                                                                                                            
    hgext.hgk =                                                                                                                                                                
    
    [keyword]                                                                                                                                                                  
    ** =                                                                                                                                                                       
    
    [keywordmaps]                                                                                                                                                              
    Id = {file|basename},v {node|short} {date|utcdate} {author|user} 
    

    在我的源文件(Java)中,我这样做:

    public static final String _rev = "$Id$";
    

    在提交$id$之后,它被扩展为:

    public static final String _rev = "$Id: Communication.java,v 96b741d87d07 2010/01/25 10:25:30 mig $";
    
        5
  •  2
  •   krystan honour    14 年前

    我们用另一个源代码控制系统Subversion解决了这个问题。

    我们要做的是,我们有一个commonassemblyinfo.cs文件,并使用一个msbuild脚本将SVN修订号插入该文件。

    我们直接调用svninfo并从revision:part中获取输出,然后将其插入commonassemblyinfo.cs文件。

    我们解决方案中的每个项目都有一个到公共文件的链接,然后进行编译,这意味着程序集在编译时使用SVN版本号进行版本控制,也意味着我们编写的所有依赖库也都进行了版本控制。

    我们很容易使用CruiseControl.net和msbuild文件实现这一点。

    我没有使用mercurial,但我相信您可以用id命令做类似的事情? 但是由于输出的格式不同,您需要了解如何使用它。

    我将有一个标准的XML文件,在应用启动时读取,您可以将信息插入其中(也适用于resx文件)。然后您可以在代码中读取该值。有点恶心,但很管用。

    我最喜欢的解决方案是我们这样做,但是你不能在Mercurial中这样做,因为变更集信息不是像SVN版本号那样纯数字的。

        6
  •  1
  •   Morten Mertner    14 年前

    这个问题似乎有多种可能的解决方法。第一个可能也是首选的解决方案是安装一个构建服务器,并且只将在那里生成的构建分发给客户。这样做的好处是,您永远不会发送未提交的更改。通过使用msbuild、nant或其他基于任务的构建工具,整个过程非常灵活。我可以安装TeamCity,只需很少的努力就可以完成最初的两个构建和运行,但是也有其他好的构建服务器。这真的应该是你的解决方案。

    如果出于某种原因,您坚持认为可以将开发人员构建分发给客户机;)那么您需要一个本地解决方案。

    一个相当简单的解决方案是使用内置支持自动增加程序集的内部版本号:

    // major.minor.build.revision
    [assembly:AssemblyVersion("1.2.*")]
    

    这个 * 使内部版本号在每次编译时自动递增(并且会发生更改)。修订号是一个随机数。从这里,您可以通过保存这两条信息来跟踪与Mercurial ID的关联,例如将其发布到某个内部Web解决方案或满足您特定需求的任何内容,或者更新生成的程序集。我怀疑你可以使用Postharp或Mono。Cecil会重写程序集,例如,通过将修订号修补为ID。如果你的程序集已签名,则需要在签名之前进行重写,如果你没有生成文件,这有点麻烦。请注意,可以将vs配置为使用自定义生成文件而不是默认的生成过程进行编译。

    我最后的建议是只为hg id创建一个单独的项目,并使用后期构建步骤将生成的程序集合并为一个。ILmerge支持对已签名的程序集进行重新签名,因此这可能更容易工作。缺点是不允许重新分配ilmerge(尽管商业用途是)。

    这不是一个解决方案,但希望能激励你前进。

        7
  •  1
  •   Roman Starkov    14 年前

    我们在这里做的是:我们不会每次在开发人员的机器上构建项目时都嵌入修订。这最多会导致上面提到的问题,最糟糕的是,这会误导您,因为您可能已经修改了工作区中的文件。

    相反,我们只在项目构建时嵌入源代码管理修订号。 TeamCity 在单独的生成服务器上。修订嵌入到两者中 AssemblyVersion AssemblyFileVersion 将最后两部分分开。

    默认情况下,版本以9999.9999结尾,我们将修订拆分为12345678版本将变为1234.5678版本(而不是说我们的版本接近1200万版本…)。

    此过程确保版本为2.3.1.1256的产品 一定地 修订版11256的原始版本。开发人员手工构建的任何东西都会变成这样:2.3.9999.9999。

    由于我们不使用mercurial,因此上述实现方式的确切细节并不直接相关,而是简单地说:teamcity处理签出所需的修订版并将其编号传递给我们的msbuild脚本,然后在自定义任务的帮助下重写assemblyinfo.cs写的。

    我特别喜欢的是,在Visual Studio中按F5键绝对不会修改任何文件—事实上,就Visual Studio而言,它使用的是一个简单的老的常规解决方案。