Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add positron.r.interpreters.exclude setting to exclude R installation paths #6472

Merged
merged 6 commits into from
Feb 26, 2025
Merged
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
3 changes: 3 additions & 0 deletions extensions/positron-python/src/client/common/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export function isNotInstalledError(error: Error): boolean {
return errorObj.code === 'ENOENT' || errorObj.code === 127 || isModuleNoInstalledError;
}

// --- Start Positron ---
// Please port any updates to this function to the copy in extensions/positron-r/src/path-utils.ts!
// --- End Positron ---
export function untildify(path: string): string {
return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ import { PythonVersion } from '../pythonEnvironments/info/pythonVersion';
import { comparePythonVersionDescending } from '../interpreter/configuration/environmentTypeComparer';

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

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

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

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

/**
* Checks if an interpreter path is excluded in the user's settings.
* Checks if an interpreter path is excluded in the settings.
* @param interpreterPath The interpreter path to check
* @returns True if the interpreter is excluded in the user's settings, false if it is not excluded
* in the user's settings, and undefined if the user has not specified any excluded interpreters.
* @returns True if the interpreter is excluded in the settings, false if it is not excluded
* in the settings, and undefined if excluded interpreters have not been specified.
*/
function userExcludedInterpreter(interpreterPath: string): boolean | undefined {
const interpretersExclude = getUserExcludedInterpreters();
function isExcludedInterpreter(interpreterPath: string): boolean | undefined {
const interpretersExclude = getExcludedInterpreters();
if (interpretersExclude.length === 0) {
return undefined;
}
Expand Down Expand Up @@ -165,8 +165,8 @@ export function printInterpreterDebugInfo(interpreters: PythonEnvironment[]): vo
// Construct interpreter setting information
const interpreterSettingInfo = {
defaultInterpreterPath: getConfiguration('python').get<string>('defaultInterpreterPath'),
'interpreters.include': getUserIncludedInterpreters(),
'interpreters.exclude': getUserExcludedInterpreters(),
'interpreters.include': getIncludedInterpreters(),
'interpreters.exclude': getExcludedInterpreters(),
};

// Construct debug information about each interpreter
Expand All @@ -193,8 +193,8 @@ export function printInterpreterDebugInfo(interpreters: PythonEnvironment[]): vo
},
enablementInfo: {
visibleInUI: shouldIncludeInterpreter(interpreter.path),
includedInSettings: userIncludedInterpreter(interpreter.path),
excludedInSettings: userExcludedInterpreter(interpreter.path),
includedInSettings: isIncludedInterpreter(interpreter.path),
excludedInSettings: isExcludedInterpreter(interpreter.path),
},
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { untildify } from '../../../../common/helpers';
import { traceError } from '../../../../logging';

// --- Start Positron ---
import { getUserIncludedInterpreters } from '../../../../positron/interpreterSettings';
import { getIncludedInterpreters } from '../../../../positron/interpreterSettings';
import { traceVerbose } from '../../../../logging';
// --- End Positron ---

Expand Down Expand Up @@ -474,7 +474,7 @@ function getAdditionalEnvDirs(): string[] {
}

// Add user-specified Python search directories.
const userIncludedDirs = getUserIncludedInterpreters();
const userIncludedDirs = getIncludedInterpreters();
additionalDirs.push(...userIncludedDirs);

// Return the list of additional directories.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { findInterpretersInDir, looksLikeBasicVirtualPython } from '../../../com
import '../../../../common/extensions';
import { traceError, traceInfo, traceVerbose, traceWarn } from '../../../../logging';
import { StopWatch } from '../../../../common/utils/stopWatch';
import { getUserIncludedInterpreters } from '../../../../positron/interpreterSettings';
import { getIncludedInterpreters } from '../../../../positron/interpreterSettings';
import { isParentPath } from '../../../common/externalDependencies';

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export function readFileSync(filePath: string): string {
* @param filePath File path to check for
* @param parentPath The potential parent path to check for
*/
// --- Start Positron ---
// Please port any updates to this function to the copy in extensions/positron-r/src/path-utils.ts!
// --- End Positron ---
export function isParentPath(filePath: string, parentPath: string): boolean {
if (!parentPath.endsWith(path.sep)) {
parentPath += path.sep;
Expand All @@ -96,10 +99,16 @@ export function resolvePath(filename: string): string {
return path.resolve(filename);
}

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

// --- Start Positron ---
// Please port any updates to this function to the copy in extensions/positron-r/src/path-utils.ts!
// --- End Positron ---
export function arePathsSame(path1: string, path2: string): boolean {
return normCasePath(path1) === normCasePath(path2);
}
Expand Down
9 changes: 9 additions & 0 deletions extensions/positron-r/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,15 @@
"default": [],
"markdownDescription": "%r.configuration.customBinaries.markdownDescription%"
},
"positron.r.interpreters.exclude": {
"scope": "window",
"type": "array",
"items": {
"type": "string"
},
"default": [],
"markdownDescription": "%r.configuration.interpreters.exclude.markdownDescription%"
},
"positron.r.kernel.path": {
"scope": "window",
"type": "string",
Expand Down
1 change: 1 addition & 0 deletions extensions/positron-r/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"r.configuration.title-dev": "Advanced",
"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).",
"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`.",
"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.",
"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.",
"r.configuration.tracing.description": "Traces the communication between VS Code and the language server",
"r.configuration.tracing.off.description": "No tracing.",
Expand Down
44 changes: 44 additions & 0 deletions extensions/positron-r/src/path-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

import * as path from 'path';
import * as os from 'os';

/**
* Returns true if given file path exists within the given parent directory, false otherwise.
* Copied from the function of the same name in extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts.
* @param filePath File path to check for
* @param parentPath The potential parent path to check for
*/
export function isParentPath(filePath: string, parentPath: string): boolean {
if (!parentPath.endsWith(path.sep)) {
parentPath += path.sep;
}
if (!filePath.endsWith(path.sep)) {
filePath += path.sep;
}
return normCasePath(filePath).startsWith(normCasePath(parentPath));
}

/**
* Adapted from the function of the same name in extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts.
*/
export function normCasePath(filePath: string): string {
return os.platform() === 'win32' ? path.normalize(filePath).toUpperCase() : path.normalize(filePath);
}

/**
* Copied from the function of the same name in extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts.
*/
export function arePathsSame(path1: string, path2: string): boolean {
return normCasePath(path1) === normCasePath(path2);
}

/**
* Copied from the function of the same name in extensions/positron-python/src/client/common/helpers.ts.
*/
export function untildify(path: string): string {
return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`);
}
57 changes: 56 additions & 1 deletion extensions/positron-r/src/r-installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import * as semver from 'semver';
import * as path from 'path';
import * as fs from 'fs';
import * as vscode from 'vscode';
import { extractValue, readLines, removeSurroundingQuotes } from './util';
import { LOGGER } from './extension';
import { MINIMUM_R_VERSION } from './constants';
import { arePathsSame, isParentPath, untildify } from './path-utils';

/**
* Extra metadata included in the LanguageRuntimeMetadata for R installations.
Expand Down Expand Up @@ -56,7 +58,8 @@ export enum ReasonDiscovered {
export enum ReasonRejected {
invalid = "invalid",
unsupported = "unsupported",
nonOrthogonal = "nonOrthogonal"
nonOrthogonal = "nonOrthogonal",
excluded = "excluded",
}

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

Expand Down Expand Up @@ -191,6 +196,14 @@ export class RInstallation {
this.usable = this.current || this.orthogonal;
if (!this.usable) {
this.reasonRejected = ReasonRejected.nonOrthogonal;
} else {
// Check if this installation has been excluded via settings
const excluded = isExcludedInstallation(this.binpath);
if (excluded) {
LOGGER.info(`R installation excluded via settings: ${this.binpath}`);
this.reasonRejected = ReasonRejected.excluded;
this.usable = false;
}
}
} else {
this.reasonRejected = ReasonRejected.unsupported;
Expand Down Expand Up @@ -283,3 +296,45 @@ function getRHomePathWindows(binpath: string): string | undefined {
}

}

/**
* Gets the list of R installations excluded via settings.
* Converts aliased paths to absolute paths. Relative paths are ignored.
* @returns List of installation paths to exclude.
*/
function getExcludedInstallations(): string[] {
const config = vscode.workspace.getConfiguration('positron.r');
const interpretersExclude = config.get<string[]>('interpreters.exclude') ?? [];
if (interpretersExclude.length > 0) {
const excludedPaths = interpretersExclude
.map((item) => untildify(item))
.filter((item) => {
if (path.isAbsolute(item)) {
return true;
}
LOGGER.info(`R installation path to exclude ${item} is not absolute...ignoring`);
return false;
});
const formattedPaths = JSON.stringify(excludedPaths, null, 2);
LOGGER.info(` R installation paths to exclude:\n${formattedPaths}`);
return excludedPaths;
}
LOGGER.debug('No installation paths specified to exclude via settings');
return [];
}

/**
* Checks if the given binary path is excluded via settings.
* @param binpath The binary path to check
* @returns True if the binary path is excluded, false if it is not excluded, and undefined if the
* no exclusions have been specified.
*/
function isExcludedInstallation(binpath: string): boolean | undefined {
const excludedInstallations = getExcludedInstallations();
if (excludedInstallations.length === 0) {
return undefined;
}
return excludedInstallations.some(
excluded => isParentPath(binpath, excluded) || arePathsSame(binpath, excluded)
);
}
Loading