cx::pm docs

The cxpm command-line tool

cxpm fetch | configure | build | test | pack | install | uninstall   [options] [directory]
cxpm run [task] [-- arguments...] [-C directory]
cxpm project create <name> [--type T] [--tests] [--example]
cxpm search | login | logout | publish | serve                      (the package index)
cxpm generate <kind> [directory]
cxpm help [command]

directory is the project, . by default. Exit status is 0 on success, 1 when the work failed (a compile error, a failing test, a missing dependency) and 2 when the command line is wrong.

The lifecycle

StageWhat it doesWhat it writes under .cxpm/
fetchClones the dependencies the manifest fetches, resolves the ones the package index publishes, and pins their commitssources/<name>/, lock.json
configureLoads the manifests, selects the toolchain, resolves every dependency; builds the dependencies that come as source and installs them into the project’s sysrootdescriptors/, configuration.json, sysroot/
buildCompiles what changed and linksbuild/<name>/, compile_commands.json
packLays the result out under the install prefix and packs itstaging/<name>/, packages/
installGives the package to the package managernothing; it writes to the install root

Each stage first runs the ones before it, and each of those does nothing when its result is still valid: cxpm install on a fresh clone fetches, configures, builds, packs and installs; cxpm build right after it compiles nothing. --reconfigure (-r) resolves everything again, --force (-f) also rebuilds what is up to date.

cxpm test builds the project, then builds each project the manifest lists in tests and runs its executable from that project’s directory. It fails if any of them fails.

cxpm uninstall removes what cxpm install installed, through the same package manager.

Options common to the stages that build:

OptionMeaning
-t, --toolchain NAMEBuild with this toolchain instead of the one the target selects
--sysroot DIRBuild against this sysroot instead of the toolchain’s
-j, --jobs NHow many sources compile at once (default: one per core)
-p, --prefix DIRInstall prefix for pack, install, uninstall (default: the target’s, then /usr/local)
--root DIRinstall/uninstall under this root instead of the sysroot, as DESTDIR does
--package-manager NAMEarchive, pacman, rpm or deb, instead of the one the toolchain names
--no-hooksfetch, configure, build, test, pack, install, publish: do not run the pre_ and post_ tasks

Tasks and hooks

A project declares tasks: a name and the shell text it runs. The target is what the package is; tasks are what you do around it, from generating a header to setting up a machine.

auto project = cx::pm::project()
                   .target_set(greeter)
                   .task_add("lint", "clang-tidy src/*.cpp -- -Iinclude")
                   .task_add("pre_build", "./tools/generate-version.sh > include/version.hpp")
                   .task_file_add("package_post_install", "packaging/postinst.in")
                   .create();

In package.json they are an object beside target; a task read from a file is {"file": "path"}:

"tasks": {
  "lint": "clang-tidy src/*.cpp -- -Iinclude",
  "package_post_install": {"file": "packaging/postinst.in"}
}

A name is 1 to 64 of a-z 0-9 + . _ -, starting with a letter or a digit. task_file_add reads the file, relative to the project, each time the manifest is loaded; a file outside the project or missing fails the load. Three kinds of task share the one list:

KindNamesRunsOn
Free taskany other namecxpm run <name>your machine
Developer hookpre_<stage>, post_<stage> for fetch, configure, build, test, pack, install, publisharound that stageyour machine
Packaged hookpackage_post_install, package_pre_remove, package_post_removeembedded by pack, run by dpkg, rpm or pacmanthe machine the package is installed on

cxpm run <task> -- <arguments> runs a task with /bin/sh -e -c: it stops at the first command that fails, $1, $2… are the arguments after --, the working directory is the project (-C DIR picks another), its output goes straight to the terminal, and cxpm exits with the task’s status. The task gets the environment cxpm test gives a tests project, the sysroot’s usr/bin and usr/lib first on PATH and LD_LIBRARY_PATH once the sysroot exists, and:

VariableValue
CXPM_PROJECT_DIRThe project directory
CXPM_SYSROOTThe workspace sysroot, .cxpm/sysroot at its root, made or not yet
CXPM_TARGET, CXPM_VERSIONThe target’s name and version; empty for a workspace root
CXPM_STAGEThe stage a developer hook wraps; run for any other task

cxpm run alone lists the tasks: free tasks, then developer hooks, then packaged hooks, each in the order declared. An unknown name fails and lists them.

Developer hooks. Each stage runs pre_<stage> before it starts and, when it succeeded, post_<stage> after it. A stage that another runs fires its hooks inside the outer ones: cxpm install runs pre_install, pre_pack, pre_build, the build, post_build, post_pack, then installs and runs post_install; cxpm configure fires pre_fetch and post_fetch around resolving the dependencies again. A failing pre_ hook stops the stage before it does anything, and no post_ hook runs; any failing hook fails the command with status 1 and a message naming the hook and its exit status. A stage runs the hooks of the project it is given and of the members that project lists, pre_ in that order and post_ in reverse; never those of a dependency it fetches, nor of a tests project. --no-hooks, or CXPM_NO_HOOKS=1, runs the stages without them.

Packaged hooks become the deb postinst, prerm and postrm, the rpm %post, %preun and %postun, and the pacman .INSTALL functions, with every @NAME@ replaced for the prefix the package installs under: @CXPM_INSTALL_PREFIX@, @CXPM_INSTALL_BINDIR@, @CXPM_INSTALL_LIBDIR@, @CXPM_INSTALL_DATADIR@, @CXPM_INSTALL_SYSCONFDIR@, @CXPM_INSTALL_LOCALSTATEDIR@ and the other CXPM_INSTALL_ directories, @CXPM_NAME@, @CXPM_VERSION@, and the target’s own variables. A name that is none of these, such as a script’s $@, is left as it is. A plain archive runs none, and pack warns when there are some. cxpm run package_post_install tries one here first, its @NAME@s replaced for --prefix, else the target’s prefix, else /usr/local.

Free tasks and developer hooks are code you wrote, run with your rights: they are not confined, and no package carries them. Only the packaged hooks leave your machine, inside the package.

The manifest: package.cpp or package.json

A manifest describes one project. A project has at most one target.

#include <cx/pm/project.hpp>
#include <cx/pm/target.hpp>

using namespace cx::pm;

auto package = cx::pm::target()
                  .name_set("greeter")
                  .version_set("0.1.0")
                  .description_set("Says hello")
                  .type_set("shared-library")
                  .sources_append({"src/*.cpp"})
                  .include_directories_append({"include"})
                  .options_append({compiler_options::standard_cxx23,
                                   compiler_options::warnings_all})
                  .link_libraries_append({"m"})
                  .dependencies_append({"zlib >= 1.2"})
                  .create();

auto project = cx::pm::project()
                   .target_set(package)
                   .members_append({"example"})
                   .tests_append({"tests"})
                   .create();

The same project in package.json, which is read without compiling or running anything:

{
  "schema": 3,
  "target": {
    "name": "greeter",
    "version": "0.1.0",
    "type": "shared-library",
    "sources": ["src/*.cpp"],
    "include_directories": ["include"],
    "options": ["standard-c++23", "warnings-all"],
    "link_libraries": ["m"],
    "dependencies": ["zlib >= 1.2"]
  },
  "members": ["example"],
  "tests": ["tests"]
}

package.cpp wins when a directory has both. cxpm generate package-cpp|package-json writes a starting point; cxpm project create writes a whole project.

Target types

typeBuiltInstalled
executableA programbin/
shared-librarylib<name>.solib/, headers in include/<name>/
static-librarylib<name>.alib/, headers in include/<name>/
header-libraryNothingHeaders in include/<name>/
dataNothingThe files listed with data_append(source, destination), under the prefix

Every type also installs lib/pkgconfig/<name>.pc, which is how a dependent project finds it. The headers of a library are those under its include_directories.

A data target ships files as they are. cxpm’s own toolchain modules are packaged this way:

auto package = cx::pm::target()
                  .name_set("cxpm-toolchain-g++")
                  .type_set("data")
                  .data_append("src/toolchain.cpp", "share/cxpm/toolchains/g++")
                  .create();

source is a file or a pattern relative to the project; destination is a directory relative to the install prefix.

Sources

sources entries are paths relative to the project or patterns: src/*.cpp matches in one directory, src/**/*.cpp in every directory below. A pattern that matches nothing is an error.

Options

An option is a neutral name that each toolchain translates, or a raw flag.

compiler_options:: names: standard_c11, standard_c17, standard_cxx17, standard_cxx20, standard_cxx23, warnings_all, warnings_extra, warnings_as_errors, optimize_none, optimize_size, optimize_speed, optimize_maximum, debug_information, position_independent_code, position_independent_executable, stack_protector_strong, stack_protector_all, threads, link_time_optimization, address_sanitizer, undefined_behavior_sanitizer, coverage. In JSON they are spelled as in the example above ("standard-c++23").

Anything that starts with - is passed to the compiler and the linker as written, and ties the manifest to toolchains that understand it. A name that is neither stops the build with the list of names the toolchain knows.

Dependencies

.dependencies_append({"zlib", "fmt >= 10"})

A name refers to, whichever exists:

  • a project of the workspace (a member of the same root manifest): built first and installed into .cxpm/sysroot;
  • a fetched dependency: a git repository with a manifest of its own, cloned by cxpm fetch, then built and installed into .cxpm/sysroot like a member. It is fetched either because the manifest names its repository (below), or because the package index publishes the name;
  • an installed package, found by pkg-config in the sysroot.
#include <cx/pm/dependency.hpp>

.dependencies_append(cx::pm::dependency()
                         .name_set("greeter")
                         .constraint_set(">= 0.1")
                         .repository_set("https://example.org/greeter.git")
                         .revision_set("v0.1.0"))

revision is a branch, a tag or a commit. .cxpm/lock.json records the commit that was fetched, and later fetches check out that commit until the manifest asks for another revision.

A dependency that cannot be resolved, or whose version does not satisfy the constraint, fails configure with the name and what was found. link_libraries is only for libraries of the system that have no pkg-config file, such as m or dl.

Workspaces

A manifest lists other projects by directory in members (built with it) and tests (built and run by cxpm test). A root manifest may have no target at all and only tie projects together; cxpm’s own is one:

auto project = cx::pm::project()
                   .members_append({"third-party", "libraries/cxpm-interface",
                                    "applications/cxpm"})
                   .tests_append({"tests/unit", "tests/cli"})
                   .create();

Projects are built in dependency order; a cycle is an error that names the projects in it. The workspace has one .cxpm/sysroot, at the root, and each project builds under its own .cxpm/build.

The package index

The index maps a package name and version to a commit of a git repository. It holds no code: cxpm fetch clones the repository.

Using packages

.dependencies_append({"greet >= 0.1"})

cxpm fetch asks the index for each name that is not a project of the workspace. A name the index does not know is left to the sysroot. For one it knows, fetch picks the newest version that every constraint in the workspace accepts — the constraints of fetched packages included — clones it into .cxpm/sources/<name>, and records the version and its commit in .cxpm/lock.json:

{
  "name": "greet",
  "repository": "https://example.org/greet.git",
  "revision": "ba5b12562248f856f2d8bda5cea758df42059e91",
  "commit": "ba5b12562248f856f2d8bda5cea758df42059e91",
  "version": "0.1.0",
  "index": "https://soft-forge.dev/cxpm/index"
}
  • Only cxpm fetch contacts the index. Every other command works from the lock file, so a build needs no network once the sources are there.
  • A locked version is kept until a constraint stops accepting it. To move to a newer one, tighten the constraint (or delete the entry from the lock) and run cxpm fetch.
  • The commit is the checksum. If the index later reports another commit for a locked version, fetch fails and names both.
  • When two projects need versions that exclude each other, fetch fails listing who needs what.
  • If the index cannot be reached, fetch warns, keeps what is locked, and leaves other names to the sysroot.

cxpm dependency edits the manifest instead of you:

cxpm dependency add greet                 # the newest version the index publishes, or a later one
cxpm dependency add "greet@>=0.1"         # a constraint of your own
cxpm dependency add https://example.org/json.git#v3 --name json
cxpm dependency list
cxpm dependency remove greet
cxpm dependency sync                      # fetch what the manifest names

add asks the index for the name, refuses a constraint no published version satisfies, writes the dependency into package.cpp or package.json, and fetches it (--no-sync leaves that to you). It checks that the manifest still reads the same afterwards and puts the file back when it does not; a package.cpp that builds its dependencies in some other way than .dependencies_append(...) has to be edited by hand.

The index is --index URL, else CXPM_INDEX, else https://soft-forge.dev/cxpm/index. off means no index. An https:// index is reached through curl, which must be installed; an http:// one needs nothing.

Publishing

cxpm login                     # once per index: shows a code to confirm, keeps a token
cxpm publish                   # in the project, at the top of its git repository
cxpm search greet

The public index at soft-forge.dev/cxpm/index signs you in with your Soft Forge account and has no password of its own. cxpm login shows an address and a code; you open the address, confirm the code there, and cxpm gets a token of its own and keeps only that. The first time, sign in on the index’s page: there you choose a plan and the name you publish under. A publication over the plan is refused with the address where the plan is changed. For CI, make a token on the page and set CXPM_TOKEN. An index you run yourself (Your own index ) has no sign-in: its operator gives you a token, for cxpm login --token <token>.

In a workspace, cxpm publish --package NAME publishes one of its projects; dependencies that are other projects of the same workspace are left out of what is published. cxpm whoami names the account the stored token belongs to.

publish sends the target’s name, version, description and dependencies, the address of the origin remote, the project’s README when there is one, and the commit of HEAD (or --revision REV). The commit must already be on the remote. --repository URL publishes another address than origin.

The first account to publish a name owns it, and a published version never changes: to fix something, publish a new version.

cxpm deprecate greet@0.1.0 "use 0.2.0"   # a warning for those who fetch it; --undo takes it back
cxpm yank greet@0.1.0                    # nothing new resolves to it; --undo puts it back
cxpm unpublish greet 0.1.0               # deletes the version for good
cxpm unpublish greet                     # deletes the package, with every version

Three things take a version back, from the mildest. deprecate leaves it resolving, with a message saying what to use instead. yank takes it out of what resolves and of the tags: the projects whose lock pins it keep it, with a warning, and its number is never published again. unpublish deletes it: the index no longer has it, so a project locked to it fails its next cxpm fetch (the message names the package, the version, the index and the way out, cxpm dependency update greet or dropping the dependency), and cxpm outdated lists it as gone. Yank when somebody may depend on the version; unpublish when it should never have been published.

unpublish says what goes and what breaks, then asks for the name of the package to be typed, and sends nothing unless it matches. --yes skips the question, for scripts; the line is read from standard input either way, so cxpm unpublish greet <<<greet works too. A maintainer deletes a version, the owner alone the package. The index refuses the last version (delete the package instead), and a version or package while another owner’s package depends on it (yank it instead); each refusal is printed as the index says it, with the way out. A deleted number is never published again, and a deleted name stays its owner’s: nobody else publishes it, and the owner only above every number it had. When the index’s operator removes a package, the name is blocked for everyone.

login stores the token in ~/.config/cxpm/credentials.json, readable by you alone. CXPM_TOKEN is used instead when set, which is what a CI job wants. logout forgets the stored token, and asks the index to revoke it where the index can (POST /-/logout).

Your own index

cxpm serve runs a package index of your own, for a team or a closed network: see Your own index .

The session: cxpm repl

cxpm with no arguments, at a terminal, opens a session; cxpm repl (or cxpm shell) does the same. Every line is a cxpm command without the program’s name:

cxpm demo> search greet
cxpm demo> dependency add greet
cxpm demo> build
cxpm demo> publish

The session adds cd, pwd, status (the directory, its manifest, the index and the account logged in to it), clear, help [command] and exit. Tab completes commands, options and paths. A command runs in a process of its own, so Ctrl-C stops the command and not the session. The prompt names the directory, in green when it holds a manifest, and shows the status of the last command when it failed.

History is kept in $XDG_STATE_HOME/cxpm/shell_history (CXPM_SHELL_HISTORY names another file; empty for none). Lines that carry --token or --password, and lines that start with a space, are not written to it.

When standard input is not a terminal, cxpm repl reads one command per line and exits with the status of the last one, so cxpm repl < script runs a script.

Toolchains

A toolchain says how to drive a compiler: executables, flag spellings, file names, the sysroot it builds against and the package manager of that sysroot. It is a toolchain.cpp or a toolchain.json:

#include <cx/pm/toolchain.hpp>

cx::pm::toolchain_descriptor toolchain =
    cx::pm::toolchain()
        .name_set("al2023")
        .language_set("c++")
        .compiler_executable_set("/usr/bin/g++")
        .linker_executable_set("/usr/bin/g++")
        .archiver_executable_set("/usr/bin/ar")
        .sysroot_set("/opt/sysroots/amazonlinux-2023")
        .package_manager_set("rpm")
        .option_mappings_set(compiler_options::gnu);

cxpm ships g++, gcc, clang++, clang and nvcc, installed as source in <prefix>/share/cxpm/toolchains/<name>/toolchain.cpp. It also looks in ~/.local/share/cxpm/toolchains and in the directories of cxpm_BUILD_EXTRA_MODULES_PATH. A target picks one with toolchain_set("name"), --toolchain overrides it, and otherwise the first toolchain for the target’s language is used.

Sysroots

Dependencies that cxpm builds go into the project’s own .cxpm/sysroot. Everything else comes from the toolchain’s sysroot: / by default, which is your system.

With another sysroot (sysroot_set or --sysroot DIR), the compiler and the linker get --sysroot, pkg-config only sees that sysroot’s packages, and nothing from the host leaks into the build. That is how a project is built for another distribution from one machine: unpack that distribution’s root filesystem with its development packages, and point a toolchain at it.

Packages

cxpm pack writes one package per project into .cxpm/packages/:

Package managerFileInstalled with
archive (default)<name>-<version>.cxpm.tarcxpm itself; a record in <prefix>/lib/cxpm/packages/ makes uninstall exact
pacman<name>-<version>-1-<arch>.pkg.tar.zstpacman -U
rpm<name>-<version>-1.<arch>.rpmrpm -i
deb<name>_<version>_<arch>.debdpkg -i

Native packages are written by cxpm itself; makepkg, rpmbuild and dpkg-deb are not needed to pack, only the package manager to install. The dependencies of the manifest become the dependencies of the package. Installing into a system root usually needs root privileges.

Confinement

A package.cpp or toolchain.cpp is code, and so is every build step. When bubblewrap (bwrap, from your distribution’s packages) is installed, cxpm runs them in a namespace of their own:

  • compiling a manifest, loading it, and every compile, link and archive command see the project, its workspace, the sysroot and the system’s own directories (/usr, /bin, /lib*, /etc, /opt), read-only except for the directories they write to;
  • they have no network, and do not see your home directory or anything else on the machine.

A compiled manifest is also never loaded into cxpm itself: the separate cxpm-descriptor-sandbox process loads it, under CPU, memory and time limits, and hands the description back as JSON.

CXPM_CONFINEMENTBehaviour
auto (default)Confine when bwrap works; warn once and run unconfined when it does not
requiredFail when confinement is not available
offNever confine

CXPM_CONFINEMENT_READABLE is a :-separated list of extra directories to show read-only, for a compiler installed outside the system directories.

Not confined: git (and curl, for an https:// index) during fetch, which need the network; the test executables cxpm test runs; tasks and hooks ; and the package manager during install.

Machine-readable output

Everything cxpm says about its progress goes to standard error as the journal: Configuring x, Compiling src/a.cpp, warning: …, error: …; standard output is only a command’s answer. An editor or a CI job that wants to read it asks for JSON:

cxpm --log-format json build          # before the command
CXPM_LOG_FORMAT=json cxpm build       # or in the environment

Each journal entry is then one JSON object on one line (NDJSON), schema 1, described by schemas/journal-1.json :

{"schema":1,"time":"2026-11-08T14:03:12.418Z","level":"info","stage":"build","project":"greeter","message":"Compiling src/greeter.cpp","source":"src/greeter.cpp","index":1,"count":2}
{"schema":1,"time":"2026-11-08T14:03:13.900Z","level":"error","stage":"build","project":"greeter","message":"Compiling src/broken.cpp failed","source":"src/broken.cpp","diagnostics":[{"file":"/w/greeter/src/broken.cpp","line":2,"column":24,"severity":"error","message":"expected ';' before '}' token","tool":"compiler"}],"raw":"/w/greeter/src/broken.cpp:2:24: error: expected ';' before '}' token\n    2 | …"}
{"schema":1,"time":"2026-11-08T14:03:13.901Z","level":"error","stage":"build","project":"greeter","message":"Failed to build greeter"}
FieldOnMeaning
time, level, messageevery entryUTC with milliseconds; debug, info, warn or error; the text the plain format prints
stageinside a stagefetch, configure, build, test, pack, install, publish, run
projectonce a project is being worked onthe target or node name
source, index, countCompiling <source>the file, and its position in the batch of sources being compiled
artifactLinking <artifact>the file name
diagnostics, rawa failed compile, link or manifesteach file:line:column: severity: message the compiler printed, each undefined reference and missing library the linker reported (file empty when the tool named none), and the untouched text

The plain format stays the default and is unchanged; --log-format plain says so explicitly. CXPM_VERBOSE=1 adds the debug entries, in both formats.

Environment

VariableEffect
CXPM_HOST_CXX, then CXXThe compiler that compiles package.cpp/toolchain.cpp (default: c++)
cxpm_BUILD_EXTRA_MODULES_PATHExtra toolchain directories, :-separated, searched before the installed ones
CXPM_HEADERS_PATHWhere the cx/pm/ manifest headers are, when not under the install prefix
CXPM_SANDBOX_HELPERPath to cxpm-descriptor-sandbox, when it is not beside cxpm
CXPM_CONFINEMENT, CXPM_CONFINEMENT_READABLESee Confinement
CXPM_INDEXThe package index fetch, search, login and publish use; off for none
CXPM_TOKENThe token publish presents, instead of the one login stored
PKG_CONFIG_PATHExtra pkg-config directories, honoured when building against the host sysroot
SOURCE_DATE_EPOCHThe timestamp written into packages, for reproducible ones
CXPM_NO_HOOKS1 runs every stage without its pre_ and post_ tasks, as --no-hooks does

What belongs in version control

Commit the manifests and, if you want pinned dependencies, .cxpm/lock.json. Ignore the rest of .cxpm/.