Embeddable C++ package manager for native applications
The pacm module is an embeddable package manager. We use it to give native applications the ability to install, update, and uninstall versioned archives (plugins, assets, or any packaged payload) from a remote HTTP endpoint, without shipping a separate management tool.
The server side is a plain JSON index served over HTTP; no special server software is required. The client fetches the index, compares it against locally-stored manifests, and downloads only what has changed. Downloads are verified by checksum before extraction. Installation is a three-phase pipeline: download, extract (via archo), finalize (move files into place).
Dependencies: base, net, json, http, archo, crypto
Five classes carry the module's responsibilities:
PackageManager is the central coordinator. It owns the local and remote package stores, creates InstallTask instances on demand, and emits signals at key lifecycle points.Package / RemotePackage / LocalPackage are JSON-backed value types describing packages on the server and on disk.InstallTask runs one installation pipeline asynchronously: download → extract → finalize. It is a state machine and emits progress and completion signals.InstallMonitor coordinates multiple concurrent InstallTask instances and aggregates their progress into a single overall signal.InstallOptions carries per-operation overrides (version pin, SDK version pin, install directory override).PackageManager
├── RemotePackageStore (LiveCollection<id, RemotePackage>)
├── LocalPackageStore (LiveCollection<id, LocalPackage>)
└── InstallTask [0..*]
├── download (http)
├── extract (archo)
└── finalize (filesystem move)
InstallMonitor (optional; aggregates N InstallTasks)Construct a PackageManager with an Options struct, then call initialize(). All directories default to subdirectories of the current working directory (pacm/install, pacm/data, pacm/tmp) if not overridden.
#include <icy/pacm/packagemanager.h>
icy::pacm::PackageManager::Options opts;
opts.endpoint = "https://packages.example.com";
opts.indexURI = "/packages.json";
opts.installDir = "/opt/myapp/plugins";
opts.dataDir = "/var/myapp/pacm/data";
opts.tempDir = "/tmp/myapp/pacm";
icy::pacm::PackageManager pm(opts);
pm.initialize();initialize() creates the directory structure and loads any previously saved local manifests from dataDir.
pm.queryRemotePackages();
for (auto& [id, pkg] : pm.remotePackages().map())
std::cout << "remote: " << id << " " << pkg->latestAsset().version() << "\n";
for (auto& [id, pkg] : pm.localPackages().map())
std::cout << "local: " << id << " " << pkg->version() << "\n";queryRemotePackages() fires the RemotePackageResponse signal when the HTTP response arrives.
installPackage() returns a started-but-not-yet-running InstallTask::Ptr. Attach signal handlers before calling start(). The method returns nullptr if the package is already up to date.
auto task = pm.installPackage("myplugin");
if (task) {
task->Progress += [](icy::pacm::InstallTask& t, int& pct) {
std::cout << "progress: " << pct << "%\n";
};
task->Complete += [](icy::pacm::InstallTask& t) {
if (t.success())
std::cout << "installed\n";
else
std::cout << "failed\n";
};
task->start();
}Pass an InstallMonitor to installPackages() to hold all tasks unstarted until after signal handlers are attached, then call startAll().
icy::pacm::InstallMonitor monitor;
monitor.Progress += [](int& pct) {
std::cout << "overall: " << pct << "%\n";
};
monitor.Complete += [](icy::pacm::LocalPackageVec& pkgs) {
std::cout << "all done: "
<< icy::pacm::getInstallTaskNamesString(pkgs) << "\n";
};
std::vector<std::string> ids = { "myplugin", "anotherpackage" };
pm.installPackages(ids, {}, &monitor);
monitor.startAll();Without a monitor, installPackages() starts all tasks immediately.
icy::pacm::InstallOptions opts;
// Pin to an exact package version.
opts.version = "1.2.0";
pm.installPackage("myplugin", opts);
// Install the latest build compatible with a specific SDK version.
opts.version = "";
opts.sdkVersion = "3.0.0";
pm.installPackage("myplugin", opts);
// Override the install directory for this operation only.
opts.installDir = "/opt/myapp/experimental";
pm.installPackage("myplugin", opts);SDK version matching is intended for plugins that must be compiled against a specific SDK release. The package JSON must include a "sdk-version" field on each asset for this to work.
We can prevent a local package from being updated past a given version:
auto& locals = pm.localPackages().map();
auto it = locals.find("myplugin");
if (it != locals.end()) {
it->second->setVersionLock("1.2.0"); // never update past 1.2.0
pm.saveLocalPackages();
}Pass an empty string to setVersionLock() to remove the lock. setSDKVersionLock() works the same way for SDK-version constraints.
// Update a single package.
auto task = pm.updatePackage("myplugin");
if (task) task->start();
// Update all installed packages.
pm.updateAllPackages();updatePackage() throws if the package is not known. updateAllPackages() silently skips packages that are already current.
pm.uninstallPackage("myplugin");
pm.uninstallPackages({ "myplugin", "anotherpackage" });The PackageUninstalled signal fires for each removed package.
If target files are in use at extraction time, installed files are left in a staging area with the state Finalizing. On the next startup we check for these and complete the move:
if (pm.hasUnfinalizedPackages())
pm.finalizeInstallations();PackagePair binds a LocalPackage* and a RemotePackage* together; either pointer may be null if the package exists on only one side.
for (auto& pair : pm.getPackagePairs()) {
std::cout << pair.id() << "\n";
if (pair.local) std::cout << " installed: " << pair.local->version() << "\n";
if (pair.remote) std::cout << " available: " << pair.remote->latestAsset().version() << "\n";
}
// Only pairs with a newer remote version available.
for (auto& pair : pm.getUpdatablePackagePairs())
std::cout << "update available: " << pair.id() << "\n";pm.cancelAllTasks();All in-progress downloads and extractions are aborted. Call this before destroying the PackageManager if tasks may still be running.
The server responds to GET /packages.json with a JSON array. Each element describes one package and its available assets:
[{
"id": "myplugin",
"type": "Plugin",
"name": "My Plugin",
"author": "Example Corp",
"description": "An example plugin.",
"assets": [{
"version": "1.0.0",
"sdk-version": "2.0.0",
"platform": "linux",
"checksum": "e4d909c290d0fb1ca068ffaddf22cbd0",
"file-name": "myplugin-1.0.0-sdk-2.0.0-linux.zip",
"file-size": 432321,
"mirrors": [
{ "url": "https://packages.example.com/myplugin-1.0.0-sdk-2.0.0-linux.zip" }
]
}]
}]The platform field on each asset is matched against Options::platform. The mirrors array may contain multiple download URLs; the first is used. Additional metadata fields may be added freely; the client ignores anything it does not recognise.
InstallationState transitions during a typical install:
None → Downloading → Extracting → Finalizing → Installed
→ Failed
→ CancelledThe redistributable command-line tool bundled with icey. It wraps the full PackageManager API and can be compiled and shipped alongside existing applications as a standalone package management utility.
# Install a package.
pacm-cli -endpoint https://packages.example.com -uri /packages.json -install myplugin
# Update all installed packages.
pacm-cli -endpoint https://packages.example.com -uri /packages.json -update
# Uninstall a package.
pacm-cli -endpoint https://packages.example.com -uri /packages.json -uninstall mypluginSupported flags:
| Flag | Description |
|---|---|
-endpoint | HTTP server base URL. |
-uri | Path to the package JSON index. |
-install | Comma-separated package IDs to install. |
-uninstall | Comma-separated package IDs to uninstall. |
-update | Update all installed packages. |
-print | Print all local and remote packages on exit. |
-install-dir | Override the installation directory. |
-data-dir | Override the manifest storage directory. |
-temp-dir | Override the download staging directory. |
-checksum-alg | Checksum algorithm (default: SHA256). |
-logfile | Log output path. |
PackageManager::Options controls all configurable behaviour:
| Field | Default | Description |
|---|---|---|
endpoint | https://localhost:3000 | HTTP server base URL. |
indexURI | /packages.json | Path to the package index on the server. |
httpUsername | "" | HTTP Basic Auth username. |
httpPassword | "" | HTTP Basic Auth password. |
httpOAuthToken | "" | OAuth token; used instead of Basic Auth when set. |
installDir | pacm/install | Final installation destination. |
dataDir | pacm/data | Local manifest storage. |
tempDir | pacm/tmp | Download and extraction staging area. |
platform | auto-detected | Platform string matched against asset records (linux, win32, mac). |
checksumAlgorithm | SHA256 | Algorithm used to verify downloaded archives. |
clearFailedCache | true | Whether to delete the cached archive if installation fails. |
All directory defaults are relative to the current working directory.