diff --git a/extensions/positron-r/src/provider.ts b/extensions/positron-r/src/provider.ts index ab56967f72b..f4906e1770d 100644 --- a/extensions/positron-r/src/provider.ts +++ b/extensions/positron-r/src/provider.ts @@ -58,6 +58,11 @@ export async function* rRuntimeDiscoverer(): AsyncGenerator { if (os.platform() === 'win32') { - const registryBinary = findCurrentRBinaryFromRegistry(); + const registryBinary = await findCurrentRBinaryFromRegistry(); if (registryBinary) { return registryBinary; } } + return findRBinaryFromPATH(); +} +async function findRBinaryFromPATH(): Promise { 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 { + // 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 { + const whichRCanonical = fs.realpathSync(whichR); + LOGGER.info(`Resolved R binary at ${whichRCanonical}`); + return whichRCanonical; +} + async function findCurrentRBinaryFromRegistry(): Promise { let userPath = await getRegistryInstallPath(winreg.HKCU); if (!userPath) { diff --git a/extensions/positron-r/src/r-installation.ts b/extensions/positron-r/src/r-installation.ts index ec40e6d73a0..e07d0c991f8 100644 --- a/extensions/positron-r/src/r-installation.ts +++ b/extensions/positron-r/src/r-installation.ts @@ -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 @@ -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; }