diff --git a/src/managers/pyenv/pyenvUtils.ts b/src/managers/pyenv/pyenvUtils.ts index 8ab51fb0..8dde2213 100644 --- a/src/managers/pyenv/pyenvUtils.ts +++ b/src/managers/pyenv/pyenvUtils.ts @@ -23,6 +23,21 @@ import { } from '../common/nativePythonFinder'; import { shortVersion, sortEnvironments } from '../common/utils'; +/** + * Returns the pyenv root directory from the pyenv executable path. + * Prefers `PYENV_ROOT` env var when set, otherwise goes up 2 levels from the binary. + * On POSIX, pyenv binary is at `/bin/pyenv` (e.g. `~/.pyenv/bin/pyenv`). + * On Windows, pyenv-win binary is at `/bin/pyenv.bat` (e.g. `~/.pyenv/pyenv-win/bin/pyenv.bat`, + * where `` is `~/.pyenv/pyenv-win`). + */ +export function getPyenvDir(pyenv: string): string { + const pyenvRoot = process.env.PYENV_ROOT; + if (pyenvRoot) { + return pyenvRoot; + } + return path.dirname(path.dirname(pyenv)); +} + async function findPyenv(): Promise { try { return await which('pyenv'); @@ -174,8 +189,9 @@ function nativeToPythonEnv( return undefined; } - const versionsPath = normalizePath(path.join(path.dirname(path.dirname(pyenv)), 'versions')); - const envsPaths = normalizePath(path.join(path.dirname(versionsPath), 'envs')); + const pyenvDir = getPyenvDir(pyenv); + const versionsPath = normalizePath(path.join(pyenvDir, 'versions')); + const envsPaths = normalizePath(path.join(pyenvDir, 'envs')); let group = undefined; const normPrefix = normalizePath(info.prefix); if (normPrefix.startsWith(versionsPath)) { diff --git a/src/test/managers/pyenv/pyenvUtils.unit.test.ts b/src/test/managers/pyenv/pyenvUtils.unit.test.ts new file mode 100644 index 00000000..0f7024d7 --- /dev/null +++ b/src/test/managers/pyenv/pyenvUtils.unit.test.ts @@ -0,0 +1,44 @@ +import assert from 'node:assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { getPyenvDir } from '../../../managers/pyenv/pyenvUtils'; + +suite('pyenvUtils - getPyenvDir', () => { + let originalPyenvRoot: string | undefined; + + setup(() => { + originalPyenvRoot = process.env.PYENV_ROOT; + delete process.env.PYENV_ROOT; + }); + + teardown(() => { + sinon.restore(); + if (originalPyenvRoot !== undefined) { + process.env.PYENV_ROOT = originalPyenvRoot; + } else { + delete process.env.PYENV_ROOT; + } + }); + + test('should use PYENV_ROOT when set', () => { + const pyenvRoot = path.join(path.sep, 'custom', 'pyenv', 'root'); + process.env.PYENV_ROOT = pyenvRoot; + const pyenvBin = path.join(path.sep, 'other', 'bin', 'pyenv'); + const result = getPyenvDir(pyenvBin); + assert.strictEqual(result, pyenvRoot); + }); + + test('should go up 2 levels on POSIX when PYENV_ROOT is not set (bin/pyenv -> pyenv root)', () => { + // e.g. /home/user/.pyenv/bin/pyenv + const pyenvBin = path.join(path.sep, 'home', 'user', '.pyenv', 'bin', 'pyenv'); + const result = getPyenvDir(pyenvBin); + assert.strictEqual(result, path.join(path.sep, 'home', 'user', '.pyenv')); + }); + + test('should go up 2 levels on Windows when PYENV_ROOT is not set (pyenv-win/bin/pyenv.bat -> pyenv-win)', () => { + // e.g. C:\Users\user\.pyenv\pyenv-win\bin\pyenv.bat -> C:\Users\user\.pyenv\pyenv-win + const pyenvBin = path.join('C:', 'Users', 'user', '.pyenv', 'pyenv-win', 'bin', 'pyenv.bat'); + const result = getPyenvDir(pyenvBin); + assert.strictEqual(result, path.join('C:', 'Users', 'user', '.pyenv', 'pyenv-win')); + }); +});