Skip to content

Commit eb8041d

Browse files
authored
Use the .NET runtime extension to find an appropriate .NET install (#7684)
2 parents b01a744 + ff944f8 commit eb8041d

File tree

4 files changed

+131
-123
lines changed

4 files changed

+131
-123
lines changed

l10n/bundle.l10n.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
2+
"Update and reload": "Update and reload",
3+
"The {0} extension requires at least {1} of the .NET Install Tool ({2}) extension. Please update to continue": "The {0} extension requires at least {1} of the .NET Install Tool ({2}) extension. Please update to continue",
4+
"Version {0} of the .NET Install Tool ({1}) was not found, {2} will not activate.": "Version {0} of the .NET Install Tool ({1}) was not found, {2} will not activate.",
25
".NET Test Log": ".NET Test Log",
36
".NET NuGet Restore": ".NET NuGet Restore",
47
"How to setup Remote Debugging": "How to setup Remote Debugging",
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: 48 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,19 @@
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 { IDotnetAcquireResult, IDotnetFindPathContext } from './dotnetRuntimeExtensionApi';
1816

19-
export const DotNetRuntimeVersion = '8.0.10';
20-
21-
interface IDotnetAcquireResult {
22-
dotnetPath: string;
23-
}
17+
const DotNetMajorVersion = '8';
18+
const DotNetMinorVersion = '0';
19+
const DotNetPatchVersion = '10';
20+
export const DotNetRuntimeVersion = `${DotNetMajorVersion}.${DotNetMinorVersion}.${DotNetPatchVersion}`;
2421

2522
/**
2623
* Resolves the dotnet runtime for a server executable from given options and the dotnet runtime VSCode extension.
@@ -39,38 +36,47 @@ export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {
3936
private hostInfo: HostExecutableInformation | undefined;
4037

4138
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-
};
39+
let dotnetExecutablePath: string;
40+
if (commonOptions.dotnetPath) {
41+
const dotnetExecutableName = this.getDotnetExecutableName();
42+
dotnetExecutablePath = path.join(commonOptions.dotnetPath, dotnetExecutableName);
43+
} else {
44+
if (this.hostInfo) {
45+
return this.hostInfo;
5446
}
55-
}
5647

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-
}
48+
this.channel.appendLine(`Locating .NET runtime version ${DotNetRuntimeVersion}`);
49+
const extensionArchitecture = (await this.getArchitectureFromTargetPlatform()) ?? process.arch;
50+
const findPathRequest: IDotnetFindPathContext = {
51+
acquireContext: {
52+
version: DotNetRuntimeVersion,
53+
requestingExtensionId: CSharpExtensionId,
54+
architecture: extensionArchitecture,
55+
mode: 'runtime',
56+
},
57+
versionSpecRequirement: 'greater_than_or_equal',
58+
};
59+
let acquireResult = await vscode.commands.executeCommand<IDotnetAcquireResult | undefined>(
60+
'dotnet.findPath',
61+
findPathRequest
62+
);
63+
if (acquireResult === undefined) {
64+
this.channel.appendLine(
65+
`Did not find .NET ${DotNetRuntimeVersion} on path, falling back to acquire runtime via ms-dotnettools.vscode-dotnet-runtime`
66+
);
67+
acquireResult = await this.acquireDotNetProcessDependencies();
68+
}
6269

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}'`);
70+
dotnetExecutablePath = acquireResult.dotnetPath;
6771
}
6872

69-
return {
73+
const hostInfo = {
7074
version: '' /* We don't need to know the version - we've already downloaded the correct one */,
7175
path: dotnetExecutablePath,
7276
env: this.getEnvironmentVariables(dotnetExecutablePath),
7377
};
78+
this.hostInfo = hostInfo;
79+
return hostInfo;
7480
}
7581

7682
private getEnvironmentVariables(dotnetExecutablePath: string): NodeJS.ProcessEnv {
@@ -100,14 +106,10 @@ export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {
100106
* Acquires the .NET runtime if it is not already present.
101107
* @returns The path to the .NET runtime
102108
*/
103-
private async acquireRuntime(): Promise<HostExecutableInformation> {
104-
if (this.hostInfo) {
105-
return this.hostInfo;
106-
}
107-
108-
// We have to use '8.0' here because the runtme extension doesn't support acquiring patch versions.
109-
// The acquisition will always acquire the latest however, so it will be at least 8.0.10.
110-
const dotnetAcquireVersion = '8.0';
109+
private async acquireRuntime(): Promise<IDotnetAcquireResult> {
110+
// The runtime extension doesn't support specifying a patch versions in the acquire API, so we only use major.minor here.
111+
// That is generally OK, as acquisition will always acquire the latest patch version.
112+
const dotnetAcquireVersion = `${DotNetMajorVersion}.${DotNetMinorVersion}`;
111113
let status = await vscode.commands.executeCommand<IDotnetAcquireResult>('dotnet.acquireStatus', {
112114
version: dotnetAcquireVersion,
113115
requestingExtensionId: CSharpExtensionId,
@@ -119,106 +121,29 @@ export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {
119121
version: dotnetAcquireVersion,
120122
requestingExtensionId: CSharpExtensionId,
121123
});
122-
if (!status?.dotnetPath) {
124+
if (!status) {
123125
throw new Error('Could not resolve the dotnet path!');
124126
}
125127
}
126128

127-
return (this.hostInfo = {
128-
version: DotNetRuntimeVersion,
129-
path: status.dotnetPath,
130-
env: process.env,
131-
});
129+
return status;
132130
}
133131

134132
/**
135133
* Acquires the .NET runtime and any other dependencies required to spawn a particular .NET executable.
136134
* @param path The path to the entrypoint assembly. Typically a .dll.
137135
*/
138-
private async acquireDotNetProcessDependencies(path: string): Promise<HostExecutableInformation> {
139-
const dotnetInfo = await this.acquireRuntime();
136+
private async acquireDotNetProcessDependencies(): Promise<IDotnetAcquireResult> {
137+
const acquireResult = await this.acquireRuntime();
140138

141-
const args = [path];
139+
const args = [this.getServerPath(this.platformInfo)];
142140
// This will install any missing Linux dependencies.
143141
await vscode.commands.executeCommand('dotnet.ensureDotnetDependencies', {
144-
command: dotnetInfo.path,
142+
command: acquireResult.dotnetPath,
145143
arguments: args,
146144
});
147145

148-
return dotnetInfo;
149-
}
150-
151-
/**
152-
* Checks dotnet --version to see if the value on the path is greater than the minimum required version.
153-
* This is adapated from similar O# server logic and should be removed when we have a stable acquisition extension.
154-
* @returns true if the dotnet version is greater than the minimum required version, false otherwise.
155-
*/
156-
private async findDotnetFromPath(): Promise<string | undefined> {
157-
try {
158-
const dotnetInfo = await getDotnetInfo([]);
159-
160-
const extensionArchitecture = await this.getArchitectureFromTargetPlatform();
161-
const dotnetArchitecture = dotnetInfo.Architecture;
162-
163-
// If the extension arhcitecture is defined, we check that it matches the dotnet architecture.
164-
// If its undefined we likely have a platform neutral server and assume it can run on any architecture.
165-
if (extensionArchitecture && extensionArchitecture !== dotnetArchitecture) {
166-
throw new Error(
167-
`The architecture of the .NET runtime (${dotnetArchitecture}) does not match the architecture of the extension (${extensionArchitecture}).`
168-
);
169-
}
170-
171-
// Verify that the dotnet we found includes a runtime version that is compatible with our requirement.
172-
const requiredRuntimeVersion = semver.parse(`${DotNetRuntimeVersion}`);
173-
if (!requiredRuntimeVersion) {
174-
throw new Error(`Unable to parse minimum required version ${DotNetRuntimeVersion}`);
175-
}
176-
177-
const coreRuntimeVersions = dotnetInfo.Runtimes['Microsoft.NETCore.App'];
178-
let matchingRuntime: RuntimeInfo | undefined = undefined;
179-
for (const runtime of coreRuntimeVersions) {
180-
// We consider a match if the runtime is greater than or equal to the required version since we roll forward.
181-
if (semver.gte(runtime.Version, requiredRuntimeVersion)) {
182-
matchingRuntime = runtime;
183-
break;
184-
}
185-
}
186-
187-
if (!matchingRuntime) {
188-
throw new Error(
189-
`No compatible .NET runtime found. Minimum required version is ${DotNetRuntimeVersion}.`
190-
);
191-
}
192-
193-
// The .NET install layout is a well known structure on all platforms.
194-
// See https://github.com/dotnet/designs/blob/main/accepted/2020/install-locations.md#net-core-install-layout
195-
//
196-
// Therefore we know that the runtime path is always in <install root>/shared/<runtime name>
197-
// and the dotnet executable is always at <install root>/dotnet(.exe).
198-
//
199-
// Since dotnet --list-runtimes will always use the real assembly path to output the runtime folder (no symlinks!)
200-
// we know the dotnet executable will be two folders up in the install root.
201-
const runtimeFolderPath = matchingRuntime.Path;
202-
const installFolder = path.dirname(path.dirname(runtimeFolderPath));
203-
const dotnetExecutablePath = path.join(installFolder, this.getDotnetExecutableName());
204-
if (!existsSync(dotnetExecutablePath)) {
205-
throw new Error(
206-
`dotnet executable path does not exist: ${dotnetExecutablePath}, dotnet installation may be corrupt.`
207-
);
208-
}
209-
210-
this.channel.appendLine(`Using dotnet configured on PATH`);
211-
return dotnetExecutablePath;
212-
} catch (e) {
213-
this.channel.appendLine(
214-
'Failed to find dotnet info from path, falling back to acquire runtime via ms-dotnettools.vscode-dotnet-runtime'
215-
);
216-
if (e instanceof Error) {
217-
this.channel.appendLine(e.message);
218-
}
219-
}
220-
221-
return undefined;
146+
return acquireResult;
222147
}
223148

224149
private async getArchitectureFromTargetPlatform(): Promise<string | undefined> {

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
import { TelemetryEventNames } from './shared/telemetryEventNames';
4546

4647
export async function activate(
@@ -82,6 +83,35 @@ export async function activate(
8283
requiredPackageIds.push('OmniSharp');
8384
}
8485

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

0 commit comments

Comments
 (0)