Add SdkArchiveDiff task to verify the sdk archive has all the expected outputs
This commit is contained in:
parent
0a73f814e1
commit
e894991b5f
10 changed files with 596 additions and 0 deletions
|
@ -199,6 +199,7 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<XPlatSourceBuildTasksAssembly>$([MSBuild]::NormalizePath('$(ArtifactsBinDir)', 'Microsoft.DotNet.SourceBuild.Tasks.XPlat', '$(Configuration)', 'Microsoft.DotNet.SourceBuild.Tasks.XPlat.dll'))</XPlatSourceBuildTasksAssembly>
|
<XPlatSourceBuildTasksAssembly>$([MSBuild]::NormalizePath('$(ArtifactsBinDir)', 'Microsoft.DotNet.SourceBuild.Tasks.XPlat', '$(Configuration)', 'Microsoft.DotNet.SourceBuild.Tasks.XPlat.dll'))</XPlatSourceBuildTasksAssembly>
|
||||||
<LeakDetectionTasksAssembly>$([MSBuild]::NormalizePath('$(ArtifactsBinDir)', 'Microsoft.DotNet.SourceBuild.Tasks.LeakDetection', '$(Configuration)', 'Microsoft.DotNet.SourceBuild.Tasks.LeakDetection.dll'))</LeakDetectionTasksAssembly>
|
<LeakDetectionTasksAssembly>$([MSBuild]::NormalizePath('$(ArtifactsBinDir)', 'Microsoft.DotNet.SourceBuild.Tasks.LeakDetection', '$(Configuration)', 'Microsoft.DotNet.SourceBuild.Tasks.LeakDetection.dll'))</LeakDetectionTasksAssembly>
|
||||||
|
<SdkArchiveDiffTasksAssembly>$([MSBuild]::NormalizePath('$(ArtifactsBinDir)', 'Microsoft.DotNet.SourceBuild.Tasks.SdkArchiveDiff', '$(Configuration)', 'Microsoft.DotNet.SourceBuild.Tasks.SdkArchiveDiff.dll'))</SdkArchiveDiffTasksAssembly>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(EnablePoison)' == 'true'">
|
<PropertyGroup Condition="'$(EnablePoison)' == 'true'">
|
||||||
|
|
|
@ -20,5 +20,6 @@
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Import Project="$(RepositoryEngineeringDir)build.sourcebuild.targets" Condition="'$(DotNetBuildSourceOnly)' == 'true'" />
|
<Import Project="$(RepositoryEngineeringDir)build.sourcebuild.targets" Condition="'$(DotNetBuildSourceOnly)' == 'true'" />
|
||||||
|
<Import Project="$(RepositoryEngineeringDir)build.targets" />
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
27
src/SourceBuild/content/eng/build.targets
Normal file
27
src/SourceBuild/content/eng/build.targets
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<Project>
|
||||||
|
<UsingTask AssemblyFile="$(SdkArchiveDiffTasksAssembly)" TaskName="FindArchiveDiffs" />
|
||||||
|
<UsingTask AssemblyFile="$(SdkArchiveDiffTasksAssembly)" TaskName="GetClosestOfficialSdk" />
|
||||||
|
|
||||||
|
<Target Name="ReportSdkArchiveDiffs"
|
||||||
|
AfterTargets="Build"
|
||||||
|
DependsOnTargets="DetermineSourceBuiltSdkVersion"
|
||||||
|
Condition="'$(ReportSdkArchiveDiffs)' == 'true'">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<SdkArchiveDiffOutputPath>$(OutDir)</SdkArchiveDiffOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<GetClosestOfficialSdk BuiltSdkPath="@(SdkTarballItem)">
|
||||||
|
<Output TaskParameter="ClosestOfficialSdkPath" PropertyName="ClosestOfficialSdkPath" />
|
||||||
|
</GetClosestOfficialSdk>
|
||||||
|
|
||||||
|
<FindArchiveDiffs BaselineArchive="@(SdkTarballItem)" TestArchive="$(ClosestOfficialSdkPath)">
|
||||||
|
<Output TaskParameter="ContentDifferences" ItemName="ContentDifferences" />
|
||||||
|
</FindArchiveDiffs>
|
||||||
|
|
||||||
|
<Message Text="Difference in sdk archive: %(ContentDifferences.Kind): %(ContentDifferences.Identity)" Importance="High" Condition="'%(ContentDifferences.Kind)' != 'Unchanged'"/>
|
||||||
|
<Delete Files="$(ClosestOfficialSdkPath)" />
|
||||||
|
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -15,6 +15,7 @@
|
||||||
UnpackTarballs;
|
UnpackTarballs;
|
||||||
BuildXPlatTasks;
|
BuildXPlatTasks;
|
||||||
BuildMSBuildSdkResolver;
|
BuildMSBuildSdkResolver;
|
||||||
|
BuildTarballDiff;
|
||||||
BuildLeakDetection;
|
BuildLeakDetection;
|
||||||
ExtractToolPackage;
|
ExtractToolPackage;
|
||||||
GenerateRootFs;
|
GenerateRootFs;
|
||||||
|
@ -116,6 +117,14 @@
|
||||||
</Touch>
|
</Touch>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="BuildSdkArchiveDiff" >
|
||||||
|
<MSBuild Projects="tasks\Microsoft.DotNet.SourceBuild.Tasks.SdkArchiveDiff\Microsoft.DotNet.SourceBuild.Tasks.SdkArchiveDiff.csproj"
|
||||||
|
Targets="Restore"
|
||||||
|
Properties="MSBuildRestoreSessionId=$([System.Guid]::NewGuid())" />
|
||||||
|
<MSBuild Projects="tasks\Microsoft.DotNet.SourceBuild.Tasks.SdkArchiveDiff\Microsoft.DotNet.SourceBuild.Tasks.SdkArchiveDiff.csproj"
|
||||||
|
Targets="Build" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
<Target Name="GenerateRootFs"
|
<Target Name="GenerateRootFs"
|
||||||
Condition="'$(BuildOS)' != 'windows' and '$(CrossBuild)' == 'true' and '$(ROOTFS_DIR)' == ''">
|
Condition="'$(BuildOS)' != 'windows' and '$(CrossBuild)' == 'true' and '$(ROOTFS_DIR)' == ''">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Formats.Tar;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Reflection.Metadata;
|
||||||
|
using System.Reflection.PortableExecutable;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static ArchiveExtensions;
|
||||||
|
|
||||||
|
public abstract class Archive : IDisposable
|
||||||
|
{
|
||||||
|
public static async Task<Archive> Create(string path)
|
||||||
|
{
|
||||||
|
if (path.EndsWith(".tar.gz"))
|
||||||
|
return await TarArchive.Create(path);
|
||||||
|
else if (path.EndsWith(".zip"))
|
||||||
|
return ZipFileArchive.Create(path);
|
||||||
|
else
|
||||||
|
throw new NotSupportedException("Unsupported archive type");
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract bool Contains(string relativePath);
|
||||||
|
|
||||||
|
public abstract string[] GetFileNames();
|
||||||
|
|
||||||
|
public abstract string[] GetFileLines(string relativePath);
|
||||||
|
|
||||||
|
public abstract Task<byte[]> GetFileBytesAsync(string relativePath);
|
||||||
|
|
||||||
|
public abstract void Dispose();
|
||||||
|
|
||||||
|
public class TarArchive : Archive
|
||||||
|
{
|
||||||
|
private string _extractedFolder;
|
||||||
|
|
||||||
|
private TarArchive(string extractedFolder)
|
||||||
|
{
|
||||||
|
_extractedFolder = extractedFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<TarArchive> Create(string path, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tmpFolder = Directory.CreateTempSubdirectory(nameof(FindArchiveDiffs));
|
||||||
|
using (var gzStream = File.OpenRead (path))
|
||||||
|
using (var gzipStream = new GZipStream (gzStream, CompressionMode.Decompress))
|
||||||
|
{
|
||||||
|
await TarFile.ExtractToDirectoryAsync(gzipStream, tmpFolder.FullName, true, cancellationToken);
|
||||||
|
}
|
||||||
|
return new TarArchive(tmpFolder.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Contains(string relativePath)
|
||||||
|
{
|
||||||
|
return File.Exists(Path.Combine(_extractedFolder, relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string[] GetFileNames()
|
||||||
|
{
|
||||||
|
return Directory.GetFiles(_extractedFolder, "*", SearchOption.AllDirectories).Select(f => f.Substring(_extractedFolder.Length + 1)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string[] GetFileLines(string relativePath)
|
||||||
|
{
|
||||||
|
return File.ReadAllLines(Path.Combine(_extractedFolder, relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<byte[]> GetFileBytesAsync(string relativePath)
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(_extractedFolder, relativePath);
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
return Task.FromResult<byte[]>([]);
|
||||||
|
return File.ReadAllBytesAsync(Path.Combine(_extractedFolder, relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_extractedFolder))
|
||||||
|
Directory.Delete(_extractedFolder, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ZipFileArchive : Archive
|
||||||
|
{
|
||||||
|
private ZipArchive _archive;
|
||||||
|
|
||||||
|
private ZipFileArchive(ZipArchive archive)
|
||||||
|
{
|
||||||
|
_archive = archive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static new ZipFileArchive Create(string path)
|
||||||
|
{
|
||||||
|
return new ZipFileArchive(new ZipArchive(File.OpenRead(path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Contains(string relativePath)
|
||||||
|
{
|
||||||
|
return _archive.GetEntry(relativePath) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string[] GetFileNames()
|
||||||
|
{
|
||||||
|
return _archive.Entries.Select(e => e.FullName).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string[] GetFileLines(string relativePath)
|
||||||
|
{
|
||||||
|
var entry = _archive.GetEntry(relativePath);
|
||||||
|
if (entry == null)
|
||||||
|
throw new ArgumentException("File not found");
|
||||||
|
return entry.Lines();
|
||||||
|
}
|
||||||
|
public override Task<byte[]> GetFileBytesAsync(string relativePath)
|
||||||
|
{
|
||||||
|
using (var entry = _archive.GetEntry(relativePath)?.Open())
|
||||||
|
{
|
||||||
|
if (entry == null)
|
||||||
|
{
|
||||||
|
return Task.FromResult<byte[]>([]);
|
||||||
|
}
|
||||||
|
return entry.ReadToEndAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
_archive.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Build.Framework;
|
||||||
|
using Microsoft.Build.Utilities;
|
||||||
|
using Task = System.Threading.Tasks.Task;
|
||||||
|
|
||||||
|
public class FindArchiveDiffs : Microsoft.Build.Utilities.Task
|
||||||
|
{
|
||||||
|
public class ArchiveItem
|
||||||
|
{
|
||||||
|
public required string Path { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public required ITaskItem BaselineArchive { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public required ITaskItem TestArchive { get; init; }
|
||||||
|
|
||||||
|
[Output]
|
||||||
|
public ITaskItem[] ContentDifferences { get; set; } = [];
|
||||||
|
|
||||||
|
public override bool Execute()
|
||||||
|
{
|
||||||
|
return Task.Run(ExecuteAsync).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExecuteAsync()
|
||||||
|
{
|
||||||
|
var baselineTask = Archive.Create(BaselineArchive.ItemSpec);
|
||||||
|
var testTask = Archive.Create(TestArchive.ItemSpec);
|
||||||
|
Task.WaitAll(baselineTask, testTask);
|
||||||
|
using var baseline = await baselineTask;
|
||||||
|
using var test = await testTask;
|
||||||
|
var baselineFiles = baseline.GetFileNames();
|
||||||
|
var testFiles = test.GetFileNames();
|
||||||
|
ContentDifferences =
|
||||||
|
GetDiffs(baselineFiles, testFiles, PathWithVersions.Equal, PathWithVersions.GetVersionAnonymousPath)
|
||||||
|
.Select(FromDiff)
|
||||||
|
.ToArray();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ITaskItem FromDiff((string, DifferenceKind) diff)
|
||||||
|
{
|
||||||
|
var item = new TaskItem(diff.Item1);
|
||||||
|
item.SetMetadata("Kind", Enum.GetName(diff.Item2));
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DifferenceKind
|
||||||
|
{
|
||||||
|
Added,
|
||||||
|
Removed,
|
||||||
|
Unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<(string, DifferenceKind DifferenceKind)> GetDiffs(
|
||||||
|
string[] originalPathsWithVersions,
|
||||||
|
string[] modifiedPathsWithVersions,
|
||||||
|
Func<string, string, bool> equalityComparer,
|
||||||
|
Func<string, string>? formatter = null)
|
||||||
|
{
|
||||||
|
formatter ??= static s => s;
|
||||||
|
// Edit distance algorithm: https://en.wikipedia.org/wiki/Longest_common_subsequence
|
||||||
|
|
||||||
|
int[,] dp = new int[originalPathsWithVersions.Length + 1, modifiedPathsWithVersions.Length + 1];
|
||||||
|
|
||||||
|
// Initialize first row and column
|
||||||
|
for (int i = 0; i <= originalPathsWithVersions.Length; i++)
|
||||||
|
{
|
||||||
|
dp[i, 0] = i;
|
||||||
|
}
|
||||||
|
for (int j = 0; j <= modifiedPathsWithVersions.Length; j++)
|
||||||
|
{
|
||||||
|
dp[0, j] = j;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute edit distance
|
||||||
|
for (int i = 1; i <= originalPathsWithVersions.Length; i++)
|
||||||
|
{
|
||||||
|
for (int j = 1; j <= modifiedPathsWithVersions.Length; j++)
|
||||||
|
{
|
||||||
|
if (equalityComparer(originalPathsWithVersions[i - 1], modifiedPathsWithVersions[j - 1]))
|
||||||
|
{
|
||||||
|
dp[i, j] = dp[i - 1, j - 1];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dp[i, j] = 1 + Math.Min(dp[i - 1, j], dp[i, j - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trace back the edits
|
||||||
|
int row = originalPathsWithVersions.Length;
|
||||||
|
int col = modifiedPathsWithVersions.Length;
|
||||||
|
|
||||||
|
List<(string, DifferenceKind)> formattedDiff = [];
|
||||||
|
while (row > 0 || col > 0)
|
||||||
|
{
|
||||||
|
var baselineItem = originalPathsWithVersions[row - 1];
|
||||||
|
var testItem = modifiedPathsWithVersions[col - 1];
|
||||||
|
if (row > 0 && col > 0 && PathWithVersions.Equal(baselineItem, testItem))
|
||||||
|
{
|
||||||
|
formattedDiff.Add((formatter(originalPathsWithVersions[row - 1]), DifferenceKind.Unchanged));
|
||||||
|
row--;
|
||||||
|
col--;
|
||||||
|
}
|
||||||
|
else if (col > 0 && (row == 0 || dp[row, col - 1] <= dp[row - 1, col]))
|
||||||
|
{
|
||||||
|
formattedDiff.Add((formatter(modifiedPathsWithVersions[col - 1]), DifferenceKind.Added));
|
||||||
|
col--;
|
||||||
|
}
|
||||||
|
else if (row > 0 && (col == 0 || dp[row, col - 1] > dp[row - 1, col]))
|
||||||
|
{
|
||||||
|
formattedDiff.Add((formatter(originalPathsWithVersions[row - 1]), DifferenceKind.Removed));
|
||||||
|
row--;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new UnreachableException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formattedDiff.Reverse();
|
||||||
|
return formattedDiff;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Build.Framework;
|
||||||
|
public class GetClosestOfficialSdk : Microsoft.Build.Utilities.Task
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public required string BuiltSdkPath { get; init; }
|
||||||
|
|
||||||
|
[Output]
|
||||||
|
public string ClosestOfficialSdkPath { get; set; } = "";
|
||||||
|
|
||||||
|
public override bool Execute()
|
||||||
|
{
|
||||||
|
return Task.Run(ExecuteAsync).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExecuteAsync()
|
||||||
|
{
|
||||||
|
var (versionString, rid, extension) = ExtractFromFilePath(BuiltSdkPath);
|
||||||
|
|
||||||
|
string downloadUrl = GetLatestOfficialSdkUrl(versionString, rid, extension);
|
||||||
|
|
||||||
|
Log.LogMessage($"Downloading {downloadUrl}");
|
||||||
|
var packageResponse = await new HttpClient().GetAsync(downloadUrl);
|
||||||
|
packageResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var packageUriPath = packageResponse.RequestMessage!.RequestUri!.LocalPath;
|
||||||
|
string downloadedVersion = PathWithVersions.GetVersionInPath(packageUriPath).ToString();
|
||||||
|
|
||||||
|
ClosestOfficialSdkPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + $".dotnet-sdk-{downloadedVersion}-{rid}{extension}");
|
||||||
|
Log.LogMessage($"Copying {packageUriPath} to {ClosestOfficialSdkPath}");
|
||||||
|
using (var file = File.Create(ClosestOfficialSdkPath))
|
||||||
|
{
|
||||||
|
await packageResponse.Content.CopyToAsync(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string GetLatestOfficialSdkUrl(string versionString, string rid, string extension)
|
||||||
|
{
|
||||||
|
// Channel in the form of 9.0.1xx
|
||||||
|
var channel = versionString[..5] + "xx";
|
||||||
|
return $"https://aka.ms/dotnet/{channel}/daily/dotnet-sdk-{rid}{extension}";
|
||||||
|
}
|
||||||
|
|
||||||
|
static (string Version, string Rid, string extension) ExtractFromFilePath(string path)
|
||||||
|
{
|
||||||
|
string extension;
|
||||||
|
if (path.EndsWith(".tar.gz"))
|
||||||
|
{
|
||||||
|
extension = ".tar.gz";
|
||||||
|
}
|
||||||
|
else if (path.EndsWith(".zip"))
|
||||||
|
{
|
||||||
|
extension = ".zip";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Invalid archive extension '{path}': must end with .tar.gz or .zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
string filename = Path.GetFileName(path)[..^extension.Length];
|
||||||
|
var dashDelimitedParts = filename.Split('-');
|
||||||
|
var (rid, versionString) = dashDelimitedParts switch
|
||||||
|
{
|
||||||
|
["dotnet", "sdk", var first, var second, var third, var fourth] when PathWithVersions.IsVersionString(first) => (third + '-' + fourth, first + '-' + second),
|
||||||
|
["dotnet", "sdk", var first, var second, var third, var fourth] when PathWithVersions.IsVersionString(third) => (first + '-' + second, third + '-' + fourth),
|
||||||
|
_ => throw new ArgumentException($"Invalid archive file name '{filename}': file name should include full build full build full build full build full build full build full build full build full build version and rid")
|
||||||
|
};
|
||||||
|
|
||||||
|
return (versionString, rid, extension);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>$(NetCurrent)</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- <PackageReference Include="Microsoft.Build.Utilities.Core" PrivateAssets="all" ExcludeAssets="Runtime" Version="$(MicrosoftBuildVersion)" /> -->
|
||||||
|
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="$(MicrosoftBuildVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -0,0 +1,113 @@
|
||||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
public static class PathWithVersions
|
||||||
|
{
|
||||||
|
public const string VersionPlaceholder = "{VERSION}";
|
||||||
|
|
||||||
|
public static bool Equal(string path1, string path2)
|
||||||
|
{
|
||||||
|
if (path1 == path2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadOnlySpan<char> directory = path1;
|
||||||
|
ReadOnlySpan<char> directory2 = path2;
|
||||||
|
while (TryGetPathLeaf(directory, out var root, out var directoryPart) && TryGetPathLeaf(directory2, out var root2, out var directoryPart2))
|
||||||
|
{
|
||||||
|
if (!ReplaceVersionString(directoryPart).SequenceEqual(ReplaceVersionString(directoryPart2)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
directory= Path.GetDirectoryName(directory);
|
||||||
|
directory2= Path.GetDirectoryName(directory2);
|
||||||
|
}
|
||||||
|
if (!directory.IsEmpty || !directory2.IsEmpty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsVersionString(ReadOnlySpan<char> directoryPart)
|
||||||
|
{
|
||||||
|
return directoryPart.Length >= 6
|
||||||
|
&& char.IsDigit(directoryPart[0])
|
||||||
|
&& directoryPart[1] == '.'
|
||||||
|
&& char.IsDigit(directoryPart[2])
|
||||||
|
&& directoryPart[3] == '.'
|
||||||
|
&& char.IsDigit(directoryPart[4])
|
||||||
|
&& ((char.IsDigit(directoryPart[5]) && char.IsDigit(directoryPart[6])) || directoryPart[5] == '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
static ReadOnlySpan<char> ReplaceVersionString(ReadOnlySpan<char> directoryPart)
|
||||||
|
{
|
||||||
|
if (IsVersionString(directoryPart))
|
||||||
|
{
|
||||||
|
return VersionPlaceholder;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return directoryPart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool TryGetPathLeaf(ReadOnlySpan<char> path, out ReadOnlySpan<char> root, out ReadOnlySpan<char> leaf)
|
||||||
|
{
|
||||||
|
if (path.IsEmpty)
|
||||||
|
{
|
||||||
|
root = default;
|
||||||
|
leaf = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
leaf = Path.GetFileName(path);
|
||||||
|
root = Path.GetDirectoryName(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetVersionAnonymousPath(string path)
|
||||||
|
{
|
||||||
|
return GetVersionAnonymousPath(path).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ReadOnlySpan<char> GetVersionAnonymousPath(ReadOnlySpan<char> path)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
bool altered = false;
|
||||||
|
ReadOnlySpan<char> myPath = path;
|
||||||
|
while (TryGetPathLeaf(myPath, out var directory, out var directoryPart))
|
||||||
|
{
|
||||||
|
sb = sb.Insert(0, Path.DirectorySeparatorChar);
|
||||||
|
var versionOrDirectory = ReplaceVersionString(directoryPart);
|
||||||
|
if (versionOrDirectory == VersionPlaceholder)
|
||||||
|
{
|
||||||
|
altered = true;
|
||||||
|
}
|
||||||
|
sb = sb.Insert(0, versionOrDirectory);
|
||||||
|
myPath = directory;
|
||||||
|
}
|
||||||
|
if (!altered)
|
||||||
|
return path;
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ReadOnlySpan<char> GetVersionInPath(ReadOnlySpan<char> path)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<char> myPath = path;
|
||||||
|
while (TryGetPathLeaf(myPath, out var directory, out var directoryPart))
|
||||||
|
{
|
||||||
|
if (IsVersionString(directoryPart))
|
||||||
|
{
|
||||||
|
return directoryPart;
|
||||||
|
}
|
||||||
|
myPath = directory;
|
||||||
|
}
|
||||||
|
throw new ArgumentException("Path does not contain a version");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
static class ArchiveExtensions
|
||||||
|
{
|
||||||
|
public static string[] Lines(this ZipArchiveEntry entry, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
return entry.ReadToString(encoding).Replace("\r\n", "\n").Split('\n').ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ReadToString(this ZipArchiveEntry entry, Encoding? encoding = null)
|
||||||
|
{
|
||||||
|
Stream stream = entry.Open();
|
||||||
|
byte[] buffer = stream.ReadToEnd();
|
||||||
|
// Remove UTF-8 BOM if present
|
||||||
|
int index = 0;
|
||||||
|
if (buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF)
|
||||||
|
{
|
||||||
|
index = 3;
|
||||||
|
}
|
||||||
|
encoding ??= Encoding.UTF8;
|
||||||
|
string fileText = encoding.GetString(buffer, index, buffer.Length - index);
|
||||||
|
return fileText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] ReadToEnd(this Stream stream)
|
||||||
|
{
|
||||||
|
int bufferSize = 2048;
|
||||||
|
byte[] buffer = new byte[bufferSize];
|
||||||
|
int offset = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int bytesRead = stream.Read(buffer, offset, bufferSize - offset);
|
||||||
|
offset += bytesRead;
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (offset == bufferSize)
|
||||||
|
{
|
||||||
|
Array.Resize(ref buffer, bufferSize * 2);
|
||||||
|
bufferSize *= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Array.Resize(ref buffer, offset);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<byte[]> ReadToEndAsync(this Stream stream)
|
||||||
|
{
|
||||||
|
int bufferSize = 2048;
|
||||||
|
byte[] buffer = new byte[bufferSize];
|
||||||
|
int offset = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int bytesRead = await stream.ReadAsync(buffer, offset, bufferSize - offset);
|
||||||
|
offset += bytesRead;
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (offset == bufferSize)
|
||||||
|
{
|
||||||
|
Array.Resize(ref buffer, bufferSize * 2);
|
||||||
|
bufferSize *= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Array.Resize(ref buffer, offset);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue