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
22 changes: 22 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ stages:
displayName: Publish build packages
artifact: BuildPackages

# Test on Linux (both glibc and musl) to verify hostfxr resolution works across distributions
- job: TestLinux
displayName: 'Test on Linux'
pool:
vmImage: ubuntu-latest

strategy:
matrix:
Alpine:
dockerImage: 'mcr.microsoft.com/dotnet/sdk:10.0-alpine'
displayName: 'Alpine (musl)'
Ubuntu:
dockerImage: 'mcr.microsoft.com/dotnet/sdk:10.0'
displayName: 'Ubuntu (glibc)'

steps:
- script: |
docker run --rm -v "$(Build.SourcesDirectory):/src" -w /src $(dockerImage) sh -c '
dotnet test dirs.proj
'
displayName: 'Test in $(displayName) Docker container'

# Keep signing variables in a separate stage
- stage: CodeSign
condition: and(succeeded('Build'), not(eq(variables['build.reason'], 'PullRequest')))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>net472;net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IsPackable>true</IsPackable>

<DevelopmentDependency>true</DevelopmentDependency>
Expand Down
202 changes: 202 additions & 0 deletions src/DotNet.ReproducibleBuilds.Isolated/HostFxrResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
#if NET
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;

namespace DotNet.ReproducibleBuilds.Isolated;

/// <summary>
/// Provides custom resolution for the hostfxr native library.
/// Required for Alpine Linux and other environments where hostfxr is not in the default search paths.
/// Uses AssemblyLoadContext.ResolvingUnmanagedDll event for resolution.
/// </summary>
/// <remarks>
/// Based on https://github.com/microsoft/MSBuildLocator/blob/c16c5354e9fda9703933079528ae67bb5ae4e34e/src/MSBuildLocator/DotNetSdkLocationHelper.cs
/// </remarks>
internal static class HostFxrResolver
{
private static readonly object s_lock = new();
private static bool s_resolverAdded;
private static readonly Lazy<List<string>> s_dotnetPathCandidates = new(ResolveDotnetPathCandidates);

internal const string HostFxrName = "hostfxr";
private static string ExeName => OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet";

/// <summary>
/// Registers a resolver for hostfxr with the current AssemblyLoadContext.
/// This enables hostfxr to be found on Alpine Linux and other environments
/// where it's not in the default search paths.
/// </summary>
public static void Register()
{
lock (s_lock)
{
if (s_resolverAdded)
{
return;
}

s_resolverAdded = true;

// For Windows hostfxr is loaded in the process.
if (OperatingSystem.IsWindows())
{
return;
}

var loadContext = AssemblyLoadContext.GetLoadContext(typeof(HostFxrResolver).Assembly);
if (loadContext != null)
{
loadContext.ResolvingUnmanagedDll += HostFxrResolver_ResolvingUnmanagedDll;
}
}
}

private static IntPtr HostFxrResolver_ResolvingUnmanagedDll(Assembly assembly, string libraryName)
{
// The DllImport hardcoded the name as hostfxr.
if (!libraryName.Equals(HostFxrName, StringComparison.Ordinal))
{
return IntPtr.Zero;
}

string hostFxrLibName = OperatingSystem.IsWindows()
? $"{HostFxrName}.dll"
: OperatingSystem.IsMacOS()
? $"lib{HostFxrName}.dylib"
: $"lib{HostFxrName}.so";

foreach (string dotnetPath in s_dotnetPathCandidates.Value)
{
string hostFxrRoot = Path.Combine(dotnetPath, "host", "fxr");
if (!Directory.Exists(hostFxrRoot))
{
continue;
}

// Get version directories and sort descending (newest first)
string[] versionDirs;
try
{
versionDirs = Directory.GetDirectories(hostFxrRoot);
}
catch
{
continue;
}

Array.Sort(versionDirs, (a, b) =>
{
string versionA = Path.GetFileName(a);
string versionB = Path.GetFileName(b);
if (Version.TryParse(versionA.Split('-')[0], out Version? va) &&
Version.TryParse(versionB.Split('-')[0], out Version? vb))
{
return vb.CompareTo(va);
}
return string.Compare(versionB, versionA, StringComparison.OrdinalIgnoreCase);
});

foreach (string versionDir in versionDirs)
{
string hostFxrAssembly = Path.Combine(versionDir, hostFxrLibName);
if (NativeLibrary.TryLoad(hostFxrAssembly, out IntPtr handle))
{
return handle;
}
}
}

return IntPtr.Zero;
}

private static List<string> ResolveDotnetPathCandidates()
{
var pathCandidates = new List<string>();

// DOTNET_ROOT (architecture-specific on 32-bit)
AddIfValid(GetDotnetPathFromROOT());

// DOTNET_HOST_PATH
string? hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath))
{
AddIfValid(ValidatePath(Path.GetDirectoryName(hostPath)));
}

// DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR
AddIfValid(FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"));

// PATH
AddIfValid(GetDotnetPathFromPATH());

return pathCandidates;

void AddIfValid(string? path)
{
if (!string.IsNullOrEmpty(path) && !pathCandidates.Contains(path!))
{
pathCandidates.Add(path!);
}
}
}

private static string? GetDotnetPathFromROOT()
{
// 32-bit architecture has (x86) suffix
string envVarName = IntPtr.Size == 4 ? "DOTNET_ROOT(x86)" : "DOTNET_ROOT";
return FindDotnetPathFromEnvVariable(envVarName);
}

private static string? GetDotnetPathFromPATH()
{
// We will generally find the dotnet exe on the path, but on linux, it is often just a 'dotnet' symlink
// (possibly even to more symlinks) that we have to resolve to the real dotnet executable.
string[] paths = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? [];
foreach (string dir in paths)
{
string? filePath = ValidatePath(dir);
if (!string.IsNullOrEmpty(filePath))
{
return filePath;
}
}

return null;
}

private static string? FindDotnetPathFromEnvVariable(string environmentVariable)
{
string? dotnetPath = Environment.GetEnvironmentVariable(environmentVariable);
return string.IsNullOrEmpty(dotnetPath) ? null : ValidatePath(dotnetPath);
}

private static string? ValidatePath(string? dotnetPath)
{
if (string.IsNullOrEmpty(dotnetPath))
{
return null;
}

string fullPathToDotnetFromRoot = Path.Combine(dotnetPath!, ExeName);
if (File.Exists(fullPathToDotnetFromRoot))
{
if (!OperatingSystem.IsWindows())
{
string? resolved = File.ResolveLinkTarget(fullPathToDotnetFromRoot, returnFinalTarget: true)?.FullName;
if (!string.IsNullOrEmpty(resolved) && File.Exists(resolved))
{
return Path.GetDirectoryName(resolved);
}
}

return dotnetPath;
}

return null;
}
}
#endif
8 changes: 7 additions & 1 deletion src/DotNet.ReproducibleBuilds.Isolated/Sdk/Sdk.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@
at https://github.com/dotnet/reproducible-builds/blob/main/docs/Reproducible-MSBuild/Techniques/toc.md
-->

<!-- Use net6.0 task for .NET Core, net472 for .NET Framework -->
<UsingTask
Condition="'$(MSBuildRuntimeType)' == 'Core'"
TaskName="DotNet.ReproducibleBuilds.Isolated.ValidateGlobalJsonSdkVersion"
AssemblyFile="$(MSBuildThisFileDirectory)..\tasks\netstandard2.0\DotNet.ReproducibleBuilds.Isolated.dll" />
AssemblyFile="$(MSBuildThisFileDirectory)..\tasks\net6.0\DotNet.ReproducibleBuilds.Isolated.dll" />
<UsingTask
Condition="'$(MSBuildRuntimeType)' != 'Core'"
TaskName="DotNet.ReproducibleBuilds.Isolated.ValidateGlobalJsonSdkVersion"
AssemblyFile="$(MSBuildThisFileDirectory)..\tasks\net472\DotNet.ReproducibleBuilds.Isolated.dll" />

<!--
Restrict the reference assembly search path to avoid machine-specific dependencies. See
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ namespace DotNet.ReproducibleBuilds.Isolated;
/// </remarks>
public class ValidateGlobalJsonSdkVersion : Task
{
#if NET
static ValidateGlobalJsonSdkVersion()
{
// Register resolver for hostfxr to handle Alpine Linux and other non-standard paths
HostFxrResolver.Register();
}
#endif

/// <summary>
/// The working directory to search for global.json.
/// Typically set to $(MSBuildProjectDirectory).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net472;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(TargetFrameworks);net472</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
Expand Down Expand Up @@ -46,8 +47,10 @@
<!-- Copy main project resources to output directory matching NuGet package layout -->
<None Include="$(RepoRoot)/src/DotNet.ReproducibleBuilds.Isolated/Sdk/*.props" CopyToOutputDirectory="PreserveNewest" Link="Sdk\%(Filename)%(Extension)" />
<None Include="$(RepoRoot)/src/DotNet.ReproducibleBuilds.Isolated/Sdk/*.targets" CopyToOutputDirectory="PreserveNewest" Link="Sdk\%(Filename)%(Extension)" />
<None Include="$(RepoRoot)/src/DotNet.ReproducibleBuilds.Isolated/bin/$(Configuration)/netstandard2.0/DotNet.ReproducibleBuilds.Isolated.dll" CopyToOutputDirectory="PreserveNewest" Link="tasks\netstandard2.0\DotNet.ReproducibleBuilds.Isolated.dll" />
<None Include="$(RepoRoot)/src/DotNet.ReproducibleBuilds.Isolated/bin/$(Configuration)/netstandard2.0/DotNet.ReproducibleBuilds.Isolated.deps.json" CopyToOutputDirectory="PreserveNewest" Link="tasks\netstandard2.0\DotNet.ReproducibleBuilds.Isolated.deps.json" />
<None Include="$(RepoRoot)/src/DotNet.ReproducibleBuilds.Isolated/bin/$(Configuration)/net472/DotNet.ReproducibleBuilds.Isolated.dll" CopyToOutputDirectory="PreserveNewest" Link="tasks\net472\DotNet.ReproducibleBuilds.Isolated.dll" />
<None Include="$(RepoRoot)/src/DotNet.ReproducibleBuilds.Isolated/bin/$(Configuration)/net472/DotNet.ReproducibleBuilds.Isolated.deps.json" CopyToOutputDirectory="PreserveNewest" Link="tasks\net472\DotNet.ReproducibleBuilds.Isolated.deps.json" />
<None Include="$(RepoRoot)/src/DotNet.ReproducibleBuilds.Isolated/bin/$(Configuration)/net6.0/DotNet.ReproducibleBuilds.Isolated.dll" CopyToOutputDirectory="PreserveNewest" Link="tasks\net6.0\DotNet.ReproducibleBuilds.Isolated.dll" />
<None Include="$(RepoRoot)/src/DotNet.ReproducibleBuilds.Isolated/bin/$(Configuration)/net6.0/DotNet.ReproducibleBuilds.Isolated.deps.json" CopyToOutputDirectory="PreserveNewest" Link="tasks\net6.0\DotNet.ReproducibleBuilds.Isolated.deps.json" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net472;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(TargetFrameworks);net472</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
Expand Down