thrift-related/PythonWorkSpace/IntegratedTool/generate_go_package.py
2026-04-09 17:37:58 +08:00

303 lines
8.8 KiB
Python

"""Generate a Go package from thrift-files/meowment-network."""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Iterable, Sequence
REPO_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_SOURCE_DIR = REPO_ROOT / "thrift-files" / "meowment-network"
DEFAULT_OUTPUT_DIR = REPO_ROOT / "compiled_output" / "golang" / "meowmentnet"
DEFAULT_PACKAGE_NAME = "meowmentnet"
DEFAULT_THRIFT_IMPORT = "github.com/apache/thrift/lib/go/thrift"
class ToolError(RuntimeError):
pass
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate a Go package from meowment-network thrift files.",
)
parser.add_argument(
"--source-dir",
type=Path,
default=DEFAULT_SOURCE_DIR,
help="Directory containing .thrift files.",
)
parser.add_argument(
"--output-dir",
type=Path,
default=DEFAULT_OUTPUT_DIR,
help="Destination directory of the generated Go package.",
)
parser.add_argument(
"--package-name",
default=DEFAULT_PACKAGE_NAME,
help="Generated Go package name.",
)
parser.add_argument(
"--compiler",
type=Path,
help="Explicit thrift compiler executable path.",
)
parser.add_argument(
"--package-prefix",
default="",
help="Optional value passed to go:package_prefix=...",
)
parser.add_argument(
"--thrift-import",
default=DEFAULT_THRIFT_IMPORT,
help="Import path of the Go thrift runtime.",
)
parser.add_argument(
"--module",
default="",
help="If set, write a go.mod file with this module path.",
)
parser.add_argument(
"--no-clean",
action="store_true",
help="Do not delete the existing output directory before copying files.",
)
parser.add_argument(
"--skip-gofmt",
action="store_true",
help="Do not run gofmt after generation.",
)
parser.add_argument(
"--skip-go-mod-tidy",
action="store_true",
help="Do not run go mod tidy after writing go.mod.",
)
return parser.parse_args()
def candidate_compilers(explicit: Path | None) -> Iterable[Path]:
seen: set[str] = set()
def emit(path: Path | None) -> Iterable[Path]:
if path is None:
return ()
resolved = str(path)
if resolved in seen:
return ()
seen.add(resolved)
return (path,)
if explicit is not None:
yield from emit(explicit)
env_compiler = Path(os.environ["THRIFT_COMPILER"]) if "THRIFT_COMPILER" in os.environ else None
yield from emit(env_compiler)
yield from emit(REPO_ROOT / "compiler" / "exe" / "thrift-go.exe")
system_compiler = shutil.which("thrift")
if system_compiler:
yield from emit(Path(system_compiler))
def probe_compiler(executable: Path) -> bool:
try:
completed = subprocess.run(
[str(executable), "-version"],
check=False,
capture_output=True,
text=True,
timeout=10,
)
except OSError:
return False
except subprocess.TimeoutExpired:
return False
return completed.returncode == 0
def resolve_compiler(explicit: Path | None) -> Path:
for compiler in candidate_compilers(explicit):
if probe_compiler(compiler):
return compiler
raise ToolError(
"No working thrift compiler was found. Use --compiler or set THRIFT_COMPILER."
)
def find_thrift_files(source_dir: Path) -> list[Path]:
if not source_dir.exists():
raise ToolError(f"Source directory does not exist: {source_dir}")
thrift_files = sorted(source_dir.glob("*.thrift"))
if not thrift_files:
raise ToolError(f"No .thrift files were found in: {source_dir}")
return thrift_files
def build_generator_option(args: argparse.Namespace) -> str:
options = [f"package={args.package_name}"]
options.append("ignore_initialisms")
if args.package_prefix:
options.append(f"package_prefix={normalize_package_prefix(args.package_prefix)}")
if args.thrift_import:
options.append(f"thrift_import={args.thrift_import}")
options.append("skip_remote")
return "go:" + ",".join(options)
def normalize_package_prefix(prefix: str) -> str:
normalized = prefix.strip()
if not normalized:
return normalized
return normalized if normalized.endswith("/") else normalized + "/"
def run_command(command: Sequence[str], cwd: Path) -> None:
completed = subprocess.run(
list(command),
cwd=str(cwd),
check=False,
capture_output=True,
text=True,
)
if completed.returncode == 0:
return
stdout = completed.stdout.strip()
stderr = completed.stderr.strip()
details = "\n".join(part for part in (stdout, stderr) if part)
if not details:
details = "The command exited with a non-zero status and produced no output."
raise ToolError(details)
def generate_go_sources(
compiler: Path,
source_dir: Path,
thrift_files: Sequence[Path],
package_name: str,
generator_option: str,
staging_root: Path,
) -> Path:
for thrift_file in thrift_files:
command = [
str(compiler),
"-o",
str(staging_root),
"-strict",
"--gen",
generator_option,
str(thrift_file),
]
try:
run_command(command, cwd=source_dir)
except ToolError as exc:
raise ToolError(f"Failed to generate {thrift_file.name}:\n{exc}") from exc
package_dir = staging_root / "gen-go" / package_name
if not package_dir.exists():
raise ToolError(f"Generated package directory not found: {package_dir}")
return package_dir
def write_go_mod(output_dir: Path, module_name: str) -> None:
go_mod = output_dir / "go.mod"
go_mod.write_text(
"\n".join(
[
f"module {module_name}",
"",
"go 1.22",
"",
"require github.com/apache/thrift v0.22.0",
"",
]
),
encoding="utf-8",
)
def copy_output(staged_package_dir: Path, output_dir: Path, clean: bool) -> None:
if clean and output_dir.exists():
shutil.rmtree(output_dir)
output_dir.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(staged_package_dir, output_dir, dirs_exist_ok=not clean)
def run_gofmt(output_dir: Path) -> None:
gofmt = shutil.which("gofmt")
if not gofmt:
print("[warn] gofmt was not found in PATH. Skipping formatting.")
return
go_files = sorted(output_dir.glob("*.go"))
if not go_files:
return
for go_file in go_files:
run_command([gofmt, "-w", str(go_file)], cwd=output_dir)
def run_go_mod_tidy(output_dir: Path) -> None:
go = shutil.which("go")
if not go:
print("[warn] go was not found in PATH. Skipping go mod tidy.")
return
run_command([go, "mod", "tidy"], cwd=output_dir)
def validate_args(args: argparse.Namespace) -> None:
if not args.package_name.strip():
raise ToolError("--package-name cannot be empty.")
def main() -> int:
args = parse_args()
try:
validate_args(args)
source_dir = args.source_dir.resolve()
output_dir = args.output_dir.resolve()
thrift_files = find_thrift_files(source_dir)
compiler = resolve_compiler(args.compiler.resolve() if args.compiler else None)
generator_option = build_generator_option(args)
with tempfile.TemporaryDirectory(prefix="thrift_go_") as tmp_dir:
staged_package_dir = generate_go_sources(
compiler=compiler,
source_dir=source_dir,
thrift_files=thrift_files,
package_name=args.package_name,
generator_option=generator_option,
staging_root=Path(tmp_dir),
)
copy_output(staged_package_dir, output_dir, clean=not args.no_clean)
if args.module:
write_go_mod(output_dir, args.module)
if not args.skip_go_mod_tidy:
run_go_mod_tidy(output_dir)
if not args.skip_gofmt:
run_gofmt(output_dir)
print(f"Compiler: {compiler}")
print(f"Source directory: {source_dir}")
print(f"Output directory: {output_dir}")
print(f"Package name: {args.package_name}")
print(f"Thrift files: {len(thrift_files)}")
if args.module:
print(f"go.mod: {args.module}")
return 0
except ToolError as exc:
print(f"[error] {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())