diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json index b15e983059..f45984c4a8 100644 --- a/extensions/notebook/package.json +++ b/extensions/notebook/package.json @@ -22,6 +22,11 @@ "default": "", "description": "%notebook.pythonPath.description%" }, + "notebook.useExistingPython": { + "type": "boolean", + "default": false, + "description": "%notebook.useExistingPython.description%" + }, "notebook.overrideEditorTheming": { "type": "boolean", "default": true, diff --git a/extensions/notebook/package.nls.json b/extensions/notebook/package.nls.json index d6819f16d1..076302509d 100644 --- a/extensions/notebook/package.nls.json +++ b/extensions/notebook/package.nls.json @@ -3,6 +3,7 @@ "description": "Defines the Data-procotol based Notebook contribution and many Notebook commands and contributions.", "notebook.configuration.title": "Notebook configuration", "notebook.pythonPath.description": "Local path to python installation used by Notebooks.", + "notebook.useExistingPython.description": "Local path to a preexisting python installation used by Notebooks.", "notebook.overrideEditorTheming.description": "Override editor default settings in the Notebook editor. Settings include background color, current line color and border", "notebook.maxTableRows.description": "Maximum number of rows returned per table in the Notebook editor", "notebook.command.new": "New Notebook", diff --git a/extensions/notebook/src/common/constants.ts b/extensions/notebook/src/common/constants.ts index 85147c6f90..722ef50703 100644 --- a/extensions/notebook/src/common/constants.ts +++ b/extensions/notebook/src/common/constants.ts @@ -21,6 +21,7 @@ export const pyspark3kernel = 'pyspark3kernel'; export const python3DisplayName = 'Python 3'; export const defaultSparkKernel = 'pyspark3kernel'; export const pythonPathConfigKey = 'pythonPath'; +export const existingPythonConfigKey = 'useExistingPython'; export const notebookConfigKey = 'notebook'; export const outputChannelName = 'dataManagement'; @@ -49,3 +50,8 @@ export enum BuiltInCommands { export enum CommandContext { WizardServiceEnabled = 'wizardservice:enabled' } + +export const pythonOfflinePipPackagesUrl = 'https://go.microsoft.com/fwlink/?linkid=2092867'; +export const pythonWindowsInstallUrl = 'https://go.microsoft.com/fwlink/?linkid=2092866'; +export const pythonMacInstallUrl = 'https://go.microsoft.com/fwlink/?linkid=2092865'; +export const pythonLinuxInstallUrl = 'https://go.microsoft.com/fwlink/?linkid=2092864'; \ No newline at end of file diff --git a/extensions/notebook/src/common/localizedConstants.ts b/extensions/notebook/src/common/localizedConstants.ts index 59930ad352..198df92b38 100644 --- a/extensions/notebook/src/common/localizedConstants.ts +++ b/extensions/notebook/src/common/localizedConstants.ts @@ -14,6 +14,6 @@ export const msgNo = localize('msgNo', 'No'); // Jupyter Constants /////////////////////////////////////////////////////// export const msgManagePackagesPowershell = localize('msgManagePackagesPowershell', '<#\n--------------------------------------------------------------------------------\n\tThis is the sandboxed instance of python used by Jupyter server.\n\tTo install packages used by the python kernel use \'.\\python.exe -m pip install\'\n--------------------------------------------------------------------------------\n#>'); -export const msgManagePackagesBash = localize('msgJupyterManagePackagesBash', ': \'\n--------------------------------------------------------------------------------\n\tThis is the sandboxed instance of python used by Jupyter server.\n\tTo install packages used by the python kernel use \'./python3.6 -m pip install\'\n--------------------------------------------------------------------------------\n\''); +export const msgManagePackagesBash = localize('msgJupyterManagePackagesBash', ': \'\n--------------------------------------------------------------------------------\n\tThis is the sandboxed instance of python used by Jupyter server.\n\tTo install packages used by the python kernel use \'./python3 -m pip install\'\n--------------------------------------------------------------------------------\n\''); export const msgManagePackagesCmd = localize('msgJupyterManagePackagesCmd', 'REM This is the sandboxed instance of python used by Jupyter server. To install packages used by the python kernel use \'.\\python.exe -m pip install\''); export const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', 'This sample code loads the file into a data frame and shows the first 10 results.'); diff --git a/extensions/notebook/src/common/utils.ts b/extensions/notebook/src/common/utils.ts index eb14a4ce6e..12baba0755 100644 --- a/extensions/notebook/src/common/utils.ts +++ b/extensions/notebook/src/common/utils.ts @@ -54,13 +54,15 @@ export function executeBufferedCommand(cmd: string, options: childProcess.ExecOp }); } -export function executeStreamedCommand(cmd: string, outputChannel?: vscode.OutputChannel): Thenable { +export function executeStreamedCommand(cmd: string, options: childProcess.SpawnOptions, outputChannel?: vscode.OutputChannel): Thenable { return new Promise((resolve, reject) => { // Start the command if (outputChannel) { outputChannel.appendLine(` > ${cmd}`); } - let child = childProcess.spawn(cmd, [], { shell: true, detached: false }); + options.shell = true; + options.detached = false; + let child = childProcess.spawn(cmd, [], options); // Add listeners to resolve/reject the promise on exit child.on('error', reject); diff --git a/extensions/notebook/src/dialog/configurePythonDialog.ts b/extensions/notebook/src/dialog/configurePythonDialog.ts index 8b547f0d50..d2610f5b5e 100644 --- a/extensions/notebook/src/dialog/configurePythonDialog.ts +++ b/extensions/notebook/src/dialog/configurePythonDialog.ts @@ -18,17 +18,20 @@ const localize = nls.loadMessageBundle(); export class ConfigurePythonDialog { private dialog: azdata.window.Dialog; - private readonly DialogTitle = localize('configurePython.dialogName', 'Configure Python for Notebooks'); - private readonly OkButtonText = localize('configurePython.okButtonText', 'Install'); - private readonly CancelButtonText = localize('configurePython.cancelButtonText', 'Cancel'); - private readonly BrowseButtonText = localize('configurePython.browseButtonText', 'Change location'); - private readonly LocationTextBoxTitle = localize('configurePython.locationTextBoxText', 'Notebook dependencies will be installed in this location'); - private readonly SelectFileLabel = localize('configurePython.selectFileLabel', 'Select'); - private readonly InstallationNote = localize('configurePython.installNote', 'This installation will take some time. It is recommended to not close the application until the installation is complete.'); - private readonly InvalidLocationMsg = localize('configurePython.invalidLocationMsg', 'The specified install location is invalid.'); + private readonly DialogTitle = localize('configurePython.dialogName', "Configure Python for Notebooks"); + private readonly InstallButtonText = localize('configurePython.okButtonText', "Install"); + private readonly CancelButtonText = localize('configurePython.cancelButtonText', "Cancel"); + private readonly BrowseButtonText = localize('configurePython.browseButtonText', "Change location"); + private readonly LocationTextBoxTitle = localize('configurePython.locationTextBoxText', "Python Install Location"); + private readonly SelectFileLabel = localize('configurePython.selectFileLabel', "Select"); + private readonly InstallationNote = localize('configurePython.installNote', "This installation will take some time. It is recommended to not close the application until the installation is complete."); + private readonly InvalidLocationMsg = localize('configurePython.invalidLocationMsg', "The specified install location is invalid."); + private readonly PythonNotFoundMsg = localize('configurePython.pythonNotFoundMsg', "No python installation was found at the specified location."); private pythonLocationTextBox: azdata.InputBoxComponent; private browseButton: azdata.ButtonComponent; + private newInstallButton: azdata.RadioButtonComponent; + private existingInstallButton: azdata.RadioButtonComponent; private _setupComplete: Deferred; @@ -46,11 +49,11 @@ export class ConfigurePythonDialog { this.initializeContent(); - this.dialog.okButton.label = this.OkButtonText; + this.dialog.okButton.label = this.InstallButtonText; this.dialog.cancelButton.label = this.CancelButtonText; this.dialog.cancelButton.onClick(() => { if (rejectOnCancel) { - this._setupComplete.reject(localize('pythonInstallDeclined', 'Python installation was declined.')); + this._setupComplete.reject(localize('configurePython.pythonInstallDeclined', "Python installation was declined.")); } else { this._setupComplete.resolve(); } @@ -92,23 +95,54 @@ export class ConfigurePythonDialog { } }); + this.createInstallRadioButtons(view.modelBuilder); + let formModel = view.modelBuilder.formContainer() .withFormItems([{ + component: this.newInstallButton, + title: localize('configurePython.installationType', "Installation Type") + }, { + component: this.existingInstallButton, + title: '' + }, { component: this.pythonLocationTextBox, title: this.LocationTextBoxTitle }, { component: this.browseButton, - title: undefined + title: '' }, { component: noteWrapper, - title: undefined + title: '' }]).component(); - await view.initializeModel(formModel); }); } + private createInstallRadioButtons(modelBuilder: azdata.ModelBuilder): void { + let useExistingPython = JupyterServerInstallation.getExistingPythonSetting(this.apiWrapper); + let buttonGroup = 'installationType'; + this.newInstallButton = modelBuilder.radioButton() + .withProperties({ + name: buttonGroup, + label: localize('configurePython.newInstall', "New Python installation"), + checked: !useExistingPython + }).component(); + this.newInstallButton.onDidClick(() => { + this.existingInstallButton.checked = false; + }); + + this.existingInstallButton = modelBuilder.radioButton() + .withProperties({ + name: buttonGroup, + label: localize('configurePython.existingInstall', "Use existing Python installation"), + checked: useExistingPython + }).component(); + this.existingInstallButton.onDidClick(() => { + this.newInstallButton.checked = false; + }); + } + private async handleInstall(): Promise { let pythonLocation = this.pythonLocationTextBox.value; if (!pythonLocation || pythonLocation.length === 0) { @@ -116,24 +150,35 @@ export class ConfigurePythonDialog { return false; } + let useExistingPython = !!this.existingInstallButton.checked; try { let isValid = await this.isFileValid(pythonLocation); if (!isValid) { return false; } + + if (useExistingPython) { + let exePath = JupyterServerInstallation.getPythonExePath(pythonLocation, true); + let pythonExists = fs.existsSync(exePath); + if (!pythonExists) { + this.showErrorMessage(this.PythonNotFoundMsg); + return false; + } + } } catch (err) { - this.apiWrapper.showErrorMessage(utils.getErrorMessage(err)); + this.showErrorMessage(utils.getErrorMessage(err)); return false; } // Don't wait on installation, since there's currently no Cancel functionality - this.jupyterInstallation.startInstallProcess(false, pythonLocation) + this.jupyterInstallation.startInstallProcess(false, { installPath: pythonLocation, existingPython: useExistingPython }) .then(() => { this._setupComplete.resolve(); }) .catch(err => { this._setupComplete.reject(utils.getErrorMessage(err)); }); + return true; } diff --git a/extensions/notebook/src/integrationTest/notebookIntegration.test.ts b/extensions/notebook/src/integrationTest/notebookIntegration.test.ts index 606ad65a6d..64d4d60fa2 100644 --- a/extensions/notebook/src/integrationTest/notebookIntegration.test.ts +++ b/extensions/notebook/src/integrationTest/notebookIntegration.test.ts @@ -6,10 +6,13 @@ import * as should from 'should'; import * as vscode from 'vscode'; import * as assert from 'assert'; +import * as path from 'path'; import 'mocha'; import { JupyterController } from '../jupyter/jupyterController'; import JupyterServerInstallation from '../jupyter/jupyterServerInstallation'; +import { pythonBundleVersion } from '../common/constants'; +import { executeStreamedCommand } from '../common/utils'; describe('Notebook Extension Python Installation', function () { this.timeout(600000); @@ -35,7 +38,7 @@ describe('Notebook Extension Python Installation', function () { jupyterController = notebookExtension.exports.getJupyterController() as JupyterController; console.log('Start Jupyter Installation'); - await jupyterController.jupyterInstallation.startInstallProcess(false, pythonInstallDir); + await jupyterController.jupyterInstallation.startInstallProcess(false, { installPath: pythonInstallDir, existingPython: false }); installComplete = true; console.log('Jupyter Installation is done'); }); @@ -44,6 +47,22 @@ describe('Notebook Extension Python Installation', function () { should(installComplete).be.true('Python setup did not complete.'); let jupyterPath = JupyterServerInstallation.getPythonInstallPath(jupyterController.jupyterInstallation.apiWrapper); console.log(`Expected python path: '${pythonInstallDir}'; actual: '${jupyterPath}'`); - should(JupyterServerInstallation.getPythonInstallPath(jupyterController.jupyterInstallation.apiWrapper)).be.equal(pythonInstallDir); + should(jupyterPath).be.equal(pythonInstallDir); + }); + + it('Use Existing Python Installation', async function () { + should(installComplete).be.true('Python setup did not complete.'); + + console.log('Uninstalling existing pip dependencies'); + let install = jupyterController.jupyterInstallation; + let pythonExe = JupyterServerInstallation.getPythonExePath(pythonInstallDir, false); + let command = `"${pythonExe}" -m pip uninstall -y jupyter pandas sparkmagic prose-codeaccelerator`; + await executeStreamedCommand(command, { env: install.execOptions.env }, install.outputChannel); + console.log('Uninstalling existing pip dependencies is done'); + + console.log('Start Existing Python Installation'); + let existingPythonPath = path.join(pythonInstallDir, pythonBundleVersion); + await install.startInstallProcess(false, { installPath: existingPythonPath, existingPython: true }); + console.log('Existing Python Installation is done'); }); }); diff --git a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts index da730714b9..bd2873b385 100644 --- a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts +++ b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts @@ -36,10 +36,6 @@ function msgDependenciesInstallationFailed(errorMessage: string): string { retur function msgDownloadPython(platform: string, pythonDownloadUrl: string): string { return localize('msgDownloadPython', "Downloading local python for platform: {0} to {1}", platform, pythonDownloadUrl); } export default class JupyterServerInstallation { - /** - * Path to the folder where all configuration sets will be stored. Should always be: - * %extension_path%/jupyter_config - */ public apiWrapper: ApiWrapper; public extensionPath: string; public pythonBinPath: string; @@ -50,6 +46,8 @@ export default class JupyterServerInstallation { private _pythonInstallationPath: string; private _pythonExecutable: string; private _pythonPackageDir: string; + private _usingExistingPython: boolean; + private _usingConda: boolean; // Allows dependencies to be installed even if an existing installation is already present private _forceInstall: boolean; @@ -63,14 +61,17 @@ export default class JupyterServerInstallation { this.apiWrapper = apiWrapper; this._pythonInstallationPath = pythonInstallationPath || JupyterServerInstallation.getPythonInstallPath(this.apiWrapper); this._forceInstall = false; + this._usingConda = false; this._installInProgress = false; + this._usingExistingPython = JupyterServerInstallation.getExistingPythonSetting(this.apiWrapper); this.configurePackagePaths(); } private async installDependencies(backgroundOperation: azdata.BackgroundOperation): Promise { - if (!fs.existsSync(this._pythonExecutable) || this._forceInstall) { + if (!fs.existsSync(this._pythonExecutable) || this._forceInstall || this._usingExistingPython) { window.showInformationMessage(msgInstallPkgStart); + this.outputChannel.show(true); this.outputChannel.appendLine(msgInstallPkgProgress); backgroundOperation.updateStatus(azdata.TaskStatus.InProgress, msgInstallPkgProgress); @@ -79,9 +80,21 @@ export default class JupyterServerInstallation { this.outputChannel.appendLine(msgPythonDownloadComplete); backgroundOperation.updateStatus(azdata.TaskStatus.InProgress, msgPythonDownloadComplete); - // Install jupyter on Windows because local python is not bundled with jupyter unlike linux and MacOS. - await this.installJupyterProsePackage(); - await this.installSparkMagic(); + if (this._usingConda) { + await this.installCondaPackages(); + } else if (this._usingExistingPython) { + await this.installPipPackages(); + } else { + await this.installOfflinePipPackages(); + } + let doOnlineInstall = this._usingExistingPython; + await this.installSparkMagic(doOnlineInstall); + + fs.remove(this._pythonPackageDir, (err: Error) => { + if (err) { + this.outputChannel.appendLine(err.message); + } + }); this.outputChannel.appendLine(msgInstallPkgFinish); backgroundOperation.updateStatus(azdata.TaskStatus.Succeeded, msgInstallPkgFinish); @@ -92,26 +105,28 @@ export default class JupyterServerInstallation { private installPythonPackage(backgroundOperation: azdata.BackgroundOperation): Promise { let bundleVersion = constants.pythonBundleVersion; let pythonVersion = constants.pythonVersion; - let packageName = 'python-#pythonversion-#platform-#bundleversion.#extension'; let platformId = utils.getOSPlatformId(); + let packageName: string; + let pythonDownloadUrl: string; + if (this._usingExistingPython) { + packageName = `python-${pythonVersion}-${bundleVersion}-offlinePackages.zip`; + pythonDownloadUrl = constants.pythonOfflinePipPackagesUrl; + } else { + let extension = process.platform === constants.winPlatform ? 'zip' : 'tar.gz'; + packageName = `python-${pythonVersion}-${platformId}-${bundleVersion}.${extension}`; - packageName = packageName.replace('#platform', platformId) - .replace('#pythonversion', pythonVersion) - .replace('#bundleversion', bundleVersion) - .replace('#extension', process.platform === constants.winPlatform ? 'zip' : 'tar.gz'); - - let pythonDownloadUrl: string = undefined; - switch (utils.getOSPlatform()) { - case utils.Platform.Windows: - pythonDownloadUrl = 'https://go.microsoft.com/fwlink/?linkid=2074021'; - break; - case utils.Platform.Mac: - pythonDownloadUrl = 'https://go.microsoft.com/fwlink/?linkid=2065976'; - break; - default: - // Default to linux - pythonDownloadUrl = 'https://go.microsoft.com/fwlink/?linkid=2065975'; - break; + switch (utils.getOSPlatform()) { + case utils.Platform.Windows: + pythonDownloadUrl = constants.pythonWindowsInstallUrl; + break; + case utils.Platform.Mac: + pythonDownloadUrl = constants.pythonMacInstallUrl; + break; + default: + // Default to linux + pythonDownloadUrl = constants.pythonLinuxInstallUrl; + break; + } } let pythonPackagePathLocal = this._pythonInstallationPath + '/' + packageName; @@ -190,25 +205,39 @@ export default class JupyterServerInstallation { private configurePackagePaths(): void { //Python source path up to bundle version - let pythonSourcePath = path.join(this._pythonInstallationPath, constants.pythonBundleVersion); + let pythonSourcePath = this._usingExistingPython + ? this._pythonInstallationPath + : path.join(this._pythonInstallationPath, constants.pythonBundleVersion); this._pythonPackageDir = path.join(pythonSourcePath, 'offlinePackages'); // Update python paths and properties to reference user's local python. let pythonBinPathSuffix = process.platform === constants.winPlatform ? '' : 'bin'; - this._pythonExecutable = JupyterServerInstallation.getPythonExePath(this._pythonInstallationPath); + this._pythonExecutable = JupyterServerInstallation.getPythonExePath(this._pythonInstallationPath, this._usingExistingPython); this.pythonBinPath = path.join(pythonSourcePath, pythonBinPathSuffix); + this._usingConda = this.checkCondaExists(); + // Store paths to python libraries required to run jupyter. this.pythonEnvVarPath = process.env['PATH']; let delimiter = path.delimiter; + this.pythonEnvVarPath = this.pythonBinPath + delimiter + this.pythonEnvVarPath; if (process.platform === constants.winPlatform) { let pythonScriptsPath = path.join(pythonSourcePath, 'Scripts'); this.pythonEnvVarPath = pythonScriptsPath + delimiter + this.pythonEnvVarPath; + + if (this._usingConda) { + this.pythonEnvVarPath = [ + path.join(pythonSourcePath, 'Library', 'mingw-w64', 'bin'), + path.join(pythonSourcePath, 'Library', 'usr', 'bin'), + path.join(pythonSourcePath, 'Library', 'bin'), + path.join(pythonSourcePath, 'condabin'), + this.pythonEnvVarPath + ].join(delimiter); + } } - this.pythonEnvVarPath = this.pythonBinPath + delimiter + this.pythonEnvVarPath; // Delete existing Python variables in ADS to prevent conflict with other installs delete process.env['PYTHONPATH']; @@ -225,7 +254,7 @@ export default class JupyterServerInstallation { } private isPythonRunning(pythonInstallPath: string): Promise { - let pythonExePath = JupyterServerInstallation.getPythonExePath(pythonInstallPath); + let pythonExePath = JupyterServerInstallation.getPythonExePath(pythonInstallPath, this._usingExistingPython); return new Promise(resolve => { fs.open(pythonExePath, 'r+', (err, fd) => { if (!err) { @@ -246,8 +275,8 @@ export default class JupyterServerInstallation { * @param installationPath Optional parameter that specifies where to install python. * The previous path (or the default) is used if a new path is not specified. */ - public async startInstallProcess(forceInstall: boolean, installationPath?: string): Promise { - let isPythonRunning = await this.isPythonRunning(installationPath ? installationPath : this._pythonInstallationPath); + public async startInstallProcess(forceInstall: boolean, installSettings?: { installPath: string, existingPython: boolean }): Promise { + let isPythonRunning = await this.isPythonRunning(installSettings ? installSettings.installPath : this._pythonInstallationPath); if (isPythonRunning) { return Promise.reject(msgPythonRunningError); } @@ -258,18 +287,19 @@ export default class JupyterServerInstallation { this._installInProgress = true; this._forceInstall = forceInstall; - if (installationPath) { - this._pythonInstallationPath = installationPath; + if (installSettings) { + this._pythonInstallationPath = installSettings.installPath; + this._usingExistingPython = installSettings.existingPython; } this.configurePackagePaths(); let updateConfig = async () => { let notebookConfig = this.apiWrapper.getConfiguration(constants.notebookConfigKey); await notebookConfig.update(constants.pythonPathConfigKey, this._pythonInstallationPath, ConfigurationTarget.Global); + await notebookConfig.update(constants.existingPythonConfigKey, this._usingExistingPython, ConfigurationTarget.Global); }; - let installReady = new Deferred(); - if (!fs.existsSync(this._pythonExecutable) || this._forceInstall) { + if (!fs.existsSync(this._pythonExecutable) || this._forceInstall || this._usingExistingPython) { this.apiWrapper.startBackgroundOperation({ displayName: msgTaskName, description: msgTaskName, @@ -311,35 +341,92 @@ export default class JupyterServerInstallation { } } - private async installJupyterProsePackage(): Promise { + private async installOfflinePipPackages(): Promise { + let installJupyterCommand: string; if (process.platform === constants.winPlatform) { let requirements = path.join(this._pythonPackageDir, 'requirements.txt'); - let installJupyterCommand = `"${this._pythonExecutable}" -m pip install --no-index -r "${requirements}" --find-links "${this._pythonPackageDir}" --no-warn-script-location`; + installJupyterCommand = `"${this._pythonExecutable}" -m pip install --no-index -r "${requirements}" --find-links "${this._pythonPackageDir}" --no-warn-script-location`; + } + + if (installJupyterCommand) { this.outputChannel.show(true); this.outputChannel.appendLine(localize('msgInstallStart', "Installing required packages to run Notebooks...")); - await utils.executeStreamedCommand(installJupyterCommand, this.outputChannel); + await this.executeCommand(installJupyterCommand); this.outputChannel.appendLine(localize('msgJupyterInstallDone', "... Jupyter installation complete.")); } else { return Promise.resolve(); } } - private async installSparkMagic(): Promise { - if (process.platform === constants.winPlatform) { + private async installSparkMagic(doOnlineInstall: boolean): Promise { + let installSparkMagic: string; + if (process.platform === constants.winPlatform || this._usingExistingPython) { let sparkWheel = path.join(this._pythonPackageDir, `sparkmagic-${constants.sparkMagicVersion}-py3-none-any.whl`); - let installSparkMagic = `"${this._pythonExecutable}" -m pip install --no-index "${sparkWheel}" --find-links "${this._pythonPackageDir}" --no-warn-script-location`; + if (doOnlineInstall) { + installSparkMagic = `"${this._pythonExecutable}" -m pip install "${sparkWheel}" --no-warn-script-location`; + } else { + installSparkMagic = `"${this._pythonExecutable}" -m pip install --no-index "${sparkWheel}" --find-links "${this._pythonPackageDir}" --no-warn-script-location`; + } + } + + if (installSparkMagic) { this.outputChannel.show(true); this.outputChannel.appendLine(localize('msgInstallingSpark', "Installing SparkMagic...")); - await utils.executeStreamedCommand(installSparkMagic, this.outputChannel); - } else { - return Promise.resolve(); + await this.executeCommand(installSparkMagic); } } + private async installPipPackages(): Promise { + this.outputChannel.show(true); + this.outputChannel.appendLine(localize('msgInstallStart', "Installing required packages to run Notebooks...")); + + let installCommand = `"${this._pythonExecutable}" -m pip install jupyter==1.0.0 pandas==0.24.2`; + await this.executeCommand(installCommand); + + installCommand = `"${this._pythonExecutable}" -m pip install prose-codeaccelerator==1.3.0 --extra-index-url https://prose-python-packages.azurewebsites.net`; + await this.executeCommand(installCommand); + + this.outputChannel.appendLine(localize('msgJupyterInstallDone', "... Jupyter installation complete.")); + } + + private async installCondaPackages(): Promise { + this.outputChannel.show(true); + this.outputChannel.appendLine(localize('msgInstallStart', "Installing required packages to run Notebooks...")); + + let installCommand = `"${this.getCondaExePath()}" install -y jupyter==1.0.0 pandas==0.24.2`; + if (process.platform !== constants.winPlatform) { + installCommand = `${installCommand} pykerberos==1.2.1`; + } + await this.executeCommand(installCommand); + + installCommand = `"${this._pythonExecutable}" -m pip install prose-codeaccelerator==1.3.0 --extra-index-url https://prose-python-packages.azurewebsites.net`; + await this.executeCommand(installCommand); + + this.outputChannel.appendLine(localize('msgJupyterInstallDone', "... Jupyter installation complete.")); + } + + private async executeCommand(command: string): Promise { + await utils.executeStreamedCommand(command, { env: this.execOptions.env }, this.outputChannel); + } + public get pythonExecutable(): string { return this._pythonExecutable; } + private getCondaExePath(): string { + return path.join(this._pythonInstallationPath, + process.platform === constants.winPlatform ? 'Scripts\\conda.exe' : 'bin/conda'); + } + + private checkCondaExists(): boolean { + if (!this._usingExistingPython) { + return false; + } + + let condaExePath = this.getCondaExePath(); + return fs.existsSync(condaExePath); + } + /** * Checks if a python executable exists at the "notebook.pythonPath" defined in the user's settings. * @param apiWrapper An ApiWrapper to use when retrieving user settings info. @@ -351,7 +438,8 @@ export default class JupyterServerInstallation { return false; } - let pythonExe = JupyterServerInstallation.getPythonExePath(pathSetting); + let useExistingInstall = JupyterServerInstallation.getExistingPythonSetting(apiWrapper); + let pythonExe = JupyterServerInstallation.getPythonExePath(pathSetting, useExistingInstall); return fs.existsSync(pythonExe); } @@ -365,6 +453,17 @@ export default class JupyterServerInstallation { return userPath ? userPath : JupyterServerInstallation.DefaultPythonLocation; } + public static getExistingPythonSetting(apiWrapper: ApiWrapper): boolean { + let useExistingPython = false; + if (apiWrapper) { + let notebookConfig = apiWrapper.getConfiguration(constants.notebookConfigKey); + if (notebookConfig) { + useExistingPython = !!notebookConfig[constants.existingPythonConfigKey]; + } + } + return useExistingPython; + } + private static getPythonPathSetting(apiWrapper: ApiWrapper): string { let path = undefined; if (apiWrapper) { @@ -387,16 +486,18 @@ export default class JupyterServerInstallation { public static getPythonBinPath(apiWrapper: ApiWrapper): string { let pythonBinPathSuffix = process.platform === constants.winPlatform ? '' : 'bin'; + let useExistingInstall = JupyterServerInstallation.getExistingPythonSetting(apiWrapper); + return path.join( JupyterServerInstallation.getPythonInstallPath(apiWrapper), - constants.pythonBundleVersion, + useExistingInstall ? '' : constants.pythonBundleVersion, pythonBinPathSuffix); } - private static getPythonExePath(pythonInstallPath: string): string { + public static getPythonExePath(pythonInstallPath: string, useExistingInstall: boolean): string { return path.join( pythonInstallPath, - constants.pythonBundleVersion, + useExistingInstall ? '' : constants.pythonBundleVersion, process.platform === constants.winPlatform ? 'python.exe' : 'bin/python3'); } } \ No newline at end of file