-
Notifications
You must be signed in to change notification settings - Fork 699
Use the .NET runtime extension to find an appropriate .NET install #7684
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ffe75f5
630078f
21c65ea
9e68455
f36d8bf
97a83fb
e3c7c02
4f21c8e
8081622
132553a
253028d
ff944f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be deprecated in favor of the options on the .net install tool side. However I plan to tackle that in a separate PR as we'll need to separate out the O# usage of this option. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about whether we should keep the existing logic as a backup, but if this has about a mo in pre-release I'm not so concerned. |
||
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 { | ||
|
@@ -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, | ||
|
@@ -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}).` | ||
dibarbet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
); | ||
} | ||
|
||
// 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> { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A breaking change document for dotnetPath should be created, IMO - though for later of course. I would also make sure this is remarked in the release notes, and that our existingPath setting is mentioned.
https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.vscode-dotnet-runtime&ssr=false#overview:~:text=I%20already%20have%20a%20.NET%20Runtime%20or%20SDK%20installed%2C%20and%20I%20want%20to%20use%20it
cc @baronfel @webreidi for awareness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't removed this in this PR - plan to in a followup. What might be an even better option is to migrate existing users of
dotnet.dotnetPath
to the new setting. We already do that for migrating O# options