Release PPT workflow 0.7.1 with portable installation and production components
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install the checked-out root Skill, or verify it, without another Skill."""
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
raise SystemExit("Use Python 3.12+ to install the complete native workflow.")
|
||||
import tomllib
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def roster(root):
|
||||
files = {}
|
||||
for path in sorted(root.rglob("*")):
|
||||
relative = path.relative_to(root)
|
||||
if any(part.startswith(".") or part == "__pycache__" for part in relative.parts):
|
||||
continue
|
||||
if path.is_symlink():
|
||||
raise ValueError(f"Source symlink is unsupported: {relative}")
|
||||
if path.is_file() and path.suffix != ".pyc":
|
||||
files[relative.as_posix()] = sha(path)
|
||||
return files
|
||||
|
||||
|
||||
def safe_target(path):
|
||||
path = Path(os.path.abspath(path.expanduser()))
|
||||
if any(item.is_symlink() for item in (path, *path.parents)):
|
||||
raise ValueError(f"Destination symlink is unsupported: {path}")
|
||||
return path
|
||||
|
||||
|
||||
def compiler_integrity(source):
|
||||
provenance = json.loads((source / "runtime/provenance.json").read_text())
|
||||
root = source / "runtime/ppt-master"
|
||||
for item in provenance["files"]:
|
||||
path = root / item["path"]
|
||||
if not path.resolve().is_relative_to(root) or sha(path) != item["sha256"]:
|
||||
raise ValueError(f"Compiler integrity failed: {item['path']}")
|
||||
return len(provenance["files"])
|
||||
|
||||
|
||||
def runtime_check(python, source):
|
||||
code = """
|
||||
import importlib.metadata, json, pathlib, re, sys
|
||||
if sys.version_info < (3, 12):
|
||||
raise RuntimeError('Native runtime needs Python 3.12+')
|
||||
source = pathlib.Path(sys.argv[1])
|
||||
versions = {}
|
||||
for line in (source / 'runtime/requirements.txt').read_text().splitlines():
|
||||
if not line or line.startswith('#'): continue
|
||||
name, expected = line.split('==')
|
||||
actual = importlib.metadata.version(name)
|
||||
if actual != expected: raise RuntimeError(f'{name}: expected {expected}, got {actual}')
|
||||
versions[name] = actual
|
||||
sys.path.insert(0, str(source / 'scripts'))
|
||||
from native_export import load_compiler
|
||||
load_compiler()
|
||||
print(json.dumps({'python': sys.version.split()[0], 'packages': versions, 'compiler_loaded': True}))
|
||||
"""
|
||||
result = subprocess.run([str(python), "-c", code, str(source)],
|
||||
text=True, capture_output=True, check=True)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def config_plan(codex, target):
|
||||
path = safe_target(codex / "config.toml")
|
||||
before = path.read_text(encoding="utf-8") if path.is_file() else ""
|
||||
data = tomllib.loads(before)
|
||||
entries = data.get("skills", {}).get("config", [])
|
||||
root_entry = str(target / "SKILL.md")
|
||||
nested = str(target / "runtime/ppt-master/SKILL.md")
|
||||
if any(item.get("path") == root_entry and item.get("enabled") is False for item in entries):
|
||||
raise ValueError("Root Skill is disabled in Codex; preserve that decision and resolve it explicitly.")
|
||||
matches = [item for item in entries if item.get("path") == nested]
|
||||
if matches:
|
||||
if len(matches) != 1 or matches[0].get("enabled") is not False:
|
||||
raise ValueError("Conflicting nested attribution entry; resolve this exact Codex entry explicitly.")
|
||||
return path, before, before
|
||||
addition = f"\n[[skills.config]]\npath = {json.dumps(nested, ensure_ascii=False)}\nenabled = false\n"
|
||||
after = before + addition
|
||||
parsed = tomllib.loads(after)
|
||||
expected = copy.deepcopy(data)
|
||||
expected.setdefault("skills", {}).setdefault("config", []).append({"path": nested, "enabled": False})
|
||||
if parsed != expected:
|
||||
raise ValueError("Codex configuration change affected unrelated fields.")
|
||||
return path, before, after
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--dest", type=Path, required=True, help="Exact installed Skill directory")
|
||||
parser.add_argument("--codex-home", type=Path, help="Exclude only the nested attribution Skill")
|
||||
parser.add_argument("--venv", type=Path, help="Dedicated Python 3.12+ virtual environment")
|
||||
parser.add_argument("--prepare-runtime", action="store_true", help="Create venv and install pinned packages")
|
||||
parser.add_argument("--replace", action="store_true", help="Back up and replace an existing Skill")
|
||||
parser.add_argument("--backup-root", type=Path, help="Backup root outside Skill discovery directories")
|
||||
parser.add_argument("--check", action="store_true", help="Read-only payload, configuration and runtime check")
|
||||
args = parser.parse_args()
|
||||
source = Path(__file__).resolve().parents[1]
|
||||
target = safe_target(args.dest)
|
||||
if target == source or target.is_relative_to(source) or source.is_relative_to(target):
|
||||
raise ValueError("Source and installed destination must be separate directories.")
|
||||
if target.name != "ppt-template-workflow":
|
||||
raise ValueError("Destination must end in ppt-template-workflow.")
|
||||
version_match = re.search(r'^ version: "([0-9.]+)"$', (source / "SKILL.md").read_text(), re.M)
|
||||
if not version_match:
|
||||
raise ValueError("Missing root Skill version.")
|
||||
files = roster(source)
|
||||
compiler_files = compiler_integrity(source)
|
||||
if args.prepare_runtime and (args.venv is None or args.check):
|
||||
raise ValueError("--prepare-runtime requires --venv and cannot be combined with --check.")
|
||||
codex = safe_target(args.codex_home) if args.codex_home else None
|
||||
config = config_plan(codex, target) if codex else None
|
||||
if args.check:
|
||||
if not target.is_dir() or roster(target) != files:
|
||||
raise ValueError("Installed payload differs from the selected source.")
|
||||
compiler_integrity(target)
|
||||
if config and config[1] != config[2]:
|
||||
raise ValueError("Nested compiler attribution entry has not been disabled.")
|
||||
elif target.exists() and not args.replace:
|
||||
raise FileExistsError("Destination exists; an authorized upgrade needs --replace (with backup).")
|
||||
elif target.exists() and not target.is_dir():
|
||||
raise ValueError("Existing destination is not a Skill directory.")
|
||||
runtime = None
|
||||
if args.venv:
|
||||
venv = safe_target(args.venv)
|
||||
if venv == target or venv.is_relative_to(target) or venv == source or venv.is_relative_to(source):
|
||||
raise ValueError("Runtime must be outside source and installed Skill.")
|
||||
python = safe_target(venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python")) if os.name == "nt" else venv / "bin/python"
|
||||
# POSIX venv interpreters are normally symlinks to their base Python.
|
||||
if args.prepare_runtime:
|
||||
if not python.is_file():
|
||||
subprocess.run([sys.executable, "-m", "venv", str(venv)], check=True)
|
||||
subprocess.run([str(python), "-m", "pip", "install", "--disable-pip-version-check",
|
||||
"-r", str(source / "runtime/requirements.txt")], check=True)
|
||||
runtime = {"executable": str(python), **runtime_check(python, source)}
|
||||
commit = subprocess.run(["git", "-C", str(source), "rev-parse", "HEAD"],
|
||||
capture_output=True, text=True)
|
||||
dirty = subprocess.run(["git", "-C", str(source), "status", "--porcelain"],
|
||||
capture_output=True, text=True) if commit.returncode == 0 else None
|
||||
receipt = {"schema_version": 1, "skill": "ppt-template-workflow",
|
||||
"version": version_match[1], "source_commit": commit.stdout.strip() if commit.returncode == 0 else None,
|
||||
"source_dirty": bool(dirty.stdout) if dirty and dirty.returncode == 0 else None,
|
||||
"installed_path": str(target), "payload": files, "compiler_files": compiler_files,
|
||||
"runtime": runtime, "status": "native-ready" if runtime else "files-only",
|
||||
"codex_attribution_disabled": bool(config), "checked": args.check}
|
||||
if not args.check:
|
||||
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "-" + uuid.uuid4().hex[:8]
|
||||
backup_root = safe_target(args.backup_root or
|
||||
((codex / "backups/ppt-template-workflow") if codex else target.parent.parent / "skill-backups"))
|
||||
if (backup_root == target or backup_root.is_relative_to(target) or backup_root.is_relative_to(target.parent)
|
||||
or backup_root == source or backup_root.is_relative_to(source)):
|
||||
raise ValueError("Backup must be outside source and Skill discovery directory.")
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
stage = Path(tempfile.mkdtemp(prefix=".ppt-install-", dir=target.parent))
|
||||
backup = backup_root / stamp / "ppt-template-workflow"
|
||||
moved = False
|
||||
config_written = False
|
||||
config_backup = None
|
||||
try:
|
||||
for relative in files:
|
||||
destination = stage / relative
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source / relative, destination)
|
||||
if roster(stage) != files:
|
||||
raise ValueError("Staged installation hash mismatch.")
|
||||
if target.exists() or (config and config[1] != config[2]):
|
||||
backup.parent.mkdir(parents=True, exist_ok=False, mode=0o700)
|
||||
if target.exists():
|
||||
target.rename(backup)
|
||||
moved = True
|
||||
if config and config[1] != config[2]:
|
||||
path, before, after = config
|
||||
if (path.read_text(encoding="utf-8") if path.is_file() else "") != before:
|
||||
raise ValueError("Codex configuration changed during installation.")
|
||||
config_backup = backup.parent / "config.toml.before"
|
||||
config_backup.write_text(before, encoding="utf-8")
|
||||
config_backup.chmod(0o600)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(after, encoding="utf-8")
|
||||
if not before:
|
||||
path.chmod(0o600)
|
||||
config_written = True
|
||||
receipt.update(backup=str(backup) if moved else None,
|
||||
config_backup=str(config_backup) if config_backup else None,
|
||||
installed_at=datetime.now(timezone.utc).isoformat())
|
||||
(stage / ".installation.json").write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
stage.rename(target)
|
||||
except Exception:
|
||||
if config_written:
|
||||
config[0].write_text(config[1], encoding="utf-8")
|
||||
if moved and not target.exists():
|
||||
backup.rename(target)
|
||||
raise
|
||||
finally:
|
||||
if stage.exists():
|
||||
shutil.rmtree(stage)
|
||||
summary = {key: value for key, value in receipt.items() if key != "payload"}
|
||||
summary["payload_files"] = len(files)
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.version_info < (3, 12):
|
||||
raise SystemExit("Use Python 3.12+ to install the complete native workflow.")
|
||||
try:
|
||||
main()
|
||||
except (OSError, ValueError, subprocess.CalledProcessError) as error:
|
||||
raise SystemExit(f"Installation failed: {error}")
|
||||
Reference in New Issue
Block a user