Skip to content

Latest commit

 

History

30 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

c3po 🤖 - C3 Project Orchestrator

A package manager for the C3 programming language that automates dependency installation and project configuration.

Built with the seali CLI framework.

Features

  • Install C3 libraries directly from GitHub or Codeberg (GitHub by default)
  • Install as a git checkout or as a zipped .c3l archive
  • Automatic project.json dependency 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.json pinning every install to a commit, and sync to restore it
  • Update and remove installed libraries
  • list to see what is installed, at which revision, and what has drifted
  • install to build a C3 program from a repository and keep its binary in $C3PO_HOME/bin, uninstall to delete it again

Requirements

  • C3 compiler (c3c) 0.8.3 or later - to build c3po, and on PATH at run time for c3po install
  • git for the default install mode
  • curl for --zip

Build

c3c build

The executable will be at build/c3po.

Usage

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.

c3po add

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.c3l

This will:

  1. Clone https://github.com/Ecoral360/seali.c3l into lib/seali.c3l/
  2. Read the library's manifest.json to resolve its name and sub-dependencies
  3. Add lib to dependency-search-paths and the library's provides name to dependencies in your project.json
  4. Recursively install any transitive dependencies declared in vendor.c3po.sources, inheriting --zip (each source carries its own URL, so it names its own host)
  5. Pin the resolved commit of everything installed in project.lock.json
  6. If your project has a manifest.json of its own, record {"name": "seali", "source": "https://github.com/Ecoral360/seali.c3l"} under its vendor.c3po.sources

Adding a library that is already present in lib/ re-reads its manifest and resolves its dependencies without refetching it.

Libraries that depend on libraries

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.

--zip

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

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:

  1. Clone the repository into a directory of its own, <dir>/c3po-install-<name>-<stamp>
  2. Restore what its project.lock.json pins, as c3po sync --force would
  3. Build each target with c3c, at -O3
  4. 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/c3po
c3po install lmichaudel/c3fmt -t tree-sitter -t c3fmt --trust=full

c3fmt 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.

C3PO_HOME

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.

Which targets get built

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.

Renaming the binary

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-lsp

It 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.

Installing from a local directory

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 plain c3po sync would. The ones already there are left alone, including any you have edited in place
    • --path never re-adds over your working copy the way the repository form does.
  • Objects go to its own build/, so a subsequent c3c build there is warm rather than starting over.
  • Nothing is deleted afterwards. --dir and --cleanup describe 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

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

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

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

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

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.

project.lock.json

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's provides, matching its entry in dependencies
  • origin - the host it came from, github or codeberg
  • ref - the branch or tag -t asked for, empty for the default branch
  • commit - what ref resolved to at install time; sync checks 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.

Predictable rewrites

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.

Library manifest format

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 in project.json dependencies
  • vendor.c3po.c3po-version - the format version of the block, currently 1; c3po stamps it into every vendor.c3po it 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 install
    • name - the dependency name that source provides, i.e. its own provides
    • source - the repository URL, without a tag or commit

Project Structure

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

About

The C3 Project Orchestrator

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages