Skip to content

Commit

Permalink
Discover (non-current) R installations from Windows registry (#4878)
Browse files Browse the repository at this point in the history
Addresses #4820

The goal is to discover non-current R installations from the Windows
registry. These were previously not discovered by Positron if the
installation also happened to be in a nonstandard location (and was not
on the PATH).

### QA Notes

The bare minimum would be to ensure that you current R installations
continue to be discovered, i.e. that this PR hasn't broken anything.
That is the most one could do on non-Windows.

More ambitious QA, which I have done, on Windows:

* Install R at a non-standard location. This will have to be via the
CRAN installer since rig doesn't give you any choice about this.
Therefore I removed my rig-installed released version of R and
re-installed released R with the CRAN installer below
`C:/nonstandardRLocation/`. Make sure the box to write to the registry
is checked (that is the default).
* Now use rig (or other means) to make *some other R version* the
current version. In my setup, R 4.2.3 and 4.3.3 are good candidates for
the new current version of R, e.g. `rig default 4.3.3`.
* Fire up Positron. You should see the released version of R, installed
at `C:/nonstandardRLocation/R-4.4.1` available to you in the interpreter
drop down after this PR. In a release build prior to this PR, that R
installation would not be picked up by Positron.

Other things that can be noticed in the Positron R output channel:

* Early on, you are likely to see the registry being consulted re: the
current R version:

  ```
2024-10-02 16:51:34.246 [info] Registry key
HKEY_LOCAL_MACHINE\SOFTWARE\R-core\R64\InstallPath reports the current R
installation is at C:\Program Files\R\R-4.2.3
2024-10-02 16:51:34.246 [info] Identified the current R binary:
C:\Program Files\R\R-4.2.3\bin\x64\R.exe
  ```
This is happening when we validate metadata of the last-used R runtime.
The logging has been tightened up and this finding is also now cached,
which eliminates some repetitive discovery and logging that happens when
we do the main, broad search for R installations.
* These are completely new registry findings re: R installations that
may or may not be the current one:

  ```
2024-10-02 16:51:34.573 [info] Registry key
HKEY_LOCAL_MACHINE\SOFTWARE\R-core\R64\4.2.3\InstallPath reports an R
installation at C:\Program Files\R\R-4.2.3
2024-10-02 16:51:34.573 [info] Registry key
HKEY_LOCAL_MACHINE\SOFTWARE\R-core\R64\4.3.3\InstallPath reports an R
installation at C:\Program Files\R\R-4.3.3
2024-10-02 16:51:34.573 [info] Registry key
HKEY_LOCAL_MACHINE\SOFTWARE\R-core\R64\4.4.1\InstallPath reports an R
installation at C:\nonstandardRLocation\R-4.4.1
2024-10-02 16:51:34.573 [info] Registry key
HKEY_LOCAL_MACHINE\SOFTWARE\R-core\R64\4.5.0 Pre-release\InstallPath
reports an R installation at C:\Program Files\R\R-devel
  ```
The installations below `C:\Program Files\R` would be discovered anyway,
because they are in a well-known place for this OS. What's new is that
we find `C:\nonstandardRLocation\R-4.4.1`.
* We no longer message about registry keys that are not found, because
folks were regularly misinterpreting that as some sort of error, whereas
it's expected that we might check for keys that aren't defined.
  • Loading branch information
jennybc authored Oct 3, 2024
1 parent e710c69 commit 9137831
Showing 1 changed file with 134 additions and 52 deletions.
186 changes: 134 additions & 52 deletions extensions/positron-r/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ export const R_DOCUMENT_SELECTORS = [
{ language: 'r', pattern: '**/*.{rmd,Rmd}' },
];

/**
* Enum represents the source from which an R binary was discovered.
*/
enum BinarySource {
/* eslint-disable-next-line @typescript-eslint/naming-convention */
HQ = 'HQ',
adHoc = 'ad hoc locations',
registry = 'Windows registry',
/* eslint-disable-next-line @typescript-eslint/naming-convention */
PATH = 'PATH'
}

/**
* Discovers R language runtimes for Positron; implements
* positron.LanguageRuntimeDiscoverer.
Expand All @@ -35,12 +47,12 @@ export const R_DOCUMENT_SELECTORS = [
*/
export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRuntimeMetadata> {
let rInstallations: Array<RInstallation> = [];
const binaries = new Set<string>();
const binaries = new Map<string, BinarySource>();

// look for R executables in the well-known place(s) for R installations on this OS
const hqBinaries = discoverHQBinaries();
for (const b of hqBinaries) {
binaries.add(b);
binaries.set(b, BinarySource.HQ);
}

// other places we might find an R binary
Expand All @@ -54,12 +66,21 @@ export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRun
.filter(b => fs.existsSync(b))
.map(b => fs.realpathSync(b));
for (const b of moreBinaries) {
binaries.add(b);
if (!binaries.has(b)) {
binaries.set(b, BinarySource.adHoc);
}
}

const registryBinaries = await discoverRegistryBinaries();
for (const b of registryBinaries) {
if (!binaries.has(b)) {
binaries.set(b, BinarySource.registry);
}
}

const pathBinary = await findRBinaryFromPATH();
if (pathBinary) {
binaries.add(pathBinary);
if (pathBinary && !binaries.has(pathBinary)) {
binaries.set(pathBinary, BinarySource.PATH);
}

// make sure we include the "current" version of R, for some definition of "current"
Expand All @@ -71,8 +92,8 @@ export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRun
binaries.delete(curBin);
}

binaries.forEach((b: string) => {
rInstallations.push(new RInstallation(b));
binaries.forEach((source, bin) => {
rInstallations.push(new RInstallation(bin));
});

// TODO: possible location to tell the user why certain R installations are being omitted from
Expand Down Expand Up @@ -297,14 +318,91 @@ function binFragments(): string[] {
}
}

/**
* Generates all possible R versions that we might find recorded in the Windows registry.
* Sort of.
* Only considers the major version of Positron's current minimum R version and that major
* version plus one.
* Naively tacks " Pre-release" onto each version numbers, because that's how r-devel shows up.
*/
function generateVersions(): string[] {
const minimumSupportedVersion = semver.coerce(MINIMUM_R_VERSION)!;
const major = minimumSupportedVersion.major;
const minor = minimumSupportedVersion.minor;
const patch = minimumSupportedVersion.patch;

const versions: string[] = [];
for (let x = major; x <= major + 1; x++) {
for (let y = (x === major ? minor : 0); y <= 9; y++) {
for (let z = (x === major && y === minor ? patch : 0); z <= 9; z++) {
versions.push(`${x}.${y}.${z}`);
versions.push(`${x}.${y}.${z} Pre-release`);
}
}
}

return versions;
}

async function discoverRegistryBinaries(): Promise<string[]> {
if (os.platform() !== 'win32') {
LOGGER.info('Skipping registry check on non-Windows platform');
return [];
}

// eslint-disable-next-line @typescript-eslint/naming-convention
const Registry = await import('@vscode/windows-registry');

const hives: any[] = ['HKEY_CURRENT_USER', 'HKEY_LOCAL_MACHINE'];
// R's install path is written to a WOW (Windows on Windows) node when e.g. an x86 build of
// R is installed on an ARM version of Windows.
const wows = ['', 'WOW6432Node'];

// The @vscode/windows-registry module is so minimalistic that it can't list the registry.
// Therefore we explicitly generate the R versions that might be there and check for each one.
const versions = generateVersions();

const discoveredKeys: string[] = [];

for (const hive of hives) {
for (const wow of wows) {
for (const version of versions) {
const R64_KEY: string = `SOFTWARE\\${wow ? wow + '\\' : ''}R-core\\R64\\${version}`;
try {
const key = Registry.GetStringRegKey(hive, R64_KEY, 'InstallPath');
if (key) {
LOGGER.info(`Registry key ${hive}\\${R64_KEY}\\InstallPath reports an R installation at ${key}`);
discoveredKeys.push(key);
}
} catch { }
}
}
}

const binPaths = discoveredKeys
.map(installPath => firstExisting(installPath, binFragments()))
.filter(binPath => binPath !== undefined);

return binPaths;
}

let cachedRBinary: string | undefined;

export async function findCurrentRBinary(): Promise<string | undefined> {
if (cachedRBinary !== undefined) {
return cachedRBinary;
}

if (os.platform() === 'win32') {
const registryBinary = await findCurrentRBinaryFromRegistry();
if (registryBinary) {
cachedRBinary = registryBinary;
return registryBinary;
}
}
return findRBinaryFromPATH();

cachedRBinary = await findRBinaryFromPATH();
return cachedRBinary;
}

async function findRBinaryFromPATH(): Promise<string | undefined> {
Expand Down Expand Up @@ -362,63 +460,47 @@ async function findRBinaryFromPATHNotWindows(whichR: string): Promise<string | u
}

async function findCurrentRBinaryFromRegistry(): Promise<string | undefined> {
let userPath = await getRegistryInstallPath('HKEY_CURRENT_USER');
if (!userPath) {
// If we didn't find R in the default user location, check WOW64
userPath = await getRegistryInstallPath('HKEY_CURRENT_USER', 'WOW6432Node');
if (os.platform() !== 'win32') {
LOGGER.info('Skipping registry check on non-Windows platform');
return undefined;
}
let machinePath = await getRegistryInstallPath('HKEY_LOCAL_MACHINE');
if (!machinePath) {
// If we didn't find R in the default machine location, check WOW64
machinePath = await getRegistryInstallPath('HKEY_LOCAL_MACHINE', 'WOW6432Node');

// eslint-disable-next-line @typescript-eslint/naming-convention
const Registry = await import('@vscode/windows-registry');

const hives: any[] = ['HKEY_CURRENT_USER', 'HKEY_LOCAL_MACHINE'];
const wows = ['', 'WOW6432Node'];

let installPath = undefined;

for (const hive of hives) {
for (const wow of wows) {
const R64_KEY: string = `SOFTWARE\\${wow ? wow + '\\' : ''}R-core\\R64`;
try {
const key = Registry.GetStringRegKey(hive, R64_KEY, 'InstallPath');
if (key) {
installPath = key;
LOGGER.info(`Registry key ${hive}\\${R64_KEY}\\InstallPath reports the current R installation is at ${key}`);
break;
}
} catch { }
}
}
if (!userPath && !machinePath) {

if (installPath === undefined) {
LOGGER.info('Cannot determine current version of R from the registry.');
return undefined;
}
const installPath = userPath || machinePath || '';

const binPath = firstExisting(installPath, binFragments());
if (!binPath) {
return undefined;
}
LOGGER.info(`Identified the current version of R from the registry: ${binPath}`);
LOGGER.info(`Identified the current R binary: ${binPath}`);

return binPath;
}

/**
* Get the registry install path for R.
*
* @param hive The Windows registry hive to check -- HKCU or HKLM
* @param wow Optionally, the WOW node to check under `Software`. R's install
* path is written to a WOW (Windows on Windows) node when e.g. an x86 build of
* R is installed on an ARM version of Windows.
*
* @returns The install path for R, or undefined if an R installation file could
* not be found at the install path.
*/
async function getRegistryInstallPath(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE', wow?: string | undefined): Promise<string | undefined> {
// 'R64' here is another place where we explicitly ignore 32-bit R
// Amend a WOW path after "Software" if requested
const R64_KEY: string = `SOFTWARE\\${wow ? wow + '\\' : ''}R-core\\R64`;

if (os.platform() !== 'win32') {
LOGGER.info('Skipping registry check on non-Windows platform');
return undefined;
}

// eslint-disable-next-line @typescript-eslint/naming-convention
const Registry = await import('@vscode/windows-registry');

try {
LOGGER.info(`Checking for 'InstallPath' in registry key ${hive}\\${R64_KEY}`);
return Registry.GetStringRegKey(hive, R64_KEY, 'InstallPath');
} catch (err) {
LOGGER.info(err as string);
return undefined;
}
}

// Should we recommend an R runtime for the workspace?
async function shouldRecommendForWorkspace(): Promise<boolean> {
// Check if the workspace contains R-related files.
Expand Down

0 comments on commit 9137831

Please sign in to comment.