Ziran packages are Git repositories. There is no central registry: a
dependency is the URL of the repository that holds it, pinned to a full commit
in a committed ziran.lock. A Ziran application is itself a package with a
ziran.toml. make install-user installs the native ziran command; project
commands build and use the compiler pinned by the application lock, and hand
the command to that toolchain's own launcher, so an installed ziran that is
older or newer than the lock behaves exactly like the pinned one.
Starting a project
ziran new DIR writes a project from a template and locks it:
ziran new hello # a command-line program (template cli)
ziran new geometry --template lib # a library with one exported module
ziran new calc --template kryonlabs/kryon # a package's first template
ziran new calc --template kryonlabs/kryon:tui # one of its named templates
ziran new calc --template ../my-templates:service # a local package directory
cli and lib ship with Ziran. Any package can offer templates by listing
directories in its ziran.toml; the first one is the default:
[templates]
app = "templates/app"
tui = "templates/tui"
A template directory holds a ziran.toml and whatever files a project starts
with. In file contents and paths, {{name}} becomes the package name and
{{module}} the same name as an identifier (my-tool becomes my_tool).
--name NAME sets the name when the directory's name is not one, and
--ref REF fetches the template from another branch, tag, or commit.
--overrides FILE copies a validated local configuration into the new
project's ignored ziran.local.toml before locking. Use it with a local
template and overrides for the toolchain and dependencies to build against
existing source checkouts without fetching copies. Relative override paths
are relative to the created project. An existing ziran.local.toml is never
overwritten, including when applying a template with init.
ziran init --template SOURCE applies a template to the project in the
current directory. It never overwrites a file. When the project already has a
ziran.toml, the template's manifest is merged into it: missing tables and
keys are added, string arrays such as module_roots gain the template's
entries, and every other value the project already sets is kept and reported.
That is how an existing command-line program becomes, for example, a Kryon
application: the template adds the dependency, tool = "kryon", and the
[tool.kryon] settings, and keeps the program's own entry.
Building and running a program
In a project directory, run, build, check, and install without a file
work on the project's program:
ziran run # build, then run the entry module's main
ziran run -- one two # arguments after -- go to the program
ziran build # build/NAME
ziran check # check the entry, or a library's exports
ziran install # copy the program to ~/.local/bin/NAME
The program is the main procedure of [package] entry, which takes nothing
or (argc: s32, argv: **u8) and returns nothing or its exit status. Ziran
writes C and compiles it with CC (default cc), honoring CFLAGS,
LDFLAGS, and LDLIBS. [package] links names system libraries to link:
[package]
name = "fetcher"
entry = "src/main.zi"
links = ["curl"]
Names are linker library basenames, such as gio-2.0, gdk_pixbuf-2.0,
or stdc++, without lib, a filename suffix or -l. They start with a
letter, digit or underscore and may then contain letters, digits, underscores,
hyphens, dots and plus signs. Paths, options and shell syntax are rejected.
System links also apply when building an explicit source file with
ziran build --project --target=c --exe, alongside the caller's LDLIBS.
System links apply to the root manifest that declares them; they are not
inherited from a dependency's [package] table. For example, a native consumer
of audio/pipeline and audio/resample declares
links = ["opus", "samplerate", "m"] in its own [package] table.
An audio/opus codec-only consumer needs links = ["opus", "m"].
Portable audio/ogg inspection and audio/wav conversion need no codec
libraries. A custom native host supplies the corresponding linker flags.
ziran install --prefix DIR installs under DIR/bin; [install] bin changes
the command name. The copy replaces an installed program in one step.
A project whose [package] tool names a dependency hands these four commands
to that dependency's project tool instead (see below). Naming a file, as in
ziran check --project src/cell.zi, or passing --target, keeps the command
on that file or target.
Adding a dependency
Pass any spelling of the repository to ziran add:
ziran add https://github.com/kryonlabs/plot.git
ziran add [email protected]:kryonlabs/plot.git
ziran add ssh://[email protected]/owner/repo.git
ziran add codeberg.org/owner/repo
ziran add kryonlabs/plot # OWNER/REPO means GitHub
The dependency is named after the repository (plot; kryon-ui becomes
kryon_ui). Use --name NAME for another name, --ref REF for a branch, tag,
or full commit instead of master, and --source for a repository that is not
a Ziran package. ziran add writes the declaration, locks it, and prints the
modules you can import:
[dependencies.plot]
git = "[email protected]:kryonlabs/plot.git"
ref = "master"
HTTPS and SSH name the same package. Ziran identifies a repository by its canonical HTTPS spelling, so switching a manifest between the two keeps the lock valid, and two packages that reach one repository over different transports share a single locked copy. Ziran fetches over the transport the manifest names; when that fails, for example SSH without a key in CI or HTTPS without credentials for a private repository, it retries once over the other transport. It never prompts for a password. URLs with embedded credentials are rejected.
Importing modules
A dependency's exported modules are imported by the dependency name and the module name, so every import says where it comes from:
#import "plot/Plot"
using Widgets :: #import "kryon/Widgets";
#import "std/text";
std/NAME is the pinned standard library. Modules in your own package use
their short names. A package can also import its own public exports as
PACKAGE/EXPORT, where PACKAGE is its [package] name and EXPORT is a
key in its [exports] table. For example, a package named plot exporting
Plot = "src/module.zi" can import plot/Plot internally. This works both
when building that package directly and when it is a dependency, even if a
consumer gives it a different dependency alias. Private modules still use
short names inside their package; self qualification exposes only public
exports and grants no access to transitive dependencies. Short-name
resolution is unchanged. If a direct dependency alias equals the package's
own name, that alias retains the whole qualified namespace; use short names
for your own modules in that case. ziran pkg list shows each direct dependency, its URL,
ref, commit and checkout, and every import path it provides:
plot [email protected]:kryonlabs/plot.git master 3e1433d0b2c1
checkout /home/me/.cache/ziran/sources/p1f0c…
#import "plot/Plot" src/module.zi
std /home/me/.cache/ziran/sources/p5a7…/std
#import "std/NAME" the standard library
A short import such as #import "Plot" also works. A short name resolves in
the importing package first, then among its direct dependencies' exports, then
in the standard library. When two direct dependencies export the same name,
the short import reports both qualified spellings; import one of them, or
rename one with module_aliases:
[dependencies.Graphics]
git = "https://example.com/graphics.git"
ref = "master"
module_aliases = { Drawing = "GraphicsDrawing" }
Only the dependency's [exports] are visible. Its other modules are private
to it, so their names never collide with yours.
Generated modules
A build step can write modules for your program, such as translations or level tables. Pass their folder to the project command that needs them:
ziran bundle --project --module-path build/gen --entry app:main -o app.zx src/app.zi
ziran check --project --module-path build/gen
module_roots and repeated --module-path DIR have no fixed folder-count
limit. Storage follows the input size, subject to manifest-size, allocation and
operating-system argument limits.
Each --module-path DIR (repeatable) adds DIR/NAME.zi and
DIR/NAME/module.zi to your package's own modules, as if they were under a
module_roots folder. Everything else still resolves from ziran.lock. The
folder belongs to your package only: dependencies never see its modules, so a
generated Value cannot replace a dependency's own Value. A generated
module may not share a name with one of your modules, and the folder may not
lie inside a locked package or the toolchain. Packages come from the lock, so
the standalone NAME=DIR spelling is refused here.
Commands that don't pass the folder never look for it, so lock, update,
fetch and check work on a fresh checkout before the build step has run;
only an import of a module that isn't there yet fails. A project tool that
runs ziran build, run or check for the project receives the same
--module-path arguments. Without --project, --module-path keeps a
command standalone.
Publishing a package
An exported module is a package-owned file. Its public name may differ from
the file name, so a package can keep the short path src/module.zi:
[package]
name = "Plot"
module_roots = ["src"]
[toolchain]
git = "https://github.com/ziranlang/ziran.git"
ref = "master"
[exports]
Plot = "src/module.zi"
Push the repository anywhere Git can reach. Prefer HTTPS URLs in published manifests so fresh clones and CI can fetch without an SSH key.
Package modules receive an internal identity derived from their repository
and commit. Two versions of a transitive dependency can coexist in one build.
The identity is carried through checked IR, saved .zir, portable bundles,
and native output names.
Commands
ziran add URL|OWNER/REPO
ziran lock
ziran update
ziran update plot
ziran update ziran
ziran fetch
ziran pkg list [plot]
ziran pkg path plot --locked
ziran pkg paths plot ziran --locked
ziran check --project
ziran ir --project --entry app:main -o build/ir
ziran build --project --target=c --entry app:main -o build/c
ziran update refreshes every package and the toolchain to their current refs.
ziran update plot refreshes Plot and its transitive dependencies while
keeping unrelated packages pinned. ziran update ziran refreshes only the
compiler toolchain.
--locked requires a matching lock and ignores local development overrides;
--offline requires cached checkouts. The cache is under
$XDG_CACHE_HOME/ziran or ~/.cache/ziran.
Normal fetch and pkg path commands also validate cached tracked payloads.
If an immutable source-cache entry has missing files or staged deletions only,
they restore the locked source from existing Git objects, including with
--locked --offline; no separate repair command or network access is needed.
A matching HEAD alone does not establish that the source is ready. Modified
tracked files, or untracked/ignored data alongside missing files, prevent repair
and produce a package cache ... preserved at diagnostic. Failed entries are
never automatically deleted. Redirected, sparse or flagged checkouts are refused;
explicit local overrides remain user-owned and are never repaired this way.
ziran pkg path returns the checkout selected by the lock, including a unique
transitive dependency. Add --submodules when a platform build needs its
vendored backend source. The command initializes the submodules at the commits
recorded by that package; with --offline, it requires them to be present.
ziran pkg paths PACKAGE... resolves several selectors with one graph checkout
and validation in the selected launcher. It uses the same direct-alias and
unique transitive package-name selection as pkg path, including ziran for
the toolchain, and accepts --locked, --offline, and a trailing --submodules.
Pinned-launcher forwarding and cache validation remain the same as pkg path.
On success stdout is a JSON array in argument order, with repetitions retained:
[{"selector":"plot","path":"/home/user/.cache/ziran/sources/p123"},
{"selector":"ziran","path":"/home/user/Projects/ziranlang/ziran"}]
Each object contains the original selector and its authoritative absolute
path. JSON escaping preserves spaces, quotes, backslashes and control
characters. An empty selection, missing or ambiguous package, graph-validation
failure, or missing requested submodules returns nonzero with no result on
stdout. Resolve the full batch before consuming any paths.
A platform build can also pin a repository that is not a Ziran package, such
as a C library it compiles from source. Mark the dependency with
source = true:
[dependencies.sqlite]
git = "https://github.com/sqlite/sqlite.git"
ref = "master"
source = true
ziran add URL --name sqlite --source writes the same declaration. A source
package is locked, fetched, and cached like any other dependency, and
ziran pkg path sqlite --locked returns its checkout, but it has no
ziran.toml, exports no modules, and cannot declare dependencies or module
aliases. Build scripts use that path instead of a vendor/ submodule.
For local development, an ignored ziran.local.toml can map package names to
working directories:
[overrides]
kryon = "../../kryonlabs/kryon"
ziran = "../../ziranlang/ziran"
Each override is the top of its own Git checkout: ziran lock records the
commit checked out there. An explicitly overridden checkout whose branch has
no first commit is also usable for local development, including an overridden
toolchain. Its lock entry has a development field instead of commit, with
an unborn: identity derived deterministically from the canonical checkout
path. This identifies local source; it does not claim a committed revision or
a reproducible source snapshot. This is an optional field in lock format 1;
older toolchains that require commit reject these development-only entries.
Only a genuine unborn branch qualifies; missing commit objects and invalid Git
metadata are refused. A directory inside another repository, such as an
uninitialized submodule, is refused, because its HEAD would be the enclosing
repository's commit. A root dependency's override selects that source for
all incoming edges, including transitive dependencies with different aliases
or exact revision pins. These edges use one actual checkout and module type
identity. The lock records each edge's declared ref separately, so changing a
ref, URL, alias or source kind in the root manifest still requires ziran lock.
Source-only edits and newer local commits can be used without changing those
declarations; build commands derive local module identities from the actual
checkout HEAD. When several locked revisions select a checkout whose commit
is already in the lock, they use that commit's locked dependency graph before
merging their identities. When an overridden checkout's own manifest declares
other dependencies than the lock records, local commands resolve the graph
from the working manifests in memory, as ziran lock would, keeping every
unchanged pin, and print a note; with --offline a newly declared branch ref
cannot be resolved. Packages without an override and --locked keep the exact
check. The lock on disk remains unchanged. Conflicting overrides for one
source are refused.
After removing overrides, run ziran lock to resolve the declared revisions
again. Development-only entries require their explicit overrides and cannot
be fetched into the immutable cache. --locked rejects a development-only
lock even after overrides have been removed. Once a checkout has its first
commit, ziran lock records the real commit instead. Exact pins select their
declared commits rather than retaining a local development identity or newer
override commit. --locked refuses an override-only ref substitution.
Targeted updates cannot preserve an unrelated unborn package or toolchain's
source snapshot; use ziran lock or a full ziran update while developing
such a graph.
Production builds omit overrides and use the locked Git commits. A host module that calls into the application can use an explicit reverse bridge:
[package]
bridge_modules = ["app"]
This makes only the named app module visible to direct dependencies. It does not change the package's public exports. Native system libraries remain ordinary platform build dependencies.
Project tools and their options
A package can ship a project tool, such as the command that builds an
application with a UI library. It names the tool and declares the options an
application may set for it in its own ziran.toml:
[tool]
project = "build/bin/kryon"
[options]
default_profile = "string"
[options.profiles."*"]
backend = "string!"
codegen = "string=c99"
[options.install]
autostart = "bool=false"
[options] holds top-level keys, [options.NAME] one table, and
[options.NAME."*"] any number of named tables. Each value is "string" or
"bool", followed by ! when it is required or =DEFAULT for a default.
The application sets those options under the dependency's alias, and its
ignored ziran.local.toml may replace any of them on one machine:
[tool.kryon]
default_profile = "desktop"
[tool.kryon.profiles.desktop]
backend = "desktop"
ziran tool kryon COMMAND rebuilds the tool if its sources changed, checks
every setting against the declaration, and reports an unknown or missing key
with its file and line. It then runs the tool from the project root. The tool
reads the merged settings as KEY=VALUE lines from the file named by
ZIRAN_TOOL_OPTIONS, for example default_profile=desktop,
profiles=desktop and profiles.desktop.backend=desktop, so no tool parses
TOML itself. ZIRAN_PROJECT_LINKS contains the validated root system library
names in manifest order, separated by spaces; native project tools append each
as a -l argument. ZIRAN_PROJECT_ROOT, ZIRAN_PROJECT_NAME,
ZIRAN_PROJECT_ENTRY, ZIRAN_PROJECT_LINKS, ZIRAN_PACKAGE_ROOT, ZIRAN_PACKAGE_ID and
ZIRAN_TOOLCHAIN_ROOT describe the project and the tool's checkout.
Handing a project to its tool
[package] tool names the dependency whose project tool runs, builds, checks,
and installs the application, and [install] bin the command name it
installs as:
[package]
name = "example"
entry = "src/app.zi"
tool = "kryon"
[install]
bin = "example"
ziran run, ziran build, and ziran check without a file then run
ziran tool kryon run, build, or check with the remaining arguments, so
ziran run desktop is ziran tool kryon run desktop. ziran install runs
the tool's install command with ZIRAN_INSTALL_PREFIX (~/.local unless
--prefix DIR is given) and ZIRAN_INSTALL_BIN; other arguments are passed
on. The tool decides what else an install includes, such as a desktop entry,
from its own options. The older [install] tool key is rejected with a
message naming its replacement.
Dependency-owned native artifacts
A dependency can declare named artifacts alongside its existing project tool:
[tool]
project = "build/bin/provider"
[artifacts.native]
inputs = ["tools/provider.zi", "src/library.c", "Makefile"]
outputs = ["lib/library.a", "include/library.h", "resources/data.bin"]
requires = ["support:native"]
inputs and outputs are nonempty arrays of package-relative input files and
staging-relative output files. List all recipe inputs, including ignored native
source inputs, configuration, assets and tool build rules; directories and
escaping paths are rejected. artifact.tsv and dependencies.tsv are reserved.
requires is optional and names artifacts through the provider's own locked
dependency aliases; self:NAME names another recipe in the same package.
Unknown selections and recipe cycles fail before invoking the affected provider.
Recipes derive outputs only from declared inputs, dependency artifacts, the
platform, resolved options and keyed toolchain/environment. Consumer identity
is not an input; encode consumer-specific variation as a declared option.
The current host command needs make, git, mktemp, which and sha256sum.
Consumers request the recipe without reconstructing its source or link graph:
ziran pkg artifact provider:native --platform linux-x86_64-gnu
ziran pkg artifact provider:native --platform windows-x86_64-mingw --locked
--platform is a required explicit OS-architecture-ABI identifier, independent
of the compiler backend (--target=c or go). The provider must validate
supported platforms and choose the correct native toolchain. The engine treats
libraries, bundles and resources identically. This command does not alter
project lifecycle delegation or explicit compiler target selection.
Per-invocation options override [tool.Alias] and ziran.local.toml values
in memory, using the dependency's existing declared option types:
ziran pkg artifact provider:native --platform linux-x86_64-gnu \
--option provider:mode=release \
--option 'support:sdk.path=/opt/SDK folder/tool=chain' \
--option provider:enabled=true
Repeat --option Alias:key=value after the platform. key is a top-level key,
group.key, or group.name.key for a declared named table. Alias matching is
case-insensitive and resolves to a locked package identity, so overrides also
reach prerequisite recipes (including self recipes). A prerequisite uses its
root consumer alias when present, otherwise its locked package name; the
provider's requires edge still uses the provider's dependency alias.
Strings are nonempty literal argv values, including spaces, quotes, backslashes
and additional = characters. Boolean values are exactly true or false.
There is no TOML parsing or shell evaluation of invocation values. Quote the
whole argument as required by your shell. Control characters, unknown or
ambiguous packages, undeclared keys, malformed paths and duplicate keys for the
same package are rejected before compiler identification, cache lookup or any
provider runs. Provider-specific value constraints remain the provider's job.
New named tables are ordered deterministically, so reordering invocation
arguments does not change the effective option file or cache identity.
Manifests, local overrides and lock files are never edited by these options.
Artifact providers receive atomic, content-addressed option snapshots so
concurrent invocations cannot overwrite one another's effective values.
The ordinary project tool is built through its existing Makefile and invoked as
artifact NAME --platform TARGET --output STAGE with the existing project,
package, toolchain and resolved ZIRAN_TOOL_OPTIONS environment. Additional
variables are ZIRAN_ARTIFACT_OUTPUT, ZIRAN_ARTIFACT_TARGET, and
ZIRAN_ARTIFACT_DEPENDENCIES. The last names a TSV file with one
dependency:artifact<TAB>absolute-descriptor-path line per requirement. Providers
read those descriptors instead of rebuilding dependencies. They write every
declared output below STAGE, send diagnostics to stderr, return nonzero on
failure, and honor serial build settings (MAKEFLAGS=-j1 and
CMAKE_BUILD_PARALLEL_LEVEL=1). Invocation uses argv, never shell interpolation.
On success stdout contains the absolute path of artifact.tsv in the shared
Ziran artifact cache. Its TSV header records schema, package, artifact,
platform, and key; remaining lines map declared relative output paths to
SHA-256 digests. Resolve each output beside the descriptor. Descriptors and
outputs are published together by directory rename only after all declared
files exist and remain inside staging. A failed build removes staging and
preserves previously published results. Cache hits verify output digests.
Python build-tool consumers
The resolved toolchain provides scripts/ziran_artifacts.py. Build scripts
can load it from the directory returned by their package wrapper's
pkg path ziran; they do not need a copied descriptor parser. For example,
with tool naming that wrapper and project the consuming project directory:
import importlib.util
from pathlib import Path
import subprocess
toolchain = Path(subprocess.check_output(
[tool, 'pkg', 'path', 'ziran'], cwd=project, text=True).strip())
spec = importlib.util.spec_from_file_location(
'ziran_artifacts', toolchain / 'scripts/ziran_artifacts.py')
artifacts = importlib.util.module_from_spec(spec)
spec.loader.exec_module(artifacts)
result = artifacts.request('provider:native', 'linux-x86_64-gnu',
project=project, ziran=tool)
library = result.output('lib/library.a')
request forwards repeated options as literal --option arguments and
uses --locked when the project has no ziran.local.toml. Its optional env
is passed to the selected tool unchanged. Callers own build resource limits
and display isolation. Artifact(absolute_descriptor, artifact=..., platform=...)
also consumes an already requested result. Construction verifies the schema,
identity, unique rows, canonical contained paths and every declared output's
SHA-256 digest. output(name) requires a declared output; path(name) resolves
a contained path such as a provider's include directory. Symlinks cannot
escape the descriptor directory.
metadata(name) reads a declared JSON object whose integer schema is 1
and whose artifact and platform match the descriptor. Its remaining
fields belong to the provider. Renderer layouts, assets, Android editions
and application output names remain provider or application policy.
Providers may expose an ordered link.txt output for foreign native support.
Each row is kind<TAB>value; supported kinds are:
archive: a declared archive output, passed as one argument.libraryorflag: one literal linker argument.pkg-config: shell-word-separated package names, queried with--libs.js-library: a declared output passed after--js-library.embed: a declared output and absolute virtual mount, joined with@, passed after--embed-file.
result.link_flags() expands those rows, followed by each prerequisite's
metadata in the order recorded by dependencies.tsv. Each prerequisite is
verified against its own output paths, artifact name and the same platform.
Cycles are rejected; repeated prerequisites on separate edges retain their
static link order. env also selects PKG_CONFIG and its environment. No
metadata or option is evaluated by a shell.
Use link_flags(include_archives=False) when ordinary imports already
compile this provider's source into the host. It omits only the current
provider's archives; its flags and prerequisite foreign archives remain.
For example, importing a Ziran library and linking its complete native
archive again would duplicate its module globals. A foreign C host instead
uses the default to link that archive once.
Identity includes the locked package graph and toolchain revisions, actual
checkout HEADs, launcher/compiler bytes, native platform/ABI, recipe and input
bytes, resolved options, dependency descriptors, CC executable bytes/version and
relevant native build environment (PATH, CFLAGS, CPPFLAGS, CXX,
CXXFLAGS, LDFLAGS, LDLIBS, AR, LD, RANLIB, PKG_CONFIG,
PKG_CONFIG_PATH, PKG_CONFIG_LIBDIR, SYSROOT, SDKROOT,
MACOSX_DEPLOYMENT_TARGET). CC must name one executable. Any dirty tracked or untracked
checkout in the resolved graph disables reuse; declared ignored inputs are
hashed regardless. Dirty builds receive unique publication directories, so
local edits cannot hit or overwrite clean results. Two independent consumers
with the same graph, options and environment share a clean artifact.
The package-driver entry point is
PkgBuildArtifact(project: string, package: string, artifact: string, target: string) -> s32.
For invocation overrides use
PkgBuildArtifactWithOptions(project: string, package: string, artifact: string, target: string, overrides: []string) -> s32.
Each slice element is the same Alias:key=value string as the CLI; an empty
slice has the original entry point's behavior. CLI dispatch passes its already
resolved graph to the same implementation.
Downstream adoption consists of adding provider-owned recipes and an artifact
command to the existing Ziran project tool, then replacing consumer source/link
reconstruction with this request and descriptor consumption. Platform-specific
sources, link decisions and assets stay in those providers.
Run the focused native fixture independently before adopting provider recipes:
env -u DISPLAY -u WAYLAND_DISPLAY build/bin/ziran run --target=c --root tests tests/package_artifacts_cases.zi -- build/bin/ziran
The regular sh tests/packages.sh build/bin/ziran runner includes this fixture.
It creates only disposable miniature input repositories under build/tests/.