From 45391bef0adc510c204b21143caf273e8757eafa Mon Sep 17 00:00:00 2001 From: Don Yin Date: Mon, 26 Oct 2026 12:54:46 +0000 Subject: [PATCH] Install prerequisites into the user library when the default is read-only Fixes #1516 Signed-off-by: Don Yin --- src/test/suite/util.test.ts | 42 +++++++++++++++++++++++++++++++++++++ src/util.ts | 12 ++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/test/suite/util.test.ts b/src/test/suite/util.test.ts index c9c51f735..f40260d0c 100644 --- a/src/test/suite/util.test.ts +++ b/src/test/suite/util.test.ts @@ -1,6 +1,7 @@ import * as assert from 'assert'; import cp from 'child_process'; import { EventEmitter } from 'events'; +import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as sinon from 'sinon'; @@ -26,6 +27,47 @@ suite('R command utilities', () => { assert.strictEqual(util.ToRStringLiteral('a"b\'c', ''), String.raw`a\"b\'c`); }); + test('package installation creates a user library when the default is read-only', async function () { + if (process.platform === 'win32' || process.getuid?.() === 0) { + this.skip(); // Requires enforced POSIX write permissions. + } + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-r-install-')); + const library = path.join(root, 'system'); + const userLibrary = path.join(root, 'user'); + const profile = path.join(root, '.Rprofile'); + fs.mkdirSync(library); + try { + fs.writeFileSync( + profile, + ` + .libPaths(${util.ToRStringLiteral(library, "'")}, include.site=FALSE) + install.packages <- function(pkgs, lib=.libPaths()[1L], ...) { + stopifnot(pkgs == 'languageserver', dir.exists(lib), lib == Sys.getenv('R_LIBS_USER')) + } + `, + ); + fs.chmodSync(library, 0o555); + sandbox.stub(vscode.window, 'showErrorMessage').resolves('Yes' as never); + const task = sandbox + .stub(vscode.tasks, 'executeTask') + .resolves({} as vscode.TaskExecution); + sandbox.stub(vscode.tasks, 'onDidEndTask').returns(new vscode.Disposable(() => {})); + await util.promptToInstallRPackage('languageserver', 'lsp.promptToInstall', root); + assert.ok(task.calledOnce); + const execution = task.firstCall.args[0].execution as vscode.ProcessExecution; + const result = cp.spawnSync(execution.process, execution.args, { + cwd: root, + encoding: 'utf8', + env: { ...process.env, R_PROFILE_USER: profile, R_LIBS_USER: userLibrary }, + }); + assert.strictEqual(result.status, 0, result.stdout + result.stderr); + assert.ok(fs.existsSync(userLibrary)); + } finally { + fs.chmodSync(library, 0o755); + fs.rmSync(root, { recursive: true, force: true }); + } + }); + test('a failed executable settles with its spawn error', async () => { const result = await Promise.race([ util.spawnAsync(path.join(os.tmpdir(), 'vscode-r-nonexistent-executable')), diff --git a/src/util.ts b/src/util.ts index e67a25a63..1b98e2c05 100644 --- a/src/util.ts +++ b/src/util.ts @@ -657,7 +657,17 @@ export async function promptToInstallRPackage( '--no-save', '--no-restore', '-e', - `install.packages('${name}', repos='${repo}')`, + `local({ + lib <- .libPaths()[1L] + if (file.access(lib, 2L) != 0L) { + lib <- strsplit(Sys.getenv('R_LIBS_USER'), .Platform$path.sep, fixed=TRUE)[[1L]][1L] + if (is.na(lib) || !nzchar(lib) || lib == 'NULL') { + stop('No user R library is configured.') + } + dir.create(lib, recursive=TRUE, showWarnings=FALSE) + } + install.packages(${ToRStringLiteral(name, "'")}, lib=lib, repos=${ToRStringLiteral(repo, "'")}) + })`, ]; void executeAsTask('Install Package', rPath, args, true); if (postInstallMsg) {