Skip to content

Commit 448fdda

Browse files
authored
Merge pull request #6515 from dibarbet/fix_dotnet_path_snap
Fix dotnet path resolution when using snap installed packages
2 parents e91e3f0 + 6cf3735 commit 448fdda

File tree

3 files changed

+46
-29
lines changed

3 files changed

+46
-29
lines changed

src/lsptoolshost/dotnetRuntimeExtensionResolver.ts

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@ import { PlatformInformation } from '../shared/platform';
1212
import { commonOptions } from '../shared/options';
1313
import { existsSync } from 'fs';
1414
import { CSharpExtensionId } from '../constants/csharpExtensionId';
15-
import { promisify } from 'util';
16-
import { exec } from 'child_process';
1715
import { getDotnetInfo } from '../shared/utils/getDotnetInfo';
18-
import { readFile, realpath } from 'fs/promises';
16+
import { readFile } from 'fs/promises';
17+
import { RuntimeInfo } from '../shared/utils/dotnetInfo';
1918

2019
export const DotNetRuntimeVersion = '7.0';
2120

@@ -62,7 +61,7 @@ export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {
6261
dotnetRuntimePath = path.dirname(dotnetInfo.path);
6362
}
6463

65-
const dotnetExecutableName = this.platformInfo.isWindows() ? 'dotnet.exe' : 'dotnet';
64+
const dotnetExecutableName = this.getDotnetExecutableName();
6665
const dotnetExecutablePath = path.join(dotnetRuntimePath, dotnetExecutableName);
6766
if (!existsSync(dotnetExecutablePath)) {
6867
throw new Error(`Cannot find dotnet path '${dotnetExecutablePath}'`);
@@ -151,39 +150,40 @@ export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {
151150
}
152151

153152
const coreRuntimeVersions = dotnetInfo.Runtimes['Microsoft.NETCore.App'];
154-
let foundRuntimeVersion = false;
155-
for (const version of coreRuntimeVersions) {
153+
let matchingRuntime: RuntimeInfo | undefined = undefined;
154+
for (const runtime of coreRuntimeVersions) {
156155
// We consider a match if the runtime is greater than or equal to the required version since we roll forward.
157-
if (semver.gt(version, requiredRuntimeVersion)) {
158-
foundRuntimeVersion = true;
156+
if (semver.gt(runtime.Version, requiredRuntimeVersion)) {
157+
matchingRuntime = runtime;
159158
break;
160159
}
161160
}
162161

163-
if (!foundRuntimeVersion) {
162+
if (!matchingRuntime) {
164163
throw new Error(
165164
`No compatible .NET runtime found. Minimum required version is ${this.minimumDotnetRuntimeVersion}.`
166165
);
167166
}
168167

169-
// Find the location of the dotnet on path.
170-
const command = this.platformInfo.isWindows() ? 'where' : 'which';
171-
const whereOutput = await promisify(exec)(`${command} dotnet`);
172-
if (!whereOutput.stdout) {
173-
throw new Error(`Unable to find dotnet from ${command}.`);
174-
}
175-
176-
// There could be multiple paths output from where. Take the first since that is what we used to run dotnet --info.
177-
const path = whereOutput.stdout.trim().replace(/\r/gm, '').split('\n')[0];
178-
if (!existsSync(path)) {
179-
throw new Error(`dotnet path does not exist: ${path}`);
168+
// The .NET install layout is a well known structure on all platforms.
169+
// See https://github.com/dotnet/designs/blob/main/accepted/2020/install-locations.md#net-core-install-layout
170+
//
171+
// Therefore we know that the runtime path is always in <install root>/shared/<runtime name>
172+
// and the dotnet executable is always at <install root>/dotnet(.exe).
173+
//
174+
// Since dotnet --list-runtimes will always use the real assembly path to output the runtime folder (no symlinks!)
175+
// we know the dotnet executable will be two folders up in the install root.
176+
const runtimeFolderPath = matchingRuntime.Path;
177+
const installFolder = path.dirname(path.dirname(runtimeFolderPath));
178+
const dotnetExecutablePath = path.join(installFolder, this.getDotnetExecutableName());
179+
if (!existsSync(dotnetExecutablePath)) {
180+
throw new Error(
181+
`dotnet executable path does not exist: ${dotnetExecutablePath}, dotnet installation may be corrupt.`
182+
);
180183
}
181184

182185
this.channel.appendLine(`Using dotnet configured on PATH`);
183-
184-
// If dotnet is just a symlink, resolve it to the actual executable so
185-
// callers will be able to get the actual directory containing the exe.
186-
return await realpath(path);
186+
return dotnetExecutablePath;
187187
} catch (e) {
188188
this.channel.appendLine(
189189
'Failed to find dotnet info from path, falling back to acquire runtime via ms-dotnettools.vscode-dotnet-runtime'
@@ -236,4 +236,8 @@ export class DotnetRuntimeExtensionResolver implements IHostExecutableResolver {
236236
throw new Error(`Unknown extension target platform: ${targetPlatform}`);
237237
}
238238
}
239+
240+
private getDotnetExecutableName(): string {
241+
return this.platformInfo.isWindows() ? 'dotnet.exe' : 'dotnet';
242+
}
239243
}

src/shared/utils/dotnetInfo.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as semver from 'semver';
77

8-
type RuntimeVersionMap = { [runtime: string]: semver.SemVer[] };
8+
type RuntimeVersionMap = { [runtime: string]: RuntimeInfo[] };
99
export interface DotnetInfo {
1010
CliPath?: string;
1111
FullInfo: string;
@@ -15,3 +15,8 @@ export interface DotnetInfo {
1515
Architecture?: string;
1616
Runtimes: RuntimeVersionMap;
1717
}
18+
19+
export interface RuntimeInfo {
20+
Version: semver.SemVer;
21+
Path: string;
22+
}

src/shared/utils/getDotnetInfo.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as semver from 'semver';
77
import { join } from 'path';
88
import { execChildProcess } from '../../common';
99
import { CoreClrDebugUtil } from '../../coreclrDebug/util';
10-
import { DotnetInfo } from './dotnetInfo';
10+
import { DotnetInfo, RuntimeInfo } from './dotnetInfo';
1111
import { EOL } from 'os';
1212

1313
// This function calls `dotnet --info` and returns the result as a DotnetInfo object.
@@ -69,7 +69,7 @@ async function parseDotnetInfo(dotnetInfo: string, dotnetExecutablePath: string
6969
}
7070
}
7171

72-
const runtimeVersions: { [runtime: string]: semver.SemVer[] } = {};
72+
const runtimeVersions: { [runtime: string]: RuntimeInfo[] } = {};
7373
const listRuntimes = await execChildProcess('dotnet --list-runtimes', process.cwd(), process.env);
7474
lines = listRuntimes.split(/\r?\n/);
7575
for (const line of lines) {
@@ -78,9 +78,17 @@ async function parseDotnetInfo(dotnetInfo: string, dotnetExecutablePath: string
7878
const runtime = match[1];
7979
const runtimeVersion = match[2];
8080
if (runtime in runtimeVersions) {
81-
runtimeVersions[runtime].push(semver.parse(runtimeVersion)!);
81+
runtimeVersions[runtime].push({
82+
Version: semver.parse(runtimeVersion)!,
83+
Path: match[3],
84+
});
8285
} else {
83-
runtimeVersions[runtime] = [semver.parse(runtimeVersion)!];
86+
runtimeVersions[runtime] = [
87+
{
88+
Version: semver.parse(runtimeVersion)!,
89+
Path: match[3],
90+
},
91+
];
8492
}
8593
}
8694
}

0 commit comments

Comments
 (0)