Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,10 +314,10 @@ RelaxVersionerは、ビルド後に、以下の位置にファイルを保存し

正確には:

* ビルド時は`$(IntermediateOutputPath)`です。
* NuGetパッケージ生成時は`$(NuspecOutputPath)`です
* 通常ビルド時は`$(IntermediateOutputPath)`です。
* NuGetパッケージ生成時は、pack requestごとの内部ワークスペースが中間出力配下に作成されます。正確なパスは内部実装であり、固定前提で参照しないでください

例えば、`FooBarProject/obj/Debug/net6.0/` のようなディレクトリ階層です。以下に保存するファイルを示します:
通常ビルドでは、例えば `FooBarProject/obj/Debug/net6.0/` のようなディレクトリ階層になります。以下に保存するファイルを示します:

* `RelaxVersioner_Metadata.cs` : バージョン属性や`ThisAssembly`クラスの定義を含む、ソースコードです。RelaxVersionerの中心的な役割を果たします。
* `RelaxVersioner_Properties.xml` : RelaxVersionerがバージョン計算を行う直前の、MSBuildの全てのプロパティを、XML形式でダンプしたものです。
Expand All @@ -334,7 +334,8 @@ RelaxVersionerは、ビルド後に、以下の位置にファイルを保存し
例えば、`RelaxVersioner_ShortVersion.txt`には、`2.5.4`のような文字列が格納されているので、
ビルド成果物をサーバーにアップロードする際に、バージョン番号をファイル名に追加して保存する事が出来るかもしれません。

これらの情報をMSBuildターゲット内から参照する場合は、テキストファイルにアクセスすることなく、以下のようにプロパティを使用できます:
これらの情報をMSBuildターゲット内から参照する場合は、テキストファイルにアクセスすることなく、以下のようにプロパティを使用できます。
特にpack時は、ファイルパスではなくこれらのプロパティを使用することを推奨します:

```xml
<Target Name="AB" AfterTargets="Compile">
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,10 @@ RelaxVersioner saves the files in the following location after build:

To be precise:

* `$(IntermediateOutputPath)` at build time.
* `$(NuspecOutputPath)` at NuGet package generation.
* `$(IntermediateOutputPath)` for normal builds.
* A request-scoped internal workspace under the intermediate output root during NuGet pack. The exact path is internal and should not be treated as stable.

For example, `FooBarProject/obj/Debug/net6.0/` directory hierarchy. Here are the files to save:
For normal builds, this is typically a directory hierarchy such as `FooBarProject/obj/Debug/net6.0/`. Here are the files to save:

* `RelaxVersioner_Metadata.cs` : Source code including version attributes and `ThisAssembly` class definition, which is the core feature of RelaxVersioner.
* `RelaxVersioner_Properties.xml` : A dump of all MSBuild properties in XML format, just before RelaxVersioner calculates the version.
Expand All @@ -328,7 +328,8 @@ For example, `FooBarProject/obj/Debug/net6.0/` directory hierarchy. Here are the

If you want to reference this information from your program, you can get it from the version attributes or `ThisAssembly`. Other, XML or text files can be referenced by CI/CD (Continuous Integration and Continuous Deployment) to apply version information to the build process. For example, `RelaxVersioner_ShortVersion.txt` contains a string like `2.5.4`, so you may be able to save your build artifacts with a version number when you upload them to the server.

If you want to reference this information from within the MSBuild target, you can use the properties as follows without having to access the text file:
If you want to reference this information from within the MSBuild target, you can use the properties as follows without having to access the text file.
Especially during pack, prefer these properties over hard-coding file paths:

```xml
<Target Name="AB" AfterTargets="Compile">
Expand Down
173 changes: 173 additions & 0 deletions RelaxVersioner.Core.Tests/PackTargetsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
////////////////////////////////////////////////////////////////////////////////////////
//
// RelaxVersioner - Git tag/branch based, full-automatic version generator.
// Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mi.kekyo.net)
//
// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0
//
////////////////////////////////////////////////////////////////////////////////////////

using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using NUnit.Framework;

namespace RelaxVersioner;

[NonParallelizable]
public sealed class PackTargetsTests
{
private static readonly SemaphoreSlim tasksAssemblyLock = new(1, 1);
private static string? tasksAssemblyPath;

[Test]
public async Task RelaxVersionerPackPrepare_UsesUniqueWorkspacePerBuildRequest()
{
#if !NET8_0
Assert.Ignore("This integration test runs once on net8.0.");
#else

var repositoryRoot = GetRepositoryRoot();
var builtTasksAssemblyPath = await EnsureTasksAssemblyPathAsync(repositoryRoot);
var tempPath = Path.Combine(
Path.GetTempPath(),
$"RelaxVersionerPackTargetsTest_{Guid.NewGuid():N}");

try
{
Directory.CreateDirectory(tempPath);

var projectPath = Path.Combine(tempPath, "PackWorkspaceProbe.csproj");
await File.WriteAllTextAsync(
projectPath,
$$"""
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="{{EscapePath(Path.Combine(repositoryRoot, "RelaxVersioner", "build", "RelaxVersioner.props"))}}" />

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>true</IsPackable>
<_RVB_MSBuildTaskPath>{{EscapePath(builtTasksAssemblyPath)}}</_RVB_MSBuildTaskPath>
</PropertyGroup>

<Import Project="{{EscapePath(Path.Combine(repositoryRoot, "RelaxVersioner", "build", "RelaxVersioner.targets"))}}" />

<Target Name="CapturePackWorkspace" DependsOnTargets="RelaxVersionerPackPrepare">
<WriteLinesToFile
File="$(BaseIntermediateOutputPath)$(RequestMarker).log"
Lines="$(RelaxVersionerPropertiesPath)"
Overwrite="true" />
</Target>

<Target Name="RunParallelPackLikeRequests">
<ItemGroup>
<_PackRequests Include="$(MSBuildProjectFullPath)" AdditionalProperties="RequestMarker=first" />
<_PackRequests Include="$(MSBuildProjectFullPath)" AdditionalProperties="RequestMarker=second" />
</ItemGroup>

<MSBuild
Projects="@(_PackRequests)"
Targets="CapturePackWorkspace"
BuildInParallel="true" />
</Target>
</Project>
""");

await TestUtilities.RunCommandAsync(
"dotnet",
tempPath,
$"msbuild \"{projectPath}\" -t:RunParallelPackLikeRequests -m:2 -nr:false");

var firstPath = (await File.ReadAllTextAsync(Path.Combine(tempPath, "obj", "first.log"))).Trim();
var secondPath = (await File.ReadAllTextAsync(Path.Combine(tempPath, "obj", "second.log"))).Trim();
var legacyPackPath = Path.GetFullPath(
Path.Combine(tempPath, "obj", "Debug", "RelaxVersioner_Properties.xml"));

Assert.Multiple(() =>
{
Assert.That(firstPath, Is.Not.Empty);
Assert.That(secondPath, Is.Not.Empty);
Assert.That(firstPath, Is.Not.EqualTo(secondPath),
"Pack-like requests should not share the same properties path.");
Assert.That(firstPath, Does.Contain("RelaxVersioner_Pack"));
Assert.That(secondPath, Does.Contain("RelaxVersioner_Pack"));
Assert.That(firstPath, Does.EndWith("RelaxVersioner_Properties.xml"));
Assert.That(secondPath, Does.EndWith("RelaxVersioner_Properties.xml"));
Assert.That(Path.GetFullPath(firstPath), Is.Not.EqualTo(legacyPackPath));
Assert.That(Path.GetFullPath(secondPath), Is.Not.EqualTo(legacyPackPath));
Assert.That(Path.GetDirectoryName(firstPath), Is.Not.EqualTo(Path.GetDirectoryName(secondPath)),
"Each request should resolve a different workspace directory.");
});
}
finally
{
if (Directory.Exists(tempPath))
{
try { Directory.Delete(tempPath, true); } catch { }
}
}
#endif
}

private static async Task<string> EnsureTasksAssemblyPathAsync(string repositoryRoot)
{
await tasksAssemblyLock.WaitAsync();
try
{
if (!string.IsNullOrWhiteSpace(tasksAssemblyPath) &&
File.Exists(tasksAssemblyPath))
{
return tasksAssemblyPath;
}

var projectPath = Path.Combine(
repositoryRoot,
"RelaxVersioner.Tasks",
"RelaxVersioner.Tasks.csproj");

await TestUtilities.RunCommandAsync(
"dotnet",
repositoryRoot,
$"build \"{projectPath}\" -c Debug -f netstandard2.0 -p:BOOTSTRAP=True -nr:false");

tasksAssemblyPath = Path.Combine(
repositoryRoot,
"RelaxVersioner.Tasks",
"bin",
"Debug",
"netstandard2.0",
"RelaxVersioner.Tasks.dll");

Assert.That(File.Exists(tasksAssemblyPath), Is.True,
$"Task assembly should exist: {tasksAssemblyPath}");
return tasksAssemblyPath;
}
finally
{
tasksAssemblyLock.Release();
}
}

private static string GetRepositoryRoot()
{
var directory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory);

while (directory != null)
{
if (directory.EnumerateFiles("RelaxVersioner.sln", SearchOption.TopDirectoryOnly).Any())
{
return directory.FullName;
}

directory = directory.Parent;
}

throw new InvalidOperationException("Repository root could not be located.");
}

private static string EscapePath(string path) =>
path.Replace("&", "&amp;").Replace("'", "&apos;").Replace("\"", "&quot;");
}
2 changes: 1 addition & 1 deletion RelaxVersioner.Core.Tests/RelaxVersioner.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageReference Include="NUnit" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="RelaxVersioner" Version="3.20.0" PrivateAssets="all" />
<PackageReference Include="RelaxVersioner" Version="3.21.0" PrivateAssets="all" />
</ItemGroup>

<ItemGroup Condition="('$(TargetFramework)' == 'net6.0') OR ('$(TargetFramework)' == 'net7.0') OR ('$(TargetFramework)' == 'net8.0') OR ('$(TargetFramework)' == 'net9.0')">
Expand Down
126 changes: 81 additions & 45 deletions RelaxVersioner.Core.Tests/TestUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
////////////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
Expand All @@ -27,60 +28,48 @@ public static async Task<string> ReadAllTextAsync(string path)

public static async Task RunGitCommandAsync(string workingDirectory, string arguments)
{
using var process = new Process
try
{
StartInfo = new ProcessStartInfo
{
FileName = "git",
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};

process.Start();

// Use Task.Run to wrap the synchronous WaitForExit for compatibility
await Task.Run(() => process.WaitForExit());

if (process.ExitCode != 0)
await RunProcessCoreAsync("git", workingDirectory, arguments, null);
}
catch (InvalidOperationException ex)
{
var error = process.StandardError.ReadToEnd();
throw new InvalidOperationException($"Git command failed: git {arguments}\nError: {error}");
throw new InvalidOperationException($"Git command failed: git {arguments}\nError: {ex.Message}", ex);
}
}

public static async Task<string> RunGitCommandWithOutputAsync(string workingDirectory, string arguments)
{
using var process = new Process
try
{
StartInfo = new ProcessStartInfo
{
FileName = "git",
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};

process.Start();

// Use Task.Run to wrap the synchronous WaitForExit for compatibility
await Task.Run(() => process.WaitForExit());

if (process.ExitCode != 0)
var (_, output, _) = await RunProcessCoreAsync("git", workingDirectory, arguments, null);
return output;
}
catch (InvalidOperationException ex)
{
var error = process.StandardError.ReadToEnd();
throw new InvalidOperationException($"Git command failed: git {arguments}\nError: {error}");
throw new InvalidOperationException($"Git command failed: git {arguments}\nError: {ex.Message}", ex);
}

return process.StandardOutput.ReadToEnd();
}

public static async Task RunCommandAsync(
string fileName,
string workingDirectory,
string arguments,
IReadOnlyDictionary<string, string?>? environmentVariables = null) =>
await RunProcessCoreAsync(fileName, workingDirectory, arguments, environmentVariables);

public static async Task<string> RunCommandWithOutputAsync(
string fileName,
string workingDirectory,
string arguments,
IReadOnlyDictionary<string, string?>? environmentVariables = null)
{
var (_, output, _) = await RunProcessCoreAsync(
fileName,
workingDirectory,
arguments,
environmentVariables);
return output;
}

public static async Task InitializeGitRepositoryWithMainBranch(string workingDirectory)
Expand Down Expand Up @@ -110,4 +99,51 @@ public static async Task InitializeGitRepositoryWithMainBranch(string workingDir
}
}
}
}

private static async Task<(int ExitCode, string StandardOutput, string StandardError)> RunProcessCoreAsync(
string fileName,
string workingDirectory,
string arguments,
IReadOnlyDictionary<string, string?>? environmentVariables)
{
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};

if (environmentVariables != null)
{
foreach (var (key, value) in environmentVariables)
{
process.StartInfo.Environment[key] = value ?? string.Empty;
}
}

process.Start();

var standardOutputTask = process.StandardOutput.ReadToEndAsync();
var standardErrorTask = process.StandardError.ReadToEndAsync();

await Task.Run(() => process.WaitForExit());

var standardOutput = await standardOutputTask;
var standardError = await standardErrorTask;

if (process.ExitCode != 0)
{
throw new InvalidOperationException(
$"Command failed: {fileName} {arguments}\nError: {standardError}");
}

return (process.ExitCode, standardOutput, standardError);
}
}
2 changes: 1 addition & 1 deletion RelaxVersioner.Core/RelaxVersioner.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="All" />
<PackageReference Condition="'$(RV_BOOTSTRAP)' != 'True'"
Include="RelaxVersioner" Version="3.20.0" PrivateAssets="all" />
Include="RelaxVersioner" Version="3.21.0" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading