Автоматическое встраивание информации о версии Mercurial в проекты Visual Studio C#
Оригинальная проблема
При создании наших проектов я хочу, чтобы mercurial id каждого репозитория был встроен в продукт (ы) этого репозитория (библиотека, приложение или тестовое приложение).
Я считаю, что это значительно упрощает отладку приложений, запускаемых клиентами на расстоянии 8 часовых поясов, если вы точно знаете, что пошло на создание конкретной версии приложения, которое они используют. Таким образом, каждый проект (приложение или библиотека) в наших системах реализует способ получения соответствующей информации о ревизиях.
Я также считаю очень полезным иметь возможность видеть, было ли приложение скомпилировано с чистыми (неизмененными) наборами изменений из хранилища. 'Hg 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. Мне уже приходилось несколько раз изменять командный файл, так как оригинальный работал под XP, но не под Vista, следующая версия работала под Vista, но не под XP, и, наконец, мне удалось заставить его работать с обоими. Будет ли он работать с Windows 7, однако, пока никто не догадывается, и со временем я думаю, что с большей вероятностью подрядчики будут ожидать, что смогут создавать наши приложения на своих Windows 7 boxen.
Наконец, у меня есть эстетическая проблема с этим решением, командные файлы и объединенные файлы шаблонов кажутся неправильным способом сделать это.
Мои актуальные вопросы
Как бы вы решили / как вы решаете проблему, которую я пытаюсь решить?
Что может быть лучше, чем то, чем я сейчас занимаюсь?
Отклоненные решения этих проблем
Перед тем, как реализовать текущее решение, я посмотрел на расширение Mercurials Keyword, так как оно казалось очевидным решением. Однако чем больше я смотрел на это и читал мнения людей, тем больше приходил к выводу, что это было неправильно.
Я также помню проблемы, вызванные подменой ключевых слов в проектах в предыдущих компаниях (мысль о том, что когда-либо придется снова использовать 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, я был бы признателен, если бы он прошел.
Наконец, моя эстетическая проблема, связанная с этим решением, стала еще сильнее, чем была раньше, поскольку пакетный файл более сложный, и теперь есть еще что-то, что может пойти не так.
Если вы можете придумать какие-либо лучшие решения, я хотел бы услышать о них.
7 ответов
Я думаю, что у меня есть ответ для вас. Это будет немного сложным, но это избавит вас от необходимости делать какие-либо пакетные файлы. Вы можете положиться на MSBuild и Custom Tasks, чтобы сделать это для вас. Я использовал пакет расширений для MSBuild (доступен на CodePlex) - но вторая задача, которая вам нужна, это то, что вы могли бы так же легко написать самостоятельно.
С помощью этого решения вы можете щелкнуть правой кнопкой мыши на DLL и увидеть в свойствах файла, из какой версии Mercurial появилась DLL (или EXE).
Вот шаги:
- Получить пакет MBBuildExtensionИЛИ Написать пользовательскую задачу для перезаписи AssemblyInfo.cs
- Создайте пользовательскую задачу сборки в своем собственном проекте, чтобы получить Mercurial Id(код ниже).
- Отредактируйте файлы проекта, для которых требуется 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 с вашим кодом, гарантируя, что местоположение dll является фиксированным относительно каждого проекта, который вы должны использовать это в. Таким образом, никто не должен ничего устанавливать.
Удачи, и я надеюсь, что это поможет!
Оди
Я только что выпустил небольшую задачу MSBuild с открытым исходным кодом, чтобы сделать именно то, что вам нужно:
- Он помещает ваш номер версии Mercurial в вашу версию сборки.NET
- По версии можно узнать, была ли сборка с незафиксированными изменениями
- Не вызывает ненужных сборок, если ревизия не изменилась
- Не зависит от сценариев Windows
- Ничего не установить - вы просто добавляете небольшую DLL к своему решению и редактируете некоторые файлы в своем проекте
Рассматривали ли вы использование строкового ресурса вместо строковой константы языка C#? Строковые ресурсы можно редактировать / заменять в выходных двоичных файлах после сборки, используя инструменты, предназначенные для локализации.
Вы отправили бы свой номер версии Mercurial в текстовый файл, который не используется сборкой C#, а затем с помощью операции после сборки замените ресурс версии фактическим значением из выпущенного текстового файла. Если вы подписываете сборки под строгим именем, замена строки ресурса должна произойти до подписания.
Вот как мы решали эту проблему в Borland несколько лет назад для продуктов Windows. С тех пор мир стал более сложным, но принцип все еще применяется.
Мы решили эту проблему с другой системой контроля версий, Subversion.
Что мы делаем, так это то, что у нас есть файл commonassemblyinfo.cs, и мы подключаем к нему номер редакции svn, используя скрипт msbuild.
Мы буквально вызываем svninfo и очищаем вывод из ревизии: part, а затем вставляем его в файл CommonAssemblyInfo.cs.
Каждый проект в нашем решении имеет ссылку на общий файл и затем компилируется, что означает, что сборки компилируются с версией при компиляции с номером редакции svn, это также означает, что все зависимые библиотеки, которые мы написали, также версионированы.
Мы достигли этого довольно легко, используя cruisecontrol.net и msbuild файлы.
Я не использовал Mercurial, но я полагаю, что вы могли бы сделать что-то подобное с командой id? но из-за формата вывода вам нужно подходить к тому, как вы его использовали, несколько иначе.
У меня был бы стандартный xml-файл, который читается при запуске приложения, и вы можете вставить информацию в него (работает и для resx-файлов). Затем вы можете прочитать это значение обратно в коде. Немного неприглядно, но это работает.
Мое любимое решение - способ, которым мы делаем это, но вы не можете сделать это в Mercurial, так как информация о наборе изменений не является чисто числовой, как номер редакции SVN.
Я мой.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 $";
Кажется, есть несколько возможных подходов к этой проблеме. Первое и, вероятно, предпочтительное решение - установить сервер сборки и распространять только сборки, сгенерированные там, среди клиентов. Преимущество в том, что вы никогда не отправляете незафиксированные изменения. Используя MSBuild, NAnt или какой-либо другой инструмент сборки на основе задач, весь процесс становится очень гибким. Я смог установить TeamCity и получить первые пару сборок и запустить их без особых усилий, но есть и другие хорошие серверы сборки. Это действительно должно быть вашим решением.
Если вы по какой-то причине настаиваете, что можно распространять сборки для разработчиков среди клиентов;), тогда вам понадобится локальное решение.
Довольно простым решением было бы использовать встроенную поддержку для автоматического увеличения номера сборки сборки:
// major.minor.build.revision
[assembly:AssemblyVersion("1.2.*")]
*
делает номер сборки автоматически увеличивающимся при каждой компиляции (и есть изменения). Номер ревизии является случайным числом. Отсюда вы можете либо отслеживать связь с идентификатором Mercurial, сохранив обе части информации, например, опубликовав ее в каком-либо внутреннем веб-решении или в соответствии с вашими конкретными потребностями, или обновить сгенерированную сборку. Я подозреваю, что вы могли бы использовать PostSharp или Mono.Cecil переписать сборку, например, исправив номер ревизии, чтобы быть идентификатором. Если ваши сборки подписаны, перезапись должна произойти до того, как вы подпишете их, что немного утомительно, если у вас нет файла сборки. Обратите внимание, что VS можно настроить для компиляции с использованием вашего пользовательского файла сборки вместо процедуры сборки по умолчанию.
Мое последнее предложение - создать отдельный проект только для идентификатора hg и использовать шаг после сборки, чтобы объединить созданные сборки в один. ILMerge поддерживает повторное подписание подписанных сборок, и поэтому, вероятно, будет легче сделать работу. Недостатком является то, что перераспределение ILMerge не допускается (хотя коммерческое использование разрешено).
Это не решение, но, надеюсь, вдохновение, чтобы вы начали.
Вот что мы делаем здесь: мы не внедряем ревизию каждый раз, когда проект создается на машинах разработчика. В лучшем случае это вызывает проблемы, о которых вы упоминали выше, а в худшем - вводит в заблуждение, поскольку вы могли изменить файлы в своем рабочем пространстве.
Вместо этого мы встраиваем номер версии контроля версий только в том случае, если наши проекты собраны под TeamCity на отдельном сервере сборки. Редакция встроена в оба AssemblyVersion
а также AssemblyFileVersion
, разделить на две последние части.
По умолчанию версия заканчивается в 9999,9999, и мы разделяем ревизию таким образом, чтобы ревизия 12345678 стала 1234,5678 (не то, чтобы мы были где-то близко к 12-миллионной ревизии...).
Этот процесс гарантирует, что продукт, чья версия 2.3.1.1256, определенно был первоначальной версией 11 256. Все, что разработчики собирают вручную, будет выглядеть следующим образом: 2.3.9999.9999.
Точные подробности того, как это достигается, не имеют прямого отношения, поскольку мы не используем Mercurial, но кратко: TeamCity обрабатывает проверку требуемой ревизии и передает ее номер в наш скрипт MSBuild, который затем выполняет перезапись AssemblyInfo.cs с помощью помощь пользовательского задания, которое мы написали.
Что мне особенно нравится в этом, так это то, что абсолютно никакие файлы не изменяются при нажатии F5 в Visual Studio - фактически, что касается Visual Studio, он работает с простым старым нормальным решением.