|
| 1 | +"""WESL packages bundled with mewnala, and wiring them into a project's `wesl.toml` |
| 2 | +so editor tooling (wgsl-analyzer) resolves `import processing::...` and |
| 3 | +`import lygia::...` the same way `Shader` does at runtime.""" |
| 4 | + |
| 5 | +import json |
| 6 | +import shutil |
| 7 | +from pathlib import Path |
| 8 | + |
| 9 | +PACKAGES = ("processing", "lygia") |
| 10 | + |
| 11 | +_BEGIN = "# BEGIN mewnala" |
| 12 | +_END = "# END mewnala" |
| 13 | +_VENDOR_DIR = Path(".mewnala") / "shaders" |
| 14 | + |
| 15 | + |
| 16 | +def shader_packages(): |
| 17 | + """Return `{name: directory}` for the WESL packages shipped in the wheel.""" |
| 18 | + root = Path(__file__).parent / "shaders" |
| 19 | + found = {name: root / name for name in PACKAGES if (root / name / "wesl.toml").is_file()} |
| 20 | + if not found: |
| 21 | + raise FileNotFoundError( |
| 22 | + f"no bundled shader packages under {root}; " |
| 23 | + "in a source checkout run `cargo run -p bundle_shaders`" |
| 24 | + ) |
| 25 | + return found |
| 26 | + |
| 27 | + |
| 28 | +def _toml_path(path, base): |
| 29 | + try: |
| 30 | + p = path.relative_to(base) |
| 31 | + except ValueError: |
| 32 | + p = path |
| 33 | + return json.dumps(p.as_posix()) |
| 34 | + |
| 35 | + |
| 36 | +def _dependency_block(packages, base): |
| 37 | + lines = [_BEGIN] |
| 38 | + for name, path in packages.items(): |
| 39 | + lines.append(f"{name} = {{ path = {_toml_path(path, base)} }}") |
| 40 | + lines.append(_END) |
| 41 | + return lines |
| 42 | + |
| 43 | + |
| 44 | +def _splice(text, block): |
| 45 | + lines = text.splitlines() |
| 46 | + if _BEGIN in lines: |
| 47 | + start = lines.index(_BEGIN) |
| 48 | + try: |
| 49 | + end = lines.index(_END, start) |
| 50 | + except ValueError: |
| 51 | + raise ValueError(f"`{_BEGIN}` without a matching `{_END}`") from None |
| 52 | + lines[start : end + 1] = block |
| 53 | + else: |
| 54 | + header = next( |
| 55 | + (i for i, l in enumerate(lines) if l.split("#", 1)[0].strip() == "[dependencies]"), |
| 56 | + None, |
| 57 | + ) |
| 58 | + if header is None: |
| 59 | + if lines and lines[-1].strip(): |
| 60 | + lines.append("") |
| 61 | + lines += ["[dependencies]", *block] |
| 62 | + else: |
| 63 | + lines[header + 1 : header + 1] = block |
| 64 | + return "\n".join(lines) + "\n" |
| 65 | + |
| 66 | + |
| 67 | +def _validate(text, path): |
| 68 | + try: |
| 69 | + import tomllib |
| 70 | + except ImportError: |
| 71 | + return |
| 72 | + try: |
| 73 | + doc = tomllib.loads(text) |
| 74 | + except tomllib.TOMLDecodeError as e: |
| 75 | + raise ValueError(f"updating {path} would produce invalid TOML ({e}); edit it by hand") from None |
| 76 | + if not isinstance(doc.get("dependencies"), dict): |
| 77 | + raise ValueError(f"{path}: `dependencies` is not a table") |
| 78 | + |
| 79 | + |
| 80 | +def shader_workspace(path=".", vendor=False): |
| 81 | + """Create or update `wesl.toml` in `path` so the bundled `processing` and |
| 82 | + `lygia` packages resolve in the editor. |
| 83 | +
|
| 84 | + Dependencies point into the installed package. With `vendor=True` they are |
| 85 | + copied to `.mewnala/shaders` first, for editors that can't see the |
| 86 | + environment's site-packages or for checking them in. Rerun after upgrading |
| 87 | + mewnala either way. Returns the path of the `wesl.toml`. |
| 88 | + """ |
| 89 | + base = Path(path).resolve() |
| 90 | + base.mkdir(parents=True, exist_ok=True) |
| 91 | + packages = shader_packages() |
| 92 | + |
| 93 | + if vendor: |
| 94 | + dst = base / _VENDOR_DIR |
| 95 | + vendored = {} |
| 96 | + for name, src in packages.items(): |
| 97 | + target = dst / name |
| 98 | + if target.exists(): |
| 99 | + shutil.rmtree(target) |
| 100 | + shutil.copytree(src, target) |
| 101 | + vendored[name] = target |
| 102 | + packages = vendored |
| 103 | + |
| 104 | + manifest = base / "wesl.toml" |
| 105 | + block = _dependency_block(packages, base) |
| 106 | + if manifest.exists(): |
| 107 | + text = _splice(manifest.read_text(encoding="utf-8"), block) |
| 108 | + else: |
| 109 | + header = ['edition = "2026_pre"', 'root = "."'] |
| 110 | + if vendor: |
| 111 | + header.append(f'exclude = ["{_VENDOR_DIR.as_posix()}/**"]') |
| 112 | + text = "\n".join([*header, "", "[dependencies]", *block]) + "\n" |
| 113 | + _validate(text, manifest) |
| 114 | + manifest.write_text(text, encoding="utf-8") |
| 115 | + return manifest |
0 commit comments