Skip to content

Commit

Permalink
Merge pull request #6074 from dibarbet/dotnet_from_path
Browse files Browse the repository at this point in the history
Try to find a valid dotnet version from the path before falling back to runtime extension
  • Loading branch information
dibarbet authored Aug 10, 2023
2 parents 066d2fc + 601f759 commit 6624e2e
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 2 deletions.
123 changes: 122 additions & 1 deletion src/lsptoolshost/dotnetRuntimeExtensionResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@

import * as path from 'path';
import * as vscode from 'vscode';
import * as semver from 'semver';
import { HostExecutableInformation } from '../shared/constants/hostExecutableInformation';
import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver';
import { PlatformInformation } from '../shared/platform';
import { Options } from '../shared/options';
import { existsSync } from 'fs';
import { CSharpExtensionId } from '../constants/csharpExtensionId';
import { promisify } from 'util';
import { exec } from 'child_process';
import { getDotnetInfo } from '../shared/utils/getDotnetInfo';
import { readFile } from 'fs/promises';

export const DotNetRuntimeVersion = '7.0';

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

private hostInfo: HostExecutableInformation | undefined;

async getHostExecutableInfo(options: Options): Promise<HostExecutableInformation> {
let dotnetRuntimePath = options.commonOptions.dotnetPath;
const serverPath = this.getServerPath(options, this.platformInfo);

// Check if we can find a valid dotnet from dotnet --version on the PATH.
if (!dotnetRuntimePath) {
const dotnetPath = await this.findDotnetFromPath();
if (dotnetPath) {
return {
version: '' /* We don't need to know the version - we've already verified its high enough */,
path: dotnetPath,
env: process.env,
};
}
}

// We didn't find it on the path, see if we can install the correct runtime using the runtime extension.
if (!dotnetRuntimePath) {
const dotnetInfo = await this.acquireDotNetProcessDependencies(serverPath);
dotnetRuntimePath = path.dirname(dotnetInfo.path);
Expand Down Expand Up @@ -101,4 +123,103 @@ export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {

return dotnetInfo;
}

/**
* Checks dotnet --version to see if the value on the path is greater than the minimum required version.
* This is adapated from similar O# server logic and should be removed when we have a stable acquisition extension.
* @returns true if the dotnet version is greater than the minimum required version, false otherwise.
*/
private async findDotnetFromPath(): Promise<string | undefined> {
try {
const dotnetInfo = await getDotnetInfo([]);
const dotnetVersionStr = dotnetInfo.Version;

const extensionArchitecture = await this.getArchitectureFromTargetPlatform();
const dotnetArchitecture = dotnetInfo.Architecture;

// If the extension arhcitecture is defined, we check that it matches the dotnet architecture.
// If its undefined we likely have a platform neutral server and assume it can run on any architecture.
if (extensionArchitecture && extensionArchitecture !== dotnetArchitecture) {
throw new Error(
`The architecture of the .NET runtime (${dotnetArchitecture}) does not match the architecture of the extension (${extensionArchitecture}).`
);
}

const dotnetVersion = semver.parse(dotnetVersionStr);
if (!dotnetVersion) {
throw new Error(`Unknown result output from 'dotnet --version'. Received ${dotnetVersionStr}`);
}

if (semver.lt(dotnetVersion, this.minimumDotnetVersion)) {
throw new Error(
`Found dotnet version ${dotnetVersion}. Minimum required version is ${this.minimumDotnetVersion}.`
);
}

// Find the location of the dotnet on path.
const command = this.platformInfo.isWindows() ? 'where' : 'which';
const whereOutput = await promisify(exec)(`${command} dotnet`);
if (!whereOutput.stdout) {
throw new Error(`Unable to find dotnet from ${command}.`);
}

const path = whereOutput.stdout.trim();
if (!existsSync(path)) {
throw new Error(`dotnet path does not exist: ${path}`);
}

this.channel.appendLine(`Using dotnet configured on PATH`);
return path;
} catch (e) {
this.channel.appendLine(
'Failed to find dotnet info from path, falling back to acquire runtime via ms-dotnettools.vscode-dotnet-runtime'
);
if (e instanceof Error) {
this.channel.appendLine(e.message);
}
}

return undefined;
}

private async getArchitectureFromTargetPlatform(): Promise<string | undefined> {
const vsixManifestFile = path.join(this.extensionPath, '.vsixmanifest');
if (!existsSync(vsixManifestFile)) {
// This is not an error as normal development F5 builds do not generate a .vsixmanifest file.
this.channel.appendLine(
`Unable to find extension target platform - no vsix manifest file exists at ${vsixManifestFile}`
);
return undefined;
}

const contents = await readFile(vsixManifestFile, 'utf-8');
const targetPlatformMatch = /TargetPlatform="(.*)"/.exec(contents);
if (!targetPlatformMatch) {
throw new Error(`Could not find extension target platform in ${vsixManifestFile}`);
}

const targetPlatform = targetPlatformMatch[1];

// The currently known extension platforms are taken from here:
// https://code.visualstudio.com/api/working-with-extensions/publishing-extension#platformspecific-extensions
switch (targetPlatform) {
case 'win32-x64':
case 'linux-x64':
case 'alpine-x64':
case 'darwin-x64':
return 'x64';
case 'win32-ia32':
return 'x86';
case 'win32-arm64':
case 'linux-arm64':
case 'alpine-arm64':
case 'darwin-arm64':
return 'arm64';
case 'linux-armhf':
case 'web':
return undefined;
default:
throw new Error(`Unknown extension target platform: ${targetPlatform}`);
}
}
}
7 changes: 6 additions & 1 deletion src/lsptoolshost/roslynLanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,12 @@ export async function activateRoslynLanguageServer(
// Create a separate channel for outputting trace logs - these are incredibly verbose and make other logs very difficult to see.
_traceChannel = vscode.window.createOutputChannel('C# LSP Trace Logs');

const hostExecutableResolver = new DotnetRuntimeExtensionResolver(platformInfo, getServerPath);
const hostExecutableResolver = new DotnetRuntimeExtensionResolver(
platformInfo,
getServerPath,
outputChannel,
context.extensionPath
);
const additionalExtensionPaths = scanExtensionPlugins();
_languageServer = new RoslynLanguageServer(
platformInfo,
Expand Down
1 change: 1 addition & 0 deletions src/shared/utils/dotnetInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export interface DotnetInfo {
Version: string;
/* a runtime-only install of dotnet will not output a runtimeId in dotnet --info. */
RuntimeId?: string;
Architecture?: string;
}
4 changes: 4 additions & 0 deletions src/shared/utils/getDotnetInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export async function getDotnetInfo(dotNetCliPaths: string[]): Promise<DotnetInf

let version: string | undefined;
let runtimeId: string | undefined;
let architecture: string | undefined;

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

Expand All @@ -43,6 +46,7 @@ export async function getDotnetInfo(dotNetCliPaths: string[]): Promise<DotnetInf
FullInfo: fullInfo,
Version: version,
RuntimeId: runtimeId,
Architecture: architecture,
};
return _dotnetInfo;
}
Expand Down

0 comments on commit 6624e2e

Please sign in to comment.