A package manager for the C3 programming language that automates dependency installation and project configuration.
Built with the seali CLI framework.
- Install C3 libraries directly from GitHub or Codeberg (GitHub by default)
- Install as a git checkout or as a zipped
.c3larchive - Automatic
project.jsondependency management, preserving the rest of the file - Rewrites are key-sorted, so a run only changes the file when the dependencies do
- Transitive dependency resolution via
manifest.json - A
project.lock.jsonpinning every install to a commit, andsyncto restore it - Update and remove installed libraries
listto see what is installed, at which revision, and what has driftedinstallto build a C3 program from a repository and keep its binary in$C3PO_HOME/bin,uninstallto delete it again
- C3 compiler (c3c) 0.8.3 or later - to build c3po, and
on
PATHat run time forc3po install gitfor the default install modecurlfor--zip
c3c buildThe executable will be at build/c3po.
c3po [OPTIONS] <COMMANDS>
Commands:
add Add a lib
install Build a repo and install its binary
uninstall Delete an installed binary
remove Remove a lib
update Update libs
sync Add all the locked libs
list List the added libs
Options:
-V, --version c3po version
Run any command with --help for its own options.
Add a library to the project:
c3po add [--origin github|codeberg] [-t <ref>] [--zip] <owner/repo>| Option | Description |
|---|---|
--origin <host> |
Host to fetch from: github or codeberg. Defaults to github. |
-t, --tag <ref> |
Branch, tag or commit to add. Defaults to the repository's default branch. |
--zip |
Add as a zipped .c3l archive instead of a git checkout. |
Example:
c3po add Ecoral360/seali.c3lThis will:
- Clone
https://github.com/Ecoral360/seali.c3lintolib/seali.c3l/ - Read the library's
manifest.jsonto resolve its name and sub-dependencies - Add
libtodependency-search-pathsand the library'sprovidesname todependenciesin yourproject.json - Recursively install any transitive dependencies declared in
vendor.c3po.sources, inheriting--zip(each source carries its own URL, so it names its own host) - Pin the resolved commit of everything installed in
project.lock.json - If your project has a
manifest.jsonof its own, record{"name": "seali", "source": "https://github.com/Ecoral360/seali.c3l"}under itsvendor.c3po.sources
Adding a library that is already present in lib/ re-reads its manifest and
resolves its dependencies without refetching it.
A project with a manifest.json is itself a library, so its dependencies have
to be declared for its own dependents, not just installed locally. c3po add
appends an entry to vendor.c3po.sources in that manifest - the name the
library provides and the URL it lives at - and anyone who runs c3po add on
your library then gets it pulled in transitively.
The URL names the repository only, with no tag or commit: which revision to take
is the dependent's call, pinned in its own lock. Its host decides where the
source is fetched from, so --origin does not carry into transitive installs;
a host c3po does not know is refused rather than guessed at.
Only the repository you asked for is recorded - whatever it drags in is already
declared in its own manifest. c3po remove takes the entry back out again.
Projects without a manifest.json are left alone.
With --zip, the repository archive is downloaded and repacked into a single
lib/<name>.c3l zip file. c3c reads zipped libraries directly, but only when
manifest.json sits at the archive root - the repacking step strips the
wrapper directory that source archives put their contents in, whatever the host
happens to call it.
Zipped libraries are pinned in the lock like any other, so c3po sync restores
the exact revision and c3po update moves them forward - by downloading the
archive of the commit the ref points at now, since there is no checkout to move.
Installing a zipped lib also clears c3c's unpacked copy of it under
<build-dir>/unpacked_c3l/. c3c caches that unpack keyed on the CRC of the
manifest inside the archive alone, and a library's manifest is static
boilerplate - so without this a new revision keeps the same key and the compiler
goes on compiling the previous revision's sources, reporting its new symbols as
not found. Only the project-wide build-dir is read; a per-target setting or
--build-dir on the command line is invisible to c3po, so clear the cache by
hand if you use one.
c3po install [--origin github|codeberg] [--tag <ref>] [-t <target>]... [-O <level>] [--trust <level>] [-o <dir>] [--rename-bin <name>] [--dir <path>] [--cleanup <when>] <owner/repo>
c3po install --path <dir> [-t <target>]... [-O <level>] [--trust <level>] [-o <dir>] [--rename-bin <name>]Installs a C3 program rather than a library, the way cargo install does:
- Clone the repository into a directory of its own,
<dir>/c3po-install-<name>-<stamp> - Restore what its
project.lock.jsonpins, asc3po sync --forcewould - Build each target with
c3c, at-O3 - Leave the executables in
$C3PO_HOME/bin
Nothing about the current directory matters: this installs a program, not a dependency of whatever project you are standing in, and writes nothing there.
With --path, steps 1 and 2 change: there is nothing to clone, and the
directory named is built where it stands. See Installing from a local
directory.
| Option | Description |
|---|---|
--origin <host> |
Host to fetch from: github or codeberg. Defaults to github. |
--tag <ref> |
Branch, tag or commit to build. Defaults to the repository's default branch. |
-t, --targets <name> |
A c3c target to build. Repeatable, built in the order given. Defaults to the project's only executable target. |
-O <level> |
Optimization level, by c3c's own numbering: 0 to 5 for -O0 to -O5, 6 for -Os, 7 for -Oz. Defaults to 3. |
--trust <level> |
Passed to c3c as --trust=<level>: none, include ($include allowed) or full ($exec / exec allowed). Defaults to none. |
-o, --output <dir> |
Where to put the binaries. Defaults to $C3PO_HOME/bin. |
-p, --path <dir> |
Build a directory already on disk instead of a repository. Takes the place of <owner/repo>, and not both. |
--rename-bin <name> |
Install the binary under this name instead of the one c3c builds it with. |
--dir <path> |
Where to download and build. Defaults to the system temp directory. |
--cleanup <when> |
Delete the build directory never, on success, on failure or always. Defaults to success. |
Examples:
c3po install Ecoral360/c3po # -> ~/.c3po/bin/c3poc3po install lmichaudel/c3fmt -t tree-sitter -t c3fmt --trust=fullc3fmt is the case every option above is
for: its tree-sitter target is a prepare step that runs a shell script to
build the static library the formatter links against, and c3c only runs a script
under --trust=full. Naming both targets builds them in that order, and the one
that produces a binary is the one that lands in $C3PO_HOME/bin.
A build directory that is kept is reported, so a failed build can be inspected where it happened. The default keeps it on failure only.
Binaries go in $C3PO_HOME/bin, and C3PO_HOME defaults to .c3po under your
home directory. The directory is created as needed; putting it on your PATH
is up to you, and c3po says so once when it is not.
With no -t, the repository's project.json has to declare exactly one
executable target, and that is the one built. c3c would pick a target itself,
but its rule is "the first executable in the file" and the order of a JSON
object is not something to lean on, so c3po names the target explicitly. A
project declaring several executables is reported with the names to choose from.
-t takes any declared target, not only executables, which is what makes a
prepare step usable as the first half of an install. A target that produces no
binary is reported as Built <target> and its output is left in the build
directory rather than in $C3PO_HOME/bin, which only ever receives executables.
An install where no target produced a binary says so.
c3c names what it builds after the target, which is not always what you want to
type. --rename-bin names the installed file instead:
c3po install Ecoral360/c3_ls --rename-bin c3-lsp # -> ~/.c3po/bin/c3-lspIt is a file name, not a path: the binary still goes to $C3PO_HOME/bin, or
wherever -o says. The build itself happens in a directory of its own inside
that one and the binary is moved into place afterwards, so a binary already
installed under the name c3c would have used is not disturbed - only the name
asked for is replaced, and only once the build has succeeded.
Exactly one of the targets being built has to produce a binary, since there is
one name to give out; an install building two executables, or none at all, is
reported before anything is compiled. c3po uninstall then takes the new name.
c3po install --path . # the project you are standing in
c3po install -p ~/src/c3fmt -t tree-sitter -t c3fmt --trust=full--path builds a directory that is already on disk - a checkout you are working
on, or a repository c3po has no way to reach - and installs its binaries exactly
as the repository form does. It takes the place of <owner/repo>: pass one or
the other, never both and never neither.
The directory is built where it stands, so it is the only form of c3po install
that touches something of yours:
- Missing dependencies are restored into its
lib/, as plainc3po syncwould. The ones already there are left alone, including any you have edited in place--pathnever re-adds over your working copy the way the repository form does.
- Objects go to its own
build/, so a subsequentc3c buildthere is warm rather than starting over. - Nothing is deleted afterwards.
--dirand--cleanupdescribe the directory c3po clones into, which this form never makes, and are ignored.
--origin and --tag are equally beside the point: whatever the directory holds
is what gets built, and moving it to another revision is git's business.
c3po uninstall <name>Deletes a binary c3po install left in $C3PO_HOME/bin, and nothing else -
there is no project state behind an installed program.
<name> is the binary's own name - the c3c target that produced it, or the
name that target sets for itself - and not always the repository it came from:
c3po install Ecoral360/dessert.c3l installs example, so c3po uninstall example is what removes it. The binaries that are there are listed when the
named one is not.
c3po remove <name>Deletes the library from lib/, drops it from dependencies and from
project.lock.json, and - if your project has a manifest.json - removes its
entry from vendor.c3po.sources, matched on the entry's name. <name> is the
dependency name (the manifest's provides); c3po scans lib/ for it when the
install directory is named differently.
Each of the four is independent, so a library left behind in only one of them still gets cleaned up.
c3po update [-n <name>]Moves every added library to the tip of the ref it tracks, or a single one with
-n, and repins project.lock.json to whatever it landed on.
Checkouts are fetched and checked out detached rather than pulled: one restored
by sync sits on a pinned commit with no upstream to pull from, and this leaves
both kinds of install in the same shape.
A zipped library has no checkout to move, so it is replaced by the archive of the commit its ref points at now. The lock is what says which repository and ref that is, so a zip the lock knows nothing about is skipped. The host is asked where the ref points before anything is deleted: when it cannot answer - an unreachable host looks the same as a ref that no longer resolves - the installed archive is left alone rather than removed for a download that would then fail.
c3po sync [--force]Adds what project.lock.json pins, at the exact commit recorded, skipping
anything already present. This is the reproducible-checkout path:
git clone <your project> && cd <your project>
c3po sync
c3c build--force re-adds libraries that are already there, replacing whatever is in
lib/. A library that cannot be restored is reported and the rest are still
added; the exit status is non-zero if any failed.
c3po list [--paths]Shows what has been added, one line per library: its name, where it came from, and the revision the lock pins it at.
dessert github.com/Ecoral360/dessert.c3l @ 8a3f697 (zip)
seali github.com/Ecoral360/seali.c3l @ 0ea6819 (missing)
handmade lib/handmade.c3l (not locked)
Added 2 libs
Missing 1 of 2 locked libs, run 'c3po sync' to restore
The two markers are the drift worth knowing about: (missing) is pinned in the
lock but absent from lib/, and (not locked) sits in lib/ with nothing in
the lock about it.
--paths prints the path of each added library and nothing else, one per line,
for feeding to another tool.
Written by add, remove and update. Commit it - it is what makes a checkout
reproducible.
{
"version": 1,
"packages": [
{
"name": "seali",
"repo": "Ecoral360/seali.c3l",
"origin": "github",
"path": "lib/seali.c3l",
"zip": false,
"ref": "",
"commit": "5cf8e31167d35755066d3af586a9067377defc81"
}
]
}name- the library'sprovides, matching its entry independenciesorigin- the host it came from,githuborcodebergref- the branch or tag-tasked for, empty for the default branchcommit- whatrefresolved to at install time;syncchecks out exactly this
Zipped installs are pinned too: c3po resolves the commit with git ls-remote
and downloads that revision's archive. Packages are kept sorted by name, so the
file only changes when the dependencies do.
Every file c3po writes is deterministic. project.json and manifest.json come
back out with their keys sorted alphabetically at every level, and the lock's
packages sorted by name. Keys c3po does not model are carried through a
catch-all map and would otherwise be emitted in hash order, reshuffling the file
on each run and burying the real change in the diff.
Libraries installed by c3po should include a manifest.json:
{
"provides": "library_name",
"sources": ["src/**"],
"vendor": {
"c3po": {
"c3po-version": 1,
"sources": [
{ "name": "dependency_name", "source": "https://github.com/owner/repo" }
]
}
}
}provides- the library name to register inproject.jsondependenciesvendor.c3po.c3po-version- the format version of the block, currently1; c3po stamps it into everyvendor.c3poit writes, and refuses to read a block carrying sources under any other version rather than guess at its shape. A block with no sources needs no version.vendor.c3po.sources- optional list of transitive dependencies to installname- the dependency name that source provides, i.e. its ownprovidessource- the repository URL, without a tag or commit
c3po/
├── src/
│ ├── main.c3 # c3po - entry point, dispatch, exit status
│ ├── cli.c3 # c3po::cli - CLI schema, seali derives the parser
│ ├── commands/
│ │ ├── add.c3 # c3po::cmd - add a lib and what it vendors
│ │ ├── install.c3 # c3po::cmd - build a repo, install its binary
│ │ ├── list.c3 # c3po::cmd - show what has been added
│ │ ├── remove.c3 # c3po::cmd - delete a lib, drop the dependency
│ │ ├── sync.c3 # c3po::cmd - restore what the lock pins
│ │ ├── uninstall.c3 # c3po::cmd - delete an installed binary
│ │ └── update.c3 # c3po::cmd - advance the installed checkouts
│ ├── fetch.c3 # c3po::fetch - git clone, archive download, repack
│ ├── home.c3 # c3po::home - $C3PO_HOME and the bin directory
│ ├── libs.c3 # c3po::libs - lib/ naming, lookup, manifests
│ ├── lock.c3 # c3po::lock - read/write project.lock.json
│ ├── project.c3 # c3po::project - read/rewrite project.json
│ ├── schemas.c3 # c3po::schemas - project.json / manifest.json shapes
│ ├── exec.c3 # c3po::exec - subprocess runner
│ └── text.c3 # c3po::text - ANSI colour formatting
├── lib/ # Added dependencies
├── project.json # C3 project configuration
├── project.lock.json # Pinned revisions, written by add/remove/update
└── build/ # Compiled output