pacm

pacm

C++ package manager built on one invariant: the install completes, or the filesystem looks like it never happened.

An interrupted install is the bug nobody catches in testing. The download finishes; the extract starts; the lid closes; on resume, half the files are on disk and the application loads the half-written plugin into the process. The crash report blames the plugin author, who shipped the package six months ago and cannot reproduce it.

Pacm is a C++ package manager built on one invariant: the install completes, or the filesystem looks like it never happened. The archive downloads to a staging directory and is verified against a SHA-256 declared in the manifest; extraction runs inside a sandbox that rejects path traversal; only then does an atomic move land the files in their final location. Installations run on libuv and never block the host application that asked for them.

State machine

The package lifecycle has three phases. The filesystem reflects the current phase at all times; if the process dies between phases, the next start knows exactly where to pick up.

  Remote Index                Local State              Filesystem
       |                          |                        |
  queryRemotePackages()     loadLocalPackages()            |
       |                          |                        |
       v                          v                        |
  RemotePackage[]           LocalPackage[]                 |
       |                          |                        |
       +--- getPackagePairs() ----+                        |
       |                                                   |
       v                                                   |
  InstallTask                                              |
    Downloading -----> tmp/package-1.0.0.zip               |
    Extracting  -----> tmp/extracted/                      |
    Finalizing  -----> install/package-name/      <-- atomic move
    Installed   -----> data/package-name.json     <-- manifest

Downloading fetches the archive from remote mirrors with progress callbacks. Extracting decompresses to a staging directory with path traversal protection, so no zip can write outside the target. Finalizing atomically moves files to the install directory. A failure at any phase leaves the previous phases intact and recoverable; the install never lands halfway.

The contract

Packages live as JSON on any HTTP server: your own CDN, GitHub releases, S3. The manifest declares versioned assets with platform-specific binaries:

{
    "id": "vision-plugin",
    "name": "Vision Plugin",
    "type": "Plugin",
    "author": "0state",
    "description": "OpenCV-based image processing plugin",
    "assets": [
        {
            "version": "2.1.0",
            "sdk-version": "3.0.0",
            "platform": "linux",
            "file-name": "vision-2.1.0-linux.zip",
            "file-size": 4521984,
            "checksum": "a1b2c3...",
            "mirrors": [
                {"url": "https://cdn.example.com/vision-2.1.0-linux.zip"},
                {"url": "https://backup.example.com/vision-2.1.0-linux.zip"}
            ]
        }
    ]
}

Platform detection runs automatically; asset selection picks the right binary for the host OS, and mirror fallback means a CDN outage does not break installations in flight.

A manifest can also describe how an installed payload should be used after it lands on disk: loader (graft), runtime (native or worker), entrypoint path, ABI version, and capability tags. Delivery concerns stay in Pacm while the host still has enough metadata to bind the installed extension cleanly.

ABI as a runtime check

A plugin compiled against SDK 2.x can link cleanly against SDK 3.x and then crash the host process the moment it touches an internal whose layout changed. The conventional answer is “use semantic versioning and discipline”. Pacm checks instead.

Every asset declares an SDK version. Three queries make use of it: latestSDKAsset(sdkVersion) returns the newest asset compatible with the running application’s SDK; setVersionLock(version) freezes a package at a specific release; hasAvailableUpdates() compares local manifests against remote while respecting both locks.

PackageManager pm(options);
pm.initialize();
pm.queryRemotePackages();

// Only install what's compatible with the running runtime
auto pairs = pm.getUpdatablePackagePairs();
pm.updatePackages(ids, &monitor);

monitor.Complete += [](LocalPackageVec& packages) {
    for (auto& pkg : packages)
        loadPlugin(pkg->getInstalledFilePath("lib/plugin.so"));
};

A working plugin is never replaced with one that would crash the host. The check is mechanical, not aspirational.

Batch installs

InstallMonitor aggregates concurrent installations into a single progress stream so the UI updates against one number instead of tracking download N of M.

InstallMonitor monitor;
pm.installPackages({"plugin-a", "plugin-b", "model-data"}, &monitor);

monitor.Progress += [](int& percent) {
    updateProgressBar(percent);  // 0-100 across all tasks
};

monitor.InstallStateChange += [](auto& task, auto& from, auto& to) {
    log("Package {} moved from {} to {}", task.name(), from, to);
};

monitor.Complete += [](auto& packages) {
    // All done; load plugins, apply models
};

Individual tasks emit their own progress events. The monitor aggregates them into one percentage and routes per-task state transitions to whatever handler the host registered.

Security

Every install verifies before it finalizes:

  • Checksum: SHA-256 (or MD5 for legacy compat) against the declared hash. Mismatch aborts.
  • Path traversal: validatePathComponent() rejects .., absolute paths, and path separators in archive entries. No zip writes outside its target.
  • Manifest: after extraction, every declared file is checked for existence. Missing files fail the install.
  • Authentication: OAuth bearer tokens or HTTP basic auth for private repositories.
  • TLS: all HTTP traffic over SSL with certificate verification.

With graft

Graft handles the loading boundary. Pacm handles the delivery. The two compose: a package’s manifest declares a graft entrypoint, and after install the host resolves that path and hands it to graft::Library.

{
  "extension": {
    "loader": "graft",
    "runtime": "native",
    "entrypoint": "lib/libmotion_detector.so",
    "abi-version": 1,
    "capabilities": ["processor.video", "detector"]
  }
}

The same pipeline works for any binary asset that needs to arrive on disk whole: machine learning models, shader files, data bundles, configuration packages. Versioning, integrity, and atomicity are properties of every payload that has to land completely or not at all.

The install completes, or it never happened.