Skip to content

Commit ffe75f5

Browse files
committed
Use the .NET runtime extension to find an appropriate .NET install
1 parent c51b3c7 commit ffe75f5

File tree

6 files changed

+107
-168
lines changed

6 files changed

+107
-168
lines changed

l10n/bundle.l10n.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
".NET Test Log": ".NET Test Log",
33
".NET NuGet Restore": ".NET NuGet Restore",
4+
"Update and reload": "Update and reload",
5+
"The {0} extension requires version {1} or greater of the .NET Install Tool ({2}) extension. Please update to continue": "The {0} extension requires version {1} or greater of the .NET Install Tool ({2}) extension. Please update to continue",
6+
"Version {0} of the .NET Install Tool ({2}) was not found, will not activate.": "Version {0} of the .NET Install Tool ({2}) was not found, will not activate.",
47
"How to setup Remote Debugging": "How to setup Remote Debugging",
58
"The C# extension for Visual Studio Code is incompatible on {0} {1} with the VS Code Remote Extensions. To see avaliable workarounds, click on '{2}'.": "The C# extension for Visual Studio Code is incompatible on {0} {1} with the VS Code Remote Extensions. To see avaliable workarounds, click on '{2}'.",
69
"The C# extension for Visual Studio Code is incompatible on {0} {1}.": "The C# extension for Visual Studio Code is incompatible on {0} {1}.",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
// Contains APIs defined by the vscode-dotnet-runtime extension
7+
8+
export interface IDotnetAcquireResult {
9+
dotnetPath: string;
10+
}
11+
12+
export interface IDotnetFindPathContext {
13+
acquireContext: IDotnetAcquireContext;
14+
versionSpecRequirement: DotnetVersionSpecRequirement;
15+
}
16+
17+
/**
18+
* https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetAcquireContext.ts
19+
*/
20+
interface IDotnetAcquireContext {
21+
version: string;
22+
requestingExtensionId?: string;
23+
errorConfiguration?: AcquireErrorConfiguration;
24+
installType?: DotnetInstallType;
25+
architecture?: string | null | undefined;
26+
mode?: DotnetInstallMode;
27+
}
28+
29+
/**
30+
* https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetAcquireContext.ts#L53C8-L53C52
31+
*/
32+
type DotnetInstallType = 'local' | 'global';
33+
34+
/**
35+
* https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/Utils/ErrorHandler.ts#L22
36+
*/
37+
enum AcquireErrorConfiguration {
38+
DisplayAllErrorPopups = 0,
39+
DisableErrorPopups = 1,
40+
}
41+
42+
/**
43+
* https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/Acquisition/DotnetInstallMode.ts
44+
*/
45+
type DotnetInstallMode = 'sdk' | 'runtime' | 'aspnetcore';
46+
47+
/**
48+
* https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/DotnetVersionSpecRequirement.ts
49+
*/
50+
type DotnetVersionSpecRequirement = 'equal' | 'greater_than_or_equal' | 'less_than_or_equal';

src/lsptoolshost/dotnetRuntimeExtensionResolver.ts

Lines changed: 24 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -5,72 +5,61 @@
55

66
import * as path from 'path';
77
import * as vscode from 'vscode';
8-
import * as semver from 'semver';
98
import { HostExecutableInformation } from '../shared/constants/hostExecutableInformation';
109
import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver';
1110
import { PlatformInformation } from '../shared/platform';
1211
import { commonOptions, languageServerOptions } from '../shared/options';
1312
import { existsSync } from 'fs';
1413
import { CSharpExtensionId } from '../constants/csharpExtensionId';
15-
import { getDotnetInfo } from '../shared/utils/getDotnetInfo';
1614
import { readFile } from 'fs/promises';
17-
import { RuntimeInfo } from '../shared/utils/dotnetInfo';
15+
import { IDotnetFindPathContext } from './dotnetRuntimeExtensionApi';
1816

1917
export const DotNetRuntimeVersion = '8.0.10';
2018

21-
interface IDotnetAcquireResult {
22-
dotnetPath: string;
23-
}
24-
2519
/**
2620
* Resolves the dotnet runtime for a server executable from given options and the dotnet runtime VSCode extension.
2721
*/
2822
export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {
2923
constructor(
3024
private platformInfo: PlatformInformation,
31-
/**
32-
* This is a function instead of a string because the server path can change while the extension is active (when the option changes).
33-
*/
34-
private getServerPath: (platform: PlatformInformation) => string,
3525
private channel: vscode.OutputChannel,
3626
private extensionPath: string
3727
) {}
3828

3929
private hostInfo: HostExecutableInformation | undefined;
4030

4131
async getHostExecutableInfo(): Promise<HostExecutableInformation> {
42-
let dotnetRuntimePath = commonOptions.dotnetPath;
43-
const serverPath = this.getServerPath(this.platformInfo);
44-
45-
// Check if we can find a valid dotnet from dotnet --version on the PATH.
46-
if (!dotnetRuntimePath) {
47-
const dotnetPath = await this.findDotnetFromPath();
48-
if (dotnetPath) {
49-
return {
50-
version: '' /* We don't need to know the version - we've already verified its high enough */,
51-
path: dotnetPath,
52-
env: this.getEnvironmentVariables(dotnetPath),
53-
};
32+
let dotnetExecutablePath: string;
33+
if (commonOptions.dotnetPath) {
34+
const dotnetExecutableName = this.getDotnetExecutableName();
35+
dotnetExecutablePath = path.join(commonOptions.dotnetPath, dotnetExecutableName);
36+
} else {
37+
if (this.hostInfo) {
38+
return this.hostInfo;
5439
}
55-
}
56-
57-
// We didn't find it on the path, see if we can install the correct runtime using the runtime extension.
58-
if (!dotnetRuntimePath) {
59-
const dotnetInfo = await this.acquireDotNetProcessDependencies(serverPath);
60-
dotnetRuntimePath = path.dirname(dotnetInfo.path);
61-
}
6240

63-
const dotnetExecutableName = this.getDotnetExecutableName();
64-
const dotnetExecutablePath = path.join(dotnetRuntimePath, dotnetExecutableName);
65-
if (!existsSync(dotnetExecutablePath)) {
66-
throw new Error(`Cannot find dotnet path '${dotnetExecutablePath}'`);
41+
this.channel.appendLine(`Acquiring .NET runtime version ${DotNetRuntimeVersion}`);
42+
const extensionArchitecture = (await this.getArchitectureFromTargetPlatform()) ?? process.arch;
43+
const findPathRequest: IDotnetFindPathContext = {
44+
acquireContext: {
45+
version: DotNetRuntimeVersion,
46+
requestingExtensionId: CSharpExtensionId,
47+
architecture: extensionArchitecture,
48+
mode: 'runtime',
49+
},
50+
versionSpecRequirement: 'greater_than_or_equal',
51+
};
52+
const result = await vscode.commands.executeCommand<string>('dotnet.findPath', findPathRequest);
53+
dotnetExecutablePath = result;
6754
}
6855

69-
return {
56+
const hostInfo = {
7057
version: '' /* We don't need to know the version - we've already downloaded the correct one */,
7158
path: dotnetExecutablePath,
7259
env: this.getEnvironmentVariables(dotnetExecutablePath),
7360
};
61+
this.hostInfo = hostInfo;
62+
return hostInfo;
7463
}
7564

7665
private getEnvironmentVariables(dotnetExecutablePath: string): NodeJS.ProcessEnv {
@@ -96,128 +85,6 @@ export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {
9685
return env;
9786
}
9887

99-
/**
100-
* Acquires the .NET runtime if it is not already present.
101-
* @returns The path to the .NET runtime
102-
*/
103-
private async acquireRuntime(): Promise<HostExecutableInformation> {
104-
if (this.hostInfo) {
105-
return this.hostInfo;
106-
}
107-
108-
let status = await vscode.commands.executeCommand<IDotnetAcquireResult>('dotnet.acquireStatus', {
109-
version: DotNetRuntimeVersion,
110-
requestingExtensionId: CSharpExtensionId,
111-
});
112-
if (status === undefined) {
113-
await vscode.commands.executeCommand('dotnet.showAcquisitionLog');
114-
115-
status = await vscode.commands.executeCommand<IDotnetAcquireResult>('dotnet.acquire', {
116-
version: DotNetRuntimeVersion,
117-
requestingExtensionId: CSharpExtensionId,
118-
});
119-
if (!status?.dotnetPath) {
120-
throw new Error('Could not resolve the dotnet path!');
121-
}
122-
}
123-
124-
return (this.hostInfo = {
125-
version: DotNetRuntimeVersion,
126-
path: status.dotnetPath,
127-
env: process.env,
128-
});
129-
}
130-
131-
/**
132-
* Acquires the .NET runtime and any other dependencies required to spawn a particular .NET executable.
133-
* @param path The path to the entrypoint assembly. Typically a .dll.
134-
*/
135-
private async acquireDotNetProcessDependencies(path: string): Promise<HostExecutableInformation> {
136-
const dotnetInfo = await this.acquireRuntime();
137-
138-
const args = [path];
139-
// This will install any missing Linux dependencies.
140-
await vscode.commands.executeCommand('dotnet.ensureDotnetDependencies', {
141-
command: dotnetInfo.path,
142-
arguments: args,
143-
});
144-
145-
return dotnetInfo;
146-
}
147-
148-
/**
149-
* Checks dotnet --version to see if the value on the path is greater than the minimum required version.
150-
* This is adapated from similar O# server logic and should be removed when we have a stable acquisition extension.
151-
* @returns true if the dotnet version is greater than the minimum required version, false otherwise.
152-
*/
153-
private async findDotnetFromPath(): Promise<string | undefined> {
154-
try {
155-
const dotnetInfo = await getDotnetInfo([]);
156-
157-
const extensionArchitecture = await this.getArchitectureFromTargetPlatform();
158-
const dotnetArchitecture = dotnetInfo.Architecture;
159-
160-
// If the extension arhcitecture is defined, we check that it matches the dotnet architecture.
161-
// If its undefined we likely have a platform neutral server and assume it can run on any architecture.
162-
if (extensionArchitecture && extensionArchitecture !== dotnetArchitecture) {
163-
throw new Error(
164-
`The architecture of the .NET runtime (${dotnetArchitecture}) does not match the architecture of the extension (${extensionArchitecture}).`
165-
);
166-
}
167-
168-
// Verify that the dotnet we found includes a runtime version that is compatible with our requirement.
169-
const requiredRuntimeVersion = semver.parse(`${DotNetRuntimeVersion}`);
170-
if (!requiredRuntimeVersion) {
171-
throw new Error(`Unable to parse minimum required version ${DotNetRuntimeVersion}`);
172-
}
173-
174-
const coreRuntimeVersions = dotnetInfo.Runtimes['Microsoft.NETCore.App'];
175-
let matchingRuntime: RuntimeInfo | undefined = undefined;
176-
for (const runtime of coreRuntimeVersions) {
177-
// We consider a match if the runtime is greater than or equal to the required version since we roll forward.
178-
if (semver.gte(runtime.Version, requiredRuntimeVersion)) {
179-
matchingRuntime = runtime;
180-
break;
181-
}
182-
}
183-
184-
if (!matchingRuntime) {
185-
throw new Error(
186-
`No compatible .NET runtime found. Minimum required version is ${DotNetRuntimeVersion}.`
187-
);
188-
}
189-
190-
// The .NET install layout is a well known structure on all platforms.
191-
// See https://github.com/dotnet/designs/blob/main/accepted/2020/install-locations.md#net-core-install-layout
192-
//
193-
// Therefore we know that the runtime path is always in <install root>/shared/<runtime name>
194-
// and the dotnet executable is always at <install root>/dotnet(.exe).
195-
//
196-
// Since dotnet --list-runtimes will always use the real assembly path to output the runtime folder (no symlinks!)
197-
// we know the dotnet executable will be two folders up in the install root.
198-
const runtimeFolderPath = matchingRuntime.Path;
199-
const installFolder = path.dirname(path.dirname(runtimeFolderPath));
200-
const dotnetExecutablePath = path.join(installFolder, this.getDotnetExecutableName());
201-
if (!existsSync(dotnetExecutablePath)) {
202-
throw new Error(
203-
`dotnet executable path does not exist: ${dotnetExecutablePath}, dotnet installation may be corrupt.`
204-
);
205-
}
206-
207-
this.channel.appendLine(`Using dotnet configured on PATH`);
208-
return dotnetExecutablePath;
209-
} catch (e) {
210-
this.channel.appendLine(
211-
'Failed to find dotnet info from path, falling back to acquire runtime via ms-dotnettools.vscode-dotnet-runtime'
212-
);
213-
if (e instanceof Error) {
214-
this.channel.appendLine(e.message);
215-
}
216-
}
217-
218-
return undefined;
219-
}
220-
22188
private async getArchitectureFromTargetPlatform(): Promise<string | undefined> {
22289
const vsixManifestFile = path.join(this.extensionPath, '.vsixmanifest');
22390
if (!existsSync(vsixManifestFile)) {

src/lsptoolshost/roslynLanguageServer.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1074,7 +1074,6 @@ export async function activateRoslynLanguageServer(
10741074

10751075
const hostExecutableResolver = new DotnetRuntimeExtensionResolver(
10761076
platformInfo,
1077-
getServerPath,
10781077
outputChannel,
10791078
context.extensionPath
10801079
);

src/main.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { debugSessionTracker } from './coreclrDebug/provisionalDebugSessionTrack
4141
import { getComponentFolder } from './lsptoolshost/builtInComponents';
4242
import { activateOmniSharpLanguageServer, ActivationResult } from './omnisharp/omnisharpLanguageServer';
4343
import { ActionOption, showErrorMessage } from './shared/observers/utils/showMessage';
44+
import { lt } from 'semver';
4445

4546
export async function activate(
4647
context: vscode.ExtensionContext
@@ -81,6 +82,35 @@ export async function activate(
8182
requiredPackageIds.push('OmniSharp');
8283
}
8384

85+
const dotnetRuntimeExtensionId = 'ms-dotnettools.vscode-dotnet-runtime';
86+
const requiredDotnetRuntimeExtensionVersion = '2.2.1';
87+
88+
const dotnetRuntimeExtension = vscode.extensions.getExtension(dotnetRuntimeExtensionId);
89+
const dotnetRuntimeExtensionVersion = dotnetRuntimeExtension?.packageJSON.version;
90+
if (lt(dotnetRuntimeExtensionVersion, requiredDotnetRuntimeExtensionVersion)) {
91+
const button = vscode.l10n.t('Update and reload');
92+
const prompt = vscode.l10n.t(
93+
'The {0} extension requires version {1} or greater of the .NET Install Tool ({2}) extension. Please update to continue',
94+
context.extension.packageJSON.displayName,
95+
requiredDotnetRuntimeExtensionVersion,
96+
dotnetRuntimeExtensionId
97+
);
98+
await vscode.window.showErrorMessage(prompt, button).then(async (selection) => {
99+
if (selection === button) {
100+
await vscode.commands.executeCommand('workbench.extensions.installExtension', dotnetRuntimeExtensionId);
101+
await vscode.commands.executeCommand('workbench.action.reloadWindow');
102+
} else {
103+
throw new Error(
104+
vscode.l10n.t(
105+
'Version {0} of the .NET Install Tool ({2}) was not found, will not activate.',
106+
requiredDotnetRuntimeExtensionVersion,
107+
dotnetRuntimeExtensionId
108+
)
109+
);
110+
}
111+
});
112+
}
113+
84114
// If the dotnet bundle is installed, this will ensure the dotnet CLI is on the path.
85115
await initializeDotnetPath();
86116

src/razor/src/extension.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ import TelemetryReporter from '@vscode/extension-telemetry';
4747
import { CSharpDevKitExports } from '../../csharpDevKitExports';
4848
import { DotnetRuntimeExtensionResolver } from '../../lsptoolshost/dotnetRuntimeExtensionResolver';
4949
import { PlatformInformation } from '../../shared/platform';
50-
import { RazorLanguageServerOptions } from './razorLanguageServerOptions';
51-
import { resolveRazorLanguageServerOptions } from './razorLanguageServerOptionsResolver';
5250
import { RazorFormatNewFileHandler } from './formatNewFile/razorFormatNewFileHandler';
5351
import { InlayHintHandler } from './inlayHint/inlayHintHandler';
5452
import { InlayHintResolveHandler } from './inlayHint/inlayHintResolveHandler';
@@ -75,16 +73,8 @@ export async function activate(
7573
const logger = new RazorLogger(eventEmitterFactory, languageServerLogLevel);
7674

7775
try {
78-
const razorOptions: RazorLanguageServerOptions = resolveRazorLanguageServerOptions(
79-
vscodeType,
80-
languageServerDir,
81-
languageServerLogLevel,
82-
logger
83-
);
84-
8576
const hostExecutableResolver = new DotnetRuntimeExtensionResolver(
8677
platformInfo,
87-
() => razorOptions.serverPath,
8878
logger.outputChannel,
8979
context.extensionPath
9080
);

0 commit comments

Comments
 (0)