Skip to content

Commit

Permalink
Use the .NET runtime extension to find an appropriate .NET install (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
dibarbet authored Nov 21, 2024
2 parents b01a744 + ff944f8 commit eb8041d
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 123 deletions.
3 changes: 3 additions & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"Update and reload": "Update and reload",
"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",
"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.",
".NET Test Log": ".NET Test Log",
".NET NuGet Restore": ".NET NuGet Restore",
"How to setup Remote Debugging": "How to setup Remote Debugging",
Expand Down
50 changes: 50 additions & 0 deletions src/lsptoolshost/dotnetRuntimeExtensionApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// Contains APIs defined by the vscode-dotnet-runtime extension

export interface IDotnetAcquireResult {
dotnetPath: string;
}

export interface IDotnetFindPathContext {
acquireContext: IDotnetAcquireContext;
versionSpecRequirement: DotnetVersionSpecRequirement;
}

/**
* https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetAcquireContext.ts
*/
interface IDotnetAcquireContext {
version: string;
requestingExtensionId?: string;
errorConfiguration?: AcquireErrorConfiguration;
installType?: DotnetInstallType;
architecture?: string | null | undefined;
mode?: DotnetInstallMode;
}

/**
* https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetAcquireContext.ts#L53C8-L53C52
*/
type DotnetInstallType = 'local' | 'global';

/**
* https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/Utils/ErrorHandler.ts#L22
*/
enum AcquireErrorConfiguration {
DisplayAllErrorPopups = 0,
DisableErrorPopups = 1,
}

/**
* https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/Acquisition/DotnetInstallMode.ts
*/
type DotnetInstallMode = 'sdk' | 'runtime' | 'aspnetcore';

/**
* https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/DotnetVersionSpecRequirement.ts
*/
type DotnetVersionSpecRequirement = 'equal' | 'greater_than_or_equal' | 'less_than_or_equal';
171 changes: 48 additions & 123 deletions src/lsptoolshost/dotnetRuntimeExtensionResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,19 @@

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 { commonOptions, languageServerOptions } from '../shared/options';
import { existsSync } from 'fs';
import { CSharpExtensionId } from '../constants/csharpExtensionId';
import { getDotnetInfo } from '../shared/utils/getDotnetInfo';
import { readFile } from 'fs/promises';
import { RuntimeInfo } from '../shared/utils/dotnetInfo';
import { IDotnetAcquireResult, IDotnetFindPathContext } from './dotnetRuntimeExtensionApi';

export const DotNetRuntimeVersion = '8.0.10';

interface IDotnetAcquireResult {
dotnetPath: string;
}
const DotNetMajorVersion = '8';
const DotNetMinorVersion = '0';
const DotNetPatchVersion = '10';
export const DotNetRuntimeVersion = `${DotNetMajorVersion}.${DotNetMinorVersion}.${DotNetPatchVersion}`;

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

async getHostExecutableInfo(): Promise<HostExecutableInformation> {
let dotnetRuntimePath = commonOptions.dotnetPath;
const serverPath = this.getServerPath(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: this.getEnvironmentVariables(dotnetPath),
};
let dotnetExecutablePath: string;
if (commonOptions.dotnetPath) {
const dotnetExecutableName = this.getDotnetExecutableName();
dotnetExecutablePath = path.join(commonOptions.dotnetPath, dotnetExecutableName);
} else {
if (this.hostInfo) {
return this.hostInfo;
}
}

// 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);
}
this.channel.appendLine(`Locating .NET runtime version ${DotNetRuntimeVersion}`);
const extensionArchitecture = (await this.getArchitectureFromTargetPlatform()) ?? process.arch;
const findPathRequest: IDotnetFindPathContext = {
acquireContext: {
version: DotNetRuntimeVersion,
requestingExtensionId: CSharpExtensionId,
architecture: extensionArchitecture,
mode: 'runtime',
},
versionSpecRequirement: 'greater_than_or_equal',
};
let acquireResult = await vscode.commands.executeCommand<IDotnetAcquireResult | undefined>(
'dotnet.findPath',
findPathRequest
);
if (acquireResult === undefined) {
this.channel.appendLine(
`Did not find .NET ${DotNetRuntimeVersion} on path, falling back to acquire runtime via ms-dotnettools.vscode-dotnet-runtime`
);
acquireResult = await this.acquireDotNetProcessDependencies();
}

const dotnetExecutableName = this.getDotnetExecutableName();
const dotnetExecutablePath = path.join(dotnetRuntimePath, dotnetExecutableName);
if (!existsSync(dotnetExecutablePath)) {
throw new Error(`Cannot find dotnet path '${dotnetExecutablePath}'`);
dotnetExecutablePath = acquireResult.dotnetPath;
}

return {
const hostInfo = {
version: '' /* We don't need to know the version - we've already downloaded the correct one */,
path: dotnetExecutablePath,
env: this.getEnvironmentVariables(dotnetExecutablePath),
};
this.hostInfo = hostInfo;
return hostInfo;
}

private getEnvironmentVariables(dotnetExecutablePath: string): NodeJS.ProcessEnv {
Expand Down Expand Up @@ -100,14 +106,10 @@ export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {
* Acquires the .NET runtime if it is not already present.
* @returns The path to the .NET runtime
*/
private async acquireRuntime(): Promise<HostExecutableInformation> {
if (this.hostInfo) {
return this.hostInfo;
}

// We have to use '8.0' here because the runtme extension doesn't support acquiring patch versions.
// The acquisition will always acquire the latest however, so it will be at least 8.0.10.
const dotnetAcquireVersion = '8.0';
private async acquireRuntime(): Promise<IDotnetAcquireResult> {
// The runtime extension doesn't support specifying a patch versions in the acquire API, so we only use major.minor here.
// That is generally OK, as acquisition will always acquire the latest patch version.
const dotnetAcquireVersion = `${DotNetMajorVersion}.${DotNetMinorVersion}`;
let status = await vscode.commands.executeCommand<IDotnetAcquireResult>('dotnet.acquireStatus', {
version: dotnetAcquireVersion,
requestingExtensionId: CSharpExtensionId,
Expand All @@ -119,106 +121,29 @@ export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {
version: dotnetAcquireVersion,
requestingExtensionId: CSharpExtensionId,
});
if (!status?.dotnetPath) {
if (!status) {
throw new Error('Could not resolve the dotnet path!');
}
}

return (this.hostInfo = {
version: DotNetRuntimeVersion,
path: status.dotnetPath,
env: process.env,
});
return status;
}

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

const args = [path];
const args = [this.getServerPath(this.platformInfo)];
// This will install any missing Linux dependencies.
await vscode.commands.executeCommand('dotnet.ensureDotnetDependencies', {
command: dotnetInfo.path,
command: acquireResult.dotnetPath,
arguments: args,
});

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 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}).`
);
}

// Verify that the dotnet we found includes a runtime version that is compatible with our requirement.
const requiredRuntimeVersion = semver.parse(`${DotNetRuntimeVersion}`);
if (!requiredRuntimeVersion) {
throw new Error(`Unable to parse minimum required version ${DotNetRuntimeVersion}`);
}

const coreRuntimeVersions = dotnetInfo.Runtimes['Microsoft.NETCore.App'];
let matchingRuntime: RuntimeInfo | undefined = undefined;
for (const runtime of coreRuntimeVersions) {
// We consider a match if the runtime is greater than or equal to the required version since we roll forward.
if (semver.gte(runtime.Version, requiredRuntimeVersion)) {
matchingRuntime = runtime;
break;
}
}

if (!matchingRuntime) {
throw new Error(
`No compatible .NET runtime found. Minimum required version is ${DotNetRuntimeVersion}.`
);
}

// The .NET install layout is a well known structure on all platforms.
// See https://github.com/dotnet/designs/blob/main/accepted/2020/install-locations.md#net-core-install-layout
//
// Therefore we know that the runtime path is always in <install root>/shared/<runtime name>
// and the dotnet executable is always at <install root>/dotnet(.exe).
//
// Since dotnet --list-runtimes will always use the real assembly path to output the runtime folder (no symlinks!)
// we know the dotnet executable will be two folders up in the install root.
const runtimeFolderPath = matchingRuntime.Path;
const installFolder = path.dirname(path.dirname(runtimeFolderPath));
const dotnetExecutablePath = path.join(installFolder, this.getDotnetExecutableName());
if (!existsSync(dotnetExecutablePath)) {
throw new Error(
`dotnet executable path does not exist: ${dotnetExecutablePath}, dotnet installation may be corrupt.`
);
}

this.channel.appendLine(`Using dotnet configured on PATH`);
return dotnetExecutablePath;
} 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;
return acquireResult;
}

private async getArchitectureFromTargetPlatform(): Promise<string | undefined> {
Expand Down
30 changes: 30 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { debugSessionTracker } from './coreclrDebug/provisionalDebugSessionTrack
import { getComponentFolder } from './lsptoolshost/builtInComponents';
import { activateOmniSharpLanguageServer, ActivationResult } from './omnisharp/omnisharpLanguageServer';
import { ActionOption, showErrorMessage } from './shared/observers/utils/showMessage';
import { lt } from 'semver';
import { TelemetryEventNames } from './shared/telemetryEventNames';

export async function activate(
Expand Down Expand Up @@ -82,6 +83,35 @@ export async function activate(
requiredPackageIds.push('OmniSharp');
}

const dotnetRuntimeExtensionId = 'ms-dotnettools.vscode-dotnet-runtime';
const requiredDotnetRuntimeExtensionVersion = '2.2.3';

const dotnetRuntimeExtension = vscode.extensions.getExtension(dotnetRuntimeExtensionId);
const dotnetRuntimeExtensionVersion = dotnetRuntimeExtension?.packageJSON.version;
if (lt(dotnetRuntimeExtensionVersion, requiredDotnetRuntimeExtensionVersion)) {
const button = vscode.l10n.t('Update and reload');
const prompt = vscode.l10n.t(
'The {0} extension requires at least {1} of the .NET Install Tool ({2}) extension. Please update to continue',
context.extension.packageJSON.displayName,
requiredDotnetRuntimeExtensionVersion,
dotnetRuntimeExtensionId
);
const selection = await vscode.window.showErrorMessage(prompt, button);
if (selection === button) {
await vscode.commands.executeCommand('workbench.extensions.installExtension', dotnetRuntimeExtensionId);
await vscode.commands.executeCommand('workbench.action.reloadWindow');
} else {
throw new Error(
vscode.l10n.t(
'Version {0} of the .NET Install Tool ({1}) was not found, {2} will not activate.',
requiredDotnetRuntimeExtensionVersion,
dotnetRuntimeExtensionId,
context.extension.packageJSON.displayName
)
);
}
}

// If the dotnet bundle is installed, this will ensure the dotnet CLI is on the path.
await initializeDotnetPath();

Expand Down

0 comments on commit eb8041d

Please sign in to comment.