Skip to content

Commit

Permalink
9 msbuild integration does not work under linux (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
matzefriedrich authored Jun 6, 2024
1 parent 1189e16 commit 2328544
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 36 deletions.
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [0.3.1-alpha1.240607.1]

### Added

- [PR 10] Adds error handling for MSBuild-related runtime errors (#9)
- [PR 10] Extends the build and SDK services location functionality to properly detect MSBuild dependencies under Linux (#9)

### Fixed

- [PR 10] Fixes MSBuild location under Linux (#9)


## [0.3.0-alpha1.240606.1]

### Added

Expand All @@ -22,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- [PR #8] Fixes broken task factories


## [0.2.0-alpha1.240419.1] - 2024-04-19

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ public async Task AddPackagesFromRepositoryCommand_ProcessRepositoryAsync_does_n
.Setup(task => task.BuildRepositoryPackagesAsync(It.IsAny<RepositoryReference>(), feed, repositoryMock.Object, null, null, It.IsAny<CancellationToken>()))
.Verifiable();

var msBuildToolsLocatorMock = new Mock<IMsBuildToolsLocator>();

var sut = new AddPackagesFromRepositoryCommand(
feedService.Object,
workspace.Object,
msBuildToolsLocatorMock.Object,
OpenRepositoryTaskFactory,
BuildPackagesTaskFactory,
new NullLogger<AddPackagesFromRepositoryCommand>());
Expand Down
4 changes: 4 additions & 0 deletions src/dotnet.nugit/Commands/AddPackagesFromRepositoryCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ namespace dotnet.nugit.Commands
public class AddPackagesFromRepositoryCommand(
INuGetFeedConfigurationService nuGetFeedConfigurationService,
INugitWorkspace workspace,
IMsBuildToolsLocator msBuildToolsLocator,
Func<IOpenRepositoryTask> openRepositoryTaskFactory,
Func<IBuildRepositoryPackagesTask> buildPackagesTaskFactory,
ILogger<AddPackagesFromRepositoryCommand> logger)
Expand All @@ -27,11 +28,14 @@ public class AddPackagesFromRepositoryCommand(
private readonly INuGetFeedConfigurationService nuGetFeedConfigurationService = nuGetFeedConfigurationService ?? throw new ArgumentNullException(nameof(nuGetFeedConfigurationService));
private readonly Func<IOpenRepositoryTask> openRepositoryTaskFactory = openRepositoryTaskFactory ?? throw new ArgumentNullException(nameof(openRepositoryTaskFactory));
private readonly INugitWorkspace workspace = workspace ?? throw new ArgumentNullException(nameof(workspace));
private readonly IMsBuildToolsLocator msBuildToolsLocator = msBuildToolsLocator ?? throw new ArgumentNullException(nameof(msBuildToolsLocator));

public async Task<int> ProcessRepositoryAsync(string repositoryReferenceString, bool headOnly, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(repositoryReferenceString)) throw new ArgumentException(Resources.ArgumentException_Value_cannot_be_null_or_whitespace, nameof(repositoryReferenceString));

this.msBuildToolsLocator.Initialize();

LocalFeedInfo? feed = await this.nuGetFeedConfigurationService.GetConfiguredLocalFeedAsync(cancellationToken);
if (feed == null) return ErrLocalFeedNotFound;

Expand Down
25 changes: 25 additions & 0 deletions src/dotnet.nugit/Services/Workspace/CustomAssemblyLoadContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace dotnet.nugit.Services.Workspace
{
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Loader;

public class CustomAssemblyLoadContext : AssemblyLoadContext
{
private readonly Dictionary<AssemblyName, Assembly> assemblies = new();

protected override Assembly Load(AssemblyName assemblyName)
{
return this.assemblies.GetValueOrDefault(assemblyName)!;
}

public void CacheAssembly(AssemblyName assemblyName, Assembly assembly)
{
ArgumentNullException.ThrowIfNull(assemblyName);
ArgumentNullException.ThrowIfNull(assembly);

this.assemblies.Add(assemblyName, assembly);
}
}
}
1 change: 0 additions & 1 deletion src/dotnet.nugit/Services/Workspace/DotNetProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ public DotNetProject(
{
ArgumentNullException.ThrowIfNull(fileSystem);
ArgumentNullException.ThrowIfNull(project);
ArgumentNullException.ThrowIfNull(projectInstance);

this.fileSystem = fileSystem;
this.project = project;
Expand Down
75 changes: 67 additions & 8 deletions src/dotnet.nugit/Services/Workspace/MsBuildToolsLocatorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,92 @@
{
using System;
using System.Collections.Generic;
using System.IO.Abstractions;
using System.Reflection;
using System.Runtime.Loader;
using Abstractions;
using Microsoft.Extensions.Logging;

public sealed class MsBuildToolsLocatorService(IEnumerable<IMsBuildToolPathLocator> msBuildLocators) : IMsBuildToolsLocator
public sealed class MsBuildToolsLocatorService : IMsBuildToolsLocator, IDisposable
{
private readonly IEnumerable<IMsBuildToolPathLocator> msBuildLocators = msBuildLocators ?? throw new ArgumentNullException(nameof(msBuildLocators));
private readonly CustomAssemblyLoadContext assemblyLoadContext;
private readonly IFileSystem fileSystem;
private readonly ILogger<MsBuildToolsLocatorService> logger;
private readonly IEnumerable<IMsBuildToolPathLocator> msBuildLocators;
private bool initialized;
private string? path;
private string? msBuildToolsPath;

public MsBuildToolsLocatorService(
IFileSystem fileSystem,
IEnumerable<IMsBuildToolPathLocator> msBuildLocators,
ILogger<MsBuildToolsLocatorService> logger)
{
this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
this.msBuildLocators = msBuildLocators ?? throw new ArgumentNullException(nameof(msBuildLocators));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.assemblyLoadContext = new CustomAssemblyLoadContext();
}

public void Dispose()
{
AssemblyLoadContext.Default.Resolving -= this.DefaultOnResolving;
}

public string? LocateMsBuildToolsPath()
{
if (this.initialized) return this.path;
if (this.initialized) return this.msBuildToolsPath;

foreach (IMsBuildToolPathLocator toolPathLocator in this.msBuildLocators)
{
if (!toolPathLocator.TryLocateMsBuildToolsPath(out string? path))
if (!toolPathLocator.TryLocateMsBuildToolsPath(out string? resolvedPath))
continue;

this.path = path;
this.msBuildToolsPath = resolvedPath;
this.initialized = true;
}

return this.path;
return this.msBuildToolsPath;
}

public void Initialize()
{
this.LocateMsBuildToolsPath();
AssemblyLoadContext.Default.Resolving += this.DefaultOnResolving;

string? buildToolsPath = this.LocateMsBuildToolsPath();
if (buildToolsPath == null) return;
}

private Assembly? DefaultOnResolving(AssemblyLoadContext context, AssemblyName assemblyName)
{
if (this.initialized == false || string.IsNullOrWhiteSpace(this.msBuildToolsPath)) return null;

this.logger.LogDebug("Resolution of assembly {AssemblyName} has failed.", assemblyName.Name);

string executionAssemblyLocation = Assembly.GetExecutingAssembly().Location;
string? installationPath = this.fileSystem.Path.GetDirectoryName(executionAssemblyLocation);
string?[] paths = { installationPath, this.msBuildToolsPath };
foreach (string? p in paths)
{
Assembly? assembly = this.LoadAssemblyFromPath(p, assemblyName);
if (assembly != null) return assembly;
}

return null;
}

private Assembly? LoadAssemblyFromPath(string? path, AssemblyName assemblyName)
{
if (string.IsNullOrWhiteSpace(path)) return null;
string assemblyPath = this.fileSystem.Path.Combine(path, $"{assemblyName.Name}.dll");
if (this.fileSystem.File.Exists(assemblyPath))
{
this.logger.LogInformation("Loading {AssemblyName} from path: {Path}", assemblyName.Name, path);
Assembly assembly = this.assemblyLoadContext.LoadFromAssemblyPath(assemblyPath);
this.assemblyLoadContext.CacheAssembly(assemblyName, assembly);
return assembly;
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public void Shutdown()
}

public LoggerVerbosity Verbosity { get; set; }
public string Parameters { get; set; } = null!;
public string? Parameters { get; set; }

private void HandleAnyEvent(object sender, BuildEventArgs? e)
{
Expand Down
33 changes: 21 additions & 12 deletions src/dotnet.nugit/Services/Workspace/ProjectWorkspaceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace dotnet.nugit.Services.Workspace
using System.Xml;
using Abstractions;
using Microsoft.Build.Evaluation.Context;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Execution;
using Microsoft.Build.FileSystem;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -36,9 +37,11 @@ public ProjectWorkspaceManager(
this.msBuildLogger = new ProjectWorkspaceLogger(workspaceLogger);
}

public async Task<IDotNetProject> LoadProjectAsync(string projectFile, string configurationName, CancellationToken cancellationToken)
public async Task<IDotNetProject> LoadProjectAsync(string projectFile, string configurationName,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(projectFile)) throw new ArgumentException(Resources.ArgumentException_Value_cannot_be_null_or_whitespace, nameof(projectFile));
if (string.IsNullOrWhiteSpace(projectFile))
throw new ArgumentException(Resources.ArgumentException_Value_cannot_be_null_or_whitespace, nameof(projectFile));

string? msBuildToolsPath = this.msBuildToolsLocator.LocateMsBuildToolsPath();
if (string.IsNullOrWhiteSpace(msBuildToolsPath))
Expand All @@ -48,27 +51,33 @@ public async Task<IDotNetProject> LoadProjectAsync(string projectFile, string co

var workspace = MSBuildWorkspace.Create();
Project project = await workspace.OpenProjectAsync(projectFile, this.msBuildLogger, new Progress<ProjectLoadProgress>(), cancellationToken);
Compilation? compilation = await project.GetCompilationAsync(cancellationToken);
if (compilation == null) return DotNetProject.Empty;

ProjectInstance projectInstance = await CreateProjectInstance(projectFile);
ProjectInstance? projectInstance = await this.CreateProjectInstance(projectFile);

return new DotNetProject(this.fileSystem, project, projectInstance);
}

private static async Task<ProjectInstance> CreateProjectInstance(string projectFile)
private async Task<ProjectInstance?> CreateProjectInstance(string projectFile)
{
await using Stream input = File.OpenRead(projectFile);
using var reader = XmlReader.Create(input);

var project = new Microsoft.Build.Evaluation.Project(reader);
try
{
var project = new Microsoft.Build.Evaluation.Project(reader);

const ProjectInstanceSettings settings = ProjectInstanceSettings.Immutable;
MSBuildFileSystemBase fs = new DefaultMsBuildFileSystem();
var evaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.Shared, fs);
ProjectInstance? projectInstance = project.CreateProjectInstance(settings, evaluationContext);
const ProjectInstanceSettings settings = ProjectInstanceSettings.Immutable;
MSBuildFileSystemBase fs = new DefaultMsBuildFileSystem();
var evaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.Shared, fs);
ProjectInstance? projectInstance = project.CreateProjectInstance(settings, evaluationContext);

return projectInstance;
return projectInstance;
}
catch (InvalidProjectFileException e)
{
this.logger.LogError(e, "Cannot load project file for evaluation.");
return null;
}
}
}
}
22 changes: 10 additions & 12 deletions src/dotnet.nugit/dotnet.nugit.csproj
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyVersion>0.1.0</AssemblyVersion>
<FileVersion>0.1.2</FileVersion>
<AssemblyName>dotnet-nugit</AssemblyName>
<AssemblyVersion>0.3.1</AssemblyVersion>
<FileVersion>0.3.1</FileVersion>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>dotnet-nugit</AssemblyName>
</PropertyGroup>

<PropertyGroup>
Expand All @@ -22,7 +22,7 @@
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/matzefriedrich/dotnet-nugit</RepositoryUrl>
<Title>dotnet nugit</Title>
<Version>0.2.0-alpha1.240419.1</Version>
<Version>0.3.1-alpha1.240607.1</Version>
</PropertyGroup>

<PropertyGroup>
Expand All @@ -46,22 +46,20 @@

<ItemGroup>
<PackageReference Include="LibGit2Sharp" Version="0.30.0"/>
<PackageReference Include="Microsoft.Build" Version="17.9.5" ExcludeAssets="runtime"/>
<PackageReference Include="Microsoft.Build.Locator" Version="1.7.8"/>
<PackageReference Include="Microsoft.Build" Version="17.10.4" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Framework" Version="17.10.4" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.7.8" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.9.2"/>
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.9.2"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0"/>
<PackageReference Include="NuGet.Common" Version="6.9.1"/>
<PackageReference Include="NuGet.Configuration" Version="6.9.1"/>
<PackageReference Include="NuGet.Packaging" Version="6.9.1"/>
<PackageReference Include="NuGet.ProjectModel" Version="6.9.1"/>
<PackageReference Include="Serilog" Version="3.1.1"/>
<PackageReference Include="NuGet.Packaging" Version="6.10.0" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
<PackageReference Include="System.IO.Abstractions" Version="21.0.2"/>
<PackageReference Include="System.Linq.Async" Version="6.0.1"/>
<PackageReference Include="YamlDotNet" Version="15.1.2"/>
<PackageReference Include="YamlDotNet" Version="15.1.6" />
</ItemGroup>

<ItemGroup>
Expand Down

0 comments on commit 2328544

Please sign in to comment.