Windows/conda 环境下稳定调用本地 Rscript 的 Skill

cd187f0ef6934d7dad2d86b45fb5a2ff

背景与场景

在 Windows 上跑数据/生信流程时,经常出现:主流程在 conda 的 Python 环境里,但其中某一步需要调用 R。常见问题包括:

  • 找不到 Rscript(PATH 混乱 / 多 R 版本 / conda 里也有 R)
  • R 包库冲突(加载到错误的 library,或提示包缺失)
  • conda/Python 环境变量污染导致 R 运行不稳定
  • 失败不可追溯(stderr 没落盘,复现困难)

Skill 做什么

sc-rscript-env-runner 把“找到并稳定调用 Rscript.exe”的流程标准化,输出一套可执行的策略与模板,包含:

  • 定位可用 Rscript.exe 的优先级策略
  • 运行前清理关键环境变量、重写 PATH 降低冲突
  • 使用 Rscript --vanilla 执行并建议 stdout/stderr 落盘,便于复现排错

使用方法(如何调用)

触发式调用(对话示例):

  • “请使用 sc-rscript-env-runner,帮我在 Windows/conda 下定位可用 Rscript,并给出稳定调用模板。”
  • “我运行时报错 ‘未找到 Rscript’,请用 sc-rscript-env-runner 给排查与解决方案(包含可复制步骤)。”
  • “我的 conda 和系统 R 可能冲突,请用 sc-rscript-env-runner 给一个更干净的运行方式,并把 stdout/stderr 落盘。”

建议同时提供的信息:

  • 当前运行方式(是否在 conda 环境内、是否有 CONDA_PREFIX)
  • 报错信息原文
  • 若已知 Rscript 路径则直接给绝对路径
  • 希望日志/输出落盘的目录

最佳实践要点

  • 优先显式指定 Rscript 绝对路径,避免依赖 PATH
  • 执行使用 Rscript --vanilla,减少用户级配置带来的不确定性
  • 运行前清理 PYTHONHOME/PYTHONPATH/R_LIBS/R_LIBS_USER
  • 固定 cwd 到输出目录,stdout/stderr 分别落盘,便于复现与排错
  • 若提示缺包,先确认“实际调用的是哪个 Rscript”(路径与版本)

附录 A:Skills数据

name: "sc-rscript-env-runner"
description: "在 Windows/conda 下定位并调用本地 Rscript 运行 R 脚本。用户遇到 Rscript 找不到、R 包环境冲突或需要从 Python 调用 R 时使用。"


# sc-rscript-env-runner

## 目标

把“如何在本机找到 Rscript.exe 并稳定调用它”的逻辑标准化,适用于:

- Python 脚本里需要执行一段 R 代码(写入 .R 文件后调用 Rscript)
- Windows + conda 混用导致 PATH / R_LIBS 冲突
- 机器上装了多个 R 版本,需要自动挑选可用且版本更高的 Rscript

## 触发场景(何时调用)

- 运行流程时报错 “未找到 Rscript”
- 运行 Rscript 时报包缺失、加载到错误的 R library、或 conda 的 R 与系统 R 冲突
- 需要将一段 R 计算封装到 Python step 中,并希望把 stdout/stderr 落盘便于追踪

## 参考实现(来自 Step2_scTenifoldKnk.py 的约定)

### 1) 定位 Rscript 的优先级

按以下顺序找可执行文件路径:

1. 用户显式指定(例如命令行参数 `--rscript`,且路径存在)
2. `PATH` 中的 `Rscript` 或 `Rscript.exe`(`shutil.which`)
3. Windows `where Rscript.exe` 的返回结果
4. 环境变量:
   - `R_HOME` 下的 `bin\Rscript.exe` 或 `bin\x64\Rscript.exe`
   - `CONDA_PREFIX` 下的 `Scripts\Rscript.exe` 或 `Library\bin\Rscript.exe`
5. 常见安装目录扫描:
   - `%LOCALAPPDATA%\Programs\R\R-*\bin\Rscript.exe`(含 `bin\x64`)
   - `C:\Program Files\R\R-*\bin\Rscript.exe`(含 `bin\x64`)
   - `C:\Program Files (x86)\R\R-*\bin\Rscript.exe`(含 `bin\x64`)

若候选不止一个,按路径里的版本号(`R-4.3.2` 这种)做排序,选择版本更高的那个。

### 2) 清理环境变量以减少 Python/conda 对 R 的污染

调用 R 前构造新的 `env`(基于 `os.environ` 拷贝),核心策略:

- 删除这些变量(避免干扰 R 与包库解析):
  - `PYTHONHOME`, `PYTHONPATH`
  - `R_LIBS`, `R_LIBS_USER`
- 重写 `PATH`:
  - 把 `Rscript.exe` 所在目录放到 PATH 第一位
  - 从原 PATH 中移除明显属于 conda/Anaconda/Mamba/Micromamba 的路径片段
  - 同时移除 `...\Library\bin`(常见 conda R 的 DLL 路径来源,易与系统 R 冲突)

### 3) 实际执行方式

- 将 R 代码写入 `run_xxx.R`
- 使用:

```text
Rscript --vanilla run_xxx.R
  • cwd 设置为输出目录,stdout/stderr 分别写入文件,便于复现与排错

推荐复用模板(Python)

当你需要在任意 step 中运行 R 时,优先复用下列模式(与仓库现有实现对齐):

from pathlib import Path
import os
import shutil
import subprocess


def find_rscript(explicit: str | None = None) -> str | None:
    if explicit:
        p = Path(explicit)
        if p.exists():
            return str(p)

    path = shutil.which("Rscript") or shutil.which("Rscript.exe")
    if path:
        return path

    candidates: list[Path] = []

    try:
        p = subprocess.run(["where", "Rscript.exe"], capture_output=True, text=True, check=False)
        if p.returncode == 0:
            for line in (p.stdout or "").splitlines():
                line = line.strip()
                if line:
                    candidates.append(Path(line))
    except Exception:
        pass

    r_home = os.environ.get("R_HOME")
    if r_home:
        candidates.append(Path(r_home) / r"bin\Rscript.exe")
        candidates.append(Path(r_home) / r"bin\x64\Rscript.exe")

    conda_prefix = os.environ.get("CONDA_PREFIX")
    if conda_prefix:
        candidates.append(Path(conda_prefix) / r"Scripts\Rscript.exe")
        candidates.append(Path(conda_prefix) / r"Library\bin\Rscript.exe")

    localapp = os.environ.get("LOCALAPPDATA")
    if localapp:
        local_r = Path(localapp) / "Programs" / "R"
        if local_r.exists():
            candidates.extend(local_r.glob(r"R-*\bin\Rscript.exe"))
            candidates.extend(local_r.glob(r"R-*\bin\x64\Rscript.exe"))

    for root in [Path(r"C:\Program Files"), Path(r"C:\Program Files (x86)")]:
        r_root = root / "R"
        if r_root.exists():
            candidates.extend(r_root.glob(r"R-*\bin\Rscript.exe"))
            candidates.extend(r_root.glob(r"R-*\bin\x64\Rscript.exe"))

    candidates = [p for p in candidates if p.exists()]
    return str(candidates[0]) if candidates else None


def clean_env_for_r(rscript: str) -> dict[str, str]:
    env = dict(os.environ)
    env.pop("PYTHONHOME", None)
    env.pop("PYTHONPATH", None)
    env.pop("R_LIBS", None)
    env.pop("R_LIBS_USER", None)

    r_bin = str(Path(rscript).resolve().parent)
    path_items = (env.get("PATH") or "").split(os.pathsep)

    drop_tokens = [
        "miniconda",
        "anaconda",
        "mambaforge",
        "micromamba",
        "conda",
        os.path.join("library", "bin").lower(),
    ]

    kept: list[str] = []
    for it in path_items:
        low = it.lower()
        if any(tok in low for tok in drop_tokens):
            continue
        kept.append(it)

    env["PATH"] = os.pathsep.join([r_bin] + kept)
    return env


def run_rscript(rscript: str, r_code: str, out_dir: Path) -> subprocess.CompletedProcess:
    out_dir.mkdir(parents=True, exist_ok=True)
    r_file = out_dir / "run.R"
    r_file.write_text(r_code.lstrip(), encoding="utf-8")
    p = subprocess.run(
        [rscript, "--vanilla", str(r_file)],
        cwd=str(out_dir),
        env=clean_env_for_r(rscript),
        capture_output=True,
        encoding="utf-8",
        errors="replace",
        text=True,
        check=False,
    )
    (out_dir / "r_stdout.txt").write_text(p.stdout or "", encoding="utf-8")
    (out_dir / "r_stderr.txt").write_text(p.stderr or "", encoding="utf-8")
    return p


## 运行与排错清单

- 优先用 `--rscript` 传入绝对路径,避免依赖 PATH
- `Rscript --vanilla` 可以减少用户级配置文件带来的不确定性
- 若提示找不到包(如 `scTenifoldKnk`),说明当前 R 的库路径里未安装对应包;先确认你实际调用的是哪个 Rscript(把 `rscript` 路径写入 manifest/日志)
- 若 conda 与系统 R 冲突,优先使用本技能的“清理 env + PATH 置顶 Rscript 目录”策略

2 个赞