23. Packages and dependencies
A module is a directory in your project. A package is a directory somebody else wrote. The difference is that you did not read it.
That sentence is the whole subject of this chapter and the next. Wolf’s answer to it starts somewhere unglamorous: with a file format that cannot do anything.
23.1 wolf.pkg is data
Here is a project with a dependency. Two directories: the program, and the package it uses.
pkg {
name: "den/logsearch",
version: "0.1.0",
edition: "1",
deps: {
rows: { path: "../rows" },
},
}
use rows
fn main() -> !int {
let w = rows.widest(31, 24)
let r = rows.fit(50, 3)
print("{w} wide, {r} rows")
0
}
The dependency is an ordinary package with a manifest of its own:
pkg {
name: "den/rows",
version: "1.4.0",
edition: "1",
}
and it builds and runs with no step in between:
$ wolf run app/main.lu
31 wide, 47 rows
Read wolf.pkg again and notice what is not in it.
There is no code: the values are strings, integers, bare words, lists, and maps, and that is the entire grammar. A manifest is data, and the consequence is the reason the format is shaped this way: reading a manifest tells you everything a package does to the machine that builds it, because a manifest cannot ask for anything to happen. Chapter 24 is about what that buys and what it costs; this section is about the reading.
name is owner/pkg, scoped from the start so that every name has an
owner. version is dotted numerics. deps maps an alias (the
identifier you write in use) to exactly one source. The alias is
deliberately not the package name: rows is what your source says, and
den/rows is who it is, so a rename upstream is a manifest edit rather
than a search-and-replace through your code.
A dependency’s source is one of two things: a local directory, or a pinned tag in a git repository.
rows: { path: "../rows" }
regex: { git: "https://github.com/forest/regex", tag: "v2.1.0" }
A path dependency is a tree you can edit: the shape a workspace of
related packages takes, and the shape this chapter’s examples use. A
git dependency names a tag, never a branch: a dependency has to be a
point, because a moving target is a subscription, not a dependency.
Two verbs read that graph back to you. wolf tree resolves the whole
thing and draws it:
$ wolf tree --dir app
capability tree (I13)
den/logsearch 0.1.0 (root) caps=[]
└── rows 1.4.0 caps=[]
effective: []
and wolf why answers the question you actually have when a name shows
up in that tree and you do not recognize it:
$ wolf why rows --dir app
den/logsearch (root) -> rows 1.4.0
One arrow here, because the graph is two nodes deep. In a real graph the
chain is the answer: why prints the path from your package to the thing
you did not ask for, which is the difference between deleting a
dependency and finding out who wanted it.
The caps=[] in the tree and the effective: [] under it are §24.3’s
subject; they are printed here because the resolver has to know them
anyway.
Adding one
You can write the deps entry by hand. wolf add writes it for you,
and does one more thing that matters:
$ wolf add rows --path ../rows --dir app
wolf add: created app/wolf.pkg (minimal manifest)
wolf add: rows 1.4.0 — capabilities: none
Two lines, and the second one is the point: wolf add tells you the
version it resolved and the authority the package declares, at the moment
you take it on, unprompted. Compare with the ecosystems most of us came
from, where adding a dependency prints a progress bar.
The edit it made is the edit you would have made:
pkg {
name: "local/app",
version: "0.1.0",
edition: "1",
deps: {
rows: { path: "../rows" },
},
}
and the project it created builds:
$ wolf run app/main.lu
31 wide, 47 rows
It is still your file. wolf add splices an entry into the text and
leaves your comments, your ordering, and your whitespace alone; wolf rm
splices one out. Neither reformats the manifest, because the manifest is
not the tool’s file to own.
wolf add also either lands whole or not at all. If the new dependency
does not resolve, the manifest edit is rolled back before the command
returns, so a failed add cannot leave you with a project that no longer
builds for a reason you did not cause.
Exercise 23-1 (comprehension · prose). Read den/logsearch’s
manifest above. Answer from the file alone: what may this package do to
the machine that builds it, what may it do to the machine that runs it,
and which of those two answers required reading anything other than this
file?
Exercise 23-6 (spelunking · wolf). Run wolf tree and then
wolf why on a project with one dependency, and then run wolf why for
a name that is not in the graph. Report all three exit codes, and say
what the third one is for.
23.3 wolf.sum — the ledger
wolf add wrote a second file, and it is a different kind of file from
the manifest:
rows 1.4.0 - caps=-
One line per resolved dependency (the name, the version, a content
address, and the capability set), under a one-line header telling you
the file is the tool’s to write and not yours to edit. This one’s
content address is -, because rows is a local tree you own and can
edit: pinning a hash to a directory you are editing would make every
save look like an attack. A
dependency fetched from a tag carries a real address in that field, a
b3: digest over the package’s source tree, and that is the field this
section is about.
The ledger is not an input to resolution: nothing in the file changes
which versions a build selects; delete it and the same manifest resolves
to the same graph. Lockfiles in other ecosystems do double duty (they
decide versions and they witness bytes), and the two jobs pull in
opposite directions, which is why “the lockfile is out of date” is a
category of problem there and not here. wolf.sum
has exactly one job: to remember what a human once fetched, so that a
later build can notice if the bytes changed.
Because that is its only job, refreshing it is boring:
$ wolf update --dir app
wolf update: wolf.sum refreshed (1 entry)
$ wolf audit --dir app
capability tree (I13)
den/logsearch 0.1.0 (root) caps=[]
└── rows 1.4.0 caps=[]
effective: []
update re-derived the whole ledger and wrote the same bytes it read,
because nothing had changed; the rendering is sorted and byte-stable by
construction, so a ledger diff in review is a real difference rather than
a formatting one. And audit agreeing silently is the ledger’s normal
state: the world matches what was witnessed.
The interesting case is when it does not. Read the rule the ledger enforces in the compiler’s own words:
$ wolf --explain E1506
E1506: a dependency's content hash does not match wolf.sum
`wolf.sum` is the integrity ledger: for every fetched dependency it
records a content hash over the package's source tree, and every later
build re-derives that hash and compares. A mismatch means the bits on
disk are not the bits the ledger witnessed — an edited store entry, a
moved tag, or a tampered mirror. The build refuses: mirrors and
transports are untrusted by construction, only the hash is. If the
change is intentional (you deliberately updated the dependency), run
`wolf update` to refresh the ledger; otherwise treat the mismatch as
the supply-chain alarm it is.
A moved tag therefore cannot change what your build compiles: the tag is
a name, the hash is the content, and the build compares content. An
author who force-pushes v1.4.0 has not
changed your dependency; they have broken your build, loudly, at the
first command that touches it.
Two details of the hash are deliberate. It is taken over the package’s
source files (.lu, .wolfi, and wolf.pkg), so a stray editor
backup, a .git directory, or a build cache in the tree cannot perturb
a package’s identity. And it is one digest over the whole tree, not one
per file, so there is one number to compare and one number to record.
The last field on the line is the capability set, and it is there for the same reason the hash is: so that a later run can notice a change. That is chapter 24.
Exercise 23-5 (comprehension · prose). An upstream author
force-pushes tag v1.4.0 of den/rows so the same version name now
serves different bytes. Walk the failure: what does your wolf.sum
notice, what does the build do about it, and what would you have to run
to tell wolf the change was intended? Then answer the pointed one: what
does wolf.sum not protect, and who is exposed to that gap?
Exercise 23-8 (design). Determinism has a price: a dependency’s bug fixes do not reach your users until you ask for them. A colleague calls this a security liability and wants automatic upgrades. Take wolf’s side without dodging the liability: name the mechanism that answers it and the reason auto-upgrading is the wrong layer for the answer.