12 KiB
MacTools Plugin Catalog
MacTools dynamic plugins use one catalog-driven flow for both production distribution and local development.
- PluginKit 2 production builds read the legacy
catalog.jsonURL. PluginKit 3 and later builds read a versioned URL such asv3/catalog.json. - Each catalog contains packages for one PluginKit ABI line. The legacy v2 catalog is kept unchanged when a new ABI is released, so older app builds continue to work.
- Local development reads a Debug-only
file://catalog, usually configured withMACTOOLS_PLUGIN_CATALOG_URL. - Both flows resolve catalog entries into local staged packages, verify checksum and manifest compatibility, then install through the same package store. The marketplace can update one plugin at a time or run a batch update for every currently updateable plugin.
Catalog v1
{
"schemaVersion": 1,
"catalogID": "com.ggbond.mactools.plugins",
"generatedAt": "2026-05-16T12:00:00Z",
"minimumHostVersion": "0.15.2",
"pluginKitVersion": 2,
"plugins": [
{
"id": "com.ggbond.mactools.demo",
"displayName": "Demo",
"summary": "示例插件",
"localizedMetadata": {
"zh-Hans": {
"displayName": "示例",
"summary": "示例插件"
},
"en": {
"displayName": "Demo",
"summary": "Demo plugin"
}
},
"version": "1.0.0",
"minimumHostVersion": "0.15.2",
"pluginKitVersion": 2,
"capabilities": {
"primaryPanel": true,
"componentPanel": false,
"configuration": true
},
"permissions": [],
"package": {
"url": "https://github.com/ggbond268/MacTools/releases/download/plugins-1.0.1/Demo.mactoolsplugin.zip",
"sha256": "...",
"size": 1234567
},
"releaseNotesURL": "https://github.com/ggbond268/MacTools/releases/tag/plugins-1.0.1",
"category": "productivity",
"releaseChannel": "beta"
}
],
"revoked": [],
"signature": {
"algorithm": "ed25519",
"value": "..."
}
}
localizedMetadata is copied from each plugin manifest and is used by the marketplace before the plugin bundle is loaded. displayName and summary remain required fallbacks for older hosts and incomplete translations.
Release catalogs must include an Ed25519 signature. Debug local catalogs may omit signature, but they still go through package checksum, manifest, staging, and same-team code signature validation.
Versioned Catalog URLs
The catalog URL is selected by the host's supported PluginKit version:
PluginKit 2 -> https://mactools.ggbond.app/plugins/catalog.json
PluginKit 3 -> https://mactools.ggbond.app/plugins/v3/catalog.json
PluginKit N -> https://mactools.ggbond.app/plugins/vN/catalog.json
The first release for a new PluginKit version uses the previous ABI catalog only as a comparison baseline. It publishes a complete catalog containing every rebuilt plugin under the new versioned path. Later releases for the same PluginKit version may use incremental merges within that path. Never overwrite the legacy v2 catalog with v3 packages.
Local Development
The default local workflow is convention based:
MacTools/
Plugins/
Demo/
plugin.json
Sources/
Bundle/
Tests/
External plugin repositories can use the same structure, as long as the manifest can resolve either a buildable project or a prebuilt bundle:
MacToolsPlugins/
Demo/
plugin.json
Sources/
Bundle/
Tests/
plugin.json declares the plugin ID, version, capabilities, bundle path, optional releaseChannel, and build scheme. In this repository make generate, make build, make run, and make build-plugin first scan Plugins/*/plugin.json and generate the local XcodeGen plugin targets. External repositories may provide their own project.yml, .xcodeproj, or the declared bundle directory. The built package contains only plugin.json and the signed .bundle; extra executables must already be copied into the bundle resources and listed in plugin.json.package.signPaths when they require an individual code signature.
From the MacTools repository, build all local plugins and generate the Debug catalog:
make build-plugin
Or build one plugin by directory name or plugin ID:
make build-plugin PLUGIN=Demo
make build-plugin PLUGIN=com.example.mactools.demo
Generated output lives under:
build/LocalPlugins/
Packages/*.mactoolsplugin
catalog.dev.json
Then run MacTools:
make run
make run starts the app executable directly so the catalog environment variable reaches the app process. If MACTOOLS_PLUGIN_CATALOG_URL is not already set and build/LocalPlugins/catalog.dev.json exists, Make uses that file automatically.
You can override the fixed directories:
make build-plugin LOCAL_PLUGIN_SOURCE_DIR=/path/to/plugins LOCAL_PLUGIN_BUILD_DIR=/path/to/build
make run MACTOOLS_PLUGIN_CATALOG_URL=file:///path/to/catalog.dev.json
For Debug runs, the catalog URL scheme selects the verification mode. A file:// URL uses the local development catalog policy, where signatures are optional. An https:// URL uses the production catalog policy, where the catalog signature is required:
make run MACTOOLS_PLUGIN_CATALOG_URL=https://mactools.ggbond.app/plugins/catalog.json
The app copies the package into its own staging and installed directories. Uninstall deletes only the installed copy under MacTools application support; it never deletes the plugin source directory or the local build directory.
Release Flow
Recommended production flow is an incremental batch plugin release:
- Run
make release. - Choose
plugin, release mode, andpatch/minor/major. - The helper analyzes the production catalog and shows the planned manifest bumps.
- After confirmation, the helper syncs
main, bumps changed plugin manifests when needed, compilesrelease: pluginchangelog fragments intoCHANGELOG.md, runs a release plan check, commits the bump, and pushes a batch tag such asplugins-1.0.1. - The
Plugin ReleaseGitHub Action reads the catalog for the current PluginKit version. The first v3 release falls back to the legacy v2 catalog only to compare versions. - In default
automode, the workflow selects only new plugins and plugins whose manifest version is higher than the previous catalog entry. - If package-relevant files changed inside a plugin or its
pluginKitVersionchanged but that plugin version did not increase, the workflow fails before signing or uploading. A PluginKit ABI change automatically becomes a fullmode=allrebuild and replaces the catalog for that ABI line; other shared host changes can usemode=allor explicit--shared-pathvalues when they really require repackaging existing plugins. - The workflow builds, signs, zips, and uploads only the selected plugin packages.
- For an ABI migration, the workflow generates a complete catalog from all rebuilt packages. For later releases within an ABI line, it generates a delta catalog and merges it into that line's catalog, keeping unchanged entries pointing at their existing assets.
- The signed catalog is committed to
docs/plugins/catalog.jsonfor v2, ordocs/plugins/vN/catalog.jsonfor PluginKit N >= 3. Deploy Pagespublishes the signed catalog to GitHub Pages.
The batch tag is stored per plugin entry through package.url and releaseNotesURL, so one catalog can point different plugins to different release tags without changing host code.
The GitHub Release body for each plugin batch is extracted from the matching CHANGELOG.md entry, such as ## [plugins-1.0.1].
An incremental release record contains only packages changed in that batch:
GitHub Release: plugins-1.0.1
calendar.mactoolsplugin.zip
display-brightness.mactoolsplugin.zip
Unchanged plugin entries remain valid because the catalog preserves their previous URLs, checksums, and versions. They are not shown as updates in the app unless their catalog version is higher than the installed version.
pluginKitVersion is the PluginKit ABI boundary. When it changes, every plugin package must be rebuilt and each plugin's manifest version must increase so installed users see an update. The new host reads the new catalog and updates all installed plugins before loading any dynamic bundle. The catalog merge step rejects mixed PluginKit versions.
When a full rebuild is needed, run the Plugin Release workflow manually with mode=all. To publish a controlled subset, use mode=selected and pass comma-separated plugin IDs or directory names in plugins.
Each zip keeps the package root:
appearance.mactoolsplugin/
plugin.json
Appearance.bundle/
Contents/
Info.plist
MacOS/Appearance
Resources/
_CodeSignature/
Local dry-run packaging uses the same release asset script:
make package-plugins-release \
PLUGIN_CODE_SIGN_IDENTITY="Developer ID Application: Example (TEAMID)" \
PLUGIN_CATALOG_PRIVATE_KEY_BASE64="$PLUGIN_CATALOG_PRIVATE_KEY_BASE64" \
PLUGIN_RELEASE_TAG=plugins-1.0.1
Generated local output:
build/PluginRelease/
Assets/*.mactoolsplugin.zip
catalog.json
docs/plugins/v3/catalog.json
The lower-level scripts are still useful for external plugin repositories. build-plugin-release-assets.sh can build all plugins or a subset with repeated --plugin arguments:
scripts/plugins/plan-plugin-release.py \
--mode all \
--previous-catalog docs/plugins/catalog.json \
--output build/PluginRelease/plan.json
scripts/plugins/build-plugin-release-assets.sh \
--base-url https://github.com/ggbond268/MacTools/releases/download/plugins-1.0.1 \
--catalog-output build/PluginRelease/catalog.delta.json \
--sign-identity "Developer ID Application: Example (TEAMID)"
scripts/plugins/merge-plugin-catalog.py \
--previous docs/plugins/catalog.json \
--updates build/PluginRelease/catalog.delta.json \
--plan build/PluginRelease/plan.json \
--plugin-kit-version 3 \
--output build/PluginRelease/catalog.merged.json
scripts/plugins/generate-plugin-catalog.sh \
--mode release \
--base-url https://github.com/ggbond268/MacTools/releases/download/plugins-1.0.1 \
--output dist/catalog.json \
--package dist/Demo.mactoolsplugin.zip \
--release-notes-url https://github.com/ggbond268/MacTools/releases/tag/plugins-1.0.1
scripts/plugins/sign-plugin-catalog.sh \
--input build/PluginRelease/catalog.merged.json \
--output docs/plugins/v3/catalog.json \
--private-key-base64 "$PLUGIN_CATALOG_PRIVATE_KEY_BASE64"
The catalog private key, Developer ID identity, and GitHub token must come from local environment variables or CI secrets. Do not commit them. The catalog public key is safe to embed in the app as PLUGIN_CATALOG_PUBLIC_KEY.
Runtime Lifecycle
Install, update, enable, disable, and uninstall are immediate at the UI contribution level:
- Installed and enabled plugins contribute panels, components, settings, permissions, and shortcuts.
- Disabled plugins are removed from UI and function lists immediately.
- Uninstalled plugins are removed from UI immediately and package files are deleted.
- Batch updates resolve the currently updateable catalog entries and rebuild plugin management state once after successful package replacements.
- Already-loaded native code is not force-unloaded in-process. The executable code is fully released after the app restarts.
This keeps the native bundle lifecycle aligned with macOS loadable bundle constraints while preserving a predictable management UI.