Skip to content

Commit 49bbc44

Browse files
authored
Allow specifying SDK search paths in global.json (#113512)
- Parse optional properties in global.json: ``` { "sdk": { "paths": [ "path/to/sdk/root", "$host$" ], "errorMessage": "<message>" } } ``` - `paths` can be absolute or relative to the global.json. `$host$` is a special value indicating the running `dotnet` path - `errorMessage` is shown on SDK resolution failure (instead of the host's default SDK resolution error message). - Make `hostfxr` search for SDKs with custom paths if specified and error out with custom message if specified - `dotnet <command>` - global.json based on process working directory, runs command using resolved SDK respecting global.json paths - `hostfxr_resolve_sdk2` - global.json based on working directory argument, returns resolved SDK respecting global.json paths - `--info` and `--list-sdks` - global.json based on process working directory, prints found SDKs respecting global.json paths - `hostfxr_get_available_sdks` and `hostfxr_get_dotnet_environment_info` - global.json based on process working directory, returns found SDKs respecting global.json paths - Update error and tracing messages to include information about configured search paths
1 parent 96b80da commit 49bbc44

File tree

15 files changed

+445
-70
lines changed

15 files changed

+445
-70
lines changed

src/installer/tests/Directory.Build.targets

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@
6060
File="$(OutDir)TestContextVariables.txt"
6161
Overwrite="true"
6262
Lines="@(TestContextVariable)" />
63+
64+
<!-- Write an empty global.json to the output, such that running the test -->
65+
<WriteLinesToFile
66+
File="$(OutDir)global.json"
67+
Lines="{}"
68+
WriteOnlyWhenDifferent="true" />
6369
</Target>
6470

6571
<Target Name="DetermineTestOutputDirectory">

src/installer/tests/HostActivation.Tests/NativeHostApis.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,32 @@ public void Hostfxr_resolve_sdk2_with_global_json_and_disallowing_previews()
237237
}
238238
}
239239

240+
[Fact]
241+
public void Hostfxr_resolve_sdk2_GlobalJson_Paths()
242+
{
243+
// With global.json specifying custom search paths, no version.
244+
// Return first search location match (latest in that location).
245+
246+
var f = sharedTestState.SdkAndFrameworkFixture;
247+
using (TestArtifact workingDir = TestArtifact.Create(nameof(workingDir)))
248+
{
249+
string globalJson = GlobalJson.Write(workingDir.Location, new GlobalJson.Sdk() { Paths = [ f.SelfRegistered, f.LocalSdkDir ] });
250+
string expectedData = string.Join(';', new[]
251+
{
252+
("resolved_sdk_dir", Path.Combine(f.SelfRegisteredGlobalSdkDir, "15.1.4-preview")),
253+
("global_json_path", globalJson)
254+
});
255+
256+
string api = ApiNames.hostfxr_resolve_sdk2;
257+
TestContext.BuiltDotNet.Exec(sharedTestState.HostApiInvokerApp.AppDll, api, f.ExeDir, workingDir.Location, "0")
258+
.EnableTracingAndCaptureOutputs()
259+
.Execute()
260+
.Should().Pass()
261+
.And.ReturnStatusCode(api, Constants.ErrorCode.Success)
262+
.And.HaveStdOutContaining($"{api} data:[{expectedData}]");
263+
}
264+
}
265+
240266
[Fact]
241267
public void Hostfxr_corehost_set_error_writer_test()
242268
{

src/installer/tests/HostActivation.Tests/SDKLookup.cs

Lines changed: 192 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public SDKLookup(SharedTestState sharedState)
3838
}
3939

4040
[Fact]
41-
public void SdkLookup_Global_Json_Single_Digit_Patch_Rollup()
41+
public void GlobalJson_SingleDigitPatch()
4242
{
4343
// Set specified SDK version = 9999.3.4-global-dummy
4444
string requestedVersion = "9999.3.4-global-dummy";
@@ -133,7 +133,7 @@ public void SdkLookup_Global_Json_Single_Digit_Patch_Rollup()
133133
}
134134

135135
[Fact]
136-
public void SdkLookup_Global_Json_Two_Part_Patch_Rollup()
136+
public void GlobalJson_TwoPartPatch()
137137
{
138138
// Set specified SDK version = 9999.3.304-global-dummy
139139
string requestedVersion = "9999.3.304-global-dummy";
@@ -226,7 +226,7 @@ public void SdkLookup_Global_Json_Two_Part_Patch_Rollup()
226226
}
227227

228228
[Fact]
229-
public void SdkLookup_Negative_Version()
229+
public void NegativeVersion()
230230
{
231231
GlobalJson.CreateEmpty(SharedState.CurrentWorkingDir);
232232

@@ -257,7 +257,7 @@ public void SdkLookup_Negative_Version()
257257
}
258258

259259
[Fact]
260-
public void SdkLookup_Must_Pick_The_Highest_Semantic_Version()
260+
public void PickHighestSemanticVersion()
261261
{
262262
GlobalJson.CreateEmpty(SharedState.CurrentWorkingDir);
263263

@@ -355,7 +355,7 @@ public void SdkLookup_Must_Pick_The_Highest_Semantic_Version()
355355
[InlineData("Latestfeature")]
356356
[InlineData("latestMINOR")]
357357
[InlineData("latESTMajor")]
358-
public void It_allows_case_insensitive_roll_forward_policy_names(string rollForward)
358+
public void RollForwardPolicy_CaseInsensitive(string rollForward)
359359
{
360360
const string Requested = "9999.0.100";
361361
AddAvailableSdkVersions(Requested);
@@ -369,7 +369,7 @@ public void It_allows_case_insensitive_roll_forward_policy_names(string rollForw
369369

370370
[Theory]
371371
[MemberData(nameof(InvalidGlobalJsonData))]
372-
public void It_falls_back_to_latest_sdk_for_invalid_global_json(string globalJsonContents, string[] messages)
372+
public void InvalidGlobalJson_FallsBackToLatestSdk(string globalJsonContents, string[] messages)
373373
{
374374
AddAvailableSdkVersions("9999.0.100", "9999.0.300-dummy.9", "9999.1.402");
375375

@@ -387,7 +387,7 @@ public void It_falls_back_to_latest_sdk_for_invalid_global_json(string globalJso
387387

388388
[Theory]
389389
[MemberData(nameof(SdkRollForwardData))]
390-
public void It_rolls_forward_as_expected(string policy, string requested, bool allowPrerelease, string expected, string[] installed)
390+
public void RollForward(string policy, string requested, bool allowPrerelease, string expected, string[] installed)
391391
{
392392
AddAvailableSdkVersions(installed);
393393

@@ -409,7 +409,7 @@ public void It_rolls_forward_as_expected(string policy, string requested, bool a
409409
}
410410

411411
[Fact]
412-
public void It_uses_latest_stable_sdk_if_allow_prerelease_is_false()
412+
public void AllowPrereleaseFalse_UseLatestRelease()
413413
{
414414
var installed = new string[] {
415415
"9999.1.702",
@@ -437,6 +437,172 @@ public void It_uses_latest_stable_sdk_if_allow_prerelease_is_false()
437437
.And.HaveStdErrContaining($"SDK path resolved to [{Path.Combine(ExecutableDotNet.BinPath, "sdk", ExpectedVersion)}]");
438438
}
439439

440+
[Fact]
441+
public void GlobalJson_Paths()
442+
{
443+
GlobalJson.Sdk sdk = new() { Paths = [] };
444+
string globalJsonPath = GlobalJson.Write(SharedState.CurrentWorkingDir, sdk );
445+
446+
// Add SDK versions
447+
AddAvailableSdkVersions("9999.0.4");
448+
449+
// Paths: none
450+
// Exe: 9999.0.4
451+
// Expected: no SDKs found
452+
RunTest()
453+
.Should().Fail()
454+
.And.FindAnySdk(false)
455+
.And.HaveStdErrContaining($"Empty search paths specified in global.json file: {globalJsonPath}");
456+
457+
sdk.Paths = [ GlobalJson.HostSdkPath ];
458+
globalJsonPath = GlobalJson.Write(SharedState.CurrentWorkingDir, sdk);
459+
460+
// Paths: $host$
461+
// Exe: 9999.0.4
462+
// Expected: 9999.0.4 from exe dir
463+
RunTest()
464+
.Should().Pass()
465+
.And.HaveStdErrContaining(ExpectedResolvedSdkOutput("9999.0.4"));
466+
467+
using TestArtifact custom = TestArtifact.Create("sdkPath");
468+
AddSdkToCustomPath(custom.Location, "9999.0.4");
469+
sdk.Paths = [ custom.Location ];
470+
globalJsonPath = GlobalJson.Write(SharedState.CurrentWorkingDir, sdk);
471+
472+
// Paths: custom (absolute)
473+
// Custom: 9999.0.4
474+
// Exe: 9999.0.4
475+
// Expected: 9999.0.4 from custom dir
476+
RunTest()
477+
.Should().Pass()
478+
.And.HaveStdErrContaining(ExpectedResolvedSdkOutput("9999.0.4", custom.Location));
479+
480+
string relativePath = Path.GetRelativePath(SharedState.CurrentWorkingDir, custom.Location);
481+
sdk.Paths = [ relativePath ];
482+
GlobalJson.Write(SharedState.CurrentWorkingDir, sdk);
483+
484+
// Paths: custom (relative, outside current directory)
485+
// Custom: 9999.0.4
486+
// Exe: 9999.0.4
487+
// Expected: 9999.0.4 from custom dir
488+
RunTest()
489+
.Should().Pass()
490+
.And.HaveStdErrContaining(ExpectedResolvedSdkOutput("9999.0.4", custom.Location));
491+
492+
string underCurrent = SharedState.CurrentWorkingDirArtifact.GetUniqueSubdirectory("sdkPath");
493+
AddSdkToCustomPath(underCurrent, "9999.0.4");
494+
495+
relativePath = Path.GetRelativePath(SharedState.CurrentWorkingDir, underCurrent);
496+
sdk.Paths = [relativePath];
497+
GlobalJson.Write(SharedState.CurrentWorkingDir, sdk);
498+
499+
// Paths: custom (relative, under current directory)
500+
// Custom: 9999.0.4
501+
// Exe: 9999.0.4
502+
// Expected: 9999.0.4 from custom dir
503+
RunTest()
504+
.Should().Pass()
505+
.And.HaveStdErrContaining(ExpectedResolvedSdkOutput("9999.0.4", Path.Combine(SharedState.CurrentWorkingDir, relativePath)));
506+
}
507+
508+
[Fact]
509+
public void GlobalJson_Paths_Multiple()
510+
{
511+
using TestArtifact custom = TestArtifact.Create("sdkPath");
512+
AddSdkToCustomPath(custom.Location, "9999.0.0");
513+
514+
GlobalJson.Sdk sdk = new() { Paths = [ custom.Location, GlobalJson.HostSdkPath ] };
515+
GlobalJson.Write(SharedState.CurrentWorkingDir, sdk);
516+
517+
// Add SDK versions
518+
AddAvailableSdkVersions("9999.0.4");
519+
520+
// Specified SDK
521+
// version: none
522+
// paths: custom, $host$
523+
// Custom: 9999.0.0
524+
// Exe: 9999.0.4
525+
// Expected: 9999.0.0 from custom dir
526+
RunTest()
527+
.Should().Pass()
528+
.And.HaveStdErrContaining(ExpectedResolvedSdkOutput("9999.0.0", custom.Location));
529+
530+
sdk.Version = "9999.0.3";
531+
GlobalJson.Write(SharedState.CurrentWorkingDir, sdk);
532+
533+
// Specified SDK
534+
// version: 9999.0.3
535+
// paths: custom, $host$
536+
// Custom: 9999.0.0
537+
// Exe: 9999.0.4
538+
// Expected: 9999.0.4 from exe dir
539+
RunTest()
540+
.Should().Pass()
541+
.And.HaveStdErrContaining(ExpectedResolvedSdkOutput("9999.0.4"));
542+
543+
sdk.Version = "9999.0.5";
544+
string globalJsonPath = GlobalJson.Write(SharedState.CurrentWorkingDir, sdk);
545+
546+
// Specified SDK
547+
// version: 9999.0.5
548+
// paths: custom, $host$
549+
// Custom: 9999.0.0
550+
// Exe: 9999.0.4
551+
// Expected: no compatible version
552+
RunTest()
553+
.Should().Fail()
554+
.And.NotFindCompatibleSdk(globalJsonPath, sdk.Version)
555+
.And.FindAnySdk(true);
556+
557+
// Verify we have the expected SDK versions
558+
RunTest("--list-sdks")
559+
.Should().Pass()
560+
.And.HaveStdOutContaining($"9999.0.0 [{custom.Location}")
561+
.And.HaveStdOutContaining($"9999.0.4 [{ExecutableDotNet.BinPath}");
562+
}
563+
564+
[Fact]
565+
public void GlobalJson_Paths_FirstMatch()
566+
{
567+
using TestArtifact custom1 = TestArtifact.Create("sdkPath1");
568+
AddSdkToCustomPath(custom1.Location, "9999.0.0");
569+
using TestArtifact custom2 = TestArtifact.Create("sdkPath2");
570+
AddSdkToCustomPath(custom2.Location, "9999.0.2");
571+
AddAvailableSdkVersions("9999.0.1");
572+
573+
GlobalJson.Sdk sdk = new() { Version = "9999.0.1", Paths = [ custom1.Location, custom2.Location, GlobalJson.HostSdkPath ] };
574+
GlobalJson.Write(SharedState.CurrentWorkingDir, sdk);
575+
576+
// Specified SDK
577+
// version: none
578+
// paths: custom1, custom2, $host$
579+
// Custom1: 9999.0.0
580+
// Custom2: 9999.0.2
581+
// Exe: 9999.0.1
582+
// Expected: 9999.0.2 from custom2 - first match is used, not best match (which would be exe which is an exact match)
583+
RunTest()
584+
.Should().Pass()
585+
.And.HaveStdErrContaining(ExpectedResolvedSdkOutput("9999.0.2", custom2.Location));
586+
587+
// Verify we have the expected SDK versions
588+
RunTest("--list-sdks")
589+
.Should().Pass()
590+
.And.HaveStdOutContaining($"9999.0.0 [{custom1.Location}")
591+
.And.HaveStdOutContaining($"9999.0.2 [{custom2.Location}")
592+
.And.HaveStdOutContaining($"9999.0.1 [{ExecutableDotNet.BinPath}");
593+
}
594+
595+
[Fact]
596+
public void GlobalJson_ErrorMessage()
597+
{
598+
GlobalJson.Sdk sdk = new() { ErrorMessage = "Custom SDK resolution error" };
599+
GlobalJson.Write(SharedState.CurrentWorkingDir, sdk);
600+
601+
RunTest()
602+
.Should().Fail()
603+
.And.HaveStdErrContaining(sdk.ErrorMessage);
604+
}
605+
440606
public static IEnumerable<object[]> InvalidGlobalJsonData
441607
{
442608
get
@@ -472,7 +638,7 @@ public static IEnumerable<object[]> InvalidGlobalJsonData
472638

473639
// Use an invalid version value
474640
yield return new object[] {
475-
GlobalJson.FormatVersionSettings(version: "invalid"),
641+
GlobalJson.FormatSettings(new GlobalJson.Sdk() { Version = "invalid" }),
476642
new[] {
477643
"Version 'invalid' is not valid for the 'sdk/version' value",
478644
IgnoringSDKSettings
@@ -490,7 +656,7 @@ public static IEnumerable<object[]> InvalidGlobalJsonData
490656

491657
// Use a policy but no version
492658
yield return new object[] {
493-
GlobalJson.FormatVersionSettings(policy: "latestPatch"),
659+
GlobalJson.FormatSettings(new GlobalJson.Sdk() { RollForward = "latestPatch" }),
494660
new[] {
495661
"The roll-forward policy 'latestPatch' requires a 'sdk/version' value",
496662
IgnoringSDKSettings
@@ -499,7 +665,7 @@ public static IEnumerable<object[]> InvalidGlobalJsonData
499665

500666
// Use an invalid policy value
501667
yield return new object[] {
502-
GlobalJson.FormatVersionSettings(policy: "invalid"),
668+
GlobalJson.FormatSettings(new GlobalJson.Sdk() { RollForward = "invalid" }),
503669
new[] {
504670
"The roll-forward policy 'invalid' is not supported for the 'sdk/rollForward' value",
505671
IgnoringSDKSettings
@@ -517,7 +683,7 @@ public static IEnumerable<object[]> InvalidGlobalJsonData
517683

518684
// Use a prerelease version and allowPrerelease = false
519685
yield return new object[] {
520-
GlobalJson.FormatVersionSettings(version: "9999.1.402-preview1", allowPrerelease: false),
686+
GlobalJson.FormatSettings(new GlobalJson.Sdk() { Version = "9999.1.402-preview1", AllowPrerelease = false }),
521687
new[] { "Ignoring the 'sdk/allowPrerelease' value" }
522688
};
523689
}
@@ -992,6 +1158,15 @@ public static IEnumerable<object[]> SdkRollForwardData
9921158
}
9931159
}
9941160

1161+
private static void AddSdkToCustomPath(string sdkRoot, string version)
1162+
{
1163+
DotNetBuilder.AddMockSDK(sdkRoot, version, version);
1164+
1165+
// Add a mock framework matching the runtime version for the mock SDK
1166+
// This allows the host to successfully resolve frameworks for the SDK at the custom location
1167+
DotNetBuilder.AddMicrosoftNETCoreAppFrameworkMockHostPolicy(sdkRoot, version);
1168+
}
1169+
9951170
// This method adds a list of new sdk version folders in the specified directory.
9961171
// The actual contents are 'fake' and the minimum required for SDK discovery.
9971172
// The dotnet.runtimeconfig.json created uses a dummy framework version (9999.0.0)
@@ -1003,8 +1178,8 @@ private void AddAvailableSdkVersions(params string[] availableVersions)
10031178
}
10041179
}
10051180

1006-
private string ExpectedResolvedSdkOutput(string expectedVersion)
1007-
=> Path.Combine("Using .NET SDK dll=[", ExecutableDotNet.BinPath, "sdk", expectedVersion, "dotnet.dll]");
1181+
private string ExpectedResolvedSdkOutput(string expectedVersion, string rootPath = null)
1182+
=> $"Using .NET SDK dll=[{Path.Combine(rootPath == null ? ExecutableDotNet.BinPath : rootPath, "sdk", expectedVersion, "dotnet.dll")}]";
10081183

10091184
private CommandResult RunTest() => RunTest("help");
10101185

@@ -1021,6 +1196,7 @@ public sealed class SharedTestState : IDisposable
10211196
{
10221197
public TestArtifact BaseArtifact { get; }
10231198

1199+
public TestArtifact CurrentWorkingDirArtifact { get; }
10241200
public string CurrentWorkingDir { get; }
10251201

10261202
public SharedTestState()
@@ -1035,10 +1211,12 @@ public SharedTestState()
10351211
.AddMockSDK("10000.0.0", "9999.0.0")
10361212
.Build();
10371213
CurrentWorkingDir = currentWorkingSdk.BinPath;
1214+
CurrentWorkingDirArtifact = new TestArtifact(CurrentWorkingDir);
10381215
}
10391216

10401217
public void Dispose()
10411218
{
1219+
CurrentWorkingDirArtifact.Dispose();
10421220
BaseArtifact.Dispose();
10431221
}
10441222
}

0 commit comments

Comments
 (0)