Skip to content

Commit

Permalink
Discover R on PATH on windows (#4190)
Browse files Browse the repository at this point in the history
Addresses #3702
Also related to #3677
and #3868

This PR deals with this situation:

* R is installed in a non-default location.
* We can't learn about this R installation from the registry (either
because registry keys were not written or permissions are keeping us
from reading the registry).
* The user has arranged for R to be discovered on the PATH.

### QA Notes

To experience the before vs. after, you need very specific setup. We're
trying to imitate a user with R in a non-default location and no
permission to access the registry. To make R discoverable, this user is
relying solely on the fact that they've put R on their PATH. If you
don't have a completely locked down corporate Windows laptop, follow the
instructions below to fake it 😬

Before: in a released version of Positron (or a release built before
this PR gets merged), the R version set up as described below WILL NOT
be discovered by Positron.

After: with this PR, the R version set up as described below WILL be
discovered by Positron.

You can also see more about discovery in the Positron R Extension output
channel. (It is expected that you will see that some R versions are
discovered more than once.) You should see new logging messages like
this:

`2024-08-01 16:19:27.712 [info] Possibly found R on PATH:
C:\notADefaultFolder\R-4.2.3\bin\x64\R.EXE.`

---

1.  **OS must be Windows**. At least one R version needs to be:

2. **Installed in an unusual place** where Positron will not
automatically discover it. TL;DR is to install somewhere other than
`C:\Program Files` or `C:\R`. For a more precise definition, here is the
[actual
code](https://github.com/posit-dev/positron/blob/4a05a525c7da4bb2778490eb6a244e0f4b54ccef/extensions/positron-r/src/provider.ts#L228-L236).
For example, I chose to put this non-default R installation in
`C:\notADefaultFolder` . This will be easiest to do if you use the CRAN
installer directly (so, not using rig). You can get old versions of R
here: [Previous releases of R for Windows
(r-project.org)](https://cran.r-project.org/bin/windows/base/old/)

<img width="588" alt="select-destination"
src="https://github.com/user-attachments/assets/9f3a7e90-9630-430a-b70c-4b44278776a7">

4. **Not recorded as the current version of R in the registry**. Make
sure it is NOT stored as the `InstallPath` subkey in these locations:

    -   `HKEY_LOCAL_MACHINE\SOFTWARE\R-core\R64`
    -   `HKEY_LOCAL_MACHINE\SOFTWARE\R-core\R`
    -   `HKEY_CURRENT_USER\SOFTWARE\R-core\R64`
    -   `HKEY_CURRENT_USER\SOFTWARE\R-core\R`
    -   `HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\R-core\R`
    -   `HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\R-core\R64`
    -   `HKEY_CURRENT_USER\SOFTWARE\WOW6432Node\R-core\R`
    -   `HKEY_CURRENT_USER\SOFTWARE\WOW6432Node\R-core\R64`

    Ways to accomplish this:

    -   Don't let the installer write to the registry.
        
<img width="589" alt="save-version-registry"
src="https://github.com/user-attachments/assets/cabee89d-7f34-443d-86f7-a30f41f9294c">

    -   Manually delete keys via the Registry Editor.

- Install some *other* version of R after this one and let *that*
version be recorded in the registry as the current version of R.

    -   Use utilities that ship with R:

> After installation you can add the Registry entries by running
`RSetReg.exe` in a sub-folder of the `bin` folder, and remove them by
running this with argument `/U`. Note that this requires administrative
privileges unless run with argument `/Personal` and neither sets up nor
removes the file associations.

5. **Have its `bin\x64` or `bin` directory on the PATH** (system or
user). Neither rig nor the CRAN installer do this, so this has to be
done explicitly. For example, the path I added to my user PATH is
`C:\notADefaultFolder\R-4.2.3\bin\x64`. There seem to be many ways to do
this, but here's what I did:

Go to About your PC \> Advanced system settings (in list on the right)
\> Environment Variables. Add an entry to `Path` for the user or the
system.

<img width="531" alt="edit-environment-variable"
src="https://github.com/user-attachments/assets/2f03db69-c373-4a8e-9439-7ed6f8258395">

You'll need to restart any shells or maybe log out / log in for this to
take effect.

Test that this has worked by launching R in a terminal and confirm it's
launching the intended version. I verified this in cmd.exe, PowerShell,
and Git Bash FWIW.

---------

Signed-off-by: Jennifer (Jenny) Bryan <[email protected]>
Co-authored-by: Jonathan <[email protected]>
  • Loading branch information
jennybc and jmcphers authored Aug 5, 2024
1 parent 0215f13 commit 5c0e593
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 55 deletions.
94 changes: 54 additions & 40 deletions extensions/positron-r/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ export async function* rRuntimeDiscoverer(): AsyncGenerator<positron.LanguageRun
binaries.add(b);
}

const pathBinary = await findRBinaryFromPATH();
if (pathBinary) {
binaries.add(pathBinary);
}

// make sure we include the "current" version of R, for some definition of "current"
// we've probably already discovered it, but we still want to single it out, so that we mark
// that particular R installation as the current one
Expand Down Expand Up @@ -294,59 +299,68 @@ function binFragments(): string[] {

export async function findCurrentRBinary(): Promise<string | undefined> {
if (os.platform() === 'win32') {
const registryBinary = findCurrentRBinaryFromRegistry();
const registryBinary = await findCurrentRBinaryFromRegistry();
if (registryBinary) {
return registryBinary;
}
}
return findRBinaryFromPATH();
}

async function findRBinaryFromPATH(): Promise<string | undefined> {
const whichR = await which('R', { nothrow: true }) as string;
if (whichR) {
LOGGER.info(`Possibly found R on PATH: ${whichR}.`);
if (os.platform() === 'win32') {
// rig puts {R Headquarters}/bin on the PATH, so that is probably how we got here.
// (The CRAN installer does NOT put R on the PATH.)
// In the rig scenario, `whichR` is anticipated to be a batch file that launches the
// current version of R ('default' in rig-speak):
// Example filepath: C:\Program Files\R\bin\R.bat
// Typical contents of this file:
// ::4.3.2
// @"C:\Program Files\R\R-4.3.2\bin\R" %*
// How it looks when r-devel is current:
// ::devel
// @"C:\Program Files\R\R-devel\bin\R" %*
// Note that this is not our preferred x64 binary, so we try to convert it.
if (path.extname(whichR).toLowerCase() === '.bat') {
const batLines = readLines(whichR);
const re = new RegExp(`^@"(.+R-(devel|[0-9]+[.][0-9]+[.][0-9]+).+)" %[*]$`);
const match = batLines.find((x: string) => re.test(x))?.match(re);
if (match) {
const whichRMatched = match[1];
const whichRHome = getRHomePath(whichRMatched);
if (!whichRHome) {
LOGGER.info(`Failed to get R home path from ${whichRMatched}`);
return undefined;
}
// we prefer the x64 binary
const whichRResolved = firstExisting(whichRHome, binFragments());
if (whichRResolved) {
LOGGER.info(`Resolved R binary at ${whichRResolved}`);
return whichRResolved;
} else {
LOGGER.info(`Can\'t find R binary within ${whichRHome}`);
return undefined;
}
}
}
// TODO: handle the case where whichR isn't picking up the rig case; do people do this,
// meaning put R on the PATH themselves, on Windows?
return await findRBinaryFromPATHWindows(whichR);
} else {
const whichRCanonical = fs.realpathSync(whichR);
LOGGER.info(`Resolved R binary at ${whichRCanonical}`);
return whichRCanonical;
return await findRBinaryFromPATHNotWindows(whichR);
}
} else {
return undefined;
}
}

async function findRBinaryFromPATHWindows(whichR: string): Promise<string | undefined> {
// The CRAN Windows installer does NOT put R on the PATH.
// If we are here, it is because the user has arranged it so.
const ext = path.extname(whichR).toLowerCase();
if (ext !== '.exe') {
// rig can put put something on the PATH that results in whichR being 'a/path/to/R.bat'
// but we aren't going to handle that.
LOGGER.info(`Unsupported extension: ${ext}.`);
return undefined;
}

// Overall idea: a discovered binpath --> homepath --> our preferred binpath
// This might just be a no-op.
// But if the input binpath is this:
// "C:\Program Files\R\R-4.3.2\bin\R.exe"
// we want to convert it to this, if it exists:
// "C:\Program Files\R\R-4.3.2\bin\x64\R.exe"
// It typically does exist for x86_64 R installations.
// It will not exist for arm64 R installations.
const whichRHome = getRHomePath(whichR);
if (!whichRHome) {
LOGGER.info(`Failed to get R home path from ${whichR}.`);
return undefined;
}
const binpathNormalized = firstExisting(whichRHome, binFragments());
if (binpathNormalized) {
LOGGER.info(`Resolved R binary at ${binpathNormalized}.`);
return binpathNormalized;
} else {
LOGGER.info(`Can't find R binary within ${whichRHome}.`);
return undefined;
}
}

async function findRBinaryFromPATHNotWindows(whichR: string): Promise<string | undefined> {
const whichRCanonical = fs.realpathSync(whichR);
LOGGER.info(`Resolved R binary at ${whichRCanonical}`);
return whichRCanonical;
}

async function findCurrentRBinaryFromRegistry(): Promise<string | undefined> {
let userPath = await getRegistryInstallPath(winreg.HKCU);
if (!userPath) {
Expand Down
30 changes: 15 additions & 15 deletions extensions/positron-r/src/r-installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,28 +134,28 @@ export class RInstallation {
}
}

export function getRHomePath(binPath: string): string | undefined {
export function getRHomePath(binpath: string): string | undefined {
switch (process.platform) {
case 'darwin':
case 'linux':
return getRHomePathNotWindows(binPath);
return getRHomePathNotWindows(binpath);
case 'win32':
return getRHomePathWindows(binPath);
return getRHomePathWindows(binpath);
default:
throw new Error('Unsupported platform');
}
}

function getRHomePathNotWindows(binPath: string): string | undefined {
const binLines = readLines(binPath);
function getRHomePathNotWindows(binpath: string): string | undefined {
const binLines = readLines(binpath);
const re = new RegExp('Shell wrapper for R executable');
if (!binLines.some(x => re.test(x))) {
LOGGER.info(`Binary is not a shell script wrapping the executable: ${binPath}`);
LOGGER.info(`Binary is not a shell script wrapping the executable: ${binpath}`);
return undefined;
}
const targetLine = binLines.find(line => line.match('R_HOME_DIR'));
if (!targetLine) {
LOGGER.info(`Can\'t determine R_HOME_DIR from the binary: ${binPath}`);
LOGGER.info(`Can\'t determine R_HOME_DIR from the binary: ${binpath}`);
return undefined;
}
// macOS: R_HOME_DIR=/Library/Frameworks/R.framework/Versions/4.3-arm64/Resources
Expand All @@ -165,23 +165,23 @@ function getRHomePathNotWindows(binPath: string): string | undefined {
const R_HOME_DIR = removeSurroundingQuotes(extractValue(targetLine, 'R_HOME_DIR'));
const homepath = R_HOME_DIR;
if (homepath === '') {
LOGGER.info(`Can\'t determine R_HOME_DIR from the binary: ${binPath}`);
LOGGER.info(`Can\'t determine R_HOME_DIR from the binary: ${binpath}`);
return undefined;
}
return homepath;
}

function getRHomePathWindows(binPath: string): string | undefined {
function getRHomePathWindows(binpath: string): string | undefined {
// find right-most 'bin' in the path and take everything to the left of it
// Examples of binPaths:
// "C:\Program Files\R\R-4.3.2\bin\R.exe" <-- the path produced by our binFragment() helper
// "C:\Program Files\R\R-4.3.2\bin\x64\R.exe" <-- but this also exists
const binIndex = binPath.lastIndexOf(path.sep + 'bin' + path.sep);
// Examples of binpaths:
// "C:\Program Files\R\R-4.3.2\bin\x64\R.exe" <-- we prefer this, if both are present
// "C:\Program Files\R\R-4.3.2\bin\R.exe" <-- usually a shim for the path above
const binIndex = binpath.lastIndexOf(path.sep + 'bin' + path.sep);
if (binIndex === -1) {
LOGGER.info(`Can\'t determine R_HOME_DIR from the path to the R binary: ${binPath}`);
LOGGER.info(`Can\'t determine R_HOME_DIR from the path to the R binary: ${binpath}`);
return undefined;
} else {
const pathUpToBin = binPath.substring(0, binIndex);
const pathUpToBin = binpath.substring(0, binIndex);
return pathUpToBin;
}

Expand Down

0 comments on commit 5c0e593

Please sign in to comment.