Hover has a package manager.
hpm ships with the compiler โ the same binary, reachable as hover hpm or the hpm symlink beside it. It installs dependencies, locks them by content hash, and gets out of the way. What follows is what it does and, more usefully, what it deliberately refuses to do.
Three words, kept apart
Most confusion about package managers comes from one word doing three jobs. Hover keeps them separate:
- A directory is the unit of naming. Every
.hvrfile in one directory shares a namespace, and that's what an import names. - A package is the unit of distribution โ a directory tree published as one archive, identified by the hash of its contents.
- An index is a list of pointers. It maps a short name to an archive URL and a hash. It stores no code.
The consequence surprises people arriving from npm or crates.io: hover-lang.org never stores your package. It publishes a static index; the bytes always come from wherever you already put them. There is no account to create and nothing to upload.
Cargo manifests
mkdir my-circuit && cd my-circuit
hover hpm init
hover hpm install hvr-rc@^0.1.0
That writes a dependency into hover.toml, resolves it, downloads it, and records the result in hover.lock. If any step fails, hover.toml is left exactly as it was โ a half-added dependency that can never install would break the next person's build for a reason nobody chose.
Then you use it, and an import names a directory rather than a file:
import <hvr-rc>;
from <hvr-rc> import Divider;
module main<>() [] {
module f = hvr_rc.RCLowPass<1k, 100n>() [vin, vout];
module d = Divider<10k, 10k>() [vin, tap];
}
Compiling never installs anything. Package paths resolve through hover.lock and nothing else. A compile that reaches for the network because a manifest changed is exactly how a build works on one machine and not another.
hover.lock is generated but belongs in version control โ it is what makes a build reproducible. It's rewritten whole and sorted by name, so two machines resolving the same dependencies produce byte-identical lockfiles and the diffs stay readable. In CI, add --locked: a dependency that would have drifted fails the build instead of quietly changing it.
Manifest checks at every dock
Every locked package carries the hash of its contents. A mismatch during install is a hard failure, never a warning โ the code you are about to compile is not the code that was reviewed has no sensible continue-anyway path.
hover hpm verify is a separate command for the same reason, running three checks per package:
- The archive is still reachable, which catches link rot.
- It still hashes to the locked value, which catches a re-uploaded release asset or outright tampering.
- The cached copy hasn't been modified since install.
Install has to stay fast and offline-capable; verify deliberately reaches the network for everything. Folding one into the other would compromise both.
Names get the same treatment. Packages from an index you added yourself are always qualified โ myindex:vendor-parts in the manifest, <myindex:vendor-parts> in an import โ so an added index can never shadow an official name. The 2021 dependency-confusion attack class has nowhere to land.
And hpm index add is the only command in the tool that prompts, because agreeing to trust an index is a real decision. Nothing else does: install runs inside Dockerfiles and CI, where a prompt either hangs forever or teaches everyone to paste --yes into every invocation.
Publishing is a git tag
A package is a directory of .hvr files. That is the entire requirement โ no build step, no compiled artifacts, no per-platform variants.
hvr-rc/
filters.hvr
dividers.hvr
hover.toml # optional, only if it has dependencies of its own
GitHub, GitLab and Codeberg all generate a .tar.gz for every tag at a stable URL, and that URL is already a complete, working package:
[dependencies.hvr-rc]
url = "https://github.com/you/hvr-rc/archive/refs/tags/v0.1.0.tar.gz"
Nothing was uploaded to anyone. That step alone is a published package. Getting a short name so people can write hvr-rc = "^0.1.0" is a separate, optional step: submit one TOML file to an index.
Retiring a version sets yanked = true rather than deleting it โ yanked versions are skipped for new resolutions but still resolve for anyone who already locked them. Deleting a version outright is npm's unpublish, and left-pad is what that looks like from the ground.
One caveat, stated plainly: the official index is live but empty. It's at hover-lang.org/packages/, it publishes a real index.tar.gz, and hpm resolves against it by default โ there is simply nothing in it yet. That costs you less than it sounds like: a package is perfectly usable without a short name. Depend on its archive URL directly and hpm locks it by hash exactly the same way.
Try it without a launch pad
You need no server, no index and no account to test a package end to end:
tar -czf /tmp/hvr-rc-0.1.0.tar.gz hvr-rc
cd /tmp && python3 -m http.server 8000 &
cd my-consumer-project
HOVER_ALLOW_INSECURE_HTTP=1 \
hover hpm install http://127.0.0.1:8000/hvr-rc-0.1.0.tar.gz
hover main.hvr
HOVER_ALLOW_INSECURE_HTTP=1 exists for precisely this. Real URLs must be https, because on a first install there is no recorded hash yet โ which is exactly the moment the content is most trusted and least verifiable.
The full command reference, the manifest and lockfile formats, the import rules and a list of every error message with what to do about it are in the package guide. If you publish something, say so on the repository โ the shipping lanes are empty and the freight is yours to name.