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

Do not assume the Linux distro to be debian if dnf is not found while installing python #19582

Merged
merged 2 commits into from
Jul 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ export namespace Interpreters {
'Interpreters.selectInterpreterTip',
'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar',
);
export const installPythonTerminalMessage = localize(
'Interpreters.installPythonTerminalMessage',
'💡 Please try installing the python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads',
);
}

export namespace InterpreterQuickPickList {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ import { inject, injectable } from 'inversify';
import { IExtensionSingleActivationService } from '../../../../../activation/types';
import { Commands } from '../../../../../common/constants';
import { IDisposableRegistry } from '../../../../../common/types';
import { ITerminalServiceFactory } from '../../../../../common/terminal/types';
import { ICommandManager } from '../../../../../common/application/types';
import { ICommandManager, ITerminalManager } from '../../../../../common/application/types';
import { sleep } from '../../../../../common/utils/async';
import { OSType } from '../../../../../common/utils/platform';
import { traceVerbose } from '../../../../../logging';
import { Interpreters } from '../../../../../common/utils/localize';

enum PackageManagers {
brew = 'brew',
apt = 'apt',
dnf = 'dnf',
}

/**
* Runs commands listed in walkthrough to install Python.
Expand All @@ -22,9 +28,15 @@ import { traceVerbose } from '../../../../../logging';
export class InstallPythonViaTerminal implements IExtensionSingleActivationService {
public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: false };

private readonly packageManagerCommands: Record<PackageManagers, string[]> = {
brew: ['brew install python3'],
dnf: ['sudo dnf install python3'],
apt: ['sudo apt-get update', 'sudo apt-get install python3 python3-venv python3-pip'],
};

constructor(
@inject(ICommandManager) private readonly commandManager: ICommandManager,
@inject(ITerminalServiceFactory) private readonly terminalServiceFactory: ITerminalServiceFactory,
@inject(ITerminalManager) private readonly terminalManager: ITerminalManager,
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
) {}

Expand All @@ -42,36 +54,49 @@ export class InstallPythonViaTerminal implements IExtensionSingleActivationServi
}

public async _installPythonOnUnix(os: OSType.Linux | OSType.OSX): Promise<void> {
const terminalService = this.terminalServiceFactory.getTerminalService({});
const commands = await getCommands(os);
const commands = await this.getCommands(os);
const terminal = this.terminalManager.createTerminal({
name: 'Python',
message: commands.length ? undefined : Interpreters.installPythonTerminalMessage,
});
terminal.show(true);
await waitForTerminalToStartup();
for (const command of commands) {
await terminalService.sendText(command);
terminal.sendText(command);
await waitForCommandToProcess();
}
}
}

async function getCommands(os: OSType.Linux | OSType.OSX) {
if (os === OSType.OSX) {
return ['brew install python3'];
private async getCommands(os: OSType.Linux | OSType.OSX) {
if (os === OSType.OSX) {
return this.packageManagerCommands[PackageManagers.brew];
}
return this.getCommandsForLinux();
}
return getCommandsForLinux();
}

async function getCommandsForLinux() {
let isDnfAvailable = false;
try {
const which = require('which') as typeof whichTypes;
const resolvedPath = await which('dnf');
traceVerbose('Resolved path to dnf module:', resolvedPath);
isDnfAvailable = resolvedPath.trim().length > 0;
} catch (ex) {
traceVerbose('Dnf not found', ex);
isDnfAvailable = false;
private async getCommandsForLinux() {
for (const packageManager of [PackageManagers.apt, PackageManagers.dnf]) {
let isPackageAvailable = false;
try {
const which = require('which') as typeof whichTypes;
const resolvedPath = await which(packageManager);
traceVerbose(`Resolved path to ${packageManager} module:`, resolvedPath);
isPackageAvailable = resolvedPath.trim().length > 0;
} catch (ex) {
traceVerbose(`${packageManager} not found`, ex);
isPackageAvailable = false;
}
if (isPackageAvailable) {
return this.packageManagerCommands[packageManager];
}
}
return [];
}
return isDnfAvailable
? ['sudo dnf install python3']
: ['sudo apt-get update', 'sudo apt-get install python3 python3-venv python3-pip'];
}

async function waitForTerminalToStartup() {
// Sometimes the terminal takes some time to start up before it can start accepting input.
await sleep(100);
}

async function waitForCommandToProcess() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,34 @@

'use strict';

import { expect } from 'chai';
import rewiremock from 'rewiremock';
import * as sinon from 'sinon';
import { anything, instance, mock, verify, when } from 'ts-mockito';
import * as TypeMoq from 'typemoq';
import { ICommandManager } from '../../../../client/common/application/types';
import { ICommandManager, ITerminalManager } from '../../../../client/common/application/types';
import { Commands } from '../../../../client/common/constants';
import { ITerminalService, ITerminalServiceFactory } from '../../../../client/common/terminal/types';
import { ITerminalService } from '../../../../client/common/terminal/types';
import { IDisposable } from '../../../../client/common/types';
import { Interpreters } from '../../../../client/common/utils/localize';
import { InstallPythonViaTerminal } from '../../../../client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal';

suite('Install Python via Terminal', () => {
let cmdManager: ICommandManager;
let terminalServiceFactory: ITerminalServiceFactory;
let terminalServiceFactory: ITerminalManager;
let installPythonCommand: InstallPythonViaTerminal;
let terminalService: ITerminalService;
let message: string | undefined;
setup(() => {
rewiremock.enable();
cmdManager = mock<ICommandManager>();
terminalServiceFactory = mock<ITerminalServiceFactory>();
terminalServiceFactory = mock<ITerminalManager>();
terminalService = mock<ITerminalService>();
when(terminalServiceFactory.getTerminalService(anything())).thenReturn(instance(terminalService));
message = undefined;
when(terminalServiceFactory.createTerminal(anything())).thenCall((options) => {
message = options.message;
return instance(terminalService);
});
installPythonCommand = new InstallPythonViaTerminal(instance(cmdManager), instance(terminalServiceFactory), []);
});

Expand All @@ -32,12 +39,18 @@ suite('Install Python via Terminal', () => {
sinon.restore();
});

test('Sends expected commands when InstallPythonOnLinux command is executed if no dnf is available', async () => {
test('Sends expected commands when InstallPythonOnLinux command is executed if apt is available', async () => {
let installCommandHandler: () => Promise<void>;
when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => {
installCommandHandler = cb;
return TypeMoq.Mock.ofType<IDisposable>().object;
});
rewiremock('which').with((cmd: string) => {
if (cmd === 'apt') {
return 'path/to/apt';
}
throw new Error('Command not found');
});
await installPythonCommand.activate();
when(terminalService.sendText('sudo apt-get update')).thenResolve();
when(terminalService.sendText('sudo apt-get install python3 python3-venv python3-pip')).thenResolve();
Expand Down Expand Up @@ -67,6 +80,23 @@ suite('Install Python via Terminal', () => {
await installCommandHandler!();

verify(terminalService.sendText('sudo dnf install python3')).once();
expect(message).to.be.equal(undefined);
});

test('Creates terminal with appropriate message when InstallPythonOnLinux command is executed if no known linux package managers are available', async () => {
let installCommandHandler: () => Promise<void>;
when(cmdManager.registerCommand(Commands.InstallPythonOnLinux, anything())).thenCall((_, cb) => {
installCommandHandler = cb;
return TypeMoq.Mock.ofType<IDisposable>().object;
});
rewiremock('which').with((_cmd: string) => {
throw new Error('Command not found');
});

await installPythonCommand.activate();
await installCommandHandler!();

expect(message).to.be.equal(Interpreters.installPythonTerminalMessage);
});

test('Sends expected commands on Mac when InstallPythonOnMac command is executed if no dnf is available', async () => {
Expand All @@ -81,5 +111,6 @@ suite('Install Python via Terminal', () => {
await installCommandHandler!();

verify(terminalService.sendText('brew install python3')).once();
expect(message).to.be.equal(undefined);
});
});