Ohbin – 从GitHub安装工具的uv包装器
摘要
Ohbin是一款Python工具,作为uv的包装器,用于将GitHub发布版二进制文件直接安装到项目中,省去了手工制作包装包的需要。它通过pyproject.toml中的简单声明式配置,自动完成下载、SHA256验证、缓存和执行。
查看缓存全文
缓存时间: 2026/06/05 11:06
prostomarkeloff/ohbin 源码:https://github.com/prostomarkeloff/ohbin
ohbin
声明二进制文件,而不是封装包。
Python 3.11+ (https://www.python.org/downloads/)
许可证:MIT (https://opensource.org/licenses/MIT)
类型检查:pyright (https://github.com/microsoft/pyright)
代码检查:ruff (https://github.com/astral-sh/ruff)
你的项目需要 ripgrep,或 oasdiff,或某个仅以 GitHub 发布形式提供的 Rust 代码检查工具。Python 无法安装它。因此,你只能要么告诉每位开发者“自己去安装”——然后看着版本漂移和 CI 崩溃——要么手写一个下载并验证的封装包,然后将其复制到每个仓库中,为每个工具都做一遍。
ohbin 消除了这种麻烦。在 pyproject.toml 中声明工具;它会在首次使用时被获取,通过固定的哈希值进行 SHA256 校验,按主机缓存,然后执行。只需要一个开发依赖,支持任意数量的工具。
uv add --dev git+https://github.com/prostomarkeloff/ohbin.git
前后对比
❌ 手工编写的封装——每个工具一个完整的包,复制到每个仓库中
# 一个下载并验证的封装 · 约180行 · 下一个工具又要重写
_PLATFORM_ASSETS = {
("linux", "x86_64"): _Asset("ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz", "4cf9f2741e6c..."),
("darwin", "arm64"): _Asset("ripgrep-14.1.1-aarch64-apple-darwin.tar.gz", "24ad767777..."),
# ...再两个,每个 SHA 从发布页手动复制
}
def ensure_binary() -> Path:
asset = _resolve_asset() # platform.machine() 猜测
with _flock(cache / ".lock"): # 并发控制(如果你肯费心)
_download(url, archive) # urllib + 重定向(+重试,如果你肯费心)
_verify_checksum(archive, asset.sha256) # hashlib
_extract(archive, binary) # tarfile, 原子重命名
return binary
# + 一个 wheel 垫片, [project.scripts], 以及一个 [tool.uv.sources] 条目——在每个仓库中
✅ ohbin——一个开发依赖,每个工具一个表
uv run ohbin add BurntSushi/ripgrep --version 14.1.1 --name rg --binary rg
[tool.ohbin.tools.rg]
repo = "BurntSushi/ripgrep"
version = "14.1.1"
binary = "rg"
# + 每个平台一个 [..assets.-] 表 —— 由 `add` 写入,包含校验和等
uv run ohbin run rg -- TODO src/
一个是你需要维护的包。另一个是你需要声明的表。
为什么还需要封装?
uv 无法安装任意的 GitHub 发布二进制文件——这并非疏忽。uv run <name> 解析为 Python 的 console-script 入口点,这是在构建时静态烘焙的 wheel 元数据。没有钩子可以读取配置表并生成命令。因此,必须有某种方式将“发布页上的二进制文件”桥接到“虚拟环境中的命令”。诚实的选项是 (a) 每个工具一个封装包——即上面的重复劳动——或者 (b) 一个通用引擎来读取清单。ohbin 就是 (b):每个工具的细节(仓库、版本、每个平台的资产 + 校验和)存放在 [tool.ohbin.tools.*] 中,一个主要基于标准库的引擎负责所有工具的下载/验证/缓存/执行。
ohbin add 完成枯燥的部分
将其指向一个仓库。它会解析发布版本,为每个平台匹配一个资产,固定每个 SHA256(来自 GitHub API 的 digest,否则通过下载并哈希),然后将其写入你的 pyproject 文件——保留注释和格式,通过 tomlkit:
$ uv run ohbin add BurntSushi/ripgrep --version 14.1.1 --name rg --binary rg
resolving BurntSushi/[email protected] ...
+ linux-x86_64 ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz (downloaded+hashed)
+ linux-aarch64 ripgrep-14.1.1-aarch64-unknown-linux-gnu.tar.gz (downloaded+hashed)
+ darwin-x86_64 ripgrep-14.1.1-x86_64-apple-darwin.tar.gz (downloaded+hashed)
+ darwin-arm64 ripgrep-14.1.1-aarch64-apple-darwin.tar.gz (downloaded+hashed)
wrote [tool.ohbin.tools.rg] to pyproject.toml
--name 设置命令名,当与仓库名不同时使用(ripgrep → rg);--binary 设置存档中可执行文件的名称。命名方案奇怪?清单是事实来源——add 只是填充它;手动修正条目即可。使用 gh CLI(用于认证和速率限制),否则使用公共 REST API(支持 GH_TOKEN / GITHUB_TOKEN)。
ohbin run 完成其余部分
uv run ohbin run rg -- --files # 首次运行:下载 → 验证 → 缓存 → 执行
uv run ohbin run rg -- TODO src/ # 后续运行:直接执行
uv run ohbin which fd # 打印缓存路径(如果需要则下载)
uv run ohbin list # 声明的工具 + 解析的平台
每个命令都接受 --pyproject-file PATH 来指定清单文件;否则会读取最近的包含 [tool.ohbin] 的 pyproject.toml(或 $OHBIN_PYPROJECT),而 add / add-gist 仅写入当前工作目录下的 pyproject。每次运行都会打印 [ohbin] resolved pyproject as <path>,因此目标永远不会是猜测。run 使用 execv 替换进程,因此工具拥有 stdin/stdout、信号和退出码——适用于 CI 和 Make,前缀可以隐藏在变量后面:
RG := uv run ohbin run rg --
search:
$(RG) TODO src/
私有二进制文件——通过 Gist 加密
add 假定发布是公开的。但是你有自己构建的工具,不想放在公开仓库中——比如闭源的代码检查器、供应商提供的二进制文件、内部 CLI。你仍然希望 ohbin run 能正常工作。答案是一个携带二进制文件的密钥 Gist,并使用密码加密。Gist 链接是链接限制的(未列出,不可搜索);密码仅存在于私有仓库中。泄露的链接本身毫无用处——没有密钥,字节就是 AES 垃圾数据,而 ohbin 正是负责解密的。没有 TTL,没有密钥服务器:要撤销,删除 Gist 或更换密码即可。
$ uv run ohbin publish-gist ./dist/mytool --password "$PW"
published mytool (current platform) to https://gist.github.com/you/ab12...
add it with: uv run ohbin add-gist https://gist.github.com/you/ab12...
publish-gist 将二进制文件 gzip 压缩,使用 openssl AES-256-CBC 加密(PBKDF2 / 20 万次迭代),将密文 base64 编码为每个平台一个 gist 文件,并在旁边写入一个 ohbin.json 索引。从各自的机器发布每个平台——传递 --gist <id> 以添加到同一个 gist:
uv run ohbin publish-gist ./dist/mytool-linux --password "$PW" --platform linux-x86_64 --gist ab12...
add-gist 读取索引,固定每个 blob 的不可变 raw_url + 密文 SHA256,并将一个 encrypted = true 的工具写入 pyproject:
$ uv run ohbin add-gist https://gist.github.com/you/ab12... --name mytool
wrote [tool.ohbin.tools.mytool] (encrypted) to pyproject.toml
run it with: uv run ohbin run --password mytool --
运行时,密码来自 --password(在工具名之前——之后的参数会传递给工具),或者来自清单中的 password 字段。run 验证下载的密文 SHA,解密,检查明文 SHA(错误的密码会被干净地捕获,而不是崩溃),然后像其他工具一样缓存并执行:
uv run ohbin run --password "$PW" mytool -- --help
需要
ghCLI(复用你的认证)和openssl在 PATH 中。密码永远不会出现在 argv 中——它通过文件描述符传递给openssl。只有在可以提交密码的情况下才使用add-gist --password存储;否则在运行时传递--password。
工作原理
ohbin run rg -- --version
│
├─ 读取 [tool.ohbin.tools.rg] _manifest (向上查找你的 pyproject)
├─ 为此 os/arch 选择资产 _platform (→ darwin-arm64)
│
├─ 已缓存? ~/.cache/ohbin/rg/14.1.1/rg
│ ├─ 是 ───────────────────────────────┐
│ └─ 否 → flock → 下载 → SHA256 ✓ │
│ _engine │
│ → 解压 (tar/zip/raw) → 添加可执行权限 │
│ ▼
▼ os.execv(binary, ["rg", "--version"]) ◄──────┘
- 缓存 —
$XDG_CACHE_HOME/ohbin/<tool>/<version>/<binary>(默认~/.cache/...)。版本在路径中,因此升级会干净地重新下载,不会与旧版本冲突。 - 并发 — 第一个调用者在
flock下下载;其余等待并重用。在 xdist / 并行 CI 下安全。 - 完整性 — 在解压之前进行 SHA256 检查。不匹配则中止;不会将部分内容放入缓存。
它经受得住网络考验
发布资产托管在可能出问题的 CDN 上;gh 有速率限制;DNS 问题可能打断克隆。每次发布查找和每次下载都会使用指数退避重试——而且真正的 404 不会被视为瞬时故障(这是导致天真的封装包在丢包时错误报告“发布未找到”的错误):
$ uv run ohbin add BurntSushi/ripgrep --version 14.1.1
ohbin: download failed (attempt 1/4): ... Connection reset by peer; retrying in 0.5s
+ linux-x86_64 ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz (downloaded+hashed)
...
这是真实运行中的一行——连接被重置,恢复成功,无需大惊小怪。
进程内使用
需要二进制文件的路径,而不是执行它?同样的清单,一个调用:
from ohbin import ensure
path = ensure("rg") # -> pathlib.Path, 首次使用时下载并验证
发现过程从当前工作目录向上查找最近的包含 [tool.ohbin] 的 pyproject.toml;设置 OHBIN_PYPROJECT 指向特定文件(CI,或从无关目录运行的调用者)。
手工封装 vs ohbin
| 每个工具一个封装包 | ohbin | |
|---|---|---|
| 需要维护的包 | 每个工具一个 | 总共一个 |
| 新工具 | 编写一个新包 | ohbin add |
| 新仓库 | 复制文件 | 一个开发依赖 |
| 校验和 | 从发布页手动固定 | 由 add 自动固定 |
| 网络韧性 | 重新实现(或跳过) | 内置重试 + 退避 |
| 完整性检查 | 每个封装重新实现 | 共享的 SHA256 |
局限性
- 仅 POSIX。 安装锁是
fcntl.flock;引擎在最顶部导入fcntl,因此 Windows 在导入时会失败。 - 四个平台。 linux/darwin × x86_64/arm64 是
add自动解析的。其他平台(windows, musl, riscv)需要手动添加——引擎可以正常运行它们。 - 启发式匹配。
add根据文件名中的 OS/架构标记匹配资产,并优先选择.tar.gz。清单是事实来源;不寻常的方案只需一行修复。
开发
git clone https://github.com/prostomarkeloff/ohbin
cd ohbin && uv sync
make lint-heavy # ruff format + ruff check --fix + pyright
make test-full # 68 个无网络测试(平台/匹配/清单/引擎/加密/gist/重试)
CI 先运行一次 lint,然后是一个 os: [ubuntu, macos, windows] × python: [3.11, 3.12, 3.13, 3.14] 矩阵。
停止复制封装包。开始声明二进制文件。
由 @prostomarkeloff (https://github.com/prostomarkeloff) 以 📦 制作。
相似文章
所有软件包都已安装
Omnibin 是一个 FUSE 文件系统,提供按需访问 nixpkgs 中所有二进制文件的功能,消除了传统软件包安装的需要。
@charliermarsh: 受 Bun 中一些代码的启发,我将 uv 的 ZIP 解压移到了每个存档的单个阻塞任务中(使用同步文件系统操作……
uv 的 ZIP 解压已通过将其移至带有同步文件系统操作的单个阻塞任务中得到优化,灵感来自 Bun 的代码,从而加快了从 PyPI 安装 Jupyter 等包的速度。
uv 0.12.0
uv 0.12.0 对 `uv init` 引入了破坏性变更,默认采用 `src/` 为基础的项目结构,配置 uv_build 后端,并设置脚本别名。该更新鼓励使用现代 Python 项目结构。
@GithubProjects:Twine 通过为您处理上传步骤,使得与他人分享 Python 程序变得轻松。- 适用于任何构建系统…
Twine 是一个 Python 实用工具,通过处理认证和文件传输,简化包上传到 PyPI 的流程,且兼容任意构建系统。
oven-sh/bun
Bun 是一个用于 JavaScript 和 TypeScript 应用的全能工具包,提供快速运行时、包管理器和测试运行器,集成在单个可执行文件中。它旨在成为 Node.js 的即插即用替代品,具有显著更快的启动速度和更低的内存使用。