Skip to content

Commit

Permalink
feat: custom MSBuild SDK
Browse files Browse the repository at this point in the history
  • Loading branch information
shlomiassaf authored Oct 8, 2022
1 parent 2e0065e commit 002cc49
Show file tree
Hide file tree
Showing 21 changed files with 1,272 additions and 15 deletions.
30 changes: 30 additions & 0 deletions Affected.sln
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetAffected.Testing.Util
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetAffected.Core.Tests", "test\DotnetAffected.Core.Tests\DotnetAffected.Core.Tests.csproj", "{3F8A1C4F-1A46-4898-91CB-A67AD8DE301C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetAffected.Tasks", "src\DotnetAffected.Tasks\DotnetAffected.Tasks.csproj", "{4376F798-3215-4CB7-873E-0D7EB090A5B2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetAffected.Tasks.Tests", "test\DotnetAffected.Tasks.Tests\DotnetAffected.Tasks.Tests.csproj", "{3F078C7E-2584-47E9-BE40-8B3375B06962}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -120,6 +124,30 @@ Global
{3F8A1C4F-1A46-4898-91CB-A67AD8DE301C}.Release|x64.Build.0 = Release|Any CPU
{3F8A1C4F-1A46-4898-91CB-A67AD8DE301C}.Release|x86.ActiveCfg = Release|Any CPU
{3F8A1C4F-1A46-4898-91CB-A67AD8DE301C}.Release|x86.Build.0 = Release|Any CPU
{4376F798-3215-4CB7-873E-0D7EB090A5B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4376F798-3215-4CB7-873E-0D7EB090A5B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4376F798-3215-4CB7-873E-0D7EB090A5B2}.Debug|x64.ActiveCfg = Debug|Any CPU
{4376F798-3215-4CB7-873E-0D7EB090A5B2}.Debug|x64.Build.0 = Debug|Any CPU
{4376F798-3215-4CB7-873E-0D7EB090A5B2}.Debug|x86.ActiveCfg = Debug|Any CPU
{4376F798-3215-4CB7-873E-0D7EB090A5B2}.Debug|x86.Build.0 = Debug|Any CPU
{4376F798-3215-4CB7-873E-0D7EB090A5B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4376F798-3215-4CB7-873E-0D7EB090A5B2}.Release|Any CPU.Build.0 = Release|Any CPU
{4376F798-3215-4CB7-873E-0D7EB090A5B2}.Release|x64.ActiveCfg = Release|Any CPU
{4376F798-3215-4CB7-873E-0D7EB090A5B2}.Release|x64.Build.0 = Release|Any CPU
{4376F798-3215-4CB7-873E-0D7EB090A5B2}.Release|x86.ActiveCfg = Release|Any CPU
{4376F798-3215-4CB7-873E-0D7EB090A5B2}.Release|x86.Build.0 = Release|Any CPU
{3F078C7E-2584-47E9-BE40-8B3375B06962}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F078C7E-2584-47E9-BE40-8B3375B06962}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F078C7E-2584-47E9-BE40-8B3375B06962}.Debug|x64.ActiveCfg = Debug|Any CPU
{3F078C7E-2584-47E9-BE40-8B3375B06962}.Debug|x64.Build.0 = Debug|Any CPU
{3F078C7E-2584-47E9-BE40-8B3375B06962}.Debug|x86.ActiveCfg = Debug|Any CPU
{3F078C7E-2584-47E9-BE40-8B3375B06962}.Debug|x86.Build.0 = Debug|Any CPU
{3F078C7E-2584-47E9-BE40-8B3375B06962}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F078C7E-2584-47E9-BE40-8B3375B06962}.Release|Any CPU.Build.0 = Release|Any CPU
{3F078C7E-2584-47E9-BE40-8B3375B06962}.Release|x64.ActiveCfg = Release|Any CPU
{3F078C7E-2584-47E9-BE40-8B3375B06962}.Release|x64.Build.0 = Release|Any CPU
{3F078C7E-2584-47E9-BE40-8B3375B06962}.Release|x86.ActiveCfg = Release|Any CPU
{3F078C7E-2584-47E9-BE40-8B3375B06962}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{1A7D0E97-544D-4162-8361-1F631D798E76} = {4881D1F3-A668-4615-BC07-60BBD6718A87}
Expand All @@ -129,5 +157,7 @@ Global
{AD4FD7CF-1334-4976-8657-D5B44E599149} = {7D522C9E-5903-4BE4-81D9-866769469A0C}
{7B1B393C-5BA3-4078-A609-AE22B46EFD7B} = {DAE457FC-8DA7-4A5E-8327-F429CAED3BD8}
{3F8A1C4F-1A46-4898-91CB-A67AD8DE301C} = {DAE457FC-8DA7-4A5E-8327-F429CAED3BD8}
{4376F798-3215-4CB7-873E-0D7EB090A5B2} = {4881D1F3-A668-4615-BC07-60BBD6718A87}
{3F078C7E-2584-47E9-BE40-8B3375B06962} = {DAE457FC-8DA7-4A5E-8327-F429CAED3BD8}
EndGlobalSection
EndGlobal
118 changes: 118 additions & 0 deletions src/DotnetAffected.Tasks/AffectedTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System;
using System.Linq;
using DotnetAffected.Abstractions;
using DotnetAffected.Core;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.Collections;
using System.Collections.Generic;

namespace DotnetAffected.Tasks
{
public class AffectedTask : Microsoft.Build.Utilities.Task
{
[Required]
public string Root { get; set; }

[Required]
public ITaskItem[] Projects { get; set; }

public ITaskItem[] AssumeChanges { get; set; }

public ITaskItem[]? FilterClasses { get; set; }

[Output]
public ITaskItem[] FilterInstances { get; private set; }

[Output]
public string[] ModifiedProjects { get; private set; }

[Output]
public int ModifiedProjectsCount { get; private set; }

public override bool Execute()
{
try
{
var affectedOptions = new AffectedOptions(Root);

var graph = new ProjectGraphFactory(affectedOptions).BuildProjectGraph();
IChangesProvider changesProvider = AssumeChanges?.Any() == true
? new AssumptionChangesProvider(graph, AssumeChanges.Select(c => c.ItemSpec))
: new GitChangesProvider();

var executor = new AffectedExecutor(affectedOptions,
graph,
changesProvider,
new PredictionChangedProjectsProvider(graph, affectedOptions));

var results = executor.Execute();
var modifiedProjectInstances = new HashSet<ProjectInstance>();
var modifiedProjects = new List<string>();
var filterInstances = new List<ITaskItem>();
var filterTypes = BuildFilterClassMetadata();

foreach (var node in results.ProjectsWithChangedFiles.Concat(results.AffectedProjects))
{
if (modifiedProjectInstances.Add(node.ProjectInstance))
{
modifiedProjects.Add(node.ProjectInstance.FullPath);

if (filterTypes.Length > 0)
{
var projectInstance = node.ProjectInstance;
foreach (var filterType in filterTypes)
{
var taskItem = new TaskItem(projectInstance.FullPath);
filterInstances.Add(taskItem);

foreach (var kvp in filterType)
taskItem.SetMetadata(kvp.Key, projectInstance.GetProperty(kvp.Key)?.EvaluatedValue ?? kvp.Value);
}
}
}
}

FilterInstances = filterInstances.ToArray();
ModifiedProjects = modifiedProjects.ToArray();
ModifiedProjectsCount = ModifiedProjects.Length;
}
catch (Exception? e)
{
while (e is not null)
{
Log.LogErrorFromException(e);
e = e.InnerException;
}
}

return !Log.HasLoggedErrors;
}

private Dictionary<string, string>[] BuildFilterClassMetadata()
{
Dictionary<string, string> Selector(ITaskItem filter)
{
var t = new Dictionary<string, string>();
foreach (var obj in filter.CloneCustomMetadata())
{
var entry = (DictionaryEntry)obj;
t[(string)entry.Key] = entry.Value as string ?? "";
}

t["AffectedFilterClassName"] = filter.ItemSpec;
return t;
}

return FilterClasses is null
? Array.Empty<Dictionary<string, string>>()
: FilterClasses.Select(Selector).ToArray();
}

static AffectedTask()
{
Lib2GitNativePathHelper.ResolveCustomNativeLibraryPath();
}
}
}
83 changes: 83 additions & 0 deletions src/DotnetAffected.Tasks/DotnetAffected.Tasks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="$(PackageDefaultsPropsPath)" />

<PropertyGroup>
<RootNamespace>DotnetAffected.Tasks</RootNamespace>

<ExcludeFromSourceBuild>true</ExcludeFromSourceBuild>

<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PackageType>MSBuildSdk</PackageType>
<IncludeBuildOutput>false</IncludeBuildOutput>
<IsPackable>true</IsPackable>

<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<PackTasks>true</PackTasks>
</PropertyGroup>

<ItemGroup>
<Compile Include="AffectedTask.cs" />
<Compile Include="Lib2GitNativePathHelper.cs" />
<None Include="Sdk\**" Pack="true" PackagePath="Sdk\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build.Prediction" />

<PackageReference Include="Microsoft.Build.Framework" Publish="false" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Publish="false" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(SourcesPath)/DotnetAffected.Core/DotnetAffected.Core.csproj" />
</ItemGroup>

<ItemGroup>
<!--
Update all PackageReference and ProjectReference Items to have
PrivateAssets="All" and default Publish to true.
This removes the dependency nodes from the generated nuspec and
forces the publish output to contain the dlls.
-->
<PackageReference Update="@(PackageReference)">
<PrivateAssets>All</PrivateAssets>
<Publish Condition="'%(PackageReference.Publish)' == ''">true</Publish>
<ExcludeAssets Condition="'%(PackageReference.Publish)' == 'false'">runtime</ExcludeAssets>
</PackageReference>
<ProjectReference Update="@(ProjectReference)">
<PrivateAssets>All</PrivateAssets>
<Publish Condition="'%(ProjectReference.Publish)' == ''">true</Publish>
</ProjectReference>

<!--
Do not include assemblies that MSBuild ships with in the package.
-->
<PackageReference Update="Microsoft.Build" PrivateAssets="All" Publish="false" ExcludeAssets="runtime" />
<PackageReference Update="Microsoft.Build.Utilities.Core" PrivateAssets="All" Publish="false" ExcludeAssets="runtime" />
</ItemGroup>

<PropertyGroup>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);_AddBuildOutputToPackageCore</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>

<Target Name="_AddBuildOutputToPackageCore" DependsOnTargets="Publish">
<!--
Create a clone for OSX/Linux libraries that starts with "lib2git".
The clone will have the first 3 characters of the filename removed ("lib").
This is required to load the libraries since LibGit2Sharp will search for them without the "lib"
-->
<ItemGroup>
<NativeLibGitToCopy Include="$(PublishDir)runtimes/**/native/libgit2*.*" />
</ItemGroup>
<Copy SourceFiles="@(NativeLibGitToCopy)" DestinationFiles="@(NativeLibGitToCopy-&gt;Replace('libgit2','git2'))" />

<!--
Publish .NET Core assets and include them in the package under tools directory.
-->
<ItemGroup>
<TfmSpecificPackageFile Include="$(PublishDir)**" PackagePath="tools/$(TargetFramework)/%(RecursiveDir)%(FileName)%(Extension)" />
</ItemGroup>
</Target>
</Project>
60 changes: 60 additions & 0 deletions src/DotnetAffected.Tasks/Lib2GitNativePathHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.IO;
using System.Runtime.InteropServices;

namespace DotnetAffected.Tasks
{
public static class Lib2GitNativePathHelper
{
public static void ResolveCustomNativeLibraryPath()
{
var assemblyDirectory = Path.GetDirectoryName(typeof(LibGit2Sharp.GlobalSettings).Assembly.Location);
var runtimesDirectory = Path.Combine(assemblyDirectory ?? "", "runtimes");

if (!Directory.Exists(runtimesDirectory))
return;

var processorArchitecture = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();

var (os, libExtension) = GetNativeInfo();

foreach (var runtimeFolder in Directory.GetDirectories(runtimesDirectory, $"{os}*-{processorArchitecture}"))
{
var libFolder = Path.Combine(runtimeFolder, "native");

foreach (var libFilePath in Directory.GetFiles(libFolder, $"*{libExtension}"))
{
if (IsLibraryLoadable(libFilePath))
{
LibGit2Sharp.GlobalSettings.NativeLibraryPath = libFolder;
return;
}

}
}
}

private static (string Os, string LibExtension) GetNativeInfo()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return ("linux", ".so");
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return ("osx", ".dylib");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return ("win", ".dll");

throw new PlatformNotSupportedException();
}

private static bool IsLibraryLoadable(string libPath)
{
if (File.Exists(libPath) && NativeLibrary.TryLoad(libPath, out var ptr))
{
NativeLibrary.Free(ptr);
return true;
}
return false;
}

}
}
27 changes: 27 additions & 0 deletions src/DotnetAffected.Tasks/LibGit2Sharp-Integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@


`DotnetAffected.Core` uses `LibGit2Sharp` to analyse changes using the git history/log.

`LibGit2Sharp` uses native, os based, git libraries to perform all git operations.

Loading of the native libraries is based on predefined locations relative to the application/assembly.
It works fine when used in the context of an application.
With a Task however, it does not.

A task is loaded in the context of the build running it.
When the task activates `LibGit2Sharp`, it will try to load the native libraries, however in the
context of the build application which does not have access to the native libraries.

This is known issue when using dependencies in a build library using Tasks.
The solution is to omit dependency declaration and instead physically include them with the
shipped library.

With `LibGit2Sharp` we also need to tell it where to look for the native libraries.
`LibGit2Sharp`' will load the file from the path we provide, however for osx/linux it will
try to load a file which does not exists since it comes with a `lib` prefix which the
os native library resolver knows how to handle but with custom load it fails.

To workaround that we just add an additional build step to the library to also have a copy
of the native library without the `lib` prefix.

So we have 2 files for each implementation, for example: `libgit2-XYZ.so` and `git2-XYZ.so`
Loading

0 comments on commit 002cc49

Please sign in to comment.