216 lines
7.3 KiB
Bash
Executable File
216 lines
7.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage:
|
|
generate-plugin-catalog.sh --mode debug --output catalog.dev.json --package Demo.mactoolsplugin [--package More.mactoolsplugin]
|
|
generate-plugin-catalog.sh --mode release --base-url https://github.com/owner/repo/releases/download/tag --output catalog.json --package Demo.mactoolsplugin.zip
|
|
|
|
Options:
|
|
--mode debug|release Debug uses file:// package URLs; release uses --base-url.
|
|
--output PATH Catalog JSON output path.
|
|
--package PATH .mactoolsplugin directory or .mactoolsplugin.zip. Repeatable.
|
|
--base-url URL Release asset base URL.
|
|
--release-notes-url URL Optional release notes URL used when plugin.json omits one.
|
|
--catalog-id ID Defaults to com.ggbond.mactools.plugins.
|
|
--minimum-host-version VER Defaults to 0.15.2.
|
|
--plugin-kit-version INT Override catalog PluginKit version; defaults to package manifests.
|
|
|
|
The script does not sign the catalog. Run sign-plugin-catalog.sh for release catalogs.
|
|
USAGE
|
|
}
|
|
|
|
MODE=""
|
|
OUTPUT=""
|
|
BASE_URL=""
|
|
RELEASE_NOTES_URL=""
|
|
CATALOG_ID="com.ggbond.mactools.plugins"
|
|
MINIMUM_HOST_VERSION="0.15.2"
|
|
PLUGIN_KIT_VERSION=""
|
|
PACKAGES=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--mode)
|
|
MODE="${2:-}"
|
|
shift 2
|
|
;;
|
|
--output)
|
|
OUTPUT="${2:-}"
|
|
shift 2
|
|
;;
|
|
--package)
|
|
PACKAGES+=("${2:-}")
|
|
shift 2
|
|
;;
|
|
--base-url)
|
|
BASE_URL="${2:-}"
|
|
shift 2
|
|
;;
|
|
--release-notes-url)
|
|
RELEASE_NOTES_URL="${2:-}"
|
|
shift 2
|
|
;;
|
|
--catalog-id)
|
|
CATALOG_ID="${2:-}"
|
|
shift 2
|
|
;;
|
|
--minimum-host-version)
|
|
MINIMUM_HOST_VERSION="${2:-}"
|
|
shift 2
|
|
;;
|
|
--plugin-kit-version)
|
|
PLUGIN_KIT_VERSION="${2:-}"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$MODE" != "debug" && "$MODE" != "release" ]]; then
|
|
echo "--mode must be debug or release." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "$OUTPUT" || ${#PACKAGES[@]} -eq 0 ]]; then
|
|
echo "--output and at least one --package are required." >&2
|
|
usage >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$MODE" == "release" && -z "$BASE_URL" ]]; then
|
|
echo "--base-url is required in release mode." >&2
|
|
exit 1
|
|
fi
|
|
|
|
python3 - "$MODE" "$OUTPUT" "$BASE_URL" "$RELEASE_NOTES_URL" "$CATALOG_ID" "$MINIMUM_HOST_VERSION" "$PLUGIN_KIT_VERSION" "${PACKAGES[@]}" <<'PY'
|
|
import hashlib
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
import tempfile
|
|
import zipfile
|
|
from datetime import datetime, timezone
|
|
|
|
mode, output, base_url, release_notes_url, catalog_id, minimum_host_version, plugin_kit_version, *packages = sys.argv[1:]
|
|
requested_plugin_kit_version = int(plugin_kit_version) if plugin_kit_version else None
|
|
|
|
def directory_metrics(path: pathlib.Path):
|
|
h = hashlib.sha256()
|
|
size = 0
|
|
for file_path in sorted(p for p in path.rglob("*") if p.is_file() and not any(part.startswith(".") for part in p.relative_to(path).parts)):
|
|
rel = file_path.relative_to(path).as_posix()
|
|
data = file_path.read_bytes()
|
|
h.update(rel.encode("utf-8"))
|
|
h.update(b"\0")
|
|
h.update(data)
|
|
h.update(b"\0")
|
|
size += len(data)
|
|
return h.hexdigest(), size
|
|
|
|
def file_metrics(path: pathlib.Path):
|
|
h = hashlib.sha256()
|
|
size = 0
|
|
with path.open("rb") as f:
|
|
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
|
h.update(chunk)
|
|
size += len(chunk)
|
|
return h.hexdigest(), size
|
|
|
|
def package_root(path: pathlib.Path):
|
|
if path.suffix == ".zip":
|
|
temp = pathlib.Path(tempfile.mkdtemp(prefix="mactools-plugin-catalog-"))
|
|
with zipfile.ZipFile(path) as zf:
|
|
zf.extractall(temp)
|
|
roots = [p for p in temp.iterdir() if p.name.endswith(".mactoolsplugin")]
|
|
if len(roots) != 1:
|
|
raise SystemExit(f"{path} must contain exactly one .mactoolsplugin root")
|
|
return roots[0]
|
|
return path
|
|
|
|
entries = []
|
|
plugin_kit_versions = set()
|
|
for raw in packages:
|
|
package_path = pathlib.Path(raw).expanduser().resolve()
|
|
if not package_path.exists():
|
|
raise SystemExit(f"Package not found: {package_path}")
|
|
|
|
root = package_root(package_path)
|
|
manifest_path = root / "plugin.json"
|
|
if not manifest_path.exists():
|
|
raise SystemExit(f"Missing plugin.json: {root}")
|
|
|
|
manifest = json.loads(manifest_path.read_text())
|
|
if "pluginKitVersion" not in manifest:
|
|
raise SystemExit(f"Missing pluginKitVersion: {manifest_path}")
|
|
manifest_plugin_kit_version = int(manifest["pluginKitVersion"])
|
|
plugin_kit_versions.add(manifest_plugin_kit_version)
|
|
if requested_plugin_kit_version is not None and manifest_plugin_kit_version != requested_plugin_kit_version:
|
|
raise SystemExit(
|
|
f"{manifest_path} uses pluginKitVersion {manifest_plugin_kit_version}, "
|
|
f"but --plugin-kit-version is {requested_plugin_kit_version}"
|
|
)
|
|
digest, size = directory_metrics(package_path) if package_path.is_dir() else file_metrics(package_path)
|
|
if mode == "debug":
|
|
url = package_path.as_uri()
|
|
else:
|
|
url = base_url.rstrip("/") + "/" + package_path.name
|
|
|
|
entries.append({
|
|
"id": manifest["id"],
|
|
"displayName": manifest.get("displayName", manifest["id"]),
|
|
"summary": manifest.get("summary", manifest.get("displayName", manifest["id"])),
|
|
"localizedMetadata": manifest.get("localizedMetadata"),
|
|
"version": manifest["version"],
|
|
"minimumHostVersion": manifest.get("minHostVersion", minimum_host_version),
|
|
"pluginKitVersion": manifest_plugin_kit_version,
|
|
"capabilities": manifest.get("capabilities", {
|
|
"primaryPanel": False,
|
|
"componentPanel": False,
|
|
"configuration": False,
|
|
}),
|
|
"permissions": manifest.get("permissions", []),
|
|
"package": {
|
|
"url": url,
|
|
"sha256": digest,
|
|
"size": size,
|
|
},
|
|
"releaseNotesURL": manifest.get("releaseNotesURL") or release_notes_url or None,
|
|
"category": manifest.get("category"),
|
|
"releaseChannel": manifest.get("releaseChannel"),
|
|
})
|
|
|
|
if requested_plugin_kit_version is None:
|
|
if len(plugin_kit_versions) != 1:
|
|
raise SystemExit(
|
|
"Packages must use one pluginKitVersion: "
|
|
+ ", ".join(str(version) for version in sorted(plugin_kit_versions))
|
|
)
|
|
catalog_plugin_kit_version = next(iter(plugin_kit_versions))
|
|
else:
|
|
catalog_plugin_kit_version = requested_plugin_kit_version
|
|
|
|
catalog = {
|
|
"schemaVersion": 1,
|
|
"catalogID": catalog_id,
|
|
"generatedAt": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
|
|
"minimumHostVersion": minimum_host_version,
|
|
"pluginKitVersion": catalog_plugin_kit_version,
|
|
"plugins": sorted(entries, key=lambda entry: entry["id"]),
|
|
"revoked": [],
|
|
}
|
|
|
|
output_path = pathlib.Path(output)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_path.write_text(json.dumps(catalog, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
|
|
PY
|