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 puts package delivery inside the application. It queries your package index, selects an asset, downloads and checks the archive, extracts it and reports progress. A plugin, a speech model and a data bundle can use the same delivery code; the host decides how each one is activated.
Ship the payload separately
A new detector model may be hundreds of megabytes. It does not need a new application executable, and every user may not need it. An optional native processor has a similar delivery problem: choose the right asset, show progress, retain its version and let the user enable it.
Pacm’s package metadata can live on an ordinary HTTPS server. Your application keeps its own interface and release policy. You can offer components on demand, pin a version that works with a particular SDK, or defer activation until the application restarts.
State machine
The install task moves through download, extraction and finalization, with progress and state changes exposed to the host. Local package records track the install; recovery after an interrupted finalization still needs to account for files already moved.
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/ <-- entries moved individually
Installed -----> data/package-name.json <-- manifest
Downloading fetches the selected asset with progress callbacks. Extracting checks the archive digest and unpacks it through the path-checked extraction layer. Finalizing moves staged entries into the install directory.
The current finalizer moves entries individually. A failure can leave a partial update, so a completion event is not sufficient evidence that a plugin is ready to load. Hashing and extraction also perform synchronous work; account for that when choosing where the task runs.
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": "auscaster",
"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"}
]
}
]
}
Assets carry platform and version metadata. The manifest can list several mirrors, but the current download path selects a URL; listing alternatives does not by itself implement automatic failover.
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.
Match the package to the host
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. Pacm uses declared SDK versions to select assets intended for the running host. The package author supplies that declaration.
Every asset declares an SDK version. Three queries make use of it: latestSDKAsset(sdkVersion) returns the newest asset with a matching SDK version label; 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();
// Select updates using the configured version and SDK locks
auto pairs = pm.getUpdatablePackagePairs();
pm.updatePackages(ids, &monitor);
monitor.Complete += [](LocalPackageVec& packages) {
// Inspect task errors and verify the installed payload.
// The host owns activation; completion alone is not that check.
};
A matching SDK label narrows asset selection. Graft can then check the native manifest and resolve the declared entrypoint. The host still needs the shared API contract, a complete installed payload and an activation policy appropriate to its process.
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) {
// Inspect results before activating plugins or replacing model data.
};
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.
Integrity and installation checks
The delivery path includes checks at several stages:
- Checksum: SHA-256 (or MD5 for legacy compat) against the declared hash. Mismatch aborts.
- Paths: package and cache path components are validated; archive extraction has its own path checks through
archo. - Manifest: extracted entries are recorded in the local install manifest. A host requiring an exact expected file set must check that contract too.
- Authentication: HTTP basic auth for private repositories.
- Transport: asset mirror validation requires HTTPS URLs. The application still owns the origin and trust of its package index.
A checksum binds the archive to the digest in the metadata. If the archive and metadata are both replaced by the same attacker, that comparison can still pass. Distribution therefore also needs a trusted index or a separate authenticity mechanism.
With graft
Graft handles the loading boundary while pacm handles the delivery, and 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 delivery machinery can carry machine learning models, shader files, data bundles and configuration packages. The application chooses how to validate and activate each kind of payload. For a running native plugin, that may mean deferring replacement until the process has released the old library.
