Skip to content

Commit 6624e2e

Browse files
authored
Merge pull request #6074 from dibarbet/dotnet_from_path
Try to find a valid dotnet version from the path before falling back to runtime extension
2 parents 066d2fc + 601f759 commit 6624e2e

File tree

4 files changed

+133
-2
lines changed

4 files changed

+133
-2
lines changed

src/lsptoolshost/dotnetRuntimeExtensionResolver.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55

66
import * as path from 'path';
77
import * as vscode from 'vscode';
8+
import * as semver from 'semver';
89
import { HostExecutableInformation } from '../shared/constants/hostExecutableInformation';
910
import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver';
1011
import { PlatformInformation } from '../shared/platform';
1112
import { Options } from '../shared/options';
1213
import { existsSync } from 'fs';
1314
import { CSharpExtensionId } from '../constants/csharpExtensionId';
15+
import { promisify } from 'util';
16+
import { exec } from 'child_process';
17+
import { getDotnetInfo } from '../shared/utils/getDotnetInfo';
18+
import { readFile } from 'fs/promises';
1419

1520
export const DotNetRuntimeVersion = '7.0';
1621

@@ -22,19 +27,36 @@ interface IDotnetAcquireResult {
2227
* Resolves the dotnet runtime for a server executable from given options and the dotnet runtime VSCode extension.
2328
*/
2429
export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {
30+
private readonly minimumDotnetVersion = '7.0.100';
2531
constructor(
2632
private platformInfo: PlatformInformation,
2733
/**
2834
* This is a function instead of a string because the server path can change while the extension is active (when the option changes).
2935
*/
30-
private getServerPath: (options: Options, platform: PlatformInformation) => string
36+
private getServerPath: (options: Options, platform: PlatformInformation) => string,
37+
private channel: vscode.OutputChannel,
38+
private extensionPath: string
3139
) {}
3240

3341
private hostInfo: HostExecutableInformation | undefined;
3442

3543
async getHostExecutableInfo(options: Options): Promise<HostExecutableInformation> {
3644
let dotnetRuntimePath = options.commonOptions.dotnetPath;
3745
const serverPath = this.getServerPath(options, this.platformInfo);
46+
47+
// Check if we can find a valid dotnet from dotnet --version on the PATH.
48+
if (!dotnetRuntimePath) {
49+
const dotnetPath = await this.findDotnetFromPath();
50+
if (dotnetPath) {
51+
return {
52+
version: '' /* We don't need to know the version - we've already verified its high enough */,
53+
path: dotnetPath,
54+
env: process.env,
55+
};
56+
}
57+
}
58+
59+
// We didn't find it on the path, see if we can install the correct runtime using the runtime extension.
3860
if (!dotnetRuntimePath) {
3961
const dotnetInfo = await this.acquireDotNetProcessDependencies(serverPath);
4062
dotnetRuntimePath = path.dirname(dotnetInfo.path);
@@ -101,4 +123,103 @@ export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {
101123

102124
return dotnetInfo;
103125
}
126+
127+
/**
128+
* Checks dotnet --version to see if the value on the path is greater than the minimum required version.
129+
* This is adapated from similar O# server logic and should be removed when we have a stable acquisition extension.
130+
* @returns true if the dotnet version is greater than the minimum required version, false otherwise.
131+
*/
132+
private async findDotnetFromPath(): Promise<string | undefined> {
133+
try {
134+
const dotnetInfo = await getDotnetInfo([]);
135+
const dotnetVersionStr = dotnetInfo.Version;
136+
137+
const extensionArchitecture = await this.getArchitectureFromTargetPlatform();
138+
const dotnetArchitecture = dotnetInfo.Architecture;
139+
140+
// If the extension arhcitecture is defined, we check that it matches the dotnet architecture.
141+
// If its undefined we likely have a platform neutral server and assume it can run on any architecture.
142+
if (extensionArchitecture && extensionArchitecture !== dotnetArchitecture) {
143+
throw new Error(
144+
`The architecture of the .NET runtime (${dotnetArchitecture}) does not match the architecture of the extension (${extensionArchitecture}).`
145+
);
146+
}
147+
148+
const dotnetVersion = semver.parse(dotnetVersionStr);
149+
if (!dotnetVersion) {
150+
throw new Error(`Unknown result output from 'dotnet --version'. Received ${dotnetVersionStr}`);
151+
}
152+
153+
if (semver.lt(dotnetVersion, this.minimumDotnetVersion)) {
154+
throw new Error(
155+
`Found dotnet version ${dotnetVersion}. Minimum required version is ${this.minimumDotnetVersion}.`
156+
);
157+
}
158+
159+
// Find the location of the dotnet on path.
160+
const command = this.platformInfo.isWindows() ? 'where' : 'which';
161+
const whereOutput = await promisify(exec)(`${command} dotnet`);
162+
if (!whereOutput.stdout) {
163+
throw new Error(`Unable to find dotnet from ${command}.`);
164+
}
165+
166+
const path = whereOutput.stdout.trim();
167+
if (!existsSync(path)) {
168+
throw new Error(`dotnet path does not exist: ${path}`);
169+
}
170+
171+
this.channel.appendLine(`Using dotnet configured on PATH`);
172+
return path;
173+
} catch (e) {
174+
this.channel.appendLine(
175+
'Failed to find dotnet info from path, falling back to acquire runtime via ms-dotnettools.vscode-dotnet-runtime'
176+
);
177+
if (e instanceof Error) {
178+
this.channel.appendLine(e.message);
179+
}
180+
}
181+
182+
return undefined;
183+
}
184+
185+
private async getArchitectureFromTargetPlatform(): Promise<string | undefined> {
186+
const vsixManifestFile = path.join(this.extensionPath, '.vsixmanifest');
187+
if (!existsSync(vsixManifestFile)) {
188+
// This is not an error as normal development F5 builds do not generate a .vsixmanifest file.
189+
this.channel.appendLine(
190+
`Unable to find extension target platform - no vsix manifest file exists at ${vsixManifestFile}`
191+
);
192+
return undefined;
193+
}
194+
195+
const contents = await readFile(vsixManifestFile, 'utf-8');
196+
const targetPlatformMatch = /TargetPlatform="(.*)"/.exec(contents);
197+
if (!targetPlatformMatch) {
198+
throw new Error(`Could not find extension target platform in ${vsixManifestFile}`);
199+
}
200+
201+
const targetPlatform = targetPlatformMatch[1];
202+
203+
// The currently known extension platforms are taken from here:
204+
// https://code.visualstudio.com/api/working-with-extensions/publishing-extension#platformspecific-extensions
205+
switch (targetPlatform) {
206+
case 'win32-x64':
207+
case 'linux-x64':
208+
case 'alpine-x64':
209+
case 'darwin-x64':
210+
return 'x64';
211+
case 'win32-ia32':
212+
return 'x86';
213+
case 'win32-arm64':
214+
case 'linux-arm64':
215+
case 'alpine-arm64':
216+
case 'darwin-arm64':
217+
return 'arm64';
218+
case 'linux-armhf':
219+
case 'web':
220+
return undefined;
221+
default:
222+
throw new Error(`Unknown extension target platform: ${targetPlatform}`);
223+
}
224+
}
104225
}

src/lsptoolshost/roslynLanguageServer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,12 @@ export async function activateRoslynLanguageServer(
733733
// Create a separate channel for outputting trace logs - these are incredibly verbose and make other logs very difficult to see.
734734
_traceChannel = vscode.window.createOutputChannel('C# LSP Trace Logs');
735735

736-
const hostExecutableResolver = new DotnetRuntimeExtensionResolver(platformInfo, getServerPath);
736+
const hostExecutableResolver = new DotnetRuntimeExtensionResolver(
737+
platformInfo,
738+
getServerPath,
739+
outputChannel,
740+
context.extensionPath
741+
);
737742
const additionalExtensionPaths = scanExtensionPlugins();
738743
_languageServer = new RoslynLanguageServer(
739744
platformInfo,

src/shared/utils/dotnetInfo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export interface DotnetInfo {
99
Version: string;
1010
/* a runtime-only install of dotnet will not output a runtimeId in dotnet --info. */
1111
RuntimeId?: string;
12+
Architecture?: string;
1213
}

src/shared/utils/getDotnetInfo.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export async function getDotnetInfo(dotNetCliPaths: string[]): Promise<DotnetInf
2626

2727
let version: string | undefined;
2828
let runtimeId: string | undefined;
29+
let architecture: string | undefined;
2930

3031
const lines = data.replace(/\r/gm, '').split('\n');
3132
for (const line of lines) {
@@ -34,6 +35,8 @@ export async function getDotnetInfo(dotNetCliPaths: string[]): Promise<DotnetInf
3435
version = match[1];
3536
} else if ((match = /^ RID:\s*([\w\-.]+)$/.exec(line))) {
3637
runtimeId = match[1];
38+
} else if ((match = /^\s*Architecture:\s*(.*)/.exec(line))) {
39+
architecture = match[1];
3740
}
3841
}
3942

@@ -43,6 +46,7 @@ export async function getDotnetInfo(dotNetCliPaths: string[]): Promise<DotnetInf
4346
FullInfo: fullInfo,
4447
Version: version,
4548
RuntimeId: runtimeId,
49+
Architecture: architecture,
4650
};
4751
return _dotnetInfo;
4852
}

0 commit comments

Comments
 (0)