mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-25 04:59:01 +08:00
- 在 NuGet 包中实现可选的 transitive global usings 功能 - 添加 XML 配置方式启用模块级自动命名空间导入 - 支持通过 GFrameworkExcludedUsing 排除特定命名空间 - 为所有运行时模块生成对应的 buildTransitive props 文件 - 添加 Python 脚本自动生成和验证命名空间配置 - 在文档中添加新的安装配置说明 - 创建单元测试验证生成脚本的同步状态
223 lines
7.1 KiB
Python
223 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT_DIR = Path(__file__).resolve().parent.parent
|
|
CONFIG_PATH = ROOT_DIR / "global-usings.modules.json"
|
|
AUTO_GENERATED_START = "<!-- <auto-generated gframework-transitive-global-usings> -->"
|
|
AUTO_GENERATED_END = "<!-- </auto-generated gframework-transitive-global-usings> -->"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ModuleConfig:
|
|
project_path: Path
|
|
namespaces: tuple[str, ...]
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate optional transitive global usings for packable GFramework runtime modules.",
|
|
)
|
|
parser.add_argument(
|
|
"--check",
|
|
action="store_true",
|
|
help="Validate that generated files are up to date instead of writing them.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def load_text(path: Path) -> tuple[str, str]:
|
|
raw = path.read_bytes()
|
|
if raw.startswith(b"\xef\xbb\xbf"):
|
|
return raw.decode("utf-8-sig"), "utf-8-sig"
|
|
|
|
return raw.decode("utf-8"), "utf-8"
|
|
|
|
|
|
def write_text(path: Path, content: str, encoding: str) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content, encoding=encoding)
|
|
|
|
|
|
def load_modules() -> list[ModuleConfig]:
|
|
data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
|
modules: list[ModuleConfig] = []
|
|
|
|
for entry in data["modules"]:
|
|
project_path = ROOT_DIR / entry["project"]
|
|
namespaces = tuple(dict.fromkeys(entry["namespaces"]))
|
|
modules.append(ModuleConfig(project_path=project_path, namespaces=namespaces))
|
|
|
|
return modules
|
|
|
|
|
|
def discover_runtime_modules() -> list[Path]:
|
|
projects: list[Path] = []
|
|
|
|
for project_path in sorted(ROOT_DIR.rglob("*.csproj")):
|
|
if "obj" in project_path.parts or "bin" in project_path.parts:
|
|
continue
|
|
|
|
if project_path.name == "GFramework.csproj":
|
|
continue
|
|
|
|
project_name = project_path.stem
|
|
if not project_name.startswith("GFramework."):
|
|
continue
|
|
|
|
if ".Tests" in project_name or "SourceGenerators" in project_name:
|
|
continue
|
|
|
|
csproj_text, _ = load_text(project_path)
|
|
if re.search(r"<IsPackable>\s*false\s*</IsPackable>", csproj_text, re.IGNORECASE):
|
|
continue
|
|
|
|
projects.append(project_path)
|
|
|
|
return projects
|
|
|
|
|
|
def resolve_package_id(project_path: Path) -> str:
|
|
project_text, _ = load_text(project_path)
|
|
match = re.search(r"<PackageId>(.*?)</PackageId>", project_text, re.DOTALL)
|
|
if match is None:
|
|
return project_path.stem
|
|
|
|
package_id = match.group(1).strip()
|
|
return package_id.replace("$(AssemblyName)", project_path.stem)
|
|
|
|
|
|
def sanitize_identifier(text: str) -> str:
|
|
return re.sub(r"[^A-Za-z0-9_]", "_", text)
|
|
|
|
|
|
def render_props(module: ModuleConfig) -> str:
|
|
item_name = f"_{sanitize_identifier(module.project_path.stem)}_TransitiveUsing"
|
|
lines = [
|
|
"<Project>",
|
|
" <!-- This file is generated by scripts/generate-module-global-usings.py. -->",
|
|
" <!-- EnableGFrameworkGlobalUsings=true enables the transitive global usings from this package. -->",
|
|
" <!-- Add <GFrameworkExcludedUsing Include=\"Namespace\" /> to opt out of specific namespaces. -->",
|
|
" <ItemGroup Condition=\"'$(EnableGFrameworkGlobalUsings)' == 'true'\">",
|
|
]
|
|
|
|
for namespace in module.namespaces:
|
|
lines.append(f" <{item_name} Include=\"{namespace}\" />")
|
|
|
|
lines.extend(
|
|
[
|
|
f" <{item_name} Remove=\"@(GFrameworkExcludedUsing)\" />",
|
|
f" <Using Include=\"@({item_name})\" />",
|
|
" </ItemGroup>",
|
|
"</Project>",
|
|
"",
|
|
],
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def render_pack_block(package_id: str) -> str:
|
|
props_path = f"buildTransitive\\{package_id}.props"
|
|
return "\n".join(
|
|
[
|
|
f" {AUTO_GENERATED_START}",
|
|
" <ItemGroup>",
|
|
f" <None Include=\"{props_path}\" Pack=\"true\" PackagePath=\"buildTransitive\" Visible=\"false\"/>",
|
|
" </ItemGroup>",
|
|
f" {AUTO_GENERATED_END}",
|
|
"",
|
|
],
|
|
)
|
|
|
|
|
|
def update_csproj(project_path: Path, package_id: str) -> tuple[str, str]:
|
|
project_text, encoding = load_text(project_path)
|
|
block = render_pack_block(package_id)
|
|
pattern = re.compile(
|
|
rf"\s*{re.escape(AUTO_GENERATED_START)}.*?{re.escape(AUTO_GENERATED_END)}\s*",
|
|
re.DOTALL,
|
|
)
|
|
|
|
if pattern.search(project_text):
|
|
updated = pattern.sub(lambda _: f"\n{block}", project_text)
|
|
else:
|
|
updated = project_text.replace("</Project>", f"{block}</Project>")
|
|
|
|
return updated, encoding
|
|
|
|
|
|
def validate_module_coverage(modules: list[ModuleConfig]) -> None:
|
|
configured_projects = {module.project_path.resolve() for module in modules}
|
|
discovered_projects = {project.resolve() for project in discover_runtime_modules()}
|
|
|
|
missing = sorted(project.relative_to(ROOT_DIR) for project in discovered_projects - configured_projects)
|
|
extra = sorted(project.relative_to(ROOT_DIR) for project in configured_projects - discovered_projects)
|
|
|
|
if missing or extra:
|
|
messages: list[str] = []
|
|
if missing:
|
|
messages.append("Unconfigured runtime modules:\n - " + "\n - ".join(str(path) for path in missing))
|
|
if extra:
|
|
messages.append("Configured modules that are not eligible runtime packages:\n - " +
|
|
"\n - ".join(str(path) for path in extra))
|
|
raise SystemExit("\n".join(messages))
|
|
|
|
|
|
def check_or_write(path: Path, expected: str, encoding: str, check_only: bool, changed: list[Path]) -> None:
|
|
if path.exists():
|
|
current, _ = load_text(path)
|
|
if current == expected:
|
|
return
|
|
elif check_only:
|
|
changed.append(path)
|
|
return
|
|
|
|
if check_only:
|
|
changed.append(path)
|
|
return
|
|
|
|
write_text(path, expected, encoding)
|
|
changed.append(path)
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
modules = load_modules()
|
|
validate_module_coverage(modules)
|
|
changed: list[Path] = []
|
|
|
|
for module in modules:
|
|
package_id = resolve_package_id(module.project_path)
|
|
props_path = module.project_path.parent / "buildTransitive" / f"{package_id}.props"
|
|
props_content = render_props(module)
|
|
check_or_write(props_path, props_content, "utf-8", args.check, changed)
|
|
|
|
updated_project, encoding = update_csproj(module.project_path, package_id)
|
|
check_or_write(module.project_path, updated_project, encoding, args.check, changed)
|
|
|
|
if args.check:
|
|
if changed:
|
|
relative_paths = "\n".join(f" - {path.relative_to(ROOT_DIR)}" for path in changed)
|
|
raise SystemExit(f"Generated module global usings are out of date:\n{relative_paths}")
|
|
|
|
print("Module global usings are up to date.")
|
|
return
|
|
|
|
if changed:
|
|
for path in changed:
|
|
print(f"Updated {path.relative_to(ROOT_DIR)}")
|
|
return
|
|
|
|
print("No module global usings changes were needed.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|