#!/usr/bin/env python3 """Resolve a site profile's chosen plugins plus their hard-dependency closure. Printed as a comma-separated list, dependencies before dependants, so the caller can stage them in order. This lives in its own file because TWO builders need the same answer: scripts/build-site.sh (Linux) and deploy/windows/installer/build-installer.ps1 (Windows). It was inline in build-site.sh; a PowerShell reimplementation would have been a second copy of the closure rules, free to drift from this one and produce a bundle whose plugin set did not match the profile it was built from. Usage: resolve_plugin_closure.py """ import json import os import sys def load_dependencies(plugins_dir, name): manifest = os.path.join(plugins_dir, name, 'manifest.json') if not os.path.exists(manifest): sys.exit('profile plugin not found on disk: %s' % name) with open(manifest) as fh: declared = json.load(fh).get('dependencies', []) names = [] for dep in declared: # name-only (strip any PEP440 range) for sep in '><=!~ ': dep = dep.split(sep)[0] names.append(dep.strip()) return names def resolve(profile_path, repo): with open(profile_path) as fh: chosen = json.load(fh).get('plugins', []) plugins_dir = os.path.join(repo, 'plugins') closure, seen = [], set() def add(name): if name in seen: return seen.add(name) for dep in load_dependencies(plugins_dir, name): add(dep) closure.append(name) for name in chosen: add(name) return closure def main(): if len(sys.argv) != 3: sys.exit('usage: resolve_plugin_closure.py ') print(','.join(resolve(sys.argv[1], sys.argv[2]))) if __name__ == '__main__': main()