mirror of
https://github.com/AutismDevelopment/Autism-Client.git
synced 2026-10-28 19:39:48 +00:00
760 lines
28 KiB
Python
760 lines
28 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""AUTISM addon toolkit: scaffold, scan, validate and build addons. Runs anywhere with Python 3.
|
||
|
|
|
||
|
|
python addon-toolkit.py # menu
|
||
|
|
python addon-toolkit.py setup --template minimal --name "My Addon" --output ../MyAddon
|
||
|
|
python addon-toolkit.py scan --project ../MyAddon
|
||
|
|
python addon-toolkit.py build --project ../MyAddon
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import re
|
||
|
|
import shutil
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||
|
|
REPO_ROOT = SCRIPT_DIR.parent
|
||
|
|
TEMPLATES = ("minimal", "advanced")
|
||
|
|
TEXT_EXTS = {".java", ".json", ".kts", ".toml", ".properties", ".md", ".yml", ".yaml"}
|
||
|
|
|
||
|
|
|
||
|
|
class ToolkitError(Exception):
|
||
|
|
"""Expected failure, shown without a traceback."""
|
||
|
|
|
||
|
|
|
||
|
|
def info(msg: str) -> None: print(f"[AUTISM] {msg}")
|
||
|
|
def ok(msg: str) -> None: print(f"[OK] {msg}")
|
||
|
|
def warn(msg: str) -> None: print(f"[WARN] {msg}")
|
||
|
|
def bad(msg: str) -> None: print(f"[FAIL] {msg}")
|
||
|
|
|
||
|
|
|
||
|
|
def path_line(label: str, path) -> None:
|
||
|
|
print(f"{label}:")
|
||
|
|
print(str(path))
|
||
|
|
|
||
|
|
|
||
|
|
def failure_item(msg: str) -> None:
|
||
|
|
if re.match(r"^[A-Za-z]:[\\/]", msg) or msg.startswith("/") or re.match(r"^\.{1,2}[\\/]", msg):
|
||
|
|
path_line("Path", msg)
|
||
|
|
else:
|
||
|
|
bad(f" - {msg}")
|
||
|
|
|
||
|
|
|
||
|
|
def is_interactive(args) -> bool:
|
||
|
|
return not getattr(args, "non_interactive", False) and sys.stdin.isatty()
|
||
|
|
|
||
|
|
|
||
|
|
def read_text(path: Path) -> str:
|
||
|
|
return path.read_text(encoding="utf-8")
|
||
|
|
|
||
|
|
|
||
|
|
def write_text(path: Path, text: str) -> None:
|
||
|
|
path.write_text(text, encoding="utf-8", newline="")
|
||
|
|
|
||
|
|
|
||
|
|
def to_folder_name(name: str) -> str:
|
||
|
|
if not name or not name.strip():
|
||
|
|
return "MyAddon"
|
||
|
|
parts = re.findall(r"[A-Za-z0-9]+", name)
|
||
|
|
if not parts:
|
||
|
|
return "MyAddon"
|
||
|
|
folder = "".join(p.upper() if len(p) == 1 else p[0].upper() + p[1:] for p in parts)
|
||
|
|
return folder[:48]
|
||
|
|
|
||
|
|
|
||
|
|
def to_package_segment(value: str) -> str:
|
||
|
|
segment = re.sub(r"[^a-z0-9]+", "", value.lower())
|
||
|
|
if not segment:
|
||
|
|
segment = "myaddon"
|
||
|
|
if not segment[0].isalpha():
|
||
|
|
segment = "addon" + segment
|
||
|
|
return segment
|
||
|
|
|
||
|
|
|
||
|
|
def to_mod_id(name: str) -> str:
|
||
|
|
mod_id = re.sub(r"[^a-z0-9_-]+", "-", name.lower()).strip("-_")
|
||
|
|
if not mod_id:
|
||
|
|
return "my-autism-addon"
|
||
|
|
if not mod_id[0].isalpha():
|
||
|
|
mod_id = "addon-" + mod_id
|
||
|
|
if len(mod_id) > 64:
|
||
|
|
mod_id = mod_id[:64].strip("-_")
|
||
|
|
return mod_id
|
||
|
|
|
||
|
|
|
||
|
|
def default_author() -> str:
|
||
|
|
return os.environ.get("USERNAME") or os.environ.get("USER") or "You"
|
||
|
|
|
||
|
|
|
||
|
|
def test_mod_id(value: str) -> bool:
|
||
|
|
return bool(re.match(r"^[a-z][a-z0-9_-]{1,63}$", value or ""))
|
||
|
|
|
||
|
|
|
||
|
|
def test_java_package(value: str) -> bool:
|
||
|
|
return bool(re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)+$", value or ""))
|
||
|
|
|
||
|
|
|
||
|
|
def test_version(value: str) -> bool:
|
||
|
|
return bool(re.match(r"^[0-9A-Za-z][0-9A-Za-z._+-]*$", value or ""))
|
||
|
|
|
||
|
|
|
||
|
|
def step_version(version: str) -> str:
|
||
|
|
if not version or not version.strip():
|
||
|
|
return "1.0.0"
|
||
|
|
m = re.match(r"^(.*?)(\d+)(\D*)$", version)
|
||
|
|
if not m:
|
||
|
|
return version + ".1"
|
||
|
|
prefix, digits, suffix = m.group(1), m.group(2), m.group(3)
|
||
|
|
nxt = str(int(digits) + 1)
|
||
|
|
if len(digits) > 1 and len(nxt) < len(digits):
|
||
|
|
nxt = nxt.rjust(len(digits), "0")
|
||
|
|
return f"{prefix}{nxt}{suffix}"
|
||
|
|
|
||
|
|
|
||
|
|
def get_toml_version(path: Path, key: str) -> str:
|
||
|
|
m = re.search(r'(?m)^\s*' + re.escape(key) + r'\s*=\s*"([^"]+)"', read_text(path))
|
||
|
|
return m.group(1) if m else ""
|
||
|
|
|
||
|
|
|
||
|
|
def set_toml_version(path: Path, key: str, value: str) -> None:
|
||
|
|
text = read_text(path)
|
||
|
|
pattern = r'(?m)^(\s*' + re.escape(key) + r'\s*=\s*)"[^"]*"'
|
||
|
|
if re.search(pattern, text):
|
||
|
|
text = re.sub(pattern, lambda m: m.group(1) + f'"{value}"', text)
|
||
|
|
else:
|
||
|
|
text += f'\n{key} = "{value}"\n'
|
||
|
|
write_text(path, text)
|
||
|
|
|
||
|
|
|
||
|
|
def set_properties_value(path: Path, key: str, value: str) -> None:
|
||
|
|
text = read_text(path)
|
||
|
|
pattern = r"(?m)^" + re.escape(key) + r"=.*$"
|
||
|
|
if re.search(pattern, text):
|
||
|
|
text = re.sub(pattern, f"{key}={value}", text)
|
||
|
|
else:
|
||
|
|
text += f"\n{key}={value}\n"
|
||
|
|
write_text(path, text)
|
||
|
|
|
||
|
|
|
||
|
|
def set_root_project_name(path: Path, name: str) -> None:
|
||
|
|
text = read_text(path)
|
||
|
|
pattern = r'(?m)^(rootProject\.name\s*=\s*)"[^"]*"'
|
||
|
|
if re.search(pattern, text):
|
||
|
|
text = re.sub(pattern, lambda m: m.group(1) + f'"{name}"', text)
|
||
|
|
else:
|
||
|
|
text += f'\nrootProject.name = "{name}"\n'
|
||
|
|
write_text(path, text)
|
||
|
|
|
||
|
|
|
||
|
|
def get_json(path: Path):
|
||
|
|
return json.loads(read_text(path))
|
||
|
|
|
||
|
|
|
||
|
|
def save_json(path: Path, data) -> None:
|
||
|
|
write_text(path, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
||
|
|
|
||
|
|
|
||
|
|
def is_under_build_or_gradle(path: Path, project: Path) -> bool:
|
||
|
|
try:
|
||
|
|
parts = path.relative_to(project).parts
|
||
|
|
except ValueError:
|
||
|
|
return False
|
||
|
|
return "build" in parts or ".gradle" in parts
|
||
|
|
|
||
|
|
|
||
|
|
def resolve_full_path(path: str) -> Path | None:
|
||
|
|
if not path or not path.strip():
|
||
|
|
return None
|
||
|
|
p = Path(path)
|
||
|
|
if p.is_absolute():
|
||
|
|
return p.resolve()
|
||
|
|
return (REPO_ROOT / p).resolve()
|
||
|
|
|
||
|
|
|
||
|
|
def is_shipped_template(path: Path) -> bool:
|
||
|
|
full = path.resolve()
|
||
|
|
return any(full == (REPO_ROOT / "addon-templates" / name).resolve() for name in TEMPLATES)
|
||
|
|
|
||
|
|
|
||
|
|
def get_first_java_package(project: Path) -> str:
|
||
|
|
entry = project / "src/main/resources/fabric.mod.json"
|
||
|
|
if entry.is_file():
|
||
|
|
data = get_json(entry)
|
||
|
|
classes = []
|
||
|
|
eps = data.get("entrypoints", {}) if isinstance(data, dict) else {}
|
||
|
|
for kind in ("client", "autism"):
|
||
|
|
if eps.get(kind):
|
||
|
|
classes += list(eps[kind]) if isinstance(eps[kind], list) else [eps[kind]]
|
||
|
|
for cls in classes:
|
||
|
|
m = re.match(r"^(.+)\.[^.]+$", str(cls))
|
||
|
|
if m:
|
||
|
|
return m.group(1)
|
||
|
|
java_root = project / "src/main/java"
|
||
|
|
if java_root.is_dir():
|
||
|
|
for java in sorted(java_root.rglob("*.java")):
|
||
|
|
m = re.search(r"(?m)^\s*package\s+([^;]+);", read_text(java))
|
||
|
|
if m:
|
||
|
|
return m.group(1).strip()
|
||
|
|
return "com.example.addon"
|
||
|
|
|
||
|
|
|
||
|
|
def class_simple_name(class_name: str, fallback: str) -> str:
|
||
|
|
m = re.search(r"\.([^.]+)$", class_name or "")
|
||
|
|
return m.group(1) if m else fallback
|
||
|
|
|
||
|
|
|
||
|
|
def replace_in_project_files(project: Path, old: str, new: str) -> None:
|
||
|
|
if not old or old == new:
|
||
|
|
return
|
||
|
|
for file in project.rglob("*"):
|
||
|
|
if not file.is_file() or file.suffix not in TEXT_EXTS:
|
||
|
|
continue
|
||
|
|
if is_under_build_or_gradle(file, project):
|
||
|
|
continue
|
||
|
|
text = read_text(file)
|
||
|
|
if old in text:
|
||
|
|
write_text(file, text.replace(old, new))
|
||
|
|
|
||
|
|
|
||
|
|
def move_java_package(project: Path, old_pkg: str, new_pkg: str) -> None:
|
||
|
|
if old_pkg == new_pkg:
|
||
|
|
return
|
||
|
|
src_root = project / "src/main/java"
|
||
|
|
old_path = src_root / old_pkg.replace(".", os.sep)
|
||
|
|
new_path = src_root / new_pkg.replace(".", os.sep)
|
||
|
|
if not old_path.is_dir():
|
||
|
|
return
|
||
|
|
if new_path.exists():
|
||
|
|
warn("Package path already exists. Not moving files.")
|
||
|
|
path_line("Path", new_path)
|
||
|
|
return
|
||
|
|
new_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
shutil.move(str(old_path), str(new_path))
|
||
|
|
cursor = old_path.parent
|
||
|
|
while src_root in cursor.parents and not any(cursor.iterdir()):
|
||
|
|
parent = cursor.parent
|
||
|
|
cursor.rmdir()
|
||
|
|
cursor = parent
|
||
|
|
|
||
|
|
|
||
|
|
def copy_addon_template(template_name: str, target: Path) -> None:
|
||
|
|
source = REPO_ROOT / "addon-templates" / template_name
|
||
|
|
if not source.is_dir():
|
||
|
|
path_line("Missing template", source)
|
||
|
|
raise ToolkitError("Template not found.")
|
||
|
|
if target.exists():
|
||
|
|
warn("Output already exists. Editing it in place.")
|
||
|
|
path_line("Path", target)
|
||
|
|
return
|
||
|
|
target.mkdir(parents=True, exist_ok=True)
|
||
|
|
for item in source.rglob("*"):
|
||
|
|
if is_under_build_or_gradle(item, source):
|
||
|
|
continue
|
||
|
|
rel = item.relative_to(source)
|
||
|
|
dest = target / rel
|
||
|
|
if item.is_dir():
|
||
|
|
dest.mkdir(parents=True, exist_ok=True)
|
||
|
|
else:
|
||
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
shutil.copy2(item, dest)
|
||
|
|
|
||
|
|
|
||
|
|
def prompt(label: str, default: str, validator=None, interactive: bool = True) -> str:
|
||
|
|
if not interactive:
|
||
|
|
return default
|
||
|
|
while True:
|
||
|
|
suffix = f" [{default}]" if default else ""
|
||
|
|
try:
|
||
|
|
value = input(f"{label}{suffix}: ").strip()
|
||
|
|
except EOFError:
|
||
|
|
return default
|
||
|
|
if not value:
|
||
|
|
value = default
|
||
|
|
if validator is None or validator(value):
|
||
|
|
return value
|
||
|
|
warn("Bad value. Fix it.")
|
||
|
|
|
||
|
|
|
||
|
|
def prompt_version(current: str, interactive: bool) -> str:
|
||
|
|
base = current if current and current.strip() else "1.0.0"
|
||
|
|
nxt = step_version(base)
|
||
|
|
if not interactive:
|
||
|
|
return nxt
|
||
|
|
while True:
|
||
|
|
try:
|
||
|
|
value = input(f"Version current {base}, blank = {nxt}: ").strip()
|
||
|
|
except EOFError:
|
||
|
|
return nxt
|
||
|
|
if not value:
|
||
|
|
return nxt
|
||
|
|
if test_version(value):
|
||
|
|
return value
|
||
|
|
warn("Bad version. Fix it.")
|
||
|
|
|
||
|
|
|
||
|
|
def get_setup_project_path(args, name_for_output: str) -> Path:
|
||
|
|
if args.project and args.project.strip():
|
||
|
|
project = resolve_full_path(args.project)
|
||
|
|
if is_shipped_template(project):
|
||
|
|
raise ToolkitError("No. The shipped templates stay clean. Use --output to copy one, or pick a real addon folder.")
|
||
|
|
return project
|
||
|
|
output = args.output
|
||
|
|
if not output or not output.strip():
|
||
|
|
default_out = str((REPO_ROOT / ".." / to_folder_name(name_for_output)).resolve())
|
||
|
|
output = prompt("Output folder", default_out, lambda v: bool(v and v.strip()), is_interactive(args))
|
||
|
|
target = resolve_full_path(output)
|
||
|
|
if is_shipped_template(target):
|
||
|
|
raise ToolkitError("No. The shipped templates stay clean. Pick a different output folder.")
|
||
|
|
copy_addon_template(args.template, target)
|
||
|
|
return target
|
||
|
|
|
||
|
|
|
||
|
|
def configure_addon(args) -> Path:
|
||
|
|
interactive = is_interactive(args)
|
||
|
|
addon_name = args.addon_name
|
||
|
|
if not addon_name or not addon_name.strip():
|
||
|
|
default_name = "My Advanced Addon" if args.template == "advanced" else "My Addon"
|
||
|
|
addon_name = prompt("Addon name", default_name, lambda v: bool(v and v.strip()), interactive)
|
||
|
|
|
||
|
|
project = get_setup_project_path(args, addon_name)
|
||
|
|
if not project.is_dir():
|
||
|
|
path_line("Missing addon folder", project)
|
||
|
|
raise ToolkitError("Addon project not found.")
|
||
|
|
|
||
|
|
fabric_path = project / "src/main/resources/fabric.mod.json"
|
||
|
|
versions_path = project / "gradle/libs.versions.toml"
|
||
|
|
properties_path = project / "gradle.properties"
|
||
|
|
settings_path = project / "settings.gradle.kts"
|
||
|
|
for required in (fabric_path, versions_path, properties_path, settings_path):
|
||
|
|
if not required.is_file():
|
||
|
|
path_line("Missing file", required)
|
||
|
|
raise ToolkitError("Missing required addon file.")
|
||
|
|
|
||
|
|
fabric = get_json(fabric_path)
|
||
|
|
old_id = str(fabric.get("id", ""))
|
||
|
|
old_package = get_first_java_package(project)
|
||
|
|
old_version = get_toml_version(versions_path, "mod-version")
|
||
|
|
eps = fabric.get("entrypoints", {}) or {}
|
||
|
|
old_client = str((eps.get("client") or [f"{old_package}.Init"])[0])
|
||
|
|
old_addon = str((eps.get("autism") or [f"{old_package}.Addon"])[0])
|
||
|
|
original_mixin = str((fabric.get("mixins") or [""])[0])
|
||
|
|
client_simple = class_simple_name(old_client, "Init")
|
||
|
|
addon_simple = class_simple_name(old_addon, "Addon")
|
||
|
|
|
||
|
|
advanced = args.advanced
|
||
|
|
|
||
|
|
addon_id = args.addon_id
|
||
|
|
if not addon_id or not addon_id.strip():
|
||
|
|
suggested = to_mod_id(addon_name) if "template" in old_id else old_id
|
||
|
|
addon_id = prompt("Addon id", suggested, test_mod_id, interactive) if advanced else suggested
|
||
|
|
|
||
|
|
addon_version = args.addon_version
|
||
|
|
if not addon_version or not addon_version.strip():
|
||
|
|
addon_version = prompt_version(old_version or "1.0.0", interactive)
|
||
|
|
|
||
|
|
package = args.package
|
||
|
|
if not package or not package.strip():
|
||
|
|
suggested_pkg = f"com.{to_package_segment(addon_id)}.addon"
|
||
|
|
package = prompt("Java package", suggested_pkg, test_java_package, interactive) if advanced else suggested_pkg
|
||
|
|
|
||
|
|
maven_group = args.maven_group
|
||
|
|
if not maven_group or not maven_group.strip():
|
||
|
|
maven_group = prompt("Maven group", package, test_java_package, interactive) if advanced else package
|
||
|
|
|
||
|
|
archive_name = args.archive_name
|
||
|
|
if not archive_name or not archive_name.strip():
|
||
|
|
archive_name = prompt("Jar/archive base name", addon_id, test_mod_id, interactive) if advanced else addon_id
|
||
|
|
|
||
|
|
author = args.author
|
||
|
|
if not author or not author.strip():
|
||
|
|
author = prompt("Author", default_author(), lambda v: bool(v and v.strip()), interactive)
|
||
|
|
|
||
|
|
description = args.description
|
||
|
|
if not description or not description.strip():
|
||
|
|
suggested_desc = f"{addon_name} addon for AUTISM Client."
|
||
|
|
description = prompt("Description", suggested_desc, lambda v: bool(v and v.strip()), interactive) if advanced else suggested_desc
|
||
|
|
|
||
|
|
replace_in_project_files(project, old_package, package)
|
||
|
|
replace_in_project_files(project, old_id, addon_id)
|
||
|
|
move_java_package(project, old_package, package)
|
||
|
|
|
||
|
|
fabric = get_json(fabric_path)
|
||
|
|
fabric["id"] = addon_id
|
||
|
|
fabric["name"] = addon_name
|
||
|
|
fabric["version"] = "${version}"
|
||
|
|
fabric["description"] = description
|
||
|
|
fabric["authors"] = [author]
|
||
|
|
fabric.setdefault("entrypoints", {})
|
||
|
|
fabric["entrypoints"]["client"] = [f"{package}.{client_simple}"]
|
||
|
|
fabric["entrypoints"]["autism"] = [f"{package}.{addon_simple}"]
|
||
|
|
|
||
|
|
if fabric.get("mixins"):
|
||
|
|
old_mixin_name = original_mixin or str(fabric["mixins"][0])
|
||
|
|
new_mixin_name = f"{addon_id}.mixins.json"
|
||
|
|
resource_dir = project / "src/main/resources"
|
||
|
|
old_mixin = resource_dir / old_mixin_name
|
||
|
|
new_mixin = resource_dir / new_mixin_name
|
||
|
|
if old_mixin.is_file() and old_mixin != new_mixin:
|
||
|
|
shutil.move(str(old_mixin), str(new_mixin))
|
||
|
|
if new_mixin.is_file():
|
||
|
|
mixin_json = get_json(new_mixin)
|
||
|
|
mixin_json["package"] = f"{package}.mixin"
|
||
|
|
save_json(new_mixin, mixin_json)
|
||
|
|
fabric["mixins"] = [new_mixin_name]
|
||
|
|
|
||
|
|
save_json(fabric_path, fabric)
|
||
|
|
set_toml_version(versions_path, "mod-version", addon_version)
|
||
|
|
set_properties_value(properties_path, "maven_group", maven_group)
|
||
|
|
set_properties_value(properties_path, "archives_base_name", archive_name)
|
||
|
|
set_root_project_name(settings_path, addon_id)
|
||
|
|
|
||
|
|
ok("Addon rewritten.")
|
||
|
|
path_line("Addon folder", project)
|
||
|
|
print(f" id: {addon_id}")
|
||
|
|
print(f" name: {addon_name}")
|
||
|
|
print(f" version: {addon_version}")
|
||
|
|
print(f" package: {package}")
|
||
|
|
print()
|
||
|
|
print("Next:")
|
||
|
|
print(f" python addon-templates/addon-toolkit.py build --project \"{project}\"")
|
||
|
|
path_line("Jar folder", project / "build" / "libs")
|
||
|
|
return project
|
||
|
|
|
||
|
|
|
||
|
|
def test_addon_project(project: Path, allow_template_names: bool = False):
|
||
|
|
failures: list[str] = []
|
||
|
|
warnings: list[str] = []
|
||
|
|
|
||
|
|
if not project.is_dir():
|
||
|
|
failures.append("Project folder does not exist.")
|
||
|
|
failures.append(str(project))
|
||
|
|
return failures, warnings
|
||
|
|
|
||
|
|
required = [
|
||
|
|
"build.gradle.kts", "settings.gradle.kts", "gradle.properties",
|
||
|
|
"gradle/libs.versions.toml", "src/main/resources/fabric.mod.json",
|
||
|
|
]
|
||
|
|
for item in required:
|
||
|
|
if not (project / item).is_file():
|
||
|
|
failures.append(f"Missing {item}")
|
||
|
|
if not (project / "gradlew.bat").is_file() and not (project / "gradlew").is_file():
|
||
|
|
failures.append("Missing gradlew / gradlew.bat")
|
||
|
|
|
||
|
|
fabric_path = project / "src/main/resources/fabric.mod.json"
|
||
|
|
if fabric_path.is_file():
|
||
|
|
try:
|
||
|
|
fabric = get_json(fabric_path)
|
||
|
|
if not test_mod_id(str(fabric.get("id", ""))):
|
||
|
|
failures.append(f"fabric.mod.json id is invalid: {fabric.get('id')}")
|
||
|
|
if not str(fabric.get("name", "")).strip():
|
||
|
|
failures.append("fabric.mod.json name is empty.")
|
||
|
|
eps = fabric.get("entrypoints", {}) or {}
|
||
|
|
if not eps.get("autism"):
|
||
|
|
failures.append("Missing autism entrypoint in fabric.mod.json.")
|
||
|
|
else:
|
||
|
|
for entry in eps["autism"]:
|
||
|
|
cls = project / ("src/main/java/" + str(entry).replace(".", "/") + ".java")
|
||
|
|
if not cls.is_file():
|
||
|
|
failures.append(f"Autism entrypoint class file is missing: {entry}")
|
||
|
|
if not (fabric.get("depends", {}) or {}).get("autism"):
|
||
|
|
failures.append("fabric.mod.json must depend on autism.")
|
||
|
|
if not allow_template_names:
|
||
|
|
if re.search(r"template|example", str(fabric.get("id", ""))):
|
||
|
|
warnings.append("Addon id still looks like a template/example.")
|
||
|
|
if re.search(r"Template|Example", str(fabric.get("name", ""))):
|
||
|
|
warnings.append("Addon name still looks like a template/example.")
|
||
|
|
if "You" in (fabric.get("authors") or []):
|
||
|
|
warnings.append("Author is still 'You'.")
|
||
|
|
except json.JSONDecodeError as exc:
|
||
|
|
failures.append(f"fabric.mod.json is not valid JSON: {exc}")
|
||
|
|
|
||
|
|
versions_path = project / "gradle/libs.versions.toml"
|
||
|
|
if versions_path.is_file():
|
||
|
|
if not test_version(get_toml_version(versions_path, "mod-version")):
|
||
|
|
failures.append("Addon mod-version is missing or invalid.")
|
||
|
|
if not get_toml_version(versions_path, "autism").strip():
|
||
|
|
failures.append("AUTISM dependency version is missing from gradle/libs.versions.toml.")
|
||
|
|
|
||
|
|
if (project / ".gradle").exists():
|
||
|
|
warnings.append(".gradle cache exists locally. Ignored; do not upload it.")
|
||
|
|
if (project / "build").exists():
|
||
|
|
warnings.append("build output exists locally. Ignored; do not upload it.")
|
||
|
|
if (project / ".github").exists():
|
||
|
|
warnings.append(".github exists inside this addon project. Fine for a copied template, but it is not root repo CI unless you move it.")
|
||
|
|
|
||
|
|
leftovers = (
|
||
|
|
"autism-minimal-addon-template", "autism-advanced-addon-template",
|
||
|
|
"AUTISM Minimal Addon Template", "AUTISM Advanced Addon Template",
|
||
|
|
)
|
||
|
|
for file in project.rglob("*"):
|
||
|
|
if not file.is_file() or file.suffix not in TEXT_EXTS or is_under_build_or_gradle(file, project):
|
||
|
|
continue
|
||
|
|
text = read_text(file)
|
||
|
|
if re.search(r"[ÂÃâ]", text):
|
||
|
|
warnings.append(f"Mojibake-looking text in {file}")
|
||
|
|
if not allow_template_names:
|
||
|
|
for leftover in leftovers:
|
||
|
|
if leftover in text:
|
||
|
|
warnings.append(f"Template leftover '{leftover}' in {file}")
|
||
|
|
break
|
||
|
|
return failures, warnings
|
||
|
|
|
||
|
|
|
||
|
|
def get_addon_project_path(args) -> Path:
|
||
|
|
if args.project and args.project.strip():
|
||
|
|
return resolve_full_path(args.project)
|
||
|
|
if args.output and args.output.strip():
|
||
|
|
target = resolve_full_path(args.output)
|
||
|
|
copy_addon_template(args.template, target)
|
||
|
|
return target
|
||
|
|
return REPO_ROOT / "addon-templates" / args.template
|
||
|
|
|
||
|
|
|
||
|
|
def scan_addon(args) -> None:
|
||
|
|
project = get_addon_project_path(args)
|
||
|
|
failures, warnings = test_addon_project(project)
|
||
|
|
info("Scanned addon project.")
|
||
|
|
path_line("Addon folder", project)
|
||
|
|
for w in warnings:
|
||
|
|
warn(f"Heads up: {w}")
|
||
|
|
if failures:
|
||
|
|
bad("Scan found broken stuff:")
|
||
|
|
for f in failures:
|
||
|
|
failure_item(f)
|
||
|
|
raise ToolkitError("Scan failed.")
|
||
|
|
ok("Scan clean.")
|
||
|
|
|
||
|
|
|
||
|
|
def validate_addon_system(args) -> None:
|
||
|
|
failures: list[str] = []
|
||
|
|
warnings: list[str] = []
|
||
|
|
|
||
|
|
root_required = [
|
||
|
|
"src/main/java/autismclient/addons/AddonManager.java",
|
||
|
|
"src/main/java/autismclient/api/AutismAddon.java",
|
||
|
|
"src/main/java/autismclient/api/SimpleAddon.java",
|
||
|
|
"src/main/java/autismclient/api/AutismAddons.java",
|
||
|
|
"src/main/java/autismclient/api/ApiVersion.java",
|
||
|
|
"src/main/java/autismclient/api/module/SimpleModule.java",
|
||
|
|
"src/main/java/autismclient/api/macro/SimpleAction.java",
|
||
|
|
"src/main/java/autismclient/api/macro/SimpleCondition.java",
|
||
|
|
"src/main/java/autismclient/api/macro/MacroActionRegistry.java",
|
||
|
|
"src/main/java/autismclient/api/macro/MacroPresetRegistry.java",
|
||
|
|
"src/main/java/autismclient/api/hud/HudElements.java",
|
||
|
|
"src/main/java/autismclient/api/event/AddonEvents.java",
|
||
|
|
"src/main/java/autismclient/gui/screen/AutismAddonsScreen.java",
|
||
|
|
"src/main/java/autismclient/util/macro/MissingAddonAction.java",
|
||
|
|
"addon-templates/README.md",
|
||
|
|
"addon-templates/addon-toolkit.py",
|
||
|
|
"addon-templates/addon-toolkit.ps1",
|
||
|
|
"addon-templates/addon-toolkit.sh",
|
||
|
|
]
|
||
|
|
for f in root_required:
|
||
|
|
if not (REPO_ROOT / f).is_file():
|
||
|
|
failures.append(f"Missing {f}")
|
||
|
|
|
||
|
|
gitignore = REPO_ROOT / ".gitignore"
|
||
|
|
if gitignore.is_file():
|
||
|
|
ignore_text = read_text(gitignore)
|
||
|
|
for pattern in ("!/src/", "!/addon-templates/", "!/addon-templates/**",
|
||
|
|
"/addon-templates/**/build/", "/addon-templates/**/.gradle/", "/mc-src*/"):
|
||
|
|
if pattern not in ignore_text:
|
||
|
|
failures.append(f".gitignore is missing {pattern}")
|
||
|
|
else:
|
||
|
|
failures.append("Missing .gitignore")
|
||
|
|
|
||
|
|
main_version = get_toml_version(REPO_ROOT / "gradle/libs.versions.toml", "mod-version")
|
||
|
|
for template_name in TEMPLATES:
|
||
|
|
project = REPO_ROOT / "addon-templates" / template_name
|
||
|
|
t_failures, t_warnings = test_addon_project(project, allow_template_names=True)
|
||
|
|
failures += [f"{template_name} template: {x}" for x in t_failures]
|
||
|
|
warnings += [f"{template_name} template: {x}" for x in t_warnings]
|
||
|
|
target_version = get_toml_version(project / "gradle/libs.versions.toml", "autism")
|
||
|
|
if main_version and target_version and main_version != target_version:
|
||
|
|
failures.append(f"{template_name} template targets AUTISM {target_version}, main client is {main_version}.")
|
||
|
|
|
||
|
|
for w in warnings:
|
||
|
|
warn(f"Heads up: {w}")
|
||
|
|
if failures:
|
||
|
|
bad("Addon system is broken:")
|
||
|
|
for f in failures:
|
||
|
|
failure_item(f)
|
||
|
|
raise ToolkitError("Addon system validation failed.")
|
||
|
|
ok("Addon system clean.")
|
||
|
|
|
||
|
|
|
||
|
|
def run_gradle(project: Path, gradle_args: list[str]) -> None:
|
||
|
|
wrapper = project / ("gradlew.bat" if os.name == "nt" else "gradlew")
|
||
|
|
if not wrapper.is_file():
|
||
|
|
path_line("Missing Gradle wrapper", wrapper)
|
||
|
|
raise ToolkitError("Missing Gradle wrapper.")
|
||
|
|
cmd = [str(wrapper)] + gradle_args
|
||
|
|
if os.name != "nt" and not os.access(wrapper, os.X_OK):
|
||
|
|
cmd = ["sh", str(wrapper)] + gradle_args
|
||
|
|
result = subprocess.run(cmd, cwd=str(project))
|
||
|
|
if result.returncode != 0:
|
||
|
|
path_line("Gradle project", project)
|
||
|
|
raise ToolkitError(f"Gradle failed with exit code {result.returncode}.")
|
||
|
|
|
||
|
|
|
||
|
|
def publish_api(args) -> None:
|
||
|
|
info("Publishing client API to Maven Local.")
|
||
|
|
run_gradle(REPO_ROOT, ["publishToMavenLocal", "--no-daemon"])
|
||
|
|
ok("Client API published.")
|
||
|
|
|
||
|
|
|
||
|
|
def build_addon(args) -> None:
|
||
|
|
project = get_addon_project_path(args)
|
||
|
|
if not project.is_dir():
|
||
|
|
path_line("Addon folder", project)
|
||
|
|
raise ToolkitError("Wrong addon folder.")
|
||
|
|
if not (project / "gradlew.bat").is_file() and not (project / "gradlew").is_file():
|
||
|
|
path_line("Addon folder", project)
|
||
|
|
raise ToolkitError("No Gradle wrapper there. Wrong addon folder.")
|
||
|
|
if not args.no_publish:
|
||
|
|
publish_api(args)
|
||
|
|
info("Building addon.")
|
||
|
|
path_line("Addon folder", project)
|
||
|
|
run_gradle(project, ["build", "--no-daemon"])
|
||
|
|
ok("Addon built.")
|
||
|
|
path_line("Jar folder", project / "build" / "libs")
|
||
|
|
|
||
|
|
|
||
|
|
def build_all_templates(args) -> None:
|
||
|
|
if not args.no_publish:
|
||
|
|
publish_api(args)
|
||
|
|
for template_name in TEMPLATES:
|
||
|
|
project = REPO_ROOT / "addon-templates" / template_name
|
||
|
|
info(f"Building {template_name} template.")
|
||
|
|
run_gradle(project, ["build", "--no-daemon"])
|
||
|
|
ok(f"{template_name} template built.")
|
||
|
|
|
||
|
|
|
||
|
|
def clean_addon(args) -> None:
|
||
|
|
project = get_addon_project_path(args)
|
||
|
|
interactive = is_interactive(args)
|
||
|
|
for folder in (".gradle", "build"):
|
||
|
|
path = project / folder
|
||
|
|
if path.exists():
|
||
|
|
confirmed = args.yes or (interactive and input(f"Remove {path} ? Type YES: ").strip() == "YES")
|
||
|
|
if confirmed:
|
||
|
|
shutil.rmtree(path)
|
||
|
|
ok("Deleted local build junk.")
|
||
|
|
path_line("Path", path)
|
||
|
|
|
||
|
|
|
||
|
|
def menu(args) -> None:
|
||
|
|
last_path = ""
|
||
|
|
while True:
|
||
|
|
print()
|
||
|
|
print("AUTISM ADDON KIT")
|
||
|
|
print("1. New addon")
|
||
|
|
print("2. Build addon")
|
||
|
|
print("3. Scan addon")
|
||
|
|
print("4. Test shipped templates")
|
||
|
|
print("5. Publish client API")
|
||
|
|
print("6. Delete local build junk")
|
||
|
|
print("0. Leave")
|
||
|
|
try:
|
||
|
|
choice = input("> ").strip()
|
||
|
|
except EOFError:
|
||
|
|
return
|
||
|
|
try:
|
||
|
|
if choice == "1":
|
||
|
|
args.template = prompt("Template minimal/advanced", args.template, lambda v: v in TEMPLATES, True)
|
||
|
|
args.project = ""
|
||
|
|
args.output = ""
|
||
|
|
last_path = str(configure_addon(args))
|
||
|
|
elif choice == "2":
|
||
|
|
default_path = last_path or str((REPO_ROOT / ".." / to_folder_name("")).resolve())
|
||
|
|
args.project = prompt("Addon folder", default_path, lambda v: bool(v and v.strip()), True)
|
||
|
|
build_addon(args)
|
||
|
|
elif choice == "3":
|
||
|
|
default_path = last_path or str((REPO_ROOT / ".." / to_folder_name("")).resolve())
|
||
|
|
args.project = prompt("Addon folder", default_path, lambda v: bool(v and v.strip()), True)
|
||
|
|
scan_addon(args)
|
||
|
|
elif choice == "4":
|
||
|
|
validate_addon_system(args)
|
||
|
|
build_all_templates(args)
|
||
|
|
elif choice == "5":
|
||
|
|
publish_api(args)
|
||
|
|
elif choice == "6":
|
||
|
|
default_path = last_path or str((REPO_ROOT / ".." / to_folder_name("")).resolve())
|
||
|
|
args.project = prompt("Addon folder", default_path, lambda v: bool(v and v.strip()), True)
|
||
|
|
clean_addon(args)
|
||
|
|
elif choice == "0":
|
||
|
|
return
|
||
|
|
else:
|
||
|
|
warn("Bad choice.")
|
||
|
|
except ToolkitError as exc:
|
||
|
|
bad(str(exc))
|
||
|
|
try:
|
||
|
|
input("Press Enter. The error stays here until you do")
|
||
|
|
except EOFError:
|
||
|
|
return
|
||
|
|
|
||
|
|
|
||
|
|
def normalize_action(value: str) -> str:
|
||
|
|
v = value.strip().lower()
|
||
|
|
return {"publishapi": "publish-api", "buildall": "build-all"}.get(v, v)
|
||
|
|
|
||
|
|
|
||
|
|
def build_parser() -> argparse.ArgumentParser:
|
||
|
|
parser = argparse.ArgumentParser(description="AUTISM addon toolkit.")
|
||
|
|
parser.add_argument("action", nargs="?", default="menu", type=normalize_action,
|
||
|
|
choices=["menu", "setup", "scan", "validate", "publish-api", "build", "build-all", "clean"])
|
||
|
|
parser.add_argument("--template", type=str.lower, choices=list(TEMPLATES), default="minimal")
|
||
|
|
parser.add_argument("--project", "--project-path", dest="project")
|
||
|
|
parser.add_argument("--output", "--output-path", dest="output")
|
||
|
|
parser.add_argument("--addon-id", dest="addon_id")
|
||
|
|
parser.add_argument("--name", "--addon-name", dest="addon_name")
|
||
|
|
parser.add_argument("--addon-version", dest="addon_version")
|
||
|
|
parser.add_argument("--package")
|
||
|
|
parser.add_argument("--author")
|
||
|
|
parser.add_argument("--description")
|
||
|
|
parser.add_argument("--maven-group", dest="maven_group")
|
||
|
|
parser.add_argument("--archive-name", dest="archive_name")
|
||
|
|
parser.add_argument("--advanced", action="store_true")
|
||
|
|
parser.add_argument("--no-publish", action="store_true", dest="no_publish")
|
||
|
|
parser.add_argument("--yes", action="store_true")
|
||
|
|
parser.add_argument("--non-interactive", action="store_true", dest="non_interactive")
|
||
|
|
parser.add_argument("--debug-stack", action="store_true", dest="debug_stack")
|
||
|
|
return parser
|
||
|
|
|
||
|
|
|
||
|
|
ACTIONS = {
|
||
|
|
"menu": menu,
|
||
|
|
"setup": configure_addon,
|
||
|
|
"scan": scan_addon,
|
||
|
|
"validate": validate_addon_system,
|
||
|
|
"publish-api": publish_api,
|
||
|
|
"build": build_addon,
|
||
|
|
"build-all": build_all_templates,
|
||
|
|
"clean": clean_addon,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv: list[str]) -> int:
|
||
|
|
args = build_parser().parse_args(argv)
|
||
|
|
try:
|
||
|
|
ACTIONS[args.action](args)
|
||
|
|
return 0
|
||
|
|
except ToolkitError as exc:
|
||
|
|
bad(str(exc))
|
||
|
|
if args.debug_stack:
|
||
|
|
raise
|
||
|
|
return 1
|
||
|
|
except KeyboardInterrupt:
|
||
|
|
print()
|
||
|
|
return 130
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main(sys.argv[1:]))
|