Skip to content

Instantly share code, notes, and snippets.

@MagerValp
Last active April 20, 2026 12:25
Show Gist options
  • Select an option

  • Save MagerValp/7e2ca0e8df7188c0b2103f18353fa3c4 to your computer and use it in GitHub Desktop.

Select an option

Save MagerValp/7e2ca0e8df7188c0b2103f18353fa3c4 to your computer and use it in GitHub Desktop.
Convert munki manifests and pkgsinfo between plist, yaml, and json
#!/usr/bin/env -S uv run --python cpython@3.14 --script
# /// script
# dependencies = [
# "PyYAML",
# ]
# [tool.uv]
# exclude-newer = "7 days"
# ///
"""
# Version History
## 0.1
* Initial prototype
"""
import os
import sys
import argparse
from pathlib import Path
from typing import Any
from collections.abc import Callable
import plistlib
import yaml
import json
# Loading
def load_plist(path: Path) -> dict[str, Any] | None:
with open(path, "rb") as f:
try:
resource = plistlib.load(f)
except:
return None
assert isinstance(resource, dict)
return resource
def load_yaml(path: Path) -> dict[str, Any] | None:
with open(path, "rt") as f:
try:
resource = yaml.safe_load(f)
except:
return None
assert isinstance(resource, dict)
return resource
def load_json(path: Path) -> dict[str, Any] | None:
with open(path, "rt") as f:
try:
resource = json.load(f)
except:
return None
assert isinstance(resource, dict)
return resource
def load_resource(path: Path) -> dict[str, Any]:
resource = load_plist(path) or load_yaml(path) or load_json(path)
if not isinstance(resource, dict):
die(f"๐Ÿ›‘ Failed to load {path}: Not a dictionary object")
return resource
def load_resources(resource_root: Path) -> dict[str, dict[str, Any]]:
resources: dict[str, dict[str, Any]] = {}
for curr_dir, dirnames, filenames in resource_root.walk():
for filename in filenames:
if filename.startswith("."):
continue
curr_file = curr_dir / filename
rel_file = curr_file.relative_to(resource_root)
resources[str(rel_file)] = load_resource(curr_file)
return resources
# Saving
def save_plist(resource: dict[str, Any], path: Path):
with open(path, "wb") as f:
plistlib.dump(resource, f)
def save_yaml(resource: dict[str, Any], path: Path):
with open(path, "wt") as f:
yaml.safe_dump(resource, f)
def save_json(resource: dict[str, Any], path: Path):
with open(path, "wt") as f:
json.dump(resource, f, indent=4, default=str)
def save_resource(resource: dict[str, Any], path: Path, save_function: Callable):
save_function(resource, path)
def save_resources(resources: dict[str, dict[str, Any]], dest_root: Path, save_function: Callable, file_ext=None):
for key, data in sorted(resources.items()):
dest_dir = dest_root
parts = key.split("/")
filename = Path(parts.pop()).stem
for subdir in parts:
dest_dir = dest_dir / subdir
if not dest_dir.exists():
os.makedirs(dest_dir)
if file_ext:
path = dest_dir/ ( filename + f".{file_ext}" )
else:
path = dest_dir / filename
save_resource(data, path, save_function)
# Main
def die(msg, code=1) -> Never:
print(msg, file=sys.stderr)
sys.exit(code)
def main(argv: list[str]) -> int:
p = argparse.ArgumentParser()
p.add_argument("--version", action="store_true", help="Print version number")
p_format_mutex = p.add_mutually_exclusive_group(required=True)
p_format_mutex.add_argument("-y", "--yaml", action="store_true", help="Output YAML format")
p_format_mutex.add_argument("-p", "--plist", action="store_true", help="Output property list format")
p_format_mutex.add_argument("-j", "--json", action="store_true", help="Output JSON format")
p.add_argument("source", help="Source Munki repo")
p.add_argument("dest", help="Destination Munki repo")
args = p.parse_args(argv[1:])
if args.version:
print(__version__)
return 0
source_path = Path(args.source)
source_manifests = source_path / "manifests"
source_pkgsinfo = source_path / "pkgsinfo"
if not source_manifests.is_dir():
die("๐Ÿ›‘ {source_path} does not look like a Munki repo")
manifests = load_resources(source_manifests)
pkgsinfo = load_resources(source_pkgsinfo)
dest_path = Path(args.dest)
dest_manifests = dest_path / "manifests"
dest_pkgsinfo = dest_path / "pkgsinfo"
for path in [dest_path, dest_manifests, dest_pkgsinfo]:
if not path.exists():
os.makedirs(path)
if args.yaml:
save_function = save_yaml
file_ext = "yaml"
elif args.plist:
save_function = save_plist
file_ext = "plist"
elif args.json:
save_function = save_json
file_ext = "json"
save_resources(manifests, dest_manifests, save_function)
save_resources(pkgsinfo, dest_pkgsinfo, save_function, file_ext)
return 0
if __name__ == '__main__':
if isinstance(__doc__, str):
for line in __doc__.splitlines():
if line.startswith("## "):
__version__ = line[3:].rstrip()
status = 1
if sys.__stdout__ is not None and sys.__stdout__.isatty():
import traceback
import pdb
try:
status = main(sys.argv)
except:
type, value, tb = sys.exc_info()
if type is not SystemExit:
traceback.print_exc()
pdb.post_mortem(tb)
else:
status = main(sys.argv)
sys.exit(status)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment