Architecture
The lifecycle
Every command is a stage, and each stage runs the ones before it when they are stale. All of them
are methods of Controllers::ProjectManager.
argv
│
▼
Views::ApplicationView a cx::console::cli router; owns usage and exit codes
│
▼
Controllers::ProjectManager
│
├─ fetch Fetcher clones each dependency that names a repository, or that the
│ package index publishes (IndexClient), into .cxpm/sources/ and records
│ the commit in .cxpm/lock.json
│
├─ configure ManifestLoader loads the manifest; ProjectGraph orders the workspace;
│ ToolchainManager picks a toolchain; PkgConfigManager resolves every
│ dependency. The result is written to .cxpm/configuration.json
│
├─ build ToolchainDriver turns the configuration into compile, link and archive
│ commands and runs the stale ones on a thread pool
│
├─ test builds each tests project and runs its executable
│
├─ pack Staging lays the target out under .cxpm/staging/ as it will be installed;
│ a PackageManager turns that tree into a package in .cxpm/packages/
│
└─ install the same PackageManager installs the package under <root><prefix>
uninstall and removes what its record lists
Every external program in that picture — the compiler, pkg-config, git, rpmbuild, the
sandbox helper — is started by Controllers::ProcessRunner, as an argument vector with no shell.
Everything a project produces is under .cxpm/
.cxpm/
descriptors/ package.cpp compiled into a plugin, reused until the source changes
sources/<name>/ fetched repositories
lock.json the commit (and index version) each fetched dependency resolved to
configuration.json the configured toolchain, dependencies and options
build/<target>/ objects, the built target, compile_commands.json
staging/ the target laid out as it will be installed
packages/ the package made from the staging tree
sysroot/ (workspace root) the members other members depend on, installed
Deleting .cxpm/ returns a project to a clean checkout.
Workspaces
A project has exactly one target. What other build systems express as several targets in one
project is several projects here: a root manifest lists members and tests, and
ProjectGraph sorts them by their declared dependencies and rejects a cycle.
When a member depends on another member, the dependency is packed and installed into the
workspace’s own .cxpm/sysroot, and the dependent finds it there through pkg-config, exactly
as it would find a package installed on the system. Nothing is found by reaching into a sibling’s
source directory. cxpm builds itself this way: cxpm-interface and the cx libraries are
installed into the sysroot, and cxpm compiles against them.
Two layers in one library
cxpm-interface is header-only and has two audiences:
- The manifest API:
Target,Project,Toolchain,Dependency, their descriptors,CompilerOptions,Statusand the exception types. Apackage.cpportoolchain.cppincludes only these. - The tool internals: everything under
CXPM::Controllers,CXPM::Manifest,CXPM::ViewsandCXPM::Modules::Generators, built on the cx libraries underthird-party/.
Components
| Component | Responsibility |
|---|---|
Views::ApplicationView |
The command router: commands, options, usage text, exit codes |
ProjectManager |
The lifecycle: decides what is stale and runs each stage |
ManifestLoader |
Loads package.cpp (through a plugin) or package.json into one ProjectDescriptor |
Workspace, ProjectGraph |
Finds the workspace root, its members and tests, and orders them |
Fetcher, VersionConstraint |
Clones dependencies, checks versions against constraints, keeps the lock file |
PackageIndex, IndexServer |
The package index: its store and HTTP API, and cxpm serve binding it to a TCP port or a unix socket with cx-networking |
IndexClient, Publisher |
The client side: what fetch, search, login and publish say to an index, and the stored tokens |
Configuration |
Reads and writes .cxpm/configuration.json and decides whether it is still valid |
ToolchainManager |
Finds toolchain descriptors (toolchain.cpp or toolchain.json) and selects one |
ToolchainDriver, SourcePatterns |
Expands source globs, builds the command lines, runs the stale ones in parallel |
PkgConfigManager |
Resolves dependencies through pkg-config; writes a library’s .pc file |
Staging |
Lays a built target out as it will be installed, for each target type |
PackageManager and PackageManagers |
The interface pack, install and uninstall go through, and the registry of backends |
ArchivePackageManager, PacmanPackageManager, RpmPackageManager, DebPackageManager |
The backends: a plain tar with an install record, and the three native formats |
TarArchive, Sha256, NativePackageSupport |
Reproducible archives and checksums the backends share |
DescriptorPlugin |
Compiles a package.cpp/toolchain.cpp and a generated loader into a shared object |
DescriptorSandbox |
Runs cxpm-descriptor-sandbox to read a descriptor out of that shared object |
ProcessRunner |
The one place a process is started; applies confinement; replaceable in tests |
Installation |
Where cxpm finds its own pieces: headers, toolchain directories, host compiler, helper |
JsonManifest.hpp (CXPM::Manifest) |
Descriptor to JSON and back: package.json, toolchain.json, the configuration, the sandbox’s output |
Modules::Generators::ManifestGenerator |
Backs cxpm generate and cxpm project create |
Two manifest formats, one representation
package.cpp is a real C++ translation unit. DescriptorPlugin compiles it, with a generated
loader, into a shared object under .cxpm/descriptors/; the manifest language is as expressive as
C++, with no parser or schema of its own to keep in step. The compiled manifest is reused until
package.cpp changes.
package.json is the declarative alternative, parsed straight into the same ProjectDescriptor
with no compiler involved. Everything after “the manifest is loaded” is identical for both.
package.cpp wins when both exist.
Both carry a schema number. A descriptor from a newer cxpm than the one reading it is refused rather than misread.
Toolchains are data, not code in cxpm
A toolchain is a ToolchainDescriptor: the executables, how each neutral option
(standard-c++23, optimize-speed, …) is spelled, how files are named, and optionally the
sysroot it compiles against and the package manager of that system. It comes from a
toolchain.cpp plugin or a toolchain.json. The ones under modules/toolchains/ are projects of
type data that install their toolchain.cpp into share/cxpm/toolchains/<name>/.
Running code cxpm did not write
Two things cxpm runs are not its own: a compiled manifest, and the build of a project.
The manifest. cxpm never calls dlopen. DescriptorSandbox starts
cxpm-descriptor-sandbox, a disposable child with CPU, memory and time limits, which loads the
plugin and prints its descriptor as JSON, the same JSON package.json is parsed from.
The build. ProcessRunner wraps the sandbox helper and every compile and link in a
bubblewrap namespace: no network, the project directory writable, and only the toolchain, the
system directories, the sysroot in use and the workspace sysroot readable. A manifest that reads
your home directory or opens a socket fails. CXPM_CONFINEMENT chooses between auto (confine
when bubblewrap works), required and off.
What this does not cover is stated in tooling.md
: cxpm test runs the
project’s own tests unconfined, and /etc is readable.
Where the design is recorded
The requirements this implements, and the findings in the original implementation that led to
it, are on the project board, planning.pb at the root of the repository, under the “SRS”
headings. roadmap.md
says which milestone did what.