com.elestrago.unity.package-tools 2.0.11 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CAHNGELOG.md +8 -0
- package/Documentation~/api.md +502 -0
- package/Documentation~/manual.md +140 -0
- package/Documentation~/samples.md +73 -0
- package/Editor/Drawers/CopyEntryPropertyDrawer.cs +95 -0
- package/Editor/Drawers/CopyEntryPropertyDrawer.cs.meta +2 -0
- package/Editor/EditorConstants.cs +6 -0
- package/Editor/Inspectors/PackageManifestConfigInspector.cs +31 -0
- package/Editor/PackageManifestConfig.cs +20 -0
- package/Editor/Tools/FileTools.cs +73 -0
- package/Samples~/ClaudeSkills/unity-package-docs/SKILL.md +309 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/README.md.template +42 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/api-chunk.md.template +41 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/api-index.md.template +26 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/api.md.template +43 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/manual.md.template +57 -0
- package/Samples~/ClaudeSkills/unity-package-docs/assets/samples.md.template +56 -0
- package/Samples~/ClaudeSkills/unity-package-docs/scripts/scan_package.py +504 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/SKILL.md +309 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/README.md.template +42 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/api-chunk.md.template +41 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/api-index.md.template +26 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/api.md.template +43 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/manual.md.template +57 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/assets/samples.md.template +56 -0
- package/Samples~/ClaudeSkills/unity-package-docs/unity-package-docs/scripts/scan_package.py +504 -0
- package/package.json +9 -4
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Scan a Unity package source tree (and optional sample dirs) and emit a JSON manifest.
|
|
3
|
+
|
|
4
|
+
The manifest is consumed by the unity-package-docs skill to render README.md,
|
|
5
|
+
manual.md, api.md and samples.md. Paths in the manifest are relative to --repo-root
|
|
6
|
+
(default: cwd) so the consumer can use them verbatim in markdown.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
scan_package.py --package <pkg-dir> [--samples <dir> ...] [--repo-root <dir>]
|
|
10
|
+
|
|
11
|
+
The script uses only the Python standard library. It is best-effort regex-based C#
|
|
12
|
+
parsing: good enough for well-formatted Unity editor code, not a full Roslyn parser.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Iterable
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Source normalization: replace string and comment contents with whitespace so
|
|
27
|
+
# brace counts and regex matches are not fooled by braces in strings/comments.
|
|
28
|
+
# Preserves line numbers (newlines are kept verbatim).
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
def _blank_preserve_newlines(s: str) -> str:
|
|
32
|
+
return re.sub(r"[^\n]", " ", s)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def normalize(src: str) -> str:
|
|
36
|
+
out: list[str] = []
|
|
37
|
+
i, n = 0, len(src)
|
|
38
|
+
while i < n:
|
|
39
|
+
c = src[i]
|
|
40
|
+
c1 = src[i + 1] if i + 1 < n else ""
|
|
41
|
+
if c == "/" and c1 == "/":
|
|
42
|
+
j = src.find("\n", i)
|
|
43
|
+
j = n if j < 0 else j
|
|
44
|
+
out.append(" " * (j - i))
|
|
45
|
+
i = j
|
|
46
|
+
elif c == "/" and c1 == "*":
|
|
47
|
+
j = src.find("*/", i + 2)
|
|
48
|
+
j = n if j < 0 else j + 2
|
|
49
|
+
out.append(_blank_preserve_newlines(src[i:j]))
|
|
50
|
+
i = j
|
|
51
|
+
elif c == "@" and c1 == '"':
|
|
52
|
+
j = i + 2
|
|
53
|
+
while j < n:
|
|
54
|
+
if src[j] == '"':
|
|
55
|
+
if j + 1 < n and src[j + 1] == '"':
|
|
56
|
+
j += 2
|
|
57
|
+
continue
|
|
58
|
+
j += 1
|
|
59
|
+
break
|
|
60
|
+
j += 1
|
|
61
|
+
inner = src[i + 1 : j - 1] if j > i + 1 else ""
|
|
62
|
+
out.append('"' + _blank_preserve_newlines(inner) + '"')
|
|
63
|
+
i = j
|
|
64
|
+
elif c == '"':
|
|
65
|
+
j = i + 1
|
|
66
|
+
while j < n:
|
|
67
|
+
if src[j] == "\\" and j + 1 < n:
|
|
68
|
+
j += 2
|
|
69
|
+
continue
|
|
70
|
+
if src[j] == '"':
|
|
71
|
+
j += 1
|
|
72
|
+
break
|
|
73
|
+
if src[j] == "\n":
|
|
74
|
+
break
|
|
75
|
+
j += 1
|
|
76
|
+
chunk = src[i:j]
|
|
77
|
+
if len(chunk) >= 2 and chunk.endswith('"'):
|
|
78
|
+
out.append('"' + " " * (len(chunk) - 2) + '"')
|
|
79
|
+
else:
|
|
80
|
+
out.append(" " * len(chunk))
|
|
81
|
+
i = j
|
|
82
|
+
elif c == "'":
|
|
83
|
+
j = i + 1
|
|
84
|
+
depth = 0
|
|
85
|
+
while j < n and depth < 6:
|
|
86
|
+
if src[j] == "\\" and j + 1 < n:
|
|
87
|
+
j += 2
|
|
88
|
+
depth += 1
|
|
89
|
+
continue
|
|
90
|
+
if src[j] == "'":
|
|
91
|
+
j += 1
|
|
92
|
+
break
|
|
93
|
+
j += 1
|
|
94
|
+
depth += 1
|
|
95
|
+
chunk = src[i:j]
|
|
96
|
+
if len(chunk) >= 2 and chunk.endswith("'"):
|
|
97
|
+
out.append("'" + " " * (len(chunk) - 2) + "'")
|
|
98
|
+
else:
|
|
99
|
+
out.append(chunk)
|
|
100
|
+
i = j
|
|
101
|
+
else:
|
|
102
|
+
out.append(c)
|
|
103
|
+
i += 1
|
|
104
|
+
return "".join(out)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Regex patterns applied to the normalized source line-by-line.
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
NAMESPACE_RE = re.compile(r"^\s*namespace\s+(?P<name>[\w.]+)\s*[{;]?\s*$")
|
|
112
|
+
|
|
113
|
+
TYPE_DECL_RE = re.compile(
|
|
114
|
+
r"^\s*(?P<mods>(?:(?:public|internal|private|protected|sealed|static|abstract|partial|new|unsafe)\s+)*)"
|
|
115
|
+
r"(?P<kind>class|struct|interface|enum)\s+"
|
|
116
|
+
r"(?P<name>[A-Za-z_]\w*)"
|
|
117
|
+
r"(?P<generic><[^>]*>)?"
|
|
118
|
+
r"\s*(?::\s*(?P<base>[^{]+?))?"
|
|
119
|
+
r"\s*(?:where\s+[^{]+)?"
|
|
120
|
+
r"\s*\{?\s*$"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
METHOD_RE = re.compile(
|
|
124
|
+
r"^\s*(?P<mods>(?:(?:public|internal|private|protected|static|virtual|override|sealed|abstract|async|extern|new|partial|unsafe)\s+)+)"
|
|
125
|
+
r"(?P<ret>(?:[\w\.\?\[\]]|<[^>]*>)+(?:\s*[\?\[\]])*)\s+"
|
|
126
|
+
r"(?P<name>[A-Za-z_]\w*)\s*"
|
|
127
|
+
r"(?P<generic><[^>]*>)?\s*"
|
|
128
|
+
r"\((?P<params>[^)]*)\)\s*"
|
|
129
|
+
r"(?:where\s+[^{;=]+)?"
|
|
130
|
+
r"\s*(?:[{;=]|=>)?\s*$"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
PROPERTY_RE = re.compile(
|
|
134
|
+
r"^\s*(?P<mods>(?:(?:public|internal|private|protected|static|virtual|override|sealed|abstract|new|readonly)\s+)+)"
|
|
135
|
+
r"(?P<type>(?:[\w\.\?\[\]]|<[^>]*>)+)\s+"
|
|
136
|
+
r"(?P<name>[A-Za-z_]\w*)\s*"
|
|
137
|
+
r"(?:\{[^}]*\}|=>\s*[^;]+;)"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
FIELD_RE = re.compile(
|
|
141
|
+
r"^\s*(?P<mods>(?:(?:public|internal|private|protected|static|readonly|const|new|volatile|unsafe)\s+)+)"
|
|
142
|
+
r"(?P<type>(?:[\w\.\?\[\]]|<[^>]*>)+)\s+"
|
|
143
|
+
r"(?P<name>[A-Za-z_]\w*)\s*"
|
|
144
|
+
r"(?:=\s*[^;]+)?;"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
ATTR_LINE_RE = re.compile(r"^\s*\[(?P<body>.+)\]\s*$")
|
|
148
|
+
SUMMARY_LINE_RE = re.compile(r"^\s*///\s?(?P<text>.*)$")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# C# file parser
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
def parse_cs_file(path: Path, rel_path: str) -> dict:
|
|
156
|
+
"""Return {'types': [...], 'namespaces': set([...])} for one .cs file."""
|
|
157
|
+
raw = path.read_text(encoding="utf-8", errors="replace")
|
|
158
|
+
norm = normalize(raw)
|
|
159
|
+
norm_lines = norm.split("\n")
|
|
160
|
+
raw_lines = raw.split("\n")
|
|
161
|
+
|
|
162
|
+
types: list[dict] = []
|
|
163
|
+
namespaces: set[str] = set()
|
|
164
|
+
|
|
165
|
+
scope_stack: list[dict] = [] # entries: {"kind": "namespace"|"type", "name": str, "open_depth": int, "type_idx": int|None}
|
|
166
|
+
brace_depth = 0
|
|
167
|
+
pending_attrs: list[str] = []
|
|
168
|
+
pending_summary: list[str] = []
|
|
169
|
+
|
|
170
|
+
def current_namespace() -> str:
|
|
171
|
+
for s in reversed(scope_stack):
|
|
172
|
+
if s["kind"] == "namespace":
|
|
173
|
+
return s["name"]
|
|
174
|
+
return ""
|
|
175
|
+
|
|
176
|
+
def current_type_idx() -> int | None:
|
|
177
|
+
for s in reversed(scope_stack):
|
|
178
|
+
if s["kind"] == "type":
|
|
179
|
+
return s["type_idx"]
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
def drain_attrs_and_summary():
|
|
183
|
+
attrs = list(pending_attrs)
|
|
184
|
+
summary = "\n".join(pending_summary).strip()
|
|
185
|
+
pending_attrs.clear()
|
|
186
|
+
pending_summary.clear()
|
|
187
|
+
return attrs, summary
|
|
188
|
+
|
|
189
|
+
for idx, line in enumerate(norm_lines):
|
|
190
|
+
raw_line = raw_lines[idx] if idx < len(raw_lines) else ""
|
|
191
|
+
stripped = line.strip()
|
|
192
|
+
|
|
193
|
+
# Capture XML summary from RAW line (the normalizer keeps the `///` prefix).
|
|
194
|
+
m = SUMMARY_LINE_RE.match(raw_line)
|
|
195
|
+
if m:
|
|
196
|
+
text = m.group("text")
|
|
197
|
+
# Drop the <summary>/</summary> envelope; keep inner text.
|
|
198
|
+
text = re.sub(r"</?summary>", "", text).strip()
|
|
199
|
+
if text:
|
|
200
|
+
pending_summary.append(text)
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
# Capture attribute lines (allow multiple attrs separated by commas).
|
|
204
|
+
# Read the body from the RAW line so string literal contents are preserved.
|
|
205
|
+
m = ATTR_LINE_RE.match(raw_line)
|
|
206
|
+
if m:
|
|
207
|
+
body = m.group("body").strip()
|
|
208
|
+
pending_attrs.append(body)
|
|
209
|
+
# Attribute lines don't open/close braces; safe to continue.
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
if not stripped:
|
|
213
|
+
# blank line breaks attribute/summary accumulation? keep summary, drop attrs only on declaration.
|
|
214
|
+
# Reset summary on blank lines to avoid stealing distant comments.
|
|
215
|
+
pending_summary.clear()
|
|
216
|
+
pending_attrs.clear()
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# Namespace declaration
|
|
220
|
+
m = NAMESPACE_RE.match(line)
|
|
221
|
+
if m:
|
|
222
|
+
name = m.group("name")
|
|
223
|
+
namespaces.add(name)
|
|
224
|
+
# If the line had a `{`, that opens a scope.
|
|
225
|
+
if "{" in line:
|
|
226
|
+
scope_stack.append({"kind": "namespace", "name": name, "open_depth": brace_depth + 1, "type_idx": None})
|
|
227
|
+
brace_depth += line.count("{") - line.count("}")
|
|
228
|
+
elif ";" in line:
|
|
229
|
+
# file-scoped namespace: applies until EOF; no brace opened.
|
|
230
|
+
scope_stack.append({"kind": "namespace", "name": name, "open_depth": 0, "type_idx": None})
|
|
231
|
+
else:
|
|
232
|
+
# brace likely on next line; record pending namespace.
|
|
233
|
+
scope_stack.append({"kind": "namespace", "name": name, "open_depth": brace_depth + 1, "type_idx": None})
|
|
234
|
+
# don't drain attrs/summary for namespace
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
# Type declaration (top-level OR nested directly inside another type body)
|
|
238
|
+
m = TYPE_DECL_RE.match(line)
|
|
239
|
+
nested_ok = current_type_idx() is None or _is_direct_type_body(scope_stack, brace_depth)
|
|
240
|
+
if m and nested_ok and not (
|
|
241
|
+
re.search(r"\b(new|return|throw)\b", line[: m.start("kind")])
|
|
242
|
+
):
|
|
243
|
+
attrs, summary = drain_attrs_and_summary()
|
|
244
|
+
mods = [w for w in m.group("mods").split() if w] if m.group("mods") else []
|
|
245
|
+
base = m.group("base").strip() if m.group("base") else None
|
|
246
|
+
base_list = [b.strip() for b in base.split(",")] if base else []
|
|
247
|
+
tinfo = {
|
|
248
|
+
"file": rel_path,
|
|
249
|
+
"line": idx + 1,
|
|
250
|
+
"namespace": current_namespace(),
|
|
251
|
+
"name": m.group("name"),
|
|
252
|
+
"kind": m.group("kind"),
|
|
253
|
+
"modifiers": mods,
|
|
254
|
+
"baseList": base_list,
|
|
255
|
+
"attributes": attrs,
|
|
256
|
+
"summary": summary,
|
|
257
|
+
"containingType": _innermost_type_name(scope_stack),
|
|
258
|
+
"members": [],
|
|
259
|
+
}
|
|
260
|
+
types.append(tinfo)
|
|
261
|
+
type_idx = len(types) - 1
|
|
262
|
+
# If brace opens on this line, push scope. Else expect next line.
|
|
263
|
+
has_open = "{" in line
|
|
264
|
+
if has_open:
|
|
265
|
+
scope_stack.append({"kind": "type", "name": tinfo["name"], "open_depth": brace_depth + 1, "type_idx": type_idx})
|
|
266
|
+
else:
|
|
267
|
+
scope_stack.append({"kind": "type", "name": tinfo["name"], "open_depth": brace_depth + 1, "type_idx": type_idx})
|
|
268
|
+
brace_depth += line.count("{") - line.count("}")
|
|
269
|
+
# Pop scopes whose open_depth now exceeds brace_depth (closed in same line)
|
|
270
|
+
while scope_stack and brace_depth < scope_stack[-1]["open_depth"] - 1:
|
|
271
|
+
scope_stack.pop()
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
# Member declarations only if we are directly inside a type scope (not nested in a method).
|
|
275
|
+
ct_idx = current_type_idx()
|
|
276
|
+
if ct_idx is not None and _is_direct_type_body(scope_stack, brace_depth):
|
|
277
|
+
member = _try_member(line, idx + 1)
|
|
278
|
+
if member is not None:
|
|
279
|
+
attrs, summary = drain_attrs_and_summary()
|
|
280
|
+
member["attributes"] = attrs
|
|
281
|
+
member["summary"] = summary
|
|
282
|
+
types[ct_idx]["members"].append(member)
|
|
283
|
+
|
|
284
|
+
# Track brace depth changes for this line.
|
|
285
|
+
opens = line.count("{")
|
|
286
|
+
closes = line.count("}")
|
|
287
|
+
brace_depth += opens - closes
|
|
288
|
+
|
|
289
|
+
# Pop scopes whose brace went below their open depth.
|
|
290
|
+
while scope_stack and brace_depth < scope_stack[-1]["open_depth"]:
|
|
291
|
+
scope_stack.pop()
|
|
292
|
+
# Clear attrs that didn't attach to anything (this line ended a statement).
|
|
293
|
+
if opens or closes or stripped.endswith(";"):
|
|
294
|
+
pending_attrs.clear()
|
|
295
|
+
pending_summary.clear()
|
|
296
|
+
|
|
297
|
+
return {"types": types, "namespaces": sorted(namespaces)}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _innermost_type_name(scope_stack: list[dict]) -> str | None:
|
|
301
|
+
for s in reversed(scope_stack):
|
|
302
|
+
if s["kind"] == "type":
|
|
303
|
+
return s["name"]
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _is_direct_type_body(scope_stack: list[dict], brace_depth: int) -> bool:
|
|
308
|
+
"""True iff brace_depth equals the open depth of the innermost type (i.e. we're directly in the type body, not nested deeper)."""
|
|
309
|
+
for s in reversed(scope_stack):
|
|
310
|
+
if s["kind"] == "type":
|
|
311
|
+
return brace_depth == s["open_depth"]
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _try_member(line: str, line_no: int) -> dict | None:
|
|
316
|
+
"""Detect a method/property/field declaration on this normalized line."""
|
|
317
|
+
m = METHOD_RE.match(line)
|
|
318
|
+
if m and m.group("ret").strip() not in ("class", "struct", "interface", "enum"):
|
|
319
|
+
return {
|
|
320
|
+
"line": line_no,
|
|
321
|
+
"kind": "method",
|
|
322
|
+
"name": m.group("name"),
|
|
323
|
+
"signature": _collapse_ws(line.split("{")[0].split(";")[0].split("=>")[0]).rstrip(),
|
|
324
|
+
}
|
|
325
|
+
m = PROPERTY_RE.match(line)
|
|
326
|
+
if m:
|
|
327
|
+
return {
|
|
328
|
+
"line": line_no,
|
|
329
|
+
"kind": "property",
|
|
330
|
+
"name": m.group("name"),
|
|
331
|
+
"signature": _collapse_ws(line.split("{")[0].split("=>")[0]).rstrip(),
|
|
332
|
+
}
|
|
333
|
+
m = FIELD_RE.match(line)
|
|
334
|
+
if m:
|
|
335
|
+
return {
|
|
336
|
+
"line": line_no,
|
|
337
|
+
"kind": "field",
|
|
338
|
+
"name": m.group("name"),
|
|
339
|
+
"signature": _collapse_ws(line.split("=")[0].split(";")[0]).rstrip(),
|
|
340
|
+
}
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _collapse_ws(s: str) -> str:
|
|
345
|
+
return re.sub(r"\s+", " ", s).strip()
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
# Asmdef + meta scanning
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
def scan_asmdefs(package_dir: Path, repo_root: Path) -> list[dict]:
|
|
353
|
+
results: list[dict] = []
|
|
354
|
+
for p in package_dir.rglob("*.asmdef"):
|
|
355
|
+
try:
|
|
356
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
357
|
+
except json.JSONDecodeError:
|
|
358
|
+
continue
|
|
359
|
+
results.append(
|
|
360
|
+
{
|
|
361
|
+
"path": _rel(p, repo_root),
|
|
362
|
+
"name": data.get("name"),
|
|
363
|
+
"rootNamespace": data.get("rootNamespace", ""),
|
|
364
|
+
"includePlatforms": data.get("includePlatforms", []),
|
|
365
|
+
"excludePlatforms": data.get("excludePlatforms", []),
|
|
366
|
+
"references": data.get("references", []),
|
|
367
|
+
"precompiledReferences": data.get("precompiledReferences", []),
|
|
368
|
+
"defineConstraints": data.get("defineConstraints", []),
|
|
369
|
+
"autoReferenced": data.get("autoReferenced", True),
|
|
370
|
+
}
|
|
371
|
+
)
|
|
372
|
+
return results
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
META_GUID_RE = re.compile(r"^guid:\s*([a-fA-F0-9]+)", re.MULTILINE)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def build_script_guid_map(repo_root: Path) -> dict[str, str]:
|
|
379
|
+
"""Walk every *.cs.meta under Assets/ and return {guid: relative_cs_path}."""
|
|
380
|
+
mapping: dict[str, str] = {}
|
|
381
|
+
assets = repo_root / "Assets"
|
|
382
|
+
if not assets.is_dir():
|
|
383
|
+
return mapping
|
|
384
|
+
for meta in assets.rglob("*.cs.meta"):
|
|
385
|
+
try:
|
|
386
|
+
text = meta.read_text(encoding="utf-8", errors="replace")
|
|
387
|
+
except OSError:
|
|
388
|
+
continue
|
|
389
|
+
m = META_GUID_RE.search(text)
|
|
390
|
+
if not m:
|
|
391
|
+
continue
|
|
392
|
+
cs_path = meta.with_suffix("") # drop the .meta
|
|
393
|
+
if cs_path.exists():
|
|
394
|
+
mapping[m.group(1)] = _rel(cs_path, repo_root)
|
|
395
|
+
return mapping
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
SCRIPT_REF_RE = re.compile(r"m_Script:\s*\{[^}]*guid:\s*([a-fA-F0-9]+)")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def scan_sample_dir(sample_dir: Path, repo_root: Path, package_types: set[str], guid_map: dict[str, str], package_files: set[str]) -> list[dict]:
|
|
402
|
+
results: list[dict] = []
|
|
403
|
+
if not sample_dir.is_dir():
|
|
404
|
+
return results
|
|
405
|
+
for p in sorted(sample_dir.rglob("*")):
|
|
406
|
+
if not p.is_file():
|
|
407
|
+
continue
|
|
408
|
+
rel = _rel(p, repo_root)
|
|
409
|
+
suf = p.suffix.lower()
|
|
410
|
+
if suf == ".cs":
|
|
411
|
+
try:
|
|
412
|
+
src = p.read_text(encoding="utf-8", errors="replace")
|
|
413
|
+
except OSError:
|
|
414
|
+
continue
|
|
415
|
+
norm = normalize(src)
|
|
416
|
+
ns_match = NAMESPACE_RE.search(norm)
|
|
417
|
+
ns = ns_match.group("name") if ns_match else ""
|
|
418
|
+
words = set(re.findall(r"\b([A-Z][A-Za-z0-9_]*)\b", norm))
|
|
419
|
+
refs = sorted(words & package_types)
|
|
420
|
+
results.append({"path": rel, "kind": "script", "namespace": ns, "packageTypesReferenced": refs})
|
|
421
|
+
elif suf in (".unity", ".prefab"):
|
|
422
|
+
try:
|
|
423
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
424
|
+
except OSError:
|
|
425
|
+
continue
|
|
426
|
+
guids = sorted(set(SCRIPT_REF_RE.findall(text)))
|
|
427
|
+
refs = []
|
|
428
|
+
for g in guids:
|
|
429
|
+
resolved = guid_map.get(g)
|
|
430
|
+
if resolved and resolved in package_files:
|
|
431
|
+
refs.append({"guid": g, "resolvedTo": resolved})
|
|
432
|
+
results.append({"path": rel, "kind": "scene" if suf == ".unity" else "prefab", "scriptsReferenced": refs})
|
|
433
|
+
return results
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# ---------------------------------------------------------------------------
|
|
437
|
+
# Utilities
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
def _rel(p: Path, root: Path) -> str:
|
|
441
|
+
try:
|
|
442
|
+
return str(p.resolve().relative_to(root.resolve())).replace(os.sep, "/")
|
|
443
|
+
except ValueError:
|
|
444
|
+
return str(p)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def iter_cs_files(root: Path) -> Iterable[Path]:
|
|
448
|
+
for p in sorted(root.rglob("*.cs")):
|
|
449
|
+
if p.is_file():
|
|
450
|
+
yield p
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ---------------------------------------------------------------------------
|
|
454
|
+
# Main
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
def main(argv: list[str]) -> int:
|
|
458
|
+
ap = argparse.ArgumentParser(description="Scan a Unity package and emit a JSON manifest.")
|
|
459
|
+
ap.add_argument("--package", required=True, help="Path to the package source directory.")
|
|
460
|
+
ap.add_argument("--samples", action="append", default=[], help="Path to a sample directory (repeatable).")
|
|
461
|
+
ap.add_argument("--repo-root", default=os.getcwd(), help="Repository root (paths in output are relative to this).")
|
|
462
|
+
args = ap.parse_args(argv)
|
|
463
|
+
|
|
464
|
+
repo_root = Path(args.repo_root).resolve()
|
|
465
|
+
package_dir = Path(args.package).resolve()
|
|
466
|
+
if not package_dir.is_dir():
|
|
467
|
+
print(f"error: package dir not found: {package_dir}", file=sys.stderr)
|
|
468
|
+
return 2
|
|
469
|
+
|
|
470
|
+
all_types: list[dict] = []
|
|
471
|
+
all_namespaces: set[str] = set()
|
|
472
|
+
package_files: set[str] = set()
|
|
473
|
+
|
|
474
|
+
for cs in iter_cs_files(package_dir):
|
|
475
|
+
rel = _rel(cs, repo_root)
|
|
476
|
+
package_files.add(rel)
|
|
477
|
+
result = parse_cs_file(cs, rel)
|
|
478
|
+
all_types.extend(result["types"])
|
|
479
|
+
all_namespaces.update(result["namespaces"])
|
|
480
|
+
|
|
481
|
+
asmdefs = scan_asmdefs(package_dir, repo_root)
|
|
482
|
+
guid_map = build_script_guid_map(repo_root)
|
|
483
|
+
|
|
484
|
+
package_type_names: set[str] = {t["name"] for t in all_types}
|
|
485
|
+
|
|
486
|
+
samples: list[dict] = []
|
|
487
|
+
for s in args.samples:
|
|
488
|
+
samples.extend(scan_sample_dir(Path(s).resolve(), repo_root, package_type_names, guid_map, package_files))
|
|
489
|
+
|
|
490
|
+
manifest = {
|
|
491
|
+
"packageDir": _rel(package_dir, repo_root),
|
|
492
|
+
"asmdefs": asmdefs,
|
|
493
|
+
"namespaces": sorted(all_namespaces),
|
|
494
|
+
"types": all_types,
|
|
495
|
+
"scriptGuidCount": len(guid_map),
|
|
496
|
+
"samples": samples,
|
|
497
|
+
}
|
|
498
|
+
json.dump(manifest, sys.stdout, indent=2)
|
|
499
|
+
sys.stdout.write("\n")
|
|
500
|
+
return 0
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
if __name__ == "__main__":
|
|
504
|
+
sys.exit(main(sys.argv[1:]))
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "com.elestrago.unity.package-tools",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"displayName": "Package Tool",
|
|
5
5
|
"description": "Tool for create unity packages",
|
|
6
6
|
"category": "unity",
|
|
7
7
|
"unity": "2021.3",
|
|
8
8
|
"homepage": "https://gitlab.com/elestrago-pkg/package-tool",
|
|
9
|
-
"documentationUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.0
|
|
10
|
-
"changelogUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.0
|
|
11
|
-
"licensesUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.0
|
|
9
|
+
"documentationUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.1.0/README.md",
|
|
10
|
+
"changelogUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.1.0/CHANGELOG.md",
|
|
11
|
+
"licensesUrl": "https://gitlab.com/elestrago-pkg/package-tool/-/blob/2.1.0/LICENSE",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"keywords": [
|
|
14
14
|
"unity",
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
"displayName": "Example Sample",
|
|
25
25
|
"description": "This is example for check test samples",
|
|
26
26
|
"path": "Samples~/ExampleSample"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"displayName": "Claude Skills",
|
|
30
|
+
"description": "This file contain claude skill for create package documentation",
|
|
31
|
+
"path": "Samples~/ClaudeSkills"
|
|
27
32
|
}
|
|
28
33
|
],
|
|
29
34
|
"author": {
|