🐛 fix(playbook): support toml without tomllib

This commit is contained in:
csh 2026-01-23 16:54:53 +08:00
parent ab0dd11afc
commit ea00d43bb8
2 changed files with 123 additions and 4 deletions

3
.gitattributes vendored
View File

@ -39,10 +39,9 @@
*.cppm text eol=lf *.cppm text eol=lf
*.mpp text eol=lf *.mpp text eol=lf
*.cmake text eol=lf *.cmake text eol=lf
*.clangd text eol=lf
CMakeLists.txt text eol=lf CMakeLists.txt text eol=lf
CMakePresets.json text eol=lf CMakePresets.json text eol=lf
*.clangd text eol=lf
.clangd text eol=lf
# Binary files (no line-ending conversion). # Binary files (no line-ending conversion).
*.png binary *.png binary

View File

@ -5,7 +5,10 @@ from pathlib import Path
from shutil import copy2, copytree, which from shutil import copy2, copytree, which
import subprocess import subprocess
try:
import tomllib import tomllib
except ModuleNotFoundError: # Python < 3.11
tomllib = None
ORDER = ["vendor", "sync_templates", "sync_standards", "install_skills", "format_md"] ORDER = ["vendor", "sync_templates", "sync_standards", "install_skills", "format_md"]
SCRIPT_DIR = Path(__file__).resolve().parent SCRIPT_DIR = Path(__file__).resolve().parent
@ -16,8 +19,125 @@ def usage() -> str:
return "Usage:\n python scripts/playbook.py -config <path>\n python scripts/playbook.py -h" return "Usage:\n python scripts/playbook.py -config <path>\n python scripts/playbook.py -h"
def strip_inline_comment(value: str) -> str:
in_single = False
in_double = False
escape = False
for idx, ch in enumerate(value):
if escape:
escape = False
continue
if in_double and ch == "\\":
escape = True
continue
if ch == "'" and not in_double:
in_single = not in_single
continue
if ch == '"' and not in_single:
in_double = not in_double
continue
if ch == "#" and not in_single and not in_double:
return value[:idx].rstrip()
return value
def split_list_items(raw: str) -> list[str]:
items: list[str] = []
buf: list[str] = []
in_single = False
in_double = False
escape = False
for ch in raw:
if escape:
buf.append(ch)
escape = False
continue
if in_double and ch == "\\":
buf.append(ch)
escape = True
continue
if ch == "'" and not in_double:
in_single = not in_single
buf.append(ch)
continue
if ch == '"' and not in_single:
in_double = not in_double
buf.append(ch)
continue
if ch == "," and not in_single and not in_double:
items.append("".join(buf).strip())
buf = []
continue
buf.append(ch)
tail = "".join(buf).strip()
if tail:
items.append(tail)
return items
def parse_toml_value(raw: str) -> object:
value = raw.strip()
if not value:
return ""
if value.startswith("[") and value.endswith("]"):
inner = value[1:-1].strip()
if not inner:
return []
return [parse_toml_value(item) for item in split_list_items(inner)]
lowered = value.lower()
if lowered == "true":
return True
if lowered == "false":
return False
if value[0] in ("'", '"') and value[-1] == value[0]:
if value[0] == "'":
return value[1:-1]
import ast
try:
return ast.literal_eval(value)
except (ValueError, SyntaxError):
return value[1:-1]
try:
if "." in value:
return float(value)
return int(value)
except ValueError:
return value
def loads_toml_minimal(raw: str) -> dict:
data: dict[str, dict] = {}
current = None
for line in raw.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if stripped.startswith("[") and stripped.endswith("]"):
section = stripped[1:-1].strip()
if not section:
raise ValueError("empty section header")
current = data.setdefault(section, {})
if not isinstance(current, dict):
raise ValueError(f"invalid section: {section}")
continue
if "=" not in stripped:
raise ValueError(f"invalid line: {line}")
key, value = stripped.split("=", 1)
key = key.strip()
if not key:
raise ValueError("missing key")
value = strip_inline_comment(value.strip())
target = current if current is not None else data
target[key] = parse_toml_value(value)
return data
def load_config(path: Path) -> dict: def load_config(path: Path) -> dict:
return tomllib.loads(path.read_text(encoding="utf-8")) raw = path.read_text(encoding="utf-8")
if tomllib is not None:
return tomllib.loads(raw)
return loads_toml_minimal(raw)
def log(message: str) -> None: def log(message: str) -> None: