Skip to content
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

Implement the "dotNetCliPaths" option to support custom .NET SDK locations #4738

Merged
merged 9 commits into from
Aug 23, 2022
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@
"omnisharp.dotnetPath": {
"type": "string",
"scope": "window",
"description": "Specified the path to a dotnet installation to use when \"useModernNet\" is set to true, instead of the default system one. Example: \"/home/username/mycustomdotnetdirectory\"."
"description": "Specified the path to a dotnet installation to use when \"useModernNet\" is set to true, instead of the default system one. This only influences the dotnet installation to use for hosting Omnisharp itself. Example: \"/home/username/mycustomdotnetdirectory\"."
},
"omnisharp.waitForDebugger": {
"type": "boolean",
Expand Down Expand Up @@ -1092,6 +1092,14 @@
"type": "string",
"description": "Path to the .runsettings file which should be used when running unit tests."
},
"omnisharp.dotNetCliPaths": {
"type": "array",
"items": {
"type": "string"
},
"description": "Paths to a local download of the .NET CLI to use for running any user code.",
"uniqueItems": true
},
"razor.plugin.path": {
"type": "string",
"scope": "machine",
Expand Down
4 changes: 2 additions & 2 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ export function safeLength<T>(arr: T[] | undefined) {
return arr ? arr.length : 0;
}

export async function execChildProcess(command: string, workingDirectory: string = getExtensionPath()): Promise<string> {
export async function execChildProcess(command: string, workingDirectory: string = getExtensionPath(), env: NodeJS.ProcessEnv = {}): Promise<string> {
return new Promise<string>((resolve, reject) => {
cp.exec(command, { cwd: workingDirectory, maxBuffer: 500 * 1024 }, (error, stdout, stderr) => {
cp.exec(command, { cwd: workingDirectory, maxBuffer: 500 * 1024, env: env }, (error, stdout, stderr) => {
if (error) {
reject(new Error(`${error}
${stdout}
Expand Down
2 changes: 1 addition & 1 deletion src/constants/IGetDotnetInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
import { DotnetInfo } from "../utils/getDotnetInfo";

export interface IGetDotnetInfo {
(): Promise<DotnetInfo>;
(dotNetCliPaths: string[]): Promise<DotnetInfo>;
}
20 changes: 11 additions & 9 deletions src/coreclr-debug/activate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import CSharpExtensionExports from '../CSharpExtensionExports';
import { getRuntimeDependencyPackageWithId } from '../tools/RuntimeDependencyPackageUtils';
import { getDotnetInfo, DotnetInfo } from '../utils/getDotnetInfo';
import { DotnetDebugConfigurationProvider } from './debugConfigurationProvider';
import { Options } from '../omnisharp/options';

let _debugUtil: CoreClrDebugUtil = null;

export async function activate(thisExtension: vscode.Extension<CSharpExtensionExports>, context: vscode.ExtensionContext, platformInformation: PlatformInformation, eventStream: EventStream) {
export async function activate(thisExtension: vscode.Extension<CSharpExtensionExports>, context: vscode.ExtensionContext, platformInformation: PlatformInformation, eventStream: EventStream, options: Options) {
_debugUtil = new CoreClrDebugUtil(context.extensionPath);

if (!CoreClrDebugUtil.existsSync(_debugUtil.debugAdapterDir())) {
Expand All @@ -29,10 +30,10 @@ export async function activate(thisExtension: vscode.Extension<CSharpExtensionEx
showInstallErrorMessage(eventStream);
}
} else if (!CoreClrDebugUtil.existsSync(_debugUtil.installCompleteFilePath())) {
completeDebuggerInstall(platformInformation, eventStream);
completeDebuggerInstall(platformInformation, eventStream, options);
}

const factory = new DebugAdapterExecutableFactory(platformInformation, eventStream, thisExtension.packageJSON, thisExtension.extensionPath);
const factory = new DebugAdapterExecutableFactory(platformInformation, eventStream, thisExtension.packageJSON, thisExtension.extensionPath, options);
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('coreclr', new DotnetDebugConfigurationProvider(platformInformation)));
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('clr', new DotnetDebugConfigurationProvider(platformInformation)));
context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('coreclr', factory));
Expand Down Expand Up @@ -73,8 +74,8 @@ async function checkIsValidArchitecture(platformInformation: PlatformInformation
return false;
}

async function completeDebuggerInstall(platformInformation: PlatformInformation, eventStream: EventStream): Promise<boolean> {
return _debugUtil.checkDotNetCli()
async function completeDebuggerInstall(platformInformation: PlatformInformation, eventStream: EventStream, options: Options): Promise<boolean> {
return _debugUtil.checkDotNetCli(options.dotNetCliPaths)
.then(async (dotnetInfo: DotnetInfo) => {

let isValidArchitecture: boolean = await checkIsValidArchitecture(platformInformation, eventStream);
Expand Down Expand Up @@ -132,7 +133,7 @@ function showDotnetToolsWarning(message: string): void {
// Else it will launch the debug adapter
export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescriptorFactory {

constructor(private readonly platformInfo: PlatformInformation, private readonly eventStream: EventStream, private readonly packageJSON: any, private readonly extensionPath: string) {
constructor(private readonly platformInfo: PlatformInformation, private readonly eventStream: EventStream, private readonly packageJSON: any, private readonly extensionPath: string, private readonly options: Options) {
}

async createDebugAdapterDescriptor(_session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable | undefined): Promise<vscode.DebugAdapterDescriptor> {
Expand Down Expand Up @@ -160,7 +161,7 @@ export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescrip
}
// install.complete does not exist, check dotnetCLI to see if we can complete.
else if (!CoreClrDebugUtil.existsSync(util.installCompleteFilePath())) {
let success: boolean = await completeDebuggerInstall(this.platformInfo, this.eventStream);
let success: boolean = await completeDebuggerInstall(this.platformInfo, this.eventStream, this.options);

if (!success) {
this.eventStream.post(new DebuggerNotInstalledFailure());
Expand All @@ -172,12 +173,13 @@ export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescrip
// debugger has finished installation, kick off our debugger process

// Check for targetArchitecture
const targetArchitecture: string = getTargetArchitecture(this.platformInfo, _session.configuration.targetArchitecture, await getDotnetInfo());
let dotNetInfo = await getDotnetInfo(this.options.dotNetCliPaths);
const targetArchitecture: string = getTargetArchitecture(this.platformInfo, _session.configuration.targetArchitecture, dotNetInfo);

// use the executable specified in the package.json if it exists or determine it based on some other information (e.g. the session)
if (!executable) {
const command = path.join(common.getExtensionPath(), ".debugger", targetArchitecture, "vsdbg-ui" + CoreClrDebugUtil.getPlatformExeExtension());
executable = new vscode.DebugAdapterExecutable(command);
executable = new vscode.DebugAdapterExecutable(command, [], { env: { 'DOTNET_ROOT' : dotNetInfo.CliPath ? path.dirname(dotNetInfo.CliPath) : '' } });
}

// make VS Code launch the DA executable
Expand Down
4 changes: 2 additions & 2 deletions src/coreclr-debug/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ export class CoreClrDebugUtil {
// is new enough for us.
// Returns: a promise that returns a DotnetInfo class
// Throws: An DotNetCliError() from the return promise if either dotnet does not exist or is too old.
public async checkDotNetCli(): Promise<DotnetInfo> {
let dotnetInfo = await getDotnetInfo();
public async checkDotNetCli(dotNetCliPaths: string[]): Promise<DotnetInfo> {
let dotnetInfo = await getDotnetInfo(dotNetCliPaths);

if (dotnetInfo.FullInfo === DOTNET_MISSING_MESSAGE) {
// something went wrong with spawning 'dotnet --info'
Expand Down
4 changes: 2 additions & 2 deletions src/features/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { IHostExecutableResolver } from '../constants/IHostExecutableResolver';
import { getDotnetInfo } from '../utils/getDotnetInfo';
import { getDecompilationAuthorization, resetDecompilationAuthorization } from '../omnisharp/decompilationPrompt';

export default function registerCommands(context: vscode.ExtensionContext, server: OmniSharpServer, platformInfo: PlatformInformation, eventStream: EventStream, optionProvider: OptionProvider, monoResolver: IHostExecutableResolver, packageJSON: any, extensionPath: string): CompositeDisposable {
export default function registerCommands(context: vscode.ExtensionContext, server: OmniSharpServer, platformInfo: PlatformInformation, eventStream: EventStream, optionProvider: OptionProvider, monoResolver: IHostExecutableResolver, dotnetResolver: IHostExecutableResolver, packageJSON: any, extensionPath: string): CompositeDisposable {
let disposable = new CompositeDisposable();
disposable.add(vscode.commands.registerCommand('o.restart', async () => restartOmniSharp(context, server, optionProvider)));
disposable.add(vscode.commands.registerCommand('o.pickProjectAndStart', async () => pickProjectAndStart(server, optionProvider)));
Expand Down Expand Up @@ -53,7 +53,7 @@ export default function registerCommands(context: vscode.ExtensionContext, serve
// Register command for generating tasks.json and launch.json assets.
disposable.add(vscode.commands.registerCommand('dotnet.generateAssets', async (selectedIndex) => generateAssets(server, selectedIndex)));

disposable.add(vscode.commands.registerCommand('csharp.reportIssue', async () => reportIssue(vscode, eventStream, getDotnetInfo, platformInfo.isValidPlatformForMono(), optionProvider.GetLatestOptions(), monoResolver)));
disposable.add(vscode.commands.registerCommand('csharp.reportIssue', async () => reportIssue(vscode, eventStream, getDotnetInfo, platformInfo.isValidPlatformForMono(), optionProvider.GetLatestOptions(), monoResolver, dotnetResolver)));

disposable.add(vscode.commands.registerCommand('csharp.showDecompilationTerms', async () => showDecompilationTerms(context, server, optionProvider)));

Expand Down
6 changes: 4 additions & 2 deletions src/features/reportIssue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import { OpenURL } from "../omnisharp/loggingEvents";
import { Options } from "../omnisharp/options";
import { IHostExecutableResolver } from "../constants/IHostExecutableResolver";
import { IGetDotnetInfo } from "../constants/IGetDotnetInfo";
import { dirname } from "path";

const issuesUrl = "https://github.com/OmniSharp/omnisharp-vscode/issues/new";

export default async function reportIssue(vscode: vscode, eventStream: EventStream, getDotnetInfo: IGetDotnetInfo, isValidPlatformForMono: boolean, options: Options, monoResolver: IHostExecutableResolver) {
const dotnetInfo = await getDotnetInfo();
export default async function reportIssue(vscode: vscode, eventStream: EventStream, getDotnetInfo: IGetDotnetInfo, isValidPlatformForMono: boolean, options: Options, monoResolver: IHostExecutableResolver, dotnetResolver: IHostExecutableResolver) {
// Get info for the dotnet that the Omnisharp executable is run on, not the dotnet Omnisharp will execute user code on.
const dotnetInfo = await getDotnetInfo([ dirname((await dotnetResolver.getHostExecutableInfo(options)).path) ]);
const monoInfo = await getMonoIfPlatformValid(isValidPlatformForMono, options, monoResolver);
let extensions = getInstalledExtensions(vscode);
let csharpExtVersion = getCsharpExtensionVersion(vscode);
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<CSharp
let coreClrDebugPromise = Promise.resolve();
if (runtimeDependenciesExist) {
// activate coreclr-debug
coreClrDebugPromise = coreclrdebug.activate(context.extension, context, platformInfo, eventStream);
coreClrDebugPromise = coreclrdebug.activate(context.extension, context, platformInfo, eventStream, optionProvider.GetLatestOptions());
}

let razorPromise = Promise.resolve();
Expand Down
2 changes: 1 addition & 1 deletion src/observers/TelemetryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class TelemetryObserver {
}

private async handleOmnisharpInitialisation(event: OmnisharpInitialisation) {
this.dotnetInfo = await getDotnetInfo();
this.dotnetInfo = await getDotnetInfo(event.dotNetCliPaths);
this.solutionId = this.createSolutionId(event.solutionPath);
}

Expand Down
2 changes: 1 addition & 1 deletion src/omnisharp/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export async function activate(context: vscode.ExtensionContext, packageJSON: an
localDisposables = undefined;
}));

disposables.add(registerCommands(context, server, platformInfo, eventStream, optionProvider, omnisharpMonoResolver, packageJSON, extensionPath));
disposables.add(registerCommands(context, server, platformInfo, eventStream, optionProvider, omnisharpMonoResolver, omnisharpDotnetResolver, packageJSON, extensionPath));

if (!context.workspaceState.get<boolean>('assetPromptDisabled')) {
disposables.add(server.onServerStart(async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/omnisharp/loggingEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class OmnisharpStart extends TelemetryEventWithMeasures {

export class OmnisharpInitialisation implements BaseEvent {
type = EventType.OmnisharpInitialisation;
constructor(public timeStamp: Date, public solutionPath: string) { }
constructor(public dotNetCliPaths: string[], public timeStamp: Date, public solutionPath: string) { }
}

export class OmnisharpLaunch implements BaseEvent {
Expand Down
8 changes: 6 additions & 2 deletions src/omnisharp/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ export class Options {
public dotnetPath: string,
public excludePaths: string[],
public maxProjectFileCountForDiagnosticAnalysis: number,
public testRunSettings: string) {
public testRunSettings: string,
public dotNetCliPaths: string[]) {
}

public static Read(vscode: vscode): Options {
Expand Down Expand Up @@ -148,6 +149,8 @@ export class Options {

const excludePaths = this.getExcludedPaths(vscode);

const dotNetCliPaths = omnisharpConfig.get<string[]>('dotNetCliPaths', []);

return new Options(
path,
useModernNet,
Expand Down Expand Up @@ -198,7 +201,8 @@ export class Options {
dotnetPath,
excludePaths,
maxProjectFileCountForDiagnosticAnalysis,
testRunSettings
testRunSettings,
dotNetCliPaths
);
}

Expand Down
6 changes: 5 additions & 1 deletion src/omnisharp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,10 @@ export class OmniSharpServer {
args.push('RoslynExtensionsOptions:AnalyzeOpenDocumentsOnly=true');
}

for (let i = 0; i < options.dotNetCliPaths.length; i++) {
args.push(`DotNetCliOptions:LocationPaths:${i}=${options.dotNetCliPaths[i]}`);
}

let launchInfo: LaunchInfo;
try {
launchInfo = await this._omnisharpManager.GetOmniSharpLaunchInfo(this.packageJSON.defaults.omniSharp, options.path, /* useFramework */ !options.useModernNet, serverUrl, latestVersionFileServerPath, installPath, this.extensionPath);
Expand All @@ -443,7 +447,7 @@ export class OmniSharpServer {
return;
}

this.eventStream.post(new ObservableEvents.OmnisharpInitialisation(new Date(), solutionPath));
this.eventStream.post(new ObservableEvents.OmnisharpInitialisation(options.dotNetCliPaths, new Date(), solutionPath));
this._fireEvent(Events.BeforeServerStart, solutionPath);

try {
Expand Down
20 changes: 18 additions & 2 deletions src/utils/getDotnetInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { join } from "path";
import { execChildProcess } from "../common";
import { CoreClrDebugUtil } from "../coreclr-debug/util";

export const DOTNET_MISSING_MESSAGE = "A valid dotnet installation could not be found.";

Expand All @@ -13,15 +15,28 @@ let _dotnetInfo: DotnetInfo;
// is new enough for us.
// Returns: a promise that returns a DotnetInfo class
// Throws: An DotNetCliError() from the return promise if either dotnet does not exist or is too old.
export async function getDotnetInfo(): Promise<DotnetInfo> {
export async function getDotnetInfo(dotNetCliPaths: string[]): Promise<DotnetInfo> {
if (_dotnetInfo !== undefined) {
return _dotnetInfo;
}

let dotnetExeName = CoreClrDebugUtil.getPlatformExeExtension();
let dotnetExecutablePath = undefined;

for (const dotnetPath of dotNetCliPaths) {
let dotnetFullPath = join(dotnetPath, dotnetExeName);
if (CoreClrDebugUtil.existsSync(dotnetFullPath)) {
dotnetExecutablePath = dotnetFullPath;
break;
}
}

let dotnetInfo = new DotnetInfo();

try {
let data = await execChildProcess('dotnet --info', process.cwd());
let data = await execChildProcess(`${dotnetExecutablePath || 'dotnet'} --info`, process.cwd());

dotnetInfo.CliPath = dotnetExecutablePath;

dotnetInfo.FullInfo = data;

Expand Down Expand Up @@ -49,6 +64,7 @@ export async function getDotnetInfo(): Promise<DotnetInfo> {
}

export class DotnetInfo {
public CliPath?: string;
public FullInfo: string;
public Version: string;
public OsVersion: string;
Expand Down
30 changes: 30 additions & 0 deletions test/unitTests/Fakes/FakeDotnetResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IHostExecutableResolver } from "../../../src/constants/IHostExecutableResolver";
import { HostExecutableInformation } from "../../../src/constants/HostExecutableInformation";

export const fakeMonoInfo: HostExecutableInformation = {
version: "someDotNetVersion",
path: "someDotNetPath",
env: undefined
};

export class FakeDotnetResolver implements IHostExecutableResolver {
public getDotnetCalled: boolean;

constructor(public willReturnDotnetInfo = true) {
this.getDotnetCalled = false;
}

async getHostExecutableInfo(): Promise<HostExecutableInformation> {
this.getDotnetCalled = true;
if (this.willReturnDotnetInfo) {
return Promise.resolve(fakeMonoInfo);
}

return Promise.resolve(undefined);
}
}
3 changes: 2 additions & 1 deletion test/unitTests/Fakes/FakeOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,6 @@ export function getEmptyOptions(): Options {
/* dotnetPath */"",
/* excludePaths */null,
/* maxProjectFileCountForDiagnosticAnalysis */null,
/* testRunSettings */"");
/* testRunSettings */"",
/* dotNetCliPaths */[]);
}
Loading