Skip to content

Commit 2a5c332

Browse files
authored
add positron.r.interpreters.exclude setting to exclude R installation paths (#6472)
### Summary - addresses #6205 - adds a new setting `positron.r.interpreters.exclude` which allows the user to specify R installation binary paths or directories containing R installations that should be excluded from the UI in Positron #### Settings UI <img width="783" alt="image" src="https://github.com/user-attachments/assets/af5a1fb3-6f2a-49a9-b2a8-24ba4023cef0" /> #### Settings JSON ```json "positron.r.interpreters.exclude": [ "/opt/local" ] ``` #### R Language Pack Output ```js ... 2025-02-25 10:11:26.178 [info] User-specified R binaries: [ "/opt/local/R/4.3-arm64/Resources/bin/R" ] ... 2025-02-25 10:11:26.178 [info] Candidate R binary at /opt/local/R/4.3-arm64/Resources/bin/R 2025-02-25 10:11:26.178 [info] User-specified R installation paths to exclude: [ "/opt/local" ] 2025-02-25 10:11:26.178 [info] User has excluded R installation at /opt/local/R/4.3-arm64/Resources/bin/R 2025-02-25 10:11:26.178 [info] R installation discovered: { "usable": false, "supported": true, "reasonDiscovered": [ "User-specified location", "Found in a conventional location for R binaries installed on a server" ], "reasonRejected": "Installation path was excluded via settings", "binpath": "/opt/local/R/4.3-arm64/Resources/bin/R", "homepath": "/Library/Frameworks/R.framework/Versions/4.3-arm64/Resources", "semVersion": { "options": {}, "loose": false, "includePrerelease": false, "raw": "4.3.3", "major": 4, "minor": 3, "patch": 3, "prerelease": [], "build": [], "version": "4.3.3" }, "version": "4.3.3", "arch": "arm64", "current": false, "orthogonal": true } ... 2025-02-25 10:11:26.178 [info] Filtering out /opt/local/R/4.3-arm64/Resources/bin/R, reason: Installation path was excluded via settings. 2025-02-25 10:11:26.178 [warning] Some discovered R installations are unusable by Positron. 2025-02-25 10:11:26.178 [warning] Learn more about R discovery at https://positron.posit.co/r-installations ``` ### Release Notes #### New Features - New setting to exclude R installation paths from the UI (#6205) #### Bug Fixes - N/A ### QA Notes - please test with and without the `positron.r.customBinaries` and `positron.r.customRootFolders` settings which allow "includes" to be specified - see https://positron.posit.co/r-installations.html#customizing-r-discovery for more info on these options - Positron will need to be restarted upon changing the settings so that discovery can re-run with the settings applied - excluding interpreters via `positron.r.interpreters.exclude` will take precedence over includes with `positron.r.customBinaries` or `positron.r.customRootFolders` - Relative paths specified in the options are ignored
1 parent eb508f9 commit 2a5c332

File tree

9 files changed

+150
-29
lines changed

9 files changed

+150
-29
lines changed

extensions/positron-python/src/client/common/helpers.ts

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export function isNotInstalledError(error: Error): boolean {
2121
return errorObj.code === 'ENOENT' || errorObj.code === 127 || isModuleNoInstalledError;
2222
}
2323

24+
// --- Start Positron ---
25+
// Please port any updates to this function to the copy in extensions/positron-r/src/path-utils.ts!
26+
// --- End Positron ---
2427
export function untildify(path: string): string {
2528
return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`);
2629
}

extensions/positron-python/src/client/positron/interpreterSettings.ts

+24-24
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ import { PythonVersion } from '../pythonEnvironments/info/pythonVersion';
1919
import { comparePythonVersionDescending } from '../interpreter/configuration/environmentTypeComparer';
2020

2121
/**
22-
* Gets the list of interpreters that the user has explicitly included in the settings.
22+
* Gets the list of interpreters included in the settings.
2323
* Converts aliased paths to absolute paths. Relative paths are not included.
24-
* @returns List of interpreters that the user has explicitly included in the settings.
24+
* @returns List of interpreters included in the settings.
2525
*/
26-
export function getUserIncludedInterpreters(): string[] {
26+
export function getIncludedInterpreters(): string[] {
2727
const interpretersInclude = getConfiguration('python').get<string[]>(INTERPRETERS_INCLUDE_SETTING_KEY) ?? [];
2828
if (interpretersInclude.length > 0) {
2929
return interpretersInclude
@@ -41,11 +41,11 @@ export function getUserIncludedInterpreters(): string[] {
4141
}
4242

4343
/**
44-
* Gets the list of interpreters that the user has explicitly excluded in the settings.
44+
* Gets the list of interpreters excluded in the settings.
4545
* Converts aliased paths to absolute paths. Relative paths are not included.
46-
* @returns List of interpreters that the user has explicitly excluded in the settings.
46+
* @returns List of interpreters excluded in the settings.
4747
*/
48-
export function getUserExcludedInterpreters(): string[] {
48+
export function getExcludedInterpreters(): string[] {
4949
const interpretersExclude = getConfiguration('python').get<string[]>(INTERPRETERS_EXCLUDE_SETTING_KEY) ?? [];
5050
if (interpretersExclude.length > 0) {
5151
return interpretersExclude
@@ -71,17 +71,17 @@ export function getUserExcludedInterpreters(): string[] {
7171
export function shouldIncludeInterpreter(interpreterPath: string): boolean {
7272
// If the settings exclude the interpreter, exclude it. Excluding an interpreter takes
7373
// precedence over including it, so we return right away if the interpreter is excluded.
74-
const userExcluded = userExcludedInterpreter(interpreterPath);
75-
if (userExcluded === true) {
74+
const excluded = isExcludedInterpreter(interpreterPath);
75+
if (excluded === true) {
7676
traceInfo(
7777
`[shouldIncludeInterpreter] Interpreter ${interpreterPath} excluded via ${INTERPRETERS_EXCLUDE_SETTING_KEY} setting`,
7878
);
7979
return false;
8080
}
8181

8282
// If the settings include the interpreter, include it.
83-
const userIncluded = userIncludedInterpreter(interpreterPath);
84-
if (userIncluded === true) {
83+
const included = isIncludedInterpreter(interpreterPath);
84+
if (included === true) {
8585
traceInfo(
8686
`[shouldIncludeInterpreter] Interpreter ${interpreterPath} included via ${INTERPRETERS_INCLUDE_SETTING_KEY} setting`,
8787
);
@@ -94,13 +94,13 @@ export function shouldIncludeInterpreter(interpreterPath: string): boolean {
9494
}
9595

9696
/**
97-
* Checks if an interpreter path is included in the user's settings.
97+
* Checks if an interpreter path is included in the settings.
9898
* @param interpreterPath The interpreter path to check
99-
* @returns True if the interpreter is included in the user's settings, false if it is not included
100-
* in the user's settings, and undefined if the user has not specified any included interpreters.
99+
* @returns True if the interpreter is included in the settings, false if it is not included
100+
* in the settings, and undefined if included interpreters have not been specified.
101101
*/
102-
function userIncludedInterpreter(interpreterPath: string): boolean | undefined {
103-
const interpretersInclude = getUserIncludedInterpreters();
102+
function isIncludedInterpreter(interpreterPath: string): boolean | undefined {
103+
const interpretersInclude = getIncludedInterpreters();
104104
if (interpretersInclude.length === 0) {
105105
return undefined;
106106
}
@@ -110,13 +110,13 @@ function userIncludedInterpreter(interpreterPath: string): boolean | undefined {
110110
}
111111

112112
/**
113-
* Checks if an interpreter path is excluded in the user's settings.
113+
* Checks if an interpreter path is excluded in the settings.
114114
* @param interpreterPath The interpreter path to check
115-
* @returns True if the interpreter is excluded in the user's settings, false if it is not excluded
116-
* in the user's settings, and undefined if the user has not specified any excluded interpreters.
115+
* @returns True if the interpreter is excluded in the settings, false if it is not excluded
116+
* in the settings, and undefined if excluded interpreters have not been specified.
117117
*/
118-
function userExcludedInterpreter(interpreterPath: string): boolean | undefined {
119-
const interpretersExclude = getUserExcludedInterpreters();
118+
function isExcludedInterpreter(interpreterPath: string): boolean | undefined {
119+
const interpretersExclude = getExcludedInterpreters();
120120
if (interpretersExclude.length === 0) {
121121
return undefined;
122122
}
@@ -165,8 +165,8 @@ export function printInterpreterDebugInfo(interpreters: PythonEnvironment[]): vo
165165
// Construct interpreter setting information
166166
const interpreterSettingInfo = {
167167
defaultInterpreterPath: getConfiguration('python').get<string>('defaultInterpreterPath'),
168-
'interpreters.include': getUserIncludedInterpreters(),
169-
'interpreters.exclude': getUserExcludedInterpreters(),
168+
'interpreters.include': getIncludedInterpreters(),
169+
'interpreters.exclude': getExcludedInterpreters(),
170170
};
171171

172172
// Construct debug information about each interpreter
@@ -193,8 +193,8 @@ export function printInterpreterDebugInfo(interpreters: PythonEnvironment[]): vo
193193
},
194194
enablementInfo: {
195195
visibleInUI: shouldIncludeInterpreter(interpreter.path),
196-
includedInSettings: userIncludedInterpreter(interpreter.path),
197-
excludedInSettings: userExcludedInterpreter(interpreter.path),
196+
includedInSettings: isIncludedInterpreter(interpreter.path),
197+
excludedInSettings: isExcludedInterpreter(interpreter.path),
198198
},
199199
}),
200200
);

extensions/positron-python/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { untildify } from '../../../../common/helpers';
2828
import { traceError } from '../../../../logging';
2929

3030
// --- Start Positron ---
31-
import { getUserIncludedInterpreters } from '../../../../positron/interpreterSettings';
31+
import { getIncludedInterpreters } from '../../../../positron/interpreterSettings';
3232
import { traceVerbose } from '../../../../logging';
3333
// --- End Positron ---
3434

@@ -474,7 +474,7 @@ function getAdditionalEnvDirs(): string[] {
474474
}
475475

476476
// Add user-specified Python search directories.
477-
const userIncludedDirs = getUserIncludedInterpreters();
477+
const userIncludedDirs = getIncludedInterpreters();
478478
additionalDirs.push(...userIncludedDirs);
479479

480480
// Return the list of additional directories.

extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/userSpecifiedEnvLocator.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { findInterpretersInDir, looksLikeBasicVirtualPython } from '../../../com
2323
import '../../../../common/extensions';
2424
import { traceError, traceInfo, traceVerbose, traceWarn } from '../../../../logging';
2525
import { StopWatch } from '../../../../common/utils/stopWatch';
26-
import { getUserIncludedInterpreters } from '../../../../positron/interpreterSettings';
26+
import { getIncludedInterpreters } from '../../../../positron/interpreterSettings';
2727
import { isParentPath } from '../../../common/externalDependencies';
2828

2929
/**
@@ -35,7 +35,7 @@ const DEFAULT_SEARCH_DEPTH = 2;
3535
* Gets all user-specified directories to look for environments.
3636
*/
3737
async function getUserSpecifiedEnvDirs(): Promise<string[]> {
38-
const envDirs = getUserIncludedInterpreters();
38+
const envDirs = getIncludedInterpreters();
3939
return [OSType.Windows, OSType.OSX].includes(getOSType()) ? uniqBy(envDirs, toLower) : uniq(envDirs);
4040
}
4141

extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts

+9
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export function readFileSync(filePath: string): string {
7373
* @param filePath File path to check for
7474
* @param parentPath The potential parent path to check for
7575
*/
76+
// --- Start Positron ---
77+
// Please port any updates to this function to the copy in extensions/positron-r/src/path-utils.ts!
78+
// --- End Positron ---
7679
export function isParentPath(filePath: string, parentPath: string): boolean {
7780
if (!parentPath.endsWith(path.sep)) {
7881
parentPath += path.sep;
@@ -96,10 +99,16 @@ export function resolvePath(filename: string): string {
9699
return path.resolve(filename);
97100
}
98101

102+
// --- Start Positron ---
103+
// Please port any updates to this function to the copy in extensions/positron-r/src/path-utils.ts!
104+
// --- End Positron ---
99105
export function normCasePath(filePath: string): string {
100106
return getOSType() === OSType.Windows ? path.normalize(filePath).toUpperCase() : path.normalize(filePath);
101107
}
102108

109+
// --- Start Positron ---
110+
// Please port any updates to this function to the copy in extensions/positron-r/src/path-utils.ts!
111+
// --- End Positron ---
103112
export function arePathsSame(path1: string, path2: string): boolean {
104113
return normCasePath(path1) === normCasePath(path2);
105114
}

extensions/positron-r/package.json

+9
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,15 @@
208208
"default": [],
209209
"markdownDescription": "%r.configuration.customBinaries.markdownDescription%"
210210
},
211+
"positron.r.interpreters.exclude": {
212+
"scope": "window",
213+
"type": "array",
214+
"items": {
215+
"type": "string"
216+
},
217+
"default": [],
218+
"markdownDescription": "%r.configuration.interpreters.exclude.markdownDescription%"
219+
},
211220
"positron.r.kernel.path": {
212221
"scope": "window",
213222
"type": "string",

extensions/positron-r/package.nls.json

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"r.configuration.title-dev": "Advanced",
3636
"r.configuration.customRootFolders.markdownDescription": "List of additional folders to search for R installations. These folders are searched after and in the same way as the default folder for your operating system (e.g. `C:/Program Files/R` on Windows).",
3737
"r.configuration.customBinaries.markdownDescription": "List of additional R binaries. If you want to use an R installation that is not automatically discovered, provide the path to its binary here. For example, on Windows this might look like `C:/some/unusual/location/R-4.4.1/bin/x64/R.exe`.",
38+
"r.configuration.interpreters.exclude.markdownDescription": "List of absolute paths to R binaries or folders containing R binaries to exclude from the available R installations. These interpreters will not be displayed in the Positron UI.\n\nExample: On Linux or Mac, add `/custom/location/R/4.3.0/bin/R` to exclude the specific installation, or `/custom/location` to exclude any R installations within the directory.\n\nRequires a restart to take effect.",
3839
"r.configuration.kernelPath.description": "Path on disk to the ARK kernel executable; use this to override the default (embedded) kernel. Note that this is not the path to R.",
3940
"r.configuration.tracing.description": "Traces the communication between VS Code and the language server",
4041
"r.configuration.tracing.off.description": "No tracing.",
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
3+
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as path from 'path';
7+
import * as os from 'os';
8+
9+
/**
10+
* Returns true if given file path exists within the given parent directory, false otherwise.
11+
* Copied from the function of the same name in extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts.
12+
* @param filePath File path to check for
13+
* @param parentPath The potential parent path to check for
14+
*/
15+
export function isParentPath(filePath: string, parentPath: string): boolean {
16+
if (!parentPath.endsWith(path.sep)) {
17+
parentPath += path.sep;
18+
}
19+
if (!filePath.endsWith(path.sep)) {
20+
filePath += path.sep;
21+
}
22+
return normCasePath(filePath).startsWith(normCasePath(parentPath));
23+
}
24+
25+
/**
26+
* Adapted from the function of the same name in extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts.
27+
*/
28+
export function normCasePath(filePath: string): string {
29+
return os.platform() === 'win32' ? path.normalize(filePath).toUpperCase() : path.normalize(filePath);
30+
}
31+
32+
/**
33+
* Copied from the function of the same name in extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts.
34+
*/
35+
export function arePathsSame(path1: string, path2: string): boolean {
36+
return normCasePath(path1) === normCasePath(path2);
37+
}
38+
39+
/**
40+
* Copied from the function of the same name in extensions/positron-python/src/client/common/helpers.ts.
41+
*/
42+
export function untildify(path: string): string {
43+
return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`);
44+
}

extensions/positron-r/src/r-installation.ts

+56-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import * as semver from 'semver';
77
import * as path from 'path';
88
import * as fs from 'fs';
9+
import * as vscode from 'vscode';
910
import { extractValue, readLines, removeSurroundingQuotes } from './util';
1011
import { LOGGER } from './extension';
1112
import { MINIMUM_R_VERSION } from './constants';
13+
import { arePathsSame, isParentPath, untildify } from './path-utils';
1214

1315
/**
1416
* Extra metadata included in the LanguageRuntimeMetadata for R installations.
@@ -56,7 +58,8 @@ export enum ReasonDiscovered {
5658
export enum ReasonRejected {
5759
invalid = "invalid",
5860
unsupported = "unsupported",
59-
nonOrthogonal = "nonOrthogonal"
61+
nonOrthogonal = "nonOrthogonal",
62+
excluded = "excluded",
6063
}
6164

6265
export function friendlyReason(reason: ReasonDiscovered | ReasonRejected | null): string {
@@ -85,6 +88,8 @@ export function friendlyReason(reason: ReasonDiscovered | ReasonRejected | null)
8588
return `Unsupported version, i.e. version is less than ${MINIMUM_R_VERSION}`;
8689
case ReasonRejected.nonOrthogonal:
8790
return 'Non-orthogonal installation that is also not the current version';
91+
case ReasonRejected.excluded:
92+
return 'Installation path was excluded via settings';
8893
}
8994
}
9095

@@ -191,6 +196,14 @@ export class RInstallation {
191196
this.usable = this.current || this.orthogonal;
192197
if (!this.usable) {
193198
this.reasonRejected = ReasonRejected.nonOrthogonal;
199+
} else {
200+
// Check if this installation has been excluded via settings
201+
const excluded = isExcludedInstallation(this.binpath);
202+
if (excluded) {
203+
LOGGER.info(`R installation excluded via settings: ${this.binpath}`);
204+
this.reasonRejected = ReasonRejected.excluded;
205+
this.usable = false;
206+
}
194207
}
195208
} else {
196209
this.reasonRejected = ReasonRejected.unsupported;
@@ -283,3 +296,45 @@ function getRHomePathWindows(binpath: string): string | undefined {
283296
}
284297

285298
}
299+
300+
/**
301+
* Gets the list of R installations excluded via settings.
302+
* Converts aliased paths to absolute paths. Relative paths are ignored.
303+
* @returns List of installation paths to exclude.
304+
*/
305+
function getExcludedInstallations(): string[] {
306+
const config = vscode.workspace.getConfiguration('positron.r');
307+
const interpretersExclude = config.get<string[]>('interpreters.exclude') ?? [];
308+
if (interpretersExclude.length > 0) {
309+
const excludedPaths = interpretersExclude
310+
.map((item) => untildify(item))
311+
.filter((item) => {
312+
if (path.isAbsolute(item)) {
313+
return true;
314+
}
315+
LOGGER.info(`R installation path to exclude ${item} is not absolute...ignoring`);
316+
return false;
317+
});
318+
const formattedPaths = JSON.stringify(excludedPaths, null, 2);
319+
LOGGER.info(` R installation paths to exclude:\n${formattedPaths}`);
320+
return excludedPaths;
321+
}
322+
LOGGER.debug('No installation paths specified to exclude via settings');
323+
return [];
324+
}
325+
326+
/**
327+
* Checks if the given binary path is excluded via settings.
328+
* @param binpath The binary path to check
329+
* @returns True if the binary path is excluded, false if it is not excluded, and undefined if the
330+
* no exclusions have been specified.
331+
*/
332+
function isExcludedInstallation(binpath: string): boolean | undefined {
333+
const excludedInstallations = getExcludedInstallations();
334+
if (excludedInstallations.length === 0) {
335+
return undefined;
336+
}
337+
return excludedInstallations.some(
338+
excluded => isParentPath(binpath, excluded) || arePathsSame(binpath, excluded)
339+
);
340+
}

0 commit comments

Comments
 (0)