The cxpm command-line tool
cxpm fetch | configure | build | test | pack | install | uninstall [options] [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
| Stage | What it does | What it writes under .cxpm/ |
|---|---|---|
fetch |
Clones the dependencies the manifest fetches, resolves the ones the package index publishes, and pins their commits | sources/<name>/, lock.json |
configure |
Loads the manifests, selects the toolchain, resolves every dependency; builds the dependencies that come as source and installs them into the project’s sysroot | descriptors/, configuration.json, sysroot/ |
build |
Compiles what changed and links | build/<name>/, compile_commands.json |
pack |
Lays the result out under the install prefix and packs it | staging/<name>/, packages/ |
install |
Gives the package to the package manager | nothing; 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:
| Option | Meaning |
|---|---|
-t, --toolchain NAME |
Build with this toolchain instead of the one the target selects |
--sysroot DIR |
Build against this sysroot instead of the toolchain’s |
-j, --jobs N |
How many sources compile at once (default: one per core) |
-p, --prefix DIR |
Install prefix for pack, install, uninstall (default: the target’s, then /usr/local) |
--root DIR |
install/uninstall under this root instead of the sysroot, as DESTDIR does |
--package-manager NAME |
archive, pacman, rpm or deb, instead of the one the toolchain names |
The manifest: package.cpp or package.json
A manifest describes one project. A project has at most one target.
#include <CXPM/Project.hpp>
#include <CXPM/Target.hpp>
using namespace CXPM;
auto target = 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({CompilerOptions::StandardCxx23,
CompilerOptions::WarningsAll})
.link_libraries_append({"m"})
.dependencies_append({"zlib >= 1.2"})
.create();
auto project = Project()
.target_set(target)
.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
type |
Built | Installed |
|---|---|---|
executable |
A program | bin/ |
shared-library |
lib<name>.so |
lib/, headers in include/<name>/ |
static-library |
lib<name>.a |
lib/, headers in include/<name>/ |
header-library |
Nothing | Headers in include/<name>/ |
data |
Nothing | The 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 target = 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.
CompilerOptions:: names: StandardC11, StandardC17, StandardCxx17, StandardCxx20,
StandardCxx23, WarningsAll, WarningsExtra, WarningsAsErrors, OptimizeNone,
OptimizeSize, OptimizeSpeed, OptimizeMaximum, DebugInformation, PositionIndependentCode,
PositionIndependentExecutable, StackProtectorStrong, StackProtectorAll, Threads,
LinkTimeOptimization, AddressSanitizer, UndefinedBehaviorSanitizer, 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/sysrootlike a member. It is fetched either because the manifest names itsrepository(below), or because the package index publishes the name; - an installed package, found by pkg-config in the sysroot.
#include <CXPM/Dependency.hpp>
.dependencies_append(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 = 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://01.dev.soft-forge.dev/cxpm/index"
}
- Only
cxpm fetchcontacts 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://01.dev.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 --user ada # once per index: asks for the password, keeps a token
cxpm publish # in the project, at the top of its git repository
cxpm search greet
An index with accounts (cx-pm-web-index
) takes login --user NAME:
cxpm asks for the password without showing it (or reads it from the first line of standard input
when that is not a terminal), trades it for a token of its own and keeps only the token. An index
run by cxpm serve has no passwords: 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.
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).
Serving an index
cxpm serve --database-path /var/cache/soft-forge/cxpm --add-account ada # prints ada's token once
cxpm serve --database-path /var/cache/soft-forge/cxpm \
--listen unix:///run/cxpm/socket.sock \
--host https://01.dev.soft-forge.dev/cxpm/index
| Option | Default | Meaning |
|---|---|---|
--listen |
http://127.0.0.1:7878 |
unix:///path/to/socket, http://host:port or host:port |
--host |
The public address a proxy serves the index at; its path is accepted as a prefix of requests | |
--database-path |
/var/lib/cxpm/index |
Where packages and accounts are kept, as JSON files |
--add-account NAME |
Create the account (or replace its token), print the token, exit |
cxpm serves plain HTTP. Put a reverse proxy in front of it for TLS and point it at the socket.
The index keeps only a hash of each token, so a lost token is replaced with --add-account, not
recovered.
| Request | Answer |
|---|---|
GET / , GET /?q=text |
The packages: name, description, owner, latest version |
GET /<name> |
A package and its versions |
GET /<name>/<version> |
One version: repository, revision, commit, dependencies |
PUT /<name>/<version> |
Publish; needs Authorization: Bearer <token> |
GET /-/whoami |
The account a token belongs to |
An index with accounts
cxpm serve knows the accounts its operator made, one token each. cx-pm-web-index, a
separate application in its own repository, is the same index with people in front of it, as npm
has them: anyone signs up on its page with a name, an email and a password, signs in, makes and
revokes tokens, and sees the packages and their versions in a browser. It is built on cxpm’s own
PackageIndex, so fetch, search and publish talk to it exactly as they talk to
cxpm serve, and its data directory keeps the packages in the same files.
What it adds to the requests above, for cxpm:
| Request | Answer |
|---|---|
POST /-/login {name, password, label} |
A new token for the account: what cxpm login --user asks for |
POST /-/logout |
Revokes the token presented: what cxpm logout asks for |
Its README describes the rest: the settings, the page, the account requests and how to deploy it behind cx-networking-http-proxy.
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 <CXPM/Toolchain.hpp>
CXPM::ToolchainDescriptor toolchain =
CXPM::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(CXPM::CompilerOptions::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 manager | File | Installed with |
|---|---|---|
archive (default) |
<name>-<version>.cxpm.tar |
cxpm itself; a record in <prefix>/lib/cxpm/packages/ makes uninstall exact |
pacman |
<name>-<version>-1-<arch>.pkg.tar.zst |
pacman -U |
rpm |
<name>-<version>-1.<arch>.rpm |
rpm -i |
deb |
<name>_<version>_<arch>.deb |
dpkg -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
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_CONFINEMENT |
Behaviour |
|---|---|
auto (default) |
Confine when bwrap works; warn once and run unconfined when it does not |
required |
Fail when confinement is not available |
off |
Never 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; and the package manager during install.
Environment
| Variable | Effect |
|---|---|
CXPM_HOST_CXX, then CXX |
The compiler that compiles package.cpp/toolchain.cpp (default: c++) |
cxpm_BUILD_EXTRA_MODULES_PATH |
Extra toolchain directories, :-separated, searched before the installed ones |
CXPM_HEADERS_PATH |
Where the CXPM/ manifest headers are, when not under the install prefix |
CXPM_SANDBOX_HELPER |
Path to cxpm-descriptor-sandbox, when it is not beside cxpm |
CXPM_CONFINEMENT, CXPM_CONFINEMENT_READABLE |
See Confinement |
CXPM_INDEX |
The package index fetch, search, login and publish use; off for none |
CXPM_TOKEN |
The token publish presents, instead of the one login stored |
PKG_CONFIG_PATH |
Extra pkg-config directories, honoured when building against the host sysroot |
SOURCE_DATE_EPOCH |
The timestamp written into packages, for reproducible ones |
What belongs in version control
Commit the manifests and, if you want pinned dependencies, .cxpm/lock.json. Ignore the rest of
.cxpm/.