【问题标题】:Embedding mercurial revision information in Visual Studio c# projects automatically在 Visual Studio c# 项目中自动嵌入 mercurial 修订信息
【发布时间】:2011-01-24 02:14:12
【问题描述】:

原来的问题

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

我发现,如果您确切地知道构建客户正在使用的特定版本的应用程序的内容,那么调试由 8 个时区以外的客户运行的应用程序会变得非常容易。因此,我们系统中的每个项目(应用程序或库)都实现了一种获取相关修订信息的方法。

我还发现能够查看应用程序是否已使用存储库中的干净(未修改)变更集进行编译非常有用。当存储库中有未提交的更改时,'Hg id' 有用地将 + 附加到更改集 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 =

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

当前解决方案的问题

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

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

此方法的第二个问题是它依赖于 windows shell 的特定行为。我已经不得不多次修改批处理文件,因为原始版本在 XP 下工作而不是 Vista,下一个版本在 Vista 但不是 XP 下工作,最后我设法让它同时工作。但它是否适用于 Windows 7 尚有待商榷,随着时间的推移,我认为承包商更有可能希望能够在他们的 Windows 7 boxen 上构建我们的应用程序。

最后,我对这个解决方案有一个审美问题,批处理文件和模板文件混在一起感觉好像是错误的做法。

我的实际问题

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

有什么比我目前正在做的更好的选择?

拒绝解决这些问题

在实施当前解决方案之前,我查看了 Mercurials 关键字扩展,因为它似乎是显而易见的解决方案。然而,我越看它和阅读人们的意见,我就越得出这样的结论:这样做是不对的。

我还记得在以前公司的项目中关键字替换给我带来的问题(只是想到必须再次使用 Source Safe 让我感到恐惧*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,因为那个承包商不再和我们在一起了。

  • 如果有人可以在 Windows 7 和/或 Vista 机器上测试此批处理文件,我将不胜感激。

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

如果您能想到任何更好的解决方案,我很想听听。

【问题讨论】:

  • @Mark Booth:我首先想到的是你的“更好的脚本语言”,但你明确地排除了这一点:(啊微软商店的乐趣生态系统和供应商锁定;)
  • 对不起,但我知道我的同事/客户是什么样的,并且任何使用当前 TortoiseHg+VS2008 安装无法完成的事情都不会成功。 *8'(
  • 就“更好的脚本”而言——我想到了两件事:1)powershell——a)是一种更好的脚本语言,b)它比喜欢cygwin; 2) 编写一个 .exe 实用程序(您可以将其包含在您的项目中作为控制)以使用附加逻辑执行与批处理文件相同的操作。
  • 您可以尝试 WSH 以获得更好的脚本。
  • Powershell 功能强大,但默认情况下不包含在 XP 中。 7中默认包含(不了解Vista)。

标签: c# visual-studio-2008 mercurial build


【解决方案1】:

我的 .hgrc 我有这个:

[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 $";

【讨论】:

  • 感谢 Kovica,但正如我所说,我拒绝使用关键字扩展,因为它太容易将字符串“$Id$”编译到可执行文件中。如果关键字扩展内置于 mercurial 而不是扩展中,并且默认情况下,我可能会考虑使用它,但就目前而言,它并不可靠。
  • A 认为没有更可靠的解决方案。如果有人意外损坏了 .hg 或不是从 clone 而是从 archive 构建的怎么办?
  • @Mr.Cat - 我认为没有比关键字扩展更可靠的解决方案了。在您没有明确启用扩展(或有人禁用它)的任何地方,您都会将文字字符串 "$ID$" 编译到目标文件中而不会抱怨。如果 mercurial 或 repo 损坏(不确定您的意思),无论如何您都需要先修复它。至于hg archive,如果您尝试从存档中构建它,我的原始解决方案将无法编译!这正是我想要的。我不希望任何源代码在没有受版本控制的情况下编译到我们的应用程序中!
【解决方案2】:

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

您可以将您的 mercurial 版本号发送到 C# 构建未使用的文本文件,然后使用构建后操作将版本资源替换为发出的文本文件中的实际值。如果您对程序集进行强名称签名,则需要在签名之前进行资源字符串替换。

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

【讨论】:

  • @dthorpe - 谢谢,这是一个有趣的想法。另外,如果这里有人知道如何进行本地化,我们也许可以考虑向我们在中国的主要客户提供我们软件的本地化版本。 *8')
  • @Mark 本地化实际上非常简单。我将自己写的一个小应用本地化成日文和德文,我只是一个业余爱好者。当然,对于大型、复杂和商业软件,这将是一件苦差事,但原理很简单。您只需从字符串资源中获取所有 UI 文本,然后为每种语言创建备用的本地化资源。
【解决方案3】:

似乎有多种可能的方法来解决这个问题。第一个也是可能首选的解决方案是安装构建服务器,并且只将那里生成的构建分发给客户。这样做的好处是您永远不会发布未提交的更改。通过使用 MSBuild、NAnt 或其他一些基于任务的构建工具,整个过程非常灵活。我能够轻松安装 TeamCity 并启动并运行前几个构建,但也有其他不错的构建服务器。这确实应该是您的解决方案。

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

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

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

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

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

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

【讨论】:

  • @Morten Mertner,AssemblyVersion 的问题在于它没有告诉您有关源代码的任何信息。当你有一个分支的开发模型时,它会变得更加复杂。如果有人将一个特定的库从(稳定)分支更改为(实验)分支,我希望能够在我的日志文件中看到该更改。使用“hg id”可以准确地告诉我哪些代码被编译到库中,以及它是否已被修改。然后我通常可以返回 DVCS,更新到该版本,重新编译并(希望)重现我的客户可能遇到的任何问题。
【解决方案4】:

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

我们所做的是我们有一个 commonassemblyinfo.cs 文件,并使用 msbuild 脚本将 svn 修订号插入其中。

我们实际上调用 svninfo 并从 revision: part 中抓取输出,然后将其插入 CommonAssemblyInfo.cs 文件。

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

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

我没有使用过 mercurial,但我相信你可以用 id 命令做类似的事情? 但由于输出格式的原因,您需要以不同的方式使用它。

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

我最喜欢的解决方案是我们这样做的方式,但你不能在 mercurial 中这样做,因为变更集信息不像 svn 修订号那样纯粹是数字。

【讨论】:

  • @krystan honour,是的,这几乎就是上述脚本所做的('hg id' 行嵌入在 HgId.bat 的 For 行中)。我最感兴趣的是他项目的所有组件部分的变更集 id,这样我就可以记录每个编译到应用程序中的库。将来,当我们开始使用 subrepos 来管理整个应用程序时,我们可能希望使用顶级 repo 的 changeset id 来定义实际的应用程序版本,但这还有一段路要走。
【解决方案5】:

我想我有一个答案给你。这将有点涉及,但它使您不必执行任何批处理文件。您可以依靠 MSBuild 和自定义任务为您执行此操作。我已经使用了 MSBuild 的扩展包(可在 CodePlex 获得) - 但您需要的第二个任务是您可以轻松编写自己的任务。

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

步骤如下:

  1. 获取MBBuildExtension Pack 编写自定义任务以覆盖 AssemblyInfo.cs
  2. 创建自定义 Build Task 在自己的项目中获取 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;

        }
    }

以及修改后的项目文件:(cmets 可能会有所帮助)

<?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,确保 dll 的位置相对于您需要使用的每个项目都是固定的它在。这样,没有人必须安装任何东西。

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

奥迪

【讨论】:

  • @Audie - 谢谢,我刚刚从另一篇 SO 文章中发现了 MBBuildExtension Pack,所以很高兴知道它可以工作。这可能不是一个非常直接的答案,但与@dthorpe 的答案一样,它为我指明了一个首要的方向。
【解决方案6】:

这就是我们在这里所做的:我们不会在每次在开发人员的机器上构建项目时都嵌入修订版。充其量这会导致您上面提到的问题,最坏的情况是它会产生误导,因为您可能在工作区中修改了文件。

相反,当我们的项目在单独的构建服务器上以TeamCity 构建时,我们只嵌入源代码控制修订号。修订版嵌入到AssemblyVersionAssemblyFileVersion 中,分为最后两部分。

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

此过程保证版本为 2.3.1.1256 的产品肯定是修订版 11,256 的原始版本。开发人员手动构建的任何内容都将如下所示:2.3.9999.9999。

由于我们没有使用 Mercurial,因此上述实现方式的具体细节并不直接相关,而是简单地说:TeamCity 处理检查所需的修订并将其编号传递给我们的 MSBuild 脚本,然后重写 AssemblyInfo .cs 在我们编写的自定义任务的帮助下。

我特别喜欢这一点的是,在 Visual Studio 中按 F5 绝对不会修改任何文件 - 事实上,就 Visual Studio 而言,它使用的是一个普通的旧解决方案。

【讨论】:

  • @romkyns,谢谢,但这正是我试图避免的情况。我想知道哪些 mercurial chagesets 用于构建应用程序的任何给定实例。我们经常将源代码留在客户机器上,有时现场服务工程师会进行一些小修改。通过嵌入 Hg id,我可以判断他们是否正在运行已提交的版本,如果没有,则更改了哪个库。这本身就很有用,即使在他们提交并向我们发送更新包之前我无法知道他们做了哪些更改。
【解决方案7】:

我刚刚发布了一个小型开源 MSBuild 任务来完全满足您的需求:

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

http://versioning.codeplex.com

【讨论】:

  • 这很酷。我已经开始在一个项目中使用它,设置起来很容易。谢谢乔。
  • 正如已经说过的“非常酷”。这应该是公认的答案 :-) 谢谢乔。
  • 我喜欢这个想法,但对于 mercurial,我想为变更集使用 160 位标识符,而不仅仅是修订号(它是一个 int),因为这在存储库中不一致。
  • @Wilka:是的,在 Mercurial 中,修订号仅与本地克隆有关,因此它不能可靠地指示特定构建中的内容。我也想使用 SHA1,但我不知道该怎么做。无论如何都要向你致敬,乔·戴利。
  • 您可以使用修订哈希!按照主页上的链接获取您可以使用的“令牌”。 Windows 限制 EXE 和 DLL 版本号必须是整数,因此您只能使用本地修订号,但您可以将修订哈希、构建日期、分支名称、标签名称等嵌入到“关于”对话框中例如。使用相同的技术制作 AboutDialog.base.cs 和 AboutDialog.cs。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-12-02
  • 2015-07-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-24
  • 2010-09-07
  • 1970-01-01
相关资源
最近更新 更多