Skip to content
Open
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
20 changes: 2 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion src/common/utils/internalVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,18 @@ export function resolveVariables(value: string, project?: Uri, env?: { [key: str
substitutions.set('${pythonProject}', project.fsPath);
}

const workspace = project ? getWorkspaceFolder(project) : undefined;
// Resolve ${workspaceFolder} for a project (workspace) scope. Prefer the workspace folder
// that owns the project URI, but fall back to the single open workspace folder when the
// owning folder can't be determined (e.g. drive-letter casing differences on Windows can
// cause workspace.getWorkspaceFolder() to return undefined). Without this fallback the
// ${workspaceFolder} token would be left unexpanded and downstream path resolution fails.
let workspace = project ? getWorkspaceFolder(project) : undefined;
if (!workspace && project) {
const folders = getWorkspaceFolders();
if (folders && folders.length === 1) {
workspace = folders[0];
}
}
if (workspace) {
substitutions.set('${workspaceFolder}', workspace.uri.fsPath);
}
Expand Down
78 changes: 47 additions & 31 deletions src/features/interpreterSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,38 +131,24 @@ async function resolvePriorityChainCore(
);
}
} else {
const expandedInterpreterPath = resolveVariables(userInterpreterPath, scope);
if (expandedInterpreterPath.includes('${')) {
traceWarn(
`${logPrefix} defaultInterpreterPath '${userInterpreterPath}' contains unresolved variables, falling back to auto-discovery`,
);
const error: SettingResolutionError = {
setting: 'defaultInterpreterPath',
kind: 'pathUnresolvedVariables',
configuredValue: userInterpreterPath,
};
errors.push(error);
} else {
const resolved = await tryResolveInterpreterPath(
nativeFinder,
api,
expandedInterpreterPath,
envManagers,
);
if (resolved) {
traceVerbose(`${logPrefix} Priority 3: Using defaultInterpreterPath: ${userInterpreterPath}`);
return { result: resolved, errors };
}
const error: SettingResolutionError = {
setting: 'defaultInterpreterPath',
kind: 'pathCannotResolve',
configuredValue: userInterpreterPath,
};
errors.push(error);
traceWarn(
`${logPrefix} defaultInterpreterPath '${userInterpreterPath}' unresolvable, falling back to auto-discovery`,
);
// Resolve relative paths against the workspace folder so the native finder doesn't
// resolve them against an unrelated current working directory (which can produce a
// malformed, duplicated path such as <workspace>/<workspace-name>/.venv/...).
const absoluteInterpreterPath = toAbsoluteInterpreterPath(expandedInterpreterPath, scope);
const resolved = await tryResolveInterpreterPath(nativeFinder, api, absoluteInterpreterPath, envManagers);
if (resolved) {
traceVerbose(`${logPrefix} Priority 3: Using defaultInterpreterPath: ${userInterpreterPath}`);
return { result: resolved, errors };
}
const error: SettingResolutionError = {
setting: 'defaultInterpreterPath',
kind: 'pathCannotResolve',
configuredValue: userInterpreterPath,
};
errors.push(error);
traceWarn(
`${logPrefix} defaultInterpreterPath '${userInterpreterPath}' unresolvable, falling back to auto-discovery`,
);
}
}

Expand Down Expand Up @@ -544,6 +530,36 @@ function getProjectSpecificEnvManager(projectManager: PythonProjectManager, scop
return undefined;
}

/**
* Resolve a (variable-expanded) interpreter path to an absolute path.
*
* `python.defaultInterpreterPath` may be configured as a relative path (e.g. `.venv/bin/python`).
* The native finder resolves relative paths against its own working directory, which is unrelated
* to the workspace and can produce malformed paths (including a duplicated workspace segment).
* To avoid this, relative paths are resolved against the workspace folder. The workspace folder is
* looked up from the scope, falling back to the single open workspace folder when needed.
*
* @param interpreterPath - The interpreter path after variable substitution.
* @param scope - The workspace folder URI, or undefined for global scope.
* @returns An absolute interpreter path when possible, otherwise the input unchanged.
*/
function toAbsoluteInterpreterPath(interpreterPath: string, scope: Uri | undefined): string {
if (path.isAbsolute(interpreterPath)) {
return interpreterPath;
}
let workspaceFolder = scope ? getWorkspaceFolder(scope) : undefined;
if (!workspaceFolder) {
const folders = getWorkspaceFolders();
if (folders && folders.length === 1) {
workspaceFolder = folders[0];
}
}
if (workspaceFolder) {
return path.resolve(workspaceFolder.uri.fsPath, interpreterPath);
}
return interpreterPath;
}

/**
* Try to resolve an interpreter path via nativeFinder and return a PriorityChainResult.
* Returns undefined if resolution fails.
Expand Down
44 changes: 44 additions & 0 deletions src/test/common/internalVariables.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,48 @@ suite('Internal Variable substitution', () => {
assert.equal(result, `Some ${item.substitution} text ${item.substitution}`);
});
});

test('Resolve ${workspaceFolder} via single-folder fallback when owning folder is undefined', () => {
// Simulates the Windows scenario where workspace.getWorkspaceFolder() fails to find the
// owning folder (e.g. drive-letter casing). With a single open folder, the token should
// still resolve instead of being left as a literal ${workspaceFolder}.
getWorkspaceFolderStub.returns(undefined);
getWorkspaceFoldersStub.returns([workspaceFolder]);

const result = resolveVariables('${workspaceFolder}/.venv/Scripts/python.exe', project.uri as unknown as Uri);

assert.equal(result, `${workspaceFolder.uri.fsPath}/.venv/Scripts/python.exe`);
assert.ok(!result.includes('${workspaceFolder}'), 'token should be expanded');
});

test('Leaves ${workspaceFolder} unresolved when no project scope is provided', () => {
// Global scope (no project) must not resolve workspace-specific variables.
getWorkspaceFolderStub.returns(undefined);
getWorkspaceFoldersStub.returns([workspaceFolder]);

const result = resolveVariables('${workspaceFolder}/.venv/Scripts/python.exe');

assert.equal(result, '${workspaceFolder}/.venv/Scripts/python.exe');
});

test('Does not use single-folder fallback for ${workspaceFolder} with multiple folders', () => {
const otherFolder = { name: 'workspace2', uri: { fsPath: path.join(home, 'workspace2') } };
getWorkspaceFolderStub.returns(undefined);
getWorkspaceFoldersStub.returns([workspaceFolder, otherFolder]);

const result = resolveVariables('${workspaceFolder}/.venv/Scripts/python.exe', project.uri as unknown as Uri);

assert.equal(result, '${workspaceFolder}/.venv/Scripts/python.exe');
});

test('Leaves ${workspaceFolder} unresolved when no folders are open', () => {
// No owning folder and no open folders at all (getWorkspaceFolders returns undefined):
// the single-folder fallback must not crash and the token stays literal.
getWorkspaceFolderStub.returns(undefined);
getWorkspaceFoldersStub.returns(undefined);

const result = resolveVariables('${workspaceFolder}/.venv/Scripts/python.exe', project.uri as unknown as Uri);

assert.equal(result, '${workspaceFolder}/.venv/Scripts/python.exe');
});
});
Loading
Loading