Package management

Reference material, synced from the compiler repo’s spec/. The guide is the gentler path in.

Hale’s v1 package-management surface is small and explicitly git-based. A project declares its direct dependencies in hale.toml; running hale fetch clones each one into vendor/<name>/ and records resolved commit SHAs in hale.lock. import "vendor/<name>" as alias; directives pick the cloned source up via the standard import resolution order (see spec/projects.md).

The fetched tree lives at vendor/ — toolchain-managed, distinct from lib/ which stays for hand-maintained sources the user vendors directly. Both paths work identically through the import resolver (each is just an <importer-dir>/<path>/ hit on path 1), but keeping them physically separate prevents hale fetch from clobbering hand-maintained source on a name collision.

The single design commitment driving the shape:

See spec/decisions.md F.26 for the rationale.

Manifest (hale.toml)

Section titled “Manifest (hale.toml)”

The manifest lives at the project root — the same directory that hosts the top-level .hl sources and (after hale fetch) the vendor/ directory. hale init [dir] writes this skeleton (plus a hello-world seed, a first tests/*_test.hl, and a .gitignore; strictly non-destructive — existing files are kept, only missing ones are filled in; 2026-08-11). It is a TOML file with one required section, [deps]:

[deps]
helpers = { git = "https://github.com/me/helpers", rev = "abc123" }
finance = { git = "https://github.com/me/finance", tag = "v0.1.0" }
ui = { git = "https://github.com/me/ui", branch = "main" }

Each entry’s key is the local namespace the consumer will use to import the dep. The value is a table with one required field and up to one optional pin:

Field Required Description
git yes The clone URL. Any scheme git understands works.
rev no Pin to a specific commit SHA.
tag no Pin to a named tag.
branch no Track a branch (lockfile still pins the resolved SHA).

Setting more than one of rev / tag / branch is a manifest error — the spec must be unambiguous. Setting none uses the remote’s default branch.

There is no [package] table, no top-level metadata, no authors / description / license fields. A project is identified by its directory name and its source.

Pin semantics

Section titled “Pin semantics”

Lockfile (hale.lock)

Section titled “Lockfile (hale.lock)”

Auto-written by hale fetch. Pins every declared dep to a resolved commit SHA so re-cloning is reproducible across machines and across time:

[[dep]]
name = "helpers"
git = "https://github.com/me/helpers"
sha = "abc1234567890abcdef..."
[[dep]]
name = "finance"
git = "https://github.com/me/finance"
sha = "deadbeefcafef00d..."

The lockfile is intended to be committed alongside the manifest. A consumer running hale fetch on a fresh checkout re-clones every dep at the locked SHA, producing the same vendor/ contents the author worked with.

If a dep listed in the manifest has no entry in the lockfile (new dep), hale fetch resolves it freshly and appends to the lockfile. If a dep is removed from the manifest, its lockfile entry is dropped on the next fetch (the lockfile is re-emitted from the current manifest, not edited in place).

The hale.lock shape is owned by the toolchain — manual edits will be overwritten on the next hale fetch. To upgrade or downgrade a dep, edit the manifest and re-run hale fetch.

The hale fetch command

Section titled “The hale fetch command”
hale fetch [repo-root]

repo-root defaults to the current working directory. The behavior, per dep:

  1. First fetch (no vendor/<name>/). Clone the URL into vendor/<name>/, checking out the requested ref (--depth 1 for tag / branch / default-branch; full clone + git checkout for rev).
  2. Re-fetch, lockfile SHA matches current HEAD. No-op — no network call.
  3. Re-fetch, lockfile SHA differs from current HEAD. Run git fetch --tags --prune origin, then git checkout the requested ref. Updates the lockfile with the new resolved SHA.
  4. Re-fetch, no lockfile entry for the dep. Same as case 1 from the consumer’s perspective — the dep is new to this project even if vendor/<name>/ was somehow already present.
  5. Collision with a hand-maintained directory. If vendor/<name>/ exists but has no .git/, hale fetch errors and refuses to overwrite it. Move or delete the directory and re-run. This guards against silently clobbering sources the user vendored by hand (e.g. before adding the dep to hale.toml).

After processing every dep, hale fetch writes a fresh hale.lock. The write is whole-file (no in-place editing) so a partial / failed fetch never leaves a corrupt lockfile.

Exit codes:

Resolution order interaction

Section titled “Resolution order interaction”

The compiler’s import resolver doesn’t know about hale.toml or hale.lock. It only knows that an import "vendor/<name>" as alias; (or import "lib/<name>" as alias;, or any other path) directive looks for source on disk at the paths described in spec/projects.md § Resolution order. hale fetch puts the source at <repo-root>/vendor/<name>/, which is exactly where path-1 of the resolver looks first when the import string is "vendor/<name>".

This separation is deliberate: the fetcher is a small, optional tool that produces an on-disk tree. The compiler treats that tree the same way it treats hand-vendored source under lib/. A project that already vendors its libraries (committed into lib/) can ignore the package manager entirely; a project that uses hale fetch exclusively gets the same compile behavior with less manual maintenance; and a project that does both keeps the trees physically separate so the toolchain never overwrites work it didn’t put there.

Library author conventions

Section titled “Library author conventions”

A library is just a git repository whose root directory holds one or more .hl files. From the consumer’s perspective the clone lands at vendor/<name>/, which becomes one Hale seed (per F.19). What this implies for library authors:

What’s NOT in v1

Section titled “What’s NOT in v1”

Explicit non-features. A future milestone may relax some of these when concrete friction demonstrates the need.

Implementation entry points

Section titled “Implementation entry points”

The compiler’s import-resolution path (which consumes the cloned source) lives in hale-cli’s parse_with_imports / resolve_import / find_workspace_root family — see spec/projects.md § Implementation entry points.