rrun-cli 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rrun_cli-0.2.0/src/rrun_cli.egg-info → rrun_cli-0.2.1}/PKG-INFO +2 -2
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/README.md +1 -1
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/pyproject.toml +1 -1
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/src/rrun/__init__.py +1 -1
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/src/rrun/doctor.py +25 -4
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/src/rrun/executor.py +32 -9
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/src/rrun/setup.py +34 -7
- {rrun_cli-0.2.0 → rrun_cli-0.2.1/src/rrun_cli.egg-info}/PKG-INFO +2 -2
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/tests/test_executor.py +27 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/tests/test_setup.py +29 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/LICENSE +0 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/setup.cfg +0 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/src/rrun/__main__.py +0 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/src/rrun/registry.py +0 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/src/rrun/remote-requirements.txt +0 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/src/rrun_cli.egg-info/SOURCES.txt +0 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/src/rrun_cli.egg-info/dependency_links.txt +0 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/src/rrun_cli.egg-info/entry_points.txt +0 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/src/rrun_cli.egg-info/requires.txt +0 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/src/rrun_cli.egg-info/top_level.txt +0 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/tests/test_cli.py +0 -0
- {rrun_cli-0.2.0 → rrun_cli-0.2.1}/tests/test_registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rrun-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Run local scripts on remote machines over SSH via stdin pipes — no escaping/encoding hell. Supports python/powershell/bash, with a unified remote Python 3.12 venv provisioner.
|
|
5
5
|
Author: waqiju
|
|
6
6
|
License-Expression: MIT
|
|
@@ -153,7 +153,7 @@ For `python` scripts, rrun requires a **3.12.x** interpreter on the remote, prob
|
|
|
153
153
|
2. Standalone base: `...\python312\python.exe` / `~/.remote-machine/python312/bin/python3`
|
|
154
154
|
3. Existing installs: `C:\Python\Python312\python.exe` / `python3.12` on PATH
|
|
155
155
|
|
|
156
|
-
If none match, run `rrun setup <host>`. It is idempotent and non-destructive: if the machine lacks Python 3.12, a [python-build-standalone](https://github.com/astral-sh/python-build-standalone) tarball is downloaded to the local cache (`~/.cache/rrun/`) and streamed over ssh stdin — no registry, no PATH changes, no admin rights, and the remote never touches GitHub. The venv gets its own `pip.ini`/`pip.conf` (global pip config untouched) plus the standard packages from the bundled `remote-requirements.txt` (override with `~/.rrun/remote-requirements.txt`).
|
|
156
|
+
If none match, run `rrun setup <host>`. It is idempotent and non-destructive: if the machine lacks a **venv-capable** Python 3.12 — none installed, or the interpreter cannot create venvs (Debian/Ubuntu split `ensurepip` into the separate `python3.12-venv` package) — a [python-build-standalone](https://github.com/astral-sh/python-build-standalone) tarball (Windows/macOS/Linux) is downloaded to the local cache (`~/.cache/rrun/`) and streamed over ssh stdin — no registry, no PATH changes, no admin rights, and the remote never touches GitHub. A half-created venv left by an interrupted earlier setup (python runs but pip is missing) is detected and recreated automatically. `rrun doctor <host>` reports all of these states before you provision. The venv gets its own `pip.ini`/`pip.conf` (global pip config untouched) plus the standard packages from the bundled `remote-requirements.txt` (override with `~/.rrun/remote-requirements.txt`).
|
|
157
157
|
|
|
158
158
|
## Security
|
|
159
159
|
|
|
@@ -125,7 +125,7 @@ For `python` scripts, rrun requires a **3.12.x** interpreter on the remote, prob
|
|
|
125
125
|
2. Standalone base: `...\python312\python.exe` / `~/.remote-machine/python312/bin/python3`
|
|
126
126
|
3. Existing installs: `C:\Python\Python312\python.exe` / `python3.12` on PATH
|
|
127
127
|
|
|
128
|
-
If none match, run `rrun setup <host>`. It is idempotent and non-destructive: if the machine lacks Python 3.12, a [python-build-standalone](https://github.com/astral-sh/python-build-standalone) tarball is downloaded to the local cache (`~/.cache/rrun/`) and streamed over ssh stdin — no registry, no PATH changes, no admin rights, and the remote never touches GitHub. The venv gets its own `pip.ini`/`pip.conf` (global pip config untouched) plus the standard packages from the bundled `remote-requirements.txt` (override with `~/.rrun/remote-requirements.txt`).
|
|
128
|
+
If none match, run `rrun setup <host>`. It is idempotent and non-destructive: if the machine lacks a **venv-capable** Python 3.12 — none installed, or the interpreter cannot create venvs (Debian/Ubuntu split `ensurepip` into the separate `python3.12-venv` package) — a [python-build-standalone](https://github.com/astral-sh/python-build-standalone) tarball (Windows/macOS/Linux) is downloaded to the local cache (`~/.cache/rrun/`) and streamed over ssh stdin — no registry, no PATH changes, no admin rights, and the remote never touches GitHub. A half-created venv left by an interrupted earlier setup (python runs but pip is missing) is detected and recreated automatically. `rrun doctor <host>` reports all of these states before you provision. The venv gets its own `pip.ini`/`pip.conf` (global pip config untouched) plus the standard packages from the bundled `remote-requirements.txt` (override with `~/.rrun/remote-requirements.txt`).
|
|
129
129
|
|
|
130
130
|
## Security
|
|
131
131
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rrun-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Run local scripts on remote machines over SSH via stdin pipes — no escaping/encoding hell. Supports python/powershell/bash, with a unified remote Python 3.12 venv provisioner."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -8,16 +8,19 @@ import time
|
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
|
|
10
10
|
from .executor import (
|
|
11
|
+
POSIX_BASE_CANDIDATES,
|
|
11
12
|
POSIX_PYTHON_CANDIDATES,
|
|
12
13
|
PS_REMOTE_CMD,
|
|
13
14
|
VENV_PY_POSIX,
|
|
14
15
|
VENV_PY_WIN,
|
|
16
|
+
WINDOWS_BASE_CANDIDATES,
|
|
15
17
|
WINDOWS_PYTHON_CANDIDATES,
|
|
16
18
|
_ssh_run,
|
|
17
19
|
build_ps_wrapper,
|
|
20
|
+
check_remote_pip,
|
|
18
21
|
probe_python,
|
|
19
22
|
)
|
|
20
|
-
from .registry import resolve_machine
|
|
23
|
+
from .registry import Machine, resolve_machine
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
@dataclass
|
|
@@ -33,6 +36,20 @@ class DoctorResult:
|
|
|
33
36
|
message: str = "" # 失败原因 / 建议
|
|
34
37
|
|
|
35
38
|
|
|
39
|
+
def _venv_missing_guidance(machine: Machine, found_version: str, mux: bool) -> str:
|
|
40
|
+
"""venv 缺失时的指引:审查基座资格(ensurepip),说明 setup 将会走哪条路。"""
|
|
41
|
+
base_cands = WINDOWS_BASE_CANDIDATES if machine.is_windows else POSIX_BASE_CANDIDATES
|
|
42
|
+
base = probe_python(machine, base_cands, mux, require_ensurepip=True)
|
|
43
|
+
if base:
|
|
44
|
+
head = "unified venv missing" if found_version else "no python 3.12 found"
|
|
45
|
+
return f"{head}; run: rrun setup {machine.name}"
|
|
46
|
+
hint = " (Debian/Ubuntu: sudo apt install python3.12-venv)" if not machine.is_windows else ""
|
|
47
|
+
if found_version:
|
|
48
|
+
return (f"unified venv missing; base python {found_version} lacks ensurepip{hint}; "
|
|
49
|
+
f"rrun setup {machine.name} will install a standalone python")
|
|
50
|
+
return f"no python 3.12 found; rrun setup {machine.name} will install a standalone python"
|
|
51
|
+
|
|
52
|
+
|
|
36
53
|
def check_machine(host: str, mux: bool = True, timeout: float = 30.0) -> DoctorResult:
|
|
37
54
|
"""检查单台机器:ssh 回显 → 远端 python 探测。不抛异常,结果全部汇入 DoctorResult。"""
|
|
38
55
|
try:
|
|
@@ -80,9 +97,13 @@ def check_machine(host: str, mux: bool = True, timeout: float = 30.0) -> DoctorR
|
|
|
80
97
|
"/.remote-machine/venv/bin/python")
|
|
81
98
|
tag = "unified venv" if r.is_unified_venv else "fallback (not the unified venv)"
|
|
82
99
|
r.checks.append(f"python {r.python_version} at {r.python_path} [{tag}]")
|
|
83
|
-
if
|
|
84
|
-
|
|
100
|
+
if r.is_unified_venv:
|
|
101
|
+
if not check_remote_pip(machine, r.python_path, mux):
|
|
102
|
+
r.message = f"unified venv is broken (pip missing); repair: rrun setup {machine.name}"
|
|
103
|
+
return r
|
|
104
|
+
else:
|
|
105
|
+
r.message = _venv_missing_guidance(machine, r.python_version, mux)
|
|
85
106
|
else:
|
|
86
|
-
r.message =
|
|
107
|
+
r.message = _venv_missing_guidance(machine, "", mux)
|
|
87
108
|
r.ok = r.ssh_ok and bool(hit) and r.is_unified_venv
|
|
88
109
|
return r
|
|
@@ -205,27 +205,33 @@ _PY_VER_PRINT_CODE = "import sys;print(*sys.version_info[:3],sep=chr(46))"
|
|
|
205
205
|
_PY_VER_CHECK_CODE = (
|
|
206
206
|
_PY_VER_PRINT_CODE + f";sys.exit(0 if sys.version_info[:2]=={REQUIRED_PY_MAJOR_MINOR} else 1)"
|
|
207
207
|
)
|
|
208
|
+
# 基座资格加码:ensurepip 必须可 import(Debian/Ubuntu 把 ensurepip 整体拆进 python3.x-venv
|
|
209
|
+
# 包,缺失时系统 python 版本对却建不了 venv——不算合格基座)。import 失败即非零退出,候选跳过。
|
|
210
|
+
_PY_BASE_CHECK_CODE = "import ensurepip;" + _PY_VER_CHECK_CODE
|
|
208
211
|
|
|
209
212
|
|
|
210
|
-
def _ps_probe_script(candidates) -> str:
|
|
211
|
-
"""Windows
|
|
213
|
+
def _ps_probe_script(candidates, require_ensurepip: bool = False) -> str:
|
|
214
|
+
"""Windows 探测脚本:逐候选校验(版本==3.12.x,require_ensurepip 时加 ensurepip 资格),
|
|
215
|
+
命中输出 path|x.y.z,全灭 exit 1。"""
|
|
216
|
+
check = _PY_BASE_CHECK_CODE if require_ensurepip else _PY_VER_CHECK_CODE
|
|
212
217
|
arr = ",".join(_ps_quote(c) for c in candidates)
|
|
213
218
|
return (
|
|
214
219
|
f"foreach($c in @({arr})){{"
|
|
215
220
|
"if(Test-Path $c){"
|
|
216
|
-
f"$v=& $c -X utf8 -c \"{
|
|
221
|
+
f"$v=& $c -X utf8 -c \"{check}\" 2>$null;"
|
|
217
222
|
"if($LASTEXITCODE -eq 0){Write-Output ($c+'|'+($v -join ''));exit 0}"
|
|
218
223
|
"}}"
|
|
219
224
|
)
|
|
220
225
|
|
|
221
226
|
|
|
222
|
-
def _bash_probe_script(candidates) -> str:
|
|
227
|
+
def _bash_probe_script(candidates, require_ensurepip: bool = False) -> str:
|
|
223
228
|
"""posix 探测脚本:同 Windows 语义。候选为可信常量($HOME 需保留展开,不可 shlex.quote)。"""
|
|
229
|
+
check = _PY_BASE_CHECK_CODE if require_ensurepip else _PY_VER_CHECK_CODE
|
|
224
230
|
items = " ".join(f'"{c}"' for c in candidates)
|
|
225
231
|
return (
|
|
226
232
|
f"for p in {items}; do\n"
|
|
227
233
|
' command -v "$p" >/dev/null 2>&1 || continue\n'
|
|
228
|
-
f' v=$("$p" -X utf8 -c \'{
|
|
234
|
+
f' v=$("$p" -X utf8 -c \'{check}\' 2>/dev/null) || continue\n'
|
|
229
235
|
' echo "$p|$v"\n'
|
|
230
236
|
" exit 0\n"
|
|
231
237
|
"done\n"
|
|
@@ -233,14 +239,19 @@ def _bash_probe_script(candidates) -> str:
|
|
|
233
239
|
)
|
|
234
240
|
|
|
235
241
|
|
|
236
|
-
def probe_python(machine: Machine, candidates, mux: bool = True
|
|
237
|
-
|
|
242
|
+
def probe_python(machine: Machine, candidates, mux: bool = True,
|
|
243
|
+
require_ensurepip: bool = False) -> "tuple[str, str] | None":
|
|
244
|
+
"""在候选中找版本==3.12.x 的远端 python,单次往返;返回 (路径, 版本),全灭返回 None。
|
|
245
|
+
|
|
246
|
+
require_ensurepip=True 用于 setup/doctor 的基座资格审查:无 ensurepip 的 python
|
|
247
|
+
建不了 venv(Debian/Ubuntu 拆包 python3.x-venv 的典型场景),跳过。
|
|
248
|
+
"""
|
|
238
249
|
if machine.is_windows:
|
|
239
250
|
rc, out, _, _ = _ssh_run(machine, PS_REMOTE_CMD,
|
|
240
|
-
build_ps_wrapper(_ps_probe_script(candidates)), 60, mux)
|
|
251
|
+
build_ps_wrapper(_ps_probe_script(candidates, require_ensurepip)), 60, mux)
|
|
241
252
|
else:
|
|
242
253
|
rc, out, _, _ = _ssh_run(machine, "bash -s",
|
|
243
|
-
_bash_probe_script(candidates).encode("utf-8"), 60, mux)
|
|
254
|
+
_bash_probe_script(candidates, require_ensurepip).encode("utf-8"), 60, mux)
|
|
244
255
|
if rc != 0 or not out.strip():
|
|
245
256
|
return None
|
|
246
257
|
line = out.decode("utf-8", "replace").strip().splitlines()[-1].strip()
|
|
@@ -262,6 +273,18 @@ def probe_python_version(machine: Machine, python_path: str, mux: bool = True) -
|
|
|
262
273
|
return out.decode("utf-8", "replace").strip().splitlines()[-1].strip()
|
|
263
274
|
|
|
264
275
|
|
|
276
|
+
def check_remote_pip(machine: Machine, python_path: str, mux: bool = True) -> bool:
|
|
277
|
+
"""检查指定远端 python 是否带 pip(半拉子 venv 检测:venv python 能跑,
|
|
278
|
+
但若创建时 ensurepip 失败过,venv 里就没有 pip)。"""
|
|
279
|
+
if machine.is_windows:
|
|
280
|
+
payload = build_ps_wrapper(f"& {_ps_quote(python_path)} -m pip --version *>$null")
|
|
281
|
+
rc, _, _, _ = _ssh_run(machine, PS_REMOTE_CMD, payload, 30, mux)
|
|
282
|
+
else:
|
|
283
|
+
rc, _, _, _ = _ssh_run(machine, "bash -s",
|
|
284
|
+
f'"{python_path}" -m pip --version >/dev/null 2>&1\n'.encode(), 30, mux)
|
|
285
|
+
return rc == 0
|
|
286
|
+
|
|
287
|
+
|
|
265
288
|
def detect_remote_python(machine: Machine, override: str = "", mux: bool = True) -> "tuple[str, str]":
|
|
266
289
|
"""解析远端 python,返回 (路径, 版本)。基线锁 3.12;machines.json "python" 字段/参数可覆盖(跳过校验)。"""
|
|
267
290
|
key = (machine.ip, override or machine.python)
|
|
@@ -38,6 +38,7 @@ from .executor import (
|
|
|
38
38
|
_check_ascii,
|
|
39
39
|
_ssh_run,
|
|
40
40
|
build_ps_wrapper,
|
|
41
|
+
check_remote_pip,
|
|
41
42
|
probe_python,
|
|
42
43
|
probe_python_version,
|
|
43
44
|
)
|
|
@@ -54,6 +55,8 @@ PBS_ASSETS = {
|
|
|
54
55
|
("Windows", "x86_64"): f"cpython-{PBS_VER}+{PBS_TAG}-x86_64-pc-windows-msvc-install_only.tar.gz",
|
|
55
56
|
("Mac", "arm64"): f"cpython-{PBS_VER}+{PBS_TAG}-aarch64-apple-darwin-install_only.tar.gz",
|
|
56
57
|
("Mac", "x86_64"): f"cpython-{PBS_VER}+{PBS_TAG}-x86_64-apple-darwin-install_only.tar.gz",
|
|
58
|
+
("Linux", "x86_64"): f"cpython-{PBS_VER}+{PBS_TAG}-x86_64-unknown-linux-gnu-install_only.tar.gz",
|
|
59
|
+
("Linux", "aarch64"): f"cpython-{PBS_VER}+{PBS_TAG}-aarch64-unknown-linux-gnu-install_only.tar.gz",
|
|
57
60
|
}
|
|
58
61
|
PBS_DOWNLOAD_BASE = f"https://github.com/astral-sh/python-build-standalone/releases/download/{PBS_TAG}/"
|
|
59
62
|
CACHE_DIR = Path(os.environ.get("XDG_CACHE_HOME", "") or Path.home() / ".cache") / "rrun"
|
|
@@ -109,6 +112,13 @@ def _ensure_local_tarball(os_name: str, arch: str) -> Path:
|
|
|
109
112
|
return path
|
|
110
113
|
|
|
111
114
|
|
|
115
|
+
def _pbs_platform(uname_sm: str) -> "tuple[str, str]":
|
|
116
|
+
"""`uname -sm` 输出 -> PBS_ASSETS 键('Linux x86_64'->('Linux','x86_64'),'Darwin arm64'->('Mac','arm64'))。"""
|
|
117
|
+
parts = uname_sm.split()
|
|
118
|
+
sys_name, arch = (parts + ["", ""])[:2]
|
|
119
|
+
return {"Darwin": "Mac", "Linux": "Linux"}.get(sys_name, sys_name), arch
|
|
120
|
+
|
|
121
|
+
|
|
112
122
|
def _install_standalone(machine: Machine, mux: bool, timeout: float) -> str:
|
|
113
123
|
"""推送 standalone python 到远端并解压,返回基座 python 路径。"""
|
|
114
124
|
if machine.is_windows:
|
|
@@ -123,11 +133,10 @@ def _install_standalone(machine: Machine, mux: bool, timeout: float) -> str:
|
|
|
123
133
|
)
|
|
124
134
|
base_py = BASE_PY_WIN
|
|
125
135
|
else:
|
|
126
|
-
rc, out, _, _ = _ssh_run(machine, "uname -
|
|
127
|
-
arch = out.decode("utf-8", "replace")
|
|
128
|
-
os_name = "Mac"
|
|
136
|
+
rc, out, _, _ = _ssh_run(machine, "uname -sm", b"", 30, mux)
|
|
137
|
+
os_name, arch = _pbs_platform(out.decode("utf-8", "replace"))
|
|
129
138
|
if (os_name, arch) not in PBS_ASSETS:
|
|
130
|
-
raise RuntimeError(f"[{machine.name}]
|
|
139
|
+
raise RuntimeError(f"[{machine.name}] no standalone python build for platform: {os_name} {arch}")
|
|
131
140
|
extract_cmd = (
|
|
132
141
|
'tools="$HOME/.remote-machine"; mkdir -p "$tools" '
|
|
133
142
|
'&& rm -rf "$tools/python312" '
|
|
@@ -149,7 +158,11 @@ def _install_standalone(machine: Machine, mux: bool, timeout: float) -> str:
|
|
|
149
158
|
|
|
150
159
|
|
|
151
160
|
def _build_ps_setup(base_py: str, pkgs: "list[str]", force: bool) -> str:
|
|
152
|
-
"""Windows 一键脚本:建 venv
|
|
161
|
+
"""Windows 一键脚本:建 venv(缺/坏/force 时)→ pip.ini → 装依赖 → 自检。全 ASCII。
|
|
162
|
+
|
|
163
|
+
“坏” = bin/python 在但 pip 不在:上次 venv 创建中途失败(如 ensurepip 缺失)
|
|
164
|
+
留下的半拉子 venv,直接重建(幂等自愈)。
|
|
165
|
+
"""
|
|
153
166
|
pkg_args = " ".join(pkgs)
|
|
154
167
|
imports = ";".join(f"import {_req_import_name(p)}" for p in pkgs) or "pass"
|
|
155
168
|
force_lit = "$true" if force else "$false"
|
|
@@ -159,6 +172,10 @@ $venv="$tools\\venv"
|
|
|
159
172
|
$venvPy="$venv\\Scripts\\python.exe"
|
|
160
173
|
New-Item -ItemType Directory -Force $tools | Out-Null
|
|
161
174
|
if({force_lit} -and (Test-Path $venv)){{Remove-Item -Recurse -Force $venv}}
|
|
175
|
+
if(Test-Path $venvPy){{
|
|
176
|
+
& $venvPy -m pip --version *>$null
|
|
177
|
+
if($LASTEXITCODE -ne 0){{Remove-Item -Recurse -Force $venv}}
|
|
178
|
+
}}
|
|
162
179
|
if(-not (Test-Path $venvPy)){{
|
|
163
180
|
if('{base_py}' -eq ''){{Write-Error "no base python";exit 1}}
|
|
164
181
|
& '{base_py}' -m venv $venv
|
|
@@ -184,6 +201,9 @@ tools="$HOME/.remote-machine"
|
|
|
184
201
|
venv="$tools/venv"
|
|
185
202
|
mkdir -p "$tools"
|
|
186
203
|
if [ "{'1' if force else '0'}" = "1" ]; then rm -rf "$venv"; fi
|
|
204
|
+
if [ -x "$venv/bin/python" ] && ! "$venv/bin/python" -m pip --version >/dev/null 2>&1; then
|
|
205
|
+
rm -rf "$venv"
|
|
206
|
+
fi
|
|
187
207
|
if [ ! -x "$venv/bin/python" ]; then
|
|
188
208
|
"{base_py}" -m venv "$venv"
|
|
189
209
|
fi
|
|
@@ -209,13 +229,20 @@ def setup_machine(host: str, force: bool = False, mux: bool = True,
|
|
|
209
229
|
base_cands = WINDOWS_BASE_CANDIDATES if machine.is_windows else POSIX_BASE_CANDIDATES
|
|
210
230
|
|
|
211
231
|
hit = probe_python(machine, [venv_py], mux)
|
|
212
|
-
|
|
232
|
+
# 半拉子 venv(python 能跑但 pip 不在——上次创建中途失败的残留)也视为待重建;
|
|
233
|
+
# 否则远端脚本删掉坏 venv 后会拿空 base_py 去重建,必然失败
|
|
234
|
+
venv_broken = bool(hit) and not check_remote_pip(machine, hit[0], mux)
|
|
235
|
+
need_create = force or not hit or venv_broken
|
|
213
236
|
base_py = ""
|
|
214
237
|
if need_create:
|
|
215
|
-
|
|
238
|
+
# 基座资格:版本 3.12 且带 ensurepip(缺 ensurepip 的系统 python 建不了 venv,
|
|
239
|
+
# Debian/Ubuntu 未装 python3.x-venv 的典型场景)——不合格则装 standalone 基座
|
|
240
|
+
base_hit = probe_python(machine, base_cands, mux, require_ensurepip=True)
|
|
216
241
|
if base_hit:
|
|
217
242
|
base_py = base_hit[0]
|
|
218
243
|
else:
|
|
244
|
+
print(f"[setup] [{machine.name}] no venv-capable python 3.12 base found; "
|
|
245
|
+
f"installing standalone python ...", file=sys.stderr)
|
|
219
246
|
base_py = _install_standalone(machine, mux, timeout)
|
|
220
247
|
result.installed_standalone = True
|
|
221
248
|
result.created_venv = True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rrun-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Run local scripts on remote machines over SSH via stdin pipes — no escaping/encoding hell. Supports python/powershell/bash, with a unified remote Python 3.12 venv provisioner.
|
|
5
5
|
Author: waqiju
|
|
6
6
|
License-Expression: MIT
|
|
@@ -153,7 +153,7 @@ For `python` scripts, rrun requires a **3.12.x** interpreter on the remote, prob
|
|
|
153
153
|
2. Standalone base: `...\python312\python.exe` / `~/.remote-machine/python312/bin/python3`
|
|
154
154
|
3. Existing installs: `C:\Python\Python312\python.exe` / `python3.12` on PATH
|
|
155
155
|
|
|
156
|
-
If none match, run `rrun setup <host>`. It is idempotent and non-destructive: if the machine lacks Python 3.12, a [python-build-standalone](https://github.com/astral-sh/python-build-standalone) tarball is downloaded to the local cache (`~/.cache/rrun/`) and streamed over ssh stdin — no registry, no PATH changes, no admin rights, and the remote never touches GitHub. The venv gets its own `pip.ini`/`pip.conf` (global pip config untouched) plus the standard packages from the bundled `remote-requirements.txt` (override with `~/.rrun/remote-requirements.txt`).
|
|
156
|
+
If none match, run `rrun setup <host>`. It is idempotent and non-destructive: if the machine lacks a **venv-capable** Python 3.12 — none installed, or the interpreter cannot create venvs (Debian/Ubuntu split `ensurepip` into the separate `python3.12-venv` package) — a [python-build-standalone](https://github.com/astral-sh/python-build-standalone) tarball (Windows/macOS/Linux) is downloaded to the local cache (`~/.cache/rrun/`) and streamed over ssh stdin — no registry, no PATH changes, no admin rights, and the remote never touches GitHub. A half-created venv left by an interrupted earlier setup (python runs but pip is missing) is detected and recreated automatically. `rrun doctor <host>` reports all of these states before you provision. The venv gets its own `pip.ini`/`pip.conf` (global pip config untouched) plus the standard packages from the bundled `remote-requirements.txt` (override with `~/.rrun/remote-requirements.txt`).
|
|
157
157
|
|
|
158
158
|
## Security
|
|
159
159
|
|
|
@@ -5,8 +5,12 @@ import base64
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
7
|
from rrun.executor import (
|
|
8
|
+
_PY_BASE_CHECK_CODE,
|
|
9
|
+
_PY_VER_CHECK_CODE,
|
|
10
|
+
_bash_probe_script,
|
|
8
11
|
_check_ascii,
|
|
9
12
|
_posix_prefix,
|
|
13
|
+
_ps_probe_script,
|
|
10
14
|
_ps_quote,
|
|
11
15
|
_ssh_args,
|
|
12
16
|
build_bash_command,
|
|
@@ -74,6 +78,29 @@ class TestBashCommand:
|
|
|
74
78
|
build_bash_command(args=["中文"])
|
|
75
79
|
|
|
76
80
|
|
|
81
|
+
class TestProbeScripts:
|
|
82
|
+
def test_bash_probe_default_is_version_only(self):
|
|
83
|
+
s = _bash_probe_script(["python3.12"])
|
|
84
|
+
assert "sys.version_info" in s
|
|
85
|
+
assert "ensurepip" not in s # exec 热路径不做基座资格加码
|
|
86
|
+
|
|
87
|
+
def test_bash_probe_require_ensurepip(self):
|
|
88
|
+
s = _bash_probe_script(["python3.12", "/usr/local/bin/python3.12"], require_ensurepip=True)
|
|
89
|
+
assert "import ensurepip" in s
|
|
90
|
+
|
|
91
|
+
def test_ps_probe_require_ensurepip(self):
|
|
92
|
+
s = _ps_probe_script([r"C:\Python\Python312\python.exe"], require_ensurepip=True)
|
|
93
|
+
assert "import ensurepip" in s
|
|
94
|
+
|
|
95
|
+
def test_ps_probe_default_is_version_only(self):
|
|
96
|
+
assert "ensurepip" not in _ps_probe_script([r"C:\py\python.exe"])
|
|
97
|
+
|
|
98
|
+
def test_check_codes_contain_no_quotes(self):
|
|
99
|
+
# bash 探测用单引号包裹检查代码,代码内出现引号会破坏组装
|
|
100
|
+
for code in (_PY_VER_CHECK_CODE, _PY_BASE_CHECK_CODE):
|
|
101
|
+
assert "'" not in code and '"' not in code
|
|
102
|
+
|
|
103
|
+
|
|
77
104
|
class TestPosixPrefix:
|
|
78
105
|
def test_empty(self):
|
|
79
106
|
assert _posix_prefix() == ""
|
|
@@ -30,6 +30,26 @@ class TestLoadRequirements:
|
|
|
30
30
|
assert load_requirements() == ["foo>=1", "bar"]
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
class TestPbsAssets:
|
|
34
|
+
def test_linux_assets_present(self):
|
|
35
|
+
assert ("Linux", "x86_64") in st.PBS_ASSETS
|
|
36
|
+
assert ("Linux", "aarch64") in st.PBS_ASSETS
|
|
37
|
+
|
|
38
|
+
def test_pbs_platform_mapping(self):
|
|
39
|
+
assert st._pbs_platform("Linux x86_64") == ("Linux", "x86_64")
|
|
40
|
+
assert st._pbs_platform("Linux aarch64") == ("Linux", "aarch64")
|
|
41
|
+
assert st._pbs_platform("Darwin arm64") == ("Mac", "arm64")
|
|
42
|
+
assert st._pbs_platform("Darwin x86_64") == ("Mac", "x86_64")
|
|
43
|
+
|
|
44
|
+
def test_pbs_platform_edge(self):
|
|
45
|
+
assert st._pbs_platform("") == ("", "")
|
|
46
|
+
assert st._pbs_platform("FreeBSD") == ("FreeBSD", "")
|
|
47
|
+
|
|
48
|
+
def test_all_mapped_posix_platforms_have_assets(self):
|
|
49
|
+
for uname in ("Linux x86_64", "Linux aarch64", "Darwin arm64", "Darwin x86_64"):
|
|
50
|
+
assert st._pbs_platform(uname) in st.PBS_ASSETS
|
|
51
|
+
|
|
52
|
+
|
|
33
53
|
class TestSetupScripts:
|
|
34
54
|
def test_bash_setup(self):
|
|
35
55
|
s = _build_bash_setup("/usr/bin/python3.12", ["requests>=2", "foo"], force=False)
|
|
@@ -41,6 +61,12 @@ class TestSetupScripts:
|
|
|
41
61
|
def test_bash_setup_force(self):
|
|
42
62
|
assert "[ \"1\" = \"1\" ]" in _build_bash_setup("p", ["requests"], force=True)
|
|
43
63
|
|
|
64
|
+
def test_bash_setup_heals_half_broken_venv(self):
|
|
65
|
+
# 上次 venv 创建中途失败(ensurepip 缺失等)留下的半拉子 venv:检测无 pip 并重建
|
|
66
|
+
s = _build_bash_setup("/usr/bin/python3.12", ["requests"], force=False)
|
|
67
|
+
assert '"$venv/bin/python" -m pip --version' in s
|
|
68
|
+
assert 'rm -rf "$venv"' in s
|
|
69
|
+
|
|
44
70
|
def test_ps_setup_ascii_and_content(self):
|
|
45
71
|
s = _build_ps_setup("C:\\py\\python.exe", ["requests"], force=False)
|
|
46
72
|
assert "$true" not in s and "$false" in s
|
|
@@ -48,3 +74,6 @@ class TestSetupScripts:
|
|
|
48
74
|
|
|
49
75
|
def test_ps_setup_force(self):
|
|
50
76
|
assert "if($true" in _build_ps_setup("C:\\py\\python.exe", ["requests"], force=True)
|
|
77
|
+
|
|
78
|
+
def test_ps_setup_heals_half_broken_venv(self):
|
|
79
|
+
assert "-m pip --version" in _build_ps_setup("C:\\py\\python.exe", ["requests"], force=False)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|