Ohbin – 从GitHub安装工具的uv包装器

Hacker News Top 工具

摘要

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

需要 gh CLI(复用你的认证)和 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) 以 📦 制作。

相似文章

所有软件包都已安装

Lobsters Hottest

Omnibin 是一个 FUSE 文件系统,提供按需访问 nixpkgs 中所有二进制文件的功能,消除了传统软件包安装的需要。

uv 0.12.0

Simon Willison's Blog

uv 0.12.0 对 `uv init` 引入了破坏性变更,默认采用 `src/` 为基础的项目结构,配置 uv_build 后端,并设置脚本别名。该更新鼓励使用现代 Python 项目结构。

oven-sh/bun

GitHub Trending (daily)

Bun 是一个用于 JavaScript 和 TypeScript 应用的全能工具包,提供快速运行时、包管理器和测试运行器,集成在单个可执行文件中。它旨在成为 Node.js 的即插即用替代品,具有显著更快的启动速度和更低的内存使用。