55
66import * as path from 'path' ;
77import * as vscode from 'vscode' ;
8+ import * as semver from 'semver' ;
89import { HostExecutableInformation } from '../shared/constants/hostExecutableInformation' ;
910import { IHostExecutableResolver } from '../shared/constants/IHostExecutableResolver' ;
1011import { PlatformInformation } from '../shared/platform' ;
1112import { Options } from '../shared/options' ;
1213import { existsSync } from 'fs' ;
1314import { 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
1520export 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 */
2429export 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 = / T a r g e t P l a t f o r m = " ( .* ) " / . 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}
0 commit comments