diff --git a/src/installer/tests/Assets/TestProjects/StandaloneApp6x/Program.cs b/src/installer/tests/Assets/TestProjects/StandaloneApp6x/Program.cs
new file mode 100644
index 0000000000000000000000000000000000000000..2bb065a6e980df1bc2ffbc158f03c29838a5cf91
--- /dev/null
+++ b/src/installer/tests/Assets/TestProjects/StandaloneApp6x/Program.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace StandaloneApp
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ Console.WriteLine("Hello World!");
+ }
+ }
+}
diff --git a/src/installer/tests/Assets/TestProjects/StandaloneApp6x/StandaloneApp6x.csproj b/src/installer/tests/Assets/TestProjects/StandaloneApp6x/StandaloneApp6x.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..c2ba2db55f93464f2563cb10e5ec34509e6274ec
--- /dev/null
+++ b/src/installer/tests/Assets/TestProjects/StandaloneApp6x/StandaloneApp6x.csproj
@@ -0,0 +1,8 @@
+
+
+ StandaloneApp
+ net6.0
+ Exe
+ $(TestTargetRid)
+
+
diff --git a/src/installer/tests/HostActivation.Tests/DependencyResolution/ResolveComponentDependencies.cs b/src/installer/tests/HostActivation.Tests/DependencyResolution/ResolveComponentDependencies.cs
index 63e305a303e9778e2a526f02af4adcd5c2ecf75c..f931594ccd31a5672a265330fc2e1c30bd817b97 100644
--- a/src/installer/tests/HostActivation.Tests/DependencyResolution/ResolveComponentDependencies.cs
+++ b/src/installer/tests/HostActivation.Tests/DependencyResolution/ResolveComponentDependencies.cs
@@ -59,7 +59,7 @@ public void ComponentWithNoDependenciesCaseChangedOnAsm()
// Linux: we fail
// Windows and Mac, probing succeeds but
// Windows: probing returns the original name
- // Mac: probing return the new name including 2 assembly probing with the same new name and the changed deps file
+ // Mac: probing return the new name including 2 assembly probing with the original and new name, and the changed deps file
var component = sharedTestState.ComponentWithNoDependencies.Copy();
@@ -89,7 +89,7 @@ public void ComponentWithNoDependenciesCaseChangedOnAsm()
sharedTestState.RunComponentResolutionTest(component)
.Should().Pass()
.And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success")
- .And.HaveStdOutContaining($"corehost_resolve_component_dependencies assemblies:[{changeFile}{Path.PathSeparator}{changeFile}{Path.PathSeparator}]")
+ .And.HaveStdOutContaining($"corehost_resolve_component_dependencies assemblies:[{component.AppDll}{Path.PathSeparator}{changeFile}{Path.PathSeparator}]")
.And.HaveStdErrContaining($"app_root='{component.Location}{Path.DirectorySeparatorChar}'")
.And.HaveStdErrContaining($"deps='{changeDepsFile}'")
.And.HaveStdErrContaining($"mgd_app='{changeFile}'");
@@ -115,7 +115,7 @@ public void ComponentWithNoDependenciesCaseChangedOnDepsAndAsm()
// Linux: we fail
// Windows and Mac, probing succeeds but
// Windows: probing returns the original name
- // Mac: probing return the new name including 2 assembly probing with the same new name and the changed deps file
+ // Mac: probing return the new name including 2 assembly probing with the original and new name, and the changed deps file
var component = sharedTestState.ComponentWithNoDependencies.Copy();
@@ -146,7 +146,7 @@ public void ComponentWithNoDependenciesCaseChangedOnDepsAndAsm()
sharedTestState.RunComponentResolutionTest(component)
.Should().Pass()
.And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success")
- .And.HaveStdOutContaining($"corehost_resolve_component_dependencies assemblies:[{changeFile}{Path.PathSeparator}{changeFile}{Path.PathSeparator}]")
+ .And.HaveStdOutContaining($"corehost_resolve_component_dependencies assemblies:[{component.AppDll}{Path.PathSeparator}{changeFile}{Path.PathSeparator}]")
.And.HaveStdErrContaining($"app_root='{component.Location}{Path.DirectorySeparatorChar}'")
.And.HaveStdErrContaining($"deps='{changeDepsFile}'")
.And.HaveStdErrContaining($"mgd_app='{changeFile}'");
@@ -242,7 +242,8 @@ public void ComponentWithDependencies()
$"{Path.Combine(sharedTestState.ComponentWithDependencies.Location, "Newtonsoft.Json.dll")}{Path.PathSeparator}]")
.And.HaveStdOutContaining(
$"corehost_resolve_component_dependencies native_search_paths:[" +
- $"{ExpectedProbingPaths(Path.Combine(sharedTestState.ComponentWithDependencies.Location, "runtimes", "win10-x86", "native"))}]");
+ $"{Path.Combine(sharedTestState.ComponentWithDependencies.Location, "runtimes", "win10-x86", "native")}" +
+ $"{Path.DirectorySeparatorChar}{Path.PathSeparator}]");
}
[Fact]
@@ -251,7 +252,6 @@ public void ComponentWithDependenciesAndDependencyRemoved()
var component = sharedTestState.ComponentWithDependencies.Copy();
// Remove a dependency
- // This will cause the resolution to fail
File.Delete(Path.Combine(component.Location, "ComponentDependency.dll"));
sharedTestState.RunComponentResolutionTest(component)
@@ -259,6 +259,7 @@ public void ComponentWithDependenciesAndDependencyRemoved()
.And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success")
.And.HaveStdOutContaining(
$"corehost_resolve_component_dependencies assemblies:[" +
+ $"{Path.Combine(component.Location, "ComponentDependency.dll")}{Path.PathSeparator}" +
$"{component.AppDll}{Path.PathSeparator}" +
$"{Path.Combine(component.Location, "Newtonsoft.Json.dll")}{Path.PathSeparator}]");
}
@@ -369,36 +370,8 @@ public void ComponentWithResourcesShouldReportResourceSearchPaths()
.Should().Pass()
.And.HaveStdOutContaining("corehost_resolve_component_dependencies:Success")
.And.HaveStdOutContaining($"corehost_resolve_component_dependencies resource_search_paths:[" +
- $"{ExpectedProbingPaths(sharedTestState.ComponentWithResources.Location)}]");
- }
-
- private string ExpectedProbingPaths(params string[] paths)
- {
- string result = string.Empty;
- foreach (string path in paths)
- {
- string expectedPath = path;
- if (expectedPath.EndsWith(Path.DirectorySeparatorChar))
- {
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- // On non-windows the paths are normalized to not end with a /
- expectedPath = expectedPath.Substring(0, expectedPath.Length - 1);
- }
- }
- else
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- // On windows all paths are normalized to end with a \
- expectedPath += Path.DirectorySeparatorChar;
- }
- }
-
- result += expectedPath + Path.PathSeparator;
- }
-
- return result;
+ $"{sharedTestState.ComponentWithResources.Location}" +
+ $"{Path.DirectorySeparatorChar}{Path.PathSeparator}]");
}
[Fact]
@@ -425,7 +398,8 @@ public void MultiThreadedComponentDependencyResolutionWhichSucceeeds()
.And.HaveStdOutContaining($"ComponentA: corehost_resolve_component_dependencies assemblies:[{sharedTestState.ComponentWithNoDependencies.AppDll}{Path.PathSeparator}]")
.And.HaveStdOutContaining($"ComponentB: corehost_resolve_component_dependencies:Success")
.And.HaveStdOutContaining($"ComponentB: corehost_resolve_component_dependencies resource_search_paths:[" +
- $"{ExpectedProbingPaths(sharedTestState.ComponentWithResources.Location)}]");
+ $"{sharedTestState.ComponentWithResources.Location}" +
+ $"{Path.DirectorySeparatorChar}{Path.PathSeparator}]");
}
[Fact]
diff --git a/src/installer/tests/HostActivation.Tests/NativeHosting/GetNativeSearchDirectories.cs b/src/installer/tests/HostActivation.Tests/NativeHosting/GetNativeSearchDirectories.cs
index 7f09c70943baee7e417599bcd8068cce6c67fcc7..234530dcae9cf66c2fd9722713d266bfadcc26d2 100644
--- a/src/installer/tests/HostActivation.Tests/NativeHosting/GetNativeSearchDirectories.cs
+++ b/src/installer/tests/HostActivation.Tests/NativeHosting/GetNativeSearchDirectories.cs
@@ -40,7 +40,7 @@ public void BasicApp()
CommandResult result = sharedState.CreateNativeHostCommand(args, sharedState.DotNet.BinPath)
.Execute();
- string pathSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.DirectorySeparatorChar.ToString() : string.Empty;
+ string pathSuffix = Path.DirectorySeparatorChar.ToString();
string expectedSearchDirectories =
Path.GetDirectoryName(sharedState.AppPath) + pathSuffix + Path.PathSeparator +
Path.Combine(sharedState.DotNet.BinPath, "shared", "Microsoft.NETCore.App", SharedTestState.NetCoreAppVersion) + pathSuffix + Path.PathSeparator;
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUsedWithSymbolicLinks.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUsedWithSymbolicLinks.cs
new file mode 100644
index 0000000000000000000000000000000000000000..b36fb4311791f9af64f9f38572d456512e6517a7
--- /dev/null
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUsedWithSymbolicLinks.cs
@@ -0,0 +1,332 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using FluentAssertions;
+using Microsoft.DotNet.Cli.Build.Framework;
+using Microsoft.DotNet.CoreSetup.Test;
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Xunit;
+
+namespace Microsoft.NET.HostModel.Tests
+{
+ public class AppHostUsedWithSymbolicLinks : IClassFixture
+ {
+ private SharedTestState sharedTestState;
+
+ private void CreateSymbolicLink(string source, string target)
+ {
+ if (!SymbolicLinking.MakeSymbolicLink(source, target, out var errorString))
+ throw new Exception($"Failed to create symbolic link '{source}' targeting: '{target}': {errorString}");
+ }
+
+ public AppHostUsedWithSymbolicLinks(AppHostUsedWithSymbolicLinks.SharedTestState fixture)
+ {
+ sharedTestState = fixture;
+ }
+
+ [Theory]
+ [InlineData ("a/b/SymlinkToApphost")]
+ [InlineData ("a/SymlinkToApphost")]
+ public void Run_apphost_behind_symlink(string symlinkRelativePath)
+ {
+ // Creating symbolic links requires administrative privilege on Windows, so skip test.
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
+ var fixture = sharedTestState.StandaloneAppFixture_Published
+ .Copy();
+
+ var appExe = fixture.TestProject.AppExe;
+ var testDir = Directory.GetParent(fixture.TestProject.Location).ToString();
+ Directory.CreateDirectory(Path.Combine(testDir, Path.GetDirectoryName(symlinkRelativePath)));
+ var symlinkFullPath = Path.Combine(testDir, symlinkRelativePath);
+
+ CreateSymbolicLink(symlinkFullPath, appExe);
+ Command.Create(symlinkFullPath)
+ .CaptureStdErr()
+ .CaptureStdOut()
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOutContaining("Hello World");
+ }
+
+ [Theory]
+ [InlineData ("a/b/FirstSymlink", "c/d/SecondSymlink")]
+ [InlineData ("a/b/FirstSymlink", "c/SecondSymlink")]
+ [InlineData ("a/FirstSymlink", "c/d/SecondSymlink")]
+ [InlineData ("a/FirstSymlink", "c/SecondSymlink")]
+ public void Run_apphost_behind_transitive_symlinks(string firstSymlinkRelativePath, string secondSymlinkRelativePath)
+ {
+ // Creating symbolic links requires administrative privilege on Windows, so skip test.
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
+ var fixture = sharedTestState.StandaloneAppFixture_Published
+ .Copy();
+
+ var appExe = fixture.TestProject.AppExe;
+ var testDir = Directory.GetParent(fixture.TestProject.Location).ToString();
+ Directory.CreateDirectory(Path.Combine(testDir, Path.GetDirectoryName(firstSymlinkRelativePath)));
+ Directory.CreateDirectory(Path.Combine(testDir, Path.GetDirectoryName(secondSymlinkRelativePath)));
+
+ // second symlink -> apphost
+ string secondSymbolicLink = Path.Combine(testDir, secondSymlinkRelativePath);
+ CreateSymbolicLink(secondSymbolicLink, appExe);
+
+ // first symlink -> second symlink
+ string firstSymbolicLink = Path.Combine(testDir, firstSymlinkRelativePath);
+ CreateSymbolicLink(firstSymbolicLink, secondSymbolicLink);
+
+ Command.Create(firstSymbolicLink)
+ .CaptureStdErr()
+ .CaptureStdOut()
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOutContaining("Hello World");
+
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ Directory.Delete(firstSymbolicLink);
+ Directory.Delete(secondSymbolicLink);
+ }
+ }
+
+ //[Theory]
+ //[InlineData("a/b/SymlinkToFrameworkDependentApp")]
+ //[InlineData("a/SymlinkToFrameworkDependentApp")]
+ [Fact(Skip = "Currently failing in OSX with \"No such file or directory\" when running Command.Create. " +
+ "CI failing to use stat on symbolic links on Linux (permission denied).")]
+ public void Run_framework_dependent_app_behind_symlink(/* string symlinkRelativePath */)
+ {
+ // Creating symbolic links requires administrative privilege on Windows, so skip test.
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
+ string symlinkRelativePath = string.Empty;
+
+ var fixture = sharedTestState.FrameworkDependentAppFixture_Published
+ .Copy();
+
+ var appExe = fixture.TestProject.AppExe;
+ var builtDotnet = fixture.BuiltDotnet.BinPath;
+ var testDir = Directory.GetParent(fixture.TestProject.Location).ToString();
+ Directory.CreateDirectory(Path.Combine(testDir, Path.GetDirectoryName(symlinkRelativePath)));
+ var symlinkFullPath = Path.Combine(testDir, symlinkRelativePath);
+
+ CreateSymbolicLink(symlinkFullPath, appExe);
+ Command.Create(symlinkFullPath)
+ .CaptureStdErr()
+ .CaptureStdOut()
+ .EnvironmentVariable("DOTNET_ROOT", builtDotnet)
+ .EnvironmentVariable("DOTNET_ROOT(x86)", builtDotnet)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOutContaining("Hello World");
+ }
+
+ [Fact(Skip = "Currently failing in OSX with \"No such file or directory\" when running Command.Create. " +
+ "CI failing to use stat on symbolic links on Linux (permission denied).")]
+ public void Run_framework_dependent_app_with_runtime_behind_symlink()
+ {
+ // Creating symbolic links requires administrative privilege on Windows, so skip test.
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
+ var fixture = sharedTestState.FrameworkDependentAppFixture_Published
+ .Copy();
+
+ var appExe = fixture.TestProject.AppExe;
+ var testDir = Directory.GetParent(fixture.TestProject.Location).ToString();
+ var dotnetSymlink = Path.Combine(testDir, "dotnet");
+ var dotnetDir = fixture.BuiltDotnet.BinPath;
+
+ CreateSymbolicLink(dotnetSymlink, dotnetDir);
+ Command.Create(appExe)
+ .EnvironmentVariable("DOTNET_ROOT", dotnetSymlink)
+ .CaptureStdErr()
+ .CaptureStdOut()
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOutContaining("Hello World");
+ }
+
+ [Fact]
+ public void Put_app_directory_behind_symlink()
+ {
+ // Creating symbolic links requires administrative privilege on Windows, so skip test.
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
+ var fixture = sharedTestState.StandaloneAppFixture_Published
+ .Copy();
+
+ var appExe = fixture.TestProject.AppExe;
+ var binDir = fixture.TestProject.OutputDirectory;
+ var binDirNewPath = Path.Combine(Directory.GetParent(fixture.TestProject.Location).ToString(), "PutTheBinDirSomewhereElse");
+ Directory.Move(binDir, binDirNewPath);
+
+ CreateSymbolicLink(binDir, binDirNewPath);
+ Command.Create(appExe)
+ .CaptureStdErr()
+ .CaptureStdOut()
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOutContaining("Hello World");
+ }
+
+ [Fact]
+ public void Put_dotnet_behind_symlink()
+ {
+ // Creating symbolic links requires administrative privilege on Windows, so skip test.
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
+ var fixture = sharedTestState.StandaloneAppFixture_Published
+ .Copy();
+
+ var appDll = fixture.TestProject.AppDll;
+ var dotnetExe = fixture.BuiltDotnet.DotnetExecutablePath;
+ var testDir = Directory.GetParent(fixture.TestProject.Location).ToString();
+ var dotnetSymlink = Path.Combine(testDir, "dotnet");
+
+ CreateSymbolicLink(dotnetSymlink, dotnetExe);
+ Command.Create(dotnetSymlink, fixture.TestProject.AppDll)
+ .CaptureStdErr()
+ .CaptureStdOut()
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOutContaining("Hello World");
+
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ Directory.Delete(dotnetSymlink);
+ }
+ }
+
+ [Fact]
+ public void Put_app_directory_behind_symlink_and_use_dotnet()
+ {
+ // Creating symbolic links requires administrative privilege on Windows, so skip test.
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
+ var fixture = sharedTestState.StandaloneAppFixture_Published
+ .Copy();
+
+ var dotnet = fixture.BuiltDotnet;
+ var binDir = fixture.TestProject.OutputDirectory;
+ var binDirNewPath = Path.Combine(Directory.GetParent(fixture.TestProject.Location).ToString(), "PutTheBinDirSomewhereElse");
+ Directory.Move(binDir, binDirNewPath);
+
+ CreateSymbolicLink(binDir, binDirNewPath);
+ dotnet.Exec(fixture.TestProject.AppDll)
+ .CaptureStdErr()
+ .CaptureStdOut()
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOutContaining("Hello World");
+ }
+
+ [Fact]
+ public void Put_app_directory_behind_symlink_and_use_dotnet_run()
+ {
+ // Creating symbolic links requires administrative privilege on Windows, so skip test.
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
+ var fixture = sharedTestState.StandaloneAppFixture_Published
+ .Copy();
+
+ var dotnet = fixture.SdkDotnet;
+ var binDir = fixture.TestProject.OutputDirectory;
+ var binDirNewPath = Path.Combine(Directory.GetParent(fixture.TestProject.Location).ToString(), "PutTheBinDirSomewhereElse");
+ Directory.Move(binDir, binDirNewPath);
+
+ CreateSymbolicLink(binDir, binDirNewPath);
+ dotnet.Exec("run")
+ .WorkingDirectory(fixture.TestProject.Location)
+ .CaptureStdErr()
+ .CaptureStdOut()
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOutContaining("Hello World");
+ }
+
+ [Fact]
+ public void Put_satellite_assembly_behind_symlink()
+ {
+ // Creating symbolic links requires administrative privilege on Windows, so skip test.
+ // If enabled, this tests will need to set the console code page to output unicode characters:
+ // Command.Create("chcp 65001").Execute();
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
+ var fixture = sharedTestState.StandaloneAppFixture_Localized
+ .Copy();
+
+ var appExe = fixture.TestProject.AppExe;
+ var binDir = fixture.TestProject.OutputDirectory;
+ var satellitesDir = Path.Combine(Directory.GetParent(fixture.TestProject.Location).ToString(), "PutSatellitesSomewhereElse");
+ Directory.CreateDirectory(satellitesDir);
+
+ var firstSatelliteDir = Directory.GetDirectories(binDir).Single(dir => dir.Contains("kn-IN"));
+ var firstSatelliteNewDir = Path.Combine(satellitesDir, "kn-IN");
+ Directory.Move(firstSatelliteDir, firstSatelliteNewDir);
+ CreateSymbolicLink(firstSatelliteDir, firstSatelliteNewDir);
+
+ var secondSatelliteDir = Directory.GetDirectories(binDir).Single(dir => dir.Contains("ta-IN"));
+ var secondSatelliteNewDir = Path.Combine(satellitesDir, "ta-IN");
+ Directory.Move(secondSatelliteDir, secondSatelliteNewDir);
+ CreateSymbolicLink(secondSatelliteDir, secondSatelliteNewDir);
+
+ Command.Create(appExe)
+ .CaptureStdErr()
+ .CaptureStdOut()
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOutContaining("ನಮಸ್ಕಾರ! வணக்கம்! Hello!");
+ }
+
+ public class SharedTestState : IDisposable
+ {
+ public TestProjectFixture StandaloneAppFixture_Localized { get; }
+ public TestProjectFixture StandaloneAppFixture_Published { get; }
+ public TestProjectFixture FrameworkDependentAppFixture_Published { get; }
+ public RepoDirectoriesProvider RepoDirectories { get; }
+
+ public SharedTestState()
+ {
+ RepoDirectories = new RepoDirectoriesProvider();
+
+ var localizedFixture = new TestProjectFixture("LocalizedApp", RepoDirectories);
+ localizedFixture
+ .EnsureRestoredForRid(localizedFixture.CurrentRid)
+ .PublishProject(runtime: localizedFixture.CurrentRid);
+
+ var publishFixture = new TestProjectFixture("StandaloneApp", RepoDirectories);
+ publishFixture
+ .EnsureRestoredForRid(publishFixture.CurrentRid)
+ .PublishProject(runtime: publishFixture.CurrentRid);
+
+ var fwPublishedFixture = new TestProjectFixture("PortableApp", RepoDirectories);
+ fwPublishedFixture
+ .EnsureRestored()
+ .PublishProject();
+
+ StandaloneAppFixture_Localized = localizedFixture;
+ StandaloneAppFixture_Published = publishFixture;
+ FrameworkDependentAppFixture_Published = fwPublishedFixture;
+ }
+
+ public void Dispose()
+ {
+ StandaloneAppFixture_Localized.Dispose();
+ StandaloneAppFixture_Published.Dispose();
+ FrameworkDependentAppFixture_Published.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/installer/tests/TestUtils/SymbolicLinking.cs b/src/installer/tests/TestUtils/SymbolicLinking.cs
new file mode 100644
index 0000000000000000000000000000000000000000..564a25f7146506f8bc933607b298a5a9d7ba28e2
--- /dev/null
+++ b/src/installer/tests/TestUtils/SymbolicLinking.cs
@@ -0,0 +1,64 @@
+// 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.Runtime.InteropServices;
+
+namespace Microsoft.DotNet.CoreSetup.Test
+{
+ public static class SymbolicLinking
+ {
+ static class Kernel32
+ {
+ [Flags]
+ internal enum SymbolicLinkFlag
+ {
+ IsFile = 0x0,
+ IsDirectory = 0x1,
+ AllowUnprivilegedCreate = 0x2
+ }
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ internal static extern bool CreateSymbolicLink(
+ string symbolicLinkName,
+ string targetFileName,
+ SymbolicLinkFlag flags);
+ }
+
+ static class libc
+ {
+ [DllImport("libc", SetLastError = true)]
+ internal static extern int symlink(
+ string targetFileName,
+ string linkPath);
+
+ [DllImport("libc", CharSet = CharSet.Ansi)]
+ internal static extern IntPtr strerror(int errnum);
+ }
+
+ public static bool MakeSymbolicLink(string symbolicLinkName, string targetFileName, out string errorMessage)
+ {
+ errorMessage = string.Empty;
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ if (!Kernel32.CreateSymbolicLink(symbolicLinkName, targetFileName, Kernel32.SymbolicLinkFlag.IsFile))
+ {
+ int errno = Marshal.GetLastWin32Error();
+ errorMessage = $"CreateSymbolicLink failed with error number {errno}";
+ return false;
+ }
+ }
+ else
+ {
+ if (libc.symlink(targetFileName, symbolicLinkName) == -1)
+ {
+ int errno = Marshal.GetLastWin32Error();
+ errorMessage = Marshal.PtrToStringAnsi(libc.strerror(errno));
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/native/corehost/deps_entry.cpp b/src/native/corehost/deps_entry.cpp
index 58c23a3f0d83218306d43593972010e380ddbe73..e65b1fec590a4dfd35caaf9955b7088d14a7f52a 100644
--- a/src/native/corehost/deps_entry.cpp
+++ b/src/native/corehost/deps_entry.cpp
@@ -30,17 +30,15 @@ static pal::string_t normalize_dir_separator(const pal::string_t& path)
// Parameters:
// base - The base directory to look for the relative path of this entry
// ietf_dir - If this is a resource asset, the IETF intermediate directory
-// look_in_base - Whether to search as a relative path
-// look_in_bundle - Whether to look within the single-file bundle
-// is_servicing - Whether the base directory is the core-servicing directory
// str - (out parameter) If the method returns true, contains the file path for this deps entry
+// search_options - Flags to instruct where to look for this deps entry
// found_in_bundle - (out parameter) True if the candidate is located within the single-file bundle.
//
// Returns:
// If the file exists in the path relative to the "base" directory within the
// single-file or on disk.
-bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_dir, bool look_in_base, bool look_in_bundle, bool is_servicing, pal::string_t* str, bool &found_in_bundle) const
+bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_dir, pal::string_t* str, uint32_t search_options, bool &found_in_bundle) const
{
pal::string_t& candidate = *str;
@@ -58,6 +56,9 @@ bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_
// Reserve space for the path below
candidate.reserve(base.length() + ietf_dir.length() + normalized_path.length() + 3);
+ bool look_in_base = search_options & deps_entry_t::search_options::look_in_base;
+ bool look_in_bundle = search_options & deps_entry_t::search_options::look_in_bundle;
+ bool is_servicing = search_options & deps_entry_t::search_options::is_servicing;
pal::string_t file_path = look_in_base ? get_filename(normalized_path) : normalized_path;
pal::string_t sub_path = ietf_dir;
append_path(&sub_path, file_path.c_str());
@@ -94,36 +95,41 @@ bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_
candidate.assign(base);
append_path(&candidate, sub_path.c_str());
- bool exists = pal::file_exists(candidate);
const pal::char_t* query_type = look_in_base ? _X("Local") : _X("Relative");
- if (!exists)
+ if (search_options & deps_entry_t::search_options::file_existence)
{
- trace::verbose(_X(" %s path query did not exist %s"), query_type, candidate.c_str());
- candidate.clear();
+ if (!pal::file_exists(candidate))
+ {
+ trace::verbose(_X(" %s path query did not exist %s"), query_type, candidate.c_str());
+ candidate.clear();
+ return false;
+ }
+
+ trace::verbose(_X(" %s path query exists %s"), query_type, candidate.c_str());
}
else
{
- trace::verbose(_X(" %s path query exists %s"), query_type, candidate.c_str());
+ trace::verbose(_X(" %s path query %s (skipped file existence check)"), query_type, candidate.c_str());
+ }
- // If a file is resolved to the servicing directory, mark it as disabled in the bundle.
- // This step is necessary because runtime will try to resolve assemblies from the bundle
- // before it uses the TPA. So putting the servicing entry into TPA is not enough, since runtime would
- // resolve it from the bundle first anyway. Disabling the file's entry in the bundle
- // ensures that the servicing entry in the TPA gets priority.
- if (is_servicing && bundle::info_t::is_single_file_bundle())
- {
- bundle::runner_t* app = bundle::runner_t::mutable_app();
- assert(!app->has_base(base));
- assert(!found_in_bundle);
+ // If a file is resolved to the servicing directory, mark it as disabled in the bundle.
+ // This step is necessary because runtime will try to resolve assemblies from the bundle
+ // before it uses the TPA. So putting the servicing entry into TPA is not enough, since runtime would
+ // resolve it from the bundle first anyway. Disabling the file's entry in the bundle
+ // ensures that the servicing entry in the TPA gets priority.
+ if (is_servicing && bundle::info_t::is_single_file_bundle())
+ {
+ bundle::runner_t* app = bundle::runner_t::mutable_app();
+ assert(!app->has_base(base));
+ assert(!found_in_bundle);
- if (app->disable(sub_path))
- {
- trace::verbose(_X(" %s disabled in bundle because of servicing override %s"), sub_path.c_str(), candidate.c_str());
- }
+ if (app->disable(sub_path))
+ {
+ trace::verbose(_X(" %s disabled in bundle because of servicing override %s"), sub_path.c_str(), candidate.c_str());
}
}
- return exists;
+ return true;
}
// -----------------------------------------------------------------------------
@@ -132,12 +138,13 @@ bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_
// Parameters:
// base - The base directory to look for the relative path of this entry
// str - If the method returns true, contains the file path for this deps entry
+// search_options - Flags to instruct where to look for this deps entry
// look_in_bundle - Whether to look within the single-file bundle
//
// Returns:
// If the file exists in the path relative to the "base" directory.
//
-bool deps_entry_t::to_dir_path(const pal::string_t& base, bool look_in_bundle, pal::string_t* str, bool& found_in_bundle) const
+bool deps_entry_t::to_dir_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options, bool& found_in_bundle) const
{
pal::string_t ietf_dir;
@@ -159,7 +166,10 @@ bool deps_entry_t::to_dir_path(const pal::string_t& base, bool look_in_bundle, p
base.c_str(), ietf_dir.c_str(), asset.name.c_str());
}
- return to_path(base, ietf_dir, true, look_in_bundle, false, str, found_in_bundle);
+
+ search_options |= deps_entry_t::search_options::look_in_base;
+ search_options &= ~deps_entry_t::search_options::is_servicing;
+ return to_path(base, ietf_dir, str, search_options, found_in_bundle);
}
// -----------------------------------------------------------------------------
@@ -169,16 +179,16 @@ bool deps_entry_t::to_dir_path(const pal::string_t& base, bool look_in_bundle, p
// Parameters:
// base - The base directory to look for the relative path of this entry
// str - If the method returns true, contains the file path for this deps entry
-// look_in_bundle - Whether to look within the single-file bundle
-// is_servicing - Whether the base directory is the core-servicing directory
+// search_options - Flags to instruct where to look for this deps entry
//
// Returns:
// If the file exists in the path relative to the "base" directory.
//
-bool deps_entry_t::to_rel_path(const pal::string_t& base, bool look_in_bundle, bool is_servicing, pal::string_t* str) const
+bool deps_entry_t::to_rel_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options) const
{
bool found_in_bundle;
- bool result = to_path(base, _X(""), false, look_in_bundle, is_servicing, str, found_in_bundle);
+ search_options &= ~deps_entry_t::search_options::look_in_base;
+ bool result = to_path(base, _X(""), str, search_options, found_in_bundle);
assert(!found_in_bundle);
return result;
}
@@ -190,12 +200,12 @@ bool deps_entry_t::to_rel_path(const pal::string_t& base, bool look_in_bundle, b
// Parameters:
// base - The base directory to look for the relative path of this entry
// str - If the method returns true, contains the file path for this deps entry
-// is_servicing - Whether the base directory is the core-servicing directory
+// search_options - Flags to instruct where to look for this deps entry
//
// Returns:
// If the file exists in the path relative to the "base" directory.
//
-bool deps_entry_t::to_full_path(const pal::string_t& base, bool is_servicing, pal::string_t* str) const
+bool deps_entry_t::to_full_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options) const
{
str->clear();
@@ -217,5 +227,6 @@ bool deps_entry_t::to_full_path(const pal::string_t& base, bool is_servicing, pa
append_path(&new_base, library_path.c_str());
}
- return to_rel_path(new_base, false, is_servicing, str);
+ search_options &= ~deps_entry_t::search_options::look_in_bundle;
+ return to_rel_path(new_base, str, search_options);
}
diff --git a/src/native/corehost/deps_entry.h b/src/native/corehost/deps_entry.h
index db2a32e27d278d422ba30cb070f5442adf1fa446..ae769f799f43bc5b5bb66ae7bd7e77161b208db3 100644
--- a/src/native/corehost/deps_entry.h
+++ b/src/native/corehost/deps_entry.h
@@ -36,6 +36,15 @@ struct deps_entry_t
count
};
+ enum search_options : uint32_t
+ {
+ none = 0x0,
+ look_in_base = 0x1, // Search entry as a relative path
+ look_in_bundle = 0x2, // Look for entry within the single-file bundle
+ is_servicing = 0x4, // Whether the base directory is the core-servicing directory
+ file_existence = 0x8, // Check for entry file existence
+ };
+
static const std::array s_known_asset_types;
pal::string_t deps_file;
@@ -52,18 +61,19 @@ struct deps_entry_t
bool is_rid_specific;
// Given a "base" dir, yield the file path within this directory or single-file bundle.
- bool to_dir_path(const pal::string_t& base, bool look_in_bundle, pal::string_t* str, bool& found_in_bundle) const;
+ bool to_dir_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options, bool& found_in_bundle) const;
// Given a "base" dir, yield the relative path in the package layout or servicing directory.
- bool to_rel_path(const pal::string_t& base, bool look_in_bundle, bool is_servicing, pal::string_t* str) const;
+ bool to_rel_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options) const;
// Given a "base" dir, yield the relative path with package name/version in the package layout or servicing location.
- bool to_full_path(const pal::string_t& base, bool is_servicing, pal::string_t* str) const;
+ bool to_full_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options) const;
private:
// Given a "base" dir, yield the filepath within this directory or relative to this directory based on "look_in_base"
+ // flag in "search_options".
// Returns a path within the single-file bundle, or a file on disk,
- bool to_path(const pal::string_t& base, const pal::string_t& ietf_code, bool look_in_base, bool look_in_bundle, bool is_servicing, pal::string_t* str, bool & found_in_bundle) const;
+ bool to_path(const pal::string_t& base, const pal::string_t& ietf_code, pal::string_t* str, uint32_t search_options, bool & found_in_bundle) const;
};
diff --git a/src/native/corehost/hostpolicy/deps_resolver.cpp b/src/native/corehost/hostpolicy/deps_resolver.cpp
index 08b2967446e393fab86ab66446f5cc884a13e272..5af59fc36ec7837909315e8dbc2f6ce48a524958 100644
--- a/src/native/corehost/hostpolicy/deps_resolver.cpp
+++ b/src/native/corehost/hostpolicy/deps_resolver.cpp
@@ -43,29 +43,28 @@ void add_unique_path(
pal::string_t* non_serviced,
const pal::string_t& svc_dir)
{
- // Resolve sym links.
- pal::string_t real = path;
- pal::realpath(&real);
-
- if (existing->count(real))
+ // To optimize startup time, we avoid calling realpath here.
+ // Because of this, there might be duplicates in the output
+ // whenever path is eiter non-normalized or a symbolic link.
+ if (existing->count(path))
{
return;
}
- trace::verbose(_X("Adding to %s path: %s"), deps_entry_t::s_known_asset_types[asset_type], real.c_str());
+ trace::verbose(_X("Adding to %s path: %s"), deps_entry_t::s_known_asset_types[asset_type], path.c_str());
- if (starts_with(real, svc_dir, false))
+ if (starts_with(path, svc_dir, false))
{
- serviced->append(real);
+ serviced->append(path);
serviced->push_back(PATH_SEPARATOR);
}
else
{
- non_serviced->append(real);
+ non_serviced->append(path);
non_serviced->push_back(PATH_SEPARATOR);
}
- existing->insert(real);
+ existing->insert(path);
}
// Return the filename from deps path; a deps path always uses a '/' for the separator.
@@ -185,6 +184,7 @@ void deps_resolver_t::setup_shared_store_probes(
{
// Shared Store probe: DOTNET_SHARED_STORE environment variable
m_probes.push_back(probe_config_t::lookup(shared));
+ m_needs_file_existence_checks = true;
}
}
@@ -192,6 +192,7 @@ void deps_resolver_t::setup_shared_store_probes(
{
// Path relative to the location of "dotnet.exe" if it's being used to run the app
m_probes.push_back(probe_config_t::lookup(args.dotnet_shared_store));
+ m_needs_file_existence_checks = true;
}
for (const auto& global_shared : args.global_shared_stores)
@@ -200,6 +201,7 @@ void deps_resolver_t::setup_shared_store_probes(
{
// Global store probe: the global location
m_probes.push_back(probe_config_t::lookup(global_shared));
+ m_needs_file_existence_checks = true;
}
}
}
@@ -236,6 +238,8 @@ void deps_resolver_t::setup_probe_config(
pal::string_t ext_pkgs = args.core_servicing;
append_path(&ext_pkgs, _X("pkgs"));
m_probes.push_back(probe_config_t::svc(ext_pkgs));
+
+ m_needs_file_existence_checks = true;
}
// The published deps directory to be probed: either app or FX directory.
@@ -253,10 +257,15 @@ void deps_resolver_t::setup_probe_config(
setup_shared_store_probes(args);
- for (const auto& probe : m_additional_probes)
+ if (m_additional_probes.size() > 0)
{
- // Additional paths
- m_probes.push_back(probe_config_t::lookup(probe));
+ for (const auto& probe : m_additional_probes)
+ {
+ // Additional paths
+ m_probes.push_back(probe_config_t::lookup(probe));
+ }
+
+ m_needs_file_existence_checks = true;
}
if (trace::is_enabled())
@@ -304,6 +313,11 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str
continue;
}
pal::string_t probe_dir = config.probe_dir;
+ uint32_t search_options = deps_entry_t::search_options::none;
+ if (needs_file_existence_checks())
+ {
+ search_options |= deps_entry_t::search_options::file_existence;
+ }
if (config.is_fx())
{
@@ -318,7 +332,7 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str
// If the deps json has the package name and version, then someone has already done rid selection and
// put the right asset in the dir. So checking just package name and version would suffice.
// No need to check further for the exact asset relative sub path.
- if (config.probe_deps_json->has_package(entry.library_name, entry.library_version) && entry.to_dir_path(probe_dir, false, candidate, found_in_bundle))
+ if (config.probe_deps_json->has_package(entry.library_name, entry.library_version) && entry.to_dir_path(probe_dir, candidate, search_options, found_in_bundle))
{
assert(!found_in_bundle);
trace::verbose(_X(" Probed deps json and matched '%s'"), candidate->c_str());
@@ -337,7 +351,7 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str
{
if (entry.is_rid_specific)
{
- if (entry.to_rel_path(deps_dir, true, false, candidate))
+ if (entry.to_rel_path(deps_dir, candidate, search_options | deps_entry_t::search_options::look_in_bundle))
{
trace::verbose(_X(" Probed deps dir and matched '%s'"), candidate->c_str());
return true;
@@ -346,7 +360,7 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str
else
{
// Non-rid assets, lookup in the published dir.
- if (entry.to_dir_path(deps_dir, true, candidate, found_in_bundle))
+ if (entry.to_dir_path(deps_dir, candidate, search_options | deps_entry_t::search_options::look_in_bundle, found_in_bundle))
{
trace::verbose(_X(" Probed deps dir and matched '%s'"), candidate->c_str());
return true;
@@ -356,7 +370,7 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str
trace::verbose(_X(" Skipping... not found in deps dir '%s'"), deps_dir.c_str());
}
- else if (entry.to_full_path(probe_dir, config.only_serviceable_assets, candidate))
+ else if (entry.to_full_path(probe_dir, candidate, search_options | (config.only_serviceable_assets ? deps_entry_t::search_options::is_servicing : 0)))
{
trace::verbose(_X(" Probed package dir and matched '%s'"), candidate->c_str());
return true;
@@ -588,10 +602,7 @@ bool deps_resolver_t::resolve_tpa_list(
// Convert the paths into a string and return it
for (const auto& item : items)
{
- // Workaround for CoreFX not being able to resolve sym links.
- pal::string_t real_asset_path = item.second.resolved_path;
- pal::realpath(&real_asset_path);
- output->append(real_asset_path);
+ output->append(item.second.resolved_path);
output->push_back(PATH_SEPARATOR);
}
diff --git a/src/native/corehost/hostpolicy/deps_resolver.h b/src/native/corehost/hostpolicy/deps_resolver.h
index 76e1739978c3ad710860b8124da2a875b15f4cbb..082cd5cf12309c8b70e4cd5bee3631b23a728189 100644
--- a/src/native/corehost/hostpolicy/deps_resolver.h
+++ b/src/native/corehost/hostpolicy/deps_resolver.h
@@ -52,6 +52,7 @@ public:
, m_managed_app(args.managed_application)
, m_core_servicing(args.core_servicing)
, m_is_framework_dependent(is_framework_dependent)
+ , m_needs_file_existence_checks(false)
{
int lowest_framework = static_cast(m_fx_definitions.size()) - 1;
int root_framework = -1;
@@ -90,6 +91,11 @@ public:
setup_additional_probes(args.probe_paths);
setup_probe_config(args);
+
+ if (m_additional_deps.size() > 0)
+ {
+ m_needs_file_existence_checks = true;
+ }
}
bool valid(pal::string_t* errors)
@@ -172,6 +178,11 @@ public:
return m_is_framework_dependent;
}
+ bool needs_file_existence_checks() const
+ {
+ return m_needs_file_existence_checks;
+ }
+
void get_app_dir(pal::string_t *app_dir) const
{
if (m_host_mode == host_mode_t::libhost)
@@ -272,6 +283,9 @@ private:
// Is the deps file for an app using shared frameworks?
const bool m_is_framework_dependent;
+
+ // File existence checks must be performed for probed paths.This will cause symlinks to be resolved.
+ bool m_needs_file_existence_checks;
};
#endif // DEPS_RESOLVER_H