Ohbin – 用于从 GitHub 安装工具的 uv 封装器
Ohbin – uv wrapper for installing tools from GitHub

原始链接: https://github.com/prostomarkeloff/ohbin

**ohbin** 是一个用于管理外部二进制依赖(例如 `ripgrep` 或自定义 Rust 代码检查工具)的工具,这些工具无法通过 Python/pip 进行安装。 无需为每个工具手动创建和维护“下载并校验”的封装包,ohbin 允许你在 `pyproject.toml` 中声明它们。 ### 主要功能: * **声明式清单:** 使用 `ohbin add ` 自动解析 GitHub 发布资产、锁定 SHA256 校验和,并更新你的 `pyproject.toml`。 * **零维护:** 仅需一个 `ohbin` 开发依赖,即可管理任意数量工具的生命周期(下载、校验、缓存和执行)。 * **鲁棒性:** 内置指数退避算法以增强网络弹性,支持并发 CI 环境的文件锁定,以及原子化缓存。 * **私有二进制文件:** 支持通过 GitHub Gist 进行安全、加密的分发,允许你通过 AES-256 加密管理内部或专有工具。 * **直接执行:** `ohbin run ` 提供无缝体验,通过替换进程使工具能够正确继承信号和 I/O,非常适合 CI/CD 流水线。 通过将二进制管理集中在一个配置表中,ohbin 消除了冗余的封装脚本,并确保了开发环境间工具版本的一致性。

Hacker News 最新 | 过往 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Ohbin – 用于安装 GitHub 工具的 uv 封装器 (github.com/prostomarkeloff) 7 分,由 notmarkeloff 发布于 2 小时前 | 隐藏 | 过往 | 收藏 | 4 条评论 帮助 pseufaux 8 分钟前 | 下一条 [–] 但这难道不是 uv 内置的功能吗?只需将 sources 表指向 GitHub 即可。 https://docs.astral.sh/uv/concepts/projects/dependencies/#pr... 回复 mr_mitm 3 分钟前 | 父级 | 下一条 [–] 据我所知,uv 只安装 Python 包。这个工具是从 GitHub 获取并运行二进制文件。 回复 ramon156 15 分钟前 | 上一条 | 下一条 [–] 如果你打算让大模型来写文档,至少得让它们写给开发者看。这份 README 看起来更像是内部备忘录,或者更像是一个推介文案,我觉得挺奇怪的。 回复 _ZeD_ 29 分钟前 | 上一条 [–] 来自原文 uv run ohbin run rg -- TODO src/ 耶 回复 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

Your project needs ripgrep, or oasdiff, or some Rust linter that ships only as a GitHub release. Python can't install it. So you either tell every developer "go install it yourself" — and watch versions drift and CI break — or you hand-write a download-and-verify wrapper package, and copy it into every repo, for every tool.

ohbin deletes that. Declare the tool in pyproject.toml; it's fetched on first use, SHA256-checked against a pinned hash, cached per host, and exec'd. One dev-dependency. Any number of tools.

uv add --dev git+https://github.com/prostomarkeloff/ohbin.git

❌ The hand-rolled wrapper — a whole package, per tool, copied into every repo

# a download-and-verify wrapper · ~180 lines · written again for the next tool
_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…"),
    # ...two more, each SHA hand-copied from the release page
}

def ensure_binary() -> Path:
    asset = _resolve_asset()                      # platform.machine() guesswork
    with _flock(cache / ".lock"):                 # concurrency, if you bother
        _download(url, archive)                   # urllib + redirects (+ retries, if you bother)
        _verify_checksum(archive, asset.sha256)   # hashlib
        _extract(archive, binary)                 # tarfile, atomic rename
    return binary
# + a wheel shim, [project.scripts], and a [tool.uv.sources] entry — in every repo

✅ ohbin — one dev-dependency, one table per tool

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"
# + one [..assets.<os>-<arch>] table per platform — written by `add`, checksums and all
uv run ohbin run rg -- TODO src/

One is a package you maintain. The other is a table you declare.


uv can't install an arbitrary GitHub-release binary — and that's not an oversight. uv run <name> resolves to a Python console-script entry point, which is static wheel metadata baked at build time. There is no hook that reads a config table and conjures a command. So something has to bridge "a binary on a release page" to "a command in your venv."

The honest choices are (a) a wrapper package per tool — the duplication above — or (b) one generic engine that reads a manifest. ohbin is (b): the per-tool detail (repo, version, per-platform asset + checksum) lives in [tool.ohbin.tools.*], and a single mostly-stdlib engine does download / verify / cache / exec for all of them.


ohbin add does the boring part

Point it at a repo. It resolves the release, matches one asset per platform, pins each SHA256 (from the GitHub API digest, else by downloading and hashing), and writes it into your pyproject — comments and formatting intact, via 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 sets the command when it differs from the repo (ripgreprg); --binary sets the executable's name inside the archive. Odd naming scheme? The manifest is the source of truth — add just fills it; fix an entry by hand. Uses the gh CLI when present (auth, rate limits), else the public REST API (GH_TOKEN / GITHUB_TOKEN honored).


uv run ohbin run rg -- --files       # first run: download → verify → cache → exec
uv run ohbin run rg -- TODO src/     # next runs: straight to exec
uv run ohbin which fd                 # print the cached path (downloads if needed)
uv run ohbin list                     # declared tools + resolved platforms

Every command takes --pyproject-file PATH to target a specific manifest; reads otherwise discover the nearest pyproject.toml with [tool.ohbin] (or $OHBIN_PYPROJECT), while add / add-gist write only to the CWD's pyproject. Each run prints [ohbin] resolved pyproject as <realpath> so the target is never a guess.

run replaces the process with execv, so the tool owns stdin/stdout, signals, and the exit code — drop-in for CI and Make, where the prefix disappears behind a variable:

RG := uv run ohbin run rg --
search:; $(RG) TODO src/

Private binaries — encrypted, through a gist

add assumes the release is public. But you have a tool you built yourself and don't want on a public repo — a closed-source linter, a vendored binary, an internal CLI. You still want ohbin run to just work.

The answer is a secret gist carrying the binary encrypted with a password. The gist link is link-gated (unlisted, not searchable); the password lives only in a private repo. A leaked link alone is useless — the bytes are AES garbage without the key, and ohbin is what decrypts them. No TTL, no key server: to revoke, delete the gist or rotate the password.

$ 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 gzips the binary, encrypts it (openssl AES-256-CBC, PBKDF2 / 200k iters), base64s the ciphertext into one gist file per platform, and writes an ohbin.json index next to them. Publish each platform from its own machine — pass --gist <id> to add to the same gist:

uv run ohbin publish-gist ./dist/mytool-linux --password "$PW" --platform linux-x86_64 --gist ab12…

add-gist reads the index, pins each blob's immutable raw_url + ciphertext SHA256, and writes an encrypted = true tool into 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 <pw> mytool -- <args>

At run time the password comes from --password (before the tool name — args after it forward to the tool), or from a password field in the manifest. run verifies the downloaded ciphertext SHA, decrypts, checks the plaintext SHA (a wrong password is caught cleanly, not as a crash), then caches and execs like any other tool:

uv run ohbin run --password "$PW" mytool -- --help

Needs the gh CLI (reuses your auth) and openssl on PATH. The password never touches argv — it's piped to openssl via a file descriptor. Store it with add-gist --password only if committing it is acceptable; otherwise pass --password at run time.


ohbin run rg -- --version
   │
   ├─ read [tool.ohbin.tools.rg]                  _manifest   (walks up to your pyproject)
   ├─ pick the asset for this os/arch             _platform   (→ darwin-arm64)
   │
   ├─ cached?  ~/.cache/ohbin/rg/14.1.1/rg
   │    ├─ yes ───────────────────────────────┐
   │    └─ no → flock → download → SHA256 ✓    │   _engine
   │              → extract (tar/zip/raw) → +x │
   ▼                                            ▼
  os.execv(binary, ["rg", "--version"])  ◄──────┘
  • Cache$XDG_CACHE_HOME/ohbin/<tool>/<version>/<binary> (~/.cache/… default). The version is in the path, so a bump is a clean new download that never collides with the old one.
  • Concurrency — the first caller downloads under a flock; the rest wait and reuse. Safe under xdist / parallel CI.
  • Integrity — SHA256-checked before extraction. A mismatch aborts; nothing partial lands in the cache.

Release assets live behind CDNs that hiccup; gh rate-limits; DNS blips mid-clone. Every release lookup and every download retries with exponential backoff — and a real 404 is never mistaken for a transient failure (the bug that makes naive wrappers cry "release not found" on a dropped packet):

$ 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)

That is a real line from a live run — a reset connection, recovered, no fuss.


Need the binary's path, not to exec it? Same manifest, one call:

from ohbin import ensure

path = ensure("rg")   # -> pathlib.Path, downloaded + verified on first use

Discovery walks up from CWD to the nearest pyproject.toml carrying [tool.ohbin]; set OHBIN_PYPROJECT to point at a specific file (CI, or callers running from an unrelated directory).


Hand-rolled wrapper vs ohbin

wrapper package per tool ohbin
Packages to maintain one per tool one, total
New tool write a new package ohbin add
New repo copy the files one dev-dependency
Checksums hand-pinned from the release page auto-pinned by add
Network resilience re-implemented (or skipped) retry + backoff, built in
Integrity check re-implemented per wrapper shared, SHA256

  • POSIX only. The install lock is fcntl.flock; the engine imports fcntl at the top, so Windows fails on import.
  • Four platforms. linux/darwin × x86_64/arm64 are what add auto-resolves. Others (windows, musl, riscv) you add by hand — the engine runs them fine.
  • Heuristic matching. add matches assets by OS/arch tokens in the filename and prefers .tar.gz. The manifest is the source of truth; an unusual scheme is a one-line fix.

git clone https://github.com/prostomarkeloff/ohbin
cd ohbin && uv sync

make lint-heavy     # ruff format + ruff check --fix + pyright
make test-full      # 68 network-free tests (platform / matching / manifest / engine / crypto / gist / retry)

CI runs the lint once, then an os: [ubuntu, macos, windows] × python: [3.11, 3.12, 3.13, 3.14] matrix.


Stop copying wrapper packages. Start declaring binaries.

Made with 📦 by @prostomarkeloff

联系我们 contact @ memedata.com