diff --git a/src/installer/tests/HostActivation.Tests/DependencyResolution/DependencyResolutionCommandResultExtensions.cs b/src/installer/tests/HostActivation.Tests/DependencyResolution/DependencyResolutionCommandResultExtensions.cs index 7e057cff09cec4..ed3335d00e4fc5 100644 --- a/src/installer/tests/HostActivation.Tests/DependencyResolution/DependencyResolutionCommandResultExtensions.cs +++ b/src/installer/tests/HostActivation.Tests/DependencyResolution/DependencyResolutionCommandResultExtensions.cs @@ -12,8 +12,9 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.DependencyResolution public static class DependencyResolutionCommandResultExtensions { // App asset resolution extensions - public const string TRUSTED_PLATFORM_ASSEMBLIES = "TRUSTED_PLATFORM_ASSEMBLIES"; - public const string NATIVE_DLL_SEARCH_DIRECTORIES = "NATIVE_DLL_SEARCH_DIRECTORIES"; + private const string TRUSTED_PLATFORM_ASSEMBLIES = nameof(TRUSTED_PLATFORM_ASSEMBLIES); + private const string NATIVE_DLL_SEARCH_DIRECTORIES = nameof(NATIVE_DLL_SEARCH_DIRECTORIES); + private const string PLATFORM_RESOURCE_ROOTS = nameof(PLATFORM_RESOURCE_ROOTS); public static AndConstraint HaveRuntimePropertyContaining(this CommandResultAssertions assertion, string propertyName, params string[] values) { @@ -67,6 +68,16 @@ public static AndConstraint NotHaveResolvedNativeLibrar return assertion.NotHaveRuntimePropertyContaining(NATIVE_DLL_SEARCH_DIRECTORIES, RelativePathsToAbsoluteAppPaths(path, app)); } + public static AndConstraint HaveResolvedResourceRootPath(this CommandResultAssertions assertion, string path, TestApp app = null) + { + return assertion.HaveRuntimePropertyContaining(PLATFORM_RESOURCE_ROOTS, RelativePathsToAbsoluteAppPaths(path, app)); + } + + public static AndConstraint NotHaveResolvedResourceRootPath(this CommandResultAssertions assertion, string path, TestApp app = null) + { + return assertion.NotHaveRuntimePropertyContaining(PLATFORM_RESOURCE_ROOTS, RelativePathsToAbsoluteAppPaths(path, app)); + } + // Component asset resolution extensions private const string assemblies = "assemblies"; private const string native_search_paths = "native_search_paths"; diff --git a/src/installer/tests/HostActivation.Tests/DependencyResolution/LocalPath.cs b/src/installer/tests/HostActivation.Tests/DependencyResolution/LocalPath.cs new file mode 100644 index 00000000000000..bad9c83eacede9 --- /dev/null +++ b/src/installer/tests/HostActivation.Tests/DependencyResolution/LocalPath.cs @@ -0,0 +1,265 @@ +// 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.IO; +using Microsoft.DotNet.Cli.Build; +using Xunit; +using static Microsoft.DotNet.CoreSetup.Test.NetCoreAppBuilder; + +namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.DependencyResolution +{ + public class LocalPath : IClassFixture + { + private readonly SharedTestState sharedState; + + public LocalPath(SharedTestState sharedState) + { + this.sharedState = sharedState; + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void RuntimeAssemblies_FrameworkDependent(bool useLocalPath) => RuntimeAssemblies(isSelfContained: false, useLocalPath); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void RuntimeAssemblies_SelfContained(bool useLocalPath) => RuntimeAssemblies(isSelfContained: true, useLocalPath); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void NativeLibraries_FrameworkDependent(bool useLocalPath) => NativeLibraries(isSelfContained: false, useLocalPath); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void NativeLibraries_SelfContained(bool useLocalPath) => NativeLibraries(isSelfContained: true, useLocalPath); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ResourceAssemblies_FrameworkDependent(bool useLocalPath) => ResourceAssemblies(isSelfContained: false, useLocalPath); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ResourceAssemblies_SelfContained(bool useLocalPath) => ResourceAssemblies(isSelfContained: true, useLocalPath); + + private void RuntimeAssemblies(bool isSelfContained, bool useLocalPath) + { + RuntimeLibraryType[] libraryTypes = [ RuntimeLibraryType.project, RuntimeLibraryType.package, RuntimeLibraryType.runtimepack ]; + + Action customizer = b => + { + foreach (var libraryType in libraryTypes) + { + string library = $"Test{libraryType}"; + (string path, string localPath) = GetPaths(libraryType, false); + b.WithRuntimeLibrary(libraryType, library, "1.0.0", p => p + .WithAssemblyGroup(null, g => g + .WithAsset(path, useLocalPath ? f => f.WithLocalPath(localPath) : null))); + + if (!isSelfContained) + { + // Add RID-specific assembly + (string ridPath, string localRidPath) = GetPaths(libraryType, true); + b.WithRuntimeLibrary(libraryType, $"{library}-{TestContext.BuildRID}", "1.0.0", p => p + .WithAssemblyGroup(TestContext.BuildRID, g => g + .WithAsset(ridPath, useLocalPath ? f => f.WithLocalPath(localRidPath) : null))); + } + } + + b.WithLocalPathsInDepsJson(useLocalPath); + }; + + using TestApp app = CreateApp(isSelfContained, customizer); + var result = sharedState.DotNetWithNetCoreApp.Exec(app.AppDll) + .EnableTracingAndCaptureOutputs() + .Execute(); + result.Should().Pass(); + + // Check all library types + foreach (var libraryType in libraryTypes) + { + // Check RID-agnostic assembly + (string path, string localPath) = GetPaths(libraryType, false); + + // Without localPath, RID-agnostic non-runtimepack runtime assemblies are assumed to be in + string relativePath = useLocalPath + ? localPath + : libraryType == RuntimeLibraryType.runtimepack ? path : Path.GetFileName(path); + string expectedPath = Path.Join(app.Location, relativePath); + result.Should().HaveResolvedAssembly(expectedPath); + if (useLocalPath) + { + result.Should().NotHaveResolvedAssembly(Path.Join(app.Location, path)); + } + + // Check RID-specific assembly + if (!isSelfContained) + { + (string ridPath, string localRidPath) = GetPaths(libraryType, true); + string expectedRidPath = Path.Join(app.Location, useLocalPath ? localRidPath : ridPath); + result.Should().HaveResolvedAssembly(expectedRidPath); + if (useLocalPath) + { + result.Should().NotHaveResolvedAssembly(Path.Join(app.Location, ridPath)); + } + } + } + + static (string Path, string LocalPath) GetPaths(RuntimeLibraryType libraryType, bool useRid) + { + string library = $"Test{libraryType}"; + string path = useRid ? $"lib/{TestContext.BuildRID}/{library}-{TestContext.BuildRID}.dll" : $"lib/{library}.dll"; + return (path, $"{libraryType}/{path}"); + } + } + + private void NativeLibraries(bool isSelfContained, bool useLocalPath) + { + NetCoreAppBuilder.RuntimeLibraryType[] libraryTypes = [NetCoreAppBuilder.RuntimeLibraryType.project, NetCoreAppBuilder.RuntimeLibraryType.package, NetCoreAppBuilder.RuntimeLibraryType.runtimepack]; + + Action customizer = b => + { + foreach (var libraryType in libraryTypes) + { + string library = $"Test{libraryType}"; + (string path, string localPath) = GetPaths(libraryType, false); + b.WithRuntimeLibrary(libraryType, library, "1.0.0", p => p + .WithNativeLibraryGroup(null, g => g + .WithAsset($"{path}/{library}.native", useLocalPath ? f => f.WithLocalPath($"{localPath}/{library}.native") : null))); + + if (!isSelfContained) + { + // Add RID-specific native library + (string ridPath, string localRidPath) = GetPaths(libraryType, true); + b.WithRuntimeLibrary(libraryType, $"{library}-{TestContext.BuildRID}", "1.0.0", p => p + .WithNativeLibraryGroup(TestContext.BuildRID, g => g + .WithAsset($"{ridPath}/{library}-{TestContext.BuildRID}.native", useLocalPath ? f => f.WithLocalPath($"{localRidPath}/{library}-{TestContext.BuildRID}.native") : null))); + } + } + + b.WithLocalPathsInDepsJson(useLocalPath); + }; + + using TestApp app = CreateApp(isSelfContained, customizer); + var result = sharedState.DotNetWithNetCoreApp.Exec(app.AppDll) + .EnableTracingAndCaptureOutputs() + .Execute(); + result.Should().Pass(); + + // Check all library types + foreach (NetCoreAppBuilder.RuntimeLibraryType libraryType in libraryTypes) + { + // Check RID-agnostic native library path + (string path, string localPath) = GetPaths(libraryType, false); + + // Without localPath, RID-agnostic non-runtimepack native libraries are assumed to be in + string relativePath = useLocalPath + ? localPath + : libraryType == RuntimeLibraryType.runtimepack ? path : string.Empty; + string expectedPath = Path.Join(app.Location, relativePath); + result.Should().HaveResolvedNativeLibraryPath(expectedPath); + if (useLocalPath) + { + result.Should().NotHaveResolvedNativeLibraryPath(Path.Join(app.Location, path)); + } + + // Check RID-specific native library path + if (!isSelfContained) + { + (string ridPath, string localRidPath) = GetPaths(libraryType, true); + string expectedRidPath = Path.Join(app.Location, useLocalPath ? localRidPath : ridPath); + result.Should().HaveResolvedNativeLibraryPath(expectedRidPath); + if (useLocalPath) + { + result.Should().NotHaveResolvedNativeLibraryPath(Path.Join(app.Location, ridPath)); + } + } + } + + static (string Path, string LocalPath) GetPaths(NetCoreAppBuilder.RuntimeLibraryType libraryType, bool useRid) + { + string path = useRid ? $"native/{TestContext.BuildRID}" : "native"; + return (path, $"{libraryType}/{path}"); + } + } + + private void ResourceAssemblies(bool isSelfContained, bool useLocalPath) + { + NetCoreAppBuilder.RuntimeLibraryType[] libraryTypes = [NetCoreAppBuilder.RuntimeLibraryType.project, NetCoreAppBuilder.RuntimeLibraryType.package, NetCoreAppBuilder.RuntimeLibraryType.runtimepack]; + + Action customizer = b => + { + foreach (var libraryType in libraryTypes) + { + string library = $"Test{libraryType}"; + (string path, string localPath) = GetPaths(libraryType); + b.WithRuntimeLibrary(libraryType, library, "1.0.0", p => p + .WithResourceAssembly($"{path}/fr/{library}.resources.dll", useLocalPath ? f => f.WithLocalPath($"{localPath}/fr/{library}.resources.dll") : null)); + } + + b.WithLocalPathsInDepsJson(useLocalPath); + }; + + using TestApp app = CreateApp(isSelfContained, customizer); + var result = sharedState.DotNetWithNetCoreApp.Exec(app.AppDll) + .EnableTracingAndCaptureOutputs() + .Execute(); + result.Should().Pass(); + + // Check all library types + foreach (var libraryType in libraryTypes) + { + (string path, string localPath) = GetPaths(libraryType); + + // Without localPath, non-runtimepack resource assemblies are assumed to be in // + string relativePath = useLocalPath + ? localPath + : libraryType == RuntimeLibraryType.runtimepack ? path : string.Empty; + string expectedPath = Path.Join(app.Location, relativePath); + result.Should().HaveResolvedResourceRootPath(expectedPath); + if (useLocalPath) + { + result.Should().NotHaveResolvedResourceRootPath(Path.Join(app.Location, path)); + } + } + + static (string Path, string LocalPath) GetPaths(NetCoreAppBuilder.RuntimeLibraryType libraryType) + { + string path = $"resources"; + return (path, $"{libraryType}/{path}"); + } + } + + private static TestApp CreateApp(bool isSelfContained, Action customizer) + { + TestApp app = TestApp.CreateEmpty("App"); + if (isSelfContained) + { + app.PopulateSelfContained(TestApp.MockedComponent.CoreClr, customizer); + } + else + { + app.PopulateFrameworkDependent(Constants.MicrosoftNETCoreApp, TestContext.MicrosoftNETCoreAppVersion, customizer); + } + return app; + } + + public class SharedTestState : SharedTestStateBase + { + public DotNetCli DotNetWithNetCoreApp { get; } + + public SharedTestState() + { + DotNetWithNetCoreApp = DotNet("WithNetCoreApp") + .AddMicrosoftNETCoreAppFrameworkMockCoreClr(TestContext.MicrosoftNETCoreAppVersion) + .Build(); + } + } + } +} diff --git a/src/installer/tests/HostActivation.Tests/DependencyResolution/ResolveComponentDependencies.cs b/src/installer/tests/HostActivation.Tests/DependencyResolution/ResolveComponentDependencies.cs index 5c66d948f6d0f2..9d3c792bdc3766 100644 --- a/src/installer/tests/HostActivation.Tests/DependencyResolution/ResolveComponentDependencies.cs +++ b/src/installer/tests/HostActivation.Tests/DependencyResolution/ResolveComponentDependencies.cs @@ -458,7 +458,7 @@ public TestApp CreateComponentWithDependencies(Action customi .WithPackage(AdditionalDependencyName, "2.0.1", p => p.WithAssemblyGroup(null, g => g .WithAsset($"lib/netstandard1.0/{AdditionalDependencyName}.dll", f => f .WithVersion("2.0.0.0", "2.0.1.23344") - .WithFileOnDiskPath($"{AdditionalDependencyName}.dll")))) + .WithLocalPath($"{AdditionalDependencyName}.dll")))) .WithPackage("Libuv", "1.9.1", p => p .WithNativeLibraryGroup("debian-x64", g => g.WithAsset("runtimes/debian-x64/native/libuv.so")) .WithNativeLibraryGroup("fedora-x64", g => g.WithAsset("runtimes/fedora-x64/native/libuv.so")) diff --git a/src/installer/tests/HostActivation.Tests/SelfContainedAppLaunch.cs b/src/installer/tests/HostActivation.Tests/SelfContainedAppLaunch.cs index f421335995f696..ba53bd13d33f91 100644 --- a/src/installer/tests/HostActivation.Tests/SelfContainedAppLaunch.cs +++ b/src/installer/tests/HostActivation.Tests/SelfContainedAppLaunch.cs @@ -3,12 +3,15 @@ using System; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using FluentAssertions; using Microsoft.DotNet.Cli.Build.Framework; using Microsoft.DotNet.CoreSetup.Test; using Microsoft.NET.HostModel.AppHost; using Xunit; +using static Microsoft.DotNet.CoreSetup.Test.NetCoreAppBuilder; +using static Microsoft.NET.HostModel.AppHost.HostWriter; namespace HostActivation.Tests { @@ -180,6 +183,51 @@ public void DevicePath() .And.HaveStdOutContaining(TestContext.MicrosoftNETCoreAppVersion); } + [Fact] + public void CustomRuntimeLocation() + { + string subdirectory = "runtime-directory"; + Action customizer = b => + { + // Find the NETCoreApp runtime pack + RuntimeLibraryBuilder netCoreApp = b.RuntimeLibraries.First( + r => r.Type == RuntimeLibraryType.runtimepack.ToString() && r.Name == $"runtimepack.{Constants.MicrosoftNETCoreApp}.Runtime.{TestContext.BuildRID}"); + + // Update all NETCoreApp asset paths to point to the subdirectory + RuntimeAssetGroupBuilder[] groups = [.. netCoreApp.AssemblyGroups, .. netCoreApp.NativeLibraryGroups]; + foreach (RuntimeAssetGroupBuilder group in groups) + { + foreach (RuntimeFileBuilder asset in group.Assets) + { + asset.Path = Path.Join(subdirectory, asset.Path); + } + } + + foreach (ResourceAssemblyBuilder resource in netCoreApp.ResourceAssemblies) + { + resource.Path = Path.Join(subdirectory, resource.Path); + } + }; + + using TestApp app = TestApp.CreateFromBuiltAssets("HelloWorld"); + app.PopulateSelfContained(TestApp.MockedComponent.None, customizer); + app.CreateAppHost(dotNetRootOptions: new() + { + Location = DotNetSearchOptions.SearchLocation.AppRelative, + AppRelativeDotNet = subdirectory + }); + + // Apphost should be able to find hostfxr based on the .NET search options + // Runtime files should be resolved based on relative path in .deps.json + Command.Create(app.AppExe) + .EnableTracingAndCaptureOutputs() + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello World") + .And.HaveStdOutContaining(TestContext.MicrosoftNETCoreAppVersion) + .And.HaveStdErrContaining($"CoreCLR path = '{Path.Join(app.Location, subdirectory, Binaries.CoreClr.FileName)}'"); + } + public class SharedTestState : IDisposable { public TestApp App { get; } diff --git a/src/installer/tests/TestUtils/DotNetBuilder.cs b/src/installer/tests/TestUtils/DotNetBuilder.cs index b761b76b517c4b..db9a316e2704fa 100644 --- a/src/installer/tests/TestUtils/DotNetBuilder.cs +++ b/src/installer/tests/TestUtils/DotNetBuilder.cs @@ -140,13 +140,13 @@ public DotNetBuilder AddMicrosoftNETCoreAppFrameworkMockCoreClr(string version, // ./shared/Microsoft.NETCore.App//coreclr.dll - this is a mock, will not actually run CoreClr .WithAsset((new NetCoreAppBuilder.RuntimeFileBuilder($"runtimes/{currentRid}/native/{Binaries.CoreClr.FileName}")) .CopyFromFile(Binaries.CoreClr.MockPath) - .WithFileOnDiskPath(Binaries.CoreClr.FileName)))) + .WithLocalPath(Binaries.CoreClr.FileName)))) .WithPackage($"runtime.{currentRid}.Microsoft.NETCore.DotNetHostPolicy", version, p => p .WithNativeLibraryGroup(null, g => g // ./shared/Microsoft.NETCore.App//hostpolicy.dll - this is the real component and will load CoreClr library .WithAsset((new NetCoreAppBuilder.RuntimeFileBuilder($"runtimes/{currentRid}/native/{Binaries.HostPolicy.FileName}")) .CopyFromFile(Binaries.HostPolicy.FilePath) - .WithFileOnDiskPath(Binaries.HostPolicy.FileName)))) + .WithLocalPath(Binaries.HostPolicy.FileName)))) .WithCustomizer(customizer) .Build(new TestApp(netCoreAppPath, "Microsoft.NETCore.App")); diff --git a/src/installer/tests/TestUtils/NetCoreAppBuilder.cs b/src/installer/tests/TestUtils/NetCoreAppBuilder.cs index 636b8260af6af0..7ddabe8d5e7ae2 100644 --- a/src/installer/tests/TestUtils/NetCoreAppBuilder.cs +++ b/src/installer/tests/TestUtils/NetCoreAppBuilder.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; namespace Microsoft.DotNet.CoreSetup.Test { @@ -16,6 +18,7 @@ public class NetCoreAppBuilder public string Runtime { get; set; } private TestApp _sourceApp; + private bool _includeLocalPathsInDepsJson = true; public Action RuntimeConfigCustomizer { get; set; } @@ -33,7 +36,9 @@ public abstract class FileBuilder public string Path { get; set; } public string SourcePath { get; set; } - public string FileOnDiskPath { get; set; } + + // If set, path relative to the app location where the file will be created. + public string LocalPath { get; set; } public FileBuilder(string path) { @@ -42,14 +47,14 @@ public FileBuilder(string path) internal void Build(BuildContext context) { - string path = ToDiskPath(FileOnDiskPath ?? Path); + string path = ToDiskPath(LocalPath ?? Path); string absolutePath = System.IO.Path.Combine(context.App.Location, path); if (SourcePath != null) { FileUtils.EnsureFileDirectoryExists(absolutePath); File.Copy(SourcePath, absolutePath); } - else if ((FileOnDiskPath == null || FileOnDiskPath.Length > 0) + else if ((LocalPath == null || LocalPath.Length > 0) && !File.Exists(absolutePath)) { FileUtils.CreateEmptyFile(absolutePath); @@ -76,15 +81,15 @@ public T CopyFromFile(string sourcePath) return this as T; } - public T WithFileOnDiskPath(string relativePath) + public T WithLocalPath(string localPath) { - FileOnDiskPath = relativePath; + LocalPath = localPath; return this as T; } public T NotOnDisk() { - FileOnDiskPath = string.Empty; + LocalPath = string.Empty; return this as T; } } @@ -109,6 +114,8 @@ public RuntimeFileBuilder WithVersion(string assemblyVersion, string fileVersion internal new RuntimeFile Build(BuildContext context) { base.Build(context); + + // TODO: Pass in LocalPath once we can upgrade to a Microsoft.Extensions.DependencyModel version that supports it. return new RuntimeFile(Path, AssemblyVersion, FileVersion); } } @@ -136,6 +143,8 @@ public ResourceAssemblyBuilder WithLocale(string locale) internal new ResourceAssembly Build(BuildContext context) { base.Build(context); + + // TODO: Pass in LocalPath once we can upgrade to a Microsoft.Extensions.DependencyModel version that supports it. return new ResourceAssembly(Path, Locale); } } @@ -367,6 +376,12 @@ public NetCoreAppBuilder WithStandardRuntimeFallbacks() .WithRuntimeFallbacks("osx-x64", "osx", "any"); } + public NetCoreAppBuilder WithLocalPathsInDepsJson(bool includeLocalPaths) + { + _includeLocalPathsInDepsJson = includeLocalPaths; + return this; + } + public NetCoreAppBuilder WithCustomizer(Action customizer) { customizer?.Invoke(this); @@ -418,7 +433,60 @@ public TestApp Build(TestApp testApp) writer.Write(dependencyContext, stream); } + // Add localPath properties + // TODO: Remove once we can upgrade to a Microsoft.Extensions.DependencyModel version that supports localPath. + if (_includeLocalPathsInDepsJson) + { + AddLocalPathsToJson(testApp.DepsJson, buildContext); + } + return testApp; } + + private void AddLocalPathsToJson(string depsJsonPath, BuildContext buildContext) + { + // Read the generated JSON + string jsonContent = File.ReadAllText(depsJsonPath); + + // Parse and modify the JSON to add localPath properties + JsonNode rootNode = JsonNode.Parse(jsonContent); + JsonNode targets = rootNode["targets"][$"{Framework}{(Runtime is null ? "" : $"/{Runtime}")}"]; + foreach (var l in RuntimeLibraries) + { + JsonNode library = targets[$"{l.Name}/{l.Version}"]; + AddLocalPathsForFiles(library, l.AssemblyGroups, "runtime"); + AddLocalPathsForFiles(library, l.NativeLibraryGroups, "native"); + + // Add localPath for resources + JsonNode assets = library["resources"]; + foreach (var resource in l.ResourceAssemblies) + { + if (string.IsNullOrEmpty(resource.LocalPath)) + continue; + + assets[resource.Path]["localPath"] = resource.LocalPath; + } + } + + // Write the modified JSON back to the file + var options = new JsonSerializerOptions { WriteIndented = true }; + string modifiedJson = rootNode.ToJsonString(options); + File.WriteAllText(depsJsonPath, modifiedJson); + } + + private void AddLocalPathsForFiles(JsonNode library, List groups, string assetType) + { + foreach (var group in groups) + { + JsonNode assets = library[!string.IsNullOrEmpty(group.Runtime) ? "runtimeTargets" : assetType]; + foreach (var asset in group.Assets) + { + if (string.IsNullOrEmpty(asset.LocalPath)) + continue; + + assets[asset.Path]["localPath"] = asset.LocalPath.Replace('\\', '/'); // .deps.json explicitly uses forward slashes; + } + } + } } } diff --git a/src/native/corehost/fxr/standalone/hostpolicy_resolver.cpp b/src/native/corehost/fxr/standalone/hostpolicy_resolver.cpp index ff68220193d47b..be784c61a693a5 100644 --- a/src/native/corehost/fxr/standalone/hostpolicy_resolver.cpp +++ b/src/native/corehost/fxr/standalone/hostpolicy_resolver.cpp @@ -27,11 +27,10 @@ namespace { trace::verbose(_X("--- Resolving %s version from deps json [%s]"), LIBHOSTPOLICY_NAME, deps_json.c_str()); - pal::string_t retval; json_parser_t json; if (!json.parse_file(deps_json)) { - return retval; + return {}; } // Look up the root package instead of the "runtime" package because we can't do a full rid resolution. @@ -43,13 +42,13 @@ namespace if (utils::starts_with(lib_name, prefix, false)) { // Extract the version information that occurs after '/' - retval = lib_name.substr(utils::strlen(prefix)); - break; + pal::string_t version = lib_name.substr(utils::strlen(prefix)); + trace::verbose(_X("Resolved version %s from dependency manifest file [%s]"), version.c_str(), deps_json.c_str()); + return version; } } - trace::verbose(_X("Resolved version %s from dependency manifest file [%s]"), retval.c_str(), deps_json.c_str()); - return retval; + return {}; } /** diff --git a/src/native/corehost/hostpolicy/deps_entry.cpp b/src/native/corehost/hostpolicy/deps_entry.cpp index 981d86305e6921..1d294d7266b435 100644 --- a/src/native/corehost/hostpolicy/deps_entry.cpp +++ b/src/native/corehost/hostpolicy/deps_entry.cpp @@ -23,22 +23,23 @@ static pal::string_t normalize_dir_separator(const pal::string_t& path) // ----------------------------------------------------------------------------- // Given a "base" directory, determine the resolved path for this file. // -// * If this file exists within the single-file bundle candidate is -// the full-path to the extracted file. +// * If this file exists within the single-file bundle: +// - if extracted, candidate is the full-path to the extracted file. +// - if not extracted, candidate is empty. // * Otherwise, candidate is the full local path of the file. // // 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 -// str - (out parameter) If the method returns true, contains the file path for this deps entry +// relative_path - Relative path of to look for this entry +// str - (out) 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. +// found_in_bundle - (out) True if the candidate is located within the single-file bundle and not extracted. // // 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, pal::string_t* str, uint32_t search_options, bool &found_in_bundle) const +static bool to_path(const pal::string_t& base, const pal::string_t& relative_path, pal::string_t* str, uint32_t search_options, bool &found_in_bundle) { pal::string_t& candidate = *str; @@ -51,17 +52,11 @@ bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_ return false; } - pal::string_t normalized_path = normalize_dir_separator(asset.relative_path); - // Reserve space for the path below - candidate.reserve(base.length() + ietf_dir.length() + normalized_path.length() + 3); + candidate.reserve(base.length() + relative_path.length() + 2); // +2 for directory separator and null terminator - 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()); assert(!is_servicing || !look_in_bundle); @@ -71,45 +66,44 @@ bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_ if (app->has_base(base)) { - // If sub_path is found in the single-file bundle, + // If relative_path is found in the single-file bundle, // app::locate() will set candidate to the full-path to the assembly extracted out to disk. bool extracted_to_disk = false; - if (app->locate(sub_path, candidate, extracted_to_disk)) + if (app->locate(relative_path, candidate, extracted_to_disk)) { found_in_bundle = !extracted_to_disk; - trace::verbose(_X(" %s found in bundle [%s] %s"), sub_path.c_str(), candidate.c_str(), extracted_to_disk ? _X("(extracted)") : _X("")); + trace::verbose(_X(" %s found in bundle [%s] %s"), relative_path.c_str(), candidate.c_str(), extracted_to_disk ? _X("(extracted)") : _X("")); return true; } else { - trace::verbose(_X(" %s not found in bundle"), sub_path.c_str()); + trace::verbose(_X(" %s not found in bundle"), relative_path.c_str()); } } else { trace::verbose(_X(" %s not searched in bundle base path %s doesn't match bundle base %s."), - sub_path.c_str(), base.c_str(), app->base_path().c_str()); + relative_path.c_str(), base.c_str(), app->base_path().c_str()); } } candidate.assign(base); - append_path(&candidate, sub_path.c_str()); + append_path(&candidate, relative_path.c_str()); - const pal::char_t* query_type = look_in_base ? _X("Local") : _X("Relative"); if (search_options & deps_entry_t::search_options::file_existence) { if (!pal::file_exists(candidate)) { - trace::verbose(_X(" %s path query did not exist %s"), query_type, candidate.c_str()); + trace::verbose(_X(" Does not exist: %s"), candidate.c_str()); candidate.clear(); return false; } - trace::verbose(_X(" %s path query exists %s"), query_type, candidate.c_str()); + trace::verbose(_X(" Exists: %s"), candidate.c_str()); } else { - trace::verbose(_X(" %s path query %s (skipped file existence check)"), query_type, candidate.c_str()); + trace::verbose(_X(" Skipped file existence check: %s"), candidate.c_str()); } // If a file is resolved to the servicing directory, mark it as disabled in the bundle. @@ -123,9 +117,9 @@ bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_ assert(!app->has_base(base)); assert(!found_in_bundle); - if (app->disable(sub_path)) + if (app->disable(relative_path)) { - trace::verbose(_X(" %s disabled in bundle because of servicing override %s"), sub_path.c_str(), candidate.c_str()); + trace::verbose(_X(" %s disabled in bundle because of servicing override %s"), relative_path.c_str(), candidate.c_str()); } } @@ -139,37 +133,54 @@ bool deps_entry_t::to_path(const pal::string_t& base, const pal::string_t& ietf_ // 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 +// found_in_bundle - Whether the asset was found in 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, pal::string_t* str, uint32_t search_options, bool& found_in_bundle) const { - pal::string_t ietf_dir; - - if (asset_type == asset_types::resources) + pal::string_t relative_path = normalize_dir_separator(asset.local_path); + if (relative_path.empty()) { - pal::string_t pal_relative_path = normalize_dir_separator(asset.relative_path); + relative_path = normalize_dir_separator(asset.relative_path); + if (library_type != _X("runtimepack")) // runtimepack assets set the path to the local path + { + pal::string_t file_name = get_filename(relative_path); - // Resources are represented as "lib///" in the deps.json. - // The is the "directory" in the pal_relative_path below, so extract it. - ietf_dir = get_directory(pal_relative_path); + // Compute the expected relative path for this asset. + // resource: / + // runtime/native: + if (asset_type == asset_types::resources) + { + // Resources are represented as "lib///" in the deps.json. + // The is the "directory" in the relative_path below, so extract it. + pal::string_t ietf_dir = get_directory(relative_path); - // get_directory returns with DIR_SEPARATOR appended that we need to remove. - remove_trailing_dir_separator(&ietf_dir); + // get_directory returns with DIR_SEPARATOR appended that we need to remove. + assert(ietf_dir.back() == DIR_SEPARATOR); + remove_trailing_dir_separator(&ietf_dir); - // Extract IETF code from "lib//" - ietf_dir = get_filename(ietf_dir); + // Extract IETF code from "lib//" + ietf_dir = get_filename(ietf_dir); - trace::verbose(_X("Detected a resource asset, will query dir/ietf-tag/resource base: %s ietf: %s asset: %s"), - base.c_str(), ietf_dir.c_str(), asset.name.c_str()); - } + trace::verbose(_X(" Detected a resource asset, will query // base: %s ietf: %s asset: %s"), + base.c_str(), ietf_dir.c_str(), asset.name.c_str()); + + relative_path = ietf_dir; + append_path(&relative_path, file_name.c_str()); + } + else + { + relative_path = std::move(file_name); + } + } + trace::verbose(_X(" Computed relative path: %s"), relative_path.c_str()); + } - 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); + return to_path(base, relative_path, str, search_options, found_in_bundle); } // ----------------------------------------------------------------------------- @@ -184,11 +195,10 @@ bool deps_entry_t::to_dir_path(const pal::string_t& base, pal::string_t* str, ui // 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, pal::string_t* str, uint32_t search_options) const +bool deps_entry_t::to_package_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options) const { bool 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); + bool result = to_path(base, normalize_dir_separator(asset.relative_path), str, search_options, found_in_bundle); assert(!found_in_bundle); return result; } @@ -205,7 +215,7 @@ bool deps_entry_t::to_rel_path(const pal::string_t& base, pal::string_t* str, ui // 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, pal::string_t* str, uint32_t search_options) const +bool deps_entry_t::to_library_package_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options) const { str->clear(); @@ -228,5 +238,5 @@ bool deps_entry_t::to_full_path(const pal::string_t& base, pal::string_t* str, u } search_options &= ~deps_entry_t::search_options::look_in_bundle; - return to_rel_path(new_base, str, search_options); + return to_package_path(new_base, str, search_options); } diff --git a/src/native/corehost/hostpolicy/deps_entry.h b/src/native/corehost/hostpolicy/deps_entry.h index ae769f799f43bc..0f1996ca632cc7 100644 --- a/src/native/corehost/hostpolicy/deps_entry.h +++ b/src/native/corehost/hostpolicy/deps_entry.h @@ -12,18 +12,23 @@ struct deps_asset_t { - deps_asset_t() : deps_asset_t(_X(""), _X(""), version_t(), version_t()) { } + deps_asset_t() : deps_asset_t(_X(""), _X(""), version_t(), version_t(), _X("")) { } deps_asset_t(const pal::string_t& name, const pal::string_t& relative_path, const version_t& assembly_version, const version_t& file_version) + : deps_asset_t(name, relative_path, assembly_version, file_version, _X("")) { } + + deps_asset_t(const pal::string_t& name, const pal::string_t& relative_path, const version_t& assembly_version, const version_t& file_version, const pal::string_t& local_path) : name(name) , relative_path(get_replaced_char(relative_path, _X('\\'), _X('/'))) // Deps file does not follow spec. It uses '\\', should use '/' , assembly_version(assembly_version) - , file_version(file_version) { } + , file_version(file_version) + , local_path(local_path.empty() ? pal::string_t() : get_replaced_char(local_path, _X('\\'), _X('/'))) { } pal::string_t name; pal::string_t relative_path; version_t assembly_version; version_t file_version; + pal::string_t local_path; }; struct deps_entry_t @@ -39,7 +44,7 @@ struct deps_entry_t enum search_options : uint32_t { none = 0x0, - look_in_base = 0x1, // Search entry as a relative path + // unused = 0x1, 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 @@ -64,17 +69,10 @@ struct deps_entry_t 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, pal::string_t* str, uint32_t search_options) const; + bool to_package_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, 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, pal::string_t* str, uint32_t search_options, bool & found_in_bundle) const; - + bool to_library_package_path(const pal::string_t& base, pal::string_t* str, uint32_t search_options) const; }; #endif // __DEPS_ENTRY_H_ diff --git a/src/native/corehost/hostpolicy/deps_format.cpp b/src/native/corehost/hostpolicy/deps_format.cpp index 7a58f12bb067f2..ddfce1b2957a2c 100644 --- a/src/native/corehost/hostpolicy/deps_format.cpp +++ b/src/native/corehost/hostpolicy/deps_format.cpp @@ -166,12 +166,13 @@ void deps_json_t::reconcile_libraries_with_targets( if (trace::is_enabled()) { - trace::info(_X(" Entry %zu for asset name: %s, relpath: %s, assemblyVersion %s, fileVersion %s"), + trace::info(_X(" Entry %zu for asset name: %s, relpath: %s, assemblyVersion %s, fileVersion %s, localPath %s"), m_deps_entries[i].size(), entry.asset.name.c_str(), entry.asset.relative_path.c_str(), entry.asset.assembly_version.as_str().c_str(), - entry.asset.file_version.as_str().c_str()); + entry.asset.file_version.as_str().c_str(), + entry.asset.local_path.empty() ? _X("(not set)") : entry.asset.local_path.c_str()); } m_deps_entries[i].push_back(std::move(entry)); @@ -405,31 +406,34 @@ void deps_json_t::process_runtime_targets(const json_parser_t::value_t& json, co version_t assembly_version, file_version; - const pal::string_t& assembly_version_str = get_optional_property(file.value, _X("assemblyVersion")); + pal::string_t assembly_version_str = get_optional_property(file.value, _X("assemblyVersion")); if (!assembly_version_str.empty()) { version_t::parse(assembly_version_str, &assembly_version); } - const pal::string_t& file_version_str = get_optional_property(file.value, _X("fileVersion")); + pal::string_t file_version_str = get_optional_property(file.value, _X("fileVersion")); if (!file_version_str.empty()) { version_t::parse(file_version_str, &file_version); } + pal::string_t local_path = get_optional_path(file.value, _X("localPath")); + pal::string_t file_name{file.name.GetString()}; - deps_asset_t asset(get_filename_without_ext(file_name), file_name, assembly_version, file_version); + deps_asset_t asset(get_filename_without_ext(file_name), file_name, assembly_version, file_version, local_path); const auto& rid = file.value[_X("rid")].GetString(); if (trace::is_enabled()) { - trace::info(_X(" %s asset: %s rid=%s assemblyVersion=%s fileVersion=%s"), + trace::info(_X(" %s asset: %s rid=%s assemblyVersion=%s fileVersion=%s localPath=%s"), deps_entry_t::s_known_asset_types[asset_type_index], asset.relative_path.c_str(), rid, asset.assembly_version.as_str().c_str(), - asset.file_version.as_str().c_str()); + asset.file_version.as_str().c_str(), + asset.local_path.empty() ? _X("(not set)") : asset.local_path.c_str()); } assets.libs[package.name.GetString()][asset_type_index].rid_assets[rid].push_back(asset); @@ -464,27 +468,30 @@ void deps_json_t::process_targets(const json_parser_t::value_t& json, const pal: { version_t assembly_version, file_version; - const pal::string_t& assembly_version_str = get_optional_property(file.value, _X("assemblyVersion")); + pal::string_t assembly_version_str = get_optional_property(file.value, _X("assemblyVersion")); if (assembly_version_str.length() > 0) { version_t::parse(assembly_version_str, &assembly_version); } - const pal::string_t& file_version_str = get_optional_property(file.value, _X("fileVersion")); + pal::string_t file_version_str = get_optional_property(file.value, _X("fileVersion")); if (file_version_str.length() > 0) { version_t::parse(file_version_str, &file_version); } + pal::string_t local_path = get_optional_path(file.value, _X("localPath")); + pal::string_t file_name{file.name.GetString()}; - deps_asset_t asset(get_filename_without_ext(file_name), file_name, assembly_version, file_version); + deps_asset_t asset(get_filename_without_ext(file_name), file_name, assembly_version, file_version, local_path); if (trace::is_enabled()) { - trace::info(_X(" %s assemblyVersion=%s fileVersion=%s"), + trace::info(_X(" %s assemblyVersion=%s fileVersion=%s localPath=%s"), asset.relative_path.c_str(), asset.assembly_version.as_str().c_str(), - asset.file_version.as_str().c_str()); + asset.file_version.as_str().c_str(), + asset.local_path.empty() ? _X("(not set)") : asset.local_path.c_str()); } asset_files.push_back(std::move(asset)); diff --git a/src/native/corehost/hostpolicy/deps_resolver.cpp b/src/native/corehost/hostpolicy/deps_resolver.cpp index 1665b6c7bda132..1f9fde70ed0925 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.cpp +++ b/src/native/corehost/hostpolicy/deps_resolver.cpp @@ -329,10 +329,10 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str else if (config.is_app()) { assert(fx_level == AppFxLevel); - if (entry.is_rid_specific) + if (entry.is_rid_specific && entry.asset.local_path.empty()) { - // Look up rid specific assets in the rid folders. - if (entry.to_rel_path(deps_dir, candidate, search_options | deps_entry_t::search_options::look_in_bundle)) + // Look up rid specific assets without a local path specified in the rid folders. + if (entry.to_package_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; @@ -340,9 +340,11 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str } else { - // Non-rid assets, lookup in the published dir. + // Look up assets relative to the deps directory if (entry.to_dir_path(deps_dir, candidate, search_options | deps_entry_t::search_options::look_in_bundle, found_in_bundle)) { + // Bundles are expected to be RID-specific themselves, so RID-specific assets are not expected to be found in the bundle. + assert(!entry.is_rid_specific || !found_in_bundle); trace::verbose(_X(" Probed deps dir and matched '%s'"), candidate->c_str()); return true; } @@ -352,7 +354,7 @@ bool deps_resolver_t::probe_deps_entry(const deps_entry_t& entry, const pal::str } else { - if (entry.to_full_path(config.probe_dir, candidate, search_options | (config.is_servicing() ? deps_entry_t::search_options::is_servicing : 0))) + if (entry.to_library_package_path(config.probe_dir, candidate, search_options | (config.is_servicing() ? deps_entry_t::search_options::is_servicing : 0))) { trace::verbose(_X(" Probed package dir and matched '%s'"), candidate->c_str()); return true; @@ -431,8 +433,8 @@ bool deps_resolver_t::resolve_tpa_list( return true; } - trace::info(_X("Processing TPA for deps entry [%s, %s, %s] with fx level: %d"), - entry.library_name.c_str(), entry.library_version.c_str(), entry.asset.relative_path.c_str(), fx_level); + trace::info(_X("Processing TPA for deps entry [%s, %s, %s, local_path: %s] with fx level: %d"), + entry.library_name.c_str(), entry.library_version.c_str(), entry.asset.relative_path.c_str(), entry.asset.local_path.empty() ? _X("") : entry.asset.local_path.c_str(), fx_level); pal::string_t resolved_path; @@ -779,8 +781,8 @@ bool deps_resolver_t::resolve_probe_dirs( return true; } - trace::verbose(_X("Processing native/culture for deps entry [%s, %s, %s]"), - entry.library_name.c_str(), entry.library_version.c_str(), entry.asset.relative_path.c_str()); + trace::verbose(_X("Processing native/culture for deps entry [%s, %s, %s, local_path: %s]"), + entry.library_name.c_str(), entry.library_version.c_str(), entry.asset.relative_path.c_str(), entry.asset.local_path.empty() ? _X("") : entry.asset.local_path.c_str()); bool found_in_bundle = false; if (probe_deps_entry(entry, deps_dir, fx_level, &candidate, found_in_bundle))