Packages
A package is the unit you write, version, and share. You can make one and publish it without knowing much about the shop — this page is what a package author needs; the Shop page covers how the shop finds and builds them.
What a package is
A directory with a package.json manifest and some combination of modules, programs,
native modules, assets, and package-private helpers.
mypackage/
├── package.json
├── main.ce
├── utils.cm
├── helper/
│ └── math.cm
├── render.c
├── source/
│ └── support.c
└── internal/
└── helpers.cm
A package’s identity is its locator — the canonical name it is reached by, such as
gitea.example/alice/image. The manifest does not assign a name; the shop entry does. For
development, pit shop link <identity> <directory> explicitly associates that identity
with an editable root.
Nothing declares the contents either. render.c is mypackage::render because of where it
sits, helper/math.cm is mypackage::helper/math for the same reason, and adding a module
is adding a file. Listing the directory is reading the list.
The manifest has three fields and no others:
| field | says |
|---|---|
aliases | the packages this one depends on, with their version selectors |
modules | a logical module name and its implementation per target |
compilation | CFLAGS and LDFLAGS, with per-target overrides |
There is no name, because package identity comes from the shop entry that selected this
root. There is no source-membership list, because a C file’s package-relative path is its
name and source/ is support C by position. There is no members: dependencies and recipe
lists supply package identities, and the shop resolves each through its installed store or
explicit development links. A nested package.json does not make the parent walk invent a
package. One repository may still hold many package roots, but each is linked or installed
independently and can later move on its own.
The unit of distribution
The package is the granularity at which everything about sharing happens:
- it is what gets published and fetched as a unit;
- it is what a version applies to — one selected version and tree per package;
- it is what the lockfile records. A lock pins each package to an exact commit and tree, and every file the build takes from that package comes from that one selected tree.
That last point is why the boundary matters: pinning a package pins everything inside it together, so a package is also the unit of reproducibility. Two files from the same package always come from the same version of it.
Source kinds
| Kind | Extension | Meaning |
|---|---|---|
| Module | .cm | Returns a value |
| Program | .ce | An entry point; returns nothing |
| Extension | .c, .cc, .cpp | Native code the shop provides as a module |
A .cm file must compile as a module and a .ce as a program; the compiler checks that
from the top-level return behaviour.
Support C lives in source/, and the folder is the rule: if your package compiles any C
module, it is compiled with everything in source/. You do not list support files, and
you do not mark a file as one — you put it in the folder or you do not.
There is exactly one such folder. A package does not have a src/ beside a vendor/
beside a source/; a vendored third-party tree goes in source/ like everything else,
minus whatever it carried that is not library code — its own main.c, its tests, its
tooling. The folder is the unit precisely so that a helper and a module can never disagree
about which one a file is.
See Writing a C Module for the native side.
Modules depend, packages do not
A package is a convenience for working on a set of modules together. The dependencies
belong to the modules — use() is written in a module, and it names exactly what that
module needs. A package is just the folder they share and a place to keep aliases.
This is worth being precise about, because it is easy to assume a package “has dependencies” the way it might in another system. It does not, and there is no field with that name. A package of ten modules where each module names a full canonical locator has an empty manifest and works perfectly well.
Locators
A locator is the text a use() names. What a locator looks like is a property of the
shop, not the language — this shop writes one as <package>::<path>:
def sprite = use('gitea.example/alice/renderer::sprite')
The part before :: is the package; the part after is a path within it, and it may contain
/ for subdirectories. That full form is canonical: it names one thing unambiguously,
from anywhere.
:: is the package boundary, and it carries meaning rather than being punctuation. A name
without it is a path, and a path is searched — through your package, your aliases, the
shop’s aliases, and the fallback packages, in that order. Your own files always win. The
whole chain is in Shop.
Aliases
Writing the full package name in every use() is tedious, so a package can declare
aliases. An alias is a short name for a package, and aliases is the field that holds them:
{
"version": "1.0.0",
"aliases": {
"renderer": "gitea.example/alice/renderer",
"mylib": "/Users/me/work/mylib"
}
}
Now the module can write the short form:
def sprite = use('renderer::sprite') // renderer -> gitea.example/alice/renderer
Aliases are scoped to the package that declares them, so your short names cannot collide
with anyone else’s, and an alias resolves to a canonical package identity before anything
else happens — see how a locator resolves. That resolution
matters: an executable is allowed one copy of any module, and it is the identity, not the
alias, that decides whether two use() statements name the same module.
An alias is also where a version selector goes. Your aliases are your dependency pins —
which packages you depend on and which version of each — which is why the field replaces
what other systems call dependencies without adding anything back. The shop declares aliases
too, at its own scope, and those are shop-wide naming rather than anybody’s dependencies.
Never write an alias whose key equals its value. "net": "net" says nothing: if no
alias claims a name, the name already is the package. The one case that tempts you is a
bare single-segment import — use('fetch') is a path, so the chain looks for a file
called fetch, not for the fetch package. Qualify the import instead:
use('fetch::fetch').
Selecting a version
An alias names not just a package but which of its versions you want, and there are three ways to say it:
- A semver selector — a version or constraint like
2.3.1or^2.1. Resolution picks a declared compatible version and pins its commit and tree. This is the usual case. - A tracked ref — a branch or moving tag like
main. Its lock row records the ref and the exact commit it resolved to, and it moves when you update. It can carry a human-facing pseudo-version, but a moving ref makes no compatibility promise. - An exact content selector — a specific commit or tree. It is compatibility-opaque: it names exact bytes and claims nothing about how they relate to a version, so it satisfies a semver requirement only through an explicit assertion, a matching release tag, or a checked protocol.
An alias added without a constraint tracks the main branch; one added with a constraint records it and moves within it. Constraints are written once and stay put; a lock moves when you ask it to.
lock.json is the generated, committed resolution. For every transitive package it records
the canonical locator, the selector kind and selector, the selected version and exact
commit and tree, the target arms chosen, the blob hashes used, and the alias paths
that justified the selection. A build from the same lock resolves to the same bytes, and
the lock is detailed enough that a resolution can be reproduced and explained rather than
merely repeated.
Package-private code
Files under internal/ are private to their package, and that is the only visibility rule
on imports. Another package importing them is rejected:
def helpers = use('internal/helpers') // fine, from a module in the same package
def helpers = use('other::internal/helpers') // denied from another package
The first names a file inside the current package by its path; the second reaches across a
package boundary into another package’s internal/, and that is what is refused. A bare
internal/ path is always your own package’s — it is not carried onward into aliases or
fallbacks — so there is no spelling of somebody else’s internal that could have worked.
That is what makes the privacy structural rather than a check somebody has to remember.
Use it for anything you do not want to be part of your published surface, including the C
a module of yours wraps: internal/time.c is reachable from your time.cm and from
nowhere at all.
Standard packages
A shop names some packages on its fallback list, so every package reaches them by bare
name without declaring anything. The shop this repository ships names two: pitlib, the
modules that are part of the language, and std, the shop’s own standard surface.
That means use('blob') and use('file') work anywhere, while networking, HTTP,
archives, and the rest are ordinary packages you name in full or alias. A different shop may
make a different choice about std. See the Standard Library for what each
contains.
Being reachable and being a fallback are different things. shoplib — nota, qop,
deflate, crypto, and the rest of the shop’s own machinery — is on no fallback list, and
you may still use it. You write shoplib::deflate and say where it came from.
Imports
use() must appear at the top level, assigned directly to a def, with a literal string
locator:
def math = use('pitlib::math/radians') // or just use('math/radians') — pitlib is standard
That restriction is what lets the compiler see every module a module imports, and the builder resolve them before anything runs.
How a locator becomes a file — the chain, the shop’s lists, and target-selected arms — belongs to the shop, and is described in Shop.