Синхронизация AssemblyVersion и Publish Version в ClickOnce приложении

в 18:04, , рубрики: .net, C#, ClickOnce, Visual Studio, visualstudio, разработка под windows

Добрый день.

Сделал приложение ClickOnce. Всё хорошо, но утомляет обновлять номер версии. Дело в том, что при выкладывании обновления нужно менять версию как в AssemblyInfo, так и в csproj. Вот так я сделал:

public static class VersionInfo {
	public const string VersionString = "1.0.3";
}

А в AssemblyInfo на это свойство ссылаемся:

[assembly: AssemblyVersion(VersionInfo.VersionString)]
[assembly: AssemblyFileVersion(VersionInfo.VersionString)]


Затем нужно залезть в свойства проекта, выбрать вкладку Publish и поменять Publish Version.

image

Либо руками править файл проекта csproj:

<ApplicationVersion>1.0.3.%2a</ApplicationVersion>

Как я уже написал выше, это утомляет, особенно морально. Захотелось автоматизировать этот процесс, чтобы можно было поменять версию только в одном месте. После чего нажать в меню Build -> Publish, и версия в остальных местах сама обновится. Мне показалось удобным менять значение свойства VersionInfo.VersionString, после чего перед компиляцией свежее значение должно прокинуться в файл проекта. Наверняка можно и по другому, но думаю, варианты решения будут похожи на мой.

Итак, нужно перед компиляцией взять значение из класса VersionInfo и положить его в файл проекта. Подобные махинации вроде должен уметь делать fody, но я не нашёл примера, как он может работать с файлами проектов. Поэтому сделал через MSBuild Task. Задача таски проста — перед компиляцией найти файл с классом VersionInfo, затем вытащить оттуда версию, найти файл проекта, засунуть туда новую версию. По пути поймать ошибки и известить пользователя о них в build output. Вот такой код получился (референсил nuget пакет «Microsoft.Build.Tasks.Core»):

public class PublishVersionSyncTask : Task {
	[Required]
	public string ProjectFilePath {
		get; set;
	}
	[Required]
	public string VersionStringFilePath {
		get; set;
	}

	[Output]
	public string Error {
		get { return this._error; }
		set { this._error = value; }
	}
	string _error;

	public override bool Execute() {
		if(!File.Exists(ProjectFilePath)) {
			Error = $"Project File "{ProjectFilePath}" does not exists";
			return true;
		}
		if(!File.Exists(VersionStringFilePath)) {
			Error = $"Version File "{VersionStringFilePath}" does not exists";
			return true;
		}
		string versionString = null;
		var allCodeLines = File.ReadAllLines(VersionStringFilePath);
		foreach(var codeLine in allCodeLines) {
			if(codeLine.Contains("VersionString")) {
				versionString = codeLine.Split('"').Where(s => s.Contains('.')).FirstOrDefault();
				break;
			}
		}
		if(String.IsNullOrEmpty(versionString)) {
			Error = "Can not find version string.";
			return true;
		}
		if(versionString.Split('.').Length != 3) {
			Error = $"Version string has wrong format: {versionString}. It must be x.y.z";
			return true;
		}
		allCodeLines = File.ReadAllLines(ProjectFilePath);
		List<string> fixedCodeLines = new List<string>();
		foreach(var codeLine in allCodeLines) {
			if(!codeLine.Contains("<ApplicationVersion>")) {
				fixedCodeLines.Add(codeLine);
				continue;
			}
			if(codeLine.Contains(versionString))
				return true;
			fixedCodeLines.Add($"    <ApplicationVersion>{versionString}.%2a</ApplicationVersion>");

		}
		try {
			if(File.Exists(ProjectFilePath + ".bak"))
				File.Delete(ProjectFilePath + ".bak");
		}
		catch {
			Error = $"Can not delete {ProjectFilePath}.bak";
			return true;
		}
		File.Copy(ProjectFilePath, ProjectFilePath + ".bak");
		try {
			File.Delete(ProjectFilePath);
		}
		catch {
			File.Delete(ProjectFilePath + ".bak");
			Error = $"Can not delete {ProjectFilePath}";
			return true;
		}
		File.WriteAllLines(ProjectFilePath, fixedCodeLines);
		File.Delete(ProjectFilePath + ".bak");
		return true;
	}
}

Roslyn умеет работать с файлами проекта «по человечески», но это будет дольше работать. А выполняться будет перед каждой компиляцией (хотя можно сделать так, чтобы в Debug конфигурации этот код не гонялся, но мне это не нужно).

Компилируем, подкладываем библиотеки где-то рядом с папкой целевого проекта. В целевом проекте создаём файл PublishVersionSynchronizer.targets с таким контентом:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <UsingTask TaskName="PublishVersionSynchronizer.PublishVersionSyncTask" AssemblyFile="$(TargetDir)....libPublishVersionSynchronizerPublishVersionSynchronizer.dll"/>
  
  <PropertyGroup>
    <BuildDependsOn>
      PublishVersionSync;
      BeforeBuild;
      CoreBuild;
      AfterBuild
    </BuildDependsOn>
  </PropertyGroup>

  <Target Name="PublishVersionSync">
    <PublishVersionSyncTask ProjectFilePath="$(MSBuildProjectFullPath)" VersionStringFilePath="$(MSBuildProjectDirectory)ConfigVersionInfo.cs">
	    <Output PropertyName="ErrorMessage" TaskParameter="Error" />
    </PublishVersionSyncTask>
    <Message Text="(out) Publish version patched" Condition="'$(ErrorMessage)' == ''"/>
	  <Error Condition="'$(ErrorMessage)' != ''" Text="$(ErrorMessage)" />
  </Target>
</Project>

Делаем этому файлу BuildAction=«Content», открываем файл проекта, дописываем импорт этого файла:

<Import Project="$(MSBuildToolsPath)Microsoft.CSharp.targets" /><!--После этой строки-->
<Import Project="PublishVersionSynchronizer.targets" />

Делаем этому файлу BuildAction=«Content», открываем файл проекта, дописываем импорт этого файла:

<Import Project="$(MSBuildToolsPath)Microsoft.CSharp.targets" /><!--После этой строки-->
<Import Project="PublishVersionSynchronizer.targets" />

И всё работает.

Если кому-то нужны исходники, они на гитхабе.

Спасибо.

Автор: zed220

Источник

Поделиться

* - обязательные к заполнению поля