182 lines
5.9 KiB
Python
182 lines
5.9 KiB
Python
"""flow package — binary package management from manifest definitions."""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from typing import Any, Dict, Optional, Tuple
|
|
|
|
from flow.core.config import FlowContext
|
|
from flow.core.paths import INSTALLED_STATE
|
|
from flow.core.variables import substitute_template
|
|
|
|
|
|
def register(subparsers):
|
|
p = subparsers.add_parser("package", aliases=["pkg"], help="Manage binary packages")
|
|
sub = p.add_subparsers(dest="package_command")
|
|
|
|
# install
|
|
inst = sub.add_parser("install", help="Install packages from manifest")
|
|
inst.add_argument("packages", nargs="+", help="Package names to install")
|
|
inst.add_argument("--dry-run", action="store_true", help="Show what would be done")
|
|
inst.set_defaults(handler=run_install)
|
|
|
|
# list
|
|
ls = sub.add_parser("list", help="List installed and available packages")
|
|
ls.add_argument("--all", action="store_true", help="Show all available packages")
|
|
ls.set_defaults(handler=run_list)
|
|
|
|
# remove
|
|
rm = sub.add_parser("remove", help="Remove installed packages")
|
|
rm.add_argument("packages", nargs="+", help="Package names to remove")
|
|
rm.set_defaults(handler=run_remove)
|
|
|
|
p.set_defaults(handler=lambda ctx, args: p.print_help())
|
|
|
|
|
|
def _load_installed() -> dict:
|
|
if INSTALLED_STATE.exists():
|
|
with open(INSTALLED_STATE) as f:
|
|
return json.load(f)
|
|
return {}
|
|
|
|
|
|
def _save_installed(state: dict):
|
|
INSTALLED_STATE.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(INSTALLED_STATE, "w") as f:
|
|
json.dump(state, f, indent=2)
|
|
|
|
|
|
def _get_definitions(ctx: FlowContext) -> dict:
|
|
"""Get package definitions from manifest (binaries section)."""
|
|
return ctx.manifest.get("binaries", {})
|
|
|
|
|
|
def _resolve_download_url(
|
|
pkg_def: Dict[str, Any],
|
|
platform_str: str,
|
|
) -> Optional[Tuple[str, Dict[str, str]]]:
|
|
"""Build GitHub release download URL from package definition."""
|
|
source = pkg_def.get("source", "")
|
|
if not source.startswith("github:"):
|
|
return None
|
|
|
|
owner_repo = source[len("github:"):]
|
|
version = pkg_def.get("version", "")
|
|
asset_pattern = pkg_def.get("asset-pattern", "")
|
|
platform_map = pkg_def.get("platform-map", {})
|
|
|
|
mapping = platform_map.get(platform_str)
|
|
if not mapping:
|
|
return None
|
|
|
|
# Build template context
|
|
template_ctx = {**mapping, "version": version}
|
|
asset = substitute_template(asset_pattern, template_ctx)
|
|
url = f"https://github.com/{owner_repo}/releases/download/v{version}/{asset}"
|
|
|
|
template_ctx["downloadUrl"] = url
|
|
return url, template_ctx
|
|
|
|
|
|
def run_install(ctx: FlowContext, args):
|
|
definitions = _get_definitions(ctx)
|
|
installed = _load_installed()
|
|
platform_str = ctx.platform.platform
|
|
had_error = False
|
|
|
|
for pkg_name in args.packages:
|
|
pkg_def = definitions.get(pkg_name)
|
|
if not pkg_def:
|
|
ctx.console.error(f"Package not found in manifest: {pkg_name}")
|
|
had_error = True
|
|
continue
|
|
|
|
ctx.console.info(f"Installing {pkg_name} v{pkg_def.get('version', '?')}...")
|
|
|
|
result = _resolve_download_url(pkg_def, platform_str)
|
|
if not result:
|
|
ctx.console.error(f"No download available for {pkg_name} on {platform_str}")
|
|
had_error = True
|
|
continue
|
|
|
|
url, template_ctx = result
|
|
|
|
if args.dry_run:
|
|
ctx.console.info(f"[{pkg_name}] Would download: {url}")
|
|
install_script = pkg_def.get("install-script", "")
|
|
if install_script:
|
|
ctx.console.info(f"[{pkg_name}] Would run install script")
|
|
continue
|
|
|
|
# Run install script with template vars resolved
|
|
install_script = pkg_def.get("install-script", "")
|
|
if not install_script:
|
|
ctx.console.error(f"Package '{pkg_name}' has no install-script")
|
|
had_error = True
|
|
continue
|
|
|
|
resolved_script = substitute_template(install_script, template_ctx)
|
|
ctx.console.info(f"Running install script for {pkg_name}...")
|
|
proc = subprocess.run(
|
|
resolved_script, shell=True,
|
|
capture_output=False,
|
|
)
|
|
if proc.returncode != 0:
|
|
ctx.console.error(f"Install script failed for {pkg_name}")
|
|
had_error = True
|
|
continue
|
|
|
|
installed[pkg_name] = {
|
|
"version": pkg_def.get("version", ""),
|
|
"source": pkg_def.get("source", ""),
|
|
}
|
|
ctx.console.success(f"Installed {pkg_name} v{pkg_def.get('version', '')}")
|
|
|
|
_save_installed(installed)
|
|
if had_error:
|
|
sys.exit(1)
|
|
|
|
|
|
def run_list(ctx: FlowContext, args):
|
|
definitions = _get_definitions(ctx)
|
|
installed = _load_installed()
|
|
|
|
headers = ["PACKAGE", "INSTALLED", "AVAILABLE"]
|
|
rows = []
|
|
|
|
if args.all:
|
|
# Show all defined packages
|
|
if not definitions:
|
|
ctx.console.info("No packages defined in manifest.")
|
|
return
|
|
for name, pkg_def in sorted(definitions.items()):
|
|
inst_ver = installed.get(name, {}).get("version", "-")
|
|
avail_ver = pkg_def.get("version", "?")
|
|
rows.append([name, inst_ver, avail_ver])
|
|
else:
|
|
# Show installed only
|
|
if not installed:
|
|
ctx.console.info("No packages installed.")
|
|
return
|
|
for name, info in sorted(installed.items()):
|
|
avail = definitions.get(name, {}).get("version", "?")
|
|
rows.append([name, info.get("version", "?"), avail])
|
|
|
|
ctx.console.table(headers, rows)
|
|
|
|
|
|
def run_remove(ctx: FlowContext, args):
|
|
installed = _load_installed()
|
|
|
|
for pkg_name in args.packages:
|
|
if pkg_name not in installed:
|
|
ctx.console.warn(f"Package not installed: {pkg_name}")
|
|
continue
|
|
|
|
# Remove from installed state
|
|
del installed[pkg_name]
|
|
ctx.console.success(f"Removed {pkg_name} from installed packages")
|
|
ctx.console.warn("Note: binary files were not automatically deleted. Remove manually if needed.")
|
|
|
|
_save_installed(installed)
|