Packages
x2c has one flat namespace, so an included .x library merges its names into
yours. A package keeps its own space. It is compiled once into an archive,
and an importing program reaches its names through an alias.
What a package looks like
A package is a directory named for the package. Only its src/ directory
holds package sources; everything else in the directory consumes it like any
other program.
packages/greet/
Makefile PACKAGE := greet, then include ../package.mk
src/greet.x the entry unit; public surface above #pragma private
tests/ consumers that import the package
examples/
That is the whole layout for a package with no native dependency. One that wraps a C library adds the pin, licence texts, and profile that Wrapping a C Library covers.
The entry unit needs no manifest and no export list:
typedef struct GreetingData {
String subject;
int count;
} *Greeting;
Greeting Greeting.new(String subject);
String Greeting.line(Greeting greeting);
#pragma private
Greeting Greeting.new(String subject) {
Greeting greeting = Scope.malloc(sizeof(struct GreetingData));
greeting.subject = subject;
greeting.count = 0;
return greeting;
}
String Greeting.line(Greeting greeting) {
greeting.count++;
return %"hello, ${greeting.subject} (${greeting.count})";
}
greet.x spells its own names bare. Compiled as a package they become
greet__Greeting, greet__Greeting_new, and greet__Greeting_line, so a
second package may publish its own Greeting in the same program.
Building one
packages/package.mk holds the recipe. A package Makefile is two lines:
PACKAGE := greet
include ../package.mk
make build translates every src/*.x in package mode and produces:
builds/greet.h the generated public header
builds/libgreet.a the archive a consumer links
builds/greet.native.rsp the native arguments a consumer needs, one per
line, empty when none
make test builds and runs tests/test-*.x, and make clean removes
builds/. A package with a pinned native dependency adds a dependency.json
and one more Makefile line, DEPENDENCY_PREFIX_VAR := GREET_PREFIX, naming
the variable that overrides the cache prefix; make prepare then fetches and
builds the dependency into a shared cache outside the worktree and leaves a
deps symlink in the package directory.
Using one
Register the directory that holds packages and import by name:
import "greet" as g;
int main(void) {
g.Greeting greeting = g.Greeting.new("x2c");
printf("%s", %"${greeting.line()}\n");
return 0;
}
x2c build --package-dir packages --output greeter greeter.x
The driver reads the unit’s recorded dependencies, adds the package’s
builds and src directories to the C include path, and links
libgreet.a together with the arguments in greet.native.rsp. It never
rebuilds the package. Build it once, and a read-only package tree still
serves every consumer. A target in x2c.toml can set package-dirs instead
of passing the flag.
as is optional; import "greet"; binds the alias greet. The alias is
only a way to spell names. The generated C always uses the package’s own
prefix.
Both clauses are optional and combine:
import "<package>" [as <alias>] [with <Name> [as <Local>] {, ...}] ;
A with clause drops the alias from names you use often:
import "greet" with Greeting; // bare Greeting
import "greet" with Greeting as Hello; // bare Hello
import "greet" as g with Greeting; // g.Greeting and Greeting
int main(void) {
Greeting greeting = Greeting.new("x2c");
printf("%s", %"${greeting.line()}\n");
return 0;
}
A with name is still only a spelling: Greeting.new compiles to
greet__Greeting_new. Any public name works, functions included. The alias
stays registered, so with never takes it away, and a local variable of the
same name shadows the binding. See the
language reference for what
does and does not cross an import.
An imported package may add a method to an external type. PCRE2, for example,
adds Var.regexpmatch, so value.regexpmatch() resolves after
import "pcre2". A method declared in the consumer still wins. If two
imported packages provide the same receiver method, the call is ambiguous;
call the selected package function explicitly, such as
pcre2.Var_regexpmatch(value).
That external type is often one from the package’s own vendored header, which
crosses the import under its own name. raylib’s Image and Vector2 are
raylib’s, so a consumer writes Image.new(...), reads image.width, and uses
a + b with no with entry for either type. The types come from the header
and the methods from raylib__.
Packages that wrap a C library
packages/yyjson/ is the worked example. It publishes the vendored
src/yyjson-0.12.h because four of its options methods take yyjson’s own flag
types, so a consumer needs the upstream header on its system include path:
make -C packages/yyjson prepare
make -C packages/yyjson build
make -C packages/yyjson test run run-lisp
x2c build --package-dir packages \
--c-include-dir packages/yyjson/deps/include \
--output catalog catalog.x
prepare verifies and builds the pinned native dependency. build creates the
package archive and link file. The three checks exercise the x2c API, example
applications, raw C header, and Lisp bindings. A program imports the ordinary
API; code that needs an unwrapped option calls yyjson’s real declarations
through yyjson-0.12.h, and a runtime Lisp session installs the package’s
group with JsonLisp.install. The package README states which values own
native storage and when borrowed views expire.
A source distribution carries packages/<name>/, including src/,
dependency.json, LICENSES/, its README, examples, and tests, together with
packages/package.mk, packages/dependency.mk, packages/tools/deps.py,
and packages/tools/bundle, which make bundle runs through x2c script.
It does not carry the ignored deps symlink or builds/ output. From the
repository root, produce a source archive with ordinary tar:
package=yyjson
tar --exclude="packages/$package/deps" \
--exclude="packages/$package/builds" \
-czf "$package-source.tar.gz" \
"packages/$package" packages/package.mk packages/dependency.mk \
packages/tools/deps.py packages/tools/bundle
The package directory includes its Makefile; keep any additional source
files or licenses that its build needs. Include other x2c packages it imports
in the same archive, or distribute them separately under a registered package
root. Native dependency sources are fetched and verified from
dependency.json during preparation.
Shared support resolves beside those files, so the bundle needs no Git
checkout. Unpack it into a directory where its sources and build outputs can
remain together. Select an installed compiler and an explicit dependency
cache. Use the native prefix installer for a compiler that can
move independently of its producer checkout. A development build can instead
use the absolute path to builds/0/x2c; keep that compiler’s support tree in
place. A package source archive does not install or bundle the compiler.
mkdir package-sources
tar -xzf yyjson-source.tar.gz -C package-sources
cd package-sources
export X2C_DEPS_DIR="$HOME/.cache/x2c-dependencies"
make -C packages/yyjson prepare build X2C=/path/to/x2c
Register the directory containing the package with --package-dir. Native
compilation and archiving use the same driver actions and retained state as
ordinary builds, keeping the generated public headers, archive, and link flags
together. Package tests that include unittest/test-support.x still need that
repository test support; it is not part of the source-package build contract.
A package over a third-party C library also has to decide what to expose, who owns each returned value, how to preserve the library’s error codes, and whether to provide Lisp bindings. That is Wrapping a C Library.
Package examples
The packages include example applications that use their x2c interfaces without requiring public services:
packages/sqlite/examples/observations.xstores readings and queries slow or failed responses.observation-history.ximports a file batch in one transaction, then reopens the database and reports endpoint history.packages/libcurl/examples/page-titles.xfetches pages concurrently withCurlEasy.get_all;endpoint-report.xhandles each batch response or Error independently, then uses the ordinary single-request operations. Batch results keep input order and borrow their lifetime fromCurlBatch. The call stays on the caller’s thread; the pinned resolver can still block during DNS lookup.examples/packages/http-json-releases/composes libcurl’s response bytes with yyjson parsing.packages/termbox2/examples/incident-filter.xhandles terminal input and rendering. Its standard run is driven through a pseudo-terminal.packages/blis/examples/page-rank.xexpresses PageRank with BLIS operators;risk-report.xshows the broader matrix workflow.packages/libuv/examples/process-report.xsupervises child processes;thread-notify.x,network-report.x,ipc-report.x, anddatagram-report.xexercise wakeups, TCP, named pipes, and UDP; andrelease-checks.xreads produced artifacts through asynchronous files.- raylib’s standard examples render PNG files in memory. Use
run-interactivefor the windowed showcase; package checks do not run it. packages/torch/examples/fit-line.xfits a line by gradient descent through libtorch autograd:x @ w + b,backward, and an in-place update underTorch.no_grad, with every operator temporary reclaimed by the step’sScope.mlp.xtrains a composed model with Adam and reloads it from a checkpoint;mnist.xtrains a convolutional network for one epoch on the MNIST files named byTORCH_MNIST; andjit-infer.xruns a TorchScript model exported from Python. Training and Inference with torch is the chapter for the package.
Run the package-local examples from the repository root:
make -C packages/libcurl run
make -C examples/packages/http-json-releases test
make -C packages/termbox2 run
make -C packages/blis run
make -C packages/libuv run
make -C packages/raylib verify
make -C packages/torch run
SQLite rows and transactions
The SQLite package keeps SQL visible while accepting ordinary x2c values.
Database.open creates a file-backed or ":memory:" connection. Prepare a
statement once, bind a positional List or a Map of exact parameter names,
and iterate copied row Lists. Rows preserve column order and duplicate names;
Statement.columns returns the names separately. Free statements before
closing their connection, with defer beside each acquisition.
import "sqlite" with Database, Statement;
void show_readings(String filename) {
Database db = Database.open(filename);
defer db.close();
Statement query = db.prepare(
"SELECT url, status FROM observation WHERE status >= ? ORDER BY url"
);
defer query.free();
query.bind(%(400));
foreach (List row, query)
printf("%s", %"${row[0]}: HTTP ${row[1]}\n");
}
SQL NULL is Var.null(), distinct from exhausted iteration. Integers retain
SQLite’s signed 64-bit domain; oversized unsigned inputs raise conv-range.
Text becomes String, while blobs and text containing NUL become copied
Bytes. These values survive later steps and statement release within their
ordinary x2c lifetimes. db.transaction(%!() => { ... }) commits on success and
rolls back on Error; nested managed transactions are rejected. SQLite’s own
code, message, and operation remain available in Error details.
SqliteLisp.install adds query, execute, NULL, and byte-list operations to an
embedded Lisp session. Each query or execute call opens its own connection,
so use a database filename to retain changes between calls. Query results are
ordinary nested Lists. The complete pinned raw SQLite API and the same native
handles remain available for advanced operations. See
packages/sqlite/README.md for ownership, the admitted native profile, and
the executable examples. SQLite is verified on macOS and checked separately:
make -C packages/sqlite prepare
make -C packages/sqlite test run run-lisp
Install packages
x2c install places a package under <home>/packages, where every build
finds it without --package-dir:
x2c install pcre2 # a name from the package index
x2c install ./mylib # a local package directory
x2c install ./mylib-1.2.tar.gz # a local archive
x2c install https://example.com/mylib-1.2.tar.gz --sha256 <hex>
x2c list
x2c remove pcre2
A name resolves through the package index, index.txt at
https://x2c-lang.dev/packages/ unless --index <url-or-path> names
another. Each index row is name version kind platform url sha256. The
compiler takes the bundle row for its own platform, otherwise the source row,
downloads the archive with curl, and refuses it when its digest differs. A
URL given directly needs --sha256.
A bundle, the output of make bundle below, installs as built. Its
BUNDLE.json names the x2c version that built it; another version refuses
it unless --force, because a bundle carries no ABI promise across releases.
A source package is a <name>/src/<name>.x tree with no dependency*.json.
The compiler translates it in package mode and archives it, the same two
commands packages/package.mk runs, and records the origin and digest in
SOURCE.json. A directory or archive carries no version of its own, so
installing one over an installed package keeps the version that package
recorded and a first install records none, which x2c list shows as -. A source package with native dependencies is refused; publish
its bundle instead.
Installed packages are not owned by the compiler’s install inventory, so a
compiler upgrade keeps them and x2c remove is the way to delete one. A
directory under <home>/packages without BUNDLE.json or SOURCE.json is
never replaced or removed. Installs and removals in one home run one at a
time. A second one waits until the first finishes and, unless -q is given,
prints a message while it waits.
Upgrade x2c before installing packages by name. Each release publishes the
index that x2c install <name> reads, which lists the bundles built by that
release’s compiler, and a bundle rejects a different compiler version.
Pin packages in a project
A project manifest can list the packages it needs, and a build installs them. Each entry pins one exact version:
[dependencies]
pcre2 = "10.48"
x2c build and x2c run install anything missing through the index before
planning the build, then write x2c.lock beside the manifest recording what
they resolved. Commit that file. A later build whose lockfile is already
satisfied by the installed packages reads no index, so it works offline, and
one that has to reinstall a package takes it from the archive the lockfile
recorded, so everyone gets the same packages.
To change a version, edit the manifest and build again; the lockfile is
rewritten. x2c remove still deletes an installed package, and the next
build reinstalls it. See
project manifests for the rules.
Movable native bundles
make bundle builds a package and assembles its public source interfaces,
generated headers, wrapper archive, licenses, and declared native headers and
static libraries:
make -C packages/yyjson bundle
mkdir native-packages
tar -xzf packages/yyjson/builds/bundle/yyjson-native.tar.gz -C native-packages
x2c build --package-dir native-packages app.x --output app
The directory packages/yyjson/builds/bundle/yyjson is also directly usable;
register its parent with --package-dir. BUNDLE_DIR=/path/to/output changes
the output parent. The directory and archive contain the same files. Move the
whole package directory, keeping src, builds, native, and licenses
together. Extracted bundle, compiler, header/archive, and consumer paths may
contain spaces. Producing packages still follows the existing Make path
restrictions; this does not promise arbitrary spaces in native build/cache
paths.
A bundle is specific to its host platform, architecture, native profile, and
matching x2c compiler/runtime. BUNDLE.json records those build identities.
Native dependency toolchain information comes from its existing cache receipt
when available; an explicit external prefix may have no receipt. A bundle
provides no stable ABI across compiler releases, no cross-platform execution,
and no automatic dependency resolution. Native system libraries and frameworks
remain supplied by the host.
Most bundles link statically. libtorch is a set of shared libraries, so the
torch bundle contains those libraries under native/lib, and a program built
against it records a run-time search path to that directory. The program
loads libtorch from the installed package, so moving or removing that package
breaks the program.
Every package carries builds/<name>.native.rsp. The compiler reads its
quoted native arguments, expands the literal {package} to the resolved
package directory, and applies C include/define options during native
compilation and ordered archives/system options during final linking.
Arguments remain individual argv values even when an expanded path contains
spaces. They are not shell commands and @ does not expand another response
file. Definitions do not change the preceding x2c source preprocessing.
A package built before 0.15.0 carries the earlier <name>.link file instead;
the compiler no longer reads it, so rebuild such a package with make build
or reinstall it.
Native bundle metadata covers yyjson, PCRE2, BLIS, libuv, termbox2, libcurl, raylib, and torch. Releases publish torch for macOS arm64 and Linux x86_64, the platforms PyTorch ships prebuilt CPU libtorch for. SQLite remains separately distributed as source in this release. Pure and mixed C/x2c packages with no external native inputs need no dependency manifest; their bundle carries an empty native response file. Distribute any other x2c package imports alongside them under the registered package root.
Libcurl bundles include its pinned OpenSSL static inputs in link order. Host certificate configuration remains the admitted profile’s system certificate bundle; native bundling does not introduce a certificate store.
Raylib also requires its native type declarations during x2c source discovery. Use the existing source include option for its bundled headers:
x2c build --package-dir native-packages \
--x-include-dir native-packages/raylib/native/include chart.x --output chart
The default raylib bundle uses the headless profile. To produce its separately admitted macOS desktop profile, select its dependency manifest explicitly:
make -C packages/raylib clean-builds
make -C packages/raylib bundle DEPENDENCY_MANIFEST=dependency-desktop.json
Prepare and validate the selected native profile before distributing it. The bundle target preserves existing dependency checksum, header, and license checks through the ordinary package build; consumer builds do not download missing inputs.
Declare a package’s native distribution
The optional distribution object in dependency.json lists selected files
and directories with the existing copies shape and ordered native arguments:
"distribution": {
"copies": [
{"from": "{prefix}/include/yyjson.h",
"to": "native/include/yyjson.h"},
{"from": "{prefix}/lib/libyyjson.a",
"to": "native/lib/libyyjson.a"}
],
"native_args": [
"--c-include-dir", "{package}/native/include",
"{package}/native/lib/libyyjson.a"
]
}
Copy sources expand {prefix} to the prepared native prefix and {package}
to the producer package. Destinations are relative to the bundle. An optional
archive_objects list holds objects the package compiles outside
x2c build, such as torch’s C++ shim; they are added to the bundled
lib<name>.a, so a consumer links them with no further input. Native
arguments retain {package} for consumer-time expansion. An optional
platform_args object appends arguments keyed by the producer’s platform
(darwin or linux) when a shared dependency manifest has platform-specific
system requirements. The manifest selected by the package Makefile remains
the native profile owner. Unless DEPENDENCY_MANIFEST names one, the
Makefile uses the first manifest present among
dependency-<os>-<arch>.json, dependency-<os>.json, and dependency.json,
where <os> is darwin or linux and <arch> is the machine name, such as
arm64 or x86_64.
Accepted options are native include directories (-I, --c-include-dir,
--c-system-dir), -D, -U, -L, -l, --rpath, archive inputs,
-pthread, and -framework <name>. A bundle of shared libraries passes its
library directory to both -L and --rpath, under {package}.
Keep native archives in dependency order after the wrapper archive. Options
that replace the consumer’s output, command, or tools and unrestricted
compiler/linker escape options are not package metadata.