From ac9cd9ea20db86aaa0340440e0091247b487296e Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 18 Jul 2025 12:32:50 -0700 Subject: [PATCH 1/3] Add `dotnetSDKPath` Setting for User SDKs and Projects The .NET `dotnet.findPath` API provided by the .NET Install Tool only uses machine wide SDKs at this time. This API is used by C# and C#DK, as well as others, to resolve the .NET SDK to use for the project system. We can update this to better enable local SDK or custom SDK scenarios. In general, the PATH can be updated to an admin SDK, but users may get confused if they have multiple SDKs on the PATH, e.g. with multiple lines in their rc profile, which has happened internally. In addition, DOTNET_ROOT and other variables impact the selection of DOTNET, and this may further complicate the issue, but DOTNET_ROOT is only applicable to the Runtime selection. In the past, `dotnetPath` was supported by C#/OmniSharp but it was deprecated. I'm unsure of the name 'dotnetSDKPath' : unfortunately we can't change the old setting, `existingDotnetPath`. Caveats: `existingDotnetPath`: Is a path to the dotnet.exe that should have runtime beside it, and is used to run the code that is shipped within C#/C#DK and other extensions. `dotnetSDKPath`: Is a path to the dotnet.exe that should have an SDK beside it (and therefore also an SDK), that should be used by the project system and CPS to run the users project. We may need to have further work from the debugger team or coordinate with them as they may have their own setting. This does allow a user to override the system PATH and use a local SDK which may not be secure or up to date in an enterprise scenario. However, VS Code generally does not run in an elevated context and suggests against doing so. We should ensure that elevation is not leveraged by the internal calling APIs, and I dont believe that is the case. --- vscode-dotnet-runtime-extension/package.json | 14 ++++++++-- .../src/extension.ts | 27 ++++++++++++++----- .../DotnetCoreAcquisitionExtension.test.ts | 7 ++--- .../src/test/mocks/MockObjects.ts | 6 ++++- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/vscode-dotnet-runtime-extension/package.json b/vscode-dotnet-runtime-extension/package.json index d427dd907e..cd32a4666c 100644 --- a/vscode-dotnet-runtime-extension/package.json +++ b/vscode-dotnet-runtime-extension/package.json @@ -83,8 +83,8 @@ }, "dotnetAcquisitionExtension.existingDotnetPath": { "type": "array", - "markdownDescription": "The path to an existing .NET host executable for an extension's code to run under, not for your project to run under.\nRestart VS Code to apply changes.\n\n⚠️ This is NOT the .NET Runtime that your project will use to run. Extensions such as `C#`, `C# DevKit`, and more have components written in .NET. This .NET PATH is the `dotnet.exe` that these extensions will use to run their code, not your code.\n\nUsing a path value in which .NET does not meet the requirements of a specific extension will cause that extension to fail.\n\n🚀 The version of .NET that is used for your project is determined by the .NET host, or dotnet.exe. The .NET host picks a runtime based on your project. To use a specific version of .NET for your project, install the .NET SDK using the `.NET Install Tool - Install SDK System-Wide` command, install .NET manually using [our instructions](https://dotnet.microsoft.com/download), or edit your PATH environment variable to point to a `dotnet.exe` that has an `/sdk/` folder with only one SDK.", - "description": "The path to an existing .NET host executable for an extension's code to run under, not for your project to run under.\nRestart VS Code to apply changes.\n\n⚠️ This is NOT the .NET Runtime that your project will use to run. Extensions such as 'C#', 'C# DevKit', and more have components written in .NET. This .NET PATH is the 'dotnet.exe' that these extensions will use to run their code, not your code.\n\nUsing a path value in which .NET does not meet the requirements of a specific extension will cause that extension to fail.\n\n🚀 The version of .NET that is used for your project is determined by the .NET host, or dotnet.exe. The .NET host picks a runtime based on your project. To use a specific version of .NET for your project, install the .NET SDK using the '.NET Install Tool - Install SDK System-Wide' command, use the instructions at https://dotnet.microsoft.com/download to manually install the .NET SDK, or edit your PATH environment variable to point to a 'dotnet.exe' that has an '/sdk/' folder with only one SDK.", + "markdownDescription": "The path to an existing .NET executable for an extension's code to run under, not for your project to run under.\nRestart VS Code to apply changes.\n\n⚠️ This is NOT the .NET Runtime that your project will use to run. Extensions such as `C#`, `C# DevKit`, and more have components written in .NET. This .NET PATH is the `dotnet.exe` that these extensions will use to run their code, not your code.\n\nUsing a path value in which .NET does not meet the requirements of a specific extension will cause that extension to fail.\n\n🚀 The version of .NET that is used for your project is determined by the .NET host, or dotnet.exe. The .NET host picks a runtime based on your project. To use a specific version of .NET for your project, install the .NET SDK using the `.NET Install Tool - Install SDK System-Wide` command, install .NET manually using [our instructions](https://dotnet.microsoft.com/download), or edit your PATH environment variable to point to a `dotnet.exe` that has an `/sdk/` folder with only one SDK.", + "description": "The path to an existing .NET executable for an extension's code to run under, not for your project to run under.\nRestart VS Code to apply changes.\n\n⚠️ This is NOT the .NET Runtime that your project will use to run. Extensions such as 'C#', 'C# DevKit', and more have components written in .NET. This .NET PATH is the 'dotnet.exe' that these extensions will use to run their code, not your code.\n\nUsing a path value in which .NET does not meet the requirements of a specific extension will cause that extension to fail.\n\n🚀 The version of .NET that is used for your project is determined by the .NET host, or dotnet.exe. The .NET host picks a runtime based on your project. To use a specific version of .NET for your project, install the .NET SDK using the '.NET Install Tool - Install SDK System-Wide' command, use the instructions at https://dotnet.microsoft.com/download to manually install the .NET SDK, or edit your PATH environment variable to point to a 'dotnet.exe' that has an '/sdk/' folder with only one SDK.", "examples": [ "C:\\Program Files\\dotnet\\dotnet.exe", "/usr/local/share/dotnet/dotnet", @@ -100,6 +100,16 @@ "/usr/lib/dotnet/dotnet" ] }, + "dotnetAcquisitionExtension.dotnetSDKPath": { + "type": "string", + "markdownDescription": "The path to a dotnet executable your project will use.\nRestart VS Code to apply changes.\n\n⚠️ By default, `dotnet.exe` picks the latest applicable SDK in the `/sdk/` folder next to itself. To use a specific SDK, make sure only that version is in the `/sdk/` folder. The SDK also includes a runtime.", + "description": "The path to a dotnet executable your project will use.\nRestart VS Code to apply changes.\n\n⚠️ By default, `dotnet.exe` picks the latest applicable SDK in the `/sdk/` folder next to itself. To use a specific SDK, make sure only that version is in the `/sdk/` folder. The SDK also includes a runtime.", + "examples": [ + "C:\\Program Files\\dotnet\\dotnet.exe", + "/usr/local/share/dotnet/dotnet", + "/usr/lib/dotnet/dotnet" + ] + }, "dotnetAcquisitionExtension.proxyUrl": { "type": "string", "description": "URL to a proxy if you use one, such as: https://proxy:port" diff --git a/vscode-dotnet-runtime-extension/src/extension.ts b/vscode-dotnet-runtime-extension/src/extension.ts index da00971434..19939b7d81 100644 --- a/vscode-dotnet-runtime-extension/src/extension.ts +++ b/vscode-dotnet-runtime-extension/src/extension.ts @@ -93,6 +93,7 @@ namespace configKeys export const enableTelemetry = 'enableTelemetry'; export const existingPath = 'existingDotnetPath'; export const existingSharedPath = 'sharedExistingDotnetPath' + export const dotnetSDKPath = 'dotnetSDKPath'; export const proxyUrl = 'proxyUrl'; export const allowInvalidPaths = 'allowInvalidPaths'; export const cacheTimeToLiveMultiplier = 'cacheTimeToLiveMultiplier'; @@ -516,15 +517,27 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex globalEventStream.post(new DotnetFindPathLookupSetting(`Looking up vscode setting.`)); const workerContext = getAcquisitionWorkerContext(commandContext.acquireContext.mode, commandContext.acquireContext); - const existingPath = await resolveExistingPathIfExists(existingPathConfigWorker, commandContext.acquireContext, workerContext, utilContext, commandContext.versionSpecRequirement); // The setting is not intended to be used as the SDK, only the runtime for extensions to run on. Ex: PowerShell policy doesn't allow us to install the runtime, let users set the path manually. - if (existingPath && commandContext.acquireContext.mode !== 'sdk') + if (commandContext.acquireContext.mode !== 'sdk') { - // We don't need to validate the existing path as it gets validated in the lookup logic already. - globalEventStream.post(new DotnetFindPathSettingFound(`Found vscode setting.`)); - loggingObserver.dispose(); - return existingPath; + const existingHostRuntimeInternalExtensionPath = await resolveExistingPathIfExists(existingPathConfigWorker, commandContext.acquireContext, workerContext, utilContext, commandContext.versionSpecRequirement); + if (existingHostRuntimeInternalExtensionPath) + { + // We don't need to validate the existing path as it gets validated in the lookup logic already. + globalEventStream.post(new DotnetFindPathSettingFound(`Found vscode setting.`)); + loggingObserver.dispose(); + return existingHostRuntimeInternalExtensionPath; + } + } + else if (commandContext.acquireContext.mode === 'sdk') + { + const existingHostSDKUserPath = extensionConfiguration.get(configKeys.dotnetSDKPath); + if (existingHostSDKUserPath) + { + // Don't validate the existing SDK path as it is a user setting and we assume the user knows what they are doing. + return { dotnetPath: existingHostSDKUserPath }; + } } const validator = new DotnetConditionValidator(workerContext, utilContext); @@ -584,7 +597,7 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex loggingObserver.dispose(); globalEventStream.post(new DotnetFindPathNoPathMetCondition(`Could not find a single host path that met the conditions. -existingPath : ${existingPath?.dotnetPath} +existingPath : ${existingHostRuntimeInternalExtensionPath?.dotnetPath} onPath : ${JSON.stringify(dotnetsOnPATH)} onRealPath : ${JSON.stringify(dotnetsOnRealPATH)} onRoot : ${dotnetOnROOT} diff --git a/vscode-dotnet-runtime-extension/src/test/functional/DotnetCoreAcquisitionExtension.test.ts b/vscode-dotnet-runtime-extension/src/test/functional/DotnetCoreAcquisitionExtension.test.ts index 49ed4d8365..5a5e38415c 100644 --- a/vscode-dotnet-runtime-extension/src/test/functional/DotnetCoreAcquisitionExtension.test.ts +++ b/vscode-dotnet-runtime-extension/src/test/functional/DotnetCoreAcquisitionExtension.test.ts @@ -61,6 +61,7 @@ suite('DotnetCoreAcquisitionExtension End to End', function () const existingPathVersionToFake = '5.0.1~x64' const pathWithIncorrectVersionForTest = path.join(__dirname, `/.dotnet/${existingPathVersionToFake}/${getDotnetExecutable()}`); + const pathForSDKSetting = path.join(__dirname, `/.dotnet/${getDotnetExecutable()}`); const mockExistingPathsWithGlobalConfig: IExistingPaths = { individualizedExtensionPaths: [{ extensionId: 'alternative.extension', path: pathWithIncorrectVersionForTest }], @@ -104,7 +105,7 @@ suite('DotnetCoreAcquisitionExtension End to End', function () extension.ReEnableActivationForManualActivation(); extension.activate(extensionContext, { telemetryReporter: new MockTelemetryReporter(), - extensionConfiguration: new MockExtensionConfiguration(mockExistingPathsWithGlobalConfig.individualizedExtensionPaths!, true, mockExistingPathsWithGlobalConfig.sharedExistingPath!), + extensionConfiguration: new MockExtensionConfiguration(mockExistingPathsWithGlobalConfig.individualizedExtensionPaths!, true, mockExistingPathsWithGlobalConfig.sharedExistingPath!, false, pathForSDKSetting), displayWorker: mockDisplayWorker, }); }); @@ -658,9 +659,9 @@ Paths: 'acquire returned: ${resultForAcquiringPathSettingRuntime.dotnetPath} whi const findPath = await vscode.commands.executeCommand('dotnet.findPath', { acquireContext: Object.assign({}, context, { mode: 'runtime' }), versionSpecRequirement: 'equal' }); assert.equal(findPath!.dotnetPath, pathWithIncorrectVersionForTest, 'findPath uses vscode setting for runtime'); // this is set for the alternative.extension in the settings - // check that find path does not use the setting even if its set because it should not use the wrong thing that does not meet the condition + // check that find path does uses the SDK setting for SDK lookup, and not the runtime setting const findSDKPath = await vscode.commands.executeCommand('dotnet.findPath', { acquireContext: Object.assign({}, context, { mode: 'sdk' }), versionSpecRequirement: 'equal' }); - assert.equal(findSDKPath?.dotnetPath ?? undefined, undefined, 'findPath does not find path setting for the SDK'); + assert.equal(findSDKPath?.dotnetPath ?? undefined, pathForSDKSetting, 'findPath uses find path setting for the SDK'); }).timeout(standardTimeoutTime * 3); test('List Sdks & Runtimes', async () => diff --git a/vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts b/vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts index 6ce72577df..1a02477fb5 100644 --- a/vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts +++ b/vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts @@ -693,7 +693,7 @@ export class MockLoggingObserver implements ILoggingObserver export class MockExtensionConfiguration implements IExtensionConfiguration { constructor(private readonly existingPaths: ILocalExistingPath[], private readonly enableTelemetry: boolean, private readonly existingSharedPath: string, - public allowInvalidPaths = false + public allowInvalidPaths = false, public sdkPath: string = '' ) {} public update(section: string, value: T): Thenable @@ -724,6 +724,10 @@ export class MockExtensionConfiguration implements IExtensionConfiguration { return true as unknown as T; } + else if (name === 'dotnetSDKPath') + { + return this.sdkPath as unknown as T; + } else { return undefined; From 1ee5ea0822757c9077e5ad166b0d6809f9a50062 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Fri, 18 Jul 2025 12:41:41 -0700 Subject: [PATCH 2/3] Fix logging --- vscode-dotnet-runtime-extension/src/extension.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vscode-dotnet-runtime-extension/src/extension.ts b/vscode-dotnet-runtime-extension/src/extension.ts index 19939b7d81..efd4d2e174 100644 --- a/vscode-dotnet-runtime-extension/src/extension.ts +++ b/vscode-dotnet-runtime-extension/src/extension.ts @@ -517,11 +517,13 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex globalEventStream.post(new DotnetFindPathLookupSetting(`Looking up vscode setting.`)); const workerContext = getAcquisitionWorkerContext(commandContext.acquireContext.mode, commandContext.acquireContext); + let existingPathForLog = ''; // The setting is not intended to be used as the SDK, only the runtime for extensions to run on. Ex: PowerShell policy doesn't allow us to install the runtime, let users set the path manually. if (commandContext.acquireContext.mode !== 'sdk') { const existingHostRuntimeInternalExtensionPath = await resolveExistingPathIfExists(existingPathConfigWorker, commandContext.acquireContext, workerContext, utilContext, commandContext.versionSpecRequirement); + existingPathForLog = existingHostRuntimeInternalExtensionPath?.dotnetPath ?? ''; if (existingHostRuntimeInternalExtensionPath) { // We don't need to validate the existing path as it gets validated in the lookup logic already. @@ -533,6 +535,7 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex else if (commandContext.acquireContext.mode === 'sdk') { const existingHostSDKUserPath = extensionConfiguration.get(configKeys.dotnetSDKPath); + existingPathForLog = existingHostSDKUserPath ?? ''; if (existingHostSDKUserPath) { // Don't validate the existing SDK path as it is a user setting and we assume the user knows what they are doing. @@ -597,7 +600,7 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex loggingObserver.dispose(); globalEventStream.post(new DotnetFindPathNoPathMetCondition(`Could not find a single host path that met the conditions. -existingPath : ${existingHostRuntimeInternalExtensionPath?.dotnetPath} +codePathSetting : ${existingPathForLog} onPath : ${JSON.stringify(dotnetsOnPATH)} onRealPath : ${JSON.stringify(dotnetsOnRealPATH)} onRoot : ${dotnetOnROOT} From eca7b94f564becfa50c88d1e4b67a8ae48c2e8e2 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Mon, 28 Jul 2025 11:46:42 -0700 Subject: [PATCH 3/3] Validate the SDK Path We decided to do this for the runtimes, may as well do it for the SDK as well. --- vscode-dotnet-runtime-extension/src/extension.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/vscode-dotnet-runtime-extension/src/extension.ts b/vscode-dotnet-runtime-extension/src/extension.ts index 08cc28bdad..a3903d794b 100644 --- a/vscode-dotnet-runtime-extension/src/extension.ts +++ b/vscode-dotnet-runtime-extension/src/extension.ts @@ -528,6 +528,7 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex globalEventStream.post(new DotnetFindPathLookupSetting(`Looking up vscode setting.`)); const workerContext = getAcquisitionWorkerContext(commandContext.acquireContext.mode, commandContext.acquireContext); let existingPathForLog = ''; + const validator = new DotnetConditionValidator(workerContext, utilContext); // The setting is not intended to be used as the SDK, only the runtime for extensions to run on. Ex: PowerShell policy doesn't allow us to install the runtime, let users set the path manually. if (commandContext.acquireContext.mode !== 'sdk') @@ -548,12 +549,15 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex existingPathForLog = existingHostSDKUserPath ?? ''; if (existingHostSDKUserPath) { - // Don't validate the existing SDK path as it is a user setting and we assume the user knows what they are doing. - return { dotnetPath: existingHostSDKUserPath }; + const validatedSDKPath = await getPathIfValid(existingHostSDKUserPath, validator, commandContext); + if (validatedSDKPath) + { + loggingObserver.dispose(); + return { dotnetPath: validatedSDKPath }; + } } } - const validator = new DotnetConditionValidator(workerContext, utilContext); const finder = new DotnetPathFinder(workerContext, utilContext); const dotnetOnShellSpawn = (await finder.findDotnetFastFromListOnly(requestedArchitecture))?.[0] ?? '';