Skip to content

Commit 0b9d327

Browse files
authored
Add platform-specific default paths for Poetry cache and virtualenvs (#1218)
Fixes #1182 Fixes #1184
1 parent 33e8098 commit 0b9d327

File tree

5 files changed

+452
-29
lines changed

5 files changed

+452
-29
lines changed

src/common/utils/platformUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
export function isWindows(): boolean {
22
return process.platform === 'win32';
33
}
4+
5+
export function isMac(): boolean {
6+
return process.platform === 'darwin';
7+
}

src/managers/poetry/poetryUtils.ts

Lines changed: 104 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { execProcess } from '../../common/childProcess.apis';
77
import { ENVS_EXTENSION_ID } from '../../common/constants';
88
import { traceError, traceInfo } from '../../common/logging';
99
import { getWorkspacePersistentState } from '../../common/persistentState';
10-
import { getUserHomeDir, untildify } from '../../common/utils/pathUtils';
11-
import { isWindows } from '../../common/utils/platformUtils';
10+
import { getUserHomeDir, normalizePath, untildify } from '../../common/utils/pathUtils';
11+
import { isMac, isWindows } from '../../common/utils/platformUtils';
1212
import { getSettingWorkspaceScope } from '../../features/settings/settingHelpers';
1313
import {
1414
isNativeEnvInfo,
@@ -214,14 +214,14 @@ export async function getPoetryVirtualenvsPath(poetryExe?: string): Promise<stri
214214
if (stdout) {
215215
const venvPath = stdout.trim();
216216
// Poetry might return the path with placeholders like {cache-dir}
217-
// If it doesn't start with / or C:\ etc., assume it's using default
218-
if (!path.isAbsolute(venvPath) || venvPath.includes('{')) {
219-
const home = getUserHomeDir();
220-
if (home) {
221-
poetryVirtualenvsPath = path.join(home, '.cache', 'pypoetry', 'virtualenvs');
222-
}
223-
} else {
217+
// Resolve the placeholder if present
218+
if (venvPath.includes('{cache-dir}')) {
219+
poetryVirtualenvsPath = await resolveVirtualenvsPath(poetry, venvPath);
220+
} else if (path.isAbsolute(venvPath)) {
224221
poetryVirtualenvsPath = venvPath;
222+
} else {
223+
// Not an absolute path and no placeholder, use platform-specific default
224+
poetryVirtualenvsPath = getDefaultPoetryVirtualenvsPath();
225225
}
226226

227227
if (poetryVirtualenvsPath) {
@@ -234,17 +234,99 @@ export async function getPoetryVirtualenvsPath(poetryExe?: string): Promise<stri
234234
}
235235
}
236236

237-
// Fallback to default location
238-
const home = getUserHomeDir();
239-
if (home) {
240-
poetryVirtualenvsPath = path.join(home, '.cache', 'pypoetry', 'virtualenvs');
237+
// Fallback to platform-specific default location
238+
poetryVirtualenvsPath = getDefaultPoetryVirtualenvsPath();
239+
if (poetryVirtualenvsPath) {
241240
await state.set(POETRY_VIRTUALENVS_PATH_KEY, poetryVirtualenvsPath);
242241
return poetryVirtualenvsPath;
243242
}
244243

245244
return undefined;
246245
}
247246

247+
/**
248+
* Returns the default Poetry cache directory based on the current platform.
249+
* - Windows: %LOCALAPPDATA%\pypoetry\Cache or %APPDATA%\pypoetry\Cache
250+
* - macOS: ~/Library/Caches/pypoetry
251+
* - Linux: ~/.cache/pypoetry
252+
*/
253+
export function getDefaultPoetryCacheDir(): string | undefined {
254+
if (isWindows()) {
255+
const localAppData = process.env.LOCALAPPDATA;
256+
if (localAppData) {
257+
return path.join(localAppData, 'pypoetry', 'Cache');
258+
}
259+
const appData = process.env.APPDATA;
260+
if (appData) {
261+
return path.join(appData, 'pypoetry', 'Cache');
262+
}
263+
return undefined;
264+
}
265+
266+
const home = getUserHomeDir();
267+
if (!home) {
268+
return undefined;
269+
}
270+
271+
if (isMac()) {
272+
return path.join(home, 'Library', 'Caches', 'pypoetry');
273+
}
274+
275+
// Linux default
276+
return path.join(home, '.cache', 'pypoetry');
277+
}
278+
279+
/**
280+
* Returns the default Poetry virtualenvs path based on the current platform.
281+
* - Windows: %LOCALAPPDATA%\pypoetry\Cache\virtualenvs or %APPDATA%\pypoetry\Cache\virtualenvs
282+
* - macOS: ~/Library/Caches/pypoetry/virtualenvs
283+
* - Linux: ~/.cache/pypoetry/virtualenvs
284+
*/
285+
export function getDefaultPoetryVirtualenvsPath(): string | undefined {
286+
const cacheDir = getDefaultPoetryCacheDir();
287+
if (cacheDir) {
288+
return path.join(cacheDir, 'virtualenvs');
289+
}
290+
return undefined;
291+
}
292+
293+
/**
294+
* Resolves the {cache-dir} placeholder in a Poetry virtualenvs path.
295+
* First tries to query Poetry's cache-dir config, then falls back to platform-specific default.
296+
* @param poetry Path to the poetry executable
297+
* @param virtualenvsPath The path possibly containing {cache-dir} placeholder
298+
* @returns The resolved path, or undefined if the placeholder cannot be resolved
299+
*/
300+
async function resolveVirtualenvsPath(poetry: string, virtualenvsPath: string): Promise<string | undefined> {
301+
if (!virtualenvsPath.includes('{cache-dir}')) {
302+
return virtualenvsPath;
303+
}
304+
305+
// Try to get the actual cache-dir from Poetry
306+
try {
307+
const { stdout } = await execProcess(`"${poetry}" config cache-dir`);
308+
if (stdout) {
309+
const cacheDir = stdout.trim();
310+
if (cacheDir && path.isAbsolute(cacheDir)) {
311+
const resolved = virtualenvsPath.replace('{cache-dir}', cacheDir);
312+
return path.normalize(resolved);
313+
}
314+
}
315+
} catch (e) {
316+
traceError('Error getting Poetry cache-dir config', e);
317+
}
318+
319+
// Fall back to platform-specific default cache dir
320+
const defaultCacheDir = getDefaultPoetryCacheDir();
321+
if (defaultCacheDir) {
322+
const resolved = virtualenvsPath.replace('{cache-dir}', defaultCacheDir);
323+
return path.normalize(resolved);
324+
}
325+
326+
// Cannot resolve the placeholder - return undefined instead of unresolved path
327+
return undefined;
328+
}
329+
248330
export async function getPoetryVersion(poetry: string): Promise<string | undefined> {
249331
try {
250332
const { stdout } = await execProcess(`"${poetry}" --version`);
@@ -274,8 +356,8 @@ export async function nativeToPythonEnv(
274356
const displayName = info.displayName || `poetry (${sv})`;
275357

276358
// Check if this is a global Poetry virtualenv by checking if it's in Poetry's virtualenvs directory
277-
// We need to use path.normalize() to ensure consistent path format comparison
278-
const normalizedPrefix = path.normalize(info.prefix);
359+
// We use normalizePath() for case-insensitive path comparison on Windows
360+
const normalizedPrefix = normalizePath(info.prefix);
279361

280362
// Determine if the environment is in Poetry's global virtualenvs directory
281363
let isGlobalPoetryEnv = false;
@@ -284,19 +366,17 @@ export async function nativeToPythonEnv(
284366
if (!isPoetryVirtualenvsInProject() || !info.project) {
285367
const virtualenvsPath = poetryVirtualenvsPath; // Use the cached value if available
286368
if (virtualenvsPath) {
287-
const normalizedVirtualenvsPath = path.normalize(virtualenvsPath);
369+
const normalizedVirtualenvsPath = normalizePath(virtualenvsPath);
288370
isGlobalPoetryEnv = normalizedPrefix.startsWith(normalizedVirtualenvsPath);
289371
} else {
290-
// Fall back to checking the default location if we haven't cached the path yet
291-
const homeDir = getUserHomeDir();
292-
if (homeDir) {
293-
const defaultPath = path.normalize(path.join(homeDir, '.cache', 'pypoetry', 'virtualenvs'));
294-
isGlobalPoetryEnv = normalizedPrefix.startsWith(defaultPath);
372+
// Fall back to checking the platform-specific default location if we haven't cached the path yet
373+
const defaultPath = getDefaultPoetryVirtualenvsPath();
374+
if (defaultPath) {
375+
const normalizedDefaultPath = normalizePath(defaultPath);
376+
isGlobalPoetryEnv = normalizedPrefix.startsWith(normalizedDefaultPath);
295377

296378
// Try to get the actual path asynchronously for next time
297-
getPoetryVirtualenvsPath(_poetry).catch((e) =>
298-
traceError(`Error getting Poetry virtualenvs path: ${e}`),
299-
);
379+
getPoetryVirtualenvsPath(_poetry).catch((e) => traceError('Error getting Poetry virtualenvs path', e));
300380
}
301381
}
302382
}

src/test/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function isMultiRootTest(): boolean {
2626
return false;
2727
}
2828
try {
29-
// eslint-disable-next-line @typescript-eslint/no-require-imports
29+
3030
const vscode = require('vscode');
3131
return Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 1;
3232
} catch {

src/test/features/projectManager.initialize.unit.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,8 @@ suite('Project Manager Initialization - Settings Preservation', () => {
310310
test('adding a workspace folder should NOT write project settings', async () => {
311311
const mockConfig = new MockWorkspaceConfiguration();
312312
(mockConfig as any).get = <T>(key: string, defaultValue?: T): T | undefined => {
313-
if (key === 'pythonProjects') return [] as unknown as T;
314-
if (key === 'defaultEnvManager') return 'ms-python.python:venv' as T;
313+
if (key === 'pythonProjects') {return [] as unknown as T;}
314+
if (key === 'defaultEnvManager') {return 'ms-python.python:venv' as T;}
315315
return defaultValue;
316316
};
317317
mockConfig.update = () => Promise.resolve();
@@ -347,8 +347,8 @@ suite('Project Manager Initialization - Settings Preservation', () => {
347347
test('removing a workspace folder should NOT write additional settings', async () => {
348348
const mockConfig = new MockWorkspaceConfiguration();
349349
(mockConfig as any).get = <T>(key: string, defaultValue?: T): T | undefined => {
350-
if (key === 'pythonProjects') return [] as unknown as T;
351-
if (key === 'defaultEnvManager') return 'ms-python.python:venv' as T;
350+
if (key === 'pythonProjects') {return [] as unknown as T;}
351+
if (key === 'defaultEnvManager') {return 'ms-python.python:venv' as T;}
352352
return defaultValue;
353353
};
354354
mockConfig.update = () => Promise.resolve();

0 commit comments

Comments
 (0)