From 370a7efea0a362aa5e3c5e3487293bb0b893c0ce Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Mon, 2 Oct 2023 07:39:53 -0700 Subject: [PATCH 01/10] show python reqs not found messages and option to install --- src/main/cli.ts | 3 + src/main/eventtypes.ts | 4 +- src/main/registry.ts | 136 ++++++++++++---------- src/main/sessionwindow/sessionwindow.ts | 99 +++++++++++++--- src/main/settingsdialog/preload.ts | 3 + src/main/settingsdialog/settingsdialog.ts | 5 + src/main/tokens.ts | 11 ++ src/main/utils.ts | 20 ++++ 8 files changed, 201 insertions(+), 80 deletions(-) diff --git a/src/main/cli.ts b/src/main/cli.ts index b0728e99..e09bbf0c 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -449,6 +449,8 @@ export async function runCommandInEnvironment( ' && ' ); + // TODO: implement timeout. in case there is network issues + return new Promise((resolve, reject) => { const shell = isWin ? spawn('cmd', ['/c', commandScript], { @@ -465,6 +467,7 @@ export async function runCommandInEnvironment( shell.on('close', code => { if (code !== 0) { console.error('Shell exit with code:', code); + resolve(false); } resolve(true); }); diff --git a/src/main/eventtypes.ts b/src/main/eventtypes.ts index a05dedc5..72058fc2 100644 --- a/src/main/eventtypes.ts +++ b/src/main/eventtypes.ts @@ -55,7 +55,9 @@ export enum EventTypeMain { SetServerLaunchArgs = 'set-server-launch-args', SetServerEnvVars = 'set-server-env-vars', SetCtrlWBehavior = 'set-ctrl-w-behavior', - SetAuthDialogResponse = 'set-auth-dialog-response' + SetAuthDialogResponse = 'set-auth-dialog-response', + InstallPythonEnvRequirements = 'install-python-env-requirements', + ShowLogs = 'show-logs' } // events sent to Renderer process diff --git a/src/main/registry.ts b/src/main/registry.ts index f974338e..9ca9bc96 100644 --- a/src/main/registry.ts +++ b/src/main/registry.ts @@ -12,7 +12,9 @@ import { IDisposable, IEnvironmentType, IPythonEnvironment, - IVersionContainer + IPythonEnvResolveError, + IVersionContainer, + PythonEnvResolveErrorType } from './tokens'; import { envPathForPythonPath, @@ -45,6 +47,7 @@ export interface IRegistry { getCurrentPythonEnvironment: () => IPythonEnvironment; getAdditionalPathIncludesForPythonPath: (pythonPath: string) => string; getRequirements: () => Registry.IRequirement[]; + getRequirementsPipInstallCommand: () => string; getEnvironmentInfo(pythonPath: string): Promise; getRunningServerList(): Promise; dispose(): Promise; @@ -53,17 +56,17 @@ export interface IRegistry { } export const SERVER_TOKEN_PREFIX = 'jlab:srvr:'; +const MIN_JLAB_VERSION_REQUIRED = '3.0.0'; export class Registry implements IRegistry, IDisposable { constructor() { - const minJLabVersionRequired = '3.0.0'; - this._requirements = [ { name: 'jupyterlab', moduleName: 'jupyterlab', commands: ['--version'], - versionRange: new semver.Range(`>=${minJLabVersionRequired}`) + versionRange: new semver.Range(`>=${MIN_JLAB_VERSION_REQUIRED}`), + pipCommand: `"jupyterlab>=${MIN_JLAB_VERSION_REQUIRED}"` } ]; @@ -78,18 +81,22 @@ export class Registry implements IRegistry, IDisposable { pythonPath = getBundledPythonPath(); } - const defaultEnv = this._resolveEnvironmentSync(pythonPath); - - if (defaultEnv) { - this._defaultEnv = defaultEnv; - if ( - defaultEnv.type === IEnvironmentType.CondaRoot && - !this._condaRootPath - ) { - // this call overrides user set appData.condaRootPath - // which is probably better for compatibility - this.setCondaRootPath(getEnvironmentPath(defaultEnv)); + try { + const defaultEnv = this._resolveEnvironmentSync(pythonPath); + + if (defaultEnv) { + this._defaultEnv = defaultEnv; + if ( + defaultEnv.type === IEnvironmentType.CondaRoot && + !this._condaRootPath + ) { + // this call overrides user set appData.condaRootPath + // which is probably better for compatibility + this.setCondaRootPath(getEnvironmentPath(defaultEnv)); + } } + } catch (error) { + // } if (!this._condaRootPath && appData.condaRootPath) { @@ -99,9 +106,13 @@ export class Registry implements IRegistry, IDisposable { // set default env from appData.condaRootPath if (!this._defaultEnv) { const pythonPath = pythonPathForEnvPath(appData.condaRootPath, true); - const defaultEnv = this._resolveEnvironmentSync(pythonPath); - if (defaultEnv) { - this._defaultEnv = defaultEnv; + try { + const defaultEnv = this._resolveEnvironmentSync(pythonPath); + if (defaultEnv) { + this._defaultEnv = defaultEnv; + } + } catch (error) { + // } } } @@ -222,17 +233,32 @@ export class Registry implements IRegistry, IDisposable { private _resolveEnvironmentSync(pythonPath: string): IPythonEnvironment { if (!this._pathExistsSync(pythonPath)) { - return; + throw { + type: PythonEnvResolveErrorType.PathNotFound + } as IPythonEnvResolveError; } - const env = this.getEnvironmentInfoSync(pythonPath); + let env: IPythonEnvironment; - if ( - env && - this._environmentSatisfiesRequirements(env, this._requirements) - ) { - return env; + try { + env = this.getEnvironmentInfoSync(pythonPath); + } catch (error) { + log.error( + `Failed to get environment info at path '${pythonPath}'.`, + error + ); + throw { + type: PythonEnvResolveErrorType.ResolveError + } as IPythonEnvResolveError; + } + + if (!this._environmentSatisfiesRequirements(env, this._requirements)) { + throw { + type: PythonEnvResolveErrorType.RequirementsNotSatisfied + } as IPythonEnvResolveError; } + + return env; } /** @@ -330,22 +356,14 @@ export class Registry implements IRegistry, IDisposable { return inUserSetEnvList; } - try { - const env = this._resolveEnvironmentSync(pythonPath); - if (env) { - this._userSetEnvironments.push(env); - this._updateEnvironments(); - this._environmentListUpdated.emit(); - } - - return env; - } catch (error) { - console.error( - `Failed to add the Python environment at: ${pythonPath}`, - error - ); - return; + const env = this._resolveEnvironmentSync(pythonPath); + if (env) { + this._userSetEnvironments.push(env); + this._updateEnvironments(); + this._environmentListUpdated.emit(); } + + return env; } validatePythonEnvironmentAtPath(pythonPath: string): boolean { @@ -386,7 +404,7 @@ export class Registry implements IRegistry, IDisposable { defaultKernel: envInfo.defaultKernel }; } catch (error) { - console.error( + log.error( `Failed to get environment info at path '${pythonPath}'.`, error ); @@ -446,6 +464,16 @@ export class Registry implements IRegistry, IDisposable { return this._requirements; } + getRequirementsPipInstallCommand(): string { + const cmdList = ['pip install']; + + this._requirements.forEach(req => { + cmdList.push(req.pipCommand); + }); + + return cmdList.join(' '); + } + getRunningServerList(): Promise { return new Promise(resolve => { if (this._defaultEnv) { @@ -886,21 +914,6 @@ export class Registry implements IRegistry, IDisposable { }); } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - private _runPythonModuleCommandSync( - pythonPath: string, - moduleName: string, - commands: string[] - ): string { - const totalCommands = ['-m', moduleName].concat(commands); - const runOptions = { - env: { PATH: this.getAdditionalPathIncludesForPythonPath(pythonPath) } - }; - - return this._runCommandSync(pythonPath, totalCommands, runOptions); - } - private async _runCommand( executablePath: string, commands: string[], @@ -971,11 +984,7 @@ export class Registry implements IRegistry, IDisposable { commands: string[], options?: ExecFileOptions ): string { - try { - return execFileSync(executablePath, commands, options).toString(); - } catch (error) { - return 'EXEC:ERROR'; - } + return execFileSync(executablePath, commands, options).toString(); } private _sortEnvironments( @@ -1116,6 +1125,11 @@ export namespace Registry { * The Range of acceptable version produced by the previous commands field */ versionRange: semver.Range; + + /** + * pip install command + */ + pipCommand: string; } export const COMMON_CONDA_LOCATIONS = [ diff --git a/src/main/sessionwindow/sessionwindow.ts b/src/main/sessionwindow/sessionwindow.ts index 80587d2c..e9eb0e35 100644 --- a/src/main/sessionwindow/sessionwindow.ts +++ b/src/main/sessionwindow/sessionwindow.ts @@ -22,7 +22,9 @@ import { TitleBarView } from '../titlebarview/titlebarview'; import { clearSession, DarkThemeBGColor, + envPathForPythonPath, getBundledPythonPath, + getLogFilePath, isDarkTheme, LightThemeBGColor } from '../utils'; @@ -31,7 +33,8 @@ import { IDisposable, IPythonEnvironment, IRect, - IVersionContainer + IVersionContainer, + PythonEnvResolveErrorType } from '../tokens'; import { IRegistry } from '../registry'; import { IApplication } from '../app'; @@ -44,6 +47,7 @@ import { SessionConfig } from '../config/sessionconfig'; import { ISignal, Signal } from '@lumino/signaling'; import { EventTypeMain } from '../eventtypes'; import { EventManager } from '../eventmanager'; +import { runCommandInEnvironment } from '../cli'; export enum ContentViewType { Welcome = 'welcome', @@ -667,6 +671,37 @@ export class SessionWindow implements IDisposable { } ); + this._evm.registerEventHandler( + EventTypeMain.InstallPythonEnvRequirements, + (event, pythonPath: string, installCommand: string) => { + if (event.sender !== this._progressView?.view?.view?.webContents) { + return; + } + + const envPath = envPathForPythonPath(decodeURIComponent(pythonPath)); + + this._showProgressView('Installing required packages'); + + const command = `python -m ${decodeURIComponent(installCommand)}`; + runCommandInEnvironment(envPath, command).then(result => { + if (result) { + this._hideProgressView(); + } else { + this._showProgressView( + 'Failed to install required packages', + `
+ + ` + ); + } + }); + } + ); + + this._evm.registerEventHandler(EventTypeMain.ShowLogs, event => { + shell.openPath(getLogFilePath()); + }); + this._evm.registerEventHandler( EventTypeMain.OpenDroppedFiles, (event, fileOrFolders: string[]) => { @@ -712,14 +747,42 @@ export class SessionWindow implements IDisposable { 'Restarting server using the selected Python enviroment' ); - const env = this._registry.addEnvironment(path); - - if (!env) { + try { + this._registry.addEnvironment(path); + } catch (error) { + let message = `Error! Python environment at '${path}' is not compatible.`; + let requirementInstallCmd = ''; + if ( + error?.type === PythonEnvResolveErrorType.RequirementsNotSatisfied + ) { + requirementInstallCmd = this._registry.getRequirementsPipInstallCommand(); + message = `Error! Required Python packages not found in the selected environment. You can install using '${requirementInstallCmd}' command.`; + } else if (error?.type === PythonEnvResolveErrorType.PathNotFound) { + message = `Error! File not found at '${path}'.`; + } else if (error?.type === PythonEnvResolveErrorType.ResolveError) { + message = `Error! Failed to get environment information at '${path}'.`; + } this._showProgressView( 'Invalid Environment', - `
Error! Python environment at '${path}' is not compatible.
- - `, + `
${message}
+ ${ + error?.type === PythonEnvResolveErrorType.RequirementsNotSatisfied + ? `` + : '' + } + + + `, false ); @@ -1172,23 +1235,23 @@ export class SessionWindow implements IDisposable { pythonPath = this._wsSettings.getValue(SettingType.pythonPath); if (pythonPath) { - const env = this._registry.addEnvironment(pythonPath); - - if (!env) { + try { + this._registry.addEnvironment(pythonPath); + } catch (error) { // reset python path to default this._wsSettings.setValue(SettingType.pythonPath, ''); this._showProgressView( 'Invalid Environment configured for project', `
Error! Python environment at '${pythonPath}' is not compatible.
- ${ - recentSessionIndex !== undefined - ? `` - : '' - } - `, + ${ + recentSessionIndex !== undefined + ? `` + : '' + } + `, false ); diff --git a/src/main/settingsdialog/preload.ts b/src/main/settingsdialog/preload.ts index 9a1136c2..c90b356b 100644 --- a/src/main/settingsdialog/preload.ts +++ b/src/main/settingsdialog/preload.ts @@ -31,6 +31,9 @@ contextBridge.exposeInMainWorld('electronAPI', { checkForUpdates: () => { ipcRenderer.send(EventTypeMain.CheckForUpdates); }, + showLogs: () => { + ipcRenderer.send(EventTypeMain.ShowLogs); + }, launchInstallerDownloadPage: () => { ipcRenderer.send(EventTypeMain.LaunchInstallerDownloadPage); }, diff --git a/src/main/settingsdialog/settingsdialog.ts b/src/main/settingsdialog/settingsdialog.ts index b806513d..da57ffda 100644 --- a/src/main/settingsdialog/settingsdialog.ts +++ b/src/main/settingsdialog/settingsdialog.ts @@ -515,6 +515,7 @@ export class SettingsDialog { Verbose Debug + Show logs @@ -551,6 +552,10 @@ export class SettingsDialog { window.electronAPI.checkForUpdates(); } + function handleShowLogs(el) { + window.electronAPI.showLogs(); + } + function onLogLevelChanged(el) { window.electronAPI.setLogLevel(el.value); } diff --git a/src/main/tokens.ts b/src/main/tokens.ts index b1a456a3..b492eb96 100644 --- a/src/main/tokens.ts +++ b/src/main/tokens.ts @@ -64,6 +64,17 @@ export interface IPythonEnvironment { defaultKernel: string; } +export enum PythonEnvResolveErrorType { + PathNotFound = 'path-not-found', + ResolveError = 'resolve-error', + RequirementsNotSatisfied = 'requirements-not-satisfied' +} + +export interface IPythonEnvResolveError { + type: PythonEnvResolveErrorType; + message?: string; +} + export interface IDisposable { dispose(): Promise; } diff --git a/src/main/utils.ts b/src/main/utils.ts index d3d4e5c1..593759c2 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -510,3 +510,23 @@ export function createUnsignScriptInEnv(envPath: string): string { ' ' )} && ${removeRuntimeFlagCommand} && cd -`; } + +export function getLogFilePath(processType: 'main' | 'renderer' = 'main') { + switch (process.platform) { + case 'win32': + return path.join( + getUserDataDir(), + `\\jupyterlab-desktop\\logs\\${processType}.log` + ); + case 'darwin': + return path.join( + getUserHomeDir(), + `/Library/Logs/jupyterlab-desktop/${processType}.log` + ); + default: + return path.join( + getUserHomeDir(), + `/.config/jupyterlab-desktop/logs/${processType}.log` + ); + } +} From 532213b7190381e59034ab95996ff36ac591b957 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Tue, 3 Oct 2023 10:02:52 -0700 Subject: [PATCH 02/10] fix log path on Windows, log command executions --- src/main/cli.ts | 7 ++++++- src/main/utils.ts | 5 +---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/cli.ts b/src/main/cli.ts index e09bbf0c..abcf06d6 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -457,13 +457,18 @@ export async function runCommandInEnvironment( env: process.env }) : spawn('bash', ['-c', commandScript], { - stdio: 'inherit', env: { ...process.env, BASH_SILENCE_DEPRECATION_WARNING: '1' } }); + if (shell.stdout) { + shell.stdout.on('data', chunk => { + console.debug('>', Buffer.from(chunk).toString()); + }); + } + shell.on('close', code => { if (code !== 0) { console.error('Shell exit with code:', code); diff --git a/src/main/utils.ts b/src/main/utils.ts index 593759c2..70b59e64 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -514,10 +514,7 @@ export function createUnsignScriptInEnv(envPath: string): string { export function getLogFilePath(processType: 'main' | 'renderer' = 'main') { switch (process.platform) { case 'win32': - return path.join( - getUserDataDir(), - `\\jupyterlab-desktop\\logs\\${processType}.log` - ); + return path.join(getUserDataDir(), `\\logs\\${processType}.log`); case 'darwin': return path.join( getUserHomeDir(), From 135bc0a6b402bdabe502c893d504210a85ee104b Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Tue, 3 Oct 2023 22:17:04 -0700 Subject: [PATCH 03/10] copy to clipboard, fix windows install issue --- scripts/copyassets.js | 4 + src/assets/copyable-span.js | 65 +++++++++++++++ src/main/cli.ts | 5 ++ src/main/eventtypes.ts | 3 +- src/main/progressview/preload.ts | 3 + src/main/progressview/progressview.ts | 4 + src/main/registry.ts | 2 +- src/main/sessionwindow/sessionwindow.ts | 101 ++++++++++++++++-------- 8 files changed, 154 insertions(+), 33 deletions(-) create mode 100644 src/assets/copyable-span.js diff --git a/scripts/copyassets.js b/scripts/copyassets.js index 70ba026b..28fc638b 100644 --- a/scripts/copyassets.js +++ b/scripts/copyassets.js @@ -61,6 +61,10 @@ function copyAssests() { path.join(srcDir, 'assets', 'jupyterlab-wordmark.svg'), path.join(dest, '../app-assets', 'jupyterlab-wordmark.svg') ); + fs.copySync( + path.join(srcDir, 'assets', 'copyable-span.js'), + path.join(dest, '../app-assets', 'copyable-span.js') + ); const toolkitPath = path.join( '../node_modules', diff --git a/src/assets/copyable-span.js b/src/assets/copyable-span.js new file mode 100644 index 00000000..7f9604eb --- /dev/null +++ b/src/assets/copyable-span.js @@ -0,0 +1,65 @@ +const template = document.createElement('template'); +template.innerHTML = ` + +
+
+
+
+
+`; + +class CopyableSpan extends HTMLElement { + constructor() { + super(); + + this.attachShadow({ mode: 'open' }); + + this.shadowRoot.appendChild(template.content.cloneNode(true)); + this.shadowRoot.querySelector('.container').onclick = evt => { + window.electronAPI.copyToClipboard(evt.currentTarget.dataset.copied); + }; + } + + static get observedAttributes() { + return ['label', 'title', 'copied']; + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case 'label': + this.shadowRoot.querySelector('.label').innerText = decodeURIComponent( + newValue + ); + break; + case 'title': + this.shadowRoot.querySelector('.container').title = decodeURIComponent( + newValue + ); + break; + case 'copied': + this.shadowRoot.querySelector( + '.container' + ).dataset.copied = decodeURIComponent(newValue); + break; + } + } +} + +window.customElements.define('copyable-span', CopyableSpan); diff --git a/src/main/cli.ts b/src/main/cli.ts index abcf06d6..62055a17 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -468,6 +468,11 @@ export async function runCommandInEnvironment( console.debug('>', Buffer.from(chunk).toString()); }); } + if (shell.stderr) { + shell.stderr.on('data', chunk => { + console.error('>', Buffer.from(chunk).toString()); + }); + } shell.on('close', code => { if (code !== 0) { diff --git a/src/main/eventtypes.ts b/src/main/eventtypes.ts index 72058fc2..5f39aa51 100644 --- a/src/main/eventtypes.ts +++ b/src/main/eventtypes.ts @@ -57,7 +57,8 @@ export enum EventTypeMain { SetCtrlWBehavior = 'set-ctrl-w-behavior', SetAuthDialogResponse = 'set-auth-dialog-response', InstallPythonEnvRequirements = 'install-python-env-requirements', - ShowLogs = 'show-logs' + ShowLogs = 'show-logs', + CopyToClipboard = 'copy-to-clipboard' } // events sent to Renderer process diff --git a/src/main/progressview/preload.ts b/src/main/progressview/preload.ts index 21deea5d..727b6978 100644 --- a/src/main/progressview/preload.ts +++ b/src/main/progressview/preload.ts @@ -34,6 +34,9 @@ contextBridge.exposeInMainWorld('electronAPI', { callback: InstallBundledPythonEnvStatusListener ) => { onInstallBundledPythonEnvStatusListener = callback; + }, + copyToClipboard: (content: string) => { + ipcRenderer.send(EventTypeMain.CopyToClipboard, content); } }); diff --git a/src/main/progressview/progressview.ts b/src/main/progressview/progressview.ts index 465dfb2f..053e015e 100644 --- a/src/main/progressview/progressview.ts +++ b/src/main/progressview/progressview.ts @@ -17,6 +17,9 @@ export class ProgressView { const progressLogo = fs.readFileSync( path.join(__dirname, '../../../app-assets/progress-logo.svg') ); + const copyableSpanSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/copyable-span.js') + ); const template = `
From f3cccb4d431efa4a728dcf82cd2200bd8f0637f4 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Mon, 9 Oct 2023 19:20:58 -0700 Subject: [PATCH 06/10] fix pip install on windows with version pin --- src/main/cli.ts | 2 +- src/main/registry.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/cli.ts b/src/main/cli.ts index 62055a17..fc8f2dbd 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -453,7 +453,7 @@ export async function runCommandInEnvironment( return new Promise((resolve, reject) => { const shell = isWin - ? spawn('cmd', ['/c', commandScript], { + ? spawn('cmd', ['/c', ...commandScript.split(' ')], { env: process.env }) : spawn('bash', ['-c', commandScript], { diff --git a/src/main/registry.ts b/src/main/registry.ts index 3b556d93..ca574963 100644 --- a/src/main/registry.ts +++ b/src/main/registry.ts @@ -66,7 +66,7 @@ export class Registry implements IRegistry, IDisposable { moduleName: 'jupyterlab', commands: ['--version'], versionRange: new semver.Range(`>=${MIN_JLAB_VERSION_REQUIRED}`), - pipCommand: 'jupyterlab' + pipCommand: `jupyterlab>=${MIN_JLAB_VERSION_REQUIRED}` } ]; From 20249e4780f99a6c3caa720ef9915b838176a9cb Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Wed, 11 Oct 2023 12:38:23 -0700 Subject: [PATCH 07/10] support quotes in windows install script --- src/main/cli.ts | 5 +++-- src/main/registry.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/cli.ts b/src/main/cli.ts index fc8f2dbd..08423c6a 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -453,8 +453,9 @@ export async function runCommandInEnvironment( return new Promise((resolve, reject) => { const shell = isWin - ? spawn('cmd', ['/c', ...commandScript.split(' ')], { - env: process.env + ? spawn('cmd', ['/c', commandScript], { + env: process.env, + windowsVerbatimArguments: true }) : spawn('bash', ['-c', commandScript], { env: { diff --git a/src/main/registry.ts b/src/main/registry.ts index ca574963..ad27ae42 100644 --- a/src/main/registry.ts +++ b/src/main/registry.ts @@ -66,7 +66,7 @@ export class Registry implements IRegistry, IDisposable { moduleName: 'jupyterlab', commands: ['--version'], versionRange: new semver.Range(`>=${MIN_JLAB_VERSION_REQUIRED}`), - pipCommand: `jupyterlab>=${MIN_JLAB_VERSION_REQUIRED}` + pipCommand: `"jupyterlab>=${MIN_JLAB_VERSION_REQUIRED}"` } ]; From d61b732cb85b99c97d4dea666733ca9e61b18aab Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sat, 14 Oct 2023 20:13:19 -0700 Subject: [PATCH 08/10] use conda install for installing missing requirements --- src/main/registry.ts | 24 ++++++++++++++++++------ src/main/sessionwindow/sessionwindow.ts | 13 ++++++++----- src/main/utils.ts | 10 +++++++++- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/main/registry.ts b/src/main/registry.ts index ad27ae42..14d89fcd 100644 --- a/src/main/registry.ts +++ b/src/main/registry.ts @@ -23,6 +23,7 @@ import { getEnvironmentPath, getUserHomeDir, isBaseCondaEnv, + isCondaEnv, isPortInUse, pythonPathForEnvPath, versionWithoutSuffix @@ -47,7 +48,7 @@ export interface IRegistry { getCurrentPythonEnvironment: () => IPythonEnvironment; getAdditionalPathIncludesForPythonPath: (pythonPath: string) => string; getRequirements: () => Registry.IRequirement[]; - getRequirementsPipInstallCommand: () => string; + getRequirementsInstallCommand: (envPath: string) => string; getEnvironmentInfo(pythonPath: string): Promise; getRunningServerList(): Promise; dispose(): Promise; @@ -66,7 +67,8 @@ export class Registry implements IRegistry, IDisposable { moduleName: 'jupyterlab', commands: ['--version'], versionRange: new semver.Range(`>=${MIN_JLAB_VERSION_REQUIRED}`), - pipCommand: `"jupyterlab>=${MIN_JLAB_VERSION_REQUIRED}"` + pipCommand: `"jupyterlab>=${MIN_JLAB_VERSION_REQUIRED}"`, + condaCommand: `"jupyterlab>=${MIN_JLAB_VERSION_REQUIRED}"` } ]; @@ -254,8 +256,11 @@ export class Registry implements IRegistry, IDisposable { } if (!this._environmentSatisfiesRequirements(env, this._requirements)) { + const envPath = envPathForPythonPath(pythonPath); log.error( - `Required Python packages not found in the selected environment. You can install using '${this.getRequirementsPipInstallCommand()}'.` + `Required Python packages not found in the selected environment. You can install using '${this.getRequirementsInstallCommand( + envPath + )}'.` ); throw { type: PythonEnvResolveErrorType.RequirementsNotSatisfied @@ -468,11 +473,14 @@ export class Registry implements IRegistry, IDisposable { return this._requirements; } - getRequirementsPipInstallCommand(): string { - const cmdList = ['pip install']; + getRequirementsInstallCommand(envPath: string): string { + const isConda = isCondaEnv(envPath); + const cmdList = [ + isConda ? 'conda install -c conda-forge -y' : 'pip install' + ]; this._requirements.forEach(req => { - cmdList.push(req.pipCommand); + cmdList.push(isConda ? req.condaCommand : req.pipCommand); }); return cmdList.join(' '); @@ -1134,6 +1142,10 @@ export namespace Registry { * pip install command */ pipCommand: string; + /** + * conda install command + */ + condaCommand: string; } export const COMMON_CONDA_LOCATIONS = [ diff --git a/src/main/sessionwindow/sessionwindow.ts b/src/main/sessionwindow/sessionwindow.ts index 41d31649..dc050dbc 100644 --- a/src/main/sessionwindow/sessionwindow.ts +++ b/src/main/sessionwindow/sessionwindow.ts @@ -694,12 +694,13 @@ export class SessionWindow implements IDisposable { } const envPath = envPathForPythonPath(pythonPath); - const installCommand = this._registry.getRequirementsPipInstallCommand(); + const installCommand = this._registry.getRequirementsInstallCommand( + envPath + ); this._showProgressView('Installing required packages'); - const command = `python -m ${installCommand}`; - runCommandInEnvironment(envPath, command).then(result => { + runCommandInEnvironment(envPath, installCommand).then(result => { if (result) { this._showProgressView( 'Finished installing packages', @@ -798,8 +799,9 @@ export class SessionWindow implements IDisposable { if ( error?.type === PythonEnvResolveErrorType.RequirementsNotSatisfied ) { + const envPath = envPathForPythonPath(path); const requirementInstallCmd = encodeURIComponent( - this._registry.getRequirementsPipInstallCommand() + this._registry.getRequirementsInstallCommand(envPath) ); message = `Error! Required Python packages not found in the selected environment. You can install using command in the selected environment.`; } else if (error?.type === PythonEnvResolveErrorType.PathNotFound) { @@ -1291,8 +1293,9 @@ export class SessionWindow implements IDisposable { if ( error?.type === PythonEnvResolveErrorType.RequirementsNotSatisfied ) { + const envPath = envPathForPythonPath(pythonPath); const requirementInstallCmd = encodeURIComponent( - this._registry.getRequirementsPipInstallCommand() + this._registry.getRequirementsInstallCommand(envPath) ); message = `Error! Required packages not found in the Python environment. You can install using command in the selected environment.`; } diff --git a/src/main/utils.ts b/src/main/utils.ts index 70b59e64..c38ea151 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -443,6 +443,12 @@ export function createCommandScriptInEnv( let hasActivate = fs.existsSync(activatePath); const isConda = isCondaEnv(envPath); + // conda commands don't work properly when called from the sub environment. + // instead call using conda from the base environment with -p parameter + const isCondaCommand = isConda && command?.startsWith('conda '); + if (isCondaCommand) { + command = `${command} -p ${envPath}`; + } // conda activate is only available in base conda environments or // conda-packed environments @@ -474,7 +480,9 @@ export function createCommandScriptInEnv( scriptLines.push(`source "${activatePath}"`); if (isConda && isBaseCondaActivate) { scriptLines.push(`source "${condaSourcePath}"`); - scriptLines.push(`conda activate "${envPath}"`); + if (!isCondaCommand) { + scriptLines.push(`conda activate "${envPath}"`); + } } if (command) { scriptLines.push(command); From 5cad6949667c8ee85c3f93d8ab49799c665bd0e3 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sat, 14 Oct 2023 20:26:20 -0700 Subject: [PATCH 09/10] log versions of mismatched packages in requirements --- src/main/registry.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/registry.ts b/src/main/registry.ts index 14d89fcd..e0dc4b6a 100644 --- a/src/main/registry.ts +++ b/src/main/registry.ts @@ -257,8 +257,15 @@ export class Registry implements IRegistry, IDisposable { if (!this._environmentSatisfiesRequirements(env, this._requirements)) { const envPath = envPathForPythonPath(pythonPath); + const versionsFound: string[] = []; + this._requirements.forEach(req => { + versionsFound.push(`${req.name}: ${env.versions[req.name]}`); + }); + log.error( - `Required Python packages not found in the selected environment. You can install using '${this.getRequirementsInstallCommand( + `Required Python packages not found in the environment path "${envPath}". Versions found are: ${versionsFound.join( + ',' + )}. You can install missing packages using '${this.getRequirementsInstallCommand( envPath )}'.` ); From 30aff0c887191eeed32079e69c107466730847fd Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sun, 15 Oct 2023 18:25:52 -0700 Subject: [PATCH 10/10] check if conda env is a base env --- src/main/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/utils.ts b/src/main/utils.ts index c38ea151..014f6ab6 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -446,7 +446,7 @@ export function createCommandScriptInEnv( // conda commands don't work properly when called from the sub environment. // instead call using conda from the base environment with -p parameter const isCondaCommand = isConda && command?.startsWith('conda '); - if (isCondaCommand) { + if (isCondaCommand && !isBaseCondaEnv(envPath)) { command = `${command} -p ${envPath}`; }