Releases: samuelbanapour/hopcli
Release list
hop v0.1.12
Pause on disk-full instead of aborting the transaction
Previously, if any package hit no space left on device during download or extraction, the whole hop install / hop migrate transaction rolled back — discarding everything already fetched and materialised in that batch, even packages with nothing to do with the one that failed. A full disk is different from a bad checksum or a 404: it's a condition the user can fix without losing any work.
Fetch and Materialise now pause and poll free space every few seconds when they hit ENOSPC (or hop's own preflight "won't fit" check), automatically retrying the same step once room frees up — instead of discarding the whole batch. Ctrl+C still cancels a paused install cleanly at any point.
Also in this release
- Fixed a stale module path in the
Makefilethat silently broke-Xversion injection on local builds, somake installalways reportedhop 0.1.0-devregardless of the actual version.
Full Changelog: v0.1.11...v0.1.12
hop v0.1.11
What's new
- hop now requires consent to run. Every command except
hop license,hop self-update, andhop versionchecks for a signed consent token first, and refuses to continue without one — see LICENSE. Government entities are exempt from that requirement. hop also refuses to run on an out-of-date build, so an old copy can't be kept around to dodge a token requirement added after it shipped. hop license accept— self-service: read the terms, give your name and email, and hop records that acceptance and emails a confirmation link. Clicking it reveals a one-time code; entering that code on the license service's/redeemwebpage is the only place the token is ever shown, and it works exactly once. Government-domain emails are auto-flagged as a heuristic, not a hard guarantee.hop menu— pick commands from a numbered menu instead of typing them. A barehopat a real terminal opens it automatically.hop migrate --batch N— splits a Homebrew migration into N-package transactions, so one blocked or oversized package only costs that batch instead of aborting the whole migration.
Full diff: v0.1.10...v0.1.11
hop v0.1.10
⚠ NOTICE: license change
Starting with this release, hop is no longer MIT licensed. Use now requires
the copyright holder's prior written consent — see LICENSE
for the exact terms. Government entities are exempt from that requirement.
This applies to v0.1.10 and every release after it. Versions through v0.1.9
were published under MIT and remain available under those original terms —
that grant is not, and cannot be, revoked retroactively.
What's new
hop menu— pick commands from a numbered menu instead of typing them.
Runhopwith no arguments at a real terminal and it opens automatically;
pick a group, pick a command, answer one prompt for its arguments, and it
runs through the exact same code path as typing the command directly.
Scripts, pipes, and CI still get plain help text, since there's nobody
there to answer a menu prompt.hop migrate --batch N— splits a Homebrew migration into transactions
of N packages instead of one all-or-nothing transaction across everything.
Previously, a single blocked package (a bottle that can't relocate,
running out of disk mid-extract) aborted the whole migration and rolled
back every package already downloaded. With--batch, only that batch is
lost — everything already committed stays installed.
Full diff: v0.1.9...v0.1.10
hop v0.1.9
What's new
- Fixed a real bug where
hopinstalled viascripts/install.shcould never actually be found onPATH.hop shellenvonly ever exportedcurrent/bin— the directory holding installed packages' current-generation binaries. It never exported the directory holding hop's own binary. Sincescripts/install.shplaces hop at$HOP_ROOT/bin/hop(not inside any generation), the documented setup lineeval "$(hop shellenv)"could add every package you install to your shell'sPATH, but never hop itself — leavinghop: command not foundfor anyone who followed the install script's own instructions.shellenvnow resolves the currently-running executable's real directory (os.Executable()+ symlink resolution) and adds it toPATHalongsidecurrent/bin, for every shell it already supports (bash/zsh/sh,fish,csh/tcsh). Verified end-to-end against the exactscripts/install.shlayout.
Known limitation (unchanged, honestly disclosed)
molten-vk and everything that depends on it (mesa, qemu's GTK/Mesa-linked pieces, gstreamer via gtk+3) still can't be relocated into hop's store — the upstream bottle wasn't built with enough Mach-O header room for install_name_tool to rewrite its load commands into hop's (longer) store path. Safely retrofitting more room requires rewriting internal rebase/bind pointer chains throughout the binary, which is out of scope for a binary patcher and risks silent corruption. hop and Homebrew coexist fine — if you need these, keep them on brew for now.
Full diff: v0.1.8...v0.1.9
hop v0.1.8
What's new
hop uninstall-self— removes hop completely: every generation, every store path, the download cache, and finally hop's own binary. A GUI app gets one courtesy step first, its~/Applicationssymlink is unlinked. Irreversible by design, and hop still never touches your shell config — it prints theeval "$(hop shellenv)"line to remove rather than editing~/.zshrcitself.- Fixed a real bug where a stale index could survive any number of binary upgrades.
LoadIndexused to pick whichever index — the on-disk cache or hop's built-in one — simply had more recipes. That let a~/.hop/cache/index.jsonleft over from anHOP_INDEX_URLthat's no longer set keep serving old, already-fixed bugs (thegtk+3OCI auth issue from v0.1.7, specifically) indefinitely, since the stale cache kept winning the recipe-count comparison every time. The cache is now trusted only whenHOP_INDEX_URLis currently set and the cache was actually fetched from that same URL — otherwise hop falls back to its built-in index, no guessing involved.
hop self-update compares version strings, not binary content
Full diff: v0.1.7...v0.1.8
hop v0.1.7
hop v0.1.6
hop can now install and manage macOS GUI apps — what Homebrew calls a cask — as a first-class package kind alongside CLI tools and OS images.
What's new
- GUI apps as packages.
hop install <app>works for a GUI app exactly like it does for a CLI tool: the download is checksum-verified, the.appbundle is copied into hop's content-addressed store, and hop symlinks it into~/Applicationsso Spotlight, Launchpad and Finder can see it. - Every existing command already works with it.
hop rollback,hop alias,hop gc,hop remove,hop verify, andhop doctorall handle a GUI app for free — it's just another installed package, so nothing about those commands needed to change. - Four real casks ship in the built-in index as a working example:
rectangle,stats,maccy,hidden-bar.
The conditions
- macOS only. GUI apps are a macOS concept; a cask recipe simply has no artifact for any other platform, so installing one on Linux fails with the ordinary "no build for this platform" error.
.dmgand.ziponly — not.pkg. A.pkginstaller runs an arbitrary, unsandboxed script as part of installing, which is incompatible with hop's atomic, always-reversible transaction model. hop rejects a.pkg-based cask outright with a clear error rather than running one.- One deliberate exception to "nothing outside Root is ever written." Making an app visible to Spotlight/Finder requires a reference outside hop's own directory, so hop symlinks the
.appinto~/Applications— the only filesystem write hop ever makes outside its own root, and only when a GUI app is actually involved. The symlink still always points back into the store, and hop only ever touches a symlink it can prove is its own — a real application that happens to share a name is never removed or replaced.
Full diff: v0.1.5...v0.1.6
hop v0.1.5
hop v0.1.4
A public-readiness sweep of Homebrew relocation
Every Homebrew formula with a real executable in the catalog was installed
and actually run — not just installed — on both platforms, to find
whatever a clean hop install wouldn't reveal on its own. darwin: 149/149
now install and run correctly. linux: 108/153, with every one of the
remaining 45 individually triaged rather than just counted — see below.
macOS:
@@HOMEBREW_PERL@@was never a recognised placeholder token (the regex
only matchedPREFIX/CELLARby name); broadened to match any
@@HOMEBREW_<NAME>@@form. Fixedautoconfandexiftool, both Perl
scripts with unpatched shebangs.LC_RPATHload commands were never patched at all (only
LC_LOAD_DYLIB/LC_ID_DYLIBviaotool -L/-D) —fbthrift,
watchman, andjasperreference sibling libraries via
@rpath/libX.dylibwith a placeholderLC_RPATHentry patched nowhere.gdk-pixbuf-csourcefailed withSymbol not found: a bare
@@HOMEBREW_PREFIX@@/lib/libgdk_pixbuf-2.0.0.dylibself-reference
(Homebrew's own convention for a keg-linked library placed directly at
prefix/librather than under its formula's ownopt/Cellar
subdirectory) that placeholder parsing had no case for at all.
Linux, the RPATH-completeness fix and the corruption it caused:
Many formulas' own bottles don't declare an RPATH entry for every
dependency consistently across every file within the same formula
(glib's libgio-2.0.so.0 needs zlib-ng-compat but only
libglib-2.0.so.0 declares it; same pattern for fontconfig/freetype,
neovim/luv, and roughly a third of the catalog) — real Homebrew papers
over this with ldconfig's shared, prefix-wide cache, which hop's
isolated per-package store has no equivalent for. Fixed by computing each
file's complete desired RPATH up front and writing it in exactly one
patchelf invocation rather than several separate ones — confirmed the
hard way that enough sequential rewrites of one file's dynamic section can
corrupt it outright (gapplication segfaulted, not merely failed to
resolve a library, after three).
That consolidation surfaced two further real bugs, both confirmed by a
full gdb backtrace, not guessed at: glibc's own dynamic loader has no
RPATH by design and self-relocates at startup via code that assumes its
own program headers exactly as built — blanket-adding one crashed
_dl_start itself before it even read the program it was asked to start,
so the fix is to never touch it; and glibc was being added to every
file's RPATH whenever it appeared anywhere in the dependency closure
(which is unconditional for every Homebrew-Linux artifact), breaking
ncdu, whose own bottle — confirmed against the untouched copy from
ghcr.io — uses the system's own glibc and never needed Homebrew's.
What's left on Linux, and why it's staying: of the 45 remaining
failures, ~42 are a library Homebrew's own formula metadata doesn't
declare either (libstdc++, libgcc_s, libffi, libxml2, libpcap,
libbz2, libexpat, libtiff, libldap, libgomp) — a minimal
container simply doesn't have them, and neither would a minimal real
machine; brew install would leave the same gap. Two more are confirmed
to originate in Homebrew's own bottles rather than here: neovim's
lpeg binding dlopen()s a literal, non-relocatable
/home/linuxbrew/.linuxbrew/... path baked into the compiled binary, not
an ELF NEEDED/RPATH entry patchelf can touch; and ncdu's bottle
mixes a literal system interpreter with an RPATH fallback to Homebrew's
own (newer) glibc, present in the untouched bottle straight from
ghcr.io, and liable to the same glibc-tunables ABI mismatch on any host
whose system glibc is close enough in version.
Release process: a manual re-run of the release workflow could build
whatever the default branch's current commit happened to be while
publishing it under whatever tag name was typed in, since actions/ checkout never pinned to the given tag — fixed. Release assets also now
carry a plain-English OS/arch label (macOS (Apple Silicon), Linux
(x86_64), ...) next to the raw filename.
Homebrew, for real this time
hop can now pull nearly 200 formulae straight from ghcr.io/homebrew/core
— networking and diagnostics (nmap, iperf3, mtr, rclone), media and
document tooling (ffmpeg, imagemagick, pandoc, exiftool), dev
version managers (pyenv, rbenv, direnv, git-lfs), GNU utilities,
and enough terminal fun (cowsay, figlet, cmatrix) to keep it
interesting.
Getting there took four separate relocation fixes, each found by actually
running the installed binary rather than trusting a clean hop install:
- Revisioned formula versions. A Homebrew formula rebuilt without a
version bump (libssh2at revision 4,zstd,ruby, others) records a
bare version that doesn't match Homebrew's own internal Cellar directory
name, so hop's store path and the binary's own embedded reference to it
diverged —dyld: Library not loaded. Fixed by reading the formula's
real revision and appending it exactly as Homebrew does. - Text-script placeholders. Relocation only ever patched Mach-O
binaries; a plain wrapper script (a Ruby or Python gem'sbin/<name>
launcher) carrying the same@@HOMEBREW_...@@tokens was left untouched. #!/usr/bin/env <interpreter>shebangs. These carry no placeholder
at all — since hop deliberately keeps a dependency-only package like
python@3.14off the user'sPATH, an unrewritten shebang silently fell
through to whatever interpreter (if any) happened to be ambient on the
machine. Now pinned to the exact resolved dependency, the same thing
brew's own installer does for gem-based formulae.- Symlinks into Homebrew's shared
opt/farm. A Python-based CLI's
bundled venv interpreter link climbs out to../opt/<formula>/...,
which doesn't exist once that formula is extracted alone into hop's own
store. This one was a pre-existing bug in the extractor itself, not new
relocation code: a path-traversal guard was validating a symlink's
target against its own containing directory instead of the overall
extraction root, so it silently dropped any relative symlink that
climbed out even one legitimate level — a routine, correct pattern in
real Homebrew bottles (Python's ownbin/python3.14 -> ../Frameworks/...,
notably). Fixed in both the tar and zip extractors.
Ruby is not in the index. It bakes literal, non-placeholder absolute
paths (/opt/homebrew/Cellar/ruby/…) directly into libruby.dylib for its
own VM bootstrap — confirmed with strings on the extracted library and by
running hop's own copy of ruby standalone. That's not a placeholder any
relocation mechanism can patch, so ruby and its one dependent (lolcat)
were removed rather than shipped broken. python@3.14 has no such problem
and is verified working, including ssl and sqlite3.
Also since the above: Python on Linux needed its own fix — Homebrew's
Linux Python bakes a literal, non-placeholder absolute path into its own
sys.base_prefix, unreachable by any ELF/RPATH patch since it's baked
into the interpreter's own C-level bootstrap. Fixed via a wrapper script
(bin/python3.X renamed to bin/python3.X-real, replaced with a small
script that sets PYTHONHOME/PYTHONPATH correctly before execing it)
rather than pyvenv.cfg, which was tried first and found insufficient —
its lookup is relative to how the interpreter was actually invoked, not
the resolved real binary, so it never triggers through a dependent
formula's own launcher. python3.14, pip3.14, and yt-dlp all verified
working on Linux as a result.
Verified end-to-end after every fix: nmap, htop, tig, ffmpeg, git,
shfmt, miller, yt-dlp, rbenv, csvkit, and cowsay all install and
actually run — including full dependency chains (openssl@3, libssh2,
python@3.14, pcre2, lua, and others).
More OS and VM images
28 image recipes now, up from 8:
- BSD:
openbsd-vm,netbsd-isojoinfreebsd-vm— three independent
BSDs, none of them Linux. - More Linux:
opensuse-tumbleweed,rocky-linux,void-linux. - Specialized:
kali-linuxandparrot-security(security testing),
proxmox-ve(a real type-1 hypervisor),tails(Tor-routed, amnesic —
the one image trusted on first use, since Tails publishes only a PGP
signature, no plain checksum manifest). - Every macOS release Apple's own infrastructure still serves: eleven
recipes,macos-lion(10.7.5, 2012) throughmacos-tahoe(26, current)
— nothing older than Lion exists in this index, and none ever will
(Apple has never re-hosted anything pre-Lion in digital form).
Each is verified against its own distro's real checksum manifest, in
whatever format that distro actually publishes — BSD-style
SHA256 (file) = digest, GNU sha256sum, per-file sidecars, or a combined
manifest with several hashes per line.
A lossy index round-trip, fixed for good
genindex, genbrew and genimages each keep their own local
recipe/artifact struct rather than importing hop's real one — deliberately,
so no single generator can be broken by an engine refactor. But each
struct only declared the fields that generator writes, and every
regeneration reads the entire index, decodes it into its own struct, and
writes the whole thing back out. A field missing from one generator's
struct was silently dropped from every other generator's recipes the
instant it ran.
This happened for real, caught by inspecting the committed index directly:
running genbrew after genimages had added kind/sha512/sha1 to the
image recipes stripped "kind":"image" from all 28 of them, since
genbrew's struct had no Kind field. Running genimages again to fix that
then stripped oci_token_url/bin/deps from all 194 Homebrew formulae,
since genimages' struct had none of...