#!/usr/bin/env python3 """Add a bundled native builder to a clean initialized project, without relocking.""" from __future__ import annotations import argparse import json from pathlib import Path import shutil import sys import tempfile import ppt_project as project_cli import template_ref as ref def prepare(root: Path): root = project_cli.root_path(root) config, _ = project_cli.check(root) ref.require(config["authoring"]["route"] == "native", "prepare requires authoring.route=native") ref.require(config["build"]["entrypoint"] is None and not config["build"]["inputs"], "prepare refuses an existing/custom builder") environment = project_cli.safe_path(root, config["environment"], "environment") old_environment = environment.read_bytes() env = ref.read_json(environment, (1,)) ref.require(not env["packages"] and not env["fonts"] and not env["tools"], "prepare refuses to overwrite a configured environment") manifest = ref.load_manifest(root / config["template"]["manifest"]) ref.require(manifest["schema_version"] == 2, "prepare requires manifest schema 2") template_root = (root / config["template"]["manifest"]).parent style = ref.read_json(ref.file_path(template_root, manifest["style"], "style"), None) workflow = root / "vendor/native-workflow" builder = root / "tools/native_builder.py" ref.require(not workflow.exists() and not builder.exists(), "prepare refuses existing native-workflow or native_builder.py paths") skill = Path(__file__).resolve().parent.parent provenance = ref.read_json(skill / "runtime/provenance.json", (1,)) package_lines = (skill / "runtime/requirements.txt").read_text(encoding="utf-8").splitlines() packages = dict(line.split("==", 1) for line in package_lines if line.strip() and not line.startswith("#")) fonts = [{"role": role, **entry, "distributed": False, "embedded": False, "verification": "declared by the selected template; installation, file identity and licenses require destination readback"} for role, entry in style["typography"]["families"].items()] new_env = {"schema_version": 1, "python": {"minimum": "3.12", "project_cli_minimum": "3.10"}, "packages": packages, "fonts": fonts, "tools": [{"name": provenance["component"], "version": provenance["version"], "path": "vendor/native-workflow/runtime/ppt-master", "source": provenance["source"], "license": provenance["license"], "upstream_modified": False}], "external_executables": [], "capabilities": {"image_generation": "host tool when needed", "PowerPoint": "host application and supported UI tool for application checks"}} old_config = (root / "project.json").read_bytes() created = [] with tempfile.TemporaryDirectory(prefix="native-prepare-", dir=root.parent) as temporary: staging = Path(temporary) / "native-workflow" staging.mkdir() shutil.copytree(skill / "runtime", staging / "runtime", ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo")) (staging / "scripts").mkdir() for name in ("native_export.py", "finalize_theme.py", "template_ref.py", "math_component.py", "component_typography.py", "paragraph_layout.py", "text_block.py", "layout_spacing.py"): shutil.copyfile(skill / "scripts" / name, staging / "scripts" / name) # The snapshot is an explicit build input; no installed Skill is needed # after this project is moved to a different directory or machine. inputs = ["vendor/native-workflow/" + path.relative_to(staging).as_posix() for path in sorted(staging.rglob("*")) if path.is_file()] try: shutil.copytree(staging, workflow) created.append(workflow) with builder.open("xb") as target: target.write((skill / "scripts/native_builder.py").read_bytes()) created.append(builder) config["build"]["entrypoint"] = "tools/native_builder.py" config["build"]["inputs"] = inputs project_cli.atomic_json(environment, new_env) project_cli.atomic_json(root / "project.json", config) project_cli.check(root, author_lock=False) except Exception: environment.write_bytes(old_environment) (root / "project.json").write_bytes(old_config) for item in reversed(created): shutil.rmtree(item) if item.is_dir() else item.unlink(missing_ok=True) raise return {"ok": True, "project": str(root), "builder": config["build"]["entrypoint"], "copied_inputs": len(inputs), "author_lock_updated": False, "next": "Review source and environment changes, add finalized page SVGs/notes, then run tools/ppt_project.py lock --project ."} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--project", type=Path, required=True) args = parser.parse_args() try: print(json.dumps(prepare(args.project), ensure_ascii=False, indent=2)) return 0 except (ref.InputError, OSError, ValueError, KeyError, TypeError) as error: print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr) return 2 if __name__ == "__main__": raise SystemExit(main())