Using a package
You have mc build and an mc.toml (A project). This page adds one thing:
source that did not come from your tree.
mc installandmc upgrade— the compiler's own libraries, and the slim binary that has to fetch them — are the next step of the same milestone (docs/specs/M44.md). Everything on this page works today.
1. Three lines in mc.toml #
[deps]
geo = "1.2.0"
is a minimum, not a pin — and the usual way to write it is not to write it:
$ mc pkg add geo --yes
add geo 1.2.0 -> myproject/mc.toml
fetch geo 1.2.0
url https://github.com/minicompiler/mc-geo/archive/refs/tags/v1.2.0.tar.gz
sha256 ba1924dc0d40b458...
into /Users/me/.mc/libs/geo/v1.2.0/
package geo 1.2.0 -> /Users/me/.mc/libs/geo/v1.2.0/
lock myproject/mc.lock (2 packages)
mc pkg add writes exactly one line into [deps] and leaves every other byte of your mc.toml
alone, and then does what mc pkg sync does. What is actually used is in mc.lock, beside
mc.toml:
[[package]]
name = "geo"
version = "1.2.0"
lib = "geo.mc"
sha256 = "ba1924dc...9776f"
deps = ["mathx"]
Both files go into git. The lock is the reproducible half: it names an exact version and the hash
of its exact bytes, and mc build re-checks that hash every single time.
The version in it is not the newest one published, and that is on purpose. mc uses minimal
version selection: every requirement is a minimum, and what gets selected is the lowest version
that satisfies all of them. Adding a dependency cannot silently upgrade another one, and a build
only moves when you move it — with mc update:
$ mc update --yes # every dependency, inside its own major
$ mc update geo --yes # just this one
2. Angle brackets in the source #
#include <geo/geo.mc> // a named file of the package
#include <geo> // its `lib` entry — the same file, once-only
The rule to remember is one sentence: angle brackets are libraries, quotes are my files. A
quote include is resolved against the includer's directory and [include].paths; an angle include
is resolved against the lock, the libraries this binary ships, and the installed mc package — in
that order, and never against the working directory.
That last part is what makes a build reproducible: a geo.mc you happen to have next to main.mc
cannot become <geo>.
3. Where the tree comes from #
Two roads, and mc build takes the first that exists:
deps/geo/ vendored, checked into your repository
~/.mc/libs/geo/v1.2.0/ installed, one directory per exact version
Vendoring is the fully offline project: deps/ plus mc.lock in git and the build needs nothing
else. Both roads are hashed against the lock, so the choice cannot change your output — that is
asserted byte for byte by make check-pkg.
--libs-dir DIR moves the second road, which is what CI should do so that no job depends on
HOME.
mc pkg vendor takes the first road from the second:
$ mc pkg vendor
vendor geo 1.2.0 -> deps/geo/ (3 files)
vendor mathx 1.0.0 -> deps/mathx/ (2 files)
vendored 2 packages into deps/
It copies mc.toml plus [package].files and nothing else, and then verifies. The object your
project produces afterwards is byte for byte the one it produced from the installation.
mc pkg list says which road each package took, and mc pkg verify rehashes everything without
compiling:
$ mc pkg list
geo 1.2.0 ba1924dc0d40 vendored
mathx 1.0.0 374cae18effd vendored
4. A compiler-module package #
A package can also teach the compiler. It goes in [compiler].modules, in the same spelling:
[compiler]
out = "build/mc-app"
modules = ["<teach/mc_teach.mc>", "user.mc"]
and the project writes the six lines that decide the order:
// user.mc
void user_init() {
teach_init();
}
A package never defines user_init — a compiler holds exactly one, and stacking two packages
that each defined it would be a link error rather than a decision. It exports <name>_init()
instead.
A compiler module is code that runs on your machine at build time. Read it before you pin it; see the warning at the top of reference/packages.md.
5. Publishing one #
A package is a source tree with an mc.toml at its root:
[package]
name = "geo"
files = ["geo.mc", "vec.mc"]
lib = "geo.mc" # what a bare `#include <geo>` means
module = "mc_geo.mc" # the file a COMPILER includes, if there is one
[deps]
mathx = "1.0.0"
Three things about files:
- it is written by hand, because
mchas no directory listing; - it is the input to the tree hash, so forgetting a file changes nothing and adding one changes the hash;
- it is a boundary: a file inside your tree that is not on the list is refused at the
consumer, with
not declared in geo's [package].files.
And one thing about what a package may read: its own tree, the libraries the binary ships, and
the dependencies it declared. Nothing else — not the consumer's files, not an absolute path, not
a package it did not name. #embed is included in that rule.
Registering it #
The road, end to end:
$ mc pkg hash . # the tree hash of this checkout
ba1924dc0d40b4582669baca5a31c34f28dac13753cc0ac2a64be71bb309776f
$ git tag v1.2.0 && git push --tags
Then, once per repository, register it on https://minicompiler.dev/me (GitHub login) — a form
that reads your [package].name off the tag and checks it is not already taken and not reserved.
From then on, a GitHub Release on the tag (not the tag alone) is what publishes a version: the
registry validates it inside the same sandbox mc sandbox run uses and, on success, writes a row
whose sha256 is the tree hash above, never the archive's. A published row never changes
afterward; the one edit allowed is yanked = true, which makes add and update skip the
version without breaking anybody who already locked it.
Publishing a package is the whole road: registering, what a Release publishes,
the validator's report, wiring minicompiler/register-action so every Release publishes itself,
and yank.
A private registry costs nothing: any directory or URL with the same index/<name>.toml
layout is one.
$ mc pkg sync --registry ../our-tap --yes
or [registry] url = "../our-tap" in mc.toml.
sh scripts/pkg-hash.sh . is the same hash in shell, which is how make check-pkg keeps the
compiler and the specification honest about each other.
6. What a build does, and does not do #
$ mc build myproject --libs-dir ~/.mc/libs
compiler build/mc-app.mc -> build/mc-app
compile main.mc -> build/app
mc build never downloads — it reads the lock, finds each tree, hashes it, and compiles —
unless you ask it to, in the one spelling that exists:
$ mc build myproject --sync --yes
fetch geo 1.2.0 https://github.com/minicompiler/mc-geo/archive/refs/tags/v1.2.0.tar.gz
fetch mathx 1.0.0 https://github.com/minicompiler/mc-mathx/archive/refs/tags/v1.0.0.tar.gz
lock myproject/mc.lock (2 packages)
compile main.mc -> build/app
That is mc pkg sync followed by the build: same plan, same hashes, same mc.lock. Leave --yes
off and you get the plan and nothing else — nothing is fetched, no lock is written, nothing is
built. On a fresh clone of a project whose deps/ is not in git, mc build --sync --yes is the
one command that takes it from checkout to binary.
When something is wrong it says so and stops:
$ mc build myproject
mc: geo 1.2.0: vec.mc does not match mc.lock
run: mc pkg verify
$ echo $?
2
Exit 2 is always "the environment is not ready" — a tree that moved, a package that is not there, a lock that no longer answers the manifest — and never "your program does not compile", which stays exit 1. A script can tell them apart.
Next #
- Publishing a package — registering, what publishes a version, the CI action
- reference/packages.md — the exhaustive version: the resolution order, the lock format, the hash, the closure rule, every message
- reference/toml.md —
[deps],[replace],[registry],[package] - Teaching the compiler — what a compiler-module package can actually do